cchost 0.4.4 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/cli.js CHANGED
@@ -13,19 +13,24 @@ const { renderTable } = require('./format');
13
13
  const color = require('./color');
14
14
  const { renderBanner } = require('./banner');
15
15
 
16
- const HELP = `cc - Multi-profile launcher for Claude Code
16
+ const HELP = `cchost - Multi-profile launcher for Claude Code
17
+
18
+ Installed as both 'cchost' (preferred) and 'cc' (legacy alias). The examples
19
+ below use cchost; prefer it in scripts because 'cc' shadows the system C
20
+ compiler on macOS and Linux.
17
21
 
18
22
  Usage:
19
- cc add <profile> [--custom [...]] Create a profile (auto-links all 7 resources to ~/.claude/)
20
- cc use <profile> [...args] Launch Claude under a named profile
21
- cc list [<profile>] [--json] [--reveal] List profiles, or show one in detail
22
- cc copy default <profile> [--force] Copy ~/.claude/ resources into a profile (only when unlinked)
23
- cc link <p>... [--projects|--tools|--config] (Re-)link a profile's resources to ~/.claude/
24
- cc unlink <profile> [--projects|--tools|--config] Restore a profile's private resources (non-destructive)
25
- cc remove <profile> [--force] Delete a profile and all its data
26
- cc doctor [--fix [--force]] [--json] Diagnose environment, storage, and platform issues
27
- cc help [command] Show usage (optionally for a single command)
28
- cc --version Print the installed cchost version
23
+ cchost add <profile> [--custom [...]] Create a profile (auto-links all 7 resources to ~/.claude/)
24
+ cchost use <profile> [...args] Launch Claude under a named profile
25
+ cchost list [<profile>] [--json] [--reveal] List profiles, or show one in detail
26
+ cchost copy default <profile> [--force] [--dry-run] Copy ~/.claude/ resources into a profile (only when unlinked)
27
+ cchost link <p>... [--projects|--tools|--config] (Re-)link a profile's resources to ~/.claude/
28
+ cchost unlink <profile> [--projects|--tools|--config] Restore a profile's private resources (non-destructive)
29
+ cchost remove <profile> [--force] Delete a profile and all its data
30
+ cchost doctor [--fix [--force]] [--json] Diagnose environment, storage, and platform issues
31
+ cchost help [command] Show usage (optionally for a single command)
32
+ cchost help sharing Task guide: what is shared, and how to change it
33
+ cchost --version Print the installed cchost version
29
34
 
30
35
  Shared resources (linked into ~/.claude/ by default):
31
36
  --projects projects/ (session history)
@@ -35,35 +40,37 @@ Shared resources (linked into ~/.claude/ by default):
35
40
  Never shared (stay per-profile):
36
41
  .credentials.json .claude.json .cc-env.json (identity / auth)
37
42
 
43
+ link, unlink, and copy accept --dry-run to preview changes without making them.
44
+
38
45
  Profiles live in ~/.claude-profiles/<profile>/ and become CLAUDE_CONFIG_DIR
39
46
  for that launch. Args after the profile name are forwarded verbatim to claude.
40
47
  `;
41
48
 
42
49
  const HELP_BY_CMD = {
43
- add: `cc add <profile> [--custom [--base-url=URL --token=TOKEN ...]]
50
+ add: `cchost add <profile> [--custom [--base-url=URL --token=TOKEN ...]]
44
51
 
45
52
  Create a new profile directory without launching Claude. All 7 shared
46
53
  resources (projects/, skills/, agents/, commands/, mcp.json, settings.json,
47
54
  CLAUDE.md) are auto-linked into ~/.claude/ so they're visible to every
48
55
  profile (and to bare \`claude\`) from the moment the profile is created.
49
56
 
50
- cc add <profile>
57
+ cchost add <profile>
51
58
  Create a regular profile. It logs in with your Anthropic
52
- subscription (OAuth) on the first 'cc use'.
59
+ subscription (OAuth) on the first 'cchost use'.
53
60
 
54
- cc add <profile> --custom
61
+ cchost add <profile> --custom
55
62
  Create a profile routed through a custom, Anthropic-compatible API
56
63
  endpoint — Moonshot/Kimi, OpenRouter, a self-hosted vLLM, a corporate
57
64
  proxy, and so on. Opens an interactive wizard; the API token is never
58
65
  echoed to the screen.
59
66
 
60
- cc add <profile> --custom --base-url=URL --token=TOKEN
67
+ cchost add <profile> --custom --base-url=URL --token=TOKEN
61
68
  [--model=NAME] [--opus=NAME] [--sonnet=NAME]
62
69
  [--haiku=NAME] [--subagent=NAME]
63
70
  Same, non-interactive. --base-url and --token are required.
64
71
 
65
- Re-running 'cc add <profile> --custom' on an existing endpoint profile EDITS
66
- its configuration. View any profile with 'cc list <profile>'.
72
+ Re-running 'cchost add <profile> --custom' on an existing endpoint profile EDITS
73
+ its configuration. View any profile with 'cchost list <profile>'.
67
74
 
68
75
  Billing: a custom-endpoint profile bills through the third-party provider,
69
76
  NOT your Anthropic subscription. A profile is born subscription or endpoint
@@ -73,31 +80,32 @@ The endpoint config lives at ~/.claude-profiles/<profile>/.cc-env.json with
73
80
  user-only permissions. It is never linked and never copied.
74
81
 
75
82
  Examples:
76
- cc add work
77
- cc add kimi --custom
78
- cc add kimi --custom --base-url=https://api.moonshot.ai/anthropic --token=sk-... --model=kimi-k2.5
83
+ cchost add work
84
+ cchost add kimi --custom
85
+ cchost add kimi --custom --base-url=https://api.moonshot.ai/anthropic --token=sk-... --model=kimi-k2.5
79
86
  `,
80
- use: `cc use <profile> [...args]
87
+ use: `cchost use <profile> [...args]
81
88
 
82
89
  Launch Claude under a named profile. The profile must already exist (create
83
- it with 'cc add <profile>'). Its directory under ~/.claude-profiles/<profile>/
90
+ it with 'cchost add <profile>'). Its directory under ~/.claude-profiles/<profile>/
84
91
  becomes CLAUDE_CONFIG_DIR for the spawned claude — credentials and per-
85
92
  profile config are isolated; the 7 shared resources are visible via symlinks.
86
93
 
87
94
  All args after the profile name are forwarded verbatim to claude.
88
95
 
89
96
  Examples:
90
- cc use work
91
- cc use work --resume
92
- cc use personal -p "summarize this codebase"
97
+ cchost use work
98
+ cchost use work --resume
99
+ cchost use personal -p "summarize this codebase"
93
100
  `,
94
- list: `cc list [<profile>] [--json] [--reveal]
101
+ list: `cchost list [<profile>] [--json] [--reveal]
95
102
 
96
103
  Without a profile name: list every profile as a table.
97
104
 
98
105
  Columns:
99
106
  Profile - profile name
100
- LoggedIn - true if .credentials.json exists
107
+ LoggedIn - true if credentials are present. On macOS the token lives in the
108
+ login Keychain and the login is inferred from .claude.json.
101
109
  Email - account email from .claude.json (masked by default; pass
102
110
  --reveal to show the full value); - if not yet logged in
103
111
  Shared - N/7 resources linked to ~/.claude/. 7/7 = fully shared (the
@@ -105,23 +113,24 @@ Columns:
105
113
  Endpoint - custom-endpoint host from .cc-env.json, or "subscription" if
106
114
  the profile uses Anthropic's subscription OAuth
107
115
 
108
- With a profile name: print that one profile's detail, including its custom
109
- endpoint configuration if it has one. The API token is masked by default;
116
+ With a profile name: print that one profile's detail: the state of each of
117
+ the 7 resources (shared, private, empty, missing, or foreign-link) and its
118
+ custom endpoint configuration if it has one. The API token is masked by default;
110
119
  --reveal prints both the token and the full email — do not paste that output
111
120
  anywhere.
112
121
 
113
- cc list Table of all profiles (emails masked)
114
- cc list --reveal Table with full emails shown
115
- cc list --json Same, as a JSON array (always masked)
116
- cc list <profile> Detail for a single profile
117
- cc list <profile> --reveal Detail with the full token + email shown
118
- cc list <profile> --json Detail as a JSON object (always masked)
122
+ cchost list Table of all profiles (emails masked)
123
+ cchost list --reveal Table with full emails shown
124
+ cchost list --json Same, as a JSON array (always masked)
125
+ cchost list <profile> Detail for a single profile
126
+ cchost list <profile> --reveal Detail with the full token + email shown
127
+ cchost list <profile> --json Detail as a JSON object (always masked)
119
128
  `,
120
- copy: `cc copy default <profile> [--force]
129
+ copy: `cchost copy default <profile> [--force] [--dry-run]
121
130
 
122
131
  Copy ~/.claude/ resources into a profile. Useful only when the profile has
123
132
  'unlinked' one or more resources (private content); for fresh v0.4.1
124
- profiles every resource is symlinked, so 'cc copy' is a no-op.
133
+ profiles every resource is symlinked, so 'cchost copy' is a no-op.
125
134
 
126
135
  <src>: default (= ~/.claude/, Claude Code's own config dir)
127
136
  <dest>: <profile-name>
@@ -135,32 +144,44 @@ For each resource:
135
144
  - If the destination is shared via symlink/hard-link to the source → skipped
136
145
  - Else, copy with skip-on-collision (entries already at the destination win)
137
146
 
138
- --force Overwrite collisions at the destination instead of skipping.
147
+ --force Overwrite collisions at the destination instead of skipping.
148
+ --dry-run Print what would be copied, skipped, or overwritten; change nothing.
139
149
 
140
150
  Examples:
141
- cc copy default work # only acts on unlinked resources
142
- cc copy default work --force # overwrite collisions
151
+ cchost copy default work --dry-run # preview first
152
+ cchost copy default work # only acts on unlinked resources
153
+ cchost copy default work --force # overwrite collisions
143
154
 
144
155
  Removed in v0.4: 'shared' as src/dest. The shared pool IS ~/.claude/ in v0.4
145
- and is reached via symlinks. Use 'cc link' / 'cc unlink' to manage sharing.
146
- Note: 'cc migrate' is a deprecated alias for 'cc copy' — it still works but
156
+ and is reached via symlinks. Use 'cchost link' / 'cchost unlink' to manage sharing.
157
+ Note: 'cchost migrate' is a deprecated alias for 'cchost copy' — it still works but
147
158
  will be removed in a future version.
148
159
  `,
149
- link: `cc link <profile> [<profile>...] [--projects|--tools|--config]
160
+ migrate: `cchost migrate <src> <dest> [--force]
161
+
162
+ Deprecated alias for 'cchost copy'. It prints a deprecation warning and
163
+ forwards its arguments. Use 'cchost copy' instead; this alias will be removed
164
+ in a future version.
150
165
 
151
- (Re-)link a profile's resources into the shared pool at ~/.claude/. 'cc add'
152
- already does this for new profiles; 'cc link' is for re-applying it after
153
- 'cc unlink' or for older profiles that pre-date v0.4.1.
166
+ Examples:
167
+ cchost migrate default work
168
+ `,
169
+ link: `cchost link <profile> [<profile>...] [--projects|--tools|--config] [--dry-run]
170
+
171
+ (Re-)link a profile's resources into the shared pool at ~/.claude/. 'cchost add'
172
+ already does this for new profiles; 'cchost link' is for re-applying it after
173
+ 'cchost unlink' or for older profiles that pre-date v0.4.1.
154
174
 
155
175
  No flag link all 7 resources
156
176
  --projects projects/
157
177
  --tools skills/, agents/, commands/
158
178
  --config mcp.json, settings.json, CLAUDE.md
159
179
  (flags can be combined: --tools --config)
180
+ --dry-run print what would be linked or refused; change nothing
160
181
 
161
182
  Refuses to overwrite private content: if the profile has a non-empty resource
162
183
  that isn't already a symlink, that resource is reported as 'refused' and left
163
- alone — move or remove it manually, then re-run 'cc link'. The other
184
+ alone — move or remove it manually, then re-run 'cchost link'. The other
164
185
  resources are still linked.
165
186
 
166
187
  On Windows, dirs use junctions (no admin/Developer Mode needed). File
@@ -168,12 +189,13 @@ symlinks may require Developer Mode; without it, files fall back to hard
168
189
  links so editing-in-place still propagates between profiles.
169
190
 
170
191
  Examples:
171
- cc link work # link all 7
172
- cc link work --tools # only skills/, agents/, commands/
173
- cc link work --config # only mcp.json, settings.json, CLAUDE.md
174
- cc link a b c # link 3 profiles in one shot
192
+ cchost link work # link all 7
193
+ cchost link work --tools # only skills/, agents/, commands/
194
+ cchost link work --config # only mcp.json, settings.json, CLAUDE.md
195
+ cchost link a b c # link 3 profiles in one shot
196
+ cchost link work --dry-run # preview
175
197
  `,
176
- unlink: `cc unlink <profile> [--projects|--tools|--config]
198
+ unlink: `cchost unlink <profile> [--projects|--tools|--config] [--dry-run]
177
199
 
178
200
  Break the symlinks and copy current ~/.claude/ content into the profile as
179
201
  private resources. Non-destructive: same-name items already private in the
@@ -183,16 +205,50 @@ No flag unlink all 7 resources
183
205
  --projects projects/
184
206
  --tools skills/, agents/, commands/
185
207
  --config mcp.json, settings.json, CLAUDE.md
208
+ --dry-run print what would be unlinked and copied; change nothing
186
209
 
187
210
  After unlinking, the profile's resource diverges from ~/.claude/. Other
188
211
  profiles still see the shared pool.
189
212
 
190
213
  Examples:
191
- cc unlink work # full private profile
192
- cc unlink work --config # only mcp.json / settings.json / CLAUDE.md private
193
- cc unlink work --tools --config # skills, agents, commands AND config private
214
+ cchost unlink work # full private profile
215
+ cchost unlink work --config # only mcp.json / settings.json / CLAUDE.md private
216
+ cchost unlink work --tools --config # skills, agents, commands AND config private
217
+ cchost unlink work --dry-run # preview
194
218
  `,
195
- remove: `cc remove <profile> [--force]
219
+ sharing: `cchost help sharing — what profiles share, and how to change it
220
+
221
+ Every profile has 7 resources. By default each one is a link into ~/.claude/,
222
+ so all profiles (and bare \`claude\`) see the same content:
223
+
224
+ --projects projects/ session history
225
+ --tools skills/ agents/ commands/ user tooling
226
+ --config mcp.json settings.json CLAUDE.md MCP servers, settings, memory
227
+
228
+ Logins (.credentials.json, .claude.json, .cc-env.json) are never shared.
229
+
230
+ See where a profile stands:
231
+ cchost list Shared column: 7/7 = all shared
232
+ cchost list work state of each resource
233
+
234
+ Make something private to one profile:
235
+ cchost unlink work --config own mcp.json / settings.json / CLAUDE.md
236
+ cchost unlink work all 7 private
237
+ The current shared content is copied in, so nothing disappears; from then
238
+ on, changes in that profile stay in that profile.
239
+
240
+ Share it again:
241
+ cchost link work --config back to the shared ~/.claude/ copy
242
+ link refuses to replace private content. Move or delete the private file or
243
+ folder first; it is not merged into ~/.claude/ automatically.
244
+
245
+ Copy shared content into a private profile (top-up, no overwrite):
246
+ cchost copy default work add what ~/.claude/ has and work lacks
247
+ cchost copy default work --force overwrite work's copies instead
248
+
249
+ Preview any of these first with --dry-run.
250
+ `,
251
+ remove: `cchost remove <profile> [--force]
196
252
 
197
253
  Delete a profile and everything inside its directory: credentials, sessions,
198
254
  and any private resources. To confirm, you must type the exact profile name —
@@ -203,7 +259,7 @@ removed — the shared content in ~/.claude/ is preserved for other profiles.
203
259
  If any resource is PRIVATE (unlinked), --force is required because its
204
260
  content is unique to this profile and will be lost forever.
205
261
  `,
206
- doctor: `cc doctor [--fix [--force]] [--json]
262
+ doctor: `cchost doctor [--fix [--force]] [--json]
207
263
 
208
264
  Run diagnostic checks across four groups:
209
265
 
@@ -213,7 +269,7 @@ Run diagnostic checks across four groups:
213
269
  ~/.claude-shared/ folded into ~/.claude/ if still present
214
270
  Platform Windows: OneDrive home, WindowsApps stub, stale ~/bin/cc;
215
271
  Git Bash: cygpath
216
- Sanity Test-spawn claude --version through the same code path cc use uses
272
+ Sanity Test-spawn claude --version through the same code path cchost use uses
217
273
 
218
274
  Exit codes: 0 = all ok, 1 = warnings, 2 = errors.
219
275
 
@@ -223,7 +279,7 @@ Exit codes: 0 = all ok, 1 = warnings, 2 = errors.
223
279
  .claude.json)
224
280
  --json Machine-readable output for CI / scripting
225
281
  `,
226
- help: `cc help [command]
282
+ help: `cchost help [command]
227
283
 
228
284
  Without an argument: print the summary of all commands.
229
285
  With a command name: print detail for just that command.
@@ -247,11 +303,11 @@ function printHelp(cmd) {
247
303
  process.stdout.write(HELP);
248
304
  }
249
305
 
250
- function prompt(question) {
306
+ function prompt(question, output = process.stdout) {
251
307
  return new Promise((resolve) => {
252
308
  const rl = readline.createInterface({
253
309
  input: process.stdin,
254
- output: process.stdout,
310
+ output,
255
311
  });
256
312
  rl.question(question, (answer) => {
257
313
  rl.close();
@@ -316,6 +372,18 @@ function parseFlagsStrict(args, flagNames, { allowPositional = false } = {}) {
316
372
  return { flags, positional };
317
373
  }
318
374
 
375
+ function resourceLabel(resource) {
376
+ return share.isFileResource(resource) ? resource : `${resource}/`;
377
+ }
378
+
379
+ // One-line explanation per resource state for `cchost list <profile>`.
380
+ const RESOURCE_STATE_NOTES = {
381
+ private: 'only this profile sees it',
382
+ empty: 'private, but empty',
383
+ missing: 'not set up; cchost link creates it',
384
+ 'foreign-link': 'points outside ~/.claude/; cchost leaves it alone',
385
+ };
386
+
319
387
  // Count how many of the 7 shared resources a profile currently has linked
320
388
  // (symlink OR — on Windows file fallback — hard link).
321
389
  function countSharedResources(profileDir) {
@@ -326,7 +394,7 @@ function countSharedResources(profileDir) {
326
394
  return n;
327
395
  }
328
396
 
329
- // Flag name -> .cc-env.json field, for `cc add --custom`.
397
+ // Flag name -> .cc-env.json field, for `cchost add --custom`.
330
398
  const ENDPOINT_VALUE_FLAGS = {
331
399
  '--base-url': 'base_url',
332
400
  '--token': 'auth_token',
@@ -364,15 +432,15 @@ async function cmdAdd(rest) {
364
432
 
365
433
  const [name, ...extra] = positional;
366
434
  if (!name) {
367
- throw usageError('Usage: cc add <profile> [--custom [--base-url=URL --token=TOKEN ...]]');
435
+ throw usageError('Usage: cchost add <profile> [--custom [--base-url=URL --token=TOKEN ...]]');
368
436
  }
369
- if (extra.length > 0) throw usageError(`cc add takes no extra arguments: ${extra[0]}`);
437
+ if (extra.length > 0) throw usageError(`cchost add takes no extra arguments: ${extra[0]}`);
370
438
  profiles.validateProfileName(name);
371
439
 
372
440
  if (Object.keys(provided).length > 0 && !custom) {
373
441
  throw usageError(
374
442
  'Endpoint flags (--base-url, --token, --model, ...) require --custom: ' +
375
- `cc add ${name} --custom ...`,
443
+ `cchost add ${name} --custom ...`,
376
444
  );
377
445
  }
378
446
 
@@ -383,7 +451,7 @@ async function cmdAdd(rest) {
383
451
  const { dir } = profiles.addProfile(name);
384
452
  console.log(`Created profile "${name}" at ${dir}.`);
385
453
  autoLinkArtifacts(name);
386
- console.log(`Next: cc use ${name}`);
454
+ console.log(`Next: cchost use ${name}`);
387
455
  return 0;
388
456
  }
389
457
 
@@ -417,7 +485,7 @@ async function cmdAdd(rest) {
417
485
  return result.code;
418
486
  }
419
487
 
420
- if (!existed) console.log(`Next: cc use ${name}`);
488
+ if (!existed) console.log(`Next: cchost use ${name}`);
421
489
  return result.code;
422
490
  }
423
491
 
@@ -448,8 +516,8 @@ function runLegacyPoolMigration() {
448
516
  }
449
517
 
450
518
  // On profile creation, symlink all 7 resources into ~/.claude/. Failures emit
451
- // a warning but do NOT abort cc add — the profile is still usable, and the
452
- // user can re-run `cc link <name>` to retry.
519
+ // a warning but do NOT abort cchost add — the profile is still usable, and the
520
+ // user can re-run `cchost link <name>` to retry.
453
521
  function autoLinkArtifacts(name) {
454
522
  let results;
455
523
  try {
@@ -462,7 +530,9 @@ function autoLinkArtifacts(name) {
462
530
  );
463
531
  return;
464
532
  }
465
- const failed = results.filter((r) => r.status === 'failed' || r.status === 'refused');
533
+ const failed = results.filter(
534
+ (r) => r.status === 'failed' || r.status === 'refused' || r.status === 'foreign-link',
535
+ );
466
536
  const linked = results.length - failed.length;
467
537
  if (failed.length === 0) {
468
538
  console.log(` Linked ${linked}/${results.length} resources to ~/.claude/.`);
@@ -476,35 +546,51 @@ function autoLinkArtifacts(name) {
476
546
  }
477
547
  process.stderr.write(
478
548
  color.stderr.yellow(
479
- ` Re-run \`cc link ${name}\` after resolving the above.`,
549
+ ` Re-run \`cchost link ${name}\` after resolving the above.`,
480
550
  ) + '\n',
481
551
  );
482
552
  }
483
553
 
484
554
  async function cmdUse(rest) {
485
555
  const [name, ...passthrough] = rest;
486
- if (!name) throw usageError('Usage: cc use <profile> [...args]');
556
+ if (!name) throw usageError('Usage: cchost use <profile> [...args]');
487
557
  profiles.validateProfileName(name);
488
558
  const dir = profiles.profileDir(name);
489
559
  if (!fs.existsSync(dir)) {
490
- const err = new Error(`Profile "${name}" does not exist. Create it with: cc add ${name}`);
560
+ const err = new Error(`Profile "${name}" does not exist. Create it with: cchost add ${name}`);
491
561
  err.exitCode = 1;
492
562
  throw err;
493
563
  }
494
564
 
495
565
  let extraEnv = {};
566
+ let unsetEnv = [];
496
567
  const envConfig = envLib.readEnvConfig(dir);
497
568
  if (envConfig) {
498
569
  extraEnv = envLib.buildEnvVars(envConfig);
570
+ unsetEnv = envLib.endpointEnvToStrip(extraEnv);
499
571
  const host = envConfig.base_url ? envLib.hostOf(envConfig.base_url) : '(unknown)';
500
572
  process.stderr.write(
501
573
  `[cchost] Profile '${name}' → ${host} (custom endpoint billing applies)\n`,
502
574
  );
575
+ if (unsetEnv.length > 0) {
576
+ process.stderr.write(
577
+ `[cchost] notice: removed inherited ${unsetEnv.join(', ')} for endpoint profile '${name}'.\n`,
578
+ );
579
+ }
580
+ } else {
581
+ const conflicts = envLib.authEnvConflicts();
582
+ if (conflicts.length > 0) {
583
+ process.stderr.write(
584
+ `[cchost] warning: ${conflicts.join(', ')} set in the environment; ` +
585
+ `this may override the subscription login for '${name}'. ` +
586
+ `Unset ${conflicts.length > 1 ? 'them' : 'it'} to use the profile's login.\n`,
587
+ );
588
+ }
503
589
  }
504
- return spawnClaude({ args: passthrough, profileDir: dir, extraEnv });
590
+ return spawnClaude({ args: passthrough, profileDir: dir, extraEnv, unsetEnv });
505
591
  }
506
592
 
507
- // `cc list <profile>` — detail for a single profile, including its custom
593
+ // `cchost list <profile>` — detail for a single profile, including its custom
508
594
  // endpoint configuration if it has one.
509
595
  function cmdListOne(name, { json, reveal }) {
510
596
  if (json && reveal) {
@@ -513,15 +599,16 @@ function cmdListOne(name, { json, reveal }) {
513
599
  profiles.validateProfileName(name);
514
600
  const dir = profiles.profileDir(name);
515
601
  if (!fs.existsSync(dir)) {
516
- const e = new Error(`Profile "${name}" does not exist. Create it with: cc add ${name}`);
602
+ const e = new Error(`Profile "${name}" does not exist. Create it with: cchost add ${name}`);
517
603
  e.exitCode = 1;
518
604
  throw e;
519
605
  }
520
606
 
521
607
  const total = share.SHARED_RESOURCES.length;
522
- const sharedCount = countSharedResources(dir);
523
- const loggedIn = profiles.hasCredentials(dir);
524
- const email = profiles.readEmail(dir);
608
+ const resources = {};
609
+ for (const r of share.SHARED_RESOURCES) resources[r] = share.resourceState(dir, r);
610
+ const sharedCount = Object.values(resources).filter((st) => st === 'shared').length;
611
+ const { credentialSource, loggedIn, email } = profiles.readProfile(dir);
525
612
  const displayEmail = reveal ? email : (email ? envLib.maskEmail(email) : email);
526
613
 
527
614
  let config = null;
@@ -537,8 +624,10 @@ function cmdListOne(name, { json, reveal }) {
537
624
  name,
538
625
  dir,
539
626
  loggedIn,
627
+ credentialSource,
540
628
  email: displayEmail,
541
629
  shared: sharedCount,
630
+ resources,
542
631
  endpoint: envLib.endpointSummary(dir),
543
632
  };
544
633
  if (config) {
@@ -558,9 +647,18 @@ function cmdListOne(name, { json, reveal }) {
558
647
 
559
648
  console.log(`Profile: ${name}`);
560
649
  console.log(`Directory: ${dir}`);
561
- console.log(`LoggedIn: ${loggedIn ? 'true' : 'false'}`);
650
+ console.log(
651
+ `LoggedIn: ${loggedIn ? 'true' : 'false'}` +
652
+ (credentialSource === 'keychain' ? ' (macOS Keychain)' : ''),
653
+ );
562
654
  console.log(`Email: ${displayEmail || '-'}`);
563
655
  console.log(`Shared: ${sharedCount}/${total}`);
656
+ const width = Math.max(...share.SHARED_RESOURCES.map((r) => resourceLabel(r).length));
657
+ for (const r of share.SHARED_RESOURCES) {
658
+ const st = resources[r];
659
+ const note = st === 'shared' ? `→ ~/.claude/${r}` : (RESOURCE_STATE_NOTES[st] || '');
660
+ console.log(` ${resourceLabel(r).padEnd(width)} ${st.padEnd(12)} ${note}`.trimEnd());
661
+ }
564
662
 
565
663
  if (configError) {
566
664
  console.log('Endpoint: (invalid config — .cc-env.json is malformed)');
@@ -620,6 +718,7 @@ function cmdList(rest) {
620
718
  name: p.name,
621
719
  dir: p.dir,
622
720
  loggedIn: p.loggedIn,
721
+ credentialSource: p.credentialSource,
623
722
  email: reveal ? p.email : (p.email ? envLib.maskEmail(p.email) : p.email),
624
723
  shared: sharedCount,
625
724
  endpoint: envLib.endpointSummary(p.dir),
@@ -633,7 +732,7 @@ function cmdList(rest) {
633
732
 
634
733
  if (enriched.length === 0) {
635
734
  console.log(`No profiles yet in ${profiles.baseDir()}.`);
636
- console.log('Create one with: cc add <name>');
735
+ console.log('Create one with: cchost add <name>');
637
736
  return 0;
638
737
  }
639
738
 
@@ -674,7 +773,7 @@ function hasPrivateContent(dir) {
674
773
  async function cmdRemove(rest) {
675
774
  const { flags, positional } = parseFlagsStrict(rest, ['--force'], { allowPositional: true });
676
775
  const [name, ...extra] = positional;
677
- if (!name) throw usageError('Usage: cc remove <profile> [--force]');
776
+ if (!name) throw usageError('Usage: cchost remove <profile> [--force]');
678
777
  if (extra.length > 0) throw usageError(`Unexpected argument: ${extra[0]}`);
679
778
  const force = flags.has('--force');
680
779
  profiles.validateProfileName(name);
@@ -689,7 +788,7 @@ async function cmdRemove(rest) {
689
788
  const err = new Error(
690
789
  `Profile "${name}" has private (unlinked) resources whose content is not ` +
691
790
  `in ~/.claude/. Removing the profile will delete that content permanently.\n` +
692
- `Re-run with --force to confirm: cc remove ${name} --force`,
791
+ `Re-run with --force to confirm: cchost remove ${name} --force`,
693
792
  );
694
793
  err.exitCode = 1;
695
794
  throw err;
@@ -721,19 +820,22 @@ async function cmdRemove(rest) {
721
820
  async function cmdLink(rest) {
722
821
  const { flags, positional } = parseFlagsStrict(
723
822
  rest,
724
- ['--tools', '--projects', '--config'],
823
+ ['--tools', '--projects', '--config', '--dry-run'],
725
824
  { allowPositional: true },
726
825
  );
727
826
  if (positional.length === 0) {
728
827
  throw usageError(
729
- 'Usage: cc link <profile> [<profile>...] [--projects|--tools|--config]',
828
+ 'Usage: cchost link <profile> [<profile>...] [--projects|--tools|--config] [--dry-run]',
730
829
  );
731
830
  }
831
+ const dryRun = flags.has('--dry-run');
732
832
  const opts = {
733
833
  tools: flags.has('--tools'),
734
834
  projects: flags.has('--projects'),
735
835
  config: flags.has('--config'),
836
+ dryRun,
736
837
  };
838
+ if (dryRun) console.log('Dry run — nothing will be changed.');
737
839
  const multi = positional.length > 1;
738
840
  const summary = [];
739
841
  for (const name of positional) {
@@ -752,19 +854,27 @@ async function cmdLink(rest) {
752
854
  process.stderr.write(color.stderr.red(` ${label}: failed — ${r.message}`) + '\n');
753
855
  } else if (r.status === 'refused') {
754
856
  process.stderr.write(color.stderr.yellow(` ${label}: refused — ${r.message}`) + '\n');
857
+ } else if (r.status === 'foreign-link') {
858
+ process.stderr.write(color.stderr.yellow(` ${label}: foreign link — ${r.message}`) + '\n');
755
859
  } else if (r.status === 'already-linked') {
756
860
  console.log(` ${label}: already linked`);
757
861
  } else if (r.relinkedFromLegacy) {
758
- console.log(` ${label}: relinked (was pointing at legacy ~/.claude-shared/)`);
862
+ console.log(
863
+ ` ${label}: ${dryRun ? 'would relink' : 'relinked'} (was pointing at legacy ~/.claude-shared/)`,
864
+ );
865
+ } else if (dryRun) {
866
+ console.log(` ${label}: would link → ~/.claude/${r.resource}`);
759
867
  } else {
760
868
  const note = r.kind === 'hardlink' ? ' (hard link — enable Windows Developer Mode for a true symlink)' : '';
761
869
  console.log(` ${label}: linked${note}`);
762
870
  }
763
871
  }
764
- const failed = results.filter((r) => r.status === 'failed' || r.status === 'refused');
872
+ const failed = results.filter(
873
+ (r) => r.status === 'failed' || r.status === 'refused' || r.status === 'foreign-link',
874
+ );
765
875
  const linked = results.length - failed.length;
766
876
  console.log(
767
- ` ${linked}/${results.length} linked` +
877
+ ` ${dryRun ? 'would end with ' : ''}${linked}/${results.length} linked` +
768
878
  (failed.length ? `, ${failed.length} not linked` : '') + '.',
769
879
  );
770
880
  summary.push({
@@ -794,19 +904,22 @@ async function cmdLink(rest) {
794
904
  async function cmdUnlink(rest) {
795
905
  const { flags, positional } = parseFlagsStrict(
796
906
  rest,
797
- ['--tools', '--projects', '--config'],
907
+ ['--tools', '--projects', '--config', '--dry-run'],
798
908
  { allowPositional: true },
799
909
  );
800
910
  const [name, ...extra] = positional;
801
911
  if (!name) {
802
- throw usageError('Usage: cc unlink <profile> [--projects|--tools|--config]');
912
+ throw usageError('Usage: cchost unlink <profile> [--projects|--tools|--config] [--dry-run]');
803
913
  }
804
914
  if (extra.length > 0) throw usageError(`Unexpected argument: ${extra[0]}`);
915
+ const dryRun = flags.has('--dry-run');
805
916
  const opts = {
806
917
  tools: flags.has('--tools'),
807
918
  projects: flags.has('--projects'),
808
919
  config: flags.has('--config'),
920
+ dryRun,
809
921
  };
922
+ if (dryRun) console.log('Dry run — nothing will be changed.');
810
923
  const results = share.shareUnlink(name, opts);
811
924
  let anyFailure = false;
812
925
  for (const r of results) {
@@ -816,6 +929,9 @@ async function cmdUnlink(rest) {
816
929
  process.stderr.write(color.stderr.red(` ${label}: failed — ${r.message}`) + '\n');
817
930
  } else if (r.status === 'not-linked') {
818
931
  console.log(` ${label}: not linked, skipping`);
932
+ } else if (dryRun) {
933
+ const what = r.items === undefined ? 'its content' : `${r.items} item(s)`;
934
+ console.log(` ${label}: would unlink and copy ${what} from ~/.claude/ into the profile`);
819
935
  } else {
820
936
  console.log(` ${label}: unlinked, content copied back`);
821
937
  }
@@ -834,24 +950,30 @@ async function cmdDoctor(rest) {
834
950
  }
835
951
 
836
952
  async function cmdCopy(rest) {
837
- const { flags, positional } = parseFlagsStrict(rest, ['--force'], { allowPositional: true });
953
+ const { flags, positional } = parseFlagsStrict(
954
+ rest,
955
+ ['--force', '--dry-run'],
956
+ { allowPositional: true },
957
+ );
838
958
  const [src, dest, ...extra] = positional;
839
- if (!src || !dest) throw usageError('Usage: cc copy <src> <dest> [--force]');
959
+ if (!src || !dest) throw usageError('Usage: cchost copy <src> <dest> [--force] [--dry-run]');
840
960
  if (extra.length > 0) throw usageError(`Unexpected extra arguments: ${extra.join(' ')}`);
841
961
  const force = flags.has('--force');
962
+ const dryRun = flags.has('--dry-run');
963
+ if (dryRun) process.stdout.write('Dry run — nothing will be changed.\n');
842
964
  if (force) {
843
965
  process.stdout.write(
844
- 'Warning: --force is set. Existing items at the destination will be overwritten.\n',
966
+ `Warning: --force is set. Existing items at the destination ${dryRun ? 'would' : 'will'} be overwritten.\n`,
845
967
  );
846
968
  }
847
- copyLib.copy(src, dest, { force });
969
+ copyLib.copy(src, dest, { force, dryRun });
848
970
  return 0;
849
971
  }
850
972
 
851
973
  async function cmdMigrate(rest) {
852
974
  process.stderr.write(
853
975
  color.stderr.yellow(
854
- "[cchost] 'cc migrate' is deprecated — use 'cc copy' instead. " +
976
+ "[cchost] 'cchost migrate' is deprecated — use 'cchost copy' instead. " +
855
977
  'This alias will be removed in a future version.',
856
978
  ) + '\n',
857
979
  );
@@ -876,8 +998,8 @@ async function run(argv) {
876
998
  if (action === 'env') {
877
999
  process.stderr.write(
878
1000
  color.stderr.red(
879
- "'cc env' was removed in v0.3.1. Configure a custom endpoint with " +
880
- "'cc add <profile> --custom', and view it with 'cc list <profile>'.",
1001
+ "'cchost env' was removed in v0.3.1. Configure a custom endpoint with " +
1002
+ "'cchost add <profile> --custom', and view it with 'cchost list <profile>'.",
881
1003
  ) + '\n',
882
1004
  );
883
1005
  return 2;