release-skill 0.2.6 → 0.2.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 (51) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codebuddy-plugin/plugin.json +1 -1
  4. package/.codex-plugin/plugin.json +2 -2
  5. package/.kimi-plugin/plugin.json +1 -1
  6. package/CHANGELOG.md +21 -0
  7. package/INSTALL.md +12 -12
  8. package/INSTALL.zh-CN.md +10 -10
  9. package/README.md +26 -11
  10. package/README.zh-CN.md +22 -11
  11. package/adapters/claude/.claude-plugin/marketplace.json +1 -1
  12. package/adapters/claude/.claude-plugin/plugin.json +1 -1
  13. package/adapters/claude/bin/release-skill.bundle.mjs +1579 -1186
  14. package/adapters/claude/skills/release-prepare/SKILL.md +5 -0
  15. package/adapters/claude/skills/release-setup/SKILL.md +10 -1
  16. package/adapters/claude/skills/release-verify/SKILL.md +4 -2
  17. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  18. package/adapters/codex/bin/release-skill.bundle.mjs +1579 -1186
  19. package/adapters/codex/skills/release-prepare/SKILL.md +5 -0
  20. package/adapters/codex/skills/release-setup/SKILL.md +10 -1
  21. package/adapters/codex/skills/release-verify/SKILL.md +4 -2
  22. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  23. package/adapters/kimi/bin/release-skill.bundle.mjs +1579 -1186
  24. package/adapters/kimi/skills/release-prepare/SKILL.md +5 -0
  25. package/adapters/kimi/skills/release-setup/SKILL.md +10 -1
  26. package/adapters/kimi/skills/release-verify/SKILL.md +4 -2
  27. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  28. package/adapters/workbuddy/bin/release-skill.bundle.mjs +1579 -1186
  29. package/adapters/workbuddy/skills/release-prepare/SKILL.md +5 -0
  30. package/adapters/workbuddy/skills/release-setup/SKILL.md +10 -1
  31. package/adapters/workbuddy/skills/release-verify/SKILL.md +4 -2
  32. package/bin/release-skill.bundle.mjs +1579 -1186
  33. package/package.json +1 -1
  34. package/skills/release-prepare/SKILL.md +5 -0
  35. package/skills/release-setup/SKILL.md +10 -1
  36. package/skills/release-verify/SKILL.md +4 -2
  37. package/skills-src/release-prepare/SKILL.md +5 -0
  38. package/skills-src/release-setup/SKILL.md +10 -1
  39. package/skills-src/release-verify/SKILL.md +4 -2
  40. package/src/adapters/npm.mjs +54 -2
  41. package/src/adapters/plugin-marketplace.mjs +4 -29
  42. package/src/commands/prepare.mjs +3 -3
  43. package/src/commands/publish.mjs +2 -2
  44. package/src/commands/reconcile.mjs +39 -0
  45. package/src/commands/setup.mjs +168 -0
  46. package/src/commands/verify.mjs +28 -1
  47. package/src/npm/npm-entry-closure.mjs +195 -0
  48. package/src/platforms/codebuddy.mjs +19 -11
  49. package/src/platforms/codex.mjs +18 -10
  50. package/src/platforms/kimi.mjs +26 -39
  51. package/src/snapshot/frozen.mjs +51 -19
@@ -9,7 +9,7 @@ const __bundlePkgRoot = __bundleResolve(__bundleDirname(__bundleFileURLToPath(im
9
9
  // Provide a real require() for CJS packages bundled into ESM (e.g. yaml, ajv).
10
10
  const __bundleRealRequire = __bundleCreateRequire(import.meta.url);
11
11
  // Package identity injected at build time — closure-independent --version probe.
12
- const __bundlePkg = Object.freeze({"name":"release-skill","version":"0.2.6"});
12
+ const __bundlePkg = Object.freeze({"name":"release-skill","version":"0.2.7"});
13
13
 
14
14
  var __create = Object.create;
15
15
  var __defProp = Object.defineProperty;
@@ -17087,7 +17087,7 @@ function kimiAuthorityDir(context, planDigest, plugin) {
17087
17087
  throw new Error(`kimi attestation authority requires a safe plugin id: "${plugin}"`);
17088
17088
  }
17089
17089
  const base = resolve4(context.root, ".release-skill", "kimi-attestations");
17090
- const dir = resolve4(base, planDigest, plugin);
17090
+ const dir = resolve4(base, plugin);
17091
17091
  const rel = relative2(base, dir);
17092
17092
  const sep6 = process.platform === "win32" ? "\\" : "/";
17093
17093
  if (rel === "" || rel === ".." || isAbsolute2(rel) || rel.startsWith(`..${sep6}`) || rel.split(sep6).some((segment) => segment === ".." || segment === "")) {
@@ -17104,17 +17104,16 @@ function buildKimiManualInstructions({
17104
17104
  version,
17105
17105
  ref,
17106
17106
  attestationDir,
17107
- requiresInstalledClosure,
17108
- managedRoot
17107
+ requiresInstalledClosure
17109
17108
  }) {
17110
17109
  return [
17111
17110
  `Kimi Code \u6CA1\u6709\u53EF\u811A\u672C\u5316\u7684\u63D2\u4EF6\u5B89\u88C5\u547D\u4EE4\u884C\u5DE5\u5177\uFF1B\u5B89\u88C5\u662F\u624B\u52A8\u4EA4\u4E92\u6B65\u9AA4\u3002`,
17112
17111
  `1) publish \u5B8C\u6210\u6240\u6709\u8FDC\u7AEF\u5199\u5165\u540E\u8FDB\u5165 PUBLISHED \u72B6\u6001\uFF08\u81EA\u52A8\u5316 Git \u5206\u652F/\u6807\u7B7E\u3001npm \u548C GitHub Release \u5199\u5165\u5DF2\u5B8C\u6210\uFF09\u3002\u6B64 kimi \u68C0\u67E5\u70B9\u6807\u8BB0\u4E3A\u9700\u8981\u4EBA\u5DE5\u5B89\u88C5\u3002`,
17113
- `2) ${requiresInstalledClosure ? `\u4EE5 KIMI_CODE_HOME="${resolve4(managedRoot, "..", "..")}" \u542F\u52A8 Kimi Code\uFF0C\u7136\u540E` : "\u5728 Kimi Code \u4E2D"}\u8FD0\u884C: /plugins install ${installUrl}\uFF08\u9501\u5B9A\u5230\u51BB\u7ED3 ref "${ref}"\uFF0C\u7248\u672C ${version}\uFF09\u3002\u786E\u8BA4\u63D2\u4EF6 "${plugin}" \u7684\u4FE1\u4EFB\u63D0\u793A\uFF0C\u7136\u540E\u8FD0\u884C /plugins reload\uFF08\u6216 /new\uFF09\u3002`,
17112
+ `2) \u5728 Kimi Code \u4E2D\u8FD0\u884C: /plugins install ${installUrl}\uFF08\u9501\u5B9A\u5230\u51BB\u7ED3 ref "${ref}"\uFF0C\u7248\u672C ${version}\uFF09\u3002\u786E\u8BA4\u63D2\u4EF6 "${plugin}" \u7684\u4FE1\u4EFB\u63D0\u793A\uFF0C\u7136\u540E\u8FD0\u884C /plugins reload\uFF08\u6216 /new\uFF09\u3002`,
17114
17113
  `3) \u5C06\u4EBA\u5DE5\u7ED3\u679C JSON \u5199\u5165: ${attestationDir}/${KIMI_ATTESTATION_FILE}`,
17115
- ` \u5FC5\u586B\u5B57\u6BB5: platform="kimi", version, planDigest\uFF08\u51BB\u7ED3\u8BA1\u5212\u6458\u8981\uFF09, result("passed" \u6216 "failed"), actor\uFF08\u786E\u8BA4\u4EBA\uFF09, confirmedAt\uFF08ISO 8601 \u65F6\u95F4\u6233\uFF09${requiresInstalledClosure ? "\uFF0CinstallPath\uFF08\u5B9E\u9645\u5B89\u88C5\u540E\u7684\u63D2\u4EF6\u76EE\u5F55\uFF0C\u5FC5\u987B\u4F4D\u4E8E\u8BE5\u8BC1\u660E\u76EE\u5F55\u7684 kimi-home/plugins/managed/ \u5185\uFF09" : ""}`,
17114
+ ` \u5FC5\u586B\u5B57\u6BB5: platform="kimi", version, planDigest\uFF08\u51BB\u7ED3\u8BA1\u5212\u6458\u8981\uFF09, result("passed" \u6216 "failed"), actor\uFF08\u786E\u8BA4\u4EBA\uFF09, confirmedAt\uFF08ISO 8601 \u65F6\u95F4\u6233\uFF09${requiresInstalledClosure ? "\uFF0CinstallPath\uFF08\u5B9E\u9645\u5B89\u88C5\u540E\u7684\u63D2\u4EF6\u76EE\u5F55\u7684\u771F\u5B9E\u8DEF\u5F84\uFF09" : ""}`,
17116
17115
  ` \u53EF\u9009\u5B57\u6BB5: note\uFF08\u5907\u6CE8\uFF09`,
17117
- `4) \u8FD0\u884C release-skill reconcile\uFF08\u5BF9\u8D26\u8FDC\u7AEF\u72B6\u6001\u5E76\u8DF3\u8FC7\u5DF2\u5B8C\u6210\u6B65\u9AA4\uFF09\uFF0C\u7136\u540E release-skill verify\uFF08\u4ECE\u540C\u4E00\u4E2A\u8BA1\u5212\u6458\u8981\u7D22\u5F15\u7684\u6743\u5A01\u76EE\u5F55\u8BFB\u53D6\u7ED3\u679C\uFF0C\u6210\u529F\u540E -> VERIFIED\uFF09\u3002`
17116
+ `4) \u82E5\u53D1\u5E03\u8FD0\u884C\u662F PARTIAL\uFF0C\u5148\u8FD0\u884C release-skill reconcile\uFF1B\u82E5\u5DF2\u662F PUBLISHED\uFF0C\u76F4\u63A5\u8FD0\u884C release-skill verify\u3002verify \u4ECE\u8BE5\u63D2\u4EF6\u7684\u7A33\u5B9A\u8BC1\u660E\u76EE\u5F55\u8BFB\u53D6\u5E76\u6821\u9A8C\u5F53\u524D planDigest\uFF0C\u6210\u529F\u540E -> VERIFIED\u3002`
17118
17117
  ];
17119
17118
  }
17120
17119
  async function readKimiManifest(pluginRootReal) {
@@ -17259,15 +17258,13 @@ async function executeKimiManualRequirement(action, context) {
17259
17258
  });
17260
17259
  }
17261
17260
  const requiresInstalledClosure = Boolean(context.plan?.skillResourceClosure);
17262
- const managedRoot = requiresInstalledClosure ? resolve4(attestationDir, "kimi-home", "plugins", "managed") : null;
17263
17261
  const instructions = buildKimiManualInstructions({
17264
17262
  installUrl,
17265
17263
  plugin: action.plugin,
17266
17264
  version: action.version,
17267
17265
  ref,
17268
17266
  attestationDir,
17269
- requiresInstalledClosure,
17270
- managedRoot
17267
+ requiresInstalledClosure
17271
17268
  });
17272
17269
  const requirement = {
17273
17270
  kind: "kimi-manual-install-requirement",
@@ -17288,7 +17285,7 @@ async function executeKimiManualRequirement(action, context) {
17288
17285
  result: '<"passed" or "failed">',
17289
17286
  actor: "<person who confirmed the install>",
17290
17287
  confirmedAt: "<ISO 8601 timestamp>",
17291
- ...requiresInstalledClosure ? { installPath: resolve4(managedRoot, action.plugin) } : {},
17288
+ ...requiresInstalledClosure ? { installPath: "<actual installed plugin directory>" } : {},
17292
17289
  note: "<optional note>"
17293
17290
  },
17294
17291
  instructions
@@ -17305,17 +17302,6 @@ async function executeKimiManualRequirement(action, context) {
17305
17302
  });
17306
17303
  }
17307
17304
  }
17308
- if (requiresInstalledClosure) {
17309
- try {
17310
- await mkdir18(managedRoot, { recursive: true, mode: 448 });
17311
- } catch (mkdirErr) {
17312
- return createResult({
17313
- actionType,
17314
- status: ActionStatus.EXECUTE_FAILED,
17315
- error: `cannot create kimi resource-closure managed root: ${mkdirErr.message}`
17316
- });
17317
- }
17318
- }
17319
17305
  const requirementPath = resolve4(attestationDir, KIMI_REQUIREMENT_FILE);
17320
17306
  let existing = null;
17321
17307
  let requirementMissing = false;
@@ -17351,11 +17337,14 @@ async function executeKimiManualRequirement(action, context) {
17351
17337
  }
17352
17338
  const { createdAt: _existingCreatedAt, ...existingBody } = existing;
17353
17339
  if (canonicalJson(existingBody) !== canonicalJson(requirement)) {
17354
- return createResult({
17355
- actionType,
17356
- status: ActionStatus.EXECUTE_FAILED,
17357
- error: "existing kimi manual-install requirement conflicts with the current frozen action; refusing to overwrite"
17358
- });
17340
+ if (existing.planDigest === planDigest) {
17341
+ return createResult({
17342
+ actionType,
17343
+ status: ActionStatus.EXECUTE_FAILED,
17344
+ error: "existing kimi manual-install requirement conflicts with the current frozen action (same planDigest); refusing to overwrite"
17345
+ });
17346
+ }
17347
+ await writeEvidenceAtomic(requirementPath, { ...requirement, createdAt: (/* @__PURE__ */ new Date()).toISOString() });
17359
17348
  }
17360
17349
  } else {
17361
17350
  await writeEvidenceAtomic(requirementPath, { ...requirement, createdAt: (/* @__PURE__ */ new Date()).toISOString() });
@@ -17438,7 +17427,7 @@ function codebuddyAuthorityDir(context, planDigest, plugin) {
17438
17427
  throw new Error(`codebuddy attestation authority requires a safe plugin id: "${plugin}"`);
17439
17428
  }
17440
17429
  const base = resolve5(context.root, ".release-skill", "codebuddy-attestations");
17441
- const dir = resolve5(base, planDigest, plugin);
17430
+ const dir = resolve5(base, plugin);
17442
17431
  const rel = relative3(base, dir);
17443
17432
  const sep6 = process.platform === "win32" ? "\\" : "/";
17444
17433
  if (rel === "" || rel === ".." || isAbsolute3(rel) || rel.startsWith(`..${sep6}`) || rel.split(sep6).some((segment) => segment === ".." || segment === "")) {
@@ -17708,11 +17697,14 @@ async function executeCodeBuddyManualRequirement(action, context) {
17708
17697
  }
17709
17698
  const { createdAt: _existingCreatedAt, ...existingBody } = existing;
17710
17699
  if (canonicalJson(existingBody) !== canonicalJson(requirement)) {
17711
- return createResult({
17712
- actionType,
17713
- status: ActionStatus.EXECUTE_FAILED,
17714
- error: "existing codebuddy manual-install requirement conflicts with the current frozen action; refusing to overwrite"
17715
- });
17700
+ if (existing.planDigest === planDigest) {
17701
+ return createResult({
17702
+ actionType,
17703
+ status: ActionStatus.EXECUTE_FAILED,
17704
+ error: "existing codebuddy manual-install requirement conflicts with the current frozen action (same planDigest); refusing to overwrite"
17705
+ });
17706
+ }
17707
+ await writeEvidenceAtomic(requirementPath, { ...requirement, createdAt: (/* @__PURE__ */ new Date()).toISOString() });
17716
17708
  }
17717
17709
  } else {
17718
17710
  await writeEvidenceAtomic(requirementPath, { ...requirement, createdAt: (/* @__PURE__ */ new Date()).toISOString() });
@@ -17792,7 +17784,7 @@ function codexAuthorityDir(context, planDigest, plugin) {
17792
17784
  throw new Error(`codex attestation authority requires a safe plugin id: "${plugin}"`);
17793
17785
  }
17794
17786
  const base = resolve6(context.root, ".release-skill", "codex-attestations");
17795
- const dir = resolve6(base, planDigest, plugin);
17787
+ const dir = resolve6(base, plugin);
17796
17788
  const rel = relative4(base, dir);
17797
17789
  const sep6 = process.platform === "win32" ? "\\" : "/";
17798
17790
  if (rel === "" || rel === ".." || isAbsolute4(rel) || rel.startsWith(`..${sep6}`) || rel.split(sep6).some((segment) => segment === ".." || segment === "")) {
@@ -17964,11 +17956,14 @@ async function executeCodexManualRequirement(action, context) {
17964
17956
  }
17965
17957
  const { createdAt: _existingCreatedAt, ...existingBody } = existing;
17966
17958
  if (canonicalJson(existingBody) !== canonicalJson(requirement)) {
17967
- return createResult({
17968
- actionType,
17969
- status: ActionStatus.EXECUTE_FAILED,
17970
- error: "existing codex manual-install requirement conflicts with the current frozen action; refusing to overwrite"
17971
- });
17959
+ if (existing.planDigest === planDigest) {
17960
+ return createResult({
17961
+ actionType,
17962
+ status: ActionStatus.EXECUTE_FAILED,
17963
+ error: "existing codex manual-install requirement conflicts with the current frozen action (same planDigest); refusing to overwrite"
17964
+ });
17965
+ }
17966
+ await writeEvidenceAtomic(requirementPath, { ...requirement, createdAt: (/* @__PURE__ */ new Date()).toISOString() });
17972
17967
  }
17973
17968
  } else {
17974
17969
  await writeEvidenceAtomic(requirementPath, { ...requirement, createdAt: (/* @__PURE__ */ new Date()).toISOString() });
@@ -18526,248 +18521,1029 @@ var init_registry = __esm({
18526
18521
  }
18527
18522
  });
18528
18523
 
18529
- // src/artifacts/safe-fs-backend-internal.mjs
18530
- import { createRequire } from "node:module";
18531
- import { dirname as dirname3, resolve as resolve7 } from "node:path";
18532
- import { fileURLToPath as fileURLToPath2 } from "node:url";
18533
- import { realpath as realpath2 } from "node:fs/promises";
18534
- import { readFileSync as readFileSync3, realpathSync as realpathSync2, lstatSync as lstatSync2 } from "node:fs";
18524
+ // src/snapshot/frozen.mjs
18535
18525
  import { createHash as createHash2 } from "node:crypto";
18536
- function failPrebuildManifest(reason, message = "prebuilds manifest not readable") {
18537
- throw new ReleaseError(
18538
- SAFE_WRITE_UNAVAILABLE,
18539
- message,
18540
- { resource: PREBUILDS_RESOURCE, reason }
18541
- );
18526
+ import { execFile as execFileCb, spawn } from "node:child_process";
18527
+ import { promisify } from "node:util";
18528
+ import {
18529
+ chmod,
18530
+ lstat as lstat4,
18531
+ mkdir as mkdir3,
18532
+ mkdtemp,
18533
+ open as open3,
18534
+ readdir as readdir2,
18535
+ realpath as realpath2,
18536
+ rm as rm3,
18537
+ unlink as unlink2
18538
+ } from "node:fs/promises";
18539
+ import { constants as fsConstants } from "node:fs";
18540
+ import { isAbsolute as isAbsolute5, join as join7, relative as relative5, resolve as resolve7 } from "node:path";
18541
+ function frozenError(message, details = {}) {
18542
+ return new ReleaseError(GATE_FAILED, message, details);
18542
18543
  }
18543
- function readTrustedPrebuildManifest() {
18544
- let lexicalStat;
18545
- try {
18546
- lexicalStat = lstatSync2(PREBUILDS_PATH, { bigint: true });
18547
- } catch {
18548
- failPrebuildManifest("MISSING");
18549
- }
18550
- if (lexicalStat.isSymbolicLink()) {
18551
- failPrebuildManifest("SYMLINK", "prebuilds manifest must not be a symlink");
18552
- }
18553
- if (!lexicalStat.isFile()) {
18554
- failPrebuildManifest("NOT_REGULAR_FILE", "prebuilds manifest is not a regular file");
18555
- }
18556
- if (lexicalStat.nlink !== 1n) {
18557
- failPrebuildManifest("UNEXPECTED_HARDLINK_COUNT", "prebuilds manifest has invalid nlink count");
18558
- }
18559
- let physicalBase;
18560
- let physicalManifest;
18561
- try {
18562
- physicalBase = realpathSync2(NATIVE_BASE);
18563
- physicalManifest = realpathSync2(PREBUILDS_PATH);
18564
- } catch {
18565
- failPrebuildManifest("REALPATH_FAILED");
18544
+ function isInside(parent, candidate) {
18545
+ const rel = relative5(parent, candidate);
18546
+ return rel === "" || !isAbsolute5(rel) && rel !== ".." && !rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`);
18547
+ }
18548
+ async function resolveFrozenPath(root, relativePath, label = "frozen path") {
18549
+ if (!relativePath || typeof relativePath !== "string" || isAbsolute5(relativePath)) {
18550
+ throw frozenError(`${label} must be a non-empty project-relative path`);
18566
18551
  }
18567
- if (!physicalManifest.startsWith(physicalBase + "/") && physicalManifest !== physicalBase) {
18568
- failPrebuildManifest(
18569
- "PHYSICAL_PATH_ESCAPE",
18570
- "prebuilds manifest physical path escapes base directory"
18571
- );
18552
+ const rootReal = await realpath2(root);
18553
+ const lexical = resolve7(rootReal, relativePath);
18554
+ if (!isInside(rootReal, lexical)) {
18555
+ throw frozenError(`${label} escapes project root`, { relativePath });
18572
18556
  }
18573
- try {
18574
- return JSON.parse(readFileSync3(physicalManifest, "utf8"));
18575
- } catch {
18576
- failPrebuildManifest("READ_FAILED");
18557
+ const lexicalStat = await lstat4(lexical).catch((err) => {
18558
+ throw frozenError(`${label} is missing`, { relativePath, cause: err.code });
18559
+ });
18560
+ if (lexicalStat.isSymbolicLink()) {
18561
+ throw frozenError(`${label} must not be a symlink`, { relativePath });
18577
18562
  }
18578
- }
18579
- function sanitizeError(err) {
18580
- if (typeof err?.message === "string") {
18581
- let msg = err.message.replace(/\/[^\s]*:\s*/g, "");
18582
- msg = msg.replace(/\/\S+/g, "<redacted>");
18583
- return msg;
18563
+ const physical = await realpath2(lexical).catch((err) => {
18564
+ throw frozenError(`${label} is missing`, { relativePath, cause: err.code });
18565
+ });
18566
+ if (!isInside(rootReal, physical)) {
18567
+ throw frozenError(`${label} resolves outside project root`, { relativePath });
18584
18568
  }
18585
- return "native addon error";
18569
+ return physical;
18586
18570
  }
18587
- function isWithinPackageBuildDir(addonPath) {
18588
- const buildDir = resolve7(NATIVE_BASE, "build/");
18589
- const resolved = resolve7(addonPath);
18590
- if (!resolved.startsWith(buildDir + "/") && resolved !== buildDir) {
18591
- return null;
18592
- }
18593
- let lstatResult;
18594
- try {
18595
- lstatResult = lstatSync2(addonPath, { bigint: true });
18596
- } catch {
18597
- return null;
18571
+ async function readStableRegularFile(filePath, displayPath) {
18572
+ const before = await lstat4(filePath);
18573
+ if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1) {
18574
+ throw frozenError(`frozen snapshot entry is not a single-link regular file: ${displayPath}`);
18598
18575
  }
18599
- if (lstatResult.isSymbolicLink()) return null;
18600
- if (!lstatResult.isFile()) return null;
18601
- if (lstatResult.nlink !== 1n) return null;
18576
+ const handle = await open3(filePath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
18602
18577
  try {
18603
- const physicalBuildDir = realpathSync2(buildDir);
18604
- const physicalAddon = realpathSync2(addonPath);
18605
- if (!(physicalAddon.startsWith(physicalBuildDir + "/") || physicalAddon === physicalBuildDir)) {
18606
- return null;
18578
+ const opened = await handle.stat();
18579
+ if (!opened.isFile() || opened.nlink !== 1 || opened.dev !== before.dev || opened.ino !== before.ino) {
18580
+ throw frozenError(`frozen snapshot entry changed before read: ${displayPath}`);
18607
18581
  }
18608
- return physicalAddon;
18609
- } catch {
18610
- return null;
18582
+ const bytes = await handle.readFile();
18583
+ const after = await handle.stat();
18584
+ if (after.dev !== opened.dev || after.ino !== opened.ino || after.nlink !== 1 || after.size !== opened.size || after.mtimeMs !== opened.mtimeMs || after.ctimeMs !== opened.ctimeMs) {
18585
+ throw frozenError(`frozen snapshot entry changed during read: ${displayPath}`);
18586
+ }
18587
+ return { bytes, mode: opened.mode };
18588
+ } finally {
18589
+ await handle.close();
18611
18590
  }
18612
18591
  }
18613
- function validateManifestEntry(entry, key) {
18614
- if (typeof entry !== "object" || entry === null) {
18615
- throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, `invalid manifest entry for ${key}`);
18616
- }
18617
- if (typeof entry.path !== "string" || entry.path.length === 0) {
18618
- throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, `manifest entry missing path for ${key}`);
18592
+ async function computeFrozenSnapshot(snapshotDir, { excludeRootEntries = [] } = {}) {
18593
+ const root = await realpath2(snapshotDir);
18594
+ const rootStat = await lstat4(root);
18595
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
18596
+ throw frozenError("frozen snapshot root must be a real directory");
18619
18597
  }
18620
- if (typeof entry.sha256 !== "string" || !/^[0-9a-f]{64}$/.test(entry.sha256)) {
18621
- throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, `manifest entry missing or invalid sha256 for ${key}`);
18598
+ const entries = [];
18599
+ const excludedRootNames = /* @__PURE__ */ new Set();
18600
+ const excludedPathPrefixes = [];
18601
+ for (const entry of excludeRootEntries) {
18602
+ if (typeof entry !== "string") {
18603
+ throw frozenError("frozen snapshot exclusion entries must be strings");
18604
+ }
18605
+ const segments = entry.split("/").filter((segment) => segment !== "" && segment !== ".");
18606
+ if (segments.length === 0 || entry.startsWith("/") || entry.includes("\\") || segments.some((segment) => segment === "..")) {
18607
+ throw frozenError(`frozen snapshot exclusion entry is not a safe relative path: ${JSON.stringify(entry)}`);
18608
+ }
18609
+ if (segments.length === 1) {
18610
+ excludedRootNames.add(segments[0]);
18611
+ } else {
18612
+ excludedPathPrefixes.push(segments.join("/"));
18613
+ }
18622
18614
  }
18623
- if (typeof entry.exports !== "object" || !Array.isArray(entry.exports)) {
18624
- throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, `manifest entry missing exports for ${key}`);
18615
+ async function walk(dir) {
18616
+ const children = await readdir2(dir, { withFileTypes: true });
18617
+ children.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
18618
+ for (const child of children) {
18619
+ if (dir === root && excludedRootNames.has(child.name)) continue;
18620
+ const absolute = join7(dir, child.name);
18621
+ const rel = relative5(root, absolute).split("\\").join("/");
18622
+ if (excludedPathPrefixes.some((prefix) => rel === prefix || rel.startsWith(`${prefix}/`))) continue;
18623
+ const st = await lstat4(absolute);
18624
+ if (st.isSymbolicLink()) {
18625
+ throw frozenError(`frozen snapshot contains symlink: ${rel}`);
18626
+ }
18627
+ if (st.isDirectory()) {
18628
+ await walk(absolute);
18629
+ continue;
18630
+ }
18631
+ const { bytes, mode } = await readStableRegularFile(absolute, rel);
18632
+ entries.push({
18633
+ path: rel,
18634
+ type: "file",
18635
+ mode,
18636
+ size: bytes.length,
18637
+ contentDigest: createHash2("sha256").update(bytes).digest("hex")
18638
+ });
18639
+ }
18625
18640
  }
18626
- const resolved = resolve7(NATIVE_BASE, entry.path);
18627
- const baseDir = NATIVE_BASE;
18628
- if (!resolved.startsWith(baseDir + "/") && resolved !== baseDir) {
18629
- throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, `manifest path escapes base directory for ${key}`);
18641
+ __name(walk, "walk");
18642
+ await walk(root);
18643
+ entries.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
18644
+ const digest = createHash2("sha256").update(JSON.stringify(entries)).digest("hex");
18645
+ return { digest, entries };
18646
+ }
18647
+ async function verifyFrozenSnapshot({ root, snapshotPath, expectedDigest }) {
18648
+ const snapshotDir = await resolveFrozenPath(root, snapshotPath, "frozen snapshot path");
18649
+ const observed = await computeFrozenSnapshot(snapshotDir);
18650
+ if (!expectedDigest || observed.digest !== expectedDigest) {
18651
+ throw frozenError("frozen snapshot digest mismatch", {
18652
+ expectedDigest,
18653
+ observedDigest: observed.digest
18654
+ });
18630
18655
  }
18631
- const sortedActual = [...entry.exports].sort();
18632
- const sortedExpected = [...EXPECTED_EXPORTS].sort();
18633
- if (sortedActual.length !== sortedExpected.length || sortedActual.some((v, i) => v !== sortedExpected[i])) {
18634
- throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, `manifest exports mismatch for ${key}`);
18656
+ return { snapshotDir, ...observed };
18657
+ }
18658
+ async function verifyFrozenFile({ root, filePath, expectedSha256, label = "frozen file" }) {
18659
+ const physical = await resolveFrozenPath(root, filePath, label);
18660
+ const { bytes } = await readStableRegularFile(physical, filePath);
18661
+ const observedSha256 = createHash2("sha256").update(bytes).digest("hex");
18662
+ if (!expectedSha256 || observedSha256 !== expectedSha256) {
18663
+ throw frozenError(`${label} SHA-256 mismatch`, { expectedSha256, observedSha256 });
18635
18664
  }
18665
+ return { physical, observedSha256, size: bytes.length };
18636
18666
  }
18637
- function requireAddon(addonPath) {
18638
- try {
18639
- const require2 = createRequire(resolve7(PKG_ROOT2, "package.json"));
18640
- return require2(addonPath);
18641
- } catch (err) {
18642
- throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, sanitizeError(err));
18667
+ async function verifyFrozenDirectoryStructure(directory, label = "frozen directory") {
18668
+ async function walk(current) {
18669
+ const children = await readdir2(current, { withFileTypes: true });
18670
+ for (const child of children) {
18671
+ const absolute = join7(current, child.name);
18672
+ const st = await lstat4(absolute);
18673
+ if (st.isSymbolicLink()) {
18674
+ throw frozenError(`${label} contains a symlink`);
18675
+ }
18676
+ if (st.isDirectory()) {
18677
+ await walk(absolute);
18678
+ } else if (!st.isFile() || st.nlink !== 1) {
18679
+ throw frozenError(`${label} contains an unsafe non-regular or hardlinked entry`);
18680
+ }
18681
+ }
18643
18682
  }
18683
+ __name(walk, "walk");
18684
+ await walk(directory);
18644
18685
  }
18645
- function readStableAddon(options = {}) {
18646
- const hooks = options._hooks || {};
18647
- const manifest = readTrustedPrebuildManifest();
18648
- if (!manifest.prebuilds || typeof manifest.prebuilds !== "object") {
18649
- throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilds manifest has invalid structure");
18686
+ async function verifyFrozenGitRepository({
18687
+ root,
18688
+ gitObjectDir,
18689
+ commit,
18690
+ tree,
18691
+ parentCommit,
18692
+ commitTimestamp,
18693
+ exec = execFile
18694
+ }) {
18695
+ const gitDir = await resolveFrozenPath(root, gitObjectDir, "frozen git object directory");
18696
+ await verifyFrozenDirectoryStructure(gitDir, "frozen git object directory");
18697
+ const { stdout } = await exec("git", ["--git-dir", gitDir, "rev-parse", `${commit}^{tree}`], { shell: false });
18698
+ if (stdout.trim() !== tree) {
18699
+ throw frozenError("frozen git object tree mismatch", { commit, expectedTree: tree, observedTree: stdout.trim() });
18650
18700
  }
18651
- const key = `${process.platform}-${process.arch}`;
18652
- const entry = manifest.prebuilds[key];
18653
- if (!entry) {
18654
- throw new ReleaseError(
18655
- SAFE_WRITE_UNAVAILABLE,
18656
- `no prebuilt addon for ${key}; build from source with native:build`
18701
+ if (commitTimestamp !== void 0) {
18702
+ const expectedTimestamp = normalizeGitTimestamp(commitTimestamp, "frozen commit timestamp");
18703
+ const { stdout: datesOut } = await exec(
18704
+ "git",
18705
+ ["--git-dir", gitDir, "show", "-s", "--format=%aI%n%cI", commit],
18706
+ { shell: false }
18657
18707
  );
18658
- }
18659
- validateManifestEntry(entry, key);
18660
- const addonFile = resolve7(NATIVE_BASE, entry.path);
18661
- const lstatFn = hooks.lstatSync || lstatSync2;
18662
- const lstatOpts = { bigint: true };
18663
- let preStat;
18664
- try {
18665
- preStat = lstatFn(addonFile, lstatOpts);
18666
- if (preStat.isSymbolicLink()) {
18667
- throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon must not be a symlink");
18708
+ const [authorDate, committerDate] = datesOut.trim().split("\n");
18709
+ if (authorDate !== expectedTimestamp || committerDate !== expectedTimestamp) {
18710
+ throw frozenError("frozen git commit dates do not match the plan freeze timestamp", {
18711
+ commit,
18712
+ expectedTimestamp,
18713
+ observedAuthorDate: authorDate,
18714
+ observedCommitterDate: committerDate
18715
+ });
18668
18716
  }
18669
- if (!preStat.isFile()) {
18670
- throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon is not a regular file");
18717
+ }
18718
+ if (parentCommit) {
18719
+ if (!/^[a-f0-9]{40,64}$/.test(parentCommit)) {
18720
+ throw frozenError("frozen Git parent must be a full hexadecimal object id");
18671
18721
  }
18672
- if (preStat.nlink !== 1n) {
18673
- throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon has invalid nlink count");
18722
+ const { stdout: parentsOut } = await exec(
18723
+ "git",
18724
+ ["--git-dir", gitDir, "rev-list", "--parents", "-n", "1", commit],
18725
+ { shell: false }
18726
+ );
18727
+ const [observedCommit, ...parents] = parentsOut.trim().split(/\s+/);
18728
+ if (observedCommit !== commit || parents.length !== 1 || parents[0] !== parentCommit) {
18729
+ throw frozenError("frozen Git commit parent mismatch", {
18730
+ commit,
18731
+ expectedParent: parentCommit,
18732
+ observedParents: parents
18733
+ });
18674
18734
  }
18675
- } catch (e) {
18676
- if (e instanceof ReleaseError) throw e;
18677
- throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon not accessible");
18678
18735
  }
18679
- let physicalAddonFile;
18680
- try {
18681
- physicalAddonFile = realpathSync2(addonFile);
18682
- } catch {
18683
- throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon not accessible");
18736
+ return { gitDir, commit, tree, ...parentCommit ? { parentCommit } : {} };
18737
+ }
18738
+ async function verifyGitTreeContent({ snapshotDir, repositoryDir, commit, expectedSnapshotDigest, exec }) {
18739
+ const snapshot = await computeFrozenSnapshot(snapshotDir);
18740
+ if (snapshot.digest !== expectedSnapshotDigest) {
18741
+ throw frozenError("frozen snapshot changed while deriving Git objects", {
18742
+ expectedDigest: expectedSnapshotDigest,
18743
+ observedDigest: snapshot.digest
18744
+ });
18684
18745
  }
18685
- const nativeBaseDir = realpathSync2(NATIVE_BASE);
18686
- if (!physicalAddonFile.startsWith(nativeBaseDir + "/") && physicalAddonFile !== nativeBaseDir) {
18687
- throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon physical path escapes base directory");
18746
+ const { stdout: treeOut } = await exec(
18747
+ "git",
18748
+ ["--git-dir", repositoryDir, "ls-tree", "-rz", commit],
18749
+ { shell: false, encoding: "buffer", maxBuffer: 64 * 1024 * 1024 }
18750
+ );
18751
+ const treeEntries = Buffer.from(treeOut).toString("utf8").split("\0").filter(Boolean).map((record) => {
18752
+ const separator = record.indexOf(" ");
18753
+ const [mode, type] = record.slice(0, separator).split(" ");
18754
+ return { path: record.slice(separator + 1), mode, type };
18755
+ }).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
18756
+ const names = treeEntries.map((entry) => entry.path);
18757
+ const expectedNames = snapshot.entries.map((entry) => entry.path);
18758
+ if (JSON.stringify(names) !== JSON.stringify(expectedNames)) {
18759
+ throw frozenError("frozen Git tree paths do not match the sealed public snapshot");
18688
18760
  }
18689
- const readFn = hooks.readFileSync || readFileSync3;
18690
- let data;
18691
- try {
18692
- data = readFn(physicalAddonFile);
18693
- } catch {
18694
- throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon not readable");
18761
+ for (const [index, entry] of snapshot.entries.entries()) {
18762
+ const gitEntry = treeEntries[index];
18763
+ const expectedMode = entry.mode & 73 ? "100755" : "100644";
18764
+ if (gitEntry.type !== "blob" || gitEntry.mode !== expectedMode) {
18765
+ throw frozenError(`frozen Git tree mode does not match the sealed public snapshot: ${entry.path}`, {
18766
+ expectedMode,
18767
+ observedMode: gitEntry.mode,
18768
+ observedType: gitEntry.type
18769
+ });
18770
+ }
18771
+ const { stdout } = await exec(
18772
+ "git",
18773
+ ["--git-dir", repositoryDir, "show", `${commit}:${entry.path}`],
18774
+ { shell: false, encoding: "buffer", maxBuffer: Math.max(64 * 1024 * 1024, entry.size + 1024) }
18775
+ );
18776
+ const digest = createHash2("sha256").update(Buffer.from(stdout)).digest("hex");
18777
+ if (digest !== entry.contentDigest) {
18778
+ throw frozenError(`frozen Git tree bytes do not match the sealed public snapshot: ${entry.path}`);
18779
+ }
18695
18780
  }
18696
- const hash = createHash2("sha256").update(data).digest("hex");
18697
- if (hash !== entry.sha256) {
18698
- throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon SHA-256 mismatch");
18781
+ }
18782
+ function normalizeGitTimestamp(value, label = "freeze timestamp") {
18783
+ if (typeof value !== "string" || value.trim() === "") {
18784
+ throw frozenError(`${label} must be a non-empty strict ISO 8601 timestamp string`, {
18785
+ received: typeof value === "string" ? value : typeof value
18786
+ });
18699
18787
  }
18700
- let postStat;
18701
- try {
18702
- postStat = lstatFn(addonFile, lstatOpts);
18703
- } catch {
18704
- throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon identity check failed");
18788
+ const trimmed = value.trim();
18789
+ const match = GIT_ISO8601_STRICT_RE.exec(trimmed);
18790
+ if (!match) {
18791
+ throw frozenError(`${label} must be strict ISO 8601 with an explicit UTC offset`, {
18792
+ received: trimmed
18793
+ });
18705
18794
  }
18706
- if (preStat.dev !== postStat.dev || preStat.ino !== postStat.ino || preStat.mode !== postStat.mode || preStat.nlink !== postStat.nlink || preStat.size !== postStat.size || preStat.mtimeNs !== postStat.mtimeNs || preStat.ctimeNs !== postStat.ctimeNs) {
18707
- throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon identity changed during read (TOCTOU detected)");
18795
+ const [, yearStr, monthStr, dayStr, hourStr, minuteStr, secondStr, zone] = match;
18796
+ const inputYear = Number(yearStr);
18797
+ const inputMonth = Number(monthStr);
18798
+ const inputDay = Number(dayStr);
18799
+ const inputHour = Number(hourStr);
18800
+ const inputMinute = Number(minuteStr);
18801
+ const inputSecond = Number(secondStr);
18802
+ if (inputYear < 1e3 || inputYear > 9999) {
18803
+ throw frozenError(`${label} year must be a four-digit ISO year`, { received: trimmed });
18708
18804
  }
18709
- const addon = requireAddon(physicalAddonFile);
18710
- let postRequireStat;
18711
- try {
18712
- postRequireStat = lstatFn(addonFile, lstatOpts);
18713
- } catch {
18714
- throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon identity check failed after require");
18805
+ const probeMs = Date.UTC(inputYear, inputMonth - 1, inputDay, inputHour, inputMinute, inputSecond);
18806
+ const probe = new Date(probeMs);
18807
+ if (probe.getUTCFullYear() !== inputYear || probe.getUTCMonth() !== inputMonth - 1 || probe.getUTCDate() !== inputDay || probe.getUTCHours() !== inputHour || probe.getUTCMinutes() !== inputMinute || probe.getUTCSeconds() !== inputSecond) {
18808
+ throw frozenError(`${label} is not a valid calendar date`, { received: trimmed });
18715
18809
  }
18716
- if (preStat.dev !== postRequireStat.dev || preStat.ino !== postRequireStat.ino || preStat.mode !== postRequireStat.mode || preStat.nlink !== postRequireStat.nlink || preStat.size !== postRequireStat.size || preStat.mtimeNs !== postRequireStat.mtimeNs || preStat.ctimeNs !== postRequireStat.ctimeNs) {
18717
- throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon identity changed during require (TOCTOU detected)");
18810
+ let offsetMs = 0;
18811
+ if (zone !== "Z") {
18812
+ const sign = zone.startsWith("-") ? -1 : 1;
18813
+ const zoneHours = Number(zone.slice(1, 3));
18814
+ const zoneMinutes = Number(zone.slice(4, 6));
18815
+ if (zoneHours > 23 || zoneMinutes > 59) {
18816
+ throw frozenError(`${label} has an invalid UTC offset`, { received: trimmed });
18817
+ }
18818
+ offsetMs = sign * (zoneHours * 60 + zoneMinutes) * 6e4;
18718
18819
  }
18719
- const addonExports = Object.keys(addon).sort();
18720
- const expectedExports = [...EXPECTED_EXPORTS].sort();
18721
- if (addonExports.length !== expectedExports.length || addonExports.some((v, i) => v !== expectedExports[i])) {
18722
- throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "addon exports do not match manifest");
18820
+ const date = new Date(probeMs - offsetMs);
18821
+ if (date.getUTCFullYear() < 1e3 || date.getUTCFullYear() > 9999) {
18822
+ throw frozenError(`${label} normalizes outside the four-digit ISO year range`, { received: trimmed });
18723
18823
  }
18724
- return addon;
18824
+ const pad = /* @__PURE__ */ __name((num) => String(num).padStart(2, "0"), "pad");
18825
+ return `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}T${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())}+00:00`;
18725
18826
  }
18726
- function loadNativeAddon(addonPath) {
18727
- if (addonPath) {
18728
- if (process.env.RELEASE_SKILL_NATIVE_DEV !== "1") {
18729
- throw new ReleaseError(
18730
- SAFE_WRITE_UNAVAILABLE,
18731
- "explicit addonPath is only allowed in development mode"
18732
- );
18827
+ function publicGitRemote({ repo, githubHost = "github.com" }) {
18828
+ if (typeof repo !== "string" || !/^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/.test(repo)) {
18829
+ throw frozenError("Git parent repo must use owner/name format");
18830
+ }
18831
+ if (typeof githubHost !== "string" || !/^[A-Za-z0-9.-]+$/.test(githubHost) || githubHost.startsWith(".") || githubHost.endsWith(".") || githubHost.includes("..")) {
18832
+ throw frozenError("Git parent githubHost must be a valid hostname");
18833
+ }
18834
+ return `https://${githubHost}/${repo}.git`;
18835
+ }
18836
+ async function buildFrozenGitRepository({
18837
+ snapshotDir,
18838
+ repositoryDir,
18839
+ version,
18840
+ expectedSnapshotDigest,
18841
+ parent,
18842
+ commitTimestamp,
18843
+ exec = execFile
18844
+ }) {
18845
+ const canonicalTimestamp = normalizeGitTimestamp(commitTimestamp, "frozen commit timestamp");
18846
+ if (!expectedSnapshotDigest) throw frozenError("Git object build requires the sealed snapshot digest");
18847
+ await mkdir3(repositoryDir, { recursive: true });
18848
+ await exec("git", ["init", "--bare", repositoryDir], { shell: false });
18849
+ let parentCommit = null;
18850
+ if (parent) {
18851
+ if (!parent.ref || typeof parent.ref !== "string") {
18852
+ throw frozenError("Git parent ref must be a non-empty string");
18733
18853
  }
18734
- const physicalPath = isWithinPackageBuildDir(addonPath);
18735
- if (!physicalPath) {
18736
- throw new ReleaseError(
18737
- SAFE_WRITE_UNAVAILABLE,
18738
- "addonPath is outside the package build directory"
18739
- );
18854
+ if (!parent.commit || !/^[a-f0-9]{40,64}$/.test(parent.commit)) {
18855
+ throw frozenError("Git parent commit must be a full hexadecimal object id");
18740
18856
  }
18741
- return requireAddon(physicalPath);
18742
- }
18743
- if (process.env.RELEASE_SKILL_NATIVE_DEV === "1") {
18744
- const devPath = DEFAULT_ADDON_PATH;
18745
- const physicalPath = isWithinPackageBuildDir(devPath);
18746
- if (!physicalPath) {
18747
- throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "dev addon path is outside build directory");
18857
+ const remoteUrl = publicGitRemote(parent);
18858
+ await exec(
18859
+ "git",
18860
+ ["--git-dir", repositoryDir, "fetch", "--no-tags", remoteUrl, parent.ref],
18861
+ { shell: false }
18862
+ );
18863
+ const { stdout: fetchedOut } = await exec(
18864
+ "git",
18865
+ ["--git-dir", repositoryDir, "rev-parse", "FETCH_HEAD^{commit}"],
18866
+ { shell: false }
18867
+ );
18868
+ if (fetchedOut.trim() !== parent.commit) {
18869
+ throw frozenError("fetched Git parent does not match the observed public baseline", {
18870
+ expectedCommit: parent.commit,
18871
+ observedCommit: fetchedOut.trim(),
18872
+ ref: parent.ref
18873
+ });
18748
18874
  }
18749
- return requireAddon(physicalPath);
18875
+ parentCommit = parent.commit;
18750
18876
  }
18751
- return readStableAddon();
18752
- }
18753
- function wrapHandle(addon, nativeHandle, nativeTokenMap, nativeIdentityMap) {
18754
- const handle = {
18755
- async openDir(name) {
18756
- try {
18757
- const nativeChild = addon.openDir(nativeHandle, name);
18758
- return wrapHandle(addon, nativeChild, nativeTokenMap, nativeIdentityMap);
18759
- } catch (err) {
18760
- throw new ReleaseError(err.code || PATH_UNSAFE, sanitizeError(err));
18761
- }
18762
- },
18763
- async readEntry(name) {
18764
- try {
18765
- return addon.readEntry(nativeHandle, name);
18766
- } catch (err) {
18767
- throw new ReleaseError(err.code || PATH_UNSAFE, sanitizeError(err));
18768
- }
18769
- },
18770
- async readFile(name) {
18877
+ const indexPath = join7(repositoryDir, "release-index");
18878
+ const env = { ...process.env, GIT_INDEX_FILE: indexPath };
18879
+ await exec("git", [
18880
+ "--git-dir",
18881
+ repositoryDir,
18882
+ "--work-tree",
18883
+ snapshotDir,
18884
+ "add",
18885
+ "--all",
18886
+ "--force",
18887
+ "--",
18888
+ "."
18889
+ ], { cwd: snapshotDir, env, shell: false });
18890
+ const { stdout: treeOut } = await exec("git", ["--git-dir", repositoryDir, "write-tree"], {
18891
+ env,
18892
+ shell: false
18893
+ });
18894
+ const tree = treeOut.trim();
18895
+ const commitEnv = {
18896
+ ...env,
18897
+ GIT_AUTHOR_NAME: "release-skill",
18898
+ GIT_AUTHOR_EMAIL: "release-skill@localhost",
18899
+ GIT_COMMITTER_NAME: "release-skill",
18900
+ GIT_COMMITTER_EMAIL: "release-skill@localhost",
18901
+ GIT_AUTHOR_DATE: canonicalTimestamp,
18902
+ GIT_COMMITTER_DATE: canonicalTimestamp
18903
+ };
18904
+ const commitArgs = ["--git-dir", repositoryDir, "commit-tree", tree];
18905
+ if (parentCommit) commitArgs.push("-p", parentCommit);
18906
+ commitArgs.push("-m", `Release ${version}`);
18907
+ const { stdout: commitOut } = await exec(
18908
+ "git",
18909
+ commitArgs,
18910
+ { env: commitEnv, shell: false }
18911
+ );
18912
+ const commit = commitOut.trim();
18913
+ await verifyGitTreeContent({ snapshotDir, repositoryDir, commit, expectedSnapshotDigest, exec });
18914
+ if (parentCommit) {
18915
+ const { stdout: parentsOut } = await exec(
18916
+ "git",
18917
+ ["--git-dir", repositoryDir, "rev-list", "--parents", "-n", "1", commit],
18918
+ { shell: false }
18919
+ );
18920
+ const [observedCommit, ...parents] = parentsOut.trim().split(/\s+/);
18921
+ if (observedCommit !== commit || parents.length !== 1 || parents[0] !== parentCommit) {
18922
+ throw frozenError("derived Git commit does not have the exact planned parent", {
18923
+ commit,
18924
+ parentCommit,
18925
+ observedParents: parents
18926
+ });
18927
+ }
18928
+ }
18929
+ return { tree, commit, ...parentCommit ? { parentCommit } : {} };
18930
+ }
18931
+ function contentEntries(entries) {
18932
+ return entries.map(({ path: path3, size, contentDigest }) => ({ path: path3, size, contentDigest }));
18933
+ }
18934
+ async function createDetachedReadHandle(bytes, directory) {
18935
+ const tempDir = await mkdtemp(join7(directory, ".tar-fd-"));
18936
+ const tempPath = join7(tempDir, "package.tgz");
18937
+ let handle;
18938
+ try {
18939
+ const writer = await open3(tempPath, "wx", 256);
18940
+ try {
18941
+ await writer.writeFile(bytes);
18942
+ await writer.sync();
18943
+ } finally {
18944
+ await writer.close();
18945
+ }
18946
+ handle = await open3(tempPath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
18947
+ await unlink2(tempPath);
18948
+ await rm3(tempDir, { recursive: true, force: true });
18949
+ return handle;
18950
+ } catch (err) {
18951
+ await handle?.close().catch(() => {
18952
+ });
18953
+ await rm3(tempDir, { recursive: true, force: true });
18954
+ throw err;
18955
+ }
18956
+ }
18957
+ async function runTarFromHandle(handle, args2) {
18958
+ const fdPath = process.platform === "linux" ? "/proc/self/fd/3" : "/dev/fd/3";
18959
+ return new Promise((resolvePromise, rejectPromise) => {
18960
+ const child = spawn("tar", args2(fdPath), {
18961
+ shell: false,
18962
+ env: process.env,
18963
+ stdio: ["ignore", "pipe", "pipe", handle.fd]
18964
+ });
18965
+ let stdout = "";
18966
+ let stderr = "";
18967
+ child.stdout.setEncoding("utf8");
18968
+ child.stderr.setEncoding("utf8");
18969
+ child.stdout.on("data", (chunk) => {
18970
+ stdout += chunk;
18971
+ });
18972
+ child.stderr.on("data", (chunk) => {
18973
+ stderr += chunk;
18974
+ });
18975
+ child.on("error", rejectPromise);
18976
+ child.on("close", (code) => {
18977
+ if (code === 0) resolvePromise({ stdout, stderr });
18978
+ else rejectPromise(new Error(`tar exited with code ${code}: ${stderr.trim()}`));
18979
+ });
18980
+ });
18981
+ }
18982
+ function validateNpmTarballListing(listOut) {
18983
+ const listed = listOut.split(/\r?\n/).filter(Boolean);
18984
+ if (listed.length === 0) {
18985
+ throw frozenError("npm tarball contains no entries");
18986
+ }
18987
+ const seen = /* @__PURE__ */ new Set();
18988
+ for (const entry of listed) {
18989
+ if (entry.startsWith("/") || entry.includes("\\") || !entry.startsWith("package/") && entry !== "package") {
18990
+ throw frozenError("npm tarball contains an unsafe or unexpected path", { entry });
18991
+ }
18992
+ const segments = entry.split("/");
18993
+ const pathSegments = entry.endsWith("/") ? segments.slice(0, -1) : segments;
18994
+ if (pathSegments.some((segment) => segment === "" || segment === "." || segment === "..")) {
18995
+ throw frozenError("npm tarball contains an unsafe or unexpected path", { entry });
18996
+ }
18997
+ const canonicalPath2 = entry.endsWith("/") ? entry.slice(0, -1) : entry;
18998
+ if (seen.has(canonicalPath2)) {
18999
+ throw frozenError("npm tarball contains a duplicate path", { entry: canonicalPath2 });
19000
+ }
19001
+ seen.add(canonicalPath2);
19002
+ }
19003
+ }
19004
+ async function inspectNpmTarballContent({ tarballBytes, tarballDir }) {
19005
+ const listHandle = await createDetachedReadHandle(tarballBytes, tarballDir);
19006
+ let listOut;
19007
+ try {
19008
+ ({ stdout: listOut } = await runTarFromHandle(listHandle, (fdPath) => ["-tzf", fdPath]));
19009
+ } finally {
19010
+ await listHandle.close();
19011
+ }
19012
+ validateNpmTarballListing(listOut);
19013
+ const verifyDir = await mkdtemp(join7(tarballDir, ".verify-"));
19014
+ try {
19015
+ const extractHandle = await createDetachedReadHandle(tarballBytes, tarballDir);
19016
+ try {
19017
+ await runTarFromHandle(extractHandle, (fdPath) => ["-xzf", fdPath, "-C", verifyDir]);
19018
+ } finally {
19019
+ await extractHandle.close();
19020
+ }
19021
+ return await computeFrozenSnapshot(join7(verifyDir, "package"));
19022
+ } finally {
19023
+ await rm3(verifyDir, { recursive: true, force: true });
19024
+ }
19025
+ }
19026
+ async function verifyNpmTarballContent({ snapshotDir, tarballBytes, tarballDir, expectedSnapshotDigest }) {
19027
+ const packed = await inspectNpmTarballContent({ tarballBytes, tarballDir });
19028
+ const original = await computeFrozenSnapshot(snapshotDir);
19029
+ if (original.digest !== expectedSnapshotDigest) {
19030
+ throw frozenError("frozen snapshot changed while deriving npm tarball", {
19031
+ expectedDigest: expectedSnapshotDigest,
19032
+ observedDigest: original.digest
19033
+ });
19034
+ }
19035
+ if (JSON.stringify(contentEntries(packed.entries)) !== JSON.stringify(contentEntries(original.entries))) {
19036
+ throw frozenError("npm tarball bytes do not match the sealed public snapshot");
19037
+ }
19038
+ }
19039
+ async function buildNpmTarballFileIndex({ tarballBytes, tarballDir }) {
19040
+ const packed = await inspectNpmTarballContent({ tarballBytes, tarballDir });
19041
+ return new Map(packed.entries.map((entry) => [entry.path, { type: entry.type }]));
19042
+ }
19043
+ async function buildFrozenNpmTarball({ snapshotDir, tarballDir, expectedSnapshotDigest, exec = execFile }) {
19044
+ await mkdir3(tarballDir, { recursive: true });
19045
+ const { stdout } = await exec(
19046
+ "npm",
19047
+ ["pack", snapshotDir, "--pack-destination", tarballDir, "--json", "--ignore-scripts"],
19048
+ { shell: false, encoding: "utf8", timeout: 12e4 }
19049
+ );
19050
+ let parsed;
19051
+ try {
19052
+ parsed = JSON.parse(stdout);
19053
+ } catch {
19054
+ throw frozenError("npm pack returned invalid JSON");
19055
+ }
19056
+ const info = Array.isArray(parsed) ? parsed[0] : parsed;
19057
+ if (!info?.filename || !info?.integrity) {
19058
+ throw frozenError("npm pack did not return filename and integrity");
19059
+ }
19060
+ const tarballPath = join7(tarballDir, info.filename);
19061
+ const { bytes } = await readStableRegularFile(tarballPath, info.filename);
19062
+ if (!expectedSnapshotDigest) throw frozenError("npm tarball build requires the sealed snapshot digest");
19063
+ await verifyNpmTarballContent({
19064
+ snapshotDir,
19065
+ tarballBytes: bytes,
19066
+ tarballDir,
19067
+ expectedSnapshotDigest
19068
+ });
19069
+ return {
19070
+ tarballPath,
19071
+ integrity: info.integrity,
19072
+ sha256: createHash2("sha256").update(bytes).digest("hex"),
19073
+ size: bytes.length
19074
+ };
19075
+ }
19076
+ async function sealFrozenSnapshot(snapshotDir) {
19077
+ async function walk(dir) {
19078
+ const children = await readdir2(dir, { withFileTypes: true });
19079
+ for (const child of children) {
19080
+ const absolute = join7(dir, child.name);
19081
+ const st = await lstat4(absolute);
19082
+ if (st.isDirectory()) {
19083
+ await walk(absolute);
19084
+ await chmod(absolute, 365);
19085
+ } else if (st.isFile()) {
19086
+ await chmod(absolute, st.mode & 73 ? 365 : 292);
19087
+ }
19088
+ }
19089
+ }
19090
+ __name(walk, "walk");
19091
+ await walk(snapshotDir);
19092
+ await chmod(snapshotDir, 365);
19093
+ }
19094
+ var execFile, GIT_ISO8601_STRICT_RE;
19095
+ var init_frozen = __esm({
19096
+ "src/snapshot/frozen.mjs"() {
19097
+ init_errors();
19098
+ execFile = promisify(execFileCb);
19099
+ __name(frozenError, "frozenError");
19100
+ __name(isInside, "isInside");
19101
+ __name(resolveFrozenPath, "resolveFrozenPath");
19102
+ __name(readStableRegularFile, "readStableRegularFile");
19103
+ __name(computeFrozenSnapshot, "computeFrozenSnapshot");
19104
+ __name(verifyFrozenSnapshot, "verifyFrozenSnapshot");
19105
+ __name(verifyFrozenFile, "verifyFrozenFile");
19106
+ __name(verifyFrozenDirectoryStructure, "verifyFrozenDirectoryStructure");
19107
+ __name(verifyFrozenGitRepository, "verifyFrozenGitRepository");
19108
+ __name(verifyGitTreeContent, "verifyGitTreeContent");
19109
+ GIT_ISO8601_STRICT_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,9})?(Z|[+-]\d{2}:\d{2})$/;
19110
+ __name(normalizeGitTimestamp, "normalizeGitTimestamp");
19111
+ __name(publicGitRemote, "publicGitRemote");
19112
+ __name(buildFrozenGitRepository, "buildFrozenGitRepository");
19113
+ __name(contentEntries, "contentEntries");
19114
+ __name(createDetachedReadHandle, "createDetachedReadHandle");
19115
+ __name(runTarFromHandle, "runTarFromHandle");
19116
+ __name(validateNpmTarballListing, "validateNpmTarballListing");
19117
+ __name(inspectNpmTarballContent, "inspectNpmTarballContent");
19118
+ __name(verifyNpmTarballContent, "verifyNpmTarballContent");
19119
+ __name(buildNpmTarballFileIndex, "buildNpmTarballFileIndex");
19120
+ __name(buildFrozenNpmTarball, "buildFrozenNpmTarball");
19121
+ __name(sealFrozenSnapshot, "sealFrozenSnapshot");
19122
+ }
19123
+ });
19124
+
19125
+ // src/npm/npm-entry-closure.mjs
19126
+ import { tmpdir } from "node:os";
19127
+ function entryError(field, target, reason, message) {
19128
+ return { field, target, reason, message };
19129
+ }
19130
+ function validateRelativeTarget(target, field, { requireDotSlash = false } = {}) {
19131
+ if (typeof target !== "string" || target.length === 0) {
19132
+ return {
19133
+ error: entryError(field, String(target ?? ""), "invalid_type", `${field} target must be a non-empty string`)
19134
+ };
19135
+ }
19136
+ if (target.includes("\0")) {
19137
+ return { error: entryError(field, target, "nul_in_path", `${field} target must not contain NUL`) };
19138
+ }
19139
+ if (target.startsWith("/") || /^[A-Za-z]:[/\\]/.test(target) || target.startsWith("\\\\")) {
19140
+ return { error: entryError(field, target, "absolute_path", `${field} target must be package-relative`) };
19141
+ }
19142
+ if (target.includes("\\")) {
19143
+ return { error: entryError(field, target, "backslash_path", `${field} target must use forward slashes`) };
19144
+ }
19145
+ if (requireDotSlash && !target.startsWith("./")) {
19146
+ return {
19147
+ error: entryError(field, target, "missing_dot_slash", `${field} target must start with "./"`)
19148
+ };
19149
+ }
19150
+ const relativeTarget = target.startsWith("./") ? target.slice(2) : target;
19151
+ const segments = relativeTarget.split("/");
19152
+ if (segments.includes("..")) {
19153
+ return {
19154
+ error: entryError(field, target, "path_escape", `${field} target escapes the package root`)
19155
+ };
19156
+ }
19157
+ if (relativeTarget.length === 0 || segments.some((segment) => segment === "" || segment === ".")) {
19158
+ return {
19159
+ error: entryError(field, target, "unsafe_segment", `${field} target contains an unsafe path segment`)
19160
+ };
19161
+ }
19162
+ if (requireDotSlash && segments.includes("node_modules")) {
19163
+ return {
19164
+ error: entryError(field, target, "unsafe_segment", `${field} target must not contain node_modules`)
19165
+ };
19166
+ }
19167
+ return { target: relativeTarget };
19168
+ }
19169
+ function checkTarget({ field, target, fileIndex, requireDotSlash = false }) {
19170
+ const validated = validateRelativeTarget(target, field, { requireDotSlash });
19171
+ if (validated.error) return { errors: [validated.error], entries: [] };
19172
+ const indexed = fileIndex.get(validated.target);
19173
+ const found = indexed?.type === "file";
19174
+ const entries = [{ field, target: validated.target, found }];
19175
+ if (found) return { entries, errors: [] };
19176
+ const reason = indexed === void 0 ? "entry_missing" : "entry_not_regular_file";
19177
+ return {
19178
+ entries,
19179
+ errors: [
19180
+ entryError(
19181
+ field,
19182
+ validated.target,
19183
+ reason,
19184
+ indexed === void 0 ? `${field} target "${validated.target}" is missing` : `${field} target "${validated.target}" is not a regular file`
19185
+ )
19186
+ ]
19187
+ };
19188
+ }
19189
+ function collectExports(value, location, result) {
19190
+ if (value === null) return;
19191
+ if (typeof value === "string") {
19192
+ if (value.includes("*")) {
19193
+ result.errors.push(
19194
+ entryError(
19195
+ "exports",
19196
+ value,
19197
+ "unsupported_entry_shape",
19198
+ `${location} uses a wildcard target that the static gate cannot verify`
19199
+ )
19200
+ );
19201
+ } else {
19202
+ result.targets.push({ field: `exports ${location}`, target: value });
19203
+ }
19204
+ return;
19205
+ }
19206
+ if (Array.isArray(value)) {
19207
+ result.errors.push(
19208
+ entryError("exports", location, "unsupported_entry_shape", `${location} uses unsupported fallback-array semantics`)
19209
+ );
19210
+ return;
19211
+ }
19212
+ if (typeof value !== "object" || value === void 0) {
19213
+ result.errors.push(
19214
+ entryError("exports", String(value), "invalid_exports_type", `${location} has an unsupported exports value`)
19215
+ );
19216
+ return;
19217
+ }
19218
+ for (const [key, child] of Object.entries(value)) {
19219
+ const childLocation = `${location}.${key}`;
19220
+ if (key.includes("*")) {
19221
+ result.errors.push(
19222
+ entryError(
19223
+ "exports",
19224
+ key,
19225
+ "unsupported_entry_shape",
19226
+ `${childLocation} uses a wildcard subpath that the static gate cannot verify`
19227
+ )
19228
+ );
19229
+ continue;
19230
+ }
19231
+ collectExports(child, childLocation, result);
19232
+ }
19233
+ }
19234
+ function checkNpmEntryClosure(manifest, fileIndex) {
19235
+ const result = { entries: [], errors: [], diagnostics: [] };
19236
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
19237
+ result.errors.push(entryError("manifest", "", "invalid_manifest", "manifest must be an object"));
19238
+ return result;
19239
+ }
19240
+ if (!(fileIndex instanceof Map)) {
19241
+ result.errors.push(entryError("fileIndex", "", "invalid_file_index", "fileIndex must be a Map"));
19242
+ return result;
19243
+ }
19244
+ if (manifest.bin !== void 0 && manifest.bin !== null) {
19245
+ if (typeof manifest.bin === "string") {
19246
+ const checked = checkTarget({ field: "bin", target: manifest.bin, fileIndex });
19247
+ result.entries.push(...checked.entries);
19248
+ result.errors.push(...checked.errors);
19249
+ } else if (typeof manifest.bin === "object" && !Array.isArray(manifest.bin)) {
19250
+ for (const [name, target] of Object.entries(manifest.bin)) {
19251
+ const checked = checkTarget({ field: "bin", target, fileIndex });
19252
+ result.entries.push(...checked.entries);
19253
+ result.errors.push(...checked.errors);
19254
+ }
19255
+ } else {
19256
+ result.errors.push(entryError("bin", String(manifest.bin), "invalid_type", "bin must be a string or object"));
19257
+ }
19258
+ }
19259
+ for (const field of SIMPLE_FIELDS) {
19260
+ if (manifest[field] === void 0 || manifest[field] === null) continue;
19261
+ const checked = checkTarget({ field, target: manifest[field], fileIndex });
19262
+ result.entries.push(...checked.entries);
19263
+ result.errors.push(...checked.errors);
19264
+ }
19265
+ if (manifest.exports !== void 0) {
19266
+ const exportsResult = { targets: [], errors: [], diagnostics: [] };
19267
+ collectExports(manifest.exports, "exports", exportsResult);
19268
+ result.errors.push(...exportsResult.errors);
19269
+ result.diagnostics.push(...exportsResult.diagnostics);
19270
+ for (const target of exportsResult.targets) {
19271
+ const checked = checkTarget({
19272
+ field: "exports",
19273
+ target: target.target,
19274
+ fileIndex,
19275
+ requireDotSlash: true
19276
+ });
19277
+ result.entries.push(...checked.entries);
19278
+ result.errors.push(...checked.errors);
19279
+ }
19280
+ }
19281
+ return result;
19282
+ }
19283
+ async function buildTarballFileIndex(tarballBytes, tarballDir = tmpdir()) {
19284
+ return buildNpmTarballFileIndex({ tarballBytes, tarballDir });
19285
+ }
19286
+ async function buildDirectoryFileIndex(packageDir) {
19287
+ const snapshot = await computeFrozenSnapshot(packageDir, { excludeRootEntries: [".git"] });
19288
+ return new Map(snapshot.entries.map((entry) => [entry.path, { type: entry.type }]));
19289
+ }
19290
+ var SIMPLE_FIELDS;
19291
+ var init_npm_entry_closure = __esm({
19292
+ "src/npm/npm-entry-closure.mjs"() {
19293
+ init_frozen();
19294
+ SIMPLE_FIELDS = ["main", "module", "types", "typings"];
19295
+ __name(entryError, "entryError");
19296
+ __name(validateRelativeTarget, "validateRelativeTarget");
19297
+ __name(checkTarget, "checkTarget");
19298
+ __name(collectExports, "collectExports");
19299
+ __name(checkNpmEntryClosure, "checkNpmEntryClosure");
19300
+ __name(buildTarballFileIndex, "buildTarballFileIndex");
19301
+ __name(buildDirectoryFileIndex, "buildDirectoryFileIndex");
19302
+ }
19303
+ });
19304
+
19305
+ // src/artifacts/safe-fs-backend-internal.mjs
19306
+ import { createRequire } from "node:module";
19307
+ import { dirname as dirname3, resolve as resolve8 } from "node:path";
19308
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
19309
+ import { realpath as realpath3 } from "node:fs/promises";
19310
+ import { readFileSync as readFileSync3, realpathSync as realpathSync2, lstatSync as lstatSync2 } from "node:fs";
19311
+ import { createHash as createHash3 } from "node:crypto";
19312
+ function failPrebuildManifest(reason, message = "prebuilds manifest not readable") {
19313
+ throw new ReleaseError(
19314
+ SAFE_WRITE_UNAVAILABLE,
19315
+ message,
19316
+ { resource: PREBUILDS_RESOURCE, reason }
19317
+ );
19318
+ }
19319
+ function readTrustedPrebuildManifest() {
19320
+ let lexicalStat;
19321
+ try {
19322
+ lexicalStat = lstatSync2(PREBUILDS_PATH, { bigint: true });
19323
+ } catch {
19324
+ failPrebuildManifest("MISSING");
19325
+ }
19326
+ if (lexicalStat.isSymbolicLink()) {
19327
+ failPrebuildManifest("SYMLINK", "prebuilds manifest must not be a symlink");
19328
+ }
19329
+ if (!lexicalStat.isFile()) {
19330
+ failPrebuildManifest("NOT_REGULAR_FILE", "prebuilds manifest is not a regular file");
19331
+ }
19332
+ if (lexicalStat.nlink !== 1n) {
19333
+ failPrebuildManifest("UNEXPECTED_HARDLINK_COUNT", "prebuilds manifest has invalid nlink count");
19334
+ }
19335
+ let physicalBase;
19336
+ let physicalManifest;
19337
+ try {
19338
+ physicalBase = realpathSync2(NATIVE_BASE);
19339
+ physicalManifest = realpathSync2(PREBUILDS_PATH);
19340
+ } catch {
19341
+ failPrebuildManifest("REALPATH_FAILED");
19342
+ }
19343
+ if (!physicalManifest.startsWith(physicalBase + "/") && physicalManifest !== physicalBase) {
19344
+ failPrebuildManifest(
19345
+ "PHYSICAL_PATH_ESCAPE",
19346
+ "prebuilds manifest physical path escapes base directory"
19347
+ );
19348
+ }
19349
+ try {
19350
+ return JSON.parse(readFileSync3(physicalManifest, "utf8"));
19351
+ } catch {
19352
+ failPrebuildManifest("READ_FAILED");
19353
+ }
19354
+ }
19355
+ function sanitizeError(err) {
19356
+ if (typeof err?.message === "string") {
19357
+ let msg = err.message.replace(/\/[^\s]*:\s*/g, "");
19358
+ msg = msg.replace(/\/\S+/g, "<redacted>");
19359
+ return msg;
19360
+ }
19361
+ return "native addon error";
19362
+ }
19363
+ function isWithinPackageBuildDir(addonPath) {
19364
+ const buildDir = resolve8(NATIVE_BASE, "build/");
19365
+ const resolved = resolve8(addonPath);
19366
+ if (!resolved.startsWith(buildDir + "/") && resolved !== buildDir) {
19367
+ return null;
19368
+ }
19369
+ let lstatResult;
19370
+ try {
19371
+ lstatResult = lstatSync2(addonPath, { bigint: true });
19372
+ } catch {
19373
+ return null;
19374
+ }
19375
+ if (lstatResult.isSymbolicLink()) return null;
19376
+ if (!lstatResult.isFile()) return null;
19377
+ if (lstatResult.nlink !== 1n) return null;
19378
+ try {
19379
+ const physicalBuildDir = realpathSync2(buildDir);
19380
+ const physicalAddon = realpathSync2(addonPath);
19381
+ if (!(physicalAddon.startsWith(physicalBuildDir + "/") || physicalAddon === physicalBuildDir)) {
19382
+ return null;
19383
+ }
19384
+ return physicalAddon;
19385
+ } catch {
19386
+ return null;
19387
+ }
19388
+ }
19389
+ function validateManifestEntry(entry, key) {
19390
+ if (typeof entry !== "object" || entry === null) {
19391
+ throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, `invalid manifest entry for ${key}`);
19392
+ }
19393
+ if (typeof entry.path !== "string" || entry.path.length === 0) {
19394
+ throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, `manifest entry missing path for ${key}`);
19395
+ }
19396
+ if (typeof entry.sha256 !== "string" || !/^[0-9a-f]{64}$/.test(entry.sha256)) {
19397
+ throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, `manifest entry missing or invalid sha256 for ${key}`);
19398
+ }
19399
+ if (typeof entry.exports !== "object" || !Array.isArray(entry.exports)) {
19400
+ throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, `manifest entry missing exports for ${key}`);
19401
+ }
19402
+ const resolved = resolve8(NATIVE_BASE, entry.path);
19403
+ const baseDir = NATIVE_BASE;
19404
+ if (!resolved.startsWith(baseDir + "/") && resolved !== baseDir) {
19405
+ throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, `manifest path escapes base directory for ${key}`);
19406
+ }
19407
+ const sortedActual = [...entry.exports].sort();
19408
+ const sortedExpected = [...EXPECTED_EXPORTS].sort();
19409
+ if (sortedActual.length !== sortedExpected.length || sortedActual.some((v, i) => v !== sortedExpected[i])) {
19410
+ throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, `manifest exports mismatch for ${key}`);
19411
+ }
19412
+ }
19413
+ function requireAddon(addonPath) {
19414
+ try {
19415
+ const require2 = createRequire(resolve8(PKG_ROOT2, "package.json"));
19416
+ return require2(addonPath);
19417
+ } catch (err) {
19418
+ throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, sanitizeError(err));
19419
+ }
19420
+ }
19421
+ function readStableAddon(options = {}) {
19422
+ const hooks = options._hooks || {};
19423
+ const manifest = readTrustedPrebuildManifest();
19424
+ if (!manifest.prebuilds || typeof manifest.prebuilds !== "object") {
19425
+ throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilds manifest has invalid structure");
19426
+ }
19427
+ const key = `${process.platform}-${process.arch}`;
19428
+ const entry = manifest.prebuilds[key];
19429
+ if (!entry) {
19430
+ throw new ReleaseError(
19431
+ SAFE_WRITE_UNAVAILABLE,
19432
+ `no prebuilt addon for ${key}; build from source with native:build`
19433
+ );
19434
+ }
19435
+ validateManifestEntry(entry, key);
19436
+ const addonFile = resolve8(NATIVE_BASE, entry.path);
19437
+ const lstatFn = hooks.lstatSync || lstatSync2;
19438
+ const lstatOpts = { bigint: true };
19439
+ let preStat;
19440
+ try {
19441
+ preStat = lstatFn(addonFile, lstatOpts);
19442
+ if (preStat.isSymbolicLink()) {
19443
+ throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon must not be a symlink");
19444
+ }
19445
+ if (!preStat.isFile()) {
19446
+ throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon is not a regular file");
19447
+ }
19448
+ if (preStat.nlink !== 1n) {
19449
+ throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon has invalid nlink count");
19450
+ }
19451
+ } catch (e) {
19452
+ if (e instanceof ReleaseError) throw e;
19453
+ throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon not accessible");
19454
+ }
19455
+ let physicalAddonFile;
19456
+ try {
19457
+ physicalAddonFile = realpathSync2(addonFile);
19458
+ } catch {
19459
+ throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon not accessible");
19460
+ }
19461
+ const nativeBaseDir = realpathSync2(NATIVE_BASE);
19462
+ if (!physicalAddonFile.startsWith(nativeBaseDir + "/") && physicalAddonFile !== nativeBaseDir) {
19463
+ throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon physical path escapes base directory");
19464
+ }
19465
+ const readFn = hooks.readFileSync || readFileSync3;
19466
+ let data;
19467
+ try {
19468
+ data = readFn(physicalAddonFile);
19469
+ } catch {
19470
+ throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon not readable");
19471
+ }
19472
+ const hash = createHash3("sha256").update(data).digest("hex");
19473
+ if (hash !== entry.sha256) {
19474
+ throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon SHA-256 mismatch");
19475
+ }
19476
+ let postStat;
19477
+ try {
19478
+ postStat = lstatFn(addonFile, lstatOpts);
19479
+ } catch {
19480
+ throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon identity check failed");
19481
+ }
19482
+ if (preStat.dev !== postStat.dev || preStat.ino !== postStat.ino || preStat.mode !== postStat.mode || preStat.nlink !== postStat.nlink || preStat.size !== postStat.size || preStat.mtimeNs !== postStat.mtimeNs || preStat.ctimeNs !== postStat.ctimeNs) {
19483
+ throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon identity changed during read (TOCTOU detected)");
19484
+ }
19485
+ const addon = requireAddon(physicalAddonFile);
19486
+ let postRequireStat;
19487
+ try {
19488
+ postRequireStat = lstatFn(addonFile, lstatOpts);
19489
+ } catch {
19490
+ throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon identity check failed after require");
19491
+ }
19492
+ if (preStat.dev !== postRequireStat.dev || preStat.ino !== postRequireStat.ino || preStat.mode !== postRequireStat.mode || preStat.nlink !== postRequireStat.nlink || preStat.size !== postRequireStat.size || preStat.mtimeNs !== postRequireStat.mtimeNs || preStat.ctimeNs !== postRequireStat.ctimeNs) {
19493
+ throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon identity changed during require (TOCTOU detected)");
19494
+ }
19495
+ const addonExports = Object.keys(addon).sort();
19496
+ const expectedExports = [...EXPECTED_EXPORTS].sort();
19497
+ if (addonExports.length !== expectedExports.length || addonExports.some((v, i) => v !== expectedExports[i])) {
19498
+ throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "addon exports do not match manifest");
19499
+ }
19500
+ return addon;
19501
+ }
19502
+ function loadNativeAddon(addonPath) {
19503
+ if (addonPath) {
19504
+ if (process.env.RELEASE_SKILL_NATIVE_DEV !== "1") {
19505
+ throw new ReleaseError(
19506
+ SAFE_WRITE_UNAVAILABLE,
19507
+ "explicit addonPath is only allowed in development mode"
19508
+ );
19509
+ }
19510
+ const physicalPath = isWithinPackageBuildDir(addonPath);
19511
+ if (!physicalPath) {
19512
+ throw new ReleaseError(
19513
+ SAFE_WRITE_UNAVAILABLE,
19514
+ "addonPath is outside the package build directory"
19515
+ );
19516
+ }
19517
+ return requireAddon(physicalPath);
19518
+ }
19519
+ if (process.env.RELEASE_SKILL_NATIVE_DEV === "1") {
19520
+ const devPath = DEFAULT_ADDON_PATH;
19521
+ const physicalPath = isWithinPackageBuildDir(devPath);
19522
+ if (!physicalPath) {
19523
+ throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "dev addon path is outside build directory");
19524
+ }
19525
+ return requireAddon(physicalPath);
19526
+ }
19527
+ return readStableAddon();
19528
+ }
19529
+ function wrapHandle(addon, nativeHandle, nativeTokenMap, nativeIdentityMap) {
19530
+ const handle = {
19531
+ async openDir(name) {
19532
+ try {
19533
+ const nativeChild = addon.openDir(nativeHandle, name);
19534
+ return wrapHandle(addon, nativeChild, nativeTokenMap, nativeIdentityMap);
19535
+ } catch (err) {
19536
+ throw new ReleaseError(err.code || PATH_UNSAFE, sanitizeError(err));
19537
+ }
19538
+ },
19539
+ async readEntry(name) {
19540
+ try {
19541
+ return addon.readEntry(nativeHandle, name);
19542
+ } catch (err) {
19543
+ throw new ReleaseError(err.code || PATH_UNSAFE, sanitizeError(err));
19544
+ }
19545
+ },
19546
+ async readFile(name) {
18771
19547
  try {
18772
19548
  const result = addon.readFile(nativeHandle, name);
18773
19549
  if (result === null) return null;
@@ -19066,11 +19842,11 @@ var PKG_ROOT2, NATIVE_BASE, PREBUILDS_PATH, PREBUILDS_RESOURCE, DEFAULT_ADDON_PA
19066
19842
  var init_safe_fs_backend_internal = __esm({
19067
19843
  "src/artifacts/safe-fs-backend-internal.mjs"() {
19068
19844
  init_errors();
19069
- PKG_ROOT2 = typeof __bundlePkgRoot !== "undefined" ? __bundlePkgRoot : resolve7(dirname3(fileURLToPath2(import.meta.url)), "..", "..");
19070
- NATIVE_BASE = resolve7(PKG_ROOT2, "native/safe-write");
19071
- PREBUILDS_PATH = resolve7(NATIVE_BASE, "prebuilds.json");
19845
+ PKG_ROOT2 = typeof __bundlePkgRoot !== "undefined" ? __bundlePkgRoot : resolve8(dirname3(fileURLToPath2(import.meta.url)), "..", "..");
19846
+ NATIVE_BASE = resolve8(PKG_ROOT2, "native/safe-write");
19847
+ PREBUILDS_PATH = resolve8(NATIVE_BASE, "prebuilds.json");
19072
19848
  PREBUILDS_RESOURCE = "native/safe-write/prebuilds.json";
19073
- DEFAULT_ADDON_PATH = resolve7(NATIVE_BASE, "build/Release/safe_write.node");
19849
+ DEFAULT_ADDON_PATH = resolve8(NATIVE_BASE, "build/Release/safe_write.node");
19074
19850
  EXPECTED_EXPORTS = Object.freeze([
19075
19851
  "openRoot",
19076
19852
  "openDir",
@@ -19105,8 +19881,8 @@ __export(safe_fs_exports, {
19105
19881
  loadSafeFs: () => loadSafeFs,
19106
19882
  requireSafeFs: () => requireSafeFs
19107
19883
  });
19108
- import { realpath as realpath3 } from "node:fs/promises";
19109
- import { tmpdir } from "node:os";
19884
+ import { realpath as realpath4 } from "node:fs/promises";
19885
+ import { tmpdir as tmpdir2 } from "node:os";
19110
19886
  async function loadSafeFs(options = {}) {
19111
19887
  const addon = loadNativeAddon(options.addonPath);
19112
19888
  return createBackend(addon);
@@ -19117,7 +19893,7 @@ async function requireSafeFs(options = {}) {
19117
19893
  if (options.root) {
19118
19894
  root = options.root;
19119
19895
  } else {
19120
- root = await realpath3(tmpdir());
19896
+ root = await realpath4(tmpdir2());
19121
19897
  }
19122
19898
  const result = await backend.probe(root);
19123
19899
  if (!result.supported) {
@@ -19142,26 +19918,26 @@ var setup_exports = {};
19142
19918
  __export(setup_exports, {
19143
19919
  setupProject: () => setupProject
19144
19920
  });
19145
- import { execFile as execFileCb } from "node:child_process";
19146
- import { createHash as createHash3 } from "node:crypto";
19921
+ import { execFile as execFileCb2 } from "node:child_process";
19922
+ import { createHash as createHash4 } from "node:crypto";
19147
19923
  import { createReadStream } from "node:fs";
19148
- import { promisify } from "node:util";
19924
+ import { promisify as promisify2 } from "node:util";
19149
19925
  import {
19150
- lstat as lstat4,
19926
+ lstat as lstat5,
19151
19927
  readFile as readFile7,
19152
- readdir as readdir2,
19153
- realpath as realpath4
19928
+ readdir as readdir3,
19929
+ realpath as realpath5
19154
19930
  } from "node:fs/promises";
19155
- import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute5, join as join7, relative as relative5, resolve as resolve8 } from "node:path";
19931
+ import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute6, join as join8, relative as relative6, resolve as resolve9 } from "node:path";
19156
19932
  function setupError(code, message, details = {}) {
19157
19933
  return new ReleaseError(code, message, details);
19158
19934
  }
19159
19935
  function safeRelative(root, path3) {
19160
- const rel = relative5(root, path3).split("\\").join("/");
19936
+ const rel = relative6(root, path3).split("\\").join("/");
19161
19937
  return rel || ".";
19162
19938
  }
19163
19939
  async function readJsonBounded(path3, label) {
19164
- const stat9 = await lstat4(path3);
19940
+ const stat9 = await lstat5(path3);
19165
19941
  if (!stat9.isFile() || stat9.isSymbolicLink() || stat9.size > MAX_JSON_BYTES) {
19166
19942
  throw setupError(CONFIG_INVALID, `${label} must be a regular JSON file no larger than 1 MiB`, { path: path3 });
19167
19943
  }
@@ -19175,11 +19951,11 @@ async function walkDiscoveryFiles(root, maxDepth = 8) {
19175
19951
  const found = [];
19176
19952
  async function walk(directory, depth) {
19177
19953
  if (depth > maxDepth) return;
19178
- const children = await readdir2(directory, { withFileTypes: true });
19954
+ const children = await readdir3(directory, { withFileTypes: true });
19179
19955
  children.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
19180
19956
  for (const child of children) {
19181
19957
  if (child.isSymbolicLink()) continue;
19182
- const absolute = join7(directory, child.name);
19958
+ const absolute = join8(directory, child.name);
19183
19959
  if (child.isDirectory()) {
19184
19960
  if (!SKIP_DIRS.has(child.name)) await walk(absolute, depth + 1);
19185
19961
  } else if (child.isFile() && (child.name === "package.json" || child.name === "public-release.json" || child.name === "SKILL.md" || /^README(?:\.|$)/i.test(child.name) || /^LICENSE(?:\.|$)/i.test(child.name) || /^CHANGELOG(?:\.|$)/i.test(child.name) || [...DISCOVERY_MANIFEST_SUFFIXES].some((suffix) => absolute.endsWith(`/${suffix}`)))) {
@@ -19192,11 +19968,11 @@ async function walkDiscoveryFiles(root, maxDepth = 8) {
19192
19968
  return found;
19193
19969
  }
19194
19970
  async function digestFile(path3) {
19195
- const before = await lstat4(path3);
19971
+ const before = await lstat5(path3);
19196
19972
  if (!before.isFile() || before.isSymbolicLink()) throw setupError(CONFIG_INVALID, "discovered file must be regular", { path: path3 });
19197
- const hash = createHash3("sha256");
19973
+ const hash = createHash4("sha256");
19198
19974
  for await (const chunk of createReadStream(path3)) hash.update(chunk);
19199
- const after = await lstat4(path3);
19975
+ const after = await lstat5(path3);
19200
19976
  if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ino !== after.ino) {
19201
19977
  throw setupError(CONFIG_INVALID, "discovered file changed while setup was reading it", { path: path3 });
19202
19978
  }
@@ -19235,6 +20011,8 @@ function summarizeLegacyReleaseConfig(value, path3) {
19235
20011
  tagPrefix: optionalString(repo.tagPrefix),
19236
20012
  npmPackage: optionalString(repo.npmPackage),
19237
20013
  npmPackageDeclared: Object.hasOwn(repo, "npmPackage"),
20014
+ npmRequiredPathCandidates: stringList(repo.npmRequiredPackagePaths),
20015
+ npmRequiredPathsDeclared: Object.hasOwn(repo, "npmRequiredPackagePaths"),
19238
20016
  docsSource: optionalString(repo.docsSource),
19239
20017
  requiredPathCandidates: stringList(repo.requiredPackagePaths),
19240
20018
  snapshotCommands: Array.isArray(repo.snapshotCommands) ? repo.snapshotCommands : []
@@ -19249,6 +20027,8 @@ function summarizeLegacyReleaseConfig(value, path3) {
19249
20027
  tagPrefix: optionalString(value.tagPrefix),
19250
20028
  npmPackage: plugins.filter((plugin) => plugin && typeof plugin === "object" && !Array.isArray(plugin)).map((plugin) => optionalString(plugin.npmPackage)).find(Boolean) ?? null,
19251
20029
  npmPackageDeclared: plugins.some((plugin) => plugin && typeof plugin === "object" && !Array.isArray(plugin) && Object.hasOwn(plugin, "npmPackage")),
20030
+ npmRequiredPathCandidates: stringList(value.npmRequiredPackagePaths),
20031
+ npmRequiredPathsDeclared: Object.hasOwn(value, "npmRequiredPackagePaths"),
19252
20032
  docsSource: null,
19253
20033
  requiredPathCandidates: stringList(value.requiredPaths),
19254
20034
  snapshotCommands: Array.isArray(value.snapshotCommands) ? value.snapshotCommands : []
@@ -19327,7 +20107,7 @@ function classifyScript(name, command2, unitId, distributionTypes) {
19327
20107
  async function discoverGit(root) {
19328
20108
  const run5 = /* @__PURE__ */ __name(async (args2) => {
19329
20109
  try {
19330
- const { stdout } = await execFile("git", args2, { cwd: root, shell: false, encoding: "utf8", timeout: 5e3 });
20110
+ const { stdout } = await execFile2("git", args2, { cwd: root, shell: false, encoding: "utf8", timeout: 5e3 });
19331
20111
  return stdout.trim();
19332
20112
  } catch {
19333
20113
  return "";
@@ -19357,7 +20137,7 @@ async function discoverGit(root) {
19357
20137
  async function discoverUnitGit(unitAbsDir, parentRoot) {
19358
20138
  const run5 = /* @__PURE__ */ __name(async (cwd, args2) => {
19359
20139
  try {
19360
- const { stdout } = await execFile("git", args2, { cwd, shell: false, encoding: "utf8", timeout: 5e3 });
20140
+ const { stdout } = await execFile2("git", args2, { cwd, shell: false, encoding: "utf8", timeout: 5e3 });
19361
20141
  return stdout.trim();
19362
20142
  } catch {
19363
20143
  return "";
@@ -19405,6 +20185,93 @@ async function discoverUnitGit(unitAbsDir, parentRoot) {
19405
20185
  }
19406
20186
  };
19407
20187
  }
20188
+ async function pathIsGitIgnored(unitAbsDir, target) {
20189
+ try {
20190
+ await execFile2("git", ["check-ignore", "--no-index", "--quiet", "--", target], {
20191
+ cwd: unitAbsDir,
20192
+ shell: false,
20193
+ timeout: 5e3
20194
+ });
20195
+ return true;
20196
+ } catch {
20197
+ return false;
20198
+ }
20199
+ }
20200
+ async function classifyNpmEntryCandidate(unitAbsDir, target, trackedFiles) {
20201
+ const tracked = trackedFiles.includes(target);
20202
+ const ignored = await pathIsGitIgnored(unitAbsDir, target);
20203
+ let stat9 = null;
20204
+ try {
20205
+ stat9 = await lstat5(join8(unitAbsDir, target));
20206
+ } catch (error) {
20207
+ if (error.code !== "ENOENT") throw error;
20208
+ }
20209
+ const exists = stat9 !== null;
20210
+ const regular = stat9?.isFile() === true && stat9?.isSymbolicLink() !== true;
20211
+ const state = exists && !regular ? "NON_REGULAR" : exists && tracked ? "TRACKED_PRESENT" : exists && ignored ? "IGNORED_PRESENT" : exists ? "UNTRACKED_PRESENT" : ignored ? "IGNORED_MISSING" : "MISSING";
20212
+ return { path: target, state, exists, tracked, ignored };
20213
+ }
20214
+ async function discoverNpmEntryCandidates(root, pkg, matchingLegacyUnits, unitGitEntry) {
20215
+ const emptyIndex = /* @__PURE__ */ new Map();
20216
+ const semantic = checkNpmEntryClosure(pkg.entryManifest, emptyIndex);
20217
+ const byPath = /* @__PURE__ */ new Map();
20218
+ const add = /* @__PURE__ */ __name((target, source) => {
20219
+ const current = byPath.get(target) ?? { path: target, sources: /* @__PURE__ */ new Set() };
20220
+ current.sources.add(source);
20221
+ byPath.set(target, current);
20222
+ }, "add");
20223
+ for (const entry of semantic.entries) add(entry.target, `package.json:${entry.field}`);
20224
+ const diagnostics = [
20225
+ ...semantic.errors.filter((error) => error.reason !== "entry_missing").map((error) => ({ code: "NPM_ENTRY_DECLARATION_INVALID", ...error })),
20226
+ ...semantic.diagnostics.map((diagnostic) => ({
20227
+ code: "NPM_ENTRY_WILDCARD_REQUIRES_REVIEW",
20228
+ ...diagnostic
20229
+ }))
20230
+ ];
20231
+ const declaredLegacyPaths = /* @__PURE__ */ new Set();
20232
+ const legacyCoverageDeclared = matchingLegacyUnits.some((unit) => unit.npmRequiredPathsDeclared);
20233
+ for (const legacy of matchingLegacyUnits) {
20234
+ for (const target of legacy.npmRequiredPathCandidates) {
20235
+ const checked = checkNpmEntryClosure({ main: target }, emptyIndex);
20236
+ const normalized = checked.entries[0]?.target;
20237
+ if (!normalized) {
20238
+ diagnostics.push({
20239
+ code: "LEGACY_NPM_REQUIRED_PATH_INVALID",
20240
+ path: target,
20241
+ reason: checked.errors[0]?.reason ?? "invalid_path"
20242
+ });
20243
+ continue;
20244
+ }
20245
+ declaredLegacyPaths.add(normalized);
20246
+ add(normalized, "public-release.json:npmRequiredPackagePaths");
20247
+ }
20248
+ }
20249
+ if (legacyCoverageDeclared) {
20250
+ for (const entry of semantic.entries) {
20251
+ if (!declaredLegacyPaths.has(entry.target)) {
20252
+ diagnostics.push({
20253
+ code: "LEGACY_NPM_ENTRY_COVERAGE_MISSING",
20254
+ path: entry.target,
20255
+ field: entry.field
20256
+ });
20257
+ }
20258
+ }
20259
+ }
20260
+ const unitAbsDir = resolve9(root, pkg.directory);
20261
+ const candidates = [];
20262
+ for (const item of [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path))) {
20263
+ candidates.push({
20264
+ ...await classifyNpmEntryCandidate(
20265
+ unitAbsDir,
20266
+ item.path,
20267
+ unitGitEntry?.trackedFiles ?? []
20268
+ ),
20269
+ sources: [...item.sources].sort()
20270
+ });
20271
+ }
20272
+ diagnostics.sort((a, b) => canonicalJson(a).localeCompare(canonicalJson(b)));
20273
+ return { candidates, diagnostics };
20274
+ }
19408
20275
  async function discoverFacts(root) {
19409
20276
  const files = await walkDiscoveryFiles(root);
19410
20277
  const packageFiles = files.filter((path3) => basename2(path3) === "package.json" && !/[\\/]adapters[\\/](?:claude|codex|kimi)[\\/]package\.json$/.test(path3));
@@ -19429,6 +20296,9 @@ async function discoverFacts(root) {
19429
20296
  private: pkg.private === true,
19430
20297
  repository: parseGithubRepo(pkg.repository),
19431
20298
  publishRegistry: typeof pkg.publishConfig?.registry === "string" ? pkg.publishConfig.registry : null,
20299
+ entryManifest: Object.fromEntries(
20300
+ ["bin", "main", "module", "types", "typings", "exports"].filter((field) => Object.hasOwn(pkg, field)).map((field) => [field, pkg[field]])
20301
+ ),
19432
20302
  files: Array.isArray(pkg.files) ? pkg.files.filter((item) => typeof item === "string").sort() : [],
19433
20303
  scripts: Object.fromEntries(Object.entries(pkg.scripts ?? {}).filter(([, value]) => typeof value === "string").sort(([a], [b]) => a.localeCompare(b)))
19434
20304
  });
@@ -19470,8 +20340,18 @@ async function discoverFacts(root) {
19470
20340
  }).filter((item) => item.name).sort((a, b) => canonicalJson(a).localeCompare(canonicalJson(b)));
19471
20341
  const unitGit = {};
19472
20342
  for (const pkg of packages) {
19473
- const unitAbsDir = resolve8(root, pkg.directory);
20343
+ const unitAbsDir = resolve9(root, pkg.directory);
19474
20344
  unitGit[pkg.directory] = await discoverUnitGit(unitAbsDir, root);
20345
+ const inferredId = safeUnitId(pkg, pkg.directory);
20346
+ const matchingLegacyUnits = legacyReleaseConfigs.flatMap((config) => config.releaseUnits).filter((unit) => unit.id === inferredId || unit.source === pkg.directory || unit.source === dirname4(pkg.path));
20347
+ const npmDiscovery = await discoverNpmEntryCandidates(
20348
+ root,
20349
+ pkg,
20350
+ matchingLegacyUnits,
20351
+ unitGit[pkg.directory]
20352
+ );
20353
+ pkg.npmEntryCandidates = npmDiscovery.candidates;
20354
+ pkg.npmEntryDiagnostics = npmDiscovery.diagnostics;
19475
20355
  }
19476
20356
  return { git: git2, packages, manifests, skills, legacyReleaseConfigs, fileDigests, unitGit };
19477
20357
  }
@@ -19708,6 +20588,8 @@ function buildCandidates(facts) {
19708
20588
  publicFileMappingCandidates: mappingResult.mappings,
19709
20589
  publicFileMappingConflicts: mappingResult.conflicts,
19710
20590
  requiredPublicFileCandidates,
20591
+ npmEntryCandidates: pkg.npmEntryCandidates ?? [],
20592
+ npmEntryDiagnostics: pkg.npmEntryDiagnostics ?? [],
19711
20593
  entrySkillCandidates: entrySkillCandidatesForUnit(facts, pkg, id),
19712
20594
  ...hasAuthorityConflict ? {
19713
20595
  authorityConflict: {
@@ -19828,6 +20710,18 @@ function buildRecommendedProposal(facts, candidates) {
19828
20710
  assumptions
19829
20711
  };
19830
20712
  }
20713
+ if (unit.distributionCandidates.includes("npm") && ((unit.npmEntryCandidates ?? []).some((candidate) => candidate.state !== "TRACKED_PRESENT") || (unit.npmEntryDiagnostics ?? []).length > 0)) {
20714
+ return {
20715
+ answers: null,
20716
+ conflicts: [{
20717
+ code: "NPM_ENTRY_REVIEW_REQUIRED",
20718
+ unit: unit.id,
20719
+ candidates: unit.npmEntryCandidates,
20720
+ diagnostics: unit.npmEntryDiagnostics
20721
+ }],
20722
+ assumptions
20723
+ };
20724
+ }
19831
20725
  const repoOwner = publicRepo.includes("/") ? publicRepo.split("/")[0] : null;
19832
20726
  const npmPublisher = legacyOwner ?? repoOwner ?? null;
19833
20727
  const pkg = facts.packages.find((item) => item.directory === unit.source);
@@ -20001,6 +20895,12 @@ function buildDecisionsRequired(candidates, localOnly) {
20001
20895
  id: `unit:${unit.id}:distributions-and-files`,
20002
20896
  description: `\u9010\u9879\u786E\u8BA4\u6E20\u9053 ${JSON.stringify(unit.distributionCandidates)}\u3001\u516C\u5F00\u6587\u4EF6\u8FB9\u754C\u548C requiredPublicFiles\uFF1B\u5019\u9009\u4E0D\u662F\u6388\u6743\u3002`
20003
20897
  });
20898
+ if ((unit.npmEntryCandidates ?? []).length > 0 || (unit.npmEntryDiagnostics ?? []).length > 0) {
20899
+ decisions.push({
20900
+ id: `unit:${unit.id}:npm-entry-closure`,
20901
+ description: "\u5BA1\u9605 package.json \u5165\u53E3\u53CA\u65E7 npmRequiredPackagePaths \u7684 tracked/untracked/ignored/missing \u72B6\u6001\uFF1Bsetup \u53EA\u62A5\u544A\u5019\u9009\uFF0C\u4E0D\u81EA\u52A8\u5199\u5165 publicFiles \u6216 requiredPublicFiles\u3002"
20902
+ });
20903
+ }
20004
20904
  }
20005
20905
  decisions.push({
20006
20906
  id: "verification-gates",
@@ -20019,7 +20919,9 @@ function buildCompactSummary(report) {
20019
20919
  branchCandidates: [...unit.branchCandidates].sort(),
20020
20920
  distributionCandidates: [...unit.distributionCandidates].sort(),
20021
20921
  publicFileMappingCount: (unit.publicFileMappingCandidates ?? []).length,
20022
- requiredPublicFileCount: (unit.requiredPublicFileCandidates ?? []).length
20922
+ requiredPublicFileCount: (unit.requiredPublicFileCandidates ?? []).length,
20923
+ npmEntryCandidates: unit.npmEntryCandidates ?? [],
20924
+ npmEntryDiagnostics: unit.npmEntryDiagnostics ?? []
20023
20925
  })),
20024
20926
  gateCandidates: (report.gateCandidates ?? []).map((gate) => ({
20025
20927
  id: gate.id,
@@ -20133,8 +21035,8 @@ async function assertConfigDirectoryStillBound(root, safeFs, expected) {
20133
21035
  async function createConfigOnce(root, config, { beforeRename } = {}) {
20134
21036
  const { loadSafeFs: loadSafeFs2 } = await Promise.resolve().then(() => (init_safe_fs(), safe_fs_exports));
20135
21037
  const safeFs = await loadSafeFs2();
20136
- const releaseDir = join7(root, ".release-skill");
20137
- const target = join7(releaseDir, "project.yaml");
21038
+ const releaseDir = join8(root, ".release-skill");
21039
+ const target = join8(releaseDir, "project.yaml");
20138
21040
  const bound = await openBoundConfigDirectory(root, safeFs);
20139
21041
  let tempToken;
20140
21042
  const bytes = Buffer.from(import_yaml.default.stringify(config, { lineWidth: 0 }), "utf8");
@@ -20188,16 +21090,16 @@ async function createConfigOnce(root, config, { beforeRename } = {}) {
20188
21090
  }
20189
21091
  }
20190
21092
  async function setupProject({ root, answersPath, write = false, confirmSetup, faultInjector } = {}) {
20191
- if (!root || typeof root !== "string" || !isAbsolute5(root)) {
21093
+ if (!root || typeof root !== "string" || !isAbsolute6(root)) {
20192
21094
  throw setupError(CONFIG_INVALID, "setup root must be an absolute path");
20193
21095
  }
20194
- const rootReal = await realpath4(root).catch((error) => {
21096
+ const rootReal = await realpath5(root).catch((error) => {
20195
21097
  throw setupError(CONFIG_INVALID, `cannot resolve setup root: ${error.message}`);
20196
21098
  });
20197
- const configPath = join7(rootReal, ".release-skill", "project.yaml");
21099
+ const configPath = join8(rootReal, ".release-skill", "project.yaml");
20198
21100
  let configExists = false;
20199
21101
  try {
20200
- const stat9 = await lstat4(configPath);
21102
+ const stat9 = await lstat5(configPath);
20201
21103
  configExists = true;
20202
21104
  if (!stat9.isFile() || stat9.isSymbolicLink()) {
20203
21105
  throw setupError(CONFIG_INVALID, "existing project.yaml must be a regular file");
@@ -20253,7 +21155,8 @@ async function setupProject({ root, answersPath, write = false, confirmSetup, fa
20253
21155
  ...parseError ? ["\u5DF2\u6709\u914D\u7F6E\u65E0\u6CD5\u89E3\u6790\uFF1B\u5148\u4EBA\u5DE5\u4FEE\u590D\uFF0C\u518D\u8FD0\u884C release-assess\u3002"] : [],
20254
21156
  ...validationErrors.length > 0 ? ["\u5DF2\u6709\u914D\u7F6E\u4E0D\u7B26\u5408 release-project schema\uFF1B\u6309 validationErrors \u4EBA\u5DE5\u589E\u91CF\u4FEE\u590D\uFF0C\u4E0D\u91CD\u65B0\u751F\u6210\u3002"] : [],
20255
21157
  ...canonicalJson(configuredUnitIds) !== canonicalJson(discoveredUnitIds) ? ["\u53D1\u73B0\u7684\u53D1\u5E03\u5355\u5143\u4E0E\u5DF2\u6709\u914D\u7F6E\u4E0D\u540C\uFF1B\u4EBA\u5DE5\u6BD4\u8F83\u540E\u4EC5\u505A\u589E\u91CF\u7F16\u8F91\uFF0C\u4E0D\u91CD\u65B0\u751F\u6210\u3002"] : [],
20256
- ...unconfiguredGateCandidateIds.length > 0 ? ["\u5B58\u5728\u672A\u914D\u7F6E\u7684\u9A8C\u8BC1\u5019\u9009\uFF1B\u9010\u9879\u5BA1\u9605\u526F\u4F5C\u7528\u540E\u51B3\u5B9A\u662F\u5426\u4EBA\u5DE5\u6CE8\u518C\u3002"] : []
21158
+ ...unconfiguredGateCandidateIds.length > 0 ? ["\u5B58\u5728\u672A\u914D\u7F6E\u7684\u9A8C\u8BC1\u5019\u9009\uFF1B\u9010\u9879\u5BA1\u9605\u526F\u4F5C\u7528\u540E\u51B3\u5B9A\u662F\u5426\u4EBA\u5DE5\u6CE8\u518C\u3002"] : [],
21159
+ ...candidates2.units.some((unit) => (unit.npmEntryCandidates ?? []).some((candidate) => candidate.state !== "TRACKED_PRESENT") || (unit.npmEntryDiagnostics ?? []).length > 0) ? ["npm \u5165\u53E3\u5019\u9009\u5B58\u5728\u672A\u8DDF\u8E2A\u3001\u88AB\u5FFD\u7565\u3001\u7F3A\u5931\u3001\u975E\u666E\u901A\u6587\u4EF6\u6216\u58F0\u660E\u8986\u76D6\u7591\u70B9\uFF1B\u4EBA\u5DE5\u589E\u91CF\u4FEE\u590D\u914D\u7F6E\u6216\u6784\u5EFA\u6D41\u7A0B\uFF0Csetup \u4E0D\u81EA\u52A8\u6539\u5199\u3002"] : []
20257
21160
  ]
20258
21161
  },
20259
21162
  recommendedGateIds: [],
@@ -20272,7 +21175,7 @@ async function setupProject({ root, answersPath, write = false, confirmSetup, fa
20272
21175
  )].sort();
20273
21176
  let answers = null;
20274
21177
  if (answersPath) {
20275
- const resolvedAnswers = isAbsolute5(answersPath) ? answersPath : resolve8(rootReal, answersPath);
21178
+ const resolvedAnswers = isAbsolute6(answersPath) ? answersPath : resolve9(rootReal, answersPath);
20276
21179
  answers = await readJsonBounded(resolvedAnswers, "setup answers");
20277
21180
  validateAnswers(answers, candidates.gates);
20278
21181
  }
@@ -20326,7 +21229,7 @@ async function setupProject({ root, answersPath, write = false, confirmSetup, fa
20326
21229
  if (faultInjector) await faultInjector("before-config-commit");
20327
21230
  const lockedFacts = await discoverFacts(rootReal);
20328
21231
  const lockedCandidates = buildCandidates(lockedFacts);
20329
- const resolvedAnswers = isAbsolute5(answersPath) ? answersPath : resolve8(rootReal, answersPath);
21232
+ const resolvedAnswers = isAbsolute6(answersPath) ? answersPath : resolve9(rootReal, answersPath);
20330
21233
  const lockedAnswers = await readJsonBounded(resolvedAnswers, "setup answers");
20331
21234
  validateAnswers(lockedAnswers, lockedCandidates.gates);
20332
21235
  const lockedAuthority = {
@@ -20399,7 +21302,7 @@ async function setupProject({ root, answersPath, write = false, confirmSetup, fa
20399
21302
  createdReport.compactSummary = buildCompactSummary(createdReport);
20400
21303
  return createdReport;
20401
21304
  }
20402
- var import_yaml, import_ajv2, import_ajv_formats2, execFile, SKIP_DIRS, DISCOVERY_MANIFEST_SUFFIXES, MANIFEST_SUFFIX_HOST_MAP, MAX_JSON_BYTES, schema, ajv2, validateProjectConfig;
21305
+ var import_yaml, import_ajv2, import_ajv_formats2, execFile2, SKIP_DIRS, DISCOVERY_MANIFEST_SUFFIXES, MANIFEST_SUFFIX_HOST_MAP, MAX_JSON_BYTES, schema, ajv2, validateProjectConfig;
20403
21306
  var init_setup = __esm({
20404
21307
  async "src/commands/setup.mjs"() {
20405
21308
  import_yaml = __toESM(require_dist(), 1);
@@ -20408,9 +21311,10 @@ var init_setup = __esm({
20408
21311
  init_digest();
20409
21312
  init_project_lock();
20410
21313
  await init_registry();
21314
+ init_npm_entry_closure();
20411
21315
  init_errors();
20412
21316
  init_trusted_resource();
20413
- execFile = promisify(execFileCb);
21317
+ execFile2 = promisify2(execFileCb2);
20414
21318
  SKIP_DIRS = /* @__PURE__ */ new Set([
20415
21319
  ".git",
20416
21320
  ".release-skill",
@@ -20500,6 +21404,9 @@ var init_setup = __esm({
20500
21404
  __name(classifyScript, "classifyScript");
20501
21405
  __name(discoverGit, "discoverGit");
20502
21406
  __name(discoverUnitGit, "discoverUnitGit");
21407
+ __name(pathIsGitIgnored, "pathIsGitIgnored");
21408
+ __name(classifyNpmEntryCandidate, "classifyNpmEntryCandidate");
21409
+ __name(discoverNpmEntryCandidates, "discoverNpmEntryCandidates");
20503
21410
  __name(discoverFacts, "discoverFacts");
20504
21411
  __name(packagePatternMatches, "packagePatternMatches");
20505
21412
  __name(buildPublicFileMappingCandidates, "buildPublicFileMappingCandidates");
@@ -20561,7 +21468,7 @@ var init_path_key = __esm({
20561
21468
  });
20562
21469
 
20563
21470
  // src/snapshot/public-path.mjs
20564
- import { isAbsolute as isAbsolute6, relative as relative6, resolve as resolve9 } from "node:path";
21471
+ import { isAbsolute as isAbsolute7, relative as relative7, resolve as resolve10 } from "node:path";
20565
21472
  function canonicalPublicPath(raw, { allowDot = false } = {}) {
20566
21473
  if (allowDot && raw === ".") {
20567
21474
  return { path: "." };
@@ -20591,11 +21498,11 @@ function resolveUnitScopedPath(baseDir, unitId, { suffix = "" } = {}) {
20591
21498
  { unitId }
20592
21499
  );
20593
21500
  }
20594
- const base = resolve9(baseDir);
20595
- const candidate = resolve9(base, `${unitId}${suffix}`);
20596
- const rel = relative6(base, candidate);
21501
+ const base = resolve10(baseDir);
21502
+ const candidate = resolve10(base, `${unitId}${suffix}`);
21503
+ const rel = relative7(base, candidate);
20597
21504
  const separator = process.platform === "win32" ? "\\" : "/";
20598
- if (rel === "" || isAbsolute6(rel) || rel === ".." || rel.startsWith(`..${separator}`)) {
21505
+ if (rel === "" || isAbsolute7(rel) || rel === ".." || rel.startsWith(`..${separator}`)) {
20599
21506
  throw new ReleaseError(
20600
21507
  PUBLIC_PATH_FORBIDDEN,
20601
21508
  `release unit path escapes its trusted base: "${unitId}"`,
@@ -20617,11 +21524,11 @@ var init_public_path = __esm({
20617
21524
  });
20618
21525
 
20619
21526
  // src/core/baseline.mjs
20620
- import { execFile as execFileCb2 } from "node:child_process";
20621
- import { promisify as promisify2 } from "node:util";
20622
- import { createHash as createHash4 } from "node:crypto";
20623
- import { lstat as lstat5, open as open3, readlink } from "node:fs/promises";
20624
- import { constants as fsConstants } from "node:fs";
21527
+ import { execFile as execFileCb3 } from "node:child_process";
21528
+ import { promisify as promisify3 } from "node:util";
21529
+ import { createHash as createHash5 } from "node:crypto";
21530
+ import { lstat as lstat6, open as open4, readlink } from "node:fs/promises";
21531
+ import { constants as fsConstants2 } from "node:fs";
20625
21532
  import path from "node:path";
20626
21533
  function isControlPlanePath(p) {
20627
21534
  const normalized = p;
@@ -20667,9 +21574,9 @@ async function computeWorkspaceDigest(root) {
20667
21574
  { stdout: changedOut },
20668
21575
  { stdout: untrackedOut }
20669
21576
  ] = await Promise.all([
20670
- execFile2("git", ["ls-files", "-s", "-z"], opts),
20671
- execFile2("git", ["diff", "--name-only", "-z"], opts),
20672
- execFile2("git", ["ls-files", "-o", "--exclude-standard", "-z"], opts)
21577
+ execFile3("git", ["ls-files", "-s", "-z"], opts),
21578
+ execFile3("git", ["diff", "--name-only", "-z"], opts),
21579
+ execFile3("git", ["ls-files", "-o", "--exclude-standard", "-z"], opts)
20673
21580
  ]);
20674
21581
  const parts = [];
20675
21582
  for (const record of splitNul(stagedOut)) {
@@ -20682,7 +21589,7 @@ async function computeWorkspaceDigest(root) {
20682
21589
  const diffOpts = { ...opts, maxBuffer: DIFF_MAX_BUFFER };
20683
21590
  const changedPaths = splitNul(changedOut).filter((p) => !isControlPlanePath(p));
20684
21591
  for (const changedPath of changedPaths) {
20685
- const { stdout: patch } = await execFile2(
21592
+ const { stdout: patch } = await execFile3(
20686
21593
  "git",
20687
21594
  ["diff", "--no-ext-diff", "--no-textconv", "--binary", "--no-color", "--", changedPath],
20688
21595
  diffOpts
@@ -20699,7 +21606,7 @@ async function computeWorkspaceDigest(root) {
20699
21606
  `Refusing to read path outside repository root: ${relPath} resolves to ${absPath}`
20700
21607
  );
20701
21608
  }
20702
- const stat9 = await lstat5(absPath);
21609
+ const stat9 = await lstat6(absPath);
20703
21610
  let type;
20704
21611
  if (stat9.isSymbolicLink()) {
20705
21612
  type = "symlink";
@@ -20713,7 +21620,7 @@ async function computeWorkspaceDigest(root) {
20713
21620
  const typeMarker = `TYPE:${type}`;
20714
21621
  let content;
20715
21622
  if (stat9.isFile()) {
20716
- const handle = await open3(absPath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
21623
+ const handle = await open4(absPath, fsConstants2.O_RDONLY | fsConstants2.O_NOFOLLOW);
20717
21624
  try {
20718
21625
  const fdStat = await handle.stat();
20719
21626
  if (fdStat.ino !== stat9.ino) {
@@ -20739,21 +21646,21 @@ async function computeWorkspaceDigest(root) {
20739
21646
  return [
20740
21647
  relPath,
20741
21648
  typeMarker,
20742
- createHash4("sha256").update(content).digest("hex")
21649
+ createHash5("sha256").update(content).digest("hex")
20743
21650
  ].join(" ");
20744
21651
  })
20745
21652
  );
20746
21653
  parts.sort();
20747
21654
  untrackedParts.sort();
20748
21655
  const combined = parts.join("\n") + (parts.length && untrackedParts.length ? "\n" : "") + untrackedParts.join("\n");
20749
- return createHash4("sha256").update(combined).digest("hex");
21656
+ return createHash5("sha256").update(combined).digest("hex");
20750
21657
  }
20751
21658
  async function captureBaseline(root) {
20752
21659
  const opts = { cwd: root, shell: false, encoding: "utf8" };
20753
21660
  const [{ stdout: headOut }, { stdout: treeOut }, { stdout: statusOut }] = await Promise.all([
20754
- execFile2("git", ["rev-parse", "HEAD"], opts),
20755
- execFile2("git", ["rev-parse", "HEAD^{tree}"], opts),
20756
- execFile2("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all"], opts)
21661
+ execFile3("git", ["rev-parse", "HEAD"], opts),
21662
+ execFile3("git", ["rev-parse", "HEAD^{tree}"], opts),
21663
+ execFile3("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all"], opts)
20757
21664
  ]);
20758
21665
  const gitHead = headOut.trim();
20759
21666
  const gitTreeHash = treeOut.trim();
@@ -20768,10 +21675,10 @@ async function captureBaseline(root) {
20768
21675
  capturedAt: (/* @__PURE__ */ new Date()).toISOString()
20769
21676
  };
20770
21677
  }
20771
- var execFile2, WORKSPACE_DIGEST_ALGORITHM, CONTROL_PLANE_PREFIXES, RESERVED_CONTROL_PREFIXES, CONTROL_PLANE_EXACT, DIGEST, IMMUTABLE_PLAN, IMMUTABLE_APPROVAL;
21678
+ var execFile3, WORKSPACE_DIGEST_ALGORITHM, CONTROL_PLANE_PREFIXES, RESERVED_CONTROL_PREFIXES, CONTROL_PLANE_EXACT, DIGEST, IMMUTABLE_PLAN, IMMUTABLE_APPROVAL;
20772
21679
  var init_baseline = __esm({
20773
21680
  "src/core/baseline.mjs"() {
20774
- execFile2 = promisify2(execFileCb2);
21681
+ execFile3 = promisify3(execFileCb3);
20775
21682
  WORKSPACE_DIGEST_ALGORITHM = "git-workspace-v2";
20776
21683
  CONTROL_PLANE_PREFIXES = [
20777
21684
  ".release-skill/lock",
@@ -20823,7 +21730,7 @@ var init_baseline = __esm({
20823
21730
 
20824
21731
  // src/core/config.mjs
20825
21732
  import { readFile as readFile8 } from "node:fs/promises";
20826
- import { resolve as resolve10, isAbsolute as isAbsolute7, relative as relative7, normalize as normalize2 } from "node:path";
21733
+ import { resolve as resolve11, isAbsolute as isAbsolute8, relative as relative8, normalize as normalize2 } from "node:path";
20827
21734
  function containsAlias(node) {
20828
21735
  if (!node || typeof node !== "object") {
20829
21736
  return false;
@@ -20848,9 +21755,9 @@ function containsAlias(node) {
20848
21755
  }
20849
21756
  function resolveConfigPath(root, configPath) {
20850
21757
  const rootNorm = normalize2(root);
20851
- const resolved = isAbsolute7(configPath) ? normalize2(configPath) : resolve10(rootNorm, configPath);
20852
- const rel = relative7(rootNorm, resolved);
20853
- if (rel.startsWith("..") || rel === ".." || isAbsolute7(rel)) {
21758
+ const resolved = isAbsolute8(configPath) ? normalize2(configPath) : resolve11(rootNorm, configPath);
21759
+ const rel = relative8(rootNorm, resolved);
21760
+ if (rel.startsWith("..") || rel === ".." || isAbsolute8(rel)) {
20854
21761
  throw new ReleaseError(
20855
21762
  CONFIG_INVALID,
20856
21763
  `config path "${configPath}" resolves outside project root "${root}"`,
@@ -21147,10 +22054,10 @@ __export(assess_exports, {
21147
22054
  Severity: () => Severity,
21148
22055
  assessProject: () => assessProject
21149
22056
  });
21150
- import { readFile as readFile9, stat as stat2, writeFile as writeFile4, mkdir as mkdir3 } from "node:fs/promises";
21151
- import { resolve as resolve11, dirname as dirname5, relative as relative8 } from "node:path";
21152
- import { execFile as execFileCb3 } from "node:child_process";
21153
- import { promisify as promisify3 } from "node:util";
22057
+ import { readFile as readFile9, stat as stat2, writeFile as writeFile4, mkdir as mkdir4 } from "node:fs/promises";
22058
+ import { resolve as resolve12, dirname as dirname5, relative as relative9 } from "node:path";
22059
+ import { execFile as execFileCb4 } from "node:child_process";
22060
+ import { promisify as promisify4 } from "node:util";
21154
22061
  async function fileExists(filePath) {
21155
22062
  try {
21156
22063
  await stat2(filePath);
@@ -21249,9 +22156,9 @@ async function checkCommonDocs(root, config) {
21249
22156
  { file: "LICENSE", code: "LICENSE_MISSING", message: "\u7F3A\u5C11 LICENSE \u6587\u4EF6" }
21250
22157
  ];
21251
22158
  for (const unit of units) {
21252
- const unitRoot = resolve11(root, unit.source);
22159
+ const unitRoot = resolve12(root, unit.source);
21253
22160
  for (const doc of requiredDocs) {
21254
- const exists = await fileExists(resolve11(unitRoot, doc.file));
22161
+ const exists = await fileExists(resolve12(unitRoot, doc.file));
21255
22162
  if (!exists) {
21256
22163
  gaps.push(
21257
22164
  createGap({
@@ -21272,9 +22179,9 @@ async function checkCommonDocs(root, config) {
21272
22179
  { file: "CONTRIBUTING.md", code: "CONTRIBUTING_MISSING", message: "\u5EFA\u8BAE\u6DFB\u52A0 CONTRIBUTING.md" }
21273
22180
  ];
21274
22181
  for (const unit of units) {
21275
- const unitRoot = resolve11(root, unit.source);
22182
+ const unitRoot = resolve12(root, unit.source);
21276
22183
  for (const doc of recommendedDocs) {
21277
- const exists = await fileExists(resolve11(unitRoot, doc.file));
22184
+ const exists = await fileExists(resolve12(unitRoot, doc.file));
21278
22185
  if (!exists) {
21279
22186
  gaps.push(
21280
22187
  createGap({
@@ -21296,9 +22203,9 @@ async function checkPluginManifests(root, config) {
21296
22203
  const units = config.releaseUnits ?? [];
21297
22204
  for (const unit of units) {
21298
22205
  const distributionTypes = new Set((unit.distributions ?? []).map((dist) => dist.type));
21299
- const unitRoot = resolve11(root, unit.source);
22206
+ const unitRoot = resolve12(root, unit.source);
21300
22207
  if (distributionTypes.has("claude-plugin")) {
21301
- const manifestPath = resolve11(unitRoot, ".claude-plugin", "plugin.json");
22208
+ const manifestPath = resolve12(unitRoot, ".claude-plugin", "plugin.json");
21302
22209
  const displayPath = unitFile(unit, ".claude-plugin/plugin.json");
21303
22210
  const exists = await fileExists(manifestPath);
21304
22211
  if (!exists) {
@@ -21345,7 +22252,7 @@ async function checkPluginManifests(root, config) {
21345
22252
  }
21346
22253
  }
21347
22254
  if (distributionTypes.has("codex-plugin")) {
21348
- const manifestPath = resolve11(unitRoot, ".codex-plugin", "plugin.json");
22255
+ const manifestPath = resolve12(unitRoot, ".codex-plugin", "plugin.json");
21349
22256
  const displayPath = unitFile(unit, ".codex-plugin/plugin.json");
21350
22257
  const exists = await fileExists(manifestPath);
21351
22258
  if (!exists) {
@@ -21392,7 +22299,7 @@ async function checkPluginManifests(root, config) {
21392
22299
  }
21393
22300
  }
21394
22301
  if (distributionTypes.has("kimi-plugin")) {
21395
- const manifestPath = resolve11(unitRoot, ".kimi-plugin", "plugin.json");
22302
+ const manifestPath = resolve12(unitRoot, ".kimi-plugin", "plugin.json");
21396
22303
  const displayPath = unitFile(unit, ".kimi-plugin/plugin.json");
21397
22304
  const exists = await fileExists(manifestPath);
21398
22305
  if (!exists) {
@@ -21439,7 +22346,7 @@ async function checkPluginManifests(root, config) {
21439
22346
  }
21440
22347
  }
21441
22348
  if (distributionTypes.has("codebuddy-plugin")) {
21442
- const manifestPath = resolve11(unitRoot, ".codebuddy-plugin", "plugin.json");
22349
+ const manifestPath = resolve12(unitRoot, ".codebuddy-plugin", "plugin.json");
21443
22350
  const displayPath = unitFile(unit, ".codebuddy-plugin/plugin.json");
21444
22351
  const exists = await fileExists(manifestPath);
21445
22352
  if (!exists) {
@@ -21505,8 +22412,8 @@ async function checkPackageMetadata(root, config) {
21505
22412
  for (const unit of units) {
21506
22413
  const hasNpmDist = (unit.distributions ?? []).some((d) => d.type === "npm");
21507
22414
  if (!hasNpmDist) continue;
21508
- const unitRoot = resolve11(root, unit.source);
21509
- const pkgPath = resolve11(unitRoot, "package.json");
22415
+ const unitRoot = resolve12(root, unit.source);
22416
+ const pkgPath = resolve12(unitRoot, "package.json");
21510
22417
  const exists = await fileExists(pkgPath);
21511
22418
  if (!exists) {
21512
22419
  gaps.push(
@@ -21558,7 +22465,7 @@ async function checkRemotePrerequisites(root, config, offline) {
21558
22465
  const gaps = [];
21559
22466
  const units = config.releaseUnits ?? [];
21560
22467
  try {
21561
- const { stdout } = await execFile3("git", ["remote", "get-url", "origin"], {
22468
+ const { stdout } = await execFile4("git", ["remote", "get-url", "origin"], {
21562
22469
  cwd: root,
21563
22470
  shell: false,
21564
22471
  encoding: "utf8",
@@ -21589,7 +22496,7 @@ async function checkRemotePrerequisites(root, config, offline) {
21589
22496
  for (const unit of units) {
21590
22497
  const npmDist = (unit.distributions ?? []).find((d) => d.type === "npm");
21591
22498
  if (!npmDist?.package) continue;
21592
- const pkgPath = resolve11(root, unit.source, "package.json");
22499
+ const pkgPath = resolve12(root, unit.source, "package.json");
21593
22500
  let version;
21594
22501
  try {
21595
22502
  const content = await readFile9(pkgPath, "utf8");
@@ -21600,7 +22507,7 @@ async function checkRemotePrerequisites(root, config, offline) {
21600
22507
  }
21601
22508
  if (!version) continue;
21602
22509
  try {
21603
- await execFile3(
22510
+ await execFile4(
21604
22511
  "npm",
21605
22512
  ["view", `${npmDist.package}@${version}`, "version"],
21606
22513
  { cwd: root, shell: false, encoding: "utf8", timeout: 15e3 }
@@ -21623,7 +22530,7 @@ async function checkReadmeStructure(root, config) {
21623
22530
  const gaps = [];
21624
22531
  const units = config.releaseUnits ?? [];
21625
22532
  for (const unit of units) {
21626
- const readmePath = resolve11(root, unit.source, "README.md");
22533
+ const readmePath = resolve12(root, unit.source, "README.md");
21627
22534
  const displayPath = unitFile(unit, "README.md");
21628
22535
  const exists = await fileExists(readmePath);
21629
22536
  if (!exists) continue;
@@ -21800,15 +22707,15 @@ async function assessProject(options) {
21800
22707
  }
21801
22708
  async function writeReport(outputPath, report) {
21802
22709
  const dir = dirname5(outputPath);
21803
- await mkdir3(dir, { recursive: true });
22710
+ await mkdir4(dir, { recursive: true });
21804
22711
  await writeFile4(outputPath, JSON.stringify(report, null, 2), "utf8");
21805
22712
  }
21806
- var execFile3, Severity, GapScope, GapCategory;
22713
+ var execFile4, Severity, GapScope, GapCategory;
21807
22714
  var init_assess = __esm({
21808
22715
  async "src/commands/assess.mjs"() {
21809
22716
  await init_config();
21810
22717
  init_errors();
21811
- execFile3 = promisify3(execFileCb3);
22718
+ execFile4 = promisify4(execFileCb4);
21812
22719
  Severity = Object.freeze({
21813
22720
  ERROR: "error",
21814
22721
  WARNING: "warning"
@@ -21844,10 +22751,10 @@ var init_assess = __esm({
21844
22751
  });
21845
22752
 
21846
22753
  // src/core/hooks.mjs
21847
- import { execFile as execFileCb4 } from "node:child_process";
21848
- import { promisify as promisify4 } from "node:util";
21849
- import { resolve as resolve12, relative as relative9, isAbsolute as isAbsolute8 } from "node:path";
21850
- import { realpath as realpath5 } from "node:fs/promises";
22754
+ import { execFile as execFileCb5 } from "node:child_process";
22755
+ import { promisify as promisify5 } from "node:util";
22756
+ import { resolve as resolve13, relative as relative10, isAbsolute as isAbsolute9 } from "node:path";
22757
+ import { realpath as realpath6 } from "node:fs/promises";
21851
22758
  function validateHook(hook) {
21852
22759
  if (!hook || typeof hook !== "object" || Array.isArray(hook)) {
21853
22760
  throw new ReleaseError("INVALID_HOOK", "hook must be a non-null object");
@@ -21858,858 +22765,286 @@ function validateHook(hook) {
21858
22765
  }
21859
22766
  for (const c of command2) {
21860
22767
  if (typeof c !== "string") {
21861
- throw new ReleaseError("INVALID_HOOK", "every element of hook.command must be a string");
21862
- }
21863
- }
21864
- if (cwd !== void 0 && typeof cwd !== "string") {
21865
- throw new ReleaseError("INVALID_HOOK", "hook.cwd must be a string when provided");
21866
- }
21867
- if (timeoutMs !== void 0) {
21868
- if (typeof timeoutMs !== "number" || !Number.isInteger(timeoutMs) || timeoutMs < 1) {
21869
- throw new ReleaseError("INVALID_HOOK", "hook.timeoutMs must be a positive integer");
21870
- }
21871
- }
21872
- if (envAllowlist !== void 0) {
21873
- if (!Array.isArray(envAllowlist)) {
21874
- throw new ReleaseError("INVALID_HOOK", "hook.envAllowlist must be an array");
21875
- }
21876
- for (const key of envAllowlist) {
21877
- if (typeof key !== "string" || !ENV_KEY_PATTERN.test(key)) {
21878
- throw new ReleaseError(
21879
- "INVALID_HOOK",
21880
- `hook.envAllowlist key "${key}" must match /^[A-Z_][A-Z0-9_]*$/`
21881
- );
21882
- }
21883
- }
21884
- }
21885
- if (cacheable !== void 0 && typeof cacheable !== "boolean") {
21886
- throw new ReleaseError("INVALID_HOOK", "hook.cacheable must be a boolean when provided");
21887
- }
21888
- if (cacheInputs !== void 0) {
21889
- if (!Array.isArray(cacheInputs) || cacheInputs.length === 0) {
21890
- throw new ReleaseError(
21891
- "INVALID_HOOK",
21892
- "hook.cacheInputs must be a non-empty array of glob strings when provided"
21893
- );
21894
- }
21895
- for (const glob of cacheInputs) {
21896
- if (typeof glob !== "string" || glob.length === 0) {
21897
- throw new ReleaseError(
21898
- "INVALID_HOOK",
21899
- "every element of hook.cacheInputs must be a non-empty string"
21900
- );
21901
- }
21902
- }
21903
- }
21904
- if (cacheable === true && (!Array.isArray(cacheInputs) || cacheInputs.length === 0)) {
21905
- throw new ReleaseError(
21906
- "INVALID_HOOK",
21907
- "hook.cacheable=true requires a non-empty hook.cacheInputs"
21908
- );
21909
- }
21910
- }
21911
- function buildFilteredEnv(envAllowlist, contextEnv) {
21912
- const env = {};
21913
- for (const key of PLATFORM_REQUIRED_VARS) {
21914
- const val = process.env[key];
21915
- if (val !== void 0) {
21916
- env[key] = val;
21917
- }
21918
- }
21919
- if (contextEnv && envAllowlist) {
21920
- for (const key of envAllowlist) {
21921
- if (key in contextEnv) {
21922
- env[key] = contextEnv[key];
21923
- }
21924
- }
21925
- }
21926
- return env;
21927
- }
21928
- async function runHook(hook, context) {
21929
- validateHook(hook);
21930
- if (!context || typeof context !== "object" || Array.isArray(context)) {
21931
- throw new ReleaseError("INVALID_HOOK", "context must be a non-null object");
21932
- }
21933
- if (typeof context.root !== "string" || !isAbsolute8(context.root)) {
21934
- throw new ReleaseError("INVALID_HOOK", "context.root must be an absolute path");
21935
- }
21936
- const { command: command2, cwd, timeoutMs, envAllowlist = [] } = hook;
21937
- const rootReal = await realpath5(context.root);
21938
- const resolvedCwd = cwd ? await realpath5(resolve12(rootReal, cwd)) : rootReal;
21939
- const rel = relative9(rootReal, resolvedCwd);
21940
- if (rel.startsWith("..") || rel === "..") {
21941
- throw new ReleaseError(
21942
- "INVALID_HOOK",
21943
- `hook.cwd "${cwd}" resolves outside project root`
21944
- );
21945
- }
21946
- const env = buildFilteredEnv(envAllowlist, context.env);
21947
- const executable = command2[0];
21948
- const args2 = command2.slice(1);
21949
- let timeoutController;
21950
- let timeoutHandle;
21951
- if (timeoutMs && timeoutMs > 0) {
21952
- timeoutController = new AbortController();
21953
- timeoutHandle = setTimeout(() => timeoutController.abort(), timeoutMs);
21954
- }
21955
- try {
21956
- const { stdout, stderr } = await execFileAsync(executable, args2, {
21957
- cwd: resolvedCwd,
21958
- env,
21959
- shell: false,
21960
- maxBuffer: 10 * 1024 * 1024,
21961
- // 10 MiB
21962
- signal: timeoutController?.signal
21963
- });
21964
- return { exitCode: 0, stdout: stdout ?? "", stderr: stderr ?? "" };
21965
- } catch (err) {
21966
- if (err.name === "AbortError" || err.killed && err.signal === "SIGTERM") {
21967
- throw new ReleaseError(
21968
- "HOOK_TIMEOUT",
21969
- `hook timed out after ${timeoutMs}ms: ${executable} ${args2.join(" ")}`,
21970
- { command: command2, timeoutMs }
21971
- );
21972
- }
21973
- if ("stdout" in err) {
21974
- return {
21975
- exitCode: typeof err.code === "number" ? err.code : 1,
21976
- stdout: err.stdout ?? "",
21977
- stderr: err.stderr ?? ""
21978
- };
21979
- }
21980
- throw err;
21981
- } finally {
21982
- if (timeoutHandle) {
21983
- clearTimeout(timeoutHandle);
21984
- }
21985
- }
21986
- }
21987
- var execFileAsync, PLATFORM_REQUIRED_VARS, ENV_KEY_PATTERN;
21988
- var init_hooks = __esm({
21989
- "src/core/hooks.mjs"() {
21990
- init_errors();
21991
- execFileAsync = promisify4(execFileCb4);
21992
- PLATFORM_REQUIRED_VARS = (() => {
21993
- const posix3 = ["PATH", "HOME", "USER", "LANG", "LC_ALL", "LC_CTYPE", "TERM"];
21994
- const win322 = ["PATH", "HOME", "USER", "SystemRoot", "SYSTEMROOT", "COMSPEC", "PATHEXT", "TEMP", "TMP"];
21995
- return new Set(process.platform === "win32" ? win322 : posix3);
21996
- })();
21997
- ENV_KEY_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
21998
- __name(validateHook, "validateHook");
21999
- __name(buildFilteredEnv, "buildFilteredEnv");
22000
- __name(runHook, "runHook");
22001
- }
22002
- });
22003
-
22004
- // src/core/hook-cache.mjs
22005
- import { readdir as readdir3, readFile as readFile10, mkdir as mkdir4, writeFile as writeFile5 } from "node:fs/promises";
22006
- import { join as join8 } from "node:path";
22007
- function globToRegExp(glob) {
22008
- let source = "";
22009
- let i = 0;
22010
- while (i < glob.length) {
22011
- const c = glob[i];
22012
- if (c === "*") {
22013
- if (glob[i + 1] === "*") {
22014
- source += ".*";
22015
- i += 2;
22016
- } else {
22017
- source += "[^/]*";
22018
- i += 1;
22019
- }
22020
- } else if (c === "?") {
22021
- source += "[^/]";
22022
- i += 1;
22023
- } else {
22024
- source += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
22025
- i += 1;
22026
- }
22027
- }
22028
- return new RegExp(`^${source}$`);
22029
- }
22030
- async function listInputFiles(root) {
22031
- const out = [];
22032
- async function walk(dirAbs, dirRel) {
22033
- let entries;
22034
- try {
22035
- entries = await readdir3(dirAbs, { withFileTypes: true });
22036
- } catch {
22037
- return;
22038
- }
22039
- for (const entry of entries) {
22040
- if (entry.isDirectory()) {
22041
- if (SKIPPED_DIRS.has(entry.name)) continue;
22042
- const rel = dirRel ? `${dirRel}/${entry.name}` : entry.name;
22043
- await walk(join8(dirAbs, entry.name), rel);
22044
- } else if (entry.isFile()) {
22045
- out.push(dirRel ? `${dirRel}/${entry.name}` : entry.name);
22046
- }
22047
- }
22048
- }
22049
- __name(walk, "walk");
22050
- await walk(root, "");
22051
- out.sort();
22052
- return out;
22053
- }
22054
- async function computeHookCacheKey(hook, root) {
22055
- const globs = Array.isArray(hook.cacheInputs) ? hook.cacheInputs : [];
22056
- const matchers = globs.map((glob) => ({ glob, re: globToRegExp(glob) }));
22057
- const allFiles = await listInputFiles(root);
22058
- const matched = [];
22059
- const hitPerGlob = matchers.map(() => false);
22060
- for (const relPath of allFiles) {
22061
- let hit = false;
22062
- for (let i = 0; i < matchers.length; i += 1) {
22063
- if (matchers[i].re.test(relPath)) {
22064
- hitPerGlob[i] = true;
22065
- hit = true;
22066
- }
22067
- }
22068
- if (hit) matched.push(relPath);
22069
- }
22070
- for (let i = 0; i < matchers.length; i += 1) {
22071
- if (!hitPerGlob[i]) {
22072
- throw new ReleaseError(
22073
- GATE_FAILED,
22074
- `hook cacheInputs glob "${matchers[i].glob}" matched no files; refusing to cache`,
22075
- { glob: matchers[i].glob }
22076
- );
22077
- }
22078
- }
22079
- matched.sort();
22080
- const fileEntries = [];
22081
- for (const relPath of matched) {
22082
- const content = await readFile10(join8(root, relPath));
22083
- fileEntries.push({ path: relPath, sha256: sha256Hex(content) });
22084
- }
22085
- const cacheKey = sha256Hex(canonicalJson(hook) + canonicalJson(fileEntries));
22086
- return { cacheKey, matchedFiles: matched };
22087
- }
22088
- function hookCacheDir(root, hookName) {
22089
- return join8(root, ...CACHE_BASE, hookName);
22090
- }
22091
- function hookCachePath(root, hookName, cacheKey) {
22092
- return join8(hookCacheDir(root, hookName), `${cacheKey}.json`);
22093
- }
22094
- async function readHookCache(root, hookName, cacheKey) {
22095
- try {
22096
- const raw = await readFile10(hookCachePath(root, hookName, cacheKey), "utf8");
22097
- const record = JSON.parse(raw);
22098
- if (record && record.cacheKey === cacheKey && record.exitCode === 0) {
22099
- return record;
22100
- }
22101
- return null;
22102
- } catch {
22103
- return null;
22104
- }
22105
- }
22106
- async function writeHookCache(root, hookName, cacheKey, result) {
22107
- if (!result || result.exitCode !== 0) {
22108
- return { ok: false, error: "refusing to cache a non-zero exit result" };
22109
- }
22110
- try {
22111
- await mkdir4(hookCacheDir(root, hookName), { recursive: true });
22112
- const record = {
22113
- cacheKey,
22114
- exitCode: 0,
22115
- stdoutTail: String(result.stdoutTail ?? "").slice(-TAIL_LENGTH),
22116
- stderrTail: String(result.stderrTail ?? "").slice(-TAIL_LENGTH),
22117
- createdAt: result.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
22118
- };
22119
- await writeFile5(hookCachePath(root, hookName, cacheKey), `${JSON.stringify(record, null, 2)}
22120
- `, "utf8");
22121
- return { ok: true };
22122
- } catch (err) {
22123
- return { ok: false, error: err.message };
22124
- }
22125
- }
22126
- var CACHE_BASE, TAIL_LENGTH, SKIPPED_DIRS;
22127
- var init_hook_cache = __esm({
22128
- "src/core/hook-cache.mjs"() {
22129
- init_digest();
22130
- init_errors();
22131
- CACHE_BASE = [".release-skill", "cache", "hooks"];
22132
- TAIL_LENGTH = 4e3;
22133
- SKIPPED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", ".release-skill"]);
22134
- __name(globToRegExp, "globToRegExp");
22135
- __name(listInputFiles, "listInputFiles");
22136
- __name(computeHookCacheKey, "computeHookCacheKey");
22137
- __name(hookCacheDir, "hookCacheDir");
22138
- __name(hookCachePath, "hookCachePath");
22139
- __name(readHookCache, "readHookCache");
22140
- __name(writeHookCache, "writeHookCache");
22141
- }
22142
- });
22143
-
22144
- // src/snapshot/frozen.mjs
22145
- import { createHash as createHash5 } from "node:crypto";
22146
- import { execFile as execFileCb5, spawn } from "node:child_process";
22147
- import { promisify as promisify5 } from "node:util";
22148
- import {
22149
- chmod,
22150
- lstat as lstat6,
22151
- mkdir as mkdir5,
22152
- mkdtemp,
22153
- open as open4,
22154
- readdir as readdir4,
22155
- realpath as realpath6,
22156
- rm as rm3,
22157
- unlink as unlink2
22158
- } from "node:fs/promises";
22159
- import { constants as fsConstants2 } from "node:fs";
22160
- import { isAbsolute as isAbsolute9, join as join9, relative as relative10, resolve as resolve13 } from "node:path";
22161
- function frozenError(message, details = {}) {
22162
- return new ReleaseError(GATE_FAILED, message, details);
22163
- }
22164
- function isInside(parent, candidate) {
22165
- const rel = relative10(parent, candidate);
22166
- return rel === "" || !isAbsolute9(rel) && rel !== ".." && !rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`);
22167
- }
22168
- async function resolveFrozenPath(root, relativePath, label = "frozen path") {
22169
- if (!relativePath || typeof relativePath !== "string" || isAbsolute9(relativePath)) {
22170
- throw frozenError(`${label} must be a non-empty project-relative path`);
22171
- }
22172
- const rootReal = await realpath6(root);
22173
- const lexical = resolve13(rootReal, relativePath);
22174
- if (!isInside(rootReal, lexical)) {
22175
- throw frozenError(`${label} escapes project root`, { relativePath });
22176
- }
22177
- const lexicalStat = await lstat6(lexical).catch((err) => {
22178
- throw frozenError(`${label} is missing`, { relativePath, cause: err.code });
22179
- });
22180
- if (lexicalStat.isSymbolicLink()) {
22181
- throw frozenError(`${label} must not be a symlink`, { relativePath });
22182
- }
22183
- const physical = await realpath6(lexical).catch((err) => {
22184
- throw frozenError(`${label} is missing`, { relativePath, cause: err.code });
22185
- });
22186
- if (!isInside(rootReal, physical)) {
22187
- throw frozenError(`${label} resolves outside project root`, { relativePath });
22188
- }
22189
- return physical;
22190
- }
22191
- async function readStableRegularFile(filePath, displayPath) {
22192
- const before = await lstat6(filePath);
22193
- if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1) {
22194
- throw frozenError(`frozen snapshot entry is not a single-link regular file: ${displayPath}`);
22195
- }
22196
- const handle = await open4(filePath, fsConstants2.O_RDONLY | fsConstants2.O_NOFOLLOW);
22197
- try {
22198
- const opened = await handle.stat();
22199
- if (!opened.isFile() || opened.nlink !== 1 || opened.dev !== before.dev || opened.ino !== before.ino) {
22200
- throw frozenError(`frozen snapshot entry changed before read: ${displayPath}`);
22201
- }
22202
- const bytes = await handle.readFile();
22203
- const after = await handle.stat();
22204
- if (after.dev !== opened.dev || after.ino !== opened.ino || after.nlink !== 1 || after.size !== opened.size || after.mtimeMs !== opened.mtimeMs || after.ctimeMs !== opened.ctimeMs) {
22205
- throw frozenError(`frozen snapshot entry changed during read: ${displayPath}`);
22206
- }
22207
- return { bytes, mode: opened.mode };
22208
- } finally {
22209
- await handle.close();
22210
- }
22211
- }
22212
- async function computeFrozenSnapshot(snapshotDir, { excludeRootEntries = [] } = {}) {
22213
- const root = await realpath6(snapshotDir);
22214
- const rootStat = await lstat6(root);
22215
- if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
22216
- throw frozenError("frozen snapshot root must be a real directory");
22217
- }
22218
- const entries = [];
22219
- const excludedRootNames = /* @__PURE__ */ new Set();
22220
- const excludedPathPrefixes = [];
22221
- for (const entry of excludeRootEntries) {
22222
- if (typeof entry !== "string") {
22223
- throw frozenError("frozen snapshot exclusion entries must be strings");
22224
- }
22225
- const segments = entry.split("/").filter((segment) => segment !== "" && segment !== ".");
22226
- if (segments.length === 0 || entry.startsWith("/") || entry.includes("\\") || segments.some((segment) => segment === "..")) {
22227
- throw frozenError(`frozen snapshot exclusion entry is not a safe relative path: ${JSON.stringify(entry)}`);
22228
- }
22229
- if (segments.length === 1) {
22230
- excludedRootNames.add(segments[0]);
22231
- } else {
22232
- excludedPathPrefixes.push(segments.join("/"));
22233
- }
22234
- }
22235
- async function walk(dir) {
22236
- const children = await readdir4(dir, { withFileTypes: true });
22237
- children.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
22238
- for (const child of children) {
22239
- if (dir === root && excludedRootNames.has(child.name)) continue;
22240
- const absolute = join9(dir, child.name);
22241
- const rel = relative10(root, absolute).split("\\").join("/");
22242
- if (excludedPathPrefixes.some((prefix) => rel === prefix || rel.startsWith(`${prefix}/`))) continue;
22243
- const st = await lstat6(absolute);
22244
- if (st.isSymbolicLink()) {
22245
- throw frozenError(`frozen snapshot contains symlink: ${rel}`);
22246
- }
22247
- if (st.isDirectory()) {
22248
- await walk(absolute);
22249
- continue;
22250
- }
22251
- const { bytes, mode } = await readStableRegularFile(absolute, rel);
22252
- entries.push({
22253
- path: rel,
22254
- type: "file",
22255
- mode,
22256
- size: bytes.length,
22257
- contentDigest: createHash5("sha256").update(bytes).digest("hex")
22258
- });
22259
- }
22260
- }
22261
- __name(walk, "walk");
22262
- await walk(root);
22263
- entries.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
22264
- const digest = createHash5("sha256").update(JSON.stringify(entries)).digest("hex");
22265
- return { digest, entries };
22266
- }
22267
- async function verifyFrozenSnapshot({ root, snapshotPath, expectedDigest }) {
22268
- const snapshotDir = await resolveFrozenPath(root, snapshotPath, "frozen snapshot path");
22269
- const observed = await computeFrozenSnapshot(snapshotDir);
22270
- if (!expectedDigest || observed.digest !== expectedDigest) {
22271
- throw frozenError("frozen snapshot digest mismatch", {
22272
- expectedDigest,
22273
- observedDigest: observed.digest
22274
- });
22275
- }
22276
- return { snapshotDir, ...observed };
22277
- }
22278
- async function verifyFrozenFile({ root, filePath, expectedSha256, label = "frozen file" }) {
22279
- const physical = await resolveFrozenPath(root, filePath, label);
22280
- const { bytes } = await readStableRegularFile(physical, filePath);
22281
- const observedSha256 = createHash5("sha256").update(bytes).digest("hex");
22282
- if (!expectedSha256 || observedSha256 !== expectedSha256) {
22283
- throw frozenError(`${label} SHA-256 mismatch`, { expectedSha256, observedSha256 });
22284
- }
22285
- return { physical, observedSha256, size: bytes.length };
22286
- }
22287
- async function verifyFrozenDirectoryStructure(directory, label = "frozen directory") {
22288
- async function walk(current) {
22289
- const children = await readdir4(current, { withFileTypes: true });
22290
- for (const child of children) {
22291
- const absolute = join9(current, child.name);
22292
- const st = await lstat6(absolute);
22293
- if (st.isSymbolicLink()) {
22294
- throw frozenError(`${label} contains a symlink`);
22295
- }
22296
- if (st.isDirectory()) {
22297
- await walk(absolute);
22298
- } else if (!st.isFile() || st.nlink !== 1) {
22299
- throw frozenError(`${label} contains an unsafe non-regular or hardlinked entry`);
22300
- }
22301
- }
22302
- }
22303
- __name(walk, "walk");
22304
- await walk(directory);
22305
- }
22306
- async function verifyFrozenGitRepository({
22307
- root,
22308
- gitObjectDir,
22309
- commit,
22310
- tree,
22311
- parentCommit,
22312
- commitTimestamp,
22313
- exec = execFile4
22314
- }) {
22315
- const gitDir = await resolveFrozenPath(root, gitObjectDir, "frozen git object directory");
22316
- await verifyFrozenDirectoryStructure(gitDir, "frozen git object directory");
22317
- const { stdout } = await exec("git", ["--git-dir", gitDir, "rev-parse", `${commit}^{tree}`], { shell: false });
22318
- if (stdout.trim() !== tree) {
22319
- throw frozenError("frozen git object tree mismatch", { commit, expectedTree: tree, observedTree: stdout.trim() });
22320
- }
22321
- if (commitTimestamp !== void 0) {
22322
- const expectedTimestamp = normalizeGitTimestamp(commitTimestamp, "frozen commit timestamp");
22323
- const { stdout: datesOut } = await exec(
22324
- "git",
22325
- ["--git-dir", gitDir, "show", "-s", "--format=%aI%n%cI", commit],
22326
- { shell: false }
22327
- );
22328
- const [authorDate, committerDate] = datesOut.trim().split("\n");
22329
- if (authorDate !== expectedTimestamp || committerDate !== expectedTimestamp) {
22330
- throw frozenError("frozen git commit dates do not match the plan freeze timestamp", {
22331
- commit,
22332
- expectedTimestamp,
22333
- observedAuthorDate: authorDate,
22334
- observedCommitterDate: committerDate
22335
- });
22336
- }
22337
- }
22338
- if (parentCommit) {
22339
- if (!/^[a-f0-9]{40,64}$/.test(parentCommit)) {
22340
- throw frozenError("frozen Git parent must be a full hexadecimal object id");
22341
- }
22342
- const { stdout: parentsOut } = await exec(
22343
- "git",
22344
- ["--git-dir", gitDir, "rev-list", "--parents", "-n", "1", commit],
22345
- { shell: false }
22346
- );
22347
- const [observedCommit, ...parents] = parentsOut.trim().split(/\s+/);
22348
- if (observedCommit !== commit || parents.length !== 1 || parents[0] !== parentCommit) {
22349
- throw frozenError("frozen Git commit parent mismatch", {
22350
- commit,
22351
- expectedParent: parentCommit,
22352
- observedParents: parents
22353
- });
22354
- }
22355
- }
22356
- return { gitDir, commit, tree, ...parentCommit ? { parentCommit } : {} };
22357
- }
22358
- async function verifyGitTreeContent({ snapshotDir, repositoryDir, commit, expectedSnapshotDigest, exec }) {
22359
- const snapshot = await computeFrozenSnapshot(snapshotDir);
22360
- if (snapshot.digest !== expectedSnapshotDigest) {
22361
- throw frozenError("frozen snapshot changed while deriving Git objects", {
22362
- expectedDigest: expectedSnapshotDigest,
22363
- observedDigest: snapshot.digest
22364
- });
22365
- }
22366
- const { stdout: treeOut } = await exec(
22367
- "git",
22368
- ["--git-dir", repositoryDir, "ls-tree", "-rz", commit],
22369
- { shell: false, encoding: "buffer", maxBuffer: 64 * 1024 * 1024 }
22370
- );
22371
- const treeEntries = Buffer.from(treeOut).toString("utf8").split("\0").filter(Boolean).map((record) => {
22372
- const separator = record.indexOf(" ");
22373
- const [mode, type] = record.slice(0, separator).split(" ");
22374
- return { path: record.slice(separator + 1), mode, type };
22375
- }).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
22376
- const names = treeEntries.map((entry) => entry.path);
22377
- const expectedNames = snapshot.entries.map((entry) => entry.path);
22378
- if (JSON.stringify(names) !== JSON.stringify(expectedNames)) {
22379
- throw frozenError("frozen Git tree paths do not match the sealed public snapshot");
22380
- }
22381
- for (const [index, entry] of snapshot.entries.entries()) {
22382
- const gitEntry = treeEntries[index];
22383
- const expectedMode = entry.mode & 73 ? "100755" : "100644";
22384
- if (gitEntry.type !== "blob" || gitEntry.mode !== expectedMode) {
22385
- throw frozenError(`frozen Git tree mode does not match the sealed public snapshot: ${entry.path}`, {
22386
- expectedMode,
22387
- observedMode: gitEntry.mode,
22388
- observedType: gitEntry.type
22389
- });
22390
- }
22391
- const { stdout } = await exec(
22392
- "git",
22393
- ["--git-dir", repositoryDir, "show", `${commit}:${entry.path}`],
22394
- { shell: false, encoding: "buffer", maxBuffer: Math.max(64 * 1024 * 1024, entry.size + 1024) }
22395
- );
22396
- const digest = createHash5("sha256").update(Buffer.from(stdout)).digest("hex");
22397
- if (digest !== entry.contentDigest) {
22398
- throw frozenError(`frozen Git tree bytes do not match the sealed public snapshot: ${entry.path}`);
22399
- }
22400
- }
22401
- }
22402
- function normalizeGitTimestamp(value, label = "freeze timestamp") {
22403
- if (typeof value !== "string" || value.trim() === "") {
22404
- throw frozenError(`${label} must be a non-empty strict ISO 8601 timestamp string`, {
22405
- received: typeof value === "string" ? value : typeof value
22406
- });
22407
- }
22408
- const trimmed = value.trim();
22409
- const match = GIT_ISO8601_STRICT_RE.exec(trimmed);
22410
- if (!match) {
22411
- throw frozenError(`${label} must be strict ISO 8601 with an explicit UTC offset`, {
22412
- received: trimmed
22413
- });
22414
- }
22415
- const [, yearStr, monthStr, dayStr, hourStr, minuteStr, secondStr, zone] = match;
22416
- const inputYear = Number(yearStr);
22417
- const inputMonth = Number(monthStr);
22418
- const inputDay = Number(dayStr);
22419
- const inputHour = Number(hourStr);
22420
- const inputMinute = Number(minuteStr);
22421
- const inputSecond = Number(secondStr);
22422
- if (inputYear < 1e3 || inputYear > 9999) {
22423
- throw frozenError(`${label} year must be a four-digit ISO year`, { received: trimmed });
22424
- }
22425
- const probeMs = Date.UTC(inputYear, inputMonth - 1, inputDay, inputHour, inputMinute, inputSecond);
22426
- const probe = new Date(probeMs);
22427
- if (probe.getUTCFullYear() !== inputYear || probe.getUTCMonth() !== inputMonth - 1 || probe.getUTCDate() !== inputDay || probe.getUTCHours() !== inputHour || probe.getUTCMinutes() !== inputMinute || probe.getUTCSeconds() !== inputSecond) {
22428
- throw frozenError(`${label} is not a valid calendar date`, { received: trimmed });
22429
- }
22430
- let offsetMs = 0;
22431
- if (zone !== "Z") {
22432
- const sign = zone.startsWith("-") ? -1 : 1;
22433
- const zoneHours = Number(zone.slice(1, 3));
22434
- const zoneMinutes = Number(zone.slice(4, 6));
22435
- if (zoneHours > 23 || zoneMinutes > 59) {
22436
- throw frozenError(`${label} has an invalid UTC offset`, { received: trimmed });
22768
+ throw new ReleaseError("INVALID_HOOK", "every element of hook.command must be a string");
22437
22769
  }
22438
- offsetMs = sign * (zoneHours * 60 + zoneMinutes) * 6e4;
22439
22770
  }
22440
- const date = new Date(probeMs - offsetMs);
22441
- if (date.getUTCFullYear() < 1e3 || date.getUTCFullYear() > 9999) {
22442
- throw frozenError(`${label} normalizes outside the four-digit ISO year range`, { received: trimmed });
22771
+ if (cwd !== void 0 && typeof cwd !== "string") {
22772
+ throw new ReleaseError("INVALID_HOOK", "hook.cwd must be a string when provided");
22443
22773
  }
22444
- const pad = /* @__PURE__ */ __name((num) => String(num).padStart(2, "0"), "pad");
22445
- return `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}T${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())}+00:00`;
22446
- }
22447
- function publicGitRemote({ repo, githubHost = "github.com" }) {
22448
- if (typeof repo !== "string" || !/^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/.test(repo)) {
22449
- throw frozenError("Git parent repo must use owner/name format");
22774
+ if (timeoutMs !== void 0) {
22775
+ if (typeof timeoutMs !== "number" || !Number.isInteger(timeoutMs) || timeoutMs < 1) {
22776
+ throw new ReleaseError("INVALID_HOOK", "hook.timeoutMs must be a positive integer");
22777
+ }
22450
22778
  }
22451
- if (typeof githubHost !== "string" || !/^[A-Za-z0-9.-]+$/.test(githubHost) || githubHost.startsWith(".") || githubHost.endsWith(".") || githubHost.includes("..")) {
22452
- throw frozenError("Git parent githubHost must be a valid hostname");
22779
+ if (envAllowlist !== void 0) {
22780
+ if (!Array.isArray(envAllowlist)) {
22781
+ throw new ReleaseError("INVALID_HOOK", "hook.envAllowlist must be an array");
22782
+ }
22783
+ for (const key of envAllowlist) {
22784
+ if (typeof key !== "string" || !ENV_KEY_PATTERN.test(key)) {
22785
+ throw new ReleaseError(
22786
+ "INVALID_HOOK",
22787
+ `hook.envAllowlist key "${key}" must match /^[A-Z_][A-Z0-9_]*$/`
22788
+ );
22789
+ }
22790
+ }
22453
22791
  }
22454
- return `https://${githubHost}/${repo}.git`;
22455
- }
22456
- async function buildFrozenGitRepository({
22457
- snapshotDir,
22458
- repositoryDir,
22459
- version,
22460
- expectedSnapshotDigest,
22461
- parent,
22462
- commitTimestamp,
22463
- exec = execFile4
22464
- }) {
22465
- const canonicalTimestamp = normalizeGitTimestamp(commitTimestamp, "frozen commit timestamp");
22466
- if (!expectedSnapshotDigest) throw frozenError("Git object build requires the sealed snapshot digest");
22467
- await mkdir5(repositoryDir, { recursive: true });
22468
- await exec("git", ["init", "--bare", repositoryDir], { shell: false });
22469
- let parentCommit = null;
22470
- if (parent) {
22471
- if (!parent.ref || typeof parent.ref !== "string") {
22472
- throw frozenError("Git parent ref must be a non-empty string");
22792
+ if (cacheable !== void 0 && typeof cacheable !== "boolean") {
22793
+ throw new ReleaseError("INVALID_HOOK", "hook.cacheable must be a boolean when provided");
22794
+ }
22795
+ if (cacheInputs !== void 0) {
22796
+ if (!Array.isArray(cacheInputs) || cacheInputs.length === 0) {
22797
+ throw new ReleaseError(
22798
+ "INVALID_HOOK",
22799
+ "hook.cacheInputs must be a non-empty array of glob strings when provided"
22800
+ );
22473
22801
  }
22474
- if (!parent.commit || !/^[a-f0-9]{40,64}$/.test(parent.commit)) {
22475
- throw frozenError("Git parent commit must be a full hexadecimal object id");
22802
+ for (const glob of cacheInputs) {
22803
+ if (typeof glob !== "string" || glob.length === 0) {
22804
+ throw new ReleaseError(
22805
+ "INVALID_HOOK",
22806
+ "every element of hook.cacheInputs must be a non-empty string"
22807
+ );
22808
+ }
22476
22809
  }
22477
- const remoteUrl = publicGitRemote(parent);
22478
- await exec(
22479
- "git",
22480
- ["--git-dir", repositoryDir, "fetch", "--no-tags", remoteUrl, parent.ref],
22481
- { shell: false }
22482
- );
22483
- const { stdout: fetchedOut } = await exec(
22484
- "git",
22485
- ["--git-dir", repositoryDir, "rev-parse", "FETCH_HEAD^{commit}"],
22486
- { shell: false }
22810
+ }
22811
+ if (cacheable === true && (!Array.isArray(cacheInputs) || cacheInputs.length === 0)) {
22812
+ throw new ReleaseError(
22813
+ "INVALID_HOOK",
22814
+ "hook.cacheable=true requires a non-empty hook.cacheInputs"
22487
22815
  );
22488
- if (fetchedOut.trim() !== parent.commit) {
22489
- throw frozenError("fetched Git parent does not match the observed public baseline", {
22490
- expectedCommit: parent.commit,
22491
- observedCommit: fetchedOut.trim(),
22492
- ref: parent.ref
22493
- });
22816
+ }
22817
+ }
22818
+ function buildFilteredEnv(envAllowlist, contextEnv) {
22819
+ const env = {};
22820
+ for (const key of PLATFORM_REQUIRED_VARS) {
22821
+ const val = process.env[key];
22822
+ if (val !== void 0) {
22823
+ env[key] = val;
22494
22824
  }
22495
- parentCommit = parent.commit;
22496
22825
  }
22497
- const indexPath = join9(repositoryDir, "release-index");
22498
- const env = { ...process.env, GIT_INDEX_FILE: indexPath };
22499
- await exec("git", [
22500
- "--git-dir",
22501
- repositoryDir,
22502
- "--work-tree",
22503
- snapshotDir,
22504
- "add",
22505
- "--all",
22506
- "--force",
22507
- "--",
22508
- "."
22509
- ], { cwd: snapshotDir, env, shell: false });
22510
- const { stdout: treeOut } = await exec("git", ["--git-dir", repositoryDir, "write-tree"], {
22511
- env,
22512
- shell: false
22513
- });
22514
- const tree = treeOut.trim();
22515
- const commitEnv = {
22516
- ...env,
22517
- GIT_AUTHOR_NAME: "release-skill",
22518
- GIT_AUTHOR_EMAIL: "release-skill@localhost",
22519
- GIT_COMMITTER_NAME: "release-skill",
22520
- GIT_COMMITTER_EMAIL: "release-skill@localhost",
22521
- GIT_AUTHOR_DATE: canonicalTimestamp,
22522
- GIT_COMMITTER_DATE: canonicalTimestamp
22523
- };
22524
- const commitArgs = ["--git-dir", repositoryDir, "commit-tree", tree];
22525
- if (parentCommit) commitArgs.push("-p", parentCommit);
22526
- commitArgs.push("-m", `Release ${version}`);
22527
- const { stdout: commitOut } = await exec(
22528
- "git",
22529
- commitArgs,
22530
- { env: commitEnv, shell: false }
22531
- );
22532
- const commit = commitOut.trim();
22533
- await verifyGitTreeContent({ snapshotDir, repositoryDir, commit, expectedSnapshotDigest, exec });
22534
- if (parentCommit) {
22535
- const { stdout: parentsOut } = await exec(
22536
- "git",
22537
- ["--git-dir", repositoryDir, "rev-list", "--parents", "-n", "1", commit],
22538
- { shell: false }
22539
- );
22540
- const [observedCommit, ...parents] = parentsOut.trim().split(/\s+/);
22541
- if (observedCommit !== commit || parents.length !== 1 || parents[0] !== parentCommit) {
22542
- throw frozenError("derived Git commit does not have the exact planned parent", {
22543
- commit,
22544
- parentCommit,
22545
- observedParents: parents
22546
- });
22826
+ if (contextEnv && envAllowlist) {
22827
+ for (const key of envAllowlist) {
22828
+ if (key in contextEnv) {
22829
+ env[key] = contextEnv[key];
22830
+ }
22547
22831
  }
22548
22832
  }
22549
- return { tree, commit, ...parentCommit ? { parentCommit } : {} };
22550
- }
22551
- function contentEntries(entries) {
22552
- return entries.map(({ path: path3, size, contentDigest }) => ({ path: path3, size, contentDigest }));
22833
+ return env;
22553
22834
  }
22554
- async function createDetachedReadHandle(bytes, directory) {
22555
- const tempDir = await mkdtemp(join9(directory, ".tar-fd-"));
22556
- const tempPath = join9(tempDir, "package.tgz");
22557
- let handle;
22835
+ async function runHook(hook, context) {
22836
+ validateHook(hook);
22837
+ if (!context || typeof context !== "object" || Array.isArray(context)) {
22838
+ throw new ReleaseError("INVALID_HOOK", "context must be a non-null object");
22839
+ }
22840
+ if (typeof context.root !== "string" || !isAbsolute9(context.root)) {
22841
+ throw new ReleaseError("INVALID_HOOK", "context.root must be an absolute path");
22842
+ }
22843
+ const { command: command2, cwd, timeoutMs, envAllowlist = [] } = hook;
22844
+ const rootReal = await realpath6(context.root);
22845
+ const resolvedCwd = cwd ? await realpath6(resolve13(rootReal, cwd)) : rootReal;
22846
+ const rel = relative10(rootReal, resolvedCwd);
22847
+ if (rel.startsWith("..") || rel === "..") {
22848
+ throw new ReleaseError(
22849
+ "INVALID_HOOK",
22850
+ `hook.cwd "${cwd}" resolves outside project root`
22851
+ );
22852
+ }
22853
+ const env = buildFilteredEnv(envAllowlist, context.env);
22854
+ const executable = command2[0];
22855
+ const args2 = command2.slice(1);
22856
+ let timeoutController;
22857
+ let timeoutHandle;
22858
+ if (timeoutMs && timeoutMs > 0) {
22859
+ timeoutController = new AbortController();
22860
+ timeoutHandle = setTimeout(() => timeoutController.abort(), timeoutMs);
22861
+ }
22558
22862
  try {
22559
- const writer = await open4(tempPath, "wx", 256);
22560
- try {
22561
- await writer.writeFile(bytes);
22562
- await writer.sync();
22563
- } finally {
22564
- await writer.close();
22565
- }
22566
- handle = await open4(tempPath, fsConstants2.O_RDONLY | fsConstants2.O_NOFOLLOW);
22567
- await unlink2(tempPath);
22568
- await rm3(tempDir, { recursive: true, force: true });
22569
- return handle;
22570
- } catch (err) {
22571
- await handle?.close().catch(() => {
22863
+ const { stdout, stderr } = await execFileAsync(executable, args2, {
22864
+ cwd: resolvedCwd,
22865
+ env,
22866
+ shell: false,
22867
+ maxBuffer: 10 * 1024 * 1024,
22868
+ // 10 MiB
22869
+ signal: timeoutController?.signal
22572
22870
  });
22573
- await rm3(tempDir, { recursive: true, force: true });
22871
+ return { exitCode: 0, stdout: stdout ?? "", stderr: stderr ?? "" };
22872
+ } catch (err) {
22873
+ if (err.name === "AbortError" || err.killed && err.signal === "SIGTERM") {
22874
+ throw new ReleaseError(
22875
+ "HOOK_TIMEOUT",
22876
+ `hook timed out after ${timeoutMs}ms: ${executable} ${args2.join(" ")}`,
22877
+ { command: command2, timeoutMs }
22878
+ );
22879
+ }
22880
+ if ("stdout" in err) {
22881
+ return {
22882
+ exitCode: typeof err.code === "number" ? err.code : 1,
22883
+ stdout: err.stdout ?? "",
22884
+ stderr: err.stderr ?? ""
22885
+ };
22886
+ }
22574
22887
  throw err;
22888
+ } finally {
22889
+ if (timeoutHandle) {
22890
+ clearTimeout(timeoutHandle);
22891
+ }
22575
22892
  }
22576
22893
  }
22577
- async function runTarFromHandle(handle, args2) {
22578
- const fdPath = process.platform === "linux" ? "/proc/self/fd/3" : "/dev/fd/3";
22579
- return new Promise((resolvePromise, rejectPromise) => {
22580
- const child = spawn("tar", args2(fdPath), {
22581
- shell: false,
22582
- env: process.env,
22583
- stdio: ["ignore", "pipe", "pipe", handle.fd]
22584
- });
22585
- let stdout = "";
22586
- let stderr = "";
22587
- child.stdout.setEncoding("utf8");
22588
- child.stderr.setEncoding("utf8");
22589
- child.stdout.on("data", (chunk) => {
22590
- stdout += chunk;
22591
- });
22592
- child.stderr.on("data", (chunk) => {
22593
- stderr += chunk;
22594
- });
22595
- child.on("error", rejectPromise);
22596
- child.on("close", (code) => {
22597
- if (code === 0) resolvePromise({ stdout, stderr });
22598
- else rejectPromise(new Error(`tar exited with code ${code}: ${stderr.trim()}`));
22599
- });
22600
- });
22601
- }
22602
- async function verifyNpmTarballContent({ snapshotDir, tarballBytes, tarballDir, expectedSnapshotDigest }) {
22603
- const listHandle = await createDetachedReadHandle(tarballBytes, tarballDir);
22604
- let listOut;
22605
- try {
22606
- ({ stdout: listOut } = await runTarFromHandle(listHandle, (fdPath) => ["-tzf", fdPath]));
22607
- } finally {
22608
- await listHandle.close();
22894
+ var execFileAsync, PLATFORM_REQUIRED_VARS, ENV_KEY_PATTERN;
22895
+ var init_hooks = __esm({
22896
+ "src/core/hooks.mjs"() {
22897
+ init_errors();
22898
+ execFileAsync = promisify5(execFileCb5);
22899
+ PLATFORM_REQUIRED_VARS = (() => {
22900
+ const posix3 = ["PATH", "HOME", "USER", "LANG", "LC_ALL", "LC_CTYPE", "TERM"];
22901
+ const win322 = ["PATH", "HOME", "USER", "SystemRoot", "SYSTEMROOT", "COMSPEC", "PATHEXT", "TEMP", "TMP"];
22902
+ return new Set(process.platform === "win32" ? win322 : posix3);
22903
+ })();
22904
+ ENV_KEY_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
22905
+ __name(validateHook, "validateHook");
22906
+ __name(buildFilteredEnv, "buildFilteredEnv");
22907
+ __name(runHook, "runHook");
22609
22908
  }
22610
- const listed = listOut.split(/\r?\n/).filter(Boolean);
22611
- if (listed.length === 0 || listed.some((entry) => !entry.startsWith("package/") || entry.startsWith("/") || entry.includes("\\") || entry.split("/").some((segment) => segment === ".."))) {
22612
- throw frozenError("npm tarball contains an unsafe or unexpected path");
22909
+ });
22910
+
22911
+ // src/core/hook-cache.mjs
22912
+ import { readdir as readdir4, readFile as readFile10, mkdir as mkdir5, writeFile as writeFile5 } from "node:fs/promises";
22913
+ import { join as join9 } from "node:path";
22914
+ function globToRegExp(glob) {
22915
+ let source = "";
22916
+ let i = 0;
22917
+ while (i < glob.length) {
22918
+ const c = glob[i];
22919
+ if (c === "*") {
22920
+ if (glob[i + 1] === "*") {
22921
+ source += ".*";
22922
+ i += 2;
22923
+ } else {
22924
+ source += "[^/]*";
22925
+ i += 1;
22926
+ }
22927
+ } else if (c === "?") {
22928
+ source += "[^/]";
22929
+ i += 1;
22930
+ } else {
22931
+ source += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
22932
+ i += 1;
22933
+ }
22613
22934
  }
22614
- const verifyDir = await mkdtemp(join9(tarballDir, ".verify-"));
22615
- try {
22616
- const extractHandle = await createDetachedReadHandle(tarballBytes, tarballDir);
22935
+ return new RegExp(`^${source}$`);
22936
+ }
22937
+ async function listInputFiles(root) {
22938
+ const out = [];
22939
+ async function walk(dirAbs, dirRel) {
22940
+ let entries;
22617
22941
  try {
22618
- await runTarFromHandle(extractHandle, (fdPath) => ["-xzf", fdPath, "-C", verifyDir]);
22619
- } finally {
22620
- await extractHandle.close();
22942
+ entries = await readdir4(dirAbs, { withFileTypes: true });
22943
+ } catch {
22944
+ return;
22621
22945
  }
22622
- const original = await computeFrozenSnapshot(snapshotDir);
22623
- if (original.digest !== expectedSnapshotDigest) {
22624
- throw frozenError("frozen snapshot changed while deriving npm tarball", {
22625
- expectedDigest: expectedSnapshotDigest,
22626
- observedDigest: original.digest
22627
- });
22946
+ for (const entry of entries) {
22947
+ if (entry.isDirectory()) {
22948
+ if (SKIPPED_DIRS.has(entry.name)) continue;
22949
+ const rel = dirRel ? `${dirRel}/${entry.name}` : entry.name;
22950
+ await walk(join9(dirAbs, entry.name), rel);
22951
+ } else if (entry.isFile()) {
22952
+ out.push(dirRel ? `${dirRel}/${entry.name}` : entry.name);
22953
+ }
22954
+ }
22955
+ }
22956
+ __name(walk, "walk");
22957
+ await walk(root, "");
22958
+ out.sort();
22959
+ return out;
22960
+ }
22961
+ async function computeHookCacheKey(hook, root) {
22962
+ const globs = Array.isArray(hook.cacheInputs) ? hook.cacheInputs : [];
22963
+ const matchers = globs.map((glob) => ({ glob, re: globToRegExp(glob) }));
22964
+ const allFiles = await listInputFiles(root);
22965
+ const matched = [];
22966
+ const hitPerGlob = matchers.map(() => false);
22967
+ for (const relPath of allFiles) {
22968
+ let hit = false;
22969
+ for (let i = 0; i < matchers.length; i += 1) {
22970
+ if (matchers[i].re.test(relPath)) {
22971
+ hitPerGlob[i] = true;
22972
+ hit = true;
22973
+ }
22628
22974
  }
22629
- const packed = await computeFrozenSnapshot(join9(verifyDir, "package"));
22630
- if (JSON.stringify(contentEntries(packed.entries)) !== JSON.stringify(contentEntries(original.entries))) {
22631
- throw frozenError("npm tarball bytes do not match the sealed public snapshot");
22975
+ if (hit) matched.push(relPath);
22976
+ }
22977
+ for (let i = 0; i < matchers.length; i += 1) {
22978
+ if (!hitPerGlob[i]) {
22979
+ throw new ReleaseError(
22980
+ GATE_FAILED,
22981
+ `hook cacheInputs glob "${matchers[i].glob}" matched no files; refusing to cache`,
22982
+ { glob: matchers[i].glob }
22983
+ );
22632
22984
  }
22633
- } finally {
22634
- await rm3(verifyDir, { recursive: true, force: true });
22635
22985
  }
22986
+ matched.sort();
22987
+ const fileEntries = [];
22988
+ for (const relPath of matched) {
22989
+ const content = await readFile10(join9(root, relPath));
22990
+ fileEntries.push({ path: relPath, sha256: sha256Hex(content) });
22991
+ }
22992
+ const cacheKey = sha256Hex(canonicalJson(hook) + canonicalJson(fileEntries));
22993
+ return { cacheKey, matchedFiles: matched };
22636
22994
  }
22637
- async function buildFrozenNpmTarball({ snapshotDir, tarballDir, expectedSnapshotDigest, exec = execFile4 }) {
22638
- await mkdir5(tarballDir, { recursive: true });
22639
- const { stdout } = await exec(
22640
- "npm",
22641
- ["pack", snapshotDir, "--pack-destination", tarballDir, "--json", "--ignore-scripts"],
22642
- { shell: false, encoding: "utf8", timeout: 12e4 }
22643
- );
22644
- let parsed;
22995
+ function hookCacheDir(root, hookName) {
22996
+ return join9(root, ...CACHE_BASE, hookName);
22997
+ }
22998
+ function hookCachePath(root, hookName, cacheKey) {
22999
+ return join9(hookCacheDir(root, hookName), `${cacheKey}.json`);
23000
+ }
23001
+ async function readHookCache(root, hookName, cacheKey) {
22645
23002
  try {
22646
- parsed = JSON.parse(stdout);
23003
+ const raw = await readFile10(hookCachePath(root, hookName, cacheKey), "utf8");
23004
+ const record = JSON.parse(raw);
23005
+ if (record && record.cacheKey === cacheKey && record.exitCode === 0) {
23006
+ return record;
23007
+ }
23008
+ return null;
22647
23009
  } catch {
22648
- throw frozenError("npm pack returned invalid JSON");
22649
- }
22650
- const info = Array.isArray(parsed) ? parsed[0] : parsed;
22651
- if (!info?.filename || !info?.integrity) {
22652
- throw frozenError("npm pack did not return filename and integrity");
23010
+ return null;
22653
23011
  }
22654
- const tarballPath = join9(tarballDir, info.filename);
22655
- const { bytes } = await readStableRegularFile(tarballPath, info.filename);
22656
- if (!expectedSnapshotDigest) throw frozenError("npm tarball build requires the sealed snapshot digest");
22657
- await verifyNpmTarballContent({
22658
- snapshotDir,
22659
- tarballBytes: bytes,
22660
- tarballDir,
22661
- expectedSnapshotDigest
22662
- });
22663
- return {
22664
- tarballPath,
22665
- integrity: info.integrity,
22666
- sha256: createHash5("sha256").update(bytes).digest("hex"),
22667
- size: bytes.length
22668
- };
22669
23012
  }
22670
- async function sealFrozenSnapshot(snapshotDir) {
22671
- async function walk(dir) {
22672
- const children = await readdir4(dir, { withFileTypes: true });
22673
- for (const child of children) {
22674
- const absolute = join9(dir, child.name);
22675
- const st = await lstat6(absolute);
22676
- if (st.isDirectory()) {
22677
- await walk(absolute);
22678
- await chmod(absolute, 365);
22679
- } else if (st.isFile()) {
22680
- await chmod(absolute, st.mode & 73 ? 365 : 292);
22681
- }
22682
- }
23013
+ async function writeHookCache(root, hookName, cacheKey, result) {
23014
+ if (!result || result.exitCode !== 0) {
23015
+ return { ok: false, error: "refusing to cache a non-zero exit result" };
23016
+ }
23017
+ try {
23018
+ await mkdir5(hookCacheDir(root, hookName), { recursive: true });
23019
+ const record = {
23020
+ cacheKey,
23021
+ exitCode: 0,
23022
+ stdoutTail: String(result.stdoutTail ?? "").slice(-TAIL_LENGTH),
23023
+ stderrTail: String(result.stderrTail ?? "").slice(-TAIL_LENGTH),
23024
+ createdAt: result.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
23025
+ };
23026
+ await writeFile5(hookCachePath(root, hookName, cacheKey), `${JSON.stringify(record, null, 2)}
23027
+ `, "utf8");
23028
+ return { ok: true };
23029
+ } catch (err) {
23030
+ return { ok: false, error: err.message };
22683
23031
  }
22684
- __name(walk, "walk");
22685
- await walk(snapshotDir);
22686
- await chmod(snapshotDir, 365);
22687
23032
  }
22688
- var execFile4, GIT_ISO8601_STRICT_RE;
22689
- var init_frozen = __esm({
22690
- "src/snapshot/frozen.mjs"() {
23033
+ var CACHE_BASE, TAIL_LENGTH, SKIPPED_DIRS;
23034
+ var init_hook_cache = __esm({
23035
+ "src/core/hook-cache.mjs"() {
23036
+ init_digest();
22691
23037
  init_errors();
22692
- execFile4 = promisify5(execFileCb5);
22693
- __name(frozenError, "frozenError");
22694
- __name(isInside, "isInside");
22695
- __name(resolveFrozenPath, "resolveFrozenPath");
22696
- __name(readStableRegularFile, "readStableRegularFile");
22697
- __name(computeFrozenSnapshot, "computeFrozenSnapshot");
22698
- __name(verifyFrozenSnapshot, "verifyFrozenSnapshot");
22699
- __name(verifyFrozenFile, "verifyFrozenFile");
22700
- __name(verifyFrozenDirectoryStructure, "verifyFrozenDirectoryStructure");
22701
- __name(verifyFrozenGitRepository, "verifyFrozenGitRepository");
22702
- __name(verifyGitTreeContent, "verifyGitTreeContent");
22703
- GIT_ISO8601_STRICT_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,9})?(Z|[+-]\d{2}:\d{2})$/;
22704
- __name(normalizeGitTimestamp, "normalizeGitTimestamp");
22705
- __name(publicGitRemote, "publicGitRemote");
22706
- __name(buildFrozenGitRepository, "buildFrozenGitRepository");
22707
- __name(contentEntries, "contentEntries");
22708
- __name(createDetachedReadHandle, "createDetachedReadHandle");
22709
- __name(runTarFromHandle, "runTarFromHandle");
22710
- __name(verifyNpmTarballContent, "verifyNpmTarballContent");
22711
- __name(buildFrozenNpmTarball, "buildFrozenNpmTarball");
22712
- __name(sealFrozenSnapshot, "sealFrozenSnapshot");
23038
+ CACHE_BASE = [".release-skill", "cache", "hooks"];
23039
+ TAIL_LENGTH = 4e3;
23040
+ SKIPPED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", ".release-skill"]);
23041
+ __name(globToRegExp, "globToRegExp");
23042
+ __name(listInputFiles, "listInputFiles");
23043
+ __name(computeHookCacheKey, "computeHookCacheKey");
23044
+ __name(hookCacheDir, "hookCacheDir");
23045
+ __name(hookCachePath, "hookCachePath");
23046
+ __name(readHookCache, "readHookCache");
23047
+ __name(writeHookCache, "writeHookCache");
22713
23048
  }
22714
23049
  });
22715
23050
 
@@ -25188,7 +25523,7 @@ import { execFile as execFileCb6 } from "node:child_process";
25188
25523
  import { promisify as promisify6 } from "node:util";
25189
25524
  import { lstat as lstat10, mkdtemp as mkdtemp2, readFile as readFile15, readdir as readdir9, realpath as realpath9, rm as rm5 } from "node:fs/promises";
25190
25525
  import { join as join13, relative as relative15, resolve as resolve17, sep as sep4 } from "node:path";
25191
- import { tmpdir as tmpdir2 } from "node:os";
25526
+ import { tmpdir as tmpdir3 } from "node:os";
25192
25527
  async function computeSourceInputClosure({ units, root }) {
25193
25528
  const realRoot = await realpath9(resolve17(root));
25194
25529
  const entriesByPath = /* @__PURE__ */ new Map();
@@ -25502,7 +25837,7 @@ async function verifyWithTemporaryGit({
25502
25837
  if (!branchLine) {
25503
25838
  return failure(REF_MISSING, `remote ref "refs/heads/${defaultBranch}" does not exist`);
25504
25839
  }
25505
- const tempRoot = await mkdtemp2(join13(tmpdir2(), "release-skill-source-authority-"));
25840
+ const tempRoot = await mkdtemp2(join13(tmpdir3(), "release-skill-source-authority-"));
25506
25841
  try {
25507
25842
  await execFn("git", ["init", "--bare", tempRoot], {
25508
25843
  encoding: "utf8",
@@ -70128,6 +70463,7 @@ __export(npm_exports, {
70128
70463
  registryTokenKey: () => registryTokenKey,
70129
70464
  resolveNpmRegistryAuthToken: () => resolveNpmRegistryAuthToken,
70130
70465
  resolvePackageCwd: () => resolvePackageCwd,
70466
+ verifyFrozenNpmTarballContract: () => verifyFrozenNpmTarballContract,
70131
70467
  verifyFrozenNpmTarballIdentity: () => verifyFrozenNpmTarballIdentity
70132
70468
  });
70133
70469
  import { createHash as createHash9, randomUUID } from "node:crypto";
@@ -70377,6 +70713,33 @@ async function verifyFrozenNpmTarballIdentity(action, root) {
70377
70713
  version: action.version
70378
70714
  });
70379
70715
  }
70716
+ async function verifyNpmTarballBufferContract(buffer, action, tarballDir) {
70717
+ const manifest = extractManifestFromTarball(buffer, {
70718
+ name: action.package,
70719
+ version: action.version
70720
+ });
70721
+ const fileIndex = await buildTarballFileIndex(buffer, tarballDir);
70722
+ const closureResult = checkNpmEntryClosure(manifest, fileIndex);
70723
+ if (closureResult.errors.length > 0) {
70724
+ throw new ReleaseError(
70725
+ GATE_FAILED,
70726
+ `npm entry closure check failed for ${action.package}@${action.version}: ${closureResult.errors.length} error(s)`,
70727
+ {
70728
+ gate: "npm-entry-closure",
70729
+ package: action.package,
70730
+ version: action.version,
70731
+ entries: closureResult.entries,
70732
+ errors: closureResult.errors,
70733
+ diagnostics: closureResult.diagnostics
70734
+ }
70735
+ );
70736
+ }
70737
+ return { manifest, closureResult };
70738
+ }
70739
+ async function verifyFrozenNpmTarballContract(action, root, tarballDir) {
70740
+ const buffer = await readVerifiedTarballBytes(action, root);
70741
+ return verifyNpmTarballBufferContract(buffer, action, tarballDir);
70742
+ }
70380
70743
  async function spawnNpmPublishTarball({ args: args2, cwd }) {
70381
70744
  return new Promise((resolvePromise, rejectPromise) => {
70382
70745
  const child = spawn3("npm", args2, {
@@ -70460,7 +70823,7 @@ function createNpmAdapter(deps = {}) {
70460
70823
  }
70461
70824
  const cwd = resolvePackageCwd(action.cwd, context.root);
70462
70825
  if (action.tarballPath) {
70463
- await verifyFrozenNpmTarballIdentity(action, context.root);
70826
+ await verifyFrozenNpmTarballContract(action, context.root);
70464
70827
  }
70465
70828
  let whoamiUser;
70466
70829
  if (action.tarballPath) {
@@ -70520,7 +70883,7 @@ function createNpmAdapter(deps = {}) {
70520
70883
  throw new Error("frozen npm tarball requires a stable Buffer publish capability");
70521
70884
  }
70522
70885
  const buffer = await readVerifiedTarballBytes(action, context.root);
70523
- const manifest = extractManifestFromTarball(buffer, { name: action.package, version: action.version });
70886
+ const { manifest } = await verifyNpmTarballBufferContract(buffer, action);
70524
70887
  let token;
70525
70888
  try {
70526
70889
  token = await resolveAuthToken({ registry: normalizedRegistry, cwd, exec, env: authEnv });
@@ -70669,6 +71032,8 @@ var init_npm = __esm({
70669
71032
  import_libnpmpublish = __toESM(require_lib25(), 1);
70670
71033
  import_npm_registry_fetch = __toESM(require_lib15(), 1);
70671
71034
  init_contract();
71035
+ init_npm_entry_closure();
71036
+ init_errors();
70672
71037
  execFile6 = promisify7(execFileCb7);
70673
71038
  NAME = "npm";
70674
71039
  SAFE_PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*|[a-z0-9][a-z0-9._-]*)$/;
@@ -70687,6 +71052,8 @@ var init_npm = __esm({
70687
71052
  __name(readVerifiedTarballBytes, "readVerifiedTarballBytes");
70688
71053
  __name(extractManifestFromTarball, "extractManifestFromTarball");
70689
71054
  __name(verifyFrozenNpmTarballIdentity, "verifyFrozenNpmTarballIdentity");
71055
+ __name(verifyNpmTarballBufferContract, "verifyNpmTarballBufferContract");
71056
+ __name(verifyFrozenNpmTarballContract, "verifyFrozenNpmTarballContract");
70690
71057
  __name(spawnNpmPublishTarball, "spawnNpmPublishTarball");
70691
71058
  __name(queryVersion, "queryVersion");
70692
71059
  __name(createNpmAdapter, "createNpmAdapter");
@@ -73253,19 +73620,7 @@ function createPluginMarketplaceAdapter(deps = {}) {
73253
73620
  }
73254
73621
  let verifiedInstallPath = null;
73255
73622
  if (normalizedAttestation.installPath) {
73256
- const managedRoot = resolve20(attestationDir, "kimi-home", "plugins", "managed");
73257
73623
  const installPathAbs = resolve20(normalizedAttestation.installPath);
73258
- const managedRootReal = await realpath11(managedRoot).catch(() => null);
73259
- if (!managedRootReal) {
73260
- return createResult({
73261
- actionType,
73262
- status: ActionStatus.OBSERVED,
73263
- observation: {
73264
- installed: false,
73265
- error: "KIMI_CODE_HOME does not exist"
73266
- }
73267
- });
73268
- }
73269
73624
  let lexicalStat;
73270
73625
  try {
73271
73626
  lexicalStat = await lstat13(installPathAbs);
@@ -73300,18 +73655,6 @@ function createPluginMarketplaceAdapter(deps = {}) {
73300
73655
  }
73301
73656
  });
73302
73657
  }
73303
- const rel = relative18(managedRootReal, installPathReal2);
73304
- const sep7 = process.platform === "win32" ? "\\" : "/";
73305
- if (rel === "" || rel === ".." || isAbsolute15(rel) || rel.startsWith(`..${sep7}`)) {
73306
- return createResult({
73307
- actionType,
73308
- status: ActionStatus.OBSERVED,
73309
- observation: {
73310
- installed: false,
73311
- error: `kimi attestation installPath escapes the managed root: ${normalizedAttestation.installPath}`
73312
- }
73313
- });
73314
- }
73315
73658
  verifiedInstallPath = installPathReal2;
73316
73659
  }
73317
73660
  let manifestDigest2 = action.manifestDigest;
@@ -79771,13 +80114,13 @@ async function buildProductionAssets(unitResults, resolvedVersions, root, runDir
79771
80114
  tarballDir: resolveUnitScopedPath(resolve22(runDir, "tarballs"), unit.id),
79772
80115
  expectedSnapshotDigest: sealed.digest
79773
80116
  });
79774
- await verifyFrozenNpmTarballIdentity({
80117
+ await verifyFrozenNpmTarballContract({
79775
80118
  package: npmDistribution.package,
79776
80119
  version,
79777
80120
  tarballPath: relative21(root, npm.tarballPath),
79778
80121
  tarballSha256: npm.sha256,
79779
80122
  integrity: npm.integrity
79780
- }, root);
80123
+ }, root, resolveUnitScopedPath(resolve22(runDir, "tarballs"), unit.id));
79781
80124
  }
79782
80125
  assets.push({
79783
80126
  snapshotPath,
@@ -82159,6 +82502,37 @@ async function reconcileRelease(options) {
82159
82502
  );
82160
82503
  }
82161
82504
  await evidence.append({ phase: "safety-gate", gate: "action-completeness", status: "passed" });
82505
+ for (const unit of plan.units ?? []) {
82506
+ const frozen = unit.frozenSnapshot;
82507
+ if (!frozen?.npm) continue;
82508
+ const npmDistribution = (unit.distributions ?? []).find((distribution) => distribution.type === "npm");
82509
+ if (!npmDistribution) {
82510
+ throw new ReleaseError(
82511
+ GATE_FAILED,
82512
+ `unit "${unit.id}" has a frozen npm tarball but no npm distribution`,
82513
+ { gate: "npm-entry-closure", unitId: unit.id }
82514
+ );
82515
+ }
82516
+ await evidence.append({
82517
+ phase: "safety-gate",
82518
+ gate: "npm-entry-closure",
82519
+ unitId: unit.id,
82520
+ status: "started"
82521
+ });
82522
+ await verifyFrozenNpmTarballContract({
82523
+ package: npmDistribution.package,
82524
+ version: unit.targetVersion,
82525
+ tarballPath: frozen.npm.tarballPath,
82526
+ tarballSha256: frozen.npm.tarballSha256,
82527
+ integrity: frozen.npm.integrity
82528
+ }, root);
82529
+ await evidence.append({
82530
+ phase: "safety-gate",
82531
+ gate: "npm-entry-closure",
82532
+ unitId: unit.id,
82533
+ status: "passed"
82534
+ });
82535
+ }
82162
82536
  await evidence.append({ phase: "safety-gate", gate: "source-run-load", status: "started" });
82163
82537
  const sourceRun = await loadRun(sourceRunPath, {
82164
82538
  requireDigest: Boolean(plan.production),
@@ -83070,6 +83444,7 @@ var init_reconcile = __esm({
83070
83444
  init_source_authority();
83071
83445
  init_contract();
83072
83446
  init_observe_retry();
83447
+ init_npm();
83073
83448
  __name(defaultClock3, "defaultClock");
83074
83449
  __name(reconcileRelease, "reconcileRelease");
83075
83450
  }
@@ -83666,7 +84041,7 @@ __export(verify_exports, {
83666
84041
  import { readFile as readFile24, writeFile as writeFile8, mkdtemp as mkdtemp4, rm as rm8, mkdir as mkdir13, lstat as lstat14, realpath as realpath13, readdir as readdir12 } from "node:fs/promises";
83667
84042
  import { realpathSync as realpathSync4 } from "node:fs";
83668
84043
  import { dirname as dirname13, join as join20, relative as relative22, isAbsolute as isAbsolute18, resolve as resolve25, basename as basename8 } from "node:path";
83669
- import { tmpdir as tmpdir3 } from "node:os";
84044
+ import { tmpdir as tmpdir4 } from "node:os";
83670
84045
  import { execFile as execFileCb12 } from "node:child_process";
83671
84046
  import { promisify as promisify12 } from "node:util";
83672
84047
  function defaultClock4() {
@@ -83693,7 +84068,7 @@ function matchesSubset2(actual, expected) {
83693
84068
  return true;
83694
84069
  }
83695
84070
  async function runSmokeTest(plan, root, options = {}) {
83696
- const baseDir = options.baseDir ?? tmpdir3();
84071
+ const baseDir = options.baseDir ?? tmpdir4();
83697
84072
  await mkdir13(baseDir, { recursive: true });
83698
84073
  const tmpDir = await mkdtemp4(join20(baseDir, "verify-smoke-"));
83699
84074
  const npmExec = options.npmExecutor ?? defaultNpmExecutor;
@@ -83796,6 +84171,24 @@ async function runSmokeTest(plan, root, options = {}) {
83796
84171
  };
83797
84172
  }
83798
84173
  const pkgRoot = join20(installDir, "node_modules", pkgName);
84174
+ {
84175
+ const dirIndex = await buildDirectoryFileIndex(pkgRoot);
84176
+ const closureResult = checkNpmEntryClosure(installedPkg, dirIndex);
84177
+ if (closureResult.errors.length > 0) {
84178
+ return {
84179
+ passed: false,
84180
+ details: {
84181
+ gate: "npm-entry-closure",
84182
+ error: `npm entry closure check failed for ${packageAtVersion}: ${closureResult.errors.map((e) => e.message).join("; ")}`,
84183
+ packageAtVersion,
84184
+ unitId,
84185
+ entries: closureResult.entries,
84186
+ errors: closureResult.errors,
84187
+ diagnostics: closureResult.diagnostics
84188
+ }
84189
+ };
84190
+ }
84191
+ }
83799
84192
  if (plan.skillResourceClosure) {
83800
84193
  const expectedUnitReceipt = plan.skillResourceClosure.unitReceipts.find((item) => item.unitId === unitId);
83801
84194
  if (!expectedUnitReceipt) {
@@ -84450,8 +84843,7 @@ async function verifyRelease(options) {
84450
84843
  } : action.type === "codebuddy-marketplace-install" ? {
84451
84844
  HOME: resolve25(runDir, "consumers", `codebuddy-${action.parameters.plugin}`)
84452
84845
  } : {
84453
- HOME: resolve25(runDir, "consumers", `kimi-${action.parameters.plugin}`),
84454
- KIMI_CODE_HOME: resolve25(runDir, "consumers", `kimi-${action.parameters.plugin}`)
84846
+ HOME: resolve25(runDir, "consumers", `kimi-${action.parameters.plugin}`)
84455
84847
  }
84456
84848
  }));
84457
84849
  } else {
@@ -84710,6 +85102,7 @@ var init_verify = __esm({
84710
85102
  init_skill_resource_closure();
84711
85103
  init_checkpoints();
84712
85104
  init_installation_contract();
85105
+ init_npm_entry_closure();
84713
85106
  execFile11 = promisify12(execFileCb12);
84714
85107
  VERIFICATION_RESOLVED_TYPES = Object.freeze({
84715
85108
  PASSED_AUTOMATIC: "PASSED_AUTOMATIC",
@@ -85140,7 +85533,7 @@ async function publishRelease(options) {
85140
85533
  if (!npmDistribution) {
85141
85534
  throw new ReleaseError(GATE_FAILED, `unit "${unit.id}" has a frozen npm tarball but no npm distribution`);
85142
85535
  }
85143
- await verifyFrozenNpmTarballIdentity({
85536
+ await verifyFrozenNpmTarballContract({
85144
85537
  package: npmDistribution.package,
85145
85538
  version: unit.targetVersion,
85146
85539
  tarballPath: frozen.npm.tarballPath,
@@ -86368,7 +86761,7 @@ var init_graph = __esm({
86368
86761
  import { readFile as readFile28, readdir as readdir14, stat as stat6, mkdir as mkdir15, writeFile as writeFile9, rm as rm9 } from "node:fs/promises";
86369
86762
  import { join as join24 } from "node:path";
86370
86763
  import { mkdtemp as mkdtemp5 } from "node:fs/promises";
86371
- import { tmpdir as tmpdir4 } from "node:os";
86764
+ import { tmpdir as tmpdir5 } from "node:os";
86372
86765
  import { createHash as createHash12 } from "node:crypto";
86373
86766
  import { readFileSync as readFileSync4 } from "node:fs";
86374
86767
  function collectStaticImports(filePath, visited = /* @__PURE__ */ new Set()) {
@@ -86506,7 +86899,7 @@ async function runProducerClosure({
86506
86899
  graph,
86507
86900
  inputSnapshot,
86508
86901
  artifactIds,
86509
- tempRootFactory = /* @__PURE__ */ __name(async () => mkdtemp5(join24(tmpdir4(), "producer-")), "tempRootFactory")
86902
+ tempRootFactory = /* @__PURE__ */ __name(async () => mkdtemp5(join24(tmpdir5(), "producer-")), "tempRootFactory")
86510
86903
  } = {}) {
86511
86904
  const generatedSet = new Set(graph.topologicalOrder);
86512
86905
  for (const id of artifactIds) {