@huynle/hub 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +6 -4
  3. package/package.json +1 -1
  4. package/packages/claude-code/package.json +1 -1
  5. package/packages/claude-code/src/wrapper.ts +2 -2
  6. package/packages/cli/dist/hub-cli.js +1126 -479
  7. package/packages/cli/package.json +1 -1
  8. package/packages/cli/src/bootstrap/PHASE3-CONTRACT.md +112 -0
  9. package/packages/cli/src/bootstrap/adapters/node-services/control.ts +164 -21
  10. package/packages/cli/src/bootstrap/adapters/node-services/plan.ts +24 -7
  11. package/packages/cli/src/bootstrap/adapters/node-services/render.ts +116 -9
  12. package/packages/cli/src/bootstrap/adapters/node-services/validate.ts +8 -2
  13. package/packages/cli/src/bootstrap/contracts/actions.ts +8 -6
  14. package/packages/cli/src/bootstrap/contracts/discovery.ts +4 -3
  15. package/packages/cli/src/bootstrap/contracts/exec.ts +4 -2
  16. package/packages/cli/src/bootstrap/contracts/paths.ts +2 -1
  17. package/packages/cli/src/bootstrap/contracts/platform.ts +5 -5
  18. package/packages/cli/src/bootstrap/discovery/discover.ts +7 -2
  19. package/packages/cli/src/bootstrap/discovery/host.ts +2 -0
  20. package/packages/cli/src/bootstrap/preview/plan.ts +5 -1
  21. package/packages/cli/src/bootstrap/profiles/desired.ts +16 -1
  22. package/packages/cli/src/bootstrap/profiles/history.ts +2 -1
  23. package/packages/cli/src/bootstrap/profiles/upgrade.ts +2 -2
  24. package/packages/cli/src/bootstrap/release/assets.ts +4 -1
  25. package/packages/cli/src/node.ts +94 -12
  26. package/packages/cli/src/setup/commands.ts +27 -7
  27. package/packages/cli/src/setup/runtime.ts +31 -4
  28. package/packages/codex/package.json +1 -1
  29. package/packages/codex/src/wrapper.ts +2 -2
  30. package/packages/node/package.json +1 -1
  31. package/packages/node/src/config.ts +49 -17
  32. package/packages/node/src/git.ts +16 -2
  33. package/packages/node/src/harnesses.ts +54 -9
  34. package/packages/node/src/host.ts +61 -15
  35. package/packages/node/src/opencode-service.ts +111 -0
  36. package/packages/node/src/opencode-warm.ts +11 -5
  37. package/packages/node/src/run.ts +50 -5
  38. package/packages/preset/package.json +1 -1
  39. package/packages/protocol/package.json +1 -1
  40. package/packages/protocol/src/index.ts +3 -1
  41. package/packages/protocol/src/launch.ts +39 -4
  42. package/packages/runtime-openshell/package.json +1 -1
  43. package/packages/runtime-sdk/package.json +1 -1
  44. package/packages/sdk/package.json +1 -1
  45. package/packages/spoke-sdk/package.json +1 -1
  46. package/src/central/agent/launch-spec.ts +4 -2
  47. package/src/central/install/CONTRACT.md +4 -4
  48. package/src/central/install/install.ps1 +37 -5
  49. package/src/central/install/render.ts +13 -8
  50. package/web/dist/assets/{ArtifactPage-4GoBqSpU.js → ArtifactPage-BKISN-Qj.js} +1 -1
  51. package/web/dist/assets/{ArtifactPane-Dx_0iWwH.js → ArtifactPane-CKOiAUNH.js} +1 -1
  52. package/web/dist/assets/{ArtifactWorkbench-CuHgARMk.js → ArtifactWorkbench-d7R1o-rs.js} +1 -1
  53. package/web/dist/assets/{Diagram-CZM_R7MG.js → Diagram-azw2B_QH.js} +1 -1
  54. package/web/dist/assets/{Library-TwvrwqSk.js → Library-CcjZXsfF.js} +2 -2
  55. package/web/dist/assets/{ProfilesDestination-rKNDpLjR.js → ProfilesDestination-DdzbzmeK.js} +1 -1
  56. package/web/dist/assets/{TeamsPage-DtJylLrt.js → TeamsPage-CDyjrK-I.js} +1 -1
  57. package/web/dist/assets/{Watchtower-Bmgdyfjb.js → Watchtower-DYwoUWME.js} +2 -2
  58. package/web/dist/assets/{index-D-xG9nrc.js → index-LNMZCqgE.js} +15 -15
  59. package/web/dist/assets/{live-7q72Lyj2.js → live-ycZ_aQZO.js} +1 -1
  60. package/web/dist/assets/{renderer2d-hfZxM6Xl.js → renderer2d-BIwbgRoX.js} +1 -1
  61. package/web/dist/assets/{renderer3d-Km9vipTM.js → renderer3d-C1a7eYGQ.js} +1 -1
  62. package/web/dist/assets/{safe-area-BAbgBiFk.js → safe-area-DHhPy61Q.js} +1 -1
  63. 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.0",
10
+ version: "0.9.1",
11
11
  private: true,
12
12
  description: "The hub CLI (`hub` / `hubctl`) for agents and scripts (internal; bundled into @huynle/hub, not published on its own).",
13
13
  license: "MIT",
@@ -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: "none", bunTarget: "bun-windows-x64-baseline" }
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, ["launchd", "systemd-user"]);
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, ["launchd", "systemd-user"]);
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, ["launchd", "systemd-user"]);
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, ["launchd", "systemd-user"]);
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 uid = options.uid ?? (typeof process.getuid === "function" ? process.getuid() : undefined);
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 ((stat.mode & 18) !== 0)
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 normalizeNodeConfig(raw) {
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 root of Array.isArray(r.workspaceRoots) ? r.workspaceRoots : []) {
5610
- if (!isAbsolutePath2(root) || root === "/") {
5611
- problems.push(`workspaceRoots: ${show2(root)} is not an absolute path (without . or .. segments, and not /)`);
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 dir of raw) {
5659
- if (!isAbsolutePath2(dir) || dir === "/") {
5660
- problems.push(`${field}: ${show2(dir)} is not an absolute path (without . or .. segments, and not /)`);
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
- if (!isAbsolutePath2(entry.passwordFile) || entry.passwordFile === "/")
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: entry.passwordFile, ...dirs ? { directories: dirs } : {} });
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.0",
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.1",
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: true,
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
- function resolvePath(p) {
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
- return path11.join(realpathSync2(current), ...rest.reverse());
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
- let link = false;
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 = path11.dirname(current);
9584
+ const parent = ops.path.dirname(current);
9301
9585
  if (parent === current)
9302
9586
  throw new RuntimeError(`cannot resolve ${p}`);
9303
- rest.push(path11.basename(current));
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 resolveUnderRoots(p, roots) {
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 = realpathSync2(root);
9610
+ realRoot = realOf(root, ops);
9318
9611
  } catch {
9319
9612
  continue;
9320
9613
  }
9321
- if (real === realRoot || real.startsWith(realRoot.endsWith(path11.sep) ? realRoot : realRoot + path11.sep))
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
- accessSync(file, constants2.X_OK);
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
- function findExecutable(name, env) {
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
- if (findExecutable(claudeBinary(env, home), env))
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 current = path12.join(data, "hub", "harnesses", "codex", "current", "bin", "codex");
9842
- return executable(current) ? current : "codex";
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.0",
10685
+ version: "0.9.1",
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
- const { config, problems } = readNodeConfig(file);
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
- return EXIT_CONFIG;
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
- startOpenCodeWarmers(config, options.openCodeService ?? (() => readServiceRegistration(openCodeServiceFile(env, home))), log, options.signal);
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 = Object.freeze({ launchd: "launch-agents", "systemd-user": "systemd-user" });
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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
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 !== "/" && (input.manager !== "systemd-user" || SYSTEMD_HOME.test(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 = input.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);
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[input.manager], "");
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, input.manager),
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 warnings = [...given, ...service.skipped.map((key) => `${key} is not an absolute path, so the service does not carry it`)];
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
- return runNode({ env: io.env, home: io.home, signal: io.signal ?? new AbortController().signal, log: (line) => io.stderr(`[hub node] ${line}
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 spawn3 } from "node:child_process";
12749
- import { realpathSync as realpathSync4, statSync as statSync4 } from "node:fs";
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 sleep5 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
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 (!statSync4(real).isDirectory())
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 = spawn3(this.#options.bin, launchArgs(input), { cwd: input.directory, env, stdio: ["pipe", "pipe", "pipe"] });
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, sleep5(waitMs).then(() => {
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), sleep5(STOP_GRACE_MS).then(() => false)]))
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), sleep5(KILL_GRACE_MS).then(() => false)]))
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 spawn4 } from "node:child_process";
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.0",
14643
+ version: "0.9.1",
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 renameSync5, rmSync as rmSync9, writeFileSync as writeFileSync6 } from "node:fs";
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
- renameSync5(tmp, dir);
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 = spawn4(io.program.execPath, [io.program.entry, "claude-code", "daemon"], { detached: true, stdio: ["ignore", log, log], env: { ...io.env, BUN_BE_BUN: "1" } });
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 = spawn4(bin, args, { stdio: "inherit", env });
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.0",
14932
+ version: "0.9.1",
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
- execFile(bin, args, { env, timeout: timeoutMs, encoding: "utf8", maxBuffer: 4 * 1024 * 1024 }, (error, stdout, stderr) => {
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.0",
15457
+ version: "0.9.1",
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 renameSync6, rmSync as rmSync10, writeFileSync as writeFileSync7 } from "node:fs";
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
- renameSync6(temp, versionDir);
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 statSync5 } from "node:fs";
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
- statSync5(path29.join(dir, ".git"));
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 = statSync5(req.directory).isDirectory();
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 spawn5 } from "node:child_process";
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 = spawn5(options.command, [...options.args], { cwd: options.cwd, env, stdio: ["pipe", "pipe", "pipe"] });
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 spawn6 } from "node:child_process";
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 = spawn6(io.program.execPath, [io.program.entry, "codex-spoke", "serve"], { detached: true, stdio: ["ignore", log, log], env: { ...io.env, BUN_BE_BUN: "1" } });
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 = spawn6(bin, args, { stdio: "inherit", env });
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 renameSync8, writeFileSync as writeFileSync8 } from "node:fs";
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) => oneLine2(new RegExp(`<key>${key}</key>\\s*<string>([^<]*)</string>`).exec(plist)?.[1]);
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) => oneLine2(new RegExp(`^${key}=(.*)$`, "m").exec(text)?.[1]?.trim().replace(/^(["'])(.*)\1$/, "$2"));
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: oneLine2(probe.osInfo?.name) ?? "Windows", version: oneLine2(probe.osInfo?.version) ?? "unknown" };
18793
- return { name: oneLine2(probe.platform, 32) ?? "unknown", version: "unknown" };
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 === "launchd" ? "launch-agents" : "systemd-user";
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 renameSync7,
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) => renameSync7(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 within3 = (t, v) => t >= v.start && (v.end === undefined || t <= v.end);
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 (!within3(t, authority.validFor))
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 (!within3(t, log.validFor))
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) || call.words.length > 4)
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 deactivate = (service, manager) => guarded(() => manager === "launchd" ? launchd.deactivate(service) : systemd.restore(service, { loaded: false, enabled: false }));
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) => guarded(() => manager === "launchd" ? launchd.state(service) : systemd.state(service)),
22011
- activate: (service, manager) => guarded(() => manager === "launchd" ? launchd.activate(service) : systemd.activate(service)),
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 ? guarded(async () => manager === "launchd" ? ok4 : systemd.restore(service, prior)) : deactivate(service, manager),
22014
- health: (service, manager) => guarded(() => manager === "launchd" ? launchd.health(service) : systemd.health(service))
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
- for (const program of ["systemctl", "launchctl"]) {
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
- if (!isAbsolutePath(file) || path34.basename(file) !== program)
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 r = await spawn(options.programs[call.program], [...call.words], { shell: false, env, timeoutMs, ...capture ? { capture: true } : {} });
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 inside = (path, dir) => path === dir || path.startsWith(dir === "/" ? "/" : `${dir}/`);
22228
- var parentOf3 = (path) => path.slice(0, path.lastIndexOf("/")) || "/";
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
- const environment = environmentFor(service, servedBy(service, profile.harnesses), ctx, options.environment);
22429
- const unit = renderServiceUnit({ service, manager: ctx.manager, layout: options.layout, home: ctx.home, environment });
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 === "launchd" ? "launchd agent" : "systemd --user unit"} (${unit.value.name})`,
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 === "systemd-user")
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: UNIT_ROOT2[facts.manager], rel: asset.installAs },
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
- const systemctl = which(deps.probe, [...pathDirs(deps.probe), "/usr/bin", "/bin"], "systemctl") ?? "/usr/bin/systemctl";
24212
- run = spawnManagerRunner({ programs: { launchctl: "/bin/launchctl", systemctl }, env: deps.env });
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({ run, uid: ports.uid ?? deps.probe.uid, unitPath: unitPathResolver(snap.roots, executionContextOf(snap)) });
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 spawn7 } from "node:child_process";
24510
- import { readdirSync as readdirSync4, statSync as statSync6, unlinkSync as unlinkSync3 } from "node:fs";
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: statSync6(path, { bigint: true }).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 = spawn7(file, [line], { cwd, detached: true, stdio: "ignore", windowsHide: true, windowsVerbatimArguments: true });
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.startsWith(`${base}/`))
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
- let roots = flags["workspace-root"]?.map((r) => path35.resolve(io.cwd ?? process.cwd(), r));
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
- renameSync8(tmp, file);
25557
+ renameSync9(tmp, file);
24911
25558
  }
24912
25559
  }
24913
25560
  const warnings = previewWarnings(snapshot, r.value.plan, bundle.node()?.notes ?? [], io.env);