diff --git a/.claude_settings.json b/.claude_settings.json
index 246fb900..969f1214 100644
--- a/.claude_settings.json
+++ b/.claude_settings.json
@@ -21,4 +21,4 @@
"mcp__puppeteer__puppeteer_evaluate"
]
}
-}
\ No newline at end of file
+}
diff --git a/.github/actions/setup-project/action.yml b/.github/actions/setup-project/action.yml
index 8ef0f33b..75030b90 100644
--- a/.github/actions/setup-project/action.yml
+++ b/.github/actions/setup-project/action.yml
@@ -1,28 +1,28 @@
-name: "Setup Project"
-description: "Common setup steps for CI workflows - checkout, Node.js, dependencies, and native modules"
+name: 'Setup Project'
+description: 'Common setup steps for CI workflows - checkout, Node.js, dependencies, and native modules'
inputs:
node-version:
- description: "Node.js version to use"
+ description: 'Node.js version to use'
required: false
- default: "22"
+ default: '22'
check-lockfile:
- description: "Run lockfile lint check for SSH URLs"
+ description: 'Run lockfile lint check for SSH URLs'
required: false
- default: "false"
+ default: 'false'
rebuild-node-pty-path:
- description: "Working directory for node-pty rebuild (empty = root)"
+ description: 'Working directory for node-pty rebuild (empty = root)'
required: false
- default: ""
+ default: ''
runs:
- using: "composite"
+ using: 'composite'
steps:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- cache: "npm"
+ cache: 'npm'
cache-dependency-path: package-lock.json
- name: Check for SSH URLs in lockfile
diff --git a/.github/scripts/upload-to-r2.js b/.github/scripts/upload-to-r2.js
index 4749bda1..b54d4b19 100644
--- a/.github/scripts/upload-to-r2.js
+++ b/.github/scripts/upload-to-r2.js
@@ -1,15 +1,11 @@
-const {
- S3Client,
- PutObjectCommand,
- GetObjectCommand,
-} = require("@aws-sdk/client-s3");
-const fs = require("fs");
-const path = require("path");
-const https = require("https");
-const { pipeline } = require("stream/promises");
+const { S3Client, PutObjectCommand, GetObjectCommand } = require('@aws-sdk/client-s3');
+const fs = require('fs');
+const path = require('path');
+const https = require('https');
+const { pipeline } = require('stream/promises');
const s3Client = new S3Client({
- region: "auto",
+ region: 'auto',
endpoint: process.env.R2_ENDPOINT,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID,
@@ -28,14 +24,14 @@ async function fetchExistingReleases() {
const response = await s3Client.send(
new GetObjectCommand({
Bucket: BUCKET,
- Key: "releases.json",
+ Key: 'releases.json',
})
);
const body = await response.Body.transformToString();
return JSON.parse(body);
} catch (error) {
- if (error.name === "NoSuchKey" || error.$metadata?.httpStatusCode === 404) {
- console.log("No existing releases.json found, creating new one");
+ if (error.name === 'NoSuchKey' || error.$metadata?.httpStatusCode === 404) {
+ console.log('No existing releases.json found, creating new one');
return { latestVersion: null, releases: [] };
}
throw error;
@@ -85,7 +81,7 @@ async function checkUrlAccessible(url, maxRetries = 10, initialDelay = 1000) {
resolve({
accessible: false,
statusCode,
- error: "Redirect without location header",
+ error: 'Redirect without location header',
});
return;
}
@@ -93,18 +89,16 @@ async function checkUrlAccessible(url, maxRetries = 10, initialDelay = 1000) {
return https
.get(redirectUrl, { timeout: 10000 }, (redirectResponse) => {
const redirectStatus = redirectResponse.statusCode;
- const contentType =
- redirectResponse.headers["content-type"] || "";
+ const contentType = redirectResponse.headers['content-type'] || '';
// Check if it's actually a file (zip/tar.gz) and not HTML
const isFile =
- contentType.includes("application/zip") ||
- contentType.includes("application/gzip") ||
- contentType.includes("application/x-gzip") ||
- contentType.includes("application/x-tar") ||
- redirectUrl.includes(".zip") ||
- redirectUrl.includes(".tar.gz");
- const isGood =
- redirectStatus >= 200 && redirectStatus < 300 && isFile;
+ contentType.includes('application/zip') ||
+ contentType.includes('application/gzip') ||
+ contentType.includes('application/x-gzip') ||
+ contentType.includes('application/x-tar') ||
+ redirectUrl.includes('.zip') ||
+ redirectUrl.includes('.tar.gz');
+ const isGood = redirectStatus >= 200 && redirectStatus < 300 && isFile;
redirectResponse.destroy();
resolve({
accessible: isGood,
@@ -113,38 +107,38 @@ async function checkUrlAccessible(url, maxRetries = 10, initialDelay = 1000) {
contentType,
});
})
- .on("error", (error) => {
+ .on('error', (error) => {
resolve({
accessible: false,
statusCode,
error: error.message,
});
})
- .on("timeout", function () {
+ .on('timeout', function () {
this.destroy();
resolve({
accessible: false,
statusCode,
- error: "Timeout following redirect",
+ error: 'Timeout following redirect',
});
});
}
// Check if status is good (200-299 range) and it's actually a file
- const contentType = response.headers["content-type"] || "";
+ const contentType = response.headers['content-type'] || '';
const isFile =
- contentType.includes("application/zip") ||
- contentType.includes("application/gzip") ||
- contentType.includes("application/x-gzip") ||
- contentType.includes("application/x-tar") ||
- url.includes(".zip") ||
- url.includes(".tar.gz");
+ contentType.includes('application/zip') ||
+ contentType.includes('application/gzip') ||
+ contentType.includes('application/x-gzip') ||
+ contentType.includes('application/x-tar') ||
+ url.includes('.zip') ||
+ url.includes('.tar.gz');
const isGood = statusCode >= 200 && statusCode < 300 && isFile;
response.destroy();
resolve({ accessible: isGood, statusCode, contentType });
});
- request.on("error", (error) => {
+ request.on('error', (error) => {
resolve({
accessible: false,
statusCode: null,
@@ -152,12 +146,12 @@ async function checkUrlAccessible(url, maxRetries = 10, initialDelay = 1000) {
});
});
- request.on("timeout", () => {
+ request.on('timeout', () => {
request.destroy();
resolve({
accessible: false,
statusCode: null,
- error: "Request timeout",
+ error: 'Request timeout',
});
});
});
@@ -168,22 +162,14 @@ async function checkUrlAccessible(url, maxRetries = 10, initialDelay = 1000) {
`✓ URL ${url} is now accessible after ${attempt} retries (status: ${result.statusCode})`
);
} else {
- console.log(
- `✓ URL ${url} is accessible (status: ${result.statusCode})`
- );
+ console.log(`✓ URL ${url} is accessible (status: ${result.statusCode})`);
}
return result.finalUrl || url; // Return the final URL (after redirects) if available
} else {
- const errorMsg = result.error ? ` - ${result.error}` : "";
- const statusMsg = result.statusCode
- ? ` (status: ${result.statusCode})`
- : "";
- const contentTypeMsg = result.contentType
- ? ` [content-type: ${result.contentType}]`
- : "";
- console.log(
- `✗ URL ${url} not accessible${statusMsg}${contentTypeMsg}${errorMsg}`
- );
+ const errorMsg = result.error ? ` - ${result.error}` : '';
+ const statusMsg = result.statusCode ? ` (status: ${result.statusCode})` : '';
+ const contentTypeMsg = result.contentType ? ` [content-type: ${result.contentType}]` : '';
+ console.log(`✗ URL ${url} not accessible${statusMsg}${contentTypeMsg}${errorMsg}`);
}
} catch (error) {
console.log(`✗ URL ${url} check failed: ${error.message}`);
@@ -191,9 +177,7 @@ async function checkUrlAccessible(url, maxRetries = 10, initialDelay = 1000) {
if (attempt < maxRetries - 1) {
const delay = initialDelay * Math.pow(2, attempt);
- console.log(
- ` Retrying in ${delay}ms... (attempt ${attempt + 1}/${maxRetries})`
- );
+ console.log(` Retrying in ${delay}ms... (attempt ${attempt + 1}/${maxRetries})`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
@@ -207,12 +191,7 @@ async function downloadFromGitHub(url, outputPath) {
const statusCode = response.statusCode;
// Follow redirects (all redirect types)
- if (
- statusCode === 301 ||
- statusCode === 302 ||
- statusCode === 307 ||
- statusCode === 308
- ) {
+ if (statusCode === 301 || statusCode === 302 || statusCode === 307 || statusCode === 308) {
const redirectUrl = response.headers.location;
response.destroy();
if (!redirectUrl) {
@@ -220,39 +199,33 @@ async function downloadFromGitHub(url, outputPath) {
return;
}
// Resolve relative redirects
- const finalRedirectUrl = redirectUrl.startsWith("http")
+ const finalRedirectUrl = redirectUrl.startsWith('http')
? redirectUrl
: new URL(redirectUrl, url).href;
console.log(` Following redirect: ${finalRedirectUrl}`);
- return downloadFromGitHub(finalRedirectUrl, outputPath)
- .then(resolve)
- .catch(reject);
+ return downloadFromGitHub(finalRedirectUrl, outputPath).then(resolve).catch(reject);
}
if (statusCode !== 200) {
response.destroy();
- reject(
- new Error(
- `Failed to download ${url}: ${statusCode} ${response.statusMessage}`
- )
- );
+ reject(new Error(`Failed to download ${url}: ${statusCode} ${response.statusMessage}`));
return;
}
const fileStream = fs.createWriteStream(outputPath);
response.pipe(fileStream);
- fileStream.on("finish", () => {
+ fileStream.on('finish', () => {
fileStream.close();
resolve();
});
- fileStream.on("error", (error) => {
+ fileStream.on('error', (error) => {
response.destroy();
reject(error);
});
});
- request.on("error", reject);
- request.on("timeout", () => {
+ request.on('error', reject);
+ request.on('timeout', () => {
request.destroy();
reject(new Error(`Request timeout for ${url}`));
});
@@ -260,8 +233,8 @@ async function downloadFromGitHub(url, outputPath) {
}
async function main() {
- const artifactsDir = "artifacts";
- const tempDir = path.join(artifactsDir, "temp");
+ const artifactsDir = 'artifacts';
+ const tempDir = path.join(artifactsDir, 'temp');
// Create temp directory for downloaded GitHub archives
if (!fs.existsSync(tempDir)) {
@@ -292,40 +265,30 @@ async function main() {
// Find all artifacts
const artifacts = {
- windows: findArtifacts(path.join(artifactsDir, "windows-builds"), /\.exe$/),
- macos: findArtifacts(path.join(artifactsDir, "macos-builds"), /-x64\.dmg$/),
- macosArm: findArtifacts(
- path.join(artifactsDir, "macos-builds"),
- /-arm64\.dmg$/
- ),
- linux: findArtifacts(
- path.join(artifactsDir, "linux-builds"),
- /\.AppImage$/
- ),
+ windows: findArtifacts(path.join(artifactsDir, 'windows-builds'), /\.exe$/),
+ macos: findArtifacts(path.join(artifactsDir, 'macos-builds'), /-x64\.dmg$/),
+ macosArm: findArtifacts(path.join(artifactsDir, 'macos-builds'), /-arm64\.dmg$/),
+ linux: findArtifacts(path.join(artifactsDir, 'linux-builds'), /\.AppImage$/),
sourceZip: [sourceZipPath],
sourceTarGz: [sourceTarGzPath],
};
- console.log("Found artifacts:");
+ console.log('Found artifacts:');
for (const [platform, files] of Object.entries(artifacts)) {
console.log(
- ` ${platform}: ${
- files.length > 0
- ? files.map((f) => path.basename(f)).join(", ")
- : "none"
- }`
+ ` ${platform}: ${files.length > 0 ? files.map((f) => path.basename(f)).join(', ') : 'none'}`
);
}
// Upload each artifact to R2
const assets = {};
const contentTypes = {
- windows: "application/x-msdownload",
- macos: "application/x-apple-diskimage",
- macosArm: "application/x-apple-diskimage",
- linux: "application/x-executable",
- sourceZip: "application/zip",
- sourceTarGz: "application/gzip",
+ windows: 'application/x-msdownload',
+ macos: 'application/x-apple-diskimage',
+ macosArm: 'application/x-apple-diskimage',
+ linux: 'application/x-executable',
+ sourceZip: 'application/zip',
+ sourceTarGz: 'application/gzip',
};
for (const [platform, files] of Object.entries(artifacts)) {
@@ -345,11 +308,11 @@ async function main() {
filename,
size,
arch:
- platform === "macosArm"
- ? "arm64"
- : platform === "sourceZip" || platform === "sourceTarGz"
- ? "source"
- : "x64",
+ platform === 'macosArm'
+ ? 'arm64'
+ : platform === 'sourceZip' || platform === 'sourceTarGz'
+ ? 'source'
+ : 'x64',
};
}
@@ -364,9 +327,7 @@ async function main() {
};
// Remove existing entry for this version if re-running
- releasesData.releases = releasesData.releases.filter(
- (r) => r.version !== VERSION
- );
+ releasesData.releases = releasesData.releases.filter((r) => r.version !== VERSION);
// Prepend new release
releasesData.releases.unshift(newRelease);
@@ -376,19 +337,19 @@ async function main() {
await s3Client.send(
new PutObjectCommand({
Bucket: BUCKET,
- Key: "releases.json",
+ Key: 'releases.json',
Body: JSON.stringify(releasesData, null, 2),
- ContentType: "application/json",
- CacheControl: "public, max-age=60",
+ ContentType: 'application/json',
+ CacheControl: 'public, max-age=60',
})
);
- console.log("Successfully updated releases.json");
+ console.log('Successfully updated releases.json');
console.log(`Latest version: ${VERSION}`);
console.log(`Total releases: ${releasesData.releases.length}`);
}
main().catch((err) => {
- console.error("Failed to upload to R2:", err);
+ console.error('Failed to upload to R2:', err);
process.exit(1);
});
diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml
index d300267f..9471a059 100644
--- a/.github/workflows/claude.yml
+++ b/.github/workflows/claude.yml
@@ -47,4 +47,3 @@ jobs:
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
# claude_args: '--allowed-tools Bash(gh pr:*)'
-
diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml
index 9f8e49a8..a4064bda 100644
--- a/.github/workflows/e2e-tests.yml
+++ b/.github/workflows/e2e-tests.yml
@@ -3,7 +3,7 @@ name: E2E Tests
on:
pull_request:
branches:
- - "*"
+ - '*'
push:
branches:
- main
@@ -21,8 +21,8 @@ jobs:
- name: Setup project
uses: ./.github/actions/setup-project
with:
- check-lockfile: "true"
- rebuild-node-pty-path: "apps/server"
+ check-lockfile: 'true'
+ rebuild-node-pty-path: 'apps/server'
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
@@ -58,7 +58,7 @@ jobs:
env:
CI: true
VITE_SERVER_URL: http://localhost:3008
- VITE_SKIP_SETUP: "true"
+ VITE_SKIP_SETUP: 'true'
- name: Upload Playwright report
uses: actions/upload-artifact@v4
diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml
index 38e0c978..4311eeb0 100644
--- a/.github/workflows/pr-check.yml
+++ b/.github/workflows/pr-check.yml
@@ -3,7 +3,7 @@ name: PR Build Check
on:
pull_request:
branches:
- - "*"
+ - '*'
push:
branches:
- main
@@ -20,7 +20,7 @@ jobs:
- name: Setup project
uses: ./.github/actions/setup-project
with:
- check-lockfile: "true"
+ check-lockfile: 'true'
- name: Run build:electron (dir only - faster CI)
run: npm run build:electron:dir
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 84cc4941..dacea631 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -3,7 +3,7 @@ name: Test Suite
on:
pull_request:
branches:
- - "*"
+ - '*'
push:
branches:
- main
@@ -20,8 +20,8 @@ jobs:
- name: Setup project
uses: ./.github/actions/setup-project
with:
- check-lockfile: "true"
- rebuild-node-pty-path: "apps/server"
+ check-lockfile: 'true'
+ rebuild-node-pty-path: 'apps/server'
- name: Run package tests
run: npm run test:packages
diff --git a/apps/app/next-env.d.ts b/apps/app/next-env.d.ts
index c4b7818f..20e7bcfb 100644
--- a/apps/app/next-env.d.ts
+++ b/apps/app/next-env.d.ts
@@ -1,6 +1,6 @@
///
///
-import "./.next/dev/types/routes.d.ts";
+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/server/src/index.ts b/apps/server/src/index.ts
index adf39f3a..26eeb377 100644
--- a/apps/server/src/index.ts
+++ b/apps/server/src/index.ts
@@ -6,53 +6,54 @@
* In web mode, this server runs on a remote host.
*/
-import express from "express";
-import cors from "cors";
-import morgan from "morgan";
-import { WebSocketServer, WebSocket } from "ws";
-import { createServer } from "http";
-import dotenv from "dotenv";
+import express from 'express';
+import cors from 'cors';
+import morgan from 'morgan';
+import { WebSocketServer, WebSocket } from 'ws';
+import { createServer } from 'http';
+import dotenv from 'dotenv';
-import { createEventEmitter, type EventEmitter } from "./lib/events.js";
-import { initAllowedPaths } from "@automaker/platform";
-import { authMiddleware, getAuthStatus } from "./lib/auth.js";
-import { createFsRoutes } from "./routes/fs/index.js";
-import { createHealthRoutes } from "./routes/health/index.js";
-import { createAgentRoutes } from "./routes/agent/index.js";
-import { createSessionsRoutes } from "./routes/sessions/index.js";
-import { createFeaturesRoutes } from "./routes/features/index.js";
-import { createAutoModeRoutes } from "./routes/auto-mode/index.js";
-import { createEnhancePromptRoutes } from "./routes/enhance-prompt/index.js";
-import { createWorktreeRoutes } from "./routes/worktree/index.js";
-import { createGitRoutes } from "./routes/git/index.js";
-import { createSetupRoutes } from "./routes/setup/index.js";
-import { createSuggestionsRoutes } from "./routes/suggestions/index.js";
-import { createModelsRoutes } from "./routes/models/index.js";
-import { createRunningAgentsRoutes } from "./routes/running-agents/index.js";
-import { createWorkspaceRoutes } from "./routes/workspace/index.js";
-import { createTemplatesRoutes } from "./routes/templates/index.js";
+import { createEventEmitter, type EventEmitter } from './lib/events.js';
+import { initAllowedPaths } from '@automaker/platform';
+import { authMiddleware, getAuthStatus } from './lib/auth.js';
+import { createFsRoutes } from './routes/fs/index.js';
+import { createHealthRoutes } from './routes/health/index.js';
+import { createAgentRoutes } from './routes/agent/index.js';
+import { createSessionsRoutes } from './routes/sessions/index.js';
+import { createFeaturesRoutes } from './routes/features/index.js';
+import { createAutoModeRoutes } from './routes/auto-mode/index.js';
+import { createEnhancePromptRoutes } from './routes/enhance-prompt/index.js';
+import { createWorktreeRoutes } from './routes/worktree/index.js';
+import { createGitRoutes } from './routes/git/index.js';
+import { createSetupRoutes } from './routes/setup/index.js';
+import { createSuggestionsRoutes } from './routes/suggestions/index.js';
+import { createModelsRoutes } from './routes/models/index.js';
+import { createRunningAgentsRoutes } from './routes/running-agents/index.js';
+import { createWorkspaceRoutes } from './routes/workspace/index.js';
+import { createTemplatesRoutes } from './routes/templates/index.js';
import {
createTerminalRoutes,
validateTerminalToken,
isTerminalEnabled,
isTerminalPasswordRequired,
-} from "./routes/terminal/index.js";
-import { createSettingsRoutes } from "./routes/settings/index.js";
-import { AgentService } from "./services/agent-service.js";
-import { FeatureLoader } from "./services/feature-loader.js";
-import { AutoModeService } from "./services/auto-mode-service.js";
-import { getTerminalService } from "./services/terminal-service.js";
-import { SettingsService } from "./services/settings-service.js";
-import { createSpecRegenerationRoutes } from "./routes/app-spec/index.js";
-import { createClaudeRoutes } from "./routes/claude/index.js";
-import { ClaudeUsageService } from "./services/claude-usage-service.js";
+} from './routes/terminal/index.js';
+import { createSettingsRoutes } from './routes/settings/index.js';
+import { AgentService } from './services/agent-service.js';
+import { FeatureLoader } from './services/feature-loader.js';
+import { AutoModeService } from './services/auto-mode-service.js';
+import { getTerminalService } from './services/terminal-service.js';
+import { SettingsService } from './services/settings-service.js';
+import { createSpecRegenerationRoutes } from './routes/app-spec/index.js';
+import { createClaudeRoutes } from './routes/claude/index.js';
+import { ClaudeUsageService } from './services/claude-usage-service.js';
+import { createContextRoutes } from './routes/context/index.js';
// Load environment variables
dotenv.config();
-const PORT = parseInt(process.env.PORT || "3008", 10);
-const DATA_DIR = process.env.DATA_DIR || "./data";
-const ENABLE_REQUEST_LOGGING = process.env.ENABLE_REQUEST_LOGGING !== "false"; // Default to true
+const PORT = parseInt(process.env.PORT || '3008', 10);
+const DATA_DIR = process.env.DATA_DIR || './data';
+const ENABLE_REQUEST_LOGGING = process.env.ENABLE_REQUEST_LOGGING !== 'false'; // Default to true
// Check for required environment variables
const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY;
@@ -71,7 +72,7 @@ if (!hasAnthropicKey) {
╚═══════════════════════════════════════════════════════════════════════╝
`);
} else {
- console.log("[Server] ✓ ANTHROPIC_API_KEY detected (API key auth)");
+ console.log('[Server] ✓ ANTHROPIC_API_KEY detected (API key auth)');
}
// Initialize security
@@ -83,7 +84,7 @@ const app = express();
// Middleware
// Custom colored logger showing only endpoint and status code (configurable via ENABLE_REQUEST_LOGGING env var)
if (ENABLE_REQUEST_LOGGING) {
- morgan.token("status-colored", (req, res) => {
+ morgan.token('status-colored', (req, res) => {
const status = res.statusCode;
if (status >= 500) return `\x1b[31m${status}\x1b[0m`; // Red for server errors
if (status >= 400) return `\x1b[33m${status}\x1b[0m`; // Yellow for client errors
@@ -92,18 +93,18 @@ if (ENABLE_REQUEST_LOGGING) {
});
app.use(
- morgan(":method :url :status-colored", {
- skip: (req) => req.url === "/api/health", // Skip health check logs
+ morgan(':method :url :status-colored', {
+ skip: (req) => req.url === '/api/health', // Skip health check logs
})
);
}
app.use(
cors({
- origin: process.env.CORS_ORIGIN || "*",
+ origin: process.env.CORS_ORIGIN || '*',
credentials: true,
})
);
-app.use(express.json({ limit: "50mb" }));
+app.use(express.json({ limit: '50mb' }));
// Create shared event emitter for streaming
const events: EventEmitter = createEventEmitter();
@@ -118,33 +119,34 @@ const claudeUsageService = new ClaudeUsageService();
// Initialize services
(async () => {
await agentService.initialize();
- console.log("[Server] Agent service initialized");
+ console.log('[Server] Agent service initialized');
})();
// Mount API routes - health is unauthenticated for monitoring
-app.use("/api/health", createHealthRoutes());
+app.use('/api/health', createHealthRoutes());
// Apply authentication to all other routes
-app.use("/api", authMiddleware);
+app.use('/api', authMiddleware);
-app.use("/api/fs", createFsRoutes(events));
-app.use("/api/agent", createAgentRoutes(agentService, events));
-app.use("/api/sessions", createSessionsRoutes(agentService));
-app.use("/api/features", createFeaturesRoutes(featureLoader));
-app.use("/api/auto-mode", createAutoModeRoutes(autoModeService));
-app.use("/api/enhance-prompt", createEnhancePromptRoutes());
-app.use("/api/worktree", createWorktreeRoutes());
-app.use("/api/git", createGitRoutes());
-app.use("/api/setup", createSetupRoutes());
-app.use("/api/suggestions", createSuggestionsRoutes(events));
-app.use("/api/models", createModelsRoutes());
-app.use("/api/spec-regeneration", createSpecRegenerationRoutes(events));
-app.use("/api/running-agents", createRunningAgentsRoutes(autoModeService));
-app.use("/api/workspace", createWorkspaceRoutes());
-app.use("/api/templates", createTemplatesRoutes());
-app.use("/api/terminal", createTerminalRoutes());
-app.use("/api/settings", createSettingsRoutes(settingsService));
-app.use("/api/claude", createClaudeRoutes(claudeUsageService));
+app.use('/api/fs', createFsRoutes(events));
+app.use('/api/agent', createAgentRoutes(agentService, events));
+app.use('/api/sessions', createSessionsRoutes(agentService));
+app.use('/api/features', createFeaturesRoutes(featureLoader));
+app.use('/api/auto-mode', createAutoModeRoutes(autoModeService));
+app.use('/api/enhance-prompt', createEnhancePromptRoutes());
+app.use('/api/worktree', createWorktreeRoutes());
+app.use('/api/git', createGitRoutes());
+app.use('/api/setup', createSetupRoutes());
+app.use('/api/suggestions', createSuggestionsRoutes(events));
+app.use('/api/models', createModelsRoutes());
+app.use('/api/spec-regeneration', createSpecRegenerationRoutes(events));
+app.use('/api/running-agents', createRunningAgentsRoutes(autoModeService));
+app.use('/api/workspace', createWorkspaceRoutes());
+app.use('/api/templates', createTemplatesRoutes());
+app.use('/api/terminal', createTerminalRoutes());
+app.use('/api/settings', createSettingsRoutes(settingsService));
+app.use('/api/claude', createClaudeRoutes(claudeUsageService));
+app.use('/api/context', createContextRoutes());
// Create HTTP server
const server = createServer(app);
@@ -155,19 +157,16 @@ const terminalWss = new WebSocketServer({ noServer: true });
const terminalService = getTerminalService();
// Handle HTTP upgrade requests manually to route to correct WebSocket server
-server.on("upgrade", (request, socket, head) => {
- const { pathname } = new URL(
- request.url || "",
- `http://${request.headers.host}`
- );
+server.on('upgrade', (request, socket, head) => {
+ const { pathname } = new URL(request.url || '', `http://${request.headers.host}`);
- if (pathname === "/api/events") {
+ if (pathname === '/api/events') {
wss.handleUpgrade(request, socket, head, (ws) => {
- wss.emit("connection", ws, request);
+ wss.emit('connection', ws, request);
});
- } else if (pathname === "/api/terminal/ws") {
+ } else if (pathname === '/api/terminal/ws') {
terminalWss.handleUpgrade(request, socket, head, (ws) => {
- terminalWss.emit("connection", ws, request);
+ terminalWss.emit('connection', ws, request);
});
} else {
socket.destroy();
@@ -175,8 +174,8 @@ server.on("upgrade", (request, socket, head) => {
});
// Events WebSocket connection handler
-wss.on("connection", (ws: WebSocket) => {
- console.log("[WebSocket] Client connected");
+wss.on('connection', (ws: WebSocket) => {
+ console.log('[WebSocket] Client connected');
// Subscribe to all events and forward to this client
const unsubscribe = events.subscribe((type, payload) => {
@@ -185,13 +184,13 @@ wss.on("connection", (ws: WebSocket) => {
}
});
- ws.on("close", () => {
- console.log("[WebSocket] Client disconnected");
+ ws.on('close', () => {
+ console.log('[WebSocket] Client disconnected');
unsubscribe();
});
- ws.on("error", (error) => {
- console.error("[WebSocket] Error:", error);
+ ws.on('error', (error) => {
+ console.error('[WebSocket] Error:', error);
unsubscribe();
});
});
@@ -212,184 +211,199 @@ terminalService.onExit((sessionId) => {
});
// Terminal WebSocket connection handler
-terminalWss.on(
- "connection",
- (ws: WebSocket, req: import("http").IncomingMessage) => {
- // Parse URL to get session ID and token
- const url = new URL(req.url || "", `http://${req.headers.host}`);
- const sessionId = url.searchParams.get("sessionId");
- const token = url.searchParams.get("token");
+terminalWss.on('connection', (ws: WebSocket, req: import('http').IncomingMessage) => {
+ // Parse URL to get session ID and token
+ const url = new URL(req.url || '', `http://${req.headers.host}`);
+ const sessionId = url.searchParams.get('sessionId');
+ const token = url.searchParams.get('token');
- console.log(`[Terminal WS] Connection attempt for session: ${sessionId}`);
+ console.log(`[Terminal WS] Connection attempt for session: ${sessionId}`);
- // Check if terminal is enabled
- if (!isTerminalEnabled()) {
- console.log("[Terminal WS] Terminal is disabled");
- ws.close(4003, "Terminal access is disabled");
- return;
- }
+ // Check if terminal is enabled
+ if (!isTerminalEnabled()) {
+ console.log('[Terminal WS] Terminal is disabled');
+ ws.close(4003, 'Terminal access is disabled');
+ return;
+ }
- // Validate token if password is required
- if (
- isTerminalPasswordRequired() &&
- !validateTerminalToken(token || undefined)
- ) {
- console.log("[Terminal WS] Invalid or missing token");
- ws.close(4001, "Authentication required");
- return;
- }
+ // Validate token if password is required
+ if (isTerminalPasswordRequired() && !validateTerminalToken(token || undefined)) {
+ console.log('[Terminal WS] Invalid or missing token');
+ ws.close(4001, 'Authentication required');
+ return;
+ }
- if (!sessionId) {
- console.log("[Terminal WS] No session ID provided");
- ws.close(4002, "Session ID required");
- return;
- }
+ if (!sessionId) {
+ console.log('[Terminal WS] No session ID provided');
+ ws.close(4002, 'Session ID required');
+ return;
+ }
- // Check if session exists
- const session = terminalService.getSession(sessionId);
- if (!session) {
- console.log(`[Terminal WS] Session ${sessionId} not found`);
- ws.close(4004, "Session not found");
- return;
- }
+ // Check if session exists
+ const session = terminalService.getSession(sessionId);
+ if (!session) {
+ console.log(`[Terminal WS] Session ${sessionId} not found`);
+ ws.close(4004, 'Session not found');
+ return;
+ }
- console.log(`[Terminal WS] Client connected to session ${sessionId}`);
+ console.log(`[Terminal WS] Client connected to session ${sessionId}`);
- // Track this connection
- if (!terminalConnections.has(sessionId)) {
- terminalConnections.set(sessionId, new Set());
- }
- terminalConnections.get(sessionId)!.add(ws);
+ // Track this connection
+ if (!terminalConnections.has(sessionId)) {
+ terminalConnections.set(sessionId, new Set());
+ }
+ terminalConnections.get(sessionId)!.add(ws);
- // Send initial connection success FIRST
+ // Send initial connection success FIRST
+ ws.send(
+ JSON.stringify({
+ type: 'connected',
+ sessionId,
+ shell: session.shell,
+ cwd: session.cwd,
+ })
+ );
+
+ // Send scrollback buffer BEFORE subscribing to prevent race condition
+ // Also clear pending output buffer to prevent duplicates from throttled flush
+ const scrollback = terminalService.getScrollbackAndClearPending(sessionId);
+ if (scrollback && scrollback.length > 0) {
ws.send(
JSON.stringify({
- type: "connected",
- sessionId,
- shell: session.shell,
- cwd: session.cwd,
+ type: 'scrollback',
+ data: scrollback,
})
);
-
- // Send scrollback buffer BEFORE subscribing to prevent race condition
- // Also clear pending output buffer to prevent duplicates from throttled flush
- const scrollback = terminalService.getScrollbackAndClearPending(sessionId);
- if (scrollback && scrollback.length > 0) {
- ws.send(
- JSON.stringify({
- type: "scrollback",
- data: scrollback,
- })
- );
- }
-
- // NOW subscribe to terminal data (after scrollback is sent)
- const unsubscribeData = terminalService.onData((sid, data) => {
- if (sid === sessionId && ws.readyState === WebSocket.OPEN) {
- ws.send(JSON.stringify({ type: "data", data }));
- }
- });
-
- // Subscribe to terminal exit
- const unsubscribeExit = terminalService.onExit((sid, exitCode) => {
- if (sid === sessionId && ws.readyState === WebSocket.OPEN) {
- ws.send(JSON.stringify({ type: "exit", exitCode }));
- ws.close(1000, "Session ended");
- }
- });
-
- // Handle incoming messages
- ws.on("message", (message) => {
- try {
- const msg = JSON.parse(message.toString());
-
- switch (msg.type) {
- case "input":
- // Write user input to terminal
- terminalService.write(sessionId, msg.data);
- break;
-
- case "resize":
- // Resize terminal with deduplication and rate limiting
- if (msg.cols && msg.rows) {
- const now = Date.now();
- const lastTime = lastResizeTime.get(sessionId) || 0;
- const lastDimensions = lastResizeDimensions.get(sessionId);
-
- // Skip if resized too recently (prevents resize storm during splits)
- if (now - lastTime < RESIZE_MIN_INTERVAL_MS) {
- break;
- }
-
- // Check if dimensions are different from last resize
- if (
- !lastDimensions ||
- lastDimensions.cols !== msg.cols ||
- lastDimensions.rows !== msg.rows
- ) {
- // Only suppress output on subsequent resizes, not the first one
- // The first resize happens on terminal open and we don't want to drop the initial prompt
- const isFirstResize = !lastDimensions;
- terminalService.resize(sessionId, msg.cols, msg.rows, !isFirstResize);
- lastResizeDimensions.set(sessionId, {
- cols: msg.cols,
- rows: msg.rows,
- });
- lastResizeTime.set(sessionId, now);
- }
- }
- break;
-
- case "ping":
- // Respond to ping
- ws.send(JSON.stringify({ type: "pong" }));
- break;
-
- default:
- console.warn(`[Terminal WS] Unknown message type: ${msg.type}`);
- }
- } catch (error) {
- console.error("[Terminal WS] Error processing message:", error);
- }
- });
-
- ws.on("close", () => {
- console.log(
- `[Terminal WS] Client disconnected from session ${sessionId}`
- );
- unsubscribeData();
- unsubscribeExit();
-
- // Remove from connections tracking
- const connections = terminalConnections.get(sessionId);
- if (connections) {
- connections.delete(ws);
- if (connections.size === 0) {
- terminalConnections.delete(sessionId);
- // DON'T delete lastResizeDimensions/lastResizeTime here!
- // The session still exists, and reconnecting clients need to know
- // this isn't the "first resize" to prevent duplicate prompts.
- // These get cleaned up when the session actually exits.
- }
- }
- });
-
- ws.on("error", (error) => {
- console.error(`[Terminal WS] Error on session ${sessionId}:`, error);
- unsubscribeData();
- unsubscribeExit();
- });
}
-);
+
+ // NOW subscribe to terminal data (after scrollback is sent)
+ const unsubscribeData = terminalService.onData((sid, data) => {
+ if (sid === sessionId && ws.readyState === WebSocket.OPEN) {
+ ws.send(JSON.stringify({ type: 'data', data }));
+ }
+ });
+
+ // Subscribe to terminal exit
+ const unsubscribeExit = terminalService.onExit((sid, exitCode) => {
+ if (sid === sessionId && ws.readyState === WebSocket.OPEN) {
+ ws.send(JSON.stringify({ type: 'exit', exitCode }));
+ ws.close(1000, 'Session ended');
+ }
+ });
+
+ // Handle incoming messages
+ ws.on('message', (message) => {
+ try {
+ const msg = JSON.parse(message.toString());
+
+ switch (msg.type) {
+ case 'input':
+ // Validate input data type and length
+ if (typeof msg.data !== 'string') {
+ ws.send(JSON.stringify({ type: 'error', message: 'Invalid input type' }));
+ break;
+ }
+ // Limit input size to 1MB to prevent memory issues
+ if (msg.data.length > 1024 * 1024) {
+ ws.send(JSON.stringify({ type: 'error', message: 'Input too large' }));
+ break;
+ }
+ // Write user input to terminal
+ terminalService.write(sessionId, msg.data);
+ break;
+
+ case 'resize':
+ // Validate resize dimensions are positive integers within reasonable bounds
+ if (
+ typeof msg.cols !== 'number' ||
+ typeof msg.rows !== 'number' ||
+ !Number.isInteger(msg.cols) ||
+ !Number.isInteger(msg.rows) ||
+ msg.cols < 1 ||
+ msg.cols > 1000 ||
+ msg.rows < 1 ||
+ msg.rows > 500
+ ) {
+ break; // Silently ignore invalid resize requests
+ }
+ // Resize terminal with deduplication and rate limiting
+ if (msg.cols && msg.rows) {
+ const now = Date.now();
+ const lastTime = lastResizeTime.get(sessionId) || 0;
+ const lastDimensions = lastResizeDimensions.get(sessionId);
+
+ // Skip if resized too recently (prevents resize storm during splits)
+ if (now - lastTime < RESIZE_MIN_INTERVAL_MS) {
+ break;
+ }
+
+ // Check if dimensions are different from last resize
+ if (
+ !lastDimensions ||
+ lastDimensions.cols !== msg.cols ||
+ lastDimensions.rows !== msg.rows
+ ) {
+ // Only suppress output on subsequent resizes, not the first one
+ // The first resize happens on terminal open and we don't want to drop the initial prompt
+ const isFirstResize = !lastDimensions;
+ terminalService.resize(sessionId, msg.cols, msg.rows, !isFirstResize);
+ lastResizeDimensions.set(sessionId, {
+ cols: msg.cols,
+ rows: msg.rows,
+ });
+ lastResizeTime.set(sessionId, now);
+ }
+ }
+ break;
+
+ case 'ping':
+ // Respond to ping
+ ws.send(JSON.stringify({ type: 'pong' }));
+ break;
+
+ default:
+ console.warn(`[Terminal WS] Unknown message type: ${msg.type}`);
+ }
+ } catch (error) {
+ console.error('[Terminal WS] Error processing message:', error);
+ }
+ });
+
+ ws.on('close', () => {
+ console.log(`[Terminal WS] Client disconnected from session ${sessionId}`);
+ unsubscribeData();
+ unsubscribeExit();
+
+ // Remove from connections tracking
+ const connections = terminalConnections.get(sessionId);
+ if (connections) {
+ connections.delete(ws);
+ if (connections.size === 0) {
+ terminalConnections.delete(sessionId);
+ // DON'T delete lastResizeDimensions/lastResizeTime here!
+ // The session still exists, and reconnecting clients need to know
+ // this isn't the "first resize" to prevent duplicate prompts.
+ // These get cleaned up when the session actually exits.
+ }
+ }
+ });
+
+ ws.on('error', (error) => {
+ console.error(`[Terminal WS] Error on session ${sessionId}:`, error);
+ unsubscribeData();
+ unsubscribeExit();
+ });
+});
// Start server with error handling for port conflicts
const startServer = (port: number) => {
server.listen(port, () => {
const terminalStatus = isTerminalEnabled()
? isTerminalPasswordRequired()
- ? "enabled (password protected)"
- : "enabled"
- : "disabled";
+ ? 'enabled (password protected)'
+ : 'enabled'
+ : 'disabled';
const portStr = port.toString().padEnd(4);
console.log(`
╔═══════════════════════════════════════════════════════╗
@@ -404,8 +418,8 @@ const startServer = (port: number) => {
`);
});
- server.on("error", (error: NodeJS.ErrnoException) => {
- if (error.code === "EADDRINUSE") {
+ server.on('error', (error: NodeJS.ErrnoException) => {
+ if (error.code === 'EADDRINUSE') {
console.error(`
╔═══════════════════════════════════════════════════════╗
║ ❌ ERROR: Port ${port} is already in use ║
@@ -426,7 +440,7 @@ const startServer = (port: number) => {
`);
process.exit(1);
} else {
- console.error("[Server] Error starting server:", error);
+ console.error('[Server] Error starting server:', error);
process.exit(1);
}
});
@@ -435,20 +449,20 @@ const startServer = (port: number) => {
startServer(PORT);
// Graceful shutdown
-process.on("SIGTERM", () => {
- console.log("SIGTERM received, shutting down...");
+process.on('SIGTERM', () => {
+ console.log('SIGTERM received, shutting down...');
terminalService.cleanup();
server.close(() => {
- console.log("Server closed");
+ console.log('Server closed');
process.exit(0);
});
});
-process.on("SIGINT", () => {
- console.log("SIGINT received, shutting down...");
+process.on('SIGINT', () => {
+ console.log('SIGINT received, shutting down...');
terminalService.cleanup();
server.close(() => {
- console.log("Server closed");
+ console.log('Server closed');
process.exit(0);
});
});
diff --git a/apps/server/src/lib/app-spec-format.ts b/apps/server/src/lib/app-spec-format.ts
index 523af533..2894bbc4 100644
--- a/apps/server/src/lib/app-spec-format.ts
+++ b/apps/server/src/lib/app-spec-format.ts
@@ -6,26 +6,26 @@
*/
// Import and re-export spec types from shared package
-export type { SpecOutput } from "@automaker/types";
-export { specOutputSchema } from "@automaker/types";
+export type { SpecOutput } from '@automaker/types';
+export { specOutputSchema } from '@automaker/types';
/**
* Escape special XML characters
*/
function escapeXml(str: string): string {
return str
- .replace(/&/g, "&")
- .replace(//g, ">")
- .replace(/"/g, """)
- .replace(/'/g, "'");
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
}
/**
* Convert structured spec output to XML format
*/
-export function specToXml(spec: import("@automaker/types").SpecOutput): string {
- const indent = " ";
+export function specToXml(spec: import('@automaker/types').SpecOutput): string {
+ const indent = ' ';
let xml = `
@@ -36,11 +36,11 @@ ${indent}${indent}${escapeXml(spec.overview)}
${indent}
${indent}
-${spec.technology_stack.map((t) => `${indent}${indent}${escapeXml(t)}`).join("\n")}
+${spec.technology_stack.map((t) => `${indent}${indent}${escapeXml(t)}`).join('\n')}
${indent}
${indent}
-${spec.core_capabilities.map((c) => `${indent}${indent}${escapeXml(c)}`).join("\n")}
+${spec.core_capabilities.map((c) => `${indent}${indent}${escapeXml(c)}`).join('\n')}
${indent}
${indent}
@@ -51,13 +51,13 @@ ${indent}${indent}${indent}${escapeXml(f.name)}
${indent}${indent}${indent}${escapeXml(f.description)}${
f.file_locations && f.file_locations.length > 0
? `\n${indent}${indent}${indent}
-${f.file_locations.map((loc) => `${indent}${indent}${indent}${indent}${escapeXml(loc)}`).join("\n")}
+${f.file_locations.map((loc) => `${indent}${indent}${indent}${indent}${escapeXml(loc)}`).join('\n')}
${indent}${indent}${indent}`
- : ""
+ : ''
}
${indent}${indent}`
)
- .join("\n")}
+ .join('\n')}
${indent}`;
// Optional sections
@@ -65,7 +65,7 @@ ${indent}`;
xml += `
${indent}
-${spec.additional_requirements.map((r) => `${indent}${indent}${escapeXml(r)}`).join("\n")}
+${spec.additional_requirements.map((r) => `${indent}${indent}${escapeXml(r)}`).join('\n')}
${indent}`;
}
@@ -73,7 +73,7 @@ ${indent}`;
xml += `
${indent}
-${spec.development_guidelines.map((g) => `${indent}${indent}${escapeXml(g)}`).join("\n")}
+${spec.development_guidelines.map((g) => `${indent}${indent}${escapeXml(g)}`).join('\n')}
${indent}`;
}
@@ -89,7 +89,7 @@ ${indent}${indent}${indent}${escapeXml(r.status)}
${indent}${indent}${indent}${escapeXml(r.description)}
${indent}${indent}`
)
- .join("\n")}
+ .join('\n')}
${indent}`;
}
diff --git a/apps/server/src/lib/auth.ts b/apps/server/src/lib/auth.ts
index 331af2cf..145c7b9d 100644
--- a/apps/server/src/lib/auth.ts
+++ b/apps/server/src/lib/auth.ts
@@ -4,7 +4,7 @@
* Supports API key authentication via header or environment variable.
*/
-import type { Request, Response, NextFunction } from "express";
+import type { Request, Response, NextFunction } from 'express';
// API key from environment (optional - if not set, auth is disabled)
const API_KEY = process.env.AUTOMAKER_API_KEY;
@@ -23,12 +23,12 @@ export function authMiddleware(req: Request, res: Response, next: NextFunction):
}
// Check for API key in header
- const providedKey = req.headers["x-api-key"] as string | undefined;
+ const providedKey = req.headers['x-api-key'] as string | undefined;
if (!providedKey) {
res.status(401).json({
success: false,
- error: "Authentication required. Provide X-API-Key header.",
+ error: 'Authentication required. Provide X-API-Key header.',
});
return;
}
@@ -36,7 +36,7 @@ export function authMiddleware(req: Request, res: Response, next: NextFunction):
if (providedKey !== API_KEY) {
res.status(403).json({
success: false,
- error: "Invalid API key.",
+ error: 'Invalid API key.',
});
return;
}
@@ -57,6 +57,6 @@ export function isAuthEnabled(): boolean {
export function getAuthStatus(): { enabled: boolean; method: string } {
return {
enabled: !!API_KEY,
- method: API_KEY ? "api_key" : "none",
+ method: API_KEY ? 'api_key' : 'none',
};
}
diff --git a/apps/server/src/lib/events.ts b/apps/server/src/lib/events.ts
index 83b21184..c7bd1512 100644
--- a/apps/server/src/lib/events.ts
+++ b/apps/server/src/lib/events.ts
@@ -2,7 +2,7 @@
* Event emitter for streaming events to WebSocket clients
*/
-import type { EventType, EventCallback } from "@automaker/types";
+import type { EventType, EventCallback } from '@automaker/types';
// Re-export event types from shared package
export type { EventType, EventCallback };
@@ -21,7 +21,7 @@ export function createEventEmitter(): EventEmitter {
try {
callback(type, payload);
} catch (error) {
- console.error("Error in event subscriber:", error);
+ console.error('Error in event subscriber:', error);
}
}
},
diff --git a/apps/server/src/lib/secure-fs.ts b/apps/server/src/lib/secure-fs.ts
index eab1be0a..cf927cbd 100644
--- a/apps/server/src/lib/secure-fs.ts
+++ b/apps/server/src/lib/secure-fs.ts
@@ -3,7 +3,7 @@
* This file exists for backward compatibility with existing imports
*/
-import { secureFs } from "@automaker/platform";
+import { secureFs } from '@automaker/platform';
export const {
access,
diff --git a/apps/server/src/middleware/validate-paths.ts b/apps/server/src/middleware/validate-paths.ts
index 5973451f..51b8ccb1 100644
--- a/apps/server/src/middleware/validate-paths.ts
+++ b/apps/server/src/middleware/validate-paths.ts
@@ -4,8 +4,8 @@
* try-catch block in every route handler
*/
-import type { Request, Response, NextFunction } from "express";
-import { validatePath, PathNotAllowedError } from "@automaker/platform";
+import type { Request, Response, NextFunction } from 'express';
+import { validatePath, PathNotAllowedError } from '@automaker/platform';
/**
* Creates a middleware that validates specified path parameters in req.body
@@ -24,7 +24,7 @@ export function validatePathParams(...paramNames: string[]) {
try {
for (const paramName of paramNames) {
// Handle optional parameters (paramName?)
- if (paramName.endsWith("?")) {
+ if (paramName.endsWith('?')) {
const actualName = paramName.slice(0, -1);
const value = req.body[actualName];
if (value) {
@@ -34,7 +34,7 @@ export function validatePathParams(...paramNames: string[]) {
}
// Handle array parameters (paramName[])
- if (paramName.endsWith("[]")) {
+ if (paramName.endsWith('[]')) {
const actualName = paramName.slice(0, -2);
const values = req.body[actualName];
if (Array.isArray(values) && values.length > 0) {
diff --git a/apps/server/src/providers/base-provider.ts b/apps/server/src/providers/base-provider.ts
index f481b83c..2b1880d3 100644
--- a/apps/server/src/providers/base-provider.ts
+++ b/apps/server/src/providers/base-provider.ts
@@ -9,7 +9,7 @@ import type {
InstallationStatus,
ValidationResult,
ModelDefinition,
-} from "./types.js";
+} from './types.js';
/**
* Base provider class that all provider implementations must extend
@@ -33,9 +33,7 @@ export abstract class BaseProvider {
* @param options Execution options
* @returns AsyncGenerator yielding provider messages
*/
- abstract executeQuery(
- options: ExecuteOptions
- ): AsyncGenerator;
+ abstract executeQuery(options: ExecuteOptions): AsyncGenerator;
/**
* Detect if the provider is installed and configured
@@ -59,7 +57,7 @@ export abstract class BaseProvider {
// Base validation (can be overridden)
if (!this.config) {
- errors.push("Provider config is missing");
+ errors.push('Provider config is missing');
}
return {
@@ -76,7 +74,7 @@ export abstract class BaseProvider {
*/
supportsFeature(feature: string): boolean {
// Default implementation - override in subclasses
- const commonFeatures = ["tools", "text"];
+ const commonFeatures = ['tools', 'text'];
return commonFeatures.includes(feature);
}
diff --git a/apps/server/src/providers/claude-provider.ts b/apps/server/src/providers/claude-provider.ts
index ea8471e1..2ed2728d 100644
--- a/apps/server/src/providers/claude-provider.ts
+++ b/apps/server/src/providers/claude-provider.ts
@@ -5,26 +5,24 @@
* with the provider architecture.
*/
-import { query, type Options } from "@anthropic-ai/claude-agent-sdk";
-import { BaseProvider } from "./base-provider.js";
+import { query, type Options } from '@anthropic-ai/claude-agent-sdk';
+import { BaseProvider } from './base-provider.js';
import type {
ExecuteOptions,
ProviderMessage,
InstallationStatus,
ModelDefinition,
-} from "./types.js";
+} from './types.js';
export class ClaudeProvider extends BaseProvider {
getName(): string {
- return "claude";
+ return 'claude';
}
/**
* Execute a query using Claude Agent SDK
*/
- async *executeQuery(
- options: ExecuteOptions
- ): AsyncGenerator {
+ async *executeQuery(options: ExecuteOptions): AsyncGenerator {
const {
prompt,
model,
@@ -38,16 +36,7 @@ export class ClaudeProvider extends BaseProvider {
} = options;
// Build Claude SDK options
- const defaultTools = [
- "Read",
- "Write",
- "Edit",
- "Glob",
- "Grep",
- "Bash",
- "WebSearch",
- "WebFetch",
- ];
+ const defaultTools = ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch'];
const toolsToUse = allowedTools || defaultTools;
const sdkOptions: Options = {
@@ -56,7 +45,7 @@ export class ClaudeProvider extends BaseProvider {
maxTurns,
cwd,
allowedTools: toolsToUse,
- permissionMode: "acceptEdits",
+ permissionMode: 'acceptEdits',
sandbox: {
enabled: true,
autoAllowBashIfSandboxed: true,
@@ -75,10 +64,10 @@ export class ClaudeProvider extends BaseProvider {
// Multi-part prompt (with images)
promptPayload = (async function* () {
const multiPartPrompt = {
- type: "user" as const,
- session_id: "",
+ type: 'user' as const,
+ session_id: '',
message: {
- role: "user" as const,
+ role: 'user' as const,
content: prompt,
},
parent_tool_use_id: null,
@@ -99,10 +88,7 @@ export class ClaudeProvider extends BaseProvider {
yield msg as ProviderMessage;
}
} catch (error) {
- console.error(
- "[ClaudeProvider] executeQuery() error during execution:",
- error
- );
+ console.error('[ClaudeProvider] executeQuery() error during execution:', error);
throw error;
}
}
@@ -116,7 +102,7 @@ export class ClaudeProvider extends BaseProvider {
const status: InstallationStatus = {
installed: true,
- method: "sdk",
+ method: 'sdk',
hasApiKey,
authenticated: hasApiKey,
};
@@ -130,53 +116,53 @@ export class ClaudeProvider extends BaseProvider {
getAvailableModels(): ModelDefinition[] {
const models = [
{
- id: "claude-opus-4-5-20251101",
- name: "Claude Opus 4.5",
- modelString: "claude-opus-4-5-20251101",
- provider: "anthropic",
- description: "Most capable Claude model",
+ id: 'claude-opus-4-5-20251101',
+ name: 'Claude Opus 4.5',
+ modelString: 'claude-opus-4-5-20251101',
+ provider: 'anthropic',
+ description: 'Most capable Claude model',
contextWindow: 200000,
maxOutputTokens: 16000,
supportsVision: true,
supportsTools: true,
- tier: "premium" as const,
+ tier: 'premium' as const,
default: true,
},
{
- id: "claude-sonnet-4-20250514",
- name: "Claude Sonnet 4",
- modelString: "claude-sonnet-4-20250514",
- provider: "anthropic",
- description: "Balanced performance and cost",
+ id: 'claude-sonnet-4-20250514',
+ name: 'Claude Sonnet 4',
+ modelString: 'claude-sonnet-4-20250514',
+ provider: 'anthropic',
+ description: 'Balanced performance and cost',
contextWindow: 200000,
maxOutputTokens: 16000,
supportsVision: true,
supportsTools: true,
- tier: "standard" as const,
+ tier: 'standard' as const,
},
{
- id: "claude-3-5-sonnet-20241022",
- name: "Claude 3.5 Sonnet",
- modelString: "claude-3-5-sonnet-20241022",
- provider: "anthropic",
- description: "Fast and capable",
+ id: 'claude-3-5-sonnet-20241022',
+ name: 'Claude 3.5 Sonnet',
+ modelString: 'claude-3-5-sonnet-20241022',
+ provider: 'anthropic',
+ description: 'Fast and capable',
contextWindow: 200000,
maxOutputTokens: 8000,
supportsVision: true,
supportsTools: true,
- tier: "standard" as const,
+ tier: 'standard' as const,
},
{
- id: "claude-3-5-haiku-20241022",
- name: "Claude 3.5 Haiku",
- modelString: "claude-3-5-haiku-20241022",
- provider: "anthropic",
- description: "Fastest Claude model",
+ id: 'claude-haiku-4-5-20251001',
+ name: 'Claude Haiku 4.5',
+ modelString: 'claude-haiku-4-5-20251001',
+ provider: 'anthropic',
+ description: 'Fastest Claude model',
contextWindow: 200000,
maxOutputTokens: 8000,
supportsVision: true,
supportsTools: true,
- tier: "basic" as const,
+ tier: 'basic' as const,
},
] satisfies ModelDefinition[];
return models;
@@ -186,7 +172,7 @@ export class ClaudeProvider extends BaseProvider {
* Check if the provider supports a specific feature
*/
supportsFeature(feature: string): boolean {
- const supportedFeatures = ["tools", "text", "vision", "thinking"];
+ const supportedFeatures = ['tools', 'text', 'vision', 'thinking'];
return supportedFeatures.includes(feature);
}
}
diff --git a/apps/server/src/providers/provider-factory.ts b/apps/server/src/providers/provider-factory.ts
index f45bf008..0ef9b36e 100644
--- a/apps/server/src/providers/provider-factory.ts
+++ b/apps/server/src/providers/provider-factory.ts
@@ -6,9 +6,9 @@
* new providers (Cursor, OpenCode, etc.) trivial - just add one line.
*/
-import { BaseProvider } from "./base-provider.js";
-import { ClaudeProvider } from "./claude-provider.js";
-import type { InstallationStatus } from "./types.js";
+import { BaseProvider } from './base-provider.js';
+import { ClaudeProvider } from './claude-provider.js';
+import type { InstallationStatus } from './types.js';
export class ProviderFactory {
/**
@@ -21,10 +21,7 @@ export class ProviderFactory {
const lowerModel = modelId.toLowerCase();
// Claude models (claude-*, opus, sonnet, haiku)
- if (
- lowerModel.startsWith("claude-") ||
- ["haiku", "sonnet", "opus"].includes(lowerModel)
- ) {
+ if (lowerModel.startsWith('claude-') || ['haiku', 'sonnet', 'opus'].includes(lowerModel)) {
return new ClaudeProvider();
}
@@ -37,9 +34,7 @@ export class ProviderFactory {
// }
// Default to Claude for unknown models
- console.warn(
- `[ProviderFactory] Unknown model prefix for "${modelId}", defaulting to Claude`
- );
+ console.warn(`[ProviderFactory] Unknown model prefix for "${modelId}", defaulting to Claude`);
return new ClaudeProvider();
}
@@ -58,9 +53,7 @@ export class ProviderFactory {
*
* @returns Map of provider name to installation status
*/
- static async checkAllProviders(): Promise<
- Record
- > {
+ static async checkAllProviders(): Promise> {
const providers = this.getAllProviders();
const statuses: Record = {};
@@ -83,8 +76,8 @@ export class ProviderFactory {
const lowerName = name.toLowerCase();
switch (lowerName) {
- case "claude":
- case "anthropic":
+ case 'claude':
+ case 'anthropic':
return new ClaudeProvider();
// Future providers:
diff --git a/apps/server/src/providers/types.ts b/apps/server/src/providers/types.ts
index 6a05b6df..f3aa22d5 100644
--- a/apps/server/src/providers/types.ts
+++ b/apps/server/src/providers/types.ts
@@ -15,7 +15,7 @@ export interface ProviderConfig {
* Message in conversation history
*/
export interface ConversationMessage {
- role: "user" | "assistant";
+ role: 'user' | 'assistant';
content: string | Array<{ type: string; text?: string; source?: object }>;
}
@@ -39,7 +39,7 @@ export interface ExecuteOptions {
* Content block in a provider message (matches Claude SDK format)
*/
export interface ContentBlock {
- type: "text" | "tool_use" | "thinking" | "tool_result";
+ type: 'text' | 'tool_use' | 'thinking' | 'tool_result';
text?: string;
thinking?: string;
name?: string;
@@ -52,11 +52,11 @@ export interface ContentBlock {
* Message returned by a provider (matches Claude SDK streaming format)
*/
export interface ProviderMessage {
- type: "assistant" | "user" | "error" | "result";
- subtype?: "success" | "error";
+ type: 'assistant' | 'user' | 'error' | 'result';
+ subtype?: 'success' | 'error';
session_id?: string;
message?: {
- role: "user" | "assistant";
+ role: 'user' | 'assistant';
content: ContentBlock[];
};
result?: string;
@@ -71,7 +71,7 @@ export interface InstallationStatus {
installed: boolean;
path?: string;
version?: string;
- method?: "cli" | "npm" | "brew" | "sdk";
+ method?: 'cli' | 'npm' | 'brew' | 'sdk';
hasApiKey?: boolean;
authenticated?: boolean;
error?: string;
@@ -99,6 +99,6 @@ export interface ModelDefinition {
maxOutputTokens?: number;
supportsVision?: boolean;
supportsTools?: boolean;
- tier?: "basic" | "standard" | "premium";
+ tier?: 'basic' | 'standard' | 'premium';
default?: boolean;
}
diff --git a/apps/server/src/routes/agent/common.ts b/apps/server/src/routes/agent/common.ts
index 0eeeacf0..9b24a76a 100644
--- a/apps/server/src/routes/agent/common.ts
+++ b/apps/server/src/routes/agent/common.ts
@@ -2,13 +2,10 @@
* Common utilities for agent routes
*/
-import { createLogger } from "@automaker/utils";
-import {
- getErrorMessage as getErrorMessageShared,
- createLogError,
-} from "../common.js";
+import { createLogger } from '@automaker/utils';
+import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
-const logger = createLogger("Agent");
+const logger = createLogger('Agent');
// Re-export shared utilities
export { getErrorMessageShared as getErrorMessage };
diff --git a/apps/server/src/routes/agent/index.ts b/apps/server/src/routes/agent/index.ts
index 61f34656..0ddf8741 100644
--- a/apps/server/src/routes/agent/index.ts
+++ b/apps/server/src/routes/agent/index.ts
@@ -2,29 +2,30 @@
* Agent routes - HTTP API for Claude agent interactions
*/
-import { Router } from "express";
-import { AgentService } from "../../services/agent-service.js";
-import type { EventEmitter } from "../../lib/events.js";
-import { validatePathParams } from "../../middleware/validate-paths.js";
-import { createStartHandler } from "./routes/start.js";
-import { createSendHandler } from "./routes/send.js";
-import { createHistoryHandler } from "./routes/history.js";
-import { createStopHandler } from "./routes/stop.js";
-import { createClearHandler } from "./routes/clear.js";
-import { createModelHandler } from "./routes/model.js";
+import { Router } from 'express';
+import { AgentService } from '../../services/agent-service.js';
+import type { EventEmitter } from '../../lib/events.js';
+import { validatePathParams } from '../../middleware/validate-paths.js';
+import { createStartHandler } from './routes/start.js';
+import { createSendHandler } from './routes/send.js';
+import { createHistoryHandler } from './routes/history.js';
+import { createStopHandler } from './routes/stop.js';
+import { createClearHandler } from './routes/clear.js';
+import { createModelHandler } from './routes/model.js';
-export function createAgentRoutes(
- agentService: AgentService,
- _events: EventEmitter
-): Router {
+export function createAgentRoutes(agentService: AgentService, _events: EventEmitter): Router {
const router = Router();
- router.post("/start", validatePathParams("workingDirectory?"), createStartHandler(agentService));
- router.post("/send", validatePathParams("workingDirectory?", "imagePaths[]"), createSendHandler(agentService));
- router.post("/history", createHistoryHandler(agentService));
- router.post("/stop", createStopHandler(agentService));
- router.post("/clear", createClearHandler(agentService));
- router.post("/model", createModelHandler(agentService));
+ router.post('/start', validatePathParams('workingDirectory?'), createStartHandler(agentService));
+ router.post(
+ '/send',
+ validatePathParams('workingDirectory?', 'imagePaths[]'),
+ createSendHandler(agentService)
+ );
+ router.post('/history', createHistoryHandler(agentService));
+ router.post('/stop', createStopHandler(agentService));
+ router.post('/clear', createClearHandler(agentService));
+ router.post('/model', createModelHandler(agentService));
return router;
}
diff --git a/apps/server/src/routes/agent/routes/clear.ts b/apps/server/src/routes/agent/routes/clear.ts
index 42418331..3ee605b6 100644
--- a/apps/server/src/routes/agent/routes/clear.ts
+++ b/apps/server/src/routes/agent/routes/clear.ts
@@ -2,9 +2,9 @@
* POST /clear endpoint - Clear conversation
*/
-import type { Request, Response } from "express";
-import { AgentService } from "../../../services/agent-service.js";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import { AgentService } from '../../../services/agent-service.js';
+import { getErrorMessage, logError } from '../common.js';
export function createClearHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise => {
@@ -12,16 +12,14 @@ export function createClearHandler(agentService: AgentService) {
const { sessionId } = req.body as { sessionId: string };
if (!sessionId) {
- res
- .status(400)
- .json({ success: false, error: "sessionId is required" });
+ res.status(400).json({ success: false, error: 'sessionId is required' });
return;
}
const result = await agentService.clearSession(sessionId);
res.json(result);
} catch (error) {
- logError(error, "Clear session failed");
+ logError(error, 'Clear session failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/agent/routes/history.ts b/apps/server/src/routes/agent/routes/history.ts
index c2b23be8..0859a142 100644
--- a/apps/server/src/routes/agent/routes/history.ts
+++ b/apps/server/src/routes/agent/routes/history.ts
@@ -2,9 +2,9 @@
* POST /history endpoint - Get conversation history
*/
-import type { Request, Response } from "express";
-import { AgentService } from "../../../services/agent-service.js";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import { AgentService } from '../../../services/agent-service.js';
+import { getErrorMessage, logError } from '../common.js';
export function createHistoryHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise => {
@@ -12,16 +12,14 @@ export function createHistoryHandler(agentService: AgentService) {
const { sessionId } = req.body as { sessionId: string };
if (!sessionId) {
- res
- .status(400)
- .json({ success: false, error: "sessionId is required" });
+ res.status(400).json({ success: false, error: 'sessionId is required' });
return;
}
const result = agentService.getHistory(sessionId);
res.json(result);
} catch (error) {
- logError(error, "Get history failed");
+ logError(error, 'Get history failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/agent/routes/model.ts b/apps/server/src/routes/agent/routes/model.ts
index 2e1b933e..8e1a1ddd 100644
--- a/apps/server/src/routes/agent/routes/model.ts
+++ b/apps/server/src/routes/agent/routes/model.ts
@@ -2,9 +2,9 @@
* POST /model endpoint - Set session model
*/
-import type { Request, Response } from "express";
-import { AgentService } from "../../../services/agent-service.js";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import { AgentService } from '../../../services/agent-service.js';
+import { getErrorMessage, logError } from '../common.js';
export function createModelHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise => {
@@ -15,16 +15,14 @@ export function createModelHandler(agentService: AgentService) {
};
if (!sessionId || !model) {
- res
- .status(400)
- .json({ success: false, error: "sessionId and model are required" });
+ res.status(400).json({ success: false, error: 'sessionId and model are required' });
return;
}
const result = await agentService.setSessionModel(sessionId, model);
res.json({ success: result });
} catch (error) {
- logError(error, "Set session model failed");
+ logError(error, 'Set session model failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/agent/routes/send.ts b/apps/server/src/routes/agent/routes/send.ts
index b39ede76..0dd2f424 100644
--- a/apps/server/src/routes/agent/routes/send.ts
+++ b/apps/server/src/routes/agent/routes/send.ts
@@ -2,28 +2,27 @@
* POST /send endpoint - Send a message
*/
-import type { Request, Response } from "express";
-import { AgentService } from "../../../services/agent-service.js";
-import { createLogger } from "@automaker/utils";
-import { getErrorMessage, logError } from "../common.js";
-const logger = createLogger("Agent");
+import type { Request, Response } from 'express';
+import { AgentService } from '../../../services/agent-service.js';
+import { createLogger } from '@automaker/utils';
+import { getErrorMessage, logError } from '../common.js';
+const logger = createLogger('Agent');
export function createSendHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise => {
try {
- const { sessionId, message, workingDirectory, imagePaths, model } =
- req.body as {
- sessionId: string;
- message: string;
- workingDirectory?: string;
- imagePaths?: string[];
- model?: string;
- };
+ const { sessionId, message, workingDirectory, imagePaths, model } = req.body as {
+ sessionId: string;
+ message: string;
+ workingDirectory?: string;
+ imagePaths?: string[];
+ model?: string;
+ };
if (!sessionId || !message) {
res.status(400).json({
success: false,
- error: "sessionId and message are required",
+ error: 'sessionId and message are required',
});
return;
}
@@ -38,13 +37,13 @@ export function createSendHandler(agentService: AgentService) {
model,
})
.catch((error) => {
- logError(error, "Send message failed (background)");
+ logError(error, 'Send message failed (background)');
});
// Return immediately - responses come via WebSocket
- res.json({ success: true, message: "Message sent" });
+ res.json({ success: true, message: 'Message sent' });
} catch (error) {
- logError(error, "Send message failed");
+ logError(error, 'Send message failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/agent/routes/start.ts b/apps/server/src/routes/agent/routes/start.ts
index 9f7d8da5..1023fa38 100644
--- a/apps/server/src/routes/agent/routes/start.ts
+++ b/apps/server/src/routes/agent/routes/start.ts
@@ -2,11 +2,11 @@
* POST /start endpoint - Start a conversation
*/
-import type { Request, Response } from "express";
-import { AgentService } from "../../../services/agent-service.js";
-import { createLogger } from "@automaker/utils";
-import { getErrorMessage, logError } from "../common.js";
-const logger = createLogger("Agent");
+import type { Request, Response } from 'express';
+import { AgentService } from '../../../services/agent-service.js';
+import { createLogger } from '@automaker/utils';
+import { getErrorMessage, logError } from '../common.js';
+const logger = createLogger('Agent');
export function createStartHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise => {
@@ -17,9 +17,7 @@ export function createStartHandler(agentService: AgentService) {
};
if (!sessionId) {
- res
- .status(400)
- .json({ success: false, error: "sessionId is required" });
+ res.status(400).json({ success: false, error: 'sessionId is required' });
return;
}
@@ -30,7 +28,7 @@ export function createStartHandler(agentService: AgentService) {
res.json(result);
} catch (error) {
- logError(error, "Start conversation failed");
+ logError(error, 'Start conversation failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/agent/routes/stop.ts b/apps/server/src/routes/agent/routes/stop.ts
index 204c7d4a..c5a5fe01 100644
--- a/apps/server/src/routes/agent/routes/stop.ts
+++ b/apps/server/src/routes/agent/routes/stop.ts
@@ -2,9 +2,9 @@
* POST /stop endpoint - Stop execution
*/
-import type { Request, Response } from "express";
-import { AgentService } from "../../../services/agent-service.js";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import { AgentService } from '../../../services/agent-service.js';
+import { getErrorMessage, logError } from '../common.js';
export function createStopHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise => {
@@ -12,16 +12,14 @@ export function createStopHandler(agentService: AgentService) {
const { sessionId } = req.body as { sessionId: string };
if (!sessionId) {
- res
- .status(400)
- .json({ success: false, error: "sessionId is required" });
+ res.status(400).json({ success: false, error: 'sessionId is required' });
return;
}
const result = await agentService.stopExecution(sessionId);
res.json(result);
} catch (error) {
- logError(error, "Stop execution failed");
+ logError(error, 'Stop execution failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/app-spec/common.ts b/apps/server/src/routes/app-spec/common.ts
index 7d730043..df412dc6 100644
--- a/apps/server/src/routes/app-spec/common.ts
+++ b/apps/server/src/routes/app-spec/common.ts
@@ -2,9 +2,9 @@
* Common utilities and state management for spec regeneration
*/
-import { createLogger } from "@automaker/utils";
+import { createLogger } from '@automaker/utils';
-const logger = createLogger("SpecRegeneration");
+const logger = createLogger('SpecRegeneration');
// Shared state for tracking generation status - private
let isRunning = false;
@@ -23,10 +23,7 @@ export function getSpecRegenerationStatus(): {
/**
* Set the running state and abort controller
*/
-export function setRunningState(
- running: boolean,
- controller: AbortController | null = null
-): void {
+export function setRunningState(running: boolean, controller: AbortController | null = null): void {
isRunning = running;
currentAbortController = controller;
}
@@ -40,14 +37,12 @@ export function logAuthStatus(context: string): void {
logger.info(`${context} - Auth Status:`);
logger.info(
` ANTHROPIC_API_KEY: ${
- hasApiKey
- ? "SET (" + process.env.ANTHROPIC_API_KEY?.substring(0, 20) + "...)"
- : "NOT SET"
+ hasApiKey ? 'SET (' + process.env.ANTHROPIC_API_KEY?.substring(0, 20) + '...)' : 'NOT SET'
}`
);
if (!hasApiKey) {
- logger.warn("⚠️ WARNING: No authentication configured! SDK will fail.");
+ logger.warn('⚠️ WARNING: No authentication configured! SDK will fail.');
}
}
@@ -56,16 +51,13 @@ export function logAuthStatus(context: string): void {
*/
export function logError(error: unknown, context: string): void {
logger.error(`❌ ${context}:`);
- logger.error("Error name:", (error as any)?.name);
- logger.error("Error message:", (error as Error)?.message);
- logger.error("Error stack:", (error as Error)?.stack);
- logger.error(
- "Full error object:",
- JSON.stringify(error, Object.getOwnPropertyNames(error), 2)
- );
+ logger.error('Error name:', (error as any)?.name);
+ logger.error('Error message:', (error as Error)?.message);
+ logger.error('Error stack:', (error as Error)?.stack);
+ logger.error('Full error object:', JSON.stringify(error, Object.getOwnPropertyNames(error), 2));
}
-import { getErrorMessage as getErrorMessageShared } from "../common.js";
+import { getErrorMessage as getErrorMessageShared } from '../common.js';
// Re-export shared utility
export { getErrorMessageShared as getErrorMessage };
diff --git a/apps/server/src/routes/app-spec/index.ts b/apps/server/src/routes/app-spec/index.ts
index b37907c8..47950cd3 100644
--- a/apps/server/src/routes/app-spec/index.ts
+++ b/apps/server/src/routes/app-spec/index.ts
@@ -2,25 +2,22 @@
* Spec Regeneration routes - HTTP API for AI-powered spec generation
*/
-import { Router } from "express";
-import type { EventEmitter } from "../../lib/events.js";
-import { createCreateHandler } from "./routes/create.js";
-import { createGenerateHandler } from "./routes/generate.js";
-import { createGenerateFeaturesHandler } from "./routes/generate-features.js";
-import { createStopHandler } from "./routes/stop.js";
-import { createStatusHandler } from "./routes/status.js";
+import { Router } from 'express';
+import type { EventEmitter } from '../../lib/events.js';
+import { createCreateHandler } from './routes/create.js';
+import { createGenerateHandler } from './routes/generate.js';
+import { createGenerateFeaturesHandler } from './routes/generate-features.js';
+import { createStopHandler } from './routes/stop.js';
+import { createStatusHandler } from './routes/status.js';
export function createSpecRegenerationRoutes(events: EventEmitter): Router {
const router = Router();
- router.post("/create", createCreateHandler(events));
- router.post("/generate", createGenerateHandler(events));
- router.post("/generate-features", createGenerateFeaturesHandler(events));
- router.post("/stop", createStopHandler());
- router.get("/status", createStatusHandler());
+ router.post('/create', createCreateHandler(events));
+ router.post('/generate', createGenerateHandler(events));
+ router.post('/generate-features', createGenerateFeaturesHandler(events));
+ router.post('/stop', createStopHandler());
+ router.get('/status', createStatusHandler());
return router;
}
-
-
-
diff --git a/apps/server/src/routes/app-spec/routes/create.ts b/apps/server/src/routes/app-spec/routes/create.ts
index 8ac211cb..ed6f68f1 100644
--- a/apps/server/src/routes/app-spec/routes/create.ts
+++ b/apps/server/src/routes/app-spec/routes/create.ts
@@ -2,24 +2,24 @@
* POST /create endpoint - Create project spec from overview
*/
-import type { Request, Response } from "express";
-import type { EventEmitter } from "../../../lib/events.js";
-import { createLogger } from "@automaker/utils";
+import type { Request, Response } from 'express';
+import type { EventEmitter } from '../../../lib/events.js';
+import { createLogger } from '@automaker/utils';
import {
getSpecRegenerationStatus,
setRunningState,
logAuthStatus,
logError,
getErrorMessage,
-} from "../common.js";
-import { generateSpec } from "../generate-spec.js";
+} from '../common.js';
+import { generateSpec } from '../generate-spec.js';
-const logger = createLogger("SpecRegeneration");
+const logger = createLogger('SpecRegeneration');
export function createCreateHandler(events: EventEmitter) {
return async (req: Request, res: Response): Promise => {
- logger.info("========== /create endpoint called ==========");
- logger.debug("Request body:", JSON.stringify(req.body, null, 2));
+ logger.info('========== /create endpoint called ==========');
+ logger.debug('Request body:', JSON.stringify(req.body, null, 2));
try {
const { projectPath, projectOverview, generateFeatures, analyzeProject, maxFeatures } =
@@ -31,37 +31,34 @@ export function createCreateHandler(events: EventEmitter) {
maxFeatures?: number;
};
- logger.debug("Parsed params:");
- logger.debug(" projectPath:", projectPath);
- logger.debug(
- " projectOverview length:",
- `${projectOverview?.length || 0} chars`
- );
- logger.debug(" generateFeatures:", generateFeatures);
- logger.debug(" analyzeProject:", analyzeProject);
- logger.debug(" maxFeatures:", maxFeatures);
+ logger.debug('Parsed params:');
+ logger.debug(' projectPath:', projectPath);
+ logger.debug(' projectOverview length:', `${projectOverview?.length || 0} chars`);
+ logger.debug(' generateFeatures:', generateFeatures);
+ logger.debug(' analyzeProject:', analyzeProject);
+ logger.debug(' maxFeatures:', maxFeatures);
if (!projectPath || !projectOverview) {
- logger.error("Missing required parameters");
+ logger.error('Missing required parameters');
res.status(400).json({
success: false,
- error: "projectPath and projectOverview required",
+ error: 'projectPath and projectOverview required',
});
return;
}
const { isRunning } = getSpecRegenerationStatus();
if (isRunning) {
- logger.warn("Generation already running, rejecting request");
- res.json({ success: false, error: "Spec generation already running" });
+ logger.warn('Generation already running, rejecting request');
+ res.json({ success: false, error: 'Spec generation already running' });
return;
}
- logAuthStatus("Before starting generation");
+ logAuthStatus('Before starting generation');
const abortController = new AbortController();
setRunningState(true, abortController);
- logger.info("Starting background generation task...");
+ logger.info('Starting background generation task...');
// Start generation in background
generateSpec(
@@ -74,24 +71,22 @@ export function createCreateHandler(events: EventEmitter) {
maxFeatures
)
.catch((error) => {
- logError(error, "Generation failed with error");
- events.emit("spec-regeneration:event", {
- type: "spec_regeneration_error",
+ logError(error, 'Generation failed with error');
+ events.emit('spec-regeneration:event', {
+ type: 'spec_regeneration_error',
error: getErrorMessage(error),
projectPath: projectPath,
});
})
.finally(() => {
- logger.info("Generation task finished (success or error)");
+ logger.info('Generation task finished (success or error)');
setRunningState(false, null);
});
- logger.info(
- "Returning success response (generation running in background)"
- );
+ logger.info('Returning success response (generation running in background)');
res.json({ success: true });
} catch (error) {
- logError(error, "Create spec route handler failed");
+ logError(error, 'Create spec route handler failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/app-spec/routes/generate-features.ts b/apps/server/src/routes/app-spec/routes/generate-features.ts
index 0226cf15..a2e6143a 100644
--- a/apps/server/src/routes/app-spec/routes/generate-features.ts
+++ b/apps/server/src/routes/app-spec/routes/generate-features.ts
@@ -2,24 +2,24 @@
* POST /generate-features endpoint - Generate features from existing spec
*/
-import type { Request, Response } from "express";
-import type { EventEmitter } from "../../../lib/events.js";
-import { createLogger } from "@automaker/utils";
+import type { Request, Response } from 'express';
+import type { EventEmitter } from '../../../lib/events.js';
+import { createLogger } from '@automaker/utils';
import {
getSpecRegenerationStatus,
setRunningState,
logAuthStatus,
logError,
getErrorMessage,
-} from "../common.js";
-import { generateFeaturesFromSpec } from "../generate-features-from-spec.js";
+} from '../common.js';
+import { generateFeaturesFromSpec } from '../generate-features-from-spec.js';
-const logger = createLogger("SpecRegeneration");
+const logger = createLogger('SpecRegeneration');
export function createGenerateFeaturesHandler(events: EventEmitter) {
return async (req: Request, res: Response): Promise => {
- logger.info("========== /generate-features endpoint called ==========");
- logger.debug("Request body:", JSON.stringify(req.body, null, 2));
+ logger.info('========== /generate-features endpoint called ==========');
+ logger.debug('Request body:', JSON.stringify(req.body, null, 2));
try {
const { projectPath, maxFeatures } = req.body as {
@@ -27,52 +27,45 @@ export function createGenerateFeaturesHandler(events: EventEmitter) {
maxFeatures?: number;
};
- logger.debug("projectPath:", projectPath);
- logger.debug("maxFeatures:", maxFeatures);
+ logger.debug('projectPath:', projectPath);
+ logger.debug('maxFeatures:', maxFeatures);
if (!projectPath) {
- logger.error("Missing projectPath parameter");
- res.status(400).json({ success: false, error: "projectPath required" });
+ logger.error('Missing projectPath parameter');
+ res.status(400).json({ success: false, error: 'projectPath required' });
return;
}
const { isRunning } = getSpecRegenerationStatus();
if (isRunning) {
- logger.warn("Generation already running, rejecting request");
- res.json({ success: false, error: "Generation already running" });
+ logger.warn('Generation already running, rejecting request');
+ res.json({ success: false, error: 'Generation already running' });
return;
}
- logAuthStatus("Before starting feature generation");
+ logAuthStatus('Before starting feature generation');
const abortController = new AbortController();
setRunningState(true, abortController);
- logger.info("Starting background feature generation task...");
+ logger.info('Starting background feature generation task...');
- generateFeaturesFromSpec(
- projectPath,
- events,
- abortController,
- maxFeatures
- )
+ generateFeaturesFromSpec(projectPath, events, abortController, maxFeatures)
.catch((error) => {
- logError(error, "Feature generation failed with error");
- events.emit("spec-regeneration:event", {
- type: "features_error",
+ logError(error, 'Feature generation failed with error');
+ events.emit('spec-regeneration:event', {
+ type: 'features_error',
error: getErrorMessage(error),
});
})
.finally(() => {
- logger.info("Feature generation task finished (success or error)");
+ logger.info('Feature generation task finished (success or error)');
setRunningState(false, null);
});
- logger.info(
- "Returning success response (generation running in background)"
- );
+ logger.info('Returning success response (generation running in background)');
res.json({ success: true });
} catch (error) {
- logError(error, "Generate features route handler failed");
+ logError(error, 'Generate features route handler failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/app-spec/routes/generate.ts b/apps/server/src/routes/app-spec/routes/generate.ts
index b866fa4e..341d634d 100644
--- a/apps/server/src/routes/app-spec/routes/generate.ts
+++ b/apps/server/src/routes/app-spec/routes/generate.ts
@@ -2,71 +2,63 @@
* POST /generate endpoint - Generate spec from project definition
*/
-import type { Request, Response } from "express";
-import type { EventEmitter } from "../../../lib/events.js";
-import { createLogger } from "@automaker/utils";
+import type { Request, Response } from 'express';
+import type { EventEmitter } from '../../../lib/events.js';
+import { createLogger } from '@automaker/utils';
import {
getSpecRegenerationStatus,
setRunningState,
logAuthStatus,
logError,
getErrorMessage,
-} from "../common.js";
-import { generateSpec } from "../generate-spec.js";
+} from '../common.js';
+import { generateSpec } from '../generate-spec.js';
-const logger = createLogger("SpecRegeneration");
+const logger = createLogger('SpecRegeneration');
export function createGenerateHandler(events: EventEmitter) {
return async (req: Request, res: Response): Promise => {
- logger.info("========== /generate endpoint called ==========");
- logger.debug("Request body:", JSON.stringify(req.body, null, 2));
+ logger.info('========== /generate endpoint called ==========');
+ logger.debug('Request body:', JSON.stringify(req.body, null, 2));
try {
- const {
- projectPath,
- projectDefinition,
- generateFeatures,
- analyzeProject,
- maxFeatures,
- } = req.body as {
- projectPath: string;
- projectDefinition: string;
- generateFeatures?: boolean;
- analyzeProject?: boolean;
- maxFeatures?: number;
- };
+ const { projectPath, projectDefinition, generateFeatures, analyzeProject, maxFeatures } =
+ req.body as {
+ projectPath: string;
+ projectDefinition: string;
+ generateFeatures?: boolean;
+ analyzeProject?: boolean;
+ maxFeatures?: number;
+ };
- logger.debug("Parsed params:");
- logger.debug(" projectPath:", projectPath);
- logger.debug(
- " projectDefinition length:",
- `${projectDefinition?.length || 0} chars`
- );
- logger.debug(" generateFeatures:", generateFeatures);
- logger.debug(" analyzeProject:", analyzeProject);
- logger.debug(" maxFeatures:", maxFeatures);
+ logger.debug('Parsed params:');
+ logger.debug(' projectPath:', projectPath);
+ logger.debug(' projectDefinition length:', `${projectDefinition?.length || 0} chars`);
+ logger.debug(' generateFeatures:', generateFeatures);
+ logger.debug(' analyzeProject:', analyzeProject);
+ logger.debug(' maxFeatures:', maxFeatures);
if (!projectPath || !projectDefinition) {
- logger.error("Missing required parameters");
+ logger.error('Missing required parameters');
res.status(400).json({
success: false,
- error: "projectPath and projectDefinition required",
+ error: 'projectPath and projectDefinition required',
});
return;
}
const { isRunning } = getSpecRegenerationStatus();
if (isRunning) {
- logger.warn("Generation already running, rejecting request");
- res.json({ success: false, error: "Spec generation already running" });
+ logger.warn('Generation already running, rejecting request');
+ res.json({ success: false, error: 'Spec generation already running' });
return;
}
- logAuthStatus("Before starting generation");
+ logAuthStatus('Before starting generation');
const abortController = new AbortController();
setRunningState(true, abortController);
- logger.info("Starting background generation task...");
+ logger.info('Starting background generation task...');
generateSpec(
projectPath,
@@ -78,24 +70,22 @@ export function createGenerateHandler(events: EventEmitter) {
maxFeatures
)
.catch((error) => {
- logError(error, "Generation failed with error");
- events.emit("spec-regeneration:event", {
- type: "spec_regeneration_error",
+ logError(error, 'Generation failed with error');
+ events.emit('spec-regeneration:event', {
+ type: 'spec_regeneration_error',
error: getErrorMessage(error),
projectPath: projectPath,
});
})
.finally(() => {
- logger.info("Generation task finished (success or error)");
+ logger.info('Generation task finished (success or error)');
setRunningState(false, null);
});
- logger.info(
- "Returning success response (generation running in background)"
- );
+ logger.info('Returning success response (generation running in background)');
res.json({ success: true });
} catch (error) {
- logError(error, "Generate spec route handler failed");
+ logError(error, 'Generate spec route handler failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/app-spec/routes/status.ts b/apps/server/src/routes/app-spec/routes/status.ts
index a3c1aac1..542dd4f3 100644
--- a/apps/server/src/routes/app-spec/routes/status.ts
+++ b/apps/server/src/routes/app-spec/routes/status.ts
@@ -2,8 +2,8 @@
* GET /status endpoint - Get generation status
*/
-import type { Request, Response } from "express";
-import { getSpecRegenerationStatus, getErrorMessage } from "../common.js";
+import type { Request, Response } from 'express';
+import { getSpecRegenerationStatus, getErrorMessage } from '../common.js';
export function createStatusHandler() {
return async (_req: Request, res: Response): Promise => {
diff --git a/apps/server/src/routes/app-spec/routes/stop.ts b/apps/server/src/routes/app-spec/routes/stop.ts
index 7c3bd5ca..0751147b 100644
--- a/apps/server/src/routes/app-spec/routes/stop.ts
+++ b/apps/server/src/routes/app-spec/routes/stop.ts
@@ -2,12 +2,8 @@
* POST /stop endpoint - Stop generation
*/
-import type { Request, Response } from "express";
-import {
- getSpecRegenerationStatus,
- setRunningState,
- getErrorMessage,
-} from "../common.js";
+import type { Request, Response } from 'express';
+import { getSpecRegenerationStatus, setRunningState, getErrorMessage } from '../common.js';
export function createStopHandler() {
return async (_req: Request, res: Response): Promise => {
diff --git a/apps/server/src/routes/auto-mode/common.ts b/apps/server/src/routes/auto-mode/common.ts
index 048d47fa..8fe9c3ab 100644
--- a/apps/server/src/routes/auto-mode/common.ts
+++ b/apps/server/src/routes/auto-mode/common.ts
@@ -2,13 +2,10 @@
* Common utilities for auto-mode routes
*/
-import { createLogger } from "@automaker/utils";
-import {
- getErrorMessage as getErrorMessageShared,
- createLogError,
-} from "../common.js";
+import { createLogger } from '@automaker/utils';
+import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
-const logger = createLogger("AutoMode");
+const logger = createLogger('AutoMode');
// Re-export shared utilities
export { getErrorMessageShared as getErrorMessage };
diff --git a/apps/server/src/routes/auto-mode/index.ts b/apps/server/src/routes/auto-mode/index.ts
index 6bdd7dbb..5f36d691 100644
--- a/apps/server/src/routes/auto-mode/index.ts
+++ b/apps/server/src/routes/auto-mode/index.ts
@@ -4,37 +4,65 @@
* Uses the AutoModeService for real feature execution with Claude Agent SDK
*/
-import { Router } from "express";
-import type { AutoModeService } from "../../services/auto-mode-service.js";
-import { validatePathParams } from "../../middleware/validate-paths.js";
-import { createStopFeatureHandler } from "./routes/stop-feature.js";
-import { createStatusHandler } from "./routes/status.js";
-import { createRunFeatureHandler } from "./routes/run-feature.js";
-import { createVerifyFeatureHandler } from "./routes/verify-feature.js";
-import { createResumeFeatureHandler } from "./routes/resume-feature.js";
-import { createContextExistsHandler } from "./routes/context-exists.js";
-import { createAnalyzeProjectHandler } from "./routes/analyze-project.js";
-import { createFollowUpFeatureHandler } from "./routes/follow-up-feature.js";
-import { createCommitFeatureHandler } from "./routes/commit-feature.js";
-import { createApprovePlanHandler } from "./routes/approve-plan.js";
+import { Router } from 'express';
+import type { AutoModeService } from '../../services/auto-mode-service.js';
+import { validatePathParams } from '../../middleware/validate-paths.js';
+import { createStopFeatureHandler } from './routes/stop-feature.js';
+import { createStatusHandler } from './routes/status.js';
+import { createRunFeatureHandler } from './routes/run-feature.js';
+import { createVerifyFeatureHandler } from './routes/verify-feature.js';
+import { createResumeFeatureHandler } from './routes/resume-feature.js';
+import { createContextExistsHandler } from './routes/context-exists.js';
+import { createAnalyzeProjectHandler } from './routes/analyze-project.js';
+import { createFollowUpFeatureHandler } from './routes/follow-up-feature.js';
+import { createCommitFeatureHandler } from './routes/commit-feature.js';
+import { createApprovePlanHandler } from './routes/approve-plan.js';
export function createAutoModeRoutes(autoModeService: AutoModeService): Router {
const router = Router();
- router.post("/stop-feature", createStopFeatureHandler(autoModeService));
- router.post("/status", validatePathParams("projectPath?"), createStatusHandler(autoModeService));
- router.post("/run-feature", validatePathParams("projectPath"), createRunFeatureHandler(autoModeService));
- router.post("/verify-feature", validatePathParams("projectPath"), createVerifyFeatureHandler(autoModeService));
- router.post("/resume-feature", validatePathParams("projectPath"), createResumeFeatureHandler(autoModeService));
- router.post("/context-exists", validatePathParams("projectPath"), createContextExistsHandler(autoModeService));
- router.post("/analyze-project", validatePathParams("projectPath"), createAnalyzeProjectHandler(autoModeService));
+ router.post('/stop-feature', createStopFeatureHandler(autoModeService));
+ router.post('/status', validatePathParams('projectPath?'), createStatusHandler(autoModeService));
router.post(
- "/follow-up-feature",
- validatePathParams("projectPath", "imagePaths[]"),
+ '/run-feature',
+ validatePathParams('projectPath'),
+ createRunFeatureHandler(autoModeService)
+ );
+ router.post(
+ '/verify-feature',
+ validatePathParams('projectPath'),
+ createVerifyFeatureHandler(autoModeService)
+ );
+ router.post(
+ '/resume-feature',
+ validatePathParams('projectPath'),
+ createResumeFeatureHandler(autoModeService)
+ );
+ router.post(
+ '/context-exists',
+ validatePathParams('projectPath'),
+ createContextExistsHandler(autoModeService)
+ );
+ router.post(
+ '/analyze-project',
+ validatePathParams('projectPath'),
+ createAnalyzeProjectHandler(autoModeService)
+ );
+ router.post(
+ '/follow-up-feature',
+ validatePathParams('projectPath', 'imagePaths[]'),
createFollowUpFeatureHandler(autoModeService)
);
- router.post("/commit-feature", validatePathParams("projectPath", "worktreePath?"), createCommitFeatureHandler(autoModeService));
- router.post("/approve-plan", validatePathParams("projectPath"), createApprovePlanHandler(autoModeService));
+ router.post(
+ '/commit-feature',
+ validatePathParams('projectPath', 'worktreePath?'),
+ createCommitFeatureHandler(autoModeService)
+ );
+ router.post(
+ '/approve-plan',
+ validatePathParams('projectPath'),
+ createApprovePlanHandler(autoModeService)
+ );
return router;
}
diff --git a/apps/server/src/routes/auto-mode/routes/analyze-project.ts b/apps/server/src/routes/auto-mode/routes/analyze-project.ts
index 492b28b5..77c95e27 100644
--- a/apps/server/src/routes/auto-mode/routes/analyze-project.ts
+++ b/apps/server/src/routes/auto-mode/routes/analyze-project.ts
@@ -2,12 +2,12 @@
* POST /analyze-project endpoint - Analyze project
*/
-import type { Request, Response } from "express";
-import type { AutoModeService } from "../../../services/auto-mode-service.js";
-import { createLogger } from "@automaker/utils";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import type { AutoModeService } from '../../../services/auto-mode-service.js';
+import { createLogger } from '@automaker/utils';
+import { getErrorMessage, logError } from '../common.js';
-const logger = createLogger("AutoMode");
+const logger = createLogger('AutoMode');
export function createAnalyzeProjectHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise => {
@@ -15,9 +15,7 @@ export function createAnalyzeProjectHandler(autoModeService: AutoModeService) {
const { projectPath } = req.body as { projectPath: string };
if (!projectPath) {
- res
- .status(400)
- .json({ success: false, error: "projectPath is required" });
+ res.status(400).json({ success: false, error: 'projectPath is required' });
return;
}
@@ -26,9 +24,9 @@ export function createAnalyzeProjectHandler(autoModeService: AutoModeService) {
logger.error(`[AutoMode] Project analysis error:`, error);
});
- res.json({ success: true, message: "Project analysis started" });
+ res.json({ success: true, message: 'Project analysis started' });
} catch (error) {
- logError(error, "Analyze project failed");
+ logError(error, 'Analyze project failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/auto-mode/routes/approve-plan.ts b/apps/server/src/routes/auto-mode/routes/approve-plan.ts
index ce3db20b..c006e506 100644
--- a/apps/server/src/routes/auto-mode/routes/approve-plan.ts
+++ b/apps/server/src/routes/auto-mode/routes/approve-plan.ts
@@ -2,12 +2,12 @@
* POST /approve-plan endpoint - Approve or reject a generated plan/spec
*/
-import type { Request, Response } from "express";
-import type { AutoModeService } from "../../../services/auto-mode-service.js";
-import { createLogger } from "@automaker/utils";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import type { AutoModeService } from '../../../services/auto-mode-service.js';
+import { createLogger } from '@automaker/utils';
+import { getErrorMessage, logError } from '../common.js';
-const logger = createLogger("AutoMode");
+const logger = createLogger('AutoMode');
export function createApprovePlanHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise => {
@@ -23,15 +23,15 @@ export function createApprovePlanHandler(autoModeService: AutoModeService) {
if (!featureId) {
res.status(400).json({
success: false,
- error: "featureId is required",
+ error: 'featureId is required',
});
return;
}
- if (typeof approved !== "boolean") {
+ if (typeof approved !== 'boolean') {
res.status(400).json({
success: false,
- error: "approved must be a boolean",
+ error: 'approved must be a boolean',
});
return;
}
@@ -41,9 +41,9 @@ export function createApprovePlanHandler(autoModeService: AutoModeService) {
// This supports cases where the server restarted while waiting for approval
logger.info(
- `[AutoMode] Plan ${approved ? "approved" : "rejected"} for feature ${featureId}${
- editedPlan ? " (with edits)" : ""
- }${feedback ? ` - Feedback: ${feedback}` : ""}`
+ `[AutoMode] Plan ${approved ? 'approved' : 'rejected'} for feature ${featureId}${
+ editedPlan ? ' (with edits)' : ''
+ }${feedback ? ` - Feedback: ${feedback}` : ''}`
);
// Resolve the pending approval (with recovery support)
@@ -67,11 +67,11 @@ export function createApprovePlanHandler(autoModeService: AutoModeService) {
success: true,
approved,
message: approved
- ? "Plan approved - implementation will continue"
- : "Plan rejected - feature execution stopped",
+ ? 'Plan approved - implementation will continue'
+ : 'Plan rejected - feature execution stopped',
});
} catch (error) {
- logError(error, "Approve plan failed");
+ logError(error, 'Approve plan failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/auto-mode/routes/commit-feature.ts b/apps/server/src/routes/auto-mode/routes/commit-feature.ts
index aaf2e6f5..7db0ae32 100644
--- a/apps/server/src/routes/auto-mode/routes/commit-feature.ts
+++ b/apps/server/src/routes/auto-mode/routes/commit-feature.ts
@@ -2,9 +2,9 @@
* POST /commit-feature endpoint - Commit feature changes
*/
-import type { Request, Response } from "express";
-import type { AutoModeService } from "../../../services/auto-mode-service.js";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import type { AutoModeService } from '../../../services/auto-mode-service.js';
+import { getErrorMessage, logError } from '../common.js';
export function createCommitFeatureHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise => {
@@ -16,23 +16,17 @@ export function createCommitFeatureHandler(autoModeService: AutoModeService) {
};
if (!projectPath || !featureId) {
- res
- .status(400)
- .json({
- success: false,
- error: "projectPath and featureId are required",
- });
+ res.status(400).json({
+ success: false,
+ error: 'projectPath and featureId are required',
+ });
return;
}
- const commitHash = await autoModeService.commitFeature(
- projectPath,
- featureId,
- worktreePath
- );
+ const commitHash = await autoModeService.commitFeature(projectPath, featureId, worktreePath);
res.json({ success: true, commitHash });
} catch (error) {
- logError(error, "Commit feature failed");
+ logError(error, 'Commit feature failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/auto-mode/routes/context-exists.ts b/apps/server/src/routes/auto-mode/routes/context-exists.ts
index 32ebb4ce..ef028f3f 100644
--- a/apps/server/src/routes/auto-mode/routes/context-exists.ts
+++ b/apps/server/src/routes/auto-mode/routes/context-exists.ts
@@ -2,9 +2,9 @@
* POST /context-exists endpoint - Check if context exists for a feature
*/
-import type { Request, Response } from "express";
-import type { AutoModeService } from "../../../services/auto-mode-service.js";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import type { AutoModeService } from '../../../services/auto-mode-service.js';
+import { getErrorMessage, logError } from '../common.js';
export function createContextExistsHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise => {
@@ -15,22 +15,17 @@ export function createContextExistsHandler(autoModeService: AutoModeService) {
};
if (!projectPath || !featureId) {
- res
- .status(400)
- .json({
- success: false,
- error: "projectPath and featureId are required",
- });
+ res.status(400).json({
+ success: false,
+ error: 'projectPath and featureId are required',
+ });
return;
}
- const exists = await autoModeService.contextExists(
- projectPath,
- featureId
- );
+ const exists = await autoModeService.contextExists(projectPath, featureId);
res.json({ success: true, exists });
} catch (error) {
- logError(error, "Check context exists failed");
+ logError(error, 'Check context exists failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/auto-mode/routes/follow-up-feature.ts b/apps/server/src/routes/auto-mode/routes/follow-up-feature.ts
index 4560f09b..1ed14c39 100644
--- a/apps/server/src/routes/auto-mode/routes/follow-up-feature.ts
+++ b/apps/server/src/routes/auto-mode/routes/follow-up-feature.ts
@@ -2,29 +2,28 @@
* POST /follow-up-feature endpoint - Follow up on a feature
*/
-import type { Request, Response } from "express";
-import type { AutoModeService } from "../../../services/auto-mode-service.js";
-import { createLogger } from "@automaker/utils";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import type { AutoModeService } from '../../../services/auto-mode-service.js';
+import { createLogger } from '@automaker/utils';
+import { getErrorMessage, logError } from '../common.js';
-const logger = createLogger("AutoMode");
+const logger = createLogger('AutoMode');
export function createFollowUpFeatureHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise => {
try {
- const { projectPath, featureId, prompt, imagePaths, useWorktrees } =
- req.body as {
- projectPath: string;
- featureId: string;
- prompt: string;
- imagePaths?: string[];
- useWorktrees?: boolean;
- };
+ const { projectPath, featureId, prompt, imagePaths, useWorktrees } = req.body as {
+ projectPath: string;
+ featureId: string;
+ prompt: string;
+ imagePaths?: string[];
+ useWorktrees?: boolean;
+ };
if (!projectPath || !featureId || !prompt) {
res.status(400).json({
success: false,
- error: "projectPath, featureId, and prompt are required",
+ error: 'projectPath, featureId, and prompt are required',
});
return;
}
@@ -32,18 +31,9 @@ export function createFollowUpFeatureHandler(autoModeService: AutoModeService) {
// Start follow-up in background
// followUpFeature derives workDir from feature.branchName
autoModeService
- .followUpFeature(
- projectPath,
- featureId,
- prompt,
- imagePaths,
- useWorktrees ?? true
- )
+ .followUpFeature(projectPath, featureId, prompt, imagePaths, useWorktrees ?? true)
.catch((error) => {
- logger.error(
- `[AutoMode] Follow up feature ${featureId} error:`,
- error
- );
+ logger.error(`[AutoMode] Follow up feature ${featureId} error:`, error);
})
.finally(() => {
// Release the starting slot when follow-up completes (success or error)
@@ -52,7 +42,7 @@ export function createFollowUpFeatureHandler(autoModeService: AutoModeService) {
res.json({ success: true });
} catch (error) {
- logError(error, "Follow up feature failed");
+ logError(error, 'Follow up feature failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/auto-mode/routes/resume-feature.ts b/apps/server/src/routes/auto-mode/routes/resume-feature.ts
index 12471fc4..198f24ef 100644
--- a/apps/server/src/routes/auto-mode/routes/resume-feature.ts
+++ b/apps/server/src/routes/auto-mode/routes/resume-feature.ts
@@ -2,12 +2,12 @@
* POST /resume-feature endpoint - Resume a feature
*/
-import type { Request, Response } from "express";
-import type { AutoModeService } from "../../../services/auto-mode-service.js";
-import { createLogger } from "@automaker/utils";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import type { AutoModeService } from '../../../services/auto-mode-service.js';
+import { createLogger } from '@automaker/utils';
+import { getErrorMessage, logError } from '../common.js';
-const logger = createLogger("AutoMode");
+const logger = createLogger('AutoMode');
export function createResumeFeatureHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise => {
@@ -21,7 +21,7 @@ export function createResumeFeatureHandler(autoModeService: AutoModeService) {
if (!projectPath || !featureId) {
res.status(400).json({
success: false,
- error: "projectPath and featureId are required",
+ error: 'projectPath and featureId are required',
});
return;
}
@@ -36,7 +36,7 @@ export function createResumeFeatureHandler(autoModeService: AutoModeService) {
res.json({ success: true });
} catch (error) {
- logError(error, "Resume feature failed");
+ logError(error, 'Resume feature failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/auto-mode/routes/run-feature.ts b/apps/server/src/routes/auto-mode/routes/run-feature.ts
index bb6f6ef7..16ed475a 100644
--- a/apps/server/src/routes/auto-mode/routes/run-feature.ts
+++ b/apps/server/src/routes/auto-mode/routes/run-feature.ts
@@ -2,12 +2,12 @@
* POST /run-feature endpoint - Run a single feature
*/
-import type { Request, Response } from "express";
-import type { AutoModeService } from "../../../services/auto-mode-service.js";
-import { createLogger } from "@automaker/utils";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import type { AutoModeService } from '../../../services/auto-mode-service.js';
+import { createLogger } from '@automaker/utils';
+import { getErrorMessage, logError } from '../common.js';
-const logger = createLogger("AutoMode");
+const logger = createLogger('AutoMode');
export function createRunFeatureHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise => {
@@ -21,7 +21,7 @@ export function createRunFeatureHandler(autoModeService: AutoModeService) {
if (!projectPath || !featureId) {
res.status(400).json({
success: false,
- error: "projectPath and featureId are required",
+ error: 'projectPath and featureId are required',
});
return;
}
@@ -40,7 +40,7 @@ export function createRunFeatureHandler(autoModeService: AutoModeService) {
res.json({ success: true });
} catch (error) {
- logError(error, "Run feature failed");
+ logError(error, 'Run feature failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/auto-mode/routes/status.ts b/apps/server/src/routes/auto-mode/routes/status.ts
index ba0ee8a1..9a1b4690 100644
--- a/apps/server/src/routes/auto-mode/routes/status.ts
+++ b/apps/server/src/routes/auto-mode/routes/status.ts
@@ -2,9 +2,9 @@
* POST /status endpoint - Get auto mode status
*/
-import type { Request, Response } from "express";
-import type { AutoModeService } from "../../../services/auto-mode-service.js";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import type { AutoModeService } from '../../../services/auto-mode-service.js';
+import { getErrorMessage, logError } from '../common.js';
export function createStatusHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise => {
@@ -15,7 +15,7 @@ export function createStatusHandler(autoModeService: AutoModeService) {
...status,
});
} catch (error) {
- logError(error, "Get status failed");
+ logError(error, 'Get status failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/auto-mode/routes/stop-feature.ts b/apps/server/src/routes/auto-mode/routes/stop-feature.ts
index 0468e9d3..bec9a4aa 100644
--- a/apps/server/src/routes/auto-mode/routes/stop-feature.ts
+++ b/apps/server/src/routes/auto-mode/routes/stop-feature.ts
@@ -2,9 +2,9 @@
* POST /stop-feature endpoint - Stop a specific feature
*/
-import type { Request, Response } from "express";
-import type { AutoModeService } from "../../../services/auto-mode-service.js";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import type { AutoModeService } from '../../../services/auto-mode-service.js';
+import { getErrorMessage, logError } from '../common.js';
export function createStopFeatureHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise => {
@@ -12,16 +12,14 @@ export function createStopFeatureHandler(autoModeService: AutoModeService) {
const { featureId } = req.body as { featureId: string };
if (!featureId) {
- res
- .status(400)
- .json({ success: false, error: "featureId is required" });
+ res.status(400).json({ success: false, error: 'featureId is required' });
return;
}
const stopped = await autoModeService.stopFeature(featureId);
res.json({ success: true, stopped });
} catch (error) {
- logError(error, "Stop feature failed");
+ logError(error, 'Stop feature failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/auto-mode/routes/verify-feature.ts b/apps/server/src/routes/auto-mode/routes/verify-feature.ts
index 456eecb2..f8f4f6f7 100644
--- a/apps/server/src/routes/auto-mode/routes/verify-feature.ts
+++ b/apps/server/src/routes/auto-mode/routes/verify-feature.ts
@@ -2,9 +2,9 @@
* POST /verify-feature endpoint - Verify a feature
*/
-import type { Request, Response } from "express";
-import type { AutoModeService } from "../../../services/auto-mode-service.js";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import type { AutoModeService } from '../../../services/auto-mode-service.js';
+import { getErrorMessage, logError } from '../common.js';
export function createVerifyFeatureHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise => {
@@ -15,22 +15,17 @@ export function createVerifyFeatureHandler(autoModeService: AutoModeService) {
};
if (!projectPath || !featureId) {
- res
- .status(400)
- .json({
- success: false,
- error: "projectPath and featureId are required",
- });
+ res.status(400).json({
+ success: false,
+ error: 'projectPath and featureId are required',
+ });
return;
}
- const passes = await autoModeService.verifyFeature(
- projectPath,
- featureId
- );
+ const passes = await autoModeService.verifyFeature(projectPath, featureId);
res.json({ success: true, passes });
} catch (error) {
- logError(error, "Verify feature failed");
+ logError(error, 'Verify feature failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/claude/index.ts b/apps/server/src/routes/claude/index.ts
index f951aa34..326b6d90 100644
--- a/apps/server/src/routes/claude/index.ts
+++ b/apps/server/src/routes/claude/index.ts
@@ -1,18 +1,18 @@
-import { Router, Request, Response } from "express";
-import { ClaudeUsageService } from "../../services/claude-usage-service.js";
+import { Router, Request, Response } from 'express';
+import { ClaudeUsageService } from '../../services/claude-usage-service.js';
export function createClaudeRoutes(service: ClaudeUsageService): Router {
const router = Router();
// Get current usage (fetches from Claude CLI)
- router.get("/usage", async (req: Request, res: Response) => {
+ router.get('/usage', async (req: Request, res: Response) => {
try {
// Check if Claude CLI is available first
const isAvailable = await service.isAvailable();
if (!isAvailable) {
res.status(503).json({
- error: "Claude CLI not found",
- message: "Please install Claude Code CLI and run 'claude login' to authenticate"
+ error: 'Claude CLI not found',
+ message: "Please install Claude Code CLI and run 'claude login' to authenticate",
});
return;
}
@@ -20,20 +20,20 @@ export function createClaudeRoutes(service: ClaudeUsageService): Router {
const usage = await service.fetchUsageData();
res.json(usage);
} catch (error) {
- const message = error instanceof Error ? error.message : "Unknown error";
+ const message = error instanceof Error ? error.message : 'Unknown error';
- if (message.includes("Authentication required") || message.includes("token_expired")) {
+ if (message.includes('Authentication required') || message.includes('token_expired')) {
res.status(401).json({
- error: "Authentication required",
- message: "Please run 'claude login' to authenticate"
+ error: 'Authentication required',
+ message: "Please run 'claude login' to authenticate",
});
- } else if (message.includes("timed out")) {
+ } else if (message.includes('timed out')) {
res.status(504).json({
- error: "Command timed out",
- message: "The Claude CLI took too long to respond"
+ error: 'Command timed out',
+ message: 'The Claude CLI took too long to respond',
});
} else {
- console.error("Error fetching usage:", error);
+ console.error('Error fetching usage:', error);
res.status(500).json({ error: message });
}
}
diff --git a/apps/server/src/routes/claude/types.ts b/apps/server/src/routes/claude/types.ts
index 2f6eb597..bd892746 100644
--- a/apps/server/src/routes/claude/types.ts
+++ b/apps/server/src/routes/claude/types.ts
@@ -29,7 +29,7 @@ export type ClaudeUsage = {
export type ClaudeStatus = {
indicator: {
- color: "green" | "yellow" | "orange" | "red" | "gray";
+ color: 'green' | 'yellow' | 'orange' | 'red' | 'gray';
};
description: string;
};
diff --git a/apps/server/src/routes/common.ts b/apps/server/src/routes/common.ts
index c2bc9a84..14589ffd 100644
--- a/apps/server/src/routes/common.ts
+++ b/apps/server/src/routes/common.ts
@@ -2,7 +2,7 @@
* Common utilities shared across all route modules
*/
-import { createLogger } from "@automaker/utils";
+import { createLogger } from '@automaker/utils';
// Re-export git utilities from shared package
export {
@@ -16,7 +16,7 @@ export {
listAllFilesInDirectory,
generateDiffsForNonGitDirectory,
getGitRepositoryDiffs,
-} from "@automaker/git-utils";
+} from '@automaker/git-utils';
type Logger = ReturnType;
@@ -24,7 +24,7 @@ type Logger = ReturnType;
* Get error message from error object
*/
export function getErrorMessage(error: unknown): string {
- return error instanceof Error ? error.message : "Unknown error";
+ return error instanceof Error ? error.message : 'Unknown error';
}
/**
diff --git a/apps/server/src/routes/context/index.ts b/apps/server/src/routes/context/index.ts
new file mode 100644
index 00000000..37e447bf
--- /dev/null
+++ b/apps/server/src/routes/context/index.ts
@@ -0,0 +1,24 @@
+/**
+ * Context routes - HTTP API for context file operations
+ *
+ * Provides endpoints for managing context files including
+ * AI-powered image description generation.
+ */
+
+import { Router } from 'express';
+import { createDescribeImageHandler } from './routes/describe-image.js';
+import { createDescribeFileHandler } from './routes/describe-file.js';
+
+/**
+ * Create the context router
+ *
+ * @returns Express router with context endpoints
+ */
+export function createContextRoutes(): Router {
+ const router = Router();
+
+ router.post('/describe-image', createDescribeImageHandler());
+ router.post('/describe-file', createDescribeFileHandler());
+
+ return router;
+}
diff --git a/apps/server/src/routes/context/routes/describe-file.ts b/apps/server/src/routes/context/routes/describe-file.ts
new file mode 100644
index 00000000..0e680b65
--- /dev/null
+++ b/apps/server/src/routes/context/routes/describe-file.ts
@@ -0,0 +1,220 @@
+/**
+ * POST /context/describe-file endpoint - Generate description for a text file
+ *
+ * Uses Claude Haiku to analyze a text file and generate a concise description
+ * suitable for context file metadata.
+ *
+ * SECURITY: This endpoint validates file paths against ALLOWED_ROOT_DIRECTORY
+ * and reads file content directly (not via Claude's Read tool) to prevent
+ * arbitrary file reads and prompt injection attacks.
+ */
+
+import type { Request, Response } from 'express';
+import { query } from '@anthropic-ai/claude-agent-sdk';
+import { createLogger } from '@automaker/utils';
+import { CLAUDE_MODEL_MAP } from '@automaker/types';
+import { PathNotAllowedError } from '@automaker/platform';
+import { createCustomOptions } from '../../../lib/sdk-options.js';
+import * as secureFs from '../../../lib/secure-fs.js';
+import * as path from 'path';
+
+const logger = createLogger('DescribeFile');
+
+/**
+ * Request body for the describe-file endpoint
+ */
+interface DescribeFileRequestBody {
+ /** Path to the file */
+ filePath: string;
+}
+
+/**
+ * Success response from the describe-file endpoint
+ */
+interface DescribeFileSuccessResponse {
+ success: true;
+ description: string;
+}
+
+/**
+ * Error response from the describe-file endpoint
+ */
+interface DescribeFileErrorResponse {
+ success: false;
+ error: string;
+}
+
+/**
+ * Extract text content from Claude SDK response messages
+ */
+async function extractTextFromStream(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ stream: AsyncIterable
+): Promise {
+ let responseText = '';
+
+ for await (const msg of stream) {
+ if (msg.type === 'assistant' && msg.message?.content) {
+ const blocks = msg.message.content as Array<{ type: string; text?: string }>;
+ for (const block of blocks) {
+ if (block.type === 'text' && block.text) {
+ responseText += block.text;
+ }
+ }
+ } else if (msg.type === 'result' && msg.subtype === 'success') {
+ responseText = msg.result || responseText;
+ }
+ }
+
+ return responseText;
+}
+
+/**
+ * Create the describe-file request handler
+ *
+ * @returns Express request handler for file description
+ */
+export function createDescribeFileHandler(): (req: Request, res: Response) => Promise {
+ return async (req: Request, res: Response): Promise => {
+ try {
+ const { filePath } = req.body as DescribeFileRequestBody;
+
+ // Validate required fields
+ if (!filePath || typeof filePath !== 'string') {
+ const response: DescribeFileErrorResponse = {
+ success: false,
+ error: 'filePath is required and must be a string',
+ };
+ res.status(400).json(response);
+ return;
+ }
+
+ logger.info(`[DescribeFile] Starting description generation for: ${filePath}`);
+
+ // Resolve the path for logging and cwd derivation
+ const resolvedPath = secureFs.resolvePath(filePath);
+
+ // Read file content using secureFs (validates path against ALLOWED_ROOT_DIRECTORY)
+ // This prevents arbitrary file reads (e.g., /etc/passwd, ~/.ssh/id_rsa)
+ // and prompt injection attacks where malicious filePath values could inject instructions
+ let fileContent: string;
+ try {
+ const content = await secureFs.readFile(resolvedPath, 'utf-8');
+ fileContent = typeof content === 'string' ? content : content.toString('utf-8');
+ } catch (readError) {
+ // Path not allowed - return 403 Forbidden
+ if (readError instanceof PathNotAllowedError) {
+ logger.warn(`[DescribeFile] Path not allowed: ${filePath}`);
+ const response: DescribeFileErrorResponse = {
+ success: false,
+ error: 'File path is not within the allowed directory',
+ };
+ res.status(403).json(response);
+ return;
+ }
+
+ // File not found
+ if (
+ readError !== null &&
+ typeof readError === 'object' &&
+ 'code' in readError &&
+ readError.code === 'ENOENT'
+ ) {
+ logger.warn(`[DescribeFile] File not found: ${resolvedPath}`);
+ const response: DescribeFileErrorResponse = {
+ success: false,
+ error: `File not found: ${filePath}`,
+ };
+ res.status(404).json(response);
+ return;
+ }
+
+ const errorMessage = readError instanceof Error ? readError.message : 'Unknown error';
+ logger.error(`[DescribeFile] Failed to read file: ${errorMessage}`);
+ const response: DescribeFileErrorResponse = {
+ success: false,
+ error: `Failed to read file: ${errorMessage}`,
+ };
+ res.status(500).json(response);
+ return;
+ }
+
+ // Truncate very large files to avoid token limits
+ const MAX_CONTENT_LENGTH = 50000;
+ const truncated = fileContent.length > MAX_CONTENT_LENGTH;
+ const contentToAnalyze = truncated
+ ? fileContent.substring(0, MAX_CONTENT_LENGTH)
+ : fileContent;
+
+ // Get the filename for context
+ const fileName = path.basename(resolvedPath);
+
+ // Build prompt with file content passed as structured data
+ // The file content is included directly, not via tool invocation
+ const instructionText = `Analyze the following file and provide a 1-2 sentence description suitable for use as context in an AI coding assistant. Focus on what the file contains, its purpose, and why an AI agent might want to use this context in the future (e.g., "API documentation for the authentication endpoints", "Configuration file for database connections", "Coding style guidelines for the project").
+
+Respond with ONLY the description text, no additional formatting, preamble, or explanation.
+
+File: ${fileName}${truncated ? ' (truncated)' : ''}`;
+
+ const promptContent = [
+ { type: 'text' as const, text: instructionText },
+ { type: 'text' as const, text: `\n\n--- FILE CONTENT ---\n${contentToAnalyze}` },
+ ];
+
+ // Use the file's directory as the working directory
+ const cwd = path.dirname(resolvedPath);
+
+ // Use centralized SDK options with proper cwd validation
+ // No tools needed since we're passing file content directly
+ const sdkOptions = createCustomOptions({
+ cwd,
+ model: CLAUDE_MODEL_MAP.haiku,
+ maxTurns: 1,
+ allowedTools: [],
+ sandbox: { enabled: true, autoAllowBashIfSandboxed: true },
+ });
+
+ const promptGenerator = (async function* () {
+ yield {
+ type: 'user' as const,
+ session_id: '',
+ message: { role: 'user' as const, content: promptContent },
+ parent_tool_use_id: null,
+ };
+ })();
+
+ const stream = query({ prompt: promptGenerator, options: sdkOptions });
+
+ // Extract the description from the response
+ const description = await extractTextFromStream(stream);
+
+ if (!description || description.trim().length === 0) {
+ logger.warn('Received empty response from Claude');
+ const response: DescribeFileErrorResponse = {
+ success: false,
+ error: 'Failed to generate description - empty response',
+ };
+ res.status(500).json(response);
+ return;
+ }
+
+ logger.info(`Description generated, length: ${description.length} chars`);
+
+ const response: DescribeFileSuccessResponse = {
+ success: true,
+ description: description.trim(),
+ };
+ res.json(response);
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
+ logger.error('File description failed:', errorMessage);
+
+ const response: DescribeFileErrorResponse = {
+ success: false,
+ error: errorMessage,
+ };
+ res.status(500).json(response);
+ }
+ };
+}
diff --git a/apps/server/src/routes/context/routes/describe-image.ts b/apps/server/src/routes/context/routes/describe-image.ts
new file mode 100644
index 00000000..64ddfa0f
--- /dev/null
+++ b/apps/server/src/routes/context/routes/describe-image.ts
@@ -0,0 +1,416 @@
+/**
+ * POST /context/describe-image endpoint - Generate description for an image
+ *
+ * Uses Claude Haiku to analyze an image and generate a concise description
+ * suitable for context file metadata.
+ *
+ * IMPORTANT:
+ * The agent runner (chat/auto-mode) sends images as multi-part content blocks (base64 image blocks),
+ * not by asking Claude to use the Read tool to open files. This endpoint now mirrors that approach
+ * so it doesn't depend on Claude's filesystem tool access or working directory restrictions.
+ */
+
+import type { Request, Response } from 'express';
+import { query } from '@anthropic-ai/claude-agent-sdk';
+import { createLogger, readImageAsBase64 } from '@automaker/utils';
+import { CLAUDE_MODEL_MAP } from '@automaker/types';
+import { createCustomOptions } from '../../../lib/sdk-options.js';
+import * as fs from 'fs';
+import * as path from 'path';
+
+const logger = createLogger('DescribeImage');
+
+/**
+ * Allowlist of safe headers to log
+ * All other headers are excluded to prevent leaking sensitive values
+ */
+const SAFE_HEADERS_ALLOWLIST = new Set([
+ 'content-type',
+ 'accept',
+ 'user-agent',
+ 'host',
+ 'referer',
+ 'content-length',
+ 'origin',
+ 'x-request-id',
+]);
+
+/**
+ * Filter request headers to only include safe, non-sensitive values
+ */
+function filterSafeHeaders(headers: Record): Record {
+ const filtered: Record = {};
+ for (const [key, value] of Object.entries(headers)) {
+ if (SAFE_HEADERS_ALLOWLIST.has(key.toLowerCase())) {
+ filtered[key] = value;
+ }
+ }
+ return filtered;
+}
+
+/**
+ * Find the actual file path, handling Unicode character variations.
+ * macOS screenshots use U+202F (NARROW NO-BREAK SPACE) before AM/PM,
+ * but this may be transmitted as a regular space through the API.
+ */
+function findActualFilePath(requestedPath: string): string | null {
+ // First, try the exact path
+ if (fs.existsSync(requestedPath)) {
+ return requestedPath;
+ }
+
+ // Try with Unicode normalization
+ const normalizedPath = requestedPath.normalize('NFC');
+ if (fs.existsSync(normalizedPath)) {
+ return normalizedPath;
+ }
+
+ // If not found, try to find the file in the directory by matching the basename
+ // This handles cases where the space character differs (U+0020 vs U+202F vs U+00A0)
+ const dir = path.dirname(requestedPath);
+ const baseName = path.basename(requestedPath);
+
+ if (!fs.existsSync(dir)) {
+ return null;
+ }
+
+ try {
+ const files = fs.readdirSync(dir);
+
+ // Normalize the requested basename for comparison
+ // Replace various space-like characters with regular space for comparison
+ const normalizeSpaces = (s: string): string => s.replace(/[\u00A0\u202F\u2009\u200A]/g, ' ');
+
+ const normalizedBaseName = normalizeSpaces(baseName);
+
+ for (const file of files) {
+ if (normalizeSpaces(file) === normalizedBaseName) {
+ logger.info(`Found matching file with different space encoding: ${file}`);
+ return path.join(dir, file);
+ }
+ }
+ } catch (err) {
+ logger.error(`Error reading directory ${dir}: ${err}`);
+ }
+
+ return null;
+}
+
+/**
+ * Request body for the describe-image endpoint
+ */
+interface DescribeImageRequestBody {
+ /** Path to the image file */
+ imagePath: string;
+}
+
+/**
+ * Success response from the describe-image endpoint
+ */
+interface DescribeImageSuccessResponse {
+ success: true;
+ description: string;
+}
+
+/**
+ * Error response from the describe-image endpoint
+ */
+interface DescribeImageErrorResponse {
+ success: false;
+ error: string;
+ requestId?: string;
+}
+
+/**
+ * Map SDK/CLI errors to a stable status + user-facing message.
+ */
+function mapDescribeImageError(rawMessage: string | undefined): {
+ statusCode: number;
+ userMessage: string;
+} {
+ const baseResponse = {
+ statusCode: 500,
+ userMessage: 'Failed to generate an image description. Please try again.',
+ };
+
+ if (!rawMessage) return baseResponse;
+
+ if (rawMessage.includes('Claude Code process exited')) {
+ return {
+ statusCode: 503,
+ userMessage:
+ 'Claude exited unexpectedly while describing the image. Try again. If it keeps happening, re-run `claude login` or update your API key in Setup so Claude can restart cleanly.',
+ };
+ }
+
+ if (
+ rawMessage.includes('Failed to spawn Claude Code process') ||
+ rawMessage.includes('Claude Code executable not found') ||
+ rawMessage.includes('Claude Code native binary not found')
+ ) {
+ return {
+ statusCode: 503,
+ userMessage:
+ 'Claude CLI could not be launched. Make sure the Claude CLI is installed and available in PATH, then try again.',
+ };
+ }
+
+ if (rawMessage.toLowerCase().includes('rate limit') || rawMessage.includes('429')) {
+ return {
+ statusCode: 429,
+ userMessage: 'Rate limited while describing the image. Please wait a moment and try again.',
+ };
+ }
+
+ if (rawMessage.toLowerCase().includes('payload too large') || rawMessage.includes('413')) {
+ return {
+ statusCode: 413,
+ userMessage:
+ 'The image is too large to send for description. Please resize/compress it and try again.',
+ };
+ }
+
+ return baseResponse;
+}
+
+/**
+ * Extract text content from Claude SDK response messages and log high-signal stream events.
+ */
+async function extractTextFromStream(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ stream: AsyncIterable,
+ requestId: string
+): Promise {
+ let responseText = '';
+ let messageCount = 0;
+
+ logger.info(`[${requestId}] [Stream] Begin reading SDK stream...`);
+
+ for await (const msg of stream) {
+ messageCount++;
+ const msgType = msg?.type;
+ const msgSubtype = msg?.subtype;
+
+ // Keep this concise but informative. Full error object is logged in catch blocks.
+ logger.info(
+ `[${requestId}] [Stream] #${messageCount} type=${String(msgType)} subtype=${String(msgSubtype ?? '')}`
+ );
+
+ if (msgType === 'assistant' && msg.message?.content) {
+ const blocks = msg.message.content as Array<{ type: string; text?: string }>;
+ logger.info(`[${requestId}] [Stream] assistant blocks=${blocks.length}`);
+ for (const block of blocks) {
+ if (block.type === 'text' && block.text) {
+ responseText += block.text;
+ }
+ }
+ }
+
+ if (msgType === 'result' && msgSubtype === 'success') {
+ if (typeof msg.result === 'string' && msg.result.length > 0) {
+ responseText = msg.result;
+ }
+ }
+ }
+
+ logger.info(
+ `[${requestId}] [Stream] End of stream. messages=${messageCount} textLength=${responseText.length}`
+ );
+
+ return responseText;
+}
+
+/**
+ * Create the describe-image request handler
+ *
+ * Uses Claude SDK query with multi-part content blocks to include the image (base64),
+ * matching the agent runner behavior.
+ *
+ * @returns Express request handler for image description
+ */
+export function createDescribeImageHandler(): (req: Request, res: Response) => Promise {
+ return async (req: Request, res: Response): Promise => {
+ const requestId = `describe-image-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
+ const startedAt = Date.now();
+
+ // Request envelope logs (high value when correlating failures)
+ // Only log safe headers to prevent leaking sensitive values (auth tokens, cookies, etc.)
+ logger.info(`[${requestId}] ===== POST /api/context/describe-image =====`);
+ logger.info(`[${requestId}] headers=${JSON.stringify(filterSafeHeaders(req.headers))}`);
+ logger.info(`[${requestId}] body=${JSON.stringify(req.body)}`);
+
+ try {
+ const { imagePath } = req.body as DescribeImageRequestBody;
+
+ // Validate required fields
+ if (!imagePath || typeof imagePath !== 'string') {
+ const response: DescribeImageErrorResponse = {
+ success: false,
+ error: 'imagePath is required and must be a string',
+ requestId,
+ };
+ res.status(400).json(response);
+ return;
+ }
+
+ logger.info(`[${requestId}] imagePath="${imagePath}" type=${typeof imagePath}`);
+
+ // Find the actual file path (handles Unicode space character variations)
+ const actualPath = findActualFilePath(imagePath);
+ if (!actualPath) {
+ logger.error(`[${requestId}] File not found: ${imagePath}`);
+ // Log hex representation of the path for debugging
+ const hexPath = Buffer.from(imagePath).toString('hex');
+ logger.error(`[${requestId}] imagePath hex: ${hexPath}`);
+ const response: DescribeImageErrorResponse = {
+ success: false,
+ error: `File not found: ${imagePath}`,
+ requestId,
+ };
+ res.status(404).json(response);
+ return;
+ }
+
+ if (actualPath !== imagePath) {
+ logger.info(`[${requestId}] Using actual path: ${actualPath}`);
+ }
+
+ // Log path + stats (this is often where issues start: missing file, perms, size)
+ let stat: fs.Stats | null = null;
+ try {
+ stat = fs.statSync(actualPath);
+ logger.info(
+ `[${requestId}] fileStats size=${stat.size} bytes mtime=${stat.mtime.toISOString()}`
+ );
+ } catch (statErr) {
+ logger.warn(
+ `[${requestId}] Unable to stat image file (continuing to read base64): ${String(statErr)}`
+ );
+ }
+
+ // Read image and convert to base64 (same as agent runner)
+ logger.info(`[${requestId}] Reading image into base64...`);
+ const imageReadStart = Date.now();
+ const imageData = await readImageAsBase64(actualPath);
+ const imageReadMs = Date.now() - imageReadStart;
+
+ const base64Length = imageData.base64.length;
+ const estimatedBytes = Math.ceil((base64Length * 3) / 4);
+ logger.info(`[${requestId}] imageReadMs=${imageReadMs}`);
+ logger.info(
+ `[${requestId}] image meta filename=${imageData.filename} mime=${imageData.mimeType} base64Len=${base64Length} estBytes=${estimatedBytes}`
+ );
+
+ // Build multi-part prompt with image block (no Read tool required)
+ const instructionText =
+ `Describe this image in 1-2 sentences suitable for use as context in an AI coding assistant. ` +
+ `Focus on what the image shows and its purpose (e.g., "UI mockup showing login form with email/password fields", ` +
+ `"Architecture diagram of microservices", "Screenshot of error message in terminal").\n\n` +
+ `Respond with ONLY the description text, no additional formatting, preamble, or explanation.`;
+
+ const promptContent = [
+ { type: 'text' as const, text: instructionText },
+ {
+ type: 'image' as const,
+ source: {
+ type: 'base64' as const,
+ media_type: imageData.mimeType,
+ data: imageData.base64,
+ },
+ },
+ ];
+
+ logger.info(`[${requestId}] Built multi-part prompt blocks=${promptContent.length}`);
+
+ const cwd = path.dirname(actualPath);
+ logger.info(`[${requestId}] Using cwd=${cwd}`);
+
+ // Use the same centralized option builder used across the server (validates cwd)
+ const sdkOptions = createCustomOptions({
+ cwd,
+ model: CLAUDE_MODEL_MAP.haiku,
+ maxTurns: 1,
+ allowedTools: [],
+ sandbox: { enabled: true, autoAllowBashIfSandboxed: true },
+ });
+
+ logger.info(
+ `[${requestId}] SDK options model=${sdkOptions.model} maxTurns=${sdkOptions.maxTurns} allowedTools=${JSON.stringify(
+ sdkOptions.allowedTools
+ )} sandbox=${JSON.stringify(sdkOptions.sandbox)}`
+ );
+
+ const promptGenerator = (async function* () {
+ yield {
+ type: 'user' as const,
+ session_id: '',
+ message: { role: 'user' as const, content: promptContent },
+ parent_tool_use_id: null,
+ };
+ })();
+
+ logger.info(`[${requestId}] Calling query()...`);
+ const queryStart = Date.now();
+ const stream = query({ prompt: promptGenerator, options: sdkOptions });
+ logger.info(`[${requestId}] query() returned stream in ${Date.now() - queryStart}ms`);
+
+ // Extract the description from the response
+ const extractStart = Date.now();
+ const description = await extractTextFromStream(stream, requestId);
+ logger.info(`[${requestId}] extractMs=${Date.now() - extractStart}`);
+
+ if (!description || description.trim().length === 0) {
+ logger.warn(`[${requestId}] Received empty response from Claude`);
+ const response: DescribeImageErrorResponse = {
+ success: false,
+ error: 'Failed to generate description - empty response',
+ requestId,
+ };
+ res.status(500).json(response);
+ return;
+ }
+
+ const totalMs = Date.now() - startedAt;
+ logger.info(`[${requestId}] Success descriptionLen=${description.length} totalMs=${totalMs}`);
+
+ const response: DescribeImageSuccessResponse = {
+ success: true,
+ description: description.trim(),
+ };
+ res.json(response);
+ } catch (error) {
+ const totalMs = Date.now() - startedAt;
+ const err = error as unknown;
+ const errMessage = err instanceof Error ? err.message : String(err);
+ const errName = err instanceof Error ? err.name : 'UnknownError';
+ const errStack = err instanceof Error ? err.stack : undefined;
+
+ logger.error(`[${requestId}] FAILED totalMs=${totalMs}`);
+ logger.error(`[${requestId}] errorName=${errName}`);
+ logger.error(`[${requestId}] errorMessage=${errMessage}`);
+ if (errStack) logger.error(`[${requestId}] errorStack=${errStack}`);
+
+ // Dump all enumerable + non-enumerable props (this is where stderr/stdout/exitCode often live)
+ try {
+ const props = err && typeof err === 'object' ? Object.getOwnPropertyNames(err) : [];
+ logger.error(`[${requestId}] errorProps=${JSON.stringify(props)}`);
+ if (err && typeof err === 'object') {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const anyErr = err as any;
+ const details = JSON.stringify(anyErr, props as unknown as string[]);
+ logger.error(`[${requestId}] errorDetails=${details}`);
+ }
+ } catch (stringifyErr) {
+ logger.error(`[${requestId}] Failed to serialize error object: ${String(stringifyErr)}`);
+ }
+
+ const { statusCode, userMessage } = mapDescribeImageError(errMessage);
+ const response: DescribeImageErrorResponse = {
+ success: false,
+ error: `${userMessage} (requestId: ${requestId})`,
+ requestId,
+ };
+ res.status(statusCode).json(response);
+ }
+ };
+}
diff --git a/apps/server/src/routes/enhance-prompt/index.ts b/apps/server/src/routes/enhance-prompt/index.ts
index bd414a5d..952bf347 100644
--- a/apps/server/src/routes/enhance-prompt/index.ts
+++ b/apps/server/src/routes/enhance-prompt/index.ts
@@ -5,8 +5,8 @@
* with different enhancement modes (improve, expand, simplify, etc.)
*/
-import { Router } from "express";
-import { createEnhanceHandler } from "./routes/enhance.js";
+import { Router } from 'express';
+import { createEnhanceHandler } from './routes/enhance.js';
/**
* Create the enhance-prompt router
@@ -16,7 +16,7 @@ import { createEnhanceHandler } from "./routes/enhance.js";
export function createEnhancePromptRoutes(): Router {
const router = Router();
- router.post("/", createEnhanceHandler());
+ router.post('/', createEnhanceHandler());
return router;
}
diff --git a/apps/server/src/routes/enhance-prompt/routes/enhance.ts b/apps/server/src/routes/enhance-prompt/routes/enhance.ts
index 9c7611b5..e0edd515 100644
--- a/apps/server/src/routes/enhance-prompt/routes/enhance.ts
+++ b/apps/server/src/routes/enhance-prompt/routes/enhance.ts
@@ -5,19 +5,19 @@
* Supports modes: improve, technical, simplify, acceptance
*/
-import type { Request, Response } from "express";
-import { query } from "@anthropic-ai/claude-agent-sdk";
-import { createLogger } from "@automaker/utils";
-import { resolveModelString } from "@automaker/model-resolver";
-import { CLAUDE_MODEL_MAP } from "@automaker/types";
+import type { Request, Response } from 'express';
+import { query } from '@anthropic-ai/claude-agent-sdk';
+import { createLogger } from '@automaker/utils';
+import { resolveModelString } from '@automaker/model-resolver';
+import { CLAUDE_MODEL_MAP } from '@automaker/types';
import {
getSystemPrompt,
buildUserPrompt,
isValidEnhancementMode,
type EnhancementMode,
-} from "../../../lib/enhancement-prompts.js";
+} from '../../../lib/enhancement-prompts.js';
-const logger = createLogger("EnhancePrompt");
+const logger = createLogger('EnhancePrompt');
/**
* Request body for the enhance endpoint
@@ -63,16 +63,16 @@ async function extractTextFromStream(
};
}>
): Promise {
- let responseText = "";
+ let responseText = '';
for await (const msg of stream) {
- if (msg.type === "assistant" && msg.message?.content) {
+ if (msg.type === 'assistant' && msg.message?.content) {
for (const block of msg.message.content) {
- if (block.type === "text" && block.text) {
+ if (block.type === 'text' && block.text) {
responseText += block.text;
}
}
- } else if (msg.type === "result" && msg.subtype === "success") {
+ } else if (msg.type === 'result' && msg.subtype === 'success') {
responseText = msg.result || responseText;
}
}
@@ -85,29 +85,25 @@ async function extractTextFromStream(
*
* @returns Express request handler for text enhancement
*/
-export function createEnhanceHandler(): (
- req: Request,
- res: Response
-) => Promise {
+export function createEnhanceHandler(): (req: Request, res: Response) => Promise {
return async (req: Request, res: Response): Promise => {
try {
- const { originalText, enhancementMode, model } =
- req.body as EnhanceRequestBody;
+ const { originalText, enhancementMode, model } = req.body as EnhanceRequestBody;
// Validate required fields
- if (!originalText || typeof originalText !== "string") {
+ if (!originalText || typeof originalText !== 'string') {
const response: EnhanceErrorResponse = {
success: false,
- error: "originalText is required and must be a string",
+ error: 'originalText is required and must be a string',
};
res.status(400).json(response);
return;
}
- if (!enhancementMode || typeof enhancementMode !== "string") {
+ if (!enhancementMode || typeof enhancementMode !== 'string') {
const response: EnhanceErrorResponse = {
success: false,
- error: "enhancementMode is required and must be a string",
+ error: 'enhancementMode is required and must be a string',
};
res.status(400).json(response);
return;
@@ -118,7 +114,7 @@ export function createEnhanceHandler(): (
if (trimmedText.length === 0) {
const response: EnhanceErrorResponse = {
success: false,
- error: "originalText cannot be empty",
+ error: 'originalText cannot be empty',
};
res.status(400).json(response);
return;
@@ -128,11 +124,9 @@ export function createEnhanceHandler(): (
const normalizedMode = enhancementMode.toLowerCase();
const validMode: EnhancementMode = isValidEnhancementMode(normalizedMode)
? normalizedMode
- : "improve";
+ : 'improve';
- logger.info(
- `Enhancing text with mode: ${validMode}, length: ${trimmedText.length} chars`
- );
+ logger.info(`Enhancing text with mode: ${validMode}, length: ${trimmedText.length} chars`);
// Get the system prompt for this mode
const systemPrompt = getSystemPrompt(validMode);
@@ -155,7 +149,7 @@ export function createEnhanceHandler(): (
systemPrompt,
maxTurns: 1,
allowedTools: [],
- permissionMode: "acceptEdits",
+ permissionMode: 'acceptEdits',
},
});
@@ -163,18 +157,16 @@ export function createEnhanceHandler(): (
const enhancedText = await extractTextFromStream(stream);
if (!enhancedText || enhancedText.trim().length === 0) {
- logger.warn("Received empty response from Claude");
+ logger.warn('Received empty response from Claude');
const response: EnhanceErrorResponse = {
success: false,
- error: "Failed to generate enhanced text - empty response",
+ error: 'Failed to generate enhanced text - empty response',
};
res.status(500).json(response);
return;
}
- logger.info(
- `Enhancement complete, output length: ${enhancedText.length} chars`
- );
+ logger.info(`Enhancement complete, output length: ${enhancedText.length} chars`);
const response: EnhanceSuccessResponse = {
success: true,
@@ -182,9 +174,8 @@ export function createEnhanceHandler(): (
};
res.json(response);
} catch (error) {
- const errorMessage =
- error instanceof Error ? error.message : "Unknown error occurred";
- logger.error("Enhancement failed:", errorMessage);
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
+ logger.error('Enhancement failed:', errorMessage);
const response: EnhanceErrorResponse = {
success: false,
diff --git a/apps/server/src/routes/features/common.ts b/apps/server/src/routes/features/common.ts
index 5006586f..7a5bf8f1 100644
--- a/apps/server/src/routes/features/common.ts
+++ b/apps/server/src/routes/features/common.ts
@@ -2,13 +2,10 @@
* Common utilities for features routes
*/
-import { createLogger } from "@automaker/utils";
-import {
- getErrorMessage as getErrorMessageShared,
- createLogError,
-} from "../common.js";
+import { createLogger } from '@automaker/utils';
+import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
-const logger = createLogger("Features");
+const logger = createLogger('Features');
// Re-export shared utilities
export { getErrorMessageShared as getErrorMessage };
diff --git a/apps/server/src/routes/features/index.ts b/apps/server/src/routes/features/index.ts
index dcd98f56..5863c4d4 100644
--- a/apps/server/src/routes/features/index.ts
+++ b/apps/server/src/routes/features/index.ts
@@ -2,27 +2,27 @@
* Features routes - HTTP API for feature management
*/
-import { Router } from "express";
-import { FeatureLoader } from "../../services/feature-loader.js";
-import { validatePathParams } from "../../middleware/validate-paths.js";
-import { createListHandler } from "./routes/list.js";
-import { createGetHandler } from "./routes/get.js";
-import { createCreateHandler } from "./routes/create.js";
-import { createUpdateHandler } from "./routes/update.js";
-import { createDeleteHandler } from "./routes/delete.js";
-import { createAgentOutputHandler } from "./routes/agent-output.js";
-import { createGenerateTitleHandler } from "./routes/generate-title.js";
+import { Router } from 'express';
+import { FeatureLoader } from '../../services/feature-loader.js';
+import { validatePathParams } from '../../middleware/validate-paths.js';
+import { createListHandler } from './routes/list.js';
+import { createGetHandler } from './routes/get.js';
+import { createCreateHandler } from './routes/create.js';
+import { createUpdateHandler } from './routes/update.js';
+import { createDeleteHandler } from './routes/delete.js';
+import { createAgentOutputHandler } from './routes/agent-output.js';
+import { createGenerateTitleHandler } from './routes/generate-title.js';
export function createFeaturesRoutes(featureLoader: FeatureLoader): Router {
const router = Router();
- router.post("/list", validatePathParams("projectPath"), createListHandler(featureLoader));
- router.post("/get", validatePathParams("projectPath"), createGetHandler(featureLoader));
- router.post("/create", validatePathParams("projectPath"), createCreateHandler(featureLoader));
- router.post("/update", validatePathParams("projectPath"), createUpdateHandler(featureLoader));
- router.post("/delete", validatePathParams("projectPath"), createDeleteHandler(featureLoader));
- router.post("/agent-output", createAgentOutputHandler(featureLoader));
- router.post("/generate-title", createGenerateTitleHandler());
+ router.post('/list', validatePathParams('projectPath'), createListHandler(featureLoader));
+ router.post('/get', validatePathParams('projectPath'), createGetHandler(featureLoader));
+ router.post('/create', validatePathParams('projectPath'), createCreateHandler(featureLoader));
+ router.post('/update', validatePathParams('projectPath'), createUpdateHandler(featureLoader));
+ router.post('/delete', validatePathParams('projectPath'), createDeleteHandler(featureLoader));
+ router.post('/agent-output', createAgentOutputHandler(featureLoader));
+ router.post('/generate-title', createGenerateTitleHandler());
return router;
}
diff --git a/apps/server/src/routes/features/routes/agent-output.ts b/apps/server/src/routes/features/routes/agent-output.ts
index 62f8f50a..f928644a 100644
--- a/apps/server/src/routes/features/routes/agent-output.ts
+++ b/apps/server/src/routes/features/routes/agent-output.ts
@@ -2,9 +2,9 @@
* POST /agent-output endpoint - Get agent output for a feature
*/
-import type { Request, Response } from "express";
-import { FeatureLoader } from "../../../services/feature-loader.js";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import { FeatureLoader } from '../../../services/feature-loader.js';
+import { getErrorMessage, logError } from '../common.js';
export function createAgentOutputHandler(featureLoader: FeatureLoader) {
return async (req: Request, res: Response): Promise => {
@@ -15,22 +15,17 @@ export function createAgentOutputHandler(featureLoader: FeatureLoader) {
};
if (!projectPath || !featureId) {
- res
- .status(400)
- .json({
- success: false,
- error: "projectPath and featureId are required",
- });
+ res.status(400).json({
+ success: false,
+ error: 'projectPath and featureId are required',
+ });
return;
}
- const content = await featureLoader.getAgentOutput(
- projectPath,
- featureId
- );
+ const content = await featureLoader.getAgentOutput(projectPath, featureId);
res.json({ success: true, content });
} catch (error) {
- logError(error, "Get agent output failed");
+ logError(error, 'Get agent output failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/features/routes/create.ts b/apps/server/src/routes/features/routes/create.ts
index cd95b487..5f04ecdb 100644
--- a/apps/server/src/routes/features/routes/create.ts
+++ b/apps/server/src/routes/features/routes/create.ts
@@ -2,10 +2,10 @@
* POST /create endpoint - Create a new feature
*/
-import type { Request, Response } from "express";
-import { FeatureLoader } from "../../../services/feature-loader.js";
-import type { Feature } from "@automaker/types";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import { FeatureLoader } from '../../../services/feature-loader.js';
+import type { Feature } from '@automaker/types';
+import { getErrorMessage, logError } from '../common.js';
export function createCreateHandler(featureLoader: FeatureLoader) {
return async (req: Request, res: Response): Promise => {
@@ -18,7 +18,7 @@ export function createCreateHandler(featureLoader: FeatureLoader) {
if (!projectPath || !feature) {
res.status(400).json({
success: false,
- error: "projectPath and feature are required",
+ error: 'projectPath and feature are required',
});
return;
}
@@ -26,7 +26,7 @@ export function createCreateHandler(featureLoader: FeatureLoader) {
const created = await featureLoader.create(projectPath, feature);
res.json({ success: true, feature: created });
} catch (error) {
- logError(error, "Create feature failed");
+ logError(error, 'Create feature failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/features/routes/delete.ts b/apps/server/src/routes/features/routes/delete.ts
index bf5408d5..2b6831f6 100644
--- a/apps/server/src/routes/features/routes/delete.ts
+++ b/apps/server/src/routes/features/routes/delete.ts
@@ -2,9 +2,9 @@
* POST /delete endpoint - Delete a feature
*/
-import type { Request, Response } from "express";
-import { FeatureLoader } from "../../../services/feature-loader.js";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import { FeatureLoader } from '../../../services/feature-loader.js';
+import { getErrorMessage, logError } from '../common.js';
export function createDeleteHandler(featureLoader: FeatureLoader) {
return async (req: Request, res: Response): Promise => {
@@ -15,19 +15,17 @@ export function createDeleteHandler(featureLoader: FeatureLoader) {
};
if (!projectPath || !featureId) {
- res
- .status(400)
- .json({
- success: false,
- error: "projectPath and featureId are required",
- });
+ res.status(400).json({
+ success: false,
+ error: 'projectPath and featureId are required',
+ });
return;
}
const success = await featureLoader.delete(projectPath, featureId);
res.json({ success });
} catch (error) {
- logError(error, "Delete feature failed");
+ logError(error, 'Delete feature failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/features/routes/generate-title.ts b/apps/server/src/routes/features/routes/generate-title.ts
index 8c6c9a30..1225a825 100644
--- a/apps/server/src/routes/features/routes/generate-title.ts
+++ b/apps/server/src/routes/features/routes/generate-title.ts
@@ -4,12 +4,12 @@
* Uses Claude Haiku to generate a short, descriptive title from feature description.
*/
-import type { Request, Response } from "express";
-import { query } from "@anthropic-ai/claude-agent-sdk";
-import { createLogger } from "@automaker/utils";
-import { CLAUDE_MODEL_MAP } from "@automaker/model-resolver";
+import type { Request, Response } from 'express';
+import { query } from '@anthropic-ai/claude-agent-sdk';
+import { createLogger } from '@automaker/utils';
+import { CLAUDE_MODEL_MAP } from '@automaker/model-resolver';
-const logger = createLogger("GenerateTitle");
+const logger = createLogger('GenerateTitle');
interface GenerateTitleRequestBody {
description: string;
@@ -44,16 +44,16 @@ async function extractTextFromStream(
};
}>
): Promise {
- let responseText = "";
+ let responseText = '';
for await (const msg of stream) {
- if (msg.type === "assistant" && msg.message?.content) {
+ if (msg.type === 'assistant' && msg.message?.content) {
for (const block of msg.message.content) {
- if (block.type === "text" && block.text) {
+ if (block.type === 'text' && block.text) {
responseText += block.text;
}
}
- } else if (msg.type === "result" && msg.subtype === "success") {
+ } else if (msg.type === 'result' && msg.subtype === 'success') {
responseText = msg.result || responseText;
}
}
@@ -61,18 +61,15 @@ async function extractTextFromStream(
return responseText;
}
-export function createGenerateTitleHandler(): (
- req: Request,
- res: Response
-) => Promise {
+export function createGenerateTitleHandler(): (req: Request, res: Response) => Promise {
return async (req: Request, res: Response): Promise => {
try {
const { description } = req.body as GenerateTitleRequestBody;
- if (!description || typeof description !== "string") {
+ if (!description || typeof description !== 'string') {
const response: GenerateTitleErrorResponse = {
success: false,
- error: "description is required and must be a string",
+ error: 'description is required and must be a string',
};
res.status(400).json(response);
return;
@@ -82,7 +79,7 @@ export function createGenerateTitleHandler(): (
if (trimmedDescription.length === 0) {
const response: GenerateTitleErrorResponse = {
success: false,
- error: "description cannot be empty",
+ error: 'description cannot be empty',
};
res.status(400).json(response);
return;
@@ -99,17 +96,17 @@ export function createGenerateTitleHandler(): (
systemPrompt: SYSTEM_PROMPT,
maxTurns: 1,
allowedTools: [],
- permissionMode: "acceptEdits",
+ permissionMode: 'acceptEdits',
},
});
const title = await extractTextFromStream(stream);
if (!title || title.trim().length === 0) {
- logger.warn("Received empty response from Claude");
+ logger.warn('Received empty response from Claude');
const response: GenerateTitleErrorResponse = {
success: false,
- error: "Failed to generate title - empty response",
+ error: 'Failed to generate title - empty response',
};
res.status(500).json(response);
return;
@@ -123,9 +120,8 @@ export function createGenerateTitleHandler(): (
};
res.json(response);
} catch (error) {
- const errorMessage =
- error instanceof Error ? error.message : "Unknown error occurred";
- logger.error("Title generation failed:", errorMessage);
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
+ logger.error('Title generation failed:', errorMessage);
const response: GenerateTitleErrorResponse = {
success: false,
diff --git a/apps/server/src/routes/features/routes/get.ts b/apps/server/src/routes/features/routes/get.ts
index 17900bb0..96f63fb8 100644
--- a/apps/server/src/routes/features/routes/get.ts
+++ b/apps/server/src/routes/features/routes/get.ts
@@ -2,9 +2,9 @@
* POST /get endpoint - Get a single feature
*/
-import type { Request, Response } from "express";
-import { FeatureLoader } from "../../../services/feature-loader.js";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import { FeatureLoader } from '../../../services/feature-loader.js';
+import { getErrorMessage, logError } from '../common.js';
export function createGetHandler(featureLoader: FeatureLoader) {
return async (req: Request, res: Response): Promise => {
@@ -15,24 +15,22 @@ export function createGetHandler(featureLoader: FeatureLoader) {
};
if (!projectPath || !featureId) {
- res
- .status(400)
- .json({
- success: false,
- error: "projectPath and featureId are required",
- });
+ res.status(400).json({
+ success: false,
+ error: 'projectPath and featureId are required',
+ });
return;
}
const feature = await featureLoader.get(projectPath, featureId);
if (!feature) {
- res.status(404).json({ success: false, error: "Feature not found" });
+ res.status(404).json({ success: false, error: 'Feature not found' });
return;
}
res.json({ success: true, feature });
} catch (error) {
- logError(error, "Get feature failed");
+ logError(error, 'Get feature failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/features/routes/list.ts b/apps/server/src/routes/features/routes/list.ts
index cc20b1a1..00127fc9 100644
--- a/apps/server/src/routes/features/routes/list.ts
+++ b/apps/server/src/routes/features/routes/list.ts
@@ -2,9 +2,9 @@
* POST /list endpoint - List all features for a project
*/
-import type { Request, Response } from "express";
-import { FeatureLoader } from "../../../services/feature-loader.js";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import { FeatureLoader } from '../../../services/feature-loader.js';
+import { getErrorMessage, logError } from '../common.js';
export function createListHandler(featureLoader: FeatureLoader) {
return async (req: Request, res: Response): Promise => {
@@ -12,16 +12,14 @@ export function createListHandler(featureLoader: FeatureLoader) {
const { projectPath } = req.body as { projectPath: string };
if (!projectPath) {
- res
- .status(400)
- .json({ success: false, error: "projectPath is required" });
+ res.status(400).json({ success: false, error: 'projectPath is required' });
return;
}
const features = await featureLoader.getAll(projectPath);
res.json({ success: true, features });
} catch (error) {
- logError(error, "List features failed");
+ logError(error, 'List features failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/features/routes/update.ts b/apps/server/src/routes/features/routes/update.ts
index 8c4c7b68..830fb21a 100644
--- a/apps/server/src/routes/features/routes/update.ts
+++ b/apps/server/src/routes/features/routes/update.ts
@@ -2,10 +2,10 @@
* POST /update endpoint - Update a feature
*/
-import type { Request, Response } from "express";
-import { FeatureLoader } from "../../../services/feature-loader.js";
-import type { Feature } from "@automaker/types";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import { FeatureLoader } from '../../../services/feature-loader.js';
+import type { Feature } from '@automaker/types';
+import { getErrorMessage, logError } from '../common.js';
export function createUpdateHandler(featureLoader: FeatureLoader) {
return async (req: Request, res: Response): Promise => {
@@ -19,19 +19,15 @@ export function createUpdateHandler(featureLoader: FeatureLoader) {
if (!projectPath || !featureId || !updates) {
res.status(400).json({
success: false,
- error: "projectPath, featureId, and updates are required",
+ error: 'projectPath, featureId, and updates are required',
});
return;
}
- const updated = await featureLoader.update(
- projectPath,
- featureId,
- updates
- );
+ const updated = await featureLoader.update(projectPath, featureId, updates);
res.json({ success: true, feature: updated });
} catch (error) {
- logError(error, "Update feature failed");
+ logError(error, 'Update feature failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/fs/common.ts b/apps/server/src/routes/fs/common.ts
index 84191451..6386c83e 100644
--- a/apps/server/src/routes/fs/common.ts
+++ b/apps/server/src/routes/fs/common.ts
@@ -2,13 +2,10 @@
* Common utilities for fs routes
*/
-import { createLogger } from "@automaker/utils";
-import {
- getErrorMessage as getErrorMessageShared,
- createLogError,
-} from "../common.js";
+import { createLogger } from '@automaker/utils';
+import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
-const logger = createLogger("FS");
+const logger = createLogger('FS');
// Re-export shared utilities
export { getErrorMessageShared as getErrorMessage };
diff --git a/apps/server/src/routes/fs/index.ts b/apps/server/src/routes/fs/index.ts
index 6fc67dad..58732b3a 100644
--- a/apps/server/src/routes/fs/index.ts
+++ b/apps/server/src/routes/fs/index.ts
@@ -3,40 +3,40 @@
* Provides REST API equivalents for Electron IPC file operations
*/
-import { Router } from "express";
-import type { EventEmitter } from "../../lib/events.js";
-import { createReadHandler } from "./routes/read.js";
-import { createWriteHandler } from "./routes/write.js";
-import { createMkdirHandler } from "./routes/mkdir.js";
-import { createReaddirHandler } from "./routes/readdir.js";
-import { createExistsHandler } from "./routes/exists.js";
-import { createStatHandler } from "./routes/stat.js";
-import { createDeleteHandler } from "./routes/delete.js";
-import { createValidatePathHandler } from "./routes/validate-path.js";
-import { createResolveDirectoryHandler } from "./routes/resolve-directory.js";
-import { createSaveImageHandler } from "./routes/save-image.js";
-import { createBrowseHandler } from "./routes/browse.js";
-import { createImageHandler } from "./routes/image.js";
-import { createSaveBoardBackgroundHandler } from "./routes/save-board-background.js";
-import { createDeleteBoardBackgroundHandler } from "./routes/delete-board-background.js";
+import { Router } from 'express';
+import type { EventEmitter } from '../../lib/events.js';
+import { createReadHandler } from './routes/read.js';
+import { createWriteHandler } from './routes/write.js';
+import { createMkdirHandler } from './routes/mkdir.js';
+import { createReaddirHandler } from './routes/readdir.js';
+import { createExistsHandler } from './routes/exists.js';
+import { createStatHandler } from './routes/stat.js';
+import { createDeleteHandler } from './routes/delete.js';
+import { createValidatePathHandler } from './routes/validate-path.js';
+import { createResolveDirectoryHandler } from './routes/resolve-directory.js';
+import { createSaveImageHandler } from './routes/save-image.js';
+import { createBrowseHandler } from './routes/browse.js';
+import { createImageHandler } from './routes/image.js';
+import { createSaveBoardBackgroundHandler } from './routes/save-board-background.js';
+import { createDeleteBoardBackgroundHandler } from './routes/delete-board-background.js';
export function createFsRoutes(_events: EventEmitter): Router {
const router = Router();
- router.post("/read", createReadHandler());
- router.post("/write", createWriteHandler());
- router.post("/mkdir", createMkdirHandler());
- router.post("/readdir", createReaddirHandler());
- router.post("/exists", createExistsHandler());
- router.post("/stat", createStatHandler());
- router.post("/delete", createDeleteHandler());
- router.post("/validate-path", createValidatePathHandler());
- router.post("/resolve-directory", createResolveDirectoryHandler());
- router.post("/save-image", createSaveImageHandler());
- router.post("/browse", createBrowseHandler());
- router.get("/image", createImageHandler());
- router.post("/save-board-background", createSaveBoardBackgroundHandler());
- router.post("/delete-board-background", createDeleteBoardBackgroundHandler());
+ router.post('/read', createReadHandler());
+ router.post('/write', createWriteHandler());
+ router.post('/mkdir', createMkdirHandler());
+ router.post('/readdir', createReaddirHandler());
+ router.post('/exists', createExistsHandler());
+ router.post('/stat', createStatHandler());
+ router.post('/delete', createDeleteHandler());
+ router.post('/validate-path', createValidatePathHandler());
+ router.post('/resolve-directory', createResolveDirectoryHandler());
+ router.post('/save-image', createSaveImageHandler());
+ router.post('/browse', createBrowseHandler());
+ router.get('/image', createImageHandler());
+ router.post('/save-board-background', createSaveBoardBackgroundHandler());
+ router.post('/delete-board-background', createDeleteBoardBackgroundHandler());
return router;
}
diff --git a/apps/server/src/routes/git/common.ts b/apps/server/src/routes/git/common.ts
index 4d7b9f92..5fd7013e 100644
--- a/apps/server/src/routes/git/common.ts
+++ b/apps/server/src/routes/git/common.ts
@@ -2,13 +2,10 @@
* Common utilities for git routes
*/
-import { createLogger } from "@automaker/utils";
-import {
- getErrorMessage as getErrorMessageShared,
- createLogError,
-} from "../common.js";
+import { createLogger } from '@automaker/utils';
+import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
-const logger = createLogger("Git");
+const logger = createLogger('Git');
// Re-export shared utilities
export { getErrorMessageShared as getErrorMessage };
diff --git a/apps/server/src/routes/git/index.ts b/apps/server/src/routes/git/index.ts
index 25dc333d..5e959ec9 100644
--- a/apps/server/src/routes/git/index.ts
+++ b/apps/server/src/routes/git/index.ts
@@ -2,16 +2,16 @@
* Git routes - HTTP API for git operations (non-worktree)
*/
-import { Router } from "express";
-import { validatePathParams } from "../../middleware/validate-paths.js";
-import { createDiffsHandler } from "./routes/diffs.js";
-import { createFileDiffHandler } from "./routes/file-diff.js";
+import { Router } from 'express';
+import { validatePathParams } from '../../middleware/validate-paths.js';
+import { createDiffsHandler } from './routes/diffs.js';
+import { createFileDiffHandler } from './routes/file-diff.js';
export function createGitRoutes(): Router {
const router = Router();
- router.post("/diffs", validatePathParams("projectPath"), createDiffsHandler());
- router.post("/file-diff", validatePathParams("projectPath", "filePath"), createFileDiffHandler());
+ router.post('/diffs', validatePathParams('projectPath'), createDiffsHandler());
+ router.post('/file-diff', validatePathParams('projectPath', 'filePath'), createFileDiffHandler());
return router;
}
diff --git a/apps/server/src/routes/git/routes/diffs.ts b/apps/server/src/routes/git/routes/diffs.ts
index eb532a03..ca919dcf 100644
--- a/apps/server/src/routes/git/routes/diffs.ts
+++ b/apps/server/src/routes/git/routes/diffs.ts
@@ -2,9 +2,9 @@
* POST /diffs endpoint - Get diffs for the main project
*/
-import type { Request, Response } from "express";
-import { getErrorMessage, logError } from "../common.js";
-import { getGitRepositoryDiffs } from "../../common.js";
+import type { Request, Response } from 'express';
+import { getErrorMessage, logError } from '../common.js';
+import { getGitRepositoryDiffs } from '../../common.js';
export function createDiffsHandler() {
return async (req: Request, res: Response): Promise => {
@@ -12,7 +12,7 @@ export function createDiffsHandler() {
const { projectPath } = req.body as { projectPath: string };
if (!projectPath) {
- res.status(400).json({ success: false, error: "projectPath required" });
+ res.status(400).json({ success: false, error: 'projectPath required' });
return;
}
@@ -25,11 +25,11 @@ export function createDiffsHandler() {
hasChanges: result.hasChanges,
});
} catch (innerError) {
- logError(innerError, "Git diff failed");
- res.json({ success: true, diff: "", files: [], hasChanges: false });
+ logError(innerError, 'Git diff failed');
+ res.json({ success: true, diff: '', files: [], hasChanges: false });
}
} catch (error) {
- logError(error, "Get diffs failed");
+ logError(error, 'Get diffs failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/git/routes/file-diff.ts b/apps/server/src/routes/git/routes/file-diff.ts
index fdf66998..6203ecc4 100644
--- a/apps/server/src/routes/git/routes/file-diff.ts
+++ b/apps/server/src/routes/git/routes/file-diff.ts
@@ -2,11 +2,11 @@
* POST /file-diff endpoint - Get diff for a specific file
*/
-import type { Request, Response } from "express";
-import { exec } from "child_process";
-import { promisify } from "util";
-import { getErrorMessage, logError } from "../common.js";
-import { generateSyntheticDiffForNewFile } from "../../common.js";
+import type { Request, Response } from 'express';
+import { exec } from 'child_process';
+import { promisify } from 'util';
+import { getErrorMessage, logError } from '../common.js';
+import { generateSyntheticDiffForNewFile } from '../../common.js';
const execAsync = promisify(exec);
@@ -19,20 +19,17 @@ export function createFileDiffHandler() {
};
if (!projectPath || !filePath) {
- res
- .status(400)
- .json({ success: false, error: "projectPath and filePath required" });
+ res.status(400).json({ success: false, error: 'projectPath and filePath required' });
return;
}
try {
// First check if the file is untracked
- const { stdout: status } = await execAsync(
- `git status --porcelain -- "${filePath}"`,
- { cwd: projectPath }
- );
+ const { stdout: status } = await execAsync(`git status --porcelain -- "${filePath}"`, {
+ cwd: projectPath,
+ });
- const isUntracked = status.trim().startsWith("??");
+ const isUntracked = status.trim().startsWith('??');
let diff: string;
if (isUntracked) {
@@ -40,23 +37,20 @@ export function createFileDiffHandler() {
diff = await generateSyntheticDiffForNewFile(projectPath, filePath);
} else {
// Use regular git diff for tracked files
- const result = await execAsync(
- `git diff HEAD -- "${filePath}"`,
- {
- cwd: projectPath,
- maxBuffer: 10 * 1024 * 1024,
- }
- );
+ const result = await execAsync(`git diff HEAD -- "${filePath}"`, {
+ cwd: projectPath,
+ maxBuffer: 10 * 1024 * 1024,
+ });
diff = result.stdout;
}
res.json({ success: true, diff, filePath });
} catch (innerError) {
- logError(innerError, "Git file diff failed");
- res.json({ success: true, diff: "", filePath });
+ logError(innerError, 'Git file diff failed');
+ res.json({ success: true, diff: '', filePath });
}
} catch (error) {
- logError(error, "Get file diff failed");
+ logError(error, 'Get file diff failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/health/common.ts b/apps/server/src/routes/health/common.ts
index 4977f831..ac335c3d 100644
--- a/apps/server/src/routes/health/common.ts
+++ b/apps/server/src/routes/health/common.ts
@@ -2,13 +2,10 @@
* Common utilities for health routes
*/
-import { createLogger } from "@automaker/utils";
-import {
- getErrorMessage as getErrorMessageShared,
- createLogError,
-} from "../common.js";
+import { createLogger } from '@automaker/utils';
+import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
-const logger = createLogger("Health");
+const logger = createLogger('Health');
// Re-export shared utilities
export { getErrorMessageShared as getErrorMessage };
diff --git a/apps/server/src/routes/health/index.ts b/apps/server/src/routes/health/index.ts
index 6ec62532..31439e66 100644
--- a/apps/server/src/routes/health/index.ts
+++ b/apps/server/src/routes/health/index.ts
@@ -2,15 +2,15 @@
* Health check routes
*/
-import { Router } from "express";
-import { createIndexHandler } from "./routes/index.js";
-import { createDetailedHandler } from "./routes/detailed.js";
+import { Router } from 'express';
+import { createIndexHandler } from './routes/index.js';
+import { createDetailedHandler } from './routes/detailed.js';
export function createHealthRoutes(): Router {
const router = Router();
- router.get("/", createIndexHandler());
- router.get("/detailed", createDetailedHandler());
+ router.get('/', createIndexHandler());
+ router.get('/detailed', createDetailedHandler());
return router;
}
diff --git a/apps/server/src/routes/health/routes/detailed.ts b/apps/server/src/routes/health/routes/detailed.ts
index 22deba78..5aa2e6b1 100644
--- a/apps/server/src/routes/health/routes/detailed.ts
+++ b/apps/server/src/routes/health/routes/detailed.ts
@@ -2,18 +2,18 @@
* GET /detailed endpoint - Detailed health check
*/
-import type { Request, Response } from "express";
-import { getAuthStatus } from "../../../lib/auth.js";
+import type { Request, Response } from 'express';
+import { getAuthStatus } from '../../../lib/auth.js';
export function createDetailedHandler() {
return (_req: Request, res: Response): void => {
res.json({
- status: "ok",
+ status: 'ok',
timestamp: new Date().toISOString(),
- version: process.env.npm_package_version || "0.1.0",
+ version: process.env.npm_package_version || '0.1.0',
uptime: process.uptime(),
memory: process.memoryUsage(),
- dataDir: process.env.DATA_DIR || "./data",
+ dataDir: process.env.DATA_DIR || './data',
auth: getAuthStatus(),
env: {
nodeVersion: process.version,
diff --git a/apps/server/src/routes/health/routes/index.ts b/apps/server/src/routes/health/routes/index.ts
index e571b78e..1501f6a6 100644
--- a/apps/server/src/routes/health/routes/index.ts
+++ b/apps/server/src/routes/health/routes/index.ts
@@ -2,14 +2,14 @@
* GET / endpoint - Basic health check
*/
-import type { Request, Response } from "express";
+import type { Request, Response } from 'express';
export function createIndexHandler() {
return (_req: Request, res: Response): void => {
res.json({
- status: "ok",
+ status: 'ok',
timestamp: new Date().toISOString(),
- version: process.env.npm_package_version || "0.1.0",
+ version: process.env.npm_package_version || '0.1.0',
});
};
}
diff --git a/apps/server/src/routes/models/common.ts b/apps/server/src/routes/models/common.ts
index 8baace0a..7f30c028 100644
--- a/apps/server/src/routes/models/common.ts
+++ b/apps/server/src/routes/models/common.ts
@@ -2,13 +2,10 @@
* Common utilities for models routes
*/
-import { createLogger } from "@automaker/utils";
-import {
- getErrorMessage as getErrorMessageShared,
- createLogError,
-} from "../common.js";
+import { createLogger } from '@automaker/utils';
+import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
-const logger = createLogger("Models");
+const logger = createLogger('Models');
// Re-export shared utilities
export { getErrorMessageShared as getErrorMessage };
diff --git a/apps/server/src/routes/models/index.ts b/apps/server/src/routes/models/index.ts
index 4ed1fda2..14d0beab 100644
--- a/apps/server/src/routes/models/index.ts
+++ b/apps/server/src/routes/models/index.ts
@@ -2,15 +2,15 @@
* Models routes - HTTP API for model providers and availability
*/
-import { Router } from "express";
-import { createAvailableHandler } from "./routes/available.js";
-import { createProvidersHandler } from "./routes/providers.js";
+import { Router } from 'express';
+import { createAvailableHandler } from './routes/available.js';
+import { createProvidersHandler } from './routes/providers.js';
export function createModelsRoutes(): Router {
const router = Router();
- router.get("/available", createAvailableHandler());
- router.get("/providers", createProvidersHandler());
+ router.get('/available', createAvailableHandler());
+ router.get('/providers', createProvidersHandler());
return router;
}
diff --git a/apps/server/src/routes/models/routes/available.ts b/apps/server/src/routes/models/routes/available.ts
index 3e26b690..4ac4e0b1 100644
--- a/apps/server/src/routes/models/routes/available.ts
+++ b/apps/server/src/routes/models/routes/available.ts
@@ -2,8 +2,8 @@
* GET /available endpoint - Get available models
*/
-import type { Request, Response } from "express";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import { getErrorMessage, logError } from '../common.js';
interface ModelDefinition {
id: string;
@@ -20,36 +20,36 @@ export function createAvailableHandler() {
try {
const models: ModelDefinition[] = [
{
- id: "claude-opus-4-5-20251101",
- name: "Claude Opus 4.5",
- provider: "anthropic",
+ id: 'claude-opus-4-5-20251101',
+ name: 'Claude Opus 4.5',
+ provider: 'anthropic',
contextWindow: 200000,
maxOutputTokens: 16384,
supportsVision: true,
supportsTools: true,
},
{
- id: "claude-sonnet-4-20250514",
- name: "Claude Sonnet 4",
- provider: "anthropic",
+ id: 'claude-sonnet-4-20250514',
+ name: 'Claude Sonnet 4',
+ provider: 'anthropic',
contextWindow: 200000,
maxOutputTokens: 16384,
supportsVision: true,
supportsTools: true,
},
{
- id: "claude-3-5-sonnet-20241022",
- name: "Claude 3.5 Sonnet",
- provider: "anthropic",
+ id: 'claude-3-5-sonnet-20241022',
+ name: 'Claude 3.5 Sonnet',
+ provider: 'anthropic',
contextWindow: 200000,
maxOutputTokens: 8192,
supportsVision: true,
supportsTools: true,
},
{
- id: "claude-3-5-haiku-20241022",
- name: "Claude 3.5 Haiku",
- provider: "anthropic",
+ id: 'claude-3-5-haiku-20241022',
+ name: 'Claude 3.5 Haiku',
+ provider: 'anthropic',
contextWindow: 200000,
maxOutputTokens: 8192,
supportsVision: true,
@@ -59,7 +59,7 @@ export function createAvailableHandler() {
res.json({ success: true, models });
} catch (error) {
- logError(error, "Get available models failed");
+ logError(error, 'Get available models failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/models/routes/providers.ts b/apps/server/src/routes/models/routes/providers.ts
index 3f140f37..b7ef1b85 100644
--- a/apps/server/src/routes/models/routes/providers.ts
+++ b/apps/server/src/routes/models/routes/providers.ts
@@ -2,9 +2,9 @@
* GET /providers endpoint - Check provider status
*/
-import type { Request, Response } from "express";
-import { ProviderFactory } from "../../../providers/provider-factory.js";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import { ProviderFactory } from '../../../providers/provider-factory.js';
+import { getErrorMessage, logError } from '../common.js';
export function createProvidersHandler() {
return async (_req: Request, res: Response): Promise => {
@@ -21,7 +21,7 @@ export function createProvidersHandler() {
res.json({ success: true, providers });
} catch (error) {
- logError(error, "Get providers failed");
+ logError(error, 'Get providers failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/running-agents/common.ts b/apps/server/src/routes/running-agents/common.ts
index acb0d7e5..c2d16a18 100644
--- a/apps/server/src/routes/running-agents/common.ts
+++ b/apps/server/src/routes/running-agents/common.ts
@@ -2,13 +2,10 @@
* Common utilities for running-agents routes
*/
-import { createLogger } from "@automaker/utils";
-import {
- getErrorMessage as getErrorMessageShared,
- createLogError,
-} from "../common.js";
+import { createLogger } from '@automaker/utils';
+import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
-const logger = createLogger("RunningAgents");
+const logger = createLogger('RunningAgents');
// Re-export shared utilities
export { getErrorMessageShared as getErrorMessage };
diff --git a/apps/server/src/routes/running-agents/index.ts b/apps/server/src/routes/running-agents/index.ts
index cef82fea..a1dbffcd 100644
--- a/apps/server/src/routes/running-agents/index.ts
+++ b/apps/server/src/routes/running-agents/index.ts
@@ -2,16 +2,14 @@
* Running Agents routes - HTTP API for tracking active agent executions
*/
-import { Router } from "express";
-import type { AutoModeService } from "../../services/auto-mode-service.js";
-import { createIndexHandler } from "./routes/index.js";
+import { Router } from 'express';
+import type { AutoModeService } from '../../services/auto-mode-service.js';
+import { createIndexHandler } from './routes/index.js';
-export function createRunningAgentsRoutes(
- autoModeService: AutoModeService
-): Router {
+export function createRunningAgentsRoutes(autoModeService: AutoModeService): Router {
const router = Router();
- router.get("/", createIndexHandler(autoModeService));
+ router.get('/', createIndexHandler(autoModeService));
return router;
}
diff --git a/apps/server/src/routes/running-agents/routes/index.ts b/apps/server/src/routes/running-agents/routes/index.ts
index e2f7e14e..72a3f838 100644
--- a/apps/server/src/routes/running-agents/routes/index.ts
+++ b/apps/server/src/routes/running-agents/routes/index.ts
@@ -2,9 +2,9 @@
* GET / endpoint - Get all running agents
*/
-import type { Request, Response } from "express";
-import type { AutoModeService } from "../../../services/auto-mode-service.js";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import type { AutoModeService } from '../../../services/auto-mode-service.js';
+import { getErrorMessage, logError } from '../common.js';
export function createIndexHandler(autoModeService: AutoModeService) {
return async (_req: Request, res: Response): Promise => {
@@ -18,7 +18,7 @@ export function createIndexHandler(autoModeService: AutoModeService) {
totalCount: runningAgents.length,
});
} catch (error) {
- logError(error, "Get running agents failed");
+ logError(error, 'Get running agents failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/sessions/common.ts b/apps/server/src/routes/sessions/common.ts
index facae648..0d1df9b6 100644
--- a/apps/server/src/routes/sessions/common.ts
+++ b/apps/server/src/routes/sessions/common.ts
@@ -2,13 +2,10 @@
* Common utilities for sessions routes
*/
-import { createLogger } from "@automaker/utils";
-import {
- getErrorMessage as getErrorMessageShared,
- createLogError,
-} from "../common.js";
+import { createLogger } from '@automaker/utils';
+import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
-const logger = createLogger("Sessions");
+const logger = createLogger('Sessions');
// Re-export shared utilities
export { getErrorMessageShared as getErrorMessage };
diff --git a/apps/server/src/routes/sessions/index.ts b/apps/server/src/routes/sessions/index.ts
index 1cae202d..e625671f 100644
--- a/apps/server/src/routes/sessions/index.ts
+++ b/apps/server/src/routes/sessions/index.ts
@@ -2,24 +2,24 @@
* Sessions routes - HTTP API for session management
*/
-import { Router } from "express";
-import { AgentService } from "../../services/agent-service.js";
-import { createIndexHandler } from "./routes/index.js";
-import { createCreateHandler } from "./routes/create.js";
-import { createUpdateHandler } from "./routes/update.js";
-import { createArchiveHandler } from "./routes/archive.js";
-import { createUnarchiveHandler } from "./routes/unarchive.js";
-import { createDeleteHandler } from "./routes/delete.js";
+import { Router } from 'express';
+import { AgentService } from '../../services/agent-service.js';
+import { createIndexHandler } from './routes/index.js';
+import { createCreateHandler } from './routes/create.js';
+import { createUpdateHandler } from './routes/update.js';
+import { createArchiveHandler } from './routes/archive.js';
+import { createUnarchiveHandler } from './routes/unarchive.js';
+import { createDeleteHandler } from './routes/delete.js';
export function createSessionsRoutes(agentService: AgentService): Router {
const router = Router();
- router.get("/", createIndexHandler(agentService));
- router.post("/", createCreateHandler(agentService));
- router.put("/:sessionId", createUpdateHandler(agentService));
- router.post("/:sessionId/archive", createArchiveHandler(agentService));
- router.post("/:sessionId/unarchive", createUnarchiveHandler(agentService));
- router.delete("/:sessionId", createDeleteHandler(agentService));
+ router.get('/', createIndexHandler(agentService));
+ router.post('/', createCreateHandler(agentService));
+ router.put('/:sessionId', createUpdateHandler(agentService));
+ router.post('/:sessionId/archive', createArchiveHandler(agentService));
+ router.post('/:sessionId/unarchive', createUnarchiveHandler(agentService));
+ router.delete('/:sessionId', createDeleteHandler(agentService));
return router;
}
diff --git a/apps/server/src/routes/sessions/routes/archive.ts b/apps/server/src/routes/sessions/routes/archive.ts
index dd9b6aa0..3407e5cd 100644
--- a/apps/server/src/routes/sessions/routes/archive.ts
+++ b/apps/server/src/routes/sessions/routes/archive.ts
@@ -2,9 +2,9 @@
* POST /:sessionId/archive endpoint - Archive a session
*/
-import type { Request, Response } from "express";
-import { AgentService } from "../../../services/agent-service.js";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import { AgentService } from '../../../services/agent-service.js';
+import { getErrorMessage, logError } from '../common.js';
export function createArchiveHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise => {
@@ -13,13 +13,13 @@ export function createArchiveHandler(agentService: AgentService) {
const success = await agentService.archiveSession(sessionId);
if (!success) {
- res.status(404).json({ success: false, error: "Session not found" });
+ res.status(404).json({ success: false, error: 'Session not found' });
return;
}
res.json({ success: true });
} catch (error) {
- logError(error, "Archive session failed");
+ logError(error, 'Archive session failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/sessions/routes/create.ts b/apps/server/src/routes/sessions/routes/create.ts
index 7faf9e36..2917168c 100644
--- a/apps/server/src/routes/sessions/routes/create.ts
+++ b/apps/server/src/routes/sessions/routes/create.ts
@@ -2,9 +2,9 @@
* POST / endpoint - Create a new session
*/
-import type { Request, Response } from "express";
-import { AgentService } from "../../../services/agent-service.js";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import { AgentService } from '../../../services/agent-service.js';
+import { getErrorMessage, logError } from '../common.js';
export function createCreateHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise => {
@@ -17,19 +17,14 @@ export function createCreateHandler(agentService: AgentService) {
};
if (!name) {
- res.status(400).json({ success: false, error: "name is required" });
+ res.status(400).json({ success: false, error: 'name is required' });
return;
}
- const session = await agentService.createSession(
- name,
- projectPath,
- workingDirectory,
- model
- );
+ const session = await agentService.createSession(name, projectPath, workingDirectory, model);
res.json({ success: true, session });
} catch (error) {
- logError(error, "Create session failed");
+ logError(error, 'Create session failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/sessions/routes/delete.ts b/apps/server/src/routes/sessions/routes/delete.ts
index 2d4c9f4c..91bbc39d 100644
--- a/apps/server/src/routes/sessions/routes/delete.ts
+++ b/apps/server/src/routes/sessions/routes/delete.ts
@@ -2,9 +2,9 @@
* DELETE /:sessionId endpoint - Delete a session
*/
-import type { Request, Response } from "express";
-import { AgentService } from "../../../services/agent-service.js";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import { AgentService } from '../../../services/agent-service.js';
+import { getErrorMessage, logError } from '../common.js';
export function createDeleteHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise => {
@@ -13,13 +13,13 @@ export function createDeleteHandler(agentService: AgentService) {
const success = await agentService.deleteSession(sessionId);
if (!success) {
- res.status(404).json({ success: false, error: "Session not found" });
+ res.status(404).json({ success: false, error: 'Session not found' });
return;
}
res.json({ success: true });
} catch (error) {
- logError(error, "Delete session failed");
+ logError(error, 'Delete session failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/sessions/routes/index.ts b/apps/server/src/routes/sessions/routes/index.ts
index 64b891db..5f82bcab 100644
--- a/apps/server/src/routes/sessions/routes/index.ts
+++ b/apps/server/src/routes/sessions/routes/index.ts
@@ -2,14 +2,14 @@
* GET / endpoint - List all sessions
*/
-import type { Request, Response } from "express";
-import { AgentService } from "../../../services/agent-service.js";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import { AgentService } from '../../../services/agent-service.js';
+import { getErrorMessage, logError } from '../common.js';
export function createIndexHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise => {
try {
- const includeArchived = req.query.includeArchived === "true";
+ const includeArchived = req.query.includeArchived === 'true';
const sessionsRaw = await agentService.listSessions(includeArchived);
// Transform to match frontend SessionListItem interface
@@ -17,7 +17,7 @@ export function createIndexHandler(agentService: AgentService) {
sessionsRaw.map(async (s) => {
const messages = await agentService.loadSession(s.id);
const lastMessage = messages[messages.length - 1];
- const preview = lastMessage?.content?.slice(0, 100) || "";
+ const preview = lastMessage?.content?.slice(0, 100) || '';
return {
id: s.id,
@@ -36,7 +36,7 @@ export function createIndexHandler(agentService: AgentService) {
res.json({ success: true, sessions });
} catch (error) {
- logError(error, "List sessions failed");
+ logError(error, 'List sessions failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/sessions/routes/unarchive.ts b/apps/server/src/routes/sessions/routes/unarchive.ts
index 07e4be17..638d3150 100644
--- a/apps/server/src/routes/sessions/routes/unarchive.ts
+++ b/apps/server/src/routes/sessions/routes/unarchive.ts
@@ -2,9 +2,9 @@
* POST /:sessionId/unarchive endpoint - Unarchive a session
*/
-import type { Request, Response } from "express";
-import { AgentService } from "../../../services/agent-service.js";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import { AgentService } from '../../../services/agent-service.js';
+import { getErrorMessage, logError } from '../common.js';
export function createUnarchiveHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise => {
@@ -13,13 +13,13 @@ export function createUnarchiveHandler(agentService: AgentService) {
const success = await agentService.unarchiveSession(sessionId);
if (!success) {
- res.status(404).json({ success: false, error: "Session not found" });
+ res.status(404).json({ success: false, error: 'Session not found' });
return;
}
res.json({ success: true });
} catch (error) {
- logError(error, "Unarchive session failed");
+ logError(error, 'Unarchive session failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/sessions/routes/update.ts b/apps/server/src/routes/sessions/routes/update.ts
index 2dbea431..7705fa22 100644
--- a/apps/server/src/routes/sessions/routes/update.ts
+++ b/apps/server/src/routes/sessions/routes/update.ts
@@ -2,9 +2,9 @@
* PUT /:sessionId endpoint - Update a session
*/
-import type { Request, Response } from "express";
-import { AgentService } from "../../../services/agent-service.js";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import { AgentService } from '../../../services/agent-service.js';
+import { getErrorMessage, logError } from '../common.js';
export function createUpdateHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise => {
@@ -22,13 +22,13 @@ export function createUpdateHandler(agentService: AgentService) {
model,
});
if (!session) {
- res.status(404).json({ success: false, error: "Session not found" });
+ res.status(404).json({ success: false, error: 'Session not found' });
return;
}
res.json({ success: true, session });
} catch (error) {
- logError(error, "Update session failed");
+ logError(error, 'Update session failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/settings/common.ts b/apps/server/src/routes/settings/common.ts
index 74057a4e..d8201bfd 100644
--- a/apps/server/src/routes/settings/common.ts
+++ b/apps/server/src/routes/settings/common.ts
@@ -5,14 +5,11 @@
* Re-exports error handling helpers from the parent routes module.
*/
-import { createLogger } from "@automaker/utils";
-import {
- getErrorMessage as getErrorMessageShared,
- createLogError,
-} from "../common.js";
+import { createLogger } from '@automaker/utils';
+import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
/** Logger instance for settings-related operations */
-export const logger = createLogger("Settings");
+export const logger = createLogger('Settings');
/**
* Extract user-friendly error message from error objects
diff --git a/apps/server/src/routes/settings/index.ts b/apps/server/src/routes/settings/index.ts
index 53c4556e..cc164856 100644
--- a/apps/server/src/routes/settings/index.ts
+++ b/apps/server/src/routes/settings/index.ts
@@ -12,17 +12,17 @@
* Mounted at /api/settings in the main server.
*/
-import { Router } from "express";
-import type { SettingsService } from "../../services/settings-service.js";
-import { validatePathParams } from "../../middleware/validate-paths.js";
-import { createGetGlobalHandler } from "./routes/get-global.js";
-import { createUpdateGlobalHandler } from "./routes/update-global.js";
-import { createGetCredentialsHandler } from "./routes/get-credentials.js";
-import { createUpdateCredentialsHandler } from "./routes/update-credentials.js";
-import { createGetProjectHandler } from "./routes/get-project.js";
-import { createUpdateProjectHandler } from "./routes/update-project.js";
-import { createMigrateHandler } from "./routes/migrate.js";
-import { createStatusHandler } from "./routes/status.js";
+import { Router } from 'express';
+import type { SettingsService } from '../../services/settings-service.js';
+import { validatePathParams } from '../../middleware/validate-paths.js';
+import { createGetGlobalHandler } from './routes/get-global.js';
+import { createUpdateGlobalHandler } from './routes/update-global.js';
+import { createGetCredentialsHandler } from './routes/get-credentials.js';
+import { createUpdateCredentialsHandler } from './routes/update-credentials.js';
+import { createGetProjectHandler } from './routes/get-project.js';
+import { createUpdateProjectHandler } from './routes/update-project.js';
+import { createMigrateHandler } from './routes/migrate.js';
+import { createStatusHandler } from './routes/status.js';
/**
* Create settings router with all endpoints
@@ -47,22 +47,30 @@ export function createSettingsRoutes(settingsService: SettingsService): Router {
const router = Router();
// Status endpoint (check if migration needed)
- router.get("/status", createStatusHandler(settingsService));
+ router.get('/status', createStatusHandler(settingsService));
// Global settings
- router.get("/global", createGetGlobalHandler(settingsService));
- router.put("/global", createUpdateGlobalHandler(settingsService));
+ router.get('/global', createGetGlobalHandler(settingsService));
+ router.put('/global', createUpdateGlobalHandler(settingsService));
// Credentials (separate for security)
- router.get("/credentials", createGetCredentialsHandler(settingsService));
- router.put("/credentials", createUpdateCredentialsHandler(settingsService));
+ router.get('/credentials', createGetCredentialsHandler(settingsService));
+ router.put('/credentials', createUpdateCredentialsHandler(settingsService));
// Project settings
- router.post("/project", validatePathParams("projectPath"), createGetProjectHandler(settingsService));
- router.put("/project", validatePathParams("projectPath"), createUpdateProjectHandler(settingsService));
+ router.post(
+ '/project',
+ validatePathParams('projectPath'),
+ createGetProjectHandler(settingsService)
+ );
+ router.put(
+ '/project',
+ validatePathParams('projectPath'),
+ createUpdateProjectHandler(settingsService)
+ );
// Migration from localStorage
- router.post("/migrate", createMigrateHandler(settingsService));
+ router.post('/migrate', createMigrateHandler(settingsService));
return router;
}
diff --git a/apps/server/src/routes/settings/routes/get-credentials.ts b/apps/server/src/routes/settings/routes/get-credentials.ts
index 2294e9c4..be15b04b 100644
--- a/apps/server/src/routes/settings/routes/get-credentials.ts
+++ b/apps/server/src/routes/settings/routes/get-credentials.ts
@@ -8,9 +8,9 @@
* Response: `{ "success": true, "credentials": { anthropic } }`
*/
-import type { Request, Response } from "express";
-import type { SettingsService } from "../../../services/settings-service.js";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import type { SettingsService } from '../../../services/settings-service.js';
+import { getErrorMessage, logError } from '../common.js';
/**
* Create handler factory for GET /api/settings/credentials
@@ -28,7 +28,7 @@ export function createGetCredentialsHandler(settingsService: SettingsService) {
credentials,
});
} catch (error) {
- logError(error, "Get credentials failed");
+ logError(error, 'Get credentials failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/settings/routes/get-global.ts b/apps/server/src/routes/settings/routes/get-global.ts
index 0e71c4eb..fa432b25 100644
--- a/apps/server/src/routes/settings/routes/get-global.ts
+++ b/apps/server/src/routes/settings/routes/get-global.ts
@@ -7,9 +7,9 @@
* Response: `{ "success": true, "settings": GlobalSettings }`
*/
-import type { Request, Response } from "express";
-import type { SettingsService } from "../../../services/settings-service.js";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import type { SettingsService } from '../../../services/settings-service.js';
+import { getErrorMessage, logError } from '../common.js';
/**
* Create handler factory for GET /api/settings/global
@@ -27,7 +27,7 @@ export function createGetGlobalHandler(settingsService: SettingsService) {
settings,
});
} catch (error) {
- logError(error, "Get global settings failed");
+ logError(error, 'Get global settings failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/settings/routes/get-project.ts b/apps/server/src/routes/settings/routes/get-project.ts
index 58f6ce7e..7cd449a2 100644
--- a/apps/server/src/routes/settings/routes/get-project.ts
+++ b/apps/server/src/routes/settings/routes/get-project.ts
@@ -8,9 +8,9 @@
* Response: `{ "success": true, "settings": ProjectSettings }`
*/
-import type { Request, Response } from "express";
-import type { SettingsService } from "../../../services/settings-service.js";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import type { SettingsService } from '../../../services/settings-service.js';
+import { getErrorMessage, logError } from '../common.js';
/**
* Create handler factory for POST /api/settings/project
@@ -23,10 +23,10 @@ export function createGetProjectHandler(settingsService: SettingsService) {
try {
const { projectPath } = req.body as { projectPath?: string };
- if (!projectPath || typeof projectPath !== "string") {
+ if (!projectPath || typeof projectPath !== 'string') {
res.status(400).json({
success: false,
- error: "projectPath is required",
+ error: 'projectPath is required',
});
return;
}
@@ -38,7 +38,7 @@ export function createGetProjectHandler(settingsService: SettingsService) {
settings,
});
} catch (error) {
- logError(error, "Get project settings failed");
+ logError(error, 'Get project settings failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/settings/routes/migrate.ts b/apps/server/src/routes/settings/routes/migrate.ts
index e95b11c0..02145d66 100644
--- a/apps/server/src/routes/settings/routes/migrate.ts
+++ b/apps/server/src/routes/settings/routes/migrate.ts
@@ -30,9 +30,9 @@
* ```
*/
-import type { Request, Response } from "express";
-import type { SettingsService } from "../../../services/settings-service.js";
-import { getErrorMessage, logError, logger } from "../common.js";
+import type { Request, Response } from 'express';
+import type { SettingsService } from '../../../services/settings-service.js';
+import { getErrorMessage, logError, logger } from '../common.js';
/**
* Create handler factory for POST /api/settings/migrate
@@ -45,32 +45,30 @@ export function createMigrateHandler(settingsService: SettingsService) {
try {
const { data } = req.body as {
data?: {
- "automaker-storage"?: string;
- "automaker-setup"?: string;
- "worktree-panel-collapsed"?: string;
- "file-browser-recent-folders"?: string;
- "automaker:lastProjectDir"?: string;
+ 'automaker-storage'?: string;
+ 'automaker-setup'?: string;
+ 'worktree-panel-collapsed'?: string;
+ 'file-browser-recent-folders'?: string;
+ 'automaker:lastProjectDir'?: string;
};
};
- if (!data || typeof data !== "object") {
+ if (!data || typeof data !== 'object') {
res.status(400).json({
success: false,
- error: "data object is required containing localStorage data",
+ error: 'data object is required containing localStorage data',
});
return;
}
- logger.info("Starting settings migration from localStorage");
+ logger.info('Starting settings migration from localStorage');
const result = await settingsService.migrateFromLocalStorage(data);
if (result.success) {
- logger.info(
- `Migration successful: ${result.migratedProjectCount} projects migrated`
- );
+ logger.info(`Migration successful: ${result.migratedProjectCount} projects migrated`);
} else {
- logger.warn(`Migration completed with errors: ${result.errors.join(", ")}`);
+ logger.warn(`Migration completed with errors: ${result.errors.join(', ')}`);
}
res.json({
@@ -81,7 +79,7 @@ export function createMigrateHandler(settingsService: SettingsService) {
errors: result.errors,
});
} catch (error) {
- logError(error, "Migration failed");
+ logError(error, 'Migration failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/settings/routes/status.ts b/apps/server/src/routes/settings/routes/status.ts
index 0354502f..04f01643 100644
--- a/apps/server/src/routes/settings/routes/status.ts
+++ b/apps/server/src/routes/settings/routes/status.ts
@@ -16,9 +16,9 @@
* ```
*/
-import type { Request, Response } from "express";
-import type { SettingsService } from "../../../services/settings-service.js";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import type { SettingsService } from '../../../services/settings-service.js';
+import { getErrorMessage, logError } from '../common.js';
/**
* Create handler factory for GET /api/settings/status
@@ -40,7 +40,7 @@ export function createStatusHandler(settingsService: SettingsService) {
needsMigration: !hasGlobalSettings,
});
} catch (error) {
- logError(error, "Get settings status failed");
+ logError(error, 'Get settings status failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/settings/routes/update-credentials.ts b/apps/server/src/routes/settings/routes/update-credentials.ts
index 16367879..c08b2445 100644
--- a/apps/server/src/routes/settings/routes/update-credentials.ts
+++ b/apps/server/src/routes/settings/routes/update-credentials.ts
@@ -8,10 +8,10 @@
* Response: `{ "success": true, "credentials": { anthropic } }`
*/
-import type { Request, Response } from "express";
-import type { SettingsService } from "../../../services/settings-service.js";
-import type { Credentials } from "../../../types/settings.js";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import type { SettingsService } from '../../../services/settings-service.js';
+import type { Credentials } from '../../../types/settings.js';
+import { getErrorMessage, logError } from '../common.js';
/**
* Create handler factory for PUT /api/settings/credentials
@@ -19,17 +19,15 @@ import { getErrorMessage, logError } from "../common.js";
* @param settingsService - Instance of SettingsService for file I/O
* @returns Express request handler
*/
-export function createUpdateCredentialsHandler(
- settingsService: SettingsService
-) {
+export function createUpdateCredentialsHandler(settingsService: SettingsService) {
return async (req: Request, res: Response): Promise => {
try {
const updates = req.body as Partial;
- if (!updates || typeof updates !== "object") {
+ if (!updates || typeof updates !== 'object') {
res.status(400).json({
success: false,
- error: "Invalid request body - expected credentials object",
+ error: 'Invalid request body - expected credentials object',
});
return;
}
@@ -44,7 +42,7 @@ export function createUpdateCredentialsHandler(
credentials: masked,
});
} catch (error) {
- logError(error, "Update credentials failed");
+ logError(error, 'Update credentials failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/settings/routes/update-global.ts b/apps/server/src/routes/settings/routes/update-global.ts
index 21af8dd2..6072f237 100644
--- a/apps/server/src/routes/settings/routes/update-global.ts
+++ b/apps/server/src/routes/settings/routes/update-global.ts
@@ -8,10 +8,10 @@
* Response: `{ "success": true, "settings": GlobalSettings }`
*/
-import type { Request, Response } from "express";
-import type { SettingsService } from "../../../services/settings-service.js";
-import type { GlobalSettings } from "../../../types/settings.js";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import type { SettingsService } from '../../../services/settings-service.js';
+import type { GlobalSettings } from '../../../types/settings.js';
+import { getErrorMessage, logError } from '../common.js';
/**
* Create handler factory for PUT /api/settings/global
@@ -24,10 +24,10 @@ export function createUpdateGlobalHandler(settingsService: SettingsService) {
try {
const updates = req.body as Partial;
- if (!updates || typeof updates !== "object") {
+ if (!updates || typeof updates !== 'object') {
res.status(400).json({
success: false,
- error: "Invalid request body - expected settings object",
+ error: 'Invalid request body - expected settings object',
});
return;
}
@@ -39,7 +39,7 @@ export function createUpdateGlobalHandler(settingsService: SettingsService) {
settings,
});
} catch (error) {
- logError(error, "Update global settings failed");
+ logError(error, 'Update global settings failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/settings/routes/update-project.ts b/apps/server/src/routes/settings/routes/update-project.ts
index 5dc38df0..f5f639f4 100644
--- a/apps/server/src/routes/settings/routes/update-project.ts
+++ b/apps/server/src/routes/settings/routes/update-project.ts
@@ -8,10 +8,10 @@
* Response: `{ "success": true, "settings": ProjectSettings }`
*/
-import type { Request, Response } from "express";
-import type { SettingsService } from "../../../services/settings-service.js";
-import type { ProjectSettings } from "../../../types/settings.js";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import type { SettingsService } from '../../../services/settings-service.js';
+import type { ProjectSettings } from '../../../types/settings.js';
+import { getErrorMessage, logError } from '../common.js';
/**
* Create handler factory for PUT /api/settings/project
@@ -27,33 +27,30 @@ export function createUpdateProjectHandler(settingsService: SettingsService) {
updates?: Partial;
};
- if (!projectPath || typeof projectPath !== "string") {
+ if (!projectPath || typeof projectPath !== 'string') {
res.status(400).json({
success: false,
- error: "projectPath is required",
+ error: 'projectPath is required',
});
return;
}
- if (!updates || typeof updates !== "object") {
+ if (!updates || typeof updates !== 'object') {
res.status(400).json({
success: false,
- error: "updates object is required",
+ error: 'updates object is required',
});
return;
}
- const settings = await settingsService.updateProjectSettings(
- projectPath,
- updates
- );
+ const settings = await settingsService.updateProjectSettings(projectPath, updates);
res.json({
success: true,
settings,
});
} catch (error) {
- logError(error, "Update project settings failed");
+ logError(error, 'Update project settings failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/setup/common.ts b/apps/server/src/routes/setup/common.ts
index 036def1e..097d7a6c 100644
--- a/apps/server/src/routes/setup/common.ts
+++ b/apps/server/src/routes/setup/common.ts
@@ -2,15 +2,12 @@
* Common utilities and state for setup routes
*/
-import { createLogger } from "@automaker/utils";
-import path from "path";
-import fs from "fs/promises";
-import {
- getErrorMessage as getErrorMessageShared,
- createLogError,
-} from "../common.js";
+import { createLogger } from '@automaker/utils';
+import path from 'path';
+import fs from 'fs/promises';
+import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
-const logger = createLogger("Setup");
+const logger = createLogger('Setup');
// Storage for API keys (in-memory cache) - private
const apiKeys: Record = {};
@@ -39,22 +36,19 @@ export function getAllApiKeys(): Record {
/**
* Helper to persist API keys to .env file
*/
-export async function persistApiKeyToEnv(
- key: string,
- value: string
-): Promise {
- const envPath = path.join(process.cwd(), ".env");
+export async function persistApiKeyToEnv(key: string, value: string): Promise {
+ const envPath = path.join(process.cwd(), '.env');
try {
- let envContent = "";
+ let envContent = '';
try {
- envContent = await fs.readFile(envPath, "utf-8");
+ envContent = await fs.readFile(envPath, 'utf-8');
} catch {
// .env file doesn't exist, we'll create it
}
// Parse existing env content
- const lines = envContent.split("\n");
+ const lines = envContent.split('\n');
const keyRegex = new RegExp(`^${key}=`);
let found = false;
const newLines = lines.map((line) => {
@@ -70,7 +64,7 @@ export async function persistApiKeyToEnv(
newLines.push(`${key}=${value}`);
}
- await fs.writeFile(envPath, newLines.join("\n"));
+ await fs.writeFile(envPath, newLines.join('\n'));
logger.info(`[Setup] Persisted ${key} to .env file`);
} catch (error) {
logger.error(`[Setup] Failed to persist ${key} to .env:`, error);
diff --git a/apps/server/src/routes/setup/get-claude-status.ts b/apps/server/src/routes/setup/get-claude-status.ts
index 2ae072ff..922d363f 100644
--- a/apps/server/src/routes/setup/get-claude-status.ts
+++ b/apps/server/src/routes/setup/get-claude-status.ts
@@ -2,36 +2,36 @@
* Business logic for getting Claude CLI status
*/
-import { exec } from "child_process";
-import { promisify } from "util";
-import os from "os";
-import path from "path";
-import fs from "fs/promises";
-import { getApiKey } from "./common.js";
+import { exec } from 'child_process';
+import { promisify } from 'util';
+import os from 'os';
+import path from 'path';
+import fs from 'fs/promises';
+import { getApiKey } from './common.js';
const execAsync = promisify(exec);
export async function getClaudeStatus() {
let installed = false;
- let version = "";
- let cliPath = "";
- let method = "none";
+ let version = '';
+ let cliPath = '';
+ let method = 'none';
- const isWindows = process.platform === "win32";
+ const isWindows = process.platform === 'win32';
// Try to find Claude CLI using platform-specific command
try {
// Use 'where' on Windows, 'which' on Unix-like systems
- const findCommand = isWindows ? "where claude" : "which claude";
+ const findCommand = isWindows ? 'where claude' : 'which claude';
const { stdout } = await execAsync(findCommand);
// 'where' on Windows can return multiple paths - take the first one
cliPath = stdout.trim().split(/\r?\n/)[0];
installed = true;
- method = "path";
+ method = 'path';
// Get version
try {
- const { stdout: versionOut } = await execAsync("claude --version");
+ const { stdout: versionOut } = await execAsync('claude --version');
version = versionOut.trim();
} catch {
// Version command might not be available
@@ -40,22 +40,22 @@ export async function getClaudeStatus() {
// Not in PATH, try common locations based on platform
const commonPaths = isWindows
? (() => {
- const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
+ const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
return [
// Windows-specific paths
- path.join(os.homedir(), ".local", "bin", "claude.exe"),
- path.join(appData, "npm", "claude.cmd"),
- path.join(appData, "npm", "claude"),
- path.join(appData, ".npm-global", "bin", "claude.cmd"),
- path.join(appData, ".npm-global", "bin", "claude"),
+ path.join(os.homedir(), '.local', 'bin', 'claude.exe'),
+ path.join(appData, 'npm', 'claude.cmd'),
+ path.join(appData, 'npm', 'claude'),
+ path.join(appData, '.npm-global', 'bin', 'claude.cmd'),
+ path.join(appData, '.npm-global', 'bin', 'claude'),
];
})()
: [
// Unix (Linux/macOS) paths
- path.join(os.homedir(), ".local", "bin", "claude"),
- path.join(os.homedir(), ".claude", "local", "claude"),
- "/usr/local/bin/claude",
- path.join(os.homedir(), ".npm-global", "bin", "claude"),
+ path.join(os.homedir(), '.local', 'bin', 'claude'),
+ path.join(os.homedir(), '.claude', 'local', 'claude'),
+ '/usr/local/bin/claude',
+ path.join(os.homedir(), '.npm-global', 'bin', 'claude'),
];
for (const p of commonPaths) {
@@ -63,7 +63,7 @@ export async function getClaudeStatus() {
await fs.access(p);
cliPath = p;
installed = true;
- method = "local";
+ method = 'local';
// Get version from this path
try {
@@ -84,11 +84,11 @@ export async function getClaudeStatus() {
// apiKeys.anthropic stores direct API keys for pay-per-use
let auth = {
authenticated: false,
- method: "none" as string,
+ method: 'none' as string,
hasCredentialsFile: false,
hasToken: false,
- hasStoredOAuthToken: !!getApiKey("anthropic_oauth_token"),
- hasStoredApiKey: !!getApiKey("anthropic"),
+ hasStoredOAuthToken: !!getApiKey('anthropic_oauth_token'),
+ hasStoredApiKey: !!getApiKey('anthropic'),
hasEnvApiKey: !!process.env.ANTHROPIC_API_KEY,
// Additional fields for detailed status
oauthTokenValid: false,
@@ -97,13 +97,13 @@ export async function getClaudeStatus() {
hasRecentActivity: false,
};
- const claudeDir = path.join(os.homedir(), ".claude");
+ const claudeDir = path.join(os.homedir(), '.claude');
// Check for recent Claude CLI activity - indicates working authentication
// The stats-cache.json file is only populated when the CLI is working properly
- const statsCachePath = path.join(claudeDir, "stats-cache.json");
+ const statsCachePath = path.join(claudeDir, 'stats-cache.json');
try {
- const statsContent = await fs.readFile(statsCachePath, "utf-8");
+ const statsContent = await fs.readFile(statsCachePath, 'utf-8');
const stats = JSON.parse(statsContent);
// Check if there's any activity (which means the CLI is authenticated and working)
@@ -111,26 +111,26 @@ export async function getClaudeStatus() {
auth.hasRecentActivity = true;
auth.hasCliAuth = true;
auth.authenticated = true;
- auth.method = "cli_authenticated";
+ auth.method = 'cli_authenticated';
}
} catch {
// Stats file doesn't exist or is invalid
}
// Check for settings.json - indicates CLI has been set up
- const settingsPath = path.join(claudeDir, "settings.json");
+ const settingsPath = path.join(claudeDir, 'settings.json');
try {
await fs.access(settingsPath);
// If settings exist but no activity, CLI might be set up but not authenticated
if (!auth.hasCliAuth) {
// Try to check for other indicators of auth
- const sessionsDir = path.join(claudeDir, "projects");
+ const sessionsDir = path.join(claudeDir, 'projects');
try {
const sessions = await fs.readdir(sessionsDir);
if (sessions.length > 0) {
auth.hasCliAuth = true;
auth.authenticated = true;
- auth.method = "cli_authenticated";
+ auth.method = 'cli_authenticated';
}
} catch {
// Sessions directory doesn't exist
@@ -143,13 +143,13 @@ export async function getClaudeStatus() {
// Check for credentials file (OAuth tokens from claude login)
// Note: Claude CLI may use ".credentials.json" (hidden) or "credentials.json" depending on version/platform
const credentialsPaths = [
- path.join(claudeDir, ".credentials.json"),
- path.join(claudeDir, "credentials.json"),
+ path.join(claudeDir, '.credentials.json'),
+ path.join(claudeDir, 'credentials.json'),
];
for (const credentialsPath of credentialsPaths) {
try {
- const credentialsContent = await fs.readFile(credentialsPath, "utf-8");
+ const credentialsContent = await fs.readFile(credentialsPath, 'utf-8');
const credentials = JSON.parse(credentialsContent);
auth.hasCredentialsFile = true;
@@ -158,11 +158,11 @@ export async function getClaudeStatus() {
auth.hasStoredOAuthToken = true;
auth.oauthTokenValid = true;
auth.authenticated = true;
- auth.method = "oauth_token"; // Stored OAuth token from credentials file
+ auth.method = 'oauth_token'; // Stored OAuth token from credentials file
} else if (credentials.api_key) {
auth.apiKeyValid = true;
auth.authenticated = true;
- auth.method = "api_key"; // Stored API key in credentials file
+ auth.method = 'api_key'; // Stored API key in credentials file
}
break; // Found and processed credentials file
} catch {
@@ -174,25 +174,25 @@ export async function getClaudeStatus() {
if (auth.hasEnvApiKey) {
auth.authenticated = true;
auth.apiKeyValid = true;
- auth.method = "api_key_env"; // API key from ANTHROPIC_API_KEY env var
+ auth.method = 'api_key_env'; // API key from ANTHROPIC_API_KEY env var
}
// In-memory stored OAuth token (from setup wizard - subscription auth)
- if (!auth.authenticated && getApiKey("anthropic_oauth_token")) {
+ if (!auth.authenticated && getApiKey('anthropic_oauth_token')) {
auth.authenticated = true;
auth.oauthTokenValid = true;
- auth.method = "oauth_token"; // Stored OAuth token from setup wizard
+ auth.method = 'oauth_token'; // Stored OAuth token from setup wizard
}
// In-memory stored API key (from settings UI - pay-per-use)
- if (!auth.authenticated && getApiKey("anthropic")) {
+ if (!auth.authenticated && getApiKey('anthropic')) {
auth.authenticated = true;
auth.apiKeyValid = true;
- auth.method = "api_key"; // Manually stored API key
+ auth.method = 'api_key'; // Manually stored API key
}
return {
- status: installed ? "installed" : "not_installed",
+ status: installed ? 'installed' : 'not_installed',
installed,
method,
version,
diff --git a/apps/server/src/routes/setup/index.ts b/apps/server/src/routes/setup/index.ts
index 2b5db942..3681b2fc 100644
--- a/apps/server/src/routes/setup/index.ts
+++ b/apps/server/src/routes/setup/index.ts
@@ -2,29 +2,29 @@
* Setup routes - HTTP API for CLI detection, API keys, and platform info
*/
-import { Router } from "express";
-import { createClaudeStatusHandler } from "./routes/claude-status.js";
-import { createInstallClaudeHandler } from "./routes/install-claude.js";
-import { createAuthClaudeHandler } from "./routes/auth-claude.js";
-import { createStoreApiKeyHandler } from "./routes/store-api-key.js";
-import { createDeleteApiKeyHandler } from "./routes/delete-api-key.js";
-import { createApiKeysHandler } from "./routes/api-keys.js";
-import { createPlatformHandler } from "./routes/platform.js";
-import { createVerifyClaudeAuthHandler } from "./routes/verify-claude-auth.js";
-import { createGhStatusHandler } from "./routes/gh-status.js";
+import { Router } from 'express';
+import { createClaudeStatusHandler } from './routes/claude-status.js';
+import { createInstallClaudeHandler } from './routes/install-claude.js';
+import { createAuthClaudeHandler } from './routes/auth-claude.js';
+import { createStoreApiKeyHandler } from './routes/store-api-key.js';
+import { createDeleteApiKeyHandler } from './routes/delete-api-key.js';
+import { createApiKeysHandler } from './routes/api-keys.js';
+import { createPlatformHandler } from './routes/platform.js';
+import { createVerifyClaudeAuthHandler } from './routes/verify-claude-auth.js';
+import { createGhStatusHandler } from './routes/gh-status.js';
export function createSetupRoutes(): Router {
const router = Router();
- router.get("/claude-status", createClaudeStatusHandler());
- router.post("/install-claude", createInstallClaudeHandler());
- router.post("/auth-claude", createAuthClaudeHandler());
- router.post("/store-api-key", createStoreApiKeyHandler());
- router.post("/delete-api-key", createDeleteApiKeyHandler());
- router.get("/api-keys", createApiKeysHandler());
- router.get("/platform", createPlatformHandler());
- router.post("/verify-claude-auth", createVerifyClaudeAuthHandler());
- router.get("/gh-status", createGhStatusHandler());
+ router.get('/claude-status', createClaudeStatusHandler());
+ router.post('/install-claude', createInstallClaudeHandler());
+ router.post('/auth-claude', createAuthClaudeHandler());
+ router.post('/store-api-key', createStoreApiKeyHandler());
+ router.post('/delete-api-key', createDeleteApiKeyHandler());
+ router.get('/api-keys', createApiKeysHandler());
+ router.get('/platform', createPlatformHandler());
+ router.post('/verify-claude-auth', createVerifyClaudeAuthHandler());
+ router.get('/gh-status', createGhStatusHandler());
return router;
}
diff --git a/apps/server/src/routes/setup/routes/api-keys.ts b/apps/server/src/routes/setup/routes/api-keys.ts
index 201e4eba..d052c187 100644
--- a/apps/server/src/routes/setup/routes/api-keys.ts
+++ b/apps/server/src/routes/setup/routes/api-keys.ts
@@ -2,19 +2,18 @@
* GET /api-keys endpoint - Get API keys status
*/
-import type { Request, Response } from "express";
-import { getApiKey, getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import { getApiKey, getErrorMessage, logError } from '../common.js';
export function createApiKeysHandler() {
return async (_req: Request, res: Response): Promise => {
try {
res.json({
success: true,
- hasAnthropicKey:
- !!getApiKey("anthropic") || !!process.env.ANTHROPIC_API_KEY,
+ hasAnthropicKey: !!getApiKey('anthropic') || !!process.env.ANTHROPIC_API_KEY,
});
} catch (error) {
- logError(error, "Get API keys failed");
+ logError(error, 'Get API keys failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/setup/routes/auth-claude.ts b/apps/server/src/routes/setup/routes/auth-claude.ts
index 2ab8401d..4531501d 100644
--- a/apps/server/src/routes/setup/routes/auth-claude.ts
+++ b/apps/server/src/routes/setup/routes/auth-claude.ts
@@ -2,8 +2,8 @@
* POST /auth-claude endpoint - Auth Claude
*/
-import type { Request, Response } from "express";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import { getErrorMessage, logError } from '../common.js';
export function createAuthClaudeHandler() {
return async (_req: Request, res: Response): Promise => {
@@ -11,11 +11,11 @@ export function createAuthClaudeHandler() {
res.json({
success: true,
requiresManualAuth: true,
- command: "claude login",
+ command: 'claude login',
message: "Please run 'claude login' in your terminal to authenticate",
});
} catch (error) {
- logError(error, "Auth Claude failed");
+ logError(error, 'Auth Claude failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/setup/routes/claude-status.ts b/apps/server/src/routes/setup/routes/claude-status.ts
index 232a47bd..f2ae4a59 100644
--- a/apps/server/src/routes/setup/routes/claude-status.ts
+++ b/apps/server/src/routes/setup/routes/claude-status.ts
@@ -2,9 +2,9 @@
* GET /claude-status endpoint - Get Claude CLI status
*/
-import type { Request, Response } from "express";
-import { getClaudeStatus } from "../get-claude-status.js";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import { getClaudeStatus } from '../get-claude-status.js';
+import { getErrorMessage, logError } from '../common.js';
export function createClaudeStatusHandler() {
return async (_req: Request, res: Response): Promise => {
@@ -15,7 +15,7 @@ export function createClaudeStatusHandler() {
...status,
});
} catch (error) {
- logError(error, "Get Claude status failed");
+ logError(error, 'Get Claude status failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/setup/routes/delete-api-key.ts b/apps/server/src/routes/setup/routes/delete-api-key.ts
index 4bb3d4e5..e64ff6b7 100644
--- a/apps/server/src/routes/setup/routes/delete-api-key.ts
+++ b/apps/server/src/routes/setup/routes/delete-api-key.ts
@@ -2,43 +2,43 @@
* POST /delete-api-key endpoint - Delete a stored API key
*/
-import type { Request, Response } from "express";
-import { createLogger } from "@automaker/utils";
-import path from "path";
-import fs from "fs/promises";
+import type { Request, Response } from 'express';
+import { createLogger } from '@automaker/utils';
+import path from 'path';
+import fs from 'fs/promises';
-const logger = createLogger("Setup");
+const logger = createLogger('Setup');
// In-memory storage reference (imported from common.ts pattern)
// We need to modify common.ts to export a deleteApiKey function
-import { setApiKey } from "../common.js";
+import { setApiKey } from '../common.js';
/**
* Remove an API key from the .env file
*/
async function removeApiKeyFromEnv(key: string): Promise {
- const envPath = path.join(process.cwd(), ".env");
+ const envPath = path.join(process.cwd(), '.env');
try {
- let envContent = "";
+ let envContent = '';
try {
- envContent = await fs.readFile(envPath, "utf-8");
+ envContent = await fs.readFile(envPath, 'utf-8');
} catch {
// .env file doesn't exist, nothing to delete
return;
}
// Parse existing env content and remove the key
- const lines = envContent.split("\n");
+ const lines = envContent.split('\n');
const keyRegex = new RegExp(`^${key}=`);
const newLines = lines.filter((line) => !keyRegex.test(line));
// Remove empty lines at the end
- while (newLines.length > 0 && newLines[newLines.length - 1].trim() === "") {
+ while (newLines.length > 0 && newLines[newLines.length - 1].trim() === '') {
newLines.pop();
}
- await fs.writeFile(envPath, newLines.join("\n") + (newLines.length > 0 ? "\n" : ""));
+ await fs.writeFile(envPath, newLines.join('\n') + (newLines.length > 0 ? '\n' : ''));
logger.info(`[Setup] Removed ${key} from .env file`);
} catch (error) {
logger.error(`[Setup] Failed to remove ${key} from .env:`, error);
@@ -54,7 +54,7 @@ export function createDeleteApiKeyHandler() {
if (!provider) {
res.status(400).json({
success: false,
- error: "Provider is required",
+ error: 'Provider is required',
});
return;
}
@@ -63,7 +63,7 @@ export function createDeleteApiKeyHandler() {
// Map provider to env key name
const envKeyMap: Record = {
- anthropic: "ANTHROPIC_API_KEY",
+ anthropic: 'ANTHROPIC_API_KEY',
};
const envKey = envKeyMap[provider];
@@ -76,7 +76,7 @@ export function createDeleteApiKeyHandler() {
}
// Clear from in-memory storage
- setApiKey(provider, "");
+ setApiKey(provider, '');
// Remove from environment
delete process.env[envKey];
@@ -91,14 +91,11 @@ export function createDeleteApiKeyHandler() {
message: `API key for ${provider} has been deleted`,
});
} catch (error) {
- logger.error("[Setup] Delete API key error:", error);
+ logger.error('[Setup] Delete API key error:', error);
res.status(500).json({
success: false,
- error: error instanceof Error ? error.message : "Failed to delete API key",
+ error: error instanceof Error ? error.message : 'Failed to delete API key',
});
}
};
}
-
-
-
diff --git a/apps/server/src/routes/setup/routes/gh-status.ts b/apps/server/src/routes/setup/routes/gh-status.ts
index 7dcf5d82..4d36561c 100644
--- a/apps/server/src/routes/setup/routes/gh-status.ts
+++ b/apps/server/src/routes/setup/routes/gh-status.ts
@@ -2,24 +2,26 @@
* GET /gh-status endpoint - Get GitHub CLI status
*/
-import type { Request, Response } from "express";
-import { exec } from "child_process";
-import { promisify } from "util";
-import os from "os";
-import path from "path";
-import fs from "fs/promises";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import { exec } from 'child_process';
+import { promisify } from 'util';
+import os from 'os';
+import path from 'path';
+import fs from 'fs/promises';
+import { getErrorMessage, logError } from '../common.js';
const execAsync = promisify(exec);
// Extended PATH to include common tool installation locations
const extendedPath = [
process.env.PATH,
- "/opt/homebrew/bin",
- "/usr/local/bin",
- "/home/linuxbrew/.linuxbrew/bin",
+ '/opt/homebrew/bin',
+ '/usr/local/bin',
+ '/home/linuxbrew/.linuxbrew/bin',
`${process.env.HOME}/.local/bin`,
-].filter(Boolean).join(":");
+]
+ .filter(Boolean)
+ .join(':');
const execEnv = {
...process.env,
@@ -44,11 +46,11 @@ async function getGhStatus(): Promise {
user: null,
};
- const isWindows = process.platform === "win32";
+ const isWindows = process.platform === 'win32';
// Check if gh CLI is installed
try {
- const findCommand = isWindows ? "where gh" : "command -v gh";
+ const findCommand = isWindows ? 'where gh' : 'command -v gh';
const { stdout } = await execAsync(findCommand, { env: execEnv });
status.path = stdout.trim().split(/\r?\n/)[0];
status.installed = true;
@@ -56,14 +58,14 @@ async function getGhStatus(): Promise {
// gh not in PATH, try common locations
const commonPaths = isWindows
? [
- path.join(process.env.LOCALAPPDATA || "", "Programs", "gh", "bin", "gh.exe"),
- path.join(process.env.ProgramFiles || "", "GitHub CLI", "gh.exe"),
+ path.join(process.env.LOCALAPPDATA || '', 'Programs', 'gh', 'bin', 'gh.exe'),
+ path.join(process.env.ProgramFiles || '', 'GitHub CLI', 'gh.exe'),
]
: [
- "/opt/homebrew/bin/gh",
- "/usr/local/bin/gh",
- path.join(os.homedir(), ".local", "bin", "gh"),
- "/home/linuxbrew/.linuxbrew/bin/gh",
+ '/opt/homebrew/bin/gh',
+ '/usr/local/bin/gh',
+ path.join(os.homedir(), '.local', 'bin', 'gh'),
+ '/home/linuxbrew/.linuxbrew/bin/gh',
];
for (const p of commonPaths) {
@@ -84,30 +86,31 @@ async function getGhStatus(): Promise {
// Get version
try {
- const { stdout } = await execAsync("gh --version", { env: execEnv });
+ const { stdout } = await execAsync('gh --version', { env: execEnv });
// Extract version from output like "gh version 2.40.1 (2024-01-09)"
const versionMatch = stdout.match(/gh version ([\d.]+)/);
- status.version = versionMatch ? versionMatch[1] : stdout.trim().split("\n")[0];
+ status.version = versionMatch ? versionMatch[1] : stdout.trim().split('\n')[0];
} catch {
// Version command failed
}
// Check authentication status
try {
- const { stdout } = await execAsync("gh auth status", { env: execEnv });
+ const { stdout } = await execAsync('gh auth status', { env: execEnv });
// If this succeeds without error, we're authenticated
status.authenticated = true;
// Try to extract username from output
- const userMatch = stdout.match(/Logged in to [^\s]+ account ([^\s]+)/i) ||
- stdout.match(/Logged in to [^\s]+ as ([^\s]+)/i);
+ const userMatch =
+ stdout.match(/Logged in to [^\s]+ account ([^\s]+)/i) ||
+ stdout.match(/Logged in to [^\s]+ as ([^\s]+)/i);
if (userMatch) {
status.user = userMatch[1];
}
} catch (error: unknown) {
// Auth status returns non-zero if not authenticated
const err = error as { stderr?: string };
- if (err.stderr?.includes("not logged in")) {
+ if (err.stderr?.includes('not logged in')) {
status.authenticated = false;
}
}
@@ -124,7 +127,7 @@ export function createGhStatusHandler() {
...status,
});
} catch (error) {
- logError(error, "Get GitHub CLI status failed");
+ logError(error, 'Get GitHub CLI status failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/setup/routes/install-claude.ts b/apps/server/src/routes/setup/routes/install-claude.ts
index c471fc6c..644f5e10 100644
--- a/apps/server/src/routes/setup/routes/install-claude.ts
+++ b/apps/server/src/routes/setup/routes/install-claude.ts
@@ -2,8 +2,8 @@
* POST /install-claude endpoint - Install Claude CLI
*/
-import type { Request, Response } from "express";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import { getErrorMessage, logError } from '../common.js';
export function createInstallClaudeHandler() {
return async (_req: Request, res: Response): Promise => {
@@ -13,10 +13,10 @@ export function createInstallClaudeHandler() {
res.json({
success: false,
error:
- "CLI installation requires terminal access. Please install manually using: npm install -g @anthropic-ai/claude-code",
+ 'CLI installation requires terminal access. Please install manually using: npm install -g @anthropic-ai/claude-code',
});
} catch (error) {
- logError(error, "Install Claude CLI failed");
+ logError(error, 'Install Claude CLI failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/setup/routes/platform.ts b/apps/server/src/routes/setup/routes/platform.ts
index 40788d0b..303cdd87 100644
--- a/apps/server/src/routes/setup/routes/platform.ts
+++ b/apps/server/src/routes/setup/routes/platform.ts
@@ -2,9 +2,9 @@
* GET /platform endpoint - Get platform info
*/
-import type { Request, Response } from "express";
-import os from "os";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import os from 'os';
+import { getErrorMessage, logError } from '../common.js';
export function createPlatformHandler() {
return async (_req: Request, res: Response): Promise => {
@@ -15,12 +15,12 @@ export function createPlatformHandler() {
platform,
arch: os.arch(),
homeDir: os.homedir(),
- isWindows: platform === "win32",
- isMac: platform === "darwin",
- isLinux: platform === "linux",
+ isWindows: platform === 'win32',
+ isMac: platform === 'darwin',
+ isLinux: platform === 'linux',
});
} catch (error) {
- logError(error, "Get platform info failed");
+ logError(error, 'Get platform info failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/setup/routes/store-api-key.ts b/apps/server/src/routes/setup/routes/store-api-key.ts
index 71011e01..e77a697e 100644
--- a/apps/server/src/routes/setup/routes/store-api-key.ts
+++ b/apps/server/src/routes/setup/routes/store-api-key.ts
@@ -2,16 +2,11 @@
* POST /store-api-key endpoint - Store API key
*/
-import type { Request, Response } from "express";
-import {
- setApiKey,
- persistApiKeyToEnv,
- getErrorMessage,
- logError,
-} from "../common.js";
-import { createLogger } from "@automaker/utils";
+import type { Request, Response } from 'express';
+import { setApiKey, persistApiKeyToEnv, getErrorMessage, logError } from '../common.js';
+import { createLogger } from '@automaker/utils';
-const logger = createLogger("Setup");
+const logger = createLogger('Setup');
export function createStoreApiKeyHandler() {
return async (req: Request, res: Response): Promise => {
@@ -22,20 +17,18 @@ export function createStoreApiKeyHandler() {
};
if (!provider || !apiKey) {
- res
- .status(400)
- .json({ success: false, error: "provider and apiKey required" });
+ res.status(400).json({ success: false, error: 'provider and apiKey required' });
return;
}
setApiKey(provider, apiKey);
// Also set as environment variable and persist to .env
- if (provider === "anthropic" || provider === "anthropic_oauth_token") {
+ if (provider === 'anthropic' || provider === 'anthropic_oauth_token') {
// Both API key and OAuth token use ANTHROPIC_API_KEY
process.env.ANTHROPIC_API_KEY = apiKey;
- await persistApiKeyToEnv("ANTHROPIC_API_KEY", apiKey);
- logger.info("[Setup] Stored API key as ANTHROPIC_API_KEY");
+ await persistApiKeyToEnv('ANTHROPIC_API_KEY', apiKey);
+ logger.info('[Setup] Stored API key as ANTHROPIC_API_KEY');
} else {
res.status(400).json({
success: false,
@@ -46,7 +39,7 @@ export function createStoreApiKeyHandler() {
res.json({ success: true });
} catch (error) {
- logError(error, "Store API key failed");
+ logError(error, 'Store API key failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/setup/routes/verify-claude-auth.ts b/apps/server/src/routes/setup/routes/verify-claude-auth.ts
index 4b5438e3..5debc5c7 100644
--- a/apps/server/src/routes/setup/routes/verify-claude-auth.ts
+++ b/apps/server/src/routes/setup/routes/verify-claude-auth.ts
@@ -3,50 +3,50 @@
* Supports verifying either CLI auth or API key auth independently
*/
-import type { Request, Response } from "express";
-import { query } from "@anthropic-ai/claude-agent-sdk";
-import { createLogger } from "@automaker/utils";
-import { getApiKey } from "../common.js";
+import type { Request, Response } from 'express';
+import { query } from '@anthropic-ai/claude-agent-sdk';
+import { createLogger } from '@automaker/utils';
+import { getApiKey } from '../common.js';
-const logger = createLogger("Setup");
+const logger = createLogger('Setup');
// Known error patterns that indicate auth failure
const AUTH_ERROR_PATTERNS = [
- "OAuth token revoked",
- "Please run /login",
- "please run /login",
- "token revoked",
- "invalid_api_key",
- "authentication_error",
- "unauthorized",
- "not authenticated",
- "authentication failed",
- "invalid api key",
- "api key is invalid",
+ 'OAuth token revoked',
+ 'Please run /login',
+ 'please run /login',
+ 'token revoked',
+ 'invalid_api_key',
+ 'authentication_error',
+ 'unauthorized',
+ 'not authenticated',
+ 'authentication failed',
+ 'invalid api key',
+ 'api key is invalid',
];
// Patterns that indicate billing/credit issues - should FAIL verification
const BILLING_ERROR_PATTERNS = [
- "credit balance is too low",
- "credit balance too low",
- "insufficient credits",
- "insufficient balance",
- "no credits",
- "out of credits",
- "billing",
- "payment required",
- "add credits",
+ 'credit balance is too low',
+ 'credit balance too low',
+ 'insufficient credits',
+ 'insufficient balance',
+ 'no credits',
+ 'out of credits',
+ 'billing',
+ 'payment required',
+ 'add credits',
];
// Patterns that indicate rate/usage limits - should FAIL verification
// Users need to wait or upgrade their plan
const RATE_LIMIT_PATTERNS = [
- "limit reached",
- "rate limit",
- "rate_limit",
- "resets", // Only valid if it's a temporary reset, not a billing issue
- "/upgrade",
- "extra-usage",
+ 'limit reached',
+ 'rate limit',
+ 'rate_limit',
+ 'resets', // Only valid if it's a temporary reset, not a billing issue
+ '/upgrade',
+ 'extra-usage',
];
function isRateLimitError(text: string): boolean {
@@ -55,43 +55,33 @@ function isRateLimitError(text: string): boolean {
if (isBillingError(text)) {
return false;
}
- return RATE_LIMIT_PATTERNS.some((pattern) =>
- lowerText.includes(pattern.toLowerCase())
- );
+ return RATE_LIMIT_PATTERNS.some((pattern) => lowerText.includes(pattern.toLowerCase()));
}
function isBillingError(text: string): boolean {
const lowerText = text.toLowerCase();
- return BILLING_ERROR_PATTERNS.some((pattern) =>
- lowerText.includes(pattern.toLowerCase())
- );
+ return BILLING_ERROR_PATTERNS.some((pattern) => lowerText.includes(pattern.toLowerCase()));
}
function containsAuthError(text: string): boolean {
const lowerText = text.toLowerCase();
- return AUTH_ERROR_PATTERNS.some((pattern) =>
- lowerText.includes(pattern.toLowerCase())
- );
+ return AUTH_ERROR_PATTERNS.some((pattern) => lowerText.includes(pattern.toLowerCase()));
}
export function createVerifyClaudeAuthHandler() {
return async (req: Request, res: Response): Promise => {
try {
// Get the auth method from the request body
- const { authMethod } = req.body as { authMethod?: "cli" | "api_key" };
+ const { authMethod } = req.body as { authMethod?: 'cli' | 'api_key' };
- logger.info(
- `[Setup] Verifying Claude authentication using method: ${
- authMethod || "auto"
- }`
- );
+ logger.info(`[Setup] Verifying Claude authentication using method: ${authMethod || 'auto'}`);
// Create an AbortController with a 30-second timeout
const abortController = new AbortController();
const timeoutId = setTimeout(() => abortController.abort(), 30000);
let authenticated = false;
- let errorMessage = "";
+ let errorMessage = '';
let receivedAnyContent = false;
// Save original env values
@@ -99,25 +89,23 @@ export function createVerifyClaudeAuthHandler() {
try {
// Configure environment based on auth method
- if (authMethod === "cli") {
+ if (authMethod === 'cli') {
// For CLI verification, remove any API key so it uses CLI credentials only
delete process.env.ANTHROPIC_API_KEY;
- logger.info(
- "[Setup] Cleared API key environment for CLI verification"
- );
- } else if (authMethod === "api_key") {
+ logger.info('[Setup] Cleared API key environment for CLI verification');
+ } else if (authMethod === 'api_key') {
// For API key verification, ensure we're using the stored API key
- const storedApiKey = getApiKey("anthropic");
+ const storedApiKey = getApiKey('anthropic');
if (storedApiKey) {
process.env.ANTHROPIC_API_KEY = storedApiKey;
- logger.info("[Setup] Using stored API key for verification");
+ logger.info('[Setup] Using stored API key for verification');
} else {
// Check env var
if (!process.env.ANTHROPIC_API_KEY) {
res.json({
success: true,
authenticated: false,
- error: "No API key configured. Please enter an API key first.",
+ error: 'No API key configured. Please enter an API key first.',
});
return;
}
@@ -128,7 +116,7 @@ export function createVerifyClaudeAuthHandler() {
const stream = query({
prompt: "Reply with only the word 'ok'",
options: {
- model: "claude-sonnet-4-20250514",
+ model: 'claude-sonnet-4-20250514',
maxTurns: 1,
allowedTools: [],
abortController,
@@ -141,50 +129,50 @@ export function createVerifyClaudeAuthHandler() {
for await (const msg of stream) {
const msgStr = JSON.stringify(msg);
allMessages.push(msgStr);
- logger.info("[Setup] Stream message:", msgStr.substring(0, 500));
+ logger.info('[Setup] Stream message:', msgStr.substring(0, 500));
// Check for billing errors FIRST - these should fail verification
if (isBillingError(msgStr)) {
- logger.error("[Setup] Found billing error in message");
+ logger.error('[Setup] Found billing error in message');
errorMessage =
- "Credit balance is too low. Please add credits to your Anthropic account at console.anthropic.com";
+ 'Credit balance is too low. Please add credits to your Anthropic account at console.anthropic.com';
authenticated = false;
break;
}
// Check if any part of the message contains auth errors
if (containsAuthError(msgStr)) {
- logger.error("[Setup] Found auth error in message");
- if (authMethod === "cli") {
+ logger.error('[Setup] Found auth error in message');
+ if (authMethod === 'cli') {
errorMessage =
"CLI authentication failed. Please run 'claude login' in your terminal to authenticate.";
} else {
- errorMessage = "API key is invalid or has been revoked.";
+ errorMessage = 'API key is invalid or has been revoked.';
}
break;
}
// Check specifically for assistant messages with text content
- if (msg.type === "assistant" && (msg as any).message?.content) {
+ if (msg.type === 'assistant' && (msg as any).message?.content) {
const content = (msg as any).message.content;
if (Array.isArray(content)) {
for (const block of content) {
- if (block.type === "text" && block.text) {
+ if (block.type === 'text' && block.text) {
const text = block.text;
- logger.info("[Setup] Assistant text:", text);
+ logger.info('[Setup] Assistant text:', text);
if (containsAuthError(text)) {
- if (authMethod === "cli") {
+ if (authMethod === 'cli') {
errorMessage =
"CLI authentication failed. Please run 'claude login' in your terminal to authenticate.";
} else {
- errorMessage = "API key is invalid or has been revoked.";
+ errorMessage = 'API key is invalid or has been revoked.';
}
break;
}
// Valid text response that's not an error
- if (text.toLowerCase().includes("ok") || text.length > 0) {
+ if (text.toLowerCase().includes('ok') || text.length > 0) {
receivedAnyContent = true;
}
}
@@ -193,34 +181,30 @@ export function createVerifyClaudeAuthHandler() {
}
// Check for result messages
- if (msg.type === "result") {
+ if (msg.type === 'result') {
const resultStr = JSON.stringify(msg);
// First check for billing errors - these should FAIL verification
if (isBillingError(resultStr)) {
- logger.error(
- "[Setup] Billing error detected - insufficient credits"
- );
+ logger.error('[Setup] Billing error detected - insufficient credits');
errorMessage =
- "Credit balance is too low. Please add credits to your Anthropic account at console.anthropic.com";
+ 'Credit balance is too low. Please add credits to your Anthropic account at console.anthropic.com';
authenticated = false;
break;
}
// Check if it's a rate limit error - should FAIL verification
else if (isRateLimitError(resultStr)) {
- logger.warn(
- "[Setup] Rate limit detected - treating as unverified"
- );
+ logger.warn('[Setup] Rate limit detected - treating as unverified');
errorMessage =
- "Rate limit reached. Please wait a while before trying again or upgrade your plan.";
+ 'Rate limit reached. Please wait a while before trying again or upgrade your plan.';
authenticated = false;
break;
} else if (containsAuthError(resultStr)) {
- if (authMethod === "cli") {
+ if (authMethod === 'cli') {
errorMessage =
"CLI authentication failed. Please run 'claude login' in your terminal to authenticate.";
} else {
- errorMessage = "API key is invalid or has been revoked.";
+ errorMessage = 'API key is invalid or has been revoked.';
}
} else {
// Got a result without errors
@@ -236,60 +220,48 @@ export function createVerifyClaudeAuthHandler() {
authenticated = true;
} else {
// No content received - might be an issue
- logger.warn("[Setup] No content received from stream");
- logger.warn("[Setup] All messages:", allMessages.join("\n"));
- errorMessage =
- "No response received from Claude. Please check your authentication.";
+ logger.warn('[Setup] No content received from stream');
+ logger.warn('[Setup] All messages:', allMessages.join('\n'));
+ errorMessage = 'No response received from Claude. Please check your authentication.';
}
} catch (error: unknown) {
- const errMessage =
- error instanceof Error ? error.message : String(error);
+ const errMessage = error instanceof Error ? error.message : String(error);
- logger.error("[Setup] Claude auth verification exception:", errMessage);
+ logger.error('[Setup] Claude auth verification exception:', errMessage);
// Check for billing errors FIRST - these always fail
if (isBillingError(errMessage)) {
authenticated = false;
errorMessage =
- "Credit balance is too low. Please add credits to your Anthropic account at console.anthropic.com";
+ 'Credit balance is too low. Please add credits to your Anthropic account at console.anthropic.com';
}
// Check for rate limit in exception - should FAIL verification
else if (isRateLimitError(errMessage)) {
authenticated = false;
errorMessage =
- "Rate limit reached. Please wait a while before trying again or upgrade your plan.";
- logger.warn(
- "[Setup] Rate limit in exception - treating as unverified"
- );
+ 'Rate limit reached. Please wait a while before trying again or upgrade your plan.';
+ logger.warn('[Setup] Rate limit in exception - treating as unverified');
}
// If we already determined auth was successful, keep it
else if (authenticated) {
- logger.info("[Setup] Auth already confirmed, ignoring exception");
+ logger.info('[Setup] Auth already confirmed, ignoring exception');
}
// Check for auth-related errors in exception
else if (containsAuthError(errMessage)) {
- if (authMethod === "cli") {
+ if (authMethod === 'cli') {
errorMessage =
"CLI authentication failed. Please run 'claude login' in your terminal to authenticate.";
} else {
- errorMessage = "API key is invalid or has been revoked.";
+ errorMessage = 'API key is invalid or has been revoked.';
}
- } else if (
- errMessage.includes("abort") ||
- errMessage.includes("timeout")
- ) {
- errorMessage = "Verification timed out. Please try again.";
- } else if (
- errMessage.includes("exit") &&
- errMessage.includes("code 1")
- ) {
+ } else if (errMessage.includes('abort') || errMessage.includes('timeout')) {
+ errorMessage = 'Verification timed out. Please try again.';
+ } else if (errMessage.includes('exit') && errMessage.includes('code 1')) {
// Process exited with code 1 but we might have gotten rate limit info in the stream
// Check if we received any content that indicated auth worked
if (receivedAnyContent && !errorMessage) {
authenticated = true;
- logger.info(
- "[Setup] Process exit 1 but content received - auth valid"
- );
+ logger.info('[Setup] Process exit 1 but content received - auth valid');
} else if (!errorMessage) {
errorMessage = errMessage;
}
@@ -301,13 +273,13 @@ export function createVerifyClaudeAuthHandler() {
// Restore original environment
if (originalAnthropicKey !== undefined) {
process.env.ANTHROPIC_API_KEY = originalAnthropicKey;
- } else if (authMethod === "cli") {
+ } else if (authMethod === 'cli') {
// If we cleared it and there was no original, keep it cleared
delete process.env.ANTHROPIC_API_KEY;
}
}
- logger.info("[Setup] Verification result:", {
+ logger.info('[Setup] Verification result:', {
authenticated,
errorMessage,
authMethod,
@@ -319,11 +291,11 @@ export function createVerifyClaudeAuthHandler() {
error: errorMessage || undefined,
});
} catch (error) {
- logger.error("[Setup] Verify Claude auth endpoint error:", error);
+ logger.error('[Setup] Verify Claude auth endpoint error:', error);
res.status(500).json({
success: false,
authenticated: false,
- error: error instanceof Error ? error.message : "Verification failed",
+ error: error instanceof Error ? error.message : 'Verification failed',
});
}
};
diff --git a/apps/server/src/routes/suggestions/common.ts b/apps/server/src/routes/suggestions/common.ts
index 4816ca66..e4e3dbe8 100644
--- a/apps/server/src/routes/suggestions/common.ts
+++ b/apps/server/src/routes/suggestions/common.ts
@@ -2,13 +2,10 @@
* Common utilities and state for suggestions routes
*/
-import { createLogger } from "@automaker/utils";
-import {
- getErrorMessage as getErrorMessageShared,
- createLogError,
-} from "../common.js";
+import { createLogger } from '@automaker/utils';
+import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
-const logger = createLogger("Suggestions");
+const logger = createLogger('Suggestions');
// Shared state for tracking generation status - private
let isRunning = false;
@@ -27,10 +24,7 @@ export function getSuggestionsStatus(): {
/**
* Set the running state and abort controller
*/
-export function setRunningState(
- running: boolean,
- controller: AbortController | null = null
-): void {
+export function setRunningState(running: boolean, controller: AbortController | null = null): void {
isRunning = running;
currentAbortController = controller;
}
diff --git a/apps/server/src/routes/suggestions/generate-suggestions.ts b/apps/server/src/routes/suggestions/generate-suggestions.ts
index d0c985d9..42514a0a 100644
--- a/apps/server/src/routes/suggestions/generate-suggestions.ts
+++ b/apps/server/src/routes/suggestions/generate-suggestions.ts
@@ -2,43 +2,43 @@
* Business logic for generating suggestions
*/
-import { query } from "@anthropic-ai/claude-agent-sdk";
-import type { EventEmitter } from "../../lib/events.js";
-import { createLogger } from "@automaker/utils";
-import { createSuggestionsOptions } from "../../lib/sdk-options.js";
+import { query } from '@anthropic-ai/claude-agent-sdk';
+import type { EventEmitter } from '../../lib/events.js';
+import { createLogger } from '@automaker/utils';
+import { createSuggestionsOptions } from '../../lib/sdk-options.js';
-const logger = createLogger("Suggestions");
+const logger = createLogger('Suggestions');
/**
* JSON Schema for suggestions output
*/
const suggestionsSchema = {
- type: "object",
+ type: 'object',
properties: {
suggestions: {
- type: "array",
+ type: 'array',
items: {
- type: "object",
+ type: 'object',
properties: {
- id: { type: "string" },
- category: { type: "string" },
- description: { type: "string" },
+ id: { type: 'string' },
+ category: { type: 'string' },
+ description: { type: 'string' },
steps: {
- type: "array",
- items: { type: "string" },
+ type: 'array',
+ items: { type: 'string' },
},
- priority: {
- type: "number",
+ priority: {
+ type: 'number',
minimum: 1,
maximum: 3,
},
- reasoning: { type: "string" },
+ reasoning: { type: 'string' },
},
- required: ["category", "description", "steps", "priority", "reasoning"],
+ required: ['category', 'description', 'steps', 'priority', 'reasoning'],
},
},
},
- required: ["suggestions"],
+ required: ['suggestions'],
additionalProperties: false,
};
@@ -49,13 +49,10 @@ export async function generateSuggestions(
abortController: AbortController
): Promise {
const typePrompts: Record = {
- features:
- "Analyze this project and suggest new features that would add value.",
- refactoring: "Analyze this project and identify refactoring opportunities.",
- security:
- "Analyze this project for security vulnerabilities and suggest fixes.",
- performance:
- "Analyze this project for performance issues and suggest optimizations.",
+ features: 'Analyze this project and suggest new features that would add value.',
+ refactoring: 'Analyze this project and identify refactoring opportunities.',
+ security: 'Analyze this project for security vulnerabilities and suggest fixes.',
+ performance: 'Analyze this project for performance issues and suggest optimizations.',
};
const prompt = `${typePrompts[suggestionType] || typePrompts.features}
@@ -71,8 +68,8 @@ For each suggestion, provide:
The response will be automatically formatted as structured JSON.`;
- events.emit("suggestions:event", {
- type: "suggestions_progress",
+ events.emit('suggestions:event', {
+ type: 'suggestions_progress',
content: `Starting ${suggestionType} analysis...\n`,
});
@@ -80,48 +77,48 @@ The response will be automatically formatted as structured JSON.`;
cwd: projectPath,
abortController,
outputFormat: {
- type: "json_schema",
+ type: 'json_schema',
schema: suggestionsSchema,
},
});
const stream = query({ prompt, options });
- let responseText = "";
+ let responseText = '';
let structuredOutput: { suggestions: Array> } | null = null;
for await (const msg of stream) {
- if (msg.type === "assistant" && msg.message.content) {
+ if (msg.type === 'assistant' && msg.message.content) {
for (const block of msg.message.content) {
- if (block.type === "text") {
+ if (block.type === 'text') {
responseText += block.text;
- events.emit("suggestions:event", {
- type: "suggestions_progress",
+ events.emit('suggestions:event', {
+ type: 'suggestions_progress',
content: block.text,
});
- } else if (block.type === "tool_use") {
- events.emit("suggestions:event", {
- type: "suggestions_tool",
+ } else if (block.type === 'tool_use') {
+ events.emit('suggestions:event', {
+ type: 'suggestions_tool',
tool: block.name,
input: block.input,
});
}
}
- } else if (msg.type === "result" && msg.subtype === "success") {
+ } else if (msg.type === 'result' && msg.subtype === 'success') {
// Check for structured output
const resultMsg = msg as any;
if (resultMsg.structured_output) {
structuredOutput = resultMsg.structured_output as {
suggestions: Array>;
};
- logger.debug("Received structured output:", structuredOutput);
+ logger.debug('Received structured output:', structuredOutput);
}
- } else if (msg.type === "result") {
+ } else if (msg.type === 'result') {
const resultMsg = msg as any;
- if (resultMsg.subtype === "error_max_structured_output_retries") {
- logger.error("Failed to produce valid structured output after retries");
- throw new Error("Could not produce valid suggestions output");
- } else if (resultMsg.subtype === "error_max_turns") {
- logger.error("Hit max turns limit before completing suggestions generation");
+ if (resultMsg.subtype === 'error_max_structured_output_retries') {
+ logger.error('Failed to produce valid structured output after retries');
+ throw new Error('Could not produce valid suggestions output');
+ } else if (resultMsg.subtype === 'error_max_turns') {
+ logger.error('Hit max turns limit before completing suggestions generation');
logger.warn(`Response text length: ${responseText.length} chars`);
// Still try to parse what we have
}
@@ -132,49 +129,44 @@ The response will be automatically formatted as structured JSON.`;
try {
if (structuredOutput && structuredOutput.suggestions) {
// Use structured output directly
- events.emit("suggestions:event", {
- type: "suggestions_complete",
- suggestions: structuredOutput.suggestions.map(
- (s: Record, i: number) => ({
- ...s,
- id: s.id || `suggestion-${Date.now()}-${i}`,
- })
- ),
+ events.emit('suggestions:event', {
+ type: 'suggestions_complete',
+ suggestions: structuredOutput.suggestions.map((s: Record, i: number) => ({
+ ...s,
+ id: s.id || `suggestion-${Date.now()}-${i}`,
+ })),
});
} else {
// Fallback: try to parse from text (for backwards compatibility)
- logger.warn("No structured output received, attempting to parse from text");
+ logger.warn('No structured output received, attempting to parse from text');
const jsonMatch = responseText.match(/\{[\s\S]*"suggestions"[\s\S]*\}/);
if (jsonMatch) {
const parsed = JSON.parse(jsonMatch[0]);
- events.emit("suggestions:event", {
- type: "suggestions_complete",
- suggestions: parsed.suggestions.map(
- (s: Record, i: number) => ({
- ...s,
- id: s.id || `suggestion-${Date.now()}-${i}`,
- })
- ),
+ events.emit('suggestions:event', {
+ type: 'suggestions_complete',
+ suggestions: parsed.suggestions.map((s: Record, i: number) => ({
+ ...s,
+ id: s.id || `suggestion-${Date.now()}-${i}`,
+ })),
});
} else {
- throw new Error("No valid JSON found in response");
+ throw new Error('No valid JSON found in response');
}
}
} catch (error) {
// Log the parsing error for debugging
- logger.error("Failed to parse suggestions JSON from AI response:", error);
+ logger.error('Failed to parse suggestions JSON from AI response:', error);
// Return generic suggestions if parsing fails
- events.emit("suggestions:event", {
- type: "suggestions_complete",
+ events.emit('suggestions:event', {
+ type: 'suggestions_complete',
suggestions: [
{
id: `suggestion-${Date.now()}-0`,
- category: "Analysis",
- description: "Review the AI analysis output for insights",
- steps: ["Review the generated analysis"],
+ category: 'Analysis',
+ description: 'Review the AI analysis output for insights',
+ steps: ['Review the generated analysis'],
priority: 1,
- reasoning:
- "The AI provided analysis but suggestions need manual review",
+ reasoning: 'The AI provided analysis but suggestions need manual review',
},
],
});
diff --git a/apps/server/src/routes/suggestions/index.ts b/apps/server/src/routes/suggestions/index.ts
index a4b2ec20..2ea6f9ae 100644
--- a/apps/server/src/routes/suggestions/index.ts
+++ b/apps/server/src/routes/suggestions/index.ts
@@ -2,19 +2,19 @@
* Suggestions routes - HTTP API for AI-powered feature suggestions
*/
-import { Router } from "express";
-import type { EventEmitter } from "../../lib/events.js";
-import { validatePathParams } from "../../middleware/validate-paths.js";
-import { createGenerateHandler } from "./routes/generate.js";
-import { createStopHandler } from "./routes/stop.js";
-import { createStatusHandler } from "./routes/status.js";
+import { Router } from 'express';
+import type { EventEmitter } from '../../lib/events.js';
+import { validatePathParams } from '../../middleware/validate-paths.js';
+import { createGenerateHandler } from './routes/generate.js';
+import { createStopHandler } from './routes/stop.js';
+import { createStatusHandler } from './routes/status.js';
export function createSuggestionsRoutes(events: EventEmitter): Router {
const router = Router();
- router.post("/generate", validatePathParams("projectPath"), createGenerateHandler(events));
- router.post("/stop", createStopHandler());
- router.get("/status", createStatusHandler());
+ router.post('/generate', validatePathParams('projectPath'), createGenerateHandler(events));
+ router.post('/stop', createStopHandler());
+ router.get('/status', createStatusHandler());
return router;
}
diff --git a/apps/server/src/routes/suggestions/routes/generate.ts b/apps/server/src/routes/suggestions/routes/generate.ts
index 6a027a05..939e0cde 100644
--- a/apps/server/src/routes/suggestions/routes/generate.ts
+++ b/apps/server/src/routes/suggestions/routes/generate.ts
@@ -2,29 +2,24 @@
* POST /generate endpoint - Generate suggestions
*/
-import type { Request, Response } from "express";
-import type { EventEmitter } from "../../../lib/events.js";
-import { createLogger } from "@automaker/utils";
-import {
- getSuggestionsStatus,
- setRunningState,
- getErrorMessage,
- logError,
-} from "../common.js";
-import { generateSuggestions } from "../generate-suggestions.js";
+import type { Request, Response } from 'express';
+import type { EventEmitter } from '../../../lib/events.js';
+import { createLogger } from '@automaker/utils';
+import { getSuggestionsStatus, setRunningState, getErrorMessage, logError } from '../common.js';
+import { generateSuggestions } from '../generate-suggestions.js';
-const logger = createLogger("Suggestions");
+const logger = createLogger('Suggestions');
export function createGenerateHandler(events: EventEmitter) {
return async (req: Request, res: Response): Promise => {
try {
- const { projectPath, suggestionType = "features" } = req.body as {
+ const { projectPath, suggestionType = 'features' } = req.body as {
projectPath: string;
suggestionType?: string;
};
if (!projectPath) {
- res.status(400).json({ success: false, error: "projectPath required" });
+ res.status(400).json({ success: false, error: 'projectPath required' });
return;
}
@@ -32,7 +27,7 @@ export function createGenerateHandler(events: EventEmitter) {
if (isRunning) {
res.json({
success: false,
- error: "Suggestions generation is already running",
+ error: 'Suggestions generation is already running',
});
return;
}
@@ -44,9 +39,9 @@ export function createGenerateHandler(events: EventEmitter) {
// Start generation in background
generateSuggestions(projectPath, suggestionType, events, abortController)
.catch((error) => {
- logError(error, "Generate suggestions failed (background)");
- events.emit("suggestions:event", {
- type: "suggestions_error",
+ logError(error, 'Generate suggestions failed (background)');
+ events.emit('suggestions:event', {
+ type: 'suggestions_error',
error: getErrorMessage(error),
});
})
@@ -56,7 +51,7 @@ export function createGenerateHandler(events: EventEmitter) {
res.json({ success: true });
} catch (error) {
- logError(error, "Generate suggestions failed");
+ logError(error, 'Generate suggestions failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/suggestions/routes/status.ts b/apps/server/src/routes/suggestions/routes/status.ts
index d62dfa17..eb135e06 100644
--- a/apps/server/src/routes/suggestions/routes/status.ts
+++ b/apps/server/src/routes/suggestions/routes/status.ts
@@ -2,8 +2,8 @@
* GET /status endpoint - Get status
*/
-import type { Request, Response } from "express";
-import { getSuggestionsStatus, getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import { getSuggestionsStatus, getErrorMessage, logError } from '../common.js';
export function createStatusHandler() {
return async (_req: Request, res: Response): Promise => {
@@ -11,7 +11,7 @@ export function createStatusHandler() {
const { isRunning } = getSuggestionsStatus();
res.json({ success: true, isRunning });
} catch (error) {
- logError(error, "Get status failed");
+ logError(error, 'Get status failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/suggestions/routes/stop.ts b/apps/server/src/routes/suggestions/routes/stop.ts
index 3a18a0be..f9e01fb6 100644
--- a/apps/server/src/routes/suggestions/routes/stop.ts
+++ b/apps/server/src/routes/suggestions/routes/stop.ts
@@ -2,13 +2,8 @@
* POST /stop endpoint - Stop suggestions generation
*/
-import type { Request, Response } from "express";
-import {
- getSuggestionsStatus,
- setRunningState,
- getErrorMessage,
- logError,
-} from "../common.js";
+import type { Request, Response } from 'express';
+import { getSuggestionsStatus, setRunningState, getErrorMessage, logError } from '../common.js';
export function createStopHandler() {
return async (_req: Request, res: Response): Promise => {
@@ -20,7 +15,7 @@ export function createStopHandler() {
setRunningState(false, null);
res.json({ success: true });
} catch (error) {
- logError(error, "Stop suggestions failed");
+ logError(error, 'Stop suggestions failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/templates/common.ts b/apps/server/src/routes/templates/common.ts
index 4ffb9e8b..d0ee96dd 100644
--- a/apps/server/src/routes/templates/common.ts
+++ b/apps/server/src/routes/templates/common.ts
@@ -2,13 +2,10 @@
* Common utilities for templates routes
*/
-import { createLogger } from "@automaker/utils";
-import {
- getErrorMessage as getErrorMessageShared,
- createLogError,
-} from "../common.js";
+import { createLogger } from '@automaker/utils';
+import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
-export const logger = createLogger("Templates");
+export const logger = createLogger('Templates');
// Re-export shared utilities
export { getErrorMessageShared as getErrorMessage };
diff --git a/apps/server/src/routes/templates/index.ts b/apps/server/src/routes/templates/index.ts
index 4e7462fe..38eb0270 100644
--- a/apps/server/src/routes/templates/index.ts
+++ b/apps/server/src/routes/templates/index.ts
@@ -3,13 +3,13 @@
* Provides API for cloning GitHub starter templates
*/
-import { Router } from "express";
-import { createCloneHandler } from "./routes/clone.js";
+import { Router } from 'express';
+import { createCloneHandler } from './routes/clone.js';
export function createTemplatesRoutes(): Router {
const router = Router();
- router.post("/clone", createCloneHandler());
+ router.post('/clone', createCloneHandler());
return router;
}
diff --git a/apps/server/src/routes/terminal/common.ts b/apps/server/src/routes/terminal/common.ts
index eccde756..6121e345 100644
--- a/apps/server/src/routes/terminal/common.ts
+++ b/apps/server/src/routes/terminal/common.ts
@@ -2,11 +2,12 @@
* Common utilities and state for terminal routes
*/
-import { createLogger } from "@automaker/utils";
-import type { Request, Response, NextFunction } from "express";
-import { getTerminalService } from "../../services/terminal-service.js";
+import { randomBytes } from 'crypto';
+import { createLogger } from '@automaker/utils';
+import type { Request, Response, NextFunction } from 'express';
+import { getTerminalService } from '../../services/terminal-service.js';
-const logger = createLogger("Terminal");
+const logger = createLogger('Terminal');
// Read env variables lazily to ensure dotenv has loaded them
function getTerminalPassword(): string | undefined {
@@ -14,21 +15,17 @@ function getTerminalPassword(): string | undefined {
}
function getTerminalEnabledConfig(): boolean {
- return process.env.TERMINAL_ENABLED !== "false"; // Enabled by default
+ return process.env.TERMINAL_ENABLED !== 'false'; // Enabled by default
}
// In-memory session tokens (would use Redis in production) - private
-const validTokens: Map =
- new Map();
+const validTokens: Map = new Map();
const TOKEN_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours
/**
* Add a token to the valid tokens map
*/
-export function addToken(
- token: string,
- data: { createdAt: Date; expiresAt: Date }
-): void {
+export function addToken(token: string, data: { createdAt: Date; expiresAt: Date }): void {
validTokens.set(token, data);
}
@@ -42,19 +39,15 @@ export function deleteToken(token: string): void {
/**
* Get token data for a given token
*/
-export function getTokenData(
- token: string
-): { createdAt: Date; expiresAt: Date } | undefined {
+export function getTokenData(token: string): { createdAt: Date; expiresAt: Date } | undefined {
return validTokens.get(token);
}
/**
- * Generate a secure random token
+ * Generate a cryptographically secure random token
*/
export function generateToken(): string {
- return `term-${Date.now()}-${Math.random()
- .toString(36)
- .substr(2, 15)}${Math.random().toString(36).substr(2, 15)}`;
+ return `term-${randomBytes(32).toString('base64url')}`;
}
/**
@@ -107,16 +100,12 @@ export function isTerminalEnabled(): boolean {
* Terminal authentication middleware
* Checks for valid session token if password is configured
*/
-export function terminalAuthMiddleware(
- req: Request,
- res: Response,
- next: NextFunction
-): void {
+export function terminalAuthMiddleware(req: Request, res: Response, next: NextFunction): void {
// Check if terminal is enabled
if (!getTerminalEnabledConfig()) {
res.status(403).json({
success: false,
- error: "Terminal access is disabled",
+ error: 'Terminal access is disabled',
});
return;
}
@@ -128,13 +117,12 @@ export function terminalAuthMiddleware(
}
// Check for session token
- const token =
- (req.headers["x-terminal-token"] as string) || (req.query.token as string);
+ const token = (req.headers['x-terminal-token'] as string) || (req.query.token as string);
if (!validateTerminalToken(token)) {
res.status(401).json({
success: false,
- error: "Terminal authentication required",
+ error: 'Terminal authentication required',
passwordRequired: true,
});
return;
@@ -155,10 +143,7 @@ export function getTokenExpiryMs(): number {
return TOKEN_EXPIRY_MS;
}
-import {
- getErrorMessage as getErrorMessageShared,
- createLogError,
-} from "../common.js";
+import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
// Re-export shared utilities
export { getErrorMessageShared as getErrorMessage };
diff --git a/apps/server/src/routes/terminal/index.ts b/apps/server/src/routes/terminal/index.ts
index 7ee0e978..380801e5 100644
--- a/apps/server/src/routes/terminal/index.ts
+++ b/apps/server/src/routes/terminal/index.ts
@@ -5,22 +5,20 @@
* WebSocket connections for real-time I/O are handled separately in index.ts.
*/
-import { Router } from "express";
+import { Router } from 'express';
import {
terminalAuthMiddleware,
validateTerminalToken,
isTerminalEnabled,
isTerminalPasswordRequired,
-} from "./common.js";
-import { createStatusHandler } from "./routes/status.js";
-import { createAuthHandler } from "./routes/auth.js";
-import { createLogoutHandler } from "./routes/logout.js";
-import {
- createSessionsListHandler,
- createSessionsCreateHandler,
-} from "./routes/sessions.js";
-import { createSessionDeleteHandler } from "./routes/session-delete.js";
-import { createSessionResizeHandler } from "./routes/session-resize.js";
+} from './common.js';
+import { createStatusHandler } from './routes/status.js';
+import { createAuthHandler } from './routes/auth.js';
+import { createLogoutHandler } from './routes/logout.js';
+import { createSessionsListHandler, createSessionsCreateHandler } from './routes/sessions.js';
+import { createSessionDeleteHandler } from './routes/session-delete.js';
+import { createSessionResizeHandler } from './routes/session-resize.js';
+import { createSettingsGetHandler, createSettingsUpdateHandler } from './routes/settings.js';
// Re-export for use in main index.ts
export { validateTerminalToken, isTerminalEnabled, isTerminalPasswordRequired };
@@ -28,17 +26,19 @@ export { validateTerminalToken, isTerminalEnabled, isTerminalPasswordRequired };
export function createTerminalRoutes(): Router {
const router = Router();
- router.get("/status", createStatusHandler());
- router.post("/auth", createAuthHandler());
- router.post("/logout", createLogoutHandler());
+ router.get('/status', createStatusHandler());
+ router.post('/auth', createAuthHandler());
+ router.post('/logout', createLogoutHandler());
// Apply terminal auth middleware to all routes below
router.use(terminalAuthMiddleware);
- router.get("/sessions", createSessionsListHandler());
- router.post("/sessions", createSessionsCreateHandler());
- router.delete("/sessions/:id", createSessionDeleteHandler());
- router.post("/sessions/:id/resize", createSessionResizeHandler());
+ router.get('/sessions', createSessionsListHandler());
+ router.post('/sessions', createSessionsCreateHandler());
+ router.delete('/sessions/:id', createSessionDeleteHandler());
+ router.post('/sessions/:id/resize', createSessionResizeHandler());
+ router.get('/settings', createSettingsGetHandler());
+ router.put('/settings', createSettingsUpdateHandler());
return router;
}
diff --git a/apps/server/src/routes/terminal/routes/auth.ts b/apps/server/src/routes/terminal/routes/auth.ts
index 234d4572..1d6156bd 100644
--- a/apps/server/src/routes/terminal/routes/auth.ts
+++ b/apps/server/src/routes/terminal/routes/auth.ts
@@ -2,7 +2,7 @@
* POST /auth endpoint - Authenticate with password to get a session token
*/
-import type { Request, Response } from "express";
+import type { Request, Response } from 'express';
import {
getTerminalEnabledConfigValue,
getTerminalPasswordConfig,
@@ -10,14 +10,14 @@ import {
addToken,
getTokenExpiryMs,
getErrorMessage,
-} from "../common.js";
+} from '../common.js';
export function createAuthHandler() {
return (req: Request, res: Response): void => {
if (!getTerminalEnabledConfigValue()) {
res.status(403).json({
success: false,
- error: "Terminal access is disabled",
+ error: 'Terminal access is disabled',
});
return;
}
@@ -41,7 +41,7 @@ export function createAuthHandler() {
if (!password || password !== terminalPassword) {
res.status(401).json({
success: false,
- error: "Invalid password",
+ error: 'Invalid password',
});
return;
}
diff --git a/apps/server/src/routes/terminal/routes/logout.ts b/apps/server/src/routes/terminal/routes/logout.ts
index 9e3c8fa3..2af85713 100644
--- a/apps/server/src/routes/terminal/routes/logout.ts
+++ b/apps/server/src/routes/terminal/routes/logout.ts
@@ -2,12 +2,12 @@
* POST /logout endpoint - Invalidate a session token
*/
-import type { Request, Response } from "express";
-import { deleteToken } from "../common.js";
+import type { Request, Response } from 'express';
+import { deleteToken } from '../common.js';
export function createLogoutHandler() {
return (req: Request, res: Response): void => {
- const token = (req.headers["x-terminal-token"] as string) || req.body.token;
+ const token = (req.headers['x-terminal-token'] as string) || req.body.token;
if (token) {
deleteToken(token);
diff --git a/apps/server/src/routes/terminal/routes/session-delete.ts b/apps/server/src/routes/terminal/routes/session-delete.ts
index aa3f96cb..dec3c694 100644
--- a/apps/server/src/routes/terminal/routes/session-delete.ts
+++ b/apps/server/src/routes/terminal/routes/session-delete.ts
@@ -2,8 +2,8 @@
* DELETE /sessions/:id endpoint - Kill a terminal session
*/
-import type { Request, Response } from "express";
-import { getTerminalService } from "../../../services/terminal-service.js";
+import type { Request, Response } from 'express';
+import { getTerminalService } from '../../../services/terminal-service.js';
export function createSessionDeleteHandler() {
return (req: Request, res: Response): void => {
@@ -14,7 +14,7 @@ export function createSessionDeleteHandler() {
if (!killed) {
res.status(404).json({
success: false,
- error: "Session not found",
+ error: 'Session not found',
});
return;
}
diff --git a/apps/server/src/routes/terminal/routes/session-resize.ts b/apps/server/src/routes/terminal/routes/session-resize.ts
index a6a8a70d..41db9763 100644
--- a/apps/server/src/routes/terminal/routes/session-resize.ts
+++ b/apps/server/src/routes/terminal/routes/session-resize.ts
@@ -2,8 +2,8 @@
* POST /sessions/:id/resize endpoint - Resize a terminal session
*/
-import type { Request, Response } from "express";
-import { getTerminalService } from "../../../services/terminal-service.js";
+import type { Request, Response } from 'express';
+import { getTerminalService } from '../../../services/terminal-service.js';
export function createSessionResizeHandler() {
return (req: Request, res: Response): void => {
@@ -14,7 +14,7 @@ export function createSessionResizeHandler() {
if (!cols || !rows) {
res.status(400).json({
success: false,
- error: "cols and rows are required",
+ error: 'cols and rows are required',
});
return;
}
@@ -24,7 +24,7 @@ export function createSessionResizeHandler() {
if (!resized) {
res.status(404).json({
success: false,
- error: "Session not found",
+ error: 'Session not found',
});
return;
}
diff --git a/apps/server/src/routes/terminal/routes/sessions.ts b/apps/server/src/routes/terminal/routes/sessions.ts
index c9d6133c..a7f42509 100644
--- a/apps/server/src/routes/terminal/routes/sessions.ts
+++ b/apps/server/src/routes/terminal/routes/sessions.ts
@@ -3,12 +3,12 @@
* POST /sessions endpoint - Create a new terminal session
*/
-import type { Request, Response } from "express";
-import { getTerminalService } from "../../../services/terminal-service.js";
-import { getErrorMessage, logError } from "../common.js";
-import { createLogger } from "@automaker/utils";
+import type { Request, Response } from 'express';
+import { getTerminalService } from '../../../services/terminal-service.js';
+import { getErrorMessage, logError } from '../common.js';
+import { createLogger } from '@automaker/utils';
-const logger = createLogger("Terminal");
+const logger = createLogger('Terminal');
export function createSessionsListHandler() {
return (_req: Request, res: Response): void => {
@@ -34,6 +34,21 @@ export function createSessionsCreateHandler() {
shell,
});
+ // Check if session creation was refused due to limit
+ if (!session) {
+ const maxSessions = terminalService.getMaxSessions();
+ const currentSessions = terminalService.getSessionCount();
+ logger.warn(`Session limit reached: ${currentSessions}/${maxSessions}`);
+ res.status(429).json({
+ success: false,
+ error: 'Maximum terminal sessions reached',
+ details: `Server limit is ${maxSessions} concurrent sessions. Please close unused terminals.`,
+ currentSessions,
+ maxSessions,
+ });
+ return;
+ }
+
res.json({
success: true,
data: {
@@ -44,10 +59,10 @@ export function createSessionsCreateHandler() {
},
});
} catch (error) {
- logError(error, "Create terminal session failed");
+ logError(error, 'Create terminal session failed');
res.status(500).json({
success: false,
- error: "Failed to create terminal session",
+ error: 'Failed to create terminal session',
details: getErrorMessage(error),
});
}
diff --git a/apps/server/src/routes/terminal/routes/settings.ts b/apps/server/src/routes/terminal/routes/settings.ts
new file mode 100644
index 00000000..9d814606
--- /dev/null
+++ b/apps/server/src/routes/terminal/routes/settings.ts
@@ -0,0 +1,83 @@
+/**
+ * GET/PUT /settings endpoint - Get/Update terminal settings
+ */
+
+import type { Request, Response } from 'express';
+import {
+ getTerminalService,
+ MIN_MAX_SESSIONS,
+ MAX_MAX_SESSIONS,
+} from '../../../services/terminal-service.js';
+import { getErrorMessage, logError } from '../common.js';
+
+export function createSettingsGetHandler() {
+ return (_req: Request, res: Response): void => {
+ try {
+ const terminalService = getTerminalService();
+ res.json({
+ success: true,
+ data: {
+ maxSessions: terminalService.getMaxSessions(),
+ currentSessions: terminalService.getSessionCount(),
+ },
+ });
+ } catch (error) {
+ logError(error, 'Get terminal settings failed');
+ res.status(500).json({
+ success: false,
+ error: 'Failed to get terminal settings',
+ details: getErrorMessage(error),
+ });
+ }
+ };
+}
+
+export function createSettingsUpdateHandler() {
+ return (req: Request, res: Response): void => {
+ try {
+ const terminalService = getTerminalService();
+ const { maxSessions } = req.body;
+
+ // Validate maxSessions if provided
+ if (maxSessions !== undefined) {
+ if (typeof maxSessions !== 'number') {
+ res.status(400).json({
+ success: false,
+ error: 'maxSessions must be a number',
+ });
+ return;
+ }
+ if (!Number.isInteger(maxSessions)) {
+ res.status(400).json({
+ success: false,
+ error: 'maxSessions must be an integer',
+ });
+ return;
+ }
+ if (maxSessions < MIN_MAX_SESSIONS || maxSessions > MAX_MAX_SESSIONS) {
+ res.status(400).json({
+ success: false,
+ error: `maxSessions must be between ${MIN_MAX_SESSIONS} and ${MAX_MAX_SESSIONS}`,
+ });
+ return;
+ }
+ terminalService.setMaxSessions(maxSessions);
+ }
+
+ res.json({
+ success: true,
+ data: {
+ maxSessions: terminalService.getMaxSessions(),
+ currentSessions: terminalService.getSessionCount(),
+ },
+ });
+ } catch (error) {
+ logError(error, 'Update terminal settings failed');
+ res.status(500).json({
+ success: false,
+ error: 'Failed to update terminal settings',
+ details: getErrorMessage(error),
+ });
+ }
+ };
+}
diff --git a/apps/server/src/routes/terminal/routes/status.ts b/apps/server/src/routes/terminal/routes/status.ts
index 014c482a..670b405c 100644
--- a/apps/server/src/routes/terminal/routes/status.ts
+++ b/apps/server/src/routes/terminal/routes/status.ts
@@ -2,12 +2,9 @@
* GET /status endpoint - Get terminal status
*/
-import type { Request, Response } from "express";
-import { getTerminalService } from "../../../services/terminal-service.js";
-import {
- getTerminalEnabledConfigValue,
- isTerminalPasswordRequired,
-} from "../common.js";
+import type { Request, Response } from 'express';
+import { getTerminalService } from '../../../services/terminal-service.js';
+import { getTerminalEnabledConfigValue, isTerminalPasswordRequired } from '../common.js';
export function createStatusHandler() {
return (_req: Request, res: Response): void => {
diff --git a/apps/server/src/routes/workspace/common.ts b/apps/server/src/routes/workspace/common.ts
index 10105baf..bec656dd 100644
--- a/apps/server/src/routes/workspace/common.ts
+++ b/apps/server/src/routes/workspace/common.ts
@@ -2,13 +2,10 @@
* Common utilities for workspace routes
*/
-import { createLogger } from "@automaker/utils";
-import {
- getErrorMessage as getErrorMessageShared,
- createLogError,
-} from "../common.js";
+import { createLogger } from '@automaker/utils';
+import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
-const logger = createLogger("Workspace");
+const logger = createLogger('Workspace');
// Re-export shared utilities
export { getErrorMessageShared as getErrorMessage };
diff --git a/apps/server/src/routes/workspace/index.ts b/apps/server/src/routes/workspace/index.ts
index ec247a89..37424997 100644
--- a/apps/server/src/routes/workspace/index.ts
+++ b/apps/server/src/routes/workspace/index.ts
@@ -3,15 +3,15 @@
* Provides API endpoints for workspace directory management
*/
-import { Router } from "express";
-import { createConfigHandler } from "./routes/config.js";
-import { createDirectoriesHandler } from "./routes/directories.js";
+import { Router } from 'express';
+import { createConfigHandler } from './routes/config.js';
+import { createDirectoriesHandler } from './routes/directories.js';
export function createWorkspaceRoutes(): Router {
const router = Router();
- router.get("/config", createConfigHandler());
- router.get("/directories", createDirectoriesHandler());
+ router.get('/config', createConfigHandler());
+ router.get('/directories', createDirectoriesHandler());
return router;
}
diff --git a/apps/server/src/routes/worktree/index.ts b/apps/server/src/routes/worktree/index.ts
index 6d81c854..a3780b45 100644
--- a/apps/server/src/routes/worktree/index.ts
+++ b/apps/server/src/routes/worktree/index.ts
@@ -2,60 +2,64 @@
* Worktree routes - HTTP API for git worktree operations
*/
-import { Router } from "express";
-import { validatePathParams } from "../../middleware/validate-paths.js";
-import { createInfoHandler } from "./routes/info.js";
-import { createStatusHandler } from "./routes/status.js";
-import { createListHandler } from "./routes/list.js";
-import { createDiffsHandler } from "./routes/diffs.js";
-import { createFileDiffHandler } from "./routes/file-diff.js";
-import { createMergeHandler } from "./routes/merge.js";
-import { createCreateHandler } from "./routes/create.js";
-import { createDeleteHandler } from "./routes/delete.js";
-import { createCreatePRHandler } from "./routes/create-pr.js";
-import { createPRInfoHandler } from "./routes/pr-info.js";
-import { createCommitHandler } from "./routes/commit.js";
-import { createPushHandler } from "./routes/push.js";
-import { createPullHandler } from "./routes/pull.js";
-import { createCheckoutBranchHandler } from "./routes/checkout-branch.js";
-import { createListBranchesHandler } from "./routes/list-branches.js";
-import { createSwitchBranchHandler } from "./routes/switch-branch.js";
+import { Router } from 'express';
+import { validatePathParams } from '../../middleware/validate-paths.js';
+import { createInfoHandler } from './routes/info.js';
+import { createStatusHandler } from './routes/status.js';
+import { createListHandler } from './routes/list.js';
+import { createDiffsHandler } from './routes/diffs.js';
+import { createFileDiffHandler } from './routes/file-diff.js';
+import { createMergeHandler } from './routes/merge.js';
+import { createCreateHandler } from './routes/create.js';
+import { createDeleteHandler } from './routes/delete.js';
+import { createCreatePRHandler } from './routes/create-pr.js';
+import { createPRInfoHandler } from './routes/pr-info.js';
+import { createCommitHandler } from './routes/commit.js';
+import { createPushHandler } from './routes/push.js';
+import { createPullHandler } from './routes/pull.js';
+import { createCheckoutBranchHandler } from './routes/checkout-branch.js';
+import { createListBranchesHandler } from './routes/list-branches.js';
+import { createSwitchBranchHandler } from './routes/switch-branch.js';
import {
createOpenInEditorHandler,
createGetDefaultEditorHandler,
-} from "./routes/open-in-editor.js";
-import { createInitGitHandler } from "./routes/init-git.js";
-import { createMigrateHandler } from "./routes/migrate.js";
-import { createStartDevHandler } from "./routes/start-dev.js";
-import { createStopDevHandler } from "./routes/stop-dev.js";
-import { createListDevServersHandler } from "./routes/list-dev-servers.js";
+} from './routes/open-in-editor.js';
+import { createInitGitHandler } from './routes/init-git.js';
+import { createMigrateHandler } from './routes/migrate.js';
+import { createStartDevHandler } from './routes/start-dev.js';
+import { createStopDevHandler } from './routes/stop-dev.js';
+import { createListDevServersHandler } from './routes/list-dev-servers.js';
export function createWorktreeRoutes(): Router {
const router = Router();
- router.post("/info", validatePathParams("projectPath"), createInfoHandler());
- router.post("/status", validatePathParams("projectPath"), createStatusHandler());
- router.post("/list", createListHandler());
- router.post("/diffs", validatePathParams("projectPath"), createDiffsHandler());
- router.post("/file-diff", validatePathParams("projectPath", "filePath"), createFileDiffHandler());
- router.post("/merge", validatePathParams("projectPath"), createMergeHandler());
- router.post("/create", validatePathParams("projectPath"), createCreateHandler());
- router.post("/delete", validatePathParams("projectPath", "worktreePath"), createDeleteHandler());
- router.post("/create-pr", createCreatePRHandler());
- router.post("/pr-info", createPRInfoHandler());
- router.post("/commit", validatePathParams("worktreePath"), createCommitHandler());
- router.post("/push", validatePathParams("worktreePath"), createPushHandler());
- router.post("/pull", validatePathParams("worktreePath"), createPullHandler());
- router.post("/checkout-branch", createCheckoutBranchHandler());
- router.post("/list-branches", validatePathParams("worktreePath"), createListBranchesHandler());
- router.post("/switch-branch", createSwitchBranchHandler());
- router.post("/open-in-editor", validatePathParams("worktreePath"), createOpenInEditorHandler());
- router.get("/default-editor", createGetDefaultEditorHandler());
- router.post("/init-git", validatePathParams("projectPath"), createInitGitHandler());
- router.post("/migrate", createMigrateHandler());
- router.post("/start-dev", validatePathParams("projectPath", "worktreePath"), createStartDevHandler());
- router.post("/stop-dev", createStopDevHandler());
- router.post("/list-dev-servers", createListDevServersHandler());
+ router.post('/info', validatePathParams('projectPath'), createInfoHandler());
+ router.post('/status', validatePathParams('projectPath'), createStatusHandler());
+ router.post('/list', createListHandler());
+ router.post('/diffs', validatePathParams('projectPath'), createDiffsHandler());
+ router.post('/file-diff', validatePathParams('projectPath', 'filePath'), createFileDiffHandler());
+ router.post('/merge', validatePathParams('projectPath'), createMergeHandler());
+ router.post('/create', validatePathParams('projectPath'), createCreateHandler());
+ router.post('/delete', validatePathParams('projectPath', 'worktreePath'), createDeleteHandler());
+ router.post('/create-pr', createCreatePRHandler());
+ router.post('/pr-info', createPRInfoHandler());
+ router.post('/commit', validatePathParams('worktreePath'), createCommitHandler());
+ router.post('/push', validatePathParams('worktreePath'), createPushHandler());
+ router.post('/pull', validatePathParams('worktreePath'), createPullHandler());
+ router.post('/checkout-branch', createCheckoutBranchHandler());
+ router.post('/list-branches', validatePathParams('worktreePath'), createListBranchesHandler());
+ router.post('/switch-branch', createSwitchBranchHandler());
+ router.post('/open-in-editor', validatePathParams('worktreePath'), createOpenInEditorHandler());
+ router.get('/default-editor', createGetDefaultEditorHandler());
+ router.post('/init-git', validatePathParams('projectPath'), createInitGitHandler());
+ router.post('/migrate', createMigrateHandler());
+ router.post(
+ '/start-dev',
+ validatePathParams('projectPath', 'worktreePath'),
+ createStartDevHandler()
+ );
+ router.post('/stop-dev', createStopDevHandler());
+ router.post('/list-dev-servers', createListDevServersHandler());
return router;
}
diff --git a/apps/server/src/routes/worktree/routes/checkout-branch.ts b/apps/server/src/routes/worktree/routes/checkout-branch.ts
index 50254a69..ef8ddc47 100644
--- a/apps/server/src/routes/worktree/routes/checkout-branch.ts
+++ b/apps/server/src/routes/worktree/routes/checkout-branch.ts
@@ -2,10 +2,10 @@
* POST /checkout-branch endpoint - Create and checkout a new branch
*/
-import type { Request, Response } from "express";
-import { exec } from "child_process";
-import { promisify } from "util";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import { exec } from 'child_process';
+import { promisify } from 'util';
+import { getErrorMessage, logError } from '../common.js';
const execAsync = promisify(exec);
@@ -20,7 +20,7 @@ export function createCheckoutBranchHandler() {
if (!worktreePath) {
res.status(400).json({
success: false,
- error: "worktreePath required",
+ error: 'worktreePath required',
});
return;
}
@@ -28,7 +28,7 @@ export function createCheckoutBranchHandler() {
if (!branchName) {
res.status(400).json({
success: false,
- error: "branchName required",
+ error: 'branchName required',
});
return;
}
@@ -38,16 +38,15 @@ export function createCheckoutBranchHandler() {
if (invalidChars.test(branchName)) {
res.status(400).json({
success: false,
- error: "Branch name contains invalid characters",
+ error: 'Branch name contains invalid characters',
});
return;
}
// Get current branch for reference
- const { stdout: currentBranchOutput } = await execAsync(
- "git rev-parse --abbrev-ref HEAD",
- { cwd: worktreePath }
- );
+ const { stdout: currentBranchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', {
+ cwd: worktreePath,
+ });
const currentBranch = currentBranchOutput.trim();
// Check if branch already exists
@@ -79,7 +78,7 @@ export function createCheckoutBranchHandler() {
},
});
} catch (error) {
- logError(error, "Checkout branch failed");
+ logError(error, 'Checkout branch failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/worktree/routes/commit.ts b/apps/server/src/routes/worktree/routes/commit.ts
index 273c7964..6cdc39c1 100644
--- a/apps/server/src/routes/worktree/routes/commit.ts
+++ b/apps/server/src/routes/worktree/routes/commit.ts
@@ -2,10 +2,10 @@
* POST /commit endpoint - Commit changes in a worktree
*/
-import type { Request, Response } from "express";
-import { exec } from "child_process";
-import { promisify } from "util";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import { exec } from 'child_process';
+import { promisify } from 'util';
+import { getErrorMessage, logError } from '../common.js';
const execAsync = promisify(exec);
@@ -20,13 +20,13 @@ export function createCommitHandler() {
if (!worktreePath || !message) {
res.status(400).json({
success: false,
- error: "worktreePath and message required",
+ error: 'worktreePath and message required',
});
return;
}
// Check for uncommitted changes
- const { stdout: status } = await execAsync("git status --porcelain", {
+ const { stdout: status } = await execAsync('git status --porcelain', {
cwd: worktreePath,
});
@@ -35,14 +35,14 @@ export function createCommitHandler() {
success: true,
result: {
committed: false,
- message: "No changes to commit",
+ message: 'No changes to commit',
},
});
return;
}
// Stage all changes
- await execAsync("git add -A", { cwd: worktreePath });
+ await execAsync('git add -A', { cwd: worktreePath });
// Create commit
await execAsync(`git commit -m "${message.replace(/"/g, '\\"')}"`, {
@@ -50,16 +50,15 @@ export function createCommitHandler() {
});
// Get commit hash
- const { stdout: hashOutput } = await execAsync("git rev-parse HEAD", {
+ const { stdout: hashOutput } = await execAsync('git rev-parse HEAD', {
cwd: worktreePath,
});
const commitHash = hashOutput.trim().substring(0, 8);
// Get branch name
- const { stdout: branchOutput } = await execAsync(
- "git rev-parse --abbrev-ref HEAD",
- { cwd: worktreePath }
- );
+ const { stdout: branchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', {
+ cwd: worktreePath,
+ });
const branchName = branchOutput.trim();
res.json({
@@ -72,7 +71,7 @@ export function createCommitHandler() {
},
});
} catch (error) {
- logError(error, "Commit worktree failed");
+ logError(error, 'Commit worktree failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/worktree/routes/create-pr.ts b/apps/server/src/routes/worktree/routes/create-pr.ts
index 488fa3b5..1e71bfab 100644
--- a/apps/server/src/routes/worktree/routes/create-pr.ts
+++ b/apps/server/src/routes/worktree/routes/create-pr.ts
@@ -2,7 +2,7 @@
* POST /create-pr endpoint - Commit changes and create a pull request from a worktree
*/
-import type { Request, Response } from "express";
+import type { Request, Response } from 'express';
import {
getErrorMessage,
logError,
@@ -10,26 +10,27 @@ import {
execEnv,
isValidBranchName,
isGhCliAvailable,
-} from "../common.js";
-import { updateWorktreePRInfo } from "../../../lib/worktree-metadata.js";
+} from '../common.js';
+import { updateWorktreePRInfo } from '../../../lib/worktree-metadata.js';
export function createCreatePRHandler() {
return async (req: Request, res: Response): Promise => {
try {
- const { worktreePath, projectPath, commitMessage, prTitle, prBody, baseBranch, draft } = req.body as {
- worktreePath: string;
- projectPath?: string;
- commitMessage?: string;
- prTitle?: string;
- prBody?: string;
- baseBranch?: string;
- draft?: boolean;
- };
+ const { worktreePath, projectPath, commitMessage, prTitle, prBody, baseBranch, draft } =
+ req.body as {
+ worktreePath: string;
+ projectPath?: string;
+ commitMessage?: string;
+ prTitle?: string;
+ prBody?: string;
+ baseBranch?: string;
+ draft?: boolean;
+ };
if (!worktreePath) {
res.status(400).json({
success: false,
- error: "worktreePath required",
+ error: 'worktreePath required',
});
return;
}
@@ -39,23 +40,23 @@ export function createCreatePRHandler() {
const effectiveProjectPath = projectPath || worktreePath;
// Get current branch name
- const { stdout: branchOutput } = await execAsync(
- "git rev-parse --abbrev-ref HEAD",
- { cwd: worktreePath, env: execEnv }
- );
+ const { stdout: branchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', {
+ cwd: worktreePath,
+ env: execEnv,
+ });
const branchName = branchOutput.trim();
// Validate branch name for security
if (!isValidBranchName(branchName)) {
res.status(400).json({
success: false,
- error: "Invalid branch name contains unsafe characters",
+ error: 'Invalid branch name contains unsafe characters',
});
return;
}
// Check for uncommitted changes
- const { stdout: status } = await execAsync("git status --porcelain", {
+ const { stdout: status } = await execAsync('git status --porcelain', {
cwd: worktreePath,
env: execEnv,
});
@@ -67,7 +68,7 @@ export function createCreatePRHandler() {
const message = commitMessage || `Changes from ${branchName}`;
// Stage all changes
- await execAsync("git add -A", { cwd: worktreePath, env: execEnv });
+ await execAsync('git add -A', { cwd: worktreePath, env: execEnv });
// Create commit
await execAsync(`git commit -m "${message.replace(/"/g, '\\"')}"`, {
@@ -76,7 +77,7 @@ export function createCreatePRHandler() {
});
// Get commit hash
- const { stdout: hashOutput } = await execAsync("git rev-parse HEAD", {
+ const { stdout: hashOutput } = await execAsync('git rev-parse HEAD', {
cwd: worktreePath,
env: execEnv,
});
@@ -100,8 +101,8 @@ export function createCreatePRHandler() {
} catch (error2: unknown) {
// Capture push error for reporting
const err = error2 as { stderr?: string; message?: string };
- pushError = err.stderr || err.message || "Push failed";
- console.error("[CreatePR] Push failed:", pushError);
+ pushError = err.stderr || err.message || 'Push failed';
+ console.error('[CreatePR] Push failed:', pushError);
}
}
@@ -115,10 +116,10 @@ export function createCreatePRHandler() {
}
// Create PR using gh CLI or provide browser fallback
- const base = baseBranch || "main";
+ const base = baseBranch || 'main';
const title = prTitle || branchName;
const body = prBody || `Changes from branch ${branchName}`;
- const draftFlag = draft ? "--draft" : "";
+ const draftFlag = draft ? '--draft' : '';
let prUrl: string | null = null;
let prError: string | null = null;
@@ -131,7 +132,7 @@ export function createCreatePRHandler() {
let upstreamRepo: string | null = null;
let originOwner: string | null = null;
try {
- const { stdout: remotes } = await execAsync("git remote -v", {
+ const { stdout: remotes } = await execAsync('git remote -v', {
cwd: worktreePath,
env: execEnv,
});
@@ -150,15 +151,17 @@ export function createCreatePRHandler() {
}
if (!match) {
// Try HTTPS format: https://github.com/owner/repo.git
- match = line.match(/^(\w+)\s+https?:\/\/[^/]+\/([^/]+)\/([^\s]+?)(?:\.git)?\s+\(fetch\)/);
+ match = line.match(
+ /^(\w+)\s+https?:\/\/[^/]+\/([^/]+)\/([^\s]+?)(?:\.git)?\s+\(fetch\)/
+ );
}
if (match) {
const [, remoteName, owner, repo] = match;
- if (remoteName === "upstream") {
+ if (remoteName === 'upstream') {
upstreamRepo = `${owner}/${repo}`;
repoUrl = `https://github.com/${owner}/${repo}`;
- } else if (remoteName === "origin") {
+ } else if (remoteName === 'origin') {
originOwner = owner;
if (!repoUrl) {
repoUrl = `https://github.com/${owner}/${repo}`;
@@ -173,7 +176,7 @@ export function createCreatePRHandler() {
// Fallback: Try to get repo URL from git config if remote parsing failed
if (!repoUrl) {
try {
- const { stdout: originUrl } = await execAsync("git config --get remote.origin.url", {
+ const { stdout: originUrl } = await execAsync('git config --get remote.origin.url', {
cwd: worktreePath,
env: execEnv,
});
@@ -217,9 +220,11 @@ export function createCreatePRHandler() {
// This is more reliable than gh pr view as it explicitly searches by branch name
// For forks, we need to use owner:branch format for the head parameter
const headRef = upstreamRepo && originOwner ? `${originOwner}:${branchName}` : branchName;
- const repoArg = upstreamRepo ? ` --repo "${upstreamRepo}"` : "";
+ const repoArg = upstreamRepo ? ` --repo "${upstreamRepo}"` : '';
- console.log(`[CreatePR] Checking for existing PR for branch: ${branchName} (headRef: ${headRef})`);
+ console.log(
+ `[CreatePR] Checking for existing PR for branch: ${branchName} (headRef: ${headRef})`
+ );
try {
const listCmd = `gh pr list${repoArg} --head "${headRef}" --json number,title,url,state --limit 1`;
console.log(`[CreatePR] Running: ${listCmd}`);
@@ -234,7 +239,9 @@ export function createCreatePRHandler() {
if (Array.isArray(existingPrs) && existingPrs.length > 0) {
const existingPr = existingPrs[0];
// PR already exists - use it and store metadata
- console.log(`[CreatePR] PR already exists for branch ${branchName}: PR #${existingPr.number}`);
+ console.log(
+ `[CreatePR] PR already exists for branch ${branchName}: PR #${existingPr.number}`
+ );
prUrl = existingPr.url;
prNumber = existingPr.number;
prAlreadyExisted = true;
@@ -244,10 +251,12 @@ export function createCreatePRHandler() {
number: existingPr.number,
url: existingPr.url,
title: existingPr.title || title,
- state: existingPr.state || "open",
+ state: existingPr.state || 'open',
createdAt: new Date().toISOString(),
});
- console.log(`[CreatePR] Stored existing PR info for branch ${branchName}: PR #${existingPr.number}`);
+ console.log(
+ `[CreatePR] Stored existing PR info for branch ${branchName}: PR #${existingPr.number}`
+ );
} else {
console.log(`[CreatePR] No existing PR found for branch ${branchName}`);
}
@@ -293,23 +302,25 @@ export function createCreatePRHandler() {
number: prNumber,
url: prUrl,
title,
- state: draft ? "draft" : "open",
+ state: draft ? 'draft' : 'open',
createdAt: new Date().toISOString(),
});
- console.log(`[CreatePR] Stored PR info for branch ${branchName}: PR #${prNumber}`);
+ console.log(
+ `[CreatePR] Stored PR info for branch ${branchName}: PR #${prNumber}`
+ );
} catch (metadataError) {
- console.error("[CreatePR] Failed to store PR metadata:", metadataError);
+ console.error('[CreatePR] Failed to store PR metadata:', metadataError);
}
}
}
} catch (ghError: unknown) {
// gh CLI failed - check if it's "already exists" error and try to fetch the PR
const err = ghError as { stderr?: string; message?: string };
- const errorMessage = err.stderr || err.message || "PR creation failed";
+ const errorMessage = err.stderr || err.message || 'PR creation failed';
console.log(`[CreatePR] gh pr create failed: ${errorMessage}`);
// If error indicates PR already exists, try to fetch it
- if (errorMessage.toLowerCase().includes("already exists")) {
+ if (errorMessage.toLowerCase().includes('already exists')) {
console.log(`[CreatePR] PR already exists error - trying to fetch existing PR`);
try {
const { stdout: viewOutput } = await execAsync(
@@ -326,13 +337,13 @@ export function createCreatePRHandler() {
number: existingPr.number,
url: existingPr.url,
title: existingPr.title || title,
- state: existingPr.state || "open",
+ state: existingPr.state || 'open',
createdAt: new Date().toISOString(),
});
console.log(`[CreatePR] Fetched and stored existing PR: #${existingPr.number}`);
}
} catch (viewError) {
- console.error("[CreatePR] Failed to fetch existing PR:", viewError);
+ console.error('[CreatePR] Failed to fetch existing PR:', viewError);
prError = errorMessage;
}
} else {
@@ -341,7 +352,7 @@ export function createCreatePRHandler() {
}
}
} else {
- prError = "gh_cli_not_available";
+ prError = 'gh_cli_not_available';
}
// Return result with browser fallback URL
@@ -362,7 +373,7 @@ export function createCreatePRHandler() {
},
});
} catch (error) {
- logError(error, "Create PR failed");
+ logError(error, 'Create PR failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/worktree/routes/delete.ts b/apps/server/src/routes/worktree/routes/delete.ts
index 419b5418..93857f78 100644
--- a/apps/server/src/routes/worktree/routes/delete.ts
+++ b/apps/server/src/routes/worktree/routes/delete.ts
@@ -2,11 +2,11 @@
* POST /delete endpoint - Delete a git worktree
*/
-import type { Request, Response } from "express";
-import { exec } from "child_process";
-import { promisify } from "util";
-import { isGitRepo } from "@automaker/git-utils";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import { exec } from 'child_process';
+import { promisify } from 'util';
+import { isGitRepo } from '@automaker/git-utils';
+import { getErrorMessage, logError } from '../common.js';
const execAsync = promisify(exec);
@@ -22,7 +22,7 @@ export function createDeleteHandler() {
if (!projectPath || !worktreePath) {
res.status(400).json({
success: false,
- error: "projectPath and worktreePath required",
+ error: 'projectPath and worktreePath required',
});
return;
}
@@ -30,7 +30,7 @@ export function createDeleteHandler() {
if (!(await isGitRepo(projectPath))) {
res.status(400).json({
success: false,
- error: "Not a git repository",
+ error: 'Not a git repository',
});
return;
}
@@ -38,7 +38,7 @@ export function createDeleteHandler() {
// Get branch name before removing worktree
let branchName: string | null = null;
try {
- const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", {
+ const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', {
cwd: worktreePath,
});
branchName = stdout.trim();
@@ -53,11 +53,11 @@ export function createDeleteHandler() {
});
} catch (error) {
// Try with prune if remove fails
- await execAsync("git worktree prune", { cwd: projectPath });
+ await execAsync('git worktree prune', { cwd: projectPath });
}
// Optionally delete the branch
- if (deleteBranch && branchName && branchName !== "main" && branchName !== "master") {
+ if (deleteBranch && branchName && branchName !== 'main' && branchName !== 'master') {
try {
await execAsync(`git branch -D ${branchName}`, { cwd: projectPath });
} catch {
@@ -73,7 +73,7 @@ export function createDeleteHandler() {
},
});
} catch (error) {
- logError(error, "Delete worktree failed");
+ logError(error, 'Delete worktree failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/worktree/routes/list-branches.ts b/apps/server/src/routes/worktree/routes/list-branches.ts
index 0b07eb17..5fab4aff 100644
--- a/apps/server/src/routes/worktree/routes/list-branches.ts
+++ b/apps/server/src/routes/worktree/routes/list-branches.ts
@@ -2,10 +2,10 @@
* POST /list-branches endpoint - List all local branches
*/
-import type { Request, Response } from "express";
-import { exec } from "child_process";
-import { promisify } from "util";
-import { getErrorMessage, logWorktreeError } from "../common.js";
+import type { Request, Response } from 'express';
+import { exec } from 'child_process';
+import { promisify } from 'util';
+import { getErrorMessage, logWorktreeError } from '../common.js';
const execAsync = promisify(exec);
@@ -25,33 +25,31 @@ export function createListBranchesHandler() {
if (!worktreePath) {
res.status(400).json({
success: false,
- error: "worktreePath required",
+ error: 'worktreePath required',
});
return;
}
// Get current branch
- const { stdout: currentBranchOutput } = await execAsync(
- "git rev-parse --abbrev-ref HEAD",
- { cwd: worktreePath }
- );
+ const { stdout: currentBranchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', {
+ cwd: worktreePath,
+ });
const currentBranch = currentBranchOutput.trim();
// List all local branches
// Use double quotes around the format string for cross-platform compatibility
// Single quotes are preserved literally on Windows; double quotes work on both
- const { stdout: branchesOutput } = await execAsync(
- 'git branch --format="%(refname:short)"',
- { cwd: worktreePath }
- );
+ const { stdout: branchesOutput } = await execAsync('git branch --format="%(refname:short)"', {
+ cwd: worktreePath,
+ });
const branches: BranchInfo[] = branchesOutput
.trim()
- .split("\n")
+ .split('\n')
.filter((b) => b.trim())
.map((name) => {
// Remove any surrounding quotes (Windows git may preserve them)
- const cleanName = name.trim().replace(/^['"]|['"]$/g, "");
+ const cleanName = name.trim().replace(/^['"]|['"]$/g, '');
return {
name: cleanName,
isCurrent: cleanName === currentBranch,
@@ -93,7 +91,7 @@ export function createListBranchesHandler() {
});
} catch (error) {
const worktreePath = req.body?.worktreePath;
- logWorktreeError(error, "List branches failed", worktreePath);
+ logWorktreeError(error, 'List branches failed', worktreePath);
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/worktree/routes/list-dev-servers.ts b/apps/server/src/routes/worktree/routes/list-dev-servers.ts
index ff5c527a..c1093ea5 100644
--- a/apps/server/src/routes/worktree/routes/list-dev-servers.ts
+++ b/apps/server/src/routes/worktree/routes/list-dev-servers.ts
@@ -5,9 +5,9 @@
* including their ports and URLs.
*/
-import type { Request, Response } from "express";
-import { getDevServerService } from "../../../services/dev-server-service.js";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import { getDevServerService } from '../../../services/dev-server-service.js';
+import { getErrorMessage, logError } from '../common.js';
export function createListDevServersHandler() {
return async (_req: Request, res: Response): Promise => {
@@ -22,7 +22,7 @@ export function createListDevServersHandler() {
},
});
} catch (error) {
- logError(error, "List dev servers failed");
+ logError(error, 'List dev servers failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/worktree/routes/merge.ts b/apps/server/src/routes/worktree/routes/merge.ts
index f9499d85..40ac8dd4 100644
--- a/apps/server/src/routes/worktree/routes/merge.ts
+++ b/apps/server/src/routes/worktree/routes/merge.ts
@@ -2,11 +2,11 @@
* POST /merge endpoint - Merge feature (merge worktree branch into main)
*/
-import type { Request, Response } from "express";
-import { exec } from "child_process";
-import { promisify } from "util";
-import path from "path";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import { exec } from 'child_process';
+import { promisify } from 'util';
+import path from 'path';
+import { getErrorMessage, logError } from '../common.js';
const execAsync = promisify(exec);
@@ -20,42 +20,34 @@ export function createMergeHandler() {
};
if (!projectPath || !featureId) {
- res
- .status(400)
- .json({
- success: false,
- error: "projectPath and featureId required",
- });
+ res.status(400).json({
+ success: false,
+ error: 'projectPath and featureId required',
+ });
return;
}
const branchName = `feature/${featureId}`;
// Git worktrees are stored in project directory
- const worktreePath = path.join(projectPath, ".worktrees", featureId);
+ const worktreePath = path.join(projectPath, '.worktrees', featureId);
// Get current branch
- const { stdout: currentBranch } = await execAsync(
- "git rev-parse --abbrev-ref HEAD",
- { cwd: projectPath }
- );
+ const { stdout: currentBranch } = await execAsync('git rev-parse --abbrev-ref HEAD', {
+ cwd: projectPath,
+ });
// Merge the feature branch
const mergeCmd = options?.squash
? `git merge --squash ${branchName}`
- : `git merge ${branchName} -m "${
- options?.message || `Merge ${branchName}`
- }"`;
+ : `git merge ${branchName} -m "${options?.message || `Merge ${branchName}`}"`;
await execAsync(mergeCmd, { cwd: projectPath });
// If squash merge, need to commit
if (options?.squash) {
- await execAsync(
- `git commit -m "${
- options?.message || `Merge ${branchName} (squash)`
- }"`,
- { cwd: projectPath }
- );
+ await execAsync(`git commit -m "${options?.message || `Merge ${branchName} (squash)`}"`, {
+ cwd: projectPath,
+ });
}
// Clean up worktree and branch
@@ -70,7 +62,7 @@ export function createMergeHandler() {
res.json({ success: true, mergedBranch: branchName });
} catch (error) {
- logError(error, "Merge worktree failed");
+ logError(error, 'Merge worktree failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/worktree/routes/migrate.ts b/apps/server/src/routes/worktree/routes/migrate.ts
index a5287a12..7165b176 100644
--- a/apps/server/src/routes/worktree/routes/migrate.ts
+++ b/apps/server/src/routes/worktree/routes/migrate.ts
@@ -5,8 +5,8 @@
* any migration since .automaker is now stored in the project directory.
*/
-import type { Request, Response } from "express";
-import { getAutomakerDir } from "@automaker/platform";
+import type { Request, Response } from 'express';
+import { getAutomakerDir } from '@automaker/platform';
export function createMigrateHandler() {
return async (req: Request, res: Response): Promise => {
@@ -15,7 +15,7 @@ export function createMigrateHandler() {
if (!projectPath) {
res.status(400).json({
success: false,
- error: "projectPath is required",
+ error: 'projectPath is required',
});
return;
}
@@ -25,7 +25,7 @@ export function createMigrateHandler() {
res.json({
success: true,
migrated: false,
- message: "No migration needed - .automaker is stored in project directory",
+ message: 'No migration needed - .automaker is stored in project directory',
path: automakerDir,
});
};
diff --git a/apps/server/src/routes/worktree/routes/open-in-editor.ts b/apps/server/src/routes/worktree/routes/open-in-editor.ts
index 04f9815f..40e71b00 100644
--- a/apps/server/src/routes/worktree/routes/open-in-editor.ts
+++ b/apps/server/src/routes/worktree/routes/open-in-editor.ts
@@ -3,10 +3,10 @@
* GET /default-editor endpoint - Get the name of the default code editor
*/
-import type { Request, Response } from "express";
-import { exec } from "child_process";
-import { promisify } from "util";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import { exec } from 'child_process';
+import { promisify } from 'util';
+import { getErrorMessage, logError } from '../common.js';
const execAsync = promisify(exec);
@@ -29,8 +29,8 @@ async function detectDefaultEditor(): Promise {
// Try Cursor first (if user has Cursor, they probably prefer it)
try {
- await execAsync("which cursor || where cursor");
- cachedEditor = { name: "Cursor", command: "cursor" };
+ await execAsync('which cursor || where cursor');
+ cachedEditor = { name: 'Cursor', command: 'cursor' };
return cachedEditor;
} catch {
// Cursor not found
@@ -38,8 +38,8 @@ async function detectDefaultEditor(): Promise {
// Try VS Code
try {
- await execAsync("which code || where code");
- cachedEditor = { name: "VS Code", command: "code" };
+ await execAsync('which code || where code');
+ cachedEditor = { name: 'VS Code', command: 'code' };
return cachedEditor;
} catch {
// VS Code not found
@@ -47,8 +47,8 @@ async function detectDefaultEditor(): Promise {
// Try Zed
try {
- await execAsync("which zed || where zed");
- cachedEditor = { name: "Zed", command: "zed" };
+ await execAsync('which zed || where zed');
+ cachedEditor = { name: 'Zed', command: 'zed' };
return cachedEditor;
} catch {
// Zed not found
@@ -56,8 +56,8 @@ async function detectDefaultEditor(): Promise {
// Try Sublime Text
try {
- await execAsync("which subl || where subl");
- cachedEditor = { name: "Sublime Text", command: "subl" };
+ await execAsync('which subl || where subl');
+ cachedEditor = { name: 'Sublime Text', command: 'subl' };
return cachedEditor;
} catch {
// Sublime not found
@@ -65,12 +65,12 @@ async function detectDefaultEditor(): Promise {
// Fallback to file manager
const platform = process.platform;
- if (platform === "darwin") {
- cachedEditor = { name: "Finder", command: "open" };
- } else if (platform === "win32") {
- cachedEditor = { name: "Explorer", command: "explorer" };
+ if (platform === 'darwin') {
+ cachedEditor = { name: 'Finder', command: 'open' };
+ } else if (platform === 'win32') {
+ cachedEditor = { name: 'Explorer', command: 'explorer' };
} else {
- cachedEditor = { name: "File Manager", command: "xdg-open" };
+ cachedEditor = { name: 'File Manager', command: 'xdg-open' };
}
return cachedEditor;
}
@@ -87,7 +87,7 @@ export function createGetDefaultEditorHandler() {
},
});
} catch (error) {
- logError(error, "Get default editor failed");
+ logError(error, 'Get default editor failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
@@ -103,7 +103,7 @@ export function createOpenInEditorHandler() {
if (!worktreePath) {
res.status(400).json({
success: false,
- error: "worktreePath required",
+ error: 'worktreePath required',
});
return;
}
@@ -125,15 +125,15 @@ export function createOpenInEditorHandler() {
let openCommand: string;
let fallbackName: string;
- if (platform === "darwin") {
+ if (platform === 'darwin') {
openCommand = `open "${worktreePath}"`;
- fallbackName = "Finder";
- } else if (platform === "win32") {
+ fallbackName = 'Finder';
+ } else if (platform === 'win32') {
openCommand = `explorer "${worktreePath}"`;
- fallbackName = "Explorer";
+ fallbackName = 'Explorer';
} else {
openCommand = `xdg-open "${worktreePath}"`;
- fallbackName = "File Manager";
+ fallbackName = 'File Manager';
}
await execAsync(openCommand);
@@ -146,7 +146,7 @@ export function createOpenInEditorHandler() {
});
}
} catch (error) {
- logError(error, "Open in editor failed");
+ logError(error, 'Open in editor failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/worktree/routes/pr-info.ts b/apps/server/src/routes/worktree/routes/pr-info.ts
index 779e81cb..cb64ccd9 100644
--- a/apps/server/src/routes/worktree/routes/pr-info.ts
+++ b/apps/server/src/routes/worktree/routes/pr-info.ts
@@ -2,7 +2,7 @@
* POST /pr-info endpoint - Get PR info and comments for a branch
*/
-import type { Request, Response } from "express";
+import type { Request, Response } from 'express';
import {
getErrorMessage,
logError,
@@ -10,7 +10,7 @@ import {
execEnv,
isValidBranchName,
isGhCliAvailable,
-} from "../common.js";
+} from '../common.js';
export interface PRComment {
id: number;
@@ -44,7 +44,7 @@ export function createPRInfoHandler() {
if (!worktreePath || !branchName) {
res.status(400).json({
success: false,
- error: "worktreePath and branchName required",
+ error: 'worktreePath and branchName required',
});
return;
}
@@ -53,7 +53,7 @@ export function createPRInfoHandler() {
if (!isValidBranchName(branchName)) {
res.status(400).json({
success: false,
- error: "Invalid branch name contains unsafe characters",
+ error: 'Invalid branch name contains unsafe characters',
});
return;
}
@@ -67,7 +67,7 @@ export function createPRInfoHandler() {
result: {
hasPR: false,
ghCliAvailable: false,
- error: "gh CLI not available",
+ error: 'gh CLI not available',
},
});
return;
@@ -79,7 +79,7 @@ export function createPRInfoHandler() {
let originRepo: string | null = null;
try {
- const { stdout: remotes } = await execAsync("git remote -v", {
+ const { stdout: remotes } = await execAsync('git remote -v', {
cwd: worktreePath,
env: execEnv,
});
@@ -87,21 +87,15 @@ export function createPRInfoHandler() {
const lines = remotes.split(/\r?\n/);
for (const line of lines) {
let match =
- line.match(
- /^(\w+)\s+.*[:/]([^/]+)\/([^/\s]+?)(?:\.git)?\s+\(fetch\)/
- ) ||
- line.match(
- /^(\w+)\s+git@[^:]+:([^/]+)\/([^\s]+?)(?:\.git)?\s+\(fetch\)/
- ) ||
- line.match(
- /^(\w+)\s+https?:\/\/[^/]+\/([^/]+)\/([^\s]+?)(?:\.git)?\s+\(fetch\)/
- );
+ line.match(/^(\w+)\s+.*[:/]([^/]+)\/([^/\s]+?)(?:\.git)?\s+\(fetch\)/) ||
+ line.match(/^(\w+)\s+git@[^:]+:([^/]+)\/([^\s]+?)(?:\.git)?\s+\(fetch\)/) ||
+ line.match(/^(\w+)\s+https?:\/\/[^/]+\/([^/]+)\/([^\s]+?)(?:\.git)?\s+\(fetch\)/);
if (match) {
const [, remoteName, owner, repo] = match;
- if (remoteName === "upstream") {
+ if (remoteName === 'upstream') {
upstreamRepo = `${owner}/${repo}`;
- } else if (remoteName === "origin") {
+ } else if (remoteName === 'origin') {
originOwner = owner;
originRepo = repo;
}
@@ -113,16 +107,11 @@ export function createPRInfoHandler() {
if (!originOwner || !originRepo) {
try {
- const { stdout: originUrl } = await execAsync(
- "git config --get remote.origin.url",
- {
- cwd: worktreePath,
- env: execEnv,
- }
- );
- const match = originUrl
- .trim()
- .match(/[:/]([^/]+)\/([^/\s]+?)(?:\.git)?$/);
+ const { stdout: originUrl } = await execAsync('git config --get remote.origin.url', {
+ cwd: worktreePath,
+ env: execEnv,
+ });
+ const match = originUrl.trim().match(/[:/]([^/]+)\/([^/\s]+?)(?:\.git)?$/);
if (match) {
if (!originOwner) {
originOwner = match[1];
@@ -137,21 +126,18 @@ export function createPRInfoHandler() {
}
const targetRepo =
- upstreamRepo || (originOwner && originRepo
- ? `${originOwner}/${originRepo}`
- : null);
- const repoFlag = targetRepo ? ` --repo "${targetRepo}"` : "";
- const headRef =
- upstreamRepo && originOwner ? `${originOwner}:${branchName}` : branchName;
+ upstreamRepo || (originOwner && originRepo ? `${originOwner}/${originRepo}` : null);
+ const repoFlag = targetRepo ? ` --repo "${targetRepo}"` : '';
+ const headRef = upstreamRepo && originOwner ? `${originOwner}:${branchName}` : branchName;
// Get PR info for the branch using gh CLI
try {
// First, find the PR associated with this branch
const listCmd = `gh pr list${repoFlag} --head "${headRef}" --json number,title,url,state,author,body --limit 1`;
- const { stdout: prListOutput } = await execAsync(
- listCmd,
- { cwd: worktreePath, env: execEnv }
- );
+ const { stdout: prListOutput } = await execAsync(listCmd, {
+ cwd: worktreePath,
+ env: execEnv,
+ });
const prList = JSON.parse(prListOutput);
@@ -173,25 +159,22 @@ export function createPRInfoHandler() {
let comments: PRComment[] = [];
try {
const viewCmd = `gh pr view ${prNumber}${repoFlag} --json comments`;
- const { stdout: commentsOutput } = await execAsync(
- viewCmd,
- { cwd: worktreePath, env: execEnv }
- );
+ const { stdout: commentsOutput } = await execAsync(viewCmd, {
+ cwd: worktreePath,
+ env: execEnv,
+ });
const commentsData = JSON.parse(commentsOutput);
- comments = (commentsData.comments || []).map((c: {
- id: number;
- author: { login: string };
- body: string;
- createdAt: string;
- }) => ({
- id: c.id,
- author: c.author?.login || "unknown",
- body: c.body,
- createdAt: c.createdAt,
- isReviewComment: false,
- }));
+ comments = (commentsData.comments || []).map(
+ (c: { id: number; author: { login: string }; body: string; createdAt: string }) => ({
+ id: c.id,
+ author: c.author?.login || 'unknown',
+ body: c.body,
+ createdAt: c.createdAt,
+ isReviewComment: false,
+ })
+ );
} catch (error) {
- console.warn("[PRInfo] Failed to fetch PR comments:", error);
+ console.warn('[PRInfo] Failed to fetch PR comments:', error);
}
// Get review comments (inline code comments)
@@ -201,33 +184,35 @@ export function createPRInfoHandler() {
try {
const reviewsEndpoint = `repos/${targetRepo}/pulls/${prNumber}/comments`;
const reviewsCmd = `gh api ${reviewsEndpoint}`;
- const { stdout: reviewsOutput } = await execAsync(
- reviewsCmd,
- { cwd: worktreePath, env: execEnv }
- );
+ const { stdout: reviewsOutput } = await execAsync(reviewsCmd, {
+ cwd: worktreePath,
+ env: execEnv,
+ });
const reviewsData = JSON.parse(reviewsOutput);
- reviewComments = reviewsData.map((c: {
- id: number;
- user: { login: string };
- body: string;
- path: string;
- line?: number;
- original_line?: number;
- created_at: string;
- }) => ({
- id: c.id,
- author: c.user?.login || "unknown",
- body: c.body,
- path: c.path,
- line: c.line || c.original_line,
- createdAt: c.created_at,
- isReviewComment: true,
- }));
+ reviewComments = reviewsData.map(
+ (c: {
+ id: number;
+ user: { login: string };
+ body: string;
+ path: string;
+ line?: number;
+ original_line?: number;
+ created_at: string;
+ }) => ({
+ id: c.id,
+ author: c.user?.login || 'unknown',
+ body: c.body,
+ path: c.path,
+ line: c.line || c.original_line,
+ createdAt: c.created_at,
+ isReviewComment: true,
+ })
+ );
} catch (error) {
- console.warn("[PRInfo] Failed to fetch review comments:", error);
+ console.warn('[PRInfo] Failed to fetch review comments:', error);
}
} else {
- console.warn("[PRInfo] Cannot fetch review comments: repository info not available");
+ console.warn('[PRInfo] Cannot fetch review comments: repository info not available');
}
const prInfo: PRInfo = {
@@ -235,8 +220,8 @@ export function createPRInfoHandler() {
title: pr.title,
url: pr.url,
state: pr.state,
- author: pr.author?.login || "unknown",
- body: pr.body || "",
+ author: pr.author?.login || 'unknown',
+ body: pr.body || '',
comments,
reviewComments,
};
@@ -251,7 +236,7 @@ export function createPRInfoHandler() {
});
} catch (error) {
// gh CLI failed - might not be authenticated or no remote
- logError(error, "Failed to get PR info");
+ logError(error, 'Failed to get PR info');
res.json({
success: true,
result: {
@@ -262,7 +247,7 @@ export function createPRInfoHandler() {
});
}
} catch (error) {
- logError(error, "PR info handler failed");
+ logError(error, 'PR info handler failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/worktree/routes/pull.ts b/apps/server/src/routes/worktree/routes/pull.ts
index 119192d0..4384e207 100644
--- a/apps/server/src/routes/worktree/routes/pull.ts
+++ b/apps/server/src/routes/worktree/routes/pull.ts
@@ -2,10 +2,10 @@
* POST /pull endpoint - Pull latest changes for a worktree/branch
*/
-import type { Request, Response } from "express";
-import { exec } from "child_process";
-import { promisify } from "util";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import { exec } from 'child_process';
+import { promisify } from 'util';
+import { getErrorMessage, logError } from '../common.js';
const execAsync = promisify(exec);
@@ -19,23 +19,22 @@ export function createPullHandler() {
if (!worktreePath) {
res.status(400).json({
success: false,
- error: "worktreePath required",
+ error: 'worktreePath required',
});
return;
}
// Get current branch name
- const { stdout: branchOutput } = await execAsync(
- "git rev-parse --abbrev-ref HEAD",
- { cwd: worktreePath }
- );
+ const { stdout: branchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', {
+ cwd: worktreePath,
+ });
const branchName = branchOutput.trim();
// Fetch latest from remote
- await execAsync("git fetch origin", { cwd: worktreePath });
+ await execAsync('git fetch origin', { cwd: worktreePath });
// Check if there are local changes that would be overwritten
- const { stdout: status } = await execAsync("git status --porcelain", {
+ const { stdout: status } = await execAsync('git status --porcelain', {
cwd: worktreePath,
});
const hasLocalChanges = status.trim().length > 0;
@@ -43,35 +42,34 @@ export function createPullHandler() {
if (hasLocalChanges) {
res.status(400).json({
success: false,
- error: "You have local changes. Please commit them before pulling.",
+ error: 'You have local changes. Please commit them before pulling.',
});
return;
}
// Pull latest changes
try {
- const { stdout: pullOutput } = await execAsync(
- `git pull origin ${branchName}`,
- { cwd: worktreePath }
- );
+ const { stdout: pullOutput } = await execAsync(`git pull origin ${branchName}`, {
+ cwd: worktreePath,
+ });
// Check if we pulled any changes
- const alreadyUpToDate = pullOutput.includes("Already up to date");
+ const alreadyUpToDate = pullOutput.includes('Already up to date');
res.json({
success: true,
result: {
branch: branchName,
pulled: !alreadyUpToDate,
- message: alreadyUpToDate ? "Already up to date" : "Pulled latest changes",
+ message: alreadyUpToDate ? 'Already up to date' : 'Pulled latest changes',
},
});
} catch (pullError: unknown) {
const err = pullError as { stderr?: string; message?: string };
- const errorMsg = err.stderr || err.message || "Pull failed";
+ const errorMsg = err.stderr || err.message || 'Pull failed';
// Check for common errors
- if (errorMsg.includes("no tracking information")) {
+ if (errorMsg.includes('no tracking information')) {
res.status(400).json({
success: false,
error: `Branch '${branchName}' has no upstream branch. Push it first or set upstream with: git branch --set-upstream-to=origin/${branchName}`,
@@ -85,7 +83,7 @@ export function createPullHandler() {
});
}
} catch (error) {
- logError(error, "Pull failed");
+ logError(error, 'Pull failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/worktree/routes/push.ts b/apps/server/src/routes/worktree/routes/push.ts
index d9447a2b..c0337f43 100644
--- a/apps/server/src/routes/worktree/routes/push.ts
+++ b/apps/server/src/routes/worktree/routes/push.ts
@@ -2,10 +2,10 @@
* POST /push endpoint - Push a worktree branch to remote
*/
-import type { Request, Response } from "express";
-import { exec } from "child_process";
-import { promisify } from "util";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import { exec } from 'child_process';
+import { promisify } from 'util';
+import { getErrorMessage, logError } from '../common.js';
const execAsync = promisify(exec);
@@ -20,20 +20,19 @@ export function createPushHandler() {
if (!worktreePath) {
res.status(400).json({
success: false,
- error: "worktreePath required",
+ error: 'worktreePath required',
});
return;
}
// Get branch name
- const { stdout: branchOutput } = await execAsync(
- "git rev-parse --abbrev-ref HEAD",
- { cwd: worktreePath }
- );
+ const { stdout: branchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', {
+ cwd: worktreePath,
+ });
const branchName = branchOutput.trim();
// Push the branch
- const forceFlag = force ? "--force" : "";
+ const forceFlag = force ? '--force' : '';
try {
await execAsync(`git push -u origin ${branchName} ${forceFlag}`, {
cwd: worktreePath,
@@ -54,7 +53,7 @@ export function createPushHandler() {
},
});
} catch (error) {
- logError(error, "Push worktree failed");
+ logError(error, 'Push worktree failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/worktree/routes/start-dev.ts b/apps/server/src/routes/worktree/routes/start-dev.ts
index fcd0cec7..13b93f9b 100644
--- a/apps/server/src/routes/worktree/routes/start-dev.ts
+++ b/apps/server/src/routes/worktree/routes/start-dev.ts
@@ -6,9 +6,9 @@
* affecting the main dev server.
*/
-import type { Request, Response } from "express";
-import { getDevServerService } from "../../../services/dev-server-service.js";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import { getDevServerService } from '../../../services/dev-server-service.js';
+import { getErrorMessage, logError } from '../common.js';
export function createStartDevHandler() {
return async (req: Request, res: Response): Promise => {
@@ -21,7 +21,7 @@ export function createStartDevHandler() {
if (!projectPath) {
res.status(400).json({
success: false,
- error: "projectPath is required",
+ error: 'projectPath is required',
});
return;
}
@@ -29,7 +29,7 @@ export function createStartDevHandler() {
if (!worktreePath) {
res.status(400).json({
success: false,
- error: "worktreePath is required",
+ error: 'worktreePath is required',
});
return;
}
@@ -50,11 +50,11 @@ export function createStartDevHandler() {
} else {
res.status(400).json({
success: false,
- error: result.error || "Failed to start dev server",
+ error: result.error || 'Failed to start dev server',
});
}
} catch (error) {
- logError(error, "Start dev server failed");
+ logError(error, 'Start dev server failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/worktree/routes/stop-dev.ts b/apps/server/src/routes/worktree/routes/stop-dev.ts
index 2c22b006..1dbc7340 100644
--- a/apps/server/src/routes/worktree/routes/stop-dev.ts
+++ b/apps/server/src/routes/worktree/routes/stop-dev.ts
@@ -5,9 +5,9 @@
* freeing up the ports for reuse.
*/
-import type { Request, Response } from "express";
-import { getDevServerService } from "../../../services/dev-server-service.js";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import { getDevServerService } from '../../../services/dev-server-service.js';
+import { getErrorMessage, logError } from '../common.js';
export function createStopDevHandler() {
return async (req: Request, res: Response): Promise => {
@@ -19,7 +19,7 @@ export function createStopDevHandler() {
if (!worktreePath) {
res.status(400).json({
success: false,
- error: "worktreePath is required",
+ error: 'worktreePath is required',
});
return;
}
@@ -38,11 +38,11 @@ export function createStopDevHandler() {
} else {
res.status(400).json({
success: false,
- error: result.error || "Failed to stop dev server",
+ error: result.error || 'Failed to stop dev server',
});
}
} catch (error) {
- logError(error, "Stop dev server failed");
+ logError(error, 'Stop dev server failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/routes/worktree/routes/switch-branch.ts b/apps/server/src/routes/worktree/routes/switch-branch.ts
index c3c4cdb4..3df7a3f2 100644
--- a/apps/server/src/routes/worktree/routes/switch-branch.ts
+++ b/apps/server/src/routes/worktree/routes/switch-branch.ts
@@ -6,10 +6,10 @@
* the user should commit first.
*/
-import type { Request, Response } from "express";
-import { exec } from "child_process";
-import { promisify } from "util";
-import { getErrorMessage, logError } from "../common.js";
+import type { Request, Response } from 'express';
+import { exec } from 'child_process';
+import { promisify } from 'util';
+import { getErrorMessage, logError } from '../common.js';
const execAsync = promisify(exec);
@@ -19,13 +19,16 @@ const execAsync = promisify(exec);
*/
async function hasUncommittedChanges(cwd: string): Promise {
try {
- const { stdout } = await execAsync("git status --porcelain", { cwd });
- const lines = stdout.trim().split("\n").filter((line) => {
- if (!line.trim()) return false;
- // Exclude .worktrees/ directory (created by automaker)
- if (line.includes(".worktrees/") || line.endsWith(".worktrees")) return false;
- return true;
- });
+ const { stdout } = await execAsync('git status --porcelain', { cwd });
+ const lines = stdout
+ .trim()
+ .split('\n')
+ .filter((line) => {
+ if (!line.trim()) return false;
+ // Exclude .worktrees/ directory (created by automaker)
+ if (line.includes('.worktrees/') || line.endsWith('.worktrees')) return false;
+ return true;
+ });
return lines.length > 0;
} catch {
return false;
@@ -38,18 +41,21 @@ async function hasUncommittedChanges(cwd: string): Promise {
*/
async function getChangesSummary(cwd: string): Promise {
try {
- const { stdout } = await execAsync("git status --short", { cwd });
- const lines = stdout.trim().split("\n").filter((line) => {
- if (!line.trim()) return false;
- // Exclude .worktrees/ directory
- if (line.includes(".worktrees/") || line.endsWith(".worktrees")) return false;
- return true;
- });
- if (lines.length === 0) return "";
- if (lines.length <= 5) return lines.join(", ");
- return `${lines.slice(0, 5).join(", ")} and ${lines.length - 5} more files`;
+ const { stdout } = await execAsync('git status --short', { cwd });
+ const lines = stdout
+ .trim()
+ .split('\n')
+ .filter((line) => {
+ if (!line.trim()) return false;
+ // Exclude .worktrees/ directory
+ if (line.includes('.worktrees/') || line.endsWith('.worktrees')) return false;
+ return true;
+ });
+ if (lines.length === 0) return '';
+ if (lines.length <= 5) return lines.join(', ');
+ return `${lines.slice(0, 5).join(', ')} and ${lines.length - 5} more files`;
} catch {
- return "unknown changes";
+ return 'unknown changes';
}
}
@@ -64,7 +70,7 @@ export function createSwitchBranchHandler() {
if (!worktreePath) {
res.status(400).json({
success: false,
- error: "worktreePath required",
+ error: 'worktreePath required',
});
return;
}
@@ -72,16 +78,15 @@ export function createSwitchBranchHandler() {
if (!branchName) {
res.status(400).json({
success: false,
- error: "branchName required",
+ error: 'branchName required',
});
return;
}
// Get current branch
- const { stdout: currentBranchOutput } = await execAsync(
- "git rev-parse --abbrev-ref HEAD",
- { cwd: worktreePath }
- );
+ const { stdout: currentBranchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', {
+ cwd: worktreePath,
+ });
const previousBranch = currentBranchOutput.trim();
if (previousBranch === branchName) {
@@ -115,7 +120,7 @@ export function createSwitchBranchHandler() {
res.status(400).json({
success: false,
error: `Cannot switch branches: you have uncommitted changes (${summary}). Please commit your changes first.`,
- code: "UNCOMMITTED_CHANGES",
+ code: 'UNCOMMITTED_CHANGES',
});
return;
}
@@ -132,7 +137,7 @@ export function createSwitchBranchHandler() {
},
});
} catch (error) {
- logError(error, "Switch branch failed");
+ logError(error, 'Switch branch failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
diff --git a/apps/server/src/services/agent-service.ts b/apps/server/src/services/agent-service.ts
index 996a4a38..93df5566 100644
--- a/apps/server/src/services/agent-service.ts
+++ b/apps/server/src/services/agent-service.ts
@@ -7,7 +7,12 @@ import path from 'path';
import * as secureFs from '../lib/secure-fs.js';
import type { EventEmitter } from '../lib/events.js';
import type { ExecuteOptions } from '@automaker/types';
-import { readImageAsBase64, buildPromptWithImages, isAbortError } from '@automaker/utils';
+import {
+ readImageAsBase64,
+ buildPromptWithImages,
+ isAbortError,
+ loadContextFiles,
+} from '@automaker/utils';
import { ProviderFactory } from '../providers/provider-factory.js';
import { createChatOptions, validateWorkingDirectory } from '../lib/sdk-options.js';
import { PathNotAllowedError } from '@automaker/platform';
@@ -178,12 +183,27 @@ export class AgentService {
await this.saveSession(sessionId, session.messages);
try {
+ // Determine the effective working directory for context loading
+ const effectiveWorkDir = workingDirectory || session.workingDirectory;
+
+ // Load project context files (CLAUDE.md, CODE_QUALITY.md, etc.)
+ const { formattedPrompt: contextFilesPrompt } = await loadContextFiles({
+ projectPath: effectiveWorkDir,
+ fsModule: secureFs as Parameters[0]['fsModule'],
+ });
+
+ // Build combined system prompt with base prompt and context files
+ const baseSystemPrompt = this.getSystemPrompt();
+ const combinedSystemPrompt = contextFilesPrompt
+ ? `${contextFilesPrompt}\n\n${baseSystemPrompt}`
+ : baseSystemPrompt;
+
// Build SDK options using centralized configuration
const sdkOptions = createChatOptions({
- cwd: workingDirectory || session.workingDirectory,
+ cwd: effectiveWorkDir,
model: model,
sessionModel: session.model,
- systemPrompt: this.getSystemPrompt(),
+ systemPrompt: combinedSystemPrompt,
abortController: session.abortController!,
});
@@ -203,8 +223,8 @@ export class AgentService {
const options: ExecuteOptions = {
prompt: '', // Will be set below based on images
model: effectiveModel,
- cwd: workingDirectory || session.workingDirectory,
- systemPrompt: this.getSystemPrompt(),
+ cwd: effectiveWorkDir,
+ systemPrompt: combinedSystemPrompt,
maxTurns: maxTurns,
allowedTools: allowedTools,
abortController: session.abortController!,
diff --git a/apps/server/src/services/auto-mode-service.ts b/apps/server/src/services/auto-mode-service.ts
index da48308e..0ab3d54d 100644
--- a/apps/server/src/services/auto-mode-service.ts
+++ b/apps/server/src/services/auto-mode-service.ts
@@ -11,10 +11,15 @@
import { ProviderFactory } from '../providers/provider-factory.js';
import type { ExecuteOptions, Feature } from '@automaker/types';
-import { buildPromptWithImages, isAbortError, classifyError } from '@automaker/utils';
+import {
+ buildPromptWithImages,
+ isAbortError,
+ classifyError,
+ loadContextFiles,
+} from '@automaker/utils';
import { resolveModelString, DEFAULT_MODELS } from '@automaker/model-resolver';
import { resolveDependencies, areDependenciesSatisfied } from '@automaker/dependency-resolver';
-import { getFeatureDir, getAutomakerDir, getFeaturesDir, getContextDir } from '@automaker/platform';
+import { getFeatureDir, getAutomakerDir, getFeaturesDir } from '@automaker/platform';
import { exec } from 'child_process';
import { promisify } from 'util';
import path from 'path';
@@ -549,7 +554,10 @@ export class AutoModeService {
// Build the prompt - use continuation prompt if provided (for recovery after plan approval)
let prompt: string;
// Load project context files (CLAUDE.md, CODE_QUALITY.md, etc.) - passed as system prompt
- const contextFiles = await this.loadContextFiles(projectPath);
+ const { formattedPrompt: contextFilesPrompt } = await loadContextFiles({
+ projectPath,
+ fsModule: secureFs as Parameters[0]['fsModule'],
+ });
if (options?.continuationPrompt) {
// Continuation prompt is used when recovering from a plan approval
@@ -595,7 +603,7 @@ export class AutoModeService {
projectPath,
planningMode: feature.planningMode,
requirePlanApproval: feature.requirePlanApproval,
- systemPrompt: contextFiles || undefined,
+ systemPrompt: contextFilesPrompt || undefined,
}
);
@@ -736,7 +744,10 @@ export class AutoModeService {
}
// Load project context files (CLAUDE.md, CODE_QUALITY.md, etc.) - passed as system prompt
- const contextFiles = await this.loadContextFiles(projectPath);
+ const { formattedPrompt: contextFilesPrompt } = await loadContextFiles({
+ projectPath,
+ fsModule: secureFs as Parameters[0]['fsModule'],
+ });
// Build complete prompt with feature info, previous context, and follow-up instructions
let fullPrompt = `## Follow-up on Feature Implementation
@@ -864,7 +875,7 @@ Address the follow-up instructions above. Review the previous work and make the
projectPath,
planningMode: 'skip', // Follow-ups don't require approval
previousContent: previousContext || undefined,
- systemPrompt: contextFiles || undefined,
+ systemPrompt: contextFilesPrompt || undefined,
}
);
@@ -1044,63 +1055,6 @@ Address the follow-up instructions above. Review the previous work and make the
}
}
- /**
- * Load context files from .automaker/context/ directory
- * These are user-defined context files (CLAUDE.md, CODE_QUALITY.md, etc.)
- * that provide project-specific rules and guidelines for the agent.
- */
- private async loadContextFiles(projectPath: string): Promise {
- // Use path.resolve for cross-platform absolute path handling
- const contextDir = path.resolve(getContextDir(projectPath));
-
- try {
- // Check if directory exists first
- await secureFs.access(contextDir);
-
- const files = await secureFs.readdir(contextDir);
- // Filter for text-based context files (case-insensitive for Windows)
- const textFiles = files.filter((f) => {
- const lower = f.toLowerCase();
- return lower.endsWith('.md') || lower.endsWith('.txt');
- });
-
- if (textFiles.length === 0) return '';
-
- const contents: string[] = [];
- for (const file of textFiles) {
- // Use path.join for cross-platform path construction
- const filePath = path.join(contextDir, file);
- const content = (await secureFs.readFile(filePath, 'utf-8')) as string;
- contents.push(`## ${file}\n\n${content}`);
- }
-
- console.log(`[AutoMode] Loaded ${textFiles.length} context file(s): ${textFiles.join(', ')}`);
-
- return `# ⚠️ CRITICAL: Project Context Files - READ AND FOLLOW STRICTLY
-
-**IMPORTANT**: The following context files contain MANDATORY project-specific rules and conventions. You MUST:
-1. Read these rules carefully before taking any action
-2. Follow ALL commands exactly as shown (e.g., if the project uses \`pnpm\`, NEVER use \`npm\` or \`npx\`)
-3. Follow ALL coding conventions, commit message formats, and architectural patterns specified
-4. Reference these rules before running ANY shell commands or making commits
-
-Failure to follow these rules will result in broken builds, failed CI, and rejected commits.
-
-${contents.join('\n\n---\n\n')}
-
----
-
-**REMINDER**: Before running any command, verify you are using the correct package manager and following the conventions above.
-
----
-
-`;
- } catch {
- // Context directory doesn't exist or is empty - this is fine
- return '';
- }
- }
-
/**
* Analyze project to gather context
*/
diff --git a/apps/server/src/services/claude-usage-service.ts b/apps/server/src/services/claude-usage-service.ts
index 409437b6..946b7b23 100644
--- a/apps/server/src/services/claude-usage-service.ts
+++ b/apps/server/src/services/claude-usage-service.ts
@@ -1,7 +1,7 @@
-import { spawn } from "child_process";
-import * as os from "os";
-import * as pty from "node-pty";
-import { ClaudeUsage } from "../routes/claude/types.js";
+import { spawn } from 'child_process';
+import * as os from 'os';
+import * as pty from 'node-pty';
+import { ClaudeUsage } from '../routes/claude/types.js';
/**
* Claude Usage Service
@@ -15,21 +15,21 @@ import { ClaudeUsage } from "../routes/claude/types.js";
* - Windows: Uses node-pty for PTY
*/
export class ClaudeUsageService {
- private claudeBinary = "claude";
+ private claudeBinary = 'claude';
private timeout = 30000; // 30 second timeout
- private isWindows = os.platform() === "win32";
+ private isWindows = os.platform() === 'win32';
/**
* Check if Claude CLI is available on the system
*/
async isAvailable(): Promise {
return new Promise((resolve) => {
- const checkCmd = this.isWindows ? "where" : "which";
+ const checkCmd = this.isWindows ? 'where' : 'which';
const proc = spawn(checkCmd, [this.claudeBinary]);
- proc.on("close", (code) => {
+ proc.on('close', (code) => {
resolve(code === 0);
});
- proc.on("error", () => {
+ proc.on('error', () => {
resolve(false);
});
});
@@ -59,12 +59,12 @@ export class ClaudeUsageService {
*/
private executeClaudeUsageCommandMac(): Promise {
return new Promise((resolve, reject) => {
- let stdout = "";
- let stderr = "";
+ let stdout = '';
+ let stderr = '';
let settled = false;
// Use a simple working directory (home or tmp)
- const workingDirectory = process.env.HOME || "/tmp";
+ const workingDirectory = process.env.HOME || '/tmp';
// Use 'expect' with an inline script to run claude /usage with a PTY
// Wait for "Current session" header, then wait for full output before exiting
@@ -86,11 +86,11 @@ export class ClaudeUsageService {
expect eof
`;
- const proc = spawn("expect", ["-c", expectScript], {
+ const proc = spawn('expect', ['-c', expectScript], {
cwd: workingDirectory,
env: {
...process.env,
- TERM: "xterm-256color",
+ TERM: 'xterm-256color',
},
});
@@ -98,26 +98,30 @@ export class ClaudeUsageService {
if (!settled) {
settled = true;
proc.kill();
- reject(new Error("Command timed out"));
+ reject(new Error('Command timed out'));
}
}, this.timeout);
- proc.stdout.on("data", (data) => {
+ proc.stdout.on('data', (data) => {
stdout += data.toString();
});
- proc.stderr.on("data", (data) => {
+ proc.stderr.on('data', (data) => {
stderr += data.toString();
});
- proc.on("close", (code) => {
+ proc.on('close', (code) => {
clearTimeout(timeoutId);
if (settled) return;
settled = true;
// Check for authentication errors in output
- if (stdout.includes("token_expired") || stdout.includes("authentication_error") ||
- stderr.includes("token_expired") || stderr.includes("authentication_error")) {
+ if (
+ stdout.includes('token_expired') ||
+ stdout.includes('authentication_error') ||
+ stderr.includes('token_expired') ||
+ stderr.includes('authentication_error')
+ ) {
reject(new Error("Authentication required - please run 'claude login'"));
return;
}
@@ -128,11 +132,11 @@ export class ClaudeUsageService {
} else if (code !== 0) {
reject(new Error(stderr || `Command exited with code ${code}`));
} else {
- reject(new Error("No output from claude command"));
+ reject(new Error('No output from claude command'));
}
});
- proc.on("error", (err) => {
+ proc.on('error', (err) => {
clearTimeout(timeoutId);
if (!settled) {
settled = true;
@@ -147,20 +151,20 @@ export class ClaudeUsageService {
*/
private executeClaudeUsageCommandWindows(): Promise {
return new Promise((resolve, reject) => {
- let output = "";
+ let output = '';
let settled = false;
let hasSeenUsageData = false;
- const workingDirectory = process.env.USERPROFILE || os.homedir() || "C:\\";
+ const workingDirectory = process.env.USERPROFILE || os.homedir() || 'C:\\';
- const ptyProcess = pty.spawn("cmd.exe", ["/c", "claude", "/usage"], {
- name: "xterm-256color",
+ const ptyProcess = pty.spawn('cmd.exe', ['/c', 'claude', '/usage'], {
+ name: 'xterm-256color',
cols: 120,
rows: 30,
cwd: workingDirectory,
env: {
...process.env,
- TERM: "xterm-256color",
+ TERM: 'xterm-256color',
} as Record,
});
@@ -168,7 +172,7 @@ export class ClaudeUsageService {
if (!settled) {
settled = true;
ptyProcess.kill();
- reject(new Error("Command timed out"));
+ reject(new Error('Command timed out'));
}
}, this.timeout);
@@ -176,21 +180,21 @@ export class ClaudeUsageService {
output += data;
// Check if we've seen the usage data (look for "Current session")
- if (!hasSeenUsageData && output.includes("Current session")) {
+ if (!hasSeenUsageData && output.includes('Current session')) {
hasSeenUsageData = true;
// Wait for full output, then send escape to exit
setTimeout(() => {
if (!settled) {
- ptyProcess.write("\x1b"); // Send escape key
+ ptyProcess.write('\x1b'); // Send escape key
}
}, 2000);
}
// Fallback: if we see "Esc to cancel" but haven't seen usage data yet
- if (!hasSeenUsageData && output.includes("Esc to cancel")) {
+ if (!hasSeenUsageData && output.includes('Esc to cancel')) {
setTimeout(() => {
if (!settled) {
- ptyProcess.write("\x1b"); // Send escape key
+ ptyProcess.write('\x1b'); // Send escape key
}
}, 3000);
}
@@ -202,7 +206,7 @@ export class ClaudeUsageService {
settled = true;
// Check for authentication errors in output
- if (output.includes("token_expired") || output.includes("authentication_error")) {
+ if (output.includes('token_expired') || output.includes('authentication_error')) {
reject(new Error("Authentication required - please run 'claude login'"));
return;
}
@@ -212,7 +216,7 @@ export class ClaudeUsageService {
} else if (exitCode !== 0) {
reject(new Error(`Command exited with code ${exitCode}`));
} else {
- reject(new Error("No output from claude command"));
+ reject(new Error('No output from claude command'));
}
});
});
@@ -223,7 +227,7 @@ export class ClaudeUsageService {
*/
private stripAnsiCodes(text: string): string {
// eslint-disable-next-line no-control-regex
- return text.replace(/\x1B\[[0-9;]*[A-Za-z]/g, "");
+ return text.replace(/\x1B\[[0-9;]*[A-Za-z]/g, '');
}
/**
@@ -248,21 +252,24 @@ export class ClaudeUsageService {
*/
private parseUsageOutput(rawOutput: string): ClaudeUsage {
const output = this.stripAnsiCodes(rawOutput);
- const lines = output.split("\n").map(l => l.trim()).filter(l => l);
+ const lines = output
+ .split('\n')
+ .map((l) => l.trim())
+ .filter((l) => l);
// Parse session usage
- const sessionData = this.parseSection(lines, "Current session", "session");
+ const sessionData = this.parseSection(lines, 'Current session', 'session');
// Parse weekly usage (all models)
- const weeklyData = this.parseSection(lines, "Current week (all models)", "weekly");
+ const weeklyData = this.parseSection(lines, 'Current week (all models)', 'weekly');
// Parse Sonnet/Opus usage - try different labels
- let sonnetData = this.parseSection(lines, "Current week (Sonnet only)", "sonnet");
+ let sonnetData = this.parseSection(lines, 'Current week (Sonnet only)', 'sonnet');
if (sonnetData.percentage === 0) {
- sonnetData = this.parseSection(lines, "Current week (Sonnet)", "sonnet");
+ sonnetData = this.parseSection(lines, 'Current week (Sonnet)', 'sonnet');
}
if (sonnetData.percentage === 0) {
- sonnetData = this.parseSection(lines, "Current week (Opus)", "sonnet");
+ sonnetData = this.parseSection(lines, 'Current week (Opus)', 'sonnet');
}
return {
@@ -294,10 +301,14 @@ export class ClaudeUsageService {
/**
* Parse a section of the usage output to extract percentage and reset time
*/
- private parseSection(lines: string[], sectionLabel: string, type: string): { percentage: number; resetTime: string; resetText: string } {
+ private parseSection(
+ lines: string[],
+ sectionLabel: string,
+ type: string
+ ): { percentage: number; resetTime: string; resetText: string } {
let percentage = 0;
let resetTime = this.getDefaultResetTime(type);
- let resetText = "";
+ let resetText = '';
// Find the LAST occurrence of the section (terminal output has multiple screen refreshes)
let sectionIndex = -1;
@@ -321,14 +332,14 @@ export class ClaudeUsageService {
const percentMatch = line.match(/(\d{1,3})\s*%\s*(left|used|remaining)/i);
if (percentMatch) {
const value = parseInt(percentMatch[1], 10);
- const isUsed = percentMatch[2].toLowerCase() === "used";
+ const isUsed = percentMatch[2].toLowerCase() === 'used';
// Convert "left" to "used" percentage (our UI shows % used)
- percentage = isUsed ? value : (100 - value);
+ percentage = isUsed ? value : 100 - value;
}
}
// Extract reset time - only take the first match
- if (!resetText && line.toLowerCase().includes("reset")) {
+ if (!resetText && line.toLowerCase().includes('reset')) {
resetText = line;
}
}
@@ -337,7 +348,7 @@ export class ClaudeUsageService {
if (resetText) {
resetTime = this.parseResetTime(resetText, type);
// Strip timezone like "(Asia/Dubai)" from the display text
- resetText = resetText.replace(/\s*\([A-Za-z_\/]+\)\s*$/, "").trim();
+ resetText = resetText.replace(/\s*\([A-Za-z_\/]+\)\s*$/, '').trim();
}
return { percentage, resetTime, resetText };
@@ -350,7 +361,9 @@ export class ClaudeUsageService {
const now = new Date();
// Try to parse duration format: "Resets in 2h 15m" or "Resets in 30m"
- const durationMatch = text.match(/(\d+)\s*h(?:ours?)?(?:\s+(\d+)\s*m(?:in)?)?|(\d+)\s*m(?:in)?/i);
+ const durationMatch = text.match(
+ /(\d+)\s*h(?:ours?)?(?:\s+(\d+)\s*m(?:in)?)?|(\d+)\s*m(?:in)?/i
+ );
if (durationMatch) {
let hours = 0;
let minutes = 0;
@@ -374,9 +387,9 @@ export class ClaudeUsageService {
const ampm = simpleTimeMatch[3].toLowerCase();
// Convert 12-hour to 24-hour
- if (ampm === "pm" && hours !== 12) {
+ if (ampm === 'pm' && hours !== 12) {
hours += 12;
- } else if (ampm === "am" && hours === 12) {
+ } else if (ampm === 'am' && hours === 12) {
hours = 0;
}
@@ -392,7 +405,9 @@ export class ClaudeUsageService {
}
// Try to parse date format: "Resets Dec 22 at 8pm" or "Resets Jan 15, 3:30pm"
- const dateMatch = text.match(/([A-Za-z]{3,})\s+(\d{1,2})(?:\s+at\s+|\s*,?\s*)(\d{1,2})(?::(\d{2}))?\s*(am|pm)/i);
+ const dateMatch = text.match(
+ /([A-Za-z]{3,})\s+(\d{1,2})(?:\s+at\s+|\s*,?\s*)(\d{1,2})(?::(\d{2}))?\s*(am|pm)/i
+ );
if (dateMatch) {
const monthName = dateMatch[1];
const day = parseInt(dateMatch[2], 10);
@@ -401,16 +416,26 @@ export class ClaudeUsageService {
const ampm = dateMatch[5].toLowerCase();
// Convert 12-hour to 24-hour
- if (ampm === "pm" && hours !== 12) {
+ if (ampm === 'pm' && hours !== 12) {
hours += 12;
- } else if (ampm === "am" && hours === 12) {
+ } else if (ampm === 'am' && hours === 12) {
hours = 0;
}
// Parse month name
const months: Record = {
- jan: 0, feb: 1, mar: 2, apr: 3, may: 4, jun: 5,
- jul: 6, aug: 7, sep: 8, oct: 9, nov: 10, dec: 11
+ jan: 0,
+ feb: 1,
+ mar: 2,
+ apr: 3,
+ may: 4,
+ jun: 5,
+ jul: 6,
+ aug: 7,
+ sep: 8,
+ oct: 9,
+ nov: 10,
+ dec: 11,
};
const month = months[monthName.toLowerCase().substring(0, 3)];
@@ -435,7 +460,7 @@ export class ClaudeUsageService {
private getDefaultResetTime(type: string): string {
const now = new Date();
- if (type === "session") {
+ if (type === 'session') {
// Session resets in ~5 hours
return new Date(now.getTime() + 5 * 60 * 60 * 1000).toISOString();
} else {
diff --git a/apps/server/src/services/feature-loader.ts b/apps/server/src/services/feature-loader.ts
index 41585103..fbf86d49 100644
--- a/apps/server/src/services/feature-loader.ts
+++ b/apps/server/src/services/feature-loader.ts
@@ -3,18 +3,18 @@
* Each feature is stored in .automaker/features/{featureId}/feature.json
*/
-import path from "path";
-import type { Feature } from "@automaker/types";
-import { createLogger } from "@automaker/utils";
-import * as secureFs from "../lib/secure-fs.js";
+import path from 'path';
+import type { Feature } from '@automaker/types';
+import { createLogger } from '@automaker/utils';
+import * as secureFs from '../lib/secure-fs.js';
import {
getFeaturesDir,
getFeatureDir,
getFeatureImagesDir,
ensureAutomakerDir,
-} from "@automaker/platform";
+} from '@automaker/platform';
-const logger = createLogger("FeatureLoader");
+const logger = createLogger('FeatureLoader');
// Re-export Feature type for convenience
export type { Feature };
@@ -39,24 +39,16 @@ export class FeatureLoader {
*/
private async deleteOrphanedImages(
projectPath: string,
- oldPaths:
- | Array
- | undefined,
- newPaths:
- | Array
- | undefined
+ oldPaths: Array | undefined,
+ newPaths: Array | undefined
): Promise {
if (!oldPaths || oldPaths.length === 0) {
return;
}
// Build sets of paths for comparison
- const oldPathSet = new Set(
- oldPaths.map((p) => (typeof p === "string" ? p : p.path))
- );
- const newPathSet = new Set(
- (newPaths || []).map((p) => (typeof p === "string" ? p : p.path))
- );
+ const oldPathSet = new Set(oldPaths.map((p) => (typeof p === 'string' ? p : p.path)));
+ const newPathSet = new Set((newPaths || []).map((p) => (typeof p === 'string' ? p : p.path)));
// Find images that were removed
for (const oldPath of oldPathSet) {
@@ -67,10 +59,7 @@ export class FeatureLoader {
console.log(`[FeatureLoader] Deleted orphaned image: ${oldPath}`);
} catch (error) {
// Ignore errors when deleting (file may already be gone)
- logger.warn(
- `[FeatureLoader] Failed to delete image: ${oldPath}`,
- error
- );
+ logger.warn(`[FeatureLoader] Failed to delete image: ${oldPath}`, error);
}
}
}
@@ -83,9 +72,7 @@ export class FeatureLoader {
projectPath: string,
featureId: string,
imagePaths?: Array
- ): Promise<
- Array | undefined
- > {
+ ): Promise | undefined> {
if (!imagePaths || imagePaths.length === 0) {
return imagePaths;
}
@@ -93,14 +80,11 @@ export class FeatureLoader {
const featureImagesDir = this.getFeatureImagesDir(projectPath, featureId);
await secureFs.mkdir(featureImagesDir, { recursive: true });
- const updatedPaths: Array<
- string | { path: string; [key: string]: unknown }
- > = [];
+ const updatedPaths: Array = [];
for (const imagePath of imagePaths) {
try {
- const originalPath =
- typeof imagePath === "string" ? imagePath : imagePath.path;
+ const originalPath = typeof imagePath === 'string' ? imagePath : imagePath.path;
// Skip if already in feature directory (already absolute path in external storage)
if (originalPath.includes(`/features/${featureId}/images/`)) {
@@ -117,9 +101,7 @@ export class FeatureLoader {
try {
await secureFs.access(fullOriginalPath);
} catch {
- logger.warn(
- `[FeatureLoader] Image not found, skipping: ${fullOriginalPath}`
- );
+ logger.warn(`[FeatureLoader] Image not found, skipping: ${fullOriginalPath}`);
continue;
}
@@ -129,9 +111,7 @@ export class FeatureLoader {
// Copy the file
await secureFs.copyFile(fullOriginalPath, newPath);
- console.log(
- `[FeatureLoader] Copied image: ${originalPath} -> ${newPath}`
- );
+ console.log(`[FeatureLoader] Copied image: ${originalPath} -> ${newPath}`);
// Try to delete the original temp file
try {
@@ -141,7 +121,7 @@ export class FeatureLoader {
}
// Update the path in the result (use absolute path)
- if (typeof imagePath === "string") {
+ if (typeof imagePath === 'string') {
updatedPaths.push(newPath);
} else {
updatedPaths.push({ ...imagePath, path: newPath });
@@ -168,20 +148,14 @@ export class FeatureLoader {
* Get the path to a feature's feature.json file
*/
getFeatureJsonPath(projectPath: string, featureId: string): string {
- return path.join(
- this.getFeatureDir(projectPath, featureId),
- "feature.json"
- );
+ return path.join(this.getFeatureDir(projectPath, featureId), 'feature.json');
}
/**
* Get the path to a feature's agent-output.md file
*/
getAgentOutputPath(projectPath: string, featureId: string): string {
- return path.join(
- this.getFeatureDir(projectPath, featureId),
- "agent-output.md"
- );
+ return path.join(this.getFeatureDir(projectPath, featureId), 'agent-output.md');
}
/**
@@ -218,10 +192,7 @@ export class FeatureLoader {
const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId);
try {
- const content = (await secureFs.readFile(
- featureJsonPath,
- "utf-8"
- )) as string;
+ const content = (await secureFs.readFile(featureJsonPath, 'utf-8')) as string;
const feature = JSON.parse(content);
if (!feature.id) {
@@ -233,7 +204,7 @@ export class FeatureLoader {
features.push(feature);
} catch (error) {
- if ((error as NodeJS.ErrnoException).code === "ENOENT") {
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
continue;
} else if (error instanceof SyntaxError) {
logger.warn(
@@ -250,14 +221,14 @@ export class FeatureLoader {
// Sort by creation order (feature IDs contain timestamp)
features.sort((a, b) => {
- const aTime = a.id ? parseInt(a.id.split("-")[1] || "0") : 0;
- const bTime = b.id ? parseInt(b.id.split("-")[1] || "0") : 0;
+ const aTime = a.id ? parseInt(a.id.split('-')[1] || '0') : 0;
+ const bTime = b.id ? parseInt(b.id.split('-')[1] || '0') : 0;
return aTime - bTime;
});
return features;
} catch (error) {
- logger.error("Failed to get all features:", error);
+ logger.error('Failed to get all features:', error);
return [];
}
}
@@ -268,19 +239,13 @@ export class FeatureLoader {
async get(projectPath: string, featureId: string): Promise {
try {
const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId);
- const content = (await secureFs.readFile(
- featureJsonPath,
- "utf-8"
- )) as string;
+ const content = (await secureFs.readFile(featureJsonPath, 'utf-8')) as string;
return JSON.parse(content);
} catch (error) {
- if ((error as NodeJS.ErrnoException).code === "ENOENT") {
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return null;
}
- logger.error(
- `[FeatureLoader] Failed to get feature ${featureId}:`,
- error
- );
+ logger.error(`[FeatureLoader] Failed to get feature ${featureId}:`, error);
throw error;
}
}
@@ -288,10 +253,7 @@ export class FeatureLoader {
/**
* Create a new feature
*/
- async create(
- projectPath: string,
- featureData: Partial
- ): Promise {
+ async create(projectPath: string, featureData: Partial): Promise {
const featureId = featureData.id || this.generateFeatureId();
const featureDir = this.getFeatureDir(projectPath, featureId);
const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId);
@@ -311,19 +273,15 @@ export class FeatureLoader {
// Ensure feature has required fields
const feature: Feature = {
- category: featureData.category || "Uncategorized",
- description: featureData.description || "",
+ category: featureData.category || 'Uncategorized',
+ description: featureData.description || '',
...featureData,
id: featureId,
imagePaths: migratedImagePaths,
};
// Write feature.json
- await secureFs.writeFile(
- featureJsonPath,
- JSON.stringify(feature, null, 2),
- "utf-8"
- );
+ await secureFs.writeFile(featureJsonPath, JSON.stringify(feature, null, 2), 'utf-8');
logger.info(`Created feature ${featureId}`);
return feature;
@@ -346,36 +304,22 @@ export class FeatureLoader {
let updatedImagePaths = updates.imagePaths;
if (updates.imagePaths !== undefined) {
// Delete orphaned images (images that were removed)
- await this.deleteOrphanedImages(
- projectPath,
- feature.imagePaths,
- updates.imagePaths
- );
+ await this.deleteOrphanedImages(projectPath, feature.imagePaths, updates.imagePaths);
// Migrate any new images
- updatedImagePaths = await this.migrateImages(
- projectPath,
- featureId,
- updates.imagePaths
- );
+ updatedImagePaths = await this.migrateImages(projectPath, featureId, updates.imagePaths);
}
// Merge updates
const updatedFeature: Feature = {
...feature,
...updates,
- ...(updatedImagePaths !== undefined
- ? { imagePaths: updatedImagePaths }
- : {}),
+ ...(updatedImagePaths !== undefined ? { imagePaths: updatedImagePaths } : {}),
};
// Write back to file
const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId);
- await secureFs.writeFile(
- featureJsonPath,
- JSON.stringify(updatedFeature, null, 2),
- "utf-8"
- );
+ await secureFs.writeFile(featureJsonPath, JSON.stringify(updatedFeature, null, 2), 'utf-8');
logger.info(`Updated feature ${featureId}`);
return updatedFeature;
@@ -391,10 +335,7 @@ export class FeatureLoader {
console.log(`[FeatureLoader] Deleted feature ${featureId}`);
return true;
} catch (error) {
- logger.error(
- `[FeatureLoader] Failed to delete feature ${featureId}:`,
- error
- );
+ logger.error(`[FeatureLoader] Failed to delete feature ${featureId}:`, error);
return false;
}
}
@@ -402,25 +343,16 @@ export class FeatureLoader {
/**
* Get agent output for a feature
*/
- async getAgentOutput(
- projectPath: string,
- featureId: string
- ): Promise {
+ async getAgentOutput(projectPath: string, featureId: string): Promise {
try {
const agentOutputPath = this.getAgentOutputPath(projectPath, featureId);
- const content = (await secureFs.readFile(
- agentOutputPath,
- "utf-8"
- )) as string;
+ const content = (await secureFs.readFile(agentOutputPath, 'utf-8')) as string;
return content;
} catch (error) {
- if ((error as NodeJS.ErrnoException).code === "ENOENT") {
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return null;
}
- logger.error(
- `[FeatureLoader] Failed to get agent output for ${featureId}:`,
- error
- );
+ logger.error(`[FeatureLoader] Failed to get agent output for ${featureId}:`, error);
throw error;
}
}
@@ -428,30 +360,23 @@ export class FeatureLoader {
/**
* Save agent output for a feature
*/
- async saveAgentOutput(
- projectPath: string,
- featureId: string,
- content: string
- ): Promise {
+ async saveAgentOutput(projectPath: string, featureId: string, content: string): Promise {
const featureDir = this.getFeatureDir(projectPath, featureId);
await secureFs.mkdir(featureDir, { recursive: true });
const agentOutputPath = this.getAgentOutputPath(projectPath, featureId);
- await secureFs.writeFile(agentOutputPath, content, "utf-8");
+ await secureFs.writeFile(agentOutputPath, content, 'utf-8');
}
/**
* Delete agent output for a feature
*/
- async deleteAgentOutput(
- projectPath: string,
- featureId: string
- ): Promise {
+ async deleteAgentOutput(projectPath: string, featureId: string): Promise {
try {
const agentOutputPath = this.getAgentOutputPath(projectPath, featureId);
await secureFs.unlink(agentOutputPath);
} catch (error) {
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
throw error;
}
}
diff --git a/apps/server/src/services/settings-service.ts b/apps/server/src/services/settings-service.ts
index a935d93a..288bde18 100644
--- a/apps/server/src/services/settings-service.ts
+++ b/apps/server/src/services/settings-service.ts
@@ -7,8 +7,8 @@
* - Per-project settings ({projectPath}/.automaker/settings.json)
*/
-import { createLogger } from "@automaker/utils";
-import * as secureFs from "../lib/secure-fs.js";
+import { createLogger } from '@automaker/utils';
+import * as secureFs from '../lib/secure-fs.js';
import {
getGlobalSettingsPath,
@@ -16,7 +16,7 @@ import {
getProjectSettingsPath,
ensureDataDir,
ensureAutomakerDir,
-} from "@automaker/platform";
+} from '@automaker/platform';
import type {
GlobalSettings,
Credentials,
@@ -27,7 +27,7 @@ import type {
TrashedProjectRef,
BoardBackgroundSettings,
WorktreeInfo,
-} from "../types/settings.js";
+} from '../types/settings.js';
import {
DEFAULT_GLOBAL_SETTINGS,
DEFAULT_CREDENTIALS,
@@ -35,9 +35,9 @@ import {
SETTINGS_VERSION,
CREDENTIALS_VERSION,
PROJECT_SETTINGS_VERSION,
-} from "../types/settings.js";
+} from '../types/settings.js';
-const logger = createLogger("SettingsService");
+const logger = createLogger('SettingsService');
/**
* Atomic file write - write to temp file then rename
@@ -47,7 +47,7 @@ async function atomicWriteJson(filePath: string, data: unknown): Promise {
const content = JSON.stringify(data, null, 2);
try {
- await secureFs.writeFile(tempPath, content, "utf-8");
+ await secureFs.writeFile(tempPath, content, 'utf-8');
await secureFs.rename(tempPath, filePath);
} catch (error) {
// Clean up temp file if it exists
@@ -65,10 +65,10 @@ async function atomicWriteJson(filePath: string, data: unknown): Promise {
*/
async function readJsonFile(filePath: string, defaultValue: T): Promise {
try {
- const content = (await secureFs.readFile(filePath, "utf-8")) as string;
+ const content = (await secureFs.readFile(filePath, 'utf-8')) as string;
return JSON.parse(content) as T;
} catch (error) {
- if ((error as NodeJS.ErrnoException).code === "ENOENT") {
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return defaultValue;
}
logger.error(`Error reading ${filePath}:`, error);
@@ -128,10 +128,7 @@ export class SettingsService {
*/
async getGlobalSettings(): Promise {
const settingsPath = getGlobalSettingsPath(this.dataDir);
- const settings = await readJsonFile(
- settingsPath,
- DEFAULT_GLOBAL_SETTINGS
- );
+ const settings = await readJsonFile(settingsPath, DEFAULT_GLOBAL_SETTINGS);
// Apply any missing defaults (for backwards compatibility)
return {
@@ -153,9 +150,7 @@ export class SettingsService {
* @param updates - Partial GlobalSettings to merge (only provided fields are updated)
* @returns Promise resolving to complete updated GlobalSettings
*/
- async updateGlobalSettings(
- updates: Partial
- ): Promise {
+ async updateGlobalSettings(updates: Partial): Promise {
await ensureDataDir(this.dataDir);
const settingsPath = getGlobalSettingsPath(this.dataDir);
@@ -175,7 +170,7 @@ export class SettingsService {
}
await atomicWriteJson(settingsPath, updated);
- logger.info("Global settings updated");
+ logger.info('Global settings updated');
return updated;
}
@@ -207,10 +202,7 @@ export class SettingsService {
*/
async getCredentials(): Promise {
const credentialsPath = getCredentialsPath(this.dataDir);
- const credentials = await readJsonFile(
- credentialsPath,
- DEFAULT_CREDENTIALS
- );
+ const credentials = await readJsonFile(credentialsPath, DEFAULT_CREDENTIALS);
return {
...DEFAULT_CREDENTIALS,
@@ -252,7 +244,7 @@ export class SettingsService {
}
await atomicWriteJson(credentialsPath, updated);
- logger.info("Credentials updated");
+ logger.info('Credentials updated');
return updated;
}
@@ -272,7 +264,7 @@ export class SettingsService {
const credentials = await this.getCredentials();
const maskKey = (key: string): string => {
- if (!key || key.length < 8) return "";
+ if (!key || key.length < 8) return '';
return `${key.substring(0, 4)}...${key.substring(key.length - 4)}`;
};
@@ -312,10 +304,7 @@ export class SettingsService {
*/
async getProjectSettings(projectPath: string): Promise {
const settingsPath = getProjectSettingsPath(projectPath);
- const settings = await readJsonFile(
- settingsPath,
- DEFAULT_PROJECT_SETTINGS
- );
+ const settings = await readJsonFile(settingsPath, DEFAULT_PROJECT_SETTINGS);
return {
...DEFAULT_PROJECT_SETTINGS,
@@ -388,11 +377,11 @@ export class SettingsService {
* @returns Promise resolving to migration result with success status and error list
*/
async migrateFromLocalStorage(localStorageData: {
- "automaker-storage"?: string;
- "automaker-setup"?: string;
- "worktree-panel-collapsed"?: string;
- "file-browser-recent-folders"?: string;
- "automaker:lastProjectDir"?: string;
+ 'automaker-storage'?: string;
+ 'automaker-setup'?: string;
+ 'worktree-panel-collapsed'?: string;
+ 'file-browser-recent-folders'?: string;
+ 'automaker:lastProjectDir'?: string;
}): Promise<{
success: boolean;
migratedGlobalSettings: boolean;
@@ -408,9 +397,9 @@ export class SettingsService {
try {
// Parse the main automaker-storage
let appState: Record = {};
- if (localStorageData["automaker-storage"]) {
+ if (localStorageData['automaker-storage']) {
try {
- const parsed = JSON.parse(localStorageData["automaker-storage"]);
+ const parsed = JSON.parse(localStorageData['automaker-storage']);
appState = parsed.state || parsed;
} catch (e) {
errors.push(`Failed to parse automaker-storage: ${e}`);
@@ -419,20 +408,14 @@ export class SettingsService {
// Extract global settings
const globalSettings: Partial = {
- theme: (appState.theme as GlobalSettings["theme"]) || "dark",
- sidebarOpen:
- appState.sidebarOpen !== undefined
- ? (appState.sidebarOpen as boolean)
- : true,
+ theme: (appState.theme as GlobalSettings['theme']) || 'dark',
+ sidebarOpen: appState.sidebarOpen !== undefined ? (appState.sidebarOpen as boolean) : true,
chatHistoryOpen: (appState.chatHistoryOpen as boolean) || false,
kanbanCardDetailLevel:
- (appState.kanbanCardDetailLevel as GlobalSettings["kanbanCardDetailLevel"]) ||
- "standard",
+ (appState.kanbanCardDetailLevel as GlobalSettings['kanbanCardDetailLevel']) || 'standard',
maxConcurrency: (appState.maxConcurrency as number) || 3,
defaultSkipTests:
- appState.defaultSkipTests !== undefined
- ? (appState.defaultSkipTests as boolean)
- : true,
+ appState.defaultSkipTests !== undefined ? (appState.defaultSkipTests as boolean) : true,
enableDependencyBlocking:
appState.enableDependencyBlocking !== undefined
? (appState.enableDependencyBlocking as boolean)
@@ -440,55 +423,48 @@ export class SettingsService {
useWorktrees: (appState.useWorktrees as boolean) || false,
showProfilesOnly: (appState.showProfilesOnly as boolean) || false,
defaultPlanningMode:
- (appState.defaultPlanningMode as GlobalSettings["defaultPlanningMode"]) ||
- "skip",
- defaultRequirePlanApproval:
- (appState.defaultRequirePlanApproval as boolean) || false,
- defaultAIProfileId:
- (appState.defaultAIProfileId as string | null) || null,
+ (appState.defaultPlanningMode as GlobalSettings['defaultPlanningMode']) || 'skip',
+ defaultRequirePlanApproval: (appState.defaultRequirePlanApproval as boolean) || false,
+ defaultAIProfileId: (appState.defaultAIProfileId as string | null) || null,
muteDoneSound: (appState.muteDoneSound as boolean) || false,
enhancementModel:
- (appState.enhancementModel as GlobalSettings["enhancementModel"]) ||
- "sonnet",
+ (appState.enhancementModel as GlobalSettings['enhancementModel']) || 'sonnet',
keyboardShortcuts:
(appState.keyboardShortcuts as KeyboardShortcuts) ||
DEFAULT_GLOBAL_SETTINGS.keyboardShortcuts,
aiProfiles: (appState.aiProfiles as AIProfile[]) || [],
projects: (appState.projects as ProjectRef[]) || [],
- trashedProjects:
- (appState.trashedProjects as TrashedProjectRef[]) || [],
+ trashedProjects: (appState.trashedProjects as TrashedProjectRef[]) || [],
projectHistory: (appState.projectHistory as string[]) || [],
projectHistoryIndex: (appState.projectHistoryIndex as number) || -1,
lastSelectedSessionByProject:
- (appState.lastSelectedSessionByProject as Record) ||
- {},
+ (appState.lastSelectedSessionByProject as Record) || {},
};
// Add direct localStorage values
- if (localStorageData["automaker:lastProjectDir"]) {
- globalSettings.lastProjectDir =
- localStorageData["automaker:lastProjectDir"];
+ if (localStorageData['automaker:lastProjectDir']) {
+ globalSettings.lastProjectDir = localStorageData['automaker:lastProjectDir'];
}
- if (localStorageData["file-browser-recent-folders"]) {
+ if (localStorageData['file-browser-recent-folders']) {
try {
globalSettings.recentFolders = JSON.parse(
- localStorageData["file-browser-recent-folders"]
+ localStorageData['file-browser-recent-folders']
);
} catch {
globalSettings.recentFolders = [];
}
}
- if (localStorageData["worktree-panel-collapsed"]) {
+ if (localStorageData['worktree-panel-collapsed']) {
globalSettings.worktreePanelCollapsed =
- localStorageData["worktree-panel-collapsed"] === "true";
+ localStorageData['worktree-panel-collapsed'] === 'true';
}
// Save global settings
await this.updateGlobalSettings(globalSettings);
migratedGlobalSettings = true;
- logger.info("Migrated global settings from localStorage");
+ logger.info('Migrated global settings from localStorage');
// Extract and save credentials
if (appState.apiKeys) {
@@ -499,13 +475,13 @@ export class SettingsService {
};
await this.updateCredentials({
apiKeys: {
- anthropic: apiKeys.anthropic || "",
- google: apiKeys.google || "",
- openai: apiKeys.openai || "",
+ anthropic: apiKeys.anthropic || '',
+ google: apiKeys.google || '',
+ openai: apiKeys.openai || '',
},
});
migratedCredentials = true;
- logger.info("Migrated credentials from localStorage");
+ logger.info('Migrated credentials from localStorage');
}
// Migrate per-project settings
@@ -522,14 +498,10 @@ export class SettingsService {
// Get unique project paths that have per-project settings
const projectPaths = new Set();
if (boardBackgroundByProject) {
- Object.keys(boardBackgroundByProject).forEach((p) =>
- projectPaths.add(p)
- );
+ Object.keys(boardBackgroundByProject).forEach((p) => projectPaths.add(p));
}
if (currentWorktreeByProject) {
- Object.keys(currentWorktreeByProject).forEach((p) =>
- projectPaths.add(p)
- );
+ Object.keys(currentWorktreeByProject).forEach((p) => projectPaths.add(p));
}
if (worktreesByProject) {
Object.keys(worktreesByProject).forEach((p) => projectPaths.add(p));
@@ -551,17 +523,15 @@ export class SettingsService {
// Get theme from project object
const project = projects.find((p) => p.path === projectPath);
if (project?.theme) {
- projectSettings.theme = project.theme as ProjectSettings["theme"];
+ projectSettings.theme = project.theme as ProjectSettings['theme'];
}
if (boardBackgroundByProject?.[projectPath]) {
- projectSettings.boardBackground =
- boardBackgroundByProject[projectPath];
+ projectSettings.boardBackground = boardBackgroundByProject[projectPath];
}
if (currentWorktreeByProject?.[projectPath]) {
- projectSettings.currentWorktree =
- currentWorktreeByProject[projectPath];
+ projectSettings.currentWorktree = currentWorktreeByProject[projectPath];
}
if (worktreesByProject?.[projectPath]) {
@@ -573,15 +543,11 @@ export class SettingsService {
migratedProjectCount++;
}
} catch (e) {
- errors.push(
- `Failed to migrate project settings for ${projectPath}: ${e}`
- );
+ errors.push(`Failed to migrate project settings for ${projectPath}: ${e}`);
}
}
- logger.info(
- `Migration complete: ${migratedProjectCount} projects migrated`
- );
+ logger.info(`Migration complete: ${migratedProjectCount} projects migrated`);
return {
success: errors.length === 0,
@@ -591,7 +557,7 @@ export class SettingsService {
errors,
};
} catch (error) {
- logger.error("Migration failed:", error);
+ logger.error('Migration failed:', error);
errors.push(`Migration failed: ${error}`);
return {
success: false,
diff --git a/apps/server/src/services/terminal-service.ts b/apps/server/src/services/terminal-service.ts
index 6d8faa7f..7d59633e 100644
--- a/apps/server/src/services/terminal-service.ts
+++ b/apps/server/src/services/terminal-service.ts
@@ -5,17 +5,29 @@
* Supports cross-platform shell detection including WSL.
*/
-import * as pty from "node-pty";
-import { EventEmitter } from "events";
-import * as os from "os";
-import * as fs from "fs";
+import * as pty from 'node-pty';
+import { EventEmitter } from 'events';
+import * as os from 'os';
+import * as fs from 'fs';
+import * as path from 'path';
// Maximum scrollback buffer size (characters)
const MAX_SCROLLBACK_SIZE = 50000; // ~50KB per terminal
+// Session limit constants - shared with routes/settings.ts
+export const MIN_MAX_SESSIONS = 1;
+export const MAX_MAX_SESSIONS = 1000;
+
+// Maximum number of concurrent terminal sessions
+// Can be overridden via TERMINAL_MAX_SESSIONS environment variable
+// Default set to 1000 - effectively unlimited for most use cases
+let maxSessions = parseInt(process.env.TERMINAL_MAX_SESSIONS || '1000', 10);
+
// Throttle output to prevent overwhelming WebSocket under heavy load
-const OUTPUT_THROTTLE_MS = 16; // ~60fps max update rate
-const OUTPUT_BATCH_SIZE = 8192; // Max bytes to send per batch
+// Using 4ms for responsive input feedback while still preventing flood
+// Note: 16ms caused perceived input lag, especially with backspace
+const OUTPUT_THROTTLE_MS = 4; // ~250fps max update rate for responsive input
+const OUTPUT_BATCH_SIZE = 4096; // Smaller batches for lower latency
export interface TerminalSession {
id: string;
@@ -53,20 +65,20 @@ export class TerminalService extends EventEmitter {
const platform = os.platform();
// Check if running in WSL
- if (platform === "linux" && this.isWSL()) {
+ if (platform === 'linux' && this.isWSL()) {
// In WSL, prefer the user's configured shell or bash
- const userShell = process.env.SHELL || "/bin/bash";
+ const userShell = process.env.SHELL || '/bin/bash';
if (fs.existsSync(userShell)) {
- return { shell: userShell, args: ["--login"] };
+ return { shell: userShell, args: ['--login'] };
}
- return { shell: "/bin/bash", args: ["--login"] };
+ return { shell: '/bin/bash', args: ['--login'] };
}
switch (platform) {
- case "win32": {
+ case 'win32': {
// Windows: prefer PowerShell, fall back to cmd
- const pwsh = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe";
- const pwshCore = "C:\\Program Files\\PowerShell\\7\\pwsh.exe";
+ const pwsh = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
+ const pwshCore = 'C:\\Program Files\\PowerShell\\7\\pwsh.exe';
if (fs.existsSync(pwshCore)) {
return { shell: pwshCore, args: [] };
@@ -74,32 +86,32 @@ export class TerminalService extends EventEmitter {
if (fs.existsSync(pwsh)) {
return { shell: pwsh, args: [] };
}
- return { shell: "cmd.exe", args: [] };
+ return { shell: 'cmd.exe', args: [] };
}
- case "darwin": {
+ case 'darwin': {
// macOS: prefer user's shell, then zsh, then bash
const userShell = process.env.SHELL;
if (userShell && fs.existsSync(userShell)) {
- return { shell: userShell, args: ["--login"] };
+ return { shell: userShell, args: ['--login'] };
}
- if (fs.existsSync("/bin/zsh")) {
- return { shell: "/bin/zsh", args: ["--login"] };
+ if (fs.existsSync('/bin/zsh')) {
+ return { shell: '/bin/zsh', args: ['--login'] };
}
- return { shell: "/bin/bash", args: ["--login"] };
+ return { shell: '/bin/bash', args: ['--login'] };
}
- case "linux":
+ case 'linux':
default: {
// Linux: prefer user's shell, then bash, then sh
const userShell = process.env.SHELL;
if (userShell && fs.existsSync(userShell)) {
- return { shell: userShell, args: ["--login"] };
+ return { shell: userShell, args: ['--login'] };
}
- if (fs.existsSync("/bin/bash")) {
- return { shell: "/bin/bash", args: ["--login"] };
+ if (fs.existsSync('/bin/bash')) {
+ return { shell: '/bin/bash', args: ['--login'] };
}
- return { shell: "/bin/sh", args: [] };
+ return { shell: '/bin/sh', args: [] };
}
}
}
@@ -110,9 +122,9 @@ export class TerminalService extends EventEmitter {
isWSL(): boolean {
try {
// Check /proc/version for Microsoft/WSL indicators
- if (fs.existsSync("/proc/version")) {
- const version = fs.readFileSync("/proc/version", "utf-8").toLowerCase();
- return version.includes("microsoft") || version.includes("wsl");
+ if (fs.existsSync('/proc/version')) {
+ const version = fs.readFileSync('/proc/version', 'utf-8').toLowerCase();
+ return version.includes('microsoft') || version.includes('wsl');
}
// Check for WSL environment variable
if (process.env.WSL_DISTRO_NAME || process.env.WSLENV) {
@@ -144,6 +156,7 @@ export class TerminalService extends EventEmitter {
/**
* Validate and resolve a working directory path
+ * Includes basic sanitization against null bytes and path normalization
*/
private resolveWorkingDirectory(requestedCwd?: string): string {
const homeDir = os.homedir();
@@ -156,11 +169,23 @@ export class TerminalService extends EventEmitter {
// Clean up the path
let cwd = requestedCwd.trim();
+ // Reject paths with null bytes (could bypass path checks)
+ if (cwd.includes('\0')) {
+ console.warn(`[Terminal] Rejecting path with null byte: ${cwd.replace(/\0/g, '\\0')}`);
+ return homeDir;
+ }
+
// Fix double slashes at start (but not for Windows UNC paths)
- if (cwd.startsWith("//") && !cwd.startsWith("//wsl")) {
+ if (cwd.startsWith('//') && !cwd.startsWith('//wsl')) {
cwd = cwd.slice(1);
}
+ // Normalize the path to resolve . and .. segments
+ // Skip normalization for WSL UNC paths as path.resolve would break them
+ if (!cwd.startsWith('//wsl')) {
+ cwd = path.resolve(cwd);
+ }
+
// Check if path exists and is a directory
try {
const stat = fs.statSync(cwd);
@@ -176,10 +201,41 @@ export class TerminalService extends EventEmitter {
}
/**
- * Create a new terminal session
+ * Get current session count
*/
- createSession(options: TerminalOptions = {}): TerminalSession {
- const id = `term-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+ getSessionCount(): number {
+ return this.sessions.size;
+ }
+
+ /**
+ * Get maximum allowed sessions
+ */
+ getMaxSessions(): number {
+ return maxSessions;
+ }
+
+ /**
+ * Set maximum allowed sessions (can be called dynamically)
+ */
+ setMaxSessions(limit: number): void {
+ if (limit >= MIN_MAX_SESSIONS && limit <= MAX_MAX_SESSIONS) {
+ maxSessions = limit;
+ console.log(`[Terminal] Max sessions limit updated to ${limit}`);
+ }
+ }
+
+ /**
+ * Create a new terminal session
+ * Returns null if the maximum session limit has been reached
+ */
+ createSession(options: TerminalOptions = {}): TerminalSession | null {
+ // Check session limit
+ if (this.sessions.size >= maxSessions) {
+ console.error(`[Terminal] Max sessions (${maxSessions}) reached, refusing new session`);
+ return null;
+ }
+
+ const id = `term-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
const { shell: detectedShell, args: shellArgs } = this.detectShell();
const shell = options.shell || detectedShell;
@@ -188,18 +244,22 @@ export class TerminalService extends EventEmitter {
const cwd = this.resolveWorkingDirectory(options.cwd);
// Build environment with some useful defaults
+ // These settings ensure consistent terminal behavior across platforms
const env: Record = {
...process.env,
- TERM: "xterm-256color",
- COLORTERM: "truecolor",
- TERM_PROGRAM: "automaker-terminal",
+ TERM: 'xterm-256color',
+ COLORTERM: 'truecolor',
+ TERM_PROGRAM: 'automaker-terminal',
+ // Ensure proper locale for character handling
+ LANG: process.env.LANG || 'en_US.UTF-8',
+ LC_ALL: process.env.LC_ALL || process.env.LANG || 'en_US.UTF-8',
...options.env,
};
console.log(`[Terminal] Creating session ${id} with shell: ${shell} in ${cwd}`);
const ptyProcess = pty.spawn(shell, shellArgs, {
- name: "xterm-256color",
+ name: 'xterm-256color',
cols: options.cols || 80,
rows: options.rows || 24,
cwd,
@@ -212,8 +272,8 @@ export class TerminalService extends EventEmitter {
cwd,
createdAt: new Date(),
shell,
- scrollbackBuffer: "",
- outputBuffer: "",
+ scrollbackBuffer: '',
+ outputBuffer: '',
flushTimeout: null,
resizeInProgress: false,
resizeDebounceTimeout: null,
@@ -233,12 +293,12 @@ export class TerminalService extends EventEmitter {
// Schedule another flush for remaining data
session.flushTimeout = setTimeout(flushOutput, OUTPUT_THROTTLE_MS);
} else {
- session.outputBuffer = "";
+ session.outputBuffer = '';
session.flushTimeout = null;
}
this.dataCallbacks.forEach((cb) => cb(id, dataToSend));
- this.emit("data", id, dataToSend);
+ this.emit('data', id, dataToSend);
};
// Forward data events with throttling
@@ -271,7 +331,7 @@ export class TerminalService extends EventEmitter {
console.log(`[Terminal] Session ${id} exited with code ${exitCode}`);
this.sessions.delete(id);
this.exitCallbacks.forEach((cb) => cb(id, exitCode));
- this.emit("exit", id, exitCode);
+ this.emit('exit', id, exitCode);
});
console.log(`[Terminal] Session ${id} created successfully`);
@@ -333,6 +393,7 @@ export class TerminalService extends EventEmitter {
/**
* Kill a terminal session
+ * Attempts graceful SIGTERM first, then SIGKILL after 1 second if still alive
*/
killSession(sessionId: string): boolean {
const session = this.sessions.get(sessionId);
@@ -350,12 +411,32 @@ export class TerminalService extends EventEmitter {
clearTimeout(session.resizeDebounceTimeout);
session.resizeDebounceTimeout = null;
}
- session.pty.kill();
- this.sessions.delete(sessionId);
- console.log(`[Terminal] Session ${sessionId} killed`);
+
+ // First try graceful SIGTERM to allow process cleanup
+ console.log(`[Terminal] Session ${sessionId} sending SIGTERM`);
+ session.pty.kill('SIGTERM');
+
+ // Schedule SIGKILL fallback if process doesn't exit gracefully
+ // The onExit handler will remove session from map when it actually exits
+ setTimeout(() => {
+ if (this.sessions.has(sessionId)) {
+ console.log(`[Terminal] Session ${sessionId} still alive after SIGTERM, sending SIGKILL`);
+ try {
+ session.pty.kill('SIGKILL');
+ } catch {
+ // Process may have already exited
+ }
+ // Force remove from map if still present
+ this.sessions.delete(sessionId);
+ }
+ }, 1000);
+
+ console.log(`[Terminal] Session ${sessionId} kill initiated`);
return true;
} catch (error) {
console.error(`[Terminal] Error killing session ${sessionId}:`, error);
+ // Still try to remove from map even if kill fails
+ this.sessions.delete(sessionId);
return false;
}
}
@@ -386,7 +467,7 @@ export class TerminalService extends EventEmitter {
// Clear any pending output that hasn't been flushed yet
// This data is already in scrollbackBuffer
- session.outputBuffer = "";
+ session.outputBuffer = '';
if (session.flushTimeout) {
clearTimeout(session.flushTimeout);
session.flushTimeout = null;
diff --git a/apps/server/src/types/settings.ts b/apps/server/src/types/settings.ts
index ef9f32d6..4b4fa3ac 100644
--- a/apps/server/src/types/settings.ts
+++ b/apps/server/src/types/settings.ts
@@ -22,7 +22,7 @@ export type {
BoardBackgroundSettings,
WorktreeInfo,
ProjectSettings,
-} from "@automaker/types";
+} from '@automaker/types';
export {
DEFAULT_KEYBOARD_SHORTCUTS,
@@ -32,4 +32,4 @@ export {
SETTINGS_VERSION,
CREDENTIALS_VERSION,
PROJECT_SETTINGS_VERSION,
-} from "@automaker/types";
+} from '@automaker/types';
diff --git a/apps/server/tests/fixtures/images.ts b/apps/server/tests/fixtures/images.ts
index b14f4adf..f7e768c6 100644
--- a/apps/server/tests/fixtures/images.ts
+++ b/apps/server/tests/fixtures/images.ts
@@ -4,11 +4,11 @@
// 1x1 transparent PNG base64 data
export const pngBase64Fixture =
- "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
export const imageDataFixture = {
base64: pngBase64Fixture,
- mimeType: "image/png",
- filename: "test.png",
- originalPath: "/path/to/test.png",
+ mimeType: 'image/png',
+ filename: 'test.png',
+ originalPath: '/path/to/test.png',
};
diff --git a/apps/server/tests/fixtures/messages.ts b/apps/server/tests/fixtures/messages.ts
index 731131e1..56eb75f7 100644
--- a/apps/server/tests/fixtures/messages.ts
+++ b/apps/server/tests/fixtures/messages.ts
@@ -2,38 +2,33 @@
* Message fixtures for testing providers and lib utilities
*/
-import type {
- ConversationMessage,
- ProviderMessage,
- ContentBlock,
-} from "@automaker/types";
+import type { ConversationMessage, ProviderMessage, ContentBlock } from '@automaker/types';
export const conversationHistoryFixture: ConversationMessage[] = [
{
- role: "user",
- content: "Hello, can you help me?",
+ role: 'user',
+ content: 'Hello, can you help me?',
},
{
- role: "assistant",
- content: "Of course! How can I assist you today?",
+ role: 'assistant',
+ content: 'Of course! How can I assist you today?',
},
{
- role: "user",
+ role: 'user',
content: [
- { type: "text", text: "What is in this image?" },
+ { type: 'text', text: 'What is in this image?' },
{
- type: "image",
- source: { type: "base64", media_type: "image/png", data: "base64data" },
+ type: 'image',
+ source: { type: 'base64', media_type: 'image/png', data: 'base64data' },
},
],
},
];
export const claudeProviderMessageFixture: ProviderMessage = {
- type: "assistant",
+ type: 'assistant',
message: {
- role: "assistant",
- content: [{ type: "text", text: "This is a test response" }],
+ role: 'assistant',
+ content: [{ type: 'text', text: 'This is a test response' }],
},
};
-
diff --git a/apps/server/tests/integration/helpers/git-test-repo.ts b/apps/server/tests/integration/helpers/git-test-repo.ts
index f307bbb1..4ec95926 100644
--- a/apps/server/tests/integration/helpers/git-test-repo.ts
+++ b/apps/server/tests/integration/helpers/git-test-repo.ts
@@ -1,11 +1,11 @@
/**
* Helper for creating test git repositories for integration tests
*/
-import { exec } from "child_process";
-import { promisify } from "util";
-import * as fs from "fs/promises";
-import * as path from "path";
-import * as os from "os";
+import { exec } from 'child_process';
+import { promisify } from 'util';
+import * as fs from 'fs/promises';
+import * as path from 'path';
+import * as os from 'os';
const execAsync = promisify(exec);
@@ -18,36 +18,36 @@ export interface TestRepo {
* Create a temporary git repository for testing
*/
export async function createTestGitRepo(): Promise {
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "automaker-test-"));
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'automaker-test-'));
// Initialize git repo
- await execAsync("git init", { cwd: tmpDir });
+ await execAsync('git init', { cwd: tmpDir });
await execAsync('git config user.email "test@example.com"', { cwd: tmpDir });
await execAsync('git config user.name "Test User"', { cwd: tmpDir });
// Create initial commit
- await fs.writeFile(path.join(tmpDir, "README.md"), "# Test Project\n");
- await execAsync("git add .", { cwd: tmpDir });
+ await fs.writeFile(path.join(tmpDir, 'README.md'), '# Test Project\n');
+ await execAsync('git add .', { cwd: tmpDir });
await execAsync('git commit -m "Initial commit"', { cwd: tmpDir });
// Create main branch explicitly
- await execAsync("git branch -M main", { cwd: tmpDir });
+ await execAsync('git branch -M main', { cwd: tmpDir });
return {
path: tmpDir,
cleanup: async () => {
try {
// Remove all worktrees first
- const { stdout } = await execAsync("git worktree list --porcelain", {
+ const { stdout } = await execAsync('git worktree list --porcelain', {
cwd: tmpDir,
- }).catch(() => ({ stdout: "" }));
+ }).catch(() => ({ stdout: '' }));
const worktrees = stdout
- .split("\n\n")
+ .split('\n\n')
.slice(1) // Skip main worktree
.map((block) => {
- const pathLine = block.split("\n").find((line) => line.startsWith("worktree "));
- return pathLine ? pathLine.replace("worktree ", "") : null;
+ const pathLine = block.split('\n').find((line) => line.startsWith('worktree '));
+ return pathLine ? pathLine.replace('worktree ', '') : null;
})
.filter(Boolean);
@@ -64,7 +64,7 @@ export async function createTestGitRepo(): Promise {
// Remove the repository
await fs.rm(tmpDir, { recursive: true, force: true });
} catch (error) {
- console.error("Failed to cleanup test repo:", error);
+ console.error('Failed to cleanup test repo:', error);
}
},
};
@@ -78,24 +78,21 @@ export async function createTestFeature(
featureId: string,
featureData: any
): Promise {
- const featuresDir = path.join(repoPath, ".automaker", "features");
+ const featuresDir = path.join(repoPath, '.automaker', 'features');
const featureDir = path.join(featuresDir, featureId);
await fs.mkdir(featureDir, { recursive: true });
- await fs.writeFile(
- path.join(featureDir, "feature.json"),
- JSON.stringify(featureData, null, 2)
- );
+ await fs.writeFile(path.join(featureDir, 'feature.json'), JSON.stringify(featureData, null, 2));
}
/**
* Get list of git branches
*/
export async function listBranches(repoPath: string): Promise {
- const { stdout } = await execAsync("git branch --list", { cwd: repoPath });
+ const { stdout } = await execAsync('git branch --list', { cwd: repoPath });
return stdout
- .split("\n")
- .map((line) => line.trim().replace(/^[*+]\s*/, ""))
+ .split('\n')
+ .map((line) => line.trim().replace(/^[*+]\s*/, ''))
.filter(Boolean);
}
@@ -104,16 +101,16 @@ export async function listBranches(repoPath: string): Promise {
*/
export async function listWorktrees(repoPath: string): Promise {
try {
- const { stdout } = await execAsync("git worktree list --porcelain", {
+ const { stdout } = await execAsync('git worktree list --porcelain', {
cwd: repoPath,
});
return stdout
- .split("\n\n")
+ .split('\n\n')
.slice(1) // Skip main worktree
.map((block) => {
- const pathLine = block.split("\n").find((line) => line.startsWith("worktree "));
- return pathLine ? pathLine.replace("worktree ", "") : null;
+ const pathLine = block.split('\n').find((line) => line.startsWith('worktree '));
+ return pathLine ? pathLine.replace('worktree ', '') : null;
})
.filter(Boolean) as string[];
} catch {
@@ -124,10 +121,7 @@ export async function listWorktrees(repoPath: string): Promise {
/**
* Check if a branch exists
*/
-export async function branchExists(
- repoPath: string,
- branchName: string
-): Promise {
+export async function branchExists(repoPath: string, branchName: string): Promise {
const branches = await listBranches(repoPath);
return branches.includes(branchName);
}
@@ -135,10 +129,7 @@ export async function branchExists(
/**
* Check if a worktree exists
*/
-export async function worktreeExists(
- repoPath: string,
- worktreePath: string
-): Promise {
+export async function worktreeExists(repoPath: string, worktreePath: string): Promise {
const worktrees = await listWorktrees(repoPath);
return worktrees.some((wt) => wt === worktreePath);
}
diff --git a/apps/server/tests/integration/routes/worktree/create.integration.test.ts b/apps/server/tests/integration/routes/worktree/create.integration.test.ts
index 03b85e7e..433b610a 100644
--- a/apps/server/tests/integration/routes/worktree/create.integration.test.ts
+++ b/apps/server/tests/integration/routes/worktree/create.integration.test.ts
@@ -1,22 +1,20 @@
-import { describe, it, expect, vi, afterEach } from "vitest";
-import { createCreateHandler } from "@/routes/worktree/routes/create.js";
-import { AUTOMAKER_INITIAL_COMMIT_MESSAGE } from "@/routes/worktree/common.js";
-import { exec } from "child_process";
-import { promisify } from "util";
-import * as fs from "fs/promises";
-import * as os from "os";
-import * as path from "path";
+import { describe, it, expect, vi, afterEach } from 'vitest';
+import { createCreateHandler } from '@/routes/worktree/routes/create.js';
+import { AUTOMAKER_INITIAL_COMMIT_MESSAGE } from '@/routes/worktree/common.js';
+import { exec } from 'child_process';
+import { promisify } from 'util';
+import * as fs from 'fs/promises';
+import * as os from 'os';
+import * as path from 'path';
const execAsync = promisify(exec);
-describe("worktree create route - repositories without commits", () => {
+describe('worktree create route - repositories without commits', () => {
let repoPath: string | null = null;
async function initRepoWithoutCommit() {
- repoPath = await fs.mkdtemp(
- path.join(os.tmpdir(), "automaker-no-commit-")
- );
- await execAsync("git init", { cwd: repoPath });
+ repoPath = await fs.mkdtemp(path.join(os.tmpdir(), 'automaker-no-commit-'));
+ await execAsync('git init', { cwd: repoPath });
await execAsync('git config user.email "test@example.com"', {
cwd: repoPath,
});
@@ -32,14 +30,14 @@ describe("worktree create route - repositories without commits", () => {
repoPath = null;
});
- it("creates an initial commit before adding a worktree when HEAD is missing", async () => {
+ it('creates an initial commit before adding a worktree when HEAD is missing', async () => {
await initRepoWithoutCommit();
const handler = createCreateHandler();
const json = vi.fn();
const status = vi.fn().mockReturnThis();
const req = {
- body: { projectPath: repoPath, branchName: "feature/no-head" },
+ body: { projectPath: repoPath, branchName: 'feature/no-head' },
} as any;
const res = {
json,
@@ -53,17 +51,12 @@ describe("worktree create route - repositories without commits", () => {
const payload = json.mock.calls[0][0];
expect(payload.success).toBe(true);
- const { stdout: commitCount } = await execAsync(
- "git rev-list --count HEAD",
- { cwd: repoPath! }
- );
+ const { stdout: commitCount } = await execAsync('git rev-list --count HEAD', {
+ cwd: repoPath!,
+ });
expect(Number(commitCount.trim())).toBeGreaterThan(0);
- const { stdout: latestMessage } = await execAsync(
- "git log -1 --pretty=%B",
- { cwd: repoPath! }
- );
+ const { stdout: latestMessage } = await execAsync('git log -1 --pretty=%B', { cwd: repoPath! });
expect(latestMessage.trim()).toBe(AUTOMAKER_INITIAL_COMMIT_MESSAGE);
});
});
-
diff --git a/apps/server/tests/integration/services/auto-mode-service.integration.test.ts b/apps/server/tests/integration/services/auto-mode-service.integration.test.ts
index ebf0857f..d9d6ee13 100644
--- a/apps/server/tests/integration/services/auto-mode-service.integration.test.ts
+++ b/apps/server/tests/integration/services/auto-mode-service.integration.test.ts
@@ -1,7 +1,7 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
-import { AutoModeService } from "@/services/auto-mode-service.js";
-import { ProviderFactory } from "@/providers/provider-factory.js";
-import { FeatureLoader } from "@/services/feature-loader.js";
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { AutoModeService } from '@/services/auto-mode-service.js';
+import { ProviderFactory } from '@/providers/provider-factory.js';
+import { FeatureLoader } from '@/services/feature-loader.js';
import {
createTestGitRepo,
createTestFeature,
@@ -10,17 +10,17 @@ import {
branchExists,
worktreeExists,
type TestRepo,
-} from "../helpers/git-test-repo.js";
-import * as fs from "fs/promises";
-import * as path from "path";
-import { exec } from "child_process";
-import { promisify } from "util";
+} from '../helpers/git-test-repo.js';
+import * as fs from 'fs/promises';
+import * as path from 'path';
+import { exec } from 'child_process';
+import { promisify } from 'util';
const execAsync = promisify(exec);
-vi.mock("@/providers/provider-factory.js");
+vi.mock('@/providers/provider-factory.js');
-describe("auto-mode-service.ts (integration)", () => {
+describe('auto-mode-service.ts (integration)', () => {
let service: AutoModeService;
let testRepo: TestRepo;
let featureLoader: FeatureLoader;
@@ -46,22 +46,22 @@ describe("auto-mode-service.ts (integration)", () => {
}
});
- describe("worktree operations", () => {
- it("should use existing git worktree for feature", async () => {
- const branchName = "feature/test-feature-1";
-
+ describe('worktree operations', () => {
+ it('should use existing git worktree for feature', async () => {
+ const branchName = 'feature/test-feature-1';
+
// Create a test feature with branchName set
- await createTestFeature(testRepo.path, "test-feature-1", {
- id: "test-feature-1",
- category: "test",
- description: "Test feature",
- status: "pending",
+ await createTestFeature(testRepo.path, 'test-feature-1', {
+ id: 'test-feature-1',
+ category: 'test',
+ description: 'Test feature',
+ status: 'pending',
branchName: branchName,
});
// Create worktree before executing (worktrees are now created when features are added/edited)
- const worktreesDir = path.join(testRepo.path, ".worktrees");
- const worktreePath = path.join(worktreesDir, "test-feature-1");
+ const worktreesDir = path.join(testRepo.path, '.worktrees');
+ const worktreePath = path.join(worktreesDir, 'test-feature-1');
await fs.mkdir(worktreesDir, { recursive: true });
await execAsync(`git worktree add -b ${branchName} "${worktreePath}" HEAD`, {
cwd: testRepo.path,
@@ -69,30 +69,28 @@ describe("auto-mode-service.ts (integration)", () => {
// Mock provider to complete quickly
const mockProvider = {
- getName: () => "claude",
+ getName: () => 'claude',
executeQuery: async function* () {
yield {
- type: "assistant",
+ type: 'assistant',
message: {
- role: "assistant",
- content: [{ type: "text", text: "Feature implemented" }],
+ role: 'assistant',
+ content: [{ type: 'text', text: 'Feature implemented' }],
},
};
yield {
- type: "result",
- subtype: "success",
+ type: 'result',
+ subtype: 'success',
};
},
};
- vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(
- mockProvider as any
- );
+ vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
// Execute feature with worktrees enabled
await service.executeFeature(
testRepo.path,
- "test-feature-1",
+ 'test-feature-1',
true, // useWorktrees
false // isAutoMode
);
@@ -107,8 +105,8 @@ describe("auto-mode-service.ts (integration)", () => {
const worktrees = await listWorktrees(testRepo.path);
expect(worktrees.length).toBeGreaterThan(0);
// Verify that at least one worktree path contains our feature ID
- const worktreePathsMatch = worktrees.some(wt =>
- wt.includes("test-feature-1") || wt.includes(".worktrees")
+ const worktreePathsMatch = worktrees.some(
+ (wt) => wt.includes('test-feature-1') || wt.includes('.worktrees')
);
expect(worktreePathsMatch).toBe(true);
@@ -116,243 +114,200 @@ describe("auto-mode-service.ts (integration)", () => {
// This is expected behavior - manual cleanup is required
}, 30000);
- it("should handle error gracefully", async () => {
- await createTestFeature(testRepo.path, "test-feature-error", {
- id: "test-feature-error",
- category: "test",
- description: "Test feature that errors",
- status: "pending",
+ it('should handle error gracefully', async () => {
+ await createTestFeature(testRepo.path, 'test-feature-error', {
+ id: 'test-feature-error',
+ category: 'test',
+ description: 'Test feature that errors',
+ status: 'pending',
});
// Mock provider that throws error
const mockProvider = {
- getName: () => "claude",
+ getName: () => 'claude',
executeQuery: async function* () {
- throw new Error("Provider error");
+ throw new Error('Provider error');
},
};
- vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(
- mockProvider as any
- );
+ vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
// Execute feature (should handle error)
- await service.executeFeature(
- testRepo.path,
- "test-feature-error",
- true,
- false
- );
+ await service.executeFeature(testRepo.path, 'test-feature-error', true, false);
// Verify feature status was updated to backlog (error status)
- const feature = await featureLoader.get(
- testRepo.path,
- "test-feature-error"
- );
- expect(feature?.status).toBe("backlog");
+ const feature = await featureLoader.get(testRepo.path, 'test-feature-error');
+ expect(feature?.status).toBe('backlog');
}, 30000);
- it("should work without worktrees", async () => {
- await createTestFeature(testRepo.path, "test-no-worktree", {
- id: "test-no-worktree",
- category: "test",
- description: "Test without worktree",
- status: "pending",
+ it('should work without worktrees', async () => {
+ await createTestFeature(testRepo.path, 'test-no-worktree', {
+ id: 'test-no-worktree',
+ category: 'test',
+ description: 'Test without worktree',
+ status: 'pending',
});
const mockProvider = {
- getName: () => "claude",
+ getName: () => 'claude',
executeQuery: async function* () {
yield {
- type: "result",
- subtype: "success",
+ type: 'result',
+ subtype: 'success',
};
},
};
- vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(
- mockProvider as any
- );
+ vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
// Execute without worktrees
await service.executeFeature(
testRepo.path,
- "test-no-worktree",
+ 'test-no-worktree',
false, // useWorktrees = false
false
);
// Feature should be updated successfully
- const feature = await featureLoader.get(
- testRepo.path,
- "test-no-worktree"
- );
- expect(feature?.status).toBe("waiting_approval");
+ const feature = await featureLoader.get(testRepo.path, 'test-no-worktree');
+ expect(feature?.status).toBe('waiting_approval');
}, 30000);
});
- describe("feature execution", () => {
- it("should execute feature and update status", async () => {
- await createTestFeature(testRepo.path, "feature-exec-1", {
- id: "feature-exec-1",
- category: "ui",
- description: "Execute this feature",
- status: "pending",
+ describe('feature execution', () => {
+ it('should execute feature and update status', async () => {
+ await createTestFeature(testRepo.path, 'feature-exec-1', {
+ id: 'feature-exec-1',
+ category: 'ui',
+ description: 'Execute this feature',
+ status: 'pending',
});
const mockProvider = {
- getName: () => "claude",
+ getName: () => 'claude',
executeQuery: async function* () {
yield {
- type: "assistant",
+ type: 'assistant',
message: {
- role: "assistant",
- content: [{ type: "text", text: "Implemented the feature" }],
+ role: 'assistant',
+ content: [{ type: 'text', text: 'Implemented the feature' }],
},
};
yield {
- type: "result",
- subtype: "success",
+ type: 'result',
+ subtype: 'success',
};
},
};
- vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(
- mockProvider as any
- );
+ vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
await service.executeFeature(
testRepo.path,
- "feature-exec-1",
+ 'feature-exec-1',
false, // Don't use worktrees so agent output is saved to main project
false
);
// Check feature status was updated
- const feature = await featureLoader.get(testRepo.path, "feature-exec-1");
- expect(feature?.status).toBe("waiting_approval");
+ const feature = await featureLoader.get(testRepo.path, 'feature-exec-1');
+ expect(feature?.status).toBe('waiting_approval');
// Check agent output was saved
- const agentOutput = await featureLoader.getAgentOutput(
- testRepo.path,
- "feature-exec-1"
- );
+ const agentOutput = await featureLoader.getAgentOutput(testRepo.path, 'feature-exec-1');
expect(agentOutput).toBeTruthy();
- expect(agentOutput).toContain("Implemented the feature");
+ expect(agentOutput).toContain('Implemented the feature');
}, 30000);
- it("should handle feature not found", async () => {
+ it('should handle feature not found', async () => {
const mockProvider = {
- getName: () => "claude",
+ getName: () => 'claude',
executeQuery: async function* () {
yield {
- type: "result",
- subtype: "success",
+ type: 'result',
+ subtype: 'success',
};
},
};
- vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(
- mockProvider as any
- );
+ vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
// Try to execute non-existent feature
- await service.executeFeature(
- testRepo.path,
- "nonexistent-feature",
- true,
- false
- );
+ await service.executeFeature(testRepo.path, 'nonexistent-feature', true, false);
// Should emit error event
expect(mockEvents.emit).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
- featureId: "nonexistent-feature",
- error: expect.stringContaining("not found"),
+ featureId: 'nonexistent-feature',
+ error: expect.stringContaining('not found'),
})
);
}, 30000);
- it("should prevent duplicate feature execution", async () => {
- await createTestFeature(testRepo.path, "feature-dup", {
- id: "feature-dup",
- category: "test",
- description: "Duplicate test",
- status: "pending",
+ it('should prevent duplicate feature execution', async () => {
+ await createTestFeature(testRepo.path, 'feature-dup', {
+ id: 'feature-dup',
+ category: 'test',
+ description: 'Duplicate test',
+ status: 'pending',
});
const mockProvider = {
- getName: () => "claude",
+ getName: () => 'claude',
executeQuery: async function* () {
// Simulate slow execution
await new Promise((resolve) => setTimeout(resolve, 500));
yield {
- type: "result",
- subtype: "success",
+ type: 'result',
+ subtype: 'success',
};
},
};
- vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(
- mockProvider as any
- );
+ vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
// Start first execution
- const promise1 = service.executeFeature(
- testRepo.path,
- "feature-dup",
- false,
- false
- );
+ const promise1 = service.executeFeature(testRepo.path, 'feature-dup', false, false);
// Try to start second execution (should throw)
await expect(
- service.executeFeature(testRepo.path, "feature-dup", false, false)
- ).rejects.toThrow("already running");
+ service.executeFeature(testRepo.path, 'feature-dup', false, false)
+ ).rejects.toThrow('already running');
await promise1;
}, 30000);
- it("should use feature-specific model", async () => {
- await createTestFeature(testRepo.path, "feature-model", {
- id: "feature-model",
- category: "test",
- description: "Model test",
- status: "pending",
- model: "claude-sonnet-4-20250514",
+ it('should use feature-specific model', async () => {
+ await createTestFeature(testRepo.path, 'feature-model', {
+ id: 'feature-model',
+ category: 'test',
+ description: 'Model test',
+ status: 'pending',
+ model: 'claude-sonnet-4-20250514',
});
const mockProvider = {
- getName: () => "claude",
+ getName: () => 'claude',
executeQuery: async function* () {
yield {
- type: "result",
- subtype: "success",
+ type: 'result',
+ subtype: 'success',
};
},
};
- vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(
- mockProvider as any
- );
+ vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
- await service.executeFeature(
- testRepo.path,
- "feature-model",
- false,
- false
- );
+ await service.executeFeature(testRepo.path, 'feature-model', false, false);
// Should have used claude-sonnet-4-20250514
- expect(ProviderFactory.getProviderForModel).toHaveBeenCalledWith(
- "claude-sonnet-4-20250514"
- );
+ expect(ProviderFactory.getProviderForModel).toHaveBeenCalledWith('claude-sonnet-4-20250514');
}, 30000);
});
- describe("auto loop", () => {
- it("should start and stop auto loop", async () => {
+ describe('auto loop', () => {
+ it('should start and stop auto loop', async () => {
const startPromise = service.startAutoLoop(testRepo.path, 2);
// Give it time to start
@@ -365,35 +320,33 @@ describe("auto-mode-service.ts (integration)", () => {
await startPromise.catch(() => {}); // Cleanup
}, 10000);
- it("should process pending features in auto loop", async () => {
+ it('should process pending features in auto loop', async () => {
// Create multiple pending features
- await createTestFeature(testRepo.path, "auto-1", {
- id: "auto-1",
- category: "test",
- description: "Auto feature 1",
- status: "pending",
+ await createTestFeature(testRepo.path, 'auto-1', {
+ id: 'auto-1',
+ category: 'test',
+ description: 'Auto feature 1',
+ status: 'pending',
});
- await createTestFeature(testRepo.path, "auto-2", {
- id: "auto-2",
- category: "test",
- description: "Auto feature 2",
- status: "pending",
+ await createTestFeature(testRepo.path, 'auto-2', {
+ id: 'auto-2',
+ category: 'test',
+ description: 'Auto feature 2',
+ status: 'pending',
});
const mockProvider = {
- getName: () => "claude",
+ getName: () => 'claude',
executeQuery: async function* () {
yield {
- type: "result",
- subtype: "success",
+ type: 'result',
+ subtype: 'success',
};
},
};
- vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(
- mockProvider as any
- );
+ vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
// Start auto loop
const startPromise = service.startAutoLoop(testRepo.path, 2);
@@ -406,25 +359,25 @@ describe("auto-mode-service.ts (integration)", () => {
await startPromise.catch(() => {});
// Check that features were updated
- const feature1 = await featureLoader.get(testRepo.path, "auto-1");
- const feature2 = await featureLoader.get(testRepo.path, "auto-2");
+ const feature1 = await featureLoader.get(testRepo.path, 'auto-1');
+ const feature2 = await featureLoader.get(testRepo.path, 'auto-2');
// At least one should have been processed
const processedCount = [feature1, feature2].filter(
- (f) => f?.status === "waiting_approval" || f?.status === "in_progress"
+ (f) => f?.status === 'waiting_approval' || f?.status === 'in_progress'
).length;
expect(processedCount).toBeGreaterThan(0);
}, 15000);
- it("should respect max concurrency", async () => {
+ it('should respect max concurrency', async () => {
// Create 5 features
for (let i = 1; i <= 5; i++) {
await createTestFeature(testRepo.path, `concurrent-${i}`, {
id: `concurrent-${i}`,
- category: "test",
+ category: 'test',
description: `Concurrent feature ${i}`,
- status: "pending",
+ status: 'pending',
});
}
@@ -432,7 +385,7 @@ describe("auto-mode-service.ts (integration)", () => {
let maxConcurrent = 0;
const mockProvider = {
- getName: () => "claude",
+ getName: () => 'claude',
executeQuery: async function* () {
concurrentCount++;
maxConcurrent = Math.max(maxConcurrent, concurrentCount);
@@ -443,15 +396,13 @@ describe("auto-mode-service.ts (integration)", () => {
concurrentCount--;
yield {
- type: "result",
- subtype: "success",
+ type: 'result',
+ subtype: 'success',
};
},
};
- vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(
- mockProvider as any
- );
+ vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
// Start with max concurrency of 2
const startPromise = service.startAutoLoop(testRepo.path, 2);
@@ -466,7 +417,7 @@ describe("auto-mode-service.ts (integration)", () => {
expect(maxConcurrent).toBeLessThanOrEqual(2);
}, 15000);
- it("should emit auto mode events", async () => {
+ it('should emit auto mode events', async () => {
const startPromise = service.startAutoLoop(testRepo.path, 1);
// Wait for start event
@@ -474,7 +425,7 @@ describe("auto-mode-service.ts (integration)", () => {
// Check start event was emitted
const startEvent = mockEvents.emit.mock.calls.find((call) =>
- call[1]?.message?.includes("Auto mode started")
+ call[1]?.message?.includes('Auto mode started')
);
expect(startEvent).toBeTruthy();
@@ -484,74 +435,69 @@ describe("auto-mode-service.ts (integration)", () => {
// Check stop event was emitted (emitted immediately by stopAutoLoop)
const stopEvent = mockEvents.emit.mock.calls.find(
(call) =>
- call[1]?.type === "auto_mode_stopped" ||
- call[1]?.message?.includes("Auto mode stopped")
+ call[1]?.type === 'auto_mode_stopped' || call[1]?.message?.includes('Auto mode stopped')
);
expect(stopEvent).toBeTruthy();
}, 10000);
});
- describe("error handling", () => {
- it("should handle provider errors gracefully", async () => {
- await createTestFeature(testRepo.path, "error-feature", {
- id: "error-feature",
- category: "test",
- description: "Error test",
- status: "pending",
+ describe('error handling', () => {
+ it('should handle provider errors gracefully', async () => {
+ await createTestFeature(testRepo.path, 'error-feature', {
+ id: 'error-feature',
+ category: 'test',
+ description: 'Error test',
+ status: 'pending',
});
const mockProvider = {
- getName: () => "claude",
+ getName: () => 'claude',
executeQuery: async function* () {
- throw new Error("Provider execution failed");
+ throw new Error('Provider execution failed');
},
};
- vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(
- mockProvider as any
- );
+ vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
// Should not throw
- await service.executeFeature(testRepo.path, "error-feature", true, false);
+ await service.executeFeature(testRepo.path, 'error-feature', true, false);
// Feature should be marked as backlog (error status)
- const feature = await featureLoader.get(testRepo.path, "error-feature");
- expect(feature?.status).toBe("backlog");
+ const feature = await featureLoader.get(testRepo.path, 'error-feature');
+ expect(feature?.status).toBe('backlog');
}, 30000);
- it("should continue auto loop after feature error", async () => {
- await createTestFeature(testRepo.path, "fail-1", {
- id: "fail-1",
- category: "test",
- description: "Will fail",
- status: "pending",
+ it('should continue auto loop after feature error', async () => {
+ await createTestFeature(testRepo.path, 'fail-1', {
+ id: 'fail-1',
+ category: 'test',
+ description: 'Will fail',
+ status: 'pending',
});
- await createTestFeature(testRepo.path, "success-1", {
- id: "success-1",
- category: "test",
- description: "Will succeed",
- status: "pending",
+ await createTestFeature(testRepo.path, 'success-1', {
+ id: 'success-1',
+ category: 'test',
+ description: 'Will succeed',
+ status: 'pending',
});
let callCount = 0;
const mockProvider = {
- getName: () => "claude",
+ getName: () => 'claude',
executeQuery: async function* () {
callCount++;
if (callCount === 1) {
- throw new Error("First feature fails");
+ throw new Error('First feature fails');
}
yield {
- type: "result",
- subtype: "success",
+ type: 'result',
+ subtype: 'success',
};
},
};
- vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(
- mockProvider as any
- );
+ vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
const startPromise = service.startAutoLoop(testRepo.path, 1);
@@ -566,200 +512,177 @@ describe("auto-mode-service.ts (integration)", () => {
}, 15000);
});
- describe("planning mode", () => {
- it("should execute feature with skip planning mode", async () => {
- await createTestFeature(testRepo.path, "skip-plan-feature", {
- id: "skip-plan-feature",
- category: "test",
- description: "Feature with skip planning",
- status: "pending",
- planningMode: "skip",
+ describe('planning mode', () => {
+ it('should execute feature with skip planning mode', async () => {
+ await createTestFeature(testRepo.path, 'skip-plan-feature', {
+ id: 'skip-plan-feature',
+ category: 'test',
+ description: 'Feature with skip planning',
+ status: 'pending',
+ planningMode: 'skip',
});
const mockProvider = {
- getName: () => "claude",
+ getName: () => 'claude',
executeQuery: async function* () {
yield {
- type: "assistant",
+ type: 'assistant',
message: {
- role: "assistant",
- content: [{ type: "text", text: "Feature implemented" }],
+ role: 'assistant',
+ content: [{ type: 'text', text: 'Feature implemented' }],
},
};
yield {
- type: "result",
- subtype: "success",
+ type: 'result',
+ subtype: 'success',
};
},
};
- vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(
- mockProvider as any
- );
+ vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
- await service.executeFeature(
- testRepo.path,
- "skip-plan-feature",
- false,
- false
- );
+ await service.executeFeature(testRepo.path, 'skip-plan-feature', false, false);
- const feature = await featureLoader.get(testRepo.path, "skip-plan-feature");
- expect(feature?.status).toBe("waiting_approval");
+ const feature = await featureLoader.get(testRepo.path, 'skip-plan-feature');
+ expect(feature?.status).toBe('waiting_approval');
}, 30000);
- it("should execute feature with lite planning mode without approval", async () => {
- await createTestFeature(testRepo.path, "lite-plan-feature", {
- id: "lite-plan-feature",
- category: "test",
- description: "Feature with lite planning",
- status: "pending",
- planningMode: "lite",
+ it('should execute feature with lite planning mode without approval', async () => {
+ await createTestFeature(testRepo.path, 'lite-plan-feature', {
+ id: 'lite-plan-feature',
+ category: 'test',
+ description: 'Feature with lite planning',
+ status: 'pending',
+ planningMode: 'lite',
requirePlanApproval: false,
});
const mockProvider = {
- getName: () => "claude",
+ getName: () => 'claude',
executeQuery: async function* () {
yield {
- type: "assistant",
+ type: 'assistant',
message: {
- role: "assistant",
- content: [{ type: "text", text: "[PLAN_GENERATED] Planning outline complete.\n\nFeature implemented" }],
+ role: 'assistant',
+ content: [
+ {
+ type: 'text',
+ text: '[PLAN_GENERATED] Planning outline complete.\n\nFeature implemented',
+ },
+ ],
},
};
yield {
- type: "result",
- subtype: "success",
+ type: 'result',
+ subtype: 'success',
};
},
};
- vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(
- mockProvider as any
- );
+ vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
- await service.executeFeature(
- testRepo.path,
- "lite-plan-feature",
- false,
- false
- );
+ await service.executeFeature(testRepo.path, 'lite-plan-feature', false, false);
- const feature = await featureLoader.get(testRepo.path, "lite-plan-feature");
- expect(feature?.status).toBe("waiting_approval");
+ const feature = await featureLoader.get(testRepo.path, 'lite-plan-feature');
+ expect(feature?.status).toBe('waiting_approval');
}, 30000);
- it("should emit planning_started event for spec mode", async () => {
- await createTestFeature(testRepo.path, "spec-plan-feature", {
- id: "spec-plan-feature",
- category: "test",
- description: "Feature with spec planning",
- status: "pending",
- planningMode: "spec",
+ it('should emit planning_started event for spec mode', async () => {
+ await createTestFeature(testRepo.path, 'spec-plan-feature', {
+ id: 'spec-plan-feature',
+ category: 'test',
+ description: 'Feature with spec planning',
+ status: 'pending',
+ planningMode: 'spec',
requirePlanApproval: false,
});
const mockProvider = {
- getName: () => "claude",
+ getName: () => 'claude',
executeQuery: async function* () {
yield {
- type: "assistant",
+ type: 'assistant',
message: {
- role: "assistant",
- content: [{ type: "text", text: "Spec generated\n\n[SPEC_GENERATED] Review the spec." }],
+ role: 'assistant',
+ content: [
+ { type: 'text', text: 'Spec generated\n\n[SPEC_GENERATED] Review the spec.' },
+ ],
},
};
yield {
- type: "result",
- subtype: "success",
+ type: 'result',
+ subtype: 'success',
};
},
};
- vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(
- mockProvider as any
- );
+ vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
- await service.executeFeature(
- testRepo.path,
- "spec-plan-feature",
- false,
- false
- );
+ await service.executeFeature(testRepo.path, 'spec-plan-feature', false, false);
// Check planning_started event was emitted
- const planningEvent = mockEvents.emit.mock.calls.find(
- (call) => call[1]?.mode === "spec"
- );
+ const planningEvent = mockEvents.emit.mock.calls.find((call) => call[1]?.mode === 'spec');
expect(planningEvent).toBeTruthy();
}, 30000);
- it("should handle feature with full planning mode", async () => {
- await createTestFeature(testRepo.path, "full-plan-feature", {
- id: "full-plan-feature",
- category: "test",
- description: "Feature with full planning",
- status: "pending",
- planningMode: "full",
+ it('should handle feature with full planning mode', async () => {
+ await createTestFeature(testRepo.path, 'full-plan-feature', {
+ id: 'full-plan-feature',
+ category: 'test',
+ description: 'Feature with full planning',
+ status: 'pending',
+ planningMode: 'full',
requirePlanApproval: false,
});
const mockProvider = {
- getName: () => "claude",
+ getName: () => 'claude',
executeQuery: async function* () {
yield {
- type: "assistant",
+ type: 'assistant',
message: {
- role: "assistant",
- content: [{ type: "text", text: "Full spec with phases\n\n[SPEC_GENERATED] Review." }],
+ role: 'assistant',
+ content: [
+ { type: 'text', text: 'Full spec with phases\n\n[SPEC_GENERATED] Review.' },
+ ],
},
};
yield {
- type: "result",
- subtype: "success",
+ type: 'result',
+ subtype: 'success',
};
},
};
- vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(
- mockProvider as any
- );
+ vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
- await service.executeFeature(
- testRepo.path,
- "full-plan-feature",
- false,
- false
- );
+ await service.executeFeature(testRepo.path, 'full-plan-feature', false, false);
// Check planning_started event was emitted with full mode
- const planningEvent = mockEvents.emit.mock.calls.find(
- (call) => call[1]?.mode === "full"
- );
+ const planningEvent = mockEvents.emit.mock.calls.find((call) => call[1]?.mode === 'full');
expect(planningEvent).toBeTruthy();
}, 30000);
- it("should track pending approval correctly", async () => {
+ it('should track pending approval correctly', async () => {
// Initially no pending approvals
- expect(service.hasPendingApproval("non-existent")).toBe(false);
+ expect(service.hasPendingApproval('non-existent')).toBe(false);
});
- it("should cancel pending approval gracefully", () => {
+ it('should cancel pending approval gracefully', () => {
// Should not throw when cancelling non-existent approval
- expect(() => service.cancelPlanApproval("non-existent")).not.toThrow();
+ expect(() => service.cancelPlanApproval('non-existent')).not.toThrow();
});
- it("should resolve approval with error for non-existent feature", async () => {
+ it('should resolve approval with error for non-existent feature', async () => {
const result = await service.resolvePlanApproval(
- "non-existent",
+ 'non-existent',
true,
undefined,
undefined,
undefined
);
expect(result.success).toBe(false);
- expect(result.error).toContain("No pending approval");
+ expect(result.error).toContain('No pending approval');
});
});
});
diff --git a/apps/server/tests/setup.ts b/apps/server/tests/setup.ts
index 2b00c614..15ecc9dc 100644
--- a/apps/server/tests/setup.ts
+++ b/apps/server/tests/setup.ts
@@ -3,11 +3,11 @@
* Runs before each test file
*/
-import { vi, beforeEach } from "vitest";
+import { vi, beforeEach } from 'vitest';
// Set test environment variables
-process.env.NODE_ENV = "test";
-process.env.DATA_DIR = "/tmp/test-data";
+process.env.NODE_ENV = 'test';
+process.env.DATA_DIR = '/tmp/test-data';
// Reset all mocks before each test
beforeEach(() => {
diff --git a/apps/server/tests/unit/lib/app-spec-format.test.ts b/apps/server/tests/unit/lib/app-spec-format.test.ts
index 43eb5359..eef78814 100644
--- a/apps/server/tests/unit/lib/app-spec-format.test.ts
+++ b/apps/server/tests/unit/lib/app-spec-format.test.ts
@@ -1,143 +1,137 @@
-import { describe, it, expect } from "vitest";
+import { describe, it, expect } from 'vitest';
import {
specToXml,
getStructuredSpecPromptInstruction,
getAppSpecFormatInstruction,
APP_SPEC_XML_FORMAT,
type SpecOutput,
-} from "@/lib/app-spec-format.js";
+} from '@/lib/app-spec-format.js';
-describe("app-spec-format.ts", () => {
- describe("specToXml", () => {
- it("should convert minimal spec to XML", () => {
+describe('app-spec-format.ts', () => {
+ describe('specToXml', () => {
+ it('should convert minimal spec to XML', () => {
const spec: SpecOutput = {
- project_name: "Test Project",
- overview: "A test project",
- technology_stack: ["TypeScript", "Node.js"],
- core_capabilities: ["Testing", "Development"],
- implemented_features: [
- { name: "Feature 1", description: "First feature" },
- ],
+ project_name: 'Test Project',
+ overview: 'A test project',
+ technology_stack: ['TypeScript', 'Node.js'],
+ core_capabilities: ['Testing', 'Development'],
+ implemented_features: [{ name: 'Feature 1', description: 'First feature' }],
};
const xml = specToXml(spec);
expect(xml).toContain('');
- expect(xml).toContain("");
- expect(xml).toContain("");
- expect(xml).toContain("Test Project");
- expect(xml).toContain("TypeScript");
- expect(xml).toContain("Testing");
+ expect(xml).toContain('');
+ expect(xml).toContain('');
+ expect(xml).toContain('Test Project');
+ expect(xml).toContain('TypeScript');
+ expect(xml).toContain('Testing');
});
- it("should escape XML special characters", () => {
+ it('should escape XML special characters', () => {
const spec: SpecOutput = {
- project_name: "Test & Project",
- overview: "Description with ",
- technology_stack: ["TypeScript"],
- core_capabilities: ["Cap"],
+ project_name: 'Test & Project',
+ overview: 'Description with ',
+ technology_stack: ['TypeScript'],
+ core_capabilities: ['Cap'],
implemented_features: [],
};
const xml = specToXml(spec);
- expect(xml).toContain("Test & Project");
- expect(xml).toContain("<tags>");
+ expect(xml).toContain('Test & Project');
+ expect(xml).toContain('<tags>');
});
- it("should include file_locations when provided", () => {
+ it('should include file_locations when provided', () => {
const spec: SpecOutput = {
- project_name: "Test",
- overview: "Test",
- technology_stack: ["TS"],
- core_capabilities: ["Cap"],
+ project_name: 'Test',
+ overview: 'Test',
+ technology_stack: ['TS'],
+ core_capabilities: ['Cap'],
implemented_features: [
{
- name: "Feature",
- description: "Desc",
- file_locations: ["src/index.ts"],
+ name: 'Feature',
+ description: 'Desc',
+ file_locations: ['src/index.ts'],
},
],
};
const xml = specToXml(spec);
- expect(xml).toContain("");
- expect(xml).toContain("src/index.ts");
+ expect(xml).toContain('');
+ expect(xml).toContain('src/index.ts');
});
- it("should not include file_locations when empty", () => {
+ it('should not include file_locations when empty', () => {
const spec: SpecOutput = {
- project_name: "Test",
- overview: "Test",
- technology_stack: ["TS"],
- core_capabilities: ["Cap"],
- implemented_features: [
- { name: "Feature", description: "Desc", file_locations: [] },
- ],
+ project_name: 'Test',
+ overview: 'Test',
+ technology_stack: ['TS'],
+ core_capabilities: ['Cap'],
+ implemented_features: [{ name: 'Feature', description: 'Desc', file_locations: [] }],
};
const xml = specToXml(spec);
- expect(xml).not.toContain("");
+ expect(xml).not.toContain('');
});
- it("should include additional_requirements when provided", () => {
+ it('should include additional_requirements when provided', () => {
const spec: SpecOutput = {
- project_name: "Test",
- overview: "Test",
- technology_stack: ["TS"],
- core_capabilities: ["Cap"],
+ project_name: 'Test',
+ overview: 'Test',
+ technology_stack: ['TS'],
+ core_capabilities: ['Cap'],
implemented_features: [],
- additional_requirements: ["Node.js 18+"],
+ additional_requirements: ['Node.js 18+'],
};
const xml = specToXml(spec);
- expect(xml).toContain("");
- expect(xml).toContain("Node.js 18+");
+ expect(xml).toContain('');
+ expect(xml).toContain('Node.js 18+');
});
- it("should include development_guidelines when provided", () => {
+ it('should include development_guidelines when provided', () => {
const spec: SpecOutput = {
- project_name: "Test",
- overview: "Test",
- technology_stack: ["TS"],
- core_capabilities: ["Cap"],
+ project_name: 'Test',
+ overview: 'Test',
+ technology_stack: ['TS'],
+ core_capabilities: ['Cap'],
implemented_features: [],
- development_guidelines: ["Use ESLint"],
+ development_guidelines: ['Use ESLint'],
};
const xml = specToXml(spec);
- expect(xml).toContain("");
- expect(xml).toContain("Use ESLint");
+ expect(xml).toContain('');
+ expect(xml).toContain('Use ESLint');
});
- it("should include implementation_roadmap when provided", () => {
+ it('should include implementation_roadmap when provided', () => {
const spec: SpecOutput = {
- project_name: "Test",
- overview: "Test",
- technology_stack: ["TS"],
- core_capabilities: ["Cap"],
+ project_name: 'Test',
+ overview: 'Test',
+ technology_stack: ['TS'],
+ core_capabilities: ['Cap'],
implemented_features: [],
- implementation_roadmap: [
- { phase: "Phase 1", status: "completed", description: "Setup" },
- ],
+ implementation_roadmap: [{ phase: 'Phase 1', status: 'completed', description: 'Setup' }],
};
const xml = specToXml(spec);
- expect(xml).toContain("");
- expect(xml).toContain("completed");
+ expect(xml).toContain('');
+ expect(xml).toContain('completed');
});
- it("should not include optional sections when empty", () => {
+ it('should not include optional sections when empty', () => {
const spec: SpecOutput = {
- project_name: "Test",
- overview: "Test",
- technology_stack: ["TS"],
- core_capabilities: ["Cap"],
+ project_name: 'Test',
+ overview: 'Test',
+ technology_stack: ['TS'],
+ core_capabilities: ['Cap'],
implemented_features: [],
additional_requirements: [],
development_guidelines: [],
@@ -146,44 +140,44 @@ describe("app-spec-format.ts", () => {
const xml = specToXml(spec);
- expect(xml).not.toContain("");
- expect(xml).not.toContain("");
- expect(xml).not.toContain("");
+ expect(xml).not.toContain('');
+ expect(xml).not.toContain('');
+ expect(xml).not.toContain('');
});
});
- describe("getStructuredSpecPromptInstruction", () => {
- it("should return non-empty prompt instruction", () => {
+ describe('getStructuredSpecPromptInstruction', () => {
+ it('should return non-empty prompt instruction', () => {
const instruction = getStructuredSpecPromptInstruction();
expect(instruction).toBeTruthy();
expect(instruction.length).toBeGreaterThan(100);
});
- it("should mention required fields", () => {
+ it('should mention required fields', () => {
const instruction = getStructuredSpecPromptInstruction();
- expect(instruction).toContain("project_name");
- expect(instruction).toContain("overview");
- expect(instruction).toContain("technology_stack");
+ expect(instruction).toContain('project_name');
+ expect(instruction).toContain('overview');
+ expect(instruction).toContain('technology_stack');
});
});
- describe("getAppSpecFormatInstruction", () => {
- it("should return non-empty format instruction", () => {
+ describe('getAppSpecFormatInstruction', () => {
+ it('should return non-empty format instruction', () => {
const instruction = getAppSpecFormatInstruction();
expect(instruction).toBeTruthy();
expect(instruction.length).toBeGreaterThan(100);
});
- it("should include critical formatting requirements", () => {
+ it('should include critical formatting requirements', () => {
const instruction = getAppSpecFormatInstruction();
- expect(instruction).toContain("CRITICAL FORMATTING REQUIREMENTS");
+ expect(instruction).toContain('CRITICAL FORMATTING REQUIREMENTS');
});
});
- describe("APP_SPEC_XML_FORMAT", () => {
- it("should contain valid XML template structure", () => {
- expect(APP_SPEC_XML_FORMAT).toContain("");
- expect(APP_SPEC_XML_FORMAT).toContain("");
+ describe('APP_SPEC_XML_FORMAT', () => {
+ it('should contain valid XML template structure', () => {
+ expect(APP_SPEC_XML_FORMAT).toContain('');
+ expect(APP_SPEC_XML_FORMAT).toContain('');
});
});
});
diff --git a/apps/server/tests/unit/lib/auth.test.ts b/apps/server/tests/unit/lib/auth.test.ts
index 97390bd3..91c1c461 100644
--- a/apps/server/tests/unit/lib/auth.test.ts
+++ b/apps/server/tests/unit/lib/auth.test.ts
@@ -1,20 +1,20 @@
-import { describe, it, expect, beforeEach, vi } from "vitest";
-import { createMockExpressContext } from "../../utils/mocks.js";
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+import { createMockExpressContext } from '../../utils/mocks.js';
/**
* Note: auth.ts reads AUTOMAKER_API_KEY at module load time.
* We need to reset modules and reimport for each test to get fresh state.
*/
-describe("auth.ts", () => {
+describe('auth.ts', () => {
beforeEach(() => {
vi.resetModules();
});
- describe("authMiddleware - no API key", () => {
- it("should call next() when no API key is set", async () => {
+ describe('authMiddleware - no API key', () => {
+ it('should call next() when no API key is set', async () => {
delete process.env.AUTOMAKER_API_KEY;
- const { authMiddleware } = await import("@/lib/auth.js");
+ const { authMiddleware } = await import('@/lib/auth.js');
const { req, res, next } = createMockExpressContext();
authMiddleware(req, res, next);
@@ -24,11 +24,11 @@ describe("auth.ts", () => {
});
});
- describe("authMiddleware - with API key", () => {
- it("should reject request without API key header", async () => {
- process.env.AUTOMAKER_API_KEY = "test-secret-key";
+ describe('authMiddleware - with API key', () => {
+ it('should reject request without API key header', async () => {
+ process.env.AUTOMAKER_API_KEY = 'test-secret-key';
- const { authMiddleware } = await import("@/lib/auth.js");
+ const { authMiddleware } = await import('@/lib/auth.js');
const { req, res, next } = createMockExpressContext();
authMiddleware(req, res, next);
@@ -36,34 +36,34 @@ describe("auth.ts", () => {
expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith({
success: false,
- error: "Authentication required. Provide X-API-Key header.",
+ error: 'Authentication required. Provide X-API-Key header.',
});
expect(next).not.toHaveBeenCalled();
});
- it("should reject request with invalid API key", async () => {
- process.env.AUTOMAKER_API_KEY = "test-secret-key";
+ it('should reject request with invalid API key', async () => {
+ process.env.AUTOMAKER_API_KEY = 'test-secret-key';
- const { authMiddleware } = await import("@/lib/auth.js");
+ const { authMiddleware } = await import('@/lib/auth.js');
const { req, res, next } = createMockExpressContext();
- req.headers["x-api-key"] = "wrong-key";
+ req.headers['x-api-key'] = 'wrong-key';
authMiddleware(req, res, next);
expect(res.status).toHaveBeenCalledWith(403);
expect(res.json).toHaveBeenCalledWith({
success: false,
- error: "Invalid API key.",
+ error: 'Invalid API key.',
});
expect(next).not.toHaveBeenCalled();
});
- it("should call next() with valid API key", async () => {
- process.env.AUTOMAKER_API_KEY = "test-secret-key";
+ it('should call next() with valid API key', async () => {
+ process.env.AUTOMAKER_API_KEY = 'test-secret-key';
- const { authMiddleware } = await import("@/lib/auth.js");
- const { req, res, next} = createMockExpressContext();
- req.headers["x-api-key"] = "test-secret-key";
+ const { authMiddleware } = await import('@/lib/auth.js');
+ const { req, res, next } = createMockExpressContext();
+ req.headers['x-api-key'] = 'test-secret-key';
authMiddleware(req, res, next);
@@ -72,44 +72,44 @@ describe("auth.ts", () => {
});
});
- describe("isAuthEnabled", () => {
- it("should return false when no API key is set", async () => {
+ describe('isAuthEnabled', () => {
+ it('should return false when no API key is set', async () => {
delete process.env.AUTOMAKER_API_KEY;
- const { isAuthEnabled } = await import("@/lib/auth.js");
+ const { isAuthEnabled } = await import('@/lib/auth.js');
expect(isAuthEnabled()).toBe(false);
});
- it("should return true when API key is set", async () => {
- process.env.AUTOMAKER_API_KEY = "test-key";
+ it('should return true when API key is set', async () => {
+ process.env.AUTOMAKER_API_KEY = 'test-key';
- const { isAuthEnabled } = await import("@/lib/auth.js");
+ const { isAuthEnabled } = await import('@/lib/auth.js');
expect(isAuthEnabled()).toBe(true);
});
});
- describe("getAuthStatus", () => {
- it("should return disabled status when no API key", async () => {
+ describe('getAuthStatus', () => {
+ it('should return disabled status when no API key', async () => {
delete process.env.AUTOMAKER_API_KEY;
- const { getAuthStatus } = await import("@/lib/auth.js");
+ const { getAuthStatus } = await import('@/lib/auth.js');
const status = getAuthStatus();
expect(status).toEqual({
enabled: false,
- method: "none",
+ method: 'none',
});
});
- it("should return enabled status when API key is set", async () => {
- process.env.AUTOMAKER_API_KEY = "test-key";
+ it('should return enabled status when API key is set', async () => {
+ process.env.AUTOMAKER_API_KEY = 'test-key';
- const { getAuthStatus } = await import("@/lib/auth.js");
+ const { getAuthStatus } = await import('@/lib/auth.js');
const status = getAuthStatus();
expect(status).toEqual({
enabled: true,
- method: "api_key",
+ method: 'api_key',
});
});
});
diff --git a/apps/server/tests/unit/lib/automaker-paths.test.ts b/apps/server/tests/unit/lib/automaker-paths.test.ts
index 15d2945c..09042ca0 100644
--- a/apps/server/tests/unit/lib/automaker-paths.test.ts
+++ b/apps/server/tests/unit/lib/automaker-paths.test.ts
@@ -1,7 +1,7 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
-import path from "path";
-import fs from "fs/promises";
-import os from "os";
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import path from 'path';
+import fs from 'fs/promises';
+import os from 'os';
import {
getAutomakerDir,
getFeaturesDir,
@@ -17,97 +17,85 @@ import {
getCredentialsPath,
getProjectSettingsPath,
ensureDataDir,
-} from "@automaker/platform";
+} from '@automaker/platform';
-describe("automaker-paths.ts", () => {
- const projectPath = path.join("/test", "project");
+describe('automaker-paths.ts', () => {
+ const projectPath = path.join('/test', 'project');
- describe("getAutomakerDir", () => {
- it("should return path to .automaker directory", () => {
- expect(getAutomakerDir(projectPath)).toBe(
- path.join(projectPath, ".automaker")
+ describe('getAutomakerDir', () => {
+ it('should return path to .automaker directory', () => {
+ expect(getAutomakerDir(projectPath)).toBe(path.join(projectPath, '.automaker'));
+ });
+
+ it('should handle paths with trailing slashes', () => {
+ const pathWithSlash = path.join('/test', 'project') + path.sep;
+ expect(getAutomakerDir(pathWithSlash)).toBe(path.join(pathWithSlash, '.automaker'));
+ });
+ });
+
+ describe('getFeaturesDir', () => {
+ it('should return path to features directory', () => {
+ expect(getFeaturesDir(projectPath)).toBe(path.join(projectPath, '.automaker', 'features'));
+ });
+ });
+
+ describe('getFeatureDir', () => {
+ it('should return path to specific feature directory', () => {
+ expect(getFeatureDir(projectPath, 'feature-123')).toBe(
+ path.join(projectPath, '.automaker', 'features', 'feature-123')
);
});
- it("should handle paths with trailing slashes", () => {
- const pathWithSlash = path.join("/test", "project") + path.sep;
- expect(getAutomakerDir(pathWithSlash)).toBe(
- path.join(pathWithSlash, ".automaker")
+ it('should handle feature IDs with special characters', () => {
+ expect(getFeatureDir(projectPath, 'my-feature_v2')).toBe(
+ path.join(projectPath, '.automaker', 'features', 'my-feature_v2')
);
});
});
- describe("getFeaturesDir", () => {
- it("should return path to features directory", () => {
- expect(getFeaturesDir(projectPath)).toBe(
- path.join(projectPath, ".automaker", "features")
+ describe('getFeatureImagesDir', () => {
+ it('should return path to feature images directory', () => {
+ expect(getFeatureImagesDir(projectPath, 'feature-123')).toBe(
+ path.join(projectPath, '.automaker', 'features', 'feature-123', 'images')
);
});
});
- describe("getFeatureDir", () => {
- it("should return path to specific feature directory", () => {
- expect(getFeatureDir(projectPath, "feature-123")).toBe(
- path.join(projectPath, ".automaker", "features", "feature-123")
- );
- });
-
- it("should handle feature IDs with special characters", () => {
- expect(getFeatureDir(projectPath, "my-feature_v2")).toBe(
- path.join(projectPath, ".automaker", "features", "my-feature_v2")
- );
+ describe('getBoardDir', () => {
+ it('should return path to board directory', () => {
+ expect(getBoardDir(projectPath)).toBe(path.join(projectPath, '.automaker', 'board'));
});
});
- describe("getFeatureImagesDir", () => {
- it("should return path to feature images directory", () => {
- expect(getFeatureImagesDir(projectPath, "feature-123")).toBe(
- path.join(projectPath, ".automaker", "features", "feature-123", "images")
- );
+ describe('getImagesDir', () => {
+ it('should return path to images directory', () => {
+ expect(getImagesDir(projectPath)).toBe(path.join(projectPath, '.automaker', 'images'));
});
});
- describe("getBoardDir", () => {
- it("should return path to board directory", () => {
- expect(getBoardDir(projectPath)).toBe(
- path.join(projectPath, ".automaker", "board")
- );
+ describe('getWorktreesDir', () => {
+ it('should return path to worktrees directory', () => {
+ expect(getWorktreesDir(projectPath)).toBe(path.join(projectPath, '.automaker', 'worktrees'));
});
});
- describe("getImagesDir", () => {
- it("should return path to images directory", () => {
- expect(getImagesDir(projectPath)).toBe(
- path.join(projectPath, ".automaker", "images")
- );
- });
- });
-
- describe("getWorktreesDir", () => {
- it("should return path to worktrees directory", () => {
- expect(getWorktreesDir(projectPath)).toBe(
- path.join(projectPath, ".automaker", "worktrees")
- );
- });
- });
-
- describe("getAppSpecPath", () => {
- it("should return path to app_spec.txt file", () => {
+ describe('getAppSpecPath', () => {
+ it('should return path to app_spec.txt file', () => {
expect(getAppSpecPath(projectPath)).toBe(
- path.join(projectPath, ".automaker", "app_spec.txt")
+ path.join(projectPath, '.automaker', 'app_spec.txt')
);
});
});
- describe("getBranchTrackingPath", () => {
- it("should return path to active-branches.json file", () => {
+ describe('getBranchTrackingPath', () => {
+ it('should return path to active-branches.json file', () => {
expect(getBranchTrackingPath(projectPath)).toBe(
- path.join(projectPath, ".automaker", "active-branches.json")
+ path.join(projectPath, '.automaker', 'active-branches.json')
);
});
});
- describe("ensureAutomakerDir", () => {
+ describe('ensureAutomakerDir', () => {
let testDir: string;
beforeEach(async () => {
@@ -123,16 +111,16 @@ describe("automaker-paths.ts", () => {
}
});
- it("should create automaker directory and return path", async () => {
+ it('should create automaker directory and return path', async () => {
const result = await ensureAutomakerDir(testDir);
- expect(result).toBe(path.join(testDir, ".automaker"));
+ expect(result).toBe(path.join(testDir, '.automaker'));
const stats = await fs.stat(result);
expect(stats.isDirectory()).toBe(true);
});
- it("should succeed if directory already exists", async () => {
- const automakerDir = path.join(testDir, ".automaker");
+ it('should succeed if directory already exists', async () => {
+ const automakerDir = path.join(testDir, '.automaker');
await fs.mkdir(automakerDir, { recursive: true });
const result = await ensureAutomakerDir(testDir);
@@ -141,53 +129,49 @@ describe("automaker-paths.ts", () => {
});
});
- describe("getGlobalSettingsPath", () => {
- it("should return path to settings.json in data directory", () => {
- const dataDir = "/test/data";
+ describe('getGlobalSettingsPath', () => {
+ it('should return path to settings.json in data directory', () => {
+ const dataDir = '/test/data';
const result = getGlobalSettingsPath(dataDir);
- expect(result).toBe(path.join(dataDir, "settings.json"));
+ expect(result).toBe(path.join(dataDir, 'settings.json'));
});
- it("should handle paths with trailing slashes", () => {
- const dataDir = "/test/data" + path.sep;
+ it('should handle paths with trailing slashes', () => {
+ const dataDir = '/test/data' + path.sep;
const result = getGlobalSettingsPath(dataDir);
- expect(result).toBe(path.join(dataDir, "settings.json"));
+ expect(result).toBe(path.join(dataDir, 'settings.json'));
});
});
- describe("getCredentialsPath", () => {
- it("should return path to credentials.json in data directory", () => {
- const dataDir = "/test/data";
+ describe('getCredentialsPath', () => {
+ it('should return path to credentials.json in data directory', () => {
+ const dataDir = '/test/data';
const result = getCredentialsPath(dataDir);
- expect(result).toBe(path.join(dataDir, "credentials.json"));
+ expect(result).toBe(path.join(dataDir, 'credentials.json'));
});
- it("should handle paths with trailing slashes", () => {
- const dataDir = "/test/data" + path.sep;
+ it('should handle paths with trailing slashes', () => {
+ const dataDir = '/test/data' + path.sep;
const result = getCredentialsPath(dataDir);
- expect(result).toBe(path.join(dataDir, "credentials.json"));
+ expect(result).toBe(path.join(dataDir, 'credentials.json'));
});
});
- describe("getProjectSettingsPath", () => {
- it("should return path to settings.json in project .automaker directory", () => {
- const projectPath = "/test/project";
+ describe('getProjectSettingsPath', () => {
+ it('should return path to settings.json in project .automaker directory', () => {
+ const projectPath = '/test/project';
const result = getProjectSettingsPath(projectPath);
- expect(result).toBe(
- path.join(projectPath, ".automaker", "settings.json")
- );
+ expect(result).toBe(path.join(projectPath, '.automaker', 'settings.json'));
});
- it("should handle paths with trailing slashes", () => {
- const projectPath = "/test/project" + path.sep;
+ it('should handle paths with trailing slashes', () => {
+ const projectPath = '/test/project' + path.sep;
const result = getProjectSettingsPath(projectPath);
- expect(result).toBe(
- path.join(projectPath, ".automaker", "settings.json")
- );
+ expect(result).toBe(path.join(projectPath, '.automaker', 'settings.json'));
});
});
- describe("ensureDataDir", () => {
+ describe('ensureDataDir', () => {
let testDir: string;
beforeEach(async () => {
@@ -202,7 +186,7 @@ describe("automaker-paths.ts", () => {
}
});
- it("should create data directory and return path", async () => {
+ it('should create data directory and return path', async () => {
const result = await ensureDataDir(testDir);
expect(result).toBe(testDir);
@@ -210,7 +194,7 @@ describe("automaker-paths.ts", () => {
expect(stats.isDirectory()).toBe(true);
});
- it("should succeed if directory already exists", async () => {
+ it('should succeed if directory already exists', async () => {
await fs.mkdir(testDir, { recursive: true });
const result = await ensureDataDir(testDir);
@@ -218,8 +202,8 @@ describe("automaker-paths.ts", () => {
expect(result).toBe(testDir);
});
- it("should create nested directories", async () => {
- const nestedDir = path.join(testDir, "nested", "deep");
+ it('should create nested directories', async () => {
+ const nestedDir = path.join(testDir, 'nested', 'deep');
const result = await ensureDataDir(nestedDir);
expect(result).toBe(nestedDir);
diff --git a/apps/server/tests/unit/lib/conversation-utils.test.ts b/apps/server/tests/unit/lib/conversation-utils.test.ts
index 3fa85bf2..bb7c6684 100644
--- a/apps/server/tests/unit/lib/conversation-utils.test.ts
+++ b/apps/server/tests/unit/lib/conversation-utils.test.ts
@@ -1,146 +1,146 @@
-import { describe, it, expect } from "vitest";
+import { describe, it, expect } from 'vitest';
import {
extractTextFromContent,
normalizeContentBlocks,
formatHistoryAsText,
convertHistoryToMessages,
-} from "@automaker/utils";
-import { conversationHistoryFixture } from "../../fixtures/messages.js";
+} from '@automaker/utils';
+import { conversationHistoryFixture } from '../../fixtures/messages.js';
-describe("conversation-utils.ts", () => {
- describe("extractTextFromContent", () => {
- it("should return string content as-is", () => {
- const result = extractTextFromContent("Hello world");
- expect(result).toBe("Hello world");
+describe('conversation-utils.ts', () => {
+ describe('extractTextFromContent', () => {
+ it('should return string content as-is', () => {
+ const result = extractTextFromContent('Hello world');
+ expect(result).toBe('Hello world');
});
- it("should extract text from single text block", () => {
- const content = [{ type: "text", text: "Hello" }];
+ it('should extract text from single text block', () => {
+ const content = [{ type: 'text', text: 'Hello' }];
const result = extractTextFromContent(content);
- expect(result).toBe("Hello");
+ expect(result).toBe('Hello');
});
- it("should extract and join multiple text blocks with newlines", () => {
+ it('should extract and join multiple text blocks with newlines', () => {
const content = [
- { type: "text", text: "First block" },
- { type: "text", text: "Second block" },
- { type: "text", text: "Third block" },
+ { type: 'text', text: 'First block' },
+ { type: 'text', text: 'Second block' },
+ { type: 'text', text: 'Third block' },
];
const result = extractTextFromContent(content);
- expect(result).toBe("First block\nSecond block\nThird block");
+ expect(result).toBe('First block\nSecond block\nThird block');
});
- it("should ignore non-text blocks", () => {
+ it('should ignore non-text blocks', () => {
const content = [
- { type: "text", text: "Text content" },
- { type: "image", source: { type: "base64", data: "abc" } },
- { type: "text", text: "More text" },
- { type: "tool_use", name: "bash", input: {} },
+ { type: 'text', text: 'Text content' },
+ { type: 'image', source: { type: 'base64', data: 'abc' } },
+ { type: 'text', text: 'More text' },
+ { type: 'tool_use', name: 'bash', input: {} },
];
const result = extractTextFromContent(content);
- expect(result).toBe("Text content\nMore text");
+ expect(result).toBe('Text content\nMore text');
});
- it("should handle blocks without text property", () => {
+ it('should handle blocks without text property', () => {
const content = [
- { type: "text", text: "Valid" },
- { type: "text" } as any,
- { type: "text", text: "Also valid" },
+ { type: 'text', text: 'Valid' },
+ { type: 'text' } as any,
+ { type: 'text', text: 'Also valid' },
];
const result = extractTextFromContent(content);
- expect(result).toBe("Valid\n\nAlso valid");
+ expect(result).toBe('Valid\n\nAlso valid');
});
- it("should handle empty array", () => {
+ it('should handle empty array', () => {
const result = extractTextFromContent([]);
- expect(result).toBe("");
+ expect(result).toBe('');
});
- it("should handle array with only non-text blocks", () => {
+ it('should handle array with only non-text blocks', () => {
const content = [
- { type: "image", source: {} },
- { type: "tool_use", name: "test" },
+ { type: 'image', source: {} },
+ { type: 'tool_use', name: 'test' },
];
const result = extractTextFromContent(content);
- expect(result).toBe("");
+ expect(result).toBe('');
});
});
- describe("normalizeContentBlocks", () => {
- it("should convert string to content block array", () => {
- const result = normalizeContentBlocks("Hello");
- expect(result).toEqual([{ type: "text", text: "Hello" }]);
+ describe('normalizeContentBlocks', () => {
+ it('should convert string to content block array', () => {
+ const result = normalizeContentBlocks('Hello');
+ expect(result).toEqual([{ type: 'text', text: 'Hello' }]);
});
- it("should return array content as-is", () => {
+ it('should return array content as-is', () => {
const content = [
- { type: "text", text: "Hello" },
- { type: "image", source: {} },
+ { type: 'text', text: 'Hello' },
+ { type: 'image', source: {} },
];
const result = normalizeContentBlocks(content);
expect(result).toBe(content);
expect(result).toHaveLength(2);
});
- it("should handle empty string", () => {
- const result = normalizeContentBlocks("");
- expect(result).toEqual([{ type: "text", text: "" }]);
+ it('should handle empty string', () => {
+ const result = normalizeContentBlocks('');
+ expect(result).toEqual([{ type: 'text', text: '' }]);
});
});
- describe("formatHistoryAsText", () => {
- it("should return empty string for empty history", () => {
+ describe('formatHistoryAsText', () => {
+ it('should return empty string for empty history', () => {
const result = formatHistoryAsText([]);
- expect(result).toBe("");
+ expect(result).toBe('');
});
- it("should format single user message", () => {
- const history = [{ role: "user" as const, content: "Hello" }];
+ it('should format single user message', () => {
+ const history = [{ role: 'user' as const, content: 'Hello' }];
const result = formatHistoryAsText(history);
- expect(result).toContain("Previous conversation:");
- expect(result).toContain("User: Hello");
- expect(result).toContain("---");
+ expect(result).toContain('Previous conversation:');
+ expect(result).toContain('User: Hello');
+ expect(result).toContain('---');
});
- it("should format single assistant message", () => {
- const history = [{ role: "assistant" as const, content: "Hi there" }];
+ it('should format single assistant message', () => {
+ const history = [{ role: 'assistant' as const, content: 'Hi there' }];
const result = formatHistoryAsText(history);
- expect(result).toContain("Assistant: Hi there");
+ expect(result).toContain('Assistant: Hi there');
});
- it("should format multiple messages with correct roles", () => {
+ it('should format multiple messages with correct roles', () => {
const history = conversationHistoryFixture.slice(0, 2);
const result = formatHistoryAsText(history);
- expect(result).toContain("User: Hello, can you help me?");
- expect(result).toContain("Assistant: Of course! How can I assist you today?");
- expect(result).toContain("---");
+ expect(result).toContain('User: Hello, can you help me?');
+ expect(result).toContain('Assistant: Of course! How can I assist you today?');
+ expect(result).toContain('---');
});
- it("should handle messages with array content (multipart)", () => {
+ it('should handle messages with array content (multipart)', () => {
const history = [conversationHistoryFixture[2]]; // Has text + image
const result = formatHistoryAsText(history);
- expect(result).toContain("What is in this image?");
- expect(result).not.toContain("base64"); // Should not include image data
+ expect(result).toContain('What is in this image?');
+ expect(result).not.toContain('base64'); // Should not include image data
});
- it("should format all messages from fixture", () => {
+ it('should format all messages from fixture', () => {
const result = formatHistoryAsText(conversationHistoryFixture);
- expect(result).toContain("Previous conversation:");
- expect(result).toContain("User: Hello, can you help me?");
- expect(result).toContain("Assistant: Of course!");
- expect(result).toContain("User: What is in this image?");
- expect(result).toContain("---");
+ expect(result).toContain('Previous conversation:');
+ expect(result).toContain('User: Hello, can you help me?');
+ expect(result).toContain('Assistant: Of course!');
+ expect(result).toContain('User: What is in this image?');
+ expect(result).toContain('---');
});
- it("should separate messages with double newlines", () => {
+ it('should separate messages with double newlines', () => {
const history = [
- { role: "user" as const, content: "First" },
- { role: "assistant" as const, content: "Second" },
+ { role: 'user' as const, content: 'First' },
+ { role: 'assistant' as const, content: 'Second' },
];
const result = formatHistoryAsText(history);
@@ -148,73 +148,71 @@ describe("conversation-utils.ts", () => {
});
});
- describe("convertHistoryToMessages", () => {
- it("should convert empty history", () => {
+ describe('convertHistoryToMessages', () => {
+ it('should convert empty history', () => {
const result = convertHistoryToMessages([]);
expect(result).toEqual([]);
});
- it("should convert single message to SDK format", () => {
- const history = [{ role: "user" as const, content: "Hello" }];
+ it('should convert single message to SDK format', () => {
+ const history = [{ role: 'user' as const, content: 'Hello' }];
const result = convertHistoryToMessages(history);
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
- type: "user",
- session_id: "",
+ type: 'user',
+ session_id: '',
message: {
- role: "user",
- content: [{ type: "text", text: "Hello" }],
+ role: 'user',
+ content: [{ type: 'text', text: 'Hello' }],
},
parent_tool_use_id: null,
});
});
- it("should normalize string content to array", () => {
- const history = [{ role: "assistant" as const, content: "Response" }];
+ it('should normalize string content to array', () => {
+ const history = [{ role: 'assistant' as const, content: 'Response' }];
const result = convertHistoryToMessages(history);
- expect(result[0].message.content).toEqual([
- { type: "text", text: "Response" },
- ]);
+ expect(result[0].message.content).toEqual([{ type: 'text', text: 'Response' }]);
});
- it("should preserve array content", () => {
+ it('should preserve array content', () => {
const history = [
{
- role: "user" as const,
+ role: 'user' as const,
content: [
- { type: "text", text: "Hello" },
- { type: "image", source: {} },
+ { type: 'text', text: 'Hello' },
+ { type: 'image', source: {} },
],
},
];
const result = convertHistoryToMessages(history);
expect(result[0].message.content).toHaveLength(2);
- expect(result[0].message.content[0]).toEqual({ type: "text", text: "Hello" });
+ expect(result[0].message.content[0]).toEqual({ type: 'text', text: 'Hello' });
});
- it("should convert multiple messages", () => {
+ it('should convert multiple messages', () => {
const history = conversationHistoryFixture.slice(0, 2);
const result = convertHistoryToMessages(history);
expect(result).toHaveLength(2);
- expect(result[0].type).toBe("user");
- expect(result[1].type).toBe("assistant");
+ expect(result[0].type).toBe('user');
+ expect(result[1].type).toBe('assistant');
});
- it("should set correct fields for SDK format", () => {
- const history = [{ role: "user" as const, content: "Test" }];
+ it('should set correct fields for SDK format', () => {
+ const history = [{ role: 'user' as const, content: 'Test' }];
const result = convertHistoryToMessages(history);
- expect(result[0].session_id).toBe("");
+ expect(result[0].session_id).toBe('');
expect(result[0].parent_tool_use_id).toBeNull();
- expect(result[0].type).toBe("user");
- expect(result[0].message.role).toBe("user");
+ expect(result[0].type).toBe('user');
+ expect(result[0].message.role).toBe('user');
});
- it("should handle all messages from fixture", () => {
+ it('should handle all messages from fixture', () => {
const result = convertHistoryToMessages(conversationHistoryFixture);
expect(result).toHaveLength(3);
diff --git a/apps/server/tests/unit/lib/dependency-resolver.test.ts b/apps/server/tests/unit/lib/dependency-resolver.test.ts
index 28a461b6..b018dacd 100644
--- a/apps/server/tests/unit/lib/dependency-resolver.test.ts
+++ b/apps/server/tests/unit/lib/dependency-resolver.test.ts
@@ -1,11 +1,11 @@
-import { describe, it, expect } from "vitest";
+import { describe, it, expect } from 'vitest';
import {
resolveDependencies,
areDependenciesSatisfied,
getBlockingDependencies,
type DependencyResolutionResult,
-} from "@automaker/dependency-resolver";
-import type { Feature } from "@automaker/types";
+} from '@automaker/dependency-resolver';
+import type { Feature } from '@automaker/types';
// Helper to create test features
function createFeature(
@@ -20,17 +20,17 @@ function createFeature(
): Feature {
return {
id,
- category: options.category || "test",
+ category: options.category || 'test',
description: options.description || `Feature ${id}`,
- status: options.status || "backlog",
+ status: options.status || 'backlog',
priority: options.priority,
dependencies: options.dependencies,
};
}
-describe("dependency-resolver.ts", () => {
- describe("resolveDependencies", () => {
- it("should handle empty feature list", () => {
+describe('dependency-resolver.ts', () => {
+ describe('resolveDependencies', () => {
+ it('should handle empty feature list', () => {
const result = resolveDependencies([]);
expect(result.orderedFeatures).toEqual([]);
@@ -39,103 +39,103 @@ describe("dependency-resolver.ts", () => {
expect(result.blockedFeatures.size).toBe(0);
});
- it("should handle features with no dependencies", () => {
+ it('should handle features with no dependencies', () => {
const features = [
- createFeature("f1", { priority: 1 }),
- createFeature("f2", { priority: 2 }),
- createFeature("f3", { priority: 3 }),
+ createFeature('f1', { priority: 1 }),
+ createFeature('f2', { priority: 2 }),
+ createFeature('f3', { priority: 3 }),
];
const result = resolveDependencies(features);
expect(result.orderedFeatures).toHaveLength(3);
- expect(result.orderedFeatures[0].id).toBe("f1"); // Highest priority first
- expect(result.orderedFeatures[1].id).toBe("f2");
- expect(result.orderedFeatures[2].id).toBe("f3");
+ expect(result.orderedFeatures[0].id).toBe('f1'); // Highest priority first
+ expect(result.orderedFeatures[1].id).toBe('f2');
+ expect(result.orderedFeatures[2].id).toBe('f3');
expect(result.circularDependencies).toEqual([]);
expect(result.missingDependencies.size).toBe(0);
expect(result.blockedFeatures.size).toBe(0);
});
- it("should order features by dependencies (simple chain)", () => {
+ it('should order features by dependencies (simple chain)', () => {
const features = [
- createFeature("f3", { dependencies: ["f2"] }),
- createFeature("f1"),
- createFeature("f2", { dependencies: ["f1"] }),
+ createFeature('f3', { dependencies: ['f2'] }),
+ createFeature('f1'),
+ createFeature('f2', { dependencies: ['f1'] }),
];
const result = resolveDependencies(features);
expect(result.orderedFeatures).toHaveLength(3);
- expect(result.orderedFeatures[0].id).toBe("f1");
- expect(result.orderedFeatures[1].id).toBe("f2");
- expect(result.orderedFeatures[2].id).toBe("f3");
+ expect(result.orderedFeatures[0].id).toBe('f1');
+ expect(result.orderedFeatures[1].id).toBe('f2');
+ expect(result.orderedFeatures[2].id).toBe('f3');
expect(result.circularDependencies).toEqual([]);
});
- it("should respect priority within same dependency level", () => {
+ it('should respect priority within same dependency level', () => {
const features = [
- createFeature("f1", { priority: 3, dependencies: ["base"] }),
- createFeature("f2", { priority: 1, dependencies: ["base"] }),
- createFeature("f3", { priority: 2, dependencies: ["base"] }),
- createFeature("base"),
+ createFeature('f1', { priority: 3, dependencies: ['base'] }),
+ createFeature('f2', { priority: 1, dependencies: ['base'] }),
+ createFeature('f3', { priority: 2, dependencies: ['base'] }),
+ createFeature('base'),
];
const result = resolveDependencies(features);
- expect(result.orderedFeatures[0].id).toBe("base");
- expect(result.orderedFeatures[1].id).toBe("f2"); // Priority 1
- expect(result.orderedFeatures[2].id).toBe("f3"); // Priority 2
- expect(result.orderedFeatures[3].id).toBe("f1"); // Priority 3
+ expect(result.orderedFeatures[0].id).toBe('base');
+ expect(result.orderedFeatures[1].id).toBe('f2'); // Priority 1
+ expect(result.orderedFeatures[2].id).toBe('f3'); // Priority 2
+ expect(result.orderedFeatures[3].id).toBe('f1'); // Priority 3
});
- it("should use default priority of 2 when not specified", () => {
+ it('should use default priority of 2 when not specified', () => {
const features = [
- createFeature("f1", { priority: 1 }),
- createFeature("f2"), // No priority = default 2
- createFeature("f3", { priority: 3 }),
+ createFeature('f1', { priority: 1 }),
+ createFeature('f2'), // No priority = default 2
+ createFeature('f3', { priority: 3 }),
];
const result = resolveDependencies(features);
- expect(result.orderedFeatures[0].id).toBe("f1");
- expect(result.orderedFeatures[1].id).toBe("f2");
- expect(result.orderedFeatures[2].id).toBe("f3");
+ expect(result.orderedFeatures[0].id).toBe('f1');
+ expect(result.orderedFeatures[1].id).toBe('f2');
+ expect(result.orderedFeatures[2].id).toBe('f3');
});
- it("should detect missing dependencies", () => {
+ it('should detect missing dependencies', () => {
const features = [
- createFeature("f1", { dependencies: ["missing1", "missing2"] }),
- createFeature("f2", { dependencies: ["f1", "missing3"] }),
+ createFeature('f1', { dependencies: ['missing1', 'missing2'] }),
+ createFeature('f2', { dependencies: ['f1', 'missing3'] }),
];
const result = resolveDependencies(features);
expect(result.missingDependencies.size).toBe(2);
- expect(result.missingDependencies.get("f1")).toEqual(["missing1", "missing2"]);
- expect(result.missingDependencies.get("f2")).toEqual(["missing3"]);
+ expect(result.missingDependencies.get('f1')).toEqual(['missing1', 'missing2']);
+ expect(result.missingDependencies.get('f2')).toEqual(['missing3']);
expect(result.orderedFeatures).toHaveLength(2);
});
- it("should detect blocked features (incomplete dependencies)", () => {
+ it('should detect blocked features (incomplete dependencies)', () => {
const features = [
- createFeature("f1", { status: "in_progress" }),
- createFeature("f2", { status: "backlog", dependencies: ["f1"] }),
- createFeature("f3", { status: "completed" }),
- createFeature("f4", { status: "backlog", dependencies: ["f3"] }),
+ createFeature('f1', { status: 'in_progress' }),
+ createFeature('f2', { status: 'backlog', dependencies: ['f1'] }),
+ createFeature('f3', { status: 'completed' }),
+ createFeature('f4', { status: 'backlog', dependencies: ['f3'] }),
];
const result = resolveDependencies(features);
expect(result.blockedFeatures.size).toBe(1);
- expect(result.blockedFeatures.get("f2")).toEqual(["f1"]);
- expect(result.blockedFeatures.has("f4")).toBe(false); // f3 is completed
+ expect(result.blockedFeatures.get('f2')).toEqual(['f1']);
+ expect(result.blockedFeatures.has('f4')).toBe(false); // f3 is completed
});
- it("should not block features whose dependencies are verified", () => {
+ it('should not block features whose dependencies are verified', () => {
const features = [
- createFeature("f1", { status: "verified" }),
- createFeature("f2", { status: "backlog", dependencies: ["f1"] }),
+ createFeature('f1', { status: 'verified' }),
+ createFeature('f2', { status: 'backlog', dependencies: ['f1'] }),
];
const result = resolveDependencies(features);
@@ -143,25 +143,25 @@ describe("dependency-resolver.ts", () => {
expect(result.blockedFeatures.size).toBe(0);
});
- it("should detect circular dependencies (simple cycle)", () => {
+ it('should detect circular dependencies (simple cycle)', () => {
const features = [
- createFeature("f1", { dependencies: ["f2"] }),
- createFeature("f2", { dependencies: ["f1"] }),
+ createFeature('f1', { dependencies: ['f2'] }),
+ createFeature('f2', { dependencies: ['f1'] }),
];
const result = resolveDependencies(features);
expect(result.circularDependencies).toHaveLength(1);
- expect(result.circularDependencies[0]).toContain("f1");
- expect(result.circularDependencies[0]).toContain("f2");
+ expect(result.circularDependencies[0]).toContain('f1');
+ expect(result.circularDependencies[0]).toContain('f2');
expect(result.orderedFeatures).toHaveLength(2); // Features still included
});
- it("should detect circular dependencies (multi-node cycle)", () => {
+ it('should detect circular dependencies (multi-node cycle)', () => {
const features = [
- createFeature("f1", { dependencies: ["f3"] }),
- createFeature("f2", { dependencies: ["f1"] }),
- createFeature("f3", { dependencies: ["f2"] }),
+ createFeature('f1', { dependencies: ['f3'] }),
+ createFeature('f2', { dependencies: ['f1'] }),
+ createFeature('f3', { dependencies: ['f2'] }),
];
const result = resolveDependencies(features);
@@ -170,47 +170,47 @@ describe("dependency-resolver.ts", () => {
expect(result.orderedFeatures).toHaveLength(3);
});
- it("should handle mixed valid and circular dependencies", () => {
+ it('should handle mixed valid and circular dependencies', () => {
const features = [
- createFeature("base"),
- createFeature("f1", { dependencies: ["base", "f2"] }),
- createFeature("f2", { dependencies: ["f1"] }), // Circular with f1
- createFeature("f3", { dependencies: ["base"] }),
+ createFeature('base'),
+ createFeature('f1', { dependencies: ['base', 'f2'] }),
+ createFeature('f2', { dependencies: ['f1'] }), // Circular with f1
+ createFeature('f3', { dependencies: ['base'] }),
];
const result = resolveDependencies(features);
expect(result.circularDependencies.length).toBeGreaterThan(0);
- expect(result.orderedFeatures[0].id).toBe("base");
+ expect(result.orderedFeatures[0].id).toBe('base');
expect(result.orderedFeatures).toHaveLength(4);
});
- it("should handle complex dependency graph", () => {
+ it('should handle complex dependency graph', () => {
const features = [
- createFeature("ui", { dependencies: ["api", "auth"], priority: 1 }),
- createFeature("api", { dependencies: ["db"], priority: 2 }),
- createFeature("auth", { dependencies: ["db"], priority: 1 }),
- createFeature("db", { priority: 1 }),
- createFeature("tests", { dependencies: ["ui"], priority: 3 }),
+ createFeature('ui', { dependencies: ['api', 'auth'], priority: 1 }),
+ createFeature('api', { dependencies: ['db'], priority: 2 }),
+ createFeature('auth', { dependencies: ['db'], priority: 1 }),
+ createFeature('db', { priority: 1 }),
+ createFeature('tests', { dependencies: ['ui'], priority: 3 }),
];
const result = resolveDependencies(features);
- const order = result.orderedFeatures.map(f => f.id);
+ const order = result.orderedFeatures.map((f) => f.id);
- expect(order[0]).toBe("db");
- expect(order.indexOf("db")).toBeLessThan(order.indexOf("api"));
- expect(order.indexOf("db")).toBeLessThan(order.indexOf("auth"));
- expect(order.indexOf("api")).toBeLessThan(order.indexOf("ui"));
- expect(order.indexOf("auth")).toBeLessThan(order.indexOf("ui"));
- expect(order.indexOf("ui")).toBeLessThan(order.indexOf("tests"));
+ expect(order[0]).toBe('db');
+ expect(order.indexOf('db')).toBeLessThan(order.indexOf('api'));
+ expect(order.indexOf('db')).toBeLessThan(order.indexOf('auth'));
+ expect(order.indexOf('api')).toBeLessThan(order.indexOf('ui'));
+ expect(order.indexOf('auth')).toBeLessThan(order.indexOf('ui'));
+ expect(order.indexOf('ui')).toBeLessThan(order.indexOf('tests'));
expect(result.circularDependencies).toEqual([]);
});
- it("should handle features with empty dependencies array", () => {
+ it('should handle features with empty dependencies array', () => {
const features = [
- createFeature("f1", { dependencies: [] }),
- createFeature("f2", { dependencies: [] }),
+ createFeature('f1', { dependencies: [] }),
+ createFeature('f2', { dependencies: [] }),
];
const result = resolveDependencies(features);
@@ -220,22 +220,20 @@ describe("dependency-resolver.ts", () => {
expect(result.blockedFeatures.size).toBe(0);
});
- it("should track multiple blocking dependencies", () => {
+ it('should track multiple blocking dependencies', () => {
const features = [
- createFeature("f1", { status: "in_progress" }),
- createFeature("f2", { status: "backlog" }),
- createFeature("f3", { status: "backlog", dependencies: ["f1", "f2"] }),
+ createFeature('f1', { status: 'in_progress' }),
+ createFeature('f2', { status: 'backlog' }),
+ createFeature('f3', { status: 'backlog', dependencies: ['f1', 'f2'] }),
];
const result = resolveDependencies(features);
- expect(result.blockedFeatures.get("f3")).toEqual(["f1", "f2"]);
+ expect(result.blockedFeatures.get('f3')).toEqual(['f1', 'f2']);
});
- it("should handle self-referencing dependency", () => {
- const features = [
- createFeature("f1", { dependencies: ["f1"] }),
- ];
+ it('should handle self-referencing dependency', () => {
+ const features = [createFeature('f1', { dependencies: ['f1'] })];
const result = resolveDependencies(features);
@@ -244,195 +242,191 @@ describe("dependency-resolver.ts", () => {
});
});
- describe("areDependenciesSatisfied", () => {
- it("should return true for feature with no dependencies", () => {
- const feature = createFeature("f1");
+ describe('areDependenciesSatisfied', () => {
+ it('should return true for feature with no dependencies', () => {
+ const feature = createFeature('f1');
const allFeatures = [feature];
expect(areDependenciesSatisfied(feature, allFeatures)).toBe(true);
});
- it("should return true for feature with empty dependencies array", () => {
- const feature = createFeature("f1", { dependencies: [] });
+ it('should return true for feature with empty dependencies array', () => {
+ const feature = createFeature('f1', { dependencies: [] });
const allFeatures = [feature];
expect(areDependenciesSatisfied(feature, allFeatures)).toBe(true);
});
- it("should return true when all dependencies are completed", () => {
+ it('should return true when all dependencies are completed', () => {
const allFeatures = [
- createFeature("f1", { status: "completed" }),
- createFeature("f2", { status: "completed" }),
- createFeature("f3", { status: "backlog", dependencies: ["f1", "f2"] }),
+ createFeature('f1', { status: 'completed' }),
+ createFeature('f2', { status: 'completed' }),
+ createFeature('f3', { status: 'backlog', dependencies: ['f1', 'f2'] }),
];
expect(areDependenciesSatisfied(allFeatures[2], allFeatures)).toBe(true);
});
- it("should return true when all dependencies are verified", () => {
+ it('should return true when all dependencies are verified', () => {
const allFeatures = [
- createFeature("f1", { status: "verified" }),
- createFeature("f2", { status: "verified" }),
- createFeature("f3", { status: "backlog", dependencies: ["f1", "f2"] }),
+ createFeature('f1', { status: 'verified' }),
+ createFeature('f2', { status: 'verified' }),
+ createFeature('f3', { status: 'backlog', dependencies: ['f1', 'f2'] }),
];
expect(areDependenciesSatisfied(allFeatures[2], allFeatures)).toBe(true);
});
- it("should return true when dependencies are mix of completed and verified", () => {
+ it('should return true when dependencies are mix of completed and verified', () => {
const allFeatures = [
- createFeature("f1", { status: "completed" }),
- createFeature("f2", { status: "verified" }),
- createFeature("f3", { status: "backlog", dependencies: ["f1", "f2"] }),
+ createFeature('f1', { status: 'completed' }),
+ createFeature('f2', { status: 'verified' }),
+ createFeature('f3', { status: 'backlog', dependencies: ['f1', 'f2'] }),
];
expect(areDependenciesSatisfied(allFeatures[2], allFeatures)).toBe(true);
});
- it("should return false when any dependency is in_progress", () => {
+ it('should return false when any dependency is in_progress', () => {
const allFeatures = [
- createFeature("f1", { status: "completed" }),
- createFeature("f2", { status: "in_progress" }),
- createFeature("f3", { status: "backlog", dependencies: ["f1", "f2"] }),
+ createFeature('f1', { status: 'completed' }),
+ createFeature('f2', { status: 'in_progress' }),
+ createFeature('f3', { status: 'backlog', dependencies: ['f1', 'f2'] }),
];
expect(areDependenciesSatisfied(allFeatures[2], allFeatures)).toBe(false);
});
- it("should return false when any dependency is in backlog", () => {
+ it('should return false when any dependency is in backlog', () => {
const allFeatures = [
- createFeature("f1", { status: "completed" }),
- createFeature("f2", { status: "backlog" }),
- createFeature("f3", { status: "backlog", dependencies: ["f1", "f2"] }),
+ createFeature('f1', { status: 'completed' }),
+ createFeature('f2', { status: 'backlog' }),
+ createFeature('f3', { status: 'backlog', dependencies: ['f1', 'f2'] }),
];
expect(areDependenciesSatisfied(allFeatures[2], allFeatures)).toBe(false);
});
- it("should return false when dependency is missing", () => {
- const allFeatures = [
- createFeature("f1", { status: "backlog", dependencies: ["missing"] }),
- ];
+ it('should return false when dependency is missing', () => {
+ const allFeatures = [createFeature('f1', { status: 'backlog', dependencies: ['missing'] })];
expect(areDependenciesSatisfied(allFeatures[0], allFeatures)).toBe(false);
});
- it("should return false when multiple dependencies are incomplete", () => {
+ it('should return false when multiple dependencies are incomplete', () => {
const allFeatures = [
- createFeature("f1", { status: "backlog" }),
- createFeature("f2", { status: "in_progress" }),
- createFeature("f3", { status: "waiting_approval" }),
- createFeature("f4", { status: "backlog", dependencies: ["f1", "f2", "f3"] }),
+ createFeature('f1', { status: 'backlog' }),
+ createFeature('f2', { status: 'in_progress' }),
+ createFeature('f3', { status: 'waiting_approval' }),
+ createFeature('f4', { status: 'backlog', dependencies: ['f1', 'f2', 'f3'] }),
];
expect(areDependenciesSatisfied(allFeatures[3], allFeatures)).toBe(false);
});
});
- describe("getBlockingDependencies", () => {
- it("should return empty array for feature with no dependencies", () => {
- const feature = createFeature("f1");
+ describe('getBlockingDependencies', () => {
+ it('should return empty array for feature with no dependencies', () => {
+ const feature = createFeature('f1');
const allFeatures = [feature];
expect(getBlockingDependencies(feature, allFeatures)).toEqual([]);
});
- it("should return empty array for feature with empty dependencies array", () => {
- const feature = createFeature("f1", { dependencies: [] });
+ it('should return empty array for feature with empty dependencies array', () => {
+ const feature = createFeature('f1', { dependencies: [] });
const allFeatures = [feature];
expect(getBlockingDependencies(feature, allFeatures)).toEqual([]);
});
- it("should return empty array when all dependencies are completed", () => {
+ it('should return empty array when all dependencies are completed', () => {
const allFeatures = [
- createFeature("f1", { status: "completed" }),
- createFeature("f2", { status: "completed" }),
- createFeature("f3", { status: "backlog", dependencies: ["f1", "f2"] }),
+ createFeature('f1', { status: 'completed' }),
+ createFeature('f2', { status: 'completed' }),
+ createFeature('f3', { status: 'backlog', dependencies: ['f1', 'f2'] }),
];
expect(getBlockingDependencies(allFeatures[2], allFeatures)).toEqual([]);
});
- it("should return empty array when all dependencies are verified", () => {
+ it('should return empty array when all dependencies are verified', () => {
const allFeatures = [
- createFeature("f1", { status: "verified" }),
- createFeature("f2", { status: "verified" }),
- createFeature("f3", { status: "backlog", dependencies: ["f1", "f2"] }),
+ createFeature('f1', { status: 'verified' }),
+ createFeature('f2', { status: 'verified' }),
+ createFeature('f3', { status: 'backlog', dependencies: ['f1', 'f2'] }),
];
expect(getBlockingDependencies(allFeatures[2], allFeatures)).toEqual([]);
});
- it("should return blocking dependencies in backlog status", () => {
+ it('should return blocking dependencies in backlog status', () => {
const allFeatures = [
- createFeature("f1", { status: "backlog" }),
- createFeature("f2", { status: "completed" }),
- createFeature("f3", { status: "backlog", dependencies: ["f1", "f2"] }),
+ createFeature('f1', { status: 'backlog' }),
+ createFeature('f2', { status: 'completed' }),
+ createFeature('f3', { status: 'backlog', dependencies: ['f1', 'f2'] }),
];
- expect(getBlockingDependencies(allFeatures[2], allFeatures)).toEqual(["f1"]);
+ expect(getBlockingDependencies(allFeatures[2], allFeatures)).toEqual(['f1']);
});
- it("should return blocking dependencies in in_progress status", () => {
+ it('should return blocking dependencies in in_progress status', () => {
const allFeatures = [
- createFeature("f1", { status: "in_progress" }),
- createFeature("f2", { status: "verified" }),
- createFeature("f3", { status: "backlog", dependencies: ["f1", "f2"] }),
+ createFeature('f1', { status: 'in_progress' }),
+ createFeature('f2', { status: 'verified' }),
+ createFeature('f3', { status: 'backlog', dependencies: ['f1', 'f2'] }),
];
- expect(getBlockingDependencies(allFeatures[2], allFeatures)).toEqual(["f1"]);
+ expect(getBlockingDependencies(allFeatures[2], allFeatures)).toEqual(['f1']);
});
- it("should return blocking dependencies in waiting_approval status", () => {
+ it('should return blocking dependencies in waiting_approval status', () => {
const allFeatures = [
- createFeature("f1", { status: "waiting_approval" }),
- createFeature("f2", { status: "completed" }),
- createFeature("f3", { status: "backlog", dependencies: ["f1", "f2"] }),
+ createFeature('f1', { status: 'waiting_approval' }),
+ createFeature('f2', { status: 'completed' }),
+ createFeature('f3', { status: 'backlog', dependencies: ['f1', 'f2'] }),
];
- expect(getBlockingDependencies(allFeatures[2], allFeatures)).toEqual(["f1"]);
+ expect(getBlockingDependencies(allFeatures[2], allFeatures)).toEqual(['f1']);
});
- it("should return all blocking dependencies", () => {
+ it('should return all blocking dependencies', () => {
const allFeatures = [
- createFeature("f1", { status: "backlog" }),
- createFeature("f2", { status: "in_progress" }),
- createFeature("f3", { status: "waiting_approval" }),
- createFeature("f4", { status: "completed" }),
- createFeature("f5", { status: "backlog", dependencies: ["f1", "f2", "f3", "f4"] }),
+ createFeature('f1', { status: 'backlog' }),
+ createFeature('f2', { status: 'in_progress' }),
+ createFeature('f3', { status: 'waiting_approval' }),
+ createFeature('f4', { status: 'completed' }),
+ createFeature('f5', { status: 'backlog', dependencies: ['f1', 'f2', 'f3', 'f4'] }),
];
const blocking = getBlockingDependencies(allFeatures[4], allFeatures);
expect(blocking).toHaveLength(3);
- expect(blocking).toContain("f1");
- expect(blocking).toContain("f2");
- expect(blocking).toContain("f3");
- expect(blocking).not.toContain("f4");
+ expect(blocking).toContain('f1');
+ expect(blocking).toContain('f2');
+ expect(blocking).toContain('f3');
+ expect(blocking).not.toContain('f4');
});
- it("should handle missing dependencies", () => {
- const allFeatures = [
- createFeature("f1", { status: "backlog", dependencies: ["missing"] }),
- ];
+ it('should handle missing dependencies', () => {
+ const allFeatures = [createFeature('f1', { status: 'backlog', dependencies: ['missing'] })];
// Missing dependencies won't be in the blocking list since they don't exist
expect(getBlockingDependencies(allFeatures[0], allFeatures)).toEqual([]);
});
- it("should handle mix of completed, verified, and incomplete dependencies", () => {
+ it('should handle mix of completed, verified, and incomplete dependencies', () => {
const allFeatures = [
- createFeature("f1", { status: "completed" }),
- createFeature("f2", { status: "verified" }),
- createFeature("f3", { status: "in_progress" }),
- createFeature("f4", { status: "backlog" }),
- createFeature("f5", { status: "backlog", dependencies: ["f1", "f2", "f3", "f4"] }),
+ createFeature('f1', { status: 'completed' }),
+ createFeature('f2', { status: 'verified' }),
+ createFeature('f3', { status: 'in_progress' }),
+ createFeature('f4', { status: 'backlog' }),
+ createFeature('f5', { status: 'backlog', dependencies: ['f1', 'f2', 'f3', 'f4'] }),
];
const blocking = getBlockingDependencies(allFeatures[4], allFeatures);
expect(blocking).toHaveLength(2);
- expect(blocking).toContain("f3");
- expect(blocking).toContain("f4");
+ expect(blocking).toContain('f3');
+ expect(blocking).toContain('f4');
});
});
});
diff --git a/apps/server/tests/unit/lib/enhancement-prompts.test.ts b/apps/server/tests/unit/lib/enhancement-prompts.test.ts
index d780612d..ab139861 100644
--- a/apps/server/tests/unit/lib/enhancement-prompts.test.ts
+++ b/apps/server/tests/unit/lib/enhancement-prompts.test.ts
@@ -1,4 +1,4 @@
-import { describe, it, expect } from "vitest";
+import { describe, it, expect } from 'vitest';
import {
getEnhancementPrompt,
getSystemPrompt,
@@ -15,38 +15,38 @@ import {
SIMPLIFY_EXAMPLES,
ACCEPTANCE_EXAMPLES,
type EnhancementMode,
-} from "@/lib/enhancement-prompts.js";
+} from '@/lib/enhancement-prompts.js';
-describe("enhancement-prompts.ts", () => {
- describe("System Prompt Constants", () => {
- it("should have non-empty improve system prompt", () => {
+describe('enhancement-prompts.ts', () => {
+ describe('System Prompt Constants', () => {
+ it('should have non-empty improve system prompt', () => {
expect(IMPROVE_SYSTEM_PROMPT).toBeDefined();
expect(IMPROVE_SYSTEM_PROMPT.length).toBeGreaterThan(100);
- expect(IMPROVE_SYSTEM_PROMPT).toContain("ANALYZE");
- expect(IMPROVE_SYSTEM_PROMPT).toContain("CLARIFY");
+ expect(IMPROVE_SYSTEM_PROMPT).toContain('ANALYZE');
+ expect(IMPROVE_SYSTEM_PROMPT).toContain('CLARIFY');
});
- it("should have non-empty technical system prompt", () => {
+ it('should have non-empty technical system prompt', () => {
expect(TECHNICAL_SYSTEM_PROMPT).toBeDefined();
expect(TECHNICAL_SYSTEM_PROMPT.length).toBeGreaterThan(100);
- expect(TECHNICAL_SYSTEM_PROMPT).toContain("technical");
+ expect(TECHNICAL_SYSTEM_PROMPT).toContain('technical');
});
- it("should have non-empty simplify system prompt", () => {
+ it('should have non-empty simplify system prompt', () => {
expect(SIMPLIFY_SYSTEM_PROMPT).toBeDefined();
expect(SIMPLIFY_SYSTEM_PROMPT.length).toBeGreaterThan(100);
- expect(SIMPLIFY_SYSTEM_PROMPT).toContain("simplify");
+ expect(SIMPLIFY_SYSTEM_PROMPT).toContain('simplify');
});
- it("should have non-empty acceptance system prompt", () => {
+ it('should have non-empty acceptance system prompt', () => {
expect(ACCEPTANCE_SYSTEM_PROMPT).toBeDefined();
expect(ACCEPTANCE_SYSTEM_PROMPT.length).toBeGreaterThan(100);
- expect(ACCEPTANCE_SYSTEM_PROMPT).toContain("acceptance criteria");
+ expect(ACCEPTANCE_SYSTEM_PROMPT).toContain('acceptance criteria');
});
});
- describe("Example Constants", () => {
- it("should have improve examples with input and output", () => {
+ describe('Example Constants', () => {
+ it('should have improve examples with input and output', () => {
expect(IMPROVE_EXAMPLES).toBeDefined();
expect(IMPROVE_EXAMPLES.length).toBeGreaterThan(0);
IMPROVE_EXAMPLES.forEach((example) => {
@@ -57,7 +57,7 @@ describe("enhancement-prompts.ts", () => {
});
});
- it("should have technical examples with input and output", () => {
+ it('should have technical examples with input and output', () => {
expect(TECHNICAL_EXAMPLES).toBeDefined();
expect(TECHNICAL_EXAMPLES.length).toBeGreaterThan(0);
TECHNICAL_EXAMPLES.forEach((example) => {
@@ -66,7 +66,7 @@ describe("enhancement-prompts.ts", () => {
});
});
- it("should have simplify examples with input and output", () => {
+ it('should have simplify examples with input and output', () => {
expect(SIMPLIFY_EXAMPLES).toBeDefined();
expect(SIMPLIFY_EXAMPLES.length).toBeGreaterThan(0);
SIMPLIFY_EXAMPLES.forEach((example) => {
@@ -75,7 +75,7 @@ describe("enhancement-prompts.ts", () => {
});
});
- it("should have acceptance examples with input and output", () => {
+ it('should have acceptance examples with input and output', () => {
expect(ACCEPTANCE_EXAMPLES).toBeDefined();
expect(ACCEPTANCE_EXAMPLES.length).toBeGreaterThan(0);
ACCEPTANCE_EXAMPLES.forEach((example) => {
@@ -85,66 +85,66 @@ describe("enhancement-prompts.ts", () => {
});
});
- describe("getEnhancementPrompt", () => {
- it("should return config for improve mode", () => {
- const config = getEnhancementPrompt("improve");
+ describe('getEnhancementPrompt', () => {
+ it('should return config for improve mode', () => {
+ const config = getEnhancementPrompt('improve');
expect(config.systemPrompt).toBe(IMPROVE_SYSTEM_PROMPT);
- expect(config.description).toContain("clear");
+ expect(config.description).toContain('clear');
});
- it("should return config for technical mode", () => {
- const config = getEnhancementPrompt("technical");
+ it('should return config for technical mode', () => {
+ const config = getEnhancementPrompt('technical');
expect(config.systemPrompt).toBe(TECHNICAL_SYSTEM_PROMPT);
- expect(config.description).toContain("technical");
+ expect(config.description).toContain('technical');
});
- it("should return config for simplify mode", () => {
- const config = getEnhancementPrompt("simplify");
+ it('should return config for simplify mode', () => {
+ const config = getEnhancementPrompt('simplify');
expect(config.systemPrompt).toBe(SIMPLIFY_SYSTEM_PROMPT);
- expect(config.description).toContain("concise");
+ expect(config.description).toContain('concise');
});
- it("should return config for acceptance mode", () => {
- const config = getEnhancementPrompt("acceptance");
+ it('should return config for acceptance mode', () => {
+ const config = getEnhancementPrompt('acceptance');
expect(config.systemPrompt).toBe(ACCEPTANCE_SYSTEM_PROMPT);
- expect(config.description).toContain("acceptance");
+ expect(config.description).toContain('acceptance');
});
- it("should handle case-insensitive mode", () => {
- const config = getEnhancementPrompt("IMPROVE");
+ it('should handle case-insensitive mode', () => {
+ const config = getEnhancementPrompt('IMPROVE');
expect(config.systemPrompt).toBe(IMPROVE_SYSTEM_PROMPT);
});
- it("should fall back to improve for invalid mode", () => {
- const config = getEnhancementPrompt("invalid-mode");
+ it('should fall back to improve for invalid mode', () => {
+ const config = getEnhancementPrompt('invalid-mode');
expect(config.systemPrompt).toBe(IMPROVE_SYSTEM_PROMPT);
});
- it("should fall back to improve for empty string", () => {
- const config = getEnhancementPrompt("");
+ it('should fall back to improve for empty string', () => {
+ const config = getEnhancementPrompt('');
expect(config.systemPrompt).toBe(IMPROVE_SYSTEM_PROMPT);
});
});
- describe("getSystemPrompt", () => {
- it("should return correct system prompt for each mode", () => {
- expect(getSystemPrompt("improve")).toBe(IMPROVE_SYSTEM_PROMPT);
- expect(getSystemPrompt("technical")).toBe(TECHNICAL_SYSTEM_PROMPT);
- expect(getSystemPrompt("simplify")).toBe(SIMPLIFY_SYSTEM_PROMPT);
- expect(getSystemPrompt("acceptance")).toBe(ACCEPTANCE_SYSTEM_PROMPT);
+ describe('getSystemPrompt', () => {
+ it('should return correct system prompt for each mode', () => {
+ expect(getSystemPrompt('improve')).toBe(IMPROVE_SYSTEM_PROMPT);
+ expect(getSystemPrompt('technical')).toBe(TECHNICAL_SYSTEM_PROMPT);
+ expect(getSystemPrompt('simplify')).toBe(SIMPLIFY_SYSTEM_PROMPT);
+ expect(getSystemPrompt('acceptance')).toBe(ACCEPTANCE_SYSTEM_PROMPT);
});
});
- describe("getExamples", () => {
- it("should return correct examples for each mode", () => {
- expect(getExamples("improve")).toBe(IMPROVE_EXAMPLES);
- expect(getExamples("technical")).toBe(TECHNICAL_EXAMPLES);
- expect(getExamples("simplify")).toBe(SIMPLIFY_EXAMPLES);
- expect(getExamples("acceptance")).toBe(ACCEPTANCE_EXAMPLES);
+ describe('getExamples', () => {
+ it('should return correct examples for each mode', () => {
+ expect(getExamples('improve')).toBe(IMPROVE_EXAMPLES);
+ expect(getExamples('technical')).toBe(TECHNICAL_EXAMPLES);
+ expect(getExamples('simplify')).toBe(SIMPLIFY_EXAMPLES);
+ expect(getExamples('acceptance')).toBe(ACCEPTANCE_EXAMPLES);
});
- it("should return arrays with example objects", () => {
- const modes: EnhancementMode[] = ["improve", "technical", "simplify", "acceptance"];
+ it('should return arrays with example objects', () => {
+ const modes: EnhancementMode[] = ['improve', 'technical', 'simplify', 'acceptance'];
modes.forEach((mode) => {
const examples = getExamples(mode);
expect(Array.isArray(examples)).toBe(true);
@@ -153,38 +153,38 @@ describe("enhancement-prompts.ts", () => {
});
});
- describe("buildUserPrompt", () => {
- const testText = "Add a logout button";
+ describe('buildUserPrompt', () => {
+ const testText = 'Add a logout button';
- it("should build prompt with examples by default", () => {
- const prompt = buildUserPrompt("improve", testText);
- expect(prompt).toContain("Example 1:");
+ it('should build prompt with examples by default', () => {
+ const prompt = buildUserPrompt('improve', testText);
+ expect(prompt).toContain('Example 1:');
expect(prompt).toContain(testText);
- expect(prompt).toContain("Now, please enhance the following task description:");
+ expect(prompt).toContain('Now, please enhance the following task description:');
});
- it("should build prompt without examples when includeExamples is false", () => {
- const prompt = buildUserPrompt("improve", testText, false);
- expect(prompt).not.toContain("Example 1:");
+ it('should build prompt without examples when includeExamples is false', () => {
+ const prompt = buildUserPrompt('improve', testText, false);
+ expect(prompt).not.toContain('Example 1:');
expect(prompt).toContain(testText);
- expect(prompt).toContain("Please enhance the following task description:");
+ expect(prompt).toContain('Please enhance the following task description:');
});
- it("should include all examples for improve mode", () => {
- const prompt = buildUserPrompt("improve", testText);
+ it('should include all examples for improve mode', () => {
+ const prompt = buildUserPrompt('improve', testText);
IMPROVE_EXAMPLES.forEach((example, index) => {
expect(prompt).toContain(`Example ${index + 1}:`);
expect(prompt).toContain(example.input);
});
});
- it("should include separator between examples", () => {
- const prompt = buildUserPrompt("improve", testText);
- expect(prompt).toContain("---");
+ it('should include separator between examples', () => {
+ const prompt = buildUserPrompt('improve', testText);
+ expect(prompt).toContain('---');
});
- it("should work with all enhancement modes", () => {
- const modes: EnhancementMode[] = ["improve", "technical", "simplify", "acceptance"];
+ it('should work with all enhancement modes', () => {
+ const modes: EnhancementMode[] = ['improve', 'technical', 'simplify', 'acceptance'];
modes.forEach((mode) => {
const prompt = buildUserPrompt(mode, testText);
expect(prompt).toContain(testText);
@@ -192,40 +192,40 @@ describe("enhancement-prompts.ts", () => {
});
});
- it("should preserve the original text exactly", () => {
- const specialText = "Add feature with special chars: <>&\"'";
- const prompt = buildUserPrompt("improve", specialText);
+ it('should preserve the original text exactly', () => {
+ const specialText = 'Add feature with special chars: <>&"\'';
+ const prompt = buildUserPrompt('improve', specialText);
expect(prompt).toContain(specialText);
});
});
- describe("isValidEnhancementMode", () => {
- it("should return true for valid modes", () => {
- expect(isValidEnhancementMode("improve")).toBe(true);
- expect(isValidEnhancementMode("technical")).toBe(true);
- expect(isValidEnhancementMode("simplify")).toBe(true);
- expect(isValidEnhancementMode("acceptance")).toBe(true);
+ describe('isValidEnhancementMode', () => {
+ it('should return true for valid modes', () => {
+ expect(isValidEnhancementMode('improve')).toBe(true);
+ expect(isValidEnhancementMode('technical')).toBe(true);
+ expect(isValidEnhancementMode('simplify')).toBe(true);
+ expect(isValidEnhancementMode('acceptance')).toBe(true);
});
- it("should return false for invalid modes", () => {
- expect(isValidEnhancementMode("invalid")).toBe(false);
- expect(isValidEnhancementMode("IMPROVE")).toBe(false); // case-sensitive
- expect(isValidEnhancementMode("")).toBe(false);
- expect(isValidEnhancementMode("random")).toBe(false);
+ it('should return false for invalid modes', () => {
+ expect(isValidEnhancementMode('invalid')).toBe(false);
+ expect(isValidEnhancementMode('IMPROVE')).toBe(false); // case-sensitive
+ expect(isValidEnhancementMode('')).toBe(false);
+ expect(isValidEnhancementMode('random')).toBe(false);
});
});
- describe("getAvailableEnhancementModes", () => {
- it("should return all four enhancement modes", () => {
+ describe('getAvailableEnhancementModes', () => {
+ it('should return all four enhancement modes', () => {
const modes = getAvailableEnhancementModes();
expect(modes).toHaveLength(4);
- expect(modes).toContain("improve");
- expect(modes).toContain("technical");
- expect(modes).toContain("simplify");
- expect(modes).toContain("acceptance");
+ expect(modes).toContain('improve');
+ expect(modes).toContain('technical');
+ expect(modes).toContain('simplify');
+ expect(modes).toContain('acceptance');
});
- it("should return an array", () => {
+ it('should return an array', () => {
const modes = getAvailableEnhancementModes();
expect(Array.isArray(modes)).toBe(true);
});
diff --git a/apps/server/tests/unit/lib/error-handler.test.ts b/apps/server/tests/unit/lib/error-handler.test.ts
index 7e5a1ac4..87a16036 100644
--- a/apps/server/tests/unit/lib/error-handler.test.ts
+++ b/apps/server/tests/unit/lib/error-handler.test.ts
@@ -1,4 +1,4 @@
-import { describe, it, expect } from "vitest";
+import { describe, it, expect } from 'vitest';
import {
isAbortError,
isAuthenticationError,
@@ -6,206 +6,206 @@ import {
classifyError,
getUserFriendlyErrorMessage,
type ErrorType,
-} from "@automaker/utils";
+} from '@automaker/utils';
-describe("error-handler.ts", () => {
- describe("isAbortError", () => {
- it("should detect AbortError by error name", () => {
- const error = new Error("Operation cancelled");
- error.name = "AbortError";
+describe('error-handler.ts', () => {
+ describe('isAbortError', () => {
+ it('should detect AbortError by error name', () => {
+ const error = new Error('Operation cancelled');
+ error.name = 'AbortError';
expect(isAbortError(error)).toBe(true);
});
- it("should detect abort error by message content", () => {
- const error = new Error("Request was aborted");
+ it('should detect abort error by message content', () => {
+ const error = new Error('Request was aborted');
expect(isAbortError(error)).toBe(true);
});
- it("should return false for non-abort errors", () => {
- const error = new Error("Something else went wrong");
+ it('should return false for non-abort errors', () => {
+ const error = new Error('Something else went wrong');
expect(isAbortError(error)).toBe(false);
});
- it("should return false for non-Error objects", () => {
- expect(isAbortError("not an error")).toBe(false);
+ it('should return false for non-Error objects', () => {
+ expect(isAbortError('not an error')).toBe(false);
expect(isAbortError(null)).toBe(false);
expect(isAbortError(undefined)).toBe(false);
});
});
- describe("isCancellationError", () => {
+ describe('isCancellationError', () => {
it("should detect 'cancelled' message", () => {
- expect(isCancellationError("Operation was cancelled")).toBe(true);
+ expect(isCancellationError('Operation was cancelled')).toBe(true);
});
it("should detect 'canceled' message", () => {
- expect(isCancellationError("Request was canceled")).toBe(true);
+ expect(isCancellationError('Request was canceled')).toBe(true);
});
it("should detect 'stopped' message", () => {
- expect(isCancellationError("Process was stopped")).toBe(true);
+ expect(isCancellationError('Process was stopped')).toBe(true);
});
it("should detect 'aborted' message", () => {
- expect(isCancellationError("Task was aborted")).toBe(true);
+ expect(isCancellationError('Task was aborted')).toBe(true);
});
- it("should be case insensitive", () => {
- expect(isCancellationError("CANCELLED")).toBe(true);
- expect(isCancellationError("Canceled")).toBe(true);
+ it('should be case insensitive', () => {
+ expect(isCancellationError('CANCELLED')).toBe(true);
+ expect(isCancellationError('Canceled')).toBe(true);
});
- it("should return false for non-cancellation errors", () => {
- expect(isCancellationError("File not found")).toBe(false);
- expect(isCancellationError("Network error")).toBe(false);
+ it('should return false for non-cancellation errors', () => {
+ expect(isCancellationError('File not found')).toBe(false);
+ expect(isCancellationError('Network error')).toBe(false);
});
});
- describe("isAuthenticationError", () => {
+ describe('isAuthenticationError', () => {
it("should detect 'Authentication failed' message", () => {
- expect(isAuthenticationError("Authentication failed")).toBe(true);
+ expect(isAuthenticationError('Authentication failed')).toBe(true);
});
it("should detect 'Invalid API key' message", () => {
- expect(isAuthenticationError("Invalid API key provided")).toBe(true);
+ expect(isAuthenticationError('Invalid API key provided')).toBe(true);
});
it("should detect 'authentication_failed' message", () => {
- expect(isAuthenticationError("authentication_failed")).toBe(true);
+ expect(isAuthenticationError('authentication_failed')).toBe(true);
});
it("should detect 'Fix external API key' message", () => {
- expect(isAuthenticationError("Fix external API key configuration")).toBe(true);
+ expect(isAuthenticationError('Fix external API key configuration')).toBe(true);
});
- it("should return false for non-authentication errors", () => {
- expect(isAuthenticationError("Network connection error")).toBe(false);
- expect(isAuthenticationError("File not found")).toBe(false);
+ it('should return false for non-authentication errors', () => {
+ expect(isAuthenticationError('Network connection error')).toBe(false);
+ expect(isAuthenticationError('File not found')).toBe(false);
});
- it("should be case sensitive", () => {
- expect(isAuthenticationError("authentication Failed")).toBe(false);
+ it('should be case sensitive', () => {
+ expect(isAuthenticationError('authentication Failed')).toBe(false);
});
});
- describe("classifyError", () => {
- it("should classify authentication errors", () => {
- const error = new Error("Authentication failed");
+ describe('classifyError', () => {
+ it('should classify authentication errors', () => {
+ const error = new Error('Authentication failed');
const result = classifyError(error);
- expect(result.type).toBe("authentication");
+ expect(result.type).toBe('authentication');
expect(result.isAuth).toBe(true);
expect(result.isAbort).toBe(false);
- expect(result.message).toBe("Authentication failed");
+ expect(result.message).toBe('Authentication failed');
expect(result.originalError).toBe(error);
});
- it("should classify abort errors", () => {
- const error = new Error("Operation aborted");
- error.name = "AbortError";
+ it('should classify abort errors', () => {
+ const error = new Error('Operation aborted');
+ error.name = 'AbortError';
const result = classifyError(error);
- expect(result.type).toBe("abort");
+ expect(result.type).toBe('abort');
expect(result.isAbort).toBe(true);
expect(result.isAuth).toBe(false);
- expect(result.message).toBe("Operation aborted");
+ expect(result.message).toBe('Operation aborted');
});
- it("should prioritize auth over abort if both match", () => {
- const error = new Error("Authentication failed and aborted");
+ it('should prioritize auth over abort if both match', () => {
+ const error = new Error('Authentication failed and aborted');
const result = classifyError(error);
- expect(result.type).toBe("authentication");
+ expect(result.type).toBe('authentication');
expect(result.isAuth).toBe(true);
expect(result.isAbort).toBe(true); // Still detected as abort too
});
- it("should classify cancellation errors", () => {
- const error = new Error("Operation was cancelled");
+ it('should classify cancellation errors', () => {
+ const error = new Error('Operation was cancelled');
const result = classifyError(error);
- expect(result.type).toBe("cancellation");
+ expect(result.type).toBe('cancellation');
expect(result.isCancellation).toBe(true);
expect(result.isAbort).toBe(false);
expect(result.isAuth).toBe(false);
});
- it("should prioritize abort over cancellation if both match", () => {
- const error = new Error("Operation aborted");
- error.name = "AbortError";
+ it('should prioritize abort over cancellation if both match', () => {
+ const error = new Error('Operation aborted');
+ error.name = 'AbortError';
const result = classifyError(error);
- expect(result.type).toBe("abort");
+ expect(result.type).toBe('abort');
expect(result.isAbort).toBe(true);
expect(result.isCancellation).toBe(true); // Still detected as cancellation too
});
it("should classify cancellation errors with 'canceled' spelling", () => {
- const error = new Error("Request was canceled");
+ const error = new Error('Request was canceled');
const result = classifyError(error);
- expect(result.type).toBe("cancellation");
+ expect(result.type).toBe('cancellation');
expect(result.isCancellation).toBe(true);
});
it("should classify cancellation errors with 'stopped' message", () => {
- const error = new Error("Process was stopped");
+ const error = new Error('Process was stopped');
const result = classifyError(error);
- expect(result.type).toBe("cancellation");
+ expect(result.type).toBe('cancellation');
expect(result.isCancellation).toBe(true);
});
- it("should classify generic Error as execution error", () => {
- const error = new Error("Something went wrong");
+ it('should classify generic Error as execution error', () => {
+ const error = new Error('Something went wrong');
const result = classifyError(error);
- expect(result.type).toBe("execution");
+ expect(result.type).toBe('execution');
expect(result.isAuth).toBe(false);
expect(result.isAbort).toBe(false);
});
- it("should classify non-Error objects as unknown", () => {
- const error = "string error";
+ it('should classify non-Error objects as unknown', () => {
+ const error = 'string error';
const result = classifyError(error);
- expect(result.type).toBe("unknown");
- expect(result.message).toBe("string error");
+ expect(result.type).toBe('unknown');
+ expect(result.message).toBe('string error');
});
- it("should handle null and undefined", () => {
+ it('should handle null and undefined', () => {
const nullResult = classifyError(null);
- expect(nullResult.type).toBe("unknown");
- expect(nullResult.message).toBe("Unknown error");
+ expect(nullResult.type).toBe('unknown');
+ expect(nullResult.message).toBe('Unknown error');
const undefinedResult = classifyError(undefined);
- expect(undefinedResult.type).toBe("unknown");
- expect(undefinedResult.message).toBe("Unknown error");
+ expect(undefinedResult.type).toBe('unknown');
+ expect(undefinedResult.message).toBe('Unknown error');
});
});
- describe("getUserFriendlyErrorMessage", () => {
- it("should return friendly message for abort errors", () => {
- const error = new Error("abort");
+ describe('getUserFriendlyErrorMessage', () => {
+ it('should return friendly message for abort errors', () => {
+ const error = new Error('abort');
const result = getUserFriendlyErrorMessage(error);
- expect(result).toBe("Operation was cancelled");
+ expect(result).toBe('Operation was cancelled');
});
- it("should return friendly message for authentication errors", () => {
- const error = new Error("Authentication failed");
+ it('should return friendly message for authentication errors', () => {
+ const error = new Error('Authentication failed');
const result = getUserFriendlyErrorMessage(error);
- expect(result).toBe("Authentication failed. Please check your API key.");
+ expect(result).toBe('Authentication failed. Please check your API key.');
});
- it("should return original message for other errors", () => {
- const error = new Error("File not found");
+ it('should return original message for other errors', () => {
+ const error = new Error('File not found');
const result = getUserFriendlyErrorMessage(error);
- expect(result).toBe("File not found");
+ expect(result).toBe('File not found');
});
- it("should handle non-Error objects", () => {
- const result = getUserFriendlyErrorMessage("Custom error");
- expect(result).toBe("Custom error");
+ it('should handle non-Error objects', () => {
+ const result = getUserFriendlyErrorMessage('Custom error');
+ expect(result).toBe('Custom error');
});
});
});
diff --git a/apps/server/tests/unit/lib/events.test.ts b/apps/server/tests/unit/lib/events.test.ts
index 4741a365..a8a78092 100644
--- a/apps/server/tests/unit/lib/events.test.ts
+++ b/apps/server/tests/unit/lib/events.test.ts
@@ -1,20 +1,20 @@
-import { describe, it, expect, vi } from "vitest";
-import { createEventEmitter, type EventType } from "@/lib/events.js";
+import { describe, it, expect, vi } from 'vitest';
+import { createEventEmitter, type EventType } from '@/lib/events.js';
-describe("events.ts", () => {
- describe("createEventEmitter", () => {
- it("should emit events to single subscriber", () => {
+describe('events.ts', () => {
+ describe('createEventEmitter', () => {
+ it('should emit events to single subscriber', () => {
const emitter = createEventEmitter();
const callback = vi.fn();
emitter.subscribe(callback);
- emitter.emit("agent:stream", { message: "test" });
+ emitter.emit('agent:stream', { message: 'test' });
expect(callback).toHaveBeenCalledOnce();
- expect(callback).toHaveBeenCalledWith("agent:stream", { message: "test" });
+ expect(callback).toHaveBeenCalledWith('agent:stream', { message: 'test' });
});
- it("should emit events to multiple subscribers", () => {
+ it('should emit events to multiple subscribers', () => {
const emitter = createEventEmitter();
const callback1 = vi.fn();
const callback2 = vi.fn();
@@ -23,42 +23,42 @@ describe("events.ts", () => {
emitter.subscribe(callback1);
emitter.subscribe(callback2);
emitter.subscribe(callback3);
- emitter.emit("feature:started", { id: "123" });
+ emitter.emit('feature:started', { id: '123' });
expect(callback1).toHaveBeenCalledOnce();
expect(callback2).toHaveBeenCalledOnce();
expect(callback3).toHaveBeenCalledOnce();
- expect(callback1).toHaveBeenCalledWith("feature:started", { id: "123" });
+ expect(callback1).toHaveBeenCalledWith('feature:started', { id: '123' });
});
- it("should support unsubscribe functionality", () => {
+ it('should support unsubscribe functionality', () => {
const emitter = createEventEmitter();
const callback = vi.fn();
const unsubscribe = emitter.subscribe(callback);
- emitter.emit("agent:stream", { test: 1 });
+ emitter.emit('agent:stream', { test: 1 });
expect(callback).toHaveBeenCalledOnce();
unsubscribe();
- emitter.emit("agent:stream", { test: 2 });
+ emitter.emit('agent:stream', { test: 2 });
expect(callback).toHaveBeenCalledOnce(); // Still called only once
});
- it("should handle errors in subscribers without crashing", () => {
+ it('should handle errors in subscribers without crashing', () => {
const emitter = createEventEmitter();
const errorCallback = vi.fn(() => {
- throw new Error("Subscriber error");
+ throw new Error('Subscriber error');
});
const normalCallback = vi.fn();
- const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
emitter.subscribe(errorCallback);
emitter.subscribe(normalCallback);
expect(() => {
- emitter.emit("feature:error", { error: "test" });
+ emitter.emit('feature:error', { error: 'test' });
}).not.toThrow();
expect(errorCallback).toHaveBeenCalledOnce();
@@ -68,17 +68,17 @@ describe("events.ts", () => {
consoleSpy.mockRestore();
});
- it("should emit different event types", () => {
+ it('should emit different event types', () => {
const emitter = createEventEmitter();
const callback = vi.fn();
emitter.subscribe(callback);
const eventTypes: EventType[] = [
- "agent:stream",
- "auto-mode:started",
- "feature:completed",
- "project:analysis-progress",
+ 'agent:stream',
+ 'auto-mode:started',
+ 'feature:completed',
+ 'project:analysis-progress',
];
eventTypes.forEach((type) => {
@@ -88,15 +88,15 @@ describe("events.ts", () => {
expect(callback).toHaveBeenCalledTimes(4);
});
- it("should handle emitting without subscribers", () => {
+ it('should handle emitting without subscribers', () => {
const emitter = createEventEmitter();
expect(() => {
- emitter.emit("agent:stream", { test: true });
+ emitter.emit('agent:stream', { test: true });
}).not.toThrow();
});
- it("should allow multiple subscriptions and unsubscriptions", () => {
+ it('should allow multiple subscriptions and unsubscriptions', () => {
const emitter = createEventEmitter();
const callback1 = vi.fn();
const callback2 = vi.fn();
@@ -106,14 +106,14 @@ describe("events.ts", () => {
const unsub2 = emitter.subscribe(callback2);
const unsub3 = emitter.subscribe(callback3);
- emitter.emit("feature:started", { test: 1 });
+ emitter.emit('feature:started', { test: 1 });
expect(callback1).toHaveBeenCalledOnce();
expect(callback2).toHaveBeenCalledOnce();
expect(callback3).toHaveBeenCalledOnce();
unsub2();
- emitter.emit("feature:started", { test: 2 });
+ emitter.emit('feature:started', { test: 2 });
expect(callback1).toHaveBeenCalledTimes(2);
expect(callback2).toHaveBeenCalledOnce(); // Still just once
expect(callback3).toHaveBeenCalledTimes(2);
@@ -121,7 +121,7 @@ describe("events.ts", () => {
unsub1();
unsub3();
- emitter.emit("feature:started", { test: 3 });
+ emitter.emit('feature:started', { test: 3 });
expect(callback1).toHaveBeenCalledTimes(2);
expect(callback2).toHaveBeenCalledOnce();
expect(callback3).toHaveBeenCalledTimes(2);
diff --git a/apps/server/tests/unit/lib/fs-utils.test.ts b/apps/server/tests/unit/lib/fs-utils.test.ts
index 2174822a..6ac49e6d 100644
--- a/apps/server/tests/unit/lib/fs-utils.test.ts
+++ b/apps/server/tests/unit/lib/fs-utils.test.ts
@@ -1,10 +1,10 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
-import { mkdirSafe, existsSafe } from "@automaker/utils";
-import fs from "fs/promises";
-import path from "path";
-import os from "os";
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { mkdirSafe, existsSafe } from '@automaker/utils';
+import fs from 'fs/promises';
+import path from 'path';
+import os from 'os';
-describe("fs-utils.ts", () => {
+describe('fs-utils.ts', () => {
let testDir: string;
beforeEach(async () => {
@@ -22,43 +22,41 @@ describe("fs-utils.ts", () => {
}
});
- describe("mkdirSafe", () => {
- it("should create a new directory", async () => {
- const newDir = path.join(testDir, "new-directory");
+ describe('mkdirSafe', () => {
+ it('should create a new directory', async () => {
+ const newDir = path.join(testDir, 'new-directory');
await mkdirSafe(newDir);
const stats = await fs.stat(newDir);
expect(stats.isDirectory()).toBe(true);
});
- it("should succeed if directory already exists", async () => {
- const existingDir = path.join(testDir, "existing");
+ it('should succeed if directory already exists', async () => {
+ const existingDir = path.join(testDir, 'existing');
await fs.mkdir(existingDir);
// Should not throw
await expect(mkdirSafe(existingDir)).resolves.toBeUndefined();
});
- it("should create nested directories", async () => {
- const nestedDir = path.join(testDir, "a", "b", "c");
+ it('should create nested directories', async () => {
+ const nestedDir = path.join(testDir, 'a', 'b', 'c');
await mkdirSafe(nestedDir);
const stats = await fs.stat(nestedDir);
expect(stats.isDirectory()).toBe(true);
});
- it("should throw if path exists as a file", async () => {
- const filePath = path.join(testDir, "file.txt");
- await fs.writeFile(filePath, "content");
+ it('should throw if path exists as a file', async () => {
+ const filePath = path.join(testDir, 'file.txt');
+ await fs.writeFile(filePath, 'content');
- await expect(mkdirSafe(filePath)).rejects.toThrow(
- "Path exists and is not a directory"
- );
+ await expect(mkdirSafe(filePath)).rejects.toThrow('Path exists and is not a directory');
});
- it("should succeed if path is a symlink to a directory", async () => {
- const realDir = path.join(testDir, "real-dir");
- const symlinkPath = path.join(testDir, "link-to-dir");
+ it('should succeed if path is a symlink to a directory', async () => {
+ const realDir = path.join(testDir, 'real-dir');
+ const symlinkPath = path.join(testDir, 'link-to-dir');
await fs.mkdir(realDir);
await fs.symlink(realDir, symlinkPath);
@@ -66,12 +64,12 @@ describe("fs-utils.ts", () => {
await expect(mkdirSafe(symlinkPath)).resolves.toBeUndefined();
});
- it("should handle ELOOP error gracefully when checking path", async () => {
+ it('should handle ELOOP error gracefully when checking path', async () => {
// Mock lstat to throw ELOOP error
const originalLstat = fs.lstat;
- const mkdirSafePath = path.join(testDir, "eloop-path");
-
- vi.spyOn(fs, "lstat").mockRejectedValueOnce({ code: "ELOOP" });
+ const mkdirSafePath = path.join(testDir, 'eloop-path');
+
+ vi.spyOn(fs, 'lstat').mockRejectedValueOnce({ code: 'ELOOP' });
// Should not throw, should return gracefully
await expect(mkdirSafe(mkdirSafePath)).resolves.toBeUndefined();
@@ -79,13 +77,13 @@ describe("fs-utils.ts", () => {
vi.restoreAllMocks();
});
- it("should handle EEXIST error gracefully when creating directory", async () => {
- const newDir = path.join(testDir, "race-condition-dir");
-
+ it('should handle EEXIST error gracefully when creating directory', async () => {
+ const newDir = path.join(testDir, 'race-condition-dir');
+
// Mock lstat to return ENOENT (path doesn't exist)
// Then mock mkdir to throw EEXIST (race condition)
- vi.spyOn(fs, "lstat").mockRejectedValueOnce({ code: "ENOENT" });
- vi.spyOn(fs, "mkdir").mockRejectedValueOnce({ code: "EEXIST" });
+ vi.spyOn(fs, 'lstat').mockRejectedValueOnce({ code: 'ENOENT' });
+ vi.spyOn(fs, 'mkdir').mockRejectedValueOnce({ code: 'EEXIST' });
// Should not throw, should return gracefully
await expect(mkdirSafe(newDir)).resolves.toBeUndefined();
@@ -93,13 +91,13 @@ describe("fs-utils.ts", () => {
vi.restoreAllMocks();
});
- it("should handle ELOOP error gracefully when creating directory", async () => {
- const newDir = path.join(testDir, "eloop-create-dir");
-
+ it('should handle ELOOP error gracefully when creating directory', async () => {
+ const newDir = path.join(testDir, 'eloop-create-dir');
+
// Mock lstat to return ENOENT (path doesn't exist)
// Then mock mkdir to throw ELOOP
- vi.spyOn(fs, "lstat").mockRejectedValueOnce({ code: "ENOENT" });
- vi.spyOn(fs, "mkdir").mockRejectedValueOnce({ code: "ELOOP" });
+ vi.spyOn(fs, 'lstat').mockRejectedValueOnce({ code: 'ENOENT' });
+ vi.spyOn(fs, 'mkdir').mockRejectedValueOnce({ code: 'ELOOP' });
// Should not throw, should return gracefully
await expect(mkdirSafe(newDir)).resolves.toBeUndefined();
@@ -108,34 +106,34 @@ describe("fs-utils.ts", () => {
});
});
- describe("existsSafe", () => {
- it("should return true for existing file", async () => {
- const filePath = path.join(testDir, "test-file.txt");
- await fs.writeFile(filePath, "content");
+ describe('existsSafe', () => {
+ it('should return true for existing file', async () => {
+ const filePath = path.join(testDir, 'test-file.txt');
+ await fs.writeFile(filePath, 'content');
const exists = await existsSafe(filePath);
expect(exists).toBe(true);
});
- it("should return true for existing directory", async () => {
- const dirPath = path.join(testDir, "test-dir");
+ it('should return true for existing directory', async () => {
+ const dirPath = path.join(testDir, 'test-dir');
await fs.mkdir(dirPath);
const exists = await existsSafe(dirPath);
expect(exists).toBe(true);
});
- it("should return false for non-existent path", async () => {
- const nonExistent = path.join(testDir, "does-not-exist");
+ it('should return false for non-existent path', async () => {
+ const nonExistent = path.join(testDir, 'does-not-exist');
const exists = await existsSafe(nonExistent);
expect(exists).toBe(false);
});
- it("should return true for symlink", async () => {
- const realFile = path.join(testDir, "real-file.txt");
- const symlinkPath = path.join(testDir, "link-to-file");
- await fs.writeFile(realFile, "content");
+ it('should return true for symlink', async () => {
+ const realFile = path.join(testDir, 'real-file.txt');
+ const symlinkPath = path.join(testDir, 'link-to-file');
+ await fs.writeFile(realFile, 'content');
await fs.symlink(realFile, symlinkPath);
const exists = await existsSafe(symlinkPath);
@@ -143,29 +141,29 @@ describe("fs-utils.ts", () => {
});
it("should return true for broken symlink (symlink exists even if target doesn't)", async () => {
- const symlinkPath = path.join(testDir, "broken-link");
- const nonExistent = path.join(testDir, "non-existent-target");
+ const symlinkPath = path.join(testDir, 'broken-link');
+ const nonExistent = path.join(testDir, 'non-existent-target');
await fs.symlink(nonExistent, symlinkPath);
const exists = await existsSafe(symlinkPath);
expect(exists).toBe(true);
});
- it("should return true for ELOOP error (symlink loop)", async () => {
+ it('should return true for ELOOP error (symlink loop)', async () => {
// Mock lstat to throw ELOOP error
- vi.spyOn(fs, "lstat").mockRejectedValueOnce({ code: "ELOOP" });
+ vi.spyOn(fs, 'lstat').mockRejectedValueOnce({ code: 'ELOOP' });
- const exists = await existsSafe("/some/path/with/loop");
+ const exists = await existsSafe('/some/path/with/loop');
expect(exists).toBe(true);
vi.restoreAllMocks();
});
- it("should throw for other errors", async () => {
+ it('should throw for other errors', async () => {
// Mock lstat to throw a non-ENOENT, non-ELOOP error
- vi.spyOn(fs, "lstat").mockRejectedValueOnce({ code: "EACCES" });
+ vi.spyOn(fs, 'lstat').mockRejectedValueOnce({ code: 'EACCES' });
- await expect(existsSafe("/some/path")).rejects.toMatchObject({ code: "EACCES" });
+ await expect(existsSafe('/some/path')).rejects.toMatchObject({ code: 'EACCES' });
vi.restoreAllMocks();
});
diff --git a/apps/server/tests/unit/lib/image-handler.test.ts b/apps/server/tests/unit/lib/image-handler.test.ts
index f57ef0e5..18b04155 100644
--- a/apps/server/tests/unit/lib/image-handler.test.ts
+++ b/apps/server/tests/unit/lib/image-handler.test.ts
@@ -1,174 +1,164 @@
-import { describe, it, expect, vi, beforeEach } from "vitest";
+import { describe, it, expect, vi, beforeEach } from 'vitest';
import {
getMimeTypeForImage,
readImageAsBase64,
convertImagesToContentBlocks,
formatImagePathsForPrompt,
-} from "@automaker/utils";
-import { pngBase64Fixture } from "../../fixtures/images.js";
-import * as fs from "fs/promises";
+} from '@automaker/utils';
+import { pngBase64Fixture } from '../../fixtures/images.js';
+import * as fs from 'fs/promises';
-vi.mock("fs/promises");
+vi.mock('fs/promises');
-describe("image-handler.ts", () => {
+describe('image-handler.ts', () => {
beforeEach(() => {
vi.clearAllMocks();
});
- describe("getMimeTypeForImage", () => {
- it("should return correct MIME type for .jpg", () => {
- expect(getMimeTypeForImage("test.jpg")).toBe("image/jpeg");
- expect(getMimeTypeForImage("/path/to/test.jpg")).toBe("image/jpeg");
+ describe('getMimeTypeForImage', () => {
+ it('should return correct MIME type for .jpg', () => {
+ expect(getMimeTypeForImage('test.jpg')).toBe('image/jpeg');
+ expect(getMimeTypeForImage('/path/to/test.jpg')).toBe('image/jpeg');
});
- it("should return correct MIME type for .jpeg", () => {
- expect(getMimeTypeForImage("test.jpeg")).toBe("image/jpeg");
+ it('should return correct MIME type for .jpeg', () => {
+ expect(getMimeTypeForImage('test.jpeg')).toBe('image/jpeg');
});
- it("should return correct MIME type for .png", () => {
- expect(getMimeTypeForImage("test.png")).toBe("image/png");
+ it('should return correct MIME type for .png', () => {
+ expect(getMimeTypeForImage('test.png')).toBe('image/png');
});
- it("should return correct MIME type for .gif", () => {
- expect(getMimeTypeForImage("test.gif")).toBe("image/gif");
+ it('should return correct MIME type for .gif', () => {
+ expect(getMimeTypeForImage('test.gif')).toBe('image/gif');
});
- it("should return correct MIME type for .webp", () => {
- expect(getMimeTypeForImage("test.webp")).toBe("image/webp");
+ it('should return correct MIME type for .webp', () => {
+ expect(getMimeTypeForImage('test.webp')).toBe('image/webp');
});
- it("should be case-insensitive", () => {
- expect(getMimeTypeForImage("test.PNG")).toBe("image/png");
- expect(getMimeTypeForImage("test.JPG")).toBe("image/jpeg");
- expect(getMimeTypeForImage("test.GIF")).toBe("image/gif");
- expect(getMimeTypeForImage("test.WEBP")).toBe("image/webp");
+ it('should be case-insensitive', () => {
+ expect(getMimeTypeForImage('test.PNG')).toBe('image/png');
+ expect(getMimeTypeForImage('test.JPG')).toBe('image/jpeg');
+ expect(getMimeTypeForImage('test.GIF')).toBe('image/gif');
+ expect(getMimeTypeForImage('test.WEBP')).toBe('image/webp');
});
- it("should default to image/png for unknown extensions", () => {
- expect(getMimeTypeForImage("test.unknown")).toBe("image/png");
- expect(getMimeTypeForImage("test.txt")).toBe("image/png");
- expect(getMimeTypeForImage("test")).toBe("image/png");
+ it('should default to image/png for unknown extensions', () => {
+ expect(getMimeTypeForImage('test.unknown')).toBe('image/png');
+ expect(getMimeTypeForImage('test.txt')).toBe('image/png');
+ expect(getMimeTypeForImage('test')).toBe('image/png');
});
- it("should handle paths with multiple dots", () => {
- expect(getMimeTypeForImage("my.image.file.jpg")).toBe("image/jpeg");
+ it('should handle paths with multiple dots', () => {
+ expect(getMimeTypeForImage('my.image.file.jpg')).toBe('image/jpeg');
});
});
- describe("readImageAsBase64", () => {
- it("should read image and return base64 data", async () => {
- const mockBuffer = Buffer.from(pngBase64Fixture, "base64");
+ describe('readImageAsBase64', () => {
+ it('should read image and return base64 data', async () => {
+ const mockBuffer = Buffer.from(pngBase64Fixture, 'base64');
vi.mocked(fs.readFile).mockResolvedValue(mockBuffer);
- const result = await readImageAsBase64("/path/to/test.png");
+ const result = await readImageAsBase64('/path/to/test.png');
expect(result).toMatchObject({
base64: pngBase64Fixture,
- mimeType: "image/png",
- filename: "test.png",
- originalPath: "/path/to/test.png",
+ mimeType: 'image/png',
+ filename: 'test.png',
+ originalPath: '/path/to/test.png',
});
- expect(fs.readFile).toHaveBeenCalledWith("/path/to/test.png");
+ expect(fs.readFile).toHaveBeenCalledWith('/path/to/test.png');
});
- it("should handle different image formats", async () => {
- const mockBuffer = Buffer.from("jpeg-data");
+ it('should handle different image formats', async () => {
+ const mockBuffer = Buffer.from('jpeg-data');
vi.mocked(fs.readFile).mockResolvedValue(mockBuffer);
- const result = await readImageAsBase64("/path/to/photo.jpg");
+ const result = await readImageAsBase64('/path/to/photo.jpg');
- expect(result.mimeType).toBe("image/jpeg");
- expect(result.filename).toBe("photo.jpg");
- expect(result.base64).toBe(mockBuffer.toString("base64"));
+ expect(result.mimeType).toBe('image/jpeg');
+ expect(result.filename).toBe('photo.jpg');
+ expect(result.base64).toBe(mockBuffer.toString('base64'));
});
- it("should extract filename from path", async () => {
- const mockBuffer = Buffer.from("data");
+ it('should extract filename from path', async () => {
+ const mockBuffer = Buffer.from('data');
vi.mocked(fs.readFile).mockResolvedValue(mockBuffer);
- const result = await readImageAsBase64("/deep/nested/path/image.webp");
+ const result = await readImageAsBase64('/deep/nested/path/image.webp');
- expect(result.filename).toBe("image.webp");
+ expect(result.filename).toBe('image.webp');
});
- it("should throw error if file cannot be read", async () => {
- vi.mocked(fs.readFile).mockRejectedValue(new Error("File not found"));
+ it('should throw error if file cannot be read', async () => {
+ vi.mocked(fs.readFile).mockRejectedValue(new Error('File not found'));
- await expect(readImageAsBase64("/nonexistent.png")).rejects.toThrow(
- "File not found"
- );
+ await expect(readImageAsBase64('/nonexistent.png')).rejects.toThrow('File not found');
});
});
- describe("convertImagesToContentBlocks", () => {
- it("should convert single image to content block", async () => {
- const mockBuffer = Buffer.from(pngBase64Fixture, "base64");
+ describe('convertImagesToContentBlocks', () => {
+ it('should convert single image to content block', async () => {
+ const mockBuffer = Buffer.from(pngBase64Fixture, 'base64');
vi.mocked(fs.readFile).mockResolvedValue(mockBuffer);
- const result = await convertImagesToContentBlocks(["/path/test.png"]);
+ const result = await convertImagesToContentBlocks(['/path/test.png']);
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
- type: "image",
+ type: 'image',
source: {
- type: "base64",
- media_type: "image/png",
+ type: 'base64',
+ media_type: 'image/png',
data: pngBase64Fixture,
},
});
});
- it("should convert multiple images to content blocks", async () => {
- const mockBuffer = Buffer.from("test-data");
+ it('should convert multiple images to content blocks', async () => {
+ const mockBuffer = Buffer.from('test-data');
vi.mocked(fs.readFile).mockResolvedValue(mockBuffer);
- const result = await convertImagesToContentBlocks([
- "/a.png",
- "/b.jpg",
- "/c.webp",
- ]);
+ const result = await convertImagesToContentBlocks(['/a.png', '/b.jpg', '/c.webp']);
expect(result).toHaveLength(3);
- expect(result[0].source.media_type).toBe("image/png");
- expect(result[1].source.media_type).toBe("image/jpeg");
- expect(result[2].source.media_type).toBe("image/webp");
+ expect(result[0].source.media_type).toBe('image/png');
+ expect(result[1].source.media_type).toBe('image/jpeg');
+ expect(result[2].source.media_type).toBe('image/webp');
});
- it("should resolve relative paths with workDir", async () => {
- const mockBuffer = Buffer.from("data");
+ it('should resolve relative paths with workDir', async () => {
+ const mockBuffer = Buffer.from('data');
vi.mocked(fs.readFile).mockResolvedValue(mockBuffer);
- await convertImagesToContentBlocks(["relative.png"], "/work/dir");
+ await convertImagesToContentBlocks(['relative.png'], '/work/dir');
// Use path-agnostic check since Windows uses backslashes
const calls = vi.mocked(fs.readFile).mock.calls;
expect(calls[0][0]).toMatch(/relative\.png$/);
- expect(calls[0][0]).toContain("work");
- expect(calls[0][0]).toContain("dir");
+ expect(calls[0][0]).toContain('work');
+ expect(calls[0][0]).toContain('dir');
});
- it("should handle absolute paths without workDir", async () => {
- const mockBuffer = Buffer.from("data");
+ it('should handle absolute paths without workDir', async () => {
+ const mockBuffer = Buffer.from('data');
vi.mocked(fs.readFile).mockResolvedValue(mockBuffer);
- await convertImagesToContentBlocks(["/absolute/path.png"]);
+ await convertImagesToContentBlocks(['/absolute/path.png']);
- expect(fs.readFile).toHaveBeenCalledWith("/absolute/path.png");
+ expect(fs.readFile).toHaveBeenCalledWith('/absolute/path.png');
});
- it("should continue processing on individual image errors", async () => {
+ it('should continue processing on individual image errors', async () => {
vi.mocked(fs.readFile)
- .mockResolvedValueOnce(Buffer.from("ok1"))
- .mockRejectedValueOnce(new Error("Failed"))
- .mockResolvedValueOnce(Buffer.from("ok2"));
+ .mockResolvedValueOnce(Buffer.from('ok1'))
+ .mockRejectedValueOnce(new Error('Failed'))
+ .mockResolvedValueOnce(Buffer.from('ok2'));
- const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
- const result = await convertImagesToContentBlocks([
- "/a.png",
- "/b.png",
- "/c.png",
- ]);
+ const result = await convertImagesToContentBlocks(['/a.png', '/b.png', '/c.png']);
expect(result).toHaveLength(2); // Only successful images
expect(consoleSpy).toHaveBeenCalled();
@@ -176,56 +166,52 @@ describe("image-handler.ts", () => {
consoleSpy.mockRestore();
});
- it("should return empty array for empty input", async () => {
+ it('should return empty array for empty input', async () => {
const result = await convertImagesToContentBlocks([]);
expect(result).toEqual([]);
});
- it("should handle undefined workDir", async () => {
- const mockBuffer = Buffer.from("data");
+ it('should handle undefined workDir', async () => {
+ const mockBuffer = Buffer.from('data');
vi.mocked(fs.readFile).mockResolvedValue(mockBuffer);
- const result = await convertImagesToContentBlocks(["/test.png"], undefined);
+ const result = await convertImagesToContentBlocks(['/test.png'], undefined);
expect(result).toHaveLength(1);
- expect(fs.readFile).toHaveBeenCalledWith("/test.png");
+ expect(fs.readFile).toHaveBeenCalledWith('/test.png');
});
});
- describe("formatImagePathsForPrompt", () => {
- it("should format single image path as bulleted list", () => {
- const result = formatImagePathsForPrompt(["/path/image.png"]);
+ describe('formatImagePathsForPrompt', () => {
+ it('should format single image path as bulleted list', () => {
+ const result = formatImagePathsForPrompt(['/path/image.png']);
- expect(result).toContain("\n\nAttached images:");
- expect(result).toContain("- /path/image.png");
+ expect(result).toContain('\n\nAttached images:');
+ expect(result).toContain('- /path/image.png');
});
- it("should format multiple image paths as bulleted list", () => {
- const result = formatImagePathsForPrompt([
- "/path/a.png",
- "/path/b.jpg",
- "/path/c.webp",
- ]);
+ it('should format multiple image paths as bulleted list', () => {
+ const result = formatImagePathsForPrompt(['/path/a.png', '/path/b.jpg', '/path/c.webp']);
- expect(result).toContain("Attached images:");
- expect(result).toContain("- /path/a.png");
- expect(result).toContain("- /path/b.jpg");
- expect(result).toContain("- /path/c.webp");
+ expect(result).toContain('Attached images:');
+ expect(result).toContain('- /path/a.png');
+ expect(result).toContain('- /path/b.jpg');
+ expect(result).toContain('- /path/c.webp');
});
- it("should return empty string for empty array", () => {
+ it('should return empty string for empty array', () => {
const result = formatImagePathsForPrompt([]);
- expect(result).toBe("");
+ expect(result).toBe('');
});
- it("should start with double newline", () => {
- const result = formatImagePathsForPrompt(["/test.png"]);
- expect(result.startsWith("\n\n")).toBe(true);
+ it('should start with double newline', () => {
+ const result = formatImagePathsForPrompt(['/test.png']);
+ expect(result.startsWith('\n\n')).toBe(true);
});
- it("should handle paths with special characters", () => {
- const result = formatImagePathsForPrompt(["/path/with spaces/image.png"]);
- expect(result).toContain("- /path/with spaces/image.png");
+ it('should handle paths with special characters', () => {
+ const result = formatImagePathsForPrompt(['/path/with spaces/image.png']);
+ expect(result).toContain('- /path/with spaces/image.png');
});
});
});
diff --git a/apps/server/tests/unit/lib/logger.test.ts b/apps/server/tests/unit/lib/logger.test.ts
index fa6034b0..56d98a9f 100644
--- a/apps/server/tests/unit/lib/logger.test.ts
+++ b/apps/server/tests/unit/lib/logger.test.ts
@@ -1,12 +1,7 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
-import {
- LogLevel,
- createLogger,
- getLogLevel,
- setLogLevel,
-} from "@automaker/utils";
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { LogLevel, createLogger, getLogLevel, setLogLevel } from '@automaker/utils';
-describe("logger.ts", () => {
+describe('logger.ts', () => {
let consoleSpy: {
log: ReturnType;
warn: ReturnType;
@@ -17,9 +12,9 @@ describe("logger.ts", () => {
beforeEach(() => {
originalLogLevel = getLogLevel();
consoleSpy = {
- log: vi.spyOn(console, "log").mockImplementation(() => {}),
- warn: vi.spyOn(console, "warn").mockImplementation(() => {}),
- error: vi.spyOn(console, "error").mockImplementation(() => {}),
+ log: vi.spyOn(console, 'log').mockImplementation(() => {}),
+ warn: vi.spyOn(console, 'warn').mockImplementation(() => {}),
+ error: vi.spyOn(console, 'error').mockImplementation(() => {}),
};
});
@@ -30,8 +25,8 @@ describe("logger.ts", () => {
consoleSpy.error.mockRestore();
});
- describe("LogLevel enum", () => {
- it("should have correct numeric values", () => {
+ describe('LogLevel enum', () => {
+ it('should have correct numeric values', () => {
expect(LogLevel.ERROR).toBe(0);
expect(LogLevel.WARN).toBe(1);
expect(LogLevel.INFO).toBe(2);
@@ -39,8 +34,8 @@ describe("logger.ts", () => {
});
});
- describe("setLogLevel and getLogLevel", () => {
- it("should set and get log level", () => {
+ describe('setLogLevel and getLogLevel', () => {
+ it('should set and get log level', () => {
setLogLevel(LogLevel.DEBUG);
expect(getLogLevel()).toBe(LogLevel.DEBUG);
@@ -49,71 +44,66 @@ describe("logger.ts", () => {
});
});
- describe("createLogger", () => {
- it("should create a logger with context prefix", () => {
+ describe('createLogger', () => {
+ it('should create a logger with context prefix', () => {
setLogLevel(LogLevel.INFO);
- const logger = createLogger("TestContext");
+ const logger = createLogger('TestContext');
- logger.info("test message");
+ logger.info('test message');
- expect(consoleSpy.log).toHaveBeenCalledWith("[TestContext]", "test message");
+ expect(consoleSpy.log).toHaveBeenCalledWith('[TestContext]', 'test message');
});
- it("should log error at all log levels", () => {
- const logger = createLogger("Test");
+ it('should log error at all log levels', () => {
+ const logger = createLogger('Test');
setLogLevel(LogLevel.ERROR);
- logger.error("error message");
- expect(consoleSpy.error).toHaveBeenCalledWith("[Test]", "error message");
+ logger.error('error message');
+ expect(consoleSpy.error).toHaveBeenCalledWith('[Test]', 'error message');
});
- it("should log warn when level is WARN or higher", () => {
- const logger = createLogger("Test");
+ it('should log warn when level is WARN or higher', () => {
+ const logger = createLogger('Test');
setLogLevel(LogLevel.ERROR);
- logger.warn("warn message 1");
+ logger.warn('warn message 1');
expect(consoleSpy.warn).not.toHaveBeenCalled();
setLogLevel(LogLevel.WARN);
- logger.warn("warn message 2");
- expect(consoleSpy.warn).toHaveBeenCalledWith("[Test]", "warn message 2");
+ logger.warn('warn message 2');
+ expect(consoleSpy.warn).toHaveBeenCalledWith('[Test]', 'warn message 2');
});
- it("should log info when level is INFO or higher", () => {
- const logger = createLogger("Test");
+ it('should log info when level is INFO or higher', () => {
+ const logger = createLogger('Test');
setLogLevel(LogLevel.WARN);
- logger.info("info message 1");
+ logger.info('info message 1');
expect(consoleSpy.log).not.toHaveBeenCalled();
setLogLevel(LogLevel.INFO);
- logger.info("info message 2");
- expect(consoleSpy.log).toHaveBeenCalledWith("[Test]", "info message 2");
+ logger.info('info message 2');
+ expect(consoleSpy.log).toHaveBeenCalledWith('[Test]', 'info message 2');
});
- it("should log debug only when level is DEBUG", () => {
- const logger = createLogger("Test");
+ it('should log debug only when level is DEBUG', () => {
+ const logger = createLogger('Test');
setLogLevel(LogLevel.INFO);
- logger.debug("debug message 1");
+ logger.debug('debug message 1');
expect(consoleSpy.log).not.toHaveBeenCalled();
setLogLevel(LogLevel.DEBUG);
- logger.debug("debug message 2");
- expect(consoleSpy.log).toHaveBeenCalledWith("[Test]", "[DEBUG]", "debug message 2");
+ logger.debug('debug message 2');
+ expect(consoleSpy.log).toHaveBeenCalledWith('[Test]', '[DEBUG]', 'debug message 2');
});
- it("should pass multiple arguments to log functions", () => {
+ it('should pass multiple arguments to log functions', () => {
setLogLevel(LogLevel.DEBUG);
- const logger = createLogger("Multi");
+ const logger = createLogger('Multi');
- logger.info("message", { data: "value" }, 123);
- expect(consoleSpy.log).toHaveBeenCalledWith(
- "[Multi]",
- "message",
- { data: "value" },
- 123
- );
+ logger.info('message', { data: 'value' }, 123);
+ expect(consoleSpy.log).toHaveBeenCalledWith('[Multi]', 'message', { data: 'value' }, 123);
});
});
});
diff --git a/apps/server/tests/unit/lib/model-resolver.test.ts b/apps/server/tests/unit/lib/model-resolver.test.ts
index bda6b380..5eb1fa70 100644
--- a/apps/server/tests/unit/lib/model-resolver.test.ts
+++ b/apps/server/tests/unit/lib/model-resolver.test.ts
@@ -1,18 +1,18 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
resolveModelString,
getEffectiveModel,
CLAUDE_MODEL_MAP,
DEFAULT_MODELS,
-} from "@automaker/model-resolver";
+} from '@automaker/model-resolver';
-describe("model-resolver.ts", () => {
+describe('model-resolver.ts', () => {
let consoleSpy: any;
beforeEach(() => {
consoleSpy = {
- log: vi.spyOn(console, "log").mockImplementation(() => {}),
- warn: vi.spyOn(console, "warn").mockImplementation(() => {}),
+ log: vi.spyOn(console, 'log').mockImplementation(() => {}),
+ warn: vi.spyOn(console, 'warn').mockImplementation(() => {}),
};
});
@@ -21,27 +21,27 @@ describe("model-resolver.ts", () => {
consoleSpy.warn.mockRestore();
});
- describe("resolveModelString", () => {
+ describe('resolveModelString', () => {
it("should resolve 'haiku' alias to full model string", () => {
- const result = resolveModelString("haiku");
- expect(result).toBe("claude-haiku-4-5");
+ const result = resolveModelString('haiku');
+ expect(result).toBe('claude-haiku-4-5');
});
it("should resolve 'sonnet' alias to full model string", () => {
- const result = resolveModelString("sonnet");
- expect(result).toBe("claude-sonnet-4-20250514");
+ const result = resolveModelString('sonnet');
+ expect(result).toBe('claude-sonnet-4-20250514');
});
it("should resolve 'opus' alias to full model string", () => {
- const result = resolveModelString("opus");
- expect(result).toBe("claude-opus-4-5-20251101");
+ const result = resolveModelString('opus');
+ expect(result).toBe('claude-opus-4-5-20251101');
expect(consoleSpy.log).toHaveBeenCalledWith(
expect.stringContaining('Resolved model alias: "opus"')
);
});
- it("should treat unknown models as falling back to default", () => {
- const models = ["o1", "o1-mini", "o3", "gpt-5.2", "unknown-model"];
+ it('should treat unknown models as falling back to default', () => {
+ const models = ['o1', 'o1-mini', 'o3', 'gpt-5.2', 'unknown-model'];
models.forEach((model) => {
const result = resolveModelString(model);
// Should fall back to default since these aren't supported
@@ -49,95 +49,91 @@ describe("model-resolver.ts", () => {
});
});
- it("should pass through full Claude model strings", () => {
- const models = [
- "claude-opus-4-5-20251101",
- "claude-sonnet-4-20250514",
- "claude-haiku-4-5",
- ];
+ it('should pass through full Claude model strings', () => {
+ const models = ['claude-opus-4-5-20251101', 'claude-sonnet-4-20250514', 'claude-haiku-4-5'];
models.forEach((model) => {
const result = resolveModelString(model);
expect(result).toBe(model);
});
expect(consoleSpy.log).toHaveBeenCalledWith(
- expect.stringContaining("Using full Claude model string")
+ expect.stringContaining('Using full Claude model string')
);
});
- it("should return default model when modelKey is undefined", () => {
+ it('should return default model when modelKey is undefined', () => {
const result = resolveModelString(undefined);
expect(result).toBe(DEFAULT_MODELS.claude);
});
- it("should return custom default model when provided", () => {
- const customDefault = "custom-model";
+ it('should return custom default model when provided', () => {
+ const customDefault = 'custom-model';
const result = resolveModelString(undefined, customDefault);
expect(result).toBe(customDefault);
});
- it("should return default for unknown model key", () => {
- const result = resolveModelString("unknown-model");
+ it('should return default for unknown model key', () => {
+ const result = resolveModelString('unknown-model');
expect(result).toBe(DEFAULT_MODELS.claude);
expect(consoleSpy.warn).toHaveBeenCalledWith(
expect.stringContaining('Unknown model key "unknown-model"')
);
});
- it("should handle empty string", () => {
- const result = resolveModelString("");
+ it('should handle empty string', () => {
+ const result = resolveModelString('');
expect(result).toBe(DEFAULT_MODELS.claude);
});
});
- describe("getEffectiveModel", () => {
- it("should prioritize explicit model over session and default", () => {
- const result = getEffectiveModel("opus", "haiku", "gpt-5.2");
- expect(result).toBe("claude-opus-4-5-20251101");
+ describe('getEffectiveModel', () => {
+ it('should prioritize explicit model over session and default', () => {
+ const result = getEffectiveModel('opus', 'haiku', 'gpt-5.2');
+ expect(result).toBe('claude-opus-4-5-20251101');
});
- it("should use session model when explicit is not provided", () => {
- const result = getEffectiveModel(undefined, "sonnet", "gpt-5.2");
- expect(result).toBe("claude-sonnet-4-20250514");
+ it('should use session model when explicit is not provided', () => {
+ const result = getEffectiveModel(undefined, 'sonnet', 'gpt-5.2');
+ expect(result).toBe('claude-sonnet-4-20250514');
});
- it("should use default when neither explicit nor session is provided", () => {
- const customDefault = "claude-haiku-4-5";
+ it('should use default when neither explicit nor session is provided', () => {
+ const customDefault = 'claude-haiku-4-5';
const result = getEffectiveModel(undefined, undefined, customDefault);
expect(result).toBe(customDefault);
});
- it("should use Claude default when no arguments provided", () => {
+ it('should use Claude default when no arguments provided', () => {
const result = getEffectiveModel();
expect(result).toBe(DEFAULT_MODELS.claude);
});
- it("should handle explicit empty strings as undefined", () => {
- const result = getEffectiveModel("", "haiku");
- expect(result).toBe("claude-haiku-4-5");
+ it('should handle explicit empty strings as undefined', () => {
+ const result = getEffectiveModel('', 'haiku');
+ expect(result).toBe('claude-haiku-4-5');
});
});
- describe("CLAUDE_MODEL_MAP", () => {
- it("should have haiku, sonnet, opus mappings", () => {
- expect(CLAUDE_MODEL_MAP).toHaveProperty("haiku");
- expect(CLAUDE_MODEL_MAP).toHaveProperty("sonnet");
- expect(CLAUDE_MODEL_MAP).toHaveProperty("opus");
+ describe('CLAUDE_MODEL_MAP', () => {
+ it('should have haiku, sonnet, opus mappings', () => {
+ expect(CLAUDE_MODEL_MAP).toHaveProperty('haiku');
+ expect(CLAUDE_MODEL_MAP).toHaveProperty('sonnet');
+ expect(CLAUDE_MODEL_MAP).toHaveProperty('opus');
});
- it("should have valid Claude model strings", () => {
- expect(CLAUDE_MODEL_MAP.haiku).toContain("haiku");
- expect(CLAUDE_MODEL_MAP.sonnet).toContain("sonnet");
- expect(CLAUDE_MODEL_MAP.opus).toContain("opus");
+ it('should have valid Claude model strings', () => {
+ expect(CLAUDE_MODEL_MAP.haiku).toContain('haiku');
+ expect(CLAUDE_MODEL_MAP.sonnet).toContain('sonnet');
+ expect(CLAUDE_MODEL_MAP.opus).toContain('opus');
});
});
- describe("DEFAULT_MODELS", () => {
- it("should have claude default", () => {
- expect(DEFAULT_MODELS).toHaveProperty("claude");
+ describe('DEFAULT_MODELS', () => {
+ it('should have claude default', () => {
+ expect(DEFAULT_MODELS).toHaveProperty('claude');
});
- it("should have valid default model", () => {
- expect(DEFAULT_MODELS.claude).toContain("claude");
+ it('should have valid default model', () => {
+ expect(DEFAULT_MODELS.claude).toContain('claude');
});
});
});
diff --git a/apps/server/tests/unit/lib/prompt-builder.test.ts b/apps/server/tests/unit/lib/prompt-builder.test.ts
index 6f76b209..1577c4aa 100644
--- a/apps/server/tests/unit/lib/prompt-builder.test.ts
+++ b/apps/server/tests/unit/lib/prompt-builder.test.ts
@@ -1,129 +1,120 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
-import * as utils from "@automaker/utils";
-import * as fs from "fs/promises";
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import * as utils from '@automaker/utils';
+import * as fs from 'fs/promises';
// Mock fs module for the image-handler's readFile calls
-vi.mock("fs/promises");
+vi.mock('fs/promises');
-describe("prompt-builder.ts", () => {
+describe('prompt-builder.ts', () => {
beforeEach(() => {
vi.clearAllMocks();
// Setup default mock for fs.readFile to return a valid image buffer
- vi.mocked(fs.readFile).mockResolvedValue(Buffer.from("fake-image-data"));
+ vi.mocked(fs.readFile).mockResolvedValue(Buffer.from('fake-image-data'));
});
afterEach(() => {
vi.restoreAllMocks();
});
- describe("buildPromptWithImages", () => {
- it("should return plain text when no images provided", async () => {
- const result = await utils.buildPromptWithImages("Hello world");
+ describe('buildPromptWithImages', () => {
+ it('should return plain text when no images provided', async () => {
+ const result = await utils.buildPromptWithImages('Hello world');
expect(result).toEqual({
- content: "Hello world",
+ content: 'Hello world',
hasImages: false,
});
});
- it("should return plain text when imagePaths is empty array", async () => {
- const result = await utils.buildPromptWithImages("Hello world", []);
+ it('should return plain text when imagePaths is empty array', async () => {
+ const result = await utils.buildPromptWithImages('Hello world', []);
expect(result).toEqual({
- content: "Hello world",
+ content: 'Hello world',
hasImages: false,
});
});
- it("should build content blocks with single image", async () => {
- const result = await utils.buildPromptWithImages("Describe this image", [
- "/test.png",
- ]);
+ it('should build content blocks with single image', async () => {
+ const result = await utils.buildPromptWithImages('Describe this image', ['/test.png']);
expect(result.hasImages).toBe(true);
expect(Array.isArray(result.content)).toBe(true);
const content = result.content as Array<{ type: string; text?: string }>;
expect(content).toHaveLength(2);
- expect(content[0]).toEqual({ type: "text", text: "Describe this image" });
- expect(content[1].type).toBe("image");
+ expect(content[0]).toEqual({ type: 'text', text: 'Describe this image' });
+ expect(content[1].type).toBe('image');
});
- it("should build content blocks with multiple images", async () => {
- const result = await utils.buildPromptWithImages("Analyze these", [
- "/a.png",
- "/b.jpg",
- ]);
+ it('should build content blocks with multiple images', async () => {
+ const result = await utils.buildPromptWithImages('Analyze these', ['/a.png', '/b.jpg']);
expect(result.hasImages).toBe(true);
const content = result.content as Array<{ type: string }>;
expect(content).toHaveLength(3); // 1 text + 2 images
- expect(content[0].type).toBe("text");
- expect(content[1].type).toBe("image");
- expect(content[2].type).toBe("image");
+ expect(content[0].type).toBe('text');
+ expect(content[1].type).toBe('image');
+ expect(content[2].type).toBe('image');
});
- it("should include image paths in text when requested", async () => {
+ it('should include image paths in text when requested', async () => {
const result = await utils.buildPromptWithImages(
- "Base prompt",
- ["/test.png"],
+ 'Base prompt',
+ ['/test.png'],
undefined,
true
);
const content = result.content as Array<{ type: string; text?: string }>;
- expect(content[0].text).toContain("Base prompt");
- expect(content[0].text).toContain("/test.png");
+ expect(content[0].text).toContain('Base prompt');
+ expect(content[0].text).toContain('/test.png');
});
- it("should not include image paths by default", async () => {
- const result = await utils.buildPromptWithImages("Base prompt", ["/test.png"]);
+ it('should not include image paths by default', async () => {
+ const result = await utils.buildPromptWithImages('Base prompt', ['/test.png']);
const content = result.content as Array<{ type: string; text?: string }>;
- expect(content[0].text).toBe("Base prompt");
- expect(content[0].text).not.toContain("Attached");
+ expect(content[0].text).toBe('Base prompt');
+ expect(content[0].text).not.toContain('Attached');
});
- it("should handle empty text content", async () => {
- const result = await utils.buildPromptWithImages("", ["/test.png"]);
+ it('should handle empty text content', async () => {
+ const result = await utils.buildPromptWithImages('', ['/test.png']);
expect(result.hasImages).toBe(true);
// When text is empty/whitespace, should only have image blocks
const content = result.content as Array<{ type: string }>;
- expect(content.every((block) => block.type === "image")).toBe(true);
+ expect(content.every((block) => block.type === 'image')).toBe(true);
});
- it("should trim text content before checking if empty", async () => {
- const result = await utils.buildPromptWithImages(" ", ["/test.png"]);
+ it('should trim text content before checking if empty', async () => {
+ const result = await utils.buildPromptWithImages(' ', ['/test.png']);
const content = result.content as Array<{ type: string }>;
// Whitespace-only text should be excluded
- expect(content.every((block) => block.type === "image")).toBe(true);
+ expect(content.every((block) => block.type === 'image')).toBe(true);
});
it("should return text when only one block and it's text", async () => {
// Make readFile reject to simulate image load failure
- vi.mocked(fs.readFile).mockRejectedValue(new Error("File not found"));
+ vi.mocked(fs.readFile).mockRejectedValue(new Error('File not found'));
- const result = await utils.buildPromptWithImages("Just text", ["/missing.png"]);
+ const result = await utils.buildPromptWithImages('Just text', ['/missing.png']);
// If no images are successfully loaded, should return just the text
- expect(result.content).toBe("Just text");
+ expect(result.content).toBe('Just text');
expect(result.hasImages).toBe(true); // Still true because images were requested
});
- it("should pass workDir for path resolution", async () => {
+ it('should pass workDir for path resolution', async () => {
// The function should use workDir to resolve relative paths
- const result = await utils.buildPromptWithImages(
- "Test",
- ["relative.png"],
- "/work/dir"
- );
+ const result = await utils.buildPromptWithImages('Test', ['relative.png'], '/work/dir');
// Verify it tried to read the file (with resolved path including workDir)
expect(fs.readFile).toHaveBeenCalled();
// The path should be resolved using workDir
const readCall = vi.mocked(fs.readFile).mock.calls[0][0];
- expect(readCall).toContain("relative.png");
+ expect(readCall).toContain('relative.png');
});
});
});
diff --git a/apps/server/tests/unit/lib/sdk-options.test.ts b/apps/server/tests/unit/lib/sdk-options.test.ts
index 0a95312e..c7324d6c 100644
--- a/apps/server/tests/unit/lib/sdk-options.test.ts
+++ b/apps/server/tests/unit/lib/sdk-options.test.ts
@@ -1,6 +1,6 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
-describe("sdk-options.ts", () => {
+describe('sdk-options.ts', () => {
let originalEnv: NodeJS.ProcessEnv;
beforeEach(() => {
@@ -12,34 +12,34 @@ describe("sdk-options.ts", () => {
process.env = originalEnv;
});
- describe("TOOL_PRESETS", () => {
- it("should export readOnly tools", async () => {
- const { TOOL_PRESETS } = await import("@/lib/sdk-options.js");
- expect(TOOL_PRESETS.readOnly).toEqual(["Read", "Glob", "Grep"]);
+ describe('TOOL_PRESETS', () => {
+ it('should export readOnly tools', async () => {
+ const { TOOL_PRESETS } = await import('@/lib/sdk-options.js');
+ expect(TOOL_PRESETS.readOnly).toEqual(['Read', 'Glob', 'Grep']);
});
- it("should export specGeneration tools", async () => {
- const { TOOL_PRESETS } = await import("@/lib/sdk-options.js");
- expect(TOOL_PRESETS.specGeneration).toEqual(["Read", "Glob", "Grep"]);
+ it('should export specGeneration tools', async () => {
+ const { TOOL_PRESETS } = await import('@/lib/sdk-options.js');
+ expect(TOOL_PRESETS.specGeneration).toEqual(['Read', 'Glob', 'Grep']);
});
- it("should export fullAccess tools", async () => {
- const { TOOL_PRESETS } = await import("@/lib/sdk-options.js");
- expect(TOOL_PRESETS.fullAccess).toContain("Read");
- expect(TOOL_PRESETS.fullAccess).toContain("Write");
- expect(TOOL_PRESETS.fullAccess).toContain("Edit");
- expect(TOOL_PRESETS.fullAccess).toContain("Bash");
+ it('should export fullAccess tools', async () => {
+ const { TOOL_PRESETS } = await import('@/lib/sdk-options.js');
+ expect(TOOL_PRESETS.fullAccess).toContain('Read');
+ expect(TOOL_PRESETS.fullAccess).toContain('Write');
+ expect(TOOL_PRESETS.fullAccess).toContain('Edit');
+ expect(TOOL_PRESETS.fullAccess).toContain('Bash');
});
- it("should export chat tools matching fullAccess", async () => {
- const { TOOL_PRESETS } = await import("@/lib/sdk-options.js");
+ it('should export chat tools matching fullAccess', async () => {
+ const { TOOL_PRESETS } = await import('@/lib/sdk-options.js');
expect(TOOL_PRESETS.chat).toEqual(TOOL_PRESETS.fullAccess);
});
});
- describe("MAX_TURNS", () => {
- it("should export turn presets", async () => {
- const { MAX_TURNS } = await import("@/lib/sdk-options.js");
+ describe('MAX_TURNS', () => {
+ it('should export turn presets', async () => {
+ const { MAX_TURNS } = await import('@/lib/sdk-options.js');
expect(MAX_TURNS.quick).toBe(50);
expect(MAX_TURNS.standard).toBe(100);
expect(MAX_TURNS.extended).toBe(250);
@@ -47,71 +47,67 @@ describe("sdk-options.ts", () => {
});
});
- describe("getModelForUseCase", () => {
- it("should return explicit model when provided", async () => {
- const { getModelForUseCase } = await import("@/lib/sdk-options.js");
- const result = getModelForUseCase("spec", "claude-sonnet-4-20250514");
- expect(result).toBe("claude-sonnet-4-20250514");
+ describe('getModelForUseCase', () => {
+ it('should return explicit model when provided', async () => {
+ const { getModelForUseCase } = await import('@/lib/sdk-options.js');
+ const result = getModelForUseCase('spec', 'claude-sonnet-4-20250514');
+ expect(result).toBe('claude-sonnet-4-20250514');
});
- it("should use environment variable for spec model", async () => {
- process.env.AUTOMAKER_MODEL_SPEC = "claude-sonnet-4-20250514";
- const { getModelForUseCase } = await import("@/lib/sdk-options.js");
- const result = getModelForUseCase("spec");
- expect(result).toBe("claude-sonnet-4-20250514");
+ it('should use environment variable for spec model', async () => {
+ process.env.AUTOMAKER_MODEL_SPEC = 'claude-sonnet-4-20250514';
+ const { getModelForUseCase } = await import('@/lib/sdk-options.js');
+ const result = getModelForUseCase('spec');
+ expect(result).toBe('claude-sonnet-4-20250514');
});
- it("should use default model for spec when no override", async () => {
+ it('should use default model for spec when no override', async () => {
delete process.env.AUTOMAKER_MODEL_SPEC;
delete process.env.AUTOMAKER_MODEL_DEFAULT;
- const { getModelForUseCase } = await import("@/lib/sdk-options.js");
- const result = getModelForUseCase("spec");
- expect(result).toContain("claude");
+ const { getModelForUseCase } = await import('@/lib/sdk-options.js');
+ const result = getModelForUseCase('spec');
+ expect(result).toContain('claude');
});
- it("should fall back to AUTOMAKER_MODEL_DEFAULT", async () => {
+ it('should fall back to AUTOMAKER_MODEL_DEFAULT', async () => {
delete process.env.AUTOMAKER_MODEL_SPEC;
- process.env.AUTOMAKER_MODEL_DEFAULT = "claude-sonnet-4-20250514";
- const { getModelForUseCase } = await import("@/lib/sdk-options.js");
- const result = getModelForUseCase("spec");
- expect(result).toBe("claude-sonnet-4-20250514");
+ process.env.AUTOMAKER_MODEL_DEFAULT = 'claude-sonnet-4-20250514';
+ const { getModelForUseCase } = await import('@/lib/sdk-options.js');
+ const result = getModelForUseCase('spec');
+ expect(result).toBe('claude-sonnet-4-20250514');
});
});
- describe("createSpecGenerationOptions", () => {
- it("should create options with spec generation settings", async () => {
+ describe('createSpecGenerationOptions', () => {
+ it('should create options with spec generation settings', async () => {
const { createSpecGenerationOptions, TOOL_PRESETS, MAX_TURNS } =
- await import("@/lib/sdk-options.js");
+ await import('@/lib/sdk-options.js');
- const options = createSpecGenerationOptions({ cwd: "/test/path" });
+ const options = createSpecGenerationOptions({ cwd: '/test/path' });
- expect(options.cwd).toBe("/test/path");
+ expect(options.cwd).toBe('/test/path');
expect(options.maxTurns).toBe(MAX_TURNS.maximum);
expect(options.allowedTools).toEqual([...TOOL_PRESETS.specGeneration]);
- expect(options.permissionMode).toBe("default");
+ expect(options.permissionMode).toBe('default');
});
- it("should include system prompt when provided", async () => {
- const { createSpecGenerationOptions } = await import(
- "@/lib/sdk-options.js"
- );
+ it('should include system prompt when provided', async () => {
+ const { createSpecGenerationOptions } = await import('@/lib/sdk-options.js');
const options = createSpecGenerationOptions({
- cwd: "/test/path",
- systemPrompt: "Custom prompt",
+ cwd: '/test/path',
+ systemPrompt: 'Custom prompt',
});
- expect(options.systemPrompt).toBe("Custom prompt");
+ expect(options.systemPrompt).toBe('Custom prompt');
});
- it("should include abort controller when provided", async () => {
- const { createSpecGenerationOptions } = await import(
- "@/lib/sdk-options.js"
- );
+ it('should include abort controller when provided', async () => {
+ const { createSpecGenerationOptions } = await import('@/lib/sdk-options.js');
const abortController = new AbortController();
const options = createSpecGenerationOptions({
- cwd: "/test/path",
+ cwd: '/test/path',
abortController,
});
@@ -119,76 +115,73 @@ describe("sdk-options.ts", () => {
});
});
- describe("createFeatureGenerationOptions", () => {
- it("should create options with feature generation settings", async () => {
+ describe('createFeatureGenerationOptions', () => {
+ it('should create options with feature generation settings', async () => {
const { createFeatureGenerationOptions, TOOL_PRESETS, MAX_TURNS } =
- await import("@/lib/sdk-options.js");
+ await import('@/lib/sdk-options.js');
- const options = createFeatureGenerationOptions({ cwd: "/test/path" });
+ const options = createFeatureGenerationOptions({ cwd: '/test/path' });
- expect(options.cwd).toBe("/test/path");
+ expect(options.cwd).toBe('/test/path');
expect(options.maxTurns).toBe(MAX_TURNS.quick);
expect(options.allowedTools).toEqual([...TOOL_PRESETS.readOnly]);
});
});
- describe("createSuggestionsOptions", () => {
- it("should create options with suggestions settings", async () => {
- const { createSuggestionsOptions, TOOL_PRESETS, MAX_TURNS } = await import(
- "@/lib/sdk-options.js"
- );
+ describe('createSuggestionsOptions', () => {
+ it('should create options with suggestions settings', async () => {
+ const { createSuggestionsOptions, TOOL_PRESETS, MAX_TURNS } =
+ await import('@/lib/sdk-options.js');
- const options = createSuggestionsOptions({ cwd: "/test/path" });
+ const options = createSuggestionsOptions({ cwd: '/test/path' });
- expect(options.cwd).toBe("/test/path");
+ expect(options.cwd).toBe('/test/path');
expect(options.maxTurns).toBe(MAX_TURNS.extended);
expect(options.allowedTools).toEqual([...TOOL_PRESETS.readOnly]);
});
- it("should include systemPrompt when provided", async () => {
- const { createSuggestionsOptions } = await import("@/lib/sdk-options.js");
+ it('should include systemPrompt when provided', async () => {
+ const { createSuggestionsOptions } = await import('@/lib/sdk-options.js');
const options = createSuggestionsOptions({
- cwd: "/test/path",
- systemPrompt: "Custom prompt",
+ cwd: '/test/path',
+ systemPrompt: 'Custom prompt',
});
- expect(options.systemPrompt).toBe("Custom prompt");
+ expect(options.systemPrompt).toBe('Custom prompt');
});
- it("should include abortController when provided", async () => {
- const { createSuggestionsOptions } = await import("@/lib/sdk-options.js");
+ it('should include abortController when provided', async () => {
+ const { createSuggestionsOptions } = await import('@/lib/sdk-options.js');
const abortController = new AbortController();
const options = createSuggestionsOptions({
- cwd: "/test/path",
+ cwd: '/test/path',
abortController,
});
expect(options.abortController).toBe(abortController);
});
- it("should include outputFormat when provided", async () => {
- const { createSuggestionsOptions } = await import("@/lib/sdk-options.js");
+ it('should include outputFormat when provided', async () => {
+ const { createSuggestionsOptions } = await import('@/lib/sdk-options.js');
const options = createSuggestionsOptions({
- cwd: "/test/path",
- outputFormat: { type: "json" },
+ cwd: '/test/path',
+ outputFormat: { type: 'json' },
});
- expect(options.outputFormat).toEqual({ type: "json" });
+ expect(options.outputFormat).toEqual({ type: 'json' });
});
});
- describe("createChatOptions", () => {
- it("should create options with chat settings", async () => {
- const { createChatOptions, TOOL_PRESETS, MAX_TURNS } = await import(
- "@/lib/sdk-options.js"
- );
+ describe('createChatOptions', () => {
+ it('should create options with chat settings', async () => {
+ const { createChatOptions, TOOL_PRESETS, MAX_TURNS } = await import('@/lib/sdk-options.js');
- const options = createChatOptions({ cwd: "/test/path" });
+ const options = createChatOptions({ cwd: '/test/path' });
- expect(options.cwd).toBe("/test/path");
+ expect(options.cwd).toBe('/test/path');
expect(options.maxTurns).toBe(MAX_TURNS.standard);
expect(options.allowedTools).toEqual([...TOOL_PRESETS.chat]);
expect(options.sandbox).toEqual({
@@ -197,41 +190,38 @@ describe("sdk-options.ts", () => {
});
});
- it("should prefer explicit model over session model", async () => {
- const { createChatOptions, getModelForUseCase } = await import(
- "@/lib/sdk-options.js"
- );
+ it('should prefer explicit model over session model', async () => {
+ const { createChatOptions, getModelForUseCase } = await import('@/lib/sdk-options.js');
const options = createChatOptions({
- cwd: "/test/path",
- model: "claude-opus-4-20250514",
- sessionModel: "claude-haiku-3-5-20241022",
+ cwd: '/test/path',
+ model: 'claude-opus-4-20250514',
+ sessionModel: 'claude-haiku-3-5-20241022',
});
- expect(options.model).toBe("claude-opus-4-20250514");
+ expect(options.model).toBe('claude-opus-4-20250514');
});
- it("should use session model when explicit model not provided", async () => {
- const { createChatOptions } = await import("@/lib/sdk-options.js");
+ it('should use session model when explicit model not provided', async () => {
+ const { createChatOptions } = await import('@/lib/sdk-options.js');
const options = createChatOptions({
- cwd: "/test/path",
- sessionModel: "claude-sonnet-4-20250514",
+ cwd: '/test/path',
+ sessionModel: 'claude-sonnet-4-20250514',
});
- expect(options.model).toBe("claude-sonnet-4-20250514");
+ expect(options.model).toBe('claude-sonnet-4-20250514');
});
});
- describe("createAutoModeOptions", () => {
- it("should create options with auto mode settings", async () => {
- const { createAutoModeOptions, TOOL_PRESETS, MAX_TURNS } = await import(
- "@/lib/sdk-options.js"
- );
+ describe('createAutoModeOptions', () => {
+ it('should create options with auto mode settings', async () => {
+ const { createAutoModeOptions, TOOL_PRESETS, MAX_TURNS } =
+ await import('@/lib/sdk-options.js');
- const options = createAutoModeOptions({ cwd: "/test/path" });
+ const options = createAutoModeOptions({ cwd: '/test/path' });
- expect(options.cwd).toBe("/test/path");
+ expect(options.cwd).toBe('/test/path');
expect(options.maxTurns).toBe(MAX_TURNS.maximum);
expect(options.allowedTools).toEqual([...TOOL_PRESETS.fullAccess]);
expect(options.sandbox).toEqual({
@@ -240,23 +230,23 @@ describe("sdk-options.ts", () => {
});
});
- it("should include systemPrompt when provided", async () => {
- const { createAutoModeOptions } = await import("@/lib/sdk-options.js");
+ it('should include systemPrompt when provided', async () => {
+ const { createAutoModeOptions } = await import('@/lib/sdk-options.js');
const options = createAutoModeOptions({
- cwd: "/test/path",
- systemPrompt: "Custom prompt",
+ cwd: '/test/path',
+ systemPrompt: 'Custom prompt',
});
- expect(options.systemPrompt).toBe("Custom prompt");
+ expect(options.systemPrompt).toBe('Custom prompt');
});
- it("should include abortController when provided", async () => {
- const { createAutoModeOptions } = await import("@/lib/sdk-options.js");
+ it('should include abortController when provided', async () => {
+ const { createAutoModeOptions } = await import('@/lib/sdk-options.js');
const abortController = new AbortController();
const options = createAutoModeOptions({
- cwd: "/test/path",
+ cwd: '/test/path',
abortController,
});
@@ -264,39 +254,37 @@ describe("sdk-options.ts", () => {
});
});
- describe("createCustomOptions", () => {
- it("should create options with custom settings", async () => {
- const { createCustomOptions } = await import("@/lib/sdk-options.js");
+ describe('createCustomOptions', () => {
+ it('should create options with custom settings', async () => {
+ const { createCustomOptions } = await import('@/lib/sdk-options.js');
const options = createCustomOptions({
- cwd: "/test/path",
+ cwd: '/test/path',
maxTurns: 10,
- allowedTools: ["Read", "Write"],
+ allowedTools: ['Read', 'Write'],
sandbox: { enabled: true },
});
- expect(options.cwd).toBe("/test/path");
+ expect(options.cwd).toBe('/test/path');
expect(options.maxTurns).toBe(10);
- expect(options.allowedTools).toEqual(["Read", "Write"]);
+ expect(options.allowedTools).toEqual(['Read', 'Write']);
expect(options.sandbox).toEqual({ enabled: true });
});
- it("should use defaults when optional params not provided", async () => {
- const { createCustomOptions, TOOL_PRESETS, MAX_TURNS } = await import(
- "@/lib/sdk-options.js"
- );
+ it('should use defaults when optional params not provided', async () => {
+ const { createCustomOptions, TOOL_PRESETS, MAX_TURNS } = await import('@/lib/sdk-options.js');
- const options = createCustomOptions({ cwd: "/test/path" });
+ const options = createCustomOptions({ cwd: '/test/path' });
expect(options.maxTurns).toBe(MAX_TURNS.maximum);
expect(options.allowedTools).toEqual([...TOOL_PRESETS.readOnly]);
});
- it("should include sandbox when provided", async () => {
- const { createCustomOptions } = await import("@/lib/sdk-options.js");
+ it('should include sandbox when provided', async () => {
+ const { createCustomOptions } = await import('@/lib/sdk-options.js');
const options = createCustomOptions({
- cwd: "/test/path",
+ cwd: '/test/path',
sandbox: { enabled: true, autoAllowBashIfSandboxed: false },
});
@@ -306,23 +294,23 @@ describe("sdk-options.ts", () => {
});
});
- it("should include systemPrompt when provided", async () => {
- const { createCustomOptions } = await import("@/lib/sdk-options.js");
+ it('should include systemPrompt when provided', async () => {
+ const { createCustomOptions } = await import('@/lib/sdk-options.js');
const options = createCustomOptions({
- cwd: "/test/path",
- systemPrompt: "Custom prompt",
+ cwd: '/test/path',
+ systemPrompt: 'Custom prompt',
});
- expect(options.systemPrompt).toBe("Custom prompt");
+ expect(options.systemPrompt).toBe('Custom prompt');
});
- it("should include abortController when provided", async () => {
- const { createCustomOptions } = await import("@/lib/sdk-options.js");
+ it('should include abortController when provided', async () => {
+ const { createCustomOptions } = await import('@/lib/sdk-options.js');
const abortController = new AbortController();
const options = createCustomOptions({
- cwd: "/test/path",
+ cwd: '/test/path',
abortController,
});
diff --git a/apps/server/tests/unit/lib/security.test.ts b/apps/server/tests/unit/lib/security.test.ts
index 18c378da..bd90d598 100644
--- a/apps/server/tests/unit/lib/security.test.ts
+++ b/apps/server/tests/unit/lib/security.test.ts
@@ -1,60 +1,56 @@
-import { describe, it, expect, beforeEach, vi } from "vitest";
-import path from "path";
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+import path from 'path';
/**
* Note: security.ts maintains module-level state (allowed paths Set).
* We need to reset modules and reimport for each test to get fresh state.
*/
-describe("security.ts", () => {
+describe('security.ts', () => {
beforeEach(() => {
vi.resetModules();
});
- describe("initAllowedPaths", () => {
- it("should load ALLOWED_ROOT_DIRECTORY if set", async () => {
- process.env.ALLOWED_ROOT_DIRECTORY = "/projects";
+ describe('initAllowedPaths', () => {
+ it('should load ALLOWED_ROOT_DIRECTORY if set', async () => {
+ process.env.ALLOWED_ROOT_DIRECTORY = '/projects';
delete process.env.DATA_DIR;
- const { initAllowedPaths, getAllowedPaths } =
- await import("@automaker/platform");
+ const { initAllowedPaths, getAllowedPaths } = await import('@automaker/platform');
initAllowedPaths();
const allowed = getAllowedPaths();
- expect(allowed).toContain(path.resolve("/projects"));
+ expect(allowed).toContain(path.resolve('/projects'));
});
- it("should include DATA_DIR if set", async () => {
+ it('should include DATA_DIR if set', async () => {
delete process.env.ALLOWED_ROOT_DIRECTORY;
- process.env.DATA_DIR = "/data/dir";
+ process.env.DATA_DIR = '/data/dir';
- const { initAllowedPaths, getAllowedPaths } =
- await import("@automaker/platform");
+ const { initAllowedPaths, getAllowedPaths } = await import('@automaker/platform');
initAllowedPaths();
const allowed = getAllowedPaths();
- expect(allowed).toContain(path.resolve("/data/dir"));
+ expect(allowed).toContain(path.resolve('/data/dir'));
});
- it("should include both ALLOWED_ROOT_DIRECTORY and DATA_DIR if both set", async () => {
- process.env.ALLOWED_ROOT_DIRECTORY = "/projects";
- process.env.DATA_DIR = "/data";
+ it('should include both ALLOWED_ROOT_DIRECTORY and DATA_DIR if both set', async () => {
+ process.env.ALLOWED_ROOT_DIRECTORY = '/projects';
+ process.env.DATA_DIR = '/data';
- const { initAllowedPaths, getAllowedPaths } =
- await import("@automaker/platform");
+ const { initAllowedPaths, getAllowedPaths } = await import('@automaker/platform');
initAllowedPaths();
const allowed = getAllowedPaths();
- expect(allowed).toContain(path.resolve("/projects"));
- expect(allowed).toContain(path.resolve("/data"));
+ expect(allowed).toContain(path.resolve('/projects'));
+ expect(allowed).toContain(path.resolve('/data'));
expect(allowed).toHaveLength(2);
});
- it("should return empty array when no paths configured", async () => {
+ it('should return empty array when no paths configured', async () => {
delete process.env.ALLOWED_ROOT_DIRECTORY;
delete process.env.DATA_DIR;
- const { initAllowedPaths, getAllowedPaths } =
- await import("@automaker/platform");
+ const { initAllowedPaths, getAllowedPaths } = await import('@automaker/platform');
initAllowedPaths();
const allowed = getAllowedPaths();
@@ -62,140 +58,129 @@ describe("security.ts", () => {
});
});
- describe("isPathAllowed", () => {
- it("should allow paths within ALLOWED_ROOT_DIRECTORY", async () => {
- process.env.ALLOWED_ROOT_DIRECTORY = "/allowed/project";
- process.env.DATA_DIR = "";
+ describe('isPathAllowed', () => {
+ it('should allow paths within ALLOWED_ROOT_DIRECTORY', async () => {
+ process.env.ALLOWED_ROOT_DIRECTORY = '/allowed/project';
+ process.env.DATA_DIR = '';
- const { initAllowedPaths, isPathAllowed } =
- await import("@automaker/platform");
+ const { initAllowedPaths, isPathAllowed } = await import('@automaker/platform');
initAllowedPaths();
// Paths within allowed directory should be allowed
- expect(isPathAllowed("/allowed/project/file.txt")).toBe(true);
- expect(isPathAllowed("/allowed/project/subdir/file.txt")).toBe(true);
+ expect(isPathAllowed('/allowed/project/file.txt')).toBe(true);
+ expect(isPathAllowed('/allowed/project/subdir/file.txt')).toBe(true);
// Paths outside allowed directory should be denied
- expect(isPathAllowed("/not/allowed/file.txt")).toBe(false);
- expect(isPathAllowed("/tmp/file.txt")).toBe(false);
- expect(isPathAllowed("/etc/passwd")).toBe(false);
+ expect(isPathAllowed('/not/allowed/file.txt')).toBe(false);
+ expect(isPathAllowed('/tmp/file.txt')).toBe(false);
+ expect(isPathAllowed('/etc/passwd')).toBe(false);
});
- it("should allow all paths when no restrictions are configured", async () => {
+ it('should allow all paths when no restrictions are configured', async () => {
delete process.env.DATA_DIR;
delete process.env.ALLOWED_ROOT_DIRECTORY;
- const { initAllowedPaths, isPathAllowed } =
- await import("@automaker/platform");
+ const { initAllowedPaths, isPathAllowed } = await import('@automaker/platform');
initAllowedPaths();
// All paths should be allowed when no restrictions are configured
- expect(isPathAllowed("/allowed/project/file.txt")).toBe(true);
- expect(isPathAllowed("/not/allowed/file.txt")).toBe(true);
- expect(isPathAllowed("/tmp/file.txt")).toBe(true);
- expect(isPathAllowed("/etc/passwd")).toBe(true);
- expect(isPathAllowed("/any/path")).toBe(true);
+ expect(isPathAllowed('/allowed/project/file.txt')).toBe(true);
+ expect(isPathAllowed('/not/allowed/file.txt')).toBe(true);
+ expect(isPathAllowed('/tmp/file.txt')).toBe(true);
+ expect(isPathAllowed('/etc/passwd')).toBe(true);
+ expect(isPathAllowed('/any/path')).toBe(true);
});
- it("should allow all paths when DATA_DIR is set but ALLOWED_ROOT_DIRECTORY is not", async () => {
- process.env.DATA_DIR = "/data";
+ it('should allow all paths when DATA_DIR is set but ALLOWED_ROOT_DIRECTORY is not', async () => {
+ process.env.DATA_DIR = '/data';
delete process.env.ALLOWED_ROOT_DIRECTORY;
- const { initAllowedPaths, isPathAllowed } =
- await import("@automaker/platform");
+ const { initAllowedPaths, isPathAllowed } = await import('@automaker/platform');
initAllowedPaths();
// DATA_DIR should be allowed
- expect(isPathAllowed("/data/settings.json")).toBe(true);
+ expect(isPathAllowed('/data/settings.json')).toBe(true);
// But all other paths should also be allowed when ALLOWED_ROOT_DIRECTORY is not set
- expect(isPathAllowed("/allowed/project/file.txt")).toBe(true);
- expect(isPathAllowed("/not/allowed/file.txt")).toBe(true);
- expect(isPathAllowed("/tmp/file.txt")).toBe(true);
- expect(isPathAllowed("/etc/passwd")).toBe(true);
- expect(isPathAllowed("/any/path")).toBe(true);
+ expect(isPathAllowed('/allowed/project/file.txt')).toBe(true);
+ expect(isPathAllowed('/not/allowed/file.txt')).toBe(true);
+ expect(isPathAllowed('/tmp/file.txt')).toBe(true);
+ expect(isPathAllowed('/etc/passwd')).toBe(true);
+ expect(isPathAllowed('/any/path')).toBe(true);
});
});
- describe("validatePath", () => {
- it("should return resolved path for allowed paths", async () => {
- process.env.ALLOWED_ROOT_DIRECTORY = "/allowed";
- process.env.DATA_DIR = "";
+ describe('validatePath', () => {
+ it('should return resolved path for allowed paths', async () => {
+ process.env.ALLOWED_ROOT_DIRECTORY = '/allowed';
+ process.env.DATA_DIR = '';
- const { initAllowedPaths, validatePath } =
- await import("@automaker/platform");
+ const { initAllowedPaths, validatePath } = await import('@automaker/platform');
initAllowedPaths();
- const result = validatePath("/allowed/file.txt");
- expect(result).toBe(path.resolve("/allowed/file.txt"));
+ const result = validatePath('/allowed/file.txt');
+ expect(result).toBe(path.resolve('/allowed/file.txt'));
});
- it("should throw error for paths outside allowed directories", async () => {
- process.env.ALLOWED_ROOT_DIRECTORY = "/allowed";
- process.env.DATA_DIR = "";
+ it('should throw error for paths outside allowed directories', async () => {
+ process.env.ALLOWED_ROOT_DIRECTORY = '/allowed';
+ process.env.DATA_DIR = '';
- const { initAllowedPaths, validatePath } =
- await import("@automaker/platform");
+ const { initAllowedPaths, validatePath } = await import('@automaker/platform');
initAllowedPaths();
// Disallowed paths should throw PathNotAllowedError
- expect(() => validatePath("/disallowed/file.txt")).toThrow();
+ expect(() => validatePath('/disallowed/file.txt')).toThrow();
});
- it("should not throw error for any path when no restrictions are configured", async () => {
+ it('should not throw error for any path when no restrictions are configured', async () => {
delete process.env.DATA_DIR;
delete process.env.ALLOWED_ROOT_DIRECTORY;
- const { initAllowedPaths, validatePath } =
- await import("@automaker/platform");
+ const { initAllowedPaths, validatePath } = await import('@automaker/platform');
initAllowedPaths();
// All paths are allowed when no restrictions configured
- expect(() => validatePath("/disallowed/file.txt")).not.toThrow();
- expect(validatePath("/disallowed/file.txt")).toBe(
- path.resolve("/disallowed/file.txt")
- );
+ expect(() => validatePath('/disallowed/file.txt')).not.toThrow();
+ expect(validatePath('/disallowed/file.txt')).toBe(path.resolve('/disallowed/file.txt'));
});
- it("should resolve relative paths within allowed directory", async () => {
+ it('should resolve relative paths within allowed directory', async () => {
const cwd = process.cwd();
process.env.ALLOWED_ROOT_DIRECTORY = cwd;
- process.env.DATA_DIR = "";
+ process.env.DATA_DIR = '';
- const { initAllowedPaths, validatePath } =
- await import("@automaker/platform");
+ const { initAllowedPaths, validatePath } = await import('@automaker/platform');
initAllowedPaths();
- const result = validatePath("./file.txt");
- expect(result).toBe(path.resolve(cwd, "./file.txt"));
+ const result = validatePath('./file.txt');
+ expect(result).toBe(path.resolve(cwd, './file.txt'));
});
});
- describe("getAllowedPaths", () => {
- it("should return array of allowed paths", async () => {
- process.env.ALLOWED_ROOT_DIRECTORY = "/projects";
- process.env.DATA_DIR = "/data";
+ describe('getAllowedPaths', () => {
+ it('should return array of allowed paths', async () => {
+ process.env.ALLOWED_ROOT_DIRECTORY = '/projects';
+ process.env.DATA_DIR = '/data';
- const { initAllowedPaths, getAllowedPaths } =
- await import("@automaker/platform");
+ const { initAllowedPaths, getAllowedPaths } = await import('@automaker/platform');
initAllowedPaths();
const result = getAllowedPaths();
expect(Array.isArray(result)).toBe(true);
expect(result.length).toBe(2);
- expect(result).toContain(path.resolve("/projects"));
- expect(result).toContain(path.resolve("/data"));
+ expect(result).toContain(path.resolve('/projects'));
+ expect(result).toContain(path.resolve('/data'));
});
- it("should return resolved paths", async () => {
- process.env.ALLOWED_ROOT_DIRECTORY = "/test";
- process.env.DATA_DIR = "";
+ it('should return resolved paths', async () => {
+ process.env.ALLOWED_ROOT_DIRECTORY = '/test';
+ process.env.DATA_DIR = '';
- const { initAllowedPaths, getAllowedPaths } =
- await import("@automaker/platform");
+ const { initAllowedPaths, getAllowedPaths } = await import('@automaker/platform');
initAllowedPaths();
const result = getAllowedPaths();
- expect(result[0]).toBe(path.resolve("/test"));
+ expect(result[0]).toBe(path.resolve('/test'));
});
});
});
diff --git a/apps/server/tests/unit/lib/worktree-metadata.test.ts b/apps/server/tests/unit/lib/worktree-metadata.test.ts
index 82f3242b..ab7967f3 100644
--- a/apps/server/tests/unit/lib/worktree-metadata.test.ts
+++ b/apps/server/tests/unit/lib/worktree-metadata.test.ts
@@ -1,4 +1,4 @@
-import { describe, it, expect, beforeEach, afterEach } from "vitest";
+import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import {
readWorktreeMetadata,
writeWorktreeMetadata,
@@ -8,12 +8,12 @@ import {
deleteWorktreeMetadata,
type WorktreeMetadata,
type WorktreePRInfo,
-} from "@/lib/worktree-metadata.js";
-import fs from "fs/promises";
-import path from "path";
-import os from "os";
+} from '@/lib/worktree-metadata.js';
+import fs from 'fs/promises';
+import path from 'path';
+import os from 'os';
-describe("worktree-metadata.ts", () => {
+describe('worktree-metadata.ts', () => {
let testProjectPath: string;
beforeEach(async () => {
@@ -29,10 +29,10 @@ describe("worktree-metadata.ts", () => {
}
});
- describe("sanitizeBranchName", () => {
+ describe('sanitizeBranchName', () => {
// Test through readWorktreeMetadata and writeWorktreeMetadata
- it("should sanitize branch names with invalid characters", async () => {
- const branch = "feature/test-branch";
+ it('should sanitize branch names with invalid characters', async () => {
+ const branch = 'feature/test-branch';
const metadata: WorktreeMetadata = {
branch,
createdAt: new Date().toISOString(),
@@ -43,8 +43,8 @@ describe("worktree-metadata.ts", () => {
expect(result).toEqual(metadata);
});
- it("should sanitize branch names with Windows invalid characters", async () => {
- const branch = "feature:test*branch?";
+ it('should sanitize branch names with Windows invalid characters', async () => {
+ const branch = 'feature:test*branch?';
const metadata: WorktreeMetadata = {
branch,
createdAt: new Date().toISOString(),
@@ -55,8 +55,8 @@ describe("worktree-metadata.ts", () => {
expect(result).toEqual(metadata);
});
- it("should sanitize Windows reserved names", async () => {
- const branch = "CON";
+ it('should sanitize Windows reserved names', async () => {
+ const branch = 'CON';
const metadata: WorktreeMetadata = {
branch,
createdAt: new Date().toISOString(),
@@ -67,10 +67,10 @@ describe("worktree-metadata.ts", () => {
expect(result).toEqual(metadata);
});
- it("should handle empty branch name", async () => {
- const branch = "";
+ it('should handle empty branch name', async () => {
+ const branch = '';
const metadata: WorktreeMetadata = {
- branch: "branch",
+ branch: 'branch',
createdAt: new Date().toISOString(),
};
@@ -80,11 +80,11 @@ describe("worktree-metadata.ts", () => {
expect(result).toEqual(metadata);
});
- it("should handle branch name that becomes empty after sanitization", async () => {
+ it('should handle branch name that becomes empty after sanitization', async () => {
// Test branch that would become empty after removing invalid chars
- const branch = "///";
+ const branch = '///';
const metadata: WorktreeMetadata = {
- branch: "branch",
+ branch: 'branch',
createdAt: new Date().toISOString(),
};
@@ -94,14 +94,14 @@ describe("worktree-metadata.ts", () => {
});
});
- describe("readWorktreeMetadata", () => {
+ describe('readWorktreeMetadata', () => {
it("should return null when metadata file doesn't exist", async () => {
- const result = await readWorktreeMetadata(testProjectPath, "nonexistent-branch");
+ const result = await readWorktreeMetadata(testProjectPath, 'nonexistent-branch');
expect(result).toBeNull();
});
- it("should read existing metadata", async () => {
- const branch = "test-branch";
+ it('should read existing metadata', async () => {
+ const branch = 'test-branch';
const metadata: WorktreeMetadata = {
branch,
createdAt: new Date().toISOString(),
@@ -112,16 +112,16 @@ describe("worktree-metadata.ts", () => {
expect(result).toEqual(metadata);
});
- it("should read metadata with PR info", async () => {
- const branch = "pr-branch";
+ it('should read metadata with PR info', async () => {
+ const branch = 'pr-branch';
const metadata: WorktreeMetadata = {
branch,
createdAt: new Date().toISOString(),
pr: {
number: 123,
- url: "https://github.com/owner/repo/pull/123",
- title: "Test PR",
- state: "open",
+ url: 'https://github.com/owner/repo/pull/123',
+ title: 'Test PR',
+ state: 'open',
createdAt: new Date().toISOString(),
},
};
@@ -132,9 +132,9 @@ describe("worktree-metadata.ts", () => {
});
});
- describe("writeWorktreeMetadata", () => {
+ describe('writeWorktreeMetadata', () => {
it("should create metadata directory if it doesn't exist", async () => {
- const branch = "new-branch";
+ const branch = 'new-branch';
const metadata: WorktreeMetadata = {
branch,
createdAt: new Date().toISOString(),
@@ -145,8 +145,8 @@ describe("worktree-metadata.ts", () => {
expect(result).toEqual(metadata);
});
- it("should overwrite existing metadata", async () => {
- const branch = "existing-branch";
+ it('should overwrite existing metadata', async () => {
+ const branch = 'existing-branch';
const metadata1: WorktreeMetadata = {
branch,
createdAt: new Date().toISOString(),
@@ -156,9 +156,9 @@ describe("worktree-metadata.ts", () => {
createdAt: new Date().toISOString(),
pr: {
number: 456,
- url: "https://github.com/owner/repo/pull/456",
- title: "Updated PR",
- state: "closed",
+ url: 'https://github.com/owner/repo/pull/456',
+ title: 'Updated PR',
+ state: 'closed',
createdAt: new Date().toISOString(),
},
};
@@ -170,14 +170,14 @@ describe("worktree-metadata.ts", () => {
});
});
- describe("updateWorktreePRInfo", () => {
+ describe('updateWorktreePRInfo', () => {
it("should create new metadata if it doesn't exist", async () => {
- const branch = "new-pr-branch";
+ const branch = 'new-pr-branch';
const prInfo: WorktreePRInfo = {
number: 789,
- url: "https://github.com/owner/repo/pull/789",
- title: "New PR",
- state: "open",
+ url: 'https://github.com/owner/repo/pull/789',
+ title: 'New PR',
+ state: 'open',
createdAt: new Date().toISOString(),
};
@@ -188,8 +188,8 @@ describe("worktree-metadata.ts", () => {
expect(result?.pr).toEqual(prInfo);
});
- it("should update existing metadata with PR info", async () => {
- const branch = "existing-pr-branch";
+ it('should update existing metadata with PR info', async () => {
+ const branch = 'existing-pr-branch';
const metadata: WorktreeMetadata = {
branch,
createdAt: new Date().toISOString(),
@@ -199,9 +199,9 @@ describe("worktree-metadata.ts", () => {
const prInfo: WorktreePRInfo = {
number: 999,
- url: "https://github.com/owner/repo/pull/999",
- title: "Updated PR",
- state: "merged",
+ url: 'https://github.com/owner/repo/pull/999',
+ title: 'Updated PR',
+ state: 'merged',
createdAt: new Date().toISOString(),
};
@@ -210,8 +210,8 @@ describe("worktree-metadata.ts", () => {
expect(result?.pr).toEqual(prInfo);
});
- it("should preserve existing metadata when updating PR info", async () => {
- const branch = "preserve-branch";
+ it('should preserve existing metadata when updating PR info', async () => {
+ const branch = 'preserve-branch';
const originalCreatedAt = new Date().toISOString();
const metadata: WorktreeMetadata = {
branch,
@@ -222,9 +222,9 @@ describe("worktree-metadata.ts", () => {
const prInfo: WorktreePRInfo = {
number: 111,
- url: "https://github.com/owner/repo/pull/111",
- title: "PR",
- state: "open",
+ url: 'https://github.com/owner/repo/pull/111',
+ title: 'PR',
+ state: 'open',
createdAt: new Date().toISOString(),
};
@@ -235,14 +235,14 @@ describe("worktree-metadata.ts", () => {
});
});
- describe("getWorktreePRInfo", () => {
+ describe('getWorktreePRInfo', () => {
it("should return null when metadata doesn't exist", async () => {
- const result = await getWorktreePRInfo(testProjectPath, "nonexistent");
+ const result = await getWorktreePRInfo(testProjectPath, 'nonexistent');
expect(result).toBeNull();
});
- it("should return null when metadata exists but has no PR info", async () => {
- const branch = "no-pr-branch";
+ it('should return null when metadata exists but has no PR info', async () => {
+ const branch = 'no-pr-branch';
const metadata: WorktreeMetadata = {
branch,
createdAt: new Date().toISOString(),
@@ -253,13 +253,13 @@ describe("worktree-metadata.ts", () => {
expect(result).toBeNull();
});
- it("should return PR info when it exists", async () => {
- const branch = "has-pr-branch";
+ it('should return PR info when it exists', async () => {
+ const branch = 'has-pr-branch';
const prInfo: WorktreePRInfo = {
number: 222,
- url: "https://github.com/owner/repo/pull/222",
- title: "Has PR",
- state: "open",
+ url: 'https://github.com/owner/repo/pull/222',
+ title: 'Has PR',
+ state: 'open',
createdAt: new Date().toISOString(),
};
@@ -269,23 +269,23 @@ describe("worktree-metadata.ts", () => {
});
});
- describe("readAllWorktreeMetadata", () => {
+ describe('readAllWorktreeMetadata', () => {
it("should return empty map when worktrees directory doesn't exist", async () => {
const result = await readAllWorktreeMetadata(testProjectPath);
expect(result.size).toBe(0);
});
- it("should return empty map when worktrees directory is empty", async () => {
- const worktreesDir = path.join(testProjectPath, ".automaker", "worktrees");
+ it('should return empty map when worktrees directory is empty', async () => {
+ const worktreesDir = path.join(testProjectPath, '.automaker', 'worktrees');
await fs.mkdir(worktreesDir, { recursive: true });
const result = await readAllWorktreeMetadata(testProjectPath);
expect(result.size).toBe(0);
});
- it("should read all worktree metadata", async () => {
- const branch1 = "branch-1";
- const branch2 = "branch-2";
+ it('should read all worktree metadata', async () => {
+ const branch1 = 'branch-1';
+ const branch2 = 'branch-2';
const metadata1: WorktreeMetadata = {
branch: branch1,
createdAt: new Date().toISOString(),
@@ -295,9 +295,9 @@ describe("worktree-metadata.ts", () => {
createdAt: new Date().toISOString(),
pr: {
number: 333,
- url: "https://github.com/owner/repo/pull/333",
- title: "PR 3",
- state: "open",
+ url: 'https://github.com/owner/repo/pull/333',
+ title: 'PR 3',
+ state: 'open',
createdAt: new Date().toISOString(),
},
};
@@ -311,12 +311,12 @@ describe("worktree-metadata.ts", () => {
expect(result.get(branch2)).toEqual(metadata2);
});
- it("should skip directories without worktree.json", async () => {
- const worktreesDir = path.join(testProjectPath, ".automaker", "worktrees");
- const emptyDir = path.join(worktreesDir, "empty-dir");
+ it('should skip directories without worktree.json', async () => {
+ const worktreesDir = path.join(testProjectPath, '.automaker', 'worktrees');
+ const emptyDir = path.join(worktreesDir, 'empty-dir');
await fs.mkdir(emptyDir, { recursive: true });
- const branch = "valid-branch";
+ const branch = 'valid-branch';
const metadata: WorktreeMetadata = {
branch,
createdAt: new Date().toISOString(),
@@ -328,13 +328,13 @@ describe("worktree-metadata.ts", () => {
expect(result.get(branch)).toEqual(metadata);
});
- it("should skip files in worktrees directory", async () => {
- const worktreesDir = path.join(testProjectPath, ".automaker", "worktrees");
+ it('should skip files in worktrees directory', async () => {
+ const worktreesDir = path.join(testProjectPath, '.automaker', 'worktrees');
await fs.mkdir(worktreesDir, { recursive: true });
- const filePath = path.join(worktreesDir, "not-a-dir.txt");
- await fs.writeFile(filePath, "content");
+ const filePath = path.join(worktreesDir, 'not-a-dir.txt');
+ await fs.writeFile(filePath, 'content');
- const branch = "valid-branch";
+ const branch = 'valid-branch';
const metadata: WorktreeMetadata = {
branch,
createdAt: new Date().toISOString(),
@@ -346,14 +346,14 @@ describe("worktree-metadata.ts", () => {
expect(result.get(branch)).toEqual(metadata);
});
- it("should skip directories with malformed JSON", async () => {
- const worktreesDir = path.join(testProjectPath, ".automaker", "worktrees");
- const badDir = path.join(worktreesDir, "bad-dir");
+ it('should skip directories with malformed JSON', async () => {
+ const worktreesDir = path.join(testProjectPath, '.automaker', 'worktrees');
+ const badDir = path.join(worktreesDir, 'bad-dir');
await fs.mkdir(badDir, { recursive: true });
- const badJsonPath = path.join(badDir, "worktree.json");
- await fs.writeFile(badJsonPath, "not valid json");
+ const badJsonPath = path.join(badDir, 'worktree.json');
+ await fs.writeFile(badJsonPath, 'not valid json');
- const branch = "valid-branch";
+ const branch = 'valid-branch';
const metadata: WorktreeMetadata = {
branch,
createdAt: new Date().toISOString(),
@@ -366,9 +366,9 @@ describe("worktree-metadata.ts", () => {
});
});
- describe("deleteWorktreeMetadata", () => {
- it("should delete worktree metadata directory", async () => {
- const branch = "to-delete";
+ describe('deleteWorktreeMetadata', () => {
+ it('should delete worktree metadata directory', async () => {
+ const branch = 'to-delete';
const metadata: WorktreeMetadata = {
branch,
createdAt: new Date().toISOString(),
@@ -385,10 +385,7 @@ describe("worktree-metadata.ts", () => {
it("should handle deletion when metadata doesn't exist", async () => {
// Should not throw
- await expect(
- deleteWorktreeMetadata(testProjectPath, "nonexistent")
- ).resolves.toBeUndefined();
+ await expect(deleteWorktreeMetadata(testProjectPath, 'nonexistent')).resolves.toBeUndefined();
});
});
});
-
diff --git a/apps/server/tests/unit/providers/base-provider.test.ts b/apps/server/tests/unit/providers/base-provider.test.ts
index ad0cd41b..f355fec9 100644
--- a/apps/server/tests/unit/providers/base-provider.test.ts
+++ b/apps/server/tests/unit/providers/base-provider.test.ts
@@ -1,23 +1,21 @@
-import { describe, it, expect } from "vitest";
-import { BaseProvider } from "@/providers/base-provider.js";
+import { describe, it, expect } from 'vitest';
+import { BaseProvider } from '@/providers/base-provider.js';
import type {
ProviderConfig,
ExecuteOptions,
ProviderMessage,
InstallationStatus,
ModelDefinition,
-} from "@automaker/types";
+} from '@automaker/types';
// Concrete implementation for testing the abstract class
class TestProvider extends BaseProvider {
getName(): string {
- return "test-provider";
+ return 'test-provider';
}
- async *executeQuery(
- _options: ExecuteOptions
- ): AsyncGenerator {
- yield { type: "text", text: "test response" };
+ async *executeQuery(_options: ExecuteOptions): AsyncGenerator {
+ yield { type: 'text', text: 'test response' };
}
async detectInstallation(): Promise {
@@ -25,37 +23,35 @@ class TestProvider extends BaseProvider {
}
getAvailableModels(): ModelDefinition[] {
- return [
- { id: "test-model-1", name: "Test Model 1", description: "A test model" },
- ];
+ return [{ id: 'test-model-1', name: 'Test Model 1', description: 'A test model' }];
}
}
-describe("base-provider.ts", () => {
- describe("constructor", () => {
- it("should initialize with empty config when none provided", () => {
+describe('base-provider.ts', () => {
+ describe('constructor', () => {
+ it('should initialize with empty config when none provided', () => {
const provider = new TestProvider();
expect(provider.getConfig()).toEqual({});
});
- it("should initialize with provided config", () => {
+ it('should initialize with provided config', () => {
const config: ProviderConfig = {
- apiKey: "test-key",
- baseUrl: "https://test.com",
+ apiKey: 'test-key',
+ baseUrl: 'https://test.com',
};
const provider = new TestProvider(config);
expect(provider.getConfig()).toEqual(config);
});
- it("should call getName() during initialization", () => {
+ it('should call getName() during initialization', () => {
const provider = new TestProvider();
- expect(provider.getName()).toBe("test-provider");
+ expect(provider.getName()).toBe('test-provider');
});
});
- describe("validateConfig", () => {
- it("should return valid when config exists", () => {
- const provider = new TestProvider({ apiKey: "test" });
+ describe('validateConfig', () => {
+ it('should return valid when config exists', () => {
+ const provider = new TestProvider({ apiKey: 'test' });
const result = provider.validateConfig();
expect(result.valid).toBe(true);
@@ -63,7 +59,7 @@ describe("base-provider.ts", () => {
expect(result.warnings).toHaveLength(0);
});
- it("should return invalid when config is undefined", () => {
+ it('should return invalid when config is undefined', () => {
// Create provider without config
const provider = new TestProvider();
// Manually set config to undefined to test edge case
@@ -72,10 +68,10 @@ describe("base-provider.ts", () => {
const result = provider.validateConfig();
expect(result.valid).toBe(false);
- expect(result.errors).toContain("Provider config is missing");
+ expect(result.errors).toContain('Provider config is missing');
});
- it("should return valid for empty config object", () => {
+ it('should return valid for empty config object', () => {
const provider = new TestProvider({});
const result = provider.validateConfig();
@@ -83,53 +79,53 @@ describe("base-provider.ts", () => {
expect(result.errors).toHaveLength(0);
});
- it("should include warnings array in result", () => {
+ it('should include warnings array in result', () => {
const provider = new TestProvider();
const result = provider.validateConfig();
- expect(result).toHaveProperty("warnings");
+ expect(result).toHaveProperty('warnings');
expect(Array.isArray(result.warnings)).toBe(true);
});
});
- describe("supportsFeature", () => {
+ describe('supportsFeature', () => {
it("should support 'tools' feature", () => {
const provider = new TestProvider();
- expect(provider.supportsFeature("tools")).toBe(true);
+ expect(provider.supportsFeature('tools')).toBe(true);
});
it("should support 'text' feature", () => {
const provider = new TestProvider();
- expect(provider.supportsFeature("text")).toBe(true);
+ expect(provider.supportsFeature('text')).toBe(true);
});
- it("should not support unknown features", () => {
+ it('should not support unknown features', () => {
const provider = new TestProvider();
- expect(provider.supportsFeature("vision")).toBe(false);
- expect(provider.supportsFeature("mcp")).toBe(false);
- expect(provider.supportsFeature("unknown")).toBe(false);
+ expect(provider.supportsFeature('vision')).toBe(false);
+ expect(provider.supportsFeature('mcp')).toBe(false);
+ expect(provider.supportsFeature('unknown')).toBe(false);
});
- it("should be case-sensitive", () => {
+ it('should be case-sensitive', () => {
const provider = new TestProvider();
- expect(provider.supportsFeature("TOOLS")).toBe(false);
- expect(provider.supportsFeature("Text")).toBe(false);
+ expect(provider.supportsFeature('TOOLS')).toBe(false);
+ expect(provider.supportsFeature('Text')).toBe(false);
});
});
- describe("getConfig", () => {
- it("should return current config", () => {
+ describe('getConfig', () => {
+ it('should return current config', () => {
const config: ProviderConfig = {
- apiKey: "test-key",
- model: "test-model",
+ apiKey: 'test-key',
+ model: 'test-model',
};
const provider = new TestProvider(config);
expect(provider.getConfig()).toEqual(config);
});
- it("should return same reference", () => {
- const config: ProviderConfig = { apiKey: "test" };
+ it('should return same reference', () => {
+ const config: ProviderConfig = { apiKey: 'test' };
const provider = new TestProvider(config);
const retrieved1 = provider.getConfig();
@@ -139,31 +135,31 @@ describe("base-provider.ts", () => {
});
});
- describe("setConfig", () => {
- it("should merge partial config with existing config", () => {
- const provider = new TestProvider({ apiKey: "original-key" });
+ describe('setConfig', () => {
+ it('should merge partial config with existing config', () => {
+ const provider = new TestProvider({ apiKey: 'original-key' });
- provider.setConfig({ model: "new-model" });
+ provider.setConfig({ model: 'new-model' });
expect(provider.getConfig()).toEqual({
- apiKey: "original-key",
- model: "new-model",
+ apiKey: 'original-key',
+ model: 'new-model',
});
});
- it("should override existing fields", () => {
- const provider = new TestProvider({ apiKey: "old-key", model: "old-model" });
+ it('should override existing fields', () => {
+ const provider = new TestProvider({ apiKey: 'old-key', model: 'old-model' });
- provider.setConfig({ apiKey: "new-key" });
+ provider.setConfig({ apiKey: 'new-key' });
expect(provider.getConfig()).toEqual({
- apiKey: "new-key",
- model: "old-model",
+ apiKey: 'new-key',
+ model: 'old-model',
});
});
- it("should accept empty object", () => {
- const provider = new TestProvider({ apiKey: "test" });
+ it('should accept empty object', () => {
+ const provider = new TestProvider({ apiKey: 'test' });
const originalConfig = provider.getConfig();
provider.setConfig({});
@@ -171,68 +167,68 @@ describe("base-provider.ts", () => {
expect(provider.getConfig()).toEqual(originalConfig);
});
- it("should handle multiple updates", () => {
+ it('should handle multiple updates', () => {
const provider = new TestProvider();
- provider.setConfig({ apiKey: "key1" });
- provider.setConfig({ model: "model1" });
- provider.setConfig({ baseUrl: "https://test.com" });
+ provider.setConfig({ apiKey: 'key1' });
+ provider.setConfig({ model: 'model1' });
+ provider.setConfig({ baseUrl: 'https://test.com' });
expect(provider.getConfig()).toEqual({
- apiKey: "key1",
- model: "model1",
- baseUrl: "https://test.com",
+ apiKey: 'key1',
+ model: 'model1',
+ baseUrl: 'https://test.com',
});
});
- it("should preserve other fields when updating one field", () => {
+ it('should preserve other fields when updating one field', () => {
const provider = new TestProvider({
- apiKey: "key",
- model: "model",
- baseUrl: "https://test.com",
+ apiKey: 'key',
+ model: 'model',
+ baseUrl: 'https://test.com',
});
- provider.setConfig({ model: "new-model" });
+ provider.setConfig({ model: 'new-model' });
expect(provider.getConfig()).toEqual({
- apiKey: "key",
- model: "new-model",
- baseUrl: "https://test.com",
+ apiKey: 'key',
+ model: 'new-model',
+ baseUrl: 'https://test.com',
});
});
});
- describe("abstract methods", () => {
- it("should require getName implementation", () => {
+ describe('abstract methods', () => {
+ it('should require getName implementation', () => {
const provider = new TestProvider();
- expect(typeof provider.getName).toBe("function");
- expect(provider.getName()).toBe("test-provider");
+ expect(typeof provider.getName).toBe('function');
+ expect(provider.getName()).toBe('test-provider');
});
- it("should require executeQuery implementation", async () => {
+ it('should require executeQuery implementation', async () => {
const provider = new TestProvider();
- expect(typeof provider.executeQuery).toBe("function");
+ expect(typeof provider.executeQuery).toBe('function');
const generator = provider.executeQuery({
- prompt: "test",
- projectDirectory: "/test",
+ prompt: 'test',
+ projectDirectory: '/test',
});
const result = await generator.next();
- expect(result.value).toEqual({ type: "text", text: "test response" });
+ expect(result.value).toEqual({ type: 'text', text: 'test response' });
});
- it("should require detectInstallation implementation", async () => {
+ it('should require detectInstallation implementation', async () => {
const provider = new TestProvider();
- expect(typeof provider.detectInstallation).toBe("function");
+ expect(typeof provider.detectInstallation).toBe('function');
const status = await provider.detectInstallation();
- expect(status).toHaveProperty("installed");
+ expect(status).toHaveProperty('installed');
});
- it("should require getAvailableModels implementation", () => {
+ it('should require getAvailableModels implementation', () => {
const provider = new TestProvider();
- expect(typeof provider.getAvailableModels).toBe("function");
+ expect(typeof provider.getAvailableModels).toBe('function');
const models = provider.getAvailableModels();
expect(Array.isArray(models)).toBe(true);
diff --git a/apps/server/tests/unit/providers/claude-provider.test.ts b/apps/server/tests/unit/providers/claude-provider.test.ts
index 41c5bf71..888cf091 100644
--- a/apps/server/tests/unit/providers/claude-provider.test.ts
+++ b/apps/server/tests/unit/providers/claude-provider.test.ts
@@ -1,11 +1,11 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
-import { ClaudeProvider } from "@/providers/claude-provider.js";
-import * as sdk from "@anthropic-ai/claude-agent-sdk";
-import { collectAsyncGenerator } from "../../utils/helpers.js";
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { ClaudeProvider } from '@/providers/claude-provider.js';
+import * as sdk from '@anthropic-ai/claude-agent-sdk';
+import { collectAsyncGenerator } from '../../utils/helpers.js';
-vi.mock("@anthropic-ai/claude-agent-sdk");
+vi.mock('@anthropic-ai/claude-agent-sdk');
-describe("claude-provider.ts", () => {
+describe('claude-provider.ts', () => {
let provider: ClaudeProvider;
beforeEach(() => {
@@ -14,17 +14,17 @@ describe("claude-provider.ts", () => {
delete process.env.ANTHROPIC_API_KEY;
});
- describe("getName", () => {
+ describe('getName', () => {
it("should return 'claude' as provider name", () => {
- expect(provider.getName()).toBe("claude");
+ expect(provider.getName()).toBe('claude');
});
});
- describe("executeQuery", () => {
- it("should execute simple text query", async () => {
+ describe('executeQuery', () => {
+ it('should execute simple text query', async () => {
const mockMessages = [
- { type: "text", text: "Response 1" },
- { type: "text", text: "Response 2" },
+ { type: 'text', text: 'Response 1' },
+ { type: 'text', text: 'Response 2' },
];
vi.mocked(sdk.query).mockReturnValue(
@@ -36,95 +36,86 @@ describe("claude-provider.ts", () => {
);
const generator = provider.executeQuery({
- prompt: "Hello",
- cwd: "/test",
+ prompt: 'Hello',
+ cwd: '/test',
});
const results = await collectAsyncGenerator(generator);
expect(results).toHaveLength(2);
- expect(results[0]).toEqual({ type: "text", text: "Response 1" });
- expect(results[1]).toEqual({ type: "text", text: "Response 2" });
+ expect(results[0]).toEqual({ type: 'text', text: 'Response 1' });
+ expect(results[1]).toEqual({ type: 'text', text: 'Response 2' });
});
- it("should pass correct options to SDK", async () => {
+ it('should pass correct options to SDK', async () => {
vi.mocked(sdk.query).mockReturnValue(
(async function* () {
- yield { type: "text", text: "test" };
+ yield { type: 'text', text: 'test' };
})()
);
const generator = provider.executeQuery({
- prompt: "Test prompt",
- model: "claude-opus-4-5-20251101",
- cwd: "/test/dir",
- systemPrompt: "You are helpful",
+ prompt: 'Test prompt',
+ model: 'claude-opus-4-5-20251101',
+ cwd: '/test/dir',
+ systemPrompt: 'You are helpful',
maxTurns: 10,
- allowedTools: ["Read", "Write"],
+ allowedTools: ['Read', 'Write'],
});
await collectAsyncGenerator(generator);
expect(sdk.query).toHaveBeenCalledWith({
- prompt: "Test prompt",
+ prompt: 'Test prompt',
options: expect.objectContaining({
- model: "claude-opus-4-5-20251101",
- systemPrompt: "You are helpful",
+ model: 'claude-opus-4-5-20251101',
+ systemPrompt: 'You are helpful',
maxTurns: 10,
- cwd: "/test/dir",
- allowedTools: ["Read", "Write"],
- permissionMode: "acceptEdits",
+ cwd: '/test/dir',
+ allowedTools: ['Read', 'Write'],
+ permissionMode: 'acceptEdits',
}),
});
});
- it("should use default allowed tools when not specified", async () => {
+ it('should use default allowed tools when not specified', async () => {
vi.mocked(sdk.query).mockReturnValue(
(async function* () {
- yield { type: "text", text: "test" };
+ yield { type: 'text', text: 'test' };
})()
);
const generator = provider.executeQuery({
- prompt: "Test",
- cwd: "/test",
+ prompt: 'Test',
+ cwd: '/test',
});
await collectAsyncGenerator(generator);
expect(sdk.query).toHaveBeenCalledWith({
- prompt: "Test",
+ prompt: 'Test',
options: expect.objectContaining({
- allowedTools: [
- "Read",
- "Write",
- "Edit",
- "Glob",
- "Grep",
- "Bash",
- "WebSearch",
- "WebFetch",
- ],
+ allowedTools: ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch'],
}),
});
});
- it("should enable sandbox by default", async () => {
+ it('should enable sandbox by default', async () => {
vi.mocked(sdk.query).mockReturnValue(
(async function* () {
- yield { type: "text", text: "test" };
+ yield { type: 'text', text: 'test' };
})()
);
const generator = provider.executeQuery({
- prompt: "Test",
- cwd: "/test",
+ prompt: 'Test',
+ cwd: '/test',
});
await collectAsyncGenerator(generator);
expect(sdk.query).toHaveBeenCalledWith({
- prompt: "Test",
+ prompt: 'Test',
options: expect.objectContaining({
sandbox: {
enabled: true,
@@ -134,110 +125,110 @@ describe("claude-provider.ts", () => {
});
});
- it("should pass abortController if provided", async () => {
+ it('should pass abortController if provided', async () => {
vi.mocked(sdk.query).mockReturnValue(
(async function* () {
- yield { type: "text", text: "test" };
+ yield { type: 'text', text: 'test' };
})()
);
const abortController = new AbortController();
const generator = provider.executeQuery({
- prompt: "Test",
- cwd: "/test",
+ prompt: 'Test',
+ cwd: '/test',
abortController,
});
await collectAsyncGenerator(generator);
expect(sdk.query).toHaveBeenCalledWith({
- prompt: "Test",
+ prompt: 'Test',
options: expect.objectContaining({
abortController,
}),
});
});
- it("should handle conversation history with sdkSessionId using resume option", async () => {
+ it('should handle conversation history with sdkSessionId using resume option', async () => {
vi.mocked(sdk.query).mockReturnValue(
(async function* () {
- yield { type: "text", text: "test" };
+ yield { type: 'text', text: 'test' };
})()
);
const conversationHistory = [
- { role: "user" as const, content: "Previous message" },
- { role: "assistant" as const, content: "Previous response" },
+ { role: 'user' as const, content: 'Previous message' },
+ { role: 'assistant' as const, content: 'Previous response' },
];
const generator = provider.executeQuery({
- prompt: "Current message",
- cwd: "/test",
+ prompt: 'Current message',
+ cwd: '/test',
conversationHistory,
- sdkSessionId: "test-session-id",
+ sdkSessionId: 'test-session-id',
});
await collectAsyncGenerator(generator);
// Should use resume option when sdkSessionId is provided with history
expect(sdk.query).toHaveBeenCalledWith({
- prompt: "Current message",
+ prompt: 'Current message',
options: expect.objectContaining({
- resume: "test-session-id",
+ resume: 'test-session-id',
}),
});
});
- it("should handle array prompt (with images)", async () => {
+ it('should handle array prompt (with images)', async () => {
vi.mocked(sdk.query).mockReturnValue(
(async function* () {
- yield { type: "text", text: "test" };
+ yield { type: 'text', text: 'test' };
})()
);
const arrayPrompt = [
- { type: "text", text: "Describe this" },
- { type: "image", source: { type: "base64", data: "..." } },
+ { type: 'text', text: 'Describe this' },
+ { type: 'image', source: { type: 'base64', data: '...' } },
];
const generator = provider.executeQuery({
prompt: arrayPrompt as any,
- cwd: "/test",
+ cwd: '/test',
});
await collectAsyncGenerator(generator);
// Should pass an async generator as prompt for array inputs
const callArgs = vi.mocked(sdk.query).mock.calls[0][0];
- expect(typeof callArgs.prompt).not.toBe("string");
+ expect(typeof callArgs.prompt).not.toBe('string');
});
- it("should use maxTurns default of 20", async () => {
+ it('should use maxTurns default of 20', async () => {
vi.mocked(sdk.query).mockReturnValue(
(async function* () {
- yield { type: "text", text: "test" };
+ yield { type: 'text', text: 'test' };
})()
);
const generator = provider.executeQuery({
- prompt: "Test",
- cwd: "/test",
+ prompt: 'Test',
+ cwd: '/test',
});
await collectAsyncGenerator(generator);
expect(sdk.query).toHaveBeenCalledWith({
- prompt: "Test",
+ prompt: 'Test',
options: expect.objectContaining({
maxTurns: 20,
}),
});
});
- it("should handle errors during execution and rethrow", async () => {
- const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
- const testError = new Error("SDK execution failed");
+ it('should handle errors during execution and rethrow', async () => {
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
+ const testError = new Error('SDK execution failed');
vi.mocked(sdk.query).mockReturnValue(
(async function* () {
@@ -246,13 +237,13 @@ describe("claude-provider.ts", () => {
);
const generator = provider.executeQuery({
- prompt: "Test",
- cwd: "/test",
+ prompt: 'Test',
+ cwd: '/test',
});
- await expect(collectAsyncGenerator(generator)).rejects.toThrow("SDK execution failed");
+ await expect(collectAsyncGenerator(generator)).rejects.toThrow('SDK execution failed');
expect(consoleErrorSpy).toHaveBeenCalledWith(
- "[ClaudeProvider] executeQuery() error during execution:",
+ '[ClaudeProvider] executeQuery() error during execution:',
testError
);
@@ -260,16 +251,16 @@ describe("claude-provider.ts", () => {
});
});
- describe("detectInstallation", () => {
- it("should return installed with SDK method", async () => {
+ describe('detectInstallation', () => {
+ it('should return installed with SDK method', async () => {
const result = await provider.detectInstallation();
expect(result.installed).toBe(true);
- expect(result.method).toBe("sdk");
+ expect(result.method).toBe('sdk');
});
- it("should detect ANTHROPIC_API_KEY", async () => {
- process.env.ANTHROPIC_API_KEY = "test-key";
+ it('should detect ANTHROPIC_API_KEY', async () => {
+ process.env.ANTHROPIC_API_KEY = 'test-key';
const result = await provider.detectInstallation();
@@ -277,7 +268,7 @@ describe("claude-provider.ts", () => {
expect(result.authenticated).toBe(true);
});
- it("should return hasApiKey false when no keys present", async () => {
+ it('should return hasApiKey false when no keys present', async () => {
const result = await provider.detectInstallation();
expect(result.hasApiKey).toBe(false);
@@ -285,54 +276,52 @@ describe("claude-provider.ts", () => {
});
});
- describe("getAvailableModels", () => {
- it("should return 4 Claude models", () => {
+ describe('getAvailableModels', () => {
+ it('should return 4 Claude models', () => {
const models = provider.getAvailableModels();
expect(models).toHaveLength(4);
});
- it("should include Claude Opus 4.5", () => {
+ it('should include Claude Opus 4.5', () => {
const models = provider.getAvailableModels();
- const opus = models.find((m) => m.id === "claude-opus-4-5-20251101");
+ const opus = models.find((m) => m.id === 'claude-opus-4-5-20251101');
expect(opus).toBeDefined();
- expect(opus?.name).toBe("Claude Opus 4.5");
- expect(opus?.provider).toBe("anthropic");
+ expect(opus?.name).toBe('Claude Opus 4.5');
+ expect(opus?.provider).toBe('anthropic');
});
- it("should include Claude Sonnet 4", () => {
+ it('should include Claude Sonnet 4', () => {
const models = provider.getAvailableModels();
- const sonnet = models.find((m) => m.id === "claude-sonnet-4-20250514");
+ const sonnet = models.find((m) => m.id === 'claude-sonnet-4-20250514');
expect(sonnet).toBeDefined();
- expect(sonnet?.name).toBe("Claude Sonnet 4");
+ expect(sonnet?.name).toBe('Claude Sonnet 4');
});
- it("should include Claude 3.5 Sonnet", () => {
+ it('should include Claude 3.5 Sonnet', () => {
const models = provider.getAvailableModels();
- const sonnet35 = models.find(
- (m) => m.id === "claude-3-5-sonnet-20241022"
- );
+ const sonnet35 = models.find((m) => m.id === 'claude-3-5-sonnet-20241022');
expect(sonnet35).toBeDefined();
});
- it("should include Claude 3.5 Haiku", () => {
+ it('should include Claude 3.5 Haiku', () => {
const models = provider.getAvailableModels();
- const haiku = models.find((m) => m.id === "claude-3-5-haiku-20241022");
+ const haiku = models.find((m) => m.id === 'claude-3-5-haiku-20241022');
expect(haiku).toBeDefined();
});
- it("should mark Opus as default", () => {
+ it('should mark Opus as default', () => {
const models = provider.getAvailableModels();
- const opus = models.find((m) => m.id === "claude-opus-4-5-20251101");
+ const opus = models.find((m) => m.id === 'claude-opus-4-5-20251101');
expect(opus?.default).toBe(true);
});
- it("should all support vision and tools", () => {
+ it('should all support vision and tools', () => {
const models = provider.getAvailableModels();
models.forEach((model) => {
@@ -341,7 +330,7 @@ describe("claude-provider.ts", () => {
});
});
- it("should have correct context windows", () => {
+ it('should have correct context windows', () => {
const models = provider.getAvailableModels();
models.forEach((model) => {
@@ -349,7 +338,7 @@ describe("claude-provider.ts", () => {
});
});
- it("should have modelString field matching id", () => {
+ it('should have modelString field matching id', () => {
const models = provider.getAvailableModels();
models.forEach((model) => {
@@ -358,38 +347,38 @@ describe("claude-provider.ts", () => {
});
});
- describe("supportsFeature", () => {
+ describe('supportsFeature', () => {
it("should support 'tools' feature", () => {
- expect(provider.supportsFeature("tools")).toBe(true);
+ expect(provider.supportsFeature('tools')).toBe(true);
});
it("should support 'text' feature", () => {
- expect(provider.supportsFeature("text")).toBe(true);
+ expect(provider.supportsFeature('text')).toBe(true);
});
it("should support 'vision' feature", () => {
- expect(provider.supportsFeature("vision")).toBe(true);
+ expect(provider.supportsFeature('vision')).toBe(true);
});
it("should support 'thinking' feature", () => {
- expect(provider.supportsFeature("thinking")).toBe(true);
+ expect(provider.supportsFeature('thinking')).toBe(true);
});
it("should not support 'mcp' feature", () => {
- expect(provider.supportsFeature("mcp")).toBe(false);
+ expect(provider.supportsFeature('mcp')).toBe(false);
});
it("should not support 'cli' feature", () => {
- expect(provider.supportsFeature("cli")).toBe(false);
+ expect(provider.supportsFeature('cli')).toBe(false);
});
- it("should not support unknown features", () => {
- expect(provider.supportsFeature("unknown")).toBe(false);
+ it('should not support unknown features', () => {
+ expect(provider.supportsFeature('unknown')).toBe(false);
});
});
- describe("validateConfig", () => {
- it("should validate config from base class", () => {
+ describe('validateConfig', () => {
+ it('should validate config from base class', () => {
const result = provider.validateConfig();
expect(result.valid).toBe(true);
@@ -397,21 +386,21 @@ describe("claude-provider.ts", () => {
});
});
- describe("config management", () => {
- it("should get and set config", () => {
- provider.setConfig({ apiKey: "test-key" });
+ describe('config management', () => {
+ it('should get and set config', () => {
+ provider.setConfig({ apiKey: 'test-key' });
const config = provider.getConfig();
- expect(config.apiKey).toBe("test-key");
+ expect(config.apiKey).toBe('test-key');
});
- it("should merge config updates", () => {
- provider.setConfig({ apiKey: "key1" });
- provider.setConfig({ model: "model1" });
+ it('should merge config updates', () => {
+ provider.setConfig({ apiKey: 'key1' });
+ provider.setConfig({ model: 'model1' });
const config = provider.getConfig();
- expect(config.apiKey).toBe("key1");
- expect(config.model).toBe("model1");
+ expect(config.apiKey).toBe('key1');
+ expect(config.model).toBe('model1');
});
});
});
diff --git a/apps/server/tests/unit/providers/provider-factory.test.ts b/apps/server/tests/unit/providers/provider-factory.test.ts
index cd34af15..069fbf86 100644
--- a/apps/server/tests/unit/providers/provider-factory.test.ts
+++ b/apps/server/tests/unit/providers/provider-factory.test.ts
@@ -1,13 +1,13 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
-import { ProviderFactory } from "@/providers/provider-factory.js";
-import { ClaudeProvider } from "@/providers/claude-provider.js";
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { ProviderFactory } from '@/providers/provider-factory.js';
+import { ClaudeProvider } from '@/providers/claude-provider.js';
-describe("provider-factory.ts", () => {
+describe('provider-factory.ts', () => {
let consoleSpy: any;
beforeEach(() => {
consoleSpy = {
- warn: vi.spyOn(console, "warn").mockImplementation(() => {}),
+ warn: vi.spyOn(console, 'warn').mockImplementation(() => {}),
};
});
@@ -15,55 +15,49 @@ describe("provider-factory.ts", () => {
consoleSpy.warn.mockRestore();
});
- describe("getProviderForModel", () => {
- describe("Claude models (claude-* prefix)", () => {
- it("should return ClaudeProvider for claude-opus-4-5-20251101", () => {
- const provider = ProviderFactory.getProviderForModel(
- "claude-opus-4-5-20251101"
- );
+ describe('getProviderForModel', () => {
+ describe('Claude models (claude-* prefix)', () => {
+ it('should return ClaudeProvider for claude-opus-4-5-20251101', () => {
+ const provider = ProviderFactory.getProviderForModel('claude-opus-4-5-20251101');
expect(provider).toBeInstanceOf(ClaudeProvider);
});
- it("should return ClaudeProvider for claude-sonnet-4-20250514", () => {
- const provider = ProviderFactory.getProviderForModel(
- "claude-sonnet-4-20250514"
- );
+ it('should return ClaudeProvider for claude-sonnet-4-20250514', () => {
+ const provider = ProviderFactory.getProviderForModel('claude-sonnet-4-20250514');
expect(provider).toBeInstanceOf(ClaudeProvider);
});
- it("should return ClaudeProvider for claude-haiku-4-5", () => {
- const provider = ProviderFactory.getProviderForModel("claude-haiku-4-5");
+ it('should return ClaudeProvider for claude-haiku-4-5', () => {
+ const provider = ProviderFactory.getProviderForModel('claude-haiku-4-5');
expect(provider).toBeInstanceOf(ClaudeProvider);
});
- it("should be case-insensitive for claude models", () => {
- const provider = ProviderFactory.getProviderForModel(
- "CLAUDE-OPUS-4-5-20251101"
- );
+ it('should be case-insensitive for claude models', () => {
+ const provider = ProviderFactory.getProviderForModel('CLAUDE-OPUS-4-5-20251101');
expect(provider).toBeInstanceOf(ClaudeProvider);
});
});
- describe("Claude aliases", () => {
+ describe('Claude aliases', () => {
it("should return ClaudeProvider for 'haiku'", () => {
- const provider = ProviderFactory.getProviderForModel("haiku");
+ const provider = ProviderFactory.getProviderForModel('haiku');
expect(provider).toBeInstanceOf(ClaudeProvider);
});
it("should return ClaudeProvider for 'sonnet'", () => {
- const provider = ProviderFactory.getProviderForModel("sonnet");
+ const provider = ProviderFactory.getProviderForModel('sonnet');
expect(provider).toBeInstanceOf(ClaudeProvider);
});
it("should return ClaudeProvider for 'opus'", () => {
- const provider = ProviderFactory.getProviderForModel("opus");
+ const provider = ProviderFactory.getProviderForModel('opus');
expect(provider).toBeInstanceOf(ClaudeProvider);
});
- it("should be case-insensitive for aliases", () => {
- const provider1 = ProviderFactory.getProviderForModel("HAIKU");
- const provider2 = ProviderFactory.getProviderForModel("Sonnet");
- const provider3 = ProviderFactory.getProviderForModel("Opus");
+ it('should be case-insensitive for aliases', () => {
+ const provider1 = ProviderFactory.getProviderForModel('HAIKU');
+ const provider2 = ProviderFactory.getProviderForModel('Sonnet');
+ const provider3 = ProviderFactory.getProviderForModel('Opus');
expect(provider1).toBeInstanceOf(ClaudeProvider);
expect(provider2).toBeInstanceOf(ClaudeProvider);
@@ -71,65 +65,61 @@ describe("provider-factory.ts", () => {
});
});
- describe("Unknown models", () => {
- it("should default to ClaudeProvider for unknown model", () => {
- const provider = ProviderFactory.getProviderForModel("unknown-model-123");
+ describe('Unknown models', () => {
+ it('should default to ClaudeProvider for unknown model', () => {
+ const provider = ProviderFactory.getProviderForModel('unknown-model-123');
expect(provider).toBeInstanceOf(ClaudeProvider);
});
- it("should warn when defaulting to Claude", () => {
- ProviderFactory.getProviderForModel("random-model");
+ it('should warn when defaulting to Claude', () => {
+ ProviderFactory.getProviderForModel('random-model');
expect(consoleSpy.warn).toHaveBeenCalledWith(
- expect.stringContaining("Unknown model prefix")
+ expect.stringContaining('Unknown model prefix')
);
+ expect(consoleSpy.warn).toHaveBeenCalledWith(expect.stringContaining('random-model'));
expect(consoleSpy.warn).toHaveBeenCalledWith(
- expect.stringContaining("random-model")
- );
- expect(consoleSpy.warn).toHaveBeenCalledWith(
- expect.stringContaining("defaulting to Claude")
+ expect.stringContaining('defaulting to Claude')
);
});
- it("should handle empty string", () => {
- const provider = ProviderFactory.getProviderForModel("");
+ it('should handle empty string', () => {
+ const provider = ProviderFactory.getProviderForModel('');
expect(provider).toBeInstanceOf(ClaudeProvider);
expect(consoleSpy.warn).toHaveBeenCalled();
});
- it("should default to ClaudeProvider for gpt models (not supported)", () => {
- const provider = ProviderFactory.getProviderForModel("gpt-5.2");
+ it('should default to ClaudeProvider for gpt models (not supported)', () => {
+ const provider = ProviderFactory.getProviderForModel('gpt-5.2');
expect(provider).toBeInstanceOf(ClaudeProvider);
expect(consoleSpy.warn).toHaveBeenCalled();
});
- it("should default to ClaudeProvider for o-series models (not supported)", () => {
- const provider = ProviderFactory.getProviderForModel("o1");
+ it('should default to ClaudeProvider for o-series models (not supported)', () => {
+ const provider = ProviderFactory.getProviderForModel('o1');
expect(provider).toBeInstanceOf(ClaudeProvider);
expect(consoleSpy.warn).toHaveBeenCalled();
});
});
});
- describe("getAllProviders", () => {
- it("should return array of all providers", () => {
+ describe('getAllProviders', () => {
+ it('should return array of all providers', () => {
const providers = ProviderFactory.getAllProviders();
expect(Array.isArray(providers)).toBe(true);
});
- it("should include ClaudeProvider", () => {
+ it('should include ClaudeProvider', () => {
const providers = ProviderFactory.getAllProviders();
- const hasClaudeProvider = providers.some(
- (p) => p instanceof ClaudeProvider
- );
+ const hasClaudeProvider = providers.some((p) => p instanceof ClaudeProvider);
expect(hasClaudeProvider).toBe(true);
});
- it("should return exactly 1 provider", () => {
+ it('should return exactly 1 provider', () => {
const providers = ProviderFactory.getAllProviders();
expect(providers).toHaveLength(1);
});
- it("should create new instances each time", () => {
+ it('should create new instances each time', () => {
const providers1 = ProviderFactory.getAllProviders();
const providers2 = ProviderFactory.getAllProviders();
@@ -137,60 +127,60 @@ describe("provider-factory.ts", () => {
});
});
- describe("checkAllProviders", () => {
- it("should return installation status for all providers", async () => {
+ describe('checkAllProviders', () => {
+ it('should return installation status for all providers', async () => {
const statuses = await ProviderFactory.checkAllProviders();
- expect(statuses).toHaveProperty("claude");
+ expect(statuses).toHaveProperty('claude');
});
- it("should call detectInstallation on each provider", async () => {
+ it('should call detectInstallation on each provider', async () => {
const statuses = await ProviderFactory.checkAllProviders();
- expect(statuses.claude).toHaveProperty("installed");
+ expect(statuses.claude).toHaveProperty('installed');
});
- it("should return correct provider names as keys", async () => {
+ it('should return correct provider names as keys', async () => {
const statuses = await ProviderFactory.checkAllProviders();
const keys = Object.keys(statuses);
- expect(keys).toContain("claude");
+ expect(keys).toContain('claude');
expect(keys).toHaveLength(1);
});
});
- describe("getProviderByName", () => {
+ describe('getProviderByName', () => {
it("should return ClaudeProvider for 'claude'", () => {
- const provider = ProviderFactory.getProviderByName("claude");
+ const provider = ProviderFactory.getProviderByName('claude');
expect(provider).toBeInstanceOf(ClaudeProvider);
});
it("should return ClaudeProvider for 'anthropic'", () => {
- const provider = ProviderFactory.getProviderByName("anthropic");
+ const provider = ProviderFactory.getProviderByName('anthropic');
expect(provider).toBeInstanceOf(ClaudeProvider);
});
- it("should be case-insensitive", () => {
- const provider1 = ProviderFactory.getProviderByName("CLAUDE");
- const provider2 = ProviderFactory.getProviderByName("ANTHROPIC");
+ it('should be case-insensitive', () => {
+ const provider1 = ProviderFactory.getProviderByName('CLAUDE');
+ const provider2 = ProviderFactory.getProviderByName('ANTHROPIC');
expect(provider1).toBeInstanceOf(ClaudeProvider);
expect(provider2).toBeInstanceOf(ClaudeProvider);
});
- it("should return null for unknown provider", () => {
- const provider = ProviderFactory.getProviderByName("unknown");
+ it('should return null for unknown provider', () => {
+ const provider = ProviderFactory.getProviderByName('unknown');
expect(provider).toBeNull();
});
- it("should return null for empty string", () => {
- const provider = ProviderFactory.getProviderByName("");
+ it('should return null for empty string', () => {
+ const provider = ProviderFactory.getProviderByName('');
expect(provider).toBeNull();
});
- it("should create new instance each time", () => {
- const provider1 = ProviderFactory.getProviderByName("claude");
- const provider2 = ProviderFactory.getProviderByName("claude");
+ it('should create new instance each time', () => {
+ const provider1 = ProviderFactory.getProviderByName('claude');
+ const provider2 = ProviderFactory.getProviderByName('claude');
expect(provider1).not.toBe(provider2);
expect(provider1).toBeInstanceOf(ClaudeProvider);
@@ -198,35 +188,33 @@ describe("provider-factory.ts", () => {
});
});
- describe("getAllAvailableModels", () => {
- it("should return array of models", () => {
+ describe('getAllAvailableModels', () => {
+ it('should return array of models', () => {
const models = ProviderFactory.getAllAvailableModels();
expect(Array.isArray(models)).toBe(true);
});
- it("should include models from all providers", () => {
+ it('should include models from all providers', () => {
const models = ProviderFactory.getAllAvailableModels();
expect(models.length).toBeGreaterThan(0);
});
- it("should return models with required fields", () => {
+ it('should return models with required fields', () => {
const models = ProviderFactory.getAllAvailableModels();
models.forEach((model) => {
- expect(model).toHaveProperty("id");
- expect(model).toHaveProperty("name");
- expect(typeof model.id).toBe("string");
- expect(typeof model.name).toBe("string");
+ expect(model).toHaveProperty('id');
+ expect(model).toHaveProperty('name');
+ expect(typeof model.id).toBe('string');
+ expect(typeof model.name).toBe('string');
});
});
- it("should include Claude models", () => {
+ it('should include Claude models', () => {
const models = ProviderFactory.getAllAvailableModels();
// Claude models should include claude-* in their IDs
- const hasClaudeModels = models.some((m) =>
- m.id.toLowerCase().includes("claude")
- );
+ const hasClaudeModels = models.some((m) => m.id.toLowerCase().includes('claude'));
expect(hasClaudeModels).toBe(true);
});
diff --git a/apps/server/tests/unit/routes/app-spec/common.test.ts b/apps/server/tests/unit/routes/app-spec/common.test.ts
index 14ec98d1..aeaf8ea5 100644
--- a/apps/server/tests/unit/routes/app-spec/common.test.ts
+++ b/apps/server/tests/unit/routes/app-spec/common.test.ts
@@ -1,65 +1,59 @@
-import { describe, it, expect, beforeEach } from "vitest";
+import { describe, it, expect, beforeEach } from 'vitest';
import {
setRunningState,
getErrorMessage,
getSpecRegenerationStatus,
-} from "@/routes/app-spec/common.js";
+} from '@/routes/app-spec/common.js';
-describe("app-spec/common.ts", () => {
+describe('app-spec/common.ts', () => {
beforeEach(() => {
// Reset state before each test
setRunningState(false, null);
});
- describe("setRunningState", () => {
- it("should set isRunning to true when running is true", () => {
+ describe('setRunningState', () => {
+ it('should set isRunning to true when running is true', () => {
setRunningState(true);
expect(getSpecRegenerationStatus().isRunning).toBe(true);
});
- it("should set isRunning to false when running is false", () => {
+ it('should set isRunning to false when running is false', () => {
setRunningState(true);
setRunningState(false);
expect(getSpecRegenerationStatus().isRunning).toBe(false);
});
- it("should set currentAbortController when provided", () => {
+ it('should set currentAbortController when provided', () => {
const controller = new AbortController();
setRunningState(true, controller);
- expect(getSpecRegenerationStatus().currentAbortController).toBe(
- controller
- );
+ expect(getSpecRegenerationStatus().currentAbortController).toBe(controller);
});
- it("should set currentAbortController to null when not provided", () => {
+ it('should set currentAbortController to null when not provided', () => {
const controller = new AbortController();
setRunningState(true, controller);
setRunningState(false);
expect(getSpecRegenerationStatus().currentAbortController).toBe(null);
});
- it("should set currentAbortController to null when explicitly passed null", () => {
+ it('should set currentAbortController to null when explicitly passed null', () => {
const controller = new AbortController();
setRunningState(true, controller);
setRunningState(true, null);
expect(getSpecRegenerationStatus().currentAbortController).toBe(null);
});
- it("should update state multiple times correctly", () => {
+ it('should update state multiple times correctly', () => {
const controller1 = new AbortController();
const controller2 = new AbortController();
setRunningState(true, controller1);
expect(getSpecRegenerationStatus().isRunning).toBe(true);
- expect(getSpecRegenerationStatus().currentAbortController).toBe(
- controller1
- );
+ expect(getSpecRegenerationStatus().currentAbortController).toBe(controller1);
setRunningState(true, controller2);
expect(getSpecRegenerationStatus().isRunning).toBe(true);
- expect(getSpecRegenerationStatus().currentAbortController).toBe(
- controller2
- );
+ expect(getSpecRegenerationStatus().currentAbortController).toBe(controller2);
setRunningState(false, null);
expect(getSpecRegenerationStatus().isRunning).toBe(false);
@@ -67,42 +61,42 @@ describe("app-spec/common.ts", () => {
});
});
- describe("getErrorMessage", () => {
- it("should return message from Error instance", () => {
- const error = new Error("Test error message");
- expect(getErrorMessage(error)).toBe("Test error message");
+ describe('getErrorMessage', () => {
+ it('should return message from Error instance', () => {
+ const error = new Error('Test error message');
+ expect(getErrorMessage(error)).toBe('Test error message');
});
it("should return 'Unknown error' for non-Error objects", () => {
- expect(getErrorMessage("string error")).toBe("Unknown error");
- expect(getErrorMessage(123)).toBe("Unknown error");
- expect(getErrorMessage(null)).toBe("Unknown error");
- expect(getErrorMessage(undefined)).toBe("Unknown error");
- expect(getErrorMessage({})).toBe("Unknown error");
- expect(getErrorMessage([])).toBe("Unknown error");
+ expect(getErrorMessage('string error')).toBe('Unknown error');
+ expect(getErrorMessage(123)).toBe('Unknown error');
+ expect(getErrorMessage(null)).toBe('Unknown error');
+ expect(getErrorMessage(undefined)).toBe('Unknown error');
+ expect(getErrorMessage({})).toBe('Unknown error');
+ expect(getErrorMessage([])).toBe('Unknown error');
});
- it("should return message from Error with empty message", () => {
- const error = new Error("");
- expect(getErrorMessage(error)).toBe("");
+ it('should return message from Error with empty message', () => {
+ const error = new Error('');
+ expect(getErrorMessage(error)).toBe('');
});
- it("should handle Error objects with custom properties", () => {
- const error = new Error("Base message");
- (error as any).customProp = "custom value";
- expect(getErrorMessage(error)).toBe("Base message");
+ it('should handle Error objects with custom properties', () => {
+ const error = new Error('Base message');
+ (error as any).customProp = 'custom value';
+ expect(getErrorMessage(error)).toBe('Base message');
});
- it("should handle Error objects created with different constructors", () => {
+ it('should handle Error objects created with different constructors', () => {
class CustomError extends Error {
constructor(message: string) {
super(message);
- this.name = "CustomError";
+ this.name = 'CustomError';
}
}
- const customError = new CustomError("Custom error message");
- expect(getErrorMessage(customError)).toBe("Custom error message");
+ const customError = new CustomError('Custom error message');
+ expect(getErrorMessage(customError)).toBe('Custom error message');
});
});
});
diff --git a/apps/server/tests/unit/routes/app-spec/parse-and-create-features.test.ts b/apps/server/tests/unit/routes/app-spec/parse-and-create-features.test.ts
index 7b3d0568..9bb5c120 100644
--- a/apps/server/tests/unit/routes/app-spec/parse-and-create-features.test.ts
+++ b/apps/server/tests/unit/routes/app-spec/parse-and-create-features.test.ts
@@ -1,11 +1,11 @@
-import { describe, it, expect } from "vitest";
+import { describe, it, expect } from 'vitest';
-describe("app-spec/parse-and-create-features.ts - JSON extraction", () => {
+describe('app-spec/parse-and-create-features.ts - JSON extraction', () => {
// Test the JSON extraction regex pattern used in parseAndCreateFeatures
const jsonExtractionPattern = /\{[\s\S]*"features"[\s\S]*\}/;
- describe("JSON extraction regex", () => {
- it("should extract JSON with features array", () => {
+ describe('JSON extraction regex', () => {
+ it('should extract JSON with features array', () => {
const content = `Here is the response:
{
"features": [
@@ -26,7 +26,7 @@ describe("app-spec/parse-and-create-features.ts - JSON extraction", () => {
expect(match![0]).toContain('"id": "feature-1"');
});
- it("should extract JSON with multiple features", () => {
+ it('should extract JSON with multiple features', () => {
const content = `Some text before
{
"features": [
@@ -49,7 +49,7 @@ Some text after`;
expect(match![0]).toContain('"feature-2"');
});
- it("should extract JSON with nested objects and arrays", () => {
+ it('should extract JSON with nested objects and arrays', () => {
const content = `Response:
{
"features": [
@@ -69,7 +69,7 @@ Some text after`;
expect(match![0]).toContain('"dep-1"');
});
- it("should handle JSON with whitespace and newlines", () => {
+ it('should handle JSON with whitespace and newlines', () => {
const content = `Text before
{
"features": [
@@ -87,7 +87,7 @@ Text after`;
expect(match![0]).toContain('"features"');
});
- it("should extract JSON when features array is empty", () => {
+ it('should extract JSON when features array is empty', () => {
const content = `Response:
{
"features": []
@@ -96,10 +96,10 @@ Text after`;
const match = content.match(jsonExtractionPattern);
expect(match).not.toBeNull();
expect(match![0]).toContain('"features"');
- expect(match![0]).toContain("[]");
+ expect(match![0]).toContain('[]');
});
- it("should not match content without features key", () => {
+ it('should not match content without features key', () => {
const content = `{
"otherKey": "value"
}`;
@@ -108,13 +108,13 @@ Text after`;
expect(match).toBeNull();
});
- it("should not match content without JSON structure", () => {
- const content = "Just plain text with features mentioned";
+ it('should not match content without JSON structure', () => {
+ const content = 'Just plain text with features mentioned';
const match = content.match(jsonExtractionPattern);
expect(match).toBeNull();
});
- it("should extract JSON when features key appears multiple times", () => {
+ it('should extract JSON when features key appears multiple times', () => {
const content = `Before:
{
"features": [
@@ -132,7 +132,7 @@ After: The word "features" appears again`;
expect(match![0]).toContain('"features"');
});
- it("should handle JSON with escaped quotes", () => {
+ it('should handle JSON with escaped quotes', () => {
const content = `{
"features": [
{
@@ -147,7 +147,7 @@ After: The word "features" appears again`;
expect(match![0]).toContain('"features"');
});
- it("should extract JSON with complex nested structure", () => {
+ it('should extract JSON with complex nested structure', () => {
const content = `Response:
{
"features": [
@@ -177,8 +177,8 @@ After: The word "features" appears again`;
});
});
- describe("JSON parsing validation", () => {
- it("should parse valid feature JSON structure", () => {
+ describe('JSON parsing validation', () => {
+ it('should parse valid feature JSON structure', () => {
const validJson = `{
"features": [
{
@@ -196,11 +196,11 @@ After: The word "features" appears again`;
expect(parsed.features).toBeDefined();
expect(Array.isArray(parsed.features)).toBe(true);
expect(parsed.features.length).toBe(1);
- expect(parsed.features[0].id).toBe("feature-1");
- expect(parsed.features[0].title).toBe("Test Feature");
+ expect(parsed.features[0].id).toBe('feature-1');
+ expect(parsed.features[0].title).toBe('Test Feature');
});
- it("should handle features with optional fields", () => {
+ it('should handle features with optional fields', () => {
const jsonWithOptionalFields = `{
"features": [
{
@@ -213,14 +213,14 @@ After: The word "features" appears again`;
}`;
const parsed = JSON.parse(jsonWithOptionalFields);
- expect(parsed.features[0].id).toBe("feature-1");
+ expect(parsed.features[0].id).toBe('feature-1');
expect(parsed.features[0].priority).toBe(2);
// description and dependencies are optional
expect(parsed.features[0].description).toBeUndefined();
expect(parsed.features[0].dependencies).toBeUndefined();
});
- it("should handle features with dependencies", () => {
+ it('should handle features with dependencies', () => {
const jsonWithDeps = `{
"features": [
{
@@ -238,7 +238,7 @@ After: The word "features" appears again`;
const parsed = JSON.parse(jsonWithDeps);
expect(parsed.features[0].dependencies).toEqual([]);
- expect(parsed.features[1].dependencies).toEqual(["feature-1"]);
+ expect(parsed.features[1].dependencies).toEqual(['feature-1']);
});
});
});
diff --git a/apps/server/tests/unit/services/agent-service.test.ts b/apps/server/tests/unit/services/agent-service.test.ts
index 1661522c..ef2a5e0d 100644
--- a/apps/server/tests/unit/services/agent-service.test.ts
+++ b/apps/server/tests/unit/services/agent-service.test.ts
@@ -1,17 +1,17 @@
-import { describe, it, expect, vi, beforeEach } from "vitest";
-import { AgentService } from "@/services/agent-service.js";
-import { ProviderFactory } from "@/providers/provider-factory.js";
-import * as fs from "fs/promises";
-import * as imageHandler from "@automaker/utils";
-import * as promptBuilder from "@automaker/utils";
-import { collectAsyncGenerator } from "../../utils/helpers.js";
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { AgentService } from '@/services/agent-service.js';
+import { ProviderFactory } from '@/providers/provider-factory.js';
+import * as fs from 'fs/promises';
+import * as imageHandler from '@automaker/utils';
+import * as promptBuilder from '@automaker/utils';
+import * as contextLoader from '@automaker/utils';
+import { collectAsyncGenerator } from '../../utils/helpers.js';
-vi.mock("fs/promises");
-vi.mock("@/providers/provider-factory.js");
-vi.mock("@automaker/utils");
-vi.mock("@automaker/utils");
+vi.mock('fs/promises');
+vi.mock('@/providers/provider-factory.js');
+vi.mock('@automaker/utils');
-describe("agent-service.ts", () => {
+describe('agent-service.ts', () => {
let service: AgentService;
const mockEvents = {
subscribe: vi.fn(),
@@ -20,86 +20,89 @@ describe("agent-service.ts", () => {
beforeEach(() => {
vi.clearAllMocks();
- service = new AgentService("/test/data", mockEvents as any);
+ service = new AgentService('/test/data', mockEvents as any);
+
+ // Mock loadContextFiles to return empty context by default
+ vi.mocked(contextLoader.loadContextFiles).mockResolvedValue({
+ files: [],
+ formattedPrompt: '',
+ });
});
- describe("initialize", () => {
- it("should create state directory", async () => {
+ describe('initialize', () => {
+ it('should create state directory', async () => {
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
await service.initialize();
- expect(fs.mkdir).toHaveBeenCalledWith(
- expect.stringContaining("agent-sessions"),
- { recursive: true }
- );
+ expect(fs.mkdir).toHaveBeenCalledWith(expect.stringContaining('agent-sessions'), {
+ recursive: true,
+ });
});
});
- describe("startConversation", () => {
- it("should create new session with empty messages", async () => {
- const error: any = new Error("ENOENT");
- error.code = "ENOENT";
+ describe('startConversation', () => {
+ it('should create new session with empty messages', async () => {
+ const error: any = new Error('ENOENT');
+ error.code = 'ENOENT';
vi.mocked(fs.readFile).mockRejectedValue(error);
const result = await service.startConversation({
- sessionId: "session-1",
- workingDirectory: "/test/dir",
+ sessionId: 'session-1',
+ workingDirectory: '/test/dir',
});
expect(result.success).toBe(true);
expect(result.messages).toEqual([]);
- expect(result.sessionId).toBe("session-1");
+ expect(result.sessionId).toBe('session-1');
});
- it("should load existing session", async () => {
+ it('should load existing session', async () => {
const existingMessages = [
{
- id: "msg-1",
- role: "user",
- content: "Hello",
- timestamp: "2024-01-01T00:00:00Z",
+ id: 'msg-1',
+ role: 'user',
+ content: 'Hello',
+ timestamp: '2024-01-01T00:00:00Z',
},
];
- vi.mocked(fs.readFile).mockResolvedValue(
- JSON.stringify(existingMessages)
- );
+ vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(existingMessages));
const result = await service.startConversation({
- sessionId: "session-1",
- workingDirectory: "/test/dir",
+ sessionId: 'session-1',
+ workingDirectory: '/test/dir',
});
expect(result.success).toBe(true);
expect(result.messages).toEqual(existingMessages);
});
- it("should use process.cwd() if no working directory provided", async () => {
- const error: any = new Error("ENOENT");
- error.code = "ENOENT";
+ it('should use process.cwd() if no working directory provided', async () => {
+ const error: any = new Error('ENOENT');
+ error.code = 'ENOENT';
vi.mocked(fs.readFile).mockRejectedValue(error);
const result = await service.startConversation({
- sessionId: "session-1",
+ sessionId: 'session-1',
});
expect(result.success).toBe(true);
});
- it("should reuse existing session if already started", async () => {
- const error: any = new Error("ENOENT");
- error.code = "ENOENT";
+ it('should reuse existing session if already started', async () => {
+ const error: any = new Error('ENOENT');
+ error.code = 'ENOENT';
vi.mocked(fs.readFile).mockRejectedValue(error);
// Start session first time
await service.startConversation({
- sessionId: "session-1",
+ sessionId: 'session-1',
});
// Start again with same ID
const result = await service.startConversation({
- sessionId: "session-1",
+ sessionId: 'session-1',
});
expect(result.success).toBe(true);
@@ -109,252 +112,237 @@ describe("agent-service.ts", () => {
});
});
- describe("sendMessage", () => {
+ describe('sendMessage', () => {
beforeEach(async () => {
- const error: any = new Error("ENOENT");
- error.code = "ENOENT";
+ const error: any = new Error('ENOENT');
+ error.code = 'ENOENT';
vi.mocked(fs.readFile).mockRejectedValue(error);
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
await service.startConversation({
- sessionId: "session-1",
- workingDirectory: "/test/dir",
+ sessionId: 'session-1',
+ workingDirectory: '/test/dir',
});
});
- it("should throw if session not found", async () => {
+ it('should throw if session not found', async () => {
await expect(
service.sendMessage({
- sessionId: "nonexistent",
- message: "Hello",
+ sessionId: 'nonexistent',
+ message: 'Hello',
})
- ).rejects.toThrow("Session nonexistent not found");
+ ).rejects.toThrow('Session nonexistent not found');
});
-
- it("should process message and stream responses", async () => {
+ it('should process message and stream responses', async () => {
const mockProvider = {
- getName: () => "claude",
+ getName: () => 'claude',
executeQuery: async function* () {
yield {
- type: "assistant",
+ type: 'assistant',
message: {
- role: "assistant",
- content: [{ type: "text", text: "Response" }],
+ role: 'assistant',
+ content: [{ type: 'text', text: 'Response' }],
},
};
yield {
- type: "result",
- subtype: "success",
+ type: 'result',
+ subtype: 'success',
};
},
};
- vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(
- mockProvider as any
- );
+ vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
vi.mocked(promptBuilder.buildPromptWithImages).mockResolvedValue({
- content: "Hello",
+ content: 'Hello',
hasImages: false,
});
const result = await service.sendMessage({
- sessionId: "session-1",
- message: "Hello",
- workingDirectory: "/custom/dir",
+ sessionId: 'session-1',
+ message: 'Hello',
+ workingDirectory: '/custom/dir',
});
expect(result.success).toBe(true);
expect(mockEvents.emit).toHaveBeenCalled();
});
- it("should handle images in message", async () => {
+ it('should handle images in message', async () => {
const mockProvider = {
- getName: () => "claude",
+ getName: () => 'claude',
executeQuery: async function* () {
yield {
- type: "result",
- subtype: "success",
+ type: 'result',
+ subtype: 'success',
};
},
};
- vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(
- mockProvider as any
- );
+ vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
vi.mocked(imageHandler.readImageAsBase64).mockResolvedValue({
- base64: "base64data",
- mimeType: "image/png",
- filename: "test.png",
- originalPath: "/path/test.png",
+ base64: 'base64data',
+ mimeType: 'image/png',
+ filename: 'test.png',
+ originalPath: '/path/test.png',
});
vi.mocked(promptBuilder.buildPromptWithImages).mockResolvedValue({
- content: "Check image",
+ content: 'Check image',
hasImages: true,
});
await service.sendMessage({
- sessionId: "session-1",
- message: "Check this",
- imagePaths: ["/path/test.png"],
+ sessionId: 'session-1',
+ message: 'Check this',
+ imagePaths: ['/path/test.png'],
});
- expect(imageHandler.readImageAsBase64).toHaveBeenCalledWith(
- "/path/test.png"
- );
+ expect(imageHandler.readImageAsBase64).toHaveBeenCalledWith('/path/test.png');
});
- it("should handle failed image loading gracefully", async () => {
+ it('should handle failed image loading gracefully', async () => {
const mockProvider = {
- getName: () => "claude",
+ getName: () => 'claude',
executeQuery: async function* () {
yield {
- type: "result",
- subtype: "success",
+ type: 'result',
+ subtype: 'success',
};
},
};
- vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(
- mockProvider as any
- );
+ vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
- vi.mocked(imageHandler.readImageAsBase64).mockRejectedValue(
- new Error("Image not found")
- );
+ vi.mocked(imageHandler.readImageAsBase64).mockRejectedValue(new Error('Image not found'));
vi.mocked(promptBuilder.buildPromptWithImages).mockResolvedValue({
- content: "Check image",
+ content: 'Check image',
hasImages: false,
});
- const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
await service.sendMessage({
- sessionId: "session-1",
- message: "Check this",
- imagePaths: ["/path/test.png"],
+ sessionId: 'session-1',
+ message: 'Check this',
+ imagePaths: ['/path/test.png'],
});
expect(consoleSpy).toHaveBeenCalled();
consoleSpy.mockRestore();
});
- it("should use custom model if provided", async () => {
+ it('should use custom model if provided', async () => {
const mockProvider = {
- getName: () => "claude",
+ getName: () => 'claude',
executeQuery: async function* () {
yield {
- type: "result",
- subtype: "success",
+ type: 'result',
+ subtype: 'success',
};
},
};
- vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(
- mockProvider as any
- );
+ vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
vi.mocked(promptBuilder.buildPromptWithImages).mockResolvedValue({
- content: "Hello",
+ content: 'Hello',
hasImages: false,
});
await service.sendMessage({
- sessionId: "session-1",
- message: "Hello",
- model: "claude-sonnet-4-20250514",
+ sessionId: 'session-1',
+ message: 'Hello',
+ model: 'claude-sonnet-4-20250514',
});
- expect(ProviderFactory.getProviderForModel).toHaveBeenCalledWith("claude-sonnet-4-20250514");
+ expect(ProviderFactory.getProviderForModel).toHaveBeenCalledWith('claude-sonnet-4-20250514');
});
- it("should save session messages", async () => {
+ it('should save session messages', async () => {
const mockProvider = {
- getName: () => "claude",
+ getName: () => 'claude',
executeQuery: async function* () {
yield {
- type: "result",
- subtype: "success",
+ type: 'result',
+ subtype: 'success',
};
},
};
- vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(
- mockProvider as any
- );
+ vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
vi.mocked(promptBuilder.buildPromptWithImages).mockResolvedValue({
- content: "Hello",
+ content: 'Hello',
hasImages: false,
});
await service.sendMessage({
- sessionId: "session-1",
- message: "Hello",
+ sessionId: 'session-1',
+ message: 'Hello',
});
expect(fs.writeFile).toHaveBeenCalled();
});
});
- describe("stopExecution", () => {
- it("should stop execution for a session", async () => {
- const error: any = new Error("ENOENT");
- error.code = "ENOENT";
+ describe('stopExecution', () => {
+ it('should stop execution for a session', async () => {
+ const error: any = new Error('ENOENT');
+ error.code = 'ENOENT';
vi.mocked(fs.readFile).mockRejectedValue(error);
await service.startConversation({
- sessionId: "session-1",
+ sessionId: 'session-1',
});
// Should return success
- const result = await service.stopExecution("session-1");
+ const result = await service.stopExecution('session-1');
expect(result.success).toBeDefined();
});
});
- describe("getHistory", () => {
- it("should return message history", async () => {
- const error: any = new Error("ENOENT");
- error.code = "ENOENT";
+ describe('getHistory', () => {
+ it('should return message history', async () => {
+ const error: any = new Error('ENOENT');
+ error.code = 'ENOENT';
vi.mocked(fs.readFile).mockRejectedValue(error);
await service.startConversation({
- sessionId: "session-1",
+ sessionId: 'session-1',
});
- const history = service.getHistory("session-1");
+ const history = service.getHistory('session-1');
expect(history).toBeDefined();
expect(history?.messages).toEqual([]);
});
- it("should handle non-existent session", () => {
- const history = service.getHistory("nonexistent");
+ it('should handle non-existent session', () => {
+ const history = service.getHistory('nonexistent');
expect(history).toBeDefined(); // Returns error object
});
});
- describe("clearSession", () => {
- it("should clear session messages", async () => {
- const error: any = new Error("ENOENT");
- error.code = "ENOENT";
+ describe('clearSession', () => {
+ it('should clear session messages', async () => {
+ const error: any = new Error('ENOENT');
+ error.code = 'ENOENT';
vi.mocked(fs.readFile).mockRejectedValue(error);
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
await service.startConversation({
- sessionId: "session-1",
+ sessionId: 'session-1',
});
- await service.clearSession("session-1");
+ await service.clearSession('session-1');
- const history = service.getHistory("session-1");
+ const history = service.getHistory('session-1');
expect(history?.messages).toEqual([]);
expect(fs.writeFile).toHaveBeenCalled();
});
diff --git a/apps/server/tests/unit/services/auto-mode-service-planning.test.ts b/apps/server/tests/unit/services/auto-mode-service-planning.test.ts
index 09483e78..7b52fe38 100644
--- a/apps/server/tests/unit/services/auto-mode-service-planning.test.ts
+++ b/apps/server/tests/unit/services/auto-mode-service-planning.test.ts
@@ -1,7 +1,7 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
-import { AutoModeService } from "@/services/auto-mode-service.js";
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { AutoModeService } from '@/services/auto-mode-service.js';
-describe("auto-mode-service.ts - Planning Mode", () => {
+describe('auto-mode-service.ts - Planning Mode', () => {
let service: AutoModeService;
const mockEvents = {
subscribe: vi.fn(),
@@ -18,98 +18,98 @@ describe("auto-mode-service.ts - Planning Mode", () => {
await service.stopAutoLoop().catch(() => {});
});
- describe("getPlanningPromptPrefix", () => {
+ describe('getPlanningPromptPrefix', () => {
// Access private method through any cast for testing
const getPlanningPromptPrefix = (svc: any, feature: any) => {
return svc.getPlanningPromptPrefix(feature);
};
- it("should return empty string for skip mode", () => {
- const feature = { id: "test", planningMode: "skip" as const };
+ it('should return empty string for skip mode', () => {
+ const feature = { id: 'test', planningMode: 'skip' as const };
const result = getPlanningPromptPrefix(service, feature);
- expect(result).toBe("");
+ expect(result).toBe('');
});
- it("should return empty string when planningMode is undefined", () => {
- const feature = { id: "test" };
+ it('should return empty string when planningMode is undefined', () => {
+ const feature = { id: 'test' };
const result = getPlanningPromptPrefix(service, feature);
- expect(result).toBe("");
+ expect(result).toBe('');
});
- it("should return lite prompt for lite mode without approval", () => {
+ it('should return lite prompt for lite mode without approval', () => {
const feature = {
- id: "test",
- planningMode: "lite" as const,
- requirePlanApproval: false
+ id: 'test',
+ planningMode: 'lite' as const,
+ requirePlanApproval: false,
};
const result = getPlanningPromptPrefix(service, feature);
- expect(result).toContain("Planning Phase (Lite Mode)");
- expect(result).toContain("[PLAN_GENERATED]");
- expect(result).toContain("Feature Request");
+ expect(result).toContain('Planning Phase (Lite Mode)');
+ expect(result).toContain('[PLAN_GENERATED]');
+ expect(result).toContain('Feature Request');
});
- it("should return lite_with_approval prompt for lite mode with approval", () => {
+ it('should return lite_with_approval prompt for lite mode with approval', () => {
const feature = {
- id: "test",
- planningMode: "lite" as const,
- requirePlanApproval: true
+ id: 'test',
+ planningMode: 'lite' as const,
+ requirePlanApproval: true,
};
const result = getPlanningPromptPrefix(service, feature);
- expect(result).toContain("Planning Phase (Lite Mode)");
- expect(result).toContain("[SPEC_GENERATED]");
- expect(result).toContain("DO NOT proceed with implementation");
+ expect(result).toContain('Planning Phase (Lite Mode)');
+ expect(result).toContain('[SPEC_GENERATED]');
+ expect(result).toContain('DO NOT proceed with implementation');
});
- it("should return spec prompt for spec mode", () => {
+ it('should return spec prompt for spec mode', () => {
const feature = {
- id: "test",
- planningMode: "spec" as const
+ id: 'test',
+ planningMode: 'spec' as const,
};
const result = getPlanningPromptPrefix(service, feature);
- expect(result).toContain("Specification Phase (Spec Mode)");
- expect(result).toContain("```tasks");
- expect(result).toContain("T001");
- expect(result).toContain("[TASK_START]");
- expect(result).toContain("[TASK_COMPLETE]");
+ expect(result).toContain('Specification Phase (Spec Mode)');
+ expect(result).toContain('```tasks');
+ expect(result).toContain('T001');
+ expect(result).toContain('[TASK_START]');
+ expect(result).toContain('[TASK_COMPLETE]');
});
- it("should return full prompt for full mode", () => {
+ it('should return full prompt for full mode', () => {
const feature = {
- id: "test",
- planningMode: "full" as const
+ id: 'test',
+ planningMode: 'full' as const,
};
const result = getPlanningPromptPrefix(service, feature);
- expect(result).toContain("Full Specification Phase (Full SDD Mode)");
- expect(result).toContain("Phase 1: Foundation");
- expect(result).toContain("Phase 2: Core Implementation");
- expect(result).toContain("Phase 3: Integration & Testing");
+ expect(result).toContain('Full Specification Phase (Full SDD Mode)');
+ expect(result).toContain('Phase 1: Foundation');
+ expect(result).toContain('Phase 2: Core Implementation');
+ expect(result).toContain('Phase 3: Integration & Testing');
});
- it("should include the separator and Feature Request header", () => {
+ it('should include the separator and Feature Request header', () => {
const feature = {
- id: "test",
- planningMode: "spec" as const
+ id: 'test',
+ planningMode: 'spec' as const,
};
const result = getPlanningPromptPrefix(service, feature);
- expect(result).toContain("---");
- expect(result).toContain("## Feature Request");
+ expect(result).toContain('---');
+ expect(result).toContain('## Feature Request');
});
- it("should instruct agent to NOT output exploration text", () => {
- const modes = ["lite", "spec", "full"] as const;
+ it('should instruct agent to NOT output exploration text', () => {
+ const modes = ['lite', 'spec', 'full'] as const;
for (const mode of modes) {
- const feature = { id: "test", planningMode: mode };
+ const feature = { id: 'test', planningMode: mode };
const result = getPlanningPromptPrefix(service, feature);
- expect(result).toContain("Do NOT output exploration text");
- expect(result).toContain("Start DIRECTLY");
+ expect(result).toContain('Do NOT output exploration text');
+ expect(result).toContain('Start DIRECTLY');
}
});
});
- describe("parseTasksFromSpec (via module)", () => {
+ describe('parseTasksFromSpec (via module)', () => {
// We need to test the module-level function
// Import it directly for testing
- it("should parse tasks from a valid tasks block", async () => {
+ it('should parse tasks from a valid tasks block', async () => {
// This tests the internal logic through integration
// The function is module-level, so we verify behavior through the service
const specContent = `
@@ -123,12 +123,12 @@ describe("auto-mode-service.ts - Planning Mode", () => {
`;
// Since parseTasksFromSpec is a module-level function,
// we verify its behavior indirectly through plan parsing
- expect(specContent).toContain("T001");
- expect(specContent).toContain("T002");
- expect(specContent).toContain("T003");
+ expect(specContent).toContain('T001');
+ expect(specContent).toContain('T002');
+ expect(specContent).toContain('T003');
});
- it("should handle tasks block with phases", () => {
+ it('should handle tasks block with phases', () => {
const specContent = `
\`\`\`tasks
## Phase 1: Setup
@@ -139,190 +139,191 @@ describe("auto-mode-service.ts - Planning Mode", () => {
- [ ] T003: Create main module | File: src/index.ts
\`\`\`
`;
- expect(specContent).toContain("Phase 1");
- expect(specContent).toContain("Phase 2");
- expect(specContent).toContain("T001");
- expect(specContent).toContain("T003");
+ expect(specContent).toContain('Phase 1');
+ expect(specContent).toContain('Phase 2');
+ expect(specContent).toContain('T001');
+ expect(specContent).toContain('T003');
});
});
- describe("plan approval flow", () => {
- it("should track pending approvals correctly", () => {
- expect(service.hasPendingApproval("test-feature")).toBe(false);
+ describe('plan approval flow', () => {
+ it('should track pending approvals correctly', () => {
+ expect(service.hasPendingApproval('test-feature')).toBe(false);
});
- it("should allow cancelling non-existent approval without error", () => {
- expect(() => service.cancelPlanApproval("non-existent")).not.toThrow();
+ it('should allow cancelling non-existent approval without error', () => {
+ expect(() => service.cancelPlanApproval('non-existent')).not.toThrow();
});
- it("should return running features count after stop", async () => {
+ it('should return running features count after stop', async () => {
const count = await service.stopAutoLoop();
expect(count).toBe(0);
});
});
- describe("resolvePlanApproval", () => {
- it("should return error when no pending approval exists", async () => {
+ describe('resolvePlanApproval', () => {
+ it('should return error when no pending approval exists', async () => {
const result = await service.resolvePlanApproval(
- "non-existent-feature",
+ 'non-existent-feature',
true,
undefined,
undefined,
undefined
);
expect(result.success).toBe(false);
- expect(result.error).toContain("No pending approval");
+ expect(result.error).toContain('No pending approval');
});
- it("should handle approval with edited plan", async () => {
+ it('should handle approval with edited plan', async () => {
// Without a pending approval, this should fail gracefully
const result = await service.resolvePlanApproval(
- "test-feature",
+ 'test-feature',
true,
- "Edited plan content",
+ 'Edited plan content',
undefined,
undefined
);
expect(result.success).toBe(false);
});
- it("should handle rejection with feedback", async () => {
+ it('should handle rejection with feedback', async () => {
const result = await service.resolvePlanApproval(
- "test-feature",
+ 'test-feature',
false,
undefined,
- "Please add more details",
+ 'Please add more details',
undefined
);
expect(result.success).toBe(false);
});
});
- describe("buildFeaturePrompt", () => {
+ describe('buildFeaturePrompt', () => {
const buildFeaturePrompt = (svc: any, feature: any) => {
return svc.buildFeaturePrompt(feature);
};
- it("should include feature ID and description", () => {
+ it('should include feature ID and description', () => {
const feature = {
- id: "feat-123",
- description: "Add user authentication",
+ id: 'feat-123',
+ description: 'Add user authentication',
};
const result = buildFeaturePrompt(service, feature);
- expect(result).toContain("feat-123");
- expect(result).toContain("Add user authentication");
+ expect(result).toContain('feat-123');
+ expect(result).toContain('Add user authentication');
});
- it("should include specification when present", () => {
+ it('should include specification when present', () => {
const feature = {
- id: "feat-123",
- description: "Test feature",
- spec: "Detailed specification here",
+ id: 'feat-123',
+ description: 'Test feature',
+ spec: 'Detailed specification here',
};
const result = buildFeaturePrompt(service, feature);
- expect(result).toContain("Specification:");
- expect(result).toContain("Detailed specification here");
+ expect(result).toContain('Specification:');
+ expect(result).toContain('Detailed specification here');
});
- it("should include image paths when present", () => {
+ it('should include image paths when present', () => {
const feature = {
- id: "feat-123",
- description: "Test feature",
+ id: 'feat-123',
+ description: 'Test feature',
imagePaths: [
- { path: "/tmp/image1.png", filename: "image1.png", mimeType: "image/png" },
- "/tmp/image2.jpg",
+ { path: '/tmp/image1.png', filename: 'image1.png', mimeType: 'image/png' },
+ '/tmp/image2.jpg',
],
};
const result = buildFeaturePrompt(service, feature);
- expect(result).toContain("Context Images Attached");
- expect(result).toContain("image1.png");
- expect(result).toContain("/tmp/image2.jpg");
+ expect(result).toContain('Context Images Attached');
+ expect(result).toContain('image1.png');
+ expect(result).toContain('/tmp/image2.jpg');
});
- it("should include summary tags instruction", () => {
+ it('should include summary tags instruction', () => {
const feature = {
- id: "feat-123",
- description: "Test feature",
+ id: 'feat-123',
+ description: 'Test feature',
};
const result = buildFeaturePrompt(service, feature);
- expect(result).toContain("");
- expect(result).toContain("");
+ expect(result).toContain('');
+ expect(result).toContain('');
});
});
- describe("extractTitleFromDescription", () => {
+ describe('extractTitleFromDescription', () => {
const extractTitle = (svc: any, description: string) => {
return svc.extractTitleFromDescription(description);
};
it("should return 'Untitled Feature' for empty description", () => {
- expect(extractTitle(service, "")).toBe("Untitled Feature");
- expect(extractTitle(service, " ")).toBe("Untitled Feature");
+ expect(extractTitle(service, '')).toBe('Untitled Feature');
+ expect(extractTitle(service, ' ')).toBe('Untitled Feature');
});
- it("should return first line if under 60 characters", () => {
- const description = "Add user login\nWith email validation";
- expect(extractTitle(service, description)).toBe("Add user login");
+ it('should return first line if under 60 characters', () => {
+ const description = 'Add user login\nWith email validation';
+ expect(extractTitle(service, description)).toBe('Add user login');
});
- it("should truncate long first lines to 60 characters", () => {
- const description = "This is a very long feature description that exceeds the sixty character limit significantly";
+ it('should truncate long first lines to 60 characters', () => {
+ const description =
+ 'This is a very long feature description that exceeds the sixty character limit significantly';
const result = extractTitle(service, description);
expect(result.length).toBe(60);
- expect(result).toContain("...");
+ expect(result).toContain('...');
});
});
- describe("PLANNING_PROMPTS structure", () => {
+ describe('PLANNING_PROMPTS structure', () => {
const getPlanningPromptPrefix = (svc: any, feature: any) => {
return svc.getPlanningPromptPrefix(feature);
};
- it("should have all required planning modes", () => {
- const modes = ["lite", "spec", "full"] as const;
+ it('should have all required planning modes', () => {
+ const modes = ['lite', 'spec', 'full'] as const;
for (const mode of modes) {
- const feature = { id: "test", planningMode: mode };
+ const feature = { id: 'test', planningMode: mode };
const result = getPlanningPromptPrefix(service, feature);
expect(result.length).toBeGreaterThan(100);
}
});
- it("lite prompt should include correct structure", () => {
- const feature = { id: "test", planningMode: "lite" as const };
+ it('lite prompt should include correct structure', () => {
+ const feature = { id: 'test', planningMode: 'lite' as const };
const result = getPlanningPromptPrefix(service, feature);
- expect(result).toContain("Goal");
- expect(result).toContain("Approach");
- expect(result).toContain("Files to Touch");
- expect(result).toContain("Tasks");
- expect(result).toContain("Risks");
+ expect(result).toContain('Goal');
+ expect(result).toContain('Approach');
+ expect(result).toContain('Files to Touch');
+ expect(result).toContain('Tasks');
+ expect(result).toContain('Risks');
});
- it("spec prompt should include task format instructions", () => {
- const feature = { id: "test", planningMode: "spec" as const };
+ it('spec prompt should include task format instructions', () => {
+ const feature = { id: 'test', planningMode: 'spec' as const };
const result = getPlanningPromptPrefix(service, feature);
- expect(result).toContain("Problem");
- expect(result).toContain("Solution");
- expect(result).toContain("Acceptance Criteria");
- expect(result).toContain("GIVEN-WHEN-THEN");
- expect(result).toContain("Implementation Tasks");
- expect(result).toContain("Verification");
+ expect(result).toContain('Problem');
+ expect(result).toContain('Solution');
+ expect(result).toContain('Acceptance Criteria');
+ expect(result).toContain('GIVEN-WHEN-THEN');
+ expect(result).toContain('Implementation Tasks');
+ expect(result).toContain('Verification');
});
- it("full prompt should include phases", () => {
- const feature = { id: "test", planningMode: "full" as const };
+ it('full prompt should include phases', () => {
+ const feature = { id: 'test', planningMode: 'full' as const };
const result = getPlanningPromptPrefix(service, feature);
- expect(result).toContain("Problem Statement");
- expect(result).toContain("User Story");
- expect(result).toContain("Technical Context");
- expect(result).toContain("Non-Goals");
- expect(result).toContain("Phase 1");
- expect(result).toContain("Phase 2");
- expect(result).toContain("Phase 3");
+ expect(result).toContain('Problem Statement');
+ expect(result).toContain('User Story');
+ expect(result).toContain('Technical Context');
+ expect(result).toContain('Non-Goals');
+ expect(result).toContain('Phase 1');
+ expect(result).toContain('Phase 2');
+ expect(result).toContain('Phase 3');
});
});
- describe("status management", () => {
- it("should report correct status", () => {
+ describe('status management', () => {
+ it('should report correct status', () => {
const status = service.getStatus();
expect(status.runningFeatures).toEqual([]);
expect(status.isRunning).toBe(false);
diff --git a/apps/server/tests/unit/services/auto-mode-service.test.ts b/apps/server/tests/unit/services/auto-mode-service.test.ts
index f108a638..ec0959d7 100644
--- a/apps/server/tests/unit/services/auto-mode-service.test.ts
+++ b/apps/server/tests/unit/services/auto-mode-service.test.ts
@@ -1,7 +1,7 @@
-import { describe, it, expect, vi, beforeEach } from "vitest";
-import { AutoModeService } from "@/services/auto-mode-service.js";
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { AutoModeService } from '@/services/auto-mode-service.js';
-describe("auto-mode-service.ts", () => {
+describe('auto-mode-service.ts', () => {
let service: AutoModeService;
const mockEvents = {
subscribe: vi.fn(),
@@ -13,29 +13,27 @@ describe("auto-mode-service.ts", () => {
service = new AutoModeService(mockEvents as any);
});
- describe("constructor", () => {
- it("should initialize with event emitter", () => {
+ describe('constructor', () => {
+ it('should initialize with event emitter', () => {
expect(service).toBeDefined();
});
});
- describe("startAutoLoop", () => {
- it("should throw if auto mode is already running", async () => {
+ describe('startAutoLoop', () => {
+ it('should throw if auto mode is already running', async () => {
// Start first loop
- const promise1 = service.startAutoLoop("/test/project", 3);
+ const promise1 = service.startAutoLoop('/test/project', 3);
// Try to start second loop
- await expect(
- service.startAutoLoop("/test/project", 3)
- ).rejects.toThrow("already running");
+ await expect(service.startAutoLoop('/test/project', 3)).rejects.toThrow('already running');
// Cleanup
await service.stopAutoLoop();
await promise1.catch(() => {});
});
- it("should emit auto mode start event", async () => {
- const promise = service.startAutoLoop("/test/project", 3);
+ it('should emit auto mode start event', async () => {
+ const promise = service.startAutoLoop('/test/project', 3);
// Give it time to emit the event
await new Promise((resolve) => setTimeout(resolve, 10));
@@ -43,7 +41,7 @@ describe("auto-mode-service.ts", () => {
expect(mockEvents.emit).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
- message: expect.stringContaining("Auto mode started"),
+ message: expect.stringContaining('Auto mode started'),
})
);
@@ -53,9 +51,9 @@ describe("auto-mode-service.ts", () => {
});
});
- describe("stopAutoLoop", () => {
- it("should stop the auto loop", async () => {
- const promise = service.startAutoLoop("/test/project", 3);
+ describe('stopAutoLoop', () => {
+ it('should stop the auto loop', async () => {
+ const promise = service.startAutoLoop('/test/project', 3);
const runningCount = await service.stopAutoLoop();
@@ -63,7 +61,7 @@ describe("auto-mode-service.ts", () => {
await promise.catch(() => {});
});
- it("should return 0 when not running", async () => {
+ it('should return 0 when not running', async () => {
const runningCount = await service.stopAutoLoop();
expect(runningCount).toBe(0);
});
diff --git a/apps/server/tests/unit/services/auto-mode-task-parsing.test.ts b/apps/server/tests/unit/services/auto-mode-task-parsing.test.ts
index becdd309..984e38c5 100644
--- a/apps/server/tests/unit/services/auto-mode-task-parsing.test.ts
+++ b/apps/server/tests/unit/services/auto-mode-task-parsing.test.ts
@@ -1,4 +1,4 @@
-import { describe, it, expect } from "vitest";
+import { describe, it, expect } from 'vitest';
/**
* Test the task parsing logic by reimplementing the parsing functions
@@ -88,59 +88,59 @@ function parseTasksFromSpec(specContent: string): ParsedTask[] {
return tasks;
}
-describe("Task Parsing", () => {
- describe("parseTaskLine", () => {
- it("should parse task with file path", () => {
- const line = "- [ ] T001: Create user model | File: src/models/user.ts";
+describe('Task Parsing', () => {
+ describe('parseTaskLine', () => {
+ it('should parse task with file path', () => {
+ const line = '- [ ] T001: Create user model | File: src/models/user.ts';
const result = parseTaskLine(line);
expect(result).toEqual({
- id: "T001",
- description: "Create user model",
- filePath: "src/models/user.ts",
+ id: 'T001',
+ description: 'Create user model',
+ filePath: 'src/models/user.ts',
phase: undefined,
- status: "pending",
+ status: 'pending',
});
});
- it("should parse task without file path", () => {
- const line = "- [ ] T002: Setup database connection";
+ it('should parse task without file path', () => {
+ const line = '- [ ] T002: Setup database connection';
const result = parseTaskLine(line);
expect(result).toEqual({
- id: "T002",
- description: "Setup database connection",
+ id: 'T002',
+ description: 'Setup database connection',
phase: undefined,
- status: "pending",
+ status: 'pending',
});
});
- it("should include phase when provided", () => {
- const line = "- [ ] T003: Write tests | File: tests/user.test.ts";
- const result = parseTaskLine(line, "Phase 1: Foundation");
- expect(result?.phase).toBe("Phase 1: Foundation");
+ it('should include phase when provided', () => {
+ const line = '- [ ] T003: Write tests | File: tests/user.test.ts';
+ const result = parseTaskLine(line, 'Phase 1: Foundation');
+ expect(result?.phase).toBe('Phase 1: Foundation');
});
- it("should return null for invalid line", () => {
- expect(parseTaskLine("- [ ] Invalid format")).toBeNull();
- expect(parseTaskLine("Not a task line")).toBeNull();
- expect(parseTaskLine("")).toBeNull();
+ it('should return null for invalid line', () => {
+ expect(parseTaskLine('- [ ] Invalid format')).toBeNull();
+ expect(parseTaskLine('Not a task line')).toBeNull();
+ expect(parseTaskLine('')).toBeNull();
});
- it("should handle multi-word descriptions", () => {
- const line = "- [ ] T004: Implement user authentication with JWT tokens | File: src/auth.ts";
+ it('should handle multi-word descriptions', () => {
+ const line = '- [ ] T004: Implement user authentication with JWT tokens | File: src/auth.ts';
const result = parseTaskLine(line);
- expect(result?.description).toBe("Implement user authentication with JWT tokens");
+ expect(result?.description).toBe('Implement user authentication with JWT tokens');
});
- it("should trim whitespace from description and file path", () => {
- const line = "- [ ] T005: Create API endpoint | File: src/routes/api.ts ";
+ it('should trim whitespace from description and file path', () => {
+ const line = '- [ ] T005: Create API endpoint | File: src/routes/api.ts ';
const result = parseTaskLine(line);
- expect(result?.description).toBe("Create API endpoint");
- expect(result?.filePath).toBe("src/routes/api.ts");
+ expect(result?.description).toBe('Create API endpoint');
+ expect(result?.filePath).toBe('src/routes/api.ts');
});
});
- describe("parseTasksFromSpec", () => {
- it("should parse tasks from a tasks code block", () => {
+ describe('parseTasksFromSpec', () => {
+ it('should parse tasks from a tasks code block', () => {
const specContent = `
## Specification
@@ -157,12 +157,12 @@ Some notes here.
`;
const tasks = parseTasksFromSpec(specContent);
expect(tasks).toHaveLength(3);
- expect(tasks[0].id).toBe("T001");
- expect(tasks[1].id).toBe("T002");
- expect(tasks[2].id).toBe("T003");
+ expect(tasks[0].id).toBe('T001');
+ expect(tasks[1].id).toBe('T002');
+ expect(tasks[2].id).toBe('T003');
});
- it("should parse tasks with phases", () => {
+ it('should parse tasks with phases', () => {
const specContent = `
\`\`\`tasks
## Phase 1: Foundation
@@ -179,20 +179,20 @@ Some notes here.
`;
const tasks = parseTasksFromSpec(specContent);
expect(tasks).toHaveLength(5);
- expect(tasks[0].phase).toBe("Phase 1: Foundation");
- expect(tasks[1].phase).toBe("Phase 1: Foundation");
- expect(tasks[2].phase).toBe("Phase 2: Implementation");
- expect(tasks[3].phase).toBe("Phase 2: Implementation");
- expect(tasks[4].phase).toBe("Phase 3: Testing");
+ expect(tasks[0].phase).toBe('Phase 1: Foundation');
+ expect(tasks[1].phase).toBe('Phase 1: Foundation');
+ expect(tasks[2].phase).toBe('Phase 2: Implementation');
+ expect(tasks[3].phase).toBe('Phase 2: Implementation');
+ expect(tasks[4].phase).toBe('Phase 3: Testing');
});
- it("should return empty array for content without tasks", () => {
- const specContent = "Just some text without any tasks";
+ it('should return empty array for content without tasks', () => {
+ const specContent = 'Just some text without any tasks';
const tasks = parseTasksFromSpec(specContent);
expect(tasks).toEqual([]);
});
- it("should fallback to finding task lines outside code block", () => {
+ it('should fallback to finding task lines outside code block', () => {
const specContent = `
## Implementation Plan
@@ -201,11 +201,11 @@ Some notes here.
`;
const tasks = parseTasksFromSpec(specContent);
expect(tasks).toHaveLength(2);
- expect(tasks[0].id).toBe("T001");
- expect(tasks[1].id).toBe("T002");
+ expect(tasks[0].id).toBe('T001');
+ expect(tasks[1].id).toBe('T002');
});
- it("should handle empty tasks block", () => {
+ it('should handle empty tasks block', () => {
const specContent = `
\`\`\`tasks
\`\`\`
@@ -214,7 +214,7 @@ Some notes here.
expect(tasks).toEqual([]);
});
- it("should handle mixed valid and invalid lines", () => {
+ it('should handle mixed valid and invalid lines', () => {
const specContent = `
\`\`\`tasks
- [ ] T001: Valid task | File: src/valid.ts
@@ -227,7 +227,7 @@ Some other text
expect(tasks).toHaveLength(2);
});
- it("should preserve task order", () => {
+ it('should preserve task order', () => {
const specContent = `
\`\`\`tasks
- [ ] T003: Third
@@ -236,12 +236,12 @@ Some other text
\`\`\`
`;
const tasks = parseTasksFromSpec(specContent);
- expect(tasks[0].id).toBe("T003");
- expect(tasks[1].id).toBe("T001");
- expect(tasks[2].id).toBe("T002");
+ expect(tasks[0].id).toBe('T003');
+ expect(tasks[1].id).toBe('T001');
+ expect(tasks[2].id).toBe('T002');
});
- it("should handle task IDs with different numbers", () => {
+ it('should handle task IDs with different numbers', () => {
const specContent = `
\`\`\`tasks
- [ ] T001: First
@@ -251,14 +251,14 @@ Some other text
`;
const tasks = parseTasksFromSpec(specContent);
expect(tasks).toHaveLength(3);
- expect(tasks[0].id).toBe("T001");
- expect(tasks[1].id).toBe("T010");
- expect(tasks[2].id).toBe("T100");
+ expect(tasks[0].id).toBe('T001');
+ expect(tasks[1].id).toBe('T010');
+ expect(tasks[2].id).toBe('T100');
});
});
- describe("spec content generation patterns", () => {
- it("should match the expected lite mode output format", () => {
+ describe('spec content generation patterns', () => {
+ it('should match the expected lite mode output format', () => {
const liteModeOutput = `
1. **Goal**: Implement user registration
2. **Approach**: Create form component, add validation, connect to API
@@ -271,12 +271,12 @@ Some other text
[PLAN_GENERATED] Planning outline complete.
`;
- expect(liteModeOutput).toContain("[PLAN_GENERATED]");
- expect(liteModeOutput).toContain("Goal");
- expect(liteModeOutput).toContain("Approach");
+ expect(liteModeOutput).toContain('[PLAN_GENERATED]');
+ expect(liteModeOutput).toContain('Goal');
+ expect(liteModeOutput).toContain('Approach');
});
- it("should match the expected spec mode output format", () => {
+ it('should match the expected spec mode output format', () => {
const specModeOutput = `
1. **Problem**: Users cannot register for accounts
@@ -300,12 +300,12 @@ Some other text
[SPEC_GENERATED] Please review the specification above.
`;
- expect(specModeOutput).toContain("[SPEC_GENERATED]");
- expect(specModeOutput).toContain("```tasks");
- expect(specModeOutput).toContain("T001");
+ expect(specModeOutput).toContain('[SPEC_GENERATED]');
+ expect(specModeOutput).toContain('```tasks');
+ expect(specModeOutput).toContain('T001');
});
- it("should match the expected full mode output format", () => {
+ it('should match the expected full mode output format', () => {
const fullModeOutput = `
1. **Problem Statement**: Users need ability to create accounts
@@ -336,10 +336,10 @@ Some other text
[SPEC_GENERATED] Please review the comprehensive specification above.
`;
- expect(fullModeOutput).toContain("Phase 1");
- expect(fullModeOutput).toContain("Phase 2");
- expect(fullModeOutput).toContain("Phase 3");
- expect(fullModeOutput).toContain("[SPEC_GENERATED]");
+ expect(fullModeOutput).toContain('Phase 1');
+ expect(fullModeOutput).toContain('Phase 2');
+ expect(fullModeOutput).toContain('Phase 3');
+ expect(fullModeOutput).toContain('[SPEC_GENERATED]');
});
});
});
diff --git a/apps/server/tests/unit/services/claude-usage-service.test.ts b/apps/server/tests/unit/services/claude-usage-service.test.ts
new file mode 100644
index 00000000..983e5806
--- /dev/null
+++ b/apps/server/tests/unit/services/claude-usage-service.test.ts
@@ -0,0 +1,644 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { ClaudeUsageService } from '@/services/claude-usage-service.js';
+import { spawn } from 'child_process';
+import * as pty from 'node-pty';
+import * as os from 'os';
+
+vi.mock('child_process');
+vi.mock('node-pty');
+vi.mock('os');
+
+describe('claude-usage-service.ts', () => {
+ let service: ClaudeUsageService;
+ let mockSpawnProcess: any;
+ let mockPtyProcess: any;
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ service = new ClaudeUsageService();
+
+ // Mock spawn process for isAvailable and Mac commands
+ mockSpawnProcess = {
+ on: vi.fn(),
+ kill: vi.fn(),
+ stdout: {
+ on: vi.fn(),
+ },
+ stderr: {
+ on: vi.fn(),
+ },
+ };
+
+ // Mock PTY process for Windows
+ mockPtyProcess = {
+ onData: vi.fn(),
+ onExit: vi.fn(),
+ write: vi.fn(),
+ kill: vi.fn(),
+ };
+
+ vi.mocked(spawn).mockReturnValue(mockSpawnProcess as any);
+ vi.mocked(pty.spawn).mockReturnValue(mockPtyProcess);
+ });
+
+ describe('isAvailable', () => {
+ it('should return true when Claude CLI is available', async () => {
+ vi.mocked(os.platform).mockReturnValue('darwin');
+
+ // Simulate successful which/where command
+ mockSpawnProcess.on.mockImplementation((event: string, callback: Function) => {
+ if (event === 'close') {
+ callback(0); // Exit code 0 = found
+ }
+ return mockSpawnProcess;
+ });
+
+ const result = await service.isAvailable();
+
+ expect(result).toBe(true);
+ expect(spawn).toHaveBeenCalledWith('which', ['claude']);
+ });
+
+ it('should return false when Claude CLI is not available', async () => {
+ vi.mocked(os.platform).mockReturnValue('darwin');
+
+ mockSpawnProcess.on.mockImplementation((event: string, callback: Function) => {
+ if (event === 'close') {
+ callback(1); // Exit code 1 = not found
+ }
+ return mockSpawnProcess;
+ });
+
+ const result = await service.isAvailable();
+
+ expect(result).toBe(false);
+ });
+
+ it('should return false on error', async () => {
+ vi.mocked(os.platform).mockReturnValue('darwin');
+
+ mockSpawnProcess.on.mockImplementation((event: string, callback: Function) => {
+ if (event === 'error') {
+ callback(new Error('Command failed'));
+ }
+ return mockSpawnProcess;
+ });
+
+ const result = await service.isAvailable();
+
+ expect(result).toBe(false);
+ });
+
+ it("should use 'where' command on Windows", async () => {
+ vi.mocked(os.platform).mockReturnValue('win32');
+ const windowsService = new ClaudeUsageService(); // Create new service after platform mock
+
+ mockSpawnProcess.on.mockImplementation((event: string, callback: Function) => {
+ if (event === 'close') {
+ callback(0);
+ }
+ return mockSpawnProcess;
+ });
+
+ await windowsService.isAvailable();
+
+ expect(spawn).toHaveBeenCalledWith('where', ['claude']);
+ });
+ });
+
+ describe('stripAnsiCodes', () => {
+ it('should strip ANSI color codes from text', () => {
+ const service = new ClaudeUsageService();
+ const input = '\x1B[31mRed text\x1B[0m Normal text';
+ // @ts-expect-error - accessing private method for testing
+ const result = service.stripAnsiCodes(input);
+
+ expect(result).toBe('Red text Normal text');
+ });
+
+ it('should handle text without ANSI codes', () => {
+ const service = new ClaudeUsageService();
+ const input = 'Plain text';
+ // @ts-expect-error - accessing private method for testing
+ const result = service.stripAnsiCodes(input);
+
+ expect(result).toBe('Plain text');
+ });
+ });
+
+ describe('parseResetTime', () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date('2025-01-15T10:00:00Z'));
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ it('should parse duration format with hours and minutes', () => {
+ const service = new ClaudeUsageService();
+ const text = 'Resets in 2h 15m';
+ // @ts-expect-error - accessing private method for testing
+ const result = service.parseResetTime(text, 'session');
+
+ const expected = new Date('2025-01-15T12:15:00Z');
+ expect(new Date(result)).toEqual(expected);
+ });
+
+ it('should parse duration format with only minutes', () => {
+ const service = new ClaudeUsageService();
+ const text = 'Resets in 30m';
+ // @ts-expect-error - accessing private method for testing
+ const result = service.parseResetTime(text, 'session');
+
+ const expected = new Date('2025-01-15T10:30:00Z');
+ expect(new Date(result)).toEqual(expected);
+ });
+
+ it('should parse simple time format (AM)', () => {
+ const service = new ClaudeUsageService();
+ const text = 'Resets 11am';
+ // @ts-expect-error - accessing private method for testing
+ const result = service.parseResetTime(text, 'session');
+
+ // Should be today at 11am, or tomorrow if already passed
+ const resultDate = new Date(result);
+ expect(resultDate.getHours()).toBe(11);
+ expect(resultDate.getMinutes()).toBe(0);
+ });
+
+ it('should parse simple time format (PM)', () => {
+ const service = new ClaudeUsageService();
+ const text = 'Resets 3pm';
+ // @ts-expect-error - accessing private method for testing
+ const result = service.parseResetTime(text, 'session');
+
+ const resultDate = new Date(result);
+ expect(resultDate.getHours()).toBe(15);
+ expect(resultDate.getMinutes()).toBe(0);
+ });
+
+ it('should parse date format with month, day, and time', () => {
+ const service = new ClaudeUsageService();
+ const text = 'Resets Dec 22 at 8pm';
+ // @ts-expect-error - accessing private method for testing
+ const result = service.parseResetTime(text, 'weekly');
+
+ const resultDate = new Date(result);
+ expect(resultDate.getMonth()).toBe(11); // December = 11
+ expect(resultDate.getDate()).toBe(22);
+ expect(resultDate.getHours()).toBe(20);
+ });
+
+ it('should parse date format with comma separator', () => {
+ const service = new ClaudeUsageService();
+ const text = 'Resets Jan 15, 3:30pm';
+ // @ts-expect-error - accessing private method for testing
+ const result = service.parseResetTime(text, 'weekly');
+
+ const resultDate = new Date(result);
+ expect(resultDate.getMonth()).toBe(0); // January = 0
+ expect(resultDate.getDate()).toBe(15);
+ expect(resultDate.getHours()).toBe(15);
+ expect(resultDate.getMinutes()).toBe(30);
+ });
+
+ it('should handle 12am correctly', () => {
+ const service = new ClaudeUsageService();
+ const text = 'Resets 12am';
+ // @ts-expect-error - accessing private method for testing
+ const result = service.parseResetTime(text, 'session');
+
+ const resultDate = new Date(result);
+ expect(resultDate.getHours()).toBe(0);
+ });
+
+ it('should handle 12pm correctly', () => {
+ const service = new ClaudeUsageService();
+ const text = 'Resets 12pm';
+ // @ts-expect-error - accessing private method for testing
+ const result = service.parseResetTime(text, 'session');
+
+ const resultDate = new Date(result);
+ expect(resultDate.getHours()).toBe(12);
+ });
+
+ it('should return default reset time for unparseable text', () => {
+ const service = new ClaudeUsageService();
+ const text = 'Invalid reset text';
+ // @ts-expect-error - accessing private method for testing
+ const result = service.parseResetTime(text, 'session');
+ // @ts-expect-error - accessing private method for testing
+ const defaultResult = service.getDefaultResetTime('session');
+
+ expect(result).toBe(defaultResult);
+ });
+ });
+
+ describe('getDefaultResetTime', () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date('2025-01-15T10:00:00Z')); // Wednesday
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ it('should return session default (5 hours from now)', () => {
+ const service = new ClaudeUsageService();
+ // @ts-expect-error - accessing private method for testing
+ const result = service.getDefaultResetTime('session');
+
+ const expected = new Date('2025-01-15T15:00:00Z');
+ expect(new Date(result)).toEqual(expected);
+ });
+
+ it('should return weekly default (next Monday at noon)', () => {
+ const service = new ClaudeUsageService();
+ // @ts-expect-error - accessing private method for testing
+ const result = service.getDefaultResetTime('weekly');
+
+ const resultDate = new Date(result);
+ // Next Monday from Wednesday should be 5 days away
+ expect(resultDate.getDay()).toBe(1); // Monday
+ expect(resultDate.getHours()).toBe(12);
+ expect(resultDate.getMinutes()).toBe(59);
+ });
+ });
+
+ describe('parseSection', () => {
+ it('should parse section with percentage left', () => {
+ const service = new ClaudeUsageService();
+ const lines = ['Current session', '████████████████░░░░ 65% left', 'Resets in 2h 15m'];
+ // @ts-expect-error - accessing private method for testing
+ const result = service.parseSection(lines, 'Current session', 'session');
+
+ expect(result.percentage).toBe(35); // 100 - 65 = 35% used
+ expect(result.resetText).toBe('Resets in 2h 15m');
+ });
+
+ it('should parse section with percentage used', () => {
+ const service = new ClaudeUsageService();
+ const lines = [
+ 'Current week (all models)',
+ '██████████░░░░░░░░░░ 40% used',
+ 'Resets Jan 15, 3:30pm',
+ ];
+ // @ts-expect-error - accessing private method for testing
+ const result = service.parseSection(lines, 'Current week (all models)', 'weekly');
+
+ expect(result.percentage).toBe(40); // Already in % used
+ });
+
+ it('should return zero percentage when section not found', () => {
+ const service = new ClaudeUsageService();
+ const lines = ['Some other text', 'No matching section'];
+ // @ts-expect-error - accessing private method for testing
+ const result = service.parseSection(lines, 'Current session', 'session');
+
+ expect(result.percentage).toBe(0);
+ });
+
+ it('should strip timezone from reset text', () => {
+ const service = new ClaudeUsageService();
+ const lines = ['Current session', '65% left', 'Resets 3pm (America/Los_Angeles)'];
+ // @ts-expect-error - accessing private method for testing
+ const result = service.parseSection(lines, 'Current session', 'session');
+
+ expect(result.resetText).toBe('Resets 3pm');
+ expect(result.resetText).not.toContain('America/Los_Angeles');
+ });
+
+ it('should handle case-insensitive section matching', () => {
+ const service = new ClaudeUsageService();
+ const lines = ['CURRENT SESSION', '65% left', 'Resets in 2h'];
+ // @ts-expect-error - accessing private method for testing
+ const result = service.parseSection(lines, 'current session', 'session');
+
+ expect(result.percentage).toBe(35);
+ });
+ });
+
+ describe('parseUsageOutput', () => {
+ it('should parse complete usage output', () => {
+ const service = new ClaudeUsageService();
+ const output = `
+Claude Code v1.0.27
+
+Current session
+████████████████░░░░ 65% left
+Resets in 2h 15m
+
+Current week (all models)
+██████████░░░░░░░░░░ 35% left
+Resets Jan 15, 3:30pm (America/Los_Angeles)
+
+Current week (Sonnet only)
+████████████████████ 80% left
+Resets Jan 15, 3:30pm (America/Los_Angeles)
+`;
+ // @ts-expect-error - accessing private method for testing
+ const result = service.parseUsageOutput(output);
+
+ expect(result.sessionPercentage).toBe(35); // 100 - 65
+ expect(result.weeklyPercentage).toBe(65); // 100 - 35
+ expect(result.sonnetWeeklyPercentage).toBe(20); // 100 - 80
+ expect(result.sessionResetText).toContain('Resets in 2h 15m');
+ expect(result.weeklyResetText).toContain('Resets Jan 15, 3:30pm');
+ expect(result.userTimezone).toBe(Intl.DateTimeFormat().resolvedOptions().timeZone);
+ });
+
+ it('should handle output with ANSI codes', () => {
+ const service = new ClaudeUsageService();
+ const output = `
+\x1B[1mClaude Code v1.0.27\x1B[0m
+
+\x1B[1mCurrent session\x1B[0m
+\x1B[32m████████████████░░░░\x1B[0m 65% left
+Resets in 2h 15m
+`;
+ // @ts-expect-error - accessing private method for testing
+ const result = service.parseUsageOutput(output);
+
+ expect(result.sessionPercentage).toBe(35);
+ });
+
+ it('should handle Opus section name', () => {
+ const service = new ClaudeUsageService();
+ const output = `
+Current session
+65% left
+Resets in 2h
+
+Current week (all models)
+35% left
+Resets Jan 15, 3pm
+
+Current week (Opus)
+90% left
+Resets Jan 15, 3pm
+`;
+ // @ts-expect-error - accessing private method for testing
+ const result = service.parseUsageOutput(output);
+
+ expect(result.sonnetWeeklyPercentage).toBe(10); // 100 - 90
+ });
+
+ it('should set default values for missing sections', () => {
+ const service = new ClaudeUsageService();
+ const output = 'Claude Code v1.0.27';
+ // @ts-expect-error - accessing private method for testing
+ const result = service.parseUsageOutput(output);
+
+ expect(result.sessionPercentage).toBe(0);
+ expect(result.weeklyPercentage).toBe(0);
+ expect(result.sonnetWeeklyPercentage).toBe(0);
+ expect(result.sessionTokensUsed).toBe(0);
+ expect(result.sessionLimit).toBe(0);
+ expect(result.costUsed).toBeNull();
+ expect(result.costLimit).toBeNull();
+ expect(result.costCurrency).toBeNull();
+ });
+ });
+
+ describe('executeClaudeUsageCommandMac', () => {
+ beforeEach(() => {
+ vi.mocked(os.platform).mockReturnValue('darwin');
+ vi.spyOn(process, 'env', 'get').mockReturnValue({ HOME: '/Users/testuser' });
+ });
+
+ it('should execute expect script and return output', async () => {
+ const mockOutput = `
+Current session
+65% left
+Resets in 2h
+`;
+
+ let stdoutCallback: Function;
+ let closeCallback: Function;
+
+ mockSpawnProcess.stdout = {
+ on: vi.fn((event: string, callback: Function) => {
+ if (event === 'data') {
+ stdoutCallback = callback;
+ }
+ }),
+ };
+ mockSpawnProcess.stderr = {
+ on: vi.fn(),
+ };
+ mockSpawnProcess.on = vi.fn((event: string, callback: Function) => {
+ if (event === 'close') {
+ closeCallback = callback;
+ }
+ return mockSpawnProcess;
+ });
+
+ const promise = service.fetchUsageData();
+
+ // Simulate stdout data
+ stdoutCallback!(Buffer.from(mockOutput));
+
+ // Simulate successful close
+ closeCallback!(0);
+
+ const result = await promise;
+
+ expect(result.sessionPercentage).toBe(35); // 100 - 65
+ expect(spawn).toHaveBeenCalledWith(
+ 'expect',
+ expect.arrayContaining(['-c']),
+ expect.any(Object)
+ );
+ });
+
+ it('should handle authentication errors', async () => {
+ const mockOutput = 'token_expired';
+
+ let stdoutCallback: Function;
+ let closeCallback: Function;
+
+ mockSpawnProcess.stdout = {
+ on: vi.fn((event: string, callback: Function) => {
+ if (event === 'data') {
+ stdoutCallback = callback;
+ }
+ }),
+ };
+ mockSpawnProcess.stderr = {
+ on: vi.fn(),
+ };
+ mockSpawnProcess.on = vi.fn((event: string, callback: Function) => {
+ if (event === 'close') {
+ closeCallback = callback;
+ }
+ return mockSpawnProcess;
+ });
+
+ const promise = service.fetchUsageData();
+
+ stdoutCallback!(Buffer.from(mockOutput));
+ closeCallback!(1);
+
+ await expect(promise).rejects.toThrow('Authentication required');
+ });
+
+ it('should handle timeout', async () => {
+ vi.useFakeTimers();
+
+ mockSpawnProcess.stdout = {
+ on: vi.fn(),
+ };
+ mockSpawnProcess.stderr = {
+ on: vi.fn(),
+ };
+ mockSpawnProcess.on = vi.fn(() => mockSpawnProcess);
+ mockSpawnProcess.kill = vi.fn();
+
+ const promise = service.fetchUsageData();
+
+ // Advance time past timeout (30 seconds)
+ vi.advanceTimersByTime(31000);
+
+ await expect(promise).rejects.toThrow('Command timed out');
+
+ vi.useRealTimers();
+ });
+ });
+
+ describe('executeClaudeUsageCommandWindows', () => {
+ beforeEach(() => {
+ vi.mocked(os.platform).mockReturnValue('win32');
+ vi.mocked(os.homedir).mockReturnValue('C:\\Users\\testuser');
+ vi.spyOn(process, 'env', 'get').mockReturnValue({ USERPROFILE: 'C:\\Users\\testuser' });
+ });
+
+ it('should use node-pty on Windows and return output', async () => {
+ const windowsService = new ClaudeUsageService(); // Create new service for Windows platform
+ const mockOutput = `
+Current session
+65% left
+Resets in 2h
+`;
+
+ let dataCallback: Function | undefined;
+ let exitCallback: Function | undefined;
+
+ const mockPty = {
+ onData: vi.fn((callback: Function) => {
+ dataCallback = callback;
+ }),
+ onExit: vi.fn((callback: Function) => {
+ exitCallback = callback;
+ }),
+ write: vi.fn(),
+ kill: vi.fn(),
+ };
+ vi.mocked(pty.spawn).mockReturnValue(mockPty as any);
+
+ const promise = windowsService.fetchUsageData();
+
+ // Simulate data
+ dataCallback!(mockOutput);
+
+ // Simulate successful exit
+ exitCallback!({ exitCode: 0 });
+
+ const result = await promise;
+
+ expect(result.sessionPercentage).toBe(35);
+ expect(pty.spawn).toHaveBeenCalledWith(
+ 'cmd.exe',
+ ['/c', 'claude', '/usage'],
+ expect.any(Object)
+ );
+ });
+
+ it('should send escape key after seeing usage data', async () => {
+ vi.useFakeTimers();
+ const windowsService = new ClaudeUsageService();
+
+ const mockOutput = 'Current session\n65% left';
+
+ let dataCallback: Function | undefined;
+ let exitCallback: Function | undefined;
+
+ const mockPty = {
+ onData: vi.fn((callback: Function) => {
+ dataCallback = callback;
+ }),
+ onExit: vi.fn((callback: Function) => {
+ exitCallback = callback;
+ }),
+ write: vi.fn(),
+ kill: vi.fn(),
+ };
+ vi.mocked(pty.spawn).mockReturnValue(mockPty as any);
+
+ const promise = windowsService.fetchUsageData();
+
+ // Simulate seeing usage data
+ dataCallback!(mockOutput);
+
+ // Advance time to trigger escape key sending
+ vi.advanceTimersByTime(2100);
+
+ expect(mockPty.write).toHaveBeenCalledWith('\x1b');
+
+ // Complete the promise to avoid unhandled rejection
+ exitCallback!({ exitCode: 0 });
+ await promise;
+
+ vi.useRealTimers();
+ });
+
+ it('should handle authentication errors on Windows', async () => {
+ const windowsService = new ClaudeUsageService();
+ let dataCallback: Function | undefined;
+ let exitCallback: Function | undefined;
+
+ const mockPty = {
+ onData: vi.fn((callback: Function) => {
+ dataCallback = callback;
+ }),
+ onExit: vi.fn((callback: Function) => {
+ exitCallback = callback;
+ }),
+ write: vi.fn(),
+ kill: vi.fn(),
+ };
+ vi.mocked(pty.spawn).mockReturnValue(mockPty as any);
+
+ const promise = windowsService.fetchUsageData();
+
+ dataCallback!('authentication_error');
+ exitCallback!({ exitCode: 1 });
+
+ await expect(promise).rejects.toThrow('Authentication required');
+ });
+
+ it('should handle timeout on Windows', async () => {
+ vi.useFakeTimers();
+ const windowsService = new ClaudeUsageService();
+
+ const mockPty = {
+ onData: vi.fn(),
+ onExit: vi.fn(),
+ write: vi.fn(),
+ kill: vi.fn(),
+ };
+ vi.mocked(pty.spawn).mockReturnValue(mockPty as any);
+
+ const promise = windowsService.fetchUsageData();
+
+ vi.advanceTimersByTime(31000);
+
+ await expect(promise).rejects.toThrow('Command timed out');
+ expect(mockPty.kill).toHaveBeenCalled();
+
+ vi.useRealTimers();
+ });
+ });
+});
diff --git a/apps/server/tests/unit/services/feature-loader.test.ts b/apps/server/tests/unit/services/feature-loader.test.ts
index 2a10ddf1..f5f54e81 100644
--- a/apps/server/tests/unit/services/feature-loader.test.ts
+++ b/apps/server/tests/unit/services/feature-loader.test.ts
@@ -1,66 +1,66 @@
-import { describe, it, expect, vi, beforeEach } from "vitest";
-import { FeatureLoader } from "@/services/feature-loader.js";
-import * as fs from "fs/promises";
-import path from "path";
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { FeatureLoader } from '@/services/feature-loader.js';
+import * as fs from 'fs/promises';
+import path from 'path';
-vi.mock("fs/promises");
+vi.mock('fs/promises');
-describe("feature-loader.ts", () => {
+describe('feature-loader.ts', () => {
let loader: FeatureLoader;
- const testProjectPath = "/test/project";
+ const testProjectPath = '/test/project';
beforeEach(() => {
vi.clearAllMocks();
loader = new FeatureLoader();
});
- describe("getFeaturesDir", () => {
- it("should return features directory path", () => {
+ describe('getFeaturesDir', () => {
+ it('should return features directory path', () => {
const result = loader.getFeaturesDir(testProjectPath);
- expect(result).toContain("test");
- expect(result).toContain("project");
- expect(result).toContain(".automaker");
- expect(result).toContain("features");
+ expect(result).toContain('test');
+ expect(result).toContain('project');
+ expect(result).toContain('.automaker');
+ expect(result).toContain('features');
});
});
- describe("getFeatureImagesDir", () => {
- it("should return feature images directory path", () => {
- const result = loader.getFeatureImagesDir(testProjectPath, "feature-123");
- expect(result).toContain("features");
- expect(result).toContain("feature-123");
- expect(result).toContain("images");
+ describe('getFeatureImagesDir', () => {
+ it('should return feature images directory path', () => {
+ const result = loader.getFeatureImagesDir(testProjectPath, 'feature-123');
+ expect(result).toContain('features');
+ expect(result).toContain('feature-123');
+ expect(result).toContain('images');
});
});
- describe("getFeatureDir", () => {
- it("should return feature directory path", () => {
- const result = loader.getFeatureDir(testProjectPath, "feature-123");
- expect(result).toContain("features");
- expect(result).toContain("feature-123");
+ describe('getFeatureDir', () => {
+ it('should return feature directory path', () => {
+ const result = loader.getFeatureDir(testProjectPath, 'feature-123');
+ expect(result).toContain('features');
+ expect(result).toContain('feature-123');
});
});
- describe("getFeatureJsonPath", () => {
- it("should return feature.json path", () => {
- const result = loader.getFeatureJsonPath(testProjectPath, "feature-123");
- expect(result).toContain("features");
- expect(result).toContain("feature-123");
- expect(result).toContain("feature.json");
+ describe('getFeatureJsonPath', () => {
+ it('should return feature.json path', () => {
+ const result = loader.getFeatureJsonPath(testProjectPath, 'feature-123');
+ expect(result).toContain('features');
+ expect(result).toContain('feature-123');
+ expect(result).toContain('feature.json');
});
});
- describe("getAgentOutputPath", () => {
- it("should return agent-output.md path", () => {
- const result = loader.getAgentOutputPath(testProjectPath, "feature-123");
- expect(result).toContain("features");
- expect(result).toContain("feature-123");
- expect(result).toContain("agent-output.md");
+ describe('getAgentOutputPath', () => {
+ it('should return agent-output.md path', () => {
+ const result = loader.getAgentOutputPath(testProjectPath, 'feature-123');
+ expect(result).toContain('features');
+ expect(result).toContain('feature-123');
+ expect(result).toContain('agent-output.md');
});
});
- describe("generateFeatureId", () => {
- it("should generate unique feature ID with timestamp", () => {
+ describe('generateFeatureId', () => {
+ it('should generate unique feature ID with timestamp', () => {
const id1 = loader.generateFeatureId();
const id2 = loader.generateFeatureId();
@@ -75,381 +75,371 @@ describe("feature-loader.ts", () => {
});
});
- describe("getAll", () => {
+ describe('getAll', () => {
it("should return empty array when features directory doesn't exist", async () => {
- vi.mocked(fs.access).mockRejectedValue(new Error("ENOENT"));
+ vi.mocked(fs.access).mockRejectedValue(new Error('ENOENT'));
const result = await loader.getAll(testProjectPath);
expect(result).toEqual([]);
});
- it("should load all features from feature directories", async () => {
+ it('should load all features from feature directories', async () => {
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.readdir).mockResolvedValue([
- { name: "feature-1", isDirectory: () => true } as any,
- { name: "feature-2", isDirectory: () => true } as any,
- { name: "file.txt", isDirectory: () => false } as any,
+ { name: 'feature-1', isDirectory: () => true } as any,
+ { name: 'feature-2', isDirectory: () => true } as any,
+ { name: 'file.txt', isDirectory: () => false } as any,
]);
vi.mocked(fs.readFile)
.mockResolvedValueOnce(
JSON.stringify({
- id: "feature-1",
- category: "ui",
- description: "Feature 1",
+ id: 'feature-1',
+ category: 'ui',
+ description: 'Feature 1',
})
)
.mockResolvedValueOnce(
JSON.stringify({
- id: "feature-2",
- category: "backend",
- description: "Feature 2",
+ id: 'feature-2',
+ category: 'backend',
+ description: 'Feature 2',
})
);
const result = await loader.getAll(testProjectPath);
expect(result).toHaveLength(2);
- expect(result[0].id).toBe("feature-1");
- expect(result[1].id).toBe("feature-2");
+ expect(result[0].id).toBe('feature-1');
+ expect(result[1].id).toBe('feature-2');
});
- it("should skip features without id field", async () => {
+ it('should skip features without id field', async () => {
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.readdir).mockResolvedValue([
- { name: "feature-1", isDirectory: () => true } as any,
- { name: "feature-2", isDirectory: () => true } as any,
+ { name: 'feature-1', isDirectory: () => true } as any,
+ { name: 'feature-2', isDirectory: () => true } as any,
]);
- const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
vi.mocked(fs.readFile)
.mockResolvedValueOnce(
JSON.stringify({
- category: "ui",
- description: "Missing ID",
+ category: 'ui',
+ description: 'Missing ID',
})
)
.mockResolvedValueOnce(
JSON.stringify({
- id: "feature-2",
- category: "backend",
- description: "Feature 2",
+ id: 'feature-2',
+ category: 'backend',
+ description: 'Feature 2',
})
);
const result = await loader.getAll(testProjectPath);
expect(result).toHaveLength(1);
- expect(result[0].id).toBe("feature-2");
+ expect(result[0].id).toBe('feature-2');
expect(consoleSpy).toHaveBeenCalledWith(
- "[FeatureLoader]",
+ '[FeatureLoader]',
expect.stringContaining("missing required 'id' field")
);
consoleSpy.mockRestore();
});
- it("should skip features with missing feature.json", async () => {
+ it('should skip features with missing feature.json', async () => {
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.readdir).mockResolvedValue([
- { name: "feature-1", isDirectory: () => true } as any,
- { name: "feature-2", isDirectory: () => true } as any,
+ { name: 'feature-1', isDirectory: () => true } as any,
+ { name: 'feature-2', isDirectory: () => true } as any,
]);
- const error: any = new Error("File not found");
- error.code = "ENOENT";
+ const error: any = new Error('File not found');
+ error.code = 'ENOENT';
vi.mocked(fs.readFile)
.mockRejectedValueOnce(error)
.mockResolvedValueOnce(
JSON.stringify({
- id: "feature-2",
- category: "backend",
- description: "Feature 2",
+ id: 'feature-2',
+ category: 'backend',
+ description: 'Feature 2',
})
);
const result = await loader.getAll(testProjectPath);
expect(result).toHaveLength(1);
- expect(result[0].id).toBe("feature-2");
+ expect(result[0].id).toBe('feature-2');
});
- it("should handle malformed JSON gracefully", async () => {
+ it('should handle malformed JSON gracefully', async () => {
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.readdir).mockResolvedValue([
- { name: "feature-1", isDirectory: () => true } as any,
+ { name: 'feature-1', isDirectory: () => true } as any,
]);
- const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
- vi.mocked(fs.readFile).mockResolvedValue("invalid json{");
+ vi.mocked(fs.readFile).mockResolvedValue('invalid json{');
const result = await loader.getAll(testProjectPath);
expect(result).toEqual([]);
expect(consoleSpy).toHaveBeenCalledWith(
- "[FeatureLoader]",
- expect.stringContaining("Failed to parse feature.json")
+ '[FeatureLoader]',
+ expect.stringContaining('Failed to parse feature.json')
);
consoleSpy.mockRestore();
});
- it("should sort features by creation order (timestamp)", async () => {
+ it('should sort features by creation order (timestamp)', async () => {
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.readdir).mockResolvedValue([
- { name: "feature-3", isDirectory: () => true } as any,
- { name: "feature-1", isDirectory: () => true } as any,
- { name: "feature-2", isDirectory: () => true } as any,
+ { name: 'feature-3', isDirectory: () => true } as any,
+ { name: 'feature-1', isDirectory: () => true } as any,
+ { name: 'feature-2', isDirectory: () => true } as any,
]);
vi.mocked(fs.readFile)
.mockResolvedValueOnce(
JSON.stringify({
- id: "feature-3000-xyz",
- category: "ui",
+ id: 'feature-3000-xyz',
+ category: 'ui',
})
)
.mockResolvedValueOnce(
JSON.stringify({
- id: "feature-1000-abc",
- category: "ui",
+ id: 'feature-1000-abc',
+ category: 'ui',
})
)
.mockResolvedValueOnce(
JSON.stringify({
- id: "feature-2000-def",
- category: "ui",
+ id: 'feature-2000-def',
+ category: 'ui',
})
);
const result = await loader.getAll(testProjectPath);
expect(result).toHaveLength(3);
- expect(result[0].id).toBe("feature-1000-abc");
- expect(result[1].id).toBe("feature-2000-def");
- expect(result[2].id).toBe("feature-3000-xyz");
+ expect(result[0].id).toBe('feature-1000-abc');
+ expect(result[1].id).toBe('feature-2000-def');
+ expect(result[2].id).toBe('feature-3000-xyz');
});
});
- describe("get", () => {
- it("should return feature by ID", async () => {
+ describe('get', () => {
+ it('should return feature by ID', async () => {
const featureData = {
- id: "feature-123",
- category: "ui",
- description: "Test feature",
+ id: 'feature-123',
+ category: 'ui',
+ description: 'Test feature',
};
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(featureData));
- const result = await loader.get(testProjectPath, "feature-123");
+ const result = await loader.get(testProjectPath, 'feature-123');
expect(result).toEqual(featureData);
});
it("should return null when feature doesn't exist", async () => {
- const error: any = new Error("File not found");
- error.code = "ENOENT";
+ const error: any = new Error('File not found');
+ error.code = 'ENOENT';
vi.mocked(fs.readFile).mockRejectedValue(error);
- const result = await loader.get(testProjectPath, "feature-123");
+ const result = await loader.get(testProjectPath, 'feature-123');
expect(result).toBeNull();
});
- it("should throw on other errors", async () => {
- vi.mocked(fs.readFile).mockRejectedValue(new Error("Permission denied"));
+ it('should throw on other errors', async () => {
+ vi.mocked(fs.readFile).mockRejectedValue(new Error('Permission denied'));
- await expect(
- loader.get(testProjectPath, "feature-123")
- ).rejects.toThrow("Permission denied");
+ await expect(loader.get(testProjectPath, 'feature-123')).rejects.toThrow('Permission denied');
});
});
- describe("create", () => {
- it("should create new feature", async () => {
+ describe('create', () => {
+ it('should create new feature', async () => {
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
const featureData = {
- category: "ui",
- description: "New feature",
+ category: 'ui',
+ description: 'New feature',
};
const result = await loader.create(testProjectPath, featureData);
expect(result).toMatchObject({
- category: "ui",
- description: "New feature",
+ category: 'ui',
+ description: 'New feature',
id: expect.stringMatching(/^feature-/),
});
expect(fs.writeFile).toHaveBeenCalled();
});
- it("should use provided ID if given", async () => {
+ it('should use provided ID if given', async () => {
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
const result = await loader.create(testProjectPath, {
- id: "custom-id",
- category: "ui",
- description: "Test",
+ id: 'custom-id',
+ category: 'ui',
+ description: 'Test',
});
- expect(result.id).toBe("custom-id");
+ expect(result.id).toBe('custom-id');
});
- it("should set default category if not provided", async () => {
+ it('should set default category if not provided', async () => {
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
const result = await loader.create(testProjectPath, {
- description: "Test",
+ description: 'Test',
});
- expect(result.category).toBe("Uncategorized");
+ expect(result.category).toBe('Uncategorized');
});
});
- describe("update", () => {
- it("should update existing feature", async () => {
+ describe('update', () => {
+ it('should update existing feature', async () => {
vi.mocked(fs.readFile).mockResolvedValue(
JSON.stringify({
- id: "feature-123",
- category: "ui",
- description: "Old description",
+ id: 'feature-123',
+ category: 'ui',
+ description: 'Old description',
})
);
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
- const result = await loader.update(testProjectPath, "feature-123", {
- description: "New description",
+ const result = await loader.update(testProjectPath, 'feature-123', {
+ description: 'New description',
});
- expect(result.description).toBe("New description");
- expect(result.category).toBe("ui");
+ expect(result.description).toBe('New description');
+ expect(result.category).toBe('ui');
expect(fs.writeFile).toHaveBeenCalled();
});
it("should throw if feature doesn't exist", async () => {
- const error: any = new Error("File not found");
- error.code = "ENOENT";
+ const error: any = new Error('File not found');
+ error.code = 'ENOENT';
vi.mocked(fs.readFile).mockRejectedValue(error);
- await expect(
- loader.update(testProjectPath, "feature-123", {})
- ).rejects.toThrow("not found");
+ await expect(loader.update(testProjectPath, 'feature-123', {})).rejects.toThrow('not found');
});
});
- describe("delete", () => {
- it("should delete feature directory", async () => {
+ describe('delete', () => {
+ it('should delete feature directory', async () => {
vi.mocked(fs.rm).mockResolvedValue(undefined);
- const result = await loader.delete(testProjectPath, "feature-123");
+ const result = await loader.delete(testProjectPath, 'feature-123');
expect(result).toBe(true);
- expect(fs.rm).toHaveBeenCalledWith(
- expect.stringContaining("feature-123"),
- { recursive: true, force: true }
- );
+ expect(fs.rm).toHaveBeenCalledWith(expect.stringContaining('feature-123'), {
+ recursive: true,
+ force: true,
+ });
});
- it("should return false on error", async () => {
- vi.mocked(fs.rm).mockRejectedValue(new Error("Permission denied"));
+ it('should return false on error', async () => {
+ vi.mocked(fs.rm).mockRejectedValue(new Error('Permission denied'));
- const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
- const result = await loader.delete(testProjectPath, "feature-123");
+ const result = await loader.delete(testProjectPath, 'feature-123');
expect(result).toBe(false);
expect(consoleSpy).toHaveBeenCalledWith(
- "[FeatureLoader]",
- expect.stringContaining("Failed to delete feature"),
- expect.objectContaining({ message: "Permission denied" })
+ '[FeatureLoader]',
+ expect.stringContaining('Failed to delete feature'),
+ expect.objectContaining({ message: 'Permission denied' })
);
consoleSpy.mockRestore();
});
});
- describe("getAgentOutput", () => {
- it("should return agent output content", async () => {
- vi.mocked(fs.readFile).mockResolvedValue("Agent output content");
+ describe('getAgentOutput', () => {
+ it('should return agent output content', async () => {
+ vi.mocked(fs.readFile).mockResolvedValue('Agent output content');
- const result = await loader.getAgentOutput(testProjectPath, "feature-123");
+ const result = await loader.getAgentOutput(testProjectPath, 'feature-123');
- expect(result).toBe("Agent output content");
+ expect(result).toBe('Agent output content');
});
it("should return null when file doesn't exist", async () => {
- const error: any = new Error("File not found");
- error.code = "ENOENT";
+ const error: any = new Error('File not found');
+ error.code = 'ENOENT';
vi.mocked(fs.readFile).mockRejectedValue(error);
- const result = await loader.getAgentOutput(testProjectPath, "feature-123");
+ const result = await loader.getAgentOutput(testProjectPath, 'feature-123');
expect(result).toBeNull();
});
- it("should throw on other errors", async () => {
- vi.mocked(fs.readFile).mockRejectedValue(new Error("Permission denied"));
+ it('should throw on other errors', async () => {
+ vi.mocked(fs.readFile).mockRejectedValue(new Error('Permission denied'));
- await expect(
- loader.getAgentOutput(testProjectPath, "feature-123")
- ).rejects.toThrow("Permission denied");
+ await expect(loader.getAgentOutput(testProjectPath, 'feature-123')).rejects.toThrow(
+ 'Permission denied'
+ );
});
});
- describe("saveAgentOutput", () => {
- it("should save agent output to file", async () => {
+ describe('saveAgentOutput', () => {
+ it('should save agent output to file', async () => {
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
- await loader.saveAgentOutput(
- testProjectPath,
- "feature-123",
- "Output content"
- );
+ await loader.saveAgentOutput(testProjectPath, 'feature-123', 'Output content');
expect(fs.writeFile).toHaveBeenCalledWith(
- expect.stringContaining("agent-output.md"),
- "Output content",
- "utf-8"
+ expect.stringContaining('agent-output.md'),
+ 'Output content',
+ 'utf-8'
);
});
});
- describe("deleteAgentOutput", () => {
- it("should delete agent output file", async () => {
+ describe('deleteAgentOutput', () => {
+ it('should delete agent output file', async () => {
vi.mocked(fs.unlink).mockResolvedValue(undefined);
- await loader.deleteAgentOutput(testProjectPath, "feature-123");
+ await loader.deleteAgentOutput(testProjectPath, 'feature-123');
- expect(fs.unlink).toHaveBeenCalledWith(
- expect.stringContaining("agent-output.md")
- );
+ expect(fs.unlink).toHaveBeenCalledWith(expect.stringContaining('agent-output.md'));
});
- it("should handle missing file gracefully", async () => {
- const error: any = new Error("File not found");
- error.code = "ENOENT";
+ it('should handle missing file gracefully', async () => {
+ const error: any = new Error('File not found');
+ error.code = 'ENOENT';
vi.mocked(fs.unlink).mockRejectedValue(error);
// Should not throw
await expect(
- loader.deleteAgentOutput(testProjectPath, "feature-123")
+ loader.deleteAgentOutput(testProjectPath, 'feature-123')
).resolves.toBeUndefined();
});
- it("should throw on other errors", async () => {
- vi.mocked(fs.unlink).mockRejectedValue(new Error("Permission denied"));
+ it('should throw on other errors', async () => {
+ vi.mocked(fs.unlink).mockRejectedValue(new Error('Permission denied'));
- await expect(
- loader.deleteAgentOutput(testProjectPath, "feature-123")
- ).rejects.toThrow("Permission denied");
+ await expect(loader.deleteAgentOutput(testProjectPath, 'feature-123')).rejects.toThrow(
+ 'Permission denied'
+ );
});
});
});
diff --git a/apps/server/tests/unit/services/settings-service.test.ts b/apps/server/tests/unit/services/settings-service.test.ts
index ecde0fb9..235387bf 100644
--- a/apps/server/tests/unit/services/settings-service.test.ts
+++ b/apps/server/tests/unit/services/settings-service.test.ts
@@ -1,8 +1,8 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
-import fs from "fs/promises";
-import path from "path";
-import os from "os";
-import { SettingsService } from "@/services/settings-service.js";
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import fs from 'fs/promises';
+import path from 'path';
+import os from 'os';
+import { SettingsService } from '@/services/settings-service.js';
import {
DEFAULT_GLOBAL_SETTINGS,
DEFAULT_CREDENTIALS,
@@ -13,9 +13,9 @@ import {
type GlobalSettings,
type Credentials,
type ProjectSettings,
-} from "@/types/settings.js";
+} from '@/types/settings.js';
-describe("settings-service.ts", () => {
+describe('settings-service.ts', () => {
let testDataDir: string;
let testProjectDir: string;
let settingsService: SettingsService;
@@ -37,120 +37,118 @@ describe("settings-service.ts", () => {
}
});
- describe("getGlobalSettings", () => {
- it("should return default settings when file does not exist", async () => {
+ describe('getGlobalSettings', () => {
+ it('should return default settings when file does not exist', async () => {
const settings = await settingsService.getGlobalSettings();
expect(settings).toEqual(DEFAULT_GLOBAL_SETTINGS);
});
- it("should read and return existing settings", async () => {
+ it('should read and return existing settings', async () => {
const customSettings: GlobalSettings = {
...DEFAULT_GLOBAL_SETTINGS,
- theme: "light",
+ theme: 'light',
sidebarOpen: false,
maxConcurrency: 5,
};
- const settingsPath = path.join(testDataDir, "settings.json");
+ const settingsPath = path.join(testDataDir, 'settings.json');
await fs.writeFile(settingsPath, JSON.stringify(customSettings, null, 2));
const settings = await settingsService.getGlobalSettings();
- expect(settings.theme).toBe("light");
+ expect(settings.theme).toBe('light');
expect(settings.sidebarOpen).toBe(false);
expect(settings.maxConcurrency).toBe(5);
});
- it("should merge with defaults for missing properties", async () => {
+ it('should merge with defaults for missing properties', async () => {
const partialSettings = {
version: SETTINGS_VERSION,
- theme: "dark",
+ theme: 'dark',
};
- const settingsPath = path.join(testDataDir, "settings.json");
+ const settingsPath = path.join(testDataDir, 'settings.json');
await fs.writeFile(settingsPath, JSON.stringify(partialSettings, null, 2));
const settings = await settingsService.getGlobalSettings();
- expect(settings.theme).toBe("dark");
+ expect(settings.theme).toBe('dark');
expect(settings.sidebarOpen).toBe(DEFAULT_GLOBAL_SETTINGS.sidebarOpen);
expect(settings.maxConcurrency).toBe(DEFAULT_GLOBAL_SETTINGS.maxConcurrency);
});
- it("should merge keyboard shortcuts deeply", async () => {
+ it('should merge keyboard shortcuts deeply', async () => {
const customSettings: GlobalSettings = {
...DEFAULT_GLOBAL_SETTINGS,
keyboardShortcuts: {
...DEFAULT_GLOBAL_SETTINGS.keyboardShortcuts,
- board: "B",
+ board: 'B',
},
};
- const settingsPath = path.join(testDataDir, "settings.json");
+ const settingsPath = path.join(testDataDir, 'settings.json');
await fs.writeFile(settingsPath, JSON.stringify(customSettings, null, 2));
const settings = await settingsService.getGlobalSettings();
- expect(settings.keyboardShortcuts.board).toBe("B");
+ expect(settings.keyboardShortcuts.board).toBe('B');
expect(settings.keyboardShortcuts.agent).toBe(
DEFAULT_GLOBAL_SETTINGS.keyboardShortcuts.agent
);
});
});
- describe("updateGlobalSettings", () => {
- it("should create settings file with updates", async () => {
+ describe('updateGlobalSettings', () => {
+ it('should create settings file with updates', async () => {
const updates: Partial = {
- theme: "light",
+ theme: 'light',
sidebarOpen: false,
};
const updated = await settingsService.updateGlobalSettings(updates);
- expect(updated.theme).toBe("light");
+ expect(updated.theme).toBe('light');
expect(updated.sidebarOpen).toBe(false);
expect(updated.version).toBe(SETTINGS_VERSION);
- const settingsPath = path.join(testDataDir, "settings.json");
- const fileContent = await fs.readFile(settingsPath, "utf-8");
+ const settingsPath = path.join(testDataDir, 'settings.json');
+ const fileContent = await fs.readFile(settingsPath, 'utf-8');
const saved = JSON.parse(fileContent);
- expect(saved.theme).toBe("light");
+ expect(saved.theme).toBe('light');
expect(saved.sidebarOpen).toBe(false);
});
- it("should merge updates with existing settings", async () => {
+ it('should merge updates with existing settings', async () => {
const initial: GlobalSettings = {
...DEFAULT_GLOBAL_SETTINGS,
- theme: "dark",
+ theme: 'dark',
maxConcurrency: 3,
};
- const settingsPath = path.join(testDataDir, "settings.json");
+ const settingsPath = path.join(testDataDir, 'settings.json');
await fs.writeFile(settingsPath, JSON.stringify(initial, null, 2));
const updates: Partial = {
- theme: "light",
+ theme: 'light',
};
const updated = await settingsService.updateGlobalSettings(updates);
- expect(updated.theme).toBe("light");
+ expect(updated.theme).toBe('light');
expect(updated.maxConcurrency).toBe(3); // Preserved from initial
});
- it("should deep merge keyboard shortcuts", async () => {
+ it('should deep merge keyboard shortcuts', async () => {
const updates: Partial = {
keyboardShortcuts: {
- board: "B",
+ board: 'B',
},
};
const updated = await settingsService.updateGlobalSettings(updates);
- expect(updated.keyboardShortcuts.board).toBe("B");
- expect(updated.keyboardShortcuts.agent).toBe(
- DEFAULT_GLOBAL_SETTINGS.keyboardShortcuts.agent
- );
+ expect(updated.keyboardShortcuts.board).toBe('B');
+ expect(updated.keyboardShortcuts.agent).toBe(DEFAULT_GLOBAL_SETTINGS.keyboardShortcuts.agent);
});
- it("should create data directory if it does not exist", async () => {
+ it('should create data directory if it does not exist', async () => {
const newDataDir = path.join(os.tmpdir(), `new-data-dir-${Date.now()}`);
const newService = new SettingsService(newDataDir);
- await newService.updateGlobalSettings({ theme: "light" });
+ await newService.updateGlobalSettings({ theme: 'light' });
const stats = await fs.stat(newDataDir);
expect(stats.isDirectory()).toBe(true);
@@ -159,248 +157,248 @@ describe("settings-service.ts", () => {
});
});
- describe("hasGlobalSettings", () => {
- it("should return false when settings file does not exist", async () => {
+ describe('hasGlobalSettings', () => {
+ it('should return false when settings file does not exist', async () => {
const exists = await settingsService.hasGlobalSettings();
expect(exists).toBe(false);
});
- it("should return true when settings file exists", async () => {
- await settingsService.updateGlobalSettings({ theme: "light" });
+ it('should return true when settings file exists', async () => {
+ await settingsService.updateGlobalSettings({ theme: 'light' });
const exists = await settingsService.hasGlobalSettings();
expect(exists).toBe(true);
});
});
- describe("getCredentials", () => {
- it("should return default credentials when file does not exist", async () => {
+ describe('getCredentials', () => {
+ it('should return default credentials when file does not exist', async () => {
const credentials = await settingsService.getCredentials();
expect(credentials).toEqual(DEFAULT_CREDENTIALS);
});
- it("should read and return existing credentials", async () => {
+ it('should read and return existing credentials', async () => {
const customCredentials: Credentials = {
...DEFAULT_CREDENTIALS,
apiKeys: {
- anthropic: "sk-test-key",
+ anthropic: 'sk-test-key',
},
};
- const credentialsPath = path.join(testDataDir, "credentials.json");
+ const credentialsPath = path.join(testDataDir, 'credentials.json');
await fs.writeFile(credentialsPath, JSON.stringify(customCredentials, null, 2));
const credentials = await settingsService.getCredentials();
- expect(credentials.apiKeys.anthropic).toBe("sk-test-key");
+ expect(credentials.apiKeys.anthropic).toBe('sk-test-key');
});
- it("should merge with defaults for missing api keys", async () => {
+ it('should merge with defaults for missing api keys', async () => {
const partialCredentials = {
version: CREDENTIALS_VERSION,
apiKeys: {
- anthropic: "sk-test",
+ anthropic: 'sk-test',
},
};
- const credentialsPath = path.join(testDataDir, "credentials.json");
+ const credentialsPath = path.join(testDataDir, 'credentials.json');
await fs.writeFile(credentialsPath, JSON.stringify(partialCredentials, null, 2));
const credentials = await settingsService.getCredentials();
- expect(credentials.apiKeys.anthropic).toBe("sk-test");
+ expect(credentials.apiKeys.anthropic).toBe('sk-test');
});
});
- describe("updateCredentials", () => {
- it("should create credentials file with updates", async () => {
+ describe('updateCredentials', () => {
+ it('should create credentials file with updates', async () => {
const updates: Partial = {
apiKeys: {
- anthropic: "sk-test-key",
+ anthropic: 'sk-test-key',
},
};
const updated = await settingsService.updateCredentials(updates);
- expect(updated.apiKeys.anthropic).toBe("sk-test-key");
+ expect(updated.apiKeys.anthropic).toBe('sk-test-key');
expect(updated.version).toBe(CREDENTIALS_VERSION);
- const credentialsPath = path.join(testDataDir, "credentials.json");
- const fileContent = await fs.readFile(credentialsPath, "utf-8");
+ const credentialsPath = path.join(testDataDir, 'credentials.json');
+ const fileContent = await fs.readFile(credentialsPath, 'utf-8');
const saved = JSON.parse(fileContent);
- expect(saved.apiKeys.anthropic).toBe("sk-test-key");
+ expect(saved.apiKeys.anthropic).toBe('sk-test-key');
});
- it("should merge updates with existing credentials", async () => {
+ it('should merge updates with existing credentials', async () => {
const initial: Credentials = {
...DEFAULT_CREDENTIALS,
apiKeys: {
- anthropic: "sk-initial",
+ anthropic: 'sk-initial',
},
};
- const credentialsPath = path.join(testDataDir, "credentials.json");
+ const credentialsPath = path.join(testDataDir, 'credentials.json');
await fs.writeFile(credentialsPath, JSON.stringify(initial, null, 2));
const updates: Partial = {
apiKeys: {
- anthropic: "sk-updated",
+ anthropic: 'sk-updated',
},
};
const updated = await settingsService.updateCredentials(updates);
- expect(updated.apiKeys.anthropic).toBe("sk-updated");
+ expect(updated.apiKeys.anthropic).toBe('sk-updated');
});
- it("should deep merge api keys", async () => {
+ it('should deep merge api keys', async () => {
const initial: Credentials = {
...DEFAULT_CREDENTIALS,
apiKeys: {
- anthropic: "sk-anthropic",
+ anthropic: 'sk-anthropic',
},
};
- const credentialsPath = path.join(testDataDir, "credentials.json");
+ const credentialsPath = path.join(testDataDir, 'credentials.json');
await fs.writeFile(credentialsPath, JSON.stringify(initial, null, 2));
const updates: Partial = {
apiKeys: {
- anthropic: "sk-updated-anthropic",
+ anthropic: 'sk-updated-anthropic',
},
};
const updated = await settingsService.updateCredentials(updates);
- expect(updated.apiKeys.anthropic).toBe("sk-updated-anthropic");
+ expect(updated.apiKeys.anthropic).toBe('sk-updated-anthropic');
});
});
- describe("getMaskedCredentials", () => {
- it("should return masked credentials for empty keys", async () => {
+ describe('getMaskedCredentials', () => {
+ it('should return masked credentials for empty keys', async () => {
const masked = await settingsService.getMaskedCredentials();
expect(masked.anthropic.configured).toBe(false);
- expect(masked.anthropic.masked).toBe("");
+ expect(masked.anthropic.masked).toBe('');
});
- it("should mask keys correctly", async () => {
+ it('should mask keys correctly', async () => {
await settingsService.updateCredentials({
apiKeys: {
- anthropic: "sk-ant-api03-1234567890abcdef",
+ anthropic: 'sk-ant-api03-1234567890abcdef',
},
});
const masked = await settingsService.getMaskedCredentials();
expect(masked.anthropic.configured).toBe(true);
- expect(masked.anthropic.masked).toBe("sk-a...cdef");
+ expect(masked.anthropic.masked).toBe('sk-a...cdef');
});
- it("should handle short keys", async () => {
+ it('should handle short keys', async () => {
await settingsService.updateCredentials({
apiKeys: {
- anthropic: "short",
+ anthropic: 'short',
},
});
const masked = await settingsService.getMaskedCredentials();
expect(masked.anthropic.configured).toBe(true);
- expect(masked.anthropic.masked).toBe("");
+ expect(masked.anthropic.masked).toBe('');
});
});
- describe("hasCredentials", () => {
- it("should return false when credentials file does not exist", async () => {
+ describe('hasCredentials', () => {
+ it('should return false when credentials file does not exist', async () => {
const exists = await settingsService.hasCredentials();
expect(exists).toBe(false);
});
- it("should return true when credentials file exists", async () => {
+ it('should return true when credentials file exists', async () => {
await settingsService.updateCredentials({
- apiKeys: { anthropic: "test" },
+ apiKeys: { anthropic: 'test' },
});
const exists = await settingsService.hasCredentials();
expect(exists).toBe(true);
});
});
- describe("getProjectSettings", () => {
- it("should return default settings when file does not exist", async () => {
+ describe('getProjectSettings', () => {
+ it('should return default settings when file does not exist', async () => {
const settings = await settingsService.getProjectSettings(testProjectDir);
expect(settings).toEqual(DEFAULT_PROJECT_SETTINGS);
});
- it("should read and return existing project settings", async () => {
+ it('should read and return existing project settings', async () => {
const customSettings: ProjectSettings = {
...DEFAULT_PROJECT_SETTINGS,
- theme: "light",
+ theme: 'light',
useWorktrees: true,
};
- const automakerDir = path.join(testProjectDir, ".automaker");
+ const automakerDir = path.join(testProjectDir, '.automaker');
await fs.mkdir(automakerDir, { recursive: true });
- const settingsPath = path.join(automakerDir, "settings.json");
+ const settingsPath = path.join(automakerDir, 'settings.json');
await fs.writeFile(settingsPath, JSON.stringify(customSettings, null, 2));
const settings = await settingsService.getProjectSettings(testProjectDir);
- expect(settings.theme).toBe("light");
+ expect(settings.theme).toBe('light');
expect(settings.useWorktrees).toBe(true);
});
- it("should merge with defaults for missing properties", async () => {
+ it('should merge with defaults for missing properties', async () => {
const partialSettings = {
version: PROJECT_SETTINGS_VERSION,
- theme: "dark",
+ theme: 'dark',
};
- const automakerDir = path.join(testProjectDir, ".automaker");
+ const automakerDir = path.join(testProjectDir, '.automaker');
await fs.mkdir(automakerDir, { recursive: true });
- const settingsPath = path.join(automakerDir, "settings.json");
+ const settingsPath = path.join(automakerDir, 'settings.json');
await fs.writeFile(settingsPath, JSON.stringify(partialSettings, null, 2));
const settings = await settingsService.getProjectSettings(testProjectDir);
- expect(settings.theme).toBe("dark");
+ expect(settings.theme).toBe('dark');
expect(settings.version).toBe(PROJECT_SETTINGS_VERSION);
});
});
- describe("updateProjectSettings", () => {
- it("should create project settings file with updates", async () => {
+ describe('updateProjectSettings', () => {
+ it('should create project settings file with updates', async () => {
const updates: Partial = {
- theme: "light",
+ theme: 'light',
useWorktrees: true,
};
const updated = await settingsService.updateProjectSettings(testProjectDir, updates);
- expect(updated.theme).toBe("light");
+ expect(updated.theme).toBe('light');
expect(updated.useWorktrees).toBe(true);
expect(updated.version).toBe(PROJECT_SETTINGS_VERSION);
- const automakerDir = path.join(testProjectDir, ".automaker");
- const settingsPath = path.join(automakerDir, "settings.json");
- const fileContent = await fs.readFile(settingsPath, "utf-8");
+ const automakerDir = path.join(testProjectDir, '.automaker');
+ const settingsPath = path.join(automakerDir, 'settings.json');
+ const fileContent = await fs.readFile(settingsPath, 'utf-8');
const saved = JSON.parse(fileContent);
- expect(saved.theme).toBe("light");
+ expect(saved.theme).toBe('light');
expect(saved.useWorktrees).toBe(true);
});
- it("should merge updates with existing project settings", async () => {
+ it('should merge updates with existing project settings', async () => {
const initial: ProjectSettings = {
...DEFAULT_PROJECT_SETTINGS,
- theme: "dark",
+ theme: 'dark',
useWorktrees: false,
};
- const automakerDir = path.join(testProjectDir, ".automaker");
+ const automakerDir = path.join(testProjectDir, '.automaker');
await fs.mkdir(automakerDir, { recursive: true });
- const settingsPath = path.join(automakerDir, "settings.json");
+ const settingsPath = path.join(automakerDir, 'settings.json');
await fs.writeFile(settingsPath, JSON.stringify(initial, null, 2));
const updates: Partial = {
- theme: "light",
+ theme: 'light',
};
const updated = await settingsService.updateProjectSettings(testProjectDir, updates);
- expect(updated.theme).toBe("light");
+ expect(updated.theme).toBe('light');
expect(updated.useWorktrees).toBe(false); // Preserved
});
- it("should deep merge board background", async () => {
+ it('should deep merge board background', async () => {
const initial: ProjectSettings = {
...DEFAULT_PROJECT_SETTINGS,
boardBackground: {
- imagePath: "/path/to/image.jpg",
+ imagePath: '/path/to/image.jpg',
cardOpacity: 0.8,
columnOpacity: 0.9,
columnBorderEnabled: true,
@@ -410,9 +408,9 @@ describe("settings-service.ts", () => {
hideScrollbar: false,
},
};
- const automakerDir = path.join(testProjectDir, ".automaker");
+ const automakerDir = path.join(testProjectDir, '.automaker');
await fs.mkdir(automakerDir, { recursive: true });
- const settingsPath = path.join(automakerDir, "settings.json");
+ const settingsPath = path.join(automakerDir, 'settings.json');
await fs.writeFile(settingsPath, JSON.stringify(initial, null, 2));
const updates: Partial = {
@@ -423,17 +421,17 @@ describe("settings-service.ts", () => {
const updated = await settingsService.updateProjectSettings(testProjectDir, updates);
- expect(updated.boardBackground?.imagePath).toBe("/path/to/image.jpg");
+ expect(updated.boardBackground?.imagePath).toBe('/path/to/image.jpg');
expect(updated.boardBackground?.cardOpacity).toBe(0.9);
expect(updated.boardBackground?.columnOpacity).toBe(0.9);
});
- it("should create .automaker directory if it does not exist", async () => {
+ it('should create .automaker directory if it does not exist', async () => {
const newProjectDir = path.join(os.tmpdir(), `new-project-${Date.now()}`);
- await settingsService.updateProjectSettings(newProjectDir, { theme: "light" });
+ await settingsService.updateProjectSettings(newProjectDir, { theme: 'light' });
- const automakerDir = path.join(newProjectDir, ".automaker");
+ const automakerDir = path.join(newProjectDir, '.automaker');
const stats = await fs.stat(automakerDir);
expect(stats.isDirectory()).toBe(true);
@@ -441,25 +439,25 @@ describe("settings-service.ts", () => {
});
});
- describe("hasProjectSettings", () => {
- it("should return false when project settings file does not exist", async () => {
+ describe('hasProjectSettings', () => {
+ it('should return false when project settings file does not exist', async () => {
const exists = await settingsService.hasProjectSettings(testProjectDir);
expect(exists).toBe(false);
});
- it("should return true when project settings file exists", async () => {
- await settingsService.updateProjectSettings(testProjectDir, { theme: "light" });
+ it('should return true when project settings file exists', async () => {
+ await settingsService.updateProjectSettings(testProjectDir, { theme: 'light' });
const exists = await settingsService.hasProjectSettings(testProjectDir);
expect(exists).toBe(true);
});
});
- describe("migrateFromLocalStorage", () => {
- it("should migrate global settings from localStorage data", async () => {
+ describe('migrateFromLocalStorage', () => {
+ it('should migrate global settings from localStorage data', async () => {
const localStorageData = {
- "automaker-storage": JSON.stringify({
+ 'automaker-storage': JSON.stringify({
state: {
- theme: "light",
+ theme: 'light',
sidebarOpen: false,
maxConcurrency: 5,
},
@@ -474,17 +472,17 @@ describe("settings-service.ts", () => {
expect(result.migratedProjectCount).toBe(0);
const settings = await settingsService.getGlobalSettings();
- expect(settings.theme).toBe("light");
+ expect(settings.theme).toBe('light');
expect(settings.sidebarOpen).toBe(false);
expect(settings.maxConcurrency).toBe(5);
});
- it("should migrate credentials from localStorage data", async () => {
+ it('should migrate credentials from localStorage data', async () => {
const localStorageData = {
- "automaker-storage": JSON.stringify({
+ 'automaker-storage': JSON.stringify({
state: {
apiKeys: {
- anthropic: "sk-test-key",
+ anthropic: 'sk-test-key',
},
},
}),
@@ -496,24 +494,24 @@ describe("settings-service.ts", () => {
expect(result.migratedCredentials).toBe(true);
const credentials = await settingsService.getCredentials();
- expect(credentials.apiKeys.anthropic).toBe("sk-test-key");
+ expect(credentials.apiKeys.anthropic).toBe('sk-test-key');
});
- it("should migrate project settings from localStorage data", async () => {
+ it('should migrate project settings from localStorage data', async () => {
const localStorageData = {
- "automaker-storage": JSON.stringify({
+ 'automaker-storage': JSON.stringify({
state: {
projects: [
{
- id: "proj1",
- name: "Project 1",
+ id: 'proj1',
+ name: 'Project 1',
path: testProjectDir,
- theme: "light",
+ theme: 'light',
},
],
boardBackgroundByProject: {
[testProjectDir]: {
- imagePath: "/path/to/image.jpg",
+ imagePath: '/path/to/image.jpg',
cardOpacity: 0.8,
columnOpacity: 0.9,
columnBorderEnabled: true,
@@ -533,30 +531,30 @@ describe("settings-service.ts", () => {
expect(result.migratedProjectCount).toBe(1);
const projectSettings = await settingsService.getProjectSettings(testProjectDir);
- expect(projectSettings.theme).toBe("light");
- expect(projectSettings.boardBackground?.imagePath).toBe("/path/to/image.jpg");
+ expect(projectSettings.theme).toBe('light');
+ expect(projectSettings.boardBackground?.imagePath).toBe('/path/to/image.jpg');
});
- it("should handle direct localStorage values", async () => {
+ it('should handle direct localStorage values', async () => {
const localStorageData = {
- "automaker:lastProjectDir": "/path/to/project",
- "file-browser-recent-folders": JSON.stringify(["/path1", "/path2"]),
- "worktree-panel-collapsed": "true",
+ 'automaker:lastProjectDir': '/path/to/project',
+ 'file-browser-recent-folders': JSON.stringify(['/path1', '/path2']),
+ 'worktree-panel-collapsed': 'true',
};
const result = await settingsService.migrateFromLocalStorage(localStorageData);
expect(result.success).toBe(true);
const settings = await settingsService.getGlobalSettings();
- expect(settings.lastProjectDir).toBe("/path/to/project");
- expect(settings.recentFolders).toEqual(["/path1", "/path2"]);
+ expect(settings.lastProjectDir).toBe('/path/to/project');
+ expect(settings.recentFolders).toEqual(['/path1', '/path2']);
expect(settings.worktreePanelCollapsed).toBe(true);
});
- it("should handle invalid JSON gracefully", async () => {
+ it('should handle invalid JSON gracefully', async () => {
const localStorageData = {
- "automaker-storage": "invalid json",
- "file-browser-recent-folders": "invalid json",
+ 'automaker-storage': 'invalid json',
+ 'file-browser-recent-folders': 'invalid json',
};
const result = await settingsService.migrateFromLocalStorage(localStorageData);
@@ -565,7 +563,7 @@ describe("settings-service.ts", () => {
expect(result.errors.length).toBeGreaterThan(0);
});
- it("should handle migration errors gracefully", async () => {
+ it('should handle migration errors gracefully', async () => {
// Create a read-only directory to cause write errors
const readOnlyDir = path.join(os.tmpdir(), `readonly-${Date.now()}`);
await fs.mkdir(readOnlyDir, { recursive: true });
@@ -573,8 +571,8 @@ describe("settings-service.ts", () => {
const readOnlyService = new SettingsService(readOnlyDir);
const localStorageData = {
- "automaker-storage": JSON.stringify({
- state: { theme: "light" },
+ 'automaker-storage': JSON.stringify({
+ state: { theme: 'light' },
}),
};
@@ -588,15 +586,15 @@ describe("settings-service.ts", () => {
});
});
- describe("getDataDir", () => {
- it("should return the data directory path", () => {
+ describe('getDataDir', () => {
+ it('should return the data directory path', () => {
const dataDir = settingsService.getDataDir();
expect(dataDir).toBe(testDataDir);
});
});
- describe("atomicWriteJson", () => {
- it("should handle write errors and clean up temp file", async () => {
+ describe('atomicWriteJson', () => {
+ it('should handle write errors and clean up temp file', async () => {
// Create a read-only directory to cause write errors
const readOnlyDir = path.join(os.tmpdir(), `readonly-${Date.now()}`);
await fs.mkdir(readOnlyDir, { recursive: true });
@@ -604,13 +602,10 @@ describe("settings-service.ts", () => {
const readOnlyService = new SettingsService(readOnlyDir);
- await expect(
- readOnlyService.updateGlobalSettings({ theme: "light" })
- ).rejects.toThrow();
+ await expect(readOnlyService.updateGlobalSettings({ theme: 'light' })).rejects.toThrow();
await fs.chmod(readOnlyDir, 0o755);
await fs.rm(readOnlyDir, { recursive: true, force: true });
});
});
});
-
diff --git a/apps/server/tests/unit/services/terminal-service.test.ts b/apps/server/tests/unit/services/terminal-service.test.ts
index d273061a..44e823b0 100644
--- a/apps/server/tests/unit/services/terminal-service.test.ts
+++ b/apps/server/tests/unit/services/terminal-service.test.ts
@@ -1,14 +1,14 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
-import { TerminalService, getTerminalService } from "@/services/terminal-service.js";
-import * as pty from "node-pty";
-import * as os from "os";
-import * as fs from "fs";
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { TerminalService, getTerminalService } from '@/services/terminal-service.js';
+import * as pty from 'node-pty';
+import * as os from 'os';
+import * as fs from 'fs';
-vi.mock("node-pty");
-vi.mock("fs");
-vi.mock("os");
+vi.mock('node-pty');
+vi.mock('fs');
+vi.mock('os');
-describe("terminal-service.ts", () => {
+describe('terminal-service.ts', () => {
let service: TerminalService;
let mockPtyProcess: any;
@@ -26,225 +26,225 @@ describe("terminal-service.ts", () => {
};
vi.mocked(pty.spawn).mockReturnValue(mockPtyProcess);
- vi.mocked(os.homedir).mockReturnValue("/home/user");
- vi.mocked(os.platform).mockReturnValue("linux");
- vi.mocked(os.arch).mockReturnValue("x64");
+ vi.mocked(os.homedir).mockReturnValue('/home/user');
+ vi.mocked(os.platform).mockReturnValue('linux');
+ vi.mocked(os.arch).mockReturnValue('x64');
});
afterEach(() => {
service.cleanup();
});
- describe("detectShell", () => {
- it("should detect PowerShell Core on Windows when available", () => {
- vi.mocked(os.platform).mockReturnValue("win32");
+ describe('detectShell', () => {
+ it('should detect PowerShell Core on Windows when available', () => {
+ vi.mocked(os.platform).mockReturnValue('win32');
vi.mocked(fs.existsSync).mockImplementation((path: any) => {
- return path === "C:\\Program Files\\PowerShell\\7\\pwsh.exe";
+ return path === 'C:\\Program Files\\PowerShell\\7\\pwsh.exe';
});
const result = service.detectShell();
- expect(result.shell).toBe("C:\\Program Files\\PowerShell\\7\\pwsh.exe");
+ expect(result.shell).toBe('C:\\Program Files\\PowerShell\\7\\pwsh.exe');
expect(result.args).toEqual([]);
});
- it("should fall back to PowerShell on Windows if Core not available", () => {
- vi.mocked(os.platform).mockReturnValue("win32");
+ it('should fall back to PowerShell on Windows if Core not available', () => {
+ vi.mocked(os.platform).mockReturnValue('win32');
vi.mocked(fs.existsSync).mockImplementation((path: any) => {
- return path === "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe";
+ return path === 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
});
const result = service.detectShell();
- expect(result.shell).toBe("C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe");
+ expect(result.shell).toBe('C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe');
expect(result.args).toEqual([]);
});
- it("should fall back to cmd.exe on Windows if no PowerShell", () => {
- vi.mocked(os.platform).mockReturnValue("win32");
+ it('should fall back to cmd.exe on Windows if no PowerShell', () => {
+ vi.mocked(os.platform).mockReturnValue('win32');
vi.mocked(fs.existsSync).mockReturnValue(false);
const result = service.detectShell();
- expect(result.shell).toBe("cmd.exe");
+ expect(result.shell).toBe('cmd.exe');
expect(result.args).toEqual([]);
});
- it("should detect user shell on macOS", () => {
- vi.mocked(os.platform).mockReturnValue("darwin");
- vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/zsh" });
+ it('should detect user shell on macOS', () => {
+ vi.mocked(os.platform).mockReturnValue('darwin');
+ vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/zsh' });
vi.mocked(fs.existsSync).mockReturnValue(true);
const result = service.detectShell();
- expect(result.shell).toBe("/bin/zsh");
- expect(result.args).toEqual(["--login"]);
+ expect(result.shell).toBe('/bin/zsh');
+ expect(result.args).toEqual(['--login']);
});
- it("should fall back to zsh on macOS if user shell not available", () => {
- vi.mocked(os.platform).mockReturnValue("darwin");
- vi.spyOn(process, "env", "get").mockReturnValue({});
+ it('should fall back to zsh on macOS if user shell not available', () => {
+ vi.mocked(os.platform).mockReturnValue('darwin');
+ vi.spyOn(process, 'env', 'get').mockReturnValue({});
vi.mocked(fs.existsSync).mockImplementation((path: any) => {
- return path === "/bin/zsh";
+ return path === '/bin/zsh';
});
const result = service.detectShell();
- expect(result.shell).toBe("/bin/zsh");
- expect(result.args).toEqual(["--login"]);
+ expect(result.shell).toBe('/bin/zsh');
+ expect(result.args).toEqual(['--login']);
});
- it("should fall back to bash on macOS if zsh not available", () => {
- vi.mocked(os.platform).mockReturnValue("darwin");
- vi.spyOn(process, "env", "get").mockReturnValue({});
+ it('should fall back to bash on macOS if zsh not available', () => {
+ vi.mocked(os.platform).mockReturnValue('darwin');
+ vi.spyOn(process, 'env', 'get').mockReturnValue({});
vi.mocked(fs.existsSync).mockReturnValue(false);
const result = service.detectShell();
- expect(result.shell).toBe("/bin/bash");
- expect(result.args).toEqual(["--login"]);
+ expect(result.shell).toBe('/bin/bash');
+ expect(result.args).toEqual(['--login']);
});
- it("should detect user shell on Linux", () => {
- vi.mocked(os.platform).mockReturnValue("linux");
- vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
+ it('should detect user shell on Linux', () => {
+ vi.mocked(os.platform).mockReturnValue('linux');
+ vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
vi.mocked(fs.existsSync).mockReturnValue(true);
const result = service.detectShell();
- expect(result.shell).toBe("/bin/bash");
- expect(result.args).toEqual(["--login"]);
+ expect(result.shell).toBe('/bin/bash');
+ expect(result.args).toEqual(['--login']);
});
- it("should fall back to bash on Linux if user shell not available", () => {
- vi.mocked(os.platform).mockReturnValue("linux");
- vi.spyOn(process, "env", "get").mockReturnValue({});
+ it('should fall back to bash on Linux if user shell not available', () => {
+ vi.mocked(os.platform).mockReturnValue('linux');
+ vi.spyOn(process, 'env', 'get').mockReturnValue({});
vi.mocked(fs.existsSync).mockImplementation((path: any) => {
- return path === "/bin/bash";
+ return path === '/bin/bash';
});
const result = service.detectShell();
- expect(result.shell).toBe("/bin/bash");
- expect(result.args).toEqual(["--login"]);
+ expect(result.shell).toBe('/bin/bash');
+ expect(result.args).toEqual(['--login']);
});
- it("should fall back to sh on Linux if bash not available", () => {
- vi.mocked(os.platform).mockReturnValue("linux");
- vi.spyOn(process, "env", "get").mockReturnValue({});
+ it('should fall back to sh on Linux if bash not available', () => {
+ vi.mocked(os.platform).mockReturnValue('linux');
+ vi.spyOn(process, 'env', 'get').mockReturnValue({});
vi.mocked(fs.existsSync).mockReturnValue(false);
const result = service.detectShell();
- expect(result.shell).toBe("/bin/sh");
+ expect(result.shell).toBe('/bin/sh');
expect(result.args).toEqual([]);
});
- it("should detect WSL and use appropriate shell", () => {
- vi.mocked(os.platform).mockReturnValue("linux");
- vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
+ it('should detect WSL and use appropriate shell', () => {
+ vi.mocked(os.platform).mockReturnValue('linux');
+ vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
vi.mocked(fs.existsSync).mockReturnValue(true);
- vi.mocked(fs.readFileSync).mockReturnValue("Linux version 5.10.0-microsoft-standard-WSL2");
+ vi.mocked(fs.readFileSync).mockReturnValue('Linux version 5.10.0-microsoft-standard-WSL2');
const result = service.detectShell();
- expect(result.shell).toBe("/bin/bash");
- expect(result.args).toEqual(["--login"]);
+ expect(result.shell).toBe('/bin/bash');
+ expect(result.args).toEqual(['--login']);
});
});
- describe("isWSL", () => {
- it("should return true if /proc/version contains microsoft", () => {
+ describe('isWSL', () => {
+ it('should return true if /proc/version contains microsoft', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
- vi.mocked(fs.readFileSync).mockReturnValue("Linux version 5.10.0-microsoft-standard-WSL2");
+ vi.mocked(fs.readFileSync).mockReturnValue('Linux version 5.10.0-microsoft-standard-WSL2');
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(fs.readFileSync).mockReturnValue("Linux version 5.10.0-wsl2");
+ vi.mocked(fs.readFileSync).mockReturnValue('Linux version 5.10.0-wsl2');
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.spyOn(process, "env", "get").mockReturnValue({ WSL_DISTRO_NAME: "Ubuntu" });
+ vi.spyOn(process, 'env', 'get').mockReturnValue({ WSL_DISTRO_NAME: 'Ubuntu' });
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.spyOn(process, "env", "get").mockReturnValue({ WSLENV: "PATH/l" });
+ vi.spyOn(process, 'env', 'get').mockReturnValue({ WSLENV: 'PATH/l' });
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.spyOn(process, "env", "get").mockReturnValue({});
+ vi.spyOn(process, 'env', 'get').mockReturnValue({});
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(fs.readFileSync).mockImplementation(() => {
- throw new Error("Permission denied");
+ throw new Error('Permission denied');
});
expect(service.isWSL()).toBe(false);
});
});
- describe("getPlatformInfo", () => {
- it("should return platform information", () => {
- vi.mocked(os.platform).mockReturnValue("linux");
- vi.mocked(os.arch).mockReturnValue("x64");
+ describe('getPlatformInfo', () => {
+ it('should return platform information', () => {
+ vi.mocked(os.platform).mockReturnValue('linux');
+ vi.mocked(os.arch).mockReturnValue('x64');
vi.mocked(fs.existsSync).mockReturnValue(true);
- vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
+ vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
const info = service.getPlatformInfo();
- expect(info.platform).toBe("linux");
- expect(info.arch).toBe("x64");
- expect(info.defaultShell).toBe("/bin/bash");
- expect(typeof info.isWSL).toBe("boolean");
+ expect(info.platform).toBe('linux');
+ expect(info.arch).toBe('x64');
+ expect(info.defaultShell).toBe('/bin/bash');
+ expect(typeof info.isWSL).toBe('boolean');
});
});
- describe("createSession", () => {
- it("should create a new terminal session", () => {
+ describe('createSession', () => {
+ it('should create a new terminal session', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ 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({
- cwd: "/test/dir",
+ cwd: '/test/dir',
cols: 100,
rows: 30,
});
expect(session.id).toMatch(/^term-/);
- expect(session.cwd).toBe("/test/dir");
- expect(session.shell).toBe("/bin/bash");
+ expect(session.cwd).toBe('/test/dir');
+ expect(session.shell).toBe('/bin/bash');
expect(pty.spawn).toHaveBeenCalledWith(
- "/bin/bash",
- ["--login"],
+ '/bin/bash',
+ ['--login'],
expect.objectContaining({
- cwd: "/test/dir",
+ cwd: '/test/dir',
cols: 100,
rows: 30,
})
);
});
- it("should use default cols and rows if not provided", () => {
+ it('should use default cols and rows if not provided', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
- vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
+ vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
service.createSession();
@@ -258,61 +258,61 @@ 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', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockImplementation(() => {
- throw 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({
- cwd: "/nonexistent",
+ cwd: '/nonexistent',
});
- expect(session.cwd).toBe("/home/user");
+ 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', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ 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({
- cwd: "/file.txt",
+ cwd: '/file.txt',
});
- expect(session.cwd).toBe("/home/user");
+ expect(session.cwd).toBe('/home/user');
});
- it("should fix double slashes in path", () => {
+ it('should fix double slashes in path', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ 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({
- cwd: "//test/dir",
+ cwd: '//test/dir',
});
- expect(session.cwd).toBe("/test/dir");
+ expect(session.cwd).toBe('/test/dir');
});
- it("should preserve WSL UNC paths", () => {
+ it('should preserve WSL UNC paths', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ 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({
- cwd: "//wsl$/Ubuntu/home",
+ cwd: '//wsl$/Ubuntu/home',
});
- expect(session.cwd).toBe("//wsl$/Ubuntu/home");
+ expect(session.cwd).toBe('//wsl$/Ubuntu/home');
});
- it("should handle data events from PTY", () => {
+ it('should handle data events from PTY', () => {
vi.useFakeTimers();
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ 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();
service.onData(dataCallback);
@@ -321,7 +321,7 @@ describe("terminal-service.ts", () => {
// Simulate data event
const onDataHandler = mockPtyProcess.onData.mock.calls[0][0];
- onDataHandler("test data");
+ onDataHandler('test data');
// Wait for throttled output
vi.advanceTimersByTime(20);
@@ -331,10 +331,10 @@ describe("terminal-service.ts", () => {
vi.useRealTimers();
});
- it("should handle exit events from PTY", () => {
+ it('should handle exit events from PTY', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ 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();
service.onExit(exitCallback);
@@ -350,32 +350,32 @@ describe("terminal-service.ts", () => {
});
});
- describe("write", () => {
- it("should write data to existing session", () => {
+ describe('write', () => {
+ it('should write data to existing session', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ 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 result = service.write(session.id, "ls\n");
+ const result = service.write(session.id, 'ls\n');
expect(result).toBe(true);
- expect(mockPtyProcess.write).toHaveBeenCalledWith("ls\n");
+ expect(mockPtyProcess.write).toHaveBeenCalledWith('ls\n');
});
- it("should return false for non-existent session", () => {
- const result = service.write("nonexistent", "data");
+ it('should return false for non-existent session', () => {
+ const result = service.write('nonexistent', 'data');
expect(result).toBe(false);
expect(mockPtyProcess.write).not.toHaveBeenCalled();
});
});
- describe("resize", () => {
- it("should resize existing session", () => {
+ describe('resize', () => {
+ it('should resize existing session', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ 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 result = service.resize(session.id, 120, 40);
@@ -384,19 +384,19 @@ describe("terminal-service.ts", () => {
expect(mockPtyProcess.resize).toHaveBeenCalledWith(120, 40);
});
- it("should return false for non-existent session", () => {
- const result = service.resize("nonexistent", 120, 40);
+ it('should return false for non-existent session', () => {
+ const result = service.resize('nonexistent', 120, 40);
expect(result).toBe(false);
expect(mockPtyProcess.resize).not.toHaveBeenCalled();
});
- it("should handle resize errors", () => {
+ it('should handle resize errors', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ 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(() => {
- throw new Error("Resize failed");
+ throw new Error('Resize failed');
});
const session = service.createSession();
@@ -406,32 +406,40 @@ describe("terminal-service.ts", () => {
});
});
- describe("killSession", () => {
- it("should kill existing session", () => {
+ describe('killSession', () => {
+ it('should kill existing session', () => {
+ vi.useFakeTimers();
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ 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 result = service.killSession(session.id);
expect(result).toBe(true);
- expect(mockPtyProcess.kill).toHaveBeenCalled();
+ expect(mockPtyProcess.kill).toHaveBeenCalledWith('SIGTERM');
+
+ // Session is removed after SIGKILL timeout (1 second)
+ vi.advanceTimersByTime(1000);
+
+ expect(mockPtyProcess.kill).toHaveBeenCalledWith('SIGKILL');
expect(service.getSession(session.id)).toBeUndefined();
+
+ vi.useRealTimers();
});
- it("should return false for non-existent session", () => {
- const result = service.killSession("nonexistent");
+ it('should return false for non-existent session', () => {
+ const result = service.killSession('nonexistent');
expect(result).toBe(false);
});
- it("should handle kill errors", () => {
+ it('should handle kill errors', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ 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(() => {
- throw new Error("Kill failed");
+ throw new Error('Kill failed');
});
const session = service.createSession();
@@ -441,11 +449,11 @@ describe("terminal-service.ts", () => {
});
});
- describe("getSession", () => {
- it("should return existing session", () => {
+ describe('getSession', () => {
+ it('should return existing session', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ 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 retrieved = service.getSession(session.id);
@@ -453,84 +461,84 @@ describe("terminal-service.ts", () => {
expect(retrieved).toBe(session);
});
- it("should return undefined for non-existent session", () => {
- const retrieved = service.getSession("nonexistent");
+ it('should return undefined for non-existent session', () => {
+ const retrieved = service.getSession('nonexistent');
expect(retrieved).toBeUndefined();
});
});
- describe("getScrollback", () => {
- it("should return scrollback buffer for existing session", () => {
+ describe('getScrollback', () => {
+ it('should return scrollback buffer for existing session', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ 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();
- session.scrollbackBuffer = "test scrollback";
+ session.scrollbackBuffer = 'test scrollback';
const scrollback = service.getScrollback(session.id);
- expect(scrollback).toBe("test scrollback");
+ expect(scrollback).toBe('test scrollback');
});
- it("should return null for non-existent session", () => {
- const scrollback = service.getScrollback("nonexistent");
+ it('should return null for non-existent session', () => {
+ const scrollback = service.getScrollback('nonexistent');
expect(scrollback).toBeNull();
});
});
- describe("getAllSessions", () => {
- it("should return all active sessions", () => {
+ describe('getAllSessions', () => {
+ it('should return all active sessions', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ 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 session2 = service.createSession({ cwd: "/dir2" });
+ const session1 = service.createSession({ cwd: '/dir1' });
+ const session2 = service.createSession({ cwd: '/dir2' });
const sessions = service.getAllSessions();
expect(sessions).toHaveLength(2);
expect(sessions[0].id).toBe(session1.id);
expect(sessions[1].id).toBe(session2.id);
- expect(sessions[0].cwd).toBe("/dir1");
- expect(sessions[1].cwd).toBe("/dir2");
+ expect(sessions[0].cwd).toBe('/dir1');
+ expect(sessions[1].cwd).toBe('/dir2');
});
- it("should return empty array if no sessions", () => {
+ it('should return empty array if no sessions', () => {
const sessions = service.getAllSessions();
expect(sessions).toEqual([]);
});
});
- describe("onData and onExit", () => {
- it("should allow subscribing and unsubscribing from data events", () => {
+ describe('onData and onExit', () => {
+ it('should allow subscribing and unsubscribing from data events', () => {
const callback = vi.fn();
const unsubscribe = service.onData(callback);
- expect(typeof unsubscribe).toBe("function");
+ expect(typeof unsubscribe).toBe('function');
unsubscribe();
});
- it("should allow subscribing and unsubscribing from exit events", () => {
+ it('should allow subscribing and unsubscribing from exit events', () => {
const callback = vi.fn();
const unsubscribe = service.onExit(callback);
- expect(typeof unsubscribe).toBe("function");
+ expect(typeof unsubscribe).toBe('function');
unsubscribe();
});
});
- describe("cleanup", () => {
- it("should clean up all sessions", () => {
+ describe('cleanup', () => {
+ it('should clean up all sessions', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ 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 session2 = service.createSession();
@@ -542,12 +550,12 @@ describe("terminal-service.ts", () => {
expect(service.getAllSessions()).toHaveLength(0);
});
- it("should handle cleanup errors gracefully", () => {
+ it('should handle cleanup errors gracefully', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ 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(() => {
- throw new Error("Kill failed");
+ throw new Error('Kill failed');
});
service.createSession();
@@ -556,8 +564,8 @@ describe("terminal-service.ts", () => {
});
});
- describe("getTerminalService", () => {
- it("should return singleton instance", () => {
+ describe('getTerminalService', () => {
+ it('should return singleton instance', () => {
const instance1 = getTerminalService();
const instance2 = getTerminalService();
diff --git a/apps/server/tests/utils/helpers.ts b/apps/server/tests/utils/helpers.ts
index 9daa99ec..cf928f07 100644
--- a/apps/server/tests/utils/helpers.ts
+++ b/apps/server/tests/utils/helpers.ts
@@ -24,7 +24,7 @@ export async function waitFor(
const start = Date.now();
while (!condition()) {
if (Date.now() - start > timeout) {
- throw new Error("Timeout waiting for condition");
+ throw new Error('Timeout waiting for condition');
}
await new Promise((resolve) => setTimeout(resolve, interval));
}
diff --git a/apps/server/tests/utils/mocks.ts b/apps/server/tests/utils/mocks.ts
index ce5b1457..380ac9fd 100644
--- a/apps/server/tests/utils/mocks.ts
+++ b/apps/server/tests/utils/mocks.ts
@@ -3,10 +3,10 @@
* Provides reusable mocks for common dependencies
*/
-import { vi } from "vitest";
-import type { ChildProcess } from "child_process";
-import { EventEmitter } from "events";
-import type { Readable } from "stream";
+import { vi } from 'vitest';
+import type { ChildProcess } from 'child_process';
+import { EventEmitter } from 'events';
+import type { Readable } from 'stream';
/**
* Mock child_process.spawn for subprocess tests
@@ -31,19 +31,19 @@ export function createMockChildProcess(options: {
process.nextTick(() => {
// Emit stdout lines
for (const line of stdout) {
- mockProcess.stdout.emit("data", Buffer.from(line + "\n"));
+ mockProcess.stdout.emit('data', Buffer.from(line + '\n'));
}
// Emit stderr lines
for (const line of stderr) {
- mockProcess.stderr.emit("data", Buffer.from(line + "\n"));
+ mockProcess.stderr.emit('data', Buffer.from(line + '\n'));
}
// Emit exit or error
if (shouldError) {
- mockProcess.emit("error", new Error("Process error"));
+ mockProcess.emit('error', new Error('Process error'));
} else {
- mockProcess.emit("exit", exitCode);
+ mockProcess.emit('exit', exitCode);
}
});
diff --git a/apps/ui/docs/AGENT_ARCHITECTURE.md b/apps/ui/docs/AGENT_ARCHITECTURE.md
index ca5bee5e..4c9f0d11 100644
--- a/apps/ui/docs/AGENT_ARCHITECTURE.md
+++ b/apps/ui/docs/AGENT_ARCHITECTURE.md
@@ -90,9 +90,9 @@ const {
clearHistory, // Clear conversation
error, // Error state
} = useElectronAgent({
- sessionId: "project_xyz",
- workingDirectory: "/path/to/project",
- onToolUse: (tool) => console.log("Using:", tool),
+ sessionId: 'project_xyz',
+ workingDirectory: '/path/to/project',
+ onToolUse: (tool) => console.log('Using:', tool),
});
```
@@ -160,7 +160,7 @@ Each session file contains:
Session IDs are generated from project paths:
```typescript
-const sessionId = `project_${projectPath.replace(/[^a-zA-Z0-9]/g, "_")}`;
+const sessionId = `project_${projectPath.replace(/[^a-zA-Z0-9]/g, '_')}`;
```
This ensures:
diff --git a/apps/ui/docs/SESSION_MANAGEMENT.md b/apps/ui/docs/SESSION_MANAGEMENT.md
index 9ca27867..b4c5eac7 100644
--- a/apps/ui/docs/SESSION_MANAGEMENT.md
+++ b/apps/ui/docs/SESSION_MANAGEMENT.md
@@ -7,24 +7,28 @@ The Automaker Agent Chat now supports multiple concurrent sessions, allowing you
## Features
### ✨ Multiple Sessions
+
- Create unlimited agent sessions per project
- Each session has its own conversation history
- Switch between sessions instantly
- Sessions persist across app restarts
### 📋 Session Organization
+
- Custom names for easy identification
- Last message preview
- Message count tracking
- Sort by most recently updated
### 🗄️ Archive & Delete
+
- Archive old sessions to declutter
- Unarchive when needed
- Permanently delete sessions
- Confirm before destructive actions
### 💾 Automatic Persistence
+
- All sessions auto-save to disk
- Survive Next.js restarts
- Survive Electron app restarts
@@ -67,6 +71,7 @@ Click the panel icon in the header to show/hide the session manager.
4. The new session is immediately active
**Example session names:**
+
- "Feature: Dark Mode"
- "Bug: Login redirect"
- "Refactor: API layer"
@@ -93,6 +98,7 @@ Click the **"Clear"** button in the chat header to delete all messages from the
3. Toggle **"Show Archived"** to view archived sessions
**When to archive:**
+
- Completed features
- Resolved bugs
- Old experiments
@@ -117,16 +123,19 @@ Click the **"Clear"** button in the chat header to delete all messages from the
Sessions are stored in your user data directory:
**macOS:**
+
```
~/Library/Application Support/automaker/agent-sessions/
```
**Windows:**
+
```
%APPDATA%/automaker/agent-sessions/
```
**Linux:**
+
```
~/.config/automaker/agent-sessions/
```
@@ -215,12 +224,14 @@ Use prefixes to organize sessions by type:
### When to Create Multiple Sessions
**Do create separate sessions for:**
+
- ✅ Different features
- ✅ Unrelated bugs
- ✅ Experimental work
- ✅ Different contexts or approaches
**Don't create separate sessions for:**
+
- ❌ Same feature, different iterations
- ❌ Related bug fixes
- ❌ Continuation of previous work
@@ -272,7 +283,7 @@ Use prefixes to organize sessions by type:
## Keyboard Shortcuts
-*(Coming soon)*
+_(Coming soon)_
- `Cmd/Ctrl + K` - Create new session
- `Cmd/Ctrl + [` - Previous session
@@ -284,11 +295,13 @@ Use prefixes to organize sessions by type:
### Session Not Saving
**Check:**
+
- Electron has write permissions
- Disk space available
- Check Electron console for errors
**Solution:**
+
```bash
# macOS - Check permissions
ls -la ~/Library/Application\ Support/automaker/
@@ -300,11 +313,13 @@ chmod -R u+w ~/Library/Application\ Support/automaker/
### Can't Switch Sessions
**Check:**
+
- Session is not archived
- No errors in console
- Agent is not currently processing
**Solution:**
+
- Wait for current message to complete
- Check for error messages
- Try clearing and reloading
@@ -312,11 +327,13 @@ chmod -R u+w ~/Library/Application\ Support/automaker/
### Session Disappeared
**Check:**
+
- Not filtered by archive status
- Not accidentally deleted
- Check backup files
**Recovery:**
+
- Toggle "Show Archived"
- Check filesystem for `.json` files
- Restore from backup if available
@@ -326,15 +343,17 @@ chmod -R u+w ~/Library/Application\ Support/automaker/
For developers integrating session management:
### Create Session
+
```typescript
const result = await window.electronAPI.sessions.create(
- "Session Name",
- "/project/path",
- "/working/directory"
+ 'Session Name',
+ '/project/path',
+ '/working/directory'
);
```
### List Sessions
+
```typescript
const { sessions } = await window.electronAPI.sessions.list(
false // includeArchived
@@ -342,21 +361,20 @@ const { sessions } = await window.electronAPI.sessions.list(
```
### Update Session
+
```typescript
-await window.electronAPI.sessions.update(
- sessionId,
- "New Name",
- ["tag1", "tag2"]
-);
+await window.electronAPI.sessions.update(sessionId, 'New Name', ['tag1', 'tag2']);
```
### Archive/Unarchive
+
```typescript
await window.electronAPI.sessions.archive(sessionId);
await window.electronAPI.sessions.unarchive(sessionId);
```
### Delete Session
+
```typescript
await window.electronAPI.sessions.delete(sessionId);
```
diff --git a/apps/ui/eslint.config.mjs b/apps/ui/eslint.config.mjs
index 150f0bad..d7bc54d4 100644
--- a/apps/ui/eslint.config.mjs
+++ b/apps/ui/eslint.config.mjs
@@ -1,35 +1,111 @@
-import { defineConfig, globalIgnores } from "eslint/config";
-import js from "@eslint/js";
-import ts from "@typescript-eslint/eslint-plugin";
-import tsParser from "@typescript-eslint/parser";
+import { defineConfig, globalIgnores } from 'eslint/config';
+import js from '@eslint/js';
+import ts from '@typescript-eslint/eslint-plugin';
+import tsParser from '@typescript-eslint/parser';
const eslintConfig = defineConfig([
js.configs.recommended,
{
- files: ["**/*.ts", "**/*.tsx"],
+ files: ['**/*.mjs', '**/*.cjs'],
+ languageOptions: {
+ globals: {
+ console: 'readonly',
+ process: 'readonly',
+ require: 'readonly',
+ __dirname: 'readonly',
+ __filename: 'readonly',
+ },
+ },
+ },
+ {
+ files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parser: tsParser,
parserOptions: {
- ecmaVersion: "latest",
- sourceType: "module",
+ ecmaVersion: 'latest',
+ sourceType: 'module',
+ },
+ globals: {
+ // Browser/DOM APIs
+ window: 'readonly',
+ document: 'readonly',
+ navigator: 'readonly',
+ Navigator: 'readonly',
+ localStorage: 'readonly',
+ sessionStorage: 'readonly',
+ fetch: 'readonly',
+ WebSocket: 'readonly',
+ File: 'readonly',
+ FileList: 'readonly',
+ FileReader: 'readonly',
+ Blob: 'readonly',
+ atob: 'readonly',
+ crypto: 'readonly',
+ prompt: 'readonly',
+ confirm: 'readonly',
+ getComputedStyle: 'readonly',
+ requestAnimationFrame: 'readonly',
+ // DOM Element Types
+ HTMLElement: 'readonly',
+ HTMLInputElement: 'readonly',
+ HTMLDivElement: 'readonly',
+ HTMLButtonElement: 'readonly',
+ HTMLSpanElement: 'readonly',
+ HTMLTextAreaElement: 'readonly',
+ HTMLHeadingElement: 'readonly',
+ HTMLParagraphElement: 'readonly',
+ HTMLImageElement: 'readonly',
+ Element: 'readonly',
+ // Event Types
+ Event: 'readonly',
+ KeyboardEvent: 'readonly',
+ DragEvent: 'readonly',
+ PointerEvent: 'readonly',
+ CustomEvent: 'readonly',
+ ClipboardEvent: 'readonly',
+ WheelEvent: 'readonly',
+ DataTransfer: 'readonly',
+ // Web APIs
+ ResizeObserver: 'readonly',
+ AbortSignal: 'readonly',
+ Audio: 'readonly',
+ ScrollBehavior: 'readonly',
+ // Timers
+ setTimeout: 'readonly',
+ setInterval: 'readonly',
+ clearTimeout: 'readonly',
+ clearInterval: 'readonly',
+ // Node.js (for scripts and Electron)
+ process: 'readonly',
+ require: 'readonly',
+ __dirname: 'readonly',
+ __filename: 'readonly',
+ NodeJS: 'readonly',
+ // React
+ React: 'readonly',
+ JSX: 'readonly',
+ // Electron
+ Electron: 'readonly',
+ // Console
+ console: 'readonly',
},
},
plugins: {
- "@typescript-eslint": ts,
+ '@typescript-eslint': ts,
},
rules: {
...ts.configs.recommended.rules,
- "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
- "@typescript-eslint/no-explicit-any": "warn",
+ '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
+ '@typescript-eslint/no-explicit-any': 'warn',
},
},
globalIgnores([
- "dist/**",
- "dist-electron/**",
- "node_modules/**",
- "server-bundle/**",
- "release/**",
- "src/routeTree.gen.ts",
+ 'dist/**',
+ 'dist-electron/**',
+ 'node_modules/**',
+ 'server-bundle/**',
+ 'release/**',
+ 'src/routeTree.gen.ts',
]),
]);
diff --git a/apps/ui/index.html b/apps/ui/index.html
index 02087b01..49a7aa1e 100644
--- a/apps/ui/index.html
+++ b/apps/ui/index.html
@@ -8,7 +8,7 @@