Merge branch 'main' of github.com:AutoMaker-Org/automaker into improve-context-page

This commit is contained in:
Test User
2025-12-22 00:50:55 -05:00
501 changed files with 17637 additions and 17437 deletions

View File

@@ -21,4 +21,4 @@
"mcp__puppeteer__puppeteer_evaluate" "mcp__puppeteer__puppeteer_evaluate"
] ]
} }
} }

View File

@@ -1,28 +1,28 @@
name: "Setup Project" name: 'Setup Project'
description: "Common setup steps for CI workflows - checkout, Node.js, dependencies, and native modules" description: 'Common setup steps for CI workflows - checkout, Node.js, dependencies, and native modules'
inputs: inputs:
node-version: node-version:
description: "Node.js version to use" description: 'Node.js version to use'
required: false required: false
default: "22" default: '22'
check-lockfile: check-lockfile:
description: "Run lockfile lint check for SSH URLs" description: 'Run lockfile lint check for SSH URLs'
required: false required: false
default: "false" default: 'false'
rebuild-node-pty-path: rebuild-node-pty-path:
description: "Working directory for node-pty rebuild (empty = root)" description: 'Working directory for node-pty rebuild (empty = root)'
required: false required: false
default: "" default: ''
runs: runs:
using: "composite" using: 'composite'
steps: steps:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: ${{ inputs.node-version }} node-version: ${{ inputs.node-version }}
cache: "npm" cache: 'npm'
cache-dependency-path: package-lock.json cache-dependency-path: package-lock.json
- name: Check for SSH URLs in lockfile - name: Check for SSH URLs in lockfile

View File

@@ -1,15 +1,11 @@
const { const { S3Client, PutObjectCommand, GetObjectCommand } = require('@aws-sdk/client-s3');
S3Client, const fs = require('fs');
PutObjectCommand, const path = require('path');
GetObjectCommand, const https = require('https');
} = require("@aws-sdk/client-s3"); const { pipeline } = require('stream/promises');
const fs = require("fs");
const path = require("path");
const https = require("https");
const { pipeline } = require("stream/promises");
const s3Client = new S3Client({ const s3Client = new S3Client({
region: "auto", region: 'auto',
endpoint: process.env.R2_ENDPOINT, endpoint: process.env.R2_ENDPOINT,
credentials: { credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID, accessKeyId: process.env.R2_ACCESS_KEY_ID,
@@ -28,14 +24,14 @@ async function fetchExistingReleases() {
const response = await s3Client.send( const response = await s3Client.send(
new GetObjectCommand({ new GetObjectCommand({
Bucket: BUCKET, Bucket: BUCKET,
Key: "releases.json", Key: 'releases.json',
}) })
); );
const body = await response.Body.transformToString(); const body = await response.Body.transformToString();
return JSON.parse(body); return JSON.parse(body);
} catch (error) { } catch (error) {
if (error.name === "NoSuchKey" || error.$metadata?.httpStatusCode === 404) { if (error.name === 'NoSuchKey' || error.$metadata?.httpStatusCode === 404) {
console.log("No existing releases.json found, creating new one"); console.log('No existing releases.json found, creating new one');
return { latestVersion: null, releases: [] }; return { latestVersion: null, releases: [] };
} }
throw error; throw error;
@@ -85,7 +81,7 @@ async function checkUrlAccessible(url, maxRetries = 10, initialDelay = 1000) {
resolve({ resolve({
accessible: false, accessible: false,
statusCode, statusCode,
error: "Redirect without location header", error: 'Redirect without location header',
}); });
return; return;
} }
@@ -93,18 +89,16 @@ async function checkUrlAccessible(url, maxRetries = 10, initialDelay = 1000) {
return https return https
.get(redirectUrl, { timeout: 10000 }, (redirectResponse) => { .get(redirectUrl, { timeout: 10000 }, (redirectResponse) => {
const redirectStatus = redirectResponse.statusCode; const redirectStatus = redirectResponse.statusCode;
const contentType = const contentType = redirectResponse.headers['content-type'] || '';
redirectResponse.headers["content-type"] || "";
// Check if it's actually a file (zip/tar.gz) and not HTML // Check if it's actually a file (zip/tar.gz) and not HTML
const isFile = const isFile =
contentType.includes("application/zip") || contentType.includes('application/zip') ||
contentType.includes("application/gzip") || contentType.includes('application/gzip') ||
contentType.includes("application/x-gzip") || contentType.includes('application/x-gzip') ||
contentType.includes("application/x-tar") || contentType.includes('application/x-tar') ||
redirectUrl.includes(".zip") || redirectUrl.includes('.zip') ||
redirectUrl.includes(".tar.gz"); redirectUrl.includes('.tar.gz');
const isGood = const isGood = redirectStatus >= 200 && redirectStatus < 300 && isFile;
redirectStatus >= 200 && redirectStatus < 300 && isFile;
redirectResponse.destroy(); redirectResponse.destroy();
resolve({ resolve({
accessible: isGood, accessible: isGood,
@@ -113,38 +107,38 @@ async function checkUrlAccessible(url, maxRetries = 10, initialDelay = 1000) {
contentType, contentType,
}); });
}) })
.on("error", (error) => { .on('error', (error) => {
resolve({ resolve({
accessible: false, accessible: false,
statusCode, statusCode,
error: error.message, error: error.message,
}); });
}) })
.on("timeout", function () { .on('timeout', function () {
this.destroy(); this.destroy();
resolve({ resolve({
accessible: false, accessible: false,
statusCode, statusCode,
error: "Timeout following redirect", error: 'Timeout following redirect',
}); });
}); });
} }
// Check if status is good (200-299 range) and it's actually a file // 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 = const isFile =
contentType.includes("application/zip") || contentType.includes('application/zip') ||
contentType.includes("application/gzip") || contentType.includes('application/gzip') ||
contentType.includes("application/x-gzip") || contentType.includes('application/x-gzip') ||
contentType.includes("application/x-tar") || contentType.includes('application/x-tar') ||
url.includes(".zip") || url.includes('.zip') ||
url.includes(".tar.gz"); url.includes('.tar.gz');
const isGood = statusCode >= 200 && statusCode < 300 && isFile; const isGood = statusCode >= 200 && statusCode < 300 && isFile;
response.destroy(); response.destroy();
resolve({ accessible: isGood, statusCode, contentType }); resolve({ accessible: isGood, statusCode, contentType });
}); });
request.on("error", (error) => { request.on('error', (error) => {
resolve({ resolve({
accessible: false, accessible: false,
statusCode: null, statusCode: null,
@@ -152,12 +146,12 @@ async function checkUrlAccessible(url, maxRetries = 10, initialDelay = 1000) {
}); });
}); });
request.on("timeout", () => { request.on('timeout', () => {
request.destroy(); request.destroy();
resolve({ resolve({
accessible: false, accessible: false,
statusCode: null, 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})` `✓ URL ${url} is now accessible after ${attempt} retries (status: ${result.statusCode})`
); );
} else { } else {
console.log( console.log(`✓ URL ${url} is accessible (status: ${result.statusCode})`);
`✓ URL ${url} is accessible (status: ${result.statusCode})`
);
} }
return result.finalUrl || url; // Return the final URL (after redirects) if available return result.finalUrl || url; // Return the final URL (after redirects) if available
} else { } else {
const errorMsg = result.error ? ` - ${result.error}` : ""; const errorMsg = result.error ? ` - ${result.error}` : '';
const statusMsg = result.statusCode const statusMsg = result.statusCode ? ` (status: ${result.statusCode})` : '';
? ` (status: ${result.statusCode})` const contentTypeMsg = result.contentType ? ` [content-type: ${result.contentType}]` : '';
: ""; console.log(`✗ URL ${url} not accessible${statusMsg}${contentTypeMsg}${errorMsg}`);
const contentTypeMsg = result.contentType
? ` [content-type: ${result.contentType}]`
: "";
console.log(
`✗ URL ${url} not accessible${statusMsg}${contentTypeMsg}${errorMsg}`
);
} }
} catch (error) { } catch (error) {
console.log(`✗ URL ${url} check failed: ${error.message}`); console.log(`✗ URL ${url} check failed: ${error.message}`);
@@ -191,9 +177,7 @@ async function checkUrlAccessible(url, maxRetries = 10, initialDelay = 1000) {
if (attempt < maxRetries - 1) { if (attempt < maxRetries - 1) {
const delay = initialDelay * Math.pow(2, attempt); const delay = initialDelay * Math.pow(2, attempt);
console.log( console.log(` Retrying in ${delay}ms... (attempt ${attempt + 1}/${maxRetries})`);
` Retrying in ${delay}ms... (attempt ${attempt + 1}/${maxRetries})`
);
await new Promise((resolve) => setTimeout(resolve, delay)); await new Promise((resolve) => setTimeout(resolve, delay));
} }
} }
@@ -207,12 +191,7 @@ async function downloadFromGitHub(url, outputPath) {
const statusCode = response.statusCode; const statusCode = response.statusCode;
// Follow redirects (all redirect types) // Follow redirects (all redirect types)
if ( if (statusCode === 301 || statusCode === 302 || statusCode === 307 || statusCode === 308) {
statusCode === 301 ||
statusCode === 302 ||
statusCode === 307 ||
statusCode === 308
) {
const redirectUrl = response.headers.location; const redirectUrl = response.headers.location;
response.destroy(); response.destroy();
if (!redirectUrl) { if (!redirectUrl) {
@@ -220,39 +199,33 @@ async function downloadFromGitHub(url, outputPath) {
return; return;
} }
// Resolve relative redirects // Resolve relative redirects
const finalRedirectUrl = redirectUrl.startsWith("http") const finalRedirectUrl = redirectUrl.startsWith('http')
? redirectUrl ? redirectUrl
: new URL(redirectUrl, url).href; : new URL(redirectUrl, url).href;
console.log(` Following redirect: ${finalRedirectUrl}`); console.log(` Following redirect: ${finalRedirectUrl}`);
return downloadFromGitHub(finalRedirectUrl, outputPath) return downloadFromGitHub(finalRedirectUrl, outputPath).then(resolve).catch(reject);
.then(resolve)
.catch(reject);
} }
if (statusCode !== 200) { if (statusCode !== 200) {
response.destroy(); response.destroy();
reject( reject(new Error(`Failed to download ${url}: ${statusCode} ${response.statusMessage}`));
new Error(
`Failed to download ${url}: ${statusCode} ${response.statusMessage}`
)
);
return; return;
} }
const fileStream = fs.createWriteStream(outputPath); const fileStream = fs.createWriteStream(outputPath);
response.pipe(fileStream); response.pipe(fileStream);
fileStream.on("finish", () => { fileStream.on('finish', () => {
fileStream.close(); fileStream.close();
resolve(); resolve();
}); });
fileStream.on("error", (error) => { fileStream.on('error', (error) => {
response.destroy(); response.destroy();
reject(error); reject(error);
}); });
}); });
request.on("error", reject); request.on('error', reject);
request.on("timeout", () => { request.on('timeout', () => {
request.destroy(); request.destroy();
reject(new Error(`Request timeout for ${url}`)); reject(new Error(`Request timeout for ${url}`));
}); });
@@ -260,8 +233,8 @@ async function downloadFromGitHub(url, outputPath) {
} }
async function main() { async function main() {
const artifactsDir = "artifacts"; const artifactsDir = 'artifacts';
const tempDir = path.join(artifactsDir, "temp"); const tempDir = path.join(artifactsDir, 'temp');
// Create temp directory for downloaded GitHub archives // Create temp directory for downloaded GitHub archives
if (!fs.existsSync(tempDir)) { if (!fs.existsSync(tempDir)) {
@@ -292,40 +265,30 @@ async function main() {
// Find all artifacts // Find all artifacts
const artifacts = { const artifacts = {
windows: findArtifacts(path.join(artifactsDir, "windows-builds"), /\.exe$/), windows: findArtifacts(path.join(artifactsDir, 'windows-builds'), /\.exe$/),
macos: findArtifacts(path.join(artifactsDir, "macos-builds"), /-x64\.dmg$/), macos: findArtifacts(path.join(artifactsDir, 'macos-builds'), /-x64\.dmg$/),
macosArm: findArtifacts( macosArm: findArtifacts(path.join(artifactsDir, 'macos-builds'), /-arm64\.dmg$/),
path.join(artifactsDir, "macos-builds"), linux: findArtifacts(path.join(artifactsDir, 'linux-builds'), /\.AppImage$/),
/-arm64\.dmg$/
),
linux: findArtifacts(
path.join(artifactsDir, "linux-builds"),
/\.AppImage$/
),
sourceZip: [sourceZipPath], sourceZip: [sourceZipPath],
sourceTarGz: [sourceTarGzPath], sourceTarGz: [sourceTarGzPath],
}; };
console.log("Found artifacts:"); console.log('Found artifacts:');
for (const [platform, files] of Object.entries(artifacts)) { for (const [platform, files] of Object.entries(artifacts)) {
console.log( console.log(
` ${platform}: ${ ` ${platform}: ${files.length > 0 ? files.map((f) => path.basename(f)).join(', ') : 'none'}`
files.length > 0
? files.map((f) => path.basename(f)).join(", ")
: "none"
}`
); );
} }
// Upload each artifact to R2 // Upload each artifact to R2
const assets = {}; const assets = {};
const contentTypes = { const contentTypes = {
windows: "application/x-msdownload", windows: 'application/x-msdownload',
macos: "application/x-apple-diskimage", macos: 'application/x-apple-diskimage',
macosArm: "application/x-apple-diskimage", macosArm: 'application/x-apple-diskimage',
linux: "application/x-executable", linux: 'application/x-executable',
sourceZip: "application/zip", sourceZip: 'application/zip',
sourceTarGz: "application/gzip", sourceTarGz: 'application/gzip',
}; };
for (const [platform, files] of Object.entries(artifacts)) { for (const [platform, files] of Object.entries(artifacts)) {
@@ -345,11 +308,11 @@ async function main() {
filename, filename,
size, size,
arch: arch:
platform === "macosArm" platform === 'macosArm'
? "arm64" ? 'arm64'
: platform === "sourceZip" || platform === "sourceTarGz" : platform === 'sourceZip' || platform === 'sourceTarGz'
? "source" ? 'source'
: "x64", : 'x64',
}; };
} }
@@ -364,9 +327,7 @@ async function main() {
}; };
// Remove existing entry for this version if re-running // Remove existing entry for this version if re-running
releasesData.releases = releasesData.releases.filter( releasesData.releases = releasesData.releases.filter((r) => r.version !== VERSION);
(r) => r.version !== VERSION
);
// Prepend new release // Prepend new release
releasesData.releases.unshift(newRelease); releasesData.releases.unshift(newRelease);
@@ -376,19 +337,19 @@ async function main() {
await s3Client.send( await s3Client.send(
new PutObjectCommand({ new PutObjectCommand({
Bucket: BUCKET, Bucket: BUCKET,
Key: "releases.json", Key: 'releases.json',
Body: JSON.stringify(releasesData, null, 2), Body: JSON.stringify(releasesData, null, 2),
ContentType: "application/json", ContentType: 'application/json',
CacheControl: "public, max-age=60", CacheControl: 'public, max-age=60',
}) })
); );
console.log("Successfully updated releases.json"); console.log('Successfully updated releases.json');
console.log(`Latest version: ${VERSION}`); console.log(`Latest version: ${VERSION}`);
console.log(`Total releases: ${releasesData.releases.length}`); console.log(`Total releases: ${releasesData.releases.length}`);
} }
main().catch((err) => { main().catch((err) => {
console.error("Failed to upload to R2:", err); console.error('Failed to upload to R2:', err);
process.exit(1); process.exit(1);
}); });

View File

@@ -47,4 +47,3 @@ jobs:
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md # 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 # or https://code.claude.com/docs/en/cli-reference for available options
# claude_args: '--allowed-tools Bash(gh pr:*)' # claude_args: '--allowed-tools Bash(gh pr:*)'

View File

@@ -3,7 +3,7 @@ name: E2E Tests
on: on:
pull_request: pull_request:
branches: branches:
- "*" - '*'
push: push:
branches: branches:
- main - main
@@ -21,8 +21,8 @@ jobs:
- name: Setup project - name: Setup project
uses: ./.github/actions/setup-project uses: ./.github/actions/setup-project
with: with:
check-lockfile: "true" check-lockfile: 'true'
rebuild-node-pty-path: "apps/server" rebuild-node-pty-path: 'apps/server'
- name: Install Playwright browsers - name: Install Playwright browsers
run: npx playwright install --with-deps chromium run: npx playwright install --with-deps chromium
@@ -58,7 +58,7 @@ jobs:
env: env:
CI: true CI: true
VITE_SERVER_URL: http://localhost:3008 VITE_SERVER_URL: http://localhost:3008
VITE_SKIP_SETUP: "true" VITE_SKIP_SETUP: 'true'
- name: Upload Playwright report - name: Upload Playwright report
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4

View File

@@ -3,7 +3,7 @@ name: PR Build Check
on: on:
pull_request: pull_request:
branches: branches:
- "*" - '*'
push: push:
branches: branches:
- main - main
@@ -20,7 +20,7 @@ jobs:
- name: Setup project - name: Setup project
uses: ./.github/actions/setup-project uses: ./.github/actions/setup-project
with: with:
check-lockfile: "true" check-lockfile: 'true'
- name: Run build:electron (dir only - faster CI) - name: Run build:electron (dir only - faster CI)
run: npm run build:electron:dir run: npm run build:electron:dir

View File

@@ -3,7 +3,7 @@ name: Test Suite
on: on:
pull_request: pull_request:
branches: branches:
- "*" - '*'
push: push:
branches: branches:
- main - main
@@ -20,8 +20,8 @@ jobs:
- name: Setup project - name: Setup project
uses: ./.github/actions/setup-project uses: ./.github/actions/setup-project
with: with:
check-lockfile: "true" check-lockfile: 'true'
rebuild-node-pty-path: "apps/server" rebuild-node-pty-path: 'apps/server'
- name: Run package tests - name: Run package tests
run: npm run test:packages run: npm run test:packages

View File

@@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts"; import './.next/dev/types/routes.d.ts';
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -299,11 +299,34 @@ terminalWss.on('connection', (ws: WebSocket, req: import('http').IncomingMessage
switch (msg.type) { switch (msg.type) {
case 'input': 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 // Write user input to terminal
terminalService.write(sessionId, msg.data); terminalService.write(sessionId, msg.data);
break; break;
case 'resize': 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 // Resize terminal with deduplication and rate limiting
if (msg.cols && msg.rows) { if (msg.cols && msg.rows) {
const now = Date.now(); const now = Date.now();

View File

@@ -6,26 +6,26 @@
*/ */
// Import and re-export spec types from shared package // Import and re-export spec types from shared package
export type { SpecOutput } from "@automaker/types"; export type { SpecOutput } from '@automaker/types';
export { specOutputSchema } from "@automaker/types"; export { specOutputSchema } from '@automaker/types';
/** /**
* Escape special XML characters * Escape special XML characters
*/ */
function escapeXml(str: string): string { function escapeXml(str: string): string {
return str return str
.replace(/&/g, "&amp;") .replace(/&/g, '&amp;')
.replace(/</g, "&lt;") .replace(/</g, '&lt;')
.replace(/>/g, "&gt;") .replace(/>/g, '&gt;')
.replace(/"/g, "&quot;") .replace(/"/g, '&quot;')
.replace(/'/g, "&apos;"); .replace(/'/g, '&apos;');
} }
/** /**
* Convert structured spec output to XML format * Convert structured spec output to XML format
*/ */
export function specToXml(spec: import("@automaker/types").SpecOutput): string { export function specToXml(spec: import('@automaker/types').SpecOutput): string {
const indent = " "; const indent = ' ';
let xml = `<?xml version="1.0" encoding="UTF-8"?> let xml = `<?xml version="1.0" encoding="UTF-8"?>
<project_specification> <project_specification>
@@ -36,11 +36,11 @@ ${indent}${indent}${escapeXml(spec.overview)}
${indent}</overview> ${indent}</overview>
${indent}<technology_stack> ${indent}<technology_stack>
${spec.technology_stack.map((t) => `${indent}${indent}<technology>${escapeXml(t)}</technology>`).join("\n")} ${spec.technology_stack.map((t) => `${indent}${indent}<technology>${escapeXml(t)}</technology>`).join('\n')}
${indent}</technology_stack> ${indent}</technology_stack>
${indent}<core_capabilities> ${indent}<core_capabilities>
${spec.core_capabilities.map((c) => `${indent}${indent}<capability>${escapeXml(c)}</capability>`).join("\n")} ${spec.core_capabilities.map((c) => `${indent}${indent}<capability>${escapeXml(c)}</capability>`).join('\n')}
${indent}</core_capabilities> ${indent}</core_capabilities>
${indent}<implemented_features> ${indent}<implemented_features>
@@ -51,13 +51,13 @@ ${indent}${indent}${indent}<name>${escapeXml(f.name)}</name>
${indent}${indent}${indent}<description>${escapeXml(f.description)}</description>${ ${indent}${indent}${indent}<description>${escapeXml(f.description)}</description>${
f.file_locations && f.file_locations.length > 0 f.file_locations && f.file_locations.length > 0
? `\n${indent}${indent}${indent}<file_locations> ? `\n${indent}${indent}${indent}<file_locations>
${f.file_locations.map((loc) => `${indent}${indent}${indent}${indent}<location>${escapeXml(loc)}</location>`).join("\n")} ${f.file_locations.map((loc) => `${indent}${indent}${indent}${indent}<location>${escapeXml(loc)}</location>`).join('\n')}
${indent}${indent}${indent}</file_locations>` ${indent}${indent}${indent}</file_locations>`
: "" : ''
} }
${indent}${indent}</feature>` ${indent}${indent}</feature>`
) )
.join("\n")} .join('\n')}
${indent}</implemented_features>`; ${indent}</implemented_features>`;
// Optional sections // Optional sections
@@ -65,7 +65,7 @@ ${indent}</implemented_features>`;
xml += ` xml += `
${indent}<additional_requirements> ${indent}<additional_requirements>
${spec.additional_requirements.map((r) => `${indent}${indent}<requirement>${escapeXml(r)}</requirement>`).join("\n")} ${spec.additional_requirements.map((r) => `${indent}${indent}<requirement>${escapeXml(r)}</requirement>`).join('\n')}
${indent}</additional_requirements>`; ${indent}</additional_requirements>`;
} }
@@ -73,7 +73,7 @@ ${indent}</additional_requirements>`;
xml += ` xml += `
${indent}<development_guidelines> ${indent}<development_guidelines>
${spec.development_guidelines.map((g) => `${indent}${indent}<guideline>${escapeXml(g)}</guideline>`).join("\n")} ${spec.development_guidelines.map((g) => `${indent}${indent}<guideline>${escapeXml(g)}</guideline>`).join('\n')}
${indent}</development_guidelines>`; ${indent}</development_guidelines>`;
} }
@@ -89,7 +89,7 @@ ${indent}${indent}${indent}<status>${escapeXml(r.status)}</status>
${indent}${indent}${indent}<description>${escapeXml(r.description)}</description> ${indent}${indent}${indent}<description>${escapeXml(r.description)}</description>
${indent}${indent}</phase>` ${indent}${indent}</phase>`
) )
.join("\n")} .join('\n')}
${indent}</implementation_roadmap>`; ${indent}</implementation_roadmap>`;
} }

View File

@@ -4,7 +4,7 @@
* Supports API key authentication via header or environment variable. * 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) // API key from environment (optional - if not set, auth is disabled)
const API_KEY = process.env.AUTOMAKER_API_KEY; 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 // 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) { if (!providedKey) {
res.status(401).json({ res.status(401).json({
success: false, success: false,
error: "Authentication required. Provide X-API-Key header.", error: 'Authentication required. Provide X-API-Key header.',
}); });
return; return;
} }
@@ -36,7 +36,7 @@ export function authMiddleware(req: Request, res: Response, next: NextFunction):
if (providedKey !== API_KEY) { if (providedKey !== API_KEY) {
res.status(403).json({ res.status(403).json({
success: false, success: false,
error: "Invalid API key.", error: 'Invalid API key.',
}); });
return; return;
} }
@@ -57,6 +57,6 @@ export function isAuthEnabled(): boolean {
export function getAuthStatus(): { enabled: boolean; method: string } { export function getAuthStatus(): { enabled: boolean; method: string } {
return { return {
enabled: !!API_KEY, enabled: !!API_KEY,
method: API_KEY ? "api_key" : "none", method: API_KEY ? 'api_key' : 'none',
}; };
} }

View File

@@ -2,7 +2,7 @@
* Event emitter for streaming events to WebSocket clients * 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 // Re-export event types from shared package
export type { EventType, EventCallback }; export type { EventType, EventCallback };
@@ -21,7 +21,7 @@ export function createEventEmitter(): EventEmitter {
try { try {
callback(type, payload); callback(type, payload);
} catch (error) { } catch (error) {
console.error("Error in event subscriber:", error); console.error('Error in event subscriber:', error);
} }
} }
}, },

View File

@@ -3,7 +3,7 @@
* This file exists for backward compatibility with existing imports * This file exists for backward compatibility with existing imports
*/ */
import { secureFs } from "@automaker/platform"; import { secureFs } from '@automaker/platform';
export const { export const {
access, access,

View File

@@ -4,8 +4,8 @@
* try-catch block in every route handler * try-catch block in every route handler
*/ */
import type { Request, Response, NextFunction } from "express"; import type { Request, Response, NextFunction } from 'express';
import { validatePath, PathNotAllowedError } from "@automaker/platform"; import { validatePath, PathNotAllowedError } from '@automaker/platform';
/** /**
* Creates a middleware that validates specified path parameters in req.body * Creates a middleware that validates specified path parameters in req.body
@@ -24,7 +24,7 @@ export function validatePathParams(...paramNames: string[]) {
try { try {
for (const paramName of paramNames) { for (const paramName of paramNames) {
// Handle optional parameters (paramName?) // Handle optional parameters (paramName?)
if (paramName.endsWith("?")) { if (paramName.endsWith('?')) {
const actualName = paramName.slice(0, -1); const actualName = paramName.slice(0, -1);
const value = req.body[actualName]; const value = req.body[actualName];
if (value) { if (value) {
@@ -34,7 +34,7 @@ export function validatePathParams(...paramNames: string[]) {
} }
// Handle array parameters (paramName[]) // Handle array parameters (paramName[])
if (paramName.endsWith("[]")) { if (paramName.endsWith('[]')) {
const actualName = paramName.slice(0, -2); const actualName = paramName.slice(0, -2);
const values = req.body[actualName]; const values = req.body[actualName];
if (Array.isArray(values) && values.length > 0) { if (Array.isArray(values) && values.length > 0) {

View File

@@ -9,7 +9,7 @@ import type {
InstallationStatus, InstallationStatus,
ValidationResult, ValidationResult,
ModelDefinition, ModelDefinition,
} from "./types.js"; } from './types.js';
/** /**
* Base provider class that all provider implementations must extend * Base provider class that all provider implementations must extend
@@ -33,9 +33,7 @@ export abstract class BaseProvider {
* @param options Execution options * @param options Execution options
* @returns AsyncGenerator yielding provider messages * @returns AsyncGenerator yielding provider messages
*/ */
abstract executeQuery( abstract executeQuery(options: ExecuteOptions): AsyncGenerator<ProviderMessage>;
options: ExecuteOptions
): AsyncGenerator<ProviderMessage>;
/** /**
* Detect if the provider is installed and configured * Detect if the provider is installed and configured
@@ -59,7 +57,7 @@ export abstract class BaseProvider {
// Base validation (can be overridden) // Base validation (can be overridden)
if (!this.config) { if (!this.config) {
errors.push("Provider config is missing"); errors.push('Provider config is missing');
} }
return { return {
@@ -76,7 +74,7 @@ export abstract class BaseProvider {
*/ */
supportsFeature(feature: string): boolean { supportsFeature(feature: string): boolean {
// Default implementation - override in subclasses // Default implementation - override in subclasses
const commonFeatures = ["tools", "text"]; const commonFeatures = ['tools', 'text'];
return commonFeatures.includes(feature); return commonFeatures.includes(feature);
} }

View File

@@ -6,9 +6,9 @@
* new providers (Cursor, OpenCode, etc.) trivial - just add one line. * new providers (Cursor, OpenCode, etc.) trivial - just add one line.
*/ */
import { BaseProvider } from "./base-provider.js"; import { BaseProvider } from './base-provider.js';
import { ClaudeProvider } from "./claude-provider.js"; import { ClaudeProvider } from './claude-provider.js';
import type { InstallationStatus } from "./types.js"; import type { InstallationStatus } from './types.js';
export class ProviderFactory { export class ProviderFactory {
/** /**
@@ -21,10 +21,7 @@ export class ProviderFactory {
const lowerModel = modelId.toLowerCase(); const lowerModel = modelId.toLowerCase();
// Claude models (claude-*, opus, sonnet, haiku) // Claude models (claude-*, opus, sonnet, haiku)
if ( if (lowerModel.startsWith('claude-') || ['haiku', 'sonnet', 'opus'].includes(lowerModel)) {
lowerModel.startsWith("claude-") ||
["haiku", "sonnet", "opus"].includes(lowerModel)
) {
return new ClaudeProvider(); return new ClaudeProvider();
} }
@@ -37,9 +34,7 @@ export class ProviderFactory {
// } // }
// Default to Claude for unknown models // Default to Claude for unknown models
console.warn( console.warn(`[ProviderFactory] Unknown model prefix for "${modelId}", defaulting to Claude`);
`[ProviderFactory] Unknown model prefix for "${modelId}", defaulting to Claude`
);
return new ClaudeProvider(); return new ClaudeProvider();
} }
@@ -58,9 +53,7 @@ export class ProviderFactory {
* *
* @returns Map of provider name to installation status * @returns Map of provider name to installation status
*/ */
static async checkAllProviders(): Promise< static async checkAllProviders(): Promise<Record<string, InstallationStatus>> {
Record<string, InstallationStatus>
> {
const providers = this.getAllProviders(); const providers = this.getAllProviders();
const statuses: Record<string, InstallationStatus> = {}; const statuses: Record<string, InstallationStatus> = {};
@@ -83,8 +76,8 @@ export class ProviderFactory {
const lowerName = name.toLowerCase(); const lowerName = name.toLowerCase();
switch (lowerName) { switch (lowerName) {
case "claude": case 'claude':
case "anthropic": case 'anthropic':
return new ClaudeProvider(); return new ClaudeProvider();
// Future providers: // Future providers:

View File

@@ -15,7 +15,7 @@ export interface ProviderConfig {
* Message in conversation history * Message in conversation history
*/ */
export interface ConversationMessage { export interface ConversationMessage {
role: "user" | "assistant"; role: 'user' | 'assistant';
content: string | Array<{ type: string; text?: string; source?: object }>; 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) * Content block in a provider message (matches Claude SDK format)
*/ */
export interface ContentBlock { export interface ContentBlock {
type: "text" | "tool_use" | "thinking" | "tool_result"; type: 'text' | 'tool_use' | 'thinking' | 'tool_result';
text?: string; text?: string;
thinking?: string; thinking?: string;
name?: string; name?: string;
@@ -52,11 +52,11 @@ export interface ContentBlock {
* Message returned by a provider (matches Claude SDK streaming format) * Message returned by a provider (matches Claude SDK streaming format)
*/ */
export interface ProviderMessage { export interface ProviderMessage {
type: "assistant" | "user" | "error" | "result"; type: 'assistant' | 'user' | 'error' | 'result';
subtype?: "success" | "error"; subtype?: 'success' | 'error';
session_id?: string; session_id?: string;
message?: { message?: {
role: "user" | "assistant"; role: 'user' | 'assistant';
content: ContentBlock[]; content: ContentBlock[];
}; };
result?: string; result?: string;
@@ -71,7 +71,7 @@ export interface InstallationStatus {
installed: boolean; installed: boolean;
path?: string; path?: string;
version?: string; version?: string;
method?: "cli" | "npm" | "brew" | "sdk"; method?: 'cli' | 'npm' | 'brew' | 'sdk';
hasApiKey?: boolean; hasApiKey?: boolean;
authenticated?: boolean; authenticated?: boolean;
error?: string; error?: string;
@@ -99,6 +99,6 @@ export interface ModelDefinition {
maxOutputTokens?: number; maxOutputTokens?: number;
supportsVision?: boolean; supportsVision?: boolean;
supportsTools?: boolean; supportsTools?: boolean;
tier?: "basic" | "standard" | "premium"; tier?: 'basic' | 'standard' | 'premium';
default?: boolean; default?: boolean;
} }

View File

@@ -2,13 +2,10 @@
* Common utilities for agent routes * Common utilities for agent routes
*/ */
import { createLogger } from "@automaker/utils"; import { createLogger } from '@automaker/utils';
import { import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
getErrorMessage as getErrorMessageShared,
createLogError,
} from "../common.js";
const logger = createLogger("Agent"); const logger = createLogger('Agent');
// Re-export shared utilities // Re-export shared utilities
export { getErrorMessageShared as getErrorMessage }; export { getErrorMessageShared as getErrorMessage };

View File

@@ -2,29 +2,30 @@
* Agent routes - HTTP API for Claude agent interactions * Agent routes - HTTP API for Claude agent interactions
*/ */
import { Router } from "express"; import { Router } from 'express';
import { AgentService } from "../../services/agent-service.js"; import { AgentService } from '../../services/agent-service.js';
import type { EventEmitter } from "../../lib/events.js"; import type { EventEmitter } from '../../lib/events.js';
import { validatePathParams } from "../../middleware/validate-paths.js"; import { validatePathParams } from '../../middleware/validate-paths.js';
import { createStartHandler } from "./routes/start.js"; import { createStartHandler } from './routes/start.js';
import { createSendHandler } from "./routes/send.js"; import { createSendHandler } from './routes/send.js';
import { createHistoryHandler } from "./routes/history.js"; import { createHistoryHandler } from './routes/history.js';
import { createStopHandler } from "./routes/stop.js"; import { createStopHandler } from './routes/stop.js';
import { createClearHandler } from "./routes/clear.js"; import { createClearHandler } from './routes/clear.js';
import { createModelHandler } from "./routes/model.js"; import { createModelHandler } from './routes/model.js';
export function createAgentRoutes( export function createAgentRoutes(agentService: AgentService, _events: EventEmitter): Router {
agentService: AgentService,
_events: EventEmitter
): Router {
const router = Router(); const router = Router();
router.post("/start", validatePathParams("workingDirectory?"), createStartHandler(agentService)); router.post('/start', validatePathParams('workingDirectory?'), createStartHandler(agentService));
router.post("/send", validatePathParams("workingDirectory?", "imagePaths[]"), createSendHandler(agentService)); router.post(
router.post("/history", createHistoryHandler(agentService)); '/send',
router.post("/stop", createStopHandler(agentService)); validatePathParams('workingDirectory?', 'imagePaths[]'),
router.post("/clear", createClearHandler(agentService)); createSendHandler(agentService)
router.post("/model", createModelHandler(agentService)); );
router.post('/history', createHistoryHandler(agentService));
router.post('/stop', createStopHandler(agentService));
router.post('/clear', createClearHandler(agentService));
router.post('/model', createModelHandler(agentService));
return router; return router;
} }

View File

@@ -2,9 +2,9 @@
* POST /clear endpoint - Clear conversation * POST /clear endpoint - Clear conversation
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import { AgentService } from "../../../services/agent-service.js"; import { AgentService } from '../../../services/agent-service.js';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
export function createClearHandler(agentService: AgentService) { export function createClearHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
@@ -12,16 +12,14 @@ export function createClearHandler(agentService: AgentService) {
const { sessionId } = req.body as { sessionId: string }; const { sessionId } = req.body as { sessionId: string };
if (!sessionId) { if (!sessionId) {
res res.status(400).json({ success: false, error: 'sessionId is required' });
.status(400)
.json({ success: false, error: "sessionId is required" });
return; return;
} }
const result = await agentService.clearSession(sessionId); const result = await agentService.clearSession(sessionId);
res.json(result); res.json(result);
} catch (error) { } catch (error) {
logError(error, "Clear session failed"); logError(error, 'Clear session failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,9 +2,9 @@
* POST /history endpoint - Get conversation history * POST /history endpoint - Get conversation history
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import { AgentService } from "../../../services/agent-service.js"; import { AgentService } from '../../../services/agent-service.js';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
export function createHistoryHandler(agentService: AgentService) { export function createHistoryHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
@@ -12,16 +12,14 @@ export function createHistoryHandler(agentService: AgentService) {
const { sessionId } = req.body as { sessionId: string }; const { sessionId } = req.body as { sessionId: string };
if (!sessionId) { if (!sessionId) {
res res.status(400).json({ success: false, error: 'sessionId is required' });
.status(400)
.json({ success: false, error: "sessionId is required" });
return; return;
} }
const result = agentService.getHistory(sessionId); const result = agentService.getHistory(sessionId);
res.json(result); res.json(result);
} catch (error) { } catch (error) {
logError(error, "Get history failed"); logError(error, 'Get history failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,9 +2,9 @@
* POST /model endpoint - Set session model * POST /model endpoint - Set session model
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import { AgentService } from "../../../services/agent-service.js"; import { AgentService } from '../../../services/agent-service.js';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
export function createModelHandler(agentService: AgentService) { export function createModelHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
@@ -15,16 +15,14 @@ export function createModelHandler(agentService: AgentService) {
}; };
if (!sessionId || !model) { if (!sessionId || !model) {
res res.status(400).json({ success: false, error: 'sessionId and model are required' });
.status(400)
.json({ success: false, error: "sessionId and model are required" });
return; return;
} }
const result = await agentService.setSessionModel(sessionId, model); const result = await agentService.setSessionModel(sessionId, model);
res.json({ success: result }); res.json({ success: result });
} catch (error) { } catch (error) {
logError(error, "Set session model failed"); logError(error, 'Set session model failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,28 +2,27 @@
* POST /send endpoint - Send a message * POST /send endpoint - Send a message
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import { AgentService } from "../../../services/agent-service.js"; import { AgentService } from '../../../services/agent-service.js';
import { createLogger } from "@automaker/utils"; import { createLogger } from '@automaker/utils';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
const logger = createLogger("Agent"); const logger = createLogger('Agent');
export function createSendHandler(agentService: AgentService) { export function createSendHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
try { try {
const { sessionId, message, workingDirectory, imagePaths, model } = const { sessionId, message, workingDirectory, imagePaths, model } = req.body as {
req.body as { sessionId: string;
sessionId: string; message: string;
message: string; workingDirectory?: string;
workingDirectory?: string; imagePaths?: string[];
imagePaths?: string[]; model?: string;
model?: string; };
};
if (!sessionId || !message) { if (!sessionId || !message) {
res.status(400).json({ res.status(400).json({
success: false, success: false,
error: "sessionId and message are required", error: 'sessionId and message are required',
}); });
return; return;
} }
@@ -38,13 +37,13 @@ export function createSendHandler(agentService: AgentService) {
model, model,
}) })
.catch((error) => { .catch((error) => {
logError(error, "Send message failed (background)"); logError(error, 'Send message failed (background)');
}); });
// Return immediately - responses come via WebSocket // Return immediately - responses come via WebSocket
res.json({ success: true, message: "Message sent" }); res.json({ success: true, message: 'Message sent' });
} catch (error) { } catch (error) {
logError(error, "Send message failed"); logError(error, 'Send message failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,11 +2,11 @@
* POST /start endpoint - Start a conversation * POST /start endpoint - Start a conversation
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import { AgentService } from "../../../services/agent-service.js"; import { AgentService } from '../../../services/agent-service.js';
import { createLogger } from "@automaker/utils"; import { createLogger } from '@automaker/utils';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
const logger = createLogger("Agent"); const logger = createLogger('Agent');
export function createStartHandler(agentService: AgentService) { export function createStartHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
@@ -17,9 +17,7 @@ export function createStartHandler(agentService: AgentService) {
}; };
if (!sessionId) { if (!sessionId) {
res res.status(400).json({ success: false, error: 'sessionId is required' });
.status(400)
.json({ success: false, error: "sessionId is required" });
return; return;
} }
@@ -30,7 +28,7 @@ export function createStartHandler(agentService: AgentService) {
res.json(result); res.json(result);
} catch (error) { } catch (error) {
logError(error, "Start conversation failed"); logError(error, 'Start conversation failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,9 +2,9 @@
* POST /stop endpoint - Stop execution * POST /stop endpoint - Stop execution
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import { AgentService } from "../../../services/agent-service.js"; import { AgentService } from '../../../services/agent-service.js';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
export function createStopHandler(agentService: AgentService) { export function createStopHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
@@ -12,16 +12,14 @@ export function createStopHandler(agentService: AgentService) {
const { sessionId } = req.body as { sessionId: string }; const { sessionId } = req.body as { sessionId: string };
if (!sessionId) { if (!sessionId) {
res res.status(400).json({ success: false, error: 'sessionId is required' });
.status(400)
.json({ success: false, error: "sessionId is required" });
return; return;
} }
const result = await agentService.stopExecution(sessionId); const result = await agentService.stopExecution(sessionId);
res.json(result); res.json(result);
} catch (error) { } catch (error) {
logError(error, "Stop execution failed"); logError(error, 'Stop execution failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,9 +2,9 @@
* Common utilities and state management for spec regeneration * 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 // Shared state for tracking generation status - private
let isRunning = false; let isRunning = false;
@@ -23,10 +23,7 @@ export function getSpecRegenerationStatus(): {
/** /**
* Set the running state and abort controller * Set the running state and abort controller
*/ */
export function setRunningState( export function setRunningState(running: boolean, controller: AbortController | null = null): void {
running: boolean,
controller: AbortController | null = null
): void {
isRunning = running; isRunning = running;
currentAbortController = controller; currentAbortController = controller;
} }
@@ -40,14 +37,12 @@ export function logAuthStatus(context: string): void {
logger.info(`${context} - Auth Status:`); logger.info(`${context} - Auth Status:`);
logger.info( logger.info(
` ANTHROPIC_API_KEY: ${ ` ANTHROPIC_API_KEY: ${
hasApiKey hasApiKey ? 'SET (' + process.env.ANTHROPIC_API_KEY?.substring(0, 20) + '...)' : 'NOT SET'
? "SET (" + process.env.ANTHROPIC_API_KEY?.substring(0, 20) + "...)"
: "NOT SET"
}` }`
); );
if (!hasApiKey) { 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 { export function logError(error: unknown, context: string): void {
logger.error(`${context}:`); logger.error(`${context}:`);
logger.error("Error name:", (error as any)?.name); logger.error('Error name:', (error as any)?.name);
logger.error("Error message:", (error as Error)?.message); logger.error('Error message:', (error as Error)?.message);
logger.error("Error stack:", (error as Error)?.stack); logger.error('Error stack:', (error as Error)?.stack);
logger.error( logger.error('Full error object:', JSON.stringify(error, Object.getOwnPropertyNames(error), 2));
"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 // Re-export shared utility
export { getErrorMessageShared as getErrorMessage }; export { getErrorMessageShared as getErrorMessage };

View File

@@ -2,25 +2,22 @@
* Spec Regeneration routes - HTTP API for AI-powered spec generation * Spec Regeneration routes - HTTP API for AI-powered spec generation
*/ */
import { Router } from "express"; import { Router } from 'express';
import type { EventEmitter } from "../../lib/events.js"; import type { EventEmitter } from '../../lib/events.js';
import { createCreateHandler } from "./routes/create.js"; import { createCreateHandler } from './routes/create.js';
import { createGenerateHandler } from "./routes/generate.js"; import { createGenerateHandler } from './routes/generate.js';
import { createGenerateFeaturesHandler } from "./routes/generate-features.js"; import { createGenerateFeaturesHandler } from './routes/generate-features.js';
import { createStopHandler } from "./routes/stop.js"; import { createStopHandler } from './routes/stop.js';
import { createStatusHandler } from "./routes/status.js"; import { createStatusHandler } from './routes/status.js';
export function createSpecRegenerationRoutes(events: EventEmitter): Router { export function createSpecRegenerationRoutes(events: EventEmitter): Router {
const router = Router(); const router = Router();
router.post("/create", createCreateHandler(events)); router.post('/create', createCreateHandler(events));
router.post("/generate", createGenerateHandler(events)); router.post('/generate', createGenerateHandler(events));
router.post("/generate-features", createGenerateFeaturesHandler(events)); router.post('/generate-features', createGenerateFeaturesHandler(events));
router.post("/stop", createStopHandler()); router.post('/stop', createStopHandler());
router.get("/status", createStatusHandler()); router.get('/status', createStatusHandler());
return router; return router;
} }

View File

@@ -2,24 +2,24 @@
* POST /create endpoint - Create project spec from overview * POST /create endpoint - Create project spec from overview
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import type { EventEmitter } from "../../../lib/events.js"; import type { EventEmitter } from '../../../lib/events.js';
import { createLogger } from "@automaker/utils"; import { createLogger } from '@automaker/utils';
import { import {
getSpecRegenerationStatus, getSpecRegenerationStatus,
setRunningState, setRunningState,
logAuthStatus, logAuthStatus,
logError, logError,
getErrorMessage, getErrorMessage,
} from "../common.js"; } from '../common.js';
import { generateSpec } from "../generate-spec.js"; import { generateSpec } from '../generate-spec.js';
const logger = createLogger("SpecRegeneration"); const logger = createLogger('SpecRegeneration');
export function createCreateHandler(events: EventEmitter) { export function createCreateHandler(events: EventEmitter) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
logger.info("========== /create endpoint called =========="); logger.info('========== /create endpoint called ==========');
logger.debug("Request body:", JSON.stringify(req.body, null, 2)); logger.debug('Request body:', JSON.stringify(req.body, null, 2));
try { try {
const { projectPath, projectOverview, generateFeatures, analyzeProject, maxFeatures } = const { projectPath, projectOverview, generateFeatures, analyzeProject, maxFeatures } =
@@ -31,37 +31,34 @@ export function createCreateHandler(events: EventEmitter) {
maxFeatures?: number; maxFeatures?: number;
}; };
logger.debug("Parsed params:"); logger.debug('Parsed params:');
logger.debug(" projectPath:", projectPath); logger.debug(' projectPath:', projectPath);
logger.debug( logger.debug(' projectOverview length:', `${projectOverview?.length || 0} chars`);
" projectOverview length:", logger.debug(' generateFeatures:', generateFeatures);
`${projectOverview?.length || 0} chars` logger.debug(' analyzeProject:', analyzeProject);
); logger.debug(' maxFeatures:', maxFeatures);
logger.debug(" generateFeatures:", generateFeatures);
logger.debug(" analyzeProject:", analyzeProject);
logger.debug(" maxFeatures:", maxFeatures);
if (!projectPath || !projectOverview) { if (!projectPath || !projectOverview) {
logger.error("Missing required parameters"); logger.error('Missing required parameters');
res.status(400).json({ res.status(400).json({
success: false, success: false,
error: "projectPath and projectOverview required", error: 'projectPath and projectOverview required',
}); });
return; return;
} }
const { isRunning } = getSpecRegenerationStatus(); const { isRunning } = getSpecRegenerationStatus();
if (isRunning) { if (isRunning) {
logger.warn("Generation already running, rejecting request"); logger.warn('Generation already running, rejecting request');
res.json({ success: false, error: "Spec generation already running" }); res.json({ success: false, error: 'Spec generation already running' });
return; return;
} }
logAuthStatus("Before starting generation"); logAuthStatus('Before starting generation');
const abortController = new AbortController(); const abortController = new AbortController();
setRunningState(true, abortController); setRunningState(true, abortController);
logger.info("Starting background generation task..."); logger.info('Starting background generation task...');
// Start generation in background // Start generation in background
generateSpec( generateSpec(
@@ -74,24 +71,22 @@ export function createCreateHandler(events: EventEmitter) {
maxFeatures maxFeatures
) )
.catch((error) => { .catch((error) => {
logError(error, "Generation failed with error"); logError(error, 'Generation failed with error');
events.emit("spec-regeneration:event", { events.emit('spec-regeneration:event', {
type: "spec_regeneration_error", type: 'spec_regeneration_error',
error: getErrorMessage(error), error: getErrorMessage(error),
projectPath: projectPath, projectPath: projectPath,
}); });
}) })
.finally(() => { .finally(() => {
logger.info("Generation task finished (success or error)"); logger.info('Generation task finished (success or error)');
setRunningState(false, null); setRunningState(false, null);
}); });
logger.info( logger.info('Returning success response (generation running in background)');
"Returning success response (generation running in background)"
);
res.json({ success: true }); res.json({ success: true });
} catch (error) { } 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) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,24 +2,24 @@
* POST /generate-features endpoint - Generate features from existing spec * POST /generate-features endpoint - Generate features from existing spec
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import type { EventEmitter } from "../../../lib/events.js"; import type { EventEmitter } from '../../../lib/events.js';
import { createLogger } from "@automaker/utils"; import { createLogger } from '@automaker/utils';
import { import {
getSpecRegenerationStatus, getSpecRegenerationStatus,
setRunningState, setRunningState,
logAuthStatus, logAuthStatus,
logError, logError,
getErrorMessage, getErrorMessage,
} from "../common.js"; } from '../common.js';
import { generateFeaturesFromSpec } from "../generate-features-from-spec.js"; import { generateFeaturesFromSpec } from '../generate-features-from-spec.js';
const logger = createLogger("SpecRegeneration"); const logger = createLogger('SpecRegeneration');
export function createGenerateFeaturesHandler(events: EventEmitter) { export function createGenerateFeaturesHandler(events: EventEmitter) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
logger.info("========== /generate-features endpoint called =========="); logger.info('========== /generate-features endpoint called ==========');
logger.debug("Request body:", JSON.stringify(req.body, null, 2)); logger.debug('Request body:', JSON.stringify(req.body, null, 2));
try { try {
const { projectPath, maxFeatures } = req.body as { const { projectPath, maxFeatures } = req.body as {
@@ -27,52 +27,45 @@ export function createGenerateFeaturesHandler(events: EventEmitter) {
maxFeatures?: number; maxFeatures?: number;
}; };
logger.debug("projectPath:", projectPath); logger.debug('projectPath:', projectPath);
logger.debug("maxFeatures:", maxFeatures); logger.debug('maxFeatures:', maxFeatures);
if (!projectPath) { if (!projectPath) {
logger.error("Missing projectPath parameter"); logger.error('Missing projectPath parameter');
res.status(400).json({ success: false, error: "projectPath required" }); res.status(400).json({ success: false, error: 'projectPath required' });
return; return;
} }
const { isRunning } = getSpecRegenerationStatus(); const { isRunning } = getSpecRegenerationStatus();
if (isRunning) { if (isRunning) {
logger.warn("Generation already running, rejecting request"); logger.warn('Generation already running, rejecting request');
res.json({ success: false, error: "Generation already running" }); res.json({ success: false, error: 'Generation already running' });
return; return;
} }
logAuthStatus("Before starting feature generation"); logAuthStatus('Before starting feature generation');
const abortController = new AbortController(); const abortController = new AbortController();
setRunningState(true, abortController); setRunningState(true, abortController);
logger.info("Starting background feature generation task..."); logger.info('Starting background feature generation task...');
generateFeaturesFromSpec( generateFeaturesFromSpec(projectPath, events, abortController, maxFeatures)
projectPath,
events,
abortController,
maxFeatures
)
.catch((error) => { .catch((error) => {
logError(error, "Feature generation failed with error"); logError(error, 'Feature generation failed with error');
events.emit("spec-regeneration:event", { events.emit('spec-regeneration:event', {
type: "features_error", type: 'features_error',
error: getErrorMessage(error), error: getErrorMessage(error),
}); });
}) })
.finally(() => { .finally(() => {
logger.info("Feature generation task finished (success or error)"); logger.info('Feature generation task finished (success or error)');
setRunningState(false, null); setRunningState(false, null);
}); });
logger.info( logger.info('Returning success response (generation running in background)');
"Returning success response (generation running in background)"
);
res.json({ success: true }); res.json({ success: true });
} catch (error) { } 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) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,71 +2,63 @@
* POST /generate endpoint - Generate spec from project definition * POST /generate endpoint - Generate spec from project definition
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import type { EventEmitter } from "../../../lib/events.js"; import type { EventEmitter } from '../../../lib/events.js';
import { createLogger } from "@automaker/utils"; import { createLogger } from '@automaker/utils';
import { import {
getSpecRegenerationStatus, getSpecRegenerationStatus,
setRunningState, setRunningState,
logAuthStatus, logAuthStatus,
logError, logError,
getErrorMessage, getErrorMessage,
} from "../common.js"; } from '../common.js';
import { generateSpec } from "../generate-spec.js"; import { generateSpec } from '../generate-spec.js';
const logger = createLogger("SpecRegeneration"); const logger = createLogger('SpecRegeneration');
export function createGenerateHandler(events: EventEmitter) { export function createGenerateHandler(events: EventEmitter) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
logger.info("========== /generate endpoint called =========="); logger.info('========== /generate endpoint called ==========');
logger.debug("Request body:", JSON.stringify(req.body, null, 2)); logger.debug('Request body:', JSON.stringify(req.body, null, 2));
try { try {
const { const { projectPath, projectDefinition, generateFeatures, analyzeProject, maxFeatures } =
projectPath, req.body as {
projectDefinition, projectPath: string;
generateFeatures, projectDefinition: string;
analyzeProject, generateFeatures?: boolean;
maxFeatures, analyzeProject?: boolean;
} = req.body as { maxFeatures?: number;
projectPath: string; };
projectDefinition: string;
generateFeatures?: boolean;
analyzeProject?: boolean;
maxFeatures?: number;
};
logger.debug("Parsed params:"); logger.debug('Parsed params:');
logger.debug(" projectPath:", projectPath); logger.debug(' projectPath:', projectPath);
logger.debug( logger.debug(' projectDefinition length:', `${projectDefinition?.length || 0} chars`);
" projectDefinition length:", logger.debug(' generateFeatures:', generateFeatures);
`${projectDefinition?.length || 0} chars` logger.debug(' analyzeProject:', analyzeProject);
); logger.debug(' maxFeatures:', maxFeatures);
logger.debug(" generateFeatures:", generateFeatures);
logger.debug(" analyzeProject:", analyzeProject);
logger.debug(" maxFeatures:", maxFeatures);
if (!projectPath || !projectDefinition) { if (!projectPath || !projectDefinition) {
logger.error("Missing required parameters"); logger.error('Missing required parameters');
res.status(400).json({ res.status(400).json({
success: false, success: false,
error: "projectPath and projectDefinition required", error: 'projectPath and projectDefinition required',
}); });
return; return;
} }
const { isRunning } = getSpecRegenerationStatus(); const { isRunning } = getSpecRegenerationStatus();
if (isRunning) { if (isRunning) {
logger.warn("Generation already running, rejecting request"); logger.warn('Generation already running, rejecting request');
res.json({ success: false, error: "Spec generation already running" }); res.json({ success: false, error: 'Spec generation already running' });
return; return;
} }
logAuthStatus("Before starting generation"); logAuthStatus('Before starting generation');
const abortController = new AbortController(); const abortController = new AbortController();
setRunningState(true, abortController); setRunningState(true, abortController);
logger.info("Starting background generation task..."); logger.info('Starting background generation task...');
generateSpec( generateSpec(
projectPath, projectPath,
@@ -78,24 +70,22 @@ export function createGenerateHandler(events: EventEmitter) {
maxFeatures maxFeatures
) )
.catch((error) => { .catch((error) => {
logError(error, "Generation failed with error"); logError(error, 'Generation failed with error');
events.emit("spec-regeneration:event", { events.emit('spec-regeneration:event', {
type: "spec_regeneration_error", type: 'spec_regeneration_error',
error: getErrorMessage(error), error: getErrorMessage(error),
projectPath: projectPath, projectPath: projectPath,
}); });
}) })
.finally(() => { .finally(() => {
logger.info("Generation task finished (success or error)"); logger.info('Generation task finished (success or error)');
setRunningState(false, null); setRunningState(false, null);
}); });
logger.info( logger.info('Returning success response (generation running in background)');
"Returning success response (generation running in background)"
);
res.json({ success: true }); res.json({ success: true });
} catch (error) { } 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) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,8 +2,8 @@
* GET /status endpoint - Get generation status * GET /status endpoint - Get generation status
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import { getSpecRegenerationStatus, getErrorMessage } from "../common.js"; import { getSpecRegenerationStatus, getErrorMessage } from '../common.js';
export function createStatusHandler() { export function createStatusHandler() {
return async (_req: Request, res: Response): Promise<void> => { return async (_req: Request, res: Response): Promise<void> => {

View File

@@ -2,12 +2,8 @@
* POST /stop endpoint - Stop generation * POST /stop endpoint - Stop generation
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import { import { getSpecRegenerationStatus, setRunningState, getErrorMessage } from '../common.js';
getSpecRegenerationStatus,
setRunningState,
getErrorMessage,
} from "../common.js";
export function createStopHandler() { export function createStopHandler() {
return async (_req: Request, res: Response): Promise<void> => { return async (_req: Request, res: Response): Promise<void> => {

View File

@@ -2,13 +2,10 @@
* Common utilities for auto-mode routes * Common utilities for auto-mode routes
*/ */
import { createLogger } from "@automaker/utils"; import { createLogger } from '@automaker/utils';
import { import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
getErrorMessage as getErrorMessageShared,
createLogError,
} from "../common.js";
const logger = createLogger("AutoMode"); const logger = createLogger('AutoMode');
// Re-export shared utilities // Re-export shared utilities
export { getErrorMessageShared as getErrorMessage }; export { getErrorMessageShared as getErrorMessage };

View File

@@ -4,37 +4,65 @@
* Uses the AutoModeService for real feature execution with Claude Agent SDK * Uses the AutoModeService for real feature execution with Claude Agent SDK
*/ */
import { Router } from "express"; import { Router } from 'express';
import type { AutoModeService } from "../../services/auto-mode-service.js"; import type { AutoModeService } from '../../services/auto-mode-service.js';
import { validatePathParams } from "../../middleware/validate-paths.js"; import { validatePathParams } from '../../middleware/validate-paths.js';
import { createStopFeatureHandler } from "./routes/stop-feature.js"; import { createStopFeatureHandler } from './routes/stop-feature.js';
import { createStatusHandler } from "./routes/status.js"; import { createStatusHandler } from './routes/status.js';
import { createRunFeatureHandler } from "./routes/run-feature.js"; import { createRunFeatureHandler } from './routes/run-feature.js';
import { createVerifyFeatureHandler } from "./routes/verify-feature.js"; import { createVerifyFeatureHandler } from './routes/verify-feature.js';
import { createResumeFeatureHandler } from "./routes/resume-feature.js"; import { createResumeFeatureHandler } from './routes/resume-feature.js';
import { createContextExistsHandler } from "./routes/context-exists.js"; import { createContextExistsHandler } from './routes/context-exists.js';
import { createAnalyzeProjectHandler } from "./routes/analyze-project.js"; import { createAnalyzeProjectHandler } from './routes/analyze-project.js';
import { createFollowUpFeatureHandler } from "./routes/follow-up-feature.js"; import { createFollowUpFeatureHandler } from './routes/follow-up-feature.js';
import { createCommitFeatureHandler } from "./routes/commit-feature.js"; import { createCommitFeatureHandler } from './routes/commit-feature.js';
import { createApprovePlanHandler } from "./routes/approve-plan.js"; import { createApprovePlanHandler } from './routes/approve-plan.js';
export function createAutoModeRoutes(autoModeService: AutoModeService): Router { export function createAutoModeRoutes(autoModeService: AutoModeService): Router {
const router = Router(); const router = Router();
router.post("/stop-feature", createStopFeatureHandler(autoModeService)); router.post('/stop-feature', createStopFeatureHandler(autoModeService));
router.post("/status", validatePathParams("projectPath?"), createStatusHandler(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( router.post(
"/follow-up-feature", '/run-feature',
validatePathParams("projectPath", "imagePaths[]"), 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) createFollowUpFeatureHandler(autoModeService)
); );
router.post("/commit-feature", validatePathParams("projectPath", "worktreePath?"), createCommitFeatureHandler(autoModeService)); router.post(
router.post("/approve-plan", validatePathParams("projectPath"), createApprovePlanHandler(autoModeService)); '/commit-feature',
validatePathParams('projectPath', 'worktreePath?'),
createCommitFeatureHandler(autoModeService)
);
router.post(
'/approve-plan',
validatePathParams('projectPath'),
createApprovePlanHandler(autoModeService)
);
return router; return router;
} }

View File

@@ -2,12 +2,12 @@
* POST /analyze-project endpoint - Analyze project * POST /analyze-project endpoint - Analyze project
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import type { AutoModeService } from "../../../services/auto-mode-service.js"; import type { AutoModeService } from '../../../services/auto-mode-service.js';
import { createLogger } from "@automaker/utils"; import { createLogger } from '@automaker/utils';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
const logger = createLogger("AutoMode"); const logger = createLogger('AutoMode');
export function createAnalyzeProjectHandler(autoModeService: AutoModeService) { export function createAnalyzeProjectHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
@@ -15,9 +15,7 @@ export function createAnalyzeProjectHandler(autoModeService: AutoModeService) {
const { projectPath } = req.body as { projectPath: string }; const { projectPath } = req.body as { projectPath: string };
if (!projectPath) { if (!projectPath) {
res res.status(400).json({ success: false, error: 'projectPath is required' });
.status(400)
.json({ success: false, error: "projectPath is required" });
return; return;
} }
@@ -26,9 +24,9 @@ export function createAnalyzeProjectHandler(autoModeService: AutoModeService) {
logger.error(`[AutoMode] Project analysis error:`, error); 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) { } catch (error) {
logError(error, "Analyze project failed"); logError(error, 'Analyze project failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,12 +2,12 @@
* POST /approve-plan endpoint - Approve or reject a generated plan/spec * POST /approve-plan endpoint - Approve or reject a generated plan/spec
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import type { AutoModeService } from "../../../services/auto-mode-service.js"; import type { AutoModeService } from '../../../services/auto-mode-service.js';
import { createLogger } from "@automaker/utils"; import { createLogger } from '@automaker/utils';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
const logger = createLogger("AutoMode"); const logger = createLogger('AutoMode');
export function createApprovePlanHandler(autoModeService: AutoModeService) { export function createApprovePlanHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
@@ -23,15 +23,15 @@ export function createApprovePlanHandler(autoModeService: AutoModeService) {
if (!featureId) { if (!featureId) {
res.status(400).json({ res.status(400).json({
success: false, success: false,
error: "featureId is required", error: 'featureId is required',
}); });
return; return;
} }
if (typeof approved !== "boolean") { if (typeof approved !== 'boolean') {
res.status(400).json({ res.status(400).json({
success: false, success: false,
error: "approved must be a boolean", error: 'approved must be a boolean',
}); });
return; return;
} }
@@ -41,9 +41,9 @@ export function createApprovePlanHandler(autoModeService: AutoModeService) {
// This supports cases where the server restarted while waiting for approval // This supports cases where the server restarted while waiting for approval
logger.info( logger.info(
`[AutoMode] Plan ${approved ? "approved" : "rejected"} for feature ${featureId}${ `[AutoMode] Plan ${approved ? 'approved' : 'rejected'} for feature ${featureId}${
editedPlan ? " (with edits)" : "" editedPlan ? ' (with edits)' : ''
}${feedback ? ` - Feedback: ${feedback}` : ""}` }${feedback ? ` - Feedback: ${feedback}` : ''}`
); );
// Resolve the pending approval (with recovery support) // Resolve the pending approval (with recovery support)
@@ -67,11 +67,11 @@ export function createApprovePlanHandler(autoModeService: AutoModeService) {
success: true, success: true,
approved, approved,
message: approved message: approved
? "Plan approved - implementation will continue" ? 'Plan approved - implementation will continue'
: "Plan rejected - feature execution stopped", : 'Plan rejected - feature execution stopped',
}); });
} catch (error) { } catch (error) {
logError(error, "Approve plan failed"); logError(error, 'Approve plan failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,9 +2,9 @@
* POST /commit-feature endpoint - Commit feature changes * POST /commit-feature endpoint - Commit feature changes
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import type { AutoModeService } from "../../../services/auto-mode-service.js"; import type { AutoModeService } from '../../../services/auto-mode-service.js';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
export function createCommitFeatureHandler(autoModeService: AutoModeService) { export function createCommitFeatureHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
@@ -16,23 +16,17 @@ export function createCommitFeatureHandler(autoModeService: AutoModeService) {
}; };
if (!projectPath || !featureId) { if (!projectPath || !featureId) {
res res.status(400).json({
.status(400) success: false,
.json({ error: 'projectPath and featureId are required',
success: false, });
error: "projectPath and featureId are required",
});
return; return;
} }
const commitHash = await autoModeService.commitFeature( const commitHash = await autoModeService.commitFeature(projectPath, featureId, worktreePath);
projectPath,
featureId,
worktreePath
);
res.json({ success: true, commitHash }); res.json({ success: true, commitHash });
} catch (error) { } catch (error) {
logError(error, "Commit feature failed"); logError(error, 'Commit feature failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,9 +2,9 @@
* POST /context-exists endpoint - Check if context exists for a feature * POST /context-exists endpoint - Check if context exists for a feature
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import type { AutoModeService } from "../../../services/auto-mode-service.js"; import type { AutoModeService } from '../../../services/auto-mode-service.js';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
export function createContextExistsHandler(autoModeService: AutoModeService) { export function createContextExistsHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
@@ -15,22 +15,17 @@ export function createContextExistsHandler(autoModeService: AutoModeService) {
}; };
if (!projectPath || !featureId) { if (!projectPath || !featureId) {
res res.status(400).json({
.status(400) success: false,
.json({ error: 'projectPath and featureId are required',
success: false, });
error: "projectPath and featureId are required",
});
return; return;
} }
const exists = await autoModeService.contextExists( const exists = await autoModeService.contextExists(projectPath, featureId);
projectPath,
featureId
);
res.json({ success: true, exists }); res.json({ success: true, exists });
} catch (error) { } catch (error) {
logError(error, "Check context exists failed"); logError(error, 'Check context exists failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,29 +2,28 @@
* POST /follow-up-feature endpoint - Follow up on a feature * POST /follow-up-feature endpoint - Follow up on a feature
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import type { AutoModeService } from "../../../services/auto-mode-service.js"; import type { AutoModeService } from '../../../services/auto-mode-service.js';
import { createLogger } from "@automaker/utils"; import { createLogger } from '@automaker/utils';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
const logger = createLogger("AutoMode"); const logger = createLogger('AutoMode');
export function createFollowUpFeatureHandler(autoModeService: AutoModeService) { export function createFollowUpFeatureHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
try { try {
const { projectPath, featureId, prompt, imagePaths, useWorktrees } = const { projectPath, featureId, prompt, imagePaths, useWorktrees } = req.body as {
req.body as { projectPath: string;
projectPath: string; featureId: string;
featureId: string; prompt: string;
prompt: string; imagePaths?: string[];
imagePaths?: string[]; useWorktrees?: boolean;
useWorktrees?: boolean; };
};
if (!projectPath || !featureId || !prompt) { if (!projectPath || !featureId || !prompt) {
res.status(400).json({ res.status(400).json({
success: false, success: false,
error: "projectPath, featureId, and prompt are required", error: 'projectPath, featureId, and prompt are required',
}); });
return; return;
} }
@@ -32,18 +31,9 @@ export function createFollowUpFeatureHandler(autoModeService: AutoModeService) {
// Start follow-up in background // Start follow-up in background
// followUpFeature derives workDir from feature.branchName // followUpFeature derives workDir from feature.branchName
autoModeService autoModeService
.followUpFeature( .followUpFeature(projectPath, featureId, prompt, imagePaths, useWorktrees ?? true)
projectPath,
featureId,
prompt,
imagePaths,
useWorktrees ?? true
)
.catch((error) => { .catch((error) => {
logger.error( logger.error(`[AutoMode] Follow up feature ${featureId} error:`, error);
`[AutoMode] Follow up feature ${featureId} error:`,
error
);
}) })
.finally(() => { .finally(() => {
// Release the starting slot when follow-up completes (success or error) // Release the starting slot when follow-up completes (success or error)
@@ -52,7 +42,7 @@ export function createFollowUpFeatureHandler(autoModeService: AutoModeService) {
res.json({ success: true }); res.json({ success: true });
} catch (error) { } catch (error) {
logError(error, "Follow up feature failed"); logError(error, 'Follow up feature failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,12 +2,12 @@
* POST /resume-feature endpoint - Resume a feature * POST /resume-feature endpoint - Resume a feature
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import type { AutoModeService } from "../../../services/auto-mode-service.js"; import type { AutoModeService } from '../../../services/auto-mode-service.js';
import { createLogger } from "@automaker/utils"; import { createLogger } from '@automaker/utils';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
const logger = createLogger("AutoMode"); const logger = createLogger('AutoMode');
export function createResumeFeatureHandler(autoModeService: AutoModeService) { export function createResumeFeatureHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
@@ -21,7 +21,7 @@ export function createResumeFeatureHandler(autoModeService: AutoModeService) {
if (!projectPath || !featureId) { if (!projectPath || !featureId) {
res.status(400).json({ res.status(400).json({
success: false, success: false,
error: "projectPath and featureId are required", error: 'projectPath and featureId are required',
}); });
return; return;
} }
@@ -36,7 +36,7 @@ export function createResumeFeatureHandler(autoModeService: AutoModeService) {
res.json({ success: true }); res.json({ success: true });
} catch (error) { } catch (error) {
logError(error, "Resume feature failed"); logError(error, 'Resume feature failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,12 +2,12 @@
* POST /run-feature endpoint - Run a single feature * POST /run-feature endpoint - Run a single feature
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import type { AutoModeService } from "../../../services/auto-mode-service.js"; import type { AutoModeService } from '../../../services/auto-mode-service.js';
import { createLogger } from "@automaker/utils"; import { createLogger } from '@automaker/utils';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
const logger = createLogger("AutoMode"); const logger = createLogger('AutoMode');
export function createRunFeatureHandler(autoModeService: AutoModeService) { export function createRunFeatureHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
@@ -21,7 +21,7 @@ export function createRunFeatureHandler(autoModeService: AutoModeService) {
if (!projectPath || !featureId) { if (!projectPath || !featureId) {
res.status(400).json({ res.status(400).json({
success: false, success: false,
error: "projectPath and featureId are required", error: 'projectPath and featureId are required',
}); });
return; return;
} }
@@ -40,7 +40,7 @@ export function createRunFeatureHandler(autoModeService: AutoModeService) {
res.json({ success: true }); res.json({ success: true });
} catch (error) { } catch (error) {
logError(error, "Run feature failed"); logError(error, 'Run feature failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,9 +2,9 @@
* POST /status endpoint - Get auto mode status * POST /status endpoint - Get auto mode status
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import type { AutoModeService } from "../../../services/auto-mode-service.js"; import type { AutoModeService } from '../../../services/auto-mode-service.js';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
export function createStatusHandler(autoModeService: AutoModeService) { export function createStatusHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
@@ -15,7 +15,7 @@ export function createStatusHandler(autoModeService: AutoModeService) {
...status, ...status,
}); });
} catch (error) { } catch (error) {
logError(error, "Get status failed"); logError(error, 'Get status failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,9 +2,9 @@
* POST /stop-feature endpoint - Stop a specific feature * POST /stop-feature endpoint - Stop a specific feature
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import type { AutoModeService } from "../../../services/auto-mode-service.js"; import type { AutoModeService } from '../../../services/auto-mode-service.js';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
export function createStopFeatureHandler(autoModeService: AutoModeService) { export function createStopFeatureHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
@@ -12,16 +12,14 @@ export function createStopFeatureHandler(autoModeService: AutoModeService) {
const { featureId } = req.body as { featureId: string }; const { featureId } = req.body as { featureId: string };
if (!featureId) { if (!featureId) {
res res.status(400).json({ success: false, error: 'featureId is required' });
.status(400)
.json({ success: false, error: "featureId is required" });
return; return;
} }
const stopped = await autoModeService.stopFeature(featureId); const stopped = await autoModeService.stopFeature(featureId);
res.json({ success: true, stopped }); res.json({ success: true, stopped });
} catch (error) { } catch (error) {
logError(error, "Stop feature failed"); logError(error, 'Stop feature failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,9 +2,9 @@
* POST /verify-feature endpoint - Verify a feature * POST /verify-feature endpoint - Verify a feature
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import type { AutoModeService } from "../../../services/auto-mode-service.js"; import type { AutoModeService } from '../../../services/auto-mode-service.js';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
export function createVerifyFeatureHandler(autoModeService: AutoModeService) { export function createVerifyFeatureHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
@@ -15,22 +15,17 @@ export function createVerifyFeatureHandler(autoModeService: AutoModeService) {
}; };
if (!projectPath || !featureId) { if (!projectPath || !featureId) {
res res.status(400).json({
.status(400) success: false,
.json({ error: 'projectPath and featureId are required',
success: false, });
error: "projectPath and featureId are required",
});
return; return;
} }
const passes = await autoModeService.verifyFeature( const passes = await autoModeService.verifyFeature(projectPath, featureId);
projectPath,
featureId
);
res.json({ success: true, passes }); res.json({ success: true, passes });
} catch (error) { } catch (error) {
logError(error, "Verify feature failed"); logError(error, 'Verify feature failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -1,18 +1,18 @@
import { Router, Request, Response } from "express"; import { Router, Request, Response } from 'express';
import { ClaudeUsageService } from "../../services/claude-usage-service.js"; import { ClaudeUsageService } from '../../services/claude-usage-service.js';
export function createClaudeRoutes(service: ClaudeUsageService): Router { export function createClaudeRoutes(service: ClaudeUsageService): Router {
const router = Router(); const router = Router();
// Get current usage (fetches from Claude CLI) // Get current usage (fetches from Claude CLI)
router.get("/usage", async (req: Request, res: Response) => { router.get('/usage', async (req: Request, res: Response) => {
try { try {
// Check if Claude CLI is available first // Check if Claude CLI is available first
const isAvailable = await service.isAvailable(); const isAvailable = await service.isAvailable();
if (!isAvailable) { if (!isAvailable) {
res.status(503).json({ res.status(503).json({
error: "Claude CLI not found", error: 'Claude CLI not found',
message: "Please install Claude Code CLI and run 'claude login' to authenticate" message: "Please install Claude Code CLI and run 'claude login' to authenticate",
}); });
return; return;
} }
@@ -20,20 +20,20 @@ export function createClaudeRoutes(service: ClaudeUsageService): Router {
const usage = await service.fetchUsageData(); const usage = await service.fetchUsageData();
res.json(usage); res.json(usage);
} catch (error) { } 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({ res.status(401).json({
error: "Authentication required", error: 'Authentication required',
message: "Please run 'claude login' to authenticate" message: "Please run 'claude login' to authenticate",
}); });
} else if (message.includes("timed out")) { } else if (message.includes('timed out')) {
res.status(504).json({ res.status(504).json({
error: "Command timed out", error: 'Command timed out',
message: "The Claude CLI took too long to respond" message: 'The Claude CLI took too long to respond',
}); });
} else { } else {
console.error("Error fetching usage:", error); console.error('Error fetching usage:', error);
res.status(500).json({ error: message }); res.status(500).json({ error: message });
} }
} }

View File

@@ -29,7 +29,7 @@ export type ClaudeUsage = {
export type ClaudeStatus = { export type ClaudeStatus = {
indicator: { indicator: {
color: "green" | "yellow" | "orange" | "red" | "gray"; color: 'green' | 'yellow' | 'orange' | 'red' | 'gray';
}; };
description: string; description: string;
}; };

View File

@@ -2,7 +2,7 @@
* Common utilities shared across all route modules * Common utilities shared across all route modules
*/ */
import { createLogger } from "@automaker/utils"; import { createLogger } from '@automaker/utils';
// Re-export git utilities from shared package // Re-export git utilities from shared package
export { export {
@@ -16,7 +16,7 @@ export {
listAllFilesInDirectory, listAllFilesInDirectory,
generateDiffsForNonGitDirectory, generateDiffsForNonGitDirectory,
getGitRepositoryDiffs, getGitRepositoryDiffs,
} from "@automaker/git-utils"; } from '@automaker/git-utils';
type Logger = ReturnType<typeof createLogger>; type Logger = ReturnType<typeof createLogger>;
@@ -24,7 +24,7 @@ type Logger = ReturnType<typeof createLogger>;
* Get error message from error object * Get error message from error object
*/ */
export function getErrorMessage(error: unknown): string { export function getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : "Unknown error"; return error instanceof Error ? error.message : 'Unknown error';
} }
/** /**

View File

@@ -5,8 +5,8 @@
* with different enhancement modes (improve, expand, simplify, etc.) * with different enhancement modes (improve, expand, simplify, etc.)
*/ */
import { Router } from "express"; import { Router } from 'express';
import { createEnhanceHandler } from "./routes/enhance.js"; import { createEnhanceHandler } from './routes/enhance.js';
/** /**
* Create the enhance-prompt router * Create the enhance-prompt router
@@ -16,7 +16,7 @@ import { createEnhanceHandler } from "./routes/enhance.js";
export function createEnhancePromptRoutes(): Router { export function createEnhancePromptRoutes(): Router {
const router = Router(); const router = Router();
router.post("/", createEnhanceHandler()); router.post('/', createEnhanceHandler());
return router; return router;
} }

View File

@@ -5,19 +5,19 @@
* Supports modes: improve, technical, simplify, acceptance * Supports modes: improve, technical, simplify, acceptance
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import { query } from "@anthropic-ai/claude-agent-sdk"; import { query } from '@anthropic-ai/claude-agent-sdk';
import { createLogger } from "@automaker/utils"; import { createLogger } from '@automaker/utils';
import { resolveModelString } from "@automaker/model-resolver"; import { resolveModelString } from '@automaker/model-resolver';
import { CLAUDE_MODEL_MAP } from "@automaker/types"; import { CLAUDE_MODEL_MAP } from '@automaker/types';
import { import {
getSystemPrompt, getSystemPrompt,
buildUserPrompt, buildUserPrompt,
isValidEnhancementMode, isValidEnhancementMode,
type EnhancementMode, 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 * Request body for the enhance endpoint
@@ -63,16 +63,16 @@ async function extractTextFromStream(
}; };
}> }>
): Promise<string> { ): Promise<string> {
let responseText = ""; let responseText = '';
for await (const msg of stream) { 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) { for (const block of msg.message.content) {
if (block.type === "text" && block.text) { if (block.type === 'text' && block.text) {
responseText += 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; responseText = msg.result || responseText;
} }
} }
@@ -85,29 +85,25 @@ async function extractTextFromStream(
* *
* @returns Express request handler for text enhancement * @returns Express request handler for text enhancement
*/ */
export function createEnhanceHandler(): ( export function createEnhanceHandler(): (req: Request, res: Response) => Promise<void> {
req: Request,
res: Response
) => Promise<void> {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
try { try {
const { originalText, enhancementMode, model } = const { originalText, enhancementMode, model } = req.body as EnhanceRequestBody;
req.body as EnhanceRequestBody;
// Validate required fields // Validate required fields
if (!originalText || typeof originalText !== "string") { if (!originalText || typeof originalText !== 'string') {
const response: EnhanceErrorResponse = { const response: EnhanceErrorResponse = {
success: false, 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); res.status(400).json(response);
return; return;
} }
if (!enhancementMode || typeof enhancementMode !== "string") { if (!enhancementMode || typeof enhancementMode !== 'string') {
const response: EnhanceErrorResponse = { const response: EnhanceErrorResponse = {
success: false, 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); res.status(400).json(response);
return; return;
@@ -118,7 +114,7 @@ export function createEnhanceHandler(): (
if (trimmedText.length === 0) { if (trimmedText.length === 0) {
const response: EnhanceErrorResponse = { const response: EnhanceErrorResponse = {
success: false, success: false,
error: "originalText cannot be empty", error: 'originalText cannot be empty',
}; };
res.status(400).json(response); res.status(400).json(response);
return; return;
@@ -128,11 +124,9 @@ export function createEnhanceHandler(): (
const normalizedMode = enhancementMode.toLowerCase(); const normalizedMode = enhancementMode.toLowerCase();
const validMode: EnhancementMode = isValidEnhancementMode(normalizedMode) const validMode: EnhancementMode = isValidEnhancementMode(normalizedMode)
? normalizedMode ? normalizedMode
: "improve"; : 'improve';
logger.info( logger.info(`Enhancing text with mode: ${validMode}, length: ${trimmedText.length} chars`);
`Enhancing text with mode: ${validMode}, length: ${trimmedText.length} chars`
);
// Get the system prompt for this mode // Get the system prompt for this mode
const systemPrompt = getSystemPrompt(validMode); const systemPrompt = getSystemPrompt(validMode);
@@ -155,7 +149,7 @@ export function createEnhanceHandler(): (
systemPrompt, systemPrompt,
maxTurns: 1, maxTurns: 1,
allowedTools: [], allowedTools: [],
permissionMode: "acceptEdits", permissionMode: 'acceptEdits',
}, },
}); });
@@ -163,18 +157,16 @@ export function createEnhanceHandler(): (
const enhancedText = await extractTextFromStream(stream); const enhancedText = await extractTextFromStream(stream);
if (!enhancedText || enhancedText.trim().length === 0) { if (!enhancedText || enhancedText.trim().length === 0) {
logger.warn("Received empty response from Claude"); logger.warn('Received empty response from Claude');
const response: EnhanceErrorResponse = { const response: EnhanceErrorResponse = {
success: false, success: false,
error: "Failed to generate enhanced text - empty response", error: 'Failed to generate enhanced text - empty response',
}; };
res.status(500).json(response); res.status(500).json(response);
return; return;
} }
logger.info( logger.info(`Enhancement complete, output length: ${enhancedText.length} chars`);
`Enhancement complete, output length: ${enhancedText.length} chars`
);
const response: EnhanceSuccessResponse = { const response: EnhanceSuccessResponse = {
success: true, success: true,
@@ -182,9 +174,8 @@ export function createEnhanceHandler(): (
}; };
res.json(response); res.json(response);
} catch (error) { } catch (error) {
const errorMessage = const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
error instanceof Error ? error.message : "Unknown error occurred"; logger.error('Enhancement failed:', errorMessage);
logger.error("Enhancement failed:", errorMessage);
const response: EnhanceErrorResponse = { const response: EnhanceErrorResponse = {
success: false, success: false,

View File

@@ -2,13 +2,10 @@
* Common utilities for features routes * Common utilities for features routes
*/ */
import { createLogger } from "@automaker/utils"; import { createLogger } from '@automaker/utils';
import { import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
getErrorMessage as getErrorMessageShared,
createLogError,
} from "../common.js";
const logger = createLogger("Features"); const logger = createLogger('Features');
// Re-export shared utilities // Re-export shared utilities
export { getErrorMessageShared as getErrorMessage }; export { getErrorMessageShared as getErrorMessage };

View File

@@ -2,27 +2,27 @@
* Features routes - HTTP API for feature management * Features routes - HTTP API for feature management
*/ */
import { Router } from "express"; import { Router } from 'express';
import { FeatureLoader } from "../../services/feature-loader.js"; import { FeatureLoader } from '../../services/feature-loader.js';
import { validatePathParams } from "../../middleware/validate-paths.js"; import { validatePathParams } from '../../middleware/validate-paths.js';
import { createListHandler } from "./routes/list.js"; import { createListHandler } from './routes/list.js';
import { createGetHandler } from "./routes/get.js"; import { createGetHandler } from './routes/get.js';
import { createCreateHandler } from "./routes/create.js"; import { createCreateHandler } from './routes/create.js';
import { createUpdateHandler } from "./routes/update.js"; import { createUpdateHandler } from './routes/update.js';
import { createDeleteHandler } from "./routes/delete.js"; import { createDeleteHandler } from './routes/delete.js';
import { createAgentOutputHandler } from "./routes/agent-output.js"; import { createAgentOutputHandler } from './routes/agent-output.js';
import { createGenerateTitleHandler } from "./routes/generate-title.js"; import { createGenerateTitleHandler } from './routes/generate-title.js';
export function createFeaturesRoutes(featureLoader: FeatureLoader): Router { export function createFeaturesRoutes(featureLoader: FeatureLoader): Router {
const router = Router(); const router = Router();
router.post("/list", validatePathParams("projectPath"), createListHandler(featureLoader)); router.post('/list', validatePathParams('projectPath'), createListHandler(featureLoader));
router.post("/get", validatePathParams("projectPath"), createGetHandler(featureLoader)); router.post('/get', validatePathParams('projectPath'), createGetHandler(featureLoader));
router.post("/create", validatePathParams("projectPath"), createCreateHandler(featureLoader)); router.post('/create', validatePathParams('projectPath'), createCreateHandler(featureLoader));
router.post("/update", validatePathParams("projectPath"), createUpdateHandler(featureLoader)); router.post('/update', validatePathParams('projectPath'), createUpdateHandler(featureLoader));
router.post("/delete", validatePathParams("projectPath"), createDeleteHandler(featureLoader)); router.post('/delete', validatePathParams('projectPath'), createDeleteHandler(featureLoader));
router.post("/agent-output", createAgentOutputHandler(featureLoader)); router.post('/agent-output', createAgentOutputHandler(featureLoader));
router.post("/generate-title", createGenerateTitleHandler()); router.post('/generate-title', createGenerateTitleHandler());
return router; return router;
} }

View File

@@ -2,9 +2,9 @@
* POST /agent-output endpoint - Get agent output for a feature * POST /agent-output endpoint - Get agent output for a feature
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import { FeatureLoader } from "../../../services/feature-loader.js"; import { FeatureLoader } from '../../../services/feature-loader.js';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
export function createAgentOutputHandler(featureLoader: FeatureLoader) { export function createAgentOutputHandler(featureLoader: FeatureLoader) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
@@ -15,22 +15,17 @@ export function createAgentOutputHandler(featureLoader: FeatureLoader) {
}; };
if (!projectPath || !featureId) { if (!projectPath || !featureId) {
res res.status(400).json({
.status(400) success: false,
.json({ error: 'projectPath and featureId are required',
success: false, });
error: "projectPath and featureId are required",
});
return; return;
} }
const content = await featureLoader.getAgentOutput( const content = await featureLoader.getAgentOutput(projectPath, featureId);
projectPath,
featureId
);
res.json({ success: true, content }); res.json({ success: true, content });
} catch (error) { } catch (error) {
logError(error, "Get agent output failed"); logError(error, 'Get agent output failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,10 +2,10 @@
* POST /create endpoint - Create a new feature * POST /create endpoint - Create a new feature
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import { FeatureLoader } from "../../../services/feature-loader.js"; import { FeatureLoader } from '../../../services/feature-loader.js';
import type { Feature } from "@automaker/types"; import type { Feature } from '@automaker/types';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
export function createCreateHandler(featureLoader: FeatureLoader) { export function createCreateHandler(featureLoader: FeatureLoader) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
@@ -18,7 +18,7 @@ export function createCreateHandler(featureLoader: FeatureLoader) {
if (!projectPath || !feature) { if (!projectPath || !feature) {
res.status(400).json({ res.status(400).json({
success: false, success: false,
error: "projectPath and feature are required", error: 'projectPath and feature are required',
}); });
return; return;
} }
@@ -26,7 +26,7 @@ export function createCreateHandler(featureLoader: FeatureLoader) {
const created = await featureLoader.create(projectPath, feature); const created = await featureLoader.create(projectPath, feature);
res.json({ success: true, feature: created }); res.json({ success: true, feature: created });
} catch (error) { } catch (error) {
logError(error, "Create feature failed"); logError(error, 'Create feature failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,9 +2,9 @@
* POST /delete endpoint - Delete a feature * POST /delete endpoint - Delete a feature
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import { FeatureLoader } from "../../../services/feature-loader.js"; import { FeatureLoader } from '../../../services/feature-loader.js';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
export function createDeleteHandler(featureLoader: FeatureLoader) { export function createDeleteHandler(featureLoader: FeatureLoader) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
@@ -15,19 +15,17 @@ export function createDeleteHandler(featureLoader: FeatureLoader) {
}; };
if (!projectPath || !featureId) { if (!projectPath || !featureId) {
res res.status(400).json({
.status(400) success: false,
.json({ error: 'projectPath and featureId are required',
success: false, });
error: "projectPath and featureId are required",
});
return; return;
} }
const success = await featureLoader.delete(projectPath, featureId); const success = await featureLoader.delete(projectPath, featureId);
res.json({ success }); res.json({ success });
} catch (error) { } catch (error) {
logError(error, "Delete feature failed"); logError(error, 'Delete feature failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -4,12 +4,12 @@
* Uses Claude Haiku to generate a short, descriptive title from feature description. * Uses Claude Haiku to generate a short, descriptive title from feature description.
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import { query } from "@anthropic-ai/claude-agent-sdk"; import { query } from '@anthropic-ai/claude-agent-sdk';
import { createLogger } from "@automaker/utils"; import { createLogger } from '@automaker/utils';
import { CLAUDE_MODEL_MAP } from "@automaker/model-resolver"; import { CLAUDE_MODEL_MAP } from '@automaker/model-resolver';
const logger = createLogger("GenerateTitle"); const logger = createLogger('GenerateTitle');
interface GenerateTitleRequestBody { interface GenerateTitleRequestBody {
description: string; description: string;
@@ -44,16 +44,16 @@ async function extractTextFromStream(
}; };
}> }>
): Promise<string> { ): Promise<string> {
let responseText = ""; let responseText = '';
for await (const msg of stream) { 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) { for (const block of msg.message.content) {
if (block.type === "text" && block.text) { if (block.type === 'text' && block.text) {
responseText += 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; responseText = msg.result || responseText;
} }
} }
@@ -61,18 +61,15 @@ async function extractTextFromStream(
return responseText; return responseText;
} }
export function createGenerateTitleHandler(): ( export function createGenerateTitleHandler(): (req: Request, res: Response) => Promise<void> {
req: Request,
res: Response
) => Promise<void> {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
try { try {
const { description } = req.body as GenerateTitleRequestBody; const { description } = req.body as GenerateTitleRequestBody;
if (!description || typeof description !== "string") { if (!description || typeof description !== 'string') {
const response: GenerateTitleErrorResponse = { const response: GenerateTitleErrorResponse = {
success: false, 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); res.status(400).json(response);
return; return;
@@ -82,7 +79,7 @@ export function createGenerateTitleHandler(): (
if (trimmedDescription.length === 0) { if (trimmedDescription.length === 0) {
const response: GenerateTitleErrorResponse = { const response: GenerateTitleErrorResponse = {
success: false, success: false,
error: "description cannot be empty", error: 'description cannot be empty',
}; };
res.status(400).json(response); res.status(400).json(response);
return; return;
@@ -99,17 +96,17 @@ export function createGenerateTitleHandler(): (
systemPrompt: SYSTEM_PROMPT, systemPrompt: SYSTEM_PROMPT,
maxTurns: 1, maxTurns: 1,
allowedTools: [], allowedTools: [],
permissionMode: "acceptEdits", permissionMode: 'acceptEdits',
}, },
}); });
const title = await extractTextFromStream(stream); const title = await extractTextFromStream(stream);
if (!title || title.trim().length === 0) { if (!title || title.trim().length === 0) {
logger.warn("Received empty response from Claude"); logger.warn('Received empty response from Claude');
const response: GenerateTitleErrorResponse = { const response: GenerateTitleErrorResponse = {
success: false, success: false,
error: "Failed to generate title - empty response", error: 'Failed to generate title - empty response',
}; };
res.status(500).json(response); res.status(500).json(response);
return; return;
@@ -123,9 +120,8 @@ export function createGenerateTitleHandler(): (
}; };
res.json(response); res.json(response);
} catch (error) { } catch (error) {
const errorMessage = const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
error instanceof Error ? error.message : "Unknown error occurred"; logger.error('Title generation failed:', errorMessage);
logger.error("Title generation failed:", errorMessage);
const response: GenerateTitleErrorResponse = { const response: GenerateTitleErrorResponse = {
success: false, success: false,

View File

@@ -2,9 +2,9 @@
* POST /get endpoint - Get a single feature * POST /get endpoint - Get a single feature
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import { FeatureLoader } from "../../../services/feature-loader.js"; import { FeatureLoader } from '../../../services/feature-loader.js';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
export function createGetHandler(featureLoader: FeatureLoader) { export function createGetHandler(featureLoader: FeatureLoader) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
@@ -15,24 +15,22 @@ export function createGetHandler(featureLoader: FeatureLoader) {
}; };
if (!projectPath || !featureId) { if (!projectPath || !featureId) {
res res.status(400).json({
.status(400) success: false,
.json({ error: 'projectPath and featureId are required',
success: false, });
error: "projectPath and featureId are required",
});
return; return;
} }
const feature = await featureLoader.get(projectPath, featureId); const feature = await featureLoader.get(projectPath, featureId);
if (!feature) { if (!feature) {
res.status(404).json({ success: false, error: "Feature not found" }); res.status(404).json({ success: false, error: 'Feature not found' });
return; return;
} }
res.json({ success: true, feature }); res.json({ success: true, feature });
} catch (error) { } catch (error) {
logError(error, "Get feature failed"); logError(error, 'Get feature failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,9 +2,9 @@
* POST /list endpoint - List all features for a project * POST /list endpoint - List all features for a project
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import { FeatureLoader } from "../../../services/feature-loader.js"; import { FeatureLoader } from '../../../services/feature-loader.js';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
export function createListHandler(featureLoader: FeatureLoader) { export function createListHandler(featureLoader: FeatureLoader) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
@@ -12,16 +12,14 @@ export function createListHandler(featureLoader: FeatureLoader) {
const { projectPath } = req.body as { projectPath: string }; const { projectPath } = req.body as { projectPath: string };
if (!projectPath) { if (!projectPath) {
res res.status(400).json({ success: false, error: 'projectPath is required' });
.status(400)
.json({ success: false, error: "projectPath is required" });
return; return;
} }
const features = await featureLoader.getAll(projectPath); const features = await featureLoader.getAll(projectPath);
res.json({ success: true, features }); res.json({ success: true, features });
} catch (error) { } catch (error) {
logError(error, "List features failed"); logError(error, 'List features failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,10 +2,10 @@
* POST /update endpoint - Update a feature * POST /update endpoint - Update a feature
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import { FeatureLoader } from "../../../services/feature-loader.js"; import { FeatureLoader } from '../../../services/feature-loader.js';
import type { Feature } from "@automaker/types"; import type { Feature } from '@automaker/types';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
export function createUpdateHandler(featureLoader: FeatureLoader) { export function createUpdateHandler(featureLoader: FeatureLoader) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
@@ -19,19 +19,15 @@ export function createUpdateHandler(featureLoader: FeatureLoader) {
if (!projectPath || !featureId || !updates) { if (!projectPath || !featureId || !updates) {
res.status(400).json({ res.status(400).json({
success: false, success: false,
error: "projectPath, featureId, and updates are required", error: 'projectPath, featureId, and updates are required',
}); });
return; return;
} }
const updated = await featureLoader.update( const updated = await featureLoader.update(projectPath, featureId, updates);
projectPath,
featureId,
updates
);
res.json({ success: true, feature: updated }); res.json({ success: true, feature: updated });
} catch (error) { } catch (error) {
logError(error, "Update feature failed"); logError(error, 'Update feature failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,13 +2,10 @@
* Common utilities for fs routes * Common utilities for fs routes
*/ */
import { createLogger } from "@automaker/utils"; import { createLogger } from '@automaker/utils';
import { import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
getErrorMessage as getErrorMessageShared,
createLogError,
} from "../common.js";
const logger = createLogger("FS"); const logger = createLogger('FS');
// Re-export shared utilities // Re-export shared utilities
export { getErrorMessageShared as getErrorMessage }; export { getErrorMessageShared as getErrorMessage };

View File

@@ -3,40 +3,40 @@
* Provides REST API equivalents for Electron IPC file operations * Provides REST API equivalents for Electron IPC file operations
*/ */
import { Router } from "express"; import { Router } from 'express';
import type { EventEmitter } from "../../lib/events.js"; import type { EventEmitter } from '../../lib/events.js';
import { createReadHandler } from "./routes/read.js"; import { createReadHandler } from './routes/read.js';
import { createWriteHandler } from "./routes/write.js"; import { createWriteHandler } from './routes/write.js';
import { createMkdirHandler } from "./routes/mkdir.js"; import { createMkdirHandler } from './routes/mkdir.js';
import { createReaddirHandler } from "./routes/readdir.js"; import { createReaddirHandler } from './routes/readdir.js';
import { createExistsHandler } from "./routes/exists.js"; import { createExistsHandler } from './routes/exists.js';
import { createStatHandler } from "./routes/stat.js"; import { createStatHandler } from './routes/stat.js';
import { createDeleteHandler } from "./routes/delete.js"; import { createDeleteHandler } from './routes/delete.js';
import { createValidatePathHandler } from "./routes/validate-path.js"; import { createValidatePathHandler } from './routes/validate-path.js';
import { createResolveDirectoryHandler } from "./routes/resolve-directory.js"; import { createResolveDirectoryHandler } from './routes/resolve-directory.js';
import { createSaveImageHandler } from "./routes/save-image.js"; import { createSaveImageHandler } from './routes/save-image.js';
import { createBrowseHandler } from "./routes/browse.js"; import { createBrowseHandler } from './routes/browse.js';
import { createImageHandler } from "./routes/image.js"; import { createImageHandler } from './routes/image.js';
import { createSaveBoardBackgroundHandler } from "./routes/save-board-background.js"; import { createSaveBoardBackgroundHandler } from './routes/save-board-background.js';
import { createDeleteBoardBackgroundHandler } from "./routes/delete-board-background.js"; import { createDeleteBoardBackgroundHandler } from './routes/delete-board-background.js';
export function createFsRoutes(_events: EventEmitter): Router { export function createFsRoutes(_events: EventEmitter): Router {
const router = Router(); const router = Router();
router.post("/read", createReadHandler()); router.post('/read', createReadHandler());
router.post("/write", createWriteHandler()); router.post('/write', createWriteHandler());
router.post("/mkdir", createMkdirHandler()); router.post('/mkdir', createMkdirHandler());
router.post("/readdir", createReaddirHandler()); router.post('/readdir', createReaddirHandler());
router.post("/exists", createExistsHandler()); router.post('/exists', createExistsHandler());
router.post("/stat", createStatHandler()); router.post('/stat', createStatHandler());
router.post("/delete", createDeleteHandler()); router.post('/delete', createDeleteHandler());
router.post("/validate-path", createValidatePathHandler()); router.post('/validate-path', createValidatePathHandler());
router.post("/resolve-directory", createResolveDirectoryHandler()); router.post('/resolve-directory', createResolveDirectoryHandler());
router.post("/save-image", createSaveImageHandler()); router.post('/save-image', createSaveImageHandler());
router.post("/browse", createBrowseHandler()); router.post('/browse', createBrowseHandler());
router.get("/image", createImageHandler()); router.get('/image', createImageHandler());
router.post("/save-board-background", createSaveBoardBackgroundHandler()); router.post('/save-board-background', createSaveBoardBackgroundHandler());
router.post("/delete-board-background", createDeleteBoardBackgroundHandler()); router.post('/delete-board-background', createDeleteBoardBackgroundHandler());
return router; return router;
} }

View File

@@ -2,13 +2,10 @@
* Common utilities for git routes * Common utilities for git routes
*/ */
import { createLogger } from "@automaker/utils"; import { createLogger } from '@automaker/utils';
import { import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
getErrorMessage as getErrorMessageShared,
createLogError,
} from "../common.js";
const logger = createLogger("Git"); const logger = createLogger('Git');
// Re-export shared utilities // Re-export shared utilities
export { getErrorMessageShared as getErrorMessage }; export { getErrorMessageShared as getErrorMessage };

View File

@@ -2,16 +2,16 @@
* Git routes - HTTP API for git operations (non-worktree) * Git routes - HTTP API for git operations (non-worktree)
*/ */
import { Router } from "express"; import { Router } from 'express';
import { validatePathParams } from "../../middleware/validate-paths.js"; import { validatePathParams } from '../../middleware/validate-paths.js';
import { createDiffsHandler } from "./routes/diffs.js"; import { createDiffsHandler } from './routes/diffs.js';
import { createFileDiffHandler } from "./routes/file-diff.js"; import { createFileDiffHandler } from './routes/file-diff.js';
export function createGitRoutes(): Router { export function createGitRoutes(): Router {
const router = Router(); const router = Router();
router.post("/diffs", validatePathParams("projectPath"), createDiffsHandler()); router.post('/diffs', validatePathParams('projectPath'), createDiffsHandler());
router.post("/file-diff", validatePathParams("projectPath", "filePath"), createFileDiffHandler()); router.post('/file-diff', validatePathParams('projectPath', 'filePath'), createFileDiffHandler());
return router; return router;
} }

View File

@@ -2,9 +2,9 @@
* POST /diffs endpoint - Get diffs for the main project * POST /diffs endpoint - Get diffs for the main project
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
import { getGitRepositoryDiffs } from "../../common.js"; import { getGitRepositoryDiffs } from '../../common.js';
export function createDiffsHandler() { export function createDiffsHandler() {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
@@ -12,7 +12,7 @@ export function createDiffsHandler() {
const { projectPath } = req.body as { projectPath: string }; const { projectPath } = req.body as { projectPath: string };
if (!projectPath) { if (!projectPath) {
res.status(400).json({ success: false, error: "projectPath required" }); res.status(400).json({ success: false, error: 'projectPath required' });
return; return;
} }
@@ -25,11 +25,11 @@ export function createDiffsHandler() {
hasChanges: result.hasChanges, hasChanges: result.hasChanges,
}); });
} catch (innerError) { } catch (innerError) {
logError(innerError, "Git diff failed"); logError(innerError, 'Git diff failed');
res.json({ success: true, diff: "", files: [], hasChanges: false }); res.json({ success: true, diff: '', files: [], hasChanges: false });
} }
} catch (error) { } catch (error) {
logError(error, "Get diffs failed"); logError(error, 'Get diffs failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,11 +2,11 @@
* POST /file-diff endpoint - Get diff for a specific file * POST /file-diff endpoint - Get diff for a specific file
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import { exec } from "child_process"; import { exec } from 'child_process';
import { promisify } from "util"; import { promisify } from 'util';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
import { generateSyntheticDiffForNewFile } from "../../common.js"; import { generateSyntheticDiffForNewFile } from '../../common.js';
const execAsync = promisify(exec); const execAsync = promisify(exec);
@@ -19,20 +19,17 @@ export function createFileDiffHandler() {
}; };
if (!projectPath || !filePath) { if (!projectPath || !filePath) {
res res.status(400).json({ success: false, error: 'projectPath and filePath required' });
.status(400)
.json({ success: false, error: "projectPath and filePath required" });
return; return;
} }
try { try {
// First check if the file is untracked // First check if the file is untracked
const { stdout: status } = await execAsync( const { stdout: status } = await execAsync(`git status --porcelain -- "${filePath}"`, {
`git status --porcelain -- "${filePath}"`, cwd: projectPath,
{ cwd: projectPath } });
);
const isUntracked = status.trim().startsWith("??"); const isUntracked = status.trim().startsWith('??');
let diff: string; let diff: string;
if (isUntracked) { if (isUntracked) {
@@ -40,23 +37,20 @@ export function createFileDiffHandler() {
diff = await generateSyntheticDiffForNewFile(projectPath, filePath); diff = await generateSyntheticDiffForNewFile(projectPath, filePath);
} else { } else {
// Use regular git diff for tracked files // Use regular git diff for tracked files
const result = await execAsync( const result = await execAsync(`git diff HEAD -- "${filePath}"`, {
`git diff HEAD -- "${filePath}"`, cwd: projectPath,
{ maxBuffer: 10 * 1024 * 1024,
cwd: projectPath, });
maxBuffer: 10 * 1024 * 1024,
}
);
diff = result.stdout; diff = result.stdout;
} }
res.json({ success: true, diff, filePath }); res.json({ success: true, diff, filePath });
} catch (innerError) { } catch (innerError) {
logError(innerError, "Git file diff failed"); logError(innerError, 'Git file diff failed');
res.json({ success: true, diff: "", filePath }); res.json({ success: true, diff: '', filePath });
} }
} catch (error) { } catch (error) {
logError(error, "Get file diff failed"); logError(error, 'Get file diff failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,13 +2,10 @@
* Common utilities for health routes * Common utilities for health routes
*/ */
import { createLogger } from "@automaker/utils"; import { createLogger } from '@automaker/utils';
import { import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
getErrorMessage as getErrorMessageShared,
createLogError,
} from "../common.js";
const logger = createLogger("Health"); const logger = createLogger('Health');
// Re-export shared utilities // Re-export shared utilities
export { getErrorMessageShared as getErrorMessage }; export { getErrorMessageShared as getErrorMessage };

View File

@@ -2,15 +2,15 @@
* Health check routes * Health check routes
*/ */
import { Router } from "express"; import { Router } from 'express';
import { createIndexHandler } from "./routes/index.js"; import { createIndexHandler } from './routes/index.js';
import { createDetailedHandler } from "./routes/detailed.js"; import { createDetailedHandler } from './routes/detailed.js';
export function createHealthRoutes(): Router { export function createHealthRoutes(): Router {
const router = Router(); const router = Router();
router.get("/", createIndexHandler()); router.get('/', createIndexHandler());
router.get("/detailed", createDetailedHandler()); router.get('/detailed', createDetailedHandler());
return router; return router;
} }

View File

@@ -2,18 +2,18 @@
* GET /detailed endpoint - Detailed health check * GET /detailed endpoint - Detailed health check
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import { getAuthStatus } from "../../../lib/auth.js"; import { getAuthStatus } from '../../../lib/auth.js';
export function createDetailedHandler() { export function createDetailedHandler() {
return (_req: Request, res: Response): void => { return (_req: Request, res: Response): void => {
res.json({ res.json({
status: "ok", status: 'ok',
timestamp: new Date().toISOString(), 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(), uptime: process.uptime(),
memory: process.memoryUsage(), memory: process.memoryUsage(),
dataDir: process.env.DATA_DIR || "./data", dataDir: process.env.DATA_DIR || './data',
auth: getAuthStatus(), auth: getAuthStatus(),
env: { env: {
nodeVersion: process.version, nodeVersion: process.version,

View File

@@ -2,14 +2,14 @@
* GET / endpoint - Basic health check * GET / endpoint - Basic health check
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
export function createIndexHandler() { export function createIndexHandler() {
return (_req: Request, res: Response): void => { return (_req: Request, res: Response): void => {
res.json({ res.json({
status: "ok", status: 'ok',
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
version: process.env.npm_package_version || "0.1.0", version: process.env.npm_package_version || '0.1.0',
}); });
}; };
} }

View File

@@ -2,13 +2,10 @@
* Common utilities for models routes * Common utilities for models routes
*/ */
import { createLogger } from "@automaker/utils"; import { createLogger } from '@automaker/utils';
import { import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
getErrorMessage as getErrorMessageShared,
createLogError,
} from "../common.js";
const logger = createLogger("Models"); const logger = createLogger('Models');
// Re-export shared utilities // Re-export shared utilities
export { getErrorMessageShared as getErrorMessage }; export { getErrorMessageShared as getErrorMessage };

View File

@@ -2,15 +2,15 @@
* Models routes - HTTP API for model providers and availability * Models routes - HTTP API for model providers and availability
*/ */
import { Router } from "express"; import { Router } from 'express';
import { createAvailableHandler } from "./routes/available.js"; import { createAvailableHandler } from './routes/available.js';
import { createProvidersHandler } from "./routes/providers.js"; import { createProvidersHandler } from './routes/providers.js';
export function createModelsRoutes(): Router { export function createModelsRoutes(): Router {
const router = Router(); const router = Router();
router.get("/available", createAvailableHandler()); router.get('/available', createAvailableHandler());
router.get("/providers", createProvidersHandler()); router.get('/providers', createProvidersHandler());
return router; return router;
} }

View File

@@ -2,8 +2,8 @@
* GET /available endpoint - Get available models * GET /available endpoint - Get available models
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
interface ModelDefinition { interface ModelDefinition {
id: string; id: string;
@@ -20,36 +20,36 @@ export function createAvailableHandler() {
try { try {
const models: ModelDefinition[] = [ const models: ModelDefinition[] = [
{ {
id: "claude-opus-4-5-20251101", id: 'claude-opus-4-5-20251101',
name: "Claude Opus 4.5", name: 'Claude Opus 4.5',
provider: "anthropic", provider: 'anthropic',
contextWindow: 200000, contextWindow: 200000,
maxOutputTokens: 16384, maxOutputTokens: 16384,
supportsVision: true, supportsVision: true,
supportsTools: true, supportsTools: true,
}, },
{ {
id: "claude-sonnet-4-20250514", id: 'claude-sonnet-4-20250514',
name: "Claude Sonnet 4", name: 'Claude Sonnet 4',
provider: "anthropic", provider: 'anthropic',
contextWindow: 200000, contextWindow: 200000,
maxOutputTokens: 16384, maxOutputTokens: 16384,
supportsVision: true, supportsVision: true,
supportsTools: true, supportsTools: true,
}, },
{ {
id: "claude-3-5-sonnet-20241022", id: 'claude-3-5-sonnet-20241022',
name: "Claude 3.5 Sonnet", name: 'Claude 3.5 Sonnet',
provider: "anthropic", provider: 'anthropic',
contextWindow: 200000, contextWindow: 200000,
maxOutputTokens: 8192, maxOutputTokens: 8192,
supportsVision: true, supportsVision: true,
supportsTools: true, supportsTools: true,
}, },
{ {
id: "claude-3-5-haiku-20241022", id: 'claude-3-5-haiku-20241022',
name: "Claude 3.5 Haiku", name: 'Claude 3.5 Haiku',
provider: "anthropic", provider: 'anthropic',
contextWindow: 200000, contextWindow: 200000,
maxOutputTokens: 8192, maxOutputTokens: 8192,
supportsVision: true, supportsVision: true,
@@ -59,7 +59,7 @@ export function createAvailableHandler() {
res.json({ success: true, models }); res.json({ success: true, models });
} catch (error) { } catch (error) {
logError(error, "Get available models failed"); logError(error, 'Get available models failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,9 +2,9 @@
* GET /providers endpoint - Check provider status * GET /providers endpoint - Check provider status
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import { ProviderFactory } from "../../../providers/provider-factory.js"; import { ProviderFactory } from '../../../providers/provider-factory.js';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
export function createProvidersHandler() { export function createProvidersHandler() {
return async (_req: Request, res: Response): Promise<void> => { return async (_req: Request, res: Response): Promise<void> => {
@@ -21,7 +21,7 @@ export function createProvidersHandler() {
res.json({ success: true, providers }); res.json({ success: true, providers });
} catch (error) { } catch (error) {
logError(error, "Get providers failed"); logError(error, 'Get providers failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,13 +2,10 @@
* Common utilities for running-agents routes * Common utilities for running-agents routes
*/ */
import { createLogger } from "@automaker/utils"; import { createLogger } from '@automaker/utils';
import { import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
getErrorMessage as getErrorMessageShared,
createLogError,
} from "../common.js";
const logger = createLogger("RunningAgents"); const logger = createLogger('RunningAgents');
// Re-export shared utilities // Re-export shared utilities
export { getErrorMessageShared as getErrorMessage }; export { getErrorMessageShared as getErrorMessage };

View File

@@ -2,16 +2,14 @@
* Running Agents routes - HTTP API for tracking active agent executions * Running Agents routes - HTTP API for tracking active agent executions
*/ */
import { Router } from "express"; import { Router } from 'express';
import type { AutoModeService } from "../../services/auto-mode-service.js"; import type { AutoModeService } from '../../services/auto-mode-service.js';
import { createIndexHandler } from "./routes/index.js"; import { createIndexHandler } from './routes/index.js';
export function createRunningAgentsRoutes( export function createRunningAgentsRoutes(autoModeService: AutoModeService): Router {
autoModeService: AutoModeService
): Router {
const router = Router(); const router = Router();
router.get("/", createIndexHandler(autoModeService)); router.get('/', createIndexHandler(autoModeService));
return router; return router;
} }

View File

@@ -2,9 +2,9 @@
* GET / endpoint - Get all running agents * GET / endpoint - Get all running agents
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import type { AutoModeService } from "../../../services/auto-mode-service.js"; import type { AutoModeService } from '../../../services/auto-mode-service.js';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
export function createIndexHandler(autoModeService: AutoModeService) { export function createIndexHandler(autoModeService: AutoModeService) {
return async (_req: Request, res: Response): Promise<void> => { return async (_req: Request, res: Response): Promise<void> => {
@@ -18,7 +18,7 @@ export function createIndexHandler(autoModeService: AutoModeService) {
totalCount: runningAgents.length, totalCount: runningAgents.length,
}); });
} catch (error) { } catch (error) {
logError(error, "Get running agents failed"); logError(error, 'Get running agents failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,13 +2,10 @@
* Common utilities for sessions routes * Common utilities for sessions routes
*/ */
import { createLogger } from "@automaker/utils"; import { createLogger } from '@automaker/utils';
import { import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
getErrorMessage as getErrorMessageShared,
createLogError,
} from "../common.js";
const logger = createLogger("Sessions"); const logger = createLogger('Sessions');
// Re-export shared utilities // Re-export shared utilities
export { getErrorMessageShared as getErrorMessage }; export { getErrorMessageShared as getErrorMessage };

View File

@@ -2,24 +2,24 @@
* Sessions routes - HTTP API for session management * Sessions routes - HTTP API for session management
*/ */
import { Router } from "express"; import { Router } from 'express';
import { AgentService } from "../../services/agent-service.js"; import { AgentService } from '../../services/agent-service.js';
import { createIndexHandler } from "./routes/index.js"; import { createIndexHandler } from './routes/index.js';
import { createCreateHandler } from "./routes/create.js"; import { createCreateHandler } from './routes/create.js';
import { createUpdateHandler } from "./routes/update.js"; import { createUpdateHandler } from './routes/update.js';
import { createArchiveHandler } from "./routes/archive.js"; import { createArchiveHandler } from './routes/archive.js';
import { createUnarchiveHandler } from "./routes/unarchive.js"; import { createUnarchiveHandler } from './routes/unarchive.js';
import { createDeleteHandler } from "./routes/delete.js"; import { createDeleteHandler } from './routes/delete.js';
export function createSessionsRoutes(agentService: AgentService): Router { export function createSessionsRoutes(agentService: AgentService): Router {
const router = Router(); const router = Router();
router.get("/", createIndexHandler(agentService)); router.get('/', createIndexHandler(agentService));
router.post("/", createCreateHandler(agentService)); router.post('/', createCreateHandler(agentService));
router.put("/:sessionId", createUpdateHandler(agentService)); router.put('/:sessionId', createUpdateHandler(agentService));
router.post("/:sessionId/archive", createArchiveHandler(agentService)); router.post('/:sessionId/archive', createArchiveHandler(agentService));
router.post("/:sessionId/unarchive", createUnarchiveHandler(agentService)); router.post('/:sessionId/unarchive', createUnarchiveHandler(agentService));
router.delete("/:sessionId", createDeleteHandler(agentService)); router.delete('/:sessionId', createDeleteHandler(agentService));
return router; return router;
} }

View File

@@ -2,9 +2,9 @@
* POST /:sessionId/archive endpoint - Archive a session * POST /:sessionId/archive endpoint - Archive a session
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import { AgentService } from "../../../services/agent-service.js"; import { AgentService } from '../../../services/agent-service.js';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
export function createArchiveHandler(agentService: AgentService) { export function createArchiveHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
@@ -13,13 +13,13 @@ export function createArchiveHandler(agentService: AgentService) {
const success = await agentService.archiveSession(sessionId); const success = await agentService.archiveSession(sessionId);
if (!success) { if (!success) {
res.status(404).json({ success: false, error: "Session not found" }); res.status(404).json({ success: false, error: 'Session not found' });
return; return;
} }
res.json({ success: true }); res.json({ success: true });
} catch (error) { } catch (error) {
logError(error, "Archive session failed"); logError(error, 'Archive session failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,9 +2,9 @@
* POST / endpoint - Create a new session * POST / endpoint - Create a new session
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import { AgentService } from "../../../services/agent-service.js"; import { AgentService } from '../../../services/agent-service.js';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
export function createCreateHandler(agentService: AgentService) { export function createCreateHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
@@ -17,19 +17,14 @@ export function createCreateHandler(agentService: AgentService) {
}; };
if (!name) { if (!name) {
res.status(400).json({ success: false, error: "name is required" }); res.status(400).json({ success: false, error: 'name is required' });
return; return;
} }
const session = await agentService.createSession( const session = await agentService.createSession(name, projectPath, workingDirectory, model);
name,
projectPath,
workingDirectory,
model
);
res.json({ success: true, session }); res.json({ success: true, session });
} catch (error) { } catch (error) {
logError(error, "Create session failed"); logError(error, 'Create session failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,9 +2,9 @@
* DELETE /:sessionId endpoint - Delete a session * DELETE /:sessionId endpoint - Delete a session
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import { AgentService } from "../../../services/agent-service.js"; import { AgentService } from '../../../services/agent-service.js';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
export function createDeleteHandler(agentService: AgentService) { export function createDeleteHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
@@ -13,13 +13,13 @@ export function createDeleteHandler(agentService: AgentService) {
const success = await agentService.deleteSession(sessionId); const success = await agentService.deleteSession(sessionId);
if (!success) { if (!success) {
res.status(404).json({ success: false, error: "Session not found" }); res.status(404).json({ success: false, error: 'Session not found' });
return; return;
} }
res.json({ success: true }); res.json({ success: true });
} catch (error) { } catch (error) {
logError(error, "Delete session failed"); logError(error, 'Delete session failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,14 +2,14 @@
* GET / endpoint - List all sessions * GET / endpoint - List all sessions
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import { AgentService } from "../../../services/agent-service.js"; import { AgentService } from '../../../services/agent-service.js';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
export function createIndexHandler(agentService: AgentService) { export function createIndexHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
try { try {
const includeArchived = req.query.includeArchived === "true"; const includeArchived = req.query.includeArchived === 'true';
const sessionsRaw = await agentService.listSessions(includeArchived); const sessionsRaw = await agentService.listSessions(includeArchived);
// Transform to match frontend SessionListItem interface // Transform to match frontend SessionListItem interface
@@ -17,7 +17,7 @@ export function createIndexHandler(agentService: AgentService) {
sessionsRaw.map(async (s) => { sessionsRaw.map(async (s) => {
const messages = await agentService.loadSession(s.id); const messages = await agentService.loadSession(s.id);
const lastMessage = messages[messages.length - 1]; const lastMessage = messages[messages.length - 1];
const preview = lastMessage?.content?.slice(0, 100) || ""; const preview = lastMessage?.content?.slice(0, 100) || '';
return { return {
id: s.id, id: s.id,
@@ -36,7 +36,7 @@ export function createIndexHandler(agentService: AgentService) {
res.json({ success: true, sessions }); res.json({ success: true, sessions });
} catch (error) { } catch (error) {
logError(error, "List sessions failed"); logError(error, 'List sessions failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,9 +2,9 @@
* POST /:sessionId/unarchive endpoint - Unarchive a session * POST /:sessionId/unarchive endpoint - Unarchive a session
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import { AgentService } from "../../../services/agent-service.js"; import { AgentService } from '../../../services/agent-service.js';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
export function createUnarchiveHandler(agentService: AgentService) { export function createUnarchiveHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
@@ -13,13 +13,13 @@ export function createUnarchiveHandler(agentService: AgentService) {
const success = await agentService.unarchiveSession(sessionId); const success = await agentService.unarchiveSession(sessionId);
if (!success) { if (!success) {
res.status(404).json({ success: false, error: "Session not found" }); res.status(404).json({ success: false, error: 'Session not found' });
return; return;
} }
res.json({ success: true }); res.json({ success: true });
} catch (error) { } catch (error) {
logError(error, "Unarchive session failed"); logError(error, 'Unarchive session failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,9 +2,9 @@
* PUT /:sessionId endpoint - Update a session * PUT /:sessionId endpoint - Update a session
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import { AgentService } from "../../../services/agent-service.js"; import { AgentService } from '../../../services/agent-service.js';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
export function createUpdateHandler(agentService: AgentService) { export function createUpdateHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
@@ -22,13 +22,13 @@ export function createUpdateHandler(agentService: AgentService) {
model, model,
}); });
if (!session) { if (!session) {
res.status(404).json({ success: false, error: "Session not found" }); res.status(404).json({ success: false, error: 'Session not found' });
return; return;
} }
res.json({ success: true, session }); res.json({ success: true, session });
} catch (error) { } catch (error) {
logError(error, "Update session failed"); logError(error, 'Update session failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -5,14 +5,11 @@
* Re-exports error handling helpers from the parent routes module. * Re-exports error handling helpers from the parent routes module.
*/ */
import { createLogger } from "@automaker/utils"; import { createLogger } from '@automaker/utils';
import { import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
getErrorMessage as getErrorMessageShared,
createLogError,
} from "../common.js";
/** Logger instance for settings-related operations */ /** Logger instance for settings-related operations */
export const logger = createLogger("Settings"); export const logger = createLogger('Settings');
/** /**
* Extract user-friendly error message from error objects * Extract user-friendly error message from error objects

View File

@@ -12,17 +12,17 @@
* Mounted at /api/settings in the main server. * Mounted at /api/settings in the main server.
*/ */
import { Router } from "express"; import { Router } from 'express';
import type { SettingsService } from "../../services/settings-service.js"; import type { SettingsService } from '../../services/settings-service.js';
import { validatePathParams } from "../../middleware/validate-paths.js"; import { validatePathParams } from '../../middleware/validate-paths.js';
import { createGetGlobalHandler } from "./routes/get-global.js"; import { createGetGlobalHandler } from './routes/get-global.js';
import { createUpdateGlobalHandler } from "./routes/update-global.js"; import { createUpdateGlobalHandler } from './routes/update-global.js';
import { createGetCredentialsHandler } from "./routes/get-credentials.js"; import { createGetCredentialsHandler } from './routes/get-credentials.js';
import { createUpdateCredentialsHandler } from "./routes/update-credentials.js"; import { createUpdateCredentialsHandler } from './routes/update-credentials.js';
import { createGetProjectHandler } from "./routes/get-project.js"; import { createGetProjectHandler } from './routes/get-project.js';
import { createUpdateProjectHandler } from "./routes/update-project.js"; import { createUpdateProjectHandler } from './routes/update-project.js';
import { createMigrateHandler } from "./routes/migrate.js"; import { createMigrateHandler } from './routes/migrate.js';
import { createStatusHandler } from "./routes/status.js"; import { createStatusHandler } from './routes/status.js';
/** /**
* Create settings router with all endpoints * Create settings router with all endpoints
@@ -47,22 +47,30 @@ export function createSettingsRoutes(settingsService: SettingsService): Router {
const router = Router(); const router = Router();
// Status endpoint (check if migration needed) // Status endpoint (check if migration needed)
router.get("/status", createStatusHandler(settingsService)); router.get('/status', createStatusHandler(settingsService));
// Global settings // Global settings
router.get("/global", createGetGlobalHandler(settingsService)); router.get('/global', createGetGlobalHandler(settingsService));
router.put("/global", createUpdateGlobalHandler(settingsService)); router.put('/global', createUpdateGlobalHandler(settingsService));
// Credentials (separate for security) // Credentials (separate for security)
router.get("/credentials", createGetCredentialsHandler(settingsService)); router.get('/credentials', createGetCredentialsHandler(settingsService));
router.put("/credentials", createUpdateCredentialsHandler(settingsService)); router.put('/credentials', createUpdateCredentialsHandler(settingsService));
// Project settings // Project settings
router.post("/project", validatePathParams("projectPath"), createGetProjectHandler(settingsService)); router.post(
router.put("/project", validatePathParams("projectPath"), createUpdateProjectHandler(settingsService)); '/project',
validatePathParams('projectPath'),
createGetProjectHandler(settingsService)
);
router.put(
'/project',
validatePathParams('projectPath'),
createUpdateProjectHandler(settingsService)
);
// Migration from localStorage // Migration from localStorage
router.post("/migrate", createMigrateHandler(settingsService)); router.post('/migrate', createMigrateHandler(settingsService));
return router; return router;
} }

View File

@@ -8,9 +8,9 @@
* Response: `{ "success": true, "credentials": { anthropic } }` * Response: `{ "success": true, "credentials": { anthropic } }`
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import type { SettingsService } from "../../../services/settings-service.js"; import type { SettingsService } from '../../../services/settings-service.js';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
/** /**
* Create handler factory for GET /api/settings/credentials * Create handler factory for GET /api/settings/credentials
@@ -28,7 +28,7 @@ export function createGetCredentialsHandler(settingsService: SettingsService) {
credentials, credentials,
}); });
} catch (error) { } catch (error) {
logError(error, "Get credentials failed"); logError(error, 'Get credentials failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -7,9 +7,9 @@
* Response: `{ "success": true, "settings": GlobalSettings }` * Response: `{ "success": true, "settings": GlobalSettings }`
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import type { SettingsService } from "../../../services/settings-service.js"; import type { SettingsService } from '../../../services/settings-service.js';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
/** /**
* Create handler factory for GET /api/settings/global * Create handler factory for GET /api/settings/global
@@ -27,7 +27,7 @@ export function createGetGlobalHandler(settingsService: SettingsService) {
settings, settings,
}); });
} catch (error) { } catch (error) {
logError(error, "Get global settings failed"); logError(error, 'Get global settings failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -8,9 +8,9 @@
* Response: `{ "success": true, "settings": ProjectSettings }` * Response: `{ "success": true, "settings": ProjectSettings }`
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import type { SettingsService } from "../../../services/settings-service.js"; import type { SettingsService } from '../../../services/settings-service.js';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
/** /**
* Create handler factory for POST /api/settings/project * Create handler factory for POST /api/settings/project
@@ -23,10 +23,10 @@ export function createGetProjectHandler(settingsService: SettingsService) {
try { try {
const { projectPath } = req.body as { projectPath?: string }; const { projectPath } = req.body as { projectPath?: string };
if (!projectPath || typeof projectPath !== "string") { if (!projectPath || typeof projectPath !== 'string') {
res.status(400).json({ res.status(400).json({
success: false, success: false,
error: "projectPath is required", error: 'projectPath is required',
}); });
return; return;
} }
@@ -38,7 +38,7 @@ export function createGetProjectHandler(settingsService: SettingsService) {
settings, settings,
}); });
} catch (error) { } catch (error) {
logError(error, "Get project settings failed"); logError(error, 'Get project settings failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -30,9 +30,9 @@
* ``` * ```
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import type { SettingsService } from "../../../services/settings-service.js"; import type { SettingsService } from '../../../services/settings-service.js';
import { getErrorMessage, logError, logger } from "../common.js"; import { getErrorMessage, logError, logger } from '../common.js';
/** /**
* Create handler factory for POST /api/settings/migrate * Create handler factory for POST /api/settings/migrate
@@ -45,32 +45,30 @@ export function createMigrateHandler(settingsService: SettingsService) {
try { try {
const { data } = req.body as { const { data } = req.body as {
data?: { data?: {
"automaker-storage"?: string; 'automaker-storage'?: string;
"automaker-setup"?: string; 'automaker-setup'?: string;
"worktree-panel-collapsed"?: string; 'worktree-panel-collapsed'?: string;
"file-browser-recent-folders"?: string; 'file-browser-recent-folders'?: string;
"automaker:lastProjectDir"?: string; 'automaker:lastProjectDir'?: string;
}; };
}; };
if (!data || typeof data !== "object") { if (!data || typeof data !== 'object') {
res.status(400).json({ res.status(400).json({
success: false, success: false,
error: "data object is required containing localStorage data", error: 'data object is required containing localStorage data',
}); });
return; return;
} }
logger.info("Starting settings migration from localStorage"); logger.info('Starting settings migration from localStorage');
const result = await settingsService.migrateFromLocalStorage(data); const result = await settingsService.migrateFromLocalStorage(data);
if (result.success) { if (result.success) {
logger.info( logger.info(`Migration successful: ${result.migratedProjectCount} projects migrated`);
`Migration successful: ${result.migratedProjectCount} projects migrated`
);
} else { } else {
logger.warn(`Migration completed with errors: ${result.errors.join(", ")}`); logger.warn(`Migration completed with errors: ${result.errors.join(', ')}`);
} }
res.json({ res.json({
@@ -81,7 +79,7 @@ export function createMigrateHandler(settingsService: SettingsService) {
errors: result.errors, errors: result.errors,
}); });
} catch (error) { } catch (error) {
logError(error, "Migration failed"); logError(error, 'Migration failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -16,9 +16,9 @@
* ``` * ```
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import type { SettingsService } from "../../../services/settings-service.js"; import type { SettingsService } from '../../../services/settings-service.js';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
/** /**
* Create handler factory for GET /api/settings/status * Create handler factory for GET /api/settings/status
@@ -40,7 +40,7 @@ export function createStatusHandler(settingsService: SettingsService) {
needsMigration: !hasGlobalSettings, needsMigration: !hasGlobalSettings,
}); });
} catch (error) { } catch (error) {
logError(error, "Get settings status failed"); logError(error, 'Get settings status failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -8,10 +8,10 @@
* Response: `{ "success": true, "credentials": { anthropic } }` * Response: `{ "success": true, "credentials": { anthropic } }`
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import type { SettingsService } from "../../../services/settings-service.js"; import type { SettingsService } from '../../../services/settings-service.js';
import type { Credentials } from "../../../types/settings.js"; import type { Credentials } from '../../../types/settings.js';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
/** /**
* Create handler factory for PUT /api/settings/credentials * 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 * @param settingsService - Instance of SettingsService for file I/O
* @returns Express request handler * @returns Express request handler
*/ */
export function createUpdateCredentialsHandler( export function createUpdateCredentialsHandler(settingsService: SettingsService) {
settingsService: SettingsService
) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
try { try {
const updates = req.body as Partial<Credentials>; const updates = req.body as Partial<Credentials>;
if (!updates || typeof updates !== "object") { if (!updates || typeof updates !== 'object') {
res.status(400).json({ res.status(400).json({
success: false, success: false,
error: "Invalid request body - expected credentials object", error: 'Invalid request body - expected credentials object',
}); });
return; return;
} }
@@ -44,7 +42,7 @@ export function createUpdateCredentialsHandler(
credentials: masked, credentials: masked,
}); });
} catch (error) { } catch (error) {
logError(error, "Update credentials failed"); logError(error, 'Update credentials failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -8,10 +8,10 @@
* Response: `{ "success": true, "settings": GlobalSettings }` * Response: `{ "success": true, "settings": GlobalSettings }`
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import type { SettingsService } from "../../../services/settings-service.js"; import type { SettingsService } from '../../../services/settings-service.js';
import type { GlobalSettings } from "../../../types/settings.js"; import type { GlobalSettings } from '../../../types/settings.js';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
/** /**
* Create handler factory for PUT /api/settings/global * Create handler factory for PUT /api/settings/global
@@ -24,10 +24,10 @@ export function createUpdateGlobalHandler(settingsService: SettingsService) {
try { try {
const updates = req.body as Partial<GlobalSettings>; const updates = req.body as Partial<GlobalSettings>;
if (!updates || typeof updates !== "object") { if (!updates || typeof updates !== 'object') {
res.status(400).json({ res.status(400).json({
success: false, success: false,
error: "Invalid request body - expected settings object", error: 'Invalid request body - expected settings object',
}); });
return; return;
} }
@@ -39,7 +39,7 @@ export function createUpdateGlobalHandler(settingsService: SettingsService) {
settings, settings,
}); });
} catch (error) { } catch (error) {
logError(error, "Update global settings failed"); logError(error, 'Update global settings failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -8,10 +8,10 @@
* Response: `{ "success": true, "settings": ProjectSettings }` * Response: `{ "success": true, "settings": ProjectSettings }`
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import type { SettingsService } from "../../../services/settings-service.js"; import type { SettingsService } from '../../../services/settings-service.js';
import type { ProjectSettings } from "../../../types/settings.js"; import type { ProjectSettings } from '../../../types/settings.js';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
/** /**
* Create handler factory for PUT /api/settings/project * Create handler factory for PUT /api/settings/project
@@ -27,33 +27,30 @@ export function createUpdateProjectHandler(settingsService: SettingsService) {
updates?: Partial<ProjectSettings>; updates?: Partial<ProjectSettings>;
}; };
if (!projectPath || typeof projectPath !== "string") { if (!projectPath || typeof projectPath !== 'string') {
res.status(400).json({ res.status(400).json({
success: false, success: false,
error: "projectPath is required", error: 'projectPath is required',
}); });
return; return;
} }
if (!updates || typeof updates !== "object") { if (!updates || typeof updates !== 'object') {
res.status(400).json({ res.status(400).json({
success: false, success: false,
error: "updates object is required", error: 'updates object is required',
}); });
return; return;
} }
const settings = await settingsService.updateProjectSettings( const settings = await settingsService.updateProjectSettings(projectPath, updates);
projectPath,
updates
);
res.json({ res.json({
success: true, success: true,
settings, settings,
}); });
} catch (error) { } catch (error) {
logError(error, "Update project settings failed"); logError(error, 'Update project settings failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,15 +2,12 @@
* Common utilities and state for setup routes * Common utilities and state for setup routes
*/ */
import { createLogger } from "@automaker/utils"; import { createLogger } from '@automaker/utils';
import path from "path"; import path from 'path';
import fs from "fs/promises"; import fs from 'fs/promises';
import { import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
getErrorMessage as getErrorMessageShared,
createLogError,
} from "../common.js";
const logger = createLogger("Setup"); const logger = createLogger('Setup');
// Storage for API keys (in-memory cache) - private // Storage for API keys (in-memory cache) - private
const apiKeys: Record<string, string> = {}; const apiKeys: Record<string, string> = {};
@@ -39,22 +36,19 @@ export function getAllApiKeys(): Record<string, string> {
/** /**
* Helper to persist API keys to .env file * Helper to persist API keys to .env file
*/ */
export async function persistApiKeyToEnv( export async function persistApiKeyToEnv(key: string, value: string): Promise<void> {
key: string, const envPath = path.join(process.cwd(), '.env');
value: string
): Promise<void> {
const envPath = path.join(process.cwd(), ".env");
try { try {
let envContent = ""; let envContent = '';
try { try {
envContent = await fs.readFile(envPath, "utf-8"); envContent = await fs.readFile(envPath, 'utf-8');
} catch { } catch {
// .env file doesn't exist, we'll create it // .env file doesn't exist, we'll create it
} }
// Parse existing env content // Parse existing env content
const lines = envContent.split("\n"); const lines = envContent.split('\n');
const keyRegex = new RegExp(`^${key}=`); const keyRegex = new RegExp(`^${key}=`);
let found = false; let found = false;
const newLines = lines.map((line) => { const newLines = lines.map((line) => {
@@ -70,7 +64,7 @@ export async function persistApiKeyToEnv(
newLines.push(`${key}=${value}`); 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`); logger.info(`[Setup] Persisted ${key} to .env file`);
} catch (error) { } catch (error) {
logger.error(`[Setup] Failed to persist ${key} to .env:`, error); logger.error(`[Setup] Failed to persist ${key} to .env:`, error);

View File

@@ -2,36 +2,36 @@
* Business logic for getting Claude CLI status * Business logic for getting Claude CLI status
*/ */
import { exec } from "child_process"; import { exec } from 'child_process';
import { promisify } from "util"; import { promisify } from 'util';
import os from "os"; import os from 'os';
import path from "path"; import path from 'path';
import fs from "fs/promises"; import fs from 'fs/promises';
import { getApiKey } from "./common.js"; import { getApiKey } from './common.js';
const execAsync = promisify(exec); const execAsync = promisify(exec);
export async function getClaudeStatus() { export async function getClaudeStatus() {
let installed = false; let installed = false;
let version = ""; let version = '';
let cliPath = ""; let cliPath = '';
let method = "none"; let method = 'none';
const isWindows = process.platform === "win32"; const isWindows = process.platform === 'win32';
// Try to find Claude CLI using platform-specific command // Try to find Claude CLI using platform-specific command
try { try {
// Use 'where' on Windows, 'which' on Unix-like systems // 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); const { stdout } = await execAsync(findCommand);
// 'where' on Windows can return multiple paths - take the first one // 'where' on Windows can return multiple paths - take the first one
cliPath = stdout.trim().split(/\r?\n/)[0]; cliPath = stdout.trim().split(/\r?\n/)[0];
installed = true; installed = true;
method = "path"; method = 'path';
// Get version // Get version
try { try {
const { stdout: versionOut } = await execAsync("claude --version"); const { stdout: versionOut } = await execAsync('claude --version');
version = versionOut.trim(); version = versionOut.trim();
} catch { } catch {
// Version command might not be available // Version command might not be available
@@ -40,22 +40,22 @@ export async function getClaudeStatus() {
// Not in PATH, try common locations based on platform // Not in PATH, try common locations based on platform
const commonPaths = isWindows 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 [ return [
// Windows-specific paths // Windows-specific paths
path.join(os.homedir(), ".local", "bin", "claude.exe"), path.join(os.homedir(), '.local', 'bin', 'claude.exe'),
path.join(appData, "npm", "claude.cmd"), path.join(appData, 'npm', 'claude.cmd'),
path.join(appData, "npm", "claude"), path.join(appData, 'npm', 'claude'),
path.join(appData, ".npm-global", "bin", "claude.cmd"), path.join(appData, '.npm-global', 'bin', 'claude.cmd'),
path.join(appData, ".npm-global", "bin", "claude"), path.join(appData, '.npm-global', 'bin', 'claude'),
]; ];
})() })()
: [ : [
// Unix (Linux/macOS) paths // Unix (Linux/macOS) paths
path.join(os.homedir(), ".local", "bin", "claude"), path.join(os.homedir(), '.local', 'bin', 'claude'),
path.join(os.homedir(), ".claude", "local", "claude"), path.join(os.homedir(), '.claude', 'local', 'claude'),
"/usr/local/bin/claude", '/usr/local/bin/claude',
path.join(os.homedir(), ".npm-global", "bin", "claude"), path.join(os.homedir(), '.npm-global', 'bin', 'claude'),
]; ];
for (const p of commonPaths) { for (const p of commonPaths) {
@@ -63,7 +63,7 @@ export async function getClaudeStatus() {
await fs.access(p); await fs.access(p);
cliPath = p; cliPath = p;
installed = true; installed = true;
method = "local"; method = 'local';
// Get version from this path // Get version from this path
try { try {
@@ -84,11 +84,11 @@ export async function getClaudeStatus() {
// apiKeys.anthropic stores direct API keys for pay-per-use // apiKeys.anthropic stores direct API keys for pay-per-use
let auth = { let auth = {
authenticated: false, authenticated: false,
method: "none" as string, method: 'none' as string,
hasCredentialsFile: false, hasCredentialsFile: false,
hasToken: false, hasToken: false,
hasStoredOAuthToken: !!getApiKey("anthropic_oauth_token"), hasStoredOAuthToken: !!getApiKey('anthropic_oauth_token'),
hasStoredApiKey: !!getApiKey("anthropic"), hasStoredApiKey: !!getApiKey('anthropic'),
hasEnvApiKey: !!process.env.ANTHROPIC_API_KEY, hasEnvApiKey: !!process.env.ANTHROPIC_API_KEY,
// Additional fields for detailed status // Additional fields for detailed status
oauthTokenValid: false, oauthTokenValid: false,
@@ -97,13 +97,13 @@ export async function getClaudeStatus() {
hasRecentActivity: false, 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 // Check for recent Claude CLI activity - indicates working authentication
// The stats-cache.json file is only populated when the CLI is working properly // 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 { try {
const statsContent = await fs.readFile(statsCachePath, "utf-8"); const statsContent = await fs.readFile(statsCachePath, 'utf-8');
const stats = JSON.parse(statsContent); const stats = JSON.parse(statsContent);
// Check if there's any activity (which means the CLI is authenticated and working) // 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.hasRecentActivity = true;
auth.hasCliAuth = true; auth.hasCliAuth = true;
auth.authenticated = true; auth.authenticated = true;
auth.method = "cli_authenticated"; auth.method = 'cli_authenticated';
} }
} catch { } catch {
// Stats file doesn't exist or is invalid // Stats file doesn't exist or is invalid
} }
// Check for settings.json - indicates CLI has been set up // 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 { try {
await fs.access(settingsPath); await fs.access(settingsPath);
// If settings exist but no activity, CLI might be set up but not authenticated // If settings exist but no activity, CLI might be set up but not authenticated
if (!auth.hasCliAuth) { if (!auth.hasCliAuth) {
// Try to check for other indicators of auth // Try to check for other indicators of auth
const sessionsDir = path.join(claudeDir, "projects"); const sessionsDir = path.join(claudeDir, 'projects');
try { try {
const sessions = await fs.readdir(sessionsDir); const sessions = await fs.readdir(sessionsDir);
if (sessions.length > 0) { if (sessions.length > 0) {
auth.hasCliAuth = true; auth.hasCliAuth = true;
auth.authenticated = true; auth.authenticated = true;
auth.method = "cli_authenticated"; auth.method = 'cli_authenticated';
} }
} catch { } catch {
// Sessions directory doesn't exist // Sessions directory doesn't exist
@@ -143,13 +143,13 @@ export async function getClaudeStatus() {
// Check for credentials file (OAuth tokens from claude login) // Check for credentials file (OAuth tokens from claude login)
// Note: Claude CLI may use ".credentials.json" (hidden) or "credentials.json" depending on version/platform // Note: Claude CLI may use ".credentials.json" (hidden) or "credentials.json" depending on version/platform
const credentialsPaths = [ 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) { for (const credentialsPath of credentialsPaths) {
try { try {
const credentialsContent = await fs.readFile(credentialsPath, "utf-8"); const credentialsContent = await fs.readFile(credentialsPath, 'utf-8');
const credentials = JSON.parse(credentialsContent); const credentials = JSON.parse(credentialsContent);
auth.hasCredentialsFile = true; auth.hasCredentialsFile = true;
@@ -158,11 +158,11 @@ export async function getClaudeStatus() {
auth.hasStoredOAuthToken = true; auth.hasStoredOAuthToken = true;
auth.oauthTokenValid = true; auth.oauthTokenValid = true;
auth.authenticated = 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) { } else if (credentials.api_key) {
auth.apiKeyValid = true; auth.apiKeyValid = true;
auth.authenticated = 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 break; // Found and processed credentials file
} catch { } catch {
@@ -174,25 +174,25 @@ export async function getClaudeStatus() {
if (auth.hasEnvApiKey) { if (auth.hasEnvApiKey) {
auth.authenticated = true; auth.authenticated = true;
auth.apiKeyValid = 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) // 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.authenticated = true;
auth.oauthTokenValid = 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) // 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.authenticated = true;
auth.apiKeyValid = true; auth.apiKeyValid = true;
auth.method = "api_key"; // Manually stored API key auth.method = 'api_key'; // Manually stored API key
} }
return { return {
status: installed ? "installed" : "not_installed", status: installed ? 'installed' : 'not_installed',
installed, installed,
method, method,
version, version,

View File

@@ -2,29 +2,29 @@
* Setup routes - HTTP API for CLI detection, API keys, and platform info * Setup routes - HTTP API for CLI detection, API keys, and platform info
*/ */
import { Router } from "express"; import { Router } from 'express';
import { createClaudeStatusHandler } from "./routes/claude-status.js"; import { createClaudeStatusHandler } from './routes/claude-status.js';
import { createInstallClaudeHandler } from "./routes/install-claude.js"; import { createInstallClaudeHandler } from './routes/install-claude.js';
import { createAuthClaudeHandler } from "./routes/auth-claude.js"; import { createAuthClaudeHandler } from './routes/auth-claude.js';
import { createStoreApiKeyHandler } from "./routes/store-api-key.js"; import { createStoreApiKeyHandler } from './routes/store-api-key.js';
import { createDeleteApiKeyHandler } from "./routes/delete-api-key.js"; import { createDeleteApiKeyHandler } from './routes/delete-api-key.js';
import { createApiKeysHandler } from "./routes/api-keys.js"; import { createApiKeysHandler } from './routes/api-keys.js';
import { createPlatformHandler } from "./routes/platform.js"; import { createPlatformHandler } from './routes/platform.js';
import { createVerifyClaudeAuthHandler } from "./routes/verify-claude-auth.js"; import { createVerifyClaudeAuthHandler } from './routes/verify-claude-auth.js';
import { createGhStatusHandler } from "./routes/gh-status.js"; import { createGhStatusHandler } from './routes/gh-status.js';
export function createSetupRoutes(): Router { export function createSetupRoutes(): Router {
const router = Router(); const router = Router();
router.get("/claude-status", createClaudeStatusHandler()); router.get('/claude-status', createClaudeStatusHandler());
router.post("/install-claude", createInstallClaudeHandler()); router.post('/install-claude', createInstallClaudeHandler());
router.post("/auth-claude", createAuthClaudeHandler()); router.post('/auth-claude', createAuthClaudeHandler());
router.post("/store-api-key", createStoreApiKeyHandler()); router.post('/store-api-key', createStoreApiKeyHandler());
router.post("/delete-api-key", createDeleteApiKeyHandler()); router.post('/delete-api-key', createDeleteApiKeyHandler());
router.get("/api-keys", createApiKeysHandler()); router.get('/api-keys', createApiKeysHandler());
router.get("/platform", createPlatformHandler()); router.get('/platform', createPlatformHandler());
router.post("/verify-claude-auth", createVerifyClaudeAuthHandler()); router.post('/verify-claude-auth', createVerifyClaudeAuthHandler());
router.get("/gh-status", createGhStatusHandler()); router.get('/gh-status', createGhStatusHandler());
return router; return router;
} }

View File

@@ -2,19 +2,18 @@
* GET /api-keys endpoint - Get API keys status * GET /api-keys endpoint - Get API keys status
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import { getApiKey, getErrorMessage, logError } from "../common.js"; import { getApiKey, getErrorMessage, logError } from '../common.js';
export function createApiKeysHandler() { export function createApiKeysHandler() {
return async (_req: Request, res: Response): Promise<void> => { return async (_req: Request, res: Response): Promise<void> => {
try { try {
res.json({ res.json({
success: true, success: true,
hasAnthropicKey: hasAnthropicKey: !!getApiKey('anthropic') || !!process.env.ANTHROPIC_API_KEY,
!!getApiKey("anthropic") || !!process.env.ANTHROPIC_API_KEY,
}); });
} catch (error) { } catch (error) {
logError(error, "Get API keys failed"); logError(error, 'Get API keys failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,8 +2,8 @@
* POST /auth-claude endpoint - Auth Claude * POST /auth-claude endpoint - Auth Claude
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
export function createAuthClaudeHandler() { export function createAuthClaudeHandler() {
return async (_req: Request, res: Response): Promise<void> => { return async (_req: Request, res: Response): Promise<void> => {
@@ -11,11 +11,11 @@ export function createAuthClaudeHandler() {
res.json({ res.json({
success: true, success: true,
requiresManualAuth: true, requiresManualAuth: true,
command: "claude login", command: 'claude login',
message: "Please run 'claude login' in your terminal to authenticate", message: "Please run 'claude login' in your terminal to authenticate",
}); });
} catch (error) { } catch (error) {
logError(error, "Auth Claude failed"); logError(error, 'Auth Claude failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,9 +2,9 @@
* GET /claude-status endpoint - Get Claude CLI status * GET /claude-status endpoint - Get Claude CLI status
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import { getClaudeStatus } from "../get-claude-status.js"; import { getClaudeStatus } from '../get-claude-status.js';
import { getErrorMessage, logError } from "../common.js"; import { getErrorMessage, logError } from '../common.js';
export function createClaudeStatusHandler() { export function createClaudeStatusHandler() {
return async (_req: Request, res: Response): Promise<void> => { return async (_req: Request, res: Response): Promise<void> => {
@@ -15,7 +15,7 @@ export function createClaudeStatusHandler() {
...status, ...status,
}); });
} catch (error) { } catch (error) {
logError(error, "Get Claude status failed"); logError(error, 'Get Claude status failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }
}; };

View File

@@ -2,43 +2,43 @@
* POST /delete-api-key endpoint - Delete a stored API key * POST /delete-api-key endpoint - Delete a stored API key
*/ */
import type { Request, Response } from "express"; import type { Request, Response } from 'express';
import { createLogger } from "@automaker/utils"; import { createLogger } from '@automaker/utils';
import path from "path"; import path from 'path';
import fs from "fs/promises"; import fs from 'fs/promises';
const logger = createLogger("Setup"); const logger = createLogger('Setup');
// In-memory storage reference (imported from common.ts pattern) // In-memory storage reference (imported from common.ts pattern)
// We need to modify common.ts to export a deleteApiKey function // 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 * Remove an API key from the .env file
*/ */
async function removeApiKeyFromEnv(key: string): Promise<void> { async function removeApiKeyFromEnv(key: string): Promise<void> {
const envPath = path.join(process.cwd(), ".env"); const envPath = path.join(process.cwd(), '.env');
try { try {
let envContent = ""; let envContent = '';
try { try {
envContent = await fs.readFile(envPath, "utf-8"); envContent = await fs.readFile(envPath, 'utf-8');
} catch { } catch {
// .env file doesn't exist, nothing to delete // .env file doesn't exist, nothing to delete
return; return;
} }
// Parse existing env content and remove the key // Parse existing env content and remove the key
const lines = envContent.split("\n"); const lines = envContent.split('\n');
const keyRegex = new RegExp(`^${key}=`); const keyRegex = new RegExp(`^${key}=`);
const newLines = lines.filter((line) => !keyRegex.test(line)); const newLines = lines.filter((line) => !keyRegex.test(line));
// Remove empty lines at the end // 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(); 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`); logger.info(`[Setup] Removed ${key} from .env file`);
} catch (error) { } catch (error) {
logger.error(`[Setup] Failed to remove ${key} from .env:`, error); logger.error(`[Setup] Failed to remove ${key} from .env:`, error);
@@ -54,7 +54,7 @@ export function createDeleteApiKeyHandler() {
if (!provider) { if (!provider) {
res.status(400).json({ res.status(400).json({
success: false, success: false,
error: "Provider is required", error: 'Provider is required',
}); });
return; return;
} }
@@ -63,7 +63,7 @@ export function createDeleteApiKeyHandler() {
// Map provider to env key name // Map provider to env key name
const envKeyMap: Record<string, string> = { const envKeyMap: Record<string, string> = {
anthropic: "ANTHROPIC_API_KEY", anthropic: 'ANTHROPIC_API_KEY',
}; };
const envKey = envKeyMap[provider]; const envKey = envKeyMap[provider];
@@ -76,7 +76,7 @@ export function createDeleteApiKeyHandler() {
} }
// Clear from in-memory storage // Clear from in-memory storage
setApiKey(provider, ""); setApiKey(provider, '');
// Remove from environment // Remove from environment
delete process.env[envKey]; delete process.env[envKey];
@@ -91,14 +91,11 @@ export function createDeleteApiKeyHandler() {
message: `API key for ${provider} has been deleted`, message: `API key for ${provider} has been deleted`,
}); });
} catch (error) { } catch (error) {
logger.error("[Setup] Delete API key error:", error); logger.error('[Setup] Delete API key error:', error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
error: error instanceof Error ? error.message : "Failed to delete API key", error: error instanceof Error ? error.message : 'Failed to delete API key',
}); });
} }
}; };
} }

Some files were not shown because too many files have changed in this diff Show More