pan-wizard 3.27.0 → 3.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@
3
3
  * release-check.js — Pre-publish validation gate.
4
4
  *
5
5
  * Wired into `prepublishOnly` so `npm publish` fails BEFORE upload if any
6
- * gate is red. Runs seven checks in order; first failure aborts.
6
+ * gate is red. Runs eight checks in order; first failure aborts.
7
7
  *
8
8
  * 1. build:hooks — hook scripts copy/build cleanly
9
9
  * 2. test:all — full test suite (unit + scenario) passes
@@ -57,7 +57,7 @@ function run(cmd, args, opts = {}) {
57
57
  }
58
58
 
59
59
  // Gate 1: build:hooks
60
- process.stderr.write('\n[release-check] Gate 1/7: build:hooks\n');
60
+ process.stderr.write('\n[release-check] Gate 1/8: build:hooks\n');
61
61
  {
62
62
  const r = run('npm', ['run', 'build:hooks']);
63
63
  logGate('build:hooks', r.status === 0, r.status !== 0 ? `exit ${r.status}` : '');
@@ -65,7 +65,7 @@ process.stderr.write('\n[release-check] Gate 1/7: build:hooks\n');
65
65
  }
66
66
 
67
67
  // Gate 2: test:all
68
- process.stderr.write('\n[release-check] Gate 2/7: test:all\n');
68
+ process.stderr.write('\n[release-check] Gate 2/8: test:all\n');
69
69
  {
70
70
  const r = run('npm', ['run', 'test:all']);
71
71
  logGate('test:all', r.status === 0, r.status !== 0 ? `exit ${r.status}` : '');
@@ -74,9 +74,9 @@ process.stderr.write('\n[release-check] Gate 2/7: test:all\n');
74
74
 
75
75
  // Gate 3: npm audit (production deps only)
76
76
  if (SKIP_AUDIT) {
77
- process.stderr.write('\n[release-check] Gate 3/7: npm audit (SKIPPED)\n');
77
+ process.stderr.write('\n[release-check] Gate 3/8: npm audit (SKIPPED)\n');
78
78
  } else {
79
- process.stderr.write('\n[release-check] Gate 3/7: npm audit --omit=dev\n');
79
+ process.stderr.write('\n[release-check] Gate 3/8: npm audit --omit=dev\n');
80
80
  const r = run('npm', ['audit', '--omit=dev', '--audit-level=high'], { capture: true });
81
81
  // npm audit exits non-zero on findings. We tolerate moderate; fail on high+.
82
82
  const ok = r.status === 0;
@@ -88,7 +88,7 @@ if (SKIP_AUDIT) {
88
88
  }
89
89
 
90
90
  // Gate 4: doc-lint counts on user-facing docs (count-SSoT enforcement)
91
- process.stderr.write('\n[release-check] Gate 4/7: doc-lint counts docs/\n');
91
+ process.stderr.write('\n[release-check] Gate 4/8: doc-lint counts docs/\n');
92
92
  {
93
93
  const tools = path.join(REPO_ROOT, 'pan-wizard-core', 'bin', 'pan-tools.cjs');
94
94
  const docsDir = path.join(REPO_ROOT, 'docs');
@@ -103,7 +103,7 @@ process.stderr.write('\n[release-check] Gate 4/7: doc-lint counts docs/\n');
103
103
 
104
104
  // Gate 5: doc↔code link graph resolves (anti-fake — a doc cannot reference a
105
105
  // code anchor that doesn't exist; deterministic, self-enforcing exit 1).
106
- process.stderr.write('\n[release-check] Gate 5/7: links validate\n');
106
+ process.stderr.write('\n[release-check] Gate 5/8: links validate\n');
107
107
  {
108
108
  const tools = path.join(REPO_ROOT, 'pan-wizard-core', 'bin', 'pan-tools.cjs');
109
109
  const r = run('node', [tools, 'links', 'validate', '--raw'], { capture: true });
@@ -125,16 +125,21 @@ process.stderr.write('\n[release-check] Gate 5/7: links validate\n');
125
125
 
126
126
  // Gate 6: npm pack — produces a non-empty, sanely-sized tarball (read the file,
127
127
  // never parse stdout)
128
- process.stderr.write('\n[release-check] Gate 6/7: npm pack (size sanity)\n');
128
+ process.stderr.write('\n[release-check] Gate 6/8: npm pack (size sanity)\n');
129
129
  {
130
130
  const tmp6 = fs.mkdtempSync(path.join(os.tmpdir(), 'pan-release-pack-'));
131
131
  const r = run('npm', ['pack', '--pack-destination', tmp6], { capture: true });
132
132
  const tgz = r.status === 0 ? fs.readdirSync(tmp6).find(f => f.endsWith('.tgz')) : null;
133
133
  const size = tgz ? fs.statSync(path.join(tmp6, tgz)).size : 0;
134
134
  const sizeMB = (size / 1024 / 1024).toFixed(2);
135
+ // R8: "zero runtime dependencies" is a headline claim (README, COMPARISON.md); a
136
+ // dependency added by accident must turn the release red before it ships. The unit
137
+ // pin is tests/package-contract.test.cjs; this is the publish-time backstop.
138
+ const deps = Object.keys(require(path.join(REPO_ROOT, 'package.json')).dependencies || {});
139
+ const zeroDeps = deps.length === 0;
135
140
  // Sane = a non-empty tarball under 50MB (large for a zero-runtime-dep tool)
136
- const ok = r.status === 0 && !!tgz && size > 0 && size < 50 * 1024 * 1024;
137
- logGate('npm pack', ok, tgz ? `${sizeMB}MB tarball` : `no tarball (exit ${r.status})`);
141
+ const ok = r.status === 0 && !!tgz && size > 0 && size < 50 * 1024 * 1024 && zeroDeps;
142
+ logGate('npm pack', ok, (tgz ? `${sizeMB}MB tarball` : `no tarball (exit ${r.status})`) + (zeroDeps ? '' : `; runtime dependencies present: ${deps.join(', ')}`));
138
143
  fs.rmSync(tmp6, { recursive: true, force: true });
139
144
  if (!ok) {
140
145
  process.stderr.write((r.stderr || '') + '\n');
@@ -144,9 +149,9 @@ process.stderr.write('\n[release-check] Gate 6/7: npm pack (size sanity)\n');
144
149
 
145
150
  // Gate 7: smoke install — pack and install into temp dir, run pan-tools
146
151
  if (SKIP_SMOKE) {
147
- process.stderr.write('\n[release-check] Gate 7/7: smoke install (SKIPPED)\n');
152
+ process.stderr.write('\n[release-check] Gate 7/8: smoke install (SKIPPED)\n');
148
153
  } else {
149
- process.stderr.write('\n[release-check] Gate 7/7: smoke install (npm pack + install + sanity)\n');
154
+ process.stderr.write('\n[release-check] Gate 7/8: smoke install (npm pack + install + sanity)\n');
150
155
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pan-release-smoke-'));
151
156
  try {
152
157
  // Pack — read the .tgz npm writes to tmpDir; never parse its stdout (see note).
@@ -192,6 +197,47 @@ if (SKIP_SMOKE) {
192
197
  }
193
198
  }
194
199
 
200
+ // Gate 8: distribution bundles — both plugin builders produce a manifest. Built into
201
+ // temp dirs (never dist/) so the gate cannot race a concurrently running test and
202
+ // leaves the checkout untouched. A bundle that fails to build is a release that
203
+ // ships a broken marketplace entry.
204
+ process.stderr.write('\n[release-check] Gate 8/8: distribution bundles (build:plugin + build:agent-plugin)\n');
205
+ {
206
+ const tmp8 = fs.mkdtempSync(path.join(os.tmpdir(), 'pan-release-bundles-'));
207
+ try {
208
+ const claudeOut = path.join(tmp8, 'claude');
209
+ const agentOut = path.join(tmp8, 'agent');
210
+ const a = run('node', [path.join(REPO_ROOT, 'scripts', 'build-plugin.js')], { capture: true, env: { ...process.env, PAN_PLUGIN_OUT: claudeOut } });
211
+ const b = run('node', [path.join(REPO_ROOT, 'scripts', 'build-agent-plugin.js')], { capture: true, env: { ...process.env, PAN_AGENT_PLUGIN_OUT: agentOut } });
212
+ const okA = a.status === 0 && fs.existsSync(path.join(claudeOut, '.claude-plugin', 'plugin.json'));
213
+ const okB = b.status === 0 && fs.existsSync(path.join(agentOut, 'plugin.json')) && fs.existsSync(path.join(agentOut, 'mcp.json'));
214
+ // R10: .agents/plugins/marketplace.json (Codex) and .github/plugin/marketplace.json
215
+ // (Copilot) resolve to ./dist/pan-agent-plugin with no rebuild-on-resolve — unlike
216
+ // the Claude `command` source. A stale dist/ shipped silently on 2026-09-10 (built
217
+ // before the vendor-directory commit). Compare it with the fresh build when it
218
+ // exists; the gate stays read-only and never writes dist/.
219
+ let staleDetail = '';
220
+ const distAgent = path.join(REPO_ROOT, 'dist', 'pan-agent-plugin');
221
+ if (okB && fs.existsSync(distAgent)) {
222
+ const { dirDigest } = require(path.join(REPO_ROOT, 'bin', 'install-lib.cjs'));
223
+ if (dirDigest(distAgent) !== dirDigest(agentOut)) {
224
+ staleDetail = 'dist/pan-agent-plugin is STALE — run `npm run build:agent-plugin` (the Codex and Copilot marketplaces install from it)';
225
+ }
226
+ }
227
+ const ok8 = okA && okB && !staleDetail;
228
+ const detail = ok8
229
+ ? 'Claude plugin + Agent Plugins bundle built' + (fs.existsSync(distAgent) ? '; dist/pan-agent-plugin matches the fresh build' : '')
230
+ : staleDetail || `claude:${okA ? 'ok' : 'FAIL exit ' + a.status} agent-plugins:${okB ? 'ok' : 'FAIL exit ' + b.status}`;
231
+ logGate('distribution bundles', ok8, detail);
232
+ if (!ok8) {
233
+ process.stderr.write((a.stderr || '') + (b.stderr || '') + '\n');
234
+ process.exit(1);
235
+ }
236
+ } finally {
237
+ try { fs.rmSync(tmp8, { recursive: true, force: true }); } catch { /* best-effort */ }
238
+ }
239
+ }
240
+
195
241
  // Summary
196
242
  process.stderr.write('\n[release-check] Summary:\n');
197
243
  for (const c of checks) {