@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.
- package/README.md +6 -6
- package/dist/bridge/switchyard-terminal.js +158 -2
- package/dist/switchyard.mjs +1332 -769
- package/package.json +1 -1
- package/web-dist/assets/AutomationView-GCthHkym.js +2 -0
- package/web-dist/assets/AutomationView-GCthHkym.js.map +1 -0
- package/web-dist/assets/Badge-CEMI3YyI.js +2 -0
- package/web-dist/assets/Badge-CEMI3YyI.js.map +1 -0
- package/web-dist/assets/InteractionCard-DaLqafmA.js +6 -0
- package/web-dist/assets/InteractionCard-DaLqafmA.js.map +1 -0
- package/web-dist/assets/{Menu-GymfuHGP.js → Menu-BcKiEc6X.js} +2 -2
- package/web-dist/assets/{Menu-GymfuHGP.js.map → Menu-BcKiEc6X.js.map} +1 -1
- package/web-dist/assets/{SegmentedControl-ChdVilXb.js → SegmentedControl-BZPzNU08.js} +2 -2
- package/web-dist/assets/{SegmentedControl-ChdVilXb.js.map → SegmentedControl-BZPzNU08.js.map} +1 -1
- package/web-dist/assets/Tabs-COH5EMql.js +184 -0
- package/web-dist/assets/Tabs-COH5EMql.js.map +1 -0
- package/web-dist/assets/TextArea-CeMEPZn4.js +2 -0
- package/web-dist/assets/TextArea-CeMEPZn4.js.map +1 -0
- package/web-dist/assets/attention-BAdea2kA.js +2 -0
- package/web-dist/assets/attention-BAdea2kA.js.map +1 -0
- package/web-dist/assets/automations-DSFUzB0w.js +2 -0
- package/web-dist/assets/automations-DSFUzB0w.js.map +1 -0
- package/web-dist/assets/board-eQ3H8yCp.js +10 -0
- package/web-dist/assets/board-eQ3H8yCp.js.map +1 -0
- package/web-dist/assets/c-Cz4nF6n_.js +1 -0
- package/web-dist/assets/c-SSSlmE39.js +2 -0
- package/web-dist/assets/c-SSSlmE39.js.map +1 -0
- package/web-dist/assets/code-D7JmYMKe.js +13 -0
- package/web-dist/assets/code-D7JmYMKe.js.map +1 -0
- package/web-dist/assets/cpp-Cts1PyIS.js +2 -0
- package/web-dist/assets/cpp-Cts1PyIS.js.map +1 -0
- package/web-dist/assets/cpp-DzjeI0uH.js +1 -0
- package/web-dist/assets/css-BGIxQoYs.js +2 -0
- package/web-dist/assets/css-BGIxQoYs.js.map +1 -0
- package/web-dist/assets/css-DgaLhXQH.js +1 -0
- package/web-dist/assets/describe-C1YimBXJ.js +5 -0
- package/web-dist/assets/describe-C1YimBXJ.js.map +1 -0
- package/web-dist/assets/diff-woXpYk--.js +2 -0
- package/web-dist/assets/diff-woXpYk--.js.map +1 -0
- package/web-dist/assets/dist-B_RpNOcK.js +56 -0
- package/web-dist/assets/dist-B_RpNOcK.js.map +1 -0
- package/web-dist/assets/dockerfile-IyjqRm3v.js +2 -0
- package/web-dist/assets/dockerfile-IyjqRm3v.js.map +1 -0
- package/web-dist/assets/enroll-1OCy5r50.js +2 -0
- package/web-dist/assets/enroll-1OCy5r50.js.map +1 -0
- package/web-dist/assets/github-dark-C-LZuMrd.js +2 -0
- package/web-dist/assets/github-dark-C-LZuMrd.js.map +1 -0
- package/web-dist/assets/github-light-EUqPIrTm.js +2 -0
- package/web-dist/assets/github-light-EUqPIrTm.js.map +1 -0
- package/web-dist/assets/go-rLFTqkRN.js +2 -0
- package/web-dist/assets/go-rLFTqkRN.js.map +1 -0
- package/web-dist/assets/html-BMn1ogTm.js +2 -0
- package/web-dist/assets/html-BMn1ogTm.js.map +1 -0
- package/web-dist/assets/html-BWDvXlHg.js +1 -0
- package/web-dist/assets/index-BhpEXLav.js +11 -0
- package/web-dist/assets/index-BhpEXLav.js.map +1 -0
- package/web-dist/assets/index-DUeOwDZ6.css +2 -0
- package/web-dist/assets/java-BtIQNnN0.js +2 -0
- package/web-dist/assets/java-BtIQNnN0.js.map +1 -0
- package/web-dist/assets/java-_ATy9zCa.js +1 -0
- package/web-dist/assets/javascript-BzbQS41l.js +2 -0
- package/web-dist/assets/javascript-BzbQS41l.js.map +1 -0
- package/web-dist/assets/javascript-CpIGwZ_Y.js +1 -0
- package/web-dist/assets/json-qhed-kSA.js +2 -0
- package/web-dist/assets/json-qhed-kSA.js.map +1 -0
- package/web-dist/assets/jsonc-CYpm1nAK.js +2 -0
- package/web-dist/assets/jsonc-CYpm1nAK.js.map +1 -0
- package/web-dist/assets/jsx-C4yDKi0A.js +1 -0
- package/web-dist/assets/jsx-D3KKoC5L.js +2 -0
- package/web-dist/assets/jsx-D3KKoC5L.js.map +1 -0
- package/web-dist/assets/logic-CBvFOyIo.js +2 -0
- package/web-dist/assets/logic-CBvFOyIo.js.map +1 -0
- package/web-dist/assets/lua-DjuecEsk.js +2 -0
- package/web-dist/assets/lua-DjuecEsk.js.map +1 -0
- package/web-dist/assets/lua-fPRBWD-V.js +1 -0
- package/web-dist/assets/{more-CRGJsz6E.js → more-RjVyxPaz.js} +2 -2
- package/web-dist/assets/{more-CRGJsz6E.js.map → more-RjVyxPaz.js.map} +1 -1
- package/web-dist/assets/now-CDjeFSLD.js +2 -0
- package/web-dist/assets/now-CDjeFSLD.js.map +1 -0
- package/web-dist/assets/python-gzcpVVnB.js +2 -0
- package/web-dist/assets/python-gzcpVVnB.js.map +1 -0
- package/web-dist/assets/ruby-Bx-U2qjP.js +2 -0
- package/web-dist/assets/ruby-Bx-U2qjP.js.map +1 -0
- package/web-dist/assets/rust-DpiIf5JO.js +2 -0
- package/web-dist/assets/rust-DpiIf5JO.js.map +1 -0
- package/web-dist/assets/scss-8bKmll0B.js +2 -0
- package/web-dist/assets/scss-8bKmll0B.js.map +1 -0
- package/web-dist/assets/send-BKjswHxB.js +2 -0
- package/web-dist/assets/send-BKjswHxB.js.map +1 -0
- package/web-dist/assets/settings-tH4UuIfJ.js +7 -0
- package/web-dist/assets/settings-tH4UuIfJ.js.map +1 -0
- package/web-dist/assets/shared-CRX6KSBm.js +3 -0
- package/web-dist/assets/shared-CRX6KSBm.js.map +1 -0
- package/web-dist/assets/shellscript-CVTB3cr6.js +1 -0
- package/web-dist/assets/shellscript-CqHdAlCp.js +2 -0
- package/web-dist/assets/shellscript-CqHdAlCp.js.map +1 -0
- package/web-dist/assets/sliders-horizontal-DAoirila.js +2 -0
- package/web-dist/assets/sliders-horizontal-DAoirila.js.map +1 -0
- package/web-dist/assets/sql-CNu3Or2W.js +2 -0
- package/web-dist/assets/sql-CNu3Or2W.js.map +1 -0
- package/web-dist/assets/sql-CfM2IJm3.js +1 -0
- package/web-dist/assets/states-DXozulGZ.js +2 -0
- package/web-dist/assets/states-DXozulGZ.js.map +1 -0
- package/web-dist/assets/swift-CyEgAFGc.js +2 -0
- package/web-dist/assets/swift-CyEgAFGc.js.map +1 -0
- package/web-dist/assets/terminal-login-CW2rE50W.js +2 -0
- package/web-dist/assets/terminal-login-CW2rE50W.js.map +1 -0
- package/web-dist/assets/toml-CcmNWLt0.js +2 -0
- package/web-dist/assets/toml-CcmNWLt0.js.map +1 -0
- package/web-dist/assets/tsx-BOvsFOI6.js +2 -0
- package/web-dist/assets/tsx-BOvsFOI6.js.map +1 -0
- package/web-dist/assets/tsx-CGosfAUg.js +1 -0
- package/web-dist/assets/typescript-Bb1J8qyy.js +1 -0
- package/web-dist/assets/typescript-j_1H8WHN.js +2 -0
- package/web-dist/assets/typescript-j_1H8WHN.js.map +1 -0
- package/web-dist/assets/useNow-C_B7JgMM.js +2 -0
- package/web-dist/assets/{useNow-Bc_NlDY3.js.map → useNow-C_B7JgMM.js.map} +1 -1
- package/web-dist/assets/xml-DuKfbPI6.js +1 -0
- package/web-dist/assets/xml-WY0zc4PG.js +2 -0
- package/web-dist/assets/xml-WY0zc4PG.js.map +1 -0
- package/web-dist/assets/yaml-DDA_XMNu.js +1 -0
- package/web-dist/assets/yaml-DijMHRDA.js +2 -0
- package/web-dist/assets/yaml-DijMHRDA.js.map +1 -0
- package/web-dist/index.html +4 -3
- package/web-dist/sw.js +1 -1
- package/web-dist/sw.js.map +1 -1
- package/web-dist/assets/AutomationView-DgZn3ltn.js +0 -2
- package/web-dist/assets/AutomationView-DgZn3ltn.js.map +0 -1
- package/web-dist/assets/InteractionCard-cvPWKSu1.js +0 -4
- package/web-dist/assets/InteractionCard-cvPWKSu1.js.map +0 -1
- package/web-dist/assets/Markdown-jXkvZeRp.js +0 -31
- package/web-dist/assets/Markdown-jXkvZeRp.js.map +0 -1
- package/web-dist/assets/Overlay-oYnnwvFM.js +0 -2
- package/web-dist/assets/Overlay-oYnnwvFM.js.map +0 -1
- package/web-dist/assets/TextArea-BsQHOQMe.js +0 -2
- package/web-dist/assets/TextArea-BsQHOQMe.js.map +0 -1
- package/web-dist/assets/attention-Bxg9W15n.js +0 -2
- package/web-dist/assets/attention-Bxg9W15n.js.map +0 -1
- package/web-dist/assets/automations-DdB97dnL.js +0 -2
- package/web-dist/assets/automations-DdB97dnL.js.map +0 -1
- package/web-dist/assets/board-s4LuDPcg.js +0 -10
- package/web-dist/assets/board-s4LuDPcg.js.map +0 -1
- package/web-dist/assets/code-B0KXn7lD.js +0 -8
- package/web-dist/assets/code-B0KXn7lD.js.map +0 -1
- package/web-dist/assets/describe-C_3FJuSs.js +0 -5
- package/web-dist/assets/describe-C_3FJuSs.js.map +0 -1
- package/web-dist/assets/dist-Bgkh2hc5.js +0 -56
- package/web-dist/assets/dist-Bgkh2hc5.js.map +0 -1
- package/web-dist/assets/enroll-DijmoolY.js +0 -2
- package/web-dist/assets/enroll-DijmoolY.js.map +0 -1
- package/web-dist/assets/index-C5n2QB-Y.css +0 -2
- package/web-dist/assets/index-Cf6ZUCfK.js +0 -11
- package/web-dist/assets/index-Cf6ZUCfK.js.map +0 -1
- package/web-dist/assets/logic-D_Eq9DMW.js +0 -2
- package/web-dist/assets/logic-D_Eq9DMW.js.map +0 -1
- package/web-dist/assets/now-D6lDofYy.js +0 -2
- package/web-dist/assets/now-D6lDofYy.js.map +0 -1
- package/web-dist/assets/settings-Csal1F-W.js +0 -8
- package/web-dist/assets/settings-Csal1F-W.js.map +0 -1
- package/web-dist/assets/sliders-horizontal-S3tmwxsP.js +0 -2
- package/web-dist/assets/sliders-horizontal-S3tmwxsP.js.map +0 -1
- package/web-dist/assets/states-DrTQuYXw.js +0 -2
- package/web-dist/assets/states-DrTQuYXw.js.map +0 -1
- package/web-dist/assets/useNow-Bc_NlDY3.js +0 -2
package/dist/switchyard.mjs
CHANGED
|
@@ -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,
|
|
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
|
|
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/
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
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
|
-
|
|
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}/
|
|
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).
|
|
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:
|
|
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({
|
|
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(`/
|
|
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
|
-
|
|
9447
|
-
const
|
|
9448
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
9525
|
+
let s = get(idOf(req));
|
|
9544
9526
|
const body = parse(PromptRequest, req.body);
|
|
9545
|
-
|
|
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
|
-
|
|
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(
|
|
9812
|
-
let s = this.cache.get(
|
|
9853
|
+
get(sql4) {
|
|
9854
|
+
let s = this.cache.get(sql4);
|
|
9813
9855
|
if (!s) {
|
|
9814
|
-
s = this.db.prepare(
|
|
9815
|
-
this.cache.set(
|
|
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
|
|
9847
|
-
return this.st.get(
|
|
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
|
|
9851
|
-
return this.st.get(
|
|
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
|
|
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 ?? [
|
|
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
|
|
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 (!
|
|
10814
|
-
log.info("
|
|
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("
|
|
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/
|
|
10838
|
-
|
|
10839
|
-
|
|
10840
|
-
|
|
10841
|
-
|
|
10842
|
-
|
|
10843
|
-
|
|
10844
|
-
|
|
10845
|
-
|
|
10846
|
-
|
|
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
|
|
10861
|
-
|
|
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
|
-
|
|
10866
|
-
|
|
10867
|
-
|
|
10868
|
-
|
|
10869
|
-
|
|
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
|
|
10872
|
-
|
|
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
|
|
10903
|
-
const
|
|
10904
|
-
|
|
10905
|
-
|
|
10906
|
-
|
|
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
|
|
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
|
|
10920
|
-
|
|
10921
|
-
|
|
10922
|
-
|
|
10923
|
-
|
|
10924
|
-
|
|
10925
|
-
|
|
10926
|
-
|
|
10927
|
-
|
|
10928
|
-
|
|
10929
|
-
|
|
10930
|
-
|
|
10931
|
-
const
|
|
10932
|
-
const
|
|
10933
|
-
|
|
10934
|
-
|
|
10935
|
-
|
|
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 :
|
|
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 (!
|
|
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) =>
|
|
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 (
|
|
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
|
|
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 =
|
|
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:
|
|
11336
|
-
webRendererPath: manifest.webRenderer ?
|
|
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
|
|
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
|
|
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 (!
|
|
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 && !
|
|
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 (!
|
|
11872
|
+
if (!existsSync7(root))
|
|
11853
11873
|
return [];
|
|
11854
|
-
if (
|
|
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) =>
|
|
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(
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
13116
|
-
import { homedir as homedir4, hostname
|
|
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
|
-
|
|
13181
|
+
mkdirSync4(d, { recursive: true, mode: 448 });
|
|
13152
13182
|
return p;
|
|
13153
13183
|
}
|
|
13154
13184
|
function loadConfig(path) {
|
|
13155
|
-
if (!
|
|
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
|
-
|
|
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 (
|
|
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() ||
|
|
13287
|
+
const name = env.SWITCHYARD_HOST_NAME?.trim() || hostname();
|
|
13258
13288
|
for (; ; ) {
|
|
13259
13289
|
if (signal?.aborted)
|
|
13260
13290
|
throw abortError(signal);
|
|
13261
|
-
if (
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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(
|
|
13472
|
-
const r = this.db.prepare(
|
|
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(
|
|
13476
|
-
return this.db.prepare(
|
|
13505
|
+
get(sql4, params = []) {
|
|
13506
|
+
return this.db.prepare(sql4).get(...params);
|
|
13477
13507
|
}
|
|
13478
|
-
all(
|
|
13479
|
-
return this.db.prepare(
|
|
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(
|
|
13490
|
-
const r = this.get(
|
|
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(
|
|
13494
|
-
return this.all(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
14703
|
-
dir =
|
|
14835
|
+
if (join15(dir, name).length > MAX_SOCKET_PATH) {
|
|
14836
|
+
dir = join15("/tmp", `switchyard-${process.getuid?.() ?? "u"}`);
|
|
14704
14837
|
}
|
|
14705
|
-
|
|
14706
|
-
return
|
|
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 ??
|
|
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
|
|
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
|
|
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 ??
|
|
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 ??
|
|
15061
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
17715
|
-
|
|
17716
|
-
|
|
17717
|
-
log.
|
|
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
|
-
|
|
17721
|
-
} catch
|
|
17722
|
-
|
|
18362
|
+
return new URL(url).origin;
|
|
18363
|
+
} catch {
|
|
18364
|
+
return url;
|
|
17723
18365
|
}
|
|
17724
|
-
|
|
17725
|
-
|
|
17726
|
-
|
|
17727
|
-
|
|
17728
|
-
|
|
17729
|
-
|
|
17730
|
-
|
|
17731
|
-
|
|
17732
|
-
|
|
17733
|
-
|
|
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
|
-
|
|
17736
|
-
|
|
17737
|
-
|
|
17738
|
-
|
|
17739
|
-
|
|
17740
|
-
|
|
17741
|
-
|
|
17742
|
-
|
|
17743
|
-
|
|
17744
|
-
|
|
17745
|
-
|
|
17746
|
-
|
|
17747
|
-
|
|
17748
|
-
|
|
17749
|
-
|
|
17750
|
-
|
|
17751
|
-
|
|
17752
|
-
|
|
17753
|
-
|
|
17754
|
-
|
|
17755
|
-
|
|
17756
|
-
|
|
17757
|
-
|
|
17758
|
-
|
|
17759
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
17926
|
-
import { closeSync as closeSync2, existsSync as
|
|
17927
|
-
import { join as
|
|
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,
|
|
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
|
|
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
|
|
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 =
|
|
18695
|
+
const base = join18(home, ".config", "herdr");
|
|
18000
18696
|
if (env.HERDR_SESSION)
|
|
18001
|
-
return
|
|
18002
|
-
const dflt =
|
|
18003
|
-
if (
|
|
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(
|
|
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
|
|
18121
|
-
import { join as
|
|
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
|
|
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 ??
|
|
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 ??
|
|
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 ??
|
|
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
|
|
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
|
|
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
|
|
19108
|
+
return join19(xdg || join19(home, ".local", "state"), "switchyard-herdr");
|
|
18413
19109
|
}
|
|
18414
19110
|
function sessionStateDir(env = process.env) {
|
|
18415
|
-
const dir =
|
|
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 ||
|
|
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 =
|
|
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
|
|
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
|
|
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 ??
|
|
19332
|
+
const exists2 = this.deps.exists ?? existsSync19;
|
|
18637
19333
|
const createdNow = {};
|
|
18638
19334
|
const fallbackCwd = () => {
|
|
18639
|
-
const dir =
|
|
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
|
|
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 ??
|
|
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 ??
|
|
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(
|
|
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 =
|
|
19712
|
-
const logFile =
|
|
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 =
|
|
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 =
|
|
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
|
|
19811
|
-
import { hostname as
|
|
19812
|
-
import { dirname as
|
|
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
|
|
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
|
|
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:
|
|
19896
|
-
systemd:
|
|
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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
@@ -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 =
|
|
19909
|
-
return
|
|
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" ?
|
|
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 =
|
|
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 (!
|
|
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 =
|
|
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(
|
|
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 ??
|
|
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 (!
|
|
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 =
|
|
20057
|
-
if (!
|
|
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 (
|
|
20756
|
+
if (existsSync22(dest))
|
|
20061
20757
|
throw new CliError(`${dest} already exists`);
|
|
20062
|
-
mkdirSync12(
|
|
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 (!
|
|
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:
|
|
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("
|
|
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("
|
|
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) => {
|