@cosyte/script-utils 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -12
- package/LICENSE +1 -1
- package/README.md +192 -12
- package/agent-notes.d.ts +92 -0
- package/agent-notes.js +512 -0
- package/attw.d.ts +18 -0
- package/attw.js +188 -8
- package/catalog.d.ts +110 -0
- package/catalog.js +251 -0
- package/catalog.json +210 -0
- package/claude-card.d.ts +64 -0
- package/claude-card.js +296 -0
- package/internal-refs.d.ts +12 -2
- package/internal-refs.js +131 -6
- package/package.json +27 -1
- package/phi-scan.d.ts +22 -0
- package/phi-scan.js +152 -4
- package/readme.d.ts +81 -0
- package/readme.js +489 -0
package/internal-refs.js
CHANGED
|
@@ -24,8 +24,16 @@
|
|
|
24
24
|
// change what this gate looks for or what it concludes.
|
|
25
25
|
|
|
26
26
|
import { execFileSync } from "node:child_process";
|
|
27
|
-
import {
|
|
28
|
-
|
|
27
|
+
import {
|
|
28
|
+
closeSync,
|
|
29
|
+
constants as fsConstants,
|
|
30
|
+
fstatSync,
|
|
31
|
+
openSync,
|
|
32
|
+
readFileSync,
|
|
33
|
+
realpathSync,
|
|
34
|
+
} from "node:fs";
|
|
35
|
+
import { basename, join } from "node:path";
|
|
36
|
+
import { fileURLToPath } from "node:url";
|
|
29
37
|
|
|
30
38
|
/**
|
|
31
39
|
* The exit codes this gate assigns, as a caller may redeclare them.
|
|
@@ -332,7 +340,7 @@ export function compileCanonicalRules(resolved) {
|
|
|
332
340
|
name: "phase or wave language",
|
|
333
341
|
patterns: [
|
|
334
342
|
new RegExp(
|
|
335
|
-
`\\b(?:roadmap
|
|
343
|
+
`\\b(?:roadmap phases?\\b[ ]?[A-Za-z0-9]*|${PHASE_NOT_CLINICAL}phases?[ -]${PHASE_NOT_FIELD}` +
|
|
336
344
|
`[A-Za-z0-9]+[a-z]?\\b|wave \\d+\\b|the \\w+ and final phase\\b|` +
|
|
337
345
|
`documentation residual\\b|${ORDINAL} (?:slice|wave)\\b)`,
|
|
338
346
|
"gi",
|
|
@@ -340,6 +348,8 @@ export function compileCanonicalRules(resolved) {
|
|
|
340
348
|
],
|
|
341
349
|
positives: [
|
|
342
350
|
"Phase 5b closes it (Phase W, Phase-L and the thirteenth slice landed earlier, in wave 2)",
|
|
351
|
+
// The plural, which ten of the eleven per-repository copies matched (`phases?[ -]`).
|
|
352
|
+
"Phases 1 to 6 shipped together",
|
|
343
353
|
],
|
|
344
354
|
negatives: [
|
|
345
355
|
"CSP-1 Study Phase Identifier, CSP-2 Study Phase Start Date/Time, CSP-3 Study Phase End " +
|
|
@@ -351,8 +361,12 @@ export function compileCanonicalRules(resolved) {
|
|
|
351
361
|
{
|
|
352
362
|
id: "adr-reference",
|
|
353
363
|
name: "ADR reference",
|
|
354
|
-
|
|
355
|
-
|
|
364
|
+
// `[ /-]`: ten of the eleven per-repository copies also matched an ADR path segment.
|
|
365
|
+
patterns: [new RegExp("\\bADR[ /-]?\\d{3,4}\\b", "gi")],
|
|
366
|
+
positives: [
|
|
367
|
+
"Decided in ADR 0015 and restated in ADR-0021",
|
|
368
|
+
"Recorded in docs/adr/0001-scope.md",
|
|
369
|
+
],
|
|
356
370
|
negatives: ["ADR is not a segment, and 0015 alone is a value"],
|
|
357
371
|
},
|
|
358
372
|
{
|
|
@@ -646,13 +660,26 @@ function compileExtraRule(rule, index) {
|
|
|
646
660
|
}
|
|
647
661
|
continue;
|
|
648
662
|
}
|
|
663
|
+
// `{ source, flags }`: the form a JSON configuration can carry, so a rule read from
|
|
664
|
+
// `scripts/internal-refs.json` can be case-insensitive (`"flags": "i"`). `g` is always added.
|
|
665
|
+
if (isPlainObject(entry) && typeof entry.source === "string") {
|
|
666
|
+
const flags = typeof entry.flags === "string" ? entry.flags : "";
|
|
667
|
+
try {
|
|
668
|
+
patterns.push(new RegExp(entry.source, flags.includes("g") ? flags : `${flags}g`));
|
|
669
|
+
} catch (cause) {
|
|
670
|
+
problems.push(`${where} carries a pattern that does not compile: ${String(cause)}`);
|
|
671
|
+
}
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
649
674
|
if (entry instanceof RegExp) {
|
|
650
675
|
patterns.push(
|
|
651
676
|
new RegExp(entry.source, entry.flags.includes("g") ? entry.flags : `${entry.flags}g`),
|
|
652
677
|
);
|
|
653
678
|
continue;
|
|
654
679
|
}
|
|
655
|
-
problems.push(
|
|
680
|
+
problems.push(
|
|
681
|
+
`${where} carries a pattern that is neither a string, a RegExp nor a { source, flags } object`,
|
|
682
|
+
);
|
|
656
683
|
}
|
|
657
684
|
if (patterns.length === 0) problems.push(`${where} declares no pattern`);
|
|
658
685
|
if (!isStringList(rule.positives) || rule.positives.length === 0) {
|
|
@@ -1361,3 +1388,101 @@ function report(hits) {
|
|
|
1361
1388
|
|
|
1362
1389
|
/** Re-exported for a caller that wants to state, in its own docs, what it cannot turn off. */
|
|
1363
1390
|
export { DEFAULT_EXIT_CODES, REFUSED_OPTIONS };
|
|
1391
|
+
|
|
1392
|
+
// ---- The command-line form (ST34) --------------------------------------------
|
|
1393
|
+
//
|
|
1394
|
+
// `node node_modules/@cosyte/script-utils/internal-refs.js` runs this gate over the repository in
|
|
1395
|
+
// the working directory with no caller script: the axes a caller would pass are read from
|
|
1396
|
+
// `scripts/internal-refs.json`, the one file a repository keeps for this gate. Keys starting with
|
|
1397
|
+
// `$` are notes and are ignored. The exit codes are the house contract, 0 clean, 1 hits, 2 the gate
|
|
1398
|
+
// could not run, and the file may not set them, the repository root or the output sinks: the
|
|
1399
|
+
// command-line form owns those, and a file that names one is refused rather than half-honoured.
|
|
1400
|
+
|
|
1401
|
+
/** Where the command-line form reads the repository's axes. */
|
|
1402
|
+
export const INTERNAL_REFS_CONFIG_PATH = "scripts/internal-refs.json";
|
|
1403
|
+
|
|
1404
|
+
/** The exit codes of the command-line form. */
|
|
1405
|
+
const CLI_EXIT_CODES = Object.freeze({ clean: 0, hits: 1, refuse: 2 });
|
|
1406
|
+
|
|
1407
|
+
/** Keys a configuration file may not set, because the command-line form sets them. */
|
|
1408
|
+
const CLI_OWNED_KEYS = ["exitCodes", "repoRoot", "write"];
|
|
1409
|
+
|
|
1410
|
+
/**
|
|
1411
|
+
* Run the gate from the command line over the repository in the working directory.
|
|
1412
|
+
*
|
|
1413
|
+
* @param {string[]} argv Only `--config <path>` is accepted (default `scripts/internal-refs.json`).
|
|
1414
|
+
* @returns {number} 0 clean, 1 hits, 2 the gate could not run.
|
|
1415
|
+
*/
|
|
1416
|
+
export function runInternalRefsCli(argv) {
|
|
1417
|
+
let path = INTERNAL_REFS_CONFIG_PATH;
|
|
1418
|
+
const args = argv.filter((arg) => arg !== "--");
|
|
1419
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
1420
|
+
if (args[i] === "--config" && typeof args[i + 1] === "string") {
|
|
1421
|
+
path = args[i + 1];
|
|
1422
|
+
i += 1;
|
|
1423
|
+
} else {
|
|
1424
|
+
process.stderr.write(
|
|
1425
|
+
`internal-refs: ${JSON.stringify(args[i])} is not an argument this gate accepts (only ` +
|
|
1426
|
+
`--config <path>). Nothing was checked.\n`,
|
|
1427
|
+
);
|
|
1428
|
+
return CLI_EXIT_CODES.refuse;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
let parsed;
|
|
1432
|
+
try {
|
|
1433
|
+
parsed = JSON.parse(readFileSync(join(process.cwd(), path), "utf8"));
|
|
1434
|
+
} catch (err) {
|
|
1435
|
+
process.stderr.write(
|
|
1436
|
+
`internal-refs: could not read ${path}: ${err.message} Nothing was checked.\n`,
|
|
1437
|
+
);
|
|
1438
|
+
return CLI_EXIT_CODES.refuse;
|
|
1439
|
+
}
|
|
1440
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1441
|
+
process.stderr.write(`internal-refs: ${path} is not a JSON object. Nothing was checked.\n`);
|
|
1442
|
+
return CLI_EXIT_CODES.refuse;
|
|
1443
|
+
}
|
|
1444
|
+
const owned = CLI_OWNED_KEYS.filter((key) => key in parsed);
|
|
1445
|
+
if (owned.length > 0) {
|
|
1446
|
+
process.stderr.write(
|
|
1447
|
+
`internal-refs: ${path} sets ${owned.join(", ")}, which the command-line form sets ` +
|
|
1448
|
+
`itself. Remove ${owned.length === 1 ? "it" : "them"}. Nothing was checked.\n`,
|
|
1449
|
+
);
|
|
1450
|
+
return CLI_EXIT_CODES.refuse;
|
|
1451
|
+
}
|
|
1452
|
+
const config = Object.fromEntries(Object.entries(parsed).filter(([key]) => !key.startsWith("$")));
|
|
1453
|
+
return runInternalRefsScan({ ...config, exitCodes: CLI_EXIT_CODES, repoRoot: process.cwd() });
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
/**
|
|
1457
|
+
* Whether node was pointed at this file, which is the command-line form.
|
|
1458
|
+
*
|
|
1459
|
+
* SELF-CONTAINED ON PURPOSE: this module is also vendored and copied as a single file, so it may not
|
|
1460
|
+
* import a sibling at load. A file whose name differs from the one node was pointed at cannot be the
|
|
1461
|
+
* entry point, and that is settled without loading anything; only a matching name asks `index.js`'s
|
|
1462
|
+
* `isCliEntrypoint`, and a copy with no `index.js` beside it compares real paths instead. Unsure is
|
|
1463
|
+
* yes, so a gate that cannot tell runs loudly rather than skipping.
|
|
1464
|
+
*
|
|
1465
|
+
* @param {string} moduleUrl This module's `import.meta.url`.
|
|
1466
|
+
* @returns {Promise<boolean>}
|
|
1467
|
+
*/
|
|
1468
|
+
async function invokedAsCommand(moduleUrl) {
|
|
1469
|
+
const entry = process.argv[1];
|
|
1470
|
+
if (typeof entry !== "string" || entry === "") return false;
|
|
1471
|
+
const own = fileURLToPath(moduleUrl);
|
|
1472
|
+
const stem = (path) => basename(path).replace(/\.[cm]?[jt]s$/, "");
|
|
1473
|
+
if (stem(entry) !== stem(own)) return false;
|
|
1474
|
+
try {
|
|
1475
|
+
const { isCliEntrypoint } = await import("./index.js");
|
|
1476
|
+
return isCliEntrypoint(moduleUrl);
|
|
1477
|
+
} catch {
|
|
1478
|
+
try {
|
|
1479
|
+
return realpathSync(entry) === realpathSync(own);
|
|
1480
|
+
} catch {
|
|
1481
|
+
return true;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
void invokedAsCommand(import.meta.url).then((command) => {
|
|
1487
|
+
if (command) process.exitCode = runInternalRefsCli(process.argv.slice(2));
|
|
1488
|
+
});
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosyte/script-utils",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Zero-dependency helpers for the repo-local gate scripts in @cosyte/* repos.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
7
7
|
"entrypoint",
|
|
8
8
|
"import-meta",
|
|
9
9
|
"phi",
|
|
10
|
+
"readme",
|
|
10
11
|
"scripts",
|
|
11
12
|
"cosyte"
|
|
12
13
|
],
|
|
@@ -37,6 +38,22 @@
|
|
|
37
38
|
"./attw": {
|
|
38
39
|
"types": "./attw.d.ts",
|
|
39
40
|
"default": "./attw.js"
|
|
41
|
+
},
|
|
42
|
+
"./readme": {
|
|
43
|
+
"types": "./readme.d.ts",
|
|
44
|
+
"default": "./readme.js"
|
|
45
|
+
},
|
|
46
|
+
"./claude-card": {
|
|
47
|
+
"types": "./claude-card.d.ts",
|
|
48
|
+
"default": "./claude-card.js"
|
|
49
|
+
},
|
|
50
|
+
"./catalog": {
|
|
51
|
+
"types": "./catalog.d.ts",
|
|
52
|
+
"default": "./catalog.js"
|
|
53
|
+
},
|
|
54
|
+
"./agent-notes": {
|
|
55
|
+
"types": "./agent-notes.d.ts",
|
|
56
|
+
"default": "./agent-notes.js"
|
|
40
57
|
}
|
|
41
58
|
},
|
|
42
59
|
"files": [
|
|
@@ -48,6 +65,15 @@
|
|
|
48
65
|
"internal-refs.d.ts",
|
|
49
66
|
"attw.js",
|
|
50
67
|
"attw.d.ts",
|
|
68
|
+
"readme.js",
|
|
69
|
+
"readme.d.ts",
|
|
70
|
+
"claude-card.js",
|
|
71
|
+
"claude-card.d.ts",
|
|
72
|
+
"catalog.js",
|
|
73
|
+
"catalog.d.ts",
|
|
74
|
+
"catalog.json",
|
|
75
|
+
"agent-notes.js",
|
|
76
|
+
"agent-notes.d.ts",
|
|
51
77
|
"check-no-emdash.sh",
|
|
52
78
|
"README.md",
|
|
53
79
|
"CHANGELOG.md"
|
package/phi-scan.d.ts
CHANGED
|
@@ -274,3 +274,25 @@ export declare function runPhiScan(config: PhiScanConfig): number;
|
|
|
274
274
|
* is read by NEITHER sweeping route, while a `.md` named explicitly on argv IS scanned.
|
|
275
275
|
*/
|
|
276
276
|
export declare function exemptsMarkdown(relPath: string): boolean;
|
|
277
|
+
|
|
278
|
+
/** Where the command-line form reads what a repository leaves out: `scripts/phi-scan.exclude`. */
|
|
279
|
+
export declare const PHI_EXCLUDE_PATH: "scripts/phi-scan.exclude";
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Read an exclusions file (one literal tracked path per line, `#` lines are comments) against the
|
|
283
|
+
* repository's tracked paths. A directory, a pattern, a repeat or a path git does not track is a
|
|
284
|
+
* problem, never a silent skip.
|
|
285
|
+
*/
|
|
286
|
+
export declare function readPhiExclusions(
|
|
287
|
+
text: string,
|
|
288
|
+
tracked: ReadonlySet<string>,
|
|
289
|
+
): { paths: Set<string>; problems: string[] };
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* The command-line form: the engine over the repository in the working directory, with exit codes
|
|
293
|
+
* 0 clean, 1 hits, 2 refuse, the whole repository as the scan root, the `--staged` route reading
|
|
294
|
+
* everything but Markdown, and `scripts/phi-scan.exclude`. The cross-cutting floor only: a
|
|
295
|
+
* repository that needs field-level detectors keeps a caller passing `detect`.
|
|
296
|
+
* `node node_modules/@cosyte/script-utils/phi-scan.js [--staged | <path>...]`.
|
|
297
|
+
*/
|
|
298
|
+
export declare function runPhiScanCli(argv: string[]): number;
|
package/phi-scan.js
CHANGED
|
@@ -39,10 +39,11 @@
|
|
|
39
39
|
// repository reads its own `node_modules` copy of this file only if it is not gitignored, and a
|
|
40
40
|
// diagnostic ABOUT a PHI leak is itself a PHI surface. Describe the shape; never write one down.
|
|
41
41
|
|
|
42
|
-
import { readFileSync, statSync, lstatSync, existsSync, readdirSync } from "node:fs";
|
|
42
|
+
import { readFileSync, statSync, lstatSync, existsSync, readdirSync, realpathSync } from "node:fs";
|
|
43
43
|
import { execFileSync } from "node:child_process";
|
|
44
44
|
import { createHash } from "node:crypto";
|
|
45
|
-
import { join, resolve, relative, sep, isAbsolute } from "node:path";
|
|
45
|
+
import { basename, join, resolve, relative, sep, isAbsolute } from "node:path";
|
|
46
|
+
import { fileURLToPath } from "node:url";
|
|
46
47
|
|
|
47
48
|
/**
|
|
48
49
|
* Every state in which the scan cannot account for something. Raised throughout, caught in `run`,
|
|
@@ -1135,10 +1136,15 @@ class PhiScan {
|
|
|
1135
1136
|
buildTargetsForStaged() {
|
|
1136
1137
|
let listBuf;
|
|
1137
1138
|
try {
|
|
1138
|
-
// SECURITY: array-form execFileSync, no shell.
|
|
1139
|
+
// SECURITY: array-form execFileSync, no shell. `--ignore-submodules=none` because
|
|
1140
|
+
// `diff.ignoreSubmodules` in a user's own git config would otherwise hide a staged gitlink
|
|
1141
|
+
// from this list, and the route would never refuse it (x12, terminology and astm pass it;
|
|
1142
|
+
// measured in x12's own suite).
|
|
1139
1143
|
listBuf = execFileSync(
|
|
1140
1144
|
"git",
|
|
1141
|
-
["diff", "--cached", "--raw", "-z", "--no-renames", "--diff-filter=d"]
|
|
1145
|
+
["diff", "--cached", "--raw", "-z", "--no-renames", "--diff-filter=d"].concat(
|
|
1146
|
+
"--ignore-submodules=none",
|
|
1147
|
+
),
|
|
1142
1148
|
{
|
|
1143
1149
|
cwd: this.cfg.repoRoot,
|
|
1144
1150
|
encoding: "buffer",
|
|
@@ -1942,3 +1948,145 @@ function gitModeKind(mode) {
|
|
|
1942
1948
|
if (mode === "160000") return "a gitlink (a nested repository)";
|
|
1943
1949
|
return `a git mode-${mode} entry`;
|
|
1944
1950
|
}
|
|
1951
|
+
|
|
1952
|
+
// ---- The command-line form (ST34) ---------------------------------------------------------------
|
|
1953
|
+
//
|
|
1954
|
+
// `node node_modules/@cosyte/script-utils/phi-scan.js [--staged | <path>...]` runs this engine over
|
|
1955
|
+
// the repository in the working directory with the house settings and no caller script: exit codes
|
|
1956
|
+
// 0 clean, 1 hits, 2 refuse; the whole repository as the one scan root; the `--staged` route reading
|
|
1957
|
+
// everything but Markdown (`exemptsMarkdown`); the allow-list at `scripts/phi-allow-list.txt` and the
|
|
1958
|
+
// override log at `phi-scan-overrides.md`, the engine's defaults; and what the repository leaves
|
|
1959
|
+
// out, in `scripts/phi-scan.exclude`.
|
|
1960
|
+
//
|
|
1961
|
+
// AN EXCLUSION IS ONE LITERAL TRACKED PATH, as `excludedPaths` requires: no pattern, no directory,
|
|
1962
|
+
// because a class quietly grows new members. One per line; a line whose first character is `#` is a
|
|
1963
|
+
// comment. An entry that names no tracked path is refused rather than ignored: an exclusion that has
|
|
1964
|
+
// outlived its file is a hole nobody is looking at.
|
|
1965
|
+
//
|
|
1966
|
+
// IT CARRIES THE CROSS-CUTTING FLOOR ONLY. A repository whose standard needs field-level detectors
|
|
1967
|
+
// keeps a caller passing `detect` until the per-standard detectors ship in this package.
|
|
1968
|
+
|
|
1969
|
+
/** Where the command-line form reads what a repository leaves out. */
|
|
1970
|
+
export const PHI_EXCLUDE_PATH = "scripts/phi-scan.exclude";
|
|
1971
|
+
|
|
1972
|
+
/** The exit codes of the command-line form. */
|
|
1973
|
+
const CLI_EXIT_CODES = Object.freeze({ clean: 0, hits: 1, refuse: 2 });
|
|
1974
|
+
|
|
1975
|
+
/**
|
|
1976
|
+
* Read an exclusions file against the repository's tracked paths.
|
|
1977
|
+
*
|
|
1978
|
+
* @param {string} text The file's contents.
|
|
1979
|
+
* @param {ReadonlySet<string>} tracked Every tracked path, repo-relative and forward-slashed.
|
|
1980
|
+
* @returns {{ paths: Set<string>, problems: string[] }}
|
|
1981
|
+
*/
|
|
1982
|
+
export function readPhiExclusions(text, tracked) {
|
|
1983
|
+
const paths = new Set();
|
|
1984
|
+
const problems = [];
|
|
1985
|
+
text.split("\n").forEach((raw, index) => {
|
|
1986
|
+
const entry = raw.trim();
|
|
1987
|
+
if (entry === "" || entry.startsWith("#")) return;
|
|
1988
|
+
const where = `line ${index + 1}: ${JSON.stringify(entry)}`;
|
|
1989
|
+
if (entry.endsWith("/") || /[*?[\]]/.test(entry)) {
|
|
1990
|
+
problems.push(`${where} is a directory or a pattern; an exclusion is one literal path.`);
|
|
1991
|
+
} else if (paths.has(entry)) {
|
|
1992
|
+
problems.push(`${where} is listed twice.`);
|
|
1993
|
+
} else if (!tracked.has(entry)) {
|
|
1994
|
+
problems.push(`${where} is not a tracked path, so it excludes nothing.`);
|
|
1995
|
+
} else {
|
|
1996
|
+
paths.add(entry);
|
|
1997
|
+
}
|
|
1998
|
+
});
|
|
1999
|
+
return { paths, problems };
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
/**
|
|
2003
|
+
* Run the engine from the command line over the repository in the working directory.
|
|
2004
|
+
*
|
|
2005
|
+
* @param {string[]} argv The engine's own arguments: `--staged`, paths, `--allow-fixture <path>`.
|
|
2006
|
+
* @returns {number} 0 clean, 1 hits, 2 the scan could not account for something.
|
|
2007
|
+
*/
|
|
2008
|
+
export function runPhiScanCli(argv) {
|
|
2009
|
+
const repoRoot = process.cwd();
|
|
2010
|
+
let excludedPaths = new Set();
|
|
2011
|
+
const file = join(repoRoot, ...PHI_EXCLUDE_PATH.split("/"));
|
|
2012
|
+
if (existsSync(file)) {
|
|
2013
|
+
let read;
|
|
2014
|
+
try {
|
|
2015
|
+
const tracked = new Set(
|
|
2016
|
+
execFileSync("git", ["ls-files", "-z"], {
|
|
2017
|
+
cwd: repoRoot,
|
|
2018
|
+
encoding: "utf8",
|
|
2019
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2020
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
2021
|
+
})
|
|
2022
|
+
.split("\0")
|
|
2023
|
+
.filter((path) => path !== ""),
|
|
2024
|
+
);
|
|
2025
|
+
read = readPhiExclusions(readFileSync(file, "utf8"), tracked);
|
|
2026
|
+
} catch (err) {
|
|
2027
|
+
process.stderr.write(
|
|
2028
|
+
`[phi-scan] could not read ${PHI_EXCLUDE_PATH}: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
2029
|
+
);
|
|
2030
|
+
return CLI_EXIT_CODES.refuse;
|
|
2031
|
+
}
|
|
2032
|
+
if (read.problems.length > 0) {
|
|
2033
|
+
process.stderr.write(
|
|
2034
|
+
`[phi-scan] ${PHI_EXCLUDE_PATH} is not usable, so nothing was scanned:\n` +
|
|
2035
|
+
read.problems.map((problem) => ` ${problem}\n`).join(""),
|
|
2036
|
+
);
|
|
2037
|
+
return CLI_EXIT_CODES.refuse;
|
|
2038
|
+
}
|
|
2039
|
+
excludedPaths = read.paths;
|
|
2040
|
+
}
|
|
2041
|
+
try {
|
|
2042
|
+
return runPhiScan({
|
|
2043
|
+
exitCodes: CLI_EXIT_CODES,
|
|
2044
|
+
scanRoots: ["."],
|
|
2045
|
+
isStagedReadable: exemptsMarkdown,
|
|
2046
|
+
excludedPaths,
|
|
2047
|
+
repoRoot,
|
|
2048
|
+
argv,
|
|
2049
|
+
});
|
|
2050
|
+
} catch (err) {
|
|
2051
|
+
// The backstop: an error the engine did not raise as a refusal must still not take node's own
|
|
2052
|
+
// exit 1, which this contract reserves for hits.
|
|
2053
|
+
process.stderr.write(
|
|
2054
|
+
`[phi-scan] the scan failed and did not complete: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
2055
|
+
);
|
|
2056
|
+
return CLI_EXIT_CODES.refuse;
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
/**
|
|
2061
|
+
* Whether node was pointed at this file, which is the command-line form.
|
|
2062
|
+
*
|
|
2063
|
+
* SELF-CONTAINED ON PURPOSE: this module is also vendored and copied as a single file, so it may not
|
|
2064
|
+
* import a sibling at load. A file whose name differs from the one node was pointed at cannot be the
|
|
2065
|
+
* entry point, and that is settled without loading anything; only a matching name asks `index.js`'s
|
|
2066
|
+
* `isCliEntrypoint`, and a copy with no `index.js` beside it compares real paths instead. Unsure is
|
|
2067
|
+
* yes, so a gate that cannot tell runs loudly rather than skipping.
|
|
2068
|
+
*
|
|
2069
|
+
* @param {string} moduleUrl This module's `import.meta.url`.
|
|
2070
|
+
* @returns {Promise<boolean>}
|
|
2071
|
+
*/
|
|
2072
|
+
async function invokedAsCommand(moduleUrl) {
|
|
2073
|
+
const entry = process.argv[1];
|
|
2074
|
+
if (typeof entry !== "string" || entry === "") return false;
|
|
2075
|
+
const own = fileURLToPath(moduleUrl);
|
|
2076
|
+
const stem = (path) => basename(path).replace(/\.[cm]?[jt]s$/, "");
|
|
2077
|
+
if (stem(entry) !== stem(own)) return false;
|
|
2078
|
+
try {
|
|
2079
|
+
const { isCliEntrypoint } = await import("./index.js");
|
|
2080
|
+
return isCliEntrypoint(moduleUrl);
|
|
2081
|
+
} catch {
|
|
2082
|
+
try {
|
|
2083
|
+
return realpathSync(entry) === realpathSync(own);
|
|
2084
|
+
} catch {
|
|
2085
|
+
return true;
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
void invokedAsCommand(import.meta.url).then((command) => {
|
|
2091
|
+
if (command) process.exitCode = runPhiScanCli(process.argv.slice(2));
|
|
2092
|
+
});
|
package/readme.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for `@cosyte/script-utils/readme`: the README gate for every `@cosyte/*` library, which
|
|
3
|
+
* checks the house skeleton and a size limit under npm's.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** The tile's alt text, as assets `alt-text.json` declares it for the light tile. */
|
|
7
|
+
export declare const TILE_ALT: string;
|
|
8
|
+
|
|
9
|
+
/** The shared lockup tile every library README opens with, byte for byte. */
|
|
10
|
+
export declare const TILE: string;
|
|
11
|
+
|
|
12
|
+
/** npm's own cut: the registry keeps this many characters of a README. */
|
|
13
|
+
export declare const NPM_README_LIMIT: 65536;
|
|
14
|
+
|
|
15
|
+
/** This gate's limit, in characters (UTF-16 code units). */
|
|
16
|
+
export declare const README_CHARACTER_LIMIT: 60000;
|
|
17
|
+
|
|
18
|
+
/** The longest tagline the gate accepts, in characters. */
|
|
19
|
+
export declare const TAGLINE_LIMIT: 120;
|
|
20
|
+
|
|
21
|
+
/** The `##` sections every library README carries, in order, with License last. */
|
|
22
|
+
export declare const REQUIRED_SECTIONS: readonly string[];
|
|
23
|
+
|
|
24
|
+
/** A rule id a finding can carry. */
|
|
25
|
+
export type ReadmeRule =
|
|
26
|
+
| "tile"
|
|
27
|
+
| "tile-alt"
|
|
28
|
+
| "title"
|
|
29
|
+
| "tagline"
|
|
30
|
+
| "badges"
|
|
31
|
+
| "section-missing"
|
|
32
|
+
| "section-order"
|
|
33
|
+
| "section-empty"
|
|
34
|
+
| "docs-link"
|
|
35
|
+
| "license-last"
|
|
36
|
+
| "license-link"
|
|
37
|
+
| "size";
|
|
38
|
+
|
|
39
|
+
/** Every rule id, with what it requires. */
|
|
40
|
+
export declare const README_RULES: Readonly<Record<ReadmeRule, string>>;
|
|
41
|
+
|
|
42
|
+
/** One place a README departs from the skeleton. */
|
|
43
|
+
export interface ReadmeFinding {
|
|
44
|
+
rule: ReadmeRule;
|
|
45
|
+
/** 1-based line, or 0 when the finding is about the whole file. */
|
|
46
|
+
line: number;
|
|
47
|
+
message: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The badge row a library README carries (npm version, CI, license, Node), one badge per line,
|
|
52
|
+
* derived from its package.json.
|
|
53
|
+
*
|
|
54
|
+
* @throws {TypeError} When the manifest has no `@cosyte/*` name, no github.com/cosyte repository
|
|
55
|
+
* URL, or no `engines.node`.
|
|
56
|
+
*/
|
|
57
|
+
export declare function badgeRow(manifest: Record<string, unknown>): string[];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check a library README against the house skeleton. Returns no findings when it follows it.
|
|
61
|
+
*
|
|
62
|
+
* @throws {TypeError} When the manifest cannot supply what the skeleton is derived from.
|
|
63
|
+
*/
|
|
64
|
+
export declare function checkReadme(
|
|
65
|
+
text: string,
|
|
66
|
+
manifest: Record<string, unknown>,
|
|
67
|
+
): ReadmeFinding[];
|
|
68
|
+
|
|
69
|
+
export interface ReadmeGateOptions {
|
|
70
|
+
/** The package directory holding README.md and package.json. Default: the working directory. */
|
|
71
|
+
dir?: string;
|
|
72
|
+
/** CLI arguments; only `--dir <path>` is accepted. */
|
|
73
|
+
argv?: string[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Run the gate over one package directory and return the exit code: 0 the README follows the
|
|
78
|
+
* skeleton, 1 at least one finding, 2 the input could not be read. It reports on stdout or stderr
|
|
79
|
+
* and never exits the process.
|
|
80
|
+
*/
|
|
81
|
+
export declare function runReadmeGate(options?: ReadmeGateOptions): 0 | 1 | 2;
|