@vancityayush/xpssh 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +84 -0
  3. package/dist/index.js +2929 -0
  4. package/package.json +56 -0
package/dist/index.js ADDED
@@ -0,0 +1,2929 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ var __defProp = Object.defineProperty;
4
+ var __returnValue = (v) => v;
5
+ function __exportSetter(name, newValue) {
6
+ this[name] = __returnValue.bind(null, newValue);
7
+ }
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, {
11
+ get: all[name],
12
+ enumerable: true,
13
+ configurable: true,
14
+ set: __exportSetter.bind(all, name)
15
+ });
16
+ };
17
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
18
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
19
+
20
+ // src/version.ts
21
+ import { createRequire as createRequire2 } from "node:module";
22
+ var require2, VERSION;
23
+ var init_version = __esm(() => {
24
+ require2 = createRequire2(import.meta.url);
25
+ VERSION = require2("../package.json").version;
26
+ });
27
+
28
+ // src/tui/store.tsx
29
+ import { createContext, useContext, useReducer } from "react";
30
+ import { jsxDEV } from "react/jsx-dev-runtime";
31
+ function reducer(state, action) {
32
+ switch (action.type) {
33
+ case "navigate":
34
+ return { ...state, screen: action.screen };
35
+ case "set-profiles":
36
+ return { ...state, profiles: action.profiles };
37
+ case "append-event":
38
+ return { ...state, transcript: [...state.transcript, action.event].slice(-MAX_TRANSCRIPT) };
39
+ case "clear-transcript":
40
+ return { ...state, transcript: [] };
41
+ case "set-busy":
42
+ return { ...state, busy: action.busy };
43
+ case "set-focus":
44
+ return { ...state, focusZone: action.zone };
45
+ case "push-history":
46
+ return { ...state, history: [...state.history, action.line].slice(-100) };
47
+ case "set-prompt":
48
+ return { ...state, prompt: action.prompt };
49
+ case "set-setup-prefill":
50
+ return { ...state, setupPrefill: action.args };
51
+ }
52
+ }
53
+ function StoreProvider({ children }) {
54
+ const [state, dispatch] = useReducer(reducer, initialState);
55
+ return /* @__PURE__ */ jsxDEV(StoreContext.Provider, {
56
+ value: { state, dispatch },
57
+ children
58
+ }, undefined, false, undefined, this);
59
+ }
60
+ function useStore() {
61
+ const store = useContext(StoreContext);
62
+ if (!store)
63
+ throw new Error("useStore outside StoreProvider");
64
+ return store;
65
+ }
66
+ var MAX_TRANSCRIPT = 200, initialState, StoreContext;
67
+ var init_store = __esm(() => {
68
+ initialState = {
69
+ screen: "dashboard",
70
+ profiles: [],
71
+ transcript: [],
72
+ busy: false,
73
+ focusZone: "screen",
74
+ history: [],
75
+ prompt: null,
76
+ setupPrefill: null
77
+ };
78
+ StoreContext = createContext(null);
79
+ });
80
+
81
+ // src/services/exec.ts
82
+ import { spawn } from "node:child_process";
83
+ var realExec = (cmd, args, opts = {}) => new Promise((resolve) => {
84
+ const child = spawn(cmd, args, { stdio: ["pipe", "pipe", "pipe"] });
85
+ let stdout = "";
86
+ let stderr = "";
87
+ let settled = false;
88
+ const settle = (result) => {
89
+ if (!settled) {
90
+ settled = true;
91
+ resolve(result);
92
+ }
93
+ };
94
+ const timer = opts.timeoutMs ? setTimeout(() => {
95
+ child.kill("SIGKILL");
96
+ settle({ code: null, stdout, stderr: stderr + `
97
+ [xpssh] timed out after ${opts.timeoutMs}ms` });
98
+ }, opts.timeoutMs) : null;
99
+ child.stdout.on("data", (chunk) => stdout += chunk.toString());
100
+ child.stderr.on("data", (chunk) => stderr += chunk.toString());
101
+ child.on("error", (err) => {
102
+ if (timer)
103
+ clearTimeout(timer);
104
+ settle({ code: null, stdout, stderr: `${err.message}` });
105
+ });
106
+ child.on("close", (code) => {
107
+ if (timer)
108
+ clearTimeout(timer);
109
+ settle({ code, stdout, stderr });
110
+ });
111
+ if (opts.stdin !== undefined) {
112
+ child.stdin.write(opts.stdin);
113
+ }
114
+ child.stdin.end();
115
+ });
116
+ var init_exec = () => {};
117
+
118
+ // src/platform/paths.ts
119
+ import { homedir } from "node:os";
120
+ import { join } from "node:path";
121
+ function resolvePaths(env = process.env) {
122
+ const home = env["HOME"] ?? env["USERPROFILE"] ?? homedir();
123
+ const configDir = env["XPSSH_CONFIG_DIR"] ?? (env["XDG_CONFIG_HOME"] ? join(env["XDG_CONFIG_HOME"], "xpssh") : join(home, ".config", "xpssh"));
124
+ const sshDir = join(home, ".ssh");
125
+ return {
126
+ home,
127
+ sshDir,
128
+ sshConfig: join(sshDir, "config"),
129
+ configDir,
130
+ manifest: join(configDir, "profiles.json")
131
+ };
132
+ }
133
+ function expandTilde(path, home) {
134
+ if (path === "~")
135
+ return home;
136
+ if (path.startsWith("~/"))
137
+ return join(home, path.slice(2));
138
+ return path;
139
+ }
140
+ function contractTilde(path, home) {
141
+ if (path === home)
142
+ return "~";
143
+ if (path.startsWith(home + "/"))
144
+ return "~/" + path.slice(home.length + 1);
145
+ return path;
146
+ }
147
+ var init_paths = () => {};
148
+
149
+ // src/platform/os.ts
150
+ function resolveOs(platform = process.platform) {
151
+ switch (platform) {
152
+ case "darwin":
153
+ return {
154
+ platform: "darwin",
155
+ hasKeychain: true,
156
+ clipboardCommands: [["pbcopy"]],
157
+ openCommand: ["open"]
158
+ };
159
+ case "win32":
160
+ return {
161
+ platform: "win32",
162
+ hasKeychain: false,
163
+ clipboardCommands: [["clip.exe"]],
164
+ openCommand: ["cmd", "/c", "start", ""]
165
+ };
166
+ default:
167
+ return {
168
+ platform: "linux",
169
+ hasKeychain: false,
170
+ clipboardCommands: [["xclip", "-selection", "clipboard"], ["xsel", "--clipboard", "--input"], ["wl-copy"]],
171
+ openCommand: ["xdg-open"]
172
+ };
173
+ }
174
+ }
175
+
176
+ // src/cli/tokenize.ts
177
+ function tokenize(line) {
178
+ const tokens = [];
179
+ let current = "";
180
+ let hasToken = false;
181
+ let quote = null;
182
+ let escaped = false;
183
+ for (const ch of line) {
184
+ if (escaped) {
185
+ current += ch;
186
+ escaped = false;
187
+ continue;
188
+ }
189
+ if (ch === "\\" && quote !== "'") {
190
+ escaped = true;
191
+ hasToken = true;
192
+ continue;
193
+ }
194
+ if (quote) {
195
+ if (ch === quote) {
196
+ quote = null;
197
+ } else {
198
+ current += ch;
199
+ }
200
+ continue;
201
+ }
202
+ if (ch === '"' || ch === "'") {
203
+ quote = ch;
204
+ hasToken = true;
205
+ continue;
206
+ }
207
+ if (ch === " " || ch === "\t") {
208
+ if (hasToken) {
209
+ tokens.push(current);
210
+ current = "";
211
+ hasToken = false;
212
+ }
213
+ continue;
214
+ }
215
+ current += ch;
216
+ hasToken = true;
217
+ }
218
+ if (escaped)
219
+ current += "\\";
220
+ if (hasToken)
221
+ tokens.push(current);
222
+ return tokens;
223
+ }
224
+
225
+ // src/commands/types.ts
226
+ function defineCommand(def) {
227
+ return def;
228
+ }
229
+ var UsageError;
230
+ var init_types = __esm(() => {
231
+ UsageError = class UsageError extends Error {
232
+ constructor(message) {
233
+ super(message);
234
+ this.name = "UsageError";
235
+ }
236
+ };
237
+ });
238
+
239
+ // src/core/providers/index.ts
240
+ function getProvider(idOrAlias) {
241
+ const needle = idOrAlias.toLowerCase();
242
+ return PROVIDERS.find((p) => p.id === needle || p.aliases.includes(needle));
243
+ }
244
+ var PROVIDERS;
245
+ var init_providers = __esm(() => {
246
+ PROVIDERS = [
247
+ {
248
+ id: "github",
249
+ label: "GitHub",
250
+ aliases: ["gh", "github.com"],
251
+ host: "github.com",
252
+ sshUser: "git",
253
+ keyType: "ed25519",
254
+ settingsUrl: "https://github.com/settings/keys",
255
+ api: {
256
+ tokenEnvVar: "XPSSH_TOKEN_GITHUB",
257
+ tokenHint: "Create a PAT with the admin:public_key scope at https://github.com/settings/tokens",
258
+ buildUploadRequest: (token, title, publicKey) => ({
259
+ url: "https://api.github.com/user/keys",
260
+ method: "POST",
261
+ headers: {
262
+ Authorization: `Bearer ${token}`,
263
+ Accept: "application/vnd.github+json",
264
+ "Content-Type": "application/json",
265
+ "User-Agent": "xpssh"
266
+ },
267
+ body: JSON.stringify({ title, key: publicKey })
268
+ })
269
+ }
270
+ },
271
+ {
272
+ id: "gitlab",
273
+ label: "GitLab",
274
+ aliases: ["gl", "gitlab.com"],
275
+ host: "gitlab.com",
276
+ sshUser: "git",
277
+ keyType: "ed25519",
278
+ settingsUrl: "https://gitlab.com/-/profile/keys",
279
+ api: {
280
+ tokenEnvVar: "XPSSH_TOKEN_GITLAB",
281
+ tokenHint: "Create a personal access token with the api scope at https://gitlab.com/-/user_settings/personal_access_tokens",
282
+ buildUploadRequest: (token, title, publicKey) => ({
283
+ url: "https://gitlab.com/api/v4/user/keys",
284
+ method: "POST",
285
+ headers: {
286
+ "PRIVATE-TOKEN": token,
287
+ "Content-Type": "application/json"
288
+ },
289
+ body: JSON.stringify({ title, key: publicKey })
290
+ })
291
+ }
292
+ },
293
+ {
294
+ id: "bitbucket",
295
+ label: "Bitbucket",
296
+ aliases: ["bb", "bitbucket.org"],
297
+ host: "bitbucket.org",
298
+ sshUser: "git",
299
+ keyType: "ed25519",
300
+ settingsUrl: "https://bitbucket.org/account/settings/ssh-keys/",
301
+ api: {
302
+ tokenEnvVar: "XPSSH_TOKEN_BITBUCKET",
303
+ tokenHint: "Create an API token with account:write at https://id.atlassian.com/manage-profile/security/api-tokens (used as Bearer)",
304
+ buildUploadRequest: (token, title, publicKey) => ({
305
+ url: "https://api.bitbucket.org/2.0/users/{uuid}/ssh-keys",
306
+ method: "POST",
307
+ headers: {
308
+ Authorization: `Bearer ${token}`,
309
+ "Content-Type": "application/json"
310
+ },
311
+ body: JSON.stringify({ label: title, key: publicKey })
312
+ })
313
+ }
314
+ },
315
+ {
316
+ id: "azure",
317
+ label: "Azure DevOps",
318
+ aliases: ["ado", "azuredevops", "dev.azure.com", "ssh.dev.azure.com"],
319
+ host: "ssh.dev.azure.com",
320
+ sshUser: "git",
321
+ keyType: "rsa",
322
+ settingsUrl: "https://dev.azure.com/_usersSettings/keys",
323
+ api: null
324
+ }
325
+ ];
326
+ });
327
+
328
+ // src/core/profile.ts
329
+ function sanitizeName(raw) {
330
+ return raw.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-");
331
+ }
332
+ function deriveProfileId(providerId, name) {
333
+ return `${providerId}-${sanitizeName(name)}`;
334
+ }
335
+ function deriveAlias(host, name, isDefault) {
336
+ return isDefault ? host : `${host}-${sanitizeName(name)}`;
337
+ }
338
+ function deriveKeyPath(providerId, name) {
339
+ return `~/.ssh/xpssh_${providerId}_${sanitizeName(name).replace(/-/g, "_")}`;
340
+ }
341
+ function clonePrefix(profile, sshUser = "git") {
342
+ return `${sshUser}@${profile.alias}:`;
343
+ }
344
+
345
+ // src/core/manifest.ts
346
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
347
+ import { dirname, join as join2 } from "node:path";
348
+ import { z } from "zod";
349
+ async function loadManifest(manifestPath) {
350
+ let raw;
351
+ try {
352
+ raw = await readFile(manifestPath, "utf8");
353
+ } catch (err) {
354
+ if (err.code === "ENOENT") {
355
+ return { version: 1, profiles: [] };
356
+ }
357
+ throw err;
358
+ }
359
+ let json;
360
+ try {
361
+ json = JSON.parse(raw);
362
+ } catch {
363
+ throw new ManifestError(`${manifestPath} is not valid JSON — fix or delete it`);
364
+ }
365
+ const parsed = manifestSchema.safeParse(json);
366
+ if (!parsed.success) {
367
+ throw new ManifestError(`${manifestPath} failed validation: ${parsed.error.issues[0]?.message}`);
368
+ }
369
+ return parsed.data;
370
+ }
371
+ async function saveManifest(manifestPath, manifest) {
372
+ manifestSchema.parse(manifest);
373
+ await mkdir(dirname(manifestPath), { recursive: true });
374
+ const tmp = join2(dirname(manifestPath), `.profiles.json.tmp-${process.pid}`);
375
+ await writeFile(tmp, JSON.stringify(manifest, null, 2) + `
376
+ `, { mode: 384 });
377
+ await rename(tmp, manifestPath);
378
+ }
379
+ function findProfile(manifest, idOrAlias) {
380
+ return manifest.profiles.find((p) => p.id === idOrAlias || p.alias === idOrAlias);
381
+ }
382
+ function upsertProfile(manifest, profile) {
383
+ if (profile.isDefault) {
384
+ const clash = manifest.profiles.find((p) => p.provider === profile.provider && p.isDefault && p.id !== profile.id);
385
+ if (clash) {
386
+ throw new ManifestError(`${clash.id} is already the default for ${profile.provider} — remove it or set up without --default`);
387
+ }
388
+ }
389
+ const rest = manifest.profiles.filter((p) => p.id !== profile.id);
390
+ return { ...manifest, profiles: [...rest, profile] };
391
+ }
392
+ function removeProfile(manifest, id) {
393
+ return { ...manifest, profiles: manifest.profiles.filter((p) => p.id !== id) };
394
+ }
395
+ var profileSchema, manifestSchema, ManifestError;
396
+ var init_manifest = __esm(() => {
397
+ profileSchema = z.object({
398
+ id: z.string().min(1),
399
+ provider: z.enum(["github", "gitlab", "bitbucket", "azure"]),
400
+ name: z.string().min(1),
401
+ email: z.string(),
402
+ alias: z.string().min(1),
403
+ keyPath: z.string().min(1),
404
+ keyType: z.enum(["ed25519", "rsa"]),
405
+ isDefault: z.boolean(),
406
+ createdAt: z.string(),
407
+ lastTest: z.object({ ok: z.boolean(), at: z.string(), message: z.string() }).optional(),
408
+ gitDirs: z.array(z.string()).default([]),
409
+ uploaded: z.object({ via: z.enum(["api", "manual"]), at: z.string() }).optional()
410
+ });
411
+ manifestSchema = z.object({
412
+ version: z.literal(1),
413
+ profiles: z.array(profileSchema)
414
+ });
415
+ ManifestError = class ManifestError extends Error {
416
+ constructor(message) {
417
+ super(message);
418
+ this.name = "ManifestError";
419
+ }
420
+ };
421
+ });
422
+
423
+ // src/core/sshconfig.ts
424
+ function splitKeepEnds(text) {
425
+ if (text === "")
426
+ return [];
427
+ const lines = text.split(/(?<=\n)/);
428
+ return lines;
429
+ }
430
+ function parseSegments(text) {
431
+ const segments = [];
432
+ let userBuf = "";
433
+ let managed = null;
434
+ for (const line of splitKeepEnds(text)) {
435
+ const stripped = line.replace(/\r?\n$/, "");
436
+ const begin = stripped.match(BEGIN_RE);
437
+ const end = stripped.match(END_RE);
438
+ if (begin) {
439
+ if (managed) {
440
+ throw new SshConfigParseError(`Nested xpssh fence: "${begin[1]}" begins inside unclosed "${managed.id}"`);
441
+ }
442
+ if (userBuf) {
443
+ segments.push({ kind: "user", text: userBuf });
444
+ userBuf = "";
445
+ }
446
+ managed = { id: begin[1], buf: line };
447
+ } else if (end) {
448
+ if (!managed) {
449
+ throw new SshConfigParseError(`Orphaned xpssh end fence for "${end[1]}"`);
450
+ }
451
+ if (end[1] !== managed.id) {
452
+ throw new SshConfigParseError(`Mismatched xpssh fences: begin "${managed.id}" closed by "${end[1]}"`);
453
+ }
454
+ segments.push({ kind: "managed", id: managed.id, text: managed.buf + line });
455
+ managed = null;
456
+ } else if (managed) {
457
+ managed.buf += line;
458
+ } else {
459
+ userBuf += line;
460
+ }
461
+ }
462
+ if (managed) {
463
+ throw new SshConfigParseError(`Unclosed xpssh fence for "${managed.id}"`);
464
+ }
465
+ if (userBuf) {
466
+ segments.push({ kind: "user", text: userBuf });
467
+ }
468
+ return segments;
469
+ }
470
+ function renderSegments(segments) {
471
+ return segments.map((s) => s.text).join("");
472
+ }
473
+ function buildHostBlock(spec) {
474
+ const lines = [
475
+ `# >>> xpssh:${spec.id} >>>`,
476
+ `Host ${spec.alias}`,
477
+ ` HostName ${spec.hostName}`,
478
+ ` User ${spec.user}`,
479
+ ` IdentityFile ${spec.identityFile}`,
480
+ ` IdentitiesOnly yes`,
481
+ ` AddKeysToAgent yes`,
482
+ ...spec.useKeychain ? [" UseKeychain yes"] : [],
483
+ `# <<< xpssh:${spec.id} <<<`
484
+ ];
485
+ return lines.join(`
486
+ `) + `
487
+ `;
488
+ }
489
+ function upsertBlock(segments, spec) {
490
+ const block = buildHostBlock(spec);
491
+ const index = segments.findIndex((s) => s.kind === "managed" && s.id === spec.id);
492
+ if (index !== -1) {
493
+ const next2 = [...segments];
494
+ next2[index] = { kind: "managed", id: spec.id, text: block };
495
+ return { segments: next2, action: "replaced" };
496
+ }
497
+ const next = [...segments];
498
+ const last = next[next.length - 1];
499
+ if (last && !last.text.endsWith(`
500
+ `)) {
501
+ next[next.length - 1] = { ...last, text: last.text + `
502
+ ` };
503
+ }
504
+ if (last) {
505
+ next.push({ kind: "user", text: `
506
+ ` });
507
+ }
508
+ next.push({ kind: "managed", id: spec.id, text: block });
509
+ return { segments: next, action: "added" };
510
+ }
511
+ function removeBlock(segments, id) {
512
+ const index = segments.findIndex((s) => s.kind === "managed" && s.id === id);
513
+ if (index === -1)
514
+ return { segments, removed: false };
515
+ const next = [...segments];
516
+ next.splice(index, 1);
517
+ const before = next[index - 1];
518
+ if (before && before.kind === "user" && /(^|\n)\n$/.test(before.text)) {
519
+ next[index - 1] = { ...before, text: before.text.replace(/\n$/, "") };
520
+ if (next[index - 1].text === "")
521
+ next.splice(index - 1, 1);
522
+ }
523
+ return { segments: next, removed: true };
524
+ }
525
+ function listManagedIds(segments) {
526
+ return segments.filter((s) => s.kind === "managed").map((s) => s.id);
527
+ }
528
+ function parseBlockFields(blockText) {
529
+ const fields = {};
530
+ for (const raw of blockText.split(`
531
+ `)) {
532
+ const line = raw.trim();
533
+ if (line.startsWith("#") || line === "")
534
+ continue;
535
+ const match = line.match(/^(\S+)\s+(.+)$/);
536
+ if (match)
537
+ fields[match[1]] = match[2];
538
+ }
539
+ return fields;
540
+ }
541
+ var BEGIN_RE, END_RE, SshConfigParseError;
542
+ var init_sshconfig = __esm(() => {
543
+ BEGIN_RE = /^# >>> xpssh:([A-Za-z0-9._-]+) >>>\s*$/;
544
+ END_RE = /^# <<< xpssh:([A-Za-z0-9._-]+) <<<\s*$/;
545
+ SshConfigParseError = class SshConfigParseError extends Error {
546
+ constructor(message) {
547
+ super(`${message} — fix ~/.ssh/config by hand or run \`xpssh doctor\``);
548
+ this.name = "SshConfigParseError";
549
+ }
550
+ };
551
+ });
552
+
553
+ // src/services/sshconfigFile.ts
554
+ import { copyFile, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, chmod } from "node:fs/promises";
555
+ import { dirname as dirname2 } from "node:path";
556
+ async function loadSshConfig(paths) {
557
+ let text;
558
+ try {
559
+ text = await readFile2(paths.sshConfig, "utf8");
560
+ } catch (err) {
561
+ if (err.code === "ENOENT")
562
+ return [];
563
+ throw err;
564
+ }
565
+ return parseSegments(text);
566
+ }
567
+ async function saveSshConfig(paths, segments) {
568
+ await mkdir2(dirname2(paths.sshConfig), { recursive: true, mode: 448 });
569
+ try {
570
+ await copyFile(paths.sshConfig, `${paths.sshConfig}.xpssh.bak`);
571
+ } catch (err) {
572
+ if (err.code !== "ENOENT")
573
+ throw err;
574
+ }
575
+ await writeFile2(paths.sshConfig, renderSegments(segments), { mode: 384 });
576
+ await chmod(paths.sshConfig, 384);
577
+ }
578
+ var init_sshconfigFile = __esm(() => {
579
+ init_sshconfig();
580
+ });
581
+
582
+ // src/services/keygen.ts
583
+ var exports_keygen = {};
584
+ __export(exports_keygen, {
585
+ readPublicKey: () => readPublicKey,
586
+ generateKeyPair: () => generateKeyPair,
587
+ deleteKeyPair: () => deleteKeyPair
588
+ });
589
+ import { mkdir as mkdir3, readFile as readFile3, unlink } from "node:fs/promises";
590
+ import { dirname as dirname3 } from "node:path";
591
+ async function generateKeyPair(exec, spec) {
592
+ await mkdir3(dirname3(spec.keyPath), { recursive: true, mode: 448 });
593
+ const args = [
594
+ "-t",
595
+ spec.keyType,
596
+ ...spec.keyType === "rsa" ? ["-b", "4096"] : [],
597
+ "-f",
598
+ spec.keyPath,
599
+ "-C",
600
+ spec.email,
601
+ "-N",
602
+ "",
603
+ "-q"
604
+ ];
605
+ const result = await exec("ssh-keygen", args);
606
+ if (result.code !== 0) {
607
+ throw new Error(`ssh-keygen failed: ${result.stderr.trim() || `exit ${result.code}`}`);
608
+ }
609
+ }
610
+ async function readPublicKey(keyPath) {
611
+ return (await readFile3(`${keyPath}.pub`, "utf8")).trim();
612
+ }
613
+ async function deleteKeyPair(keyPath) {
614
+ for (const file of [keyPath, `${keyPath}.pub`]) {
615
+ try {
616
+ await unlink(file);
617
+ } catch (err) {
618
+ if (err.code !== "ENOENT")
619
+ throw err;
620
+ }
621
+ }
622
+ }
623
+ var init_keygen = () => {};
624
+
625
+ // src/services/clipboard.ts
626
+ async function copyToClipboard(exec, os, text) {
627
+ for (const [cmd, ...args] of os.clipboardCommands) {
628
+ const result = await exec(cmd, args, { stdin: text });
629
+ if (result.code === 0)
630
+ return true;
631
+ }
632
+ return false;
633
+ }
634
+
635
+ // src/services/browser.ts
636
+ async function openInBrowser(exec, os, url) {
637
+ const [cmd, ...args] = os.openCommand;
638
+ const result = await exec(cmd, [...args, url]);
639
+ return result.code === 0;
640
+ }
641
+
642
+ // src/core/sshOutput.ts
643
+ function classifyTestOutput(code, stdout, stderr) {
644
+ const combined = `${stdout}
645
+ ${stderr}`.trim();
646
+ const firstLine = combined.split(`
647
+ `).map((l) => l.trim()).find((l) => l.length > 0 && !l.startsWith("Warning:")) ?? "";
648
+ if (DENIED_RE.test(combined)) {
649
+ return { ok: false, message: "Permission denied — the key is not registered with the provider yet" };
650
+ }
651
+ if (SUCCESS_RE.test(combined)) {
652
+ return { ok: true, message: firstLine };
653
+ }
654
+ if (code === 0 || code === 1) {
655
+ return { ok: true, message: firstLine || "Authenticated" };
656
+ }
657
+ return { ok: false, message: firstLine || `ssh exited with code ${code}` };
658
+ }
659
+ function parseAgentList(stdout) {
660
+ const keys = [];
661
+ for (const line of stdout.split(`
662
+ `)) {
663
+ const trimmed = line.trim();
664
+ if (!trimmed || /has no identities/i.test(trimmed))
665
+ continue;
666
+ const match = trimmed.match(/^(\d+)\s+(\S+)\s+(.*?)\s+\((\S+)\)$/);
667
+ if (match) {
668
+ keys.push({
669
+ bits: Number(match[1]),
670
+ fingerprint: match[2],
671
+ comment: match[3],
672
+ type: match[4]
673
+ });
674
+ }
675
+ }
676
+ return keys;
677
+ }
678
+ var SUCCESS_RE, DENIED_RE;
679
+ var init_sshOutput = __esm(() => {
680
+ SUCCESS_RE = /successfully authenticated|shell access is not supported|authenticated/i;
681
+ DENIED_RE = /permission denied/i;
682
+ });
683
+
684
+ // src/services/sshTest.ts
685
+ async function testConnection(exec, alias, sshUser = "git") {
686
+ const result = await exec("ssh", ["-T", "-o", "StrictHostKeyChecking=accept-new", "-o", "BatchMode=yes", `${sshUser}@${alias}`], { timeoutMs: 20000 });
687
+ return classifyTestOutput(result.code, result.stdout, result.stderr);
688
+ }
689
+ var init_sshTest = __esm(() => {
690
+ init_sshOutput();
691
+ });
692
+
693
+ // src/services/agent.ts
694
+ async function getAgentStatus(exec) {
695
+ const result = await exec("ssh-add", ["-l"]);
696
+ if (result.code === 2 || result.code === null)
697
+ return { running: false, keys: [] };
698
+ return { running: true, keys: parseAgentList(result.stdout) };
699
+ }
700
+ async function ensureAgentRunning(exec, env) {
701
+ const status = await getAgentStatus(exec);
702
+ if (status.running)
703
+ return true;
704
+ const result = await exec("ssh-agent", ["-s"]);
705
+ if (result.code !== 0)
706
+ return false;
707
+ for (const [, name, value] of result.stdout.matchAll(/(SSH_AUTH_SOCK|SSH_AGENT_PID)=([^;]+);/g)) {
708
+ env[name] = value;
709
+ process.env[name] = value;
710
+ }
711
+ return (await getAgentStatus(exec)).running;
712
+ }
713
+ async function addKeyToAgent(exec, os, absKeyPath) {
714
+ const args = os.hasKeychain ? ["--apple-use-keychain", absKeyPath] : [absKeyPath];
715
+ const result = await exec("ssh-add", args);
716
+ if (result.code !== 0) {
717
+ throw new Error(`ssh-add failed: ${result.stderr.trim() || `exit ${result.code}`}`);
718
+ }
719
+ }
720
+ async function removeKeyFromAgent(exec, absKeyPath) {
721
+ const result = await exec("ssh-add", ["-d", absKeyPath]);
722
+ return result.code === 0;
723
+ }
724
+ var init_agent = __esm(() => {
725
+ init_sshOutput();
726
+ });
727
+
728
+ // src/core/gitconfig.ts
729
+ function buildProfileGitconfig(email, keyPath) {
730
+ return [
731
+ "[user]",
732
+ ` email = ${email}`,
733
+ "[core]",
734
+ ` sshCommand = ssh -i ${keyPath} -o IdentitiesOnly=yes`,
735
+ ""
736
+ ].join(`
737
+ `);
738
+ }
739
+ function deriveGitconfigPath(keyPath) {
740
+ return `${keyPath}.gitconfig`;
741
+ }
742
+ function includeIfKey(dir) {
743
+ const normalized = dir.endsWith("/") ? dir : `${dir}/`;
744
+ return `includeIf.gitdir:${normalized}.path`;
745
+ }
746
+
747
+ // src/services/gitconfig.ts
748
+ import { unlink as unlink2, writeFile as writeFile3 } from "node:fs/promises";
749
+ async function linkGitIdentity(exec, home, profile, dir) {
750
+ const gitconfigPath = deriveGitconfigPath(profile.keyPath);
751
+ await writeFile3(expandTilde(gitconfigPath, home), buildProfileGitconfig(profile.email, profile.keyPath));
752
+ const result = await exec("git", ["config", "--global", includeIfKey(dir), gitconfigPath]);
753
+ if (result.code !== 0) {
754
+ throw new Error(`git config failed: ${result.stderr.trim() || `exit ${result.code}`}`);
755
+ }
756
+ }
757
+ var init_gitconfig = __esm(() => {
758
+ init_paths();
759
+ });
760
+
761
+ // src/services/api.ts
762
+ async function uploadKey(provider, token, title, publicKey, fetchFn = fetch) {
763
+ if (!provider.api) {
764
+ return { ok: false, message: `${provider.label} has no key-upload API — add it manually at ${provider.settingsUrl}` };
765
+ }
766
+ const request = provider.api.buildUploadRequest(token, title, publicKey);
767
+ let url = request.url;
768
+ if (url.includes("{uuid}")) {
769
+ const who = await fetchFn("https://api.bitbucket.org/2.0/user", {
770
+ headers: { Authorization: request.headers["Authorization"] }
771
+ });
772
+ if (!who.ok) {
773
+ return { ok: false, message: `Token rejected while looking up your Bitbucket account (HTTP ${who.status})` };
774
+ }
775
+ const { uuid } = await who.json();
776
+ url = url.replace("{uuid}", encodeURIComponent(uuid));
777
+ }
778
+ const response = await fetchFn(url, { method: request.method, headers: request.headers, body: request.body });
779
+ if (response.ok) {
780
+ return { ok: true, message: `Public key uploaded to ${provider.label}` };
781
+ }
782
+ let detail = "";
783
+ try {
784
+ const body = await response.json();
785
+ detail = body.message ?? body.error?.message ?? "";
786
+ } catch {}
787
+ if (response.status === 401 || response.status === 403) {
788
+ return { ok: false, message: `Token rejected by ${provider.label} (HTTP ${response.status}) — ${provider.api.tokenHint}` };
789
+ }
790
+ if (response.status === 422 || /already in use|has already been taken/i.test(detail)) {
791
+ return { ok: true, message: `${provider.label} reports this key is already registered` };
792
+ }
793
+ return { ok: false, message: `${provider.label} upload failed (HTTP ${response.status})${detail ? `: ${detail}` : ""}` };
794
+ }
795
+
796
+ // src/commands/setup.ts
797
+ import { access } from "node:fs/promises";
798
+ import { hostname } from "node:os";
799
+ async function resolveSetupPlan(args, ctx) {
800
+ let provider = args.provider ? getProvider(args.provider) : undefined;
801
+ if (args.provider && !provider) {
802
+ throw new UsageError(`Unknown provider "${args.provider}" — expected one of: ${PROVIDERS.map((p) => p.id).join(", ")}`);
803
+ }
804
+ if (!provider) {
805
+ if (ctx.yes)
806
+ throw new UsageError("Missing <provider> (required with -y)");
807
+ provider = await ctx.promptSelect("Which git provider?", PROVIDERS.map((p) => ({ label: p.label, value: p })));
808
+ }
809
+ const manifest = await loadManifest(ctx.paths.manifest);
810
+ const existingForProvider = manifest.profiles.filter((p) => p.provider === provider.id);
811
+ let name = args.name;
812
+ if (!name) {
813
+ if (ctx.yes) {
814
+ name = "personal";
815
+ } else {
816
+ name = await ctx.promptText("Profile name for this account (e.g. personal, work)", {
817
+ defaultValue: existingForProvider.length === 0 ? "personal" : ""
818
+ });
819
+ }
820
+ }
821
+ name = sanitizeName(name);
822
+ if (!name)
823
+ throw new UsageError("Profile name must contain at least one letter or digit");
824
+ let email = args.email;
825
+ if (!email) {
826
+ if (ctx.yes)
827
+ throw new UsageError("Missing --email (required with -y)");
828
+ email = await ctx.promptText(`Email for the ${provider.label} key comment`);
829
+ }
830
+ if (!email.includes("@"))
831
+ throw new UsageError(`"${email}" does not look like an email address`);
832
+ const hasDefault = existingForProvider.some((p) => p.isDefault);
833
+ const isDefault = args.default ?? !hasDefault;
834
+ const keyType = args.keyType ?? provider.keyType;
835
+ const profileId = deriveProfileId(provider.id, name);
836
+ const token = args.token ?? (provider.api ? ctx.env[provider.api.tokenEnvVar] : undefined);
837
+ return {
838
+ provider,
839
+ name,
840
+ email,
841
+ keyType,
842
+ isDefault,
843
+ profileId,
844
+ alias: deriveAlias(provider.host, name, isDefault),
845
+ keyPath: deriveKeyPath(provider.id, name),
846
+ noBrowser: args.noBrowser,
847
+ noClipboard: args.noClipboard,
848
+ noAgent: args.noAgent,
849
+ force: args.force,
850
+ token,
851
+ dir: args.dir
852
+ };
853
+ }
854
+ async function fileExists(path) {
855
+ try {
856
+ await access(path);
857
+ return true;
858
+ } catch {
859
+ return false;
860
+ }
861
+ }
862
+ async function executeSetupPipeline(plan, ctx) {
863
+ for (const step of setupSteps) {
864
+ ctx.emit({ type: "step", id: step.id, label: step.label, status: "start" });
865
+ try {
866
+ await step.run(plan, ctx);
867
+ ctx.emit({ type: "step", id: step.id, label: step.label, status: "done" });
868
+ } catch (err) {
869
+ ctx.emit({ type: "step", id: step.id, label: step.label, status: "fail" });
870
+ throw err;
871
+ }
872
+ }
873
+ }
874
+ var setupSteps, setupCommand;
875
+ var init_setup = __esm(() => {
876
+ init_types();
877
+ init_providers();
878
+ init_manifest();
879
+ init_sshconfig();
880
+ init_sshconfigFile();
881
+ init_keygen();
882
+ init_sshTest();
883
+ init_agent();
884
+ init_gitconfig();
885
+ init_paths();
886
+ setupSteps = [
887
+ {
888
+ id: "generate-key",
889
+ label: "Generate SSH key",
890
+ async run(plan, ctx) {
891
+ const absKeyPath = expandTilde(plan.keyPath, ctx.paths.home);
892
+ if (await fileExists(absKeyPath)) {
893
+ if (!plan.force) {
894
+ const overwrite = await ctx.confirm(`${plan.keyPath} already exists — overwrite it?`);
895
+ if (!overwrite)
896
+ throw new Error(`Key ${plan.keyPath} already exists (use --force to overwrite)`);
897
+ }
898
+ const { deleteKeyPair: deleteKeyPair2 } = await Promise.resolve().then(() => (init_keygen(), exports_keygen));
899
+ await deleteKeyPair2(absKeyPath);
900
+ }
901
+ await generateKeyPair(ctx.exec, { keyType: plan.keyType, keyPath: absKeyPath, email: plan.email });
902
+ ctx.emit({ type: "info", text: `${plan.keyType} key written to ${plan.keyPath}` });
903
+ }
904
+ },
905
+ {
906
+ id: "write-ssh-config",
907
+ label: "Add host entry to ~/.ssh/config",
908
+ async run(plan, ctx) {
909
+ const segments = await loadSshConfig(ctx.paths);
910
+ const { segments: next, action } = upsertBlock(segments, {
911
+ id: plan.profileId,
912
+ alias: plan.alias,
913
+ hostName: plan.provider.host,
914
+ user: plan.provider.sshUser,
915
+ identityFile: plan.keyPath,
916
+ useKeychain: ctx.os.hasKeychain
917
+ });
918
+ await saveSshConfig(ctx.paths, next);
919
+ ctx.emit({ type: "info", text: `Host ${plan.alias} ${action} in ~/.ssh/config` });
920
+ }
921
+ },
922
+ {
923
+ id: "add-to-agent",
924
+ label: "Load key into ssh-agent",
925
+ async run(plan, ctx) {
926
+ if (plan.noAgent) {
927
+ ctx.emit({ type: "info", text: "Skipped (--no-agent)" });
928
+ return;
929
+ }
930
+ const running = await ensureAgentRunning(ctx.exec, ctx.env);
931
+ if (!running) {
932
+ ctx.emit({ type: "warn", text: "ssh-agent unavailable — key will load on first use via AddKeysToAgent" });
933
+ return;
934
+ }
935
+ try {
936
+ await addKeyToAgent(ctx.exec, ctx.os, expandTilde(plan.keyPath, ctx.paths.home));
937
+ ctx.emit({ type: "info", text: ctx.os.hasKeychain ? "Key in agent (persisted to Keychain)" : "Key in agent" });
938
+ } catch (err) {
939
+ ctx.emit({ type: "warn", text: err.message });
940
+ }
941
+ }
942
+ },
943
+ {
944
+ id: "save-profile",
945
+ label: "Record profile",
946
+ async run(plan, ctx) {
947
+ const manifest = await loadManifest(ctx.paths.manifest);
948
+ const profile = {
949
+ id: plan.profileId,
950
+ provider: plan.provider.id,
951
+ name: plan.name,
952
+ email: plan.email,
953
+ alias: plan.alias,
954
+ keyPath: plan.keyPath,
955
+ keyType: plan.keyType,
956
+ isDefault: plan.isDefault,
957
+ createdAt: new Date().toISOString(),
958
+ gitDirs: plan.dir ? [plan.dir] : []
959
+ };
960
+ await saveManifest(ctx.paths.manifest, upsertProfile(manifest, profile));
961
+ }
962
+ },
963
+ {
964
+ id: "link-gitconfig",
965
+ label: "Bind directory to this git identity",
966
+ async run(plan, ctx) {
967
+ if (!plan.dir)
968
+ return;
969
+ await linkGitIdentity(ctx.exec, ctx.paths.home, { email: plan.email, keyPath: plan.keyPath }, plan.dir);
970
+ ctx.emit({ type: "info", text: `Repos under ${plan.dir} now commit as ${plan.email} using this key` });
971
+ }
972
+ },
973
+ {
974
+ id: "deliver-pubkey",
975
+ label: "Deliver public key to provider",
976
+ async run(plan, ctx) {
977
+ const absKeyPath = expandTilde(plan.keyPath, ctx.paths.home);
978
+ const publicKey = await readPublicKey(absKeyPath);
979
+ if (plan.token && plan.provider.api) {
980
+ const title = `xpssh:${plan.profileId}@${hostname()}`;
981
+ const outcome = await uploadKey(plan.provider, plan.token, title, publicKey, ctx.fetch);
982
+ ctx.emit({ type: outcome.ok ? "success" : "warn", text: outcome.message });
983
+ if (outcome.ok) {
984
+ const manifest = await loadManifest(ctx.paths.manifest);
985
+ const profile = manifest.profiles.find((p) => p.id === plan.profileId);
986
+ if (profile) {
987
+ profile.uploaded = { via: "api", at: new Date().toISOString() };
988
+ await saveManifest(ctx.paths.manifest, manifest);
989
+ }
990
+ return;
991
+ }
992
+ }
993
+ if (!plan.noClipboard) {
994
+ const copied = await copyToClipboard(ctx.exec, ctx.os, publicKey);
995
+ ctx.emit(copied ? { type: "success", text: "Public key copied to clipboard" } : { type: "warn", text: `Clipboard unavailable — copy it yourself: cat ${plan.keyPath}.pub` });
996
+ }
997
+ if (!plan.noBrowser) {
998
+ const opened = await openInBrowser(ctx.exec, ctx.os, plan.provider.settingsUrl);
999
+ ctx.emit(opened ? { type: "info", text: `Opened ${plan.provider.settingsUrl} — paste the key there` } : { type: "warn", text: `Add the key manually at ${plan.provider.settingsUrl}` });
1000
+ } else {
1001
+ ctx.emit({ type: "info", text: `Add the key at ${plan.provider.settingsUrl}` });
1002
+ }
1003
+ }
1004
+ },
1005
+ {
1006
+ id: "test-connection",
1007
+ label: "Test SSH connection",
1008
+ async run(plan, ctx) {
1009
+ const ready = await ctx.confirm(`Key added on ${plan.provider.label}? Test the connection now?`);
1010
+ if (!ready) {
1011
+ ctx.emit({ type: "info", text: `Skipped — run \`xpssh test ${plan.profileId}\` when ready` });
1012
+ return;
1013
+ }
1014
+ const result = await testConnection(ctx.exec, plan.alias, plan.provider.sshUser);
1015
+ const manifest = await loadManifest(ctx.paths.manifest);
1016
+ const profile = manifest.profiles.find((p) => p.id === plan.profileId);
1017
+ if (profile) {
1018
+ profile.lastTest = { ok: result.ok, at: new Date().toISOString(), message: result.message };
1019
+ await saveManifest(ctx.paths.manifest, manifest);
1020
+ }
1021
+ ctx.emit(result.ok ? { type: "success", text: result.message } : { type: "warn", text: `${result.message} — run \`xpssh test ${plan.profileId}\` after adding the key` });
1022
+ }
1023
+ }
1024
+ ];
1025
+ setupCommand = defineCommand({
1026
+ name: "setup",
1027
+ summary: "Generate a key and wire up SSH for a git provider",
1028
+ usage: "xpssh setup <provider> [-n <name>] [-e <email>] [-t ed25519|rsa] [--default] [--token <tok>] [--dir <path>] [--force] [--no-browser] [--no-clipboard] [--no-agent] [-y]",
1029
+ flags: [
1030
+ { name: "name", short: "n", type: "string", description: "profile name (work, personal, ...)", valueHint: "<name>" },
1031
+ { name: "email", short: "e", type: "string", description: "email for the key comment", valueHint: "<email>" },
1032
+ { name: "type", short: "t", type: "string", description: "key algorithm (default: provider preference)", valueHint: "ed25519|rsa" },
1033
+ { name: "default", type: "boolean", description: "make this the bare-host default profile" },
1034
+ { name: "token", type: "string", description: "API token — upload the key directly, skip browser", valueHint: "<token>" },
1035
+ { name: "dir", type: "string", description: "bind a directory to this identity (git includeIf)", valueHint: "<path>" },
1036
+ { name: "force", type: "boolean", description: "overwrite an existing key file" },
1037
+ { name: "no-browser", type: "boolean", description: "don't open the provider settings page" },
1038
+ { name: "no-clipboard", type: "boolean", description: "don't copy the public key" },
1039
+ { name: "no-agent", type: "boolean", description: "don't load the key into ssh-agent" },
1040
+ { name: "yes", short: "y", type: "boolean", description: "never prompt (missing inputs become errors)" }
1041
+ ],
1042
+ parse(positionals, values) {
1043
+ if (values["type"] && values["type"] !== "ed25519" && values["type"] !== "rsa") {
1044
+ throw new UsageError(`--type must be ed25519 or rsa, got "${values["type"]}"`);
1045
+ }
1046
+ return {
1047
+ provider: positionals[0],
1048
+ name: values["name"],
1049
+ email: values["email"],
1050
+ keyType: values["type"],
1051
+ default: values["default"] === true ? true : undefined,
1052
+ token: values["token"],
1053
+ dir: values["dir"],
1054
+ force: values["force"] === true,
1055
+ noBrowser: values["no-browser"] === true,
1056
+ noClipboard: values["no-clipboard"] === true,
1057
+ noAgent: values["no-agent"] === true
1058
+ };
1059
+ },
1060
+ async run(args, ctx) {
1061
+ const plan = await resolveSetupPlan(args, ctx);
1062
+ await executeSetupPipeline(plan, ctx);
1063
+ return {
1064
+ ok: true,
1065
+ message: `Profile ${plan.profileId} ready — clone with ${clonePrefix(plan, plan.provider.sshUser)}<owner>/<repo>.git`
1066
+ };
1067
+ }
1068
+ });
1069
+ });
1070
+
1071
+ // src/commands/list.ts
1072
+ import { access as access2 } from "node:fs/promises";
1073
+ async function gatherProfileRows(paths) {
1074
+ const manifest = await loadManifest(paths.manifest);
1075
+ return Promise.all(manifest.profiles.map(async (p) => {
1076
+ let keyExists = false;
1077
+ try {
1078
+ await access2(expandTilde(p.keyPath, paths.home));
1079
+ keyExists = true;
1080
+ } catch {}
1081
+ return { ...p, keyExists, clonePrefix: clonePrefix(p) };
1082
+ }));
1083
+ }
1084
+ var listCommand;
1085
+ var init_list = __esm(() => {
1086
+ init_types();
1087
+ init_manifest();
1088
+ init_paths();
1089
+ listCommand = defineCommand({
1090
+ name: "list",
1091
+ aliases: ["ls"],
1092
+ summary: "Show all managed profiles and their status",
1093
+ usage: "xpssh list [--json]",
1094
+ flags: [{ name: "json", type: "boolean", description: "machine-readable output" }],
1095
+ parse(_positionals, values) {
1096
+ return { json: values["json"] === true };
1097
+ },
1098
+ async run(args, ctx) {
1099
+ const rows = await gatherProfileRows(ctx.paths);
1100
+ if (rows.length === 0) {
1101
+ if (args.json) {
1102
+ ctx.emit({ type: "info", text: JSON.stringify({ profiles: [] }) });
1103
+ return { ok: true };
1104
+ }
1105
+ return { ok: true, message: "No profiles yet — run `xpssh setup <provider>` to create one" };
1106
+ }
1107
+ if (args.json) {
1108
+ ctx.emit({ type: "info", text: JSON.stringify({ profiles: rows }, null, 2) });
1109
+ return { ok: true };
1110
+ }
1111
+ for (const row of rows) {
1112
+ const status = !row.keyExists ? "key missing" : row.lastTest ? row.lastTest.ok ? "tested ok" : "test failed" : "untested";
1113
+ const type = status === "tested ok" ? "success" : status === "untested" ? "info" : "warn";
1114
+ ctx.emit({
1115
+ type,
1116
+ text: `${row.id} ${row.keyType} ${row.email} [${status}]${row.isDefault ? " (default)" : ""} clone: ${row.clonePrefix}owner/repo.git`
1117
+ });
1118
+ }
1119
+ return { ok: true };
1120
+ }
1121
+ });
1122
+ });
1123
+
1124
+ // src/commands/test.ts
1125
+ var testCommand;
1126
+ var init_test = __esm(() => {
1127
+ init_types();
1128
+ init_manifest();
1129
+ init_sshTest();
1130
+ testCommand = defineCommand({
1131
+ name: "test",
1132
+ summary: "Test SSH authentication for a profile",
1133
+ usage: "xpssh test [<profile>] [--all]",
1134
+ flags: [{ name: "all", type: "boolean", description: "test every profile" }],
1135
+ parse(positionals, values) {
1136
+ return { profile: positionals[0], all: values["all"] === true };
1137
+ },
1138
+ async run(args, ctx) {
1139
+ const manifest = await loadManifest(ctx.paths.manifest);
1140
+ if (manifest.profiles.length === 0) {
1141
+ return { ok: false, message: "No profiles to test — run `xpssh setup <provider>` first" };
1142
+ }
1143
+ let targets = manifest.profiles;
1144
+ if (!args.all) {
1145
+ if (!args.profile) {
1146
+ if (manifest.profiles.length === 1) {
1147
+ targets = manifest.profiles;
1148
+ } else if (ctx.yes) {
1149
+ throw new UsageError("Multiple profiles exist — pass <profile> or --all");
1150
+ } else {
1151
+ const chosen = await ctx.promptSelect("Which profile?", manifest.profiles.map((p) => ({ label: `${p.id} (${p.email})`, value: p })));
1152
+ targets = [chosen];
1153
+ }
1154
+ } else {
1155
+ const profile = findProfile(manifest, args.profile);
1156
+ if (!profile) {
1157
+ throw new UsageError(`No profile "${args.profile}" — run \`xpssh list\` to see what exists`);
1158
+ }
1159
+ targets = [profile];
1160
+ }
1161
+ }
1162
+ let allOk = true;
1163
+ for (const profile of targets) {
1164
+ ctx.emit({ type: "step", id: profile.id, label: `ssh -T git@${profile.alias}`, status: "start" });
1165
+ const result = await testConnection(ctx.exec, profile.alias);
1166
+ profile.lastTest = { ok: result.ok, at: new Date().toISOString(), message: result.message };
1167
+ ctx.emit({ type: "step", id: profile.id, label: `ssh -T git@${profile.alias}`, status: result.ok ? "done" : "fail" });
1168
+ ctx.emit({ type: result.ok ? "success" : "error", text: `${profile.id}: ${result.message}` });
1169
+ if (!result.ok)
1170
+ allOk = false;
1171
+ }
1172
+ await saveManifest(ctx.paths.manifest, manifest);
1173
+ return allOk ? { ok: true, message: targets.length > 1 ? "All connections authenticated" : undefined } : { ok: false, message: "Some connections failed — make sure the public key is added to the provider" };
1174
+ }
1175
+ });
1176
+ });
1177
+
1178
+ // src/commands/copy.ts
1179
+ var copyCommand;
1180
+ var init_copy = __esm(() => {
1181
+ init_types();
1182
+ init_manifest();
1183
+ init_providers();
1184
+ init_keygen();
1185
+ init_paths();
1186
+ copyCommand = defineCommand({
1187
+ name: "copy",
1188
+ summary: "Copy a profile's public key to the clipboard",
1189
+ usage: "xpssh copy <profile> [--open]",
1190
+ flags: [{ name: "open", type: "boolean", description: "also open the provider's SSH settings page" }],
1191
+ parse(positionals, values) {
1192
+ return { profile: positionals[0], open: values["open"] === true };
1193
+ },
1194
+ async run(args, ctx) {
1195
+ if (!args.profile)
1196
+ throw new UsageError("Missing <profile> — run `xpssh list` to see what exists");
1197
+ const manifest = await loadManifest(ctx.paths.manifest);
1198
+ const profile = findProfile(manifest, args.profile);
1199
+ if (!profile)
1200
+ throw new UsageError(`No profile "${args.profile}" — run \`xpssh list\` to see what exists`);
1201
+ const publicKey = await readPublicKey(expandTilde(profile.keyPath, ctx.paths.home));
1202
+ const copied = await copyToClipboard(ctx.exec, ctx.os, publicKey);
1203
+ if (!copied) {
1204
+ ctx.emit({ type: "info", text: publicKey });
1205
+ return { ok: false, message: "Clipboard unavailable — public key printed above" };
1206
+ }
1207
+ if (args.open) {
1208
+ const provider = getProvider(profile.provider);
1209
+ await openInBrowser(ctx.exec, ctx.os, provider.settingsUrl);
1210
+ ctx.emit({ type: "info", text: `Opened ${provider.settingsUrl}` });
1211
+ }
1212
+ return { ok: true, message: `Public key for ${profile.id} copied to clipboard` };
1213
+ }
1214
+ });
1215
+ });
1216
+
1217
+ // src/commands/remove.ts
1218
+ var removeCommand;
1219
+ var init_remove = __esm(() => {
1220
+ init_types();
1221
+ init_manifest();
1222
+ init_sshconfig();
1223
+ init_sshconfigFile();
1224
+ init_keygen();
1225
+ init_paths();
1226
+ removeCommand = defineCommand({
1227
+ name: "remove",
1228
+ aliases: ["rm"],
1229
+ summary: "Remove a profile: ssh config block, key files, manifest entry",
1230
+ usage: "xpssh remove <profile> [--keep-key] [-y]",
1231
+ flags: [
1232
+ { name: "keep-key", type: "boolean", description: "keep the key files on disk" },
1233
+ { name: "yes", short: "y", type: "boolean", description: "skip confirmation" }
1234
+ ],
1235
+ parse(positionals, values) {
1236
+ return { profile: positionals[0], keepKey: values["keep-key"] === true };
1237
+ },
1238
+ async run(args, ctx) {
1239
+ if (!args.profile)
1240
+ throw new UsageError("Missing <profile> — run `xpssh list` to see what exists");
1241
+ const manifest = await loadManifest(ctx.paths.manifest);
1242
+ const profile = findProfile(manifest, args.profile);
1243
+ if (!profile)
1244
+ throw new UsageError(`No profile "${args.profile}" — run \`xpssh list\` to see what exists`);
1245
+ const what = args.keepKey ? "ssh config entry" : `ssh config entry and key files (${profile.keyPath})`;
1246
+ const confirmed = await ctx.confirm(`Remove ${profile.id} — ${what}?`);
1247
+ if (!confirmed)
1248
+ return { ok: false, message: "Aborted" };
1249
+ const segments = await loadSshConfig(ctx.paths);
1250
+ const { segments: next, removed } = removeBlock(segments, profile.id);
1251
+ if (removed) {
1252
+ await saveSshConfig(ctx.paths, next);
1253
+ ctx.emit({ type: "info", text: `Removed Host ${profile.alias} from ~/.ssh/config` });
1254
+ } else {
1255
+ ctx.emit({ type: "warn", text: "No managed ssh config block found (already removed?)" });
1256
+ }
1257
+ if (!args.keepKey) {
1258
+ await deleteKeyPair(expandTilde(profile.keyPath, ctx.paths.home));
1259
+ ctx.emit({ type: "info", text: `Deleted ${profile.keyPath}(.pub)` });
1260
+ }
1261
+ await saveManifest(ctx.paths.manifest, removeProfile(manifest, profile.id));
1262
+ return { ok: true, message: `Profile ${profile.id} removed` };
1263
+ }
1264
+ });
1265
+ });
1266
+
1267
+ // src/commands/agent.ts
1268
+ function isAgentSubcommand(value) {
1269
+ return AGENT_SUBCOMMANDS.includes(value);
1270
+ }
1271
+ async function requireProfile(ctx, idOrAlias, subcommand) {
1272
+ if (!idOrAlias)
1273
+ throw new UsageError(`Missing <profile> — usage: xpssh agent ${subcommand} <profile>`);
1274
+ const manifest = await loadManifest(ctx.paths.manifest);
1275
+ const profile = findProfile(manifest, idOrAlias);
1276
+ if (!profile)
1277
+ throw new UsageError(`No profile "${idOrAlias}" — run \`xpssh list\` to see what exists`);
1278
+ return profile;
1279
+ }
1280
+ var AGENT_SUBCOMMANDS, agentCommand;
1281
+ var init_agent2 = __esm(() => {
1282
+ init_types();
1283
+ init_manifest();
1284
+ init_agent();
1285
+ init_paths();
1286
+ AGENT_SUBCOMMANDS = ["list", "add", "remove", "start"];
1287
+ agentCommand = defineCommand({
1288
+ name: "agent",
1289
+ summary: "Inspect and manage keys in ssh-agent",
1290
+ usage: "xpssh agent [list|add <profile>|remove <profile>|start]",
1291
+ flags: [],
1292
+ parse(positionals) {
1293
+ const subcommand = positionals[0] ?? "list";
1294
+ if (!isAgentSubcommand(subcommand)) {
1295
+ throw new UsageError(`Unknown agent subcommand "${subcommand}" — expected one of: ${AGENT_SUBCOMMANDS.join(", ")}`);
1296
+ }
1297
+ return { subcommand, profile: positionals[1] };
1298
+ },
1299
+ async run(args, ctx) {
1300
+ switch (args.subcommand) {
1301
+ case "list": {
1302
+ const status = await getAgentStatus(ctx.exec);
1303
+ if (!status.running) {
1304
+ return { ok: true, message: "ssh-agent is not running — start it with `xpssh agent start`" };
1305
+ }
1306
+ if (status.keys.length === 0) {
1307
+ return { ok: true, message: "Agent running, no keys loaded" };
1308
+ }
1309
+ const manifest = await loadManifest(ctx.paths.manifest);
1310
+ for (const key of status.keys) {
1311
+ const owner = manifest.profiles.find((p) => p.email === key.comment);
1312
+ const line = `${key.bits} ${key.type} ${key.fingerprint} ${key.comment}`;
1313
+ ctx.emit({ type: "info", text: owner ? `${line} ← ${owner.id}` : line });
1314
+ }
1315
+ return { ok: true };
1316
+ }
1317
+ case "add": {
1318
+ const profile = await requireProfile(ctx, args.profile, "add");
1319
+ const running = await ensureAgentRunning(ctx.exec, ctx.env);
1320
+ if (!running)
1321
+ return { ok: false, message: "Could not start ssh-agent" };
1322
+ await addKeyToAgent(ctx.exec, ctx.os, expandTilde(profile.keyPath, ctx.paths.home));
1323
+ return {
1324
+ ok: true,
1325
+ message: ctx.os.hasKeychain ? `Key for ${profile.id} added to ssh-agent (persisted to Keychain)` : `Key for ${profile.id} added to ssh-agent`
1326
+ };
1327
+ }
1328
+ case "remove": {
1329
+ const profile = await requireProfile(ctx, args.profile, "remove");
1330
+ const removed = await removeKeyFromAgent(ctx.exec, expandTilde(profile.keyPath, ctx.paths.home));
1331
+ if (!removed) {
1332
+ return {
1333
+ ok: false,
1334
+ message: `Could not remove ${profile.id} from ssh-agent — the key is probably not loaded`
1335
+ };
1336
+ }
1337
+ return { ok: true, message: `Key for ${profile.id} removed from ssh-agent` };
1338
+ }
1339
+ case "start": {
1340
+ const running = await ensureAgentRunning(ctx.exec, ctx.env);
1341
+ return running ? { ok: true, message: "ssh-agent is running" } : { ok: false, message: "Failed to start ssh-agent" };
1342
+ }
1343
+ }
1344
+ }
1345
+ });
1346
+ });
1347
+
1348
+ // src/commands/upload.ts
1349
+ import { hostname as hostname2 } from "node:os";
1350
+ var uploadCommand;
1351
+ var init_upload = __esm(() => {
1352
+ init_types();
1353
+ init_manifest();
1354
+ init_providers();
1355
+ init_keygen();
1356
+ init_paths();
1357
+ uploadCommand = defineCommand({
1358
+ name: "upload",
1359
+ summary: "Upload a profile's public key via the provider API",
1360
+ usage: "xpssh upload <profile> [--token <token>]",
1361
+ flags: [{ name: "token", type: "string", description: "provider API token", valueHint: "<token>" }],
1362
+ parse(positionals, values) {
1363
+ return { profile: positionals[0], token: values["token"] };
1364
+ },
1365
+ async run(args, ctx) {
1366
+ if (!args.profile)
1367
+ throw new UsageError("Missing <profile> — run `xpssh list` to see what exists");
1368
+ const manifest = await loadManifest(ctx.paths.manifest);
1369
+ const profile = findProfile(manifest, args.profile);
1370
+ if (!profile)
1371
+ throw new UsageError(`No profile "${args.profile}" — run \`xpssh list\` to see what exists`);
1372
+ const provider = getProvider(profile.provider);
1373
+ if (!provider.api) {
1374
+ return {
1375
+ ok: false,
1376
+ message: `${provider.label} has no key-upload API — add the key manually at ${provider.settingsUrl}`
1377
+ };
1378
+ }
1379
+ let token = args.token ?? ctx.env[provider.api.tokenEnvVar];
1380
+ if (!token && !ctx.yes) {
1381
+ token = await ctx.promptSecret(`${provider.label} API token (${provider.api.tokenHint})`);
1382
+ }
1383
+ if (!token) {
1384
+ throw new UsageError(`No API token — pass --token <token> or set ${provider.api.tokenEnvVar}`);
1385
+ }
1386
+ const publicKey = await readPublicKey(expandTilde(profile.keyPath, ctx.paths.home));
1387
+ const title = `xpssh:${profile.id}@${hostname2()}`;
1388
+ const outcome = await uploadKey(provider, token, title, publicKey, ctx.fetch);
1389
+ if (!outcome.ok) {
1390
+ return { ok: false, message: outcome.message };
1391
+ }
1392
+ profile.uploaded = { via: "api", at: new Date().toISOString() };
1393
+ await saveManifest(ctx.paths.manifest, manifest);
1394
+ return { ok: true, message: outcome.message };
1395
+ }
1396
+ });
1397
+ });
1398
+
1399
+ // src/commands/doctor.ts
1400
+ import { access as access3, chmod as chmod2, stat } from "node:fs/promises";
1401
+ async function fileExists2(path) {
1402
+ try {
1403
+ await access3(path);
1404
+ return true;
1405
+ } catch {
1406
+ return false;
1407
+ }
1408
+ }
1409
+ async function looseMode(path) {
1410
+ try {
1411
+ const { mode } = await stat(path);
1412
+ return (mode & 63) !== 0 ? mode & 511 : null;
1413
+ } catch {
1414
+ return null;
1415
+ }
1416
+ }
1417
+ function shadowedByEarlierHost(segments, blockIndex, alias) {
1418
+ return segments.slice(0, blockIndex).some((segment) => segment.kind === "user" && [...segment.text.matchAll(HOST_LINE_RE)].some((match) => match[1].trim().split(/\s+/).some((pattern) => pattern === alias || pattern === "*")));
1419
+ }
1420
+ var HOST_LINE_RE, doctorCommand;
1421
+ var init_doctor = __esm(() => {
1422
+ init_types();
1423
+ init_manifest();
1424
+ init_sshconfig();
1425
+ init_sshconfigFile();
1426
+ init_providers();
1427
+ init_paths();
1428
+ HOST_LINE_RE = /^\s*Host\s+(.+)$/gm;
1429
+ doctorCommand = defineCommand({
1430
+ name: "doctor",
1431
+ summary: "Reconcile manifest, ssh config, and key files; report drift",
1432
+ usage: "xpssh doctor [--fix]",
1433
+ flags: [{ name: "fix", type: "boolean", description: "repair what can be repaired" }],
1434
+ parse(_positionals, values) {
1435
+ return { fix: values["fix"] === true };
1436
+ },
1437
+ async run(args, ctx) {
1438
+ let segments;
1439
+ try {
1440
+ segments = await loadSshConfig(ctx.paths);
1441
+ } catch (err) {
1442
+ if (err instanceof SshConfigParseError) {
1443
+ ctx.emit({ type: "error", text: err.message });
1444
+ return { ok: false, message: "~/.ssh/config has broken xpssh fences — fix manually" };
1445
+ }
1446
+ throw err;
1447
+ }
1448
+ const manifest = await loadManifest(ctx.paths.manifest);
1449
+ let found = 0;
1450
+ let fixed = 0;
1451
+ let configDirty = false;
1452
+ const report = async (text, fix) => {
1453
+ found += 1;
1454
+ ctx.emit({ type: "warn", text });
1455
+ if (args.fix && fix) {
1456
+ ctx.emit({ type: "info", text: await fix() });
1457
+ fixed += 1;
1458
+ }
1459
+ };
1460
+ const blockSpecFor = (profile) => {
1461
+ const provider = getProvider(profile.provider);
1462
+ return {
1463
+ id: profile.id,
1464
+ alias: profile.alias,
1465
+ hostName: provider.host,
1466
+ user: provider.sshUser,
1467
+ identityFile: profile.keyPath,
1468
+ useKeychain: ctx.os.hasKeychain
1469
+ };
1470
+ };
1471
+ const managedIds = new Set(listManagedIds(segments));
1472
+ for (const profile of manifest.profiles) {
1473
+ if (managedIds.has(profile.id))
1474
+ continue;
1475
+ await report(`${profile.id}: missing ssh config block`, () => {
1476
+ segments = upsertBlock(segments, blockSpecFor(profile)).segments;
1477
+ configDirty = true;
1478
+ return `${profile.id}: re-added Host ${profile.alias} to ~/.ssh/config`;
1479
+ });
1480
+ }
1481
+ const profileIds = new Set(manifest.profiles.map((p) => p.id));
1482
+ for (const id of listManagedIds(segments)) {
1483
+ if (profileIds.has(id))
1484
+ continue;
1485
+ await report(`orphan ssh config block "${id}"`, () => {
1486
+ segments = removeBlock(segments, id).segments;
1487
+ configDirty = true;
1488
+ return `removed orphan block "${id}" from ~/.ssh/config`;
1489
+ });
1490
+ }
1491
+ for (const profile of manifest.profiles) {
1492
+ const block = segments.find((s) => s.kind === "managed" && s.id === profile.id);
1493
+ if (!block)
1494
+ continue;
1495
+ const fields = parseBlockFields(block.text);
1496
+ if (fields["Host"] === profile.alias && fields["IdentityFile"] === profile.keyPath)
1497
+ continue;
1498
+ await report(`${profile.id}: ssh config block is stale (expected Host ${profile.alias}, IdentityFile ${profile.keyPath})`, () => {
1499
+ segments = upsertBlock(segments, blockSpecFor(profile)).segments;
1500
+ configDirty = true;
1501
+ return `${profile.id}: regenerated ssh config block`;
1502
+ });
1503
+ }
1504
+ for (const profile of manifest.profiles) {
1505
+ const absKeyPath = expandTilde(profile.keyPath, ctx.paths.home);
1506
+ if (await fileExists2(absKeyPath) && await fileExists2(`${absKeyPath}.pub`))
1507
+ continue;
1508
+ await report(`${profile.id}: key file missing (${profile.keyPath}) — regenerate with ` + `\`xpssh setup ${profile.provider} -n ${profile.name} --force\` or detach with ` + `\`xpssh remove ${profile.id} --keep-key\``);
1509
+ }
1510
+ if (ctx.os.platform !== "win32") {
1511
+ const sensitive = [ctx.paths.sshConfig, ...manifest.profiles.map((p) => expandTilde(p.keyPath, ctx.paths.home))];
1512
+ for (const path of sensitive) {
1513
+ const mode = await looseMode(path);
1514
+ if (mode === null)
1515
+ continue;
1516
+ const display = contractTilde(path, ctx.paths.home);
1517
+ await report(`${display}: permissions too open (${mode.toString(8)}) — should be 600`, async () => {
1518
+ await chmod2(path, 384);
1519
+ return `restored 600 permissions on ${display}`;
1520
+ });
1521
+ }
1522
+ }
1523
+ for (const profile of manifest.profiles) {
1524
+ const blockIndex = segments.findIndex((s) => s.kind === "managed" && s.id === profile.id);
1525
+ if (blockIndex === -1)
1526
+ continue;
1527
+ if (shadowedByEarlierHost(segments, blockIndex, profile.alias)) {
1528
+ await report(`${profile.id}: alias ${profile.alias} may be shadowed by an earlier Host entry`);
1529
+ }
1530
+ }
1531
+ if (configDirty)
1532
+ await saveSshConfig(ctx.paths, segments);
1533
+ if (found === 0)
1534
+ return { ok: true, message: "No issues found" };
1535
+ return {
1536
+ ok: found === fixed,
1537
+ message: `${found} issue${found === 1 ? "" : "s"} found, ${fixed} fixed`
1538
+ };
1539
+ }
1540
+ });
1541
+ });
1542
+
1543
+ // src/commands/registry.ts
1544
+ function lookupCommand(name) {
1545
+ const needle = name.toLowerCase();
1546
+ return COMMANDS.find((c) => c.name === needle || c.aliases?.includes(needle));
1547
+ }
1548
+ var COMMANDS;
1549
+ var init_registry = __esm(() => {
1550
+ init_setup();
1551
+ init_list();
1552
+ init_test();
1553
+ init_copy();
1554
+ init_remove();
1555
+ init_agent2();
1556
+ init_upload();
1557
+ init_doctor();
1558
+ COMMANDS = [
1559
+ setupCommand,
1560
+ listCommand,
1561
+ testCommand,
1562
+ copyCommand,
1563
+ removeCommand,
1564
+ agentCommand,
1565
+ uploadCommand,
1566
+ doctorCommand
1567
+ ];
1568
+ });
1569
+
1570
+ // src/cli/parse.ts
1571
+ import { parseArgs } from "node:util";
1572
+ function resolveCommand(tokens) {
1573
+ const [name, ...rest] = tokens;
1574
+ if (!name)
1575
+ throw new UsageError("No command given — try `xpssh help`");
1576
+ const def = lookupCommand(name);
1577
+ if (!def)
1578
+ throw new UsageError(`Unknown command "${name}" — try \`xpssh help\``);
1579
+ const options = {};
1580
+ for (const flag of def.flags) {
1581
+ options[flag.name] = flag.short ? { type: flag.type, short: flag.short } : { type: flag.type };
1582
+ }
1583
+ let parsed;
1584
+ try {
1585
+ parsed = parseArgs({ args: rest, options, allowPositionals: true, strict: true });
1586
+ } catch (err) {
1587
+ throw new UsageError(`${err.message.split(`
1588
+ `)[0]}
1589
+ Usage: ${def.usage}`);
1590
+ }
1591
+ const args = def.parse(parsed.positionals, parsed.values);
1592
+ return { def, args };
1593
+ }
1594
+ function hasYesFlag(tokens) {
1595
+ return tokens.includes("-y") || tokens.includes("--yes");
1596
+ }
1597
+ var init_parse = __esm(() => {
1598
+ init_types();
1599
+ init_registry();
1600
+ });
1601
+
1602
+ // src/tui/dispatch.ts
1603
+ var exports_dispatch = {};
1604
+ __export(exports_dispatch, {
1605
+ useCommandDispatch: () => useCommandDispatch,
1606
+ sessionLog: () => sessionLog
1607
+ });
1608
+ import { useCallback } from "react";
1609
+ function useCommandDispatch() {
1610
+ const { state, dispatch } = useStore();
1611
+ const refreshProfiles = useCallback(async () => {
1612
+ const rows = await gatherProfileRows(resolvePaths(process.env));
1613
+ dispatch({ type: "set-profiles", profiles: rows });
1614
+ }, [dispatch]);
1615
+ const makeContext = useCallback((yes, onEvent) => {
1616
+ const ask = (build) => new Promise((resolve) => {
1617
+ dispatch({
1618
+ type: "set-prompt",
1619
+ prompt: build((value) => {
1620
+ dispatch({ type: "set-prompt", prompt: null });
1621
+ resolve(value);
1622
+ })
1623
+ });
1624
+ });
1625
+ return {
1626
+ exec: realExec,
1627
+ fetch: globalThis.fetch,
1628
+ env: process.env,
1629
+ paths: resolvePaths(process.env),
1630
+ os: resolveOs(),
1631
+ yes,
1632
+ emit: (event) => {
1633
+ dispatch({ type: "append-event", event });
1634
+ onEvent?.(event);
1635
+ },
1636
+ confirm: (message) => yes ? Promise.resolve(true) : ask((resolve) => ({ kind: "confirm", message, resolve })),
1637
+ promptText: (message, options) => ask((resolve) => ({ kind: "text", message, defaultValue: options?.defaultValue, resolve })),
1638
+ promptSecret: (message) => ask((resolve) => ({ kind: "secret", message, resolve })),
1639
+ promptSelect: (message, choices) => ask((resolve) => ({
1640
+ kind: "select",
1641
+ message,
1642
+ choices,
1643
+ resolve
1644
+ }))
1645
+ };
1646
+ }, [dispatch]);
1647
+ const runLine = useCallback(async (line) => {
1648
+ const tokens = tokenize(line);
1649
+ if (tokens.length === 0)
1650
+ return;
1651
+ dispatch({ type: "push-history", line });
1652
+ dispatch({ type: "append-event", event: { type: "info", text: `❯ ${line}` } });
1653
+ let resolved;
1654
+ try {
1655
+ resolved = resolveCommand(tokens);
1656
+ } catch (err) {
1657
+ dispatch({ type: "append-event", event: { type: "error", text: err.message } });
1658
+ return;
1659
+ }
1660
+ if (resolved.def.name === "setup" && !hasYesFlag(tokens)) {
1661
+ dispatch({ type: "set-setup-prefill", args: resolved.args });
1662
+ dispatch({ type: "navigate", screen: "setup" });
1663
+ return;
1664
+ }
1665
+ const ctx = makeContext(hasYesFlag(tokens));
1666
+ dispatch({ type: "set-busy", busy: true });
1667
+ try {
1668
+ const result = await resolved.def.run(resolved.args, ctx);
1669
+ if (result.message) {
1670
+ dispatch({
1671
+ type: "append-event",
1672
+ event: { type: result.ok ? "success" : "error", text: result.message }
1673
+ });
1674
+ sessionLog.push(`${result.ok ? "✓" : "✗"} ${result.message}`);
1675
+ }
1676
+ } catch (err) {
1677
+ const text = err instanceof UsageError ? err.message : `✗ ${err.message}`;
1678
+ dispatch({ type: "append-event", event: { type: "error", text } });
1679
+ } finally {
1680
+ dispatch({ type: "set-busy", busy: false });
1681
+ dispatch({ type: "set-prompt", prompt: null });
1682
+ await refreshProfiles();
1683
+ }
1684
+ }, [dispatch, makeContext, refreshProfiles]);
1685
+ return { runLine, refreshProfiles, makeContext, busy: state.busy };
1686
+ }
1687
+ var sessionLog;
1688
+ var init_dispatch = __esm(() => {
1689
+ init_exec();
1690
+ init_paths();
1691
+ init_parse();
1692
+ init_list();
1693
+ init_types();
1694
+ init_store();
1695
+ sessionLog = [];
1696
+ });
1697
+
1698
+ // src/tui/components/Header.tsx
1699
+ import { Box, Text } from "ink";
1700
+ import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
1701
+ function Header() {
1702
+ const { state } = useStore();
1703
+ const tested = state.profiles.filter((p) => p.lastTest?.ok).length;
1704
+ return /* @__PURE__ */ jsxDEV2(Box, {
1705
+ paddingX: 1,
1706
+ justifyContent: "space-between",
1707
+ children: [
1708
+ /* @__PURE__ */ jsxDEV2(Text, {
1709
+ children: [
1710
+ /* @__PURE__ */ jsxDEV2(Text, {
1711
+ bold: true,
1712
+ color: "cyan",
1713
+ children: "xpssh"
1714
+ }, undefined, false, undefined, this),
1715
+ /* @__PURE__ */ jsxDEV2(Text, {
1716
+ dimColor: true,
1717
+ children: [
1718
+ " v",
1719
+ VERSION
1720
+ ]
1721
+ }, undefined, true, undefined, this)
1722
+ ]
1723
+ }, undefined, true, undefined, this),
1724
+ /* @__PURE__ */ jsxDEV2(Text, {
1725
+ dimColor: true,
1726
+ children: [
1727
+ state.profiles.length,
1728
+ " profile",
1729
+ state.profiles.length === 1 ? "" : "s",
1730
+ " · ",
1731
+ tested,
1732
+ " tested"
1733
+ ]
1734
+ }, undefined, true, undefined, this)
1735
+ ]
1736
+ }, undefined, true, undefined, this);
1737
+ }
1738
+ var init_Header = __esm(() => {
1739
+ init_version();
1740
+ init_store();
1741
+ });
1742
+
1743
+ // src/tui/components/Transcript.tsx
1744
+ import { Box as Box2, Text as Text2 } from "ink";
1745
+ import { Spinner } from "@inkjs/ui";
1746
+ import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
1747
+ function EventLine({ event }) {
1748
+ if (event.type === "step") {
1749
+ const icon = event.status === "done" ? "✓" : event.status === "fail" ? "✗" : "·";
1750
+ const color2 = event.status === "done" ? "green" : event.status === "fail" ? "red" : undefined;
1751
+ return /* @__PURE__ */ jsxDEV3(Text2, {
1752
+ color: color2,
1753
+ dimColor: event.status === "start",
1754
+ children: [
1755
+ icon,
1756
+ " ",
1757
+ event.label
1758
+ ]
1759
+ }, undefined, true, undefined, this);
1760
+ }
1761
+ const prefix = { success: "✓", error: "✗", warn: "!", info: " " }[event.type];
1762
+ const color = { success: "green", error: "red", warn: "yellow", info: undefined }[event.type];
1763
+ return /* @__PURE__ */ jsxDEV3(Text2, {
1764
+ color,
1765
+ children: [
1766
+ prefix,
1767
+ " ",
1768
+ event.text
1769
+ ]
1770
+ }, undefined, true, undefined, this);
1771
+ }
1772
+ function Transcript() {
1773
+ const { state } = useStore();
1774
+ const events = state.transcript.slice(-VISIBLE);
1775
+ if (events.length === 0 && !state.busy)
1776
+ return null;
1777
+ return /* @__PURE__ */ jsxDEV3(Box2, {
1778
+ flexDirection: "column",
1779
+ paddingX: 1,
1780
+ borderStyle: "round",
1781
+ borderDimColor: true,
1782
+ children: [
1783
+ events.map((event, i) => /* @__PURE__ */ jsxDEV3(EventLine, {
1784
+ event
1785
+ }, state.transcript.length - events.length + i, false, undefined, this)),
1786
+ state.busy && /* @__PURE__ */ jsxDEV3(Spinner, {
1787
+ label: "working…"
1788
+ }, undefined, false, undefined, this)
1789
+ ]
1790
+ }, undefined, true, undefined, this);
1791
+ }
1792
+ var VISIBLE = 6;
1793
+ var init_Transcript = __esm(() => {
1794
+ init_store();
1795
+ });
1796
+
1797
+ // src/tui/components/CommandBar.tsx
1798
+ import { useState } from "react";
1799
+ import { Box as Box3, Text as Text3, useInput } from "ink";
1800
+ import TextInput from "ink-text-input";
1801
+ import { jsxDEV as jsxDEV4 } from "react/jsx-dev-runtime";
1802
+ function CommandBar() {
1803
+ const { state, dispatch } = useStore();
1804
+ const { runLine } = useCommandDispatch();
1805
+ const [value, setValue] = useState("");
1806
+ const [historyIndex, setHistoryIndex] = useState(null);
1807
+ const focused = state.focusZone === "bar" && !state.prompt;
1808
+ useInput((_input, key) => {
1809
+ if (key.escape) {
1810
+ setValue("");
1811
+ setHistoryIndex(null);
1812
+ dispatch({ type: "set-focus", zone: "screen" });
1813
+ return;
1814
+ }
1815
+ if (key.upArrow && state.history.length > 0) {
1816
+ const next = historyIndex === null ? state.history.length - 1 : Math.max(0, historyIndex - 1);
1817
+ setHistoryIndex(next);
1818
+ setValue(state.history[next] ?? "");
1819
+ }
1820
+ if (key.downArrow && historyIndex !== null) {
1821
+ const next = historyIndex + 1;
1822
+ if (next >= state.history.length) {
1823
+ setHistoryIndex(null);
1824
+ setValue("");
1825
+ } else {
1826
+ setHistoryIndex(next);
1827
+ setValue(state.history[next] ?? "");
1828
+ }
1829
+ }
1830
+ }, { isActive: focused });
1831
+ return /* @__PURE__ */ jsxDEV4(Box3, {
1832
+ borderStyle: "round",
1833
+ paddingX: 1,
1834
+ borderColor: focused ? "cyan" : undefined,
1835
+ borderDimColor: !focused,
1836
+ children: [
1837
+ /* @__PURE__ */ jsxDEV4(Text3, {
1838
+ color: focused ? "cyan" : "gray",
1839
+ children: "❯ "
1840
+ }, undefined, false, undefined, this),
1841
+ /* @__PURE__ */ jsxDEV4(TextInput, {
1842
+ value,
1843
+ onChange: (next) => {
1844
+ setValue(next);
1845
+ setHistoryIndex(null);
1846
+ },
1847
+ onSubmit: (line) => {
1848
+ setValue("");
1849
+ setHistoryIndex(null);
1850
+ dispatch({ type: "set-focus", zone: "screen" });
1851
+ runLine(line);
1852
+ },
1853
+ focus: focused,
1854
+ placeholder: focused ? "setup github -e you@example.com" : "press / to type a command",
1855
+ showCursor: focused
1856
+ }, undefined, false, undefined, this)
1857
+ ]
1858
+ }, undefined, true, undefined, this);
1859
+ }
1860
+ var init_CommandBar = __esm(() => {
1861
+ init_store();
1862
+ init_dispatch();
1863
+ });
1864
+
1865
+ // src/tui/components/PromptOverlay.tsx
1866
+ import { useState as useState2 } from "react";
1867
+ import { Box as Box4, Text as Text4 } from "ink";
1868
+ import { ConfirmInput, Select } from "@inkjs/ui";
1869
+ import TextInput2 from "ink-text-input";
1870
+ import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
1871
+ function PromptOverlay() {
1872
+ const { state } = useStore();
1873
+ const prompt = state.prompt;
1874
+ const [text, setText] = useState2("");
1875
+ if (!prompt)
1876
+ return null;
1877
+ return /* @__PURE__ */ jsxDEV5(Box4, {
1878
+ flexDirection: "column",
1879
+ borderStyle: "round",
1880
+ borderColor: "yellow",
1881
+ paddingX: 1,
1882
+ children: [
1883
+ /* @__PURE__ */ jsxDEV5(Text4, {
1884
+ bold: true,
1885
+ children: prompt.message
1886
+ }, undefined, false, undefined, this),
1887
+ prompt.kind === "confirm" && /* @__PURE__ */ jsxDEV5(ConfirmInput, {
1888
+ onConfirm: () => prompt.resolve(true),
1889
+ onCancel: () => prompt.resolve(false)
1890
+ }, undefined, false, undefined, this),
1891
+ (prompt.kind === "text" || prompt.kind === "secret") && /* @__PURE__ */ jsxDEV5(Box4, {
1892
+ children: [
1893
+ /* @__PURE__ */ jsxDEV5(Text4, {
1894
+ color: "yellow",
1895
+ children: "→ "
1896
+ }, undefined, false, undefined, this),
1897
+ /* @__PURE__ */ jsxDEV5(TextInput2, {
1898
+ value: text,
1899
+ onChange: setText,
1900
+ onSubmit: (answer) => {
1901
+ setText("");
1902
+ prompt.resolve(answer || ("defaultValue" in prompt ? prompt.defaultValue ?? "" : ""));
1903
+ },
1904
+ mask: prompt.kind === "secret" ? "*" : undefined,
1905
+ placeholder: "defaultValue" in prompt ? prompt.defaultValue : undefined,
1906
+ focus: true
1907
+ }, undefined, false, undefined, this)
1908
+ ]
1909
+ }, undefined, true, undefined, this),
1910
+ prompt.kind === "select" && /* @__PURE__ */ jsxDEV5(Select, {
1911
+ options: prompt.choices.map((choice, i) => ({ label: choice.label, value: String(i) })),
1912
+ onChange: (index) => prompt.resolve(prompt.choices[Number(index)].value)
1913
+ }, undefined, false, undefined, this)
1914
+ ]
1915
+ }, undefined, true, undefined, this);
1916
+ }
1917
+ var init_PromptOverlay = __esm(() => {
1918
+ init_store();
1919
+ });
1920
+
1921
+ // src/tui/components/ProfileRow.tsx
1922
+ import { Box as Box5, Text as Text5 } from "ink";
1923
+ import { Badge } from "@inkjs/ui";
1924
+ import { jsxDEV as jsxDEV6 } from "react/jsx-dev-runtime";
1925
+ function ProfileRow({ profile }) {
1926
+ const status = !profile.keyExists ? { color: "red", label: "key missing" } : profile.lastTest ? profile.lastTest.ok ? { color: "green", label: "tested" } : { color: "red", label: "failed" } : { color: "yellow", label: "untested" };
1927
+ return /* @__PURE__ */ jsxDEV6(Box5, {
1928
+ gap: 1,
1929
+ children: [
1930
+ /* @__PURE__ */ jsxDEV6(Box5, {
1931
+ width: 22,
1932
+ children: [
1933
+ /* @__PURE__ */ jsxDEV6(Text5, {
1934
+ bold: true,
1935
+ children: profile.id
1936
+ }, undefined, false, undefined, this),
1937
+ profile.isDefault && /* @__PURE__ */ jsxDEV6(Text5, {
1938
+ dimColor: true,
1939
+ children: " ★"
1940
+ }, undefined, false, undefined, this)
1941
+ ]
1942
+ }, undefined, true, undefined, this),
1943
+ /* @__PURE__ */ jsxDEV6(Box5, {
1944
+ width: 10,
1945
+ children: /* @__PURE__ */ jsxDEV6(Badge, {
1946
+ color: status.color,
1947
+ children: status.label
1948
+ }, undefined, false, undefined, this)
1949
+ }, undefined, false, undefined, this),
1950
+ /* @__PURE__ */ jsxDEV6(Box5, {
1951
+ width: 26,
1952
+ children: /* @__PURE__ */ jsxDEV6(Text5, {
1953
+ dimColor: true,
1954
+ children: profile.email
1955
+ }, undefined, false, undefined, this)
1956
+ }, undefined, false, undefined, this),
1957
+ /* @__PURE__ */ jsxDEV6(Text5, {
1958
+ dimColor: true,
1959
+ children: [
1960
+ profile.clonePrefix,
1961
+ "owner/repo.git"
1962
+ ]
1963
+ }, undefined, true, undefined, this)
1964
+ ]
1965
+ }, undefined, true, undefined, this);
1966
+ }
1967
+ var init_ProfileRow = () => {};
1968
+
1969
+ // src/tui/screens/Dashboard.tsx
1970
+ import { Box as Box6, Text as Text6 } from "ink";
1971
+ import { Select as Select2 } from "@inkjs/ui";
1972
+ import { jsxDEV as jsxDEV7 } from "react/jsx-dev-runtime";
1973
+ function Dashboard() {
1974
+ const { state, dispatch } = useStore();
1975
+ const { runLine } = useCommandDispatch();
1976
+ const active = state.focusZone === "screen" && !state.prompt && !state.busy;
1977
+ return /* @__PURE__ */ jsxDEV7(Box6, {
1978
+ flexDirection: "column",
1979
+ gap: 1,
1980
+ paddingX: 1,
1981
+ children: [
1982
+ state.profiles.length > 0 ? /* @__PURE__ */ jsxDEV7(Box6, {
1983
+ flexDirection: "column",
1984
+ children: state.profiles.map((profile) => /* @__PURE__ */ jsxDEV7(ProfileRow, {
1985
+ profile
1986
+ }, profile.id, false, undefined, this))
1987
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV7(Text6, {
1988
+ dimColor: true,
1989
+ children: "No SSH profiles yet — set one up below."
1990
+ }, undefined, false, undefined, this),
1991
+ /* @__PURE__ */ jsxDEV7(Select2, {
1992
+ isDisabled: !active,
1993
+ options: [
1994
+ { label: "➕ Setup a new SSH key", value: "screen:setup" },
1995
+ { label: "\uD83D\uDDC2 Manage keys", value: "screen:keys" },
1996
+ { label: "\uD83D\uDD0C Test connections", value: "screen:test" },
1997
+ { label: "\uD83D\uDD11 SSH agent", value: "screen:agent" },
1998
+ { label: "❓ Help", value: "screen:help" }
1999
+ ],
2000
+ onChange: (value) => {
2001
+ if (value.startsWith("screen:")) {
2002
+ dispatch({ type: "navigate", screen: value.slice("screen:".length) });
2003
+ } else {
2004
+ runLine(value);
2005
+ }
2006
+ }
2007
+ }, undefined, false, undefined, this)
2008
+ ]
2009
+ }, undefined, true, undefined, this);
2010
+ }
2011
+ var init_Dashboard = __esm(() => {
2012
+ init_store();
2013
+ init_dispatch();
2014
+ init_ProfileRow();
2015
+ });
2016
+
2017
+ // src/tui/screens/KeyManager.tsx
2018
+ import { useState as useState3 } from "react";
2019
+ import { Box as Box7, Text as Text7, useInput as useInput2 } from "ink";
2020
+ import { Select as Select3 } from "@inkjs/ui";
2021
+ import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
2022
+ function KeyManager() {
2023
+ const { state, dispatch } = useStore();
2024
+ const { runLine } = useCommandDispatch();
2025
+ const [selected, setSelected] = useState3(null);
2026
+ const active = state.focusZone === "screen" && !state.prompt && !state.busy;
2027
+ useInput2((_input, key) => {
2028
+ if (key.escape) {
2029
+ if (selected)
2030
+ setSelected(null);
2031
+ else
2032
+ dispatch({ type: "navigate", screen: "dashboard" });
2033
+ }
2034
+ }, { isActive: active });
2035
+ if (state.profiles.length === 0) {
2036
+ return /* @__PURE__ */ jsxDEV8(Box7, {
2037
+ paddingX: 1,
2038
+ children: /* @__PURE__ */ jsxDEV8(Text7, {
2039
+ dimColor: true,
2040
+ children: "No profiles to manage — run setup first (esc to go back)."
2041
+ }, undefined, false, undefined, this)
2042
+ }, undefined, false, undefined, this);
2043
+ }
2044
+ if (!selected) {
2045
+ return /* @__PURE__ */ jsxDEV8(Box7, {
2046
+ flexDirection: "column",
2047
+ paddingX: 1,
2048
+ gap: 1,
2049
+ children: [
2050
+ /* @__PURE__ */ jsxDEV8(Text7, {
2051
+ bold: true,
2052
+ children: "Pick a profile"
2053
+ }, undefined, false, undefined, this),
2054
+ /* @__PURE__ */ jsxDEV8(Select3, {
2055
+ isDisabled: !active,
2056
+ options: state.profiles.map((p) => ({ label: `${p.id} (${p.email})`, value: p.id })),
2057
+ onChange: (id) => setSelected(id)
2058
+ }, undefined, false, undefined, this)
2059
+ ]
2060
+ }, undefined, true, undefined, this);
2061
+ }
2062
+ return /* @__PURE__ */ jsxDEV8(Box7, {
2063
+ flexDirection: "column",
2064
+ paddingX: 1,
2065
+ gap: 1,
2066
+ children: [
2067
+ /* @__PURE__ */ jsxDEV8(Text7, {
2068
+ bold: true,
2069
+ children: selected
2070
+ }, undefined, false, undefined, this),
2071
+ /* @__PURE__ */ jsxDEV8(Select3, {
2072
+ isDisabled: !active,
2073
+ options: [
2074
+ { label: "\uD83D\uDCCB Copy public key", value: `copy ${selected}` },
2075
+ { label: "\uD83C\uDF10 Copy + open provider settings", value: `copy ${selected} --open` },
2076
+ { label: "\uD83D\uDD0C Test connection", value: `test ${selected}` },
2077
+ { label: "\uD83D\uDDD1 Remove profile", value: `remove ${selected}` }
2078
+ ],
2079
+ onChange: (line) => {
2080
+ setSelected(null);
2081
+ runLine(line);
2082
+ }
2083
+ }, undefined, false, undefined, this),
2084
+ /* @__PURE__ */ jsxDEV8(Text7, {
2085
+ dimColor: true,
2086
+ children: "esc to go back"
2087
+ }, undefined, false, undefined, this)
2088
+ ]
2089
+ }, undefined, true, undefined, this);
2090
+ }
2091
+ var init_KeyManager = __esm(() => {
2092
+ init_store();
2093
+ init_dispatch();
2094
+ });
2095
+
2096
+ // src/tui/components/StepList.tsx
2097
+ import { Box as Box8, Text as Text8 } from "ink";
2098
+ import { Spinner as Spinner2 } from "@inkjs/ui";
2099
+ import { jsxDEV as jsxDEV9 } from "react/jsx-dev-runtime";
2100
+ function StepList({ steps }) {
2101
+ return /* @__PURE__ */ jsxDEV9(Box8, {
2102
+ flexDirection: "column",
2103
+ children: steps.map((step) => /* @__PURE__ */ jsxDEV9(Box8, {
2104
+ gap: 1,
2105
+ children: step.status === "running" ? /* @__PURE__ */ jsxDEV9(Spinner2, {
2106
+ label: step.label
2107
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV9(Text8, {
2108
+ color: step.status === "done" ? "green" : step.status === "failed" ? "red" : undefined,
2109
+ dimColor: step.status === "pending",
2110
+ children: [
2111
+ step.status === "done" ? "✓" : step.status === "failed" ? "✗" : "○",
2112
+ " ",
2113
+ step.label
2114
+ ]
2115
+ }, undefined, true, undefined, this)
2116
+ }, step.id, false, undefined, this))
2117
+ }, undefined, false, undefined, this);
2118
+ }
2119
+ var init_StepList = () => {};
2120
+
2121
+ // src/tui/screens/SetupWizard.tsx
2122
+ import { useMemo, useState as useState4 } from "react";
2123
+ import { Box as Box9, Text as Text9, useInput as useInput3 } from "ink";
2124
+ import { Select as Select4 } from "@inkjs/ui";
2125
+ import TextInput3 from "ink-text-input";
2126
+ import { jsxDEV as jsxDEV10 } from "react/jsx-dev-runtime";
2127
+ function firstStage(args) {
2128
+ if (!args.provider || !getProvider(args.provider))
2129
+ return "provider";
2130
+ if (!args.name)
2131
+ return "name";
2132
+ if (!args.email)
2133
+ return "email";
2134
+ return "review";
2135
+ }
2136
+ function SetupWizard() {
2137
+ const { state, dispatch } = useStore();
2138
+ const { makeContext, refreshProfiles } = useCommandDispatch();
2139
+ const initialArgs = state.setupPrefill ?? EMPTY_ARGS;
2140
+ const [args, setArgs] = useState4(initialArgs);
2141
+ const [stage, setStage] = useState4(() => firstStage(initialArgs));
2142
+ const [text, setText] = useState4("");
2143
+ const [steps, setSteps] = useState4([]);
2144
+ const [plan, setPlan] = useState4(null);
2145
+ const [error, setError] = useState4(null);
2146
+ const interactive = state.focusZone === "screen" && !state.prompt && !state.busy;
2147
+ const provider = args.provider ? getProvider(args.provider) : undefined;
2148
+ useInput3((_input, key) => {
2149
+ if (!key.escape)
2150
+ return;
2151
+ if (stage === "running")
2152
+ return;
2153
+ dispatch({ type: "set-setup-prefill", args: null });
2154
+ dispatch({ type: "navigate", screen: "dashboard" });
2155
+ }, { isActive: interactive });
2156
+ const stepRows = useMemo(() => setupSteps.map((s) => ({ id: s.id, label: s.label, status: "pending" })), []);
2157
+ async function start() {
2158
+ setStage("running");
2159
+ setSteps(stepRows);
2160
+ setError(null);
2161
+ const ctx = makeContext(false, (event) => {
2162
+ if (event.type !== "step")
2163
+ return;
2164
+ setSteps((current) => current.map((row) => row.id === event.id ? { ...row, status: event.status === "start" ? "running" : event.status === "done" ? "done" : "failed" } : row));
2165
+ });
2166
+ dispatch({ type: "set-busy", busy: true });
2167
+ try {
2168
+ const resolved = await resolveSetupPlan(args, ctx);
2169
+ setPlan(resolved);
2170
+ await executeSetupPipeline(resolved, ctx);
2171
+ } catch (err) {
2172
+ setError(err.message);
2173
+ } finally {
2174
+ dispatch({ type: "set-busy", busy: false });
2175
+ dispatch({ type: "set-prompt", prompt: null });
2176
+ await refreshProfiles();
2177
+ setStage("finished");
2178
+ }
2179
+ }
2180
+ return /* @__PURE__ */ jsxDEV10(Box9, {
2181
+ flexDirection: "column",
2182
+ paddingX: 1,
2183
+ gap: 1,
2184
+ children: [
2185
+ /* @__PURE__ */ jsxDEV10(Text9, {
2186
+ bold: true,
2187
+ children: [
2188
+ "Set up a new SSH key",
2189
+ provider ? ` — ${provider.label}` : ""
2190
+ ]
2191
+ }, undefined, true, undefined, this),
2192
+ stage === "provider" && /* @__PURE__ */ jsxDEV10(Select4, {
2193
+ isDisabled: !interactive,
2194
+ options: PROVIDERS.map((p) => ({ label: p.label, value: p.id })),
2195
+ onChange: (id) => {
2196
+ setArgs((a) => ({ ...a, provider: id }));
2197
+ setStage(firstStage({ ...args, provider: id }));
2198
+ }
2199
+ }, undefined, false, undefined, this),
2200
+ stage === "name" && /* @__PURE__ */ jsxDEV10(Box9, {
2201
+ flexDirection: "column",
2202
+ children: [
2203
+ /* @__PURE__ */ jsxDEV10(Text9, {
2204
+ children: "Profile name for this account (e.g. personal, work)"
2205
+ }, undefined, false, undefined, this),
2206
+ /* @__PURE__ */ jsxDEV10(Box9, {
2207
+ children: [
2208
+ /* @__PURE__ */ jsxDEV10(Text9, {
2209
+ color: "cyan",
2210
+ children: "→ "
2211
+ }, undefined, false, undefined, this),
2212
+ /* @__PURE__ */ jsxDEV10(TextInput3, {
2213
+ value: text,
2214
+ onChange: setText,
2215
+ focus: interactive,
2216
+ placeholder: "personal",
2217
+ onSubmit: (value) => {
2218
+ const name = value.trim() || "personal";
2219
+ setText("");
2220
+ setArgs((a) => ({ ...a, name }));
2221
+ setStage(firstStage({ ...args, name }));
2222
+ }
2223
+ }, undefined, false, undefined, this)
2224
+ ]
2225
+ }, undefined, true, undefined, this)
2226
+ ]
2227
+ }, undefined, true, undefined, this),
2228
+ stage === "email" && /* @__PURE__ */ jsxDEV10(Box9, {
2229
+ flexDirection: "column",
2230
+ children: [
2231
+ /* @__PURE__ */ jsxDEV10(Text9, {
2232
+ children: "Email for the key comment"
2233
+ }, undefined, false, undefined, this),
2234
+ /* @__PURE__ */ jsxDEV10(Box9, {
2235
+ children: [
2236
+ /* @__PURE__ */ jsxDEV10(Text9, {
2237
+ color: "cyan",
2238
+ children: "→ "
2239
+ }, undefined, false, undefined, this),
2240
+ /* @__PURE__ */ jsxDEV10(TextInput3, {
2241
+ value: text,
2242
+ onChange: setText,
2243
+ focus: interactive,
2244
+ placeholder: "you@example.com",
2245
+ onSubmit: (value) => {
2246
+ if (!value.includes("@"))
2247
+ return;
2248
+ setText("");
2249
+ setArgs((a) => ({ ...a, email: value.trim() }));
2250
+ setStage("delivery");
2251
+ }
2252
+ }, undefined, false, undefined, this)
2253
+ ]
2254
+ }, undefined, true, undefined, this)
2255
+ ]
2256
+ }, undefined, true, undefined, this),
2257
+ stage === "delivery" && /* @__PURE__ */ jsxDEV10(Box9, {
2258
+ flexDirection: "column",
2259
+ children: [
2260
+ /* @__PURE__ */ jsxDEV10(Text9, {
2261
+ children: [
2262
+ "How should the public key reach ",
2263
+ provider?.label,
2264
+ "?"
2265
+ ]
2266
+ }, undefined, true, undefined, this),
2267
+ /* @__PURE__ */ jsxDEV10(Select4, {
2268
+ isDisabled: !interactive,
2269
+ options: [
2270
+ { label: "Clipboard + open settings page (manual paste)", value: "manual" },
2271
+ ...provider?.api ? [{ label: `Upload via API token (${provider.api.tokenEnvVar})`, value: "token" }] : []
2272
+ ],
2273
+ onChange: (choice) => {
2274
+ if (choice === "token") {
2275
+ setStage("review");
2276
+ setArgs((a) => ({ ...a, token: a.token ?? process.env[provider.api.tokenEnvVar] }));
2277
+ } else {
2278
+ setStage("review");
2279
+ }
2280
+ }
2281
+ }, undefined, false, undefined, this)
2282
+ ]
2283
+ }, undefined, true, undefined, this),
2284
+ stage === "review" && /* @__PURE__ */ jsxDEV10(Box9, {
2285
+ flexDirection: "column",
2286
+ gap: 1,
2287
+ children: [
2288
+ /* @__PURE__ */ jsxDEV10(Box9, {
2289
+ flexDirection: "column",
2290
+ children: [
2291
+ /* @__PURE__ */ jsxDEV10(Text9, {
2292
+ children: [
2293
+ "provider ",
2294
+ /* @__PURE__ */ jsxDEV10(Text9, {
2295
+ bold: true,
2296
+ children: provider?.label
2297
+ }, undefined, false, undefined, this),
2298
+ " · profile ",
2299
+ /* @__PURE__ */ jsxDEV10(Text9, {
2300
+ bold: true,
2301
+ children: args.name
2302
+ }, undefined, false, undefined, this),
2303
+ " · email",
2304
+ " ",
2305
+ /* @__PURE__ */ jsxDEV10(Text9, {
2306
+ bold: true,
2307
+ children: args.email
2308
+ }, undefined, false, undefined, this)
2309
+ ]
2310
+ }, undefined, true, undefined, this),
2311
+ /* @__PURE__ */ jsxDEV10(Text9, {
2312
+ dimColor: true,
2313
+ children: [
2314
+ "key ",
2315
+ args.keyType ?? provider?.keyType,
2316
+ " · delivery ",
2317
+ args.token ? "API upload" : "clipboard + browser"
2318
+ ]
2319
+ }, undefined, true, undefined, this)
2320
+ ]
2321
+ }, undefined, true, undefined, this),
2322
+ /* @__PURE__ */ jsxDEV10(Select4, {
2323
+ isDisabled: !interactive,
2324
+ options: [
2325
+ { label: "▶ Create the key", value: "go" },
2326
+ { label: "Cancel", value: "cancel" }
2327
+ ],
2328
+ onChange: (choice) => {
2329
+ if (choice === "go")
2330
+ start();
2331
+ else {
2332
+ dispatch({ type: "set-setup-prefill", args: null });
2333
+ dispatch({ type: "navigate", screen: "dashboard" });
2334
+ }
2335
+ }
2336
+ }, undefined, false, undefined, this)
2337
+ ]
2338
+ }, undefined, true, undefined, this),
2339
+ (stage === "running" || stage === "finished") && /* @__PURE__ */ jsxDEV10(Box9, {
2340
+ flexDirection: "column",
2341
+ gap: 1,
2342
+ children: [
2343
+ /* @__PURE__ */ jsxDEV10(StepList, {
2344
+ steps
2345
+ }, undefined, false, undefined, this),
2346
+ error && /* @__PURE__ */ jsxDEV10(Text9, {
2347
+ color: "red",
2348
+ children: [
2349
+ "✗ ",
2350
+ error
2351
+ ]
2352
+ }, undefined, true, undefined, this),
2353
+ stage === "finished" && !error && plan && /* @__PURE__ */ jsxDEV10(Text9, {
2354
+ color: "green",
2355
+ children: [
2356
+ "✓ Clone with ",
2357
+ clonePrefix(plan, plan.provider.sshUser),
2358
+ "<owner>/<repo>.git"
2359
+ ]
2360
+ }, undefined, true, undefined, this),
2361
+ stage === "finished" && /* @__PURE__ */ jsxDEV10(Select4, {
2362
+ isDisabled: !interactive,
2363
+ options: [
2364
+ { label: "Back to dashboard", value: "dashboard" },
2365
+ ...plan && error ? [{ label: "Retry", value: "retry" }] : []
2366
+ ],
2367
+ onChange: (choice) => {
2368
+ if (choice === "retry")
2369
+ start();
2370
+ else {
2371
+ dispatch({ type: "set-setup-prefill", args: null });
2372
+ dispatch({ type: "navigate", screen: "dashboard" });
2373
+ }
2374
+ }
2375
+ }, undefined, false, undefined, this)
2376
+ ]
2377
+ }, undefined, true, undefined, this),
2378
+ stage !== "running" && stage !== "finished" && /* @__PURE__ */ jsxDEV10(Text9, {
2379
+ dimColor: true,
2380
+ children: "esc to cancel"
2381
+ }, undefined, false, undefined, this)
2382
+ ]
2383
+ }, undefined, true, undefined, this);
2384
+ }
2385
+ var EMPTY_ARGS;
2386
+ var init_SetupWizard = __esm(() => {
2387
+ init_providers();
2388
+ init_setup();
2389
+ init_store();
2390
+ init_dispatch();
2391
+ init_StepList();
2392
+ EMPTY_ARGS = { noBrowser: false, noClipboard: false, noAgent: false, force: false };
2393
+ });
2394
+
2395
+ // src/tui/screens/Tester.tsx
2396
+ import { useState as useState5 } from "react";
2397
+ import { Box as Box10, Text as Text10, useInput as useInput4 } from "ink";
2398
+ import { MultiSelect, Badge as Badge2 } from "@inkjs/ui";
2399
+ import { jsxDEV as jsxDEV11 } from "react/jsx-dev-runtime";
2400
+ function Tester() {
2401
+ const { state, dispatch } = useStore();
2402
+ const { runLine } = useCommandDispatch();
2403
+ const [running, setRunning] = useState5(false);
2404
+ const active = state.focusZone === "screen" && !state.prompt && !state.busy && !running;
2405
+ useInput4((_input, key) => {
2406
+ if (key.escape)
2407
+ dispatch({ type: "navigate", screen: "dashboard" });
2408
+ }, { isActive: active });
2409
+ if (state.profiles.length === 0) {
2410
+ return /* @__PURE__ */ jsxDEV11(Box10, {
2411
+ paddingX: 1,
2412
+ children: /* @__PURE__ */ jsxDEV11(Text10, {
2413
+ dimColor: true,
2414
+ children: "No profiles to test — run setup first (esc to go back)."
2415
+ }, undefined, false, undefined, this)
2416
+ }, undefined, false, undefined, this);
2417
+ }
2418
+ return /* @__PURE__ */ jsxDEV11(Box10, {
2419
+ flexDirection: "column",
2420
+ paddingX: 1,
2421
+ gap: 1,
2422
+ children: [
2423
+ /* @__PURE__ */ jsxDEV11(Text10, {
2424
+ bold: true,
2425
+ children: "Select profiles to test (space toggles, enter runs)"
2426
+ }, undefined, false, undefined, this),
2427
+ /* @__PURE__ */ jsxDEV11(MultiSelect, {
2428
+ isDisabled: !active,
2429
+ options: state.profiles.map((p) => ({ label: `${p.id} (${p.alias})`, value: p.id })),
2430
+ defaultValue: state.profiles.map((p) => p.id),
2431
+ onSubmit: async (ids) => {
2432
+ setRunning(true);
2433
+ for (const id of ids) {
2434
+ await runLine(`test ${id}`);
2435
+ }
2436
+ setRunning(false);
2437
+ }
2438
+ }, undefined, false, undefined, this),
2439
+ /* @__PURE__ */ jsxDEV11(Box10, {
2440
+ flexDirection: "column",
2441
+ children: state.profiles.filter((p) => p.lastTest).map((p) => /* @__PURE__ */ jsxDEV11(Box10, {
2442
+ gap: 1,
2443
+ children: [
2444
+ /* @__PURE__ */ jsxDEV11(Badge2, {
2445
+ color: p.lastTest.ok ? "green" : "red",
2446
+ children: p.lastTest.ok ? "ok" : "fail"
2447
+ }, undefined, false, undefined, this),
2448
+ /* @__PURE__ */ jsxDEV11(Text10, {
2449
+ children: p.id
2450
+ }, undefined, false, undefined, this),
2451
+ /* @__PURE__ */ jsxDEV11(Text10, {
2452
+ dimColor: true,
2453
+ children: p.lastTest.message
2454
+ }, undefined, false, undefined, this)
2455
+ ]
2456
+ }, p.id, true, undefined, this))
2457
+ }, undefined, false, undefined, this),
2458
+ /* @__PURE__ */ jsxDEV11(Text10, {
2459
+ dimColor: true,
2460
+ children: "esc to go back"
2461
+ }, undefined, false, undefined, this)
2462
+ ]
2463
+ }, undefined, true, undefined, this);
2464
+ }
2465
+ var init_Tester = __esm(() => {
2466
+ init_store();
2467
+ init_dispatch();
2468
+ });
2469
+
2470
+ // src/tui/screens/AgentScreen.tsx
2471
+ import { useEffect, useState as useState6 } from "react";
2472
+ import { Box as Box11, Text as Text11, useInput as useInput5 } from "ink";
2473
+ import { Select as Select5 } from "@inkjs/ui";
2474
+ import { jsxDEV as jsxDEV12 } from "react/jsx-dev-runtime";
2475
+ function AgentScreen() {
2476
+ const { state, dispatch } = useStore();
2477
+ const { runLine } = useCommandDispatch();
2478
+ const [status, setStatus] = useState6(null);
2479
+ const active = state.focusZone === "screen" && !state.prompt && !state.busy;
2480
+ useEffect(() => {
2481
+ getAgentStatus(realExec).then(setStatus);
2482
+ }, [state.busy]);
2483
+ useInput5((_input, key) => {
2484
+ if (key.escape)
2485
+ dispatch({ type: "navigate", screen: "dashboard" });
2486
+ }, { isActive: active });
2487
+ return /* @__PURE__ */ jsxDEV12(Box11, {
2488
+ flexDirection: "column",
2489
+ paddingX: 1,
2490
+ gap: 1,
2491
+ children: [
2492
+ /* @__PURE__ */ jsxDEV12(Text11, {
2493
+ bold: true,
2494
+ children: [
2495
+ "ssh-agent:",
2496
+ " ",
2497
+ status === null ? /* @__PURE__ */ jsxDEV12(Text11, {
2498
+ dimColor: true,
2499
+ children: "checking…"
2500
+ }, undefined, false, undefined, this) : status.running ? /* @__PURE__ */ jsxDEV12(Text11, {
2501
+ color: "green",
2502
+ children: [
2503
+ "running · ",
2504
+ status.keys.length,
2505
+ " key(s)"
2506
+ ]
2507
+ }, undefined, true, undefined, this) : /* @__PURE__ */ jsxDEV12(Text11, {
2508
+ color: "red",
2509
+ children: "not running"
2510
+ }, undefined, false, undefined, this)
2511
+ ]
2512
+ }, undefined, true, undefined, this),
2513
+ status?.running && status.keys.length > 0 && /* @__PURE__ */ jsxDEV12(Box11, {
2514
+ flexDirection: "column",
2515
+ children: status.keys.map((key) => /* @__PURE__ */ jsxDEV12(Text11, {
2516
+ dimColor: true,
2517
+ children: [
2518
+ key.bits,
2519
+ " ",
2520
+ key.type,
2521
+ " ",
2522
+ key.fingerprint,
2523
+ " ",
2524
+ key.comment
2525
+ ]
2526
+ }, key.fingerprint, true, undefined, this))
2527
+ }, undefined, false, undefined, this),
2528
+ /* @__PURE__ */ jsxDEV12(Select5, {
2529
+ isDisabled: !active,
2530
+ options: [
2531
+ ...status?.running === false ? [{ label: "▶ Start ssh-agent", value: "agent start" }] : [],
2532
+ ...state.profiles.map((p) => ({ label: `➕ Load ${p.id}`, value: `agent add ${p.id}` })),
2533
+ ...state.profiles.map((p) => ({ label: `➖ Unload ${p.id}`, value: `agent remove ${p.id}` }))
2534
+ ],
2535
+ onChange: (line) => void runLine(line)
2536
+ }, undefined, false, undefined, this),
2537
+ /* @__PURE__ */ jsxDEV12(Text11, {
2538
+ dimColor: true,
2539
+ children: "esc to go back"
2540
+ }, undefined, false, undefined, this)
2541
+ ]
2542
+ }, undefined, true, undefined, this);
2543
+ }
2544
+ var init_AgentScreen = __esm(() => {
2545
+ init_exec();
2546
+ init_agent();
2547
+ init_store();
2548
+ init_dispatch();
2549
+ });
2550
+
2551
+ // src/tui/screens/HelpScreen.tsx
2552
+ import { Box as Box12, Text as Text12, useInput as useInput6 } from "ink";
2553
+ import { jsxDEV as jsxDEV13 } from "react/jsx-dev-runtime";
2554
+ function HelpScreen() {
2555
+ const { state, dispatch } = useStore();
2556
+ useInput6((_input, key) => {
2557
+ if (key.escape)
2558
+ dispatch({ type: "navigate", screen: "dashboard" });
2559
+ }, { isActive: state.focusZone === "screen" && !state.prompt });
2560
+ return /* @__PURE__ */ jsxDEV13(Box12, {
2561
+ flexDirection: "column",
2562
+ paddingX: 1,
2563
+ gap: 1,
2564
+ children: [
2565
+ /* @__PURE__ */ jsxDEV13(Text12, {
2566
+ bold: true,
2567
+ children: "Commands (type them in the bar below, same as the CLI)"
2568
+ }, undefined, false, undefined, this),
2569
+ /* @__PURE__ */ jsxDEV13(Box12, {
2570
+ flexDirection: "column",
2571
+ children: COMMANDS.map((c) => /* @__PURE__ */ jsxDEV13(Box12, {
2572
+ children: [
2573
+ /* @__PURE__ */ jsxDEV13(Box12, {
2574
+ width: 12,
2575
+ children: /* @__PURE__ */ jsxDEV13(Text12, {
2576
+ color: "cyan",
2577
+ children: c.name
2578
+ }, undefined, false, undefined, this)
2579
+ }, undefined, false, undefined, this),
2580
+ /* @__PURE__ */ jsxDEV13(Text12, {
2581
+ dimColor: true,
2582
+ children: c.summary
2583
+ }, undefined, false, undefined, this)
2584
+ ]
2585
+ }, c.name, true, undefined, this))
2586
+ }, undefined, false, undefined, this),
2587
+ /* @__PURE__ */ jsxDEV13(Box12, {
2588
+ flexDirection: "column",
2589
+ children: [
2590
+ /* @__PURE__ */ jsxDEV13(Text12, {
2591
+ bold: true,
2592
+ children: "Keys"
2593
+ }, undefined, false, undefined, this),
2594
+ /* @__PURE__ */ jsxDEV13(Text12, {
2595
+ dimColor: true,
2596
+ children: "/ or : focus command bar · ↑↓ history · 1/2 screens · esc back · q quit"
2597
+ }, undefined, false, undefined, this)
2598
+ ]
2599
+ }, undefined, true, undefined, this)
2600
+ ]
2601
+ }, undefined, true, undefined, this);
2602
+ }
2603
+ var init_HelpScreen = __esm(() => {
2604
+ init_store();
2605
+ init_registry();
2606
+ });
2607
+
2608
+ // src/tui/App.tsx
2609
+ var exports_App = {};
2610
+ __export(exports_App, {
2611
+ App: () => App
2612
+ });
2613
+ import { useEffect as useEffect2 } from "react";
2614
+ import { Box as Box13, Text as Text13, useApp, useInput as useInput7, useWindowSize } from "ink";
2615
+ import { ThemeProvider, defaultTheme } from "@inkjs/ui";
2616
+ import { jsxDEV as jsxDEV14 } from "react/jsx-dev-runtime";
2617
+ function Shell() {
2618
+ const { state, dispatch } = useStore();
2619
+ const { refreshProfiles } = useCommandDispatch();
2620
+ const { exit } = useApp();
2621
+ const { rows, columns } = useWindowSize();
2622
+ useEffect2(() => {
2623
+ refreshProfiles();
2624
+ }, []);
2625
+ useInput7((input, key) => {
2626
+ if (input === "/" || input === ":" || key.ctrl && input === "k") {
2627
+ dispatch({ type: "set-focus", zone: "bar" });
2628
+ return;
2629
+ }
2630
+ if (input === "q")
2631
+ exit();
2632
+ if (input === "?")
2633
+ dispatch({ type: "navigate", screen: "help" });
2634
+ if (key.ctrl && input === "l")
2635
+ dispatch({ type: "clear-transcript" });
2636
+ const screen = SCREEN_KEYS[input];
2637
+ if (screen)
2638
+ dispatch({ type: "navigate", screen });
2639
+ }, { isActive: state.focusZone === "screen" && !state.prompt && !state.busy && state.screen !== "setup" });
2640
+ return /* @__PURE__ */ jsxDEV14(Box13, {
2641
+ flexDirection: "column",
2642
+ width: columns,
2643
+ height: rows,
2644
+ children: [
2645
+ /* @__PURE__ */ jsxDEV14(Header, {}, undefined, false, undefined, this),
2646
+ /* @__PURE__ */ jsxDEV14(Box13, {
2647
+ flexDirection: "column",
2648
+ flexGrow: 1,
2649
+ overflow: "hidden",
2650
+ children: [
2651
+ state.screen === "dashboard" && /* @__PURE__ */ jsxDEV14(Dashboard, {}, undefined, false, undefined, this),
2652
+ state.screen === "keys" && /* @__PURE__ */ jsxDEV14(KeyManager, {}, undefined, false, undefined, this),
2653
+ state.screen === "setup" && /* @__PURE__ */ jsxDEV14(SetupWizard, {}, JSON.stringify(state.setupPrefill), false, undefined, this),
2654
+ state.screen === "test" && /* @__PURE__ */ jsxDEV14(Tester, {}, undefined, false, undefined, this),
2655
+ state.screen === "agent" && /* @__PURE__ */ jsxDEV14(AgentScreen, {}, undefined, false, undefined, this),
2656
+ state.screen === "help" && /* @__PURE__ */ jsxDEV14(HelpScreen, {}, undefined, false, undefined, this)
2657
+ ]
2658
+ }, undefined, true, undefined, this),
2659
+ /* @__PURE__ */ jsxDEV14(Transcript, {}, undefined, false, undefined, this),
2660
+ /* @__PURE__ */ jsxDEV14(PromptOverlay, {}, undefined, false, undefined, this),
2661
+ /* @__PURE__ */ jsxDEV14(CommandBar, {}, undefined, false, undefined, this),
2662
+ /* @__PURE__ */ jsxDEV14(Box13, {
2663
+ paddingX: 1,
2664
+ children: /* @__PURE__ */ jsxDEV14(Text13, {
2665
+ dimColor: true,
2666
+ children: "/ command · 1 dash · 2 keys · 3 setup · 4 test · 5 agent · ? help · ctrl+l clear · q quit"
2667
+ }, undefined, false, undefined, this)
2668
+ }, undefined, false, undefined, this)
2669
+ ]
2670
+ }, undefined, true, undefined, this);
2671
+ }
2672
+ function App() {
2673
+ return /* @__PURE__ */ jsxDEV14(ThemeProvider, {
2674
+ theme: defaultTheme,
2675
+ children: /* @__PURE__ */ jsxDEV14(StoreProvider, {
2676
+ children: /* @__PURE__ */ jsxDEV14(Shell, {}, undefined, false, undefined, this)
2677
+ }, undefined, false, undefined, this)
2678
+ }, undefined, false, undefined, this);
2679
+ }
2680
+ var SCREEN_KEYS;
2681
+ var init_App = __esm(() => {
2682
+ init_store();
2683
+ init_dispatch();
2684
+ init_Header();
2685
+ init_Transcript();
2686
+ init_CommandBar();
2687
+ init_PromptOverlay();
2688
+ init_Dashboard();
2689
+ init_KeyManager();
2690
+ init_SetupWizard();
2691
+ init_Tester();
2692
+ init_AgentScreen();
2693
+ init_HelpScreen();
2694
+ SCREEN_KEYS = {
2695
+ "1": "dashboard",
2696
+ "2": "keys",
2697
+ "3": "setup",
2698
+ "4": "test",
2699
+ "5": "agent"
2700
+ };
2701
+ });
2702
+
2703
+ // src/cli/help.ts
2704
+ var exports_help = {};
2705
+ __export(exports_help, {
2706
+ renderHelp: () => renderHelp
2707
+ });
2708
+ function renderHelp(commandName) {
2709
+ if (commandName) {
2710
+ const def = lookupCommand(commandName);
2711
+ if (def)
2712
+ return renderCommandHelp(def);
2713
+ }
2714
+ const lines = [
2715
+ `xpssh v${VERSION} — SSH keys for git providers, done right`,
2716
+ "",
2717
+ "Usage: xpssh <command> [options]",
2718
+ " xpssh launch the interactive TUI",
2719
+ "",
2720
+ "Commands:",
2721
+ ...COMMANDS.map((c) => ` ${c.name.padEnd(10)} ${c.summary}`),
2722
+ ` ${"help".padEnd(10)} Show help for a command`,
2723
+ "",
2724
+ "Run `xpssh help <command>` for flags."
2725
+ ];
2726
+ return lines.join(`
2727
+ `);
2728
+ }
2729
+ function renderCommandHelp(def) {
2730
+ const lines = [def.summary, "", `Usage: ${def.usage}`];
2731
+ if (def.flags.length > 0) {
2732
+ lines.push("", "Options:");
2733
+ for (const flag of def.flags) {
2734
+ const left = [flag.short ? `-${flag.short},` : " ", `--${flag.name}`, flag.valueHint ?? ""].join(" ").trimEnd();
2735
+ lines.push(` ${left.padEnd(28)} ${flag.description}`);
2736
+ }
2737
+ }
2738
+ return lines.join(`
2739
+ `);
2740
+ }
2741
+ var init_help = __esm(() => {
2742
+ init_registry();
2743
+ init_version();
2744
+ });
2745
+
2746
+ // src/cli/run.ts
2747
+ var exports_run = {};
2748
+ __export(exports_run, {
2749
+ runCli: () => runCli
2750
+ });
2751
+ import * as readline from "node:readline/promises";
2752
+ import { styleText } from "node:util";
2753
+ function renderEvent(event) {
2754
+ switch (event.type) {
2755
+ case "step":
2756
+ if (event.status === "start")
2757
+ process.stdout.write(styleText("dim", `· ${event.label}
2758
+ `));
2759
+ else if (event.status === "fail")
2760
+ process.stdout.write(styleText("red", `✗ ${event.label}
2761
+ `));
2762
+ else
2763
+ process.stdout.write(styleText("green", `✓ ${event.label}
2764
+ `));
2765
+ break;
2766
+ case "success":
2767
+ process.stdout.write(styleText("green", `✓ ${event.text}
2768
+ `));
2769
+ break;
2770
+ case "error":
2771
+ process.stdout.write(styleText("red", `✗ ${event.text}
2772
+ `));
2773
+ break;
2774
+ case "warn":
2775
+ process.stdout.write(styleText("yellow", `! ${event.text}
2776
+ `));
2777
+ break;
2778
+ default:
2779
+ process.stdout.write(` ${event.text}
2780
+ `);
2781
+ }
2782
+ }
2783
+ function buildTerminalContext(yes) {
2784
+ const rl = () => readline.createInterface({ input: process.stdin, output: process.stdout });
2785
+ return {
2786
+ exec: realExec,
2787
+ fetch: globalThis.fetch,
2788
+ env: process.env,
2789
+ paths: resolvePaths(process.env),
2790
+ os: resolveOs(),
2791
+ yes,
2792
+ emit: renderEvent,
2793
+ async confirm(message) {
2794
+ if (yes)
2795
+ return true;
2796
+ const iface = rl();
2797
+ try {
2798
+ const answer = await iface.question(`${message} ${styleText("dim", "[y/N] ")}`);
2799
+ return /^y(es)?$/i.test(answer.trim());
2800
+ } finally {
2801
+ iface.close();
2802
+ }
2803
+ },
2804
+ async promptText(message, options) {
2805
+ if (yes)
2806
+ throw new UsageError(`Missing input: ${message}`);
2807
+ const iface = rl();
2808
+ try {
2809
+ const hint = options?.defaultValue ? styleText("dim", ` (${options.defaultValue})`) : "";
2810
+ const answer = (await iface.question(`${message}${hint}: `)).trim();
2811
+ return answer || options?.defaultValue || "";
2812
+ } finally {
2813
+ iface.close();
2814
+ }
2815
+ },
2816
+ async promptSecret(message) {
2817
+ if (yes)
2818
+ throw new UsageError(`Missing input: ${message}`);
2819
+ const muted = { active: false };
2820
+ const iface = readline.createInterface({
2821
+ input: process.stdin,
2822
+ output: new Proxy(process.stdout, {
2823
+ get(target, prop) {
2824
+ if (prop === "write" && muted.active) {
2825
+ return () => true;
2826
+ }
2827
+ const value = Reflect.get(target, prop);
2828
+ return typeof value === "function" ? value.bind(target) : value;
2829
+ }
2830
+ }),
2831
+ terminal: true
2832
+ });
2833
+ try {
2834
+ process.stdout.write(`${message}: `);
2835
+ muted.active = true;
2836
+ const answer = await iface.question("");
2837
+ process.stdout.write(`
2838
+ `);
2839
+ return answer.trim();
2840
+ } finally {
2841
+ iface.close();
2842
+ }
2843
+ },
2844
+ async promptSelect(message, choices) {
2845
+ if (yes)
2846
+ throw new UsageError(`Missing input: ${message}`);
2847
+ process.stdout.write(`${message}
2848
+ `);
2849
+ choices.forEach((choice, i) => process.stdout.write(` ${i + 1}) ${choice.label}
2850
+ `));
2851
+ const iface = rl();
2852
+ try {
2853
+ for (;; ) {
2854
+ const answer = await iface.question(`Choose [1-${choices.length}]: `);
2855
+ const index = Number(answer.trim()) - 1;
2856
+ const choice = choices[index];
2857
+ if (choice)
2858
+ return choice.value;
2859
+ process.stdout.write(styleText("yellow", `Not a valid choice
2860
+ `));
2861
+ }
2862
+ } finally {
2863
+ iface.close();
2864
+ }
2865
+ }
2866
+ };
2867
+ }
2868
+ async function runCli(tokens) {
2869
+ if (tokens[0] === "help" || tokens.includes("--help") || tokens.includes("-h")) {
2870
+ const target = tokens[0] === "help" ? tokens[1] : tokens[0];
2871
+ process.stdout.write(renderHelp(target === "-h" || target === "--help" ? undefined : target) + `
2872
+ `);
2873
+ return 0;
2874
+ }
2875
+ try {
2876
+ const { def, args } = resolveCommand(tokens);
2877
+ const ctx = buildTerminalContext(hasYesFlag(tokens));
2878
+ const result = await def.run(args, ctx);
2879
+ if (result.message) {
2880
+ renderEvent({ type: result.ok ? "success" : "error", text: result.message });
2881
+ }
2882
+ return result.ok ? 0 : 1;
2883
+ } catch (err) {
2884
+ if (err instanceof UsageError) {
2885
+ process.stderr.write(styleText("red", `${err.message}
2886
+ `));
2887
+ return 2;
2888
+ }
2889
+ process.stderr.write(styleText("red", `✗ ${err.message}
2890
+ `));
2891
+ return 1;
2892
+ }
2893
+ }
2894
+ var init_run = __esm(() => {
2895
+ init_exec();
2896
+ init_paths();
2897
+ init_types();
2898
+ init_parse();
2899
+ init_help();
2900
+ });
2901
+
2902
+ // src/index.tsx
2903
+ init_version();
2904
+ import { jsxDEV as jsxDEV15 } from "react/jsx-dev-runtime";
2905
+ var args = process.argv.slice(2);
2906
+ if (args.includes("--version") || args.includes("-v")) {
2907
+ console.log(VERSION);
2908
+ process.exit(0);
2909
+ }
2910
+ if (args[0] === "ui" || args.length === 0 && process.stdout.isTTY) {
2911
+ const { render } = await import("ink");
2912
+ const { App: App2 } = await Promise.resolve().then(() => (init_App(), exports_App));
2913
+ const { sessionLog: sessionLog2 } = await Promise.resolve().then(() => (init_dispatch(), exports_dispatch));
2914
+ const app = render(/* @__PURE__ */ jsxDEV15(App2, {}, undefined, false, undefined, this), { alternateScreen: true });
2915
+ await app.waitUntilExit();
2916
+ if (sessionLog2.length > 0) {
2917
+ console.log("xpssh session:");
2918
+ for (const line of sessionLog2)
2919
+ console.log(` ${line}`);
2920
+ }
2921
+ process.exit(0);
2922
+ }
2923
+ if (args.length === 0) {
2924
+ const { renderHelp: renderHelp2 } = await Promise.resolve().then(() => (init_help(), exports_help));
2925
+ console.log(renderHelp2());
2926
+ process.exit(0);
2927
+ }
2928
+ var { runCli: runCli2 } = await Promise.resolve().then(() => (init_run(), exports_run));
2929
+ process.exit(await runCli2(args));