devswitch-cli 1.0.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.
Files changed (3) hide show
  1. package/README.md +182 -0
  2. package/bin/devswitch.cjs +2440 -0
  3. package/package.json +49 -0
@@ -0,0 +1,2440 @@
1
+ #!/usr/bin/env node
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+
29
+ // ../core/paths.ts
30
+ var os = __toESM(require("os"), 1);
31
+ var path = __toESM(require("path"), 1);
32
+ var fs = __toESM(require("fs"), 1);
33
+ var APP_DIR_NAME = "devswitch";
34
+ function getDataDir() {
35
+ const override = process.env.DEVSWITCH_DATA_DIR;
36
+ if (override && override.trim()) {
37
+ return override.trim();
38
+ }
39
+ const platform2 = os.platform();
40
+ const home = os.homedir();
41
+ if (platform2 === "win32") {
42
+ const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
43
+ return path.join(appData, APP_DIR_NAME);
44
+ }
45
+ if (platform2 === "darwin") {
46
+ return path.join(home, "Library", "Application Support", APP_DIR_NAME);
47
+ }
48
+ const xdg = process.env.XDG_CONFIG_HOME;
49
+ const base = xdg && xdg.trim() ? xdg.trim() : path.join(home, ".config");
50
+ return path.join(base, APP_DIR_NAME);
51
+ }
52
+ function ensureDataDir() {
53
+ const dir = getDataDir();
54
+ if (!fs.existsSync(dir)) {
55
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
56
+ }
57
+ return dir;
58
+ }
59
+ function getProfilesFilePath() {
60
+ return path.join(getDataDir(), "profiles.json");
61
+ }
62
+ function getLogsFilePath() {
63
+ return path.join(getDataDir(), "logs.json");
64
+ }
65
+ function getLegacyStoreCandidates() {
66
+ const platform2 = os.platform();
67
+ const home = os.homedir();
68
+ let configBases = [];
69
+ if (platform2 === "win32") {
70
+ const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
71
+ configBases = [appData];
72
+ } else if (platform2 === "darwin") {
73
+ configBases = [path.join(home, "Library", "Application Support")];
74
+ } else {
75
+ const xdg = process.env.XDG_CONFIG_HOME;
76
+ const base = xdg && xdg.trim() ? xdg.trim() : path.join(home, ".config");
77
+ configBases = [base];
78
+ }
79
+ const legacyAppFolders = ["dev-switch", "DevSwitch", "Electron"];
80
+ const profiles = [];
81
+ const logs = [];
82
+ for (const cfg of configBases) {
83
+ for (const folder of legacyAppFolders) {
84
+ profiles.push(path.join(cfg, folder, "dev-switch-data.json"));
85
+ logs.push(path.join(cfg, folder, "dev-switch-logs.json"));
86
+ }
87
+ }
88
+ return { profiles, logs };
89
+ }
90
+
91
+ // ../core/jsonStore.ts
92
+ var fs2 = __toESM(require("fs"), 1);
93
+ var path2 = __toESM(require("path"), 1);
94
+ var JsonStore = class {
95
+ filePath;
96
+ defaults;
97
+ constructor(filePath, defaults) {
98
+ this.filePath = filePath;
99
+ this.defaults = defaults;
100
+ }
101
+ getFilePath() {
102
+ return this.filePath;
103
+ }
104
+ /** Read the entire document fresh from disk, falling back to defaults. */
105
+ read() {
106
+ try {
107
+ if (!fs2.existsSync(this.filePath)) {
108
+ return structuredClone(this.defaults);
109
+ }
110
+ const raw = fs2.readFileSync(this.filePath, "utf8");
111
+ if (!raw.trim()) {
112
+ return structuredClone(this.defaults);
113
+ }
114
+ const parsed = JSON.parse(raw);
115
+ return { ...structuredClone(this.defaults), ...parsed };
116
+ } catch {
117
+ return structuredClone(this.defaults);
118
+ }
119
+ }
120
+ /** Atomically write the entire document to disk. */
121
+ write(data) {
122
+ ensureDataDir();
123
+ const dir = path2.dirname(this.filePath);
124
+ const tmp = path2.join(
125
+ dir,
126
+ `.${path2.basename(this.filePath)}.${process.pid}.tmp`
127
+ );
128
+ const json = JSON.stringify(data, null, 2);
129
+ fs2.writeFileSync(tmp, json, { mode: 384 });
130
+ fs2.renameSync(tmp, this.filePath);
131
+ }
132
+ get(key, fallback) {
133
+ const data = this.read();
134
+ const value = data[key];
135
+ return value === void 0 ? fallback : value;
136
+ }
137
+ set(key, value) {
138
+ const data = this.read();
139
+ data[key] = value;
140
+ this.write(data);
141
+ }
142
+ };
143
+
144
+ // ../core/utils/encryption.ts
145
+ var import_crypto = __toESM(require("crypto"), 1);
146
+ var import_os = __toESM(require("os"), 1);
147
+ function getMachineKey() {
148
+ const machineId = import_os.default.hostname() + import_os.default.platform() + import_os.default.arch();
149
+ return import_crypto.default.scryptSync(machineId, "dev-switch-salt", 32);
150
+ }
151
+ function encryptPassphrase(passphrase) {
152
+ if (!passphrase) return "";
153
+ const key = getMachineKey();
154
+ const iv = import_crypto.default.randomBytes(16);
155
+ const cipher = import_crypto.default.createCipheriv("aes-256-gcm", key, iv);
156
+ let encrypted = cipher.update(passphrase, "utf8", "hex");
157
+ encrypted += cipher.final("hex");
158
+ const authTag = cipher.getAuthTag();
159
+ return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
160
+ }
161
+
162
+ // ../core/utils/environment.ts
163
+ var import_os2 = __toESM(require("os"), 1);
164
+ var isWindows = import_os2.default.platform() === "win32";
165
+ var isMac = import_os2.default.platform() === "darwin";
166
+ var isLinux = import_os2.default.platform() === "linux";
167
+ var isDev = process.env.NODE_ENV === "development";
168
+
169
+ // ../core/utils/providerUtils.ts
170
+ var PROVIDER_SSH_CONFIGS = {
171
+ github: { sshHost: "github.com", sshUser: "git" },
172
+ gitlab: { sshHost: "gitlab.com", sshUser: "git" },
173
+ bitbucket: { sshHost: "bitbucket.org", sshUser: "git" },
174
+ azure: { sshHost: "ssh.dev.azure.com", sshUser: "git" },
175
+ other: { sshHost: "git.example.com", sshUser: "git" }
176
+ };
177
+ function getProviderSSHConfig(provider) {
178
+ return PROVIDER_SSH_CONFIGS[provider || "github"];
179
+ }
180
+ function isSSHAuthSuccess(output) {
181
+ const lower = output.toLowerCase();
182
+ return lower.includes("authenticated") || // GitHub: "You've successfully authenticated"
183
+ lower.includes("welcome to gitlab") || lower.includes("logged in as") || // Bitbucket
184
+ lower.includes("shell access is not permitted");
185
+ }
186
+
187
+ // ../core/services/storageService.ts
188
+ var fs3 = __toESM(require("fs"), 1);
189
+ var StorageService = class {
190
+ store;
191
+ constructor() {
192
+ this.store = new JsonStore(getProfilesFilePath(), {
193
+ profiles: []
194
+ });
195
+ this.migrateFromElectronStoreIfNeeded();
196
+ }
197
+ /**
198
+ * One-time import of profiles from the old electron-store JSON files.
199
+ * Runs only if the new store has never been migrated AND is empty, so it
200
+ * never clobbers data created directly in the new location.
201
+ */
202
+ migrateFromElectronStoreIfNeeded() {
203
+ try {
204
+ const current = this.store.read();
205
+ if (current._migratedFromElectronStore) return;
206
+ if (current.profiles && current.profiles.length > 0) {
207
+ current._migratedFromElectronStore = true;
208
+ this.store.write(current);
209
+ return;
210
+ }
211
+ const { profiles: candidates } = getLegacyStoreCandidates();
212
+ for (const candidate of candidates) {
213
+ if (!fs3.existsSync(candidate)) continue;
214
+ try {
215
+ const raw = fs3.readFileSync(candidate, "utf8");
216
+ if (!raw.trim()) continue;
217
+ const parsed = JSON.parse(raw);
218
+ if (parsed.profiles && parsed.profiles.length > 0) {
219
+ current.profiles = parsed.profiles;
220
+ current._migratedFromElectronStore = true;
221
+ this.store.write(current);
222
+ return;
223
+ }
224
+ } catch {
225
+ }
226
+ }
227
+ current._migratedFromElectronStore = true;
228
+ this.store.write(current);
229
+ } catch {
230
+ }
231
+ }
232
+ getAllProfiles() {
233
+ return this.store.get("profiles", []);
234
+ }
235
+ getProfile(id) {
236
+ return this.getAllProfiles().find((p) => p.id === id);
237
+ }
238
+ /**
239
+ * Look up a profile by a human-friendly identifier used on the CLI:
240
+ * matches (case-insensitively) against name, username, or email, and also
241
+ * accepts a full/lowercased id. Returns the first match.
242
+ */
243
+ findProfile(identifier) {
244
+ const profiles = this.getAllProfiles();
245
+ const needle = identifier.trim().toLowerCase();
246
+ const byId = profiles.find((p) => p.id.toLowerCase() === needle);
247
+ if (byId) return byId;
248
+ const exact = profiles.find(
249
+ (p) => p.name.toLowerCase() === needle || p.username.toLowerCase() === needle || p.email.toLowerCase() === needle
250
+ );
251
+ if (exact) return exact;
252
+ return profiles.find(
253
+ (p) => p.name.toLowerCase().includes(needle) || p.username.toLowerCase().includes(needle)
254
+ );
255
+ }
256
+ saveProfile(profile) {
257
+ const profiles = this.getAllProfiles();
258
+ const index = profiles.findIndex((p) => p.id === profile.id);
259
+ if (index >= 0) {
260
+ profiles[index] = profile;
261
+ } else {
262
+ profiles.push(profile);
263
+ }
264
+ this.store.set("profiles", profiles);
265
+ }
266
+ deleteProfile(id) {
267
+ const profiles = this.getAllProfiles();
268
+ const filtered = profiles.filter((p) => p.id !== id);
269
+ if (filtered.length < profiles.length) {
270
+ this.store.set("profiles", filtered);
271
+ return true;
272
+ }
273
+ return false;
274
+ }
275
+ clear() {
276
+ this.store.set("profiles", []);
277
+ }
278
+ };
279
+ var storageService = new StorageService();
280
+
281
+ // ../core/services/logService.ts
282
+ var fs4 = __toESM(require("fs"), 1);
283
+ var import_crypto2 = require("crypto");
284
+ var LogService = class {
285
+ store;
286
+ maxLogs = 500;
287
+ /** Default source tag for logs created in this process. */
288
+ defaultSource = "app";
289
+ constructor() {
290
+ this.store = new JsonStore(getLogsFilePath(), {
291
+ logs: []
292
+ });
293
+ this.migrateFromElectronStoreIfNeeded();
294
+ }
295
+ /** Set the origin tag (the CLI entrypoint calls this with 'cli'). */
296
+ setDefaultSource(source) {
297
+ this.defaultSource = source;
298
+ }
299
+ migrateFromElectronStoreIfNeeded() {
300
+ try {
301
+ const current = this.store.read();
302
+ if (current._migratedFromElectronStore) return;
303
+ if (current.logs && current.logs.length > 0) {
304
+ current._migratedFromElectronStore = true;
305
+ this.store.write(current);
306
+ return;
307
+ }
308
+ const { logs: candidates } = getLegacyStoreCandidates();
309
+ for (const candidate of candidates) {
310
+ if (!fs4.existsSync(candidate)) continue;
311
+ try {
312
+ const raw = fs4.readFileSync(candidate, "utf8");
313
+ if (!raw.trim()) continue;
314
+ const parsed = JSON.parse(raw);
315
+ if (parsed.logs && parsed.logs.length > 0) {
316
+ current.logs = parsed.logs;
317
+ current._migratedFromElectronStore = true;
318
+ this.store.write(current);
319
+ return;
320
+ }
321
+ } catch {
322
+ }
323
+ }
324
+ current._migratedFromElectronStore = true;
325
+ this.store.write(current);
326
+ } catch {
327
+ }
328
+ }
329
+ getAllLogs() {
330
+ return this.store.get("logs", []);
331
+ }
332
+ addLog(action, message, details, source) {
333
+ const log = {
334
+ id: (0, import_crypto2.randomUUID)(),
335
+ timestamp: Date.now(),
336
+ action,
337
+ message,
338
+ source: source ?? this.defaultSource,
339
+ details
340
+ };
341
+ const logs = this.getAllLogs();
342
+ logs.unshift(log);
343
+ if (logs.length > this.maxLogs) {
344
+ logs.length = this.maxLogs;
345
+ }
346
+ this.store.set("logs", logs);
347
+ return log;
348
+ }
349
+ clearLogs() {
350
+ this.store.set("logs", []);
351
+ }
352
+ clearLogsBefore(timestamp) {
353
+ if (typeof timestamp !== "number" || Number.isNaN(timestamp) || !Number.isFinite(timestamp)) {
354
+ return;
355
+ }
356
+ const logs = this.getAllLogs();
357
+ const filtered = logs.filter((log) => log.timestamp >= timestamp);
358
+ this.store.set("logs", filtered);
359
+ }
360
+ };
361
+ var logService = new LogService();
362
+
363
+ // ../core/services/sshKeyService.ts
364
+ var import_child_process = require("child_process");
365
+ var import_util = require("util");
366
+ var path3 = __toESM(require("path"), 1);
367
+ var fs5 = __toESM(require("fs"), 1);
368
+ var import_os3 = __toESM(require("os"), 1);
369
+ var execAsync = (0, import_util.promisify)(import_child_process.exec);
370
+ var SSHKeyService = class {
371
+ getSshDir() {
372
+ return path3.join(import_os3.default.homedir(), ".ssh");
373
+ }
374
+ async generateKey(params) {
375
+ try {
376
+ const sshDir = this.getSshDir();
377
+ if (!fs5.existsSync(sshDir)) {
378
+ fs5.mkdirSync(sshDir, { mode: 448 });
379
+ }
380
+ const keyPath = path3.join(sshDir, params.name);
381
+ if (fs5.existsSync(keyPath)) {
382
+ return {
383
+ success: false,
384
+ error: `SSH key '${params.name}' already exists`
385
+ };
386
+ }
387
+ let command;
388
+ if (params.algorithm === "ed25519") {
389
+ command = `ssh-keygen -t ed25519 -C "${params.email}" -f "${keyPath}" -N "${params.passphrase || ""}"`;
390
+ } else {
391
+ command = `ssh-keygen -t rsa -b 4096 -C "${params.email}" -f "${keyPath}" -N "${params.passphrase || ""}"`;
392
+ }
393
+ await execAsync(command);
394
+ if (fs5.existsSync(keyPath)) {
395
+ fs5.chmodSync(keyPath, 384);
396
+ if (fs5.existsSync(`${keyPath}.pub`)) {
397
+ fs5.chmodSync(`${keyPath}.pub`, 420);
398
+ }
399
+ logService.addLog(
400
+ "SSH_KEY_GENERATED",
401
+ `SSH key '${params.name}' generated`,
402
+ {
403
+ keyPath,
404
+ algorithm: params.algorithm
405
+ }
406
+ );
407
+ return { success: true, keyPath };
408
+ }
409
+ return {
410
+ success: false,
411
+ error: "Key generation failed - file not created"
412
+ };
413
+ } catch (error2) {
414
+ return {
415
+ success: false,
416
+ error: error2 instanceof Error ? error2.message : "Unknown error"
417
+ };
418
+ }
419
+ }
420
+ getDefaultKeyPath() {
421
+ return path3.join(this.getSshDir(), "id_rsa");
422
+ }
423
+ keyExists(keyPath) {
424
+ return fs5.existsSync(keyPath);
425
+ }
426
+ getPublicKeyContent(privateKeyPath) {
427
+ const publicKeyPath = `${privateKeyPath}.pub`;
428
+ if (fs5.existsSync(publicKeyPath)) {
429
+ return fs5.readFileSync(publicKeyPath, "utf8").trim();
430
+ }
431
+ return null;
432
+ }
433
+ deleteKey(keyPath) {
434
+ try {
435
+ if (fs5.existsSync(keyPath)) {
436
+ fs5.unlinkSync(keyPath);
437
+ }
438
+ const publicKeyPath = `${keyPath}.pub`;
439
+ if (fs5.existsSync(publicKeyPath)) {
440
+ fs5.unlinkSync(publicKeyPath);
441
+ }
442
+ return { success: true };
443
+ } catch (error2) {
444
+ return {
445
+ success: false,
446
+ error: error2 instanceof Error ? error2.message : "Failed to delete key"
447
+ };
448
+ }
449
+ }
450
+ async checkDefaultKeys() {
451
+ const sshDir = this.getSshDir();
452
+ const defaultKeys = [];
453
+ const rsaPrivatePath = path3.join(sshDir, "id_rsa");
454
+ const rsaPublicPath = `${rsaPrivatePath}.pub`;
455
+ if (fs5.existsSync(rsaPrivatePath) && fs5.existsSync(rsaPublicPath)) {
456
+ defaultKeys.push({
457
+ algorithm: "rsa",
458
+ privatePath: rsaPrivatePath,
459
+ publicPath: rsaPublicPath
460
+ });
461
+ }
462
+ const ed25519PrivatePath = path3.join(sshDir, "id_ed25519");
463
+ const ed25519PublicPath = `${ed25519PrivatePath}.pub`;
464
+ if (fs5.existsSync(ed25519PrivatePath) && fs5.existsSync(ed25519PublicPath)) {
465
+ defaultKeys.push({
466
+ algorithm: "ed25519",
467
+ privatePath: ed25519PrivatePath,
468
+ publicPath: ed25519PublicPath
469
+ });
470
+ }
471
+ return defaultKeys;
472
+ }
473
+ /**
474
+ * Scan the .ssh directory and find all SSH key pairs.
475
+ */
476
+ async scanAllSSHKeys() {
477
+ const sshDir = this.getSshDir();
478
+ const keys = [];
479
+ try {
480
+ if (!fs5.existsSync(sshDir)) {
481
+ return keys;
482
+ }
483
+ const files = fs5.readdirSync(sshDir);
484
+ const pubFiles = files.filter((f) => f.endsWith(".pub"));
485
+ for (const pubFile of pubFiles) {
486
+ const publicPath = path3.join(sshDir, pubFile);
487
+ const privateFileName = pubFile.replace(".pub", "");
488
+ const privatePath = path3.join(sshDir, privateFileName);
489
+ if (!fs5.existsSync(privatePath)) {
490
+ continue;
491
+ }
492
+ const publicKeyContent = fs5.readFileSync(publicPath, "utf8").trim();
493
+ const email = this.extractEmailFromPublicKey(publicKeyContent);
494
+ let algorithm = "rsa";
495
+ if (publicKeyContent.startsWith("ssh-ed25519")) algorithm = "ed25519";
496
+ else if (publicKeyContent.startsWith("ssh-rsa")) algorithm = "rsa";
497
+ else if (publicKeyContent.startsWith("ecdsa-sha2")) algorithm = "ecdsa";
498
+ else if (publicKeyContent.startsWith("ssh-dss")) algorithm = "dsa";
499
+ keys.push({ privatePath, publicPath, email, algorithm });
500
+ }
501
+ return keys;
502
+ } catch (error2) {
503
+ console.error("Error scanning SSH keys:", error2);
504
+ return keys;
505
+ }
506
+ }
507
+ extractEmailFromPublicKey(publicKeyContent) {
508
+ try {
509
+ const parts = publicKeyContent.trim().split(/\s+/);
510
+ if (parts.length >= 3) {
511
+ const lastPart = parts[parts.length - 1];
512
+ if (lastPart.includes("@")) {
513
+ return lastPart;
514
+ }
515
+ }
516
+ return null;
517
+ } catch {
518
+ return null;
519
+ }
520
+ }
521
+ };
522
+ var sshKeyService = new SSHKeyService();
523
+
524
+ // ../core/services/sshAgentService.ts
525
+ var import_child_process2 = require("child_process");
526
+ var import_util2 = require("util");
527
+ var fs6 = __toESM(require("fs"), 1);
528
+ var execAsync2 = (0, import_util2.promisify)(import_child_process2.exec);
529
+ var SSHAgentService = class {
530
+ async addKeyToAgent(params) {
531
+ try {
532
+ if (!fs6.existsSync(params.keyPath)) {
533
+ return {
534
+ success: false,
535
+ error: `SSH key not found at ${params.keyPath}`
536
+ };
537
+ }
538
+ if (isWindows) {
539
+ return await this.addKeyToAgentWindows(params);
540
+ } else {
541
+ return await this.addKeyToAgentUnix(params);
542
+ }
543
+ } catch (error2) {
544
+ return {
545
+ success: false,
546
+ error: error2 instanceof Error ? error2.message : "Failed to add key to agent"
547
+ };
548
+ }
549
+ }
550
+ async addKeyToAgentUnix(params) {
551
+ try {
552
+ const agentCheck = await execAsync2("pgrep ssh-agent").catch(() => null);
553
+ if (!agentCheck) {
554
+ try {
555
+ await execAsync2('eval "$(ssh-agent -s)"');
556
+ } catch (error2) {
557
+ console.error("Failed to start ssh-agent:", error2);
558
+ return {
559
+ success: false,
560
+ error: "ssh-agent is not running and could not be started"
561
+ };
562
+ }
563
+ }
564
+ let command;
565
+ if (params.passphrase) {
566
+ command = `echo "${params.passphrase}" | ssh-add "${params.keyPath}"`;
567
+ } else {
568
+ command = `ssh-add "${params.keyPath}"`;
569
+ }
570
+ await execAsync2(command);
571
+ return { success: true };
572
+ } catch (error2) {
573
+ return {
574
+ success: false,
575
+ error: error2 instanceof Error ? error2.message : "Failed to add key to agent"
576
+ };
577
+ }
578
+ }
579
+ async addKeyToAgentWindows(params) {
580
+ try {
581
+ const serviceCheck = await execAsync2("sc query ssh-agent").catch(
582
+ () => null
583
+ );
584
+ if (!serviceCheck || !serviceCheck.stdout.includes("RUNNING")) {
585
+ try {
586
+ await execAsync2("Start-Service ssh-agent", {
587
+ shell: "powershell.exe"
588
+ });
589
+ } catch (error2) {
590
+ console.error("Failed to start ssh-agent service:", error2);
591
+ return {
592
+ success: false,
593
+ error: "ssh-agent service is not running and could not be started"
594
+ };
595
+ }
596
+ }
597
+ const command = `ssh-add "${params.keyPath}"`;
598
+ await execAsync2(command);
599
+ return { success: true };
600
+ } catch (error2) {
601
+ return {
602
+ success: false,
603
+ error: error2 instanceof Error ? error2.message : "Failed to add key to agent"
604
+ };
605
+ }
606
+ }
607
+ async listKeys() {
608
+ try {
609
+ const { stdout } = await execAsync2("ssh-add -l");
610
+ const keys = stdout.split("\n").filter((line) => line.trim()).map((line) => line.trim());
611
+ return { success: true, keys };
612
+ } catch {
613
+ return { success: true, keys: [] };
614
+ }
615
+ }
616
+ };
617
+ var sshAgentService = new SSHAgentService();
618
+
619
+ // ../core/services/sshConfigService.ts
620
+ var fs7 = __toESM(require("fs"), 1);
621
+ var path4 = __toESM(require("path"), 1);
622
+ var import_os4 = __toESM(require("os"), 1);
623
+ var SSHConfigService = class {
624
+ getConfigPath() {
625
+ return path4.join(import_os4.default.homedir(), ".ssh", "config");
626
+ }
627
+ async readConfig() {
628
+ try {
629
+ const configPath = this.getConfigPath();
630
+ if (!fs7.existsSync(configPath)) {
631
+ return { content: "" };
632
+ }
633
+ const content = fs7.readFileSync(configPath, "utf8");
634
+ return { content };
635
+ } catch (error2) {
636
+ return {
637
+ content: "",
638
+ error: error2 instanceof Error ? error2.message : "Failed to read config"
639
+ };
640
+ }
641
+ }
642
+ async updateConfig(profile) {
643
+ try {
644
+ const configPath = this.getConfigPath();
645
+ const sshDir = path4.dirname(configPath);
646
+ if (!fs7.existsSync(sshDir)) {
647
+ fs7.mkdirSync(sshDir, { mode: 448 });
648
+ }
649
+ let content = "";
650
+ if (fs7.existsSync(configPath)) {
651
+ content = fs7.readFileSync(configPath, "utf8");
652
+ }
653
+ content = this.removeProfileEntry(content, profile.id);
654
+ if (profile.keyPath && profile.sshKeyType !== "default") {
655
+ const hostAlias = this.getHostAlias(profile);
656
+ const entry = this.generateConfigEntry(profile, hostAlias);
657
+ content = content.trim() + "\n\n" + entry + "\n";
658
+ }
659
+ fs7.writeFileSync(configPath, content, { mode: 384 });
660
+ logService.addLog(
661
+ "SSH_CONFIG_UPDATED",
662
+ `SSH config updated for profile "${profile.name}"`,
663
+ {
664
+ profileId: profile.id,
665
+ provider: profile.provider
666
+ }
667
+ );
668
+ return { success: true };
669
+ } catch (error2) {
670
+ return {
671
+ success: false,
672
+ error: error2 instanceof Error ? error2.message : "Failed to update config"
673
+ };
674
+ }
675
+ }
676
+ async removeProfileConfig(profileId) {
677
+ try {
678
+ const configPath = this.getConfigPath();
679
+ if (!fs7.existsSync(configPath)) {
680
+ return { success: true };
681
+ }
682
+ let content = fs7.readFileSync(configPath, "utf8");
683
+ content = this.removeProfileEntry(content, profileId);
684
+ fs7.writeFileSync(configPath, content, { mode: 384 });
685
+ return { success: true };
686
+ } catch (error2) {
687
+ return {
688
+ success: false,
689
+ error: error2 instanceof Error ? error2.message : "Failed to remove config"
690
+ };
691
+ }
692
+ }
693
+ getHostAlias(profile) {
694
+ const { sshHost } = getProviderSSHConfig(profile.provider);
695
+ return `${sshHost}-${profile.username}`;
696
+ }
697
+ generateConfigEntry(profile, hostAlias) {
698
+ const { sshHost, sshUser } = getProviderSSHConfig(profile.provider);
699
+ return `# DevSwitch Profile: ${profile.email} (${profile.id})
700
+ Host ${hostAlias}
701
+ HostName ${sshHost}
702
+ User ${sshUser}
703
+ IdentityFile ${profile.keyPath}
704
+ IdentitiesOnly yes
705
+ # DevSwitch Profile End: (${profile.id})`;
706
+ }
707
+ removeProfileEntry(content, profileId) {
708
+ const lines = content.split("\n");
709
+ const result = [];
710
+ let inProfileBlock = false;
711
+ for (let i = 0; i < lines.length; i++) {
712
+ const line = lines[i];
713
+ if (line.includes("# DevSwitch Profile:") && line.includes(`(${profileId})`)) {
714
+ inProfileBlock = true;
715
+ continue;
716
+ }
717
+ if (inProfileBlock && line.includes("# DevSwitch Profile End:") && line.includes(`(${profileId})`)) {
718
+ inProfileBlock = false;
719
+ if (i + 1 < lines.length && lines[i + 1].trim() === "") {
720
+ i++;
721
+ }
722
+ continue;
723
+ }
724
+ if (inProfileBlock) {
725
+ continue;
726
+ }
727
+ result.push(line);
728
+ }
729
+ const cleaned = result.join("\n").replace(/\n{3,}/g, "\n\n").trim();
730
+ return cleaned + "\n";
731
+ }
732
+ checkProfileConfigured(profile) {
733
+ try {
734
+ const configPath = this.getConfigPath();
735
+ if (!fs7.existsSync(configPath)) {
736
+ return false;
737
+ }
738
+ const content = fs7.readFileSync(configPath, "utf8");
739
+ return content.includes(`(${profile.id})`);
740
+ } catch {
741
+ return false;
742
+ }
743
+ }
744
+ getHostAliasForKeyPath(keyPath) {
745
+ try {
746
+ const configPath = this.getConfigPath();
747
+ if (!fs7.existsSync(configPath)) {
748
+ return null;
749
+ }
750
+ const content = fs7.readFileSync(configPath, "utf8");
751
+ const lines = content.split("\n");
752
+ let currentHost = null;
753
+ for (let i = 0; i < lines.length; i++) {
754
+ const line = lines[i].trim();
755
+ const hostMatch = line.match(/^Host\s+(.+)$/i);
756
+ if (hostMatch) {
757
+ currentHost = hostMatch[1];
758
+ continue;
759
+ }
760
+ const identityMatch = line.match(/^IdentityFile\s+(.+)$/i);
761
+ if (identityMatch && currentHost) {
762
+ const configKeyPath = identityMatch[1].replace(/^~/, import_os4.default.homedir());
763
+ const normalizedConfigPath = path4.normalize(configKeyPath);
764
+ const normalizedKeyPath = path4.normalize(keyPath);
765
+ if (normalizedConfigPath === normalizedKeyPath) {
766
+ return currentHost;
767
+ }
768
+ }
769
+ }
770
+ return null;
771
+ } catch (error2) {
772
+ console.error("Error finding host alias:", error2);
773
+ return null;
774
+ }
775
+ }
776
+ async getGlobalGitConfig() {
777
+ try {
778
+ const { exec: exec4 } = await import("child_process");
779
+ const { promisify: promisify4 } = await import("util");
780
+ const execAsync4 = promisify4(exec4);
781
+ const { stdout } = await execAsync4("git config --global --list");
782
+ const config = {};
783
+ const lines = stdout.trim().split("\n");
784
+ for (const line of lines) {
785
+ const [key, ...valueParts] = line.split("=");
786
+ if (key && valueParts.length > 0) {
787
+ config[key] = valueParts.join("=");
788
+ }
789
+ }
790
+ return config;
791
+ } catch (error2) {
792
+ console.error("Error getting global git config:", error2);
793
+ return {};
794
+ }
795
+ }
796
+ getAllHostKeyMappings() {
797
+ try {
798
+ const configPath = this.getConfigPath();
799
+ if (!fs7.existsSync(configPath)) {
800
+ return [];
801
+ }
802
+ const content = fs7.readFileSync(configPath, "utf8");
803
+ const lines = content.split("\n");
804
+ const mappings = [];
805
+ let currentHost = null;
806
+ let currentHostname = null;
807
+ let currentIdentityFile = null;
808
+ for (let i = 0; i < lines.length; i++) {
809
+ const line = lines[i].trim();
810
+ if (line.startsWith("#") || !line) {
811
+ continue;
812
+ }
813
+ const hostMatch = line.match(/^Host\s+(.+)$/i);
814
+ if (hostMatch) {
815
+ if (currentHost && currentIdentityFile) {
816
+ mappings.push({
817
+ host: currentHost,
818
+ hostname: currentHostname || "",
819
+ identityFile: currentIdentityFile.replace(/^~/, import_os4.default.homedir()),
820
+ username: this.extractUsernameFromHost(currentHost)
821
+ });
822
+ }
823
+ currentHost = hostMatch[1];
824
+ currentHostname = null;
825
+ currentIdentityFile = null;
826
+ continue;
827
+ }
828
+ const hostnameMatch = line.match(/^HostName\s+(.+)$/i);
829
+ if (hostnameMatch && currentHost) {
830
+ currentHostname = hostnameMatch[1];
831
+ continue;
832
+ }
833
+ const identityMatch = line.match(/^IdentityFile\s+(.+)$/i);
834
+ if (identityMatch && currentHost) {
835
+ currentIdentityFile = identityMatch[1];
836
+ continue;
837
+ }
838
+ }
839
+ if (currentHost && currentIdentityFile) {
840
+ mappings.push({
841
+ host: currentHost,
842
+ hostname: currentHostname || "",
843
+ identityFile: currentIdentityFile.replace(/^~/, import_os4.default.homedir()),
844
+ username: this.extractUsernameFromHost(currentHost)
845
+ });
846
+ }
847
+ return mappings;
848
+ } catch (error2) {
849
+ console.error("Error parsing SSH config:", error2);
850
+ return [];
851
+ }
852
+ }
853
+ extractUsernameFromHost(host) {
854
+ try {
855
+ const separators = ["-", "_"];
856
+ for (const separator of separators) {
857
+ if (host.includes(separator)) {
858
+ const parts = host.split(separator);
859
+ if (parts.length > 1) {
860
+ const username = parts.slice(1).join(separator);
861
+ return username || null;
862
+ }
863
+ }
864
+ }
865
+ return null;
866
+ } catch {
867
+ return null;
868
+ }
869
+ }
870
+ };
871
+ var sshConfigService = new SSHConfigService();
872
+
873
+ // ../core/services/gitService.ts
874
+ var import_child_process3 = require("child_process");
875
+ var fs8 = __toESM(require("fs"), 1);
876
+ var path5 = __toESM(require("path"), 1);
877
+ var GitService = class {
878
+ async cloneRepository(params) {
879
+ try {
880
+ const { repoUrl, destinationFolder, username, email, hostAlias } = params;
881
+ if (!fs8.existsSync(destinationFolder)) {
882
+ return { success: false, error: "Destination folder does not exist" };
883
+ }
884
+ const sshUrl = this.convertToSSHUrl(repoUrl, hostAlias);
885
+ if (!sshUrl) {
886
+ return { success: false, error: "Invalid repository URL format" };
887
+ }
888
+ const repoName = this.extractRepoName(sshUrl);
889
+ const clonePath = path5.join(destinationFolder, repoName);
890
+ if (fs8.existsSync(clonePath)) {
891
+ return {
892
+ success: false,
893
+ error: `Directory ${repoName} already exists in the destination folder`
894
+ };
895
+ }
896
+ try {
897
+ (0, import_child_process3.execSync)(`git clone ${sshUrl}`, {
898
+ cwd: destinationFolder,
899
+ stdio: "pipe",
900
+ encoding: "utf-8"
901
+ });
902
+ } catch (error2) {
903
+ return {
904
+ success: false,
905
+ error: `Failed to clone repository: ${error2 instanceof Error ? error2.message : "Unknown error"}`
906
+ };
907
+ }
908
+ try {
909
+ (0, import_child_process3.execSync)(`git config user.name "${username}"`, {
910
+ cwd: clonePath,
911
+ stdio: "pipe"
912
+ });
913
+ (0, import_child_process3.execSync)(`git config user.email "${email}"`, {
914
+ cwd: clonePath,
915
+ stdio: "pipe"
916
+ });
917
+ } catch (error2) {
918
+ return {
919
+ success: false,
920
+ error: `Repository cloned but failed to configure git settings: ${error2 instanceof Error ? error2.message : "Unknown error"}`
921
+ };
922
+ }
923
+ return { success: true, clonedPath: clonePath };
924
+ } catch (error2) {
925
+ return {
926
+ success: false,
927
+ error: error2 instanceof Error ? error2.message : "Failed to clone repository"
928
+ };
929
+ }
930
+ }
931
+ async updateProjectConfig(params) {
932
+ try {
933
+ const {
934
+ projectPath,
935
+ username,
936
+ email,
937
+ repoUrl,
938
+ remoteName = "origin",
939
+ hostAlias
940
+ } = params;
941
+ if (!fs8.existsSync(projectPath)) {
942
+ return { success: false, error: "Project path does not exist" };
943
+ }
944
+ const gitDir = path5.join(projectPath, ".git");
945
+ if (!fs8.existsSync(gitDir)) {
946
+ return { success: false, error: "Not a git repository" };
947
+ }
948
+ let oldOrigin = "";
949
+ if (repoUrl && repoUrl.trim()) {
950
+ try {
951
+ oldOrigin = (0, import_child_process3.execSync)(`git remote get-url ${remoteName}`, {
952
+ cwd: projectPath,
953
+ encoding: "utf-8"
954
+ }).trim();
955
+ } catch {
956
+ oldOrigin = "none";
957
+ }
958
+ const sshUrl = this.convertToSSHUrl(repoUrl, hostAlias);
959
+ if (!sshUrl) {
960
+ return { success: false, error: "Invalid repository URL format" };
961
+ }
962
+ try {
963
+ if (oldOrigin === "none") {
964
+ (0, import_child_process3.execSync)(`git remote add ${remoteName} ${sshUrl}`, {
965
+ cwd: projectPath,
966
+ stdio: "pipe"
967
+ });
968
+ } else {
969
+ (0, import_child_process3.execSync)(`git remote set-url ${remoteName} ${sshUrl}`, {
970
+ cwd: projectPath,
971
+ stdio: "pipe"
972
+ });
973
+ }
974
+ } catch (error2) {
975
+ return {
976
+ success: false,
977
+ error: `Failed to update remote ${remoteName}: ${error2 instanceof Error ? error2.message : "Unknown error"}`
978
+ };
979
+ }
980
+ }
981
+ try {
982
+ (0, import_child_process3.execSync)(`git config user.name "${username}"`, {
983
+ cwd: projectPath,
984
+ stdio: "pipe"
985
+ });
986
+ (0, import_child_process3.execSync)(`git config user.email "${email}"`, {
987
+ cwd: projectPath,
988
+ stdio: "pipe"
989
+ });
990
+ } catch (error2) {
991
+ return {
992
+ success: false,
993
+ error: `${repoUrl ? "Remote updated but " : ""}Failed to configure git settings: ${error2 instanceof Error ? error2.message : "Unknown error"}`
994
+ };
995
+ }
996
+ return { success: true, oldOrigin };
997
+ } catch (error2) {
998
+ return {
999
+ success: false,
1000
+ error: error2 instanceof Error ? error2.message : "Failed to update project configuration"
1001
+ };
1002
+ }
1003
+ }
1004
+ async getProjectRemotes(projectPath) {
1005
+ try {
1006
+ if (!fs8.existsSync(projectPath)) {
1007
+ return { success: false, error: "Project path does not exist" };
1008
+ }
1009
+ const gitDir = path5.join(projectPath, ".git");
1010
+ if (!fs8.existsSync(gitDir)) {
1011
+ return { success: false, error: "Not a git repository" };
1012
+ }
1013
+ let remoteOutput = "";
1014
+ try {
1015
+ remoteOutput = (0, import_child_process3.execSync)("git remote -v", {
1016
+ cwd: projectPath,
1017
+ encoding: "utf-8"
1018
+ }).trim();
1019
+ } catch {
1020
+ return { success: true, remotes: [] };
1021
+ }
1022
+ if (!remoteOutput) {
1023
+ return { success: true, remotes: [] };
1024
+ }
1025
+ const remotes = [];
1026
+ const lines = remoteOutput.split("\n");
1027
+ for (const line of lines) {
1028
+ const match = line.match(/^(\S+)\s+(\S+)\s+\((fetch|push)\)$/);
1029
+ if (match) {
1030
+ const [, name, url, type] = match;
1031
+ remotes.push({ name, url, type });
1032
+ }
1033
+ }
1034
+ return { success: true, remotes };
1035
+ } catch (error2) {
1036
+ return {
1037
+ success: false,
1038
+ error: error2 instanceof Error ? error2.message : "Failed to get project remotes"
1039
+ };
1040
+ }
1041
+ }
1042
+ async getProjectConfig(projectPath) {
1043
+ try {
1044
+ if (!fs8.existsSync(projectPath)) {
1045
+ return { success: false, error: "Project path does not exist" };
1046
+ }
1047
+ const gitDir = path5.join(projectPath, ".git");
1048
+ if (!fs8.existsSync(gitDir)) {
1049
+ return { success: false, error: "Not a git repository" };
1050
+ }
1051
+ const config = {};
1052
+ try {
1053
+ config.username = (0, import_child_process3.execSync)("git config user.name", {
1054
+ cwd: projectPath,
1055
+ encoding: "utf-8"
1056
+ }).trim();
1057
+ } catch {
1058
+ }
1059
+ try {
1060
+ config.email = (0, import_child_process3.execSync)("git config user.email", {
1061
+ cwd: projectPath,
1062
+ encoding: "utf-8"
1063
+ }).trim();
1064
+ } catch {
1065
+ }
1066
+ try {
1067
+ config.origin = (0, import_child_process3.execSync)("git remote get-url origin", {
1068
+ cwd: projectPath,
1069
+ encoding: "utf-8"
1070
+ }).trim();
1071
+ } catch {
1072
+ }
1073
+ return { success: true, config };
1074
+ } catch (error2) {
1075
+ return {
1076
+ success: false,
1077
+ error: error2 instanceof Error ? error2.message : "Failed to get project configuration"
1078
+ };
1079
+ }
1080
+ }
1081
+ convertToSSHUrl(url, hostAlias) {
1082
+ const sshMatch = url.match(/git@[^:]+:([^/]+)\/(.+?)(?:\.git)?$/);
1083
+ if (sshMatch) {
1084
+ const [, username, repo] = sshMatch;
1085
+ return `git@${hostAlias}:${username}/${repo}.git`;
1086
+ }
1087
+ const httpsMatch = url.match(/https?:\/\/[^/]+\/([^/]+)\/(.+?)(?:\.git)?$/);
1088
+ if (httpsMatch) {
1089
+ const [, username, repo] = httpsMatch;
1090
+ return `git@${hostAlias}:${username}/${repo}.git`;
1091
+ }
1092
+ return null;
1093
+ }
1094
+ extractRepoName(sshUrl) {
1095
+ const match = sshUrl.match(/\/([^/]+?)(?:\.git)?$/);
1096
+ return match ? match[1] : "repository";
1097
+ }
1098
+ };
1099
+ var gitService = new GitService();
1100
+
1101
+ // ../core/services/sshTestService.ts
1102
+ var import_child_process4 = require("child_process");
1103
+ var import_util3 = require("util");
1104
+ var execAsync3 = (0, import_util3.promisify)(import_child_process4.exec);
1105
+ async function testSSHConnection(params) {
1106
+ try {
1107
+ const keyFlag = params.keyPath ? ` -i "${params.keyPath}"` : "";
1108
+ const command = `ssh -T${keyFlag} -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new ${params.sshUser}@${params.hostAlias}`;
1109
+ try {
1110
+ const { stdout, stderr } = await execAsync3(command);
1111
+ const output = (stdout + "\n" + stderr).trim();
1112
+ return { success: true, output };
1113
+ } catch (err) {
1114
+ const execErr = err;
1115
+ const output = ((execErr.stdout || "") + "\n" + (execErr.stderr || "")).trim();
1116
+ if (isSSHAuthSuccess(output)) {
1117
+ return { success: true, output };
1118
+ }
1119
+ return {
1120
+ success: false,
1121
+ output,
1122
+ error: execErr.message || "SSH connection failed"
1123
+ };
1124
+ }
1125
+ } catch (error2) {
1126
+ return {
1127
+ success: false,
1128
+ output: "",
1129
+ error: error2 instanceof Error ? error2.message : "Failed to run SSH test"
1130
+ };
1131
+ }
1132
+ }
1133
+
1134
+ // ../core/services/profileManager.ts
1135
+ var profileManager_exports = {};
1136
+ __export(profileManager_exports, {
1137
+ createProfile: () => createProfile,
1138
+ deleteProfile: () => deleteProfile,
1139
+ getAllProfiles: () => getAllProfiles,
1140
+ getProfileById: () => getProfileById,
1141
+ scanAndSync: () => scanAndSync,
1142
+ switchProfile: () => switchProfile,
1143
+ updateProfile: () => updateProfile
1144
+ });
1145
+ var import_crypto3 = require("crypto");
1146
+ var path6 = __toESM(require("path"), 1);
1147
+ var SYNC_COLORS = [
1148
+ "#3b82f6",
1149
+ "#10b981",
1150
+ "#f59e0b",
1151
+ "#ef4444",
1152
+ "#8b5cf6",
1153
+ "#ec4899",
1154
+ "#06b6d4",
1155
+ "#84cc16"
1156
+ ];
1157
+ var SYNC_EMOJIS = ["\u{1F464}", "\u{1F4BB}", "\u{1F680}", "\u{1F511}", "\u{1F4BC}", "\u{1F3AF}", "\u{1F31F}", "\u{1F4A1}"];
1158
+ async function createProfile(input, source = "app") {
1159
+ const profile = {
1160
+ id: (0, import_crypto3.randomUUID)(),
1161
+ name: input.name,
1162
+ email: input.email,
1163
+ username: input.username,
1164
+ sshKeyType: input.sshKeyType,
1165
+ keyPath: null,
1166
+ keyAlgorithm: null,
1167
+ hasPassphrase: false,
1168
+ passphraseEncrypted: null,
1169
+ hostConfigured: false,
1170
+ provider: input.provider,
1171
+ avatar: input.avatar,
1172
+ color: input.color,
1173
+ tags: input.tags,
1174
+ createdAt: Date.now(),
1175
+ updatedAt: Date.now()
1176
+ };
1177
+ if (input.sshKeyType === "default") {
1178
+ profile.keyPath = sshKeyService.getDefaultKeyPath();
1179
+ } else if (input.sshKeyType === "generated" && input.keyAlgorithm && input.keyName) {
1180
+ const result = await sshKeyService.generateKey({
1181
+ algorithm: input.keyAlgorithm,
1182
+ name: input.keyName,
1183
+ passphrase: input.passphrase,
1184
+ email: input.email
1185
+ });
1186
+ if (result.success && result.keyPath) {
1187
+ profile.keyPath = result.keyPath;
1188
+ profile.keyAlgorithm = input.keyAlgorithm;
1189
+ if (input.passphrase) {
1190
+ profile.hasPassphrase = true;
1191
+ profile.passphraseEncrypted = encryptPassphrase(input.passphrase);
1192
+ }
1193
+ await sshAgentService.addKeyToAgent({
1194
+ keyPath: result.keyPath,
1195
+ passphrase: input.passphrase
1196
+ });
1197
+ } else {
1198
+ throw new Error(result.error || "Failed to generate SSH key");
1199
+ }
1200
+ } else if (input.sshKeyType === "existing" && input.existingKeyPath) {
1201
+ profile.keyPath = input.existingKeyPath;
1202
+ }
1203
+ if (profile.keyPath) {
1204
+ const configResult = await sshConfigService.updateConfig(profile);
1205
+ profile.hostConfigured = configResult.success;
1206
+ }
1207
+ storageService.saveProfile(profile);
1208
+ logService.addLog(
1209
+ "PROFILE_CREATED",
1210
+ `Profile "${profile.name}" created`,
1211
+ {
1212
+ profileId: profile.id,
1213
+ provider: profile.provider,
1214
+ sshKeyType: profile.sshKeyType
1215
+ },
1216
+ source
1217
+ );
1218
+ return profile;
1219
+ }
1220
+ async function updateProfile(input, source = "app") {
1221
+ const existingProfile = storageService.getProfile(input.id);
1222
+ if (!existingProfile) {
1223
+ throw new Error("Profile not found");
1224
+ }
1225
+ const updatedProfile = {
1226
+ ...existingProfile,
1227
+ name: input.name ?? existingProfile.name,
1228
+ email: input.email ?? existingProfile.email,
1229
+ username: input.username ?? existingProfile.username,
1230
+ avatar: input.avatar ?? existingProfile.avatar,
1231
+ color: input.color ?? existingProfile.color,
1232
+ tags: input.tags ?? existingProfile.tags,
1233
+ provider: input.provider ?? existingProfile.provider,
1234
+ updatedAt: Date.now()
1235
+ };
1236
+ if (input.sshKeyType) {
1237
+ updatedProfile.sshKeyType = input.sshKeyType;
1238
+ if (input.sshKeyType === "default") {
1239
+ updatedProfile.keyPath = sshKeyService.getDefaultKeyPath();
1240
+ updatedProfile.keyAlgorithm = null;
1241
+ updatedProfile.hasPassphrase = false;
1242
+ updatedProfile.passphraseEncrypted = null;
1243
+ } else if (input.sshKeyType === "generated" && input.keyAlgorithm && input.keyName) {
1244
+ const result = await sshKeyService.generateKey({
1245
+ algorithm: input.keyAlgorithm,
1246
+ name: input.keyName,
1247
+ passphrase: input.passphrase,
1248
+ email: updatedProfile.email
1249
+ });
1250
+ if (result.success && result.keyPath) {
1251
+ updatedProfile.keyPath = result.keyPath;
1252
+ updatedProfile.keyAlgorithm = input.keyAlgorithm;
1253
+ if (input.passphrase) {
1254
+ updatedProfile.hasPassphrase = true;
1255
+ updatedProfile.passphraseEncrypted = encryptPassphrase(
1256
+ input.passphrase
1257
+ );
1258
+ }
1259
+ await sshAgentService.addKeyToAgent({
1260
+ keyPath: result.keyPath,
1261
+ passphrase: input.passphrase
1262
+ });
1263
+ }
1264
+ } else if (input.sshKeyType === "existing" && input.existingKeyPath) {
1265
+ updatedProfile.keyPath = input.existingKeyPath;
1266
+ updatedProfile.keyAlgorithm = null;
1267
+ updatedProfile.hasPassphrase = false;
1268
+ updatedProfile.passphraseEncrypted = null;
1269
+ }
1270
+ }
1271
+ if (updatedProfile.keyPath) {
1272
+ const configResult = await sshConfigService.updateConfig(updatedProfile);
1273
+ updatedProfile.hostConfigured = configResult.success;
1274
+ }
1275
+ storageService.saveProfile(updatedProfile);
1276
+ logService.addLog(
1277
+ "PROFILE_UPDATED",
1278
+ `Profile "${updatedProfile.name}" updated`,
1279
+ {
1280
+ profileId: updatedProfile.id,
1281
+ provider: updatedProfile.provider
1282
+ },
1283
+ source
1284
+ );
1285
+ return updatedProfile;
1286
+ }
1287
+ async function deleteProfile(id, source = "app") {
1288
+ const profile = storageService.getProfile(id);
1289
+ if (profile) {
1290
+ await sshConfigService.removeProfileConfig(id);
1291
+ if (profile.sshKeyType === "generated" && profile.keyPath) {
1292
+ sshKeyService.deleteKey(profile.keyPath);
1293
+ }
1294
+ logService.addLog(
1295
+ "PROFILE_DELETED",
1296
+ `Profile "${profile.name}" deleted`,
1297
+ {
1298
+ profileId: profile.id,
1299
+ provider: profile.provider
1300
+ },
1301
+ source
1302
+ );
1303
+ }
1304
+ return storageService.deleteProfile(id);
1305
+ }
1306
+ function getAllProfiles() {
1307
+ return storageService.getAllProfiles();
1308
+ }
1309
+ function getProfileById(id) {
1310
+ return storageService.getProfile(id) || null;
1311
+ }
1312
+ async function switchProfile(id, options = {}, source = "app") {
1313
+ const { setGlobalGit = true } = options;
1314
+ const profile = storageService.getProfile(id);
1315
+ if (!profile) {
1316
+ return { success: false, error: "Profile not found" };
1317
+ }
1318
+ if (profile.keyPath) {
1319
+ const configResult = await sshConfigService.updateConfig(profile);
1320
+ if (configResult.success && !profile.hostConfigured) {
1321
+ profile.hostConfigured = true;
1322
+ storageService.saveProfile(profile);
1323
+ }
1324
+ }
1325
+ let agentLoaded = false;
1326
+ if (profile.keyPath) {
1327
+ const agentResult = await sshAgentService.addKeyToAgent({
1328
+ keyPath: profile.keyPath,
1329
+ passphrase: options.passphrase
1330
+ });
1331
+ agentLoaded = agentResult.success;
1332
+ }
1333
+ let globalGitSet = false;
1334
+ if (setGlobalGit) {
1335
+ try {
1336
+ const { execSync: execSync3 } = await import("child_process");
1337
+ execSync3(`git config --global user.name "${profile.username}"`, {
1338
+ stdio: "pipe"
1339
+ });
1340
+ execSync3(`git config --global user.email "${profile.email}"`, {
1341
+ stdio: "pipe"
1342
+ });
1343
+ globalGitSet = true;
1344
+ } catch {
1345
+ globalGitSet = false;
1346
+ }
1347
+ }
1348
+ const { sshHost } = getProviderSSHConfig(profile.provider);
1349
+ const hostAlias = `${sshHost}-${profile.username}`;
1350
+ const cloneUrlExample = `git@${hostAlias}:<owner>/<repo>.git`;
1351
+ logService.addLog(
1352
+ "PROFILE_SWITCHED",
1353
+ `Switched to profile "${profile.name}"`,
1354
+ {
1355
+ profileId: profile.id,
1356
+ provider: profile.provider,
1357
+ globalGitSet,
1358
+ agentLoaded
1359
+ },
1360
+ source
1361
+ );
1362
+ return {
1363
+ success: true,
1364
+ profile,
1365
+ hostAlias,
1366
+ cloneUrlExample,
1367
+ agentLoaded,
1368
+ globalGitSet
1369
+ };
1370
+ }
1371
+ async function scanAndSync(source = "app") {
1372
+ try {
1373
+ const allKeys = await sshKeyService.scanAllSSHKeys();
1374
+ const hostMappings = sshConfigService.getAllHostKeyMappings();
1375
+ const existingProfiles = storageService.getAllProfiles();
1376
+ const syncedProfiles = [];
1377
+ const skippedKeys = [];
1378
+ for (const keyInfo of allKeys) {
1379
+ const existingProfile = existingProfiles.find(
1380
+ (p) => p.keyPath === keyInfo.privatePath
1381
+ );
1382
+ if (existingProfile) {
1383
+ skippedKeys.push(keyInfo.privatePath);
1384
+ continue;
1385
+ }
1386
+ const hostMapping = hostMappings.find(
1387
+ (m) => path6.normalize(m.identityFile) === path6.normalize(keyInfo.privatePath)
1388
+ );
1389
+ let username = hostMapping?.username || null;
1390
+ if (!username && keyInfo.email) {
1391
+ username = keyInfo.email.split("@")[0];
1392
+ }
1393
+ if (!username) {
1394
+ username = path6.basename(keyInfo.privatePath);
1395
+ }
1396
+ const email = keyInfo.email || `${username}@local`;
1397
+ const profileName = `${username} (Synced)`;
1398
+ const color = SYNC_COLORS[syncedProfiles.length % SYNC_COLORS.length];
1399
+ const avatar = SYNC_EMOJIS[syncedProfiles.length % SYNC_EMOJIS.length];
1400
+ const profile = {
1401
+ id: (0, import_crypto3.randomUUID)(),
1402
+ name: profileName,
1403
+ email,
1404
+ username,
1405
+ sshKeyType: "existing",
1406
+ keyPath: keyInfo.privatePath,
1407
+ keyAlgorithm: keyInfo.algorithm === "rsa" || keyInfo.algorithm === "ed25519" ? keyInfo.algorithm : null,
1408
+ hasPassphrase: false,
1409
+ passphraseEncrypted: null,
1410
+ hostConfigured: hostMapping ? true : false,
1411
+ avatar,
1412
+ color,
1413
+ createdAt: Date.now(),
1414
+ updatedAt: Date.now()
1415
+ };
1416
+ if (!hostMapping) {
1417
+ await sshConfigService.updateConfig(profile);
1418
+ profile.hostConfigured = true;
1419
+ }
1420
+ storageService.saveProfile(profile);
1421
+ syncedProfiles.push(profile);
1422
+ }
1423
+ if (syncedProfiles.length > 0) {
1424
+ logService.addLog(
1425
+ "PROFILE_CREATED",
1426
+ `Synced ${syncedProfiles.length} profile(s) from SSH keys`,
1427
+ {
1428
+ syncedCount: syncedProfiles.length
1429
+ },
1430
+ source
1431
+ );
1432
+ }
1433
+ return {
1434
+ success: true,
1435
+ syncedCount: syncedProfiles.length,
1436
+ skippedCount: skippedKeys.length,
1437
+ profiles: syncedProfiles
1438
+ };
1439
+ } catch (error2) {
1440
+ return {
1441
+ success: false,
1442
+ syncedCount: 0,
1443
+ skippedCount: 0,
1444
+ profiles: [],
1445
+ error: error2 instanceof Error ? error2.message : "Failed to sync profiles"
1446
+ };
1447
+ }
1448
+ }
1449
+
1450
+ // src/args.ts
1451
+ function parseArgs(argv) {
1452
+ const positionals = [];
1453
+ const flags = {};
1454
+ for (let i = 0; i < argv.length; i++) {
1455
+ const arg = argv[i];
1456
+ if (arg.startsWith("--")) {
1457
+ const body = arg.slice(2);
1458
+ const eq = body.indexOf("=");
1459
+ if (eq >= 0) {
1460
+ flags[body.slice(0, eq)] = body.slice(eq + 1);
1461
+ } else {
1462
+ const next = argv[i + 1];
1463
+ if (next !== void 0 && !next.startsWith("-")) {
1464
+ flags[body] = next;
1465
+ i++;
1466
+ } else {
1467
+ flags[body] = true;
1468
+ }
1469
+ }
1470
+ } else if (arg.startsWith("-") && arg.length > 1) {
1471
+ const short = arg.slice(1);
1472
+ const next = argv[i + 1];
1473
+ if (next !== void 0 && !next.startsWith("-")) {
1474
+ flags[short] = next;
1475
+ i++;
1476
+ } else {
1477
+ flags[short] = true;
1478
+ }
1479
+ } else {
1480
+ positionals.push(arg);
1481
+ }
1482
+ }
1483
+ return { positionals, flags };
1484
+ }
1485
+ function flagStr(flags, ...names) {
1486
+ for (const name of names) {
1487
+ const v = flags[name];
1488
+ if (typeof v === "string") return v;
1489
+ }
1490
+ return void 0;
1491
+ }
1492
+ function flagBool(flags, ...names) {
1493
+ for (const name of names) {
1494
+ if (flags[name] === true || flags[name] === "true") return true;
1495
+ }
1496
+ return false;
1497
+ }
1498
+
1499
+ // src/ui.ts
1500
+ var useColor = process.stdout.isTTY && !process.env.NO_COLOR;
1501
+ function wrap(code, str) {
1502
+ return useColor ? `\x1B[${code}m${str}\x1B[0m` : str;
1503
+ }
1504
+ var c = {
1505
+ bold: (s) => wrap("1", s),
1506
+ dim: (s) => wrap("2", s),
1507
+ red: (s) => wrap("31", s),
1508
+ green: (s) => wrap("32", s),
1509
+ yellow: (s) => wrap("33", s),
1510
+ blue: (s) => wrap("34", s),
1511
+ magenta: (s) => wrap("35", s),
1512
+ cyan: (s) => wrap("36", s),
1513
+ gray: (s) => wrap("90", s)
1514
+ };
1515
+ var sym = {
1516
+ ok: useColor ? c.green("\u2713") : "[ok]",
1517
+ err: useColor ? c.red("\u2717") : "[x]",
1518
+ warn: useColor ? c.yellow("!") : "[!]",
1519
+ info: useColor ? c.blue("i") : "[i]",
1520
+ arrow: useColor ? c.cyan("\u2192") : "->",
1521
+ dot: c.gray("\u2022")
1522
+ };
1523
+ function success(msg) {
1524
+ console.log(`${sym.ok} ${msg}`);
1525
+ }
1526
+ function error(msg) {
1527
+ console.error(`${sym.err} ${c.red(msg)}`);
1528
+ }
1529
+ function warn(msg) {
1530
+ console.log(`${sym.warn} ${c.yellow(msg)}`);
1531
+ }
1532
+ function info(msg) {
1533
+ console.log(`${sym.info} ${msg}`);
1534
+ }
1535
+ function heading(msg) {
1536
+ console.log("\n" + c.bold(msg));
1537
+ }
1538
+ function table(headers, rows) {
1539
+ const widths = headers.map(
1540
+ (h, i) => Math.max(stripLen(h), ...rows.map((r) => stripLen(r[i] ?? "")))
1541
+ );
1542
+ const pad = (s, w) => s + " ".repeat(Math.max(0, w - stripLen(s)));
1543
+ const headerLine = headers.map((h, i) => c.bold(pad(h, widths[i]))).join(" ");
1544
+ console.log(headerLine);
1545
+ console.log(c.gray(widths.map((w) => "\u2500".repeat(w)).join(" ")));
1546
+ for (const row of rows) {
1547
+ console.log(row.map((cell, i) => pad(cell ?? "", widths[i])).join(" "));
1548
+ }
1549
+ }
1550
+ function stripLen(s) {
1551
+ return s.replace(/\x1b\[[0-9;]*m/g, "").length;
1552
+ }
1553
+
1554
+ // src/version.ts
1555
+ var import_meta = {};
1556
+ function resolveVersion() {
1557
+ if ("1.0.1") {
1558
+ return "1.0.1";
1559
+ }
1560
+ try {
1561
+ const { readFileSync: readFileSync6 } = require("fs");
1562
+ const path8 = new URL("../package.json", import_meta.url);
1563
+ const pkg = JSON.parse(readFileSync6(path8, "utf8"));
1564
+ return pkg.version || "0.0.0-dev";
1565
+ } catch {
1566
+ return "0.0.0-dev";
1567
+ }
1568
+ }
1569
+ var CLI_VERSION = resolveVersion();
1570
+
1571
+ // src/commands/help.ts
1572
+ var GENERAL_HELP = `
1573
+ ${c.bold("devswitch")} ${c.gray("v" + CLI_VERSION)} \u2014 manage multiple Git profiles & SSH keys
1574
+
1575
+ ${c.bold("USAGE")}
1576
+ devswitch <command> [options]
1577
+
1578
+ ${c.bold("COMMANDS")}
1579
+ ${c.cyan("list")} List all saved profiles ${c.gray("(alias: ls)")}
1580
+ ${c.cyan("use")} <profile> Switch to a profile ${c.gray("(alias: switch)")}
1581
+ ${c.cyan("current")} Show the currently active profile ${c.gray("(alias: whoami)")}
1582
+ ${c.cyan("add")} Create a new profile ${c.gray("(alias: create)")}
1583
+ ${c.cyan("remove")} <profile> Delete a profile ${c.gray("(alias: rm, delete)")}
1584
+ ${c.cyan("show")} <profile> Show full details of a profile ${c.gray("(alias: view, info)")}
1585
+ ${c.cyan("sync")} Import unmanaged SSH keys as profiles
1586
+ ${c.cyan("test")} <profile> Test the SSH connection for a profile
1587
+ ${c.cyan("pubkey")} <profile> Print a profile's public key
1588
+ ${c.cyan("clone")} <url> [dir] Clone a repo using a profile's identity
1589
+ ${c.cyan("logs")} Show recent activity log
1590
+ ${c.cyan("path")} Show the shared data directory path
1591
+ ${c.cyan("doctor")} Diagnose environment & data store
1592
+ ${c.cyan("help")} [command] Show help (this screen)
1593
+ ${c.cyan("version")} Print the CLI version ${c.gray("(alias: -v)")}
1594
+
1595
+ ${c.bold("GLOBAL OPTIONS")}
1596
+ -h, --help Show help for a command
1597
+ -v, --version Print version
1598
+ --json Output machine-readable JSON where supported
1599
+ --no-color Disable colored output (or set NO_COLOR=1)
1600
+
1601
+ ${c.bold("EXAMPLES")}
1602
+ devswitch list
1603
+ devswitch use work
1604
+ devswitch add --name "Work" --email me@work.com --username me --provider github --generate
1605
+ devswitch sync
1606
+ devswitch clone git@github.com:acme/app.git ~/code --profile work
1607
+
1608
+ ${c.gray("Profiles are shared with the DevSwitch desktop app \u2014 changes here appear there and vice versa.")}
1609
+ ${c.gray("Run 'devswitch help <command>' for details on a specific command.")}
1610
+ `;
1611
+ var COMMAND_HELP = {
1612
+ list: `${c.bold("devswitch list")} \u2014 list all saved profiles
1613
+
1614
+ USAGE
1615
+ devswitch list [--json]
1616
+
1617
+ OPTIONS
1618
+ --json Print profiles as JSON
1619
+
1620
+ Shows each profile's name, username, email, provider, SSH key type, and whether
1621
+ its SSH host alias is configured.`,
1622
+ use: `${c.bold("devswitch use")} <profile> \u2014 switch to a profile
1623
+
1624
+ USAGE
1625
+ devswitch use <profile> [options]
1626
+
1627
+ ARGUMENTS
1628
+ <profile> Profile name, username, email, or id (partial match ok)
1629
+
1630
+ OPTIONS
1631
+ --no-global-git Do NOT change the global git user.name / user.email
1632
+ --json Print result as JSON
1633
+
1634
+ WHAT IT DOES
1635
+ \u2022 Ensures the profile's Host alias exists in ~/.ssh/config
1636
+ \u2022 Loads the profile's key into ssh-agent
1637
+ \u2022 Sets global git user.name / user.email (unless --no-global-git)
1638
+ \u2022 Prints the clone URL format to use for this profile`,
1639
+ add: `${c.bold("devswitch add")} \u2014 create a new profile
1640
+
1641
+ USAGE
1642
+ devswitch add [options]
1643
+
1644
+ OPTIONS
1645
+ --name <name> Friendly profile name
1646
+ --email <email> Git email
1647
+ --username <username> Provider username
1648
+ --provider <provider> github | gitlab | bitbucket | azure | other (default: github)
1649
+ --default Use the default key (~/.ssh/id_ed25519 or id_rsa)
1650
+ --generate Generate a new SSH key
1651
+ --algorithm <algo> ed25519 | rsa (with --generate, default: ed25519)
1652
+ --key-name <name> Filename for the generated key (with --generate)
1653
+ --passphrase <pass> Passphrase for the generated key (optional)
1654
+ --existing <path> Use an existing private key at <path>
1655
+ --avatar <emoji> Avatar emoji (default \u{1F464})
1656
+ --color <hex> Accent color (default #3b82f6)
1657
+
1658
+ If required values are missing, you'll be prompted interactively.`,
1659
+ remove: `${c.bold("devswitch remove")} <profile> \u2014 delete a profile
1660
+
1661
+ USAGE
1662
+ devswitch remove <profile> [--yes] [--json]
1663
+
1664
+ ARGUMENTS
1665
+ <profile> Profile name, username, email, or id
1666
+
1667
+ OPTIONS
1668
+ -y, --yes Skip the confirmation prompt
1669
+
1670
+ Removes the profile's ~/.ssh/config entry. Keys generated by DevSwitch are also
1671
+ deleted from disk; imported/default keys are left untouched.`,
1672
+ show: `${c.bold("devswitch show")} <profile> \u2014 show full profile details
1673
+
1674
+ USAGE
1675
+ devswitch show <profile> [--json]`,
1676
+ sync: `${c.bold("devswitch sync")} \u2014 import unmanaged SSH keys as profiles
1677
+
1678
+ USAGE
1679
+ devswitch sync [--json]
1680
+
1681
+ Scans ~/.ssh and ~/.ssh/config, then creates profiles for any keys not already
1682
+ managed by DevSwitch. Existing profiles are never duplicated.`,
1683
+ test: `${c.bold("devswitch test")} <profile> \u2014 test the SSH connection
1684
+
1685
+ USAGE
1686
+ devswitch test <profile> [--json]
1687
+
1688
+ Runs 'ssh -T' against the profile's provider host alias using its key.`,
1689
+ clone: `${c.bold("devswitch clone")} <url> [dir] \u2014 clone a repo with a profile's identity
1690
+
1691
+ USAGE
1692
+ devswitch clone <repo-url> [destination-dir] --profile <profile>
1693
+
1694
+ ARGUMENTS
1695
+ <repo-url> HTTPS or SSH repository URL
1696
+ [destination-dir] Folder to clone into (default: current directory)
1697
+
1698
+ OPTIONS
1699
+ --profile <profile> Profile to use (required)
1700
+
1701
+ Rewrites the URL to use the profile's SSH host alias and sets the repo's local
1702
+ git user.name / user.email.`,
1703
+ pubkey: `${c.bold("devswitch pubkey")} <profile> \u2014 print a profile's SSH public key`,
1704
+ logs: `${c.bold("devswitch logs")} \u2014 show recent activity
1705
+
1706
+ USAGE
1707
+ devswitch logs [--limit <n>] [--json]`,
1708
+ path: `${c.bold("devswitch path")} \u2014 print the shared data directory path`,
1709
+ doctor: `${c.bold("devswitch doctor")} \u2014 diagnose environment and data store`,
1710
+ current: `${c.bold("devswitch current")} \u2014 show the active profile (based on global git config)`
1711
+ };
1712
+ function printHelp(command) {
1713
+ if (command && COMMAND_HELP[command]) {
1714
+ console.log("\n" + COMMAND_HELP[command] + "\n");
1715
+ return;
1716
+ }
1717
+ console.log(GENERAL_HELP);
1718
+ }
1719
+
1720
+ // src/commands/list.ts
1721
+ async function listCommand(args) {
1722
+ const profiles = profileManager_exports.getAllProfiles();
1723
+ if (flagBool(args.flags, "json")) {
1724
+ console.log(JSON.stringify(profiles, null, 2));
1725
+ return 0;
1726
+ }
1727
+ if (profiles.length === 0) {
1728
+ info(
1729
+ "No profiles yet. Create one with 'devswitch add' or import with 'devswitch sync'."
1730
+ );
1731
+ return 0;
1732
+ }
1733
+ const rows = profiles.map((p) => [
1734
+ p.avatar ? `${p.avatar} ${p.name}` : p.name,
1735
+ p.username,
1736
+ p.email,
1737
+ p.provider || "github",
1738
+ p.keyAlgorithm || p.sshKeyType,
1739
+ p.hostConfigured ? c.green("yes") : c.gray("no")
1740
+ ]);
1741
+ table(["NAME", "USERNAME", "EMAIL", "PROVIDER", "KEY", "SSH CFG"], rows);
1742
+ console.log("");
1743
+ info(
1744
+ `${profiles.length} profile${profiles.length === 1 ? "" : "s"}. Use ${c.cyan("devswitch use <name>")} to switch.`
1745
+ );
1746
+ return 0;
1747
+ }
1748
+
1749
+ // src/commands/use.ts
1750
+ async function useCommand(args) {
1751
+ const identifier = args.positionals[0];
1752
+ if (!identifier) {
1753
+ error("Usage: devswitch use <profile>");
1754
+ return 1;
1755
+ }
1756
+ const profile = storageService.findProfile(identifier);
1757
+ if (!profile) {
1758
+ error(`No profile found matching "${identifier}".`);
1759
+ info("Run 'devswitch list' to see available profiles.");
1760
+ return 1;
1761
+ }
1762
+ const setGlobalGit = !flagBool(args.flags, "no-global-git");
1763
+ const result = await profileManager_exports.switchProfile(
1764
+ profile.id,
1765
+ { setGlobalGit },
1766
+ "cli"
1767
+ );
1768
+ if (flagBool(args.flags, "json")) {
1769
+ console.log(JSON.stringify(result, null, 2));
1770
+ return result.success ? 0 : 1;
1771
+ }
1772
+ if (!result.success) {
1773
+ error(result.error || "Failed to switch profile.");
1774
+ return 1;
1775
+ }
1776
+ success(
1777
+ `Switched to ${c.bold(profile.name)} ${c.gray(`(${profile.username})`)}`
1778
+ );
1779
+ if (result.globalGitSet) {
1780
+ info(`Global git identity set to ${profile.username} <${profile.email}>`);
1781
+ } else if (setGlobalGit) {
1782
+ warn("Could not set global git identity (is git installed?).");
1783
+ }
1784
+ if (result.agentLoaded) {
1785
+ info("SSH key loaded into ssh-agent.");
1786
+ } else {
1787
+ warn(
1788
+ "SSH key was not loaded into ssh-agent (it may be passphrase-protected)."
1789
+ );
1790
+ }
1791
+ console.log("");
1792
+ console.log(` ${c.gray("Clone repos with:")} ${result.cloneUrlExample}`);
1793
+ return 0;
1794
+ }
1795
+
1796
+ // src/commands/add.ts
1797
+ var fs9 = __toESM(require("fs"), 1);
1798
+
1799
+ // src/prompt.ts
1800
+ var readline = __toESM(require("readline"), 1);
1801
+ var import_stream = require("stream");
1802
+ function createInterface2(muted = false) {
1803
+ let muteValue = false;
1804
+ const mutableStdout = new import_stream.Writable({
1805
+ write(chunk, encoding, callback) {
1806
+ if (!muteValue) {
1807
+ process.stdout.write(chunk, encoding);
1808
+ }
1809
+ callback();
1810
+ }
1811
+ });
1812
+ const rl = readline.createInterface({
1813
+ input: process.stdin,
1814
+ output: mutableStdout,
1815
+ terminal: true
1816
+ });
1817
+ return {
1818
+ rl,
1819
+ setMuted: (v) => {
1820
+ muteValue = muted ? v : false;
1821
+ }
1822
+ };
1823
+ }
1824
+ function ask(question, defaultValue) {
1825
+ return new Promise((resolve2) => {
1826
+ const { rl } = createInterface2();
1827
+ const suffix = defaultValue ? ` (${defaultValue})` : "";
1828
+ rl.question(`${question}${suffix}: `, (answer) => {
1829
+ rl.close();
1830
+ const trimmed = answer.trim();
1831
+ resolve2(trimmed || defaultValue || "");
1832
+ });
1833
+ });
1834
+ }
1835
+ function askSecret(question) {
1836
+ return new Promise((resolve2) => {
1837
+ const { rl, setMuted } = createInterface2(true);
1838
+ rl.question(`${question}: `, (answer) => {
1839
+ rl.close();
1840
+ process.stdout.write("\n");
1841
+ resolve2(answer.trim());
1842
+ });
1843
+ setMuted(true);
1844
+ });
1845
+ }
1846
+ async function confirm(question, defaultYes = false) {
1847
+ const hint = defaultYes ? "Y/n" : "y/N";
1848
+ const answer = (await ask(`${question} [${hint}]`)).toLowerCase();
1849
+ if (!answer) return defaultYes;
1850
+ return answer === "y" || answer === "yes";
1851
+ }
1852
+ async function select(question, options) {
1853
+ console.log(question);
1854
+ options.forEach((opt, i) => console.log(` ${i + 1}) ${opt}`));
1855
+ const answer = await ask("Enter choice number");
1856
+ const idx = parseInt(answer, 10) - 1;
1857
+ if (Number.isNaN(idx) || idx < 0 || idx >= options.length) return -1;
1858
+ return idx;
1859
+ }
1860
+
1861
+ // src/commands/add.ts
1862
+ var PROVIDERS = [
1863
+ "github",
1864
+ "gitlab",
1865
+ "bitbucket",
1866
+ "azure",
1867
+ "other"
1868
+ ];
1869
+ async function addCommand(args) {
1870
+ const { flags } = args;
1871
+ const jsonOut = flagBool(flags, "json");
1872
+ const interactive = process.stdin.isTTY && !jsonOut;
1873
+ let name = flagStr(flags, "name");
1874
+ let email = flagStr(flags, "email");
1875
+ let username = flagStr(flags, "username");
1876
+ let provider = flagStr(flags, "provider");
1877
+ if (provider && !PROVIDERS.includes(provider)) {
1878
+ error(
1879
+ `Invalid provider "${provider}". Choose one of: ${PROVIDERS.join(", ")}`
1880
+ );
1881
+ return 1;
1882
+ }
1883
+ if (interactive) {
1884
+ if (!name) name = await ask("Profile name");
1885
+ if (!email) email = await ask("Email");
1886
+ if (!username) username = await ask("Username");
1887
+ if (!provider) {
1888
+ const idx = await select("Git provider:", PROVIDERS);
1889
+ provider = idx >= 0 ? PROVIDERS[idx] : "github";
1890
+ }
1891
+ }
1892
+ provider = provider || "github";
1893
+ if (!name || !email || !username) {
1894
+ error(
1895
+ "Missing required fields. Provide --name, --email and --username (or run interactively)."
1896
+ );
1897
+ return 1;
1898
+ }
1899
+ let sshKeyType = "default";
1900
+ let keyAlgorithm;
1901
+ let keyName;
1902
+ let passphrase;
1903
+ let existingKeyPath;
1904
+ const wantsDefault = flagBool(flags, "default");
1905
+ const wantsGenerate = flagBool(flags, "generate");
1906
+ const existingFlag = flagStr(flags, "existing");
1907
+ if (existingFlag) {
1908
+ sshKeyType = "existing";
1909
+ existingKeyPath = existingFlag;
1910
+ } else if (wantsGenerate) {
1911
+ sshKeyType = "generated";
1912
+ } else if (wantsDefault) {
1913
+ sshKeyType = "default";
1914
+ } else if (interactive) {
1915
+ const idx = await select("SSH key:", [
1916
+ "Use default key (~/.ssh/id_ed25519 or id_rsa)",
1917
+ "Generate a new key",
1918
+ "Use an existing key file"
1919
+ ]);
1920
+ sshKeyType = idx === 1 ? "generated" : idx === 2 ? "existing" : "default";
1921
+ }
1922
+ if (sshKeyType === "generated") {
1923
+ keyAlgorithm = flagStr(flags, "algorithm") || "ed25519";
1924
+ if (keyAlgorithm !== "ed25519" && keyAlgorithm !== "rsa") {
1925
+ error('Invalid --algorithm. Use "ed25519" or "rsa".');
1926
+ return 1;
1927
+ }
1928
+ keyName = flagStr(flags, "key-name");
1929
+ if (!keyName && interactive) {
1930
+ keyName = await ask("Key filename", `id_${username}`);
1931
+ }
1932
+ if (!keyName) {
1933
+ error("Generating a key requires --key-name.");
1934
+ return 1;
1935
+ }
1936
+ passphrase = flagStr(flags, "passphrase");
1937
+ if (passphrase === void 0 && interactive) {
1938
+ const usePass = await confirm(
1939
+ "Protect the key with a passphrase?",
1940
+ false
1941
+ );
1942
+ if (usePass) passphrase = await askSecret("Passphrase");
1943
+ }
1944
+ }
1945
+ if (sshKeyType === "existing") {
1946
+ if (!existingKeyPath && interactive) {
1947
+ existingKeyPath = await ask("Path to existing private key");
1948
+ }
1949
+ if (!existingKeyPath) {
1950
+ error("Using an existing key requires --existing <path>.");
1951
+ return 1;
1952
+ }
1953
+ if (!fs9.existsSync(existingKeyPath)) {
1954
+ error(`Key file not found: ${existingKeyPath}`);
1955
+ return 1;
1956
+ }
1957
+ }
1958
+ const input = {
1959
+ name,
1960
+ email,
1961
+ username,
1962
+ provider,
1963
+ sshKeyType,
1964
+ keyAlgorithm,
1965
+ keyName,
1966
+ passphrase: passphrase || void 0,
1967
+ existingKeyPath,
1968
+ avatar: flagStr(flags, "avatar") || "\u{1F464}",
1969
+ color: flagStr(flags, "color") || "#3b82f6"
1970
+ };
1971
+ try {
1972
+ const profile = await profileManager_exports.createProfile(input, "cli");
1973
+ if (jsonOut) {
1974
+ console.log(JSON.stringify(profile, null, 2));
1975
+ return 0;
1976
+ }
1977
+ success(
1978
+ `Created profile ${c.bold(profile.name)} ${c.gray(`(${profile.username})`)}`
1979
+ );
1980
+ if (profile.keyPath) info(`SSH key: ${profile.keyPath}`);
1981
+ if (profile.hostConfigured) info("SSH config entry added.");
1982
+ console.log("");
1983
+ info(`Activate it with: ${c.cyan(`devswitch use ${profile.username}`)}`);
1984
+ return 0;
1985
+ } catch (err) {
1986
+ error(err instanceof Error ? err.message : "Failed to create profile.");
1987
+ return 1;
1988
+ }
1989
+ }
1990
+
1991
+ // src/commands/remove.ts
1992
+ async function removeCommand(args) {
1993
+ const identifier = args.positionals[0];
1994
+ if (!identifier) {
1995
+ error("Usage: devswitch remove <profile>");
1996
+ return 1;
1997
+ }
1998
+ const profile = storageService.findProfile(identifier);
1999
+ if (!profile) {
2000
+ error(`No profile found matching "${identifier}".`);
2001
+ return 1;
2002
+ }
2003
+ const jsonOut = flagBool(args.flags, "json");
2004
+ const skipConfirm = flagBool(args.flags, "yes", "y");
2005
+ const interactive = process.stdin.isTTY && !jsonOut;
2006
+ if (!skipConfirm && interactive) {
2007
+ const willDeleteKey = profile.sshKeyType === "generated";
2008
+ const extra = willDeleteKey ? ` This will also delete the generated SSH key at ${profile.keyPath}.` : "";
2009
+ const ok = await confirm(
2010
+ `Delete profile "${profile.name}"?${extra}`,
2011
+ false
2012
+ );
2013
+ if (!ok) {
2014
+ info("Cancelled.");
2015
+ return 0;
2016
+ }
2017
+ } else if (!skipConfirm && !interactive) {
2018
+ error("Refusing to delete without confirmation. Re-run with --yes.");
2019
+ return 1;
2020
+ }
2021
+ const deleted = await profileManager_exports.deleteProfile(profile.id, "cli");
2022
+ if (jsonOut) {
2023
+ console.log(JSON.stringify({ success: deleted, id: profile.id }, null, 2));
2024
+ return deleted ? 0 : 1;
2025
+ }
2026
+ if (deleted) {
2027
+ success(`Removed profile ${c.bold(profile.name)}.`);
2028
+ return 0;
2029
+ }
2030
+ error("Failed to remove profile.");
2031
+ return 1;
2032
+ }
2033
+
2034
+ // src/commands/show.ts
2035
+ async function showCommand(args) {
2036
+ const identifier = args.positionals[0];
2037
+ if (!identifier) {
2038
+ error("Usage: devswitch show <profile>");
2039
+ return 1;
2040
+ }
2041
+ const profile = storageService.findProfile(identifier);
2042
+ if (!profile) {
2043
+ error(`No profile found matching "${identifier}".`);
2044
+ return 1;
2045
+ }
2046
+ if (flagBool(args.flags, "json")) {
2047
+ console.log(JSON.stringify(profile, null, 2));
2048
+ return 0;
2049
+ }
2050
+ const { sshHost } = getProviderSSHConfig(profile.provider);
2051
+ const hostAlias = `${sshHost}-${profile.username}`;
2052
+ const configured = sshConfigService.checkProfileConfigured(profile);
2053
+ heading(`${profile.avatar ? profile.avatar + " " : ""}${profile.name}`);
2054
+ const line = (label, value) => console.log(` ${c.gray(label.padEnd(14))} ${value}`);
2055
+ line("Username", profile.username);
2056
+ line("Email", profile.email);
2057
+ line("Provider", profile.provider || "github");
2058
+ line("SSH key type", profile.sshKeyType);
2059
+ line("Algorithm", profile.keyAlgorithm || "\u2014");
2060
+ line("Key path", profile.keyPath || "\u2014");
2061
+ line("Passphrase", profile.hasPassphrase ? "yes (encrypted)" : "no");
2062
+ line("Host alias", hostAlias);
2063
+ line(
2064
+ "SSH config",
2065
+ configured ? c.green("configured") : c.yellow("not configured")
2066
+ );
2067
+ if (profile.providerMeta?.connected || profile.githubConnected) {
2068
+ line(
2069
+ "OAuth",
2070
+ c.green(
2071
+ `connected (${profile.providerMeta?.username || profile.githubUsername || "?"})`
2072
+ )
2073
+ );
2074
+ }
2075
+ line("Created", new Date(profile.createdAt).toLocaleString());
2076
+ line("Updated", new Date(profile.updatedAt).toLocaleString());
2077
+ console.log("");
2078
+ console.log(
2079
+ ` ${c.gray("Clone with:")} git clone git@${hostAlias}:<owner>/<repo>.git`
2080
+ );
2081
+ return 0;
2082
+ }
2083
+
2084
+ // src/commands/sync.ts
2085
+ async function syncCommand(args) {
2086
+ const result = await profileManager_exports.scanAndSync("cli");
2087
+ if (flagBool(args.flags, "json")) {
2088
+ console.log(JSON.stringify(result, null, 2));
2089
+ return result.success ? 0 : 1;
2090
+ }
2091
+ if (!result.success) {
2092
+ error(result.error || "Sync failed.");
2093
+ return 1;
2094
+ }
2095
+ if (result.syncedCount > 0) {
2096
+ success(
2097
+ `Synced ${c.bold(String(result.syncedCount))} new profile${result.syncedCount === 1 ? "" : "s"}.`
2098
+ );
2099
+ for (const p of result.profiles) {
2100
+ console.log(` ${c.gray("\u2022")} ${p.name} ${c.gray(`(${p.keyPath})`)}`);
2101
+ }
2102
+ } else {
2103
+ info("No new profiles to sync. All SSH keys are already managed.");
2104
+ }
2105
+ if (result.skippedCount > 0) {
2106
+ info(
2107
+ `Skipped ${result.skippedCount} already-managed key${result.skippedCount === 1 ? "" : "s"}.`
2108
+ );
2109
+ }
2110
+ return 0;
2111
+ }
2112
+
2113
+ // src/commands/test.ts
2114
+ async function testCommand(args) {
2115
+ const identifier = args.positionals[0];
2116
+ if (!identifier) {
2117
+ error("Usage: devswitch test <profile>");
2118
+ return 1;
2119
+ }
2120
+ const profile = storageService.findProfile(identifier);
2121
+ if (!profile) {
2122
+ error(`No profile found matching "${identifier}".`);
2123
+ return 1;
2124
+ }
2125
+ const { sshHost, sshUser } = getProviderSSHConfig(profile.provider);
2126
+ const hostAlias = `${sshHost}-${profile.username}`;
2127
+ if (profile.keyPath) {
2128
+ await sshConfigService.updateConfig(profile);
2129
+ }
2130
+ const jsonOut = flagBool(args.flags, "json");
2131
+ if (!jsonOut)
2132
+ info(
2133
+ `Testing SSH connection for ${c.bold(profile.name)} via ${hostAlias}...`
2134
+ );
2135
+ const result = await testSSHConnection({
2136
+ hostAlias,
2137
+ sshUser,
2138
+ keyPath: profile.keyPath || void 0
2139
+ });
2140
+ if (jsonOut) {
2141
+ console.log(
2142
+ JSON.stringify({ profile: profile.name, hostAlias, ...result }, null, 2)
2143
+ );
2144
+ return result.success ? 0 : 1;
2145
+ }
2146
+ if (result.success) {
2147
+ success("Authentication succeeded.");
2148
+ if (result.output) console.log(c.gray(result.output));
2149
+ return 0;
2150
+ }
2151
+ error("Authentication failed.");
2152
+ if (result.output) console.log(c.gray(result.output));
2153
+ if (result.error) console.log(c.gray(result.error));
2154
+ return 1;
2155
+ }
2156
+
2157
+ // src/commands/pubkey.ts
2158
+ async function pubkeyCommand(args) {
2159
+ const identifier = args.positionals[0];
2160
+ if (!identifier) {
2161
+ error("Usage: devswitch pubkey <profile>");
2162
+ return 1;
2163
+ }
2164
+ const profile = storageService.findProfile(identifier);
2165
+ if (!profile) {
2166
+ error(`No profile found matching "${identifier}".`);
2167
+ return 1;
2168
+ }
2169
+ if (!profile.keyPath) {
2170
+ error("This profile has no associated SSH key.");
2171
+ return 1;
2172
+ }
2173
+ const content = sshKeyService.getPublicKeyContent(profile.keyPath);
2174
+ if (!content) {
2175
+ error(`Public key not found for ${profile.keyPath}.pub`);
2176
+ return 1;
2177
+ }
2178
+ console.log(content);
2179
+ return 0;
2180
+ }
2181
+
2182
+ // src/commands/clone.ts
2183
+ var path7 = __toESM(require("path"), 1);
2184
+ async function cloneCommand(args) {
2185
+ const repoUrl = args.positionals[0];
2186
+ const destination = args.positionals[1] || process.cwd();
2187
+ if (!repoUrl) {
2188
+ error(
2189
+ "Usage: devswitch clone <repo-url> [destination-dir] --profile <profile>"
2190
+ );
2191
+ return 1;
2192
+ }
2193
+ const profileId = flagStr(args.flags, "profile", "p");
2194
+ if (!profileId) {
2195
+ error("A profile is required. Pass --profile <name>.");
2196
+ return 1;
2197
+ }
2198
+ const profile = storageService.findProfile(profileId);
2199
+ if (!profile) {
2200
+ error(`No profile found matching "${profileId}".`);
2201
+ return 1;
2202
+ }
2203
+ const { sshHost } = getProviderSSHConfig(profile.provider);
2204
+ const hostAlias = `${sshHost}-${profile.username}`;
2205
+ const jsonOut = flagBool(args.flags, "json");
2206
+ if (!jsonOut) {
2207
+ info(
2208
+ `Cloning ${repoUrl} into ${path7.resolve(destination)} as ${c.bold(profile.name)}...`
2209
+ );
2210
+ }
2211
+ const result = await gitService.cloneRepository({
2212
+ repoUrl,
2213
+ destinationFolder: destination,
2214
+ username: profile.username,
2215
+ email: profile.email,
2216
+ hostAlias
2217
+ });
2218
+ if (jsonOut) {
2219
+ console.log(JSON.stringify(result, null, 2));
2220
+ return result.success ? 0 : 1;
2221
+ }
2222
+ if (result.success) {
2223
+ success(`Cloned to ${result.clonedPath}`);
2224
+ info(`Local git identity set to ${profile.username} <${profile.email}>`);
2225
+ return 0;
2226
+ }
2227
+ error(result.error || "Clone failed.");
2228
+ return 1;
2229
+ }
2230
+
2231
+ // src/commands/logs.ts
2232
+ async function logsCommand(args) {
2233
+ const limitStr = flagStr(args.flags, "limit", "n");
2234
+ const limit = limitStr ? parseInt(limitStr, 10) : 20;
2235
+ const logs = logService.getAllLogs().slice(0, Number.isNaN(limit) ? 20 : limit);
2236
+ if (flagBool(args.flags, "json")) {
2237
+ console.log(JSON.stringify(logs, null, 2));
2238
+ return 0;
2239
+ }
2240
+ if (logs.length === 0) {
2241
+ info("No activity logged yet.");
2242
+ return 0;
2243
+ }
2244
+ for (const log of logs) {
2245
+ const time = new Date(log.timestamp).toLocaleString();
2246
+ const src = log.source === "cli" ? c.magenta("[cli]") : c.blue("[app]");
2247
+ console.log(`${c.gray(time)} ${src} ${c.cyan(log.action)} ${log.message}`);
2248
+ }
2249
+ return 0;
2250
+ }
2251
+
2252
+ // src/commands/path.ts
2253
+ async function pathCommand(args) {
2254
+ const data = {
2255
+ dataDir: getDataDir(),
2256
+ profiles: getProfilesFilePath(),
2257
+ logs: getLogsFilePath()
2258
+ };
2259
+ if (flagBool(args.flags, "json")) {
2260
+ console.log(JSON.stringify(data, null, 2));
2261
+ return 0;
2262
+ }
2263
+ console.log(`${c.gray("Data dir:")} ${data.dataDir}`);
2264
+ console.log(`${c.gray("Profiles:")} ${data.profiles}`);
2265
+ console.log(`${c.gray("Logs: ")} ${data.logs}`);
2266
+ return 0;
2267
+ }
2268
+
2269
+ // src/commands/doctor.ts
2270
+ var fs10 = __toESM(require("fs"), 1);
2271
+ var import_child_process5 = require("child_process");
2272
+ function checkCmd(cmd) {
2273
+ try {
2274
+ return (0, import_child_process5.execSync)(cmd, {
2275
+ stdio: ["ignore", "pipe", "ignore"],
2276
+ encoding: "utf-8"
2277
+ }).trim().split("\n")[0];
2278
+ } catch {
2279
+ return null;
2280
+ }
2281
+ }
2282
+ async function doctorCommand(args) {
2283
+ const dataDir = getDataDir();
2284
+ const profilesPath = getProfilesFilePath();
2285
+ const profiles = storageService.getAllProfiles();
2286
+ const git = checkCmd("git --version");
2287
+ const sshKeygen = checkCmd("ssh-keygen --help 2>&1 | head -1") || checkCmd("which ssh-keygen");
2288
+ const sshAdd = checkCmd("ssh-add -l");
2289
+ const checks = {
2290
+ cliVersion: CLI_VERSION,
2291
+ node: process.version,
2292
+ dataDir,
2293
+ dataDirExists: fs10.existsSync(dataDir),
2294
+ profilesFile: profilesPath,
2295
+ profilesFileExists: fs10.existsSync(profilesPath),
2296
+ profileCount: profiles.length,
2297
+ git: git || null,
2298
+ sshKeygenAvailable: !!sshKeygen,
2299
+ sshAgentRunning: sshAdd !== null
2300
+ };
2301
+ if (flagBool(args.flags, "json")) {
2302
+ console.log(JSON.stringify(checks, null, 2));
2303
+ return 0;
2304
+ }
2305
+ heading("DevSwitch CLI \u2014 doctor");
2306
+ const row = (ok, label, detail) => console.log(
2307
+ ` ${ok ? sym.ok : sym.warn} ${label.padEnd(22)} ${c.gray(detail)}`
2308
+ );
2309
+ row(true, "CLI version", CLI_VERSION);
2310
+ row(true, "Node", process.version);
2311
+ row(
2312
+ checks.dataDirExists,
2313
+ "Shared data dir",
2314
+ dataDir + (checks.dataDirExists ? "" : " (will be created)")
2315
+ );
2316
+ row(
2317
+ checks.profilesFileExists,
2318
+ "Profiles file",
2319
+ checks.profilesFileExists ? profilesPath : "not created yet"
2320
+ );
2321
+ row(true, "Profiles stored", String(checks.profileCount));
2322
+ row(!!git, "git", git || "not found in PATH");
2323
+ row(!!sshKeygen, "ssh-keygen", sshKeygen ? "available" : "not found");
2324
+ row(
2325
+ checks.sshAgentRunning,
2326
+ "ssh-agent",
2327
+ checks.sshAgentRunning ? "reachable" : "not running / unreachable"
2328
+ );
2329
+ const notConfigured = profiles.filter(
2330
+ (p) => p.keyPath && !sshConfigService.checkProfileConfigured(p) && p.sshKeyType !== "default"
2331
+ );
2332
+ if (notConfigured.length > 0) {
2333
+ console.log("");
2334
+ console.log(
2335
+ ` ${sym.warn} ${notConfigured.length} profile(s) missing SSH config entries:`
2336
+ );
2337
+ for (const p of notConfigured)
2338
+ console.log(
2339
+ ` ${c.gray("\u2022")} ${p.name} \u2014 run 'devswitch use ${p.username}'`
2340
+ );
2341
+ }
2342
+ console.log("");
2343
+ return 0;
2344
+ }
2345
+
2346
+ // src/commands/current.ts
2347
+ async function currentCommand(args) {
2348
+ const gitConfig = await sshConfigService.getGlobalGitConfig();
2349
+ const email = gitConfig["user.email"];
2350
+ const name = gitConfig["user.name"];
2351
+ const profiles = storageService.getAllProfiles();
2352
+ const match = profiles.find(
2353
+ (p) => email && p.email.toLowerCase() === email.toLowerCase()
2354
+ ) || profiles.find(
2355
+ (p) => name && p.username.toLowerCase() === name.toLowerCase()
2356
+ );
2357
+ if (flagBool(args.flags, "json")) {
2358
+ console.log(
2359
+ JSON.stringify(
2360
+ {
2361
+ globalGit: { name: name || null, email: email || null },
2362
+ activeProfile: match || null
2363
+ },
2364
+ null,
2365
+ 2
2366
+ )
2367
+ );
2368
+ return 0;
2369
+ }
2370
+ console.log(
2371
+ `${c.gray("Global git user:")} ${name || c.gray("unset")} <${email || c.gray("unset")}>`
2372
+ );
2373
+ if (match) {
2374
+ console.log(
2375
+ `${c.gray("Active profile: ")} ${match.avatar ? match.avatar + " " : ""}${c.bold(match.name)} ${c.gray(`(${match.provider || "github"})`)}`
2376
+ );
2377
+ } else {
2378
+ info(
2379
+ "No DevSwitch profile matches the current global git identity. Use 'devswitch use <profile>' to switch."
2380
+ );
2381
+ }
2382
+ return 0;
2383
+ }
2384
+
2385
+ // src/index.ts
2386
+ logService.setDefaultSource("cli");
2387
+ var COMMANDS = {
2388
+ list: listCommand,
2389
+ ls: listCommand,
2390
+ use: useCommand,
2391
+ switch: useCommand,
2392
+ add: addCommand,
2393
+ create: addCommand,
2394
+ new: addCommand,
2395
+ remove: removeCommand,
2396
+ rm: removeCommand,
2397
+ delete: removeCommand,
2398
+ show: showCommand,
2399
+ view: showCommand,
2400
+ info: showCommand,
2401
+ sync: syncCommand,
2402
+ test: testCommand,
2403
+ pubkey: pubkeyCommand,
2404
+ clone: cloneCommand,
2405
+ logs: logsCommand,
2406
+ log: logsCommand,
2407
+ path: pathCommand,
2408
+ doctor: doctorCommand,
2409
+ current: currentCommand,
2410
+ whoami: currentCommand
2411
+ };
2412
+ async function main() {
2413
+ const argv = process.argv.slice(2);
2414
+ const [command, ...rest] = argv;
2415
+ if (!command || command === "help") {
2416
+ printHelp(rest[0]);
2417
+ return 0;
2418
+ }
2419
+ if (command === "--version" || command === "-v" || command === "version") {
2420
+ console.log(CLI_VERSION);
2421
+ return 0;
2422
+ }
2423
+ const parsed = parseArgs(rest);
2424
+ if (flagBool(parsed.flags, "help", "h")) {
2425
+ printHelp(command);
2426
+ return 0;
2427
+ }
2428
+ const handler = COMMANDS[command];
2429
+ if (!handler) {
2430
+ error(`Unknown command: "${command}"`);
2431
+ printHelp();
2432
+ return 1;
2433
+ }
2434
+ return handler(parsed);
2435
+ }
2436
+ main().then((code) => process.exit(code)).catch((err) => {
2437
+ error(err instanceof Error ? err.message : String(err));
2438
+ if (process.env.DEVSWITCH_DEBUG) console.error(err);
2439
+ process.exit(1);
2440
+ });