mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-14 06:17:27 +00:00
feat: clarify account history sync route (#1187)
* feat: clarify account history sync route * fix: clarify shared context command examples * fix: report missing bare profile settings
This commit is contained in:
1 parent
8df43585f4
commit
be9effcce3
13 files changed
+767
-17
No files matched your search
@@ -0,0 +1,140 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { DEFAULT_ACCOUNT_CONTEXT_GROUP, type AccountContextPolicy } from './account-context';
|
||||
import { getCcsDir } from '../config/config-loader-facade';
|
||||
import { getDefaultClaudeConfigDir } from '../utils/claude-config-path';
|
||||
|
||||
export type SettingsSyncState = 'shared' | 'profile-local' | 'missing' | 'unknown';
|
||||
|
||||
export interface AccountSettingsSyncSummary {
|
||||
state: SettingsSyncState;
|
||||
profile_settings_path: string;
|
||||
shared_settings_path: string;
|
||||
root_settings_path: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface AccountHistorySummary {
|
||||
project_count: number;
|
||||
session_count: number;
|
||||
projects_path: string;
|
||||
projects_shared: boolean;
|
||||
deeper_artifacts_shared: boolean;
|
||||
}
|
||||
|
||||
function safeRealpath(targetPath: string): string | null {
|
||||
try {
|
||||
return fs.realpathSync(targetPath);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function countTopLevelDirectories(targetPath: string): number {
|
||||
try {
|
||||
return fs
|
||||
.readdirSync(targetPath, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory()).length;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function countJsonFiles(targetPath: string): number {
|
||||
try {
|
||||
return fs.readdirSync(targetPath).filter((entry) => entry.endsWith('.json')).length;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function resolvesTo(targetPath: string, expectedPath: string): boolean {
|
||||
const realTarget = safeRealpath(targetPath);
|
||||
const realExpected = safeRealpath(expectedPath);
|
||||
return !!realTarget && !!realExpected && realTarget === realExpected;
|
||||
}
|
||||
|
||||
export function describeSettingsSync(
|
||||
instancePath: string,
|
||||
options: { bare?: boolean } = {}
|
||||
): AccountSettingsSyncSummary {
|
||||
const rootSettingsPath = path.join(getDefaultClaudeConfigDir(), 'settings.json');
|
||||
const sharedSettingsPath = path.join(getCcsDir(), 'shared', 'settings.json');
|
||||
const profileSettingsPath = path.join(instancePath, 'settings.json');
|
||||
|
||||
if (options.bare) {
|
||||
if (!fs.existsSync(profileSettingsPath)) {
|
||||
return {
|
||||
state: 'missing',
|
||||
profile_settings_path: profileSettingsPath,
|
||||
shared_settings_path: sharedSettingsPath,
|
||||
root_settings_path: rootSettingsPath,
|
||||
description: 'missing (bare profile; no local settings.json yet)',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
state: 'profile-local',
|
||||
profile_settings_path: profileSettingsPath,
|
||||
shared_settings_path: sharedSettingsPath,
|
||||
root_settings_path: rootSettingsPath,
|
||||
description: 'profile-local (bare profile; not linked to ~/.claude/settings.json)',
|
||||
};
|
||||
}
|
||||
|
||||
if (!fs.existsSync(profileSettingsPath)) {
|
||||
return {
|
||||
state: 'missing',
|
||||
profile_settings_path: profileSettingsPath,
|
||||
shared_settings_path: sharedSettingsPath,
|
||||
root_settings_path: rootSettingsPath,
|
||||
description: 'missing (run or repair this profile to recreate settings link)',
|
||||
};
|
||||
}
|
||||
|
||||
if (resolvesTo(profileSettingsPath, rootSettingsPath)) {
|
||||
return {
|
||||
state: 'shared',
|
||||
profile_settings_path: profileSettingsPath,
|
||||
shared_settings_path: sharedSettingsPath,
|
||||
root_settings_path: rootSettingsPath,
|
||||
description: 'shared with ~/.claude/settings.json',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
state: 'unknown',
|
||||
profile_settings_path: profileSettingsPath,
|
||||
shared_settings_path: sharedSettingsPath,
|
||||
root_settings_path: rootSettingsPath,
|
||||
description: 'not linked to ~/.claude/settings.json',
|
||||
};
|
||||
}
|
||||
|
||||
export function summarizeAccountHistory(
|
||||
instancePath: string,
|
||||
policy: AccountContextPolicy
|
||||
): AccountHistorySummary {
|
||||
const projectsPath = path.join(instancePath, 'projects');
|
||||
const sessionEnvPath = path.join(instancePath, 'session-env');
|
||||
const group = policy.group || DEFAULT_ACCOUNT_CONTEXT_GROUP;
|
||||
const sharedGroupRoot = path.join(getCcsDir(), 'shared', 'context-groups', group);
|
||||
const sharedProjectsPath = path.join(sharedGroupRoot, 'projects');
|
||||
const deeperArtifacts = ['session-env', 'file-history', 'shell-snapshots', 'todos'];
|
||||
|
||||
return {
|
||||
project_count: countTopLevelDirectories(projectsPath),
|
||||
session_count: countJsonFiles(sessionEnvPath),
|
||||
projects_path: projectsPath,
|
||||
projects_shared: policy.mode === 'shared' && resolvesTo(projectsPath, sharedProjectsPath),
|
||||
deeper_artifacts_shared:
|
||||
policy.mode === 'shared' &&
|
||||
policy.continuityMode === 'deeper' &&
|
||||
deeperArtifacts.every((artifact) =>
|
||||
resolvesTo(
|
||||
path.join(instancePath, artifact),
|
||||
path.join(sharedGroupRoot, 'continuity', artifact)
|
||||
)
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -81,18 +81,25 @@ class AuthCommands {
|
||||
);
|
||||
console.log('');
|
||||
console.log(subheader('Examples'));
|
||||
console.log(` ${dim('# Create & login to work profile')}`);
|
||||
console.log(` ${dim('# Create two isolated accounts and choose one explicitly at runtime')}`);
|
||||
console.log(` ${color('ccs auth create work', 'command')}`);
|
||||
console.log(` ${color('ccs auth create personal', 'command')}`);
|
||||
console.log(` ${color('ccs work "review code"', 'command')}`);
|
||||
console.log(` ${color('ccs personal "write tests"', 'command')}`);
|
||||
console.log('');
|
||||
console.log(` ${dim('# Create account with shared project context (default group)')}`);
|
||||
console.log(
|
||||
` ${dim('# Optional: share local project history while credentials stay isolated')}`
|
||||
);
|
||||
console.log(` ${color('ccs auth create work2 --share-context', 'command')}`);
|
||||
console.log('');
|
||||
console.log(` ${dim('# Share context only within a specific group')}`);
|
||||
console.log(` ${color('ccs auth create backup --context-group sprint-a', 'command')}`);
|
||||
console.log(
|
||||
` ${color('ccs auth create backup --share-context --context-group sprint-a', 'command')}`
|
||||
);
|
||||
console.log('');
|
||||
console.log(` ${dim('# Advanced: deeper shared continuity for session history artifacts')}`);
|
||||
console.log(
|
||||
` ${color('ccs auth create backup --context-group sprint-a --deeper-continuity', 'command')}`
|
||||
` ${color('ccs auth create backup --share-context --context-group sprint-a --deeper-continuity', 'command')}`
|
||||
);
|
||||
console.log('');
|
||||
console.log(` ${dim('# Create clean profile without shared commands/skills/agents')}`);
|
||||
@@ -113,9 +120,6 @@ class AuthCommands {
|
||||
console.log(` ${dim('# List all profiles')}`);
|
||||
console.log(` ${color('ccs auth list', 'command')}`);
|
||||
console.log('');
|
||||
console.log(` ${dim('# Use work profile')}`);
|
||||
console.log(` ${color('ccs work "review code"', 'command')}`);
|
||||
console.log('');
|
||||
console.log(subheader('Options'));
|
||||
console.log(
|
||||
` ${color('--force', 'command')} Allow overwriting existing profile (create)`
|
||||
@@ -147,14 +151,24 @@ class AuthCommands {
|
||||
` By default, ${color('ccs', 'command')} uses Claude CLI defaults from ~/.claude/`
|
||||
);
|
||||
console.log(
|
||||
` Use ${color('ccs auth default <profile>', 'command')} to change the default profile.`
|
||||
` Recommended two-account route: create ${color('work', 'command')} and ${color('personal', 'command')}, then run the profile you want.`
|
||||
);
|
||||
console.log(
|
||||
` Account profiles stay isolated unless you opt in with ${color('--share-context', 'command')}.`
|
||||
` Use ${color('ccs auth default <profile>', 'command')} to change the default profile.`
|
||||
);
|
||||
console.log(` Account logins, tokens, and .anthropic stay isolated for every profile.`);
|
||||
console.log(
|
||||
` Non-bare account profiles share basic ${color('settings.json', 'path')} with ${color('~/.claude/settings.json', 'path')}; ${color('ccs auth show <profile>', 'command')} shows the link state.`
|
||||
);
|
||||
console.log(
|
||||
` History sync is opt-in: both accounts need shared mode and the same ${color('context_group', 'path')}.`
|
||||
);
|
||||
console.log(
|
||||
` ${color('--deeper-continuity', 'command')} requires shared mode and syncs session-env/file-history/todos/shell-snapshots.`
|
||||
);
|
||||
console.log(
|
||||
` To make future plain ${color('ccs', 'command')} resume with an account, set ${color('ccs auth default <profile>', 'command')}; back up the current native lane first with ${color('ccs auth backup default', 'command')}.`
|
||||
);
|
||||
console.log(
|
||||
` Existing profiles: open ${color('ccs config', 'command')} -> Accounts -> Edit Context.`
|
||||
);
|
||||
|
||||
@@ -280,7 +280,9 @@ export async function handleCreate(ctx: CommandContext, args: string[]): Promise
|
||||
`Profile: ${profileName}\n` +
|
||||
`Instance: ${instancePath}\n` +
|
||||
`Type: account\n` +
|
||||
`Context: ${formatAccountContextPolicy(contextPolicy)}` +
|
||||
`Context: ${formatAccountContextPolicy(contextPolicy)}\n` +
|
||||
`Tokens: isolated per account\n` +
|
||||
`Settings: ${effectiveBare ? 'profile-local (bare)' : 'shared with ~/.claude/settings.json'}` +
|
||||
(effectiveBare ? '\nMode: bare (no shared symlinks)' : ''),
|
||||
'Profile Created'
|
||||
)
|
||||
@@ -289,6 +291,14 @@ export async function handleCreate(ctx: CommandContext, args: string[]): Promise
|
||||
console.log(header('Usage'));
|
||||
console.log(` ${color(`ccs ${profileName} "your prompt here"`, 'command')}`);
|
||||
console.log('');
|
||||
console.log(
|
||||
'To keep two accounts separate, create another account and run either profile by name:'
|
||||
);
|
||||
console.log(` ${color('ccs auth create personal', 'command')}`);
|
||||
console.log(
|
||||
` ${color(`ccs ${profileName}`, 'command')} / ${color('ccs personal', 'command')}`
|
||||
);
|
||||
console.log('');
|
||||
console.log(
|
||||
warnBox(
|
||||
`Running the command below will SWITCH your default\n` +
|
||||
|
||||
@@ -8,10 +8,18 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { initUI, header, color, fail, table } from '../../utils/ui';
|
||||
import { resolveAccountContextPolicy, formatAccountContextPolicy } from '../account-context';
|
||||
import { describeSettingsSync, summarizeAccountHistory } from '../account-profile-diagnostics';
|
||||
import { resolveConfiguredPlainCcsResumeLane } from '../resume-lane-diagnostics';
|
||||
import { exitWithError } from '../../errors';
|
||||
import { ExitCode } from '../../errors/exit-codes';
|
||||
import { CommandContext, ProfileOutput, parseArgs } from './types';
|
||||
|
||||
function formatHistorySummary(history: ReturnType<typeof summarizeAccountHistory>): string {
|
||||
const scope = history.projects_shared ? 'shared projects' : 'profile-local projects';
|
||||
const deeper = history.deeper_artifacts_shared ? ', deeper artifacts shared' : '';
|
||||
return `${scope}: ${history.project_count} project(s), ${history.session_count} session env file(s)${deeper}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the show command
|
||||
*/
|
||||
@@ -37,6 +45,11 @@ export async function handleShow(ctx: CommandContext, args: string[]): Promise<v
|
||||
const isDefault = profileName === defaultProfile;
|
||||
const instancePath = ctx.instanceMgr.getInstancePath(profileName);
|
||||
const contextPolicy = resolveAccountContextPolicy(profile);
|
||||
const settingsSync = describeSettingsSync(instancePath, { bare: profile.bare === true });
|
||||
const historySummary = summarizeAccountHistory(instancePath, contextPolicy);
|
||||
const plainCcsLane = await resolveConfiguredPlainCcsResumeLane().catch(() => null);
|
||||
const plainCcsUsesThisAccount =
|
||||
!!plainCcsLane && path.resolve(plainCcsLane.configDir) === path.resolve(instancePath);
|
||||
|
||||
// Count sessions
|
||||
let sessionCount = 0;
|
||||
@@ -63,6 +76,24 @@ export async function handleShow(ctx: CommandContext, args: string[]): Promise<v
|
||||
continuity_mode: contextPolicy.mode === 'shared' ? contextPolicy.continuityMode : null,
|
||||
instance_path: instancePath,
|
||||
session_count: sessionCount,
|
||||
settings_sync: {
|
||||
state: settingsSync.state,
|
||||
profile_settings_path: settingsSync.profile_settings_path,
|
||||
shared_settings_path: settingsSync.shared_settings_path,
|
||||
root_settings_path: settingsSync.root_settings_path,
|
||||
},
|
||||
history: historySummary,
|
||||
...(plainCcsLane
|
||||
? {
|
||||
plain_ccs_lane: {
|
||||
kind: plainCcsLane.kind,
|
||||
label: plainCcsLane.label,
|
||||
config_dir: plainCcsLane.configDir,
|
||||
project_count: plainCcsLane.projectCount,
|
||||
uses_this_account: plainCcsUsesThisAccount,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(profile.bare ? { bare: true } : {}),
|
||||
};
|
||||
console.log(JSON.stringify(output, null, 2));
|
||||
@@ -81,6 +112,19 @@ export async function handleShow(ctx: CommandContext, args: string[]): Promise<v
|
||||
['Created', new Date(profile.created).toLocaleString()],
|
||||
['Last Used', profile.last_used ? new Date(profile.last_used).toLocaleString() : 'Never'],
|
||||
['Context', formatAccountContextPolicy(contextPolicy)],
|
||||
['Credentials', 'isolated per account'],
|
||||
['Settings', settingsSync.description],
|
||||
['History', formatHistorySummary(historySummary)],
|
||||
...(plainCcsLane
|
||||
? [
|
||||
[
|
||||
'Plain ccs',
|
||||
plainCcsUsesThisAccount
|
||||
? `uses this account lane (${plainCcsLane.projectCount} project(s))`
|
||||
: `${plainCcsLane.label} (${plainCcsLane.projectCount} project(s))`,
|
||||
],
|
||||
]
|
||||
: []),
|
||||
...(profile.bare ? [['Bare', 'yes (no shared symlinks)']] : []),
|
||||
['Sessions', `${sessionCount}`],
|
||||
];
|
||||
|
||||
@@ -40,6 +40,26 @@ export interface ProfileOutput {
|
||||
continuity_mode?: 'standard' | 'deeper' | null;
|
||||
instance_path?: string;
|
||||
session_count?: number;
|
||||
settings_sync?: {
|
||||
state: 'shared' | 'profile-local' | 'missing' | 'unknown';
|
||||
profile_settings_path: string;
|
||||
shared_settings_path: string;
|
||||
root_settings_path: string;
|
||||
};
|
||||
history?: {
|
||||
project_count: number;
|
||||
session_count: number;
|
||||
projects_path: string;
|
||||
projects_shared: boolean;
|
||||
deeper_artifacts_shared: boolean;
|
||||
};
|
||||
plain_ccs_lane?: {
|
||||
kind: string;
|
||||
label: string;
|
||||
config_dir: string;
|
||||
project_count: number;
|
||||
uses_this_account: boolean;
|
||||
};
|
||||
bare?: boolean;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user