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/CHANGELOG.md +112 -0
- package/README.md +114 -44
- package/bin/cc.js +6 -1
- package/lib/actions.js +290 -0
- package/lib/banner.js +3 -1
- package/lib/ccforward.js +77 -0
- package/lib/cli.js +363 -104
- package/lib/color.js +3 -1
- package/lib/copy.js +43 -41
- package/lib/doctor.js +211 -68
- package/lib/env.js +65 -9
- package/lib/profiles.js +47 -14
- package/lib/share.js +124 -6
- package/lib/spawn.js +2 -1
- package/lib/ui/login.html +117 -0
- package/lib/ui/page.html +1171 -0
- package/lib/ui/server.js +384 -0
- package/lib/ui/tokens.css +80 -0
- package/package.json +3 -2
package/lib/color.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
function colorEnabled(stream) {
|
|
4
|
-
|
|
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
|
-
// `
|
|
4
|
-
// ~/.claude/ (default) into a profile. Renamed from `
|
|
5
|
-
// `
|
|
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
|
-
// `
|
|
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 `
|
|
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 `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
183
|
+
log(` ${label} ${copiedWord}`);
|
|
184
184
|
totalCopied += 1;
|
|
185
185
|
} else if (r === 'skipped') {
|
|
186
|
-
|
|
186
|
+
log(` ${label} skipped (already present — pass --force to overwrite)`);
|
|
187
187
|
totalSkipped += 1;
|
|
188
188
|
} else {
|
|
189
|
-
|
|
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
|
-
|
|
197
|
+
log(` ${label} empty at source`);
|
|
198
198
|
} else if (copied > 0) {
|
|
199
199
|
const more = skipped > 0 ? `, skipped ${skipped} (already present)` : '';
|
|
200
|
-
|
|
200
|
+
log(` ${label} ${copiedWord} ${copied} item(s)${more}`);
|
|
201
201
|
} else {
|
|
202
|
-
|
|
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
|
-
|
|
211
|
+
log('');
|
|
212
212
|
if (totalCopied === 0 && totalShared === share.SHARED_RESOURCES.length) {
|
|
213
|
-
|
|
213
|
+
log(
|
|
214
214
|
`Nothing to copy — all ${share.SHARED_RESOURCES.length} resources are already shared via symlinks.`,
|
|
215
215
|
);
|
|
216
|
-
|
|
217
|
-
`Tip: run \`
|
|
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
|
-
|
|
221
|
-
|
|
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
|
-
|
|
225
|
-
|
|
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
|
);
|