@claudexor/cli 3.6.0 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/dist/accounts-projection.d.ts +4 -2
  2. package/dist/accounts-projection.d.ts.map +1 -1
  3. package/dist/accounts-projection.js +49 -2
  4. package/dist/accounts-projection.js.map +1 -1
  5. package/dist/accounts-services.d.ts +7 -1
  6. package/dist/accounts-services.d.ts.map +1 -1
  7. package/dist/accounts-services.js +7 -2
  8. package/dist/accounts-services.js.map +1 -1
  9. package/dist/agy-quota-source.d.ts +1 -0
  10. package/dist/agy-quota-source.d.ts.map +1 -1
  11. package/dist/agy-quota-source.js +61 -108
  12. package/dist/agy-quota-source.js.map +1 -1
  13. package/dist/capabilities.d.ts.map +1 -1
  14. package/dist/capabilities.js +6 -1
  15. package/dist/capabilities.js.map +1 -1
  16. package/dist/claudexord-entry.d.ts +7 -2
  17. package/dist/claudexord-entry.d.ts.map +1 -1
  18. package/dist/claudexord-entry.js +33 -11
  19. package/dist/claudexord-entry.js.map +1 -1
  20. package/dist/cli.js +2 -1
  21. package/dist/cli.js.map +1 -1
  22. package/dist/command-flags.d.ts.map +1 -1
  23. package/dist/command-flags.js +1 -0
  24. package/dist/command-flags.js.map +1 -1
  25. package/dist/control-services.d.ts +28 -14
  26. package/dist/control-services.d.ts.map +1 -1
  27. package/dist/control-services.js +16 -5
  28. package/dist/control-services.js.map +1 -1
  29. package/dist/credential-commands.d.ts +16 -0
  30. package/dist/credential-commands.d.ts.map +1 -1
  31. package/dist/credential-commands.js +47 -13
  32. package/dist/credential-commands.js.map +1 -1
  33. package/dist/credential-profile-mutations.d.ts +7 -0
  34. package/dist/credential-profile-mutations.d.ts.map +1 -1
  35. package/dist/credential-profile-mutations.js +63 -31
  36. package/dist/credential-profile-mutations.js.map +1 -1
  37. package/dist/delegation-belt-descriptor.d.ts +2 -1
  38. package/dist/delegation-belt-descriptor.d.ts.map +1 -1
  39. package/dist/delegation-belt-descriptor.js +2 -1
  40. package/dist/delegation-belt-descriptor.js.map +1 -1
  41. package/dist/harness-command-specs.d.ts.map +1 -1
  42. package/dist/harness-command-specs.js +4 -4
  43. package/dist/harness-command-specs.js.map +1 -1
  44. package/dist/harness-command.d.ts +5 -3
  45. package/dist/harness-command.d.ts.map +1 -1
  46. package/dist/harness-command.js +5 -3
  47. package/dist/harness-command.js.map +1 -1
  48. package/dist/harness-install-lease.d.ts +11 -0
  49. package/dist/harness-install-lease.d.ts.map +1 -0
  50. package/dist/harness-install-lease.js +98 -0
  51. package/dist/harness-install-lease.js.map +1 -0
  52. package/dist/harness-install-proof.d.ts +36 -0
  53. package/dist/harness-install-proof.d.ts.map +1 -0
  54. package/dist/harness-install-proof.js +148 -0
  55. package/dist/harness-install-proof.js.map +1 -0
  56. package/dist/harness-install-recipes.d.ts +80 -0
  57. package/dist/harness-install-recipes.d.ts.map +1 -0
  58. package/dist/harness-install-recipes.js +130 -0
  59. package/dist/harness-install-recipes.js.map +1 -0
  60. package/dist/harness-installer.d.ts +45 -46
  61. package/dist/harness-installer.d.ts.map +1 -1
  62. package/dist/harness-installer.js +248 -156
  63. package/dist/harness-installer.js.map +1 -1
  64. package/dist/native-login.d.ts +7 -0
  65. package/dist/native-login.d.ts.map +1 -1
  66. package/dist/native-login.js +29 -13
  67. package/dist/native-login.js.map +1 -1
  68. package/dist/profile-registration.d.ts +2 -0
  69. package/dist/profile-registration.d.ts.map +1 -1
  70. package/dist/profile-registration.js +48 -5
  71. package/dist/profile-registration.js.map +1 -1
  72. package/dist/quota-services.d.ts +2 -2
  73. package/dist/remote-command.d.ts +0 -2
  74. package/dist/remote-command.d.ts.map +1 -1
  75. package/dist/remote-command.js +3 -74
  76. package/dist/remote-command.js.map +1 -1
  77. package/dist/setup-attach-command.d.ts +30 -0
  78. package/dist/setup-attach-command.d.ts.map +1 -0
  79. package/dist/setup-attach-command.js +118 -0
  80. package/dist/setup-attach-command.js.map +1 -0
  81. package/dist/setup-job-store.d.ts +2 -2
  82. package/dist/setup-job-support.d.ts +23 -13
  83. package/dist/setup-job-support.d.ts.map +1 -1
  84. package/dist/setup-job-support.js +55 -13
  85. package/dist/setup-job-support.js.map +1 -1
  86. package/dist/setup-jobs.d.ts +4 -4
  87. package/dist/setup-jobs.d.ts.map +1 -1
  88. package/dist/setup-jobs.js +50 -47
  89. package/dist/setup-jobs.js.map +1 -1
  90. package/dist/setup-login-capability.d.ts +37 -0
  91. package/dist/setup-login-capability.d.ts.map +1 -0
  92. package/dist/setup-login-capability.js +96 -0
  93. package/dist/setup-login-capability.js.map +1 -0
  94. package/dist/setup-login-io.d.ts +4 -1
  95. package/dist/setup-login-io.d.ts.map +1 -1
  96. package/dist/setup-login-io.js +22 -3
  97. package/dist/setup-login-io.js.map +1 -1
  98. package/dist/setup-login-pty.d.ts +64 -22
  99. package/dist/setup-login-pty.d.ts.map +1 -1
  100. package/dist/setup-login-pty.js +289 -48
  101. package/dist/setup-login-pty.js.map +1 -1
  102. package/dist/setup-login-runner-support.d.ts +12 -0
  103. package/dist/setup-login-runner-support.d.ts.map +1 -0
  104. package/dist/setup-login-runner-support.js +65 -0
  105. package/dist/setup-login-runner-support.js.map +1 -0
  106. package/dist/setup-login-runner.d.ts +2 -0
  107. package/dist/setup-login-runner.d.ts.map +1 -1
  108. package/dist/setup-login-runner.js +119 -116
  109. package/dist/setup-login-runner.js.map +1 -1
  110. package/dist/setup-runner-outcome.d.ts +10 -0
  111. package/dist/setup-runner-outcome.d.ts.map +1 -0
  112. package/dist/setup-runner-outcome.js +49 -0
  113. package/dist/setup-runner-outcome.js.map +1 -0
  114. package/package.json +24 -24
@@ -1,7 +1,9 @@
1
1
  /**
2
- * `claudexor harness install` — the disclosed, pinned remote vendor installer
2
+ * `claudexor harness install` — the disclosed, pinned vendor installer
3
3
  * (issue #89; restored from the PR #82 cut with the security objections
4
- * fixed).
4
+ * fixed). The historical/default `remote` target remains the SSH-host flow;
5
+ * an explicit `local` target installs into the managed toolchain root that
6
+ * local binary resolution and confinement already own.
5
7
  *
6
8
  * Contract:
7
9
  * - npm-distributed harnesses (claude/codex/opencode) install ONE exact
@@ -16,13 +18,26 @@
16
18
  * downloaded first (never piped to a shell), its size and sha256 are
17
19
  * printed, and it runs in the visible PTY the operator is watching — the
18
20
  * same principle as interactive SSH auth.
19
- * - NOTHING executes without disclosure: the exact command and install
21
+ * - NOTHING executes without authorization: the exact command and install
20
22
  * destination print first, and execution needs a TTY confirmation or an
21
23
  * explicit `--yes` (the macOS flow confirms against this module's own
22
- * `--dry-run --json` disclosure before passing `--yes`).
23
- * - failures are typed and loud; a failed download or non-zero installer exit
24
- * never reads as success, and the temp download dir is removed on every
25
- * path.
24
+ * `--dry-run --json` disclosure before passing `--yes`). For a host
25
+ * integration, `--target local --yes` IS that authorization, granted by the
26
+ * owner action that invoked it; there is no second confirmation hidden
27
+ * inside the producer, and the unattended script path never claims the
28
+ * human-observed verification the watched remote path earns.
29
+ * - concurrent installs into one prefix are serialized by a cross-process
30
+ * lease. A lease whose owner died is NOT reclaimed automatically: it fails
31
+ * closed with a typed `install_lock_stale` and the exact cleanup path.
32
+ * - failures are typed and loud; a failed download, a non-zero installer exit,
33
+ * or a failed post-install binary/version proof never reads as success, and
34
+ * the temp download dir is removed on every path.
35
+ * - the post-install PROOF is what the local target has instead of a witness.
36
+ * A watched remote install keeps the historical exit-code contract, because
37
+ * the operator is looking at the terminal; an unattended local install must
38
+ * earn its `ok:true`, so exit zero without a resolvable launcher and a
39
+ * matching `--version` is a typed failure, and `installedBinary` /
40
+ * `installedVersion` are present on every local success.
26
41
  * - `--json` keeps stdout pure: exactly ONE JSON object. In json mode every
27
42
  * human progress line goes to stderr and child processes (npm/curl/the
28
43
  * vendor script) run with their stdout routed onto stderr, so vendor
@@ -31,56 +46,40 @@
31
46
  import { spawnSync } from "node:child_process";
32
47
  import { existsSync, mkdirSync } from "node:fs";
33
48
  import { type ParsedArgs } from "./args.js";
34
- import { INSTALLABLE_HARNESSES } from "./harness-command-specs.js";
35
- export type InstallableHarness = (typeof INSTALLABLE_HARNESSES)[number];
36
- export declare function isInstallableHarness(value: string): value is InstallableHarness;
37
- /** Exact npm pins. Each version ALIASES the harness package's vendor-version
38
- * SSOT (vendor-cli-version.ts there). For claude/codex that is the version
39
- * this release's freshness gates verified; the opencode pin is a
40
- * deterministic install target only — no recorded verification fixture
41
- * vouches for it (its vendor-cli-version.ts discloses this). Cursor and agy
42
- * are absent deliberately: they ship no npm artifact (see SCRIPT_INSTALLERS
43
- * below). */
44
- export type HarnessInstallVerification = "release_verified" | "deterministic_only" | "human_observed";
45
- export declare const CURSOR_INSTALL_URL = "https://cursor.com/install";
46
- /**
47
- * Google's official Antigravity CLI installer, as published on
48
- * antigravity.google/docs/cli/install (`curl -fsSL <this url> | bash`, which
49
- * Claudexor deliberately does NOT do — see the header). Verified end to end on
50
- * 2026-08-16: the script fetched from this URL installed agy 1.1.13 to
51
- * `~/.local/bin/agy`, which is the destination disclosed below. The vendor
52
- * ships one signed Go binary and no npm package, so it takes the same
53
- * human-observed path as cursor.
54
- */
55
- export declare const AGY_INSTALL_URL = "https://antigravity.google/cli/install.sh";
56
- /** The vendor's Windows installer, from the same documentation page. */
57
- export declare const AGY_INSTALL_URL_WINDOWS = "https://antigravity.google/cli/install.ps1";
58
- export interface HarnessInstallerDisclosure {
59
- harness: InstallableHarness;
60
- command: string;
61
- installLocation: string;
62
- /** Exact vendor version the command installs; null exactly for the script
63
- * vendors (cursor, agy), which have no pinnable artifact — disclosed, never
64
- * faked. */
65
- pinnedVersion: string | null;
66
- /** Evidence behind the install target. Package-registry integrity verifies
67
- * downloaded bytes for every npm pin, but only release_verified means the
68
- * exact vendor version was exercised by this release's freshness gates. */
69
- verification: HarnessInstallVerification;
70
- }
71
- export declare function harnessInstallerDisclosure(harness: InstallableHarness): HarnessInstallerDisclosure;
49
+ import { type HarnessInstallTarget, type InstallableHarness } from "./harness-install-recipes.js";
50
+ export { AGY_INSTALL_URL, AGY_INSTALL_URL_WINDOWS, CURSOR_INSTALL_URL, HARNESS_INSTALL_TARGETS, harnessInstallerDisclosure, isHarnessInstallTarget, isInstallableHarness, type HarnessInstallTarget, type InstallableHarness, } from "./harness-install-recipes.js";
72
51
  export interface HarnessInstallRunResult {
73
52
  exitCode: number;
74
53
  /** Set when the installer refused loudly WITHOUT running the vendor
75
54
  * payload (failed/unreadable download); never a silent partial install. */
76
55
  refusal?: string;
56
+ /** Stable machine reason when the payload did not run or did not verify. */
57
+ code?: string;
58
+ /** Evidence for the exact unpinned vendor-script bytes that ran. */
59
+ installerSha256?: string;
60
+ installerByteLength?: number;
61
+ /** Present on every SUCCESS: the absolute launcher the proof executed. */
62
+ installedBinary?: string;
63
+ /** Present on every SUCCESS: the exact npm pin, or the script vendor's own
64
+ * trimmed `--version` line. */
65
+ installedVersion?: string;
77
66
  }
78
67
  export declare function runHarnessInstaller(harness: InstallableHarness, options?: {
79
68
  home?: string;
80
69
  nodePath?: string;
70
+ /** Explicit install layout. Omitted preserves the historical remote target. */
71
+ target?: HarnessInstallTarget;
81
72
  spawn?: typeof spawnSync;
82
73
  mkdir?: typeof mkdirSync;
83
74
  exists?: typeof existsSync;
75
+ /** Production installs serialize cross-process. Tests may disable only this
76
+ * wrapper while preserving the install recipe under test. */
77
+ lock?: boolean;
78
+ lockTimeoutMs?: number;
79
+ platform?: NodeJS.Platform;
80
+ /** Test/integration source before the clean allowlist and target-aware PATH
81
+ * normalization are applied. Provider credentials are still scrubbed. */
82
+ sourceEnv?: NodeJS.ProcessEnv;
84
83
  /** `--json` stdout purity: route human progress lines AND child stdout
85
84
  * to stderr, so stdout carries exactly one JSON object (the caller's
86
85
  * final envelope). Human mode keeps everything on stdout as before. */
@@ -88,5 +87,5 @@ export declare function runHarnessInstaller(harness: InstallableHarness, options
88
87
  }): HarnessInstallRunResult;
89
88
  export declare function harnessInstallCommand(args: ParsedArgs, json: boolean,
90
89
  /** Test seam: forwarded to `runHarnessInstaller` (spawn/home/... fakes). */
91
- runnerOptions?: Omit<NonNullable<Parameters<typeof runHarnessInstaller>[1]>, "json">): number;
90
+ runnerOptions?: Omit<NonNullable<Parameters<typeof runHarnessInstaller>[1]>, "json" | "target">): number;
92
91
  //# sourceMappingURL=harness-installer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"harness-installer.d.ts","sourceRoot":"","sources":["../src/harness-installer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE/C,OAAO,EAAE,UAAU,EAAE,SAAS,EAA+C,MAAM,SAAS,CAAC;AAQ7F,OAAO,EAAY,KAAK,UAAU,EAAE,MAAM,WAAW,CAAC;AAEtD,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAEnE,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAC;AAExE,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,kBAAkB,CAE/E;AAED;;;;;;aAMa;AACb,MAAM,MAAM,0BAA0B,GACpC,kBAAkB,GAAG,oBAAoB,GAAG,gBAAgB,CAAC;AA6B/D,eAAO,MAAM,kBAAkB,+BAA+B,CAAC;AAC/D;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,8CAA8C,CAAC;AAC3E,wEAAwE;AACxE,eAAO,MAAM,uBAAuB,+CAA+C,CAAC;AAoCpF,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,kBAAkB,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB;;gBAEY;IACZ,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B;;+EAE2E;IAC3E,YAAY,EAAE,0BAA0B,CAAC;CAC1C;AAED,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,kBAAkB,GAC1B,0BAA0B,CAgC5B;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB;+EAC2E;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,kBAAkB,EAC3B,OAAO,GAAE;IACP,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,SAAS,CAAC;IACzB,KAAK,CAAC,EAAE,OAAO,SAAS,CAAC;IACzB,MAAM,CAAC,EAAE,OAAO,UAAU,CAAC;IAC3B;;2EAEuE;IACvE,IAAI,CAAC,EAAE,OAAO,CAAC;CACX,GACL,uBAAuB,CAkGzB;AAwCD,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,OAAO;AACb,4EAA4E;AAC5E,aAAa,GAAE,IAAI,CAAC,WAAW,CAAC,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAM,GACvF,MAAM,CAoDR"}
1
+ {"version":3,"file":"harness-installer.d.ts","sourceRoot":"","sources":["../src/harness-installer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE/C,OAAO,EAAE,UAAU,EAAE,SAAS,EAA+C,MAAM,SAAS,CAAC;AAI7F,OAAO,EAAqB,KAAK,UAAU,EAAE,MAAM,WAAW,CAAC;AAe/D,OAAO,EAQL,KAAK,oBAAoB,EACzB,KAAK,kBAAkB,EACxB,MAAM,8BAA8B,CAAC;AAItC,OAAO,EACL,eAAe,EACf,uBAAuB,EACvB,kBAAkB,EAClB,uBAAuB,EACvB,0BAA0B,EAC1B,sBAAsB,EACtB,oBAAoB,EACpB,KAAK,oBAAoB,EACzB,KAAK,kBAAkB,GACxB,MAAM,8BAA8B,CAAC;AAEtC,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB;+EAC2E;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,0EAA0E;IAC1E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;mCAC+B;IAC/B,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AA6CD,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,kBAAkB,EAC3B,OAAO,GAAE;IACP,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,KAAK,CAAC,EAAE,OAAO,SAAS,CAAC;IACzB,KAAK,CAAC,EAAE,OAAO,SAAS,CAAC;IACzB,MAAM,CAAC,EAAE,OAAO,UAAU,CAAC;IAC3B;iEAC6D;IAC7D,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC3B;6EACyE;IACzE,SAAS,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IAC9B;;2EAEuE;IACvE,IAAI,CAAC,EAAE,OAAO,CAAC;CACX,GACL,uBAAuB,CAkNzB;AA8CD,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,OAAO;AACb,4EAA4E;AAC5E,aAAa,GAAE,IAAI,CACjB,WAAW,CAAC,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC,EACtD,MAAM,GAAG,QAAQ,CACb,GACL,MAAM,CA0ER"}
@@ -1,7 +1,9 @@
1
1
  /**
2
- * `claudexor harness install` — the disclosed, pinned remote vendor installer
2
+ * `claudexor harness install` — the disclosed, pinned vendor installer
3
3
  * (issue #89; restored from the PR #82 cut with the security objections
4
- * fixed).
4
+ * fixed). The historical/default `remote` target remains the SSH-host flow;
5
+ * an explicit `local` target installs into the managed toolchain root that
6
+ * local binary resolution and confinement already own.
5
7
  *
6
8
  * Contract:
7
9
  * - npm-distributed harnesses (claude/codex/opencode) install ONE exact
@@ -16,13 +18,26 @@
16
18
  * downloaded first (never piped to a shell), its size and sha256 are
17
19
  * printed, and it runs in the visible PTY the operator is watching — the
18
20
  * same principle as interactive SSH auth.
19
- * - NOTHING executes without disclosure: the exact command and install
21
+ * - NOTHING executes without authorization: the exact command and install
20
22
  * destination print first, and execution needs a TTY confirmation or an
21
23
  * explicit `--yes` (the macOS flow confirms against this module's own
22
- * `--dry-run --json` disclosure before passing `--yes`).
23
- * - failures are typed and loud; a failed download or non-zero installer exit
24
- * never reads as success, and the temp download dir is removed on every
25
- * path.
24
+ * `--dry-run --json` disclosure before passing `--yes`). For a host
25
+ * integration, `--target local --yes` IS that authorization, granted by the
26
+ * owner action that invoked it; there is no second confirmation hidden
27
+ * inside the producer, and the unattended script path never claims the
28
+ * human-observed verification the watched remote path earns.
29
+ * - concurrent installs into one prefix are serialized by a cross-process
30
+ * lease. A lease whose owner died is NOT reclaimed automatically: it fails
31
+ * closed with a typed `install_lock_stale` and the exact cleanup path.
32
+ * - failures are typed and loud; a failed download, a non-zero installer exit,
33
+ * or a failed post-install binary/version proof never reads as success, and
34
+ * the temp download dir is removed on every path.
35
+ * - the post-install PROOF is what the local target has instead of a witness.
36
+ * A watched remote install keeps the historical exit-code contract, because
37
+ * the operator is looking at the terminal; an unattended local install must
38
+ * earn its `ok:true`, so exit zero without a resolvable launcher and a
39
+ * matching `--version` is a typed failure, and `installedBinary` /
40
+ * `installedVersion` are present on every local success.
26
41
  * - `--json` keeps stdout pure: exactly ONE JSON object. In json mode every
27
42
  * human progress line goes to stderr and child processes (npm/curl/the
28
43
  * vendor script) run with their stdout routed onto stderr, so vendor
@@ -33,106 +48,61 @@ import { createHash } from "node:crypto";
33
48
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, readSync, rmSync } from "node:fs";
34
49
  import { homedir, tmpdir } from "node:os";
35
50
  import { dirname, join, resolve } from "node:path";
36
- import { CLAUDE_VENDOR_CLI_VERSION } from "@claudexor/harness-claude";
37
- import { CODEX_VENDOR_CLI_VERSION } from "@claudexor/harness-codex";
38
- import { OPENCODE_VENDOR_CLI_VERSION } from "@claudexor/harness-opencode";
39
51
  import { composeBaseEnv } from "@claudexor/core";
40
- import { flagBool } from "./args.js";
52
+ import { flagBool, flagStr } from "./args.js";
53
+ import { CliError, renderCliFailure } from "./cli-error.js";
41
54
  import { print, printJson, printUsageError } from "./cli-io.js";
42
55
  import { INSTALLABLE_HARNESSES } from "./harness-command-specs.js";
43
- export function isInstallableHarness(value) {
44
- return INSTALLABLE_HARNESSES.includes(value);
45
- }
46
- const NPM_PINS = {
47
- claude: {
48
- npmPackage: "@anthropic-ai/claude-code",
49
- version: CLAUDE_VENDOR_CLI_VERSION,
50
- verification: "release_verified",
51
- },
52
- codex: {
53
- npmPackage: "@openai/codex",
54
- version: CODEX_VENDOR_CLI_VERSION,
55
- verification: "release_verified",
56
- },
57
- opencode: {
58
- npmPackage: "opencode-ai",
59
- version: OPENCODE_VENDOR_CLI_VERSION,
60
- verification: "deterministic_only",
61
- },
62
- };
63
- export const CURSOR_INSTALL_URL = "https://cursor.com/install";
64
- /**
65
- * Google's official Antigravity CLI installer, as published on
66
- * antigravity.google/docs/cli/install (`curl -fsSL <this url> | bash`, which
67
- * Claudexor deliberately does NOT do — see the header). Verified end to end on
68
- * 2026-08-16: the script fetched from this URL installed agy 1.1.13 to
69
- * `~/.local/bin/agy`, which is the destination disclosed below. The vendor
70
- * ships one signed Go binary and no npm package, so it takes the same
71
- * human-observed path as cursor.
72
- */
73
- export const AGY_INSTALL_URL = "https://antigravity.google/cli/install.sh";
74
- /** The vendor's Windows installer, from the same documentation page. */
75
- export const AGY_INSTALL_URL_WINDOWS = "https://antigravity.google/cli/install.ps1";
76
- /** Vendors distributed as a shell installer instead of a pinnable npm
77
- * artifact. ONE branch serves both entries; the per-harness text below is the
78
- * only thing that differs, so a third such vendor is a row, not a fork. */
79
- const SCRIPT_INSTALLERS = {
80
- agy: {
81
- url: AGY_INSTALL_URL,
82
- // The one vendor here that publishes a Windows installer of its own; the
83
- // POSIX row would otherwise be all Claudexor could honestly offer.
84
- windowsUrl: AGY_INSTALL_URL_WINDOWS,
85
- installLocation: "~/.local/bin (as selected by Google's Antigravity installer)",
86
- pinNote: "none — Antigravity ships no pinnable npm artifact; the vendor script is downloaded in full, its size and sha256 print, and it runs in this terminal where you watch it",
87
- },
88
- cursor: {
89
- url: CURSOR_INSTALL_URL,
90
- installLocation: "~/.local/bin (or ~/.cursor/bin, as selected by Cursor's installer)",
91
- pinNote: "none — Cursor ships no pinnable npm artifact; the vendor script is downloaded in full, its size and sha256 print, and it runs in this terminal where you watch it",
92
- },
93
- };
94
- /** Membership is asked of the TABLE, never re-typed: a third script vendor is
95
- * one row, and cannot be added to the table yet fall through here. */
96
- function scriptInstaller(harness) {
97
- return Object.hasOwn(SCRIPT_INSTALLERS, harness)
98
- ? SCRIPT_INSTALLERS[harness]
99
- : null;
56
+ import { acquireHarnessInstallLease, HARNESS_INSTALL_LOCK_TIMEOUT_MS, } from "./harness-install-lease.js";
57
+ import { proveInstalledNpm, proveInstalledScriptVendor, } from "./harness-install-proof.js";
58
+ import { harnessInstallerDisclosure, isHarnessInstallTarget, isInstallableHarness, NPM_PINS, scriptInstaller, TARGET_LAYOUTS, } from "./harness-install-recipes.js";
59
+ // The recipe surface is re-exported from its historical home: callers and
60
+ // tests import `harness-installer.js`, and the split is an internal one.
61
+ export { AGY_INSTALL_URL, AGY_INSTALL_URL_WINDOWS, CURSOR_INSTALL_URL, HARNESS_INSTALL_TARGETS, harnessInstallerDisclosure, isHarnessInstallTarget, isInstallableHarness, } from "./harness-install-recipes.js";
62
+ function verificationFailure(harness, reason, evidence = {}) {
63
+ return {
64
+ exitCode: 1,
65
+ code: "install_verification_failed",
66
+ refusal: `${harness} installer exited successfully, but installation verification failed: ${reason}`,
67
+ ...evidence,
68
+ };
100
69
  }
101
- export function harnessInstallerDisclosure(harness) {
102
- const pin = NPM_PINS[harness];
103
- if (pin) {
104
- return {
105
- harness,
106
- command: `npm install --global --prefix ~/.claudexor/remote/vendor ${pin.npmPackage}@${pin.version}`,
107
- installLocation: "~/.claudexor/remote/vendor/bin",
108
- pinnedVersion: pin.version,
109
- verification: pin.verification,
110
- };
111
- }
112
- const script = scriptInstaller(harness);
113
- /* c8 ignore next -- every non-npm harness has a script row; this is the
114
- unreachable guard that keeps the two tables honest. */
115
- if (!script)
116
- throw new Error(`harness ${harness} has neither an npm pin nor a script installer`);
117
- const windows = process.platform === "win32";
118
- const url = windows ? (script.windowsUrl ?? script.url) : script.url;
119
- const file = windows && script.windowsUrl ? "install.ps1" : "install.sh";
120
- const runner = windows && script.windowsUrl ? "powershell -ExecutionPolicy Bypass -File" : "/bin/sh";
70
+ /** The managed toolchain root and its npm/PATH contract are POSIX-only in this
71
+ * release, so the local target refuses on Windows instead of installing into a
72
+ * prefix nothing would then resolve. The remote target is unaffected. */
73
+ function localPlatformRefusal(platform) {
74
+ if (platform !== "win32")
75
+ return null;
121
76
  return {
122
- harness,
123
- command: `curl --fail --silent --show-error --location ${url} ` +
124
- `--output <private-tmpdir>/${file} && ${runner} <private-tmpdir>/${file}`,
125
- installLocation: windows && script.windowsUrl
126
- ? "%LOCALAPPDATA%\\agy\\bin (as selected by Google's Antigravity installer)"
127
- : script.installLocation,
128
- pinnedVersion: null,
129
- verification: "human_observed",
77
+ exitCode: 1,
78
+ code: "unsupported_platform",
79
+ refusal: "--target local is not supported on Windows by this release; nothing was executed",
130
80
  };
131
81
  }
82
+ /** An unexpected throw is still ONE typed JSON object, never a stack trace on
83
+ * a machine caller's stdout. */
84
+ function harnessInstallException(error) {
85
+ const causeCode = error && typeof error === "object" && typeof error.code === "string"
86
+ ? String(error.code)
87
+ : null;
88
+ const detail = error instanceof Error ? error.message : String(error);
89
+ return new CliError("operational", `harness installer failed without a verified result: ${detail}`, {
90
+ code: "harness_install_failed",
91
+ ...(causeCode ? { details: { causeCode } } : {}),
92
+ });
93
+ }
132
94
  export function runHarnessInstaller(harness, options = {}) {
133
95
  const home = resolve(options.home ?? homedir());
96
+ const target = options.target ?? "remote";
97
+ const platform = options.platform ?? process.platform;
98
+ if (target === "local") {
99
+ const unsupported = localPlatformRefusal(platform);
100
+ if (unsupported)
101
+ return unsupported;
102
+ }
134
103
  const spawn = options.spawn ?? spawnSync;
135
104
  const json = options.json === true;
105
+ const runnerNodePath = resolve(options.nodePath ?? process.execPath);
136
106
  // stdin inherited; child stdout -> OUR stderr (fd 2) in json mode so
137
107
  // vendor/npm output stays visible but never pollutes the JSON envelope.
138
108
  const childStdio = json ? [0, 2, 2] : "inherit";
@@ -144,80 +114,180 @@ export function runHarnessInstaller(harness, options = {}) {
144
114
  };
145
115
  // Vendor-controlled npm/curl/shell children receive the shared minimal
146
116
  // runtime env, never the parent process's provider credentials.
147
- const environment = { ...composeBaseEnv("clean"), HOME: home };
117
+ const resolutionSource = {
118
+ ...(options.sourceEnv ?? process.env),
119
+ HOME: home,
120
+ // Local resolution must not read the SSH-runtime vendor prefix; the remote
121
+ // flow keeps whatever its own runtime already exported.
122
+ ...(target === "local" ? { CLAUDEXOR_REMOTE_RUNTIME: "0" } : {}),
123
+ };
124
+ const environment = {
125
+ ...composeBaseEnv("clean", resolutionSource, runnerNodePath, platform),
126
+ HOME: home,
127
+ };
148
128
  const pin = NPM_PINS[harness];
129
+ const script = scriptInstaller(harness);
130
+ let npmCLI;
149
131
  if (pin) {
150
- const vendorRoot = join(home, ".claudexor", "remote", "vendor");
151
- (options.mkdir ?? mkdirSync)(vendorRoot, { recursive: true, mode: 0o700 });
152
- const nodePath = resolve(options.nodePath ?? process.execPath);
153
- const npmCLI = resolve(dirname(nodePath), "..", "lib", "node_modules", "npm", "bin", "npm-cli.js");
132
+ npmCLI = resolve(dirname(runnerNodePath), "..", "lib", "node_modules", "npm", "bin", "npm-cli.js");
154
133
  if (!(options.exists ?? existsSync)(npmCLI)) {
155
134
  return {
156
135
  exitCode: 1,
136
+ code: "embedded_npm_missing",
157
137
  refusal: `the bundled npm entrypoint is missing at ${npmCLI} ` +
158
138
  "(expected inside the pinned Node runtime next to this CLI); nothing was executed",
159
139
  };
160
140
  }
161
- const result = spawn(nodePath, [npmCLI, "install", "--global", "--prefix", vendorRoot, `${pin.npmPackage}@${pin.version}`], { stdio: childStdio, env: environment });
162
- return { exitCode: result.status ?? 1 };
163
- }
164
- // Script vendors (cursor, agy): download the COMPLETE vendor script before
165
- // execution (`--fail` rejects HTTP error bodies), read it back and print its
166
- // size + sha256 so the watching human sees exactly which bytes are about to
167
- // run, and remove the private temp dir on every success/failure path.
168
- const script = scriptInstaller(harness);
169
- /* c8 ignore next */
170
- if (!script)
171
- return { exitCode: 1, refusal: `no installer is defined for ${harness}` };
172
- const windows = process.platform === "win32";
173
- const useWindowsScript = windows && script.windowsUrl !== undefined;
174
- const url = useWindowsScript ? script.windowsUrl : script.url;
175
- if (windows && !useWindowsScript) {
176
- return {
177
- exitCode: 1,
178
- refusal: `${harness} publishes no Windows installer; install it yourself and re-run \`claudexor doctor\``,
179
- };
180
141
  }
181
- const temporaryDirectory = mkdtempSync(join(tmpdir(), `claudexor-${harness}-install-`));
182
- const installerPath = join(temporaryDirectory, useWindowsScript ? "install.ps1" : "install.sh");
142
+ const proofRuntime = { runnerNodePath, platform, resolutionSource, environment, spawn };
143
+ // The remote target runs in a PTY the operator is watching and keeps the
144
+ // historical exit-code contract. Only the unattended local target has to
145
+ // prove what it installed.
146
+ const proofRequired = target === "local";
147
+ const proveInstalled = () => pin
148
+ ? proveInstalledNpm({
149
+ vendorRoot: TARGET_LAYOUTS[target].root(home),
150
+ npmPackage: pin.npmPackage,
151
+ binaryNames: pin.binaryNames,
152
+ expectedVersion: pin.version,
153
+ }, proofRuntime)
154
+ : /* c8 ignore next -- every non-npm harness has a script row */
155
+ proveInstalledScriptVendor(script?.binaryName ?? harness, proofRuntime);
156
+ let lease;
183
157
  try {
184
- const downloaded = spawn("curl",
185
- // `url`, never `script.url`: on Windows the disclosure names the
186
- // vendor's PowerShell installer, and the bytes fetched MUST be the bytes
187
- // disclosed both reviewers of the sprint triad caught the divergence.
188
- ["--fail", "--silent", "--show-error", "--location", url, "--output", installerPath], { stdio: childStdio, env: environment });
189
- if (downloaded.status !== 0) {
158
+ // Serialization belongs to the same contract as the proof: a machine caller
159
+ // can race itself, while the remote flow is one operator watching one
160
+ // terminal. Taking the lease there would add a 120s block and a manual
161
+ // cleanup step to a path this release promises to leave alone.
162
+ if (proofRequired && options.lock !== false) {
163
+ try {
164
+ lease = acquireHarnessInstallLease(home, options.lockTimeoutMs ?? HARNESS_INSTALL_LOCK_TIMEOUT_MS);
165
+ }
166
+ catch (error) {
167
+ const code = typeof error.code === "string"
168
+ ? String(error.code)
169
+ : "install_lock_failed";
170
+ return {
171
+ exitCode: 1,
172
+ code,
173
+ refusal: `${error instanceof Error ? error.message : String(error)}; nothing was executed`,
174
+ };
175
+ }
176
+ }
177
+ if (pin && npmCLI) {
178
+ const vendorRoot = TARGET_LAYOUTS[target].root(home);
179
+ const alreadyInstalled = proofRequired ? proveInstalled() : null;
180
+ if (alreadyInstalled?.ok) {
181
+ note(`${harness} ${pin.version} is already installed at ${vendorRoot}; nothing to change`);
182
+ return { exitCode: 0, ...alreadyInstalled.proof };
183
+ }
184
+ (options.mkdir ?? mkdirSync)(vendorRoot, { recursive: true, mode: 0o700 });
185
+ const result = spawn(runnerNodePath, [npmCLI, "install", "--global", "--prefix", vendorRoot, `${pin.npmPackage}@${pin.version}`], { stdio: childStdio, env: environment });
186
+ if (result.status !== 0)
187
+ return { exitCode: result.status ?? 1 };
188
+ if (!proofRequired)
189
+ return { exitCode: 0 };
190
+ const installed = proveInstalled();
191
+ return installed.ok
192
+ ? { exitCode: 0, ...installed.proof }
193
+ : verificationFailure(harness, installed.reason);
194
+ }
195
+ // Script vendors (cursor, agy): download the COMPLETE vendor script before
196
+ // execution (`--fail` rejects HTTP error bodies), read it back and record
197
+ // its size + sha256 so the receipt names exactly which bytes ran. Remote
198
+ // execution stays human-observed; local `--yes` is explicitly authorized
199
+ // unattended execution. The private temp dir is removed on every path.
200
+ /* c8 ignore next */
201
+ if (!script)
190
202
  return {
191
- exitCode: downloaded.status ?? 1,
192
- refusal: `the download of ${url} failed (curl exit ${downloaded.status ?? "unknown"}); nothing was executed`,
203
+ exitCode: 1,
204
+ code: "no_installer",
205
+ refusal: `no installer is defined for ${harness}`,
193
206
  };
207
+ const alreadyInstalled = proofRequired ? proveInstalled() : null;
208
+ if (alreadyInstalled?.ok) {
209
+ note(`${harness} is already installed at ${alreadyInstalled.proof.installedBinary}; nothing to change`);
210
+ return { exitCode: 0, ...alreadyInstalled.proof };
194
211
  }
195
- let payload;
196
- try {
197
- payload = readFileSync(installerPath);
198
- }
199
- catch {
212
+ const windows = platform === "win32";
213
+ const useWindowsScript = windows && script.windowsUrl !== undefined;
214
+ const url = useWindowsScript ? script.windowsUrl : script.url;
215
+ if (windows && !useWindowsScript) {
200
216
  return {
201
217
  exitCode: 1,
202
- refusal: "the downloaded installer script could not be read back; nothing was executed",
218
+ code: "unsupported_platform",
219
+ refusal: `${harness} publishes no Windows installer; install it yourself and re-run \`claudexor doctor\``,
203
220
  };
204
221
  }
205
- note(`${harness} installer downloaded: ${payload.length} bytes, ` +
206
- `sha256 ${createHash("sha256").update(payload).digest("hex")}`);
207
- const runner = useWindowsScript
208
- ? ["powershell", ["-ExecutionPolicy", "Bypass", "-File", installerPath]]
209
- : ["/bin/sh", [installerPath]];
210
- note(`running: ${runner[0]} ${runner[1].join(" ")}`);
211
- const executed = spawn(runner[0], runner[1], { stdio: childStdio, env: environment });
212
- return { exitCode: executed.status ?? 1 };
222
+ const temporaryDirectory = mkdtempSync(join(tmpdir(), `claudexor-${harness}-install-`));
223
+ const installerPath = join(temporaryDirectory, useWindowsScript ? "install.ps1" : "install.sh");
224
+ try {
225
+ const downloaded = spawn("curl",
226
+ // `url`, never `script.url`: on Windows the disclosure names the
227
+ // vendor's PowerShell installer, and the bytes fetched MUST be the bytes
228
+ // disclosed both reviewers of the sprint triad caught the divergence.
229
+ ["--fail", "--silent", "--show-error", "--location", url, "--output", installerPath], { stdio: childStdio, env: environment });
230
+ if (downloaded.status !== 0) {
231
+ return {
232
+ exitCode: downloaded.status ?? 1,
233
+ code: "installer_download_failed",
234
+ refusal: `the download of ${url} failed (curl exit ${downloaded.status ?? "unknown"}); nothing was executed`,
235
+ };
236
+ }
237
+ let payload;
238
+ try {
239
+ payload = readFileSync(installerPath);
240
+ }
241
+ catch {
242
+ return {
243
+ exitCode: 1,
244
+ code: "installer_read_failed",
245
+ refusal: "the downloaded installer script could not be read back; nothing was executed",
246
+ };
247
+ }
248
+ // Local only: an unattended install must not "run" an empty body and
249
+ // report success. The watched remote flow keeps upstream's behaviour —
250
+ // the human sees "0 bytes" printed and the vendor script exit honestly.
251
+ if (proofRequired && payload.length === 0) {
252
+ return {
253
+ exitCode: 1,
254
+ code: "installer_empty",
255
+ refusal: "the downloaded installer script is empty; nothing was executed",
256
+ };
257
+ }
258
+ const installerSha256 = createHash("sha256").update(payload).digest("hex");
259
+ const installerByteLength = payload.length;
260
+ note(`${harness} installer downloaded: ${installerByteLength} bytes, sha256 ${installerSha256}`);
261
+ const runner = useWindowsScript
262
+ ? ["powershell", ["-ExecutionPolicy", "Bypass", "-File", installerPath]]
263
+ : ["/bin/sh", [installerPath]];
264
+ note(`running: ${runner[0]} ${runner[1].join(" ")}`);
265
+ const executed = spawn(runner[0], runner[1], { stdio: childStdio, env: environment });
266
+ if (executed.status !== 0) {
267
+ return { exitCode: executed.status ?? 1, installerSha256, installerByteLength };
268
+ }
269
+ if (!proofRequired)
270
+ return { exitCode: 0 };
271
+ const installed = proveInstalled();
272
+ return installed.ok
273
+ ? { exitCode: 0, installerSha256, installerByteLength, ...installed.proof }
274
+ : verificationFailure(harness, installed.reason, { installerSha256, installerByteLength });
275
+ }
276
+ finally {
277
+ rmSync(temporaryDirectory, { recursive: true, force: true });
278
+ }
213
279
  }
214
280
  finally {
215
- rmSync(temporaryDirectory, { recursive: true, force: true });
281
+ lease?.release();
216
282
  }
217
283
  }
218
- const INSTALL_USAGE = `usage: claudexor harness install <${INSTALLABLE_HARNESSES.join("|")}> [--dry-run] [--yes]`;
284
+ const INSTALL_USAGE = `usage: claudexor harness install <${INSTALLABLE_HARNESSES.join("|")}> [--target <local|remote>] [--dry-run] [--yes]`;
219
285
  function pinDisclosureLine(disclosure) {
220
286
  if (disclosure.pinnedVersion === null) {
287
+ if (disclosure.verification === "unattended_unpinned") {
288
+ return ("Version pin: none — this vendor ships no pinnable artifact; this authorized " +
289
+ "unattended install records the downloaded script's size and sha256 instead of a human's attention");
290
+ }
221
291
  return `Version pin: ${scriptInstaller(disclosure.harness)?.pinNote ?? "none"}`;
222
292
  }
223
293
  if (disclosure.verification === "deterministic_only") {
@@ -258,7 +328,25 @@ runnerOptions = {}) {
258
328
  if (!isInstallableHarness(harness) || args._.length !== 3) {
259
329
  return printUsageError(json, INSTALL_USAGE);
260
330
  }
261
- const disclosure = harnessInstallerDisclosure(harness);
331
+ const targetValue = flagStr(args, "target");
332
+ if ((Object.prototype.hasOwnProperty.call(args.flags, "target") && targetValue === undefined) ||
333
+ (targetValue !== undefined && !isHarnessInstallTarget(targetValue))) {
334
+ return printUsageError(json, `${INSTALL_USAGE}\nclaudexor: --target must be local or remote`);
335
+ }
336
+ const target = targetValue ?? "remote";
337
+ const disclosure = harnessInstallerDisclosure(harness, target);
338
+ // Refuse the unsupported layout BEFORE the dry run, so a machine caller's
339
+ // disclosure never advertises an install this host cannot perform.
340
+ if (target === "local") {
341
+ const unsupported = localPlatformRefusal(runnerOptions.platform ?? process.platform);
342
+ if (unsupported) {
343
+ if (json)
344
+ printJson({ ok: false, dryRun: flagBool(args, "dry-run"), ...unsupported, ...disclosure });
345
+ else
346
+ print(`Install refused: ${unsupported.refusal}`);
347
+ return 1;
348
+ }
349
+ }
262
350
  if (flagBool(args, "dry-run")) {
263
351
  if (json)
264
352
  printJson({ ok: true, dryRun: true, ...disclosure });
@@ -267,7 +355,8 @@ runnerOptions = {}) {
267
355
  return 0;
268
356
  }
269
357
  // Disclosure precedes EVERY execution path; --json without --yes refuses
270
- // (machine callers must have shown the dry-run disclosure themselves).
358
+ // (machine callers must have shown the dry-run disclosure themselves, and a
359
+ // local `--yes` IS the authorization their owning Connect action granted).
271
360
  if (!json)
272
361
  printHumanDisclosure(disclosure);
273
362
  if (!flagBool(args, "yes")) {
@@ -291,16 +380,19 @@ runnerOptions = {}) {
291
380
  return 1;
292
381
  }
293
382
  }
294
- const result = runHarnessInstaller(harness, { ...runnerOptions, json });
383
+ let result;
384
+ try {
385
+ result = runHarnessInstaller(harness, { ...runnerOptions, json, target });
386
+ }
387
+ catch (error) {
388
+ return renderCliFailure(json, harnessInstallException(error), {
389
+ messagePrefix: "claudexor harness install:",
390
+ extras: { dryRun: false, ...disclosure },
391
+ });
392
+ }
295
393
  const ok = result.exitCode === 0 && result.refusal === undefined;
296
394
  if (json) {
297
- printJson({
298
- ok,
299
- dryRun: false,
300
- exitCode: result.exitCode,
301
- ...(result.refusal === undefined ? {} : { refusal: result.refusal }),
302
- ...disclosure,
303
- });
395
+ printJson({ ok, dryRun: false, ...result, ...disclosure });
304
396
  }
305
397
  else if (result.refusal !== undefined) {
306
398
  print(`Install refused: ${result.refusal}`);