cchost 0.3.1 → 0.4.1
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/CHANGELOG.md +113 -0
- package/README.md +82 -70
- package/lib/banner.js +29 -0
- package/lib/cli.js +341 -151
- package/lib/copy.js +242 -0
- package/lib/doctor.js +154 -22
- package/lib/env.js +20 -0
- package/lib/profiles.js +10 -0
- package/lib/share.js +343 -57
- package/package.json +1 -1
- package/lib/migrate.js +0 -145
package/lib/cli.js
CHANGED
|
@@ -6,26 +6,35 @@ const readline = require('readline');
|
|
|
6
6
|
|
|
7
7
|
const profiles = require('./profiles');
|
|
8
8
|
const share = require('./share');
|
|
9
|
-
const
|
|
9
|
+
const copyLib = require('./copy');
|
|
10
10
|
const envLib = require('./env');
|
|
11
11
|
const { spawnClaude } = require('./spawn');
|
|
12
12
|
const { renderTable } = require('./format');
|
|
13
13
|
const color = require('./color');
|
|
14
|
+
const { renderBanner } = require('./banner');
|
|
14
15
|
|
|
15
16
|
const HELP = `cc - Multi-profile launcher for Claude Code
|
|
16
17
|
|
|
17
18
|
Usage:
|
|
18
|
-
cc add <profile> [--custom [...]] Create a profile (
|
|
19
|
+
cc add <profile> [--custom [...]] Create a profile (auto-links all 7 resources to ~/.claude/)
|
|
19
20
|
cc use <profile> [...args] Launch Claude under a named profile
|
|
20
|
-
cc list [<profile>] [--json] [--reveal] List
|
|
21
|
-
cc
|
|
22
|
-
cc link <p>...
|
|
23
|
-
cc unlink <profile>
|
|
24
|
-
cc remove <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
|
|
25
26
|
cc doctor [--fix [--force]] [--json] Diagnose environment, storage, and platform issues
|
|
26
27
|
cc help [command] Show usage (optionally for a single command)
|
|
27
28
|
cc --version Print the installed cchost version
|
|
28
29
|
|
|
30
|
+
Shared resources (linked into ~/.claude/ by default):
|
|
31
|
+
--projects projects/ (session history)
|
|
32
|
+
--tools skills/ agents/ commands/ (user tooling)
|
|
33
|
+
--config mcp.json settings.json CLAUDE.md (MCP, settings, user memory)
|
|
34
|
+
|
|
35
|
+
Never shared (stay per-profile):
|
|
36
|
+
.credentials.json .claude.json .cc-env.json (identity / auth)
|
|
37
|
+
|
|
29
38
|
Profiles live in ~/.claude-profiles/<profile>/ and become CLAUDE_CONFIG_DIR
|
|
30
39
|
for that launch. Args after the profile name are forwarded verbatim to claude.
|
|
31
40
|
`;
|
|
@@ -33,7 +42,10 @@ for that launch. Args after the profile name are forwarded verbatim to claude.
|
|
|
33
42
|
const HELP_BY_CMD = {
|
|
34
43
|
add: `cc add <profile> [--custom [--base-url=URL --token=TOKEN ...]]
|
|
35
44
|
|
|
36
|
-
Create a new profile directory without launching Claude.
|
|
45
|
+
Create a new profile directory without launching Claude. All 7 shared
|
|
46
|
+
resources (projects/, skills/, agents/, commands/, mcp.json, settings.json,
|
|
47
|
+
CLAUDE.md) are auto-linked into ~/.claude/ so they're visible to every
|
|
48
|
+
profile (and to bare \`claude\`) from the moment the profile is created.
|
|
37
49
|
|
|
38
50
|
cc add <profile>
|
|
39
51
|
Create a regular profile. It logs in with your Anthropic
|
|
@@ -50,31 +62,27 @@ Create a new profile directory without launching Claude.
|
|
|
50
62
|
[--haiku=NAME] [--subagent=NAME]
|
|
51
63
|
Same, non-interactive. --base-url and --token are required.
|
|
52
64
|
|
|
53
|
-
Re-running 'cc add <profile> --custom' on an existing endpoint profile
|
|
54
|
-
|
|
55
|
-
flags only the flags you pass are changed. View a config with
|
|
56
|
-
'cc list <profile>'.
|
|
65
|
+
Re-running 'cc add <profile> --custom' on an existing endpoint profile EDITS
|
|
66
|
+
its configuration. View any profile with 'cc list <profile>'.
|
|
57
67
|
|
|
58
68
|
Billing: a custom-endpoint profile bills through the third-party provider,
|
|
59
69
|
NOT your Anthropic subscription. A profile is born subscription or endpoint
|
|
60
|
-
and stays that way
|
|
61
|
-
Anthropic OAuth credentials.
|
|
70
|
+
and stays that way.
|
|
62
71
|
|
|
63
72
|
The endpoint config lives at ~/.claude-profiles/<profile>/.cc-env.json with
|
|
64
|
-
user-only permissions. It is never
|
|
73
|
+
user-only permissions. It is never linked and never copied.
|
|
65
74
|
|
|
66
75
|
Examples:
|
|
67
76
|
cc add work
|
|
68
77
|
cc add kimi --custom
|
|
69
78
|
cc add kimi --custom --base-url=https://api.moonshot.ai/anthropic --token=sk-... --model=kimi-k2.5
|
|
70
|
-
cc add kimi --custom --model=kimi-k3 # edit just the model later
|
|
71
79
|
`,
|
|
72
80
|
use: `cc use <profile> [...args]
|
|
73
81
|
|
|
74
82
|
Launch Claude under a named profile. The profile must already exist (create
|
|
75
83
|
it with 'cc add <profile>'). Its directory under ~/.claude-profiles/<profile>/
|
|
76
|
-
becomes CLAUDE_CONFIG_DIR for the spawned claude — credentials
|
|
77
|
-
|
|
84
|
+
becomes CLAUDE_CONFIG_DIR for the spawned claude — credentials and per-
|
|
85
|
+
profile config are isolated; the 7 shared resources are visible via symlinks.
|
|
78
86
|
|
|
79
87
|
All args after the profile name are forwarded verbatim to claude.
|
|
80
88
|
|
|
@@ -90,98 +98,129 @@ Without a profile name: list every profile as a table.
|
|
|
90
98
|
Columns:
|
|
91
99
|
Profile - profile name
|
|
92
100
|
LoggedIn - true if .credentials.json exists
|
|
93
|
-
Email - account email from .claude.json
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
Endpoint - custom-endpoint host from .cc-env.json, or "subscription" if
|
|
98
|
-
profile uses Anthropic's subscription OAuth
|
|
101
|
+
Email - account email from .claude.json (masked by default; pass
|
|
102
|
+
--reveal to show the full value); - if not yet logged in
|
|
103
|
+
Shared - N/7 resources linked to ~/.claude/. 7/7 = fully shared (the
|
|
104
|
+
v0.4.1 default), 0/7 = fully private
|
|
105
|
+
Endpoint - custom-endpoint host from .cc-env.json, or "subscription" if
|
|
106
|
+
the profile uses Anthropic's subscription OAuth
|
|
99
107
|
|
|
100
108
|
With a profile name: print that one profile's detail, including its custom
|
|
101
109
|
endpoint configuration if it has one. The API token is masked by default;
|
|
102
|
-
--reveal prints
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
cc list
|
|
106
|
-
cc list
|
|
107
|
-
cc list
|
|
108
|
-
cc list <profile>
|
|
110
|
+
--reveal prints both the token and the full email — do not paste that output
|
|
111
|
+
anywhere.
|
|
112
|
+
|
|
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)
|
|
109
119
|
`,
|
|
110
|
-
|
|
120
|
+
copy: `cc copy default <profile> [--force]
|
|
111
121
|
|
|
112
|
-
Copy
|
|
113
|
-
|
|
122
|
+
Copy ~/.claude/ resources into a profile. Useful only when the profile has
|
|
123
|
+
'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.
|
|
114
125
|
|
|
115
|
-
<src>: default
|
|
116
|
-
|
|
117
|
-
shared = ~/.claude-shared/ (the cc shared pool)
|
|
118
|
-
<dest>: shared | <profile-name>
|
|
126
|
+
<src>: default (= ~/.claude/, Claude Code's own config dir)
|
|
127
|
+
<dest>: <profile-name>
|
|
119
128
|
|
|
120
|
-
|
|
121
|
-
projects
|
|
129
|
+
Resources considered (same 7 as everywhere):
|
|
130
|
+
--projects projects/
|
|
131
|
+
--tools skills/ agents/ commands/
|
|
132
|
+
--config mcp.json settings.json CLAUDE.md
|
|
122
133
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
134
|
+
For each resource:
|
|
135
|
+
- If the destination is shared via symlink/hard-link to the source → skipped
|
|
136
|
+
- Else, copy with skip-on-collision (entries already at the destination win)
|
|
126
137
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
Collision behavior (default): existing entries at the destination are silently
|
|
130
|
-
skipped — the source entry is left alone.
|
|
131
|
-
|
|
132
|
-
--force Overwrite existing entries instead of skipping.
|
|
133
|
-
Warning: existing content at the destination will be cleared.
|
|
138
|
+
--force Overwrite collisions at the destination instead of skipping.
|
|
134
139
|
|
|
135
140
|
Examples:
|
|
136
|
-
cc
|
|
137
|
-
cc
|
|
138
|
-
|
|
139
|
-
|
|
141
|
+
cc copy default work # only acts on unlinked resources
|
|
142
|
+
cc copy default work --force # overwrite collisions
|
|
143
|
+
|
|
144
|
+
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
|
|
147
|
+
will be removed in a future version.
|
|
140
148
|
`,
|
|
141
|
-
link: `cc link <profile> [<profile>...]
|
|
149
|
+
link: `cc link <profile> [<profile>...] [--projects|--tools|--config]
|
|
142
150
|
|
|
143
|
-
|
|
144
|
-
|
|
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.
|
|
145
154
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
155
|
+
No flag link all 7 resources
|
|
156
|
+
--projects projects/
|
|
157
|
+
--tools skills/, agents/, commands/
|
|
158
|
+
--config mcp.json, settings.json, CLAUDE.md
|
|
159
|
+
(flags can be combined: --tools --config)
|
|
149
160
|
|
|
150
|
-
|
|
151
|
-
|
|
161
|
+
Refuses to overwrite private content: if the profile has a non-empty resource
|
|
162
|
+
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
|
|
164
|
+
resources are still linked.
|
|
152
165
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
166
|
+
On Windows, dirs use junctions (no admin/Developer Mode needed). File
|
|
167
|
+
symlinks may require Developer Mode; without it, files fall back to hard
|
|
168
|
+
links so editing-in-place still propagates between profiles.
|
|
169
|
+
|
|
170
|
+
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
|
|
157
175
|
`,
|
|
158
|
-
unlink: `cc unlink <profile>
|
|
176
|
+
unlink: `cc unlink <profile> [--projects|--tools|--config]
|
|
177
|
+
|
|
178
|
+
Break the symlinks and copy current ~/.claude/ content into the profile as
|
|
179
|
+
private resources. Non-destructive: same-name items already private in the
|
|
180
|
+
profile are kept, so re-running unlink is idempotent.
|
|
159
181
|
|
|
160
|
-
|
|
161
|
-
|
|
182
|
+
No flag unlink all 7 resources
|
|
183
|
+
--projects projects/
|
|
184
|
+
--tools skills/, agents/, commands/
|
|
185
|
+
--config mcp.json, settings.json, CLAUDE.md
|
|
186
|
+
|
|
187
|
+
After unlinking, the profile's resource diverges from ~/.claude/. Other
|
|
188
|
+
profiles still see the shared pool.
|
|
189
|
+
|
|
190
|
+
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
|
|
162
194
|
`,
|
|
163
|
-
remove: `cc remove <profile>
|
|
195
|
+
remove: `cc remove <profile> [--force]
|
|
164
196
|
|
|
165
197
|
Delete a profile and everything inside its directory: credentials, sessions,
|
|
166
|
-
|
|
167
|
-
|
|
198
|
+
and any private resources. To confirm, you must type the exact profile name —
|
|
199
|
+
y/n is intentionally not accepted because credential loss is unrecoverable.
|
|
168
200
|
|
|
169
|
-
If
|
|
170
|
-
removed — the shared
|
|
201
|
+
If a profile's resources are symlinks to ~/.claude/, only the symlinks are
|
|
202
|
+
removed — the shared content in ~/.claude/ is preserved for other profiles.
|
|
203
|
+
If any resource is PRIVATE (unlinked), --force is required because its
|
|
204
|
+
content is unique to this profile and will be lost forever.
|
|
171
205
|
`,
|
|
172
206
|
doctor: `cc doctor [--fix [--force]] [--json]
|
|
173
207
|
|
|
174
208
|
Run diagnostic checks across four groups:
|
|
175
209
|
|
|
176
|
-
Environment Node >=18, claude on PATH, claude
|
|
177
|
-
Storage ~/.claude-profiles/ writable, .claude.json parseable,
|
|
178
|
-
|
|
210
|
+
Environment Node >=18, claude on PATH, claude version, no conflicting env vars
|
|
211
|
+
Storage ~/.claude-profiles/ writable, .claude.json parseable, every
|
|
212
|
+
profile's 7 resource links present and valid; legacy
|
|
213
|
+
~/.claude-shared/ folded into ~/.claude/ if still present
|
|
214
|
+
Platform Windows: OneDrive home, WindowsApps stub, stale ~/bin/cc;
|
|
215
|
+
Git Bash: cygpath
|
|
179
216
|
Sanity Test-spawn claude --version through the same code path cc use uses
|
|
180
217
|
|
|
181
218
|
Exit codes: 0 = all ok, 1 = warnings, 2 = errors.
|
|
182
219
|
|
|
183
|
-
--fix Auto-apply safe fixes: create missing dirs, recreate broken
|
|
184
|
-
|
|
220
|
+
--fix Auto-apply safe fixes: create missing dirs, recreate broken
|
|
221
|
+
links, backfill missing resource links
|
|
222
|
+
--fix --force Also apply confirmation-required fixes (e.g. reset corrupt
|
|
223
|
+
.claude.json)
|
|
185
224
|
--json Machine-readable output for CI / scripting
|
|
186
225
|
`,
|
|
187
226
|
help: `cc help [command]
|
|
@@ -199,10 +238,13 @@ function usageError(msg) {
|
|
|
199
238
|
|
|
200
239
|
function printHelp(cmd) {
|
|
201
240
|
if (cmd && Object.prototype.hasOwnProperty.call(HELP_BY_CMD, cmd)) {
|
|
241
|
+
// Per-command help: no banner, just the detail.
|
|
202
242
|
process.stdout.write(HELP_BY_CMD[cmd]);
|
|
203
|
-
|
|
204
|
-
process.stdout.write(HELP);
|
|
243
|
+
return;
|
|
205
244
|
}
|
|
245
|
+
// Top-level help: banner + summary.
|
|
246
|
+
process.stdout.write(renderBanner());
|
|
247
|
+
process.stdout.write(HELP);
|
|
206
248
|
}
|
|
207
249
|
|
|
208
250
|
function prompt(question) {
|
|
@@ -274,6 +316,16 @@ function parseFlagsStrict(args, flagNames, { allowPositional = false } = {}) {
|
|
|
274
316
|
return { flags, positional };
|
|
275
317
|
}
|
|
276
318
|
|
|
319
|
+
// Count how many of the 7 shared resources a profile currently has linked
|
|
320
|
+
// (symlink OR — on Windows file fallback — hard link).
|
|
321
|
+
function countSharedResources(profileDir) {
|
|
322
|
+
let n = 0;
|
|
323
|
+
for (const r of share.SHARED_RESOURCES) {
|
|
324
|
+
if (share.isSharedResource(profileDir, r)) n += 1;
|
|
325
|
+
}
|
|
326
|
+
return n;
|
|
327
|
+
}
|
|
328
|
+
|
|
277
329
|
// Flag name -> .cc-env.json field, for `cc add --custom`.
|
|
278
330
|
const ENDPOINT_VALUE_FLAGS = {
|
|
279
331
|
'--base-url': 'base_url',
|
|
@@ -324,45 +376,40 @@ async function cmdAdd(rest) {
|
|
|
324
376
|
);
|
|
325
377
|
}
|
|
326
378
|
|
|
327
|
-
//
|
|
379
|
+
// Fold any legacy ~/.claude-shared/ pool into ~/.claude/ before any link work.
|
|
380
|
+
runLegacyPoolMigration();
|
|
381
|
+
|
|
328
382
|
if (!custom) {
|
|
329
383
|
const { dir } = profiles.addProfile(name);
|
|
330
384
|
console.log(`Created profile "${name}" at ${dir}.`);
|
|
385
|
+
autoLinkArtifacts(name);
|
|
331
386
|
console.log(`Next: cc use ${name}`);
|
|
332
387
|
return 0;
|
|
333
388
|
}
|
|
334
389
|
|
|
335
|
-
// Custom endpoint profile.
|
|
336
|
-
// existing endpoint profile is edited in place. Either way the directory
|
|
337
|
-
// must exist before runConfigure reads/writes .cc-env.json inside it.
|
|
390
|
+
// Custom endpoint profile.
|
|
338
391
|
const dir = profiles.profileDir(name);
|
|
339
392
|
const existed = fs.existsSync(dir);
|
|
340
|
-
if (!existed)
|
|
393
|
+
if (!existed) {
|
|
394
|
+
profiles.addProfile(name);
|
|
395
|
+
autoLinkArtifacts(name);
|
|
396
|
+
}
|
|
341
397
|
|
|
342
398
|
let result;
|
|
343
399
|
try {
|
|
344
400
|
result = await envLib.runConfigure(name, dir, provided);
|
|
345
401
|
} catch (err) {
|
|
346
|
-
// A freshly-created profile that could not be configured leaves nothing
|
|
347
|
-
// behind — `cc add --custom` is atomic.
|
|
348
402
|
if (!existed) {
|
|
349
403
|
try {
|
|
350
404
|
removeRecursivelyPreservingLinks(dir);
|
|
351
|
-
} catch {
|
|
352
|
-
// best-effort cleanup
|
|
353
|
-
}
|
|
405
|
+
} catch { /* best-effort */ }
|
|
354
406
|
}
|
|
355
407
|
throw err;
|
|
356
408
|
}
|
|
357
409
|
|
|
358
410
|
if (!result.written) {
|
|
359
|
-
// Wizard cancelled.
|
|
360
411
|
if (!existed) {
|
|
361
|
-
try {
|
|
362
|
-
removeRecursivelyPreservingLinks(dir);
|
|
363
|
-
} catch {
|
|
364
|
-
// best-effort cleanup
|
|
365
|
-
}
|
|
412
|
+
try { removeRecursivelyPreservingLinks(dir); } catch { /* best-effort */ }
|
|
366
413
|
console.log(`Cancelled. Profile "${name}" was not created.`);
|
|
367
414
|
} else {
|
|
368
415
|
console.log('Cancelled. No changes written.');
|
|
@@ -374,6 +421,66 @@ async function cmdAdd(rest) {
|
|
|
374
421
|
return result.code;
|
|
375
422
|
}
|
|
376
423
|
|
|
424
|
+
// One-time v0.3 → v0.4 migration. Folds ~/.claude-shared/ into ~/.claude/ and
|
|
425
|
+
// re-points existing profile symlinks. Quiet when there's no legacy pool.
|
|
426
|
+
function runLegacyPoolMigration() {
|
|
427
|
+
let result;
|
|
428
|
+
try {
|
|
429
|
+
result = share.migrateLegacyPool({ log: (line) => console.log(line) });
|
|
430
|
+
} catch (err) {
|
|
431
|
+
process.stderr.write(
|
|
432
|
+
color.stderr.yellow(` warning: legacy pool migration failed: ${err.message}`) + '\n',
|
|
433
|
+
);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (!result.migrated) return;
|
|
437
|
+
const moved = result.moves.length;
|
|
438
|
+
const relinked = result.relinked.length;
|
|
439
|
+
console.log(
|
|
440
|
+
`Migrated legacy ~/.claude-shared/ into ~/.claude/ ` +
|
|
441
|
+
`(${moved} item(s) moved, ${relinked} profile link(s) repointed).`,
|
|
442
|
+
);
|
|
443
|
+
for (const f of result.failures) {
|
|
444
|
+
process.stderr.write(
|
|
445
|
+
color.stderr.yellow(` warning: ${JSON.stringify(f)}`) + '\n',
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// 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.
|
|
453
|
+
function autoLinkArtifacts(name) {
|
|
454
|
+
let results;
|
|
455
|
+
try {
|
|
456
|
+
results = share.shareLink(name);
|
|
457
|
+
} catch (err) {
|
|
458
|
+
process.stderr.write(
|
|
459
|
+
color.stderr.yellow(
|
|
460
|
+
` warning: could not link resources for "${name}": ${err.message}`,
|
|
461
|
+
) + '\n',
|
|
462
|
+
);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const failed = results.filter((r) => r.status === 'failed' || r.status === 'refused');
|
|
466
|
+
const linked = results.length - failed.length;
|
|
467
|
+
if (failed.length === 0) {
|
|
468
|
+
console.log(` Linked ${linked}/${results.length} resources to ~/.claude/.`);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
console.log(` Linked ${linked}/${results.length} resources to ~/.claude/.`);
|
|
472
|
+
for (const r of failed) {
|
|
473
|
+
process.stderr.write(
|
|
474
|
+
color.stderr.yellow(` warning: ${r.resource} not linked — ${r.message}`) + '\n',
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
process.stderr.write(
|
|
478
|
+
color.stderr.yellow(
|
|
479
|
+
` Re-run \`cc link ${name}\` after resolving the above.`,
|
|
480
|
+
) + '\n',
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
377
484
|
async function cmdUse(rest) {
|
|
378
485
|
const [name, ...passthrough] = rest;
|
|
379
486
|
if (!name) throw usageError('Usage: cc use <profile> [...args]');
|
|
@@ -385,10 +492,8 @@ async function cmdUse(rest) {
|
|
|
385
492
|
throw err;
|
|
386
493
|
}
|
|
387
494
|
|
|
388
|
-
// If the profile carries a custom-endpoint config, inject the corresponding
|
|
389
|
-
// env vars so Claude Code routes through the third-party provider.
|
|
390
495
|
let extraEnv = {};
|
|
391
|
-
const envConfig = envLib.readEnvConfig(dir);
|
|
496
|
+
const envConfig = envLib.readEnvConfig(dir);
|
|
392
497
|
if (envConfig) {
|
|
393
498
|
extraEnv = envLib.buildEnvVars(envConfig);
|
|
394
499
|
const host = envConfig.base_url ? envLib.hostOf(envConfig.base_url) : '(unknown)';
|
|
@@ -413,12 +518,11 @@ function cmdListOne(name, { json, reveal }) {
|
|
|
413
518
|
throw e;
|
|
414
519
|
}
|
|
415
520
|
|
|
416
|
-
const total = share.
|
|
417
|
-
const sharedCount =
|
|
418
|
-
isLinkLike(path.join(dir, sub)),
|
|
419
|
-
).length;
|
|
521
|
+
const total = share.SHARED_RESOURCES.length;
|
|
522
|
+
const sharedCount = countSharedResources(dir);
|
|
420
523
|
const loggedIn = profiles.hasCredentials(dir);
|
|
421
524
|
const email = profiles.readEmail(dir);
|
|
525
|
+
const displayEmail = reveal ? email : (email ? envLib.maskEmail(email) : email);
|
|
422
526
|
|
|
423
527
|
let config = null;
|
|
424
528
|
let configError = false;
|
|
@@ -433,14 +537,13 @@ function cmdListOne(name, { json, reveal }) {
|
|
|
433
537
|
name,
|
|
434
538
|
dir,
|
|
435
539
|
loggedIn,
|
|
436
|
-
email,
|
|
540
|
+
email: displayEmail,
|
|
437
541
|
shared: sharedCount,
|
|
438
542
|
endpoint: envLib.endpointSummary(dir),
|
|
439
543
|
};
|
|
440
544
|
if (config) {
|
|
441
545
|
obj.endpointConfig = {
|
|
442
546
|
base_url: config.base_url || null,
|
|
443
|
-
// The token is always masked in JSON output — scripts never need it.
|
|
444
547
|
auth_token: config.auth_token ? envLib.maskToken(config.auth_token) : null,
|
|
445
548
|
model: config.model || null,
|
|
446
549
|
opus: config.opus || config.model || null,
|
|
@@ -456,7 +559,7 @@ function cmdListOne(name, { json, reveal }) {
|
|
|
456
559
|
console.log(`Profile: ${name}`);
|
|
457
560
|
console.log(`Directory: ${dir}`);
|
|
458
561
|
console.log(`LoggedIn: ${loggedIn ? 'true' : 'false'}`);
|
|
459
|
-
console.log(`Email: ${
|
|
562
|
+
console.log(`Email: ${displayEmail || '-'}`);
|
|
460
563
|
console.log(`Shared: ${sharedCount}/${total}`);
|
|
461
564
|
|
|
462
565
|
if (configError) {
|
|
@@ -484,16 +587,18 @@ function cmdListOne(name, { json, reveal }) {
|
|
|
484
587
|
console.log(` Subagent: ${config.subagent || config.model || '-'}`);
|
|
485
588
|
if (reveal) {
|
|
486
589
|
console.log('');
|
|
487
|
-
console.log('WARNING: the full API key
|
|
488
|
-
console.log('into chats, issues, screenshots, or logs.');
|
|
590
|
+
console.log('WARNING: the full API key and email are printed above. Do not paste this');
|
|
591
|
+
console.log('output into chats, issues, screenshots, or logs.');
|
|
489
592
|
}
|
|
490
593
|
return 0;
|
|
491
594
|
}
|
|
492
595
|
|
|
493
596
|
function cmdList(rest) {
|
|
494
|
-
const { flags, positional } = parseFlagsStrict(
|
|
495
|
-
|
|
496
|
-
|
|
597
|
+
const { flags, positional } = parseFlagsStrict(
|
|
598
|
+
rest,
|
|
599
|
+
['--json', '--reveal'],
|
|
600
|
+
{ allowPositional: true },
|
|
601
|
+
);
|
|
497
602
|
const json = flags.has('--json');
|
|
498
603
|
const reveal = flags.has('--reveal');
|
|
499
604
|
const [name, ...extra] = positional;
|
|
@@ -502,28 +607,26 @@ function cmdList(rest) {
|
|
|
502
607
|
if (name) {
|
|
503
608
|
return cmdListOne(name, { json, reveal });
|
|
504
609
|
}
|
|
505
|
-
if (reveal) {
|
|
506
|
-
throw usageError('--reveal
|
|
610
|
+
if (json && reveal) {
|
|
611
|
+
throw usageError('--reveal cannot be combined with --json.');
|
|
507
612
|
}
|
|
508
613
|
|
|
509
614
|
const list = profiles.listProfiles();
|
|
510
|
-
const total = share.
|
|
615
|
+
const total = share.SHARED_RESOURCES.length;
|
|
511
616
|
|
|
512
617
|
const enriched = list.map((p) => {
|
|
513
|
-
const sharedCount =
|
|
514
|
-
isLinkLike(path.join(p.dir, sub))
|
|
515
|
-
).length;
|
|
618
|
+
const sharedCount = countSharedResources(p.dir);
|
|
516
619
|
return {
|
|
517
620
|
name: p.name,
|
|
518
621
|
dir: p.dir,
|
|
519
622
|
loggedIn: p.loggedIn,
|
|
520
|
-
email: p.email,
|
|
623
|
+
email: reveal ? p.email : (p.email ? envLib.maskEmail(p.email) : p.email),
|
|
521
624
|
shared: sharedCount,
|
|
522
625
|
endpoint: envLib.endpointSummary(p.dir),
|
|
523
626
|
};
|
|
524
627
|
});
|
|
525
628
|
|
|
526
|
-
if (
|
|
629
|
+
if (json) {
|
|
527
630
|
process.stdout.write(JSON.stringify(enriched, null, 2) + '\n');
|
|
528
631
|
return 0;
|
|
529
632
|
}
|
|
@@ -548,9 +651,32 @@ function cmdList(rest) {
|
|
|
548
651
|
return 0;
|
|
549
652
|
}
|
|
550
653
|
|
|
654
|
+
// True if the profile has any private (non-shared, non-empty) resource that
|
|
655
|
+
// would be permanently lost on remove.
|
|
656
|
+
function hasPrivateContent(dir) {
|
|
657
|
+
for (const r of share.SHARED_RESOURCES) {
|
|
658
|
+
const p = path.join(dir, r);
|
|
659
|
+
if (!fs.existsSync(p)) continue;
|
|
660
|
+
if (share.isSharedResource(dir, r)) continue;
|
|
661
|
+
if (share.isFileResource(r)) {
|
|
662
|
+
try {
|
|
663
|
+
if (fs.statSync(p).size > 0) return true;
|
|
664
|
+
} catch { /* skip */ }
|
|
665
|
+
} else {
|
|
666
|
+
try {
|
|
667
|
+
if (fs.readdirSync(p).length > 0) return true;
|
|
668
|
+
} catch { /* skip */ }
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
|
|
551
674
|
async function cmdRemove(rest) {
|
|
552
|
-
const [
|
|
553
|
-
|
|
675
|
+
const { flags, positional } = parseFlagsStrict(rest, ['--force'], { allowPositional: true });
|
|
676
|
+
const [name, ...extra] = positional;
|
|
677
|
+
if (!name) throw usageError('Usage: cc remove <profile> [--force]');
|
|
678
|
+
if (extra.length > 0) throw usageError(`Unexpected argument: ${extra[0]}`);
|
|
679
|
+
const force = flags.has('--force');
|
|
554
680
|
profiles.validateProfileName(name);
|
|
555
681
|
const dir = profiles.profileDir(name);
|
|
556
682
|
if (!fs.existsSync(dir)) {
|
|
@@ -558,13 +684,24 @@ async function cmdRemove(rest) {
|
|
|
558
684
|
err.exitCode = 1;
|
|
559
685
|
throw err;
|
|
560
686
|
}
|
|
687
|
+
|
|
688
|
+
if (hasPrivateContent(dir) && !force) {
|
|
689
|
+
const err = new Error(
|
|
690
|
+
`Profile "${name}" has private (unlinked) resources whose content is not ` +
|
|
691
|
+
`in ~/.claude/. Removing the profile will delete that content permanently.\n` +
|
|
692
|
+
`Re-run with --force to confirm: cc remove ${name} --force`,
|
|
693
|
+
);
|
|
694
|
+
err.exitCode = 1;
|
|
695
|
+
throw err;
|
|
696
|
+
}
|
|
697
|
+
|
|
561
698
|
console.log('This will permanently delete:');
|
|
562
699
|
console.log(` ${dir}`);
|
|
563
|
-
console.log('including credentials, sessions,
|
|
564
|
-
for (const sub of share.
|
|
700
|
+
console.log('including credentials, sessions, and any private resources.');
|
|
701
|
+
for (const sub of share.SHARED_RESOURCES) {
|
|
565
702
|
const sub_p = path.join(dir, sub);
|
|
566
|
-
if (
|
|
567
|
-
console.log(` (${sub}
|
|
703
|
+
if (share.isSharedResource(dir, sub)) {
|
|
704
|
+
console.log(` (${sub} is shared with ~/.claude/ — only the link will be removed)`);
|
|
568
705
|
}
|
|
569
706
|
}
|
|
570
707
|
const answer = await prompt(`Type the profile name "${name}" to confirm: `);
|
|
@@ -582,34 +719,60 @@ async function cmdRemove(rest) {
|
|
|
582
719
|
}
|
|
583
720
|
|
|
584
721
|
async function cmdLink(rest) {
|
|
585
|
-
|
|
586
|
-
|
|
722
|
+
const { flags, positional } = parseFlagsStrict(
|
|
723
|
+
rest,
|
|
724
|
+
['--tools', '--projects', '--config'],
|
|
725
|
+
{ allowPositional: true },
|
|
726
|
+
);
|
|
727
|
+
if (positional.length === 0) {
|
|
728
|
+
throw usageError(
|
|
729
|
+
'Usage: cc link <profile> [<profile>...] [--projects|--tools|--config]',
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
const opts = {
|
|
733
|
+
tools: flags.has('--tools'),
|
|
734
|
+
projects: flags.has('--projects'),
|
|
735
|
+
config: flags.has('--config'),
|
|
736
|
+
};
|
|
737
|
+
const multi = positional.length > 1;
|
|
587
738
|
const summary = [];
|
|
588
|
-
for (const name of
|
|
739
|
+
for (const name of positional) {
|
|
589
740
|
if (multi) console.log(`\n[${name}]`);
|
|
590
741
|
let results;
|
|
591
742
|
try {
|
|
592
|
-
results = share.shareLink(name);
|
|
743
|
+
results = share.shareLink(name, opts);
|
|
593
744
|
} catch (err) {
|
|
594
|
-
// Profile-level failure (invalid name, profile missing) — nothing linked.
|
|
595
745
|
process.stderr.write(color.stderr.red(` error: ${err.message}`) + '\n');
|
|
596
|
-
summary.push({ name, linked: 0, total:
|
|
746
|
+
summary.push({ name, linked: 0, total: 0, error: err.message });
|
|
597
747
|
continue;
|
|
598
748
|
}
|
|
599
749
|
for (const r of results) {
|
|
750
|
+
const label = share.isFileResource(r.resource) ? r.resource : `${r.resource}/`;
|
|
600
751
|
if (r.status === 'failed') {
|
|
601
|
-
process.stderr.write(color.stderr.red(` ${
|
|
752
|
+
process.stderr.write(color.stderr.red(` ${label}: failed — ${r.message}`) + '\n');
|
|
753
|
+
} else if (r.status === 'refused') {
|
|
754
|
+
process.stderr.write(color.stderr.yellow(` ${label}: refused — ${r.message}`) + '\n');
|
|
602
755
|
} else if (r.status === 'already-linked') {
|
|
603
|
-
console.log(` ${
|
|
756
|
+
console.log(` ${label}: already linked`);
|
|
757
|
+
} else if (r.relinkedFromLegacy) {
|
|
758
|
+
console.log(` ${label}: relinked (was pointing at legacy ~/.claude-shared/)`);
|
|
604
759
|
} else {
|
|
605
|
-
const note = r.
|
|
606
|
-
console.log(` ${
|
|
760
|
+
const note = r.kind === 'hardlink' ? ' (hard link — enable Windows Developer Mode for a true symlink)' : '';
|
|
761
|
+
console.log(` ${label}: linked${note}`);
|
|
607
762
|
}
|
|
608
763
|
}
|
|
609
|
-
const failed = results.filter((r) => r.status === 'failed');
|
|
764
|
+
const failed = results.filter((r) => r.status === 'failed' || r.status === 'refused');
|
|
610
765
|
const linked = results.length - failed.length;
|
|
611
|
-
console.log(
|
|
612
|
-
|
|
766
|
+
console.log(
|
|
767
|
+
` ${linked}/${results.length} linked` +
|
|
768
|
+
(failed.length ? `, ${failed.length} not linked` : '') + '.',
|
|
769
|
+
);
|
|
770
|
+
summary.push({
|
|
771
|
+
name,
|
|
772
|
+
linked,
|
|
773
|
+
total: results.length,
|
|
774
|
+
failed: failed.map((f) => f.resource),
|
|
775
|
+
});
|
|
613
776
|
}
|
|
614
777
|
|
|
615
778
|
const anyFailure = summary.some((s) => s.error || (s.failed && s.failed.length));
|
|
@@ -619,7 +782,7 @@ async function cmdLink(rest) {
|
|
|
619
782
|
if (s.error) {
|
|
620
783
|
console.log(` ${s.name}: error — ${s.error}`);
|
|
621
784
|
} else if (s.failed && s.failed.length) {
|
|
622
|
-
console.log(` ${s.name}: ${s.linked}/${s.total} linked (
|
|
785
|
+
console.log(` ${s.name}: ${s.linked}/${s.total} linked (not linked: ${s.failed.join(', ')})`);
|
|
623
786
|
} else {
|
|
624
787
|
console.log(` ${s.name}: ${s.linked}/${s.total} linked`);
|
|
625
788
|
}
|
|
@@ -629,18 +792,32 @@ async function cmdLink(rest) {
|
|
|
629
792
|
}
|
|
630
793
|
|
|
631
794
|
async function cmdUnlink(rest) {
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
|
|
795
|
+
const { flags, positional } = parseFlagsStrict(
|
|
796
|
+
rest,
|
|
797
|
+
['--tools', '--projects', '--config'],
|
|
798
|
+
{ allowPositional: true },
|
|
799
|
+
);
|
|
800
|
+
const [name, ...extra] = positional;
|
|
801
|
+
if (!name) {
|
|
802
|
+
throw usageError('Usage: cc unlink <profile> [--projects|--tools|--config]');
|
|
803
|
+
}
|
|
804
|
+
if (extra.length > 0) throw usageError(`Unexpected argument: ${extra[0]}`);
|
|
805
|
+
const opts = {
|
|
806
|
+
tools: flags.has('--tools'),
|
|
807
|
+
projects: flags.has('--projects'),
|
|
808
|
+
config: flags.has('--config'),
|
|
809
|
+
};
|
|
810
|
+
const results = share.shareUnlink(name, opts);
|
|
635
811
|
let anyFailure = false;
|
|
636
812
|
for (const r of results) {
|
|
813
|
+
const label = share.isFileResource(r.resource) ? r.resource : `${r.resource}/`;
|
|
637
814
|
if (r.status === 'failed') {
|
|
638
815
|
anyFailure = true;
|
|
639
|
-
process.stderr.write(color.stderr.red(` ${
|
|
816
|
+
process.stderr.write(color.stderr.red(` ${label}: failed — ${r.message}`) + '\n');
|
|
640
817
|
} else if (r.status === 'not-linked') {
|
|
641
|
-
console.log(` ${
|
|
818
|
+
console.log(` ${label}: not linked, skipping`);
|
|
642
819
|
} else {
|
|
643
|
-
console.log(` ${
|
|
820
|
+
console.log(` ${label}: unlinked, content copied back`);
|
|
644
821
|
}
|
|
645
822
|
}
|
|
646
823
|
return anyFailure ? 1 : 0;
|
|
@@ -656,19 +833,31 @@ async function cmdDoctor(rest) {
|
|
|
656
833
|
return doctor.runDoctor({ fix, force, json, prompt });
|
|
657
834
|
}
|
|
658
835
|
|
|
659
|
-
async function
|
|
836
|
+
async function cmdCopy(rest) {
|
|
660
837
|
const { flags, positional } = parseFlagsStrict(rest, ['--force'], { allowPositional: true });
|
|
661
838
|
const [src, dest, ...extra] = positional;
|
|
662
|
-
if (!src || !dest) throw usageError('Usage: cc
|
|
839
|
+
if (!src || !dest) throw usageError('Usage: cc copy <src> <dest> [--force]');
|
|
663
840
|
if (extra.length > 0) throw usageError(`Unexpected extra arguments: ${extra.join(' ')}`);
|
|
664
841
|
const force = flags.has('--force');
|
|
665
842
|
if (force) {
|
|
666
|
-
process.stdout.write(
|
|
843
|
+
process.stdout.write(
|
|
844
|
+
'Warning: --force is set. Existing items at the destination will be overwritten.\n',
|
|
845
|
+
);
|
|
667
846
|
}
|
|
668
|
-
|
|
847
|
+
copyLib.copy(src, dest, { force });
|
|
669
848
|
return 0;
|
|
670
849
|
}
|
|
671
850
|
|
|
851
|
+
async function cmdMigrate(rest) {
|
|
852
|
+
process.stderr.write(
|
|
853
|
+
color.stderr.yellow(
|
|
854
|
+
"[cchost] 'cc migrate' is deprecated — use 'cc copy' instead. " +
|
|
855
|
+
'This alias will be removed in a future version.',
|
|
856
|
+
) + '\n',
|
|
857
|
+
);
|
|
858
|
+
return cmdCopy(rest);
|
|
859
|
+
}
|
|
860
|
+
|
|
672
861
|
async function run(argv) {
|
|
673
862
|
const [action, ...rest] = argv;
|
|
674
863
|
if (!action || action === '--help' || action === '-h') {
|
|
@@ -685,7 +874,6 @@ async function run(argv) {
|
|
|
685
874
|
return 0;
|
|
686
875
|
}
|
|
687
876
|
if (action === 'env') {
|
|
688
|
-
// `cc env` was removed in v0.3.1 — point upgraders at the new commands.
|
|
689
877
|
process.stderr.write(
|
|
690
878
|
color.stderr.red(
|
|
691
879
|
"'cc env' was removed in v0.3.1. Configure a custom endpoint with " +
|
|
@@ -701,6 +889,8 @@ async function run(argv) {
|
|
|
701
889
|
return await cmdUse(rest);
|
|
702
890
|
case 'list':
|
|
703
891
|
return cmdList(rest);
|
|
892
|
+
case 'copy':
|
|
893
|
+
return await cmdCopy(rest);
|
|
704
894
|
case 'migrate':
|
|
705
895
|
return await cmdMigrate(rest);
|
|
706
896
|
case 'link':
|