release-skill 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/.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 +42 -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 +2619 -1728
  14. package/adapters/claude/schemas/.render-manifest.json +2 -2
  15. package/adapters/claude/schemas/release-project.schema.json +60 -0
  16. package/adapters/claude/skills/release-prepare/SKILL.md +5 -0
  17. package/adapters/claude/skills/release-setup/SKILL.md +10 -1
  18. package/adapters/claude/skills/release-verify/SKILL.md +4 -2
  19. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  20. package/adapters/codex/bin/release-skill.bundle.mjs +2619 -1728
  21. package/adapters/codex/schemas/.render-manifest.json +2 -2
  22. package/adapters/codex/schemas/release-project.schema.json +60 -0
  23. package/adapters/codex/skills/release-prepare/SKILL.md +5 -0
  24. package/adapters/codex/skills/release-setup/SKILL.md +10 -1
  25. package/adapters/codex/skills/release-verify/SKILL.md +4 -2
  26. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  27. package/adapters/kimi/bin/release-skill.bundle.mjs +2619 -1728
  28. package/adapters/kimi/schemas/.render-manifest.json +2 -2
  29. package/adapters/kimi/schemas/release-project.schema.json +60 -0
  30. package/adapters/kimi/skills/release-prepare/SKILL.md +5 -0
  31. package/adapters/kimi/skills/release-setup/SKILL.md +10 -1
  32. package/adapters/kimi/skills/release-verify/SKILL.md +4 -2
  33. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  34. package/adapters/workbuddy/bin/release-skill.bundle.mjs +2619 -1728
  35. package/adapters/workbuddy/schemas/.render-manifest.json +2 -2
  36. package/adapters/workbuddy/schemas/release-project.schema.json +60 -0
  37. package/adapters/workbuddy/skills/release-prepare/SKILL.md +5 -0
  38. package/adapters/workbuddy/skills/release-setup/SKILL.md +10 -1
  39. package/adapters/workbuddy/skills/release-verify/SKILL.md +4 -2
  40. package/bin/release-skill.bundle.mjs +2619 -1728
  41. package/package.json +1 -1
  42. package/schemas/.render-manifest.json +2 -2
  43. package/schemas/release-project.schema.json +60 -0
  44. package/skills/release-prepare/SKILL.md +5 -0
  45. package/skills/release-setup/SKILL.md +10 -1
  46. package/skills/release-verify/SKILL.md +4 -2
  47. package/skills-src/release-prepare/SKILL.md +5 -0
  48. package/skills-src/release-setup/SKILL.md +10 -1
  49. package/skills-src/release-verify/SKILL.md +4 -2
  50. package/src/adapters/npm.mjs +54 -2
  51. package/src/adapters/plugin-marketplace.mjs +4 -29
  52. package/src/commands/prepare.mjs +49 -3
  53. package/src/commands/publish.mjs +2 -2
  54. package/src/commands/reconcile.mjs +39 -0
  55. package/src/commands/setup.mjs +168 -0
  56. package/src/commands/verify.mjs +28 -1
  57. package/src/core/config.mjs +48 -0
  58. package/src/core/public-surface.mjs +509 -0
  59. package/src/npm/npm-entry-closure.mjs +195 -0
  60. package/src/platforms/codebuddy.mjs +19 -11
  61. package/src/platforms/codex.mjs +18 -10
  62. package/src/platforms/kimi.mjs +26 -39
  63. package/src/snapshot/frozen.mjs +51 -19
@@ -24,6 +24,7 @@ import addFormats from 'ajv-formats';
24
24
  import { canonicalJson, sha256Hex } from '../core/digest.mjs';
25
25
  import { acquireProjectLock } from '../artifacts/project-lock.mjs';
26
26
  import { getPlatform, PLATFORMS } from '../platforms/registry.mjs';
27
+ import { checkNpmEntryClosure } from '../npm/npm-entry-closure.mjs';
27
28
  import {
28
29
  CONFIG_EXISTS,
29
30
  CONFIG_INVALID,
@@ -201,6 +202,8 @@ function summarizeLegacyReleaseConfig(value, path) {
201
202
  tagPrefix: optionalString(repo.tagPrefix),
202
203
  npmPackage: optionalString(repo.npmPackage),
203
204
  npmPackageDeclared: Object.hasOwn(repo, 'npmPackage'),
205
+ npmRequiredPathCandidates: stringList(repo.npmRequiredPackagePaths),
206
+ npmRequiredPathsDeclared: Object.hasOwn(repo, 'npmRequiredPackagePaths'),
204
207
  docsSource: optionalString(repo.docsSource),
205
208
  requiredPathCandidates: stringList(repo.requiredPackagePaths),
206
209
  snapshotCommands: Array.isArray(repo.snapshotCommands) ? repo.snapshotCommands : [],
@@ -223,6 +226,8 @@ function summarizeLegacyReleaseConfig(value, path) {
223
226
  npmPackageDeclared: plugins.some((plugin) => (
224
227
  plugin && typeof plugin === 'object' && !Array.isArray(plugin) && Object.hasOwn(plugin, 'npmPackage')
225
228
  )),
229
+ npmRequiredPathCandidates: stringList(value.npmRequiredPackagePaths),
230
+ npmRequiredPathsDeclared: Object.hasOwn(value, 'npmRequiredPackagePaths'),
226
231
  docsSource: null,
227
232
  requiredPathCandidates: stringList(value.requiredPaths),
228
233
  snapshotCommands: Array.isArray(value.snapshotCommands) ? value.snapshotCommands : [],
@@ -467,6 +472,111 @@ async function discoverUnitGit(unitAbsDir, parentRoot) {
467
472
  };
468
473
  }
469
474
 
475
+ async function pathIsGitIgnored(unitAbsDir, target) {
476
+ // --no-index also classifies generated paths that do not exist yet.
477
+ try {
478
+ await execFile('git', ['check-ignore', '--no-index', '--quiet', '--', target], {
479
+ cwd: unitAbsDir,
480
+ shell: false,
481
+ timeout: 5000,
482
+ });
483
+ return true;
484
+ } catch {
485
+ return false;
486
+ }
487
+ }
488
+
489
+ async function classifyNpmEntryCandidate(unitAbsDir, target, trackedFiles) {
490
+ const tracked = trackedFiles.includes(target);
491
+ const ignored = await pathIsGitIgnored(unitAbsDir, target);
492
+ let stat = null;
493
+ try {
494
+ stat = await lstat(join(unitAbsDir, target));
495
+ } catch (error) {
496
+ if (error.code !== 'ENOENT') throw error;
497
+ }
498
+ const exists = stat !== null;
499
+ const regular = stat?.isFile() === true && stat?.isSymbolicLink() !== true;
500
+ const state = exists && !regular
501
+ ? 'NON_REGULAR'
502
+ : exists && tracked
503
+ ? 'TRACKED_PRESENT'
504
+ : exists && ignored
505
+ ? 'IGNORED_PRESENT'
506
+ : exists
507
+ ? 'UNTRACKED_PRESENT'
508
+ : ignored
509
+ ? 'IGNORED_MISSING'
510
+ : 'MISSING';
511
+ return { path: target, state, exists, tracked, ignored };
512
+ }
513
+
514
+ async function discoverNpmEntryCandidates(root, pkg, matchingLegacyUnits, unitGitEntry) {
515
+ const emptyIndex = new Map();
516
+ const semantic = checkNpmEntryClosure(pkg.entryManifest, emptyIndex);
517
+ const byPath = new Map();
518
+ const add = (target, source) => {
519
+ const current = byPath.get(target) ?? { path: target, sources: new Set() };
520
+ current.sources.add(source);
521
+ byPath.set(target, current);
522
+ };
523
+ for (const entry of semantic.entries) add(entry.target, `package.json:${entry.field}`);
524
+
525
+ const diagnostics = [
526
+ ...semantic.errors
527
+ .filter((error) => error.reason !== 'entry_missing')
528
+ .map((error) => ({ code: 'NPM_ENTRY_DECLARATION_INVALID', ...error })),
529
+ ...semantic.diagnostics.map((diagnostic) => ({
530
+ code: 'NPM_ENTRY_WILDCARD_REQUIRES_REVIEW',
531
+ ...diagnostic,
532
+ })),
533
+ ];
534
+ const declaredLegacyPaths = new Set();
535
+ const legacyCoverageDeclared = matchingLegacyUnits.some((unit) => unit.npmRequiredPathsDeclared);
536
+ for (const legacy of matchingLegacyUnits) {
537
+ for (const target of legacy.npmRequiredPathCandidates) {
538
+ const checked = checkNpmEntryClosure({ main: target }, emptyIndex);
539
+ const normalized = checked.entries[0]?.target;
540
+ if (!normalized) {
541
+ diagnostics.push({
542
+ code: 'LEGACY_NPM_REQUIRED_PATH_INVALID',
543
+ path: target,
544
+ reason: checked.errors[0]?.reason ?? 'invalid_path',
545
+ });
546
+ continue;
547
+ }
548
+ declaredLegacyPaths.add(normalized);
549
+ add(normalized, 'public-release.json:npmRequiredPackagePaths');
550
+ }
551
+ }
552
+ if (legacyCoverageDeclared) {
553
+ for (const entry of semantic.entries) {
554
+ if (!declaredLegacyPaths.has(entry.target)) {
555
+ diagnostics.push({
556
+ code: 'LEGACY_NPM_ENTRY_COVERAGE_MISSING',
557
+ path: entry.target,
558
+ field: entry.field,
559
+ });
560
+ }
561
+ }
562
+ }
563
+
564
+ const unitAbsDir = resolve(root, pkg.directory);
565
+ const candidates = [];
566
+ for (const item of [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path))) {
567
+ candidates.push({
568
+ ...await classifyNpmEntryCandidate(
569
+ unitAbsDir,
570
+ item.path,
571
+ unitGitEntry?.trackedFiles ?? [],
572
+ ),
573
+ sources: [...item.sources].sort(),
574
+ });
575
+ }
576
+ diagnostics.sort((a, b) => canonicalJson(a).localeCompare(canonicalJson(b)));
577
+ return { candidates, diagnostics };
578
+ }
579
+
470
580
  async function discoverFacts(root) {
471
581
  const files = await walkDiscoveryFiles(root);
472
582
  const packageFiles = files.filter((path) => (
@@ -494,6 +604,11 @@ async function discoverFacts(root) {
494
604
  private: pkg.private === true,
495
605
  repository: parseGithubRepo(pkg.repository),
496
606
  publishRegistry: typeof pkg.publishConfig?.registry === 'string' ? pkg.publishConfig.registry : null,
607
+ entryManifest: Object.fromEntries(
608
+ ['bin', 'main', 'module', 'types', 'typings', 'exports']
609
+ .filter((field) => Object.hasOwn(pkg, field))
610
+ .map((field) => [field, pkg[field]]),
611
+ ),
497
612
  files: Array.isArray(pkg.files) ? pkg.files.filter((item) => typeof item === 'string').sort() : [],
498
613
  scripts: Object.fromEntries(Object.entries(pkg.scripts ?? {})
499
614
  .filter(([, value]) => typeof value === 'string')
@@ -557,6 +672,22 @@ async function discoverFacts(root) {
557
672
  for (const pkg of packages) {
558
673
  const unitAbsDir = resolve(root, pkg.directory);
559
674
  unitGit[pkg.directory] = await discoverUnitGit(unitAbsDir, root);
675
+ const inferredId = safeUnitId(pkg, pkg.directory);
676
+ const matchingLegacyUnits = legacyReleaseConfigs
677
+ .flatMap((config) => config.releaseUnits)
678
+ .filter((unit) => (
679
+ unit.id === inferredId ||
680
+ unit.source === pkg.directory ||
681
+ unit.source === dirname(pkg.path)
682
+ ));
683
+ const npmDiscovery = await discoverNpmEntryCandidates(
684
+ root,
685
+ pkg,
686
+ matchingLegacyUnits,
687
+ unitGit[pkg.directory],
688
+ );
689
+ pkg.npmEntryCandidates = npmDiscovery.candidates;
690
+ pkg.npmEntryDiagnostics = npmDiscovery.diagnostics;
560
691
  }
561
692
 
562
693
  return { git, packages, manifests, skills, legacyReleaseConfigs, fileDigests, unitGit };
@@ -871,6 +1002,8 @@ function buildCandidates(facts) {
871
1002
  publicFileMappingCandidates: mappingResult.mappings,
872
1003
  publicFileMappingConflicts: mappingResult.conflicts,
873
1004
  requiredPublicFileCandidates,
1005
+ npmEntryCandidates: pkg.npmEntryCandidates ?? [],
1006
+ npmEntryDiagnostics: pkg.npmEntryDiagnostics ?? [],
874
1007
  entrySkillCandidates: entrySkillCandidatesForUnit(facts, pkg, id),
875
1008
  ...(hasAuthorityConflict ? {
876
1009
  authorityConflict: {
@@ -1027,6 +1160,24 @@ function buildRecommendedProposal(facts, candidates) {
1027
1160
  assumptions,
1028
1161
  };
1029
1162
  }
1163
+ if (
1164
+ unit.distributionCandidates.includes('npm') &&
1165
+ (
1166
+ (unit.npmEntryCandidates ?? []).some((candidate) => candidate.state !== 'TRACKED_PRESENT') ||
1167
+ (unit.npmEntryDiagnostics ?? []).length > 0
1168
+ )
1169
+ ) {
1170
+ return {
1171
+ answers: null,
1172
+ conflicts: [{
1173
+ code: 'NPM_ENTRY_REVIEW_REQUIRED',
1174
+ unit: unit.id,
1175
+ candidates: unit.npmEntryCandidates,
1176
+ diagnostics: unit.npmEntryDiagnostics,
1177
+ }],
1178
+ assumptions,
1179
+ };
1180
+ }
1030
1181
 
1031
1182
  // Infer npm publisher: prefer legacy owner, then parse from repo slug
1032
1183
  const repoOwner = publicRepo.includes('/') ? publicRepo.split('/')[0] : null;
@@ -1245,6 +1396,15 @@ function buildDecisionsRequired(candidates, localOnly) {
1245
1396
  id: `unit:${unit.id}:distributions-and-files`,
1246
1397
  description: `逐项确认渠道 ${JSON.stringify(unit.distributionCandidates)}、公开文件边界和 requiredPublicFiles;候选不是授权。`,
1247
1398
  });
1399
+ if (
1400
+ (unit.npmEntryCandidates ?? []).length > 0 ||
1401
+ (unit.npmEntryDiagnostics ?? []).length > 0
1402
+ ) {
1403
+ decisions.push({
1404
+ id: `unit:${unit.id}:npm-entry-closure`,
1405
+ description: '审阅 package.json 入口及旧 npmRequiredPackagePaths 的 tracked/untracked/ignored/missing 状态;setup 只报告候选,不自动写入 publicFiles 或 requiredPublicFiles。',
1406
+ });
1407
+ }
1248
1408
  }
1249
1409
  decisions.push({
1250
1410
  id: 'verification-gates',
@@ -1271,6 +1431,8 @@ function buildCompactSummary(report) {
1271
1431
  distributionCandidates: [...unit.distributionCandidates].sort(),
1272
1432
  publicFileMappingCount: (unit.publicFileMappingCandidates ?? []).length,
1273
1433
  requiredPublicFileCount: (unit.requiredPublicFileCandidates ?? []).length,
1434
+ npmEntryCandidates: unit.npmEntryCandidates ?? [],
1435
+ npmEntryDiagnostics: unit.npmEntryDiagnostics ?? [],
1274
1436
  })),
1275
1437
  gateCandidates: (report.gateCandidates ?? []).map((gate) => ({
1276
1438
  id: gate.id,
@@ -1519,6 +1681,12 @@ export async function setupProject({ root, answersPath, write = false, confirmSe
1519
1681
  ...(unconfiguredGateCandidateIds.length > 0
1520
1682
  ? ['存在未配置的验证候选;逐项审阅副作用后决定是否人工注册。']
1521
1683
  : []),
1684
+ ...(candidates.units.some((unit) => (
1685
+ (unit.npmEntryCandidates ?? []).some((candidate) => candidate.state !== 'TRACKED_PRESENT') ||
1686
+ (unit.npmEntryDiagnostics ?? []).length > 0
1687
+ ))
1688
+ ? ['npm 入口候选存在未跟踪、被忽略、缺失、非普通文件或声明覆盖疑点;人工增量修复配置或构建流程,setup 不自动改写。']
1689
+ : []),
1522
1690
  ],
1523
1691
  },
1524
1692
  recommendedGateIds: [],
@@ -63,6 +63,10 @@ import {
63
63
  shouldSkipVerification,
64
64
  INSTALLATION_CONTRACT_ALGORITHM_VERSION,
65
65
  } from '../core/installation-contract.mjs';
66
+ import {
67
+ buildDirectoryFileIndex,
68
+ checkNpmEntryClosure,
69
+ } from '../npm/npm-entry-closure.mjs';
66
70
 
67
71
  // ---------------------------------------------------------------------------
68
72
  // Constants
@@ -294,6 +298,30 @@ export async function runSmokeTest(plan, root, options = {}) {
294
298
  }
295
299
 
296
300
  const pkgRoot = join(installDir, 'node_modules', pkgName);
301
+
302
+ // Entry closure check: verify all declared entry points (bin, main,
303
+ // module, types, typings, exports) exist as regular files in the
304
+ // installed package. This catches incident-class tarballs where
305
+ // name/version are correct but distribution files are missing.
306
+ {
307
+ const dirIndex = await buildDirectoryFileIndex(pkgRoot);
308
+ const closureResult = checkNpmEntryClosure(installedPkg, dirIndex);
309
+ if (closureResult.errors.length > 0) {
310
+ return {
311
+ passed: false,
312
+ details: {
313
+ gate: 'npm-entry-closure',
314
+ error: `npm entry closure check failed for ${packageAtVersion}: ${closureResult.errors.map((e) => e.message).join('; ')}`,
315
+ packageAtVersion,
316
+ unitId,
317
+ entries: closureResult.entries,
318
+ errors: closureResult.errors,
319
+ diagnostics: closureResult.diagnostics,
320
+ },
321
+ };
322
+ }
323
+ }
324
+
297
325
  if (plan.skillResourceClosure) {
298
326
  const expectedUnitReceipt = plan.skillResourceClosure.unitReceipts
299
327
  .find((item) => item.unitId === unitId);
@@ -1225,7 +1253,6 @@ export async function verifyRelease(options) {
1225
1253
  }
1226
1254
  : {
1227
1255
  HOME: resolve(runDir, 'consumers', `kimi-${action.parameters.plugin}`),
1228
- KIMI_CODE_HOME: resolve(runDir, 'consumers', `kimi-${action.parameters.plugin}`),
1229
1256
  },
1230
1257
  }));
1231
1258
  } else {
@@ -23,6 +23,7 @@ import Ajv from 'ajv';
23
23
  import addFormats from 'ajv-formats';
24
24
  import { canonicalJson, sha256Hex } from './digest.mjs';
25
25
  import { ReleaseError, CONFIG_INVALID } from './errors.mjs';
26
+ import { validatePublicSurfaceGlob } from './public-surface.mjs';
26
27
  import { canonicalPublicPath, publicPathCollisionKey } from '../snapshot/public-path.mjs';
27
28
  import { isReservedReleaseControlPath } from './baseline.mjs';
28
29
  import { readTrustedPackageResource } from './trusted-resource.mjs';
@@ -294,6 +295,53 @@ export async function loadProjectConfig({ root, configPath } = {}) {
294
295
  );
295
296
  }
296
297
 
298
+ // Validate expected public-surface roots and the deliberately small glob
299
+ // language before generic schema validation so diagnostics identify the
300
+ // exact unit, scan root, and pattern.
301
+ const surface = unit.expectedPublicSurface;
302
+ if (surface && typeof surface === 'object' && Array.isArray(surface.scanRoots)) {
303
+ for (const [rootIndex, scanRoot] of surface.scanRoots.entries()) {
304
+ if (!scanRoot || typeof scanRoot !== 'object') continue;
305
+ if (typeof scanRoot.root === 'string') {
306
+ try {
307
+ canonicalPublicPath(scanRoot.root, { allowDot: true });
308
+ } catch (err) {
309
+ throw new ReleaseError(
310
+ CONFIG_INVALID,
311
+ `unit "${unit.id}" has invalid expectedPublicSurface scan root: ${err.message}`,
312
+ {
313
+ unitId: unit.id,
314
+ rootIndex,
315
+ root: scanRoot.root,
316
+ field: 'expectedPublicSurface.scanRoots[].root',
317
+ },
318
+ );
319
+ }
320
+ }
321
+ for (const field of ['include', 'exclude']) {
322
+ if (!Array.isArray(scanRoot[field])) continue;
323
+ for (const pattern of scanRoot[field]) {
324
+ if (typeof pattern !== 'string') continue;
325
+ try {
326
+ validatePublicSurfaceGlob(pattern);
327
+ } catch (err) {
328
+ throw new ReleaseError(
329
+ CONFIG_INVALID,
330
+ `unit "${unit.id}" has invalid expectedPublicSurface ${field} glob: ${err.message}`,
331
+ {
332
+ unitId: unit.id,
333
+ rootIndex,
334
+ pattern,
335
+ field: `expectedPublicSurface.scanRoots[].${field}`,
336
+ reason: err.details?.reason ?? 'PUBLIC_SURFACE_GLOB_INVALID',
337
+ },
338
+ );
339
+ }
340
+ }
341
+ }
342
+ }
343
+ }
344
+
297
345
  // Validate publicFiles[].from and .to
298
346
  if (Array.isArray(unit.publicFiles)) {
299
347
  for (const mapping of unit.publicFiles) {