cchost 0.4.4 → 0.6.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/lib/color.js CHANGED
@@ -1,7 +1,9 @@
1
1
  'use strict';
2
2
 
3
3
  function colorEnabled(stream) {
4
- if (process.env.NO_COLOR && process.env.NO_COLOR.length > 0) return false;
4
+ // Per no-color.org, NO_COLOR disables color only when set to a non-empty
5
+ // value; NO_COLOR="" leaves color enabled.
6
+ if (process.env.NO_COLOR) return false;
5
7
  if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== '0') return true;
6
8
  const s = stream || process.stdout;
7
9
  return Boolean(s && s.isTTY);
package/lib/copy.js CHANGED
@@ -1,17 +1,17 @@
1
1
  'use strict';
2
2
 
3
- // `cc copy` — copy mcp.json / settings.json / CLAUDE.md / artifact dirs from
4
- // ~/.claude/ (default) into a profile. Renamed from `cc migrate` in v0.4.1;
5
- // `cc migrate` still works as a deprecation alias.
3
+ // `cchost copy` — copy mcp.json / settings.json / CLAUDE.md / artifact dirs from
4
+ // ~/.claude/ (default) into a profile. Renamed from `cchost migrate` in v0.4.1;
5
+ // `cchost migrate` still works as a deprecation alias.
6
6
  //
7
7
  // Only direction supported: default → <profile>. The reverse and profile→
8
8
  // profile were considered and dropped — they're rare enough that a manual
9
9
  // `cp -r` is fine and keeps the surface small.
10
10
  //
11
11
  // Most v0.4 profiles have all 7 resources symlinked into the pool, so
12
- // `cc copy default foo` is usually a no-op (every resource resolves to the
12
+ // `cchost copy default foo` is usually a no-op (every resource resolves to the
13
13
  // same path as the source). The command is meaningful only when a resource
14
- // has been unlinked via `cc unlink`.
14
+ // has been unlinked via `cchost unlink`.
15
15
 
16
16
  const fs = require('fs');
17
17
  const path = require('path');
@@ -35,7 +35,7 @@ function rejectSharedKeyword(role) {
35
35
  return usageError(
36
36
  `'shared' is not a valid copy ${role} in v0.4. ` +
37
37
  "The shared pool is now ~/.claude/ itself, reached via per-profile " +
38
- "symlinks. Use `cc link <profile>` / `cc unlink <profile>` to manage " +
38
+ "symlinks. Use `cchost link <profile>` / `cchost unlink <profile>` to manage " +
39
39
  "which resources are shared.",
40
40
  );
41
41
  }
@@ -86,10 +86,11 @@ function sameOnDisk(a, b) {
86
86
  }
87
87
 
88
88
  // Copy directory contents item-by-item with skip-on-collision (default) or
89
- // overwrite (force). Returns { copied, skipped }.
90
- function copyDirContents(srcRoot, dstRoot, { force = false } = {}) {
89
+ // overwrite (force). Returns { copied, skipped }. With dryRun the counts are
90
+ // what a real run would produce, and nothing is written.
91
+ function copyDirContents(srcRoot, dstRoot, { force = false, dryRun = false } = {}) {
91
92
  if (!fs.existsSync(srcRoot)) return { copied: 0, skipped: 0 };
92
- fs.mkdirSync(dstRoot, { recursive: true });
93
+ if (!dryRun) fs.mkdirSync(dstRoot, { recursive: true });
93
94
  let copied = 0;
94
95
  let skipped = 0;
95
96
  for (const ent of fs.readdirSync(srcRoot, { withFileTypes: true })) {
@@ -97,29 +98,26 @@ function copyDirContents(srcRoot, dstRoot, { force = false } = {}) {
97
98
  const from = path.join(srcRoot, ent.name);
98
99
  const to = path.join(dstRoot, ent.name);
99
100
  if (fs.existsSync(to)) {
100
- if (force) {
101
- fs.rmSync(to, { recursive: true, force: true });
102
- } else {
101
+ if (!force) {
103
102
  skipped += 1;
104
103
  continue;
105
104
  }
105
+ if (!dryRun) fs.rmSync(to, { recursive: true, force: true });
106
106
  }
107
- fs.cpSync(from, to, { recursive: true, verbatimSymlinks: true });
107
+ if (!dryRun) fs.cpSync(from, to, { recursive: true, verbatimSymlinks: true });
108
108
  copied += 1;
109
109
  }
110
110
  return { copied, skipped };
111
111
  }
112
112
 
113
113
  // Copy a single file. Returns 'copied' | 'skipped' | 'absent'.
114
- function copySingleFile(srcPath, dstPath, { force = false } = {}) {
114
+ function copySingleFile(srcPath, dstPath, { force = false, dryRun = false } = {}) {
115
115
  if (!fs.existsSync(srcPath)) return 'absent';
116
116
  if (fs.existsSync(dstPath)) {
117
- if (force) {
118
- fs.rmSync(dstPath, { recursive: true, force: true });
119
- } else {
120
- return 'skipped';
121
- }
117
+ if (!force) return 'skipped';
118
+ if (!dryRun) fs.rmSync(dstPath, { recursive: true, force: true });
122
119
  }
120
+ if (dryRun) return 'copied';
123
121
  fs.mkdirSync(path.dirname(dstPath), { recursive: true });
124
122
  fs.copyFileSync(srcPath, dstPath);
125
123
  return 'copied';
@@ -131,8 +129,9 @@ function formatResourceLabel(resource) {
131
129
 
132
130
  // Main entry: copy each of the 7 shared resources from src → dest, honoring
133
131
  // the v0.4 symlink semantics. Logs per-resource progress and a closing
134
- // summary line. Returns a structured result for tests.
135
- function copy(srcArg, destArg, { force = false } = {}) {
132
+ // summary line. Returns a structured result for tests. With dryRun the same
133
+ // report is printed in the conditional ("would copy") and nothing is written.
134
+ function copy(srcArg, destArg, { force = false, dryRun = false, log = console.log } = {}) {
136
135
  const src = resolveSource(srcArg);
137
136
  const dest = resolveDest(destArg);
138
137
 
@@ -140,9 +139,10 @@ function copy(srcArg, destArg, { force = false } = {}) {
140
139
  throw usageError(`Source and destination resolve to the same directory (${src.dir}).`);
141
140
  }
142
141
 
143
- profiles.ensureProfileDir(dest.name);
142
+ if (!dryRun) profiles.ensureProfileDir(dest.name);
143
+ const copiedWord = dryRun ? 'would copy' : 'copied';
144
144
  if (!fs.existsSync(src.dir)) {
145
- console.log(`Source ${src.dir} does not exist. Nothing to copy.`);
145
+ log(`Source ${src.dir} does not exist. Nothing to copy.`);
146
146
  return { copied: 0, skipped: 0, shared: 0, results: [] };
147
147
  }
148
148
 
@@ -165,41 +165,41 @@ function copy(srcArg, destArg, { force = false } = {}) {
165
165
 
166
166
  // Already-shared check: same on-disk target via symlink or hard link.
167
167
  if (fs.existsSync(dstPath) && fs.existsSync(srcPath) && sameOnDisk(srcPath, dstPath)) {
168
- console.log(` ${label} shared via symlink — skipped`);
168
+ log(` ${label} shared via symlink — skipped`);
169
169
  results.push({ resource, status: 'shared' });
170
170
  totalShared += 1;
171
171
  continue;
172
172
  }
173
173
 
174
174
  if (!fs.existsSync(srcPath)) {
175
- console.log(` ${label} not present at source`);
175
+ log(` ${label} not present at source`);
176
176
  results.push({ resource, status: 'absent' });
177
177
  continue;
178
178
  }
179
179
 
180
180
  if (isFile) {
181
- const r = copySingleFile(srcPath, dstPath, { force });
181
+ const r = copySingleFile(srcPath, dstPath, { force, dryRun });
182
182
  if (r === 'copied') {
183
- console.log(` ${label} copied`);
183
+ log(` ${label} ${copiedWord}`);
184
184
  totalCopied += 1;
185
185
  } else if (r === 'skipped') {
186
- console.log(` ${label} skipped (already present — pass --force to overwrite)`);
186
+ log(` ${label} skipped (already present — pass --force to overwrite)`);
187
187
  totalSkipped += 1;
188
188
  } else {
189
- console.log(` ${label} not present at source`);
189
+ log(` ${label} not present at source`);
190
190
  }
191
191
  results.push({ resource, status: r });
192
192
  continue;
193
193
  }
194
194
 
195
- const { copied, skipped } = copyDirContents(srcPath, dstPath, { force });
195
+ const { copied, skipped } = copyDirContents(srcPath, dstPath, { force, dryRun });
196
196
  if (copied === 0 && skipped === 0) {
197
- console.log(` ${label} empty at source`);
197
+ log(` ${label} empty at source`);
198
198
  } else if (copied > 0) {
199
199
  const more = skipped > 0 ? `, skipped ${skipped} (already present)` : '';
200
- console.log(` ${label} copied ${copied} item(s)${more}`);
200
+ log(` ${label} ${copiedWord} ${copied} item(s)${more}`);
201
201
  } else {
202
- console.log(
202
+ log(
203
203
  ` ${label} skipped ${skipped} item(s) (all already present — pass --force to overwrite)`,
204
204
  );
205
205
  }
@@ -208,21 +208,23 @@ function copy(srcArg, destArg, { force = false } = {}) {
208
208
  results.push({ resource, status: 'dir', copied, skipped });
209
209
  }
210
210
 
211
- console.log('');
211
+ log('');
212
212
  if (totalCopied === 0 && totalShared === share.SHARED_RESOURCES.length) {
213
- console.log(
213
+ log(
214
214
  `Nothing to copy — all ${share.SHARED_RESOURCES.length} resources are already shared via symlinks.`,
215
215
  );
216
- console.log(
217
- `Tip: run \`cc unlink ${dest.name}\` first if you want private copies in this profile.`,
216
+ log(
217
+ `Tip: run \`cchost unlink ${dest.name}\` first if you want private copies in this profile.`,
218
218
  );
219
219
  } else if (totalCopied === 0) {
220
- console.log(
221
- `Nothing copied. ${totalShared} shared via symlink, ${totalSkipped} skipped (already present).`,
220
+ log(
221
+ `${dryRun ? 'Nothing would be copied' : 'Nothing copied'}. ` +
222
+ `${totalShared} shared via symlink, ${totalSkipped} skipped (already present).`,
222
223
  );
223
224
  } else {
224
- console.log(
225
- `Done. Copied ${totalCopied} item(s), ${totalShared} already shared via symlinks` +
225
+ log(
226
+ `${dryRun ? 'Dry run: would copy' : 'Done. Copied'} ${totalCopied} item(s), ` +
227
+ `${totalShared} already shared via symlinks` +
226
228
  (totalSkipped > 0 ? `, ${totalSkipped} skipped (already present)` : '') +
227
229
  '.',
228
230
  );