mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 10:15:40 +00:00
fix(config): guard Claude extension metadata
This commit is contained in:
1 parent
b82f10e639
commit
56ba37911a
5 files changed
+98
-31
No files matched your search
@@ -7,10 +7,7 @@
|
|||||||
|
|
||||||
import { initUI, header, dim, color, subheader, fail, warn } from '../utils/ui';
|
import { initUI, header, dim, color, subheader, fail, warn } from '../utils/ui';
|
||||||
import { getCcsDir } from '../utils/config-manager';
|
import { getCcsDir } from '../utils/config-manager';
|
||||||
import {
|
import { CLAUDE_EXTENSION_HOSTS, type ClaudeExtensionHost } from '../shared/claude-extension-hosts';
|
||||||
CLAUDE_EXTENSION_HOSTS,
|
|
||||||
type ClaudeExtensionHost,
|
|
||||||
} from '../shared/claude-extension-hosts';
|
|
||||||
import {
|
import {
|
||||||
renderClaudeExtensionSettingsJson,
|
renderClaudeExtensionSettingsJson,
|
||||||
resolveClaudeExtensionSetup,
|
resolveClaudeExtensionSetup,
|
||||||
@@ -157,8 +154,12 @@ function showHelp(): void {
|
|||||||
);
|
);
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(subheader('Notes:'));
|
console.log(subheader('Notes:'));
|
||||||
console.log(` ${dim('- Use ccs persist <profile> for shared ~/.claude/settings.json setup when possible.')}`);
|
console.log(
|
||||||
console.log(` ${dim('- claude-extension output prints JSON only; replace the full environmentVariables setting.')}`);
|
` ${dim('- Use ccs persist <profile> for shared ~/.claude/settings.json setup when possible.')}`
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
` ${dim('- claude-extension output prints JSON only; replace the full environmentVariables setting.')}`
|
||||||
|
);
|
||||||
console.log('');
|
console.log('');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +179,9 @@ export async function handleEnvCommand(args: string[]): Promise<void> {
|
|||||||
const flagsWithValues = ['format', 'shell', 'ide'];
|
const flagsWithValues = ['format', 'shell', 'ide'];
|
||||||
const profile = findProfile(args, flagsWithValues);
|
const profile = findProfile(args, flagsWithValues);
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
console.error(fail('Usage: ccs env <profile> [--format openai|anthropic|raw|claude-extension]'));
|
console.error(
|
||||||
|
fail('Usage: ccs env <profile> [--format openai|anthropic|raw|claude-extension]')
|
||||||
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -720,7 +720,9 @@ async function showHelp(): Promise<void> {
|
|||||||
console.log(subheader('Notes'));
|
console.log(subheader('Notes'));
|
||||||
console.log(' [i] CLIProxy profiles require the proxy to be running.');
|
console.log(' [i] CLIProxy profiles require the proxy to be running.');
|
||||||
console.log(' [i] Copilot profiles require copilot-api daemon.');
|
console.log(' [i] Copilot profiles require copilot-api daemon.');
|
||||||
console.log(' [i] Account/default flows remove stale ANTHROPIC_* overrides before applying new setup.');
|
console.log(
|
||||||
|
' [i] Account/default flows remove stale ANTHROPIC_* overrides before applying new setup.'
|
||||||
|
);
|
||||||
console.log(
|
console.log(
|
||||||
' [i] For IDE-local settings.json snippets, use: ccs env <profile> --format claude-extension'
|
' [i] For IDE-local settings.json snippets, use: ccs env <profile> --format claude-extension'
|
||||||
);
|
);
|
||||||
@@ -832,7 +834,9 @@ export async function handlePersistCommand(args: string[]): Promise<void> {
|
|||||||
// Warning about modification
|
// Warning about modification
|
||||||
console.log(warn(`This will modify ${getClaudeSettingsDisplayPath()}`));
|
console.log(warn(`This will modify ${getClaudeSettingsDisplayPath()}`));
|
||||||
console.log(dim(' Existing hooks and other settings will be preserved.'));
|
console.log(dim(' Existing hooks and other settings will be preserved.'));
|
||||||
console.log(dim(' Existing managed profile env keys will be replaced to avoid stale routing.'));
|
console.log(
|
||||||
|
dim(' Existing managed profile env keys will be replaced to avoid stale routing.')
|
||||||
|
);
|
||||||
console.log('');
|
console.log('');
|
||||||
// Check if settings.json exists for backup
|
// Check if settings.json exists for backup
|
||||||
const settingsPath = getClaudeSettingsPath();
|
const settingsPath = getClaudeSettingsPath();
|
||||||
|
|||||||
@@ -38,5 +38,7 @@ export const CLAUDE_EXTENSION_HOSTS: ClaudeExtensionHostDefinition[] = [
|
|||||||
export function getClaudeExtensionHostDefinition(
|
export function getClaudeExtensionHostDefinition(
|
||||||
host: ClaudeExtensionHost = 'vscode'
|
host: ClaudeExtensionHost = 'vscode'
|
||||||
): ClaudeExtensionHostDefinition {
|
): ClaudeExtensionHostDefinition {
|
||||||
return CLAUDE_EXTENSION_HOSTS.find((candidate) => candidate.id === host) ?? CLAUDE_EXTENSION_HOSTS[0];
|
return (
|
||||||
|
CLAUDE_EXTENSION_HOSTS.find((candidate) => candidate.id === host) ?? CLAUDE_EXTENSION_HOSTS[0]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,11 @@ import ProfileDetector from '../auth/profile-detector';
|
|||||||
import { resolveProfileContinuityInheritance } from '../auth/profile-continuity-inheritance';
|
import { resolveProfileContinuityInheritance } from '../auth/profile-continuity-inheritance';
|
||||||
import { resolveAccountContextPolicy, isAccountContextMetadata } from '../auth/account-context';
|
import { resolveAccountContextPolicy, isAccountContextMetadata } from '../auth/account-context';
|
||||||
import type { ProfileDetectionResult } from '../auth/profile-detector';
|
import type { ProfileDetectionResult } from '../auth/profile-detector';
|
||||||
import { getEffectiveEnvVars, getRemoteEnvVars, getCompositeEnvVars } from '../cliproxy/config/env-builder';
|
import {
|
||||||
|
getEffectiveEnvVars,
|
||||||
|
getRemoteEnvVars,
|
||||||
|
getCompositeEnvVars,
|
||||||
|
} from '../cliproxy/config/env-builder';
|
||||||
import { CLIPROXY_DEFAULT_PORT } from '../cliproxy/config/port-manager';
|
import { CLIPROXY_DEFAULT_PORT } from '../cliproxy/config/port-manager';
|
||||||
import { getProxyTarget } from '../cliproxy/proxy-target-resolver';
|
import { getProxyTarget } from '../cliproxy/proxy-target-resolver';
|
||||||
import { generateCopilotEnv } from '../copilot/copilot-executor';
|
import { generateCopilotEnv } from '../copilot/copilot-executor';
|
||||||
@@ -70,14 +74,19 @@ function describeProfile(profileName: string, result: ProfileDetectionResult): s
|
|||||||
? 'Use Claude Code defaults with no CCS-specific transport override.'
|
? 'Use Claude Code defaults with no CCS-specific transport override.'
|
||||||
: `Use the current default profile resolution for "${result.name}".`;
|
: `Use the current default profile resolution for "${result.name}".`;
|
||||||
}
|
}
|
||||||
if (result.type === 'cliproxy') return 'OAuth or CLIProxy-backed profile for Anthropic-compatible routing.';
|
if (result.type === 'cliproxy')
|
||||||
|
return 'OAuth or CLIProxy-backed profile for Anthropic-compatible routing.';
|
||||||
if (result.type === 'settings') return 'API profile backed by a CCS settings file.';
|
if (result.type === 'settings') return 'API profile backed by a CCS settings file.';
|
||||||
if (result.type === 'account') return 'Claude account instance isolated through CLAUDE_CONFIG_DIR.';
|
if (result.type === 'account')
|
||||||
|
return 'Claude account instance isolated through CLAUDE_CONFIG_DIR.';
|
||||||
if (result.type === 'copilot') return 'GitHub Copilot profile routed through copilot-api.';
|
if (result.type === 'copilot') return 'GitHub Copilot profile routed through copilot-api.';
|
||||||
return 'Native Claude profile resolution.';
|
return 'Native Claude profile resolution.';
|
||||||
}
|
}
|
||||||
|
|
||||||
function createProfileOption(profileName: string, result: ProfileDetectionResult): ClaudeExtensionProfileOption {
|
function createProfileOption(
|
||||||
|
profileName: string,
|
||||||
|
result: ProfileDetectionResult
|
||||||
|
): ClaudeExtensionProfileOption {
|
||||||
return {
|
return {
|
||||||
name: profileName,
|
name: profileName,
|
||||||
profileType: result.type,
|
profileType: result.type,
|
||||||
@@ -89,7 +98,13 @@ function createProfileOption(profileName: string, result: ProfileDetectionResult
|
|||||||
export function listClaudeExtensionProfiles(): ClaudeExtensionProfileOption[] {
|
export function listClaudeExtensionProfiles(): ClaudeExtensionProfileOption[] {
|
||||||
const detector = new ProfileDetector();
|
const detector = new ProfileDetector();
|
||||||
const all = detector.getAllProfiles();
|
const all = detector.getAllProfiles();
|
||||||
const orderedNames = ['default', ...all.accounts, ...all.settings, ...all.cliproxy, ...all.cliproxyVariants];
|
const orderedNames = [
|
||||||
|
'default',
|
||||||
|
...all.accounts,
|
||||||
|
...all.settings,
|
||||||
|
...all.cliproxy,
|
||||||
|
...all.cliproxyVariants,
|
||||||
|
];
|
||||||
const deduped = [...new Set(orderedNames)];
|
const deduped = [...new Set(orderedNames)];
|
||||||
try {
|
try {
|
||||||
detector.detectProfileType('copilot');
|
detector.detectProfileType('copilot');
|
||||||
@@ -106,7 +121,9 @@ export function listClaudeExtensionProfiles(): ClaudeExtensionProfileOption[] {
|
|||||||
async function resolveExtensionEnv(
|
async function resolveExtensionEnv(
|
||||||
requestedProfile: string,
|
requestedProfile: string,
|
||||||
result: ProfileDetectionResult
|
result: ProfileDetectionResult
|
||||||
): Promise<Pick<ClaudeExtensionSetup, 'extensionEnv' | 'warnings' | 'notes' | 'disableLoginPrompt'>> {
|
): Promise<
|
||||||
|
Pick<ClaudeExtensionSetup, 'extensionEnv' | 'warnings' | 'notes' | 'disableLoginPrompt'>
|
||||||
|
> {
|
||||||
const warnings: string[] = [];
|
const warnings: string[] = [];
|
||||||
const notes: string[] = [];
|
const notes: string[] = [];
|
||||||
const requestedIsDefault = requestedProfile === 'default';
|
const requestedIsDefault = requestedProfile === 'default';
|
||||||
@@ -144,7 +161,9 @@ async function resolveExtensionEnv(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
notes.push('Default profile clears CCS-managed transport overrides and uses native Claude defaults.');
|
notes.push(
|
||||||
|
'Default profile clears CCS-managed transport overrides and uses native Claude defaults.'
|
||||||
|
);
|
||||||
return { extensionEnv: {}, warnings, notes, disableLoginPrompt: false };
|
return { extensionEnv: {}, warnings, notes, disableLoginPrompt: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,30 +174,60 @@ async function resolveExtensionEnv(
|
|||||||
});
|
});
|
||||||
const env =
|
const env =
|
||||||
result.type === 'settings'
|
result.type === 'settings'
|
||||||
? result.env ?? (result.settingsPath ? loadSettingsFromFile(expandPath(result.settingsPath)) : {})
|
? (result.env ??
|
||||||
|
(result.settingsPath ? loadSettingsFromFile(expandPath(result.settingsPath)) : {}))
|
||||||
: result.type === 'copilot'
|
: result.type === 'copilot'
|
||||||
? generateCopilotEnv(result.copilotConfig!, continuity.claudeConfigDir)
|
? (() => {
|
||||||
|
if (!result.copilotConfig) {
|
||||||
|
throw new Error(`Profile "${requestedProfile}" is missing copilot configuration.`);
|
||||||
|
}
|
||||||
|
return generateCopilotEnv(result.copilotConfig, continuity.claudeConfigDir);
|
||||||
|
})()
|
||||||
: (() => {
|
: (() => {
|
||||||
|
if (!result.provider) {
|
||||||
|
throw new Error(
|
||||||
|
`Profile "${requestedProfile}" is missing CLIProxy provider metadata.`
|
||||||
|
);
|
||||||
|
}
|
||||||
const proxyTarget = getProxyTarget();
|
const proxyTarget = getProxyTarget();
|
||||||
const port = result.port || CLIPROXY_DEFAULT_PORT;
|
const port = result.port || CLIPROXY_DEFAULT_PORT;
|
||||||
if (proxyTarget.isRemote) {
|
if (proxyTarget.isRemote) {
|
||||||
warnings.push(`CLIProxy is configured for remote routing via ${proxyTarget.protocol}://${proxyTarget.host}:${proxyTarget.port}.`);
|
warnings.push(
|
||||||
|
`CLIProxy is configured for remote routing via ${proxyTarget.protocol}://${proxyTarget.host}:${proxyTarget.port}.`
|
||||||
|
);
|
||||||
return result.isComposite && result.compositeTiers && result.compositeDefaultTier
|
return result.isComposite && result.compositeTiers && result.compositeDefaultTier
|
||||||
? getCompositeEnvVars(result.compositeTiers, result.compositeDefaultTier, port, result.settingsPath, proxyTarget)
|
? getCompositeEnvVars(
|
||||||
: getRemoteEnvVars(result.provider!, proxyTarget, result.settingsPath);
|
result.compositeTiers,
|
||||||
|
result.compositeDefaultTier,
|
||||||
|
port,
|
||||||
|
result.settingsPath,
|
||||||
|
proxyTarget
|
||||||
|
)
|
||||||
|
: getRemoteEnvVars(result.provider, proxyTarget, result.settingsPath);
|
||||||
}
|
}
|
||||||
warnings.push('CLIProxy-backed profiles require the local or remote proxy endpoint to be reachable.');
|
warnings.push(
|
||||||
|
'CLIProxy-backed profiles require the local or remote proxy endpoint to be reachable.'
|
||||||
|
);
|
||||||
return result.isComposite && result.compositeTiers && result.compositeDefaultTier
|
return result.isComposite && result.compositeTiers && result.compositeDefaultTier
|
||||||
? getCompositeEnvVars(result.compositeTiers, result.compositeDefaultTier, port, result.settingsPath)
|
? getCompositeEnvVars(
|
||||||
: getEffectiveEnvVars(result.provider!, port, result.settingsPath);
|
result.compositeTiers,
|
||||||
|
result.compositeDefaultTier,
|
||||||
|
port,
|
||||||
|
result.settingsPath
|
||||||
|
)
|
||||||
|
: getEffectiveEnvVars(result.provider, port, result.settingsPath);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
if (!requestedIsDefault && continuity.claudeConfigDir && !env.CLAUDE_CONFIG_DIR) {
|
if (!requestedIsDefault && continuity.claudeConfigDir && !env.CLAUDE_CONFIG_DIR) {
|
||||||
env.CLAUDE_CONFIG_DIR = continuity.claudeConfigDir;
|
env.CLAUDE_CONFIG_DIR = continuity.claudeConfigDir;
|
||||||
notes.push(`Continuity inheritance adds CLAUDE_CONFIG_DIR from account "${continuity.sourceAccount}".`);
|
notes.push(
|
||||||
|
`Continuity inheritance adds CLAUDE_CONFIG_DIR from account "${continuity.sourceAccount}".`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (result.type === 'copilot') {
|
if (result.type === 'copilot') {
|
||||||
warnings.push('copilot-api must stay reachable for this profile to work inside the IDE extension.');
|
warnings.push(
|
||||||
|
'copilot-api must stay reachable for this profile to work inside the IDE extension.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (Object.keys(env).length === 0) {
|
if (Object.keys(env).length === 0) {
|
||||||
throw new Error(`Profile "${requestedProfile}" has no extension environment to export.`);
|
throw new Error(`Profile "${requestedProfile}" has no extension environment to export.`);
|
||||||
@@ -187,7 +236,9 @@ async function resolveExtensionEnv(
|
|||||||
return { extensionEnv: sortEnvRecord(env), warnings, notes, disableLoginPrompt: true };
|
return { extensionEnv: sortEnvRecord(env), warnings, notes, disableLoginPrompt: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveClaudeExtensionSetup(requestedProfile: string): Promise<ClaudeExtensionSetup> {
|
export async function resolveClaudeExtensionSetup(
|
||||||
|
requestedProfile: string
|
||||||
|
): Promise<ClaudeExtensionSetup> {
|
||||||
const detector = new ProfileDetector();
|
const detector = new ProfileDetector();
|
||||||
const result = detector.detectProfileType(requestedProfile);
|
const result = detector.detectProfileType(requestedProfile);
|
||||||
const resolved = await resolveExtensionEnv(requestedProfile, result);
|
const resolved = await resolveExtensionEnv(requestedProfile, result);
|
||||||
@@ -212,7 +263,10 @@ export function renderClaudeExtensionSettingsJson(
|
|||||||
): string {
|
): string {
|
||||||
const definition = getClaudeExtensionHostDefinition(host);
|
const definition = getClaudeExtensionHostDefinition(host);
|
||||||
const payload: Record<string, unknown> = {
|
const payload: Record<string, unknown> = {
|
||||||
[definition.settingsKey]: Object.entries(setup.extensionEnv).map(([name, value]) => ({ name, value })),
|
[definition.settingsKey]: Object.entries(setup.extensionEnv).map(([name, value]) => ({
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
if (definition.disableLoginPromptKey && setup.disableLoginPrompt) {
|
if (definition.disableLoginPromptKey && setup.disableLoginPrompt) {
|
||||||
payload[definition.disableLoginPromptKey] = true;
|
payload[definition.disableLoginPromptKey] = true;
|
||||||
@@ -224,7 +278,9 @@ export function renderSharedClaudeSettingsJson(setup: ClaudeExtensionSetup): str
|
|||||||
return JSON.stringify({ env: setup.extensionEnv }, null, 2);
|
return JSON.stringify({ env: setup.extensionEnv }, null, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getClaudeExtensionHostMetadata(host: ClaudeExtensionHost): ClaudeExtensionHostDefinition {
|
export function getClaudeExtensionHostMetadata(
|
||||||
|
host: ClaudeExtensionHost
|
||||||
|
): ClaudeExtensionHostDefinition {
|
||||||
return getClaudeExtensionHostDefinition(host);
|
return getClaudeExtensionHostDefinition(host);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ const VALID_HOSTS = new Set(CLAUDE_EXTENSION_HOSTS.map((host) => host.id));
|
|||||||
function getHostFromRequest(req: Request): ClaudeExtensionHost {
|
function getHostFromRequest(req: Request): ClaudeExtensionHost {
|
||||||
const rawHost = String(req.query.host || 'vscode');
|
const rawHost = String(req.query.host || 'vscode');
|
||||||
if (!VALID_HOSTS.has(rawHost as ClaudeExtensionHost)) {
|
if (!VALID_HOSTS.has(rawHost as ClaudeExtensionHost)) {
|
||||||
throw new Error(`Invalid host "${rawHost}". Use: ${CLAUDE_EXTENSION_HOSTS.map((host) => host.id).join(', ')}`);
|
throw new Error(
|
||||||
|
`Invalid host "${rawHost}". Use: ${CLAUDE_EXTENSION_HOSTS.map((host) => host.id).join(', ')}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return rawHost as ClaudeExtensionHost;
|
return rawHost as ClaudeExtensionHost;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user