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 { getWebSearchHookEnv } from '../utils/websearch-manager';
import { wireChildProcessSignals } from '../utils/signal-forwarder';
import { runCleanup } from '../errors';
export class ClaudeAdapter implements TargetAdapter {
readonly type: TargetType = 'claude';
@@ -63,11 +64,19 @@ export class ClaudeAdapter implements TargetAdapter {
env: NodeJS.ProcessEnv,
_options?: { cwd?: string; binaryInfo?: TargetBinaryInfo }
): void {
const exitWithCleanup = (code: number): never => {
try {
runCleanup();
} catch {
// Cleanup should be best-effort on launch errors.
}
process.exit(code);
};
const claudeCli = detectClaudeCli();
if (!claudeCli) {
void ErrorManager.showClaudeNotFound();
process.exit(1);
return;
return exitWithCleanup(1);
}
const isWindows = process.platform === 'win32';
@@ -118,7 +127,7 @@ export class ClaudeAdapter implements TargetAdapter {
} else {
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 { escapeShellArg } from '../utils/shell-executor';
import { wireChildProcessSignals } from '../utils/signal-forwarder';
import { runCleanup } from '../errors';
export class DroidAdapter implements TargetAdapter {
readonly type: TargetType = 'droid';
@@ -52,6 +53,11 @@ export class DroidAdapter implements TargetAdapter {
}
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];
}
@@ -67,26 +73,32 @@ export class DroidAdapter implements TargetAdapter {
env: NodeJS.ProcessEnv,
options?: { cwd?: string; binaryInfo?: TargetBinaryInfo }
): 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();
if (!droidPath) {
console.error('[X] Droid CLI not found. Install: npm i -g @factory/cli');
process.exit(1);
return;
return exitWithCleanup(1);
}
try {
const stat = fs.statSync(droidPath);
if (!stat.isFile()) {
console.error(`[X] Droid CLI path is not a file: ${droidPath}`);
process.exit(1);
return;
return exitWithCleanup(1);
}
} catch (err) {
const error = err as NodeJS.ErrnoException;
console.error(
`[X] Droid CLI path is not accessible (${error.code || 'unknown'}): ${droidPath}`
);
process.exit(1);
return;
return exitWithCleanup(1);
}
const isWindows = process.platform === 'win32';
@@ -138,7 +150,7 @@ export class DroidAdapter implements TargetAdapter {
} else {
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/);
});
});