mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 18:18:43 +00:00
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:
@@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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/);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user