cchost 0.4.3 → 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/CHANGELOG.md +78 -0
- package/README.md +103 -149
- package/lib/banner.js +3 -1
- package/lib/cli.js +220 -98
- package/lib/color.js +3 -1
- package/lib/copy.js +31 -29
- package/lib/doctor.js +201 -68
- package/lib/env.js +65 -9
- package/lib/profiles.js +47 -14
- package/lib/share.js +63 -6
- package/lib/spawn.js +2 -1
- package/package.json +8 -4
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 = `
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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: `
|
|
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
|
-
|
|
57
|
+
cchost add <profile>
|
|
51
58
|
Create a regular profile. It logs in with your Anthropic
|
|
52
|
-
subscription (OAuth) on the first '
|
|
59
|
+
subscription (OAuth) on the first 'cchost use'.
|
|
53
60
|
|
|
54
|
-
|
|
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
|
-
|
|
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 '
|
|
66
|
-
its configuration. View any profile with '
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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: `
|
|
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 '
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
97
|
+
cchost use work
|
|
98
|
+
cchost use work --resume
|
|
99
|
+
cchost use personal -p "summarize this codebase"
|
|
93
100
|
`,
|
|
94
|
-
list: `
|
|
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
|
|
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
|
|
109
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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: `
|
|
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 '
|
|
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
|
|
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
|
-
|
|
142
|
-
|
|
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 '
|
|
146
|
-
Note: '
|
|
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
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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 '
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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: `
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
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: `
|
|
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
|
|
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: `
|
|
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
|
|
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 `
|
|
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:
|
|
435
|
+
throw usageError('Usage: cchost add <profile> [--custom [--base-url=URL --token=TOKEN ...]]');
|
|
368
436
|
}
|
|
369
|
-
if (extra.length > 0) throw usageError(`
|
|
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
|
-
`
|
|
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:
|
|
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:
|
|
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
|
|
452
|
-
// user can re-run `
|
|
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(
|
|
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 \`
|
|
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:
|
|
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:
|
|
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
|
-
// `
|
|
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:
|
|
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
|
|
523
|
-
const
|
|
524
|
-
const
|
|
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(
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
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(
|
|
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:
|
|
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(
|
|
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:
|
|
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
|
-
|
|
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] '
|
|
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
|
-
"'
|
|
880
|
-
"'
|
|
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;
|