@webpresso/agent-kit 3.3.3 → 3.3.4

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 (136) hide show
  1. package/THIRD-PARTY-NOTICES.md +12 -10
  2. package/catalog/agent/rules/changeset-release.md +15 -4
  3. package/catalog/agent/rules/managed-tool-binaries.md +67 -0
  4. package/catalog/agent/rules/parallel-orchestration-quota.md +99 -1
  5. package/catalog/agent/rules/pre-implementation.md +36 -20
  6. package/catalog/agent/rules/rtk-routing.md +22 -16
  7. package/catalog/agent/skills/autopilot/SKILL.md +4 -3
  8. package/catalog/agent/skills/ultragoal/SKILL.md +11 -9
  9. package/dist/esm/audit/hook-surface.js +5 -1
  10. package/dist/esm/audit/repo-guardrails.d.ts +8 -0
  11. package/dist/esm/audit/repo-guardrails.js +71 -1
  12. package/dist/esm/blueprint/core/parser.js +38 -12
  13. package/dist/esm/blueprint/execution/artifacts.js +8 -4
  14. package/dist/esm/blueprint/execution/frontmatter-keys.d.ts +17 -0
  15. package/dist/esm/blueprint/execution/frontmatter-keys.js +24 -0
  16. package/dist/esm/blueprint/execution/metadata.js +8 -4
  17. package/dist/esm/blueprint/lifecycle/audit.d.ts +21 -0
  18. package/dist/esm/blueprint/lifecycle/audit.js +89 -3
  19. package/dist/esm/blueprint/lifecycle/review-provenance.js +63 -10
  20. package/dist/esm/blueprint/lifecycle/rollout-anchor.d.ts +22 -0
  21. package/dist/esm/blueprint/lifecycle/rollout-anchor.js +37 -1
  22. package/dist/esm/blueprint/markdown/blocked-line.d.ts +49 -0
  23. package/dist/esm/blueprint/markdown/blocked-line.js +63 -0
  24. package/dist/esm/blueprint/markdown/helpers.js +10 -4
  25. package/dist/esm/blueprint/tracked-document/parser.js +3 -3
  26. package/dist/esm/blueprint/trust/gate-row.d.ts +55 -0
  27. package/dist/esm/blueprint/trust/gate-row.js +113 -0
  28. package/dist/esm/blueprint/trust/promotion.d.ts +17 -0
  29. package/dist/esm/blueprint/trust/promotion.js +19 -19
  30. package/dist/esm/build/atomic-file-copy.d.ts +32 -0
  31. package/dist/esm/build/atomic-file-copy.js +44 -0
  32. package/dist/esm/build/vendor-oxlint-plugins.d.ts +8 -0
  33. package/dist/esm/build/vendor-oxlint-plugins.js +53 -0
  34. package/dist/esm/cli/commands/blueprint/mutations.d.ts +15 -0
  35. package/dist/esm/cli/commands/blueprint/mutations.js +8 -2
  36. package/dist/esm/cli/commands/compile.d.ts +13 -0
  37. package/dist/esm/cli/commands/compile.js +182 -62
  38. package/dist/esm/cli/commands/format.js +17 -1
  39. package/dist/esm/cli/commands/gain/index.js +10 -3
  40. package/dist/esm/cli/commands/init/gitignore-patcher.js +6 -0
  41. package/dist/esm/cli/commands/init/index.js +29 -48
  42. package/dist/esm/cli/commands/init/mcp-spec.d.ts +48 -13
  43. package/dist/esm/cli/commands/init/mcp-spec.js +86 -30
  44. package/dist/esm/cli/commands/init/merge.js +30 -10
  45. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.d.ts +56 -31
  46. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +180 -43
  47. package/dist/esm/cli/commands/init/scaffolders/rtk/index.d.ts +17 -5
  48. package/dist/esm/cli/commands/init/scaffolders/rtk/index.js +56 -75
  49. package/dist/esm/cli/commands/init/scaffolders/runtime-check/index.js +7 -6
  50. package/dist/esm/cli/commands/init/untracked-collision.d.ts +12 -1
  51. package/dist/esm/cli/commands/init/untracked-collision.js +23 -1
  52. package/dist/esm/cli/commands/pr-help.js +1 -1
  53. package/dist/esm/cli/commands/pr.js +18 -4
  54. package/dist/esm/cli/commands/review.js +60 -6
  55. package/dist/esm/cli/commands/worktree/router-dispatch.d.ts +24 -2
  56. package/dist/esm/cli/commands/worktree/router-dispatch.js +80 -48
  57. package/dist/esm/cli/commands/worktree/router.js +5 -2
  58. package/dist/esm/cli/direct-provider-launch.js +12 -4
  59. package/dist/esm/compiler/flatten.d.ts +10 -1
  60. package/dist/esm/compiler/flatten.js +14 -4
  61. package/dist/esm/compose/admin-blocks-registry-integrity.d.ts +22 -0
  62. package/dist/esm/compose/admin-blocks-registry-integrity.js +90 -0
  63. package/dist/esm/compose/compose.d.ts +8 -0
  64. package/dist/esm/compose/compose.js +115 -8
  65. package/dist/esm/compose/ops-report-schema.js +17 -3
  66. package/dist/esm/compose/registry-resolve.d.ts +17 -0
  67. package/dist/esm/compose/registry-resolve.js +78 -11
  68. package/dist/esm/config/oxlint/graphql-conventions.js +2 -5
  69. package/dist/esm/config/oxlint/import-hygiene.js +21 -8
  70. package/dist/esm/config/oxlint/oxlint-config.fixture.d.ts +1 -1
  71. package/dist/esm/config/oxlint/oxlint-config.fixture.js +2 -3
  72. package/dist/esm/config/oxlint/oxlintrc.d.ts +5 -7
  73. package/dist/esm/config/oxlint/oxlintrc.js +18 -21
  74. package/dist/esm/config/oxlint/path-roles.js +53 -0
  75. package/dist/esm/config/oxlint/query-patterns.js +2 -5
  76. package/dist/esm/daemon/domains/blueprint-exec-ledger.d.ts +86 -0
  77. package/dist/esm/daemon/domains/blueprint-exec-ledger.js +134 -0
  78. package/dist/esm/daemon/domains/blueprint-exec.d.ts +106 -0
  79. package/dist/esm/daemon/domains/blueprint-exec.js +161 -0
  80. package/dist/esm/daemon/domains/launch.d.ts +25 -0
  81. package/dist/esm/daemon/domains/launch.js +30 -1
  82. package/dist/esm/daemon/handlers/blueprints-handlers.d.ts +12 -1
  83. package/dist/esm/daemon/handlers/blueprints-handlers.js +148 -6
  84. package/dist/esm/daemon/handlers/product.d.ts +10 -0
  85. package/dist/esm/daemon/handlers/product.js +10 -1
  86. package/dist/esm/daemon/methods.d.ts +21 -1
  87. package/dist/esm/daemon/methods.js +20 -0
  88. package/dist/esm/daemon/protocol/contract.d.ts +58 -0
  89. package/dist/esm/daemon/protocol/contract.js +38 -0
  90. package/dist/esm/daemon/server.js +7 -0
  91. package/dist/esm/docs-linter/blueprint-plan.js +2 -2
  92. package/dist/esm/errors/wp-error.js +21 -2
  93. package/dist/esm/hooks/doctor.d.ts +11 -0
  94. package/dist/esm/hooks/doctor.js +114 -45
  95. package/dist/esm/hooks/pretool-guard/dev-routing.js +1 -1
  96. package/dist/esm/hooks/pretool-guard/rtk-rewrite.d.ts +24 -0
  97. package/dist/esm/hooks/pretool-guard/rtk-rewrite.js +50 -0
  98. package/dist/esm/hooks/pretool-guard/runner.js +6 -0
  99. package/dist/esm/hooks/status/index.js +1 -1
  100. package/dist/esm/mcp/blueprint/_shared/lifecycle.js +39 -11
  101. package/dist/esm/mcp/blueprint/handlers/review-log.d.ts +14 -7
  102. package/dist/esm/mcp/blueprint/handlers/review-log.js +46 -22
  103. package/dist/esm/mcp/blueprint/registration.js +1 -1
  104. package/dist/esm/mcp/tools/_shared/redact.d.ts +22 -0
  105. package/dist/esm/mcp/tools/_shared/redact.js +114 -8
  106. package/dist/esm/mcp/tools/gain.js +28 -2
  107. package/dist/esm/mcp/tools/worktree.js +65 -3
  108. package/dist/esm/mcp/tools/wp-ui-compose.js +38 -4
  109. package/dist/esm/mcp/tools/wp-ui-preview.js +14 -7
  110. package/dist/esm/review/lifecycle-writers.d.ts +76 -0
  111. package/dist/esm/review/lifecycle-writers.js +330 -0
  112. package/dist/esm/review/subject.js +66 -32
  113. package/dist/esm/status/snapshot.d.ts +5 -0
  114. package/dist/esm/status/snapshot.js +27 -2
  115. package/dist/esm/test/shard-durations.json +0 -1
  116. package/dist/esm/test-helpers/global-setup.js +38 -3
  117. package/dist/esm/tool-runtime/managed-rtk.d.ts +44 -0
  118. package/dist/esm/tool-runtime/managed-rtk.js +275 -0
  119. package/dist/esm/tool-runtime/resolve-runner.js +18 -194
  120. package/dist/esm/utils/package-root.d.ts +32 -0
  121. package/dist/esm/utils/package-root.js +82 -0
  122. package/dist/esm/worktrees/git-metadata.d.ts +41 -0
  123. package/dist/esm/worktrees/git-metadata.js +48 -0
  124. package/package.json +16 -21
  125. package/dist/esm/config/oxlint/code-safety.d.ts +0 -18
  126. package/dist/esm/config/oxlint/foundation-purity.d.ts +0 -21
  127. package/dist/esm/config/oxlint/graphql-conventions.d.ts +0 -23
  128. package/dist/esm/config/oxlint/import-hygiene.d.ts +0 -34
  129. package/dist/esm/config/oxlint/index.d.ts +0 -22
  130. package/dist/esm/config/oxlint/index.js +0 -28
  131. package/dist/esm/config/oxlint/monorepo-paths.d.ts +0 -22
  132. package/dist/esm/config/oxlint/query-patterns.d.ts +0 -24
  133. package/dist/esm/config/oxlint/testing-quality.d.ts +0 -35
  134. package/dist/esm/config/oxlint/tier-boundaries.d.ts +0 -35
  135. package/dist/esm/output-transforms/rulesync.d.ts +0 -2
  136. package/dist/esm/output-transforms/rulesync.js +0 -79
@@ -51,6 +51,10 @@ export declare const wpStatusSnapshotSchema: z.ZodObject<{
51
51
  path: z.ZodString;
52
52
  lifecycle: z.ZodString;
53
53
  approvalsSatisfied: z.ZodBoolean;
54
+ approvalsSatisfiedBy: z.ZodOptional<z.ZodEnum<{
55
+ delivery: "delivery";
56
+ plan: "plan";
57
+ }>>;
54
58
  }, z.core.$strip>>;
55
59
  ultragoal: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
56
60
  reviews: z.ZodOptional<z.ZodObject<{
@@ -127,6 +131,7 @@ export interface PrReadinessEvidence {
127
131
  readonly path: string;
128
132
  readonly lifecycle: string;
129
133
  readonly approvalsSatisfied: boolean;
134
+ readonly approvalsSatisfiedBy?: "plan" | "delivery";
130
135
  };
131
136
  }
132
137
  interface ReadinessEvaluation {
@@ -42,6 +42,9 @@ const statusContextSchema = z.object({
42
42
  path: z.string(),
43
43
  lifecycle: z.string(),
44
44
  approvalsSatisfied: z.boolean(),
45
+ // Which review purpose carried the satisfying approval ("plan" or
46
+ // "delivery"), so a satisfied/unsatisfied result is attributable.
47
+ approvalsSatisfiedBy: z.enum(["plan", "delivery"]).optional(),
45
48
  })
46
49
  .optional(),
47
50
  ultragoal: z.record(z.string(), z.unknown()).optional(),
@@ -319,6 +322,20 @@ function selectBlueprintFile(projectRoot, pr) {
319
322
  const priority = (file) => file.changeType === "ADDED" ? 0 : file.changeType === "RENAMED" ? 1 : 2;
320
323
  return candidates.toSorted((left, right) => priority(left) - priority(right))[0];
321
324
  }
325
+ // The authoritative CI gate (`validateApprovalGate` -> `provenanceBackedApprovalReviewers`)
326
+ // accepts an approval recorded for EITHER purpose: it defaults to
327
+ // `["plan", "delivery"]` when no purpose is pinned. This advisory snapshot used to
328
+ // query `"delivery"` only, so a blueprint approved at the plan boundary reported
329
+ // `review-provenance` unsatisfied while CI passed on the same commit — an advisory
330
+ // blocker stricter than the gate it advises about, which reads as a real problem.
331
+ // Union both purposes here so the two agree.
332
+ //
333
+ // Deliberately NOT reusing `provenanceBackedApprovalReviewers` directly: that helper
334
+ // resolves v2 provenance at `HEAD`, whereas this snapshot must answer for the PR's
335
+ // exact target commit. Matching its purpose set while keeping `targetHash` is the
336
+ // smallest change that makes the two evaluators agree without loosening exact-SHA
337
+ // semantics.
338
+ const APPROVAL_PURPOSES = ["plan", "delivery"];
322
339
  function resolveBlueprintEvidence(projectRoot, pr, targetHash) {
323
340
  const blueprintFile = selectBlueprintFile(projectRoot, pr);
324
341
  if (!blueprintFile || !targetHash)
@@ -327,12 +344,20 @@ function resolveBlueprintEvidence(projectRoot, pr, targetHash) {
327
344
  const authority = readReviewAuthorityAtRef(projectRoot, targetHash, blueprintFile.stableSlug, "delivery");
328
345
  if (authority.blueprintPath !== blueprintFile.path)
329
346
  return undefined;
330
- const approvals = collectV2ReviewProvenance(path.resolve(projectRoot, blueprintFile.path), "delivery", targetHash);
347
+ const blueprintPath = path.resolve(projectRoot, blueprintFile.path);
348
+ const perPurpose = APPROVAL_PURPOSES.map((purpose) => ({
349
+ purpose,
350
+ reviewers: collectV2ReviewProvenance(blueprintPath, purpose, targetHash).map((approval) => approval.reviewer),
351
+ }));
352
+ const satisfiedPurpose = perPurpose.find((entry) => evaluateApprovalReviewers(entry.reviewers).satisfied);
331
353
  return {
332
354
  path: blueprintFile.path,
333
355
  lifecycle: authority.lifecycle,
334
- approvalsSatisfied: evaluateApprovalReviewers(approvals.map((approval) => approval.reviewer))
356
+ approvalsSatisfied: evaluateApprovalReviewers(perPurpose.flatMap((entry) => entry.reviewers))
335
357
  .satisfied,
358
+ // Which purpose carried the approval, so a red/green flip is attributable
359
+ // instead of opaque.
360
+ approvalsSatisfiedBy: satisfiedPurpose?.purpose,
336
361
  };
337
362
  }
338
363
  catch {
@@ -560,7 +560,6 @@
560
560
  "src/audit/no-legacy-cli-bin.test.ts": 56.865957999999864,
561
561
  "src/mcp/tools/session-elision.test.ts": 4564.6535,
562
562
  "src/hooks/pretool-guard/validators/ux-quality.test.ts": 105.19341700000041,
563
- "src/config/oxlint/monorepo-paths.test.ts": 11.33191700000043,
564
563
  "src/cli/commands/init/source-repo-hook-policy.test.ts": 65.89804200000003,
565
564
  "src/secrets/config/schema.test.ts": 190.3588749999999,
566
565
  "scripts/bench/scenarios/_schema.test.ts": 53.78470799999968,
@@ -40,13 +40,38 @@ async function withDistBuildLock(fn) {
40
40
  // idempotent, re-checked-under-lock build; every other process blocks until that release, then finds
41
41
  // the sentinels already satisfied and returns immediately. In the steady state (dist/ already built)
42
42
  // this is a single existsSync check with no lock contention at all.
43
+ const AGENT_CORE_SENTINEL = join(REPO_ROOT, "packages/agent-core/dist/esm/repo-root/index.js");
44
+ const AGENT_CONFIG_OXLINT_SENTINEL = join(REPO_ROOT, "packages/agent-config/dist/esm/oxlint/import-hygiene.js");
45
+ function buildWorkspacePackage(filter) {
46
+ execFileSync("pnpm", ["--filter", filter, "run", "build"], {
47
+ cwd: REPO_ROOT,
48
+ encoding: "utf8",
49
+ env: { ...process.env, HUSKY: "0" },
50
+ stdio: ["ignore", "pipe", "pipe"],
51
+ });
52
+ }
53
+ const VENDORED_OXLINT_PLUGIN = join(REPO_ROOT, "dist/esm/config/oxlint/import-hygiene.js");
54
+ /** agent-config oxlint SSOT is imported at build/test time (rules map + vendor source). */
55
+ function ensureAgentConfigBuiltSync() {
56
+ if (!existsSync(AGENT_CORE_SENTINEL)) {
57
+ buildWorkspacePackage("@webpresso/agent-core");
58
+ }
59
+ if (!existsSync(AGENT_CONFIG_OXLINT_SENTINEL)) {
60
+ buildWorkspacePackage("@webpresso/agent-config");
61
+ }
62
+ }
43
63
  async function ensureDistPrepared() {
44
- // Fast path: nothing to build. Only pay the lock when a build artifact is actually missing.
45
- if (existsSync(DIST_SENTINEL) && existsSync(MIGRATION_SENTINEL))
64
+ // Fast path: agent-kit dist + migration assets + vendored inject plugins.
65
+ if (existsSync(DIST_SENTINEL) &&
66
+ existsSync(MIGRATION_SENTINEL) &&
67
+ existsSync(VENDORED_OXLINT_PLUGIN) &&
68
+ existsSync(AGENT_CONFIG_OXLINT_SENTINEL)) {
46
69
  return;
70
+ }
47
71
  await withDistBuildLock(async () => {
48
72
  let builtDist = false;
49
73
  // Re-check under the lock: another run may have completed the build while we waited.
74
+ ensureAgentConfigBuiltSync();
50
75
  if (!existsSync(DIST_SENTINEL)) {
51
76
  execFileSync("./node_modules/.bin/tshy", [], {
52
77
  cwd: REPO_ROOT,
@@ -73,7 +98,17 @@ async function ensureDistPrepared() {
73
98
  env: { ...process.env, HUSKY: "0" },
74
99
  });
75
100
  }
76
- if (builtDist) {
101
+ if (builtDist || !existsSync(VENDORED_OXLINT_PLUGIN)) {
102
+ execFileSync("bun", ["src/build/generate-oxlintrc.ts"], {
103
+ cwd: REPO_ROOT,
104
+ encoding: "utf8",
105
+ env: { ...process.env, HUSKY: "0" },
106
+ });
107
+ execFileSync("bun", ["src/build/vendor-oxlint-plugins.ts"], {
108
+ cwd: REPO_ROOT,
109
+ encoding: "utf8",
110
+ env: { ...process.env, HUSKY: "0" },
111
+ });
77
112
  execFileSync("bun", ["src/build/normalize-tsconfig-json-exports.ts"], {
78
113
  cwd: REPO_ROOT,
79
114
  encoding: "utf8",
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Managed RTK binary — single ownership for install + resolve + capability probe.
3
+ *
4
+ * Consumers never brew/cargo install rtk. Setup downloads a pinned upstream
5
+ * release into the Webpresso cache and every call site uses that absolute path.
6
+ * Filters stay upstream (Apache-2.0); we only own packaging + path resolution.
7
+ */
8
+ /** Pinned upstream token-killer release (https://github.com/rtk-ai/rtk). */
9
+ export declare const RTK_MANAGED_VERSION = "0.43.0";
10
+ export type ManagedRtkEnsureResult = {
11
+ readonly ok: true;
12
+ readonly path: string;
13
+ readonly installed: boolean;
14
+ } | {
15
+ readonly ok: false;
16
+ readonly error: string;
17
+ };
18
+ export type ManagedRtkDownloader = (url: string, destFile: string) => void;
19
+ /** Test seam. */
20
+ export declare function resetManagedRtkCacheForTests(): void;
21
+ export declare function managedRtkAssetName(platform?: NodeJS.Platform, arch?: string): string | null;
22
+ export declare function managedRtkDownloadUrl(version?: string): string | null;
23
+ /** Cache dir: `envPaths("webpresso").cache/tools/rtk/<version>/`. */
24
+ export declare function managedRtkInstallDir(version?: string): string;
25
+ export declare function managedRtkExecutablePath(version?: string): string;
26
+ /**
27
+ * Resolve the managed executable if present on disk (no network, no probe).
28
+ * Prefers explicit WP_RTK_BIN, then managed cache install.
29
+ */
30
+ export declare function resolveManagedRtkExecutable(): string | null;
31
+ /** Capability probe against a concrete binary (or managed path). Fail-open. */
32
+ export declare function probeRtkCapabilityAt(executable: string): boolean;
33
+ /** Process-cached: managed binary exists and passes capability probe. */
34
+ export declare function isManagedRtkAvailable(): boolean;
35
+ /**
36
+ * Ensure the pinned managed rtk binary is on disk. Downloads once into the
37
+ * Webpresso cache. No brew/cargo. Idempotent.
38
+ */
39
+ export declare function ensureManagedRtk(options?: {
40
+ readonly downloader?: ManagedRtkDownloader;
41
+ readonly version?: string;
42
+ }): ManagedRtkEnsureResult;
43
+ /** Write a tiny marker file for diagnostics (optional). */
44
+ export declare function writeManagedRtkVersionMarker(path: string, version: string): void;
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Managed RTK binary — single ownership for install + resolve + capability probe.
3
+ *
4
+ * Consumers never brew/cargo install rtk. Setup downloads a pinned upstream
5
+ * release into the Webpresso cache and every call site uses that absolute path.
6
+ * Filters stay upstream (Apache-2.0); we only own packaging + path resolution.
7
+ */
8
+ import { spawnSync } from "node:child_process";
9
+ import { randomBytes } from "node:crypto";
10
+ import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, renameSync, rmSync, writeFileSync, } from "node:fs";
11
+ import { tmpdir } from "node:os";
12
+ import { basename, dirname, join } from "node:path";
13
+ import envPaths from "env-paths";
14
+ /** Pinned upstream token-killer release (https://github.com/rtk-ai/rtk). */
15
+ export const RTK_MANAGED_VERSION = "0.43.0";
16
+ const RTK_PROBE_TIMEOUT_MS = 3000;
17
+ const RTK_CAPABILITY_ARGS = ["gain", "--help"];
18
+ const BINARY_NAME = process.platform === "win32" ? "rtk.exe" : "rtk";
19
+ let cachedExecutable;
20
+ let cachedCapability;
21
+ /** Test seam. */
22
+ export function resetManagedRtkCacheForTests() {
23
+ cachedExecutable = undefined;
24
+ cachedCapability = undefined;
25
+ }
26
+ export function managedRtkAssetName(platform = process.platform, arch = process.arch) {
27
+ if (platform === "darwin" && arch === "arm64")
28
+ return "rtk-aarch64-apple-darwin.tar.gz";
29
+ if (platform === "darwin" && arch === "x64")
30
+ return "rtk-x86_64-apple-darwin.tar.gz";
31
+ if (platform === "linux" && arch === "arm64")
32
+ return "rtk-aarch64-unknown-linux-gnu.tar.gz";
33
+ if (platform === "linux" && arch === "x64")
34
+ return "rtk-x86_64-unknown-linux-musl.tar.gz";
35
+ if (platform === "win32" && arch === "x64")
36
+ return "rtk-x86_64-pc-windows-msvc.zip";
37
+ return null;
38
+ }
39
+ export function managedRtkDownloadUrl(version = RTK_MANAGED_VERSION) {
40
+ const asset = managedRtkAssetName();
41
+ if (!asset)
42
+ return null;
43
+ return `https://github.com/rtk-ai/rtk/releases/download/v${version}/${asset}`;
44
+ }
45
+ /** Cache dir: `envPaths("webpresso").cache/tools/rtk/<version>/`. */
46
+ export function managedRtkInstallDir(version = RTK_MANAGED_VERSION) {
47
+ const cacheRoot = process.env.WP_RTK_CACHE_ROOT ?? envPaths("webpresso", { suffix: "" }).cache;
48
+ return join(cacheRoot, "tools", "rtk", version);
49
+ }
50
+ export function managedRtkExecutablePath(version = RTK_MANAGED_VERSION) {
51
+ return join(managedRtkInstallDir(version), BINARY_NAME);
52
+ }
53
+ /**
54
+ * Resolve the managed executable if present on disk (no network, no probe).
55
+ * Prefers explicit WP_RTK_BIN, then managed cache install.
56
+ */
57
+ export function resolveManagedRtkExecutable() {
58
+ // Skip is process-env policy for this call — do not poison the path cache.
59
+ if (process.env.WP_SKIP_RTK === "1")
60
+ return null;
61
+ if (cachedExecutable !== undefined)
62
+ return cachedExecutable;
63
+ const override = process.env.WP_RTK_BIN?.trim();
64
+ if (override && existsSync(override)) {
65
+ cachedExecutable = override;
66
+ return cachedExecutable;
67
+ }
68
+ const managed = managedRtkExecutablePath();
69
+ if (existsSync(managed)) {
70
+ cachedExecutable = managed;
71
+ return cachedExecutable;
72
+ }
73
+ cachedExecutable = null;
74
+ return null;
75
+ }
76
+ /** Capability probe against a concrete binary (or managed path). Fail-open. */
77
+ export function probeRtkCapabilityAt(executable) {
78
+ try {
79
+ const result = spawnSync(executable, [...RTK_CAPABILITY_ARGS], {
80
+ encoding: "utf8",
81
+ timeout: RTK_PROBE_TIMEOUT_MS,
82
+ stdio: ["ignore", "pipe", "pipe"],
83
+ });
84
+ return result.error == null && result.status === 0;
85
+ }
86
+ catch {
87
+ return false;
88
+ }
89
+ }
90
+ /** Process-cached: managed binary exists and passes capability probe. */
91
+ export function isManagedRtkAvailable() {
92
+ if (cachedCapability !== undefined)
93
+ return cachedCapability;
94
+ if (process.env.WP_SKIP_RTK === "1") {
95
+ cachedCapability = false;
96
+ return false;
97
+ }
98
+ const path = resolveManagedRtkExecutable();
99
+ if (!path) {
100
+ cachedCapability = false;
101
+ return false;
102
+ }
103
+ cachedCapability = probeRtkCapabilityAt(path);
104
+ return cachedCapability;
105
+ }
106
+ function defaultDownloader(url, destFile) {
107
+ // Prefer curl when present (HTTP/2, redirects); fall back to node fetch via spawn node -e is avoided.
108
+ const curl = spawnSync("curl", ["-fsSL", "--retry", "2", "--retry-delay", "1", "-o", destFile, url], { encoding: "utf8", timeout: 120_000 });
109
+ if (curl.error || curl.status !== 0) {
110
+ throw new Error(`download failed (${curl.status ?? "error"}): ${String(curl.stderr ?? curl.error ?? "").trim() || url}`);
111
+ }
112
+ }
113
+ function isRegularFile(path) {
114
+ try {
115
+ return lstatSync(path).isFile();
116
+ }
117
+ catch {
118
+ return false;
119
+ }
120
+ }
121
+ /** Prefer a real file named rtk / rtk.exe — ignore directories of the same name. */
122
+ function findExtractedBinary(root) {
123
+ const stack = [root];
124
+ while (stack.length > 0) {
125
+ const dir = stack.pop();
126
+ let entries;
127
+ try {
128
+ entries = readdirSync(dir);
129
+ }
130
+ catch {
131
+ continue;
132
+ }
133
+ for (const name of entries) {
134
+ const full = join(dir, name);
135
+ if ((name === BINARY_NAME || name === "rtk") && isRegularFile(full)) {
136
+ return full;
137
+ }
138
+ try {
139
+ // shallow walk only one extra level for nested release layouts
140
+ const nested = readdirSync(full);
141
+ for (const child of nested) {
142
+ const nestedFull = join(full, child);
143
+ if ((child === BINARY_NAME || child === "rtk") && isRegularFile(nestedFull)) {
144
+ return nestedFull;
145
+ }
146
+ }
147
+ }
148
+ catch {
149
+ // not a dir
150
+ }
151
+ }
152
+ }
153
+ return null;
154
+ }
155
+ function extractArchive(archivePath, extractDir) {
156
+ mkdirSync(extractDir, { recursive: true });
157
+ if (archivePath.endsWith(".zip")) {
158
+ const unzip = spawnSync("unzip", ["-qo", archivePath, "-d", extractDir], {
159
+ encoding: "utf8",
160
+ timeout: 60_000,
161
+ });
162
+ if (unzip.status !== 0) {
163
+ throw new Error(`unzip failed: ${String(unzip.stderr ?? "").trim()}`);
164
+ }
165
+ return;
166
+ }
167
+ const tar = spawnSync("tar", ["-xzf", archivePath, "-C", extractDir], {
168
+ encoding: "utf8",
169
+ timeout: 60_000,
170
+ });
171
+ if (tar.status !== 0) {
172
+ throw new Error(`tar extract failed: ${String(tar.stderr ?? "").trim()}`);
173
+ }
174
+ }
175
+ /**
176
+ * Ensure the pinned managed rtk binary is on disk. Downloads once into the
177
+ * Webpresso cache. No brew/cargo. Idempotent.
178
+ */
179
+ export function ensureManagedRtk(options = {}) {
180
+ if (process.env.WP_SKIP_RTK === "1") {
181
+ return { ok: false, error: "WP_SKIP_RTK=1" };
182
+ }
183
+ const version = options.version ?? RTK_MANAGED_VERSION;
184
+ const dest = managedRtkExecutablePath(version);
185
+ if (existsSync(dest) && probeRtkCapabilityAt(dest)) {
186
+ resetManagedRtkCacheForTests();
187
+ cachedExecutable = dest;
188
+ cachedCapability = true;
189
+ return { ok: true, path: dest, installed: false };
190
+ }
191
+ const url = managedRtkDownloadUrl(version);
192
+ if (!url) {
193
+ return {
194
+ ok: false,
195
+ error: `no managed rtk asset for ${process.platform}/${process.arch}`,
196
+ };
197
+ }
198
+ const installDir = managedRtkInstallDir(version);
199
+ mkdirSync(installDir, { recursive: true });
200
+ const work = mkdtempSync(join(tmpdir(), "wp-rtk-"));
201
+ try {
202
+ const archiveName = basename(url);
203
+ const archivePath = join(work, archiveName);
204
+ const download = options.downloader ?? defaultDownloader;
205
+ download(url, archivePath);
206
+ const extractDir = join(work, "extract");
207
+ extractArchive(archivePath, extractDir);
208
+ const found = findExtractedBinary(extractDir);
209
+ if (!found) {
210
+ return { ok: false, error: `archive did not contain ${BINARY_NAME}` };
211
+ }
212
+ mkdirSync(dirname(dest), { recursive: true });
213
+ // Stage under a unique name so concurrent setup never clobbers a live dest
214
+ // mid-probe, and we only delete our own staged file on failure.
215
+ const staged = join(installDir, `.rtk-staging-${process.pid}-${Date.now()}-${randomBytes(6).toString("hex")}`);
216
+ try {
217
+ try {
218
+ renameSync(found, staged);
219
+ }
220
+ catch {
221
+ const cp = spawnSync("cp", [found, staged], { encoding: "utf8" });
222
+ if (cp.status !== 0) {
223
+ return { ok: false, error: `failed to stage managed rtk at ${staged}` };
224
+ }
225
+ }
226
+ if (process.platform !== "win32") {
227
+ chmodSync(staged, 0o755);
228
+ }
229
+ if (!probeRtkCapabilityAt(staged)) {
230
+ return { ok: false, error: "downloaded rtk failed capability probe (rtk gain --help)" };
231
+ }
232
+ try {
233
+ renameSync(staged, dest);
234
+ }
235
+ catch {
236
+ // Cross-device rename: replace dest via copy then drop stage.
237
+ const cp = spawnSync("cp", [staged, dest], { encoding: "utf8" });
238
+ if (cp.status !== 0) {
239
+ return { ok: false, error: `failed to place managed rtk at ${dest}` };
240
+ }
241
+ if (process.platform !== "win32") {
242
+ chmodSync(dest, 0o755);
243
+ }
244
+ }
245
+ }
246
+ finally {
247
+ try {
248
+ rmSync(staged, { force: true });
249
+ }
250
+ catch {
251
+ // ignore
252
+ }
253
+ }
254
+ resetManagedRtkCacheForTests();
255
+ cachedExecutable = dest;
256
+ cachedCapability = true;
257
+ return { ok: true, path: dest, installed: true };
258
+ }
259
+ catch (error) {
260
+ const message = error instanceof Error ? error.message : String(error);
261
+ return { ok: false, error: message };
262
+ }
263
+ finally {
264
+ try {
265
+ rmSync(work, { recursive: true, force: true });
266
+ }
267
+ catch {
268
+ // ignore
269
+ }
270
+ }
271
+ }
272
+ /** Write a tiny marker file for diagnostics (optional). */
273
+ export function writeManagedRtkVersionMarker(path, version) {
274
+ writeFileSync(`${path}.version`, `${version}\n`, "utf8");
275
+ }