@cosmicdrift/kumiko-guards 0.281.0 → 0.283.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/package.json +2 -2
- package/src/_lib/generic-reason.ts +31 -1
- package/src/_lib/guard-kit.ts +58 -0
- package/src/changes.json +23 -0
- package/src/check-runtime-isolation.ts +214 -0
- package/src/cli.ts +55 -31
- package/src/guard-escape-hatch-declared.ts +90 -24
- package/src/guard-upgrade-state.ts +212 -0
- package/src/index.ts +3 -0
- package/src/run-guards.ts +28 -10
- package/src/run-repo-checks.ts +22 -4
- package/src/run-ui-guards.ts +19 -4
- package/src/runtime-isolation-classify.ts +337 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-guards",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.283.0",
|
|
4
4
|
"description": "AST-based security guards for Kumiko repos: direct-fs/fetch, tenant escalation, admin-API, escape hatches and related checks, run over a shared ts-morph project.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"kumiko-guards": "./src/cli.ts"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@cosmicdrift/kumiko-repo-manifest": "0.
|
|
30
|
+
"@cosmicdrift/kumiko-repo-manifest": "0.283.0",
|
|
31
31
|
"ts-morph": "^28.0.0"
|
|
32
32
|
},
|
|
33
33
|
"publishConfig": {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Node, SyntaxKind } from "ts-morph";
|
|
1
|
+
import { type Node, SyntaxKind, VariableDeclarationKind } from "ts-morph";
|
|
2
2
|
|
|
3
3
|
const GENERIC_REASONS = new Set([
|
|
4
4
|
"",
|
|
@@ -29,6 +29,36 @@ export function literalReasonText(node: Node | undefined): string | undefined {
|
|
|
29
29
|
return undefined;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
// Resolves an Identifier to a module-local `const` initializer, so a
|
|
33
|
+
// shared reason constant clears the same way as writing its text inline.
|
|
34
|
+
// No import-boundary resolution, no `let` (reassignment stays unjudged).
|
|
35
|
+
function resolveConstIdentifierText(node: Node): string | undefined {
|
|
36
|
+
if (!node.isKind(SyntaxKind.Identifier)) return undefined;
|
|
37
|
+
const declarations = node.getSymbol()?.getDeclarations() ?? [];
|
|
38
|
+
for (const decl of declarations) {
|
|
39
|
+
if (!decl.isKind(SyntaxKind.VariableDeclaration)) continue;
|
|
40
|
+
if (decl.getSourceFile() !== node.getSourceFile()) continue;
|
|
41
|
+
if (decl.getVariableStatement()?.getDeclarationKind() !== VariableDeclarationKind.Const) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const text = literalReasonText(decl.getInitializer());
|
|
45
|
+
if (text !== undefined) return text;
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Same contract as literalReasonText, plus one hop: an Identifier that
|
|
51
|
+
// resolves to a module-local `const` string/template initializer resolves
|
|
52
|
+
// to that text. An import, a call, a template with substitutions, or a
|
|
53
|
+
// `let`/reassigned binding stays undefined — same "not statically judgeable"
|
|
54
|
+
// convention as literalReasonText.
|
|
55
|
+
export function resolveReasonText(node: Node | undefined): string | undefined {
|
|
56
|
+
if (!node) return undefined;
|
|
57
|
+
const literal = literalReasonText(node);
|
|
58
|
+
if (literal !== undefined) return literal;
|
|
59
|
+
return resolveConstIdentifierText(node);
|
|
60
|
+
}
|
|
61
|
+
|
|
32
62
|
export function isGenericReason(text: string): boolean {
|
|
33
63
|
const lowered = text.trim().toLowerCase();
|
|
34
64
|
if (GENERIC_PREFIXES.some((prefix) => lowered.startsWith(prefix))) {
|
package/src/_lib/guard-kit.ts
CHANGED
|
@@ -479,6 +479,23 @@ export function guardKitPreflightError(guardCount: number, rootCount: number): s
|
|
|
479
479
|
return undefined;
|
|
480
480
|
}
|
|
481
481
|
|
|
482
|
+
/**
|
|
483
|
+
* Validates CLI args for one subcommand against the flags it actually
|
|
484
|
+
* understands — every arg must match `known` exactly (not just a `--`-prefix
|
|
485
|
+
* check, which let a single-dash typo or a stray positional through
|
|
486
|
+
* unnoticed). An unknown arg must fail loud, never pass through silently.
|
|
487
|
+
*/
|
|
488
|
+
export function cliFlagsError(
|
|
489
|
+
subcommand: string,
|
|
490
|
+
argv: readonly string[],
|
|
491
|
+
known: readonly string[],
|
|
492
|
+
): string | undefined {
|
|
493
|
+
const unknown = argv.filter((arg) => !known.includes(arg));
|
|
494
|
+
if (unknown.length === 0) return undefined;
|
|
495
|
+
const knownList = known.length > 0 ? known.join(", ") : "(none)";
|
|
496
|
+
return `Unknown argument${unknown.length > 1 ? "s" : ""} for "${subcommand}": ${unknown.join(", ")}. Known flags: ${knownList}`;
|
|
497
|
+
}
|
|
498
|
+
|
|
482
499
|
export type GuardKitBannerDeps = {
|
|
483
500
|
readonly resolution?: RootResolution;
|
|
484
501
|
};
|
|
@@ -505,6 +522,47 @@ export function printGuardKitBanner(
|
|
|
505
522
|
if (project) console.log(`Project: ${project.getSourceFiles().length} files`);
|
|
506
523
|
}
|
|
507
524
|
|
|
525
|
+
export type SuiteInventory = {
|
|
526
|
+
readonly count: number;
|
|
527
|
+
readonly names: readonly string[];
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
export type GuardKitInventory = {
|
|
531
|
+
readonly version: string;
|
|
532
|
+
readonly total: number;
|
|
533
|
+
readonly suites: {
|
|
534
|
+
readonly guards: SuiteInventory;
|
|
535
|
+
readonly ui: SuiteInventory;
|
|
536
|
+
readonly checks: SuiteInventory;
|
|
537
|
+
};
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
function suiteInventory(items: readonly { readonly name: string }[]): SuiteInventory {
|
|
541
|
+
const names = items.map((item) => item.name);
|
|
542
|
+
return { count: names.length, names };
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Static registration inventory for the three suites — reads each array's
|
|
547
|
+
* `.name` only, no scan/project/guard.run(). What a consumer's CI checks
|
|
548
|
+
* against instead of running the guards, e.g. so a guard dropped from a
|
|
549
|
+
* suite's array is missing here too, not just silently absent from a run.
|
|
550
|
+
*/
|
|
551
|
+
export function buildGuardKitInventory(args: {
|
|
552
|
+
readonly guards: readonly { readonly name: string }[];
|
|
553
|
+
readonly uiGuards: readonly { readonly name: string }[];
|
|
554
|
+
readonly checks: readonly { readonly name: string }[];
|
|
555
|
+
}): GuardKitInventory {
|
|
556
|
+
const guards = suiteInventory(args.guards);
|
|
557
|
+
const ui = suiteInventory(args.uiGuards);
|
|
558
|
+
const checks = suiteInventory(args.checks);
|
|
559
|
+
return {
|
|
560
|
+
version: guardKitVersion(),
|
|
561
|
+
total: guards.count + ui.count + checks.count,
|
|
562
|
+
suites: { guards, ui, checks },
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
508
566
|
/**
|
|
509
567
|
* Vacuity floor for guards with their own `main()`.
|
|
510
568
|
*
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,27 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.283.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Escape-Hatch-Declared Guard resolves a module-local const reason instead of forcing duplicate literals",
|
|
6
|
+
"detail": "`_lib/generic-reason.ts`'s `literalReasonText` only accepted a string/template literal in place — an Identifier (even a module-local `const SOME_REASON = \"...\"`) fell through as \"not statically judgeable\" and was rejected exactly like a real placeholder. Every consumer declaring several hooks with the same justification (e.g. publicstatus's five GDPR delete hooks) had to repeat the same reason text literally in each `declareEscapeHatch({ reason })` / `escapeHatch: { reason }` / `unsafeAllTenants: { reason }` / `acknowledgeCrossTenant(reason)` call, because a shared constant made the guard fail.\nThe guard now resolves an Identifier to a module-local `const` initializer (string or non-templated template literal only — no imports, no `let`, no reassignment) via a new `resolveReasonText`, used at all four call sites; `isGenericReason` is unchanged and still applies to the resolved text, so a const resolving to `\"todo\"` is rejected exactly as before. An import, a function call, or a template literal with substitutions still doesn't resolve, and the `unsafe-raw-outside-system-scope` / `system-identity-outside-declared-scope` findings now say why when a nearby `declareEscapeHatch` call's reason is one of those three shapes. Both R2 and R3's base messages now also name `declareEscapeHatch({ reason: \"...\" })` as a direct body statement of a named hook among the allowed ways to clear the finding."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.282.0",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "kumiko-guards bin now passes flags through to the guards, ui, and checks subcommands",
|
|
12
|
+
"detail": "The bin's `guards` subcommand called run-guards.ts's suite runner directly, skipping the `--explain`/`--write-security-baseline`/`--strict-security-baseline` handling that only existed in run-guards.ts's own `if (import.meta.main)` block — a consumer running `bunx @cosmicdrift/kumiko-guards guards --write-security-baseline` got a normal guard run with the flag silently dropped. Each suite (guards, ui, checks) now exports a `run*Cli(argv)` function that validates argv against that suite's known flags and applies them; both the bin and the suite's own direct-invocation entry point call the same function, so they cannot drift apart again. Validation matches each arg exactly against the known list (not just a `--`-prefix check), so a single-dash typo or a stray positional also exits 1 with the flags that subcommand accepts, rather than passing through unnoticed."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.282.0",
|
|
16
|
+
"type": "improvement",
|
|
17
|
+
"title": "kumiko-guards list prints the registration inventory as JSON"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"version": "0.282.0",
|
|
21
|
+
"type": "improvement",
|
|
22
|
+
"title": "Port the upgrade-state and runtime-isolation guards",
|
|
23
|
+
"detail": "guard-upgrade-state and check-runtime-isolation ported from the private infra/guards package into the public @cosmicdrift/kumiko-guards package, rebuilt onto the public RepoCheck pattern with single-repo root resolution. guard-app-dockerfile, guard-doc-status, check-licenses, and check-security stay in infra/guards as CDGS-specific house rules (private registry scope, CDGS doc taxonomy, and CDGS license/security exception files with no equivalent consumer-facing mechanism in the public package) — they were deliberately not ported, not forgotten."
|
|
24
|
+
},
|
|
2
25
|
{
|
|
3
26
|
"version": "0.3.0",
|
|
4
27
|
"type": "improvement",
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Runtime-Isolation Guard.
|
|
3
|
+
//
|
|
4
|
+
// Prevents production code (runtime) from transitively loading test or
|
|
5
|
+
// tooling modules — the original trigger was a top-level Vitest import in a
|
|
6
|
+
// testing helper that crashed drizzle-kit under Node. Every file gets a
|
|
7
|
+
// runtime context assigned; every import edge is checked against a compat
|
|
8
|
+
// matrix.
|
|
9
|
+
//
|
|
10
|
+
// Per-file classification (highest priority first):
|
|
11
|
+
// 1. File directive → // @runtime <kind> in the first 5 lines
|
|
12
|
+
// 2. Path pattern → *.test.ts, *.integration.ts, *.e2e.ts,
|
|
13
|
+
// **/__tests__/**, **/testing/** → test;
|
|
14
|
+
// plus a handful of verified-isomorphic
|
|
15
|
+
// single-file/directory carve-outs (see
|
|
16
|
+
// runtime-isolation-classify.ts)
|
|
17
|
+
// 3. Workspace → package.json `"kumiko": { "runtime": "..." }`
|
|
18
|
+
// 4. Client reachability → transitively reachable, via value imports,
|
|
19
|
+
// from a browser-bundle entry
|
|
20
|
+
// (`src/client-*.tsx`) or from any file that
|
|
21
|
+
// already classifies as "client" on its own
|
|
22
|
+
// (directive/path/workspace)
|
|
23
|
+
// 5. Default → runtime
|
|
24
|
+
//
|
|
25
|
+
// Compat matrix: which runtime context may import which.
|
|
26
|
+
// runtime → runtime, client
|
|
27
|
+
// client → client
|
|
28
|
+
// dev → runtime, client, dev, tooling
|
|
29
|
+
// tooling → runtime, client, dev, tooling, test
|
|
30
|
+
// test → everything
|
|
31
|
+
//
|
|
32
|
+
// Single-repo only (unlike infra/guards' check-runtime-isolation.ts, which
|
|
33
|
+
// scans every sibling checkout in one shared ts-morph project): this public
|
|
34
|
+
// package only ever resolves the repo `cwd` sits in, so there is exactly one
|
|
35
|
+
// repo root to classify files against.
|
|
36
|
+
//
|
|
37
|
+
// Pure classification + violation detection live in
|
|
38
|
+
// `runtime-isolation-classify.ts` (unit-testable with an in-memory ts-morph
|
|
39
|
+
// project) — this file only does the repo/glob resolution, ts-morph project
|
|
40
|
+
// setup, and RepoCheck wiring.
|
|
41
|
+
//
|
|
42
|
+
// A file/import edge classified "client" importing "runtime" can be a real
|
|
43
|
+
// finding this guard needs to see, but can also be a known, reviewed
|
|
44
|
+
// exception (e.g. samples/recipes are worked examples, not the framework's
|
|
45
|
+
// own production surface — see kumiko-framework#2337 for the one currently
|
|
46
|
+
// frozen here). Exceptions are tracked via a per-repo baseline file
|
|
47
|
+
// (`.kumiko-runtime-isolation-baseline.json`, same `baselineRatchet`
|
|
48
|
+
// mechanism as check-complexity/guard-pii-annotations/etc.), not a
|
|
49
|
+
// hardcoded list in this source file: a consumer repo can freeze or clear
|
|
50
|
+
// its own findings the same way, with `--write-baseline`.
|
|
51
|
+
//
|
|
52
|
+
// Usage:
|
|
53
|
+
// bun packages/guards/src/check-runtime-isolation.ts
|
|
54
|
+
// bun packages/guards/src/check-runtime-isolation.ts --write-baseline
|
|
55
|
+
|
|
56
|
+
import * as path from "node:path";
|
|
57
|
+
import { Project } from "ts-morph";
|
|
58
|
+
import {
|
|
59
|
+
baselineRatchet,
|
|
60
|
+
type GuardViolation,
|
|
61
|
+
type RepoCheck,
|
|
62
|
+
reportResults,
|
|
63
|
+
runRepoChecks,
|
|
64
|
+
} from "./_lib/guard-kit";
|
|
65
|
+
import { frameworkTsConfigPath, type RepoRoot, resolveRepoRoots } from "./_lib/roots";
|
|
66
|
+
import { type ScanSpec, scanFiles } from "./_lib/scan-scope";
|
|
67
|
+
import {
|
|
68
|
+
classify,
|
|
69
|
+
computeClientReachablePaths,
|
|
70
|
+
findRuntimeIsolationViolations,
|
|
71
|
+
isClientEntryPath,
|
|
72
|
+
type Runtime,
|
|
73
|
+
} from "./runtime-isolation-classify";
|
|
74
|
+
|
|
75
|
+
const SCAN: ScanSpec = { scope: "source", extensions: ["ts", "tsx"] };
|
|
76
|
+
const BASELINE_FILE = ".kumiko-runtime-isolation-baseline.json";
|
|
77
|
+
|
|
78
|
+
type RawViolation = {
|
|
79
|
+
readonly file: string;
|
|
80
|
+
readonly line: number;
|
|
81
|
+
readonly fileRuntime: Runtime;
|
|
82
|
+
readonly importedSpec: string;
|
|
83
|
+
readonly importedRuntime: Runtime;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
function printViolation(v: RawViolation, root: string): void {
|
|
87
|
+
const fileRel = path.relative(root, v.file);
|
|
88
|
+
console.log(` ${fileRel}:${v.line}`);
|
|
89
|
+
console.log(` [${v.fileRuntime}] imports [${v.importedRuntime}] "${v.importedSpec}"`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function violationKey(rootAbsPath: string, v: RawViolation): string {
|
|
93
|
+
return `${path.relative(rootAbsPath, v.file)}::${v.importedSpec}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function countByKey(
|
|
97
|
+
rootAbsPath: string,
|
|
98
|
+
violations: readonly RawViolation[],
|
|
99
|
+
): Record<string, number> {
|
|
100
|
+
const counts: Record<string, number> = {};
|
|
101
|
+
for (const v of violations) {
|
|
102
|
+
const key = violationKey(rootAbsPath, v);
|
|
103
|
+
counts[key] = (counts[key] ?? 0) + 1;
|
|
104
|
+
}
|
|
105
|
+
return counts;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function ratchetFor(rootAbsPath: string) {
|
|
109
|
+
return baselineRatchet({
|
|
110
|
+
file: path.join(rootAbsPath, BASELINE_FILE),
|
|
111
|
+
formatVersion: 1,
|
|
112
|
+
unit: "runtime-isolation violation(s)",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function scanRoot(root: RepoRoot): {
|
|
117
|
+
allViolations: readonly RawViolation[];
|
|
118
|
+
outsideRoot: readonly string[];
|
|
119
|
+
scannedFiles: number;
|
|
120
|
+
} {
|
|
121
|
+
const tsConfigFilePath = frameworkTsConfigPath();
|
|
122
|
+
const project =
|
|
123
|
+
tsConfigFilePath !== undefined
|
|
124
|
+
? new Project({
|
|
125
|
+
tsConfigFilePath,
|
|
126
|
+
skipAddingFilesFromTsConfig: true,
|
|
127
|
+
skipFileDependencyResolution: true,
|
|
128
|
+
})
|
|
129
|
+
: new Project({ skipAddingFilesFromTsConfig: true, skipFileDependencyResolution: true });
|
|
130
|
+
|
|
131
|
+
const paths = scanFiles(SCAN, [root]);
|
|
132
|
+
// Exact-path lookup, never a re-glob: `project.getSourceFiles(paths)`
|
|
133
|
+
// treats each array entry as a glob pattern — pathologically slow (and,
|
|
134
|
+
// for a bracketed filename like `[id].tsx`, a broken character class)
|
|
135
|
+
// over thousands of paths. `getSourceFile(path)` is an exact lookup.
|
|
136
|
+
const scannedFiles = paths
|
|
137
|
+
.map((p) => project.getSourceFile(p) ?? project.addSourceFileAtPath(p))
|
|
138
|
+
.filter((f) => {
|
|
139
|
+
const fp = f.getFilePath();
|
|
140
|
+
return !fp.includes("/node_modules/") && !fp.includes("/dist/");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const workspaceCache = new Map<string, Runtime | null>();
|
|
144
|
+
const clientReachable = computeClientReachablePaths(scannedFiles, (sf) => {
|
|
145
|
+
const rel = path.relative(root.absPath, sf.getFilePath());
|
|
146
|
+
if (isClientEntryPath(rel)) return true;
|
|
147
|
+
return classify(sf.getFilePath(), root.absPath, workspaceCache) === "client";
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const { violations: allViolations, outsideRoot } = findRuntimeIsolationViolations(
|
|
151
|
+
scannedFiles,
|
|
152
|
+
root.absPath,
|
|
153
|
+
workspaceCache,
|
|
154
|
+
clientReachable,
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
return { allViolations, outsideRoot, scannedFiles: scannedFiles.length };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export const check: RepoCheck = {
|
|
161
|
+
name: "Runtime-Isolation Check",
|
|
162
|
+
hint:
|
|
163
|
+
"runtime may import runtime/client only; client may import client only; " +
|
|
164
|
+
"dev/tooling/test are more permissive. See runtime-isolation-classify.ts for the compat matrix. " +
|
|
165
|
+
"New, deliberate exception? `bun packages/guards/src/check-runtime-isolation.ts --write-baseline`",
|
|
166
|
+
run(roots) {
|
|
167
|
+
const root = roots[0];
|
|
168
|
+
if (!root) return { violations: [], matchedFiles: 0, notApplicable: true };
|
|
169
|
+
const rootAbsPath = root.absPath;
|
|
170
|
+
|
|
171
|
+
const { allViolations, outsideRoot, scannedFiles } = scanRoot(root);
|
|
172
|
+
for (const v of allViolations) printViolation(v, rootAbsPath);
|
|
173
|
+
|
|
174
|
+
const counts = countByKey(rootAbsPath, allViolations);
|
|
175
|
+
const resolveLine = (key: string): number => {
|
|
176
|
+
const relFile = key.split("::")[0];
|
|
177
|
+
return allViolations.find((v) => path.relative(rootAbsPath, v.file) === relFile)?.line ?? 1;
|
|
178
|
+
};
|
|
179
|
+
const violations: GuardViolation[] = ratchetFor(rootAbsPath).check(
|
|
180
|
+
counts,
|
|
181
|
+
"runtime may import runtime/client only; client may import client only — see runtime-isolation-classify.ts for the compat matrix.",
|
|
182
|
+
{
|
|
183
|
+
formatDriftRemediation:
|
|
184
|
+
"Run `bun packages/guards/src/check-runtime-isolation.ts --write-baseline` once.",
|
|
185
|
+
resolveLine,
|
|
186
|
+
},
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
if (outsideRoot.length > 0) {
|
|
190
|
+
console.log(
|
|
191
|
+
` Runtime-Isolation Check: ${outsideRoot.length} import target(s) outside the repo root (skipped).`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return { violations, matchedFiles: scannedFiles, notApplicable: false };
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
if (import.meta.main) {
|
|
200
|
+
const args = process.argv.slice(2);
|
|
201
|
+
if (args.includes("--write-baseline")) {
|
|
202
|
+
const root = resolveRepoRoots()[0];
|
|
203
|
+
if (!root) {
|
|
204
|
+
console.error("No repo root resolved — nothing to baseline.");
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
const { allViolations } = scanRoot(root);
|
|
208
|
+
for (const v of allViolations) printViolation(v, root.absPath);
|
|
209
|
+
ratchetFor(root.absPath).write(countByKey(root.absPath, allViolations));
|
|
210
|
+
process.exit(0);
|
|
211
|
+
}
|
|
212
|
+
const failed = reportResults(await runRepoChecks([check]));
|
|
213
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
214
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -1,55 +1,79 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// Consumer entry point for `bunx @cosmicdrift/kumiko-guards`. Runs all three
|
|
3
3
|
// suites (or one, via subcommand) using the exact call forms the three
|
|
4
|
-
// runners already use in their own `if (import.meta.main)` blocks
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
import { REPO_CHECKS } from "./run-repo-checks";
|
|
14
|
-
import { UI_GUARDS } from "./run-ui-guards";
|
|
15
|
-
|
|
16
|
-
const SUBCOMMANDS = ["guards", "ui", "checks"] as const;
|
|
4
|
+
// runners already use in their own `if (import.meta.main)` blocks — including
|
|
5
|
+
// flags, which each runner's own *Cli function validates and applies so this
|
|
6
|
+
// bin and the direct `bun run-*.ts` invocation can never drift apart.
|
|
7
|
+
import { buildGuardKitInventory, cliFlagsError } from "./_lib/guard-kit";
|
|
8
|
+
import { GUARD_FLAGS, GUARDS, runGuardsCli } from "./run-guards";
|
|
9
|
+
import { REPO_CHECK_FLAGS, REPO_CHECKS, runRepoChecksCli } from "./run-repo-checks";
|
|
10
|
+
import { runUiGuardsCli, UI_GUARD_FLAGS, UI_GUARDS } from "./run-ui-guards";
|
|
11
|
+
|
|
12
|
+
const SUBCOMMANDS = ["guards", "ui", "checks", "list"] as const;
|
|
17
13
|
type Subcommand = (typeof SUBCOMMANDS)[number];
|
|
18
14
|
|
|
15
|
+
const SUBCOMMAND_FLAGS: Record<Subcommand, readonly string[]> = {
|
|
16
|
+
guards: GUARD_FLAGS,
|
|
17
|
+
ui: UI_GUARD_FLAGS,
|
|
18
|
+
checks: REPO_CHECK_FLAGS,
|
|
19
|
+
list: [],
|
|
20
|
+
};
|
|
21
|
+
|
|
19
22
|
function isSubcommand(value: string): value is Subcommand {
|
|
20
23
|
return (SUBCOMMANDS as readonly string[]).includes(value);
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const project = buildSharedProject(UI_GUARDS);
|
|
31
|
-
printGuardKitBanner(UI_GUARDS.length, project);
|
|
32
|
-
return reportResults(runGuards(UI_GUARDS, project));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function runRepoChecksSuite(): Promise<number> {
|
|
36
|
-
printGuardKitBanner(REPO_CHECKS.length);
|
|
37
|
-
return reportResults(await runRepoChecks(REPO_CHECKS));
|
|
26
|
+
function printHelp(): void {
|
|
27
|
+
console.log("Usage: kumiko-guards [guards|ui|checks|list] [flags]");
|
|
28
|
+
console.log();
|
|
29
|
+
for (const sub of SUBCOMMANDS) {
|
|
30
|
+
const flags = SUBCOMMAND_FLAGS[sub];
|
|
31
|
+
console.log(` ${sub}${flags.length > 0 ? ` [${flags.join("|")}]` : ""}`);
|
|
32
|
+
}
|
|
38
33
|
}
|
|
39
34
|
|
|
40
35
|
async function main(): Promise<void> {
|
|
41
36
|
const subcommand = process.argv[2];
|
|
37
|
+
if (subcommand === "--help" || subcommand === "-h") {
|
|
38
|
+
printHelp();
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
42
41
|
if (subcommand !== undefined && !isSubcommand(subcommand)) {
|
|
43
42
|
console.error(
|
|
44
43
|
`Unknown subcommand "${subcommand}". Valid subcommands: ${SUBCOMMANDS.join(", ")}`,
|
|
45
44
|
);
|
|
46
45
|
process.exit(1);
|
|
47
46
|
}
|
|
47
|
+
const flags = process.argv.slice(3);
|
|
48
|
+
|
|
49
|
+
if (subcommand === "list") {
|
|
50
|
+
const flagsError = cliFlagsError("list", flags, SUBCOMMAND_FLAGS.list);
|
|
51
|
+
if (flagsError !== undefined) {
|
|
52
|
+
console.error(flagsError);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
// Registration inventory only — no scan, no project, no guard.run(). What
|
|
56
|
+
// CI checks against instead of running the guards: a guard dropped from a
|
|
57
|
+
// suite's array goes missing here too, not just silently from a run.
|
|
58
|
+
const inventory = buildGuardKitInventory({
|
|
59
|
+
guards: GUARDS,
|
|
60
|
+
uiGuards: UI_GUARDS,
|
|
61
|
+
checks: REPO_CHECKS,
|
|
62
|
+
});
|
|
63
|
+
console.log(JSON.stringify(inventory));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
48
66
|
|
|
49
67
|
let failed = 0;
|
|
50
|
-
if (subcommand === undefined || subcommand === "guards")
|
|
51
|
-
|
|
52
|
-
|
|
68
|
+
if (subcommand === undefined || subcommand === "guards") {
|
|
69
|
+
failed += runGuardsCli(subcommand === "guards" ? flags : []);
|
|
70
|
+
}
|
|
71
|
+
if (subcommand === undefined || subcommand === "ui") {
|
|
72
|
+
failed += runUiGuardsCli(subcommand === "ui" ? flags : []);
|
|
73
|
+
}
|
|
74
|
+
if (subcommand === undefined || subcommand === "checks") {
|
|
75
|
+
failed += await runRepoChecksCli(subcommand === "checks" ? flags : []);
|
|
76
|
+
}
|
|
53
77
|
|
|
54
78
|
process.exit(failed > 0 ? 1 : 0);
|
|
55
79
|
}
|