@attalabs/vinaya 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/aeg-root/enforcement.md +1 -0
- package/dist/index.js +300 -74
- package/package.json +1 -1
- package/templates/custom-check.template.ts +10 -0
package/aeg-root/enforcement.md
CHANGED
|
@@ -44,6 +44,7 @@ The same check implementations run at ring 0 and ring 1 — one codebase, two en
|
|
|
44
44
|
| Starting the Dig (before authoring a brief) / starting Step 0 (before executing one) / **every push on a task branch before its PR exists** | Ever started work only to find out halfway it was blocked on something else? | hook | Refuses to start work until every precondition for the task is checked live and found clear. | **`verify-dispatch` CLI** (`packages/aeg-core/bin/verify-dispatch.ts` — mandated by `roles/developer.md` and `skills/brief-authoring/SKILL.md`; **now also hook-automated** — task 25) | Every dispatch precondition is re-checked live against the forge (the task's row/Issue are derived directly from a Milestone + `vinaya/tranche:<slug>`-labeled Issues as of the forge-native migration — no `origin/main` file read involved; leftover-branch detection still fetches `origin/main` for its own commit-count comparison): the task's row and Issue exist; the Issue passes the rationale gate; every `depends-on` PR is merged and no `conflicts-with` PR is open/in-flight; every named project's prior tranche is archived. (2026-07-13: the immediately-prior task's three-predicate archival bar — the row-adjacency predicate — was removed once the provenance-posting signal it existed to protect became automated.) A `NOT READY` result is a stop condition. Every `gh` call this script makes carries an explicit `-R <owner>/<repo>` (task 23) — from a linked `git worktree add` checkout, an untargeted `gh issue list`/`gh issue view`/`gh pr list` silently scopes to the wrong repo (or returns nothing), which previously produced false prior-tranche-archival blockers. **`.husky/pre-push`** now runs this gate itself on a `task/*/*` branch's first push (before a PR exists), refusing on genuine `NOT READY` and failing OPEN — loudly — when a `gh auth status` reachability probe fails or `verify-dispatch`'s own repo/token resolution fails (task 18's fail-open precedent). The gate reads only the `dispatch-readiness:` predicate, not verify-dispatch's combined exit code, which also folds in `leftover-detection` (a pre-Step-0 "safe to branch fresh" advisory that would otherwise false-block every push after the first on a task legitimately mid-flight — discovered live while building this gate). Once a PR exists, later pushes skip the gate: dispatch was already validated once. | `packages/aeg-core/bin/verify-dispatch.ts` |
|
|
45
45
|
| Opening a task PR whose surface includes real code | Ever had a PR's description quietly stop matching what the code does? | event | Refuses a code-carrying pull request whose description no longer matches what it changes. | **Brief Validation — `checkPremiseCoverage`** | The PR body carries a `Premise:` block with at least one assertion (`contains`/`absent`/`sha256`) whose path falls inside the task's surface map — re-assertable at any point via `verify-dispatch --premise` or `verify-task`. | `packages/aeg-core/src/brief-validation.ts` |
|
|
46
46
|
| Opening a task PR (final self-check before creation) | Ever opened a PR and only then discovered the tests were failing? | event | Runs the whole exit check before a pull request is created, so failures surface first. | **`verify-task` CLI** (`packages/aeg-core/bin/verify-task.ts` — mandated by `roles/developer.md`; **now also hook-automated** — task 25) | Typecheck, lint, tests, build, `verify-docs --pr`, and the premise coverage/recheck pair all pass, as one summary, against the PR's actual diff, before `open-pr.ts` is invoked. **`open-pr.ts` now runs this composite itself** for task branches (via `gatePlanForBranch`), invoked wholesale rather than partially re-implemented — non-task branches are unaffected (byte-identical gate set). `verify-docs --pr` runs twice on a task-branch PR-open as an accepted, measured overlap (~4s with a warm turbo cache). | `packages/aeg-core/bin/verify-task.ts` |
|
|
47
|
+
| Spawning a check (`vinaya check`, ring-0 pre-push AND ring-1 CI) | Ever had a check quietly read a secret it had no business seeing, because nothing scoped what it could reach? | hook | Governs which environment variables a spawned check's child process can see, instead of every check inheriting the full parent environment unconditionally. | **Env allowlist** (`CheckSpec['env']`, `apps/vinaya/cli/src/checks/contract.ts`; construction: `buildCheckEnv`, `apps/vinaya/cli/src/checks/runner.ts`) | **Warn status today (task 2):** the grammar (`true` / `{ optional: true }` / `{ anyOf: [...] }` / a literal string) and the construction function exist and are unit-tested, but `runOne`'s `spawn()` call still passes no `env` override — every check still inherits the full parent environment, unchanged. `vinaya check` prints one warning line per check whose bin reads `process.env`/`Bun.env`/`Deno.env` with no `env` declared (fires identically at the ring-0 pre-push hook and ring-1 CI, since both invoke the same `vinaya check --all`); `vinaya doctor` carries the same diagnostic permanently. All 15 core checks in `registry.ts` carry an audited declaration. **Once flipped (task 3, a later minor):** a spawned check's child process sees only a fixed baseline (`PATH`, `LANG`, `HOME`, `HTTPS_PROXY`, `HTTP_PROXY`, `NO_PROXY`, `TMPDIR`) plus whatever its own `env` declaration explicitly forwards — an undeclared variable a check's own code reads becomes genuinely invisible to it, not merely warned about. | `apps/vinaya/cli/src/checks/runner.ts` |
|
|
47
48
|
|
|
48
49
|
**Documentation coverage, specifically** (a historical pain point): the code→document ownership rule is enforced at *two* prevention chokepoints — at every push (over the branch's cumulative change set) and again at pull-request creation and editing. A change to owned code cannot be published, let alone turned into a pull request, without its owning document.
|
|
49
50
|
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync12 } from "node:fs";
|
|
5
5
|
import { dirname as dirname5, join as join15 } from "node:path";
|
|
6
6
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
7
7
|
|
|
@@ -1276,91 +1276,153 @@ function coreCheckRegistry() {
|
|
|
1276
1276
|
name: "brief-shape",
|
|
1277
1277
|
run: bin("check-brief-shape"),
|
|
1278
1278
|
scope: "diff",
|
|
1279
|
-
timeoutMs: 15000
|
|
1279
|
+
timeoutMs: 15000,
|
|
1280
|
+
env: { PR_BODY: { optional: true } }
|
|
1280
1281
|
},
|
|
1281
1282
|
{
|
|
1282
1283
|
name: "doc-coverage",
|
|
1283
1284
|
run: bin("check-doc-coverage"),
|
|
1284
1285
|
scope: "diff",
|
|
1285
|
-
timeoutMs: 15000
|
|
1286
|
+
timeoutMs: 15000,
|
|
1287
|
+
env: {
|
|
1288
|
+
BASE_SHA: { optional: true },
|
|
1289
|
+
PR_BODY: { optional: true },
|
|
1290
|
+
PR_BODY_FILE: { optional: true },
|
|
1291
|
+
PR_LABELS: { optional: true },
|
|
1292
|
+
WAIVER_LABEL_ACTOR: { optional: true }
|
|
1293
|
+
}
|
|
1286
1294
|
},
|
|
1287
1295
|
{
|
|
1288
1296
|
name: "coherence",
|
|
1289
1297
|
run: bin("check-coherence"),
|
|
1290
1298
|
scope: "full",
|
|
1291
|
-
timeoutMs: 30000
|
|
1299
|
+
timeoutMs: 30000,
|
|
1300
|
+
env: {
|
|
1301
|
+
AEG_REPO: { optional: true },
|
|
1302
|
+
BRANCH: { optional: true },
|
|
1303
|
+
GITHUB_TOKEN: { optional: true },
|
|
1304
|
+
GH_TOKEN: { optional: true }
|
|
1305
|
+
}
|
|
1292
1306
|
},
|
|
1293
1307
|
{
|
|
1294
1308
|
name: "dispatch-readiness",
|
|
1295
1309
|
run: bin("check-dispatch-readiness"),
|
|
1296
1310
|
scope: "full",
|
|
1297
|
-
timeoutMs: 30000
|
|
1311
|
+
timeoutMs: 30000,
|
|
1312
|
+
env: {
|
|
1313
|
+
AEG_REPO: { optional: true },
|
|
1314
|
+
BRANCH: { optional: true },
|
|
1315
|
+
GITHUB_TOKEN: { optional: true },
|
|
1316
|
+
GH_TOKEN: { optional: true }
|
|
1317
|
+
}
|
|
1298
1318
|
},
|
|
1299
1319
|
{
|
|
1300
1320
|
name: "closes-n",
|
|
1301
1321
|
run: bin("check-closes-n"),
|
|
1302
1322
|
scope: "diff",
|
|
1303
|
-
timeoutMs: 15000
|
|
1323
|
+
timeoutMs: 15000,
|
|
1324
|
+
env: {
|
|
1325
|
+
BRANCH: { optional: true },
|
|
1326
|
+
PR_BODY: { optional: true },
|
|
1327
|
+
AEG_REPO: { optional: true }
|
|
1328
|
+
}
|
|
1304
1329
|
},
|
|
1305
1330
|
{
|
|
1306
1331
|
name: "single-plan-pr",
|
|
1307
1332
|
run: bin("check-single-plan-pr"),
|
|
1308
1333
|
scope: "diff",
|
|
1309
|
-
timeoutMs: 15000
|
|
1334
|
+
timeoutMs: 15000,
|
|
1335
|
+
env: {
|
|
1336
|
+
BASE_SHA: { optional: true },
|
|
1337
|
+
PR_NUMBER: { optional: true }
|
|
1338
|
+
}
|
|
1310
1339
|
},
|
|
1311
1340
|
{
|
|
1312
1341
|
name: "test-plan",
|
|
1313
1342
|
run: bin("check-test-plan"),
|
|
1314
1343
|
scope: "diff",
|
|
1315
|
-
timeoutMs: 15000
|
|
1344
|
+
timeoutMs: 15000,
|
|
1345
|
+
env: {
|
|
1346
|
+
PR_BODY: { optional: true },
|
|
1347
|
+
BRANCH: { optional: true }
|
|
1348
|
+
}
|
|
1316
1349
|
},
|
|
1317
1350
|
{
|
|
1318
1351
|
name: "no-disk-state",
|
|
1319
1352
|
run: bin("check-no-disk-state"),
|
|
1320
1353
|
scope: "diff",
|
|
1321
|
-
timeoutMs: 15000
|
|
1354
|
+
timeoutMs: 15000,
|
|
1355
|
+
env: { BASE_SHA: { optional: true } }
|
|
1322
1356
|
},
|
|
1323
1357
|
{
|
|
1324
1358
|
name: "registry-gates",
|
|
1325
1359
|
run: bin("check-registry-gates"),
|
|
1326
1360
|
scope: "full",
|
|
1327
|
-
timeoutMs: 30000
|
|
1361
|
+
timeoutMs: 30000,
|
|
1362
|
+
env: { AEG_REPO: { optional: true } }
|
|
1328
1363
|
},
|
|
1329
1364
|
{
|
|
1330
1365
|
name: "review-gate",
|
|
1331
1366
|
run: bin("check-review-gate"),
|
|
1332
1367
|
scope: "full",
|
|
1333
|
-
timeoutMs: 30000
|
|
1368
|
+
timeoutMs: 30000,
|
|
1369
|
+
env: {
|
|
1370
|
+
BRANCH: { optional: true },
|
|
1371
|
+
PR_NUMBER: { optional: true }
|
|
1372
|
+
}
|
|
1334
1373
|
},
|
|
1335
1374
|
{
|
|
1336
1375
|
name: "branch-topology",
|
|
1337
1376
|
run: bin("check-branch-topology"),
|
|
1338
1377
|
scope: "full",
|
|
1339
|
-
timeoutMs: 30000
|
|
1378
|
+
timeoutMs: 30000,
|
|
1379
|
+
env: {
|
|
1380
|
+
BRANCH: { optional: true },
|
|
1381
|
+
AEG_REPO: { optional: true }
|
|
1382
|
+
}
|
|
1340
1383
|
},
|
|
1341
1384
|
{
|
|
1342
1385
|
name: "dead-branch-push",
|
|
1343
1386
|
run: bin("check-dead-branch-push"),
|
|
1344
1387
|
scope: "full",
|
|
1345
|
-
timeoutMs: 30000
|
|
1388
|
+
timeoutMs: 30000,
|
|
1389
|
+
env: { BRANCH: { optional: true } }
|
|
1346
1390
|
},
|
|
1347
1391
|
{
|
|
1348
1392
|
name: "first-push-dispatch",
|
|
1349
1393
|
run: bin("check-first-push-dispatch"),
|
|
1350
1394
|
scope: "full",
|
|
1351
|
-
timeoutMs: 30000
|
|
1395
|
+
timeoutMs: 30000,
|
|
1396
|
+
env: {
|
|
1397
|
+
BRANCH: { optional: true },
|
|
1398
|
+
AEG_REPO: { optional: true },
|
|
1399
|
+
GITHUB_TOKEN: { optional: true },
|
|
1400
|
+
GH_TOKEN: { optional: true }
|
|
1401
|
+
}
|
|
1352
1402
|
},
|
|
1353
1403
|
{
|
|
1354
1404
|
name: "doc-coverage-push",
|
|
1355
1405
|
run: bin("check-doc-coverage-push"),
|
|
1356
1406
|
scope: "diff",
|
|
1357
|
-
timeoutMs: 15000
|
|
1407
|
+
timeoutMs: 15000,
|
|
1408
|
+
env: {
|
|
1409
|
+
OVERRIDE_DOCS: { optional: true },
|
|
1410
|
+
BASE_SHA: { optional: true },
|
|
1411
|
+
PR_BODY: { optional: true },
|
|
1412
|
+
PR_BODY_FILE: { optional: true },
|
|
1413
|
+
PR_LABELS: { optional: true },
|
|
1414
|
+
WAIVER_LABEL_ACTOR: { optional: true }
|
|
1415
|
+
}
|
|
1358
1416
|
},
|
|
1359
1417
|
{
|
|
1360
1418
|
name: "issue-assignment",
|
|
1361
1419
|
run: bin("check-issue-assignment"),
|
|
1362
1420
|
scope: "full",
|
|
1363
|
-
timeoutMs: 30000
|
|
1421
|
+
timeoutMs: 30000,
|
|
1422
|
+
env: {
|
|
1423
|
+
AEG_REPO: { optional: true },
|
|
1424
|
+
BRANCH: { optional: true }
|
|
1425
|
+
}
|
|
1364
1426
|
}
|
|
1365
1427
|
];
|
|
1366
1428
|
}
|
|
@@ -1390,17 +1452,70 @@ function shouldSkip(spec, opts) {
|
|
|
1390
1452
|
return !opts.changedFiles.some((f) => regexes.some((re) => re.test(f)));
|
|
1391
1453
|
}
|
|
1392
1454
|
var KILL_GRACE_MS = 2000;
|
|
1455
|
+
var activeKillers = new Set;
|
|
1456
|
+
var signalForwardingInstalled = false;
|
|
1457
|
+
function installSignalForwarding() {
|
|
1458
|
+
if (signalForwardingInstalled)
|
|
1459
|
+
return;
|
|
1460
|
+
signalForwardingInstalled = true;
|
|
1461
|
+
const forward = (_signal, exitCode) => {
|
|
1462
|
+
for (const kill of activeKillers)
|
|
1463
|
+
kill("SIGTERM");
|
|
1464
|
+
setTimeout(() => {
|
|
1465
|
+
for (const kill of activeKillers)
|
|
1466
|
+
kill("SIGKILL");
|
|
1467
|
+
process.exit(exitCode);
|
|
1468
|
+
}, KILL_GRACE_MS);
|
|
1469
|
+
};
|
|
1470
|
+
process.on("SIGINT", () => forward("SIGINT", 130));
|
|
1471
|
+
process.on("SIGTERM", () => forward("SIGTERM", 143));
|
|
1472
|
+
}
|
|
1393
1473
|
async function runOne(spec, timeoutMs) {
|
|
1394
1474
|
const start = performance.now();
|
|
1395
1475
|
const proc = spawn(spec.run, spec.args ?? [], {
|
|
1396
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
1476
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1477
|
+
detached: true
|
|
1397
1478
|
});
|
|
1479
|
+
function killTree(signal) {
|
|
1480
|
+
const pid = proc.pid;
|
|
1481
|
+
if (pid === undefined)
|
|
1482
|
+
return;
|
|
1483
|
+
try {
|
|
1484
|
+
process.kill(-pid, signal);
|
|
1485
|
+
} catch {
|
|
1486
|
+
try {
|
|
1487
|
+
proc.kill(signal);
|
|
1488
|
+
} catch {}
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
installSignalForwarding();
|
|
1492
|
+
activeKillers.add(killTree);
|
|
1493
|
+
function groupStillAlive(pid) {
|
|
1494
|
+
try {
|
|
1495
|
+
process.kill(-pid, 0);
|
|
1496
|
+
return true;
|
|
1497
|
+
} catch {
|
|
1498
|
+
return false;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1398
1501
|
let timedOut = false;
|
|
1399
|
-
let
|
|
1502
|
+
let escalationDone = Promise.resolve();
|
|
1503
|
+
let cancelEscalation;
|
|
1400
1504
|
const timer = setTimeout(() => {
|
|
1401
1505
|
timedOut = true;
|
|
1402
|
-
|
|
1403
|
-
|
|
1506
|
+
killTree("SIGTERM");
|
|
1507
|
+
escalationDone = new Promise((resolve) => {
|
|
1508
|
+
const graceTimer = setTimeout(() => {
|
|
1509
|
+
const pid = proc.pid;
|
|
1510
|
+
if (pid !== undefined && groupStillAlive(pid))
|
|
1511
|
+
killTree("SIGKILL");
|
|
1512
|
+
resolve();
|
|
1513
|
+
}, KILL_GRACE_MS);
|
|
1514
|
+
cancelEscalation = () => {
|
|
1515
|
+
clearTimeout(graceTimer);
|
|
1516
|
+
resolve();
|
|
1517
|
+
};
|
|
1518
|
+
});
|
|
1404
1519
|
}, timeoutMs);
|
|
1405
1520
|
let stderrText = "";
|
|
1406
1521
|
proc.stderr.setEncoding("utf-8");
|
|
@@ -1417,7 +1532,13 @@ async function runOne(spec, timeoutMs) {
|
|
|
1417
1532
|
});
|
|
1418
1533
|
});
|
|
1419
1534
|
clearTimeout(timer);
|
|
1420
|
-
|
|
1535
|
+
if (timedOut) {
|
|
1536
|
+
const pid = proc.pid;
|
|
1537
|
+
if (pid === undefined || !groupStillAlive(pid))
|
|
1538
|
+
cancelEscalation?.();
|
|
1539
|
+
await escalationDone;
|
|
1540
|
+
}
|
|
1541
|
+
activeKillers.delete(killTree);
|
|
1421
1542
|
const durationMs = performance.now() - start;
|
|
1422
1543
|
if (timedOut) {
|
|
1423
1544
|
return { name: spec.name, status: "timeout", exitCode: null, errors: [], durationMs };
|
|
@@ -1498,13 +1619,70 @@ import { existsSync as existsSync4, mkdirSync, readFileSync, writeFileSync } fro
|
|
|
1498
1619
|
import { homedir } from "node:os";
|
|
1499
1620
|
import { dirname as dirname2, join as join4 } from "node:path";
|
|
1500
1621
|
import { z } from "zod";
|
|
1622
|
+
var EnvEntrySchema = z.union([
|
|
1623
|
+
z.literal(true),
|
|
1624
|
+
z.object({ optional: z.literal(true) }),
|
|
1625
|
+
z.object({ anyOf: z.array(z.string()).min(2) }),
|
|
1626
|
+
z.string()
|
|
1627
|
+
]);
|
|
1501
1628
|
var CheckEntrySchema = z.object({
|
|
1502
1629
|
run: z.string(),
|
|
1503
1630
|
scope: z.enum(["diff", "full"]),
|
|
1504
1631
|
include: z.array(z.string()).optional(),
|
|
1505
1632
|
args: z.array(z.string()).optional(),
|
|
1506
|
-
timeoutMs: z.number().optional()
|
|
1633
|
+
timeoutMs: z.number().optional(),
|
|
1634
|
+
env: z.record(z.string(), EnvEntrySchema).optional()
|
|
1635
|
+
}).superRefine((entry2, ctx) => {
|
|
1636
|
+
if (!entry2.env)
|
|
1637
|
+
return;
|
|
1638
|
+
for (const [key, value] of Object.entries(entry2.env)) {
|
|
1639
|
+
if (typeof value !== "object" || value === null || !("anyOf" in value))
|
|
1640
|
+
continue;
|
|
1641
|
+
const members = value.anyOf;
|
|
1642
|
+
if (new Set(members).size !== members.length) {
|
|
1643
|
+
ctx.addIssue({
|
|
1644
|
+
code: z.ZodIssueCode.custom,
|
|
1645
|
+
message: `env.${key}.anyOf has duplicate members`,
|
|
1646
|
+
path: ["env", key, "anyOf"]
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1649
|
+
if (!members.includes(key)) {
|
|
1650
|
+
ctx.addIssue({
|
|
1651
|
+
code: z.ZodIssueCode.custom,
|
|
1652
|
+
message: `env.${key}.anyOf must include "${key}" itself as a member`,
|
|
1653
|
+
path: ["env", key, "anyOf"]
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1507
1657
|
});
|
|
1658
|
+
var HIGH_ENTROPY_MIN_LENGTH = 20;
|
|
1659
|
+
function looksHighEntropy(value) {
|
|
1660
|
+
if (value.length < HIGH_ENTROPY_MIN_LENGTH)
|
|
1661
|
+
return false;
|
|
1662
|
+
if (/\s/.test(value))
|
|
1663
|
+
return false;
|
|
1664
|
+
const classes = [/[a-z]/.test(value), /[A-Z]/.test(value), /[0-9]/.test(value)].filter(Boolean).length;
|
|
1665
|
+
return classes >= 2;
|
|
1666
|
+
}
|
|
1667
|
+
function lintEnvDeclarations(checks) {
|
|
1668
|
+
const warnings = [];
|
|
1669
|
+
if (!checks)
|
|
1670
|
+
return warnings;
|
|
1671
|
+
for (const [checkName, entry2] of Object.entries(checks)) {
|
|
1672
|
+
if (!entry2.env)
|
|
1673
|
+
continue;
|
|
1674
|
+
for (const [key, value] of Object.entries(entry2.env)) {
|
|
1675
|
+
if (typeof value !== "string")
|
|
1676
|
+
continue;
|
|
1677
|
+
if (value === "true" || value === "false") {
|
|
1678
|
+
warnings.push(`check "${checkName}" env.${key} is the literal string "${value}" — did you mean the boolean form \`${key}: true\` (forward the caller's value) rather than a hardcoded literal?`);
|
|
1679
|
+
} else if (looksHighEntropy(value)) {
|
|
1680
|
+
warnings.push(`check "${checkName}" env.${key} looks like a high-entropy literal (possible secret committed to config) — env declarations should reference variable NAMES the caller sets, not literal secret values.`);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
return warnings;
|
|
1685
|
+
}
|
|
1508
1686
|
var BRIEF_BUILTINS = [
|
|
1509
1687
|
"tier",
|
|
1510
1688
|
"testPlan",
|
|
@@ -1603,6 +1781,31 @@ function loadConfigChecked() {
|
|
|
1603
1781
|
return { ok: true, config: parsed.data };
|
|
1604
1782
|
}
|
|
1605
1783
|
|
|
1784
|
+
// src/lib/env-lint.ts
|
|
1785
|
+
import { existsSync as existsSync5, readFileSync as readFileSync2 } from "node:fs";
|
|
1786
|
+
var ENV_READ_PATTERN = /\b(?:process\.env|Bun\.env|Deno\.env)\b/;
|
|
1787
|
+
function checksMissingEnvDeclaration(specs) {
|
|
1788
|
+
const names = [];
|
|
1789
|
+
for (const spec of specs) {
|
|
1790
|
+
if (spec.env)
|
|
1791
|
+
continue;
|
|
1792
|
+
if (!existsSync5(spec.run))
|
|
1793
|
+
continue;
|
|
1794
|
+
let source;
|
|
1795
|
+
try {
|
|
1796
|
+
source = readFileSync2(spec.run, "utf8");
|
|
1797
|
+
} catch {
|
|
1798
|
+
continue;
|
|
1799
|
+
}
|
|
1800
|
+
if (ENV_READ_PATTERN.test(source))
|
|
1801
|
+
names.push(spec.name);
|
|
1802
|
+
}
|
|
1803
|
+
return names;
|
|
1804
|
+
}
|
|
1805
|
+
function envDeclarationWarning(name) {
|
|
1806
|
+
return `check "${name}" reads environment variables directly but declares no \`env\` — it will lose environment access at the next minor. Declare \`env\` for it (CheckSpec['env'] in src/checks/contract.ts, or vinaya.config.json's checks.<name>.env).`;
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1606
1809
|
// src/commands/check.ts
|
|
1607
1810
|
function git(args) {
|
|
1608
1811
|
try {
|
|
@@ -1665,6 +1868,9 @@ async function checkCommand(args) {
|
|
|
1665
1868
|
console.error(`Unknown check: ${requestedName}`);
|
|
1666
1869
|
process.exit(2);
|
|
1667
1870
|
}
|
|
1871
|
+
for (const name of checksMissingEnvDeclaration(specsToRun)) {
|
|
1872
|
+
console.error(`⚠ ${envDeclarationWarning(name)}`);
|
|
1873
|
+
}
|
|
1668
1874
|
const changed = diffOnly ? changedFiles() : null;
|
|
1669
1875
|
const outcomes = specsToRun.length > 0 ? await runChecks(specsToRun, {
|
|
1670
1876
|
parallel: requestedParallel ?? defaultParallelism(),
|
|
@@ -1695,7 +1901,7 @@ async function checkCommand(args) {
|
|
|
1695
1901
|
|
|
1696
1902
|
// src/commands/demo.ts
|
|
1697
1903
|
import { execFileSync as execFileSync5, spawnSync } from "node:child_process";
|
|
1698
|
-
import { existsSync as
|
|
1904
|
+
import { existsSync as existsSync6, readFileSync as readFileSync3, rmSync, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1699
1905
|
import { isAbsolute, join as join5 } from "node:path";
|
|
1700
1906
|
var DEMO_BRANCH_PREFIX = "vinaya/demo-break-";
|
|
1701
1907
|
var FIXTURE_PATH = ".vinaya-demo-brief.md";
|
|
@@ -1738,9 +1944,9 @@ function gitDirAbs(repoRoot) {
|
|
|
1738
1944
|
function recoverFromCrash(repoRoot, statePath) {
|
|
1739
1945
|
const currentBranch = currentBranchName(repoRoot);
|
|
1740
1946
|
let state = null;
|
|
1741
|
-
if (
|
|
1947
|
+
if (existsSync6(statePath)) {
|
|
1742
1948
|
try {
|
|
1743
|
-
state = JSON.parse(
|
|
1949
|
+
state = JSON.parse(readFileSync3(statePath, "utf-8"));
|
|
1744
1950
|
} catch {
|
|
1745
1951
|
state = null;
|
|
1746
1952
|
}
|
|
@@ -1810,14 +2016,14 @@ function cleanup(repoRoot, originalBranch, demoBranch, statePath) {
|
|
|
1810
2016
|
gitCapture(repoRoot, ["clean", "-fd"]);
|
|
1811
2017
|
gitCapture(repoRoot, ["checkout", originalBranch]);
|
|
1812
2018
|
gitCapture(repoRoot, ["branch", "-D", demoBranch]);
|
|
1813
|
-
if (
|
|
2019
|
+
if (existsSync6(statePath))
|
|
1814
2020
|
rmSync(statePath, { force: true });
|
|
1815
2021
|
}
|
|
1816
2022
|
async function runDemoBreak(repoRoot, args) {
|
|
1817
2023
|
const keep = args.includes("--keep");
|
|
1818
2024
|
const hookDir = resolveHookDir(repoRoot);
|
|
1819
2025
|
const hookPath = join5(repoRoot, hookDir, "pre-commit");
|
|
1820
|
-
if (!
|
|
2026
|
+
if (!existsSync6(hookPath)) {
|
|
1821
2027
|
console.error("Vinaya hooks are not installed in this repo. Run `vinaya init` first.");
|
|
1822
2028
|
return 1;
|
|
1823
2029
|
}
|
|
@@ -1875,7 +2081,7 @@ Attempting to commit…
|
|
|
1875
2081
|
process.stdout.write(`✓ Commit passed — the fix worked.
|
|
1876
2082
|
`);
|
|
1877
2083
|
if (keep) {
|
|
1878
|
-
if (
|
|
2084
|
+
if (existsSync6(statePath))
|
|
1879
2085
|
unlinkSync(statePath);
|
|
1880
2086
|
process.stdout.write(`
|
|
1881
2087
|
--keep: leaving \`${demoBranch}\` checked out for you to inspect. Clean up with:
|
|
@@ -1902,7 +2108,7 @@ async function demoBreakCommand(args) {
|
|
|
1902
2108
|
}
|
|
1903
2109
|
|
|
1904
2110
|
// src/commands/doctor.ts
|
|
1905
|
-
import { existsSync as
|
|
2111
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5, statSync } from "node:fs";
|
|
1906
2112
|
import { join as join8 } from "node:path";
|
|
1907
2113
|
|
|
1908
2114
|
// src/lib/artifacts.ts
|
|
@@ -2272,7 +2478,7 @@ function buildInitProductOps(name) {
|
|
|
2272
2478
|
}
|
|
2273
2479
|
|
|
2274
2480
|
// src/lib/ops.ts
|
|
2275
|
-
import { chmodSync, existsSync as
|
|
2481
|
+
import { chmodSync, existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync4, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
2276
2482
|
import { dirname as dirname3, join as join7, resolve, sep } from "node:path";
|
|
2277
2483
|
var MARKER_NS = "vinaya:managed";
|
|
2278
2484
|
function markerLines(marker, comment) {
|
|
@@ -2300,9 +2506,9 @@ function containedAbs(repoRoot, relPath) {
|
|
|
2300
2506
|
}
|
|
2301
2507
|
function fileContains(repoRoot, relPath, needle) {
|
|
2302
2508
|
const p = abs(repoRoot, relPath);
|
|
2303
|
-
if (!
|
|
2509
|
+
if (!existsSync7(p))
|
|
2304
2510
|
return false;
|
|
2305
|
-
return
|
|
2511
|
+
return readFileSync4(p, "utf-8").includes(needle);
|
|
2306
2512
|
}
|
|
2307
2513
|
function planInstall(ops, repoRoot, ownedFiles = new Set) {
|
|
2308
2514
|
const entries = [];
|
|
@@ -2310,7 +2516,7 @@ function planInstall(ops, repoRoot, ownedFiles = new Set) {
|
|
|
2310
2516
|
for (const op of ops) {
|
|
2311
2517
|
switch (op.kind) {
|
|
2312
2518
|
case "create-file": {
|
|
2313
|
-
const exists =
|
|
2519
|
+
const exists = existsSync7(abs(repoRoot, op.path));
|
|
2314
2520
|
let action;
|
|
2315
2521
|
if (!exists)
|
|
2316
2522
|
action = "create";
|
|
@@ -2328,7 +2534,7 @@ function planInstall(ops, repoRoot, ownedFiles = new Set) {
|
|
|
2328
2534
|
let action;
|
|
2329
2535
|
if (fileContains(repoRoot, op.path, begin))
|
|
2330
2536
|
action = "skip-present";
|
|
2331
|
-
else if (
|
|
2537
|
+
else if (existsSync7(abs(repoRoot, op.path)))
|
|
2332
2538
|
action = "append";
|
|
2333
2539
|
else
|
|
2334
2540
|
action = "create-host";
|
|
@@ -2412,7 +2618,7 @@ function writeFileWithDirs(target, content, mode) {
|
|
|
2412
2618
|
}
|
|
2413
2619
|
function appendBlock(repoRoot, op) {
|
|
2414
2620
|
const target = abs(repoRoot, op.path);
|
|
2415
|
-
const existing =
|
|
2621
|
+
const existing = readFileSync4(target, "utf-8");
|
|
2416
2622
|
const sep2 = existing.endsWith(`
|
|
2417
2623
|
`) ? `
|
|
2418
2624
|
` : `
|
|
@@ -2523,7 +2729,7 @@ function planEject(manifest, repoRoot) {
|
|
|
2523
2729
|
escapes.push(b.path);
|
|
2524
2730
|
continue;
|
|
2525
2731
|
}
|
|
2526
|
-
if (!
|
|
2732
|
+
if (!existsSync7(p)) {
|
|
2527
2733
|
actions.push({
|
|
2528
2734
|
kind: "strip-block",
|
|
2529
2735
|
path: b.path,
|
|
@@ -2534,7 +2740,7 @@ function planEject(manifest, repoRoot) {
|
|
|
2534
2740
|
});
|
|
2535
2741
|
continue;
|
|
2536
2742
|
}
|
|
2537
|
-
const content =
|
|
2743
|
+
const content = readFileSync4(p, "utf-8");
|
|
2538
2744
|
const stripped = stripBlockFromContent(content, b.marker, b.comment);
|
|
2539
2745
|
const removesHost = stripped !== null && blockStripLeavesEmpty(stripped);
|
|
2540
2746
|
actions.push({
|
|
@@ -2552,7 +2758,7 @@ function planEject(manifest, repoRoot) {
|
|
|
2552
2758
|
escapes.push(f);
|
|
2553
2759
|
continue;
|
|
2554
2760
|
}
|
|
2555
|
-
actions.push({ kind: "delete-file", path: f, present:
|
|
2761
|
+
actions.push({ kind: "delete-file", path: f, present: existsSync7(p) });
|
|
2556
2762
|
}
|
|
2557
2763
|
for (const name of manifest.labels) {
|
|
2558
2764
|
actions.push({ kind: "report-label", name });
|
|
@@ -2586,7 +2792,7 @@ function applyEject(plan, repoRoot) {
|
|
|
2586
2792
|
const p = containedAbs(repoRoot, a.path);
|
|
2587
2793
|
if (p === null)
|
|
2588
2794
|
continue;
|
|
2589
|
-
const content =
|
|
2795
|
+
const content = readFileSync4(p, "utf-8");
|
|
2590
2796
|
const stripped = stripBlockFromContent(content, a.marker, a.comment);
|
|
2591
2797
|
if (stripped === null)
|
|
2592
2798
|
continue;
|
|
@@ -2609,7 +2815,7 @@ function applyEject(plan, repoRoot) {
|
|
|
2609
2815
|
|
|
2610
2816
|
// src/commands/doctor.ts
|
|
2611
2817
|
function readVersion() {
|
|
2612
|
-
const pkg = JSON.parse(
|
|
2818
|
+
const pkg = JSON.parse(readFileSync5(join8(packageRoot(import.meta.url), "package.json"), "utf-8"));
|
|
2613
2819
|
return pkg.version;
|
|
2614
2820
|
}
|
|
2615
2821
|
function realDeps3() {
|
|
@@ -2629,11 +2835,11 @@ var warn = (check, message) => ({ check, severity: "warn", message });
|
|
|
2629
2835
|
var error = (check, message) => ({ check, severity: "error", message });
|
|
2630
2836
|
function readConfig(repoRoot) {
|
|
2631
2837
|
const p = join8(repoRoot, CONFIG_PATH);
|
|
2632
|
-
if (!
|
|
2838
|
+
if (!existsSync8(p))
|
|
2633
2839
|
return { kind: "missing" };
|
|
2634
2840
|
let raw;
|
|
2635
2841
|
try {
|
|
2636
|
-
raw = JSON.parse(
|
|
2842
|
+
raw = JSON.parse(readFileSync5(p, "utf-8"));
|
|
2637
2843
|
} catch (err) {
|
|
2638
2844
|
return { kind: "invalid", error: `invalid JSON: ${err.message}` };
|
|
2639
2845
|
}
|
|
@@ -2663,13 +2869,13 @@ function diagnoseInstall(repoRoot, ctx, manifest) {
|
|
|
2663
2869
|
if (op.kind === "create-file") {
|
|
2664
2870
|
const check = labelForPath(op.path);
|
|
2665
2871
|
const abs2 = join8(repoRoot, op.path);
|
|
2666
|
-
const exists =
|
|
2872
|
+
const exists = existsSync8(abs2);
|
|
2667
2873
|
const owned = ownedFiles.has(op.path);
|
|
2668
2874
|
if (!exists) {
|
|
2669
2875
|
findings.push(owned ? error(check, `${op.path} is recorded as vinaya-managed but missing on disk — run \`vinaya upgrade\`.`) : error(check, `${op.path} is not installed — run \`vinaya init\`.`));
|
|
2670
2876
|
continue;
|
|
2671
2877
|
}
|
|
2672
|
-
const content =
|
|
2878
|
+
const content = readFileSync5(abs2, "utf-8");
|
|
2673
2879
|
if (!owned) {
|
|
2674
2880
|
findings.push(content === op.content ? warn(check, `${op.path} has vinaya's own content but isn't recorded in the manifest.`) : info(check, `${op.path} exists but is foreign content — not vinaya-managed, left untouched.`));
|
|
2675
2881
|
continue;
|
|
@@ -2688,11 +2894,11 @@ function diagnoseInstall(repoRoot, ctx, manifest) {
|
|
|
2688
2894
|
const check = "hooks";
|
|
2689
2895
|
const abs2 = join8(repoRoot, op.path);
|
|
2690
2896
|
const owned = ownedBlocks.has(blockKey(op.path, op.marker));
|
|
2691
|
-
if (!
|
|
2897
|
+
if (!existsSync8(abs2)) {
|
|
2692
2898
|
findings.push(owned ? error(check, `${op.path} is missing — likely a fresh clone (raw git hooks aren't tracked by git). Run \`vinaya upgrade\` to restore it.`) : info(check, `${op.path} is not installed.`));
|
|
2693
2899
|
continue;
|
|
2694
2900
|
}
|
|
2695
|
-
const content =
|
|
2901
|
+
const content = readFileSync5(abs2, "utf-8");
|
|
2696
2902
|
const { begin, end } = markerLines(op.marker, op.comment);
|
|
2697
2903
|
const hasMarkers = content.includes(begin) && content.includes(end);
|
|
2698
2904
|
if (!hasMarkers) {
|
|
@@ -2718,7 +2924,20 @@ function diagnoseCustomChecks(repoRoot, config) {
|
|
|
2718
2924
|
const findings = [];
|
|
2719
2925
|
for (const [name, entry2] of Object.entries(config.checks ?? {})) {
|
|
2720
2926
|
const scriptAbs = join8(repoRoot, entry2.run);
|
|
2721
|
-
findings.push(
|
|
2927
|
+
findings.push(existsSync8(scriptAbs) ? ok("checks", `custom check '${name}' → ${entry2.run}`) : error("checks", `custom check '${name}' points at a missing script: ${entry2.run}`));
|
|
2928
|
+
}
|
|
2929
|
+
return findings;
|
|
2930
|
+
}
|
|
2931
|
+
function diagnoseEnvDeclarations(repoRoot, config) {
|
|
2932
|
+
const customSpecs = Object.entries(config?.checks ?? {}).map(([name, entry2]) => ({
|
|
2933
|
+
name,
|
|
2934
|
+
...entry2,
|
|
2935
|
+
run: join8(repoRoot, entry2.run)
|
|
2936
|
+
}));
|
|
2937
|
+
const missing = checksMissingEnvDeclaration([...coreCheckRegistry(), ...customSpecs]);
|
|
2938
|
+
const findings = missing.map((name) => info("env", envDeclarationWarning(name)));
|
|
2939
|
+
for (const message of lintEnvDeclarations(config?.checks)) {
|
|
2940
|
+
findings.push(warn("env", message));
|
|
2722
2941
|
}
|
|
2723
2942
|
return findings;
|
|
2724
2943
|
}
|
|
@@ -2791,6 +3010,7 @@ async function runDoctor(args, deps) {
|
|
|
2791
3010
|
hasDrift = install.hasDrift;
|
|
2792
3011
|
findings.push(...diagnoseCustomChecks(repo.repoRoot, configRead.config));
|
|
2793
3012
|
}
|
|
3013
|
+
findings.push(...diagnoseEnvDeclarations(repo.repoRoot, configRead.kind === "ok" ? configRead.config : null));
|
|
2794
3014
|
findings.push(...await diagnoseEnvironment(deps, hasDrift));
|
|
2795
3015
|
findings.push(await diagnoseBranchProtection(deps, repo.owner, repo.repo));
|
|
2796
3016
|
const healthy = findings.every((f) => f.severity === "ok" || f.severity === "info");
|
|
@@ -2806,7 +3026,7 @@ async function doctorCommand(args) {
|
|
|
2806
3026
|
}
|
|
2807
3027
|
|
|
2808
3028
|
// src/commands/eject.ts
|
|
2809
|
-
import { existsSync as
|
|
3029
|
+
import { existsSync as existsSync9, readFileSync as readFileSync6 } from "node:fs";
|
|
2810
3030
|
import { join as join9 } from "node:path";
|
|
2811
3031
|
|
|
2812
3032
|
// src/lib/prompt.ts
|
|
@@ -2889,11 +3109,11 @@ function parse(args) {
|
|
|
2889
3109
|
}
|
|
2890
3110
|
function readManifest(repoRoot) {
|
|
2891
3111
|
const p = join9(repoRoot, CONFIG_PATH);
|
|
2892
|
-
if (!
|
|
3112
|
+
if (!existsSync9(p))
|
|
2893
3113
|
return { kind: "none" };
|
|
2894
3114
|
let raw;
|
|
2895
3115
|
try {
|
|
2896
|
-
raw = JSON.parse(
|
|
3116
|
+
raw = JSON.parse(readFileSync6(p, "utf-8"));
|
|
2897
3117
|
} catch (err) {
|
|
2898
3118
|
return { kind: "orphan", reason: `vinaya.config.json is not valid JSON: ${err.message}` };
|
|
2899
3119
|
}
|
|
@@ -2976,7 +3196,7 @@ async function ejectCommand(args) {
|
|
|
2976
3196
|
}
|
|
2977
3197
|
|
|
2978
3198
|
// src/commands/init.ts
|
|
2979
|
-
import { existsSync as
|
|
3199
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
|
|
2980
3200
|
import { join as join10 } from "node:path";
|
|
2981
3201
|
function realDeps5() {
|
|
2982
3202
|
return {
|
|
@@ -2998,17 +3218,17 @@ function flags(args) {
|
|
|
2998
3218
|
var PRODUCT_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
2999
3219
|
function readManifest2(repoRoot) {
|
|
3000
3220
|
const p = join10(repoRoot, CONFIG_PATH);
|
|
3001
|
-
if (!
|
|
3221
|
+
if (!existsSync10(p))
|
|
3002
3222
|
return null;
|
|
3003
3223
|
try {
|
|
3004
|
-
return VinayaConfigSchema.parse(JSON.parse(
|
|
3224
|
+
return VinayaConfigSchema.parse(JSON.parse(readFileSync7(p, "utf-8"))).managed ?? null;
|
|
3005
3225
|
} catch {
|
|
3006
3226
|
return null;
|
|
3007
3227
|
}
|
|
3008
3228
|
}
|
|
3009
3229
|
function writeManifest(repoRoot, manifest) {
|
|
3010
3230
|
const configAbs = join10(repoRoot, CONFIG_PATH);
|
|
3011
|
-
const seed = JSON.parse(
|
|
3231
|
+
const seed = JSON.parse(readFileSync7(configAbs, "utf-8"));
|
|
3012
3232
|
writeFileSync4(configAbs, `${JSON.stringify({ ...seed, managed: manifest }, null, 2)}
|
|
3013
3233
|
`, "utf-8");
|
|
3014
3234
|
}
|
|
@@ -3148,7 +3368,7 @@ import { execFileSync as execFileSync6 } from "node:child_process";
|
|
|
3148
3368
|
|
|
3149
3369
|
// src/lib/forge-write.ts
|
|
3150
3370
|
import { mkdtempSync, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "node:fs";
|
|
3151
|
-
import { readFileSync as
|
|
3371
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
3152
3372
|
import { tmpdir } from "node:os";
|
|
3153
3373
|
import { join as join11 } from "node:path";
|
|
3154
3374
|
class ForgeArgError extends Error {
|
|
@@ -3160,13 +3380,13 @@ function locateBody(args) {
|
|
|
3160
3380
|
const p = args[i + 1];
|
|
3161
3381
|
if (!p)
|
|
3162
3382
|
throw new ForgeArgError("`--body-file` was given with no path.");
|
|
3163
|
-
return { body:
|
|
3383
|
+
return { body: readFileSync8(p, "utf8"), source: { kind: "file", argIndex: i + 1, inlineForm: false } };
|
|
3164
3384
|
}
|
|
3165
3385
|
if (a.startsWith("--body-file=")) {
|
|
3166
3386
|
const p = a.slice("--body-file=".length);
|
|
3167
3387
|
if (!p)
|
|
3168
3388
|
throw new ForgeArgError("`--body-file=` was given with no path.");
|
|
3169
|
-
return { body:
|
|
3389
|
+
return { body: readFileSync8(p, "utf8"), source: { kind: "file", argIndex: i, inlineForm: true } };
|
|
3170
3390
|
}
|
|
3171
3391
|
if (a === "--body" || a === "-b") {
|
|
3172
3392
|
const v = args[i + 1];
|
|
@@ -3456,7 +3676,7 @@ function issueEditCommand(args) {
|
|
|
3456
3676
|
}
|
|
3457
3677
|
|
|
3458
3678
|
// src/commands/new-check.ts
|
|
3459
|
-
import { chmodSync as chmodSync2, existsSync as
|
|
3679
|
+
import { chmodSync as chmodSync2, existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "node:fs";
|
|
3460
3680
|
import { join as join12 } from "node:path";
|
|
3461
3681
|
var TEMPLATE_PATH = join12(packageRoot(import.meta.url), "templates", "custom-check.template.ts");
|
|
3462
3682
|
var CHECKS_DIR = join12("scripts", "vinaya-checks");
|
|
@@ -3468,14 +3688,14 @@ function newCheckCommand(args) {
|
|
|
3468
3688
|
process.exit(2);
|
|
3469
3689
|
}
|
|
3470
3690
|
const scriptsDir = join12(process.cwd(), CHECKS_DIR);
|
|
3471
|
-
if (!
|
|
3691
|
+
if (!existsSync11(scriptsDir))
|
|
3472
3692
|
mkdirSync3(scriptsDir, { recursive: true });
|
|
3473
3693
|
const targetPath = join12(scriptsDir, `${name}.ts`);
|
|
3474
|
-
if (
|
|
3694
|
+
if (existsSync11(targetPath)) {
|
|
3475
3695
|
console.error(`Error: ${targetPath} already exists.`);
|
|
3476
3696
|
process.exit(1);
|
|
3477
3697
|
}
|
|
3478
|
-
const template =
|
|
3698
|
+
const template = readFileSync9(TEMPLATE_PATH, "utf-8");
|
|
3479
3699
|
const contents = template.split("{{CHECK_NAME}}").join(name);
|
|
3480
3700
|
writeFileSync6(targetPath, contents, "utf-8");
|
|
3481
3701
|
chmodSync2(targetPath, 493);
|
|
@@ -3639,16 +3859,16 @@ function prEditCommand(args) {
|
|
|
3639
3859
|
|
|
3640
3860
|
// src/commands/studio.ts
|
|
3641
3861
|
import { spawn as spawn2 } from "node:child_process";
|
|
3642
|
-
import { existsSync as
|
|
3862
|
+
import { existsSync as existsSync12, readFileSync as readFileSync10 } from "node:fs";
|
|
3643
3863
|
import { dirname as dirname4, join as join13 } from "node:path";
|
|
3644
3864
|
function resolveStudioTarget(cwd) {
|
|
3645
3865
|
let dir = cwd;
|
|
3646
3866
|
for (;; ) {
|
|
3647
3867
|
const webDir = join13(dir, "apps", "vinaya", "web");
|
|
3648
3868
|
const pkgPath = join13(webDir, "package.json");
|
|
3649
|
-
if (
|
|
3869
|
+
if (existsSync12(pkgPath)) {
|
|
3650
3870
|
try {
|
|
3651
|
-
const pkg = JSON.parse(
|
|
3871
|
+
const pkg = JSON.parse(readFileSync10(pkgPath, "utf-8"));
|
|
3652
3872
|
if (pkg.name === "@atta/vinaya-web") {
|
|
3653
3873
|
return { kind: "workspace", webDir };
|
|
3654
3874
|
}
|
|
@@ -3681,7 +3901,7 @@ async function runStudio(cwd, args) {
|
|
|
3681
3901
|
}
|
|
3682
3902
|
|
|
3683
3903
|
// src/commands/upgrade.ts
|
|
3684
|
-
import { existsSync as
|
|
3904
|
+
import { existsSync as existsSync13, readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "node:fs";
|
|
3685
3905
|
import { join as join14 } from "node:path";
|
|
3686
3906
|
function realDeps6() {
|
|
3687
3907
|
return {
|
|
@@ -3699,11 +3919,11 @@ function flags2(args) {
|
|
|
3699
3919
|
}
|
|
3700
3920
|
function readManifest3(repoRoot) {
|
|
3701
3921
|
const p = join14(repoRoot, CONFIG_PATH);
|
|
3702
|
-
if (!
|
|
3922
|
+
if (!existsSync13(p))
|
|
3703
3923
|
return { kind: "missing" };
|
|
3704
3924
|
let raw;
|
|
3705
3925
|
try {
|
|
3706
|
-
raw = JSON.parse(
|
|
3926
|
+
raw = JSON.parse(readFileSync11(p, "utf-8"));
|
|
3707
3927
|
} catch (err) {
|
|
3708
3928
|
return { kind: "invalid", error: `invalid JSON: ${err.message}` };
|
|
3709
3929
|
}
|
|
@@ -3720,7 +3940,7 @@ function readManifest3(repoRoot) {
|
|
|
3720
3940
|
}
|
|
3721
3941
|
function writeManifestVersion(repoRoot, manifest) {
|
|
3722
3942
|
const configAbs = join14(repoRoot, CONFIG_PATH);
|
|
3723
|
-
const seed = JSON.parse(
|
|
3943
|
+
const seed = JSON.parse(readFileSync11(configAbs, "utf-8"));
|
|
3724
3944
|
const updated = { ...manifest, version: MANAGED_MANIFEST_VERSION };
|
|
3725
3945
|
writeFileSync7(configAbs, `${JSON.stringify({ ...seed, managed: updated }, null, 2)}
|
|
3726
3946
|
`, "utf-8");
|
|
@@ -3734,7 +3954,7 @@ function planUpgrade(ops, repoRoot, manifest) {
|
|
|
3734
3954
|
for (const op of ops) {
|
|
3735
3955
|
if (op.kind === "create-file") {
|
|
3736
3956
|
const abs2 = join14(repoRoot, op.path);
|
|
3737
|
-
const exists =
|
|
3957
|
+
const exists = existsSync13(abs2);
|
|
3738
3958
|
const owned = ownedFiles.has(op.path);
|
|
3739
3959
|
let action;
|
|
3740
3960
|
if (op.path === CONFIG_PATH) {
|
|
@@ -3744,7 +3964,7 @@ function planUpgrade(ops, repoRoot, manifest) {
|
|
|
3744
3964
|
} else if (!exists) {
|
|
3745
3965
|
action = "recreate";
|
|
3746
3966
|
hasChanges = true;
|
|
3747
|
-
} else if (
|
|
3967
|
+
} else if (readFileSync11(abs2, "utf-8") !== op.content) {
|
|
3748
3968
|
action = "regenerate";
|
|
3749
3969
|
hasChanges = true;
|
|
3750
3970
|
} else {
|
|
@@ -3757,11 +3977,11 @@ function planUpgrade(ops, repoRoot, manifest) {
|
|
|
3757
3977
|
let action;
|
|
3758
3978
|
if (!owned) {
|
|
3759
3979
|
action = "not-installed";
|
|
3760
|
-
} else if (!
|
|
3980
|
+
} else if (!existsSync13(abs2)) {
|
|
3761
3981
|
action = "recreate-host";
|
|
3762
3982
|
hasChanges = true;
|
|
3763
3983
|
} else {
|
|
3764
|
-
const content =
|
|
3984
|
+
const content = readFileSync11(abs2, "utf-8");
|
|
3765
3985
|
const { begin, end } = markerLines(op.marker, op.comment);
|
|
3766
3986
|
if (!(content.includes(begin) && content.includes(end))) {
|
|
3767
3987
|
action = "recreate-append";
|
|
@@ -3838,7 +4058,7 @@ function renderUpgradeDiff(plan) {
|
|
|
3838
4058
|
}
|
|
3839
4059
|
function regenerateBlock(repoRoot, op) {
|
|
3840
4060
|
const abs2 = join14(repoRoot, op.path);
|
|
3841
|
-
const content =
|
|
4061
|
+
const content = readFileSync11(abs2, "utf-8");
|
|
3842
4062
|
const stripped = stripBlockFromContent(content, op.marker, op.comment);
|
|
3843
4063
|
if (stripped !== null) {
|
|
3844
4064
|
writeFileSync7(abs2, stripped.endsWith(`
|
|
@@ -4079,6 +4299,9 @@ var COMMANDS = [
|
|
|
4079
4299
|
{ flag: "--diff-only", description: "Scope diff-declared checks to changed files" },
|
|
4080
4300
|
{ flag: "--parallel[=n]", description: "Concurrency cap (default: cpu-derived)" }
|
|
4081
4301
|
],
|
|
4302
|
+
details: [
|
|
4303
|
+
"Warns (stderr, every output mode, never affecting the exit code) for any check whose executable reads `process.env`/`Bun.env`/`Deno.env` directly with no `env` declared on its `CheckSpec` — those checks will lose environment access once the env allowlist is wired as the spawn default in a later minor. Declare `env` (a core check's own registration, or `vinaya.config.json`'s `checks.<name>.env` for a custom one) to silence the warning ahead of that flip."
|
|
4304
|
+
],
|
|
4082
4305
|
status: "shipped"
|
|
4083
4306
|
},
|
|
4084
4307
|
{
|
|
@@ -4148,6 +4371,9 @@ var COMMANDS = [
|
|
|
4148
4371
|
name: "doctor",
|
|
4149
4372
|
description: "Diagnose hook, workflow, and config health — report only, never mutates",
|
|
4150
4373
|
flags: [{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }],
|
|
4374
|
+
details: [
|
|
4375
|
+
'Carries the same env-declaration diagnostic `vinaya check` warns with — permanently, at `info` severity, not just ahead of the spawn-default flip — plus a `warn`-severity lint over suspicious `env` literal forms (a stray `"true"`/`"false"` string, or a high-entropy literal that reads like a leaked secret committed to config).'
|
|
4376
|
+
],
|
|
4151
4377
|
status: "shipped"
|
|
4152
4378
|
},
|
|
4153
4379
|
{
|
|
@@ -4252,7 +4478,7 @@ function printHelp() {
|
|
|
4252
4478
|
// src/index.ts
|
|
4253
4479
|
var PACKAGE_ROOT = join15(dirname5(fileURLToPath2(import.meta.url)), "..");
|
|
4254
4480
|
function readVersion2() {
|
|
4255
|
-
const pkg = JSON.parse(
|
|
4481
|
+
const pkg = JSON.parse(readFileSync12(join15(PACKAGE_ROOT, "package.json"), "utf-8"));
|
|
4256
4482
|
return pkg.version;
|
|
4257
4483
|
}
|
|
4258
4484
|
var [, , command, ...args] = process.argv;
|
package/package.json
CHANGED
|
@@ -7,6 +7,16 @@
|
|
|
7
7
|
* never sleep past it inside this file. This file is standalone (no import
|
|
8
8
|
* from the vinaya CLI's own source tree) because it lives in YOUR repo, not
|
|
9
9
|
* inside `@attalabs/vinaya`.
|
|
10
|
+
*
|
|
11
|
+
* If this check reads `process.env` directly, declare which variables it
|
|
12
|
+
* needs on its REGISTRATION in `vinaya.config.json` (not in this file —
|
|
13
|
+
* there is no CheckSpec type to attach it to here) via `checks.{{CHECK_NAME}}.env`.
|
|
14
|
+
* Four forms: `"MY_VAR": true` forwards your value verbatim; `{ "optional":
|
|
15
|
+
* true }` forwards it if set, tolerating absence; `{ "anyOf": ["A", "B"] }`
|
|
16
|
+
* forwards whichever of those you've set, each under its own name; a
|
|
17
|
+
* literal string sets the key to that exact value. Example:
|
|
18
|
+
* { "checks": { "{{CHECK_NAME}}": { "run": "...", "scope": "diff",
|
|
19
|
+
* "env": { "MY_TOKEN": { "optional": true } } } } }
|
|
10
20
|
*/
|
|
11
21
|
|
|
12
22
|
type CheckError = {
|