cchost 0.1.0 → 0.1.2
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/README.md +17 -12
- package/lib/cli.js +25 -71
- package/lib/migrate.js +3 -1
- package/lib/share.js +4 -1
- package/lib/spawn.js +1 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -18,11 +18,10 @@ Requires Node.js 18+ and Claude Code 2.1.140+ already installed. Get Claude Code
|
|
|
18
18
|
| --- | --- |
|
|
19
19
|
| `cc add <profile> [--email=] [--org=] [--name=]` | Create a profile directory. Optional alias flags (EXPERIMENTAL, see below). |
|
|
20
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
|
|
22
|
-
| `cc current [--json]` | Print the active profile (`CC_ACTIVE_PROFILE`) or "no profile active". |
|
|
21
|
+
| `cc list [--original] [--json]` | Show every profile: login state, account email, and storage mode (`shared` / `isolated`). |
|
|
23
22
|
| `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/`
|
|
25
|
-
| `cc unlink <profile>` | Restore a profile to its own private `projects/`
|
|
23
|
+
| `cc link <profile> [<profile>...]` | Link a profile's `projects/` to `~/.claude-shared/projects`. Migrates existing content; renames on collision. |
|
|
24
|
+
| `cc unlink <profile>` | Restore a profile to its own private `projects/` (copies shared content back). |
|
|
26
25
|
| `cc remove <profile>` | Delete a profile. Requires typing the profile name to confirm. |
|
|
27
26
|
| `cc help [command]` | Print usage (top-level, or for a single command). |
|
|
28
27
|
|
|
@@ -34,10 +33,18 @@ cc use work # first launch: Claude prompts for login
|
|
|
34
33
|
cc use work --resume # resume last conversation under "work"
|
|
35
34
|
cc use personal -p "summarize" # one-shot prompt under "personal"
|
|
36
35
|
cc list
|
|
37
|
-
cc current
|
|
38
36
|
cc remove old-profile
|
|
39
37
|
```
|
|
40
38
|
|
|
39
|
+
`cc list` output:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
Profile LoggedIn Email Storage
|
|
43
|
+
personal true personal@example.com isolated
|
|
44
|
+
scratch false - isolated
|
|
45
|
+
work true work@example.com shared
|
|
46
|
+
```
|
|
47
|
+
|
|
41
48
|
### Migrating existing projects
|
|
42
49
|
|
|
43
50
|
If you've been using Claude Code without profiles, your history lives in `~/.claude/projects/`. Pull it into a profile or the shared pool:
|
|
@@ -57,10 +64,11 @@ If you want each profile to keep separate credentials but share project history
|
|
|
57
64
|
```bash
|
|
58
65
|
cc link work
|
|
59
66
|
cc link personal
|
|
60
|
-
cc list # the Linked column shows "linked" vs "isolated"
|
|
61
67
|
```
|
|
62
68
|
|
|
63
|
-
`work/projects` and `personal/projects` become links to `~/.claude-shared/projects`.
|
|
69
|
+
`work/projects` and `personal/projects` become links to `~/.claude-shared/projects`. On Windows this uses directory junctions (no admin/Developer Mode required); on macOS/Linux, regular symlinks.
|
|
70
|
+
|
|
71
|
+
**Only `projects/` is shared.** That's the directory Claude Code uses for resumable conversation transcripts (`projects/<project>/<session>.jsonl`). The sibling `sessions/` folder holds transient per-process registry state — PID, working directory, status, timestamps — and is **never** linked, migrated, or copied across profiles. Sharing it would corrupt Claude's running-process bookkeeping. `sessions/` stays per-profile under `~/.claude-profiles/<name>/sessions/`.
|
|
64
72
|
|
|
65
73
|
`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
74
|
|
|
@@ -80,11 +88,11 @@ If two profiles share history and you launch them against the same project at th
|
|
|
80
88
|
|
|
81
89
|
1. Resolves the profile directory under `~/.claude-profiles/<name>/`
|
|
82
90
|
2. Applies any stored alias to `.claude.json` `oauthAccount` (best-effort)
|
|
83
|
-
3. Sets `CLAUDE_CONFIG_DIR`
|
|
91
|
+
3. Sets `CLAUDE_CONFIG_DIR` in its **own** spawn environment
|
|
84
92
|
4. Spawns `claude` with `stdio: 'inherit'` so the TUI works correctly
|
|
85
93
|
5. Forwards the child's exit code
|
|
86
94
|
|
|
87
|
-
The env
|
|
95
|
+
The env var is set on the child process only — that's all `claude` needs.
|
|
88
96
|
|
|
89
97
|
## Sensitive data
|
|
90
98
|
|
|
@@ -104,9 +112,6 @@ Earlier versions had bugs where Windows read `.claude.json` from `%USERPROFILE%`
|
|
|
104
112
|
**Profile name rejected**
|
|
105
113
|
Names must be free of path separators, whitespace, `..`, leading dots, and reserved DOS names (CON, PRN, COM1…). Stick to letters, digits, dashes, underscores.
|
|
106
114
|
|
|
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
115
|
**Old shell-script version was leaking keystrokes / leaking `use work` into the prompt**
|
|
111
116
|
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
117
|
|
package/lib/cli.js
CHANGED
|
@@ -16,11 +16,10 @@ const HELP = `cc - Multi-profile launcher for Claude Code
|
|
|
16
16
|
Usage:
|
|
17
17
|
cc add <profile> [--email=] [--org=] [--name=] Create a profile (EXPERIMENTAL aliases)
|
|
18
18
|
cc use <profile> [...args] Launch Claude under a named profile
|
|
19
|
-
cc list [--original] [--json] Show all profiles
|
|
20
|
-
cc current [--json] Show the active profile (from CC_ACTIVE_PROFILE)
|
|
19
|
+
cc list [--original] [--json] Show all profiles: login state, email, storage (shared/isolated)
|
|
21
20
|
cc migrate <src> <dest> Copy projects from default|shared to shared|<profile>
|
|
22
|
-
cc link <p>... Replace each profile's projects/
|
|
23
|
-
cc unlink <profile> Restore a profile's private projects/
|
|
21
|
+
cc link <p>... Replace each profile's projects/ with a link to shared
|
|
22
|
+
cc unlink <profile> Restore a profile's private projects/
|
|
24
23
|
cc remove <profile> Delete a profile and all its data (typed confirmation)
|
|
25
24
|
cc help [command] Show usage (optionally for a single command)
|
|
26
25
|
|
|
@@ -58,28 +57,22 @@ Examples:
|
|
|
58
57
|
`,
|
|
59
58
|
list: `cc list [--original] [--json]
|
|
60
59
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
Columns:
|
|
61
|
+
Profile - profile name
|
|
62
|
+
LoggedIn - true if .credentials.json exists
|
|
63
|
+
Email - account email (alias-aware; - if not yet logged in)
|
|
64
|
+
Storage - 'shared' if projects/ is linked to ~/.claude-shared, 'isolated' otherwise
|
|
65
|
+
(sessions/ is transient per-process state and is never shared)
|
|
64
66
|
|
|
65
67
|
By default, the displayed email reflects any alias set via 'cc add'.
|
|
66
68
|
With --original, shows the real value from .claude.json.
|
|
67
69
|
|
|
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).
|
|
70
|
+
With --json, emits an array of {name, dir, loggedIn, email, storage, alias}.
|
|
78
71
|
`,
|
|
79
72
|
migrate: `cc migrate <src> <dest>
|
|
80
73
|
|
|
81
|
-
Copy projects/
|
|
82
|
-
This is a COPY — the source is left untouched.
|
|
74
|
+
Copy projects/ from a source to a destination. sessions/ (transient per-process
|
|
75
|
+
registry state) is never migrated. This is a COPY — the source is left untouched.
|
|
83
76
|
|
|
84
77
|
<src>: default | shared
|
|
85
78
|
default = ~/.claude/ (Claude's built-in non-profile data)
|
|
@@ -97,9 +90,10 @@ Examples:
|
|
|
97
90
|
`,
|
|
98
91
|
link: `cc link <profile> [<profile>...]
|
|
99
92
|
|
|
100
|
-
Replace each profile's projects/
|
|
93
|
+
Replace each profile's projects/ with a link to ~/.claude-shared/projects/.
|
|
101
94
|
Existing content is migrated into shared first; collisions are renamed with a
|
|
102
|
-
__<profile> suffix (never overwritten).
|
|
95
|
+
__<profile> suffix (never overwritten). sessions/ (transient per-process state)
|
|
96
|
+
is intentionally never linked — it stays per-profile.
|
|
103
97
|
|
|
104
98
|
On Windows, link uses directory junctions (no admin/Developer Mode needed).
|
|
105
99
|
On macOS/Linux, regular symlinks.
|
|
@@ -109,8 +103,8 @@ are reported but don't abort the rest of the batch.
|
|
|
109
103
|
`,
|
|
110
104
|
unlink: `cc unlink <profile>
|
|
111
105
|
|
|
112
|
-
Remove a profile's
|
|
113
|
-
into the profile as private
|
|
106
|
+
Remove a profile's projects/ link to ~/.claude-shared/ and copy shared content
|
|
107
|
+
back into the profile as a private dir.
|
|
114
108
|
`,
|
|
115
109
|
remove: `cc remove <profile>
|
|
116
110
|
|
|
@@ -118,8 +112,8 @@ Delete a profile and everything inside its directory: credentials, sessions,
|
|
|
118
112
|
projects, plugins, and history. To confirm, you must type the exact profile
|
|
119
113
|
name — y/n is intentionally not accepted because credential loss is unrecoverable.
|
|
120
114
|
|
|
121
|
-
If the profile's projects/
|
|
122
|
-
|
|
115
|
+
If the profile's projects/ is a link to ~/.claude-shared/, only the link is
|
|
116
|
+
removed — the shared target is preserved.
|
|
123
117
|
`,
|
|
124
118
|
help: `cc help [command]
|
|
125
119
|
|
|
@@ -268,61 +262,30 @@ async function cmdUse(rest) {
|
|
|
268
262
|
return spawnClaude({
|
|
269
263
|
args: passthrough,
|
|
270
264
|
profileDir: dir,
|
|
271
|
-
activeProfile: name,
|
|
272
265
|
});
|
|
273
266
|
}
|
|
274
267
|
|
|
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
268
|
function cmdList(rest) {
|
|
290
269
|
const { flags } = parseFlagsStrict(rest, ['--json', '--original']);
|
|
291
270
|
const list = profiles.listProfiles();
|
|
292
|
-
const active = process.env.CC_ACTIVE_PROFILE || '';
|
|
293
271
|
const useOriginal = flags.has('--original');
|
|
294
272
|
|
|
295
273
|
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
274
|
const alias = p.alias || {};
|
|
300
275
|
const displayEmail = useOriginal ? p.email : (alias.email || p.email);
|
|
276
|
+
const storage = isLinkLike(path.join(p.dir, 'projects')) ? 'shared' : 'isolated';
|
|
301
277
|
return {
|
|
302
278
|
name: p.name,
|
|
303
279
|
dir: p.dir,
|
|
304
280
|
loggedIn: p.loggedIn,
|
|
305
281
|
email: displayEmail,
|
|
306
|
-
|
|
307
|
-
active: p.name === active,
|
|
308
|
-
linked,
|
|
309
|
-
shared: { projects: projectsLink, sessions: sessionsLink },
|
|
282
|
+
storage,
|
|
310
283
|
alias,
|
|
311
284
|
};
|
|
312
285
|
});
|
|
313
286
|
|
|
314
287
|
if (flags.has('--json')) {
|
|
315
|
-
|
|
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');
|
|
288
|
+
process.stdout.write(JSON.stringify(enriched, null, 2) + '\n');
|
|
326
289
|
return 0;
|
|
327
290
|
}
|
|
328
291
|
|
|
@@ -332,14 +295,13 @@ function cmdList(rest) {
|
|
|
332
295
|
return 0;
|
|
333
296
|
}
|
|
334
297
|
|
|
335
|
-
const rows = [['Profile', 'LoggedIn', 'Email', '
|
|
298
|
+
const rows = [['Profile', 'LoggedIn', 'Email', 'Storage']];
|
|
336
299
|
for (const p of enriched) {
|
|
337
300
|
rows.push([
|
|
338
301
|
p.name,
|
|
339
302
|
p.loggedIn ? 'true' : 'false',
|
|
340
|
-
p.email
|
|
341
|
-
p.
|
|
342
|
-
p.linked ? 'linked' : 'isolated',
|
|
303
|
+
p.email ? p.email : '-',
|
|
304
|
+
p.storage,
|
|
343
305
|
]);
|
|
344
306
|
}
|
|
345
307
|
console.log(renderTable(rows));
|
|
@@ -359,9 +321,6 @@ async function cmdRemove(rest) {
|
|
|
359
321
|
console.log('This will permanently delete:');
|
|
360
322
|
console.log(` ${dir}`);
|
|
361
323
|
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
324
|
for (const sub of share.SHARED_SUBDIRS) {
|
|
366
325
|
const sub_p = path.join(dir, sub);
|
|
367
326
|
if (isLinkLike(sub_p)) {
|
|
@@ -379,9 +338,6 @@ async function cmdRemove(rest) {
|
|
|
379
338
|
throw new Error(`Failed to fully remove ${dir}: ${err.message}`);
|
|
380
339
|
}
|
|
381
340
|
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
341
|
return 0;
|
|
386
342
|
}
|
|
387
343
|
|
|
@@ -437,8 +393,6 @@ async function run(argv) {
|
|
|
437
393
|
return await cmdUse(rest);
|
|
438
394
|
case 'list':
|
|
439
395
|
return cmdList(rest);
|
|
440
|
-
case 'current':
|
|
441
|
-
return cmdCurrent(rest);
|
|
442
396
|
case 'migrate':
|
|
443
397
|
return await cmdMigrate(rest);
|
|
444
398
|
case 'link':
|
package/lib/migrate.js
CHANGED
|
@@ -6,7 +6,9 @@ const os = require('os');
|
|
|
6
6
|
|
|
7
7
|
const profiles = require('./profiles');
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
// Only projects/ holds the resumable conversation transcripts. sessions/ is transient
|
|
10
|
+
// per-process state and is intentionally not migrated.
|
|
11
|
+
const SUBDIRS = ['projects'];
|
|
10
12
|
const EXCLUDED_FILES = new Set(['.credentials.json', '.claude.json']);
|
|
11
13
|
|
|
12
14
|
function defaultClaudeDir() {
|
package/lib/share.js
CHANGED
|
@@ -5,7 +5,10 @@ const path = require('path');
|
|
|
5
5
|
|
|
6
6
|
const profiles = require('./profiles');
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
// Only projects/ holds the resumable conversation transcripts (projects/<proj>/<session>.jsonl).
|
|
9
|
+
// sessions/ is transient per-process registry state (PID, cwd, status, timestamps) and
|
|
10
|
+
// must stay per-profile — sharing it would corrupt Claude's running-process bookkeeping.
|
|
11
|
+
const SHARED_SUBDIRS = ['projects'];
|
|
9
12
|
|
|
10
13
|
function symlinkType() {
|
|
11
14
|
return process.platform === 'win32' ? 'junction' : 'dir';
|
package/lib/spawn.js
CHANGED
|
@@ -155,7 +155,7 @@ function platformSpawn(bin, args, opts) {
|
|
|
155
155
|
return spawn(bin, args, opts);
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
function spawnClaude({ args = [], profileDir = null,
|
|
158
|
+
function spawnClaude({ args = [], profileDir = null, extraEnv = {} } = {}) {
|
|
159
159
|
const bin = findClaude();
|
|
160
160
|
if (!bin) {
|
|
161
161
|
const err = new Error(
|
|
@@ -174,11 +174,6 @@ function spawnClaude({ args = [], profileDir = null, activeProfile = null, extra
|
|
|
174
174
|
} else {
|
|
175
175
|
delete childEnv.CLAUDE_CONFIG_DIR;
|
|
176
176
|
}
|
|
177
|
-
if (activeProfile) {
|
|
178
|
-
childEnv.CC_ACTIVE_PROFILE = activeProfile;
|
|
179
|
-
} else {
|
|
180
|
-
delete childEnv.CC_ACTIVE_PROFILE;
|
|
181
|
-
}
|
|
182
177
|
|
|
183
178
|
return new Promise((resolve) => {
|
|
184
179
|
let child;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cchost",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Host multiple isolated Claude Code accounts on one machine via the `cc` command.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
@@ -43,4 +43,4 @@
|
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"vitest": "^2.1.0"
|
|
45
45
|
}
|
|
46
|
-
}
|
|
46
|
+
}
|