cchost 0.3.0 → 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 +147 -0
- package/README.md +123 -63
- package/lib/banner.js +29 -0
- package/lib/cli.js +545 -207
- package/lib/copy.js +242 -0
- package/lib/doctor.js +154 -22
- package/lib/env.js +40 -45
- 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,46 +6,83 @@ 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>
|
|
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 [--json]
|
|
21
|
-
cc
|
|
22
|
-
cc
|
|
23
|
-
cc
|
|
24
|
-
cc
|
|
25
|
-
cc remove <profile> Delete a profile and all its data (typed confirmation)
|
|
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
26
|
cc doctor [--fix [--force]] [--json] Diagnose environment, storage, and platform issues
|
|
27
27
|
cc help [command] Show usage (optionally for a single command)
|
|
28
28
|
cc --version Print the installed cchost version
|
|
29
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
|
+
|
|
30
38
|
Profiles live in ~/.claude-profiles/<profile>/ and become CLAUDE_CONFIG_DIR
|
|
31
39
|
for that launch. Args after the profile name are forwarded verbatim to claude.
|
|
32
40
|
`;
|
|
33
41
|
|
|
34
42
|
const HELP_BY_CMD = {
|
|
35
|
-
add: `cc add <profile>
|
|
43
|
+
add: `cc add <profile> [--custom [--base-url=URL --token=TOKEN ...]]
|
|
44
|
+
|
|
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.
|
|
49
|
+
|
|
50
|
+
cc add <profile>
|
|
51
|
+
Create a regular profile. It logs in with your Anthropic
|
|
52
|
+
subscription (OAuth) on the first 'cc use'.
|
|
53
|
+
|
|
54
|
+
cc add <profile> --custom
|
|
55
|
+
Create a profile routed through a custom, Anthropic-compatible API
|
|
56
|
+
endpoint — Moonshot/Kimi, OpenRouter, a self-hosted vLLM, a corporate
|
|
57
|
+
proxy, and so on. Opens an interactive wizard; the API token is never
|
|
58
|
+
echoed to the screen.
|
|
59
|
+
|
|
60
|
+
cc add <profile> --custom --base-url=URL --token=TOKEN
|
|
61
|
+
[--model=NAME] [--opus=NAME] [--sonnet=NAME]
|
|
62
|
+
[--haiku=NAME] [--subagent=NAME]
|
|
63
|
+
Same, non-interactive. --base-url and --token are required.
|
|
64
|
+
|
|
65
|
+
Re-running 'cc add <profile> --custom' on an existing endpoint profile EDITS
|
|
66
|
+
its configuration. View any profile with 'cc list <profile>'.
|
|
36
67
|
|
|
37
|
-
|
|
68
|
+
Billing: a custom-endpoint profile bills through the third-party provider,
|
|
69
|
+
NOT your Anthropic subscription. A profile is born subscription or endpoint
|
|
70
|
+
and stays that way.
|
|
71
|
+
|
|
72
|
+
The endpoint config lives at ~/.claude-profiles/<profile>/.cc-env.json with
|
|
73
|
+
user-only permissions. It is never linked and never copied.
|
|
38
74
|
|
|
39
75
|
Examples:
|
|
40
76
|
cc add work
|
|
41
|
-
cc add
|
|
77
|
+
cc add kimi --custom
|
|
78
|
+
cc add kimi --custom --base-url=https://api.moonshot.ai/anthropic --token=sk-... --model=kimi-k2.5
|
|
42
79
|
`,
|
|
43
80
|
use: `cc use <profile> [...args]
|
|
44
81
|
|
|
45
82
|
Launch Claude under a named profile. The profile must already exist (create
|
|
46
83
|
it with 'cc add <profile>'). Its directory under ~/.claude-profiles/<profile>/
|
|
47
|
-
becomes CLAUDE_CONFIG_DIR for the spawned claude — credentials
|
|
48
|
-
|
|
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.
|
|
49
86
|
|
|
50
87
|
All args after the profile name are forwarded verbatim to claude.
|
|
51
88
|
|
|
@@ -54,133 +91,136 @@ Examples:
|
|
|
54
91
|
cc use work --resume
|
|
55
92
|
cc use personal -p "summarize this codebase"
|
|
56
93
|
`,
|
|
57
|
-
list: `cc list [--json]
|
|
94
|
+
list: `cc list [<profile>] [--json] [--reveal]
|
|
95
|
+
|
|
96
|
+
Without a profile name: list every profile as a table.
|
|
58
97
|
|
|
59
98
|
Columns:
|
|
60
99
|
Profile - profile name
|
|
61
100
|
LoggedIn - true if .credentials.json exists
|
|
62
|
-
Email - account email from .claude.json
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
Endpoint - custom-endpoint host from .cc-env.json, or "subscription" if
|
|
67
|
-
profile uses Anthropic's subscription OAuth
|
|
68
|
-
|
|
69
|
-
With
|
|
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
|
|
107
|
+
|
|
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;
|
|
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)
|
|
70
119
|
`,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
Configure a custom, Anthropic-compatible API endpoint for a profile. This
|
|
74
|
-
routes the profile through a third-party provider (Moonshot/Kimi, OpenRouter,
|
|
75
|
-
vLLM, a corporate proxy, ...) instead of Anthropic's subscription OAuth.
|
|
120
|
+
copy: `cc copy default <profile> [--force]
|
|
76
121
|
|
|
77
|
-
|
|
78
|
-
|
|
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.
|
|
79
125
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
Interactive wizard. Prompts for every field; the token is not echoed.
|
|
83
|
-
On an existing endpoint profile, the wizard runs in edit mode and
|
|
84
|
-
prefills current values.
|
|
126
|
+
<src>: default (= ~/.claude/, Claude Code's own config dir)
|
|
127
|
+
<dest>: <profile-name>
|
|
85
128
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
when editing, only the flags you pass are changed.
|
|
129
|
+
Resources considered (same 7 as everywhere):
|
|
130
|
+
--projects projects/
|
|
131
|
+
--tools skills/ agents/ commands/
|
|
132
|
+
--config mcp.json settings.json CLAUDE.md
|
|
91
133
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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)
|
|
95
137
|
|
|
96
|
-
|
|
97
|
-
to run on a profile that already has OAuth credentials. To stop using an
|
|
98
|
-
endpoint, run 'cc remove <profile>' and recreate the profile.
|
|
99
|
-
|
|
100
|
-
The config lives at ~/.claude-profiles/<profile>/.cc-env.json with user-only
|
|
101
|
-
permissions. It is never migrated and never linked.
|
|
138
|
+
--force Overwrite collisions at the destination instead of skipping.
|
|
102
139
|
|
|
103
140
|
Examples:
|
|
104
|
-
cc
|
|
105
|
-
cc
|
|
106
|
-
cc env kimi show
|
|
107
|
-
cc use kimi
|
|
108
|
-
`,
|
|
109
|
-
migrate: `cc migrate <src> <dest> [--force]
|
|
110
|
-
|
|
111
|
-
Copy artifact directories from a source to a destination. This is a COPY — the
|
|
112
|
-
source is left untouched. sessions/ is never migrated (transient per-process state).
|
|
141
|
+
cc copy default work # only acts on unlinked resources
|
|
142
|
+
cc copy default work --force # overwrite collisions
|
|
113
143
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
projects/, skills/, agents/, commands/
|
|
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.
|
|
148
|
+
`,
|
|
149
|
+
link: `cc link <profile> [<profile>...] [--projects|--tools|--config]
|
|
121
150
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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.
|
|
125
154
|
|
|
126
|
-
|
|
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)
|
|
127
160
|
|
|
128
|
-
|
|
129
|
-
|
|
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.
|
|
130
165
|
|
|
131
|
-
|
|
132
|
-
|
|
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.
|
|
133
169
|
|
|
134
170
|
Examples:
|
|
135
|
-
cc
|
|
136
|
-
cc
|
|
137
|
-
cc
|
|
138
|
-
cc
|
|
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
|
|
139
175
|
`,
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
Replace each profile's artifact directories with links to ~/.claude-shared/:
|
|
143
|
-
projects/, skills/, agents/, commands/
|
|
176
|
+
unlink: `cc unlink <profile> [--projects|--tools|--config]
|
|
144
177
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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.
|
|
148
181
|
|
|
149
|
-
|
|
150
|
-
|
|
182
|
+
No flag unlink all 7 resources
|
|
183
|
+
--projects projects/
|
|
184
|
+
--tools skills/, agents/, commands/
|
|
185
|
+
--config mcp.json, settings.json, CLAUDE.md
|
|
151
186
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
three, and one profile failing does not stop the rest of the batch. A
|
|
155
|
-
per-directory summary is printed; exit code is 1 if anything failed.
|
|
156
|
-
`,
|
|
157
|
-
unlink: `cc unlink <profile>
|
|
187
|
+
After unlinking, the profile's resource diverges from ~/.claude/. Other
|
|
188
|
+
profiles still see the shared pool.
|
|
158
189
|
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
161
194
|
`,
|
|
162
|
-
remove: `cc remove <profile>
|
|
195
|
+
remove: `cc remove <profile> [--force]
|
|
163
196
|
|
|
164
197
|
Delete a profile and everything inside its directory: credentials, sessions,
|
|
165
|
-
|
|
166
|
-
|
|
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.
|
|
167
200
|
|
|
168
|
-
If
|
|
169
|
-
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.
|
|
170
205
|
`,
|
|
171
206
|
doctor: `cc doctor [--fix [--force]] [--json]
|
|
172
207
|
|
|
173
208
|
Run diagnostic checks across four groups:
|
|
174
209
|
|
|
175
|
-
Environment Node >=18, claude on PATH, claude
|
|
176
|
-
Storage ~/.claude-profiles/ writable, .claude.json parseable,
|
|
177
|
-
|
|
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
|
|
178
216
|
Sanity Test-spawn claude --version through the same code path cc use uses
|
|
179
217
|
|
|
180
218
|
Exit codes: 0 = all ok, 1 = warnings, 2 = errors.
|
|
181
219
|
|
|
182
|
-
--fix Auto-apply safe fixes: create missing dirs, recreate broken
|
|
183
|
-
|
|
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)
|
|
184
224
|
--json Machine-readable output for CI / scripting
|
|
185
225
|
`,
|
|
186
226
|
help: `cc help [command]
|
|
@@ -198,10 +238,13 @@ function usageError(msg) {
|
|
|
198
238
|
|
|
199
239
|
function printHelp(cmd) {
|
|
200
240
|
if (cmd && Object.prototype.hasOwnProperty.call(HELP_BY_CMD, cmd)) {
|
|
241
|
+
// Per-command help: no banner, just the detail.
|
|
201
242
|
process.stdout.write(HELP_BY_CMD[cmd]);
|
|
202
|
-
|
|
203
|
-
process.stdout.write(HELP);
|
|
243
|
+
return;
|
|
204
244
|
}
|
|
245
|
+
// Top-level help: banner + summary.
|
|
246
|
+
process.stdout.write(renderBanner());
|
|
247
|
+
process.stdout.write(HELP);
|
|
205
248
|
}
|
|
206
249
|
|
|
207
250
|
function prompt(question) {
|
|
@@ -273,14 +316,169 @@ function parseFlagsStrict(args, flagNames, { allowPositional = false } = {}) {
|
|
|
273
316
|
return { flags, positional };
|
|
274
317
|
}
|
|
275
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
|
+
|
|
329
|
+
// Flag name -> .cc-env.json field, for `cc add --custom`.
|
|
330
|
+
const ENDPOINT_VALUE_FLAGS = {
|
|
331
|
+
'--base-url': 'base_url',
|
|
332
|
+
'--token': 'auth_token',
|
|
333
|
+
'--model': 'model',
|
|
334
|
+
'--opus': 'opus',
|
|
335
|
+
'--sonnet': 'sonnet',
|
|
336
|
+
'--haiku': 'haiku',
|
|
337
|
+
'--subagent': 'subagent',
|
|
338
|
+
};
|
|
339
|
+
|
|
276
340
|
async function cmdAdd(rest) {
|
|
277
|
-
const
|
|
278
|
-
|
|
341
|
+
const positional = [];
|
|
342
|
+
const provided = {};
|
|
343
|
+
let custom = false;
|
|
344
|
+
|
|
345
|
+
for (const a of rest) {
|
|
346
|
+
if (a.startsWith('-')) {
|
|
347
|
+
const eq = a.indexOf('=');
|
|
348
|
+
const flagName = eq === -1 ? a : a.slice(0, eq);
|
|
349
|
+
if (flagName === '--custom') {
|
|
350
|
+
custom = true;
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
if (Object.prototype.hasOwnProperty.call(ENDPOINT_VALUE_FLAGS, flagName)) {
|
|
354
|
+
if (eq === -1) {
|
|
355
|
+
throw usageError(`Flag ${flagName} requires a value: ${flagName}=VALUE`);
|
|
356
|
+
}
|
|
357
|
+
provided[ENDPOINT_VALUE_FLAGS[flagName]] = a.slice(eq + 1);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
throw usageError(`Unknown flag: ${flagName}`);
|
|
361
|
+
}
|
|
362
|
+
positional.push(a);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const [name, ...extra] = positional;
|
|
366
|
+
if (!name) {
|
|
367
|
+
throw usageError('Usage: cc add <profile> [--custom [--base-url=URL --token=TOKEN ...]]');
|
|
368
|
+
}
|
|
279
369
|
if (extra.length > 0) throw usageError(`cc add takes no extra arguments: ${extra[0]}`);
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
370
|
+
profiles.validateProfileName(name);
|
|
371
|
+
|
|
372
|
+
if (Object.keys(provided).length > 0 && !custom) {
|
|
373
|
+
throw usageError(
|
|
374
|
+
'Endpoint flags (--base-url, --token, --model, ...) require --custom: ' +
|
|
375
|
+
`cc add ${name} --custom ...`,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Fold any legacy ~/.claude-shared/ pool into ~/.claude/ before any link work.
|
|
380
|
+
runLegacyPoolMigration();
|
|
381
|
+
|
|
382
|
+
if (!custom) {
|
|
383
|
+
const { dir } = profiles.addProfile(name);
|
|
384
|
+
console.log(`Created profile "${name}" at ${dir}.`);
|
|
385
|
+
autoLinkArtifacts(name);
|
|
386
|
+
console.log(`Next: cc use ${name}`);
|
|
387
|
+
return 0;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Custom endpoint profile.
|
|
391
|
+
const dir = profiles.profileDir(name);
|
|
392
|
+
const existed = fs.existsSync(dir);
|
|
393
|
+
if (!existed) {
|
|
394
|
+
profiles.addProfile(name);
|
|
395
|
+
autoLinkArtifacts(name);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
let result;
|
|
399
|
+
try {
|
|
400
|
+
result = await envLib.runConfigure(name, dir, provided);
|
|
401
|
+
} catch (err) {
|
|
402
|
+
if (!existed) {
|
|
403
|
+
try {
|
|
404
|
+
removeRecursivelyPreservingLinks(dir);
|
|
405
|
+
} catch { /* best-effort */ }
|
|
406
|
+
}
|
|
407
|
+
throw err;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (!result.written) {
|
|
411
|
+
if (!existed) {
|
|
412
|
+
try { removeRecursivelyPreservingLinks(dir); } catch { /* best-effort */ }
|
|
413
|
+
console.log(`Cancelled. Profile "${name}" was not created.`);
|
|
414
|
+
} else {
|
|
415
|
+
console.log('Cancelled. No changes written.');
|
|
416
|
+
}
|
|
417
|
+
return result.code;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (!existed) console.log(`Next: cc use ${name}`);
|
|
421
|
+
return result.code;
|
|
422
|
+
}
|
|
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
|
+
);
|
|
284
482
|
}
|
|
285
483
|
|
|
286
484
|
async function cmdUse(rest) {
|
|
@@ -294,10 +492,8 @@ async function cmdUse(rest) {
|
|
|
294
492
|
throw err;
|
|
295
493
|
}
|
|
296
494
|
|
|
297
|
-
// If the profile carries a custom-endpoint config, inject the corresponding
|
|
298
|
-
// env vars so Claude Code routes through the third-party provider.
|
|
299
495
|
let extraEnv = {};
|
|
300
|
-
const envConfig = envLib.readEnvConfig(dir);
|
|
496
|
+
const envConfig = envLib.readEnvConfig(dir);
|
|
301
497
|
if (envConfig) {
|
|
302
498
|
extraEnv = envLib.buildEnvVars(envConfig);
|
|
303
499
|
const host = envConfig.base_url ? envLib.hostOf(envConfig.base_url) : '(unknown)';
|
|
@@ -308,26 +504,129 @@ async function cmdUse(rest) {
|
|
|
308
504
|
return spawnClaude({ args: passthrough, profileDir: dir, extraEnv });
|
|
309
505
|
}
|
|
310
506
|
|
|
507
|
+
// `cc list <profile>` — detail for a single profile, including its custom
|
|
508
|
+
// endpoint configuration if it has one.
|
|
509
|
+
function cmdListOne(name, { json, reveal }) {
|
|
510
|
+
if (json && reveal) {
|
|
511
|
+
throw usageError('--reveal cannot be combined with --json.');
|
|
512
|
+
}
|
|
513
|
+
profiles.validateProfileName(name);
|
|
514
|
+
const dir = profiles.profileDir(name);
|
|
515
|
+
if (!fs.existsSync(dir)) {
|
|
516
|
+
const e = new Error(`Profile "${name}" does not exist. Create it with: cc add ${name}`);
|
|
517
|
+
e.exitCode = 1;
|
|
518
|
+
throw e;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const total = share.SHARED_RESOURCES.length;
|
|
522
|
+
const sharedCount = countSharedResources(dir);
|
|
523
|
+
const loggedIn = profiles.hasCredentials(dir);
|
|
524
|
+
const email = profiles.readEmail(dir);
|
|
525
|
+
const displayEmail = reveal ? email : (email ? envLib.maskEmail(email) : email);
|
|
526
|
+
|
|
527
|
+
let config = null;
|
|
528
|
+
let configError = false;
|
|
529
|
+
try {
|
|
530
|
+
config = envLib.readEnvConfig(dir);
|
|
531
|
+
} catch {
|
|
532
|
+
configError = true;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (json) {
|
|
536
|
+
const obj = {
|
|
537
|
+
name,
|
|
538
|
+
dir,
|
|
539
|
+
loggedIn,
|
|
540
|
+
email: displayEmail,
|
|
541
|
+
shared: sharedCount,
|
|
542
|
+
endpoint: envLib.endpointSummary(dir),
|
|
543
|
+
};
|
|
544
|
+
if (config) {
|
|
545
|
+
obj.endpointConfig = {
|
|
546
|
+
base_url: config.base_url || null,
|
|
547
|
+
auth_token: config.auth_token ? envLib.maskToken(config.auth_token) : null,
|
|
548
|
+
model: config.model || null,
|
|
549
|
+
opus: config.opus || config.model || null,
|
|
550
|
+
sonnet: config.sonnet || config.model || null,
|
|
551
|
+
haiku: config.haiku || config.model || null,
|
|
552
|
+
subagent: config.subagent || config.model || null,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
process.stdout.write(JSON.stringify(obj, null, 2) + '\n');
|
|
556
|
+
return 0;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
console.log(`Profile: ${name}`);
|
|
560
|
+
console.log(`Directory: ${dir}`);
|
|
561
|
+
console.log(`LoggedIn: ${loggedIn ? 'true' : 'false'}`);
|
|
562
|
+
console.log(`Email: ${displayEmail || '-'}`);
|
|
563
|
+
console.log(`Shared: ${sharedCount}/${total}`);
|
|
564
|
+
|
|
565
|
+
if (configError) {
|
|
566
|
+
console.log('Endpoint: (invalid config — .cc-env.json is malformed)');
|
|
567
|
+
return 0;
|
|
568
|
+
}
|
|
569
|
+
if (!config) {
|
|
570
|
+
console.log('Endpoint: subscription');
|
|
571
|
+
return 0;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
console.log(
|
|
575
|
+
`Endpoint: ${config.base_url ? envLib.hostOf(config.base_url) : '(invalid config)'}`,
|
|
576
|
+
);
|
|
577
|
+
console.log(` Base URL: ${config.base_url || '-'}`);
|
|
578
|
+
if (reveal) {
|
|
579
|
+
console.log(` Token: ${config.auth_token || '-'}`);
|
|
580
|
+
} else {
|
|
581
|
+
console.log(` Token: ${config.auth_token ? envLib.maskToken(config.auth_token) : '-'}`);
|
|
582
|
+
}
|
|
583
|
+
console.log(` Model: ${config.model || '-'}`);
|
|
584
|
+
console.log(` Opus: ${config.opus || config.model || '-'}`);
|
|
585
|
+
console.log(` Sonnet: ${config.sonnet || config.model || '-'}`);
|
|
586
|
+
console.log(` Haiku: ${config.haiku || config.model || '-'}`);
|
|
587
|
+
console.log(` Subagent: ${config.subagent || config.model || '-'}`);
|
|
588
|
+
if (reveal) {
|
|
589
|
+
console.log('');
|
|
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.');
|
|
592
|
+
}
|
|
593
|
+
return 0;
|
|
594
|
+
}
|
|
595
|
+
|
|
311
596
|
function cmdList(rest) {
|
|
312
|
-
const { flags } = parseFlagsStrict(
|
|
597
|
+
const { flags, positional } = parseFlagsStrict(
|
|
598
|
+
rest,
|
|
599
|
+
['--json', '--reveal'],
|
|
600
|
+
{ allowPositional: true },
|
|
601
|
+
);
|
|
602
|
+
const json = flags.has('--json');
|
|
603
|
+
const reveal = flags.has('--reveal');
|
|
604
|
+
const [name, ...extra] = positional;
|
|
605
|
+
if (extra.length > 0) throw usageError(`Unexpected argument: ${extra[0]}`);
|
|
606
|
+
|
|
607
|
+
if (name) {
|
|
608
|
+
return cmdListOne(name, { json, reveal });
|
|
609
|
+
}
|
|
610
|
+
if (json && reveal) {
|
|
611
|
+
throw usageError('--reveal cannot be combined with --json.');
|
|
612
|
+
}
|
|
613
|
+
|
|
313
614
|
const list = profiles.listProfiles();
|
|
314
|
-
const total = share.
|
|
615
|
+
const total = share.SHARED_RESOURCES.length;
|
|
315
616
|
|
|
316
617
|
const enriched = list.map((p) => {
|
|
317
|
-
const sharedCount =
|
|
318
|
-
isLinkLike(path.join(p.dir, sub))
|
|
319
|
-
).length;
|
|
618
|
+
const sharedCount = countSharedResources(p.dir);
|
|
320
619
|
return {
|
|
321
620
|
name: p.name,
|
|
322
621
|
dir: p.dir,
|
|
323
622
|
loggedIn: p.loggedIn,
|
|
324
|
-
email: p.email,
|
|
623
|
+
email: reveal ? p.email : (p.email ? envLib.maskEmail(p.email) : p.email),
|
|
325
624
|
shared: sharedCount,
|
|
326
625
|
endpoint: envLib.endpointSummary(p.dir),
|
|
327
626
|
};
|
|
328
627
|
});
|
|
329
628
|
|
|
330
|
-
if (
|
|
629
|
+
if (json) {
|
|
331
630
|
process.stdout.write(JSON.stringify(enriched, null, 2) + '\n');
|
|
332
631
|
return 0;
|
|
333
632
|
}
|
|
@@ -352,9 +651,32 @@ function cmdList(rest) {
|
|
|
352
651
|
return 0;
|
|
353
652
|
}
|
|
354
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
|
+
|
|
355
674
|
async function cmdRemove(rest) {
|
|
356
|
-
const [
|
|
357
|
-
|
|
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');
|
|
358
680
|
profiles.validateProfileName(name);
|
|
359
681
|
const dir = profiles.profileDir(name);
|
|
360
682
|
if (!fs.existsSync(dir)) {
|
|
@@ -362,13 +684,24 @@ async function cmdRemove(rest) {
|
|
|
362
684
|
err.exitCode = 1;
|
|
363
685
|
throw err;
|
|
364
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
|
+
|
|
365
698
|
console.log('This will permanently delete:');
|
|
366
699
|
console.log(` ${dir}`);
|
|
367
|
-
console.log('including credentials, sessions,
|
|
368
|
-
for (const sub of share.
|
|
700
|
+
console.log('including credentials, sessions, and any private resources.');
|
|
701
|
+
for (const sub of share.SHARED_RESOURCES) {
|
|
369
702
|
const sub_p = path.join(dir, sub);
|
|
370
|
-
if (
|
|
371
|
-
console.log(` (${sub}
|
|
703
|
+
if (share.isSharedResource(dir, sub)) {
|
|
704
|
+
console.log(` (${sub} is shared with ~/.claude/ — only the link will be removed)`);
|
|
372
705
|
}
|
|
373
706
|
}
|
|
374
707
|
const answer = await prompt(`Type the profile name "${name}" to confirm: `);
|
|
@@ -386,34 +719,60 @@ async function cmdRemove(rest) {
|
|
|
386
719
|
}
|
|
387
720
|
|
|
388
721
|
async function cmdLink(rest) {
|
|
389
|
-
|
|
390
|
-
|
|
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;
|
|
391
738
|
const summary = [];
|
|
392
|
-
for (const name of
|
|
739
|
+
for (const name of positional) {
|
|
393
740
|
if (multi) console.log(`\n[${name}]`);
|
|
394
741
|
let results;
|
|
395
742
|
try {
|
|
396
|
-
results = share.shareLink(name);
|
|
743
|
+
results = share.shareLink(name, opts);
|
|
397
744
|
} catch (err) {
|
|
398
|
-
// Profile-level failure (invalid name, profile missing) — nothing linked.
|
|
399
745
|
process.stderr.write(color.stderr.red(` error: ${err.message}`) + '\n');
|
|
400
|
-
summary.push({ name, linked: 0, total:
|
|
746
|
+
summary.push({ name, linked: 0, total: 0, error: err.message });
|
|
401
747
|
continue;
|
|
402
748
|
}
|
|
403
749
|
for (const r of results) {
|
|
750
|
+
const label = share.isFileResource(r.resource) ? r.resource : `${r.resource}/`;
|
|
404
751
|
if (r.status === 'failed') {
|
|
405
|
-
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');
|
|
406
755
|
} else if (r.status === 'already-linked') {
|
|
407
|
-
console.log(` ${
|
|
756
|
+
console.log(` ${label}: already linked`);
|
|
757
|
+
} else if (r.relinkedFromLegacy) {
|
|
758
|
+
console.log(` ${label}: relinked (was pointing at legacy ~/.claude-shared/)`);
|
|
408
759
|
} else {
|
|
409
|
-
const note = r.
|
|
410
|
-
console.log(` ${
|
|
760
|
+
const note = r.kind === 'hardlink' ? ' (hard link — enable Windows Developer Mode for a true symlink)' : '';
|
|
761
|
+
console.log(` ${label}: linked${note}`);
|
|
411
762
|
}
|
|
412
763
|
}
|
|
413
|
-
const failed = results.filter((r) => r.status === 'failed');
|
|
764
|
+
const failed = results.filter((r) => r.status === 'failed' || r.status === 'refused');
|
|
414
765
|
const linked = results.length - failed.length;
|
|
415
|
-
console.log(
|
|
416
|
-
|
|
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
|
+
});
|
|
417
776
|
}
|
|
418
777
|
|
|
419
778
|
const anyFailure = summary.some((s) => s.error || (s.failed && s.failed.length));
|
|
@@ -423,7 +782,7 @@ async function cmdLink(rest) {
|
|
|
423
782
|
if (s.error) {
|
|
424
783
|
console.log(` ${s.name}: error — ${s.error}`);
|
|
425
784
|
} else if (s.failed && s.failed.length) {
|
|
426
|
-
console.log(` ${s.name}: ${s.linked}/${s.total} linked (
|
|
785
|
+
console.log(` ${s.name}: ${s.linked}/${s.total} linked (not linked: ${s.failed.join(', ')})`);
|
|
427
786
|
} else {
|
|
428
787
|
console.log(` ${s.name}: ${s.linked}/${s.total} linked`);
|
|
429
788
|
}
|
|
@@ -433,18 +792,32 @@ async function cmdLink(rest) {
|
|
|
433
792
|
}
|
|
434
793
|
|
|
435
794
|
async function cmdUnlink(rest) {
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
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);
|
|
439
811
|
let anyFailure = false;
|
|
440
812
|
for (const r of results) {
|
|
813
|
+
const label = share.isFileResource(r.resource) ? r.resource : `${r.resource}/`;
|
|
441
814
|
if (r.status === 'failed') {
|
|
442
815
|
anyFailure = true;
|
|
443
|
-
process.stderr.write(color.stderr.red(` ${
|
|
816
|
+
process.stderr.write(color.stderr.red(` ${label}: failed — ${r.message}`) + '\n');
|
|
444
817
|
} else if (r.status === 'not-linked') {
|
|
445
|
-
console.log(` ${
|
|
818
|
+
console.log(` ${label}: not linked, skipping`);
|
|
446
819
|
} else {
|
|
447
|
-
console.log(` ${
|
|
820
|
+
console.log(` ${label}: unlinked, content copied back`);
|
|
448
821
|
}
|
|
449
822
|
}
|
|
450
823
|
return anyFailure ? 1 : 0;
|
|
@@ -460,73 +833,29 @@ async function cmdDoctor(rest) {
|
|
|
460
833
|
return doctor.runDoctor({ fix, force, json, prompt });
|
|
461
834
|
}
|
|
462
835
|
|
|
463
|
-
async function
|
|
836
|
+
async function cmdCopy(rest) {
|
|
464
837
|
const { flags, positional } = parseFlagsStrict(rest, ['--force'], { allowPositional: true });
|
|
465
838
|
const [src, dest, ...extra] = positional;
|
|
466
|
-
if (!src || !dest) throw usageError('Usage: cc
|
|
839
|
+
if (!src || !dest) throw usageError('Usage: cc copy <src> <dest> [--force]');
|
|
467
840
|
if (extra.length > 0) throw usageError(`Unexpected extra arguments: ${extra.join(' ')}`);
|
|
468
841
|
const force = flags.has('--force');
|
|
469
842
|
if (force) {
|
|
470
|
-
process.stdout.write(
|
|
843
|
+
process.stdout.write(
|
|
844
|
+
'Warning: --force is set. Existing items at the destination will be overwritten.\n',
|
|
845
|
+
);
|
|
471
846
|
}
|
|
472
|
-
|
|
847
|
+
copyLib.copy(src, dest, { force });
|
|
473
848
|
return 0;
|
|
474
849
|
}
|
|
475
850
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
'--subagent': 'subagent',
|
|
485
|
-
};
|
|
486
|
-
|
|
487
|
-
async function cmdEnv(rest) {
|
|
488
|
-
const positional = [];
|
|
489
|
-
const provided = {};
|
|
490
|
-
let reveal = false;
|
|
491
|
-
|
|
492
|
-
for (const a of rest) {
|
|
493
|
-
if (a.startsWith('-')) {
|
|
494
|
-
const eq = a.indexOf('=');
|
|
495
|
-
const flagName = eq === -1 ? a : a.slice(0, eq);
|
|
496
|
-
if (flagName === '--reveal') {
|
|
497
|
-
reveal = true;
|
|
498
|
-
continue;
|
|
499
|
-
}
|
|
500
|
-
if (Object.prototype.hasOwnProperty.call(ENV_VALUE_FLAGS, flagName)) {
|
|
501
|
-
if (eq === -1) {
|
|
502
|
-
throw usageError(`Flag ${flagName} requires a value: ${flagName}=VALUE`);
|
|
503
|
-
}
|
|
504
|
-
provided[ENV_VALUE_FLAGS[flagName]] = a.slice(eq + 1);
|
|
505
|
-
continue;
|
|
506
|
-
}
|
|
507
|
-
throw usageError(`Unknown flag: ${flagName}`);
|
|
508
|
-
}
|
|
509
|
-
positional.push(a);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
const [name, sub, ...extra] = positional;
|
|
513
|
-
if (!name) throw usageError('Usage: cc env <profile> [show] [--base-url=URL --token=TOKEN ...]');
|
|
514
|
-
if (extra.length > 0) throw usageError(`Unexpected argument: ${extra[0]}`);
|
|
515
|
-
profiles.validateProfileName(name);
|
|
516
|
-
const dir = profiles.profileDir(name);
|
|
517
|
-
|
|
518
|
-
if (sub === 'show') {
|
|
519
|
-
if (Object.keys(provided).length > 0) {
|
|
520
|
-
throw usageError('cc env <profile> show does not take configuration flags.');
|
|
521
|
-
}
|
|
522
|
-
return envLib.runShow(name, dir, { reveal });
|
|
523
|
-
}
|
|
524
|
-
if (sub) throw usageError(`Unknown subcommand for cc env: ${sub}`);
|
|
525
|
-
if (reveal) {
|
|
526
|
-
throw usageError('Unknown flag: --reveal (only valid with: cc env <profile> show)');
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
return envLib.runConfigure(name, dir, provided);
|
|
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);
|
|
530
859
|
}
|
|
531
860
|
|
|
532
861
|
async function run(argv) {
|
|
@@ -544,6 +873,15 @@ async function run(argv) {
|
|
|
544
873
|
printHelp(rest[0]);
|
|
545
874
|
return 0;
|
|
546
875
|
}
|
|
876
|
+
if (action === 'env') {
|
|
877
|
+
process.stderr.write(
|
|
878
|
+
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>'.",
|
|
881
|
+
) + '\n',
|
|
882
|
+
);
|
|
883
|
+
return 2;
|
|
884
|
+
}
|
|
547
885
|
switch (action) {
|
|
548
886
|
case 'add':
|
|
549
887
|
return await cmdAdd(rest);
|
|
@@ -551,8 +889,8 @@ async function run(argv) {
|
|
|
551
889
|
return await cmdUse(rest);
|
|
552
890
|
case 'list':
|
|
553
891
|
return cmdList(rest);
|
|
554
|
-
case '
|
|
555
|
-
return await
|
|
892
|
+
case 'copy':
|
|
893
|
+
return await cmdCopy(rest);
|
|
556
894
|
case 'migrate':
|
|
557
895
|
return await cmdMigrate(rest);
|
|
558
896
|
case 'link':
|