superpowers-manager 0.1.6 → 0.3.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.
Files changed (59) hide show
  1. package/README.md +184 -64
  2. package/bin/superpowers-manager.js +9 -237
  3. package/dist/adapter-result.js +204 -0
  4. package/dist/adapter.js +649 -0
  5. package/dist/atomic.js +142 -0
  6. package/dist/cli-arguments.js +32 -0
  7. package/dist/cli.js +293 -0
  8. package/dist/codex-json.js +89 -0
  9. package/dist/codex-state.js +78 -0
  10. package/dist/commands/context.js +1 -0
  11. package/dist/commands/install.js +376 -0
  12. package/dist/commands/pin.js +79 -0
  13. package/dist/commands/prepare.js +527 -0
  14. package/dist/commands/probe.js +392 -0
  15. package/dist/commands/track-latest.js +44 -0
  16. package/dist/commands/uninstall.js +308 -0
  17. package/dist/commands/unpin.js +95 -0
  18. package/dist/commands/update.js +138 -0
  19. package/dist/domain/fingerprint.js +4 -0
  20. package/dist/domain/refs.js +37 -0
  21. package/dist/effective-selection.js +123 -0
  22. package/dist/generated-plugin.js +748 -0
  23. package/dist/git.js +38 -0
  24. package/dist/hooks.js +290 -0
  25. package/dist/lifecycle.js +220 -0
  26. package/dist/manifest-overlay.js +125 -0
  27. package/dist/provenance.js +111 -0
  28. package/dist/python-json-format.js +83 -0
  29. package/dist/python-json.js +29 -0
  30. package/dist/python-text.js +74 -0
  31. package/dist/safe-path.js +137 -0
  32. package/dist/safety-error.js +10 -0
  33. package/dist/selection-state-cli.js +85 -0
  34. package/dist/selection-store.js +172 -0
  35. package/dist/selection.js +200 -0
  36. package/dist/status.js +24 -0
  37. package/dist/strict-json.js +248 -0
  38. package/dist/upstream-cli.js +110 -0
  39. package/dist/upstream-version.js +40 -0
  40. package/dist/upstream.js +253 -0
  41. package/dist/validate-generated-plugin-cli.js +118 -0
  42. package/dist/validator.js +317 -0
  43. package/dist/workspace.js +139 -0
  44. package/package.json +25 -2
  45. package/scripts/adapters/codex/adapter +0 -598
  46. package/scripts/adapters/codex/lib.sh +0 -391
  47. package/scripts/adapters/codex/validate-generated-plugin.py +0 -314
  48. package/scripts/core/adapter.sh +0 -92
  49. package/scripts/core/common.sh +0 -35
  50. package/scripts/core/lifecycle.sh +0 -127
  51. package/scripts/core/provenance.sh +0 -135
  52. package/scripts/core/status.sh +0 -27
  53. package/scripts/core/upstream.sh +0 -146
  54. package/scripts/core/validate-adapter-response.py +0 -245
  55. package/scripts/install +0 -60
  56. package/scripts/prepare +0 -126
  57. package/scripts/probe +0 -61
  58. package/scripts/uninstall +0 -44
  59. package/scripts/update +0 -31
@@ -0,0 +1,317 @@
1
+ import { spawn } from "node:child_process";
2
+ import { lstat, realpath, stat } from "node:fs/promises";
3
+ import { sep } from "node:path";
4
+ import { hasTerminalControl, pythonUnicodeEscapeBytes, } from "./adapter-result.js";
5
+ // The legacy SUPERPOWERS_VALIDATOR path. Its behaviour is frozen by parity, so it
6
+ // carries no limits at all; see the spec's D2. Bounding it is PR 12's row.
7
+ export const UNBOUNDED_LEGACY = { kind: "unbounded" };
8
+ // SUPERPOWERS_VALIDATOR_EXECUTABLE. 30s is ~270x a measured realistic validator run
9
+ // (0.11s, dominated by interpreter startup); 64 KiB per stream clears a
10
+ // one-diagnostic-line-per-file report (30 KB on a real tree) with headroom.
11
+ export const BOUNDED_EXECUTABLE = {
12
+ kind: "bounded",
13
+ timeoutMs: 30_000,
14
+ graceMs: 2_000,
15
+ drainMs: 200,
16
+ maxBytesPerStream: 64 * 1024,
17
+ };
18
+ // Retains the first `limit` bytes and counts the rest. A null limit retains
19
+ // everything, which is the unbounded policy's contract. push() always consumes the
20
+ // whole chunk: ceasing to consume would block the child on a full pipe.
21
+ class Sink {
22
+ limit;
23
+ chunks = [];
24
+ kept = 0;
25
+ dropped = 0;
26
+ constructor(limit) {
27
+ this.limit = limit;
28
+ }
29
+ push(chunk) {
30
+ if (this.limit === null) {
31
+ this.chunks.push(chunk);
32
+ return;
33
+ }
34
+ const room = this.limit - this.kept;
35
+ if (room <= 0) {
36
+ this.dropped += chunk.length;
37
+ return;
38
+ }
39
+ const take = chunk.length <= room ? chunk : chunk.subarray(0, room);
40
+ this.chunks.push(take);
41
+ this.kept += take.length;
42
+ this.dropped += chunk.length - take.length;
43
+ }
44
+ done() {
45
+ // Buffer.toString, NOT TextDecoder: TextDecoder strips a leading BOM and the
46
+ // legacy path's current string accumulator preserves it. Measured:
47
+ // decode(<BOM>hi) is "hi" but toString("utf8") is "\ufeffhi". The legacy path
48
+ // must be byte-identical, so BOTH paths use toString.
49
+ return {
50
+ text: Buffer.concat(this.chunks).toString("utf8"),
51
+ droppedBytes: this.dropped,
52
+ };
53
+ }
54
+ }
55
+ // Signal the child's whole process group. Only meaningful for a child spawned
56
+ // under the BOUNDED policy: that spawn passes `detached: true`, so the child
57
+ // leads a group of its own and -pid reaches its descendants. The LEGACY path
58
+ // spawns without `detached`, so its child does not lead a group and this
59
+ // function is not for it. Wrapped because the group is already gone in the
60
+ // ordinary race between SIGTERM and SIGKILL.
61
+ function signalGroup(child, signal) {
62
+ if (child.pid === undefined)
63
+ return;
64
+ try {
65
+ process.kill(-child.pid, signal);
66
+ }
67
+ catch {
68
+ /* the group exited between the timer firing and this call */
69
+ }
70
+ }
71
+ export function runValidator(argv, policy, env, workspace) {
72
+ return new Promise((resolveRun) => {
73
+ const [command, ...args] = argv;
74
+ const limit = policy.kind === "bounded" ? policy.maxBytesPerStream : null;
75
+ const out = new Sink(limit);
76
+ const err = new Sink(limit);
77
+ let settled = false;
78
+ // Settlement and termination are independent BY CONSTRUCTION, which is why
79
+ // there is no grace-timer handle to clear here: the timedOut settle is nested
80
+ // inside the SIGKILL callback, so on the timeout path the promise cannot
81
+ // resolve before the escalation has run. That nesting is the fix for an
82
+ // earlier revision which settled first and left the SIGKILL on a cancellable
83
+ // timer -- the historical shape the `survivor.sh` case in the unit suite
84
+ // exists to catch. Only the timeout timer is cleared here, and that clear is
85
+ // load-bearing rather than belt-and-braces: on a clean exit the `exit` handler
86
+ // has already disarmed it, and on the timeout path it has already fired -- but
87
+ // on the BOUNDED LAUNCH-FAILURE path neither happens. Node emits no `exit`
88
+ // event for an asynchronously failing spawn (an ENOENT spawn yields `error`
89
+ // then `close` with -2 and nothing else), so this is the only thing that
90
+ // disarms the timer for it.
91
+ const settle = (run) => {
92
+ if (settled)
93
+ return;
94
+ settled = true;
95
+ if (timeoutTimer !== undefined)
96
+ clearTimeout(timeoutTimer);
97
+ resolveRun(run);
98
+ };
99
+ let timeoutTimer;
100
+ let timedOut = false;
101
+ let lastCode = null;
102
+ // No timer is unref'ed. The escalation must actually run, and an unref'ed timer
103
+ // does not keep the event loop alive.
104
+ // On darwin, ENOEXEC is delivered as a SYNCHRONOUS throw here rather than on
105
+ // the `error` event, so a handler-only implementation would miss it. On Linux
106
+ // this branch is not reached for ENOEXEC at all: POSIX execvp falls back to
107
+ // /bin/sh on ENOEXEC, so the spawn itself succeeds. Because the throw happens
108
+ // inside this Promise executor, it rejects runValidator's returned promise --
109
+ // not an unhandled rejection -- which is what this try/catch prevents.
110
+ let child;
111
+ try {
112
+ child = spawn(command, args, {
113
+ env: { ...process.env, ...env, TMPDIR: workspace },
114
+ stdio: ["ignore", "pipe", "pipe"],
115
+ // D4: under the BOUNDED policy only, the child leads its own process group
116
+ // so a timeout can signal the GROUP -- signalling the process alone leaves
117
+ // descendants holding the inherited pipes, and `close` then waits for them
118
+ // (measured at 5279 ms against a 300 ms timeout). The LEGACY path must not
119
+ // acquire group leadership: that changes process and session semantics for
120
+ // a path whose whole contract is that it is unchanged.
121
+ detached: policy.kind === "bounded",
122
+ });
123
+ }
124
+ catch (cause) {
125
+ settle({
126
+ kind: "launchFailed",
127
+ errno: cause.code ?? "UNKNOWN",
128
+ cause,
129
+ });
130
+ return;
131
+ }
132
+ // No setEncoding: the cap counts BYTES, and a string accumulator would count
133
+ // UTF-16 code units instead.
134
+ child.stdout.on("data", (chunk) => out.push(chunk));
135
+ child.stderr.on("data", (chunk) => err.push(chunk));
136
+ // Both error and close fire on a launch failure, so settle() is idempotent.
137
+ child.on("error", (cause) => {
138
+ settle({ kind: "launchFailed", errno: cause.code ?? "UNKNOWN", cause });
139
+ });
140
+ child.on("close", (code) => {
141
+ // On the timeout path, settling here would resolve BEFORE SIGKILL and the
142
+ // escalation would never complete. That path settles itself, after the kill.
143
+ if (timedOut)
144
+ return;
145
+ settle({ kind: "exited", code, stdout: out.done(), stderr: err.done() });
146
+ });
147
+ // Settling on `close` alone is not a wall-clock bound: `close` fires only when
148
+ // every writer has released the pipes, and a descendant that outlives the child
149
+ // holds them. Settle on whichever comes first -- `close`, or the child's `exit`
150
+ // plus a bounded drain for trailing output.
151
+ child.on("exit", (code) => {
152
+ // `exit` carries the status and fires as soon as the CHILD is gone; `close`
153
+ // may lag it indefinitely. Capture the code here or a successful validator
154
+ // whose descendant lingers would settle with a null code and be rejected.
155
+ lastCode = code;
156
+ // BOUNDED ONLY. The legacy path settles on `close`, exactly as it does today:
157
+ // a validator that backgrounds `(sleep 0.6; echo late) &` has "late" captured
158
+ // under `close` and lost under a 200 ms drain, and that output is captured
159
+ // today.
160
+ if (policy.kind !== "bounded")
161
+ return;
162
+ // The TIMEOUT path owns its own settlement, after the escalation has run.
163
+ if (timedOut)
164
+ return;
165
+ // The drain settle below is now the one that will resolve this run, so the
166
+ // timeout must not fire behind it. A child exiting inside
167
+ // [timeoutMs - drainMs, timeoutMs) already has its drain settle queued when
168
+ // the timeout comes due; without this the timeout would SIGTERM the group of
169
+ // a validator that had ALREADY exited cleanly, then leave the SIGKILL
170
+ // escalation pending behind a settle that reports `exited`. Race-free rather
171
+ // than merely unlikely: either the timeout has fired, in which case
172
+ // `timedOut` is true and we returned above, or it has not, and now it never
173
+ // will. The bound is unchanged -- settlement is exitTime + drainMs and
174
+ // exitTime < timeoutMs. Deliberately NOT paired with a `timedOut` re-check
175
+ // inside the drain callback: with the timer cleared that check is
176
+ // unreachable, and unfalsifiable defensive code is what this module keeps
177
+ // being bitten by.
178
+ clearTimeout(timeoutTimer);
179
+ setTimeout(() => {
180
+ settle({
181
+ kind: "exited",
182
+ code: lastCode,
183
+ stdout: out.done(),
184
+ stderr: err.done(),
185
+ });
186
+ }, policy.drainMs);
187
+ });
188
+ if (policy.kind === "bounded") {
189
+ timeoutTimer = setTimeout(() => {
190
+ timedOut = true;
191
+ // Signal the GROUP, not the process: descendants hold the pipes. SIGTERM
192
+ // first, because the contract promises stderr carries the reason, and
193
+ // reading continues through the grace window so the child can print it.
194
+ signalGroup(child, "SIGTERM");
195
+ setTimeout(() => {
196
+ signalGroup(child, "SIGKILL");
197
+ // Settle ONLY after SIGKILL has been sent. An earlier revision settled
198
+ // first and left the escalation on an unref'ed timer, which never fires
199
+ // because the CLI exits as soon as its handler returns -- measured, the
200
+ // manager exited at 146 ms and the descendant survived. The wall-clock
201
+ // bound is therefore timeout + grace + drain, which is still a bound.
202
+ setTimeout(() => {
203
+ settle({
204
+ kind: "timedOut",
205
+ afterMs: policy.timeoutMs,
206
+ stdout: out.done(),
207
+ stderr: err.done(),
208
+ });
209
+ }, policy.drainMs);
210
+ }, policy.graceMs);
211
+ }, policy.timeoutMs);
212
+ }
213
+ });
214
+ }
215
+ // Every probe below is best-effort BY DESIGN: spawn is the authority on whether the
216
+ // path is runnable, and this result exists only for disclosure and for
217
+ // disambiguating EACCES between "a directory" and "not executable". A failed probe
218
+ // therefore degrades the MESSAGE, never the verdict — no probe result can admit a
219
+ // validator that spawn would have rejected.
220
+ export async function resolveValidator(configured) {
221
+ // Every probe is skipped for a bare name. lstat and stat are CWD-relative, so on
222
+ // a bare name they would describe a file in the current directory while spawn
223
+ // runs whatever PATH selects -- the same divergence that makes realpath unsafe
224
+ // here.
225
+ const pathLike = configured.includes(sep);
226
+ let isSymlink = false;
227
+ if (pathLike) {
228
+ try {
229
+ isSymlink = (await lstat(configured)).isSymbolicLink();
230
+ }
231
+ catch {
232
+ /* spawn reports the real reason */
233
+ }
234
+ }
235
+ // Resolve ONLY when the configured value contains a path separator -- which is
236
+ // exactly when spawn bypasses PATH and the two agree about the target. For a
237
+ // bare name, realpath resolves relative to the CWD while spawn searches PATH:
238
+ // measured from a directory containing `node`, realpath("node") gave one
239
+ // interpreter and spawn("node") ran another. Resolving a bare name would let the
240
+ // manager confidently disclose a file it is not about to run.
241
+ let resolved = null;
242
+ if (configured.includes(sep)) {
243
+ try {
244
+ resolved = await realpath(configured);
245
+ }
246
+ catch {
247
+ /* unresolvable; stays null and is never claimed */
248
+ }
249
+ }
250
+ let isDirectory = false;
251
+ let exists = false;
252
+ if (pathLike) {
253
+ try {
254
+ const s = await stat(configured);
255
+ isDirectory = s.isDirectory();
256
+ exists = true;
257
+ }
258
+ catch {
259
+ /* spawn reports the real reason */
260
+ }
261
+ }
262
+ return { configured, resolved, isSymlink, isDirectory, exists };
263
+ }
264
+ // Escape only when there is something to escape. pythonUnicodeEscape retains only
265
+ // printable ASCII, so escaping unconditionally would render a legitimate non-ASCII
266
+ // path unrecognisable to the operator who configured it. This is a legibility
267
+ // measure, not a security control.
268
+ export function displayPath(value) {
269
+ return hasTerminalControl(value)
270
+ ? pythonUnicodeEscapeBytes(Buffer.from(value, "utf8"))
271
+ : value;
272
+ }
273
+ export function launchFailureMessage(errno, resolution) {
274
+ const shown = displayPath(resolution.configured);
275
+ if (errno === "ENOENT" && resolution.exists) {
276
+ // The validator IS there; spawn's ENOENT came from its interpreter line.
277
+ // Measured: an executable with `#!/definitely/missing` raises ENOENT, and
278
+ // reporting "validator not found" would send the operator looking for a file
279
+ // that is present.
280
+ return `external plugin validator is present but its interpreter is missing: ${shown}`;
281
+ }
282
+ if (errno === "ENOENT")
283
+ return `external plugin validator not found: ${shown}`;
284
+ if (errno === "EACCES" && resolution.isDirectory)
285
+ return `external plugin validator is a directory: ${shown}`;
286
+ if (errno === "EACCES")
287
+ return `external plugin validator is not executable: ${shown}`;
288
+ if (errno === "ENOEXEC")
289
+ return `external plugin validator is not a runnable program: ${shown}`;
290
+ return `cannot execute external plugin validator: ${shown} (${errno})`;
291
+ }
292
+ // Only the commands that would actually run a validator. probe stays read-only, and
293
+ // a validator misconfiguration is none of its business.
294
+ const VALIDATOR_COMMANDS = new Set([
295
+ "prepare",
296
+ "install",
297
+ "update",
298
+ ]);
299
+ // Legacy emptiness semantics exactly: empty string means unset, whitespace-only
300
+ // counts as set. No trimming — a trim would silently change legacy behaviour.
301
+ export function bothConfigured(env) {
302
+ return ((env.SUPERPOWERS_VALIDATOR || "").length > 0 &&
303
+ (env.SUPERPOWERS_VALIDATOR_EXECUTABLE || "").length > 0);
304
+ }
305
+ // The second exported accessor preflight derives its enforcement from. It is an
306
+ // accessor rather than an inline check because an accessor that under-reports what
307
+ // preflight enforces is the blind spot slice 2 closed.
308
+ export function configurationErrors(cmd, env) {
309
+ if (!VALIDATOR_COMMANDS.has(cmd))
310
+ return [];
311
+ if (!bothConfigured(env))
312
+ return [];
313
+ return [
314
+ "SUPERPOWERS_VALIDATOR and SUPERPOWERS_VALIDATOR_EXECUTABLE are both set — " +
315
+ "unset one; the manager never chooses between them silently",
316
+ ];
317
+ }
@@ -0,0 +1,139 @@
1
+ import { rmSync } from "node:fs";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { SafetyError } from "./safety-error.js";
5
+ const MANAGED_SIGNALS = ["SIGHUP", "SIGINT", "SIGTERM"];
6
+ // Each active workspace carries its caller's failure reporter. The signal
7
+ // path still invokes it, for parity with the normal path, but a signal death
8
+ // never builds a result outcome (see src/adapter-result.ts), so a report
9
+ // that only lands in the adapter's buffered log would never become
10
+ // observable — the signal path therefore also writes the hand-written
11
+ // diagnostic straight to process.stderr, unconditionally; that raw write is
12
+ // the only thing actually visible to a caller. Previously the signal path
13
+ // called the module-private cleanup and swallowed the rejection in
14
+ // Promise.allSettled, so a signal-time failure was silent entirely — this is
15
+ // the state PR 11.4 removed from the normal path.
16
+ const active = new Map();
17
+ let exiting = false;
18
+ const handlers = new Map();
19
+ // One phrasing for one condition. The adapter re-emits this for a suppressed
20
+ // failure, so a change here must not leave two wordings for the same event.
21
+ export function workspaceRemovalFailure(path) {
22
+ return `cannot remove workspace ${path}`;
23
+ }
24
+ async function cleanup(path) {
25
+ try {
26
+ await rm(path, { recursive: true, force: true });
27
+ }
28
+ catch (cause) {
29
+ throw new SafetyError("workspace", workspaceRemovalFailure(path), {
30
+ cause,
31
+ });
32
+ }
33
+ }
34
+ // Synchronous by contract. An `await` here would yield to the event loop with
35
+ // the listeners still registered, and every signal arriving during that window
36
+ // would be consumed and discarded — an uninterruptible process, which is worse
37
+ // than the leak the handler exists to prevent. Being synchronous means the
38
+ // handler cannot be re-entered, so the window does not exist.
39
+ function cleanupForSignal(signal) {
40
+ if (exiting)
41
+ return;
42
+ exiting = true;
43
+ for (const [path, report] of active) {
44
+ try {
45
+ rmSync(path, { recursive: true, force: true });
46
+ }
47
+ catch {
48
+ // The reporter is invoked for parity with the normal path, but nothing
49
+ // ever reads it back here — no outcome is built before the process
50
+ // dies — so the diagnostic is also written straight to stderr,
51
+ // unconditionally; that write is the only thing actually observable.
52
+ // The diagnostic is hand-written and names the workspace; the caught
53
+ // cause is not interpolated.
54
+ //
55
+ // Reporting is guarded: if either call throws — plausibly EPIPE after
56
+ // SIGHUP, when the process on the other end of the pipe is already
57
+ // gone — the exception must not escape this loop, or the remaining
58
+ // workspaces' cleanup, deregistration, and the re-raise below never run.
59
+ try {
60
+ if (report)
61
+ report(path);
62
+ // Node documents pipe writes as asynchronous on some platforms
63
+ // (notably macOS); a saturated pipe could drop this write. Small
64
+ // buffers like this one are written inline, which is why the
65
+ // covering test observes it reliably.
66
+ process.stderr.write(`${workspaceRemovalFailure(path)}\n`);
67
+ }
68
+ catch {
69
+ // See above: a reporting failure must not block cleanup/re-raise.
70
+ }
71
+ }
72
+ }
73
+ active.clear();
74
+ // Remove only the listeners this coordinator registered, then re-raise so the
75
+ // default disposition terminates the process by the signal.
76
+ for (const [managed, handler] of handlers)
77
+ process.off(managed, handler);
78
+ handlers.clear();
79
+ process.kill(process.pid, signal);
80
+ }
81
+ function registerCoordinator() {
82
+ if (handlers.size > 0)
83
+ return;
84
+ for (const signal of MANAGED_SIGNALS) {
85
+ const handler = () => {
86
+ cleanupForSignal(signal);
87
+ };
88
+ handlers.set(signal, handler);
89
+ process.on(signal, handler);
90
+ }
91
+ }
92
+ function deregisterCoordinator() {
93
+ if (active.size > 0)
94
+ return;
95
+ for (const [signal, handler] of handlers)
96
+ process.off(signal, handler);
97
+ handlers.clear();
98
+ }
99
+ export async function withWorkspace(parent, prefix, fn, options = {}) {
100
+ const remove = options.cleanup ?? cleanup;
101
+ let workspace;
102
+ try {
103
+ workspace = await mkdtemp(join(parent, prefix));
104
+ }
105
+ catch (cause) {
106
+ throw new SafetyError("workspace", "cannot create workspace", { cause });
107
+ }
108
+ active.set(workspace, options.onCleanupFailure);
109
+ registerCoordinator();
110
+ try {
111
+ let result;
112
+ let failed = false;
113
+ let callbackError;
114
+ try {
115
+ result = await fn(workspace);
116
+ }
117
+ catch (error) {
118
+ failed = true;
119
+ callbackError = error;
120
+ }
121
+ try {
122
+ await remove(workspace);
123
+ }
124
+ catch (cleanupError) {
125
+ if (failed)
126
+ throw callbackError;
127
+ if (options.onCleanupFailure === undefined)
128
+ throw cleanupError;
129
+ options.onCleanupFailure(workspace);
130
+ }
131
+ if (failed)
132
+ throw callbackError;
133
+ return result;
134
+ }
135
+ finally {
136
+ active.delete(workspace);
137
+ deregisterCoordinator();
138
+ }
139
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superpowers-manager",
3
- "version": "0.1.6",
3
+ "version": "0.3.0",
4
4
  "description": "Install and update the latest stable obra/superpowers release directly from upstream, without waiting for agent marketplaces to catch up. Codex supported today.",
5
5
  "keywords": [
6
6
  "superpowers",
@@ -18,6 +18,28 @@
18
18
  ],
19
19
  "license": "MIT",
20
20
  "type": "module",
21
+ "packageManager": "pnpm@11.22.0",
22
+ "scripts": {
23
+ "prebuild": "node --input-type=module -e \"import{rmSync}from'node:fs';rmSync('dist',{recursive:true,force:true})\"",
24
+ "build": "tsc -p tsconfig.json",
25
+ "postbuild": "node tests/write-build-id.js",
26
+ "prepack": "node --input-type=module -e \"import{existsSync}from'node:fs';if(!existsSync('dist/cli.js')){console.error('error: dist/cli.js is missing — run pnpm install --frozen-lockfile && pnpm run build');process.exit(1)}\"",
27
+ "lint": "oxlint src/ bin/ tests/bin/ tests/unit/ tests/baseline/ tests/lib/ tests/tools/ tests/run-node-suites.js tests/build-id.js tests/write-build-id.js tests/assert-matcher-gate.js",
28
+ "format": "prettier --write src/ bin/ tests/bin/ tests/unit/ tests/baseline/ tests/lib/ tests/tools/ tests/run-node-suites.js tests/build-id.js tests/write-build-id.js tests/assert-matcher-gate.js",
29
+ "format:check": "prettier --check src/ bin/ tests/bin/ tests/unit/ tests/baseline/ tests/lib/ tests/tools/ tests/run-node-suites.js tests/build-id.js tests/write-build-id.js tests/assert-matcher-gate.js",
30
+ "typecheck:js": "tsc -p tests/tsconfig.json",
31
+ "test": "node tests/run-node-suites.js",
32
+ "check:static": "pnpm run format:check && pnpm run lint && pnpm run build && pnpm run typecheck:js",
33
+ "check": "pnpm run check:static && pnpm run test"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^24.13.3",
37
+ "oxlint": "^1.75.0",
38
+ "oxlint-tsgolint": "7.0.2001",
39
+ "prettier": "^3.9.6",
40
+ "typescript": "~7.0.2",
41
+ "yaml": "^2.9.0"
42
+ },
21
43
  "repository": {
22
44
  "type": "git",
23
45
  "url": "git+https://github.com/j7an/superpowers-manager.git"
@@ -34,7 +56,8 @@
34
56
  },
35
57
  "files": [
36
58
  "bin/",
37
- "scripts/",
59
+ "dist/",
60
+ "!dist/.build-id",
38
61
  "config/upstream-ref",
39
62
  ".agents/plugins/marketplace.json",
40
63
  "plugins/superpowers/.codex-plugin/plugin.template.json",