@attalabs/vinaya 0.1.3 → 0.3.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 +2 -1
- package/aeg-root/roles/archivist.md +1 -1
- package/aeg-root/roles/developer.md +2 -2
- package/aeg-root/state-machine.md +17 -3
- package/aeg-root/tranche-model.md +5 -3
- package/dist/checks/bin/check-branch-topology.js +69 -11
- package/dist/checks/bin/check-brief-shape.js +69 -11
- package/dist/checks/bin/check-closes-n.js +69 -11
- package/dist/checks/bin/check-coherence.js +69 -11
- package/dist/checks/bin/check-dead-branch-push.js +69 -11
- package/dist/checks/bin/check-dispatch-readiness.js +69 -11
- package/dist/checks/bin/check-doc-coverage-push.js +69 -11
- package/dist/checks/bin/check-doc-coverage.js +69 -11
- package/dist/checks/bin/check-first-push-dispatch.js +69 -11
- package/dist/checks/bin/check-issue-assignment.js +69 -11
- package/dist/checks/bin/check-no-disk-state.js +69 -11
- package/dist/checks/bin/check-reader-resolvable-prose.js +69 -11
- package/dist/checks/bin/check-registry-gates.js +69 -11
- package/dist/checks/bin/check-review-gate.js +69 -11
- package/dist/checks/bin/check-single-plan-pr.js +69 -11
- package/dist/checks/bin/check-test-plan.js +69 -11
- package/dist/index.js +362 -70
- package/package.json +1 -1
- package/templates/custom-check.template.ts +10 -0
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,138 @@ 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;
|
|
1393
|
-
|
|
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
|
+
}
|
|
1473
|
+
var ENV_BASELINE_KEYS = ["PATH", "LANG", "HOME", "HTTPS_PROXY", "HTTP_PROXY", "NO_PROXY", "TMPDIR"];
|
|
1474
|
+
function buildCheckEnv(env, callerEnv = process.env) {
|
|
1475
|
+
const out = {};
|
|
1476
|
+
for (const key of ENV_BASELINE_KEYS) {
|
|
1477
|
+
const value = callerEnv[key];
|
|
1478
|
+
if (value !== undefined)
|
|
1479
|
+
out[key] = value;
|
|
1480
|
+
}
|
|
1481
|
+
if (!env)
|
|
1482
|
+
return out;
|
|
1483
|
+
for (const [key, decl] of Object.entries(env)) {
|
|
1484
|
+
if (decl === true || typeof decl === "object" && "optional" in decl) {
|
|
1485
|
+
const value = callerEnv[key];
|
|
1486
|
+
if (value !== undefined)
|
|
1487
|
+
out[key] = value;
|
|
1488
|
+
} else if (typeof decl === "object" && "anyOf" in decl) {
|
|
1489
|
+
for (const member of decl.anyOf) {
|
|
1490
|
+
const value = callerEnv[member];
|
|
1491
|
+
if (value !== undefined)
|
|
1492
|
+
out[member] = value;
|
|
1493
|
+
}
|
|
1494
|
+
} else if (typeof decl === "string") {
|
|
1495
|
+
out[key] = decl;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
return out;
|
|
1499
|
+
}
|
|
1500
|
+
function missingEnvErrors(spec, callerEnv) {
|
|
1501
|
+
if (!spec.env)
|
|
1502
|
+
return [];
|
|
1503
|
+
const errors = [];
|
|
1504
|
+
for (const [key, decl] of Object.entries(spec.env)) {
|
|
1505
|
+
if (decl === true) {
|
|
1506
|
+
if (callerEnv[key] === undefined) {
|
|
1507
|
+
errors.push({
|
|
1508
|
+
schema: CHECK_SCHEMA_VERSION,
|
|
1509
|
+
check: spec.name,
|
|
1510
|
+
severity: "error",
|
|
1511
|
+
message: `Could not run check "${spec.name}": required environment variable \`${key}\` is not set.`,
|
|
1512
|
+
agent_recovery_prompt: `Set \`${key}\` in the environment before re-running \`vinaya check ${spec.name}\`, or relax its declaration to \`{ optional: true }\` in its \`env\` registration if the check can tolerate absence.`
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
} else if (typeof decl === "object" && decl !== null && "anyOf" in decl) {
|
|
1516
|
+
const satisfied = decl.anyOf.some((member) => callerEnv[member] !== undefined);
|
|
1517
|
+
if (!satisfied) {
|
|
1518
|
+
errors.push({
|
|
1519
|
+
schema: CHECK_SCHEMA_VERSION,
|
|
1520
|
+
check: spec.name,
|
|
1521
|
+
severity: "error",
|
|
1522
|
+
message: `Could not run check "${spec.name}": none of its required environment variables (${decl.anyOf.join(", ")}) are set.`,
|
|
1523
|
+
agent_recovery_prompt: `Set one of ${decl.anyOf.join(", ")} in the environment before re-running \`vinaya check ${spec.name}\`.`
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
return errors;
|
|
1529
|
+
}
|
|
1530
|
+
async function runOne(spec, timeoutMs, callerEnv) {
|
|
1394
1531
|
const start = performance.now();
|
|
1532
|
+
const envErrors = missingEnvErrors(spec, callerEnv);
|
|
1533
|
+
if (envErrors.length > 0) {
|
|
1534
|
+
return {
|
|
1535
|
+
name: spec.name,
|
|
1536
|
+
status: "error",
|
|
1537
|
+
exitCode: null,
|
|
1538
|
+
errors: envErrors,
|
|
1539
|
+
durationMs: performance.now() - start
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1395
1542
|
const proc = spawn(spec.run, spec.args ?? [], {
|
|
1396
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
1543
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1544
|
+
detached: true,
|
|
1545
|
+
env: buildCheckEnv(spec.env, callerEnv)
|
|
1397
1546
|
});
|
|
1547
|
+
function killTree(signal) {
|
|
1548
|
+
const pid = proc.pid;
|
|
1549
|
+
if (pid === undefined)
|
|
1550
|
+
return;
|
|
1551
|
+
try {
|
|
1552
|
+
process.kill(-pid, signal);
|
|
1553
|
+
} catch {
|
|
1554
|
+
try {
|
|
1555
|
+
proc.kill(signal);
|
|
1556
|
+
} catch {}
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
installSignalForwarding();
|
|
1560
|
+
activeKillers.add(killTree);
|
|
1561
|
+
function groupStillAlive(pid) {
|
|
1562
|
+
try {
|
|
1563
|
+
process.kill(-pid, 0);
|
|
1564
|
+
return true;
|
|
1565
|
+
} catch {
|
|
1566
|
+
return false;
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1398
1569
|
let timedOut = false;
|
|
1399
|
-
let
|
|
1570
|
+
let escalationDone = Promise.resolve();
|
|
1571
|
+
let cancelEscalation;
|
|
1400
1572
|
const timer = setTimeout(() => {
|
|
1401
1573
|
timedOut = true;
|
|
1402
|
-
|
|
1403
|
-
|
|
1574
|
+
killTree("SIGTERM");
|
|
1575
|
+
escalationDone = new Promise((resolve) => {
|
|
1576
|
+
const graceTimer = setTimeout(() => {
|
|
1577
|
+
const pid = proc.pid;
|
|
1578
|
+
if (pid !== undefined && groupStillAlive(pid))
|
|
1579
|
+
killTree("SIGKILL");
|
|
1580
|
+
resolve();
|
|
1581
|
+
}, KILL_GRACE_MS);
|
|
1582
|
+
cancelEscalation = () => {
|
|
1583
|
+
clearTimeout(graceTimer);
|
|
1584
|
+
resolve();
|
|
1585
|
+
};
|
|
1586
|
+
});
|
|
1404
1587
|
}, timeoutMs);
|
|
1405
1588
|
let stderrText = "";
|
|
1406
1589
|
proc.stderr.setEncoding("utf-8");
|
|
@@ -1417,7 +1600,13 @@ async function runOne(spec, timeoutMs) {
|
|
|
1417
1600
|
});
|
|
1418
1601
|
});
|
|
1419
1602
|
clearTimeout(timer);
|
|
1420
|
-
|
|
1603
|
+
if (timedOut) {
|
|
1604
|
+
const pid = proc.pid;
|
|
1605
|
+
if (pid === undefined || !groupStillAlive(pid))
|
|
1606
|
+
cancelEscalation?.();
|
|
1607
|
+
await escalationDone;
|
|
1608
|
+
}
|
|
1609
|
+
activeKillers.delete(killTree);
|
|
1421
1610
|
const durationMs = performance.now() - start;
|
|
1422
1611
|
if (timedOut) {
|
|
1423
1612
|
return { name: spec.name, status: "timeout", exitCode: null, errors: [], durationMs };
|
|
@@ -1469,6 +1658,7 @@ async function runOne(spec, timeoutMs) {
|
|
|
1469
1658
|
return { name: spec.name, status, exitCode, errors, durationMs };
|
|
1470
1659
|
}
|
|
1471
1660
|
async function runChecks(specs, opts) {
|
|
1661
|
+
const callerEnv = opts.callerEnv ?? process.env;
|
|
1472
1662
|
const results = new Array(specs.length);
|
|
1473
1663
|
const toRun = [];
|
|
1474
1664
|
for (let i = 0;i < specs.length; i++) {
|
|
@@ -1485,7 +1675,7 @@ async function runChecks(specs, opts) {
|
|
|
1485
1675
|
const idx = toRun[cursor];
|
|
1486
1676
|
cursor += 1;
|
|
1487
1677
|
const spec = specs[idx];
|
|
1488
|
-
results[idx] = await runOne(spec, spec.timeoutMs ?? opts.defaultTimeoutMs);
|
|
1678
|
+
results[idx] = await runOne(spec, spec.timeoutMs ?? opts.defaultTimeoutMs, callerEnv);
|
|
1489
1679
|
}
|
|
1490
1680
|
};
|
|
1491
1681
|
const workerCount = Math.max(1, Math.min(opts.parallel, toRun.length));
|
|
@@ -1498,13 +1688,70 @@ import { existsSync as existsSync4, mkdirSync, readFileSync, writeFileSync } fro
|
|
|
1498
1688
|
import { homedir } from "node:os";
|
|
1499
1689
|
import { dirname as dirname2, join as join4 } from "node:path";
|
|
1500
1690
|
import { z } from "zod";
|
|
1691
|
+
var EnvEntrySchema = z.union([
|
|
1692
|
+
z.literal(true),
|
|
1693
|
+
z.object({ optional: z.literal(true) }),
|
|
1694
|
+
z.object({ anyOf: z.array(z.string()).min(2) }),
|
|
1695
|
+
z.string()
|
|
1696
|
+
]);
|
|
1501
1697
|
var CheckEntrySchema = z.object({
|
|
1502
1698
|
run: z.string(),
|
|
1503
1699
|
scope: z.enum(["diff", "full"]),
|
|
1504
1700
|
include: z.array(z.string()).optional(),
|
|
1505
1701
|
args: z.array(z.string()).optional(),
|
|
1506
|
-
timeoutMs: z.number().optional()
|
|
1702
|
+
timeoutMs: z.number().optional(),
|
|
1703
|
+
env: z.record(z.string(), EnvEntrySchema).optional()
|
|
1704
|
+
}).superRefine((entry2, ctx) => {
|
|
1705
|
+
if (!entry2.env)
|
|
1706
|
+
return;
|
|
1707
|
+
for (const [key, value] of Object.entries(entry2.env)) {
|
|
1708
|
+
if (typeof value !== "object" || value === null || !("anyOf" in value))
|
|
1709
|
+
continue;
|
|
1710
|
+
const members = value.anyOf;
|
|
1711
|
+
if (new Set(members).size !== members.length) {
|
|
1712
|
+
ctx.addIssue({
|
|
1713
|
+
code: z.ZodIssueCode.custom,
|
|
1714
|
+
message: `env.${key}.anyOf has duplicate members`,
|
|
1715
|
+
path: ["env", key, "anyOf"]
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
if (!members.includes(key)) {
|
|
1719
|
+
ctx.addIssue({
|
|
1720
|
+
code: z.ZodIssueCode.custom,
|
|
1721
|
+
message: `env.${key}.anyOf must include "${key}" itself as a member`,
|
|
1722
|
+
path: ["env", key, "anyOf"]
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1507
1726
|
});
|
|
1727
|
+
var HIGH_ENTROPY_MIN_LENGTH = 20;
|
|
1728
|
+
function looksHighEntropy(value) {
|
|
1729
|
+
if (value.length < HIGH_ENTROPY_MIN_LENGTH)
|
|
1730
|
+
return false;
|
|
1731
|
+
if (/\s/.test(value))
|
|
1732
|
+
return false;
|
|
1733
|
+
const classes = [/[a-z]/.test(value), /[A-Z]/.test(value), /[0-9]/.test(value)].filter(Boolean).length;
|
|
1734
|
+
return classes >= 2;
|
|
1735
|
+
}
|
|
1736
|
+
function lintEnvDeclarations(checks) {
|
|
1737
|
+
const warnings = [];
|
|
1738
|
+
if (!checks)
|
|
1739
|
+
return warnings;
|
|
1740
|
+
for (const [checkName, entry2] of Object.entries(checks)) {
|
|
1741
|
+
if (!entry2.env)
|
|
1742
|
+
continue;
|
|
1743
|
+
for (const [key, value] of Object.entries(entry2.env)) {
|
|
1744
|
+
if (typeof value !== "string")
|
|
1745
|
+
continue;
|
|
1746
|
+
if (value === "true" || value === "false") {
|
|
1747
|
+
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?`);
|
|
1748
|
+
} else if (looksHighEntropy(value)) {
|
|
1749
|
+
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.`);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
return warnings;
|
|
1754
|
+
}
|
|
1508
1755
|
var BRIEF_BUILTINS = [
|
|
1509
1756
|
"tier",
|
|
1510
1757
|
"testPlan",
|
|
@@ -1902,7 +2149,7 @@ async function demoBreakCommand(args) {
|
|
|
1902
2149
|
}
|
|
1903
2150
|
|
|
1904
2151
|
// src/commands/doctor.ts
|
|
1905
|
-
import { existsSync as
|
|
2152
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5, statSync } from "node:fs";
|
|
1906
2153
|
import { join as join8 } from "node:path";
|
|
1907
2154
|
|
|
1908
2155
|
// src/lib/artifacts.ts
|
|
@@ -2271,8 +2518,33 @@ function buildInitProductOps(name) {
|
|
|
2271
2518
|
];
|
|
2272
2519
|
}
|
|
2273
2520
|
|
|
2521
|
+
// src/lib/env-lint.ts
|
|
2522
|
+
import { existsSync as existsSync6, readFileSync as readFileSync3 } from "node:fs";
|
|
2523
|
+
var ENV_READ_PATTERN = /\b(?:process\.env|Bun\.env|Deno\.env)\b/;
|
|
2524
|
+
function checksMissingEnvDeclaration(specs) {
|
|
2525
|
+
const names = [];
|
|
2526
|
+
for (const spec of specs) {
|
|
2527
|
+
if (spec.env)
|
|
2528
|
+
continue;
|
|
2529
|
+
if (!existsSync6(spec.run))
|
|
2530
|
+
continue;
|
|
2531
|
+
let source;
|
|
2532
|
+
try {
|
|
2533
|
+
source = readFileSync3(spec.run, "utf8");
|
|
2534
|
+
} catch {
|
|
2535
|
+
continue;
|
|
2536
|
+
}
|
|
2537
|
+
if (ENV_READ_PATTERN.test(source))
|
|
2538
|
+
names.push(spec.name);
|
|
2539
|
+
}
|
|
2540
|
+
return names;
|
|
2541
|
+
}
|
|
2542
|
+
function envDeclarationWarning(name) {
|
|
2543
|
+
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).`;
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2274
2546
|
// src/lib/ops.ts
|
|
2275
|
-
import { chmodSync, existsSync as
|
|
2547
|
+
import { chmodSync, existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync4, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
2276
2548
|
import { dirname as dirname3, join as join7, resolve, sep } from "node:path";
|
|
2277
2549
|
var MARKER_NS = "vinaya:managed";
|
|
2278
2550
|
function markerLines(marker, comment) {
|
|
@@ -2300,9 +2572,9 @@ function containedAbs(repoRoot, relPath) {
|
|
|
2300
2572
|
}
|
|
2301
2573
|
function fileContains(repoRoot, relPath, needle) {
|
|
2302
2574
|
const p = abs(repoRoot, relPath);
|
|
2303
|
-
if (!
|
|
2575
|
+
if (!existsSync7(p))
|
|
2304
2576
|
return false;
|
|
2305
|
-
return
|
|
2577
|
+
return readFileSync4(p, "utf-8").includes(needle);
|
|
2306
2578
|
}
|
|
2307
2579
|
function planInstall(ops, repoRoot, ownedFiles = new Set) {
|
|
2308
2580
|
const entries = [];
|
|
@@ -2310,7 +2582,7 @@ function planInstall(ops, repoRoot, ownedFiles = new Set) {
|
|
|
2310
2582
|
for (const op of ops) {
|
|
2311
2583
|
switch (op.kind) {
|
|
2312
2584
|
case "create-file": {
|
|
2313
|
-
const exists =
|
|
2585
|
+
const exists = existsSync7(abs(repoRoot, op.path));
|
|
2314
2586
|
let action;
|
|
2315
2587
|
if (!exists)
|
|
2316
2588
|
action = "create";
|
|
@@ -2328,7 +2600,7 @@ function planInstall(ops, repoRoot, ownedFiles = new Set) {
|
|
|
2328
2600
|
let action;
|
|
2329
2601
|
if (fileContains(repoRoot, op.path, begin))
|
|
2330
2602
|
action = "skip-present";
|
|
2331
|
-
else if (
|
|
2603
|
+
else if (existsSync7(abs(repoRoot, op.path)))
|
|
2332
2604
|
action = "append";
|
|
2333
2605
|
else
|
|
2334
2606
|
action = "create-host";
|
|
@@ -2412,7 +2684,7 @@ function writeFileWithDirs(target, content, mode) {
|
|
|
2412
2684
|
}
|
|
2413
2685
|
function appendBlock(repoRoot, op) {
|
|
2414
2686
|
const target = abs(repoRoot, op.path);
|
|
2415
|
-
const existing =
|
|
2687
|
+
const existing = readFileSync4(target, "utf-8");
|
|
2416
2688
|
const sep2 = existing.endsWith(`
|
|
2417
2689
|
`) ? `
|
|
2418
2690
|
` : `
|
|
@@ -2523,7 +2795,7 @@ function planEject(manifest, repoRoot) {
|
|
|
2523
2795
|
escapes.push(b.path);
|
|
2524
2796
|
continue;
|
|
2525
2797
|
}
|
|
2526
|
-
if (!
|
|
2798
|
+
if (!existsSync7(p)) {
|
|
2527
2799
|
actions.push({
|
|
2528
2800
|
kind: "strip-block",
|
|
2529
2801
|
path: b.path,
|
|
@@ -2534,7 +2806,7 @@ function planEject(manifest, repoRoot) {
|
|
|
2534
2806
|
});
|
|
2535
2807
|
continue;
|
|
2536
2808
|
}
|
|
2537
|
-
const content =
|
|
2809
|
+
const content = readFileSync4(p, "utf-8");
|
|
2538
2810
|
const stripped = stripBlockFromContent(content, b.marker, b.comment);
|
|
2539
2811
|
const removesHost = stripped !== null && blockStripLeavesEmpty(stripped);
|
|
2540
2812
|
actions.push({
|
|
@@ -2552,7 +2824,7 @@ function planEject(manifest, repoRoot) {
|
|
|
2552
2824
|
escapes.push(f);
|
|
2553
2825
|
continue;
|
|
2554
2826
|
}
|
|
2555
|
-
actions.push({ kind: "delete-file", path: f, present:
|
|
2827
|
+
actions.push({ kind: "delete-file", path: f, present: existsSync7(p) });
|
|
2556
2828
|
}
|
|
2557
2829
|
for (const name of manifest.labels) {
|
|
2558
2830
|
actions.push({ kind: "report-label", name });
|
|
@@ -2586,7 +2858,7 @@ function applyEject(plan, repoRoot) {
|
|
|
2586
2858
|
const p = containedAbs(repoRoot, a.path);
|
|
2587
2859
|
if (p === null)
|
|
2588
2860
|
continue;
|
|
2589
|
-
const content =
|
|
2861
|
+
const content = readFileSync4(p, "utf-8");
|
|
2590
2862
|
const stripped = stripBlockFromContent(content, a.marker, a.comment);
|
|
2591
2863
|
if (stripped === null)
|
|
2592
2864
|
continue;
|
|
@@ -2609,7 +2881,7 @@ function applyEject(plan, repoRoot) {
|
|
|
2609
2881
|
|
|
2610
2882
|
// src/commands/doctor.ts
|
|
2611
2883
|
function readVersion() {
|
|
2612
|
-
const pkg = JSON.parse(
|
|
2884
|
+
const pkg = JSON.parse(readFileSync5(join8(packageRoot(import.meta.url), "package.json"), "utf-8"));
|
|
2613
2885
|
return pkg.version;
|
|
2614
2886
|
}
|
|
2615
2887
|
function realDeps3() {
|
|
@@ -2629,11 +2901,11 @@ var warn = (check, message) => ({ check, severity: "warn", message });
|
|
|
2629
2901
|
var error = (check, message) => ({ check, severity: "error", message });
|
|
2630
2902
|
function readConfig(repoRoot) {
|
|
2631
2903
|
const p = join8(repoRoot, CONFIG_PATH);
|
|
2632
|
-
if (!
|
|
2904
|
+
if (!existsSync8(p))
|
|
2633
2905
|
return { kind: "missing" };
|
|
2634
2906
|
let raw;
|
|
2635
2907
|
try {
|
|
2636
|
-
raw = JSON.parse(
|
|
2908
|
+
raw = JSON.parse(readFileSync5(p, "utf-8"));
|
|
2637
2909
|
} catch (err) {
|
|
2638
2910
|
return { kind: "invalid", error: `invalid JSON: ${err.message}` };
|
|
2639
2911
|
}
|
|
@@ -2663,13 +2935,13 @@ function diagnoseInstall(repoRoot, ctx, manifest) {
|
|
|
2663
2935
|
if (op.kind === "create-file") {
|
|
2664
2936
|
const check = labelForPath(op.path);
|
|
2665
2937
|
const abs2 = join8(repoRoot, op.path);
|
|
2666
|
-
const exists =
|
|
2938
|
+
const exists = existsSync8(abs2);
|
|
2667
2939
|
const owned = ownedFiles.has(op.path);
|
|
2668
2940
|
if (!exists) {
|
|
2669
2941
|
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
2942
|
continue;
|
|
2671
2943
|
}
|
|
2672
|
-
const content =
|
|
2944
|
+
const content = readFileSync5(abs2, "utf-8");
|
|
2673
2945
|
if (!owned) {
|
|
2674
2946
|
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
2947
|
continue;
|
|
@@ -2688,11 +2960,11 @@ function diagnoseInstall(repoRoot, ctx, manifest) {
|
|
|
2688
2960
|
const check = "hooks";
|
|
2689
2961
|
const abs2 = join8(repoRoot, op.path);
|
|
2690
2962
|
const owned = ownedBlocks.has(blockKey(op.path, op.marker));
|
|
2691
|
-
if (!
|
|
2963
|
+
if (!existsSync8(abs2)) {
|
|
2692
2964
|
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
2965
|
continue;
|
|
2694
2966
|
}
|
|
2695
|
-
const content =
|
|
2967
|
+
const content = readFileSync5(abs2, "utf-8");
|
|
2696
2968
|
const { begin, end } = markerLines(op.marker, op.comment);
|
|
2697
2969
|
const hasMarkers = content.includes(begin) && content.includes(end);
|
|
2698
2970
|
if (!hasMarkers) {
|
|
@@ -2718,7 +2990,20 @@ function diagnoseCustomChecks(repoRoot, config) {
|
|
|
2718
2990
|
const findings = [];
|
|
2719
2991
|
for (const [name, entry2] of Object.entries(config.checks ?? {})) {
|
|
2720
2992
|
const scriptAbs = join8(repoRoot, entry2.run);
|
|
2721
|
-
findings.push(
|
|
2993
|
+
findings.push(existsSync8(scriptAbs) ? ok("checks", `custom check '${name}' → ${entry2.run}`) : error("checks", `custom check '${name}' points at a missing script: ${entry2.run}`));
|
|
2994
|
+
}
|
|
2995
|
+
return findings;
|
|
2996
|
+
}
|
|
2997
|
+
function diagnoseEnvDeclarations(repoRoot, config) {
|
|
2998
|
+
const customSpecs = Object.entries(config?.checks ?? {}).map(([name, entry2]) => ({
|
|
2999
|
+
name,
|
|
3000
|
+
...entry2,
|
|
3001
|
+
run: join8(repoRoot, entry2.run)
|
|
3002
|
+
}));
|
|
3003
|
+
const missing = checksMissingEnvDeclaration([...coreCheckRegistry(), ...customSpecs]);
|
|
3004
|
+
const findings = missing.map((name) => info("env", envDeclarationWarning(name)));
|
|
3005
|
+
for (const message of lintEnvDeclarations(config?.checks)) {
|
|
3006
|
+
findings.push(warn("env", message));
|
|
2722
3007
|
}
|
|
2723
3008
|
return findings;
|
|
2724
3009
|
}
|
|
@@ -2791,6 +3076,7 @@ async function runDoctor(args, deps) {
|
|
|
2791
3076
|
hasDrift = install.hasDrift;
|
|
2792
3077
|
findings.push(...diagnoseCustomChecks(repo.repoRoot, configRead.config));
|
|
2793
3078
|
}
|
|
3079
|
+
findings.push(...diagnoseEnvDeclarations(repo.repoRoot, configRead.kind === "ok" ? configRead.config : null));
|
|
2794
3080
|
findings.push(...await diagnoseEnvironment(deps, hasDrift));
|
|
2795
3081
|
findings.push(await diagnoseBranchProtection(deps, repo.owner, repo.repo));
|
|
2796
3082
|
const healthy = findings.every((f) => f.severity === "ok" || f.severity === "info");
|
|
@@ -2806,7 +3092,7 @@ async function doctorCommand(args) {
|
|
|
2806
3092
|
}
|
|
2807
3093
|
|
|
2808
3094
|
// src/commands/eject.ts
|
|
2809
|
-
import { existsSync as
|
|
3095
|
+
import { existsSync as existsSync9, readFileSync as readFileSync6 } from "node:fs";
|
|
2810
3096
|
import { join as join9 } from "node:path";
|
|
2811
3097
|
|
|
2812
3098
|
// src/lib/prompt.ts
|
|
@@ -2889,11 +3175,11 @@ function parse(args) {
|
|
|
2889
3175
|
}
|
|
2890
3176
|
function readManifest(repoRoot) {
|
|
2891
3177
|
const p = join9(repoRoot, CONFIG_PATH);
|
|
2892
|
-
if (!
|
|
3178
|
+
if (!existsSync9(p))
|
|
2893
3179
|
return { kind: "none" };
|
|
2894
3180
|
let raw;
|
|
2895
3181
|
try {
|
|
2896
|
-
raw = JSON.parse(
|
|
3182
|
+
raw = JSON.parse(readFileSync6(p, "utf-8"));
|
|
2897
3183
|
} catch (err) {
|
|
2898
3184
|
return { kind: "orphan", reason: `vinaya.config.json is not valid JSON: ${err.message}` };
|
|
2899
3185
|
}
|
|
@@ -2976,7 +3262,7 @@ async function ejectCommand(args) {
|
|
|
2976
3262
|
}
|
|
2977
3263
|
|
|
2978
3264
|
// src/commands/init.ts
|
|
2979
|
-
import { existsSync as
|
|
3265
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
|
|
2980
3266
|
import { join as join10 } from "node:path";
|
|
2981
3267
|
function realDeps5() {
|
|
2982
3268
|
return {
|
|
@@ -2998,17 +3284,17 @@ function flags(args) {
|
|
|
2998
3284
|
var PRODUCT_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
2999
3285
|
function readManifest2(repoRoot) {
|
|
3000
3286
|
const p = join10(repoRoot, CONFIG_PATH);
|
|
3001
|
-
if (!
|
|
3287
|
+
if (!existsSync10(p))
|
|
3002
3288
|
return null;
|
|
3003
3289
|
try {
|
|
3004
|
-
return VinayaConfigSchema.parse(JSON.parse(
|
|
3290
|
+
return VinayaConfigSchema.parse(JSON.parse(readFileSync7(p, "utf-8"))).managed ?? null;
|
|
3005
3291
|
} catch {
|
|
3006
3292
|
return null;
|
|
3007
3293
|
}
|
|
3008
3294
|
}
|
|
3009
3295
|
function writeManifest(repoRoot, manifest) {
|
|
3010
3296
|
const configAbs = join10(repoRoot, CONFIG_PATH);
|
|
3011
|
-
const seed = JSON.parse(
|
|
3297
|
+
const seed = JSON.parse(readFileSync7(configAbs, "utf-8"));
|
|
3012
3298
|
writeFileSync4(configAbs, `${JSON.stringify({ ...seed, managed: manifest }, null, 2)}
|
|
3013
3299
|
`, "utf-8");
|
|
3014
3300
|
}
|
|
@@ -3148,7 +3434,7 @@ import { execFileSync as execFileSync6 } from "node:child_process";
|
|
|
3148
3434
|
|
|
3149
3435
|
// src/lib/forge-write.ts
|
|
3150
3436
|
import { mkdtempSync, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "node:fs";
|
|
3151
|
-
import { readFileSync as
|
|
3437
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
3152
3438
|
import { tmpdir } from "node:os";
|
|
3153
3439
|
import { join as join11 } from "node:path";
|
|
3154
3440
|
class ForgeArgError extends Error {
|
|
@@ -3160,13 +3446,13 @@ function locateBody(args) {
|
|
|
3160
3446
|
const p = args[i + 1];
|
|
3161
3447
|
if (!p)
|
|
3162
3448
|
throw new ForgeArgError("`--body-file` was given with no path.");
|
|
3163
|
-
return { body:
|
|
3449
|
+
return { body: readFileSync8(p, "utf8"), source: { kind: "file", argIndex: i + 1, inlineForm: false } };
|
|
3164
3450
|
}
|
|
3165
3451
|
if (a.startsWith("--body-file=")) {
|
|
3166
3452
|
const p = a.slice("--body-file=".length);
|
|
3167
3453
|
if (!p)
|
|
3168
3454
|
throw new ForgeArgError("`--body-file=` was given with no path.");
|
|
3169
|
-
return { body:
|
|
3455
|
+
return { body: readFileSync8(p, "utf8"), source: { kind: "file", argIndex: i, inlineForm: true } };
|
|
3170
3456
|
}
|
|
3171
3457
|
if (a === "--body" || a === "-b") {
|
|
3172
3458
|
const v = args[i + 1];
|
|
@@ -3456,7 +3742,7 @@ function issueEditCommand(args) {
|
|
|
3456
3742
|
}
|
|
3457
3743
|
|
|
3458
3744
|
// src/commands/new-check.ts
|
|
3459
|
-
import { chmodSync as chmodSync2, existsSync as
|
|
3745
|
+
import { chmodSync as chmodSync2, existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "node:fs";
|
|
3460
3746
|
import { join as join12 } from "node:path";
|
|
3461
3747
|
var TEMPLATE_PATH = join12(packageRoot(import.meta.url), "templates", "custom-check.template.ts");
|
|
3462
3748
|
var CHECKS_DIR = join12("scripts", "vinaya-checks");
|
|
@@ -3468,14 +3754,14 @@ function newCheckCommand(args) {
|
|
|
3468
3754
|
process.exit(2);
|
|
3469
3755
|
}
|
|
3470
3756
|
const scriptsDir = join12(process.cwd(), CHECKS_DIR);
|
|
3471
|
-
if (!
|
|
3757
|
+
if (!existsSync11(scriptsDir))
|
|
3472
3758
|
mkdirSync3(scriptsDir, { recursive: true });
|
|
3473
3759
|
const targetPath = join12(scriptsDir, `${name}.ts`);
|
|
3474
|
-
if (
|
|
3760
|
+
if (existsSync11(targetPath)) {
|
|
3475
3761
|
console.error(`Error: ${targetPath} already exists.`);
|
|
3476
3762
|
process.exit(1);
|
|
3477
3763
|
}
|
|
3478
|
-
const template =
|
|
3764
|
+
const template = readFileSync9(TEMPLATE_PATH, "utf-8");
|
|
3479
3765
|
const contents = template.split("{{CHECK_NAME}}").join(name);
|
|
3480
3766
|
writeFileSync6(targetPath, contents, "utf-8");
|
|
3481
3767
|
chmodSync2(targetPath, 493);
|
|
@@ -3639,16 +3925,16 @@ function prEditCommand(args) {
|
|
|
3639
3925
|
|
|
3640
3926
|
// src/commands/studio.ts
|
|
3641
3927
|
import { spawn as spawn2 } from "node:child_process";
|
|
3642
|
-
import { existsSync as
|
|
3928
|
+
import { existsSync as existsSync12, readFileSync as readFileSync10 } from "node:fs";
|
|
3643
3929
|
import { dirname as dirname4, join as join13 } from "node:path";
|
|
3644
3930
|
function resolveStudioTarget(cwd) {
|
|
3645
3931
|
let dir = cwd;
|
|
3646
3932
|
for (;; ) {
|
|
3647
3933
|
const webDir = join13(dir, "apps", "vinaya", "web");
|
|
3648
3934
|
const pkgPath = join13(webDir, "package.json");
|
|
3649
|
-
if (
|
|
3935
|
+
if (existsSync12(pkgPath)) {
|
|
3650
3936
|
try {
|
|
3651
|
-
const pkg = JSON.parse(
|
|
3937
|
+
const pkg = JSON.parse(readFileSync10(pkgPath, "utf-8"));
|
|
3652
3938
|
if (pkg.name === "@atta/vinaya-web") {
|
|
3653
3939
|
return { kind: "workspace", webDir };
|
|
3654
3940
|
}
|
|
@@ -3681,7 +3967,7 @@ async function runStudio(cwd, args) {
|
|
|
3681
3967
|
}
|
|
3682
3968
|
|
|
3683
3969
|
// src/commands/upgrade.ts
|
|
3684
|
-
import { existsSync as
|
|
3970
|
+
import { existsSync as existsSync13, readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "node:fs";
|
|
3685
3971
|
import { join as join14 } from "node:path";
|
|
3686
3972
|
function realDeps6() {
|
|
3687
3973
|
return {
|
|
@@ -3699,11 +3985,11 @@ function flags2(args) {
|
|
|
3699
3985
|
}
|
|
3700
3986
|
function readManifest3(repoRoot) {
|
|
3701
3987
|
const p = join14(repoRoot, CONFIG_PATH);
|
|
3702
|
-
if (!
|
|
3988
|
+
if (!existsSync13(p))
|
|
3703
3989
|
return { kind: "missing" };
|
|
3704
3990
|
let raw;
|
|
3705
3991
|
try {
|
|
3706
|
-
raw = JSON.parse(
|
|
3992
|
+
raw = JSON.parse(readFileSync11(p, "utf-8"));
|
|
3707
3993
|
} catch (err) {
|
|
3708
3994
|
return { kind: "invalid", error: `invalid JSON: ${err.message}` };
|
|
3709
3995
|
}
|
|
@@ -3720,7 +4006,7 @@ function readManifest3(repoRoot) {
|
|
|
3720
4006
|
}
|
|
3721
4007
|
function writeManifestVersion(repoRoot, manifest) {
|
|
3722
4008
|
const configAbs = join14(repoRoot, CONFIG_PATH);
|
|
3723
|
-
const seed = JSON.parse(
|
|
4009
|
+
const seed = JSON.parse(readFileSync11(configAbs, "utf-8"));
|
|
3724
4010
|
const updated = { ...manifest, version: MANAGED_MANIFEST_VERSION };
|
|
3725
4011
|
writeFileSync7(configAbs, `${JSON.stringify({ ...seed, managed: updated }, null, 2)}
|
|
3726
4012
|
`, "utf-8");
|
|
@@ -3734,7 +4020,7 @@ function planUpgrade(ops, repoRoot, manifest) {
|
|
|
3734
4020
|
for (const op of ops) {
|
|
3735
4021
|
if (op.kind === "create-file") {
|
|
3736
4022
|
const abs2 = join14(repoRoot, op.path);
|
|
3737
|
-
const exists =
|
|
4023
|
+
const exists = existsSync13(abs2);
|
|
3738
4024
|
const owned = ownedFiles.has(op.path);
|
|
3739
4025
|
let action;
|
|
3740
4026
|
if (op.path === CONFIG_PATH) {
|
|
@@ -3744,7 +4030,7 @@ function planUpgrade(ops, repoRoot, manifest) {
|
|
|
3744
4030
|
} else if (!exists) {
|
|
3745
4031
|
action = "recreate";
|
|
3746
4032
|
hasChanges = true;
|
|
3747
|
-
} else if (
|
|
4033
|
+
} else if (readFileSync11(abs2, "utf-8") !== op.content) {
|
|
3748
4034
|
action = "regenerate";
|
|
3749
4035
|
hasChanges = true;
|
|
3750
4036
|
} else {
|
|
@@ -3757,11 +4043,11 @@ function planUpgrade(ops, repoRoot, manifest) {
|
|
|
3757
4043
|
let action;
|
|
3758
4044
|
if (!owned) {
|
|
3759
4045
|
action = "not-installed";
|
|
3760
|
-
} else if (!
|
|
4046
|
+
} else if (!existsSync13(abs2)) {
|
|
3761
4047
|
action = "recreate-host";
|
|
3762
4048
|
hasChanges = true;
|
|
3763
4049
|
} else {
|
|
3764
|
-
const content =
|
|
4050
|
+
const content = readFileSync11(abs2, "utf-8");
|
|
3765
4051
|
const { begin, end } = markerLines(op.marker, op.comment);
|
|
3766
4052
|
if (!(content.includes(begin) && content.includes(end))) {
|
|
3767
4053
|
action = "recreate-append";
|
|
@@ -3838,7 +4124,7 @@ function renderUpgradeDiff(plan) {
|
|
|
3838
4124
|
}
|
|
3839
4125
|
function regenerateBlock(repoRoot, op) {
|
|
3840
4126
|
const abs2 = join14(repoRoot, op.path);
|
|
3841
|
-
const content =
|
|
4127
|
+
const content = readFileSync11(abs2, "utf-8");
|
|
3842
4128
|
const stripped = stripBlockFromContent(content, op.marker, op.comment);
|
|
3843
4129
|
if (stripped !== null) {
|
|
3844
4130
|
writeFileSync7(abs2, stripped.endsWith(`
|
|
@@ -4079,6 +4365,9 @@ var COMMANDS = [
|
|
|
4079
4365
|
{ flag: "--diff-only", description: "Scope diff-declared checks to changed files" },
|
|
4080
4366
|
{ flag: "--parallel[=n]", description: "Concurrency cap (default: cpu-derived)" }
|
|
4081
4367
|
],
|
|
4368
|
+
details: [
|
|
4369
|
+
"Each spawned check's child process sees only a fixed baseline (`PATH`, `LANG`, `HOME`, `HTTPS_PROXY`, `HTTP_PROXY`, `NO_PROXY`, `TMPDIR`) plus whatever its `CheckSpec['env']` declaration explicitly forwards — never the full parent environment. A required (`true`) or unsatisfied `anyOf` declaration missing from the caller's environment synthesizes a `CheckError` before the check ever spawns. Declare `env` (a core check's own registration, or `vinaya.config.json`'s `checks.<name>.env` for a custom one) for any check that reads `process.env`/`Bun.env`/`Deno.env` directly — `vinaya doctor` carries the permanent diagnostic for one that doesn't."
|
|
4370
|
+
],
|
|
4082
4371
|
status: "shipped"
|
|
4083
4372
|
},
|
|
4084
4373
|
{
|
|
@@ -4148,6 +4437,9 @@ var COMMANDS = [
|
|
|
4148
4437
|
name: "doctor",
|
|
4149
4438
|
description: "Diagnose hook, workflow, and config health — report only, never mutates",
|
|
4150
4439
|
flags: [{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }],
|
|
4440
|
+
details: [
|
|
4441
|
+
'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).'
|
|
4442
|
+
],
|
|
4151
4443
|
status: "shipped"
|
|
4152
4444
|
},
|
|
4153
4445
|
{
|
|
@@ -4252,7 +4544,7 @@ function printHelp() {
|
|
|
4252
4544
|
// src/index.ts
|
|
4253
4545
|
var PACKAGE_ROOT = join15(dirname5(fileURLToPath2(import.meta.url)), "..");
|
|
4254
4546
|
function readVersion2() {
|
|
4255
|
-
const pkg = JSON.parse(
|
|
4547
|
+
const pkg = JSON.parse(readFileSync12(join15(PACKAGE_ROOT, "package.json"), "utf-8"));
|
|
4256
4548
|
return pkg.version;
|
|
4257
4549
|
}
|
|
4258
4550
|
var [, , command, ...args] = process.argv;
|