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.
- package/CHANGELOG.md +328 -0
- package/ENTERPRISE.md +927 -0
- package/LICENSE +120 -0
- package/README.md +1245 -0
- package/ROADMAP.md +556 -0
- package/bin/acuvo-mcp.mjs +208 -0
- package/bin/acuvo.mjs +3108 -0
- package/lib/acceptance-consent.mjs +168 -0
- package/lib/acceptance.mjs +859 -0
- package/lib/account.mjs +226 -0
- package/lib/acuvo-dir.mjs +72 -0
- package/lib/acuvo-models.mjs +141 -0
- package/lib/apply-patch.mjs +570 -0
- package/lib/ask-user.mjs +173 -0
- package/lib/audit.mjs +530 -0
- package/lib/auto-lease.mjs +174 -0
- package/lib/background.mjs +842 -0
- package/lib/best-of.mjs +334 -0
- package/lib/board.mjs +232 -0
- package/lib/breaker.mjs +93 -0
- package/lib/budget.mjs +1375 -0
- package/lib/builtin-skills.mjs +135 -0
- package/lib/cache-floor.mjs +204 -0
- package/lib/chain.mjs +303 -0
- package/lib/changed-paths.mjs +84 -0
- package/lib/chat.mjs +434 -0
- package/lib/checkpoint.mjs +637 -0
- package/lib/child-lifetime.mjs +71 -0
- package/lib/cli-args.mjs +1255 -0
- package/lib/code-review.mjs +1382 -0
- package/lib/colour.mjs +82 -0
- package/lib/command.mjs +2847 -0
- package/lib/compact.mjs +1151 -0
- package/lib/completion.mjs +515 -0
- package/lib/creative-engines.mjs +779 -0
- package/lib/db-inspect.mjs +1624 -0
- package/lib/delete.mjs +111 -0
- package/lib/design-loop.mjs +570 -0
- package/lib/diff-preview.mjs +1044 -0
- package/lib/doctor.mjs +2139 -0
- package/lib/dropped.mjs +216 -0
- package/lib/edit-diagnostics.mjs +277 -0
- package/lib/edit.mjs +460 -0
- package/lib/env-file.mjs +250 -0
- package/lib/escalate.mjs +702 -0
- package/lib/evaluate.mjs +284 -0
- package/lib/fetch-text.mjs +952 -0
- package/lib/fleet-budget.mjs +256 -0
- package/lib/gh.mjs +1536 -0
- package/lib/git.mjs +1341 -0
- package/lib/github.mjs +261 -0
- package/lib/h2.mjs +194 -0
- package/lib/handoff.mjs +417 -0
- package/lib/hooks.mjs +626 -0
- package/lib/http-probe.mjs +907 -0
- package/lib/image-director.mjs +322 -0
- package/lib/image-edit.mjs +522 -0
- package/lib/imagegen.mjs +998 -0
- package/lib/interrupt.mjs +234 -0
- package/lib/learned.mjs +353 -0
- package/lib/lease-watch.mjs +115 -0
- package/lib/lease.mjs +868 -0
- package/lib/localize.mjs +834 -0
- package/lib/log-tail.mjs +1052 -0
- package/lib/login.mjs +157 -0
- package/lib/lsp.mjs +1613 -0
- package/lib/mcp-consent.mjs +377 -0
- package/lib/mcp-defaults.mjs +780 -0
- package/lib/mcp-server.mjs +1343 -0
- package/lib/mcp.mjs +1263 -0
- package/lib/media.mjs +1283 -0
- package/lib/memory-workspace.mjs +179 -0
- package/lib/model-json.mjs +132 -0
- package/lib/model-tier.mjs +171 -0
- package/lib/model.mjs +1445 -0
- package/lib/parallel.mjs +144 -0
- package/lib/perchance.mjs +210 -0
- package/lib/plan-coherence.mjs +1461 -0
- package/lib/plan-ledger.mjs +981 -0
- package/lib/plan.mjs +461 -0
- package/lib/policy.mjs +783 -0
- package/lib/prefix-order.mjs +38 -0
- package/lib/project-memory.mjs +127 -0
- package/lib/prompt.mjs +109 -0
- package/lib/python.mjs +862 -0
- package/lib/rcfile.mjs +853 -0
- package/lib/read-window.mjs +743 -0
- package/lib/refute-tools.mjs +34 -0
- package/lib/refute.mjs +806 -0
- package/lib/repl-driver.mjs +264 -0
- package/lib/repl.mjs +324 -0
- package/lib/replay.mjs +1218 -0
- package/lib/repo-map.mjs +1101 -0
- package/lib/report.mjs +419 -0
- package/lib/search-rank.mjs +99 -0
- package/lib/search.mjs +659 -0
- package/lib/secret-paths.mjs +54 -0
- package/lib/session.mjs +1017 -0
- package/lib/skills.mjs +703 -0
- package/lib/slash.mjs +356 -0
- package/lib/spawn-argv.mjs +1151 -0
- package/lib/spend.mjs +250 -0
- package/lib/steer.mjs +280 -0
- package/lib/stream.mjs +253 -0
- package/lib/stuck.mjs +712 -0
- package/lib/subagent.mjs +749 -0
- package/lib/terminal-graphics.mjs +171 -0
- package/lib/tool-prefix.mjs +226 -0
- package/lib/tool-shortlist.mjs +162 -0
- package/lib/tools.mjs +2333 -0
- package/lib/tsserver.mjs +423 -0
- package/lib/turn.mjs +5672 -0
- package/lib/untrusted-block.mjs +271 -0
- package/lib/verify-claim.mjs +299 -0
- package/lib/vision.mjs +330 -0
- package/lib/voice-task.mjs +561 -0
- package/lib/warm-provider.mjs +255 -0
- package/lib/websearch.mjs +401 -0
- package/lib/workspace.mjs +928 -0
- package/lib/write-approval.mjs +235 -0
- package/lib/write-many.mjs +162 -0
- package/package.json +62 -0
- package/scripts/bundle.mjs +768 -0
- package/scripts/cache-floor.mjs +176 -0
- package/scripts/machine.mjs +226 -0
- package/scripts/test.mjs +139 -0
- package/skills/accessibility.md +87 -0
- package/skills/acuvo-design-system.md +123 -0
- package/skills/animation.md +84 -0
- package/skills/api-design.md +82 -0
- package/skills/auth-and-sessions.md +78 -0
- package/skills/build-with-a-framework.md +101 -0
- package/skills/colour-and-contrast.md +112 -0
- package/skills/creative-engines.md +81 -0
- package/skills/css-layout.md +85 -0
- package/skills/data-and-charts.md +77 -0
- package/skills/debugging.md +76 -0
- package/skills/designing-by-looking.md +84 -0
- package/skills/error-handling.md +78 -0
- package/skills/forms-and-validation.md +93 -0
- package/skills/nextjs-app-router.md +75 -0
- package/skills/page-composition.md +103 -0
- package/skills/performance.md +77 -0
- package/skills/plan-before-building.md +52 -0
- package/skills/planning-and-delegating.md +72 -0
- package/skills/refactoring.md +70 -0
- package/skills/security-basics.md +76 -0
- package/skills/state-management.md +73 -0
- package/skills/supabase-multitenant.md +72 -0
- package/skills/typescript-strict.md +90 -0
- package/skills/typography.md +135 -0
- package/skills/verify-your-own-work.md +62 -0
- package/skills/web-app-quality.md +62 -0
- 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));
|