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