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.
- package/README.md +182 -0
- package/bin/devswitch.cjs +2440 -0
- 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
|
+
});
|