cchost 0.2.0 → 0.2.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/README.md CHANGED
@@ -18,10 +18,10 @@ Requires Node.js 18+ and Claude Code 2.1.140+ already installed. Get Claude Code
18
18
  | --- | --- |
19
19
  | `cc add <profile>` | Create a profile directory. |
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 [--json]` | Show every profile: login state, account email, and storage mode (`shared` / `isolated`). |
22
- | `cc migrate <src> <dest>` | Copy projects from `default`\|`shared` to `shared`\|`<profile>`. Skip-on-collision; credential files never copied. |
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). |
21
+ | `cc list [--json]` | Show every profile: login state, account email, and how many artifact dirs are linked to shared (N/4). |
22
+ | `cc migrate <src> <dest> [--force]` | Copy artifacts from `default`\|`shared` to `shared`\|`<profile>`. Skip-on-collision by default; `--force` overwrites. |
23
+ | `cc link <profile> [<profile>...]` | Link a profile's artifact dirs to `~/.claude-shared/`. Migrates existing content; renames on collision. |
24
+ | `cc unlink <profile>` | Restore a profile to its own private artifact dirs (copies shared content back). |
25
25
  | `cc remove <profile>` | Delete a profile. Requires typing the profile name to confirm. |
26
26
  | `cc doctor [--fix [--force]] [--json]` | Diagnose environment, storage, and platform issues. |
27
27
  | `cc help [command]` | Print usage (top-level, or for a single command). |
@@ -40,42 +40,63 @@ cc remove old-profile
40
40
  `cc list` output:
41
41
 
42
42
  ```
43
- Profile LoggedIn Email Storage
44
- personal true personal@example.com isolated
45
- scratch false - isolated
46
- work true work@example.com shared
43
+ Profile LoggedIn Email Shared
44
+ personal true personal@example.com 4/4
45
+ scratch false - 0/4
46
+ work true work@example.com 0/4
47
47
  ```
48
48
 
49
- ### Migrating existing projects
49
+ ### What migrate copies and what link shares
50
50
 
51
- If you've been using Claude Code without profiles, your history lives in `~/.claude/projects/`. Pull it into a profile or the shared pool:
51
+ | Artifact | `cc migrate` copies it | `cc link` shares it | Notes |
52
+ | --- | :---: | :---: | --- |
53
+ | `projects/` | ✓ | ✓ | Resumable conversation transcripts |
54
+ | `skills/` | ✓ | ✓ | Custom slash-command scripts |
55
+ | `agents/` | ✓ | ✓ | Custom agent definitions |
56
+ | `commands/` | ✓ | ✓ | Custom command configs |
57
+ | `mcp.json` | ✓ (default→profile only) | — | Per-profile MCP server config; may carry auth tokens |
58
+ | `settings.json` | ✓ (default→profile only) | — | Per-profile UI/feature settings |
59
+ | `CLAUDE.md` | ✓ (default→profile only) | — | Per-profile project instructions |
60
+ | `sessions/` | — | — | Transient per-process state; sharing would corrupt bookkeeping |
61
+ | `plugins/` | — | — | Carries auth tokens; deferred to a future opt-in flag |
62
+ | `.credentials.json` | — | — | Never touched — each profile keeps its own auth |
63
+ | `.claude.json` | — | — | Never touched — re-synced from server on every launch |
64
+
65
+ `mcp.json`, `settings.json`, and `CLAUDE.md` are **not** copied to `shared` and are **not** linked because they typically carry auth tokens or per-profile configuration that should not leak across accounts.
66
+
67
+ ### Migrating existing artifacts
68
+
69
+ If you've been using Claude Code without profiles, your history lives in `~/.claude/`. Pull it into a profile or the shared pool:
52
70
 
53
71
  ```bash
54
- cc migrate default work # copy ~/.claude/projects → ~/.claude-profiles/work/projects
55
- cc migrate default shared # copy ~/.claude/projects → ~/.claude-shared/projects
56
- cc migrate shared personal # copy ~/.claude-shared/projects → ~/.claude-profiles/personal/projects
72
+ cc migrate default work # copy ~/.claude/* → ~/.claude-profiles/work/
73
+ cc migrate default shared # copy ~/.claude/projects,skills,agents,commands → ~/.claude-shared/
74
+ cc migrate shared personal # copy ~/.claude-shared/* → ~/.claude-profiles/personal/
75
+ cc migrate default work --force # same as first, overwriting anything that already exists
57
76
  ```
58
77
 
59
- 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.
78
+ This is a **copy** — the source is left untouched. By default, if an entry already exists at the destination it is silently skipped. Pass `--force` to overwrite instead — existing destination content will be cleared.
79
+
80
+ Credential files (`.credentials.json`, `.claude.json`) are never copied: each profile keeps its own auth state.
60
81
 
61
- ### Shared history across profiles
82
+ ### Shared artifacts across profiles
62
83
 
63
- 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):
84
+ If you want each profile to keep separate credentials but share project history and skills (e.g. rotate accounts on the same project when usage limits hit):
64
85
 
65
86
  ```bash
66
87
  cc link work
67
88
  cc link personal
68
89
  ```
69
90
 
70
- `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.
91
+ `work/` and `personal/` each get four linked dirs (`projects`, `skills`, `agents`, `commands`) pointing to `~/.claude-shared/`. On Windows this uses directory junctions (no admin/Developer Mode required); on macOS/Linux, regular symlinks.
71
92
 
72
- **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/`.
93
+ `sessions/` is **never** linked. It holds transient per-process registry state — PID, working directory, status, timestamps — and sharing it would corrupt Claude's running-process bookkeeping. It stays per-profile under `~/.claude-profiles/<name>/sessions/`.
73
94
 
74
95
  `cc link a b c` links several profiles at once; per-profile failures are reported but don't abort the rest of the batch.
75
96
 
76
97
  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.
77
98
 
78
- `cc remove` is safe with shared profiles — it deletes only the link, never recursing into the shared target.
99
+ `cc remove` is safe with shared profiles — it deletes only the links, never recursing into the shared target.
79
100
 
80
101
  ## How it works
81
102
 
package/lib/cli.js CHANGED
@@ -16,10 +16,10 @@ const HELP = `cc - Multi-profile launcher for Claude Code
16
16
  Usage:
17
17
  cc add <profile> Create a profile directory
18
18
  cc use <profile> [...args] Launch Claude under a named profile
19
- cc list [--json] Show all profiles: login state, email, storage (shared/isolated)
20
- cc migrate <src> <dest> Copy projects from default|shared to shared|<profile>
21
- cc link <p>... Replace each profile's projects/ with a link to shared
22
- cc unlink <profile> Restore a profile's private projects/
19
+ cc list [--json] Show all profiles: login state, email, shared-dir count (N/4)
20
+ cc migrate <src> <dest> [--force] Copy artifact dirs (and config files) between profiles
21
+ cc link <p>... Link each profile's artifact dirs to ~/.claude-shared/
22
+ cc unlink <profile> Restore a profile's private artifact dirs
23
23
  cc remove <profile> Delete a profile and all its data (typed confirmation)
24
24
  cc doctor [--fix [--force]] [--json] Diagnose environment, storage, and platform issues
25
25
  cc help [command] Show usage (optionally for a single command)
@@ -58,47 +58,64 @@ Columns:
58
58
  Profile - profile name
59
59
  LoggedIn - true if .credentials.json exists
60
60
  Email - account email from .claude.json; - if not yet logged in
61
- Storage - 'shared' if projects/ is linked to ~/.claude-shared, 'isolated' otherwise
61
+ Shared - N/4 linked to ~/.claude-shared/ (projects, skills, agents, commands)
62
+ 4/4 = fully shared, 0/4 = fully isolated
62
63
  (sessions/ is transient per-process state and is never shared)
63
64
 
64
- With --json, emits an array of {name, dir, loggedIn, email, storage}.
65
+ With --json, emits an array of {name, dir, loggedIn, email, shared}.
65
66
  `,
66
- migrate: `cc migrate <src> <dest>
67
+ migrate: `cc migrate <src> <dest> [--force]
67
68
 
68
- Copy projects/ from a source to a destination. sessions/ (transient per-process
69
- registry state) is never migrated. This is a COPY — the source is left untouched.
69
+ Copy artifact directories from a source to a destination. This is a COPY — the
70
+ source is left untouched. sessions/ is never migrated (transient per-process state).
70
71
 
71
72
  <src>: default | shared
72
73
  default = ~/.claude/ (Claude's built-in non-profile data)
73
74
  shared = ~/.claude-shared/ (the cc shared pool)
74
75
  <dest>: shared | <profile-name>
75
76
 
76
- On collisions, existing entries at the destination are left alone (the source
77
- entry is skipped). Credential files (.credentials.json, .claude.json) are
78
- never copied — the destination keeps its own auth state.
77
+ Directories copied in all cases:
78
+ projects/, skills/, agents/, commands/
79
+
80
+ Extra files copied only when src=default and dest=<profile>:
81
+ mcp.json, settings.json, CLAUDE.md
82
+ (These carry per-profile config and are NOT copied to shared.)
83
+
84
+ Never copied: .credentials.json, .claude.json, sessions/, plugins/
85
+
86
+ Collision behavior (default): existing entries at the destination are silently
87
+ skipped — the source entry is left alone.
88
+
89
+ --force Overwrite existing entries instead of skipping.
90
+ Warning: existing content at the destination will be cleared.
79
91
 
80
92
  Examples:
81
- cc migrate default work # ~/.claude/projects -> ~/.claude-profiles/work/projects
82
- cc migrate default shared # ~/.claude/projects -> ~/.claude-shared/projects
83
- cc migrate shared work # ~/.claude-shared/projects -> ~/.claude-profiles/work/projects
93
+ cc migrate default work # copy ~/.claude/* to ~/.claude-profiles/work/
94
+ cc migrate default shared # copy dirs to ~/.claude-shared/
95
+ cc migrate shared work # copy shared dirs to ~/.claude-profiles/work/
96
+ cc migrate default work --force # same, overwriting any existing items
84
97
  `,
85
98
  link: `cc link <profile> [<profile>...]
86
99
 
87
- Replace each profile's projects/ with a link to ~/.claude-shared/projects/.
88
- Existing content is migrated into shared first; collisions are renamed with a
89
- __<profile> suffix (never overwritten). sessions/ (transient per-process state)
90
- is intentionally never linked — it stays per-profile.
100
+ Replace each profile's artifact directories with links to ~/.claude-shared/:
101
+ projects/, skills/, agents/, commands/
102
+
103
+ Existing content in each directory is migrated into shared first; collisions
104
+ are renamed with a __<profile> suffix (never overwritten). sessions/ (transient
105
+ per-process state) is intentionally never linked — it stays per-profile.
91
106
 
92
107
  On Windows, link uses directory junctions (no admin/Developer Mode needed).
93
108
  On macOS/Linux, regular symlinks.
94
109
 
95
- Multiple profiles can be linked at once: 'cc link a b c'. Per-profile failures
96
- are reported but don't abort the rest of the batch.
110
+ Multiple profiles can be linked at once: 'cc link a b c'. Failures are handled
111
+ per directory and per profile: one directory failing does not stop the other
112
+ three, and one profile failing does not stop the rest of the batch. A
113
+ per-directory summary is printed; exit code is 1 if anything failed.
97
114
  `,
98
115
  unlink: `cc unlink <profile>
99
116
 
100
- Remove a profile's projects/ link to ~/.claude-shared/ and copy shared content
101
- back into the profile as a private dir.
117
+ Remove each linked artifact directory (projects/, skills/, agents/, commands/)
118
+ and copy the shared content back into the profile as private dirs.
102
119
  `,
103
120
  remove: `cc remove <profile>
104
121
 
@@ -240,15 +257,18 @@ async function cmdUse(rest) {
240
257
  function cmdList(rest) {
241
258
  const { flags } = parseFlagsStrict(rest, ['--json']);
242
259
  const list = profiles.listProfiles();
260
+ const total = share.SHARED_SUBDIRS.length;
243
261
 
244
262
  const enriched = list.map((p) => {
245
- const storage = isLinkLike(path.join(p.dir, 'projects')) ? 'shared' : 'isolated';
263
+ const sharedCount = share.SHARED_SUBDIRS.filter(sub =>
264
+ isLinkLike(path.join(p.dir, sub))
265
+ ).length;
246
266
  return {
247
267
  name: p.name,
248
268
  dir: p.dir,
249
269
  loggedIn: p.loggedIn,
250
270
  email: p.email,
251
- storage,
271
+ shared: sharedCount,
252
272
  };
253
273
  });
254
274
 
@@ -263,13 +283,13 @@ function cmdList(rest) {
263
283
  return 0;
264
284
  }
265
285
 
266
- const rows = [['Profile', 'LoggedIn', 'Email', 'Storage']];
286
+ const rows = [['Profile', 'LoggedIn', 'Email', 'Shared']];
267
287
  for (const p of enriched) {
268
288
  rows.push([
269
289
  p.name,
270
290
  p.loggedIn ? 'true' : 'false',
271
291
  p.email ? p.email : '-',
272
- p.storage,
292
+ `${p.shared}/${total}`,
273
293
  ]);
274
294
  }
275
295
  console.log(renderTable(rows));
@@ -311,29 +331,67 @@ async function cmdRemove(rest) {
311
331
 
312
332
  async function cmdLink(rest) {
313
333
  if (rest.length === 0) throw usageError('Usage: cc link <profile> [<profile>...]');
314
- const failures = [];
334
+ const multi = rest.length > 1;
335
+ const summary = [];
315
336
  for (const name of rest) {
316
- if (rest.length > 1) console.log(`\n[${name}]`);
337
+ if (multi) console.log(`\n[${name}]`);
338
+ let results;
317
339
  try {
318
- share.shareLink(name);
340
+ results = share.shareLink(name);
319
341
  } catch (err) {
342
+ // Profile-level failure (invalid name, profile missing) — nothing linked.
320
343
  process.stderr.write(color.stderr.red(` error: ${err.message}`) + '\n');
321
- failures.push({ name, message: err.message });
344
+ summary.push({ name, linked: 0, total: share.SHARED_SUBDIRS.length, error: err.message });
345
+ continue;
346
+ }
347
+ for (const r of results) {
348
+ if (r.status === 'failed') {
349
+ process.stderr.write(color.stderr.red(` ${r.sub}: failed — ${r.message}`) + '\n');
350
+ } else if (r.status === 'already-linked') {
351
+ console.log(` ${r.sub}: already linked`);
352
+ } else {
353
+ const note = r.moved ? ` (migrated ${r.moved} item(s) into shared)` : '';
354
+ console.log(` ${r.sub}: linked${note}`);
355
+ }
322
356
  }
357
+ const failed = results.filter((r) => r.status === 'failed');
358
+ const linked = results.length - failed.length;
359
+ console.log(` ${linked}/${results.length} directories linked${failed.length ? `, ${failed.length} failed` : ''}.`);
360
+ summary.push({ name, linked, total: results.length, failed: failed.map((f) => f.sub) });
323
361
  }
324
- if (rest.length > 1) {
325
- const ok = rest.length - failures.length;
326
- console.log(`\nSummary: ${ok} succeeded, ${failures.length} failed.`);
327
- for (const f of failures) console.log(` ${f.name}: ${f.message}`);
362
+
363
+ const anyFailure = summary.some((s) => s.error || (s.failed && s.failed.length));
364
+ if (multi) {
365
+ console.log('\nSummary:');
366
+ for (const s of summary) {
367
+ if (s.error) {
368
+ console.log(` ${s.name}: error — ${s.error}`);
369
+ } else if (s.failed && s.failed.length) {
370
+ console.log(` ${s.name}: ${s.linked}/${s.total} linked (failed: ${s.failed.join(', ')})`);
371
+ } else {
372
+ console.log(` ${s.name}: ${s.linked}/${s.total} linked`);
373
+ }
374
+ }
328
375
  }
329
- return failures.length === 0 ? 0 : 1;
376
+ return anyFailure ? 1 : 0;
330
377
  }
331
378
 
332
379
  async function cmdUnlink(rest) {
333
380
  const [name] = rest;
334
381
  if (!name) throw usageError('Usage: cc unlink <profile>');
335
- share.shareUnlink(name);
336
- return 0;
382
+ const results = share.shareUnlink(name);
383
+ let anyFailure = false;
384
+ for (const r of results) {
385
+ if (r.status === 'failed') {
386
+ anyFailure = true;
387
+ process.stderr.write(color.stderr.red(` ${r.sub}: failed — ${r.message}`) + '\n');
388
+ } else if (r.status === 'not-linked') {
389
+ console.log(` ${r.sub}: not linked, skipping`);
390
+ } else {
391
+ console.log(` ${r.sub}: unlinked, content copied back`);
392
+ }
393
+ }
394
+ return anyFailure ? 1 : 0;
337
395
  }
338
396
 
339
397
  async function cmdDoctor(rest) {
@@ -347,10 +405,15 @@ async function cmdDoctor(rest) {
347
405
  }
348
406
 
349
407
  async function cmdMigrate(rest) {
350
- const [src, dest, ...extra] = rest;
351
- if (!src || !dest) throw usageError('Usage: cc migrate <src> <dest>');
408
+ const { flags, positional } = parseFlagsStrict(rest, ['--force'], { allowPositional: true });
409
+ const [src, dest, ...extra] = positional;
410
+ if (!src || !dest) throw usageError('Usage: cc migrate <src> <dest> [--force]');
352
411
  if (extra.length > 0) throw usageError(`Unexpected extra arguments: ${extra.join(' ')}`);
353
- migrateLib.migrate(src, dest);
412
+ const force = flags.has('--force');
413
+ if (force) {
414
+ process.stdout.write('Warning: --force is set. Existing items at the destination will be overwritten and their current content will be cleared.\n');
415
+ }
416
+ migrateLib.migrate(src, dest, { force });
354
417
  return 0;
355
418
  }
356
419
 
package/lib/migrate.js CHANGED
@@ -6,9 +6,14 @@ const os = require('os');
6
6
 
7
7
  const profiles = require('./profiles');
8
8
 
9
- // Only projects/ holds the resumable conversation transcripts. sessions/ is transient
10
- // per-process state and is intentionally not migrated.
11
- const SUBDIRS = ['projects'];
9
+ // sessions/ is transient per-process state — intentionally not migrated.
10
+ // plugins/ carries auth tokens — excluded until an explicit opt-in flag is added.
11
+ const SUBDIRS = ['projects', 'skills', 'agents', 'commands'];
12
+
13
+ // Single files copied only when src=default and dest=<profile>.
14
+ // Not copied to shared: these carry per-profile config / auth tokens.
15
+ const SINGLE_FILES = ['mcp.json', 'settings.json', 'CLAUDE.md'];
16
+
12
17
  const EXCLUDED_FILES = new Set(['.credentials.json', '.claude.json']);
13
18
 
14
19
  function defaultClaudeDir() {
@@ -36,7 +41,7 @@ function resolveDest(dest) {
36
41
  return { kind: 'profile', name: dest, dir: profiles.profileDir(dest) };
37
42
  }
38
43
 
39
- function copyTreeSkipCollisions(srcRoot, dstRoot) {
44
+ function copyTreeSkipCollisions(srcRoot, dstRoot, { force = false } = {}) {
40
45
  if (!fs.existsSync(srcRoot)) return { copied: 0, skipped: 0 };
41
46
  fs.mkdirSync(dstRoot, { recursive: true });
42
47
  let copied = 0;
@@ -46,8 +51,12 @@ function copyTreeSkipCollisions(srcRoot, dstRoot) {
46
51
  const from = path.join(srcRoot, ent.name);
47
52
  const to = path.join(dstRoot, ent.name);
48
53
  if (fs.existsSync(to)) {
49
- skipped += 1;
50
- continue;
54
+ if (force) {
55
+ fs.rmSync(to, { recursive: true, force: true });
56
+ } else {
57
+ skipped += 1;
58
+ continue;
59
+ }
51
60
  }
52
61
  fs.cpSync(from, to, { recursive: true, verbatimSymlinks: true });
53
62
  copied += 1;
@@ -55,7 +64,23 @@ function copyTreeSkipCollisions(srcRoot, dstRoot) {
55
64
  return { copied, skipped };
56
65
  }
57
66
 
58
- function migrate(srcArg, destArg) {
67
+ // Returns 'copied', 'skipped', or 'absent'.
68
+ function copySingleFile(srcDir, dstDir, fileName, { force = false } = {}) {
69
+ const from = path.join(srcDir, fileName);
70
+ if (!fs.existsSync(from)) return 'absent';
71
+ const to = path.join(dstDir, fileName);
72
+ if (fs.existsSync(to)) {
73
+ if (force) {
74
+ fs.rmSync(to, { recursive: true, force: true });
75
+ } else {
76
+ return 'skipped';
77
+ }
78
+ }
79
+ fs.copyFileSync(from, to);
80
+ return 'copied';
81
+ }
82
+
83
+ function migrate(srcArg, destArg, { force = false } = {}) {
59
84
  const src = resolveSource(srcArg);
60
85
  const dest = resolveDest(destArg);
61
86
 
@@ -71,7 +96,7 @@ function migrate(srcArg, destArg) {
71
96
 
72
97
  if (!fs.existsSync(src.dir)) {
73
98
  console.log(`Source ${src.dir} does not exist. Nothing to migrate.`);
74
- return { copied: 0, skipped: 0 };
99
+ return { copied: 0, skipped: 0, files: {} };
75
100
  }
76
101
 
77
102
  let totalCopied = 0;
@@ -80,13 +105,27 @@ function migrate(srcArg, destArg) {
80
105
  const srcSub = path.join(src.dir, sub);
81
106
  const dstSub = path.join(dest.dir, sub);
82
107
  if (!fs.existsSync(srcSub)) continue;
83
- const { copied, skipped } = copyTreeSkipCollisions(srcSub, dstSub);
108
+ const { copied, skipped } = copyTreeSkipCollisions(srcSub, dstSub, { force });
84
109
  console.log(` ${sub}: copied ${copied}, skipped ${skipped}`);
85
110
  totalCopied += copied;
86
111
  totalSkipped += skipped;
87
112
  }
113
+
114
+ // Single config files: only when migrating from default into a profile.
115
+ const fileResults = {};
116
+ if (src.kind === 'default' && dest.kind === 'profile') {
117
+ for (const f of SINGLE_FILES) {
118
+ const result = copySingleFile(src.dir, dest.dir, f, { force });
119
+ fileResults[f] = result;
120
+ const label = result === 'absent' ? 'not present at source' : result;
121
+ console.log(` ${f}: ${label}`);
122
+ if (result === 'copied') totalCopied += 1;
123
+ else if (result === 'skipped') totalSkipped += 1;
124
+ }
125
+ }
126
+
88
127
  console.log(`Done. Total copied: ${totalCopied}, total skipped (already present): ${totalSkipped}.`);
89
- return { copied: totalCopied, skipped: totalSkipped };
128
+ return { copied: totalCopied, skipped: totalSkipped, files: fileResults };
90
129
  }
91
130
 
92
131
  module.exports = {
@@ -95,6 +134,8 @@ module.exports = {
95
134
  resolveSource,
96
135
  resolveDest,
97
136
  copyTreeSkipCollisions,
137
+ copySingleFile,
98
138
  SUBDIRS,
139
+ SINGLE_FILES,
99
140
  EXCLUDED_FILES,
100
141
  };
package/lib/share.js CHANGED
@@ -5,10 +5,11 @@ const path = require('path');
5
5
 
6
6
  const profiles = require('./profiles');
7
7
 
8
- // Only projects/ holds the resumable conversation transcripts (projects/<proj>/<session>.jsonl).
8
+ // Artifact directories that are safe to share across profiles via junctions/symlinks.
9
9
  // sessions/ is transient per-process registry state (PID, cwd, status, timestamps) and
10
10
  // must stay per-profile — sharing it would corrupt Claude's running-process bookkeeping.
11
- const SHARED_SUBDIRS = ['projects'];
11
+ // plugins/ carries auth tokens and is intentionally excluded (future opt-in).
12
+ const SHARED_SUBDIRS = ['projects', 'skills', 'agents', 'commands'];
12
13
 
13
14
  function symlinkType() {
14
15
  return process.platform === 'win32' ? 'junction' : 'dir';
@@ -70,6 +71,11 @@ function migrateIntoShared(profileName, src, sharedTarget) {
70
71
  return moved;
71
72
  }
72
73
 
74
+ // Links each of the four artifact dirs independently. A failure on one
75
+ // directory is recorded and the remaining directories are still attempted —
76
+ // callers get a per-directory result and decide how to report it.
77
+ // Returns [{ sub, status, moved?, message? }]; status is one of:
78
+ // 'linked' | 'already-linked' | 'failed'
73
79
  function shareLink(profileName) {
74
80
  profiles.validateProfileName(profileName);
75
81
  const pdir = profiles.profileDir(profileName);
@@ -79,26 +85,31 @@ function shareLink(profileName) {
79
85
  throw err;
80
86
  }
81
87
  ensureSharedDirs();
88
+ const results = [];
82
89
  for (const sub of SHARED_SUBDIRS) {
83
90
  const target = path.join(profiles.sharedDir(), sub);
84
91
  const link = path.join(pdir, sub);
85
- if (isLink(link)) {
86
- console.log(` ${sub}: already linked`);
87
- continue;
88
- }
89
- if (fs.existsSync(link)) {
90
- const moved = migrateIntoShared(profileName, link, target);
91
- console.log(` ${sub}: migrated ${moved} item(s) into shared`);
92
- }
93
92
  try {
93
+ if (isLink(link)) {
94
+ results.push({ sub, status: 'already-linked' });
95
+ continue;
96
+ }
97
+ let moved = 0;
98
+ if (fs.existsSync(link)) {
99
+ moved = migrateIntoShared(profileName, link, target);
100
+ }
94
101
  fs.symlinkSync(target, link, symlinkType());
102
+ results.push({ sub, status: 'linked', moved });
95
103
  } catch (err) {
96
- throw new Error(`Cannot link ${link} -> ${target}: ${err.message}`);
104
+ results.push({ sub, status: 'failed', message: err.message });
97
105
  }
98
- console.log(` ${sub}: linked -> ${target}`);
99
106
  }
107
+ return results;
100
108
  }
101
109
 
110
+ // Mirror of shareLink: each directory is restored independently.
111
+ // Returns [{ sub, status, message? }]; status is one of:
112
+ // 'unlinked' | 'not-linked' | 'failed'
102
113
  function shareUnlink(profileName) {
103
114
  profiles.validateProfileName(profileName);
104
115
  const pdir = profiles.profileDir(profileName);
@@ -107,22 +118,28 @@ function shareUnlink(profileName) {
107
118
  err.exitCode = 1;
108
119
  throw err;
109
120
  }
121
+ const results = [];
110
122
  for (const sub of SHARED_SUBDIRS) {
111
123
  const link = path.join(pdir, sub);
112
124
  const target = path.join(profiles.sharedDir(), sub);
113
- if (!isLink(link)) {
114
- console.log(` ${sub}: not linked, skipping`);
115
- continue;
116
- }
117
- removeLink(link);
118
- fs.mkdirSync(link, { recursive: true });
119
- if (fs.existsSync(target)) {
120
- for (const ent of fs.readdirSync(target)) {
121
- copyRecursive(path.join(target, ent), path.join(link, ent));
125
+ try {
126
+ if (!isLink(link)) {
127
+ results.push({ sub, status: 'not-linked' });
128
+ continue;
122
129
  }
130
+ removeLink(link);
131
+ fs.mkdirSync(link, { recursive: true });
132
+ if (fs.existsSync(target)) {
133
+ for (const ent of fs.readdirSync(target)) {
134
+ copyRecursive(path.join(target, ent), path.join(link, ent));
135
+ }
136
+ }
137
+ results.push({ sub, status: 'unlinked' });
138
+ } catch (err) {
139
+ results.push({ sub, status: 'failed', message: err.message });
123
140
  }
124
- console.log(` ${sub}: unlinked, content copied back`);
125
141
  }
142
+ return results;
126
143
  }
127
144
 
128
145
  module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cchost",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Host multiple isolated Claude Code accounts on one machine via the `cc` command.",
5
5
  "keywords": [
6
6
  "claude",