@bremmdev/arca-env 0.0.0-stage → 0.1.2

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.
@@ -0,0 +1,2783 @@
1
+ import { n as createMaskedWriter, r as needlesFor, t as Redactor } from "./redact-D4Txfinc.mjs";
2
+ import { parseArgs } from "node:util";
3
+ import { appendFileSync, chmodSync, closeSync, copyFileSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
4
+ import { basename, dirname, join, relative, resolve } from "node:path";
5
+ import { constants, homedir } from "node:os";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
+ import spawn from "cross-spawn";
8
+ import { createCipheriv, createDecipheriv, createHash, hkdfSync, randomBytes, randomInt, scryptSync } from "node:crypto";
9
+ import { execFileSync } from "node:child_process";
10
+ //#region src/io.ts
11
+ function stream(s) {
12
+ return {
13
+ write: (chunk) => {
14
+ s.write(chunk);
15
+ },
16
+ get isTTY() {
17
+ return Boolean(s.isTTY);
18
+ },
19
+ get columns() {
20
+ return s.columns;
21
+ },
22
+ get rows() {
23
+ return s.rows;
24
+ },
25
+ get colorDepth() {
26
+ return s.isTTY && typeof s.getColorDepth === "function" ? s.getColorDepth() : void 0;
27
+ }
28
+ };
29
+ }
30
+ function processIO() {
31
+ return {
32
+ env: process.env,
33
+ cwd: process.cwd(),
34
+ platform: process.platform,
35
+ stdout: stream(process.stdout),
36
+ stderr: stream(process.stderr),
37
+ stdinIsTTY: Boolean(process.stdin.isTTY),
38
+ readStdin: async () => {
39
+ const chunks = [];
40
+ for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
41
+ return Buffer.concat(chunks).toString("utf8");
42
+ },
43
+ prompts: clackPrompts(),
44
+ childStdin: "inherit",
45
+ handleSignals: true
46
+ };
47
+ }
48
+ function clackPrompts() {
49
+ const load = () => import("@clack/prompts");
50
+ const output = process.stderr;
51
+ return {
52
+ async password(message) {
53
+ const p = await load();
54
+ const r = await p.password({
55
+ message,
56
+ output
57
+ });
58
+ return p.isCancel(r) ? null : r;
59
+ },
60
+ async text(message, opts = {}) {
61
+ const p = await load();
62
+ const r = await p.text({
63
+ message,
64
+ output,
65
+ placeholder: opts.placeholder,
66
+ initialValue: opts.initial
67
+ });
68
+ return p.isCancel(r) ? null : r;
69
+ },
70
+ async confirm(message, initial = true) {
71
+ const p = await load();
72
+ const r = await p.confirm({
73
+ message,
74
+ output,
75
+ initialValue: initial
76
+ });
77
+ return p.isCancel(r) ? null : r;
78
+ },
79
+ async multiselect(message, options, initial) {
80
+ const p = await load();
81
+ const r = await p.multiselect({
82
+ message,
83
+ output,
84
+ options,
85
+ initialValues: initial,
86
+ required: false
87
+ });
88
+ return p.isCancel(r) ? null : r;
89
+ }
90
+ };
91
+ }
92
+ //#endregion
93
+ //#region src/context.ts
94
+ /**
95
+ * Env vars that agent tools set in the commands they run. Order matters: the first hit names
96
+ * the agent. Only markers specific to agent-launched processes belong here, not ones an IDE
97
+ * sets in every terminal (those would lock humans out). To verify per tool in spike S3.
98
+ */
99
+ const AGENT_MARKERS = [
100
+ {
101
+ env: "ARCA_AGENT",
102
+ name: "agent"
103
+ },
104
+ {
105
+ env: "AI_AGENT",
106
+ name: "agent"
107
+ },
108
+ {
109
+ env: "CLAUDECODE",
110
+ name: "claude-code"
111
+ },
112
+ {
113
+ env: "CLAUDE_CODE_ENTRYPOINT",
114
+ name: "claude-code"
115
+ },
116
+ {
117
+ env: "CURSOR_AGENT",
118
+ name: "cursor"
119
+ },
120
+ {
121
+ env: "CODEX_SANDBOX",
122
+ name: "codex"
123
+ },
124
+ {
125
+ env: "CODEX_THREAD_ID",
126
+ name: "codex"
127
+ },
128
+ {
129
+ env: "GEMINI_CLI",
130
+ name: "gemini-cli"
131
+ },
132
+ {
133
+ env: "OPENCODE",
134
+ name: "opencode"
135
+ },
136
+ {
137
+ env: "AUGMENT_AGENT",
138
+ name: "augment"
139
+ },
140
+ {
141
+ env: "CLINE_ACTIVE",
142
+ name: "cline"
143
+ },
144
+ {
145
+ env: "GOOSE_TERMINAL",
146
+ name: "goose"
147
+ },
148
+ {
149
+ env: "REPL_ID",
150
+ name: "replit"
151
+ }
152
+ ];
153
+ const FALSY = /* @__PURE__ */ new Set([
154
+ "",
155
+ "0",
156
+ "false",
157
+ "no",
158
+ "off"
159
+ ]);
160
+ function detectAgent(env, extraMarkers = []) {
161
+ const markers = [...AGENT_MARKERS, ...extraMarkers.map((e) => ({
162
+ env: e,
163
+ name: "agent"
164
+ }))];
165
+ for (const m of markers) {
166
+ const v = env[m.env];
167
+ if (v === void 0 || FALSY.has(v.trim().toLowerCase())) continue;
168
+ let name = m.name;
169
+ if ((m.env === "AI_AGENT" || m.env === "ARCA_AGENT") && v !== "1" && v.toLowerCase() !== "true") name = v.split(/[_\s]/)[0] || m.name;
170
+ return {
171
+ detected: true,
172
+ name,
173
+ via: m.env
174
+ };
175
+ }
176
+ return { detected: false };
177
+ }
178
+ const CI_VARS = [
179
+ "CI",
180
+ "CONTINUOUS_INTEGRATION",
181
+ "BUILD_NUMBER",
182
+ "GITHUB_ACTIONS",
183
+ "GITLAB_CI",
184
+ "VERCEL",
185
+ "NETLIFY",
186
+ "BUILDKITE",
187
+ "CIRCLECI",
188
+ "TF_BUILD",
189
+ "CODEBUILD_BUILD_ID",
190
+ "BITBUCKET_BUILD_NUMBER",
191
+ "CF_PAGES",
192
+ "RENDER",
193
+ "RAILWAY_ENVIRONMENT"
194
+ ];
195
+ function detectCI(env) {
196
+ return CI_VARS.some((k) => env[k] !== void 0 && !FALSY.has(env[k].trim().toLowerCase()));
197
+ }
198
+ function buildContext(io, extraMarkers = []) {
199
+ return {
200
+ agent: detectAgent(io.env, extraMarkers),
201
+ interactiveOutput: io.stdout.isTTY && io.stderr.isTTY,
202
+ stdinIsTTY: io.stdinIsTTY,
203
+ ci: detectCI(io.env),
204
+ activeProject: io.env.ARCA_ACTIVE || null
205
+ };
206
+ }
207
+ /**
208
+ * Checks that this `arca run` was started by a package.json script: the package manager's
209
+ * lifecycle vars must match the script in package.json, the script must contain `arca run`,
210
+ * and the command arca was asked to run must be the one written in the script (extra
211
+ * arguments appended by `npm run x -- …` are allowed).
212
+ */
213
+ function verifyScript(io, command) {
214
+ const name = io.env.npm_lifecycle_event;
215
+ const script = io.env.npm_lifecycle_script;
216
+ if (!name || script === void 0) return { kind: "adhoc" };
217
+ const pkgPath = findPackageJson(io);
218
+ if (!pkgPath) return {
219
+ kind: "script",
220
+ name,
221
+ verified: false,
222
+ reason: "no package.json found"
223
+ };
224
+ let scripts = {};
225
+ try {
226
+ scripts = JSON.parse(readFileSync(pkgPath, "utf8")).scripts ?? {};
227
+ } catch {
228
+ return {
229
+ kind: "script",
230
+ name,
231
+ verified: false,
232
+ reason: "package.json is not valid JSON"
233
+ };
234
+ }
235
+ if (!Object.hasOwn(scripts, name)) return { kind: "adhoc" };
236
+ if (scripts[name] !== script) return {
237
+ kind: "script",
238
+ name,
239
+ verified: false,
240
+ reason: `the "${name}" script in package.json doesn't match what the package manager ran`
241
+ };
242
+ const declared = arcaRunCommands(script);
243
+ if (declared.length === 0) return {
244
+ kind: "script",
245
+ name,
246
+ verified: false,
247
+ reason: `the "${name}" script doesn't use \`arca run\``
248
+ };
249
+ if (!declared.some((cmd) => cmd.length > 0 && cmd.length <= command.length && cmd.every((t, i) => sameToken(t, command[i])))) return {
250
+ kind: "script",
251
+ name,
252
+ verified: false,
253
+ reason: `the command doesn't match the "${name}" script`
254
+ };
255
+ return {
256
+ kind: "script",
257
+ name,
258
+ verified: true
259
+ };
260
+ }
261
+ function sameToken(a, b) {
262
+ return a === b;
263
+ }
264
+ function findPackageJson(io) {
265
+ const fromEnv = io.env.npm_package_json;
266
+ if (fromEnv && existsSync(fromEnv)) return fromEnv;
267
+ let dir = resolve(io.cwd);
268
+ for (;;) {
269
+ const p = join(dir, "package.json");
270
+ if (existsSync(p)) return p;
271
+ const parent = dirname(dir);
272
+ if (parent === dir) return null;
273
+ dir = parent;
274
+ }
275
+ }
276
+ /** The commands after `arca run [opts] --` in a script, one per `arca run` occurrence. */
277
+ function arcaRunCommands(script) {
278
+ const out = [];
279
+ for (const segment of splitShell(script)) {
280
+ const tokens = tokenize(segment);
281
+ for (let i = 0; i < tokens.length - 1; i++) {
282
+ if (!(/(^|[\\/])arca(\.cmd|\.js|\.mjs)?$/.test(tokens[i]) && tokens[i + 1] === "run")) continue;
283
+ out.push(commandAfterRunOptions(tokens.slice(i + 2)));
284
+ }
285
+ }
286
+ return out;
287
+ }
288
+ /** Skips arca's own options and returns the command (matches the CLI's parsing). */
289
+ function commandAfterRunOptions(tokens) {
290
+ let i = 0;
291
+ while (i < tokens.length) {
292
+ const t = tokens[i];
293
+ if (t === "--") return tokens.slice(i + 1);
294
+ if (!t.startsWith("-")) return tokens.slice(i);
295
+ i += RUN_OPTIONS_WITH_VALUE.has(t) ? 2 : 1;
296
+ }
297
+ return [];
298
+ }
299
+ const RUN_OPTIONS_WITH_VALUE = /* @__PURE__ */ new Set([
300
+ "--only",
301
+ "--project-dir",
302
+ "--profile",
303
+ "-p"
304
+ ]);
305
+ /** Splits a script on && || ; | (outside quotes). */
306
+ function splitShell(script) {
307
+ const parts = [];
308
+ let cur = "";
309
+ let quote = null;
310
+ for (let i = 0; i < script.length; i++) {
311
+ const c = script[i];
312
+ if (quote) {
313
+ if (c === quote) quote = null;
314
+ cur += c;
315
+ continue;
316
+ }
317
+ if (c === "\"" || c === "'") {
318
+ quote = c;
319
+ cur += c;
320
+ continue;
321
+ }
322
+ if (c === ";" || c === "|" || c === "&") {
323
+ parts.push(cur);
324
+ cur = "";
325
+ if (script[i + 1] === c) i++;
326
+ continue;
327
+ }
328
+ cur += c;
329
+ }
330
+ parts.push(cur);
331
+ return parts;
332
+ }
333
+ function tokenize(s) {
334
+ const tokens = [];
335
+ let cur = "";
336
+ let has = false;
337
+ let quote = null;
338
+ for (const c of s) {
339
+ if (quote) {
340
+ if (c === quote) quote = null;
341
+ else cur += c;
342
+ continue;
343
+ }
344
+ if (c === "\"" || c === "'") {
345
+ quote = c;
346
+ has = true;
347
+ continue;
348
+ }
349
+ if (/\s/.test(c)) {
350
+ if (has || cur) tokens.push(cur);
351
+ cur = "";
352
+ has = false;
353
+ continue;
354
+ }
355
+ cur += c;
356
+ }
357
+ if (has || cur) tokens.push(cur);
358
+ return tokens;
359
+ }
360
+ //#endregion
361
+ //#region src/errors.ts
362
+ /** An expected failure: printed without a stack trace, exits with `code`. */
363
+ var ArcaError = class extends Error {
364
+ code;
365
+ constructor(message, code = 1) {
366
+ super(message);
367
+ this.code = code;
368
+ this.name = "ArcaError";
369
+ }
370
+ };
371
+ //#endregion
372
+ //#region src/config.ts
373
+ const CONFIG_FILE = "arca.json";
374
+ const SCHEMA_REF = "./node_modules/@bremmdev/arca-env/schema.json";
375
+ /** Profiles aren't implemented yet. Everything uses this single set internally. */
376
+ const DEFAULT_PROFILE = "default";
377
+ const VAR_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
378
+ function isSensitive(decl) {
379
+ return decl?.sensitive !== false;
380
+ }
381
+ function newProjectId() {
382
+ const alphabet = "abcdefghijklmnopqrstuvwxyz234567";
383
+ const bytes = randomBytes(16);
384
+ let id = "arc_";
385
+ for (const b of bytes) id += alphabet[b % 32];
386
+ return id;
387
+ }
388
+ /** Walks up from `cwd` to the nearest arca.json. */
389
+ function findConfigFile(cwd) {
390
+ let dir = resolve(cwd);
391
+ for (;;) {
392
+ const file = join(dir, CONFIG_FILE);
393
+ if (existsSync(file)) return file;
394
+ const parent = dirname(dir);
395
+ if (parent === dir) return null;
396
+ dir = parent;
397
+ }
398
+ }
399
+ function loadProject(cwd) {
400
+ const file = findConfigFile(cwd);
401
+ if (!file) return null;
402
+ return parseProject(readFileSync(file, "utf8"), file);
403
+ }
404
+ function parseProject(text, file) {
405
+ let raw;
406
+ try {
407
+ raw = JSON.parse(text);
408
+ } catch (e) {
409
+ throw new ArcaError(`arca: ${file} is not valid JSON (${e.message}).`);
410
+ }
411
+ const warnings = [];
412
+ const fail = (msg) => {
413
+ throw new ArcaError(`arca: invalid ${CONFIG_FILE}: ${msg}\n in ${file}`);
414
+ };
415
+ if (!isObject(raw)) fail("expected an object");
416
+ const o = raw;
417
+ if (typeof o.id !== "string" || !/^arc_[a-z2-7]{8,}$/.test(o.id)) fail("\"id\" must be an arca project ID like \"arc_7k2m9q4x8w1z3p5n\". Run `npx arca init` to create one.");
418
+ const name = o.name === void 0 ? dirnameName(file) : o.name;
419
+ if (typeof name !== "string") fail("\"name\" must be a string");
420
+ const envFiles = o.envFiles ?? [];
421
+ if (!Array.isArray(envFiles) || envFiles.some((f) => typeof f !== "string")) fail("\"envFiles\" must be an array of file names");
422
+ const lg = o.leakGuard ?? {};
423
+ if (!isObject(lg)) fail("\"leakGuard\" must be an object");
424
+ const ignore = lg.ignore ?? [];
425
+ if (!Array.isArray(ignore) || ignore.some((k) => typeof k !== "string")) fail("\"leakGuard.ignore\" must be an array of variable names");
426
+ const vars = {};
427
+ const rawVars = o.vars ?? {};
428
+ if (!isObject(rawVars)) fail("\"vars\" must be an object");
429
+ for (const [key, value] of Object.entries(rawVars)) {
430
+ if (!VAR_NAME.test(key)) fail(`"${key}" is not a valid environment variable name`);
431
+ if (!isObject(value)) fail(`vars.${key} must be an object (use {} for no options)`);
432
+ vars[key] = parseDecl(key, value, fail);
433
+ }
434
+ for (const key of ["profiles", "defaultProfile"]) if (key in o) warnings.push(`"${key}" in ${CONFIG_FILE} is ignored: profiles aren't available in this version yet.`);
435
+ const known = /* @__PURE__ */ new Set([
436
+ "$schema",
437
+ "id",
438
+ "name",
439
+ "envFiles",
440
+ "leakGuard",
441
+ "vars",
442
+ "profiles",
443
+ "defaultProfile"
444
+ ]);
445
+ for (const key of Object.keys(o)) if (!known.has(key)) warnings.push(`Unknown field "${key}" in ${CONFIG_FILE} is ignored.`);
446
+ return {
447
+ root: dirname(file),
448
+ file,
449
+ config: {
450
+ id: o.id,
451
+ name,
452
+ envFiles,
453
+ leakGuard: { ignore },
454
+ vars
455
+ },
456
+ warnings
457
+ };
458
+ }
459
+ function parseDecl(key, v, fail) {
460
+ const d = {};
461
+ for (const field of [
462
+ "description",
463
+ "example",
464
+ "docs"
465
+ ]) if (v[field] !== void 0) {
466
+ if (typeof v[field] !== "string") fail(`vars.${key}.${field} must be a string`);
467
+ d[field] = v[field];
468
+ }
469
+ for (const field of ["required", "sensitive"]) if (v[field] !== void 0) {
470
+ if (typeof v[field] !== "boolean") fail(`vars.${key}.${field} must be true or false`);
471
+ d[field] = v[field];
472
+ }
473
+ if ("default" in v || "value" in v) fail(`vars.${key} has a value. ${CONFIG_FILE} holds names only: put secrets in arca (\`npx arca set ${key}\`) and config in .env.`);
474
+ if (v.generate !== void 0) {
475
+ const g = v.generate;
476
+ if (!isObject(g)) fail(`vars.${key}.generate must be an object like { "bytes": 32 }`);
477
+ const gen = g;
478
+ const bytes = gen.bytes ?? 32;
479
+ const encoding = gen.encoding ?? "base64url";
480
+ if (typeof bytes !== "number" || !Number.isInteger(bytes) || bytes < 8 || bytes > 1024) fail(`vars.${key}.generate.bytes must be an integer from 8 to 1024`);
481
+ if (encoding !== "base64url" && encoding !== "base64" && encoding !== "hex") fail(`vars.${key}.generate.encoding must be "base64url", "base64" or "hex"`);
482
+ d.generate = {
483
+ bytes,
484
+ encoding
485
+ };
486
+ }
487
+ return d;
488
+ }
489
+ function isObject(v) {
490
+ return typeof v === "object" && v !== null && !Array.isArray(v);
491
+ }
492
+ function dirnameName(file) {
493
+ return dirname(file).split(/[\\/]/).pop() ?? "project";
494
+ }
495
+ /**
496
+ * Edits arca.json as raw JSON, so fields arca doesn't know about (and their order) survive.
497
+ * Comments aren't supported because the file is plain JSON.
498
+ */
499
+ function updateConfigFile(file, mutate) {
500
+ const raw = JSON.parse(readFileSync(file, "utf8"));
501
+ mutate(raw);
502
+ writeFileSync(file, JSON.stringify(raw, null, 2) + "\n");
503
+ }
504
+ function declareVars(file, decls) {
505
+ updateConfigFile(file, (raw) => {
506
+ const vars = isObject(raw.vars) ? raw.vars : {};
507
+ for (const [name, decl] of Object.entries(decls)) if (!(name in vars)) vars[name] = decl;
508
+ raw.vars = vars;
509
+ });
510
+ }
511
+ function newConfig(name) {
512
+ return {
513
+ $schema: SCHEMA_REF,
514
+ id: newProjectId(),
515
+ name,
516
+ vars: {}
517
+ };
518
+ }
519
+ //#endregion
520
+ //#region src/envfiles.ts
521
+ const LINE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_.-]*)\s*[=:]\s?(.*)$/;
522
+ function parseEnvEntries(text) {
523
+ const lines = text.replace(/\r\n?/g, "\n").split("\n");
524
+ const entries = [];
525
+ for (let i = 0; i < lines.length; i++) {
526
+ const line = lines[i];
527
+ const m = LINE.exec(line);
528
+ if (!m) continue;
529
+ const key = m[1];
530
+ let rest = m[2].trimStart();
531
+ const start = i;
532
+ let value;
533
+ const quote = rest[0];
534
+ if (quote === "\"" || quote === "'" || quote === "`") {
535
+ let body = rest.slice(1);
536
+ let close = findClose(body, quote);
537
+ while (close === -1 && i + 1 < lines.length) {
538
+ i++;
539
+ body += "\n" + lines[i];
540
+ close = findClose(body, quote);
541
+ }
542
+ if (close === -1) {
543
+ i = start;
544
+ value = stripInlineComment(rest);
545
+ } else {
546
+ value = body.slice(0, close);
547
+ if (quote === "\"") value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\"/g, "\"");
548
+ }
549
+ } else value = stripInlineComment(rest);
550
+ entries.push({
551
+ key,
552
+ value,
553
+ startLine: start,
554
+ endLine: i
555
+ });
556
+ }
557
+ return entries;
558
+ }
559
+ function findClose(body, quote) {
560
+ for (let i = 0; i < body.length; i++) {
561
+ if (body[i] === "\\" && quote === "\"") {
562
+ i++;
563
+ continue;
564
+ }
565
+ if (body[i] === quote) return i;
566
+ }
567
+ return -1;
568
+ }
569
+ /** Like dotenv (used by Next.js and Vite): an unquoted value ends at the first `#`. */
570
+ function stripInlineComment(raw) {
571
+ const hash = raw.indexOf("#");
572
+ return (hash === -1 ? raw : raw.slice(0, hash)).trim();
573
+ }
574
+ function parseEnv(text) {
575
+ const out = {};
576
+ for (const e of parseEnvEntries(text)) out[e.key] = e.value;
577
+ return out;
578
+ }
579
+ function readEnvFile(path, name = path) {
580
+ if (!existsSync(path)) return null;
581
+ try {
582
+ if (!statSync(path).isFile()) return null;
583
+ return {
584
+ path,
585
+ name,
586
+ entries: parseEnvEntries(readFileSync(path, "utf8"))
587
+ };
588
+ } catch {
589
+ return null;
590
+ }
591
+ }
592
+ const DOTENV_NAME = /^\.env(\..+)?$/;
593
+ /** Every `.env*` file in the given directories (`.env`, `.env.local`, `.env.example`, …). */
594
+ function findEnvFiles(dirs) {
595
+ const seen = /* @__PURE__ */ new Set();
596
+ const out = [];
597
+ for (const dir of dirs) {
598
+ let names;
599
+ try {
600
+ names = readdirSync(dir);
601
+ } catch {
602
+ continue;
603
+ }
604
+ for (const n of names.sort()) {
605
+ if (!DOTENV_NAME.test(n)) continue;
606
+ const path = resolve(dir, n);
607
+ if (seen.has(path)) continue;
608
+ seen.add(path);
609
+ const f = readEnvFile(path, n);
610
+ if (f) out.push(f);
611
+ }
612
+ }
613
+ return out;
614
+ }
615
+ /** Files a framework loads in development, lowest precedence first. */
616
+ const DEV_FILES = [
617
+ ".env",
618
+ ".env.development",
619
+ ".env.local",
620
+ ".env.development.local"
621
+ ];
622
+ /** Files that hold examples, not real values. Import skips them. */
623
+ function isTemplateFile(name) {
624
+ return /\.(example|sample|template|dist|defaults)$/i.test(name);
625
+ }
626
+ /** Merged values of the given files (later files win). */
627
+ function loadEnvFiles(root, files) {
628
+ const out = {};
629
+ for (const f of files) {
630
+ const file = readEnvFile(join(root, f), f);
631
+ if (!file) continue;
632
+ for (const e of file.entries) out[e.key] = e.value;
633
+ }
634
+ return out;
635
+ }
636
+ /**
637
+ * Removes the entries for `keys` from a dotenv file. Comments and other lines stay.
638
+ * Deletes the file only if nothing but whitespace is left. Returns what happened.
639
+ */
640
+ function removeEntries(path, keys) {
641
+ const text = readFileSync(path, "utf8");
642
+ const eol = text.includes("\r\n") ? "\r\n" : "\n";
643
+ const lines = text.replace(/\r\n?/g, "\n").split("\n");
644
+ const drop = /* @__PURE__ */ new Set();
645
+ for (const e of parseEnvEntries(text)) {
646
+ if (!keys.has(e.key)) continue;
647
+ for (let i = e.startLine; i <= e.endLine; i++) drop.add(i);
648
+ }
649
+ if (drop.size === 0) return "unchanged";
650
+ const out = lines.filter((_, i) => !drop.has(i)).join(eol);
651
+ if (out.trim() === "") {
652
+ rmSync(path);
653
+ return "deleted";
654
+ }
655
+ writeFileSync(path, out);
656
+ return "edited";
657
+ }
658
+ //#endregion
659
+ //#region src/leakguard.ts
660
+ const PUBLIC_PREFIXES = [
661
+ "NEXT_PUBLIC_",
662
+ "VITE_",
663
+ "PUBLIC_",
664
+ "NUXT_PUBLIC_",
665
+ "EXPO_PUBLIC_",
666
+ "REACT_APP_",
667
+ "GATSBY_",
668
+ "STORYBOOK_"
669
+ ];
670
+ const SECRET_NAME = /(SECRET|TOKEN|PASSWORD|PASSWD|PASSPHRASE|PRIVATE|API_?KEY|_KEY$|^KEY$|CREDENTIAL|DSN|AUTH|SALT|SIGNING|WEBHOOK)/i;
671
+ const KNOWN_PREFIXES = /^(sk_live_|sk_test_|rk_live_|rk_test_|whsec_|ghp_|gho_|ghs_|github_pat_|glpat-|xox[abpr]-|AKIA[0-9A-Z]{12}|ASIA[0-9A-Z]{12}|AIza[0-9A-Za-z_-]{20}|sk-[A-Za-z0-9_-]{20}|sk-ant-|re_[A-Za-z0-9]{16}|SG\.|npm_[A-Za-z0-9]{20}|-----BEGIN)/;
672
+ const CREDENTIAL_URL = /^[a-z][a-z0-9+.-]*:\/\/[^:@/\s]*:[^@/\s]+@/i;
673
+ const JWT = /^eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}$/;
674
+ function isPublicName(key) {
675
+ return PUBLIC_PREFIXES.some((p) => key.startsWith(p));
676
+ }
677
+ function shannonEntropy(s) {
678
+ const counts = /* @__PURE__ */ new Map();
679
+ for (const c of s) counts.set(c, (counts.get(c) ?? 0) + 1);
680
+ let h = 0;
681
+ for (const n of counts.values()) {
682
+ const p = n / s.length;
683
+ h -= p * Math.log2(p);
684
+ }
685
+ return h;
686
+ }
687
+ /** Heuristic: does this .env entry look like a secret? Returns the reason, or null. */
688
+ function secretReason(key, value) {
689
+ const v = value.trim();
690
+ if (v === "") return null;
691
+ if (CREDENTIAL_URL.test(v)) return "a URL with a password";
692
+ if (KNOWN_PREFIXES.test(v)) return "a known secret format";
693
+ if (JWT.test(v)) return "a token (JWT)";
694
+ if (v.length >= 24 && !/\s/.test(v) && !/^https?:\/\//i.test(v) && shannonEntropy(v) > 4) return "a long random-looking value";
695
+ if (!isPublicName(key) && SECRET_NAME.test(key) && plausibleSecretValue(v)) return "a secret-like name";
696
+ return null;
697
+ }
698
+ function plausibleSecretValue(v) {
699
+ if (v.length < 8) return false;
700
+ if (/^(true|false|yes|no|on|off|null|undefined)$/i.test(v)) return false;
701
+ if (/^-?\d+(\.\d+)?$/.test(v)) return false;
702
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(v) && !CREDENTIAL_URL.test(v)) return false;
703
+ if (/^(your|change|replace|xxx|todo|<)/i.test(v)) return false;
704
+ return true;
705
+ }
706
+ function scanEnvFiles(project, files, secrets) {
707
+ const ignore = new Set(project.config.leakGuard.ignore);
708
+ const needles = secrets.flatMap((s) => needlesFor(s).map((n) => ({
709
+ ...n,
710
+ name: s.name
711
+ })));
712
+ const findings = [];
713
+ for (const file of files) {
714
+ const name = relative(project.root, file.path) || file.name;
715
+ for (const entry of file.entries) {
716
+ if (entry.value.trim() === "") continue;
717
+ const hit = needles.find((n) => entry.value.includes(n.text));
718
+ if (hit) {
719
+ const detail = hit.name === entry.key ? `${entry.key} is stored in arca but also in plaintext in ${name}` : `${entry.key} in ${name} contains the value of ${hit.name}, which is stored in arca`;
720
+ findings.push({
721
+ file: name,
722
+ key: entry.key,
723
+ rule: "stored",
724
+ detail,
725
+ tracked: false
726
+ });
727
+ continue;
728
+ }
729
+ if (ignore.has(entry.key)) continue;
730
+ const decl = project.config.vars[entry.key];
731
+ if (decl && isSensitive(decl)) {
732
+ findings.push({
733
+ file: name,
734
+ key: entry.key,
735
+ rule: "declared",
736
+ detail: `${entry.key} is declared sensitive in arca.json but set in ${name}`,
737
+ tracked: false
738
+ });
739
+ continue;
740
+ }
741
+ if (!decl) {
742
+ const reason = secretReason(entry.key, entry.value);
743
+ if (reason) findings.push({
744
+ file: name,
745
+ key: entry.key,
746
+ rule: "heuristic",
747
+ detail: `${entry.key} in ${name} looks like a secret (${reason})`,
748
+ tracked: false
749
+ });
750
+ }
751
+ }
752
+ }
753
+ markTracked(project.root, findings);
754
+ return findings;
755
+ }
756
+ /** Escalates findings in files tracked by git. One git call, only when there are findings. */
757
+ function markTracked(root, findings) {
758
+ if (findings.length === 0) return;
759
+ const files = [...new Set(findings.map((f) => f.file))];
760
+ try {
761
+ const out = execFileSync("git", [
762
+ "ls-files",
763
+ "--",
764
+ ...files
765
+ ], {
766
+ cwd: root,
767
+ encoding: "utf8",
768
+ stdio: [
769
+ "ignore",
770
+ "pipe",
771
+ "ignore"
772
+ ],
773
+ timeout: 3e3
774
+ });
775
+ const tracked = new Set(out.split(/\r?\n/).filter(Boolean).map((f) => f.replace(/\//g, "\\")));
776
+ for (const f of findings) if (tracked.has(f.file) || tracked.has(f.file.replace(/\\/g, "/")) || tracked.has(f.file.replace(/\//g, "\\"))) f.tracked = true;
777
+ } catch {}
778
+ }
779
+ function summarizeFindings(findings) {
780
+ const files = [...new Set(findings.map((f) => f.file))];
781
+ const tracked = findings.some((f) => f.tracked);
782
+ return `arca: ${findings.length === 1 ? "1 possible secret" : `${findings.length} possible secrets`} in ${files.join(", ")}${tracked ? " (committed to git!)" : ""}. Run \`npx arca doctor\` for details.`;
783
+ }
784
+ //#endregion
785
+ //#region src/messages.ts
786
+ function detected(agent) {
787
+ return `detected: ${agent.name} via ${agent.via}`;
788
+ }
789
+ function agentRevealMessage(agent) {
790
+ return [
791
+ `arca: secret values cannot be shown to AI agents (${detected(agent)}).`,
792
+ "You don't need the values. Secrets are injected automatically when you run package",
793
+ "scripts, for example `npm run dev` or `npm test`. Their output is masked as [arca:NAME].",
794
+ " • See which variables exist: npx arca list",
795
+ " • Check that they are set: npx arca check",
796
+ " • A value is missing or wrong: ask the user to run `npx arca set NAME`",
797
+ " • Need a new random secret: ask the user to run `npx arca set NAME --generate`"
798
+ ].join("\n");
799
+ }
800
+ function agentWriteMessage(agent, command) {
801
+ return [
802
+ `arca: only a human can change secret values (${detected(agent)}).`,
803
+ "Ask the user to run this in their own terminal:",
804
+ ` ${command}`,
805
+ "If this is a new variable, you can declare it in arca.json under \"vars\" first,",
806
+ "so `npx arca check` and `npx arca setup` know about it."
807
+ ].join("\n");
808
+ }
809
+ function agentAdhocMessage(agent, command, invocation) {
810
+ const why = invocation.kind === "script" && !invocation.verified && invocation.reason ? `\n(Not accepted as a package script: ${invocation.reason}.)` : "";
811
+ return [
812
+ `arca: AI agents can only use secrets through package.json scripts (${detected(agent)}).${why}`,
813
+ `Refused: arca run -- ${command}`,
814
+ "Run the command through a package.json script that uses `arca run`, e.g. `npm run dev`.",
815
+ "If no script exists for what you need, ask the user to add one."
816
+ ].join("\n");
817
+ }
818
+ function agentDeniedMessage(agent) {
819
+ return [`arca: the user has turned off secrets for AI agents in this project (${detected(agent)}).`, "Ask the user to run the command themselves."].join("\n");
820
+ }
821
+ function notInteractiveMessage(command) {
822
+ return ["arca: this needs a person at an interactive terminal (stdin and stdout must be a terminal).", `Run it yourself in a terminal: ${command}`].join("\n");
823
+ }
824
+ function agentsSnippet() {
825
+ return `## Secrets (arca)
826
+
827
+ Secrets are stored outside the repo with arca and injected when commands run.
828
+ \`.env\` files in this project contain only non-sensitive config, which you may read and edit.
829
+
830
+ - Run the app, tests and tools through package scripts (\`npm run dev\`, \`npm test\`, …),
831
+ which wrap commands in \`arca run\`. Ad-hoc \`arca run -- <cmd>\` is refused for agents.
832
+ Output is always masked: secrets appear as \`[arca:NAME]\`.
833
+ - \`npx arca list\` shows which variables exist. \`npx arca check\` verifies required ones are set.
834
+ - Do not try to read secret values (\`npx arca reveal\`, printing \`process.env\`, reading the
835
+ arca store).
836
+ - Only the user can set or change values. If a value is missing or wrong, ask the user
837
+ to run \`npx arca set NAME\` (or \`npx arca set NAME --generate\` for a random secret).
838
+ - To add a secret: declare it in \`arca.json\` under \`vars\`, then ask the user to set it.
839
+ To add non-sensitive config: put it in \`.env\` and declare it with \`"sensitive": false\`.
840
+ - Never put secrets (keys, tokens, passwords, connection strings with credentials) in
841
+ \`.env\` files. Don't change URLs that receive credentials. Those are declared sensitive
842
+ and live in arca.
843
+ - Don't work around arca: don't unset or change agent environment variables (such as
844
+ \`CLAUDECODE\` or \`AI_AGENT\`), don't add scripts that print \`process.env\`, and don't read
845
+ the arca store or the OS keychain. If arca blocks you, ask the user.
846
+ `;
847
+ }
848
+ //#endregion
849
+ //#region src/policy.ts
850
+ function decideRun(ctx, invocation, policy, displayCommand) {
851
+ if (!ctx.agent.detected) return { allow: true };
852
+ if (policy === "deny") return {
853
+ allow: false,
854
+ message: agentDeniedMessage(ctx.agent)
855
+ };
856
+ if (invocation.kind === "script" && invocation.verified) return { allow: true };
857
+ return {
858
+ allow: false,
859
+ message: agentAdhocMessage(ctx.agent, displayCommand, invocation)
860
+ };
861
+ }
862
+ /**
863
+ * The human gate, required before any value is shown or changed:
864
+ * no agent marker, and a real terminal. If an agent marker is present but the session is
865
+ * fully interactive (a human in an IDE terminal with leaked markers), a typed challenge
866
+ * lets the human through.
867
+ */
868
+ async function humanGate(io, ctx, opts) {
869
+ const stdinOk = ctx.stdinIsTTY || Boolean(opts.allowPipedStdin);
870
+ const interactive = ctx.interactiveOutput && stdinOk;
871
+ if (ctx.agent.detected) {
872
+ if (interactive && ctx.stdinIsTTY) {
873
+ await challenge(io, ctx);
874
+ return;
875
+ }
876
+ throw new ArcaError(opts.kind === "reveal" ? agentRevealMessage(ctx.agent) : agentWriteMessage(ctx.agent, opts.command));
877
+ }
878
+ if (!interactive) throw new ArcaError(notInteractiveMessage(opts.command));
879
+ }
880
+ async function challenge(io, ctx) {
881
+ const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
882
+ let code = "";
883
+ for (let i = 0; i < 4; i++) code += alphabet[randomInt(32)];
884
+ io.stderr.write(`arca: this terminal looks like it's driven by an AI agent (${ctx.agent.name} via ${ctx.agent.via}).\nThis confirmation must be completed by a human. AI agents must not answer this prompt.
885
+ Type this code to continue: ${code}\n`);
886
+ const answer = await io.prompts.text("Code");
887
+ if (answer === null || answer.trim().toUpperCase() !== code) throw new ArcaError("arca: the code did not match. Nothing was changed.");
888
+ }
889
+ //#endregion
890
+ //#region src/runner.ts
891
+ async function runCommand(s, opts) {
892
+ const { io, ctx } = s;
893
+ const command = opts.command;
894
+ if (command.length === 0) throw new ArcaError("arca: nothing to run. Usage: arca run -- <command> [args…]", 2);
895
+ const project = s.project;
896
+ if (!project) {
897
+ if (!ctx.ci) s.note(s.err("dim", "arca: no arca.json found, running without secrets. Set up arca with `npx arca init`."));
898
+ return spawnChild(s, command, io.env, null);
899
+ }
900
+ s.warnConfig();
901
+ if (ctx.activeProject === project.config.id) return spawnChild(s, command, io.env, null);
902
+ const display = command.join(" ");
903
+ if (ctx.agent.detected) {
904
+ const decision = decideRun(ctx, verifyScript(io, command), (await s.policy()).agentRun, display);
905
+ if (!decision.allow) throw new ArcaError(decision.message);
906
+ }
907
+ const envFileValues = loadEnvFiles(project.root, project.config.envFiles);
908
+ if (!s.hasVault()) {
909
+ const env = {
910
+ ...envFileValues,
911
+ ...io.env
912
+ };
913
+ const missing = missingRequired(s, project, env);
914
+ if (ctx.ci || opts.passthrough) {
915
+ if (missing.length) throw new ArcaError(missingMessage(project, missing, true));
916
+ return spawnChild(s, command, env, null);
917
+ }
918
+ if (missing.filter((n) => isSensitive(project.config.vars[n])).length) throw new ArcaError(missingMessage(project, missing, false));
919
+ s.note(s.err("dim", `arca: no secrets stored for ${project.config.name} on this machine yet. Running without them.`));
920
+ return spawnChild(s, command, env, null);
921
+ }
922
+ const vault = await s.readVault();
923
+ const only = opts.only ? new Set(opts.only) : null;
924
+ const vaultValues = {};
925
+ for (const [name, v] of Object.entries(vault.vars)) if (!only || only.has(name)) vaultValues[name] = v.value;
926
+ if (only) {
927
+ const unknown = [...only].filter((n) => !(n in vault.vars));
928
+ if (unknown.length) s.note(s.err("yellow", `arca: --only names not stored in arca: ${unknown.join(", ")}`));
929
+ }
930
+ const overridden = Object.keys(vaultValues).filter((k) => io.env[k] !== void 0 && io.env[k] !== vaultValues[k]);
931
+ const env = opts.noOverride ? {
932
+ ...envFileValues,
933
+ ...vaultValues,
934
+ ...io.env
935
+ } : {
936
+ ...envFileValues,
937
+ ...io.env,
938
+ ...vaultValues
939
+ };
940
+ if (overridden.length && !opts.noOverride && !s.quiet) {
941
+ const names = overridden.join(", ");
942
+ const verb = overridden.length === 1 ? "overrides a different value" : "override different values";
943
+ s.note(s.err("dim", `arca: ${names} from arca ${verb} inherited from your shell (use --no-override to keep the shell's).`));
944
+ }
945
+ const missing = missingRequired(s, project, env);
946
+ if (missing.length) throw new ArcaError(missingMessage(project, missing, false));
947
+ const secrets = s.secrets(vault);
948
+ if (!s.quiet) {
949
+ const findings = scanEnvFiles(project, s.envFilesForGuard(), secrets);
950
+ if (findings.length) s.note(s.err("yellow", summarizeFindings(findings)));
951
+ }
952
+ env.ARCA_ACTIVE = project.config.id;
953
+ env.ARCA_PROJECT = project.config.name;
954
+ delete env.ARCA_PASSPHRASE;
955
+ addConsoleGuard(env, secrets.map((x) => x.name));
956
+ return spawnChild(s, command, env, secrets.flatMap(needlesFor));
957
+ }
958
+ /**
959
+ * Preloads the console guard into every Node process the command starts, so values are
960
+ * masked before in-process console wrappers (e.g. dev tools that forward server logs to the
961
+ * browser) can see them. Only names are passed; the guard reads values from its own env.
962
+ */
963
+ function addConsoleGuard(env, names, guardFile = defaultGuardFile()) {
964
+ if (names.length === 0 || !guardFile) return;
965
+ env.ARCA_MASK = names.join(",");
966
+ const flag = `--import=${pathToFileURL(guardFile).href}`;
967
+ const current = env.NODE_OPTIONS ?? "";
968
+ if (!current.includes(flag)) env.NODE_OPTIONS = current ? `${current} ${flag}` : flag;
969
+ }
970
+ function defaultGuardFile() {
971
+ const file = fileURLToPath(new URL("./guard.mjs", import.meta.url));
972
+ return existsSync(file) ? file : null;
973
+ }
974
+ /** Required declared vars that are neither in the env nor in a .env file the framework will load. */
975
+ function missingRequired(s, project, env) {
976
+ const required = Object.entries(project.config.vars).filter(([, d]) => d.required).map(([n]) => n);
977
+ if (required.length === 0) return [];
978
+ let fileKeys = null;
979
+ return required.filter((name) => {
980
+ if (env[name] !== void 0 && env[name] !== "") return false;
981
+ fileKeys ??= new Set(s.envFilesForGuard().filter((f) => !isTemplateFile(f.name)).flatMap((f) => f.entries.filter((e) => e.value !== "").map((e) => e.key)));
982
+ return !fileKeys.has(name);
983
+ });
984
+ }
985
+ function missingMessage(project, missing, ci) {
986
+ const n = missing.length;
987
+ const head = `arca: ${n} required variable${n === 1 ? " is" : "s are"} not set for ${project.config.name}:\n ${missing.join(", ")}`;
988
+ if (ci) return `${head}\nAdd ${n === 1 ? "it" : "them"} to your CI or hosting platform's environment variables.`;
989
+ return `${head}\nRun \`npx arca setup\` to set them. Takes about a minute. (AI agents: ask the user to run it.)`;
990
+ }
991
+ /** Spawns the command. With needles, stdout and stderr are piped through the masker. */
992
+ function spawnChild(s, command, env, needles) {
993
+ const { io } = s;
994
+ const masked = needles !== null;
995
+ if (masked) forwardTerminalHints(s, env);
996
+ const [file, ...args] = command;
997
+ const plain = io.childStdin === "inherit" ? "inherit" : "pipe";
998
+ const child = spawn(file, args, {
999
+ env,
1000
+ cwd: io.cwd,
1001
+ shell: false,
1002
+ stdio: masked ? [
1003
+ io.childStdin,
1004
+ "pipe",
1005
+ "pipe"
1006
+ ] : [
1007
+ io.childStdin,
1008
+ plain,
1009
+ plain
1010
+ ]
1011
+ });
1012
+ return new Promise((resolvePromise, reject) => {
1013
+ const writers = [];
1014
+ if (masked) {
1015
+ const out = createMaskedWriter(new Redactor(needles), (t) => io.stdout.write(t));
1016
+ const err = createMaskedWriter(new Redactor(needles), (t) => io.stderr.write(t));
1017
+ child.stdout.on("data", (c) => out.write(c));
1018
+ child.stderr.on("data", (c) => err.write(c));
1019
+ writers.push(out, err);
1020
+ } else if (io.childStdin === "ignore") {
1021
+ child.stdout?.on("data", (c) => io.stdout.write(c.toString("utf8")));
1022
+ child.stderr?.on("data", (c) => io.stderr.write(c.toString("utf8")));
1023
+ }
1024
+ const cleanups = io.handleSignals ? forwardSignals(child, io) : [];
1025
+ child.on("error", (e) => {
1026
+ cleanups.forEach((c) => c());
1027
+ if (e.code === "ENOENT") reject(new ArcaError(`arca: command not found: ${file}`, 127));
1028
+ else reject(e);
1029
+ });
1030
+ child.on("close", (code, signal) => {
1031
+ cleanups.forEach((c) => c());
1032
+ for (const w of writers) w.end();
1033
+ if (code !== null) resolvePromise(code);
1034
+ else resolvePromise(128 + (signal && constants.signals[signal] || 1));
1035
+ });
1036
+ });
1037
+ }
1038
+ /** The child's output is a pipe, so tell it about colours and the terminal size. */
1039
+ function forwardTerminalHints(s, env) {
1040
+ const { stdout } = s.io;
1041
+ if (stdout.isTTY) {
1042
+ if (env.FORCE_COLOR === void 0 && !env.NO_COLOR) {
1043
+ const depth = stdout.colorDepth ?? 4;
1044
+ env.FORCE_COLOR = depth >= 24 ? "3" : depth >= 8 ? "2" : depth >= 4 ? "1" : "0";
1045
+ }
1046
+ if (stdout.columns && env.COLUMNS === void 0) env.COLUMNS = String(stdout.columns);
1047
+ if (stdout.rows && env.LINES === void 0) env.LINES = String(stdout.rows);
1048
+ }
1049
+ }
1050
+ function forwardSignals(child, io) {
1051
+ const signals = [
1052
+ "SIGINT",
1053
+ "SIGTERM",
1054
+ "SIGHUP"
1055
+ ];
1056
+ if (process.platform === "win32") signals.push("SIGBREAK");
1057
+ const cleanups = [];
1058
+ for (const sig of signals) {
1059
+ const handler = () => {
1060
+ if (sig === "SIGINT" && (io.stdinIsTTY || process.platform === "win32")) return;
1061
+ if (process.platform === "win32" && sig !== "SIGTERM") return;
1062
+ try {
1063
+ child.kill(sig);
1064
+ } catch {}
1065
+ };
1066
+ process.on(sig, handler);
1067
+ cleanups.push(() => process.off(sig, handler));
1068
+ }
1069
+ return cleanups;
1070
+ }
1071
+ function newMasterKey() {
1072
+ return randomBytes(32);
1073
+ }
1074
+ function vaultKey(master, projectId, profile) {
1075
+ return Buffer.from(hkdfSync("sha256", master, projectId, `arca/vault/v1/${profile}`, 32));
1076
+ }
1077
+ function policyKey(master, projectId) {
1078
+ return Buffer.from(hkdfSync("sha256", master, projectId, "arca/policy/v1", 32));
1079
+ }
1080
+ function seal(key, plaintext, aad) {
1081
+ const iv = randomBytes(12);
1082
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
1083
+ cipher.setAAD(Buffer.from(aad, "utf8"));
1084
+ const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
1085
+ return {
1086
+ iv: iv.toString("base64"),
1087
+ tag: cipher.getAuthTag().toString("base64"),
1088
+ ct: ct.toString("base64")
1089
+ };
1090
+ }
1091
+ function open(key, sealed, aad) {
1092
+ try {
1093
+ const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(sealed.iv, "base64"));
1094
+ decipher.setAAD(Buffer.from(aad, "utf8"));
1095
+ decipher.setAuthTag(Buffer.from(sealed.tag, "base64"));
1096
+ return Buffer.concat([decipher.update(Buffer.from(sealed.ct, "base64")), decipher.final()]).toString("utf8");
1097
+ } catch {
1098
+ throw new ArcaError("arca: could not decrypt the vault. The master key in your keychain does not match this store, or the file was modified.");
1099
+ }
1100
+ }
1101
+ const SCRYPT = {
1102
+ N: 2 ** 17,
1103
+ r: 8,
1104
+ p: 1
1105
+ };
1106
+ function scryptKey(passphrase, salt, params) {
1107
+ return scryptSync(passphrase.normalize("NFC"), salt, 32, {
1108
+ ...params,
1109
+ maxmem: 256 * 1024 * 1024
1110
+ });
1111
+ }
1112
+ function wrapMasterKey(master, passphrase, params = SCRYPT) {
1113
+ const salt = randomBytes(16);
1114
+ const sealed = seal(scryptKey(passphrase, salt, params), master.toString("base64"), "arca-master-key:v1");
1115
+ return {
1116
+ format: "arca-master-key",
1117
+ v: 1,
1118
+ kdf: "scrypt",
1119
+ salt: salt.toString("base64"),
1120
+ ...params,
1121
+ ...sealed
1122
+ };
1123
+ }
1124
+ function unwrapMasterKey(wrapped, passphrase) {
1125
+ const key = scryptKey(passphrase, Buffer.from(wrapped.salt, "base64"), wrapped);
1126
+ try {
1127
+ return Buffer.from(open(key, wrapped, "arca-master-key:v1"), "base64");
1128
+ } catch {
1129
+ throw new ArcaError("arca: wrong passphrase.");
1130
+ }
1131
+ }
1132
+ function generateSecret(bytes, encoding) {
1133
+ return randomBytes(bytes).toString(encoding);
1134
+ }
1135
+ //#endregion
1136
+ //#region src/store.ts
1137
+ var Store = class {
1138
+ root;
1139
+ constructor(root) {
1140
+ this.root = root;
1141
+ }
1142
+ projectDir(id) {
1143
+ return join(this.root, "projects", id);
1144
+ }
1145
+ vaultFile(id, profile) {
1146
+ return join(this.projectDir(id), `${profile}.vault`);
1147
+ }
1148
+ hasVault(id, profile) {
1149
+ return existsSync(this.vaultFile(id, profile));
1150
+ }
1151
+ /** True when any vault exists, used to refuse creating a new master key over old data. */
1152
+ hasAnyVault() {
1153
+ const dir = join(this.root, "projects");
1154
+ if (!existsSync(dir)) return false;
1155
+ for (const id of readdirSync(dir)) {
1156
+ const pdir = join(dir, id);
1157
+ try {
1158
+ if (readdirSync(pdir).some((f) => f.endsWith(".vault"))) return true;
1159
+ } catch {}
1160
+ }
1161
+ return false;
1162
+ }
1163
+ projectIds() {
1164
+ const dir = join(this.root, "projects");
1165
+ return existsSync(dir) ? readdirSync(dir).filter((d) => d.startsWith("arc_")) : [];
1166
+ }
1167
+ readVault(master, id, profile) {
1168
+ const file = this.vaultFile(id, profile);
1169
+ if (!existsSync(file)) return emptyVault();
1170
+ const env = readEnvelope(file, "arca-vault");
1171
+ if (env.projectId !== id || env.profile !== profile) throw new ArcaError(`arca: ${file} belongs to a different project or profile.`);
1172
+ const data = JSON.parse(open(vaultKey(master, id, profile), env, vaultAad(id, profile)));
1173
+ return {
1174
+ vars: data.vars ?? {},
1175
+ removed: data.removed ?? {},
1176
+ meta: data.meta ?? { createdAt: (/* @__PURE__ */ new Date()).toISOString() }
1177
+ };
1178
+ }
1179
+ writeVault(master, id, profile, data) {
1180
+ const envelope = {
1181
+ format: "arca-vault",
1182
+ v: 1,
1183
+ projectId: id,
1184
+ profile,
1185
+ kdf: "hkdf-sha256",
1186
+ alg: "aes-256-gcm",
1187
+ ...seal(vaultKey(master, id, profile), JSON.stringify(data), vaultAad(id, profile))
1188
+ };
1189
+ this.withLock(id, () => {
1190
+ const file = this.vaultFile(id, profile);
1191
+ if (existsSync(file)) copyFileSync(file, `${file}.bak`);
1192
+ atomicWrite(file, JSON.stringify(envelope, null, 2) + "\n");
1193
+ });
1194
+ }
1195
+ policyFile(id) {
1196
+ return join(this.projectDir(id), "policy.vault");
1197
+ }
1198
+ readPolicy(master, id) {
1199
+ const file = this.policyFile(id);
1200
+ if (!existsSync(file) || !master) return { agentRun: "scripts" };
1201
+ const env = readEnvelope(file, "arca-policy");
1202
+ return { agentRun: JSON.parse(open(policyKey(master, id), env, `arca-policy:v1:${id}`)).agentRun === "deny" ? "deny" : "scripts" };
1203
+ }
1204
+ hasPolicy(id) {
1205
+ return existsSync(this.policyFile(id));
1206
+ }
1207
+ writePolicy(master, id, data) {
1208
+ const envelope = {
1209
+ format: "arca-policy",
1210
+ v: 1,
1211
+ projectId: id,
1212
+ kdf: "hkdf-sha256",
1213
+ alg: "aes-256-gcm",
1214
+ ...seal(policyKey(master, id), JSON.stringify(data), `arca-policy:v1:${id}`)
1215
+ };
1216
+ this.withLock(id, () => atomicWrite(this.policyFile(id), JSON.stringify(envelope, null, 2) + "\n"));
1217
+ }
1218
+ removeProject(id) {
1219
+ rmSync(this.projectDir(id), {
1220
+ recursive: true,
1221
+ force: true
1222
+ });
1223
+ const reg = this.readRegistry();
1224
+ delete reg[id];
1225
+ this.writeJson("registry.json", reg);
1226
+ }
1227
+ readGlobalConfig() {
1228
+ return this.readJson("config.json") ?? {};
1229
+ }
1230
+ writeGlobalConfig(cfg) {
1231
+ this.writeJson("config.json", cfg);
1232
+ }
1233
+ readRegistry() {
1234
+ return this.readJson("registry.json") ?? {};
1235
+ }
1236
+ /** Records where a project lives. Returns the previous entry so callers can detect ID collisions. */
1237
+ touchRegistry(id, info) {
1238
+ const reg = this.readRegistry();
1239
+ const prev = reg[id];
1240
+ const roots = new Set(prev?.roots ?? []);
1241
+ roots.add(info.root);
1242
+ reg[id] = {
1243
+ name: info.name,
1244
+ roots: [...roots],
1245
+ gitRemote: info.gitRemote ?? prev?.gitRemote ?? null,
1246
+ lastUsed: (/* @__PURE__ */ new Date()).toISOString()
1247
+ };
1248
+ this.writeJson("registry.json", reg);
1249
+ return prev;
1250
+ }
1251
+ readJson(name) {
1252
+ const file = join(this.root, name);
1253
+ if (!existsSync(file)) return null;
1254
+ try {
1255
+ return JSON.parse(readFileSync(file, "utf8"));
1256
+ } catch {
1257
+ throw new ArcaError(`arca: ${file} is corrupt (not valid JSON).`);
1258
+ }
1259
+ }
1260
+ writeJson(name, data) {
1261
+ ensureDir(this.root);
1262
+ atomicWrite(join(this.root, name), JSON.stringify(data, null, 2) + "\n");
1263
+ }
1264
+ /** Serialises writes to one project's files across processes. */
1265
+ withLock(id, fn) {
1266
+ const dir = this.projectDir(id);
1267
+ ensureDir(dir);
1268
+ const lock = join(dir, ".lock");
1269
+ const deadline = Date.now() + 5e3;
1270
+ for (;;) try {
1271
+ closeSync(openSync(lock, "wx"));
1272
+ break;
1273
+ } catch (e) {
1274
+ if (e.code !== "EEXIST") throw e;
1275
+ try {
1276
+ if (Date.now() - statSync(lock).mtimeMs > 1e4) {
1277
+ rmSync(lock, { force: true });
1278
+ continue;
1279
+ }
1280
+ } catch {
1281
+ continue;
1282
+ }
1283
+ if (Date.now() > deadline) throw new ArcaError(`arca: the store is locked by another arca process (${lock}).`);
1284
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50);
1285
+ }
1286
+ try {
1287
+ return fn();
1288
+ } finally {
1289
+ rmSync(lock, { force: true });
1290
+ }
1291
+ }
1292
+ };
1293
+ function emptyVault() {
1294
+ return {
1295
+ vars: {},
1296
+ removed: {},
1297
+ meta: { createdAt: (/* @__PURE__ */ new Date()).toISOString() }
1298
+ };
1299
+ }
1300
+ function vaultAad(id, profile) {
1301
+ return `arca-vault:v1:${id}:${profile}`;
1302
+ }
1303
+ function readEnvelope(file, format) {
1304
+ let env;
1305
+ try {
1306
+ env = JSON.parse(readFileSync(file, "utf8"));
1307
+ } catch {
1308
+ throw new ArcaError(`arca: ${file} is corrupt. A previous version may be in ${file}.bak.`);
1309
+ }
1310
+ if (env.format !== format || env.v !== 1) throw new ArcaError(`arca: ${file} has an unknown format.`);
1311
+ return env;
1312
+ }
1313
+ function ensureDir(dir) {
1314
+ if (!existsSync(dir)) mkdirSync(dir, {
1315
+ recursive: true,
1316
+ mode: 448
1317
+ });
1318
+ }
1319
+ function atomicWrite(file, content) {
1320
+ const tmp = `${file}.${process.pid}.tmp`;
1321
+ const fd = openSync(tmp, "w", 384);
1322
+ try {
1323
+ writeFileSync(fd, content);
1324
+ fsyncSync(fd);
1325
+ } finally {
1326
+ closeSync(fd);
1327
+ }
1328
+ for (let attempt = 0;; attempt++) try {
1329
+ renameSync(tmp, file);
1330
+ break;
1331
+ } catch (e) {
1332
+ const code = e.code;
1333
+ if (attempt >= 10 || code !== "EPERM" && code !== "EBUSY" && code !== "EACCES") {
1334
+ rmSync(tmp, { force: true });
1335
+ throw e;
1336
+ }
1337
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
1338
+ }
1339
+ if (process.platform !== "win32") chmodSync(file, 384);
1340
+ }
1341
+ //#endregion
1342
+ //#region src/paths.ts
1343
+ /** The store lives outside every project: never in the repo, never in `.vault/`. */
1344
+ function storeRoot(env, platform) {
1345
+ if (env.ARCA_HOME) return env.ARCA_HOME;
1346
+ if (platform === "win32") return join(env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local"), "arca");
1347
+ if (platform === "darwin") return join(homedir(), "Library", "Application Support", "arca");
1348
+ return join(env.XDG_DATA_HOME || join(homedir(), ".local", "share"), "arca");
1349
+ }
1350
+ //#endregion
1351
+ //#region src/keychain.ts
1352
+ const SERVICE = "arca";
1353
+ /** The keychain account. A custom ARCA_HOME gets its own entry so stores never share keys by accident. */
1354
+ function keychainAccount(store, io) {
1355
+ const defaultRoot = storeRoot({
1356
+ ...io.env,
1357
+ ARCA_HOME: void 0
1358
+ }, io.platform);
1359
+ if (store.root === defaultRoot) return "master-key/v1";
1360
+ return `master-key/v1/${createHash("sha256").update(store.root).digest("hex").slice(0, 12)}`;
1361
+ }
1362
+ function chooseBackend(store, io) {
1363
+ const configured = store.readGlobalConfig().keychain ?? io.env.ARCA_KEYCHAIN;
1364
+ if (configured === "os" || configured === "passphrase") return configured;
1365
+ if (io.platform === "linux") {
1366
+ if (readSafe("/proc/version").toLowerCase().includes("microsoft") || !io.env.DBUS_SESSION_BUS_ADDRESS) return "passphrase";
1367
+ }
1368
+ return "os";
1369
+ }
1370
+ function readSafe(file) {
1371
+ try {
1372
+ return readFileSync(file, "utf8");
1373
+ } catch {
1374
+ return "";
1375
+ }
1376
+ }
1377
+ async function osEntry() {
1378
+ try {
1379
+ const { Entry } = await import("@napi-rs/keyring");
1380
+ return Entry;
1381
+ } catch (e) {
1382
+ throw new ArcaError(`arca: the OS keychain binding could not be loaded (${e.message}).\nUse passphrase mode instead: set ARCA_KEYCHAIN=passphrase before the first \`npx arca set\`.`);
1383
+ }
1384
+ }
1385
+ async function loadMasterKey(store, io, opts) {
1386
+ const backend = chooseBackend(store, io);
1387
+ const key = backend === "os" ? await readOsKey(store, io) : await readPassphraseKey(store, io);
1388
+ if (key) return key;
1389
+ if (!opts.create) return null;
1390
+ if (store.hasAnyVault()) throw new ArcaError(`arca: the master key is missing from your keychain, but encrypted vaults exist in
1391
+ ${store.root}\nWithout the key they cannot be decrypted. Restore the keychain entry, or delete that folder to start over.`);
1392
+ const fresh = newMasterKey();
1393
+ if (backend === "os") await writeOsKey(store, io, fresh);
1394
+ else await writePassphraseKey(store, io, fresh);
1395
+ store.writeGlobalConfig({
1396
+ ...store.readGlobalConfig(),
1397
+ keychain: backend
1398
+ });
1399
+ return fresh;
1400
+ }
1401
+ /** Like loadMasterKey without create, but fails with a clear message. */
1402
+ async function requireMasterKey(store, io) {
1403
+ const key = await loadMasterKey(store, io, { create: false });
1404
+ if (!key) throw new ArcaError(`arca: no master key found, so the stored secrets cannot be decrypted.
1405
+ Store: ${store.root}\n Run \`npx arca doctor\` for details.`);
1406
+ return key;
1407
+ }
1408
+ async function readOsKey(store, io) {
1409
+ const Entry = await osEntry();
1410
+ let value;
1411
+ try {
1412
+ value = new Entry(SERVICE, keychainAccount(store, io)).getPassword();
1413
+ } catch (e) {
1414
+ throw new ArcaError(`arca: could not read the OS keychain (${e.message}).\nIf this machine has no keychain, use passphrase mode: set ARCA_KEYCHAIN=passphrase.`);
1415
+ }
1416
+ if (!value) return null;
1417
+ const key = Buffer.from(value, "base64");
1418
+ if (key.length !== 32) throw new ArcaError("arca: the master key in the keychain is malformed.");
1419
+ return key;
1420
+ }
1421
+ async function writeOsKey(store, io, key) {
1422
+ const entry = new (await (osEntry()))(SERVICE, keychainAccount(store, io));
1423
+ entry.setPassword(key.toString("base64"));
1424
+ if (entry.getPassword() !== key.toString("base64")) throw new ArcaError("arca: the OS keychain did not keep the master key. Use passphrase mode: set ARCA_KEYCHAIN=passphrase.");
1425
+ }
1426
+ function wrappedFile(store) {
1427
+ return join(store.root, "master.key.enc");
1428
+ }
1429
+ async function readPassphraseKey(store, io) {
1430
+ const file = wrappedFile(store);
1431
+ if (!existsSync(file)) return null;
1432
+ return unwrapMasterKey(JSON.parse(readFileSync(file, "utf8")), await getPassphrase(io, "arca passphrase"));
1433
+ }
1434
+ async function writePassphraseKey(store, io, key) {
1435
+ let passphrase = io.env.ARCA_PASSPHRASE;
1436
+ if (!passphrase) {
1437
+ passphrase = await getPassphrase(io, "Choose an arca passphrase (protects all your vaults on this machine)");
1438
+ if (await getPassphrase(io, "Repeat the passphrase") !== passphrase) throw new ArcaError("arca: the passphrases did not match.");
1439
+ }
1440
+ if (passphrase.length < 8) throw new ArcaError("arca: use a passphrase of at least 8 characters.");
1441
+ ensureDir(store.root);
1442
+ atomicWrite(wrappedFile(store), JSON.stringify(wrapMasterKey(key, passphrase), null, 2) + "\n");
1443
+ }
1444
+ async function getPassphrase(io, message) {
1445
+ if (io.env.ARCA_PASSPHRASE) return io.env.ARCA_PASSPHRASE;
1446
+ if (!io.stdinIsTTY || !io.stderr.isTTY) throw new ArcaError("arca: this store uses a passphrase. Run the command in an interactive terminal, or set ARCA_PASSPHRASE.");
1447
+ const value = await io.prompts.password(message);
1448
+ if (value === null) throw new ArcaError("arca: cancelled.");
1449
+ return value;
1450
+ }
1451
+ //#endregion
1452
+ //#region src/style.ts
1453
+ const codes = {
1454
+ bold: [1, 22],
1455
+ dim: [2, 22],
1456
+ red: [31, 39],
1457
+ green: [32, 39],
1458
+ yellow: [33, 39],
1459
+ cyan: [36, 39],
1460
+ gray: [90, 39]
1461
+ };
1462
+ function colorEnabled(s, env) {
1463
+ if (env.NO_COLOR) return false;
1464
+ if (env.FORCE_COLOR !== void 0) return env.FORCE_COLOR !== "0" && env.FORCE_COLOR !== "false";
1465
+ return s.isTTY;
1466
+ }
1467
+ function painter(s, env) {
1468
+ const on = colorEnabled(s, env);
1469
+ return (style, text) => {
1470
+ if (!on) return text;
1471
+ const [open, close] = codes[style];
1472
+ return `\u001b[${open}m${text}\u001b[${close}m`;
1473
+ };
1474
+ }
1475
+ /** Pads a string to a visible width (ignores ANSI escapes). */
1476
+ function pad(text, width) {
1477
+ const visible = text.replace(/\u001b\[[0-9;]*m/g, "").length;
1478
+ return visible >= width ? text : text + " ".repeat(width - visible);
1479
+ }
1480
+ //#endregion
1481
+ //#region src/session.ts
1482
+ /** Per-invocation state shared by all commands. */
1483
+ var Session = class {
1484
+ io;
1485
+ store;
1486
+ ctx;
1487
+ project;
1488
+ out;
1489
+ err;
1490
+ profile = DEFAULT_PROFILE;
1491
+ quiet = false;
1492
+ json = false;
1493
+ master = null;
1494
+ constructor(io, opts = {}) {
1495
+ this.io = io;
1496
+ this.store = new Store(storeRoot(io.env, io.platform));
1497
+ const markers = safeMarkers(this.store);
1498
+ this.ctx = buildContext(io, markers);
1499
+ this.project = loadProject(opts.projectDir ? resolve(io.cwd, opts.projectDir) : io.cwd);
1500
+ this.out = painter(io.stdout, io.env);
1501
+ this.err = painter(io.stderr, io.env);
1502
+ }
1503
+ print(text = "") {
1504
+ this.io.stdout.write(text + "\n");
1505
+ }
1506
+ /** Status and warnings go to stderr, so stdout stays clean for data. */
1507
+ note(text) {
1508
+ this.io.stderr.write(text + "\n");
1509
+ }
1510
+ warnConfig() {
1511
+ if (!this.project || this.quiet) return;
1512
+ for (const w of this.project.warnings) this.note(this.err("yellow", `arca: ${w}`));
1513
+ }
1514
+ requireProject() {
1515
+ if (!this.project) throw new ArcaError("arca: no arca.json found in this folder or any parent. Run `npx arca init` to set up arca for this project.");
1516
+ return this.project;
1517
+ }
1518
+ get projectId() {
1519
+ return this.requireProject().config.id;
1520
+ }
1521
+ hasVault() {
1522
+ return this.project ? this.store.hasVault(this.project.config.id, this.profile) : false;
1523
+ }
1524
+ async masterKey(create = false) {
1525
+ if (this.master) return this.master;
1526
+ this.master = create ? await loadMasterKey(this.store, this.io, { create: true }) : await requireMasterKey(this.store, this.io);
1527
+ if (!this.master) throw new ArcaError("arca: no master key.");
1528
+ return this.master;
1529
+ }
1530
+ /** The project's vault. Doesn't touch the keychain when no vault exists yet. */
1531
+ async readVault() {
1532
+ if (!this.hasVault()) return emptyVault();
1533
+ return this.store.readVault(await this.masterKey(), this.projectId, this.profile);
1534
+ }
1535
+ async writeVault(data) {
1536
+ const project = this.requireProject();
1537
+ const master = await this.masterKey(true);
1538
+ this.store.writeVault(master, project.config.id, this.profile, data);
1539
+ const prev = this.store.touchRegistry(project.config.id, {
1540
+ name: project.config.name,
1541
+ root: project.root,
1542
+ gitRemote: gitRemote(project.root)
1543
+ });
1544
+ this.warnCollision(prev);
1545
+ }
1546
+ warnCollision(prev) {
1547
+ const project = this.project;
1548
+ if (!project || !prev?.gitRemote) return;
1549
+ const remote = gitRemote(project.root);
1550
+ if (remote && remote !== prev.gitRemote) this.note(this.err("yellow", `arca: arca.json id ${project.config.id} is also used by ${prev.roots[0] ?? "another project"} (git remote ${prev.gitRemote}).\n Two different repos share secrets. If that is wrong, run \`npx arca init --new-id\` in one of them.`));
1551
+ }
1552
+ async policy() {
1553
+ if (!this.project || !this.store.hasPolicy(this.project.config.id)) return { agentRun: "scripts" };
1554
+ return this.store.readPolicy(await this.masterKey(), this.project.config.id);
1555
+ }
1556
+ /** Stored values that count as secrets (masking needles, leak-guard exact matches). */
1557
+ secrets(vault) {
1558
+ const decls = this.project?.config.vars ?? {};
1559
+ return Object.entries(vault.vars).filter(([name]) => isSensitive(decls[name])).map(([name, v]) => ({
1560
+ name,
1561
+ value: v.value
1562
+ }));
1563
+ }
1564
+ /** Nearest directory with a package.json, from cwd up to the project root. */
1565
+ packageRoot() {
1566
+ let dir = resolve(this.io.cwd);
1567
+ for (;;) {
1568
+ if (existsSync(join(dir, "package.json"))) return dir;
1569
+ const parent = dirname(dir);
1570
+ if (parent === dir || this.project && dir === this.project.root) return null;
1571
+ dir = parent;
1572
+ }
1573
+ }
1574
+ /** Every .env* file the leak guard reads: project root, nearest package root, and envFiles. */
1575
+ envFilesForGuard() {
1576
+ const project = this.requireProject();
1577
+ const dirs = [project.root];
1578
+ const pkg = this.packageRoot();
1579
+ if (pkg && pkg !== project.root) dirs.push(pkg);
1580
+ const files = findEnvFiles(dirs);
1581
+ for (const f of project.config.envFiles) {
1582
+ const path = resolve(project.root, f);
1583
+ if (files.some((x) => x.path === path)) continue;
1584
+ const file = readEnvFile(path, f);
1585
+ if (file) files.push(file);
1586
+ }
1587
+ return files;
1588
+ }
1589
+ };
1590
+ function safeMarkers(store) {
1591
+ try {
1592
+ return store.readGlobalConfig().agentMarkers ?? [];
1593
+ } catch {
1594
+ return [];
1595
+ }
1596
+ }
1597
+ function gitRemote(root) {
1598
+ try {
1599
+ return execFileSync("git", [
1600
+ "config",
1601
+ "--get",
1602
+ "remote.origin.url"
1603
+ ], {
1604
+ cwd: root,
1605
+ encoding: "utf8",
1606
+ stdio: [
1607
+ "ignore",
1608
+ "pipe",
1609
+ "ignore"
1610
+ ],
1611
+ timeout: 3e3
1612
+ }).trim() || null;
1613
+ } catch {
1614
+ return null;
1615
+ }
1616
+ }
1617
+ //#endregion
1618
+ //#region src/util.ts
1619
+ function levenshtein(a, b) {
1620
+ const dp = Array.from({ length: b.length + 1 }, (_, i) => i);
1621
+ for (let i = 1; i <= a.length; i++) {
1622
+ let prev = dp[0];
1623
+ dp[0] = i;
1624
+ for (let j = 1; j <= b.length; j++) {
1625
+ const tmp = dp[j];
1626
+ dp[j] = Math.min(dp[j] + 1, dp[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
1627
+ prev = tmp;
1628
+ }
1629
+ }
1630
+ return dp[b.length];
1631
+ }
1632
+ /** The closest candidate within a small edit distance, for "did you mean …?". */
1633
+ function suggest(input, candidates) {
1634
+ let best = null;
1635
+ let bestDist = Infinity;
1636
+ for (const c of candidates) {
1637
+ if (c === input) continue;
1638
+ const d = levenshtein(input.toUpperCase(), c.toUpperCase());
1639
+ if (d < bestDist) {
1640
+ best = c;
1641
+ bestDist = d;
1642
+ }
1643
+ }
1644
+ const limit = Math.max(1, Math.min(3, Math.floor(input.length / 4)));
1645
+ return bestDist <= limit ? best : null;
1646
+ }
1647
+ function plural(n, one, many = `${one}s`) {
1648
+ return `${n} ${n === 1 ? one : many}`;
1649
+ }
1650
+ function shortDate(iso) {
1651
+ return iso ? iso.slice(0, 10) : "";
1652
+ }
1653
+ //#endregion
1654
+ //#region src/commands/values.ts
1655
+ const now = () => (/* @__PURE__ */ new Date()).toISOString();
1656
+ function storeValue(vault, name, value, source) {
1657
+ const old = vault.vars[name];
1658
+ vault.vars[name] = {
1659
+ value,
1660
+ updatedAt: now(),
1661
+ source,
1662
+ previous: old ? {
1663
+ value: old.value,
1664
+ updatedAt: old.updatedAt
1665
+ } : vault.removed[name] ?? null
1666
+ };
1667
+ delete vault.removed[name];
1668
+ }
1669
+ function checkName(name, usage) {
1670
+ if (!name) throw new ArcaError(`arca: missing variable name. Usage: ${usage}`, 2);
1671
+ if (!VAR_NAME.test(name)) throw new ArcaError(`arca: "${name}" is not a valid environment variable name.`, 2);
1672
+ return name;
1673
+ }
1674
+ async function setCommand(s, opts) {
1675
+ const project = s.requireProject();
1676
+ let name = checkName(opts.name, "npx arca set NAME");
1677
+ const fromStdin = opts.value === void 0 && opts.generate === void 0 && !s.io.stdinIsTTY;
1678
+ await humanGate(s.io, s.ctx, {
1679
+ kind: "write",
1680
+ command: `npx arca set ${name}`,
1681
+ allowPipedStdin: fromStdin
1682
+ });
1683
+ const declared = project.config.vars;
1684
+ if (!(name in declared)) {
1685
+ const guess = suggest(name, Object.keys(declared));
1686
+ if (guess && s.io.stdinIsTTY) {
1687
+ const yes = await s.io.prompts.confirm(`${name} isn't declared in arca.json. Did you mean ${guess}?`, true);
1688
+ if (yes === null) throw new ArcaError("arca: cancelled.");
1689
+ if (yes) name = guess;
1690
+ }
1691
+ }
1692
+ const decl = declared[name];
1693
+ let value;
1694
+ if (opts.generate !== void 0) value = generateSecret(opts.generate === true ? decl?.generate?.bytes ?? 32 : opts.generate, decl?.generate?.encoding ?? "base64url");
1695
+ else if (opts.value !== void 0) {
1696
+ s.note(s.err("yellow", "⚠ value passed on the command line; it may be in your shell history. Next time use `npx arca set NAME` (hidden prompt)."));
1697
+ value = opts.value;
1698
+ } else if (fromStdin) value = (await s.io.readStdin()).replace(/\r?\n$/, "");
1699
+ else {
1700
+ const entered = await s.io.prompts.password(`${name}${decl?.description ? ` (${decl.description})` : ""}`);
1701
+ if (entered === null) throw new ArcaError("arca: cancelled. Nothing was changed.");
1702
+ value = entered;
1703
+ }
1704
+ if (value === "") throw new ArcaError("arca: empty value. Nothing was changed. (To remove a value, use `npx arca unset NAME`.)");
1705
+ const vault = await s.readVault();
1706
+ const existed = name in vault.vars;
1707
+ storeValue(vault, name, value, opts.generate !== void 0 ? "generate" : fromStdin ? "stdin" : "set");
1708
+ await s.writeVault(vault);
1709
+ if (!(name in declared)) {
1710
+ let declare = false;
1711
+ if (s.io.stdinIsTTY) declare = await s.io.prompts.confirm(`Declare ${name} in arca.json, so teammates know it's needed?`, true) === true;
1712
+ if (declare) declareVars(project.file, { [name]: { required: true } });
1713
+ else s.note(s.err("dim", `arca: ${name} isn't declared in arca.json. It's still injected; declare it so \`npx arca check\` and teammates know about it.`));
1714
+ }
1715
+ if (isSensitive(decl) && value.length < 6) s.note(s.err("yellow", `⚠ ${name} is shorter than 6 characters, so it can't be masked in output.`));
1716
+ if (decl && !isSensitive(decl)) s.note(s.err("dim", `arca: ${name} is declared as config (sensitive: false). It's stored in arca anyway and arca's value wins over .env.`));
1717
+ const what = opts.generate !== void 0 ? "Generated" : existed ? "Updated" : "Set";
1718
+ s.note(`${s.err("green", "✔")} ${what} ${name} for ${project.config.name}${existed ? " (previous value kept, `npx arca restore " + name + "` undoes this)" : ""}`);
1719
+ return 0;
1720
+ }
1721
+ async function unsetCommand(s, rawName) {
1722
+ const project = s.requireProject();
1723
+ const name = checkName(rawName, "npx arca unset NAME");
1724
+ await humanGate(s.io, s.ctx, {
1725
+ kind: "write",
1726
+ command: `npx arca unset ${name}`
1727
+ });
1728
+ const vault = await s.readVault();
1729
+ const v = vault.vars[name];
1730
+ if (!v) throw new ArcaError(`arca: ${name} is not stored in arca for ${project.config.name}.`);
1731
+ vault.removed[name] = {
1732
+ value: v.value,
1733
+ updatedAt: v.updatedAt
1734
+ };
1735
+ delete vault.vars[name];
1736
+ await s.writeVault(vault);
1737
+ s.note(`${s.err("green", "✔")} Removed ${name}. \`npx arca restore ${name}\` brings it back.`);
1738
+ return 0;
1739
+ }
1740
+ async function restoreCommand(s, rawName) {
1741
+ s.requireProject();
1742
+ const name = checkName(rawName, "npx arca restore NAME");
1743
+ await humanGate(s.io, s.ctx, {
1744
+ kind: "write",
1745
+ command: `npx arca restore ${name}`
1746
+ });
1747
+ const vault = await s.readVault();
1748
+ const current = vault.vars[name];
1749
+ if (current?.previous) vault.vars[name] = {
1750
+ value: current.previous.value,
1751
+ updatedAt: now(),
1752
+ source: "restore",
1753
+ previous: {
1754
+ value: current.value,
1755
+ updatedAt: current.updatedAt
1756
+ }
1757
+ };
1758
+ else if (!current && vault.removed[name]) storeValue(vault, name, vault.removed[name].value, "restore");
1759
+ else throw new ArcaError(`arca: there's no previous value of ${name} to restore.`);
1760
+ await s.writeVault(vault);
1761
+ s.note(`${s.err("green", "✔")} Restored the previous value of ${name}. Run it again to undo.`);
1762
+ return 0;
1763
+ }
1764
+ async function revealCommand(s, rawName) {
1765
+ const project = s.requireProject();
1766
+ const name = checkName(rawName, "npx arca reveal NAME");
1767
+ await humanGate(s.io, s.ctx, {
1768
+ kind: "reveal",
1769
+ command: `npx arca reveal ${name}`
1770
+ });
1771
+ const vault = await s.readVault();
1772
+ const v = vault.vars[name];
1773
+ if (!v) {
1774
+ const guess = suggest(name, Object.keys(vault.vars));
1775
+ throw new ArcaError(`arca: ${name} is not stored in arca for ${project.config.name}.${guess ? ` Did you mean ${guess}?` : ""}`);
1776
+ }
1777
+ s.print(v.value);
1778
+ return 0;
1779
+ }
1780
+ async function exportCommand(s, opts) {
1781
+ const project = s.requireProject();
1782
+ const format = opts.format ?? "dotenv";
1783
+ if (![
1784
+ "dotenv",
1785
+ "json",
1786
+ "shell"
1787
+ ].includes(format)) throw new ArcaError("arca: --format must be dotenv, json or shell.", 2);
1788
+ await humanGate(s.io, s.ctx, {
1789
+ kind: "reveal",
1790
+ command: "npx arca export"
1791
+ });
1792
+ const vault = await s.readVault();
1793
+ const entries = Object.entries(vault.vars).map(([k, v]) => [k, v.value]);
1794
+ let text;
1795
+ if (format === "json") text = JSON.stringify(Object.fromEntries(entries), null, 2);
1796
+ else if (format === "shell") text = entries.map(([k, v]) => `export ${k}='${v.replace(/'/g, `'\\''`)}'`).join("\n");
1797
+ else text = entries.map(([k, v]) => `${k}=${JSON.stringify(v)}`).join("\n");
1798
+ if (opts.output) {
1799
+ writeFileSync(resolve(s.io.cwd, opts.output), text + "\n", { mode: 384 });
1800
+ s.note(`${s.err("green", "✔")} Wrote ${plural(entries.length, "secret")} to ${opts.output}`);
1801
+ s.note(s.err("yellow", `⚠ That file holds plaintext secrets. Delete it when you're done. The leak guard will flag it while it sits in ${project.config.name}.`));
1802
+ } else s.print(text);
1803
+ return 0;
1804
+ }
1805
+ async function setupCommand(s, opts) {
1806
+ const project = s.requireProject();
1807
+ await humanGate(s.io, s.ctx, {
1808
+ kind: "write",
1809
+ command: "npx arca setup"
1810
+ });
1811
+ const vault = await s.readVault();
1812
+ const todo = Object.entries(project.config.vars).filter(([name, d]) => isSensitive(d) && !(name in vault.vars) && (opts.all || d.required));
1813
+ if (todo.length === 0) {
1814
+ s.note(`${s.err("green", "✔")} All ${opts.all ? "declared" : "required"} secrets are set for ${project.config.name}.`);
1815
+ return 0;
1816
+ }
1817
+ s.note(`arca: ${plural(todo.length, "variable")} to set for ${project.config.name}. Leave a prompt empty to skip it.\n`);
1818
+ let set = 0;
1819
+ for (const [name, d] of todo) {
1820
+ s.note(`${s.err("cyan", "◆")} ${s.err("bold", name)}${d.description ? `: ${d.description}` : ""}`);
1821
+ if (d.docs) s.note(` get it at ${d.docs}`);
1822
+ if (d.example) s.note(s.err("dim", ` example: ${d.example}`));
1823
+ let value = null;
1824
+ if (d.generate) {
1825
+ const gen = await s.io.prompts.confirm(`Generate a random value for ${name}?`, true);
1826
+ if (gen === null) break;
1827
+ if (gen) value = generateSecret(d.generate.bytes ?? 32, d.generate.encoding ?? "base64url");
1828
+ }
1829
+ if (value === null) {
1830
+ const entered = await s.io.prompts.password(name);
1831
+ if (entered === null) break;
1832
+ value = entered === "" ? null : entered;
1833
+ }
1834
+ if (value === null) {
1835
+ s.note(s.err("dim", ` skipped ${name}`));
1836
+ continue;
1837
+ }
1838
+ storeValue(vault, name, value, "setup");
1839
+ await s.writeVault(vault);
1840
+ set++;
1841
+ }
1842
+ const left = todo.length - set;
1843
+ if (left === 0) s.note(`\n${s.err("green", "✔")} All ${opts.all ? "declared" : "required"} variables set. Run \`npm run dev\`.`);
1844
+ else s.note(`\narca: set ${set}, ${left} still missing. Run \`npx arca setup\` again later.`);
1845
+ return left === 0 ? 0 : 1;
1846
+ }
1847
+ /** Decides which entries of which files are secrets. Shared with `arca init`. */
1848
+ function planImport(s, files, opts = {}) {
1849
+ const project = s.requireProject();
1850
+ const sources = [];
1851
+ for (const f of files) {
1852
+ const file = readEnvFile(resolve(s.io.cwd, f), f) ?? readEnvFile(join(project.root, f), f);
1853
+ if (file) sources.push(file);
1854
+ }
1855
+ const merged = /* @__PURE__ */ new Map();
1856
+ for (const file of sources) for (const e of file.entries) merged.set(e.key, {
1857
+ value: e.value,
1858
+ file: file.name
1859
+ });
1860
+ const only = opts.only ? new Set(opts.only) : null;
1861
+ const secrets = /* @__PURE__ */ new Map();
1862
+ const skipped = [];
1863
+ for (const [key, v] of merged) {
1864
+ if (only && !only.has(key)) continue;
1865
+ if (v.value === "") {
1866
+ skipped.push({
1867
+ key,
1868
+ why: "empty"
1869
+ });
1870
+ continue;
1871
+ }
1872
+ const decl = project.config.vars[key];
1873
+ if (decl && !isSensitive(decl)) {
1874
+ skipped.push({
1875
+ key,
1876
+ why: "declared as config"
1877
+ });
1878
+ continue;
1879
+ }
1880
+ if (decl || only || opts.includePublic || secretReason(key, v.value)) secrets.set(key, v);
1881
+ else skipped.push({
1882
+ key,
1883
+ why: "doesn't look secret"
1884
+ });
1885
+ }
1886
+ return {
1887
+ sources,
1888
+ secrets,
1889
+ skipped
1890
+ };
1891
+ }
1892
+ function defaultImportFiles(s) {
1893
+ const root = s.requireProject().root;
1894
+ return DEV_FILES.filter((f) => existsSync(join(root, f)));
1895
+ }
1896
+ async function importCommand(s, opts) {
1897
+ const project = s.requireProject();
1898
+ await humanGate(s.io, s.ctx, {
1899
+ kind: "write",
1900
+ command: "npx arca import" + (opts.delete ? " --delete" : "")
1901
+ });
1902
+ const files = opts.files.length ? opts.files : defaultImportFiles(s);
1903
+ if (files.length === 0) throw new ArcaError(`arca: no .env files found in ${project.root}. Name the file: npx arca import path/to/.env`);
1904
+ const plan = planImport(s, files, opts);
1905
+ if (plan.sources.length === 0) throw new ArcaError(`arca: none of these files exist: ${files.join(", ")}`);
1906
+ const vault = await s.readVault();
1907
+ const conflicts = [...plan.secrets].filter(([k, v]) => vault.vars[k] && vault.vars[k].value !== v.value).map(([k]) => k);
1908
+ let overwrite = Boolean(opts.yes);
1909
+ if (conflicts.length && !opts.yes) {
1910
+ const answer = await s.io.prompts.confirm(`${conflicts.join(", ")} already in arca with a different value. Overwrite?`, false);
1911
+ if (answer === null) throw new ArcaError("arca: cancelled. Nothing was changed.");
1912
+ overwrite = answer;
1913
+ }
1914
+ const imported = [];
1915
+ for (const [key, v] of plan.secrets) {
1916
+ if (vault.vars[key]?.value === v.value) {
1917
+ imported.push(key);
1918
+ continue;
1919
+ }
1920
+ if (vault.vars[key] && !overwrite) continue;
1921
+ storeValue(vault, key, v.value, `import:${v.file}`);
1922
+ imported.push(key);
1923
+ }
1924
+ if (imported.length) await s.writeVault(vault);
1925
+ const undeclared = imported.filter((k) => !(k in project.config.vars));
1926
+ if (undeclared.length) declareVars(project.file, Object.fromEntries(undeclared.map((k) => [k, {}])));
1927
+ s.note(`${s.err("green", "✔")} Imported ${plural(imported.length, "secret")} into arca${imported.length ? `: ${imported.join(", ")}` : ""}`);
1928
+ if (undeclared.length) s.note(`${s.err("green", "✔")} Declared ${undeclared.join(", ")} in arca.json`);
1929
+ const config = plan.skipped.filter((x) => x.why !== "empty");
1930
+ if (config.length) s.note(s.err("dim", ` stays in .env: ${config.map((x) => x.key).join(", ")}${opts.includePublic ? "" : " (use --include-public or --only to import these too)"}`));
1931
+ if (opts.delete && imported.length) {
1932
+ const keys = new Set(imported);
1933
+ for (const src of plan.sources) {
1934
+ const result = removeEntries(src.path, keys);
1935
+ const name = relative(project.root, src.path) || src.name;
1936
+ if (result === "edited") s.note(`${s.err("green", "✔")} Removed the imported entries from ${name} (other lines kept)`);
1937
+ if (result === "deleted") s.note(`${s.err("green", "✔")} Deleted ${name} (nothing else was in it)`);
1938
+ }
1939
+ } else if (imported.length) {
1940
+ s.note(s.err("yellow", `⚠ Your .env files were not changed, so these secrets still have plaintext copies.`));
1941
+ s.note(` Remove just those entries (config lines stay): npx arca import --delete`);
1942
+ }
1943
+ return 0;
1944
+ }
1945
+ //#endregion
1946
+ //#region src/commands/init.ts
1947
+ const KNOWN_TOOLS = /* @__PURE__ */ new Set([
1948
+ "next",
1949
+ "vite",
1950
+ "astro",
1951
+ "nuxt",
1952
+ "nuxi",
1953
+ "react-router",
1954
+ "remix",
1955
+ "svelte-kit",
1956
+ "vinxi",
1957
+ "tsx",
1958
+ "node",
1959
+ "vitest",
1960
+ "jest",
1961
+ "playwright",
1962
+ "prisma",
1963
+ "drizzle-kit",
1964
+ "wrangler",
1965
+ "turbo",
1966
+ "nodemon",
1967
+ "ts-node",
1968
+ "tsup",
1969
+ "tanstack",
1970
+ "storybook",
1971
+ "start-storybook",
1972
+ "netlify",
1973
+ "vercel",
1974
+ "nest",
1975
+ "expo",
1976
+ "react-scripts",
1977
+ "webpack"
1978
+ ]);
1979
+ /** The tool a script runs first (`next dev` → next, `npx prisma migrate` → prisma). Null for compound scripts. */
1980
+ function firstCommand(script) {
1981
+ if (/&&|\|\||[;|<>]/.test(script)) return null;
1982
+ const tokens = tokenize(script);
1983
+ let i = 0;
1984
+ if (tokens[i] === "npx" || tokens[i] === "bunx" || tokens[i] === "dotenv") i++;
1985
+ if ((tokens[i] === "pnpm" || tokens[i] === "yarn") && tokens[i + 1] === "exec") i += 2;
1986
+ const t = tokens[i];
1987
+ if (!t || /^[A-Za-z_][A-Za-z0-9_]*=/.test(t) || t === "cross-env") return null;
1988
+ return basename(t);
1989
+ }
1990
+ const FRAMEWORKS = [
1991
+ ["next", "Next.js"],
1992
+ ["@tanstack/react-start", "TanStack Start"],
1993
+ ["@tanstack/solid-start", "TanStack Start"],
1994
+ ["astro", "Astro"],
1995
+ ["nuxt", "Nuxt"],
1996
+ ["@sveltejs/kit", "SvelteKit"],
1997
+ ["@remix-run/dev", "Remix"],
1998
+ ["@react-router/dev", "React Router"],
1999
+ ["vite", "Vite"]
2000
+ ];
2001
+ function detectFramework(pkg) {
2002
+ const deps = {
2003
+ ...pkg.dependencies,
2004
+ ...pkg.devDependencies
2005
+ };
2006
+ for (const [dep, label] of FRAMEWORKS) if (deps[dep]) return `${label} ${deps[dep].replace(/^[\^~]/, "")}`;
2007
+ return null;
2008
+ }
2009
+ async function initCommand(s, opts) {
2010
+ const { io } = s;
2011
+ const root = io.cwd;
2012
+ const configFile = join(root, CONFIG_FILE);
2013
+ const pkgFile = join(root, "package.json");
2014
+ const pkg = existsSync(pkgFile) ? JSON.parse(readFileSync(pkgFile, "utf8")) : null;
2015
+ const interactive = s.ctx.interactiveOutput && s.ctx.stdinIsTTY && !opts.yes;
2016
+ const ok = (t) => s.note(`${s.err("green", "✔")} ${t}`);
2017
+ const step = (t) => s.note(`${s.err("cyan", "◇")} ${t}`);
2018
+ const ask = async (q, initial = true) => {
2019
+ if (!interactive) return initial;
2020
+ const r = await io.prompts.confirm(q, initial);
2021
+ if (r === null) throw new ArcaError("arca: cancelled.");
2022
+ return r;
2023
+ };
2024
+ let created = false;
2025
+ if (existsSync(configFile)) {
2026
+ if (opts.newId) {
2027
+ const id = newProjectId();
2028
+ updateConfigFile(configFile, (raw) => {
2029
+ raw.id = id;
2030
+ });
2031
+ ok(`New project ID ${id}. This project no longer shares secrets with the old ID.`);
2032
+ return 0;
2033
+ }
2034
+ const existing = parseProject(readFileSync(configFile, "utf8"), configFile);
2035
+ step(`arca.json already exists (${existing.config.name}, ${existing.config.id}). Checking the rest of the setup.`);
2036
+ } else {
2037
+ let name = pkg?.name?.replace(/^@[^/]+\//, "") ?? basename(root);
2038
+ if (interactive) {
2039
+ const answer = await io.prompts.text("Project name", { initial: name });
2040
+ if (answer === null) throw new ArcaError("arca: cancelled.");
2041
+ name = answer.trim() || name;
2042
+ }
2043
+ writeFileSync(configFile, JSON.stringify(newConfig(name), null, 2) + "\n");
2044
+ created = true;
2045
+ }
2046
+ const framework = pkg ? detectFramework(pkg) : null;
2047
+ if (framework) step(`Detected ${framework}`);
2048
+ const devFiles = DEV_FILES.map((f) => readEnvFile(join(root, f), f)).filter((f) => f !== null);
2049
+ const otherFiles = safeReaddir(root).filter((n) => /^\.env\./.test(n) && !DEV_FILES.includes(n) && !isTemplateFile(n));
2050
+ const merged = /* @__PURE__ */ new Map();
2051
+ for (const f of devFiles) for (const e of f.entries) merged.set(e.key, e.value);
2052
+ let secretKeys = [];
2053
+ const config = parseProject(readFileSync(configFile, "utf8"), configFile).config;
2054
+ if (merged.size === 0) step("No .env files with values found. Secrets go in arca (`npx arca set NAME`), non-sensitive config can stay in .env.");
2055
+ else {
2056
+ step(`Found ${devFiles.map((f) => `${f.name} (${plural(f.entries.length, "var")})`).join(", ")}`);
2057
+ const keys = [...merged.keys()];
2058
+ const suggested = keys.filter((k) => {
2059
+ const d = config.vars[k];
2060
+ if (d) return d.sensitive !== false;
2061
+ return Boolean(secretReason(k, merged.get(k)));
2062
+ });
2063
+ if (interactive) {
2064
+ const picked = await io.prompts.multiselect("Which of these are secrets? Secrets move to arca, the rest stays in .env as config.", keys.map((k) => ({
2065
+ value: k,
2066
+ label: k,
2067
+ hint: isPublicName(k) ? "public" : secretReason(k, merged.get(k)) ?? void 0
2068
+ })), suggested);
2069
+ if (picked === null) throw new ArcaError("arca: cancelled.");
2070
+ secretKeys = picked;
2071
+ } else secretKeys = suggested;
2072
+ const stays = keys.filter((k) => !secretKeys.includes(k));
2073
+ s.note(` → arca ${secretKeys.join(", ") || "(none)"}`);
2074
+ s.note(` stays ${stays.join(", ") || "(none)"}`);
2075
+ const decls = {};
2076
+ for (const k of secretKeys) decls[k] = { required: true };
2077
+ for (const k of stays) decls[k] = { sensitive: false };
2078
+ declareVars(configFile, decls);
2079
+ }
2080
+ if (otherFiles.length) step(`Left alone: ${otherFiles.join(", ")} (separate sets of values arrive with profiles in a later version)`);
2081
+ ok(`${created ? "Created" : "Updated"} arca.json${created ? ` (id ${parseProject(readFileSync(configFile, "utf8"), configFile).config.id})` : ""}`);
2082
+ const session = new Session(io);
2083
+ let imported = 0;
2084
+ if (secretKeys.length && opts.import !== false) try {
2085
+ await humanGate(io, session.ctx, {
2086
+ kind: "write",
2087
+ command: "npx arca import"
2088
+ });
2089
+ const hadKey = session.store.readGlobalConfig().keychain !== void 0;
2090
+ const vault = await session.readVault();
2091
+ for (const k of secretKeys) {
2092
+ const v = merged.get(k);
2093
+ if (v === "") continue;
2094
+ storeValue(vault, k, v, `import:${[...devFiles].reverse().find((f) => f.entries.some((e) => e.key === k))?.name ?? ".env"}`);
2095
+ imported++;
2096
+ }
2097
+ await session.writeVault(vault);
2098
+ if (!hadKey) ok(`Master key created in ${chooseBackend(session.store, io) === "os" ? keychainLabel(io.platform) : "a passphrase-protected file"}`);
2099
+ ok(`Imported ${plural(imported, "secret")} into arca`);
2100
+ ok("Your .env files were not changed");
2101
+ } catch (e) {
2102
+ if (!(e instanceof ArcaError)) throw e;
2103
+ s.note(`${s.err("yellow", "✋")} Moving secret values into arca needs a human. Ask the user to run: npx arca import`);
2104
+ s.note(" (the .env files stay untouched)");
2105
+ }
2106
+ if (pkg?.scripts) {
2107
+ const candidates = Object.entries(pkg.scripts).filter(([k, v]) => k !== "start" && !/\barca\s+run\b/.test(v) && KNOWN_TOOLS.has(firstCommand(v) ?? ""));
2108
+ let toWrap = candidates.map(([k]) => k);
2109
+ if (candidates.length && interactive) {
2110
+ const picked = await io.prompts.multiselect("Wrap these package.json scripts with `arca run`, so they get secrets?", candidates.map(([k, v]) => ({
2111
+ value: k,
2112
+ label: k,
2113
+ hint: v
2114
+ })), toWrap);
2115
+ if (picked === null) throw new ArcaError("arca: cancelled.");
2116
+ toWrap = picked;
2117
+ }
2118
+ if (toWrap.length) {
2119
+ wrapScripts(pkgFile, toWrap);
2120
+ ok(`Wrapped scripts: ${toWrap.join(", ")}`);
2121
+ for (const k of toWrap) s.note(s.err("dim", ` ${k}: "${pkg.scripts[k]}" → "arca run -- ${pkg.scripts[k]}"`));
2122
+ }
2123
+ const wrappedAlready = Object.keys(pkg.scripts).filter((k) => /\barca\s+run\b/.test(pkg.scripts[k]));
2124
+ if (wrappedAlready.length && !toWrap.length) step(`Already wrapped: ${wrappedAlready.join(", ")}`);
2125
+ const wrappedNow = /* @__PURE__ */ new Set([...wrappedAlready, ...toWrap]);
2126
+ const skipped = Object.entries(pkg.scripts).filter(([k]) => !wrappedNow.has(k));
2127
+ if (skipped.length) {
2128
+ step("Not wrapped:");
2129
+ for (const [k, v] of skipped) s.note(s.err("dim", ` ${k}: ${skipReason(k, v, pkg.scripts, wrappedNow)}`));
2130
+ }
2131
+ const start = pkg.scripts.start;
2132
+ if (start && !/\barca\s+run\b/.test(start) && !pkg.scripts["start:local"] && KNOWN_TOOLS.has(firstCommand(start) ?? "")) {
2133
+ if (await ask("Add \"start:local\" to run the production build locally with your secrets?", true)) {
2134
+ addScript(pkgFile, "start:local", `arca run -- ${start}`);
2135
+ ok(`Added "start:local": "arca run -- ${start}"`);
2136
+ }
2137
+ }
2138
+ } else if (!pkg) step("No package.json here. Wrap your commands yourself: arca run -- <command>");
2139
+ const docs = ["AGENTS.md", "CLAUDE.md"].filter((f) => existsSync(join(root, f)));
2140
+ const marker = "## Secrets (arca)";
2141
+ const missingSnippet = docs.filter((f) => !readFileSync(join(root, f), "utf8").includes(marker));
2142
+ if (docs.length === 0) {
2143
+ if (await ask("Create AGENTS.md with a short \"Secrets (arca)\" section for AI agents?", true)) {
2144
+ writeFileSync(join(root, "AGENTS.md"), `# Agent instructions\n\n${agentsSnippet()}`);
2145
+ ok("Created AGENTS.md with the \"Secrets (arca)\" section");
2146
+ }
2147
+ } else if (missingSnippet.length) {
2148
+ if (await ask(`Add a "Secrets (arca)" section to ${missingSnippet.join(" and ")}?`, true)) {
2149
+ for (const f of missingSnippet) {
2150
+ const text = readFileSync(join(root, f), "utf8");
2151
+ appendFileSync(join(root, f), `${text.endsWith("\n") ? "" : "\n"}\n${agentsSnippet()}`);
2152
+ }
2153
+ ok(`Added the "Secrets (arca)" section to ${missingSnippet.join(" and ")}`);
2154
+ }
2155
+ }
2156
+ const outdated = docs.filter((f) => {
2157
+ const section = findSection(readFileSync(join(root, f), "utf8"), marker);
2158
+ return section !== null && section.text.trim() !== agentsSnippet().trim();
2159
+ });
2160
+ if (outdated.length && await ask(`Update the "Secrets (arca)" section in ${outdated.join(" and ")} to the current text?`, true)) {
2161
+ for (const f of outdated) {
2162
+ const text = readFileSync(join(root, f), "utf8");
2163
+ const section = findSection(text, marker);
2164
+ const trailing = /\s*$/.exec(section.text)?.[0] ?? "\n";
2165
+ writeFileSync(join(root, f), text.slice(0, section.start) + agentsSnippet().trimEnd() + trailing + text.slice(section.end));
2166
+ }
2167
+ ok(`Updated the "Secrets (arca)" section in ${outdated.join(" and ")}`);
2168
+ }
2169
+ const deps = {
2170
+ ...pkg?.dependencies,
2171
+ ...pkg?.devDependencies
2172
+ };
2173
+ if (pkg && !deps["@bremmdev/arca-env"]) s.note(`\nInstall arca in this project: npm i -D @bremmdev/arca-env`);
2174
+ if (imported) {
2175
+ s.note("\n" + s.err("yellow", `⚠ The ${plural(imported, "secret")} still have plaintext copies in ${devFiles.map((f) => f.name).join(", ")}.`));
2176
+ s.note(" Remove just those entries (config lines stay): npx arca import --delete");
2177
+ } else if (!secretKeys.length) s.note("\nNext: npx arca set DATABASE_URL or npx arca setup");
2178
+ return 0;
2179
+ }
2180
+ /** The arca section of an AGENTS.md: from its heading to the next heading or HTML comment block. */
2181
+ function findSection(text, marker) {
2182
+ const start = text.indexOf(marker);
2183
+ if (start === -1) return null;
2184
+ const next = text.slice(start + marker.length).search(/\n(?=#{1,2} |<!--)/);
2185
+ const end = next === -1 ? text.length : start + marker.length + next + 1;
2186
+ return {
2187
+ start,
2188
+ end,
2189
+ text: text.slice(start, end)
2190
+ };
2191
+ }
2192
+ /** Why `init` leaves a script unwrapped, in words a user can act on. */
2193
+ function skipReason(name, script, scripts, wrapped) {
2194
+ if (name === "start") return "production start, left alone on purpose (the host provides the env, and arca may not be installed there)";
2195
+ if (/&&|\|\||;/.test(script)) {
2196
+ if (script.split(/&&|\|\||;/).map((p) => p.trim()).filter(Boolean).every((part) => {
2197
+ const run = /^(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?([\w:.-]+)/.exec(part)?.[1];
2198
+ if (run) return wrapped.has(run) || !(run in scripts) || !KNOWN_TOOLS.has(firstCommand(scripts[run]) ?? "");
2199
+ const tool = firstCommand(part);
2200
+ return tool !== null && !KNOWN_TOOLS.has(tool);
2201
+ })) return "chains other scripts and tools; the ones that need secrets are wrapped themselves";
2202
+ return "compound command, wrap the part that needs secrets by hand";
2203
+ }
2204
+ const tool = firstCommand(script);
2205
+ if (tool === null) return "sets env vars inline or uses cross-env, wrap by hand if it needs secrets";
2206
+ return `${tool} doesn't need secrets (wrap by hand if it does: arca run -- ${script})`;
2207
+ }
2208
+ function addScript(pkgFile, name, script) {
2209
+ const text = readFileSync(pkgFile, "utf8");
2210
+ const indent = /\n(\s+)"/.exec(text)?.[1] ?? " ";
2211
+ const pkg = JSON.parse(text);
2212
+ pkg.scripts = {
2213
+ ...pkg.scripts,
2214
+ [name]: script
2215
+ };
2216
+ const eol = text.includes("\r\n") ? "\r\n" : "\n";
2217
+ writeFileSync(pkgFile, JSON.stringify(pkg, null, indent).replace(/\n/g, eol) + eol);
2218
+ }
2219
+ function wrapScripts(pkgFile, names) {
2220
+ const text = readFileSync(pkgFile, "utf8");
2221
+ const indent = /\n(\s+)"/.exec(text)?.[1] ?? " ";
2222
+ const pkg = JSON.parse(text);
2223
+ for (const n of names) {
2224
+ const v = pkg.scripts[n];
2225
+ if (!/\barca\s+run\b/.test(v)) pkg.scripts[n] = `arca run -- ${v}`;
2226
+ }
2227
+ const eol = text.includes("\r\n") ? "\r\n" : "\n";
2228
+ writeFileSync(pkgFile, JSON.stringify(pkg, null, indent).replace(/\n/g, eol) + eol);
2229
+ }
2230
+ function safeReaddir(dir) {
2231
+ try {
2232
+ return readdirSync(dir);
2233
+ } catch {
2234
+ return [];
2235
+ }
2236
+ }
2237
+ function keychainLabel(platform) {
2238
+ return platform === "win32" ? "Windows Credential Manager" : platform === "darwin" ? "the macOS Keychain" : "the Secret Service keyring";
2239
+ }
2240
+ //#endregion
2241
+ //#region src/commands/info.ts
2242
+ function rows(s, vault) {
2243
+ const project = s.requireProject();
2244
+ const decls = project.config.vars;
2245
+ const files = s.envFilesForGuard().filter((f) => !isTemplateFile(f.name));
2246
+ const inFile = (name) => files.find((f) => f.entries.some((e) => e.key === name && e.value !== ""));
2247
+ return [.../* @__PURE__ */ new Set([...Object.keys(decls), ...Object.keys(vault.vars)])].map((name) => {
2248
+ const d = decls[name];
2249
+ const stored = vault.vars[name];
2250
+ const sensitive = isSensitive(d);
2251
+ const file = inFile(name);
2252
+ const base = {
2253
+ name,
2254
+ sensitive,
2255
+ required: Boolean(d?.required),
2256
+ declared: Boolean(d),
2257
+ description: d?.description
2258
+ };
2259
+ if (stored) return {
2260
+ ...base,
2261
+ status: "arca",
2262
+ updatedAt: stored.updatedAt
2263
+ };
2264
+ if (file) return {
2265
+ ...base,
2266
+ status: sensitive ? "env-file-sensitive" : "env-file",
2267
+ source: relative(project.root, file.path) || file.name
2268
+ };
2269
+ return {
2270
+ ...base,
2271
+ status: d?.required ? "missing" : "unset"
2272
+ };
2273
+ });
2274
+ }
2275
+ function statusText(r, p) {
2276
+ switch (r.status) {
2277
+ case "arca": return p("green", "✔ arca");
2278
+ case "env-file": return p("cyan", `✔ ${r.source}`);
2279
+ case "env-file-sensitive": return p("yellow", `⚠ ${r.source} (should be in arca)`);
2280
+ case "missing": return p("red", "✖ missing (required)");
2281
+ case "unset": return p("dim", "– not set");
2282
+ }
2283
+ }
2284
+ async function listCommand(s) {
2285
+ const project = s.requireProject();
2286
+ s.warnConfig();
2287
+ const list = rows(s, await s.readVault());
2288
+ if (s.json) {
2289
+ s.print(JSON.stringify(list.map(({ description, ...r }) => ({
2290
+ ...r,
2291
+ description: description ?? null
2292
+ })), null, 2));
2293
+ return 0;
2294
+ }
2295
+ const p = s.out;
2296
+ if (list.length === 0) {
2297
+ s.print(`No variables declared or stored for ${project.config.name} yet. Declare them in arca.json, then run \`npx arca setup\`.`);
2298
+ return 0;
2299
+ }
2300
+ const w = Math.max(4, ...list.map((r) => r.name.length)) + 2;
2301
+ const sw = Math.max(...list.map((r) => statusText(r, (_, t) => t).length)) + 2;
2302
+ s.print(p("bold", `${pad("NAME", w)}${pad("STATUS", sw)}${pad("KIND", 9)}UPDATED`));
2303
+ for (const r of list) {
2304
+ const kind = r.sensitive ? "secret" : "config";
2305
+ const extra = r.declared ? "" : p("dim", " (undeclared)");
2306
+ s.print(`${pad(r.name, w)}${pad(statusText(r, p), sw)}${pad(kind, 9)}${shortDate(r.updatedAt)}${extra}`);
2307
+ }
2308
+ return 0;
2309
+ }
2310
+ async function checkCommand(s, opts) {
2311
+ const project = s.requireProject();
2312
+ s.warnConfig();
2313
+ const vault = await s.readVault();
2314
+ const files = s.envFilesForGuard().filter((f) => !isTemplateFile(f.name));
2315
+ const envFileValues = loadEnvFiles(project.root, project.config.envFiles);
2316
+ const present = (name) => name in vault.vars || Boolean(s.io.env[name]) || Boolean(envFileValues[name]) || files.some((f) => f.entries.some((e) => e.key === name && e.value !== ""));
2317
+ const required = Object.entries(project.config.vars).filter(([, d]) => d.required);
2318
+ const missing = required.filter(([n]) => !present(n)).map(([n]) => n);
2319
+ const misplaced = required.filter(([n, d]) => isSensitive(d) && !(n in vault.vars) && !s.io.env[n] && present(n)).map(([n]) => n);
2320
+ const findings = scanEnvFiles(project, s.envFilesForGuard(), s.secrets(vault));
2321
+ const undeclared = Object.keys(vault.vars).filter((n) => !(n in project.config.vars));
2322
+ if (s.json) s.print(JSON.stringify({
2323
+ ok: missing.length === 0 && (!opts.strict || findings.length === 0),
2324
+ missing,
2325
+ inEnvFiles: misplaced,
2326
+ undeclared,
2327
+ leakGuard: findings
2328
+ }, null, 2));
2329
+ else {
2330
+ const p = s.out;
2331
+ if (missing.length === 0) s.print(`${p("green", "✔")} All ${plural(required.length, "required variable")} set for ${project.config.name}`);
2332
+ else {
2333
+ s.print(`${p("red", "✖")} ${plural(missing.length, "required variable")} missing for ${project.config.name}: ${missing.join(", ")}`);
2334
+ s.print(` Run \`npx arca setup\` to set them. (AI agents: ask the user to run it.)`);
2335
+ }
2336
+ for (const n of misplaced) s.print(`${p("yellow", "⚠")} ${n} is a secret but comes from a .env file. Move it: npx arca import --only ${n} --delete`);
2337
+ if (undeclared.length) s.print(p("dim", ` stored but not declared in arca.json: ${undeclared.join(", ")}`));
2338
+ if (findings.length) s.print(`${p("yellow", "⚠")} Leak guard: ${plural(findings.length, "possible secret")} in .env files (run \`npx arca doctor\` for details)`);
2339
+ }
2340
+ if (missing.length) return 1;
2341
+ if (opts.strict && findings.length) return 1;
2342
+ return 0;
2343
+ }
2344
+ async function doctorCommand(s) {
2345
+ const p = s.out;
2346
+ const ok = (t) => s.print(` ${p("green", "✔")} ${t}`);
2347
+ const warn = (t) => s.print(` ${p("yellow", "⚠")} ${t}`);
2348
+ const bad = (t) => s.print(` ${p("red", "✖")} ${t}`);
2349
+ const info = (t) => s.print(` ${p("dim", "·")} ${t}`);
2350
+ let problems = 0;
2351
+ s.print(p("bold", "Context"));
2352
+ info(`Node ${process.version} on ${s.io.platform}`);
2353
+ const a = s.ctx.agent;
2354
+ if (a.detected) info(`You look like an AI agent: ${a.name} via ${a.via}. Values can't be shown or changed from here.`);
2355
+ else info("No AI agent detected");
2356
+ info(s.ctx.interactiveOutput && s.ctx.stdinIsTTY ? "Interactive terminal" : "Not an interactive terminal");
2357
+ if (s.ctx.ci) info("CI detected: wrapped scripts pass through without a store");
2358
+ s.print(p("bold", "\nStore"));
2359
+ info(`Location: ${s.store.root}`);
2360
+ info(`Key storage: ${chooseBackend(s.store, s.io) === "os" ? osKeychainName(s.io.platform) : "passphrase (master.key.enc)"}`);
2361
+ let master = null;
2362
+ try {
2363
+ master = await loadMasterKey(s.store, s.io, { create: false });
2364
+ if (master) ok("Master key found");
2365
+ else if (s.store.hasAnyVault()) {
2366
+ bad("Master key missing, but vaults exist. They cannot be decrypted.");
2367
+ problems++;
2368
+ } else info("No master key yet (created on the first `npx arca set`)");
2369
+ } catch (e) {
2370
+ bad(e.message.replace(/^arca: /, ""));
2371
+ problems++;
2372
+ }
2373
+ if (!s.project) {
2374
+ s.print(p("bold", "\nProject"));
2375
+ info("No arca.json here or in any parent folder. Run `npx arca init`.");
2376
+ return problems ? 1 : 0;
2377
+ }
2378
+ const project = s.project;
2379
+ s.print(p("bold", "\nProject"));
2380
+ info(`${project.config.name} (${project.config.id})`);
2381
+ info(`Config: ${project.file}`);
2382
+ for (const w of project.warnings) warn(w);
2383
+ const vault = master && s.hasVault() ? s.store.readVault(master, project.config.id, s.profile) : null;
2384
+ if (s.hasVault()) ok(`Vault: ${plural(Object.keys(vault?.vars ?? {}).length, "secret")} stored`);
2385
+ else info("No secrets stored for this project on this machine yet");
2386
+ const reg = s.store.readRegistry()[project.config.id];
2387
+ const remote = gitRemote(project.root);
2388
+ if (reg?.gitRemote && remote && reg.gitRemote !== remote) {
2389
+ warn(`The ID is also used by ${reg.roots[0]} (git remote ${reg.gitRemote}). If these are different projects: npx arca init --new-id`);
2390
+ problems++;
2391
+ }
2392
+ const decls = project.config.vars;
2393
+ const short = Object.entries(vault?.vars ?? {}).filter(([n, v]) => isSensitive(decls[n]) && v.value.length < 6);
2394
+ for (const [n] of short) warn(`${n} is shorter than 6 characters, so it isn't masked in output`);
2395
+ const missing = Object.entries(decls).filter(([n, d]) => d.required && isSensitive(d) && !vault?.vars[n]).map(([n]) => n);
2396
+ if (missing.length) warn(`Required secrets not stored: ${missing.join(", ")}. Run \`npx arca setup\`.`);
2397
+ const pkgFile = join(s.packageRoot() ?? project.root, "package.json");
2398
+ if (existsSync(pkgFile)) {
2399
+ s.print(p("bold", "\nScripts"));
2400
+ const scripts = JSON.parse(readFileSync(pkgFile, "utf8")).scripts ?? {};
2401
+ const wrapped = Object.keys(scripts).filter((k) => /\barca\s+run\b/.test(scripts[k]));
2402
+ const unwrapped = Object.entries(scripts).filter(([k, v]) => k !== "start" && !/\barca\s+run\b/.test(v) && KNOWN_TOOLS.has(firstCommand(v) ?? "")).map(([k]) => k);
2403
+ if (wrapped.length) ok(`Wrapped with arca run: ${wrapped.join(", ")}`);
2404
+ else warn("No scripts use `arca run` yet, so nothing gets secrets. Run `npx arca init` to wrap them.");
2405
+ if (unwrapped.length) info(`Not wrapped (no secrets): ${unwrapped.join(", ")}`);
2406
+ }
2407
+ const modeFiles = project.config.envFiles.filter((f) => /^\.env\.(development|production|test)(\.local)?$/.test(f));
2408
+ for (const f of modeFiles) warn(`envFiles lists ${f}. arca injects it as process env, which beats the framework's own mode files (e.g. in \`next build\`). Prefer a name frameworks don't load.`);
2409
+ s.print(p("bold", "\nLeak guard (.env files)"));
2410
+ const files = s.envFilesForGuard();
2411
+ const findings = scanEnvFiles(project, files, vault ? s.secrets(vault) : []);
2412
+ if (files.length === 0) info("No .env files");
2413
+ else if (findings.length === 0) ok(`${plural(files.length, "file")} checked, no secrets found: ${files.map((f) => f.name).join(", ")}`);
2414
+ for (const f of findings) {
2415
+ (f.tracked ? bad : warn)(`${f.detail}${f.tracked ? " (committed to git!)" : ""}`);
2416
+ s.print(` ${p("dim", fixFor(f))}`);
2417
+ problems++;
2418
+ }
2419
+ const secretPrefixes = new Set(Object.keys(vault?.vars ?? {}).map((n) => n.split("_")[0]));
2420
+ for (const file of files) for (const e of file.entries) {
2421
+ if (!/^https?:\/\//i.test(e.value) || decls[e.key]?.sensitive === true) continue;
2422
+ if (secretPrefixes.has(e.key.split("_")[0]) && !(e.key in (vault?.vars ?? {}))) info(`${e.key} in ${file.name} is a URL next to the secret ${e.key.split("_")[0]}_… If it receives that credential, keep it in arca so only you can change it.`);
2423
+ }
2424
+ if (master) {
2425
+ const policy = s.store.readPolicy(master, project.config.id);
2426
+ s.print(p("bold", "\nAgent policy"));
2427
+ info(policy.agentRun === "scripts" ? "Agents get secrets only through package.json scripts (default)" : "Agents never get secrets (deny)");
2428
+ }
2429
+ s.print("");
2430
+ s.print(problems ? `${p("yellow", `${plural(problems, "issue")} found.`)}` : p("green", "No problems found."));
2431
+ return problems ? 1 : 0;
2432
+ }
2433
+ function fixFor(f) {
2434
+ if (f.rule === "stored") return `fix: delete the line from ${f.file}, or run: npx arca import ${f.file} --only ${f.key} --delete`;
2435
+ if (f.rule === "declared") return `fix: npx arca import ${f.file} --only ${f.key} --delete (or declare it "sensitive": false if it's config)`;
2436
+ return `fix: npx arca import ${f.file} --only ${f.key} --delete (or, if it's not secret, declare it "sensitive": false or add it to leakGuard.ignore)`;
2437
+ }
2438
+ function osKeychainName(platform) {
2439
+ if (platform === "win32") return "Windows Credential Manager";
2440
+ if (platform === "darwin") return "macOS Keychain";
2441
+ return "Secret Service (GNOME Keyring / KWallet)";
2442
+ }
2443
+ async function statusCommand(s, version) {
2444
+ const p = s.out;
2445
+ s.print(`${p("bold", "arca")} ${version}: secrets out of your .env files, masked in all output`);
2446
+ if (!s.project) {
2447
+ s.print("\nNo arca.json in this folder or any parent. Get started: npx arca init");
2448
+ s.print("Commands: npx arca help");
2449
+ return 0;
2450
+ }
2451
+ s.warnConfig();
2452
+ const project = s.project;
2453
+ const vault = await s.readVault();
2454
+ const missing = rows(s, vault).filter((r) => r.status === "missing").map((r) => r.name);
2455
+ s.print(`\nProject ${project.config.name} (${project.config.id})`);
2456
+ s.print(`Secrets ${plural(Object.keys(vault.vars).length, "stored")}${missing.length ? p("red", `, missing: ${missing.join(", ")}`) : ""}`);
2457
+ const policy = await s.policy();
2458
+ s.print(`Agents ${policy.agentRun === "scripts" ? "package scripts only, output masked" : "denied"}`);
2459
+ s.print(`You ${s.ctx.agent.detected ? `AI agent (${s.ctx.agent.name} via ${s.ctx.agent.via})` : s.ctx.interactiveOutput ? "interactive terminal" : "non-interactive"}`);
2460
+ s.print(`Store ${s.store.root}`);
2461
+ s.print(`\n${missing.length ? "Next: npx arca setup" : "Run your app as usual, e.g. npm run dev"} · npx arca help`);
2462
+ return 0;
2463
+ }
2464
+ async function policyCommand(s, args) {
2465
+ const project = s.requireProject();
2466
+ const [action = "show", key, value] = args;
2467
+ if (action === "show") {
2468
+ const policy = await s.policy();
2469
+ s.print(`agent-run: ${policy.agentRun}`);
2470
+ s.print(s.out("dim", policy.agentRun === "scripts" ? " Agents get secrets only through verified package.json scripts (default)." : " Agents never get secrets."));
2471
+ return 0;
2472
+ }
2473
+ if (action !== "set" || key !== "agent-run" || value !== "scripts" && value !== "deny") throw new ArcaError("arca: usage: npx arca policy [show] | npx arca policy set agent-run <scripts|deny>", 2);
2474
+ await humanGate(s.io, s.ctx, {
2475
+ kind: "write",
2476
+ command: `npx arca policy set agent-run ${value}`
2477
+ });
2478
+ const master = await s.masterKey(true);
2479
+ s.store.writePolicy(master, project.config.id, { agentRun: value });
2480
+ s.note(`${s.err("green", "✔")} agent-run is now ${value} for ${project.config.name}`);
2481
+ return 0;
2482
+ }
2483
+ async function projectsCommand(s, args) {
2484
+ const [action, id] = args;
2485
+ const reg = s.store.readRegistry();
2486
+ if (!action || action === "list") {
2487
+ const ids = /* @__PURE__ */ new Set([...Object.keys(reg), ...s.store.projectIds()]);
2488
+ if (ids.size === 0) {
2489
+ s.print("No projects on this machine yet.");
2490
+ return 0;
2491
+ }
2492
+ for (const pid of ids) {
2493
+ const e = reg[pid];
2494
+ s.print(`${pid} ${e?.name ?? "(unknown)"} ${s.out("dim", e?.roots.join(", ") ?? "")}`);
2495
+ }
2496
+ return 0;
2497
+ }
2498
+ if (action !== "rm" || !id) throw new ArcaError("arca: usage: npx arca projects [list] | npx arca projects rm <id>", 2);
2499
+ await humanGate(s.io, s.ctx, {
2500
+ kind: "write",
2501
+ command: `npx arca projects rm ${id}`
2502
+ });
2503
+ if (!reg[id] && !s.store.projectIds().includes(id)) throw new ArcaError(`arca: no project ${id} on this machine.`);
2504
+ if (!await s.io.prompts.confirm(`Delete all secrets of ${reg[id]?.name ?? id} on this machine? This can't be undone.`, false)) throw new ArcaError("arca: cancelled.");
2505
+ s.store.removeProject(id);
2506
+ s.note(`${s.err("green", "✔")} Removed ${id}`);
2507
+ return 0;
2508
+ }
2509
+ //#endregion
2510
+ //#region src/main.ts
2511
+ const VERSION = "0.1.0";
2512
+ const HELP = `arca: keep secrets out of .env files and out of reach of AI agents
2513
+
2514
+ Usage: npx arca <command> [options] (inside package.json scripts, plain "arca" works)
2515
+
2516
+ Everyday
2517
+ run [opts] -- <cmd> Run a command with the project's secrets injected, output masked
2518
+ --only A,B inject only these
2519
+ --passthrough run without the store, checking required vars (like CI)
2520
+ --no-override let values inherited from the shell win over arca
2521
+ list, ls Variables and where each one comes from (names only, never values)
2522
+ check [--strict] Exit 1 if required variables are missing (--strict: also on leak-guard findings)
2523
+ doctor Diagnose keychain, project, scripts and .env files
2524
+
2525
+ Values (only a person at a terminal)
2526
+ set NAME [--generate] Store a value (hidden prompt, or piped: … | arca set NAME)
2527
+ setup [--all] Set every missing required variable, one prompt each
2528
+ unset NAME Remove a value restore NAME Undo the last change
2529
+ reveal NAME Print a value export Print all values (--format dotenv|json|shell, --output file)
2530
+ import [files] [--only A,B] [--include-public] [--delete]
2531
+ Move secrets from .env files into arca. Files are untouched unless --delete.
2532
+
2533
+ Project
2534
+ init [--yes] [--new-id] Set up arca: arca.json, import secrets, wrap scripts, AGENTS.md
2535
+ policy [set agent-run scripts|deny]
2536
+ projects [rm <id>] Projects with secrets on this machine
2537
+
2538
+ Global options: --project-dir <path>, --json, --quiet, --help, --version
2539
+ Docs: https://www.npmjs.com/package/@bremmdev/arca-env`;
2540
+ const GLOBAL_OPTIONS = {
2541
+ "project-dir": { type: "string" },
2542
+ json: { type: "boolean" },
2543
+ quiet: {
2544
+ type: "boolean",
2545
+ short: "q"
2546
+ },
2547
+ help: {
2548
+ type: "boolean",
2549
+ short: "h"
2550
+ }
2551
+ };
2552
+ function parse(args, options = {}) {
2553
+ try {
2554
+ const r = parseArgs({
2555
+ args,
2556
+ options: {
2557
+ ...GLOBAL_OPTIONS,
2558
+ ...options
2559
+ },
2560
+ allowPositionals: true,
2561
+ strict: true
2562
+ });
2563
+ return {
2564
+ values: r.values,
2565
+ positionals: r.positionals
2566
+ };
2567
+ } catch (e) {
2568
+ throw new ArcaError(`arca: ${e.message.replace(/\. To specify a positional.*$/s, "")}\nRun \`npx arca help\` for usage.`, 2);
2569
+ }
2570
+ }
2571
+ const COMMANDS = [
2572
+ "run",
2573
+ "list",
2574
+ "ls",
2575
+ "check",
2576
+ "doctor",
2577
+ "set",
2578
+ "setup",
2579
+ "unset",
2580
+ "restore",
2581
+ "reveal",
2582
+ "export",
2583
+ "import",
2584
+ "init",
2585
+ "policy",
2586
+ "projects",
2587
+ "help",
2588
+ "version"
2589
+ ];
2590
+ async function main(argv, io) {
2591
+ try {
2592
+ return await dispatch(argv, io);
2593
+ } catch (e) {
2594
+ if (e instanceof ArcaError) {
2595
+ if (e.message) io.stderr.write(e.message + "\n");
2596
+ return e.code;
2597
+ }
2598
+ io.stderr.write(`arca: unexpected error: ${e.stack ?? String(e)}\n`);
2599
+ return 1;
2600
+ }
2601
+ }
2602
+ async function dispatch(argv, io) {
2603
+ const [command, ...rest] = argv;
2604
+ if (command === "--version" || command === "-v" || command === "version") {
2605
+ io.stdout.write("0.1.0\n");
2606
+ return 0;
2607
+ }
2608
+ if (command === "--help" || command === "-h" || command === "help") {
2609
+ io.stdout.write(HELP + "\n");
2610
+ return 0;
2611
+ }
2612
+ if (command === void 0 || command.startsWith("-") && command !== "--") {
2613
+ const { values } = parse(argv);
2614
+ return statusCommand(session(io, values), VERSION);
2615
+ }
2616
+ if (command === "run" || command === "--") return run(command === "--" ? ["--", ...rest] : rest, io);
2617
+ if (!COMMANDS.includes(command)) {
2618
+ const guess = suggest(command, COMMANDS);
2619
+ throw new ArcaError(`arca: unknown command "${command}".${guess ? ` Did you mean \`npx arca ${guess}\`?` : ""} Run \`npx arca help\`.`, 2);
2620
+ }
2621
+ switch (command) {
2622
+ case "list":
2623
+ case "ls": {
2624
+ const { values } = parse(rest, profileOption);
2625
+ return listCommand(session(io, values));
2626
+ }
2627
+ case "check": {
2628
+ const { values } = parse(rest, {
2629
+ strict: { type: "boolean" },
2630
+ ...profileOption
2631
+ });
2632
+ return checkCommand(session(io, values), { strict: Boolean(values.strict) });
2633
+ }
2634
+ case "doctor": {
2635
+ const { values } = parse(rest);
2636
+ return doctorCommand(session(io, values));
2637
+ }
2638
+ case "set": {
2639
+ const { values, positionals } = parse(rest.map((a) => a === "--generate" || a === "-g" ? "--generate=default" : a), {
2640
+ generate: { type: "string" },
2641
+ ...profileOption
2642
+ });
2643
+ let generate;
2644
+ if (typeof values.generate === "string") if (values.generate === "default") generate = true;
2645
+ else {
2646
+ generate = Number(values.generate);
2647
+ if (!Number.isInteger(generate) || generate < 8 || generate > 1024) throw new ArcaError("arca: --generate takes a number of bytes from 8 to 1024.", 2);
2648
+ }
2649
+ if (positionals.length > 2) throw new ArcaError("arca: too many arguments. Usage: npx arca set NAME [value]", 2);
2650
+ return setCommand(session(io, values), {
2651
+ name: positionals[0],
2652
+ value: positionals[1],
2653
+ generate
2654
+ });
2655
+ }
2656
+ case "setup": {
2657
+ const { values } = parse(rest, {
2658
+ all: { type: "boolean" },
2659
+ ...profileOption
2660
+ });
2661
+ return setupCommand(session(io, values), { all: Boolean(values.all) });
2662
+ }
2663
+ case "unset":
2664
+ case "restore":
2665
+ case "reveal": {
2666
+ const { values, positionals } = parse(rest, {
2667
+ copy: { type: "boolean" },
2668
+ ...profileOption
2669
+ });
2670
+ if (values.copy) throw new ArcaError("arca: --copy is not available yet. Use `npx arca reveal NAME` in your terminal.", 2);
2671
+ const s = session(io, values);
2672
+ if (command === "unset") return unsetCommand(s, positionals[0]);
2673
+ if (command === "restore") return restoreCommand(s, positionals[0]);
2674
+ return revealCommand(s, positionals[0]);
2675
+ }
2676
+ case "export": {
2677
+ const { values } = parse(rest, {
2678
+ format: { type: "string" },
2679
+ output: {
2680
+ type: "string",
2681
+ short: "o"
2682
+ },
2683
+ ...profileOption
2684
+ });
2685
+ return exportCommand(session(io, values), {
2686
+ format: values.format,
2687
+ output: values.output
2688
+ });
2689
+ }
2690
+ case "import": {
2691
+ const { values, positionals } = parse(rest, {
2692
+ only: { type: "string" },
2693
+ "include-public": { type: "boolean" },
2694
+ delete: { type: "boolean" },
2695
+ yes: {
2696
+ type: "boolean",
2697
+ short: "y"
2698
+ },
2699
+ ...profileOption
2700
+ });
2701
+ return importCommand(session(io, values), {
2702
+ files: positionals,
2703
+ only: splitList(values.only),
2704
+ includePublic: Boolean(values["include-public"]),
2705
+ delete: Boolean(values.delete),
2706
+ yes: Boolean(values.yes)
2707
+ });
2708
+ }
2709
+ case "init": {
2710
+ const { values } = parse(rest, {
2711
+ yes: {
2712
+ type: "boolean",
2713
+ short: "y"
2714
+ },
2715
+ "new-id": { type: "boolean" },
2716
+ "no-import": { type: "boolean" }
2717
+ });
2718
+ return initCommand(session(io, values), {
2719
+ yes: Boolean(values.yes),
2720
+ newId: Boolean(values["new-id"]),
2721
+ import: !values["no-import"]
2722
+ });
2723
+ }
2724
+ case "policy": {
2725
+ const { values, positionals } = parse(rest);
2726
+ return policyCommand(session(io, values), positionals);
2727
+ }
2728
+ case "projects": {
2729
+ const { values, positionals } = parse(rest);
2730
+ return projectsCommand(session(io, values), positionals);
2731
+ }
2732
+ }
2733
+ return 2;
2734
+ }
2735
+ const profileOption = { profile: {
2736
+ type: "string",
2737
+ short: "p"
2738
+ } };
2739
+ function session(io, values) {
2740
+ if (values.help) {
2741
+ io.stdout.write(HELP + "\n");
2742
+ throw new ArcaError("", 0);
2743
+ }
2744
+ if (values.profile !== void 0) throw new ArcaError("arca: profiles are not available in this version yet. Every command uses the project's single set of values.", 2);
2745
+ const s = new Session(io, { projectDir: values["project-dir"] });
2746
+ s.json = Boolean(values.json);
2747
+ s.quiet = Boolean(values.quiet);
2748
+ return s;
2749
+ }
2750
+ function splitList(v) {
2751
+ if (typeof v !== "string") return void 0;
2752
+ return v.split(",").map((x) => x.trim()).filter(Boolean);
2753
+ }
2754
+ /** `arca run [opts] [--] <cmd…>`: arca's options end at `--` or at the first non-option. */
2755
+ async function run(args, io) {
2756
+ const own = [];
2757
+ let i = 0;
2758
+ for (; i < args.length; i++) {
2759
+ const a = args[i];
2760
+ if (a === "--") {
2761
+ i++;
2762
+ break;
2763
+ }
2764
+ if (!a.startsWith("-")) break;
2765
+ own.push(a);
2766
+ if (RUN_OPTIONS_WITH_VALUE.has(a) && i + 1 < args.length) own.push(args[++i]);
2767
+ }
2768
+ const command = args.slice(i);
2769
+ const { values } = parse(own, {
2770
+ only: { type: "string" },
2771
+ passthrough: { type: "boolean" },
2772
+ "no-override": { type: "boolean" },
2773
+ ...profileOption
2774
+ });
2775
+ return runCommand(session(io, values), {
2776
+ command,
2777
+ only: splitList(values.only),
2778
+ passthrough: Boolean(values.passthrough),
2779
+ noOverride: Boolean(values["no-override"])
2780
+ });
2781
+ }
2782
+ //#endregion
2783
+ export { detectAgent as a, parseEnvEntries as i, main as n, detectCI as o, parseEnv as r, processIO as s, VERSION as t };