mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-03 22:19:42 +02:00
feat(custom-model): skip Claude Code's first-run wizard on custom-model launches
A fresh, isolated CLAUDE_CONFIG_DIR (used to keep an injected API key away from a stored claude.ai OAuth login) looks like a brand-new Claude Code profile to the CLI, so it replays its ENTIRE first-run sequence on every single launch: the theme picker, the security-notes screen, the per-project "trust this folder?" dialog, and (running with --dangerously-skip-permissions) a one-time bypass-permissions warning — confirmed live, none of which a real, already-onboarded profile shows again. - New registry-declared env-kind field `skipFirstRunPrompts` (alongside apiKeyTrustFile, which it reuses) — claude's entry only, carried through buildCustomModelInjection (pure) into applyCustomModelInjection (IO). - seedFirstRunOnboardingState(): merges hasCompletedOnboarding: true and this session's own projects[workingDir].hasTrustDialogAccepted: true into the same <configDir>/.claude.json the API-key trust file already writes to — other projects and other fields on this session's own entry are left untouched. - seedSkipBypassPermissionsPrompt(): merges skipDangerousModePermissionPrompt: true into <configDir>/settings.json, a separate file, same corrupt-tolerant merge behavior. - applyCustomModelInjection() gains an optional workingDir parameter, threaded from session.workingDir (dedicated apply route) / resolvedCasePath (quick-start route) — boot recovery omits it (a dialog already answered once needs no re-seed on the same, persisted isolated directory). Tests added at the pure-builder, IO-wrapper (including merge-preserves- other-fields and corrupt-file-tolerance cases), and existing directory- listing assertions updated for the new settings.json file. Typecheck/ lint/format clean; full suite shows no new regressions (baseline pre-existing Windows-environment failures unchanged, 8 more passing tests than before — the ones added here). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RqZeHrRS6DYcGcGX2p9EwG
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
2c89359d42
commit
211b872335
@@ -68,9 +68,10 @@ describe('applyCustomModelInjection: CLAUDE_CONFIG_DIR isolation', () => {
|
||||
expect(applied?.envOverrides.CLAUDE_CONFIG_DIR).toBe(expectedDir);
|
||||
expect(applied?.configDir).toBe(expectedDir);
|
||||
expect(existsSync(expectedDir)).toBe(true);
|
||||
// Only the trust-seed file and the projects link — no real OAuth credential/config.
|
||||
// The trust-seed file, the skipFirstRunPrompts settings.json, and the projects link —
|
||||
// no real OAuth credential/config.
|
||||
const entries = readdirSync(expectedDir).filter((name) => name !== 'projects');
|
||||
expect(entries).toEqual(['.claude.json']);
|
||||
expect(entries.sort()).toEqual(['.claude.json', 'settings.json']);
|
||||
});
|
||||
|
||||
it('claude: symlinks (or junctions) projects back to the real config dir so the response viewer keeps working', () => {
|
||||
@@ -189,6 +190,104 @@ describe('applyCustomModelInjection: apiKeyTrustFile (pre-approves the injected
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyCustomModelInjection: skipFirstRunPrompts (an isolated dir replays claude's whole first-run sequence)", () => {
|
||||
it("claude: seeds hasCompletedOnboarding and this session's own project trust into .claude.json", () => {
|
||||
const sessionId = 'sess-firstrun-1';
|
||||
sessionsToClean.push(sessionId);
|
||||
const applied = applyCustomModelInjection(
|
||||
entryOrThrow('claude'),
|
||||
endpoint,
|
||||
'qwen3',
|
||||
sessionId,
|
||||
undefined,
|
||||
'/home/user/myproject'
|
||||
);
|
||||
const written = JSON.parse(readFileSync(join(applied!.configDir!, '.claude.json'), 'utf8')) as {
|
||||
hasCompletedOnboarding: boolean;
|
||||
projects: Record<string, { hasTrustDialogAccepted: boolean }>;
|
||||
};
|
||||
expect(written.hasCompletedOnboarding).toBe(true);
|
||||
expect(written.projects['/home/user/myproject'].hasTrustDialogAccepted).toBe(true);
|
||||
});
|
||||
|
||||
it('claude: seeds skipDangerousModePermissionPrompt into settings.json', () => {
|
||||
const sessionId = 'sess-firstrun-2';
|
||||
sessionsToClean.push(sessionId);
|
||||
const applied = applyCustomModelInjection(entryOrThrow('claude'), endpoint, 'qwen3', sessionId);
|
||||
const written = JSON.parse(readFileSync(join(applied!.configDir!, 'settings.json'), 'utf8')) as {
|
||||
skipDangerousModePermissionPrompt: boolean;
|
||||
};
|
||||
expect(written.skipDangerousModePermissionPrompt).toBe(true);
|
||||
});
|
||||
|
||||
it('claude: with no workingDir given (boot recovery), hasCompletedOnboarding/settings still seed, but no project entry is added', () => {
|
||||
const sessionId = 'sess-firstrun-3';
|
||||
sessionsToClean.push(sessionId);
|
||||
const applied = applyCustomModelInjection(entryOrThrow('claude'), endpoint, 'qwen3', sessionId);
|
||||
const written = JSON.parse(readFileSync(join(applied!.configDir!, '.claude.json'), 'utf8')) as {
|
||||
hasCompletedOnboarding: boolean;
|
||||
projects?: Record<string, unknown>;
|
||||
};
|
||||
expect(written.hasCompletedOnboarding).toBeUndefined();
|
||||
expect(written.projects).toBeUndefined();
|
||||
});
|
||||
|
||||
it("claude: merges onto an existing project entry's other fields rather than overwriting them", () => {
|
||||
const sessionId = 'sess-firstrun-4';
|
||||
sessionsToClean.push(sessionId);
|
||||
const configDir = customModelConfigDir(sessionId);
|
||||
mkdirSync(configDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(configDir, '.claude.json'),
|
||||
JSON.stringify({ projects: { '/home/user/myproject': { allowedTools: ['Bash'] } } })
|
||||
);
|
||||
|
||||
const applied = applyCustomModelInjection(
|
||||
entryOrThrow('claude'),
|
||||
endpoint,
|
||||
'qwen3',
|
||||
sessionId,
|
||||
undefined,
|
||||
'/home/user/myproject'
|
||||
);
|
||||
|
||||
const written = JSON.parse(readFileSync(join(applied!.configDir!, '.claude.json'), 'utf8')) as {
|
||||
projects: Record<string, { allowedTools: string[]; hasTrustDialogAccepted: boolean }>;
|
||||
};
|
||||
expect(written.projects['/home/user/myproject'].allowedTools).toEqual(['Bash']);
|
||||
expect(written.projects['/home/user/myproject'].hasTrustDialogAccepted).toBe(true);
|
||||
});
|
||||
|
||||
it('claude: a corrupt existing settings.json is treated as absent rather than failing the apply', () => {
|
||||
const sessionId = 'sess-firstrun-5';
|
||||
sessionsToClean.push(sessionId);
|
||||
const configDir = customModelConfigDir(sessionId);
|
||||
mkdirSync(configDir, { recursive: true });
|
||||
writeFileSync(join(configDir, 'settings.json'), '{ not valid json');
|
||||
|
||||
expect(() => applyCustomModelInjection(entryOrThrow('claude'), endpoint, 'qwen3', sessionId)).not.toThrow();
|
||||
const written = JSON.parse(readFileSync(join(configDir, 'settings.json'), 'utf8')) as {
|
||||
skipDangerousModePermissionPrompt: boolean;
|
||||
};
|
||||
expect(written.skipDangerousModePermissionPrompt).toBe(true);
|
||||
});
|
||||
|
||||
it('pi: has no skipFirstRunPrompts concept (no apiKeyTrustFile either) — nothing beyond its own config file', () => {
|
||||
const sessionId = 'sess-firstrun-pi';
|
||||
sessionsToClean.push(sessionId);
|
||||
const applied = applyCustomModelInjection(
|
||||
entryOrThrow('pi'),
|
||||
endpoint,
|
||||
'qwen3',
|
||||
sessionId,
|
||||
undefined,
|
||||
'/home/user/myproject'
|
||||
);
|
||||
const entries = readdirSync(applied!.configDir!);
|
||||
expect(entries).not.toContain('settings.json');
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyCustomModelInjection: pre-existing behavior unaffected', () => {
|
||||
it('opencode: still returns a plain env-kind result with no configDir', () => {
|
||||
const sessionId = 'sess-opencode-1';
|
||||
|
||||
@@ -83,6 +83,18 @@ describe('buildCustomModelInjection', () => {
|
||||
expect(result.apiKey).toBe('my-key');
|
||||
});
|
||||
|
||||
it('claude: also declares skipFirstRunPrompts on the env-kind result', () => {
|
||||
const result = buildCustomModelInjection(entryOrThrow('claude'), endpoint, 'qwen3');
|
||||
if (result.kind !== 'env') throw new Error('unreachable');
|
||||
expect(result.skipFirstRunPrompts).toBe(true);
|
||||
});
|
||||
|
||||
it('opencode: has no skipFirstRunPrompts (no apiKeyTrustFile/configDirVar concept for it either)', () => {
|
||||
const result = buildCustomModelInjection(entryOrThrow('opencode'), endpoint, 'qwen3');
|
||||
if (result.kind !== 'env') throw new Error('unreachable');
|
||||
expect(result.skipFirstRunPrompts).toBeUndefined();
|
||||
});
|
||||
|
||||
it('claude: falls back to a dummy key when the endpoint has none', () => {
|
||||
const result = buildCustomModelInjection(entryOrThrow('claude'), { ...endpoint, apiKey: undefined }, 'qwen3');
|
||||
if (result.kind !== 'env') throw new Error('unreachable');
|
||||
|
||||
Reference in New Issue
Block a user