feat: add Node.js version management and improve error handling

- Introduced a .nvmrc file to specify the Node.js version (22) for the project, ensuring consistent development environments.
- Enhanced error handling in the startServer function to provide clearer messages when the Node.js executable cannot be found, improving debugging experience.
- Updated package.json files across various modules to enforce Node.js version compatibility and ensure consistent dependency versions.

These changes aim to streamline development processes and enhance the application's reliability by enforcing version control and improving error reporting.
This commit is contained in:
Test User
2025-12-31 18:42:33 -05:00
parent 2b89b0606c
commit 59bbbd43c5
21 changed files with 387 additions and 269 deletions

2
.nvmrc Normal file
View File

@@ -0,0 +1,2 @@
22

View File

@@ -5,6 +5,9 @@
"author": "AutoMaker Team", "author": "AutoMaker Team",
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"private": true, "private": true,
"engines": {
"node": ">=22.0.0 <23.0.0"
},
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"scripts": { "scripts": {
@@ -21,35 +24,35 @@
"test:unit": "vitest run tests/unit" "test:unit": "vitest run tests/unit"
}, },
"dependencies": { "dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.1.72", "@anthropic-ai/claude-agent-sdk": "0.1.72",
"@automaker/dependency-resolver": "^1.0.0", "@automaker/dependency-resolver": "1.0.0",
"@automaker/git-utils": "^1.0.0", "@automaker/git-utils": "1.0.0",
"@automaker/model-resolver": "^1.0.0", "@automaker/model-resolver": "1.0.0",
"@automaker/platform": "^1.0.0", "@automaker/platform": "1.0.0",
"@automaker/prompts": "^1.0.0", "@automaker/prompts": "1.0.0",
"@automaker/types": "^1.0.0", "@automaker/types": "1.0.0",
"@automaker/utils": "^1.0.0", "@automaker/utils": "1.0.0",
"@modelcontextprotocol/sdk": "^1.25.1", "@modelcontextprotocol/sdk": "1.25.1",
"cookie-parser": "^1.4.7", "cookie-parser": "1.4.7",
"cors": "^2.8.5", "cors": "2.8.5",
"dotenv": "^17.2.3", "dotenv": "17.2.3",
"express": "^5.2.1", "express": "5.2.1",
"morgan": "^1.10.1", "morgan": "1.10.1",
"node-pty": "1.1.0-beta41", "node-pty": "1.1.0-beta41",
"ws": "^8.18.3" "ws": "8.18.3"
}, },
"devDependencies": { "devDependencies": {
"@types/cookie": "^0.6.0", "@types/cookie": "0.6.0",
"@types/cookie-parser": "^1.4.10", "@types/cookie-parser": "1.4.10",
"@types/cors": "^2.8.19", "@types/cors": "2.8.19",
"@types/express": "^5.0.6", "@types/express": "5.0.6",
"@types/morgan": "^1.9.10", "@types/morgan": "1.9.10",
"@types/node": "^22", "@types/node": "22.19.3",
"@types/ws": "^8.18.1", "@types/ws": "8.18.1",
"@vitest/coverage-v8": "^4.0.16", "@vitest/coverage-v8": "4.0.16",
"@vitest/ui": "^4.0.16", "@vitest/ui": "4.0.16",
"tsx": "^4.21.0", "tsx": "4.21.0",
"typescript": "^5", "typescript": "5.9.3",
"vitest": "^4.0.16" "vitest": "4.0.16"
} }
} }

View File

@@ -76,7 +76,10 @@ async function saveSessions(): Promise<void> {
try { try {
await secureFs.mkdir(path.dirname(SESSIONS_FILE), { recursive: true }); await secureFs.mkdir(path.dirname(SESSIONS_FILE), { recursive: true });
const sessions = Array.from(validSessions.entries()); const sessions = Array.from(validSessions.entries());
await secureFs.writeFile(SESSIONS_FILE, JSON.stringify(sessions), 'utf-8'); await secureFs.writeFile(SESSIONS_FILE, JSON.stringify(sessions), {
encoding: 'utf-8',
mode: 0o600,
});
} catch (error) { } catch (error) {
console.error('[Auth] Failed to save sessions:', error); console.error('[Auth] Failed to save sessions:', error);
} }
@@ -113,7 +116,7 @@ function ensureApiKey(): string {
const newKey = crypto.randomUUID(); const newKey = crypto.randomUUID();
try { try {
secureFs.mkdirSync(path.dirname(API_KEY_FILE), { recursive: true }); secureFs.mkdirSync(path.dirname(API_KEY_FILE), { recursive: true });
secureFs.writeFileSync(API_KEY_FILE, newKey, { encoding: 'utf-8' }); secureFs.writeFileSync(API_KEY_FILE, newKey, { encoding: 'utf-8', mode: 0o600 });
console.log('[Auth] Generated new API key'); console.log('[Auth] Generated new API key');
} catch (error) { } catch (error) {
console.error('[Auth] Failed to save API key:', error); console.error('[Auth] Failed to save API key:', error);

View File

@@ -15,6 +15,24 @@ import type {
ModelDefinition, ModelDefinition,
} from './types.js'; } from './types.js';
// Automaker-specific environment variables that should not pollute agent processes
// These are internal to Automaker and would interfere with user projects
// (e.g., PORT=3008 would cause Next.js/Vite to use the wrong port)
const AUTOMAKER_ENV_VARS = ['PORT', 'DATA_DIR', 'AUTOMAKER_API_KEY', 'NODE_PATH'];
/**
* Build a clean environment for the SDK, excluding Automaker-specific variables
*/
function buildCleanEnv(): Record<string, string | undefined> {
const cleanEnv: Record<string, string | undefined> = {};
for (const [key, value] of Object.entries(process.env)) {
if (!AUTOMAKER_ENV_VARS.includes(key)) {
cleanEnv[key] = value;
}
}
return cleanEnv;
}
export class ClaudeProvider extends BaseProvider { export class ClaudeProvider extends BaseProvider {
getName(): string { getName(): string {
return 'claude'; return 'claude';
@@ -57,6 +75,9 @@ export class ClaudeProvider extends BaseProvider {
systemPrompt, systemPrompt,
maxTurns, maxTurns,
cwd, cwd,
// Pass clean environment to SDK, excluding Automaker-specific variables
// This prevents PORT, DATA_DIR, etc. from polluting agent-spawned processes
env: buildCleanEnv(),
// Only restrict tools if explicitly set OR (no MCP / unrestricted disabled) // Only restrict tools if explicitly set OR (no MCP / unrestricted disabled)
...(allowedTools && shouldRestrictTools && { allowedTools }), ...(allowedTools && shouldRestrictTools && { allowedTools }),
...(!allowedTools && shouldRestrictTools && { allowedTools: defaultTools }), ...(!allowedTools && shouldRestrictTools && { allowedTools: defaultTools }),

View File

@@ -257,8 +257,18 @@ export class TerminalService extends EventEmitter {
// Build environment with some useful defaults // Build environment with some useful defaults
// These settings ensure consistent terminal behavior across platforms // These settings ensure consistent terminal behavior across platforms
// First, create a clean copy of process.env excluding Automaker-specific variables
// that could pollute user shells (e.g., PORT would affect Next.js/other dev servers)
const automakerEnvVars = ['PORT', 'DATA_DIR', 'AUTOMAKER_API_KEY', 'NODE_PATH'];
const cleanEnv: Record<string, string> = {};
for (const [key, value] of Object.entries(process.env)) {
if (value !== undefined && !automakerEnvVars.includes(key)) {
cleanEnv[key] = value;
}
}
const env: Record<string, string> = { const env: Record<string, string> = {
...process.env, ...cleanEnv,
TERM: 'xterm-256color', TERM: 'xterm-256color',
COLORTERM: 'truecolor', COLORTERM: 'truecolor',
TERM_PROGRAM: 'automaker-terminal', TERM_PROGRAM: 'automaker-terminal',

View File

@@ -2,11 +2,22 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { TerminalService, getTerminalService } from '@/services/terminal-service.js'; import { TerminalService, getTerminalService } from '@/services/terminal-service.js';
import * as pty from 'node-pty'; import * as pty from 'node-pty';
import * as os from 'os'; import * as os from 'os';
import * as fs from 'fs'; import * as platform from '@automaker/platform';
import * as secureFs from '@/lib/secure-fs.js';
vi.mock('node-pty'); vi.mock('node-pty');
vi.mock('fs');
vi.mock('os'); vi.mock('os');
vi.mock('@automaker/platform', async () => {
const actual = await vi.importActual('@automaker/platform');
return {
...actual,
systemPathExists: vi.fn(),
systemPathReadFileSync: vi.fn(),
getWslVersionPath: vi.fn(),
isAllowedSystemPath: vi.fn(() => true), // Allow all paths in tests
};
});
vi.mock('@/lib/secure-fs.js');
describe('terminal-service.ts', () => { describe('terminal-service.ts', () => {
let service: TerminalService; let service: TerminalService;
@@ -29,6 +40,12 @@ describe('terminal-service.ts', () => {
vi.mocked(os.homedir).mockReturnValue('/home/user'); vi.mocked(os.homedir).mockReturnValue('/home/user');
vi.mocked(os.platform).mockReturnValue('linux'); vi.mocked(os.platform).mockReturnValue('linux');
vi.mocked(os.arch).mockReturnValue('x64'); vi.mocked(os.arch).mockReturnValue('x64');
// Default mocks for system paths and secureFs
vi.mocked(platform.systemPathExists).mockReturnValue(true);
vi.mocked(platform.systemPathReadFileSync).mockReturnValue('');
vi.mocked(platform.getWslVersionPath).mockReturnValue('/proc/version');
vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => true } as any);
}); });
afterEach(() => { afterEach(() => {
@@ -38,7 +55,7 @@ describe('terminal-service.ts', () => {
describe('detectShell', () => { describe('detectShell', () => {
it('should detect PowerShell Core on Windows when available', () => { it('should detect PowerShell Core on Windows when available', () => {
vi.mocked(os.platform).mockReturnValue('win32'); vi.mocked(os.platform).mockReturnValue('win32');
vi.mocked(fs.existsSync).mockImplementation((path: any) => { vi.mocked(platform.systemPathExists).mockImplementation((path: string) => {
return path === 'C:\\Program Files\\PowerShell\\7\\pwsh.exe'; return path === 'C:\\Program Files\\PowerShell\\7\\pwsh.exe';
}); });
@@ -50,7 +67,7 @@ describe('terminal-service.ts', () => {
it('should fall back to PowerShell on Windows if Core not available', () => { it('should fall back to PowerShell on Windows if Core not available', () => {
vi.mocked(os.platform).mockReturnValue('win32'); vi.mocked(os.platform).mockReturnValue('win32');
vi.mocked(fs.existsSync).mockImplementation((path: any) => { vi.mocked(platform.systemPathExists).mockImplementation((path: string) => {
return path === 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'; return path === 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
}); });
@@ -62,7 +79,7 @@ describe('terminal-service.ts', () => {
it('should fall back to cmd.exe on Windows if no PowerShell', () => { it('should fall back to cmd.exe on Windows if no PowerShell', () => {
vi.mocked(os.platform).mockReturnValue('win32'); vi.mocked(os.platform).mockReturnValue('win32');
vi.mocked(fs.existsSync).mockReturnValue(false); vi.mocked(platform.systemPathExists).mockReturnValue(false);
const result = service.detectShell(); const result = service.detectShell();
@@ -73,7 +90,7 @@ describe('terminal-service.ts', () => {
it('should detect user shell on macOS', () => { it('should detect user shell on macOS', () => {
vi.mocked(os.platform).mockReturnValue('darwin'); vi.mocked(os.platform).mockReturnValue('darwin');
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/zsh' }); vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/zsh' });
vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(platform.systemPathExists).mockReturnValue(true);
const result = service.detectShell(); const result = service.detectShell();
@@ -84,7 +101,7 @@ describe('terminal-service.ts', () => {
it('should fall back to zsh on macOS if user shell not available', () => { it('should fall back to zsh on macOS if user shell not available', () => {
vi.mocked(os.platform).mockReturnValue('darwin'); vi.mocked(os.platform).mockReturnValue('darwin');
vi.spyOn(process, 'env', 'get').mockReturnValue({}); vi.spyOn(process, 'env', 'get').mockReturnValue({});
vi.mocked(fs.existsSync).mockImplementation((path: any) => { vi.mocked(platform.systemPathExists).mockImplementation((path: string) => {
return path === '/bin/zsh'; return path === '/bin/zsh';
}); });
@@ -97,7 +114,7 @@ describe('terminal-service.ts', () => {
it('should fall back to bash on macOS if zsh not available', () => { it('should fall back to bash on macOS if zsh not available', () => {
vi.mocked(os.platform).mockReturnValue('darwin'); vi.mocked(os.platform).mockReturnValue('darwin');
vi.spyOn(process, 'env', 'get').mockReturnValue({}); vi.spyOn(process, 'env', 'get').mockReturnValue({});
vi.mocked(fs.existsSync).mockReturnValue(false); vi.mocked(platform.systemPathExists).mockReturnValue(false);
const result = service.detectShell(); const result = service.detectShell();
@@ -108,7 +125,7 @@ describe('terminal-service.ts', () => {
it('should detect user shell on Linux', () => { it('should detect user shell on Linux', () => {
vi.mocked(os.platform).mockReturnValue('linux'); vi.mocked(os.platform).mockReturnValue('linux');
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' }); vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(platform.systemPathExists).mockReturnValue(true);
const result = service.detectShell(); const result = service.detectShell();
@@ -119,7 +136,7 @@ describe('terminal-service.ts', () => {
it('should fall back to bash on Linux if user shell not available', () => { it('should fall back to bash on Linux if user shell not available', () => {
vi.mocked(os.platform).mockReturnValue('linux'); vi.mocked(os.platform).mockReturnValue('linux');
vi.spyOn(process, 'env', 'get').mockReturnValue({}); vi.spyOn(process, 'env', 'get').mockReturnValue({});
vi.mocked(fs.existsSync).mockImplementation((path: any) => { vi.mocked(platform.systemPathExists).mockImplementation((path: string) => {
return path === '/bin/bash'; return path === '/bin/bash';
}); });
@@ -132,7 +149,7 @@ describe('terminal-service.ts', () => {
it('should fall back to sh on Linux if bash not available', () => { it('should fall back to sh on Linux if bash not available', () => {
vi.mocked(os.platform).mockReturnValue('linux'); vi.mocked(os.platform).mockReturnValue('linux');
vi.spyOn(process, 'env', 'get').mockReturnValue({}); vi.spyOn(process, 'env', 'get').mockReturnValue({});
vi.mocked(fs.existsSync).mockReturnValue(false); vi.mocked(platform.systemPathExists).mockReturnValue(false);
const result = service.detectShell(); const result = service.detectShell();
@@ -143,8 +160,10 @@ describe('terminal-service.ts', () => {
it('should detect WSL and use appropriate shell', () => { it('should detect WSL and use appropriate shell', () => {
vi.mocked(os.platform).mockReturnValue('linux'); vi.mocked(os.platform).mockReturnValue('linux');
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' }); vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(platform.systemPathExists).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue('Linux version 5.10.0-microsoft-standard-WSL2'); vi.mocked(platform.systemPathReadFileSync).mockReturnValue(
'Linux version 5.10.0-microsoft-standard-WSL2'
);
const result = service.detectShell(); const result = service.detectShell();
@@ -155,43 +174,45 @@ describe('terminal-service.ts', () => {
describe('isWSL', () => { describe('isWSL', () => {
it('should return true if /proc/version contains microsoft', () => { it('should return true if /proc/version contains microsoft', () => {
vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(platform.systemPathExists).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue('Linux version 5.10.0-microsoft-standard-WSL2'); vi.mocked(platform.systemPathReadFileSync).mockReturnValue(
'Linux version 5.10.0-microsoft-standard-WSL2'
);
expect(service.isWSL()).toBe(true); expect(service.isWSL()).toBe(true);
}); });
it('should return true if /proc/version contains wsl', () => { it('should return true if /proc/version contains wsl', () => {
vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(platform.systemPathExists).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue('Linux version 5.10.0-wsl2'); vi.mocked(platform.systemPathReadFileSync).mockReturnValue('Linux version 5.10.0-wsl2');
expect(service.isWSL()).toBe(true); expect(service.isWSL()).toBe(true);
}); });
it('should return true if WSL_DISTRO_NAME is set', () => { it('should return true if WSL_DISTRO_NAME is set', () => {
vi.mocked(fs.existsSync).mockReturnValue(false); vi.mocked(platform.systemPathExists).mockReturnValue(false);
vi.spyOn(process, 'env', 'get').mockReturnValue({ WSL_DISTRO_NAME: 'Ubuntu' }); vi.spyOn(process, 'env', 'get').mockReturnValue({ WSL_DISTRO_NAME: 'Ubuntu' });
expect(service.isWSL()).toBe(true); expect(service.isWSL()).toBe(true);
}); });
it('should return true if WSLENV is set', () => { it('should return true if WSLENV is set', () => {
vi.mocked(fs.existsSync).mockReturnValue(false); vi.mocked(platform.systemPathExists).mockReturnValue(false);
vi.spyOn(process, 'env', 'get').mockReturnValue({ WSLENV: 'PATH/l' }); vi.spyOn(process, 'env', 'get').mockReturnValue({ WSLENV: 'PATH/l' });
expect(service.isWSL()).toBe(true); expect(service.isWSL()).toBe(true);
}); });
it('should return false if not in WSL', () => { it('should return false if not in WSL', () => {
vi.mocked(fs.existsSync).mockReturnValue(false); vi.mocked(platform.systemPathExists).mockReturnValue(false);
vi.spyOn(process, 'env', 'get').mockReturnValue({}); vi.spyOn(process, 'env', 'get').mockReturnValue({});
expect(service.isWSL()).toBe(false); expect(service.isWSL()).toBe(false);
}); });
it('should return false if error reading /proc/version', () => { it('should return false if error reading /proc/version', () => {
vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(platform.systemPathExists).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockImplementation(() => { vi.mocked(platform.systemPathReadFileSync).mockImplementation(() => {
throw new Error('Permission denied'); throw new Error('Permission denied');
}); });
@@ -203,7 +224,7 @@ describe('terminal-service.ts', () => {
it('should return platform information', () => { it('should return platform information', () => {
vi.mocked(os.platform).mockReturnValue('linux'); vi.mocked(os.platform).mockReturnValue('linux');
vi.mocked(os.arch).mockReturnValue('x64'); vi.mocked(os.arch).mockReturnValue('x64');
vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(platform.systemPathExists).mockReturnValue(true);
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' }); vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
const info = service.getPlatformInfo(); const info = service.getPlatformInfo();
@@ -216,20 +237,21 @@ describe('terminal-service.ts', () => {
}); });
describe('createSession', () => { describe('createSession', () => {
it('should create a new terminal session', () => { it('should create a new terminal session', async () => {
vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(platform.systemPathExists).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any); vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => true } as any);
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' }); vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
const session = service.createSession({ const session = await service.createSession({
cwd: '/test/dir', cwd: '/test/dir',
cols: 100, cols: 100,
rows: 30, rows: 30,
}); });
expect(session.id).toMatch(/^term-/); expect(session).not.toBeNull();
expect(session.cwd).toBe('/test/dir'); expect(session!.id).toMatch(/^term-/);
expect(session.shell).toBe('/bin/bash'); expect(session!.cwd).toBe('/test/dir');
expect(session!.shell).toBe('/bin/bash');
expect(pty.spawn).toHaveBeenCalledWith( expect(pty.spawn).toHaveBeenCalledWith(
'/bin/bash', '/bin/bash',
['--login'], ['--login'],
@@ -241,12 +263,12 @@ describe('terminal-service.ts', () => {
); );
}); });
it('should use default cols and rows if not provided', () => { it('should use default cols and rows if not provided', async () => {
vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(platform.systemPathExists).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any); vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => true } as any);
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' }); vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
service.createSession(); await service.createSession();
expect(pty.spawn).toHaveBeenCalledWith( expect(pty.spawn).toHaveBeenCalledWith(
expect.any(String), expect.any(String),
@@ -258,66 +280,68 @@ describe('terminal-service.ts', () => {
); );
}); });
it('should fall back to home directory if cwd does not exist', () => { it('should fall back to home directory if cwd does not exist', async () => {
vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(platform.systemPathExists).mockReturnValue(true);
vi.mocked(fs.statSync).mockImplementation(() => { vi.mocked(secureFs.stat).mockRejectedValue(new Error('ENOENT'));
throw new Error('ENOENT');
});
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' }); vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
const session = service.createSession({ const session = await service.createSession({
cwd: '/nonexistent', cwd: '/nonexistent',
}); });
expect(session.cwd).toBe('/home/user'); expect(session).not.toBeNull();
expect(session!.cwd).toBe('/home/user');
}); });
it('should fall back to home directory if cwd is not a directory', () => { it('should fall back to home directory if cwd is not a directory', async () => {
vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(platform.systemPathExists).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => false } as any); vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => false } as any);
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' }); vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
const session = service.createSession({ const session = await service.createSession({
cwd: '/file.txt', cwd: '/file.txt',
}); });
expect(session.cwd).toBe('/home/user'); expect(session).not.toBeNull();
expect(session!.cwd).toBe('/home/user');
}); });
it('should fix double slashes in path', () => { it('should fix double slashes in path', async () => {
vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(platform.systemPathExists).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any); vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => true } as any);
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' }); vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
const session = service.createSession({ const session = await service.createSession({
cwd: '//test/dir', cwd: '//test/dir',
}); });
expect(session.cwd).toBe('/test/dir'); expect(session).not.toBeNull();
expect(session!.cwd).toBe('/test/dir');
}); });
it('should preserve WSL UNC paths', () => { it('should preserve WSL UNC paths', async () => {
vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(platform.systemPathExists).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any); vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => true } as any);
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' }); vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
const session = service.createSession({ const session = await service.createSession({
cwd: '//wsl$/Ubuntu/home', cwd: '//wsl$/Ubuntu/home',
}); });
expect(session.cwd).toBe('//wsl$/Ubuntu/home'); expect(session).not.toBeNull();
expect(session!.cwd).toBe('//wsl$/Ubuntu/home');
}); });
it('should handle data events from PTY', () => { it('should handle data events from PTY', async () => {
vi.useFakeTimers(); vi.useFakeTimers();
vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(platform.systemPathExists).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any); vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => true } as any);
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' }); vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
const dataCallback = vi.fn(); const dataCallback = vi.fn();
service.onData(dataCallback); service.onData(dataCallback);
service.createSession(); await service.createSession();
// Simulate data event // Simulate data event
const onDataHandler = mockPtyProcess.onData.mock.calls[0][0]; const onDataHandler = mockPtyProcess.onData.mock.calls[0][0];
@@ -331,33 +355,34 @@ describe('terminal-service.ts', () => {
vi.useRealTimers(); vi.useRealTimers();
}); });
it('should handle exit events from PTY', () => { it('should handle exit events from PTY', async () => {
vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(platform.systemPathExists).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any); vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => true } as any);
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' }); vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
const exitCallback = vi.fn(); const exitCallback = vi.fn();
service.onExit(exitCallback); service.onExit(exitCallback);
const session = service.createSession(); const session = await service.createSession();
// Simulate exit event // Simulate exit event
const onExitHandler = mockPtyProcess.onExit.mock.calls[0][0]; const onExitHandler = mockPtyProcess.onExit.mock.calls[0][0];
onExitHandler({ exitCode: 0 }); onExitHandler({ exitCode: 0 });
expect(exitCallback).toHaveBeenCalledWith(session.id, 0); expect(session).not.toBeNull();
expect(service.getSession(session.id)).toBeUndefined(); expect(exitCallback).toHaveBeenCalledWith(session!.id, 0);
expect(service.getSession(session!.id)).toBeUndefined();
}); });
}); });
describe('write', () => { describe('write', () => {
it('should write data to existing session', () => { it('should write data to existing session', async () => {
vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(platform.systemPathExists).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any); vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => true } as any);
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' }); vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
const session = service.createSession(); const session = await service.createSession();
const result = service.write(session.id, 'ls\n'); const result = service.write(session!.id, 'ls\n');
expect(result).toBe(true); expect(result).toBe(true);
expect(mockPtyProcess.write).toHaveBeenCalledWith('ls\n'); expect(mockPtyProcess.write).toHaveBeenCalledWith('ls\n');
@@ -372,13 +397,13 @@ describe('terminal-service.ts', () => {
}); });
describe('resize', () => { describe('resize', () => {
it('should resize existing session', () => { it('should resize existing session', async () => {
vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(platform.systemPathExists).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any); vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => true } as any);
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' }); vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
const session = service.createSession(); const session = await service.createSession();
const result = service.resize(session.id, 120, 40); const result = service.resize(session!.id, 120, 40);
expect(result).toBe(true); expect(result).toBe(true);
expect(mockPtyProcess.resize).toHaveBeenCalledWith(120, 40); expect(mockPtyProcess.resize).toHaveBeenCalledWith(120, 40);
@@ -391,30 +416,30 @@ describe('terminal-service.ts', () => {
expect(mockPtyProcess.resize).not.toHaveBeenCalled(); expect(mockPtyProcess.resize).not.toHaveBeenCalled();
}); });
it('should handle resize errors', () => { it('should handle resize errors', async () => {
vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(platform.systemPathExists).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any); vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => true } as any);
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' }); vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
mockPtyProcess.resize.mockImplementation(() => { mockPtyProcess.resize.mockImplementation(() => {
throw new Error('Resize failed'); throw new Error('Resize failed');
}); });
const session = service.createSession(); const session = await service.createSession();
const result = service.resize(session.id, 120, 40); const result = service.resize(session!.id, 120, 40);
expect(result).toBe(false); expect(result).toBe(false);
}); });
}); });
describe('killSession', () => { describe('killSession', () => {
it('should kill existing session', () => { it('should kill existing session', async () => {
vi.useFakeTimers(); vi.useFakeTimers();
vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(platform.systemPathExists).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any); vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => true } as any);
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' }); vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
const session = service.createSession(); const session = await service.createSession();
const result = service.killSession(session.id); const result = service.killSession(session!.id);
expect(result).toBe(true); expect(result).toBe(true);
expect(mockPtyProcess.kill).toHaveBeenCalledWith('SIGTERM'); expect(mockPtyProcess.kill).toHaveBeenCalledWith('SIGTERM');
@@ -423,7 +448,7 @@ describe('terminal-service.ts', () => {
vi.advanceTimersByTime(1000); vi.advanceTimersByTime(1000);
expect(mockPtyProcess.kill).toHaveBeenCalledWith('SIGKILL'); expect(mockPtyProcess.kill).toHaveBeenCalledWith('SIGKILL');
expect(service.getSession(session.id)).toBeUndefined(); expect(service.getSession(session!.id)).toBeUndefined();
vi.useRealTimers(); vi.useRealTimers();
}); });
@@ -434,29 +459,29 @@ describe('terminal-service.ts', () => {
expect(result).toBe(false); expect(result).toBe(false);
}); });
it('should handle kill errors', () => { it('should handle kill errors', async () => {
vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(platform.systemPathExists).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any); vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => true } as any);
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' }); vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
mockPtyProcess.kill.mockImplementation(() => { mockPtyProcess.kill.mockImplementation(() => {
throw new Error('Kill failed'); throw new Error('Kill failed');
}); });
const session = service.createSession(); const session = await service.createSession();
const result = service.killSession(session.id); const result = service.killSession(session!.id);
expect(result).toBe(false); expect(result).toBe(false);
}); });
}); });
describe('getSession', () => { describe('getSession', () => {
it('should return existing session', () => { it('should return existing session', async () => {
vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(platform.systemPathExists).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any); vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => true } as any);
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' }); vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
const session = service.createSession(); const session = await service.createSession();
const retrieved = service.getSession(session.id); const retrieved = service.getSession(session!.id);
expect(retrieved).toBe(session); expect(retrieved).toBe(session);
}); });
@@ -469,15 +494,15 @@ describe('terminal-service.ts', () => {
}); });
describe('getScrollback', () => { describe('getScrollback', () => {
it('should return scrollback buffer for existing session', () => { it('should return scrollback buffer for existing session', async () => {
vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(platform.systemPathExists).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any); vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => true } as any);
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' }); vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
const session = service.createSession(); const session = await service.createSession();
session.scrollbackBuffer = 'test scrollback'; session!.scrollbackBuffer = 'test scrollback';
const scrollback = service.getScrollback(session.id); const scrollback = service.getScrollback(session!.id);
expect(scrollback).toBe('test scrollback'); expect(scrollback).toBe('test scrollback');
}); });
@@ -490,19 +515,21 @@ describe('terminal-service.ts', () => {
}); });
describe('getAllSessions', () => { describe('getAllSessions', () => {
it('should return all active sessions', () => { it('should return all active sessions', async () => {
vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(platform.systemPathExists).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any); vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => true } as any);
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' }); vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
const session1 = service.createSession({ cwd: '/dir1' }); const session1 = await service.createSession({ cwd: '/dir1' });
const session2 = service.createSession({ cwd: '/dir2' }); const session2 = await service.createSession({ cwd: '/dir2' });
const sessions = service.getAllSessions(); const sessions = service.getAllSessions();
expect(sessions).toHaveLength(2); expect(sessions).toHaveLength(2);
expect(sessions[0].id).toBe(session1.id); expect(session1).not.toBeNull();
expect(sessions[1].id).toBe(session2.id); expect(session2).not.toBeNull();
expect(sessions[0].id).toBe(session1!.id);
expect(sessions[1].id).toBe(session2!.id);
expect(sessions[0].cwd).toBe('/dir1'); expect(sessions[0].cwd).toBe('/dir1');
expect(sessions[1].cwd).toBe('/dir2'); expect(sessions[1].cwd).toBe('/dir2');
}); });
@@ -535,30 +562,32 @@ describe('terminal-service.ts', () => {
}); });
describe('cleanup', () => { describe('cleanup', () => {
it('should clean up all sessions', () => { it('should clean up all sessions', async () => {
vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(platform.systemPathExists).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any); vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => true } as any);
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' }); vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
const session1 = service.createSession(); const session1 = await service.createSession();
const session2 = service.createSession(); const session2 = await service.createSession();
service.cleanup(); service.cleanup();
expect(service.getSession(session1.id)).toBeUndefined(); expect(session1).not.toBeNull();
expect(service.getSession(session2.id)).toBeUndefined(); expect(session2).not.toBeNull();
expect(service.getSession(session1!.id)).toBeUndefined();
expect(service.getSession(session2!.id)).toBeUndefined();
expect(service.getAllSessions()).toHaveLength(0); expect(service.getAllSessions()).toHaveLength(0);
}); });
it('should handle cleanup errors gracefully', () => { it('should handle cleanup errors gracefully', async () => {
vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(platform.systemPathExists).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any); vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => true } as any);
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' }); vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
mockPtyProcess.kill.mockImplementation(() => { mockPtyProcess.kill.mockImplementation(() => {
throw new Error('Kill failed'); throw new Error('Kill failed');
}); });
service.createSession(); await service.createSession();
expect(() => service.cleanup()).not.toThrow(); expect(() => service.cleanup()).not.toThrow();
}); });

View File

@@ -10,6 +10,9 @@
"author": "AutoMaker Team", "author": "AutoMaker Team",
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"private": true, "private": true,
"engines": {
"node": ">=22.0.0 <23.0.0"
},
"main": "dist-electron/main.js", "main": "dist-electron/main.js",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@@ -35,87 +38,87 @@
"dev:electron:wsl:gpu": "cross-env MESA_D3D12_DEFAULT_ADAPTER_NAME=NVIDIA vite" "dev:electron:wsl:gpu": "cross-env MESA_D3D12_DEFAULT_ADAPTER_NAME=NVIDIA vite"
}, },
"dependencies": { "dependencies": {
"@automaker/dependency-resolver": "^1.0.0", "@automaker/dependency-resolver": "1.0.0",
"@automaker/types": "^1.0.0", "@automaker/types": "1.0.0",
"@codemirror/lang-xml": "^6.1.0", "@codemirror/lang-xml": "6.1.0",
"@codemirror/theme-one-dark": "^6.1.3", "@codemirror/theme-one-dark": "6.1.3",
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "6.3.1",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "10.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "3.2.2",
"@lezer/highlight": "^1.2.3", "@lezer/highlight": "1.2.3",
"@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-checkbox": "1.3.3",
"@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-collapsible": "1.1.12",
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-dropdown-menu": "2.1.16",
"@radix-ui/react-label": "^2.1.8", "@radix-ui/react-label": "2.1.8",
"@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-popover": "1.1.15",
"@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-radio-group": "1.3.8",
"@radix-ui/react-select": "^2.2.6", "@radix-ui/react-select": "2.2.6",
"@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-slider": "1.3.6",
"@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-slot": "1.2.4",
"@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-switch": "1.2.6",
"@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tabs": "1.1.13",
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "1.2.8",
"@tanstack/react-query": "^5.90.12", "@tanstack/react-query": "5.90.12",
"@tanstack/react-router": "^1.141.6", "@tanstack/react-router": "1.141.6",
"@uiw/react-codemirror": "^4.25.4", "@uiw/react-codemirror": "4.25.4",
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-fit": "0.10.0",
"@xterm/addon-search": "^0.15.0", "@xterm/addon-search": "0.15.0",
"@xterm/addon-web-links": "^0.11.0", "@xterm/addon-web-links": "0.11.0",
"@xterm/addon-webgl": "^0.18.0", "@xterm/addon-webgl": "0.18.0",
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "5.5.0",
"@xyflow/react": "^12.10.0", "@xyflow/react": "12.10.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "0.7.1",
"clsx": "^2.1.1", "clsx": "2.1.1",
"cmdk": "^1.1.1", "cmdk": "1.1.1",
"dagre": "^0.8.5", "dagre": "0.8.5",
"dotenv": "^17.2.3", "dotenv": "17.2.3",
"geist": "^1.5.1", "geist": "1.5.1",
"lucide-react": "^0.562.0", "lucide-react": "0.562.0",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"react-markdown": "^10.1.0", "react-markdown": "10.1.0",
"react-resizable-panels": "^3.0.6", "react-resizable-panels": "3.0.6",
"rehype-raw": "^7.0.0", "rehype-raw": "7.0.0",
"sonner": "^2.0.7", "sonner": "2.0.7",
"tailwind-merge": "^3.4.0", "tailwind-merge": "3.4.0",
"usehooks-ts": "^3.1.1", "usehooks-ts": "3.1.1",
"zustand": "^5.0.9" "zustand": "5.0.9"
}, },
"optionalDependencies": { "optionalDependencies": {
"lightningcss-darwin-arm64": "^1.29.2", "lightningcss-darwin-arm64": "1.29.2",
"lightningcss-darwin-x64": "^1.29.2", "lightningcss-darwin-x64": "1.29.2",
"lightningcss-linux-arm-gnueabihf": "^1.29.2", "lightningcss-linux-arm-gnueabihf": "1.29.2",
"lightningcss-linux-arm64-gnu": "^1.29.2", "lightningcss-linux-arm64-gnu": "1.29.2",
"lightningcss-linux-arm64-musl": "^1.29.2", "lightningcss-linux-arm64-musl": "1.29.2",
"lightningcss-linux-x64-gnu": "^1.29.2", "lightningcss-linux-x64-gnu": "1.29.2",
"lightningcss-linux-x64-musl": "^1.29.2", "lightningcss-linux-x64-musl": "1.29.2",
"lightningcss-win32-arm64-msvc": "^1.29.2", "lightningcss-win32-arm64-msvc": "1.29.2",
"lightningcss-win32-x64-msvc": "^1.29.2" "lightningcss-win32-x64-msvc": "1.29.2"
}, },
"devDependencies": { "devDependencies": {
"@electron/rebuild": "^4.0.2", "@electron/rebuild": "4.0.2",
"@eslint/js": "^9.0.0", "@eslint/js": "9.0.0",
"@playwright/test": "^1.57.0", "@playwright/test": "1.57.0",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "4.1.18",
"@tanstack/router-plugin": "^1.141.7", "@tanstack/router-plugin": "1.141.7",
"@types/dagre": "^0.7.53", "@types/dagre": "0.7.53",
"@types/node": "^22", "@types/node": "22.19.3",
"@types/react": "^19.2.7", "@types/react": "19.2.7",
"@types/react-dom": "^19.2.3", "@types/react-dom": "19.2.3",
"@typescript-eslint/eslint-plugin": "^8.50.0", "@typescript-eslint/eslint-plugin": "8.50.0",
"@typescript-eslint/parser": "^8.50.0", "@typescript-eslint/parser": "8.50.0",
"@vitejs/plugin-react": "^5.1.2", "@vitejs/plugin-react": "5.1.2",
"cross-env": "^10.1.0", "cross-env": "10.1.0",
"electron": "39.2.7", "electron": "39.2.7",
"electron-builder": "^26.0.12", "electron-builder": "26.0.12",
"eslint": "^9.39.2", "eslint": "9.39.2",
"tailwindcss": "^4.1.18", "tailwindcss": "4.1.18",
"tw-animate-css": "^1.4.0", "tw-animate-css": "1.4.0",
"typescript": "5.9.3", "typescript": "5.9.3",
"vite": "^7.3.0", "vite": "7.3.0",
"vite-plugin-electron": "^0.29.0", "vite-plugin-electron": "0.29.0",
"vite-plugin-electron-renderer": "^0.14.6" "vite-plugin-electron-renderer": "0.14.6"
}, },
"build": { "build": {
"appId": "com.automaker.app", "appId": "com.automaker.app",

View File

@@ -355,8 +355,11 @@ async function startServer(): Promise<void> {
`Node.js executable not found at: ${command} (source: ${nodeResult.source})` `Node.js executable not found at: ${command} (source: ${nodeResult.source})`
); );
} }
} catch { } catch (error) {
throw new Error(`Node.js executable not found at: ${command} (source: ${nodeResult.source})`); const originalError = error instanceof Error ? error.message : String(error);
throw new Error(
`Failed to verify Node.js executable at: ${command} (source: ${nodeResult.source}). Reason: ${originalError}`
);
} }
} }

View File

@@ -39,7 +39,9 @@ function validateScriptPath(targetPath) {
const resolved = path.resolve(__dirname, targetPath); const resolved = path.resolve(__dirname, targetPath);
const normalizedBase = path.resolve(__dirname); const normalizedBase = path.resolve(__dirname);
if (!resolved.startsWith(normalizedBase + path.sep) && resolved !== normalizedBase) { if (!resolved.startsWith(normalizedBase + path.sep) && resolved !== normalizedBase) {
throw new Error(`[init.mjs] Security: Path access denied outside script directory: ${targetPath}`); throw new Error(
`[init.mjs] Security: Path access denied outside script directory: ${targetPath}`
);
} }
return resolved; return resolved;
} }

View File

@@ -25,12 +25,15 @@
], ],
"author": "AutoMaker Team", "author": "AutoMaker Team",
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"engines": {
"node": ">=22.0.0 <23.0.0"
},
"dependencies": { "dependencies": {
"@automaker/types": "^1.0.0" "@automaker/types": "1.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.10.5", "@types/node": "22.19.3",
"typescript": "^5.7.3", "typescript": "5.9.3",
"vitest": "^4.0.16" "vitest": "4.0.16"
} }
} }

View File

@@ -18,13 +18,16 @@
], ],
"author": "AutoMaker Team", "author": "AutoMaker Team",
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"engines": {
"node": ">=22.0.0 <23.0.0"
},
"dependencies": { "dependencies": {
"@automaker/types": "^1.0.0", "@automaker/types": "1.0.0",
"@automaker/utils": "^1.0.0" "@automaker/utils": "1.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.10.5", "@types/node": "22.19.3",
"typescript": "^5.7.3", "typescript": "5.9.3",
"vitest": "^4.0.16" "vitest": "4.0.16"
} }
} }

View File

@@ -18,12 +18,15 @@
], ],
"author": "AutoMaker Team", "author": "AutoMaker Team",
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"engines": {
"node": ">=22.0.0 <23.0.0"
},
"dependencies": { "dependencies": {
"@automaker/types": "^1.0.0" "@automaker/types": "1.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.10.5", "@types/node": "22.19.3",
"typescript": "^5.7.3", "typescript": "5.9.3",
"vitest": "^4.0.16" "vitest": "4.0.16"
} }
} }

View File

@@ -17,13 +17,16 @@
], ],
"author": "AutoMaker Team", "author": "AutoMaker Team",
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"engines": {
"node": ">=22.0.0 <23.0.0"
},
"dependencies": { "dependencies": {
"@automaker/types": "^1.0.0", "@automaker/types": "1.0.0",
"p-limit": "^6.2.0" "p-limit": "6.2.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.10.5", "@types/node": "22.19.3",
"typescript": "^5.7.3", "typescript": "5.9.3",
"vitest": "^4.0.16" "vitest": "4.0.16"
} }
} }

View File

@@ -165,17 +165,26 @@ export async function readFile(
}, `readFile(${filePath})`); }, `readFile(${filePath})`);
} }
/**
* Options for writeFile
*/
export interface WriteFileOptions {
encoding?: BufferEncoding;
mode?: number;
flag?: string;
}
/** /**
* Wrapper around fs.writeFile that validates path first * Wrapper around fs.writeFile that validates path first
*/ */
export async function writeFile( export async function writeFile(
filePath: string, filePath: string,
data: string | Buffer, data: string | Buffer,
encoding?: BufferEncoding optionsOrEncoding?: BufferEncoding | WriteFileOptions
): Promise<void> { ): Promise<void> {
const validatedPath = validatePath(filePath); const validatedPath = validatePath(filePath);
return executeWithRetry( return executeWithRetry(
() => fs.writeFile(validatedPath, data, encoding), () => fs.writeFile(validatedPath, data, optionsOrEncoding),
`writeFile(${filePath})` `writeFile(${filePath})`
); );
} }

View File

@@ -114,13 +114,16 @@ export function getShellPaths(): string[] {
if (process.platform === 'win32') { if (process.platform === 'win32') {
return [ return [
process.env.COMSPEC || 'cmd.exe', process.env.COMSPEC || 'cmd.exe',
'cmd.exe',
'powershell.exe', 'powershell.exe',
'pwsh.exe', // PowerShell Core short form
'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe', 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe',
'C:\\Program Files\\PowerShell\\7\\pwsh.exe', 'C:\\Program Files\\PowerShell\\7\\pwsh.exe',
'C:\\Program Files\\PowerShell\\7-preview\\pwsh.exe', // Preview versions
]; ];
} }
return ['/bin/zsh', '/bin/bash', '/bin/sh']; return ['/bin/zsh', '/bin/bash', '/bin/sh', '/usr/bin/zsh', '/usr/bin/bash'];
} }
// ============================================================================= // =============================================================================

View File

@@ -18,12 +18,15 @@
], ],
"author": "AutoMaker Team", "author": "AutoMaker Team",
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"engines": {
"node": ">=22.0.0 <23.0.0"
},
"dependencies": { "dependencies": {
"@automaker/types": "^1.0.0" "@automaker/types": "1.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.10.5", "@types/node": "22.19.3",
"typescript": "^5.7.3", "typescript": "5.9.3",
"vitest": "^4.0.16" "vitest": "4.0.16"
} }
} }

View File

@@ -208,6 +208,9 @@ This feature depends on: {{dependencies}}
**Verification:** **Verification:**
{{verificationInstructions}} {{verificationInstructions}}
{{/if}} {{/if}}
**CRITICAL - Port Protection:**
NEVER kill or terminate processes running on ports 3007 or 3008. These are reserved for the Automaker application. Killing these ports will crash Automaker and terminate this session.
`; `;
export const DEFAULT_AUTO_MODE_FOLLOW_UP_PROMPT_TEMPLATE = `## Follow-up on Feature Implementation export const DEFAULT_AUTO_MODE_FOLLOW_UP_PROMPT_TEMPLATE = `## Follow-up on Feature Implementation
@@ -299,6 +302,9 @@ You have access to several tools:
4. Ask questions when requirements are unclear 4. Ask questions when requirements are unclear
5. Guide users toward good software design principles 5. Guide users toward good software design principles
**CRITICAL - Port Protection:**
NEVER kill or terminate processes running on ports 3007 or 3008. These are reserved for the Automaker application itself. Killing these ports will crash Automaker and terminate your session.
Remember: You're a collaborative partner in the development process. Be helpful, clear, and thorough.`; Remember: You're a collaborative partner in the development process. Be helpful, clear, and thorough.`;
/** /**

View File

@@ -15,8 +15,11 @@
], ],
"author": "AutoMaker Team", "author": "AutoMaker Team",
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"engines": {
"node": ">=22.0.0 <23.0.0"
},
"devDependencies": { "devDependencies": {
"@types/node": "^22.10.5", "@types/node": "22.19.3",
"typescript": "^5.7.3" "typescript": "5.9.3"
} }
} }

View File

@@ -17,13 +17,16 @@
], ],
"author": "AutoMaker Team", "author": "AutoMaker Team",
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"engines": {
"node": ">=22.0.0 <23.0.0"
},
"dependencies": { "dependencies": {
"@automaker/platform": "^1.0.0", "@automaker/platform": "1.0.0",
"@automaker/types": "^1.0.0" "@automaker/types": "1.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.10.5", "@types/node": "22.19.3",
"typescript": "^5.7.3", "typescript": "5.9.3",
"vitest": "^4.0.16" "vitest": "4.0.16"
} }
} }

8
package-lock.json generated
View File

@@ -1246,7 +1246,7 @@
}, },
"node_modules/@electron/node-gyp": { "node_modules/@electron/node-gyp": {
"version": "10.2.0-electron.1", "version": "10.2.0-electron.1",
"resolved": "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", "resolved": "git+ssh://git@github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2",
"integrity": "sha512-4MSBTT8y07YUDqf69/vSh80Hh791epYqGtWHO3zSKhYFwQg+gx9wi1PqbqP6YqC4WMsNxZ5l9oDmnWdK5pfCKQ==", "integrity": "sha512-4MSBTT8y07YUDqf69/vSh80Hh791epYqGtWHO3zSKhYFwQg+gx9wi1PqbqP6YqC4WMsNxZ5l9oDmnWdK5pfCKQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
@@ -13953,9 +13953,9 @@
} }
}, },
"node_modules/qs": { "node_modules/qs": {
"version": "6.14.0", "version": "6.14.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"side-channel": "^1.1.0" "side-channel": "^1.1.0"

View File

@@ -2,6 +2,9 @@
"name": "automaker", "name": "automaker",
"version": "1.0.0", "version": "1.0.0",
"private": true, "private": true,
"engines": {
"node": ">=22.0.0 <23.0.0"
},
"workspaces": [ "workspaces": [
"apps/*", "apps/*",
"libs/*" "libs/*"
@@ -53,13 +56,13 @@
] ]
}, },
"dependencies": { "dependencies": {
"cross-spawn": "^7.0.6", "cross-spawn": "7.0.6",
"rehype-sanitize": "^6.0.0", "rehype-sanitize": "6.0.0",
"tree-kill": "^1.2.2" "tree-kill": "1.2.2"
}, },
"devDependencies": { "devDependencies": {
"husky": "^9.1.7", "husky": "9.1.7",
"lint-staged": "^16.2.7", "lint-staged": "16.2.7",
"prettier": "^3.7.4" "prettier": "3.7.4"
} }
} }