vouchington-tooling 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/ast-grep-examples/index.d.mts +15 -0
- package/dist/ast-grep-examples/index.mjs +138 -0
- package/dist/cli/commands/ast-grep-examples.d.mts +4 -0
- package/dist/cli/commands/ast-grep-examples.mjs +10 -0
- package/dist/cli/commands/gha-workspace-policy.d.mts +12 -0
- package/dist/cli/commands/gha-workspace-policy.mjs +18 -0
- package/dist/cli/commands/require-up-to-date.d.mts +4 -0
- package/dist/cli/commands/require-up-to-date.mjs +11 -0
- package/dist/cli/index.mjs +15 -0
- package/dist/cli/parse-options.d.mts +8 -0
- package/dist/cli/parse-options.mjs +142 -0
- package/dist/cli/parse.d.mts +17 -0
- package/dist/cli/parse.mjs +9 -86
- package/dist/cli/usage.d.mts +1 -1
- package/dist/cli/usage.mjs +9 -0
- package/dist/gitleaks-directory-scan/index.d.mts +8 -0
- package/dist/gitleaks-directory-scan/index.mjs +30 -0
- package/dist/index.d.mts +6 -0
- package/dist/index.mjs +3 -0
- package/dist/pnpm-install/native-health.d.mts +3 -1
- package/dist/pnpm-install/native-health.mjs +48 -6
- package/dist/require-up-to-date/index.d.mts +8 -0
- package/dist/require-up-to-date/index.mjs +31 -0
- package/package.json +16 -1
- package/scripts/gha/diagnose-port-collision.sh +6 -2
- package/scripts/gitleaks-directory-scan.sh +49 -0
- package/skills/dotnet-test-authoring/SKILL.md +5 -0
- package/skills/manifest.json +18 -18
- package/skills/playwright-authoring/SKILL.md +3 -0
- package/skills/retrospective/SKILL.md +6 -1
- package/skills/storybook-authoring/SKILL.md +3 -0
- package/skills/swift-test-authoring/SKILL.md +3 -0
- package/skills/test-authoring/SKILL.md +3 -0
- package/skills/test-authoring/references/core-practice.md +2 -1
- package/skills/test-authoring/references/tautological-tests.md +46 -0
package/README.md
CHANGED
|
@@ -19,6 +19,10 @@ vouchington runner-port-policy --file ./policy.json
|
|
|
19
19
|
vouchington runner-port-policy --reserved 2200
|
|
20
20
|
vouchington with-host-lock --name expensive-build --timeout-seconds 60 -- make build
|
|
21
21
|
vouchington gha-runtime-audit --pr-workflow CI --push-workflow '/^Main CI \\(.+\\)$/'
|
|
22
|
+
vouchington require-up-to-date --remote origin --branch main
|
|
23
|
+
vouchington gitleaks-directory-scan --config .gitleaks.toml
|
|
24
|
+
vouchington ast-grep-examples --rules ast-grep-tests --config sgconfig.yml
|
|
25
|
+
vouchington gha-workspace-policy
|
|
22
26
|
vouchington gha-output name
|
|
23
27
|
vouchington gha-needs-results
|
|
24
28
|
vouchington download-with-diagnostics <url> <destination>
|
|
@@ -79,6 +83,14 @@ download`), and extracts each selected name into its own directory. Ordinary abs
|
|
|
79
83
|
`availability=unavailable`. Artifact listing retries up to three times with bounded backoff;
|
|
80
84
|
exhausted transport errors, invalid names, and cancellation remain hard failures.
|
|
81
85
|
|
|
86
|
+
`require-up-to-date` fetches the requested remote branch and fails unless its fetched tip is an
|
|
87
|
+
ancestor of `HEAD`. `gitleaks-directory-scan` builds and scans isolated staged-index and current
|
|
88
|
+
nonignored-working-tree mirrors with an explicit config; `--directory` selects the repository root.
|
|
89
|
+
`ast-grep-examples` runs native `ast-grep test`, then validates each scoped rule's `files:` and
|
|
90
|
+
`ignores:` examples with project `languageGlobs` replay from its root `--config`.
|
|
91
|
+
`gha-workspace-policy` checks tracked workflow and composite-action files in the current repository;
|
|
92
|
+
pass `--root`, `--workflow-directory`, or `--action-directory` for consumer-owned layouts.
|
|
93
|
+
|
|
82
94
|
`retrospective-transcript` discovers Codex and Claude transcripts by default. It also reads a
|
|
83
95
|
Claude-compatible transcript when `CURSOR_SESSION_ID` is set, and Grok's `updates.jsonl` session
|
|
84
96
|
layout when `GROK_SESSION_ID` is set. Use `--grok-sessions-dir` to point discovery at a nondefault
|
|
@@ -177,6 +189,9 @@ import { rateLimitDelay } from 'vouchington-tooling/gha-rate-limit'
|
|
|
177
189
|
import { parseCheckpoint } from 'vouchington-tooling/gha-pr-checkpoint'
|
|
178
190
|
import { checkWorkspaceGatesPolicy } from 'vouchington-tooling/workspace-gates'
|
|
179
191
|
import { checkGhaWorkspacePolicy } from 'vouchington-tooling/gha-workspace-policy'
|
|
192
|
+
import { requireUpToDate } from 'vouchington-tooling/require-up-to-date'
|
|
193
|
+
import { runGitleaksDirectoryScan } from 'vouchington-tooling/gitleaks-directory-scan'
|
|
194
|
+
import { runAstGrepExamples } from 'vouchington-tooling/ast-grep-examples'
|
|
180
195
|
import { validateNugetUpdate } from 'vouchington-tooling/nuget-central-version'
|
|
181
196
|
import { normalizeSwiftSource } from 'vouchington-tooling/swift-semantic-equal'
|
|
182
197
|
import { parseUniqueSwiftBinaryTargetChecksum } from 'vouchington-tooling/swift-source-offset'
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface AstGrepResult {
|
|
2
|
+
status: number | null;
|
|
3
|
+
stdout?: string;
|
|
4
|
+
stderr?: string;
|
|
5
|
+
}
|
|
6
|
+
export type AstGrepExamplesExecutor = (args: readonly string[], cwd: string) => AstGrepResult;
|
|
7
|
+
export interface AstGrepExamplesOptions {
|
|
8
|
+
readonly rules: string;
|
|
9
|
+
readonly config: string;
|
|
10
|
+
readonly executable?: string;
|
|
11
|
+
readonly execute?: AstGrepExamplesExecutor;
|
|
12
|
+
}
|
|
13
|
+
export declare function astGrepExamplesArguments(options: AstGrepExamplesOptions): string[];
|
|
14
|
+
export declare function runAstGrepExamples(options: AstGrepExamplesOptions): number;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
5
|
+
import picomatch from 'picomatch';
|
|
6
|
+
import { parse as yamlLoad, stringify as yamlDump } from 'yaml';
|
|
7
|
+
export function astGrepExamplesArguments(options) {
|
|
8
|
+
return [
|
|
9
|
+
'test',
|
|
10
|
+
'--test-dir',
|
|
11
|
+
needPath(options.rules, '--rules'),
|
|
12
|
+
'--config',
|
|
13
|
+
needPath(options.config, '--config'),
|
|
14
|
+
];
|
|
15
|
+
}
|
|
16
|
+
function needPath(value, option) {
|
|
17
|
+
if (!value)
|
|
18
|
+
throw new Error(`${option} requires a path`);
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
function eligible(rule, file) {
|
|
22
|
+
const path = file.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
23
|
+
return ((!rule.files?.length || picomatch(rule.files)(path)) &&
|
|
24
|
+
!(rule.ignores?.length && picomatch(rule.ignores)(path)));
|
|
25
|
+
}
|
|
26
|
+
function loadRules(directory) {
|
|
27
|
+
return fs.readdirSync(directory, { recursive: true })
|
|
28
|
+
.filter((file) => /\.ya?ml$/u.test(file))
|
|
29
|
+
.toSorted()
|
|
30
|
+
.map((file) => {
|
|
31
|
+
const ruleFile = join(directory, file);
|
|
32
|
+
return { ruleFile, rule: yamlLoad(fs.readFileSync(ruleFile, 'utf8')) };
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function assertExample(rule, example) {
|
|
36
|
+
if (!example.code || !example.file)
|
|
37
|
+
throw new Error(`${rule.id}: examples require non-empty code and file`);
|
|
38
|
+
safeRelativePath(example.file, `${rule.id}: example.file`);
|
|
39
|
+
}
|
|
40
|
+
function safeRelativePath(value, label) {
|
|
41
|
+
if (value.includes('\0') ||
|
|
42
|
+
isAbsolute(value) ||
|
|
43
|
+
value
|
|
44
|
+
.replace(/\\/g, '/')
|
|
45
|
+
.split('/')
|
|
46
|
+
.some((component) => component === '..'))
|
|
47
|
+
throw new Error(`${label} must stay within the temporary rule directory`);
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
function defaultExecute(executable) {
|
|
51
|
+
return (args, cwd) => {
|
|
52
|
+
const result = spawnSync(executable, args, { cwd, encoding: 'utf8' });
|
|
53
|
+
if (result.error)
|
|
54
|
+
throw new Error(`ast-grep failed to spawn: ${result.error.message}`);
|
|
55
|
+
return result;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function writeNativeTests(rules, testDir, semanticDir) {
|
|
59
|
+
for (const { rule } of rules) {
|
|
60
|
+
if (!/^[A-Za-z0-9_.-]+$/u.test(rule.id))
|
|
61
|
+
throw new Error('rule id must be a filesystem-safe name');
|
|
62
|
+
const { examples } = rule;
|
|
63
|
+
const invalid = [
|
|
64
|
+
...new Set(examples.filter((example) => !example.isValid).map((example) => example.code)),
|
|
65
|
+
];
|
|
66
|
+
const valid = [
|
|
67
|
+
...new Set(examples
|
|
68
|
+
.filter((example) => example.isValid && eligible(rule, example.file) && !invalid.includes(example.code))
|
|
69
|
+
.map((example) => example.code)),
|
|
70
|
+
];
|
|
71
|
+
fs.writeFileSync(join(testDir, `${rule.id}.yml`), yamlDump({ id: rule.id, valid, invalid }));
|
|
72
|
+
const { examples: _examples, files: _files, ignores: _ignores, ...semanticRule } = rule;
|
|
73
|
+
fs.writeFileSync(join(semanticDir, `${rule.id}.yml`), yamlDump(semanticRule));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function scanScopedRule(loaded, root, languageGlobs, execute) {
|
|
77
|
+
const { invalid, rule } = loaded;
|
|
78
|
+
const expected = new Map();
|
|
79
|
+
for (const example of rule.examples) {
|
|
80
|
+
assertExample(rule, example);
|
|
81
|
+
const file = example.file.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
82
|
+
expected.set(file, eligible(rule, file));
|
|
83
|
+
const path = join(root, file);
|
|
84
|
+
fs.mkdirSync(dirname(path), { recursive: true });
|
|
85
|
+
fs.writeFileSync(path, invalid);
|
|
86
|
+
}
|
|
87
|
+
fs.copyFileSync(loaded.ruleFile, join(root, 'rule.yml'));
|
|
88
|
+
fs.writeFileSync(join(root, 'sgconfig.yml'), yamlDump({ languageGlobs }));
|
|
89
|
+
const result = execute(['scan', '--rule', 'rule.yml', '--config', 'sgconfig.yml', '--json', '--no-ignore', '--hidden'], root);
|
|
90
|
+
if (result.status === null || result.status > 1)
|
|
91
|
+
throw new Error(`ast-grep scan failed (exit ${result.status ?? 'signal'}): ${result.stderr ?? ''}`);
|
|
92
|
+
const found = new Set(result.stdout?.trim()
|
|
93
|
+
? JSON.parse(result.stdout).map((f) => f.file.replace(/\\/g, '/').replace(/^\.\//, ''))
|
|
94
|
+
: []);
|
|
95
|
+
for (const [file, shouldFind] of expected)
|
|
96
|
+
if (found.has(file) !== shouldFind)
|
|
97
|
+
throw new Error(`${rule.id}: expected ${file} ${shouldFind ? 'to produce' : 'not to produce'} a finding`);
|
|
98
|
+
}
|
|
99
|
+
export function runAstGrepExamples(options) {
|
|
100
|
+
const rulesDirectory = resolve(needPath(options.rules, '--rules'));
|
|
101
|
+
const config = resolve(needPath(options.config, '--config'));
|
|
102
|
+
const languageGlobs = yamlLoad(fs.readFileSync(config, 'utf8')).languageGlobs;
|
|
103
|
+
if (!languageGlobs)
|
|
104
|
+
throw new Error(`${config}: missing languageGlobs`);
|
|
105
|
+
const rules = loadRules(rulesDirectory).map((loaded) => {
|
|
106
|
+
const { rule } = loaded;
|
|
107
|
+
const examples = rule.examples ?? [];
|
|
108
|
+
for (const example of examples)
|
|
109
|
+
assertExample(rule, example);
|
|
110
|
+
const invalid = examples.find((example) => !example.isValid);
|
|
111
|
+
if (!examples.some((example) => example.isValid) || invalid === undefined)
|
|
112
|
+
throw new Error(`${rule.id}: examples require valid and invalid cases`);
|
|
113
|
+
return { ...loaded, rule: { ...rule, examples }, invalid: invalid.code };
|
|
114
|
+
});
|
|
115
|
+
const execute = options.execute ?? defaultExecute(options.executable ?? 'ast-grep');
|
|
116
|
+
const root = fs.mkdtempSync(join(tmpdir(), 'ast-grep-examples-'));
|
|
117
|
+
try {
|
|
118
|
+
const tests = join(root, 'native-tests');
|
|
119
|
+
const semantic = join(root, 'semantic-rules');
|
|
120
|
+
fs.mkdirSync(tests);
|
|
121
|
+
fs.mkdirSync(semantic);
|
|
122
|
+
writeNativeTests(rules, tests, semantic);
|
|
123
|
+
const nativeConfig = join(root, 'sgconfig.yml');
|
|
124
|
+
fs.writeFileSync(nativeConfig, yamlDump({ ruleDirs: [semantic] }));
|
|
125
|
+
const native = execute(['test', '--config', nativeConfig, '--test-dir', tests, '--skip-snapshot-tests'], process.cwd());
|
|
126
|
+
if (native.status !== 0)
|
|
127
|
+
throw new Error(`${native.stdout ?? ''}\n${native.stderr ?? ''}`);
|
|
128
|
+
for (const loaded of rules.filter(({ rule }) => rule.files?.length || rule.ignores?.length)) {
|
|
129
|
+
const scopedRoot = join(root, 'path-scans', loaded.rule.id);
|
|
130
|
+
fs.mkdirSync(scopedRoot, { recursive: true });
|
|
131
|
+
scanScopedRule(loaded, scopedRoot, languageGlobs, execute);
|
|
132
|
+
}
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
fs.rmSync(root, { force: true, recursive: true });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { runAstGrepExamples } from '../../ast-grep-examples/index.mjs';
|
|
2
|
+
export function runAstGrepExamplesCommand(options) {
|
|
3
|
+
try {
|
|
4
|
+
return runAstGrepExamples(options);
|
|
5
|
+
}
|
|
6
|
+
catch (error) {
|
|
7
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
8
|
+
return 1;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { checkGhaWorkspacePolicy, type GhaWorkspacePolicyOptions } from '../../gha-workspace-policy/index.mts';
|
|
2
|
+
import { type SharedContext } from '../../shared-context/index.mts';
|
|
3
|
+
export interface GhaWorkspacePolicyCliOptions extends GhaWorkspacePolicyOptions {
|
|
4
|
+
readonly root?: string;
|
|
5
|
+
}
|
|
6
|
+
interface Dependencies {
|
|
7
|
+
readonly buildContext?: (root: string) => Promise<SharedContext>;
|
|
8
|
+
readonly check?: typeof checkGhaWorkspacePolicy;
|
|
9
|
+
readonly stderr?: NodeJS.WritableStream;
|
|
10
|
+
}
|
|
11
|
+
export declare function runGhaWorkspacePolicy(options: GhaWorkspacePolicyCliOptions, dependencies?: Dependencies): Promise<number>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { checkGhaWorkspacePolicy, } from '../../gha-workspace-policy/index.mjs';
|
|
2
|
+
import { buildSharedContext } from '../../shared-context/index.mjs';
|
|
3
|
+
export async function runGhaWorkspacePolicy(options, dependencies = {}) {
|
|
4
|
+
const context = await (dependencies.buildContext ?? buildSharedContext)(options.root ?? process.cwd());
|
|
5
|
+
const result = await (dependencies.check ?? checkGhaWorkspacePolicy)(context, {
|
|
6
|
+
...(options.workflowDirectories === undefined
|
|
7
|
+
? {}
|
|
8
|
+
: { workflowDirectories: options.workflowDirectories }),
|
|
9
|
+
...(options.actionDirectories === undefined
|
|
10
|
+
? {}
|
|
11
|
+
: { actionDirectories: options.actionDirectories }),
|
|
12
|
+
});
|
|
13
|
+
if (result.errors.length === 0)
|
|
14
|
+
return 0;
|
|
15
|
+
const stderr = dependencies.stderr ?? process.stderr;
|
|
16
|
+
stderr.write(`${result.errors.join('\n')}\n`);
|
|
17
|
+
return 1;
|
|
18
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { requireUpToDate } from '../../require-up-to-date/index.mjs';
|
|
2
|
+
export function runRequireUpToDate(options) {
|
|
3
|
+
try {
|
|
4
|
+
requireUpToDate(options);
|
|
5
|
+
return 0;
|
|
6
|
+
}
|
|
7
|
+
catch (error) {
|
|
8
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
9
|
+
return 1;
|
|
10
|
+
}
|
|
11
|
+
}
|
package/dist/cli/index.mjs
CHANGED
|
@@ -21,6 +21,9 @@ import { runLinkSkill } from './commands/link-skill.mjs';
|
|
|
21
21
|
import { runRetrospectiveFactsCommand } from './commands/retrospective-facts.mjs';
|
|
22
22
|
import { runAgentBlackboardCommand } from './commands/agent-blackboard.mjs';
|
|
23
23
|
import { runWithHostLock } from './commands/with-host-lock.mjs';
|
|
24
|
+
import { runRequireUpToDate } from './commands/require-up-to-date.mjs';
|
|
25
|
+
import { runAstGrepExamplesCommand } from './commands/ast-grep-examples.mjs';
|
|
26
|
+
import { runGhaWorkspacePolicy } from './commands/gha-workspace-policy.mjs';
|
|
24
27
|
import { parseCli } from './parse.mjs';
|
|
25
28
|
import { packageScriptPath } from './script-path.mjs';
|
|
26
29
|
import { printUsage } from './usage.mjs';
|
|
@@ -119,6 +122,18 @@ export function runCli(argv = process.argv) {
|
|
|
119
122
|
return runRetrospectiveFactsCommand(parsed.args);
|
|
120
123
|
case 'agent-blackboard':
|
|
121
124
|
return runAgentBlackboardCommand(parsed.args);
|
|
125
|
+
case 'require-up-to-date':
|
|
126
|
+
return runRequireUpToDate(parsed);
|
|
127
|
+
case 'gitleaks-directory-scan':
|
|
128
|
+
return runScript('bash', packageScriptPath('scripts/gitleaks-directory-scan.sh'), [
|
|
129
|
+
'--config',
|
|
130
|
+
parsed.config,
|
|
131
|
+
...(parsed.directory === undefined ? [] : ['--root', parsed.directory]),
|
|
132
|
+
]);
|
|
133
|
+
case 'ast-grep-examples':
|
|
134
|
+
return runAstGrepExamplesCommand(parsed);
|
|
135
|
+
case 'gha-workspace-policy':
|
|
136
|
+
return runGhaWorkspacePolicy(parsed);
|
|
122
137
|
}
|
|
123
138
|
}
|
|
124
139
|
function readInstalledVersion() {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ParsedCli } from './parse.mts';
|
|
2
|
+
export declare function parseLinkSkill(args: readonly string[]): ParsedCli;
|
|
3
|
+
export declare function parseRunnerPortPolicy(args: readonly string[]): ParsedCli;
|
|
4
|
+
export declare function parseHttpOrigin(args: readonly string[]): ParsedCli;
|
|
5
|
+
export declare function parseRequireUpToDate(args: readonly string[]): ParsedCli;
|
|
6
|
+
export declare function parseGitleaksDirectoryScan(args: readonly string[]): ParsedCli;
|
|
7
|
+
export declare function parseAstGrepExamples(args: readonly string[]): ParsedCli;
|
|
8
|
+
export declare function parseGhaWorkspacePolicy(args: readonly string[]): ParsedCli;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
export function parseLinkSkill(args) {
|
|
2
|
+
const [name, ...flags] = args;
|
|
3
|
+
if (name === undefined || name.startsWith('-'))
|
|
4
|
+
return { kind: 'error', message: 'link-skill requires a skill name' };
|
|
5
|
+
const options = parseOptions(flags, ['source-root', 'target-root'], 'link-skill', 'path');
|
|
6
|
+
if ('kind' in options)
|
|
7
|
+
return options;
|
|
8
|
+
const sourceRoot = options.values['source-root'];
|
|
9
|
+
const targetRoot = options.values['target-root'];
|
|
10
|
+
if (sourceRoot === undefined || targetRoot === undefined) {
|
|
11
|
+
return { kind: 'error', message: 'link-skill requires --source-root and --target-root' };
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
kind: 'link-skill',
|
|
15
|
+
name,
|
|
16
|
+
sourceRoot,
|
|
17
|
+
targetRoot,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function parseRunnerPortPolicy(args) {
|
|
21
|
+
const options = parseOptions(args, ['file', 'reserved'], 'runner-port-policy', (flag) => flag === '--reserved' ? 'port' : 'path');
|
|
22
|
+
if ('kind' in options)
|
|
23
|
+
return options;
|
|
24
|
+
const file = options.values.file;
|
|
25
|
+
const reservedValue = options.values.reserved;
|
|
26
|
+
const reserved = reservedValue === undefined ? undefined : Number(reservedValue);
|
|
27
|
+
if (reservedValue !== undefined && !Number.isInteger(reserved)) {
|
|
28
|
+
return { kind: 'error', message: '--reserved must be an integer' };
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
kind: 'runner-port-policy',
|
|
32
|
+
...(file === undefined ? {} : { file }),
|
|
33
|
+
...(reserved === undefined ? {} : { reserved }),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function parseHttpOrigin(args) {
|
|
37
|
+
let field = 'origin';
|
|
38
|
+
const values = [];
|
|
39
|
+
let index = 0;
|
|
40
|
+
while (index < args.length) {
|
|
41
|
+
const flag = args[index];
|
|
42
|
+
index += 1;
|
|
43
|
+
if (flag === '--help' || flag === '-h')
|
|
44
|
+
return { kind: 'help' };
|
|
45
|
+
if (flag === '--field') {
|
|
46
|
+
const value = args[index];
|
|
47
|
+
if (value === undefined)
|
|
48
|
+
return { kind: 'error', message: '--field requires a name' };
|
|
49
|
+
field = value;
|
|
50
|
+
index += 1;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (flag === '--') {
|
|
54
|
+
values.push(...args.slice(index));
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
if (flag.startsWith('-'))
|
|
58
|
+
return { kind: 'error', message: `unknown http-origin option: ${flag}` };
|
|
59
|
+
values.push(flag);
|
|
60
|
+
}
|
|
61
|
+
if (values.length > 1)
|
|
62
|
+
return { kind: 'error', message: 'http-origin accepts at most one value' };
|
|
63
|
+
return { kind: 'http-origin', field, value: values[0] ?? '' };
|
|
64
|
+
}
|
|
65
|
+
export function parseRequireUpToDate(args) {
|
|
66
|
+
const options = parseOptions(args, ['remote', 'branch'], 'require-up-to-date', 'name');
|
|
67
|
+
if ('kind' in options)
|
|
68
|
+
return options;
|
|
69
|
+
const remote = options.values.remote;
|
|
70
|
+
const branch = options.values.branch;
|
|
71
|
+
if (remote === undefined)
|
|
72
|
+
return { kind: 'error', message: '--remote requires a name' };
|
|
73
|
+
if (branch === undefined)
|
|
74
|
+
return { kind: 'error', message: '--branch requires a name' };
|
|
75
|
+
return { kind: 'require-up-to-date', remote, branch };
|
|
76
|
+
}
|
|
77
|
+
export function parseGitleaksDirectoryScan(args) {
|
|
78
|
+
const options = parseOptions(args, ['config', 'directory'], 'gitleaks-directory-scan', 'path');
|
|
79
|
+
if ('kind' in options)
|
|
80
|
+
return options;
|
|
81
|
+
const config = options.values.config;
|
|
82
|
+
const directory = options.values.directory;
|
|
83
|
+
if (config === undefined)
|
|
84
|
+
return { kind: 'error', message: '--config requires a path' };
|
|
85
|
+
return {
|
|
86
|
+
kind: 'gitleaks-directory-scan',
|
|
87
|
+
config,
|
|
88
|
+
...(directory === undefined ? {} : { directory }),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
export function parseAstGrepExamples(args) {
|
|
92
|
+
const options = parseOptions(args, ['rules', 'config'], 'ast-grep-examples', 'path');
|
|
93
|
+
if ('kind' in options)
|
|
94
|
+
return options;
|
|
95
|
+
const rules = options.values.rules;
|
|
96
|
+
const config = options.values.config;
|
|
97
|
+
if (rules === undefined)
|
|
98
|
+
return { kind: 'error', message: '--rules requires a path' };
|
|
99
|
+
if (config === undefined)
|
|
100
|
+
return { kind: 'error', message: '--config requires a path' };
|
|
101
|
+
return { kind: 'ast-grep-examples', rules, config };
|
|
102
|
+
}
|
|
103
|
+
export function parseGhaWorkspacePolicy(args) {
|
|
104
|
+
const options = parseOptions(args, ['root', 'workflow-directory', 'action-directory'], 'gha-workspace-policy', 'path');
|
|
105
|
+
if ('kind' in options)
|
|
106
|
+
return options;
|
|
107
|
+
const root = options.values.root;
|
|
108
|
+
return {
|
|
109
|
+
kind: 'gha-workspace-policy',
|
|
110
|
+
...(root === undefined ? {} : { root }),
|
|
111
|
+
...(options.workflowDirectories.length === 0
|
|
112
|
+
? {}
|
|
113
|
+
: { workflowDirectories: options.workflowDirectories }),
|
|
114
|
+
...(options.actionDirectories.length === 0
|
|
115
|
+
? {}
|
|
116
|
+
: { actionDirectories: options.actionDirectories }),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function parseOptions(args, allowed, command, missingValue) {
|
|
120
|
+
const result = { values: {}, workflowDirectories: [], actionDirectories: [] };
|
|
121
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
122
|
+
const flag = args[index];
|
|
123
|
+
if (flag === '--help' || flag === '-h')
|
|
124
|
+
return { kind: 'help' };
|
|
125
|
+
if (!flag.startsWith('--') || !allowed.includes(flag.slice(2))) {
|
|
126
|
+
return { kind: 'error', message: `unknown ${command} option: ${flag}` };
|
|
127
|
+
}
|
|
128
|
+
const value = args[index + 1];
|
|
129
|
+
if (value === undefined) {
|
|
130
|
+
const noun = typeof missingValue === 'string' ? missingValue : missingValue(flag);
|
|
131
|
+
return { kind: 'error', message: `${flag} requires a ${noun}` };
|
|
132
|
+
}
|
|
133
|
+
if (flag === '--workflow-directory')
|
|
134
|
+
result.workflowDirectories.push(value);
|
|
135
|
+
else if (flag === '--action-directory')
|
|
136
|
+
result.actionDirectories.push(value);
|
|
137
|
+
else
|
|
138
|
+
result.values[flag.slice(2)] = value;
|
|
139
|
+
index += 1;
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
}
|
package/dist/cli/parse.d.mts
CHANGED
|
@@ -60,6 +60,23 @@ export type ParsedCli = {
|
|
|
60
60
|
} | {
|
|
61
61
|
kind: 'agent-blackboard';
|
|
62
62
|
args: string[];
|
|
63
|
+
} | {
|
|
64
|
+
kind: 'require-up-to-date';
|
|
65
|
+
remote: string;
|
|
66
|
+
branch: string;
|
|
67
|
+
} | {
|
|
68
|
+
kind: 'gitleaks-directory-scan';
|
|
69
|
+
config: string;
|
|
70
|
+
directory?: string;
|
|
71
|
+
} | {
|
|
72
|
+
kind: 'ast-grep-examples';
|
|
73
|
+
rules: string;
|
|
74
|
+
config: string;
|
|
75
|
+
} | {
|
|
76
|
+
kind: 'gha-workspace-policy';
|
|
77
|
+
root?: string;
|
|
78
|
+
workflowDirectories?: string[];
|
|
79
|
+
actionDirectories?: string[];
|
|
63
80
|
} | ParsedGhaRuntimeAudit | ParsedGhaArtifactsCleanup;
|
|
64
81
|
export type ScriptCommand = 'gha-output' | 'gha-needs-results' | 'download-with-diagnostics' | 'download-optional-run-artifacts' | 'host-pressure-diagnostics' | 'allocate-browser-safe-ports' | 'diagnose-port-collision' | 'prepare-trivy-db' | 'check-cache-size' | 'make-shard-matrix' | 'load-runner-env' | 'clean-workspace' | 'install-github-release' | 'run-with-timeout' | 'lint-links' | 'materialize-pr-context' | 'wait-for-apt-locks' | 'install-playwright-chromium-arm64' | 'ghcr-package-retention' | 'harness-admission-lane' | 'harness-assert-gates';
|
|
65
82
|
export declare function parseCli(argv: readonly string[]): ParsedCli;
|
package/dist/cli/parse.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parseGhaArtifactsCleanup, } from './parse-gha-artifacts-cleanup.mjs';
|
|
2
2
|
import { parseGhaRuntimeAudit } from './parse-gha-runtime-audit.mjs';
|
|
3
|
+
import { parseAstGrepExamples, parseGhaWorkspacePolicy, parseGitleaksDirectoryScan, parseHttpOrigin, parseLinkSkill, parseRequireUpToDate, parseRunnerPortPolicy, } from './parse-options.mjs';
|
|
3
4
|
const SCRIPT_COMMANDS = new Set([
|
|
4
5
|
'gha-output',
|
|
5
6
|
'gha-needs-results',
|
|
@@ -62,6 +63,14 @@ export function parseCli(argv) {
|
|
|
62
63
|
return { kind: 'retrospective-facts', args: rest };
|
|
63
64
|
if (command === 'agent-blackboard')
|
|
64
65
|
return { kind: 'agent-blackboard', args: rest };
|
|
66
|
+
if (command === 'require-up-to-date')
|
|
67
|
+
return parseRequireUpToDate(rest);
|
|
68
|
+
if (command === 'gitleaks-directory-scan')
|
|
69
|
+
return parseGitleaksDirectoryScan(rest);
|
|
70
|
+
if (command === 'ast-grep-examples')
|
|
71
|
+
return parseAstGrepExamples(rest);
|
|
72
|
+
if (command === 'gha-workspace-policy')
|
|
73
|
+
return parseGhaWorkspacePolicy(rest);
|
|
65
74
|
if (command === 'gha-artifacts-cleanup')
|
|
66
75
|
return parseGhaArtifactsCleanup(rest);
|
|
67
76
|
if (command !== undefined && SCRIPT_COMMANDS.has(command)) {
|
|
@@ -69,89 +78,3 @@ export function parseCli(argv) {
|
|
|
69
78
|
}
|
|
70
79
|
return { kind: 'error', message: `unknown command: ${command}` };
|
|
71
80
|
}
|
|
72
|
-
function parseLinkSkill(args) {
|
|
73
|
-
const [name, ...flags] = args;
|
|
74
|
-
if (name === undefined || name.startsWith('-'))
|
|
75
|
-
return { kind: 'error', message: 'link-skill requires a skill name' };
|
|
76
|
-
let sourceRoot;
|
|
77
|
-
let targetRoot;
|
|
78
|
-
for (let index = 0; index < flags.length; index += 1) {
|
|
79
|
-
const flag = flags[index];
|
|
80
|
-
const value = flags[index + 1];
|
|
81
|
-
if (flag !== '--source-root' && flag !== '--target-root')
|
|
82
|
-
return { kind: 'error', message: `unknown link-skill option: ${flag}` };
|
|
83
|
-
if (value === undefined)
|
|
84
|
-
return { kind: 'error', message: `${flag} requires a path` };
|
|
85
|
-
if (flag === '--source-root')
|
|
86
|
-
sourceRoot = value;
|
|
87
|
-
else
|
|
88
|
-
targetRoot = value;
|
|
89
|
-
index += 1;
|
|
90
|
-
}
|
|
91
|
-
if (sourceRoot === undefined || targetRoot === undefined)
|
|
92
|
-
return { kind: 'error', message: 'link-skill requires --source-root and --target-root' };
|
|
93
|
-
return { kind: 'link-skill', name, sourceRoot, targetRoot };
|
|
94
|
-
}
|
|
95
|
-
function parseRunnerPortPolicy(args) {
|
|
96
|
-
let file;
|
|
97
|
-
let reserved;
|
|
98
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
99
|
-
const flag = args[index];
|
|
100
|
-
if (flag === '--file') {
|
|
101
|
-
const value = args[index + 1];
|
|
102
|
-
if (value === undefined)
|
|
103
|
-
return { kind: 'error', message: '--file requires a path' };
|
|
104
|
-
file = value;
|
|
105
|
-
index += 1;
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
if (flag === '--reserved') {
|
|
109
|
-
const value = args[index + 1];
|
|
110
|
-
if (value === undefined)
|
|
111
|
-
return { kind: 'error', message: '--reserved requires a port' };
|
|
112
|
-
const port = Number(value);
|
|
113
|
-
if (!Number.isInteger(port))
|
|
114
|
-
return { kind: 'error', message: '--reserved must be an integer' };
|
|
115
|
-
reserved = port;
|
|
116
|
-
index += 1;
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
if (flag === '--help' || flag === '-h')
|
|
120
|
-
return { kind: 'help' };
|
|
121
|
-
return { kind: 'error', message: `unknown runner-port-policy option: ${flag}` };
|
|
122
|
-
}
|
|
123
|
-
return {
|
|
124
|
-
kind: 'runner-port-policy',
|
|
125
|
-
...(file === undefined ? {} : { file }),
|
|
126
|
-
...(reserved === undefined ? {} : { reserved }),
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
function parseHttpOrigin(args) {
|
|
130
|
-
let field = 'origin';
|
|
131
|
-
const values = [];
|
|
132
|
-
let index = 0;
|
|
133
|
-
while (index < args.length) {
|
|
134
|
-
const flag = args[index];
|
|
135
|
-
index += 1;
|
|
136
|
-
if (flag === '--help' || flag === '-h')
|
|
137
|
-
return { kind: 'help' };
|
|
138
|
-
if (flag === '--field') {
|
|
139
|
-
const value = args[index];
|
|
140
|
-
if (value === undefined)
|
|
141
|
-
return { kind: 'error', message: '--field requires a name' };
|
|
142
|
-
field = value;
|
|
143
|
-
index += 1;
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
if (flag === '--') {
|
|
147
|
-
values.push(...args.slice(index));
|
|
148
|
-
break;
|
|
149
|
-
}
|
|
150
|
-
if (flag.startsWith('-'))
|
|
151
|
-
return { kind: 'error', message: `unknown http-origin option: ${flag}` };
|
|
152
|
-
values.push(flag);
|
|
153
|
-
}
|
|
154
|
-
if (values.length > 1)
|
|
155
|
-
return { kind: 'error', message: 'http-origin accepts at most one value' };
|
|
156
|
-
return { kind: 'http-origin', field, value: values[0] ?? '' };
|
|
157
|
-
}
|
package/dist/cli/usage.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const USAGE = "Usage: vouchington <command> [options]\n\nCommands:\n runner-port-policy Print or validate a runner port policy\n with-host-lock Run a command under a host-wide lock\n gha-runtime-audit Audit successful GitHub Actions job runtimes\n gha-output Write a collision-safe multiline GITHUB_OUTPUT record\n gha-needs-results Fail if required GitHub Actions job results failed\n download-with-diagnostics Download a URL and report HTTP status on failure\n download-optional-run-artifacts Download optional artifacts from the current run\n host-pressure-diagnostics Print a bounded host memory/OOM/PSI snapshot\n allocate-browser-safe-ports Allocate Fetch-safe localhost ports\n diagnose-port-collision Capture bounded localhost port diagnostics\n prepare-trivy-db Download the Trivy vulnerability database\n gha-artifacts-cleanup Delete classified GitHub Actions artifacts\n http-origin Validate an optional HTTP(S) origin\n vitest-blob-manifest Stamp a vitest-blob-manifest:v1 identity file\n vitest-report-attempt Write or read a Vitest report-attempt marker\n prepare-vitest-reports Validate and select Vitest report JSON files\n pnpm-install Install a pnpm workspace with retry and release-age fail-fast\n check-cache-size Measure a path and decide whether to save a GHA cache\n make-shard-matrix Emit a [1..N] GitHub Actions shard matrix\n load-runner-env Overlay a runner env file onto GITHUB_ENV with injection guards\n clean-workspace Reset a persistent-runner workspace with a fork-PR trust gate\n install-github-release Download a checksum-verified GitHub Release binary\n run-with-timeout Run a command with GNU timeout or a Perl fallback\n lint-links Two-pass lychee: internal links fail, external warn\n materialize-pr-context Dump PR title/body/files/diff/comments and #N crawl\n wait-for-apt-locks Wait until apt/dpkg lock files are free\n install-playwright-chromium-arm64 Install Playwright Chromium from browsers.json\n ghcr-package-retention Delete old GHCR package versions past KEEP_MIN\n harness-admission-lane Compute a GITHUB_RUN_ID admission lane for fleet fan-out\n harness-assert-gates Fail if any named HARNESS_*_ENABLED gate is enabled\n nuget-central-version Validate a Directory.Packages.props PackageVersion delta\n swift-semantic-equal Compare Swift sources ignoring comments and whitespace\n post-review Post one COMMENT review from a staged payload file\n stage-review-payload Validate a review payload file into a staging directory\n retrospective-transcript Format facts from Claude-compatible, Codex, or Grok transcripts\n link-skill Link one packaged skill into an explicit consumer directory\n retrospective-facts Gather immutable facts for a retrospective\n agent-blackboard Probe and journal an Agent Blackboard deployment\n\nOptions:\n -h, --help Show this help\n -v, --version Print the package version\n\nrunner-port-policy\n (no args) Print the shipped policy as JSON\n --file <path> Validate and print a policy file\n --reserved <port> Print true if the port is reserved\n\nwith-host-lock\n --name <family>\n [--slots <n>]\n --timeout-seconds <n>\n [--command-timeout-seconds <n>]\n [--failure-diagnostics <absolute-script>]\n [--on-acquire-timeout fail|run-unlocked]\n -- <command> [args...]\n\ngha-runtime-audit\n [--repository owner/name] Default GITHUB_REPOSITORY\n [--branch main]\n --pr-workflow <name|/regex/> Repeatable\n --push-workflow <name|/regex/> Repeatable\n\ngha-output <name>\ngha-needs-results [label]\ndownload-with-diagnostics <url> <destination> [-- curl-args...]\ndownload-optional-run-artifacts (--name <name> | --pattern <pattern>) --dir <directory>\nhost-pressure-diagnostics\nallocate-browser-safe-ports [count] [--policy path] [--forbidden-ports path]\ndiagnose-port-collision [--ports \"2200 2216\"] [--output-dir PATH]\nprepare-trivy-db\ngha-artifacts-cleanup run --run-id <id> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\ngha-artifacts-cleanup sweep --older-than-hours <n> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\nhttp-origin [--field NAME] [value]\nvitest-blob-manifest <suite> [reports-directory]\nvitest-report-attempt <write DIRECTORY SUITE|read ROOT>\nprepare-vitest-reports [primary-directory] [fallback-directory] [output-directory]\npnpm-install --runner-lifecycle persistent|ephemeral|ephemeral-full --install-scripts true|false\ncheck-cache-size <path> <max-bytes> <label>\nmake-shard-matrix <total>\nload-runner-env\nclean-workspace\ninstall-github-release --repo owner/name --version X --asset 'name-{platform}.tar.gz' --bin name [--tag-prefix PREFIX] [--expected-sha256 SHA256] [--no-checksum] [--checksums-asset NAME] [--version-flag FLAG] [--bin-dir DIR]\nrun-with-timeout <timeout-seconds> <kill-after-seconds> <command...>\nlint-links [--offline] [--config PATH] [--glob PATTERN] [files...]\nmaterialize-pr-context\nwait-for-apt-locks\ninstall-playwright-chromium-arm64 [name:archive...]\nghcr-package-retention <url-encoded-package>...\nharness-admission-lane <lanes>\nharness-assert-gates <gate>...\nnuget-central-version <trusted-props> <candidate-props> <metadata-json> <output-props>\nswift-semantic-equal <base> <head> <file.swift>\npost-review\nstage-review-payload optional|required <source> <destination>\nretrospective-transcript [--session-id ID] [--jsonl PATH] [--projects-dir PATH] [--codex-sessions-dir PATH] [--grok-sessions-dir PATH]\nlink-skill <name> --source-root <skills-dir> --target-root <consumer-skills-dir> Link a packaged or repository-local skill\nretrospective-facts (--pr NUMBER | --branch NAME | --no-pr) [--repo OWNER/NAME] [--raw]\nagent-blackboard probe\nagent-blackboard journal append --session-id UUID --agent NAME --version VERSION --file PATH [--parent-session-id UUID] [--timestamp ISO8601]\nagent-blackboard journal entries --session-id UUID\nagent-blackboard snapshot partition --snapshot PATH --checksum SHA256 --counts '{\"sessions\":N,\"entries\":N,\"records\":N,\"bytes\":N}'\nagent-blackboard snapshot cleanup [--snapshot PATH] [--partition-directory PATH --receipt JSON]\n";
|
|
1
|
+
export declare const USAGE = "Usage: vouchington <command> [options]\n\nCommands:\n runner-port-policy Print or validate a runner port policy\n with-host-lock Run a command under a host-wide lock\n gha-runtime-audit Audit successful GitHub Actions job runtimes\n require-up-to-date Require HEAD to include a fetched remote branch\n gitleaks-directory-scan Scan a directory with Gitleaks\n ast-grep-examples Run AST-grep rule examples\n gha-workspace-policy Check GitHub Actions workspace safety policy\n gha-output Write a collision-safe multiline GITHUB_OUTPUT record\n gha-needs-results Fail if required GitHub Actions job results failed\n download-with-diagnostics Download a URL and report HTTP status on failure\n download-optional-run-artifacts Download optional artifacts from the current run\n host-pressure-diagnostics Print a bounded host memory/OOM/PSI snapshot\n allocate-browser-safe-ports Allocate Fetch-safe localhost ports\n diagnose-port-collision Capture bounded localhost port diagnostics\n prepare-trivy-db Download the Trivy vulnerability database\n gha-artifacts-cleanup Delete classified GitHub Actions artifacts\n http-origin Validate an optional HTTP(S) origin\n vitest-blob-manifest Stamp a vitest-blob-manifest:v1 identity file\n vitest-report-attempt Write or read a Vitest report-attempt marker\n prepare-vitest-reports Validate and select Vitest report JSON files\n pnpm-install Install a pnpm workspace with retry and release-age fail-fast\n check-cache-size Measure a path and decide whether to save a GHA cache\n make-shard-matrix Emit a [1..N] GitHub Actions shard matrix\n load-runner-env Overlay a runner env file onto GITHUB_ENV with injection guards\n clean-workspace Reset a persistent-runner workspace with a fork-PR trust gate\n install-github-release Download a checksum-verified GitHub Release binary\n run-with-timeout Run a command with GNU timeout or a Perl fallback\n lint-links Two-pass lychee: internal links fail, external warn\n materialize-pr-context Dump PR title/body/files/diff/comments and #N crawl\n wait-for-apt-locks Wait until apt/dpkg lock files are free\n install-playwright-chromium-arm64 Install Playwright Chromium from browsers.json\n ghcr-package-retention Delete old GHCR package versions past KEEP_MIN\n harness-admission-lane Compute a GITHUB_RUN_ID admission lane for fleet fan-out\n harness-assert-gates Fail if any named HARNESS_*_ENABLED gate is enabled\n nuget-central-version Validate a Directory.Packages.props PackageVersion delta\n swift-semantic-equal Compare Swift sources ignoring comments and whitespace\n post-review Post one COMMENT review from a staged payload file\n stage-review-payload Validate a review payload file into a staging directory\n retrospective-transcript Format facts from Claude-compatible, Codex, or Grok transcripts\n link-skill Link one packaged skill into an explicit consumer directory\n retrospective-facts Gather immutable facts for a retrospective\n agent-blackboard Probe and journal an Agent Blackboard deployment\n\nOptions:\n -h, --help Show this help\n -v, --version Print the package version\n\nrunner-port-policy\n (no args) Print the shipped policy as JSON\n --file <path> Validate and print a policy file\n --reserved <port> Print true if the port is reserved\n\nwith-host-lock\n --name <family>\n [--slots <n>]\n --timeout-seconds <n>\n [--command-timeout-seconds <n>]\n [--failure-diagnostics <absolute-script>]\n [--on-acquire-timeout fail|run-unlocked]\n -- <command> [args...]\n\ngha-runtime-audit\n [--repository owner/name] Default GITHUB_REPOSITORY\n [--branch main]\n --pr-workflow <name|/regex/> Repeatable\n --push-workflow <name|/regex/> Repeatable\n\nrequire-up-to-date --remote <name> --branch <name>\ngitleaks-directory-scan --config <path> [--directory <path>]\nast-grep-examples --rules <directory> --config <path>\ngha-workspace-policy [--root <directory>] [--workflow-directory <directory>] [--action-directory <directory>]\n\ngha-output <name>\ngha-needs-results [label]\ndownload-with-diagnostics <url> <destination> [-- curl-args...]\ndownload-optional-run-artifacts (--name <name> | --pattern <pattern>) --dir <directory>\nhost-pressure-diagnostics\nallocate-browser-safe-ports [count] [--policy path] [--forbidden-ports path]\ndiagnose-port-collision [--ports \"2200 2216\"] [--output-dir PATH]\nprepare-trivy-db\ngha-artifacts-cleanup run --run-id <id> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\ngha-artifacts-cleanup sweep --older-than-hours <n> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\nhttp-origin [--field NAME] [value]\nvitest-blob-manifest <suite> [reports-directory]\nvitest-report-attempt <write DIRECTORY SUITE|read ROOT>\nprepare-vitest-reports [primary-directory] [fallback-directory] [output-directory]\npnpm-install --runner-lifecycle persistent|ephemeral|ephemeral-full --install-scripts true|false\ncheck-cache-size <path> <max-bytes> <label>\nmake-shard-matrix <total>\nload-runner-env\nclean-workspace\ninstall-github-release --repo owner/name --version X --asset 'name-{platform}.tar.gz' --bin name [--tag-prefix PREFIX] [--expected-sha256 SHA256] [--no-checksum] [--checksums-asset NAME] [--version-flag FLAG] [--bin-dir DIR]\nrun-with-timeout <timeout-seconds> <kill-after-seconds> <command...>\nlint-links [--offline] [--config PATH] [--glob PATTERN] [files...]\nmaterialize-pr-context\nwait-for-apt-locks\ninstall-playwright-chromium-arm64 [name:archive...]\nghcr-package-retention <url-encoded-package>...\nharness-admission-lane <lanes>\nharness-assert-gates <gate>...\nnuget-central-version <trusted-props> <candidate-props> <metadata-json> <output-props>\nswift-semantic-equal <base> <head> <file.swift>\npost-review\nstage-review-payload optional|required <source> <destination>\nretrospective-transcript [--session-id ID] [--jsonl PATH] [--projects-dir PATH] [--codex-sessions-dir PATH] [--grok-sessions-dir PATH]\nlink-skill <name> --source-root <skills-dir> --target-root <consumer-skills-dir> Link a packaged or repository-local skill\nretrospective-facts (--pr NUMBER | --branch NAME | --no-pr) [--repo OWNER/NAME] [--raw]\nagent-blackboard probe\nagent-blackboard journal append --session-id UUID --agent NAME --version VERSION --file PATH [--parent-session-id UUID] [--timestamp ISO8601]\nagent-blackboard journal entries --session-id UUID\nagent-blackboard snapshot partition --snapshot PATH --checksum SHA256 --counts '{\"sessions\":N,\"entries\":N,\"records\":N,\"bytes\":N}'\nagent-blackboard snapshot cleanup [--snapshot PATH] [--partition-directory PATH --receipt JSON]\n";
|
|
2
2
|
export declare function printUsage(stream?: NodeJS.WritableStream): void;
|
package/dist/cli/usage.mjs
CHANGED
|
@@ -4,6 +4,10 @@ Commands:
|
|
|
4
4
|
runner-port-policy Print or validate a runner port policy
|
|
5
5
|
with-host-lock Run a command under a host-wide lock
|
|
6
6
|
gha-runtime-audit Audit successful GitHub Actions job runtimes
|
|
7
|
+
require-up-to-date Require HEAD to include a fetched remote branch
|
|
8
|
+
gitleaks-directory-scan Scan a directory with Gitleaks
|
|
9
|
+
ast-grep-examples Run AST-grep rule examples
|
|
10
|
+
gha-workspace-policy Check GitHub Actions workspace safety policy
|
|
7
11
|
gha-output Write a collision-safe multiline GITHUB_OUTPUT record
|
|
8
12
|
gha-needs-results Fail if required GitHub Actions job results failed
|
|
9
13
|
download-with-diagnostics Download a URL and report HTTP status on failure
|
|
@@ -64,6 +68,11 @@ gha-runtime-audit
|
|
|
64
68
|
--pr-workflow <name|/regex/> Repeatable
|
|
65
69
|
--push-workflow <name|/regex/> Repeatable
|
|
66
70
|
|
|
71
|
+
require-up-to-date --remote <name> --branch <name>
|
|
72
|
+
gitleaks-directory-scan --config <path> [--directory <path>]
|
|
73
|
+
ast-grep-examples --rules <directory> --config <path>
|
|
74
|
+
gha-workspace-policy [--root <directory>] [--workflow-directory <directory>] [--action-directory <directory>]
|
|
75
|
+
|
|
67
76
|
gha-output <name>
|
|
68
77
|
gha-needs-results [label]
|
|
69
78
|
download-with-diagnostics <url> <destination> [-- curl-args...]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type DirectoryScanExecutor = (executable: string, args: readonly string[]) => Promise<number>;
|
|
2
|
+
export interface GitleaksDirectoryScanOptions {
|
|
3
|
+
readonly config: string;
|
|
4
|
+
readonly directory?: string;
|
|
5
|
+
readonly execute?: DirectoryScanExecutor;
|
|
6
|
+
}
|
|
7
|
+
export declare function gitleaksDirectoryScanArguments(options: GitleaksDirectoryScanOptions): string[];
|
|
8
|
+
export declare function runGitleaksDirectoryScan(options: GitleaksDirectoryScanOptions): Promise<number>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
function requirePath(value, option) {
|
|
4
|
+
if (!value)
|
|
5
|
+
throw new Error(`${option} requires a path`);
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
export function gitleaksDirectoryScanArguments(options) {
|
|
9
|
+
return [
|
|
10
|
+
'--config',
|
|
11
|
+
requirePath(options.config, '--config'),
|
|
12
|
+
...(options.directory === undefined
|
|
13
|
+
? []
|
|
14
|
+
: ['--root', requirePath(options.directory, '--directory')]),
|
|
15
|
+
];
|
|
16
|
+
}
|
|
17
|
+
function defaultExecute(executable, args) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const child = spawn(executable, args, { stdio: 'inherit' });
|
|
20
|
+
child.once('error', reject);
|
|
21
|
+
child.once('close', (code) => resolve(code ?? 1));
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export function runGitleaksDirectoryScan(options) {
|
|
25
|
+
const script = fileURLToPath(new URL('../../scripts/gitleaks-directory-scan.sh', import.meta.url));
|
|
26
|
+
return (options.execute ?? defaultExecute)('bash', [
|
|
27
|
+
script,
|
|
28
|
+
...gitleaksDirectoryScanArguments(options),
|
|
29
|
+
]);
|
|
30
|
+
}
|
package/dist/index.d.mts
CHANGED
|
@@ -17,6 +17,12 @@ export { auditCiJobRuntime, parseWorkflowNameMatch } from './gha-runtime-audit/i
|
|
|
17
17
|
export type { GhApiExecutor, RuntimeAuditOptions, RuntimeAuditResult, RuntimeAuditWorkflowFilter, RuntimeJobResult, RuntimeSample, } from './gha-runtime-audit/index.mts';
|
|
18
18
|
export { checkGhaWorkspacePolicy } from './gha-workspace-policy/index.mts';
|
|
19
19
|
export type { GhaWorkspacePolicyOptions } from './gha-workspace-policy/index.mts';
|
|
20
|
+
export { requireUpToDate } from './require-up-to-date/index.mts';
|
|
21
|
+
export type { GitExecutor, RequireUpToDateOptions } from './require-up-to-date/index.mts';
|
|
22
|
+
export { gitleaksDirectoryScanArguments, runGitleaksDirectoryScan, } from './gitleaks-directory-scan/index.mts';
|
|
23
|
+
export type { DirectoryScanExecutor, GitleaksDirectoryScanOptions, } from './gitleaks-directory-scan/index.mts';
|
|
24
|
+
export { astGrepExamplesArguments, runAstGrepExamples } from './ast-grep-examples/index.mts';
|
|
25
|
+
export type { AstGrepExamplesExecutor, AstGrepExamplesOptions } from './ast-grep-examples/index.mts';
|
|
20
26
|
export { createVitestBlobManifest, createVitestReportAttempt, inspectVitestBlobBundle, parseVitestBlobManifest, parseVitestReportAttempt, readVitestReportAttempts, serializeVitestBlobManifest, serializeVitestReportAttempt, VITEST_BLOB_MANIFEST_FILENAME, VITEST_BLOB_MANIFEST_VERSION, VITEST_REPORT_ATTEMPT_PREFIX, VITEST_REPORT_ATTEMPT_VERSION, vitestBlobBundlePaths, writeVitestBlobManifest, writeVitestReportAttempt, } from './vitest-blob-manifest/index.mts';
|
|
21
27
|
export type { InspectedVitestBlobBundle, VitestBlobIdentity, VitestBlobManifest, VitestReportAttempt, VitestReportAttemptIdentity, } from './vitest-blob-manifest/index.mts';
|
|
22
28
|
export { prepareVitestReports } from './vitest-blob-manifest/reports.mts';
|
package/dist/index.mjs
CHANGED
|
@@ -9,6 +9,9 @@ export { extractAlterTableAddColumnLocations, extractCreateIndexMetadata, extrac
|
|
|
9
9
|
export { dollarQuoteEnd, lineOf, maskSqlQuotedText, readDollarQuoteDelimiter, readStringLiteral, splitSqlStatements, sqlFragments, stripSqlComments, } from './sql-scanner/index.mjs';
|
|
10
10
|
export { auditCiJobRuntime, parseWorkflowNameMatch } from './gha-runtime-audit/index.mjs';
|
|
11
11
|
export { checkGhaWorkspacePolicy } from './gha-workspace-policy/index.mjs';
|
|
12
|
+
export { requireUpToDate } from './require-up-to-date/index.mjs';
|
|
13
|
+
export { gitleaksDirectoryScanArguments, runGitleaksDirectoryScan, } from './gitleaks-directory-scan/index.mjs';
|
|
14
|
+
export { astGrepExamplesArguments, runAstGrepExamples } from './ast-grep-examples/index.mjs';
|
|
12
15
|
export { createVitestBlobManifest, createVitestReportAttempt, inspectVitestBlobBundle, parseVitestBlobManifest, parseVitestReportAttempt, readVitestReportAttempts, serializeVitestBlobManifest, serializeVitestReportAttempt, VITEST_BLOB_MANIFEST_FILENAME, VITEST_BLOB_MANIFEST_VERSION, VITEST_REPORT_ATTEMPT_PREFIX, VITEST_REPORT_ATTEMPT_VERSION, vitestBlobBundlePaths, writeVitestBlobManifest, writeVitestReportAttempt, } from './vitest-blob-manifest/index.mjs';
|
|
13
16
|
export { prepareVitestReports } from './vitest-blob-manifest/reports.mjs';
|
|
14
17
|
export { findWorkspaceLinkMismatches, formatReleaseAgeFailure, INSTALL_TERMINATION_FAILED, isReleaseAgeViolation, parseInstallOptions, parseReleaseAgeViolations, runInstallLifecycle, } from './pnpm-install/index.mjs';
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export type NativeFamily = 'elf' | 'macho' | 'pe';
|
|
2
2
|
export declare function nativeFamilyFromMagic(buffer: Buffer): NativeFamily | undefined;
|
|
3
3
|
export declare function expectedNativeFamily(platform?: NodeJS.Platform): NativeFamily | undefined;
|
|
4
|
-
|
|
4
|
+
declare function packageExcludesPlatform(owner: string, platform: string): Promise<boolean>;
|
|
5
|
+
export declare function mismatchedNativeBinaries(root?: string, platform?: NodeJS.Platform, readPackageExclusion?: typeof packageExcludesPlatform): Promise<string[]>;
|
|
5
6
|
export declare function nativeBinariesMatchRuntime(root?: string, platform?: NodeJS.Platform): Promise<boolean>;
|
|
6
7
|
export declare function repairedNativeBinariesMatchRuntime(paths: string[], platform?: NodeJS.Platform): Promise<boolean>;
|
|
8
|
+
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { glob, open, stat } from 'node:fs/promises';
|
|
1
|
+
import { glob, open, readFile, stat } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
const ELF = Buffer.from([0x7f, 0x45, 0x4c, 0x46]);
|
|
4
4
|
const PE = Buffer.from([0x4d, 0x5a]);
|
|
@@ -58,7 +58,38 @@ async function searchRoot(nodeModules) {
|
|
|
58
58
|
return nodeModules;
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
-
|
|
61
|
+
function excludesPlatform(os, platform) {
|
|
62
|
+
const values = typeof os === 'string' ? [os] : Array.isArray(os) ? os : undefined;
|
|
63
|
+
if (values === undefined || !values.every((value) => typeof value === 'string'))
|
|
64
|
+
return false;
|
|
65
|
+
if (values.length === 1 && values[0] === 'any')
|
|
66
|
+
return false;
|
|
67
|
+
if (values.includes(`!${platform}`))
|
|
68
|
+
return true;
|
|
69
|
+
return values.some((value) => !value.startsWith('!')) && !values.includes(platform);
|
|
70
|
+
}
|
|
71
|
+
function packageRoot(pathname, searchRoot) {
|
|
72
|
+
const segments = path.relative(searchRoot, pathname).split(path.sep);
|
|
73
|
+
const nodeModules = segments.lastIndexOf('node_modules');
|
|
74
|
+
const packageStart = nodeModules === -1 ? 0 : nodeModules + 1;
|
|
75
|
+
const name = segments[packageStart];
|
|
76
|
+
if (name === undefined || name === '.bin')
|
|
77
|
+
return undefined;
|
|
78
|
+
const packageLength = name.startsWith('@') ? 2 : 1;
|
|
79
|
+
if (segments.length <= packageStart + packageLength)
|
|
80
|
+
return undefined;
|
|
81
|
+
return path.join(searchRoot, ...segments.slice(0, packageStart + packageLength));
|
|
82
|
+
}
|
|
83
|
+
async function packageExcludesPlatform(owner, platform) {
|
|
84
|
+
try {
|
|
85
|
+
const manifest = JSON.parse(await readFile(path.join(owner, 'package.json'), 'utf8'));
|
|
86
|
+
return excludesPlatform(manifest.os, platform);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export async function mismatchedNativeBinaries(root = process.cwd(), platform = process.platform, readPackageExclusion = packageExcludesPlatform) {
|
|
62
93
|
const expected = expectedNativeFamily(platform);
|
|
63
94
|
if (expected === undefined)
|
|
64
95
|
return [];
|
|
@@ -72,17 +103,28 @@ export async function mismatchedNativeBinaries(root = process.cwd(), platform =
|
|
|
72
103
|
return [];
|
|
73
104
|
}
|
|
74
105
|
const cwd = await searchRoot(nodeModules);
|
|
106
|
+
const pattern = cwd === nodeModules ? ['**/*.{node,bin}', '.bin/*.{node,bin}'] : '**/*.{node,bin}';
|
|
75
107
|
const mismatches = [];
|
|
76
|
-
for await (const relative of glob(
|
|
108
|
+
for await (const relative of glob(pattern, { cwd })) {
|
|
77
109
|
const pathname = path.join(cwd, relative);
|
|
78
110
|
const magic = await readMagic(pathname);
|
|
79
111
|
if (magic === undefined)
|
|
80
112
|
continue;
|
|
81
113
|
const family = nativeFamilyFromMagic(magic);
|
|
82
|
-
if (family !== undefined && family !== expected)
|
|
83
|
-
mismatches.push(pathname);
|
|
114
|
+
if (family !== undefined && family !== expected) {
|
|
115
|
+
mismatches.push({ owner: packageRoot(pathname, cwd), pathname });
|
|
116
|
+
}
|
|
84
117
|
}
|
|
85
|
-
|
|
118
|
+
const owners = [
|
|
119
|
+
...new Set(mismatches.flatMap(({ owner }) => (owner === undefined ? [] : [owner]))),
|
|
120
|
+
];
|
|
121
|
+
const exclusions = new Map(await Promise.all(owners.map(async (owner) => [
|
|
122
|
+
owner,
|
|
123
|
+
await readPackageExclusion(owner, platform),
|
|
124
|
+
])));
|
|
125
|
+
return mismatches
|
|
126
|
+
.filter(({ owner }) => owner === undefined || !exclusions.get(owner))
|
|
127
|
+
.map(({ pathname }) => pathname);
|
|
86
128
|
}
|
|
87
129
|
export async function nativeBinariesMatchRuntime(root = process.cwd(), platform = process.platform) {
|
|
88
130
|
return (await mismatchedNativeBinaries(root, platform)).length === 0;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type GitExecutor = (args: readonly string[], cwd?: string) => number;
|
|
2
|
+
export interface RequireUpToDateOptions {
|
|
3
|
+
readonly remote: string;
|
|
4
|
+
readonly branch: string;
|
|
5
|
+
readonly cwd?: string;
|
|
6
|
+
readonly execute?: GitExecutor;
|
|
7
|
+
}
|
|
8
|
+
export declare function requireUpToDate(options: RequireUpToDateOptions): void;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
function defaultExecute(args, cwd) {
|
|
3
|
+
try {
|
|
4
|
+
execFileSync('git', args, { cwd, stdio: 'inherit' });
|
|
5
|
+
return 0;
|
|
6
|
+
}
|
|
7
|
+
catch (error) {
|
|
8
|
+
const status = error.status;
|
|
9
|
+
if (typeof status === 'number')
|
|
10
|
+
return status;
|
|
11
|
+
throw error;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function validateName(value, option) {
|
|
15
|
+
if (!value || value.startsWith('-') || /\s/u.test(value)) {
|
|
16
|
+
throw new Error(`${option} must be a non-option Git name`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function requireUpToDate(options) {
|
|
20
|
+
validateName(options.remote, '--remote');
|
|
21
|
+
validateName(options.branch, '--branch');
|
|
22
|
+
const execute = options.execute ?? defaultExecute;
|
|
23
|
+
const fetchStatus = execute(['fetch', '--quiet', options.remote, options.branch], options.cwd);
|
|
24
|
+
if (fetchStatus !== 0)
|
|
25
|
+
throw new Error(`git fetch failed with exit code ${fetchStatus}`);
|
|
26
|
+
const ancestryStatus = execute(['merge-base', '--is-ancestor', 'FETCH_HEAD', 'HEAD'], options.cwd);
|
|
27
|
+
if (ancestryStatus === 1)
|
|
28
|
+
throw new Error(`Current HEAD is not up to date with ${options.remote}/${options.branch}`);
|
|
29
|
+
if (ancestryStatus !== 0)
|
|
30
|
+
throw new Error(`git merge-base failed with exit code ${ancestryStatus}`);
|
|
31
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vouchington-tooling",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Vouchington CLI and extractable tooling libraries.",
|
|
5
5
|
"homepage": "https://github.com/vouchington/vouchington-tooling/tree/main/packages/vouchington-tooling#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -81,6 +81,21 @@
|
|
|
81
81
|
"import": "./dist/gha-workspace-policy/index.mjs",
|
|
82
82
|
"default": "./dist/gha-workspace-policy/index.mjs"
|
|
83
83
|
},
|
|
84
|
+
"./require-up-to-date": {
|
|
85
|
+
"types": "./dist/require-up-to-date/index.d.mts",
|
|
86
|
+
"import": "./dist/require-up-to-date/index.mjs",
|
|
87
|
+
"default": "./dist/require-up-to-date/index.mjs"
|
|
88
|
+
},
|
|
89
|
+
"./gitleaks-directory-scan": {
|
|
90
|
+
"types": "./dist/gitleaks-directory-scan/index.d.mts",
|
|
91
|
+
"import": "./dist/gitleaks-directory-scan/index.mjs",
|
|
92
|
+
"default": "./dist/gitleaks-directory-scan/index.mjs"
|
|
93
|
+
},
|
|
94
|
+
"./ast-grep-examples": {
|
|
95
|
+
"types": "./dist/ast-grep-examples/index.d.mts",
|
|
96
|
+
"import": "./dist/ast-grep-examples/index.mjs",
|
|
97
|
+
"default": "./dist/ast-grep-examples/index.mjs"
|
|
98
|
+
},
|
|
84
99
|
"./vitest-blob-manifest": {
|
|
85
100
|
"types": "./dist/vitest-blob-manifest/index.d.mts",
|
|
86
101
|
"import": "./dist/vitest-blob-manifest/index.mjs",
|
|
@@ -208,8 +208,12 @@ done
|
|
|
208
208
|
{
|
|
209
209
|
echo "# Docker diagnostics (best effort)"
|
|
210
210
|
if command -v docker >/dev/null 2>&1; then
|
|
211
|
-
|
|
212
|
-
|
|
211
|
+
docker_probe_timeout_seconds=${VOUCHINGTON_DOCKER_DIAGNOSTIC_TIMEOUT_SECONDS:-10}
|
|
212
|
+
case "$docker_probe_timeout_seconds" in
|
|
213
|
+
''|*[!0-9]*|0) docker_probe_timeout_seconds=10 ;;
|
|
214
|
+
esac
|
|
215
|
+
run_bounded "$docker_probe_timeout_seconds" docker version --format 'server={{.Server.Version}}' 2>&1 || true
|
|
216
|
+
docker_ps_output=$(run_bounded "$docker_probe_timeout_seconds" docker ps --no-trunc --format 'container={{.ID}} names={{.Names}} ports={{.Ports}}' 2>&1 || true)
|
|
213
217
|
for port in "${valid_ports[@]}"; do
|
|
214
218
|
echo "published_port=$port"
|
|
215
219
|
printf '%s\n' "$docker_ps_output" | awk -v port="$port" '/^probe=/ || $0 ~ "(^|[^0-9])" port "->"' || true
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
CDPATH=''
|
|
4
|
+
|
|
5
|
+
config_path=''
|
|
6
|
+
repo_root=$(pwd -P)
|
|
7
|
+
while [ "$#" -gt 0 ]; do
|
|
8
|
+
case "$1" in
|
|
9
|
+
--config) config_path=${2:?--config requires a path}; shift 2 ;;
|
|
10
|
+
--root) repo_root=${2:?--root requires a path}; shift 2 ;;
|
|
11
|
+
*) echo "unknown gitleaks-directory-scan option: $1" >&2; exit 2 ;;
|
|
12
|
+
esac
|
|
13
|
+
done
|
|
14
|
+
for command_name in git gitleaks tar; do
|
|
15
|
+
command -v "$command_name" >/dev/null 2>&1 || {
|
|
16
|
+
echo "$command_name is required but was not found on PATH" >&2; exit 127;
|
|
17
|
+
}
|
|
18
|
+
done
|
|
19
|
+
[ -n "$config_path" ] || { echo '--config requires a path' >&2; exit 2; }
|
|
20
|
+
case "$config_path" in /*|*'..'*|*'//'*) echo '--config must be a repository-relative path' >&2; exit 2 ;; esac
|
|
21
|
+
repo_root=$(cd -- "$repo_root" && pwd -P)
|
|
22
|
+
[ -f "$repo_root/$config_path" ] || {
|
|
23
|
+
echo "required Gitleaks config is missing: $repo_root/$config_path" >&2; exit 1;
|
|
24
|
+
}
|
|
25
|
+
scratch_dir=$(mktemp -d "${TMPDIR:-/tmp}/gitleaks-directory.XXXXXX")
|
|
26
|
+
cleanup() { [ -n "${scratch_dir:-}" ] && rm -rf -- "$scratch_dir"; }
|
|
27
|
+
trap cleanup EXIT
|
|
28
|
+
trap 'exit 129' HUP
|
|
29
|
+
trap 'exit 130' INT
|
|
30
|
+
trap 'exit 143' TERM
|
|
31
|
+
index_dir="$scratch_dir/index"
|
|
32
|
+
working_dir="$scratch_dir/working"
|
|
33
|
+
mkdir -p "$index_dir" "$working_dir"
|
|
34
|
+
git -C "$repo_root" checkout-index --all --prefix="$index_dir/"
|
|
35
|
+
index_config_path="$index_dir/$config_path"
|
|
36
|
+
[ -f "$index_config_path" ] || {
|
|
37
|
+
echo "required staged Gitleaks config is missing: $config_path" >&2; exit 1;
|
|
38
|
+
}
|
|
39
|
+
git -C "$repo_root" ls-files --cached --others --exclude-standard -z |
|
|
40
|
+
while IFS= read -r -d '' candidate_path; do
|
|
41
|
+
if [ -e "$repo_root/$candidate_path" ] || [ -L "$repo_root/$candidate_path" ]; then
|
|
42
|
+
printf '%s\0' "$candidate_path"
|
|
43
|
+
fi
|
|
44
|
+
done |
|
|
45
|
+
tar -C "$repo_root" --null -T - -cf - |
|
|
46
|
+
tar -C "$working_dir" -xf -
|
|
47
|
+
scan_directory() ( cd -- "$1"; gitleaks dir --config "$2" --redact=100 .; )
|
|
48
|
+
scan_directory "$index_dir" "$index_config_path"
|
|
49
|
+
scan_directory "$working_dir" "$working_dir/$config_path"
|
|
@@ -14,3 +14,8 @@ Consumer wrappers own solution layout, test frameworks, native dependencies, and
|
|
|
14
14
|
|
|
15
15
|
Keep portable library tests separate from rendered application tests; batch compatible native targets
|
|
16
16
|
in one selection pass so a shared build validates the same source set.
|
|
17
|
+
|
|
18
|
+
Before finishing an assertion, name a concrete defect in the code under test that would make it fail;
|
|
19
|
+
an `Assert.True(true)` or a re-implemented computation compared against itself passes for every input
|
|
20
|
+
and proves nothing, so treat [tautological tests](../test-authoring/references/tautological-tests.md)
|
|
21
|
+
as a defect even when coverage looks satisfied.
|
package/skills/manifest.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
{
|
|
5
5
|
"name": "agent-workflow",
|
|
6
6
|
"plugin": "vouchington-workflow",
|
|
7
|
-
"pluginVersion": "0.6.
|
|
7
|
+
"pluginVersion": "0.6.3",
|
|
8
8
|
"path": "agent-workflow/SKILL.md"
|
|
9
9
|
},
|
|
10
10
|
{
|
|
@@ -17,13 +17,13 @@
|
|
|
17
17
|
{
|
|
18
18
|
"name": "blackboard",
|
|
19
19
|
"plugin": "vouchington-workflow",
|
|
20
|
-
"pluginVersion": "0.6.
|
|
20
|
+
"pluginVersion": "0.6.3",
|
|
21
21
|
"path": "blackboard/SKILL.md"
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
"name": "dependabot",
|
|
25
25
|
"plugin": "vouchington-workflow",
|
|
26
|
-
"pluginVersion": "0.6.
|
|
26
|
+
"pluginVersion": "0.6.3",
|
|
27
27
|
"path": "dependabot/SKILL.md",
|
|
28
28
|
"prerequisites": ["github-actions-checklist"]
|
|
29
29
|
},
|
|
@@ -36,26 +36,26 @@
|
|
|
36
36
|
{
|
|
37
37
|
"name": "git-commit-checklist",
|
|
38
38
|
"plugin": "vouchington-workflow",
|
|
39
|
-
"pluginVersion": "0.6.
|
|
39
|
+
"pluginVersion": "0.6.3",
|
|
40
40
|
"path": "git-commit-checklist/SKILL.md"
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
"name": "github-actions-authoring",
|
|
44
44
|
"plugin": "vouchington-workflow",
|
|
45
|
-
"pluginVersion": "0.6.
|
|
45
|
+
"pluginVersion": "0.6.3",
|
|
46
46
|
"path": "github-actions-authoring/SKILL.md",
|
|
47
47
|
"prerequisites": ["github-actions-checklist"]
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
50
|
"name": "github-actions-checklist",
|
|
51
51
|
"plugin": "vouchington-workflow",
|
|
52
|
-
"pluginVersion": "0.6.
|
|
52
|
+
"pluginVersion": "0.6.3",
|
|
53
53
|
"path": "github-actions-checklist/SKILL.md"
|
|
54
54
|
},
|
|
55
55
|
{
|
|
56
56
|
"name": "github-issue",
|
|
57
57
|
"plugin": "vouchington-workflow",
|
|
58
|
-
"pluginVersion": "0.6.
|
|
58
|
+
"pluginVersion": "0.6.3",
|
|
59
59
|
"path": "github-issue/SKILL.md"
|
|
60
60
|
},
|
|
61
61
|
{
|
|
@@ -68,25 +68,25 @@
|
|
|
68
68
|
{
|
|
69
69
|
"name": "npm-publishing",
|
|
70
70
|
"plugin": "vouchington-workflow",
|
|
71
|
-
"pluginVersion": "0.6.
|
|
71
|
+
"pluginVersion": "0.6.3",
|
|
72
72
|
"path": "npm-publishing/SKILL.md"
|
|
73
73
|
},
|
|
74
74
|
{
|
|
75
75
|
"name": "organize-github-issues",
|
|
76
76
|
"plugin": "vouchington-workflow",
|
|
77
|
-
"pluginVersion": "0.6.
|
|
77
|
+
"pluginVersion": "0.6.3",
|
|
78
78
|
"path": "organize-github-issues/SKILL.md"
|
|
79
79
|
},
|
|
80
80
|
{
|
|
81
81
|
"name": "package-json-checklist",
|
|
82
82
|
"plugin": "vouchington-workflow",
|
|
83
|
-
"pluginVersion": "0.6.
|
|
83
|
+
"pluginVersion": "0.6.3",
|
|
84
84
|
"path": "package-json-checklist/SKILL.md"
|
|
85
85
|
},
|
|
86
86
|
{
|
|
87
87
|
"name": "planning",
|
|
88
88
|
"plugin": "vouchington-workflow",
|
|
89
|
-
"pluginVersion": "0.6.
|
|
89
|
+
"pluginVersion": "0.6.3",
|
|
90
90
|
"path": "planning/SKILL.md"
|
|
91
91
|
},
|
|
92
92
|
{
|
|
@@ -110,43 +110,43 @@
|
|
|
110
110
|
{
|
|
111
111
|
"name": "pr-description",
|
|
112
112
|
"plugin": "vouchington-workflow",
|
|
113
|
-
"pluginVersion": "0.6.
|
|
113
|
+
"pluginVersion": "0.6.3",
|
|
114
114
|
"path": "pr-description/SKILL.md"
|
|
115
115
|
},
|
|
116
116
|
{
|
|
117
117
|
"name": "retrospective",
|
|
118
118
|
"plugin": "vouchington-workflow",
|
|
119
|
-
"pluginVersion": "0.6.
|
|
119
|
+
"pluginVersion": "0.6.3",
|
|
120
120
|
"path": "retrospective/SKILL.md"
|
|
121
121
|
},
|
|
122
122
|
{
|
|
123
123
|
"name": "retrospective-distill",
|
|
124
124
|
"plugin": "vouchington-workflow",
|
|
125
|
-
"pluginVersion": "0.6.
|
|
125
|
+
"pluginVersion": "0.6.3",
|
|
126
126
|
"path": "retrospective-distill/SKILL.md"
|
|
127
127
|
},
|
|
128
128
|
{
|
|
129
129
|
"name": "review-ci-logs",
|
|
130
130
|
"plugin": "vouchington-workflow",
|
|
131
|
-
"pluginVersion": "0.6.
|
|
131
|
+
"pluginVersion": "0.6.3",
|
|
132
132
|
"path": "review-ci-logs/SKILL.md"
|
|
133
133
|
},
|
|
134
134
|
{
|
|
135
135
|
"name": "review-github-issue-taxonomy",
|
|
136
136
|
"plugin": "vouchington-workflow",
|
|
137
|
-
"pluginVersion": "0.6.
|
|
137
|
+
"pluginVersion": "0.6.3",
|
|
138
138
|
"path": "review-github-issue-taxonomy/SKILL.md"
|
|
139
139
|
},
|
|
140
140
|
{
|
|
141
141
|
"name": "revisit-followups",
|
|
142
142
|
"plugin": "vouchington-workflow",
|
|
143
|
-
"pluginVersion": "0.6.
|
|
143
|
+
"pluginVersion": "0.6.3",
|
|
144
144
|
"path": "revisit-followups/SKILL.md"
|
|
145
145
|
},
|
|
146
146
|
{
|
|
147
147
|
"name": "static-analysis-checklist",
|
|
148
148
|
"plugin": "vouchington-workflow",
|
|
149
|
-
"pluginVersion": "0.6.
|
|
149
|
+
"pluginVersion": "0.6.3",
|
|
150
150
|
"path": "static-analysis-checklist/SKILL.md"
|
|
151
151
|
},
|
|
152
152
|
{
|
|
@@ -13,3 +13,6 @@ failure. Prefer browser coverage for real browser interactions rather than dupli
|
|
|
13
13
|
Consumer wrappers own environments, credentials, personas, fixtures, and suite commands.
|
|
14
14
|
|
|
15
15
|
Read [browser reliability](references/browser-reliability.md) for locator, waiting, state, and network rules.
|
|
16
|
+
|
|
17
|
+
Read [tautological tests](../test-authoring/references/tautological-tests.md) before finishing any
|
|
18
|
+
assertion that is not obviously falsifiable by a defect in the flow under test.
|
|
@@ -17,5 +17,10 @@ Read the local `AGENTS.md`, `CLAUDE.md`, and journal guidance first.
|
|
|
17
17
|
4. Save through the repository's required durable mechanism and report the record identifier plus
|
|
18
18
|
any follow-up decisions.
|
|
19
19
|
|
|
20
|
+
Use raw evidence only for local verification. Save only bounded structured facts or redacted summaries
|
|
21
|
+
in a durable retrospective; never embed unredacted logs, command output, environment dumps, provider
|
|
22
|
+
payloads, or transcript content there.
|
|
23
|
+
|
|
20
24
|
Do not prescribe transcript access, retention, a report schema, a journal provider, or issue
|
|
21
|
-
filing policy. Consumer wrappers add those local contracts
|
|
25
|
+
filing policy. Consumer wrappers add those local contracts without weakening this minimization
|
|
26
|
+
boundary.
|
|
@@ -13,3 +13,6 @@ end-to-end setup or production data handling.
|
|
|
13
13
|
Consumer wrappers own Storybook configuration, exclusions, visual baselines, and commands.
|
|
14
14
|
|
|
15
15
|
Read [component coverage](references/component-coverage.md) for direct stories and browser isolation.
|
|
16
|
+
|
|
17
|
+
Read [tautological tests](../test-authoring/references/tautological-tests.md) before finishing any
|
|
18
|
+
interaction assertion that is not obviously falsifiable by a defect in the component under test.
|
|
@@ -13,3 +13,6 @@ fixtures small enough to make failures readable.
|
|
|
13
13
|
Consumer wrappers own test targets, coverage thresholds, view-inspection libraries, and fixture APIs.
|
|
14
14
|
|
|
15
15
|
Read [network test doubles](references/network-test-doubles.md) for cancellation and shared-state safety.
|
|
16
|
+
|
|
17
|
+
Read [tautological tests](../test-authoring/references/tautological-tests.md) before finishing any
|
|
18
|
+
assertion that is not obviously falsifiable by a defect in the code under test.
|
|
@@ -15,3 +15,6 @@ A consumer wrapper owns those choices.
|
|
|
15
15
|
|
|
16
16
|
Read [core practice](references/core-practice.md) for the shared boundary, completion, and evidence
|
|
17
17
|
rules before choosing a runner-specific approach.
|
|
18
|
+
|
|
19
|
+
Read [tautological tests](references/tautological-tests.md) before finishing any test whose
|
|
20
|
+
assertion is not obviously falsifiable by a defect in the code under test.
|
|
@@ -7,4 +7,5 @@ failure paths, authorization, and security-relevant validation rather than priva
|
|
|
7
7
|
Start with a failing test when the behavior is testable. Finish only when the production path, its
|
|
8
8
|
public contract, documentation, and generated artifacts move together. Do not leave placeholders or
|
|
9
9
|
test-only production branches. For every acceptance criterion, retain evidence from a focused test,
|
|
10
|
-
review, or explicitly justified manual check
|
|
10
|
+
review, or explicitly justified manual check — evidence only counts if it is falsifiable, so confirm
|
|
11
|
+
the assertion is not [tautological](tautological-tests.md) before treating it as coverage.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Tautological tests
|
|
2
|
+
|
|
3
|
+
A test is tautological when no defect in the code under test could make its assertion fail. Before
|
|
4
|
+
trusting an assertion, name a concrete defect it would catch; if none exists, the assertion proves
|
|
5
|
+
nothing regardless of how many lines it exercises. A test name is part of the assertion: `it('returns
|
|
6
|
+
200')` that never reads the status is itself the defect, since reviewers trust the name and skim the
|
|
7
|
+
body instead of checking that it matches what is asserted.
|
|
8
|
+
|
|
9
|
+
Watch for these shapes, most consequential first. A forced-value tautology transforms the result
|
|
10
|
+
before asserting on the transformed constant, as in `expect(x.then(() => undefined))
|
|
11
|
+
.resolves.toBeUndefined()`: the `.then` still depends on `x` fulfilling rather than rejecting, but
|
|
12
|
+
every fulfillment value collapses to the same `undefined`, so the assertion no longer depends on what
|
|
13
|
+
the code actually returned — ask whether the assertion still depends on the code's output, not merely
|
|
14
|
+
on whether it threw; a test named after the data it claims to return is asserting nothing about that
|
|
15
|
+
data. A literal tautology asserts a literal against itself, `expect(true).toBe(true)`, with
|
|
16
|
+
equivalents in other runners (`XCTAssertTrue(true)`, `Assert.True(true)`). A self-comparison,
|
|
17
|
+
`expect(x).toBe(x)`, and a literal echo that asserts on a value the test itself just wrote fail the
|
|
18
|
+
same way: the input and the expectation are the same value with no code path in between. A mock echo
|
|
19
|
+
asserts that a mock returns what it was configured to return, exercising the mock's configuration
|
|
20
|
+
rather than the code under test. A re-implementation recomputes the production algorithm inside the
|
|
21
|
+
test and compares the two computations, so a bug shared by both cancels out invisibly. A vacuous
|
|
22
|
+
range or shape check — `expect(x.length).toBeGreaterThanOrEqual(0)`, or a `toBeDefined()`/`typeof`
|
|
23
|
+
check on a static import or a value the test just constructed — holds for every possible output. A
|
|
24
|
+
no-assert test awaits a call and asserts nothing afterward: it fails if the call throws, but nothing
|
|
25
|
+
distinguishes a correct return value from an incorrect one, and an `expect-expect` lint rule will
|
|
26
|
+
flag the missing assertion regardless.
|
|
27
|
+
|
|
28
|
+
Coverage pressure is the usual root cause: a line must be covered and no assertion is obvious. Before
|
|
29
|
+
reaching for a weak assertion, check whether the code takes a null- or error-swallowing path for the
|
|
30
|
+
case under test — a repository-lookup helper that returns `null` instead of throwing for a not-found
|
|
31
|
+
condition, for example — and assert the exact expected outcome for the case under test, such as
|
|
32
|
+
`.toBeNull()` for the not-found path or a definite non-null shape for the found path, rather than a
|
|
33
|
+
looser definedness check that would still pass if a regression collapsed the two. Otherwise assert
|
|
34
|
+
the shape of the returned payload. Only when neither is available, fall back to an assertion that
|
|
35
|
+
depends on nothing but successful completion, such as `await expect(run())
|
|
36
|
+
.resolves.not.toBeInstanceOf(Error)`, paired with a test name that says "resolves" rather than
|
|
37
|
+
"succeeds" or "returns 200" — a legitimately weak assertion, honestly labeled as weak. Satisfying a
|
|
38
|
+
coverage percentage while asserting nothing is never acceptable; a 403 executes the same lines as a
|
|
39
|
+
200, and coverage tooling cannot tell them apart.
|
|
40
|
+
|
|
41
|
+
Delete a tautological test if removing it does not reduce coverage; if it does, that test was the sole
|
|
42
|
+
owner of that coverage and must be strengthened in place, not deleted, since a steady coverage number
|
|
43
|
+
is not evidence the remaining tests check anything real. Consumer repositories may enforce the
|
|
44
|
+
mechanical shapes above with ast-grep rules and an `expect-expect` lint rule; this document is the
|
|
45
|
+
judgment layer for shapes those tools cannot express, such as a definedness check that needs dataflow
|
|
46
|
+
analysis, a mock echo, or a re-implementation — tooling and this guidance should agree, not diverge.
|