cchost 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Allen Benny
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # cchost
2
+
3
+ Host multiple isolated Claude Code accounts on one machine via a single `cc` command. Works identically in PowerShell, cmd, bash, zsh, and fish on Windows, macOS, and Linux.
4
+
5
+ Each profile is a complete, isolated `CLAUDE_CONFIG_DIR` — credentials, session history, project state, plugins, and settings. Create one with `cc add <name>`; launch under it with `cc use <name>`.
6
+
7
+ ## Install
8
+
9
+ ```
10
+ npm install -g cchost
11
+ ```
12
+
13
+ Requires Node.js 18+ and Claude Code 2.1.140+ already installed. Get Claude Code from <https://code.claude.com> (`npm install -g @anthropic-ai/claude-code`).
14
+
15
+ ## Commands
16
+
17
+ | Command | What it does |
18
+ | --- | --- |
19
+ | `cc add <profile> [--email=] [--org=] [--name=]` | Create a profile directory. Optional alias flags (EXPERIMENTAL, see below). |
20
+ | `cc use <profile> [...args]` | Launch Claude under that profile. Profile must already exist. Args after the name are forwarded verbatim to `claude`. |
21
+ | `cc list [--original] [--json]` | Show every profile, login state, account email, active marker, and linked/isolated status. |
22
+ | `cc current [--json]` | Print the active profile (`CC_ACTIVE_PROFILE`) or "no profile active". |
23
+ | `cc migrate <src> <dest>` | Copy projects from `default`\|`shared` to `shared`\|`<profile>`. Skip-on-collision; credential files never copied. |
24
+ | `cc link <profile> [<profile>...]` | Link a profile's `projects/` and `sessions/` to `~/.claude-shared`. Migrates existing content; renames on collision. |
25
+ | `cc unlink <profile>` | Restore a profile to its own private `projects/` and `sessions/` (copies shared content back). |
26
+ | `cc remove <profile>` | Delete a profile. Requires typing the profile name to confirm. |
27
+ | `cc help [command]` | Print usage (top-level, or for a single command). |
28
+
29
+ ### Examples
30
+
31
+ ```bash
32
+ cc add work # create the profile, do not launch
33
+ cc use work # first launch: Claude prompts for login
34
+ cc use work --resume # resume last conversation under "work"
35
+ cc use personal -p "summarize" # one-shot prompt under "personal"
36
+ cc list
37
+ cc current
38
+ cc remove old-profile
39
+ ```
40
+
41
+ ### Migrating existing projects
42
+
43
+ If you've been using Claude Code without profiles, your history lives in `~/.claude/projects/`. Pull it into a profile or the shared pool:
44
+
45
+ ```bash
46
+ cc migrate default work # copy ~/.claude/projects → ~/.claude-profiles/work/projects
47
+ cc migrate default shared # copy ~/.claude/projects → ~/.claude-shared/projects
48
+ cc migrate shared personal # copy ~/.claude-shared/projects → ~/.claude-profiles/personal/projects
49
+ ```
50
+
51
+ This is a **copy** — the source is left untouched. If an entry already exists at the destination, the source entry is skipped (no overwrite). Credential files (`.credentials.json`, `.claude.json`) are never copied: each profile keeps its own auth state.
52
+
53
+ ### Shared history across profiles
54
+
55
+ If you want each profile to keep separate credentials but share project history (e.g. rotate accounts on the same project when usage limits hit):
56
+
57
+ ```bash
58
+ cc link work
59
+ cc link personal
60
+ cc list # the Linked column shows "linked" vs "isolated"
61
+ ```
62
+
63
+ `work/projects` and `personal/projects` become links to `~/.claude-shared/projects`. Same for `sessions/`. On Windows this uses directory junctions (no admin/Developer Mode required); on macOS/Linux, regular symlinks.
64
+
65
+ `cc link a b c` links several profiles at once; per-profile failures are reported but don't abort the rest of the batch.
66
+
67
+ If two profiles share history and you launch them against the same project at the same time, both will write to the same session files (last write wins). Don't do that.
68
+
69
+ `cc remove` is safe with shared profiles — it deletes only the link, never recursing into the shared target.
70
+
71
+ ### Aliases (`cc add` flags) — EXPERIMENTAL
72
+
73
+ `cc add work --email=work@example.com --org=Acme --name="Work Account"` stores those values in `~/.claude-profiles/work/.cc-alias.json`. On every `cc use`, they're written into the profile's `.claude.json` `oauthAccount` so `cc list` and Claude's own UI display them instead of the real underlying account.
74
+
75
+ **EXPERIMENTAL.** Claude Code may overwrite `oauthAccount` on server re-sync; we re-apply on the next `cc use`, but masking across all of Claude's UI surfaces has not been verified. Use `cc list --original` to see the real values from `.claude.json`.
76
+
77
+ ## How it works
78
+
79
+ `cc` is a Node CLI with **zero runtime dependencies**. For `cc use` it:
80
+
81
+ 1. Resolves the profile directory under `~/.claude-profiles/<name>/`
82
+ 2. Applies any stored alias to `.claude.json` `oauthAccount` (best-effort)
83
+ 3. Sets `CLAUDE_CONFIG_DIR` and `CC_ACTIVE_PROFILE` in its **own** spawn environment
84
+ 4. Spawns `claude` with `stdio: 'inherit'` so the TUI works correctly
85
+ 5. Forwards the child's exit code
86
+
87
+ The env vars are set on the child process only — that's all `claude` needs.
88
+
89
+ ## Sensitive data
90
+
91
+ `~/.claude-profiles/<profile>/` holds real OAuth credentials. Treat it like SSH keys: don't commit it, don't share it, don't sync it to public cloud storage. The `.gitignore` shipped with this project excludes profile directories from anywhere they might land. `cc migrate` and `cc link` never copy `.credentials.json` or `.claude.json` across profiles.
92
+
93
+ ## Troubleshooting
94
+
95
+ **`Claude Code does not appear to be installed`**
96
+ Install it (`npm install -g @anthropic-ai/claude-code`) and make sure `claude --version` runs in a fresh terminal.
97
+
98
+ **`warning: Claude Code <version> is older than 2.1.140`**
99
+ Earlier versions had bugs where Windows read `.claude.json` from `%USERPROFILE%` regardless of `CLAUDE_CONFIG_DIR`, breaking isolation. Run `claude update`.
100
+
101
+ **`Profile "<name>" does not exist. Create it with: cc add <name>`**
102
+ `cc use` does not auto-create profiles — that prevents typos like `cc use wokr` from silently creating a new, blank one. Run `cc add <name>` first.
103
+
104
+ **Profile name rejected**
105
+ Names must be free of path separators, whitespace, `..`, leading dots, and reserved DOS names (CON, PRN, COM1…). Stick to letters, digits, dashes, underscores.
106
+
107
+ **`cc current` says no profile is active inside a Claude session**
108
+ That means `CC_ACTIVE_PROFILE` isn't set in your shell — `cc use` sets it on the spawned `claude`, so it's visible to nested `cc current` invocations but not in unrelated shells.
109
+
110
+ **Old shell-script version was leaking keystrokes / leaking `use work` into the prompt**
111
+ That was a known bug in the cmd-based predecessor (`%*` ignored `shift`). This package forwards only the argv after `<profile>` to `claude`; the action and profile name themselves are consumed by `cc`.
112
+
113
+ ## License
114
+
115
+ MIT
package/bin/cc.js ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { run } = require('../lib/cli');
5
+
6
+ (async () => {
7
+ try {
8
+ const code = await run(process.argv.slice(2));
9
+ process.exit(typeof code === 'number' ? code : 0);
10
+ } catch (err) {
11
+ const message = err && err.message ? err.message : String(err);
12
+ process.stderr.write(message + '\n');
13
+ const code = err && Number.isInteger(err.exitCode) ? err.exitCode : 1;
14
+ process.exit(code);
15
+ }
16
+ })();
package/lib/cli.js ADDED
@@ -0,0 +1,458 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const readline = require('readline');
6
+
7
+ const profiles = require('./profiles');
8
+ const share = require('./share');
9
+ const migrateLib = require('./migrate');
10
+ const { spawnClaude } = require('./spawn');
11
+ const { renderTable } = require('./format');
12
+ const color = require('./color');
13
+
14
+ const HELP = `cc - Multi-profile launcher for Claude Code
15
+
16
+ Usage:
17
+ cc add <profile> [--email=] [--org=] [--name=] Create a profile (EXPERIMENTAL aliases)
18
+ cc use <profile> [...args] Launch Claude under a named profile
19
+ cc list [--original] [--json] Show all profiles, login state, email, active, linked
20
+ cc current [--json] Show the active profile (from CC_ACTIVE_PROFILE)
21
+ cc migrate <src> <dest> Copy projects from default|shared to shared|<profile>
22
+ cc link <p>... Replace each profile's projects/sessions with links to shared
23
+ cc unlink <profile> Restore a profile's private projects/sessions
24
+ cc remove <profile> Delete a profile and all its data (typed confirmation)
25
+ cc help [command] Show usage (optionally for a single command)
26
+
27
+ Profiles live in ~/.claude-profiles/<profile>/ and become CLAUDE_CONFIG_DIR
28
+ for that launch. Args after the profile name are forwarded verbatim to claude.
29
+ `;
30
+
31
+ const HELP_BY_CMD = {
32
+ add: `cc add <profile> [--email=...] [--org=...] [--name=...]
33
+
34
+ Create a new profile directory without launching Claude. Optional alias flags
35
+ record account values shown in 'cc list' and written into the profile's
36
+ .claude.json oauthAccount on subsequent 'cc use' launches.
37
+
38
+ EXPERIMENTAL: Claude Code may overwrite these on server re-sync. Masking
39
+ behavior across all UI surfaces has not been verified.
40
+
41
+ Examples:
42
+ cc add work
43
+ cc add work --email=work@example.com --org=Acme --name="Work Account"
44
+ `,
45
+ use: `cc use <profile> [...args]
46
+
47
+ Launch Claude under a named profile. The profile must already exist (create
48
+ it with 'cc add <profile>'). Its directory under ~/.claude-profiles/<profile>/
49
+ becomes CLAUDE_CONFIG_DIR for the spawned claude — credentials, sessions,
50
+ projects, plugins, and history are all isolated.
51
+
52
+ All args after the profile name are forwarded verbatim to claude.
53
+
54
+ Examples:
55
+ cc use work
56
+ cc use work --resume
57
+ cc use personal -p "summarize this codebase"
58
+ `,
59
+ list: `cc list [--original] [--json]
60
+
61
+ Show every profile under ~/.claude-profiles/, whether it has credentials, the
62
+ account email, which one matches CC_ACTIVE_PROFILE, and whether the profile
63
+ is linked to ~/.claude-shared.
64
+
65
+ By default, the displayed email reflects any alias set via 'cc add'.
66
+ With --original, shows the real value from .claude.json.
67
+
68
+ With --json, emits an array of
69
+ {name, dir, loggedIn, email, active, linked, shared:{projects, sessions}, alias}.
70
+ `,
71
+ current: `cc current [--json]
72
+
73
+ Print the value of CC_ACTIVE_PROFILE, or "no profile active" if it isn't set.
74
+ CC_ACTIVE_PROFILE is set by 'cc use' on the spawned claude, so nested cc
75
+ invocations see it.
76
+
77
+ With --json, emits {profile, configDir} (both null when nothing is active).
78
+ `,
79
+ migrate: `cc migrate <src> <dest>
80
+
81
+ Copy projects/ (and sessions/ if present) from a source to a destination.
82
+ This is a COPY — the source is left untouched.
83
+
84
+ <src>: default | shared
85
+ default = ~/.claude/ (Claude's built-in non-profile data)
86
+ shared = ~/.claude-shared/ (the cc shared pool)
87
+ <dest>: shared | <profile-name>
88
+
89
+ On collisions, existing entries at the destination are left alone (the source
90
+ entry is skipped). Credential files (.credentials.json, .claude.json) are
91
+ never copied — the destination keeps its own auth state.
92
+
93
+ Examples:
94
+ cc migrate default work # ~/.claude/projects -> ~/.claude-profiles/work/projects
95
+ cc migrate default shared # ~/.claude/projects -> ~/.claude-shared/projects
96
+ cc migrate shared work # ~/.claude-shared/projects -> ~/.claude-profiles/work/projects
97
+ `,
98
+ link: `cc link <profile> [<profile>...]
99
+
100
+ Replace each profile's projects/ and sessions/ with links to ~/.claude-shared.
101
+ Existing content is migrated into shared first; collisions are renamed with a
102
+ __<profile> suffix (never overwritten).
103
+
104
+ On Windows, link uses directory junctions (no admin/Developer Mode needed).
105
+ On macOS/Linux, regular symlinks.
106
+
107
+ Multiple profiles can be linked at once: 'cc link a b c'. Per-profile failures
108
+ are reported but don't abort the rest of the batch.
109
+ `,
110
+ unlink: `cc unlink <profile>
111
+
112
+ Remove a profile's links to ~/.claude-shared/ and copy shared content back
113
+ into the profile as private dirs.
114
+ `,
115
+ remove: `cc remove <profile>
116
+
117
+ Delete a profile and everything inside its directory: credentials, sessions,
118
+ projects, plugins, and history. To confirm, you must type the exact profile
119
+ name — y/n is intentionally not accepted because credential loss is unrecoverable.
120
+
121
+ If the profile's projects/ or sessions/ is a link to ~/.claude-shared/, only
122
+ the link is removed — the shared target is preserved.
123
+ `,
124
+ help: `cc help [command]
125
+
126
+ Without an argument: print the summary of all commands.
127
+ With a command name: print detail for just that command.
128
+ `,
129
+ };
130
+
131
+ function usageError(msg) {
132
+ const e = new Error(msg);
133
+ e.exitCode = 2;
134
+ return e;
135
+ }
136
+
137
+ function printHelp(cmd) {
138
+ if (cmd && Object.prototype.hasOwnProperty.call(HELP_BY_CMD, cmd)) {
139
+ process.stdout.write(HELP_BY_CMD[cmd]);
140
+ } else {
141
+ process.stdout.write(HELP);
142
+ }
143
+ }
144
+
145
+ function prompt(question) {
146
+ return new Promise((resolve) => {
147
+ const rl = readline.createInterface({
148
+ input: process.stdin,
149
+ output: process.stdout,
150
+ });
151
+ rl.question(question, (answer) => {
152
+ rl.close();
153
+ resolve(answer);
154
+ });
155
+ });
156
+ }
157
+
158
+ function isLinkLike(p) {
159
+ try {
160
+ return fs.lstatSync(p).isSymbolicLink();
161
+ } catch {
162
+ return false;
163
+ }
164
+ }
165
+
166
+ function removeRecursivelyPreservingLinks(p) {
167
+ let st;
168
+ try {
169
+ st = fs.lstatSync(p);
170
+ } catch {
171
+ return;
172
+ }
173
+ if (st.isSymbolicLink()) {
174
+ try {
175
+ fs.unlinkSync(p);
176
+ return;
177
+ } catch (err) {
178
+ if (process.platform === 'win32') {
179
+ fs.rmdirSync(p);
180
+ return;
181
+ }
182
+ throw err;
183
+ }
184
+ }
185
+ if (st.isDirectory()) {
186
+ for (const ent of fs.readdirSync(p)) {
187
+ removeRecursivelyPreservingLinks(path.join(p, ent));
188
+ }
189
+ fs.rmdirSync(p);
190
+ return;
191
+ }
192
+ fs.unlinkSync(p);
193
+ }
194
+
195
+ function parseFlagsStrict(args, flagNames, { allowPositional = false } = {}) {
196
+ const flags = new Set();
197
+ const positional = [];
198
+ for (const a of args) {
199
+ if (a.startsWith('-')) {
200
+ if (!flagNames.includes(a)) {
201
+ throw usageError(`Unknown flag: ${a}`);
202
+ }
203
+ flags.add(a);
204
+ } else {
205
+ positional.push(a);
206
+ }
207
+ }
208
+ if (!allowPositional && positional.length > 0) {
209
+ throw usageError(`Unexpected argument: ${positional[0]}`);
210
+ }
211
+ return { flags, positional };
212
+ }
213
+
214
+ function parseAliasFlags(args) {
215
+ const out = {};
216
+ for (const a of args) {
217
+ if (!a.startsWith('--')) {
218
+ throw usageError(`Unexpected positional argument: ${a}`);
219
+ }
220
+ const eq = a.indexOf('=');
221
+ if (eq === -1) {
222
+ throw usageError(`Flag must be in --key=value form: ${a}`);
223
+ }
224
+ const key = a.slice(2, eq);
225
+ const value = a.slice(eq + 1);
226
+ if (!profiles.ALLOWED_ALIAS_KEYS.includes(key)) {
227
+ throw usageError(`Unknown flag: --${key}. Allowed: --email, --org, --name.`);
228
+ }
229
+ if (value.length === 0) {
230
+ throw usageError(`Flag --${key} requires a non-empty value.`);
231
+ }
232
+ out[key] = value;
233
+ }
234
+ return out;
235
+ }
236
+
237
+ async function cmdAdd(rest) {
238
+ const [name, ...flagArgs] = rest;
239
+ if (!name) throw usageError('Usage: cc add <profile> [--email=...] [--org=...] [--name=...]');
240
+ const aliasFlags = parseAliasFlags(flagArgs);
241
+ const { dir } = profiles.addProfile(name, aliasFlags);
242
+ console.log(`Created profile "${name}" at ${dir}.`);
243
+ if (Object.keys(aliasFlags).length > 0) {
244
+ process.stderr.write(
245
+ color.stderr.yellow('Aliases are EXPERIMENTAL — Claude may overwrite them on server re-sync.') + '\n'
246
+ );
247
+ console.log(` Stored aliases: ${JSON.stringify(aliasFlags)}`);
248
+ }
249
+ console.log(`Next: cc use ${name}`);
250
+ return 0;
251
+ }
252
+
253
+ async function cmdUse(rest) {
254
+ const [name, ...passthrough] = rest;
255
+ if (!name) throw usageError('Usage: cc use <profile> [...args]');
256
+ profiles.validateProfileName(name);
257
+ const dir = profiles.profileDir(name);
258
+ if (!fs.existsSync(dir)) {
259
+ const err = new Error(`Profile "${name}" does not exist. Create it with: cc add ${name}`);
260
+ err.exitCode = 1;
261
+ throw err;
262
+ }
263
+ try {
264
+ profiles.applyAlias(dir);
265
+ } catch {
266
+ // best-effort; never block the launch
267
+ }
268
+ return spawnClaude({
269
+ args: passthrough,
270
+ profileDir: dir,
271
+ activeProfile: name,
272
+ });
273
+ }
274
+
275
+ function cmdCurrent(rest) {
276
+ const { flags } = parseFlagsStrict(rest, ['--json']);
277
+ const value = process.env.CC_ACTIVE_PROFILE || '';
278
+ if (flags.has('--json')) {
279
+ const payload = value
280
+ ? { profile: value, configDir: process.env.CLAUDE_CONFIG_DIR || profiles.profileDir(value) }
281
+ : { profile: null, configDir: null };
282
+ process.stdout.write(JSON.stringify(payload) + '\n');
283
+ return 0;
284
+ }
285
+ console.log(value ? value : 'no profile active');
286
+ return 0;
287
+ }
288
+
289
+ function cmdList(rest) {
290
+ const { flags } = parseFlagsStrict(rest, ['--json', '--original']);
291
+ const list = profiles.listProfiles();
292
+ const active = process.env.CC_ACTIVE_PROFILE || '';
293
+ const useOriginal = flags.has('--original');
294
+
295
+ const enriched = list.map((p) => {
296
+ const projectsLink = isLinkLike(path.join(p.dir, 'projects'));
297
+ const sessionsLink = isLinkLike(path.join(p.dir, 'sessions'));
298
+ const linked = projectsLink || sessionsLink;
299
+ const alias = p.alias || {};
300
+ const displayEmail = useOriginal ? p.email : (alias.email || p.email);
301
+ return {
302
+ name: p.name,
303
+ dir: p.dir,
304
+ loggedIn: p.loggedIn,
305
+ email: displayEmail,
306
+ realEmail: p.email,
307
+ active: p.name === active,
308
+ linked,
309
+ shared: { projects: projectsLink, sessions: sessionsLink },
310
+ alias,
311
+ };
312
+ });
313
+
314
+ if (flags.has('--json')) {
315
+ const payload = enriched.map((p) => ({
316
+ name: p.name,
317
+ dir: p.dir,
318
+ loggedIn: p.loggedIn,
319
+ email: p.email,
320
+ active: p.active,
321
+ linked: p.linked,
322
+ shared: p.shared,
323
+ alias: p.alias,
324
+ }));
325
+ process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
326
+ return 0;
327
+ }
328
+
329
+ if (enriched.length === 0) {
330
+ console.log(`No profiles yet in ${profiles.baseDir()}.`);
331
+ console.log('Create one with: cc add <name>');
332
+ return 0;
333
+ }
334
+
335
+ const rows = [['Profile', 'LoggedIn', 'Email', 'Active', 'Linked']];
336
+ for (const p of enriched) {
337
+ rows.push([
338
+ p.name,
339
+ p.loggedIn ? 'true' : 'false',
340
+ p.email || '',
341
+ p.active ? color.stdout.green('*') : '',
342
+ p.linked ? 'linked' : 'isolated',
343
+ ]);
344
+ }
345
+ console.log(renderTable(rows));
346
+ return 0;
347
+ }
348
+
349
+ async function cmdRemove(rest) {
350
+ const [name] = rest;
351
+ if (!name) throw usageError('Usage: cc remove <profile>');
352
+ profiles.validateProfileName(name);
353
+ const dir = profiles.profileDir(name);
354
+ if (!fs.existsSync(dir)) {
355
+ const err = new Error(`Profile "${name}" does not exist at ${dir}.`);
356
+ err.exitCode = 1;
357
+ throw err;
358
+ }
359
+ console.log('This will permanently delete:');
360
+ console.log(` ${dir}`);
361
+ console.log('including credentials, sessions, projects, plugins, and history.');
362
+ if (name === process.env.CC_ACTIVE_PROFILE) {
363
+ console.log(color.stderr.yellow(`Note: "${name}" is the currently active profile in this shell.`));
364
+ }
365
+ for (const sub of share.SHARED_SUBDIRS) {
366
+ const sub_p = path.join(dir, sub);
367
+ if (isLinkLike(sub_p)) {
368
+ console.log(` (${sub}/ is a link to shared storage — only the link will be removed)`);
369
+ }
370
+ }
371
+ const answer = await prompt(`Type the profile name "${name}" to confirm: `);
372
+ if (answer.trim() !== name) {
373
+ console.log('Aborted.');
374
+ return 0;
375
+ }
376
+ try {
377
+ removeRecursivelyPreservingLinks(dir);
378
+ } catch (err) {
379
+ throw new Error(`Failed to fully remove ${dir}: ${err.message}`);
380
+ }
381
+ console.log(`Removed ${dir}.`);
382
+ if (name === process.env.CC_ACTIVE_PROFILE) {
383
+ console.log('Note: the active profile in this shell no longer exists; run `cc use <name>` to switch.');
384
+ }
385
+ return 0;
386
+ }
387
+
388
+ async function cmdLink(rest) {
389
+ if (rest.length === 0) throw usageError('Usage: cc link <profile> [<profile>...]');
390
+ const failures = [];
391
+ for (const name of rest) {
392
+ if (rest.length > 1) console.log(`\n[${name}]`);
393
+ try {
394
+ share.shareLink(name);
395
+ } catch (err) {
396
+ process.stderr.write(color.stderr.red(` error: ${err.message}`) + '\n');
397
+ failures.push({ name, message: err.message });
398
+ }
399
+ }
400
+ if (rest.length > 1) {
401
+ const ok = rest.length - failures.length;
402
+ console.log(`\nSummary: ${ok} succeeded, ${failures.length} failed.`);
403
+ for (const f of failures) console.log(` ${f.name}: ${f.message}`);
404
+ }
405
+ return failures.length === 0 ? 0 : 1;
406
+ }
407
+
408
+ async function cmdUnlink(rest) {
409
+ const [name] = rest;
410
+ if (!name) throw usageError('Usage: cc unlink <profile>');
411
+ share.shareUnlink(name);
412
+ return 0;
413
+ }
414
+
415
+ async function cmdMigrate(rest) {
416
+ const [src, dest, ...extra] = rest;
417
+ if (!src || !dest) throw usageError('Usage: cc migrate <src> <dest>');
418
+ if (extra.length > 0) throw usageError(`Unexpected extra arguments: ${extra.join(' ')}`);
419
+ migrateLib.migrate(src, dest);
420
+ return 0;
421
+ }
422
+
423
+ async function run(argv) {
424
+ const [action, ...rest] = argv;
425
+ if (!action || action === '--help' || action === '-h') {
426
+ printHelp();
427
+ return 0;
428
+ }
429
+ if (action === 'help') {
430
+ printHelp(rest[0]);
431
+ return 0;
432
+ }
433
+ switch (action) {
434
+ case 'add':
435
+ return await cmdAdd(rest);
436
+ case 'use':
437
+ return await cmdUse(rest);
438
+ case 'list':
439
+ return cmdList(rest);
440
+ case 'current':
441
+ return cmdCurrent(rest);
442
+ case 'migrate':
443
+ return await cmdMigrate(rest);
444
+ case 'link':
445
+ return await cmdLink(rest);
446
+ case 'unlink':
447
+ return await cmdUnlink(rest);
448
+ case 'remove':
449
+ return await cmdRemove(rest);
450
+ default: {
451
+ process.stderr.write(color.stderr.red(`Unknown command: ${action}`) + '\n\n');
452
+ printHelp();
453
+ return 2;
454
+ }
455
+ }
456
+ }
457
+
458
+ module.exports = { run, printHelp, removeRecursivelyPreservingLinks };
package/lib/color.js ADDED
@@ -0,0 +1,26 @@
1
+ 'use strict';
2
+
3
+ function colorEnabled(stream) {
4
+ if (process.env.NO_COLOR && process.env.NO_COLOR.length > 0) return false;
5
+ if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== '0') return true;
6
+ const s = stream || process.stdout;
7
+ return Boolean(s && s.isTTY);
8
+ }
9
+
10
+ function wrap(stream, open, close) {
11
+ return (text) => (colorEnabled(stream) ? `\x1b[${open}m${text}\x1b[${close}m` : String(text));
12
+ }
13
+
14
+ const stderr = {
15
+ yellow: wrap(process.stderr, 33, 39),
16
+ red: wrap(process.stderr, 31, 39),
17
+ dim: wrap(process.stderr, 2, 22),
18
+ };
19
+
20
+ const stdout = {
21
+ green: wrap(process.stdout, 32, 39),
22
+ bold: wrap(process.stdout, 1, 22),
23
+ dim: wrap(process.stdout, 2, 22),
24
+ };
25
+
26
+ module.exports = { colorEnabled, stderr, stdout };
package/lib/format.js ADDED
@@ -0,0 +1,26 @@
1
+ 'use strict';
2
+
3
+ function renderTable(rows) {
4
+ if (!Array.isArray(rows) || rows.length === 0) return '';
5
+ const cols = rows[0].length;
6
+ const widths = new Array(cols).fill(0);
7
+ for (const row of rows) {
8
+ for (let i = 0; i < cols; i++) {
9
+ const s = String(row[i] == null ? '' : row[i]);
10
+ if (s.length > widths[i]) widths[i] = s.length;
11
+ }
12
+ }
13
+ return rows
14
+ .map((row) =>
15
+ row
16
+ .map((v, i) => {
17
+ const s = String(v == null ? '' : v);
18
+ return i === cols - 1 ? s : s.padEnd(widths[i] + 2);
19
+ })
20
+ .join('')
21
+ .replace(/\s+$/, ''),
22
+ )
23
+ .join('\n');
24
+ }
25
+
26
+ module.exports = { renderTable };