@spencerboucher/switchyard 0.6.0 → 0.7.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 (163) hide show
  1. package/README.md +6 -6
  2. package/dist/bridge/switchyard-terminal.js +62 -1
  3. package/dist/switchyard.mjs +869 -643
  4. package/package.json +1 -1
  5. package/web-dist/assets/AutomationView-BIW_pO0M.js +2 -0
  6. package/web-dist/assets/AutomationView-BIW_pO0M.js.map +1 -0
  7. package/web-dist/assets/Badge-BiDp7NKI.js +2 -0
  8. package/web-dist/assets/Badge-BiDp7NKI.js.map +1 -0
  9. package/web-dist/assets/InteractionCard-CWxyDv81.js +6 -0
  10. package/web-dist/assets/InteractionCard-CWxyDv81.js.map +1 -0
  11. package/web-dist/assets/{Menu-GymfuHGP.js → Menu-BDt1bxwz.js} +2 -2
  12. package/web-dist/assets/{Menu-GymfuHGP.js.map → Menu-BDt1bxwz.js.map} +1 -1
  13. package/web-dist/assets/{SegmentedControl-ChdVilXb.js → SegmentedControl-D-2BCZzd.js} +2 -2
  14. package/web-dist/assets/{SegmentedControl-ChdVilXb.js.map → SegmentedControl-D-2BCZzd.js.map} +1 -1
  15. package/web-dist/assets/Tabs-ByiPklAw.js +184 -0
  16. package/web-dist/assets/Tabs-ByiPklAw.js.map +1 -0
  17. package/web-dist/assets/TextArea-BYr88SnV.js +2 -0
  18. package/web-dist/assets/TextArea-BYr88SnV.js.map +1 -0
  19. package/web-dist/assets/attention-Bya8XHKI.js +2 -0
  20. package/web-dist/assets/attention-Bya8XHKI.js.map +1 -0
  21. package/web-dist/assets/automations-DiXKRcnE.js +2 -0
  22. package/web-dist/assets/automations-DiXKRcnE.js.map +1 -0
  23. package/web-dist/assets/board-BrLaJwLJ.js +10 -0
  24. package/web-dist/assets/board-BrLaJwLJ.js.map +1 -0
  25. package/web-dist/assets/c-Cz4nF6n_.js +1 -0
  26. package/web-dist/assets/c-SSSlmE39.js +2 -0
  27. package/web-dist/assets/c-SSSlmE39.js.map +1 -0
  28. package/web-dist/assets/code-DKLO8bzz.js +11 -0
  29. package/web-dist/assets/code-DKLO8bzz.js.map +1 -0
  30. package/web-dist/assets/cpp-Cts1PyIS.js +2 -0
  31. package/web-dist/assets/cpp-Cts1PyIS.js.map +1 -0
  32. package/web-dist/assets/cpp-DzjeI0uH.js +1 -0
  33. package/web-dist/assets/css-BGIxQoYs.js +2 -0
  34. package/web-dist/assets/css-BGIxQoYs.js.map +1 -0
  35. package/web-dist/assets/css-DgaLhXQH.js +1 -0
  36. package/web-dist/assets/describe-DnvH6_fl.js +5 -0
  37. package/web-dist/assets/describe-DnvH6_fl.js.map +1 -0
  38. package/web-dist/assets/diff-woXpYk--.js +2 -0
  39. package/web-dist/assets/diff-woXpYk--.js.map +1 -0
  40. package/web-dist/assets/{dist-Bgkh2hc5.js → dist-BE_V9dwJ.js} +2 -2
  41. package/web-dist/assets/dist-BE_V9dwJ.js.map +1 -0
  42. package/web-dist/assets/dockerfile-IyjqRm3v.js +2 -0
  43. package/web-dist/assets/dockerfile-IyjqRm3v.js.map +1 -0
  44. package/web-dist/assets/enroll-DUDpbJoB.js +2 -0
  45. package/web-dist/assets/enroll-DUDpbJoB.js.map +1 -0
  46. package/web-dist/assets/github-dark-C-LZuMrd.js +2 -0
  47. package/web-dist/assets/github-dark-C-LZuMrd.js.map +1 -0
  48. package/web-dist/assets/github-light-EUqPIrTm.js +2 -0
  49. package/web-dist/assets/github-light-EUqPIrTm.js.map +1 -0
  50. package/web-dist/assets/go-rLFTqkRN.js +2 -0
  51. package/web-dist/assets/go-rLFTqkRN.js.map +1 -0
  52. package/web-dist/assets/html-BMn1ogTm.js +2 -0
  53. package/web-dist/assets/html-BMn1ogTm.js.map +1 -0
  54. package/web-dist/assets/html-BWDvXlHg.js +1 -0
  55. package/web-dist/assets/index-DvRcMGEe.css +2 -0
  56. package/web-dist/assets/index-vIzH1m8u.js +11 -0
  57. package/web-dist/assets/index-vIzH1m8u.js.map +1 -0
  58. package/web-dist/assets/java-BtIQNnN0.js +2 -0
  59. package/web-dist/assets/java-BtIQNnN0.js.map +1 -0
  60. package/web-dist/assets/java-_ATy9zCa.js +1 -0
  61. package/web-dist/assets/javascript-BzbQS41l.js +2 -0
  62. package/web-dist/assets/javascript-BzbQS41l.js.map +1 -0
  63. package/web-dist/assets/javascript-CpIGwZ_Y.js +1 -0
  64. package/web-dist/assets/json-qhed-kSA.js +2 -0
  65. package/web-dist/assets/json-qhed-kSA.js.map +1 -0
  66. package/web-dist/assets/jsonc-CYpm1nAK.js +2 -0
  67. package/web-dist/assets/jsonc-CYpm1nAK.js.map +1 -0
  68. package/web-dist/assets/jsx-C4yDKi0A.js +1 -0
  69. package/web-dist/assets/jsx-D3KKoC5L.js +2 -0
  70. package/web-dist/assets/jsx-D3KKoC5L.js.map +1 -0
  71. package/web-dist/assets/logic-xb-QF00E.js +2 -0
  72. package/web-dist/assets/logic-xb-QF00E.js.map +1 -0
  73. package/web-dist/assets/lua-DjuecEsk.js +2 -0
  74. package/web-dist/assets/lua-DjuecEsk.js.map +1 -0
  75. package/web-dist/assets/lua-fPRBWD-V.js +1 -0
  76. package/web-dist/assets/{more-CRGJsz6E.js → more-BP5x45cs.js} +2 -2
  77. package/web-dist/assets/{more-CRGJsz6E.js.map → more-BP5x45cs.js.map} +1 -1
  78. package/web-dist/assets/now--XjJC37b.js +2 -0
  79. package/web-dist/assets/now--XjJC37b.js.map +1 -0
  80. package/web-dist/assets/python-gzcpVVnB.js +2 -0
  81. package/web-dist/assets/python-gzcpVVnB.js.map +1 -0
  82. package/web-dist/assets/ruby-Bx-U2qjP.js +2 -0
  83. package/web-dist/assets/ruby-Bx-U2qjP.js.map +1 -0
  84. package/web-dist/assets/rust-DpiIf5JO.js +2 -0
  85. package/web-dist/assets/rust-DpiIf5JO.js.map +1 -0
  86. package/web-dist/assets/scss-8bKmll0B.js +2 -0
  87. package/web-dist/assets/scss-8bKmll0B.js.map +1 -0
  88. package/web-dist/assets/send-BKjswHxB.js +2 -0
  89. package/web-dist/assets/send-BKjswHxB.js.map +1 -0
  90. package/web-dist/assets/settings-D8T1esWq.js +7 -0
  91. package/web-dist/assets/settings-D8T1esWq.js.map +1 -0
  92. package/web-dist/assets/shared-CZ7fcu_B.js +3 -0
  93. package/web-dist/assets/shared-CZ7fcu_B.js.map +1 -0
  94. package/web-dist/assets/shellscript-CVTB3cr6.js +1 -0
  95. package/web-dist/assets/shellscript-CqHdAlCp.js +2 -0
  96. package/web-dist/assets/shellscript-CqHdAlCp.js.map +1 -0
  97. package/web-dist/assets/sliders-horizontal-DAoirila.js +2 -0
  98. package/web-dist/assets/sliders-horizontal-DAoirila.js.map +1 -0
  99. package/web-dist/assets/sql-CNu3Or2W.js +2 -0
  100. package/web-dist/assets/sql-CNu3Or2W.js.map +1 -0
  101. package/web-dist/assets/sql-CfM2IJm3.js +1 -0
  102. package/web-dist/assets/states--geW2Ntl.js +2 -0
  103. package/web-dist/assets/states--geW2Ntl.js.map +1 -0
  104. package/web-dist/assets/swift-CyEgAFGc.js +2 -0
  105. package/web-dist/assets/swift-CyEgAFGc.js.map +1 -0
  106. package/web-dist/assets/terminal-login-BJxV78au.js +2 -0
  107. package/web-dist/assets/terminal-login-BJxV78au.js.map +1 -0
  108. package/web-dist/assets/toml-CcmNWLt0.js +2 -0
  109. package/web-dist/assets/toml-CcmNWLt0.js.map +1 -0
  110. package/web-dist/assets/tsx-BOvsFOI6.js +2 -0
  111. package/web-dist/assets/tsx-BOvsFOI6.js.map +1 -0
  112. package/web-dist/assets/tsx-CGosfAUg.js +1 -0
  113. package/web-dist/assets/typescript-Bb1J8qyy.js +1 -0
  114. package/web-dist/assets/typescript-j_1H8WHN.js +2 -0
  115. package/web-dist/assets/typescript-j_1H8WHN.js.map +1 -0
  116. package/web-dist/assets/useNow-jxdoOC_1.js +2 -0
  117. package/web-dist/assets/{useNow-Bc_NlDY3.js.map → useNow-jxdoOC_1.js.map} +1 -1
  118. package/web-dist/assets/xml-DuKfbPI6.js +1 -0
  119. package/web-dist/assets/xml-WY0zc4PG.js +2 -0
  120. package/web-dist/assets/xml-WY0zc4PG.js.map +1 -0
  121. package/web-dist/assets/yaml-DDA_XMNu.js +1 -0
  122. package/web-dist/assets/yaml-DijMHRDA.js +2 -0
  123. package/web-dist/assets/yaml-DijMHRDA.js.map +1 -0
  124. package/web-dist/index.html +4 -3
  125. package/web-dist/sw.js +1 -1
  126. package/web-dist/sw.js.map +1 -1
  127. package/web-dist/assets/AutomationView-DgZn3ltn.js +0 -2
  128. package/web-dist/assets/AutomationView-DgZn3ltn.js.map +0 -1
  129. package/web-dist/assets/InteractionCard-cvPWKSu1.js +0 -4
  130. package/web-dist/assets/InteractionCard-cvPWKSu1.js.map +0 -1
  131. package/web-dist/assets/Markdown-jXkvZeRp.js +0 -31
  132. package/web-dist/assets/Markdown-jXkvZeRp.js.map +0 -1
  133. package/web-dist/assets/Overlay-oYnnwvFM.js +0 -2
  134. package/web-dist/assets/Overlay-oYnnwvFM.js.map +0 -1
  135. package/web-dist/assets/TextArea-BsQHOQMe.js +0 -2
  136. package/web-dist/assets/TextArea-BsQHOQMe.js.map +0 -1
  137. package/web-dist/assets/attention-Bxg9W15n.js +0 -2
  138. package/web-dist/assets/attention-Bxg9W15n.js.map +0 -1
  139. package/web-dist/assets/automations-DdB97dnL.js +0 -2
  140. package/web-dist/assets/automations-DdB97dnL.js.map +0 -1
  141. package/web-dist/assets/board-s4LuDPcg.js +0 -10
  142. package/web-dist/assets/board-s4LuDPcg.js.map +0 -1
  143. package/web-dist/assets/code-B0KXn7lD.js +0 -8
  144. package/web-dist/assets/code-B0KXn7lD.js.map +0 -1
  145. package/web-dist/assets/describe-C_3FJuSs.js +0 -5
  146. package/web-dist/assets/describe-C_3FJuSs.js.map +0 -1
  147. package/web-dist/assets/dist-Bgkh2hc5.js.map +0 -1
  148. package/web-dist/assets/enroll-DijmoolY.js +0 -2
  149. package/web-dist/assets/enroll-DijmoolY.js.map +0 -1
  150. package/web-dist/assets/index-C5n2QB-Y.css +0 -2
  151. package/web-dist/assets/index-Cf6ZUCfK.js +0 -11
  152. package/web-dist/assets/index-Cf6ZUCfK.js.map +0 -1
  153. package/web-dist/assets/logic-D_Eq9DMW.js +0 -2
  154. package/web-dist/assets/logic-D_Eq9DMW.js.map +0 -1
  155. package/web-dist/assets/now-D6lDofYy.js +0 -2
  156. package/web-dist/assets/now-D6lDofYy.js.map +0 -1
  157. package/web-dist/assets/settings-Csal1F-W.js +0 -8
  158. package/web-dist/assets/settings-Csal1F-W.js.map +0 -1
  159. package/web-dist/assets/sliders-horizontal-S3tmwxsP.js +0 -2
  160. package/web-dist/assets/sliders-horizontal-S3tmwxsP.js.map +0 -1
  161. package/web-dist/assets/states-DrTQuYXw.js +0 -2
  162. package/web-dist/assets/states-DrTQuYXw.js.map +0 -1
  163. package/web-dist/assets/useNow-Bc_NlDY3.js +0 -2
@@ -683,7 +683,7 @@ var Device = z3.object({
683
683
  permission: z3.enum(["granted", "denied", "default", "unknown"])
684
684
  }),
685
685
  preferredMobile: z3.boolean(),
686
- enrolledVia: z3.enum(["enrollment", "passkey", "oidc", "admin"]).default("enrollment")
686
+ enrolledVia: z3.enum(["enrollment", "passkey", "oidc", "admin", "approval"]).default("enrollment")
687
687
  });
688
688
  var ToolItemData = z3.object({
689
689
  toolCallId: z3.string(),
@@ -1049,7 +1049,6 @@ var PushPayload = z4.union([
1049
1049
  z4.object({ type: z4.literal("clear"), tag: z4.string() })
1050
1050
  ]);
1051
1051
  var SetupState = z4.object({
1052
- initialized: z4.boolean(),
1053
1052
  passkey: z4.object({
1054
1053
  available: z4.boolean(),
1055
1054
  registered: z4.boolean(),
@@ -1057,10 +1056,6 @@ var SetupState = z4.object({
1057
1056
  }),
1058
1057
  oidc: z4.object({ label: z4.string() }).nullable()
1059
1058
  });
1060
- var InitResponse = z4.object({
1061
- alreadyInitialized: z4.boolean(),
1062
- enrollment: CreateEnrollmentResponse
1063
- });
1064
1059
  var PasskeyRegisterOptionsResponse = z4.object({
1065
1060
  challengeId: z4.string(),
1066
1061
  options: z4.unknown()
@@ -1096,6 +1091,31 @@ var OidcClaimResponse = z4.union([
1096
1091
  z4.object({ status: z4.literal("pending") }),
1097
1092
  z4.object({ status: z4.literal("done"), deviceId: z4.string() })
1098
1093
  ]);
1094
+ var TerminalLoginStartRequest = z4.object({ name: z4.string().min(1).max(80) });
1095
+ var TerminalLoginStartResponse = z4.object({
1096
+ userCode: z4.string(),
1097
+ pollToken: z4.string(),
1098
+ verifyUrl: z4.string(),
1099
+ expiresAt: z4.string(),
1100
+ intervalMs: z4.number().int()
1101
+ });
1102
+ var TerminalLoginClaimRequest = z4.object({
1103
+ userCode: z4.string().min(1),
1104
+ pollToken: z4.string().min(1)
1105
+ });
1106
+ var TerminalLoginClaimResponse = z4.union([
1107
+ z4.object({ status: z4.literal("pending") }),
1108
+ z4.object({ status: z4.literal("denied") }),
1109
+ z4.object({ status: z4.literal("done"), deviceId: z4.string(), credential: z4.string() })
1110
+ ]);
1111
+ var TerminalLoginRequest = z4.object({
1112
+ userCode: z4.string(),
1113
+ name: z4.string(),
1114
+ requestedFrom: z4.string(),
1115
+ createdAt: z4.string(),
1116
+ expiresAt: z4.string(),
1117
+ status: z4.enum(["pending", "approved", "denied"])
1118
+ });
1099
1119
  var AuthMethods = z4.object({
1100
1120
  passkeys: z4.array(Passkey),
1101
1121
  passkeyAvailability: z4.object({ available: z4.boolean(), reason: z4.string().nullable() }),
@@ -1854,7 +1874,7 @@ var ApiClient = class {
1854
1874
  constructor(opts) {
1855
1875
  this.baseUrl = opts.coordinatorUrl.replace(/\/+$/, "");
1856
1876
  this.credential = opts.credential;
1857
- this.doFetch = opts.fetch ?? ((input, init2) => fetch(input, init2));
1877
+ this.doFetch = opts.fetch ?? ((input, init) => fetch(input, init));
1858
1878
  }
1859
1879
  async request(method, path, body) {
1860
1880
  const headers = { accept: "application/json" };
@@ -2320,8 +2340,8 @@ function runRawTerminal(opts) {
2320
2340
  import { Command, Option } from "commander";
2321
2341
 
2322
2342
  // packages/cli/dist/commands/auth.js
2323
- import { hostname } from "node:os";
2324
- import QRCode from "qrcode";
2343
+ import { hostname as hostname3 } from "node:os";
2344
+ import QRCode2 from "qrcode";
2325
2345
 
2326
2346
  // packages/cli/dist/config.js
2327
2347
  import { chmodSync, existsSync as existsSync2, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
@@ -2483,121 +2503,10 @@ function trimUrl(url) {
2483
2503
  return url.trim().replace(/\/+$/, "");
2484
2504
  }
2485
2505
 
2486
- // packages/cli/dist/commands/auth.js
2487
- function adminApi(ctx, flags) {
2488
- const dataDir = flags.dataDir ?? ctx.paths?.dataDir ?? defaultDataDir(ctx.env);
2489
- const token = readAdminToken(dataDir, ctx.env);
2490
- if (!token) {
2491
- throw new CliError(`No admin token: run this on the coordinator machine (looked in ${dataDir}/admin.token) or set SWITCHYARD_ADMIN_TOKEN`);
2492
- }
2493
- const url = trimUrl(flags.coordinator ?? readEndpoint(dataDir)?.localUrl ?? DEFAULT_LOCAL_URL);
2494
- return new ApiClient({ coordinatorUrl: url, credential: token, fetch: ctx.fetch });
2495
- }
2496
- function clockTime(iso) {
2497
- const d = new Date(iso);
2498
- return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
2499
- }
2500
- async function printDeviceLink(ctx, url) {
2501
- const qr = await QRCode.toString(url, { type: "terminal", small: true });
2502
- ctx.out("");
2503
- ctx.out(` ${url}`);
2504
- ctx.out("");
2505
- ctx.out(qr.trimEnd());
2506
- ctx.out("");
2507
- }
2508
- async function printInit(ctx, r) {
2509
- if (r.alreadyInitialized) {
2510
- ctx.out("Already set up. New single-use sign-in link for one device:");
2511
- await printDeviceLink(ctx, r.enrollment.url);
2512
- return;
2513
- }
2514
- ctx.out(`Switchyard is set up. Open this single-use link on your first device (expires ${clockTime(r.enrollment.expiresAt)}):`);
2515
- await printDeviceLink(ctx, r.enrollment.url);
2516
- ctx.out("Then add a passkey in Settings \u203A Sign-in so other devices can sign in without a link.");
2517
- }
2518
- async function init(ctx, flags) {
2519
- const api = adminApi(ctx, flags);
2520
- const r = await api.post("/api/admin/init", {});
2521
- if (flags.json) {
2522
- ctx.out(JSON.stringify(r, null, 2));
2523
- return r;
2524
- }
2525
- await printInit(ctx, r);
2526
- return r;
2527
- }
2528
- async function pair(ctx, flags) {
2529
- const api = adminApi(ctx, flags);
2530
- const kind = flags.host ? "host" : "device";
2531
- const e = await api.post("/api/enrollments", { kind });
2532
- if (flags.json) {
2533
- ctx.out(JSON.stringify(e, null, 2));
2534
- return e;
2535
- }
2536
- const qr = await QRCode.toString(e.url, { type: "terminal", small: true });
2537
- if (kind === "host") {
2538
- ctx.out(`Host enrollment (single use, expires ${e.expiresAt}). On the host, run:`);
2539
- ctx.out("");
2540
- ctx.out(` ${e.command ?? `switchyard host enroll --coordinator <url> --token ${e.token}`}`);
2541
- ctx.out("");
2542
- ctx.out(`Token: ${e.token}`);
2543
- } else {
2544
- ctx.out(`Open this link on the device to enroll it (single use, expires ${e.expiresAt}):`);
2545
- ctx.out("");
2546
- ctx.out(` ${e.url}`);
2547
- ctx.out("");
2548
- ctx.out(qr.trimEnd());
2549
- ctx.out("");
2550
- ctx.out(`Terminal: switchyard login --coordinator ${originOf(e.url)} --token ${e.token}`);
2551
- }
2552
- ctx.out("The link does not provide network access: the device must already reach this URL.");
2553
- return e;
2554
- }
2555
- function originOf(url) {
2556
- try {
2557
- return new URL(url).origin;
2558
- } catch {
2559
- return url;
2560
- }
2561
- }
2562
- function parseEnrollmentInput(token, coordinator) {
2563
- const t = token.trim();
2564
- if (/^https?:\/\//.test(t)) {
2565
- const u = new URL(t);
2566
- const params = new URLSearchParams(u.hash.replace(/^#/, ""));
2567
- const fromUrl = params.get("token") ?? u.searchParams.get("token");
2568
- if (!fromUrl)
2569
- throw new CliError("That URL has no enrollment token");
2570
- return { url: trimUrl(coordinator ?? u.origin), token: fromUrl };
2571
- }
2572
- if (!coordinator)
2573
- throw new CliError("--coordinator <url> is required");
2574
- return { url: trimUrl(coordinator), token: t };
2575
- }
2576
- async function login(ctx, flags) {
2577
- const { url, token } = parseEnrollmentInput(flags.token, flags.coordinator);
2578
- const api = new ApiClient({ coordinatorUrl: url, credential: null, fetch: ctx.fetch });
2579
- const name = flags.name ?? `${hostname().replace(/\.local$/, "")} terminal`;
2580
- const res = await api.post("/api/enroll", {
2581
- token,
2582
- name,
2583
- kind: "terminal"
2584
- });
2585
- if (!res.credential)
2586
- throw new CliError("The coordinator returned no credential");
2587
- const path = ctx.paths?.clientConfig ?? clientConfigPath(ctx.env);
2588
- writeClientConfig(path, {
2589
- coordinatorUrl: url,
2590
- credential: res.credential,
2591
- deviceId: res.deviceId
2592
- });
2593
- ctx.out(`Enrolled "${name}" (${res.deviceId}) with ${url}.`);
2594
- ctx.out(`Credential saved to ${path} (mode 0600). Open a session's Pi terminal with \`switchyard tui <session>\`.`);
2595
- return { path, deviceId: res.deviceId };
2596
- }
2597
-
2598
- // packages/cli/dist/commands/coordinator.js
2599
- import { existsSync as existsSync6, mkdirSync as mkdirSync4 } from "node:fs";
2600
- import { resolve as resolve4 } from "node:path";
2506
+ // packages/cli/dist/commands/up.js
2507
+ import { spawn as spawn4 } from "node:child_process";
2508
+ import { existsSync as existsSync14 } from "node:fs";
2509
+ import { hostname as hostname2 } from "node:os";
2601
2510
 
2602
2511
  // packages/coordinator/dist/attention/router.js
2603
2512
  var DESKTOP_RECENT_MS = 5 * 6e4;
@@ -2895,12 +2804,10 @@ var Auth = class {
2895
2804
  store;
2896
2805
  config;
2897
2806
  hub;
2898
- owner;
2899
- constructor(store, config, hub, owner) {
2807
+ constructor(store, config, hub) {
2900
2808
  this.store = store;
2901
2809
  this.config = config;
2902
2810
  this.hub = hub;
2903
- this.owner = owner;
2904
2811
  }
2905
2812
  get adminTokenPath() {
2906
2813
  return join3(this.config.dataDir, "admin.token");
@@ -2940,14 +2847,13 @@ var Auth = class {
2940
2847
  return this.store.devices.count("is_admin = 0 AND revoked_at IS NULL");
2941
2848
  }
2942
2849
  enrollmentUrl(token, kind) {
2943
- return kind === "device" ? `${this.config.publicUrl}/enroll#token=${token}` : `${this.config.publicUrl}/enroll#token=${token}&kind=host`;
2850
+ return kind === "device" ? `${this.config.publicUrl}/sign-in#token=${token}` : `${this.config.publicUrl}/sign-in#token=${token}&kind=host`;
2944
2851
  }
2945
2852
  hostEnrollCommand(token) {
2946
2853
  return `switchyard host enroll --coordinator ${this.config.publicUrl} --token ${token}`;
2947
2854
  }
2948
2855
  /**
2949
- * Create a single-use enrollment (device 10 min, host 30 min by default). An enrollment created
2950
- * by a device (the local admin included) also marks the server set up.
2856
+ * Create a single-use enrollment (device 10 min, host 30 min by default).
2951
2857
  */
2952
2858
  createEnrollment(kind, createdBy, ttlMs = kind === "device" ? DEVICE_ENROLLMENT_TTL_MS : HOST_ENROLLMENT_TTL_MS) {
2953
2859
  const token = newToken();
@@ -2955,8 +2861,6 @@ var Auth = class {
2955
2861
  const now = this.store.now();
2956
2862
  const expiresAt = new Date(now.getTime() + ttlMs).toISOString();
2957
2863
  this.store.tx(() => {
2958
- if (createdBy !== null)
2959
- this.owner.markInitialized();
2960
2864
  this.store.insertEnrollment({
2961
2865
  id,
2962
2866
  kind,
@@ -4459,7 +4363,7 @@ function createSession(ctx, input) {
4459
4363
  nativeHandle: null,
4460
4364
  profileSnapshot: profile,
4461
4365
  runtime: "starting",
4462
- runtimeDetail: null,
4366
+ runtimeDetail: workspace ? "Starting Pi\u2026" : "Creating worktree\u2026",
4463
4367
  connectivity: host.connectivity,
4464
4368
  runId: null,
4465
4369
  taskId: null,
@@ -4475,19 +4379,27 @@ function createSession(ctx, input) {
4475
4379
  ctx.store.putSession(session);
4476
4380
  const choice = input.workspace;
4477
4381
  const started = (async () => {
4382
+ const t0 = Date.now();
4478
4383
  try {
4479
4384
  let s = session;
4480
4385
  if (!("existingId" in choice)) {
4481
4386
  const ws = await createWorkspace(ctx, project, checkout, choice.newBranch, choice.base);
4387
+ ctx.log.info({ sessionId: session.id, ms: Date.now() - t0 }, "session start: worktree ready");
4482
4388
  s = ctx.store.tx(() => {
4483
4389
  const cur = ctx.store.sessions.require(session.id);
4484
- return ctx.store.putSession({ ...cur, workspaceId: ws.id, cwd: ws.path });
4390
+ return ctx.store.putSession({
4391
+ ...cur,
4392
+ workspaceId: ws.id,
4393
+ cwd: ws.path,
4394
+ runtimeDetail: "Starting Pi\u2026"
4395
+ });
4485
4396
  });
4486
4397
  }
4487
4398
  await startOnHost(ctx, s, {
4488
4399
  initialPrompt: input.prompt.trim() ? input.prompt : null,
4489
4400
  nativeHandle: null
4490
4401
  });
4402
+ ctx.log.info({ sessionId: session.id, ms: Date.now() - t0 }, "session start: started on host");
4491
4403
  } catch (err) {
4492
4404
  const message = err instanceof Error ? err.message : String(err);
4493
4405
  ctx.log.warn({ err, sessionId: session.id }, "session start failed");
@@ -7328,10 +7240,23 @@ INSERT OR IGNORE INTO settings(key, value)
7328
7240
  WHERE EXISTS (SELECT 1 FROM devices WHERE is_admin = 0 AND revoked_at IS NULL);
7329
7241
  `;
7330
7242
 
7243
+ // packages/coordinator/dist/db/migrations/003_terminal_login.js
7244
+ var sql3 = `
7245
+ DROP TABLE auth_flows;
7246
+
7247
+ CREATE TABLE auth_flows (
7248
+ id TEXT PRIMARY KEY,
7249
+ kind TEXT NOT NULL CHECK (kind IN ('passkey_register','passkey_auth','oidc','terminal_login')),
7250
+ data TEXT NOT NULL,
7251
+ expires_at TEXT NOT NULL
7252
+ );
7253
+ `;
7254
+
7331
7255
  // packages/coordinator/dist/db/index.js
7332
7256
  var MIGRATIONS = [
7333
7257
  { version: 1, name: "001_init", sql },
7334
- { version: 2, name: "002_owner_auth", sql: sql2 }
7258
+ { version: 2, name: "002_owner_auth", sql: sql2 },
7259
+ { version: 3, name: "003_terminal_login", sql: sql3 }
7335
7260
  ];
7336
7261
  var LATEST_MIGRATION = MIGRATIONS[MIGRATIONS.length - 1].version;
7337
7262
  function openDb(path) {
@@ -8171,8 +8096,11 @@ import * as client from "openid-client";
8171
8096
  // packages/coordinator/dist/owner.js
8172
8097
  import { randomBytes as randomBytes2 } from "node:crypto";
8173
8098
  import { isIP } from "node:net";
8174
- var OWNER_INITIALIZED_KEY = "owner.initialized_at";
8175
8099
  var OWNER_WEBAUTHN_USER_KEY = "owner.webauthn_user_id";
8100
+ function isLoopbackUrl(url) {
8101
+ const host = new URL(url).hostname.replace(/^\[|\]$/g, "");
8102
+ return host === "localhost" || host === "::1" || /^127\.\d+\.\d+\.\d+$/.test(host);
8103
+ }
8176
8104
  var Owner = class {
8177
8105
  store;
8178
8106
  config;
@@ -8180,18 +8108,6 @@ var Owner = class {
8180
8108
  this.store = store;
8181
8109
  this.config = config;
8182
8110
  }
8183
- isInitialized() {
8184
- return this.config.oidc !== null || this.store.getSetting(OWNER_INITIALIZED_KEY, null) !== null;
8185
- }
8186
- /** Mark the server set up. Returns true when this call set it. */
8187
- markInitialized() {
8188
- return this.store.tx(() => {
8189
- if (this.store.getSetting(OWNER_INITIALIZED_KEY, null) !== null)
8190
- return false;
8191
- this.store.setSetting(OWNER_INITIALIZED_KEY, this.store.nowIso());
8192
- return true;
8193
- });
8194
- }
8195
8111
  /** Stable random WebAuthn user handle for the owner (16 bytes, created lazily). */
8196
8112
  webauthnUserId() {
8197
8113
  return this.store.tx(() => {
@@ -8851,53 +8767,6 @@ function registerAttentionRoutes(app, ctx) {
8851
8767
  app.post("/api/attention/:id/acknowledge", async (req) => ctx.attention.acknowledge(req.params.id));
8852
8768
  }
8853
8769
 
8854
- // packages/coordinator/dist/routes/devices.js
8855
- import QRCode2 from "qrcode";
8856
- import { z as z14 } from "zod";
8857
- var UpdateDeviceRequest = z14.object({
8858
- name: z14.string().min(1).max(80).optional(),
8859
- preferredMobile: z14.boolean().optional()
8860
- });
8861
- async function enrollmentResponse(ctx, kind, createdBy) {
8862
- const e = ctx.auth.createEnrollment(kind, createdBy);
8863
- const qrSvg = await QRCode2.toString(e.url, { type: "svg", margin: 1 });
8864
- return { token: e.token, url: e.url, expiresAt: e.expiresAt, qrSvg, command: e.command };
8865
- }
8866
- function publicDevice(ctx, id) {
8867
- return ctx.store.devices.require(id);
8868
- }
8869
- function registerDeviceRoutes(app, ctx) {
8870
- app.post("/api/enrollments", async (req, reply) => {
8871
- const body = parse(CreateEnrollmentRequest, req.body);
8872
- return reply.code(201).send(await enrollmentResponse(ctx, body.kind, deviceOf(req).id));
8873
- });
8874
- app.post("/api/devices/enrollments", async (req, reply) => reply.code(201).send(await enrollmentResponse(ctx, "device", deviceOf(req).id)));
8875
- app.get("/api/devices", async () => ctx.store.devices.list("is_admin = 0", [], "created_at"));
8876
- app.get("/api/devices/:id", async (req) => publicDevice(ctx, req.params.id));
8877
- app.patch("/api/devices/:id", async (req) => {
8878
- const { id } = req.params;
8879
- const body = parse(UpdateDeviceRequest, req.body);
8880
- return ctx.store.tx(() => {
8881
- const d = ctx.store.devices.require(id);
8882
- if (body.preferredMobile) {
8883
- for (const other of ctx.store.devices.list("is_admin = 0")) {
8884
- if (other.id !== id && other.preferredMobile) {
8885
- ctx.store.putDevice({ ...other, preferredMobile: false });
8886
- }
8887
- }
8888
- }
8889
- return ctx.store.putDevice({
8890
- ...d,
8891
- name: body.name ?? d.name,
8892
- preferredMobile: body.preferredMobile ?? d.preferredMobile
8893
- });
8894
- });
8895
- });
8896
- const revoke = async (req) => ctx.auth.revokeDevice(req.params.id);
8897
- app.post("/api/devices/:id/revoke", revoke);
8898
- app.delete("/api/devices/:id", revoke);
8899
- }
8900
-
8901
8770
  // packages/coordinator/dist/routes/auth.js
8902
8771
  var HANDOFF_PAGE = `<!doctype html>
8903
8772
  <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
@@ -8908,7 +8777,6 @@ var HANDOFF_PAGE = `<!doctype html>
8908
8777
  function setupState(ctx) {
8909
8778
  const availability = ctx.owner.passkeyAvailability();
8910
8779
  return {
8911
- initialized: ctx.owner.isInitialized(),
8912
8780
  passkey: {
8913
8781
  available: availability.available,
8914
8782
  registered: ctx.passkeys.count() > 0,
@@ -8988,6 +8856,18 @@ function registerAuthRoutes(app, ctx) {
8988
8856
  ctx.auth.setDeviceCookie(reply, r.credential);
8989
8857
  return { status: "done", deviceId: r.deviceId };
8990
8858
  });
8859
+ app.post("/api/auth/terminal/start", async (req, reply) => {
8860
+ limitAuth(req);
8861
+ const body = parse(TerminalLoginStartRequest, req.body);
8862
+ return reply.code(201).send(ctx.terminalLogins.start(body.name, req.ip));
8863
+ });
8864
+ app.post("/api/auth/terminal/claim", async (req) => {
8865
+ const body = parse(TerminalLoginClaimRequest, req.body);
8866
+ return ctx.terminalLogins.claim(body.userCode, body.pollToken);
8867
+ });
8868
+ app.get("/api/auth/terminal/requests/:code", async (req) => ctx.terminalLogins.describe(req.params.code));
8869
+ app.post("/api/auth/terminal/requests/:code/approve", async (req) => ctx.terminalLogins.approve(req.params.code));
8870
+ app.post("/api/auth/terminal/requests/:code/deny", async (req) => ctx.terminalLogins.deny(req.params.code));
8991
8871
  app.get("/api/auth/methods", async () => {
8992
8872
  const availability = ctx.owner.passkeyAvailability();
8993
8873
  return {
@@ -9006,15 +8886,6 @@ function registerAuthRoutes(app, ctx) {
9006
8886
  ctx.passkeys.remove(req.params.id);
9007
8887
  return { ok: true };
9008
8888
  });
9009
- app.post("/api/admin/init", async (req) => {
9010
- const device = deviceOf(req);
9011
- if (!device.isAdmin || device.id !== ADMIN_DEVICE_ID) {
9012
- throw new SwitchyardError("forbidden", "Only the server's local admin token can initialize");
9013
- }
9014
- const alreadyInitialized = !ctx.owner.markInitialized();
9015
- const enrollment = await enrollmentResponse(ctx, "device", device.id);
9016
- return { alreadyInitialized, enrollment };
9017
- });
9018
8889
  }
9019
8890
  function redirectError(ctx, req, reply, err) {
9020
8891
  const code = err instanceof SwitchyardError ? err.code : "oidc_failed";
@@ -9024,7 +8895,54 @@ function redirectError(ctx, req, reply, err) {
9024
8895
  else if (err.code === "forbidden")
9025
8896
  ctx.log.info({ detail }, "OIDC sign-in refused");
9026
8897
  const qs = new URLSearchParams({ error: code, detail });
9027
- return reply.redirect(`/enroll?${qs.toString()}`, 302);
8898
+ return reply.redirect(`/sign-in?${qs.toString()}`, 302);
8899
+ }
8900
+
8901
+ // packages/coordinator/dist/routes/devices.js
8902
+ import QRCode from "qrcode";
8903
+ import { z as z14 } from "zod";
8904
+ var UpdateDeviceRequest = z14.object({
8905
+ name: z14.string().min(1).max(80).optional(),
8906
+ preferredMobile: z14.boolean().optional()
8907
+ });
8908
+ async function enrollmentResponse(ctx, kind, createdBy) {
8909
+ const e = ctx.auth.createEnrollment(kind, createdBy);
8910
+ const qrSvg = await QRCode.toString(e.url, { type: "svg", margin: 1 });
8911
+ return { token: e.token, url: e.url, expiresAt: e.expiresAt, qrSvg, command: e.command };
8912
+ }
8913
+ function publicDevice(ctx, id) {
8914
+ return ctx.store.devices.require(id);
8915
+ }
8916
+ function registerDeviceRoutes(app, ctx) {
8917
+ app.post("/api/enrollments", async (req, reply) => {
8918
+ const body = parse(CreateEnrollmentRequest, req.body);
8919
+ return reply.code(201).send(await enrollmentResponse(ctx, body.kind, deviceOf(req).id));
8920
+ });
8921
+ app.post("/api/devices/enrollments", async (req, reply) => reply.code(201).send(await enrollmentResponse(ctx, "device", deviceOf(req).id)));
8922
+ app.get("/api/devices", async () => ctx.store.devices.list("is_admin = 0", [], "created_at"));
8923
+ app.get("/api/devices/:id", async (req) => publicDevice(ctx, req.params.id));
8924
+ app.patch("/api/devices/:id", async (req) => {
8925
+ const { id } = req.params;
8926
+ const body = parse(UpdateDeviceRequest, req.body);
8927
+ return ctx.store.tx(() => {
8928
+ const d = ctx.store.devices.require(id);
8929
+ if (body.preferredMobile) {
8930
+ for (const other of ctx.store.devices.list("is_admin = 0")) {
8931
+ if (other.id !== id && other.preferredMobile) {
8932
+ ctx.store.putDevice({ ...other, preferredMobile: false });
8933
+ }
8934
+ }
8935
+ }
8936
+ return ctx.store.putDevice({
8937
+ ...d,
8938
+ name: body.name ?? d.name,
8939
+ preferredMobile: body.preferredMobile ?? d.preferredMobile
8940
+ });
8941
+ });
8942
+ });
8943
+ const revoke = async (req) => ctx.auth.revokeDevice(req.params.id);
8944
+ app.post("/api/devices/:id/revoke", revoke);
8945
+ app.delete("/api/devices/:id", revoke);
9028
8946
  }
9029
8947
 
9030
8948
  // packages/coordinator/dist/routes/hosts.js
@@ -9808,11 +9726,11 @@ var Statements = class {
9808
9726
  constructor(db) {
9809
9727
  this.db = db;
9810
9728
  }
9811
- get(sql3) {
9812
- let s = this.cache.get(sql3);
9729
+ get(sql4) {
9730
+ let s = this.cache.get(sql4);
9813
9731
  if (!s) {
9814
- s = this.db.prepare(sql3);
9815
- this.cache.set(sql3, s);
9732
+ s = this.db.prepare(sql4);
9733
+ this.cache.set(sql4, s);
9816
9734
  }
9817
9735
  return s;
9818
9736
  }
@@ -9843,12 +9761,12 @@ var JsonRepo = class {
9843
9761
  }
9844
9762
  /** List with an optional SQL `WHERE` fragment over the indexed columns. */
9845
9763
  list(where, params = [], orderBy = "rowid") {
9846
- const sql3 = `SELECT json FROM ${this.table}${where ? ` WHERE ${where}` : ""} ORDER BY ${orderBy}`;
9847
- return this.st.get(sql3).all(...params).map((r) => JSON.parse(r.json));
9764
+ const sql4 = `SELECT json FROM ${this.table}${where ? ` WHERE ${where}` : ""} ORDER BY ${orderBy}`;
9765
+ return this.st.get(sql4).all(...params).map((r) => JSON.parse(r.json));
9848
9766
  }
9849
9767
  count(where, params = []) {
9850
- const sql3 = `SELECT COUNT(*) AS n FROM ${this.table}${where ? ` WHERE ${where}` : ""}`;
9851
- return this.st.get(sql3).get(...params).n;
9768
+ const sql4 = `SELECT COUNT(*) AS n FROM ${this.table}${where ? ` WHERE ${where}` : ""}`;
9769
+ return this.st.get(sql4).get(...params).n;
9852
9770
  }
9853
9771
  /**
9854
9772
  * Update by id, or insert when missing. `extra` columns are written in both cases when given
@@ -10594,44 +10512,169 @@ var TerminalHub = class {
10594
10512
  }
10595
10513
  };
10596
10514
 
10597
- // packages/coordinator/dist/server.js
10598
- var MIN_HOST_ENROLL_SECRET_LENGTH = 32;
10599
- var PUBLIC_ROUTES = /* @__PURE__ */ new Set([
10600
- "/api/enroll",
10601
- "/api/setup",
10602
- "/api/auth/passkey/options",
10603
- "/api/auth/passkey/verify",
10604
- "/api/auth/oidc/begin",
10605
- "/api/auth/oidc/callback",
10606
- "/api/auth/oidc/claim",
10607
- "/api/hosts/enroll",
10608
- "/api/hooks/:automationId",
10609
- "/healthz",
10610
- "/readyz",
10611
- "/ws/host"
10612
- ]);
10613
- function isStaticFileRoute(req) {
10614
- if (req.method !== "GET" && req.method !== "HEAD")
10615
- return false;
10616
- const config = req.routeOptions.config;
10617
- return typeof config?.file === "string" && typeof config.rootPath === "string";
10618
- }
10619
- function isApiLikePath(url) {
10620
- let path = url.split("?")[0] ?? "";
10621
- try {
10622
- path = decodeURIComponent(path);
10623
- } catch {
10624
- return true;
10625
- }
10515
+ // packages/coordinator/dist/terminal-login.js
10516
+ import { randomInt } from "node:crypto";
10517
+ var TERMINAL_LOGIN_TTL_MS = 10 * 6e4;
10518
+ var TERMINAL_LOGIN_POLL_MS = 2e3;
10519
+ var CODE_ALPHABET = "BCDFGHJKLMNPQRSTVWXZ";
10520
+ function normalizeUserCode(s) {
10521
+ return s.toUpperCase().replace(/[^A-Z]/g, "");
10522
+ }
10523
+ function formatUserCode(n) {
10524
+ return `${n.slice(0, 4)}-${n.slice(4)}`;
10525
+ }
10526
+ var TerminalLogins = class {
10527
+ store;
10528
+ config;
10529
+ auth;
10530
+ flows;
10531
+ constructor(store, config, auth) {
10532
+ this.store = store;
10533
+ this.config = config;
10534
+ this.auth = auth;
10535
+ this.flows = new AuthFlows(store);
10536
+ }
10537
+ load(code) {
10538
+ const id = normalizeUserCode(code);
10539
+ const flow = this.flows.get("terminal_login", id);
10540
+ if (!flow) {
10541
+ throw new SwitchyardError("not_found", "This sign-in request expired or does not exist. Run `switchyard login` again.");
10542
+ }
10543
+ return { id, flow };
10544
+ }
10545
+ view(id, flow) {
10546
+ return {
10547
+ userCode: formatUserCode(id),
10548
+ name: flow.name,
10549
+ requestedFrom: flow.requestedFrom,
10550
+ createdAt: flow.createdAt,
10551
+ expiresAt: flow.expiresAt,
10552
+ status: flow.status
10553
+ };
10554
+ }
10555
+ start(name, requestedFrom) {
10556
+ let id = "";
10557
+ for (let attempt = 0; attempt < 5 && !id; attempt++) {
10558
+ let candidate = "";
10559
+ for (let i = 0; i < 8; i++)
10560
+ candidate += CODE_ALPHABET[randomInt(0, CODE_ALPHABET.length)];
10561
+ if (!this.flows.get("terminal_login", candidate))
10562
+ id = candidate;
10563
+ }
10564
+ if (!id)
10565
+ throw new SwitchyardError("internal", "Could not allocate a sign-in code");
10566
+ const pollToken = newToken();
10567
+ const now = this.store.now();
10568
+ const createdAt = now.toISOString();
10569
+ const expiresAt = new Date(now.getTime() + TERMINAL_LOGIN_TTL_MS).toISOString();
10570
+ const flow = {
10571
+ name,
10572
+ requestedFrom,
10573
+ createdAt,
10574
+ expiresAt,
10575
+ pollTokenHash: sha256(pollToken),
10576
+ status: "pending",
10577
+ result: null
10578
+ };
10579
+ this.flows.put("terminal_login", id, flow, TERMINAL_LOGIN_TTL_MS);
10580
+ const userCode = formatUserCode(id);
10581
+ return {
10582
+ userCode,
10583
+ pollToken,
10584
+ verifyUrl: `${this.config.publicUrl}/terminal-login?code=${userCode}`,
10585
+ expiresAt,
10586
+ intervalMs: TERMINAL_LOGIN_POLL_MS
10587
+ };
10588
+ }
10589
+ describe(code) {
10590
+ const { id, flow } = this.load(code);
10591
+ return this.view(id, flow);
10592
+ }
10593
+ approve(code) {
10594
+ return this.store.tx(() => {
10595
+ const { id, flow } = this.load(code);
10596
+ if (flow.status !== "pending") {
10597
+ throw new SwitchyardError("invalid_request", "This sign-in was already approved or denied");
10598
+ }
10599
+ const { device, credential } = this.auth.createDevice(flow.name, "terminal", "approval");
10600
+ const next = {
10601
+ ...flow,
10602
+ status: "approved",
10603
+ result: { deviceId: device.id, credential }
10604
+ };
10605
+ this.flows.update(id, next);
10606
+ return this.view(id, next);
10607
+ });
10608
+ }
10609
+ deny(code) {
10610
+ return this.store.tx(() => {
10611
+ const { id, flow } = this.load(code);
10612
+ if (flow.status !== "pending") {
10613
+ throw new SwitchyardError("invalid_request", "This sign-in was already approved or denied");
10614
+ }
10615
+ const next = { ...flow, status: "denied" };
10616
+ this.flows.update(id, next);
10617
+ return this.view(id, next);
10618
+ });
10619
+ }
10620
+ claim(code, pollToken) {
10621
+ const id = normalizeUserCode(code);
10622
+ const flow = this.flows.get("terminal_login", id);
10623
+ if (!flow || !safeEqualHex(sha256(pollToken), flow.pollTokenHash)) {
10624
+ throw new SwitchyardError("unauthorized", "Unknown or expired sign-in");
10625
+ }
10626
+ if (flow.status === "pending")
10627
+ return { status: "pending" };
10628
+ this.flows.delete(id);
10629
+ if (flow.status === "denied" || !flow.result)
10630
+ return { status: "denied" };
10631
+ return { status: "done", deviceId: flow.result.deviceId, credential: flow.result.credential };
10632
+ }
10633
+ };
10634
+
10635
+ // packages/coordinator/dist/server.js
10636
+ var MIN_HOST_ENROLL_SECRET_LENGTH = 32;
10637
+ var PUBLIC_ROUTES = /* @__PURE__ */ new Set([
10638
+ "/api/enroll",
10639
+ "/api/setup",
10640
+ "/api/auth/passkey/options",
10641
+ "/api/auth/passkey/verify",
10642
+ "/api/auth/oidc/begin",
10643
+ "/api/auth/oidc/callback",
10644
+ "/api/auth/oidc/claim",
10645
+ "/api/auth/terminal/start",
10646
+ "/api/auth/terminal/claim",
10647
+ "/api/hosts/enroll",
10648
+ "/api/hooks/:automationId",
10649
+ "/healthz",
10650
+ "/readyz",
10651
+ "/ws/host"
10652
+ ]);
10653
+ function isStaticFileRoute(req) {
10654
+ if (req.method !== "GET" && req.method !== "HEAD")
10655
+ return false;
10656
+ const config = req.routeOptions.config;
10657
+ return typeof config?.file === "string" && typeof config.rootPath === "string";
10658
+ }
10659
+ function isApiLikePath(url) {
10660
+ let path = url.split("?")[0] ?? "";
10661
+ try {
10662
+ path = decodeURIComponent(path);
10663
+ } catch {
10664
+ return true;
10665
+ }
10626
10666
  return path.startsWith("/api/") || path.startsWith("/ws/") || path.startsWith("/adapter-frames/");
10627
10667
  }
10628
- function originOf2(url) {
10668
+ function originOf(url) {
10629
10669
  return new URL(url).origin;
10630
10670
  }
10631
10671
  function validateAuthOptions(opts) {
10632
10672
  if (opts.hostEnrollSecret !== void 0 && opts.hostEnrollSecret.length < MIN_HOST_ENROLL_SECRET_LENGTH) {
10633
10673
  throw new SwitchyardError("invalid_request", `SWITCHYARD_HOST_ENROLL_SECRET must be at least ${MIN_HOST_ENROLL_SECRET_LENGTH} characters`);
10634
10674
  }
10675
+ if (!opts.oidc && opts.publicUrl && !isLoopbackUrl(opts.publicUrl)) {
10676
+ throw new SwitchyardError("invalid_request", `Single sign-on is required for ${opts.publicUrl}: set SWITCHYARD_OIDC_ISSUER, SWITCHYARD_OIDC_CLIENT_ID and an owner (SWITCHYARD_OIDC_OWNER_*), see docs/auth.md. Without single sign-on, Switchyard runs only at a localhost public URL.`);
10677
+ }
10635
10678
  if (opts.oidc && !hasOwnerMatch(opts.oidc.owner)) {
10636
10679
  throw new SwitchyardError("invalid_request", "OIDC needs an owner match: set SWITCHYARD_OIDC_OWNER_SUBJECT, SWITCHYARD_OIDC_OWNER_EMAIL or SWITCHYARD_OIDC_OWNER_GROUP");
10637
10680
  }
@@ -10656,7 +10699,7 @@ function createCoordinator(opts) {
10656
10699
  const config = {
10657
10700
  dataDir,
10658
10701
  publicUrl,
10659
- trustedOrigins: opts.trustedOrigins ?? [originOf2(publicUrl)],
10702
+ trustedOrigins: opts.trustedOrigins ?? [originOf(publicUrl)],
10660
10703
  webDist: opts.webDist ? resolve3(opts.webDist) : null,
10661
10704
  secureCookies: publicUrl.startsWith("https:"),
10662
10705
  unreachableAfterMs: opts.unreachableAfterMs ?? 6e4,
@@ -10669,12 +10712,11 @@ function createCoordinator(opts) {
10669
10712
  hub.onListenerError = (name, err) => log.error({ err, event: name }, "event listener failed");
10670
10713
  const store = new Store2(db, hub, opts.clock);
10671
10714
  const owner = new Owner(store, config);
10672
- const auth = new Auth(store, config, hub, owner);
10715
+ const auth = new Auth(store, config, hub);
10673
10716
  const adminToken = auth.ensureAdminToken();
10674
- if (config.oidc)
10675
- owner.markInitialized();
10676
10717
  const passkeys = new Passkeys(store, owner, config, auth);
10677
10718
  const oidc = config.oidc ? new Oidc(store, owner, config, auth) : null;
10719
+ const terminalLogins = new TerminalLogins(store, config, auth);
10678
10720
  const limits = opts.rateLimits === false ? null : opts.rateLimits ?? {};
10679
10721
  const storeClock = () => store.now().getTime();
10680
10722
  const limiters = {
@@ -10703,6 +10745,7 @@ function createCoordinator(opts) {
10703
10745
  owner,
10704
10746
  passkeys,
10705
10747
  oidc,
10748
+ terminalLogins,
10706
10749
  limiters,
10707
10750
  log,
10708
10751
  services: /* @__PURE__ */ new Map()
@@ -10810,10 +10853,10 @@ function createCoordinator(opts) {
10810
10853
  sweep();
10811
10854
  retentionTimer = setInterval(sweep, opts.retentionIntervalMs ?? 36e5);
10812
10855
  retentionTimer.unref();
10813
- if (!owner.isInitialized()) {
10814
- log.info("not set up yet: run `switchyard init` on this server");
10856
+ if (!config.oidc && auth.activeDeviceCount() === 0 && passkeys.count() === 0) {
10857
+ log.info("no device signed in yet: run `switchyard pair` on this machine");
10815
10858
  if (opts.setupHint !== false) {
10816
- process.stderr.write("Switchyard is not set up yet. Run `switchyard init` on this server to create the first sign-in link.\n");
10859
+ process.stderr.write("No device is signed in yet. Run `switchyard pair` on this machine for a single-use sign-in link.\n");
10817
10860
  }
10818
10861
  }
10819
10862
  },
@@ -10834,162 +10877,11 @@ function createCoordinator(opts) {
10834
10877
  return coordinator;
10835
10878
  }
10836
10879
 
10837
- // packages/cli/dist/commands/coordinator.js
10838
- function parseListen(v) {
10839
- const s = v.trim();
10840
- let host = "0.0.0.0";
10841
- let portStr = s;
10842
- const v6 = s.match(/^\[([^\]]+)\]:(\d+)$/);
10843
- if (v6) {
10844
- host = v6[1];
10845
- portStr = v6[2];
10846
- } else if (s.includes(":")) {
10847
- const i = s.lastIndexOf(":");
10848
- host = s.slice(0, i) || "0.0.0.0";
10849
- portStr = s.slice(i + 1);
10850
- } else if (!/^\d+$/.test(s)) {
10851
- host = s;
10852
- portStr = String(DEFAULT_PORT);
10853
- }
10854
- const port = Number(portStr);
10855
- if (!Number.isInteger(port) || port < 0 || port > 65535) {
10856
- throw new CliError(`Invalid listen address "${v}" (expected host:port)`);
10857
- }
10858
- return { host, port };
10859
- }
10860
- function parseOrigins(v) {
10861
- if (!v?.trim())
10862
- return void 0;
10863
- return v.split(",").map((o) => o.trim().replace(/\/+$/, "")).filter(Boolean);
10864
- }
10865
- var DEFAULT_OIDC_LABEL = "Sign in with single sign-on";
10866
- var DEFAULT_OIDC_SCOPES = "openid profile email groups";
10867
- function envValue(env, key) {
10868
- const v = env[key]?.trim();
10869
- return v ? v : void 0;
10870
- }
10871
- function authOptionsFromEnv(env) {
10872
- const out = {};
10873
- const secret = envValue(env, "SWITCHYARD_HOST_ENROLL_SECRET");
10874
- if (secret)
10875
- out.hostEnrollSecret = secret;
10876
- const proxies = parseOrigins(env.SWITCHYARD_TRUSTED_PROXIES);
10877
- if (proxies)
10878
- out.trustedProxies = proxies;
10879
- const issuer = envValue(env, "SWITCHYARD_OIDC_ISSUER");
10880
- const clientId = envValue(env, "SWITCHYARD_OIDC_CLIENT_ID");
10881
- if (issuer && clientId) {
10882
- if (!/^https?:\/\//.test(issuer)) {
10883
- throw new CliError(`SWITCHYARD_OIDC_ISSUER must be an http(s) URL, got "${issuer}"`);
10884
- }
10885
- out.oidc = {
10886
- issuer,
10887
- clientId,
10888
- clientSecret: envValue(env, "SWITCHYARD_OIDC_CLIENT_SECRET") ?? null,
10889
- label: envValue(env, "SWITCHYARD_OIDC_LABEL") ?? DEFAULT_OIDC_LABEL,
10890
- scopes: envValue(env, "SWITCHYARD_OIDC_SCOPES") ?? DEFAULT_OIDC_SCOPES,
10891
- owner: {
10892
- subject: envValue(env, "SWITCHYARD_OIDC_OWNER_SUBJECT") ?? null,
10893
- email: envValue(env, "SWITCHYARD_OIDC_OWNER_EMAIL") ?? null,
10894
- group: envValue(env, "SWITCHYARD_OIDC_OWNER_GROUP") ?? null
10895
- }
10896
- };
10897
- } else if (issuer || clientId) {
10898
- throw new CliError("Single sign-on needs both SWITCHYARD_OIDC_ISSUER and SWITCHYARD_OIDC_CLIENT_ID");
10899
- }
10900
- return out;
10901
- }
10902
- function coordinatorOptions(flags, env) {
10903
- const dataDir = flags.dataDir ? resolve4(flags.dataDir) : defaultDataDir(env);
10904
- const listen = parseListen(flags.listen ?? env.SWITCHYARD_LISTEN ?? `0.0.0.0:${DEFAULT_PORT}`);
10905
- const publicUrl = (flags.publicUrl ?? env.SWITCHYARD_PUBLIC_URL)?.trim() || void 0;
10906
- if (publicUrl && !/^https?:\/\//.test(publicUrl)) {
10907
- throw new CliError(`Public URL must start with http:// or https://, got "${publicUrl}"`);
10908
- }
10909
- const trustedOrigins = parseOrigins(flags.trustedOrigins ?? env.SWITCHYARD_TRUSTED_ORIGINS);
10910
- return {
10911
- dataDir,
10912
- listen,
10913
- publicUrl,
10914
- trustedOrigins,
10915
- webDist: resolveWebDist(flags.webDist, env),
10916
- ...authOptionsFromEnv(env)
10917
- };
10918
- }
10919
- function waitForSignal() {
10920
- return new Promise((res) => {
10921
- const on = (sig) => {
10922
- process.off("SIGINT", on);
10923
- process.off("SIGTERM", on);
10924
- res(sig);
10925
- };
10926
- process.on("SIGINT", on);
10927
- process.on("SIGTERM", on);
10928
- });
10929
- }
10930
- async function coordinatorStart(ctx, flags) {
10931
- const opts = coordinatorOptions(flags, ctx.env);
10932
- const c = createCoordinator(opts);
10933
- await c.start();
10934
- writeEndpoint(opts.dataDir, {
10935
- localUrl: localUrlFor(opts.listen, c.port),
10936
- publicUrl: c.config.publicUrl
10937
- });
10938
- ctx.err(`Switchyard coordinator listening on ${opts.listen.host}:${c.port} (public URL ${c.config.publicUrl}; data ${opts.dataDir}${opts.webDist ? "" : "; web app not found, API only"})`);
10939
- const sig = await waitForSignal();
10940
- ctx.err(`${sig}: stopping coordinator`);
10941
- await c.stop();
10942
- }
10943
- function localUrlFor(listen, port) {
10944
- const h = listen.host ?? "0.0.0.0";
10945
- const host = h === "0.0.0.0" || h === "::" || h === "" ? "127.0.0.1" : h.includes(":") ? `[${h}]` : h;
10946
- return `http://${host}:${port}`;
10947
- }
10948
- function dataDirOf(flags, env) {
10949
- return flags.dataDir ? resolve4(flags.dataDir) : defaultDataDir(env);
10950
- }
10951
- function coordinatorBackup(ctx, out, flags) {
10952
- const r = backup(dataDirOf(flags, ctx.env), out);
10953
- ctx.out(`Backed up to ${r.file} (schema ${r.migrationLevel})`);
10954
- }
10955
- function coordinatorRestore(ctx, file, flags) {
10956
- const r = restore(dataDirOf(flags, ctx.env), file);
10957
- ctx.out(`Restored ${r.restoredFrom} (schema ${r.migrationLevel}).`);
10958
- if (r.previous)
10959
- ctx.out(`The previous database was kept as ${r.previous}`);
10960
- if (r.migrationLevel < LATEST_MIGRATION) {
10961
- ctx.out("Migrations to the current schema run when the coordinator next starts.");
10962
- }
10963
- }
10964
- function coordinatorMigrate(ctx, flags) {
10965
- const dataDir = dataDirOf(flags, ctx.env);
10966
- const holder = lockHolder(dataDir);
10967
- if (holder !== null) {
10968
- throw new CliError(`Coordinator (pid ${holder}) is running on ${dataDir}; it migrated at start`);
10969
- }
10970
- const path = dbPath(dataDir);
10971
- const existed = existsSync6(path);
10972
- mkdirSync4(dataDir, { recursive: true });
10973
- const db = openDb(path);
10974
- try {
10975
- const before = existed ? migrationLevel(db) : 0;
10976
- migrate(db);
10977
- const after = migrationLevel(db);
10978
- ctx.out(before === after ? `Schema is current (level ${after}) at ${path}` : `Migrated ${path} from level ${before} to ${after}`);
10979
- } finally {
10980
- db.close();
10981
- }
10982
- }
10983
-
10984
- // packages/cli/dist/commands/doctor.js
10985
- import { execFile as execFile3 } from "node:child_process";
10986
- import { existsSync as existsSync14, readFileSync as readFileSync9 } from "node:fs";
10987
-
10988
10880
  // packages/adapter-sdk/dist/discovery.js
10989
- import { existsSync as existsSync7 } from "node:fs";
10881
+ import { existsSync as existsSync6 } from "node:fs";
10990
10882
  import { readFile } from "node:fs/promises";
10991
10883
  import { homedir as homedir2 } from "node:os";
10992
- import { isAbsolute, join as join6, resolve as resolve5 } from "node:path";
10884
+ import { isAbsolute, join as join6, resolve as resolve4 } from "node:path";
10993
10885
  function piAgentDir(env = process.env, home = homedir2()) {
10994
10886
  const fromEnv = env.PI_CODING_AGENT_DIR?.trim();
10995
10887
  if (fromEnv)
@@ -11061,7 +10953,7 @@ function piPackageDirCandidates(source, agentDir, opts = {}) {
11061
10953
  return [join6(agentDir, "git", parsed.host, parsed.path)];
11062
10954
  case "local": {
11063
10955
  const p = expandHome(parsed.path, home);
11064
- return [isAbsolute(p) ? p : resolve5(agentDir, p)];
10956
+ return [isAbsolute(p) ? p : resolve4(agentDir, p)];
11065
10957
  }
11066
10958
  }
11067
10959
  }
@@ -11087,7 +10979,7 @@ async function discoverPiPackages(opts = {}) {
11087
10979
  };
11088
10980
  const diag = (level, message) => out.diagnostics.push({ sessionId: null, adapterId: null, level, message });
11089
10981
  const settingsPath = join6(agentDir, "settings.json");
11090
- if (!existsSync7(settingsPath))
10982
+ if (!existsSync6(settingsPath))
11091
10983
  return out;
11092
10984
  let settings;
11093
10985
  try {
@@ -11109,14 +11001,14 @@ async function discoverPiPackages(opts = {}) {
11109
11001
  home,
11110
11002
  globalNpmRoot: opts.globalNpmRoot
11111
11003
  });
11112
- const dir = candidates.find((c) => existsSync7(c));
11004
+ const dir = candidates.find((c) => existsSync6(c));
11113
11005
  if (!dir) {
11114
11006
  diag("warning", `Pi package ${source} could not be resolved (looked in ${candidates.join(", ")}); any adapter it provides is not loaded.`);
11115
11007
  continue;
11116
11008
  }
11117
11009
  let pkg = null;
11118
11010
  const pkgPath = join6(dir, "package.json");
11119
- if (existsSync7(pkgPath)) {
11011
+ if (existsSync6(pkgPath)) {
11120
11012
  try {
11121
11013
  pkg = await readJson(pkgPath);
11122
11014
  } catch (err) {
@@ -11268,7 +11160,7 @@ function genericWidgetPanel(lines) {
11268
11160
 
11269
11161
  // packages/adapter-sdk/dist/manifest.js
11270
11162
  import { readFile as readFile2 } from "node:fs/promises";
11271
- import { dirname as dirname4, isAbsolute as isAbsolute2, join as join7, resolve as resolve6 } from "node:path";
11163
+ import { dirname as dirname4, isAbsolute as isAbsolute2, join as join7, resolve as resolve5 } from "node:path";
11272
11164
  import { validRange } from "semver";
11273
11165
  import { z as z18 } from "zod";
11274
11166
  var MANIFEST_FILE = "switchyard-adapter.json";
@@ -11312,7 +11204,7 @@ function parseManifest(input, path = null) {
11312
11204
  throw new ManifestError(`invalid adapter manifest: ${issues}`, path);
11313
11205
  }
11314
11206
  async function loadManifest(pathOrDir) {
11315
- const abs = resolve6(pathOrDir);
11207
+ const abs = resolve5(pathOrDir);
11316
11208
  const manifestPath = abs.endsWith(".json") ? abs : join7(abs, MANIFEST_FILE);
11317
11209
  let raw;
11318
11210
  try {
@@ -11332,8 +11224,8 @@ async function loadManifest(pathOrDir) {
11332
11224
  manifest,
11333
11225
  manifestPath,
11334
11226
  dir,
11335
- serverPath: resolve6(dir, manifest.server),
11336
- webRendererPath: manifest.webRenderer ? resolve6(dir, manifest.webRenderer) : null
11227
+ serverPath: resolve5(dir, manifest.server),
11228
+ webRendererPath: manifest.webRenderer ? resolve5(dir, manifest.webRenderer) : null
11337
11229
  };
11338
11230
  }
11339
11231
 
@@ -11388,9 +11280,9 @@ function responseFitsRequest(req, resp) {
11388
11280
  }
11389
11281
 
11390
11282
  // packages/adapter-sdk/dist/registry.js
11391
- import { existsSync as existsSync8 } from "node:fs";
11283
+ import { existsSync as existsSync7 } from "node:fs";
11392
11284
  import { readdir, readFile as readFile3 } from "node:fs/promises";
11393
- import { join as join8, resolve as resolve7 } from "node:path";
11285
+ import { join as join8, resolve as resolve6 } from "node:path";
11394
11286
  import { pathToFileURL } from "node:url";
11395
11287
  import { satisfies } from "semver";
11396
11288
 
@@ -11474,6 +11366,10 @@ var AdapterSession = class {
11474
11366
  this.activeTools.set(e.toolCallId, e);
11475
11367
  await this.callAll("onToolStart", e);
11476
11368
  }
11369
+ /** `tool_execution_update`: runs `onToolUpdate` hooks with the partial result. */
11370
+ async onToolUpdate(e) {
11371
+ await this.callAll("onToolUpdate", e);
11372
+ }
11477
11373
  /** `tool_execution_end`: runs `onToolEnd` hooks, then expires answers queued for the call. */
11478
11374
  async onToolEnd(e) {
11479
11375
  try {
@@ -11811,7 +11707,7 @@ function isServerAdapter(x) {
11811
11707
  async function loadAdapterFromDir(pathOrDir, tier) {
11812
11708
  const m = await loadManifest(pathOrDir);
11813
11709
  const warnings = [];
11814
- if (!existsSync8(m.serverPath)) {
11710
+ if (!existsSync7(m.serverPath)) {
11815
11711
  throw new AdapterLoadError(`server module ${m.serverPath} does not exist`, m.manifest);
11816
11712
  }
11817
11713
  let mod;
@@ -11832,7 +11728,7 @@ async function loadAdapterFromDir(pathOrDir, tier) {
11832
11728
  throw new AdapterLoadError(`${m.serverPath} must default-export a ServerAdapter object or factory`, m.manifest);
11833
11729
  }
11834
11730
  let webRendererPath = m.webRendererPath;
11835
- if (webRendererPath && !existsSync8(webRendererPath)) {
11731
+ if (webRendererPath && !existsSync7(webRendererPath)) {
11836
11732
  warnings.push(`web renderer ${webRendererPath} does not exist; portable panels are used`);
11837
11733
  webRendererPath = null;
11838
11734
  }
@@ -11849,12 +11745,12 @@ async function loadAdapterFromDir(pathOrDir, tier) {
11849
11745
  };
11850
11746
  }
11851
11747
  async function subdirsWithManifest(root) {
11852
- if (!existsSync8(root))
11748
+ if (!existsSync7(root))
11853
11749
  return [];
11854
- if (existsSync8(join8(root, MANIFEST_FILE)))
11750
+ if (existsSync7(join8(root, MANIFEST_FILE)))
11855
11751
  return [root];
11856
11752
  const entries = await readdir(root, { withFileTypes: true });
11857
- return entries.filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => join8(root, e.name)).filter((d) => existsSync8(join8(d, MANIFEST_FILE))).sort();
11753
+ return entries.filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => join8(root, e.name)).filter((d) => existsSync7(join8(d, MANIFEST_FILE))).sort();
11858
11754
  }
11859
11755
  var AdapterRegistry = class {
11860
11756
  opts;
@@ -11872,7 +11768,7 @@ var AdapterRegistry = class {
11872
11768
  this.diags = [];
11873
11769
  const candidates = [];
11874
11770
  if (opts.projectRoot) {
11875
- for (const d of await this.safeList(resolve7(opts.projectRoot, PROJECT_ADAPTER_DIR))) {
11771
+ for (const d of await this.safeList(resolve6(opts.projectRoot, PROJECT_ADAPTER_DIR))) {
11876
11772
  candidates.push({ tier: "project", path: d });
11877
11773
  }
11878
11774
  }
@@ -11887,7 +11783,7 @@ var AdapterRegistry = class {
11887
11783
  this.diagnostic("warning", null, `Cannot read ${pkgDir}/package.json: ${err.message}`);
11888
11784
  }
11889
11785
  for (const e of entries)
11890
- candidates.push({ tier: "plugin", path: resolve7(pkgDir, e) });
11786
+ candidates.push({ tier: "plugin", path: resolve6(pkgDir, e) });
11891
11787
  }
11892
11788
  const loaded = [];
11893
11789
  for (const c of candidates) {
@@ -12158,7 +12054,7 @@ async function runChecks(path, commands, opts = {}) {
12158
12054
 
12159
12055
  // packages/host-agent/dist/files.js
12160
12056
  import { lstat, readdir as readdir2, readFile as readFile4, realpath, stat } from "node:fs/promises";
12161
- import { join as join9, relative, resolve as resolve8, sep } from "node:path";
12057
+ import { join as join9, relative, resolve as resolve7, sep } from "node:path";
12162
12058
  var MAX_READ_BYTES = 1024 * 1024;
12163
12059
  var BINARY_SNIFF_BYTES = 8e3;
12164
12060
  function isInside(root, target) {
@@ -12182,7 +12078,7 @@ async function resolveInside(root, rel) {
12182
12078
  if (rel.includes("\0"))
12183
12079
  throw new SwitchyardError("invalid_request", "Invalid path");
12184
12080
  const rootReal = await realRoot(root);
12185
- const target = resolve8(rootReal, rel.replace(/^[/\\]+/, ""));
12081
+ const target = resolve7(rootReal, rel.replace(/^[/\\]+/, ""));
12186
12082
  if (!isInside(rootReal, target)) {
12187
12083
  throw new SwitchyardError("forbidden", "Path is outside the workspace");
12188
12084
  }
@@ -12261,7 +12157,7 @@ async function readWorkspaceFile(root, file) {
12261
12157
  import { execFile } from "node:child_process";
12262
12158
  import { appendFile, lstat as lstat2, mkdir, readFile as readFile5, realpath as realpath2, stat as stat2 } from "node:fs/promises";
12263
12159
  import { tmpdir } from "node:os";
12264
- import { join as join10, resolve as resolve9, sep as sep2 } from "node:path";
12160
+ import { join as join10, resolve as resolve8, sep as sep2 } from "node:path";
12265
12161
  var MAX_BUFFER = 64 * 1024 * 1024;
12266
12162
  var EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
12267
12163
  var MAX_PATCH_BYTES = 2 * 1024 * 1024;
@@ -12338,7 +12234,7 @@ async function realpathOr(path) {
12338
12234
  }
12339
12235
  async function commonDirOf(path) {
12340
12236
  const { stdout } = await runGit(path, ["rev-parse", "--git-common-dir"]);
12341
- return realpathOr(resolve9(path, stdout.trim()));
12237
+ return realpathOr(resolve8(path, stdout.trim()));
12342
12238
  }
12343
12239
  function notARepo(path) {
12344
12240
  return new SwitchyardError("not_a_git_repo", `${path} is not a git repository`);
@@ -13112,8 +13008,8 @@ var CommandRouter = class {
13112
13008
  };
13113
13009
 
13114
13010
  // packages/host-agent/dist/config.js
13115
- import { chmodSync as chmodSync3, existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync5, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
13116
- import { homedir as homedir4, hostname as hostname2 } from "node:os";
13011
+ import { chmodSync as chmodSync3, existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync5, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
13012
+ import { homedir as homedir4, hostname } from "node:os";
13117
13013
  import { dirname as dirname5, join as join12 } from "node:path";
13118
13014
  import { z as z21 } from "zod";
13119
13015
  var HostConfigSchema = z21.object({
@@ -13148,11 +13044,11 @@ function statePaths(stateDir) {
13148
13044
  logs: join12(stateDir, "logs")
13149
13045
  };
13150
13046
  for (const d of [p.root, p.runtime, p.logs])
13151
- mkdirSync5(d, { recursive: true, mode: 448 });
13047
+ mkdirSync4(d, { recursive: true, mode: 448 });
13152
13048
  return p;
13153
13049
  }
13154
13050
  function loadConfig(path) {
13155
- if (!existsSync9(path)) {
13051
+ if (!existsSync8(path)) {
13156
13052
  throw new SwitchyardError("not_found", `Host config ${path} not found; enroll this host first (switchyard host enroll)`);
13157
13053
  }
13158
13054
  let raw;
@@ -13171,7 +13067,7 @@ function loadConfig(path) {
13171
13067
  }
13172
13068
  function saveConfig(path, config) {
13173
13069
  const cfg = HostConfigSchema.parse(config);
13174
- mkdirSync5(dirname5(path), { recursive: true, mode: 448 });
13070
+ mkdirSync4(dirname5(path), { recursive: true, mode: 448 });
13175
13071
  const tmp = `${path}.${process.pid}.tmp`;
13176
13072
  writeFileSync4(tmp, `${JSON.stringify(cfg, null, 2)}
13177
13073
  `, { mode: 384 });
@@ -13249,16 +13145,16 @@ function abortError(signal) {
13249
13145
  async function waitForConfig(opts) {
13250
13146
  const { configPath, env, log, signal } = opts;
13251
13147
  const t = { ...DEFAULT_TIMING, ...opts.timing };
13252
- if (existsSync9(configPath))
13148
+ if (existsSync8(configPath))
13253
13149
  return loadConfig(configPath);
13254
13150
  const secret = env.SWITCHYARD_HOST_ENROLL_SECRET?.trim();
13255
13151
  const coordinatorUrl = env.SWITCHYARD_COORDINATOR_URL?.trim();
13256
13152
  if (secret && coordinatorUrl) {
13257
- const name = env.SWITCHYARD_HOST_NAME?.trim() || hostname2();
13153
+ const name = env.SWITCHYARD_HOST_NAME?.trim() || hostname();
13258
13154
  for (; ; ) {
13259
13155
  if (signal?.aborted)
13260
13156
  throw abortError(signal);
13261
- if (existsSync9(configPath))
13157
+ if (existsSync8(configPath))
13262
13158
  return loadConfig(configPath);
13263
13159
  try {
13264
13160
  const enrolled = await enroll({
@@ -13288,7 +13184,7 @@ async function waitForConfig(opts) {
13288
13184
  for (; ; ) {
13289
13185
  if (signal?.aborted)
13290
13186
  throw abortError(signal);
13291
- if (existsSync9(configPath)) {
13187
+ if (existsSync8(configPath)) {
13292
13188
  try {
13293
13189
  return loadConfig(configPath);
13294
13190
  } catch (err) {
@@ -13308,7 +13204,7 @@ async function waitForConfig(opts) {
13308
13204
  }
13309
13205
 
13310
13206
  // packages/host-agent/dist/db/index.js
13311
- import { mkdirSync as mkdirSync6 } from "node:fs";
13207
+ import { mkdirSync as mkdirSync5 } from "node:fs";
13312
13208
  import { dirname as dirname6 } from "node:path";
13313
13209
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
13314
13210
 
@@ -13392,7 +13288,7 @@ var HostDb = class {
13392
13288
  constructor(path) {
13393
13289
  this.path = path;
13394
13290
  if (path !== ":memory:")
13395
- mkdirSync6(dirname6(path), { recursive: true });
13291
+ mkdirSync5(dirname6(path), { recursive: true });
13396
13292
  this.db = new DatabaseSync3(path);
13397
13293
  this.db.exec("PRAGMA journal_mode = WAL");
13398
13294
  this.db.exec("PRAGMA foreign_keys = ON");
@@ -13468,15 +13364,15 @@ var HostDb = class {
13468
13364
  else
13469
13365
  fn();
13470
13366
  }
13471
- run(sql3, params = []) {
13472
- const r = this.db.prepare(sql3).run(...params);
13367
+ run(sql4, params = []) {
13368
+ const r = this.db.prepare(sql4).run(...params);
13473
13369
  return { changes: Number(r.changes), lastInsertRowid: Number(r.lastInsertRowid) };
13474
13370
  }
13475
- get(sql3, params = []) {
13476
- return this.db.prepare(sql3).get(...params);
13371
+ get(sql4, params = []) {
13372
+ return this.db.prepare(sql4).get(...params);
13477
13373
  }
13478
- all(sql3, params = []) {
13479
- return this.db.prepare(sql3).all(...params);
13374
+ all(sql4, params = []) {
13375
+ return this.db.prepare(sql4).all(...params);
13480
13376
  }
13481
13377
  // ---- meta ----
13482
13378
  getMeta(key) {
@@ -13486,12 +13382,12 @@ var HostDb = class {
13486
13382
  this.run("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value", [key, value]);
13487
13383
  }
13488
13384
  // ---- JSON row helpers ----
13489
- getJson(sql3, params = []) {
13490
- const r = this.get(sql3, params);
13385
+ getJson(sql4, params = []) {
13386
+ const r = this.get(sql4, params);
13491
13387
  return r ? JSON.parse(r.json) : void 0;
13492
13388
  }
13493
- allJson(sql3, params = []) {
13494
- return this.all(sql3, params).map((r) => JSON.parse(r.json));
13389
+ allJson(sql4, params = []) {
13390
+ return this.all(sql4, params).map((r) => JSON.parse(r.json));
13495
13391
  }
13496
13392
  close() {
13497
13393
  try {
@@ -14150,7 +14046,7 @@ ${stderr}`));
14150
14046
  import { accessSync, constants, readFileSync as readFileSync6, realpathSync } from "node:fs";
14151
14047
  import { open, readdir as readdir4, readFile as readFile6, stat as stat4 } from "node:fs/promises";
14152
14048
  import { homedir as homedir5 } from "node:os";
14153
- import { delimiter, dirname as dirname7, join as join13, resolve as resolve10 } from "node:path";
14049
+ import { delimiter, dirname as dirname7, join as join13, resolve as resolve9 } from "node:path";
14154
14050
  import { pathToFileURL as pathToFileURL2 } from "node:url";
14155
14051
 
14156
14052
  // packages/pi-runtime/dist/transcript.js
@@ -14413,7 +14309,7 @@ async function listImportable(sessionsRoot = defaultSessionsRoot(), now = Date.n
14413
14309
  }
14414
14310
  function findOnPath(bin) {
14415
14311
  if (bin.includes("/"))
14416
- return resolve10(bin);
14312
+ return resolve9(bin);
14417
14313
  for (const dir of (process.env.PATH ?? "").split(delimiter)) {
14418
14314
  if (!dir)
14419
14315
  continue;
@@ -14676,14 +14572,14 @@ function writeJsonl(stream, value) {
14676
14572
  import { spawn as spawn2 } from "node:child_process";
14677
14573
  import { randomUUID } from "node:crypto";
14678
14574
  import { EventEmitter as EventEmitter3 } from "node:events";
14679
- import { chmodSync as chmodSync4, existsSync as existsSync10, mkdirSync as mkdirSync7, rmSync as rmSync2 } from "node:fs";
14575
+ import { chmodSync as chmodSync4, existsSync as existsSync9, mkdirSync as mkdirSync6, rmSync as rmSync2 } from "node:fs";
14680
14576
  import { createServer } from "node:net";
14681
14577
  import { tmpdir as tmpdir2 } from "node:os";
14682
14578
  import { join as join14 } from "node:path";
14683
14579
  import { fileURLToPath as fileURLToPath2 } from "node:url";
14684
14580
  function bridgeExtensionPath() {
14685
14581
  const sibling = fileURLToPath2(new URL("./bridge/switchyard-bridge.js", import.meta.url));
14686
- if (existsSync10(sibling))
14582
+ if (existsSync9(sibling))
14687
14583
  return sibling;
14688
14584
  return fileURLToPath2(new URL("../dist/bridge/switchyard-bridge.js", import.meta.url));
14689
14585
  }
@@ -14702,7 +14598,7 @@ function shortSocketPath(runtimeDir, prefix) {
14702
14598
  if (join14(dir, name).length > MAX_SOCKET_PATH) {
14703
14599
  dir = join14("/tmp", `switchyard-${process.getuid?.() ?? "u"}`);
14704
14600
  }
14705
- mkdirSync7(dir, { recursive: true, mode: 448 });
14601
+ mkdirSync6(dir, { recursive: true, mode: 448 });
14706
14602
  return join14(dir, name);
14707
14603
  }
14708
14604
  var PiRpcProcess = class _PiRpcProcess extends EventEmitter3 {
@@ -14941,12 +14837,12 @@ var PiRpcProcess = class _PiRpcProcess extends EventEmitter3 {
14941
14837
  // packages/pi-runtime/dist/session-runtime.js
14942
14838
  import { randomUUID as randomUUID2 } from "node:crypto";
14943
14839
  import { EventEmitter as EventEmitter5 } from "node:events";
14944
- import { existsSync as existsSync12 } from "node:fs";
14840
+ import { existsSync as existsSync11 } from "node:fs";
14945
14841
 
14946
14842
  // packages/pi-runtime/dist/terminal-process.js
14947
14843
  import { spawn as spawn3 } from "node:child_process";
14948
14844
  import { EventEmitter as EventEmitter4 } from "node:events";
14949
- import { closeSync, existsSync as existsSync11, mkdirSync as mkdirSync8, openSync, readFileSync as readFileSync7 } from "node:fs";
14845
+ import { closeSync, existsSync as existsSync10, mkdirSync as mkdirSync7, openSync, readFileSync as readFileSync7 } from "node:fs";
14950
14846
  import { createRequire } from "node:module";
14951
14847
  import { createConnection } from "node:net";
14952
14848
  import { tmpdir as tmpdir3 } from "node:os";
@@ -14954,7 +14850,7 @@ import { basename as basename2, dirname as dirname8, join as join15 } from "node
14954
14850
  import { fileURLToPath as fileURLToPath3 } from "node:url";
14955
14851
  function resolveDist(rel) {
14956
14852
  const sibling = fileURLToPath3(new URL(`./${rel}`, import.meta.url));
14957
- if (existsSync11(sibling))
14853
+ if (existsSync10(sibling))
14958
14854
  return sibling;
14959
14855
  return fileURLToPath3(new URL(`../dist/${rel}`, import.meta.url));
14960
14856
  }
@@ -14971,7 +14867,7 @@ function terminalSupport() {
14971
14867
  return { ok: false, reason: `node-pty is not available: ${err.message}` };
14972
14868
  }
14973
14869
  const holder = holderScriptPath();
14974
- if (!existsSync11(holder))
14870
+ if (!existsSync10(holder))
14975
14871
  return { ok: false, reason: `Terminal holder not built: ${holder}` };
14976
14872
  return { ok: true };
14977
14873
  }
@@ -15058,7 +14954,7 @@ var PiTerminalProcess = class _PiTerminalProcess extends EventEmitter4 {
15058
14954
  rows: opts.rows ?? 40
15059
14955
  };
15060
14956
  const logPath = opts.logPath ?? join15(dirname8(socketPath), `${basename2(socketPath, ".sock")}.log`);
15061
- mkdirSync8(dirname8(logPath), { recursive: true, mode: 448 });
14957
+ mkdirSync7(dirname8(logPath), { recursive: true, mode: 448 });
15062
14958
  const fd = openSync(logPath, "a", 384);
15063
14959
  try {
15064
14960
  const child = spawn3(process.execPath, [holderScriptPath()], {
@@ -15210,6 +15106,7 @@ var PiTerminalProcess = class _PiTerminalProcess extends EventEmitter4 {
15210
15106
  type: rec.event === "ui_prompt_end" ? "ui_prompt_end" : "ui_prompt_start",
15211
15107
  kind: typeof rec.kind === "string" ? rec.kind : "unknown",
15212
15108
  title: typeof rec.title === "string" ? rec.title : null,
15109
+ remoteBacked: rec.remoteBacked === true,
15213
15110
  seq: ++this.seq,
15214
15111
  at: (/* @__PURE__ */ new Date()).toISOString(),
15215
15112
  pid: this.pid ?? 0
@@ -15390,7 +15287,7 @@ var ManagedSession = class _ManagedSession extends EventEmitter5 {
15390
15287
  await this.attachTerminal(this.opts.terminalSocket);
15391
15288
  return;
15392
15289
  }
15393
- if (this._native && !existsSync12(this._native.sessionFile)) {
15290
+ if (this._native && !existsSync11(this._native.sessionFile)) {
15394
15291
  const message = `Native session file not found: ${this._native.sessionFile}`;
15395
15292
  this.fail(message);
15396
15293
  throw new SwitchyardError("native_session_missing", message, {
@@ -16180,7 +16077,7 @@ ${output}`.trimEnd() }));
16180
16077
  };
16181
16078
 
16182
16079
  // packages/host-agent/dist/supervisor.js
16183
- import { existsSync as existsSync13, realpathSync as realpathSync2, statSync } from "node:fs";
16080
+ import { existsSync as existsSync12, realpathSync as realpathSync2, statSync } from "node:fs";
16184
16081
  import { join as join16, sep as sep3 } from "node:path";
16185
16082
  var RESTART_NOTICE = "Host agent restarted; resume to continue";
16186
16083
  var QUEUED_ANSWER_SUMMARY = "Answered from your earlier form";
@@ -16522,7 +16419,7 @@ var SessionSupervisor = class {
16522
16419
  if (!pi.compatible) {
16523
16420
  throw new SwitchyardError("pi_incompatible", pi.reason ?? "Pi is not available on this host");
16524
16421
  }
16525
- if (!existsSync13(p.workspacePath) || !statSync(p.workspacePath).isDirectory()) {
16422
+ if (!existsSync12(p.workspacePath) || !statSync(p.workspacePath).isDirectory()) {
16526
16423
  throw new SwitchyardError("not_found", `Workspace ${p.workspacePath} does not exist`);
16527
16424
  }
16528
16425
  const prev = this.records.get(p.sessionId);
@@ -16537,7 +16434,7 @@ var SessionSupervisor = class {
16537
16434
  }
16538
16435
  }
16539
16436
  let nextOrder = prev?.nextOrder ?? 0;
16540
- if (!prev && nativeHandle && existsSync13(nativeHandle.sessionFile)) {
16437
+ if (!prev && nativeHandle && existsSync12(nativeHandle.sessionFile)) {
16541
16438
  try {
16542
16439
  const native = await readNativeTranscript(nativeHandle.sessionFile, {
16543
16440
  piBin: this.opts.piBin
@@ -16547,6 +16444,8 @@ var SessionSupervisor = class {
16547
16444
  nextOrder = 0;
16548
16445
  }
16549
16446
  }
16447
+ const t0 = Date.now();
16448
+ let adaptersMs = 0;
16550
16449
  const now = nowIso();
16551
16450
  let control = { controlRevision: p.controlRevision, controlMode: p.controlMode };
16552
16451
  for (const c of [prev, this.pendingControl.get(p.sessionId)]) {
@@ -16568,7 +16467,7 @@ var SessionSupervisor = class {
16568
16467
  profile: p.profile,
16569
16468
  nativeHandle,
16570
16469
  runtime: "starting",
16571
- runtimeDetail: null,
16470
+ runtimeDetail: "Starting Pi\u2026",
16572
16471
  controlRevision: control.controlRevision,
16573
16472
  controlMode: control.controlMode,
16574
16473
  origin: p.origin,
@@ -16588,7 +16487,11 @@ var SessionSupervisor = class {
16588
16487
  const l = this.newLive(rec);
16589
16488
  this.live.set(rec.id, l);
16590
16489
  this.db.tx(() => this.saveAndEmit(rec));
16591
- await this.openAdapters(l, p.workspacePath);
16490
+ const adaptersStart = Date.now();
16491
+ this.enqueue(l, async () => {
16492
+ await this.openAdapters(l, p.workspacePath);
16493
+ adaptersMs = Date.now() - adaptersStart;
16494
+ });
16592
16495
  const ms = new ManagedSession({
16593
16496
  sessionId: rec.id,
16594
16497
  cwd: p.workspacePath,
@@ -16604,8 +16507,16 @@ var SessionSupervisor = class {
16604
16507
  });
16605
16508
  l.ms = ms;
16606
16509
  this.wire(l, ms);
16510
+ const piStart = Date.now();
16607
16511
  try {
16608
16512
  await ms.start();
16513
+ this.opts.log.info({
16514
+ sessionId: rec.id,
16515
+ piStartMs: Date.now() - piStart,
16516
+ adaptersMs,
16517
+ totalMs: Date.now() - t0,
16518
+ transport: ms.transport
16519
+ }, "session started");
16609
16520
  const socketPath = ms.terminalSocket;
16610
16521
  rec.terminal = socketPath ? { socketPath } : null;
16611
16522
  this.db.tx(() => this.saveAndEmit(rec));
@@ -16755,7 +16666,7 @@ var SessionSupervisor = class {
16755
16666
  };
16756
16667
  }
16757
16668
  async readNative(sessionFile) {
16758
- if (!existsSync13(sessionFile)) {
16669
+ if (!existsSync12(sessionFile)) {
16759
16670
  throw new SwitchyardError("native_session_missing", `Native session file not found: ${sessionFile}`);
16760
16671
  }
16761
16672
  return readNativeTranscript(sessionFile, { piBin: this.opts.piBin });
@@ -17013,6 +16924,21 @@ var SessionSupervisor = class {
17013
16924
  await this.applyToolCard(l, e);
17014
16925
  return;
17015
16926
  }
16927
+ case "tool_execution_update": {
16928
+ if (ev.parentToolCallId || !adapters)
16929
+ return;
16930
+ const id = String(ev.toolCallId);
16931
+ const pr = ev.partialResult;
16932
+ const details = pr && typeof pr === "object" && "details" in pr ? pr.details : void 0;
16933
+ await adapters.onToolUpdate({
16934
+ toolCallId: id,
16935
+ name: String(ev.toolName),
16936
+ args: ms?.toolItem(id)?.data.args ?? {},
16937
+ partialResult: pr,
16938
+ details
16939
+ });
16940
+ return;
16941
+ }
17016
16942
  case "tool_execution_end": {
17017
16943
  if (ev.parentToolCallId || !adapters)
17018
16944
  return;
@@ -17215,6 +17141,8 @@ var SessionSupervisor = class {
17215
17141
  */
17216
17142
  onTerminalPrompt(l, b) {
17217
17143
  if (b.type === "ui_prompt_start") {
17144
+ if (b.kind === "custom" && b.remoteBacked)
17145
+ return;
17218
17146
  if (b.kind !== "custom" && l.ms?.remoteDialogs)
17219
17147
  return;
17220
17148
  if (l.terminalPrompt)
@@ -17541,9 +17469,10 @@ var WorkspaceService = class {
17541
17469
  }
17542
17470
  async create(checkoutPath, branch, base, setupCommand, worktreeDir) {
17543
17471
  const wt = await createWorktree(checkoutPath, branch, base, worktreeDir);
17544
- const git = await worktreeStatus(wt.path, wt.baseBranch).catch(() => null);
17545
17472
  const initial = setupCommand ? { state: "running", log: "" } : { state: "none", log: "" };
17546
- this.emitUpdate(wt.path, { branch: wt.branch, baseBranch: wt.baseBranch, git, setup: initial });
17473
+ this.emitUpdate(wt.path, { branch: wt.branch, baseBranch: wt.baseBranch, setup: initial });
17474
+ void worktreeStatus(wt.path, wt.baseBranch).then((git) => this.emitUpdate(wt.path, { git }), () => {
17475
+ });
17547
17476
  let setupDone = Promise.resolve(initial);
17548
17477
  if (setupCommand) {
17549
17478
  setupDone = runSetup(wt.path, setupCommand, (setup) => this.emitUpdate(wt.path, { setup }), {
@@ -17565,7 +17494,7 @@ var WorkspaceService = class {
17565
17494
  branch: wt.branch,
17566
17495
  baseBranch: wt.baseBranch,
17567
17496
  provenance: "managed",
17568
- git,
17497
+ git: null,
17569
17498
  setup: initial,
17570
17499
  setupDone
17571
17500
  };
@@ -17711,16 +17640,16 @@ async function runHostAgent(opts = {}) {
17711
17640
  log.warn({ sessions: interruptedOnStart.length, dispatches: failedDispatches }, "recovered after restart");
17712
17641
  }
17713
17642
  router.pruneReceipts();
17714
- const reattach = await supervisor.reattachTerminals();
17715
- interruptedOnStart.push(...reattach.lost);
17716
- if (reattach.reattached.length > 0 || reattach.lost.length > 0) {
17717
- log.info({ reattached: reattach.reattached.length, lost: reattach.lost.length }, "terminal sessions after restart");
17718
- }
17719
17643
  try {
17720
17644
  await catalog.init();
17721
17645
  } catch (err) {
17722
17646
  log.warn({ err: err.message }, "adapter discovery failed; Pi package adapters are not loaded");
17723
17647
  }
17648
+ const reattach = await supervisor.reattachTerminals();
17649
+ interruptedOnStart.push(...reattach.lost);
17650
+ if (reattach.reattached.length > 0 || reattach.lost.length > 0) {
17651
+ log.info({ reattached: reattach.reattached.length, lost: reattach.lost.length }, "terminal sessions after restart");
17652
+ }
17724
17653
  supervisor.emitAdapters();
17725
17654
  link.start();
17726
17655
  let stopping = null;
@@ -17762,15 +17691,445 @@ async function runHostAgent(opts = {}) {
17762
17691
  return handle;
17763
17692
  }
17764
17693
 
17765
- // packages/cli/dist/commands/doctor.js
17766
- function defaultDoctorDeps(ctx) {
17767
- const dataDir = ctx.paths?.dataDir ?? defaultDataDir(ctx.env);
17768
- return {
17769
- nodeVersion: process.versions.node,
17770
- detectPi: () => detectPi(ctx.env.SWITCHYARD_PI_BIN ?? "pi"),
17771
- gitVersion: () => new Promise((resolve13) => execFile3("git", ["--version"], (err, stdout) => resolve13(err ? null : stdout.trim()))),
17694
+ // packages/cli/dist/commands/coordinator.js
17695
+ import { existsSync as existsSync13, mkdirSync as mkdirSync8 } from "node:fs";
17696
+ import { resolve as resolve10 } from "node:path";
17697
+ function parseListen(v) {
17698
+ const s = v.trim();
17699
+ let host = "0.0.0.0";
17700
+ let portStr = s;
17701
+ const v6 = s.match(/^\[([^\]]+)\]:(\d+)$/);
17702
+ if (v6) {
17703
+ host = v6[1];
17704
+ portStr = v6[2];
17705
+ } else if (s.includes(":")) {
17706
+ const i = s.lastIndexOf(":");
17707
+ host = s.slice(0, i) || "0.0.0.0";
17708
+ portStr = s.slice(i + 1);
17709
+ } else if (!/^\d+$/.test(s)) {
17710
+ host = s;
17711
+ portStr = String(DEFAULT_PORT);
17712
+ }
17713
+ const port = Number(portStr);
17714
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
17715
+ throw new CliError(`Invalid listen address "${v}" (expected host:port)`);
17716
+ }
17717
+ return { host, port };
17718
+ }
17719
+ function parseOrigins(v) {
17720
+ if (!v?.trim())
17721
+ return void 0;
17722
+ return v.split(",").map((o) => o.trim().replace(/\/+$/, "")).filter(Boolean);
17723
+ }
17724
+ var DEFAULT_OIDC_LABEL = "Sign in with single sign-on";
17725
+ var DEFAULT_OIDC_SCOPES = "openid profile email groups";
17726
+ function envValue(env, key) {
17727
+ const v = env[key]?.trim();
17728
+ return v ? v : void 0;
17729
+ }
17730
+ function authOptionsFromEnv(env) {
17731
+ const out = {};
17732
+ const secret = envValue(env, "SWITCHYARD_HOST_ENROLL_SECRET");
17733
+ if (secret)
17734
+ out.hostEnrollSecret = secret;
17735
+ const proxies = parseOrigins(env.SWITCHYARD_TRUSTED_PROXIES);
17736
+ if (proxies)
17737
+ out.trustedProxies = proxies;
17738
+ const issuer = envValue(env, "SWITCHYARD_OIDC_ISSUER");
17739
+ const clientId = envValue(env, "SWITCHYARD_OIDC_CLIENT_ID");
17740
+ if (issuer && clientId) {
17741
+ if (!/^https?:\/\//.test(issuer)) {
17742
+ throw new CliError(`SWITCHYARD_OIDC_ISSUER must be an http(s) URL, got "${issuer}"`);
17743
+ }
17744
+ out.oidc = {
17745
+ issuer,
17746
+ clientId,
17747
+ clientSecret: envValue(env, "SWITCHYARD_OIDC_CLIENT_SECRET") ?? null,
17748
+ label: envValue(env, "SWITCHYARD_OIDC_LABEL") ?? DEFAULT_OIDC_LABEL,
17749
+ scopes: envValue(env, "SWITCHYARD_OIDC_SCOPES") ?? DEFAULT_OIDC_SCOPES,
17750
+ owner: {
17751
+ subject: envValue(env, "SWITCHYARD_OIDC_OWNER_SUBJECT") ?? null,
17752
+ email: envValue(env, "SWITCHYARD_OIDC_OWNER_EMAIL") ?? null,
17753
+ group: envValue(env, "SWITCHYARD_OIDC_OWNER_GROUP") ?? null
17754
+ }
17755
+ };
17756
+ } else if (issuer || clientId) {
17757
+ throw new CliError("Single sign-on needs both SWITCHYARD_OIDC_ISSUER and SWITCHYARD_OIDC_CLIENT_ID");
17758
+ }
17759
+ return out;
17760
+ }
17761
+ function coordinatorOptions(flags, env) {
17762
+ const dataDir = flags.dataDir ? resolve10(flags.dataDir) : defaultDataDir(env);
17763
+ const listen = parseListen(flags.listen ?? env.SWITCHYARD_LISTEN ?? `0.0.0.0:${DEFAULT_PORT}`);
17764
+ const publicUrl = (flags.publicUrl ?? env.SWITCHYARD_PUBLIC_URL)?.trim() || void 0;
17765
+ if (publicUrl && !/^https?:\/\//.test(publicUrl)) {
17766
+ throw new CliError(`Public URL must start with http:// or https://, got "${publicUrl}"`);
17767
+ }
17768
+ const trustedOrigins = parseOrigins(flags.trustedOrigins ?? env.SWITCHYARD_TRUSTED_ORIGINS);
17769
+ return {
17770
+ dataDir,
17771
+ listen,
17772
+ publicUrl,
17773
+ trustedOrigins,
17774
+ webDist: resolveWebDist(flags.webDist, env),
17775
+ ...authOptionsFromEnv(env)
17776
+ };
17777
+ }
17778
+ function waitForSignal() {
17779
+ return new Promise((res) => {
17780
+ const on = (sig) => {
17781
+ process.off("SIGINT", on);
17782
+ process.off("SIGTERM", on);
17783
+ res(sig);
17784
+ };
17785
+ process.on("SIGINT", on);
17786
+ process.on("SIGTERM", on);
17787
+ });
17788
+ }
17789
+ async function coordinatorStart(ctx, flags) {
17790
+ const opts = coordinatorOptions(flags, ctx.env);
17791
+ const c = createCoordinator(opts);
17792
+ await c.start();
17793
+ writeEndpoint(opts.dataDir, {
17794
+ localUrl: localUrlFor(opts.listen, c.port),
17795
+ publicUrl: c.config.publicUrl
17796
+ });
17797
+ ctx.err(`Switchyard coordinator listening on ${opts.listen.host}:${c.port} (public URL ${c.config.publicUrl}; data ${opts.dataDir}${opts.webDist ? "" : "; web app not found, API only"})`);
17798
+ const sig = await waitForSignal();
17799
+ ctx.err(`${sig}: stopping coordinator`);
17800
+ await c.stop();
17801
+ }
17802
+ function localUrlFor(listen, port) {
17803
+ const h = listen.host ?? "0.0.0.0";
17804
+ const host = h === "0.0.0.0" || h === "::" || h === "" ? "127.0.0.1" : h.includes(":") ? `[${h}]` : h;
17805
+ return `http://${host}:${port}`;
17806
+ }
17807
+ function dataDirOf(flags, env) {
17808
+ return flags.dataDir ? resolve10(flags.dataDir) : defaultDataDir(env);
17809
+ }
17810
+ function coordinatorBackup(ctx, out, flags) {
17811
+ const r = backup(dataDirOf(flags, ctx.env), out);
17812
+ ctx.out(`Backed up to ${r.file} (schema ${r.migrationLevel})`);
17813
+ }
17814
+ function coordinatorRestore(ctx, file, flags) {
17815
+ const r = restore(dataDirOf(flags, ctx.env), file);
17816
+ ctx.out(`Restored ${r.restoredFrom} (schema ${r.migrationLevel}).`);
17817
+ if (r.previous)
17818
+ ctx.out(`The previous database was kept as ${r.previous}`);
17819
+ if (r.migrationLevel < LATEST_MIGRATION) {
17820
+ ctx.out("Migrations to the current schema run when the coordinator next starts.");
17821
+ }
17822
+ }
17823
+ function coordinatorMigrate(ctx, flags) {
17824
+ const dataDir = dataDirOf(flags, ctx.env);
17825
+ const holder = lockHolder(dataDir);
17826
+ if (holder !== null) {
17827
+ throw new CliError(`Coordinator (pid ${holder}) is running on ${dataDir}; it migrated at start`);
17828
+ }
17829
+ const path = dbPath(dataDir);
17830
+ const existed = existsSync13(path);
17831
+ mkdirSync8(dataDir, { recursive: true });
17832
+ const db = openDb(path);
17833
+ try {
17834
+ const before = existed ? migrationLevel(db) : 0;
17835
+ migrate(db);
17836
+ const after = migrationLevel(db);
17837
+ ctx.out(before === after ? `Schema is current (level ${after}) at ${path}` : `Migrated ${path} from level ${before} to ${after}`);
17838
+ } finally {
17839
+ db.close();
17840
+ }
17841
+ }
17842
+
17843
+ // packages/cli/dist/commands/up.js
17844
+ var DEV_ORIGINS = ["http://localhost:5173", "http://127.0.0.1:5173"];
17845
+ function upOptions(flags, env) {
17846
+ const port = flags.port === void 0 ? DEFAULT_PORT : Number(flags.port);
17847
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
17848
+ throw new CliError(`Invalid port "${flags.port}"`);
17849
+ }
17850
+ const listenHost = flags.host ?? "0.0.0.0";
17851
+ const dataDir = flags.dataDir ?? defaultDataDir(env);
17852
+ const explicitPublic = (flags.publicUrl ?? env.SWITCHYARD_PUBLIC_URL)?.trim();
17853
+ const publicUrl = explicitPublic || port !== 0 ? (explicitPublic || `http://localhost:${port}`).replace(/\/+$/, "") : void 0;
17854
+ let trusted = parseOrigins(flags.trustedOrigins ?? env.SWITCHYARD_TRUSTED_ORIGINS);
17855
+ if (!trusted && publicUrl) {
17856
+ trusted = [new URL(publicUrl).origin];
17857
+ if (!explicitPublic)
17858
+ trusted.push(`http://127.0.0.1:${port}`);
17859
+ }
17860
+ if (flags.dev && trusted)
17861
+ trusted = [.../* @__PURE__ */ new Set([...trusted, ...DEV_ORIGINS])];
17862
+ return {
17863
+ dataDir,
17864
+ listen: { host: listenHost, port },
17865
+ publicUrl,
17866
+ trustedOrigins: trusted,
17867
+ webDist: resolveWebDist(flags.webDist, env),
17868
+ // Same server env as `coordinator start`: shared host secret, trusted proxies and OIDC.
17869
+ ...authOptionsFromEnv(env)
17870
+ };
17871
+ }
17872
+ async function ensureLocalHost(coordinator, localUrl, configPath, name, fetchImpl) {
17873
+ if (existsSync14(configPath)) {
17874
+ const cfg2 = loadConfig(configPath);
17875
+ const known = coordinator.store.hosts.get(cfg2.hostId);
17876
+ if (known && !known.revokedAt) {
17877
+ return { config: { ...cfg2, coordinatorUrl: localUrl }, enrolled: false };
17878
+ }
17879
+ }
17880
+ const api = new ApiClient({
17881
+ coordinatorUrl: localUrl,
17882
+ credential: coordinator.adminToken,
17883
+ fetch: fetchImpl
17884
+ });
17885
+ const e = await api.post("/api/hosts/enrollments", {});
17886
+ const cfg = await enroll({
17887
+ coordinatorUrl: localUrl,
17888
+ token: e.token,
17889
+ name,
17890
+ configPath,
17891
+ fetch: fetchImpl
17892
+ });
17893
+ const { configPath: _p, ...config } = cfg;
17894
+ return { config, enrolled: true };
17895
+ }
17896
+ function openBrowser(url, platform = process.platform, spawnFn = spawn4) {
17897
+ const cmd = platform === "darwin" ? "open" : platform === "linux" ? "xdg-open" : null;
17898
+ if (!cmd)
17899
+ return;
17900
+ try {
17901
+ const child = spawnFn(cmd, [url], { detached: true, stdio: "ignore" });
17902
+ child.on("error", () => {
17903
+ });
17904
+ child.unref();
17905
+ } catch {
17906
+ }
17907
+ }
17908
+ async function firstSignInLink(ctx, coordinator, open2, opener) {
17909
+ if (coordinator.config.oidc || coordinator.auth.activeDeviceCount() > 0)
17910
+ return null;
17911
+ const e = coordinator.auth.createEnrollment("device", ADMIN_DEVICE_ID);
17912
+ await printSignInLink(ctx, e.url, e.expiresAt);
17913
+ if (open2)
17914
+ opener(e.url);
17915
+ return e.url;
17916
+ }
17917
+ async function startStandalone(ctx, flags, deps = {}) {
17918
+ if (flags.dev && !ctx.env.SWITCHYARD_LOG_LEVEL)
17919
+ process.env.SWITCHYARD_LOG_LEVEL = "debug";
17920
+ const opts = upOptions(flags, ctx.env);
17921
+ const coordinator = createCoordinator({ ...opts, setupHint: false });
17922
+ await coordinator.start();
17923
+ let firstLink;
17924
+ try {
17925
+ firstLink = await firstSignInLink(ctx, coordinator, flags.open ?? process.stdout.isTTY === true, deps.open ?? ((url) => openBrowser(url)));
17926
+ } catch (err) {
17927
+ await coordinator.stop();
17928
+ throw err;
17929
+ }
17930
+ const localUrl = localUrlFor(opts.listen, coordinator.port);
17931
+ writeEndpoint(opts.dataDir, { localUrl, publicUrl: coordinator.config.publicUrl });
17932
+ let host;
17933
+ try {
17934
+ const paths = standaloneHostPaths(opts.dataDir);
17935
+ const { config, enrolled } = await ensureLocalHost(coordinator, localUrl, paths.configPath, flags.name ?? hostname2().replace(/\.local$/, ""));
17936
+ if (enrolled)
17937
+ ctx.err(`Enrolled the local host "${config.name}" (${config.hostId})`);
17938
+ host = await runHostAgent({ config, stateDir: paths.stateDir, handleSignals: false });
17939
+ } catch (err) {
17940
+ await coordinator.stop();
17941
+ throw err;
17942
+ }
17943
+ return {
17944
+ coordinator,
17945
+ host,
17946
+ localUrl,
17947
+ firstLink,
17948
+ async stop() {
17949
+ await host.stop();
17950
+ await coordinator.stop();
17951
+ }
17952
+ };
17953
+ }
17954
+ async function up(ctx, flags) {
17955
+ const s = await startStandalone(ctx, flags);
17956
+ const c = s.coordinator;
17957
+ ctx.err([
17958
+ `Switchyard is up: ${c.config.publicUrl} (listening on port ${c.port})`,
17959
+ `Data: ${c.config.dataDir}`,
17960
+ c.config.webDist ? null : "Web app bundle not found: API only (run `pnpm build`)",
17961
+ c.config.oidc ? `Sign in with single sign-on at ${c.config.publicUrl} Terminal: switchyard login --coordinator ${c.config.publicUrl}` : `Another browser: switchyard pair Terminal: switchyard login --coordinator ${c.config.publicUrl}`,
17962
+ "Schedules run only while this process is running."
17963
+ ].filter(Boolean).join("\n"));
17964
+ const sig = await waitForSignal();
17965
+ ctx.err(`${sig}: stopping (Pi sessions on this host are stopped and can be resumed later)`);
17966
+ await s.stop();
17967
+ }
17968
+
17969
+ // packages/cli/dist/commands/auth.js
17970
+ function adminApi(ctx, flags) {
17971
+ const dataDir = flags.dataDir ?? ctx.paths?.dataDir ?? defaultDataDir(ctx.env);
17972
+ const token = readAdminToken(dataDir, ctx.env);
17973
+ if (!token) {
17974
+ throw new CliError(`No admin token: run this on the coordinator machine (looked in ${dataDir}/admin.token) or set SWITCHYARD_ADMIN_TOKEN`);
17975
+ }
17976
+ const url = trimUrl(flags.coordinator ?? readEndpoint(dataDir)?.localUrl ?? DEFAULT_LOCAL_URL);
17977
+ return new ApiClient({ coordinatorUrl: url, credential: token, fetch: ctx.fetch });
17978
+ }
17979
+ function clockTime(iso) {
17980
+ const d = new Date(iso);
17981
+ return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
17982
+ }
17983
+ async function printDeviceLink(ctx, url) {
17984
+ const qr = await QRCode2.toString(url, { type: "terminal", small: true });
17985
+ ctx.out("");
17986
+ ctx.out(` ${url}`);
17987
+ ctx.out("");
17988
+ ctx.out(qr.trimEnd());
17989
+ ctx.out("");
17990
+ }
17991
+ async function printSignInLink(ctx, url, expiresAt) {
17992
+ ctx.out(`Open this single-use sign-in link in your browser (expires ${clockTime(expiresAt)}):`);
17993
+ await printDeviceLink(ctx, url);
17994
+ ctx.out("Then add a passkey in Settings \u203A Sign-in as a backup, or run `switchyard pair` for another browser.");
17995
+ }
17996
+ async function pair(ctx, flags) {
17997
+ const api = adminApi(ctx, flags);
17998
+ const kind = flags.host ? "host" : "device";
17999
+ const e = await api.post("/api/enrollments", { kind });
18000
+ if (flags.json) {
18001
+ ctx.out(JSON.stringify(e, null, 2));
18002
+ return e;
18003
+ }
18004
+ const qr = await QRCode2.toString(e.url, { type: "terminal", small: true });
18005
+ if (kind === "host") {
18006
+ ctx.out(`Host enrollment (single use, expires ${e.expiresAt}). On the host, run:`);
18007
+ ctx.out("");
18008
+ ctx.out(` ${e.command ?? `switchyard host enroll --coordinator <url> --token ${e.token}`}`);
18009
+ ctx.out("");
18010
+ ctx.out(`Token: ${e.token}`);
18011
+ } else {
18012
+ ctx.out(`Open this link in a browser to sign it in (single use, expires ${e.expiresAt}):`);
18013
+ ctx.out("");
18014
+ ctx.out(` ${e.url}`);
18015
+ ctx.out("");
18016
+ ctx.out(qr.trimEnd());
18017
+ ctx.out("");
18018
+ ctx.out(`Terminal: switchyard login --coordinator ${originOf2(e.url)} --token ${e.token}`);
18019
+ }
18020
+ ctx.out("The link does not provide network access: the device must already reach this URL.");
18021
+ return e;
18022
+ }
18023
+ function originOf2(url) {
18024
+ try {
18025
+ return new URL(url).origin;
18026
+ } catch {
18027
+ return url;
18028
+ }
18029
+ }
18030
+ function parseEnrollmentInput(token, coordinator) {
18031
+ const t = token.trim();
18032
+ if (/^https?:\/\//.test(t)) {
18033
+ const u = new URL(t);
18034
+ const params = new URLSearchParams(u.hash.replace(/^#/, ""));
18035
+ const fromUrl = params.get("token") ?? u.searchParams.get("token");
18036
+ if (!fromUrl)
18037
+ throw new CliError("That URL has no enrollment token");
18038
+ return { url: trimUrl(coordinator ?? u.origin), token: fromUrl };
18039
+ }
18040
+ if (!coordinator)
18041
+ throw new CliError("--coordinator <url> is required");
18042
+ return { url: trimUrl(coordinator), token: t };
18043
+ }
18044
+ async function loginWithToken(ctx, flags) {
18045
+ const { url, token } = parseEnrollmentInput(flags.token, flags.coordinator);
18046
+ const api = new ApiClient({ coordinatorUrl: url, credential: null, fetch: ctx.fetch });
18047
+ const name = flags.name ?? `${hostname3().replace(/\.local$/, "")} terminal`;
18048
+ const res = await api.post("/api/enroll", {
18049
+ token,
18050
+ name,
18051
+ kind: "terminal"
18052
+ });
18053
+ if (!res.credential)
18054
+ throw new CliError("The coordinator returned no credential");
18055
+ const path = ctx.paths?.clientConfig ?? clientConfigPath(ctx.env);
18056
+ writeClientConfig(path, {
18057
+ coordinatorUrl: url,
18058
+ credential: res.credential,
18059
+ deviceId: res.deviceId
18060
+ });
18061
+ ctx.out(`Enrolled "${name}" (${res.deviceId}) with ${url}.`);
18062
+ ctx.out(`Credential saved to ${path} (mode 0600). Open a session's Pi terminal with \`switchyard tui <session>\`.`);
18063
+ return { path, deviceId: res.deviceId };
18064
+ }
18065
+ var EXPIRED2 = "The sign-in expired before it was approved. Run `switchyard login` again.";
18066
+ async function login(ctx, flags, deps = {}) {
18067
+ if (flags.token)
18068
+ return loginWithToken(ctx, { ...flags, token: flags.token });
18069
+ const path = ctx.paths?.clientConfig ?? clientConfigPath(ctx.env);
18070
+ const raw = flags.coordinator ?? ctx.env.SWITCHYARD_URL ?? readClientConfig(path)?.coordinatorUrl;
18071
+ if (!raw)
18072
+ throw new CliError("--coordinator <url> is required");
18073
+ const url = trimUrl(raw);
18074
+ const api = new ApiClient({ coordinatorUrl: url, credential: null, fetch: ctx.fetch });
18075
+ const name = flags.name ?? `${hostname3().replace(/\.local$/, "")} terminal`;
18076
+ const started = await api.post("/api/auth/terminal/start", { name });
18077
+ ctx.out("To sign in this terminal, open this link in a browser where you use Switchyard:");
18078
+ ctx.out("");
18079
+ ctx.out(` ${started.verifyUrl}`);
18080
+ ctx.out("");
18081
+ ctx.out(`Check that it shows the code ${started.userCode}. Waiting for approval (expires ${clockTime(started.expiresAt)})\u2026`);
18082
+ const interactive = process.stdout.isTTY === true && !ctx.env.SSH_CONNECTION && !ctx.env.SSH_TTY;
18083
+ if (flags.open ?? interactive)
18084
+ (deps.open ?? ((u) => openBrowser(u)))(started.verifyUrl);
18085
+ const sleep3 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
18086
+ const now = deps.now ?? Date.now;
18087
+ const deadline = Date.parse(started.expiresAt);
18088
+ for (; ; ) {
18089
+ await sleep3(started.intervalMs);
18090
+ let r;
18091
+ try {
18092
+ r = await api.post("/api/auth/terminal/claim", {
18093
+ userCode: started.userCode,
18094
+ pollToken: started.pollToken
18095
+ });
18096
+ } catch (err) {
18097
+ if (err instanceof ApiRequestError) {
18098
+ if (err.status === 401)
18099
+ throw new CliError(EXPIRED2);
18100
+ if (err.status === 0 && now() < deadline)
18101
+ continue;
18102
+ }
18103
+ throw err;
18104
+ }
18105
+ if (r.status === "denied")
18106
+ throw new CliError("The sign-in was denied in the browser.");
18107
+ if (r.status === "done") {
18108
+ writeClientConfig(path, {
18109
+ coordinatorUrl: url,
18110
+ credential: r.credential,
18111
+ deviceId: r.deviceId
18112
+ });
18113
+ ctx.out(`Signed in "${name}" (${r.deviceId}) to ${url}.`);
18114
+ ctx.out(`Credential saved to ${path} (mode 0600). Open a session's Pi terminal with \`switchyard tui <session>\`.`);
18115
+ return { path, deviceId: r.deviceId };
18116
+ }
18117
+ if (now() >= deadline)
18118
+ throw new CliError(EXPIRED2);
18119
+ }
18120
+ }
18121
+
18122
+ // packages/cli/dist/commands/doctor.js
18123
+ import { execFile as execFile3 } from "node:child_process";
18124
+ import { existsSync as existsSync15, readFileSync as readFileSync9 } from "node:fs";
18125
+ function defaultDoctorDeps(ctx) {
18126
+ const dataDir = ctx.paths?.dataDir ?? defaultDataDir(ctx.env);
18127
+ return {
18128
+ nodeVersion: process.versions.node,
18129
+ detectPi: () => detectPi(ctx.env.SWITCHYARD_PI_BIN ?? "pi"),
18130
+ gitVersion: () => new Promise((resolve13) => execFile3("git", ["--version"], (err, stdout) => resolve13(err ? null : stdout.trim()))),
17772
18131
  hostConfigPaths: [defaultConfigPath(ctx.env), standaloneHostPaths(dataDir).configPath],
17773
- readFile: (p) => existsSync14(p) ? readFileSync9(p, "utf8") : null
18132
+ readFile: (p) => existsSync15(p) ? readFileSync9(p, "utf8") : null
17774
18133
  };
17775
18134
  }
17776
18135
  function findHostConfig(deps) {
@@ -17900,13 +18259,13 @@ async function checkSignIn(ctx, url, add) {
17900
18259
  try {
17901
18260
  const api = new ApiClient({ coordinatorUrl: url, credential: null, fetch: ctx.fetch });
17902
18261
  const s = await api.get("/api/setup");
17903
- if (!s.initialized) {
17904
- add("FAIL", "Sign-in", "not set up: run `switchyard init` on the server");
17905
- return;
17906
- }
17907
18262
  const passkey = !s.passkey.available ? `unavailable, ${s.passkey.reason ?? "unsupported"}` : s.passkey.registered ? "registered" : "none registered yet";
17908
18263
  const sso = s.oidc ? `on (${s.oidc.label})` : "off";
17909
- add("PASS", "Sign-in", `set up (passkey: ${passkey}, single sign-on: ${sso})`);
18264
+ if (!s.oidc && !s.passkey.registered) {
18265
+ add("WARN", "Sign-in", "no single sign-on and no passkey: sign in with a link from `switchyard pair` on the server");
18266
+ return;
18267
+ }
18268
+ add("PASS", "Sign-in", `passkey: ${passkey}, single sign-on: ${sso}`);
17910
18269
  } catch (err) {
17911
18270
  add("WARN", "Sign-in", `could not read the setup state: ${err.message}`);
17912
18271
  }
@@ -17922,8 +18281,8 @@ function printDoctor(ctx, results) {
17922
18281
  }
17923
18282
 
17924
18283
  // packages/cli/dist/commands/herdr.js
17925
- import { spawn as spawn4 } from "node:child_process";
17926
- import { closeSync as closeSync2, existsSync as existsSync18, openSync as openSync2, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "node:fs";
18284
+ import { spawn as spawn5 } from "node:child_process";
18285
+ import { closeSync as closeSync2, existsSync as existsSync19, openSync as openSync2, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "node:fs";
17927
18286
  import { join as join21 } from "node:path";
17928
18287
 
17929
18288
  // packages/cli/dist/context.js
@@ -17934,7 +18293,7 @@ function defaultCtx() {
17934
18293
  err: (l) => process.stderr.write(`${l}
17935
18294
  `),
17936
18295
  env: process.env,
17937
- fetch: (input, init2) => fetch(input, init2)
18296
+ fetch: (input, init) => fetch(input, init)
17938
18297
  };
17939
18298
  }
17940
18299
  function connect(ctx, flags) {
@@ -17975,7 +18334,7 @@ function ago(iso, now = Date.now()) {
17975
18334
  }
17976
18335
 
17977
18336
  // packages/cli/dist/herdr/client.js
17978
- import { existsSync as existsSync15, readdirSync } from "node:fs";
18337
+ import { existsSync as existsSync16, readdirSync } from "node:fs";
17979
18338
  import { createConnection as createConnection3 } from "node:net";
17980
18339
  import { homedir as homedir6 } from "node:os";
17981
18340
  import { join as join17 } from "node:path";
@@ -18000,11 +18359,11 @@ function herdrSocketPath(env = process.env, home = homedir6()) {
18000
18359
  if (env.HERDR_SESSION)
18001
18360
  return join17(base, "sessions", env.HERDR_SESSION, "herdr.sock");
18002
18361
  const dflt = join17(base, "herdr.sock");
18003
- if (existsSync15(dflt))
18362
+ if (existsSync16(dflt))
18004
18363
  return dflt;
18005
18364
  let live = [];
18006
18365
  try {
18007
- live = readdirSync(join17(base, "sessions")).map((s) => join17(base, "sessions", s, "herdr.sock")).filter((p) => existsSync15(p));
18366
+ live = readdirSync(join17(base, "sessions")).map((s) => join17(base, "sessions", s, "herdr.sock")).filter((p) => existsSync16(p));
18008
18367
  } catch {
18009
18368
  live = [];
18010
18369
  }
@@ -18117,7 +18476,7 @@ function nextSeq() {
18117
18476
  }
18118
18477
 
18119
18478
  // packages/cli/dist/herdr/daemon.js
18120
- import { existsSync as existsSync17, mkdirSync as mkdirSync10, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "node:fs";
18479
+ import { existsSync as existsSync18, mkdirSync as mkdirSync10, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "node:fs";
18121
18480
  import { join as join19 } from "node:path";
18122
18481
 
18123
18482
  // packages/cli/dist/herdr/mirror.js
@@ -18404,7 +18763,7 @@ function planMirror(st, h, mem, now) {
18404
18763
  }
18405
18764
 
18406
18765
  // packages/cli/dist/herdr/util.js
18407
- import { chmodSync as chmodSync5, existsSync as existsSync16, mkdirSync as mkdirSync9, writeFileSync as writeFileSync5 } from "node:fs";
18766
+ import { chmodSync as chmodSync5, existsSync as existsSync17, mkdirSync as mkdirSync9, writeFileSync as writeFileSync5 } from "node:fs";
18408
18767
  import { homedir as homedir7 } from "node:os";
18409
18768
  import { join as join18 } from "node:path";
18410
18769
  function herdrStateDir(env = process.env, home = homedir7()) {
@@ -18434,7 +18793,7 @@ exec sh '${pluginRoot}/switchyard-herdr.sh' "$@"
18434
18793
  return path;
18435
18794
  }
18436
18795
  function launcherExists(env = process.env) {
18437
- return existsSync16(stableLauncherPath(env));
18796
+ return existsSync17(stableLauncherPath(env));
18438
18797
  }
18439
18798
  async function isAtShell(call, paneId) {
18440
18799
  const info = (await call("pane.process_info", { pane_id: paneId }))?.process_info ?? {};
@@ -18633,7 +18992,7 @@ var MirrorDaemon = class {
18633
18992
  const titleOf = (sid) => st.sessions.find((s) => s.id === sid)?.title ?? sid;
18634
18993
  const launcher = this.deps.launcherPath;
18635
18994
  const paneCmd = (sid) => launcherCommand(launcher, ["pane", sid]);
18636
- const exists2 = this.deps.exists ?? existsSync17;
18995
+ const exists2 = this.deps.exists ?? existsSync18;
18637
18996
  const createdNow = {};
18638
18997
  const fallbackCwd = () => {
18639
18998
  const dir = join19(this.deps.stateDir, "spaces");
@@ -19739,13 +20098,13 @@ async function herdrDaemon(ctx, flags) {
19739
20098
  }
19740
20099
  return;
19741
20100
  }
19742
- const running = existsSync18(pidFile) ? Number(readFileSync12(pidFile, "utf8").trim()) : 0;
20101
+ const running = existsSync19(pidFile) ? Number(readFileSync12(pidFile, "utf8").trim()) : 0;
19743
20102
  if (flags.ensure) {
19744
20103
  if (running && pidAlive2(running))
19745
20104
  return;
19746
20105
  const fd = openSync2(logFile, "a", 384);
19747
20106
  try {
19748
- const child = spawn4(process.execPath, [process.argv[1], "herdr", "daemon"], {
20107
+ const child = spawn5(process.execPath, [process.argv[1], "herdr", "daemon"], {
19749
20108
  detached: true,
19750
20109
  stdio: ["ignore", fd, fd],
19751
20110
  env: { ...process.env, SWITCHYARD_HERDR_LAUNCHER: launcherPath }
@@ -19807,14 +20166,14 @@ async function herdrStatus(ctx, flags) {
19807
20166
  }
19808
20167
 
19809
20168
  // packages/cli/dist/commands/host.js
19810
- import { existsSync as existsSync20, mkdirSync as mkdirSync12 } from "node:fs";
19811
- import { hostname as hostname3 } from "node:os";
20169
+ import { existsSync as existsSync21, mkdirSync as mkdirSync12 } from "node:fs";
20170
+ import { hostname as hostname4 } from "node:os";
19812
20171
  import { dirname as dirname10, join as join23, resolve as resolve12 } from "node:path";
19813
20172
  import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
19814
20173
 
19815
20174
  // packages/cli/dist/commands/service.js
19816
20175
  import { execFile as execFile4 } from "node:child_process";
19817
- import { existsSync as existsSync19, mkdirSync as mkdirSync11, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "node:fs";
20176
+ import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "node:fs";
19818
20177
  import { homedir as homedir8 } from "node:os";
19819
20178
  import { dirname as dirname9, join as join22, resolve as resolve11 } from "node:path";
19820
20179
  var LAUNCHD_LABEL = "com.switchyard.host";
@@ -19906,7 +20265,7 @@ function systemdEscape(s, exec = true) {
19906
20265
  }
19907
20266
  function loadTemplate(kind, dir = deployDir()) {
19908
20267
  const p = join22(dir, TEMPLATE_FILES[kind]);
19909
- return existsSync19(p) ? readFileSync13(p, "utf8") : TEMPLATES[kind];
20268
+ return existsSync20(p) ? readFileSync13(p, "utf8") : TEMPLATES[kind];
19910
20269
  }
19911
20270
  function renderService(kind, vars, template = TEMPLATES[kind]) {
19912
20271
  for (const [k, v] of Object.entries(vars)) {
@@ -19950,7 +20309,7 @@ function loadCommands(kind, file, uid, instance) {
19950
20309
  ["systemctl", "--user", "restart", unit]
19951
20310
  ];
19952
20311
  }
19953
- function stableNodePath(execPath, exists2 = existsSync19) {
20312
+ function stableNodePath(execPath, exists2 = existsSync20) {
19954
20313
  const m = execPath.match(/^(.*)\/Cellar\/node(?:@[^/]+)?\/[^/]+\/bin\/node$/);
19955
20314
  if (m) {
19956
20315
  const candidate = `${m[1]}/bin/node`;
@@ -19974,7 +20333,7 @@ async function installService(ctx, flags, deps = defaultInstallDeps()) {
19974
20333
  throw new CliError("--instance needs --config and --state-dir, so the second host agent keeps its own enrollment and state");
19975
20334
  }
19976
20335
  const config = flags.config ?? defaultConfigPath(ctx.env, deps.home);
19977
- if (!existsSync19(config) && !flags.print) {
20336
+ if (!existsSync20(config) && !flags.print) {
19978
20337
  throw new CliError(`No host config at ${config}. Enroll first: switchyard host enroll --coordinator <url> --token <t>`);
19979
20338
  }
19980
20339
  const stateDir = flags.stateDir ? resolve11(flags.stateDir) : defaultStateDir(ctx.env, deps.platform, deps.home);
@@ -20024,7 +20383,7 @@ async function hostEnroll(ctx, flags) {
20024
20383
  if (!flags.token === !flags.secret) {
20025
20384
  throw new CliError("Pass exactly one of --token or --secret");
20026
20385
  }
20027
- const name = flags.name ?? hostname3().replace(/\.local$/, "");
20386
+ const name = flags.name ?? hostname4().replace(/\.local$/, "");
20028
20387
  const cfg = await enroll({
20029
20388
  coordinatorUrl: flags.coordinator,
20030
20389
  ...flags.token ? { token: flags.token } : { secret: flags.secret },
@@ -20038,7 +20397,7 @@ async function hostEnroll(ctx, flags) {
20038
20397
  }
20039
20398
  async function hostRun(ctx, flags) {
20040
20399
  const configPath = flags.config ?? defaultConfigPath(ctx.env);
20041
- if (!existsSync20(configPath)) {
20400
+ if (!existsSync21(configPath)) {
20042
20401
  ctx.err(`No host config at ${configPath} yet: waiting for it (with SWITCHYARD_HOST_ENROLL_SECRET and SWITCHYARD_COORDINATOR_URL set, the host enrolls itself).`);
20043
20402
  }
20044
20403
  const agent = await runHostAgent({
@@ -20054,10 +20413,10 @@ async function hostRun(ctx, flags) {
20054
20413
  function hostBackup(ctx, out, flags) {
20055
20414
  const stateDir = flags.stateDir ?? defaultStateDir(ctx.env);
20056
20415
  const src = join23(stateDir, "host.db");
20057
- if (!existsSync20(src))
20416
+ if (!existsSync21(src))
20058
20417
  throw new CliError(`No host database at ${src}`);
20059
20418
  const dest = resolve12(out);
20060
- if (existsSync20(dest))
20419
+ if (existsSync21(dest))
20061
20420
  throw new CliError(`${dest} already exists`);
20062
20421
  mkdirSync12(dirname10(dest), { recursive: true });
20063
20422
  const db = new DatabaseSync4(src);
@@ -20074,7 +20433,7 @@ async function hostStatus(ctx, flags) {
20074
20433
  const configPath = flags.config ?? defaultConfigPath(ctx.env);
20075
20434
  const stateDir = flags.stateDir ?? defaultStateDir(ctx.env);
20076
20435
  const status = { configPath, stateDir };
20077
- if (!existsSync20(configPath)) {
20436
+ if (!existsSync21(configPath)) {
20078
20437
  status.enrolled = false;
20079
20438
  } else {
20080
20439
  const cfg = loadConfig(configPath);
@@ -20107,7 +20466,7 @@ async function hostStatus(ctx, flags) {
20107
20466
  try {
20108
20467
  const kind = serviceKindFor(process.platform);
20109
20468
  const file = serviceFilePath(kind);
20110
- status.service = { kind, file, installed: existsSync20(file) };
20469
+ status.service = { kind, file, installed: existsSync21(file) };
20111
20470
  } catch {
20112
20471
  status.service = null;
20113
20472
  }
@@ -20259,136 +20618,6 @@ async function tuiCommand(ctx, sessionId, flags, run = runRawTerminal) {
20259
20618
  throw new CliError(result.message);
20260
20619
  }
20261
20620
 
20262
- // packages/cli/dist/commands/up.js
20263
- import { spawn as spawn5 } from "node:child_process";
20264
- import { existsSync as existsSync21 } from "node:fs";
20265
- import { hostname as hostname4 } from "node:os";
20266
- var DEV_ORIGINS = ["http://localhost:5173", "http://127.0.0.1:5173"];
20267
- function upOptions(flags, env) {
20268
- const port = flags.port === void 0 ? DEFAULT_PORT : Number(flags.port);
20269
- if (!Number.isInteger(port) || port < 0 || port > 65535) {
20270
- throw new CliError(`Invalid port "${flags.port}"`);
20271
- }
20272
- const listenHost = flags.host ?? "0.0.0.0";
20273
- const dataDir = flags.dataDir ?? defaultDataDir(env);
20274
- const explicitPublic = (flags.publicUrl ?? env.SWITCHYARD_PUBLIC_URL)?.trim();
20275
- const publicUrl = explicitPublic || port !== 0 ? (explicitPublic || `http://localhost:${port}`).replace(/\/+$/, "") : void 0;
20276
- let trusted = parseOrigins(flags.trustedOrigins ?? env.SWITCHYARD_TRUSTED_ORIGINS);
20277
- if (!trusted && publicUrl) {
20278
- trusted = [new URL(publicUrl).origin];
20279
- if (!explicitPublic)
20280
- trusted.push(`http://127.0.0.1:${port}`);
20281
- }
20282
- if (flags.dev && trusted)
20283
- trusted = [.../* @__PURE__ */ new Set([...trusted, ...DEV_ORIGINS])];
20284
- return {
20285
- dataDir,
20286
- listen: { host: listenHost, port },
20287
- publicUrl,
20288
- trustedOrigins: trusted,
20289
- webDist: resolveWebDist(flags.webDist, env),
20290
- // Same server env as `coordinator start`: shared host secret, trusted proxies and OIDC.
20291
- ...authOptionsFromEnv(env)
20292
- };
20293
- }
20294
- async function ensureLocalHost(coordinator, localUrl, configPath, name, fetchImpl) {
20295
- if (existsSync21(configPath)) {
20296
- const cfg2 = loadConfig(configPath);
20297
- const known = coordinator.store.hosts.get(cfg2.hostId);
20298
- if (known && !known.revokedAt) {
20299
- return { config: { ...cfg2, coordinatorUrl: localUrl }, enrolled: false };
20300
- }
20301
- }
20302
- const api = new ApiClient({
20303
- coordinatorUrl: localUrl,
20304
- credential: coordinator.adminToken,
20305
- fetch: fetchImpl
20306
- });
20307
- const e = await api.post("/api/hosts/enrollments", {});
20308
- const cfg = await enroll({
20309
- coordinatorUrl: localUrl,
20310
- token: e.token,
20311
- name,
20312
- configPath,
20313
- fetch: fetchImpl
20314
- });
20315
- const { configPath: _p, ...config } = cfg;
20316
- return { config, enrolled: true };
20317
- }
20318
- function openBrowser(url, platform = process.platform, spawnFn = spawn5) {
20319
- const cmd = platform === "darwin" ? "open" : platform === "linux" ? "xdg-open" : null;
20320
- if (!cmd)
20321
- return;
20322
- try {
20323
- const child = spawnFn(cmd, [url], { detached: true, stdio: "ignore" });
20324
- child.on("error", () => {
20325
- });
20326
- child.unref();
20327
- } catch {
20328
- }
20329
- }
20330
- async function initializeLocally(ctx, coordinator, open2, opener) {
20331
- if (coordinator.owner.isInitialized())
20332
- return null;
20333
- coordinator.owner.markInitialized();
20334
- const e = coordinator.auth.createEnrollment("device", ADMIN_DEVICE_ID);
20335
- await printInit(ctx, { alreadyInitialized: false, enrollment: e });
20336
- if (open2)
20337
- opener(e.url);
20338
- return e.url;
20339
- }
20340
- async function startStandalone(ctx, flags, deps = {}) {
20341
- if (flags.dev && !ctx.env.SWITCHYARD_LOG_LEVEL)
20342
- process.env.SWITCHYARD_LOG_LEVEL = "debug";
20343
- const opts = upOptions(flags, ctx.env);
20344
- const coordinator = createCoordinator({ ...opts, setupHint: false });
20345
- await coordinator.start();
20346
- let firstLink;
20347
- try {
20348
- firstLink = await initializeLocally(ctx, coordinator, flags.open ?? process.stdout.isTTY === true, deps.open ?? ((url) => openBrowser(url)));
20349
- } catch (err) {
20350
- await coordinator.stop();
20351
- throw err;
20352
- }
20353
- const localUrl = localUrlFor(opts.listen, coordinator.port);
20354
- writeEndpoint(opts.dataDir, { localUrl, publicUrl: coordinator.config.publicUrl });
20355
- let host;
20356
- try {
20357
- const paths = standaloneHostPaths(opts.dataDir);
20358
- const { config, enrolled } = await ensureLocalHost(coordinator, localUrl, paths.configPath, flags.name ?? hostname4().replace(/\.local$/, ""));
20359
- if (enrolled)
20360
- ctx.err(`Enrolled the local host "${config.name}" (${config.hostId})`);
20361
- host = await runHostAgent({ config, stateDir: paths.stateDir, handleSignals: false });
20362
- } catch (err) {
20363
- await coordinator.stop();
20364
- throw err;
20365
- }
20366
- return {
20367
- coordinator,
20368
- host,
20369
- localUrl,
20370
- firstLink,
20371
- async stop() {
20372
- await host.stop();
20373
- await coordinator.stop();
20374
- }
20375
- };
20376
- }
20377
- async function up(ctx, flags) {
20378
- const s = await startStandalone(ctx, flags);
20379
- const c = s.coordinator;
20380
- ctx.err([
20381
- `Switchyard is up: ${c.config.publicUrl} (listening on port ${c.port})`,
20382
- `Data: ${c.config.dataDir}`,
20383
- c.config.webDist ? null : "Web app bundle not found: API only (run `pnpm build`)",
20384
- "Another device: switchyard pair Terminal: switchyard tui <session>",
20385
- "Schedules run only while this process is running."
20386
- ].filter(Boolean).join("\n"));
20387
- const sig = await waitForSignal();
20388
- ctx.err(`${sig}: stopping (Pi sessions on this host are stopped and can be resumed later)`);
20389
- await s.stop();
20390
- }
20391
-
20392
20621
  // packages/cli/dist/commands/work.js
20393
20622
  function byId2(list) {
20394
20623
  return new Map(list.map((x) => [x.id, x]));
@@ -20596,13 +20825,10 @@ function buildProgram(ctx = defaultCtx()) {
20596
20825
  });
20597
20826
  host.command("backup <out>").description("Consistent backup of the host database").option("--state-dir <dir>", "state dir (env SWITCHYARD_STATE_DIR)").action((out, o) => hostBackup(ctx, out, o));
20598
20827
  host.command("status").description("Show this host's enrollment, link and Pi status").option("--config <path>", "host.json path").option("--state-dir <dir>", "state dir").option("--json", "JSON output").action((o) => hostStatus(ctx, o));
20599
- program.command("init").description("Set up this server and print a single-use sign-in link for your first device").option("--coordinator <url>", "local coordinator URL (default: the running one)").option("--data-dir <dir>", "data dir holding admin.token").option("--json", "JSON output").action(async (o) => {
20600
- await init(ctx, o);
20601
- });
20602
- program.command("pair").description("Create a single-use link and QR for another device (or --host); also the recovery path when every device is lost").option("--host", "enroll a host instead of a device").option("--coordinator <url>", "local coordinator URL (default: the running one)").option("--data-dir <dir>", "data dir holding admin.token").option("--json", "JSON output").action(async (o) => {
20828
+ program.command("pair").description("Print a single-use sign-in link and QR for a browser (or --host for a host token), using the admin token: for local servers and recovery").option("--host", "enroll a host instead of a device").option("--coordinator <url>", "local coordinator URL (default: the running one)").option("--data-dir <dir>", "data dir holding admin.token").option("--json", "JSON output").action(async (o) => {
20603
20829
  await pair(ctx, o);
20604
20830
  });
20605
- program.command("login").description("Enroll this terminal as a device (writes ~/.config/switchyard/client.json)").option("--coordinator <url>", "coordinator URL").requiredOption("--token <token>", "device enrollment token or link (switchyard pair)").option("--name <name>", "device name").action(async (o) => {
20831
+ program.command("login").description("Sign this terminal in by approving it in a browser (writes ~/.config/switchyard/client.json)").option("--coordinator <url>", "coordinator URL (default: SWITCHYARD_URL or client.json)").option("--name <name>", "device name").option("--token <token>", "single-use sign-in link or token from `switchyard pair`, instead of browser approval").option("--open", "open the approval page in a browser (default: when interactive and not over SSH)").option("--no-open", "never open a browser").action(async (o) => {
20606
20832
  await login(ctx, o);
20607
20833
  });
20608
20834
  connectionOptions(program.command("doctor").description("Check Node, Pi, git, coordinator, credentials, host link and push")).action(async (o) => {