diff --git a/README.md b/README.md
index eb6e93d2..8d4347fe 100644
--- a/README.md
+++ b/README.md
@@ -118,7 +118,10 @@ cd automaker
# 2. Install dependencies
npm install
-# 3. Run Automaker (pick your mode)
+# 3. Build local shared packages
+npm run build:packages
+
+# 4. Run Automaker (pick your mode)
npm run dev
# Then choose your run mode when prompted, or use specific commands below
```
diff --git a/apps/app/next-env.d.ts b/apps/app/next-env.d.ts
deleted file mode 100644
index 20e7bcfb..00000000
--- a/apps/app/next-env.d.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-///
-///
-import './.next/dev/types/routes.d.ts';
-
-// NOTE: This file should not be edited
-// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/apps/ui/src/main.ts b/apps/ui/src/main.ts
index d6d3d2a3..5c3fb52f 100644
--- a/apps/ui/src/main.ts
+++ b/apps/ui/src/main.ts
@@ -10,6 +10,7 @@ import { spawn, ChildProcess } from 'child_process';
import fs from 'fs';
import http, { Server } from 'http';
import { app, BrowserWindow, ipcMain, dialog, shell, screen } from 'electron';
+import { findNodeExecutable, buildEnhancedPath } from '@automaker/platform';
// Development environment
const isDev = !app.isPackaged;
@@ -274,12 +275,22 @@ async function startStaticServer(): Promise {
* Start the backend server
*/
async function startServer(): Promise {
- let command: string;
+ // Find Node.js executable (handles desktop launcher scenarios)
+ const nodeResult = findNodeExecutable({
+ skipSearch: isDev,
+ logger: (msg: string) => console.log(`[Electron] ${msg}`),
+ });
+ const command = nodeResult.nodePath;
+
+ // Validate that the found Node executable actually exists
+ if (command !== 'node' && !fs.existsSync(command)) {
+ throw new Error(`Node.js executable not found at: ${command} (source: ${nodeResult.source})`);
+ }
+
let args: string[];
let serverPath: string;
if (isDev) {
- command = 'node';
serverPath = path.join(__dirname, '../../server/src/index.ts');
const serverNodeModules = path.join(__dirname, '../../server/node_modules/tsx');
@@ -302,7 +313,6 @@ async function startServer(): Promise {
args = [tsxCliPath, 'watch', serverPath];
} else {
- command = 'node';
serverPath = path.join(process.resourcesPath, 'server', 'index.js');
args = [serverPath];
@@ -315,8 +325,15 @@ async function startServer(): Promise {
? path.join(process.resourcesPath, 'server', 'node_modules')
: path.join(__dirname, '../../server/node_modules');
+ // Build enhanced PATH that includes Node.js directory (cross-platform)
+ const enhancedPath = buildEnhancedPath(command, process.env.PATH || '');
+ if (enhancedPath !== process.env.PATH) {
+ console.log(`[Electron] Enhanced PATH with Node directory: ${path.dirname(command)}`);
+ }
+
const env = {
...process.env,
+ PATH: enhancedPath,
PORT: SERVER_PORT.toString(),
DATA_DIR: app.getPath('userData'),
NODE_PATH: serverNodeModules,
@@ -511,6 +528,16 @@ app.whenReady().then(async () => {
createWindow();
} catch (error) {
console.error('[Electron] Failed to start:', error);
+ const errorMessage = (error as Error).message;
+ const isNodeError = errorMessage.includes('Node.js');
+ dialog.showErrorBox(
+ 'Automaker Failed to Start',
+ `The application failed to start.\n\n${errorMessage}\n\n${
+ isNodeError
+ ? 'Please install Node.js from https://nodejs.org or via a package manager (Homebrew, nvm, fnm).'
+ : 'Please check the application logs for more details.'
+ }`
+ );
app.quit();
}
diff --git a/libs/platform/src/index.ts b/libs/platform/src/index.ts
index 0794e109..ae4cc0d8 100644
--- a/libs/platform/src/index.ts
+++ b/libs/platform/src/index.ts
@@ -44,3 +44,11 @@ export {
// Secure file system (validates paths before I/O operations)
export * as secureFs from './secure-fs.js';
+
+// Node.js executable finder (cross-platform)
+export {
+ findNodeExecutable,
+ buildEnhancedPath,
+ type NodeFinderResult,
+ type NodeFinderOptions,
+} from './node-finder.js';
diff --git a/libs/platform/src/node-finder.ts b/libs/platform/src/node-finder.ts
new file mode 100644
index 00000000..ed2cbb03
--- /dev/null
+++ b/libs/platform/src/node-finder.ts
@@ -0,0 +1,386 @@
+/**
+ * Cross-platform Node.js executable finder
+ *
+ * Handles finding Node.js when the app is launched from desktop environments
+ * (macOS Finder, Windows Explorer, Linux desktop) where PATH may be limited.
+ */
+
+import { execSync } from 'child_process';
+import fs from 'fs';
+import path from 'path';
+import os from 'os';
+
+/** Pattern to match version directories (e.g., "v18.17.0", "18.17.0", "v18") */
+const VERSION_DIR_PATTERN = /^v?\d+/;
+
+/** Pattern to identify pre-release versions (beta, rc, alpha, nightly, canary) */
+const PRE_RELEASE_PATTERN = /-(beta|rc|alpha|nightly|canary|dev|pre)/i;
+
+/** Result of finding Node.js executable */
+export interface NodeFinderResult {
+ /** Path to the Node.js executable */
+ nodePath: string;
+ /** How Node.js was found */
+ source:
+ | 'homebrew'
+ | 'system'
+ | 'nvm'
+ | 'fnm'
+ | 'nvm-windows'
+ | 'program-files'
+ | 'scoop'
+ | 'chocolatey'
+ | 'which'
+ | 'where'
+ | 'fallback';
+}
+
+/** Options for finding Node.js */
+export interface NodeFinderOptions {
+ /** Skip the search and return 'node' immediately (useful for dev mode) */
+ skipSearch?: boolean;
+ /** Custom logger function */
+ logger?: (message: string) => void;
+}
+
+/**
+ * Check if a file exists and is executable
+ * On Windows, only checks existence (X_OK is not meaningful)
+ */
+function isExecutable(filePath: string): boolean {
+ try {
+ if (process.platform === 'win32') {
+ // On Windows, fs.constants.X_OK is not meaningful - just check existence
+ fs.accessSync(filePath, fs.constants.F_OK);
+ } else {
+ // On Unix-like systems, check for execute permission
+ fs.accessSync(filePath, fs.constants.X_OK);
+ }
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+/**
+ * Find Node.js executable from version manager directories (NVM, fnm)
+ * Uses semantic version sorting to prefer the latest stable version
+ * Pre-release versions (beta, rc, alpha) are deprioritized but used as fallback
+ */
+function findNodeFromVersionManager(
+ basePath: string,
+ binSubpath: string = 'bin/node'
+): string | null {
+ if (!fs.existsSync(basePath)) return null;
+
+ try {
+ const allVersions = fs
+ .readdirSync(basePath)
+ .filter((v) => VERSION_DIR_PATTERN.test(v))
+ // Semantic version sort - newest first using localeCompare with numeric option
+ .sort((a, b) => b.localeCompare(a, undefined, { numeric: true, sensitivity: 'base' }));
+
+ // Separate stable and pre-release versions, preferring stable
+ const stableVersions = allVersions.filter((v) => !PRE_RELEASE_PATTERN.test(v));
+ const preReleaseVersions = allVersions.filter((v) => PRE_RELEASE_PATTERN.test(v));
+
+ // Try stable versions first, then fall back to pre-release
+ for (const version of [...stableVersions, ...preReleaseVersions]) {
+ const nodePath = path.join(basePath, version, binSubpath);
+ if (isExecutable(nodePath)) {
+ return nodePath;
+ }
+ }
+ } catch {
+ // Directory read failed, skip this location
+ }
+
+ return null;
+}
+
+/**
+ * Find Node.js on macOS
+ */
+function findNodeMacOS(homeDir: string): NodeFinderResult | null {
+ // Check Homebrew paths in order of preference
+ const homebrewPaths = [
+ // Apple Silicon
+ '/opt/homebrew/bin/node',
+ // Intel
+ '/usr/local/bin/node',
+ ];
+
+ for (const nodePath of homebrewPaths) {
+ if (isExecutable(nodePath)) {
+ return { nodePath, source: 'homebrew' };
+ }
+ }
+
+ // System Node
+ if (isExecutable('/usr/bin/node')) {
+ return { nodePath: '/usr/bin/node', source: 'system' };
+ }
+
+ // NVM installation
+ const nvmPath = path.join(homeDir, '.nvm', 'versions', 'node');
+ const nvmNode = findNodeFromVersionManager(nvmPath);
+ if (nvmNode) {
+ return { nodePath: nvmNode, source: 'nvm' };
+ }
+
+ // fnm installation (multiple possible locations)
+ const fnmPaths = [
+ path.join(homeDir, '.local', 'share', 'fnm', 'node-versions'),
+ path.join(homeDir, 'Library', 'Application Support', 'fnm', 'node-versions'),
+ ];
+
+ for (const fnmBasePath of fnmPaths) {
+ const fnmNode = findNodeFromVersionManager(fnmBasePath);
+ if (fnmNode) {
+ return { nodePath: fnmNode, source: 'fnm' };
+ }
+ }
+
+ return null;
+}
+
+/**
+ * Find Node.js on Linux
+ */
+function findNodeLinux(homeDir: string): NodeFinderResult | null {
+ // Common Linux paths
+ const systemPaths = [
+ '/usr/bin/node',
+ '/usr/local/bin/node',
+ // Snap installation
+ '/snap/bin/node',
+ ];
+
+ for (const nodePath of systemPaths) {
+ if (isExecutable(nodePath)) {
+ return { nodePath, source: 'system' };
+ }
+ }
+
+ // NVM installation
+ const nvmPath = path.join(homeDir, '.nvm', 'versions', 'node');
+ const nvmNode = findNodeFromVersionManager(nvmPath);
+ if (nvmNode) {
+ return { nodePath: nvmNode, source: 'nvm' };
+ }
+
+ // fnm installation
+ const fnmPaths = [
+ path.join(homeDir, '.local', 'share', 'fnm', 'node-versions'),
+ path.join(homeDir, '.fnm', 'node-versions'),
+ ];
+
+ for (const fnmBasePath of fnmPaths) {
+ const fnmNode = findNodeFromVersionManager(fnmBasePath);
+ if (fnmNode) {
+ return { nodePath: fnmNode, source: 'fnm' };
+ }
+ }
+
+ return null;
+}
+
+/**
+ * Find Node.js on Windows
+ */
+function findNodeWindows(homeDir: string): NodeFinderResult | null {
+ // Program Files paths
+ const programFilesPaths = [
+ path.join(process.env.PROGRAMFILES || 'C:\\Program Files', 'nodejs', 'node.exe'),
+ path.join(process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)', 'nodejs', 'node.exe'),
+ ];
+
+ for (const nodePath of programFilesPaths) {
+ if (isExecutable(nodePath)) {
+ return { nodePath, source: 'program-files' };
+ }
+ }
+
+ // NVM for Windows
+ const nvmWindowsPath = path.join(
+ process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming'),
+ 'nvm'
+ );
+ const nvmNode = findNodeFromVersionManager(nvmWindowsPath, 'node.exe');
+ if (nvmNode) {
+ return { nodePath: nvmNode, source: 'nvm-windows' };
+ }
+
+ // fnm on Windows (prioritize canonical installation path over shell shims)
+ const fnmWindowsPaths = [
+ path.join(homeDir, '.fnm', 'node-versions'),
+ path.join(
+ process.env.LOCALAPPDATA || path.join(homeDir, 'AppData', 'Local'),
+ 'fnm',
+ 'node-versions'
+ ),
+ ];
+
+ for (const fnmBasePath of fnmWindowsPaths) {
+ const fnmNode = findNodeFromVersionManager(fnmBasePath, 'node.exe');
+ if (fnmNode) {
+ return { nodePath: fnmNode, source: 'fnm' };
+ }
+ }
+
+ // Scoop installation
+ const scoopPath = path.join(homeDir, 'scoop', 'apps', 'nodejs', 'current', 'node.exe');
+ if (isExecutable(scoopPath)) {
+ return { nodePath: scoopPath, source: 'scoop' };
+ }
+
+ // Chocolatey installation
+ const chocoPath = path.join(
+ process.env.ChocolateyInstall || 'C:\\ProgramData\\chocolatey',
+ 'bin',
+ 'node.exe'
+ );
+ if (isExecutable(chocoPath)) {
+ return { nodePath: chocoPath, source: 'chocolatey' };
+ }
+
+ return null;
+}
+
+/**
+ * Try to find Node.js using shell commands (which/where)
+ */
+function findNodeViaShell(
+ platform: NodeJS.Platform,
+ logger: (message: string) => void = () => {}
+): NodeFinderResult | null {
+ try {
+ const command = platform === 'win32' ? 'where node' : 'which node';
+ const result = execSync(command, {
+ encoding: 'utf8',
+ stdio: ['pipe', 'pipe', 'pipe'],
+ }).trim();
+
+ // 'where' on Windows can return multiple lines, take the first
+ const nodePath = result.split(/\r?\n/)[0];
+
+ // Validate path: check for null bytes (security) and executable permission
+ if (nodePath && !nodePath.includes('\x00') && isExecutable(nodePath)) {
+ return {
+ nodePath,
+ source: platform === 'win32' ? 'where' : 'which',
+ };
+ }
+ } catch {
+ // Shell command failed (likely when launched from desktop without PATH)
+ logger('Shell command failed to find Node.js (expected when launched from desktop)');
+ }
+
+ return null;
+}
+
+/**
+ * Find Node.js executable - handles desktop launcher scenarios where PATH is limited
+ *
+ * @param options - Configuration options
+ * @returns Result with path and source information
+ *
+ * @example
+ * ```typescript
+ * import { findNodeExecutable } from '@automaker/platform';
+ *
+ * // In development, skip the search
+ * const result = findNodeExecutable({ skipSearch: isDev });
+ * console.log(`Using Node.js from ${result.source}: ${result.nodePath}`);
+ *
+ * // Spawn a process with the found Node.js
+ * spawn(result.nodePath, ['script.js']);
+ * ```
+ */
+export function findNodeExecutable(options: NodeFinderOptions = {}): NodeFinderResult {
+ const { skipSearch = false, logger = () => {} } = options;
+
+ // Skip search if requested (e.g., in development mode)
+ if (skipSearch) {
+ return { nodePath: 'node', source: 'fallback' };
+ }
+
+ const platform = process.platform;
+ const homeDir = os.homedir();
+
+ // Platform-specific search
+ let result: NodeFinderResult | null = null;
+
+ switch (platform) {
+ case 'darwin':
+ result = findNodeMacOS(homeDir);
+ break;
+ case 'linux':
+ result = findNodeLinux(homeDir);
+ break;
+ case 'win32':
+ result = findNodeWindows(homeDir);
+ break;
+ }
+
+ if (result) {
+ logger(`Found Node.js via ${result.source} at: ${result.nodePath}`);
+ return result;
+ }
+
+ // Fallback - try shell resolution (works when launched from terminal)
+ result = findNodeViaShell(platform, logger);
+ if (result) {
+ logger(`Found Node.js via ${result.source} at: ${result.nodePath}`);
+ return result;
+ }
+
+ // Ultimate fallback
+ logger('Could not find Node.js, falling back to "node"');
+ return { nodePath: 'node', source: 'fallback' };
+}
+
+/**
+ * Build an enhanced PATH that includes the Node.js directory
+ * Useful for ensuring child processes can find Node.js
+ *
+ * @param nodePath - Path to the Node.js executable
+ * @param currentPath - Current PATH environment variable
+ * @returns Enhanced PATH with Node.js directory prepended if not already present
+ *
+ * @example
+ * ```typescript
+ * import { findNodeExecutable, buildEnhancedPath } from '@automaker/platform';
+ *
+ * const { nodePath } = findNodeExecutable();
+ * const enhancedPath = buildEnhancedPath(nodePath, process.env.PATH);
+ *
+ * spawn(nodePath, ['script.js'], {
+ * env: { ...process.env, PATH: enhancedPath }
+ * });
+ * ```
+ */
+export function buildEnhancedPath(nodePath: string, currentPath: string = ''): string {
+ // If using fallback 'node', don't modify PATH
+ if (nodePath === 'node') {
+ return currentPath;
+ }
+
+ const nodeDir = path.dirname(nodePath);
+
+ // Don't add if already present or if it's just '.'
+ // Use path segment matching to avoid false positives (e.g., /opt/node vs /opt/node-v18)
+ // Normalize paths for comparison to handle mixed separators on Windows
+ const normalizedNodeDir = path.normalize(nodeDir);
+ const pathSegments = currentPath.split(path.delimiter).map((s) => path.normalize(s));
+ if (normalizedNodeDir === '.' || pathSegments.includes(normalizedNodeDir)) {
+ return currentPath;
+ }
+
+ // Use platform-appropriate path separator
+ // Handle empty currentPath without adding trailing delimiter
+ if (!currentPath) {
+ return nodeDir;
+ }
+ return `${nodeDir}${path.delimiter}${currentPath}`;
+}
diff --git a/libs/platform/tests/node-finder.test.ts b/libs/platform/tests/node-finder.test.ts
new file mode 100644
index 00000000..6956884b
--- /dev/null
+++ b/libs/platform/tests/node-finder.test.ts
@@ -0,0 +1,197 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { findNodeExecutable, buildEnhancedPath } from '../src/node-finder.js';
+import path from 'path';
+import fs from 'fs';
+
+describe('node-finder', () => {
+ describe('version sorting and pre-release filtering', () => {
+ // Test the PRE_RELEASE_PATTERN logic indirectly
+ const PRE_RELEASE_PATTERN = /-(beta|rc|alpha|nightly|canary|dev|pre)/i;
+
+ it('should identify pre-release versions correctly', () => {
+ const preReleaseVersions = [
+ 'v20.0.0-beta',
+ 'v18.17.0-rc1',
+ 'v19.0.0-alpha',
+ 'v21.0.0-nightly',
+ 'v20.0.0-canary',
+ 'v18.0.0-dev',
+ 'v17.0.0-pre',
+ ];
+
+ for (const version of preReleaseVersions) {
+ expect(PRE_RELEASE_PATTERN.test(version)).toBe(true);
+ }
+ });
+
+ it('should not match stable versions as pre-release', () => {
+ const stableVersions = ['v18.17.0', 'v20.10.0', 'v16.20.2', '18.17.0', 'v21.0.0'];
+
+ for (const version of stableVersions) {
+ expect(PRE_RELEASE_PATTERN.test(version)).toBe(false);
+ }
+ });
+
+ it('should sort versions with numeric comparison', () => {
+ const versions = ['v18.9.0', 'v18.17.0', 'v20.0.0', 'v8.0.0'];
+ const sorted = [...versions].sort((a, b) =>
+ b.localeCompare(a, undefined, { numeric: true, sensitivity: 'base' })
+ );
+
+ expect(sorted).toEqual(['v20.0.0', 'v18.17.0', 'v18.9.0', 'v8.0.0']);
+ });
+
+ it('should prefer stable over pre-release when filtering', () => {
+ const allVersions = ['v20.0.0-beta', 'v19.9.9', 'v18.17.0', 'v21.0.0-rc1'];
+
+ const stableVersions = allVersions.filter((v) => !PRE_RELEASE_PATTERN.test(v));
+ const preReleaseVersions = allVersions.filter((v) => PRE_RELEASE_PATTERN.test(v));
+ const prioritized = [...stableVersions, ...preReleaseVersions];
+
+ // Stable versions should come first
+ expect(prioritized[0]).toBe('v19.9.9');
+ expect(prioritized[1]).toBe('v18.17.0');
+ // Pre-release versions should come after
+ expect(prioritized[2]).toBe('v20.0.0-beta');
+ expect(prioritized[3]).toBe('v21.0.0-rc1');
+ });
+ });
+
+ describe('findNodeExecutable', () => {
+ it("should return 'node' with fallback source when skipSearch is true", () => {
+ const result = findNodeExecutable({ skipSearch: true });
+
+ expect(result.nodePath).toBe('node');
+ expect(result.source).toBe('fallback');
+ });
+
+ it('should call logger when node is found', () => {
+ const logger = vi.fn();
+ findNodeExecutable({ logger });
+
+ // Logger should be called at least once (either found or fallback message)
+ expect(logger).toHaveBeenCalled();
+ });
+
+ it('should return a valid NodeFinderResult structure', () => {
+ const result = findNodeExecutable();
+
+ expect(result).toHaveProperty('nodePath');
+ expect(result).toHaveProperty('source');
+ expect(typeof result.nodePath).toBe('string');
+ expect(result.nodePath.length).toBeGreaterThan(0);
+ });
+
+ it('should find node on the current system', () => {
+ // This test verifies that node can be found on the test machine
+ const result = findNodeExecutable();
+
+ // Should find node since we're running in Node.js
+ expect(result.nodePath).toBeDefined();
+
+ // Source should be one of the valid sources
+ const validSources = [
+ 'homebrew',
+ 'system',
+ 'nvm',
+ 'fnm',
+ 'nvm-windows',
+ 'program-files',
+ 'scoop',
+ 'chocolatey',
+ 'which',
+ 'where',
+ 'fallback',
+ ];
+ expect(validSources).toContain(result.source);
+ });
+
+ it('should find an executable node binary', () => {
+ const result = findNodeExecutable();
+
+ // Skip this test if fallback is used (node not found via path search)
+ if (result.source === 'fallback') {
+ expect(result.nodePath).toBe('node');
+ return;
+ }
+
+ // Verify the found path is actually executable
+ if (process.platform === 'win32') {
+ // On Windows, just check file exists (X_OK is not meaningful)
+ expect(() => fs.accessSync(result.nodePath, fs.constants.F_OK)).not.toThrow();
+ } else {
+ // On Unix-like systems, verify execute permission
+ expect(() => fs.accessSync(result.nodePath, fs.constants.X_OK)).not.toThrow();
+ }
+ });
+ });
+
+ describe('buildEnhancedPath', () => {
+ const delimiter = path.delimiter;
+
+ it("should return current path unchanged when nodePath is 'node'", () => {
+ const currentPath = '/usr/bin:/usr/local/bin';
+ const result = buildEnhancedPath('node', currentPath);
+
+ expect(result).toBe(currentPath);
+ });
+
+ it("should return empty string when nodePath is 'node' and currentPath is empty", () => {
+ const result = buildEnhancedPath('node', '');
+
+ expect(result).toBe('');
+ });
+
+ it('should prepend node directory to path', () => {
+ const nodePath = '/opt/homebrew/bin/node';
+ const currentPath = '/usr/bin:/usr/local/bin';
+
+ const result = buildEnhancedPath(nodePath, currentPath);
+
+ expect(result).toBe(`/opt/homebrew/bin${delimiter}${currentPath}`);
+ });
+
+ it('should not duplicate node directory if already in path', () => {
+ const nodePath = '/usr/local/bin/node';
+ const currentPath = '/usr/local/bin:/usr/bin';
+
+ const result = buildEnhancedPath(nodePath, currentPath);
+
+ expect(result).toBe(currentPath);
+ });
+
+ it('should handle empty currentPath without trailing delimiter', () => {
+ const nodePath = '/opt/homebrew/bin/node';
+
+ const result = buildEnhancedPath(nodePath, '');
+
+ expect(result).toBe('/opt/homebrew/bin');
+ });
+
+ it('should handle Windows-style paths', () => {
+ // On Windows, path.dirname recognizes backslash paths
+ // On other platforms, backslash is not a path separator
+ const nodePath = 'C:\\Program Files\\nodejs\\node.exe';
+ const currentPath = 'C:\\Windows\\System32';
+
+ const result = buildEnhancedPath(nodePath, currentPath);
+
+ if (process.platform === 'win32') {
+ // On Windows, should prepend the node directory
+ expect(result).toBe(`C:\\Program Files\\nodejs${delimiter}${currentPath}`);
+ } else {
+ // On non-Windows, backslash paths are treated as relative paths
+ // path.dirname returns '.' so the function returns currentPath unchanged
+ expect(result).toBe(currentPath);
+ }
+ });
+
+ it('should use default empty string for currentPath', () => {
+ const nodePath = '/usr/local/bin/node';
+
+ const result = buildEnhancedPath(nodePath);
+
+ expect(result).toBe('/usr/local/bin');
+ });
+ });
+});