claude-switcher-account 2.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 +172 -0
- package/dist/bin.js +1277 -0
- package/package.json +61 -0
- package/scripts/postinstall.mjs +25 -0
package/dist/bin.js
ADDED
|
@@ -0,0 +1,1277 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
6
|
+
import { homedir as homedir5 } from "os";
|
|
7
|
+
import { join as join6 } from "path";
|
|
8
|
+
|
|
9
|
+
// src/app.ts
|
|
10
|
+
import { homedir, userInfo } from "os";
|
|
11
|
+
import { join as join2 } from "path";
|
|
12
|
+
|
|
13
|
+
// src/credentialStore.ts
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
|
|
16
|
+
// src/stores/macKeychainStore.ts
|
|
17
|
+
import { execFile } from "child_process";
|
|
18
|
+
import { promisify } from "util";
|
|
19
|
+
var execFileP = promisify(execFile);
|
|
20
|
+
var CLAUDE_SERVICE = "Claude Code-credentials";
|
|
21
|
+
var STORE_SERVICE = "claude-profiles-accounts";
|
|
22
|
+
var defaultExec = async (args) => {
|
|
23
|
+
const { stdout } = await execFileP("security", args);
|
|
24
|
+
return stdout;
|
|
25
|
+
};
|
|
26
|
+
var MacKeychainStore = class {
|
|
27
|
+
exec;
|
|
28
|
+
osUsername;
|
|
29
|
+
platform;
|
|
30
|
+
constructor(opts) {
|
|
31
|
+
this.exec = opts.exec ?? defaultExec;
|
|
32
|
+
this.osUsername = opts.osUsername;
|
|
33
|
+
this.platform = opts.platform ?? process.platform;
|
|
34
|
+
}
|
|
35
|
+
isAvailable() {
|
|
36
|
+
return this.platform === "darwin";
|
|
37
|
+
}
|
|
38
|
+
async readCanonical() {
|
|
39
|
+
try {
|
|
40
|
+
const out = await this.exec(["find-generic-password", "-w", "-s", CLAUDE_SERVICE, "-a", this.osUsername]);
|
|
41
|
+
return out.trim();
|
|
42
|
+
} catch {
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async writeCanonical(blob) {
|
|
47
|
+
await this.exec(["add-generic-password", "-U", "-s", CLAUDE_SERVICE, "-a", this.osUsername, "-w", blob]);
|
|
48
|
+
}
|
|
49
|
+
async readSaved(name) {
|
|
50
|
+
try {
|
|
51
|
+
const out = await this.exec(["find-generic-password", "-w", "-s", STORE_SERVICE, "-a", name]);
|
|
52
|
+
return out.trim();
|
|
53
|
+
} catch {
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async writeSaved(name, blob) {
|
|
58
|
+
await this.exec(["add-generic-password", "-U", "-s", STORE_SERVICE, "-a", name, "-w", blob]);
|
|
59
|
+
}
|
|
60
|
+
async deleteSaved(name) {
|
|
61
|
+
try {
|
|
62
|
+
await this.exec(["delete-generic-password", "-s", STORE_SERVICE, "-a", name]);
|
|
63
|
+
} catch {
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// src/stores/fileStore.ts
|
|
69
|
+
import { mkdirSync, readFileSync, writeFileSync, renameSync, existsSync } from "fs";
|
|
70
|
+
import { dirname } from "path";
|
|
71
|
+
var FileStore = class {
|
|
72
|
+
canonicalPath;
|
|
73
|
+
credsPath;
|
|
74
|
+
platform;
|
|
75
|
+
constructor(opts) {
|
|
76
|
+
this.canonicalPath = opts.canonicalPath;
|
|
77
|
+
this.credsPath = opts.credsPath;
|
|
78
|
+
this.platform = opts.platform ?? process.platform;
|
|
79
|
+
}
|
|
80
|
+
isAvailable() {
|
|
81
|
+
return this.platform === "linux" || this.platform === "win32";
|
|
82
|
+
}
|
|
83
|
+
atomicWrite(path, data, mode) {
|
|
84
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
85
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
86
|
+
writeFileSync(tmp, data, mode !== void 0 ? { mode } : void 0);
|
|
87
|
+
renameSync(tmp, path);
|
|
88
|
+
}
|
|
89
|
+
readMap() {
|
|
90
|
+
if (!existsSync(this.credsPath)) return {};
|
|
91
|
+
try {
|
|
92
|
+
const parsed = JSON.parse(readFileSync(this.credsPath, "utf8"));
|
|
93
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
94
|
+
} catch {
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async readCanonical() {
|
|
99
|
+
try {
|
|
100
|
+
return readFileSync(this.canonicalPath, "utf8").trim();
|
|
101
|
+
} catch {
|
|
102
|
+
return "";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async writeCanonical(blob) {
|
|
106
|
+
this.atomicWrite(this.canonicalPath, blob, 384);
|
|
107
|
+
}
|
|
108
|
+
async readSaved(name) {
|
|
109
|
+
return this.readMap()[name] ?? "";
|
|
110
|
+
}
|
|
111
|
+
async writeSaved(name, blob) {
|
|
112
|
+
const map = this.readMap();
|
|
113
|
+
map[name] = blob;
|
|
114
|
+
this.atomicWrite(this.credsPath, JSON.stringify(map, null, 2), 384);
|
|
115
|
+
}
|
|
116
|
+
async deleteSaved(name) {
|
|
117
|
+
const map = this.readMap();
|
|
118
|
+
if (!(name in map)) return;
|
|
119
|
+
delete map[name];
|
|
120
|
+
this.atomicWrite(this.credsPath, JSON.stringify(map, null, 2), 384);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// src/credentialStore.ts
|
|
125
|
+
function createCredentialStore(opts) {
|
|
126
|
+
if (opts.platform === "darwin") {
|
|
127
|
+
return new MacKeychainStore({ osUsername: opts.osUsername, platform: opts.platform });
|
|
128
|
+
}
|
|
129
|
+
return new FileStore({
|
|
130
|
+
canonicalPath: join(opts.home, ".claude", ".credentials.json"),
|
|
131
|
+
credsPath: join(opts.home, ".claude-profiles", "creds.json"),
|
|
132
|
+
platform: opts.platform
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/metadataStore.ts
|
|
137
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync as renameSync2, existsSync as existsSync2, statSync, rmSync } from "fs";
|
|
138
|
+
import { dirname as dirname2 } from "path";
|
|
139
|
+
var LOCK_STALE_MS = 1e4;
|
|
140
|
+
var LOCK_RETRY_MS = 25;
|
|
141
|
+
var LOCK_TIMEOUT_MS = 5e3;
|
|
142
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
143
|
+
var MetadataStore = class {
|
|
144
|
+
path;
|
|
145
|
+
lockPath;
|
|
146
|
+
constructor(opts) {
|
|
147
|
+
this.path = opts.path;
|
|
148
|
+
this.lockPath = `${opts.path}.lock`;
|
|
149
|
+
}
|
|
150
|
+
read() {
|
|
151
|
+
if (!existsSync2(this.path)) return { accounts: [] };
|
|
152
|
+
try {
|
|
153
|
+
const parsed = JSON.parse(readFileSync2(this.path, "utf8"));
|
|
154
|
+
return { lastActive: parsed?.lastActive, accounts: Array.isArray(parsed?.accounts) ? parsed.accounts : [] };
|
|
155
|
+
} catch {
|
|
156
|
+
return { accounts: [] };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async acquireLock() {
|
|
160
|
+
mkdirSync2(dirname2(this.lockPath), { recursive: true });
|
|
161
|
+
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
162
|
+
for (; ; ) {
|
|
163
|
+
try {
|
|
164
|
+
mkdirSync2(this.lockPath);
|
|
165
|
+
return;
|
|
166
|
+
} catch {
|
|
167
|
+
try {
|
|
168
|
+
if (Date.now() - statSync(this.lockPath).mtimeMs > LOCK_STALE_MS) {
|
|
169
|
+
rmSync(this.lockPath, { recursive: true, force: true });
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
} catch {
|
|
173
|
+
}
|
|
174
|
+
if (Date.now() > deadline) throw new Error(`Timed out acquiring lock ${this.lockPath}`);
|
|
175
|
+
await sleep(LOCK_RETRY_MS);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
releaseLock() {
|
|
180
|
+
rmSync(this.lockPath, { recursive: true, force: true });
|
|
181
|
+
}
|
|
182
|
+
async update(mutator) {
|
|
183
|
+
await this.acquireLock();
|
|
184
|
+
try {
|
|
185
|
+
const data = this.read();
|
|
186
|
+
mutator(data);
|
|
187
|
+
mkdirSync2(dirname2(this.path), { recursive: true });
|
|
188
|
+
const tmp = `${this.path}.tmp-${process.pid}`;
|
|
189
|
+
writeFileSync2(tmp, JSON.stringify({ lastActive: data.lastActive, accounts: data.accounts }, null, 2));
|
|
190
|
+
renameSync2(tmp, this.path);
|
|
191
|
+
return data;
|
|
192
|
+
} finally {
|
|
193
|
+
this.releaseLock();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// src/profileFile.ts
|
|
199
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, renameSync as renameSync3 } from "fs";
|
|
200
|
+
import { dirname as dirname3 } from "path";
|
|
201
|
+
var ProfileFile = class {
|
|
202
|
+
path;
|
|
203
|
+
constructor(opts) {
|
|
204
|
+
this.path = opts.path;
|
|
205
|
+
}
|
|
206
|
+
read() {
|
|
207
|
+
try {
|
|
208
|
+
const d = JSON.parse(readFileSync3(this.path, "utf8"));
|
|
209
|
+
return d && typeof d === "object" ? d : null;
|
|
210
|
+
} catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
peekOauthAccount() {
|
|
215
|
+
const oa = this.read()?.oauthAccount;
|
|
216
|
+
return oa && typeof oa === "object" ? oa : void 0;
|
|
217
|
+
}
|
|
218
|
+
peekLabel() {
|
|
219
|
+
const oa = this.peekOauthAccount();
|
|
220
|
+
const email = typeof oa?.emailAddress === "string" ? oa.emailAddress : void 0;
|
|
221
|
+
if (!email) return null;
|
|
222
|
+
return {
|
|
223
|
+
email,
|
|
224
|
+
displayName: typeof oa?.displayName === "string" ? oa.displayName : void 0,
|
|
225
|
+
organizationName: typeof oa?.organizationName === "string" ? oa.organizationName : void 0
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
currentIdentity() {
|
|
229
|
+
const oa = this.peekOauthAccount();
|
|
230
|
+
return {
|
|
231
|
+
uuid: typeof oa?.accountUuid === "string" ? oa.accountUuid : void 0,
|
|
232
|
+
email: typeof oa?.emailAddress === "string" ? oa.emailAddress : void 0
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
applyOauthAccount(snapshot) {
|
|
236
|
+
let d = this.read();
|
|
237
|
+
if (!d) {
|
|
238
|
+
if (!snapshot) return;
|
|
239
|
+
d = {};
|
|
240
|
+
}
|
|
241
|
+
if (snapshot) d.oauthAccount = snapshot;
|
|
242
|
+
else delete d.oauthAccount;
|
|
243
|
+
try {
|
|
244
|
+
mkdirSync3(dirname3(this.path), { recursive: true });
|
|
245
|
+
const tmp = `${this.path}.tmp-${process.pid}`;
|
|
246
|
+
writeFileSync3(tmp, JSON.stringify(d, null, 2));
|
|
247
|
+
renameSync3(tmp, this.path);
|
|
248
|
+
} catch {
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// src/config.ts
|
|
254
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
255
|
+
var VALID = ["delete-switch", "keep-switch", "keep", "none"];
|
|
256
|
+
var DEFAULT = { logoutBehavior: "keep" };
|
|
257
|
+
var ConfigStore = class {
|
|
258
|
+
path;
|
|
259
|
+
constructor(opts) {
|
|
260
|
+
this.path = opts.path;
|
|
261
|
+
}
|
|
262
|
+
load() {
|
|
263
|
+
try {
|
|
264
|
+
const d = JSON.parse(readFileSync4(this.path, "utf8"));
|
|
265
|
+
const b = d?.logoutBehavior;
|
|
266
|
+
return VALID.includes(b) ? { logoutBehavior: b } : DEFAULT;
|
|
267
|
+
} catch {
|
|
268
|
+
return DEFAULT;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// src/fingerprint.ts
|
|
274
|
+
import { createHash } from "crypto";
|
|
275
|
+
function fingerprint(blob) {
|
|
276
|
+
return createHash("sha256").update(blob).digest("hex");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// src/order.ts
|
|
280
|
+
function compareByEmail(a, b) {
|
|
281
|
+
return a.email.toLowerCase().localeCompare(b.email.toLowerCase());
|
|
282
|
+
}
|
|
283
|
+
function orderAccounts(views) {
|
|
284
|
+
return [...views].sort(compareByEmail);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/accountManager.ts
|
|
288
|
+
function toView(a, active) {
|
|
289
|
+
return {
|
|
290
|
+
name: a.name,
|
|
291
|
+
email: a.email,
|
|
292
|
+
displayName: a.displayName,
|
|
293
|
+
organizationName: a.organizationName,
|
|
294
|
+
savedAt: a.savedAt,
|
|
295
|
+
active
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
var AccountManager = class {
|
|
299
|
+
store;
|
|
300
|
+
meta;
|
|
301
|
+
profile;
|
|
302
|
+
config;
|
|
303
|
+
now;
|
|
304
|
+
constructor(deps) {
|
|
305
|
+
this.store = deps.store;
|
|
306
|
+
this.meta = deps.meta;
|
|
307
|
+
this.profile = deps.profile;
|
|
308
|
+
this.config = deps.config;
|
|
309
|
+
this.now = deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
310
|
+
}
|
|
311
|
+
matchByIdentity(accounts, id) {
|
|
312
|
+
if (id.uuid) {
|
|
313
|
+
const m = accounts.find((a) => a.oauthAccount?.accountUuid === id.uuid);
|
|
314
|
+
if (m) return m;
|
|
315
|
+
}
|
|
316
|
+
if (id.email) return accounts.find((a) => a.email === id.email);
|
|
317
|
+
return void 0;
|
|
318
|
+
}
|
|
319
|
+
async saveCurrent(name) {
|
|
320
|
+
const blob = await this.store.readCanonical();
|
|
321
|
+
if (!blob) throw new Error("No current Claude login found.");
|
|
322
|
+
const fp = fingerprint(blob);
|
|
323
|
+
const data = this.meta.read();
|
|
324
|
+
const label = this.profile.peekLabel();
|
|
325
|
+
let finalName = name;
|
|
326
|
+
let matched;
|
|
327
|
+
if (!finalName) {
|
|
328
|
+
matched = data.accounts.find((a) => a.fingerprint === fp);
|
|
329
|
+
finalName = matched?.name;
|
|
330
|
+
}
|
|
331
|
+
if (!finalName) {
|
|
332
|
+
const email2 = label?.email;
|
|
333
|
+
const collision = email2 ? data.accounts.find((a) => a.name === email2 && a.fingerprint !== fp) : void 0;
|
|
334
|
+
if (collision) {
|
|
335
|
+
throw new Error(
|
|
336
|
+
`Current login does not match saved account '${email2}'. Its profile may be stale \u2014 retry after the session initializes.`
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
finalName = email2;
|
|
340
|
+
}
|
|
341
|
+
if (!finalName) throw new Error("Could not determine an account name.");
|
|
342
|
+
await this.store.writeSaved(finalName, blob);
|
|
343
|
+
const email = matched ? matched.email : label?.email ?? finalName;
|
|
344
|
+
const current = this.profile.peekOauthAccount();
|
|
345
|
+
const currentEmail = typeof current?.emailAddress === "string" ? current.emailAddress : void 0;
|
|
346
|
+
const oauthAccount = current && (currentEmail === email || currentEmail === finalName) ? current : matched?.oauthAccount;
|
|
347
|
+
const entry = {
|
|
348
|
+
name: finalName,
|
|
349
|
+
email: matched ? matched.email : label?.email ?? finalName,
|
|
350
|
+
displayName: matched ? matched.displayName : label?.displayName,
|
|
351
|
+
organizationName: matched ? matched.organizationName : label?.organizationName,
|
|
352
|
+
fingerprint: fp,
|
|
353
|
+
savedAt: this.now(),
|
|
354
|
+
oauthAccount
|
|
355
|
+
};
|
|
356
|
+
const nameToWrite = finalName;
|
|
357
|
+
await this.meta.update((d) => {
|
|
358
|
+
d.accounts = [...d.accounts.filter((a) => a.name !== nameToWrite), entry];
|
|
359
|
+
});
|
|
360
|
+
return toView(entry, true);
|
|
361
|
+
}
|
|
362
|
+
/** Map a 1-based index (as printed by `list`) to an account name, or null if out of range. */
|
|
363
|
+
async resolveByIndex(n) {
|
|
364
|
+
if (!Number.isInteger(n) || n < 1) return null;
|
|
365
|
+
const ordered = orderAccounts(await this.listAccounts());
|
|
366
|
+
return ordered[n - 1]?.name ?? null;
|
|
367
|
+
}
|
|
368
|
+
async switchTo(name) {
|
|
369
|
+
const blob = (await this.store.readSaved(name)).trim();
|
|
370
|
+
if (!blob) throw new Error(`Saved account '${name}' not found.`);
|
|
371
|
+
await this.store.writeCanonical(blob);
|
|
372
|
+
const saved = this.meta.read().accounts.find((a) => a.name === name);
|
|
373
|
+
this.profile.applyOauthAccount(saved?.oauthAccount);
|
|
374
|
+
}
|
|
375
|
+
async removeAccount(name) {
|
|
376
|
+
await this.store.deleteSaved(name);
|
|
377
|
+
await this.meta.update((d) => {
|
|
378
|
+
d.accounts = d.accounts.filter((a) => a.name !== name);
|
|
379
|
+
if (d.lastActive === name) d.lastActive = void 0;
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
async renameAccount(oldName, newName) {
|
|
383
|
+
const data = this.meta.read();
|
|
384
|
+
const entry = data.accounts.find((a) => a.name === oldName);
|
|
385
|
+
if (!entry) throw new Error(`Saved account '${oldName}' not found.`);
|
|
386
|
+
if (data.accounts.some((a) => a.name === newName)) throw new Error(`Account '${newName}' already exists.`);
|
|
387
|
+
const blob = await this.store.readSaved(oldName);
|
|
388
|
+
if (blob) await this.store.writeSaved(newName, blob);
|
|
389
|
+
await this.store.deleteSaved(oldName);
|
|
390
|
+
await this.meta.update((d) => {
|
|
391
|
+
d.accounts = d.accounts.map((a) => a.name === oldName ? { ...a, name: newName } : a);
|
|
392
|
+
if (d.lastActive === oldName) d.lastActive = newName;
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
async listAccounts() {
|
|
396
|
+
const accounts = this.meta.read().accounts;
|
|
397
|
+
const id = this.profile.currentIdentity();
|
|
398
|
+
const byId = id.uuid || id.email ? this.matchByIdentity(accounts, id) : void 0;
|
|
399
|
+
let activeFp = null;
|
|
400
|
+
if (!byId) {
|
|
401
|
+
const blob = await this.store.readCanonical();
|
|
402
|
+
activeFp = blob ? fingerprint(blob) : null;
|
|
403
|
+
}
|
|
404
|
+
return accounts.map((a) => toView(a, byId ? a.name === byId.name : activeFp !== null && a.fingerprint === activeFp));
|
|
405
|
+
}
|
|
406
|
+
current() {
|
|
407
|
+
const label = this.profile.peekLabel();
|
|
408
|
+
const accounts = this.meta.read().accounts;
|
|
409
|
+
const byId = this.matchByIdentity(accounts, this.profile.currentIdentity());
|
|
410
|
+
return { saved: byId ? toView(byId, true) : null, label };
|
|
411
|
+
}
|
|
412
|
+
async refreshStoredCredential(name, blob, fp) {
|
|
413
|
+
try {
|
|
414
|
+
await this.store.writeSaved(name, blob);
|
|
415
|
+
} catch {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
await this.meta.update((d) => {
|
|
419
|
+
const idx = d.accounts.findIndex((a) => a.name === name);
|
|
420
|
+
if (idx < 0) return;
|
|
421
|
+
const cur = d.accounts[idx];
|
|
422
|
+
d.accounts[idx] = {
|
|
423
|
+
...cur,
|
|
424
|
+
fingerprint: fp,
|
|
425
|
+
oauthAccount: this.profile.peekOauthAccount() ?? cur.oauthAccount,
|
|
426
|
+
savedAt: this.now()
|
|
427
|
+
};
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
async autoSaveIfNewLogin() {
|
|
431
|
+
if (!this.store.isAvailable()) return null;
|
|
432
|
+
let blob = "";
|
|
433
|
+
try {
|
|
434
|
+
blob = await this.store.readCanonical();
|
|
435
|
+
} catch {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
if (!blob) return null;
|
|
439
|
+
const fp = fingerprint(blob);
|
|
440
|
+
if (this.meta.read().accounts.some((a) => a.fingerprint === fp)) return null;
|
|
441
|
+
if (!this.profile.peekLabel()?.email) return null;
|
|
442
|
+
try {
|
|
443
|
+
return await this.saveCurrent();
|
|
444
|
+
} catch {
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
pickNext(exclude) {
|
|
449
|
+
return [...this.meta.read().accounts].filter((a) => a.name !== exclude).sort((a, b) => (b.savedAt || "").localeCompare(a.savedAt || "") || a.name.localeCompare(b.name))[0];
|
|
450
|
+
}
|
|
451
|
+
async reconcileOnChange() {
|
|
452
|
+
const nothing = { autoSaved: null, removed: null, switchedTo: null };
|
|
453
|
+
if (!this.store.isAvailable()) return nothing;
|
|
454
|
+
let blob = "";
|
|
455
|
+
try {
|
|
456
|
+
blob = await this.store.readCanonical();
|
|
457
|
+
} catch {
|
|
458
|
+
blob = "";
|
|
459
|
+
}
|
|
460
|
+
const loggedIn = !!this.profile.peekLabel();
|
|
461
|
+
if (!blob && !loggedIn) {
|
|
462
|
+
const behavior = this.config.logoutBehavior;
|
|
463
|
+
if (behavior === "none") return nothing;
|
|
464
|
+
const wasActive = this.meta.read().lastActive;
|
|
465
|
+
let removed = null;
|
|
466
|
+
if (behavior === "delete-switch" && wasActive && this.meta.read().accounts.some((a) => a.name === wasActive)) {
|
|
467
|
+
try {
|
|
468
|
+
await this.removeAccount(wasActive);
|
|
469
|
+
removed = wasActive;
|
|
470
|
+
} catch {
|
|
471
|
+
return nothing;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
let switchedTo = null;
|
|
475
|
+
if (behavior === "delete-switch" || behavior === "keep-switch") {
|
|
476
|
+
const next = this.pickNext(wasActive);
|
|
477
|
+
if (next) {
|
|
478
|
+
try {
|
|
479
|
+
await this.switchTo(next.name);
|
|
480
|
+
switchedTo = next.name;
|
|
481
|
+
} catch {
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
await this.meta.update((d) => {
|
|
486
|
+
d.lastActive = switchedTo ?? void 0;
|
|
487
|
+
});
|
|
488
|
+
return { autoSaved: null, removed, switchedTo };
|
|
489
|
+
}
|
|
490
|
+
if (blob) {
|
|
491
|
+
const fpNow = fingerprint(blob);
|
|
492
|
+
const accounts = this.meta.read().accounts;
|
|
493
|
+
const known = this.matchByIdentity(accounts, this.profile.currentIdentity()) ?? accounts.find((a) => a.fingerprint === fpNow);
|
|
494
|
+
if (known) {
|
|
495
|
+
await this.meta.update((d) => {
|
|
496
|
+
d.lastActive = known.name;
|
|
497
|
+
});
|
|
498
|
+
if (known.fingerprint !== fpNow) await this.refreshStoredCredential(known.name, blob, fpNow);
|
|
499
|
+
return nothing;
|
|
500
|
+
}
|
|
501
|
+
const saved = await this.autoSaveIfNewLogin();
|
|
502
|
+
if (saved) {
|
|
503
|
+
await this.meta.update((d) => {
|
|
504
|
+
d.lastActive = saved.name;
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
return { autoSaved: saved, removed: null, switchedTo: null };
|
|
508
|
+
}
|
|
509
|
+
return nothing;
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
// src/app.ts
|
|
514
|
+
function createManager(env) {
|
|
515
|
+
const platform = env?.platform ?? process.platform;
|
|
516
|
+
const home = env?.home ?? homedir();
|
|
517
|
+
const osUsername = env?.osUsername ?? userInfo().username;
|
|
518
|
+
const store = createCredentialStore({ platform, home, osUsername });
|
|
519
|
+
const meta = new MetadataStore({ path: join2(home, ".claude-profiles", "accounts.json") });
|
|
520
|
+
const profile = new ProfileFile({ path: join2(home, ".claude.json") });
|
|
521
|
+
const config = new ConfigStore({ path: join2(home, ".claude-profiles", "config.json") }).load();
|
|
522
|
+
return new AccountManager({ store, meta, profile, config });
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// src/format.ts
|
|
526
|
+
var SYMBOLS = { active: "\u25CF", ok: "\u2713", err: "\u2717", warn: "!", off: "\u25CB" };
|
|
527
|
+
function formatRelative(iso, nowMs) {
|
|
528
|
+
const then = Date.parse(iso);
|
|
529
|
+
if (Number.isNaN(then)) return "\u2014";
|
|
530
|
+
const s = Math.max(0, Math.floor((nowMs - then) / 1e3));
|
|
531
|
+
if (s < 60) return "just now";
|
|
532
|
+
const m = Math.floor(s / 60);
|
|
533
|
+
if (m < 60) return `${m}m ago`;
|
|
534
|
+
const h = Math.floor(m / 60);
|
|
535
|
+
if (h < 24) return `${h}h ago`;
|
|
536
|
+
const d = Math.floor(h / 24);
|
|
537
|
+
if (d < 7) return `${d}d ago`;
|
|
538
|
+
const w = Math.floor(d / 7);
|
|
539
|
+
if (w < 5) return `${w}w ago`;
|
|
540
|
+
const mo = Math.floor(d / 30);
|
|
541
|
+
if (mo < 12) return `${mo}mo ago`;
|
|
542
|
+
return `${Math.floor(d / 365)}y ago`;
|
|
543
|
+
}
|
|
544
|
+
function pad(s, width) {
|
|
545
|
+
return s.length >= width ? s : s + " ".repeat(width - s.length);
|
|
546
|
+
}
|
|
547
|
+
function truncate(s, max) {
|
|
548
|
+
return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
|
|
549
|
+
}
|
|
550
|
+
var ORG_MAX = 20;
|
|
551
|
+
function formatList(views, nowMs, colors, opts = {}) {
|
|
552
|
+
const header = ` ${colors.bold("claudep")} ${colors.dim(`\xB7 ${views.length} account${views.length === 1 ? "" : "s"}`)}`;
|
|
553
|
+
if (views.length === 0) {
|
|
554
|
+
return [
|
|
555
|
+
header,
|
|
556
|
+
colors.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"),
|
|
557
|
+
colors.dim(" No saved accounts yet."),
|
|
558
|
+
colors.dim(` \u2192 log in with ${colors.cyan("claude")}, then run ${colors.cyan("claudep list")}`)
|
|
559
|
+
].join("\n");
|
|
560
|
+
}
|
|
561
|
+
const ordered = orderAccounts(views);
|
|
562
|
+
const idxW = String(ordered.length).length;
|
|
563
|
+
const emailW = Math.max(...ordered.map((v) => v.email.length));
|
|
564
|
+
const orgW = Math.min(ORG_MAX, Math.max(...ordered.map((v) => (v.organizationName ?? "\u2014").length)));
|
|
565
|
+
const rows = ordered.map((v, i) => {
|
|
566
|
+
const idx = colors.dim(pad(String(i + 1), idxW));
|
|
567
|
+
const mark = v.active ? colors.green(SYMBOLS.active) : " ";
|
|
568
|
+
const email = v.active ? colors.green(pad(v.email, emailW)) : pad(v.email, emailW);
|
|
569
|
+
const org = colors.dim(pad(truncate(v.organizationName ?? "\u2014", ORG_MAX), orgW));
|
|
570
|
+
const when = v.active ? colors.green("active") : colors.dim(formatRelative(v.savedAt, nowMs));
|
|
571
|
+
return ` ${idx} ${mark} ${email} ${org} ${when}`;
|
|
572
|
+
});
|
|
573
|
+
const lines = [header, colors.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"), ...rows, ""];
|
|
574
|
+
if (opts.external) {
|
|
575
|
+
lines.push(
|
|
576
|
+
` ${colors.yellow(SYMBOLS.warn)} ${colors.dim(`${opts.external} is active but not saved yet`)}`,
|
|
577
|
+
colors.dim(" run any command again to capture it")
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
if (ordered.length > 1) lines.push(colors.dim(" \u2192 claudep <n> to switch"));
|
|
581
|
+
return lines.join("\n").replace(/\n+$/, "");
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// src/color.ts
|
|
585
|
+
function makeColors(enabled) {
|
|
586
|
+
const wrap = (code) => (s) => enabled ? `\x1B[${code}m${s}\x1B[0m` : s;
|
|
587
|
+
return {
|
|
588
|
+
green: wrap("32"),
|
|
589
|
+
dim: wrap("2"),
|
|
590
|
+
bold: wrap("1"),
|
|
591
|
+
cyan: wrap("36"),
|
|
592
|
+
red: wrap("31"),
|
|
593
|
+
yellow: wrap("33")
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// src/doctor.ts
|
|
598
|
+
import { homedir as homedir2, userInfo as userInfo2, platform as osPlatform } from "os";
|
|
599
|
+
import { join as join3 } from "path";
|
|
600
|
+
async function runDoctor(deps, colors = makeColors(false)) {
|
|
601
|
+
const checks = [];
|
|
602
|
+
checks.push({
|
|
603
|
+
section: "System",
|
|
604
|
+
items: [
|
|
605
|
+
{ ok: true, label: "platform", detail: `${deps.platform} (node ${process.version})` },
|
|
606
|
+
{
|
|
607
|
+
ok: deps.store.isAvailable(),
|
|
608
|
+
label: "credential backend",
|
|
609
|
+
detail: deps.store.isAvailable() ? "available" : "unavailable",
|
|
610
|
+
hint: "This OS/backend is not supported yet."
|
|
611
|
+
}
|
|
612
|
+
]
|
|
613
|
+
});
|
|
614
|
+
const blob = await deps.store.readCanonical().catch(() => "");
|
|
615
|
+
const label = deps.profile.peekLabel();
|
|
616
|
+
checks.push({
|
|
617
|
+
section: "Claude login",
|
|
618
|
+
items: [
|
|
619
|
+
{
|
|
620
|
+
ok: !!blob,
|
|
621
|
+
label: "canonical slot",
|
|
622
|
+
detail: blob ? "present" : "missing",
|
|
623
|
+
hint: "run `claude` and log in once, then `claudep doctor`"
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
ok: !!label,
|
|
627
|
+
label: "active identity",
|
|
628
|
+
detail: label ? `${label.email}${label.organizationName ? ` \xB7 ${label.organizationName}` : ""}` : "unknown",
|
|
629
|
+
hint: "wait for the session to initialize"
|
|
630
|
+
}
|
|
631
|
+
]
|
|
632
|
+
});
|
|
633
|
+
const accounts = deps.meta.read().accounts;
|
|
634
|
+
checks.push({
|
|
635
|
+
section: "Store",
|
|
636
|
+
items: [
|
|
637
|
+
{ ok: true, label: "config", detail: `logout=${deps.config.logoutBehavior}` },
|
|
638
|
+
{ ok: true, label: "accounts", detail: `${accounts.length} saved` }
|
|
639
|
+
]
|
|
640
|
+
});
|
|
641
|
+
const lines = [
|
|
642
|
+
` ${colors.bold("claudep")} ${colors.dim("\xB7 doctor")}`,
|
|
643
|
+
colors.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")
|
|
644
|
+
];
|
|
645
|
+
let ok = true;
|
|
646
|
+
let issues = 0;
|
|
647
|
+
for (const group of checks) {
|
|
648
|
+
lines.push(` ${colors.bold(group.section)}`);
|
|
649
|
+
for (const c of group.items) {
|
|
650
|
+
const sym = c.ok ? colors.green(SYMBOLS.ok) : colors.red(SYMBOLS.err);
|
|
651
|
+
if (!c.ok) {
|
|
652
|
+
ok = false;
|
|
653
|
+
issues++;
|
|
654
|
+
}
|
|
655
|
+
lines.push(` ${sym} ${c.label.padEnd(18)} ${colors.dim(c.detail)}`);
|
|
656
|
+
if (!c.ok && c.hint) lines.push(colors.dim(` \u2192 ${c.hint}`));
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
lines.push("");
|
|
660
|
+
lines.push(
|
|
661
|
+
ok ? ` ${colors.green(SYMBOLS.ok)} All checks passed` : ` ${colors.red(SYMBOLS.err)} ${issues} issue${issues === 1 ? "" : "s"} found`
|
|
662
|
+
);
|
|
663
|
+
return { ok, report: lines.join("\n") };
|
|
664
|
+
}
|
|
665
|
+
function buildDoctorDeps(env) {
|
|
666
|
+
const platform = env?.platform ?? osPlatform();
|
|
667
|
+
const home = env?.home ?? homedir2();
|
|
668
|
+
const osUsername = env?.osUsername ?? userInfo2().username;
|
|
669
|
+
return {
|
|
670
|
+
store: createCredentialStore({ platform, home, osUsername }),
|
|
671
|
+
profile: new ProfileFile({ path: join3(home, ".claude.json") }),
|
|
672
|
+
meta: new MetadataStore({ path: join3(home, ".claude-profiles", "accounts.json") }),
|
|
673
|
+
config: new ConfigStore({ path: join3(home, ".claude-profiles", "config.json") }).load(),
|
|
674
|
+
platform
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// package.json
|
|
679
|
+
var package_default = {
|
|
680
|
+
name: "claude-switcher-account",
|
|
681
|
+
version: "2.1.0",
|
|
682
|
+
description: "Switch between multiple Claude Code accounts from the terminal \u2014 save every login, swap in one command, no re-authenticating. macOS, Linux, Windows.",
|
|
683
|
+
keywords: [
|
|
684
|
+
"claude",
|
|
685
|
+
"claude-code",
|
|
686
|
+
"claude-code-cli",
|
|
687
|
+
"anthropic",
|
|
688
|
+
"account-switcher",
|
|
689
|
+
"switch-account",
|
|
690
|
+
"multiple-accounts",
|
|
691
|
+
"multi-account",
|
|
692
|
+
"login-switcher",
|
|
693
|
+
"profile-manager",
|
|
694
|
+
"credentials",
|
|
695
|
+
"keychain",
|
|
696
|
+
"cli",
|
|
697
|
+
"developer-tools"
|
|
698
|
+
],
|
|
699
|
+
homepage: "https://github.com/khainguyenhm-itr/claude-switcher-account#readme",
|
|
700
|
+
repository: {
|
|
701
|
+
type: "git",
|
|
702
|
+
url: "git+https://github.com/khainguyenhm-itr/claude-switcher-account.git"
|
|
703
|
+
},
|
|
704
|
+
bugs: {
|
|
705
|
+
url: "https://github.com/khainguyenhm-itr/claude-switcher-account/issues"
|
|
706
|
+
},
|
|
707
|
+
license: "MIT",
|
|
708
|
+
author: "khainguyenhm-itr",
|
|
709
|
+
type: "module",
|
|
710
|
+
bin: {
|
|
711
|
+
claudep: "dist/bin.js"
|
|
712
|
+
},
|
|
713
|
+
files: [
|
|
714
|
+
"dist",
|
|
715
|
+
"scripts/postinstall.mjs"
|
|
716
|
+
],
|
|
717
|
+
engines: {
|
|
718
|
+
node: ">=18"
|
|
719
|
+
},
|
|
720
|
+
scripts: {
|
|
721
|
+
build: "tsup src/bin.ts --format esm --target node18 --clean",
|
|
722
|
+
prepare: "npm run build",
|
|
723
|
+
postinstall: "node scripts/postinstall.mjs",
|
|
724
|
+
prepublishOnly: "npm run typecheck && npm test",
|
|
725
|
+
test: "vitest run --coverage",
|
|
726
|
+
"test:watch": "vitest",
|
|
727
|
+
typecheck: "tsc --noEmit"
|
|
728
|
+
},
|
|
729
|
+
dependencies: {
|
|
730
|
+
commander: "^15.0.0"
|
|
731
|
+
},
|
|
732
|
+
devDependencies: {
|
|
733
|
+
"@types/node": "^22.0.0",
|
|
734
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
735
|
+
tsup: "^8.5.1",
|
|
736
|
+
typescript: "^5.6.0",
|
|
737
|
+
vitest: "^4.1.10"
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
// src/version.ts
|
|
742
|
+
var PKG_NAME = package_default.name;
|
|
743
|
+
var VERSION = package_default.version;
|
|
744
|
+
function compareVersions(a, b) {
|
|
745
|
+
const pa = a.split(".").map((n) => Number(n) || 0);
|
|
746
|
+
const pb = b.split(".").map((n) => Number(n) || 0);
|
|
747
|
+
for (let i = 0; i < 3; i++) {
|
|
748
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
749
|
+
if (d !== 0) return d < 0 ? -1 : 1;
|
|
750
|
+
}
|
|
751
|
+
return 0;
|
|
752
|
+
}
|
|
753
|
+
async function checkForUpdate(current, pkg = PKG_NAME, fetchFn = fetch) {
|
|
754
|
+
try {
|
|
755
|
+
const res = await fetchFn(`https://registry.npmjs.org/${pkg}/latest`, { headers: { accept: "application/json" } });
|
|
756
|
+
if (!res.ok) return null;
|
|
757
|
+
const data = await res.json();
|
|
758
|
+
if (!data.version) return null;
|
|
759
|
+
return { current, latest: data.version, isNewer: compareVersions(data.version, current) > 0 };
|
|
760
|
+
} catch {
|
|
761
|
+
return null;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
function formatVersion(info, colors = makeColors(false)) {
|
|
765
|
+
const head = ` ${colors.bold(`claudep ${VERSION}`)} ${colors.dim(`\xB7 npm: ${PKG_NAME}`)}`;
|
|
766
|
+
const rule = colors.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
767
|
+
let result;
|
|
768
|
+
if (!info) result = colors.dim(" \xB7 could not check for updates");
|
|
769
|
+
else if (info.isNewer)
|
|
770
|
+
result = ` ${colors.yellow("\u25B2")} v${info.latest} available
|
|
771
|
+
${colors.dim(` update: ${colors.cyan(`npm i -g ${PKG_NAME}`)}`)}`;
|
|
772
|
+
else result = ` ${colors.green("\u2713")} up to date`;
|
|
773
|
+
return [head, rule, result].join("\n");
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// src/menu.ts
|
|
777
|
+
import { createInterface } from "readline/promises";
|
|
778
|
+
function pad2(s, width) {
|
|
779
|
+
return s.length >= width ? s : s + " ".repeat(width - s.length);
|
|
780
|
+
}
|
|
781
|
+
function buildMenuChoices(accounts) {
|
|
782
|
+
const ordered = orderAccounts(accounts);
|
|
783
|
+
if (!ordered.length) return [];
|
|
784
|
+
const emailW = Math.max(...ordered.map((a) => a.email.length));
|
|
785
|
+
return ordered.map((a, i) => {
|
|
786
|
+
const mark = a.active ? SYMBOLS.active : " ";
|
|
787
|
+
const when = a.active ? "active" : formatRelative(a.savedAt, Date.now());
|
|
788
|
+
const org = a.organizationName ? ` ${a.organizationName}` : "";
|
|
789
|
+
return {
|
|
790
|
+
label: `${i + 1} ${mark} ${pad2(a.email, emailW)}${org} ${when}`,
|
|
791
|
+
action: { type: "switch", name: a.name }
|
|
792
|
+
};
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
function accountChoices(views) {
|
|
796
|
+
return orderAccounts(views).map((v) => ({ label: v.email, action: { type: "switch", name: v.name } }));
|
|
797
|
+
}
|
|
798
|
+
async function dispatchMenu(manager, action, io) {
|
|
799
|
+
switch (action.type) {
|
|
800
|
+
case "quit":
|
|
801
|
+
return;
|
|
802
|
+
case "switch":
|
|
803
|
+
try {
|
|
804
|
+
await manager.switchTo(action.name);
|
|
805
|
+
io.out(`${SYMBOLS.ok} Now using ${action.name}`);
|
|
806
|
+
} catch (e) {
|
|
807
|
+
io.out(`${SYMBOLS.err} ${e.message}`);
|
|
808
|
+
}
|
|
809
|
+
return;
|
|
810
|
+
case "doctor":
|
|
811
|
+
io.out(await io.doctor());
|
|
812
|
+
return;
|
|
813
|
+
case "rename": {
|
|
814
|
+
const views = await manager.listAccounts();
|
|
815
|
+
if (!views.length) {
|
|
816
|
+
io.out("No saved accounts.");
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
const choice = await io.pick("Rename which account?", accountChoices(views));
|
|
820
|
+
if (!choice || choice.action.type !== "switch") return;
|
|
821
|
+
const oldName = choice.action.name;
|
|
822
|
+
const newName = (await io.ask(`New name for ${oldName}: `)).trim();
|
|
823
|
+
if (!newName) {
|
|
824
|
+
io.out("Cancelled.");
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
try {
|
|
828
|
+
await manager.renameAccount(oldName, newName);
|
|
829
|
+
io.out(`${SYMBOLS.ok} Renamed ${oldName} \u2192 ${newName}`);
|
|
830
|
+
} catch (e) {
|
|
831
|
+
io.out(`${SYMBOLS.err} ${e.message}`);
|
|
832
|
+
}
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
async function runMenu(manager, io) {
|
|
838
|
+
try {
|
|
839
|
+
await manager.reconcileOnChange();
|
|
840
|
+
} catch {
|
|
841
|
+
}
|
|
842
|
+
const accounts = await manager.listAccounts();
|
|
843
|
+
const choice = await io.pick(`claudep \xB7 ${accounts.length} account${accounts.length === 1 ? "" : "s"}`, buildMenuChoices(accounts));
|
|
844
|
+
if (!choice) return;
|
|
845
|
+
await dispatchMenu(manager, choice.action, io);
|
|
846
|
+
}
|
|
847
|
+
var DIVIDER = "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500";
|
|
848
|
+
function interactiveSelect(title, choices) {
|
|
849
|
+
const stdin = process.stdin;
|
|
850
|
+
const stdout = process.stdout;
|
|
851
|
+
const colors = makeColors(stdout.isTTY === true && !process.env.NO_COLOR);
|
|
852
|
+
const rename = { label: "rename", action: { type: "rename" } };
|
|
853
|
+
const doctor = { label: "doctor", action: { type: "doctor" } };
|
|
854
|
+
return new Promise((resolve) => {
|
|
855
|
+
if (!stdin.isTTY) {
|
|
856
|
+
resolve(null);
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
let selected = Math.max(0, choices.findIndex((c) => c.label.includes(SYMBOLS.active)));
|
|
860
|
+
const header = ` ${colors.bold("claudep")} ${colors.dim(title.replace(/^claudep\s*/, ""))}`;
|
|
861
|
+
const footer = ` ${colors.cyan("r")} ${colors.dim("rename")} ${colors.cyan("d")} ${colors.dim("doctor")} ${colors.cyan("q")} ${colors.dim("quit")}`;
|
|
862
|
+
const hint = colors.dim(" \u2191/\u2193 move \xB7 \u21B5 switch \xB7 1-9 pick \xB7 q quit");
|
|
863
|
+
const chrome = 4;
|
|
864
|
+
stdout.write(`${header}
|
|
865
|
+
${colors.dim(` ${DIVIDER}`)}
|
|
866
|
+
`);
|
|
867
|
+
if (!choices.length) stdout.write(colors.dim(" (no saved accounts yet)\n"));
|
|
868
|
+
const render = (first) => {
|
|
869
|
+
if (!first) stdout.write(`\x1B[${choices.length + chrome}A`);
|
|
870
|
+
for (let i = 0; i < choices.length; i++) {
|
|
871
|
+
const active = i === selected;
|
|
872
|
+
const line = active ? `${colors.cyan("\u276F")} ${choices[i].label}` : ` ${choices[i].label}`;
|
|
873
|
+
stdout.write(`\x1B[2K\r${line}
|
|
874
|
+
`);
|
|
875
|
+
}
|
|
876
|
+
stdout.write(`\x1B[2K\r${colors.dim(` ${DIVIDER}`)}
|
|
877
|
+
`);
|
|
878
|
+
stdout.write(`\x1B[2K\r${footer}
|
|
879
|
+
`);
|
|
880
|
+
stdout.write(`\x1B[2K\r
|
|
881
|
+
`);
|
|
882
|
+
stdout.write(`\x1B[2K\r${hint}
|
|
883
|
+
`);
|
|
884
|
+
};
|
|
885
|
+
render(true);
|
|
886
|
+
stdin.setRawMode(true);
|
|
887
|
+
stdin.resume();
|
|
888
|
+
stdin.setEncoding("utf8");
|
|
889
|
+
stdout.write("\x1B[?1000h\x1B[?1006h");
|
|
890
|
+
const move = (delta) => {
|
|
891
|
+
if (!choices.length) return;
|
|
892
|
+
selected = (selected + delta + choices.length) % choices.length;
|
|
893
|
+
render(false);
|
|
894
|
+
};
|
|
895
|
+
const finish = (result) => {
|
|
896
|
+
stdout.write("\x1B[?1000l\x1B[?1006l");
|
|
897
|
+
stdin.setRawMode(false);
|
|
898
|
+
stdin.pause();
|
|
899
|
+
stdin.removeListener("data", onData);
|
|
900
|
+
resolve(result);
|
|
901
|
+
};
|
|
902
|
+
const onData = (data) => {
|
|
903
|
+
const mouse = /\x1b\[<(\d+);\d+;\d+[Mm]/.exec(data);
|
|
904
|
+
if (mouse) {
|
|
905
|
+
const btn = Number(mouse[1]);
|
|
906
|
+
if (btn === 64) move(-1);
|
|
907
|
+
else if (btn === 65) move(1);
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
if (data === "\x1B[A" || data === "\x1BOA" || data === "k") return move(-1);
|
|
911
|
+
if (data === "\x1B[B" || data === "\x1BOB" || data === "j") return move(1);
|
|
912
|
+
if (data === "r") return finish(rename);
|
|
913
|
+
if (data === "d") return finish(doctor);
|
|
914
|
+
if (data === "\r" || data === "\n") return finish(choices[selected] ?? null);
|
|
915
|
+
if (data === "" || data === "q" || data === "\x1B") return finish(null);
|
|
916
|
+
if (data.length === 1 && data >= "1" && data <= "9") {
|
|
917
|
+
const i = Number(data) - 1;
|
|
918
|
+
if (i < choices.length) finish(choices[i]);
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
stdin.on("data", onData);
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
async function runInteractiveMenu(manager, out) {
|
|
925
|
+
const io = {
|
|
926
|
+
pick: (title, choices) => interactiveSelect(title, choices),
|
|
927
|
+
ask: async (q) => {
|
|
928
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
929
|
+
const a = await rl.question(q);
|
|
930
|
+
rl.close();
|
|
931
|
+
return a;
|
|
932
|
+
},
|
|
933
|
+
confirm: async (q) => {
|
|
934
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
935
|
+
const a = (await rl.question(`${q} [y/N] `)).trim().toLowerCase();
|
|
936
|
+
rl.close();
|
|
937
|
+
return a === "y" || a === "yes";
|
|
938
|
+
},
|
|
939
|
+
doctor: async () => (await runDoctor(buildDoctorDeps())).report,
|
|
940
|
+
out
|
|
941
|
+
};
|
|
942
|
+
await runMenu(manager, io);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// src/daemon.ts
|
|
946
|
+
import { watchFile, unwatchFile } from "fs";
|
|
947
|
+
import { homedir as homedir3 } from "os";
|
|
948
|
+
import { join as join4 } from "path";
|
|
949
|
+
function formatReconcileLog(r) {
|
|
950
|
+
if (r.autoSaved) return `captured new login ${r.autoSaved.email}`;
|
|
951
|
+
if (r.removed && r.switchedTo) return `logged out of ${r.removed} \u2014 switched to ${r.switchedTo}`;
|
|
952
|
+
if (r.switchedTo) return `logged out \u2014 switched to ${r.switchedTo}`;
|
|
953
|
+
if (r.removed) return `logged out \u2014 removed ${r.removed}`;
|
|
954
|
+
return null;
|
|
955
|
+
}
|
|
956
|
+
async function reconcileAndLog(manager, log) {
|
|
957
|
+
try {
|
|
958
|
+
const r = await manager.reconcileOnChange();
|
|
959
|
+
const msg = formatReconcileLog(r);
|
|
960
|
+
if (msg) log(`[claudep] ${msg}`);
|
|
961
|
+
} catch {
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
function startDaemon(manager, opts = {}) {
|
|
965
|
+
const path = opts.watchPath ?? join4(homedir3(), ".claude.json");
|
|
966
|
+
const interval = opts.intervalMs ?? 1e3;
|
|
967
|
+
const log = opts.log ?? ((s) => console.log(s));
|
|
968
|
+
const subscribe = opts.watch ?? defaultWatch;
|
|
969
|
+
log(`[claudep] watching ${path} every ${interval}ms \u2014 instant capture on login/switch/logout`);
|
|
970
|
+
void reconcileAndLog(manager, log);
|
|
971
|
+
return subscribe(path, interval, () => void reconcileAndLog(manager, log));
|
|
972
|
+
}
|
|
973
|
+
function defaultWatch(path, intervalMs, onChange) {
|
|
974
|
+
const listener = () => onChange();
|
|
975
|
+
watchFile(path, { interval: intervalMs }, listener);
|
|
976
|
+
return () => unwatchFile(path, listener);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// src/autostart.ts
|
|
980
|
+
import { execFile as execFile2 } from "child_process";
|
|
981
|
+
import { promisify as promisify2 } from "util";
|
|
982
|
+
import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, rmSync as rmSync2, existsSync as existsSync3 } from "fs";
|
|
983
|
+
import { homedir as homedir4 } from "os";
|
|
984
|
+
import { dirname as dirname4, join as join5 } from "path";
|
|
985
|
+
var execFileP2 = promisify2(execFile2);
|
|
986
|
+
var LABEL = "com.claude-switcher-account.daemon";
|
|
987
|
+
var WIN_TASK = "claude-switcher-account";
|
|
988
|
+
var UNIT = "claude-switcher-account.service";
|
|
989
|
+
var LEGACY_LABEL = "com.claude-login-switcher.daemon";
|
|
990
|
+
var LEGACY_WIN_TASK = "claude-login-switcher";
|
|
991
|
+
var LEGACY_UNIT = "claude-login-switcher.service";
|
|
992
|
+
var defaultExec2 = async (file, args) => {
|
|
993
|
+
await execFileP2(file, args);
|
|
994
|
+
};
|
|
995
|
+
function launchdPlist(p) {
|
|
996
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
997
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
998
|
+
<plist version="1.0">
|
|
999
|
+
<dict>
|
|
1000
|
+
<key>Label</key><string>${LABEL}</string>
|
|
1001
|
+
<key>ProgramArguments</key>
|
|
1002
|
+
<array>
|
|
1003
|
+
<string>${p.nodePath}</string>
|
|
1004
|
+
<string>${p.binPath}</string>
|
|
1005
|
+
<string>daemon</string>
|
|
1006
|
+
<string>run</string>
|
|
1007
|
+
</array>
|
|
1008
|
+
<key>RunAtLoad</key><true/>
|
|
1009
|
+
<key>KeepAlive</key><true/>
|
|
1010
|
+
<key>StandardOutPath</key><string>${p.logPath}</string>
|
|
1011
|
+
<key>StandardErrorPath</key><string>${p.logPath}</string>
|
|
1012
|
+
</dict>
|
|
1013
|
+
</plist>
|
|
1014
|
+
`;
|
|
1015
|
+
}
|
|
1016
|
+
function systemdUnit(p) {
|
|
1017
|
+
return `[Unit]
|
|
1018
|
+
Description=claude-switcher-account instant login watcher
|
|
1019
|
+
|
|
1020
|
+
[Service]
|
|
1021
|
+
ExecStart=${p.nodePath} ${p.binPath} daemon run
|
|
1022
|
+
Restart=always
|
|
1023
|
+
StandardOutput=append:${p.logPath}
|
|
1024
|
+
StandardError=append:${p.logPath}
|
|
1025
|
+
|
|
1026
|
+
[Install]
|
|
1027
|
+
WantedBy=default.target
|
|
1028
|
+
`;
|
|
1029
|
+
}
|
|
1030
|
+
function windowsCreateArgs(p) {
|
|
1031
|
+
return ["/Create", "/F", "/SC", "ONLOGON", "/TN", WIN_TASK, "/TR", `"${p.nodePath}" "${p.binPath}" daemon run`];
|
|
1032
|
+
}
|
|
1033
|
+
function plistPath(home = homedir4()) {
|
|
1034
|
+
return join5(home, "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
1035
|
+
}
|
|
1036
|
+
function systemdPath(home = homedir4()) {
|
|
1037
|
+
return join5(home, ".config", "systemd", "user", UNIT);
|
|
1038
|
+
}
|
|
1039
|
+
function legacyPlistPath(home = homedir4()) {
|
|
1040
|
+
return join5(home, "Library", "LaunchAgents", `${LEGACY_LABEL}.plist`);
|
|
1041
|
+
}
|
|
1042
|
+
function legacySystemdPath(home = homedir4()) {
|
|
1043
|
+
return join5(home, ".config", "systemd", "user", LEGACY_UNIT);
|
|
1044
|
+
}
|
|
1045
|
+
function autostartInstalled(platform, home = homedir4()) {
|
|
1046
|
+
if (platform === "darwin") return existsSync3(plistPath(home)) || existsSync3(legacyPlistPath(home));
|
|
1047
|
+
if (platform === "linux") return existsSync3(systemdPath(home)) || existsSync3(legacySystemdPath(home));
|
|
1048
|
+
return false;
|
|
1049
|
+
}
|
|
1050
|
+
function writeFile(path, contents) {
|
|
1051
|
+
mkdirSync4(dirname4(path), { recursive: true });
|
|
1052
|
+
writeFileSync4(path, contents);
|
|
1053
|
+
}
|
|
1054
|
+
async function removeLegacyAutostart(platform, home = homedir4(), exec = defaultExec2) {
|
|
1055
|
+
if (platform === "darwin") {
|
|
1056
|
+
const plist = legacyPlistPath(home);
|
|
1057
|
+
await exec("launchctl", ["unload", plist]).catch(() => void 0);
|
|
1058
|
+
if (existsSync3(plist)) rmSync2(plist, { force: true });
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
if (platform === "linux") {
|
|
1062
|
+
await exec("systemctl", ["--user", "disable", "--now", LEGACY_UNIT]).catch(() => void 0);
|
|
1063
|
+
const unit = legacySystemdPath(home);
|
|
1064
|
+
if (existsSync3(unit)) rmSync2(unit, { force: true });
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
if (platform === "win32") {
|
|
1068
|
+
await exec("schtasks", ["/Delete", "/F", "/TN", LEGACY_WIN_TASK]).catch(() => void 0);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
async function installAutostart(platform, paths, home = homedir4(), exec = defaultExec2) {
|
|
1072
|
+
await removeLegacyAutostart(platform, home, exec);
|
|
1073
|
+
if (platform === "darwin") {
|
|
1074
|
+
const plist = plistPath(home);
|
|
1075
|
+
writeFile(plist, launchdPlist(paths));
|
|
1076
|
+
await exec("launchctl", ["unload", plist]).catch(() => void 0);
|
|
1077
|
+
await exec("launchctl", ["load", "-w", plist]);
|
|
1078
|
+
return `launchd agent installed at ${plist}`;
|
|
1079
|
+
}
|
|
1080
|
+
if (platform === "linux") {
|
|
1081
|
+
const unit = systemdPath(home);
|
|
1082
|
+
writeFile(unit, systemdUnit(paths));
|
|
1083
|
+
await exec("systemctl", ["--user", "daemon-reload"]);
|
|
1084
|
+
await exec("systemctl", ["--user", "enable", "--now", UNIT]);
|
|
1085
|
+
return `systemd user service installed at ${unit}`;
|
|
1086
|
+
}
|
|
1087
|
+
if (platform === "win32") {
|
|
1088
|
+
await exec("schtasks", windowsCreateArgs(paths));
|
|
1089
|
+
return `Task Scheduler task '${WIN_TASK}' created (runs at logon)`;
|
|
1090
|
+
}
|
|
1091
|
+
throw new Error(`Autostart not supported on ${platform}.`);
|
|
1092
|
+
}
|
|
1093
|
+
async function uninstallAutostart(platform, home = homedir4(), exec = defaultExec2) {
|
|
1094
|
+
await removeLegacyAutostart(platform, home, exec);
|
|
1095
|
+
if (platform === "darwin") {
|
|
1096
|
+
const plist = plistPath(home);
|
|
1097
|
+
await exec("launchctl", ["unload", plist]).catch(() => void 0);
|
|
1098
|
+
if (existsSync3(plist)) rmSync2(plist, { force: true });
|
|
1099
|
+
return "launchd agent removed";
|
|
1100
|
+
}
|
|
1101
|
+
if (platform === "linux") {
|
|
1102
|
+
await exec("systemctl", ["--user", "disable", "--now", UNIT]).catch(() => void 0);
|
|
1103
|
+
const unit = systemdPath(home);
|
|
1104
|
+
if (existsSync3(unit)) rmSync2(unit, { force: true });
|
|
1105
|
+
return "systemd user service removed";
|
|
1106
|
+
}
|
|
1107
|
+
if (platform === "win32") {
|
|
1108
|
+
await exec("schtasks", ["/Delete", "/F", "/TN", WIN_TASK]);
|
|
1109
|
+
return `Task Scheduler task '${WIN_TASK}' removed`;
|
|
1110
|
+
}
|
|
1111
|
+
throw new Error(`Autostart not supported on ${platform}.`);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// src/cli.ts
|
|
1115
|
+
async function defaultConfirm(question) {
|
|
1116
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
1117
|
+
const answer = (await rl.question(`${question} [y/N] `)).trim().toLowerCase();
|
|
1118
|
+
rl.close();
|
|
1119
|
+
return answer === "y" || answer === "yes";
|
|
1120
|
+
}
|
|
1121
|
+
function buildProgram(deps = {}) {
|
|
1122
|
+
const out = deps.out ?? ((s) => console.log(s));
|
|
1123
|
+
const now = deps.now ?? (() => Date.now());
|
|
1124
|
+
const confirm = deps.confirm ?? defaultConfirm;
|
|
1125
|
+
const manager = () => deps.manager ?? createManager();
|
|
1126
|
+
const checkUpdate = deps.checkUpdate ?? ((current) => checkForUpdate(current));
|
|
1127
|
+
const runMenu2 = deps.menu ?? runInteractiveMenu;
|
|
1128
|
+
const useColor = process.stdout.isTTY === true && !process.env.NO_COLOR;
|
|
1129
|
+
const colors = makeColors(useColor);
|
|
1130
|
+
const ok = (s) => ` ${colors.green(SYMBOLS.ok)} ${s}`;
|
|
1131
|
+
const err = (s) => ` ${colors.red(SYMBOLS.err)} ${s}`;
|
|
1132
|
+
const note = (s) => colors.dim(` ${s}`);
|
|
1133
|
+
const listHint = `run ${colors.cyan("claudep list")} to see saved accounts`;
|
|
1134
|
+
const program2 = new Command();
|
|
1135
|
+
program2.name("claudep").description("Switch between Claude Code logins").version(VERSION).exitOverride();
|
|
1136
|
+
async function reconcileFirst(mgr) {
|
|
1137
|
+
try {
|
|
1138
|
+
await mgr.reconcileOnChange();
|
|
1139
|
+
} catch {
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
async function switchByArg(mgr, arg, bareNumeric) {
|
|
1143
|
+
const isNum = /^\d+$/.test(arg);
|
|
1144
|
+
if (bareNumeric && !isNum) {
|
|
1145
|
+
out(err(`Unknown account '${arg}'`) + "\n" + note(listHint));
|
|
1146
|
+
process.exitCode = 1;
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
const name = isNum ? await mgr.resolveByIndex(Number(arg)) : arg;
|
|
1150
|
+
if (!name) {
|
|
1151
|
+
out(err(`No account #${arg}`) + "\n" + note(listHint));
|
|
1152
|
+
process.exitCode = 1;
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
try {
|
|
1156
|
+
await mgr.switchTo(name);
|
|
1157
|
+
out(ok(`Now using ${name}`) + "\n" + note("restart running claude sessions to apply"));
|
|
1158
|
+
} catch (e) {
|
|
1159
|
+
out(err(e.message) + "\n" + note(listHint));
|
|
1160
|
+
process.exitCode = 1;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
program2.argument("[target]", "account number to switch to").action(async (target) => {
|
|
1164
|
+
if (target !== void 0) {
|
|
1165
|
+
const mgr = manager();
|
|
1166
|
+
await reconcileFirst(mgr);
|
|
1167
|
+
await switchByArg(mgr, target, true);
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
if (!deps.menu && (!process.stdin.isTTY || !process.stdout.isTTY)) {
|
|
1171
|
+
out(program2.helpInformation());
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
await runMenu2(manager(), out);
|
|
1175
|
+
});
|
|
1176
|
+
program2.command("version").description("Show the version and check npm for a newer one").action(async () => {
|
|
1177
|
+
out(formatVersion(await checkUpdate(VERSION), colors));
|
|
1178
|
+
});
|
|
1179
|
+
program2.command("list").aliases(["ls", "status", "current"]).description("List saved accounts").option("--json", "machine-readable output").action(async (opts) => {
|
|
1180
|
+
const mgr = manager();
|
|
1181
|
+
await reconcileFirst(mgr);
|
|
1182
|
+
const views = await mgr.listAccounts();
|
|
1183
|
+
if (opts.json) {
|
|
1184
|
+
out(JSON.stringify(views, null, 2));
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
const cur = mgr.current();
|
|
1188
|
+
const external = !cur.saved && cur.label ? cur.label.email : void 0;
|
|
1189
|
+
out(formatList(views, now(), colors, { external }));
|
|
1190
|
+
});
|
|
1191
|
+
program2.command("switch <target>").alias("use").description("Make an account the active login (by name or number)").action(async (target) => {
|
|
1192
|
+
const mgr = manager();
|
|
1193
|
+
await reconcileFirst(mgr);
|
|
1194
|
+
await switchByArg(mgr, target, false);
|
|
1195
|
+
});
|
|
1196
|
+
program2.command("remove <target>").alias("rm").description("Forget a saved account (by name or number)").option("-y, --yes", "skip confirmation").action(async (target, opts) => {
|
|
1197
|
+
const mgr = manager();
|
|
1198
|
+
await reconcileFirst(mgr);
|
|
1199
|
+
const name = /^\d+$/.test(target) ? await mgr.resolveByIndex(Number(target)) : target;
|
|
1200
|
+
if (!name) {
|
|
1201
|
+
out(err(`No account #${target}`) + "\n" + note(listHint));
|
|
1202
|
+
process.exitCode = 1;
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
if (!opts.yes) {
|
|
1206
|
+
const yes = await confirm(`Remove ${name}? This deletes its stored credential.`);
|
|
1207
|
+
if (!yes) {
|
|
1208
|
+
out(" Cancelled.");
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
await mgr.removeAccount(name);
|
|
1213
|
+
out(ok(`Removed ${name}`));
|
|
1214
|
+
});
|
|
1215
|
+
program2.command("rename <old> <new>").description("Relabel a saved account").action(async (oldName, newName) => {
|
|
1216
|
+
const mgr = manager();
|
|
1217
|
+
await reconcileFirst(mgr);
|
|
1218
|
+
try {
|
|
1219
|
+
await mgr.renameAccount(oldName, newName);
|
|
1220
|
+
out(ok(`Renamed ${oldName} ${colors.dim("\u2192")} ${newName}`));
|
|
1221
|
+
} catch (e) {
|
|
1222
|
+
out(err(e.message));
|
|
1223
|
+
process.exitCode = 1;
|
|
1224
|
+
}
|
|
1225
|
+
});
|
|
1226
|
+
program2.command("doctor").description("Diagnose credential path, config, and store").action(async () => {
|
|
1227
|
+
const { ok: healthy, report } = await runDoctor(buildDoctorDeps(), colors);
|
|
1228
|
+
out(report);
|
|
1229
|
+
if (!healthy) process.exitCode = 1;
|
|
1230
|
+
});
|
|
1231
|
+
const daemonPaths = () => ({
|
|
1232
|
+
nodePath: process.execPath,
|
|
1233
|
+
binPath: process.argv[1] ?? "",
|
|
1234
|
+
logPath: join6(homedir5(), ".claude-profiles", "daemon.log")
|
|
1235
|
+
});
|
|
1236
|
+
const daemon = program2.command("daemon").description("Background watcher for instant capture/switch");
|
|
1237
|
+
daemon.command("run").description("Run the watcher in the foreground (used by the OS autostart service)").action(async () => {
|
|
1238
|
+
startDaemon(manager(), { log: out });
|
|
1239
|
+
await new Promise(() => {
|
|
1240
|
+
});
|
|
1241
|
+
});
|
|
1242
|
+
daemon.command("install").description("Install autostart so the watcher runs on login, and capture the current login now").action(async () => {
|
|
1243
|
+
try {
|
|
1244
|
+
const msg = await installAutostart(process.platform, daemonPaths());
|
|
1245
|
+
await manager().reconcileOnChange().catch(() => void 0);
|
|
1246
|
+
out(ok(msg) + "\n" + note("instant capture is on \u2014 new logins are saved automatically"));
|
|
1247
|
+
} catch (e) {
|
|
1248
|
+
out(err(e.message));
|
|
1249
|
+
process.exitCode = 1;
|
|
1250
|
+
}
|
|
1251
|
+
});
|
|
1252
|
+
daemon.command("uninstall").description("Remove the autostart watcher").action(async () => {
|
|
1253
|
+
try {
|
|
1254
|
+
out(ok(await uninstallAutostart(process.platform)));
|
|
1255
|
+
} catch (e) {
|
|
1256
|
+
out(err(e.message));
|
|
1257
|
+
process.exitCode = 1;
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
daemon.command("status").description("Show whether the autostart watcher is installed").action(() => {
|
|
1261
|
+
const on = autostartInstalled(process.platform);
|
|
1262
|
+
out(
|
|
1263
|
+
on ? ` ${colors.green(SYMBOLS.active)} daemon autostart installed` : ` ${colors.dim(`${SYMBOLS.off} daemon not installed`)}
|
|
1264
|
+
` + note(`run: ${colors.cyan("claudep daemon install")}`)
|
|
1265
|
+
);
|
|
1266
|
+
});
|
|
1267
|
+
return program2;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// src/bin.ts
|
|
1271
|
+
var program = buildProgram();
|
|
1272
|
+
try {
|
|
1273
|
+
await program.parseAsync(process.argv);
|
|
1274
|
+
} catch (err) {
|
|
1275
|
+
const code = err.exitCode ?? 1;
|
|
1276
|
+
process.exit(code);
|
|
1277
|
+
}
|