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 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, 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". |
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/` 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). |
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`. Same for `sessions/`. On Windows this uses directory junctions (no admin/Developer Mode required); on macOS/Linux, regular symlinks.
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` and `CC_ACTIVE_PROFILE` in its **own** spawn environment
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 vars are set on the child process only — that's all `claude` needs.
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, login state, email, active, linked
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/sessions with links to shared
23
- cc unlink <profile> Restore a profile's private projects/sessions
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
- 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.
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/ (and sessions/ if present) from a source to a destination.
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/ and sessions/ with links to ~/.claude-shared.
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 links to ~/.claude-shared/ and copy shared content back
113
- into the profile as private dirs.
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/ or sessions/ is a link to ~/.claude-shared/, only
122
- the link is removed — the shared target is preserved.
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
- realEmail: p.email,
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
- 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');
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', 'Active', 'Linked']];
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.active ? color.stdout.green('*') : '',
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
- const SUBDIRS = ['projects', 'sessions'];
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
- const SHARED_SUBDIRS = ['projects', 'sessions'];
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, activeProfile = null, extraEnv = {} } = {}) {
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.0",
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
+ }