@gfdelarue/testkit 0.6.5 → 0.6.7

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 (134) hide show
  1. package/README.md +134 -0
  2. package/dist/app/bugs/ci-report.js +2 -2
  3. package/dist/app/campaigns/brief.js +1 -1
  4. package/dist/app/cleanup/index.js +1 -1
  5. package/dist/app/doctor/index.js +1 -1
  6. package/dist/app/environments/static-values.js +1 -1
  7. package/dist/app/environments/supplied.js +1 -1
  8. package/dist/app/environments/task-databases.js +2 -2
  9. package/dist/app/evidence/ci-status.js +1 -0
  10. package/dist/app/evidence/ci.js +1 -0
  11. package/dist/app/evidence/index.js +1 -0
  12. package/dist/app/evidence/inputs.js +1 -0
  13. package/dist/app/evidence/report.js +1 -0
  14. package/dist/app/lint/index.js +1 -1
  15. package/dist/app/run/engine-evidence-persistence.js +1 -1
  16. package/dist/app/run/engine-execution-failures.js +1 -1
  17. package/dist/app/run/engine-execution.js +1 -1
  18. package/dist/app/run/operation-progress.js +1 -1
  19. package/dist/app/run/orchestrator.js +1 -1
  20. package/dist/app/run/results.js +1 -1
  21. package/dist/app/run/run-events.js +1 -1
  22. package/dist/app/run/run-finalization.js +1 -1
  23. package/dist/app/run/run-guards.js +3 -3
  24. package/dist/app/run/scheduler/estimates.js +1 -1
  25. package/dist/app/run/scheduler/index.js +1 -1
  26. package/dist/app/run/task-batches.js +1 -1
  27. package/dist/app/run/worker-loop.js +1 -1
  28. package/dist/app/status/runs.js +1 -1
  29. package/dist/app/typecheck/index.js +4 -4
  30. package/dist/cli/command-catalog.js +1 -1
  31. package/dist/cli/command-registry.js +1 -1
  32. package/dist/cli/commands/evidence.js +2 -0
  33. package/dist/cli/renderers/destroy/text.js +1 -1
  34. package/dist/cli/renderers/evidence/text.js +1 -0
  35. package/dist/cli/renderers/run/failure.js +1 -1
  36. package/dist/cli/renderers/run/status.js +1 -1
  37. package/dist/cli/renderers/run/text-reporter.js +23 -23
  38. package/dist/config/loader/decode-discovery.js +1 -1
  39. package/dist/config/loader/discover-tests.js +1 -1
  40. package/dist/config/loader/discovery-result.js +1 -1
  41. package/dist/config/loader/discovery.js +2 -2
  42. package/dist/config/loader/env.js +1 -1
  43. package/dist/config/loader/file-metadata.js +1 -1
  44. package/dist/config/loader/groups.js +2 -2
  45. package/dist/config/loader/index.js +1 -1
  46. package/dist/config/loader/load.js +1 -1
  47. package/dist/config/loader/path-policy.js +1 -1
  48. package/dist/config/loader/runtime-isolation.js +1 -1
  49. package/dist/config/loader/runtime.js +1 -1
  50. package/dist/config/loader/test-selection.js +1 -1
  51. package/dist/config/loader/validation.js +1 -1
  52. package/dist/database/admin.js +4 -4
  53. package/dist/database/cleanup.js +1 -1
  54. package/dist/database/configured-steps.js +1 -1
  55. package/dist/database/docker.js +3 -5
  56. package/dist/database/index.js +2 -2
  57. package/dist/database/local-postgres.js +3 -4
  58. package/dist/database/postgres/index.js +2 -2
  59. package/dist/database/progress.js +1 -1
  60. package/dist/database/roles.js +2 -2
  61. package/dist/database/runtime-config.js +1 -1
  62. package/dist/database/schema-source.js +3 -3
  63. package/dist/database/source-url.js +1 -1
  64. package/dist/database/template-steps.js +3 -3
  65. package/dist/domain/operation-reason.js +1 -1
  66. package/dist/domain/suite-selection.js +1 -1
  67. package/dist/engines/adapters/evidence-files.js +1 -1
  68. package/dist/engines/adapters/evidence-identity.js +1 -1
  69. package/dist/engines/adapters/execution.js +1 -1
  70. package/dist/engines/adapters/node-startup-cache.js +1 -1
  71. package/dist/engines/adapters/workspace.js +1 -1
  72. package/dist/engines/contracts/fixture-manifest.js +1 -1
  73. package/dist/engines/installations/package-resolution.js +1 -1
  74. package/dist/engines/managed-module-resolution.js +2 -2
  75. package/dist/engines/node-vitest/adapter/adapter.js +1 -1
  76. package/dist/engines/node-vitest/adapter/evidence.js +2 -2
  77. package/dist/engines/node-vitest/runtime/data/report.js +3 -3
  78. package/dist/engines/node-vitest/runtime/data/scenario.js +2 -2
  79. package/dist/engines/node-vitest/selection.js +1 -1
  80. package/dist/engines/playwright/adapter/adapter.js +1 -1
  81. package/dist/engines/playwright/adapter/config.js +3 -3
  82. package/dist/engines/playwright/adapter/index.js +1 -1
  83. package/dist/environment/lifecycle-controller.js +1 -1
  84. package/dist/environment/template.js +1 -1
  85. package/dist/evidence/artifacts.js +3 -3
  86. package/dist/evidence/bundles.js +2 -2
  87. package/dist/evidence/environment-record.js +1 -1
  88. package/dist/evidence/index.js +1 -1
  89. package/dist/evidence/logs.js +2 -2
  90. package/dist/evidence/operation-timings.js +1 -1
  91. package/dist/evidence/reading.js +1 -1
  92. package/dist/evidence/reporting.js +1 -1
  93. package/dist/evidence/run-evidence.js +1 -1
  94. package/dist/evidence/run-inputs.js +1 -0
  95. package/dist/evidence/run-model.js +1 -1
  96. package/dist/evidence/setup-progress.js +1 -1
  97. package/dist/evidence/storage-inventory.js +1 -1
  98. package/dist/evidence/storage.js +2 -2
  99. package/dist/process/capture-output.js +1 -1
  100. package/dist/process/execa-bundle.json +3 -3
  101. package/dist/process/host/platform.js +1 -1
  102. package/dist/process/locks.js +4 -4
  103. package/dist/process/process-group.js +1 -1
  104. package/dist/process/process.js +1 -1
  105. package/dist/process/ready-deadline.js +1 -1
  106. package/dist/process/runs/manifest.js +1 -1
  107. package/dist/process/runs/stop.js +1 -1
  108. package/dist/process/windows-native.js +1 -1
  109. package/dist/repo/content.js +4 -0
  110. package/dist/repo/fingerprint-policy.js +1 -1
  111. package/dist/repo/managed-git.js +1 -1
  112. package/dist/repo/metadata.js +1 -1
  113. package/dist/repo/state.js +4 -4
  114. package/dist/steps/configured-step-testkit-modules.js +1 -1
  115. package/dist/steps/configured-steps.js +1 -1
  116. package/dist/steps/environment.js +1 -1
  117. package/dist/steps/index.js +1 -1
  118. package/dist/steps/run.js +2 -2
  119. package/dist/toolchains/esbuild.js +1 -1
  120. package/dist/toolchains/index.js +3 -3
  121. package/dist/toolchains/node-integrity.js +1 -1
  122. package/dist/toolchains/node-provisioning.js +2 -2
  123. package/dist/toolchains/semver.js +1 -1
  124. package/dist/tracker/gitea.js +1 -1
  125. package/dist/tracker/github.js +1 -1
  126. package/dist/tracker/requests.js +1 -1
  127. package/dist/tracker/responses.js +1 -1
  128. package/docs/bug-records.md +18 -2
  129. package/docs/release-notes.md +36 -0
  130. package/package.json +1 -1
  131. package/dist/app/run/planning-types.js +0 -1
  132. package/dist/app/run/selection.js +0 -1
  133. package/dist/config/authoring/http-methods.js +0 -1
  134. package/dist/evidence/artifact-validation.js +0 -1
@@ -1,5 +1,5 @@
1
- import C from"crypto";import p from"fs";import c from"path";import{appendFingerprintPathToHash as et,hasIncludedFingerprintDescendantAtPrefix as rt,normalizeFingerprintPolicy as _,shouldIgnoreCanonicalFingerprintPathAtPrefix as N,normalizeCanonicalRelativePath as k}from"./fingerprint-policy.js";import{decodeGitPathRecord as $}from"./git-path.js";import{runManagedGitCommand as nt,gitCommandLabel as D,REPOSITORY_COMMAND_GUARD_ARGS as ot}from"./managed-git.js";import{physicalNodeMode as W,readPhysicalNode as F,sameIdentityRecord as it,compareStrings as y}from"./physical-tree.js";import{normalizeManagedHostEnvironment as at,requireManagedHostCommandSuccess as j,resolveManagedHostExecutableIfAvailable as st}from"../process/index.js";import{readCiPullRequestHeadSha as ct,resolveCiGitReference as z}from"./ci-environment.js";import{parseGitRepository as lt}from"./remote.js";import{hasErrorCode as v}from"../kernel/error-serialization.js";export async function collectRepoState(t,e={}){const r=_(e.fingerprints),o=e.env??process.env,n=z(o);return await B(t,r,o,n)??ut(t,r,n)}export async function collectGitRepoState(t,e={}){const r=_(e.fingerprints),o=e.env??process.env;return B(t,r,o,z(o))}async function B(t,e,r,o){const n=ct(r),i=st("git",c.resolve(t),at(process.env));if(!i&&!Ot(t)||i&&jt(t))return null;const a=await Y(t);if(a===null)return null;const s=K(a,t),u=await R(s.repoRoot,["rev-parse","--verify","--quiet","HEAD"]),m=await dt(s.repoRoot,u,n),f=await R(s.repoRoot,["symbolic-ref","--quiet","--short","HEAD"]),b=await R(s.repoRoot,["config","--get","remote.origin.url"]),P=u!==null&&f===null,h=await pt(s,e,u,""),G=!!h,d=u||"unborn";return{kind:G?"dirty":"commit",repoRoot:s.repoRoot,worktreePath:s.repoRoot,branch:f??(P?o.branch:null),detached:P,commitSha:u,pullRequest:o.pullRequest,pullRequestHeadSha:m,remoteUrl:b,repository:lt(b),dirty:G,dirtyFingerprint:h,contentFingerprint:null,cacheKey:h?`dirty/${d}-${h}`:`commits/${d}`}}function ut(t,e,r){const o=p.realpathSync.native(c.resolve(t)),n=Q(o,e);if(Q(o,e)!==n)throw new Error("Physical repository snapshot changed while collecting repository state");const a=n.slice(0,24);return{kind:"nogit",repoRoot:null,worktreePath:o,branch:null,detached:!1,commitSha:null,pullRequest:r.pullRequest,pullRequestHeadSha:null,remoteUrl:null,repository:null,dirty:!1,dirtyFingerprint:null,contentFingerprint:a,cacheKey:`nogit/${a}`}}async function Y(t){const e=["rev-parse","--show-toplevel"],r=await L(t,e);if(r.exitCode===0){const o=r.stdout.trim();if(o.length>0)return o;throw new Error("Git rev-parse returned an empty repository root")}if(Ft(r))return null;throw j(D(e),r),new Error("Unreachable Git repository probe result")}function K(t,e){const r=p.realpathSync.native(c.resolve(t)),o=p.realpathSync.native(c.resolve(e)),n=Dt(c.relative(r,o));if(n===".."||n.startsWith("../")||c.isAbsolute(n)||process.platform==="win32"&&c.win32.isAbsolute(n))throw new Error(`Product directory must be inside its Git worktree: ${o}`);return{productDir:o,productPathspec:n?$t(n):".",productRootRelativePath:n,repoRoot:r}}export function summarizeRepoStateForMetadata(t){return t?{kind:t.kind,cacheKey:t.cacheKey,branch:t.branch,detached:t.detached,commitSha:t.commitSha,dirty:t.dirty,dirtyFingerprint:t.dirtyFingerprint,contentFingerprint:t.contentFingerprint,pullRequest:t.pullRequest,pullRequestHeadSha:t.pullRequestHeadSha,repoRoot:t.repoRoot,repository:t.repository,worktreePath:t.worktreePath,remoteUrl:t.remoteUrl}:null}async function dt(t,e,r){if(e===null||r===null)return null;if(e===r)return r;const o=await w(t,["cat-file","commit","HEAD"]),n=[];for(const i of o.split(`
2
- `)){if(i.length===0)break;i.startsWith("parent ")&&n.push(i.slice(7))}return n.length>=2&&n.includes(r)?r:null}async function pt(t,e,r,o){const n=await O(t,e,r,o),i=await O(t,e,r,o);if(n!==i)throw new Error("Git HEAD, index, untracked inventory, submodule state, or physical snapshot changed while collecting repository state");return n?.slice(0,24)??null}async function O(t,e,r,o){await X(t.repoRoot,r);const n=C.createHash("sha256");let i=!1;const a=await Rt(t.repoRoot),[s,u]=await Promise.all([V(t,a),ht(t,r,a)]),m=bt(s),f=Pt(u),b=[...new Set([...m.keys(),...f.keys()])].sort(y);for(const d of b){const g=m.get(d)??[],E=f.get(d)??[],M=g.some(x=>x.mode==="160000")||E.some(x=>x.mode==="160000");if(N(d,o,e)&&(!M||!rt(d,o,e)))continue;const tt=g.length>0?g[0]:E[0],H=F(t.productDir,d),U=M?await mt(t,tt,H,e,o):null;(!yt(g,E)||!gt(g,H,U,a))&&(i=!0,vt(n,d,E,g,H,U,a))}const P=await w(t.repoRoot,["ls-files","--others","--exclude-per-directory=.gitignore","--",t.productPathspec]),h=Ct(t,T(P,"Git ls-files").map(d=>$(d))).filter(d=>!N(d.productRelativePath,o,e));if(h.length>0){i=!0,n.update("untracked-state-v2\0");for(const d of h)l(n,"path",d.productRelativePath),q(n,F(t.productDir,d.productRelativePath),a)}const G=await V(t,a);if(!kt(s,G))throw new Error("Git index changed while collecting repository state");return await X(t.repoRoot,r),i?n.digest("hex"):null}async function V(t,e){const r=await w(t.repoRoot,["ls-files","--stage","--abbrev=64","--",t.productPathspec]);return ft(t,r,e)}function ft(t,e,r){const o=[];for(const n of T(e,"Git ls-files")){const i=n.indexOf(" ");if(i<0)throw new Error("Git ls-files returned malformed stage metadata");const a=n.slice(0,i).split(" "),s=k($(n.slice(i+1)));if(a.length!==3||!/^[0-7]{6}$/u.test(a[0])||!J(a[1],r)||!/^[0-3]$/u.test(a[2]))throw new Error("Git ls-files returned invalid stage metadata");const u=A(t,s);if(!u)throw new Error(`Git ls-files returned a path outside the product: ${s}`);o.push({mode:a[0],objectId:a[1],productRelativePath:u,rootRelativePath:s,stage:Number(a[2])})}return o.sort(xt)}async function ht(t,e,r){if(e===null)return[];const o=await w(t.repoRoot,["ls-tree","-r","--full-tree",e,"--",t.productPathspec]);return wt(t,o,r)}async function mt(t,e,r,o,n){if(r.kind==="missing"||r.kind==="ancestor-missing")return{kind:"missing",node:r};if(r.kind!=="directory")return{kind:"non-directory",node:r};const i=It(t,e),a=await Y(i);if(a===null||!Ht(a,i))throw new Error(`Gitlink is not initialized as a repository: ${e.productRelativePath}`);const s=K(a,i),u=await R(s.repoRoot,["rev-parse","--verify","--quiet","HEAD"]),m=await O(s,o,u,St(n,e.productRelativePath)),f=F(t.productDir,e.productRelativePath);if(f.kind!=="directory"||!it(f.identity,r.identity))throw new Error(`Gitlink path changed while collecting repository state: ${e.productRelativePath}`);return{kind:"initialized",dirtyFingerprint:m,head:u,identity:f.identity}}function gt(t,e,r,o){if(t.length!==1||t[0]?.stage!==0)return!1;const n=t[0];return n.mode==="160000"?r?.kind==="initialized"&&r.head===n.objectId&&r.dirtyFingerprint===null:n.mode==="120000"?e.kind==="symlink"&&S(o,e.target)===n.objectId:n.mode==="100644"||n.mode==="100755"?e.kind==="file"&&Et(e.mode,n.mode)===n.mode&&S(o,e.contents)===n.objectId:!1}function yt(t,e){return e.length===0&&t.length===0?!0:e.length!==1||t.length!==1||t[0]?.stage!==0?!1:e[0]?.mode===t[0]?.mode&&e[0]?.objectId===t[0]?.objectId}function vt(t,e,r,o,n,i,a){t.update("tracked-path-state-v2\0"),l(t,"path",e),r.length===0&&t.update("head-missing\0");for(const s of r)t.update("head-entry\0"),l(t,"mode",s.mode),l(t,"object",s.objectId);o.length===0&&t.update("index-missing\0");for(const s of o)t.update("index-entry\0"),l(t,"mode",s.mode),l(t,"object",s.objectId),l(t,"stage",s.stage.toString());i?Gt(t,i,a):q(t,n,a)}function wt(t,e,r){const o=[];for(const n of T(e,"Git ls-tree")){const i=n.indexOf(" "),a=n.slice(0,i).split(" ");if(i<0||a.length!==3||!/^[0-7]{6}$/u.test(a[0])||!/^(?:blob|commit)$/u.test(a[1])||!J(a[2],r))throw new Error("Git ls-tree returned invalid tree metadata");const s=k($(n.slice(i+1))),u=A(t,s);if(!u)throw new Error(`Git ls-tree returned a path outside the product: ${s}`);o.push({mode:a[0],objectId:a[2],productRelativePath:u,rootRelativePath:s})}return o.sort((n,i)=>y(n.productRelativePath,i.productRelativePath))}async function Rt(t){const e=(await w(t,["rev-parse","--show-object-format"])).trim();if(e==="sha1"||e==="sha256")return e;throw new Error(`Unsupported Git object format: ${e||"empty"}`)}function bt(t){const e=new Map;for(const r of t){const o=e.get(r.productRelativePath);o?o.push(r):e.set(r.productRelativePath,[r])}return e}function Pt(t){const e=new Map;for(const r of t){const o=e.get(r.productRelativePath);o?o.push(r):e.set(r.productRelativePath,[r])}return e}function Gt(t,e,r){if(t.update("gitlink-worktree-state-v2\0"),l(t,"kind",e.kind),e.kind==="initialized"){e.head===null?t.update("head-missing\0"):l(t,"head",e.head),e.dirtyFingerprint===null?t.update("nested-clean\0"):l(t,"nested-dirty",e.dirtyFingerprint);return}q(t,e.node,r)}function q(t,e,r){switch(t.update("physical-node-v2\0"),l(t,"kind",e.kind),e.kind){case"file":l(t,"mode",process.platform==="win32"?"platform-mode-ignored":e.mode&73n?"100755":"100644"),l(t,"blob",S(r,e.contents)),I(t,e.contents);break;case"symlink":l(t,"blob",S(r,e.target)),I(t,e.target);break;case"directory":l(t,"mode",W(e.mode));break;case"ancestor-symlink":l(t,"obstacle",e.obstaclePath),I(t,e.target);break;case"ancestor-non-directory":l(t,"obstacle",e.obstaclePath),l(t,"mode",W(e.mode));break;case"ancestor-missing":l(t,"obstacle",e.obstaclePath);break;case"missing":break}}function Et(t,e){return process.platform==="win32"?e:t&73n?"100755":"100644"}function S(t,e){return C.createHash(t).update(`blob ${e.byteLength.toString()}\0`).update(e).digest("hex")}function kt(t,e){return t.length===e.length&&t.every((r,o)=>{const n=e[o];return r.mode===n.mode&&r.objectId===n.objectId&&r.productRelativePath===n.productRelativePath&&r.rootRelativePath===n.rootRelativePath&&r.stage===n.stage})}function J(t,e){const r=e==="sha1"?40:64;return t.length===r&&/^[0-9a-f]+$/u.test(t)}function St(t,e){return t?e?`${t}/${e}`:t:e}function It(t,e){const r=e.productRelativePath.split("/");if(r.some(i=>i.length===0||i==="."||i===".."))throw new Error(`Git returned an invalid product path: ${e.productRelativePath}`);const o=c.resolve(t.productDir,...r),n=c.relative(t.productDir,o);if(n===".."||n.startsWith(`..${c.sep}`)||c.isAbsolute(n))throw new Error(`Git returned a path outside the product: ${e.rootRelativePath}`);return o}function Ht(t,e){const r=p.realpathSync.native(c.resolve(t)),o=p.realpathSync.native(c.resolve(e));return process.platform==="win32"?r.toLocaleLowerCase("en-US")===o.toLocaleLowerCase("en-US"):r===o}function l(t,e,r){t.update(`${e}\0`),I(t,Buffer.from(r))}function I(t,e){t.update(`${e.byteLength.toString()}\0`),t.update(e),t.update("\0")}function xt(t,e){return y(t.productRelativePath,e.productRelativePath)||t.stage-e.stage||y(t.mode,e.mode)||y(t.objectId,e.objectId)}function Ct(t,e){const r=new Map;for(const o of e){const n=k(o);if(n===".."||n.startsWith("../")||c.isAbsolute(n)||process.platform==="win32"&&c.win32.isAbsolute(n))throw new Error(`Git returned a path outside its worktree: ${n}`);const i=A(t,n);i&&r.set(n,{productRelativePath:i,rootRelativePath:n})}return[...r.values()].sort((o,n)=>y(o.productRelativePath,n.productRelativePath))}function A(t,e){if(!e)return null;if(!t.productRootRelativePath)return e;const r=`${t.productRootRelativePath}/`;return(process.platform==="win32"?e.toLocaleLowerCase("en-US").startsWith(r.toLocaleLowerCase("en-US")):e.startsWith(r))?e.slice(r.length):null}function $t(t){return`:(top,literal)${t}`}function Dt(t){return k(t.split(c.sep).join("/"))}function Q(t,e){const r=C.createHash("sha256");return et(r,t,t,e),r.digest("hex")}async function w(t,e){const r=await L(t,e);return j(D(e),r).stdout}async function R(t,e){const r=await L(t,e);if(r.exitCode===0)return r.stdout.trim()||null;if(r.exitCode===1&&r.stdout.trim().length===0&&r.stderr.trim().length===0)return null;throw j(D(e),r),new Error("Unreachable Git command result")}function L(t,e){return nt(t,["--no-replace-objects","--no-pager",...ot,"-c","core.quotePath=true",...e],{repositoryConfigOnly:!0})}async function X(t,e){if(await R(t,["rev-parse","--verify","--quiet","HEAD"])!==e)throw new Error("Git HEAD changed while collecting repository state")}function T(t,e){if(t.length===0)return[];if(!t.endsWith(`
1
+ import x from"crypto";import p from"fs";import c from"path";import{appendFingerprintPathToHash as et,hasIncludedFingerprintDescendantAtPrefix as rt,normalizeFingerprintPolicy as N,shouldIgnoreCanonicalFingerprintPathAtPrefix as U,joinCanonicalPath as nt,normalizeCanonicalRelativePath as E}from"./fingerprint-policy.js";import{decodeGitPathRecord as D}from"./git-path.js";import{runManagedGitCommand as ot,gitCommandLabel as $,REPOSITORY_COMMAND_GUARD_ARGS as it}from"./managed-git.js";import{physicalNodeMode as W,readPhysicalNode as j,sameIdentityRecord as at,compareStrings as y}from"./physical-tree.js";import{normalizeManagedHostEnvironment as st,requireManagedHostCommandSuccess as O,resolveManagedHostExecutableIfAvailable as ct}from"../process/index.js";import{readCiPullRequestHeadSha as lt,resolveCiGitReference as z}from"./ci-environment.js";import{parseGitRepository as ut}from"./remote.js";import{hasErrorCode as v}from"../kernel/error-serialization.js";export async function collectRepoState(t,e={}){return await collectGitRepoState(t,e)??pt(t,e)}export async function collectGitRepoState(t,e={}){const r=N(e.fingerprints),o=e.env??process.env;return dt(t,r,o,z(o))}async function dt(t,e,r,o){const n=lt(r),i=ct("git",c.resolve(t),st(process.env));if(!i&&!Ot(t)||i&&jt(t))return null;const a=await B(t);if(a===null)return null;const s=Y(a,t),u=await R(s.repoRoot,["rev-parse","--verify","--quiet","HEAD"]),m=await ft(s.repoRoot,u,n),f=await R(s.repoRoot,["symbolic-ref","--quiet","--short","HEAD"]),b=await R(s.repoRoot,["config","--get","remote.origin.url"]),P=u!==null&&f===null,h=await ht(s,e,u,""),G=!!h,d=u||"unborn";return{kind:G?"dirty":"commit",cacheKey:h?`dirty/${d}-${h}`:`commits/${d}`,branch:f??(P?o.branch:null),detached:P,commitSha:u,dirty:G,dirtyFingerprint:h,contentFingerprint:null,pullRequest:o.pullRequest,pullRequestHeadSha:m,repoRoot:s.repoRoot,repository:ut(b),worktreePath:s.repoRoot,remoteUrl:b}}function pt(t,e){const r=N(e.fingerprints),o=z(e.env??process.env),n=p.realpathSync.native(c.resolve(t)),i=Q(n,r);if(Q(n,r)!==i)throw new Error("Physical repository snapshot changed while collecting repository state");const s=i.slice(0,24);return{kind:"nogit",cacheKey:`nogit/${s}`,branch:null,detached:!1,commitSha:null,dirty:!1,dirtyFingerprint:null,contentFingerprint:s,pullRequest:o.pullRequest,pullRequestHeadSha:null,repoRoot:null,repository:null,worktreePath:n,remoteUrl:null}}async function B(t){const e=["rev-parse","--show-toplevel"],r=await T(t,e);if(r.exitCode===0){const o=r.stdout.trim();if(o.length>0)return o;throw new Error("Git rev-parse returned an empty repository root")}if($t(r))return null;throw O($(e),r),new Error("Unreachable Git repository probe result")}function Y(t,e){const r=p.realpathSync.native(c.resolve(t)),o=p.realpathSync.native(c.resolve(e)),n=Dt(c.relative(r,o));if(n===".."||n.startsWith("../")||c.isAbsolute(n)||process.platform==="win32"&&c.win32.isAbsolute(n))throw new Error(`Product directory must be inside its Git worktree: ${o}`);return{productDir:o,productPathspec:n?xt(n):".",productRootRelativePath:n,repoRoot:r}}async function ft(t,e,r){if(e===null||r===null)return null;if(e===r)return r;const o=await w(t,["cat-file","commit","HEAD"]),n=[];for(const i of o.split(`
2
+ `)){if(i.length===0)break;i.startsWith("parent ")&&n.push(i.slice(7))}return n.length>=2&&n.includes(r)?r:null}async function ht(t,e,r,o){const n=await A(t,e,r,o),i=await A(t,e,r,o);if(n!==i)throw new Error("Git HEAD, index, untracked inventory, submodule state, or physical snapshot changed while collecting repository state");return n?.slice(0,24)??null}async function A(t,e,r,o){await X(t.repoRoot,r);const n=x.createHash("sha256");let i=!1;const a=await Pt(t.repoRoot),[s,u]=await Promise.all([K(t,a),gt(t,r,a)]),m=V(s),f=V(u),b=[...new Set([...m.keys(),...f.keys()])].sort(y);for(const d of b){const g=m.get(d)??[],S=f.get(d)??[],M=g.some(H=>H.mode==="160000")||S.some(H=>H.mode==="160000");if(U(d,o,e)&&(!M||!rt(d,o,e)))continue;const tt=g.length>0?g[0]:S[0],C=j(t.productDir,d),_=M?await yt(t,tt,C,e,o):null;(!wt(g,S)||!vt(g,C,_,a))&&(i=!0,Rt(n,d,S,g,C,_,a))}const P=await w(t.repoRoot,["ls-files","--others","--exclude-per-directory=.gitignore","--",t.productPathspec]),h=Ht(t,q(P,"Git ls-files").map(d=>D(d))).filter(d=>!U(d.productRelativePath,o,e));if(h.length>0){i=!0,n.update("untracked-state-v2\0");for(const d of h)l(n,"path",d.productRelativePath),L(n,j(t.productDir,d.productRelativePath),a)}const G=await K(t,a);if(!Et(s,G))throw new Error("Git index changed while collecting repository state");return await X(t.repoRoot,r),i?n.digest("hex"):null}async function K(t,e){const r=await w(t.repoRoot,["ls-files","--stage","--abbrev=64","--",t.productPathspec]);return mt(t,r,e)}function mt(t,e,r){const o=[];for(const n of q(e,"Git ls-files")){const i=n.indexOf(" ");if(i<0)throw new Error("Git ls-files returned malformed stage metadata");const a=n.slice(0,i).split(" "),s=E(D(n.slice(i+1)));if(a.length!==3||!/^[0-7]{6}$/u.test(a[0])||!J(a[1],r)||!/^[0-3]$/u.test(a[2]))throw new Error("Git ls-files returned invalid stage metadata");const u=F(t,s);if(!u)throw new Error(`Git ls-files returned a path outside the product: ${s}`);o.push({mode:a[0],objectId:a[1],productRelativePath:u,rootRelativePath:s,stage:Number(a[2])})}return o.sort(Ct)}async function gt(t,e,r){if(e===null)return[];const o=await w(t.repoRoot,["ls-tree","-r","--full-tree",e,"--",t.productPathspec]);return bt(t,o,r)}async function yt(t,e,r,o,n){if(r.kind==="missing"||r.kind==="ancestor-missing")return{kind:"missing",node:r};if(r.kind!=="directory")return{kind:"non-directory",node:r};const i=kt(t,e),a=await B(i);if(a===null||!It(a,i))throw new Error(`Gitlink is not initialized as a repository: ${e.productRelativePath}`);const s=Y(a,i),u=await R(s.repoRoot,["rev-parse","--verify","--quiet","HEAD"]),m=await A(s,o,u,nt(n,e.productRelativePath)),f=j(t.productDir,e.productRelativePath);if(f.kind!=="directory"||!at(f.identity,r.identity))throw new Error(`Gitlink path changed while collecting repository state: ${e.productRelativePath}`);return{kind:"initialized",dirtyFingerprint:m,head:u,identity:f.identity}}function vt(t,e,r,o){if(t.length!==1||t[0]?.stage!==0)return!1;const n=t[0];return n.mode==="160000"?r?.kind==="initialized"&&r.head===n.objectId&&r.dirtyFingerprint===null:n.mode==="120000"?e.kind==="symlink"&&k(o,e.target)===n.objectId:n.mode==="100644"||n.mode==="100755"?e.kind==="file"&&St(e.mode,n.mode)===n.mode&&k(o,e.contents)===n.objectId:!1}function wt(t,e){return e.length===0&&t.length===0?!0:e.length!==1||t.length!==1||t[0]?.stage!==0?!1:e[0]?.mode===t[0]?.mode&&e[0]?.objectId===t[0]?.objectId}function Rt(t,e,r,o,n,i,a){t.update("tracked-path-state-v2\0"),l(t,"path",e),r.length===0&&t.update("head-missing\0");for(const s of r)t.update("head-entry\0"),l(t,"mode",s.mode),l(t,"object",s.objectId);o.length===0&&t.update("index-missing\0");for(const s of o)t.update("index-entry\0"),l(t,"mode",s.mode),l(t,"object",s.objectId),l(t,"stage",s.stage.toString());i?Gt(t,i,a):L(t,n,a)}function bt(t,e,r){const o=[];for(const n of q(e,"Git ls-tree")){const i=n.indexOf(" "),a=n.slice(0,i).split(" ");if(i<0||a.length!==3||!/^[0-7]{6}$/u.test(a[0])||!/^(?:blob|commit)$/u.test(a[1])||!J(a[2],r))throw new Error("Git ls-tree returned invalid tree metadata");const s=E(D(n.slice(i+1))),u=F(t,s);if(!u)throw new Error(`Git ls-tree returned a path outside the product: ${s}`);o.push({mode:a[0],objectId:a[2],productRelativePath:u,rootRelativePath:s})}return o.sort((n,i)=>y(n.productRelativePath,i.productRelativePath))}async function Pt(t){const e=(await w(t,["rev-parse","--show-object-format"])).trim();if(e==="sha1"||e==="sha256")return e;throw new Error(`Unsupported Git object format: ${e||"empty"}`)}function V(t){const e=new Map;for(const r of t){const o=e.get(r.productRelativePath);o?o.push(r):e.set(r.productRelativePath,[r])}return e}function Gt(t,e,r){if(t.update("gitlink-worktree-state-v2\0"),l(t,"kind",e.kind),e.kind==="initialized"){e.head===null?t.update("head-missing\0"):l(t,"head",e.head),e.dirtyFingerprint===null?t.update("nested-clean\0"):l(t,"nested-dirty",e.dirtyFingerprint);return}L(t,e.node,r)}function L(t,e,r){switch(t.update("physical-node-v2\0"),l(t,"kind",e.kind),e.kind){case"file":l(t,"mode",process.platform==="win32"?"platform-mode-ignored":e.mode&73n?"100755":"100644"),l(t,"blob",k(r,e.contents)),I(t,e.contents);break;case"symlink":l(t,"blob",k(r,e.target)),I(t,e.target);break;case"directory":l(t,"mode",W(e.mode));break;case"ancestor-symlink":l(t,"obstacle",e.obstaclePath),I(t,e.target);break;case"ancestor-non-directory":l(t,"obstacle",e.obstaclePath),l(t,"mode",W(e.mode));break;case"ancestor-missing":l(t,"obstacle",e.obstaclePath);break;case"missing":break}}function St(t,e){return process.platform==="win32"?e:t&73n?"100755":"100644"}function k(t,e){return x.createHash(t).update(`blob ${e.byteLength.toString()}\0`).update(e).digest("hex")}function Et(t,e){return t.length===e.length&&t.every((r,o)=>{const n=e[o];return r.mode===n.mode&&r.objectId===n.objectId&&r.productRelativePath===n.productRelativePath&&r.rootRelativePath===n.rootRelativePath&&r.stage===n.stage})}function J(t,e){const r=e==="sha1"?40:64;return t.length===r&&/^[0-9a-f]+$/u.test(t)}function kt(t,e){const r=e.productRelativePath.split("/");if(r.some(i=>i.length===0||i==="."||i===".."))throw new Error(`Git returned an invalid product path: ${e.productRelativePath}`);const o=c.resolve(t.productDir,...r),n=c.relative(t.productDir,o);if(n===".."||n.startsWith(`..${c.sep}`)||c.isAbsolute(n))throw new Error(`Git returned a path outside the product: ${e.rootRelativePath}`);return o}function It(t,e){const r=p.realpathSync.native(c.resolve(t)),o=p.realpathSync.native(c.resolve(e));return process.platform==="win32"?r.toLocaleLowerCase("en-US")===o.toLocaleLowerCase("en-US"):r===o}function l(t,e,r){t.update(`${e}\0`),I(t,Buffer.from(r))}function I(t,e){t.update(`${e.byteLength.toString()}\0`),t.update(e),t.update("\0")}function Ct(t,e){return y(t.productRelativePath,e.productRelativePath)||t.stage-e.stage||y(t.mode,e.mode)||y(t.objectId,e.objectId)}function Ht(t,e){const r=new Map;for(const o of e){const n=E(o);if(n===".."||n.startsWith("../")||c.isAbsolute(n)||process.platform==="win32"&&c.win32.isAbsolute(n))throw new Error(`Git returned a path outside its worktree: ${n}`);const i=F(t,n);i&&r.set(n,{productRelativePath:i,rootRelativePath:n})}return[...r.values()].sort((o,n)=>y(o.productRelativePath,n.productRelativePath))}function F(t,e){if(!e)return null;if(!t.productRootRelativePath)return e;const r=`${t.productRootRelativePath}/`;return(process.platform==="win32"?e.toLocaleLowerCase("en-US").startsWith(r.toLocaleLowerCase("en-US")):e.startsWith(r))?e.slice(r.length):null}function xt(t){return`:(top,literal)${t}`}function Dt(t){return E(t.split(c.sep).join("/"))}function Q(t,e){const r=x.createHash("sha256");return et(r,t,t,e),r.digest("hex")}async function w(t,e){const r=await T(t,e);return O($(e),r).stdout}async function R(t,e){const r=await T(t,e);if(r.exitCode===0)return r.stdout.trim()||null;if(r.exitCode===1&&r.stdout.trim().length===0&&r.stderr.trim().length===0)return null;throw O($(e),r),new Error("Unreachable Git command result")}function T(t,e){return ot(t,["--no-replace-objects","--no-pager",...it,"-c","core.quotePath=true",...e],{repositoryConfigOnly:!0})}async function X(t,e){if(await R(t,["rev-parse","--verify","--quiet","HEAD"])!==e)throw new Error("Git HEAD changed while collecting repository state")}function q(t,e){if(t.length===0)return[];if(!t.endsWith(`
3
3
  `))throw new Error(`${e} returned an unterminated path record`);return t.slice(0,-1).split(`
4
- `)}function Ft(t){return`${t.stderr}
5
- ${t.stdout}`.toLowerCase().includes("not a git repository")}function jt(t){const e=new Set(["GIT_CEILING_DIRECTORIES","GIT_DISCOVERY_ACROSS_FILESYSTEM"]);if(Object.keys(process.env).some(r=>e.has(process.platform==="win32"?r.toUpperCase():r)))return!1;try{let r=p.realpathSync.native(c.resolve(t));for(;;){if(!p.statSync(r).isDirectory())return!1;for(const n of[".git","HEAD"])try{return p.lstatSync(c.join(r,n)),!1}catch(i){if(!v(i,"ENOENT"))return!1}const o=c.dirname(r);if(o===r)return!0;r=o}}catch{return!1}}function Ot(t){let e=p.realpathSync.native(c.resolve(t)),r=p.statSync(e).dev;const o=qt(),n=Lt(process.env.GIT_DISCOVERY_ACROSS_FILESYSTEM);let i=c.dirname(e);for(;i!==e;){if(o.has(e))return!1;if(Z(c.join(e,".git")))return!0;const a=p.statSync(i).dev;if(!n&&a!==r)return!1;e=i,r=a,i=c.dirname(e)}return o.has(e)?!1:Z(c.join(e,".git"))}function qt(){const t=process.env.GIT_CEILING_DIRECTORIES;return t?new Set(t.split(c.delimiter).filter(Boolean).map(e=>At(e))):new Set}function At(t){const e=c.resolve(t);try{return p.realpathSync.native(e)}catch(r){if(v(r,"ENOENT")||v(r,"ENOTDIR"))return e;throw r}}function Lt(t){return/^(?:1|on|true|yes)$/iu.test(t||"")}function Z(t){try{return p.lstatSync(t),!0}catch(e){if(v(e,"ENOENT")||v(e,"ENOTDIR"))return!1;throw e}}
4
+ `)}function $t(t){return`${t.stderr}
5
+ ${t.stdout}`.toLowerCase().includes("not a git repository")}function jt(t){const e=new Set(["GIT_CEILING_DIRECTORIES","GIT_DISCOVERY_ACROSS_FILESYSTEM"]);if(Object.keys(process.env).some(r=>e.has(process.platform==="win32"?r.toUpperCase():r)))return!1;try{let r=p.realpathSync.native(c.resolve(t));for(;;){if(!p.statSync(r).isDirectory())return!1;for(const n of[".git","HEAD"])try{return p.lstatSync(c.join(r,n)),!1}catch(i){if(!v(i,"ENOENT"))return!1}const o=c.dirname(r);if(o===r)return!0;r=o}}catch{return!1}}function Ot(t){let e=p.realpathSync.native(c.resolve(t)),r=p.statSync(e).dev;const o=At(),n=Ft(process.env.GIT_DISCOVERY_ACROSS_FILESYSTEM);let i=c.dirname(e);for(;i!==e;){if(o.has(e))return!1;if(Z(c.join(e,".git")))return!0;const a=p.statSync(i).dev;if(!n&&a!==r)return!1;e=i,r=a,i=c.dirname(e)}return o.has(e)?!1:Z(c.join(e,".git"))}function At(){const t=process.env.GIT_CEILING_DIRECTORIES;return t?new Set(t.split(c.delimiter).filter(Boolean).map(e=>Lt(e))):new Set}function Lt(t){const e=c.resolve(t);try{return p.realpathSync.native(e)}catch(r){if(v(r,"ENOENT")||v(r,"ENOTDIR"))return e;throw r}}function Ft(t){return/^(?:1|on|true|yes)$/iu.test(t||"")}function Z(t){try{return p.lstatSync(t),!0}catch(e){if(v(e,"ENOENT")||v(e,"ENOTDIR"))return!1;throw e}}
@@ -1 +1 @@
1
- export const CONFIGURED_STEP_TESTKIT_MODULE_SPECIFIERS=["@gfdelarue/testkit","@gfdelarue/testkit/config","@gfdelarue/testkit/config/build-assets","@gfdelarue/testkit/config/database-steps","@gfdelarue/testkit/config/next-runtime-tsconfig","@gfdelarue/testkit/drizzle","@gfdelarue/testkit/env","@gfdelarue/testkit/tooling/vitest"];const n=new Set(["@gfdelarue/testkit/environment","@gfdelarue/testkit/node-vitest/data","@gfdelarue/testkit/playwright"]);export const CONFIGURED_STEP_TESTKIT_MODULE_DIST_PATHS={"@gfdelarue/testkit":["index"],"@gfdelarue/testkit/config":["config","authoring","index"],"@gfdelarue/testkit/config/build-assets":["config","runtime","build-assets"],"@gfdelarue/testkit/config/database-steps":["database","steps"],"@gfdelarue/testkit/config/next-runtime-tsconfig":["config","runtime","next-runtime-tsconfig"],"@gfdelarue/testkit/drizzle":["database","drizzle","index"],"@gfdelarue/testkit/env":["config","runtime","env","index"],"@gfdelarue/testkit/tooling/vitest":["toolchains","vitest","index"]};export function isTestkitPackageSpecifier(t){return t==="@gfdelarue/testkit"||t.startsWith("@gfdelarue/testkit/")}export function requireConfiguredStepTestkitModuleSpecifier(t){if(n.has(t))throw new Error(`Configured lifecycle modules run in Node and may not import engine authoring module ${JSON.stringify(t)}`);const e=CONFIGURED_STEP_TESTKIT_MODULE_SPECIFIERS.find(i=>i===t);if(!e)throw new Error(`Unsupported configured Testkit module specifier ${JSON.stringify(t)}`);return e}
1
+ const e=new Set(["@gfdelarue/testkit/environment","@gfdelarue/testkit/node-vitest/data","@gfdelarue/testkit/playwright"]);export const CONFIGURED_STEP_TESTKIT_MODULE_DIST_PATHS={"@gfdelarue/testkit":["index"],"@gfdelarue/testkit/config":["config","authoring","index"],"@gfdelarue/testkit/config/build-assets":["config","runtime","build-assets"],"@gfdelarue/testkit/config/database-steps":["database","steps"],"@gfdelarue/testkit/config/next-runtime-tsconfig":["config","runtime","next-runtime-tsconfig"],"@gfdelarue/testkit/drizzle":["database","drizzle","index"],"@gfdelarue/testkit/env":["config","runtime","env","index"],"@gfdelarue/testkit/tooling/vitest":["toolchains","vitest","index"]};export function isTestkitPackageSpecifier(t){return t==="@gfdelarue/testkit"||t.startsWith("@gfdelarue/testkit/")}export function requireConfiguredStepTestkitModuleSpecifier(t){if(e.has(t))throw new Error(`Configured lifecycle modules run in Node and may not import engine authoring module ${JSON.stringify(t)}`);if(!Object.hasOwn(CONFIGURED_STEP_TESTKIT_MODULE_DIST_PATHS,t))throw new Error(`Unsupported configured Testkit module specifier ${JSON.stringify(t)}`);return t}
@@ -1 +1 @@
1
- import c from"fs";import a from"path";import{isTestkitPackageSpecifier as w,requireConfiguredStepTestkitModuleSpecifier as p}from"./configured-step-testkit-modules.js";import{isUnknownArray as g,isNonNullObject as l,normalizeOptionalString as f}from"../kernel/unknown-values.js";export function normalizeConfiguredInputs(n,r){if(n==null)return[];if(!g(n))throw new Error(`${r}.inputs must be an array`);return n.map((t,e)=>{const i=f(t);if(!i)throw new Error(`${r}.inputs[${String(e)}] must be a non-empty string`);return i})}export function normalizeConfiguredSteps(n,r){if(n==null)return[];if(!g(n))throw new Error(`${r} must be an array`);return n.map((t,e)=>normalizeConfiguredStep(t,`${r}[${String(e)}]`))}export function normalizeConfiguredStep(n,r){if(!l(n))throw new Error(`${r} must be an object`);const t=f(n.kind);if(t==="command"){const e=f(n.run);if(!e)throw new Error(`${r}.run must be a non-empty string`);const i=normalizeStepArgs(n.args,r);return{kind:t,cmd:e,cwd:f(n.cwd),inputs:normalizeConfiguredInputs(n.inputs,r),...i!==void 0?{args:i}:{}}}if(t==="sql-file"){const e=f(n.path);if(!e)throw new Error(`${r}.path must be a non-empty string`);const i=normalizeStepArgs(n.args,r);return{kind:t,path:e,cwd:f(n.cwd),inputs:normalizeConfiguredInputs(n.inputs,r),...i!==void 0?{args:i}:{}}}if(t==="module"){const e=f(n.target);if(!e)throw new Error(`${r}.target must be a non-empty string`);const i=normalizeStepArgs(n.args,r);return{kind:t,specifier:e,cwd:f(n.cwd),inputs:normalizeConfiguredInputs(n.inputs,r),...i!==void 0?{args:i}:{}}}throw new Error(`${r}.kind must be one of: command, sql-file, module`)}export function parseModuleSpecifier(n){const[r="",t]=String(n).split("#",2);return{modulePath:r,exportName:t||"default"}}export function normalizeStepArgs(n,r){if(n!=null)try{const t=JSON.stringify(n);if(typeof t!="string")throw new Error("JSON serialization returned undefined");return JSON.parse(t)}catch{throw new Error(`${r}.args must be JSON-serializable`)}}export function isBareModuleSpecifier(n){return typeof n!="string"||n.length===0||n.startsWith("./")||n.startsWith("../")?!1:!a.isAbsolute(n)}export function resolveConfiguredCwd(n,r){return a.resolve(n,r||".")}export function resolveConfiguredPath(n,r,t){return a.resolve(resolveConfiguredCwd(n,r),t)}export function collectConfiguredInputs(n,{inputs:r=[],steps:t=[]}={}){const e=new Set;for(const i of r)e.add(resolveConfiguredPath(n,null,i));for(const i of t){if(i.kind==="sql-file"&&e.add(resolveConfiguredPath(n,i.cwd,i.path)),i.kind==="module"){const u=m(i),{modulePath:o}=parseModuleSpecifier(u),d=resolveConfiguredPath(n,i.cwd,o);o.startsWith("@gfdelarue/testkit")||c.existsSync(d)?o.startsWith("@gfdelarue/testkit")||e.add(d):isBareModuleSpecifier(o)||e.add(d)}for(const u of i.inputs||[])e.add(resolveConfiguredPath(n,i.cwd,u))}return[...e].sort()}export function summarizeConfiguredStep(n){return n.kind==="command"?`command: ${n.cmd.trim()}`:n.kind==="sql-file"?`sql: ${n.path}`:`module: ${n.specifier}`}export function finalizeConfiguredInputs(n=[],r){return n.map(t=>r(t))}export function finalizeConfiguredStep(n,r){return n.kind==="command"?{...n,cmd:r(n.cmd),...typeof n.cwd=="string"?{cwd:r(n.cwd)}:{},inputs:finalizeConfiguredInputs(n.inputs,r),...n.args!==void 0?{args:n.args}:{}}:n.kind==="sql-file"?{...n,path:r(n.path),...typeof n.cwd=="string"?{cwd:r(n.cwd)}:{},inputs:finalizeConfiguredInputs(n.inputs,r),...n.args!==void 0?{args:n.args}:{}}:{...n,specifier:r(n.specifier),...typeof n.cwd=="string"?{cwd:r(n.cwd)}:{},inputs:finalizeConfiguredInputs(n.inputs,r),...n.args!==void 0?{args:n.args}:{}}}export function finalizeConfiguredSteps(n=[],r){return n.map(t=>finalizeConfiguredStep(t,r))}export function validateConfiguredCollection({productDir:n,label:r,inputs:t=[],steps:e=[]}){for(const i of t)s(resolveConfiguredPath(n,null,i),`${r} input`);for(const i of e){if(i.cwd&&s(resolveConfiguredCwd(n,i.cwd),`${r} step cwd`),i.kind==="sql-file"&&s(resolveConfiguredPath(n,i.cwd,i.path),`${r} sql file`),i.kind==="module"){const u=m(i),{modulePath:o}=parseModuleSpecifier(u),d=resolveConfiguredPath(n,i.cwd,o);w(o)?p(o):c.existsSync(d)?s(d,`${r} module`):isBareModuleSpecifier(o)||s(d,`${r} module`)}for(const u of i.inputs||[])s(resolveConfiguredPath(n,i.cwd,u),`${r} step input`)}}function s(n,r){if(!c.existsSync(n))throw new Error(`${r} does not exist: ${n}`)}function m(n){const r="specifier"in n?n.specifier:void 0,t="target"in n?n.target:void 0;return r||t}
1
+ import c from"fs";import d from"path";import{isTestkitPackageSpecifier as p,requireConfiguredStepTestkitModuleSpecifier as m}from"./configured-step-testkit-modules.js";import{isUnknownArray as a,isNonNullObject as l,normalizeOptionalString as s}from"../kernel/unknown-values.js";const w={command:"run","sql-file":"path",module:"target"};export function normalizeConfiguredInputs(n,i){if(n==null)return[];if(!a(n))throw new Error(`${i}.inputs must be an array`);return n.map((e,o)=>{const r=s(e);if(!r)throw new Error(`${i}.inputs[${String(o)}] must be a non-empty string`);return r})}export function normalizeConfiguredSteps(n,i){if(n==null)return[];if(!a(n))throw new Error(`${i} must be an array`);return n.map((e,o)=>normalizeConfiguredStep(e,`${i}[${String(o)}]`))}export function normalizeConfiguredStep(n,i){if(!l(n))throw new Error(`${i} must be an object`);const e=s(n.kind);if(e!=="command"&&e!=="sql-file"&&e!=="module")throw new Error(`${i}.kind must be one of: command, sql-file, module`);const o=w[e],r=s(n[o]);if(!r)throw new Error(`${i}.${o} must be a non-empty string`);const t=normalizeStepArgs(n.args,i),f={cwd:s(n.cwd),inputs:normalizeConfiguredInputs(n.inputs,i),...t!==void 0?{args:t}:{}};return e==="command"?{kind:e,cmd:r,...f}:e==="sql-file"?{kind:e,path:r,...f}:{kind:e,specifier:r,...f}}export function parseModuleSpecifier(n){const[i="",e]=String(n).split("#",2);return{modulePath:i,exportName:e||"default"}}export function normalizeStepArgs(n,i){if(n!=null)try{const e=JSON.stringify(n);if(typeof e!="string")throw new Error("JSON serialization returned undefined");return JSON.parse(e)}catch{throw new Error(`${i}.args must be JSON-serializable`)}}export function isBareModuleSpecifier(n){return typeof n!="string"||n.length===0||n.startsWith("./")||n.startsWith("../")?!1:!d.isAbsolute(n)}export function resolveConfiguredCwd(n,i){return d.resolve(n,i||".")}export function resolveConfiguredPath(n,i,e){return d.resolve(resolveConfiguredCwd(n,i),e)}export function collectConfiguredInputs(n,{inputs:i=[],steps:e=[]}={}){const o=new Set;for(const r of i)o.add(resolveConfiguredPath(n,null,r));for(const r of e){if(r.kind==="sql-file"&&o.add(resolveConfiguredPath(n,r.cwd,r.path)),r.kind==="module"){const{modulePath:t}=parseModuleSpecifier(r.specifier),f=resolveConfiguredPath(n,r.cwd,t);!t.startsWith("@gfdelarue/testkit")&&(c.existsSync(f)||!isBareModuleSpecifier(t))&&o.add(f)}for(const t of r.inputs)o.add(resolveConfiguredPath(n,r.cwd,t))}return[...o].sort()}export function summarizeConfiguredStep(n){return n.kind==="command"?`command: ${n.cmd.trim()}`:n.kind==="sql-file"?`sql: ${n.path}`:`module: ${n.specifier}`}export function finalizeConfiguredInputs(n=[],i){return n.map(e=>i(e))}export function finalizeConfiguredStep(n,i){return{...n.kind==="command"?{...n,cmd:i(n.cmd)}:n.kind==="sql-file"?{...n,path:i(n.path)}:{...n,specifier:i(n.specifier)},...typeof n.cwd=="string"?{cwd:i(n.cwd)}:{},inputs:finalizeConfiguredInputs(n.inputs,i)}}export function finalizeConfiguredSteps(n=[],i){return n.map(e=>finalizeConfiguredStep(e,i))}export function validateConfiguredCollection({productDir:n,label:i,inputs:e=[],steps:o=[]}){for(const r of e)u(resolveConfiguredPath(n,null,r),`${i} input`);for(const r of o){if(r.cwd&&u(resolveConfiguredCwd(n,r.cwd),`${i} step cwd`),r.kind==="sql-file"&&u(resolveConfiguredPath(n,r.cwd,r.path),`${i} sql file`),r.kind==="module"){const{modulePath:t}=parseModuleSpecifier(r.specifier);p(t)?m(t):isBareModuleSpecifier(t)||u(resolveConfiguredPath(n,r.cwd,t),`${i} module`)}for(const t of r.inputs)u(resolveConfiguredPath(n,r.cwd,t),`${i} step input`)}}function u(n,i){if(!c.existsSync(n))throw new Error(`${i} does not exist: ${n}`)}
@@ -1 +1 @@
1
- import m from"path";import{joinPathList as E,packageBinPath as w,shellQuote as S,stringifyUnknown as l}from"../process/host/platform.js";const U=new Set(["GH_TOKEN","GITEA_TOKEN","GITHUB_TOKEN"]);export function buildExecutionEnv(r,e={},t=process.env,i={}){return o(r,null,e,t,i)}export function buildTemplateExecutionEnv(r,e={},t=process.env,i={}){return o(r,null,e,t,{...i,omitRuntimeDatabaseBindings:!0})}export function buildTaskExecutionEnv(r,e,t={},i=process.env,s={}){return o(r,e,t,i,s)}export function resolveExecutionEnvValues(r,e,t=process.env){return o(r,null,{},t,{includeTestkitControls:!1},new Set(e))}function o(r,e,t,i,s={},n=null){const a=Object.fromEntries(Object.entries(c(s.inheritProcessEnv===!1?{}:i,n,!1)).filter(([v])=>v!=="DATABASE_URL"&&!U.has(v.toUpperCase()))),u=buildTemplateContext(r,e),p=s.omitRuntimeDatabaseBindings===!0,b=c(r.testkit.serviceEnv,n,p),f=c(r.testkit.local?.env,n,p);return{...a,...d(b,u),...d(f,u),...d(c(t,n,!1),u),...s.includeTestkitControls===!1?{}:{TESTKIT_ACTIVE:"1",TESTKIT_MODE:s.mode||r.testkit.mode||"test",...r.runtimeId?{TESTKIT_RUNTIME_ID:String(r.runtimeId)}:{},...e?.leaseId?{TESTKIT_LEASE_ID:String(e.leaseId)}:{},...e?.leaseDir?{TESTKIT_LEASE_DIR:e.leaseDir}:{}}}}export function buildTemplateContext(r,e){const t=r.testkit.templateContext||{};return{...t,runtimeId:r.runtimeId||t.runtimeId||null,runtimeLabel:r.runtimeLabel||t.runtimeLabel||null,productDir:r.productDir||t.productDir||null,serviceName:r.name,serviceStateDir:r.stateDir||t.serviceStateDir||null,prepareDir:r.testkit.prepareDir||t.prepareDir||null,leaseId:e?.leaseId||null,leaseDir:e?.leaseDir||null}}export function serviceDatabaseBinding(r,e=r.name){return r.testkit.templateContext?.databaseBindingsByService?.get(e)??null}export function withDatabaseBindings(r,e){return{...r,testkit:{...r.testkit,templateContext:{...r.testkit.templateContext,databaseBindingsByService:e}}}}export function finalizeString(r,e){return resolveTemplateString(r,e)}export function finalizeSerializable(r,e){return typeof r=="string"?finalizeString(r,e):Array.isArray(r)?r.map(t=>finalizeSerializable(t,e)):r===null||typeof r!="object"?r:Object.fromEntries(Object.entries(r).map(([t,i])=>[t,finalizeSerializable(i,e)]))}export function resolveTemplateString(r,e){return typeof r!="string"?r:r.replace(/\{([a-zA-Z]+)(?::([^}]+))?\}/g,(t,i,s)=>{switch(i){case"runtime":case"runtimeId":return String(e.runtimeId);case"service":return e.serviceName;case"stateDir":return String(e.serviceStateDir);case"productDir":return e.productDir||"";case"bin":{const n=m.resolve(e.productDir||process.cwd(),e.serviceCwd||".");return S(w(n,s))}case"pathList":{const n=l(s||"").split(",").map(a=>a.trim()).filter(Boolean).map(a=>m.resolve(e.productDir||process.cwd(),a));return E(n)}case"prepareDir":return e.prepareDir||"";case"lease":return e.leaseId?String(e.leaseId):"";case"leaseDir":return e.leaseDir||"";case"port":{const n=s||e.serviceName,a=e.portMap?.get(n);if(!a)throw new Error(`Unknown port placeholder for service "${n}"`);return String(a)}case"baseUrl":{const n=s||e.serviceName,a=e.baseUrlByService?.get(n);if(!a)throw new Error(`Unknown baseUrl placeholder for service "${n}"`);return a}case"readyUrl":{const n=s||e.serviceName,a=e.readyUrlByService?.get(n);if(!a)throw new Error(`Unknown readyUrl placeholder for service "${n}"`);return a}case"publicHost":return e.publicHost||"127.0.0.1";case"publicBaseUrl":{const n=s||e.serviceName,a=e.publicBaseUrlByService?.get(n)||e.baseUrlByService?.get(n);if(!a)throw new Error(`Unknown publicBaseUrl placeholder for service "${n}"`);return a}case"privateBaseUrl":{const n=s||e.serviceName,a=e.privateBaseUrlByService?.get(n)||e.baseUrlByService?.get(n);if(!a)throw new Error(`Unknown privateBaseUrl placeholder for service "${n}"`);return a}case"serviceContextUrl":{const n=s||e.serviceName,a=e.serviceContextUrlByService?.get(n)||e.baseUrlByService?.get(n);if(!a)throw new Error(`Unknown serviceContextUrl placeholder for service "${n}"`);return a}case"dbUrl":case"dbHost":case"dbPort":case"dbName":case"dbUser":case"dbPassword":case"dbSsl":{const n=s||e.serviceName;return h(i,t,n,e)}default:throw new Error(`Unsupported template token "{${i}${s?`:${s}`:""}}"`)}})}function d(r,e){return Object.fromEntries(Object.entries(r||{}).map(([t,i])=>[t,typeof i=="string"?D(t,i,e):i]))}function D(r,e,t){try{return finalizeString(e,t)}catch(i){const s=i instanceof Error?i.message:l(i);throw new Error(`Service "${t.serviceName}" env ${r}: ${s}`,{cause:i})}}function c(r,e,t){return Object.fromEntries(Object.entries(r||{}).filter(([i,s])=>e&&!e.has(i)?!1:!(t&&typeof s=="string"&&findDatabasePlaceholders(s).length>0)))}export function findDatabasePlaceholders(r){return[...r.matchAll(/\{db(?:Url|Host|Port|Name|User|Password|Ssl)(?::([^}]+))?\}/g)].map(e=>({placeholder:e[0],serviceName:e.at(1)??null}))}function h(r,e,t,i){const s=i.databaseBindingsByService?.get(t);if(!s)throw new Error(`Cannot resolve ${e}: service "${t}" has no database binding in this context`);switch(r){case"dbUrl":return s.url;case"dbHost":return s.host;case"dbPort":return String(s.port);case"dbName":return s.database;case"dbUser":return s.user;case"dbPassword":return s.password;case"dbSsl":return T(s)}}function T(r){const e=r.sslMode,t=l(e||"").trim();return t&&!I(t)?t:B(r.sslMode)?"1":"0"}function B(r){const e=l(r||"").trim().toLowerCase();return!!(e&&!["0","false","no","off","disable","disabled"].includes(e))}function I(r){return["0","1","true","false","yes","no","on","off","disable","disabled","require","verify-ca","verify-full"].includes(l(r||"").trim().toLowerCase())}
1
+ import b from"path";import{joinPathList as S,packageBinPath as f,shellQuote as D,stringifyUnknown as E}from"../process/host/platform.js";const T=new Set(["GH_TOKEN","GITEA_TOKEN","GITHUB_TOKEN"]),U={port:"portMap",baseUrl:"baseUrlByService",readyUrl:"readyUrlByService",publicBaseUrl:"publicBaseUrlByService",privateBaseUrl:"privateBaseUrlByService",serviceContextUrl:"serviceContextUrlByService"},B=new Set(["publicBaseUrl","privateBaseUrl","serviceContextUrl"]),h=new Set(["1","true","yes","on","require","verify-ca","verify-full"]),w=new Set(["0","false","no","off","disable","disabled"]);export function buildExecutionEnv(r,e={},t=process.env,s={}){return l(r,null,e,t,s)}export function buildTemplateExecutionEnv(r,e={},t=process.env,s={}){return l(r,null,e,t,{...s,omitRuntimeDatabaseBindings:!0})}export function buildTaskExecutionEnv(r,e,t={},s=process.env,n={}){return l(r,e,t,s,n)}export function resolveExecutionEnvValues(r,e,t=process.env){return l(r,null,{},t,{includeTestkitControls:!1},new Set(e))}function l(r,e,t,s,n={},i=null){const a=Object.fromEntries(Object.entries(o(n.inheritProcessEnv===!1?{}:s,i,!1)).filter(([d])=>d!=="DATABASE_URL"&&!T.has(d.toUpperCase()))),u=buildTemplateContext(r,e),p=n.omitRuntimeDatabaseBindings===!0,m=o(r.testkit.serviceEnv,i,p),v=o(r.testkit.local?.env,i,p);return{...a,...c(m,u),...c(v,u),...c(o(t,i,!1),u),...n.includeTestkitControls===!1?{}:{TESTKIT_ACTIVE:"1",TESTKIT_MODE:n.mode||r.testkit.mode||"test",...r.runtimeId?{TESTKIT_RUNTIME_ID:String(r.runtimeId)}:{},...e?.leaseId?{TESTKIT_LEASE_ID:String(e.leaseId)}:{},...e?.leaseDir?{TESTKIT_LEASE_DIR:e.leaseDir}:{}}}}export function buildTemplateContext(r,e){const t=r.testkit.templateContext||{};return{...t,runtimeId:r.runtimeId||t.runtimeId||null,runtimeLabel:r.runtimeLabel||t.runtimeLabel||null,productDir:r.productDir||t.productDir||null,serviceName:r.name,serviceStateDir:r.stateDir||t.serviceStateDir||null,prepareDir:r.testkit.prepareDir||t.prepareDir||null,leaseId:e?.leaseId||null,leaseDir:e?.leaseDir||null}}export function serviceDatabaseBinding(r,e=r.name){return r.testkit.templateContext?.databaseBindingsByService?.get(e)??null}export function withDatabaseBindings(r,e){return{...r,testkit:{...r.testkit,templateContext:{...r.testkit.templateContext,databaseBindingsByService:e}}}}export function finalizeString(r,e){return resolveTemplateString(r,e)}export function finalizeSerializable(r,e){return typeof r=="string"?finalizeString(r,e):Array.isArray(r)?r.map(t=>finalizeSerializable(t,e)):r===null||typeof r!="object"?r:Object.fromEntries(Object.entries(r).map(([t,s])=>[t,finalizeSerializable(s,e)]))}export function resolveTemplateString(r,e){return typeof r!="string"?r:r.replace(/\{([a-zA-Z]+)(?::([^}]+))?\}/g,(t,s,n)=>{switch(s){case"runtime":case"runtimeId":return String(e.runtimeId);case"service":return e.serviceName;case"stateDir":return String(e.serviceStateDir);case"productDir":return e.productDir||"";case"bin":{const i=b.resolve(e.productDir||process.cwd(),e.serviceCwd||".");return D(f(i,n))}case"pathList":{const i=E(n||"").split(",").map(a=>a.trim()).filter(Boolean).map(a=>b.resolve(e.productDir||process.cwd(),a));return S(i)}case"prepareDir":return e.prepareDir||"";case"lease":return e.leaseId?String(e.leaseId):"";case"leaseDir":return e.leaseDir||"";case"port":case"baseUrl":case"readyUrl":case"publicBaseUrl":case"privateBaseUrl":case"serviceContextUrl":return I(s,n||e.serviceName,e);case"publicHost":return e.publicHost||"127.0.0.1";case"dbUrl":case"dbHost":case"dbPort":case"dbName":case"dbUser":case"dbPassword":case"dbSsl":{const i=n||e.serviceName;return C(s,t,i,e)}default:throw new Error(`Unsupported template token "{${s}${n?`:${n}`:""}}"`)}})}function I(r,e,t){const s=t[U[r]]?.get(e)||(B.has(r)?t.baseUrlByService?.get(e):void 0);if(!s)throw new Error(`Unknown ${r} placeholder for service "${e}"`);return String(s)}function c(r,e){return Object.fromEntries(Object.entries(r||{}).map(([t,s])=>[t,typeof s=="string"?_(t,s,e):s]))}function _(r,e,t){try{return finalizeString(e,t)}catch(s){const n=s instanceof Error?s.message:E(s);throw new Error(`Service "${t.serviceName}" env ${r}: ${n}`,{cause:s})}}function o(r,e,t){return Object.fromEntries(Object.entries(r||{}).filter(([s,n])=>e&&!e.has(s)?!1:!(t&&typeof n=="string"&&findDatabasePlaceholders(n).length>0)))}export function findDatabasePlaceholders(r){return[...r.matchAll(/\{db(?:Url|Host|Port|Name|User|Password|Ssl)(?::([^}]+))?\}/g)].map(e=>({placeholder:e[0],serviceName:e.at(1)??null}))}function C(r,e,t,s){const n=s.databaseBindingsByService?.get(t);if(!n)throw new Error(`Cannot resolve ${e}: service "${t}" has no database binding in this context`);switch(r){case"dbUrl":return n.url;case"dbHost":return n.host;case"dbPort":return String(n.port);case"dbName":return n.database;case"dbUser":return n.user;case"dbPassword":return n.password;case"dbSsl":return y(n.sslMode)}}function y(r){const e=r.trim();return h.has(e.toLowerCase())?"1":!e||w.has(e.toLowerCase())?"0":e}
@@ -1 +1 @@
1
- export{buildExecutionEnv,buildTaskExecutionEnv,buildTemplateContext,buildTemplateExecutionEnv,finalizeSerializable,finalizeString,findDatabasePlaceholders,resolveExecutionEnvValues,resolveTemplateString,serviceDatabaseBinding,withDatabaseBindings}from"./environment.js";export{collectConfiguredInputs,configuredCommandInvocation,parseModuleSpecifier,resolveConfiguredCwd,resolveConfiguredPath}from"./run.js";
1
+ export{buildExecutionEnv,buildTaskExecutionEnv,buildTemplateContext,buildTemplateExecutionEnv,finalizeSerializable,finalizeString,findDatabasePlaceholders,resolveExecutionEnvValues,resolveTemplateString,serviceDatabaseBinding,withDatabaseBindings}from"./environment.js";export{collectConfiguredInputs}from"./run.js";
package/dist/steps/run.js CHANGED
@@ -1,3 +1,3 @@
1
- import{formatCapturedFailure as g,writeCapturedLine as S}from"../evidence/step-output.js";import w from"crypto";import m from"fs";import{createRequire as M}from"module";import u from"path";import{fileURLToPath as D,pathToFileURL as k}from"url";import"../config/runtime/build-assets.js";import{buildWithEsbuildRetry as I}from"../toolchains/esbuild.js";import{isBareModuleSpecifier as N,parseModuleSpecifier as f,resolveConfiguredCwd as C,resolveConfiguredPath as j}from"./configured-steps.js";import{CONFIGURED_STEP_TESTKIT_MODULE_DIST_PATHS as U,isTestkitPackageSpecifier as h,requireConfiguredStepTestkitModuleSpecifier as L}from"./configured-step-testkit-modules.js";import{readDynamicProperty as y}from"../kernel/unknown-values.js";export{collectConfiguredInputs,parseModuleSpecifier,resolveConfiguredCwd,resolveConfiguredPath}from"./configured-steps.js";import"../toolchains/index.js";import{resolvePosixCommandShell as R}from"../process/index.js";import{captureOutput as T,managedHostProcessInfrastructureError as $,startManagedHostProcess as b}from"../process/index.js";const E=String,P=u.resolve(u.dirname(D(import.meta.url)),"..",".."),F=Object.fromEntries(Object.entries(U).map(([e,t])=>[e,u.join(P,"dist",...t)+".js"])),v=u.join(P,"dist","steps","module-runner.js");export async function runConfiguredStep(e,t,r,n,o){const i=C(e.productDir,t.cwd);if(t.kind==="command"){const s=configuredCommandInvocation(t.cmd,r),d=b({recoveryIdentity:"none",args:s.args,cwd:i,displayLabel:`Configured step ${e.name}`,env:r,executable:s.command,windowsVerbatimArguments:s.windowsVerbatimArguments,outputMode:"caller-drained",signal:o.signal,timeoutMs:o.timeoutMs});await O(d,o);return}if(!B(t))return G(t);const{exportName:c}=f(t.specifier),a={args:t.args??{},databaseUrl:r.DATABASE_URL||null,productDir:e.productDir,cwd:i,serviceName:e.name,env:{...r},ownedContainer:o.ownedContainer||null,runtimeId:e.runtimeId||null,stateDir:e.stateDir,prepareDir:e.testkit.prepareDir||null},p=await A(e.productDir,t),l=u.join(e.productDir,".testkit","_template-steps",`${p.cacheKey}.${String(process.pid)}.${w.randomUUID()}.context.json`);m.writeFileSync(l,JSON.stringify(a));try{const s=b({recoveryIdentity:"none",args:[v,p.outputFile,c,l],cwd:i,displayLabel:`Configured module step ${e.name}`,env:r,executable:n?.nodeExecutable||process.execPath,outputMode:"caller-drained",signal:o.signal,timeoutMs:o.timeoutMs});await O(s,o)}finally{m.rmSync(l,{force:!0})}}export function configuredCommandInvocation(e,t,r=process.platform){return r==="win32"?{command:t.ComSpec?.trim()||"cmd.exe",args:["/d","/s","/c",`"${e}"`],windowsVerbatimArguments:!0}:{command:R(t),args:["-lc",e]}}export function applySetupNodeOptions(e={}){const t=E(e.TESTKIT_SETUP_NODE_OPTIONS||"").trim();return t?{...e,NODE_OPTIONS:[e.NODE_OPTIONS,t].filter(Boolean).join(" ")}:e}async function A(e,t){const{modulePath:r}=f(t.specifier),n=K(e,t),o=u.join(e,".testkit","_template-steps");m.mkdirSync(o,{recursive:!0});const i=q(n);if(h(r))return{outputFile:n,cacheKey:i};const c=u.join(o,`${u.basename(r).replace(/\W+/g,"-")}-${i.slice(0,12)}.mjs`);return await I({absWorkingDir:e,bundle:!0,entryPoints:[n],format:"esm",legalComments:"none",outfile:c,platform:"node",sourcemap:"inline",target:"es2020",plugins:[H()]}),{outputFile:c,cacheKey:i}}function K(e,t){const{modulePath:r}=f(t.specifier);if(h(r))return _(r);const n=j(e,t.cwd,r);if(m.existsSync(n)||!N(r))return n;const o=C(e,t.cwd);return M(u.join(o,"__testkit__.cjs")).resolve(r)}function q(e){const t=m.readFileSync(e,"utf8");return w.createHash("sha256").update(e).update("\0").update(t).digest("hex")}function H(){return{name:"testkit-configured-step-modules",setup(e){e.onResolve({filter:/^@gfdelarue\/testkit(?:\/.*)?$/},t=>({external:!0,path:k(_(t.path)).href}))}}}function _(e){const t=L(e);return F[t]}async function O(e,{livePrefix:t="",liveWriter:r=null,logRecord:n=null}={}){const o=[],i=[],c=[T(x(e.stdout),{livePrefix:t,liveWriter:r,onLine:d=>{o.push(d),S(n,"stdout",d)}}),T(x(e.stderr),{livePrefix:t,liveWriter:r,onLine:d=>{i.push(d),S(n,"stderr",d)}})],a=await e.completion;await Promise.all(c);const p=o.join(`
1
+ import{formatCapturedFailure as g,writeCapturedLine as S}from"../evidence/step-output.js";import w from"crypto";import p from"fs";import{createRequire as P}from"module";import a from"path";import{fileURLToPath as x,pathToFileURL as D}from"url";import"../config/runtime/build-assets.js";import{buildWithEsbuildRetry as M}from"../toolchains/esbuild.js";import{isBareModuleSpecifier as I,parseModuleSpecifier as f,resolveConfiguredCwd as C,resolveConfiguredPath as N}from"./configured-steps.js";import{CONFIGURED_STEP_TESTKIT_MODULE_DIST_PATHS as j,isTestkitPackageSpecifier as y,requireConfiguredStepTestkitModuleSpecifier as k}from"./configured-step-testkit-modules.js";export{collectConfiguredInputs}from"./configured-steps.js";import"../toolchains/index.js";import{resolvePosixCommandShell as U}from"../process/index.js";import{captureOutput as h,managedHostProcessInfrastructureError as L,startManagedHostProcess as T}from"../process/index.js";const R=String,b=a.resolve(a.dirname(x(import.meta.url)),"..",".."),F=Object.fromEntries(Object.entries(j).map(([e,t])=>[e,a.join(b,"dist",...t)+".js"])),$=a.join(b,"dist","steps","module-runner.js");export async function runConfiguredStep(e,t,r,n,o){const i=C(e.productDir,t.cwd);if(t.kind==="command"){const s=configuredCommandInvocation(t.cmd,r),c=T({recoveryIdentity:"none",args:s.args,cwd:i,displayLabel:`Configured step ${e.name}`,env:r,executable:s.command,windowsVerbatimArguments:s.windowsVerbatimArguments,outputMode:"caller-drained",signal:o.signal,timeoutMs:o.timeoutMs});await _(c,o);return}const{exportName:d}=f(t.specifier),u={args:t.args??{},databaseUrl:r.DATABASE_URL||null,productDir:e.productDir,cwd:i,serviceName:e.name,env:{...r},ownedContainer:o.ownedContainer||null,runtimeId:e.runtimeId||null,stateDir:e.stateDir,prepareDir:e.testkit.prepareDir||null},m=await A(e.productDir,t),l=a.join(e.productDir,".testkit","_template-steps",`${m.cacheKey}.${String(process.pid)}.${w.randomUUID()}.context.json`);p.writeFileSync(l,JSON.stringify(u));try{const s=T({recoveryIdentity:"none",args:[$,m.outputFile,d,l],cwd:i,displayLabel:`Configured module step ${e.name}`,env:r,executable:n?.nodeExecutable||process.execPath,outputMode:"caller-drained",signal:o.signal,timeoutMs:o.timeoutMs});await _(s,o)}finally{p.rmSync(l,{force:!0})}}export function configuredCommandInvocation(e,t,r=process.platform){return r==="win32"?{command:t.ComSpec?.trim()||"cmd.exe",args:["/d","/s","/c",`"${e}"`],windowsVerbatimArguments:!0}:{command:U(t),args:["-lc",e]}}export function applySetupNodeOptions(e={}){const t=R(e.TESTKIT_SETUP_NODE_OPTIONS||"").trim();return t?{...e,NODE_OPTIONS:[e.NODE_OPTIONS,t].filter(Boolean).join(" ")}:e}async function A(e,t){const{modulePath:r}=f(t.specifier),n=v(e,t),o=a.join(e,".testkit","_template-steps");p.mkdirSync(o,{recursive:!0});const i=K(n);if(y(r))return{outputFile:n,cacheKey:i};const d=a.join(o,`${a.basename(r).replace(/\W+/g,"-")}-${i.slice(0,12)}.mjs`);return await M({absWorkingDir:e,bundle:!0,entryPoints:[n],format:"esm",legalComments:"none",outfile:d,platform:"node",sourcemap:"inline",target:"es2020",plugins:[q()]}),{outputFile:d,cacheKey:i}}function v(e,t){const{modulePath:r}=f(t.specifier);if(y(r))return E(r);const n=N(e,t.cwd,r);if(p.existsSync(n)||!I(r))return n;const o=C(e,t.cwd);return P(a.join(o,"__testkit__.cjs")).resolve(r)}function K(e){const t=p.readFileSync(e,"utf8");return w.createHash("sha256").update(e).update("\0").update(t).digest("hex")}function q(){return{name:"testkit-configured-step-modules",setup(e){e.onResolve({filter:/^@gfdelarue\/testkit(?:\/.*)?$/},t=>({external:!0,path:D(E(t.path)).href}))}}}function E(e){const t=k(e);return F[t]}async function _(e,{livePrefix:t="",liveWriter:r=null,logRecord:n=null}={}){const o=[],i=[],d=[h(O(e.stdout),{livePrefix:t,liveWriter:r,onLine:c=>{o.push(c),S(n,"stdout",c)}}),h(O(e.stderr),{livePrefix:t,liveWriter:r,onLine:c=>{i.push(c),S(n,"stderr",c)}})],u=await e.completion;await Promise.all(d);const m=o.join(`
2
2
  `),l=i.join(`
3
- `),s=$("Configured step",a);if(s)throw new Error(g(s.message,p,l,"Configured step process supervision failed"),{cause:s});if(a.exitCode!==0)throw new Error(g(a.exitSignal?`Configured step terminated by ${a.exitSignal}`:null,p,l,`Step failed with exit code ${String(a.exitCode)}`))}function x(e){return!e||!("read"in e)?null:e}function B(e){return y(e,"kind")==="module"}function G(e){throw new Error(`Unsupported template step kind "${E(y(e,"kind"))}"`)}
3
+ `),s=L("Configured step",u);if(s)throw new Error(g(s.message,m,l,"Configured step process supervision failed"),{cause:s});if(u.exitCode!==0)throw new Error(g(u.exitSignal?`Configured step terminated by ${u.exitSignal}`:null,m,l,`Step failed with exit code ${String(u.exitCode)}`))}function O(e){return!e||!("read"in e)?null:e}
@@ -1 +1 @@
1
- import{build as c,stop as a}from"esbuild";const m=[/service is no longer running/iu,/service was stopped/iu,/cannot call write after a stream was destroyed/iu],o=3e4,l=String;class u extends Error{constructor(e){super(`The esbuild service operation did not settle within ${e.toString()}ms`),this.name="EsbuildServiceAttemptTimeoutError"}}export async function buildWithEsbuildRetry(t,e={}){const i=p(e.retries??1),r=E(e.attemptTimeoutMs);let s=0;for(;;)try{return await f(()=>c(t),r)}catch(n){if(s>=i||!isEsbuildServiceStoppedError(n))throw n;s+=1;try{await T()}catch{}}}export function isEsbuildServiceStoppedError(t){if(t instanceof u)return!0;const e=d(t);return m.some(i=>i.test(e))}async function f(t,e){let i;const r=new Promise((s,n)=>{i=setTimeout(()=>{n(new u(e))},e)});try{return await Promise.race([Promise.resolve().then(t),r])}finally{i!==void 0&&clearTimeout(i)}}function d(t){const e=typeof t=="object"&&t!==null||typeof t=="function"?Reflect.get(t,"message"):void 0;return l(e||t||"")}function p(t){const e=Number(t);return!Number.isFinite(e)||e<0?0:Math.floor(e)}function E(t){if(t===void 0)return o;const e=Number(t);return!Number.isSafeInteger(e)||e<=0?o:e}async function T(){try{await a()}catch{}await b()}function b(){return new Promise(t=>setTimeout(t,25))}
1
+ import{build as o,stop as u}from"esbuild";const a=[/service is no longer running/iu,/service was stopped/iu,/cannot call write after a stream was destroyed/iu],c=3e4,l=String;class s extends Error{constructor(e){super(`The esbuild service operation did not settle within ${e.toString()}ms`),this.name="EsbuildServiceAttemptTimeoutError"}}export async function buildWithEsbuildRetry(t,{attemptTimeoutMs:e=c}={}){const i=()=>d(()=>o(t),e);try{return await i()}catch(n){if(!isEsbuildServiceStoppedError(n))throw n}return await f(),i()}export function isEsbuildServiceStoppedError(t){if(t instanceof s)return!0;const e=m(t);return a.some(i=>i.test(e))}async function d(t,e){let i;const n=new Promise((p,r)=>{i=setTimeout(()=>{r(new s(e))},e)});try{return await Promise.race([Promise.resolve().then(t),n])}finally{i!==void 0&&clearTimeout(i)}}function m(t){const e=typeof t=="object"&&t!==null||typeof t=="function"?Reflect.get(t,"message"):void 0;return l(e||t||"")}async function f(){try{await u()}catch{}await E()}function E(){return new Promise(t=>setTimeout(t,25))}
@@ -1,4 +1,4 @@
1
- import _ from"crypto";import h from"fs";import m from"path";import{prepareCommandForSpawn as I,resolveCommandForSpawn as A}from"../process/host/platform.js";import{normalizeContainedRelativePath as k,resolveContainedPath as V}from"../process/files/contained-path.js";import{deriveSemverMinimum as D,isExactSemver as L,normalizeSemver as f,satisfiesSemver as g}from"./semver.js";import{managedHostProcessInfrastructureError as M,runManagedHostProcess as j}from"../process/index.js";import{managedNodeRelease as H}from"./managed-node-releases.js";import{createSharedToolchainOperation as P,waitForSharedToolchainOperation as R}from"./shared-operation.js";import{TOOLCHAIN_METADATA_TIMEOUT_MS as O}from"./timeouts.js";import{rejectUnknownKeys as F}from"../kernel/known-keys.js";import{isUnknownRecord as N,normalizeOptionalString as l}from"../kernel/unknown-values.js";import{readJsonFileOrNull as W}from"../process/files/json-file.js";const z=".",q="auto",U="require-host",B=600*1e3,K=['"use strict";','const { spawn } = require("node:child_process");',"const [command, ...args] = process.argv.slice(1);",'if (typeof command !== "string" || command.length === 0) {',' process.stderr.write("Missing Windows toolchain command\\n");'," process.exitCode = 126;","} else {"," let spawnFailed = false;"," const child = spawn(command, args, {"," shell: false,",' stdio: "inherit",'," windowsHide: true,"," windowsVerbatimArguments: true,"," });",' child.once("error", (error) => {'," spawnFailed = true;"," process.stderr.write(`${error.stack ?? String(error)}\\n`);"," process.exitCode = 126;"," });",' child.once("close", (code, signal) => {'," if (spawnFailed) return;"," if (signal !== null) {"," process.stderr.write(`Windows toolchain command exited on signal ${signal}\\n`);"," process.exitCode = 1;"," return;"," }"," process.exitCode = code ?? 1;"," });","}"].join(`
2
- `),v=new WeakMap,y=new WeakSet;export function normalizeToolchainRegistry(n){if(n==null)return{};if(!N(n))throw new Error("testkit.config.ts toolchains must be an object");const e={};for(const[o,r]of Object.entries(n))e[o]=x(r,`testkit.config.ts toolchains.${o}`);return e}export function normalizeRuntimeToolchain(n,e,o={}){if(n==null)return null;if(typeof n=="string"){const t=n.trim();if(!t)throw new Error(`${e} must reference a non-empty toolchain name`);const i=o[t];if(!i)throw new Error(`${e} references unknown toolchain "${t}"`);return{...i,refName:t,sourceLabel:`${e} -> ${t}`}}return{...x(n,e),refName:null,sourceLabel:e}}export async function resolveConfiguredToolchain(n,e={}){const o=n?.testkit?.runtime?.toolchain;if(!n||!o)return null;let r=v.get(n);if(r===void 0){r=P(i=>J(n,o,{...e,signal:i})),v.set(n,r);const t=r;r.promise.catch(()=>{v.get(n)===t&&v.delete(n)})}return R(r,e.signal)}export function announceResolvedToolchain(n,e,o=null){return!e||y.has(n)||(y.add(n),o?.toolchainResolved&&o.toolchainResolved({serviceName:n.name,runtimeLabel:n.runtimeLabel||n.name},e.summary)),Promise.resolve()}export function applyToolchainEnv(n,e,o=process.env){if(!e)return{...n};const r={...n,PATH:ne(e.binDir,n.PATH||o.PATH||""),TESTKIT_TOOLCHAIN_KIND:"node",TESTKIT_TOOLCHAIN_NODE_VERSION:e.nodeVersion,TESTKIT_TOOLCHAIN_NODE_SOURCE:e.nodeSource,TESTKIT_TOOLCHAIN_INSTALL_MODE:e.install};return e.npmVersion&&(r.TESTKIT_TOOLCHAIN_NPM_VERSION=e.npmVersion,r.TESTKIT_TOOLCHAIN_NPM_SOURCE=e.npmSource),r}function x(n,e){if(!N(n))throw new Error(`${e} must be an object`);F(n,["cwd","detect","install","node","npm"],e);const o=k(l(n.cwd)||z,`${e}.cwd`),r=l(n.detect)||q,t=l(n.install)||U;if(r!=="auto"&&r!=="off")throw new Error(`${e}.detect must be "auto" or "off"`);if(t!=="require-host"&&t!=="download")throw new Error(`${e}.install must be "require-host" or "download"`);return{cwd:o,detect:r,install:t,node:l(n.node),npm:l(n.npm)}}async function J(n,e,o){const r=n.productDir,t=V(r,e.cwd,`${e.sourceLabel}.cwd`),i=G(t),s=e.node||(e.detect==="auto"?i.nodeSpec:null),a=e.node?"toolchain.node":e.detect==="auto"?i.nodeSource??"none":"none",d=e.npm||(e.detect==="auto"?i.npmSpec:null),c=e.npm?"toolchain.npm":e.detect==="auto"?i.npmSource??"none":"none";if(!s&&!d)throw new Error(`Service "${n.name}" enables runtime.toolchain but no Node/npm version could be resolved in ${e.cwd}`);return e.install==="require-host"?Q({config:n,cwd:t,nodeSpec:s,nodeSource:a,npmSpec:d,npmSource:c,toolchain:e,options:o}):X({config:n,cwd:t,nodeSpec:s,nodeSource:a,npmSpec:d,npmSource:c,toolchain:e,options:o})}function G(n){const e=m.join(n,"package.json"),o=W(e),r=ee(m.join(n,".tool-versions")),t=b(m.join(n,".nvmrc")),i=b(m.join(n,".node-version")),s=l(p(o,"volta","node")),a=l(p(o,"engines","node")),d=l(r.nodejs),c=s||t||i||d||a;let u=null;s?u="package.json#volta.node":t?u=".nvmrc":i?u=".node-version":d?u=".tool-versions#nodejs":a&&(u="package.json#engines.node");const E=l(p(o,"volta","npm")),$=l(p(o,"engines","npm")),T=Z(p(o,"packageManager")),C=E||(T?.name==="npm"?T.version:null)||$;let w=null;return E?w="package.json#volta.npm":T?.name==="npm"?w="package.json#packageManager":$&&(w="package.json#engines.npm"),{nodeSpec:c,nodeSource:u,npmSpec:C,npmSource:w}}async function Q({config:n,cwd:e,nodeSpec:o,nodeSource:r,npmSpec:t,npmSource:i,toolchain:s,options:a}){const d=f(a.hostNodeVersion||process.versions.node);if(o&&!g(d,o))throw new Error(`Service "${n.name}" requires Node ${o} (${r}), but host Node is ${d}`);let c=null;if(t&&(c=f(a.hostNpmVersion||await Y({command:"npm",args:["--version"],cwd:e,env:process.env,commandExecutor:a.commandExecutor,signal:a.signal})),!g(c,t)))throw new Error(`Service "${n.name}" requires npm ${t} (${i}), but host npm is ${c}`);return{install:s.install,cwd:s.cwd,nodeVersion:d,nodeSource:r,nodeSpec:o,npmVersion:c,npmSource:i,npmSpec:t,nodeExecutable:process.execPath,binDir:m.dirname(process.execPath),toolchainDir:m.dirname(process.execPath),fingerprint:S({install:s.install,nodeVersion:d,npmVersion:c,nodeSource:r,npmSource:i}),summary:`${s.install} node=${d}${c?` npm=${c}`:""} (${r})`}}async function X({config:n,nodeSpec:e,nodeSource:o,npmSpec:r,npmSource:t,toolchain:i,options:s}){if(!e)throw new Error(`Service "${n.name}" download toolchain mode requires a resolvable Node version`);const a=L(e)?f(e):D(e);if(!a)throw new Error(`Service "${n.name}" cannot deterministically provision Node from "${e}". Provide an exact version in runtime.toolchain.node, .nvmrc, .node-version, .tool-versions, or package.json#volta.node.`);const d=await provisionNodeToolchain({productDir:n.productDir,version:a,fetchImpl:s.fetchImpl||fetchToolchain,commandExecutor:s.commandExecutor||executeToolchainCommand,signal:s.signal});if(!g(a,e))throw new Error(`Service "${n.name}" resolved Node ${a}, which does not satisfy ${e}`);const c=d.npmVersion?f(d.npmVersion):null;if(r&&(!c||!g(c,r)))throw new Error(`Service "${n.name}" requires npm ${r} (${t}), but Node ${a} bundles npm ${c||"unknown"}`);return{install:i.install,cwd:i.cwd,nodeVersion:a,nodeSource:o,nodeSpec:e,npmVersion:c,npmSource:t,npmSpec:r,nodeExecutable:d.nodeExecutable,binDir:d.binDir,toolchainDir:d.toolchainDir,fingerprint:S({install:i.install,nodeVersion:a,npmVersion:c,nodeSource:o,npmSource:t}),summary:`download node=${a}${c?` npm=${c}`:""} (${o})`}}export async function provisionNodeToolchain({productDir:n,version:e,fetchImpl:o=fetchToolchain,commandExecutor:r=executeToolchainCommand,signal:t}){const i=f(e);if(e!==i)throw new Error(`Downloaded Node toolchain version must use canonical x.y.z form; received ${JSON.stringify(e)}`);const s=H(i),{provisionNodeToolchainWithRelease:a}=await import("./node-provisioning.js");return a({commandExecutor:r,fetchImpl:o,productDir:n,release:s,signal:t})}function S(n){return _.createHash("sha256").update(JSON.stringify({kind:"node",...n})).digest("hex")}async function Y({command:n,args:e,cwd:o,env:r,commandExecutor:t=executeToolchainCommand,signal:i}){const s=await t(n,e,{cwd:o,env:r,signal:i,stdio:"pipe",timeoutMs:O,windowsHide:!0});return s.stdout==null?"":typeof s.stdout=="string"?s.stdout.trim():Buffer.from(s.stdout).toString().trim()}function Z(n){const e=l(n);if(!e)return null;const o=e.lastIndexOf("@");return o<=0||o===e.length-1?null:{name:e.slice(0,o),version:e.slice(o+1)}}function b(n){if(!h.existsSync(n))return null;const o=h.readFileSync(n,"utf8").split(/\r?\n/).find(r=>r.trim().length>0);return o?o.trim():null}function ee(n){if(!h.existsSync(n))return{};const e={};for(const o of h.readFileSync(n,"utf8").split(/\r?\n/)){const r=o.trim();if(!r||r.startsWith("#"))continue;const[t,i]=r.split(/\s+/,2);t&&i&&(e[t]=i.trim())}return e}function ne(n,e){const o=l(n);if(!o)return e;const r=(e||"").split(m.delimiter).filter(Boolean).filter(t=>t!==o);return[o,...r].join(m.delimiter)}export async function fetchToolchain(n,e={}){const o=AbortSignal.timeout(B),r=await fetch(n,{signal:e.signal===void 0?o:AbortSignal.any([e.signal,o])});return{arrayBuffer:()=>r.arrayBuffer(),body:r.body===null?null:oe(r.body),ok:r.ok,status:r.status}}async function*oe(n){const e=n.getReader();let o=!1;try{for(;;){const r=await e.read();if(r.done){o=!0;return}yield r.value}}finally{o||await e.cancel(),e.releaseLock()}}export function prepareToolchainCommandForManagedHost(n,e,o={}){const r={env:o.env||process.env,platform:o.platform},t=A(n,r),i=I(t,e,r);return i.windowsVerbatimArguments?(re(t,e),{executable:o.hostNodeExecutable||process.execPath,args:["--input-type=commonjs","--eval",K,"--",i.command,...i.args]}):{executable:i.command,args:i.args}}function re(n,e){if(/[%!"\0\r\n]/u.test(n))throw new Error(`Windows batch toolchain commands cannot safely preserve command path ${JSON.stringify(n)}`);const o=e.find(r=>/[%!"\0\r\n]/u.test(r)||r.endsWith("\\"));if(o!==void 0)throw new Error(`Windows batch toolchain commands cannot safely preserve argument ${JSON.stringify(o)}`)}export async function executeToolchainCommand(n,e,o={}){const r=te(o.timeoutMs),t=m.resolve(o.cwd||process.cwd()),i=prepareToolchainCommandForManagedHost(n,e,{env:o.env||process.env}),s=await j({args:i.args,cwd:t,displayLabel:`Toolchain command ${n}`,env:o.env||process.env,executable:i.executable,signal:o.signal,timeoutMs:r}),a=M(`Toolchain command ${n}`,s);if(a)throw a;if(s.exitCode!==0)throw new Error(`Toolchain command ${n} exited with code ${String(s.exitCode)}${ie(s.stdout,s.stderr)}`);return{stdout:s.stdout}}function te(n){const e=n??O;if(!Number.isSafeInteger(e)||e<=0)throw new Error("Toolchain command timeout must be a positive safe integer in milliseconds");return e}function ie(n,e){const o=[n,e].map(r=>r.trim()).filter(Boolean).join(`
1
+ import b from"crypto";import h from"fs";import m from"path";import{prepareCommandForSpawn as I,resolveCommandForSpawn as A}from"../process/host/platform.js";import{normalizeContainedRelativePath as k,resolveContainedPath as D}from"../process/files/contained-path.js";import{deriveSemverMinimum as L,isExactSemver as M,normalizeSemver as p,satisfiesSemver as g}from"./semver.js";import{managedHostProcessInfrastructureError as j,runManagedHostProcess as H}from"../process/index.js";import{managedNodeRelease as V}from"./managed-node-releases.js";import{createSharedToolchainOperation as P,waitForSharedToolchainOperation as R}from"./shared-operation.js";import{TOOLCHAIN_METADATA_TIMEOUT_MS as O}from"./timeouts.js";import{rejectUnknownKeys as F}from"../kernel/known-keys.js";import{isUnknownRecord as T,normalizeOptionalString as d}from"../kernel/unknown-values.js";import{readJsonFileOrNull as W}from"../process/files/json-file.js";const z=".",q="auto",U="require-host",B=600*1e3,K=['"use strict";','const { spawn } = require("node:child_process");',"const [command, ...args] = process.argv.slice(1);",'if (typeof command !== "string" || command.length === 0) {',' process.stderr.write("Missing Windows toolchain command\\n");'," process.exitCode = 126;","} else {"," let spawnFailed = false;"," const child = spawn(command, args, {"," shell: false,",' stdio: "inherit",'," windowsHide: true,"," windowsVerbatimArguments: true,"," });",' child.once("error", (error) => {'," spawnFailed = true;"," process.stderr.write(`${error.stack ?? String(error)}\\n`);"," process.exitCode = 126;"," });",' child.once("close", (code, signal) => {'," if (spawnFailed) return;"," if (signal !== null) {"," process.stderr.write(`Windows toolchain command exited on signal ${signal}\\n`);"," process.exitCode = 1;"," return;"," }"," process.exitCode = code ?? 1;"," });","}"].join(`
2
+ `),S=new WeakMap,$=new WeakSet;export function normalizeToolchainRegistry(n){if(n==null)return{};if(!T(n))throw new Error("testkit.config.ts toolchains must be an object");const e={};for(const[o,r]of Object.entries(n))e[o]=y(r,`testkit.config.ts toolchains.${o}`);return e}export function normalizeRuntimeToolchain(n,e,o={}){if(n==null)return null;if(typeof n=="string"){const i=n.trim();if(!i)throw new Error(`${e} must reference a non-empty toolchain name`);const t=o[i];if(!t)throw new Error(`${e} references unknown toolchain "${i}"`);return{...t,refName:i,sourceLabel:`${e} -> ${i}`}}return{...y(n,e),refName:null,sourceLabel:e}}export async function resolveConfiguredToolchain(n,e={}){const o=n?.testkit?.runtime?.toolchain;if(!n||!o)return null;let r=S.get(n);if(r===void 0){r=P(t=>J(n,o,{...e,signal:t})),S.set(n,r);const i=r;r.promise.catch(()=>{S.get(n)===i&&S.delete(n)})}return R(r,e.signal)}export function announceResolvedToolchain(n,e,o=null){return!e||$.has(n)||($.add(n),o?.toolchainResolved&&o.toolchainResolved({serviceName:n.name,runtimeLabel:n.runtimeLabel||n.name},e.summary)),Promise.resolve()}export function applyToolchainEnv(n,e,o=process.env){if(!e)return{...n};const r={...n,PATH:oe(e.binDir,n.PATH||o.PATH||""),TESTKIT_TOOLCHAIN_KIND:"node",TESTKIT_TOOLCHAIN_NODE_VERSION:e.nodeVersion,TESTKIT_TOOLCHAIN_NODE_SOURCE:e.nodeSource,TESTKIT_TOOLCHAIN_INSTALL_MODE:e.install};return e.npmVersion&&(r.TESTKIT_TOOLCHAIN_NPM_VERSION=e.npmVersion,r.TESTKIT_TOOLCHAIN_NPM_SOURCE=e.npmSource),r}function y(n,e){if(!T(n))throw new Error(`${e} must be an object`);F(n,["cwd","detect","install","node","npm"],e);const o=k(d(n.cwd)||z,`${e}.cwd`),r=d(n.detect)||q,i=d(n.install)||U;if(r!=="auto"&&r!=="off")throw new Error(`${e}.detect must be "auto" or "off"`);if(i!=="require-host"&&i!=="download")throw new Error(`${e}.install must be "require-host" or "download"`);return{cwd:o,detect:r,install:i,node:d(n.node),npm:d(n.npm)}}async function J(n,e,o){const r=n.productDir,i=D(r,e.cwd,`${e.sourceLabel}.cwd`),t=G(i),s=e.node||(e.detect==="auto"?t.nodeSpec:null),c=e.node?"toolchain.node":e.detect==="auto"?t.nodeSource??"none":"none",a=e.npm||(e.detect==="auto"?t.npmSpec:null),l=e.npm?"toolchain.npm":e.detect==="auto"?t.npmSource??"none":"none";if(!s&&!a)throw new Error(`Service "${n.name}" enables runtime.toolchain but no Node/npm version could be resolved in ${e.cwd}`);return e.install==="require-host"?Q({config:n,cwd:i,nodeSpec:s,nodeSource:c,npmSpec:a,npmSource:l,toolchain:e,options:o}):X({config:n,cwd:i,nodeSpec:s,nodeSource:c,npmSpec:a,npmSource:l,toolchain:e,options:o})}function G(n){const e=m.join(n,"package.json"),o=W(e),r=ne(m.join(n,".tool-versions")),i=_(m.join(n,".nvmrc")),t=_(m.join(n,".node-version")),s=d(f(o,"volta","node")),c=d(f(o,"engines","node")),a=d(r.nodejs),l=s||i||t||a||c;let u=null;s?u="package.json#volta.node":i?u=".nvmrc":t?u=".node-version":a?u=".tool-versions#nodejs":c&&(u="package.json#engines.node");const N=d(f(o,"volta","npm")),E=d(f(o,"engines","npm")),v=ee(f(o,"packageManager")),C=N||(v?.name==="npm"?v.version:null)||E;let w=null;return N?w="package.json#volta.npm":v?.name==="npm"?w="package.json#packageManager":E&&(w="package.json#engines.npm"),{nodeSpec:l,nodeSource:u,npmSpec:C,npmSource:w}}async function Q(n){const{config:e,cwd:o,nodeSpec:r,nodeSource:i,npmSpec:t,npmSource:s,options:c}=n,a=p(c.hostNodeVersion||process.versions.node);if(r&&!g(a,r))throw new Error(`Service "${e.name}" requires Node ${r} (${i}), but host Node is ${a}`);let l=null;if(t&&(l=p(c.hostNpmVersion||await Z({command:"npm",args:["--version"],cwd:o,env:process.env,commandExecutor:c.commandExecutor,signal:c.signal})),!g(l,t)))throw new Error(`Service "${e.name}" requires npm ${t} (${s}), but host npm is ${l}`);return x(n,{nodeVersion:a,npmVersion:l,nodeExecutable:process.execPath,binDir:m.dirname(process.execPath),toolchainDir:m.dirname(process.execPath)})}async function X(n){const{config:e,nodeSpec:o,npmSpec:r,npmSource:i,options:t}=n;if(!o)throw new Error(`Service "${e.name}" download toolchain mode requires a resolvable Node version`);const s=M(o)?p(o):L(o);if(!s)throw new Error(`Service "${e.name}" cannot deterministically provision Node from "${o}". Provide an exact version in runtime.toolchain.node, .nvmrc, .node-version, .tool-versions, or package.json#volta.node.`);const c=await provisionNodeToolchain({productDir:e.productDir,version:s,fetchImpl:t.fetchImpl||fetchToolchain,commandExecutor:t.commandExecutor||executeToolchainCommand,signal:t.signal});if(!g(s,o))throw new Error(`Service "${e.name}" resolved Node ${s}, which does not satisfy ${o}`);const a=c.npmVersion?p(c.npmVersion):null;if(r&&(!a||!g(a,r)))throw new Error(`Service "${e.name}" requires npm ${r} (${i}), but Node ${s} bundles npm ${a||"unknown"}`);return x(n,{nodeVersion:s,npmVersion:a,nodeExecutable:c.nodeExecutable,binDir:c.binDir,toolchainDir:c.toolchainDir})}function x({nodeSource:n,nodeSpec:e,npmSource:o,npmSpec:r,toolchain:i},{nodeVersion:t,npmVersion:s,nodeExecutable:c,binDir:a,toolchainDir:l}){return{install:i.install,cwd:i.cwd,nodeVersion:t,nodeSource:n,nodeSpec:e,npmVersion:s,npmSource:o,npmSpec:r,nodeExecutable:c,binDir:a,toolchainDir:l,fingerprint:Y({install:i.install,nodeVersion:t,npmVersion:s,nodeSource:n,npmSource:o}),summary:`${i.install} node=${t}${s?` npm=${s}`:""} (${n})`}}export async function provisionNodeToolchain({productDir:n,version:e,fetchImpl:o=fetchToolchain,commandExecutor:r=executeToolchainCommand,signal:i}){const t=p(e);if(e!==t)throw new Error(`Downloaded Node toolchain version must use canonical x.y.z form; received ${JSON.stringify(e)}`);const s=V(t),{provisionNodeToolchainWithRelease:c}=await import("./node-provisioning.js");return c({commandExecutor:r,fetchImpl:o,productDir:n,release:s,signal:i})}function Y(n){return b.createHash("sha256").update(JSON.stringify({kind:"node",...n})).digest("hex")}async function Z({command:n,args:e,cwd:o,env:r,commandExecutor:i=executeToolchainCommand,signal:t}){const s=await i(n,e,{cwd:o,env:r,signal:t,stdio:"pipe",timeoutMs:O,windowsHide:!0});return s.stdout==null?"":typeof s.stdout=="string"?s.stdout.trim():Buffer.from(s.stdout).toString().trim()}function ee(n){const e=d(n);if(!e)return null;const o=e.lastIndexOf("@");return o<=0||o===e.length-1?null:{name:e.slice(0,o),version:e.slice(o+1)}}function _(n){if(!h.existsSync(n))return null;const o=h.readFileSync(n,"utf8").split(/\r?\n/).find(r=>r.trim().length>0);return o?o.trim():null}function ne(n){if(!h.existsSync(n))return{};const e={};for(const o of h.readFileSync(n,"utf8").split(/\r?\n/)){const r=o.trim();if(!r||r.startsWith("#"))continue;const[i,t]=r.split(/\s+/,2);i&&t&&(e[i]=t.trim())}return e}function oe(n,e){const o=d(n);if(!o)return e;const r=(e||"").split(m.delimiter).filter(Boolean).filter(i=>i!==o);return[o,...r].join(m.delimiter)}export async function fetchToolchain(n,e={}){const o=AbortSignal.timeout(B),r=await fetch(n,{signal:e.signal===void 0?o:AbortSignal.any([e.signal,o])});return{arrayBuffer:()=>r.arrayBuffer(),body:r.body===null?null:re(r.body),ok:r.ok,status:r.status}}async function*re(n){const e=n.getReader();let o=!1;try{for(;;){const r=await e.read();if(r.done){o=!0;return}yield r.value}}finally{o||await e.cancel(),e.releaseLock()}}export function prepareToolchainCommandForManagedHost(n,e,o={}){const r={env:o.env||process.env,platform:o.platform},i=A(n,r),t=I(i,e,r);return t.windowsVerbatimArguments?(te(i,e),{executable:o.hostNodeExecutable||process.execPath,args:["--input-type=commonjs","--eval",K,"--",t.command,...t.args]}):{executable:t.command,args:t.args}}function te(n,e){if(/[%!"\0\r\n]/u.test(n))throw new Error(`Windows batch toolchain commands cannot safely preserve command path ${JSON.stringify(n)}`);const o=e.find(r=>/[%!"\0\r\n]/u.test(r)||r.endsWith("\\"));if(o!==void 0)throw new Error(`Windows batch toolchain commands cannot safely preserve argument ${JSON.stringify(o)}`)}export async function executeToolchainCommand(n,e,o={}){const r=m.resolve(o.cwd||process.cwd()),i=prepareToolchainCommandForManagedHost(n,e,{env:o.env||process.env}),t=await H({args:i.args,cwd:r,displayLabel:`Toolchain command ${n}`,env:o.env||process.env,executable:i.executable,signal:o.signal,timeoutMs:o.timeoutMs??O}),s=j(`Toolchain command ${n}`,t);if(s)throw s;if(t.exitCode!==0)throw new Error(`Toolchain command ${n} exited with code ${String(t.exitCode)}${ie(t.stdout,t.stderr)}`);return{stdout:t.stdout}}function ie(n,e){const o=[n,e].map(r=>r.trim()).filter(Boolean).join(`
3
3
  `);return o?`
4
- ${o.slice(-4e3)}`:""}function p(n,...e){let o=n;for(const r of e){if(!N(o))return;o=o[r]}return o}
4
+ ${o.slice(-4e3)}`:""}function f(n,...e){let o=n;for(const r of e){if(!T(o))return;o=o[r]}return o}
@@ -1 +1 @@
1
- import O from"node:crypto";import u from"node:fs";import b from"node:path";import{normalizeArchivePath as L,portableCollisionKey as _,requireSafeMode as W,requireSafeSize as z}from"./archive.js";import{isPathWithin as C}from"../process/files/paths.js";import{throwIfSignalAborted as p}from"./shared-operation.js";const D=128,q=4,F=64*1024,K=1024*1024,H=new Map([["bin/corepack","../lib/node_modules/corepack/dist/corepack.js"],["bin/npm","../lib/node_modules/npm/bin/npm-cli.js"],["bin/npx","../lib/node_modules/npm/bin/npx-cli.js"]]);export function validateNodeArchiveEntries(o,e){const t=e.archive;let n=0,s=0;const a=new Map,h=new Map;if(o.length!==t.entryCount)throw B(o.length,n,0,e);for(const i of o){const r=L(i.path);if(r!==t.rootDirectory&&!r.startsWith(`${t.rootDirectory}/`))throw new Error(`Node archive entry escapes its locked root: ${JSON.stringify(r)}`);W(i.mode,r),z(i.sizeBytes,r);const l=_(r),f=h.get(l);if(f!==void 0)throw new Error(`Node archive contains colliding paths ${JSON.stringify(f)} and ${JSON.stringify(r)}`);if(h.set(l,r),a.set(r,i),i.kind==="forbidden")throw new Error(`Node archive entry ${JSON.stringify(r)} has forbidden type ${JSON.stringify(i.rawType)}`);if(i.kind==="directory"){if(i.sizeBytes!==0||i.linkTarget!==void 0)throw new Error(`Node archive directory ${JSON.stringify(r)} has invalid metadata`);continue}if(i.kind==="file"){if(i.linkTarget!==void 0)throw new Error(`Node archive file ${JSON.stringify(r)} has link metadata`);n+=1,s+=i.sizeBytes,P(o.length,n,s,e);continue}const v=r.slice(t.rootDirectory.length+1),y=H.get(v);if(y===void 0||i.linkTarget!==y||i.sizeBytes!==Buffer.byteLength(y,"utf8"))throw new Error(`Node archive contains an unsupported symbolic link at ${JSON.stringify(r)}`);M(r,y,t.rootDirectory),s+=i.sizeBytes,P(o.length,n,s,e)}if(a.get(t.rootDirectory)?.kind!=="directory")throw new Error("Node archive is missing its locked root directory");for(const[i]of a)for(const r of G(i)){const l=a.get(r);if(l?.kind==="file")throw new Error(`Node archive entry ${JSON.stringify(i)} has a file ancestor`);if(l?.kind==="symlink")throw new Error(`Node archive entry ${JSON.stringify(i)} would write through a symbolic link`)}if(n!==t.fileCount||s!==t.unpackedSizeBytes)throw B(o.length,n,s,e)}export async function inspectInstalledNodeTree(o,e,t){X(e);const n=await u.promises.lstat(o);if(!n.isDirectory()||n.isSymbolicLink())throw new Error("Installed Node toolchain content must be a real directory");const s=await u.promises.realpath(o),a=[],h=new Map;let N=0,i=0;const r=new Set,l=new AbortController,f={occurred:!1},v=()=>{y(t?.reason)};t?.addEventListener("abort",v,{once:!0}),t?.aborted&&v();try{if(await x(o,""),await Promise.all(r),f.occurred)throw f.error;return a.sort((d,w)=>R(d.path,w.path)),{entryCount:a.length,fileCount:N,sha256:O.createHash("sha256").update(JSON.stringify(a)).digest("hex"),unpackedSizeBytes:i}}catch(d){throw y(d),await Promise.all(r),f.error}finally{t?.removeEventListener("abort",v)}function y(d){f.occurred||(f.occurred=!0,f.error=d,l.abort(d))}async function j(d,w,S){const m=a.length;for(a.push({kind:"file",path:w,sha256:"",sizeBytes:S}),J(a.length,N,i,e);r.size>=q;)await Promise.race(r);p(l.signal);const E=Y(d,S,l.signal).then(c=>{a[m]={kind:"file",path:w,sha256:c,sizeBytes:S}}).catch(y).then(()=>{r.delete(E)});r.add(E)}async function x(d,w){p(l.signal);const S=await u.promises.readdir(d,{withFileTypes:!0});S.sort((m,E)=>R(m.name,E.name));for(const m of S){p(l.signal);const E=w?`${w}/${m.name}`:m.name,c=L(E),I=_(c),T=h.get(I);if(T!==void 0)throw new Error(`Installed Node toolchain contains colliding paths ${JSON.stringify(T)} and ${JSON.stringify(c)}`);h.set(I,c);const g=b.join(d,m.name);if(!C(o,g))throw new Error(`Installed Node toolchain path escapes its root: ${JSON.stringify(c)}`);const k=await u.promises.lstat(g);if(k.isSymbolicLink()){const $=await u.promises.readlink(g),A=H.get(c);if(A===void 0||$!==A)throw new Error(`Installed Node toolchain contains an unsupported symbolic link at ${JSON.stringify(c)}`);M(c,$,".");const U=await u.promises.realpath(g);if(!C(s,U))throw new Error(`Installed Node toolchain symbolic link escapes its root: ${JSON.stringify(c)}`);if(!(await u.promises.stat(g)).isFile())throw new Error(`Installed Node toolchain symbolic link is not file-backed: ${JSON.stringify(c)}`);i+=Buffer.byteLength($,"utf8"),a.push({kind:"symlink",path:c,target:$})}else if(k.isDirectory()){a.push({kind:"directory",path:c}),J(a.length,N,i,e),await x(g,c);continue}else if(k.isFile()){if(k.nlink!==1)throw new Error(`Installed Node toolchain contains a multiply-linked file at ${JSON.stringify(c)}`);N+=1,i+=z(k.size,c),await j(g,c,k.size)}else throw new Error(`Installed Node toolchain contains a special file at ${JSON.stringify(c)}`);J(a.length,N,i,e)}}}export class InstalledNodeTreeMismatchError extends Error{}export async function verifyInstalledNodeTree(o,e,t){const n=await inspectInstalledNodeTree(o,{maxEntries:e.entryCount,maxFiles:e.fileCount,maxUnpackedSizeBytes:e.unpackedSizeBytes},t);if(n.entryCount!==e.entryCount||n.fileCount!==e.fileCount||n.unpackedSizeBytes!==e.unpackedSizeBytes||n.sha256!==e.sha256)throw new InstalledNodeTreeMismatchError(`Installed Node toolchain does not match its trusted tree digest and shape: expected ${JSON.stringify(e)}, received ${JSON.stringify(n)}`)}function M(o,e,t){if(b.posix.isAbsolute(e)||b.win32.isAbsolute(e)||e.includes("\0"))throw new Error(`Node toolchain symbolic link has an absolute target at ${JSON.stringify(o)}`);const n=b.posix.normalize(b.posix.join(b.posix.dirname(o),e));if(t==="."?n===".."||n.startsWith("../"):n!==t&&!n.startsWith(`${t}/`))throw new Error(`Node toolchain symbolic link escapes its root at ${JSON.stringify(o)}`)}function G(o){const e=o.split("/");if(e.length>D)throw new Error("Node archive path has too many segments");const t=[];for(let n=1;n<e.length;n+=1)t.push(e.slice(0,n).join("/"));return t}function P(o,e,t,n){const s=n.archive;if(o>s.entryCount||e>s.fileCount||t>s.unpackedSizeBytes)throw B(o,e,t,n)}function J(o,e,t,n){if(o>n.maxEntries||e>n.maxFiles||t>n.maxUnpackedSizeBytes)throw new Error("Installed Node toolchain exceeds its locked tree shape")}function B(o,e,t,n){return new Error(`Node ${n.version} archive shape mismatch: received ${o.toString()} entries, ${e.toString()} files, and ${t.toString()} unpacked bytes`)}function X(o){for(const[e,t]of Object.entries(o))if(!Number.isSafeInteger(t)||t<0)throw new Error(`${e} must be a non-negative safe integer`)}async function Y(o,e,t){if(p(t),e<=F)return Q(o,t);const n=O.createHash("sha256"),s=u.createReadStream(o,{highWaterMark:K}),a=()=>s.destroy(t?.reason);t?.addEventListener("abort",a,{once:!0});try{for await(const h of s)p(t),n.update(h);return p(t),n.digest("hex")}finally{t?.removeEventListener("abort",a),s.destroy(),s.closed||await new Promise(h=>s.once("close",h))}}async function Q(o,e){const t=await u.promises.open(o,"r");try{const n=O.createHash("sha256"),s=Buffer.allocUnsafe(F);for(;;){p(e);const{bytesRead:a}=await t.read(s,0,s.length,null);if(p(e),a===0)return n.digest("hex");n.update(s.subarray(0,a))}}finally{await t.close()}}function R(o,e){return o<e?-1:o>e?1:0}
1
+ import O from"node:crypto";import u from"node:fs";import b from"node:path";import{normalizeArchivePath as _,portableCollisionKey as z,requireSafeMode as j,requireSafeSize as L}from"./archive.js";import{isPathWithin as C}from"../process/files/paths.js";import{throwIfSignalAborted as p}from"./shared-operation.js";const D=128,K=4,F=64*1024,q=1024*1024,H=new Map([["bin/corepack","../lib/node_modules/corepack/dist/corepack.js"],["bin/npm","../lib/node_modules/npm/bin/npm-cli.js"],["bin/npx","../lib/node_modules/npm/bin/npx-cli.js"]]);export function validateNodeArchiveEntries(o,t){const e=t.archive;let n=0,s=0;const a=new Map,h=new Map;if(o.length!==e.entryCount)throw B(o.length,n,0,t);for(const i of o){const r=_(i.path);if(r!==e.rootDirectory&&!r.startsWith(`${e.rootDirectory}/`))throw new Error(`Node archive entry escapes its locked root: ${JSON.stringify(r)}`);j(i.mode,r),L(i.sizeBytes,r);const l=z(r),f=h.get(l);if(f!==void 0)throw new Error(`Node archive contains colliding paths ${JSON.stringify(f)} and ${JSON.stringify(r)}`);if(h.set(l,r),a.set(r,i),i.kind==="forbidden")throw new Error(`Node archive entry ${JSON.stringify(r)} has forbidden type ${JSON.stringify(i.rawType)}`);if(i.kind==="directory"){if(i.sizeBytes!==0||i.linkTarget!==void 0)throw new Error(`Node archive directory ${JSON.stringify(r)} has invalid metadata`);continue}if(i.kind==="file"){if(i.linkTarget!==void 0)throw new Error(`Node archive file ${JSON.stringify(r)} has link metadata`);n+=1,s+=i.sizeBytes,P(o.length,n,s,t);continue}const v=r.slice(e.rootDirectory.length+1),y=H.get(v);if(y===void 0||i.linkTarget!==y||i.sizeBytes!==Buffer.byteLength(y,"utf8"))throw new Error(`Node archive contains an unsupported symbolic link at ${JSON.stringify(r)}`);M(r,y,e.rootDirectory),s+=i.sizeBytes,P(o.length,n,s,t)}if(a.get(e.rootDirectory)?.kind!=="directory")throw new Error("Node archive is missing its locked root directory");for(const[i]of a)for(const r of G(i)){const l=a.get(r);if(l?.kind==="file")throw new Error(`Node archive entry ${JSON.stringify(i)} has a file ancestor`);if(l?.kind==="symlink")throw new Error(`Node archive entry ${JSON.stringify(i)} would write through a symbolic link`)}if(n!==e.fileCount||s!==e.unpackedSizeBytes)throw B(o.length,n,s,t)}export async function inspectInstalledNodeTree(o,t,e){const n=await u.promises.lstat(o);if(!n.isDirectory()||n.isSymbolicLink())throw new Error("Installed Node toolchain content must be a real directory");const s=await u.promises.realpath(o),a=[],h=new Map;let N=0,i=0;const r=new Set,l=new AbortController,f={occurred:!1},v=()=>{y(e?.reason)};e?.addEventListener("abort",v,{once:!0}),e?.aborted&&v();try{if(await x(o,""),await Promise.all(r),f.occurred)throw f.error;return a.sort((d,w)=>R(d.path,w.path)),{entryCount:a.length,fileCount:N,sha256:O.createHash("sha256").update(JSON.stringify(a)).digest("hex"),unpackedSizeBytes:i}}catch(d){throw y(d),await Promise.all(r),f.error}finally{e?.removeEventListener("abort",v)}function y(d){f.occurred||(f.occurred=!0,f.error=d,l.abort(d))}async function U(d,w,S){const m=a.length;for(a.push({kind:"file",path:w,sha256:"",sizeBytes:S}),J(a.length,N,i,t);r.size>=K;)await Promise.race(r);p(l.signal);const E=X(d,S,l.signal).then(c=>{a[m]={kind:"file",path:w,sha256:c,sizeBytes:S}}).catch(y).then(()=>{r.delete(E)});r.add(E)}async function x(d,w){p(l.signal);const S=await u.promises.readdir(d,{withFileTypes:!0});S.sort((m,E)=>R(m.name,E.name));for(const m of S){p(l.signal);const E=w?`${w}/${m.name}`:m.name,c=_(E),I=z(c),A=h.get(I);if(A!==void 0)throw new Error(`Installed Node toolchain contains colliding paths ${JSON.stringify(A)} and ${JSON.stringify(c)}`);h.set(I,c);const g=b.join(d,m.name);if(!C(o,g))throw new Error(`Installed Node toolchain path escapes its root: ${JSON.stringify(c)}`);const k=await u.promises.lstat(g);if(k.isSymbolicLink()){const $=await u.promises.readlink(g),T=H.get(c);if(T===void 0||$!==T)throw new Error(`Installed Node toolchain contains an unsupported symbolic link at ${JSON.stringify(c)}`);M(c,$,".");const W=await u.promises.realpath(g);if(!C(s,W))throw new Error(`Installed Node toolchain symbolic link escapes its root: ${JSON.stringify(c)}`);if(!(await u.promises.stat(g)).isFile())throw new Error(`Installed Node toolchain symbolic link is not file-backed: ${JSON.stringify(c)}`);i+=Buffer.byteLength($,"utf8"),a.push({kind:"symlink",path:c,target:$})}else if(k.isDirectory()){a.push({kind:"directory",path:c}),J(a.length,N,i,t),await x(g,c);continue}else if(k.isFile()){if(k.nlink!==1)throw new Error(`Installed Node toolchain contains a multiply-linked file at ${JSON.stringify(c)}`);N+=1,i+=L(k.size,c),await U(g,c,k.size)}else throw new Error(`Installed Node toolchain contains a special file at ${JSON.stringify(c)}`);J(a.length,N,i,t)}}}export class InstalledNodeTreeMismatchError extends Error{}export async function verifyInstalledNodeTree(o,t,e){const n=await inspectInstalledNodeTree(o,{maxEntries:t.entryCount,maxFiles:t.fileCount,maxUnpackedSizeBytes:t.unpackedSizeBytes},e);if(n.entryCount!==t.entryCount||n.fileCount!==t.fileCount||n.unpackedSizeBytes!==t.unpackedSizeBytes||n.sha256!==t.sha256)throw new InstalledNodeTreeMismatchError(`Installed Node toolchain does not match its trusted tree digest and shape: expected ${JSON.stringify(t)}, received ${JSON.stringify(n)}`)}function M(o,t,e){if(b.posix.isAbsolute(t)||b.win32.isAbsolute(t)||t.includes("\0"))throw new Error(`Node toolchain symbolic link has an absolute target at ${JSON.stringify(o)}`);const n=b.posix.normalize(b.posix.join(b.posix.dirname(o),t));if(e==="."?n===".."||n.startsWith("../"):n!==e&&!n.startsWith(`${e}/`))throw new Error(`Node toolchain symbolic link escapes its root at ${JSON.stringify(o)}`)}function G(o){const t=o.split("/");if(t.length>D)throw new Error("Node archive path has too many segments");const e=[];for(let n=1;n<t.length;n+=1)e.push(t.slice(0,n).join("/"));return e}function P(o,t,e,n){const s=n.archive;if(o>s.entryCount||t>s.fileCount||e>s.unpackedSizeBytes)throw B(o,t,e,n)}function J(o,t,e,n){if(o>n.maxEntries||t>n.maxFiles||e>n.maxUnpackedSizeBytes)throw new Error("Installed Node toolchain exceeds its locked tree shape")}function B(o,t,e,n){return new Error(`Node ${n.version} archive shape mismatch: received ${o.toString()} entries, ${t.toString()} files, and ${e.toString()} unpacked bytes`)}async function X(o,t,e){if(p(e),t<=F)return Y(o,e);const n=O.createHash("sha256"),s=u.createReadStream(o,{highWaterMark:q}),a=()=>s.destroy(e?.reason);e?.addEventListener("abort",a,{once:!0});try{for await(const h of s)p(e),n.update(h);return p(e),n.digest("hex")}finally{e?.removeEventListener("abort",a),s.destroy(),s.closed||await new Promise(h=>s.once("close",h))}}async function Y(o,t){const e=await u.promises.open(o,"r");try{const n=O.createHash("sha256"),s=Buffer.allocUnsafe(F);for(;;){p(t);const{bytesRead:a}=await e.read(s,0,s.length,null);if(p(t),a===0)return n.digest("hex");n.update(s.subarray(0,a))}}finally{await e.close()}}function R(o,t){return o<t?-1:o>t?1:0}
@@ -1,2 +1,2 @@
1
- import V from"node:crypto";import u from"node:fs";import d from"node:path";import{TarArchiveInspector as z,ZipArchiveExtractor as M}from"./archive.js";import{withProcessLock as L,withProcessLockProgress as P}from"../process/locks.js";import{createContainedTemporaryDirectory as $,existingRealInstallDirectory as B,NODE_TOOLCHAIN_CONTENT_DIRECTORY as R,NODE_TOOLCHAIN_MARKER_FILENAME as j,nodeToolchainCacheKey as H,prepareNodeToolchainCache as F,readRealManifestFile as p,removeContainedInstallDirectory as U,requireContainedExecutable as v,requireRealContentDirectory as Y}from"./node-cache.js";import{InstalledNodeTreeMismatchError as q,validateNodeArchiveEntries as J,verifyInstalledNodeTree as g}from"./node-integrity.js";import{createSharedToolchainOperation as K,throwIfSignalAborted as m,waitForPromise as f,waitForSharedToolchainOperation as W}from"./shared-operation.js";import{TOOLCHAIN_METADATA_TIMEOUT_MS as G}from"./timeouts.js";import{isUnknownRecord as Z}from"../kernel/unknown-values.js";const w=2,E=300*1e3,Q=Object.freeze([100,250,500,1e3,2e3,3e3,3e3]),y=new Map;export async function provisionNodeToolchainWithRelease({archiveTimeoutMs:e=E,commandExecutor:r,fetchImpl:t,productDir:n,release:o,signal:i,zipExtractor:a}){m(i);const s=F(n,o),l=H(s);let c=y.get(l);if(c===void 0){c=K(k=>X({archiveTimeoutMs:e,commandExecutor:r,fetchImpl:t,paths:s,productDir:n,release:o,signal:k,zipExtractor:a})),y.set(l,c);const h=c;c.promise.then(()=>{I(l,h)},()=>{I(l,h)})}return W(c,i)}export async function extractTrustedNodeArchive({archivePath:e,commandExecutor:r,destination:t,release:n,signal:o,timeoutMs:i=E,zipExtractor:a=new M}){if(le(i),u.existsSync(t))throw new Error(`Node archive destination already exists: ${t}`);u.mkdirSync(t,{mode:448});const s=AbortSignal.timeout(i),l=o===void 0?s:AbortSignal.any([o,s]),c=n.archive.format==="zip"?a:new z;try{const h=await c.inspect(e,n.archive.format,{maxEntries:n.archive.entryCount,maxUnpackedSizeBytes:n.archive.unpackedSizeBytes,signal:l});J(h,n),n.archive.format==="zip"?await a.extract(e,t,n.archive.format,{maxEntries:n.archive.entryCount,maxUnpackedSizeBytes:n.archive.unpackedSizeBytes,signal:l}):(await r("tar",["-xzf",e,"-C",t],{cwd:d.dirname(t),env:process.env,signal:l,timeoutMs:i}),m(l))}catch(h){throw u.rmSync(t,{recursive:!0,force:!0}),h}}async function X(e){let r=null;const t=await b(e.paths,e.release,e.commandExecutor,e.signal,n=>{r=n});return t!==null?t:L(e.paths.lockPath,()=>P(`installing Node ${e.release.version}`,async()=>{if(m(e.signal),r===null||!T(e.paths,r)){const n=await b(e.paths,e.release,e.commandExecutor,e.signal);if(n!==null)return n}return U(e.paths),ee(e)}),{signal:e.signal,timeoutMs:null})}async function ee(e){const r=$(e.paths.rootDirectory,`.tmp-${e.release.version}`),t=e.release.archive.format==="zip"?".zip":".tar.gz",n=d.join(r,`node${t}`),o=d.join(r,"extracted"),i=d.join(r,"publish"),a=d.join(i,R);try{const s=await f(e.fetchImpl(e.release.archive.url,{signal:e.signal}),e.signal);if(!s.ok)throw new Error(`Failed to download ${e.release.archive.url}: HTTP ${String(s.status)}`);await te(s,n,e.release,e.signal),await extractTrustedNodeArchive({archivePath:n,commandExecutor:e.commandExecutor,destination:o,release:e.release,signal:e.signal,timeoutMs:e.archiveTimeoutMs,zipExtractor:e.zipExtractor});const l=d.join(o,e.release.archive.rootDirectory);se(o,l),await g(l,e.release.installed,e.signal),u.mkdirSync(i,{mode:448}),u.renameSync(l,a),u.rmSync(o,{recursive:!0,force:!0}),u.rmSync(n,{force:!0});const c=x(a,e.release),h=await D(a,c,e.release,e.commandExecutor,e.signal);return oe(d.join(i,j),ie(e.release,h.npmVersion)),await renameNodeToolchainPublishDirectory(i,e.paths.installDirectory,e.signal),{binDir:S(e.paths.contentDirectory,e.release),nodeExecutable:d.join(e.paths.contentDirectory,c.nodeRelativePath),npmVersion:h.npmVersion,toolchainDir:e.paths.installDirectory}}finally{u.rmSync(r,{recursive:!0,force:!0})}}export async function renameNodeToolchainPublishDirectory(e,r,t){for(let n=0;;n+=1){m(t);try{await u.promises.rename(e,r);return}catch(o){const i=nodeToolchainPublishRetryDelay(o,process.platform,n);if(i===null||re(r))throw o;await f(new Promise(a=>setTimeout(a,i)),t)}}}function re(e){try{return u.lstatSync(e).isDirectory()&&u.readdirSync(e).length>0}catch{return!1}}export function nodeToolchainPublishRetryDelay(e,r,t){if(r!=="win32"||!(e instanceof Error))return null;const n=e.code;return n!=="EACCES"&&n!=="EBUSY"&&n!=="EPERM"?null:Q[t]??null}async function b(e,r,t,n,o){if(!B(e))return null;const i=p(e);if(i===null)return null;const a=ae(i);if(a===null||!ce(a,r))return null;Y(e);const s=o?N(e,i):null;try{await g(e.contentDirectory,r.installed,n)}catch(h){return m(n),h instanceof q&&s!==null&&T(e,s)&&o?.(s),null}const l=x(e.contentDirectory,r),c=await D(e.contentDirectory,l,r,t,n);return c.npmVersion!==a.npmVersion?null:{binDir:S(e.contentDirectory,r),nodeExecutable:l.nodeExecutable,npmVersion:c.npmVersion,toolchainDir:e.installDirectory}}function N(e,r){try{const t=u.lstatSync(e.installDirectory,{bigint:!0});return!t.isDirectory()||t.isSymbolicLink()||t.ino===0n||t.birthtimeNs===0n?null:{birthtimeNs:t.birthtimeNs,dev:t.dev,ino:t.ino,manifestSource:r}}catch{return null}}function T(e,r){try{const t=N(e,r.manifestSource);return t!==null&&t.birthtimeNs===r.birthtimeNs&&t.dev===r.dev&&t.ino===r.ino&&p(e)===r.manifestSource}catch{return!1}}async function te(e,r,t,n){const o=[];let i=0;if(e.body!==void 0&&e.body!==null){const l=e.body[Symbol.asyncIterator]();for(;;){const c=await f(l.next(),n);if(c.done)break;if(!(c.value instanceof Uint8Array))throw new Error("Node toolchain download yielded a non-binary chunk");const h=Buffer.from(c.value.buffer,c.value.byteOffset,c.value.byteLength);if(i+=h.byteLength,i>t.archive.sizeBytes)throw await l.return?.(),new Error(`Node toolchain archive exceeds its trusted ${t.archive.sizeBytes.toString()} byte size`);o.push(h)}}else{const l=await f(e.arrayBuffer(),n),c=Buffer.from(l);i=c.byteLength,i<=t.archive.sizeBytes&&o.push(c)}if(i!==t.archive.sizeBytes)throw new Error(`Node toolchain archive size mismatch: expected ${t.archive.sizeBytes.toString()}, received ${i.toString()}`);const a=Buffer.concat(o,i),s=V.createHash("sha256").update(a).digest("hex");if(s!==t.archive.sha256)throw new Error(`Node toolchain archive digest mismatch: expected ${t.archive.sha256}, received ${s}`);m(n),u.writeFileSync(r,a,{flag:"wx",mode:384})}function x(e,r){const t=r.target.platform==="win32"?"node.exe":"bin/node",n=r.target.platform==="win32"?"npm.cmd":"bin/npm";return{nodeExecutable:v(e,t,{allowSymbolicLink:!1}),nodeRelativePath:t,npmCli:r.target.platform==="win32"?v(e,"node_modules/npm/bin/npm-cli.js",{allowSymbolicLink:!1}):null,npmExecutable:v(e,n,{allowSymbolicLink:r.target.platform!=="win32"})}}async function D(e,r,t,n,o){const i=S(e,t),a=await O(r.nodeExecutable,e,i,n,o);if(a!==t.version)throw new Error(`Downloaded Node toolchain version mismatch: expected ${t.version}, received ${a}`);return{npmVersion:await O(r.npmCli===null?r.npmExecutable:r.nodeExecutable,e,i,n,o,r.npmCli===null?[]:[r.npmCli])}}async function O(e,r,t,n,o,i=[]){const a=nodeToolchainVersionInvocation(r,t,process.env,i),s=await n(e,a.args,{...a.options,signal:o});return ne(typeof s.stdout=="string"?s.stdout:Buffer.from(s.stdout??"").toString())}export function nodeToolchainVersionInvocation(e,r,t,n=[]){const o=(t.PATH??"").split(d.delimiter).filter(i=>i.length>0&&i!==r);return{args:[...n,"--version"],options:{cwd:e,env:{...t,PATH:[r,...o].join(d.delimiter)},stdio:"pipe",timeoutMs:G,windowsHide:!0}}}function ne(e){const r=e.trim().replace(/^v/u,"");if(!/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/u.test(r))throw new Error(`Toolchain command returned an invalid version: ${JSON.stringify(e)}`);return r}function S(e,r){return r.target.platform==="win32"?e:d.join(e,"bin")}function ie(e,r){return{arch:e.target.arch,archiveSha256:e.archive.sha256,installedSha256:e.installed.sha256,npmVersion:r,platform:e.target.platform,schemaVersion:w,version:e.version}}function oe(e,r){u.writeFileSync(e,`${JSON.stringify(r,null,2)}
2
- `,{encoding:"utf8",flag:"wx",mode:384})}function ae(e){let r;try{r=JSON.parse(e)}catch{return null}if(!Z(r))return null;const t=["arch","archiveSha256","installedSha256","npmVersion","platform","schemaVersion","version"],n=Object.keys(r).filter(o=>o!=="kind").sort();return n.length!==t.length||n.some((o,i)=>o!==t[i])||r.schemaVersion!==w||!A(r.arch)||!C(r.archiveSha256)||!C(r.installedSha256)||!_(r.npmVersion)||!A(r.platform)||!_(r.version)?null:{arch:r.arch,archiveSha256:r.archiveSha256,installedSha256:r.installedSha256,npmVersion:r.npmVersion,platform:r.platform,schemaVersion:w,version:r.version}}function ce(e,r){return e.version===r.version&&e.platform===r.target.platform&&e.arch===r.target.arch&&e.archiveSha256===r.archive.sha256&&e.installedSha256===r.installed.sha256}function se(e,r){const t=u.readdirSync(e);if(t.length!==1||t[0]!==d.basename(r)||!u.lstatSync(r).isDirectory()||u.lstatSync(r).isSymbolicLink())throw new Error(`Expected Node archive to contain only ${d.basename(r)}`)}function I(e,r){y.get(e)===r&&y.delete(e)}function le(e){if(!Number.isSafeInteger(e)||e<=0)throw new Error("Node archive timeout must be a positive safe integer")}function A(e){return typeof e=="string"&&e.length>0}function C(e){return typeof e=="string"&&/^[a-f0-9]{64}$/u.test(e)}function _(e){return typeof e=="string"&&/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/u.test(e)}
1
+ import V from"node:crypto";import u from"node:fs";import d from"node:path";import{TarArchiveInspector as z,ZipArchiveExtractor as M}from"./archive.js";import{withProcessLock as L,withProcessLockProgress as $}from"../process/locks.js";import{createContainedTemporaryDirectory as B,existingRealInstallDirectory as P,NODE_TOOLCHAIN_CONTENT_DIRECTORY as R,NODE_TOOLCHAIN_MARKER_FILENAME as j,nodeToolchainCacheKey as H,prepareNodeToolchainCache as F,readRealManifestFile as p,removeContainedInstallDirectory as U,requireContainedExecutable as v,requireRealContentDirectory as Y}from"./node-cache.js";import{InstalledNodeTreeMismatchError as J,validateNodeArchiveEntries as K,verifyInstalledNodeTree as g}from"./node-integrity.js";import{createSharedToolchainOperation as W,throwIfSignalAborted as m,waitForPromise as f,waitForSharedToolchainOperation as q}from"./shared-operation.js";import{TOOLCHAIN_METADATA_TIMEOUT_MS as G}from"./timeouts.js";import{isUnknownRecord as Z}from"../kernel/unknown-values.js";const w=2,E=300*1e3,Q=Object.freeze([100,250,500,1e3,2e3,3e3,3e3]),y=new Map;export async function provisionNodeToolchainWithRelease({archiveTimeoutMs:e=E,commandExecutor:r,fetchImpl:t,productDir:n,release:o,signal:i,zipExtractor:a}){m(i);const s=F(n,o),l=H(s);let c=y.get(l);if(c===void 0){c=W(k=>X({archiveTimeoutMs:e,commandExecutor:r,fetchImpl:t,paths:s,productDir:n,release:o,signal:k,zipExtractor:a})),y.set(l,c);const h=c;c.promise.then(()=>{I(l,h)},()=>{I(l,h)})}return q(c,i)}export async function extractTrustedNodeArchive({archivePath:e,commandExecutor:r,destination:t,release:n,signal:o,timeoutMs:i=E,zipExtractor:a=new M}){if(u.existsSync(t))throw new Error(`Node archive destination already exists: ${t}`);u.mkdirSync(t,{mode:448});const s=AbortSignal.timeout(i),l=o===void 0?s:AbortSignal.any([o,s]),c=n.archive.format==="zip"?a:new z;try{const h=await c.inspect(e,n.archive.format,{maxEntries:n.archive.entryCount,maxUnpackedSizeBytes:n.archive.unpackedSizeBytes,signal:l});K(h,n),n.archive.format==="zip"?await a.extract(e,t,n.archive.format,{maxEntries:n.archive.entryCount,maxUnpackedSizeBytes:n.archive.unpackedSizeBytes,signal:l}):(await r("tar",["-xzf",e,"-C",t],{cwd:d.dirname(t),env:process.env,signal:l,timeoutMs:i}),m(l))}catch(h){throw u.rmSync(t,{recursive:!0,force:!0}),h}}async function X(e){let r=null;const t=await b(e.paths,e.release,e.commandExecutor,e.signal,n=>{r=n});return t!==null?t:L(e.paths.lockPath,()=>$(`installing Node ${e.release.version}`,async()=>{if(m(e.signal),r===null||!x(e.paths,r)){const n=await b(e.paths,e.release,e.commandExecutor,e.signal);if(n!==null)return n}return U(e.paths),ee(e)}),{signal:e.signal,timeoutMs:null})}async function ee(e){const r=B(e.paths.rootDirectory,`.tmp-${e.release.version}`),t=e.release.archive.format==="zip"?".zip":".tar.gz",n=d.join(r,`node${t}`),o=d.join(r,"extracted"),i=d.join(r,"publish"),a=d.join(i,R);try{const s=await f(e.fetchImpl(e.release.archive.url,{signal:e.signal}),e.signal);if(!s.ok)throw new Error(`Failed to download ${e.release.archive.url}: HTTP ${String(s.status)}`);await te(s,n,e.release,e.signal),await extractTrustedNodeArchive({archivePath:n,commandExecutor:e.commandExecutor,destination:o,release:e.release,signal:e.signal,timeoutMs:e.archiveTimeoutMs,zipExtractor:e.zipExtractor});const l=d.join(o,e.release.archive.rootDirectory);se(o,l),await g(l,e.release.installed,e.signal),u.mkdirSync(i,{mode:448}),u.renameSync(l,a),u.rmSync(o,{recursive:!0,force:!0}),u.rmSync(n,{force:!0});const c=T(a,e.release),h=await D(a,c,e.release,e.commandExecutor,e.signal);return oe(d.join(i,j),ie(e.release,h.npmVersion)),await renameNodeToolchainPublishDirectory(i,e.paths.installDirectory,e.signal),{binDir:S(e.paths.contentDirectory,e.release),nodeExecutable:d.join(e.paths.contentDirectory,c.nodeRelativePath),npmVersion:h.npmVersion,toolchainDir:e.paths.installDirectory}}finally{u.rmSync(r,{recursive:!0,force:!0})}}export async function renameNodeToolchainPublishDirectory(e,r,t){for(let n=0;;n+=1){m(t);try{await u.promises.rename(e,r);return}catch(o){const i=nodeToolchainPublishRetryDelay(o,process.platform,n);if(i===null||re(r))throw o;await f(new Promise(a=>setTimeout(a,i)),t)}}}function re(e){try{return u.lstatSync(e).isDirectory()&&u.readdirSync(e).length>0}catch{return!1}}export function nodeToolchainPublishRetryDelay(e,r,t){if(r!=="win32"||!(e instanceof Error))return null;const n=e.code;return n!=="EACCES"&&n!=="EBUSY"&&n!=="EPERM"?null:Q[t]??null}async function b(e,r,t,n,o){if(!P(e))return null;const i=p(e);if(i===null)return null;const a=ae(i);if(a===null||!ce(a,r))return null;Y(e);const s=o?N(e,i):null;try{await g(e.contentDirectory,r.installed,n)}catch(h){return m(n),h instanceof J&&s!==null&&x(e,s)&&o?.(s),null}const l=T(e.contentDirectory,r),c=await D(e.contentDirectory,l,r,t,n);return c.npmVersion!==a.npmVersion?null:{binDir:S(e.contentDirectory,r),nodeExecutable:l.nodeExecutable,npmVersion:c.npmVersion,toolchainDir:e.installDirectory}}function N(e,r){try{const t=u.lstatSync(e.installDirectory,{bigint:!0});return!t.isDirectory()||t.isSymbolicLink()||t.ino===0n||t.birthtimeNs===0n?null:{birthtimeNs:t.birthtimeNs,dev:t.dev,ino:t.ino,manifestSource:r}}catch{return null}}function x(e,r){try{const t=N(e,r.manifestSource);return t!==null&&t.birthtimeNs===r.birthtimeNs&&t.dev===r.dev&&t.ino===r.ino&&p(e)===r.manifestSource}catch{return!1}}async function te(e,r,t,n){const o=[];let i=0;if(e.body!==void 0&&e.body!==null){const l=e.body[Symbol.asyncIterator]();for(;;){const c=await f(l.next(),n);if(c.done)break;if(!(c.value instanceof Uint8Array))throw new Error("Node toolchain download yielded a non-binary chunk");const h=Buffer.from(c.value.buffer,c.value.byteOffset,c.value.byteLength);if(i+=h.byteLength,i>t.archive.sizeBytes)throw await l.return?.(),new Error(`Node toolchain archive exceeds its trusted ${t.archive.sizeBytes.toString()} byte size`);o.push(h)}}else{const l=await f(e.arrayBuffer(),n),c=Buffer.from(l);i=c.byteLength,i<=t.archive.sizeBytes&&o.push(c)}if(i!==t.archive.sizeBytes)throw new Error(`Node toolchain archive size mismatch: expected ${t.archive.sizeBytes.toString()}, received ${i.toString()}`);const a=Buffer.concat(o,i),s=V.createHash("sha256").update(a).digest("hex");if(s!==t.archive.sha256)throw new Error(`Node toolchain archive digest mismatch: expected ${t.archive.sha256}, received ${s}`);m(n),u.writeFileSync(r,a,{flag:"wx",mode:384})}function T(e,r){const t=r.target.platform==="win32"?"node.exe":"bin/node",n=r.target.platform==="win32"?"npm.cmd":"bin/npm";return{nodeExecutable:v(e,t,{allowSymbolicLink:!1}),nodeRelativePath:t,npmCli:r.target.platform==="win32"?v(e,"node_modules/npm/bin/npm-cli.js",{allowSymbolicLink:!1}):null,npmExecutable:v(e,n,{allowSymbolicLink:r.target.platform!=="win32"})}}async function D(e,r,t,n,o){const i=S(e,t),a=await O(r.nodeExecutable,e,i,n,o);if(a!==t.version)throw new Error(`Downloaded Node toolchain version mismatch: expected ${t.version}, received ${a}`);return{npmVersion:await O(r.npmCli===null?r.npmExecutable:r.nodeExecutable,e,i,n,o,r.npmCli===null?[]:[r.npmCli])}}async function O(e,r,t,n,o,i=[]){const a=nodeToolchainVersionInvocation(r,t,process.env,i),s=await n(e,a.args,{...a.options,signal:o});return ne(typeof s.stdout=="string"?s.stdout:Buffer.from(s.stdout??"").toString())}export function nodeToolchainVersionInvocation(e,r,t,n=[]){const o=(t.PATH??"").split(d.delimiter).filter(i=>i.length>0&&i!==r);return{args:[...n,"--version"],options:{cwd:e,env:{...t,PATH:[r,...o].join(d.delimiter)},stdio:"pipe",timeoutMs:G,windowsHide:!0}}}function ne(e){const r=e.trim().replace(/^v/u,"");if(!/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/u.test(r))throw new Error(`Toolchain command returned an invalid version: ${JSON.stringify(e)}`);return r}function S(e,r){return r.target.platform==="win32"?e:d.join(e,"bin")}function ie(e,r){return{arch:e.target.arch,archiveSha256:e.archive.sha256,installedSha256:e.installed.sha256,npmVersion:r,platform:e.target.platform,schemaVersion:w,version:e.version}}function oe(e,r){u.writeFileSync(e,`${JSON.stringify(r,null,2)}
2
+ `,{encoding:"utf8",flag:"wx",mode:384})}function ae(e){let r;try{r=JSON.parse(e)}catch{return null}if(!Z(r))return null;const t=["arch","archiveSha256","installedSha256","npmVersion","platform","schemaVersion","version"],n=Object.keys(r).filter(o=>o!=="kind").sort();return n.length!==t.length||n.some((o,i)=>o!==t[i])||r.schemaVersion!==w||!A(r.arch)||!C(r.archiveSha256)||!C(r.installedSha256)||!_(r.npmVersion)||!A(r.platform)||!_(r.version)?null:{arch:r.arch,archiveSha256:r.archiveSha256,installedSha256:r.installedSha256,npmVersion:r.npmVersion,platform:r.platform,schemaVersion:w,version:r.version}}function ce(e,r){return e.version===r.version&&e.platform===r.target.platform&&e.arch===r.target.arch&&e.archiveSha256===r.archive.sha256&&e.installedSha256===r.installed.sha256}function se(e,r){const t=u.readdirSync(e);if(t.length!==1||t[0]!==d.basename(r)||!u.lstatSync(r).isDirectory()||u.lstatSync(r).isSymbolicLink())throw new Error(`Expected Node archive to contain only ${d.basename(r)}`)}function I(e,r){y.get(e)===r&&y.delete(e)}function A(e){return typeof e=="string"&&e.length>0}function C(e){return typeof e=="string"&&/^[a-f0-9]{64}$/u.test(e)}function _(e){return typeof e=="string"&&/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/u.test(e)}
@@ -1 +1 @@
1
- import{readDynamicProperty as f,isUnknownRecord as S}from"../kernel/unknown-values.js";const c=String;export function normalizeSemver(r){return formatSemver(parseSemver(r))}export function isExactSemver(r){try{return parseSemver(r),!0}catch{return!1}}export function parseSemver(r){const e=c(r||"").trim().replace(/^v/iu,"").match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?$/u);if(!e)throw u(r);const o=Number.parseInt(e[1],10),n=Number.parseInt(e[2]||"0",10),i=Number.parseInt(e[3]||"0",10);if(!s(o)||!s(n)||!s(i))throw u(r);return{major:o,minor:n,patch:i}}export function compareSemver(r,t){const e=m(r),o=m(t);return e.major!==o.major?e.major-o.major:e.minor!==o.minor?e.minor-o.minor:e.patch-o.patch}export function formatSemver(r){const t=m(r);return`${c(t.major)}.${c(t.minor)}.${c(t.patch)}`}export function satisfiesSemver(r,t){const e=d(t);if(!e)return!0;const o=m(r);for(const n of j(e))if(x(o,n))return!0;return!1}export function deriveSemverMinimum(r){const t=d(r);if(!t)return null;const e=[];for(const o of j(t)){const n=w(o);n!==null&&e.push(n)}return e.length===0?null:(e.sort(compareSemver),formatSemver(e[0]))}function x(r,t){return l(t).every(o=>v(r,o))}function w(r){const t=l(r);if(t.length===0)return null;let e=null;for(const n of t){if(n.operator==="="||n.operator===">="){e=h(e,n.version);continue}n.operator===">"&&(e=h(e,b(n.version)))}const o=e||{major:0,minor:0,patch:0};return t.every(n=>v(o,n))?o:null}function l(r){const t=[];for(const e of $(r))t.push(...g(e));return t}function $(r){return r.trim().replace(/([<>=])\s+(?=v?\d)/giu,"$1").split(/\s+/u).filter(Boolean)}function g(r){const t=r.trim();if(!t)return[];const e=t.match(/^(<=|>=|<|>|=)?\s*(.*)$/u);if(!e)throw new Error(`Unsupported semver comparator "${t}"`);const o=N(e[1]),n=e[2];if(o!==null){if(!n)throw new Error(`Unsupported semver comparator "${t}"`);return[{operator:o,version:parseSemver(n)}]}if(n.startsWith("^")){const i=n.slice(1),a=parseSemver(i);return[{operator:">=",version:a},{operator:"<",version:y(a,I(i))}]}if(n.startsWith("~")){const i=parseSemver(n.slice(1));return[{operator:">=",version:i},{operator:"<",version:{major:i.major,minor:i.minor+1,patch:0}}]}if(/^\d+(?:\.(?:x|\*))?$/iu.test(n)){const[i]=n.split("."),a=parseSemver(i);return[{operator:">=",version:a},{operator:"<",version:{major:a.major+1,minor:0,patch:0}}]}if(/^\d+\.\d+(?:\.(?:x|\*))?$/iu.test(n)){const[i,a]=n.split("."),p=parseSemver(`${i}.${a}`);return[{operator:">=",version:p},{operator:"<",version:{major:p.major,minor:p.minor+1,patch:0}}]}return[{operator:"=",version:parseSemver(n)}]}function v(r,t){const e=compareSemver(r,t.version);switch(t.operator){case"=":return e===0;case">":return e>0;case">=":return e>=0;case"<":return e<0;case"<=":return e<=0}}function y(r,t){return r.major>0||t<=1?{major:r.major+1,minor:0,patch:0}:r.minor>0||t===2?{major:0,minor:r.minor+1,patch:0}:{major:0,minor:0,patch:r.patch+1}}function I(r){return r.trim().replace(/^v/iu,"").split(".").length}function b(r){return{major:r.major,minor:r.minor,patch:r.patch+1}}function h(r,t){return r&&compareSemver(r,t)>=0?r:t}function m(r){return typeof r=="string"?parseSemver(r):E(r)}function E(r){if(!S(r))throw u(r);const t=f(r,"major"),e=f(r,"minor"),o=f(r,"patch");if(!s(t)||!s(e)||!s(o))throw u(r);return{major:t,minor:e,patch:o}}function N(r){switch(r){case"=":case">":case">=":case"<":case"<=":return r;case void 0:return null;default:throw new Error(`Unsupported semver operator "${r}"`)}}function d(r){return c(r||"").trim()}function j(r){return r.split("||").map(t=>t.trim()).filter(Boolean)}function u(r){return new Error(`Invalid semver "${c(r)}"`)}function s(r){return typeof r=="number"&&Number.isInteger(r)&&r>=0}
1
+ const c=String;export function normalizeSemver(r){return formatSemver(parseSemver(r))}export function isExactSemver(r){try{return parseSemver(r),!0}catch{return!1}}export function parseSemver(r){const e=c(r||"").trim().replace(/^v/iu,"").match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?$/u);if(!e)throw h(r);const o=Number.parseInt(e[1],10),n=Number.parseInt(e[2]||"0",10),i=Number.parseInt(e[3]||"0",10);if(!m(o)||!m(n)||!m(i))throw h(r);return{major:o,minor:n,patch:i}}export function compareSemver(r,t){return r.major!==t.major?r.major-t.major:r.minor!==t.minor?r.minor-t.minor:r.patch-t.patch}export function formatSemver(r){return`${c(r.major)}.${c(r.minor)}.${c(r.patch)}`}export function satisfiesSemver(r,t){const e=l(t);if(!e)return!0;const o=parseSemver(r);for(const n of v(e))if(d(o,n))return!0;return!1}export function deriveSemverMinimum(r){const t=l(r);if(!t)return null;const e=[];for(const o of v(t)){const n=j(o);n!==null&&e.push(n)}return e.length===0?null:(e.sort(compareSemver),formatSemver(e[0]))}function d(r,t){return u(t).every(o=>p(r,o))}function j(r){const t=u(r);if(t.length===0)return null;let e=null;for(const n of t){if(n.operator==="="||n.operator===">="){e=f(e,n.version);continue}n.operator===">"&&(e=f(e,g(n.version)))}const o=e||{major:0,minor:0,patch:0};return t.every(n=>p(o,n))?o:null}function u(r){const t=[];for(const e of x(r))t.push(...S(e));return t}function x(r){return r.trim().replace(/([<>=])\s+(?=v?\d)/giu,"$1").split(/\s+/u).filter(Boolean)}function S(r){const t=r.trim();if(!t)return[];const e=t.match(/^(<=|>=|<|>|=)?\s*(.*)$/u);if(!e)throw new Error(`Unsupported semver comparator "${t}"`);const o=E(e[1]),n=e[2];if(o!==null){if(!n)throw new Error(`Unsupported semver comparator "${t}"`);return[{operator:o,version:parseSemver(n)}]}if(n.startsWith("^")){const i=n.slice(1),a=parseSemver(i);return[{operator:">=",version:a},{operator:"<",version:w(a,$(i))}]}if(n.startsWith("~")){const i=parseSemver(n.slice(1));return[{operator:">=",version:i},{operator:"<",version:{major:i.major,minor:i.minor+1,patch:0}}]}if(/^\d+(?:\.(?:x|\*))?$/iu.test(n)){const[i]=n.split("."),a=parseSemver(i);return[{operator:">=",version:a},{operator:"<",version:{major:a.major+1,minor:0,patch:0}}]}if(/^\d+\.\d+(?:\.(?:x|\*))?$/iu.test(n)){const[i,a]=n.split("."),s=parseSemver(`${i}.${a}`);return[{operator:">=",version:s},{operator:"<",version:{major:s.major,minor:s.minor+1,patch:0}}]}return[{operator:"=",version:parseSemver(n)}]}function p(r,t){const e=compareSemver(r,t.version);switch(t.operator){case"=":return e===0;case">":return e>0;case">=":return e>=0;case"<":return e<0;case"<=":return e<=0}}function w(r,t){return r.major>0||t<=1?{major:r.major+1,minor:0,patch:0}:r.minor>0||t===2?{major:0,minor:r.minor+1,patch:0}:{major:0,minor:0,patch:r.patch+1}}function $(r){return r.trim().replace(/^v/iu,"").split(".").length}function g(r){return{major:r.major,minor:r.minor,patch:r.patch+1}}function f(r,t){return r&&compareSemver(r,t)>=0?r:t}function E(r){switch(r){case"=":case">":case">=":case"<":case"<=":return r;case void 0:return null;default:throw new Error(`Unsupported semver operator "${r}"`)}}function l(r){return c(r||"").trim()}function v(r){return r.split("||").map(t=>t.trim()).filter(Boolean)}function h(r){return new Error(`Invalid semver "${c(r)}"`)}function m(r){return typeof r=="number"&&Number.isInteger(r)&&r>=0}
@@ -1 +1 @@
1
- import{createTrackerHttp as f,TRACKER_PAGE_LIMIT as h,TrackerError as g}from"./http.js";import{commentRequest as I,commitStatusRequest as T,getRequest as k,TRACKER_USER_AGENT as w,trackerRepository as L,trackerRoutes as E}from"./requests.js";import{decodeGiteaVersion as R,decodeTrackerComment as S,decodeTrackerComments as G,decodeTrackerCommitStatus as A,decodeTrackerIssue as C,decodeTrackerIssues as $,decodeTrackerLabel as x,decodeTrackerLabels as q,missingIssueLabels as O,unlabelledIssueError as P}from"./responses.js";import{closeDuplicateIssue as N,closeIssue as _,commenterLogin as j}from"./ownership.js";const v="50";export function giteaHeaders(u){return{Accept:"application/json",Authorization:`token ${u}`,"User-Agent":w}}export function giteaRequests(u){const r=E(u),n=(e,l,d={})=>k(e,{...d,limit:v,page:String(l)});return{routes:r,createIssue:(e,l)=>({body:{body:e.body,labels:l,title:e.title},method:"POST",url:r.issues}),createLabel:e=>({body:{color:`#${e.color}`,description:e.description,name:e.name},method:"POST",url:r.labels}),deleteLabel:e=>({method:"DELETE",url:`${r.labels}/${String(e)}`}),listComments:(e,l)=>n(r.comments(e),l),listLabels:e=>n(r.labels,e),listOpenIssues:(e,l)=>n(r.issues,l,{...e===null?{}:{labels:e},state:"open",type:"issues"})}}export async function readGiteaVersion(u){const n=await f({headers:{Accept:"application/json","User-Agent":w},label:"Gitea version probe",secrets:[]}).exchange({method:"GET",url:`${u}/api/v1/version`});return n.ok?R(n.body):null}function z(u){const r=u.get("x-total-count");return r!==null&&/^\d+$/u.test(r)?Number(r):Number.POSITIVE_INFINITY}export function createGiteaTracker(u){const r=L(u),n=giteaRequests(r),e=f({headers:giteaHeaders(u.token),label:"Gitea",secrets:[u.token]});async function l(s,i,c){const o=new Map;for(let t=1;t<=h;t++){const a=await e.send(s(t)),p=o.size;for(const m of i(a.body,a.source))o.has(c(m))||o.set(c(m),m);if(o.size===p||o.size>=z(a.headers))return[...o.values()]}throw new g("rejected",`Gitea returned more than ${String(h)} pages`)}const d=()=>l(n.listLabels,q,s=>s.id);return{target:r,closeDuplicateIssue(s,i){return N(e,n.routes,s,i,"gitea")},async createIssue(s){const i=s.labels.length===0?[]:await d(),c=s.labels.map(m=>{const b=y(i,m);if(b===void 0)throw new g("rejected",`Gitea repository ${r.owner}/${r.name} has no label named ${JSON.stringify(m)}`);return b.id}),o=await e.send(n.createIssue(s,c)),{issue:t}=C(o.body,o.source),a=O(t,s.labels);if(a.length===0)return t;const p=await _(e,n.routes,t,"gitea");throw P("Gitea",t,a,r,p)},async ensureLabels(s){if(s.length===0)return;const i=new Set((await d()).map(t=>t.name)),c=[];for(const t of s){if(i.has(t.name))continue;const a=await e.send(n.createLabel(t));c.push(x(a.body,a.source)),i.add(t.name)}if(c.length===0)return;const o=await d();for(const t of c){if(y(o,t.name)?.id===t.id)continue;const a=await e.exchange(n.deleteLabel(t.id));if(!a.ok&&a.status!==404)throw e.failure(a)}},async listOpenIssues(s){const i=o=>l(t=>n.listOpenIssues(o,t),$,t=>t.issue.number);let c=await i(s);return(await d()).filter(o=>o.name===s).length>1&&(c=await i(null)),c.filter(o=>!o.pullRequest&&o.issue.labels.includes(s)).map(o=>o.issue)},async setCommitStatus(s){const i=await e.send(T(n.routes,s));return{creator:A(i.body,i.source).creator}},async upsertPullRequestComment(s){const i=await l(a=>n.listComments(s.pullRequest,a),G,a=>a.id),c=s.author??await j(e,r.apiUrl),o=c===null?void 0:i.find(a=>a.body.startsWith(s.marker)&&a.author===c),t=await e.send(I(n.routes,s,o));return{created:o===void 0,url:S(t.body,t.source).url}}}}function y(u,r){return u.filter(n=>n.name===r).sort((n,e)=>n.id-e.id)[0]}
1
+ import{createTrackerHttp as f,TRACKER_PAGE_LIMIT as h,TrackerError as g}from"./http.js";import{commentRequest as I,commitStatusRequest as k,getRequest as T,TRACKER_USER_AGENT as w,trackerRepository as S,trackerRoutes as L}from"./requests.js";import{decodeGiteaVersion as R,decodeTrackerComment as C,decodeTrackerComments as E,decodeTrackerCommitStatus as G,decodeTrackerCommitStatuses as x,isMissingObjectRejection as A,decodeTrackerIssue as $,decodeTrackerIssues as q,decodeTrackerLabel as O,decodeTrackerLabels as P,missingIssueLabels as j,unlabelledIssueError as N}from"./responses.js";import{closeDuplicateIssue as _,closeIssue as v,commenterLogin as z}from"./ownership.js";const U="50";export function giteaHeaders(l){return{Accept:"application/json",Authorization:`token ${l}`,"User-Agent":w}}export function giteaRequests(l){const n=L(l),o=(e,c,m={})=>T(e,{...m,limit:U,page:String(c)});return{routes:n,createIssue:(e,c)=>({body:{body:e.body,labels:c,title:e.title},method:"POST",url:n.issues}),createLabel:e=>({body:{color:`#${e.color}`,description:e.description,name:e.name},method:"POST",url:n.labels}),deleteLabel:e=>({method:"DELETE",url:`${n.labels}/${String(e)}`}),listComments:(e,c)=>o(n.comments(e),c),listCommitStatuses:(e,c)=>o(n.commitStatuses(e),c),listLabels:e=>o(n.labels,e),listOpenIssues:(e,c)=>o(n.issues,c,{...e===null?{}:{labels:e},state:"open",type:"issues"})}}export async function readGiteaVersion(l){const o=await f({headers:{Accept:"application/json","User-Agent":w},label:"Gitea version probe",secrets:[]}).exchange({method:"GET",url:`${l}/api/v1/version`});return o.ok?R(o.body):null}function D(l){const n=l.get("x-total-count");return n!==null&&/^\d+$/u.test(n)?Number(n):Number.POSITIVE_INFINITY}export function createGiteaTracker(l){const n=S(l),o=giteaRequests(n),e=f({headers:giteaHeaders(l.token),label:"Gitea",secrets:[l.token]});async function c(s,r,u,i=()=>!1){const t=new Map;for(let a=1;a<=h;a++){const d=await e.exchange(s(a));if(!d.ok){if(a===1&&i(d))return[];throw e.failure(d)}const b=t.size;for(const p of r(d.body,d.source))t.has(u(p))||t.set(u(p),p);if(t.size===b||t.size>=D(d.headers))return[...t.values()]}throw new g("rejected",`Gitea returned more than ${String(h)} pages`)}const m=()=>c(o.listLabels,P,s=>s.id);return{target:n,closeDuplicateIssue(s,r){return _(e,o.routes,s,r,"gitea")},async createIssue(s){const r=s.labels.length===0?[]:await m(),u=s.labels.map(b=>{const p=y(r,b);if(p===void 0)throw new g("rejected",`Gitea repository ${n.owner}/${n.name} has no label named ${JSON.stringify(b)}`);return p.id}),i=await e.send(o.createIssue(s,u)),{issue:t}=$(i.body,i.source),a=j(t,s.labels);if(a.length===0)return t;const d=await v(e,o.routes,t,"gitea");throw N("Gitea",t,a,n,d)},async ensureLabels(s){if(s.length===0)return;const r=new Set((await m()).map(t=>t.name)),u=[];for(const t of s){if(r.has(t.name))continue;const a=await e.send(o.createLabel(t));u.push(O(a.body,a.source)),r.add(t.name)}if(u.length===0)return;const i=await m();for(const t of u){if(y(i,t.name)?.id===t.id)continue;const a=await e.exchange(o.deleteLabel(t.id));if(!a.ok&&a.status!==404)throw e.failure(a)}},async listOpenIssues(s){const r=i=>c(t=>o.listOpenIssues(i,t),q,t=>t.issue.number);let u=await r(s);return(await m()).filter(i=>i.name===s).length>1&&(u=await r(null)),u.filter(i=>!i.pullRequest&&i.issue.labels.includes(s)).map(i=>i.issue)},listCommitStatuses(s){return c(r=>o.listCommitStatuses(s,r),x,r=>r.id,r=>r.status===404&&A(r.body))},async setCommitStatus(s){const r=await e.send(k(o.routes,s));return{creator:G(r.body,r.source).creator}},async upsertPullRequestComment(s){const r=await c(a=>o.listComments(s.pullRequest,a),E,a=>a.id),u=s.author??await z(e,n.apiUrl),i=u===null?void 0:r.find(a=>a.body.startsWith(s.marker)&&a.author===u),t=await e.send(I(o.routes,s,i));return{created:i===void 0,url:C(t.body,t.source).url}}}}function y(l,n){return l.filter(o=>o.name===n).sort((o,e)=>o.id-e.id)[0]}
@@ -1 +1 @@
1
- import{createTrackerHttp as g,nextPageLink as h,TRACKER_PAGE_LIMIT as m,TrackerError as b}from"./http.js";import{commentRequest as w,commitStatusRequest as f,getRequest as d,TRACKER_USER_AGENT as L,trackerRepository as I,trackerRoutes as k}from"./requests.js";import{decodeTrackerComment as y,decodeTrackerComments as R,decodeTrackerCommitStatus as T,decodeTrackerIssue as C,decodeTrackerIssues as E,decodeTrackerLabel as x,decodeTrackerLabels as A,isDuplicateLabelRejection as P,missingIssueLabels as G,unlabelledIssueError as S}from"./responses.js";import{closeDuplicateIssue as _,closeIssue as q,commenterLogin as U}from"./ownership.js";const p="100",H=100;export function githubHeaders(i){return{Accept:"application/vnd.github+json",Authorization:`Bearer ${i}`,"User-Agent":L,"X-GitHub-Api-Version":"2022-11-28"}}export function githubRequests(i){const u=k(i);return{routes:u,createIssue:e=>({body:{body:e.body,labels:e.labels,title:e.title},method:"POST",url:u.issues}),createLabel:e=>({body:{color:e.color,description:Array.from(e.description).slice(0,H).join(""),name:e.name},method:"POST",url:u.labels}),listComments:e=>d(u.comments(e),{per_page:p}),listLabels:()=>d(u.labels,{per_page:p}),listOpenIssues:e=>d(u.issues,{labels:e,per_page:p,state:"open"}),nextPage(e){const a=h(e);if(a===null)return null;if(!URL.canParse(a)||new URL(a).origin!==new URL(i.apiUrl).origin)throw new b("rejected",`GitHub returned a next page outside ${i.apiUrl}`);return{method:"GET",url:a}}}}export function createGithubTracker(i){const u=I(i),e=githubRequests(u),a=g({headers:githubHeaders(i.token),label:"GitHub",secrets:[i.token]});async function l(s,o){const t=[];let r=s;for(let n=0;r!==null;n++){if(n===m)throw new b("rejected",`GitHub returned more than ${String(m)} pages`);const c=await a.send(r);t.push(...o(c.body,c.source)),r=e.nextPage(c.headers.get("link"))}return t}return{target:u,closeDuplicateIssue(s,o){return _(a,e.routes,s,o,"github")},async createIssue(s){const o=await a.send(e.createIssue(s)),{issue:t}=C(o.body,o.source),r=G(t,s.labels);if(r.length===0)return t;const n=await q(a,e.routes,t,"github");throw S("GitHub",t,r,u,n)},async ensureLabels(s){if(s.length===0)return;const o=new Set((await l(e.listLabels(),A)).map(t=>t.name.toLowerCase()));for(const t of s){const r=t.name.toLowerCase();if(o.has(r))continue;const n=await a.exchange(e.createLabel(t));if(n.ok)x(n.body,n.source);else if(n.status!==422||!P(n.body))throw a.failure(n);o.add(r)}},async listOpenIssues(s){const o=s.toLowerCase(),t=new Map;for(const r of await l(e.listOpenIssues(s),E))r.pullRequest||!r.issue.labels.some(n=>n.toLowerCase()===o)||t.set(r.issue.number,r.issue);return[...t.values()]},async setCommitStatus(s){const o=await a.send(f(e.routes,s));return{creator:T(o.body,o.source).creator}},async upsertPullRequestComment(s){const o=await l(e.listComments(s.pullRequest),R),t=s.author??await U(a,u.apiUrl),r=t===null?void 0:o.find(c=>c.body.startsWith(s.marker)&&c.author===t),n=await a.send(w(e.routes,s,r));return{created:r===void 0,url:y(n.body,n.source).url}}}}
1
+ import{createTrackerHttp as g,nextPageLink as h,TRACKER_PAGE_LIMIT as p,TrackerError as b}from"./http.js";import{commentRequest as w,commitStatusRequest as f,getRequest as d,TRACKER_USER_AGENT as L,trackerRepository as k,trackerRoutes as I}from"./requests.js";import{decodeTrackerComment as T,decodeTrackerComments as y,decodeTrackerCommitStatus as R,decodeTrackerCommitStatuses as C,decodeTrackerIssue as S,decodeTrackerIssues as E,decodeTrackerLabel as x,decodeTrackerLabels as A,isDuplicateLabelRejection as P,missingIssueLabels as _,unlabelledIssueError as G}from"./responses.js";import{closeDuplicateIssue as q,closeIssue as U,commenterLogin as H}from"./ownership.js";const m="100",O=100;export function githubHeaders(u){return{Accept:"application/vnd.github+json",Authorization:`Bearer ${u}`,"User-Agent":L,"X-GitHub-Api-Version":"2022-11-28"}}export function githubRequests(u){const i=I(u);return{routes:i,createIssue:e=>({body:{body:e.body,labels:e.labels,title:e.title},method:"POST",url:i.issues}),createLabel:e=>({body:{color:e.color,description:Array.from(e.description).slice(0,O).join(""),name:e.name},method:"POST",url:i.labels}),listComments:e=>d(i.comments(e),{per_page:m}),listCommitStatuses:e=>d(i.commitStatuses(e),{per_page:m}),listLabels:()=>d(i.labels,{per_page:m}),listOpenIssues:e=>d(i.issues,{labels:e,per_page:m,state:"open"}),nextPage(e){const a=h(e);if(a===null)return null;if(!URL.canParse(a)||new URL(a).origin!==new URL(u.apiUrl).origin)throw new b("rejected",`GitHub returned a next page outside ${u.apiUrl}`);return{method:"GET",url:a}}}}export function createGithubTracker(u){const i=k(u),e=githubRequests(i),a=g({headers:githubHeaders(u.token),label:"GitHub",secrets:[u.token]});async function l(t,o){const s=[];let r=t;for(let n=0;r!==null;n++){if(n===p)throw new b("rejected",`GitHub returned more than ${String(p)} pages`);const c=await a.send(r);s.push(...o(c.body,c.source)),r=e.nextPage(c.headers.get("link"))}return s}return{target:i,closeDuplicateIssue(t,o){return q(a,e.routes,t,o,"github")},async createIssue(t){const o=await a.send(e.createIssue(t)),{issue:s}=S(o.body,o.source),r=_(s,t.labels);if(r.length===0)return s;const n=await U(a,e.routes,s,"github");throw G("GitHub",s,r,i,n)},async ensureLabels(t){if(t.length===0)return;const o=new Set((await l(e.listLabels(),A)).map(s=>s.name.toLowerCase()));for(const s of t){const r=s.name.toLowerCase();if(o.has(r))continue;const n=await a.exchange(e.createLabel(s));if(n.ok)x(n.body,n.source);else if(n.status!==422||!P(n.body))throw a.failure(n);o.add(r)}},listCommitStatuses(t){return l(e.listCommitStatuses(t),C)},async listOpenIssues(t){const o=t.toLowerCase(),s=new Map;for(const r of await l(e.listOpenIssues(t),E))r.pullRequest||!r.issue.labels.some(n=>n.toLowerCase()===o)||s.set(r.issue.number,r.issue);return[...s.values()]},async setCommitStatus(t){const o=await a.send(f(e.routes,t));return{creator:R(o.body,o.source).creator}},async upsertPullRequestComment(t){const o=await l(e.listComments(t.pullRequest),y),s=t.author??await H(a,i.apiUrl),r=s===null?void 0:o.find(c=>c.body.startsWith(t.marker)&&c.author===s),n=await a.send(w(e.routes,t,r));return{created:r===void 0,url:T(n.body,n.source).url}}}}
@@ -1 +1 @@
1
- export const TRACKER_USER_AGENT="testkit";export function trackerRepository(t){return{apiUrl:t.apiUrl.replace(/\/+$/u,""),name:t.name,owner:t.owner,provider:t.provider}}export function trackerRoutes(t){const e=`${t.apiUrl}/repos/${encodeURIComponent(t.owner)}/${encodeURIComponent(t.name)}`;return{comment:o=>`${e}/issues/comments/${String(o)}`,comments:o=>`${e}/issues/${String(o)}/comments`,issues:`${e}/issues`,labels:`${e}/labels`,status:o=>`${e}/statuses/${encodeURIComponent(o)}`}}export function getRequest(t,e){return{method:"GET",url:`${t}?${new URLSearchParams(e).toString()}`}}export function commitStatusRequest(t,e){return{body:{context:e.context,description:e.description,state:e.state,...e.targetUrl===null?{}:{target_url:e.targetUrl}},method:"POST",url:t.status(e.sha)}}export function commentRequest(t,e,o){return o===void 0?{body:{body:e.body},method:"POST",url:t.comments(e.pullRequest)}:{body:{body:e.body},method:"PATCH",url:t.comment(o.id)}}
1
+ export const TRACKER_USER_AGENT="testkit";export function trackerRepository(t){return{apiUrl:t.apiUrl.replace(/\/+$/u,""),name:t.name,owner:t.owner,provider:t.provider}}export function trackerRoutes(t){const e=`${t.apiUrl}/repos/${encodeURIComponent(t.owner)}/${encodeURIComponent(t.name)}`;return{comment:o=>`${e}/issues/comments/${String(o)}`,comments:o=>`${e}/issues/${String(o)}/comments`,commitStatuses:o=>`${e}/commits/${encodeURIComponent(o)}/statuses`,issues:`${e}/issues`,labels:`${e}/labels`,status:o=>`${e}/statuses/${encodeURIComponent(o)}`}}export function getRequest(t,e){return{method:"GET",url:`${t}?${new URLSearchParams(e).toString()}`}}export function commitStatusRequest(t,e){return{body:{context:e.context,description:e.description,state:e.state,...e.targetUrl===null?{}:{target_url:e.targetUrl}},method:"POST",url:t.status(e.sha)}}export function commentRequest(t,e,o){return o===void 0?{body:{body:e.body},method:"POST",url:t.comments(e.pullRequest)}:{body:{body:e.body},method:"PATCH",url:t.comment(o.id)}}
@@ -1 +1 @@
1
- import{isUnknownRecord as s}from"../kernel/unknown-values.js";import{TrackerError as c}from"./http.js";export function decodeTrackerLabel(e,t){const n=o(e,t,"label");return{id:l(n.id,t,"label.id"),name:i(n.name,t,"label.name")}}export function decodeTrackerLabels(e,t){return a(e,t,"labels").map(n=>decodeTrackerLabel(n,t))}export function decodeTrackerIssue(e,t){const n=o(e,t,"issue");return{issue:{body:m(n.body,t,"issue.body"),labels:(n.labels===void 0?[]:a(n.labels,t,"issue.labels")).map(r=>typeof r=="string"?r:i(o(r,t,"issue.labels[]").name,t,"issue.labels[].name")),number:l(n.number,t,"issue.number"),title:i(n.title,t,"issue.title"),url:f(n.html_url,t,"issue.html_url")},pullRequest:n.pull_request!==void 0&&n.pull_request!==null}}export function missingIssueLabels(e,t){const n=new Set(e.labels.map(r=>r.toLowerCase()));return t.filter(r=>!n.has(r.toLowerCase()))}export function unlabelledIssueError(e,t,n,r,b){const p=`${r.owner}/${r.name}`,h=b?"Testkit closed that issue. Run this again":"Testkit could not close that issue. Close it, then run this again";return new c("forbidden",`${e} created ${t.url} without these labels: ${n.join(", ")}. The token's user may not label issues in ${p}. ${h} with a token whose user may label issues.`)}export function decodeTrackerIssues(e,t){return a(e,t,"issues").map(n=>decodeTrackerIssue(n,t))}export function decodeTrackerComment(e,t){const n=o(e,t,"comment");return{author:d(n.user),body:m(n.body,t,"comment.body"),id:l(n.id,t,"comment.id"),url:f(n.html_url,t,"comment.html_url")}}export function decodeTrackerUserLogin(e,t){return i(o(e,t,"user").login,t,"user.login")}export function decodeTrackerComments(e,t){return a(e,t,"comments").map(n=>decodeTrackerComment(n,t))}export function decodeTrackerCommitStatus(e,t){const n=o(e,t,"commit status");return{context:i(n.context,t,"commit status.context"),creator:d(n.creator)}}function d(e){return s(e)&&typeof e.login=="string"?e.login:null}export function isDuplicateLabelRejection(e){return!s(e)||!Array.isArray(e.errors)?!1:e.errors.some(t=>s(t)&&t.code==="already_exists")}export function decodeGiteaVersion(e){return s(e)&&typeof e.version=="string"&&e.version.length>0?e.version:null}function u(e,t,n){return new c("rejected",`${e} returned a response Testkit cannot read: ${t} must be ${n}`)}function o(e,t,n){if(!s(e))throw u(t,n,"an object");return e}function a(e,t,n){if(!Array.isArray(e))throw u(t,n,"a list");return e}function i(e,t,n){if(typeof e!="string")throw u(t,n,"a string");return e}function m(e,t,n){return e==null?"":i(e,t,n)}function l(e,t,n){if(typeof e!="number"||!Number.isSafeInteger(e)||e<=0)throw u(t,n,"a positive integer");return e}function f(e,t,n){const r=i(e,t,n);if(!/^https?:\/\/[^\s/]/u.test(r)||!URL.canParse(r))throw u(t,n,"an http or https URL");return r}
1
+ import{isUnknownRecord as o}from"../kernel/unknown-values.js";import{TrackerError as m}from"./http.js";export function decodeTrackerLabel(e,t){const n=s(e,t,"label");return{id:l(n.id,t,"label.id"),name:i(n.name,t,"label.name")}}export function decodeTrackerLabels(e,t){return a(e,t,"labels").map(n=>decodeTrackerLabel(n,t))}export function decodeTrackerIssue(e,t){const n=s(e,t,"issue");return{issue:{body:c(n.body,t,"issue.body"),labels:(n.labels===void 0?[]:a(n.labels,t,"issue.labels")).map(r=>typeof r=="string"?r:i(s(r,t,"issue.labels[]").name,t,"issue.labels[].name")),number:l(n.number,t,"issue.number"),title:i(n.title,t,"issue.title"),url:f(n.html_url,t,"issue.html_url")},pullRequest:n.pull_request!==void 0&&n.pull_request!==null}}export function missingIssueLabels(e,t){const n=new Set(e.labels.map(r=>r.toLowerCase()));return t.filter(r=>!n.has(r.toLowerCase()))}export function unlabelledIssueError(e,t,n,r,p){const b=`${r.owner}/${r.name}`,g=p?"Testkit closed that issue. Run this again":"Testkit could not close that issue. Close it, then run this again";return new m("forbidden",`${e} created ${t.url} without these labels: ${n.join(", ")}. The token's user may not label issues in ${b}. ${g} with a token whose user may label issues.`)}export function decodeTrackerIssues(e,t){return a(e,t,"issues").map(n=>decodeTrackerIssue(n,t))}export function decodeTrackerComment(e,t){const n=s(e,t,"comment");return{author:d(n.user),body:c(n.body,t,"comment.body"),id:l(n.id,t,"comment.id"),url:f(n.html_url,t,"comment.html_url")}}export function decodeTrackerUserLogin(e,t){return i(s(e,t,"user").login,t,"user.login")}export function decodeTrackerComments(e,t){return a(e,t,"comments").map(n=>decodeTrackerComment(n,t))}export function decodeTrackerCommitStatus(e,t){const n=s(e,t,"commit status");return{context:i(n.context,t,"commit status.context"),creator:d(n.creator)}}export function decodeTrackerCommitStatuses(e,t){return a(e,t,"commit statuses").map(n=>{const r=s(n,t,"commit status");return{context:i(r.context,t,"commit status.context"),createdAt:typeof r.created_at=="string"?r.created_at:null,description:c(r.description,t,"commit status.description"),id:l(r.id,t,"commit status.id"),state:i(r.state??r.status,t,"commit status.state"),targetUrl:typeof r.target_url=="string"&&r.target_url.length>0?r.target_url:null}})}function d(e){return o(e)&&typeof e.login=="string"?e.login:null}export function isDuplicateLabelRejection(e){return!o(e)||!Array.isArray(e.errors)?!1:e.errors.some(t=>o(t)&&t.code==="already_exists")}export function isMissingObjectRejection(e){return JSON.stringify(e??null).includes("object does not exist")}export function decodeGiteaVersion(e){return o(e)&&typeof e.version=="string"&&e.version.length>0?e.version:null}function u(e,t,n){return new m("rejected",`${e} returned a response Testkit cannot read: ${t} must be ${n}`)}function s(e,t,n){if(!o(e))throw u(t,n,"an object");return e}function a(e,t,n){if(!Array.isArray(e))throw u(t,n,"a list");return e}function i(e,t,n){if(typeof e!="string")throw u(t,n,"a string");return e}function c(e,t,n){return e==null?"":i(e,t,n)}function l(e,t,n){if(typeof e!="number"||!Number.isSafeInteger(e)||e<=0)throw u(t,n,"a positive integer");return e}function f(e,t,n){const r=i(e,t,n);if(!/^https?:\/\/[^\s/]/u.test(r)||!URL.canParse(r))throw u(t,n,"an http or https URL");return r}
@@ -292,9 +292,9 @@ When Testkit cannot read the issues, every failure is new.
292
292
  | Target URL | The CI run's URL, when the CI provider gives one. |
293
293
  | Description | See below. At most 140 characters. |
294
294
 
295
- The description has one of two forms.
295
+ The description starts with the run's outcome, in one of three forms.
296
296
 
297
- | Run | Description |
297
+ | Run | Outcome |
298
298
  | -------------------------------- | -------------------------- |
299
299
  | Every file passed | `142 files passed` |
300
300
  | Tests failed | `3 failed: 1 new, 2 known` |
@@ -302,6 +302,22 @@ The description has one of two forms.
302
302
 
303
303
  The counts are failures. A script failure counts as one failure. An unnamed failure counts as one new failure.
304
304
 
305
+ After the outcome, separated by `·`, comes what the run covered:
306
+
307
+ ```text
308
+ <outcome> · <scope> of <code>[ in <product>] · <platform>-<arch> Node <version> · Testkit <version>
309
+ 142 files passed · full suite of tree 3f9a2c1b07de4a5b6c7d in apps/api · linux-x64 Node v24.11.0 · Testkit 0.6.7
310
+ ```
311
+
312
+ | Part | Value |
313
+ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
314
+ | Scope | `full suite` when the run had no file, group, suite, type, or Vitest project filter. `selected files` otherwise. |
315
+ | Code | `this commit` for a clean checkout of the status's commit, `tree <hex>` with the first 20 hex digits of a merge commit's tree, `a modified checkout`, or `a supplied environment`. |
316
+ | Product | The product directory relative to the repository root, when it isn't the root. |
317
+ | Platform | Node's `process.platform` and `process.arch`, the Node version, and the Testkit version. |
318
+
319
+ Testkit shortens a long outcome to make room for the rest. When too little room is left, the description is the outcome alone. `testkit evidence` reads only descriptions in this exact form.
320
+
305
321
  Testkit redacts the secrets it recognises in the reason. [Issue body](#issue-body) lists the forms.
306
322
 
307
323
  A run result stored before Testkit 0.5.0 can keep the rest of a secret after a `<redacted>`. The description therefore ends at the first `<redacted>` in the stored reason, keeping a quote that directly follows it. A redacted URL password is the exception. `testkit run` always redacted a URL password whole, so the description continues past it.
@@ -1,3 +1,39 @@
1
+ # 0.6.7
2
+
3
+ ## `testkit evidence` tells you which results still hold
4
+
5
+ `testkit evidence` reports, for each test file, whether a recorded result
6
+ applies to the code as it is now, so an agent or a developer can decide what to
7
+ run instead of running the whole suite to find out. Each file is `passed`,
8
+ `failed`, or `skipped` on identical inputs, `inconsistent` when identical inputs
9
+ both passed and failed it, `stale` with the exact changes since its newest
10
+ result, or `unrun`. The report ends with the files that need a run and a
11
+ `testkit run` command for them.
12
+
13
+ To support it, every run now records its inputs under `inputs` in `run.json`:
14
+ the commit and the object ID of every file in the repository that differs from
15
+ it, the env files Testkit loads, the dependency install records, the platform,
16
+ CPU architecture, Node and Testkit versions, Git line-ending settings, and the
17
+ options that change how tests run. Testkit reads the files again when the run
18
+ ends and records any that changed while it ran. `testkit evidence` reads the
19
+ runs of every worktree of the repository, so a new worktree sees the main
20
+ checkout's results, and it checks each `run.json` against its manifest first.
21
+
22
+ `testkit ci report` now adds what the run covered to its commit status
23
+ description, such as `412 files passed · full suite of this commit · linux-x64
24
+ Node v24.11.0 · Testkit 0.6.7`, and `testkit evidence` reads those statuses for
25
+ HEAD and the nine commits before it. The campaign brief tells the agent to run
26
+ `testkit evidence` before it runs tests.
27
+
28
+ # 0.6.6
29
+
30
+ ## Data-flow evidence preserves own JSON properties
31
+
32
+ `dataFlow.record()` now preserves an own `__proto__` property in saved JSON
33
+ evidence, including in nested objects and arrays. Recording and artifact
34
+ validation previously dropped that property. Sensitive values remain redacted,
35
+ and recording leaves the caller's data unchanged.
36
+
1
37
  # 0.6.5
2
38
 
3
39
  ## `testkit bug bisect` finds the commit that broke a recorded test