@huynle/hub 0.8.7 → 0.9.1
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/CHANGELOG.md +152 -0
- package/README.md +75 -16
- package/package.json +5 -1
- package/packages/claude-code/package.json +1 -1
- package/packages/claude-code/src/wrapper.ts +2 -2
- package/packages/cli/dist/hub-cli.js +1413 -523
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/bootstrap/PHASE3-CONTRACT.md +112 -0
- package/packages/cli/src/bootstrap/adapters/node-services/control.ts +164 -21
- package/packages/cli/src/bootstrap/adapters/node-services/plan.ts +24 -7
- package/packages/cli/src/bootstrap/adapters/node-services/render.ts +116 -9
- package/packages/cli/src/bootstrap/adapters/node-services/validate.ts +8 -2
- package/packages/cli/src/bootstrap/contracts/actions.ts +8 -6
- package/packages/cli/src/bootstrap/contracts/discovery.ts +4 -3
- package/packages/cli/src/bootstrap/contracts/exec.ts +4 -2
- package/packages/cli/src/bootstrap/contracts/paths.ts +2 -1
- package/packages/cli/src/bootstrap/contracts/platform.ts +5 -5
- package/packages/cli/src/bootstrap/discovery/discover.ts +7 -2
- package/packages/cli/src/bootstrap/discovery/host.ts +2 -0
- package/packages/cli/src/bootstrap/preview/plan.ts +5 -1
- package/packages/cli/src/bootstrap/profiles/desired.ts +16 -1
- package/packages/cli/src/bootstrap/profiles/history.ts +2 -1
- package/packages/cli/src/bootstrap/profiles/upgrade.ts +2 -2
- package/packages/cli/src/bootstrap/release/assets.ts +4 -1
- package/packages/cli/src/cli.ts +23 -10
- package/packages/cli/src/http.ts +1 -0
- package/packages/cli/src/launches.ts +5 -1
- package/packages/cli/src/mcp.ts +28 -8
- package/packages/cli/src/messaging.ts +14 -0
- package/packages/cli/src/node.ts +94 -12
- package/packages/cli/src/setup/commands.ts +27 -7
- package/packages/cli/src/setup/runtime.ts +31 -4
- package/packages/cli/src/team-intake-mcp.ts +26 -2
- package/packages/codex/package.json +1 -1
- package/packages/codex/src/wrapper.ts +2 -2
- package/packages/node/package.json +1 -1
- package/packages/node/src/config.ts +49 -17
- package/packages/node/src/git.ts +16 -2
- package/packages/node/src/harnesses.ts +54 -9
- package/packages/node/src/host.ts +61 -15
- package/packages/node/src/opencode-service.ts +111 -0
- package/packages/node/src/opencode-warm.ts +11 -5
- package/packages/node/src/run.ts +50 -5
- package/packages/preset/package.json +1 -1
- package/packages/protocol/package.json +1 -1
- package/packages/protocol/schema/app-delivery/v1.schema.json +677 -0
- package/packages/protocol/src/artifacts.ts +43 -0
- package/packages/protocol/src/credentials.ts +38 -0
- package/packages/protocol/src/index.ts +59 -6
- package/packages/protocol/src/launch.ts +46 -6
- package/packages/protocol/src/markdown.ts +753 -0
- package/packages/protocol/src/media-allowlist.ts +240 -0
- package/packages/protocol/src/media.ts +120 -0
- package/packages/protocol/src/preview-bundle.ts +221 -0
- package/packages/protocol/src/voice-recording.ts +8 -0
- package/packages/runtime-openshell/package.json +1 -1
- package/packages/runtime-sdk/package.json +1 -1
- package/packages/sdk/package.json +1 -1
- package/packages/sdk/src/client.ts +17 -3
- package/packages/sdk/src/errors.ts +1 -0
- package/packages/sdk/src/team-intake.ts +8 -0
- package/packages/sdk/src/types.ts +16 -2
- package/packages/spoke-sdk/package.json +1 -1
- package/src/central/agent/delivery.ts +35 -6
- package/src/central/agent/errors.ts +2 -0
- package/src/central/agent/events.ts +11 -2
- package/src/central/agent/launch-engine.ts +65 -4
- package/src/central/agent/launch-routes.ts +8 -0
- package/src/central/agent/launch-spec.ts +22 -3
- package/src/central/agent/launch-store.ts +2 -2
- package/src/central/agent/message-routes.ts +49 -11
- package/src/central/agent/messages.ts +17 -4
- package/src/central/agent/not-after.ts +28 -0
- package/src/central/agent/placement.ts +36 -4
- package/src/central/agent/routes.ts +3 -1
- package/src/central/agent/webhook-routes.ts +21 -9
- package/src/central/agent/webhooks.ts +27 -9
- package/src/central/artifacts/host.ts +3 -1
- package/src/central/artifacts/maintenance.ts +2 -1
- package/src/central/artifacts/preview-bundle.ts +81 -0
- package/src/central/artifacts/previews.ts +94 -0
- package/src/central/artifacts/routes.ts +12 -1
- package/src/central/artifacts/store.ts +198 -8
- package/src/central/config.ts +34 -0
- package/src/central/fetch-budget.ts +59 -0
- package/src/central/fetcher.ts +372 -0
- package/src/central/http.ts +277 -22
- package/src/central/install/CONTRACT.md +4 -4
- package/src/central/install/install.ps1 +37 -5
- package/src/central/install/render.ts +13 -8
- package/src/central/media-remote.ts +248 -0
- package/src/central/media-timeline.ts +317 -0
- package/src/central/media-unfurl.ts +119 -0
- package/src/central/profiles/content.ts +3 -35
- package/src/central/recording/routes.ts +2 -2
- package/src/central/recording/schema.ts +3 -1
- package/src/central/registry.ts +449 -21
- package/src/central/spoke-ws.ts +17 -3
- package/src/central/store.ts +21 -0
- package/src/central/teams/intake-routes.ts +2 -1
- package/src/central/teams/intake.ts +200 -15
- package/src/central/unfurl-parse.ts +313 -0
- package/src/index.ts +68 -8
- package/src/speakable.ts +33 -1
- package/src/spoke/conversation.ts +7 -4
- package/src/spoke/deliver.ts +19 -0
- package/src/spoke/media-bytes.ts +171 -0
- package/src/spoke/media-policy.ts +394 -0
- package/src/spoke/media.ts +189 -0
- package/src/spoke/own-server.ts +30 -0
- package/src/spoke/preview-bundle.ts +538 -0
- package/src/spoke/spoke.ts +387 -17
- package/src/spoke/timeline.ts +59 -5
- package/src/spoke/voice-front.ts +335 -0
- package/web/dist/assets/24BXRY42-Zfr7jm_p.js +2 -0
- package/web/dist/assets/4FHPYW2L-C6Z67098.js +2 -0
- package/web/dist/assets/4LWW3ZNH-CUiYeygH.js +1 -0
- package/web/dist/assets/5THPTBWS-Ce_hS451.js +1 -0
- package/web/dist/assets/5XYWFMKW-CW-BCKiK.js +4 -0
- package/web/dist/assets/64PMEAB3-BjpCDGU9.js +8 -0
- package/web/dist/assets/6BEWHPQE-BpNa96Ze.js +2 -0
- package/web/dist/assets/6PIGVMFT-YXn9Hgnb.js +1 -0
- package/web/dist/assets/75JW6PN6-BFpDBuGN.js +2 -0
- package/web/dist/assets/7WLXJQOR-D6QVYUu9.js +1 -0
- package/web/dist/assets/7XWI72E6-BMuIspEy.js +2 -0
- package/web/dist/assets/A4UD2L5Z-BQzcD9dU.js +8 -0
- package/web/dist/assets/ArtifactPage-BKISN-Qj.js +1 -0
- package/web/dist/assets/ArtifactPane-CKOiAUNH.js +1 -0
- package/web/dist/assets/ArtifactWorkbench-d7R1o-rs.js +6 -0
- package/web/dist/assets/BGW36YJW-CJMhya8S.js +4 -0
- package/web/dist/assets/CGIZI67J-DYDSLME8.js +2 -0
- package/web/dist/assets/DKTLEL5E-DciMwFJ1.js +1 -0
- package/web/dist/assets/DNNCRC2E-BIeKf5t-.js +5 -0
- package/web/dist/assets/Diagram-azw2B_QH.js +1 -0
- package/web/dist/assets/Diagram-vdpmp3xu.css +1 -0
- package/web/dist/assets/E67EBMDD--AucIw59.js +5 -0
- package/web/dist/assets/EFYWJBJK-CVywe_D5.js +1 -0
- package/web/dist/assets/EXE5EH6O-DR1IHCTr.js +4 -0
- package/web/dist/assets/FTXIJQGB-BZVuiBn1.js +1 -0
- package/web/dist/assets/G3MDBVQH-BZmnMkFw.js +1 -0
- package/web/dist/assets/G6GPDDNC-DEz8F449.js +2 -0
- package/web/dist/assets/G76NGGFM-CrB2V34K.js +1 -0
- package/web/dist/assets/GH6G3TJC-D3RTYEH4.js +2 -0
- package/web/dist/assets/HPNUA7RO-B19NeNOJ.js +1 -0
- package/web/dist/assets/HQRGC5D7-Dmpzq6Rn.js +4 -0
- package/web/dist/assets/IX6Z2TVR-DphRHnek.js +5 -0
- package/web/dist/assets/J77NQRCJ-B7AGcxvw.js +3 -0
- package/web/dist/assets/JAD36DTZ-B9-dbsS0.js +5 -0
- package/web/dist/assets/LF3NJQAX-koL91UZ9.js +2 -0
- package/web/dist/assets/LRZCOFJZ-Cdjasfuf.js +6 -0
- package/web/dist/assets/Library-CcjZXsfF.js +2 -0
- package/web/dist/assets/Lightbox-C8KNKwOA.css +1 -0
- package/web/dist/assets/Lightbox-Du56MXGX.js +1 -0
- package/web/dist/assets/MediaImage-Ds_JUx3a.js +4 -0
- package/web/dist/assets/N5NF2FGY-OuapYTfr.js +2 -0
- package/web/dist/assets/N7OWWQHY-BNLd-Q_U.js +2 -0
- package/web/dist/assets/NT5JY5WP-1lJM-aun.js +1 -0
- package/web/dist/assets/OT7W3RNY-jDL3drzX.js +4 -0
- package/web/dist/assets/P67P66DB-CE-opRb8.js +3 -0
- package/web/dist/assets/PFLLLM7K-DDJFgzW3.js +2 -0
- package/web/dist/assets/PUAMC4MJ-CtBm-WuU.js +12 -0
- package/web/dist/assets/PVFCHRLY-CG5JXv60.js +2 -0
- package/web/dist/assets/ProfilesDestination-DdzbzmeK.js +1 -0
- package/web/dist/assets/QQW2P742-BbcEkpa4.js +1 -0
- package/web/dist/assets/RVARI57O-CgA1WoRv.js +1 -0
- package/web/dist/assets/S63OEDPG-CKlhpvl2.js +3 -0
- package/web/dist/assets/SG5TVYLW-BBtiwXPL.js +2 -0
- package/web/dist/assets/TeamsPage-CDyjrK-I.js +1 -0
- package/web/dist/assets/UGRSUGA2-urZ_hx83.js +5 -0
- package/web/dist/assets/V44WA6U7-DYfX3OlM.js +1 -0
- package/web/dist/assets/V67O4WWV-xYxJ00QW.js +1 -0
- package/web/dist/assets/VBUCM3YD-JHGjuJC_.js +1 -0
- package/web/dist/assets/VINGRCPA-DZuJa3SH.js +7 -0
- package/web/dist/assets/WL6DNKX4-BHqripdA.js +2 -0
- package/web/dist/assets/WQ63CJYP-D6G5frDY.js +2 -0
- package/web/dist/assets/Watchtower-DYwoUWME.js +2 -0
- package/web/dist/assets/XOYAOB3S-L-KeA1EF.js +13 -0
- package/web/dist/assets/Y5LQLG3M-dr2-erb8.js +2 -0
- package/web/dist/assets/YIK6JTN6--1t-Ud8k.js +2 -0
- package/web/dist/assets/artifacts-DU9uRgs1.js +1 -0
- package/web/dist/assets/index-C3ci5UtY.css +1 -0
- package/web/dist/assets/index-LNMZCqgE.js +36 -0
- package/web/dist/assets/lazy-f3CuUNJZ.js +43 -0
- package/web/dist/assets/live-ycZ_aQZO.js +5 -0
- package/web/dist/assets/mermaid.worker-WDVRAh32.js +1 -0
- package/web/dist/assets/pane-layout-0RrLHGDr.js +1 -0
- package/web/dist/assets/preload-helper-BLdHbfdN.js +1 -0
- package/web/dist/assets/{renderer2d-CYag1CnR.js → renderer2d-BIwbgRoX.js} +1 -1
- package/web/dist/assets/{renderer3d-BRB6rgDJ.js → renderer3d-C1a7eYGQ.js} +1 -1
- package/web/dist/assets/{safe-area-fOy0AOuG.js → safe-area-DHhPy61Q.js} +1 -1
- package/web/dist/assets/{silero-worker-DhlIHyD_.js → silero-worker-e802lB5T.js} +1 -1
- package/web/dist/assets/smart-turn-v3.2-cpu-WpeLRbPA.onnx +0 -0
- package/web/dist/assets/smart-turn-worker-DaVJPdvJ.js +11 -0
- package/web/dist/assets/source-BWlvaODa.js +1 -0
- package/web/dist/assets/toast-Cuu2K0hx.js +1 -0
- package/web/dist/assets/ui-CFB4wQfF.js +3 -0
- package/web/dist/index.html +8 -6
- package/web/src/voice/turn/LICENSE +28 -0
- package/web/dist/assets/ArtifactPage-Dq7VW-bU.js +0 -1
- package/web/dist/assets/ArtifactPane-DKwbMTX9.js +0 -1
- package/web/dist/assets/ArtifactWorkbench-vrgIfncX.js +0 -6
- package/web/dist/assets/Library-D6-Go6Cu.js +0 -2
- package/web/dist/assets/ProfilesDestination-BOML2Dgf.js +0 -1
- package/web/dist/assets/TeamsPage-CNBTE8gB.js +0 -1
- package/web/dist/assets/Watchtower-Dor1Ubjy.js +0 -2
- package/web/dist/assets/hub-CrbItiwP.js +0 -13
- package/web/dist/assets/index-82Ce0IAf.css +0 -1
- package/web/dist/assets/index-D9SP3_0z.js +0 -22
- package/web/dist/assets/live-BNJgy0vg.js +0 -5
- package/web/dist/assets/pane-layout-EdR1kRrG.js +0 -1
- package/web/dist/assets/preload-helper-DqrJTq0t.js +0 -1
- package/web/dist/assets/source-Wbq35qGp.js +0 -1
- package/web/dist/assets/ui-j5FFk90B.js +0 -3
|
@@ -7,7 +7,7 @@ import { createInterface } from "readline";
|
|
|
7
7
|
// packages/cli/package.json
|
|
8
8
|
var package_default = {
|
|
9
9
|
name: "@huynle/hub-cli",
|
|
10
|
-
version: "0.
|
|
10
|
+
version: "0.9.1",
|
|
11
11
|
private: true,
|
|
12
12
|
description: "The hub CLI (`hub` / `hubctl`) for agents and scripts (internal; bundled into @huynle/hub, not published on its own).",
|
|
13
13
|
license: "MIT",
|
|
@@ -710,7 +710,11 @@ var decisionFields = (options) => {
|
|
|
710
710
|
var whyField = (options) => options.why !== undefined ? { why: options.why } : {};
|
|
711
711
|
var signalOf = (options) => options.signal ? { signal: options.signal } : {};
|
|
712
712
|
var idempotent = (options) => ({ ...signalOf(options), ...options.idempotencyKey ? { headers: { "idempotency-key": options.idempotencyKey } } : {} });
|
|
713
|
-
var sendFields = (options) => ({
|
|
713
|
+
var sendFields = (options) => ({
|
|
714
|
+
...options.wake !== undefined ? { wake: options.wake } : {},
|
|
715
|
+
...options.urgent !== undefined ? { urgent: options.urgent } : {},
|
|
716
|
+
...options.notAfter !== undefined ? { notAfter: options.notAfter } : {}
|
|
717
|
+
});
|
|
714
718
|
var roleTarget = (options) => ({ ...options.session ? { session: options.session } : {}, ...options.project !== undefined ? { project: options.project } : {} });
|
|
715
719
|
|
|
716
720
|
class HubClient {
|
|
@@ -916,7 +920,7 @@ class HubClient {
|
|
|
916
920
|
return this.call("POST", `${AGENT}/roles/release`, { body: { role: bareRole(role), ...roleTarget(options) }, ...signalOf(options) });
|
|
917
921
|
}
|
|
918
922
|
msg(to, text, options = {}) {
|
|
919
|
-
return this.call("POST", `${AGENT}/msg`, { body: { to, text, ...sendFields(options), ...options.all ? { all: true } : {} }, ...
|
|
923
|
+
return this.call("POST", `${AGENT}/msg`, { body: { to, text, ...sendFields(options), ...options.all ? { all: true } : {} }, ...idempotent(options) });
|
|
920
924
|
}
|
|
921
925
|
async ask(to, text, options = {}) {
|
|
922
926
|
if (options.all && options.wait !== undefined)
|
|
@@ -933,7 +937,7 @@ class HubClient {
|
|
|
933
937
|
...options.wait !== undefined ? { wait: first } : {},
|
|
934
938
|
...decisionFields(options)
|
|
935
939
|
};
|
|
936
|
-
const asked = await this.call("POST", `${AGENT}/ask`, { body, ...
|
|
940
|
+
const asked = await this.call("POST", `${AGENT}/ask`, { body, ...idempotent(options) });
|
|
937
941
|
if ("sent" in asked)
|
|
938
942
|
return asked;
|
|
939
943
|
let current = asked;
|
|
@@ -1110,6 +1114,7 @@ var EXIT_OF = {
|
|
|
1110
1114
|
offline: EXIT.unavailable,
|
|
1111
1115
|
busy: EXIT.unavailable,
|
|
1112
1116
|
gone: EXIT.gone,
|
|
1117
|
+
expired: EXIT.gone,
|
|
1113
1118
|
unauthorized: EXIT.unauthorized,
|
|
1114
1119
|
token_expired: EXIT.unauthorized
|
|
1115
1120
|
};
|
|
@@ -1690,7 +1695,7 @@ var SUPPORTED_TARGETS = Object.freeze([
|
|
|
1690
1695
|
{ target: "darwin-x64", platform: "darwin", arch: "x64", libc: "none", serviceManager: "launchd", bunTarget: "bun-darwin-x64-baseline" },
|
|
1691
1696
|
{ target: "linux-x64", platform: "linux", arch: "x64", libc: "glibc", serviceManager: "systemd-user", bunTarget: "bun-linux-x64-baseline" },
|
|
1692
1697
|
{ target: "linux-arm64", platform: "linux", arch: "arm64", libc: "glibc", serviceManager: "systemd-user", bunTarget: "bun-linux-arm64" },
|
|
1693
|
-
{ target: "windows-x64", platform: "windows", arch: "x64", libc: "none", serviceManager: "
|
|
1698
|
+
{ target: "windows-x64", platform: "windows", arch: "x64", libc: "none", serviceManager: "task-scheduler", bunTarget: "bun-windows-x64-baseline" }
|
|
1694
1699
|
]);
|
|
1695
1700
|
var UNSUPPORTED_TARGETS = Object.freeze([
|
|
1696
1701
|
{ target: "windows-arm64", reason: "Windows on arm64 has no hubctl build yet (only windows-x64 is built)" },
|
|
@@ -1711,7 +1716,7 @@ function checkTarget(target) {
|
|
|
1711
1716
|
return fail("", "unsupported_platform", known ? `${target} is not supported: ${known.reason}` : `${target} is not a supported target (supported: ${SUPPORTED_TARGETS.map((t) => t.target).join(", ")})`);
|
|
1712
1717
|
}
|
|
1713
1718
|
// packages/cli/src/bootstrap/contracts/paths.ts
|
|
1714
|
-
var ROOT_IDS = ["home", "config", "state", "data", "user-bin", "launch-agents", "systemd-user", "system-bin"];
|
|
1719
|
+
var ROOT_IDS = ["home", "config", "state", "data", "user-bin", "launch-agents", "systemd-user", "system-bin", "task-defs"];
|
|
1715
1720
|
var ELEVATED_ROOTS = ["system-bin"];
|
|
1716
1721
|
var BAD_REL = /[\u0000-\u001f\u007f\\]/;
|
|
1717
1722
|
function isSafeRel(rel) {
|
|
@@ -1793,7 +1798,8 @@ var EMBEDDED_ASSETS = Object.freeze({
|
|
|
1793
1798
|
"service/launchd/claude-code": { kind: "launchd-plist", service: "claude-code", manager: "launchd", source: "scripts/io.github.huynle.hub-claude-code.plist", installAs: "io.github.huynle.hub-claude-code.plist" },
|
|
1794
1799
|
"service/systemd-user/claude-code": { kind: "systemd-unit", service: "claude-code", manager: "systemd-user", source: "scripts/hub-claude-code.service", installAs: "hub-claude-code.service" },
|
|
1795
1800
|
"service/launchd/codex-spoke": { kind: "launchd-plist", service: "codex-spoke", manager: "launchd", source: "scripts/io.github.huynle.hub-codex-spoke.plist", installAs: "io.github.huynle.hub-codex-spoke.plist" },
|
|
1796
|
-
"service/systemd-user/codex-spoke": { kind: "systemd-unit", service: "codex-spoke", manager: "systemd-user", source: "scripts/hub-codex-spoke.service", installAs: "hub-codex-spoke.service" }
|
|
1801
|
+
"service/systemd-user/codex-spoke": { kind: "systemd-unit", service: "codex-spoke", manager: "systemd-user", source: "scripts/hub-codex-spoke.service", installAs: "hub-codex-spoke.service" },
|
|
1802
|
+
"service/task-scheduler/node": { kind: "task-xml", service: "node", manager: "task-scheduler", source: "scripts/hub-node.task.xml", installAs: "hub-node.xml" }
|
|
1797
1803
|
});
|
|
1798
1804
|
// packages/cli/src/bootstrap/contracts/harness.ts
|
|
1799
1805
|
var HARNESS_ORDER = ["opencode", "claude-code", "codex"];
|
|
@@ -1879,6 +1885,7 @@ function createAdapterRegistry() {
|
|
|
1879
1885
|
// packages/cli/src/bootstrap/contracts/actions.ts
|
|
1880
1886
|
var EXEC_KEYS = ["command", "cmd", "commands", "shell", "script", "scripts", "exec", "execute", "argv", "args", "arguments", "run", "eval", "sh", "bash", "program", "spawn", "hook", "hooks"];
|
|
1881
1887
|
var CONFIG_OWNERS = ["user", "hub", ...HARNESS_ORDER.map((h) => `harness:${h}`)];
|
|
1888
|
+
var SERVICE_MANAGERS = ["launchd", "systemd-user", "task-scheduler"];
|
|
1882
1889
|
var harnessVersionRef = (harness, version) => ({ root: "data", rel: `hub/harnesses/${harness}/${version}` });
|
|
1883
1890
|
var harnessCurrentRef = (harness) => ({ root: "data", rel: `hub/harnesses/${harness}/current` });
|
|
1884
1891
|
var COMMON2 = ["id", "kind", "escalation", "summary"];
|
|
@@ -1907,7 +1914,7 @@ var ASSET_ID = /^[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9][a-z0-9._-]*)*$/;
|
|
|
1907
1914
|
var MACHINE_NAME = /^[A-Za-z0-9._-]{1,64}$/;
|
|
1908
1915
|
var MCP_NAME = /^[a-z0-9][a-z0-9-]{0,31}$/;
|
|
1909
1916
|
var JSON_POINTER = /^(?:\/(?:[^~/]|~[01])*)+$/;
|
|
1910
|
-
var SERVICE_ROOT = { launchd: "launch-agents", "systemd-user": "systemd-user" };
|
|
1917
|
+
var SERVICE_ROOT = Object.freeze({ launchd: "launch-agents", "systemd-user": "systemd-user", "task-scheduler": "task-defs" });
|
|
1911
1918
|
var EXEC = new Set(EXEC_KEYS);
|
|
1912
1919
|
function findExecKeys(value, base = "") {
|
|
1913
1920
|
const found = [];
|
|
@@ -2094,7 +2101,7 @@ function checkAction(c, path, value) {
|
|
|
2094
2101
|
break;
|
|
2095
2102
|
case "install-service": {
|
|
2096
2103
|
oneOf(c, at("service"), r.service, SERVICE_IDS);
|
|
2097
|
-
oneOf(c, at("manager"), r.manager,
|
|
2104
|
+
oneOf(c, at("manager"), r.manager, SERVICE_MANAGERS);
|
|
2098
2105
|
ref("path");
|
|
2099
2106
|
digest(c, at("sha256"), r.sha256);
|
|
2100
2107
|
const asset = typeof r.asset === "string" ? EMBEDDED_ASSETS[r.asset] : undefined;
|
|
@@ -2107,18 +2114,18 @@ function checkAction(c, path, value) {
|
|
|
2107
2114
|
}
|
|
2108
2115
|
case "deactivate-service":
|
|
2109
2116
|
oneOf(c, at("service"), r.service, SERVICE_IDS);
|
|
2110
|
-
oneOf(c, at("manager"), r.manager,
|
|
2117
|
+
oneOf(c, at("manager"), r.manager, SERVICE_MANAGERS);
|
|
2111
2118
|
break;
|
|
2112
2119
|
case "remove-service":
|
|
2113
2120
|
oneOf(c, at("service"), r.service, SERVICE_IDS);
|
|
2114
|
-
oneOf(c, at("manager"), r.manager,
|
|
2121
|
+
oneOf(c, at("manager"), r.manager, SERVICE_MANAGERS);
|
|
2115
2122
|
ref("path");
|
|
2116
2123
|
digest(c, at("sha256"), r.sha256);
|
|
2117
2124
|
checkServiceUnitPath(c, at, r);
|
|
2118
2125
|
break;
|
|
2119
2126
|
case "activate-service":
|
|
2120
2127
|
oneOf(c, at("service"), r.service, SERVICE_IDS);
|
|
2121
|
-
oneOf(c, at("manager"), r.manager,
|
|
2128
|
+
oneOf(c, at("manager"), r.manager, SERVICE_MANAGERS);
|
|
2122
2129
|
if (r.confirm !== "explicit")
|
|
2123
2130
|
c.add(at("confirm"), "invalid_value", "services are only activated on explicit confirmation");
|
|
2124
2131
|
break;
|
|
@@ -2252,12 +2259,14 @@ function validateDiscoverySnapshot(value) {
|
|
|
2252
2259
|
semver(c, pointer(p, "version"), rt.version);
|
|
2253
2260
|
absPath(c, pointer(p, "path"), rt.path);
|
|
2254
2261
|
});
|
|
2255
|
-
oneOf(c, "/managers/service", managers.service, ["launchd", "systemd-user", "none"]);
|
|
2262
|
+
oneOf(c, "/managers/service", managers.service, ["launchd", "systemd-user", "task-scheduler", "none"]);
|
|
2256
2263
|
const platform = host?.platform;
|
|
2257
2264
|
if (managers.service === "launchd" && platform !== "darwin")
|
|
2258
2265
|
c.add("/managers/service", "inconsistent", "launchd is macOS only");
|
|
2259
2266
|
if (managers.service === "systemd-user" && platform !== "linux")
|
|
2260
2267
|
c.add("/managers/service", "inconsistent", "systemd is Linux only");
|
|
2268
|
+
if (managers.service === "task-scheduler" && platform !== "win32")
|
|
2269
|
+
c.add("/managers/service", "inconsistent", "Task Scheduler is Windows only");
|
|
2261
2270
|
}
|
|
2262
2271
|
list(c, "/harnesses", r.harnesses, HARNESS_ORDER.length, (p, v) => checkHarness(c, p, v), (v) => v?.id);
|
|
2263
2272
|
list(c, "/runtimes", r.runtimes, RUNTIME_IDS.length, (p, v) => {
|
|
@@ -2275,7 +2284,7 @@ function validateDiscoverySnapshot(value) {
|
|
|
2275
2284
|
return;
|
|
2276
2285
|
absPath(c, pointer(p, "path"), cf.path);
|
|
2277
2286
|
oneOf(c, pointer(p, "owner"), cf.owner, [...CONFIG_OWNERS, "package-manager"]);
|
|
2278
|
-
oneOf(c, pointer(p, "format"), cf.format, ["json", "jsonc", "toml", "plist", "unit"]);
|
|
2287
|
+
oneOf(c, pointer(p, "format"), cf.format, ["json", "jsonc", "toml", "plist", "unit", "xml"]);
|
|
2279
2288
|
if (cf.sha256 !== null)
|
|
2280
2289
|
digest(c, pointer(p, "sha256"), cf.sha256);
|
|
2281
2290
|
}, (v) => v?.path);
|
|
@@ -3349,6 +3358,99 @@ function parseFrame(raw) {
|
|
|
3349
3358
|
}
|
|
3350
3359
|
}
|
|
3351
3360
|
|
|
3361
|
+
// packages/protocol/src/launch.ts
|
|
3362
|
+
var WORKSPACE_MODES = ["worktree", "checkout", "clone"];
|
|
3363
|
+
var RUNTIME_KINDS = ["host", "openshell"];
|
|
3364
|
+
var LAUNCH_END_WHEN = ["idle", "asked", "never"];
|
|
3365
|
+
var CLEANUP_POLICIES = ["keep", "remove"];
|
|
3366
|
+
var PATH_MAX = 4096;
|
|
3367
|
+
var REF_MAX = 200;
|
|
3368
|
+
var REMOTE_MAX = 2048;
|
|
3369
|
+
var LABELS_MAX = 32;
|
|
3370
|
+
var LABEL_VALUE_MAX = 256;
|
|
3371
|
+
var LABEL_KEY = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,62}$/;
|
|
3372
|
+
function isAbsolutePath2(v) {
|
|
3373
|
+
if (typeof v !== "string" || v.length > PATH_MAX || UNSAFE_CHARS.test(v))
|
|
3374
|
+
return false;
|
|
3375
|
+
if (WINDOWS_ROOT2.test(v))
|
|
3376
|
+
return !v.includes("\\") && normalSegments(v.slice(2));
|
|
3377
|
+
if (!v.startsWith("/"))
|
|
3378
|
+
return false;
|
|
3379
|
+
return normalSegments(v);
|
|
3380
|
+
}
|
|
3381
|
+
function normalSegments(rest) {
|
|
3382
|
+
if (!rest.startsWith("/"))
|
|
3383
|
+
return false;
|
|
3384
|
+
if (rest === "/")
|
|
3385
|
+
return true;
|
|
3386
|
+
return rest.slice(1).replace(/\/$/, "").split("/").every((part) => part !== "" && part !== "." && part !== "..");
|
|
3387
|
+
}
|
|
3388
|
+
var WINDOWS_ROOT2 = /^[A-Z]:(?=\/)/;
|
|
3389
|
+
var isWindowsAbsolutePath = (v) => typeof v === "string" && WINDOWS_ROOT2.test(v) && isAbsolutePath2(v);
|
|
3390
|
+
function canonicalNativePath(value) {
|
|
3391
|
+
if (typeof value !== "string")
|
|
3392
|
+
return;
|
|
3393
|
+
let p = value.replace(/^(\\\\\?\\|\\\?\?\\)(?=[A-Za-z]:\\)/, "");
|
|
3394
|
+
if (/^[A-Za-z]:[\\/]/.test(p))
|
|
3395
|
+
p = p[0].toUpperCase() + p.slice(1).replace(/\\/g, "/");
|
|
3396
|
+
else if (!p.startsWith("/") || p.startsWith("//"))
|
|
3397
|
+
return;
|
|
3398
|
+
p = p.replace(/\/{2,}/g, "/");
|
|
3399
|
+
if (p.endsWith("/") && p !== "/" && !/^[A-Z]:\/$/.test(p))
|
|
3400
|
+
p = p.slice(0, -1);
|
|
3401
|
+
return isAbsolutePath2(p) ? p : undefined;
|
|
3402
|
+
}
|
|
3403
|
+
function isRefName(v) {
|
|
3404
|
+
if (typeof v !== "string" || v.length === 0 || v.length > REF_MAX)
|
|
3405
|
+
return false;
|
|
3406
|
+
if (!/^[A-Za-z0-9._/-]+$/.test(v) || v.startsWith("-") || v.endsWith(".") || v.includes(".."))
|
|
3407
|
+
return false;
|
|
3408
|
+
return v.split("/").every((part) => part !== "" && !part.startsWith(".") && !part.endsWith(".lock"));
|
|
3409
|
+
}
|
|
3410
|
+
function isGitRemote(v) {
|
|
3411
|
+
if (typeof v !== "string" || v.length === 0 || v.length > REMOTE_MAX || v.startsWith("-") || /[\s\p{Cc}\p{Cf}]/u.test(v))
|
|
3412
|
+
return false;
|
|
3413
|
+
if (/^[A-Za-z0-9][A-Za-z0-9._-]*@[A-Za-z0-9][A-Za-z0-9.-]*:[^-:][^:]*$/.test(v))
|
|
3414
|
+
return true;
|
|
3415
|
+
try {
|
|
3416
|
+
const url = new URL(v);
|
|
3417
|
+
return (url.protocol === "https:" || url.protocol === "ssh:") && url.hostname.length > 0 && !url.hostname.startsWith("-") && url.pathname.length > 1;
|
|
3418
|
+
} catch {
|
|
3419
|
+
return false;
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
function launchWorkspaceProblem(v) {
|
|
3423
|
+
if (!isObj(v))
|
|
3424
|
+
return "workspace must be an object";
|
|
3425
|
+
if (!isAbsolutePath2(v.repo))
|
|
3426
|
+
return `workspace.repo must be an absolute path of at most ${PATH_MAX} characters without . or .. segments`;
|
|
3427
|
+
if (!WORKSPACE_MODES.includes(v.mode))
|
|
3428
|
+
return `workspace.mode must be one of ${WORKSPACE_MODES.join(", ")}`;
|
|
3429
|
+
if (v.branch !== undefined && !isRefName(v.branch))
|
|
3430
|
+
return "workspace.branch must be a valid branch name (letters, digits, . _ / -; not starting with -)";
|
|
3431
|
+
if (v.base !== undefined && !isRefName(v.base))
|
|
3432
|
+
return "workspace.base must be a valid branch name (letters, digits, . _ / -; not starting with -)";
|
|
3433
|
+
if (v.remote !== undefined && !isGitRemote(v.remote))
|
|
3434
|
+
return "workspace.remote must be an https or ssh git remote";
|
|
3435
|
+
if (v.mode === "clone" && v.remote === undefined)
|
|
3436
|
+
return "workspace.remote is required for a clone";
|
|
3437
|
+
return;
|
|
3438
|
+
}
|
|
3439
|
+
function labelsProblem(v) {
|
|
3440
|
+
if (!isObj(v))
|
|
3441
|
+
return "labels must be an object";
|
|
3442
|
+
const entries = Object.entries(v);
|
|
3443
|
+
if (entries.length > LABELS_MAX)
|
|
3444
|
+
return `at most ${LABELS_MAX} labels`;
|
|
3445
|
+
for (const [key, value] of entries) {
|
|
3446
|
+
if (!LABEL_KEY.test(key))
|
|
3447
|
+
return `label key ${JSON.stringify(key.slice(0, 80))} must be 1–63 letters, digits, . _ / - (starting with a letter or digit)`;
|
|
3448
|
+
if (typeof value !== "string" || value.length > LABEL_VALUE_MAX || UNSAFE_CHARS.test(value))
|
|
3449
|
+
return `label ${key} must be text of at most ${LABEL_VALUE_MAX} printable characters`;
|
|
3450
|
+
}
|
|
3451
|
+
return;
|
|
3452
|
+
}
|
|
3453
|
+
|
|
3352
3454
|
// packages/protocol/src/contracts.ts
|
|
3353
3455
|
var SLUG = /^[a-z0-9-]{2,40}$/;
|
|
3354
3456
|
var isSlug = (v) => typeof v === "string" && SLUG.test(v);
|
|
@@ -3551,6 +3653,113 @@ function sessionContextProblem(v) {
|
|
|
3551
3653
|
return;
|
|
3552
3654
|
}
|
|
3553
3655
|
|
|
3656
|
+
// packages/protocol/src/media.ts
|
|
3657
|
+
var MEDIA_LIMITS = {
|
|
3658
|
+
maxBytes: 8 * 1024 * 1024,
|
|
3659
|
+
maxPixels: 40000000,
|
|
3660
|
+
maxSide: 16384,
|
|
3661
|
+
chunkBytes: 512 * 1024,
|
|
3662
|
+
perRow: 8
|
|
3663
|
+
};
|
|
3664
|
+
var MEDIA_PATH_MAX = 4096;
|
|
3665
|
+
var MEDIA_REF_MAX = 2 + Math.ceil(MEDIA_PATH_MAX * 4 / 3) + 4;
|
|
3666
|
+
var FILE_REF = /^f(0|[1-9]\d{0,2})$/;
|
|
3667
|
+
var TOOL_REF = /^t(0|[1-9]\d{0,3})\.(0|[1-9]\d{0,2})$/;
|
|
3668
|
+
var PATH_REF = /^p:([A-Za-z0-9_-]+)$/;
|
|
3669
|
+
function base64url(bytes) {
|
|
3670
|
+
let binary = "";
|
|
3671
|
+
for (const byte of bytes)
|
|
3672
|
+
binary += String.fromCharCode(byte);
|
|
3673
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
3674
|
+
}
|
|
3675
|
+
function fromBase64url(text) {
|
|
3676
|
+
if (text.length % 4 === 1)
|
|
3677
|
+
return;
|
|
3678
|
+
try {
|
|
3679
|
+
const binary = atob(text.replace(/-/g, "+").replace(/_/g, "/"));
|
|
3680
|
+
return Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
3681
|
+
} catch {
|
|
3682
|
+
return;
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3685
|
+
function mediaPathRef(path) {
|
|
3686
|
+
return `p:${base64url(new TextEncoder().encode(path))}`;
|
|
3687
|
+
}
|
|
3688
|
+
function parseMediaRef(ref) {
|
|
3689
|
+
if (typeof ref !== "string" || ref.length > MEDIA_REF_MAX)
|
|
3690
|
+
return;
|
|
3691
|
+
const file = FILE_REF.exec(ref);
|
|
3692
|
+
if (file)
|
|
3693
|
+
return { kind: "file", index: Number(file[1]) };
|
|
3694
|
+
const tool = TOOL_REF.exec(ref);
|
|
3695
|
+
if (tool)
|
|
3696
|
+
return { kind: "tool", part: Number(tool[1]), item: Number(tool[2]) };
|
|
3697
|
+
const path = PATH_REF.exec(ref);
|
|
3698
|
+
if (!path)
|
|
3699
|
+
return;
|
|
3700
|
+
const bytes = fromBase64url(path[1]);
|
|
3701
|
+
if (!bytes || bytes.length === 0 || bytes.length > MEDIA_PATH_MAX)
|
|
3702
|
+
return;
|
|
3703
|
+
let text;
|
|
3704
|
+
try {
|
|
3705
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
3706
|
+
} catch {
|
|
3707
|
+
return;
|
|
3708
|
+
}
|
|
3709
|
+
if (text.includes("\x00") || mediaPathRef(text) !== ref)
|
|
3710
|
+
return;
|
|
3711
|
+
return { kind: "path", path: text };
|
|
3712
|
+
}
|
|
3713
|
+
var isMediaOffset = (offset) => Number.isInteger(offset) && offset >= 0 && offset <= MEDIA_LIMITS.maxBytes;
|
|
3714
|
+
|
|
3715
|
+
// packages/protocol/src/preview-bundle.ts
|
|
3716
|
+
var PREVIEW_BUNDLE_FEATURE = "previewBundle";
|
|
3717
|
+
var PREVIEW_BUNDLE_LIMITS = {
|
|
3718
|
+
files: 200,
|
|
3719
|
+
fileBytes: 5 * 1024 * 1024,
|
|
3720
|
+
totalBytes: 20 * 1024 * 1024,
|
|
3721
|
+
chunkBytes: 512 * 1024,
|
|
3722
|
+
inlineBytes: 384 * 1024,
|
|
3723
|
+
bundlePathMax: 200,
|
|
3724
|
+
pathMax: 4096,
|
|
3725
|
+
refMax: 300,
|
|
3726
|
+
skipped: 200
|
|
3727
|
+
};
|
|
3728
|
+
var PREVIEW_MEDIA_TYPES = {
|
|
3729
|
+
html: "text/html",
|
|
3730
|
+
htm: "text/html",
|
|
3731
|
+
css: "text/css",
|
|
3732
|
+
js: "text/javascript",
|
|
3733
|
+
mjs: "text/javascript",
|
|
3734
|
+
json: "application/json",
|
|
3735
|
+
txt: "text/plain",
|
|
3736
|
+
csv: "text/csv",
|
|
3737
|
+
md: "text/markdown",
|
|
3738
|
+
pdf: "application/pdf",
|
|
3739
|
+
png: "image/png",
|
|
3740
|
+
jpg: "image/jpeg",
|
|
3741
|
+
jpeg: "image/jpeg",
|
|
3742
|
+
gif: "image/gif",
|
|
3743
|
+
webp: "image/webp",
|
|
3744
|
+
svg: "image/svg+xml"
|
|
3745
|
+
};
|
|
3746
|
+
var MEDIA_TYPES = new Set(Object.values(PREVIEW_MEDIA_TYPES));
|
|
3747
|
+
var HEX64 = /^[0-9a-f]{64}$/;
|
|
3748
|
+
var int2 = (v, min, max) => Number.isSafeInteger(v) && v >= min && v <= max;
|
|
3749
|
+
var isPreviewPath = (v) => typeof v === "string" && v.length > 0 && v.length <= PREVIEW_BUNDLE_LIMITS.pathMax && !/[\u0000\r\n]/.test(v);
|
|
3750
|
+
function isPreviewBundleRequest(v) {
|
|
3751
|
+
if (typeof v.sessionID !== "string" || !v.sessionID)
|
|
3752
|
+
return false;
|
|
3753
|
+
if (typeof v.messageID !== "string" || !v.messageID || v.messageID.length > 300)
|
|
3754
|
+
return false;
|
|
3755
|
+
if (!isPreviewPath(v.path))
|
|
3756
|
+
return false;
|
|
3757
|
+
const chunk = [v.bundle, v.file, v.offset].filter((x) => x !== undefined).length;
|
|
3758
|
+
if (chunk === 0)
|
|
3759
|
+
return true;
|
|
3760
|
+
return chunk === 3 && typeof v.bundle === "string" && HEX64.test(v.bundle) && int2(v.file, 0, PREVIEW_BUNDLE_LIMITS.files - 1) && int2(v.offset, 0, PREVIEW_BUNDLE_LIMITS.fileBytes - 1);
|
|
3761
|
+
}
|
|
3762
|
+
|
|
3554
3763
|
// packages/protocol/src/harness-events.ts
|
|
3555
3764
|
var HARNESS_EVENTS = [
|
|
3556
3765
|
"session.created",
|
|
@@ -3628,74 +3837,6 @@ function harnessEventProblem(event) {
|
|
|
3628
3837
|
return "event data must be an object";
|
|
3629
3838
|
return CHECKS[event.type](event.data);
|
|
3630
3839
|
}
|
|
3631
|
-
// packages/protocol/src/launch.ts
|
|
3632
|
-
var WORKSPACE_MODES = ["worktree", "checkout", "clone"];
|
|
3633
|
-
var RUNTIME_KINDS = ["host", "openshell"];
|
|
3634
|
-
var LAUNCH_END_WHEN = ["idle", "asked", "never"];
|
|
3635
|
-
var CLEANUP_POLICIES = ["keep", "remove"];
|
|
3636
|
-
var PATH_MAX = 4096;
|
|
3637
|
-
var REF_MAX = 200;
|
|
3638
|
-
var REMOTE_MAX = 2048;
|
|
3639
|
-
var LABELS_MAX = 32;
|
|
3640
|
-
var LABEL_VALUE_MAX = 256;
|
|
3641
|
-
var LABEL_KEY = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,62}$/;
|
|
3642
|
-
function isAbsolutePath2(v) {
|
|
3643
|
-
if (typeof v !== "string" || !v.startsWith("/") || v.length > PATH_MAX || UNSAFE_CHARS.test(v))
|
|
3644
|
-
return false;
|
|
3645
|
-
if (v === "/")
|
|
3646
|
-
return true;
|
|
3647
|
-
return v.slice(1).replace(/\/$/, "").split("/").every((part) => part !== "" && part !== "." && part !== "..");
|
|
3648
|
-
}
|
|
3649
|
-
function isRefName(v) {
|
|
3650
|
-
if (typeof v !== "string" || v.length === 0 || v.length > REF_MAX)
|
|
3651
|
-
return false;
|
|
3652
|
-
if (!/^[A-Za-z0-9._/-]+$/.test(v) || v.startsWith("-") || v.endsWith(".") || v.includes(".."))
|
|
3653
|
-
return false;
|
|
3654
|
-
return v.split("/").every((part) => part !== "" && !part.startsWith(".") && !part.endsWith(".lock"));
|
|
3655
|
-
}
|
|
3656
|
-
function isGitRemote(v) {
|
|
3657
|
-
if (typeof v !== "string" || v.length === 0 || v.length > REMOTE_MAX || v.startsWith("-") || /[\s\p{Cc}\p{Cf}]/u.test(v))
|
|
3658
|
-
return false;
|
|
3659
|
-
if (/^[A-Za-z0-9][A-Za-z0-9._-]*@[A-Za-z0-9][A-Za-z0-9.-]*:[^-:][^:]*$/.test(v))
|
|
3660
|
-
return true;
|
|
3661
|
-
try {
|
|
3662
|
-
const url = new URL(v);
|
|
3663
|
-
return (url.protocol === "https:" || url.protocol === "ssh:") && url.hostname.length > 0 && !url.hostname.startsWith("-") && url.pathname.length > 1;
|
|
3664
|
-
} catch {
|
|
3665
|
-
return false;
|
|
3666
|
-
}
|
|
3667
|
-
}
|
|
3668
|
-
function launchWorkspaceProblem(v) {
|
|
3669
|
-
if (!isObj(v))
|
|
3670
|
-
return "workspace must be an object";
|
|
3671
|
-
if (!isAbsolutePath2(v.repo))
|
|
3672
|
-
return `workspace.repo must be an absolute path of at most ${PATH_MAX} characters without . or .. segments`;
|
|
3673
|
-
if (!WORKSPACE_MODES.includes(v.mode))
|
|
3674
|
-
return `workspace.mode must be one of ${WORKSPACE_MODES.join(", ")}`;
|
|
3675
|
-
if (v.branch !== undefined && !isRefName(v.branch))
|
|
3676
|
-
return "workspace.branch must be a valid branch name (letters, digits, . _ / -; not starting with -)";
|
|
3677
|
-
if (v.base !== undefined && !isRefName(v.base))
|
|
3678
|
-
return "workspace.base must be a valid branch name (letters, digits, . _ / -; not starting with -)";
|
|
3679
|
-
if (v.remote !== undefined && !isGitRemote(v.remote))
|
|
3680
|
-
return "workspace.remote must be an https or ssh git remote";
|
|
3681
|
-
if (v.mode === "clone" && v.remote === undefined)
|
|
3682
|
-
return "workspace.remote is required for a clone";
|
|
3683
|
-
return;
|
|
3684
|
-
}
|
|
3685
|
-
function labelsProblem(v) {
|
|
3686
|
-
if (!isObj(v))
|
|
3687
|
-
return "labels must be an object";
|
|
3688
|
-
const entries = Object.entries(v);
|
|
3689
|
-
if (entries.length > LABELS_MAX)
|
|
3690
|
-
return `at most ${LABELS_MAX} labels`;
|
|
3691
|
-
for (const [key, value] of entries) {
|
|
3692
|
-
if (!LABEL_KEY.test(key))
|
|
3693
|
-
return `label key ${JSON.stringify(key.slice(0, 80))} must be 1–63 letters, digits, . _ / - (starting with a letter or digit)`;
|
|
3694
|
-
if (typeof value !== "string" || value.length > LABEL_VALUE_MAX || UNSAFE_CHARS.test(value))
|
|
3695
|
-
return `label ${key} must be text of at most ${LABEL_VALUE_MAX} printable characters`;
|
|
3696
|
-
}
|
|
3697
|
-
return;
|
|
3698
|
-
}
|
|
3699
3840
|
// packages/protocol/src/node.ts
|
|
3700
3841
|
var NODE_ROLE = "node";
|
|
3701
3842
|
var NODE_FEATURES = ["sandbox"];
|
|
@@ -3936,6 +4077,60 @@ var ARTIFACT_LIMITS = {
|
|
|
3936
4077
|
aiJobsPerArtifact: 2,
|
|
3937
4078
|
aiInstructionChars: 8000
|
|
3938
4079
|
};
|
|
4080
|
+
// packages/protocol/src/media-allowlist.ts
|
|
4081
|
+
var PUBLIC_SUFFIXES = new Set([
|
|
4082
|
+
...["co", "org", "ac", "gov", "net", "me", "ltd", "plc", "sch", "nhs"].map((s) => `${s}.uk`),
|
|
4083
|
+
...["com", "net", "org", "edu", "gov", "id", "asn"].map((s) => `${s}.au`),
|
|
4084
|
+
...["co", "ne", "or", "ac", "go", "gr", "ed", "ad", "lg"].map((s) => `${s}.jp`),
|
|
4085
|
+
...["co", "org", "net", "ac", "govt", "school", "geek", "gen", "kiwi", "maori"].map((s) => `${s}.nz`),
|
|
4086
|
+
...["com", "net", "org", "gov", "edu"].map((s) => `${s}.br`),
|
|
4087
|
+
...["com", "net", "org", "gov", "edu", "ac"].map((s) => `${s}.cn`),
|
|
4088
|
+
...["com", "net", "org", "gob", "edu"].map((s) => `${s}.mx`),
|
|
4089
|
+
...["co", "net", "org", "firm", "gen", "ind", "ac", "edu", "gov", "res"].map((s) => `${s}.in`),
|
|
4090
|
+
...["co", "org", "net", "ac", "gov", "web"].map((s) => `${s}.za`),
|
|
4091
|
+
...["com", "net", "org", "gov", "edu"].map((s) => `${s}.tr`),
|
|
4092
|
+
...["com", "net", "org", "gov", "edu"].map((s) => `${s}.sg`),
|
|
4093
|
+
...["com", "net", "org", "gov", "edu"].map((s) => `${s}.hk`),
|
|
4094
|
+
...["co", "or", "ne", "go", "ac", "re"].map((s) => `${s}.kr`),
|
|
4095
|
+
...["com", "net", "org", "gov", "edu"].map((s) => `${s}.tw`),
|
|
4096
|
+
...["com", "net", "org", "gov", "edu"].map((s) => `${s}.ar`),
|
|
4097
|
+
...["co", "or", "ac", "go"].map((s) => `${s}.id`),
|
|
4098
|
+
...["co", "or", "ac", "go"].map((s) => `${s}.th`),
|
|
4099
|
+
...["com", "net", "org", "gov", "edu"].map((s) => `${s}.my`),
|
|
4100
|
+
...["com", "net", "org", "gov", "edu"].map((s) => `${s}.ph`),
|
|
4101
|
+
...["com", "net", "org", "gov", "edu"].map((s) => `${s}.pk`),
|
|
4102
|
+
...["com", "net", "org", "gov", "edu"].map((s) => `${s}.ua`),
|
|
4103
|
+
...["com", "net", "org", "gov", "edu"].map((s) => `${s}.pl`),
|
|
4104
|
+
...["co", "or", "ac"].map((s) => `${s}.at`),
|
|
4105
|
+
...["co", "org", "ac"].map((s) => `${s}.il`),
|
|
4106
|
+
"com.es",
|
|
4107
|
+
"org.es",
|
|
4108
|
+
"co.it",
|
|
4109
|
+
"com.ru",
|
|
4110
|
+
"co.ca",
|
|
4111
|
+
"eu.org",
|
|
4112
|
+
"github.io",
|
|
4113
|
+
"githubpreview.dev",
|
|
4114
|
+
"gitlab.io",
|
|
4115
|
+
"pages.dev",
|
|
4116
|
+
"workers.dev",
|
|
4117
|
+
"netlify.app",
|
|
4118
|
+
"vercel.app",
|
|
4119
|
+
"herokuapp.com",
|
|
4120
|
+
"appspot.com",
|
|
4121
|
+
"blogspot.com",
|
|
4122
|
+
"cloudfront.net",
|
|
4123
|
+
"azurewebsites.net",
|
|
4124
|
+
"web.app",
|
|
4125
|
+
"firebaseapp.com",
|
|
4126
|
+
"s3.amazonaws.com",
|
|
4127
|
+
"ngrok.io",
|
|
4128
|
+
"ngrok-free.app",
|
|
4129
|
+
"glitch.me",
|
|
4130
|
+
"repl.co",
|
|
4131
|
+
"fly.dev",
|
|
4132
|
+
"onrender.com"
|
|
4133
|
+
]);
|
|
3939
4134
|
// packages/protocol/src/graph.ts
|
|
3940
4135
|
var GRAPH_LIMITS = {
|
|
3941
4136
|
nodes: 2000,
|
|
@@ -4054,7 +4249,7 @@ var IMPLIED_OPENCODE_FEATURES = [
|
|
|
4054
4249
|
"expose",
|
|
4055
4250
|
"create"
|
|
4056
4251
|
];
|
|
4057
|
-
var OPTIONAL_SPOKE_FEATURES = [...IMPLIED_OPENCODE_FEATURES, "autoName", "sessionTokens", "deliver", "hubPrompt", "permissionMessage", "actTokens", "launch", "fork", "revert", "dismissError"];
|
|
4252
|
+
var OPTIONAL_SPOKE_FEATURES = [...IMPLIED_OPENCODE_FEATURES, "autoName", "sessionTokens", "deliver", "hubPrompt", "permissionMessage", "actTokens", "launch", "fork", "revert", "dismissError", "timelineMedia", PREVIEW_BUNDLE_FEATURE, "voiceFront"];
|
|
4058
4253
|
var CORE_OPS = ["turn", "steer", "interrupt", "stop", "permission", "form"];
|
|
4059
4254
|
var KNOWN_SPOKE_FEATURES = [...CORE_OPS, ...OPTIONAL_SPOKE_FEATURES, "turnEvents", PROFILE_APPLY_FEATURE, SESSION_CONTEXT_FEATURE];
|
|
4060
4255
|
var SPOKE_FEATURES = [...OPTIONAL_SPOKE_FEATURES, PROFILE_APPLY_FEATURE, SESSION_CONTEXT_FEATURE];
|
|
@@ -4072,6 +4267,8 @@ function featureForOp(op) {
|
|
|
4072
4267
|
case "revertRestore":
|
|
4073
4268
|
case "revertCommit":
|
|
4074
4269
|
return "revert";
|
|
4270
|
+
case "removeFront":
|
|
4271
|
+
return "voiceFront";
|
|
4075
4272
|
default:
|
|
4076
4273
|
return op;
|
|
4077
4274
|
}
|
|
@@ -4252,15 +4449,20 @@ function isServerRequest(v) {
|
|
|
4252
4449
|
return nonEmpty(v.sessionID) && promptBody(v);
|
|
4253
4450
|
case "timelineFile":
|
|
4254
4451
|
return nonEmpty(v.sessionID) && nonEmpty(v.messageID) && v.messageID.length <= 300 && Number.isInteger(v.index) && Number(v.index) >= 0 && Number(v.index) < 1000;
|
|
4452
|
+
case "timelineMedia":
|
|
4453
|
+
return nonEmpty(v.sessionID) && nonEmpty(v.messageID) && v.messageID.length <= 300 && str2(v.ref) && parseMediaRef(v.ref) !== undefined && (v.offset === undefined || isMediaOffset(v.offset));
|
|
4454
|
+
case "previewBundle":
|
|
4455
|
+
return isPreviewBundleRequest(v);
|
|
4255
4456
|
case "diff":
|
|
4256
4457
|
return nonEmpty(v.sessionID) && nonEmpty(v.file) && v.file.length <= 4096;
|
|
4257
4458
|
case "rename":
|
|
4258
4459
|
return nonEmpty(v.sessionID) && str2(v.title) && v.title.length <= 1000;
|
|
4259
4460
|
case "conversation":
|
|
4461
|
+
return nonEmpty(v.sessionID) && typeof v.on === "boolean" && (v.frontID === undefined || nonEmpty(v.frontID) && v.frontID.length <= 300);
|
|
4260
4462
|
case "expose":
|
|
4261
4463
|
return nonEmpty(v.sessionID) && typeof v.on === "boolean";
|
|
4262
4464
|
case "create":
|
|
4263
|
-
return nonEmpty(v.directory) && v.directory.startsWith("/") && v.directory.length <= 4096 && (v.title === undefined || str2(v.title) && v.title.length <= 200) && (v.herdrWorkspace === undefined || nonEmpty(v.herdrWorkspace) && v.herdrWorkspace.length <= 100) && (v.launchID === undefined || deliverID(v.launchID)) && (v.agent === undefined || agentName(v.agent)) && (v.model === undefined || isModelSelection(v.model));
|
|
4465
|
+
return nonEmpty(v.directory) && (v.directory.startsWith("/") || isWindowsAbsolutePath(v.directory)) && v.directory.length <= 4096 && (v.title === undefined || str2(v.title) && v.title.length <= 200) && (v.herdrWorkspace === undefined || nonEmpty(v.herdrWorkspace) && v.herdrWorkspace.length <= 100) && (v.launchID === undefined || deliverID(v.launchID)) && (v.agent === undefined || agentName(v.agent)) && (v.model === undefined || isModelSelection(v.model));
|
|
4264
4466
|
case "sessionTokens":
|
|
4265
4467
|
return Array.isArray(v.tokens) && v.tokens.length <= SESSION_TOKENS_MAX && v.tokens.every(isSessionToken) && (v.hubURL === undefined || isHubURL(v.hubURL));
|
|
4266
4468
|
case "actTokens":
|
|
@@ -4279,6 +4481,7 @@ function isServerRequest(v) {
|
|
|
4279
4481
|
case "revertRestore":
|
|
4280
4482
|
case "revertCommit":
|
|
4281
4483
|
case "dismissError":
|
|
4484
|
+
case "removeFront":
|
|
4282
4485
|
return nonEmpty(v.sessionID);
|
|
4283
4486
|
default:
|
|
4284
4487
|
return false;
|
|
@@ -5300,18 +5503,22 @@ class OpenCodeWarmer {
|
|
|
5300
5503
|
}
|
|
5301
5504
|
}
|
|
5302
5505
|
async function readServiceRegistration(file, options = {}) {
|
|
5303
|
-
const
|
|
5506
|
+
const win32 = isWin32(options);
|
|
5507
|
+
const uid = win32 ? undefined : options.uid ?? (typeof process.getuid === "function" ? process.getuid() : undefined);
|
|
5304
5508
|
let info;
|
|
5305
5509
|
let fd;
|
|
5306
5510
|
try {
|
|
5307
5511
|
const before = lstatSync3(file);
|
|
5308
5512
|
if (!before.isFile())
|
|
5309
5513
|
return;
|
|
5310
|
-
fd = openSync4(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
5514
|
+
fd = win32 ? openSync4(file, "r") : openSync4(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
5311
5515
|
const stat = fstatSync(fd);
|
|
5312
5516
|
if (!stat.isFile() || stat.dev !== before.dev || stat.ino !== before.ino)
|
|
5313
5517
|
return;
|
|
5314
|
-
if (
|
|
5518
|
+
if (win32) {
|
|
5519
|
+
if (!(options.acl ?? defaultWinAcl()).check(nativeWinPath(file)).ok)
|
|
5520
|
+
return;
|
|
5521
|
+
} else if ((stat.mode & 18) !== 0)
|
|
5315
5522
|
return;
|
|
5316
5523
|
if (uid !== undefined && stat.uid !== uid)
|
|
5317
5524
|
return;
|
|
@@ -5422,7 +5629,23 @@ function nodeConfigPath(env = process.env, home = homedir2()) {
|
|
|
5422
5629
|
var isObject3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
5423
5630
|
var whole = (v, min, max) => typeof v === "number" && Number.isInteger(v) && v >= min && v <= max;
|
|
5424
5631
|
var show2 = (v) => JSON.stringify(v)?.slice(0, 80) ?? String(v);
|
|
5425
|
-
function
|
|
5632
|
+
function configPathOf(raw, options) {
|
|
5633
|
+
if ((options.platform ?? process.platform) !== "win32")
|
|
5634
|
+
return isAbsolutePath2(raw) ? raw : undefined;
|
|
5635
|
+
if (typeof raw !== "string")
|
|
5636
|
+
return;
|
|
5637
|
+
let native = raw;
|
|
5638
|
+
if (/^~[\\/]/.test(raw)) {
|
|
5639
|
+
const home = canonicalNativePath(options.home ?? homedir2());
|
|
5640
|
+
if (!home || !isWindowsAbsolutePath(home))
|
|
5641
|
+
return;
|
|
5642
|
+
native = `${home}/${raw.slice(2)}`;
|
|
5643
|
+
}
|
|
5644
|
+
const canonical = canonicalNativePath(native);
|
|
5645
|
+
return canonical && isWindowsAbsolutePath(canonical) ? canonical : undefined;
|
|
5646
|
+
}
|
|
5647
|
+
var isRoot = (p) => p === "/" || /^[A-Z]:\/$/.test(p);
|
|
5648
|
+
function normalizeNodeConfig(raw, options = {}) {
|
|
5426
5649
|
const problems = [];
|
|
5427
5650
|
let r = {};
|
|
5428
5651
|
if (isObject3(raw))
|
|
@@ -5432,9 +5655,10 @@ function normalizeNodeConfig(raw) {
|
|
|
5432
5655
|
const workspaceRoots = [];
|
|
5433
5656
|
if (r.workspaceRoots !== undefined && !Array.isArray(r.workspaceRoots))
|
|
5434
5657
|
problems.push("workspaceRoots must be a list of absolute paths");
|
|
5435
|
-
for (const
|
|
5436
|
-
|
|
5437
|
-
|
|
5658
|
+
for (const entry of Array.isArray(r.workspaceRoots) ? r.workspaceRoots : []) {
|
|
5659
|
+
const root = configPathOf(entry, options);
|
|
5660
|
+
if (root === undefined || isRoot(root)) {
|
|
5661
|
+
problems.push(`workspaceRoots: ${show2(entry)} is not an absolute path (without . or .. segments, and not /)`);
|
|
5438
5662
|
continue;
|
|
5439
5663
|
}
|
|
5440
5664
|
const clean = root.replace(/\/+$/, "");
|
|
@@ -5469,21 +5693,22 @@ function normalizeNodeConfig(raw) {
|
|
|
5469
5693
|
}
|
|
5470
5694
|
const openshell = r.openshell === undefined ? undefined : normalizeOpenShell(r.openshell, problems);
|
|
5471
5695
|
const harnesses = r.harnesses === undefined ? undefined : normalizeHarnesses(r.harnesses, problems);
|
|
5472
|
-
const opencode = r.opencode === undefined ? undefined : normalizeOpenCode(r.opencode, problems);
|
|
5696
|
+
const opencode = r.opencode === undefined ? undefined : normalizeOpenCode(r.opencode, problems, options);
|
|
5473
5697
|
return {
|
|
5474
5698
|
config: { workspaceRoots, labels, slots, ...memoryMB !== undefined ? { memoryMB } : {}, ...openshell ? { openshell } : {}, ...harnesses ? { harnesses } : {}, ...opencode ? { opencode } : {} },
|
|
5475
5699
|
problems
|
|
5476
5700
|
};
|
|
5477
5701
|
}
|
|
5478
|
-
function normalizeDirectories(raw, field, problems) {
|
|
5702
|
+
function normalizeDirectories(raw, field, problems, options) {
|
|
5479
5703
|
if (!Array.isArray(raw)) {
|
|
5480
5704
|
problems.push(`${field} must be a list of absolute paths`);
|
|
5481
5705
|
return;
|
|
5482
5706
|
}
|
|
5483
5707
|
const out = [];
|
|
5484
|
-
for (const
|
|
5485
|
-
|
|
5486
|
-
|
|
5708
|
+
for (const entry of raw) {
|
|
5709
|
+
const dir = configPathOf(entry, options);
|
|
5710
|
+
if (dir === undefined || isRoot(dir)) {
|
|
5711
|
+
problems.push(`${field}: ${show2(entry)} is not an absolute path (without . or .. segments, and not /)`);
|
|
5487
5712
|
continue;
|
|
5488
5713
|
}
|
|
5489
5714
|
const clean = dir.replace(/\/+$/, "");
|
|
@@ -5514,12 +5739,12 @@ function loopbackOrigin(raw) {
|
|
|
5514
5739
|
return;
|
|
5515
5740
|
return url.origin;
|
|
5516
5741
|
}
|
|
5517
|
-
function normalizeOpenCode(raw, problems) {
|
|
5742
|
+
function normalizeOpenCode(raw, problems, options) {
|
|
5518
5743
|
if (!isObject3(raw)) {
|
|
5519
5744
|
problems.push("opencode must be an object");
|
|
5520
5745
|
return;
|
|
5521
5746
|
}
|
|
5522
|
-
const directories = raw.directories === undefined ? undefined : normalizeDirectories(raw.directories, "opencode.directories", problems);
|
|
5747
|
+
const directories = raw.directories === undefined ? undefined : normalizeDirectories(raw.directories, "opencode.directories", problems, options);
|
|
5523
5748
|
const servers = [];
|
|
5524
5749
|
if (raw.servers !== undefined && !Array.isArray(raw.servers))
|
|
5525
5750
|
problems.push("opencode.servers must be a list of {url, passwordFile, directories?}");
|
|
@@ -5531,12 +5756,13 @@ function normalizeOpenCode(raw, problems) {
|
|
|
5531
5756
|
const url = loopbackOrigin(entry.url);
|
|
5532
5757
|
if (!url)
|
|
5533
5758
|
return void problems.push(`${field}.url must be an http(s) URL on 127.0.0.1, [::1] or localhost, without user info, path or query`);
|
|
5534
|
-
|
|
5759
|
+
const passwordFile = configPathOf(entry.passwordFile, options);
|
|
5760
|
+
if (passwordFile === undefined || isRoot(passwordFile))
|
|
5535
5761
|
return void problems.push(`${field}.passwordFile must be the absolute path of a private (chmod 600) file holding the server's password`);
|
|
5536
5762
|
if (servers.some((s) => s.url === url))
|
|
5537
5763
|
return void problems.push(`${field}: ${url} is listed twice; the first one is used`);
|
|
5538
|
-
const dirs = entry.directories === undefined ? undefined : normalizeDirectories(entry.directories, `${field}.directories`, problems);
|
|
5539
|
-
servers.push({ url, passwordFile
|
|
5764
|
+
const dirs = entry.directories === undefined ? undefined : normalizeDirectories(entry.directories, `${field}.directories`, problems, options);
|
|
5765
|
+
servers.push({ url, passwordFile, ...dirs ? { directories: dirs } : {} });
|
|
5540
5766
|
});
|
|
5541
5767
|
if (servers.length > OPENCODE_SERVERS_MAX) {
|
|
5542
5768
|
problems.push(`opencode.servers: at most ${OPENCODE_SERVERS_MAX} servers; the rest are ignored`);
|
|
@@ -5598,7 +5824,7 @@ function normalizeOpenShell(raw, problems) {
|
|
|
5598
5824
|
problems.push(...profiles.problems);
|
|
5599
5825
|
return { enabled, ...gateway ? { gateway } : {}, ...image ? { image } : {}, profiles };
|
|
5600
5826
|
}
|
|
5601
|
-
function readNodeConfig(file) {
|
|
5827
|
+
function readNodeConfig(file, options = {}) {
|
|
5602
5828
|
let text;
|
|
5603
5829
|
try {
|
|
5604
5830
|
text = readFileSync4(file, "utf8");
|
|
@@ -5608,7 +5834,7 @@ function readNodeConfig(file) {
|
|
|
5608
5834
|
return { config: normalizeNodeConfig(undefined).config, problems: [`${file}: cannot read (${cause.message})`] };
|
|
5609
5835
|
}
|
|
5610
5836
|
try {
|
|
5611
|
-
return normalizeNodeConfig(JSON.parse(text));
|
|
5837
|
+
return normalizeNodeConfig(JSON.parse(text), options);
|
|
5612
5838
|
} catch {
|
|
5613
5839
|
return { config: normalizeNodeConfig(undefined).config, problems: [`${file}: not valid JSON`] };
|
|
5614
5840
|
}
|
|
@@ -5739,6 +5965,14 @@ function durationArg(value, flag) {
|
|
|
5739
5965
|
throw new UsageError(`${flag} takes seconds or a duration like 30s, 5m, 2h, 1d (got "${value}")`);
|
|
5740
5966
|
return Number(match[1]) * UNIT_S[match[2]];
|
|
5741
5967
|
}
|
|
5968
|
+
var ISO_DATE_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:?\d{2})?$/i;
|
|
5969
|
+
function notAfterArg(value, what) {
|
|
5970
|
+
const text = value.trim();
|
|
5971
|
+
const ms = /^\d+$/.test(text) ? Number(text) : ISO_DATE_TIME.test(text) ? Date.parse(text) : Number.NaN;
|
|
5972
|
+
if (!Number.isSafeInteger(ms))
|
|
5973
|
+
throw new UsageError(`${what} takes epoch milliseconds or an ISO 8601 date-time like 2027-01-15T12:00:00Z (got "${value}")`);
|
|
5974
|
+
return ms;
|
|
5975
|
+
}
|
|
5742
5976
|
var iso = (ms) => new Date(ms).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
5743
5977
|
function askOutcome(asked, waited) {
|
|
5744
5978
|
const { message, answer } = asked;
|
|
@@ -5911,7 +6145,8 @@ var LAUNCH_FLAGS = {
|
|
|
5911
6145
|
prompt: "string",
|
|
5912
6146
|
end: "string",
|
|
5913
6147
|
timeout: "string",
|
|
5914
|
-
cleanup: "string"
|
|
6148
|
+
cleanup: "string",
|
|
6149
|
+
"not-after": "string"
|
|
5915
6150
|
};
|
|
5916
6151
|
var choices = (options) => options.length === 2 ? options.join(" or ") : `${options.slice(0, -1).join(", ")} or ${options.at(-1)}`;
|
|
5917
6152
|
function oneOf3(flags, flag, options) {
|
|
@@ -5970,10 +6205,12 @@ async function launchSpecArg(flags, context) {
|
|
|
5970
6205
|
const runtime = kind === undefined ? undefined : { kind, ...profile !== undefined ? { profile } : {} };
|
|
5971
6206
|
const placement = defined({ labels: nodeLabels, machine, mode: flags.prefer === true ? "prefer" : undefined });
|
|
5972
6207
|
const end = defined({ when, timeout: str(flags, "timeout"), cleanup });
|
|
6208
|
+
const notAfterText = str(flags, "not-after");
|
|
6209
|
+
const notAfter = notAfterText === undefined ? undefined : notAfterArg(notAfterText, "--not-after");
|
|
5973
6210
|
return {
|
|
5974
6211
|
...defined({ key: str(flags, "key"), name, roles, harness: str(flags, "harness"), agent: str(flags, "agent"), model: str(flags, "model") }),
|
|
5975
6212
|
workspace,
|
|
5976
|
-
...defined({ runtime, placement, prompt, labels, end })
|
|
6213
|
+
...defined({ runtime, placement, prompt, labels, end, notAfter })
|
|
5977
6214
|
};
|
|
5978
6215
|
}
|
|
5979
6216
|
var at2 = (name) => `@${name.replace(/^@+/, "")}`;
|
|
@@ -6432,6 +6669,23 @@ function dueOf(args) {
|
|
|
6432
6669
|
throw new ToolArgError(`"due" must be an ISO date/time or epoch milliseconds`);
|
|
6433
6670
|
return ms;
|
|
6434
6671
|
}
|
|
6672
|
+
function notAfterOf(args) {
|
|
6673
|
+
const value = args.notAfter;
|
|
6674
|
+
if (value === undefined || value === null || value === "")
|
|
6675
|
+
return;
|
|
6676
|
+
if (typeof value === "number" && Number.isSafeInteger(value))
|
|
6677
|
+
return value;
|
|
6678
|
+
if (typeof value === "string") {
|
|
6679
|
+
try {
|
|
6680
|
+
return notAfterArg(value, `"notAfter"`);
|
|
6681
|
+
} catch (cause) {
|
|
6682
|
+
if (cause instanceof UsageError)
|
|
6683
|
+
throw new ToolArgError(cause.message);
|
|
6684
|
+
throw cause;
|
|
6685
|
+
}
|
|
6686
|
+
}
|
|
6687
|
+
throw new ToolArgError(`"notAfter" must be epoch milliseconds or an ISO 8601 date-time like 2027-01-15T12:00:00Z`);
|
|
6688
|
+
}
|
|
6435
6689
|
var line4 = (i) => `${i.id} ${i.status}${i.progress ? `/${i.progress}` : ""} ${i.priority} r${i.revision} ${sanitizeText(i.title).replace(/\s+/g, " ")}`;
|
|
6436
6690
|
function intakeText(i) {
|
|
6437
6691
|
const from = i.source.kind === "user" ? "the user" : i.source.kind === "app" ? `app ${i.source.tokenID}` : `session ${i.source.sessionID}`;
|
|
@@ -6441,6 +6695,7 @@ function intakeText(i) {
|
|
|
6441
6695
|
`status: ${i.status}${i.progress ? ` (${i.progress})` : ""} priority: ${i.priority} from: ${from}`,
|
|
6442
6696
|
...i.assignmentID ? [`assignment: ${i.assignmentID} in workspace ${i.workspaceID ?? "?"}${i.sessionID ? `, delivered to ${i.sessionID}` : ""}`] : [],
|
|
6443
6697
|
...i.due !== undefined ? [`due: ${new Date(i.due).toISOString()}`] : [],
|
|
6698
|
+
...i.notAfter !== undefined ? [`not after: ${new Date(i.notAfter).toISOString()}`] : [],
|
|
6444
6699
|
...i.labels?.length ? [`labels: ${i.labels.join(", ")}`] : [],
|
|
6445
6700
|
...i.criteria?.length ? ["done means:", ...i.criteria.map((c) => `- ${sanitizeText(c)}`)] : [],
|
|
6446
6701
|
...i.ref ? [`ref: ${sanitizeText(i.ref)}`] : [],
|
|
@@ -6452,7 +6707,7 @@ function intakeText(i) {
|
|
|
6452
6707
|
var teamIntakeTools = [
|
|
6453
6708
|
{
|
|
6454
6709
|
name: "team_submit",
|
|
6455
|
-
description: "Give work to a team: its entry role (e.g. the project manager) receives the task with the team's context and owns it as an assignment in the team's workspace; while the role is vacant it waits queued. Pass `key` to make retries idempotent. App tokens need scope teams:submit; sessions submit under the team's communication policy.",
|
|
6710
|
+
description: "Give work to a team: its entry role (e.g. the project manager) receives the task with the team's context and owns it as an assignment in the team's workspace; while the role is vacant it waits queued (with `notAfter`, only until then: still queued at that time, it is cancelled with reason not_after instead of delivered late). Pass `key` to make retries idempotent. App tokens need scope teams:submit; sessions submit under the team's communication policy.",
|
|
6456
6711
|
properties: {
|
|
6457
6712
|
team: TEAM,
|
|
6458
6713
|
title: { ...STRING3, description: "One line, at most 120 characters." },
|
|
@@ -6460,6 +6715,10 @@ var teamIntakeTools = [
|
|
|
6460
6715
|
priority: { type: "string", enum: PRIORITIES, description: "Default normal." },
|
|
6461
6716
|
labels: { type: "array", items: STRING3, description: "Slugs (lowercase letters, digits, dashes)." },
|
|
6462
6717
|
due: { ...STRING3, description: "When it is due: an ISO date/time." },
|
|
6718
|
+
notAfter: {
|
|
6719
|
+
type: ["number", "string"],
|
|
6720
|
+
description: "Never deliver after this time: epoch milliseconds or an ISO 8601 date-time (at most 30 days ahead). Already passed: nothing is submitted (expired). Still queued then: cancelled (reason not_after)."
|
|
6721
|
+
},
|
|
6463
6722
|
criteria: { type: "array", items: STRING3, description: "What done means (at most 8 lines)." },
|
|
6464
6723
|
ref: { ...STRING3, description: "An external reference, e.g. a Brain task ID." },
|
|
6465
6724
|
key: { ...STRING3, description: "Idempotency-Key: the same key and body return the original submission." }
|
|
@@ -6472,10 +6731,11 @@ var teamIntakeTools = [
|
|
|
6472
6731
|
const labels = strings2(args, "labels");
|
|
6473
6732
|
const criteria = strings2(args, "criteria");
|
|
6474
6733
|
const due = dueOf(args);
|
|
6734
|
+
const notAfter = notAfterOf(args);
|
|
6475
6735
|
const brief = optional4(args, "brief");
|
|
6476
6736
|
const ref = optional4(args, "ref");
|
|
6477
6737
|
const key = optional4(args, "key");
|
|
6478
|
-
const { intake, created } = await hub.teams.submit(need3(args, "team"), { title: need3(args, "title"), ...brief ? { brief } : {}, ...priority ? { priority } : {}, ...labels ? { labels } : {}, ...due !== undefined ? { due } : {}, ...criteria ? { criteria } : {}, ...ref ? { ref } : {} }, { signal, ...key ? { idempotencyKey: key } : {} });
|
|
6738
|
+
const { intake, created } = await hub.teams.submit(need3(args, "team"), { title: need3(args, "title"), ...brief ? { brief } : {}, ...priority ? { priority } : {}, ...labels ? { labels } : {}, ...due !== undefined ? { due } : {}, ...notAfter !== undefined ? { notAfter } : {}, ...criteria ? { criteria } : {}, ...ref ? { ref } : {} }, { signal, ...key ? { idempotencyKey: key } : {} });
|
|
6479
6739
|
return `${created ? "Submitted" : "Already submitted (same key)"}: ${intake.id} is ${intake.status}${intake.status === "queued" ? " (waiting for the entry role)" : ""}
|
|
6480
6740
|
${intakeText(intake)}`;
|
|
6481
6741
|
}
|
|
@@ -6641,7 +6901,7 @@ var revision = (args) => {
|
|
|
6641
6901
|
throw new ArgError(`"expectedRevision" is required: the team's revision you read (team_show)`);
|
|
6642
6902
|
return value;
|
|
6643
6903
|
};
|
|
6644
|
-
var
|
|
6904
|
+
var int3 = (args, key, min, max) => {
|
|
6645
6905
|
const value = args[key];
|
|
6646
6906
|
if (value === undefined || value === null)
|
|
6647
6907
|
return;
|
|
@@ -6748,7 +7008,7 @@ var teamTools = [
|
|
|
6748
7008
|
const tags = strings3(args, "tags");
|
|
6749
7009
|
const project = optional5(args, "project");
|
|
6750
7010
|
const metadata = record(args, "metadata");
|
|
6751
|
-
const limit =
|
|
7011
|
+
const limit = int3(args, "limit", 1, 200);
|
|
6752
7012
|
const cursor = optional5(args, "cursor");
|
|
6753
7013
|
if (args.paused !== undefined && typeof args.paused !== "boolean")
|
|
6754
7014
|
throw new ArgError(`"paused" must be true or false`);
|
|
@@ -6815,8 +7075,8 @@ var teamTools = [
|
|
|
6815
7075
|
required: ["team"],
|
|
6816
7076
|
readOnly: true,
|
|
6817
7077
|
run: async (hub, args, signal) => {
|
|
6818
|
-
const from =
|
|
6819
|
-
const to = args.to === undefined || args.to === null || args.to === "draft" ? "draft" :
|
|
7078
|
+
const from = int3(args, "from", 1, 1e9);
|
|
7079
|
+
const to = args.to === undefined || args.to === null || args.to === "draft" ? "draft" : int3(args, "to", 1, 1e9);
|
|
6820
7080
|
const { from: a, to: b, diff } = await hub.teams.diff(need4(args, "team"), { signal, ...from !== undefined ? { from } : {}, to });
|
|
6821
7081
|
const part = (label, d) => ["added", "removed", "changed"].flatMap((k) => d[k]?.length ? [`${label} ${k}: ${d[k].join(", ")}`] : []);
|
|
6822
7082
|
const lines = [
|
|
@@ -7387,10 +7647,28 @@ var optionalExpires = (args) => {
|
|
|
7387
7647
|
}
|
|
7388
7648
|
throw new ArgError3(`"expires" must be seconds or a duration like 30s, 5m, 2h, 1d`);
|
|
7389
7649
|
};
|
|
7650
|
+
var optionalNotAfter = (args) => {
|
|
7651
|
+
const value = args.notAfter;
|
|
7652
|
+
if (value === undefined || value === null)
|
|
7653
|
+
return;
|
|
7654
|
+
if (typeof value === "number" && Number.isSafeInteger(value))
|
|
7655
|
+
return value;
|
|
7656
|
+
if (typeof value === "string") {
|
|
7657
|
+
try {
|
|
7658
|
+
return notAfterArg(value, '"notAfter"');
|
|
7659
|
+
} catch (cause) {
|
|
7660
|
+
if (cause instanceof UsageError)
|
|
7661
|
+
throw new ArgError3(cause.message);
|
|
7662
|
+
throw cause;
|
|
7663
|
+
}
|
|
7664
|
+
}
|
|
7665
|
+
throw new ArgError3(`"notAfter" must be epoch milliseconds or an ISO 8601 date-time like 2027-01-15T12:00:00Z`);
|
|
7666
|
+
};
|
|
7390
7667
|
var sendOptions = (args) => {
|
|
7391
7668
|
const wake = optionalBool(args, "wake");
|
|
7392
7669
|
const urgent = optionalBool(args, "urgent");
|
|
7393
|
-
|
|
7670
|
+
const notAfter = optionalNotAfter(args);
|
|
7671
|
+
return { ...wake !== undefined ? { wake } : {}, ...urgent !== undefined ? { urgent } : {}, ...notAfter !== undefined ? { notAfter } : {} };
|
|
7394
7672
|
};
|
|
7395
7673
|
var whyOption = (args) => {
|
|
7396
7674
|
const why = optional7(args, "why");
|
|
@@ -7464,7 +7742,8 @@ function launchSpec(args) {
|
|
|
7464
7742
|
placement: definedFields({ labels: nodeLabels, machine, mode: prefer ? "prefer" : undefined }),
|
|
7465
7743
|
prompt: optional7(args, "prompt"),
|
|
7466
7744
|
labels: optionalLabels(args, "labels"),
|
|
7467
|
-
end: definedFields({ when: optionalEnum(args, "end", LAUNCH_END_WHEN), timeout: optional7(args, "timeout"), cleanup: optionalEnum(args, "cleanup", CLEANUP_POLICIES) })
|
|
7745
|
+
end: definedFields({ when: optionalEnum(args, "end", LAUNCH_END_WHEN), timeout: optional7(args, "timeout"), cleanup: optionalEnum(args, "cleanup", CLEANUP_POLICIES) }),
|
|
7746
|
+
notAfter: optionalNotAfter(args)
|
|
7468
7747
|
})
|
|
7469
7748
|
};
|
|
7470
7749
|
}
|
|
@@ -7514,6 +7793,7 @@ var STRING6 = { type: "string" };
|
|
|
7514
7793
|
var SEND = {
|
|
7515
7794
|
wake: { type: "boolean", description: "false: admit the message without starting a turn." },
|
|
7516
7795
|
urgent: { type: "boolean", description: "Steer into the recipient's running turn." },
|
|
7796
|
+
notAfter: { type: ["number", "string"], description: "Never deliver after this time: epoch milliseconds or an ISO 8601 date-time (at most 30 days ahead). Already passed: nothing is sent (expired)." },
|
|
7517
7797
|
all: { type: "boolean", description: "Send to every session the address names (each holder of a role)." }
|
|
7518
7798
|
};
|
|
7519
7799
|
var TO = { type: "string", description: "The recipient: @name, @@role, @@role#team (a role of one of your teams), @name.project, ses_…, or @me (the user)." };
|
|
@@ -7671,7 +7951,7 @@ var webhookText = (w) => `${w.id}${w.label ? ` ${w.label}` : ""} ${w.url} ${w
|
|
|
7671
7951
|
var webhookTools = [
|
|
7672
7952
|
{
|
|
7673
7953
|
name: "webhook_create",
|
|
7674
|
-
description:
|
|
7954
|
+
description: `Create a durable signed webhook for canonical compact events (e.g. launch.*, session.*, message.changed, team.intake). The signing secret is returned only here. Needs events:subscribe plus every selected family's scope; events ["team.intake"] alone needs only teams:submit (an app gets only its own submissions).`,
|
|
7675
7955
|
properties: { url: STRING6, events: { type: "array", items: STRING6 }, label: STRING6 },
|
|
7676
7956
|
required: ["url"],
|
|
7677
7957
|
readOnly: false,
|
|
@@ -8188,13 +8468,13 @@ var TOOLS = [
|
|
|
8188
8468
|
{
|
|
8189
8469
|
name: "msg",
|
|
8190
8470
|
description: "Tell a session something (fire and forget). Sent as this server's credential, not as an OpenCode session.",
|
|
8191
|
-
properties: { to: TO, text: { ...STRING6, description: "The message." }, ...SEND },
|
|
8471
|
+
properties: { to: TO, text: { ...STRING6, description: "The message." }, ...SEND, idempotencyKey: IDEMPOTENCY3 },
|
|
8192
8472
|
required: ["to", "text"],
|
|
8193
8473
|
readOnly: false,
|
|
8194
8474
|
run: async (hub, args, signal) => {
|
|
8195
8475
|
const to = need7(args, "to");
|
|
8196
8476
|
const text = need7(args, "text");
|
|
8197
|
-
const options = { ...sendOptions(args), signal };
|
|
8477
|
+
const options = { ...sendOptions(args), ...idemOption(args), signal };
|
|
8198
8478
|
if (optionalBool(args, "all"))
|
|
8199
8479
|
return sentAllText(await hub.msg(to, text, { ...options, all: true }));
|
|
8200
8480
|
return sentText(await hub.msg(to, text, options));
|
|
@@ -8208,7 +8488,8 @@ var TOOLS = [
|
|
|
8208
8488
|
text: { ...STRING6, description: "The question." },
|
|
8209
8489
|
wait: { type: "number", minimum: 0, maximum: WAIT_POLL_MAX_S, description: `Seconds to wait for the answer (0–${WAIT_POLL_MAX_S}).` },
|
|
8210
8490
|
expires: { type: ["number", "string"], description: "When the ask expires: seconds, or 30s, 5m, 2h, 1d (1 minute to 7 days; default 24 h)." },
|
|
8211
|
-
...SEND
|
|
8491
|
+
...SEND,
|
|
8492
|
+
idempotencyKey: IDEMPOTENCY3
|
|
8212
8493
|
},
|
|
8213
8494
|
required: ["to", "text"],
|
|
8214
8495
|
readOnly: false,
|
|
@@ -8217,7 +8498,7 @@ var TOOLS = [
|
|
|
8217
8498
|
const text = need7(args, "text");
|
|
8218
8499
|
const wait = optionalWait(args);
|
|
8219
8500
|
const expires = optionalExpires(args);
|
|
8220
|
-
const options = { ...sendOptions(args), ...expires !== undefined ? { expires } : {}, signal };
|
|
8501
|
+
const options = { ...sendOptions(args), ...expires !== undefined ? { expires } : {}, ...idemOption(args), signal };
|
|
8221
8502
|
if (optionalBool(args, "all")) {
|
|
8222
8503
|
if (wait !== undefined)
|
|
8223
8504
|
throw new ArgError3(`"wait" cannot be combined with "all"`);
|
|
@@ -8381,7 +8662,7 @@ ${decisionText((await hub.decision(need7(args, "id"), { signal })).decision)}`
|
|
|
8381
8662
|
},
|
|
8382
8663
|
{
|
|
8383
8664
|
name: "launch",
|
|
8384
|
-
description: "Start a new session on a node (a hub node with the repo under its workspace roots). The same key again answers the same launch. Needs an app token with scope sessions:launch and a grant with verb launch covering the repo.",
|
|
8665
|
+
description: "Start a new session on a node (a hub node with the repo under its workspace roots). The same key again answers the same launch. With notAfter, a launch still waiting for a machine at that time is never started (it ends expired). Needs an app token with scope sessions:launch and a grant with verb launch covering the repo.",
|
|
8385
8666
|
properties: {
|
|
8386
8667
|
repo: { ...STRING6, description: "Absolute path of the repository on the node (or where a clone goes)." },
|
|
8387
8668
|
mode: { type: "string", enum: [...WORKSPACE_MODES], description: "worktree (default: <repo>/.worktrees/<branch>, needs branch), checkout, or clone (needs remote)." },
|
|
@@ -8403,7 +8684,8 @@ ${decisionText((await hub.decision(need7(args, "id"), { signal })).decision)}`
|
|
|
8403
8684
|
prompt: { ...STRING6, description: "The session's first prompt." },
|
|
8404
8685
|
end: { type: "string", enum: [...LAUNCH_END_WHEN], description: "When it ends: idle (default; idle with nothing open for 30 s), asked (with a question open: needs_user), never." },
|
|
8405
8686
|
timeout: { ...STRING6, description: "Longest run, e.g. 4h or 90m." },
|
|
8406
|
-
cleanup: { type: "string", enum: [...CLEANUP_POLICIES], description: "keep (default) or remove the workspace when it ends." }
|
|
8687
|
+
cleanup: { type: "string", enum: [...CLEANUP_POLICIES], description: "keep (default) or remove the workspace when it ends." },
|
|
8688
|
+
notAfter: { type: ["number", "string"], description: "Do not start after this time: epoch milliseconds or an ISO 8601 date-time (at most 30 days ahead). Still waiting for a machine then, the launch ends expired; once placed it runs (timeout bounds it). Already passed: nothing is launched (expired)." }
|
|
8407
8689
|
},
|
|
8408
8690
|
required: ["repo"],
|
|
8409
8691
|
readOnly: false,
|
|
@@ -8721,7 +9003,7 @@ function grantText2(g) {
|
|
|
8721
9003
|
var requestedText = (body) => `requested; waiting for the user (Decision ${body.decision.id})`;
|
|
8722
9004
|
|
|
8723
9005
|
// packages/cli/src/node.ts
|
|
8724
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync7, readFileSync as readFileSync8, realpathSync as realpathSync3, rmSync as rmSync8, writeFileSync as writeFileSync4 } from "node:fs";
|
|
9006
|
+
import { appendFileSync, existsSync as existsSync5, mkdirSync as mkdirSync7, readFileSync as readFileSync8, realpathSync as realpathSync3, renameSync as renameSync5, rmSync as rmSync8, statSync as statSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
8725
9007
|
import path16 from "node:path";
|
|
8726
9008
|
// packages/node/src/connection.ts
|
|
8727
9009
|
var UNSUPPORTED_RETRY_MS = 30 * 60000;
|
|
@@ -8945,7 +9227,7 @@ import { arch as osArch, platform as osPlatform } from "node:os";
|
|
|
8945
9227
|
// packages/node/package.json
|
|
8946
9228
|
var package_default2 = {
|
|
8947
9229
|
name: "@huynle/hub-node",
|
|
8948
|
-
version: "0.
|
|
9230
|
+
version: "0.9.1",
|
|
8949
9231
|
private: true,
|
|
8950
9232
|
description: "`hub node`: the per-machine daemon that prepares, collects and cleans up launch workspaces (internal; shipped inside @huynle/hub, not published on its own).",
|
|
8951
9233
|
license: "MIT",
|
|
@@ -8962,46 +9244,251 @@ import path11 from "node:path";
|
|
|
8962
9244
|
|
|
8963
9245
|
// packages/node/src/git.ts
|
|
8964
9246
|
import { spawn } from "node:child_process";
|
|
8965
|
-
var ALLOWED_PROTOCOLS = "https:ssh";
|
|
8966
|
-
var GIT_TIMEOUT_MS = 120000;
|
|
8967
|
-
var MAX_BUFFER = 16 * 1024 * 1024;
|
|
8968
|
-
var STDERR_MAX = 600;
|
|
8969
|
-
var KEPT_GIT_VARS = new Set(["GIT_SSH", "GIT_SSH_COMMAND", "GIT_SSH_VARIANT"]);
|
|
8970
9247
|
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
8974
|
-
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
|
|
8979
|
-
|
|
8980
|
-
|
|
8981
|
-
|
|
8982
|
-
|
|
8983
|
-
|
|
8984
|
-
|
|
8985
|
-
if (value !== undefined)
|
|
8986
|
-
env[name] = value;
|
|
8987
|
-
return env;
|
|
8988
|
-
}
|
|
8989
|
-
function redact(text) {
|
|
8990
|
-
return text.replace(/([a-z][a-z0-9+.-]*:\/\/)[^\s/@]+@/gi, "$1***@");
|
|
9248
|
+
// packages/cli/src/bootstrap/adapters/process-tree.ts
|
|
9249
|
+
import { execFile, spawn as nodeSpawn } from "node:child_process";
|
|
9250
|
+
var TOOL_TIMEOUT_MS = 15000;
|
|
9251
|
+
var START_SLACK_MS = 2000;
|
|
9252
|
+
var FILETIME_EPOCH_MS = 11644473600000n;
|
|
9253
|
+
var defaultExec = (file, args) => new Promise((resolve) => {
|
|
9254
|
+
execFile(file, [...args], { shell: false, windowsHide: true, timeout: TOOL_TIMEOUT_MS, encoding: "utf8", maxBuffer: 1 << 20 }, (error, stdout) => {
|
|
9255
|
+
const code = error?.code;
|
|
9256
|
+
resolve({ code: error ? typeof code === "number" ? code : null : 0, stdout: String(stdout ?? "") });
|
|
9257
|
+
});
|
|
9258
|
+
});
|
|
9259
|
+
function systemRootOf(value) {
|
|
9260
|
+
const v = value?.replace(/\\+$/, "");
|
|
9261
|
+
return v && /^[A-Za-z]:\\[A-Za-z0-9 ._\\()-]*$/.test(v) ? v : "C:\\Windows";
|
|
8991
9262
|
}
|
|
8992
|
-
function
|
|
8993
|
-
const
|
|
8994
|
-
|
|
8995
|
-
|
|
8996
|
-
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
const
|
|
9000
|
-
if (
|
|
9001
|
-
return;
|
|
9002
|
-
|
|
9003
|
-
|
|
9004
|
-
|
|
9263
|
+
function processTree(deps = {}) {
|
|
9264
|
+
const platform = deps.platform ?? process.platform;
|
|
9265
|
+
if (platform === "win32") {
|
|
9266
|
+
const exec = deps.exec ?? defaultExec;
|
|
9267
|
+
const root = systemRootOf(deps.systemRoot ?? process.env.SystemRoot ?? process.env.SYSTEMROOT);
|
|
9268
|
+
const taskkill = `${root}\\System32\\taskkill.exe`;
|
|
9269
|
+
const powershell = `${root}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`;
|
|
9270
|
+
const killTree = async (pid) => {
|
|
9271
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
9272
|
+
return;
|
|
9273
|
+
try {
|
|
9274
|
+
await exec(taskkill, ["/T", "/F", "/PID", String(pid)]);
|
|
9275
|
+
} catch {}
|
|
9276
|
+
};
|
|
9277
|
+
return {
|
|
9278
|
+
detached: false,
|
|
9279
|
+
killTree,
|
|
9280
|
+
async reap(pid, startedAt) {
|
|
9281
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
9282
|
+
return;
|
|
9283
|
+
const since = (BigInt(Math.max(0, Math.floor(startedAt) - START_SLACK_MS)) + FILETIME_EPOCH_MS) * 10000n;
|
|
9284
|
+
const script = `$t=[DateTime]::FromFileTimeUtc(${since}); ` + `Get-CimInstance -ClassName Win32_Process -Filter 'ParentProcessId=${pid}' | ` + `Where-Object { $_.CreationDate.ToUniversalTime() -ge $t } | ForEach-Object { $_.ProcessId }`;
|
|
9285
|
+
let stdout = "";
|
|
9286
|
+
try {
|
|
9287
|
+
stdout = (await exec(powershell, ["-NoProfile", "-NonInteractive", "-Command", script])).stdout;
|
|
9288
|
+
} catch {
|
|
9289
|
+
return;
|
|
9290
|
+
}
|
|
9291
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
9292
|
+
const child = line.trim();
|
|
9293
|
+
if (/^[1-9]\d*$/.test(child))
|
|
9294
|
+
await killTree(Number(child));
|
|
9295
|
+
}
|
|
9296
|
+
}
|
|
9297
|
+
};
|
|
9298
|
+
}
|
|
9299
|
+
const kill = deps.kill ?? ((pid, signal) => void process.kill(pid, signal));
|
|
9300
|
+
const group = (pid) => {
|
|
9301
|
+
try {
|
|
9302
|
+
kill(-pid, "SIGKILL");
|
|
9303
|
+
return true;
|
|
9304
|
+
} catch {
|
|
9305
|
+
return false;
|
|
9306
|
+
}
|
|
9307
|
+
};
|
|
9308
|
+
return {
|
|
9309
|
+
detached: true,
|
|
9310
|
+
async killTree(pid) {
|
|
9311
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
9312
|
+
return;
|
|
9313
|
+
if (group(pid))
|
|
9314
|
+
return;
|
|
9315
|
+
try {
|
|
9316
|
+
kill(pid, "SIGKILL");
|
|
9317
|
+
} catch {}
|
|
9318
|
+
},
|
|
9319
|
+
async reap(pid) {
|
|
9320
|
+
if (Number.isInteger(pid) && pid > 0)
|
|
9321
|
+
group(pid);
|
|
9322
|
+
}
|
|
9323
|
+
};
|
|
9324
|
+
}
|
|
9325
|
+
var DRAIN_GRACE_MS = 1000;
|
|
9326
|
+
function runBounded(request, options = {}) {
|
|
9327
|
+
const tree = options.tree ?? processTree();
|
|
9328
|
+
const spawn = options.spawn ?? nodeSpawn;
|
|
9329
|
+
const now = options.now ?? Date.now;
|
|
9330
|
+
const env = Object.fromEntries(Object.entries(request.env).filter((e) => typeof e[1] === "string"));
|
|
9331
|
+
return new Promise((resolve) => {
|
|
9332
|
+
let timedOut = false;
|
|
9333
|
+
let aborted = false;
|
|
9334
|
+
let overflowed = false;
|
|
9335
|
+
let ended = false;
|
|
9336
|
+
const out = [];
|
|
9337
|
+
const err = [];
|
|
9338
|
+
const size = { out: 0, err: 0 };
|
|
9339
|
+
const result = (exitCode, spawnError) => ({
|
|
9340
|
+
exitCode,
|
|
9341
|
+
stdout: Buffer.concat(out).toString("utf8"),
|
|
9342
|
+
stderr: Buffer.concat(err).toString("utf8"),
|
|
9343
|
+
timedOut,
|
|
9344
|
+
aborted,
|
|
9345
|
+
overflowed,
|
|
9346
|
+
...spawnError ? { spawnError } : {}
|
|
9347
|
+
});
|
|
9348
|
+
if (request.signal?.aborted) {
|
|
9349
|
+
aborted = true;
|
|
9350
|
+
return resolve(result(null));
|
|
9351
|
+
}
|
|
9352
|
+
const startedAt = now();
|
|
9353
|
+
let child;
|
|
9354
|
+
try {
|
|
9355
|
+
child = spawn(request.file, [...request.args], {
|
|
9356
|
+
shell: false,
|
|
9357
|
+
env,
|
|
9358
|
+
stdio: ["ignore", "pipe", request.stderr ? "pipe" : "ignore"],
|
|
9359
|
+
windowsHide: true,
|
|
9360
|
+
detached: tree.detached
|
|
9361
|
+
});
|
|
9362
|
+
} catch (error) {
|
|
9363
|
+
return resolve(result(null, error));
|
|
9364
|
+
}
|
|
9365
|
+
let settled = false;
|
|
9366
|
+
let exited = false;
|
|
9367
|
+
const pid = child.pid;
|
|
9368
|
+
const end = () => {
|
|
9369
|
+
if (ended || pid === undefined)
|
|
9370
|
+
return;
|
|
9371
|
+
ended = true;
|
|
9372
|
+
tree.killTree(pid);
|
|
9373
|
+
};
|
|
9374
|
+
const timer = setTimeout(() => {
|
|
9375
|
+
timedOut = true;
|
|
9376
|
+
end();
|
|
9377
|
+
}, request.timeoutMs);
|
|
9378
|
+
const onAbort = () => {
|
|
9379
|
+
aborted = true;
|
|
9380
|
+
end();
|
|
9381
|
+
};
|
|
9382
|
+
request.signal?.addEventListener("abort", onAbort, { once: true });
|
|
9383
|
+
const finish = (r) => {
|
|
9384
|
+
if (settled)
|
|
9385
|
+
return;
|
|
9386
|
+
settled = true;
|
|
9387
|
+
clearTimeout(timer);
|
|
9388
|
+
request.signal?.removeEventListener("abort", onAbort);
|
|
9389
|
+
resolve(r);
|
|
9390
|
+
};
|
|
9391
|
+
const collect = (into, key) => (chunk) => {
|
|
9392
|
+
const room = request.maxOutputBytes - size[key];
|
|
9393
|
+
if (chunk.length > room) {
|
|
9394
|
+
overflowed = true;
|
|
9395
|
+
if (room > 0)
|
|
9396
|
+
into.push(chunk.subarray(0, room));
|
|
9397
|
+
size[key] = request.maxOutputBytes;
|
|
9398
|
+
if ((request.overflow ?? "kill") === "kill")
|
|
9399
|
+
end();
|
|
9400
|
+
return;
|
|
9401
|
+
}
|
|
9402
|
+
into.push(chunk);
|
|
9403
|
+
size[key] += chunk.length;
|
|
9404
|
+
};
|
|
9405
|
+
child.stdout?.on("data", collect(out, "out"));
|
|
9406
|
+
child.stderr?.on("data", collect(err, "err"));
|
|
9407
|
+
const streams = [child.stdout, child.stderr].filter((s) => Boolean(s));
|
|
9408
|
+
const drained = () => new Promise((done) => {
|
|
9409
|
+
let open = streams.filter((s) => !s.readableEnded && !s.destroyed).length;
|
|
9410
|
+
if (open === 0)
|
|
9411
|
+
return done();
|
|
9412
|
+
const grace = setTimeout(() => {
|
|
9413
|
+
for (const s of streams)
|
|
9414
|
+
s.destroy();
|
|
9415
|
+
done();
|
|
9416
|
+
}, DRAIN_GRACE_MS);
|
|
9417
|
+
for (const s of streams) {
|
|
9418
|
+
if (s.readableEnded || s.destroyed)
|
|
9419
|
+
continue;
|
|
9420
|
+
const one = () => {
|
|
9421
|
+
if (--open === 0) {
|
|
9422
|
+
clearTimeout(grace);
|
|
9423
|
+
done();
|
|
9424
|
+
}
|
|
9425
|
+
};
|
|
9426
|
+
s.once("end", one);
|
|
9427
|
+
s.once("close", () => s.readableEnded ? undefined : one());
|
|
9428
|
+
}
|
|
9429
|
+
});
|
|
9430
|
+
child.on("error", (error) => {
|
|
9431
|
+
if (!exited)
|
|
9432
|
+
finish(result(null, error));
|
|
9433
|
+
});
|
|
9434
|
+
child.on("exit", (code) => {
|
|
9435
|
+
if (exited)
|
|
9436
|
+
return;
|
|
9437
|
+
exited = true;
|
|
9438
|
+
clearTimeout(timer);
|
|
9439
|
+
const exitCode = typeof code === "number" && !ended ? code : null;
|
|
9440
|
+
(async () => {
|
|
9441
|
+
if (pid !== undefined)
|
|
9442
|
+
await tree.reap(pid, startedAt);
|
|
9443
|
+
await drained();
|
|
9444
|
+
finish(result(exitCode));
|
|
9445
|
+
})();
|
|
9446
|
+
});
|
|
9447
|
+
});
|
|
9448
|
+
}
|
|
9449
|
+
|
|
9450
|
+
// packages/node/src/git.ts
|
|
9451
|
+
var ALLOWED_PROTOCOLS = "https:ssh";
|
|
9452
|
+
var GIT_TIMEOUT_MS = 120000;
|
|
9453
|
+
var MAX_BUFFER = 16 * 1024 * 1024;
|
|
9454
|
+
var STDERR_MAX = 600;
|
|
9455
|
+
var KEPT_GIT_VARS = new Set(["GIT_SSH", "GIT_SSH_COMMAND", "GIT_SSH_VARIANT"]);
|
|
9456
|
+
|
|
9457
|
+
class GitError extends Error {
|
|
9458
|
+
name = "GitError";
|
|
9459
|
+
}
|
|
9460
|
+
function gitEnv(base = process.env, overlay = {}) {
|
|
9461
|
+
const env = {};
|
|
9462
|
+
for (const [name, value] of Object.entries(base)) {
|
|
9463
|
+
if (value === undefined)
|
|
9464
|
+
continue;
|
|
9465
|
+
if (name.startsWith("GIT_") && !KEPT_GIT_VARS.has(name))
|
|
9466
|
+
continue;
|
|
9467
|
+
env[name] = value;
|
|
9468
|
+
}
|
|
9469
|
+
Object.assign(env, { GIT_TERMINAL_PROMPT: "0", GIT_ALLOW_PROTOCOL: ALLOWED_PROTOCOLS, GCM_INTERACTIVE: "never", LC_ALL: "C" });
|
|
9470
|
+
for (const [name, value] of Object.entries(overlay))
|
|
9471
|
+
if (value !== undefined)
|
|
9472
|
+
env[name] = value;
|
|
9473
|
+
return env;
|
|
9474
|
+
}
|
|
9475
|
+
function redact(text) {
|
|
9476
|
+
return text.replace(/([a-z][a-z0-9+.-]*:\/\/)[^\s/@]+@/gi, "$1***@");
|
|
9477
|
+
}
|
|
9478
|
+
function runGit(args, options) {
|
|
9479
|
+
const argv = ["-c", "protocol.ext.allow=never", ...args];
|
|
9480
|
+
const what = `git ${args.find((a) => !a.startsWith("-")) ?? ""}`.trim();
|
|
9481
|
+
const win32 = (options.platform ?? process.platform) === "win32";
|
|
9482
|
+
return new Promise((resolve, reject) => {
|
|
9483
|
+
if (options.signal?.aborted)
|
|
9484
|
+
return reject(new GitError(`${what} cancelled`));
|
|
9485
|
+
let settled = false;
|
|
9486
|
+
const finish = (error, stdout) => {
|
|
9487
|
+
if (settled)
|
|
9488
|
+
return;
|
|
9489
|
+
settled = true;
|
|
9490
|
+
clearTimeout(timer);
|
|
9491
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
9005
9492
|
if (error)
|
|
9006
9493
|
reject(error);
|
|
9007
9494
|
else
|
|
@@ -9011,7 +9498,7 @@ function runGit(args, options) {
|
|
|
9011
9498
|
cwd: options.cwd,
|
|
9012
9499
|
env: gitEnv(options.baseEnv ?? process.env, options.env),
|
|
9013
9500
|
stdio: ["ignore", "pipe", "pipe"],
|
|
9014
|
-
detached:
|
|
9501
|
+
detached: !win32,
|
|
9015
9502
|
windowsHide: true
|
|
9016
9503
|
});
|
|
9017
9504
|
const out = [];
|
|
@@ -9035,6 +9522,13 @@ function runGit(args, options) {
|
|
|
9035
9522
|
function stop(why) {
|
|
9036
9523
|
if (settled)
|
|
9037
9524
|
return;
|
|
9525
|
+
if (win32) {
|
|
9526
|
+
if (child.pid)
|
|
9527
|
+
(options.tree ?? processTree({ platform: "win32" })).killTree(child.pid);
|
|
9528
|
+
else
|
|
9529
|
+
child.kill("SIGKILL");
|
|
9530
|
+
return finish(new GitError(`${what} ${why}`));
|
|
9531
|
+
}
|
|
9038
9532
|
try {
|
|
9039
9533
|
if (child.pid)
|
|
9040
9534
|
process.kill(-child.pid, "SIGKILL");
|
|
@@ -9052,43 +9546,72 @@ function runGit(args, options) {
|
|
|
9052
9546
|
|
|
9053
9547
|
// packages/node/src/host.ts
|
|
9054
9548
|
var LAUNCH_MARKER = "hub-launch";
|
|
9055
|
-
|
|
9549
|
+
var nativeOps = {
|
|
9550
|
+
win32: process.platform === "win32",
|
|
9551
|
+
path: path11,
|
|
9552
|
+
realpath: (p) => realpathSync2(p),
|
|
9553
|
+
isSymlink: (p) => {
|
|
9554
|
+
try {
|
|
9555
|
+
return lstatSync4(p).isSymbolicLink();
|
|
9556
|
+
} catch {
|
|
9557
|
+
return false;
|
|
9558
|
+
}
|
|
9559
|
+
}
|
|
9560
|
+
};
|
|
9561
|
+
function realOf(p, ops) {
|
|
9562
|
+
const real = ops.realpath(p);
|
|
9563
|
+
if (!ops.win32)
|
|
9564
|
+
return real;
|
|
9565
|
+
const canonical = canonicalNativePath(real);
|
|
9566
|
+
if (canonical === undefined || !isWindowsAbsolutePath(canonical))
|
|
9567
|
+
throw new RuntimeError(`${p} resolves to ${real}, which is not on a local drive`);
|
|
9568
|
+
return canonical;
|
|
9569
|
+
}
|
|
9570
|
+
function resolvePath(p, ops = nativeOps) {
|
|
9056
9571
|
const rest = [];
|
|
9057
9572
|
let current = p;
|
|
9058
9573
|
for (;; ) {
|
|
9574
|
+
let real;
|
|
9059
9575
|
try {
|
|
9060
|
-
|
|
9576
|
+
real = realOf(current, ops);
|
|
9061
9577
|
} catch (cause) {
|
|
9578
|
+
if (cause instanceof RuntimeError)
|
|
9579
|
+
throw cause;
|
|
9062
9580
|
if (cause.code !== "ENOENT")
|
|
9063
9581
|
throw new RuntimeError(`cannot resolve ${p}: ${cause.message}`);
|
|
9064
|
-
|
|
9065
|
-
try {
|
|
9066
|
-
link = lstatSync4(current).isSymbolicLink();
|
|
9067
|
-
} catch {}
|
|
9068
|
-
if (link)
|
|
9582
|
+
if (ops.isSymlink(current))
|
|
9069
9583
|
throw new RuntimeError(`${p} goes through a dangling symlink`);
|
|
9070
|
-
const parent =
|
|
9584
|
+
const parent = ops.path.dirname(current);
|
|
9071
9585
|
if (parent === current)
|
|
9072
9586
|
throw new RuntimeError(`cannot resolve ${p}`);
|
|
9073
|
-
rest.push(
|
|
9587
|
+
rest.push(ops.path.basename(current));
|
|
9074
9588
|
current = parent;
|
|
9589
|
+
continue;
|
|
9075
9590
|
}
|
|
9591
|
+
return ops.win32 ? path11.posix.join(real, ...rest.reverse()) : path11.join(real, ...rest.reverse());
|
|
9076
9592
|
}
|
|
9077
9593
|
}
|
|
9078
|
-
function
|
|
9594
|
+
function within3(real, root, ops) {
|
|
9595
|
+
if (!ops.win32)
|
|
9596
|
+
return real === root || real.startsWith(root.endsWith(path11.sep) ? root : root + path11.sep);
|
|
9597
|
+
const r = real.toLowerCase();
|
|
9598
|
+
const base = root.toLowerCase();
|
|
9599
|
+
return r === base || r.startsWith(base.endsWith("/") ? base : `${base}/`);
|
|
9600
|
+
}
|
|
9601
|
+
function resolveUnderRoots(p, roots, ops = nativeOps) {
|
|
9079
9602
|
if (roots.length === 0)
|
|
9080
9603
|
throw new RuntimeError("this node has no workspaceRoots in node.json, so it prepares no workspaces");
|
|
9081
9604
|
if (!isAbsolutePath2(p))
|
|
9082
9605
|
throw new RuntimeError(`${p} is not an absolute path`);
|
|
9083
|
-
const real = resolvePath(p);
|
|
9606
|
+
const real = resolvePath(p, ops);
|
|
9084
9607
|
for (const root of roots) {
|
|
9085
9608
|
let realRoot;
|
|
9086
9609
|
try {
|
|
9087
|
-
realRoot =
|
|
9610
|
+
realRoot = realOf(root, ops);
|
|
9088
9611
|
} catch {
|
|
9089
9612
|
continue;
|
|
9090
9613
|
}
|
|
9091
|
-
if (real
|
|
9614
|
+
if (within3(real, realRoot, ops))
|
|
9092
9615
|
return real;
|
|
9093
9616
|
}
|
|
9094
9617
|
throw new RuntimeError(`${p} is not under this node's workspace roots`);
|
|
@@ -9143,7 +9666,7 @@ class HostRuntime {
|
|
|
9143
9666
|
const directory = resolveUnderRoots(existing, this.roots);
|
|
9144
9667
|
return this.prepared(repo, directory, "worktree", branch, base, true, ctx);
|
|
9145
9668
|
}
|
|
9146
|
-
const directory = resolveUnderRoots(path11.join(repo, ".worktrees", ...branch.split("/")), this.roots);
|
|
9669
|
+
const directory = resolveUnderRoots(path11.posix.join(repo, ".worktrees", ...branch.split("/")), this.roots);
|
|
9147
9670
|
if (existsSync3(directory))
|
|
9148
9671
|
throw new RuntimeError(`${directory} exists and is not the worktree of ${branch}`);
|
|
9149
9672
|
if (base)
|
|
@@ -9175,7 +9698,7 @@ class HostRuntime {
|
|
|
9175
9698
|
const target = resolveUnderRoots(input.workspace.repo, this.roots);
|
|
9176
9699
|
if (existsSync3(target) || isLink3(target))
|
|
9177
9700
|
throw new RuntimeError(`${target} exists; a clone needs a new directory`);
|
|
9178
|
-
const parent = path11.dirname(target);
|
|
9701
|
+
const parent = path11.posix.dirname(target);
|
|
9179
9702
|
mkdirSync6(parent, { recursive: true });
|
|
9180
9703
|
if (resolveUnderRoots(parent, this.roots) !== parent)
|
|
9181
9704
|
throw new RuntimeError(`${parent} moved while preparing`);
|
|
@@ -9547,19 +10070,30 @@ import { accessSync, constants as constants2, readFileSync as readFileSync6, sta
|
|
|
9547
10070
|
import { homedir as homedir3 } from "node:os";
|
|
9548
10071
|
import path12 from "node:path";
|
|
9549
10072
|
var KNOWN = ["opencode", "claude-code", "codex"];
|
|
9550
|
-
function executable(file) {
|
|
10073
|
+
function executable(file, win32 = false) {
|
|
9551
10074
|
try {
|
|
9552
10075
|
if (!statSync3(file).isFile())
|
|
9553
10076
|
return false;
|
|
9554
|
-
|
|
10077
|
+
if (!win32)
|
|
10078
|
+
accessSync(file, constants2.X_OK);
|
|
9555
10079
|
return true;
|
|
9556
10080
|
} catch {
|
|
9557
10081
|
return false;
|
|
9558
10082
|
}
|
|
9559
10083
|
}
|
|
9560
|
-
|
|
10084
|
+
var WIN_RUNNABLE = [".exe", ".com"];
|
|
10085
|
+
function windowsNames(name, env) {
|
|
10086
|
+
if (WIN_RUNNABLE.includes(path12.win32.extname(name).toLowerCase()))
|
|
10087
|
+
return [name];
|
|
10088
|
+
const listed = (env.PATHEXT ?? "").split(";").map((e) => e.trim().toLowerCase()).filter((e) => WIN_RUNNABLE.includes(e));
|
|
10089
|
+
const order = listed.length ? [...new Set(listed)] : WIN_RUNNABLE;
|
|
10090
|
+
return order.map((ext) => `${name}${ext}`);
|
|
10091
|
+
}
|
|
10092
|
+
function findExecutable(name, env, platform = process.platform) {
|
|
9561
10093
|
if (!name)
|
|
9562
10094
|
return;
|
|
10095
|
+
if (platform === "win32")
|
|
10096
|
+
return findWindowsExecutable(name, env);
|
|
9563
10097
|
if (path12.isAbsolute(name))
|
|
9564
10098
|
return executable(name) ? name : undefined;
|
|
9565
10099
|
if (name.includes("/"))
|
|
@@ -9573,13 +10107,30 @@ function findExecutable(name, env) {
|
|
|
9573
10107
|
}
|
|
9574
10108
|
return;
|
|
9575
10109
|
}
|
|
10110
|
+
function findWindowsExecutable(name, env) {
|
|
10111
|
+
if (path12.isAbsolute(name) || path12.win32.isAbsolute(name))
|
|
10112
|
+
return windowsNames(name, env).find((file) => executable(file, true));
|
|
10113
|
+
if (/[\\/]/.test(name))
|
|
10114
|
+
return;
|
|
10115
|
+
for (const dir of (env.PATH ?? env.Path ?? "").split(";")) {
|
|
10116
|
+
if (!dir || !(path12.isAbsolute(dir) || path12.win32.isAbsolute(dir)))
|
|
10117
|
+
continue;
|
|
10118
|
+
for (const file of windowsNames(name, env)) {
|
|
10119
|
+
const full = path12.join(dir, file);
|
|
10120
|
+
if (executable(full, true))
|
|
10121
|
+
return full;
|
|
10122
|
+
}
|
|
10123
|
+
}
|
|
10124
|
+
return;
|
|
10125
|
+
}
|
|
9576
10126
|
function detectHarnesses(options = {}) {
|
|
9577
10127
|
const env = options.env ?? process.env;
|
|
9578
10128
|
const found = new Set(["opencode"]);
|
|
9579
10129
|
const home = options.home ?? homedir3();
|
|
9580
|
-
|
|
10130
|
+
const platform = options.platform ?? process.platform;
|
|
10131
|
+
if (findExecutable(claudeBinary(env, home), env, platform))
|
|
9581
10132
|
found.add("claude-code");
|
|
9582
|
-
if (findExecutable(codexBinary(env, home), env))
|
|
10133
|
+
if (findExecutable(codexBinary(env, home, platform), env, platform))
|
|
9583
10134
|
found.add("codex");
|
|
9584
10135
|
for (const [name, on] of Object.entries(options.overrides ?? {})) {
|
|
9585
10136
|
if (on)
|
|
@@ -9604,12 +10155,21 @@ function claudeBinary(env, home = homedir3()) {
|
|
|
9604
10155
|
} catch {}
|
|
9605
10156
|
return "claude";
|
|
9606
10157
|
}
|
|
9607
|
-
function codexBinary(env, home = homedir3()) {
|
|
10158
|
+
function codexBinary(env, home = homedir3(), platform = process.platform) {
|
|
9608
10159
|
if (env.HUB_CODEX_BIN)
|
|
9609
10160
|
return env.HUB_CODEX_BIN;
|
|
9610
10161
|
const data = env.XDG_DATA_HOME && path12.isAbsolute(env.XDG_DATA_HOME) ? env.XDG_DATA_HOME : path12.join(home, ".local", "share");
|
|
9611
|
-
const
|
|
9612
|
-
|
|
10162
|
+
const win32 = platform === "win32";
|
|
10163
|
+
const current = path12.join(data, "hub", "harnesses", "codex", "current", "bin", win32 ? "codex.exe" : "codex");
|
|
10164
|
+
return executable(current, win32) ? current : "codex";
|
|
10165
|
+
}
|
|
10166
|
+
function opencodeBinary(env, home = homedir3(), platform = process.platform) {
|
|
10167
|
+
if (env.HUB_OPENCODE_BIN)
|
|
10168
|
+
return env.HUB_OPENCODE_BIN;
|
|
10169
|
+
const data = env.XDG_DATA_HOME && path12.isAbsolute(env.XDG_DATA_HOME) ? env.XDG_DATA_HOME : path12.join(home, ".local", "share");
|
|
10170
|
+
const win32 = platform === "win32";
|
|
10171
|
+
const current = path12.join(data, "hub", "harnesses", "opencode", "current", "bin", win32 ? "opencode.exe" : "opencode");
|
|
10172
|
+
return executable(current, win32) ? current : "opencode";
|
|
9613
10173
|
}
|
|
9614
10174
|
// packages/node/src/run.ts
|
|
9615
10175
|
import { homedir as homedir4 } from "node:os";
|
|
@@ -10029,6 +10589,91 @@ class OpenShellCli {
|
|
|
10029
10589
|
}
|
|
10030
10590
|
}
|
|
10031
10591
|
|
|
10592
|
+
// packages/node/src/opencode-service.ts
|
|
10593
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
10594
|
+
var START_BACKOFF_MS = 30000;
|
|
10595
|
+
var START_BACKOFF_MAX_MS = 10 * 60000;
|
|
10596
|
+
var START_TIMEOUT_MS = 90000;
|
|
10597
|
+
var STDERR_MAX2 = 2000;
|
|
10598
|
+
function startRun(env = process.env, timeoutMs = START_TIMEOUT_MS) {
|
|
10599
|
+
return (file, args) => new Promise((resolve) => {
|
|
10600
|
+
const clean = Object.fromEntries(Object.entries(env).filter((e) => typeof e[1] === "string"));
|
|
10601
|
+
let child;
|
|
10602
|
+
try {
|
|
10603
|
+
child = spawn3(file, [...args], { shell: false, windowsHide: true, env: clean, stdio: ["ignore", "ignore", "pipe"] });
|
|
10604
|
+
} catch (cause) {
|
|
10605
|
+
return resolve({ code: null, stderr: "", spawnError: cause.message });
|
|
10606
|
+
}
|
|
10607
|
+
let stderr = "";
|
|
10608
|
+
let done = false;
|
|
10609
|
+
const finish = (r) => {
|
|
10610
|
+
if (done)
|
|
10611
|
+
return;
|
|
10612
|
+
done = true;
|
|
10613
|
+
clearTimeout(timer);
|
|
10614
|
+
child.stderr?.destroy();
|
|
10615
|
+
resolve(r);
|
|
10616
|
+
};
|
|
10617
|
+
child.stderr?.on("data", (chunk) => {
|
|
10618
|
+
if (stderr.length < STDERR_MAX2)
|
|
10619
|
+
stderr += chunk.toString("utf8").slice(0, STDERR_MAX2 - stderr.length);
|
|
10620
|
+
});
|
|
10621
|
+
child.on("error", (cause) => finish({ code: null, stderr, spawnError: cause.message }));
|
|
10622
|
+
child.on("exit", (code) => finish({ code, stderr }));
|
|
10623
|
+
const timer = setTimeout(() => {
|
|
10624
|
+
child.kill();
|
|
10625
|
+
finish({ code: null, stderr, spawnError: `timed out after ${Math.round(timeoutMs / 1000)} s` });
|
|
10626
|
+
}, timeoutMs);
|
|
10627
|
+
});
|
|
10628
|
+
}
|
|
10629
|
+
var oneLine2 = (text) => text.replace(/[\p{Cc}\p{Cf}\u2028\u2029]+/gu, " ").replace(/\s+/g, " ").trim().slice(0, 300);
|
|
10630
|
+
|
|
10631
|
+
class OpenCodeServiceStarter {
|
|
10632
|
+
options;
|
|
10633
|
+
nextAttempt = 0;
|
|
10634
|
+
delay = START_BACKOFF_MS;
|
|
10635
|
+
missingLogged = false;
|
|
10636
|
+
constructor(options) {
|
|
10637
|
+
this.options = options;
|
|
10638
|
+
}
|
|
10639
|
+
async start() {
|
|
10640
|
+
const now = (this.options.now ?? Date.now)();
|
|
10641
|
+
if (now < this.nextAttempt)
|
|
10642
|
+
return false;
|
|
10643
|
+
const binary = this.options.binary();
|
|
10644
|
+
if (!binary) {
|
|
10645
|
+
if (!this.missingLogged)
|
|
10646
|
+
this.options.log("OpenCode is not installed (no opencode.exe found), so its background service cannot be started");
|
|
10647
|
+
this.missingLogged = true;
|
|
10648
|
+
return false;
|
|
10649
|
+
}
|
|
10650
|
+
this.missingLogged = false;
|
|
10651
|
+
this.options.log("OpenCode's background service is not running; starting it (opencode service start)");
|
|
10652
|
+
const r = await (this.options.run ?? startRun(this.options.env))(binary, ["service", "start"]);
|
|
10653
|
+
if (r.code === 0) {
|
|
10654
|
+
this.delay = START_BACKOFF_MS;
|
|
10655
|
+
this.nextAttempt = 0;
|
|
10656
|
+
this.options.log("started OpenCode's background service");
|
|
10657
|
+
return true;
|
|
10658
|
+
}
|
|
10659
|
+
const why = r.spawnError ?? (oneLine2(r.stderr) || `exit ${r.code}`);
|
|
10660
|
+
this.options.log(`could not start OpenCode's background service (${why}); retrying in ${Math.round(this.delay / 1000)} s`);
|
|
10661
|
+
this.nextAttempt = (this.options.now ?? Date.now)() + this.delay;
|
|
10662
|
+
this.delay = Math.min(this.delay * 2, START_BACKOFF_MAX_MS);
|
|
10663
|
+
return false;
|
|
10664
|
+
}
|
|
10665
|
+
}
|
|
10666
|
+
function serviceDiscovery(read, starter, platform = process.platform) {
|
|
10667
|
+
if (platform !== "win32")
|
|
10668
|
+
return read;
|
|
10669
|
+
return async () => {
|
|
10670
|
+
const endpoint = await read();
|
|
10671
|
+
if (endpoint)
|
|
10672
|
+
return endpoint;
|
|
10673
|
+
return await starter.start() ? read() : undefined;
|
|
10674
|
+
};
|
|
10675
|
+
}
|
|
10676
|
+
|
|
10032
10677
|
// packages/runtime-openshell/src/runtime.ts
|
|
10033
10678
|
import { createHash as createHash2 } from "node:crypto";
|
|
10034
10679
|
import { existsSync as existsSync4, mkdtempSync as mkdtempSync2, readFileSync as readFileSync7, rmSync as rmSync7, writeFileSync as writeFileSync3 } from "node:fs";
|
|
@@ -10037,7 +10682,7 @@ import path14 from "node:path";
|
|
|
10037
10682
|
// packages/runtime-openshell/package.json
|
|
10038
10683
|
var package_default3 = {
|
|
10039
10684
|
name: "@huynle/hub-runtime-openshell",
|
|
10040
|
-
version: "0.
|
|
10685
|
+
version: "0.9.1",
|
|
10041
10686
|
private: true,
|
|
10042
10687
|
description: "The `openshell` runtime of `hub node`: launches in NVIDIA OpenShell sandboxes, driven through the openshell CLI (internal; shipped inside @huynle/hub, not published on its own).",
|
|
10043
10688
|
license: "MIT",
|
|
@@ -10597,6 +11242,16 @@ async function sweepSandboxes(client, machineID) {
|
|
|
10597
11242
|
|
|
10598
11243
|
// packages/node/src/run.ts
|
|
10599
11244
|
var EXIT_CONFIG = 78;
|
|
11245
|
+
var IDLE_RECHECK_MS = 60000;
|
|
11246
|
+
var sleep5 = (ms, signal) => new Promise((resolve) => {
|
|
11247
|
+
const timer = setTimeout(done, ms);
|
|
11248
|
+
function done() {
|
|
11249
|
+
clearTimeout(timer);
|
|
11250
|
+
signal.removeEventListener("abort", done);
|
|
11251
|
+
resolve();
|
|
11252
|
+
}
|
|
11253
|
+
signal.addEventListener("abort", done, { once: true });
|
|
11254
|
+
});
|
|
10600
11255
|
var openCodeServiceFile = (env, home) => path15.join(env.XDG_STATE_HOME || path15.join(home, ".local", "state"), "opencode", "service.json");
|
|
10601
11256
|
function startOpenCodeWarmers(config, service, log, signal) {
|
|
10602
11257
|
const roots = () => config.workspaceRoots;
|
|
@@ -10632,6 +11287,7 @@ async function setupOpenShell(config, credentials, options, env, log) {
|
|
|
10632
11287
|
async function runNode(options) {
|
|
10633
11288
|
const env = options.env ?? process.env;
|
|
10634
11289
|
const home = options.home ?? homedir4();
|
|
11290
|
+
const platform = options.platform ?? process.platform;
|
|
10635
11291
|
const log = options.log ?? ((line) => process.stderr.write(`[hub node] ${line}
|
|
10636
11292
|
`));
|
|
10637
11293
|
const credentials = readSpokeCredentials(env, home);
|
|
@@ -10640,12 +11296,23 @@ async function runNode(options) {
|
|
|
10640
11296
|
return 1;
|
|
10641
11297
|
}
|
|
10642
11298
|
const file = nodeConfigPath(env, home);
|
|
10643
|
-
|
|
11299
|
+
let { config, problems } = readNodeConfig(file, { platform, home });
|
|
10644
11300
|
for (const problem of problems)
|
|
10645
11301
|
log(`${file}: ${problem}`);
|
|
10646
11302
|
if (config.workspaceRoots.length === 0) {
|
|
10647
11303
|
log(`${file} lists no workspaceRoots, so this node can prepare no workspaces; add e.g. {"workspaceRoots": ["${home}/projects"]}`);
|
|
10648
|
-
|
|
11304
|
+
if (platform !== "win32")
|
|
11305
|
+
return EXIT_CONFIG;
|
|
11306
|
+
log(`waiting: re-reading ${file} every ${Math.round((options.idleRecheckMs ?? IDLE_RECHECK_MS) / 1000)} s until it lists workspaceRoots`);
|
|
11307
|
+
while (config.workspaceRoots.length === 0) {
|
|
11308
|
+
await sleep5(options.idleRecheckMs ?? IDLE_RECHECK_MS, options.signal);
|
|
11309
|
+
if (options.signal.aborted)
|
|
11310
|
+
return 0;
|
|
11311
|
+
({ config, problems } = readNodeConfig(file, { platform, home }));
|
|
11312
|
+
}
|
|
11313
|
+
for (const problem of problems)
|
|
11314
|
+
log(`${file}: ${problem}`);
|
|
11315
|
+
log(`workspace roots now set in ${file}; starting`);
|
|
10649
11316
|
}
|
|
10650
11317
|
if (options.signal.aborted)
|
|
10651
11318
|
return 0;
|
|
@@ -10670,8 +11337,16 @@ async function runNode(options) {
|
|
|
10670
11337
|
});
|
|
10671
11338
|
log(`hub node ${NODE_VERSION}: ${config.slots} slots, roots ${config.workspaceRoots.join(", ")}; host harnesses ${harnesses.join(", ") || "none"}`);
|
|
10672
11339
|
daemon.start();
|
|
10673
|
-
if (harnesses.includes("opencode"))
|
|
10674
|
-
|
|
11340
|
+
if (harnesses.includes("opencode")) {
|
|
11341
|
+
const read = options.openCodeService ?? (() => readServiceRegistration(openCodeServiceFile(env, home)));
|
|
11342
|
+
const starter = new OpenCodeServiceStarter({
|
|
11343
|
+
binary: options.openCodeBinary ?? (() => findExecutable(opencodeBinary(env, home, platform), env, platform)),
|
|
11344
|
+
log,
|
|
11345
|
+
env,
|
|
11346
|
+
...options.startOpenCode ? { run: options.startOpenCode } : {}
|
|
11347
|
+
});
|
|
11348
|
+
startOpenCodeWarmers(config, serviceDiscovery(read, starter, platform), log, options.signal);
|
|
11349
|
+
}
|
|
10675
11350
|
await new Promise((resolve) => {
|
|
10676
11351
|
if (options.signal.aborted)
|
|
10677
11352
|
return resolve();
|
|
@@ -10972,6 +11647,64 @@ NoNewPrivileges=true
|
|
|
10972
11647
|
WantedBy=default.target
|
|
10973
11648
|
`;
|
|
10974
11649
|
|
|
11650
|
+
// scripts/hub-node.task.xml
|
|
11651
|
+
var hub_node_task_default = `<?xml version="1.0" encoding="UTF-16"?>
|
|
11652
|
+
<!-- The hub node as a per-user logon task (Windows Task Scheduler). hubctl setup and \`hub node install\` render it
|
|
11653
|
+
(no visible window: the hidden console host starts hubctl; native paths; Windows argv quoting) and write it as
|
|
11654
|
+
UTF-16LE with a BOM, which schtasks /XML requires. -->
|
|
11655
|
+
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
11656
|
+
<RegistrationInfo>
|
|
11657
|
+
<Description>Hub node (prepares launch workspaces for the hub). Started at logon without a window; started again within a minute after it exits.</Description>
|
|
11658
|
+
</RegistrationInfo>
|
|
11659
|
+
<Triggers>
|
|
11660
|
+
<LogonTrigger>
|
|
11661
|
+
<Enabled>true</Enabled>
|
|
11662
|
+
<UserId>__USER__</UserId>
|
|
11663
|
+
<Repetition>
|
|
11664
|
+
<Interval>PT1M</Interval>
|
|
11665
|
+
<StopAtDurationEnd>false</StopAtDurationEnd>
|
|
11666
|
+
</Repetition>
|
|
11667
|
+
</LogonTrigger>
|
|
11668
|
+
</Triggers>
|
|
11669
|
+
<Principals>
|
|
11670
|
+
<Principal id="Author">
|
|
11671
|
+
<UserId>__USER__</UserId>
|
|
11672
|
+
<LogonType>InteractiveToken</LogonType>
|
|
11673
|
+
<RunLevel>LeastPrivilege</RunLevel>
|
|
11674
|
+
</Principal>
|
|
11675
|
+
</Principals>
|
|
11676
|
+
<Settings>
|
|
11677
|
+
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
|
11678
|
+
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
|
11679
|
+
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
|
11680
|
+
<AllowHardTerminate>true</AllowHardTerminate>
|
|
11681
|
+
<StartWhenAvailable>false</StartWhenAvailable>
|
|
11682
|
+
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
|
|
11683
|
+
<IdleSettings>
|
|
11684
|
+
<StopOnIdleEnd>false</StopOnIdleEnd>
|
|
11685
|
+
<RestartOnIdle>false</RestartOnIdle>
|
|
11686
|
+
</IdleSettings>
|
|
11687
|
+
<AllowStartOnDemand>true</AllowStartOnDemand>
|
|
11688
|
+
<Enabled>true</Enabled>
|
|
11689
|
+
<Hidden>false</Hidden>
|
|
11690
|
+
<RunOnlyIfIdle>false</RunOnlyIfIdle>
|
|
11691
|
+
<WakeToRun>false</WakeToRun>
|
|
11692
|
+
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
|
|
11693
|
+
<Priority>7</Priority>
|
|
11694
|
+
<RestartOnFailure>
|
|
11695
|
+
<Interval>PT1M</Interval>
|
|
11696
|
+
<Count>999</Count>
|
|
11697
|
+
</RestartOnFailure>
|
|
11698
|
+
</Settings>
|
|
11699
|
+
<Actions Context="Author">
|
|
11700
|
+
<Exec>
|
|
11701
|
+
<Command>%SystemRoot%\\System32\\conhost.exe</Command>
|
|
11702
|
+
<Arguments>--headless __ARGV__</Arguments>
|
|
11703
|
+
</Exec>
|
|
11704
|
+
</Actions>
|
|
11705
|
+
</Task>
|
|
11706
|
+
`;
|
|
11707
|
+
|
|
10975
11708
|
// packages/cli/src/bootstrap/release/assets.ts
|
|
10976
11709
|
var EMBEDDED_TEXT = Object.freeze({
|
|
10977
11710
|
"service/launchd/node": io_github_huynle_hub_node_default,
|
|
@@ -10979,7 +11712,8 @@ var EMBEDDED_TEXT = Object.freeze({
|
|
|
10979
11712
|
"service/launchd/claude-code": io_github_huynle_hub_claude_code_default,
|
|
10980
11713
|
"service/systemd-user/claude-code": hub_claude_code_default,
|
|
10981
11714
|
"service/launchd/codex-spoke": io_github_huynle_hub_codex_spoke_default,
|
|
10982
|
-
"service/systemd-user/codex-spoke": hub_codex_spoke_default
|
|
11715
|
+
"service/systemd-user/codex-spoke": hub_codex_spoke_default,
|
|
11716
|
+
"service/task-scheduler/node": hub_node_task_default
|
|
10983
11717
|
});
|
|
10984
11718
|
|
|
10985
11719
|
// packages/cli/src/bootstrap/adapters/node-services/render.ts
|
|
@@ -10988,7 +11722,34 @@ var SERVICE_ENVIRONMENT = Object.freeze({
|
|
|
10988
11722
|
"claude-code": ["HUB_SPOKE_CONFIG", "XDG_CONFIG_HOME", "HUB_STATE_DIR", "XDG_STATE_HOME", "HUB_CLAUDE_SOCKET", "HUB_CLAUDE_BIN"],
|
|
10989
11723
|
"codex-spoke": ["HUB_SPOKE_CONFIG", "XDG_CONFIG_HOME", "HUB_STATE_DIR", "XDG_STATE_HOME", "CODEX_HOME", "HUB_CODEX_BIN", "HUB_CODEX_DAEMON_SOCKET"]
|
|
10990
11724
|
});
|
|
10991
|
-
var UNIT_ROOT =
|
|
11725
|
+
var UNIT_ROOT = SERVICE_ROOT;
|
|
11726
|
+
var TASK_NAME = /^hub-(?:node|claude-code|codex-spoke|test-[a-z0-9-]{1,32})-[A-Za-z0-9._-]{1,64}$/;
|
|
11727
|
+
var TEST_PREFIX = /^hub-test-[a-z0-9-]{1,32}$/;
|
|
11728
|
+
var SID2 = /^S-1-[0-9]{1,12}(?:-[0-9]{1,10}){1,15}$/;
|
|
11729
|
+
function taskNameFor(service, user, env) {
|
|
11730
|
+
const safe = user.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 64);
|
|
11731
|
+
if (!safe)
|
|
11732
|
+
throw new Error("the Windows user name is empty; set USERNAME");
|
|
11733
|
+
const prefix = env.HUBCTL_TEST_TASK_PREFIX;
|
|
11734
|
+
if (prefix !== undefined && !TEST_PREFIX.test(prefix))
|
|
11735
|
+
throw new Error("HUBCTL_TEST_TASK_PREFIX must start with hub-test- (lower-case letters, digits and -)");
|
|
11736
|
+
return `${prefix ?? `hub-${service}`}-${safe}`;
|
|
11737
|
+
}
|
|
11738
|
+
function quoteWindowsArg(word) {
|
|
11739
|
+
if (word !== "" && !/[\s"]/.test(word))
|
|
11740
|
+
return word;
|
|
11741
|
+
let out = '"';
|
|
11742
|
+
let slashes = 0;
|
|
11743
|
+
for (const ch of word) {
|
|
11744
|
+
if (ch === "\\")
|
|
11745
|
+
slashes++;
|
|
11746
|
+
else {
|
|
11747
|
+
out += "\\".repeat(ch === '"' ? slashes * 2 + 1 : slashes) + ch;
|
|
11748
|
+
slashes = 0;
|
|
11749
|
+
}
|
|
11750
|
+
}
|
|
11751
|
+
return `${out}${"\\".repeat(slashes * 2)}"`;
|
|
11752
|
+
}
|
|
10992
11753
|
var assetIDFor = (service, manager) => `service/${manager}/${service}`;
|
|
10993
11754
|
function serviceName(service, manager) {
|
|
10994
11755
|
const file = EMBEDDED_ASSETS[assetIDFor(service, manager)].installAs;
|
|
@@ -11048,6 +11809,40 @@ ${indent}<string>${xml(value)}</string>`).join("");
|
|
|
11048
11809
|
return template.replace(/<!--[\s\S]*?-->/, () => `<!-- ${header2(asset, by)} -->`).replace(programArguments, (_all, open, _words, close) => `${open}${argv.map((w) => `
|
|
11049
11810
|
${indent}<string>${xml(w)}</string>`).join("")}${close}`).replaceAll("__HOME__", xml(home)).replace(/(<key>PATH<\/key>\s*<string>[^<]*<\/string>)/, (line) => `${line}${extra}`);
|
|
11050
11811
|
}
|
|
11812
|
+
var xmlText = (text) => text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
11813
|
+
function renderTask(c, input, asset, template) {
|
|
11814
|
+
if (input.layout.kind !== "standalone")
|
|
11815
|
+
c.add("/layout", "unsupported_platform", "a Windows logon task starts the standalone hubctl.exe only, not a script under a runtime");
|
|
11816
|
+
if (Object.keys(input.environment ?? {}).length > 0)
|
|
11817
|
+
c.add("/environment", "invalid_value", "a Task Scheduler task carries no environment of its own");
|
|
11818
|
+
const task = input.task;
|
|
11819
|
+
if (!task)
|
|
11820
|
+
c.add("/task", "missing_field", "a Task Scheduler task needs its name and the user's SID");
|
|
11821
|
+
else {
|
|
11822
|
+
if (!TASK_NAME.test(task.name))
|
|
11823
|
+
c.add("/task/name", "invalid_value", "expected a hub task name (hub-<service>-<user>)");
|
|
11824
|
+
if (!SID2.test(task.userSid))
|
|
11825
|
+
c.add("/task/userSid", "invalid_value", "expected the user's SID (S-1-…)");
|
|
11826
|
+
}
|
|
11827
|
+
if (!isAbsolutePath(input.home) || !isWindowsPath(input.home))
|
|
11828
|
+
c.add("/home", "invalid_path", "a Windows logon task needs a Windows home directory (C:/…)");
|
|
11829
|
+
if (!/<Arguments>--headless __ARGV__<\/Arguments>/.test(template))
|
|
11830
|
+
c.add("/asset", "inconsistent", `${asset} does not start hubctl under the hidden console host`);
|
|
11831
|
+
if (c.issues.length > 0)
|
|
11832
|
+
return;
|
|
11833
|
+
const argv = serviceArgv(input.layout, input.service);
|
|
11834
|
+
const words = argv.map((w, i) => i === 0 ? toNativePath(w, "win32") : w);
|
|
11835
|
+
if (words.some((w) => w.includes('"')))
|
|
11836
|
+
c.add("/layout", "invalid_path", "the program path may not contain a double quote");
|
|
11837
|
+
return template.replace(/<!--[\s\S]*?-->/, () => `<!-- ${header2(asset, input.writtenBy)} -->`).replaceAll("__USER__", () => task.userSid).replace("__ARGV__", () => xmlText(words.map(quoteWindowsArg).join(" ")));
|
|
11838
|
+
}
|
|
11839
|
+
var utf16le = (text) => {
|
|
11840
|
+
const body = Buffer.from(text, "utf16le");
|
|
11841
|
+
const bytes = new Uint8Array(body.length + 2);
|
|
11842
|
+
bytes.set([255, 254]);
|
|
11843
|
+
bytes.set(body, 2);
|
|
11844
|
+
return bytes;
|
|
11845
|
+
};
|
|
11051
11846
|
function renderServiceUnit(input) {
|
|
11052
11847
|
const c = new Checker;
|
|
11053
11848
|
const asset = assetIDFor(input.service, input.manager);
|
|
@@ -11057,22 +11852,44 @@ function renderServiceUnit(input) {
|
|
|
11057
11852
|
c.add("/service", "invalid_value", `no embedded template for ${input.service} under ${input.manager}`);
|
|
11058
11853
|
return c.result(undefined);
|
|
11059
11854
|
}
|
|
11855
|
+
if (input.manager === "task-scheduler") {
|
|
11856
|
+
const text = renderTask(c, input, asset, template);
|
|
11857
|
+
if (text === undefined || c.issues.length > 0)
|
|
11858
|
+
return c.result(undefined);
|
|
11859
|
+
if (PLACEHOLDER.test(text))
|
|
11860
|
+
c.add("/asset", "inconsistent", `${asset} has a placeholder this hubctl does not fill`);
|
|
11861
|
+
if (c.issues.length > 0)
|
|
11862
|
+
return c.result(undefined);
|
|
11863
|
+
const bytes = utf16le(text);
|
|
11864
|
+
return c.result({
|
|
11865
|
+
asset,
|
|
11866
|
+
service: input.service,
|
|
11867
|
+
manager: input.manager,
|
|
11868
|
+
path: { root: UNIT_ROOT[input.manager], rel: entry.installAs },
|
|
11869
|
+
name: input.task.name,
|
|
11870
|
+
argv: serviceArgv(input.layout, input.service),
|
|
11871
|
+
text,
|
|
11872
|
+
bytes,
|
|
11873
|
+
sha256: `sha256:${sha256Hex(bytes)}`
|
|
11874
|
+
});
|
|
11875
|
+
}
|
|
11876
|
+
const manager = input.manager;
|
|
11060
11877
|
let argv = [];
|
|
11061
11878
|
try {
|
|
11062
11879
|
argv = serviceArgv(input.layout, input.service);
|
|
11063
11880
|
} catch (error) {
|
|
11064
11881
|
c.add("/layout", "invalid_path", error.message);
|
|
11065
11882
|
}
|
|
11066
|
-
const homeOK = isAbsolutePath(input.home) && input.home !== "/" && (
|
|
11883
|
+
const homeOK = isAbsolutePath(input.home) && input.home !== "/" && (manager !== "systemd-user" || SYSTEMD_HOME.test(input.home));
|
|
11067
11884
|
if (!homeOK)
|
|
11068
11885
|
c.add("/home", "invalid_path", "the service's home must be an absolute, normalized directory other than /");
|
|
11069
11886
|
const env = checkEnvironment(c, input.service, input.environment ?? {});
|
|
11070
11887
|
if (c.issues.length > 0)
|
|
11071
11888
|
return c.result(undefined);
|
|
11072
11889
|
const command = SERVICE_COMMANDS[input.service];
|
|
11073
|
-
const raw =
|
|
11890
|
+
const raw = manager === "systemd-user" ? renderSystemd(c, asset, template, argv, command, input.home, env, input.writtenBy) : renderLaunchd(c, asset, template, argv, command, input.home, env, input.writtenBy);
|
|
11074
11891
|
const asBun = input.layout.kind === "script";
|
|
11075
|
-
const text = asBun ? raw : raw.replace(BUN_BE_BUN[
|
|
11892
|
+
const text = asBun ? raw : raw.replace(BUN_BE_BUN[manager], "");
|
|
11076
11893
|
if (!asBun && text.includes("BUN_BE_BUN"))
|
|
11077
11894
|
c.add("/asset", "inconsistent", `${asset} sets BUN_BE_BUN, which a standalone hubctl must not get`);
|
|
11078
11895
|
if (PLACEHOLDER.test(text))
|
|
@@ -11087,7 +11904,7 @@ function renderServiceUnit(input) {
|
|
|
11087
11904
|
service: input.service,
|
|
11088
11905
|
manager: input.manager,
|
|
11089
11906
|
path: { root: UNIT_ROOT[input.manager], rel: entry.installAs },
|
|
11090
|
-
name: serviceName(input.service,
|
|
11907
|
+
name: serviceName(input.service, manager),
|
|
11091
11908
|
argv,
|
|
11092
11909
|
text,
|
|
11093
11910
|
bytes,
|
|
@@ -11095,6 +11912,39 @@ function renderServiceUnit(input) {
|
|
|
11095
11912
|
});
|
|
11096
11913
|
}
|
|
11097
11914
|
|
|
11915
|
+
// packages/cli/src/bootstrap/discovery/util.ts
|
|
11916
|
+
function normalizeAbs(value) {
|
|
11917
|
+
if (typeof value !== "string")
|
|
11918
|
+
return;
|
|
11919
|
+
let p = value;
|
|
11920
|
+
if (/^[A-Za-z]:[\\/]/.test(p))
|
|
11921
|
+
p = p[0].toUpperCase() + p.slice(1).replace(/\\/g, "/");
|
|
11922
|
+
else if (!p.startsWith("/") || p.startsWith("//"))
|
|
11923
|
+
return;
|
|
11924
|
+
p = p.replace(/\/{2,}/g, "/");
|
|
11925
|
+
if (p.endsWith("/") && p !== "/" && !/^[A-Z]:\/$/.test(p))
|
|
11926
|
+
p = p.slice(0, -1);
|
|
11927
|
+
return isAbsolutePath(p) ? p : undefined;
|
|
11928
|
+
}
|
|
11929
|
+
function canonicalWinLink(text) {
|
|
11930
|
+
const stripped = text.replace(/^(\\\\\?\\|\\\?\?\\)/, "");
|
|
11931
|
+
return normalizeAbs(stripped) ?? text;
|
|
11932
|
+
}
|
|
11933
|
+
var probePath = (platform, text) => text !== undefined && platform === "win32" ? canonicalWinLink(text) : text;
|
|
11934
|
+
var join = (dir, rel) => dir.endsWith("/") ? `${dir}${rel}` : `${dir}/${rel}`;
|
|
11935
|
+
var decode = (bytes) => bytes ? new TextDecoder("utf-8", { fatal: false }).decode(bytes) : "";
|
|
11936
|
+
var oneLine3 = (v, max = 64) => v && v.length <= max && !/[\u0000-\u001f\u007f]/.test(v) ? v : undefined;
|
|
11937
|
+
var hostAbs = (platform, value) => {
|
|
11938
|
+
const p = normalizeAbs(value);
|
|
11939
|
+
return p && (platform !== "win32" || isWindowsPath(p)) ? p : undefined;
|
|
11940
|
+
};
|
|
11941
|
+
var underDir = (p, dir) => {
|
|
11942
|
+
const prefix = dir.endsWith("/") ? dir : `${dir}/`;
|
|
11943
|
+
return isWindowsPath(p) ? p.toLowerCase().startsWith(prefix.toLowerCase()) : p.startsWith(prefix);
|
|
11944
|
+
};
|
|
11945
|
+
var WINDOWS_COMMAND_EXTENSIONS = [".com", ".exe", ".bat", ".cmd"];
|
|
11946
|
+
var isWindowsExecutable = (path) => WINDOWS_COMMAND_EXTENSIONS.some((e) => path.toLowerCase().endsWith(e));
|
|
11947
|
+
|
|
11098
11948
|
// packages/cli/src/node.ts
|
|
11099
11949
|
var NODE_LABEL = "io.github.huynle.hub-node";
|
|
11100
11950
|
var NODE_UNIT = "hub-node.service";
|
|
@@ -11169,13 +12019,58 @@ function systemd(spec, io, extra) {
|
|
|
11169
12019
|
check: [`systemctl --user status ${unit}`, `journalctl --user -u ${unit} -f`]
|
|
11170
12020
|
};
|
|
11171
12021
|
}
|
|
12022
|
+
function taskScheduler(spec, io, extra) {
|
|
12023
|
+
if (spec.service !== "node")
|
|
12024
|
+
throw new CliError(EXIT.unavailable, `${spec.command} install does not support Windows yet: hubctl installs no user service for it there`, spec.elsewhere);
|
|
12025
|
+
const home = normalizeAbs(io.home);
|
|
12026
|
+
if (!home)
|
|
12027
|
+
throw new CliError(EXIT.usage, `cannot write the ${spec.command} task: the home directory ${io.home} is not a drive path`);
|
|
12028
|
+
const state = normalizeAbs(resolveHubStateDir(io.env, io.home).dir);
|
|
12029
|
+
if (!state)
|
|
12030
|
+
throw new CliError(EXIT.usage, `cannot write the ${spec.command} task: the hub state directory is not a drive path`);
|
|
12031
|
+
const executable = normalizeAbs(io.program.execPath);
|
|
12032
|
+
const found = programLayout(io.program);
|
|
12033
|
+
const layout = found.kind === "standalone" && executable ? { kind: "standalone", executable } : found;
|
|
12034
|
+
const { environment, skipped } = carried(spec, io, extra);
|
|
12035
|
+
let name;
|
|
12036
|
+
try {
|
|
12037
|
+
name = taskNameFor(spec.service, io.env.USERNAME ?? "", io.env);
|
|
12038
|
+
} catch (error) {
|
|
12039
|
+
throw new CliError(EXIT.usage, `cannot write the ${spec.command} task: ${error.message}`);
|
|
12040
|
+
}
|
|
12041
|
+
const r = renderServiceUnit({ service: spec.service, manager: "task-scheduler", layout, home, writtenBy: spec.command, task: { name, userSid: (io.userSid ?? (() => defaultWinAcl().sid()))() } });
|
|
12042
|
+
if (!r.ok)
|
|
12043
|
+
throw new CliError(EXIT.usage, `cannot write the ${spec.command} task: ${r.issues.map((i) => `${i.path ? `${i.path}: ` : ""}${i.message}`).join("; ")}`);
|
|
12044
|
+
const file = toNativePath(`${state}/tasks/${r.value.path.rel}`, "win32");
|
|
12045
|
+
const stopNode = `Get-CimInstance Win32_Process -Filter "Name='hubctl.exe'" | Where-Object CommandLine -like '* node run' | Invoke-CimMethod -MethodName Terminate # PowerShell: /End stops only the hidden console host`;
|
|
12046
|
+
return {
|
|
12047
|
+
kind: "task-scheduler",
|
|
12048
|
+
file,
|
|
12049
|
+
content: r.value.text,
|
|
12050
|
+
bytes: r.value.bytes,
|
|
12051
|
+
skipped: [...Object.keys(environment), ...skipped].sort(),
|
|
12052
|
+
dirs: [],
|
|
12053
|
+
load: [`schtasks /Create /TN ${name} /XML "${file}" /F`, `schtasks /Run /TN ${name}`],
|
|
12054
|
+
unload: [`schtasks /End /TN ${name}`, stopNode, `schtasks /Delete /TN ${name} /F`],
|
|
12055
|
+
check: [`schtasks /Query /TN ${name} /V /FO LIST`]
|
|
12056
|
+
};
|
|
12057
|
+
}
|
|
11172
12058
|
function serviceFor(spec, io, extra) {
|
|
11173
12059
|
if (io.platform === "darwin")
|
|
11174
12060
|
return launchd(spec, io, extra);
|
|
11175
12061
|
if (io.platform === "linux")
|
|
11176
12062
|
return systemd(spec, io, extra);
|
|
12063
|
+
if (io.platform === "win32")
|
|
12064
|
+
return taskScheduler(spec, io, extra);
|
|
11177
12065
|
throw new CliError(EXIT.unavailable, `${spec.command} install supports macOS (launchd) and Linux (systemd), not ${io.platform}`, spec.elsewhere);
|
|
11178
12066
|
}
|
|
12067
|
+
var readBytes = (file) => {
|
|
12068
|
+
try {
|
|
12069
|
+
return readFileSync8(file);
|
|
12070
|
+
} catch {
|
|
12071
|
+
return;
|
|
12072
|
+
}
|
|
12073
|
+
};
|
|
11179
12074
|
var readText = (file) => {
|
|
11180
12075
|
try {
|
|
11181
12076
|
return readFileSync8(file, "utf8");
|
|
@@ -11202,21 +12097,22 @@ function installNode(io, options) {
|
|
|
11202
12097
|
}
|
|
11203
12098
|
function installService(spec, io, options, given, extra) {
|
|
11204
12099
|
const service = serviceFor(spec, io, extra);
|
|
11205
|
-
const
|
|
12100
|
+
const why = service.kind === "task-scheduler" ? "is set in this shell, but a logon task has no environment of its own, so the node does not see it" : "is not an absolute path, so the service does not carry it";
|
|
12101
|
+
const warnings = [...given, ...service.skipped.map((key) => `${key} ${why}`)];
|
|
11206
12102
|
const load = commands(`Load it ${NOT_RUN}:`, service.load);
|
|
11207
12103
|
if (options.dryRun)
|
|
11208
12104
|
return { stdout: `would write ${service.file}:
|
|
11209
12105
|
|
|
11210
12106
|
${service.content}
|
|
11211
12107
|
${load}`, warnings };
|
|
11212
|
-
const current = readText(service.file);
|
|
12108
|
+
const current = service.bytes ? readBytes(service.file) : readText(service.file);
|
|
11213
12109
|
for (const dir of service.dirs)
|
|
11214
12110
|
mkdirSync7(dir, { recursive: true });
|
|
11215
|
-
if (current === service.content)
|
|
12111
|
+
if (service.bytes ? current !== undefined && Buffer.from(service.bytes).equals(current) : current === service.content)
|
|
11216
12112
|
return { stdout: `${service.file} is up to date
|
|
11217
12113
|
${load}`, warnings };
|
|
11218
12114
|
mkdirSync7(path16.dirname(service.file), { recursive: true });
|
|
11219
|
-
writeFileSync4(service.file, service.content, { mode: 420 });
|
|
12115
|
+
writeFileSync4(service.file, service.bytes ?? service.content, { mode: 420 });
|
|
11220
12116
|
return { stdout: `${current === undefined ? "wrote" : "updated"} ${service.file}
|
|
11221
12117
|
${load}`, warnings };
|
|
11222
12118
|
}
|
|
@@ -11238,6 +12134,10 @@ ${stop}`, warnings: [] };
|
|
|
11238
12134
|
function serviceState(spec, io, extra) {
|
|
11239
12135
|
try {
|
|
11240
12136
|
const s = serviceFor(spec, io, extra);
|
|
12137
|
+
if (s.bytes) {
|
|
12138
|
+
const bytes = readBytes(s.file);
|
|
12139
|
+
return { kind: s.kind, file: s.file, installed: bytes !== undefined, current: bytes !== undefined && Buffer.from(s.bytes).equals(bytes), check: s.check };
|
|
12140
|
+
}
|
|
11241
12141
|
const text = readText(s.file);
|
|
11242
12142
|
return { kind: s.kind, file: s.file, installed: text !== undefined, current: text === s.content, check: s.check };
|
|
11243
12143
|
} catch {
|
|
@@ -11278,9 +12178,28 @@ function nodeStatusText(status) {
|
|
|
11278
12178
|
return lines.join(`
|
|
11279
12179
|
`);
|
|
11280
12180
|
}
|
|
12181
|
+
var NODE_LOG_MAX_BYTES = 1024 * 1024;
|
|
12182
|
+
function nodeLogger(o) {
|
|
12183
|
+
const file = path16.join(o.stateDir, "node.log");
|
|
12184
|
+
return (line) => {
|
|
12185
|
+
o.stderr(`[hub node] ${line}
|
|
12186
|
+
`);
|
|
12187
|
+
if (o.platform !== "win32")
|
|
12188
|
+
return;
|
|
12189
|
+
try {
|
|
12190
|
+
mkdirSync7(o.stateDir, { recursive: true });
|
|
12191
|
+
try {
|
|
12192
|
+
if (statSync4(file).size > NODE_LOG_MAX_BYTES)
|
|
12193
|
+
renameSync5(file, `${file}.1`);
|
|
12194
|
+
} catch {}
|
|
12195
|
+
appendFileSync(file, `${(o.now ?? (() => new Date))().toISOString()} ${line}
|
|
12196
|
+
`);
|
|
12197
|
+
} catch {}
|
|
12198
|
+
};
|
|
12199
|
+
}
|
|
11281
12200
|
function runNodeCommand(io) {
|
|
11282
|
-
|
|
11283
|
-
|
|
12201
|
+
const log = nodeLogger({ platform: io.platform ?? process.platform, stateDir: resolveHubStateDir(io.env, io.home).dir, stderr: (s) => io.stderr(s) });
|
|
12202
|
+
return runNode({ env: io.env, home: io.home, signal: io.signal ?? new AbortController().signal, log });
|
|
11284
12203
|
}
|
|
11285
12204
|
|
|
11286
12205
|
// packages/cli/src/openshell.ts
|
|
@@ -12515,8 +13434,8 @@ async function runMcpPipe(options) {
|
|
|
12515
13434
|
}
|
|
12516
13435
|
|
|
12517
13436
|
// packages/claude-code/src/launcher.ts
|
|
12518
|
-
import { spawn as
|
|
12519
|
-
import { realpathSync as realpathSync4, statSync as
|
|
13437
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
13438
|
+
import { realpathSync as realpathSync4, statSync as statSync5 } from "node:fs";
|
|
12520
13439
|
import path19 from "node:path";
|
|
12521
13440
|
var INTERRUPTED = "Interrupted from the hub";
|
|
12522
13441
|
var CONTROL_WAIT_MS = 5000;
|
|
@@ -12525,7 +13444,7 @@ var KILL_GRACE_MS = 1000;
|
|
|
12525
13444
|
var LINE_MAX = 16 * 1024 * 1024;
|
|
12526
13445
|
var STDERR_KEEP = 2000;
|
|
12527
13446
|
var record2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : {};
|
|
12528
|
-
var
|
|
13447
|
+
var sleep6 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
12529
13448
|
var AGENT2 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
|
|
12530
13449
|
var MODEL = /^[A-Za-z0-9][A-Za-z0-9._:@/\[\]-]{0,299}$/;
|
|
12531
13450
|
var PROVIDERS = new Set(["anthropic", "claude-code"]);
|
|
@@ -12539,7 +13458,7 @@ function launchDirectory(directory, roots) {
|
|
|
12539
13458
|
} catch {
|
|
12540
13459
|
throw new Error(`${directory} does not exist`);
|
|
12541
13460
|
}
|
|
12542
|
-
if (!
|
|
13461
|
+
if (!statSync5(real).isDirectory())
|
|
12543
13462
|
throw new Error(`${directory} is not a directory`);
|
|
12544
13463
|
for (const root of roots) {
|
|
12545
13464
|
let base;
|
|
@@ -12613,7 +13532,7 @@ class ClaudeLauncher {
|
|
|
12613
13532
|
delete env.BUN_BE_BUN;
|
|
12614
13533
|
let child;
|
|
12615
13534
|
try {
|
|
12616
|
-
child =
|
|
13535
|
+
child = spawn4(this.#options.bin, launchArgs(input), { cwd: input.directory, env, stdio: ["pipe", "pipe", "pipe"] });
|
|
12617
13536
|
} catch (cause) {
|
|
12618
13537
|
throw new Error(`cannot run ${this.#options.bin}: ${cause.message}`);
|
|
12619
13538
|
}
|
|
@@ -12701,7 +13620,7 @@ class ClaudeLauncher {
|
|
|
12701
13620
|
const answered = new Promise((resolve) => proc.controls.set(id, resolve));
|
|
12702
13621
|
proc.child.stdin.write(`${JSON.stringify({ type: "control_request", request_id: id, request })}
|
|
12703
13622
|
`);
|
|
12704
|
-
const response = await Promise.race([answered,
|
|
13623
|
+
const response = await Promise.race([answered, sleep6(waitMs).then(() => {
|
|
12705
13624
|
return;
|
|
12706
13625
|
})]);
|
|
12707
13626
|
proc.controls.delete(id);
|
|
@@ -12712,10 +13631,10 @@ class ClaudeLauncher {
|
|
|
12712
13631
|
for (const proc of procs)
|
|
12713
13632
|
proc.child.stdin.end();
|
|
12714
13633
|
await Promise.all(procs.map(async (proc) => {
|
|
12715
|
-
if (await Promise.race([proc.exited.then(() => true),
|
|
13634
|
+
if (await Promise.race([proc.exited.then(() => true), sleep6(STOP_GRACE_MS).then(() => false)]))
|
|
12716
13635
|
return;
|
|
12717
13636
|
proc.child.kill("SIGTERM");
|
|
12718
|
-
if (await Promise.race([proc.exited.then(() => true),
|
|
13637
|
+
if (await Promise.race([proc.exited.then(() => true), sleep6(KILL_GRACE_MS).then(() => false)]))
|
|
12719
13638
|
return;
|
|
12720
13639
|
proc.child.kill("SIGKILL");
|
|
12721
13640
|
await proc.exited;
|
|
@@ -13714,14 +14633,14 @@ async function startDaemon(options) {
|
|
|
13714
14633
|
}
|
|
13715
14634
|
|
|
13716
14635
|
// packages/claude-code/src/wrapper.ts
|
|
13717
|
-
import { spawn as
|
|
14636
|
+
import { spawn as spawn5 } from "node:child_process";
|
|
13718
14637
|
import { randomBytes as randomBytes9 } from "node:crypto";
|
|
13719
14638
|
import { closeSync as closeSync5, openSync as openSync5 } from "node:fs";
|
|
13720
14639
|
import path23 from "node:path";
|
|
13721
14640
|
// packages/claude-code/package.json
|
|
13722
14641
|
var package_default4 = {
|
|
13723
14642
|
name: "@huynle/hub-claude-code",
|
|
13724
|
-
version: "0.
|
|
14643
|
+
version: "0.9.1",
|
|
13725
14644
|
private: true,
|
|
13726
14645
|
description: "The Claude Code hooks spoke: per-user daemon, hook bridge, plugin dir, `hub claude` wrapper and daemon-backed MCP server (internal; shipped inside @huynle/hub, not published on its own).",
|
|
13727
14646
|
license: "MIT",
|
|
@@ -13734,7 +14653,7 @@ var package_default4 = {
|
|
|
13734
14653
|
|
|
13735
14654
|
// packages/claude-code/src/plugin.ts
|
|
13736
14655
|
import { createHash as createHash5 } from "node:crypto";
|
|
13737
|
-
import { existsSync as existsSync7, mkdirSync as mkdirSync9, renameSync as
|
|
14656
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync9, renameSync as renameSync6, rmSync as rmSync9, writeFileSync as writeFileSync6 } from "node:fs";
|
|
13738
14657
|
import path22 from "node:path";
|
|
13739
14658
|
var HOOK_EVENTS = [
|
|
13740
14659
|
"SessionStart",
|
|
@@ -13792,7 +14711,7 @@ function writePlugin(base, options) {
|
|
|
13792
14711
|
writeFileSync6(path22.join(tmp, file), content, { mode: 384 });
|
|
13793
14712
|
}
|
|
13794
14713
|
try {
|
|
13795
|
-
|
|
14714
|
+
renameSync6(tmp, dir);
|
|
13796
14715
|
} catch (cause) {
|
|
13797
14716
|
rmSync9(tmp, { recursive: true, force: true });
|
|
13798
14717
|
if (!existsSync7(dir))
|
|
@@ -13818,7 +14737,7 @@ function spawnDaemon(io) {
|
|
|
13818
14737
|
ensurePrivateDir(dir);
|
|
13819
14738
|
const log = openSync5(path23.join(dir, "daemon.log"), "a", 384);
|
|
13820
14739
|
try {
|
|
13821
|
-
const child =
|
|
14740
|
+
const child = spawn5(io.program.execPath, [io.program.entry, "claude-code", "daemon"], { detached: true, windowsHide: true, stdio: ["ignore", log, log], env: { ...io.env, BUN_BE_BUN: "1" } });
|
|
13822
14741
|
child.on("error", () => {});
|
|
13823
14742
|
child.unref();
|
|
13824
14743
|
} finally {
|
|
@@ -13857,7 +14776,7 @@ async function prepare(io, socket) {
|
|
|
13857
14776
|
}
|
|
13858
14777
|
function spawnClaude(bin, args, env, stderr) {
|
|
13859
14778
|
return new Promise((resolve) => {
|
|
13860
|
-
const child =
|
|
14779
|
+
const child = spawn5(bin, args, { stdio: "inherit", windowsHide: true, env });
|
|
13861
14780
|
const ignore = () => {};
|
|
13862
14781
|
const forward = () => child.kill("SIGTERM");
|
|
13863
14782
|
process.on("SIGINT", ignore);
|
|
@@ -14010,7 +14929,7 @@ import path26 from "node:path";
|
|
|
14010
14929
|
// packages/preset/package.json
|
|
14011
14930
|
var package_default5 = {
|
|
14012
14931
|
name: "@huynle/hub-preset",
|
|
14013
|
-
version: "0.
|
|
14932
|
+
version: "0.9.1",
|
|
14014
14933
|
private: true,
|
|
14015
14934
|
description: "The preset spoke: drives a harness without a spoke of its own through a terminal pane (tmux), described by a data-only preset; official presets in presets/ (internal; shipped inside @huynle/hub, not published on its own).",
|
|
14016
14935
|
license: "MIT",
|
|
@@ -14286,12 +15205,12 @@ class PresetSpoke {
|
|
|
14286
15205
|
}
|
|
14287
15206
|
|
|
14288
15207
|
// packages/preset/src/tmux.ts
|
|
14289
|
-
import { execFile } from "node:child_process";
|
|
15208
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
14290
15209
|
import { randomBytes as randomBytes10 } from "node:crypto";
|
|
14291
15210
|
var TMUX_TIMEOUT_MS = 5000;
|
|
14292
15211
|
function tmuxRunner(bin = "tmux", env = process.env, timeoutMs = TMUX_TIMEOUT_MS) {
|
|
14293
15212
|
return (args) => new Promise((resolve) => {
|
|
14294
|
-
|
|
15213
|
+
execFile2(bin, args, { env, timeout: timeoutMs, encoding: "utf8", maxBuffer: 4 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
14295
15214
|
const exit = error?.code;
|
|
14296
15215
|
const code = !error ? 0 : typeof exit === "number" ? exit : 1;
|
|
14297
15216
|
resolve({ code, stdout: stdout ?? "", stderr: stderr || (error ? error.message : "") });
|
|
@@ -14535,7 +15454,7 @@ import { openSync as openSync6, closeSync as closeSync6, readFileSync as readFil
|
|
|
14535
15454
|
// packages/codex/package.json
|
|
14536
15455
|
var package_default6 = {
|
|
14537
15456
|
name: "@huynle/hub-codex",
|
|
14538
|
-
version: "0.
|
|
15457
|
+
version: "0.9.1",
|
|
14539
15458
|
private: true,
|
|
14540
15459
|
description: "The Codex app-server spoke: attaches to Codex's app-server (its own over stdio, and the shared daemon's socket when present), and the `hub codex` wrapper (internal; shipped inside @huynle/hub, not published on its own).",
|
|
14541
15460
|
license: "MIT",
|
|
@@ -14735,7 +15654,7 @@ async function openAppServer(channel, options) {
|
|
|
14735
15654
|
|
|
14736
15655
|
// packages/codex/src/profile.ts
|
|
14737
15656
|
import { randomBytes as randomBytes11 } from "node:crypto";
|
|
14738
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync11, readdirSync as readdirSync2, renameSync as
|
|
15657
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync11, readdirSync as readdirSync2, renameSync as renameSync7, rmSync as rmSync10, writeFileSync as writeFileSync7 } from "node:fs";
|
|
14739
15658
|
import path28 from "node:path";
|
|
14740
15659
|
var CODEX_PROFILE_CAPABILITIES = Object.freeze({
|
|
14741
15660
|
instructions: true,
|
|
@@ -14829,7 +15748,7 @@ class CodexProfiles {
|
|
|
14829
15748
|
mkdirSync11(path28.join(temp, "skills", skill.name), { recursive: true, mode: 448 });
|
|
14830
15749
|
writeFileSync7(path28.join(temp, "skills", skill.name, "SKILL.md"), skillFile(skill), { mode: 384 });
|
|
14831
15750
|
}
|
|
14832
|
-
|
|
15751
|
+
renameSync7(temp, versionDir);
|
|
14833
15752
|
} catch (cause) {
|
|
14834
15753
|
rmSync10(temp, { recursive: true, force: true });
|
|
14835
15754
|
throw cause;
|
|
@@ -14863,7 +15782,7 @@ class CodexProfiles {
|
|
|
14863
15782
|
}
|
|
14864
15783
|
|
|
14865
15784
|
// packages/codex/src/sessions.ts
|
|
14866
|
-
import { statSync as
|
|
15785
|
+
import { statSync as statSync6 } from "node:fs";
|
|
14867
15786
|
import path29 from "node:path";
|
|
14868
15787
|
import { randomBytes as randomBytes13 } from "node:crypto";
|
|
14869
15788
|
|
|
@@ -14893,7 +15812,7 @@ var textInput = (text) => ({ type: "text", text, text_elements: [] });
|
|
|
14893
15812
|
function gitRoot2(cwd) {
|
|
14894
15813
|
for (let dir = path29.resolve(cwd);; dir = path29.dirname(dir)) {
|
|
14895
15814
|
try {
|
|
14896
|
-
|
|
15815
|
+
statSync6(path29.join(dir, ".git"));
|
|
14897
15816
|
return dir;
|
|
14898
15817
|
} catch {}
|
|
14899
15818
|
if (path29.dirname(dir) === dir)
|
|
@@ -15161,7 +16080,7 @@ class CodexSessions {
|
|
|
15161
16080
|
throw new Error(`The directory must be an absolute path (got ${JSON.stringify(req.directory)})`);
|
|
15162
16081
|
let isDir = false;
|
|
15163
16082
|
try {
|
|
15164
|
-
isDir =
|
|
16083
|
+
isDir = statSync6(req.directory).isDirectory();
|
|
15165
16084
|
} catch {}
|
|
15166
16085
|
if (!isDir)
|
|
15167
16086
|
throw new Error(`${req.directory} does not exist or is not a directory`);
|
|
@@ -15344,7 +16263,7 @@ function input(text, images) {
|
|
|
15344
16263
|
}
|
|
15345
16264
|
|
|
15346
16265
|
// packages/codex/src/stdio.ts
|
|
15347
|
-
import { spawn as
|
|
16266
|
+
import { spawn as spawn6 } from "node:child_process";
|
|
15348
16267
|
var STDIO_MAX_LINE = 16 * 1024 * 1024;
|
|
15349
16268
|
var KILL_GRACE_MS2 = 2000;
|
|
15350
16269
|
var LOG_LINE_MAX = 2000;
|
|
@@ -15360,7 +16279,7 @@ function spawnStdioChannel(options) {
|
|
|
15360
16279
|
cb(reason);
|
|
15361
16280
|
};
|
|
15362
16281
|
const env = Object.fromEntries(Object.entries(options.env).filter(([, v]) => v !== undefined));
|
|
15363
|
-
const child =
|
|
16282
|
+
const child = spawn6(options.command, [...options.args], { cwd: options.cwd, env, stdio: ["pipe", "pipe", "pipe"] });
|
|
15364
16283
|
let exitDone;
|
|
15365
16284
|
const exited = new Promise((resolve) => exitDone = resolve);
|
|
15366
16285
|
child.on("error", (error) => {
|
|
@@ -16021,7 +16940,7 @@ function spokeServiceStatusText(name, status) {
|
|
|
16021
16940
|
}
|
|
16022
16941
|
|
|
16023
16942
|
// packages/codex/src/wrapper.ts
|
|
16024
|
-
import { spawn as
|
|
16943
|
+
import { spawn as spawn7 } from "node:child_process";
|
|
16025
16944
|
import { closeSync as closeSync7, openSync as openSync7 } from "node:fs";
|
|
16026
16945
|
import path32 from "node:path";
|
|
16027
16946
|
var SERVICE_START_MS = 8000;
|
|
@@ -16031,7 +16950,7 @@ function spawnService(io) {
|
|
|
16031
16950
|
ensurePrivateDir2(dir);
|
|
16032
16951
|
const log = openSync7(path32.join(dir, "spoke.log"), "a", 384);
|
|
16033
16952
|
try {
|
|
16034
|
-
const child =
|
|
16953
|
+
const child = spawn7(io.program.execPath, [io.program.entry, "codex-spoke", "serve"], { detached: true, windowsHide: true, stdio: ["ignore", log, log], env: { ...io.env, BUN_BE_BUN: "1" } });
|
|
16035
16954
|
child.on("error", () => {});
|
|
16036
16955
|
child.unref();
|
|
16037
16956
|
} finally {
|
|
@@ -16060,7 +16979,7 @@ async function ensureService(io) {
|
|
|
16060
16979
|
var SIGNALS2 = { SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGKILL: 9, SIGTERM: 15 };
|
|
16061
16980
|
function spawnCodex(bin, args, env, stderr) {
|
|
16062
16981
|
return new Promise((resolve) => {
|
|
16063
|
-
const child =
|
|
16982
|
+
const child = spawn7(bin, args, { stdio: "inherit", windowsHide: true, env });
|
|
16064
16983
|
const ignore = () => {};
|
|
16065
16984
|
const forward = () => child.kill("SIGTERM");
|
|
16066
16985
|
process.on("SIGINT", ignore);
|
|
@@ -16094,7 +17013,7 @@ async function runCodex(args, given) {
|
|
|
16094
17013
|
}
|
|
16095
17014
|
|
|
16096
17015
|
// packages/cli/src/setup/commands.ts
|
|
16097
|
-
import { existsSync as existsSync9, readFileSync as readFileSync13, renameSync as
|
|
17016
|
+
import { existsSync as existsSync9, readFileSync as readFileSync13, renameSync as renameSync9, writeFileSync as writeFileSync8 } from "node:fs";
|
|
16098
17017
|
import { hostname as hostname4 } from "node:os";
|
|
16099
17018
|
import path35 from "node:path";
|
|
16100
17019
|
|
|
@@ -18019,44 +18938,9 @@ function renderYAML(vars) {
|
|
|
18019
18938
|
}
|
|
18020
18939
|
|
|
18021
18940
|
// packages/cli/src/bootstrap/discovery/node-probe.ts
|
|
18022
|
-
import childProcess from "node:child_process";
|
|
18023
|
-
import fs from "node:fs";
|
|
18024
|
-
import os from "node:os";
|
|
18025
|
-
|
|
18026
|
-
// packages/cli/src/bootstrap/discovery/util.ts
|
|
18027
|
-
function normalizeAbs(value) {
|
|
18028
|
-
if (typeof value !== "string")
|
|
18029
|
-
return;
|
|
18030
|
-
let p = value;
|
|
18031
|
-
if (/^[A-Za-z]:[\\/]/.test(p))
|
|
18032
|
-
p = p[0].toUpperCase() + p.slice(1).replace(/\\/g, "/");
|
|
18033
|
-
else if (!p.startsWith("/") || p.startsWith("//"))
|
|
18034
|
-
return;
|
|
18035
|
-
p = p.replace(/\/{2,}/g, "/");
|
|
18036
|
-
if (p.endsWith("/") && p !== "/" && !/^[A-Z]:\/$/.test(p))
|
|
18037
|
-
p = p.slice(0, -1);
|
|
18038
|
-
return isAbsolutePath(p) ? p : undefined;
|
|
18039
|
-
}
|
|
18040
|
-
function canonicalWinLink(text) {
|
|
18041
|
-
const stripped = text.replace(/^(\\\\\?\\|\\\?\?\\)/, "");
|
|
18042
|
-
return normalizeAbs(stripped) ?? text;
|
|
18043
|
-
}
|
|
18044
|
-
var probePath = (platform, text) => text !== undefined && platform === "win32" ? canonicalWinLink(text) : text;
|
|
18045
|
-
var join = (dir, rel) => dir.endsWith("/") ? `${dir}${rel}` : `${dir}/${rel}`;
|
|
18046
|
-
var decode = (bytes) => bytes ? new TextDecoder("utf-8", { fatal: false }).decode(bytes) : "";
|
|
18047
|
-
var oneLine2 = (v, max = 64) => v && v.length <= max && !/[\u0000-\u001f\u007f]/.test(v) ? v : undefined;
|
|
18048
|
-
var hostAbs = (platform, value) => {
|
|
18049
|
-
const p = normalizeAbs(value);
|
|
18050
|
-
return p && (platform !== "win32" || isWindowsPath(p)) ? p : undefined;
|
|
18051
|
-
};
|
|
18052
|
-
var underDir = (p, dir) => {
|
|
18053
|
-
const prefix = dir.endsWith("/") ? dir : `${dir}/`;
|
|
18054
|
-
return isWindowsPath(p) ? p.toLowerCase().startsWith(prefix.toLowerCase()) : p.startsWith(prefix);
|
|
18055
|
-
};
|
|
18056
|
-
var WINDOWS_COMMAND_EXTENSIONS = [".com", ".exe", ".bat", ".cmd"];
|
|
18057
|
-
var isWindowsExecutable = (path) => WINDOWS_COMMAND_EXTENSIONS.some((e) => path.toLowerCase().endsWith(e));
|
|
18058
|
-
|
|
18059
|
-
// packages/cli/src/bootstrap/discovery/node-probe.ts
|
|
18941
|
+
import childProcess from "node:child_process";
|
|
18942
|
+
import fs from "node:fs";
|
|
18943
|
+
import os from "node:os";
|
|
18060
18944
|
var MAX_READ_BYTES = 16 * 1024 * 1024;
|
|
18061
18945
|
var kindOf = (st) => st.isSymbolicLink() ? "symlink" : st.isDirectory() ? "directory" : st.isFile() ? "file" : "other";
|
|
18062
18946
|
function attempt(f) {
|
|
@@ -18155,6 +19039,10 @@ function createNodeProbe(options = {}) {
|
|
|
18155
19039
|
}
|
|
18156
19040
|
// packages/cli/src/bootstrap/profiles/desired.ts
|
|
18157
19041
|
var NO_SERVICES_ON_TARGET = "needs user services, which hubctl does not install on Windows yet";
|
|
19042
|
+
var NO_SERVICE_ON_TARGET = "needs a user service hubctl does not install on Windows yet";
|
|
19043
|
+
function servicesWithoutTemplate(profile, manager, at = "/profile/services") {
|
|
19044
|
+
return profile.services.flatMap((service, i) => EMBEDDED_ASSETS[`service/${manager}/${service}`] ? [] : [{ path: `${at}/${i}`, code: "unsupported_platform", message: `${service} ${NO_SERVICE_ON_TARGET}` }]);
|
|
19045
|
+
}
|
|
18158
19046
|
|
|
18159
19047
|
// packages/cli/src/bootstrap/preview/plan.ts
|
|
18160
19048
|
var PHASE = {
|
|
@@ -18202,6 +19090,11 @@ function planSetup(input) {
|
|
|
18202
19090
|
return failed(issuesAt("/snapshot/host/target", target.ok ? fail("", "unsupported_platform", "unsupported host") : target));
|
|
18203
19091
|
if (profile.services.length > 0 && target.value.serviceManager === "none")
|
|
18204
19092
|
return fail("/profile/services", "unsupported_platform", `${profile.id} ${NO_SERVICES_ON_TARGET}`);
|
|
19093
|
+
if (target.value.serviceManager !== "none") {
|
|
19094
|
+
const missing = servicesWithoutTemplate(profile, target.value.serviceManager);
|
|
19095
|
+
if (missing.length > 0)
|
|
19096
|
+
return failed(missing);
|
|
19097
|
+
}
|
|
18205
19098
|
if (profile.services.length > 0 && snapshot.managers.service === "none")
|
|
18206
19099
|
return fail("/profile/services", "no_service_manager", `the ${profile.id} profile installs services, but this host has no user service manager (launchd or systemd --user)`);
|
|
18207
19100
|
const planInput = { snapshot, profile };
|
|
@@ -18499,6 +19392,8 @@ function discoverRoots(probe) {
|
|
|
18499
19392
|
paths["launch-agents"] = join(home, "Library/LaunchAgents");
|
|
18500
19393
|
if (probe.platform === "linux")
|
|
18501
19394
|
paths["systemd-user"] = join(config, "systemd/user");
|
|
19395
|
+
if (windows)
|
|
19396
|
+
paths["task-defs"] = join(paths.state, "hub/tasks");
|
|
18502
19397
|
return ROOT_IDS.flatMap((id) => paths[id] ? [{ id, path: paths[id] }] : []);
|
|
18503
19398
|
}
|
|
18504
19399
|
function hubStateRoot(probe, home) {
|
|
@@ -18550,17 +19445,17 @@ function discoverLibc(probe) {
|
|
|
18550
19445
|
function discoverOS(probe) {
|
|
18551
19446
|
if (probe.platform === "darwin") {
|
|
18552
19447
|
const plist = decode(probe.readFile("/System/Library/CoreServices/SystemVersion.plist", 64 * 1024));
|
|
18553
|
-
const value = (key) =>
|
|
19448
|
+
const value = (key) => oneLine3(new RegExp(`<key>${key}</key>\\s*<string>([^<]*)</string>`).exec(plist)?.[1]);
|
|
18554
19449
|
return { name: value("ProductName") ?? "macOS", version: value("ProductVersion") ?? "unknown" };
|
|
18555
19450
|
}
|
|
18556
19451
|
if (probe.platform === "linux") {
|
|
18557
19452
|
const text = decode(probe.readFile("/etc/os-release", 64 * 1024) ?? probe.readFile("/usr/lib/os-release", 64 * 1024));
|
|
18558
|
-
const value = (key) =>
|
|
19453
|
+
const value = (key) => oneLine3(new RegExp(`^${key}=(.*)$`, "m").exec(text)?.[1]?.trim().replace(/^(["'])(.*)\1$/, "$2"));
|
|
18559
19454
|
return { name: value("NAME") ?? "Linux", version: value("VERSION_ID") ?? "unknown" };
|
|
18560
19455
|
}
|
|
18561
19456
|
if (probe.platform === "win32")
|
|
18562
|
-
return { name:
|
|
18563
|
-
return { name:
|
|
19457
|
+
return { name: oneLine3(probe.osInfo?.name) ?? "Windows", version: oneLine3(probe.osInfo?.version) ?? "unknown" };
|
|
19458
|
+
return { name: oneLine3(probe.platform, 32) ?? "unknown", version: "unknown" };
|
|
18564
19459
|
}
|
|
18565
19460
|
function discoverContainer(probe) {
|
|
18566
19461
|
if (probe.platform !== "linux")
|
|
@@ -18610,10 +19505,13 @@ function configCandidates(platform, roots, context = "host") {
|
|
|
18610
19505
|
add("launch-agents", asset.installAs, "hub", "plist");
|
|
18611
19506
|
if (platform === "linux" && asset.manager === "systemd-user")
|
|
18612
19507
|
add("systemd-user", asset.installAs, "hub", "unit");
|
|
19508
|
+
if (platform === "win32" && asset.manager === "task-scheduler")
|
|
19509
|
+
add("task-defs", asset.installAs, "hub", "xml");
|
|
18613
19510
|
}
|
|
18614
19511
|
return list.filter((c) => c !== undefined);
|
|
18615
19512
|
}
|
|
18616
19513
|
var HARNESS_BIN_DIR = { opencode: "/bin", "claude-code": "", codex: "/bin" };
|
|
19514
|
+
var systemRootOf2 = (probe) => hostAbs("win32", probe.env.SystemRoot ?? probe.env.SYSTEMROOT) ?? "C:/Windows";
|
|
18617
19515
|
function discover(probe, options) {
|
|
18618
19516
|
let roots = discoverRoots(probe);
|
|
18619
19517
|
if (!roots)
|
|
@@ -18635,6 +19533,8 @@ function discover(probe, options) {
|
|
|
18635
19533
|
service = "launchd";
|
|
18636
19534
|
if (probe.platform === "linux" && probe.lstat("/run/systemd/system") === "directory" && which(probe, [...dirs, "/usr/bin", "/bin"], "systemctl"))
|
|
18637
19535
|
service = "systemd-user";
|
|
19536
|
+
if (probe.platform === "win32" && probe.executable(join(systemRootOf2(probe), "System32/schtasks.exe")))
|
|
19537
|
+
service = "task-scheduler";
|
|
18638
19538
|
const shims = [];
|
|
18639
19539
|
const harnesses = HARNESS_ORDER.map((id) => {
|
|
18640
19540
|
const name = HARNESS_BINARIES[id];
|
|
@@ -19032,7 +19932,7 @@ function installedHarnesses(history) {
|
|
|
19032
19932
|
}
|
|
19033
19933
|
function installedServiceUnits(history, manager) {
|
|
19034
19934
|
const byFile = new Map(Object.values(EMBEDDED_ASSETS).filter((a) => a.manager === manager).map((a) => [a.installAs, a.service]));
|
|
19035
|
-
const root = manager
|
|
19935
|
+
const root = SERVICE_ROOT[manager];
|
|
19036
19936
|
const units = {};
|
|
19037
19937
|
for (const tx of history) {
|
|
19038
19938
|
if (tx.outcome !== "committed")
|
|
@@ -19153,7 +20053,7 @@ import {
|
|
|
19153
20053
|
readFileSync as readFileSync12,
|
|
19154
20054
|
readlinkSync as readlinkSync2,
|
|
19155
20055
|
readSync as readSync2,
|
|
19156
|
-
renameSync as
|
|
20056
|
+
renameSync as renameSync8,
|
|
19157
20057
|
rmdirSync,
|
|
19158
20058
|
symlinkSync as symlinkSync2,
|
|
19159
20059
|
unlinkSync as unlinkSync2,
|
|
@@ -19264,7 +20164,7 @@ var posixFs = {
|
|
|
19264
20164
|
abort: () => finish(false)
|
|
19265
20165
|
};
|
|
19266
20166
|
},
|
|
19267
|
-
rename: (from, to) =>
|
|
20167
|
+
rename: (from, to) => renameSync8(from, to),
|
|
19268
20168
|
link: (from, to) => linkSync(from, to),
|
|
19269
20169
|
unlink: (path) => unlinkSync2(path),
|
|
19270
20170
|
mkdir: (path, mode) => mkdirSync12(path, { mode }),
|
|
@@ -20355,208 +21255,6 @@ import { closeSync as closeSync9, mkdtempSync as mkdtempSync3, openSync as openS
|
|
|
20355
21255
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
20356
21256
|
import { join as join2 } from "node:path";
|
|
20357
21257
|
|
|
20358
|
-
// packages/cli/src/bootstrap/adapters/process-tree.ts
|
|
20359
|
-
import { execFile as execFile2, spawn as nodeSpawn } from "node:child_process";
|
|
20360
|
-
var TOOL_TIMEOUT_MS = 15000;
|
|
20361
|
-
var START_SLACK_MS = 2000;
|
|
20362
|
-
var FILETIME_EPOCH_MS = 11644473600000n;
|
|
20363
|
-
var defaultExec = (file, args) => new Promise((resolve) => {
|
|
20364
|
-
execFile2(file, [...args], { shell: false, windowsHide: true, timeout: TOOL_TIMEOUT_MS, encoding: "utf8", maxBuffer: 1 << 20 }, (error, stdout) => {
|
|
20365
|
-
const code = error?.code;
|
|
20366
|
-
resolve({ code: error ? typeof code === "number" ? code : null : 0, stdout: String(stdout ?? "") });
|
|
20367
|
-
});
|
|
20368
|
-
});
|
|
20369
|
-
function systemRootOf(value) {
|
|
20370
|
-
const v = value?.replace(/\\+$/, "");
|
|
20371
|
-
return v && /^[A-Za-z]:\\[A-Za-z0-9 ._\\()-]*$/.test(v) ? v : "C:\\Windows";
|
|
20372
|
-
}
|
|
20373
|
-
function processTree(deps = {}) {
|
|
20374
|
-
const platform = deps.platform ?? process.platform;
|
|
20375
|
-
if (platform === "win32") {
|
|
20376
|
-
const exec = deps.exec ?? defaultExec;
|
|
20377
|
-
const root = systemRootOf(deps.systemRoot ?? process.env.SystemRoot ?? process.env.SYSTEMROOT);
|
|
20378
|
-
const taskkill = `${root}\\System32\\taskkill.exe`;
|
|
20379
|
-
const powershell = `${root}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`;
|
|
20380
|
-
const killTree = async (pid) => {
|
|
20381
|
-
if (!Number.isInteger(pid) || pid <= 0)
|
|
20382
|
-
return;
|
|
20383
|
-
try {
|
|
20384
|
-
await exec(taskkill, ["/T", "/F", "/PID", String(pid)]);
|
|
20385
|
-
} catch {}
|
|
20386
|
-
};
|
|
20387
|
-
return {
|
|
20388
|
-
detached: false,
|
|
20389
|
-
killTree,
|
|
20390
|
-
async reap(pid, startedAt) {
|
|
20391
|
-
if (!Number.isInteger(pid) || pid <= 0)
|
|
20392
|
-
return;
|
|
20393
|
-
const since = (BigInt(Math.max(0, Math.floor(startedAt) - START_SLACK_MS)) + FILETIME_EPOCH_MS) * 10000n;
|
|
20394
|
-
const script = `$t=[DateTime]::FromFileTimeUtc(${since}); ` + `Get-CimInstance -ClassName Win32_Process -Filter 'ParentProcessId=${pid}' | ` + `Where-Object { $_.CreationDate.ToUniversalTime() -ge $t } | ForEach-Object { $_.ProcessId }`;
|
|
20395
|
-
let stdout = "";
|
|
20396
|
-
try {
|
|
20397
|
-
stdout = (await exec(powershell, ["-NoProfile", "-NonInteractive", "-Command", script])).stdout;
|
|
20398
|
-
} catch {
|
|
20399
|
-
return;
|
|
20400
|
-
}
|
|
20401
|
-
for (const line of stdout.split(/\r?\n/)) {
|
|
20402
|
-
const child = line.trim();
|
|
20403
|
-
if (/^[1-9]\d*$/.test(child))
|
|
20404
|
-
await killTree(Number(child));
|
|
20405
|
-
}
|
|
20406
|
-
}
|
|
20407
|
-
};
|
|
20408
|
-
}
|
|
20409
|
-
const kill = deps.kill ?? ((pid, signal) => void process.kill(pid, signal));
|
|
20410
|
-
const group = (pid) => {
|
|
20411
|
-
try {
|
|
20412
|
-
kill(-pid, "SIGKILL");
|
|
20413
|
-
return true;
|
|
20414
|
-
} catch {
|
|
20415
|
-
return false;
|
|
20416
|
-
}
|
|
20417
|
-
};
|
|
20418
|
-
return {
|
|
20419
|
-
detached: true,
|
|
20420
|
-
async killTree(pid) {
|
|
20421
|
-
if (!Number.isInteger(pid) || pid <= 0)
|
|
20422
|
-
return;
|
|
20423
|
-
if (group(pid))
|
|
20424
|
-
return;
|
|
20425
|
-
try {
|
|
20426
|
-
kill(pid, "SIGKILL");
|
|
20427
|
-
} catch {}
|
|
20428
|
-
},
|
|
20429
|
-
async reap(pid) {
|
|
20430
|
-
if (Number.isInteger(pid) && pid > 0)
|
|
20431
|
-
group(pid);
|
|
20432
|
-
}
|
|
20433
|
-
};
|
|
20434
|
-
}
|
|
20435
|
-
var DRAIN_GRACE_MS = 1000;
|
|
20436
|
-
function runBounded(request, options = {}) {
|
|
20437
|
-
const tree = options.tree ?? processTree();
|
|
20438
|
-
const spawn = options.spawn ?? nodeSpawn;
|
|
20439
|
-
const now = options.now ?? Date.now;
|
|
20440
|
-
const env = Object.fromEntries(Object.entries(request.env).filter((e) => typeof e[1] === "string"));
|
|
20441
|
-
return new Promise((resolve) => {
|
|
20442
|
-
let timedOut = false;
|
|
20443
|
-
let aborted = false;
|
|
20444
|
-
let overflowed = false;
|
|
20445
|
-
let ended = false;
|
|
20446
|
-
const out = [];
|
|
20447
|
-
const err = [];
|
|
20448
|
-
const size = { out: 0, err: 0 };
|
|
20449
|
-
const result = (exitCode, spawnError) => ({
|
|
20450
|
-
exitCode,
|
|
20451
|
-
stdout: Buffer.concat(out).toString("utf8"),
|
|
20452
|
-
stderr: Buffer.concat(err).toString("utf8"),
|
|
20453
|
-
timedOut,
|
|
20454
|
-
aborted,
|
|
20455
|
-
overflowed,
|
|
20456
|
-
...spawnError ? { spawnError } : {}
|
|
20457
|
-
});
|
|
20458
|
-
if (request.signal?.aborted) {
|
|
20459
|
-
aborted = true;
|
|
20460
|
-
return resolve(result(null));
|
|
20461
|
-
}
|
|
20462
|
-
const startedAt = now();
|
|
20463
|
-
let child;
|
|
20464
|
-
try {
|
|
20465
|
-
child = spawn(request.file, [...request.args], {
|
|
20466
|
-
shell: false,
|
|
20467
|
-
env,
|
|
20468
|
-
stdio: ["ignore", "pipe", request.stderr ? "pipe" : "ignore"],
|
|
20469
|
-
windowsHide: true,
|
|
20470
|
-
detached: tree.detached
|
|
20471
|
-
});
|
|
20472
|
-
} catch (error) {
|
|
20473
|
-
return resolve(result(null, error));
|
|
20474
|
-
}
|
|
20475
|
-
let settled = false;
|
|
20476
|
-
let exited = false;
|
|
20477
|
-
const pid = child.pid;
|
|
20478
|
-
const end = () => {
|
|
20479
|
-
if (ended || pid === undefined)
|
|
20480
|
-
return;
|
|
20481
|
-
ended = true;
|
|
20482
|
-
tree.killTree(pid);
|
|
20483
|
-
};
|
|
20484
|
-
const timer = setTimeout(() => {
|
|
20485
|
-
timedOut = true;
|
|
20486
|
-
end();
|
|
20487
|
-
}, request.timeoutMs);
|
|
20488
|
-
const onAbort = () => {
|
|
20489
|
-
aborted = true;
|
|
20490
|
-
end();
|
|
20491
|
-
};
|
|
20492
|
-
request.signal?.addEventListener("abort", onAbort, { once: true });
|
|
20493
|
-
const finish = (r) => {
|
|
20494
|
-
if (settled)
|
|
20495
|
-
return;
|
|
20496
|
-
settled = true;
|
|
20497
|
-
clearTimeout(timer);
|
|
20498
|
-
request.signal?.removeEventListener("abort", onAbort);
|
|
20499
|
-
resolve(r);
|
|
20500
|
-
};
|
|
20501
|
-
const collect = (into, key) => (chunk) => {
|
|
20502
|
-
const room = request.maxOutputBytes - size[key];
|
|
20503
|
-
if (chunk.length > room) {
|
|
20504
|
-
overflowed = true;
|
|
20505
|
-
if (room > 0)
|
|
20506
|
-
into.push(chunk.subarray(0, room));
|
|
20507
|
-
size[key] = request.maxOutputBytes;
|
|
20508
|
-
if ((request.overflow ?? "kill") === "kill")
|
|
20509
|
-
end();
|
|
20510
|
-
return;
|
|
20511
|
-
}
|
|
20512
|
-
into.push(chunk);
|
|
20513
|
-
size[key] += chunk.length;
|
|
20514
|
-
};
|
|
20515
|
-
child.stdout?.on("data", collect(out, "out"));
|
|
20516
|
-
child.stderr?.on("data", collect(err, "err"));
|
|
20517
|
-
const streams = [child.stdout, child.stderr].filter((s) => Boolean(s));
|
|
20518
|
-
const drained = () => new Promise((done) => {
|
|
20519
|
-
let open = streams.filter((s) => !s.readableEnded && !s.destroyed).length;
|
|
20520
|
-
if (open === 0)
|
|
20521
|
-
return done();
|
|
20522
|
-
const grace = setTimeout(() => {
|
|
20523
|
-
for (const s of streams)
|
|
20524
|
-
s.destroy();
|
|
20525
|
-
done();
|
|
20526
|
-
}, DRAIN_GRACE_MS);
|
|
20527
|
-
for (const s of streams) {
|
|
20528
|
-
if (s.readableEnded || s.destroyed)
|
|
20529
|
-
continue;
|
|
20530
|
-
const one = () => {
|
|
20531
|
-
if (--open === 0) {
|
|
20532
|
-
clearTimeout(grace);
|
|
20533
|
-
done();
|
|
20534
|
-
}
|
|
20535
|
-
};
|
|
20536
|
-
s.once("end", one);
|
|
20537
|
-
s.once("close", () => s.readableEnded ? undefined : one());
|
|
20538
|
-
}
|
|
20539
|
-
});
|
|
20540
|
-
child.on("error", (error) => {
|
|
20541
|
-
if (!exited)
|
|
20542
|
-
finish(result(null, error));
|
|
20543
|
-
});
|
|
20544
|
-
child.on("exit", (code) => {
|
|
20545
|
-
if (exited)
|
|
20546
|
-
return;
|
|
20547
|
-
exited = true;
|
|
20548
|
-
clearTimeout(timer);
|
|
20549
|
-
const exitCode = typeof code === "number" && !ended ? code : null;
|
|
20550
|
-
(async () => {
|
|
20551
|
-
if (pid !== undefined)
|
|
20552
|
-
await tree.reap(pid, startedAt);
|
|
20553
|
-
await drained();
|
|
20554
|
-
finish(result(exitCode));
|
|
20555
|
-
})();
|
|
20556
|
-
});
|
|
20557
|
-
});
|
|
20558
|
-
}
|
|
20559
|
-
|
|
20560
21258
|
// packages/cli/src/bootstrap/adapters/claude/ports.ts
|
|
20561
21259
|
class DownloadTooLarge extends Error {
|
|
20562
21260
|
constructor(url, maxBytes) {
|
|
@@ -21282,7 +21980,7 @@ var OIDC_ISSUER_V2 = "1.3.6.1.4.1.57264.1.8";
|
|
|
21282
21980
|
var OIDC_ISSUER_V1 = "1.3.6.1.4.1.57264.1.1";
|
|
21283
21981
|
var isObj5 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
21284
21982
|
var b64 = (s) => typeof s === "string" && /^[A-Za-z0-9+/]*={0,2}$/.test(s) ? Buffer.from(s, "base64") : undefined;
|
|
21285
|
-
var
|
|
21983
|
+
var within4 = (t, v) => t >= v.start && (v.end === undefined || t <= v.end);
|
|
21286
21984
|
function tlv(buf, at) {
|
|
21287
21985
|
const tag = buf[at];
|
|
21288
21986
|
let len = buf[at + 1];
|
|
@@ -21417,7 +22115,7 @@ function verifySigstoreBundle(bundle, artifact, options) {
|
|
|
21417
22115
|
const t = b.payload.integratedTime * 1000;
|
|
21418
22116
|
if (t < Date.parse(leaf.validFrom) || t > Date.parse(leaf.validTo))
|
|
21419
22117
|
return failed2("the entry was logged outside the certificate's validity");
|
|
21420
|
-
if (!
|
|
22118
|
+
if (!within4(t, authority.validFor))
|
|
21421
22119
|
return failed2("the entry was logged outside the CA's validity");
|
|
21422
22120
|
const sans = (leaf.subjectAltName ?? "").split(/,\s*/);
|
|
21423
22121
|
if (!sans.includes(`URI:${identity.san}`))
|
|
@@ -21437,7 +22135,7 @@ function verifySigstoreBundle(bundle, artifact, options) {
|
|
|
21437
22135
|
const log = trust.logs.find((l) => l.logID === b.payload.logID.toLowerCase());
|
|
21438
22136
|
if (!log)
|
|
21439
22137
|
return unavailable("the entry is in a transparency log this hubctl does not trust");
|
|
21440
|
-
if (!
|
|
22138
|
+
if (!within4(t, log.validFor))
|
|
21441
22139
|
return failed2("the entry was logged outside the log key's validity");
|
|
21442
22140
|
if (!verifySET(log, b))
|
|
21443
22141
|
return failed2("the transparency log's signed entry timestamp does not verify");
|
|
@@ -21574,13 +22272,50 @@ var labelOfTarget = (word) => {
|
|
|
21574
22272
|
const at = word?.lastIndexOf("/") ?? -1;
|
|
21575
22273
|
return at > 0 && DOMAIN.test(word.slice(0, at)) ? word.slice(at + 1) : undefined;
|
|
21576
22274
|
};
|
|
22275
|
+
var TASK_FILES = new Set(Object.values(EMBEDDED_ASSETS).flatMap((a) => a.manager === "task-scheduler" ? [a.installAs] : []));
|
|
22276
|
+
var TASK_FILE = /^[A-Z]:\\(?:[^\\/:*?"<>|]+\\)*([^\\/:*?"<>|]+)$/;
|
|
22277
|
+
function taskQueryWords(name) {
|
|
22278
|
+
if (!TASK_NAME.test(name))
|
|
22279
|
+
throw new Error(`not a hub task name: ${JSON.stringify(name)}`);
|
|
22280
|
+
const script = "$ErrorActionPreference='Stop';$s=New-Object -ComObject Schedule.Service;$s.Connect();" + `$t=$s.GetFolder('\\').GetTask('${name}');` + "'state='+[int]$t.State;'enabled='+$t.Enabled;'result='+$t.LastTaskResult;" + "foreach($r in $t.GetInstances(0)){'pid='+$r.EnginePID}";
|
|
22281
|
+
return ["-NoProfile", "-NonInteractive", "-Command", script];
|
|
22282
|
+
}
|
|
22283
|
+
function checkWindowsCall(program, words) {
|
|
22284
|
+
if (program === "taskkill")
|
|
22285
|
+
return words.length === 4 && words[0] === "/T" && words[1] === "/F" && words[2] === "/PID" && /^[1-9][0-9]{0,9}$/.test(words[3]);
|
|
22286
|
+
if (program === "powershell") {
|
|
22287
|
+
const name = /GetTask\('([^']*)'\)/.exec(words[words.length - 1] ?? "")?.[1];
|
|
22288
|
+
if (!name || !TASK_NAME.test(name))
|
|
22289
|
+
return false;
|
|
22290
|
+
const expected = taskQueryWords(name);
|
|
22291
|
+
return words.length === expected.length && words.every((w, i) => w === expected[i]);
|
|
22292
|
+
}
|
|
22293
|
+
const [verb, tn, name] = words;
|
|
22294
|
+
if (tn !== "/TN" || !TASK_NAME.test(name ?? ""))
|
|
22295
|
+
return false;
|
|
22296
|
+
if (verb === "/Run" || verb === "/End")
|
|
22297
|
+
return words.length === 3;
|
|
22298
|
+
if (verb === "/Delete")
|
|
22299
|
+
return words.length === 4 && words[3] === "/F";
|
|
22300
|
+
if (verb === "/Query")
|
|
22301
|
+
return words.length === 4 && words[3] === "/XML";
|
|
22302
|
+
if (verb === "/Create")
|
|
22303
|
+
return words.length === 6 && words[3] === "/XML" && words[5] === "/F" && TASK_FILES.has(TASK_FILE.exec(words[4])?.[1] ?? "");
|
|
22304
|
+
return false;
|
|
22305
|
+
}
|
|
22306
|
+
var WINDOWS_PROGRAMS = new Set(["schtasks", "powershell", "taskkill"]);
|
|
21577
22307
|
function checkManagerCall(call) {
|
|
21578
|
-
if (!isRecord2(call) || !Array.isArray(call.words)
|
|
22308
|
+
if (!isRecord2(call) || !Array.isArray(call.words))
|
|
22309
|
+
return false;
|
|
22310
|
+
const windows = typeof call.program === "string" && WINDOWS_PROGRAMS.has(call.program);
|
|
22311
|
+
if (call.words.length > (windows ? 6 : 4))
|
|
21579
22312
|
return false;
|
|
21580
22313
|
const w = call.words;
|
|
21581
22314
|
if (!w.every((x) => typeof x === "string" && x.length > 0 && x.length <= 4096 && !CONTROL5.test(x)))
|
|
21582
22315
|
return false;
|
|
21583
22316
|
const words = w;
|
|
22317
|
+
if (windows)
|
|
22318
|
+
return checkWindowsCall(call.program, words);
|
|
21584
22319
|
if (call.program === "systemctl") {
|
|
21585
22320
|
if (words[0] !== "--user")
|
|
21586
22321
|
return false;
|
|
@@ -21614,6 +22349,24 @@ function unitPathResolver(roots, context) {
|
|
|
21614
22349
|
return where.ok ? where.value : undefined;
|
|
21615
22350
|
};
|
|
21616
22351
|
}
|
|
22352
|
+
var taskFields = (output) => showFields(output.replaceAll("\r", ""));
|
|
22353
|
+
var taskPids = (output) => output.replaceAll("\r", "").split(`
|
|
22354
|
+
`).flatMap((line) => /^pid=([1-9][0-9]{0,9})$/.exec(line.trim()) ? [Number(line.trim().slice(4))] : []);
|
|
22355
|
+
function taskSchedulerHealth(output) {
|
|
22356
|
+
const fields = taskFields(output);
|
|
22357
|
+
const state = /^[0-4]$/.test(fields.state ?? "") ? Number(fields.state) : undefined;
|
|
22358
|
+
const result = /^-?[0-9]{1,10}$/.test(fields.result ?? "") ? Number(fields.result) >>> 0 : undefined;
|
|
22359
|
+
const pid = taskPids(output)[0];
|
|
22360
|
+
if (state === 4)
|
|
22361
|
+
return { state: "running", restarts: 0, detail: pid ? `running, pid ${pid}` : "running" };
|
|
22362
|
+
if (state === 2)
|
|
22363
|
+
return { state: "starting", restarts: 0, detail: "queued" };
|
|
22364
|
+
if (state === 1)
|
|
22365
|
+
return { state: "stopped", restarts: 0, detail: "disabled" };
|
|
22366
|
+
if (state === 3)
|
|
22367
|
+
return { state: result === 267011 ? "starting" : "stopped", restarts: 0, detail: `ready, last result 0x${(result ?? 0).toString(16)}` };
|
|
22368
|
+
return { state: "stopped", restarts: 0, detail: "unknown" };
|
|
22369
|
+
}
|
|
21617
22370
|
var ok4 = txOk(undefined);
|
|
21618
22371
|
function showFields(output) {
|
|
21619
22372
|
const fields = {};
|
|
@@ -21775,20 +22528,84 @@ function createServiceControl(options) {
|
|
|
21775
22528
|
return await call("launchctl", "print", `${domain}/${label}`) === 0 ? txFail("internal", `${label} is still loaded after launchctl bootout`) : ok4;
|
|
21776
22529
|
}
|
|
21777
22530
|
};
|
|
21778
|
-
const
|
|
22531
|
+
const taskOf = (service) => {
|
|
22532
|
+
const name = options.taskName?.(service);
|
|
22533
|
+
return name && TASK_NAME.test(name) ? name : undefined;
|
|
22534
|
+
};
|
|
22535
|
+
const noTask = (service) => txFail("invalid", `this hubctl has no Task Scheduler task name for the ${service} service`);
|
|
22536
|
+
const tasks = {
|
|
22537
|
+
async query(name) {
|
|
22538
|
+
const r = await options.run(checked("powershell", ...taskQueryWords(name)));
|
|
22539
|
+
return r.exitCode === 0 ? r.output ?? "" : undefined;
|
|
22540
|
+
},
|
|
22541
|
+
async endTrees(name) {
|
|
22542
|
+
for (const pid of taskPids(await tasks.query(name) ?? ""))
|
|
22543
|
+
await call("taskkill", "/T", "/F", "/PID", String(pid));
|
|
22544
|
+
},
|
|
22545
|
+
async state(service) {
|
|
22546
|
+
const name = taskOf(service);
|
|
22547
|
+
if (!name)
|
|
22548
|
+
return noTask(service);
|
|
22549
|
+
const r = await options.run(checked("schtasks", "/Query", "/TN", name, "/XML"));
|
|
22550
|
+
if (r.exitCode !== 0)
|
|
22551
|
+
return txOk({ loaded: false, enabled: false });
|
|
22552
|
+
const enabled = /<Settings>[\s\S]*?<Enabled>(true|false)<\/Enabled>/.exec(r.output ?? "")?.[1] !== "false";
|
|
22553
|
+
return txOk({ loaded: true, enabled });
|
|
22554
|
+
},
|
|
22555
|
+
async activate(service) {
|
|
22556
|
+
const name = taskOf(service);
|
|
22557
|
+
if (!name)
|
|
22558
|
+
return noTask(service);
|
|
22559
|
+
const file = options.unitPath(service, "task-scheduler");
|
|
22560
|
+
if (!file)
|
|
22561
|
+
return txFail("invalid", `this host has no task-defs directory for ${name}`);
|
|
22562
|
+
await tasks.endTrees(name);
|
|
22563
|
+
const created = await call("schtasks", "/Create", "/TN", name, "/XML", toNativePath(file, "win32"), "/F");
|
|
22564
|
+
if (created !== 0)
|
|
22565
|
+
return txFail("internal", `schtasks /Create of ${name} failed (exit ${created})`);
|
|
22566
|
+
const run = await call("schtasks", "/Run", "/TN", name);
|
|
22567
|
+
if (run !== 0)
|
|
22568
|
+
return txFail("internal", `schtasks /Run of ${name} failed (exit ${run})`);
|
|
22569
|
+
return settle(() => tasks.health(service), name, `schtasks /Query /TN ${name} /V /FO LIST and the Task Scheduler history`);
|
|
22570
|
+
},
|
|
22571
|
+
async health(service) {
|
|
22572
|
+
const name = taskOf(service);
|
|
22573
|
+
if (!name)
|
|
22574
|
+
return noTask(service);
|
|
22575
|
+
const output = await tasks.query(name);
|
|
22576
|
+
return txOk(output === undefined ? { state: "stopped", restarts: 0, detail: "not registered" } : taskSchedulerHealth(output));
|
|
22577
|
+
},
|
|
22578
|
+
async deactivate(service) {
|
|
22579
|
+
const name = taskOf(service);
|
|
22580
|
+
if (!name)
|
|
22581
|
+
return noTask(service);
|
|
22582
|
+
await tasks.endTrees(name);
|
|
22583
|
+
await call("schtasks", "/End", "/TN", name);
|
|
22584
|
+
await call("schtasks", "/Delete", "/TN", name, "/F");
|
|
22585
|
+
return await call("schtasks", "/Query", "/TN", name, "/XML") === 0 ? txFail("internal", `${name} is still registered after schtasks /Delete`) : ok4;
|
|
22586
|
+
}
|
|
22587
|
+
};
|
|
22588
|
+
const by = (manager, calls) => guarded(() => {
|
|
22589
|
+
const f = Object.hasOwn(calls, manager) ? calls[manager] : undefined;
|
|
22590
|
+
if (!f)
|
|
22591
|
+
throw new Error(`unknown service manager ${String(manager)}`);
|
|
22592
|
+
return f();
|
|
22593
|
+
});
|
|
22594
|
+
const deactivate = (service, manager) => by(manager, { launchd: () => launchd.deactivate(service), "systemd-user": () => systemd.restore(service, { loaded: false, enabled: false }), "task-scheduler": () => tasks.deactivate(service) });
|
|
21779
22595
|
return {
|
|
21780
|
-
state: (service, manager) =>
|
|
21781
|
-
activate: (service, manager) =>
|
|
22596
|
+
state: (service, manager) => by(manager, { launchd: () => launchd.state(service), "systemd-user": () => systemd.state(service), "task-scheduler": () => tasks.state(service) }),
|
|
22597
|
+
activate: (service, manager) => by(manager, { launchd: () => launchd.activate(service), "systemd-user": () => systemd.activate(service), "task-scheduler": () => tasks.activate(service) }),
|
|
21782
22598
|
deactivate,
|
|
21783
|
-
restore: (service, manager, prior) => prior.loaded ?
|
|
21784
|
-
health: (service, manager) =>
|
|
22599
|
+
restore: (service, manager, prior) => prior.loaded ? by(manager, { launchd: async () => ok4, "systemd-user": () => systemd.restore(service, prior), "task-scheduler": async () => ok4 }) : deactivate(service, manager),
|
|
22600
|
+
health: (service, manager) => by(manager, { launchd: () => launchd.health(service), "systemd-user": () => systemd.health(service), "task-scheduler": () => tasks.health(service) })
|
|
21785
22601
|
};
|
|
21786
22602
|
}
|
|
21787
22603
|
var MAX_OUTPUT2 = 64 * 1024;
|
|
21788
|
-
var capturesOutput = (call) => call.program === "systemctl" ? call.words[1] === "show" : call.words[0] === "print";
|
|
22604
|
+
var capturesOutput = (call) => call.program === "systemctl" ? call.words[1] === "show" : call.program === "launchctl" ? call.words[0] === "print" : call.program === "powershell" || call.program === "schtasks" && call.words[0] === "/Query";
|
|
21789
22605
|
var BUS_ENV = ["XDG_RUNTIME_DIR", "DBUS_SESSION_BUS_ADDRESS"];
|
|
22606
|
+
var WINDOWS_ENV = ["SystemRoot", "windir", "SystemDrive", "TEMP", "TMP", "USERPROFILE", "LOCALAPPDATA", "APPDATA", "USERNAME", "USERDOMAIN"];
|
|
21790
22607
|
var defaultSpawn = (file, words, options) => new Promise((resolve) => {
|
|
21791
|
-
const child = nodeSpawn2(file, [...words], { shell: false, env: { ...options.env }, stdio: ["ignore", options.capture ? "pipe" : "ignore", "ignore"], timeout: options.timeoutMs, killSignal: "SIGKILL" });
|
|
22608
|
+
const child = nodeSpawn2(file, [...words], { shell: false, windowsHide: true, env: { ...options.env }, stdio: ["ignore", options.capture ? "pipe" : "ignore", "ignore"], timeout: options.timeoutMs, killSignal: "SIGKILL" });
|
|
21792
22609
|
let output = "";
|
|
21793
22610
|
child.stdout?.setEncoding("utf8");
|
|
21794
22611
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -21800,13 +22617,16 @@ var defaultSpawn = (file, words, options) => new Promise((resolve) => {
|
|
|
21800
22617
|
child.on("close", (code, signal) => done(code ?? (signal ? 128 : 1)));
|
|
21801
22618
|
});
|
|
21802
22619
|
function spawnManagerRunner(options) {
|
|
21803
|
-
|
|
22620
|
+
const given = Object.keys(options.programs);
|
|
22621
|
+
const windows = given.some((p) => WINDOWS_PROGRAMS.has(p));
|
|
22622
|
+
for (const program of windows ? given : ["systemctl", "launchctl"]) {
|
|
21804
22623
|
const file = options.programs[program];
|
|
21805
|
-
|
|
22624
|
+
const base = file === undefined ? "" : path34.basename(file);
|
|
22625
|
+
if (!isAbsolutePath(file) || base !== program && base.toLowerCase() !== `${program}.exe`)
|
|
21806
22626
|
throw new Error(`${program} must be given as an absolute path to ${program}`);
|
|
21807
22627
|
}
|
|
21808
22628
|
const env = {};
|
|
21809
|
-
for (const key of BUS_ENV)
|
|
22629
|
+
for (const key of windows ? WINDOWS_ENV : BUS_ENV)
|
|
21810
22630
|
if (options.env?.[key])
|
|
21811
22631
|
env[key] = options.env[key];
|
|
21812
22632
|
const spawn = options.spawn ?? defaultSpawn;
|
|
@@ -21815,7 +22635,10 @@ function spawnManagerRunner(options) {
|
|
|
21815
22635
|
if (!checkManagerCall(call))
|
|
21816
22636
|
throw new Error(`refused a service manager call outside the fixed set (${String(call.program)})`);
|
|
21817
22637
|
const capture = capturesOutput(call);
|
|
21818
|
-
const
|
|
22638
|
+
const file = options.programs[call.program];
|
|
22639
|
+
if (!file)
|
|
22640
|
+
throw new Error(`this host's service manager does not use ${call.program}`);
|
|
22641
|
+
const r = await spawn(file, [...call.words], { shell: false, env, timeoutMs, ...capture ? { capture: true } : {} });
|
|
21819
22642
|
if (typeof r === "number")
|
|
21820
22643
|
return { exitCode: r };
|
|
21821
22644
|
return capture ? { exitCode: r.exitCode, output: r.output.slice(0, MAX_OUTPUT2) } : { exitCode: r.exitCode };
|
|
@@ -21994,8 +22817,12 @@ function launchSessionExecutor(probe) {
|
|
|
21994
22817
|
var DEFAULT_SLOTS2 = 4;
|
|
21995
22818
|
var BOOTSTRAP_SLOTS_MAX = 64;
|
|
21996
22819
|
var RESERVED_LABELS = ["os", "arch"];
|
|
21997
|
-
var
|
|
21998
|
-
var
|
|
22820
|
+
var folded = (p) => isWindowsPath(p) ? p.toLowerCase() : p;
|
|
22821
|
+
var inside = (path, dir) => {
|
|
22822
|
+
const [p, d] = [folded(path), folded(dir)];
|
|
22823
|
+
return p === d || p.startsWith(d.endsWith("/") ? d : `${d}/`);
|
|
22824
|
+
};
|
|
22825
|
+
var parentOf3 = (path) => folded(path.slice(0, path.lastIndexOf("/")) || "/");
|
|
21999
22826
|
function otherViews(roots, context) {
|
|
22000
22827
|
return roots.flatMap((r) => r.containerPath !== undefined && r.containerPath !== r.path ? [context === "container" ? r.path : r.containerPath] : []);
|
|
22001
22828
|
}
|
|
@@ -22118,6 +22945,7 @@ function serviceContextOf(input) {
|
|
|
22118
22945
|
// packages/cli/src/bootstrap/adapters/node-services/plan.ts
|
|
22119
22946
|
var NODE_CONFIG_REF = { root: "config", rel: "hub/node.json" };
|
|
22120
22947
|
var CONFIG_DIR = { root: "config", rel: "hub" };
|
|
22948
|
+
var UNIT_KIND = { launchd: "launchd agent", "systemd-user": "systemd --user unit", "task-scheduler": "logon task" };
|
|
22121
22949
|
var LABEL3 = { node: "hub node", "claude-code": "Claude Code spoke", "codex-spoke": "Codex spoke" };
|
|
22122
22950
|
var SERVICE_HARNESS = Object.fromEntries(Object.entries(HARNESS_SERVICE).map(([h, s]) => [s, h]));
|
|
22123
22951
|
var BIN_SETTING = { "claude-code": "HUB_CLAUDE_BIN", codex: "HUB_CODEX_BIN" };
|
|
@@ -22194,9 +23022,19 @@ function planNodeServices(input, options) {
|
|
|
22194
23022
|
}
|
|
22195
23023
|
}
|
|
22196
23024
|
const units = [];
|
|
23025
|
+
const uncarried = new Set;
|
|
22197
23026
|
for (const service of profile.services) {
|
|
22198
|
-
|
|
22199
|
-
|
|
23027
|
+
let environment = environmentFor(service, servedBy(service, profile.harnesses), ctx, options.environment);
|
|
23028
|
+
if (ctx.manager === "task-scheduler") {
|
|
23029
|
+
for (const key of Object.keys(environment))
|
|
23030
|
+
uncarried.add(key);
|
|
23031
|
+
environment = {};
|
|
23032
|
+
if (!options.task) {
|
|
23033
|
+
issues.push({ path: `/services/${service}/task`, code: "missing_field", message: "a Windows logon task needs its name and your SID; hubctl could not find them (USERNAME, whoami)" });
|
|
23034
|
+
continue;
|
|
23035
|
+
}
|
|
23036
|
+
}
|
|
23037
|
+
const unit = renderServiceUnit({ service, manager: ctx.manager, layout: options.layout, home: ctx.home, environment, ...ctx.manager === "task-scheduler" ? { task: options.task } : {} });
|
|
22200
23038
|
if (!unit.ok) {
|
|
22201
23039
|
issues.push(...prefixed(`/services/${service}`, unit));
|
|
22202
23040
|
continue;
|
|
@@ -22206,7 +23044,7 @@ function planNodeServices(input, options) {
|
|
|
22206
23044
|
id: `install-${service}-service`,
|
|
22207
23045
|
kind: "install-service",
|
|
22208
23046
|
escalation: "user",
|
|
22209
|
-
summary: `Write the ${LABEL3[service]} ${ctx.manager
|
|
23047
|
+
summary: `Write the ${LABEL3[service]} ${UNIT_KIND[ctx.manager]} (${unit.value.name})`,
|
|
22210
23048
|
service,
|
|
22211
23049
|
manager: ctx.manager,
|
|
22212
23050
|
path: unit.value.path,
|
|
@@ -22238,7 +23076,7 @@ function planNodeServices(input, options) {
|
|
|
22238
23076
|
}
|
|
22239
23077
|
if (issues.length > 0)
|
|
22240
23078
|
return failed3(issues);
|
|
22241
|
-
return { ok: true, value: { actions, units, ...settings ? { settings } : {}, context: ctx, notes: notesFor(options, ctx) } };
|
|
23079
|
+
return { ok: true, value: { actions, units, ...settings ? { settings } : {}, context: ctx, notes: notesFor(options, ctx, [...uncarried].sort()) } };
|
|
22242
23080
|
}
|
|
22243
23081
|
function checkLaunch(input, options, ctx, settings) {
|
|
22244
23082
|
const launch = options.launch;
|
|
@@ -22256,11 +23094,15 @@ function checkLaunch(input, options, ctx, settings) {
|
|
|
22256
23094
|
return [{ path: "/launch/workspace", code: "invalid_path", message: `${where.value} is outside the node's workspace roots` }];
|
|
22257
23095
|
return [];
|
|
22258
23096
|
}
|
|
22259
|
-
function notesFor(options, ctx) {
|
|
23097
|
+
function notesFor(options, ctx, uncarried) {
|
|
22260
23098
|
const notes = [];
|
|
22261
23099
|
if (!options.activate)
|
|
22262
23100
|
notes.push("The services are installed but not loaded: pass --confirm-activation to load them (hubctl never loads a service without it).");
|
|
22263
|
-
if (ctx.manager === "
|
|
23101
|
+
if (ctx.manager === "task-scheduler") {
|
|
23102
|
+
notes.push("The hub node runs as a logon task for you (no window, no administrator rights, no stored password): Windows starts it at each logon and again within a minute after it exits; it runs while you are logged in.");
|
|
23103
|
+
if (uncarried.length > 0)
|
|
23104
|
+
notes.push(`The logon task does not see ${uncarried.join(", ")} from this shell (a task has no environment of its own); set them as user environment variables if the node needs them.`);
|
|
23105
|
+
} else if (ctx.manager === "systemd-user")
|
|
22264
23106
|
notes.push(options.linger ? "To keep the services running while you are logged out, run `loginctl enable-linger` yourself; hubctl does not change login settings." : "systemd --user services stop when you log out; keeping them running (linger) is opt-in.");
|
|
22265
23107
|
else
|
|
22266
23108
|
notes.push("launchd user agents run while you are logged in.");
|
|
@@ -22566,7 +23408,6 @@ function createOpencodeHarness(options) {
|
|
|
22566
23408
|
}
|
|
22567
23409
|
|
|
22568
23410
|
// packages/cli/src/bootstrap/profiles/upgrade.ts
|
|
22569
|
-
var UNIT_ROOT2 = { launchd: "launch-agents", "systemd-user": "systemd-user" };
|
|
22570
23411
|
function removeServiceActions(service, facts) {
|
|
22571
23412
|
const actions = [
|
|
22572
23413
|
{ id: `deactivate-${service}`, kind: "deactivate-service", escalation: "user", summary: `Unload the hub ${service} service (profile downgrade)`, service, manager: facts.manager }
|
|
@@ -22581,7 +23422,7 @@ function removeServiceActions(service, facts) {
|
|
|
22581
23422
|
summary: `Remove the hub ${service} ${facts.manager} unit (profile downgrade)`,
|
|
22582
23423
|
service,
|
|
22583
23424
|
manager: facts.manager,
|
|
22584
|
-
path: { root:
|
|
23425
|
+
path: { root: SERVICE_ROOT[facts.manager], rel: asset.installAs },
|
|
22585
23426
|
sha256
|
|
22586
23427
|
});
|
|
22587
23428
|
return actions;
|
|
@@ -23784,6 +24625,10 @@ function codexSpokeStatus(options) {
|
|
|
23784
24625
|
}
|
|
23785
24626
|
|
|
23786
24627
|
// packages/cli/src/setup/runtime.ts
|
|
24628
|
+
function windowsServicePrograms(systemRoot) {
|
|
24629
|
+
const dir = `${systemRoot.replace(/\/+$/, "")}/System32`;
|
|
24630
|
+
return { schtasks: `${dir}/schtasks.exe`, powershell: `${dir}/WindowsPowerShell/v1.0/powershell.exe`, taskkill: `${dir}/taskkill.exe` };
|
|
24631
|
+
}
|
|
23787
24632
|
function readinessGate(consent, harness) {
|
|
23788
24633
|
return { requireReady: consent.requireReady === true || (consent.strict ?? []).includes(harness), ...consent.onReadiness ? { onReadiness: consent.onReadiness } : {} };
|
|
23789
24634
|
}
|
|
@@ -23952,7 +24797,8 @@ function createSetupRuntime(deps) {
|
|
|
23952
24797
|
...choices.node ? { node: choices.node } : {},
|
|
23953
24798
|
...choices.activate ? { activate: true } : {},
|
|
23954
24799
|
environment: deps.env,
|
|
23955
|
-
...choices.launch ? { launch: choices.launch } : {}
|
|
24800
|
+
...choices.launch ? { launch: choices.launch } : {},
|
|
24801
|
+
...input.snapshot.managers.service === "task-scheduler" && input.profile.services.length > 0 ? { task: taskIdentity() } : {}
|
|
23956
24802
|
});
|
|
23957
24803
|
if (!planned.ok)
|
|
23958
24804
|
return planned;
|
|
@@ -23973,15 +24819,28 @@ function createSetupRuntime(deps) {
|
|
|
23973
24819
|
];
|
|
23974
24820
|
return { sources: { adapters: registry, contributors, ...choices.enrollment ? { enrollment: choices.enrollment } : {} }, node: () => nodePlan, history: records };
|
|
23975
24821
|
}
|
|
24822
|
+
function taskIdentity() {
|
|
24823
|
+
return { name: taskNameFor("node", deps.env.USERNAME ?? "", deps.env), userSid: (ports.userSid ?? (() => defaultWinAcl().sid()))() };
|
|
24824
|
+
}
|
|
23976
24825
|
function control(snap) {
|
|
23977
24826
|
if (snap.managers.service === "none")
|
|
23978
24827
|
return;
|
|
24828
|
+
const tasks = snap.managers.service === "task-scheduler";
|
|
23979
24829
|
let run = ports.serviceRunner;
|
|
23980
24830
|
if (!run) {
|
|
23981
|
-
|
|
23982
|
-
|
|
24831
|
+
if (tasks)
|
|
24832
|
+
run = spawnManagerRunner({ programs: windowsServicePrograms(systemRootOf2(deps.probe)), env: deps.env });
|
|
24833
|
+
else {
|
|
24834
|
+
const systemctl = which(deps.probe, [...pathDirs(deps.probe), "/usr/bin", "/bin"], "systemctl") ?? "/usr/bin/systemctl";
|
|
24835
|
+
run = spawnManagerRunner({ programs: { launchctl: "/bin/launchctl", systemctl }, env: deps.env });
|
|
24836
|
+
}
|
|
23983
24837
|
}
|
|
23984
|
-
return createServiceControl({
|
|
24838
|
+
return createServiceControl({
|
|
24839
|
+
run,
|
|
24840
|
+
uid: ports.uid ?? deps.probe.uid,
|
|
24841
|
+
unitPath: unitPathResolver(snap.roots, executionContextOf(snap)),
|
|
24842
|
+
...tasks ? { taskName: (service) => taskNameFor(service, deps.env.USERNAME ?? "", deps.env) } : {}
|
|
24843
|
+
});
|
|
23985
24844
|
}
|
|
23986
24845
|
function hubctlSource() {
|
|
23987
24846
|
const manifest = deps.manifest;
|
|
@@ -24276,14 +25135,14 @@ class BootstrapStateStore {
|
|
|
24276
25135
|
}
|
|
24277
25136
|
|
|
24278
25137
|
// packages/cli/src/setup/self-aside.ts
|
|
24279
|
-
import { spawn as
|
|
24280
|
-
import { readdirSync as readdirSync4, statSync as
|
|
25138
|
+
import { spawn as spawn8 } from "node:child_process";
|
|
25139
|
+
import { readdirSync as readdirSync4, statSync as statSync7, unlinkSync as unlinkSync3 } from "node:fs";
|
|
24281
25140
|
var nodeSelfAsideIo = (env = process.env) => ({
|
|
24282
25141
|
platform: process.platform,
|
|
24283
25142
|
systemRoot: env.SystemRoot ?? env.SYSTEMROOT ?? "C:\\Windows",
|
|
24284
25143
|
stat: (path) => {
|
|
24285
25144
|
try {
|
|
24286
|
-
return { ino:
|
|
25145
|
+
return { ino: statSync7(path, { bigint: true }).ino };
|
|
24287
25146
|
} catch {
|
|
24288
25147
|
return;
|
|
24289
25148
|
}
|
|
@@ -24291,7 +25150,7 @@ var nodeSelfAsideIo = (env = process.env) => ({
|
|
|
24291
25150
|
readdir: (path) => readdirSync4(path),
|
|
24292
25151
|
unlink: (path) => unlinkSync3(path),
|
|
24293
25152
|
spawnDetached: (file, line, cwd) => {
|
|
24294
|
-
const child =
|
|
25153
|
+
const child = spawn8(file, [line], { cwd, detached: true, stdio: "ignore", windowsHide: true, windowsVerbatimArguments: true });
|
|
24295
25154
|
child.on("error", () => {});
|
|
24296
25155
|
child.unref();
|
|
24297
25156
|
}
|
|
@@ -24544,20 +25403,38 @@ function refFor(abs, snapshot) {
|
|
|
24544
25403
|
for (const id of ["home"]) {
|
|
24545
25404
|
const root = snapshot.roots.find((r) => r.id === id);
|
|
24546
25405
|
const base = root ? rootPathIn(root, ctx) : undefined;
|
|
24547
|
-
if (base && abs
|
|
24548
|
-
return { root: id, rel: abs.slice(base.length + 1) };
|
|
25406
|
+
if (base && underDir(abs, base))
|
|
25407
|
+
return { root: id, rel: abs.slice(base.length + (base.endsWith("/") ? 0 : 1)) };
|
|
24549
25408
|
}
|
|
24550
25409
|
return;
|
|
24551
25410
|
}
|
|
25411
|
+
function workspaceRootOf(raw, where) {
|
|
25412
|
+
if (where.platform !== "win32")
|
|
25413
|
+
return path35.resolve(where.cwd, raw);
|
|
25414
|
+
if (/^[\\/]{2}/.test(raw))
|
|
25415
|
+
return;
|
|
25416
|
+
const home = normalizeAbs(where.home);
|
|
25417
|
+
const tilde = /^~(?:[\\/](.*))?$/.exec(raw);
|
|
25418
|
+
if (tilde)
|
|
25419
|
+
return home ? normalizeAbs(path35.win32.join(home, tilde[1] ?? "")) : undefined;
|
|
25420
|
+
const resolved = path35.win32.resolve(path35.win32.normalize(where.cwd), raw);
|
|
25421
|
+
return /^[\\/]{2}/.test(resolved) ? undefined : normalizeAbs(resolved);
|
|
25422
|
+
}
|
|
24552
25423
|
function choicesOf(io, flags, profile, snapshot) {
|
|
24553
25424
|
const enrollment = str(flags, "enrollment");
|
|
24554
25425
|
if (enrollment !== undefined && !ENROLLMENT_MODES.includes(enrollment))
|
|
24555
25426
|
throw new Refusal("usage", `--enrollment is ${ENROLLMENT_MODES.join(" or ")}`);
|
|
24556
|
-
|
|
25427
|
+
const where = { platform: snapshot.host.platform, cwd: io.cwd ?? process.cwd(), home: io.home };
|
|
25428
|
+
let roots = flags["workspace-root"]?.map((r) => {
|
|
25429
|
+
const root = workspaceRootOf(r, where);
|
|
25430
|
+
if (root === undefined)
|
|
25431
|
+
throw new Refusal("usage", `--workspace-root ${r} is not a local absolute path (a drive path such as C:\\Users\\you\\projects, or ~/projects)`);
|
|
25432
|
+
return root;
|
|
25433
|
+
});
|
|
24557
25434
|
if (!roots?.length && profile.services.includes("node")) {
|
|
24558
25435
|
const existing = readNodeConfig(nodeConfigPath(io.env, io.home)).config.workspaceRoots;
|
|
24559
25436
|
if (existing.length > 0)
|
|
24560
|
-
roots = [...existing];
|
|
25437
|
+
roots = where.platform === "win32" ? existing.map((r) => workspaceRootOf(r, where) ?? r) : [...existing];
|
|
24561
25438
|
}
|
|
24562
25439
|
const launchHarness = str(flags, "verify-launch");
|
|
24563
25440
|
let launch;
|
|
@@ -24677,7 +25554,7 @@ async function preview3(io, flags, json) {
|
|
|
24677
25554
|
else {
|
|
24678
25555
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
24679
25556
|
writeFileSync8(tmp, text, { mode: 384 });
|
|
24680
|
-
|
|
25557
|
+
renameSync9(tmp, file);
|
|
24681
25558
|
}
|
|
24682
25559
|
}
|
|
24683
25560
|
const warnings = previewWarnings(snapshot, r.value.plan, bundle.node()?.notes ?? [], io.env);
|
|
@@ -25130,10 +26007,13 @@ Identity
|
|
|
25130
26007
|
hub role release <role[.project]> [--session <addr>]
|
|
25131
26008
|
|
|
25132
26009
|
Messaging (text "-" reads it from stdin)
|
|
25133
|
-
hub msg <addr> "text" [--all] [--no-wake] [--urgent]
|
|
25134
|
-
|
|
26010
|
+
hub msg <addr> "text" [--all] [--no-wake] [--urgent] [--not-after <ms|ISO time>] [--key K]
|
|
26011
|
+
tell: fire and forget; --not-after: never delivered after that time
|
|
26012
|
+
hub ask <addr> "text" [--wait [secs]] [--expires 2h] [--not-after <ms|ISO time>] [--all] [--no-wake] [--urgent] [--key K]
|
|
25135
26013
|
tracked until answered, declined or expired; --wait alone waits until
|
|
25136
|
-
it expires; exit 0 answered, 1 declined or no answer yet, 8 expired
|
|
26014
|
+
it expires; exit 0 answered, 1 declined or no answer yet, 8 expired;
|
|
26015
|
+
--key (Idempotency-Key): a retry with the same key and text answers
|
|
26016
|
+
the first send again and sends nothing (24 h, per credential)
|
|
25137
26017
|
hub reply <msgID> "text" [--no-wake] goes to the real sender
|
|
25138
26018
|
hub decline <msgID> --why "reason"
|
|
25139
26019
|
hub ask @me "question" [--option id=label[:safe]]... [--recommend id [--why "…"]] [--urgency low|normal|high]
|
|
@@ -25168,9 +26048,10 @@ Launching (users; agents and app tokens with sessions:launch under a grant with
|
|
|
25168
26048
|
[--name <@name>] [--role R]... [--harness H] [--agent A] [--model provider/model]
|
|
25169
26049
|
[--runtime host|openshell [--profile P]] [--machine M] [--node-label k=v]... [--prefer]
|
|
25170
26050
|
[--label k=v]... [--prompt "text" | --prompt -] [--end idle|asked|never] [--timeout 4h]
|
|
25171
|
-
[--cleanup keep|remove] [--key K]
|
|
26051
|
+
[--cleanup keep|remove] [--key K] [--not-after <ms|ISO time>]
|
|
25172
26052
|
start a session; the same --key again answers the same launch;
|
|
25173
|
-
--label tags it for apps, --node-label picks nodes (--prefer: not required)
|
|
26053
|
+
--label tags it for apps, --node-label picks nodes (--prefer: not required);
|
|
26054
|
+
--not-after: still waiting for a machine then, it never starts (expired)
|
|
25174
26055
|
hub launches list [--state S[,S]] [--label k=v]... your launches (agents: the ones they started)
|
|
25175
26056
|
hub launches show <launchID> · hub launches stop <launchID> (stop: exit 8 when it already ended)
|
|
25176
26057
|
|
|
@@ -25299,7 +26180,16 @@ var whyOf = (flags) => {
|
|
|
25299
26180
|
const why = str(flags, "why");
|
|
25300
26181
|
return why !== undefined ? { why } : {};
|
|
25301
26182
|
};
|
|
25302
|
-
var sendFlags = (flags) =>
|
|
26183
|
+
var sendFlags = (flags) => {
|
|
26184
|
+
const notAfter = str(flags, "not-after");
|
|
26185
|
+
const key = str(flags, "key");
|
|
26186
|
+
return {
|
|
26187
|
+
...flags["no-wake"] === true ? { wake: false } : {},
|
|
26188
|
+
...flags.urgent === true ? { urgent: true } : {},
|
|
26189
|
+
...notAfter === undefined ? {} : { notAfter: notAfterArg(notAfter, "--not-after") },
|
|
26190
|
+
...key !== undefined ? { idempotencyKey: key } : {}
|
|
26191
|
+
};
|
|
26192
|
+
};
|
|
25303
26193
|
var nodeIo = (io) => ({
|
|
25304
26194
|
env: io.env,
|
|
25305
26195
|
home: io.home,
|
|
@@ -25421,7 +26311,7 @@ var COMMANDS = {
|
|
|
25421
26311
|
},
|
|
25422
26312
|
msg: {
|
|
25423
26313
|
args: ["<addr>", "<text>"],
|
|
25424
|
-
flags: { all: "bool", "no-wake": "bool", urgent: "bool" },
|
|
26314
|
+
flags: { all: "bool", "no-wake": "bool", urgent: "bool", "not-after": "string", key: "string" },
|
|
25425
26315
|
async handle(context) {
|
|
25426
26316
|
const { flags, args } = context.parsed;
|
|
25427
26317
|
const text = await textArg(args[1], context.io.stdin);
|
|
@@ -25433,7 +26323,7 @@ var COMMANDS = {
|
|
|
25433
26323
|
},
|
|
25434
26324
|
ask: {
|
|
25435
26325
|
args: ["<addr>", "<text>"],
|
|
25436
|
-
flags: { all: "bool", "no-wake": "bool", urgent: "bool", expires: "string", wait: "optional", ...DECISION_FLAGS },
|
|
26326
|
+
flags: { all: "bool", "no-wake": "bool", urgent: "bool", "not-after": "string", key: "string", expires: "string", wait: "optional", ...DECISION_FLAGS },
|
|
25437
26327
|
async handle(context) {
|
|
25438
26328
|
const { flags, args } = context.parsed;
|
|
25439
26329
|
const decision = decisionOptions(flags, args[0]);
|