@spencerboucher/switchyard 0.6.0 → 0.8.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 (164) hide show
  1. package/README.md +6 -6
  2. package/dist/bridge/switchyard-terminal.js +158 -2
  3. package/dist/switchyard.mjs +1332 -769
  4. package/package.json +1 -1
  5. package/web-dist/assets/AutomationView-GCthHkym.js +2 -0
  6. package/web-dist/assets/AutomationView-GCthHkym.js.map +1 -0
  7. package/web-dist/assets/Badge-CEMI3YyI.js +2 -0
  8. package/web-dist/assets/Badge-CEMI3YyI.js.map +1 -0
  9. package/web-dist/assets/InteractionCard-DaLqafmA.js +6 -0
  10. package/web-dist/assets/InteractionCard-DaLqafmA.js.map +1 -0
  11. package/web-dist/assets/{Menu-GymfuHGP.js → Menu-BcKiEc6X.js} +2 -2
  12. package/web-dist/assets/{Menu-GymfuHGP.js.map → Menu-BcKiEc6X.js.map} +1 -1
  13. package/web-dist/assets/{SegmentedControl-ChdVilXb.js → SegmentedControl-BZPzNU08.js} +2 -2
  14. package/web-dist/assets/{SegmentedControl-ChdVilXb.js.map → SegmentedControl-BZPzNU08.js.map} +1 -1
  15. package/web-dist/assets/Tabs-COH5EMql.js +184 -0
  16. package/web-dist/assets/Tabs-COH5EMql.js.map +1 -0
  17. package/web-dist/assets/TextArea-CeMEPZn4.js +2 -0
  18. package/web-dist/assets/TextArea-CeMEPZn4.js.map +1 -0
  19. package/web-dist/assets/attention-BAdea2kA.js +2 -0
  20. package/web-dist/assets/attention-BAdea2kA.js.map +1 -0
  21. package/web-dist/assets/automations-DSFUzB0w.js +2 -0
  22. package/web-dist/assets/automations-DSFUzB0w.js.map +1 -0
  23. package/web-dist/assets/board-eQ3H8yCp.js +10 -0
  24. package/web-dist/assets/board-eQ3H8yCp.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-D7JmYMKe.js +13 -0
  29. package/web-dist/assets/code-D7JmYMKe.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-C1YimBXJ.js +5 -0
  37. package/web-dist/assets/describe-C1YimBXJ.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-B_RpNOcK.js +56 -0
  41. package/web-dist/assets/dist-B_RpNOcK.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-1OCy5r50.js +2 -0
  45. package/web-dist/assets/enroll-1OCy5r50.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-BhpEXLav.js +11 -0
  56. package/web-dist/assets/index-BhpEXLav.js.map +1 -0
  57. package/web-dist/assets/index-DUeOwDZ6.css +2 -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-CBvFOyIo.js +2 -0
  72. package/web-dist/assets/logic-CBvFOyIo.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-RjVyxPaz.js} +2 -2
  77. package/web-dist/assets/{more-CRGJsz6E.js.map → more-RjVyxPaz.js.map} +1 -1
  78. package/web-dist/assets/now-CDjeFSLD.js +2 -0
  79. package/web-dist/assets/now-CDjeFSLD.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-tH4UuIfJ.js +7 -0
  91. package/web-dist/assets/settings-tH4UuIfJ.js.map +1 -0
  92. package/web-dist/assets/shared-CRX6KSBm.js +3 -0
  93. package/web-dist/assets/shared-CRX6KSBm.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-DXozulGZ.js +2 -0
  103. package/web-dist/assets/states-DXozulGZ.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-CW2rE50W.js +2 -0
  107. package/web-dist/assets/terminal-login-CW2rE50W.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-C_B7JgMM.js +2 -0
  117. package/web-dist/assets/{useNow-Bc_NlDY3.js.map → useNow-C_B7JgMM.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 +0 -56
  148. package/web-dist/assets/dist-Bgkh2hc5.js.map +0 -1
  149. package/web-dist/assets/enroll-DijmoolY.js +0 -2
  150. package/web-dist/assets/enroll-DijmoolY.js.map +0 -1
  151. package/web-dist/assets/index-C5n2QB-Y.css +0 -2
  152. package/web-dist/assets/index-Cf6ZUCfK.js +0 -11
  153. package/web-dist/assets/index-Cf6ZUCfK.js.map +0 -1
  154. package/web-dist/assets/logic-D_Eq9DMW.js +0 -2
  155. package/web-dist/assets/logic-D_Eq9DMW.js.map +0 -1
  156. package/web-dist/assets/now-D6lDofYy.js +0 -2
  157. package/web-dist/assets/now-D6lDofYy.js.map +0 -1
  158. package/web-dist/assets/settings-Csal1F-W.js +0 -8
  159. package/web-dist/assets/settings-Csal1F-W.js.map +0 -1
  160. package/web-dist/assets/sliders-horizontal-S3tmwxsP.js +0 -2
  161. package/web-dist/assets/sliders-horizontal-S3tmwxsP.js.map +0 -1
  162. package/web-dist/assets/states-DrTQuYXw.js +0 -2
  163. package/web-dist/assets/states-DrTQuYXw.js.map +0 -1
  164. package/web-dist/assets/useNow-Bc_NlDY3.js +0 -2
@@ -380,7 +380,8 @@ var SessionModel = z3.object({
380
380
  var SlashCommand = z3.object({
381
381
  name: z3.string().min(1),
382
382
  description: z3.string().nullable(),
383
- source: z3.enum(["extension", "prompt", "skill"])
383
+ source: z3.enum(["extension", "prompt", "skill"]),
384
+ argumentHint: z3.string().nullable().optional()
384
385
  });
385
386
  var ContextUsage = z3.object({
386
387
  /** Estimated tokens in context; null right after compaction until the next reply. */
@@ -683,7 +684,7 @@ var Device = z3.object({
683
684
  permission: z3.enum(["granted", "denied", "default", "unknown"])
684
685
  }),
685
686
  preferredMobile: z3.boolean(),
686
- enrolledVia: z3.enum(["enrollment", "passkey", "oidc", "admin"]).default("enrollment")
687
+ enrolledVia: z3.enum(["enrollment", "passkey", "oidc", "admin", "approval"]).default("enrollment")
687
688
  });
688
689
  var ToolItemData = z3.object({
689
690
  toolCallId: z3.string(),
@@ -921,6 +922,51 @@ var SetThinkingRequest = z4.object({ level: ThinkingLevel });
921
922
  var CompactRequest = z4.object({ instructions: z4.string().max(4e3).optional() });
922
923
  var SessionModelsResult = z4.object({ models: z4.array(SessionModel) });
923
924
  var SessionCommandsResult = z4.object({ commands: z4.array(SlashCommand) });
925
+ var CompleteRequest = z4.object({ text: z4.string().min(1).max(500) });
926
+ var CompletionItem = z4.object({
927
+ value: z4.string(),
928
+ label: z4.string(),
929
+ description: z4.string().nullable()
930
+ });
931
+ var SessionCompleteResult = z4.object({
932
+ items: z4.array(CompletionItem).max(50),
933
+ prefix: z4.string()
934
+ });
935
+ var TreeRow = z4.object({
936
+ entryId: z4.string(),
937
+ parentId: z4.string().nullable(),
938
+ depth: z4.number().int(),
939
+ role: z4.enum(["user", "assistant"]),
940
+ text: z4.string(),
941
+ activeBranch: z4.boolean(),
942
+ leaf: z4.boolean(),
943
+ label: z4.string().nullable(),
944
+ at: z4.string().nullable()
945
+ });
946
+ var SessionTreeResult = z4.object({ rows: z4.array(TreeRow) });
947
+ var BranchRequest = z4.object({
948
+ entryId: z4.string().nullable(),
949
+ fork: z4.boolean().default(false)
950
+ });
951
+ var HostBranchResult = z4.object({
952
+ sessionFile: z4.string().nullable(),
953
+ prefill: z4.string().nullable()
954
+ });
955
+ var BranchResult = z4.object({
956
+ session: Session.nullable(),
957
+ prefill: z4.string().nullable()
958
+ });
959
+ var SessionExportResult = z4.object({ fileName: z4.string(), content: z4.string() });
960
+ var CommandViewAction = z4.object({
961
+ label: z4.string().min(1).max(80),
962
+ command: z4.string().startsWith("/").max(500),
963
+ tone: z4.enum(["default", "primary", "danger"]).optional()
964
+ });
965
+ var CommandView = z4.object({
966
+ title: z4.string().min(1).max(200),
967
+ body: UINodeSchema,
968
+ actions: z4.array(CommandViewAction).max(30).default([])
969
+ });
924
970
  var ModelStateResult = z4.object({
925
971
  model: SessionModel.nullable(),
926
972
  thinking: ThinkingLevel.nullable()
@@ -1049,7 +1095,6 @@ var PushPayload = z4.union([
1049
1095
  z4.object({ type: z4.literal("clear"), tag: z4.string() })
1050
1096
  ]);
1051
1097
  var SetupState = z4.object({
1052
- initialized: z4.boolean(),
1053
1098
  passkey: z4.object({
1054
1099
  available: z4.boolean(),
1055
1100
  registered: z4.boolean(),
@@ -1057,10 +1102,6 @@ var SetupState = z4.object({
1057
1102
  }),
1058
1103
  oidc: z4.object({ label: z4.string() }).nullable()
1059
1104
  });
1060
- var InitResponse = z4.object({
1061
- alreadyInitialized: z4.boolean(),
1062
- enrollment: CreateEnrollmentResponse
1063
- });
1064
1105
  var PasskeyRegisterOptionsResponse = z4.object({
1065
1106
  challengeId: z4.string(),
1066
1107
  options: z4.unknown()
@@ -1096,6 +1137,31 @@ var OidcClaimResponse = z4.union([
1096
1137
  z4.object({ status: z4.literal("pending") }),
1097
1138
  z4.object({ status: z4.literal("done"), deviceId: z4.string() })
1098
1139
  ]);
1140
+ var TerminalLoginStartRequest = z4.object({ name: z4.string().min(1).max(80) });
1141
+ var TerminalLoginStartResponse = z4.object({
1142
+ userCode: z4.string(),
1143
+ pollToken: z4.string(),
1144
+ verifyUrl: z4.string(),
1145
+ expiresAt: z4.string(),
1146
+ intervalMs: z4.number().int()
1147
+ });
1148
+ var TerminalLoginClaimRequest = z4.object({
1149
+ userCode: z4.string().min(1),
1150
+ pollToken: z4.string().min(1)
1151
+ });
1152
+ var TerminalLoginClaimResponse = z4.union([
1153
+ z4.object({ status: z4.literal("pending") }),
1154
+ z4.object({ status: z4.literal("denied") }),
1155
+ z4.object({ status: z4.literal("done"), deviceId: z4.string(), credential: z4.string() })
1156
+ ]);
1157
+ var TerminalLoginRequest = z4.object({
1158
+ userCode: z4.string(),
1159
+ name: z4.string(),
1160
+ requestedFrom: z4.string(),
1161
+ createdAt: z4.string(),
1162
+ expiresAt: z4.string(),
1163
+ status: z4.enum(["pending", "approved", "denied"])
1164
+ });
1099
1165
  var AuthMethods = z4.object({
1100
1166
  passkeys: z4.array(Passkey),
1101
1167
  passkeyAvailability: z4.object({ available: z4.boolean(), reason: z4.string().nullable() }),
@@ -1301,6 +1367,20 @@ var HostCommand = z8.discriminatedUnion("type", [
1301
1367
  z8.object({ type: z8.literal("session.snapshot"), sessionId: z8.string() }),
1302
1368
  z8.object({ type: z8.literal("session.models"), sessionId: z8.string() }),
1303
1369
  z8.object({ type: z8.literal("session.commands"), sessionId: z8.string() }),
1370
+ z8.object({
1371
+ type: z8.literal("session.complete"),
1372
+ sessionId: z8.string(),
1373
+ text: z8.string().max(500)
1374
+ }),
1375
+ z8.object({ type: z8.literal("session.tree"), sessionId: z8.string() }),
1376
+ z8.object({
1377
+ type: z8.literal("session.branch"),
1378
+ sessionId: z8.string(),
1379
+ entryId: z8.string().nullable(),
1380
+ fork: z8.boolean()
1381
+ }),
1382
+ z8.object({ type: z8.literal("session.reload"), sessionId: z8.string() }),
1383
+ z8.object({ type: z8.literal("session.export"), sessionId: z8.string() }),
1304
1384
  z8.object({
1305
1385
  type: z8.literal("session.setModel"),
1306
1386
  sessionId: z8.string(),
@@ -1854,7 +1934,7 @@ var ApiClient = class {
1854
1934
  constructor(opts) {
1855
1935
  this.baseUrl = opts.coordinatorUrl.replace(/\/+$/, "");
1856
1936
  this.credential = opts.credential;
1857
- this.doFetch = opts.fetch ?? ((input, init2) => fetch(input, init2));
1937
+ this.doFetch = opts.fetch ?? ((input, init) => fetch(input, init));
1858
1938
  }
1859
1939
  async request(method, path, body) {
1860
1940
  const headers = { accept: "application/json" };
@@ -2320,8 +2400,8 @@ function runRawTerminal(opts) {
2320
2400
  import { Command, Option } from "commander";
2321
2401
 
2322
2402
  // packages/cli/dist/commands/auth.js
2323
- import { hostname } from "node:os";
2324
- import QRCode from "qrcode";
2403
+ import { hostname as hostname3 } from "node:os";
2404
+ import QRCode2 from "qrcode";
2325
2405
 
2326
2406
  // packages/cli/dist/config.js
2327
2407
  import { chmodSync, existsSync as existsSync2, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
@@ -2483,121 +2563,10 @@ function trimUrl(url) {
2483
2563
  return url.trim().replace(/\/+$/, "");
2484
2564
  }
2485
2565
 
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";
2566
+ // packages/cli/dist/commands/up.js
2567
+ import { spawn as spawn4 } from "node:child_process";
2568
+ import { existsSync as existsSync15 } from "node:fs";
2569
+ import { hostname as hostname2 } from "node:os";
2601
2570
 
2602
2571
  // packages/coordinator/dist/attention/router.js
2603
2572
  var DESKTOP_RECENT_MS = 5 * 6e4;
@@ -2895,12 +2864,10 @@ var Auth = class {
2895
2864
  store;
2896
2865
  config;
2897
2866
  hub;
2898
- owner;
2899
- constructor(store, config, hub, owner) {
2867
+ constructor(store, config, hub) {
2900
2868
  this.store = store;
2901
2869
  this.config = config;
2902
2870
  this.hub = hub;
2903
- this.owner = owner;
2904
2871
  }
2905
2872
  get adminTokenPath() {
2906
2873
  return join3(this.config.dataDir, "admin.token");
@@ -2940,14 +2907,13 @@ var Auth = class {
2940
2907
  return this.store.devices.count("is_admin = 0 AND revoked_at IS NULL");
2941
2908
  }
2942
2909
  enrollmentUrl(token, kind) {
2943
- return kind === "device" ? `${this.config.publicUrl}/enroll#token=${token}` : `${this.config.publicUrl}/enroll#token=${token}&kind=host`;
2910
+ return kind === "device" ? `${this.config.publicUrl}/sign-in#token=${token}` : `${this.config.publicUrl}/sign-in#token=${token}&kind=host`;
2944
2911
  }
2945
2912
  hostEnrollCommand(token) {
2946
2913
  return `switchyard host enroll --coordinator ${this.config.publicUrl} --token ${token}`;
2947
2914
  }
2948
2915
  /**
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.
2916
+ * Create a single-use enrollment (device 10 min, host 30 min by default).
2951
2917
  */
2952
2918
  createEnrollment(kind, createdBy, ttlMs = kind === "device" ? DEVICE_ENROLLMENT_TTL_MS : HOST_ENROLLMENT_TTL_MS) {
2953
2919
  const token = newToken();
@@ -2955,8 +2921,6 @@ var Auth = class {
2955
2921
  const now = this.store.now();
2956
2922
  const expiresAt = new Date(now.getTime() + ttlMs).toISOString();
2957
2923
  this.store.tx(() => {
2958
- if (createdBy !== null)
2959
- this.owner.markInitialized();
2960
2924
  this.store.insertEnrollment({
2961
2925
  id,
2962
2926
  kind,
@@ -4459,7 +4423,7 @@ function createSession(ctx, input) {
4459
4423
  nativeHandle: null,
4460
4424
  profileSnapshot: profile,
4461
4425
  runtime: "starting",
4462
- runtimeDetail: null,
4426
+ runtimeDetail: workspace ? "Starting Pi\u2026" : "Creating worktree\u2026",
4463
4427
  connectivity: host.connectivity,
4464
4428
  runId: null,
4465
4429
  taskId: null,
@@ -4475,19 +4439,27 @@ function createSession(ctx, input) {
4475
4439
  ctx.store.putSession(session);
4476
4440
  const choice = input.workspace;
4477
4441
  const started = (async () => {
4442
+ const t0 = Date.now();
4478
4443
  try {
4479
4444
  let s = session;
4480
4445
  if (!("existingId" in choice)) {
4481
4446
  const ws = await createWorkspace(ctx, project, checkout, choice.newBranch, choice.base);
4447
+ ctx.log.info({ sessionId: session.id, ms: Date.now() - t0 }, "session start: worktree ready");
4482
4448
  s = ctx.store.tx(() => {
4483
4449
  const cur = ctx.store.sessions.require(session.id);
4484
- return ctx.store.putSession({ ...cur, workspaceId: ws.id, cwd: ws.path });
4450
+ return ctx.store.putSession({
4451
+ ...cur,
4452
+ workspaceId: ws.id,
4453
+ cwd: ws.path,
4454
+ runtimeDetail: "Starting Pi\u2026"
4455
+ });
4485
4456
  });
4486
4457
  }
4487
4458
  await startOnHost(ctx, s, {
4488
4459
  initialPrompt: input.prompt.trim() ? input.prompt : null,
4489
4460
  nativeHandle: null
4490
4461
  });
4462
+ ctx.log.info({ sessionId: session.id, ms: Date.now() - t0 }, "session start: started on host");
4491
4463
  } catch (err) {
4492
4464
  const message = err instanceof Error ? err.message : String(err);
4493
4465
  ctx.log.warn({ err, sessionId: session.id }, "session start failed");
@@ -7328,10 +7300,23 @@ INSERT OR IGNORE INTO settings(key, value)
7328
7300
  WHERE EXISTS (SELECT 1 FROM devices WHERE is_admin = 0 AND revoked_at IS NULL);
7329
7301
  `;
7330
7302
 
7303
+ // packages/coordinator/dist/db/migrations/003_terminal_login.js
7304
+ var sql3 = `
7305
+ DROP TABLE auth_flows;
7306
+
7307
+ CREATE TABLE auth_flows (
7308
+ id TEXT PRIMARY KEY,
7309
+ kind TEXT NOT NULL CHECK (kind IN ('passkey_register','passkey_auth','oidc','terminal_login')),
7310
+ data TEXT NOT NULL,
7311
+ expires_at TEXT NOT NULL
7312
+ );
7313
+ `;
7314
+
7331
7315
  // packages/coordinator/dist/db/index.js
7332
7316
  var MIGRATIONS = [
7333
7317
  { version: 1, name: "001_init", sql },
7334
- { version: 2, name: "002_owner_auth", sql: sql2 }
7318
+ { version: 2, name: "002_owner_auth", sql: sql2 },
7319
+ { version: 3, name: "003_terminal_login", sql: sql3 }
7335
7320
  ];
7336
7321
  var LATEST_MIGRATION = MIGRATIONS[MIGRATIONS.length - 1].version;
7337
7322
  function openDb(path) {
@@ -8171,8 +8156,11 @@ import * as client from "openid-client";
8171
8156
  // packages/coordinator/dist/owner.js
8172
8157
  import { randomBytes as randomBytes2 } from "node:crypto";
8173
8158
  import { isIP } from "node:net";
8174
- var OWNER_INITIALIZED_KEY = "owner.initialized_at";
8175
8159
  var OWNER_WEBAUTHN_USER_KEY = "owner.webauthn_user_id";
8160
+ function isLoopbackUrl(url) {
8161
+ const host = new URL(url).hostname.replace(/^\[|\]$/g, "");
8162
+ return host === "localhost" || host === "::1" || /^127\.\d+\.\d+\.\d+$/.test(host);
8163
+ }
8176
8164
  var Owner = class {
8177
8165
  store;
8178
8166
  config;
@@ -8180,18 +8168,6 @@ var Owner = class {
8180
8168
  this.store = store;
8181
8169
  this.config = config;
8182
8170
  }
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
8171
  /** Stable random WebAuthn user handle for the owner (16 bytes, created lazily). */
8196
8172
  webauthnUserId() {
8197
8173
  return this.store.tx(() => {
@@ -8851,53 +8827,6 @@ function registerAttentionRoutes(app, ctx) {
8851
8827
  app.post("/api/attention/:id/acknowledge", async (req) => ctx.attention.acknowledge(req.params.id));
8852
8828
  }
8853
8829
 
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
8830
  // packages/coordinator/dist/routes/auth.js
8902
8831
  var HANDOFF_PAGE = `<!doctype html>
8903
8832
  <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
@@ -8908,7 +8837,6 @@ var HANDOFF_PAGE = `<!doctype html>
8908
8837
  function setupState(ctx) {
8909
8838
  const availability = ctx.owner.passkeyAvailability();
8910
8839
  return {
8911
- initialized: ctx.owner.isInitialized(),
8912
8840
  passkey: {
8913
8841
  available: availability.available,
8914
8842
  registered: ctx.passkeys.count() > 0,
@@ -8988,6 +8916,18 @@ function registerAuthRoutes(app, ctx) {
8988
8916
  ctx.auth.setDeviceCookie(reply, r.credential);
8989
8917
  return { status: "done", deviceId: r.deviceId };
8990
8918
  });
8919
+ app.post("/api/auth/terminal/start", async (req, reply) => {
8920
+ limitAuth(req);
8921
+ const body = parse(TerminalLoginStartRequest, req.body);
8922
+ return reply.code(201).send(ctx.terminalLogins.start(body.name, req.ip));
8923
+ });
8924
+ app.post("/api/auth/terminal/claim", async (req) => {
8925
+ const body = parse(TerminalLoginClaimRequest, req.body);
8926
+ return ctx.terminalLogins.claim(body.userCode, body.pollToken);
8927
+ });
8928
+ app.get("/api/auth/terminal/requests/:code", async (req) => ctx.terminalLogins.describe(req.params.code));
8929
+ app.post("/api/auth/terminal/requests/:code/approve", async (req) => ctx.terminalLogins.approve(req.params.code));
8930
+ app.post("/api/auth/terminal/requests/:code/deny", async (req) => ctx.terminalLogins.deny(req.params.code));
8991
8931
  app.get("/api/auth/methods", async () => {
8992
8932
  const availability = ctx.owner.passkeyAvailability();
8993
8933
  return {
@@ -9006,15 +8946,6 @@ function registerAuthRoutes(app, ctx) {
9006
8946
  ctx.passkeys.remove(req.params.id);
9007
8947
  return { ok: true };
9008
8948
  });
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
8949
  }
9019
8950
  function redirectError(ctx, req, reply, err) {
9020
8951
  const code = err instanceof SwitchyardError ? err.code : "oidc_failed";
@@ -9024,7 +8955,54 @@ function redirectError(ctx, req, reply, err) {
9024
8955
  else if (err.code === "forbidden")
9025
8956
  ctx.log.info({ detail }, "OIDC sign-in refused");
9026
8957
  const qs = new URLSearchParams({ error: code, detail });
9027
- return reply.redirect(`/enroll?${qs.toString()}`, 302);
8958
+ return reply.redirect(`/sign-in?${qs.toString()}`, 302);
8959
+ }
8960
+
8961
+ // packages/coordinator/dist/routes/devices.js
8962
+ import QRCode from "qrcode";
8963
+ import { z as z14 } from "zod";
8964
+ var UpdateDeviceRequest = z14.object({
8965
+ name: z14.string().min(1).max(80).optional(),
8966
+ preferredMobile: z14.boolean().optional()
8967
+ });
8968
+ async function enrollmentResponse(ctx, kind, createdBy) {
8969
+ const e = ctx.auth.createEnrollment(kind, createdBy);
8970
+ const qrSvg = await QRCode.toString(e.url, { type: "svg", margin: 1 });
8971
+ return { token: e.token, url: e.url, expiresAt: e.expiresAt, qrSvg, command: e.command };
8972
+ }
8973
+ function publicDevice(ctx, id) {
8974
+ return ctx.store.devices.require(id);
8975
+ }
8976
+ function registerDeviceRoutes(app, ctx) {
8977
+ app.post("/api/enrollments", async (req, reply) => {
8978
+ const body = parse(CreateEnrollmentRequest, req.body);
8979
+ return reply.code(201).send(await enrollmentResponse(ctx, body.kind, deviceOf(req).id));
8980
+ });
8981
+ app.post("/api/devices/enrollments", async (req, reply) => reply.code(201).send(await enrollmentResponse(ctx, "device", deviceOf(req).id)));
8982
+ app.get("/api/devices", async () => ctx.store.devices.list("is_admin = 0", [], "created_at"));
8983
+ app.get("/api/devices/:id", async (req) => publicDevice(ctx, req.params.id));
8984
+ app.patch("/api/devices/:id", async (req) => {
8985
+ const { id } = req.params;
8986
+ const body = parse(UpdateDeviceRequest, req.body);
8987
+ return ctx.store.tx(() => {
8988
+ const d = ctx.store.devices.require(id);
8989
+ if (body.preferredMobile) {
8990
+ for (const other of ctx.store.devices.list("is_admin = 0")) {
8991
+ if (other.id !== id && other.preferredMobile) {
8992
+ ctx.store.putDevice({ ...other, preferredMobile: false });
8993
+ }
8994
+ }
8995
+ }
8996
+ return ctx.store.putDevice({
8997
+ ...d,
8998
+ name: body.name ?? d.name,
8999
+ preferredMobile: body.preferredMobile ?? d.preferredMobile
9000
+ });
9001
+ });
9002
+ });
9003
+ const revoke = async (req) => ctx.auth.revokeDevice(req.params.id);
9004
+ app.post("/api/devices/:id/revoke", revoke);
9005
+ app.delete("/api/devices/:id", revoke);
9028
9006
  }
9029
9007
 
9030
9008
  // packages/coordinator/dist/routes/hosts.js
@@ -9443,49 +9421,20 @@ function registerSessionRoutes(app, ctx) {
9443
9421
  where.push("archived_at IS NULL");
9444
9422
  return ctx.store.sessions.list(where.join(" AND ") || void 0, params, "last_activity_at DESC");
9445
9423
  });
9446
- app.get("/api/sessions/importable", async (req) => {
9447
- const q = parse(ImportableQuery, req.query, "query");
9448
- requireConnected(ctx, q.hostId);
9449
- const list = normalizeImportable(await ctx.hostHub.request(q.hostId, { type: "session.importable" }));
9450
- return list.map((s) => ({
9451
- ...s,
9452
- importedSessionId: ctx.store.sessionByNativeFile(q.hostId, s.sessionFile)?.id ?? null
9453
- }));
9454
- });
9455
- app.post("/api/sessions/import", async (req, reply) => {
9456
- const body = parse(ImportSessionRequest, req.body);
9457
- requireConnected(ctx, body.hostId);
9458
- const already = ctx.store.sessionByNativeFile(body.hostId, body.sessionFile);
9459
- if (already) {
9460
- throw new SwitchyardError("conflict", "This Pi session was already imported", {
9461
- sessionId: already.id
9462
- });
9463
- }
9464
- const list = normalizeImportable(await ctx.hostHub.request(body.hostId, { type: "session.importable" }));
9465
- const native = list.find((s) => s.sessionFile === body.sessionFile);
9466
- if (!native)
9467
- throw new SwitchyardError("not_found", "Pi session file not found on host");
9468
- if (!native.inactive) {
9469
- throw new SwitchyardError("conflict", "This Pi session is still active; close it in Pi before importing");
9470
- }
9471
- const rawItems = await ctx.hostHub.request(body.hostId, {
9472
- type: "session.readNative",
9473
- sessionFile: body.sessionFile
9474
- });
9475
- const ws = ctx.store.workspaceByPath(body.hostId, native.cwd);
9476
- const projectId = body.projectId ?? ws?.projectId ?? null;
9424
+ function importNative(native, hostId, projectIdIn, items) {
9425
+ const ws = ctx.store.workspaceByPath(hostId, native.cwd);
9426
+ const projectId = projectIdIn ?? ws?.projectId ?? null;
9477
9427
  if (projectId)
9478
9428
  ctx.store.projects.require(projectId);
9479
9429
  const project = projectId ? ctx.store.projects.get(projectId) : null;
9480
- const host = ctx.store.hosts.require(body.hostId);
9430
+ const host = ctx.store.hosts.require(hostId);
9481
9431
  const now = ctx.store.nowIso();
9482
9432
  const id = newId("ses");
9483
- const items = Array.isArray(rawItems) ? rawItems : rawItems?.items ?? [];
9484
9433
  const session = {
9485
9434
  id,
9486
9435
  title: native.name ?? native.cwd.split("/").pop() ?? "Imported session",
9487
9436
  subtitle: null,
9488
- hostId: body.hostId,
9437
+ hostId,
9489
9438
  workspaceId: ws && ws.projectId === projectId ? ws.id : null,
9490
9439
  projectId,
9491
9440
  cwd: native.cwd,
@@ -9519,6 +9468,39 @@ function registerSessionRoutes(app, ctx) {
9519
9468
  ctx.store.upsertItem(item.data);
9520
9469
  });
9521
9470
  });
9471
+ return session;
9472
+ }
9473
+ app.get("/api/sessions/importable", async (req) => {
9474
+ const q = parse(ImportableQuery, req.query, "query");
9475
+ requireConnected(ctx, q.hostId);
9476
+ const list = normalizeImportable(await ctx.hostHub.request(q.hostId, { type: "session.importable" }));
9477
+ return list.map((s) => ({
9478
+ ...s,
9479
+ importedSessionId: ctx.store.sessionByNativeFile(q.hostId, s.sessionFile)?.id ?? null
9480
+ }));
9481
+ });
9482
+ app.post("/api/sessions/import", async (req, reply) => {
9483
+ const body = parse(ImportSessionRequest, req.body);
9484
+ requireConnected(ctx, body.hostId);
9485
+ const already = ctx.store.sessionByNativeFile(body.hostId, body.sessionFile);
9486
+ if (already) {
9487
+ throw new SwitchyardError("conflict", "This Pi session was already imported", {
9488
+ sessionId: already.id
9489
+ });
9490
+ }
9491
+ const list = normalizeImportable(await ctx.hostHub.request(body.hostId, { type: "session.importable" }));
9492
+ const native = list.find((s) => s.sessionFile === body.sessionFile);
9493
+ if (!native)
9494
+ throw new SwitchyardError("not_found", "Pi session file not found on host");
9495
+ if (!native.inactive) {
9496
+ throw new SwitchyardError("conflict", "This Pi session is still active; close it in Pi before importing");
9497
+ }
9498
+ const rawItems = await ctx.hostHub.request(body.hostId, {
9499
+ type: "session.readNative",
9500
+ sessionFile: body.sessionFile
9501
+ });
9502
+ const items = Array.isArray(rawItems) ? rawItems : rawItems?.items ?? [];
9503
+ const session = importNative(native, body.hostId, body.projectId ?? null, items);
9522
9504
  return reply.code(201).send(session);
9523
9505
  });
9524
9506
  app.get("/api/sessions/:id", async (req) => get(idOf(req)));
@@ -9540,9 +9522,15 @@ function registerSessionRoutes(app, ctx) {
9540
9522
  return ctx.hostHub.request(s.hostId, { type: "session.snapshot", sessionId: s.id });
9541
9523
  });
9542
9524
  app.post("/api/sessions/:id/prompt", async (req) => {
9543
- const s = get(idOf(req));
9525
+ let s = get(idOf(req));
9544
9526
  const body = parse(PromptRequest, req.body);
9545
- requireLiveSession(ctx, s);
9527
+ if (!s.readOnly && !s.archivedAt && s.nativeHandle && !LIVE_RUNTIMES2.includes(s.runtime) && body.mode === "prompt") {
9528
+ await resumeSession(s);
9529
+ s = get(s.id);
9530
+ requireConnected(ctx, s.hostId);
9531
+ } else {
9532
+ requireLiveSession(ctx, s);
9533
+ }
9546
9534
  const run = ctx.store.runForSession(s);
9547
9535
  const result = await ctx.hostHub.request(s.hostId, {
9548
9536
  type: "session.prompt",
@@ -9553,6 +9541,9 @@ function registerSessionRoutes(app, ctx) {
9553
9541
  origin: "human",
9554
9542
  controlRevision: run?.controlRevision ?? 1
9555
9543
  });
9544
+ const view = result?.view;
9545
+ if (view)
9546
+ CommandView.parse(view);
9556
9547
  return {
9557
9548
  ok: true,
9558
9549
  clientMessageId: body.clientMessageId ?? null,
@@ -9570,6 +9561,54 @@ function registerSessionRoutes(app, ctx) {
9570
9561
  requireSessionControls(ctx, s);
9571
9562
  return SessionCommandsResult.parse(await ctx.hostHub.request(s.hostId, { type: "session.commands", sessionId: s.id }));
9572
9563
  });
9564
+ app.post("/api/sessions/:id/complete", async (req) => {
9565
+ const s = get(idOf(req));
9566
+ const body = parse(CompleteRequest, req.body);
9567
+ requireSessionControls(ctx, s);
9568
+ return SessionCompleteResult.parse(await ctx.hostHub.request(s.hostId, {
9569
+ type: "session.complete",
9570
+ sessionId: s.id,
9571
+ text: body.text
9572
+ }));
9573
+ });
9574
+ app.get("/api/sessions/:id/tree", async (req) => {
9575
+ const s = get(idOf(req));
9576
+ requireSessionControls(ctx, s);
9577
+ return SessionTreeResult.parse(await ctx.hostHub.request(s.hostId, { type: "session.tree", sessionId: s.id }));
9578
+ });
9579
+ app.post("/api/sessions/:id/branch", async (req, reply) => {
9580
+ const s = get(idOf(req));
9581
+ const body = parse(BranchRequest, req.body);
9582
+ requireSessionControls(ctx, s);
9583
+ const r = HostBranchResult.parse(await ctx.hostHub.request(s.hostId, {
9584
+ type: "session.branch",
9585
+ sessionId: s.id,
9586
+ entryId: body.entryId,
9587
+ fork: body.fork
9588
+ }));
9589
+ if (!r.sessionFile)
9590
+ return { session: null, prefill: r.prefill };
9591
+ const native = await ctx.hostHub.request(s.hostId, { type: "session.readNative", sessionFile: r.sessionFile });
9592
+ const session = importNative({
9593
+ sessionId: native.sessionId,
9594
+ sessionFile: r.sessionFile,
9595
+ cwd: native.cwd,
9596
+ name: `${native.name ?? s.title} (branch)`,
9597
+ mtime: ctx.store.nowIso()
9598
+ }, s.hostId, s.projectId, native.items ?? []);
9599
+ return reply.code(201).send({ session, prefill: r.prefill });
9600
+ });
9601
+ app.post("/api/sessions/:id/reload", async (req) => {
9602
+ const s = get(idOf(req));
9603
+ requireSessionControls(ctx, s);
9604
+ await ctx.hostHub.request(s.hostId, { type: "session.reload", sessionId: s.id });
9605
+ return { ok: true };
9606
+ });
9607
+ app.get("/api/sessions/:id/export", async (req) => {
9608
+ const s = get(idOf(req));
9609
+ requireSessionControls(ctx, s);
9610
+ return SessionExportResult.parse(await ctx.hostHub.request(s.hostId, { type: "session.export", sessionId: s.id }));
9611
+ });
9573
9612
  app.post("/api/sessions/:id/model", async (req) => {
9574
9613
  const s = get(idOf(req));
9575
9614
  const body = parse(SetModelRequest, req.body);
@@ -9625,8 +9664,7 @@ function registerSessionRoutes(app, ctx) {
9625
9664
  });
9626
9665
  app.post("/api/sessions/:id/takeover", async (req) => takeover(ctx, idOf(req)));
9627
9666
  app.post("/api/sessions/:id/return-to-automation", async (req) => returnToAutomation(ctx, idOf(req)));
9628
- app.post("/api/sessions/:id/resume", async (req) => {
9629
- const s = get(idOf(req));
9667
+ async function resumeSession(s) {
9630
9668
  if (!s.nativeHandle) {
9631
9669
  throw new SwitchyardError("native_session_missing", "This session has no native Pi session to resume");
9632
9670
  }
@@ -9659,6 +9697,10 @@ function registerSessionRoutes(app, ctx) {
9659
9697
  });
9660
9698
  throw err;
9661
9699
  }
9700
+ }
9701
+ app.post("/api/sessions/:id/resume", async (req) => {
9702
+ const s = get(idOf(req));
9703
+ await resumeSession(s);
9662
9704
  return get(s.id);
9663
9705
  });
9664
9706
  app.post("/api/sessions/:id/rename", async (req) => {
@@ -9808,11 +9850,11 @@ var Statements = class {
9808
9850
  constructor(db) {
9809
9851
  this.db = db;
9810
9852
  }
9811
- get(sql3) {
9812
- let s = this.cache.get(sql3);
9853
+ get(sql4) {
9854
+ let s = this.cache.get(sql4);
9813
9855
  if (!s) {
9814
- s = this.db.prepare(sql3);
9815
- this.cache.set(sql3, s);
9856
+ s = this.db.prepare(sql4);
9857
+ this.cache.set(sql4, s);
9816
9858
  }
9817
9859
  return s;
9818
9860
  }
@@ -9843,12 +9885,12 @@ var JsonRepo = class {
9843
9885
  }
9844
9886
  /** List with an optional SQL `WHERE` fragment over the indexed columns. */
9845
9887
  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));
9888
+ const sql4 = `SELECT json FROM ${this.table}${where ? ` WHERE ${where}` : ""} ORDER BY ${orderBy}`;
9889
+ return this.st.get(sql4).all(...params).map((r) => JSON.parse(r.json));
9848
9890
  }
9849
9891
  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;
9892
+ const sql4 = `SELECT COUNT(*) AS n FROM ${this.table}${where ? ` WHERE ${where}` : ""}`;
9893
+ return this.st.get(sql4).get(...params).n;
9852
9894
  }
9853
9895
  /**
9854
9896
  * Update by id, or insert when missing. `extra` columns are written in both cases when given
@@ -10594,6 +10636,126 @@ var TerminalHub = class {
10594
10636
  }
10595
10637
  };
10596
10638
 
10639
+ // packages/coordinator/dist/terminal-login.js
10640
+ import { randomInt } from "node:crypto";
10641
+ var TERMINAL_LOGIN_TTL_MS = 10 * 6e4;
10642
+ var TERMINAL_LOGIN_POLL_MS = 2e3;
10643
+ var CODE_ALPHABET = "BCDFGHJKLMNPQRSTVWXZ";
10644
+ function normalizeUserCode(s) {
10645
+ return s.toUpperCase().replace(/[^A-Z]/g, "");
10646
+ }
10647
+ function formatUserCode(n) {
10648
+ return `${n.slice(0, 4)}-${n.slice(4)}`;
10649
+ }
10650
+ var TerminalLogins = class {
10651
+ store;
10652
+ config;
10653
+ auth;
10654
+ flows;
10655
+ constructor(store, config, auth) {
10656
+ this.store = store;
10657
+ this.config = config;
10658
+ this.auth = auth;
10659
+ this.flows = new AuthFlows(store);
10660
+ }
10661
+ load(code) {
10662
+ const id = normalizeUserCode(code);
10663
+ const flow = this.flows.get("terminal_login", id);
10664
+ if (!flow) {
10665
+ throw new SwitchyardError("not_found", "This sign-in request expired or does not exist. Run `switchyard login` again.");
10666
+ }
10667
+ return { id, flow };
10668
+ }
10669
+ view(id, flow) {
10670
+ return {
10671
+ userCode: formatUserCode(id),
10672
+ name: flow.name,
10673
+ requestedFrom: flow.requestedFrom,
10674
+ createdAt: flow.createdAt,
10675
+ expiresAt: flow.expiresAt,
10676
+ status: flow.status
10677
+ };
10678
+ }
10679
+ start(name, requestedFrom) {
10680
+ let id = "";
10681
+ for (let attempt = 0; attempt < 5 && !id; attempt++) {
10682
+ let candidate = "";
10683
+ for (let i = 0; i < 8; i++)
10684
+ candidate += CODE_ALPHABET[randomInt(0, CODE_ALPHABET.length)];
10685
+ if (!this.flows.get("terminal_login", candidate))
10686
+ id = candidate;
10687
+ }
10688
+ if (!id)
10689
+ throw new SwitchyardError("internal", "Could not allocate a sign-in code");
10690
+ const pollToken = newToken();
10691
+ const now = this.store.now();
10692
+ const createdAt = now.toISOString();
10693
+ const expiresAt = new Date(now.getTime() + TERMINAL_LOGIN_TTL_MS).toISOString();
10694
+ const flow = {
10695
+ name,
10696
+ requestedFrom,
10697
+ createdAt,
10698
+ expiresAt,
10699
+ pollTokenHash: sha256(pollToken),
10700
+ status: "pending",
10701
+ result: null
10702
+ };
10703
+ this.flows.put("terminal_login", id, flow, TERMINAL_LOGIN_TTL_MS);
10704
+ const userCode = formatUserCode(id);
10705
+ return {
10706
+ userCode,
10707
+ pollToken,
10708
+ verifyUrl: `${this.config.publicUrl}/terminal-login?code=${userCode}`,
10709
+ expiresAt,
10710
+ intervalMs: TERMINAL_LOGIN_POLL_MS
10711
+ };
10712
+ }
10713
+ describe(code) {
10714
+ const { id, flow } = this.load(code);
10715
+ return this.view(id, flow);
10716
+ }
10717
+ approve(code) {
10718
+ return this.store.tx(() => {
10719
+ const { id, flow } = this.load(code);
10720
+ if (flow.status !== "pending") {
10721
+ throw new SwitchyardError("invalid_request", "This sign-in was already approved or denied");
10722
+ }
10723
+ const { device, credential } = this.auth.createDevice(flow.name, "terminal", "approval");
10724
+ const next = {
10725
+ ...flow,
10726
+ status: "approved",
10727
+ result: { deviceId: device.id, credential }
10728
+ };
10729
+ this.flows.update(id, next);
10730
+ return this.view(id, next);
10731
+ });
10732
+ }
10733
+ deny(code) {
10734
+ return this.store.tx(() => {
10735
+ const { id, flow } = this.load(code);
10736
+ if (flow.status !== "pending") {
10737
+ throw new SwitchyardError("invalid_request", "This sign-in was already approved or denied");
10738
+ }
10739
+ const next = { ...flow, status: "denied" };
10740
+ this.flows.update(id, next);
10741
+ return this.view(id, next);
10742
+ });
10743
+ }
10744
+ claim(code, pollToken) {
10745
+ const id = normalizeUserCode(code);
10746
+ const flow = this.flows.get("terminal_login", id);
10747
+ if (!flow || !safeEqualHex(sha256(pollToken), flow.pollTokenHash)) {
10748
+ throw new SwitchyardError("unauthorized", "Unknown or expired sign-in");
10749
+ }
10750
+ if (flow.status === "pending")
10751
+ return { status: "pending" };
10752
+ this.flows.delete(id);
10753
+ if (flow.status === "denied" || !flow.result)
10754
+ return { status: "denied" };
10755
+ return { status: "done", deviceId: flow.result.deviceId, credential: flow.result.credential };
10756
+ }
10757
+ };
10758
+
10597
10759
  // packages/coordinator/dist/server.js
10598
10760
  var MIN_HOST_ENROLL_SECRET_LENGTH = 32;
10599
10761
  var PUBLIC_ROUTES = /* @__PURE__ */ new Set([
@@ -10604,6 +10766,8 @@ var PUBLIC_ROUTES = /* @__PURE__ */ new Set([
10604
10766
  "/api/auth/oidc/begin",
10605
10767
  "/api/auth/oidc/callback",
10606
10768
  "/api/auth/oidc/claim",
10769
+ "/api/auth/terminal/start",
10770
+ "/api/auth/terminal/claim",
10607
10771
  "/api/hosts/enroll",
10608
10772
  "/api/hooks/:automationId",
10609
10773
  "/healthz",
@@ -10625,13 +10789,16 @@ function isApiLikePath(url) {
10625
10789
  }
10626
10790
  return path.startsWith("/api/") || path.startsWith("/ws/") || path.startsWith("/adapter-frames/");
10627
10791
  }
10628
- function originOf2(url) {
10792
+ function originOf(url) {
10629
10793
  return new URL(url).origin;
10630
10794
  }
10631
10795
  function validateAuthOptions(opts) {
10632
10796
  if (opts.hostEnrollSecret !== void 0 && opts.hostEnrollSecret.length < MIN_HOST_ENROLL_SECRET_LENGTH) {
10633
10797
  throw new SwitchyardError("invalid_request", `SWITCHYARD_HOST_ENROLL_SECRET must be at least ${MIN_HOST_ENROLL_SECRET_LENGTH} characters`);
10634
10798
  }
10799
+ if (!opts.oidc && opts.publicUrl && !isLoopbackUrl(opts.publicUrl)) {
10800
+ 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.`);
10801
+ }
10635
10802
  if (opts.oidc && !hasOwnerMatch(opts.oidc.owner)) {
10636
10803
  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
10804
  }
@@ -10656,7 +10823,7 @@ function createCoordinator(opts) {
10656
10823
  const config = {
10657
10824
  dataDir,
10658
10825
  publicUrl,
10659
- trustedOrigins: opts.trustedOrigins ?? [originOf2(publicUrl)],
10826
+ trustedOrigins: opts.trustedOrigins ?? [originOf(publicUrl)],
10660
10827
  webDist: opts.webDist ? resolve3(opts.webDist) : null,
10661
10828
  secureCookies: publicUrl.startsWith("https:"),
10662
10829
  unreachableAfterMs: opts.unreachableAfterMs ?? 6e4,
@@ -10669,12 +10836,11 @@ function createCoordinator(opts) {
10669
10836
  hub.onListenerError = (name, err) => log.error({ err, event: name }, "event listener failed");
10670
10837
  const store = new Store2(db, hub, opts.clock);
10671
10838
  const owner = new Owner(store, config);
10672
- const auth = new Auth(store, config, hub, owner);
10839
+ const auth = new Auth(store, config, hub);
10673
10840
  const adminToken = auth.ensureAdminToken();
10674
- if (config.oidc)
10675
- owner.markInitialized();
10676
10841
  const passkeys = new Passkeys(store, owner, config, auth);
10677
10842
  const oidc = config.oidc ? new Oidc(store, owner, config, auth) : null;
10843
+ const terminalLogins = new TerminalLogins(store, config, auth);
10678
10844
  const limits = opts.rateLimits === false ? null : opts.rateLimits ?? {};
10679
10845
  const storeClock = () => store.now().getTime();
10680
10846
  const limiters = {
@@ -10703,6 +10869,7 @@ function createCoordinator(opts) {
10703
10869
  owner,
10704
10870
  passkeys,
10705
10871
  oidc,
10872
+ terminalLogins,
10706
10873
  limiters,
10707
10874
  log,
10708
10875
  services: /* @__PURE__ */ new Map()
@@ -10810,10 +10977,10 @@ function createCoordinator(opts) {
10810
10977
  sweep();
10811
10978
  retentionTimer = setInterval(sweep, opts.retentionIntervalMs ?? 36e5);
10812
10979
  retentionTimer.unref();
10813
- if (!owner.isInitialized()) {
10814
- log.info("not set up yet: run `switchyard init` on this server");
10980
+ if (!config.oidc && auth.activeDeviceCount() === 0 && passkeys.count() === 0) {
10981
+ log.info("no device signed in yet: run `switchyard pair` on this machine");
10815
10982
  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");
10983
+ process.stderr.write("No device is signed in yet. Run `switchyard pair` on this machine for a single-use sign-in link.\n");
10817
10984
  }
10818
10985
  }
10819
10986
  },
@@ -10834,207 +11001,56 @@ function createCoordinator(opts) {
10834
11001
  return coordinator;
10835
11002
  }
10836
11003
 
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 };
11004
+ // packages/adapter-sdk/dist/discovery.js
11005
+ import { existsSync as existsSync6 } from "node:fs";
11006
+ import { readFile } from "node:fs/promises";
11007
+ import { homedir as homedir2 } from "node:os";
11008
+ import { isAbsolute, join as join6, resolve as resolve4 } from "node:path";
11009
+ function piAgentDir(env = process.env, home = homedir2()) {
11010
+ const fromEnv = env.PI_CODING_AGENT_DIR?.trim();
11011
+ if (fromEnv)
11012
+ return expandHome(fromEnv, home);
11013
+ return join6(home, ".pi", "agent");
10859
11014
  }
10860
- function parseOrigins(v) {
10861
- if (!v?.trim())
10862
- return void 0;
10863
- return v.split(",").map((o) => o.trim().replace(/\/+$/, "")).filter(Boolean);
11015
+ function defaultOwnerAdapterDir(home = homedir2()) {
11016
+ return join6(home, ".config", "switchyard", "adapters");
10864
11017
  }
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;
11018
+ function expandHome(p, home) {
11019
+ if (p === "~")
11020
+ return home;
11021
+ if (p.startsWith("~/"))
11022
+ return join6(home, p.slice(2));
11023
+ return p;
10870
11024
  }
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;
11025
+ function isLocalPath(s) {
11026
+ return s.startsWith(".") || s.startsWith("/") || s.startsWith("~") || /^[A-Za-z]:[\\/]/.test(s);
10901
11027
  }
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}"`);
11028
+ function parseNpmName(spec) {
11029
+ const s = spec.trim();
11030
+ if (s.startsWith("@")) {
11031
+ const at2 = s.indexOf("@", 1);
11032
+ return at2 === -1 ? s : s.slice(0, at2);
10908
11033
  }
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
- };
11034
+ const at = s.indexOf("@");
11035
+ return at === -1 ? s : s.slice(0, at);
10918
11036
  }
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
- // packages/adapter-sdk/dist/discovery.js
10989
- import { existsSync as existsSync7 } from "node:fs";
10990
- import { readFile } from "node:fs/promises";
10991
- import { homedir as homedir2 } from "node:os";
10992
- import { isAbsolute, join as join6, resolve as resolve5 } from "node:path";
10993
- function piAgentDir(env = process.env, home = homedir2()) {
10994
- const fromEnv = env.PI_CODING_AGENT_DIR?.trim();
10995
- if (fromEnv)
10996
- return expandHome(fromEnv, home);
10997
- return join6(home, ".pi", "agent");
10998
- }
10999
- function defaultOwnerAdapterDir(home = homedir2()) {
11000
- return join6(home, ".config", "switchyard", "adapters");
11001
- }
11002
- function expandHome(p, home) {
11003
- if (p === "~")
11004
- return home;
11005
- if (p.startsWith("~/"))
11006
- return join6(home, p.slice(2));
11007
- return p;
11008
- }
11009
- function isLocalPath(s) {
11010
- return s.startsWith(".") || s.startsWith("/") || s.startsWith("~") || /^[A-Za-z]:[\\/]/.test(s);
11011
- }
11012
- function parseNpmName(spec) {
11013
- const s = spec.trim();
11014
- if (s.startsWith("@")) {
11015
- const at2 = s.indexOf("@", 1);
11016
- return at2 === -1 ? s : s.slice(0, at2);
11017
- }
11018
- const at = s.indexOf("@");
11019
- return at === -1 ? s : s.slice(0, at);
11020
- }
11021
- function parseGitSource(source) {
11022
- let s = source.trim();
11023
- const prefixed = s.startsWith("git:");
11024
- if (prefixed)
11025
- s = s.slice(4).trim();
11026
- if (!prefixed && !/^(https?|ssh|git):\/\//i.test(s))
11027
- return null;
11028
- s = s.replace(/^(https?|ssh|git):\/\//i, "");
11029
- s = s.replace(/^[^@/]+@/, "");
11030
- s = s.replace(/^([^/:]+):(?!\d)/, "$1/");
11031
- s = s.split("#")[0] ?? s;
11032
- const [hostPart, ...rest] = s.split("/");
11033
- const host = (hostPart ?? "").replace(/:\d+$/, "");
11034
- const path = rest.join("/").replace(/@[^/]*$/, "").replace(/\/+$/, "").replace(/\.git$/, "");
11035
- if (!host || !path)
11036
- return null;
11037
- return { host, path };
11037
+ function parseGitSource(source) {
11038
+ let s = source.trim();
11039
+ const prefixed = s.startsWith("git:");
11040
+ if (prefixed)
11041
+ s = s.slice(4).trim();
11042
+ if (!prefixed && !/^(https?|ssh|git):\/\//i.test(s))
11043
+ return null;
11044
+ s = s.replace(/^(https?|ssh|git):\/\//i, "");
11045
+ s = s.replace(/^[^@/]+@/, "");
11046
+ s = s.replace(/^([^/:]+):(?!\d)/, "$1/");
11047
+ s = s.split("#")[0] ?? s;
11048
+ const [hostPart, ...rest] = s.split("/");
11049
+ const host = (hostPart ?? "").replace(/:\d+$/, "");
11050
+ const path = rest.join("/").replace(/@[^/]*$/, "").replace(/\/+$/, "").replace(/\.git$/, "");
11051
+ if (!host || !path)
11052
+ return null;
11053
+ return { host, path };
11038
11054
  }
11039
11055
  function parsePiPackageSource(source) {
11040
11056
  const s = source.trim();
@@ -11061,7 +11077,7 @@ function piPackageDirCandidates(source, agentDir, opts = {}) {
11061
11077
  return [join6(agentDir, "git", parsed.host, parsed.path)];
11062
11078
  case "local": {
11063
11079
  const p = expandHome(parsed.path, home);
11064
- return [isAbsolute(p) ? p : resolve5(agentDir, p)];
11080
+ return [isAbsolute(p) ? p : resolve4(agentDir, p)];
11065
11081
  }
11066
11082
  }
11067
11083
  }
@@ -11087,7 +11103,7 @@ async function discoverPiPackages(opts = {}) {
11087
11103
  };
11088
11104
  const diag = (level, message) => out.diagnostics.push({ sessionId: null, adapterId: null, level, message });
11089
11105
  const settingsPath = join6(agentDir, "settings.json");
11090
- if (!existsSync7(settingsPath))
11106
+ if (!existsSync6(settingsPath))
11091
11107
  return out;
11092
11108
  let settings;
11093
11109
  try {
@@ -11109,14 +11125,14 @@ async function discoverPiPackages(opts = {}) {
11109
11125
  home,
11110
11126
  globalNpmRoot: opts.globalNpmRoot
11111
11127
  });
11112
- const dir = candidates.find((c) => existsSync7(c));
11128
+ const dir = candidates.find((c) => existsSync6(c));
11113
11129
  if (!dir) {
11114
11130
  diag("warning", `Pi package ${source} could not be resolved (looked in ${candidates.join(", ")}); any adapter it provides is not loaded.`);
11115
11131
  continue;
11116
11132
  }
11117
11133
  let pkg = null;
11118
11134
  const pkgPath = join6(dir, "package.json");
11119
- if (existsSync7(pkgPath)) {
11135
+ if (existsSync6(pkgPath)) {
11120
11136
  try {
11121
11137
  pkg = await readJson(pkgPath);
11122
11138
  } catch (err) {
@@ -11268,7 +11284,7 @@ function genericWidgetPanel(lines) {
11268
11284
 
11269
11285
  // packages/adapter-sdk/dist/manifest.js
11270
11286
  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";
11287
+ import { dirname as dirname4, isAbsolute as isAbsolute2, join as join7, resolve as resolve5 } from "node:path";
11272
11288
  import { validRange } from "semver";
11273
11289
  import { z as z18 } from "zod";
11274
11290
  var MANIFEST_FILE = "switchyard-adapter.json";
@@ -11312,7 +11328,7 @@ function parseManifest(input, path = null) {
11312
11328
  throw new ManifestError(`invalid adapter manifest: ${issues}`, path);
11313
11329
  }
11314
11330
  async function loadManifest(pathOrDir) {
11315
- const abs = resolve6(pathOrDir);
11331
+ const abs = resolve5(pathOrDir);
11316
11332
  const manifestPath = abs.endsWith(".json") ? abs : join7(abs, MANIFEST_FILE);
11317
11333
  let raw;
11318
11334
  try {
@@ -11332,8 +11348,8 @@ async function loadManifest(pathOrDir) {
11332
11348
  manifest,
11333
11349
  manifestPath,
11334
11350
  dir,
11335
- serverPath: resolve6(dir, manifest.server),
11336
- webRendererPath: manifest.webRenderer ? resolve6(dir, manifest.webRenderer) : null
11351
+ serverPath: resolve5(dir, manifest.server),
11352
+ webRendererPath: manifest.webRenderer ? resolve5(dir, manifest.webRenderer) : null
11337
11353
  };
11338
11354
  }
11339
11355
 
@@ -11388,9 +11404,9 @@ function responseFitsRequest(req, resp) {
11388
11404
  }
11389
11405
 
11390
11406
  // packages/adapter-sdk/dist/registry.js
11391
- import { existsSync as existsSync8 } from "node:fs";
11407
+ import { existsSync as existsSync7 } from "node:fs";
11392
11408
  import { readdir, readFile as readFile3 } from "node:fs/promises";
11393
- import { join as join8, resolve as resolve7 } from "node:path";
11409
+ import { join as join8, resolve as resolve6 } from "node:path";
11394
11410
  import { pathToFileURL } from "node:url";
11395
11411
  import { satisfies } from "semver";
11396
11412
 
@@ -11474,6 +11490,10 @@ var AdapterSession = class {
11474
11490
  this.activeTools.set(e.toolCallId, e);
11475
11491
  await this.callAll("onToolStart", e);
11476
11492
  }
11493
+ /** `tool_execution_update`: runs `onToolUpdate` hooks with the partial result. */
11494
+ async onToolUpdate(e) {
11495
+ await this.callAll("onToolUpdate", e);
11496
+ }
11477
11497
  /** `tool_execution_end`: runs `onToolEnd` hooks, then expires answers queued for the call. */
11478
11498
  async onToolEnd(e) {
11479
11499
  try {
@@ -11811,7 +11831,7 @@ function isServerAdapter(x) {
11811
11831
  async function loadAdapterFromDir(pathOrDir, tier) {
11812
11832
  const m = await loadManifest(pathOrDir);
11813
11833
  const warnings = [];
11814
- if (!existsSync8(m.serverPath)) {
11834
+ if (!existsSync7(m.serverPath)) {
11815
11835
  throw new AdapterLoadError(`server module ${m.serverPath} does not exist`, m.manifest);
11816
11836
  }
11817
11837
  let mod;
@@ -11832,7 +11852,7 @@ async function loadAdapterFromDir(pathOrDir, tier) {
11832
11852
  throw new AdapterLoadError(`${m.serverPath} must default-export a ServerAdapter object or factory`, m.manifest);
11833
11853
  }
11834
11854
  let webRendererPath = m.webRendererPath;
11835
- if (webRendererPath && !existsSync8(webRendererPath)) {
11855
+ if (webRendererPath && !existsSync7(webRendererPath)) {
11836
11856
  warnings.push(`web renderer ${webRendererPath} does not exist; portable panels are used`);
11837
11857
  webRendererPath = null;
11838
11858
  }
@@ -11849,12 +11869,12 @@ async function loadAdapterFromDir(pathOrDir, tier) {
11849
11869
  };
11850
11870
  }
11851
11871
  async function subdirsWithManifest(root) {
11852
- if (!existsSync8(root))
11872
+ if (!existsSync7(root))
11853
11873
  return [];
11854
- if (existsSync8(join8(root, MANIFEST_FILE)))
11874
+ if (existsSync7(join8(root, MANIFEST_FILE)))
11855
11875
  return [root];
11856
11876
  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();
11877
+ return entries.filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => join8(root, e.name)).filter((d) => existsSync7(join8(d, MANIFEST_FILE))).sort();
11858
11878
  }
11859
11879
  var AdapterRegistry = class {
11860
11880
  opts;
@@ -11872,7 +11892,7 @@ var AdapterRegistry = class {
11872
11892
  this.diags = [];
11873
11893
  const candidates = [];
11874
11894
  if (opts.projectRoot) {
11875
- for (const d of await this.safeList(resolve7(opts.projectRoot, PROJECT_ADAPTER_DIR))) {
11895
+ for (const d of await this.safeList(resolve6(opts.projectRoot, PROJECT_ADAPTER_DIR))) {
11876
11896
  candidates.push({ tier: "project", path: d });
11877
11897
  }
11878
11898
  }
@@ -11887,7 +11907,7 @@ var AdapterRegistry = class {
11887
11907
  this.diagnostic("warning", null, `Cannot read ${pkgDir}/package.json: ${err.message}`);
11888
11908
  }
11889
11909
  for (const e of entries)
11890
- candidates.push({ tier: "plugin", path: resolve7(pkgDir, e) });
11910
+ candidates.push({ tier: "plugin", path: resolve6(pkgDir, e) });
11891
11911
  }
11892
11912
  const loaded = [];
11893
11913
  for (const c of candidates) {
@@ -12158,7 +12178,7 @@ async function runChecks(path, commands, opts = {}) {
12158
12178
 
12159
12179
  // packages/host-agent/dist/files.js
12160
12180
  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";
12181
+ import { join as join9, relative, resolve as resolve7, sep } from "node:path";
12162
12182
  var MAX_READ_BYTES = 1024 * 1024;
12163
12183
  var BINARY_SNIFF_BYTES = 8e3;
12164
12184
  function isInside(root, target) {
@@ -12182,7 +12202,7 @@ async function resolveInside(root, rel) {
12182
12202
  if (rel.includes("\0"))
12183
12203
  throw new SwitchyardError("invalid_request", "Invalid path");
12184
12204
  const rootReal = await realRoot(root);
12185
- const target = resolve8(rootReal, rel.replace(/^[/\\]+/, ""));
12205
+ const target = resolve7(rootReal, rel.replace(/^[/\\]+/, ""));
12186
12206
  if (!isInside(rootReal, target)) {
12187
12207
  throw new SwitchyardError("forbidden", "Path is outside the workspace");
12188
12208
  }
@@ -12261,7 +12281,7 @@ async function readWorkspaceFile(root, file) {
12261
12281
  import { execFile } from "node:child_process";
12262
12282
  import { appendFile, lstat as lstat2, mkdir, readFile as readFile5, realpath as realpath2, stat as stat2 } from "node:fs/promises";
12263
12283
  import { tmpdir } from "node:os";
12264
- import { join as join10, resolve as resolve9, sep as sep2 } from "node:path";
12284
+ import { join as join10, resolve as resolve8, sep as sep2 } from "node:path";
12265
12285
  var MAX_BUFFER = 64 * 1024 * 1024;
12266
12286
  var EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
12267
12287
  var MAX_PATCH_BYTES = 2 * 1024 * 1024;
@@ -12338,7 +12358,7 @@ async function realpathOr(path) {
12338
12358
  }
12339
12359
  async function commonDirOf(path) {
12340
12360
  const { stdout } = await runGit(path, ["rev-parse", "--git-common-dir"]);
12341
- return realpathOr(resolve9(path, stdout.trim()));
12361
+ return realpathOr(resolve8(path, stdout.trim()));
12342
12362
  }
12343
12363
  function notARepo(path) {
12344
12364
  return new SwitchyardError("not_a_git_repo", `${path} is not a git repository`);
@@ -12911,7 +12931,7 @@ function toErrorBody(err) {
12911
12931
  function queueKey(command) {
12912
12932
  if (command.type === "checks.run")
12913
12933
  return `checks:${command.sessionId}`;
12914
- if (command.type === "session.answer" || command.type === "session.abort" || command.type === "session.setControl" || command.type === "session.adapterAction" || command.type === "session.snapshot" || command.type === "session.models" || command.type === "session.commands") {
12934
+ if (command.type === "session.answer" || command.type === "session.abort" || command.type === "session.setControl" || command.type === "session.adapterAction" || command.type === "session.snapshot" || command.type === "session.models" || command.type === "session.commands" || command.type === "session.complete" || command.type === "session.tree" || command.type === "session.export" || command.type === "session.reload") {
12915
12935
  return `control:${command.sessionId}`;
12916
12936
  }
12917
12937
  if ("sessionId" in command && typeof command.sessionId === "string") {
@@ -13070,6 +13090,16 @@ var CommandRouter = class {
13070
13090
  return sup.models(c.sessionId);
13071
13091
  case "session.commands":
13072
13092
  return sup.commands(c.sessionId);
13093
+ case "session.complete":
13094
+ return sup.complete(c.sessionId, c.text);
13095
+ case "session.tree":
13096
+ return sup.tree(c.sessionId);
13097
+ case "session.branch":
13098
+ return sup.branch(c.sessionId, c.entryId, c.fork);
13099
+ case "session.reload":
13100
+ return sup.reload(c.sessionId);
13101
+ case "session.export":
13102
+ return sup.exportSession(c.sessionId);
13073
13103
  case "session.setModel":
13074
13104
  return sup.setModel(c.sessionId, c.provider, c.modelId);
13075
13105
  case "session.setThinking":
@@ -13112,8 +13142,8 @@ var CommandRouter = class {
13112
13142
  };
13113
13143
 
13114
13144
  // 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";
13145
+ import { chmodSync as chmodSync3, existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync5, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
13146
+ import { homedir as homedir4, hostname } from "node:os";
13117
13147
  import { dirname as dirname5, join as join12 } from "node:path";
13118
13148
  import { z as z21 } from "zod";
13119
13149
  var HostConfigSchema = z21.object({
@@ -13148,11 +13178,11 @@ function statePaths(stateDir) {
13148
13178
  logs: join12(stateDir, "logs")
13149
13179
  };
13150
13180
  for (const d of [p.root, p.runtime, p.logs])
13151
- mkdirSync5(d, { recursive: true, mode: 448 });
13181
+ mkdirSync4(d, { recursive: true, mode: 448 });
13152
13182
  return p;
13153
13183
  }
13154
13184
  function loadConfig(path) {
13155
- if (!existsSync9(path)) {
13185
+ if (!existsSync8(path)) {
13156
13186
  throw new SwitchyardError("not_found", `Host config ${path} not found; enroll this host first (switchyard host enroll)`);
13157
13187
  }
13158
13188
  let raw;
@@ -13171,7 +13201,7 @@ function loadConfig(path) {
13171
13201
  }
13172
13202
  function saveConfig(path, config) {
13173
13203
  const cfg = HostConfigSchema.parse(config);
13174
- mkdirSync5(dirname5(path), { recursive: true, mode: 448 });
13204
+ mkdirSync4(dirname5(path), { recursive: true, mode: 448 });
13175
13205
  const tmp = `${path}.${process.pid}.tmp`;
13176
13206
  writeFileSync4(tmp, `${JSON.stringify(cfg, null, 2)}
13177
13207
  `, { mode: 384 });
@@ -13249,16 +13279,16 @@ function abortError(signal) {
13249
13279
  async function waitForConfig(opts) {
13250
13280
  const { configPath, env, log, signal } = opts;
13251
13281
  const t = { ...DEFAULT_TIMING, ...opts.timing };
13252
- if (existsSync9(configPath))
13282
+ if (existsSync8(configPath))
13253
13283
  return loadConfig(configPath);
13254
13284
  const secret = env.SWITCHYARD_HOST_ENROLL_SECRET?.trim();
13255
13285
  const coordinatorUrl = env.SWITCHYARD_COORDINATOR_URL?.trim();
13256
13286
  if (secret && coordinatorUrl) {
13257
- const name = env.SWITCHYARD_HOST_NAME?.trim() || hostname2();
13287
+ const name = env.SWITCHYARD_HOST_NAME?.trim() || hostname();
13258
13288
  for (; ; ) {
13259
13289
  if (signal?.aborted)
13260
13290
  throw abortError(signal);
13261
- if (existsSync9(configPath))
13291
+ if (existsSync8(configPath))
13262
13292
  return loadConfig(configPath);
13263
13293
  try {
13264
13294
  const enrolled = await enroll({
@@ -13288,7 +13318,7 @@ async function waitForConfig(opts) {
13288
13318
  for (; ; ) {
13289
13319
  if (signal?.aborted)
13290
13320
  throw abortError(signal);
13291
- if (existsSync9(configPath)) {
13321
+ if (existsSync8(configPath)) {
13292
13322
  try {
13293
13323
  return loadConfig(configPath);
13294
13324
  } catch (err) {
@@ -13308,7 +13338,7 @@ async function waitForConfig(opts) {
13308
13338
  }
13309
13339
 
13310
13340
  // packages/host-agent/dist/db/index.js
13311
- import { mkdirSync as mkdirSync6 } from "node:fs";
13341
+ import { mkdirSync as mkdirSync5 } from "node:fs";
13312
13342
  import { dirname as dirname6 } from "node:path";
13313
13343
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
13314
13344
 
@@ -13392,7 +13422,7 @@ var HostDb = class {
13392
13422
  constructor(path) {
13393
13423
  this.path = path;
13394
13424
  if (path !== ":memory:")
13395
- mkdirSync6(dirname6(path), { recursive: true });
13425
+ mkdirSync5(dirname6(path), { recursive: true });
13396
13426
  this.db = new DatabaseSync3(path);
13397
13427
  this.db.exec("PRAGMA journal_mode = WAL");
13398
13428
  this.db.exec("PRAGMA foreign_keys = ON");
@@ -13468,15 +13498,15 @@ var HostDb = class {
13468
13498
  else
13469
13499
  fn();
13470
13500
  }
13471
- run(sql3, params = []) {
13472
- const r = this.db.prepare(sql3).run(...params);
13501
+ run(sql4, params = []) {
13502
+ const r = this.db.prepare(sql4).run(...params);
13473
13503
  return { changes: Number(r.changes), lastInsertRowid: Number(r.lastInsertRowid) };
13474
13504
  }
13475
- get(sql3, params = []) {
13476
- return this.db.prepare(sql3).get(...params);
13505
+ get(sql4, params = []) {
13506
+ return this.db.prepare(sql4).get(...params);
13477
13507
  }
13478
- all(sql3, params = []) {
13479
- return this.db.prepare(sql3).all(...params);
13508
+ all(sql4, params = []) {
13509
+ return this.db.prepare(sql4).all(...params);
13480
13510
  }
13481
13511
  // ---- meta ----
13482
13512
  getMeta(key) {
@@ -13486,12 +13516,12 @@ var HostDb = class {
13486
13516
  this.run("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value", [key, value]);
13487
13517
  }
13488
13518
  // ---- JSON row helpers ----
13489
- getJson(sql3, params = []) {
13490
- const r = this.get(sql3, params);
13519
+ getJson(sql4, params = []) {
13520
+ const r = this.get(sql4, params);
13491
13521
  return r ? JSON.parse(r.json) : void 0;
13492
13522
  }
13493
- allJson(sql3, params = []) {
13494
- return this.all(sql3, params).map((r) => JSON.parse(r.json));
13523
+ allJson(sql4, params = []) {
13524
+ return this.all(sql4, params).map((r) => JSON.parse(r.json));
13495
13525
  }
13496
13526
  close() {
13497
13527
  try {
@@ -14150,7 +14180,7 @@ ${stderr}`));
14150
14180
  import { accessSync, constants, readFileSync as readFileSync6, realpathSync } from "node:fs";
14151
14181
  import { open, readdir as readdir4, readFile as readFile6, stat as stat4 } from "node:fs/promises";
14152
14182
  import { homedir as homedir5 } from "node:os";
14153
- import { delimiter, dirname as dirname7, join as join13, resolve as resolve10 } from "node:path";
14183
+ import { delimiter, dirname as dirname7, join as join13, resolve as resolve9 } from "node:path";
14154
14184
  import { pathToFileURL as pathToFileURL2 } from "node:url";
14155
14185
 
14156
14186
  // packages/pi-runtime/dist/transcript.js
@@ -14413,7 +14443,7 @@ async function listImportable(sessionsRoot = defaultSessionsRoot(), now = Date.n
14413
14443
  }
14414
14444
  function findOnPath(bin) {
14415
14445
  if (bin.includes("/"))
14416
- return resolve10(bin);
14446
+ return resolve9(bin);
14417
14447
  for (const dir of (process.env.PATH ?? "").split(delimiter)) {
14418
14448
  if (!dir)
14419
14449
  continue;
@@ -14672,14 +14702,117 @@ function writeJsonl(stream, value) {
14672
14702
  });
14673
14703
  }
14674
14704
 
14705
+ // packages/pi-runtime/dist/pi-session-manager.js
14706
+ import { existsSync as existsSync9 } from "node:fs";
14707
+ import { dirname as dirname8, join as join14 } from "node:path";
14708
+ import { pathToFileURL as pathToFileURL3 } from "node:url";
14709
+ var managerPromise = null;
14710
+ function loadPiSessionManager(piBin = "pi") {
14711
+ managerPromise ??= (async () => {
14712
+ const root = resolvePiPackageRoot(piBin);
14713
+ if (!root)
14714
+ return null;
14715
+ try {
14716
+ const mod = await import(pathToFileURL3(join14(root, "dist", "index.js")).href);
14717
+ return mod.SessionManager ?? null;
14718
+ } catch {
14719
+ return null;
14720
+ }
14721
+ })();
14722
+ return managerPromise;
14723
+ }
14724
+ function messageText(message) {
14725
+ const c = message?.content;
14726
+ if (typeof c === "string")
14727
+ return c;
14728
+ if (!Array.isArray(c))
14729
+ return "";
14730
+ return c.filter((p) => p?.type === "text" && typeof p.text === "string").map((p) => p.text).join(" ");
14731
+ }
14732
+ var MAX_ROWS = 2e3;
14733
+ function treeRows(sm) {
14734
+ const active = new Set((sm.getBranch() ?? []).map((e) => e.id));
14735
+ const leafId = sm.getLeafId?.() ?? null;
14736
+ const rows = [];
14737
+ const walk = (nodes, depth, parentId) => {
14738
+ for (const node of nodes) {
14739
+ if (rows.length >= MAX_ROWS)
14740
+ return;
14741
+ const entry = node.entry;
14742
+ const role = entry?.message?.role;
14743
+ const shown = entry?.type === "message" && (role === "user" || role === "assistant");
14744
+ if (shown) {
14745
+ const raw = messageText(entry.message).replace(/\s+/g, " ").trim();
14746
+ const text = raw ? raw.slice(0, 160) : role === "assistant" ? "(tool calls)" : "";
14747
+ rows.push({
14748
+ entryId: entry.id,
14749
+ parentId,
14750
+ depth,
14751
+ role,
14752
+ text,
14753
+ activeBranch: active.has(entry.id),
14754
+ leaf: entry.id === leafId,
14755
+ label: node.label ?? null,
14756
+ at: typeof entry.timestamp === "string" ? entry.timestamp : null
14757
+ });
14758
+ walk(node.children ?? [], depth + 1, entry.id);
14759
+ } else {
14760
+ walk(node.children ?? [], depth, parentId);
14761
+ }
14762
+ }
14763
+ };
14764
+ walk(sm.getTree() ?? [], 0, null);
14765
+ if (!rows.some((r) => r.leaf)) {
14766
+ const last = [...rows].reverse().find((r) => r.activeBranch);
14767
+ if (last)
14768
+ last.leaf = true;
14769
+ }
14770
+ return rows;
14771
+ }
14772
+ async function branchSession(sessionFile, entryId, fork, piBin, load = loadPiSessionManager, checkFile = true) {
14773
+ const SM = await load(piBin);
14774
+ if (!SM) {
14775
+ throw new SwitchyardError("invalid_request", "Pi's SessionManager could not be loaded on this host");
14776
+ }
14777
+ const sm = SM.open(sessionFile, dirname8(sessionFile));
14778
+ let target;
14779
+ let prefill = null;
14780
+ if (fork) {
14781
+ const e = entryId ? sm.getEntry(entryId) : void 0;
14782
+ if (!e || e.type !== "message" || e.message?.role !== "user") {
14783
+ throw new SwitchyardError("invalid_request", "Pick one of your messages to fork from");
14784
+ }
14785
+ prefill = messageText(e.message);
14786
+ target = e.parentId ?? null;
14787
+ if (!target)
14788
+ return { sessionFile: null, prefill };
14789
+ } else {
14790
+ target = entryId ?? sm.getLeafId();
14791
+ if (!target) {
14792
+ throw new SwitchyardError("invalid_request", "This session has nothing to branch yet");
14793
+ }
14794
+ }
14795
+ const hasMessages = (sm.getBranch(target) ?? []).some((e) => e?.type === "message" && (e.message?.role === "user" || e.message?.role === "assistant"));
14796
+ if (!hasMessages) {
14797
+ if (fork)
14798
+ return { sessionFile: null, prefill };
14799
+ throw new SwitchyardError("invalid_request", "There is no conversation before that point");
14800
+ }
14801
+ const file = sm.createBranchedSession(target);
14802
+ if (!file || checkFile && !existsSync9(file)) {
14803
+ throw new SwitchyardError("invalid_request", "Pi did not write a branched session");
14804
+ }
14805
+ return { sessionFile: file, prefill };
14806
+ }
14807
+
14675
14808
  // packages/pi-runtime/dist/rpc-process.js
14676
14809
  import { spawn as spawn2 } from "node:child_process";
14677
14810
  import { randomUUID } from "node:crypto";
14678
14811
  import { EventEmitter as EventEmitter3 } from "node:events";
14679
- import { chmodSync as chmodSync4, existsSync as existsSync10, mkdirSync as mkdirSync7, rmSync as rmSync2 } from "node:fs";
14812
+ import { chmodSync as chmodSync4, existsSync as existsSync10, mkdirSync as mkdirSync6, rmSync as rmSync2 } from "node:fs";
14680
14813
  import { createServer } from "node:net";
14681
14814
  import { tmpdir as tmpdir2 } from "node:os";
14682
- import { join as join14 } from "node:path";
14815
+ import { join as join15 } from "node:path";
14683
14816
  import { fileURLToPath as fileURLToPath2 } from "node:url";
14684
14817
  function bridgeExtensionPath() {
14685
14818
  const sibling = fileURLToPath2(new URL("./bridge/switchyard-bridge.js", import.meta.url));
@@ -14699,11 +14832,11 @@ var MAX_SOCKET_PATH = 100;
14699
14832
  function shortSocketPath(runtimeDir, prefix) {
14700
14833
  const name = `${prefix}-${randomUUID().slice(0, 8)}.sock`;
14701
14834
  let dir = runtimeDir;
14702
- if (join14(dir, name).length > MAX_SOCKET_PATH) {
14703
- dir = join14("/tmp", `switchyard-${process.getuid?.() ?? "u"}`);
14835
+ if (join15(dir, name).length > MAX_SOCKET_PATH) {
14836
+ dir = join15("/tmp", `switchyard-${process.getuid?.() ?? "u"}`);
14704
14837
  }
14705
- mkdirSync7(dir, { recursive: true, mode: 448 });
14706
- return join14(dir, name);
14838
+ mkdirSync6(dir, { recursive: true, mode: 448 });
14839
+ return join15(dir, name);
14707
14840
  }
14708
14841
  var PiRpcProcess = class _PiRpcProcess extends EventEmitter3 {
14709
14842
  child;
@@ -14756,7 +14889,7 @@ var PiRpcProcess = class _PiRpcProcess extends EventEmitter3 {
14756
14889
  let proc = null;
14757
14890
  const early = [];
14758
14891
  if (useBridge) {
14759
- socketPath = shortSocketPath(opts.runtimeDir ?? join14(tmpdir2(), "switchyard"), "bridge");
14892
+ socketPath = shortSocketPath(opts.runtimeDir ?? join15(tmpdir2(), "switchyard"), "bridge");
14760
14893
  server = createServer((socket) => {
14761
14894
  if (proc)
14762
14895
  proc.attachBridgeSocket(socket);
@@ -14943,14 +15076,46 @@ import { randomUUID as randomUUID2 } from "node:crypto";
14943
15076
  import { EventEmitter as EventEmitter5 } from "node:events";
14944
15077
  import { existsSync as existsSync12 } from "node:fs";
14945
15078
 
15079
+ // packages/pi-runtime/dist/command-hints.js
15080
+ import { readFile as readFile7 } from "node:fs/promises";
15081
+ function parseArgumentHint(markdown) {
15082
+ const lines = markdown.split(/\r?\n/);
15083
+ if (lines[0] !== "---")
15084
+ return null;
15085
+ const end = lines.indexOf("---", 1);
15086
+ if (end < 0)
15087
+ return null;
15088
+ for (const line of lines.slice(1, end)) {
15089
+ const m = /^argument-hint:\s*(.*)$/.exec(line);
15090
+ if (!m)
15091
+ continue;
15092
+ let value = (m[1] ?? "").trim();
15093
+ const q = value[0];
15094
+ if (value.length >= 2 && (q === '"' || q === "'") && value.endsWith(q)) {
15095
+ value = value.slice(1, -1).trim();
15096
+ }
15097
+ return value ? value.slice(0, 80) : null;
15098
+ }
15099
+ return null;
15100
+ }
15101
+ async function readArgumentHint(path) {
15102
+ if (!path.endsWith(".md"))
15103
+ return null;
15104
+ try {
15105
+ return parseArgumentHint(await readFile7(path, "utf8"));
15106
+ } catch {
15107
+ return null;
15108
+ }
15109
+ }
15110
+
14946
15111
  // packages/pi-runtime/dist/terminal-process.js
14947
15112
  import { spawn as spawn3 } from "node:child_process";
14948
15113
  import { EventEmitter as EventEmitter4 } from "node:events";
14949
- import { closeSync, existsSync as existsSync11, mkdirSync as mkdirSync8, openSync, readFileSync as readFileSync7 } from "node:fs";
15114
+ import { closeSync, existsSync as existsSync11, mkdirSync as mkdirSync7, openSync, readFileSync as readFileSync7 } from "node:fs";
14950
15115
  import { createRequire } from "node:module";
14951
15116
  import { createConnection } from "node:net";
14952
15117
  import { tmpdir as tmpdir3 } from "node:os";
14953
- import { basename as basename2, dirname as dirname8, join as join15 } from "node:path";
15118
+ import { basename as basename2, dirname as dirname9, join as join16 } from "node:path";
14954
15119
  import { fileURLToPath as fileURLToPath3 } from "node:url";
14955
15120
  function resolveDist(rel) {
14956
15121
  const sibling = fileURLToPath3(new URL(`./${rel}`, import.meta.url));
@@ -15036,7 +15201,7 @@ var PiTerminalProcess = class _PiTerminalProcess extends EventEmitter4 {
15036
15201
  }
15037
15202
  /** Start a holder running interactive Pi and attach to it. */
15038
15203
  static async start(opts) {
15039
- const runtimeDir = opts.runtimeDir ?? join15(tmpdir3(), "switchyard");
15204
+ const runtimeDir = opts.runtimeDir ?? join16(tmpdir3(), "switchyard");
15040
15205
  const socketPath = shortSocketPath(runtimeDir, "term");
15041
15206
  const env = {};
15042
15207
  for (const [k, v] of Object.entries({ ...process.env, ...opts.env ?? {} })) {
@@ -15057,8 +15222,8 @@ var PiTerminalProcess = class _PiTerminalProcess extends EventEmitter4 {
15057
15222
  cols: opts.cols ?? 120,
15058
15223
  rows: opts.rows ?? 40
15059
15224
  };
15060
- const logPath = opts.logPath ?? join15(dirname8(socketPath), `${basename2(socketPath, ".sock")}.log`);
15061
- mkdirSync8(dirname8(logPath), { recursive: true, mode: 448 });
15225
+ const logPath = opts.logPath ?? join16(dirname9(socketPath), `${basename2(socketPath, ".sock")}.log`);
15226
+ mkdirSync7(dirname9(logPath), { recursive: true, mode: 448 });
15062
15227
  const fd = openSync(logPath, "a", 384);
15063
15228
  try {
15064
15229
  const child = spawn3(process.execPath, [holderScriptPath()], {
@@ -15210,6 +15375,7 @@ var PiTerminalProcess = class _PiTerminalProcess extends EventEmitter4 {
15210
15375
  type: rec.event === "ui_prompt_end" ? "ui_prompt_end" : "ui_prompt_start",
15211
15376
  kind: typeof rec.kind === "string" ? rec.kind : "unknown",
15212
15377
  title: typeof rec.title === "string" ? rec.title : null,
15378
+ remoteBacked: rec.remoteBacked === true,
15213
15379
  seq: ++this.seq,
15214
15380
  at: (/* @__PURE__ */ new Date()).toISOString(),
15215
15381
  pid: this.pid ?? 0
@@ -15517,7 +15683,22 @@ var ManagedSession = class _ManagedSession extends EventEmitter5 {
15517
15683
  } else {
15518
15684
  data = await this.call(proc, { type: "follow_up", ...body });
15519
15685
  }
15520
- return data?.disposition ?? "started";
15686
+ if (data?.disposition === "view") {
15687
+ const view = CommandView.safeParse(data.view);
15688
+ if (!view.success) {
15689
+ throw new SwitchyardError("invalid_request", "The command returned an invalid view");
15690
+ }
15691
+ return { disposition: "view", view: view.data };
15692
+ }
15693
+ return { disposition: data?.disposition ?? "started", view: null };
15694
+ }
15695
+ /** Reload Pi's extensions, skills, prompts and context files (terminal sessions only). */
15696
+ async reload() {
15697
+ const proc = this.requireAlive();
15698
+ if (!(proc instanceof PiTerminalProcess)) {
15699
+ throw new SwitchyardError("invalid_request", "Reload needs a terminal session");
15700
+ }
15701
+ await this.call(proc, { type: "reload" }, 1e4);
15521
15702
  }
15522
15703
  /** Abort the current run; with `clearQueue`, first clear and return queued messages. */
15523
15704
  async abort(clearQueue = false) {
@@ -15569,19 +15750,36 @@ var ManagedSession = class _ManagedSession extends EventEmitter5 {
15569
15750
  type: "get_commands"
15570
15751
  });
15571
15752
  const out = [];
15753
+ const hints = [];
15572
15754
  for (const c of data?.commands ?? []) {
15573
15755
  if (typeof c?.name !== "string" || !c.name)
15574
15756
  continue;
15575
15757
  if (c.source !== "extension" && c.source !== "prompt" && c.source !== "skill")
15576
15758
  continue;
15577
- out.push({
15759
+ const entry = {
15578
15760
  name: c.name,
15579
15761
  description: typeof c.description === "string" ? c.description : null,
15580
- source: c.source
15581
- });
15762
+ source: c.source,
15763
+ argumentHint: null
15764
+ };
15765
+ if (c.source === "prompt" && typeof c.sourceInfo?.path === "string") {
15766
+ hints.push(readArgumentHint(c.sourceInfo.path).then((h) => {
15767
+ entry.argumentHint = h;
15768
+ }));
15769
+ }
15770
+ out.push(entry);
15582
15771
  }
15772
+ await Promise.all(hints);
15583
15773
  return out.sort((a, b) => a.name.localeCompare(b.name));
15584
15774
  }
15775
+ /** Pi's argument completions for a draft such as `/model cl` (terminal sessions only). */
15776
+ async complete(text) {
15777
+ const proc = this.requireAlive();
15778
+ if (!(proc instanceof PiTerminalProcess))
15779
+ return { items: [], prefix: "" };
15780
+ const data = await this.call(proc, { type: "complete", text }, 5e3);
15781
+ return SessionCompleteResult.parse({ items: data?.items ?? [], prefix: data?.prefix ?? "" });
15782
+ }
15585
15783
  async setModel(provider, modelId) {
15586
15784
  await this.call(this.requireAlive(), { type: "set_model", provider, modelId });
15587
15785
  await this.refreshModelState();
@@ -16181,7 +16379,8 @@ ${output}`.trimEnd() }));
16181
16379
 
16182
16380
  // packages/host-agent/dist/supervisor.js
16183
16381
  import { existsSync as existsSync13, realpathSync as realpathSync2, statSync } from "node:fs";
16184
- import { join as join16, sep as sep3 } from "node:path";
16382
+ import { readFile as readFile8, stat as stat5 } from "node:fs/promises";
16383
+ import { basename as basename3, dirname as dirname10, join as join17, sep as sep3 } from "node:path";
16185
16384
  var RESTART_NOTICE = "Host agent restarted; resume to continue";
16186
16385
  var QUEUED_ANSWER_SUMMARY = "Answered from your earlier form";
16187
16386
  var PI_CLOSED_SUMMARY = "Pi closed this question (timeout or abort)";
@@ -16547,6 +16746,8 @@ var SessionSupervisor = class {
16547
16746
  nextOrder = 0;
16548
16747
  }
16549
16748
  }
16749
+ const t0 = Date.now();
16750
+ let adaptersMs = 0;
16550
16751
  const now = nowIso();
16551
16752
  let control = { controlRevision: p.controlRevision, controlMode: p.controlMode };
16552
16753
  for (const c of [prev, this.pendingControl.get(p.sessionId)]) {
@@ -16568,7 +16769,7 @@ var SessionSupervisor = class {
16568
16769
  profile: p.profile,
16569
16770
  nativeHandle,
16570
16771
  runtime: "starting",
16571
- runtimeDetail: null,
16772
+ runtimeDetail: "Starting Pi\u2026",
16572
16773
  controlRevision: control.controlRevision,
16573
16774
  controlMode: control.controlMode,
16574
16775
  origin: p.origin,
@@ -16588,7 +16789,11 @@ var SessionSupervisor = class {
16588
16789
  const l = this.newLive(rec);
16589
16790
  this.live.set(rec.id, l);
16590
16791
  this.db.tx(() => this.saveAndEmit(rec));
16591
- await this.openAdapters(l, p.workspacePath);
16792
+ const adaptersStart = Date.now();
16793
+ this.enqueue(l, async () => {
16794
+ await this.openAdapters(l, p.workspacePath);
16795
+ adaptersMs = Date.now() - adaptersStart;
16796
+ });
16592
16797
  const ms = new ManagedSession({
16593
16798
  sessionId: rec.id,
16594
16799
  cwd: p.workspacePath,
@@ -16604,8 +16809,16 @@ var SessionSupervisor = class {
16604
16809
  });
16605
16810
  l.ms = ms;
16606
16811
  this.wire(l, ms);
16812
+ const piStart = Date.now();
16607
16813
  try {
16608
16814
  await ms.start();
16815
+ this.opts.log.info({
16816
+ sessionId: rec.id,
16817
+ piStartMs: Date.now() - piStart,
16818
+ adaptersMs,
16819
+ totalMs: Date.now() - t0,
16820
+ transport: ms.transport
16821
+ }, "session started");
16609
16822
  const socketPath = ms.terminalSocket;
16610
16823
  rec.terminal = socketPath ? { socketPath } : null;
16611
16824
  this.db.tx(() => this.saveAndEmit(rec));
@@ -16639,8 +16852,8 @@ var SessionSupervisor = class {
16639
16852
  }
16640
16853
  async prompt(sessionId, text, images, mode) {
16641
16854
  const ms = this.requireLive(sessionId);
16642
- const disposition = await ms.prompt(text, images, mode);
16643
- return { disposition };
16855
+ const { disposition, view } = await ms.prompt(text, images, mode);
16856
+ return { disposition, view };
16644
16857
  }
16645
16858
  async abort(sessionId, clearQueue) {
16646
16859
  const ms = this.requireLive(sessionId);
@@ -16676,6 +16889,41 @@ var SessionSupervisor = class {
16676
16889
  async commands(sessionId) {
16677
16890
  return { commands: await this.requireLive(sessionId).commands() };
16678
16891
  }
16892
+ async complete(sessionId, text) {
16893
+ return this.requireLive(sessionId).complete(text);
16894
+ }
16895
+ async reload(sessionId) {
16896
+ await this.requireLive(sessionId).reload();
16897
+ return { ok: true };
16898
+ }
16899
+ piFile(sessionId) {
16900
+ const file = this.requireRecord(sessionId).nativeHandle?.sessionFile;
16901
+ if (!file)
16902
+ throw new SwitchyardError("invalid_request", "This session has no Pi session file");
16903
+ return file;
16904
+ }
16905
+ async tree(sessionId) {
16906
+ const file = this.piFile(sessionId);
16907
+ const SM = await loadPiSessionManager(this.opts.piBin);
16908
+ if (!SM) {
16909
+ throw new SwitchyardError("invalid_request", "Pi's SessionManager could not be loaded on this host");
16910
+ }
16911
+ return { rows: treeRows(SM.open(file, dirname10(file))) };
16912
+ }
16913
+ async branch(sessionId, entryId, fork) {
16914
+ return branchSession(this.piFile(sessionId), entryId, fork, this.opts.piBin);
16915
+ }
16916
+ async exportSession(sessionId) {
16917
+ const file = this.piFile(sessionId);
16918
+ const info = await stat5(file).catch(() => null);
16919
+ if (!info) {
16920
+ throw new SwitchyardError("native_session_missing", `Native session file not found: ${file}`);
16921
+ }
16922
+ if (info.size > 50 * 1024 * 1024) {
16923
+ throw new SwitchyardError("invalid_request", "Session file is too large to export (over 50 MB)");
16924
+ }
16925
+ return { fileName: basename3(file), content: await readFile8(file, "utf8") };
16926
+ }
16679
16927
  async setModel(sessionId, provider, modelId) {
16680
16928
  return this.requireLive(sessionId).setModel(provider, modelId);
16681
16929
  }
@@ -16744,7 +16992,7 @@ var SessionSupervisor = class {
16744
16992
  }
16745
16993
  async importable() {
16746
16994
  const env = { ...process.env, ...this.opts.env ?? {} };
16747
- const root = join16(piAgentDir(env), "sessions");
16995
+ const root = join17(piAgentDir(env), "sessions");
16748
16996
  const list = await listImportable(root);
16749
16997
  const liveFiles = new Set([...this.live.values()].flatMap((l) => l.rec.nativeHandle ? [realpathOr2(l.rec.nativeHandle.sessionFile)] : []));
16750
16998
  return {
@@ -17013,6 +17261,21 @@ var SessionSupervisor = class {
17013
17261
  await this.applyToolCard(l, e);
17014
17262
  return;
17015
17263
  }
17264
+ case "tool_execution_update": {
17265
+ if (ev.parentToolCallId || !adapters)
17266
+ return;
17267
+ const id = String(ev.toolCallId);
17268
+ const pr = ev.partialResult;
17269
+ const details = pr && typeof pr === "object" && "details" in pr ? pr.details : void 0;
17270
+ await adapters.onToolUpdate({
17271
+ toolCallId: id,
17272
+ name: String(ev.toolName),
17273
+ args: ms?.toolItem(id)?.data.args ?? {},
17274
+ partialResult: pr,
17275
+ details
17276
+ });
17277
+ return;
17278
+ }
17016
17279
  case "tool_execution_end": {
17017
17280
  if (ev.parentToolCallId || !adapters)
17018
17281
  return;
@@ -17215,6 +17478,8 @@ var SessionSupervisor = class {
17215
17478
  */
17216
17479
  onTerminalPrompt(l, b) {
17217
17480
  if (b.type === "ui_prompt_start") {
17481
+ if (b.kind === "custom" && b.remoteBacked)
17482
+ return;
17218
17483
  if (b.kind !== "custom" && l.ms?.remoteDialogs)
17219
17484
  return;
17220
17485
  if (l.terminalPrompt)
@@ -17541,9 +17806,10 @@ var WorkspaceService = class {
17541
17806
  }
17542
17807
  async create(checkoutPath, branch, base, setupCommand, worktreeDir) {
17543
17808
  const wt = await createWorktree(checkoutPath, branch, base, worktreeDir);
17544
- const git = await worktreeStatus(wt.path, wt.baseBranch).catch(() => null);
17545
17809
  const initial = setupCommand ? { state: "running", log: "" } : { state: "none", log: "" };
17546
- this.emitUpdate(wt.path, { branch: wt.branch, baseBranch: wt.baseBranch, git, setup: initial });
17810
+ this.emitUpdate(wt.path, { branch: wt.branch, baseBranch: wt.baseBranch, setup: initial });
17811
+ void worktreeStatus(wt.path, wt.baseBranch).then((git) => this.emitUpdate(wt.path, { git }), () => {
17812
+ });
17547
17813
  let setupDone = Promise.resolve(initial);
17548
17814
  if (setupCommand) {
17549
17815
  setupDone = runSetup(wt.path, setupCommand, (setup) => this.emitUpdate(wt.path, { setup }), {
@@ -17565,7 +17831,7 @@ var WorkspaceService = class {
17565
17831
  branch: wt.branch,
17566
17832
  baseBranch: wt.baseBranch,
17567
17833
  provenance: "managed",
17568
- git,
17834
+ git: null,
17569
17835
  setup: initial,
17570
17836
  setupDone
17571
17837
  };
@@ -17710,59 +17976,489 @@ async function runHostAgent(opts = {}) {
17710
17976
  if (interruptedOnStart.length > 0 || failedDispatches > 0) {
17711
17977
  log.warn({ sessions: interruptedOnStart.length, dispatches: failedDispatches }, "recovered after restart");
17712
17978
  }
17713
- 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");
17979
+ router.pruneReceipts();
17980
+ try {
17981
+ await catalog.init();
17982
+ } catch (err) {
17983
+ log.warn({ err: err.message }, "adapter discovery failed; Pi package adapters are not loaded");
17984
+ }
17985
+ const reattach = await supervisor.reattachTerminals();
17986
+ interruptedOnStart.push(...reattach.lost);
17987
+ if (reattach.reattached.length > 0 || reattach.lost.length > 0) {
17988
+ log.info({ reattached: reattach.reattached.length, lost: reattach.lost.length }, "terminal sessions after restart");
17989
+ }
17990
+ supervisor.emitAdapters();
17991
+ link.start();
17992
+ let stopping = null;
17993
+ const onSignal = (signal) => {
17994
+ log.info({ signal }, "stopping host agent");
17995
+ void handle.stop().finally(() => process.exit(0));
17996
+ };
17997
+ if (opts.handleSignals !== false) {
17998
+ process.once("SIGTERM", onSignal);
17999
+ process.once("SIGINT", onSignal);
18000
+ }
18001
+ const handle = {
18002
+ hostId: config.hostId,
18003
+ config,
18004
+ db,
18005
+ link,
18006
+ supervisor,
18007
+ router,
18008
+ dispatcher,
18009
+ pi,
18010
+ interruptedOnStart,
18011
+ terminalSessions: terminalEnabled,
18012
+ get spawnedPiProcesses() {
18013
+ return supervisor.spawnedProcesses;
18014
+ },
18015
+ stop(stopOpts = {}) {
18016
+ stopping ??= (async () => {
18017
+ process.off("SIGTERM", onSignal);
18018
+ process.off("SIGINT", onSignal);
18019
+ relay.closeAll();
18020
+ await supervisor.stopAll(stopOpts);
18021
+ await new Promise((r) => setTimeout(r, 50));
18022
+ await link.stop();
18023
+ db.close();
18024
+ })();
18025
+ return stopping;
18026
+ }
18027
+ };
18028
+ return handle;
18029
+ }
18030
+
18031
+ // packages/cli/dist/commands/coordinator.js
18032
+ import { existsSync as existsSync14, mkdirSync as mkdirSync8 } from "node:fs";
18033
+ import { resolve as resolve10 } from "node:path";
18034
+ function parseListen(v) {
18035
+ const s = v.trim();
18036
+ let host = "0.0.0.0";
18037
+ let portStr = s;
18038
+ const v6 = s.match(/^\[([^\]]+)\]:(\d+)$/);
18039
+ if (v6) {
18040
+ host = v6[1];
18041
+ portStr = v6[2];
18042
+ } else if (s.includes(":")) {
18043
+ const i = s.lastIndexOf(":");
18044
+ host = s.slice(0, i) || "0.0.0.0";
18045
+ portStr = s.slice(i + 1);
18046
+ } else if (!/^\d+$/.test(s)) {
18047
+ host = s;
18048
+ portStr = String(DEFAULT_PORT);
18049
+ }
18050
+ const port = Number(portStr);
18051
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
18052
+ throw new CliError(`Invalid listen address "${v}" (expected host:port)`);
18053
+ }
18054
+ return { host, port };
18055
+ }
18056
+ function parseOrigins(v) {
18057
+ if (!v?.trim())
18058
+ return void 0;
18059
+ return v.split(",").map((o) => o.trim().replace(/\/+$/, "")).filter(Boolean);
18060
+ }
18061
+ var DEFAULT_OIDC_LABEL = "Sign in with single sign-on";
18062
+ var DEFAULT_OIDC_SCOPES = "openid profile email groups";
18063
+ function envValue(env, key) {
18064
+ const v = env[key]?.trim();
18065
+ return v ? v : void 0;
18066
+ }
18067
+ function authOptionsFromEnv(env) {
18068
+ const out = {};
18069
+ const secret = envValue(env, "SWITCHYARD_HOST_ENROLL_SECRET");
18070
+ if (secret)
18071
+ out.hostEnrollSecret = secret;
18072
+ const proxies = parseOrigins(env.SWITCHYARD_TRUSTED_PROXIES);
18073
+ if (proxies)
18074
+ out.trustedProxies = proxies;
18075
+ const issuer = envValue(env, "SWITCHYARD_OIDC_ISSUER");
18076
+ const clientId = envValue(env, "SWITCHYARD_OIDC_CLIENT_ID");
18077
+ if (issuer && clientId) {
18078
+ if (!/^https?:\/\//.test(issuer)) {
18079
+ throw new CliError(`SWITCHYARD_OIDC_ISSUER must be an http(s) URL, got "${issuer}"`);
18080
+ }
18081
+ out.oidc = {
18082
+ issuer,
18083
+ clientId,
18084
+ clientSecret: envValue(env, "SWITCHYARD_OIDC_CLIENT_SECRET") ?? null,
18085
+ label: envValue(env, "SWITCHYARD_OIDC_LABEL") ?? DEFAULT_OIDC_LABEL,
18086
+ scopes: envValue(env, "SWITCHYARD_OIDC_SCOPES") ?? DEFAULT_OIDC_SCOPES,
18087
+ owner: {
18088
+ subject: envValue(env, "SWITCHYARD_OIDC_OWNER_SUBJECT") ?? null,
18089
+ email: envValue(env, "SWITCHYARD_OIDC_OWNER_EMAIL") ?? null,
18090
+ group: envValue(env, "SWITCHYARD_OIDC_OWNER_GROUP") ?? null
18091
+ }
18092
+ };
18093
+ } else if (issuer || clientId) {
18094
+ throw new CliError("Single sign-on needs both SWITCHYARD_OIDC_ISSUER and SWITCHYARD_OIDC_CLIENT_ID");
18095
+ }
18096
+ return out;
18097
+ }
18098
+ function coordinatorOptions(flags, env) {
18099
+ const dataDir = flags.dataDir ? resolve10(flags.dataDir) : defaultDataDir(env);
18100
+ const listen = parseListen(flags.listen ?? env.SWITCHYARD_LISTEN ?? `0.0.0.0:${DEFAULT_PORT}`);
18101
+ const publicUrl = (flags.publicUrl ?? env.SWITCHYARD_PUBLIC_URL)?.trim() || void 0;
18102
+ if (publicUrl && !/^https?:\/\//.test(publicUrl)) {
18103
+ throw new CliError(`Public URL must start with http:// or https://, got "${publicUrl}"`);
18104
+ }
18105
+ const trustedOrigins = parseOrigins(flags.trustedOrigins ?? env.SWITCHYARD_TRUSTED_ORIGINS);
18106
+ return {
18107
+ dataDir,
18108
+ listen,
18109
+ publicUrl,
18110
+ trustedOrigins,
18111
+ webDist: resolveWebDist(flags.webDist, env),
18112
+ ...authOptionsFromEnv(env)
18113
+ };
18114
+ }
18115
+ function waitForSignal() {
18116
+ return new Promise((res) => {
18117
+ const on = (sig) => {
18118
+ process.off("SIGINT", on);
18119
+ process.off("SIGTERM", on);
18120
+ res(sig);
18121
+ };
18122
+ process.on("SIGINT", on);
18123
+ process.on("SIGTERM", on);
18124
+ });
18125
+ }
18126
+ async function coordinatorStart(ctx, flags) {
18127
+ const opts = coordinatorOptions(flags, ctx.env);
18128
+ const c = createCoordinator(opts);
18129
+ await c.start();
18130
+ writeEndpoint(opts.dataDir, {
18131
+ localUrl: localUrlFor(opts.listen, c.port),
18132
+ publicUrl: c.config.publicUrl
18133
+ });
18134
+ 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"})`);
18135
+ const sig = await waitForSignal();
18136
+ ctx.err(`${sig}: stopping coordinator`);
18137
+ await c.stop();
18138
+ }
18139
+ function localUrlFor(listen, port) {
18140
+ const h = listen.host ?? "0.0.0.0";
18141
+ const host = h === "0.0.0.0" || h === "::" || h === "" ? "127.0.0.1" : h.includes(":") ? `[${h}]` : h;
18142
+ return `http://${host}:${port}`;
18143
+ }
18144
+ function dataDirOf(flags, env) {
18145
+ return flags.dataDir ? resolve10(flags.dataDir) : defaultDataDir(env);
18146
+ }
18147
+ function coordinatorBackup(ctx, out, flags) {
18148
+ const r = backup(dataDirOf(flags, ctx.env), out);
18149
+ ctx.out(`Backed up to ${r.file} (schema ${r.migrationLevel})`);
18150
+ }
18151
+ function coordinatorRestore(ctx, file, flags) {
18152
+ const r = restore(dataDirOf(flags, ctx.env), file);
18153
+ ctx.out(`Restored ${r.restoredFrom} (schema ${r.migrationLevel}).`);
18154
+ if (r.previous)
18155
+ ctx.out(`The previous database was kept as ${r.previous}`);
18156
+ if (r.migrationLevel < LATEST_MIGRATION) {
18157
+ ctx.out("Migrations to the current schema run when the coordinator next starts.");
18158
+ }
18159
+ }
18160
+ function coordinatorMigrate(ctx, flags) {
18161
+ const dataDir = dataDirOf(flags, ctx.env);
18162
+ const holder = lockHolder(dataDir);
18163
+ if (holder !== null) {
18164
+ throw new CliError(`Coordinator (pid ${holder}) is running on ${dataDir}; it migrated at start`);
18165
+ }
18166
+ const path = dbPath(dataDir);
18167
+ const existed = existsSync14(path);
18168
+ mkdirSync8(dataDir, { recursive: true });
18169
+ const db = openDb(path);
18170
+ try {
18171
+ const before = existed ? migrationLevel(db) : 0;
18172
+ migrate(db);
18173
+ const after = migrationLevel(db);
18174
+ ctx.out(before === after ? `Schema is current (level ${after}) at ${path}` : `Migrated ${path} from level ${before} to ${after}`);
18175
+ } finally {
18176
+ db.close();
18177
+ }
18178
+ }
18179
+
18180
+ // packages/cli/dist/commands/up.js
18181
+ var DEV_ORIGINS = ["http://localhost:5173", "http://127.0.0.1:5173"];
18182
+ function upOptions(flags, env) {
18183
+ const port = flags.port === void 0 ? DEFAULT_PORT : Number(flags.port);
18184
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
18185
+ throw new CliError(`Invalid port "${flags.port}"`);
18186
+ }
18187
+ const listenHost = flags.host ?? "0.0.0.0";
18188
+ const dataDir = flags.dataDir ?? defaultDataDir(env);
18189
+ const explicitPublic = (flags.publicUrl ?? env.SWITCHYARD_PUBLIC_URL)?.trim();
18190
+ const publicUrl = explicitPublic || port !== 0 ? (explicitPublic || `http://localhost:${port}`).replace(/\/+$/, "") : void 0;
18191
+ let trusted = parseOrigins(flags.trustedOrigins ?? env.SWITCHYARD_TRUSTED_ORIGINS);
18192
+ if (!trusted && publicUrl) {
18193
+ trusted = [new URL(publicUrl).origin];
18194
+ if (!explicitPublic)
18195
+ trusted.push(`http://127.0.0.1:${port}`);
18196
+ }
18197
+ if (flags.dev && trusted)
18198
+ trusted = [.../* @__PURE__ */ new Set([...trusted, ...DEV_ORIGINS])];
18199
+ return {
18200
+ dataDir,
18201
+ listen: { host: listenHost, port },
18202
+ publicUrl,
18203
+ trustedOrigins: trusted,
18204
+ webDist: resolveWebDist(flags.webDist, env),
18205
+ // Same server env as `coordinator start`: shared host secret, trusted proxies and OIDC.
18206
+ ...authOptionsFromEnv(env)
18207
+ };
18208
+ }
18209
+ async function ensureLocalHost(coordinator, localUrl, configPath, name, fetchImpl) {
18210
+ if (existsSync15(configPath)) {
18211
+ const cfg2 = loadConfig(configPath);
18212
+ const known = coordinator.store.hosts.get(cfg2.hostId);
18213
+ if (known && !known.revokedAt) {
18214
+ return { config: { ...cfg2, coordinatorUrl: localUrl }, enrolled: false };
18215
+ }
18216
+ }
18217
+ const api = new ApiClient({
18218
+ coordinatorUrl: localUrl,
18219
+ credential: coordinator.adminToken,
18220
+ fetch: fetchImpl
18221
+ });
18222
+ const e = await api.post("/api/hosts/enrollments", {});
18223
+ const cfg = await enroll({
18224
+ coordinatorUrl: localUrl,
18225
+ token: e.token,
18226
+ name,
18227
+ configPath,
18228
+ fetch: fetchImpl
18229
+ });
18230
+ const { configPath: _p, ...config } = cfg;
18231
+ return { config, enrolled: true };
18232
+ }
18233
+ function openBrowser(url, platform = process.platform, spawnFn = spawn4) {
18234
+ const cmd = platform === "darwin" ? "open" : platform === "linux" ? "xdg-open" : null;
18235
+ if (!cmd)
18236
+ return;
18237
+ try {
18238
+ const child = spawnFn(cmd, [url], { detached: true, stdio: "ignore" });
18239
+ child.on("error", () => {
18240
+ });
18241
+ child.unref();
18242
+ } catch {
18243
+ }
18244
+ }
18245
+ async function firstSignInLink(ctx, coordinator, open2, opener) {
18246
+ if (coordinator.config.oidc || coordinator.auth.activeDeviceCount() > 0)
18247
+ return null;
18248
+ const e = coordinator.auth.createEnrollment("device", ADMIN_DEVICE_ID);
18249
+ await printSignInLink(ctx, e.url, e.expiresAt);
18250
+ if (open2)
18251
+ opener(e.url);
18252
+ return e.url;
18253
+ }
18254
+ async function startStandalone(ctx, flags, deps = {}) {
18255
+ if (flags.dev && !ctx.env.SWITCHYARD_LOG_LEVEL)
18256
+ process.env.SWITCHYARD_LOG_LEVEL = "debug";
18257
+ const opts = upOptions(flags, ctx.env);
18258
+ const coordinator = createCoordinator({ ...opts, setupHint: false });
18259
+ await coordinator.start();
18260
+ let firstLink;
18261
+ try {
18262
+ firstLink = await firstSignInLink(ctx, coordinator, flags.open ?? process.stdout.isTTY === true, deps.open ?? ((url) => openBrowser(url)));
18263
+ } catch (err) {
18264
+ await coordinator.stop();
18265
+ throw err;
18266
+ }
18267
+ const localUrl = localUrlFor(opts.listen, coordinator.port);
18268
+ writeEndpoint(opts.dataDir, { localUrl, publicUrl: coordinator.config.publicUrl });
18269
+ let host;
18270
+ try {
18271
+ const paths = standaloneHostPaths(opts.dataDir);
18272
+ const { config, enrolled } = await ensureLocalHost(coordinator, localUrl, paths.configPath, flags.name ?? hostname2().replace(/\.local$/, ""));
18273
+ if (enrolled)
18274
+ ctx.err(`Enrolled the local host "${config.name}" (${config.hostId})`);
18275
+ host = await runHostAgent({ config, stateDir: paths.stateDir, handleSignals: false });
18276
+ } catch (err) {
18277
+ await coordinator.stop();
18278
+ throw err;
18279
+ }
18280
+ return {
18281
+ coordinator,
18282
+ host,
18283
+ localUrl,
18284
+ firstLink,
18285
+ async stop() {
18286
+ await host.stop();
18287
+ await coordinator.stop();
18288
+ }
18289
+ };
18290
+ }
18291
+ async function up(ctx, flags) {
18292
+ const s = await startStandalone(ctx, flags);
18293
+ const c = s.coordinator;
18294
+ ctx.err([
18295
+ `Switchyard is up: ${c.config.publicUrl} (listening on port ${c.port})`,
18296
+ `Data: ${c.config.dataDir}`,
18297
+ c.config.webDist ? null : "Web app bundle not found: API only (run `pnpm build`)",
18298
+ 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}`,
18299
+ "Schedules run only while this process is running."
18300
+ ].filter(Boolean).join("\n"));
18301
+ const sig = await waitForSignal();
18302
+ ctx.err(`${sig}: stopping (Pi sessions on this host are stopped and can be resumed later)`);
18303
+ await s.stop();
18304
+ }
18305
+
18306
+ // packages/cli/dist/commands/auth.js
18307
+ function adminApi(ctx, flags) {
18308
+ const dataDir = flags.dataDir ?? ctx.paths?.dataDir ?? defaultDataDir(ctx.env);
18309
+ const token = readAdminToken(dataDir, ctx.env);
18310
+ if (!token) {
18311
+ throw new CliError(`No admin token: run this on the coordinator machine (looked in ${dataDir}/admin.token) or set SWITCHYARD_ADMIN_TOKEN`);
18312
+ }
18313
+ const url = trimUrl(flags.coordinator ?? readEndpoint(dataDir)?.localUrl ?? DEFAULT_LOCAL_URL);
18314
+ return new ApiClient({ coordinatorUrl: url, credential: token, fetch: ctx.fetch });
18315
+ }
18316
+ function clockTime(iso) {
18317
+ const d = new Date(iso);
18318
+ return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
18319
+ }
18320
+ async function printDeviceLink(ctx, url) {
18321
+ const qr = await QRCode2.toString(url, { type: "terminal", small: true });
18322
+ ctx.out("");
18323
+ ctx.out(` ${url}`);
18324
+ ctx.out("");
18325
+ ctx.out(qr.trimEnd());
18326
+ ctx.out("");
18327
+ }
18328
+ async function printSignInLink(ctx, url, expiresAt) {
18329
+ ctx.out(`Open this single-use sign-in link in your browser (expires ${clockTime(expiresAt)}):`);
18330
+ await printDeviceLink(ctx, url);
18331
+ ctx.out("Then add a passkey in Settings \u203A Sign-in as a backup, or run `switchyard pair` for another browser.");
18332
+ }
18333
+ async function pair(ctx, flags) {
18334
+ const api = adminApi(ctx, flags);
18335
+ const kind = flags.host ? "host" : "device";
18336
+ const e = await api.post("/api/enrollments", { kind });
18337
+ if (flags.json) {
18338
+ ctx.out(JSON.stringify(e, null, 2));
18339
+ return e;
18340
+ }
18341
+ const qr = await QRCode2.toString(e.url, { type: "terminal", small: true });
18342
+ if (kind === "host") {
18343
+ ctx.out(`Host enrollment (single use, expires ${e.expiresAt}). On the host, run:`);
18344
+ ctx.out("");
18345
+ ctx.out(` ${e.command ?? `switchyard host enroll --coordinator <url> --token ${e.token}`}`);
18346
+ ctx.out("");
18347
+ ctx.out(`Token: ${e.token}`);
18348
+ } else {
18349
+ ctx.out(`Open this link in a browser to sign it in (single use, expires ${e.expiresAt}):`);
18350
+ ctx.out("");
18351
+ ctx.out(` ${e.url}`);
18352
+ ctx.out("");
18353
+ ctx.out(qr.trimEnd());
18354
+ ctx.out("");
18355
+ ctx.out(`Terminal: switchyard login --coordinator ${originOf2(e.url)} --token ${e.token}`);
17718
18356
  }
18357
+ ctx.out("The link does not provide network access: the device must already reach this URL.");
18358
+ return e;
18359
+ }
18360
+ function originOf2(url) {
17719
18361
  try {
17720
- await catalog.init();
17721
- } catch (err) {
17722
- log.warn({ err: err.message }, "adapter discovery failed; Pi package adapters are not loaded");
18362
+ return new URL(url).origin;
18363
+ } catch {
18364
+ return url;
17723
18365
  }
17724
- supervisor.emitAdapters();
17725
- link.start();
17726
- let stopping = null;
17727
- const onSignal = (signal) => {
17728
- log.info({ signal }, "stopping host agent");
17729
- void handle.stop().finally(() => process.exit(0));
17730
- };
17731
- if (opts.handleSignals !== false) {
17732
- process.once("SIGTERM", onSignal);
17733
- process.once("SIGINT", onSignal);
18366
+ }
18367
+ function parseEnrollmentInput(token, coordinator) {
18368
+ const t = token.trim();
18369
+ if (/^https?:\/\//.test(t)) {
18370
+ const u = new URL(t);
18371
+ const params = new URLSearchParams(u.hash.replace(/^#/, ""));
18372
+ const fromUrl = params.get("token") ?? u.searchParams.get("token");
18373
+ if (!fromUrl)
18374
+ throw new CliError("That URL has no enrollment token");
18375
+ return { url: trimUrl(coordinator ?? u.origin), token: fromUrl };
17734
18376
  }
17735
- const handle = {
17736
- hostId: config.hostId,
17737
- config,
17738
- db,
17739
- link,
17740
- supervisor,
17741
- router,
17742
- dispatcher,
17743
- pi,
17744
- interruptedOnStart,
17745
- terminalSessions: terminalEnabled,
17746
- get spawnedPiProcesses() {
17747
- return supervisor.spawnedProcesses;
17748
- },
17749
- stop(stopOpts = {}) {
17750
- stopping ??= (async () => {
17751
- process.off("SIGTERM", onSignal);
17752
- process.off("SIGINT", onSignal);
17753
- relay.closeAll();
17754
- await supervisor.stopAll(stopOpts);
17755
- await new Promise((r) => setTimeout(r, 50));
17756
- await link.stop();
17757
- db.close();
17758
- })();
17759
- return stopping;
18377
+ if (!coordinator)
18378
+ throw new CliError("--coordinator <url> is required");
18379
+ return { url: trimUrl(coordinator), token: t };
18380
+ }
18381
+ async function loginWithToken(ctx, flags) {
18382
+ const { url, token } = parseEnrollmentInput(flags.token, flags.coordinator);
18383
+ const api = new ApiClient({ coordinatorUrl: url, credential: null, fetch: ctx.fetch });
18384
+ const name = flags.name ?? `${hostname3().replace(/\.local$/, "")} terminal`;
18385
+ const res = await api.post("/api/enroll", {
18386
+ token,
18387
+ name,
18388
+ kind: "terminal"
18389
+ });
18390
+ if (!res.credential)
18391
+ throw new CliError("The coordinator returned no credential");
18392
+ const path = ctx.paths?.clientConfig ?? clientConfigPath(ctx.env);
18393
+ writeClientConfig(path, {
18394
+ coordinatorUrl: url,
18395
+ credential: res.credential,
18396
+ deviceId: res.deviceId
18397
+ });
18398
+ ctx.out(`Enrolled "${name}" (${res.deviceId}) with ${url}.`);
18399
+ ctx.out(`Credential saved to ${path} (mode 0600). Open a session's Pi terminal with \`switchyard tui <session>\`.`);
18400
+ return { path, deviceId: res.deviceId };
18401
+ }
18402
+ var EXPIRED2 = "The sign-in expired before it was approved. Run `switchyard login` again.";
18403
+ async function login(ctx, flags, deps = {}) {
18404
+ if (flags.token)
18405
+ return loginWithToken(ctx, { ...flags, token: flags.token });
18406
+ const path = ctx.paths?.clientConfig ?? clientConfigPath(ctx.env);
18407
+ const raw = flags.coordinator ?? ctx.env.SWITCHYARD_URL ?? readClientConfig(path)?.coordinatorUrl;
18408
+ if (!raw)
18409
+ throw new CliError("--coordinator <url> is required");
18410
+ const url = trimUrl(raw);
18411
+ const api = new ApiClient({ coordinatorUrl: url, credential: null, fetch: ctx.fetch });
18412
+ const name = flags.name ?? `${hostname3().replace(/\.local$/, "")} terminal`;
18413
+ const started = await api.post("/api/auth/terminal/start", { name });
18414
+ ctx.out("To sign in this terminal, open this link in a browser where you use Switchyard:");
18415
+ ctx.out("");
18416
+ ctx.out(` ${started.verifyUrl}`);
18417
+ ctx.out("");
18418
+ ctx.out(`Check that it shows the code ${started.userCode}. Waiting for approval (expires ${clockTime(started.expiresAt)})\u2026`);
18419
+ const interactive = process.stdout.isTTY === true && !ctx.env.SSH_CONNECTION && !ctx.env.SSH_TTY;
18420
+ if (flags.open ?? interactive)
18421
+ (deps.open ?? ((u) => openBrowser(u)))(started.verifyUrl);
18422
+ const sleep3 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
18423
+ const now = deps.now ?? Date.now;
18424
+ const deadline = Date.parse(started.expiresAt);
18425
+ for (; ; ) {
18426
+ await sleep3(started.intervalMs);
18427
+ let r;
18428
+ try {
18429
+ r = await api.post("/api/auth/terminal/claim", {
18430
+ userCode: started.userCode,
18431
+ pollToken: started.pollToken
18432
+ });
18433
+ } catch (err) {
18434
+ if (err instanceof ApiRequestError) {
18435
+ if (err.status === 401)
18436
+ throw new CliError(EXPIRED2);
18437
+ if (err.status === 0 && now() < deadline)
18438
+ continue;
18439
+ }
18440
+ throw err;
17760
18441
  }
17761
- };
17762
- return handle;
18442
+ if (r.status === "denied")
18443
+ throw new CliError("The sign-in was denied in the browser.");
18444
+ if (r.status === "done") {
18445
+ writeClientConfig(path, {
18446
+ coordinatorUrl: url,
18447
+ credential: r.credential,
18448
+ deviceId: r.deviceId
18449
+ });
18450
+ ctx.out(`Signed in "${name}" (${r.deviceId}) to ${url}.`);
18451
+ ctx.out(`Credential saved to ${path} (mode 0600). Open a session's Pi terminal with \`switchyard tui <session>\`.`);
18452
+ return { path, deviceId: r.deviceId };
18453
+ }
18454
+ if (now() >= deadline)
18455
+ throw new CliError(EXPIRED2);
18456
+ }
17763
18457
  }
17764
18458
 
17765
18459
  // packages/cli/dist/commands/doctor.js
18460
+ import { execFile as execFile3 } from "node:child_process";
18461
+ import { existsSync as existsSync16, readFileSync as readFileSync9 } from "node:fs";
17766
18462
  function defaultDoctorDeps(ctx) {
17767
18463
  const dataDir = ctx.paths?.dataDir ?? defaultDataDir(ctx.env);
17768
18464
  return {
@@ -17770,7 +18466,7 @@ function defaultDoctorDeps(ctx) {
17770
18466
  detectPi: () => detectPi(ctx.env.SWITCHYARD_PI_BIN ?? "pi"),
17771
18467
  gitVersion: () => new Promise((resolve13) => execFile3("git", ["--version"], (err, stdout) => resolve13(err ? null : stdout.trim()))),
17772
18468
  hostConfigPaths: [defaultConfigPath(ctx.env), standaloneHostPaths(dataDir).configPath],
17773
- readFile: (p) => existsSync14(p) ? readFileSync9(p, "utf8") : null
18469
+ readFile: (p) => existsSync16(p) ? readFileSync9(p, "utf8") : null
17774
18470
  };
17775
18471
  }
17776
18472
  function findHostConfig(deps) {
@@ -17900,13 +18596,13 @@ async function checkSignIn(ctx, url, add) {
17900
18596
  try {
17901
18597
  const api = new ApiClient({ coordinatorUrl: url, credential: null, fetch: ctx.fetch });
17902
18598
  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
18599
  const passkey = !s.passkey.available ? `unavailable, ${s.passkey.reason ?? "unsupported"}` : s.passkey.registered ? "registered" : "none registered yet";
17908
18600
  const sso = s.oidc ? `on (${s.oidc.label})` : "off";
17909
- add("PASS", "Sign-in", `set up (passkey: ${passkey}, single sign-on: ${sso})`);
18601
+ if (!s.oidc && !s.passkey.registered) {
18602
+ add("WARN", "Sign-in", "no single sign-on and no passkey: sign in with a link from `switchyard pair` on the server");
18603
+ return;
18604
+ }
18605
+ add("PASS", "Sign-in", `passkey: ${passkey}, single sign-on: ${sso}`);
17910
18606
  } catch (err) {
17911
18607
  add("WARN", "Sign-in", `could not read the setup state: ${err.message}`);
17912
18608
  }
@@ -17922,9 +18618,9 @@ function printDoctor(ctx, results) {
17922
18618
  }
17923
18619
 
17924
18620
  // 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";
17927
- import { join as join21 } from "node:path";
18621
+ import { spawn as spawn5 } from "node:child_process";
18622
+ import { closeSync as closeSync2, existsSync as existsSync20, openSync as openSync2, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "node:fs";
18623
+ import { join as join22 } from "node:path";
17928
18624
 
17929
18625
  // packages/cli/dist/context.js
17930
18626
  function defaultCtx() {
@@ -17934,7 +18630,7 @@ function defaultCtx() {
17934
18630
  err: (l) => process.stderr.write(`${l}
17935
18631
  `),
17936
18632
  env: process.env,
17937
- fetch: (input, init2) => fetch(input, init2)
18633
+ fetch: (input, init) => fetch(input, init)
17938
18634
  };
17939
18635
  }
17940
18636
  function connect(ctx, flags) {
@@ -17975,10 +18671,10 @@ function ago(iso, now = Date.now()) {
17975
18671
  }
17976
18672
 
17977
18673
  // packages/cli/dist/herdr/client.js
17978
- import { existsSync as existsSync15, readdirSync } from "node:fs";
18674
+ import { existsSync as existsSync17, readdirSync } from "node:fs";
17979
18675
  import { createConnection as createConnection3 } from "node:net";
17980
18676
  import { homedir as homedir6 } from "node:os";
17981
- import { join as join17 } from "node:path";
18677
+ import { join as join18 } from "node:path";
17982
18678
  var HerdrUnavailable = class extends Error {
17983
18679
  constructor(message) {
17984
18680
  super(message);
@@ -17996,15 +18692,15 @@ var HerdrError = class extends Error {
17996
18692
  function herdrSocketPath(env = process.env, home = homedir6()) {
17997
18693
  if (env.HERDR_SOCKET_PATH)
17998
18694
  return env.HERDR_SOCKET_PATH;
17999
- const base = join17(home, ".config", "herdr");
18695
+ const base = join18(home, ".config", "herdr");
18000
18696
  if (env.HERDR_SESSION)
18001
- return join17(base, "sessions", env.HERDR_SESSION, "herdr.sock");
18002
- const dflt = join17(base, "herdr.sock");
18003
- if (existsSync15(dflt))
18697
+ return join18(base, "sessions", env.HERDR_SESSION, "herdr.sock");
18698
+ const dflt = join18(base, "herdr.sock");
18699
+ if (existsSync17(dflt))
18004
18700
  return dflt;
18005
18701
  let live = [];
18006
18702
  try {
18007
- live = readdirSync(join17(base, "sessions")).map((s) => join17(base, "sessions", s, "herdr.sock")).filter((p) => existsSync15(p));
18703
+ live = readdirSync(join18(base, "sessions")).map((s) => join18(base, "sessions", s, "herdr.sock")).filter((p) => existsSync17(p));
18008
18704
  } catch {
18009
18705
  live = [];
18010
18706
  }
@@ -18117,11 +18813,11 @@ function nextSeq() {
18117
18813
  }
18118
18814
 
18119
18815
  // packages/cli/dist/herdr/daemon.js
18120
- import { existsSync as existsSync17, mkdirSync as mkdirSync10, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "node:fs";
18121
- import { join as join19 } from "node:path";
18816
+ import { existsSync as existsSync19, mkdirSync as mkdirSync10, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "node:fs";
18817
+ import { join as join20 } from "node:path";
18122
18818
 
18123
18819
  // packages/cli/dist/herdr/mirror.js
18124
- import { basename as basename3 } from "node:path";
18820
+ import { basename as basename4 } from "node:path";
18125
18821
  var SOURCE = "switchyard";
18126
18822
  var PENDING_MS = 15e3;
18127
18823
  var NEW_SESSION_GRACE_MS = 5e3;
@@ -18153,10 +18849,10 @@ function projectOfKey(key, st) {
18153
18849
  function spaceLabel(key, st) {
18154
18850
  const w = st.workspaces.find((x) => x.id === key);
18155
18851
  if (w) {
18156
- const project2 = st.projects.find((p) => p.id === w.projectId)?.name ?? basename3(w.path);
18852
+ const project2 = st.projects.find((p) => p.id === w.projectId)?.name ?? basename4(w.path);
18157
18853
  let label = project2;
18158
18854
  if (w.provenance !== "main")
18159
- label += ` \xB7 ${w.branch ?? basename3(w.path)}`;
18855
+ label += ` \xB7 ${w.branch ?? basename4(w.path)}`;
18160
18856
  const hosts = new Set(st.checkouts.filter((c) => c.projectId === w.projectId).map((c) => c.hostId));
18161
18857
  if (hosts.size >= 2)
18162
18858
  label += ` \xB7 ${hostName(st, w.hostId)}`;
@@ -18165,7 +18861,7 @@ function spaceLabel(key, st) {
18165
18861
  const cwd = key.split(":").slice(2).join(":");
18166
18862
  const s = st.sessions.find((x) => spaceKey(x) === key);
18167
18863
  const project = s?.projectId && st.projects.find((p) => p.id === s.projectId)?.name || null;
18168
- return cap2(`${project ?? basename3(cwd)} \xB7 ${basename3(cwd)}`);
18864
+ return cap2(`${project ?? basename4(cwd)} \xB7 ${basename4(cwd)}`);
18169
18865
  }
18170
18866
  function tabLabel(s, overrides = {}) {
18171
18867
  return cap2(overrides[s.id] ?? s.title);
@@ -18404,27 +19100,27 @@ function planMirror(st, h, mem, now) {
18404
19100
  }
18405
19101
 
18406
19102
  // packages/cli/dist/herdr/util.js
18407
- import { chmodSync as chmodSync5, existsSync as existsSync16, mkdirSync as mkdirSync9, writeFileSync as writeFileSync5 } from "node:fs";
19103
+ import { chmodSync as chmodSync5, existsSync as existsSync18, mkdirSync as mkdirSync9, writeFileSync as writeFileSync5 } from "node:fs";
18408
19104
  import { homedir as homedir7 } from "node:os";
18409
- import { join as join18 } from "node:path";
19105
+ import { join as join19 } from "node:path";
18410
19106
  function herdrStateDir(env = process.env, home = homedir7()) {
18411
19107
  const xdg = env.XDG_STATE_HOME?.trim();
18412
- return join18(xdg || join18(home, ".local", "state"), "switchyard-herdr");
19108
+ return join19(xdg || join19(home, ".local", "state"), "switchyard-herdr");
18413
19109
  }
18414
19110
  function sessionStateDir(env = process.env) {
18415
- const dir = join18(herdrStateDir(env), herdrSessionName(env));
19111
+ const dir = join19(herdrStateDir(env), herdrSessionName(env));
18416
19112
  mkdirSync9(dir, { recursive: true, mode: 448 });
18417
19113
  return dir;
18418
19114
  }
18419
19115
  function stableLauncherPath(env = process.env) {
18420
- return env.SWITCHYARD_HERDR_LAUNCHER || join18(herdrStateDir(env), "switchyard-herdr");
19116
+ return env.SWITCHYARD_HERDR_LAUNCHER || join19(herdrStateDir(env), "switchyard-herdr");
18421
19117
  }
18422
19118
  function writeStableLauncher(stateRoot, pluginRoot) {
18423
19119
  if (pluginRoot.includes("'") || stateRoot.includes("'")) {
18424
19120
  throw new CliError("Plugin path contains an apostrophe");
18425
19121
  }
18426
19122
  mkdirSync9(stateRoot, { recursive: true, mode: 448 });
18427
- const path = join18(stateRoot, "switchyard-herdr");
19123
+ const path = join19(stateRoot, "switchyard-herdr");
18428
19124
  writeFileSync5(path, `#!/bin/sh
18429
19125
  exec sh '${pluginRoot}/switchyard-herdr.sh' "$@"
18430
19126
  `, {
@@ -18434,7 +19130,7 @@ exec sh '${pluginRoot}/switchyard-herdr.sh' "$@"
18434
19130
  return path;
18435
19131
  }
18436
19132
  function launcherExists(env = process.env) {
18437
- return existsSync16(stableLauncherPath(env));
19133
+ return existsSync18(stableLauncherPath(env));
18438
19134
  }
18439
19135
  async function isAtShell(call, paneId) {
18440
19136
  const info = (await call("pane.process_info", { pane_id: paneId }))?.process_info ?? {};
@@ -18508,7 +19204,7 @@ var MirrorDaemon = class {
18508
19204
  return (this.deps.now ?? Date.now)();
18509
19205
  }
18510
19206
  get spacesFile() {
18511
- return join19(this.deps.stateDir, "spaces.json");
19207
+ return join20(this.deps.stateDir, "spaces.json");
18512
19208
  }
18513
19209
  async start() {
18514
19210
  this.load();
@@ -18633,10 +19329,10 @@ var MirrorDaemon = class {
18633
19329
  const titleOf = (sid) => st.sessions.find((s) => s.id === sid)?.title ?? sid;
18634
19330
  const launcher = this.deps.launcherPath;
18635
19331
  const paneCmd = (sid) => launcherCommand(launcher, ["pane", sid]);
18636
- const exists2 = this.deps.exists ?? existsSync17;
19332
+ const exists2 = this.deps.exists ?? existsSync19;
18637
19333
  const createdNow = {};
18638
19334
  const fallbackCwd = () => {
18639
- const dir = join19(this.deps.stateDir, "spaces");
19335
+ const dir = join20(this.deps.stateDir, "spaces");
18640
19336
  mkdirSync10(dir, { recursive: true, mode: 448 });
18641
19337
  return dir;
18642
19338
  };
@@ -18862,7 +19558,7 @@ var MirrorDaemon = class {
18862
19558
 
18863
19559
  // packages/cli/dist/herdr/new-session.js
18864
19560
  import { readFileSync as readFileSync11 } from "node:fs";
18865
- import { basename as basename4, join as join20 } from "node:path";
19561
+ import { basename as basename5, join as join21 } from "node:path";
18866
19562
 
18867
19563
  // packages/cli/dist/herdr/pane.js
18868
19564
  var LIVE = /* @__PURE__ */ new Set(["starting", "idle", "running", "waiting_input"]);
@@ -19489,7 +20185,7 @@ function workspaceItems(st, projectId) {
19489
20185
  own.length > 0 ? plural(own.length, "session") : "",
19490
20186
  w.git?.dirty ? "modified" : ""
19491
20187
  ].filter(Boolean).join(" \xB7 ");
19492
- const label = `${w.branch ?? basename4(w.path)}${w.provenance === "main" ? " (main checkout)" : ""}`;
20188
+ const label = `${w.branch ?? basename5(w.path)}${w.provenance === "main" ? " (main checkout)" : ""}`;
19493
20189
  return {
19494
20190
  item: { label, detail, value: { kind: "existing", workspaceId: w.id } },
19495
20191
  active: latest(own.map((x) => x.lastActivityAt)),
@@ -19536,7 +20232,7 @@ async function chooseSession(st, screen, opts = {}) {
19536
20232
  if (pick?.kind === "existing") {
19537
20233
  const w = st.workspaces.find((x) => x.id === pick.workspaceId);
19538
20234
  if (w && w.provenance !== "main")
19539
- c.push(w.branch ?? basename4(w.path));
20235
+ c.push(w.branch ?? basename5(w.path));
19540
20236
  } else if (pick?.kind === "new" && pick.branch) {
19541
20237
  c.push(pick.branch);
19542
20238
  }
@@ -19677,7 +20373,7 @@ async function newHere(ctx) {
19677
20373
  const ws = snap.focused_workspace_id;
19678
20374
  let key = null;
19679
20375
  try {
19680
- const raw = JSON.parse(readFileSync11(join20(sessionStateDir(ctx.env), "spaces.json"), "utf8"));
20376
+ const raw = JSON.parse(readFileSync11(join21(sessionStateDir(ctx.env), "spaces.json"), "utf8"));
19681
20377
  key = ws ? raw.spaces?.[ws]?.key ?? null : null;
19682
20378
  } catch {
19683
20379
  key = null;
@@ -19708,8 +20404,8 @@ async function herdrDaemon(ctx, flags) {
19708
20404
  }
19709
20405
  const launcherPath = stableLauncherPath(env);
19710
20406
  const dir = sessionStateDir(env);
19711
- const pidFile = join21(dir, "daemon.pid");
19712
- const logFile = join21(dir, "daemon.log");
20407
+ const pidFile = join22(dir, "daemon.pid");
20408
+ const logFile = join22(dir, "daemon.log");
19713
20409
  const socket = herdrSocketPath(env);
19714
20410
  const call = (method, params) => herdrCall(method, params, socket);
19715
20411
  if (flags.once) {
@@ -19739,13 +20435,13 @@ async function herdrDaemon(ctx, flags) {
19739
20435
  }
19740
20436
  return;
19741
20437
  }
19742
- const running = existsSync18(pidFile) ? Number(readFileSync12(pidFile, "utf8").trim()) : 0;
20438
+ const running = existsSync20(pidFile) ? Number(readFileSync12(pidFile, "utf8").trim()) : 0;
19743
20439
  if (flags.ensure) {
19744
20440
  if (running && pidAlive2(running))
19745
20441
  return;
19746
20442
  const fd = openSync2(logFile, "a", 384);
19747
20443
  try {
19748
- const child = spawn4(process.execPath, [process.argv[1], "herdr", "daemon"], {
20444
+ const child = spawn5(process.execPath, [process.argv[1], "herdr", "daemon"], {
19749
20445
  detached: true,
19750
20446
  stdio: ["ignore", fd, fd],
19751
20447
  env: { ...process.env, SWITCHYARD_HERDR_LAUNCHER: launcherPath }
@@ -19807,16 +20503,16 @@ async function herdrStatus(ctx, flags) {
19807
20503
  }
19808
20504
 
19809
20505
  // 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";
19812
- import { dirname as dirname10, join as join23, resolve as resolve12 } from "node:path";
20506
+ import { existsSync as existsSync22, mkdirSync as mkdirSync12 } from "node:fs";
20507
+ import { hostname as hostname4 } from "node:os";
20508
+ import { dirname as dirname12, join as join24, resolve as resolve12 } from "node:path";
19813
20509
  import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
19814
20510
 
19815
20511
  // packages/cli/dist/commands/service.js
19816
20512
  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";
20513
+ import { existsSync as existsSync21, mkdirSync as mkdirSync11, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "node:fs";
19818
20514
  import { homedir as homedir8 } from "node:os";
19819
- import { dirname as dirname9, join as join22, resolve as resolve11 } from "node:path";
20515
+ import { dirname as dirname11, join as join23, resolve as resolve11 } from "node:path";
19820
20516
  var LAUNCHD_LABEL = "com.switchyard.host";
19821
20517
  var SYSTEMD_UNIT = "switchyard-host.service";
19822
20518
  var TEMPLATES = {
@@ -19892,8 +20588,8 @@ WantedBy=default.target
19892
20588
  `
19893
20589
  };
19894
20590
  var TEMPLATE_FILES = {
19895
- launchd: join22("launchd", `${LAUNCHD_LABEL}.plist`),
19896
- systemd: join22("systemd", SYSTEMD_UNIT)
20591
+ launchd: join23("launchd", `${LAUNCHD_LABEL}.plist`),
20592
+ systemd: join23("systemd", SYSTEMD_UNIT)
19897
20593
  };
19898
20594
  function xmlEscape(s) {
19899
20595
  return s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
@@ -19905,8 +20601,8 @@ function systemdEscape(s, exec = true) {
19905
20601
  return exec ? out.replaceAll("$", "$$$$") : out;
19906
20602
  }
19907
20603
  function loadTemplate(kind, dir = deployDir()) {
19908
- const p = join22(dir, TEMPLATE_FILES[kind]);
19909
- return existsSync19(p) ? readFileSync13(p, "utf8") : TEMPLATES[kind];
20604
+ const p = join23(dir, TEMPLATE_FILES[kind]);
20605
+ return existsSync21(p) ? readFileSync13(p, "utf8") : TEMPLATES[kind];
19910
20606
  }
19911
20607
  function renderService(kind, vars, template = TEMPLATES[kind]) {
19912
20608
  for (const [k, v] of Object.entries(vars)) {
@@ -19937,7 +20633,7 @@ function serviceNames(instance) {
19937
20633
  }
19938
20634
  function serviceFilePath(kind, home = homedir8(), instance) {
19939
20635
  const { label, unit } = serviceNames(instance);
19940
- return kind === "launchd" ? join22(home, "Library", "LaunchAgents", `${label}.plist`) : join22(home, ".config", "systemd", "user", unit);
20636
+ return kind === "launchd" ? join23(home, "Library", "LaunchAgents", `${label}.plist`) : join23(home, ".config", "systemd", "user", unit);
19941
20637
  }
19942
20638
  function loadCommands(kind, file, uid, instance) {
19943
20639
  const { label, unit } = serviceNames(instance);
@@ -19950,7 +20646,7 @@ function loadCommands(kind, file, uid, instance) {
19950
20646
  ["systemctl", "--user", "restart", unit]
19951
20647
  ];
19952
20648
  }
19953
- function stableNodePath(execPath, exists2 = existsSync19) {
20649
+ function stableNodePath(execPath, exists2 = existsSync21) {
19954
20650
  const m = execPath.match(/^(.*)\/Cellar\/node(?:@[^/]+)?\/[^/]+\/bin\/node$/);
19955
20651
  if (m) {
19956
20652
  const candidate = `${m[1]}/bin/node`;
@@ -19974,11 +20670,11 @@ async function installService(ctx, flags, deps = defaultInstallDeps()) {
19974
20670
  throw new CliError("--instance needs --config and --state-dir, so the second host agent keeps its own enrollment and state");
19975
20671
  }
19976
20672
  const config = flags.config ?? defaultConfigPath(ctx.env, deps.home);
19977
- if (!existsSync19(config) && !flags.print) {
20673
+ if (!existsSync21(config) && !flags.print) {
19978
20674
  throw new CliError(`No host config at ${config}. Enroll first: switchyard host enroll --coordinator <url> --token <t>`);
19979
20675
  }
19980
20676
  const stateDir = flags.stateDir ? resolve11(flags.stateDir) : defaultStateDir(ctx.env, deps.platform, deps.home);
19981
- const logDir = join22(stateDir, "logs");
20677
+ const logDir = join23(stateDir, "logs");
19982
20678
  const content = renderService(kind, {
19983
20679
  node: stableNodePath(process.execPath),
19984
20680
  cli: cliEntry(),
@@ -19994,7 +20690,7 @@ async function installService(ctx, flags, deps = defaultInstallDeps()) {
19994
20690
  ctx.out(content.trimEnd());
19995
20691
  return { file, content };
19996
20692
  }
19997
- mkdirSync11(dirname9(file), { recursive: true });
20693
+ mkdirSync11(dirname11(file), { recursive: true });
19998
20694
  mkdirSync11(logDir, { recursive: true, mode: 448 });
19999
20695
  writeFileSync8(file, content, { mode: 420 });
20000
20696
  ctx.out(`Wrote ${file}`);
@@ -20024,7 +20720,7 @@ async function hostEnroll(ctx, flags) {
20024
20720
  if (!flags.token === !flags.secret) {
20025
20721
  throw new CliError("Pass exactly one of --token or --secret");
20026
20722
  }
20027
- const name = flags.name ?? hostname3().replace(/\.local$/, "");
20723
+ const name = flags.name ?? hostname4().replace(/\.local$/, "");
20028
20724
  const cfg = await enroll({
20029
20725
  coordinatorUrl: flags.coordinator,
20030
20726
  ...flags.token ? { token: flags.token } : { secret: flags.secret },
@@ -20038,7 +20734,7 @@ async function hostEnroll(ctx, flags) {
20038
20734
  }
20039
20735
  async function hostRun(ctx, flags) {
20040
20736
  const configPath = flags.config ?? defaultConfigPath(ctx.env);
20041
- if (!existsSync20(configPath)) {
20737
+ if (!existsSync22(configPath)) {
20042
20738
  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
20739
  }
20044
20740
  const agent = await runHostAgent({
@@ -20053,13 +20749,13 @@ async function hostRun(ctx, flags) {
20053
20749
  }
20054
20750
  function hostBackup(ctx, out, flags) {
20055
20751
  const stateDir = flags.stateDir ?? defaultStateDir(ctx.env);
20056
- const src = join23(stateDir, "host.db");
20057
- if (!existsSync20(src))
20752
+ const src = join24(stateDir, "host.db");
20753
+ if (!existsSync22(src))
20058
20754
  throw new CliError(`No host database at ${src}`);
20059
20755
  const dest = resolve12(out);
20060
- if (existsSync20(dest))
20756
+ if (existsSync22(dest))
20061
20757
  throw new CliError(`${dest} already exists`);
20062
- mkdirSync12(dirname10(dest), { recursive: true });
20758
+ mkdirSync12(dirname12(dest), { recursive: true });
20063
20759
  const db = new DatabaseSync4(src);
20064
20760
  try {
20065
20761
  db.exec("PRAGMA busy_timeout = 5000");
@@ -20074,7 +20770,7 @@ async function hostStatus(ctx, flags) {
20074
20770
  const configPath = flags.config ?? defaultConfigPath(ctx.env);
20075
20771
  const stateDir = flags.stateDir ?? defaultStateDir(ctx.env);
20076
20772
  const status = { configPath, stateDir };
20077
- if (!existsSync20(configPath)) {
20773
+ if (!existsSync22(configPath)) {
20078
20774
  status.enrolled = false;
20079
20775
  } else {
20080
20776
  const cfg = loadConfig(configPath);
@@ -20107,7 +20803,7 @@ async function hostStatus(ctx, flags) {
20107
20803
  try {
20108
20804
  const kind = serviceKindFor(process.platform);
20109
20805
  const file = serviceFilePath(kind);
20110
- status.service = { kind, file, installed: existsSync20(file) };
20806
+ status.service = { kind, file, installed: existsSync22(file) };
20111
20807
  } catch {
20112
20808
  status.service = null;
20113
20809
  }
@@ -20259,136 +20955,6 @@ async function tuiCommand(ctx, sessionId, flags, run = runRawTerminal) {
20259
20955
  throw new CliError(result.message);
20260
20956
  }
20261
20957
 
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
20958
  // packages/cli/dist/commands/work.js
20393
20959
  function byId2(list) {
20394
20960
  return new Map(list.map((x) => [x.id, x]));
@@ -20596,13 +21162,10 @@ function buildProgram(ctx = defaultCtx()) {
20596
21162
  });
20597
21163
  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
21164
  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) => {
21165
+ 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
21166
  await pair(ctx, o);
20604
21167
  });
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) => {
21168
+ 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
21169
  await login(ctx, o);
20607
21170
  });
20608
21171
  connectionOptions(program.command("doctor").description("Check Node, Pi, git, coordinator, credentials, host link and push")).action(async (o) => {