cchost 0.3.1 → 0.4.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/CHANGELOG.md +113 -0
- package/README.md +82 -70
- package/lib/banner.js +29 -0
- package/lib/cli.js +341 -151
- package/lib/copy.js +242 -0
- package/lib/doctor.js +154 -22
- package/lib/env.js +20 -0
- package/lib/profiles.js +10 -0
- package/lib/share.js +343 -57
- package/package.json +1 -1
- package/lib/migrate.js +0 -145
package/lib/copy.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
'use strict';
|
|
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.
|
|
6
|
+
//
|
|
7
|
+
// Only direction supported: default → <profile>. The reverse and profile→
|
|
8
|
+
// profile were considered and dropped — they're rare enough that a manual
|
|
9
|
+
// `cp -r` is fine and keeps the surface small.
|
|
10
|
+
//
|
|
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
|
|
13
|
+
// same path as the source). The command is meaningful only when a resource
|
|
14
|
+
// has been unlinked via `cc unlink`.
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
const profiles = require('./profiles');
|
|
20
|
+
const share = require('./share');
|
|
21
|
+
|
|
22
|
+
const EXCLUDED_FILES = new Set(['.credentials.json', '.claude.json', '.cc-env.json']);
|
|
23
|
+
|
|
24
|
+
function defaultClaudeDir() {
|
|
25
|
+
return profiles.defaultClaudeDir();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function usageError(msg) {
|
|
29
|
+
const e = new Error(msg);
|
|
30
|
+
e.exitCode = 2;
|
|
31
|
+
return e;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function rejectSharedKeyword(role) {
|
|
35
|
+
return usageError(
|
|
36
|
+
`'shared' is not a valid copy ${role} in v0.4. ` +
|
|
37
|
+
"The shared pool is now ~/.claude/ itself, reached via per-profile " +
|
|
38
|
+
"symlinks. Use `cc link <profile>` / `cc unlink <profile>` to manage " +
|
|
39
|
+
"which resources are shared.",
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resolveSource(src) {
|
|
44
|
+
if (src === 'default') return { kind: 'default', dir: defaultClaudeDir() };
|
|
45
|
+
if (src === 'shared') throw rejectSharedKeyword('source');
|
|
46
|
+
throw usageError(`Invalid copy source "${src}". Allowed: default.`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resolveDest(dest) {
|
|
50
|
+
if (dest === 'shared') throw rejectSharedKeyword('destination');
|
|
51
|
+
if (dest === 'default') {
|
|
52
|
+
throw usageError('Cannot copy into "default". Allowed destination: <profile>.');
|
|
53
|
+
}
|
|
54
|
+
profiles.validateProfileName(dest);
|
|
55
|
+
return { kind: 'profile', name: dest, dir: profiles.profileDir(dest) };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Return resolved real paths, or null if a path can't be realpath'd. Used to
|
|
59
|
+
// detect "src and dst are the same file/dir under symlinks/hardlinks".
|
|
60
|
+
function realOrNull(p) {
|
|
61
|
+
try {
|
|
62
|
+
return fs.realpathSync(p);
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Compare two existing paths for "are these the same on-disk object?". Honors
|
|
69
|
+
// symlinks (via realpath) and hard links (via inode comparison for files).
|
|
70
|
+
function sameOnDisk(a, b) {
|
|
71
|
+
const ra = realOrNull(a);
|
|
72
|
+
const rb = realOrNull(b);
|
|
73
|
+
if (ra && rb && ra === rb) return true;
|
|
74
|
+
// Inode comparison — covers Windows hard links where realpath does not
|
|
75
|
+
// resolve to the same string but the files share an inode.
|
|
76
|
+
try {
|
|
77
|
+
const sa = fs.statSync(a);
|
|
78
|
+
const sb = fs.statSync(b);
|
|
79
|
+
if (sa.isFile() && sb.isFile() && sa.ino !== 0 && sa.ino === sb.ino && sa.dev === sb.dev) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
// either side missing — not the same
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
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 } = {}) {
|
|
91
|
+
if (!fs.existsSync(srcRoot)) return { copied: 0, skipped: 0 };
|
|
92
|
+
fs.mkdirSync(dstRoot, { recursive: true });
|
|
93
|
+
let copied = 0;
|
|
94
|
+
let skipped = 0;
|
|
95
|
+
for (const ent of fs.readdirSync(srcRoot, { withFileTypes: true })) {
|
|
96
|
+
if (EXCLUDED_FILES.has(ent.name)) continue;
|
|
97
|
+
const from = path.join(srcRoot, ent.name);
|
|
98
|
+
const to = path.join(dstRoot, ent.name);
|
|
99
|
+
if (fs.existsSync(to)) {
|
|
100
|
+
if (force) {
|
|
101
|
+
fs.rmSync(to, { recursive: true, force: true });
|
|
102
|
+
} else {
|
|
103
|
+
skipped += 1;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
fs.cpSync(from, to, { recursive: true, verbatimSymlinks: true });
|
|
108
|
+
copied += 1;
|
|
109
|
+
}
|
|
110
|
+
return { copied, skipped };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Copy a single file. Returns 'copied' | 'skipped' | 'absent'.
|
|
114
|
+
function copySingleFile(srcPath, dstPath, { force = false } = {}) {
|
|
115
|
+
if (!fs.existsSync(srcPath)) return 'absent';
|
|
116
|
+
if (fs.existsSync(dstPath)) {
|
|
117
|
+
if (force) {
|
|
118
|
+
fs.rmSync(dstPath, { recursive: true, force: true });
|
|
119
|
+
} else {
|
|
120
|
+
return 'skipped';
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
fs.mkdirSync(path.dirname(dstPath), { recursive: true });
|
|
124
|
+
fs.copyFileSync(srcPath, dstPath);
|
|
125
|
+
return 'copied';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function formatResourceLabel(resource) {
|
|
129
|
+
return share.isFileResource(resource) ? resource : `${resource}/`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Main entry: copy each of the 7 shared resources from src → dest, honoring
|
|
133
|
+
// 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 } = {}) {
|
|
136
|
+
const src = resolveSource(srcArg);
|
|
137
|
+
const dest = resolveDest(destArg);
|
|
138
|
+
|
|
139
|
+
if (path.resolve(src.dir) === path.resolve(dest.dir)) {
|
|
140
|
+
throw usageError(`Source and destination resolve to the same directory (${src.dir}).`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
profiles.ensureProfileDir(dest.name);
|
|
144
|
+
if (!fs.existsSync(src.dir)) {
|
|
145
|
+
console.log(`Source ${src.dir} does not exist. Nothing to copy.`);
|
|
146
|
+
return { copied: 0, skipped: 0, shared: 0, results: [] };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let totalCopied = 0;
|
|
150
|
+
let totalSkipped = 0;
|
|
151
|
+
let totalShared = 0;
|
|
152
|
+
const results = [];
|
|
153
|
+
|
|
154
|
+
// Compute the column width so the per-line output aligns.
|
|
155
|
+
const maxLabel = share.SHARED_RESOURCES.reduce(
|
|
156
|
+
(m, r) => Math.max(m, formatResourceLabel(r).length),
|
|
157
|
+
0,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
for (const resource of share.SHARED_RESOURCES) {
|
|
161
|
+
const label = formatResourceLabel(resource).padEnd(maxLabel + 1);
|
|
162
|
+
const srcPath = path.join(src.dir, resource);
|
|
163
|
+
const dstPath = path.join(dest.dir, resource);
|
|
164
|
+
const isFile = share.isFileResource(resource);
|
|
165
|
+
|
|
166
|
+
// Already-shared check: same on-disk target via symlink or hard link.
|
|
167
|
+
if (fs.existsSync(dstPath) && fs.existsSync(srcPath) && sameOnDisk(srcPath, dstPath)) {
|
|
168
|
+
console.log(` ${label} shared via symlink — skipped`);
|
|
169
|
+
results.push({ resource, status: 'shared' });
|
|
170
|
+
totalShared += 1;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!fs.existsSync(srcPath)) {
|
|
175
|
+
console.log(` ${label} not present at source`);
|
|
176
|
+
results.push({ resource, status: 'absent' });
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (isFile) {
|
|
181
|
+
const r = copySingleFile(srcPath, dstPath, { force });
|
|
182
|
+
if (r === 'copied') {
|
|
183
|
+
console.log(` ${label} copied`);
|
|
184
|
+
totalCopied += 1;
|
|
185
|
+
} else if (r === 'skipped') {
|
|
186
|
+
console.log(` ${label} skipped (already present — pass --force to overwrite)`);
|
|
187
|
+
totalSkipped += 1;
|
|
188
|
+
} else {
|
|
189
|
+
console.log(` ${label} not present at source`);
|
|
190
|
+
}
|
|
191
|
+
results.push({ resource, status: r });
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const { copied, skipped } = copyDirContents(srcPath, dstPath, { force });
|
|
196
|
+
if (copied === 0 && skipped === 0) {
|
|
197
|
+
console.log(` ${label} empty at source`);
|
|
198
|
+
} else if (copied > 0) {
|
|
199
|
+
const more = skipped > 0 ? `, skipped ${skipped} (already present)` : '';
|
|
200
|
+
console.log(` ${label} copied ${copied} item(s)${more}`);
|
|
201
|
+
} else {
|
|
202
|
+
console.log(
|
|
203
|
+
` ${label} skipped ${skipped} item(s) (all already present — pass --force to overwrite)`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
totalCopied += copied;
|
|
207
|
+
totalSkipped += skipped;
|
|
208
|
+
results.push({ resource, status: 'dir', copied, skipped });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
console.log('');
|
|
212
|
+
if (totalCopied === 0 && totalShared === share.SHARED_RESOURCES.length) {
|
|
213
|
+
console.log(
|
|
214
|
+
`Nothing to copy — all ${share.SHARED_RESOURCES.length} resources are already shared via symlinks.`,
|
|
215
|
+
);
|
|
216
|
+
console.log(
|
|
217
|
+
`Tip: run \`cc unlink ${dest.name}\` first if you want private copies in this profile.`,
|
|
218
|
+
);
|
|
219
|
+
} else if (totalCopied === 0) {
|
|
220
|
+
console.log(
|
|
221
|
+
`Nothing copied. ${totalShared} shared via symlink, ${totalSkipped} skipped (already present).`,
|
|
222
|
+
);
|
|
223
|
+
} else {
|
|
224
|
+
console.log(
|
|
225
|
+
`Done. Copied ${totalCopied} item(s), ${totalShared} already shared via symlinks` +
|
|
226
|
+
(totalSkipped > 0 ? `, ${totalSkipped} skipped (already present)` : '') +
|
|
227
|
+
'.',
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return { copied: totalCopied, skipped: totalSkipped, shared: totalShared, results };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
module.exports = {
|
|
235
|
+
copy,
|
|
236
|
+
defaultClaudeDir,
|
|
237
|
+
resolveSource,
|
|
238
|
+
resolveDest,
|
|
239
|
+
copyDirContents,
|
|
240
|
+
copySingleFile,
|
|
241
|
+
EXCLUDED_FILES,
|
|
242
|
+
};
|
package/lib/doctor.js
CHANGED
|
@@ -5,6 +5,7 @@ const path = require('path');
|
|
|
5
5
|
const os = require('os');
|
|
6
6
|
|
|
7
7
|
const profiles = require('./profiles');
|
|
8
|
+
const share = require('./share');
|
|
8
9
|
const { findClaude, getClaudeVersion, compareVersion, MIN_VERSION } = require('./spawn');
|
|
9
10
|
const color = require('./color');
|
|
10
11
|
|
|
@@ -157,32 +158,162 @@ function checkProfileClaudeJson(prof) {
|
|
|
157
158
|
|
|
158
159
|
function checkProfileLinks(prof) {
|
|
159
160
|
const results = [];
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
161
|
+
for (const resource of share.SHARED_RESOURCES) {
|
|
162
|
+
const pp = path.join(prof.dir, resource);
|
|
163
|
+
const isFile = share.isFileResource(resource);
|
|
164
|
+
const label = isFile ? resource : `${resource}/`;
|
|
165
|
+
const expected = path.join(profiles.defaultClaudeDir(), resource);
|
|
166
|
+
|
|
167
|
+
let stat;
|
|
168
|
+
try { stat = fs.lstatSync(pp); } catch { stat = null; }
|
|
169
|
+
|
|
170
|
+
if (!stat) {
|
|
171
|
+
// Resource missing entirely — safe to auto-create the link.
|
|
172
|
+
results.push(
|
|
173
|
+
mk('Storage', `"${prof.name}" ${label}`, 'warn', 'missing — not linked', {
|
|
174
|
+
safe: true,
|
|
175
|
+
description: `Create link to ~/.claude/${resource}`,
|
|
176
|
+
cmd: null,
|
|
177
|
+
apply: async () => {
|
|
178
|
+
const r = share.shareLink(prof.name, subsetForResource(resource))[0];
|
|
179
|
+
if (r && r.status === 'linked') {
|
|
180
|
+
process.stdout.write(` Linked: ${pp} -> ${expected}\n`);
|
|
181
|
+
} else if (r) {
|
|
182
|
+
process.stdout.write(` Could not link ${pp}: ${r.message || r.status}\n`);
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
}),
|
|
186
|
+
);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
164
189
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
190
|
+
if (stat.isSymbolicLink()) {
|
|
191
|
+
// Symlink — check it resolves correctly.
|
|
192
|
+
if (share.isSharedResource(prof.dir, resource)) {
|
|
193
|
+
results.push(mk('Storage', `"${prof.name}" ${label} link`, 'ok', 'valid'));
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
// Broken or points elsewhere.
|
|
197
|
+
results.push(
|
|
198
|
+
mk('Storage', `"${prof.name}" ${label} link`, 'error', 'broken — target missing or wrong', {
|
|
199
|
+
safe: true,
|
|
200
|
+
description: `Recreate link to ${expected}`,
|
|
201
|
+
cmd: null,
|
|
202
|
+
apply: async () => {
|
|
203
|
+
share.removeLink(pp);
|
|
204
|
+
const r = share.shareLink(prof.name, subsetForResource(resource))[0];
|
|
205
|
+
if (r && (r.status === 'linked' || r.status === 'already-linked')) {
|
|
206
|
+
process.stdout.write(` Recreated: ${pp} -> ${expected}\n`);
|
|
207
|
+
} else if (r) {
|
|
208
|
+
process.stdout.write(` Could not recreate ${pp}: ${r.message || r.status}\n`);
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
}),
|
|
212
|
+
);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Not a symlink. Could be (a) hard link sharing (Windows file fallback)
|
|
217
|
+
// → ok, or (b) genuinely private content → warn (no auto-fix).
|
|
218
|
+
if (share.isSharedResource(prof.dir, resource)) {
|
|
219
|
+
results.push(mk('Storage', `"${prof.name}" ${label} link`, 'ok', 'valid (hard link)'));
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Private — distinguish empty (safe to auto-link) from has-content (warn).
|
|
224
|
+
let hasContent = false;
|
|
225
|
+
try {
|
|
226
|
+
if (isFile) hasContent = stat.size > 0;
|
|
227
|
+
else hasContent = fs.readdirSync(pp).length > 0;
|
|
228
|
+
} catch { hasContent = true; }
|
|
229
|
+
|
|
230
|
+
if (!hasContent) {
|
|
231
|
+
results.push(
|
|
232
|
+
mk('Storage', `"${prof.name}" ${label}`, 'warn', 'private (empty) — not linked', {
|
|
233
|
+
safe: true,
|
|
234
|
+
description: `Replace with link to ~/.claude/${resource}`,
|
|
235
|
+
cmd: null,
|
|
236
|
+
apply: async () => {
|
|
237
|
+
try {
|
|
238
|
+
if (isFile) fs.unlinkSync(pp);
|
|
239
|
+
else fs.rmdirSync(pp);
|
|
240
|
+
} catch { /* fall through */ }
|
|
241
|
+
const r = share.shareLink(prof.name, subsetForResource(resource))[0];
|
|
242
|
+
if (r && r.status === 'linked') {
|
|
243
|
+
process.stdout.write(` Linked: ${pp} -> ${expected}\n`);
|
|
244
|
+
} else if (r) {
|
|
245
|
+
process.stdout.write(` Could not link ${pp}: ${r.message || r.status}\n`);
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
}),
|
|
249
|
+
);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
results.push(
|
|
254
|
+
mk('Storage', `"${prof.name}" ${label}`, 'warn', 'private (has content) — not linked', {
|
|
255
|
+
safe: false,
|
|
256
|
+
description: 'Move or remove the private content, then re-run cc link',
|
|
257
|
+
cmd: `cc copy default ${prof.name} # if you want to merge it the other way; ` +
|
|
258
|
+
`or manually move ${pp} aside, then: cc link ${prof.name}`,
|
|
259
|
+
apply: null,
|
|
260
|
+
}),
|
|
261
|
+
);
|
|
262
|
+
}
|
|
183
263
|
return results;
|
|
184
264
|
}
|
|
185
265
|
|
|
266
|
+
// Build the share.shareLink opts to target exactly one resource.
|
|
267
|
+
function subsetForResource(resource) {
|
|
268
|
+
if (share.PROJECT_SUBDIRS.includes(resource)) return { projects: true };
|
|
269
|
+
if (share.TOOL_SUBDIRS.includes(resource)) {
|
|
270
|
+
// shareLink with {tools:true} targets all 3 tool dirs; that's fine for
|
|
271
|
+
// backfilling, the others are no-ops if already linked.
|
|
272
|
+
return { tools: true };
|
|
273
|
+
}
|
|
274
|
+
if (share.CONFIG_FILES.includes(resource)) return { config: true };
|
|
275
|
+
return {};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// v0.3 → v0.4 migration nudge. If the legacy ~/.claude-shared/ pool still
|
|
279
|
+
// exists, surface it as a warning with a safe auto-fix that runs the
|
|
280
|
+
// one-time migration.
|
|
281
|
+
function checkLegacyPool() {
|
|
282
|
+
const legacy = profiles.sharedDir();
|
|
283
|
+
if (!fs.existsSync(legacy)) return null;
|
|
284
|
+
let hasContent = false;
|
|
285
|
+
try {
|
|
286
|
+
hasContent = fs.readdirSync(legacy).length > 0;
|
|
287
|
+
} catch {
|
|
288
|
+
// Unreadable — still flag it.
|
|
289
|
+
hasContent = true;
|
|
290
|
+
}
|
|
291
|
+
if (!hasContent) {
|
|
292
|
+
// Empty dir — safe to remove silently.
|
|
293
|
+
return mk('Storage', '~/.claude-shared/ (legacy)', 'warn', 'empty dir from a prior cchost version', {
|
|
294
|
+
safe: true,
|
|
295
|
+
description: 'Remove the empty legacy pool',
|
|
296
|
+
cmd: null,
|
|
297
|
+
apply: async () => {
|
|
298
|
+
try { fs.rmdirSync(legacy); process.stdout.write(` Removed ${legacy}\n`); } catch (e) {
|
|
299
|
+
process.stdout.write(` Could not remove ${legacy}: ${e.message}\n`);
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
return mk('Storage', '~/.claude-shared/ (legacy)', 'warn', 'pre-v0.4 pool with content — migrate into ~/.claude/', {
|
|
305
|
+
safe: true,
|
|
306
|
+
description: 'Move legacy pool contents into ~/.claude/ and re-point profile symlinks',
|
|
307
|
+
cmd: null,
|
|
308
|
+
apply: async () => {
|
|
309
|
+
const result = share.migrateLegacyPool({ log: (line) => process.stdout.write(` ${line}\n`) });
|
|
310
|
+
process.stdout.write(
|
|
311
|
+
` Done: ${result.moves.length} item(s) moved, ${result.relinked.length} link(s) repointed.\n`,
|
|
312
|
+
);
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
186
317
|
// --- Platform ---
|
|
187
318
|
|
|
188
319
|
function checkOneDrive() {
|
|
@@ -246,6 +377,7 @@ function runChecks() {
|
|
|
246
377
|
checkCcShadow(),
|
|
247
378
|
checkProfilesDirWritable(),
|
|
248
379
|
];
|
|
380
|
+
results.push(checkLegacyPool());
|
|
249
381
|
for (const prof of profiles.listProfiles()) {
|
|
250
382
|
results.push(checkProfileClaudeJson(prof));
|
|
251
383
|
results.push(...checkProfileLinks(prof));
|
package/lib/env.js
CHANGED
|
@@ -194,6 +194,25 @@ function maskToken(token) {
|
|
|
194
194
|
return `${token.slice(0, 6)}...${token.slice(-4)} (${n} chars)`;
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
// Mask an email so it's safe to paste in screenshots/logs.
|
|
198
|
+
// allen@cyber.co.in → a*************.in
|
|
199
|
+
// Keeps the first character and the final TLD (everything after the last dot);
|
|
200
|
+
// everything in between is replaced with `*` of the original length so the
|
|
201
|
+
// width matches the original. If there's no usable TLD, mask everything after
|
|
202
|
+
// the first char.
|
|
203
|
+
function maskEmail(email) {
|
|
204
|
+
if (!email || typeof email !== 'string') return email;
|
|
205
|
+
if (email.length < 2) return '***';
|
|
206
|
+
const lastDot = email.lastIndexOf('.');
|
|
207
|
+
const first = email[0];
|
|
208
|
+
if (lastDot < 1 || lastDot === email.length - 1) {
|
|
209
|
+
return first + '*'.repeat(email.length - 1);
|
|
210
|
+
}
|
|
211
|
+
const tld = email.slice(lastDot); // includes the leading dot
|
|
212
|
+
const middleLen = lastDot - 1;
|
|
213
|
+
return first + '*'.repeat(middleLen) + tld;
|
|
214
|
+
}
|
|
215
|
+
|
|
197
216
|
function validateBaseUrl(url, warn) {
|
|
198
217
|
if (!/^https?:\/\/.+/.test(url)) {
|
|
199
218
|
throw usageError(`Invalid base_url "${url}": must start with http:// or https://`);
|
|
@@ -446,6 +465,7 @@ module.exports = {
|
|
|
446
465
|
hostOf,
|
|
447
466
|
endpointSummary,
|
|
448
467
|
maskToken,
|
|
468
|
+
maskEmail,
|
|
449
469
|
validateBaseUrl,
|
|
450
470
|
validateToken,
|
|
451
471
|
validateField,
|
package/lib/profiles.js
CHANGED
|
@@ -17,6 +17,15 @@ function baseDir() {
|
|
|
17
17
|
return path.join(os.homedir(), '.claude-profiles');
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
// ~/.claude/ — Claude Code's own default config dir. Since v0.4 this is also
|
|
21
|
+
// the shared artifact pool: every profile's projects/skills/agents/commands
|
|
22
|
+
// symlink back here, so one set of tools is visible to every profile.
|
|
23
|
+
function defaultClaudeDir() {
|
|
24
|
+
return path.join(os.homedir(), '.claude');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ~/.claude-shared/ — legacy pool from v0.2/0.3. Kept for migration detection
|
|
28
|
+
// only; new symlinks go to defaultClaudeDir(). Removed once migration runs.
|
|
20
29
|
function sharedDir() {
|
|
21
30
|
return path.join(os.homedir(), '.claude-shared');
|
|
22
31
|
}
|
|
@@ -143,6 +152,7 @@ function listProfiles() {
|
|
|
143
152
|
module.exports = {
|
|
144
153
|
baseDir,
|
|
145
154
|
sharedDir,
|
|
155
|
+
defaultClaudeDir,
|
|
146
156
|
profileDir,
|
|
147
157
|
validateProfileName,
|
|
148
158
|
ensureBaseDir,
|