Skip to content

Commit b8bb402

Browse files
committed
fix(auth): handle CLI not found errors with helpful messages
Add error handling for missing CLI commands in authentication flows Implement consistent error messages and installation instructions Update runner modules to handle missing CLI executables Simplify workflow configuration by removing unused parameters
1 parent da4ffb5 commit b8bb402

File tree

7 files changed

+161
-40
lines changed

7 files changed

+161
-40
lines changed

src/infra/engines/providers/claude/auth.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,34 @@ export async function ensureAuth(options?: ClaudeAuthOptions): Promise<boolean>
124124
console.log(`\nRunning Claude authentication...\n`);
125125
console.log(`Config directory: ${configDir}\n`);
126126

127-
await execa('claude', ['setup-token'], {
128-
env: { ...process.env, CLAUDE_CONFIG_DIR: configDir },
129-
stdio: 'inherit',
130-
});
127+
try {
128+
await execa('claude', ['setup-token'], {
129+
env: { ...process.env, CLAUDE_CONFIG_DIR: configDir },
130+
stdio: 'inherit',
131+
});
132+
} catch (error) {
133+
const err = error as unknown as { code?: string; stderr?: string; message?: string };
134+
const stderr = err?.stderr ?? '';
135+
const message = err?.message ?? '';
136+
const notFound =
137+
err?.code === 'ENOENT' ||
138+
/not recognized as an internal or external command/i.test(stderr || message) ||
139+
/command not found/i.test(stderr || message) ||
140+
/No such file or directory/i.test(stderr || message);
141+
142+
if (notFound) {
143+
console.error(`\n────────────────────────────────────────────────────────────`);
144+
console.error(` ⚠️ ${metadata.name} CLI Not Found`);
145+
console.error(`────────────────────────────────────────────────────────────`);
146+
console.error(`\n'${metadata.cliBinary} setup-token' failed because the CLI is missing.`);
147+
console.error(`Please install ${metadata.name} CLI before trying again:\n`);
148+
console.error(` ${metadata.installCommand}\n`);
149+
console.error(`────────────────────────────────────────────────────────────\n`);
150+
throw new Error(`${metadata.name} CLI is not installed.`);
151+
}
152+
153+
throw error;
154+
}
131155

132156
// Verify the credentials were created
133157
try {

src/infra/engines/providers/claude/execution/runner.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { homedir } from 'node:os';
33

44
import { spawnProcess } from '../../../../process/spawn.js';
55
import { buildClaudeExecCommand } from './commands.js';
6+
import { metadata } from '../metadata.js';
67
import { expandHomeDir } from '../../../../../shared/utils/index.js';
78
import { logger } from '../../../../../shared/logging/index.js';
89

@@ -124,12 +125,14 @@ export async function runClaude(options: RunClaudeOptions): Promise<RunClaudeRes
124125
logger.debug(`Claude runner - prompt length: ${prompt.length}, lines: ${prompt.split('\n').length}`);
125126
logger.debug(`Claude runner - args count: ${args.length}, model: ${model ?? 'default'}`);
126127

127-
const result = await spawnProcess({
128-
command,
129-
args,
130-
cwd: workingDir,
131-
env: mergedEnv,
132-
stdinInput: prompt, // Pass prompt via stdin instead of command-line argument
128+
let result;
129+
try {
130+
result = await spawnProcess({
131+
command,
132+
args,
133+
cwd: workingDir,
134+
env: mergedEnv,
135+
stdinInput: prompt, // Pass prompt via stdin instead of command-line argument
133136
onStdout: inheritTTY
134137
? undefined
135138
: (chunk) => {
@@ -154,8 +157,21 @@ export async function runClaude(options: RunClaudeOptions): Promise<RunClaudeRes
154157
},
155158
signal: abortSignal,
156159
stdioMode: inheritTTY ? 'inherit' : 'pipe',
157-
timeout,
158-
});
160+
timeout,
161+
});
162+
} catch (error) {
163+
const err = error as unknown as { code?: string; message?: string };
164+
const message = err?.message ?? '';
165+
const notFound = err?.code === 'ENOENT' || /not recognized as an internal or external command/i.test(message) || /command not found/i.test(message);
166+
if (notFound) {
167+
const full = `${command} ${args.join(' ')}`.trim();
168+
const install = metadata.installCommand;
169+
const name = metadata.name;
170+
logger.error(`${name} CLI not found when executing: ${full}`);
171+
throw new Error(`'${command}' is not available on this system. Please install ${name} first:\n ${install}`);
172+
}
173+
throw error;
174+
}
159175

160176
if (result.exitCode !== 0) {
161177
const errorOutput = result.stderr.trim() || result.stdout.trim() || 'no error output';

src/infra/engines/providers/codex/auth.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,34 @@ export async function ensureAuth(): Promise<boolean> {
7474
}
7575

7676
// Run interactive login via Codex CLI with proper env.
77-
await execa('codex', ['login'], {
78-
env: { ...process.env, CODEX_HOME: codexHome },
79-
stdio: 'inherit',
80-
});
77+
try {
78+
await execa('codex', ['login'], {
79+
env: { ...process.env, CODEX_HOME: codexHome },
80+
stdio: 'inherit',
81+
});
82+
} catch (error) {
83+
const err = error as unknown as { code?: string; stderr?: string; message?: string };
84+
const stderr = err?.stderr ?? '';
85+
const message = err?.message ?? '';
86+
const notFound =
87+
err?.code === 'ENOENT' ||
88+
/not recognized as an internal or external command/i.test(stderr || message) ||
89+
/command not found/i.test(stderr || message) ||
90+
/No such file or directory/i.test(stderr || message);
91+
92+
if (notFound) {
93+
console.error(`\n────────────────────────────────────────────────────────────`);
94+
console.error(` ⚠️ ${metadata.name} CLI Not Found`);
95+
console.error(`────────────────────────────────────────────────────────────`);
96+
console.error(`\n'${metadata.cliBinary} login' failed because the CLI is missing.`);
97+
console.error(`Please install ${metadata.name} CLI before trying again:\n`);
98+
console.error(` ${metadata.installCommand}\n`);
99+
console.error(`────────────────────────────────────────────────────────────\n`);
100+
throw new Error(`${metadata.name} CLI is not installed.`);
101+
}
102+
103+
throw error;
104+
}
81105

82106
// Ensure the auth credential path exists; create a placeholder if still absent.
83107
try {

src/infra/engines/providers/codex/execution/runner.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { homedir } from 'node:os';
33

44
import { spawnProcess } from '../../../../process/spawn.js';
55
import { buildCodexExecCommand } from './commands.js';
6+
import { metadata } from '../metadata.js';
67
import { expandHomeDir } from '../../../../../shared/utils/index.js';
78
import { logger } from '../../../../../shared/logging/index.js';
89

@@ -140,12 +141,14 @@ export async function runCodex(options: RunCodexOptions): Promise<RunCodexResult
140141
`Codex runner - CLI: ${command} ${args.map((arg) => (/\s/.test(arg) ? `"${arg}"` : arg)).join(' ')} | stdin preview: ${prompt.slice(0, 120)}`,
141142
);
142143

143-
const result = await spawnProcess({
144-
command,
145-
args,
146-
cwd: workingDir,
147-
env: mergedEnv,
148-
stdinInput: prompt, // Pass prompt via stdin instead of command-line argument
144+
let result;
145+
try {
146+
result = await spawnProcess({
147+
command,
148+
args,
149+
cwd: workingDir,
150+
env: mergedEnv,
151+
stdinInput: prompt, // Pass prompt via stdin instead of command-line argument
149152
onStdout: inheritTTY
150153
? undefined
151154
: (chunk) => {
@@ -170,8 +173,21 @@ export async function runCodex(options: RunCodexOptions): Promise<RunCodexResult
170173
},
171174
signal: abortSignal,
172175
stdioMode: inheritTTY ? 'inherit' : 'pipe',
173-
timeout,
174-
});
176+
timeout,
177+
});
178+
} catch (error) {
179+
const err = error as unknown as { code?: string; message?: string };
180+
const message = err?.message ?? '';
181+
const notFound = err?.code === 'ENOENT' || /not recognized as an internal or external command/i.test(message) || /command not found/i.test(message);
182+
if (notFound) {
183+
const full = `${command} ${args.join(' ')}`.trim();
184+
const install = metadata.installCommand;
185+
const name = metadata.name;
186+
logger.error(`${name} CLI not found when executing: ${full}`);
187+
throw new Error(`'${command}' is not available on this system. Please install ${name} first:\n ${install}`);
188+
}
189+
throw error;
190+
}
175191

176192
if (result.exitCode !== 0) {
177193
const errorOutput = result.stderr.trim() || result.stdout.trim() || 'no error output';

src/infra/engines/providers/cursor/auth.ts

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -120,13 +120,38 @@ export async function ensureAuth(options?: CursorAuthOptions): Promise<boolean>
120120
await mkdir(configDir, { recursive: true });
121121

122122
// Set CURSOR_CONFIG_DIR to control where cursor-agent stores authentication
123-
await execa('cursor-agent', ['login'], {
124-
env: {
125-
...process.env,
126-
CURSOR_CONFIG_DIR: configDir,
127-
},
128-
stdio: 'inherit',
129-
});
123+
try {
124+
await execa('cursor-agent', ['login'], {
125+
env: {
126+
...process.env,
127+
CURSOR_CONFIG_DIR: configDir,
128+
},
129+
stdio: 'inherit',
130+
});
131+
} catch (error) {
132+
const err = error as unknown as { code?: string; stderr?: string; message?: string };
133+
const stderr = err?.stderr ?? '';
134+
const message = err?.message ?? '';
135+
const notFound =
136+
err?.code === 'ENOENT' ||
137+
/not recognized as an internal or external command/i.test(stderr || message) ||
138+
/command not found/i.test(stderr || message) ||
139+
/No such file or directory/i.test(stderr || message);
140+
141+
if (notFound) {
142+
console.error(`\n────────────────────────────────────────────────────────────`);
143+
console.error(` ⚠️ ${metadata.name} CLI Not Found`);
144+
console.error(`────────────────────────────────────────────────────────────`);
145+
console.error(`\n'${metadata.cliBinary} login' failed because the CLI is missing.`);
146+
console.error(`Please install ${metadata.name} CLI before trying again:\n`);
147+
console.error(` ${metadata.installCommand}\n`);
148+
console.error(`────────────────────────────────────────────────────────────\n`);
149+
throw new Error(`${metadata.name} CLI is not installed.`);
150+
}
151+
152+
// Re-throw other errors to preserve original failure context
153+
throw error;
154+
}
130155

131156
// Verify the config file was created
132157
try {

src/infra/engines/providers/cursor/execution/runner.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { homedir } from 'node:os';
33

44
import { spawnProcess } from '../../../../process/spawn.js';
55
import { buildCursorExecCommand } from './commands.js';
6+
import { metadata } from '../metadata.js';
67
import { expandHomeDir } from '../../../../../shared/utils/index.js';
78
import { logger } from '../../../../../shared/logging/index.js';
89

@@ -130,12 +131,14 @@ export async function runCursor(options: RunCursorOptions): Promise<RunCursorRes
130131
logger.debug(`Cursor runner - prompt length: ${prompt.length}, lines: ${prompt.split('\n').length}`);
131132
logger.debug(`Cursor runner - args count: ${args.length}, model: ${model ?? 'auto'}`);
132133

133-
const result = await spawnProcess({
134-
command,
135-
args,
136-
cwd: workingDir,
137-
env: mergedEnv,
138-
stdinInput: prompt, // Pass prompt via stdin instead of command-line argument
134+
let result;
135+
try {
136+
result = await spawnProcess({
137+
command,
138+
args,
139+
cwd: workingDir,
140+
env: mergedEnv,
141+
stdinInput: prompt, // Pass prompt via stdin instead of command-line argument
139142
onStdout: inheritTTY
140143
? undefined
141144
: (chunk) => {
@@ -166,8 +169,21 @@ export async function runCursor(options: RunCursorOptions): Promise<RunCursorRes
166169
},
167170
signal: abortSignal,
168171
stdioMode: inheritTTY ? 'inherit' : 'pipe',
169-
timeout,
170-
});
172+
timeout,
173+
});
174+
} catch (error) {
175+
const err = error as unknown as { code?: string; message?: string };
176+
const message = err?.message ?? '';
177+
const notFound = err?.code === 'ENOENT' || /not recognized as an internal or external command/i.test(message) || /command not found/i.test(message);
178+
if (notFound) {
179+
const full = `${command} ${args.join(' ')}`.trim();
180+
const install = metadata.installCommand;
181+
const name = metadata.name;
182+
logger.error(`${name} CLI not found when executing: ${full}`);
183+
throw new Error(`'${command}' is not available on this system. Please install ${name} first:\n ${install}`);
184+
}
185+
throw error;
186+
}
171187

172188
if (result.exitCode !== 0) {
173189
const errorOutput = result.stderr.trim() || result.stdout.trim() || 'no error output';

templates/workflows/codemachine.workflow.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export default {
22
name: 'CodeMachine Workflow',
33
steps: [
4-
resolveStep('git-commit', { executeOnce: true, engine: 'codex', model: 'gpt-5', modelReasoningEffort: 'low' }), // Commit the initial project specification to git
4+
resolveStep('git-commit', { executeOnce: true }), // Commit the initial project specification to git
55
resolveStep('arch-agent', { executeOnce: true }), // Define system architecture and technical design decisions
66
resolveStep('plan-agent', { executeOnce: true }), // Generate comprehensive iterative development plan with architectural artifacts
77
resolveStep('task-breakdown', { executeOnce: true }), // Extract and structure tasks from project plan into JSON format

0 commit comments

Comments
 (0)