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:
Kai (Tam Nhu) Tran authored and GitHub committed 2026-05-05 13:33:04 -04:00
1 parent 8df43585f4
commit be9effcce3
13 files changed
+767 -17

No files matched your search

+140
View File
@@ -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)
)
),
};
}
+23 -9
View File
@@ -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.`
);
+11 -1
View File
@@ -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` +
+44
View File
@@ -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}`],
];
+20
View File
@@ -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;
}