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
package/lib/env-file.mjs
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ── ⚠️⚠️ THE FIX FOR THE DARK MEDIA HALF WAS ITSELF DARK ───────────────────
|
|
3
|
+
*
|
|
4
|
+
* `bin/acuvo.mjs` grew a `.env` loader whose own comment explains exactly why:
|
|
5
|
+
* *"Measured today: `mediaToolNames(process.env)` returned `[]` in an ordinary
|
|
6
|
+
* terminal, on a machine where every one of those services is configured and
|
|
7
|
+
* working... `see_page` — the capability this CLI is sold on — was never even
|
|
8
|
+
* OFFERED."*
|
|
9
|
+
*
|
|
10
|
+
* ⚠️ IT LOADED `.env`. Measured 2026-08-12 across this whole machine: there is
|
|
11
|
+
* **no plain `.env` anywhere** — every file is `.env.local`, which is the name
|
|
12
|
+
* Next.js, Vite and Create React App all use for the one that holds secrets and
|
|
13
|
+
* is git-ignored. So the loader never fired once, and the media half it was
|
|
14
|
+
* written to rescue stayed exactly as dark as before. A fix that cannot run is
|
|
15
|
+
* indistinguishable from the bug.
|
|
16
|
+
*
|
|
17
|
+
* ⚠️ AND IT WAS INLINE IN `bin/`, WHICH IS WHY NOBODY CAUGHT IT. Eight lines
|
|
18
|
+
* inside `main()` cannot be imported, so no test could assert which filenames it
|
|
19
|
+
* looks for. That is the whole reason this file exists as a module: the list of
|
|
20
|
+
* candidate names is now a value a test can read.
|
|
21
|
+
*
|
|
22
|
+
* ── PRECEDENCE ─────────────────────────────────────────────────────────────
|
|
23
|
+
*
|
|
24
|
+
* ⭐ `.env.local` IS LOADED BEFORE `.env`, and the order is load-bearing because
|
|
25
|
+
* `process.loadEnvFile` DOES NOT OVERWRITE. First writer wins, so "load first"
|
|
26
|
+
* means "higher precedence" — the inverse of what the reading order suggests,
|
|
27
|
+
* and worth stating because getting it backwards silently makes the committed
|
|
28
|
+
* `.env` beat the private `.env.local`.
|
|
29
|
+
*
|
|
30
|
+
* ⭐ AND IT WALKS UP. A monorepo keeps one `.env.local` at the top and runs
|
|
31
|
+
* tools from `packages/whatever`; stopping at the workspace root would find
|
|
32
|
+
* nothing in the common layout. It stops at the filesystem root or a `.git`
|
|
33
|
+
* directory — the same boundary every other developer tool treats as "the
|
|
34
|
+
* project" — so it can never wander into a sibling checkout or a home
|
|
35
|
+
* directory it was not pointed at.
|
|
36
|
+
*
|
|
37
|
+
* ⚠️ A REAL ENVIRONMENT VARIABLE ALWAYS WINS over every file, because the
|
|
38
|
+
* loader never overwrites. An explicit `export` in this shell must beat a stale
|
|
39
|
+
* file somebody forgot about, or debugging becomes guesswork about which value
|
|
40
|
+
* is live.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import { existsSync, statSync, readFileSync } from 'node:fs';
|
|
44
|
+
import { join, dirname, resolve } from 'node:path';
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* In precedence order — earlier wins, because `loadEnvFile` does not overwrite.
|
|
48
|
+
* ⚠️ `.env.example` is deliberately ABSENT: it is documentation, it is committed,
|
|
49
|
+
* and its values are placeholders. Loading it would set `OPENROUTER_API_KEY` to
|
|
50
|
+
* something like `sk-or-v1-...` and produce a 401 that blames the user's key.
|
|
51
|
+
*/
|
|
52
|
+
export const ENV_FILENAMES = Object.freeze(['.env.local', '.env']);
|
|
53
|
+
|
|
54
|
+
/** How far up to walk before giving up. Deep enough for any real monorepo. */
|
|
55
|
+
export const MAX_WALK_UP = 8;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Every env file that exists, nearest directory first, in precedence order.
|
|
59
|
+
* Pure and exported so a test can assert the NAMES without touching the
|
|
60
|
+
* process environment — the assertion that would have caught the original bug.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} from directory to start at
|
|
63
|
+
* @param {{ stopAtGit?: boolean }} opts
|
|
64
|
+
* @returns {string[]} absolute paths, highest precedence first
|
|
65
|
+
*/
|
|
66
|
+
export function envFileCandidates(from, { stopAtGit = true } = {}) {
|
|
67
|
+
if (typeof from !== 'string' || from === '') return [];
|
|
68
|
+
|
|
69
|
+
const found = [];
|
|
70
|
+
let dir = resolve(from);
|
|
71
|
+
|
|
72
|
+
for (let i = 0; i < MAX_WALK_UP; i += 1) {
|
|
73
|
+
for (const name of ENV_FILENAMES) {
|
|
74
|
+
const candidate = join(dir, name);
|
|
75
|
+
try {
|
|
76
|
+
if (existsSync(candidate) && statSync(candidate).isFile()) found.push(candidate);
|
|
77
|
+
} catch { /* unreadable — treat as absent */ }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* ⚠️ THE `.git` STOP COMES **AFTER** THIS DIRECTORY'S FILES ARE COLLECTED.
|
|
82
|
+
* The repository root is the most likely place for the file, so stopping
|
|
83
|
+
* before reading it would skip the single most common location.
|
|
84
|
+
*/
|
|
85
|
+
if (stopAtGit) {
|
|
86
|
+
try { if (existsSync(join(dir, '.git'))) break; } catch { /* keep walking */ }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const parent = dirname(dir);
|
|
90
|
+
if (parent === dir) break;
|
|
91
|
+
dir = parent;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return found;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Load every env file we can find, best-effort.
|
|
99
|
+
*
|
|
100
|
+
* ⚠️ NEVER THROWS AND NEVER FAILS THE RUN. No env file is the normal case, and
|
|
101
|
+
* a malformed one must not stop a coding session that never needed it.
|
|
102
|
+
*
|
|
103
|
+
* @param {string[]} roots directories to search, in order
|
|
104
|
+
* @param {{ load?: Function, existsImpl?: Function }} opts
|
|
105
|
+
* @returns {{ loaded: string[], failed: Array<{file: string, error: string}> }}
|
|
106
|
+
*/
|
|
107
|
+
/**
|
|
108
|
+
* ── ⚠️⚠️ THE MASTER KEY: A CLONED REPOSITORY WAS SETTING OUR OWN SWITCHES ────
|
|
109
|
+
*
|
|
110
|
+
* `bin/acuvo.mjs` calls `envLoad([root, process.cwd()])`, and this loader walks
|
|
111
|
+
* up from the WORKSPACE reading `.env.local` / `.env` into `process.env`. That is
|
|
112
|
+
* correct and wanted for a PROJECT’s variables — DATABASE_URL, STRIPE_KEY, the
|
|
113
|
+
* things the code under test needs.
|
|
114
|
+
*
|
|
115
|
+
* ⚠️⚠️ IT WAS ALSO SETTING OURS. Measured 2026-08-15 with one `.env.local` in a
|
|
116
|
+
* cloned repository, every value chosen by the repository:
|
|
117
|
+
*
|
|
118
|
+
* npm installs enabled false → TRUE (ACUVO_ALLOW_INSTALL)
|
|
119
|
+
* MCP consent bypassed false → TRUE (ACUVO_TRUST_MCP)
|
|
120
|
+
* git push enabled false → TRUE (ACUVO_ALLOW_PUSH)
|
|
121
|
+
* reviewer independent true → FALSE (ACUVO_REFUTE_MODEL → the builder)
|
|
122
|
+
* provider pin ours → theirs (ACUVO_PROVIDER_ORDER)
|
|
123
|
+
*
|
|
124
|
+
* Every guard closed today — the install gate, MCP consent, the independent
|
|
125
|
+
* second opinion, the measured provider pin — defeated by one file in a
|
|
126
|
+
* repository somebody cloned. It is the same defect `policy.mjs` names and
|
|
127
|
+
* solves: **the config lives in the workspace, and the agent can write to the
|
|
128
|
+
* workspace.** Its answer is that a workspace layer may only ever REMOVE
|
|
129
|
+
* permission, never add. This is that rule, applied to the environment.
|
|
130
|
+
*
|
|
131
|
+
* ⭐ SO A WORKSPACE ENV FILE MAY SET THE PROJECT’S VARIABLES AND NONE OF OURS.
|
|
132
|
+
* The refusal is REPORTED, never silent — a variable that vanishes without a
|
|
133
|
+
* word is a bug report we would never receive.
|
|
134
|
+
*
|
|
135
|
+
* ⚠️ AND IT IS A PREFIX RULE, NOT A LIST OF THE SWITCHES WE HAPPEN TO HAVE
|
|
136
|
+
* TODAY. A list goes stale the moment somebody adds `ACUVO_ALLOW_ANYTHING`, and
|
|
137
|
+
* the person adding it will not be reading this file. Anything beginning
|
|
138
|
+
* `ACUVO_` is ours by construction.
|
|
139
|
+
*
|
|
140
|
+
* ⚠️ `process.loadEnvFile` DOES NOT OVERRIDE AN ALREADY-SET VARIABLE, which is
|
|
141
|
+
* why our own OPENROUTER_API_KEY survived the probe. That is a mitigation, not a
|
|
142
|
+
* defence: every variable the operator has NOT set is the repository’s to
|
|
143
|
+
* choose, and the switches above are all unset by default. Relying on it would
|
|
144
|
+
* be relying on the user having already configured the thing being attacked.
|
|
145
|
+
*/
|
|
146
|
+
export const OURS_PREFIX = /^ACUVO_/i;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Variables that steer a CHILD PROCESS rather than this one. A repository that
|
|
150
|
+
* cannot set our switches directly must not be able to set them through the
|
|
151
|
+
* environment a spawned python, gh or node inherits either.
|
|
152
|
+
*
|
|
153
|
+
* ⚠️ `command.mjs`’s `scrubEnvironment` already strips conventionally-named
|
|
154
|
+
* secrets and the node injection variables from CHILDREN. This list is the
|
|
155
|
+
* INBOUND half — stopping them entering `process.env` at all — because a
|
|
156
|
+
* variable we never accept is one no future spawn site can forget to scrub.
|
|
157
|
+
*/
|
|
158
|
+
export const CHILD_STEERING_VARS = Object.freeze([
|
|
159
|
+
'NODE_OPTIONS', 'NODE_PATH', 'NODE_REPL_EXTERNAL_MODULE',
|
|
160
|
+
'PYTHONPATH', 'PYTHONSTARTUP', 'PYTHONHOME', 'PYTHONWARNINGS', 'PYTEST_ADDOPTS',
|
|
161
|
+
'GH_HOST', 'GH_TOKEN', 'GH_ENTERPRISE_TOKEN', 'GITHUB_TOKEN', 'GH_CONFIG_DIR',
|
|
162
|
+
'GIT_SSH_COMMAND', 'GIT_EXTERNAL_DIFF', 'GIT_PAGER',
|
|
163
|
+
'LD_PRELOAD', 'DYLD_INSERT_LIBRARIES',
|
|
164
|
+
'OPENROUTER_API_KEY', 'OPENROUTER_CODEGEN_MODEL',
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Is this a name a file INSIDE the workspace may set?
|
|
169
|
+
* @param {string} name
|
|
170
|
+
*/
|
|
171
|
+
export function workspaceMaySet(name) {
|
|
172
|
+
const n = String(name ?? '');
|
|
173
|
+
if (OURS_PREFIX.test(n)) return false;
|
|
174
|
+
return !CHILD_STEERING_VARS.some((v) => v.toLowerCase() === n.toLowerCase());
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The NAMES an env file declares, without evaluating it.
|
|
179
|
+
*
|
|
180
|
+
* ⚠️ NAMES ONLY, DELIBERATELY. The value is never needed to decide whether the
|
|
181
|
+
* name is allowed, and not parsing values means this cannot become a second,
|
|
182
|
+
* subtly-different dotenv implementation that disagrees with Node’s about
|
|
183
|
+
* quoting or multi-line strings. Node still does the actual loading.
|
|
184
|
+
*/
|
|
185
|
+
export function declaredNames(text) {
|
|
186
|
+
const out = [];
|
|
187
|
+
for (const raw of String(text ?? '').split(/\r?\n/)) {
|
|
188
|
+
const line = raw.trim();
|
|
189
|
+
if (!line || line.startsWith('#')) continue;
|
|
190
|
+
const m = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(line);
|
|
191
|
+
if (m) out.push(m[1]);
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
export function loadEnvFiles(roots, { load = process.loadEnvFile, readImpl = null, env = process.env } = {}) {
|
|
196
|
+
const loaded = [];
|
|
197
|
+
const failed = [];
|
|
198
|
+
/** Names a workspace file tried to set and was not allowed to. Reported, never silent. */
|
|
199
|
+
const refused = [];
|
|
200
|
+
if (typeof load !== 'function') return { loaded, failed, refused };
|
|
201
|
+
|
|
202
|
+
const readText = readImpl ?? ((f) => readFileSync(f, 'utf8'));
|
|
203
|
+
|
|
204
|
+
const seen = new Set();
|
|
205
|
+
for (const root of roots) {
|
|
206
|
+
for (const file of envFileCandidates(root)) {
|
|
207
|
+
if (seen.has(file)) continue;
|
|
208
|
+
seen.add(file);
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* ⭐ SNAPSHOT THE NAMES WE OWN, LOAD, THEN PUT THEM BACK.
|
|
212
|
+
*
|
|
213
|
+
* ⚠️ Why not filter BEFORE loading? Because `process.loadEnvFile` is
|
|
214
|
+
* Node’s own parser and offers no hook — reimplementing dotenv to filter
|
|
215
|
+
* first would be a second parser that disagrees with the first about
|
|
216
|
+
* quoting, which is a worse bug than the one being fixed. So the file is
|
|
217
|
+
* loaded, and anything it set that it may not set is restored exactly as
|
|
218
|
+
* it was, including "was not set at all".
|
|
219
|
+
*/
|
|
220
|
+
let guarded = [];
|
|
221
|
+
try {
|
|
222
|
+
guarded = declaredNames(readText(file)).filter((n) => !workspaceMaySet(n));
|
|
223
|
+
} catch {
|
|
224
|
+
// Unreadable here means load() fails below and is recorded there.
|
|
225
|
+
}
|
|
226
|
+
const restore = guarded.map((n) => [n, Object.prototype.hasOwnProperty.call(env, n) ? env[n] : undefined]);
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
load(file);
|
|
230
|
+
loaded.push(file);
|
|
231
|
+
} catch (e) {
|
|
232
|
+
failed.push({ file, error: e?.message ?? String(e) });
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
for (const [name, previous] of restore) {
|
|
237
|
+
const now = env[name];
|
|
238
|
+
if (previous === undefined) delete env[name];
|
|
239
|
+
else env[name] = previous;
|
|
240
|
+
/**
|
|
241
|
+
* ⚠️ Only a name the file actually CHANGED is reported. Declaring a
|
|
242
|
+
* variable that already held the same value is not an attempt at
|
|
243
|
+
* anything, and crying wolf about it teaches people to ignore the line.
|
|
244
|
+
*/
|
|
245
|
+
if (now !== previous) refused.push({ file, name });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return { loaded, failed, refused };
|
|
250
|
+
}
|