@huynle/hub 0.9.0 → 0.9.2
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 +36 -0
- package/README.md +7 -4
- package/package.json +1 -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 +1126 -479
- 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/node.ts +94 -12
- package/packages/cli/src/setup/commands.ts +27 -7
- package/packages/cli/src/setup/runtime.ts +31 -4
- 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/src/index.ts +3 -1
- package/packages/protocol/src/launch.ts +39 -4
- 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/spoke-sdk/package.json +1 -1
- package/src/central/agent/launch-spec.ts +4 -2
- package/src/central/agent/webhooks.ts +5 -2
- package/src/central/config.ts +24 -2
- package/src/central/http.ts +1 -0
- 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/push-target.ts +11 -2
- package/web/dist/assets/{ArtifactPage-4GoBqSpU.js → ArtifactPage-BKISN-Qj.js} +1 -1
- package/web/dist/assets/{ArtifactPane-Dx_0iWwH.js → ArtifactPane-CKOiAUNH.js} +1 -1
- package/web/dist/assets/{ArtifactWorkbench-CuHgARMk.js → ArtifactWorkbench-d7R1o-rs.js} +1 -1
- package/web/dist/assets/{Diagram-CZM_R7MG.js → Diagram-azw2B_QH.js} +1 -1
- package/web/dist/assets/{Library-TwvrwqSk.js → Library-CcjZXsfF.js} +2 -2
- package/web/dist/assets/{ProfilesDestination-rKNDpLjR.js → ProfilesDestination-DdzbzmeK.js} +1 -1
- package/web/dist/assets/{TeamsPage-DtJylLrt.js → TeamsPage-CDyjrK-I.js} +1 -1
- package/web/dist/assets/{Watchtower-Bmgdyfjb.js → Watchtower-DYwoUWME.js} +2 -2
- package/web/dist/assets/{index-D-xG9nrc.js → index-LNMZCqgE.js} +15 -15
- package/web/dist/assets/{live-7q72Lyj2.js → live-ycZ_aQZO.js} +1 -1
- package/web/dist/assets/{renderer2d-hfZxM6Xl.js → renderer2d-BIwbgRoX.js} +1 -1
- package/web/dist/assets/{renderer3d-Km9vipTM.js → renderer3d-C1a7eYGQ.js} +1 -1
- package/web/dist/assets/{safe-area-BAbgBiFk.js → safe-area-DHhPy61Q.js} +1 -1
- package/web/dist/index.html +1 -1
|
@@ -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.9.
|
|
10
|
+
version: "0.9.2",
|
|
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",
|
|
@@ -1695,7 +1695,7 @@ var SUPPORTED_TARGETS = Object.freeze([
|
|
|
1695
1695
|
{ target: "darwin-x64", platform: "darwin", arch: "x64", libc: "none", serviceManager: "launchd", bunTarget: "bun-darwin-x64-baseline" },
|
|
1696
1696
|
{ target: "linux-x64", platform: "linux", arch: "x64", libc: "glibc", serviceManager: "systemd-user", bunTarget: "bun-linux-x64-baseline" },
|
|
1697
1697
|
{ target: "linux-arm64", platform: "linux", arch: "arm64", libc: "glibc", serviceManager: "systemd-user", bunTarget: "bun-linux-arm64" },
|
|
1698
|
-
{ 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" }
|
|
1699
1699
|
]);
|
|
1700
1700
|
var UNSUPPORTED_TARGETS = Object.freeze([
|
|
1701
1701
|
{ target: "windows-arm64", reason: "Windows on arm64 has no hubctl build yet (only windows-x64 is built)" },
|
|
@@ -1716,7 +1716,7 @@ function checkTarget(target) {
|
|
|
1716
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(", ")})`);
|
|
1717
1717
|
}
|
|
1718
1718
|
// packages/cli/src/bootstrap/contracts/paths.ts
|
|
1719
|
-
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"];
|
|
1720
1720
|
var ELEVATED_ROOTS = ["system-bin"];
|
|
1721
1721
|
var BAD_REL = /[\u0000-\u001f\u007f\\]/;
|
|
1722
1722
|
function isSafeRel(rel) {
|
|
@@ -1798,7 +1798,8 @@ var EMBEDDED_ASSETS = Object.freeze({
|
|
|
1798
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" },
|
|
1799
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" },
|
|
1800
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" },
|
|
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" }
|
|
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" }
|
|
1802
1803
|
});
|
|
1803
1804
|
// packages/cli/src/bootstrap/contracts/harness.ts
|
|
1804
1805
|
var HARNESS_ORDER = ["opencode", "claude-code", "codex"];
|
|
@@ -1884,6 +1885,7 @@ function createAdapterRegistry() {
|
|
|
1884
1885
|
// packages/cli/src/bootstrap/contracts/actions.ts
|
|
1885
1886
|
var EXEC_KEYS = ["command", "cmd", "commands", "shell", "script", "scripts", "exec", "execute", "argv", "args", "arguments", "run", "eval", "sh", "bash", "program", "spawn", "hook", "hooks"];
|
|
1886
1887
|
var CONFIG_OWNERS = ["user", "hub", ...HARNESS_ORDER.map((h) => `harness:${h}`)];
|
|
1888
|
+
var SERVICE_MANAGERS = ["launchd", "systemd-user", "task-scheduler"];
|
|
1887
1889
|
var harnessVersionRef = (harness, version) => ({ root: "data", rel: `hub/harnesses/${harness}/${version}` });
|
|
1888
1890
|
var harnessCurrentRef = (harness) => ({ root: "data", rel: `hub/harnesses/${harness}/current` });
|
|
1889
1891
|
var COMMON2 = ["id", "kind", "escalation", "summary"];
|
|
@@ -1912,7 +1914,7 @@ var ASSET_ID = /^[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9][a-z0-9._-]*)*$/;
|
|
|
1912
1914
|
var MACHINE_NAME = /^[A-Za-z0-9._-]{1,64}$/;
|
|
1913
1915
|
var MCP_NAME = /^[a-z0-9][a-z0-9-]{0,31}$/;
|
|
1914
1916
|
var JSON_POINTER = /^(?:\/(?:[^~/]|~[01])*)+$/;
|
|
1915
|
-
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" });
|
|
1916
1918
|
var EXEC = new Set(EXEC_KEYS);
|
|
1917
1919
|
function findExecKeys(value, base = "") {
|
|
1918
1920
|
const found = [];
|
|
@@ -2099,7 +2101,7 @@ function checkAction(c, path, value) {
|
|
|
2099
2101
|
break;
|
|
2100
2102
|
case "install-service": {
|
|
2101
2103
|
oneOf(c, at("service"), r.service, SERVICE_IDS);
|
|
2102
|
-
oneOf(c, at("manager"), r.manager,
|
|
2104
|
+
oneOf(c, at("manager"), r.manager, SERVICE_MANAGERS);
|
|
2103
2105
|
ref("path");
|
|
2104
2106
|
digest(c, at("sha256"), r.sha256);
|
|
2105
2107
|
const asset = typeof r.asset === "string" ? EMBEDDED_ASSETS[r.asset] : undefined;
|
|
@@ -2112,18 +2114,18 @@ function checkAction(c, path, value) {
|
|
|
2112
2114
|
}
|
|
2113
2115
|
case "deactivate-service":
|
|
2114
2116
|
oneOf(c, at("service"), r.service, SERVICE_IDS);
|
|
2115
|
-
oneOf(c, at("manager"), r.manager,
|
|
2117
|
+
oneOf(c, at("manager"), r.manager, SERVICE_MANAGERS);
|
|
2116
2118
|
break;
|
|
2117
2119
|
case "remove-service":
|
|
2118
2120
|
oneOf(c, at("service"), r.service, SERVICE_IDS);
|
|
2119
|
-
oneOf(c, at("manager"), r.manager,
|
|
2121
|
+
oneOf(c, at("manager"), r.manager, SERVICE_MANAGERS);
|
|
2120
2122
|
ref("path");
|
|
2121
2123
|
digest(c, at("sha256"), r.sha256);
|
|
2122
2124
|
checkServiceUnitPath(c, at, r);
|
|
2123
2125
|
break;
|
|
2124
2126
|
case "activate-service":
|
|
2125
2127
|
oneOf(c, at("service"), r.service, SERVICE_IDS);
|
|
2126
|
-
oneOf(c, at("manager"), r.manager,
|
|
2128
|
+
oneOf(c, at("manager"), r.manager, SERVICE_MANAGERS);
|
|
2127
2129
|
if (r.confirm !== "explicit")
|
|
2128
2130
|
c.add(at("confirm"), "invalid_value", "services are only activated on explicit confirmation");
|
|
2129
2131
|
break;
|
|
@@ -2257,12 +2259,14 @@ function validateDiscoverySnapshot(value) {
|
|
|
2257
2259
|
semver(c, pointer(p, "version"), rt.version);
|
|
2258
2260
|
absPath(c, pointer(p, "path"), rt.path);
|
|
2259
2261
|
});
|
|
2260
|
-
oneOf(c, "/managers/service", managers.service, ["launchd", "systemd-user", "none"]);
|
|
2262
|
+
oneOf(c, "/managers/service", managers.service, ["launchd", "systemd-user", "task-scheduler", "none"]);
|
|
2261
2263
|
const platform = host?.platform;
|
|
2262
2264
|
if (managers.service === "launchd" && platform !== "darwin")
|
|
2263
2265
|
c.add("/managers/service", "inconsistent", "launchd is macOS only");
|
|
2264
2266
|
if (managers.service === "systemd-user" && platform !== "linux")
|
|
2265
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");
|
|
2266
2270
|
}
|
|
2267
2271
|
list(c, "/harnesses", r.harnesses, HARNESS_ORDER.length, (p, v) => checkHarness(c, p, v), (v) => v?.id);
|
|
2268
2272
|
list(c, "/runtimes", r.runtimes, RUNTIME_IDS.length, (p, v) => {
|
|
@@ -2280,7 +2284,7 @@ function validateDiscoverySnapshot(value) {
|
|
|
2280
2284
|
return;
|
|
2281
2285
|
absPath(c, pointer(p, "path"), cf.path);
|
|
2282
2286
|
oneOf(c, pointer(p, "owner"), cf.owner, [...CONFIG_OWNERS, "package-manager"]);
|
|
2283
|
-
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"]);
|
|
2284
2288
|
if (cf.sha256 !== null)
|
|
2285
2289
|
digest(c, pointer(p, "sha256"), cf.sha256);
|
|
2286
2290
|
}, (v) => v?.path);
|
|
@@ -3354,6 +3358,99 @@ function parseFrame(raw) {
|
|
|
3354
3358
|
}
|
|
3355
3359
|
}
|
|
3356
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
|
+
|
|
3357
3454
|
// packages/protocol/src/contracts.ts
|
|
3358
3455
|
var SLUG = /^[a-z0-9-]{2,40}$/;
|
|
3359
3456
|
var isSlug = (v) => typeof v === "string" && SLUG.test(v);
|
|
@@ -3740,74 +3837,6 @@ function harnessEventProblem(event) {
|
|
|
3740
3837
|
return "event data must be an object";
|
|
3741
3838
|
return CHECKS[event.type](event.data);
|
|
3742
3839
|
}
|
|
3743
|
-
// packages/protocol/src/launch.ts
|
|
3744
|
-
var WORKSPACE_MODES = ["worktree", "checkout", "clone"];
|
|
3745
|
-
var RUNTIME_KINDS = ["host", "openshell"];
|
|
3746
|
-
var LAUNCH_END_WHEN = ["idle", "asked", "never"];
|
|
3747
|
-
var CLEANUP_POLICIES = ["keep", "remove"];
|
|
3748
|
-
var PATH_MAX = 4096;
|
|
3749
|
-
var REF_MAX = 200;
|
|
3750
|
-
var REMOTE_MAX = 2048;
|
|
3751
|
-
var LABELS_MAX = 32;
|
|
3752
|
-
var LABEL_VALUE_MAX = 256;
|
|
3753
|
-
var LABEL_KEY = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,62}$/;
|
|
3754
|
-
function isAbsolutePath2(v) {
|
|
3755
|
-
if (typeof v !== "string" || !v.startsWith("/") || v.length > PATH_MAX || UNSAFE_CHARS.test(v))
|
|
3756
|
-
return false;
|
|
3757
|
-
if (v === "/")
|
|
3758
|
-
return true;
|
|
3759
|
-
return v.slice(1).replace(/\/$/, "").split("/").every((part) => part !== "" && part !== "." && part !== "..");
|
|
3760
|
-
}
|
|
3761
|
-
function isRefName(v) {
|
|
3762
|
-
if (typeof v !== "string" || v.length === 0 || v.length > REF_MAX)
|
|
3763
|
-
return false;
|
|
3764
|
-
if (!/^[A-Za-z0-9._/-]+$/.test(v) || v.startsWith("-") || v.endsWith(".") || v.includes(".."))
|
|
3765
|
-
return false;
|
|
3766
|
-
return v.split("/").every((part) => part !== "" && !part.startsWith(".") && !part.endsWith(".lock"));
|
|
3767
|
-
}
|
|
3768
|
-
function isGitRemote(v) {
|
|
3769
|
-
if (typeof v !== "string" || v.length === 0 || v.length > REMOTE_MAX || v.startsWith("-") || /[\s\p{Cc}\p{Cf}]/u.test(v))
|
|
3770
|
-
return false;
|
|
3771
|
-
if (/^[A-Za-z0-9][A-Za-z0-9._-]*@[A-Za-z0-9][A-Za-z0-9.-]*:[^-:][^:]*$/.test(v))
|
|
3772
|
-
return true;
|
|
3773
|
-
try {
|
|
3774
|
-
const url = new URL(v);
|
|
3775
|
-
return (url.protocol === "https:" || url.protocol === "ssh:") && url.hostname.length > 0 && !url.hostname.startsWith("-") && url.pathname.length > 1;
|
|
3776
|
-
} catch {
|
|
3777
|
-
return false;
|
|
3778
|
-
}
|
|
3779
|
-
}
|
|
3780
|
-
function launchWorkspaceProblem(v) {
|
|
3781
|
-
if (!isObj(v))
|
|
3782
|
-
return "workspace must be an object";
|
|
3783
|
-
if (!isAbsolutePath2(v.repo))
|
|
3784
|
-
return `workspace.repo must be an absolute path of at most ${PATH_MAX} characters without . or .. segments`;
|
|
3785
|
-
if (!WORKSPACE_MODES.includes(v.mode))
|
|
3786
|
-
return `workspace.mode must be one of ${WORKSPACE_MODES.join(", ")}`;
|
|
3787
|
-
if (v.branch !== undefined && !isRefName(v.branch))
|
|
3788
|
-
return "workspace.branch must be a valid branch name (letters, digits, . _ / -; not starting with -)";
|
|
3789
|
-
if (v.base !== undefined && !isRefName(v.base))
|
|
3790
|
-
return "workspace.base must be a valid branch name (letters, digits, . _ / -; not starting with -)";
|
|
3791
|
-
if (v.remote !== undefined && !isGitRemote(v.remote))
|
|
3792
|
-
return "workspace.remote must be an https or ssh git remote";
|
|
3793
|
-
if (v.mode === "clone" && v.remote === undefined)
|
|
3794
|
-
return "workspace.remote is required for a clone";
|
|
3795
|
-
return;
|
|
3796
|
-
}
|
|
3797
|
-
function labelsProblem(v) {
|
|
3798
|
-
if (!isObj(v))
|
|
3799
|
-
return "labels must be an object";
|
|
3800
|
-
const entries = Object.entries(v);
|
|
3801
|
-
if (entries.length > LABELS_MAX)
|
|
3802
|
-
return `at most ${LABELS_MAX} labels`;
|
|
3803
|
-
for (const [key, value] of entries) {
|
|
3804
|
-
if (!LABEL_KEY.test(key))
|
|
3805
|
-
return `label key ${JSON.stringify(key.slice(0, 80))} must be 1–63 letters, digits, . _ / - (starting with a letter or digit)`;
|
|
3806
|
-
if (typeof value !== "string" || value.length > LABEL_VALUE_MAX || UNSAFE_CHARS.test(value))
|
|
3807
|
-
return `label ${key} must be text of at most ${LABEL_VALUE_MAX} printable characters`;
|
|
3808
|
-
}
|
|
3809
|
-
return;
|
|
3810
|
-
}
|
|
3811
3840
|
// packages/protocol/src/node.ts
|
|
3812
3841
|
var NODE_ROLE = "node";
|
|
3813
3842
|
var NODE_FEATURES = ["sandbox"];
|
|
@@ -4433,7 +4462,7 @@ function isServerRequest(v) {
|
|
|
4433
4462
|
case "expose":
|
|
4434
4463
|
return nonEmpty(v.sessionID) && typeof v.on === "boolean";
|
|
4435
4464
|
case "create":
|
|
4436
|
-
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));
|
|
4437
4466
|
case "sessionTokens":
|
|
4438
4467
|
return Array.isArray(v.tokens) && v.tokens.length <= SESSION_TOKENS_MAX && v.tokens.every(isSessionToken) && (v.hubURL === undefined || isHubURL(v.hubURL));
|
|
4439
4468
|
case "actTokens":
|
|
@@ -5474,18 +5503,22 @@ class OpenCodeWarmer {
|
|
|
5474
5503
|
}
|
|
5475
5504
|
}
|
|
5476
5505
|
async function readServiceRegistration(file, options = {}) {
|
|
5477
|
-
const
|
|
5506
|
+
const win32 = isWin32(options);
|
|
5507
|
+
const uid = win32 ? undefined : options.uid ?? (typeof process.getuid === "function" ? process.getuid() : undefined);
|
|
5478
5508
|
let info;
|
|
5479
5509
|
let fd;
|
|
5480
5510
|
try {
|
|
5481
5511
|
const before = lstatSync3(file);
|
|
5482
5512
|
if (!before.isFile())
|
|
5483
5513
|
return;
|
|
5484
|
-
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));
|
|
5485
5515
|
const stat = fstatSync(fd);
|
|
5486
5516
|
if (!stat.isFile() || stat.dev !== before.dev || stat.ino !== before.ino)
|
|
5487
5517
|
return;
|
|
5488
|
-
if (
|
|
5518
|
+
if (win32) {
|
|
5519
|
+
if (!(options.acl ?? defaultWinAcl()).check(nativeWinPath(file)).ok)
|
|
5520
|
+
return;
|
|
5521
|
+
} else if ((stat.mode & 18) !== 0)
|
|
5489
5522
|
return;
|
|
5490
5523
|
if (uid !== undefined && stat.uid !== uid)
|
|
5491
5524
|
return;
|
|
@@ -5596,7 +5629,23 @@ function nodeConfigPath(env = process.env, home = homedir2()) {
|
|
|
5596
5629
|
var isObject3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
5597
5630
|
var whole = (v, min, max) => typeof v === "number" && Number.isInteger(v) && v >= min && v <= max;
|
|
5598
5631
|
var show2 = (v) => JSON.stringify(v)?.slice(0, 80) ?? String(v);
|
|
5599
|
-
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 = {}) {
|
|
5600
5649
|
const problems = [];
|
|
5601
5650
|
let r = {};
|
|
5602
5651
|
if (isObject3(raw))
|
|
@@ -5606,9 +5655,10 @@ function normalizeNodeConfig(raw) {
|
|
|
5606
5655
|
const workspaceRoots = [];
|
|
5607
5656
|
if (r.workspaceRoots !== undefined && !Array.isArray(r.workspaceRoots))
|
|
5608
5657
|
problems.push("workspaceRoots must be a list of absolute paths");
|
|
5609
|
-
for (const
|
|
5610
|
-
|
|
5611
|
-
|
|
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 /)`);
|
|
5612
5662
|
continue;
|
|
5613
5663
|
}
|
|
5614
5664
|
const clean = root.replace(/\/+$/, "");
|
|
@@ -5643,21 +5693,22 @@ function normalizeNodeConfig(raw) {
|
|
|
5643
5693
|
}
|
|
5644
5694
|
const openshell = r.openshell === undefined ? undefined : normalizeOpenShell(r.openshell, problems);
|
|
5645
5695
|
const harnesses = r.harnesses === undefined ? undefined : normalizeHarnesses(r.harnesses, problems);
|
|
5646
|
-
const opencode = r.opencode === undefined ? undefined : normalizeOpenCode(r.opencode, problems);
|
|
5696
|
+
const opencode = r.opencode === undefined ? undefined : normalizeOpenCode(r.opencode, problems, options);
|
|
5647
5697
|
return {
|
|
5648
5698
|
config: { workspaceRoots, labels, slots, ...memoryMB !== undefined ? { memoryMB } : {}, ...openshell ? { openshell } : {}, ...harnesses ? { harnesses } : {}, ...opencode ? { opencode } : {} },
|
|
5649
5699
|
problems
|
|
5650
5700
|
};
|
|
5651
5701
|
}
|
|
5652
|
-
function normalizeDirectories(raw, field, problems) {
|
|
5702
|
+
function normalizeDirectories(raw, field, problems, options) {
|
|
5653
5703
|
if (!Array.isArray(raw)) {
|
|
5654
5704
|
problems.push(`${field} must be a list of absolute paths`);
|
|
5655
5705
|
return;
|
|
5656
5706
|
}
|
|
5657
5707
|
const out = [];
|
|
5658
|
-
for (const
|
|
5659
|
-
|
|
5660
|
-
|
|
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 /)`);
|
|
5661
5712
|
continue;
|
|
5662
5713
|
}
|
|
5663
5714
|
const clean = dir.replace(/\/+$/, "");
|
|
@@ -5688,12 +5739,12 @@ function loopbackOrigin(raw) {
|
|
|
5688
5739
|
return;
|
|
5689
5740
|
return url.origin;
|
|
5690
5741
|
}
|
|
5691
|
-
function normalizeOpenCode(raw, problems) {
|
|
5742
|
+
function normalizeOpenCode(raw, problems, options) {
|
|
5692
5743
|
if (!isObject3(raw)) {
|
|
5693
5744
|
problems.push("opencode must be an object");
|
|
5694
5745
|
return;
|
|
5695
5746
|
}
|
|
5696
|
-
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);
|
|
5697
5748
|
const servers = [];
|
|
5698
5749
|
if (raw.servers !== undefined && !Array.isArray(raw.servers))
|
|
5699
5750
|
problems.push("opencode.servers must be a list of {url, passwordFile, directories?}");
|
|
@@ -5705,12 +5756,13 @@ function normalizeOpenCode(raw, problems) {
|
|
|
5705
5756
|
const url = loopbackOrigin(entry.url);
|
|
5706
5757
|
if (!url)
|
|
5707
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`);
|
|
5708
|
-
|
|
5759
|
+
const passwordFile = configPathOf(entry.passwordFile, options);
|
|
5760
|
+
if (passwordFile === undefined || isRoot(passwordFile))
|
|
5709
5761
|
return void problems.push(`${field}.passwordFile must be the absolute path of a private (chmod 600) file holding the server's password`);
|
|
5710
5762
|
if (servers.some((s) => s.url === url))
|
|
5711
5763
|
return void problems.push(`${field}: ${url} is listed twice; the first one is used`);
|
|
5712
|
-
const dirs = entry.directories === undefined ? undefined : normalizeDirectories(entry.directories, `${field}.directories`, problems);
|
|
5713
|
-
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 } : {} });
|
|
5714
5766
|
});
|
|
5715
5767
|
if (servers.length > OPENCODE_SERVERS_MAX) {
|
|
5716
5768
|
problems.push(`opencode.servers: at most ${OPENCODE_SERVERS_MAX} servers; the rest are ignored`);
|
|
@@ -5772,7 +5824,7 @@ function normalizeOpenShell(raw, problems) {
|
|
|
5772
5824
|
problems.push(...profiles.problems);
|
|
5773
5825
|
return { enabled, ...gateway ? { gateway } : {}, ...image ? { image } : {}, profiles };
|
|
5774
5826
|
}
|
|
5775
|
-
function readNodeConfig(file) {
|
|
5827
|
+
function readNodeConfig(file, options = {}) {
|
|
5776
5828
|
let text;
|
|
5777
5829
|
try {
|
|
5778
5830
|
text = readFileSync4(file, "utf8");
|
|
@@ -5782,7 +5834,7 @@ function readNodeConfig(file) {
|
|
|
5782
5834
|
return { config: normalizeNodeConfig(undefined).config, problems: [`${file}: cannot read (${cause.message})`] };
|
|
5783
5835
|
}
|
|
5784
5836
|
try {
|
|
5785
|
-
return normalizeNodeConfig(JSON.parse(text));
|
|
5837
|
+
return normalizeNodeConfig(JSON.parse(text), options);
|
|
5786
5838
|
} catch {
|
|
5787
5839
|
return { config: normalizeNodeConfig(undefined).config, problems: [`${file}: not valid JSON`] };
|
|
5788
5840
|
}
|
|
@@ -8951,7 +9003,7 @@ function grantText2(g) {
|
|
|
8951
9003
|
var requestedText = (body) => `requested; waiting for the user (Decision ${body.decision.id})`;
|
|
8952
9004
|
|
|
8953
9005
|
// packages/cli/src/node.ts
|
|
8954
|
-
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";
|
|
8955
9007
|
import path16 from "node:path";
|
|
8956
9008
|
// packages/node/src/connection.ts
|
|
8957
9009
|
var UNSUPPORTED_RETRY_MS = 30 * 60000;
|
|
@@ -9169,29 +9221,233 @@ class NodeConnection {
|
|
|
9169
9221
|
} catch {}
|
|
9170
9222
|
}
|
|
9171
9223
|
}
|
|
9172
|
-
// packages/node/src/daemon.ts
|
|
9173
|
-
import { randomUUID } from "node:crypto";
|
|
9174
|
-
import { arch as osArch, platform as osPlatform } from "node:os";
|
|
9175
|
-
// packages/node/package.json
|
|
9176
|
-
var package_default2 = {
|
|
9177
|
-
name: "@huynle/hub-node",
|
|
9178
|
-
version: "0.9.
|
|
9179
|
-
private: true,
|
|
9180
|
-
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).",
|
|
9181
|
-
license: "MIT",
|
|
9182
|
-
type: "module",
|
|
9183
|
-
exports: {
|
|
9184
|
-
".": "./src/index.ts",
|
|
9185
|
-
"./package.json": "./package.json"
|
|
9186
|
-
}
|
|
9187
|
-
};
|
|
9188
|
-
|
|
9189
|
-
// packages/node/src/host.ts
|
|
9190
|
-
import { existsSync as existsSync3, lstatSync as lstatSync4, mkdirSync as mkdirSync6, readFileSync as readFileSync5, realpathSync as realpathSync2, rmSync as rmSync6, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
9191
|
-
import path11 from "node:path";
|
|
9224
|
+
// packages/node/src/daemon.ts
|
|
9225
|
+
import { randomUUID } from "node:crypto";
|
|
9226
|
+
import { arch as osArch, platform as osPlatform } from "node:os";
|
|
9227
|
+
// packages/node/package.json
|
|
9228
|
+
var package_default2 = {
|
|
9229
|
+
name: "@huynle/hub-node",
|
|
9230
|
+
version: "0.9.2",
|
|
9231
|
+
private: true,
|
|
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).",
|
|
9233
|
+
license: "MIT",
|
|
9234
|
+
type: "module",
|
|
9235
|
+
exports: {
|
|
9236
|
+
".": "./src/index.ts",
|
|
9237
|
+
"./package.json": "./package.json"
|
|
9238
|
+
}
|
|
9239
|
+
};
|
|
9240
|
+
|
|
9241
|
+
// packages/node/src/host.ts
|
|
9242
|
+
import { existsSync as existsSync3, lstatSync as lstatSync4, mkdirSync as mkdirSync6, readFileSync as readFileSync5, realpathSync as realpathSync2, rmSync as rmSync6, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
9243
|
+
import path11 from "node:path";
|
|
9244
|
+
|
|
9245
|
+
// packages/node/src/git.ts
|
|
9246
|
+
import { spawn } from "node:child_process";
|
|
9247
|
+
|
|
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";
|
|
9262
|
+
}
|
|
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
|
+
}
|
|
9192
9449
|
|
|
9193
9450
|
// packages/node/src/git.ts
|
|
9194
|
-
import { spawn } from "node:child_process";
|
|
9195
9451
|
var ALLOWED_PROTOCOLS = "https:ssh";
|
|
9196
9452
|
var GIT_TIMEOUT_MS = 120000;
|
|
9197
9453
|
var MAX_BUFFER = 16 * 1024 * 1024;
|
|
@@ -9222,6 +9478,7 @@ function redact(text) {
|
|
|
9222
9478
|
function runGit(args, options) {
|
|
9223
9479
|
const argv = ["-c", "protocol.ext.allow=never", ...args];
|
|
9224
9480
|
const what = `git ${args.find((a) => !a.startsWith("-")) ?? ""}`.trim();
|
|
9481
|
+
const win32 = (options.platform ?? process.platform) === "win32";
|
|
9225
9482
|
return new Promise((resolve, reject) => {
|
|
9226
9483
|
if (options.signal?.aborted)
|
|
9227
9484
|
return reject(new GitError(`${what} cancelled`));
|
|
@@ -9241,7 +9498,7 @@ function runGit(args, options) {
|
|
|
9241
9498
|
cwd: options.cwd,
|
|
9242
9499
|
env: gitEnv(options.baseEnv ?? process.env, options.env),
|
|
9243
9500
|
stdio: ["ignore", "pipe", "pipe"],
|
|
9244
|
-
detached:
|
|
9501
|
+
detached: !win32,
|
|
9245
9502
|
windowsHide: true
|
|
9246
9503
|
});
|
|
9247
9504
|
const out = [];
|
|
@@ -9265,6 +9522,13 @@ function runGit(args, options) {
|
|
|
9265
9522
|
function stop(why) {
|
|
9266
9523
|
if (settled)
|
|
9267
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
|
+
}
|
|
9268
9532
|
try {
|
|
9269
9533
|
if (child.pid)
|
|
9270
9534
|
process.kill(-child.pid, "SIGKILL");
|
|
@@ -9282,43 +9546,72 @@ function runGit(args, options) {
|
|
|
9282
9546
|
|
|
9283
9547
|
// packages/node/src/host.ts
|
|
9284
9548
|
var LAUNCH_MARKER = "hub-launch";
|
|
9285
|
-
|
|
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) {
|
|
9286
9571
|
const rest = [];
|
|
9287
9572
|
let current = p;
|
|
9288
9573
|
for (;; ) {
|
|
9574
|
+
let real;
|
|
9289
9575
|
try {
|
|
9290
|
-
|
|
9576
|
+
real = realOf(current, ops);
|
|
9291
9577
|
} catch (cause) {
|
|
9578
|
+
if (cause instanceof RuntimeError)
|
|
9579
|
+
throw cause;
|
|
9292
9580
|
if (cause.code !== "ENOENT")
|
|
9293
9581
|
throw new RuntimeError(`cannot resolve ${p}: ${cause.message}`);
|
|
9294
|
-
|
|
9295
|
-
try {
|
|
9296
|
-
link = lstatSync4(current).isSymbolicLink();
|
|
9297
|
-
} catch {}
|
|
9298
|
-
if (link)
|
|
9582
|
+
if (ops.isSymlink(current))
|
|
9299
9583
|
throw new RuntimeError(`${p} goes through a dangling symlink`);
|
|
9300
|
-
const parent =
|
|
9584
|
+
const parent = ops.path.dirname(current);
|
|
9301
9585
|
if (parent === current)
|
|
9302
9586
|
throw new RuntimeError(`cannot resolve ${p}`);
|
|
9303
|
-
rest.push(
|
|
9587
|
+
rest.push(ops.path.basename(current));
|
|
9304
9588
|
current = parent;
|
|
9589
|
+
continue;
|
|
9305
9590
|
}
|
|
9591
|
+
return ops.win32 ? path11.posix.join(real, ...rest.reverse()) : path11.join(real, ...rest.reverse());
|
|
9306
9592
|
}
|
|
9307
9593
|
}
|
|
9308
|
-
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) {
|
|
9309
9602
|
if (roots.length === 0)
|
|
9310
9603
|
throw new RuntimeError("this node has no workspaceRoots in node.json, so it prepares no workspaces");
|
|
9311
9604
|
if (!isAbsolutePath2(p))
|
|
9312
9605
|
throw new RuntimeError(`${p} is not an absolute path`);
|
|
9313
|
-
const real = resolvePath(p);
|
|
9606
|
+
const real = resolvePath(p, ops);
|
|
9314
9607
|
for (const root of roots) {
|
|
9315
9608
|
let realRoot;
|
|
9316
9609
|
try {
|
|
9317
|
-
realRoot =
|
|
9610
|
+
realRoot = realOf(root, ops);
|
|
9318
9611
|
} catch {
|
|
9319
9612
|
continue;
|
|
9320
9613
|
}
|
|
9321
|
-
if (real
|
|
9614
|
+
if (within3(real, realRoot, ops))
|
|
9322
9615
|
return real;
|
|
9323
9616
|
}
|
|
9324
9617
|
throw new RuntimeError(`${p} is not under this node's workspace roots`);
|
|
@@ -9373,7 +9666,7 @@ class HostRuntime {
|
|
|
9373
9666
|
const directory = resolveUnderRoots(existing, this.roots);
|
|
9374
9667
|
return this.prepared(repo, directory, "worktree", branch, base, true, ctx);
|
|
9375
9668
|
}
|
|
9376
|
-
const directory = resolveUnderRoots(path11.join(repo, ".worktrees", ...branch.split("/")), this.roots);
|
|
9669
|
+
const directory = resolveUnderRoots(path11.posix.join(repo, ".worktrees", ...branch.split("/")), this.roots);
|
|
9377
9670
|
if (existsSync3(directory))
|
|
9378
9671
|
throw new RuntimeError(`${directory} exists and is not the worktree of ${branch}`);
|
|
9379
9672
|
if (base)
|
|
@@ -9405,7 +9698,7 @@ class HostRuntime {
|
|
|
9405
9698
|
const target = resolveUnderRoots(input.workspace.repo, this.roots);
|
|
9406
9699
|
if (existsSync3(target) || isLink3(target))
|
|
9407
9700
|
throw new RuntimeError(`${target} exists; a clone needs a new directory`);
|
|
9408
|
-
const parent = path11.dirname(target);
|
|
9701
|
+
const parent = path11.posix.dirname(target);
|
|
9409
9702
|
mkdirSync6(parent, { recursive: true });
|
|
9410
9703
|
if (resolveUnderRoots(parent, this.roots) !== parent)
|
|
9411
9704
|
throw new RuntimeError(`${parent} moved while preparing`);
|
|
@@ -9777,19 +10070,30 @@ import { accessSync, constants as constants2, readFileSync as readFileSync6, sta
|
|
|
9777
10070
|
import { homedir as homedir3 } from "node:os";
|
|
9778
10071
|
import path12 from "node:path";
|
|
9779
10072
|
var KNOWN = ["opencode", "claude-code", "codex"];
|
|
9780
|
-
function executable(file) {
|
|
10073
|
+
function executable(file, win32 = false) {
|
|
9781
10074
|
try {
|
|
9782
10075
|
if (!statSync3(file).isFile())
|
|
9783
10076
|
return false;
|
|
9784
|
-
|
|
10077
|
+
if (!win32)
|
|
10078
|
+
accessSync(file, constants2.X_OK);
|
|
9785
10079
|
return true;
|
|
9786
10080
|
} catch {
|
|
9787
10081
|
return false;
|
|
9788
10082
|
}
|
|
9789
10083
|
}
|
|
9790
|
-
|
|
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) {
|
|
9791
10093
|
if (!name)
|
|
9792
10094
|
return;
|
|
10095
|
+
if (platform === "win32")
|
|
10096
|
+
return findWindowsExecutable(name, env);
|
|
9793
10097
|
if (path12.isAbsolute(name))
|
|
9794
10098
|
return executable(name) ? name : undefined;
|
|
9795
10099
|
if (name.includes("/"))
|
|
@@ -9803,13 +10107,30 @@ function findExecutable(name, env) {
|
|
|
9803
10107
|
}
|
|
9804
10108
|
return;
|
|
9805
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
|
+
}
|
|
9806
10126
|
function detectHarnesses(options = {}) {
|
|
9807
10127
|
const env = options.env ?? process.env;
|
|
9808
10128
|
const found = new Set(["opencode"]);
|
|
9809
10129
|
const home = options.home ?? homedir3();
|
|
9810
|
-
|
|
10130
|
+
const platform = options.platform ?? process.platform;
|
|
10131
|
+
if (findExecutable(claudeBinary(env, home), env, platform))
|
|
9811
10132
|
found.add("claude-code");
|
|
9812
|
-
if (findExecutable(codexBinary(env, home), env))
|
|
10133
|
+
if (findExecutable(codexBinary(env, home, platform), env, platform))
|
|
9813
10134
|
found.add("codex");
|
|
9814
10135
|
for (const [name, on] of Object.entries(options.overrides ?? {})) {
|
|
9815
10136
|
if (on)
|
|
@@ -9834,12 +10155,21 @@ function claudeBinary(env, home = homedir3()) {
|
|
|
9834
10155
|
} catch {}
|
|
9835
10156
|
return "claude";
|
|
9836
10157
|
}
|
|
9837
|
-
function codexBinary(env, home = homedir3()) {
|
|
10158
|
+
function codexBinary(env, home = homedir3(), platform = process.platform) {
|
|
9838
10159
|
if (env.HUB_CODEX_BIN)
|
|
9839
10160
|
return env.HUB_CODEX_BIN;
|
|
9840
10161
|
const data = env.XDG_DATA_HOME && path12.isAbsolute(env.XDG_DATA_HOME) ? env.XDG_DATA_HOME : path12.join(home, ".local", "share");
|
|
9841
|
-
const
|
|
9842
|
-
|
|
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";
|
|
9843
10173
|
}
|
|
9844
10174
|
// packages/node/src/run.ts
|
|
9845
10175
|
import { homedir as homedir4 } from "node:os";
|
|
@@ -10259,6 +10589,91 @@ class OpenShellCli {
|
|
|
10259
10589
|
}
|
|
10260
10590
|
}
|
|
10261
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
|
+
|
|
10262
10677
|
// packages/runtime-openshell/src/runtime.ts
|
|
10263
10678
|
import { createHash as createHash2 } from "node:crypto";
|
|
10264
10679
|
import { existsSync as existsSync4, mkdtempSync as mkdtempSync2, readFileSync as readFileSync7, rmSync as rmSync7, writeFileSync as writeFileSync3 } from "node:fs";
|
|
@@ -10267,7 +10682,7 @@ import path14 from "node:path";
|
|
|
10267
10682
|
// packages/runtime-openshell/package.json
|
|
10268
10683
|
var package_default3 = {
|
|
10269
10684
|
name: "@huynle/hub-runtime-openshell",
|
|
10270
|
-
version: "0.9.
|
|
10685
|
+
version: "0.9.2",
|
|
10271
10686
|
private: true,
|
|
10272
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).",
|
|
10273
10688
|
license: "MIT",
|
|
@@ -10827,6 +11242,16 @@ async function sweepSandboxes(client, machineID) {
|
|
|
10827
11242
|
|
|
10828
11243
|
// packages/node/src/run.ts
|
|
10829
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
|
+
});
|
|
10830
11255
|
var openCodeServiceFile = (env, home) => path15.join(env.XDG_STATE_HOME || path15.join(home, ".local", "state"), "opencode", "service.json");
|
|
10831
11256
|
function startOpenCodeWarmers(config, service, log, signal) {
|
|
10832
11257
|
const roots = () => config.workspaceRoots;
|
|
@@ -10862,6 +11287,7 @@ async function setupOpenShell(config, credentials, options, env, log) {
|
|
|
10862
11287
|
async function runNode(options) {
|
|
10863
11288
|
const env = options.env ?? process.env;
|
|
10864
11289
|
const home = options.home ?? homedir4();
|
|
11290
|
+
const platform = options.platform ?? process.platform;
|
|
10865
11291
|
const log = options.log ?? ((line) => process.stderr.write(`[hub node] ${line}
|
|
10866
11292
|
`));
|
|
10867
11293
|
const credentials = readSpokeCredentials(env, home);
|
|
@@ -10870,12 +11296,23 @@ async function runNode(options) {
|
|
|
10870
11296
|
return 1;
|
|
10871
11297
|
}
|
|
10872
11298
|
const file = nodeConfigPath(env, home);
|
|
10873
|
-
|
|
11299
|
+
let { config, problems } = readNodeConfig(file, { platform, home });
|
|
10874
11300
|
for (const problem of problems)
|
|
10875
11301
|
log(`${file}: ${problem}`);
|
|
10876
11302
|
if (config.workspaceRoots.length === 0) {
|
|
10877
11303
|
log(`${file} lists no workspaceRoots, so this node can prepare no workspaces; add e.g. {"workspaceRoots": ["${home}/projects"]}`);
|
|
10878
|
-
|
|
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`);
|
|
10879
11316
|
}
|
|
10880
11317
|
if (options.signal.aborted)
|
|
10881
11318
|
return 0;
|
|
@@ -10900,8 +11337,16 @@ async function runNode(options) {
|
|
|
10900
11337
|
});
|
|
10901
11338
|
log(`hub node ${NODE_VERSION}: ${config.slots} slots, roots ${config.workspaceRoots.join(", ")}; host harnesses ${harnesses.join(", ") || "none"}`);
|
|
10902
11339
|
daemon.start();
|
|
10903
|
-
if (harnesses.includes("opencode"))
|
|
10904
|
-
|
|
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
|
+
}
|
|
10905
11350
|
await new Promise((resolve) => {
|
|
10906
11351
|
if (options.signal.aborted)
|
|
10907
11352
|
return resolve();
|
|
@@ -11202,6 +11647,64 @@ NoNewPrivileges=true
|
|
|
11202
11647
|
WantedBy=default.target
|
|
11203
11648
|
`;
|
|
11204
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
|
+
|
|
11205
11708
|
// packages/cli/src/bootstrap/release/assets.ts
|
|
11206
11709
|
var EMBEDDED_TEXT = Object.freeze({
|
|
11207
11710
|
"service/launchd/node": io_github_huynle_hub_node_default,
|
|
@@ -11209,7 +11712,8 @@ var EMBEDDED_TEXT = Object.freeze({
|
|
|
11209
11712
|
"service/launchd/claude-code": io_github_huynle_hub_claude_code_default,
|
|
11210
11713
|
"service/systemd-user/claude-code": hub_claude_code_default,
|
|
11211
11714
|
"service/launchd/codex-spoke": io_github_huynle_hub_codex_spoke_default,
|
|
11212
|
-
"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
|
|
11213
11717
|
});
|
|
11214
11718
|
|
|
11215
11719
|
// packages/cli/src/bootstrap/adapters/node-services/render.ts
|
|
@@ -11218,7 +11722,34 @@ var SERVICE_ENVIRONMENT = Object.freeze({
|
|
|
11218
11722
|
"claude-code": ["HUB_SPOKE_CONFIG", "XDG_CONFIG_HOME", "HUB_STATE_DIR", "XDG_STATE_HOME", "HUB_CLAUDE_SOCKET", "HUB_CLAUDE_BIN"],
|
|
11219
11723
|
"codex-spoke": ["HUB_SPOKE_CONFIG", "XDG_CONFIG_HOME", "HUB_STATE_DIR", "XDG_STATE_HOME", "CODEX_HOME", "HUB_CODEX_BIN", "HUB_CODEX_DAEMON_SOCKET"]
|
|
11220
11724
|
});
|
|
11221
|
-
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
|
+
}
|
|
11222
11753
|
var assetIDFor = (service, manager) => `service/${manager}/${service}`;
|
|
11223
11754
|
function serviceName(service, manager) {
|
|
11224
11755
|
const file = EMBEDDED_ASSETS[assetIDFor(service, manager)].installAs;
|
|
@@ -11278,6 +11809,40 @@ ${indent}<string>${xml(value)}</string>`).join("");
|
|
|
11278
11809
|
return template.replace(/<!--[\s\S]*?-->/, () => `<!-- ${header2(asset, by)} -->`).replace(programArguments, (_all, open, _words, close) => `${open}${argv.map((w) => `
|
|
11279
11810
|
${indent}<string>${xml(w)}</string>`).join("")}${close}`).replaceAll("__HOME__", xml(home)).replace(/(<key>PATH<\/key>\s*<string>[^<]*<\/string>)/, (line) => `${line}${extra}`);
|
|
11280
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
|
+
};
|
|
11281
11846
|
function renderServiceUnit(input) {
|
|
11282
11847
|
const c = new Checker;
|
|
11283
11848
|
const asset = assetIDFor(input.service, input.manager);
|
|
@@ -11287,22 +11852,44 @@ function renderServiceUnit(input) {
|
|
|
11287
11852
|
c.add("/service", "invalid_value", `no embedded template for ${input.service} under ${input.manager}`);
|
|
11288
11853
|
return c.result(undefined);
|
|
11289
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;
|
|
11290
11877
|
let argv = [];
|
|
11291
11878
|
try {
|
|
11292
11879
|
argv = serviceArgv(input.layout, input.service);
|
|
11293
11880
|
} catch (error) {
|
|
11294
11881
|
c.add("/layout", "invalid_path", error.message);
|
|
11295
11882
|
}
|
|
11296
|
-
const homeOK = isAbsolutePath(input.home) && input.home !== "/" && (
|
|
11883
|
+
const homeOK = isAbsolutePath(input.home) && input.home !== "/" && (manager !== "systemd-user" || SYSTEMD_HOME.test(input.home));
|
|
11297
11884
|
if (!homeOK)
|
|
11298
11885
|
c.add("/home", "invalid_path", "the service's home must be an absolute, normalized directory other than /");
|
|
11299
11886
|
const env = checkEnvironment(c, input.service, input.environment ?? {});
|
|
11300
11887
|
if (c.issues.length > 0)
|
|
11301
11888
|
return c.result(undefined);
|
|
11302
11889
|
const command = SERVICE_COMMANDS[input.service];
|
|
11303
|
-
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);
|
|
11304
11891
|
const asBun = input.layout.kind === "script";
|
|
11305
|
-
const text = asBun ? raw : raw.replace(BUN_BE_BUN[
|
|
11892
|
+
const text = asBun ? raw : raw.replace(BUN_BE_BUN[manager], "");
|
|
11306
11893
|
if (!asBun && text.includes("BUN_BE_BUN"))
|
|
11307
11894
|
c.add("/asset", "inconsistent", `${asset} sets BUN_BE_BUN, which a standalone hubctl must not get`);
|
|
11308
11895
|
if (PLACEHOLDER.test(text))
|
|
@@ -11317,7 +11904,7 @@ function renderServiceUnit(input) {
|
|
|
11317
11904
|
service: input.service,
|
|
11318
11905
|
manager: input.manager,
|
|
11319
11906
|
path: { root: UNIT_ROOT[input.manager], rel: entry.installAs },
|
|
11320
|
-
name: serviceName(input.service,
|
|
11907
|
+
name: serviceName(input.service, manager),
|
|
11321
11908
|
argv,
|
|
11322
11909
|
text,
|
|
11323
11910
|
bytes,
|
|
@@ -11325,6 +11912,39 @@ function renderServiceUnit(input) {
|
|
|
11325
11912
|
});
|
|
11326
11913
|
}
|
|
11327
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
|
+
|
|
11328
11948
|
// packages/cli/src/node.ts
|
|
11329
11949
|
var NODE_LABEL = "io.github.huynle.hub-node";
|
|
11330
11950
|
var NODE_UNIT = "hub-node.service";
|
|
@@ -11399,13 +12019,58 @@ function systemd(spec, io, extra) {
|
|
|
11399
12019
|
check: [`systemctl --user status ${unit}`, `journalctl --user -u ${unit} -f`]
|
|
11400
12020
|
};
|
|
11401
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
|
+
}
|
|
11402
12058
|
function serviceFor(spec, io, extra) {
|
|
11403
12059
|
if (io.platform === "darwin")
|
|
11404
12060
|
return launchd(spec, io, extra);
|
|
11405
12061
|
if (io.platform === "linux")
|
|
11406
12062
|
return systemd(spec, io, extra);
|
|
12063
|
+
if (io.platform === "win32")
|
|
12064
|
+
return taskScheduler(spec, io, extra);
|
|
11407
12065
|
throw new CliError(EXIT.unavailable, `${spec.command} install supports macOS (launchd) and Linux (systemd), not ${io.platform}`, spec.elsewhere);
|
|
11408
12066
|
}
|
|
12067
|
+
var readBytes = (file) => {
|
|
12068
|
+
try {
|
|
12069
|
+
return readFileSync8(file);
|
|
12070
|
+
} catch {
|
|
12071
|
+
return;
|
|
12072
|
+
}
|
|
12073
|
+
};
|
|
11409
12074
|
var readText = (file) => {
|
|
11410
12075
|
try {
|
|
11411
12076
|
return readFileSync8(file, "utf8");
|
|
@@ -11432,21 +12097,22 @@ function installNode(io, options) {
|
|
|
11432
12097
|
}
|
|
11433
12098
|
function installService(spec, io, options, given, extra) {
|
|
11434
12099
|
const service = serviceFor(spec, io, extra);
|
|
11435
|
-
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}`)];
|
|
11436
12102
|
const load = commands(`Load it ${NOT_RUN}:`, service.load);
|
|
11437
12103
|
if (options.dryRun)
|
|
11438
12104
|
return { stdout: `would write ${service.file}:
|
|
11439
12105
|
|
|
11440
12106
|
${service.content}
|
|
11441
12107
|
${load}`, warnings };
|
|
11442
|
-
const current = readText(service.file);
|
|
12108
|
+
const current = service.bytes ? readBytes(service.file) : readText(service.file);
|
|
11443
12109
|
for (const dir of service.dirs)
|
|
11444
12110
|
mkdirSync7(dir, { recursive: true });
|
|
11445
|
-
if (current === service.content)
|
|
12111
|
+
if (service.bytes ? current !== undefined && Buffer.from(service.bytes).equals(current) : current === service.content)
|
|
11446
12112
|
return { stdout: `${service.file} is up to date
|
|
11447
12113
|
${load}`, warnings };
|
|
11448
12114
|
mkdirSync7(path16.dirname(service.file), { recursive: true });
|
|
11449
|
-
writeFileSync4(service.file, service.content, { mode: 420 });
|
|
12115
|
+
writeFileSync4(service.file, service.bytes ?? service.content, { mode: 420 });
|
|
11450
12116
|
return { stdout: `${current === undefined ? "wrote" : "updated"} ${service.file}
|
|
11451
12117
|
${load}`, warnings };
|
|
11452
12118
|
}
|
|
@@ -11468,6 +12134,10 @@ ${stop}`, warnings: [] };
|
|
|
11468
12134
|
function serviceState(spec, io, extra) {
|
|
11469
12135
|
try {
|
|
11470
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
|
+
}
|
|
11471
12141
|
const text = readText(s.file);
|
|
11472
12142
|
return { kind: s.kind, file: s.file, installed: text !== undefined, current: text === s.content, check: s.check };
|
|
11473
12143
|
} catch {
|
|
@@ -11508,9 +12178,28 @@ function nodeStatusText(status) {
|
|
|
11508
12178
|
return lines.join(`
|
|
11509
12179
|
`);
|
|
11510
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
|
+
}
|
|
11511
12200
|
function runNodeCommand(io) {
|
|
11512
|
-
|
|
11513
|
-
|
|
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 });
|
|
11514
12203
|
}
|
|
11515
12204
|
|
|
11516
12205
|
// packages/cli/src/openshell.ts
|
|
@@ -12745,8 +13434,8 @@ async function runMcpPipe(options) {
|
|
|
12745
13434
|
}
|
|
12746
13435
|
|
|
12747
13436
|
// packages/claude-code/src/launcher.ts
|
|
12748
|
-
import { spawn as
|
|
12749
|
-
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";
|
|
12750
13439
|
import path19 from "node:path";
|
|
12751
13440
|
var INTERRUPTED = "Interrupted from the hub";
|
|
12752
13441
|
var CONTROL_WAIT_MS = 5000;
|
|
@@ -12755,7 +13444,7 @@ var KILL_GRACE_MS = 1000;
|
|
|
12755
13444
|
var LINE_MAX = 16 * 1024 * 1024;
|
|
12756
13445
|
var STDERR_KEEP = 2000;
|
|
12757
13446
|
var record2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : {};
|
|
12758
|
-
var
|
|
13447
|
+
var sleep6 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
12759
13448
|
var AGENT2 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
|
|
12760
13449
|
var MODEL = /^[A-Za-z0-9][A-Za-z0-9._:@/\[\]-]{0,299}$/;
|
|
12761
13450
|
var PROVIDERS = new Set(["anthropic", "claude-code"]);
|
|
@@ -12769,7 +13458,7 @@ function launchDirectory(directory, roots) {
|
|
|
12769
13458
|
} catch {
|
|
12770
13459
|
throw new Error(`${directory} does not exist`);
|
|
12771
13460
|
}
|
|
12772
|
-
if (!
|
|
13461
|
+
if (!statSync5(real).isDirectory())
|
|
12773
13462
|
throw new Error(`${directory} is not a directory`);
|
|
12774
13463
|
for (const root of roots) {
|
|
12775
13464
|
let base;
|
|
@@ -12843,7 +13532,7 @@ class ClaudeLauncher {
|
|
|
12843
13532
|
delete env.BUN_BE_BUN;
|
|
12844
13533
|
let child;
|
|
12845
13534
|
try {
|
|
12846
|
-
child =
|
|
13535
|
+
child = spawn4(this.#options.bin, launchArgs(input), { cwd: input.directory, env, stdio: ["pipe", "pipe", "pipe"] });
|
|
12847
13536
|
} catch (cause) {
|
|
12848
13537
|
throw new Error(`cannot run ${this.#options.bin}: ${cause.message}`);
|
|
12849
13538
|
}
|
|
@@ -12931,7 +13620,7 @@ class ClaudeLauncher {
|
|
|
12931
13620
|
const answered = new Promise((resolve) => proc.controls.set(id, resolve));
|
|
12932
13621
|
proc.child.stdin.write(`${JSON.stringify({ type: "control_request", request_id: id, request })}
|
|
12933
13622
|
`);
|
|
12934
|
-
const response = await Promise.race([answered,
|
|
13623
|
+
const response = await Promise.race([answered, sleep6(waitMs).then(() => {
|
|
12935
13624
|
return;
|
|
12936
13625
|
})]);
|
|
12937
13626
|
proc.controls.delete(id);
|
|
@@ -12942,10 +13631,10 @@ class ClaudeLauncher {
|
|
|
12942
13631
|
for (const proc of procs)
|
|
12943
13632
|
proc.child.stdin.end();
|
|
12944
13633
|
await Promise.all(procs.map(async (proc) => {
|
|
12945
|
-
if (await Promise.race([proc.exited.then(() => true),
|
|
13634
|
+
if (await Promise.race([proc.exited.then(() => true), sleep6(STOP_GRACE_MS).then(() => false)]))
|
|
12946
13635
|
return;
|
|
12947
13636
|
proc.child.kill("SIGTERM");
|
|
12948
|
-
if (await Promise.race([proc.exited.then(() => true),
|
|
13637
|
+
if (await Promise.race([proc.exited.then(() => true), sleep6(KILL_GRACE_MS).then(() => false)]))
|
|
12949
13638
|
return;
|
|
12950
13639
|
proc.child.kill("SIGKILL");
|
|
12951
13640
|
await proc.exited;
|
|
@@ -13944,14 +14633,14 @@ async function startDaemon(options) {
|
|
|
13944
14633
|
}
|
|
13945
14634
|
|
|
13946
14635
|
// packages/claude-code/src/wrapper.ts
|
|
13947
|
-
import { spawn as
|
|
14636
|
+
import { spawn as spawn5 } from "node:child_process";
|
|
13948
14637
|
import { randomBytes as randomBytes9 } from "node:crypto";
|
|
13949
14638
|
import { closeSync as closeSync5, openSync as openSync5 } from "node:fs";
|
|
13950
14639
|
import path23 from "node:path";
|
|
13951
14640
|
// packages/claude-code/package.json
|
|
13952
14641
|
var package_default4 = {
|
|
13953
14642
|
name: "@huynle/hub-claude-code",
|
|
13954
|
-
version: "0.9.
|
|
14643
|
+
version: "0.9.2",
|
|
13955
14644
|
private: true,
|
|
13956
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).",
|
|
13957
14646
|
license: "MIT",
|
|
@@ -13964,7 +14653,7 @@ var package_default4 = {
|
|
|
13964
14653
|
|
|
13965
14654
|
// packages/claude-code/src/plugin.ts
|
|
13966
14655
|
import { createHash as createHash5 } from "node:crypto";
|
|
13967
|
-
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";
|
|
13968
14657
|
import path22 from "node:path";
|
|
13969
14658
|
var HOOK_EVENTS = [
|
|
13970
14659
|
"SessionStart",
|
|
@@ -14022,7 +14711,7 @@ function writePlugin(base, options) {
|
|
|
14022
14711
|
writeFileSync6(path22.join(tmp, file), content, { mode: 384 });
|
|
14023
14712
|
}
|
|
14024
14713
|
try {
|
|
14025
|
-
|
|
14714
|
+
renameSync6(tmp, dir);
|
|
14026
14715
|
} catch (cause) {
|
|
14027
14716
|
rmSync9(tmp, { recursive: true, force: true });
|
|
14028
14717
|
if (!existsSync7(dir))
|
|
@@ -14048,7 +14737,7 @@ function spawnDaemon(io) {
|
|
|
14048
14737
|
ensurePrivateDir(dir);
|
|
14049
14738
|
const log = openSync5(path23.join(dir, "daemon.log"), "a", 384);
|
|
14050
14739
|
try {
|
|
14051
|
-
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" } });
|
|
14052
14741
|
child.on("error", () => {});
|
|
14053
14742
|
child.unref();
|
|
14054
14743
|
} finally {
|
|
@@ -14087,7 +14776,7 @@ async function prepare(io, socket) {
|
|
|
14087
14776
|
}
|
|
14088
14777
|
function spawnClaude(bin, args, env, stderr) {
|
|
14089
14778
|
return new Promise((resolve) => {
|
|
14090
|
-
const child =
|
|
14779
|
+
const child = spawn5(bin, args, { stdio: "inherit", windowsHide: true, env });
|
|
14091
14780
|
const ignore = () => {};
|
|
14092
14781
|
const forward = () => child.kill("SIGTERM");
|
|
14093
14782
|
process.on("SIGINT", ignore);
|
|
@@ -14240,7 +14929,7 @@ import path26 from "node:path";
|
|
|
14240
14929
|
// packages/preset/package.json
|
|
14241
14930
|
var package_default5 = {
|
|
14242
14931
|
name: "@huynle/hub-preset",
|
|
14243
|
-
version: "0.9.
|
|
14932
|
+
version: "0.9.2",
|
|
14244
14933
|
private: true,
|
|
14245
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).",
|
|
14246
14935
|
license: "MIT",
|
|
@@ -14516,12 +15205,12 @@ class PresetSpoke {
|
|
|
14516
15205
|
}
|
|
14517
15206
|
|
|
14518
15207
|
// packages/preset/src/tmux.ts
|
|
14519
|
-
import { execFile } from "node:child_process";
|
|
15208
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
14520
15209
|
import { randomBytes as randomBytes10 } from "node:crypto";
|
|
14521
15210
|
var TMUX_TIMEOUT_MS = 5000;
|
|
14522
15211
|
function tmuxRunner(bin = "tmux", env = process.env, timeoutMs = TMUX_TIMEOUT_MS) {
|
|
14523
15212
|
return (args) => new Promise((resolve) => {
|
|
14524
|
-
|
|
15213
|
+
execFile2(bin, args, { env, timeout: timeoutMs, encoding: "utf8", maxBuffer: 4 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
14525
15214
|
const exit = error?.code;
|
|
14526
15215
|
const code = !error ? 0 : typeof exit === "number" ? exit : 1;
|
|
14527
15216
|
resolve({ code, stdout: stdout ?? "", stderr: stderr || (error ? error.message : "") });
|
|
@@ -14765,7 +15454,7 @@ import { openSync as openSync6, closeSync as closeSync6, readFileSync as readFil
|
|
|
14765
15454
|
// packages/codex/package.json
|
|
14766
15455
|
var package_default6 = {
|
|
14767
15456
|
name: "@huynle/hub-codex",
|
|
14768
|
-
version: "0.9.
|
|
15457
|
+
version: "0.9.2",
|
|
14769
15458
|
private: true,
|
|
14770
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).",
|
|
14771
15460
|
license: "MIT",
|
|
@@ -14965,7 +15654,7 @@ async function openAppServer(channel, options) {
|
|
|
14965
15654
|
|
|
14966
15655
|
// packages/codex/src/profile.ts
|
|
14967
15656
|
import { randomBytes as randomBytes11 } from "node:crypto";
|
|
14968
|
-
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";
|
|
14969
15658
|
import path28 from "node:path";
|
|
14970
15659
|
var CODEX_PROFILE_CAPABILITIES = Object.freeze({
|
|
14971
15660
|
instructions: true,
|
|
@@ -15059,7 +15748,7 @@ class CodexProfiles {
|
|
|
15059
15748
|
mkdirSync11(path28.join(temp, "skills", skill.name), { recursive: true, mode: 448 });
|
|
15060
15749
|
writeFileSync7(path28.join(temp, "skills", skill.name, "SKILL.md"), skillFile(skill), { mode: 384 });
|
|
15061
15750
|
}
|
|
15062
|
-
|
|
15751
|
+
renameSync7(temp, versionDir);
|
|
15063
15752
|
} catch (cause) {
|
|
15064
15753
|
rmSync10(temp, { recursive: true, force: true });
|
|
15065
15754
|
throw cause;
|
|
@@ -15093,7 +15782,7 @@ class CodexProfiles {
|
|
|
15093
15782
|
}
|
|
15094
15783
|
|
|
15095
15784
|
// packages/codex/src/sessions.ts
|
|
15096
|
-
import { statSync as
|
|
15785
|
+
import { statSync as statSync6 } from "node:fs";
|
|
15097
15786
|
import path29 from "node:path";
|
|
15098
15787
|
import { randomBytes as randomBytes13 } from "node:crypto";
|
|
15099
15788
|
|
|
@@ -15123,7 +15812,7 @@ var textInput = (text) => ({ type: "text", text, text_elements: [] });
|
|
|
15123
15812
|
function gitRoot2(cwd) {
|
|
15124
15813
|
for (let dir = path29.resolve(cwd);; dir = path29.dirname(dir)) {
|
|
15125
15814
|
try {
|
|
15126
|
-
|
|
15815
|
+
statSync6(path29.join(dir, ".git"));
|
|
15127
15816
|
return dir;
|
|
15128
15817
|
} catch {}
|
|
15129
15818
|
if (path29.dirname(dir) === dir)
|
|
@@ -15391,7 +16080,7 @@ class CodexSessions {
|
|
|
15391
16080
|
throw new Error(`The directory must be an absolute path (got ${JSON.stringify(req.directory)})`);
|
|
15392
16081
|
let isDir = false;
|
|
15393
16082
|
try {
|
|
15394
|
-
isDir =
|
|
16083
|
+
isDir = statSync6(req.directory).isDirectory();
|
|
15395
16084
|
} catch {}
|
|
15396
16085
|
if (!isDir)
|
|
15397
16086
|
throw new Error(`${req.directory} does not exist or is not a directory`);
|
|
@@ -15574,7 +16263,7 @@ function input(text, images) {
|
|
|
15574
16263
|
}
|
|
15575
16264
|
|
|
15576
16265
|
// packages/codex/src/stdio.ts
|
|
15577
|
-
import { spawn as
|
|
16266
|
+
import { spawn as spawn6 } from "node:child_process";
|
|
15578
16267
|
var STDIO_MAX_LINE = 16 * 1024 * 1024;
|
|
15579
16268
|
var KILL_GRACE_MS2 = 2000;
|
|
15580
16269
|
var LOG_LINE_MAX = 2000;
|
|
@@ -15590,7 +16279,7 @@ function spawnStdioChannel(options) {
|
|
|
15590
16279
|
cb(reason);
|
|
15591
16280
|
};
|
|
15592
16281
|
const env = Object.fromEntries(Object.entries(options.env).filter(([, v]) => v !== undefined));
|
|
15593
|
-
const child =
|
|
16282
|
+
const child = spawn6(options.command, [...options.args], { cwd: options.cwd, env, stdio: ["pipe", "pipe", "pipe"] });
|
|
15594
16283
|
let exitDone;
|
|
15595
16284
|
const exited = new Promise((resolve) => exitDone = resolve);
|
|
15596
16285
|
child.on("error", (error) => {
|
|
@@ -16251,7 +16940,7 @@ function spokeServiceStatusText(name, status) {
|
|
|
16251
16940
|
}
|
|
16252
16941
|
|
|
16253
16942
|
// packages/codex/src/wrapper.ts
|
|
16254
|
-
import { spawn as
|
|
16943
|
+
import { spawn as spawn7 } from "node:child_process";
|
|
16255
16944
|
import { closeSync as closeSync7, openSync as openSync7 } from "node:fs";
|
|
16256
16945
|
import path32 from "node:path";
|
|
16257
16946
|
var SERVICE_START_MS = 8000;
|
|
@@ -16261,7 +16950,7 @@ function spawnService(io) {
|
|
|
16261
16950
|
ensurePrivateDir2(dir);
|
|
16262
16951
|
const log = openSync7(path32.join(dir, "spoke.log"), "a", 384);
|
|
16263
16952
|
try {
|
|
16264
|
-
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" } });
|
|
16265
16954
|
child.on("error", () => {});
|
|
16266
16955
|
child.unref();
|
|
16267
16956
|
} finally {
|
|
@@ -16290,7 +16979,7 @@ async function ensureService(io) {
|
|
|
16290
16979
|
var SIGNALS2 = { SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGKILL: 9, SIGTERM: 15 };
|
|
16291
16980
|
function spawnCodex(bin, args, env, stderr) {
|
|
16292
16981
|
return new Promise((resolve) => {
|
|
16293
|
-
const child =
|
|
16982
|
+
const child = spawn7(bin, args, { stdio: "inherit", windowsHide: true, env });
|
|
16294
16983
|
const ignore = () => {};
|
|
16295
16984
|
const forward = () => child.kill("SIGTERM");
|
|
16296
16985
|
process.on("SIGINT", ignore);
|
|
@@ -16324,7 +17013,7 @@ async function runCodex(args, given) {
|
|
|
16324
17013
|
}
|
|
16325
17014
|
|
|
16326
17015
|
// packages/cli/src/setup/commands.ts
|
|
16327
|
-
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";
|
|
16328
17017
|
import { hostname as hostname4 } from "node:os";
|
|
16329
17018
|
import path35 from "node:path";
|
|
16330
17019
|
|
|
@@ -18243,50 +18932,15 @@ function renderYAML(vars) {
|
|
|
18243
18932
|
"# the profile and the pins must stay exactly what was previewed and signed.",
|
|
18244
18933
|
...VAR_ORDER.map((key) => `${key}: ${canonicalJSON(vars[key])}`)
|
|
18245
18934
|
];
|
|
18246
|
-
return `${lines.join(`
|
|
18247
|
-
`)}
|
|
18248
|
-
`;
|
|
18249
|
-
}
|
|
18250
|
-
|
|
18251
|
-
// packages/cli/src/bootstrap/discovery/node-probe.ts
|
|
18252
|
-
import childProcess from "node:child_process";
|
|
18253
|
-
import fs from "node:fs";
|
|
18254
|
-
import os from "node:os";
|
|
18255
|
-
|
|
18256
|
-
// packages/cli/src/bootstrap/discovery/util.ts
|
|
18257
|
-
function normalizeAbs(value) {
|
|
18258
|
-
if (typeof value !== "string")
|
|
18259
|
-
return;
|
|
18260
|
-
let p = value;
|
|
18261
|
-
if (/^[A-Za-z]:[\\/]/.test(p))
|
|
18262
|
-
p = p[0].toUpperCase() + p.slice(1).replace(/\\/g, "/");
|
|
18263
|
-
else if (!p.startsWith("/") || p.startsWith("//"))
|
|
18264
|
-
return;
|
|
18265
|
-
p = p.replace(/\/{2,}/g, "/");
|
|
18266
|
-
if (p.endsWith("/") && p !== "/" && !/^[A-Z]:\/$/.test(p))
|
|
18267
|
-
p = p.slice(0, -1);
|
|
18268
|
-
return isAbsolutePath(p) ? p : undefined;
|
|
18269
|
-
}
|
|
18270
|
-
function canonicalWinLink(text) {
|
|
18271
|
-
const stripped = text.replace(/^(\\\\\?\\|\\\?\?\\)/, "");
|
|
18272
|
-
return normalizeAbs(stripped) ?? text;
|
|
18935
|
+
return `${lines.join(`
|
|
18936
|
+
`)}
|
|
18937
|
+
`;
|
|
18273
18938
|
}
|
|
18274
|
-
var probePath = (platform, text) => text !== undefined && platform === "win32" ? canonicalWinLink(text) : text;
|
|
18275
|
-
var join = (dir, rel) => dir.endsWith("/") ? `${dir}${rel}` : `${dir}/${rel}`;
|
|
18276
|
-
var decode = (bytes) => bytes ? new TextDecoder("utf-8", { fatal: false }).decode(bytes) : "";
|
|
18277
|
-
var oneLine2 = (v, max = 64) => v && v.length <= max && !/[\u0000-\u001f\u007f]/.test(v) ? v : undefined;
|
|
18278
|
-
var hostAbs = (platform, value) => {
|
|
18279
|
-
const p = normalizeAbs(value);
|
|
18280
|
-
return p && (platform !== "win32" || isWindowsPath(p)) ? p : undefined;
|
|
18281
|
-
};
|
|
18282
|
-
var underDir = (p, dir) => {
|
|
18283
|
-
const prefix = dir.endsWith("/") ? dir : `${dir}/`;
|
|
18284
|
-
return isWindowsPath(p) ? p.toLowerCase().startsWith(prefix.toLowerCase()) : p.startsWith(prefix);
|
|
18285
|
-
};
|
|
18286
|
-
var WINDOWS_COMMAND_EXTENSIONS = [".com", ".exe", ".bat", ".cmd"];
|
|
18287
|
-
var isWindowsExecutable = (path) => WINDOWS_COMMAND_EXTENSIONS.some((e) => path.toLowerCase().endsWith(e));
|
|
18288
18939
|
|
|
18289
18940
|
// 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";
|
|
18290
18944
|
var MAX_READ_BYTES = 16 * 1024 * 1024;
|
|
18291
18945
|
var kindOf = (st) => st.isSymbolicLink() ? "symlink" : st.isDirectory() ? "directory" : st.isFile() ? "file" : "other";
|
|
18292
18946
|
function attempt(f) {
|
|
@@ -18385,6 +19039,10 @@ function createNodeProbe(options = {}) {
|
|
|
18385
19039
|
}
|
|
18386
19040
|
// packages/cli/src/bootstrap/profiles/desired.ts
|
|
18387
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
|
+
}
|
|
18388
19046
|
|
|
18389
19047
|
// packages/cli/src/bootstrap/preview/plan.ts
|
|
18390
19048
|
var PHASE = {
|
|
@@ -18432,6 +19090,11 @@ function planSetup(input) {
|
|
|
18432
19090
|
return failed(issuesAt("/snapshot/host/target", target.ok ? fail("", "unsupported_platform", "unsupported host") : target));
|
|
18433
19091
|
if (profile.services.length > 0 && target.value.serviceManager === "none")
|
|
18434
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
|
+
}
|
|
18435
19098
|
if (profile.services.length > 0 && snapshot.managers.service === "none")
|
|
18436
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)`);
|
|
18437
19100
|
const planInput = { snapshot, profile };
|
|
@@ -18729,6 +19392,8 @@ function discoverRoots(probe) {
|
|
|
18729
19392
|
paths["launch-agents"] = join(home, "Library/LaunchAgents");
|
|
18730
19393
|
if (probe.platform === "linux")
|
|
18731
19394
|
paths["systemd-user"] = join(config, "systemd/user");
|
|
19395
|
+
if (windows)
|
|
19396
|
+
paths["task-defs"] = join(paths.state, "hub/tasks");
|
|
18732
19397
|
return ROOT_IDS.flatMap((id) => paths[id] ? [{ id, path: paths[id] }] : []);
|
|
18733
19398
|
}
|
|
18734
19399
|
function hubStateRoot(probe, home) {
|
|
@@ -18780,17 +19445,17 @@ function discoverLibc(probe) {
|
|
|
18780
19445
|
function discoverOS(probe) {
|
|
18781
19446
|
if (probe.platform === "darwin") {
|
|
18782
19447
|
const plist = decode(probe.readFile("/System/Library/CoreServices/SystemVersion.plist", 64 * 1024));
|
|
18783
|
-
const value = (key) =>
|
|
19448
|
+
const value = (key) => oneLine3(new RegExp(`<key>${key}</key>\\s*<string>([^<]*)</string>`).exec(plist)?.[1]);
|
|
18784
19449
|
return { name: value("ProductName") ?? "macOS", version: value("ProductVersion") ?? "unknown" };
|
|
18785
19450
|
}
|
|
18786
19451
|
if (probe.platform === "linux") {
|
|
18787
19452
|
const text = decode(probe.readFile("/etc/os-release", 64 * 1024) ?? probe.readFile("/usr/lib/os-release", 64 * 1024));
|
|
18788
|
-
const value = (key) =>
|
|
19453
|
+
const value = (key) => oneLine3(new RegExp(`^${key}=(.*)$`, "m").exec(text)?.[1]?.trim().replace(/^(["'])(.*)\1$/, "$2"));
|
|
18789
19454
|
return { name: value("NAME") ?? "Linux", version: value("VERSION_ID") ?? "unknown" };
|
|
18790
19455
|
}
|
|
18791
19456
|
if (probe.platform === "win32")
|
|
18792
|
-
return { name:
|
|
18793
|
-
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" };
|
|
18794
19459
|
}
|
|
18795
19460
|
function discoverContainer(probe) {
|
|
18796
19461
|
if (probe.platform !== "linux")
|
|
@@ -18840,10 +19505,13 @@ function configCandidates(platform, roots, context = "host") {
|
|
|
18840
19505
|
add("launch-agents", asset.installAs, "hub", "plist");
|
|
18841
19506
|
if (platform === "linux" && asset.manager === "systemd-user")
|
|
18842
19507
|
add("systemd-user", asset.installAs, "hub", "unit");
|
|
19508
|
+
if (platform === "win32" && asset.manager === "task-scheduler")
|
|
19509
|
+
add("task-defs", asset.installAs, "hub", "xml");
|
|
18843
19510
|
}
|
|
18844
19511
|
return list.filter((c) => c !== undefined);
|
|
18845
19512
|
}
|
|
18846
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";
|
|
18847
19515
|
function discover(probe, options) {
|
|
18848
19516
|
let roots = discoverRoots(probe);
|
|
18849
19517
|
if (!roots)
|
|
@@ -18865,6 +19533,8 @@ function discover(probe, options) {
|
|
|
18865
19533
|
service = "launchd";
|
|
18866
19534
|
if (probe.platform === "linux" && probe.lstat("/run/systemd/system") === "directory" && which(probe, [...dirs, "/usr/bin", "/bin"], "systemctl"))
|
|
18867
19535
|
service = "systemd-user";
|
|
19536
|
+
if (probe.platform === "win32" && probe.executable(join(systemRootOf2(probe), "System32/schtasks.exe")))
|
|
19537
|
+
service = "task-scheduler";
|
|
18868
19538
|
const shims = [];
|
|
18869
19539
|
const harnesses = HARNESS_ORDER.map((id) => {
|
|
18870
19540
|
const name = HARNESS_BINARIES[id];
|
|
@@ -19262,7 +19932,7 @@ function installedHarnesses(history) {
|
|
|
19262
19932
|
}
|
|
19263
19933
|
function installedServiceUnits(history, manager) {
|
|
19264
19934
|
const byFile = new Map(Object.values(EMBEDDED_ASSETS).filter((a) => a.manager === manager).map((a) => [a.installAs, a.service]));
|
|
19265
|
-
const root = manager
|
|
19935
|
+
const root = SERVICE_ROOT[manager];
|
|
19266
19936
|
const units = {};
|
|
19267
19937
|
for (const tx of history) {
|
|
19268
19938
|
if (tx.outcome !== "committed")
|
|
@@ -19383,7 +20053,7 @@ import {
|
|
|
19383
20053
|
readFileSync as readFileSync12,
|
|
19384
20054
|
readlinkSync as readlinkSync2,
|
|
19385
20055
|
readSync as readSync2,
|
|
19386
|
-
renameSync as
|
|
20056
|
+
renameSync as renameSync8,
|
|
19387
20057
|
rmdirSync,
|
|
19388
20058
|
symlinkSync as symlinkSync2,
|
|
19389
20059
|
unlinkSync as unlinkSync2,
|
|
@@ -19494,7 +20164,7 @@ var posixFs = {
|
|
|
19494
20164
|
abort: () => finish(false)
|
|
19495
20165
|
};
|
|
19496
20166
|
},
|
|
19497
|
-
rename: (from, to) =>
|
|
20167
|
+
rename: (from, to) => renameSync8(from, to),
|
|
19498
20168
|
link: (from, to) => linkSync(from, to),
|
|
19499
20169
|
unlink: (path) => unlinkSync2(path),
|
|
19500
20170
|
mkdir: (path, mode) => mkdirSync12(path, { mode }),
|
|
@@ -20585,208 +21255,6 @@ import { closeSync as closeSync9, mkdtempSync as mkdtempSync3, openSync as openS
|
|
|
20585
21255
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
20586
21256
|
import { join as join2 } from "node:path";
|
|
20587
21257
|
|
|
20588
|
-
// packages/cli/src/bootstrap/adapters/process-tree.ts
|
|
20589
|
-
import { execFile as execFile2, spawn as nodeSpawn } from "node:child_process";
|
|
20590
|
-
var TOOL_TIMEOUT_MS = 15000;
|
|
20591
|
-
var START_SLACK_MS = 2000;
|
|
20592
|
-
var FILETIME_EPOCH_MS = 11644473600000n;
|
|
20593
|
-
var defaultExec = (file, args) => new Promise((resolve) => {
|
|
20594
|
-
execFile2(file, [...args], { shell: false, windowsHide: true, timeout: TOOL_TIMEOUT_MS, encoding: "utf8", maxBuffer: 1 << 20 }, (error, stdout) => {
|
|
20595
|
-
const code = error?.code;
|
|
20596
|
-
resolve({ code: error ? typeof code === "number" ? code : null : 0, stdout: String(stdout ?? "") });
|
|
20597
|
-
});
|
|
20598
|
-
});
|
|
20599
|
-
function systemRootOf(value) {
|
|
20600
|
-
const v = value?.replace(/\\+$/, "");
|
|
20601
|
-
return v && /^[A-Za-z]:\\[A-Za-z0-9 ._\\()-]*$/.test(v) ? v : "C:\\Windows";
|
|
20602
|
-
}
|
|
20603
|
-
function processTree(deps = {}) {
|
|
20604
|
-
const platform = deps.platform ?? process.platform;
|
|
20605
|
-
if (platform === "win32") {
|
|
20606
|
-
const exec = deps.exec ?? defaultExec;
|
|
20607
|
-
const root = systemRootOf(deps.systemRoot ?? process.env.SystemRoot ?? process.env.SYSTEMROOT);
|
|
20608
|
-
const taskkill = `${root}\\System32\\taskkill.exe`;
|
|
20609
|
-
const powershell = `${root}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`;
|
|
20610
|
-
const killTree = async (pid) => {
|
|
20611
|
-
if (!Number.isInteger(pid) || pid <= 0)
|
|
20612
|
-
return;
|
|
20613
|
-
try {
|
|
20614
|
-
await exec(taskkill, ["/T", "/F", "/PID", String(pid)]);
|
|
20615
|
-
} catch {}
|
|
20616
|
-
};
|
|
20617
|
-
return {
|
|
20618
|
-
detached: false,
|
|
20619
|
-
killTree,
|
|
20620
|
-
async reap(pid, startedAt) {
|
|
20621
|
-
if (!Number.isInteger(pid) || pid <= 0)
|
|
20622
|
-
return;
|
|
20623
|
-
const since = (BigInt(Math.max(0, Math.floor(startedAt) - START_SLACK_MS)) + FILETIME_EPOCH_MS) * 10000n;
|
|
20624
|
-
const script = `$t=[DateTime]::FromFileTimeUtc(${since}); ` + `Get-CimInstance -ClassName Win32_Process -Filter 'ParentProcessId=${pid}' | ` + `Where-Object { $_.CreationDate.ToUniversalTime() -ge $t } | ForEach-Object { $_.ProcessId }`;
|
|
20625
|
-
let stdout = "";
|
|
20626
|
-
try {
|
|
20627
|
-
stdout = (await exec(powershell, ["-NoProfile", "-NonInteractive", "-Command", script])).stdout;
|
|
20628
|
-
} catch {
|
|
20629
|
-
return;
|
|
20630
|
-
}
|
|
20631
|
-
for (const line of stdout.split(/\r?\n/)) {
|
|
20632
|
-
const child = line.trim();
|
|
20633
|
-
if (/^[1-9]\d*$/.test(child))
|
|
20634
|
-
await killTree(Number(child));
|
|
20635
|
-
}
|
|
20636
|
-
}
|
|
20637
|
-
};
|
|
20638
|
-
}
|
|
20639
|
-
const kill = deps.kill ?? ((pid, signal) => void process.kill(pid, signal));
|
|
20640
|
-
const group = (pid) => {
|
|
20641
|
-
try {
|
|
20642
|
-
kill(-pid, "SIGKILL");
|
|
20643
|
-
return true;
|
|
20644
|
-
} catch {
|
|
20645
|
-
return false;
|
|
20646
|
-
}
|
|
20647
|
-
};
|
|
20648
|
-
return {
|
|
20649
|
-
detached: true,
|
|
20650
|
-
async killTree(pid) {
|
|
20651
|
-
if (!Number.isInteger(pid) || pid <= 0)
|
|
20652
|
-
return;
|
|
20653
|
-
if (group(pid))
|
|
20654
|
-
return;
|
|
20655
|
-
try {
|
|
20656
|
-
kill(pid, "SIGKILL");
|
|
20657
|
-
} catch {}
|
|
20658
|
-
},
|
|
20659
|
-
async reap(pid) {
|
|
20660
|
-
if (Number.isInteger(pid) && pid > 0)
|
|
20661
|
-
group(pid);
|
|
20662
|
-
}
|
|
20663
|
-
};
|
|
20664
|
-
}
|
|
20665
|
-
var DRAIN_GRACE_MS = 1000;
|
|
20666
|
-
function runBounded(request, options = {}) {
|
|
20667
|
-
const tree = options.tree ?? processTree();
|
|
20668
|
-
const spawn = options.spawn ?? nodeSpawn;
|
|
20669
|
-
const now = options.now ?? Date.now;
|
|
20670
|
-
const env = Object.fromEntries(Object.entries(request.env).filter((e) => typeof e[1] === "string"));
|
|
20671
|
-
return new Promise((resolve) => {
|
|
20672
|
-
let timedOut = false;
|
|
20673
|
-
let aborted = false;
|
|
20674
|
-
let overflowed = false;
|
|
20675
|
-
let ended = false;
|
|
20676
|
-
const out = [];
|
|
20677
|
-
const err = [];
|
|
20678
|
-
const size = { out: 0, err: 0 };
|
|
20679
|
-
const result = (exitCode, spawnError) => ({
|
|
20680
|
-
exitCode,
|
|
20681
|
-
stdout: Buffer.concat(out).toString("utf8"),
|
|
20682
|
-
stderr: Buffer.concat(err).toString("utf8"),
|
|
20683
|
-
timedOut,
|
|
20684
|
-
aborted,
|
|
20685
|
-
overflowed,
|
|
20686
|
-
...spawnError ? { spawnError } : {}
|
|
20687
|
-
});
|
|
20688
|
-
if (request.signal?.aborted) {
|
|
20689
|
-
aborted = true;
|
|
20690
|
-
return resolve(result(null));
|
|
20691
|
-
}
|
|
20692
|
-
const startedAt = now();
|
|
20693
|
-
let child;
|
|
20694
|
-
try {
|
|
20695
|
-
child = spawn(request.file, [...request.args], {
|
|
20696
|
-
shell: false,
|
|
20697
|
-
env,
|
|
20698
|
-
stdio: ["ignore", "pipe", request.stderr ? "pipe" : "ignore"],
|
|
20699
|
-
windowsHide: true,
|
|
20700
|
-
detached: tree.detached
|
|
20701
|
-
});
|
|
20702
|
-
} catch (error) {
|
|
20703
|
-
return resolve(result(null, error));
|
|
20704
|
-
}
|
|
20705
|
-
let settled = false;
|
|
20706
|
-
let exited = false;
|
|
20707
|
-
const pid = child.pid;
|
|
20708
|
-
const end = () => {
|
|
20709
|
-
if (ended || pid === undefined)
|
|
20710
|
-
return;
|
|
20711
|
-
ended = true;
|
|
20712
|
-
tree.killTree(pid);
|
|
20713
|
-
};
|
|
20714
|
-
const timer = setTimeout(() => {
|
|
20715
|
-
timedOut = true;
|
|
20716
|
-
end();
|
|
20717
|
-
}, request.timeoutMs);
|
|
20718
|
-
const onAbort = () => {
|
|
20719
|
-
aborted = true;
|
|
20720
|
-
end();
|
|
20721
|
-
};
|
|
20722
|
-
request.signal?.addEventListener("abort", onAbort, { once: true });
|
|
20723
|
-
const finish = (r) => {
|
|
20724
|
-
if (settled)
|
|
20725
|
-
return;
|
|
20726
|
-
settled = true;
|
|
20727
|
-
clearTimeout(timer);
|
|
20728
|
-
request.signal?.removeEventListener("abort", onAbort);
|
|
20729
|
-
resolve(r);
|
|
20730
|
-
};
|
|
20731
|
-
const collect = (into, key) => (chunk) => {
|
|
20732
|
-
const room = request.maxOutputBytes - size[key];
|
|
20733
|
-
if (chunk.length > room) {
|
|
20734
|
-
overflowed = true;
|
|
20735
|
-
if (room > 0)
|
|
20736
|
-
into.push(chunk.subarray(0, room));
|
|
20737
|
-
size[key] = request.maxOutputBytes;
|
|
20738
|
-
if ((request.overflow ?? "kill") === "kill")
|
|
20739
|
-
end();
|
|
20740
|
-
return;
|
|
20741
|
-
}
|
|
20742
|
-
into.push(chunk);
|
|
20743
|
-
size[key] += chunk.length;
|
|
20744
|
-
};
|
|
20745
|
-
child.stdout?.on("data", collect(out, "out"));
|
|
20746
|
-
child.stderr?.on("data", collect(err, "err"));
|
|
20747
|
-
const streams = [child.stdout, child.stderr].filter((s) => Boolean(s));
|
|
20748
|
-
const drained = () => new Promise((done) => {
|
|
20749
|
-
let open = streams.filter((s) => !s.readableEnded && !s.destroyed).length;
|
|
20750
|
-
if (open === 0)
|
|
20751
|
-
return done();
|
|
20752
|
-
const grace = setTimeout(() => {
|
|
20753
|
-
for (const s of streams)
|
|
20754
|
-
s.destroy();
|
|
20755
|
-
done();
|
|
20756
|
-
}, DRAIN_GRACE_MS);
|
|
20757
|
-
for (const s of streams) {
|
|
20758
|
-
if (s.readableEnded || s.destroyed)
|
|
20759
|
-
continue;
|
|
20760
|
-
const one = () => {
|
|
20761
|
-
if (--open === 0) {
|
|
20762
|
-
clearTimeout(grace);
|
|
20763
|
-
done();
|
|
20764
|
-
}
|
|
20765
|
-
};
|
|
20766
|
-
s.once("end", one);
|
|
20767
|
-
s.once("close", () => s.readableEnded ? undefined : one());
|
|
20768
|
-
}
|
|
20769
|
-
});
|
|
20770
|
-
child.on("error", (error) => {
|
|
20771
|
-
if (!exited)
|
|
20772
|
-
finish(result(null, error));
|
|
20773
|
-
});
|
|
20774
|
-
child.on("exit", (code) => {
|
|
20775
|
-
if (exited)
|
|
20776
|
-
return;
|
|
20777
|
-
exited = true;
|
|
20778
|
-
clearTimeout(timer);
|
|
20779
|
-
const exitCode = typeof code === "number" && !ended ? code : null;
|
|
20780
|
-
(async () => {
|
|
20781
|
-
if (pid !== undefined)
|
|
20782
|
-
await tree.reap(pid, startedAt);
|
|
20783
|
-
await drained();
|
|
20784
|
-
finish(result(exitCode));
|
|
20785
|
-
})();
|
|
20786
|
-
});
|
|
20787
|
-
});
|
|
20788
|
-
}
|
|
20789
|
-
|
|
20790
21258
|
// packages/cli/src/bootstrap/adapters/claude/ports.ts
|
|
20791
21259
|
class DownloadTooLarge extends Error {
|
|
20792
21260
|
constructor(url, maxBytes) {
|
|
@@ -21512,7 +21980,7 @@ var OIDC_ISSUER_V2 = "1.3.6.1.4.1.57264.1.8";
|
|
|
21512
21980
|
var OIDC_ISSUER_V1 = "1.3.6.1.4.1.57264.1.1";
|
|
21513
21981
|
var isObj5 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
21514
21982
|
var b64 = (s) => typeof s === "string" && /^[A-Za-z0-9+/]*={0,2}$/.test(s) ? Buffer.from(s, "base64") : undefined;
|
|
21515
|
-
var
|
|
21983
|
+
var within4 = (t, v) => t >= v.start && (v.end === undefined || t <= v.end);
|
|
21516
21984
|
function tlv(buf, at) {
|
|
21517
21985
|
const tag = buf[at];
|
|
21518
21986
|
let len = buf[at + 1];
|
|
@@ -21647,7 +22115,7 @@ function verifySigstoreBundle(bundle, artifact, options) {
|
|
|
21647
22115
|
const t = b.payload.integratedTime * 1000;
|
|
21648
22116
|
if (t < Date.parse(leaf.validFrom) || t > Date.parse(leaf.validTo))
|
|
21649
22117
|
return failed2("the entry was logged outside the certificate's validity");
|
|
21650
|
-
if (!
|
|
22118
|
+
if (!within4(t, authority.validFor))
|
|
21651
22119
|
return failed2("the entry was logged outside the CA's validity");
|
|
21652
22120
|
const sans = (leaf.subjectAltName ?? "").split(/,\s*/);
|
|
21653
22121
|
if (!sans.includes(`URI:${identity.san}`))
|
|
@@ -21667,7 +22135,7 @@ function verifySigstoreBundle(bundle, artifact, options) {
|
|
|
21667
22135
|
const log = trust.logs.find((l) => l.logID === b.payload.logID.toLowerCase());
|
|
21668
22136
|
if (!log)
|
|
21669
22137
|
return unavailable("the entry is in a transparency log this hubctl does not trust");
|
|
21670
|
-
if (!
|
|
22138
|
+
if (!within4(t, log.validFor))
|
|
21671
22139
|
return failed2("the entry was logged outside the log key's validity");
|
|
21672
22140
|
if (!verifySET(log, b))
|
|
21673
22141
|
return failed2("the transparency log's signed entry timestamp does not verify");
|
|
@@ -21804,13 +22272,50 @@ var labelOfTarget = (word) => {
|
|
|
21804
22272
|
const at = word?.lastIndexOf("/") ?? -1;
|
|
21805
22273
|
return at > 0 && DOMAIN.test(word.slice(0, at)) ? word.slice(at + 1) : undefined;
|
|
21806
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"]);
|
|
21807
22307
|
function checkManagerCall(call) {
|
|
21808
|
-
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))
|
|
21809
22312
|
return false;
|
|
21810
22313
|
const w = call.words;
|
|
21811
22314
|
if (!w.every((x) => typeof x === "string" && x.length > 0 && x.length <= 4096 && !CONTROL5.test(x)))
|
|
21812
22315
|
return false;
|
|
21813
22316
|
const words = w;
|
|
22317
|
+
if (windows)
|
|
22318
|
+
return checkWindowsCall(call.program, words);
|
|
21814
22319
|
if (call.program === "systemctl") {
|
|
21815
22320
|
if (words[0] !== "--user")
|
|
21816
22321
|
return false;
|
|
@@ -21844,6 +22349,24 @@ function unitPathResolver(roots, context) {
|
|
|
21844
22349
|
return where.ok ? where.value : undefined;
|
|
21845
22350
|
};
|
|
21846
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
|
+
}
|
|
21847
22370
|
var ok4 = txOk(undefined);
|
|
21848
22371
|
function showFields(output) {
|
|
21849
22372
|
const fields = {};
|
|
@@ -22005,20 +22528,84 @@ function createServiceControl(options) {
|
|
|
22005
22528
|
return await call("launchctl", "print", `${domain}/${label}`) === 0 ? txFail("internal", `${label} is still loaded after launchctl bootout`) : ok4;
|
|
22006
22529
|
}
|
|
22007
22530
|
};
|
|
22008
|
-
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) });
|
|
22009
22595
|
return {
|
|
22010
|
-
state: (service, manager) =>
|
|
22011
|
-
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) }),
|
|
22012
22598
|
deactivate,
|
|
22013
|
-
restore: (service, manager, prior) => prior.loaded ?
|
|
22014
|
-
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) })
|
|
22015
22601
|
};
|
|
22016
22602
|
}
|
|
22017
22603
|
var MAX_OUTPUT2 = 64 * 1024;
|
|
22018
|
-
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";
|
|
22019
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"];
|
|
22020
22607
|
var defaultSpawn = (file, words, options) => new Promise((resolve) => {
|
|
22021
|
-
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" });
|
|
22022
22609
|
let output = "";
|
|
22023
22610
|
child.stdout?.setEncoding("utf8");
|
|
22024
22611
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -22030,13 +22617,16 @@ var defaultSpawn = (file, words, options) => new Promise((resolve) => {
|
|
|
22030
22617
|
child.on("close", (code, signal) => done(code ?? (signal ? 128 : 1)));
|
|
22031
22618
|
});
|
|
22032
22619
|
function spawnManagerRunner(options) {
|
|
22033
|
-
|
|
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"]) {
|
|
22034
22623
|
const file = options.programs[program];
|
|
22035
|
-
|
|
22624
|
+
const base = file === undefined ? "" : path34.basename(file);
|
|
22625
|
+
if (!isAbsolutePath(file) || base !== program && base.toLowerCase() !== `${program}.exe`)
|
|
22036
22626
|
throw new Error(`${program} must be given as an absolute path to ${program}`);
|
|
22037
22627
|
}
|
|
22038
22628
|
const env = {};
|
|
22039
|
-
for (const key of BUS_ENV)
|
|
22629
|
+
for (const key of windows ? WINDOWS_ENV : BUS_ENV)
|
|
22040
22630
|
if (options.env?.[key])
|
|
22041
22631
|
env[key] = options.env[key];
|
|
22042
22632
|
const spawn = options.spawn ?? defaultSpawn;
|
|
@@ -22045,7 +22635,10 @@ function spawnManagerRunner(options) {
|
|
|
22045
22635
|
if (!checkManagerCall(call))
|
|
22046
22636
|
throw new Error(`refused a service manager call outside the fixed set (${String(call.program)})`);
|
|
22047
22637
|
const capture = capturesOutput(call);
|
|
22048
|
-
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 } : {} });
|
|
22049
22642
|
if (typeof r === "number")
|
|
22050
22643
|
return { exitCode: r };
|
|
22051
22644
|
return capture ? { exitCode: r.exitCode, output: r.output.slice(0, MAX_OUTPUT2) } : { exitCode: r.exitCode };
|
|
@@ -22224,8 +22817,12 @@ function launchSessionExecutor(probe) {
|
|
|
22224
22817
|
var DEFAULT_SLOTS2 = 4;
|
|
22225
22818
|
var BOOTSTRAP_SLOTS_MAX = 64;
|
|
22226
22819
|
var RESERVED_LABELS = ["os", "arch"];
|
|
22227
|
-
var
|
|
22228
|
-
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("/")) || "/");
|
|
22229
22826
|
function otherViews(roots, context) {
|
|
22230
22827
|
return roots.flatMap((r) => r.containerPath !== undefined && r.containerPath !== r.path ? [context === "container" ? r.path : r.containerPath] : []);
|
|
22231
22828
|
}
|
|
@@ -22348,6 +22945,7 @@ function serviceContextOf(input) {
|
|
|
22348
22945
|
// packages/cli/src/bootstrap/adapters/node-services/plan.ts
|
|
22349
22946
|
var NODE_CONFIG_REF = { root: "config", rel: "hub/node.json" };
|
|
22350
22947
|
var CONFIG_DIR = { root: "config", rel: "hub" };
|
|
22948
|
+
var UNIT_KIND = { launchd: "launchd agent", "systemd-user": "systemd --user unit", "task-scheduler": "logon task" };
|
|
22351
22949
|
var LABEL3 = { node: "hub node", "claude-code": "Claude Code spoke", "codex-spoke": "Codex spoke" };
|
|
22352
22950
|
var SERVICE_HARNESS = Object.fromEntries(Object.entries(HARNESS_SERVICE).map(([h, s]) => [s, h]));
|
|
22353
22951
|
var BIN_SETTING = { "claude-code": "HUB_CLAUDE_BIN", codex: "HUB_CODEX_BIN" };
|
|
@@ -22424,9 +23022,19 @@ function planNodeServices(input, options) {
|
|
|
22424
23022
|
}
|
|
22425
23023
|
}
|
|
22426
23024
|
const units = [];
|
|
23025
|
+
const uncarried = new Set;
|
|
22427
23026
|
for (const service of profile.services) {
|
|
22428
|
-
|
|
22429
|
-
|
|
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 } : {} });
|
|
22430
23038
|
if (!unit.ok) {
|
|
22431
23039
|
issues.push(...prefixed(`/services/${service}`, unit));
|
|
22432
23040
|
continue;
|
|
@@ -22436,7 +23044,7 @@ function planNodeServices(input, options) {
|
|
|
22436
23044
|
id: `install-${service}-service`,
|
|
22437
23045
|
kind: "install-service",
|
|
22438
23046
|
escalation: "user",
|
|
22439
|
-
summary: `Write the ${LABEL3[service]} ${ctx.manager
|
|
23047
|
+
summary: `Write the ${LABEL3[service]} ${UNIT_KIND[ctx.manager]} (${unit.value.name})`,
|
|
22440
23048
|
service,
|
|
22441
23049
|
manager: ctx.manager,
|
|
22442
23050
|
path: unit.value.path,
|
|
@@ -22468,7 +23076,7 @@ function planNodeServices(input, options) {
|
|
|
22468
23076
|
}
|
|
22469
23077
|
if (issues.length > 0)
|
|
22470
23078
|
return failed3(issues);
|
|
22471
|
-
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()) } };
|
|
22472
23080
|
}
|
|
22473
23081
|
function checkLaunch(input, options, ctx, settings) {
|
|
22474
23082
|
const launch = options.launch;
|
|
@@ -22486,11 +23094,15 @@ function checkLaunch(input, options, ctx, settings) {
|
|
|
22486
23094
|
return [{ path: "/launch/workspace", code: "invalid_path", message: `${where.value} is outside the node's workspace roots` }];
|
|
22487
23095
|
return [];
|
|
22488
23096
|
}
|
|
22489
|
-
function notesFor(options, ctx) {
|
|
23097
|
+
function notesFor(options, ctx, uncarried) {
|
|
22490
23098
|
const notes = [];
|
|
22491
23099
|
if (!options.activate)
|
|
22492
23100
|
notes.push("The services are installed but not loaded: pass --confirm-activation to load them (hubctl never loads a service without it).");
|
|
22493
|
-
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")
|
|
22494
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.");
|
|
22495
23107
|
else
|
|
22496
23108
|
notes.push("launchd user agents run while you are logged in.");
|
|
@@ -22796,7 +23408,6 @@ function createOpencodeHarness(options) {
|
|
|
22796
23408
|
}
|
|
22797
23409
|
|
|
22798
23410
|
// packages/cli/src/bootstrap/profiles/upgrade.ts
|
|
22799
|
-
var UNIT_ROOT2 = { launchd: "launch-agents", "systemd-user": "systemd-user" };
|
|
22800
23411
|
function removeServiceActions(service, facts) {
|
|
22801
23412
|
const actions = [
|
|
22802
23413
|
{ id: `deactivate-${service}`, kind: "deactivate-service", escalation: "user", summary: `Unload the hub ${service} service (profile downgrade)`, service, manager: facts.manager }
|
|
@@ -22811,7 +23422,7 @@ function removeServiceActions(service, facts) {
|
|
|
22811
23422
|
summary: `Remove the hub ${service} ${facts.manager} unit (profile downgrade)`,
|
|
22812
23423
|
service,
|
|
22813
23424
|
manager: facts.manager,
|
|
22814
|
-
path: { root:
|
|
23425
|
+
path: { root: SERVICE_ROOT[facts.manager], rel: asset.installAs },
|
|
22815
23426
|
sha256
|
|
22816
23427
|
});
|
|
22817
23428
|
return actions;
|
|
@@ -24014,6 +24625,10 @@ function codexSpokeStatus(options) {
|
|
|
24014
24625
|
}
|
|
24015
24626
|
|
|
24016
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
|
+
}
|
|
24017
24632
|
function readinessGate(consent, harness) {
|
|
24018
24633
|
return { requireReady: consent.requireReady === true || (consent.strict ?? []).includes(harness), ...consent.onReadiness ? { onReadiness: consent.onReadiness } : {} };
|
|
24019
24634
|
}
|
|
@@ -24182,7 +24797,8 @@ function createSetupRuntime(deps) {
|
|
|
24182
24797
|
...choices.node ? { node: choices.node } : {},
|
|
24183
24798
|
...choices.activate ? { activate: true } : {},
|
|
24184
24799
|
environment: deps.env,
|
|
24185
|
-
...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() } : {}
|
|
24186
24802
|
});
|
|
24187
24803
|
if (!planned.ok)
|
|
24188
24804
|
return planned;
|
|
@@ -24203,15 +24819,28 @@ function createSetupRuntime(deps) {
|
|
|
24203
24819
|
];
|
|
24204
24820
|
return { sources: { adapters: registry, contributors, ...choices.enrollment ? { enrollment: choices.enrollment } : {} }, node: () => nodePlan, history: records };
|
|
24205
24821
|
}
|
|
24822
|
+
function taskIdentity() {
|
|
24823
|
+
return { name: taskNameFor("node", deps.env.USERNAME ?? "", deps.env), userSid: (ports.userSid ?? (() => defaultWinAcl().sid()))() };
|
|
24824
|
+
}
|
|
24206
24825
|
function control(snap) {
|
|
24207
24826
|
if (snap.managers.service === "none")
|
|
24208
24827
|
return;
|
|
24828
|
+
const tasks = snap.managers.service === "task-scheduler";
|
|
24209
24829
|
let run = ports.serviceRunner;
|
|
24210
24830
|
if (!run) {
|
|
24211
|
-
|
|
24212
|
-
|
|
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
|
+
}
|
|
24213
24837
|
}
|
|
24214
|
-
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
|
+
});
|
|
24215
24844
|
}
|
|
24216
24845
|
function hubctlSource() {
|
|
24217
24846
|
const manifest = deps.manifest;
|
|
@@ -24506,14 +25135,14 @@ class BootstrapStateStore {
|
|
|
24506
25135
|
}
|
|
24507
25136
|
|
|
24508
25137
|
// packages/cli/src/setup/self-aside.ts
|
|
24509
|
-
import { spawn as
|
|
24510
|
-
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";
|
|
24511
25140
|
var nodeSelfAsideIo = (env = process.env) => ({
|
|
24512
25141
|
platform: process.platform,
|
|
24513
25142
|
systemRoot: env.SystemRoot ?? env.SYSTEMROOT ?? "C:\\Windows",
|
|
24514
25143
|
stat: (path) => {
|
|
24515
25144
|
try {
|
|
24516
|
-
return { ino:
|
|
25145
|
+
return { ino: statSync7(path, { bigint: true }).ino };
|
|
24517
25146
|
} catch {
|
|
24518
25147
|
return;
|
|
24519
25148
|
}
|
|
@@ -24521,7 +25150,7 @@ var nodeSelfAsideIo = (env = process.env) => ({
|
|
|
24521
25150
|
readdir: (path) => readdirSync4(path),
|
|
24522
25151
|
unlink: (path) => unlinkSync3(path),
|
|
24523
25152
|
spawnDetached: (file, line, cwd) => {
|
|
24524
|
-
const child =
|
|
25153
|
+
const child = spawn8(file, [line], { cwd, detached: true, stdio: "ignore", windowsHide: true, windowsVerbatimArguments: true });
|
|
24525
25154
|
child.on("error", () => {});
|
|
24526
25155
|
child.unref();
|
|
24527
25156
|
}
|
|
@@ -24774,20 +25403,38 @@ function refFor(abs, snapshot) {
|
|
|
24774
25403
|
for (const id of ["home"]) {
|
|
24775
25404
|
const root = snapshot.roots.find((r) => r.id === id);
|
|
24776
25405
|
const base = root ? rootPathIn(root, ctx) : undefined;
|
|
24777
|
-
if (base && abs
|
|
24778
|
-
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)) };
|
|
24779
25408
|
}
|
|
24780
25409
|
return;
|
|
24781
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
|
+
}
|
|
24782
25423
|
function choicesOf(io, flags, profile, snapshot) {
|
|
24783
25424
|
const enrollment = str(flags, "enrollment");
|
|
24784
25425
|
if (enrollment !== undefined && !ENROLLMENT_MODES.includes(enrollment))
|
|
24785
25426
|
throw new Refusal("usage", `--enrollment is ${ENROLLMENT_MODES.join(" or ")}`);
|
|
24786
|
-
|
|
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
|
+
});
|
|
24787
25434
|
if (!roots?.length && profile.services.includes("node")) {
|
|
24788
25435
|
const existing = readNodeConfig(nodeConfigPath(io.env, io.home)).config.workspaceRoots;
|
|
24789
25436
|
if (existing.length > 0)
|
|
24790
|
-
roots = [...existing];
|
|
25437
|
+
roots = where.platform === "win32" ? existing.map((r) => workspaceRootOf(r, where) ?? r) : [...existing];
|
|
24791
25438
|
}
|
|
24792
25439
|
const launchHarness = str(flags, "verify-launch");
|
|
24793
25440
|
let launch;
|
|
@@ -24907,7 +25554,7 @@ async function preview3(io, flags, json) {
|
|
|
24907
25554
|
else {
|
|
24908
25555
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
24909
25556
|
writeFileSync8(tmp, text, { mode: 384 });
|
|
24910
|
-
|
|
25557
|
+
renameSync9(tmp, file);
|
|
24911
25558
|
}
|
|
24912
25559
|
}
|
|
24913
25560
|
const warnings = previewWarnings(snapshot, r.value.plan, bundle.node()?.notes ?? [], io.env);
|