cchost 0.1.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/LICENSE +21 -0
- package/README.md +115 -0
- package/bin/cc.js +16 -0
- package/lib/cli.js +458 -0
- package/lib/color.js +26 -0
- package/lib/format.js +26 -0
- package/lib/migrate.js +98 -0
- package/lib/profiles.js +240 -0
- package/lib/share.js +131 -0
- package/lib/spawn.js +229 -0
- package/package.json +46 -0
package/lib/migrate.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
const profiles = require('./profiles');
|
|
8
|
+
|
|
9
|
+
const SUBDIRS = ['projects', 'sessions'];
|
|
10
|
+
const EXCLUDED_FILES = new Set(['.credentials.json', '.claude.json']);
|
|
11
|
+
|
|
12
|
+
function defaultClaudeDir() {
|
|
13
|
+
return path.join(os.homedir(), '.claude');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function usageError(msg) {
|
|
17
|
+
const e = new Error(msg);
|
|
18
|
+
e.exitCode = 2;
|
|
19
|
+
return e;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function resolveSource(src) {
|
|
23
|
+
if (src === 'default') return { kind: 'default', dir: defaultClaudeDir() };
|
|
24
|
+
if (src === 'shared') return { kind: 'shared', dir: profiles.sharedDir() };
|
|
25
|
+
throw usageError(`Invalid migrate source "${src}". Allowed: default, shared.`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolveDest(dest) {
|
|
29
|
+
if (dest === 'shared') return { kind: 'shared', dir: profiles.sharedDir() };
|
|
30
|
+
if (dest === 'default') {
|
|
31
|
+
throw usageError('Cannot migrate into "default". Allowed destinations: shared, <profile>.');
|
|
32
|
+
}
|
|
33
|
+
profiles.validateProfileName(dest);
|
|
34
|
+
return { kind: 'profile', name: dest, dir: profiles.profileDir(dest) };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function copyTreeSkipCollisions(srcRoot, dstRoot) {
|
|
38
|
+
if (!fs.existsSync(srcRoot)) return { copied: 0, skipped: 0 };
|
|
39
|
+
fs.mkdirSync(dstRoot, { recursive: true });
|
|
40
|
+
let copied = 0;
|
|
41
|
+
let skipped = 0;
|
|
42
|
+
for (const ent of fs.readdirSync(srcRoot, { withFileTypes: true })) {
|
|
43
|
+
if (EXCLUDED_FILES.has(ent.name)) continue;
|
|
44
|
+
const from = path.join(srcRoot, ent.name);
|
|
45
|
+
const to = path.join(dstRoot, ent.name);
|
|
46
|
+
if (fs.existsSync(to)) {
|
|
47
|
+
skipped += 1;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
fs.cpSync(from, to, { recursive: true, verbatimSymlinks: true });
|
|
51
|
+
copied += 1;
|
|
52
|
+
}
|
|
53
|
+
return { copied, skipped };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function migrate(srcArg, destArg) {
|
|
57
|
+
const src = resolveSource(srcArg);
|
|
58
|
+
const dest = resolveDest(destArg);
|
|
59
|
+
|
|
60
|
+
if (path.resolve(src.dir) === path.resolve(dest.dir)) {
|
|
61
|
+
throw usageError(`Source and destination resolve to the same directory (${src.dir}).`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (dest.kind === 'profile') {
|
|
65
|
+
profiles.ensureProfileDir(dest.name);
|
|
66
|
+
} else {
|
|
67
|
+
fs.mkdirSync(dest.dir, { recursive: true });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!fs.existsSync(src.dir)) {
|
|
71
|
+
console.log(`Source ${src.dir} does not exist. Nothing to migrate.`);
|
|
72
|
+
return { copied: 0, skipped: 0 };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let totalCopied = 0;
|
|
76
|
+
let totalSkipped = 0;
|
|
77
|
+
for (const sub of SUBDIRS) {
|
|
78
|
+
const srcSub = path.join(src.dir, sub);
|
|
79
|
+
const dstSub = path.join(dest.dir, sub);
|
|
80
|
+
if (!fs.existsSync(srcSub)) continue;
|
|
81
|
+
const { copied, skipped } = copyTreeSkipCollisions(srcSub, dstSub);
|
|
82
|
+
console.log(` ${sub}: copied ${copied}, skipped ${skipped}`);
|
|
83
|
+
totalCopied += copied;
|
|
84
|
+
totalSkipped += skipped;
|
|
85
|
+
}
|
|
86
|
+
console.log(`Done. Total copied: ${totalCopied}, total skipped (already present): ${totalSkipped}.`);
|
|
87
|
+
return { copied: totalCopied, skipped: totalSkipped };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
migrate,
|
|
92
|
+
defaultClaudeDir,
|
|
93
|
+
resolveSource,
|
|
94
|
+
resolveDest,
|
|
95
|
+
copyTreeSkipCollisions,
|
|
96
|
+
SUBDIRS,
|
|
97
|
+
EXCLUDED_FILES,
|
|
98
|
+
};
|
package/lib/profiles.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
const RESERVED_WIN = new Set([
|
|
8
|
+
'CON', 'PRN', 'AUX', 'NUL',
|
|
9
|
+
'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',
|
|
10
|
+
'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
const MAX_NAME_LEN = 64;
|
|
14
|
+
const INVALID_CHARS_RE = /[\\/:*?"<>|\x00-\x1f]/;
|
|
15
|
+
|
|
16
|
+
const ALIAS_FILE = '.cc-alias.json';
|
|
17
|
+
const ALLOWED_ALIAS_KEYS = ['email', 'org', 'name'];
|
|
18
|
+
const OAUTH_FIELD_MAP = {
|
|
19
|
+
email: 'emailAddress',
|
|
20
|
+
org: 'organizationName',
|
|
21
|
+
name: 'displayName',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function baseDir() {
|
|
25
|
+
return path.join(os.homedir(), '.claude-profiles');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sharedDir() {
|
|
29
|
+
return path.join(os.homedir(), '.claude-shared');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function profileDir(name) {
|
|
33
|
+
return path.join(baseDir(), name);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function usageError(msg) {
|
|
37
|
+
const e = new Error(msg);
|
|
38
|
+
e.exitCode = 2;
|
|
39
|
+
return e;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function validateProfileName(name) {
|
|
43
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
44
|
+
throw usageError('Profile name is required.');
|
|
45
|
+
}
|
|
46
|
+
if (name.length > MAX_NAME_LEN) {
|
|
47
|
+
throw usageError(`Profile name must be ${MAX_NAME_LEN} characters or fewer.`);
|
|
48
|
+
}
|
|
49
|
+
if (/\s/.test(name)) {
|
|
50
|
+
throw usageError('Profile name cannot contain whitespace.');
|
|
51
|
+
}
|
|
52
|
+
if (INVALID_CHARS_RE.test(name)) {
|
|
53
|
+
throw usageError('Profile name contains disallowed characters (path separators, quotes, or control chars).');
|
|
54
|
+
}
|
|
55
|
+
if (name === '.' || name === '..' || name.startsWith('.')) {
|
|
56
|
+
throw usageError('Profile name cannot start with a dot.');
|
|
57
|
+
}
|
|
58
|
+
if (name.startsWith('-')) {
|
|
59
|
+
throw usageError(`Profile name cannot start with "-" (looks like a flag): ${name}`);
|
|
60
|
+
}
|
|
61
|
+
if (RESERVED_WIN.has(name.toUpperCase())) {
|
|
62
|
+
throw usageError(`"${name}" is a reserved name on Windows.`);
|
|
63
|
+
}
|
|
64
|
+
return name;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function ensureBaseDir() {
|
|
68
|
+
try {
|
|
69
|
+
fs.mkdirSync(baseDir(), { recursive: true });
|
|
70
|
+
} catch (err) {
|
|
71
|
+
throw new Error(`Cannot create profile base directory ${baseDir()}: ${err.message}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function ensureProfileDir(name) {
|
|
76
|
+
validateProfileName(name);
|
|
77
|
+
ensureBaseDir();
|
|
78
|
+
const dir = profileDir(name);
|
|
79
|
+
let created = false;
|
|
80
|
+
if (!fs.existsSync(dir)) {
|
|
81
|
+
try {
|
|
82
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
83
|
+
} catch (err) {
|
|
84
|
+
throw new Error(`Cannot create profile directory ${dir}: ${err.message}`);
|
|
85
|
+
}
|
|
86
|
+
created = true;
|
|
87
|
+
}
|
|
88
|
+
return { dir, created };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function aliasPath(dir) {
|
|
92
|
+
return path.join(dir, ALIAS_FILE);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function readAlias(dir) {
|
|
96
|
+
try {
|
|
97
|
+
const raw = fs.readFileSync(aliasPath(dir), 'utf8');
|
|
98
|
+
if (!raw.trim()) return {};
|
|
99
|
+
const data = JSON.parse(raw);
|
|
100
|
+
if (!data || typeof data !== 'object') return {};
|
|
101
|
+
const out = {};
|
|
102
|
+
for (const k of ALLOWED_ALIAS_KEYS) {
|
|
103
|
+
if (typeof data[k] === 'string' && data[k].length > 0) out[k] = data[k];
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
} catch {
|
|
107
|
+
return {};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function writeAlias(dir, data) {
|
|
112
|
+
fs.writeFileSync(aliasPath(dir), JSON.stringify(data, null, 2));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function addProfile(name, aliasFlags) {
|
|
116
|
+
validateProfileName(name);
|
|
117
|
+
const dir = profileDir(name);
|
|
118
|
+
if (fs.existsSync(dir)) {
|
|
119
|
+
const e = new Error(`Profile "${name}" already exists at ${dir}.`);
|
|
120
|
+
e.exitCode = 1;
|
|
121
|
+
throw e;
|
|
122
|
+
}
|
|
123
|
+
const alias = {};
|
|
124
|
+
for (const [k, v] of Object.entries(aliasFlags || {})) {
|
|
125
|
+
if (!ALLOWED_ALIAS_KEYS.includes(k)) {
|
|
126
|
+
throw usageError(`Unknown flag: --${k}. Allowed: --email, --org, --name.`);
|
|
127
|
+
}
|
|
128
|
+
if (typeof v !== 'string' || v.length === 0) {
|
|
129
|
+
throw usageError(`Flag --${k} requires a non-empty value.`);
|
|
130
|
+
}
|
|
131
|
+
alias[k] = v;
|
|
132
|
+
}
|
|
133
|
+
ensureBaseDir();
|
|
134
|
+
fs.mkdirSync(dir);
|
|
135
|
+
if (Object.keys(alias).length > 0) {
|
|
136
|
+
writeAlias(dir, alias);
|
|
137
|
+
}
|
|
138
|
+
return { dir, alias };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function applyAlias(dir) {
|
|
142
|
+
const alias = readAlias(dir);
|
|
143
|
+
if (Object.keys(alias).length === 0) return false;
|
|
144
|
+
const claudeJsonPath = path.join(dir, '.claude.json');
|
|
145
|
+
if (!fs.existsSync(claudeJsonPath)) return false;
|
|
146
|
+
let data;
|
|
147
|
+
try {
|
|
148
|
+
const raw = fs.readFileSync(claudeJsonPath, 'utf8');
|
|
149
|
+
if (!raw.trim()) return false;
|
|
150
|
+
data = JSON.parse(raw);
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
if (!data || typeof data !== 'object') return false;
|
|
155
|
+
if (!data.oauthAccount || typeof data.oauthAccount !== 'object') return false;
|
|
156
|
+
let mutated = false;
|
|
157
|
+
for (const [k, v] of Object.entries(alias)) {
|
|
158
|
+
const oauthKey = OAUTH_FIELD_MAP[k];
|
|
159
|
+
if (!oauthKey) continue;
|
|
160
|
+
if (data.oauthAccount[oauthKey] !== v) {
|
|
161
|
+
data.oauthAccount[oauthKey] = v;
|
|
162
|
+
mutated = true;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (mutated) {
|
|
166
|
+
try {
|
|
167
|
+
fs.writeFileSync(claudeJsonPath, JSON.stringify(data, null, 2));
|
|
168
|
+
return true;
|
|
169
|
+
} catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function readEmail(dir) {
|
|
177
|
+
try {
|
|
178
|
+
const raw = fs.readFileSync(path.join(dir, '.claude.json'), 'utf8');
|
|
179
|
+
if (!raw.trim()) return '';
|
|
180
|
+
const data = JSON.parse(raw);
|
|
181
|
+
if (data && data.oauthAccount && typeof data.oauthAccount.emailAddress === 'string') {
|
|
182
|
+
return data.oauthAccount.emailAddress;
|
|
183
|
+
}
|
|
184
|
+
return '';
|
|
185
|
+
} catch {
|
|
186
|
+
return '';
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function hasCredentials(dir) {
|
|
191
|
+
try {
|
|
192
|
+
return fs.existsSync(path.join(dir, '.credentials.json'));
|
|
193
|
+
} catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function listProfiles() {
|
|
199
|
+
const base = baseDir();
|
|
200
|
+
if (!fs.existsSync(base)) return [];
|
|
201
|
+
let entries;
|
|
202
|
+
try {
|
|
203
|
+
entries = fs.readdirSync(base, { withFileTypes: true });
|
|
204
|
+
} catch (err) {
|
|
205
|
+
throw new Error(`Cannot read ${base}: ${err.message}`);
|
|
206
|
+
}
|
|
207
|
+
const out = [];
|
|
208
|
+
for (const ent of entries) {
|
|
209
|
+
if (!ent.isDirectory()) continue;
|
|
210
|
+
const name = ent.name;
|
|
211
|
+
const dir = path.join(base, name);
|
|
212
|
+
out.push({
|
|
213
|
+
name,
|
|
214
|
+
dir,
|
|
215
|
+
loggedIn: hasCredentials(dir),
|
|
216
|
+
email: readEmail(dir),
|
|
217
|
+
alias: readAlias(dir),
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
out.sort((a, b) => a.name.localeCompare(b.name));
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
module.exports = {
|
|
225
|
+
baseDir,
|
|
226
|
+
sharedDir,
|
|
227
|
+
profileDir,
|
|
228
|
+
validateProfileName,
|
|
229
|
+
ensureBaseDir,
|
|
230
|
+
ensureProfileDir,
|
|
231
|
+
readEmail,
|
|
232
|
+
hasCredentials,
|
|
233
|
+
listProfiles,
|
|
234
|
+
addProfile,
|
|
235
|
+
readAlias,
|
|
236
|
+
writeAlias,
|
|
237
|
+
applyAlias,
|
|
238
|
+
ALLOWED_ALIAS_KEYS,
|
|
239
|
+
OAUTH_FIELD_MAP,
|
|
240
|
+
};
|
package/lib/share.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const profiles = require('./profiles');
|
|
7
|
+
|
|
8
|
+
const SHARED_SUBDIRS = ['projects', 'sessions'];
|
|
9
|
+
|
|
10
|
+
function symlinkType() {
|
|
11
|
+
return process.platform === 'win32' ? 'junction' : 'dir';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isLink(p) {
|
|
15
|
+
try {
|
|
16
|
+
return fs.lstatSync(p).isSymbolicLink();
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function removeLink(p) {
|
|
23
|
+
try {
|
|
24
|
+
fs.unlinkSync(p);
|
|
25
|
+
return;
|
|
26
|
+
} catch (err) {
|
|
27
|
+
if (process.platform !== 'win32') throw err;
|
|
28
|
+
}
|
|
29
|
+
fs.rmdirSync(p);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function copyRecursive(src, dst) {
|
|
33
|
+
fs.cpSync(src, dst, { recursive: true, verbatimSymlinks: true });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function ensureSharedDirs() {
|
|
37
|
+
const base = profiles.sharedDir();
|
|
38
|
+
for (const sub of SHARED_SUBDIRS) {
|
|
39
|
+
try {
|
|
40
|
+
fs.mkdirSync(path.join(base, sub), { recursive: true });
|
|
41
|
+
} catch (err) {
|
|
42
|
+
throw new Error(`Cannot create ${path.join(base, sub)}: ${err.message}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return base;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function migrateIntoShared(profileName, src, sharedTarget) {
|
|
49
|
+
if (!fs.existsSync(src)) return 0;
|
|
50
|
+
fs.mkdirSync(sharedTarget, { recursive: true });
|
|
51
|
+
let moved = 0;
|
|
52
|
+
for (const ent of fs.readdirSync(src)) {
|
|
53
|
+
const from = path.join(src, ent);
|
|
54
|
+
let to = path.join(sharedTarget, ent);
|
|
55
|
+
if (fs.existsSync(to)) {
|
|
56
|
+
to = path.join(sharedTarget, `${ent}__${profileName}`);
|
|
57
|
+
let i = 2;
|
|
58
|
+
while (fs.existsSync(to)) {
|
|
59
|
+
to = path.join(sharedTarget, `${ent}__${profileName}_${i}`);
|
|
60
|
+
i += 1;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
fs.renameSync(from, to);
|
|
64
|
+
moved += 1;
|
|
65
|
+
}
|
|
66
|
+
fs.rmdirSync(src);
|
|
67
|
+
return moved;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function shareLink(profileName) {
|
|
71
|
+
profiles.validateProfileName(profileName);
|
|
72
|
+
const pdir = profiles.profileDir(profileName);
|
|
73
|
+
if (!fs.existsSync(pdir)) {
|
|
74
|
+
const err = new Error(`Profile "${profileName}" does not exist. Create it with \`cc add ${profileName}\` first.`);
|
|
75
|
+
err.exitCode = 1;
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
ensureSharedDirs();
|
|
79
|
+
for (const sub of SHARED_SUBDIRS) {
|
|
80
|
+
const target = path.join(profiles.sharedDir(), sub);
|
|
81
|
+
const link = path.join(pdir, sub);
|
|
82
|
+
if (isLink(link)) {
|
|
83
|
+
console.log(` ${sub}: already linked`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (fs.existsSync(link)) {
|
|
87
|
+
const moved = migrateIntoShared(profileName, link, target);
|
|
88
|
+
console.log(` ${sub}: migrated ${moved} item(s) into shared`);
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
fs.symlinkSync(target, link, symlinkType());
|
|
92
|
+
} catch (err) {
|
|
93
|
+
throw new Error(`Cannot link ${link} -> ${target}: ${err.message}`);
|
|
94
|
+
}
|
|
95
|
+
console.log(` ${sub}: linked -> ${target}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function shareUnlink(profileName) {
|
|
100
|
+
profiles.validateProfileName(profileName);
|
|
101
|
+
const pdir = profiles.profileDir(profileName);
|
|
102
|
+
if (!fs.existsSync(pdir)) {
|
|
103
|
+
const err = new Error(`Profile "${profileName}" does not exist.`);
|
|
104
|
+
err.exitCode = 1;
|
|
105
|
+
throw err;
|
|
106
|
+
}
|
|
107
|
+
for (const sub of SHARED_SUBDIRS) {
|
|
108
|
+
const link = path.join(pdir, sub);
|
|
109
|
+
const target = path.join(profiles.sharedDir(), sub);
|
|
110
|
+
if (!isLink(link)) {
|
|
111
|
+
console.log(` ${sub}: not linked, skipping`);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
removeLink(link);
|
|
115
|
+
fs.mkdirSync(link, { recursive: true });
|
|
116
|
+
if (fs.existsSync(target)) {
|
|
117
|
+
for (const ent of fs.readdirSync(target)) {
|
|
118
|
+
copyRecursive(path.join(target, ent), path.join(link, ent));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
console.log(` ${sub}: unlinked, content copied back`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = {
|
|
126
|
+
SHARED_SUBDIRS,
|
|
127
|
+
isLink,
|
|
128
|
+
removeLink,
|
|
129
|
+
shareLink,
|
|
130
|
+
shareUnlink,
|
|
131
|
+
};
|
package/lib/spawn.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawn, spawnSync } = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
const color = require('./color');
|
|
9
|
+
|
|
10
|
+
const MIN_VERSION = [2, 1, 140];
|
|
11
|
+
const MIN_VERSION_STR = MIN_VERSION.join('.');
|
|
12
|
+
const CMD_META_RE = /([()\][%!^"`<>&|;, *?])/g;
|
|
13
|
+
const CACHE_FILE = path.join(os.homedir(), '.claude-profiles', '.cc-cache.json');
|
|
14
|
+
|
|
15
|
+
function pathListSep() {
|
|
16
|
+
return process.platform === 'win32' ? ';' : ':';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function pathExt() {
|
|
20
|
+
if (process.platform !== 'win32') return [''];
|
|
21
|
+
const raw = process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD';
|
|
22
|
+
return raw.split(';').map((s) => s.trim()).filter(Boolean);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function findClaude() {
|
|
26
|
+
const PATH = process.env.PATH || process.env.Path || '';
|
|
27
|
+
if (!PATH) return null;
|
|
28
|
+
const dirs = PATH.split(pathListSep()).filter(Boolean);
|
|
29
|
+
const exts = pathExt();
|
|
30
|
+
for (const dir of dirs) {
|
|
31
|
+
for (const ext of exts) {
|
|
32
|
+
const full = path.join(dir, 'claude' + ext);
|
|
33
|
+
try {
|
|
34
|
+
const st = fs.statSync(full);
|
|
35
|
+
if (st.isFile()) return full;
|
|
36
|
+
} catch {
|
|
37
|
+
// not found, keep scanning
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parseVersion(text) {
|
|
45
|
+
if (!text) return null;
|
|
46
|
+
const m = String(text).match(/(\d+)\.(\d+)\.(\d+)/);
|
|
47
|
+
if (!m) return null;
|
|
48
|
+
return { major: +m[1], minor: +m[2], patch: +m[3], raw: m[0] };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function compareVersion(v, target) {
|
|
52
|
+
if (!v) return 0;
|
|
53
|
+
const [a, b, c] = target;
|
|
54
|
+
if (v.major !== a) return v.major - a;
|
|
55
|
+
if (v.minor !== b) return v.minor - b;
|
|
56
|
+
return v.patch - c;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getClaudeVersion(bin) {
|
|
60
|
+
try {
|
|
61
|
+
const opts = { encoding: 'utf8', timeout: 5000 };
|
|
62
|
+
let res;
|
|
63
|
+
if (process.platform === 'win32' && /\.(cmd|bat)$/i.test(bin)) {
|
|
64
|
+
const cmdLine = [escapeCommandWin(bin), escapeArgWin('--version')].join(' ');
|
|
65
|
+
res = spawnSync(process.env.COMSPEC || 'cmd.exe', ['/d', '/s', '/c', `"${cmdLine}"`], { ...opts, windowsVerbatimArguments: true });
|
|
66
|
+
} else {
|
|
67
|
+
res = spawnSync(bin, ['--version'], opts);
|
|
68
|
+
}
|
|
69
|
+
if (!res || res.status !== 0) return null;
|
|
70
|
+
return parseVersion(res.stdout);
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readCache() {
|
|
77
|
+
try {
|
|
78
|
+
const raw = fs.readFileSync(CACHE_FILE, 'utf8');
|
|
79
|
+
return JSON.parse(raw);
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function writeCache(data) {
|
|
86
|
+
try {
|
|
87
|
+
fs.mkdirSync(path.dirname(CACHE_FILE), { recursive: true });
|
|
88
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify(data));
|
|
89
|
+
} catch {
|
|
90
|
+
// cache is best-effort; ignore write failures
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function cachedClaudeVersion(bin) {
|
|
95
|
+
let mtimeMs = 0;
|
|
96
|
+
try {
|
|
97
|
+
mtimeMs = fs.statSync(bin).mtimeMs;
|
|
98
|
+
} catch {
|
|
99
|
+
return getClaudeVersion(bin);
|
|
100
|
+
}
|
|
101
|
+
const cache = readCache();
|
|
102
|
+
if (
|
|
103
|
+
cache &&
|
|
104
|
+
cache.binPath === bin &&
|
|
105
|
+
cache.binMtimeMs === mtimeMs &&
|
|
106
|
+
cache.minVersion === MIN_VERSION_STR &&
|
|
107
|
+
cache.version
|
|
108
|
+
) {
|
|
109
|
+
return cache.version;
|
|
110
|
+
}
|
|
111
|
+
const v = getClaudeVersion(bin);
|
|
112
|
+
if (v) {
|
|
113
|
+
writeCache({
|
|
114
|
+
binPath: bin,
|
|
115
|
+
binMtimeMs: mtimeMs,
|
|
116
|
+
minVersion: MIN_VERSION_STR,
|
|
117
|
+
version: v,
|
|
118
|
+
checkedAt: new Date().toISOString(),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return v;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function warnIfOld(bin, write) {
|
|
125
|
+
const w = write || ((s) => process.stderr.write(s + '\n'));
|
|
126
|
+
const v = cachedClaudeVersion(bin);
|
|
127
|
+
if (v && compareVersion(v, MIN_VERSION) < 0) {
|
|
128
|
+
const msg = `warning: Claude Code ${v.raw} is older than ${MIN_VERSION_STR}. CLAUDE_CONFIG_DIR isolation has known bugs in this range. Run \`claude update\` before relying on profiles.`;
|
|
129
|
+
w(color.stderr.yellow(msg));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function escapeArgWin(arg) {
|
|
134
|
+
let s = String(arg);
|
|
135
|
+
s = s.replace(/(\\*)"/g, '$1$1\\"');
|
|
136
|
+
s = s.replace(/(\\*)$/, '$1$1');
|
|
137
|
+
s = `"${s}"`;
|
|
138
|
+
s = s.replace(CMD_META_RE, '^$1');
|
|
139
|
+
return s;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function escapeCommandWin(cmd) {
|
|
143
|
+
return cmd.replace(CMD_META_RE, '^$1');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function platformSpawn(bin, args, opts) {
|
|
147
|
+
if (process.platform === 'win32' && /\.(cmd|bat)$/i.test(bin)) {
|
|
148
|
+
const cmdLine = [escapeCommandWin(bin), ...args.map(escapeArgWin)].join(' ');
|
|
149
|
+
return spawn(
|
|
150
|
+
process.env.COMSPEC || 'cmd.exe',
|
|
151
|
+
['/d', '/s', '/c', `"${cmdLine}"`],
|
|
152
|
+
{ ...opts, windowsVerbatimArguments: true },
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
return spawn(bin, args, opts);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function spawnClaude({ args = [], profileDir = null, activeProfile = null, extraEnv = {} } = {}) {
|
|
159
|
+
const bin = findClaude();
|
|
160
|
+
if (!bin) {
|
|
161
|
+
const err = new Error(
|
|
162
|
+
'Claude Code does not appear to be installed (no `claude` on PATH).\n' +
|
|
163
|
+
'Install it: npm install -g @anthropic-ai/claude-code\n' +
|
|
164
|
+
'Docs: https://code.claude.com',
|
|
165
|
+
);
|
|
166
|
+
err.exitCode = 127;
|
|
167
|
+
throw err;
|
|
168
|
+
}
|
|
169
|
+
warnIfOld(bin);
|
|
170
|
+
|
|
171
|
+
const childEnv = { ...process.env, ...extraEnv };
|
|
172
|
+
if (profileDir) {
|
|
173
|
+
childEnv.CLAUDE_CONFIG_DIR = profileDir;
|
|
174
|
+
} else {
|
|
175
|
+
delete childEnv.CLAUDE_CONFIG_DIR;
|
|
176
|
+
}
|
|
177
|
+
if (activeProfile) {
|
|
178
|
+
childEnv.CC_ACTIVE_PROFILE = activeProfile;
|
|
179
|
+
} else {
|
|
180
|
+
delete childEnv.CC_ACTIVE_PROFILE;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return new Promise((resolve) => {
|
|
184
|
+
let child;
|
|
185
|
+
try {
|
|
186
|
+
child = platformSpawn(bin, args, {
|
|
187
|
+
stdio: 'inherit',
|
|
188
|
+
env: childEnv,
|
|
189
|
+
windowsHide: false,
|
|
190
|
+
});
|
|
191
|
+
} catch (err) {
|
|
192
|
+
console.error(`Failed to launch claude: ${err.message}`);
|
|
193
|
+
resolve(127);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
child.on('error', (err) => {
|
|
198
|
+
console.error(`Failed to launch claude: ${err.message}`);
|
|
199
|
+
resolve(127);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
child.on('exit', (code, signal) => {
|
|
203
|
+
if (signal) {
|
|
204
|
+
try {
|
|
205
|
+
process.kill(process.pid, signal);
|
|
206
|
+
} catch {
|
|
207
|
+
resolve(128);
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
resolve(code == null ? 0 : code);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = {
|
|
217
|
+
findClaude,
|
|
218
|
+
getClaudeVersion,
|
|
219
|
+
cachedClaudeVersion,
|
|
220
|
+
parseVersion,
|
|
221
|
+
compareVersion,
|
|
222
|
+
warnIfOld,
|
|
223
|
+
platformSpawn,
|
|
224
|
+
spawnClaude,
|
|
225
|
+
escapeArgWin,
|
|
226
|
+
escapeCommandWin,
|
|
227
|
+
MIN_VERSION,
|
|
228
|
+
CACHE_FILE,
|
|
229
|
+
};
|