acuvo-code 0.2.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 (154) hide show
  1. package/CHANGELOG.md +328 -0
  2. package/ENTERPRISE.md +927 -0
  3. package/LICENSE +120 -0
  4. package/README.md +1245 -0
  5. package/ROADMAP.md +556 -0
  6. package/bin/acuvo-mcp.mjs +208 -0
  7. package/bin/acuvo.mjs +3108 -0
  8. package/lib/acceptance-consent.mjs +168 -0
  9. package/lib/acceptance.mjs +859 -0
  10. package/lib/account.mjs +226 -0
  11. package/lib/acuvo-dir.mjs +72 -0
  12. package/lib/acuvo-models.mjs +141 -0
  13. package/lib/apply-patch.mjs +570 -0
  14. package/lib/ask-user.mjs +173 -0
  15. package/lib/audit.mjs +530 -0
  16. package/lib/auto-lease.mjs +174 -0
  17. package/lib/background.mjs +842 -0
  18. package/lib/best-of.mjs +334 -0
  19. package/lib/board.mjs +232 -0
  20. package/lib/breaker.mjs +93 -0
  21. package/lib/budget.mjs +1375 -0
  22. package/lib/builtin-skills.mjs +135 -0
  23. package/lib/cache-floor.mjs +204 -0
  24. package/lib/chain.mjs +303 -0
  25. package/lib/changed-paths.mjs +84 -0
  26. package/lib/chat.mjs +434 -0
  27. package/lib/checkpoint.mjs +637 -0
  28. package/lib/child-lifetime.mjs +71 -0
  29. package/lib/cli-args.mjs +1255 -0
  30. package/lib/code-review.mjs +1382 -0
  31. package/lib/colour.mjs +82 -0
  32. package/lib/command.mjs +2847 -0
  33. package/lib/compact.mjs +1151 -0
  34. package/lib/completion.mjs +515 -0
  35. package/lib/creative-engines.mjs +779 -0
  36. package/lib/db-inspect.mjs +1624 -0
  37. package/lib/delete.mjs +111 -0
  38. package/lib/design-loop.mjs +570 -0
  39. package/lib/diff-preview.mjs +1044 -0
  40. package/lib/doctor.mjs +2139 -0
  41. package/lib/dropped.mjs +216 -0
  42. package/lib/edit-diagnostics.mjs +277 -0
  43. package/lib/edit.mjs +460 -0
  44. package/lib/env-file.mjs +250 -0
  45. package/lib/escalate.mjs +702 -0
  46. package/lib/evaluate.mjs +284 -0
  47. package/lib/fetch-text.mjs +952 -0
  48. package/lib/fleet-budget.mjs +256 -0
  49. package/lib/gh.mjs +1536 -0
  50. package/lib/git.mjs +1341 -0
  51. package/lib/github.mjs +261 -0
  52. package/lib/h2.mjs +194 -0
  53. package/lib/handoff.mjs +417 -0
  54. package/lib/hooks.mjs +626 -0
  55. package/lib/http-probe.mjs +907 -0
  56. package/lib/image-director.mjs +322 -0
  57. package/lib/image-edit.mjs +522 -0
  58. package/lib/imagegen.mjs +998 -0
  59. package/lib/interrupt.mjs +234 -0
  60. package/lib/learned.mjs +353 -0
  61. package/lib/lease-watch.mjs +115 -0
  62. package/lib/lease.mjs +868 -0
  63. package/lib/localize.mjs +834 -0
  64. package/lib/log-tail.mjs +1052 -0
  65. package/lib/login.mjs +157 -0
  66. package/lib/lsp.mjs +1613 -0
  67. package/lib/mcp-consent.mjs +377 -0
  68. package/lib/mcp-defaults.mjs +780 -0
  69. package/lib/mcp-server.mjs +1343 -0
  70. package/lib/mcp.mjs +1263 -0
  71. package/lib/media.mjs +1283 -0
  72. package/lib/memory-workspace.mjs +179 -0
  73. package/lib/model-json.mjs +132 -0
  74. package/lib/model-tier.mjs +171 -0
  75. package/lib/model.mjs +1445 -0
  76. package/lib/parallel.mjs +144 -0
  77. package/lib/perchance.mjs +210 -0
  78. package/lib/plan-coherence.mjs +1461 -0
  79. package/lib/plan-ledger.mjs +981 -0
  80. package/lib/plan.mjs +461 -0
  81. package/lib/policy.mjs +783 -0
  82. package/lib/prefix-order.mjs +38 -0
  83. package/lib/project-memory.mjs +127 -0
  84. package/lib/prompt.mjs +109 -0
  85. package/lib/python.mjs +862 -0
  86. package/lib/rcfile.mjs +853 -0
  87. package/lib/read-window.mjs +743 -0
  88. package/lib/refute-tools.mjs +34 -0
  89. package/lib/refute.mjs +806 -0
  90. package/lib/repl-driver.mjs +264 -0
  91. package/lib/repl.mjs +324 -0
  92. package/lib/replay.mjs +1218 -0
  93. package/lib/repo-map.mjs +1101 -0
  94. package/lib/report.mjs +419 -0
  95. package/lib/search-rank.mjs +99 -0
  96. package/lib/search.mjs +659 -0
  97. package/lib/secret-paths.mjs +54 -0
  98. package/lib/session.mjs +1017 -0
  99. package/lib/skills.mjs +703 -0
  100. package/lib/slash.mjs +356 -0
  101. package/lib/spawn-argv.mjs +1151 -0
  102. package/lib/spend.mjs +250 -0
  103. package/lib/steer.mjs +280 -0
  104. package/lib/stream.mjs +253 -0
  105. package/lib/stuck.mjs +712 -0
  106. package/lib/subagent.mjs +749 -0
  107. package/lib/terminal-graphics.mjs +171 -0
  108. package/lib/tool-prefix.mjs +226 -0
  109. package/lib/tool-shortlist.mjs +162 -0
  110. package/lib/tools.mjs +2333 -0
  111. package/lib/tsserver.mjs +423 -0
  112. package/lib/turn.mjs +5672 -0
  113. package/lib/untrusted-block.mjs +271 -0
  114. package/lib/verify-claim.mjs +299 -0
  115. package/lib/vision.mjs +330 -0
  116. package/lib/voice-task.mjs +561 -0
  117. package/lib/warm-provider.mjs +255 -0
  118. package/lib/websearch.mjs +401 -0
  119. package/lib/workspace.mjs +928 -0
  120. package/lib/write-approval.mjs +235 -0
  121. package/lib/write-many.mjs +162 -0
  122. package/package.json +62 -0
  123. package/scripts/bundle.mjs +768 -0
  124. package/scripts/cache-floor.mjs +176 -0
  125. package/scripts/machine.mjs +226 -0
  126. package/scripts/test.mjs +139 -0
  127. package/skills/accessibility.md +87 -0
  128. package/skills/acuvo-design-system.md +123 -0
  129. package/skills/animation.md +84 -0
  130. package/skills/api-design.md +82 -0
  131. package/skills/auth-and-sessions.md +78 -0
  132. package/skills/build-with-a-framework.md +101 -0
  133. package/skills/colour-and-contrast.md +112 -0
  134. package/skills/creative-engines.md +81 -0
  135. package/skills/css-layout.md +85 -0
  136. package/skills/data-and-charts.md +77 -0
  137. package/skills/debugging.md +76 -0
  138. package/skills/designing-by-looking.md +84 -0
  139. package/skills/error-handling.md +78 -0
  140. package/skills/forms-and-validation.md +93 -0
  141. package/skills/nextjs-app-router.md +75 -0
  142. package/skills/page-composition.md +103 -0
  143. package/skills/performance.md +77 -0
  144. package/skills/plan-before-building.md +52 -0
  145. package/skills/planning-and-delegating.md +72 -0
  146. package/skills/refactoring.md +70 -0
  147. package/skills/security-basics.md +76 -0
  148. package/skills/state-management.md +73 -0
  149. package/skills/supabase-multitenant.md +72 -0
  150. package/skills/typescript-strict.md +90 -0
  151. package/skills/typography.md +135 -0
  152. package/skills/verify-your-own-work.md +62 -0
  153. package/skills/web-app-quality.md +62 -0
  154. package/skills/working-in-the-background.md +64 -0
@@ -0,0 +1,768 @@
1
+ /**
2
+ * ── THE BUNDLER ─────────────────────────────────────────────────────────────
3
+ *
4
+ * One file in, one file out: `bin/acuvo.mjs` and everything it reaches become a
5
+ * single `.mjs` a stranger can curl and run. Zero dependencies, because "zero
6
+ * dependencies" is the package's headline property and a build tool that pulls
7
+ * in half of npm to prove it would be a joke.
8
+ *
9
+ * ⚠️ A BUNDLE THAT IMPORTS CLEANLY AND MISBEHAVES AT RUNTIME IS WORSE THAN NO
10
+ * BUNDLE. It installs in one command and then lies in a way nobody can diff. So
11
+ * this file is built around the four things that silently produce exactly that:
12
+ *
13
+ * 1. `import` / `export` text that is NOT code. `lib/session.mjs` holds a
14
+ * template literal whose body contains `import { saveSession } from
15
+ * './session.mjs';` at column 0 — a line-based parser reports a phantom
16
+ * self-cycle AND rewrites the inside of a string constant that the CLI
17
+ * prints to users. Hence `maskNonCode`: everything that is a comment, a
18
+ * string, a template chunk or a regex body becomes spaces, and it preserves
19
+ * LENGTH and NEWLINES exactly so every offset and line number still points
20
+ * where it did. Every scan below reads the mask and slices the original.
21
+ * 2. `import.meta.url`. Five sites use it to find the package root or read
22
+ * package.json. In one flat file there is no package.json one directory up
23
+ * and `--version` has no try/catch, so the naive bundle dies on the first
24
+ * command the README tells a stranger to run. Asset references are inlined
25
+ * and materialised into a temp dir on demand; bare ones are rewritten to a
26
+ * URL under the bundle's own directory.
27
+ * 3. builtin imports colliding once every module shares one scope — so each
28
+ * builtin is hoisted ONCE as a namespace (`builtinSlug`) and every module
29
+ * destructures from it.
30
+ * 4. the shebang: once, at byte 0, nowhere else. A module's own shebang has
31
+ * its `#!` turned into `//`, which keeps the byte count and kills the
32
+ * syntax error.
33
+ *
34
+ * ── HOW MODULES ARE JOINED ──────────────────────────────────────────────────
35
+ * Each module becomes `__acuvo_modules['lib/x.mjs'] = (() => { …body…; return
36
+ * { …exports… }; })();` in dependency order. Per-module scope means NOTHING has
37
+ * to be renamed — the hardest and most bug-prone half of a bundler simply does
38
+ * not happen. Imports become destructuring from the registry. A module that
39
+ * contains `await` gets an async wrapper and an awaited call, so top-level await
40
+ * survives. Circular imports are refused by name rather than silently emitting a
41
+ * file where one half of the cycle is undefined.
42
+ *
43
+ * ── WHAT IT WILL NOT DO ─────────────────────────────────────────────────────
44
+ * `export default`, `export *` and re-exports throw with the form named. They
45
+ * are absent from this package, and a bundler that half-supports a form is how
46
+ * you get a bundle that is one byte wrong.
47
+ *
48
+ * ⚠️ IT NEVER OPENS A FILE THAT COULD HOLD A SECRET. It reads exactly the
49
+ * modules in the import graph plus the assets those modules name. No .env, no
50
+ * environment, no glob. That is why the emitted file cannot leak a key, and
51
+ * `scanForSecrets` is the belt to that braces — the output is scanned before it
52
+ * is ever written.
53
+ */
54
+
55
+ import { readFileSync, writeFileSync, mkdirSync, chmodSync } from 'node:fs';
56
+ import { join, dirname, resolve, posix } from 'node:path';
57
+ import { fileURLToPath, pathToFileURL } from 'node:url';
58
+
59
+ /* ════════════════════════════════════════════════════════════════════════════
60
+ * 1. maskNonCode — the foundation everything else reads.
61
+ * ════════════════════════════════════════════════════════════════════════════ */
62
+
63
+ const IDENT = /[A-Za-z0-9_$]/;
64
+
65
+ /**
66
+ * Words after which a `/` opens a regular expression rather than dividing.
67
+ * `return /a"b/.test(x)` is the case that matters here: miss it and the quote
68
+ * inside the pattern is read as a string opener and the rest of the file is
69
+ * garbage.
70
+ */
71
+ const KEYWORD_BEFORE_REGEX = new Set([
72
+ 'return', 'typeof', 'instanceof', 'in', 'of', 'new', 'delete', 'void',
73
+ 'throw', 'case', 'do', 'else', 'yield', 'await',
74
+ ]);
75
+
76
+ /** Punctuation after which a `/` opens a regex. `)` and `]` are deliberately
77
+ * absent: `(x) / 2` and `arr[0] / 3` are division, and division is far commoner
78
+ * in this codebase than `if (x) /re/.test(y)`. */
79
+ const PUNCT_BEFORE_REGEX = '(,=:[!&|?{};+-*%~^<>';
80
+
81
+ function prevSignificant(out, from) {
82
+ let j = from;
83
+ while (j >= 0 && (out[j] === ' ' || out[j] === '\t' || out[j] === '\r' || out[j] === '\n')) j--;
84
+ return j;
85
+ }
86
+
87
+ function regexAllowed(out, at) {
88
+ const j = prevSignificant(out, at - 1);
89
+ if (j < 0) return true;
90
+ const c = out[j];
91
+ if (IDENT.test(c)) {
92
+ let k = j;
93
+ while (k >= 0 && IDENT.test(out[k])) k--;
94
+ return KEYWORD_BEFORE_REGEX.has(out.slice(k + 1, j + 1).join(''));
95
+ }
96
+ if (c === ')' || c === ']') return false;
97
+ return PUNCT_BEFORE_REGEX.includes(c);
98
+ }
99
+
100
+ /**
101
+ * Blank every byte that is not executable code, preserving length and newlines.
102
+ *
103
+ * ⭐ LENGTH AND NEWLINES ARE THE CONTRACT. Every index the parser finds in the
104
+ * mask is used to slice the ORIGINAL source, so a mask one byte short would
105
+ * rewrite the wrong span — which is precisely how a bundler mangles the inside
106
+ * of a string constant.
107
+ */
108
+ export function maskNonCode(src) {
109
+ const n = src.length;
110
+ const out = src.split('');
111
+ const blank = (a, b) => {
112
+ for (let i = a; i < b && i < n; i++) if (out[i] !== '\n') out[i] = ' ';
113
+ };
114
+
115
+ let i = 0;
116
+
117
+ // A shebang is not JavaScript. Treat it as a comment so nothing downstream
118
+ // trips over the `#`.
119
+ if (src[0] === '#' && src[1] === '!') {
120
+ let e = src.indexOf('\n');
121
+ if (e === -1) e = n;
122
+ blank(0, e);
123
+ i = e;
124
+ }
125
+
126
+ let mode = 'code';
127
+ let quote = '';
128
+ let braceDepth = 0;
129
+ const templateBrace = [];
130
+
131
+ while (i < n) {
132
+ const c = src[i];
133
+
134
+ if (mode === 'code') {
135
+ if (c === '/') {
136
+ const d = src[i + 1];
137
+ if (d === '/') {
138
+ let e = src.indexOf('\n', i);
139
+ if (e === -1) e = n;
140
+ blank(i, e);
141
+ i = e;
142
+ continue;
143
+ }
144
+ if (d === '*') {
145
+ const close = src.indexOf('*/', i + 2);
146
+ const e = close === -1 ? n : close + 2;
147
+ blank(i, e);
148
+ i = e;
149
+ continue;
150
+ }
151
+ if (regexAllowed(out, i)) {
152
+ let j = i + 1;
153
+ let inClass = false;
154
+ let ok = false;
155
+ while (j < n) {
156
+ const r = src[j];
157
+ if (r === '\\') { j += 2; continue; }
158
+ if (r === '\n') break;
159
+ if (inClass) { if (r === ']') inClass = false; j++; continue; }
160
+ if (r === '[') { inClass = true; j++; continue; }
161
+ if (r === '/') { ok = true; break; }
162
+ j++;
163
+ }
164
+ if (ok) {
165
+ blank(i + 1, j); // keep both `/` delimiters, kill the body
166
+ let k = j + 1;
167
+ while (k < n && /[a-z]/i.test(src[k])) k++;
168
+ i = k;
169
+ continue;
170
+ }
171
+ }
172
+ i++;
173
+ continue;
174
+ }
175
+ if (c === "'" || c === '"') { quote = c; mode = 'string'; i++; continue; }
176
+ if (c === '`') { mode = 'template'; i++; continue; }
177
+ if (c === '{') { braceDepth++; i++; continue; }
178
+ if (c === '}') {
179
+ if (templateBrace.length && braceDepth === templateBrace[templateBrace.length - 1]) {
180
+ templateBrace.pop();
181
+ mode = 'template';
182
+ i++;
183
+ continue;
184
+ }
185
+ braceDepth--;
186
+ i++;
187
+ continue;
188
+ }
189
+ i++;
190
+ continue;
191
+ }
192
+
193
+ if (mode === 'string') {
194
+ if (c === '\\') { blank(i, i + 2); i += 2; continue; }
195
+ if (c === quote) { mode = 'code'; i++; continue; }
196
+ if (c === '\n') { mode = 'code'; i++; continue; } // unterminated — bail, do not eat the file
197
+ blank(i, i + 1);
198
+ i++;
199
+ continue;
200
+ }
201
+
202
+ // template
203
+ if (c === '\\') { blank(i, i + 2); i += 2; continue; }
204
+ if (c === '`') { mode = 'code'; i++; continue; }
205
+ if (c === '$' && src[i + 1] === '{') {
206
+ templateBrace.push(braceDepth);
207
+ mode = 'code';
208
+ i += 2;
209
+ continue;
210
+ }
211
+ blank(i, i + 1);
212
+ i++;
213
+ }
214
+
215
+ return out.join('');
216
+ }
217
+
218
+ /* ════════════════════════════════════════════════════════════════════════════
219
+ * 2. parseModule
220
+ * ════════════════════════════════════════════════════════════════════════════ */
221
+
222
+ const NAME_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
223
+
224
+ function parseNamedList(text) {
225
+ const names = [];
226
+ for (const raw of text.split(',')) {
227
+ const part = raw.trim();
228
+ if (!part) continue;
229
+ const m = part.match(/^([A-Za-z_$][A-Za-z0-9_$]*)(?:\s+as\s+([A-Za-z_$][A-Za-z0-9_$]*))?$/);
230
+ if (!m) throw new Error(`unsupported import/export entry: ${JSON.stringify(part)}`);
231
+ names.push({ imported: m[1], local: m[2] ?? m[1] });
232
+ }
233
+ return names;
234
+ }
235
+
236
+ /** Find `from` as a word, at brace depth 0, starting at `from`. */
237
+ function findFromKeyword(masked, start, limit) {
238
+ let depth = 0;
239
+ for (let i = start; i < limit; i++) {
240
+ const c = masked[i];
241
+ if (c === '{') { depth++; continue; }
242
+ if (c === '}') { depth--; continue; }
243
+ if (depth !== 0) continue;
244
+ if (c !== 'f') continue;
245
+ if (masked.slice(i, i + 4) !== 'from') continue;
246
+ const before = i === 0 ? ' ' : masked[i - 1];
247
+ const after = masked[i + 4] ?? ' ';
248
+ if (IDENT.test(before) || IDENT.test(after)) continue;
249
+ return i;
250
+ }
251
+ return -1;
252
+ }
253
+
254
+ function skipSpace(text, i) {
255
+ while (i < text.length && /\s/.test(text[i])) i++;
256
+ return i;
257
+ }
258
+
259
+ /**
260
+ * Read a module's shape: what it imports, what it exports, the shebang, every
261
+ * `import.meta.url` site, and every dynamic `import()`.
262
+ *
263
+ * Everything is located in the MASK and sliced from the SOURCE, which is the
264
+ * only reason an `import` inside a template literal does not count.
265
+ */
266
+ export function parseModule(src) {
267
+ const masked = maskNonCode(src);
268
+
269
+ for (const [re, label] of [
270
+ [/(^|[\n;])[ \t]*export\s+default\b/, 'export default'],
271
+ [/(^|[\n;])[ \t]*export\s*\*/, 'export *'],
272
+ [/(^|[\n;])[ \t]*export\s*\{[^}]*\}\s*from\b/, 're-export (`export { … } from …`)'],
273
+ ]) {
274
+ if (re.test(masked)) {
275
+ throw new Error(
276
+ `${label} is not supported by this bundler — no module in this package uses it, ` +
277
+ `and a bundler that half-supports a form is how a bundle ends up one byte wrong.`,
278
+ );
279
+ }
280
+ }
281
+
282
+ const shebang = src.startsWith('#!')
283
+ ? src.slice(0, src.indexOf('\n') === -1 ? src.length : src.indexOf('\n'))
284
+ : null;
285
+
286
+ /* ── imports ─────────────────────────────────────────────────────────── */
287
+ const imports = [];
288
+ const importRe = /(^|[\n;])([ \t]*)import\b/g;
289
+ let m;
290
+ while ((m = importRe.exec(masked)) !== null) {
291
+ const start = m.index + m[1].length + m[2].length;
292
+ const afterKeyword = start + 'import'.length;
293
+ const next = masked[afterKeyword];
294
+ if (next === '(' || next === '.') continue; // dynamic import / import.meta
295
+ if (next !== undefined && !/[\s{*'"]/.test(next)) continue;
296
+
297
+ let p = skipSpace(masked, afterKeyword);
298
+ let names = [];
299
+ let namespace = null;
300
+ let defaultName = null;
301
+ let quoteAt;
302
+
303
+ if (masked[p] === "'" || masked[p] === '"') {
304
+ quoteAt = p; // side-effect import: `import './x.mjs';`
305
+ } else {
306
+ const fromAt = findFromKeyword(masked, p, masked.length);
307
+ if (fromAt === -1) continue; // not an import statement we can read
308
+ const clause = masked.slice(p, fromAt);
309
+ const brace = clause.match(/\{([\s\S]*)\}/);
310
+ if (brace) names = parseNamedList(brace[1]);
311
+ const ns = clause.match(/\*\s*as\s+([A-Za-z_$][A-Za-z0-9_$]*)/);
312
+ if (ns) namespace = ns[1];
313
+ const head = clause.split(/[{*]/)[0].trim().replace(/,$/, '').trim();
314
+ if (head && NAME_RE.test(head)) defaultName = head;
315
+ quoteAt = skipSpace(masked, fromAt + 4);
316
+ if (masked[quoteAt] !== "'" && masked[quoteAt] !== '"') continue;
317
+ }
318
+
319
+ const close = masked.indexOf(masked[quoteAt], quoteAt + 1);
320
+ if (close === -1) continue;
321
+ const specifier = src.slice(quoteAt + 1, close);
322
+
323
+ let end = close + 1;
324
+ while (end < masked.length && (masked[end] === ' ' || masked[end] === '\t')) end++;
325
+ end = masked[end] === ';' ? end + 1 : close + 1;
326
+
327
+ imports.push({ names, namespace, defaultName, specifier, start, end });
328
+ }
329
+
330
+ /* ── exports ─────────────────────────────────────────────────────────── */
331
+ const exports = [];
332
+ const declRe = /(^|[\n;])([ \t]*)export\s+(async\s+function\s*\*?|function\s*\*?|class|const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)/g;
333
+ while ((m = declRe.exec(masked)) !== null) {
334
+ const start = m.index + m[1].length + m[2].length;
335
+ const keywordAt = masked.indexOf(m[3].trimEnd()[0], start + 'export'.length);
336
+ exports.push({
337
+ kind: 'decl',
338
+ names: [m[4]],
339
+ locals: [m[4]],
340
+ start,
341
+ end: keywordAt, // [start, end) is `export ` — cut it and the declaration stands alone
342
+ });
343
+ }
344
+ const listRe = /(^|[\n;])([ \t]*)export\s*\{([^}]*)\}\s*;?/g;
345
+ while ((m = listRe.exec(masked)) !== null) {
346
+ const start = m.index + m[1].length + m[2].length;
347
+ const entries = parseNamedList(m[3]);
348
+ exports.push({
349
+ kind: 'list',
350
+ names: entries.map((e) => e.local),
351
+ locals: entries.map((e) => e.imported),
352
+ start,
353
+ end: m.index + m[0].length,
354
+ });
355
+ }
356
+ exports.sort((a, b) => a.start - b.start);
357
+
358
+ /* ── import.meta.url ─────────────────────────────────────────────────── */
359
+ const metaUrls = [];
360
+ const newUrlRe = /new\s+URL\s*\(\s*(['"])/g;
361
+ while ((m = newUrlRe.exec(masked)) !== null) {
362
+ const quoteAt = m.index + m[0].length - 1;
363
+ const close = masked.indexOf(m[1], quoteAt + 1);
364
+ if (close === -1) continue;
365
+ const tail = masked.slice(close + 1);
366
+ const t = tail.match(/^\s*,\s*import\.meta\.url\s*\)/);
367
+ if (!t) continue;
368
+ metaUrls.push({
369
+ kind: 'new-url',
370
+ specifier: src.slice(quoteAt + 1, close),
371
+ start: m.index,
372
+ end: close + 1 + t[0].length,
373
+ });
374
+ }
375
+ const bareRe = /import\.meta\.url/g;
376
+ while ((m = bareRe.exec(masked)) !== null) {
377
+ const inside = metaUrls.some((u) => m.index >= u.start && m.index < u.end);
378
+ if (inside) continue;
379
+ metaUrls.push({ kind: 'bare', specifier: null, start: m.index, end: m.index + m[0].length });
380
+ }
381
+ metaUrls.sort((a, b) => a.start - b.start);
382
+
383
+ /* ── dynamic import() ────────────────────────────────────────────────── */
384
+ const dynamicImports = [];
385
+ const dynRe = /\bimport\s*\(\s*(['"])/g;
386
+ while ((m = dynRe.exec(masked)) !== null) {
387
+ const quoteAt = m.index + m[0].length - 1;
388
+ const close = masked.indexOf(m[1], quoteAt + 1);
389
+ if (close === -1) continue;
390
+ const t = masked.slice(close + 1).match(/^\s*\)/);
391
+ if (!t) continue;
392
+ dynamicImports.push({
393
+ specifier: src.slice(quoteAt + 1, close),
394
+ start: m.index,
395
+ end: close + 1 + t[0].length,
396
+ });
397
+ }
398
+
399
+ return { shebang, imports, exports, metaUrls, dynamicImports, masked };
400
+ }
401
+
402
+ /* ════════════════════════════════════════════════════════════════════════════
403
+ * 3. the graph
404
+ * ════════════════════════════════════════════════════════════════════════════ */
405
+
406
+ /** `bin/acuvo.mjs` + `../lib/turn.mjs` → `lib/turn.mjs`. Posix ids throughout,
407
+ * so a bundle built on Windows is byte-identical to one built on Linux. */
408
+ export function resolveSpecifier(fromId, specifier) {
409
+ return posix.normalize(posix.join(posix.dirname(fromId), specifier));
410
+ }
411
+
412
+ const isRelative = (s) => s.startsWith('./') || s.startsWith('../');
413
+ const isBuiltin = (s) => s.startsWith('node:');
414
+
415
+ /** `node:dns/promises` → `__node_dns_promises`, and never the same as `node:dns`. */
416
+ export function builtinSlug(specifier) {
417
+ return `__node_${specifier.replace(/^node:/, '').replace(/[^A-Za-z0-9_$]/g, '_')}`;
418
+ }
419
+
420
+ /**
421
+ * Depth-first, dependencies before dependents.
422
+ *
423
+ * ⚠️ A DYNAMIC IMPORT IS NOT AN ORDERING EDGE. `lib/subagent.mjs` reaches
424
+ * `lib/turn.mjs` through `await import(…)` precisely because the static edge
425
+ * runs the other way; counting it would report a cycle in a tree that has none.
426
+ * The target still has to be IN the bundle, so it is walked as its own root.
427
+ */
428
+ export function buildGraph({ entry, readFile }) {
429
+ const modules = new Map();
430
+ const order = [];
431
+ const state = new Map(); // id -> 'open' | 'done'
432
+ const dynamicRoots = [];
433
+
434
+ const load = (id, importer) => {
435
+ if (modules.has(id)) return modules.get(id);
436
+ const src = readFile(id);
437
+ if (src === null || src === undefined) {
438
+ throw new Error(
439
+ importer
440
+ ? `cannot resolve '${id}' — imported by '${importer}'`
441
+ : `entry module not found: '${id}'`,
442
+ );
443
+ }
444
+ const parsed = parseModule(src);
445
+ const record = { id, src, parsed };
446
+ modules.set(id, record);
447
+ return record;
448
+ };
449
+
450
+ const visit = (id, importer, stack) => {
451
+ const status = state.get(id);
452
+ if (status === 'done') return;
453
+ if (status === 'open') {
454
+ const at = stack.indexOf(id);
455
+ const loop = [...stack.slice(at === -1 ? 0 : at), id].join(' -> ');
456
+ throw new Error(`circular import: ${loop}`);
457
+ }
458
+ const record = load(id, importer);
459
+ state.set(id, 'open');
460
+ stack.push(id);
461
+ for (const imp of record.parsed.imports) {
462
+ if (isBuiltin(imp.specifier)) continue;
463
+ if (!isRelative(imp.specifier)) {
464
+ throw new Error(
465
+ `'${id}' imports '${imp.specifier}' — this package has zero dependencies, ` +
466
+ `so only relative paths and node: builtins can be bundled.`,
467
+ );
468
+ }
469
+ visit(resolveSpecifier(id, imp.specifier), id, stack);
470
+ }
471
+ for (const dyn of record.parsed.dynamicImports) {
472
+ if (isBuiltin(dyn.specifier) || !isRelative(dyn.specifier)) continue;
473
+ dynamicRoots.push([resolveSpecifier(id, dyn.specifier), id]);
474
+ }
475
+ stack.pop();
476
+ state.set(id, 'done');
477
+ order.push(id);
478
+ };
479
+
480
+ visit(entry, null, []);
481
+ // Dynamic targets last: they are reachable but never on the init path.
482
+ for (let i = 0; i < dynamicRoots.length; i++) {
483
+ const [id, importer] = dynamicRoots[i];
484
+ if (state.get(id) === 'done') continue;
485
+ visit(id, importer, []);
486
+ }
487
+ // The entry must be evaluated last — it is the one with the side effects.
488
+ if (order.at(-1) !== entry) {
489
+ order.splice(order.indexOf(entry), 1);
490
+ order.push(entry);
491
+ }
492
+
493
+ return { order, modules };
494
+ }
495
+
496
+ /* ════════════════════════════════════════════════════════════════════════════
497
+ * 4. bundle
498
+ * ════════════════════════════════════════════════════════════════════════════ */
499
+
500
+ const REGISTRY = '__acuvo_modules';
501
+
502
+ function applyEdits(src, edits) {
503
+ const sorted = [...edits].sort((a, b) => a.start - b.start);
504
+ let out = '';
505
+ let at = 0;
506
+ for (const e of sorted) {
507
+ if (e.start < at) continue; // overlapping edit — the outer one already won
508
+ out += src.slice(at, e.start) + e.text;
509
+ at = e.end;
510
+ }
511
+ return out + src.slice(at);
512
+ }
513
+
514
+ const q = (s) => JSON.stringify(s);
515
+
516
+ function assetPrelude(assets) {
517
+ return [
518
+ 'const __acuvo_assets = {',
519
+ ...[...assets.entries()].map(([name, body]) => ` ${q(name)}: ${q(body)},`),
520
+ '};',
521
+ 'let __acuvo_assetDir = null;',
522
+ '/**',
523
+ ' * Materialise an inlined asset on first use.',
524
+ ' *',
525
+ ' * ⚠️ INTO A TEMP DIRECTORY, NEVER BESIDE THE BUNDLE. The pitch is "one file";',
526
+ ' * a file that drops a folder next to itself the first time you run it is not',
527
+ ' * one file. The directory is removed on exit.',
528
+ ' */',
529
+ 'const __acuvo_asset = (name) => {',
530
+ ' if (__acuvo_assetDir === null) {',
531
+ " __acuvo_assetDir = __node_fs.mkdtempSync(__node_path.join(__node_os.tmpdir(), 'acuvo-bundle-'));",
532
+ ' try {',
533
+ " process.on('exit', () => {",
534
+ ' try { __node_fs.rmSync(__acuvo_assetDir, { recursive: true, force: true }); } catch {}',
535
+ ' });',
536
+ ' } catch {}',
537
+ ' }',
538
+ ' const target = __node_path.join(__acuvo_assetDir, name);',
539
+ ' if (!__node_fs.existsSync(target)) {',
540
+ ' __node_fs.mkdirSync(__node_path.dirname(target), { recursive: true });',
541
+ ' __node_fs.writeFileSync(target, __acuvo_assets[name]);',
542
+ ' }',
543
+ ' return __node_url.pathToFileURL(target);',
544
+ '};',
545
+ ].join('\n');
546
+ }
547
+
548
+ const META_PRELUDE = [
549
+ '/**',
550
+ " * A module's own URL, had the tree been unpacked beside this file. `dirname`",
551
+ ' * of it is a real directory, so the package-root arithmetic in the source',
552
+ ' * yields a real path instead of crashing on a file that is not there.',
553
+ ' */',
554
+ "const __acuvo_base = new URL('./', import.meta.url);",
555
+ 'const __acuvo_meta = (id) => new URL(id, __acuvo_base).href;',
556
+ ].join('\n');
557
+
558
+ /**
559
+ * Turn a module graph into one file.
560
+ *
561
+ * Returns `{ code, assets, moduleIds }`. `assets` is every file inlined because
562
+ * a module named it through `new URL(…, import.meta.url)`.
563
+ */
564
+ export function bundle({ entry, readFile }) {
565
+ const { order, modules } = buildGraph({ entry, readFile });
566
+
567
+ const builtins = new Set();
568
+ const assets = new Map(); // id -> contents
569
+ const bodies = [];
570
+ const exportedBy = new Map();
571
+
572
+ for (const id of order) {
573
+ exportedBy.set(id, new Set(modules.get(id).parsed.exports.flatMap((e) => e.names)));
574
+ }
575
+
576
+ for (const id of order) {
577
+ const { src, parsed } = modules.get(id);
578
+ const edits = [];
579
+
580
+ if (parsed.shebang) edits.push({ start: 0, end: 2, text: '//' });
581
+
582
+ for (const imp of parsed.imports) {
583
+ let source;
584
+ if (isBuiltin(imp.specifier)) {
585
+ builtins.add(imp.specifier);
586
+ source = builtinSlug(imp.specifier);
587
+ } else {
588
+ const target = resolveSpecifier(id, imp.specifier);
589
+ source = `${REGISTRY}[${q(target)}]`;
590
+ const available = exportedBy.get(target);
591
+ for (const n of imp.names) {
592
+ if (available && !available.has(n.imported)) {
593
+ throw new Error(
594
+ `'${id}' imports { ${n.imported} } from '${imp.specifier}', which does not export it`,
595
+ );
596
+ }
597
+ }
598
+ }
599
+ const parts = [];
600
+ if (imp.names.length) {
601
+ const fields = imp.names
602
+ .map((n) => (n.imported === n.local ? n.local : `${n.imported}: ${n.local}`))
603
+ .join(', ');
604
+ parts.push(`const { ${fields} } = ${source};`);
605
+ }
606
+ if (imp.namespace) parts.push(`const ${imp.namespace} = ${source};`);
607
+ if (imp.defaultName) {
608
+ parts.push(
609
+ isBuiltin(imp.specifier)
610
+ ? `const ${imp.defaultName} = ${source}.default ?? ${source};`
611
+ : `const ${imp.defaultName} = ${source}.default;`,
612
+ );
613
+ }
614
+ edits.push({ start: imp.start, end: imp.end, text: parts.join(' ') || ';' });
615
+ }
616
+
617
+ for (const exp of parsed.exports) {
618
+ edits.push({ start: exp.start, end: exp.end, text: exp.kind === 'list' ? ';' : '' });
619
+ }
620
+
621
+ for (const meta of parsed.metaUrls) {
622
+ if (meta.kind === 'new-url') {
623
+ const assetId = resolveSpecifier(id, meta.specifier);
624
+ if (!assets.has(assetId)) {
625
+ const contents = readFile(assetId);
626
+ if (contents === null || contents === undefined) {
627
+ throw new Error(`'${id}' references the asset '${assetId}', which does not exist`);
628
+ }
629
+ assets.set(assetId, contents);
630
+ }
631
+ edits.push({ start: meta.start, end: meta.end, text: `__acuvo_asset(${q(assetId)})` });
632
+ } else {
633
+ edits.push({ start: meta.start, end: meta.end, text: `__acuvo_meta(${q(id)})` });
634
+ }
635
+ }
636
+
637
+ for (const dyn of parsed.dynamicImports) {
638
+ if (!isRelative(dyn.specifier)) continue;
639
+ const target = resolveSpecifier(id, dyn.specifier);
640
+ edits.push({
641
+ start: dyn.start,
642
+ end: dyn.end,
643
+ text: `Promise.resolve(${REGISTRY}[${q(target)}])`,
644
+ });
645
+ }
646
+
647
+ const body = applyEdits(src, edits);
648
+ const returns = parsed.exports
649
+ .flatMap((e) => e.names.map((n, i) => [n, e.locals[i] ?? n]))
650
+ .map(([name, local]) => (name === local ? name : `${name}: ${local}`))
651
+ .join(', ');
652
+
653
+ // Only an `await` anywhere in the module warrants an async wrapper. Wrapping
654
+ // a module that merely defines async functions would be harmless but adds a
655
+ // microtask tick per module for nothing.
656
+ const wantsAsync = /\bawait\b/.test(parsed.masked);
657
+ const open = wantsAsync ? `await (async () => {` : `(() => {`;
658
+
659
+ bodies.push(
660
+ `${REGISTRY}[${q(id)}] = ${open}\n${body}\nreturn { ${returns} };\n})();`,
661
+ );
662
+ }
663
+
664
+ const needsAssets = assets.size > 0;
665
+ const needsMeta = order.some((id) => modules.get(id).parsed.metaUrls.some((u) => u.kind === 'bare'));
666
+ if (needsAssets) for (const b of ['node:fs', 'node:path', 'node:os', 'node:url']) builtins.add(b);
667
+
668
+ const entryShebang = modules.get(entry).parsed.shebang ?? '#!/usr/bin/env node';
669
+
670
+ const head = [
671
+ entryShebang,
672
+ '/* acuvo-code — generated bundle. Do not edit; edit the source tree and run `npm run bundle`. */',
673
+ '',
674
+ // Single-quoted on purpose: `from 'node:fs'` is the form a human greps for.
675
+ ...[...builtins].sort().map((s) => `import * as ${builtinSlug(s)} from '${s}';`),
676
+ '',
677
+ `const ${REGISTRY} = Object.create(null);`,
678
+ ];
679
+ if (needsMeta) head.push('', META_PRELUDE);
680
+ if (needsAssets) head.push('', assetPrelude(assets));
681
+
682
+ const code = `${head.join('\n')}\n\n${bodies.join('\n\n')}\n`;
683
+
684
+ return { code, assets: [...assets.keys()].sort(), moduleIds: order };
685
+ }
686
+
687
+ /* ════════════════════════════════════════════════════════════════════════════
688
+ * 5. scanForSecrets
689
+ * ════════════════════════════════════════════════════════════════════════════ */
690
+
691
+ /**
692
+ * The shapes that actually leak. Deliberately narrow: a scanner that fires on
693
+ * `process.env.OPENROUTER_API_KEY` teaches everyone to ignore it, and a check
694
+ * that fails correct work is worse than no check.
695
+ *
696
+ * The real guarantee is structural — this bundler only ever opens files named
697
+ * by the import graph — and this is the belt to that braces.
698
+ */
699
+ const SECRET_PATTERNS = [
700
+ ['openrouter key', /sk-or-v1-[A-Za-z0-9]{24,}/],
701
+ ['openai key', /sk-[A-Za-z0-9]{32,}/],
702
+ ['github token', /gh[pousr]_[A-Za-z0-9]{30,}/],
703
+ ['aws access key id', /AKIA[0-9A-Z]{16}/],
704
+ ['private key block', /-----BEGIN (?:[A-Z]+ )?PRIVATE KEY-----/],
705
+ ['slack token', /xox[abprs]-[A-Za-z0-9-]{10,}/],
706
+ ['google api key', /AIza[0-9A-Za-z_-]{35}/],
707
+ ];
708
+
709
+ export function scanForSecrets(text) {
710
+ const found = [];
711
+ for (const [label, re] of SECRET_PATTERNS) {
712
+ const all = new RegExp(re.source, 'g');
713
+ let m;
714
+ while ((m = all.exec(text)) !== null) {
715
+ found.push({ label, index: m.index, sample: `${m[0].slice(0, 12)}…` });
716
+ }
717
+ }
718
+ return found;
719
+ }
720
+
721
+ /* ════════════════════════════════════════════════════════════════════════════
722
+ * 6. the CLI
723
+ * ════════════════════════════════════════════════════════════════════════════ */
724
+
725
+ function main(argv) {
726
+ const args = argv.slice(2);
727
+ const flag = (name, fallback) => {
728
+ const i = args.indexOf(name);
729
+ return i === -1 || i === args.length - 1 ? fallback : args[i + 1];
730
+ };
731
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
732
+ const entry = flag('--entry', 'bin/acuvo.mjs');
733
+ const out = flag('--out', 'dist/acuvo.mjs');
734
+
735
+ const readFile = (id) => {
736
+ try {
737
+ return readFileSync(join(root, id), 'utf8');
738
+ } catch {
739
+ return null;
740
+ }
741
+ };
742
+
743
+ const { code, assets, moduleIds } = bundle({ entry, readFile });
744
+
745
+ const leaks = scanForSecrets(code);
746
+ if (leaks.length) {
747
+ process.stderr.write(
748
+ `refusing to write ${out} — ${leaks.length} possible secret(s):\n` +
749
+ leaks.map((l) => ` ${l.label} at byte ${l.index} (${l.sample})\n`).join(''),
750
+ );
751
+ return 1;
752
+ }
753
+
754
+ const target = resolve(root, out);
755
+ mkdirSync(dirname(target), { recursive: true });
756
+ writeFileSync(target, code);
757
+ try { chmodSync(target, 0o755); } catch {}
758
+
759
+ process.stdout.write(
760
+ `${out} ${Buffer.byteLength(code)} bytes ` +
761
+ `${moduleIds.length} modules ${assets.length} asset(s): ${assets.join(', ') || 'none'}\n`,
762
+ );
763
+ return 0;
764
+ }
765
+
766
+ const invokedDirectly = process.argv[1]
767
+ && pathToFileURL(process.argv[1]).href === import.meta.url;
768
+ if (invokedDirectly) process.exit(main(process.argv));