fix(targets): run cleanup before adapter launch exits

- wrap adapter early-exit paths with a cleanup-aware exit helper

- invoke cleanup before exiting on child spawn failures

- validate Droid profile names in buildArgs and add unit coverage
This commit is contained in:
Tam Nhu Tran
2026-02-17 20:53:03 +07:00
parent ef578e988b
commit 812bb5c0a5
3 changed files with 48 additions and 10 deletions
+12 -3
View File
@@ -13,6 +13,7 @@ import { escapeShellArg, stripAnthropicEnv } from '../utils/shell-executor';
import { ErrorManager } from '../utils/error-manager'; import { ErrorManager } from '../utils/error-manager';
import { getWebSearchHookEnv } from '../utils/websearch-manager'; import { getWebSearchHookEnv } from '../utils/websearch-manager';
import { wireChildProcessSignals } from '../utils/signal-forwarder'; import { wireChildProcessSignals } from '../utils/signal-forwarder';
import { runCleanup } from '../errors';
export class ClaudeAdapter implements TargetAdapter { export class ClaudeAdapter implements TargetAdapter {
readonly type: TargetType = 'claude'; readonly type: TargetType = 'claude';
@@ -63,11 +64,19 @@ export class ClaudeAdapter implements TargetAdapter {
env: NodeJS.ProcessEnv, env: NodeJS.ProcessEnv,
_options?: { cwd?: string; binaryInfo?: TargetBinaryInfo } _options?: { cwd?: string; binaryInfo?: TargetBinaryInfo }
): void { ): void {
const exitWithCleanup = (code: number): never => {
try {
runCleanup();
} catch {
// Cleanup should be best-effort on launch errors.
}
process.exit(code);
};
const claudeCli = detectClaudeCli(); const claudeCli = detectClaudeCli();
if (!claudeCli) { if (!claudeCli) {
void ErrorManager.showClaudeNotFound(); void ErrorManager.showClaudeNotFound();
process.exit(1); return exitWithCleanup(1);
return;
} }
const isWindows = process.platform === 'win32'; const isWindows = process.platform === 'win32';
@@ -118,7 +127,7 @@ export class ClaudeAdapter implements TargetAdapter {
} else { } else {
console.error(`[X] Failed to start Claude CLI (${claudeCli}): ${err.message}`); console.error(`[X] Failed to start Claude CLI (${claudeCli}): ${err.message}`);
} }
process.exit(1); return exitWithCleanup(1);
}); });
} }
+19 -7
View File
@@ -13,6 +13,7 @@ import type { ProfileType } from '../types/profile';
import { upsertCcsModel } from './droid-config-manager'; import { upsertCcsModel } from './droid-config-manager';
import { escapeShellArg } from '../utils/shell-executor'; import { escapeShellArg } from '../utils/shell-executor';
import { wireChildProcessSignals } from '../utils/signal-forwarder'; import { wireChildProcessSignals } from '../utils/signal-forwarder';
import { runCleanup } from '../errors';
export class DroidAdapter implements TargetAdapter { export class DroidAdapter implements TargetAdapter {
readonly type: TargetType = 'droid'; readonly type: TargetType = 'droid';
@@ -52,6 +53,11 @@ export class DroidAdapter implements TargetAdapter {
} }
buildArgs(profile: string, userArgs: string[]): string[] { buildArgs(profile: string, userArgs: string[]): string[] {
if (!/^[a-zA-Z0-9_-]+$/.test(profile)) {
throw new Error(
`Invalid profile name "${profile}" for Droid target: only alphanumeric, underscore, hyphen allowed`
);
}
return ['-m', `custom:ccs-${profile}`, ...userArgs]; return ['-m', `custom:ccs-${profile}`, ...userArgs];
} }
@@ -67,26 +73,32 @@ export class DroidAdapter implements TargetAdapter {
env: NodeJS.ProcessEnv, env: NodeJS.ProcessEnv,
options?: { cwd?: string; binaryInfo?: TargetBinaryInfo } options?: { cwd?: string; binaryInfo?: TargetBinaryInfo }
): void { ): void {
const exitWithCleanup = (code: number): never => {
try {
runCleanup();
} catch {
// Cleanup should be best-effort on launch errors.
}
process.exit(code);
};
const droidPath = options?.binaryInfo?.path || detectDroidCli(); const droidPath = options?.binaryInfo?.path || detectDroidCli();
if (!droidPath) { if (!droidPath) {
console.error('[X] Droid CLI not found. Install: npm i -g @factory/cli'); console.error('[X] Droid CLI not found. Install: npm i -g @factory/cli');
process.exit(1); return exitWithCleanup(1);
return;
} }
try { try {
const stat = fs.statSync(droidPath); const stat = fs.statSync(droidPath);
if (!stat.isFile()) { if (!stat.isFile()) {
console.error(`[X] Droid CLI path is not a file: ${droidPath}`); console.error(`[X] Droid CLI path is not a file: ${droidPath}`);
process.exit(1); return exitWithCleanup(1);
return;
} }
} catch (err) { } catch (err) {
const error = err as NodeJS.ErrnoException; const error = err as NodeJS.ErrnoException;
console.error( console.error(
`[X] Droid CLI path is not accessible (${error.code || 'unknown'}): ${droidPath}` `[X] Droid CLI path is not accessible (${error.code || 'unknown'}): ${droidPath}`
); );
process.exit(1); return exitWithCleanup(1);
return;
} }
const isWindows = process.platform === 'win32'; const isWindows = process.platform === 'win32';
@@ -138,7 +150,7 @@ export class DroidAdapter implements TargetAdapter {
} else { } else {
console.error(`[X] Failed to start Droid CLI (${droidPath}):`, err.message); console.error(`[X] Failed to start Droid CLI (${droidPath}):`, err.message);
} }
process.exit(1); return exitWithCleanup(1);
}); });
} }
+17
View File
@@ -0,0 +1,17 @@
/**
* Unit tests for Droid adapter argument building.
*/
import { describe, it, expect } from 'bun:test';
import { DroidAdapter } from '../../../src/targets/droid-adapter';
describe('DroidAdapter.buildArgs', () => {
it('builds droid model args for valid profile names', () => {
const adapter = new DroidAdapter();
expect(adapter.buildArgs('gemini_01', ['--help'])).toEqual(['-m', 'custom:ccs-gemini_01', '--help']);
});
it('rejects unsafe profile names', () => {
const adapter = new DroidAdapter();
expect(() => adapter.buildArgs('bad profile', [])).toThrow(/Invalid profile name/);
});
});