superpowers-mcp 6.4.1 → 6.4.3

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.
package/scripts/setup.js CHANGED
@@ -7,16 +7,109 @@
7
7
  const path = require("path");
8
8
  const fs = require("fs");
9
9
 
10
- const compiledRunner = path.join(__dirname, "..", "out", "setup-runner.js");
10
+ const repoRoot = path.join(__dirname, "..");
11
+ const compiledRunner = path.join(repoRoot, "out", "setup-runner.js");
12
+ const lockPath = path.join(repoRoot, "out", ".setup-build.lock");
11
13
 
12
- if (!fs.existsSync(compiledRunner)) {
13
- const { execSync } = require("child_process");
14
+ const inputFiles = [
15
+ path.join(repoRoot, "src", "setup-runner.ts"),
16
+ path.join(repoRoot, "src", "setup-cli.ts"),
17
+ path.join(repoRoot, "esbuild.js"),
18
+ path.join(repoRoot, "package.json"),
19
+ ];
20
+
21
+ function sleepSync(ms) {
22
+ // Portable synchronous sleep; blocks this thread while waiting on the lock.
23
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
24
+ }
25
+
26
+ function newestInputMtime() {
27
+ let newest = 0;
28
+ for (const file of inputFiles) {
29
+ try {
30
+ newest = Math.max(newest, fs.statSync(file).mtimeMs);
31
+ } catch {
32
+ // Ignore missing inputs.
33
+ }
34
+ }
35
+ return newest;
36
+ }
37
+
38
+ // Rebuild when the output is missing OR any build input is newer, so edits to the
39
+ // sources are never silently ignored (the old check only tested for existence).
40
+ function needsBuild() {
41
+ if (!fs.existsSync(compiledRunner)) {
42
+ return true;
43
+ }
44
+ try {
45
+ return fs.statSync(compiledRunner).mtimeMs < newestInputMtime();
46
+ } catch {
47
+ return true;
48
+ }
49
+ }
50
+
51
+ function acquireLock() {
52
+ fs.mkdirSync(path.dirname(lockPath), { recursive: true });
53
+ try {
54
+ fs.writeFileSync(lockPath, String(process.pid), { flag: "wx" });
55
+ return true;
56
+ } catch (err) {
57
+ if (err && err.code === "EEXIST") {
58
+ return false; // another build holds the lock
59
+ }
60
+ // Unexpected lock error (e.g. permissions) — proceed without exclusion; the
61
+ // build itself will surface any real failure.
62
+ return true;
63
+ }
64
+ }
65
+
66
+ function releaseLock() {
14
67
  try {
15
- execSync("node esbuild.js", { cwd: path.join(__dirname, ".."), stdio: "inherit" });
16
- } catch (e) {
17
- console.error("Failed to build out/setup-runner.js automatically:", e.message);
68
+ fs.unlinkSync(lockPath);
69
+ } catch {
70
+ // Ignore: we may not own it.
71
+ }
72
+ }
73
+
74
+ function lockIsStale() {
75
+ try {
76
+ return Date.now() - fs.statSync(lockPath).mtimeMs > 120000;
77
+ } catch {
78
+ return false; // already gone
79
+ }
80
+ }
81
+
82
+ if (needsBuild()) {
83
+ const { execSync } = require("child_process");
84
+
85
+ const deadline = Date.now() + 60000;
86
+ let owned = acquireLock();
87
+
88
+ // Wait for a concurrent build to finish (and release the lock) before building,
89
+ // so two invocations never run esbuild at once and interleave writes to out/*.
90
+ while (!owned && needsBuild() && Date.now() < deadline) {
91
+ if (lockIsStale()) {
92
+ releaseLock();
93
+ owned = acquireLock();
94
+ break;
95
+ }
96
+ sleepSync(250);
97
+ }
98
+
99
+ if (owned) {
100
+ try {
101
+ execSync("node esbuild.js", { cwd: repoRoot, stdio: "inherit" });
102
+ } catch (e) {
103
+ console.error("Failed to build out/setup-runner.js automatically:", e.message);
104
+ process.exit(1);
105
+ } finally {
106
+ releaseLock();
107
+ }
108
+ } else if (needsBuild()) {
109
+ console.error("Timed out waiting for a concurrent build of out/setup-runner.js.");
18
110
  process.exit(1);
19
111
  }
112
+ // If !owned but !needsBuild(), another process finished the build — reuse it.
20
113
  }
21
114
 
22
115
  const runner = require(compiledRunner);
@@ -58,6 +58,7 @@ function parseArgs(argv) {
58
58
  else if (arg === "--ref" || arg === "--repo" || arg === "--ignore") {
59
59
  const value = argv[++i];
60
60
  if (!value) throw new Error(`Missing value for ${arg}`);
61
+ if (value.startsWith("--")) throw new Error(`Missing value for ${arg} (got "${value}")`);
61
62
  if (arg === "--ref") opts.ref = value;
62
63
  else if (arg === "--repo") opts.repo = value;
63
64
  else opts.ignore.push(value);
@@ -269,19 +270,46 @@ async function main() {
269
270
  }
270
271
  const upstreamSkills = [...new Set(Object.keys(tree.files).map(skillNameOf).filter(Boolean))].sort();
271
272
  const recorded = { repo, ref, commit: tree.commit, capturedAt: new Date().toISOString().slice(0, 10), files, upstreamSkills, ignoredUpstreamSkills: ignored };
272
- fs.writeFileSync(BASELINE_PATH, JSON.stringify(recorded, null, 2) + "\n", "utf-8");
273
+ // Atomic write: temp file + rename so an interrupted --record can never
274
+ // truncate/corrupt the baseline (which a later run would then silently rebuild).
275
+ const tmpPath = `${BASELINE_PATH}.${process.pid}.tmp`;
276
+ try {
277
+ fs.writeFileSync(tmpPath, JSON.stringify(recorded, null, 2) + "\n", "utf-8");
278
+ fs.renameSync(tmpPath, BASELINE_PATH);
279
+ } catch (writeErr) {
280
+ try { fs.unlinkSync(tmpPath); } catch { /* ignore */ }
281
+ throw writeErr;
282
+ }
283
+
284
+ const coverageAfter = classifyCoverage(recorded, localPaths);
285
+ const movedCount = previousDrift.changed.length + previousDrift.added.length + previousDrift.removed.length;
286
+ if (opts.json) {
287
+ console.log(JSON.stringify({
288
+ recorded: true,
289
+ repo,
290
+ ref,
291
+ commit: recorded.commit,
292
+ capturedAt: recorded.capturedAt,
293
+ source: tree.source,
294
+ tracked: Object.keys(files).length,
295
+ upstreamTotal: Object.keys(tree.files).length,
296
+ movedSincePrevious: movedCount,
297
+ ignored: opts.ignore,
298
+ coverage: coverageAfter,
299
+ }, null, 2));
300
+ return;
301
+ }
273
302
 
274
303
  console.log(`Baseline recorded from ${tree.source}: ${repo}@${ref} ${tree.commit.slice(0, 12)} — ${Object.keys(files).length} tracked files of ${Object.keys(tree.files).length} upstream skill files`);
275
304
  if (Object.keys(previousFiles).length > 0) {
276
- const moved = previousDrift.changed.length + previousDrift.added.length + previousDrift.removed.length;
277
- console.log(moved === 0
305
+ console.log(movedCount === 0
278
306
  ? "No adopted skill file moved upstream since the previous baseline."
279
- : `warning: ${moved} adopted skill file(s) moved upstream since the previous baseline — record only what you actually reviewed.`);
307
+ : `warning: ${movedCount} adopted skill file(s) moved upstream since the previous baseline — record only what you actually reviewed.`);
280
308
  }
281
309
  if (opts.ignore.length > 0) {
282
310
  console.log(`Recorded as deliberately not adopted: ${opts.ignore.join(", ")}`);
283
311
  }
284
- reportCoverage(classifyCoverage(recorded, localPaths));
312
+ reportCoverage(coverageAfter);
285
313
  return;
286
314
  }
287
315
 
@@ -305,6 +333,13 @@ async function main() {
305
333
 
306
334
  const tree = await fetchUpstreamTree(repo, ref);
307
335
  const drift = classifyDrift(baseline.files, tree.files, ignored);
336
+ // A truncated upstream tree has files missing from the listing; those baseline
337
+ // files would be misclassified as "removed". Suppress the unreliable removals so
338
+ // the report does not invent drift (changed/added only cover files that ARE listed
339
+ // and remain trustworthy).
340
+ if (tree.truncated) {
341
+ drift.removed = [];
342
+ }
308
343
  const driftCount = drift.changed.length + drift.added.length + drift.removed.length;
309
344
  if (opts.json) {
310
345
  console.log(JSON.stringify({