vigiles 2.2.0 → 2.4.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/README.md +176 -152
- package/dist/cli.js +91 -1
- package/dist/eval.d.ts +34 -1
- package/dist/eval.js +55 -20
- package/dist/harness-assert.d.ts +109 -0
- package/dist/harness-assert.js +220 -0
- package/dist/harness-test.d.ts +46 -0
- package/dist/harness-test.js +81 -9
- package/dist/jest.d.ts +9 -0
- package/dist/jest.js +23 -0
- package/dist/judge.d.ts +29 -0
- package/dist/judge.js +88 -0
- package/dist/mcp.d.ts +48 -0
- package/dist/mcp.js +247 -0
- package/dist/plugin-loader.d.ts +37 -0
- package/dist/plugin-loader.js +195 -0
- package/dist/run-hook.d.ts +77 -0
- package/dist/run-hook.js +80 -0
- package/dist/run-scripts.d.ts +20 -0
- package/dist/run-scripts.js +70 -0
- package/dist/vitest.d.mts +9 -0
- package/dist/vitest.mjs +22 -0
- package/package.json +37 -5
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loadPlugin = loadPlugin;
|
|
4
|
+
exports.resolveHarness = resolveHarness;
|
|
5
|
+
/**
|
|
6
|
+
* vigiles — load a real plugin/repo harness for testing.
|
|
7
|
+
*
|
|
8
|
+
* The unit that matters is not a single hook but the *assembled machine*: the
|
|
9
|
+
* hooks, settings, CLAUDE.md, skills, subagents, and commands a plugin/repo
|
|
10
|
+
* actually ships, working together. `loadPlugin` reads that real harness so a
|
|
11
|
+
* `runHarnessTest` / `runEval` runs against what ships — not a hand-retyped
|
|
12
|
+
* subset that can drift. Hooks, CLAUDE.md and skills are exercisable at the
|
|
13
|
+
* deterministic tier; subagents/commands/MCP are materialized but only run under
|
|
14
|
+
* a real model, so `LoadedPlugin.warnings` flags them (no silent empty machine).
|
|
15
|
+
*
|
|
16
|
+
* runHarnessTest({ plugin: "./", model: scriptModel([...]) });
|
|
17
|
+
*
|
|
18
|
+
* Resolution order for hooks: inline `hooks` in `.claude-plugin/plugin.json`, a
|
|
19
|
+
* `hooks` string path in plugin.json, the `hooks/hooks.json` convention (e.g.
|
|
20
|
+
* obra/superpowers), then a plain repo's `.claude/settings.json`. `${CLAUDE_
|
|
21
|
+
* PLUGIN_ROOT}` in any hook command is expanded to the plugin's absolute path,
|
|
22
|
+
* so the real hook scripts run from where they live (no copying needed). The
|
|
23
|
+
* plugin's CLAUDE.md and skills/ are materialized into the sandbox so the
|
|
24
|
+
* assembled context is present too.
|
|
25
|
+
*/
|
|
26
|
+
const node_fs_1 = require("node:fs");
|
|
27
|
+
const node_path_1 = require("node:path");
|
|
28
|
+
const MAX_SKILL_FILE_BYTES = 256 * 1024;
|
|
29
|
+
/** Read and return the `.hooks` field of a JSON file, or undefined on any error. */
|
|
30
|
+
function readHooksFile(path) {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse((0, node_fs_1.readFileSync)(path, "utf-8"))
|
|
33
|
+
.hooks;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Read the hooks block, handling the real-world plugin layouts:
|
|
41
|
+
* 1. inline `hooks` object in .claude-plugin/plugin.json,
|
|
42
|
+
* 2. a `hooks` *string* in plugin.json pointing at a hooks JSON file,
|
|
43
|
+
* 3. the `hooks/hooks.json` convention (e.g. obra/superpowers) — auto-discovered,
|
|
44
|
+
* 4. a plain repo's `.claude/settings.json`.
|
|
45
|
+
*/
|
|
46
|
+
function readHooks(root) {
|
|
47
|
+
const manifestPath = (0, node_path_1.join)(root, ".claude-plugin", "plugin.json");
|
|
48
|
+
if ((0, node_fs_1.existsSync)(manifestPath)) {
|
|
49
|
+
const m = JSON.parse((0, node_fs_1.readFileSync)(manifestPath, "utf-8"));
|
|
50
|
+
if (typeof m.hooks === "string")
|
|
51
|
+
return readHooksFile((0, node_path_1.join)(root, m.hooks));
|
|
52
|
+
if (m.hooks !== undefined)
|
|
53
|
+
return m.hooks;
|
|
54
|
+
}
|
|
55
|
+
const conventionPath = (0, node_path_1.join)(root, "hooks", "hooks.json");
|
|
56
|
+
if ((0, node_fs_1.existsSync)(conventionPath))
|
|
57
|
+
return readHooksFile(conventionPath);
|
|
58
|
+
const settingsPath = (0, node_path_1.join)(root, ".claude", "settings.json");
|
|
59
|
+
if ((0, node_fs_1.existsSync)(settingsPath))
|
|
60
|
+
return readHooksFile(settingsPath);
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
/** Recursively collect text files under `dir` as `relativePath → contents`. */
|
|
64
|
+
function readTree(dir, base) {
|
|
65
|
+
const out = {};
|
|
66
|
+
for (const entry of (0, node_fs_1.readdirSync)(dir)) {
|
|
67
|
+
const full = (0, node_path_1.join)(dir, entry);
|
|
68
|
+
const st = (0, node_fs_1.statSync)(full);
|
|
69
|
+
if (st.isDirectory()) {
|
|
70
|
+
Object.assign(out, readTree(full, base));
|
|
71
|
+
}
|
|
72
|
+
else if (st.isFile() && st.size <= MAX_SKILL_FILE_BYTES) {
|
|
73
|
+
out[(0, node_path_1.relative)(base, full)] = (0, node_fs_1.readFileSync)(full, "utf-8");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Load the real harness at `pluginPath`. Returns the resolved settings (hooks),
|
|
80
|
+
* the files (CLAUDE.md + skills + agents + commands) to write into the test
|
|
81
|
+
* sandbox, and `warnings` for surfaces the deterministic tier can't drive. Merge
|
|
82
|
+
* `settings` with any inline settings and spread `files` into the fixture.
|
|
83
|
+
*/
|
|
84
|
+
function loadPlugin(pluginPath) {
|
|
85
|
+
const root = (0, node_path_1.resolve)(pluginPath);
|
|
86
|
+
const hooks = readHooks(root);
|
|
87
|
+
// Expand ${CLAUDE_PLUGIN_ROOT} to the real absolute path so the actual hook
|
|
88
|
+
// scripts execute — we test the shipped wiring, not a reimplementation.
|
|
89
|
+
const resolvedHooks = hooks
|
|
90
|
+
? JSON.parse(JSON.stringify(hooks).replaceAll("${CLAUDE_PLUGIN_ROOT}", root))
|
|
91
|
+
: undefined;
|
|
92
|
+
const files = {};
|
|
93
|
+
const claudeMd = (0, node_path_1.join)(root, "CLAUDE.md");
|
|
94
|
+
if ((0, node_fs_1.existsSync)(claudeMd)) {
|
|
95
|
+
files["CLAUDE.md"] = (0, node_fs_1.readFileSync)(claudeMd, "utf-8");
|
|
96
|
+
}
|
|
97
|
+
// Materialize each project-level surface under .claude/<surface>/ so the
|
|
98
|
+
// assembled context is present in the sandbox (best-effort — headless
|
|
99
|
+
// activation of plugin skills/subagents/commands is not guaranteed; the body
|
|
100
|
+
// is present for the agent to read either way). Counting what we materialize
|
|
101
|
+
// also lets us warn about surfaces the deterministic tier can't drive.
|
|
102
|
+
const counts = {};
|
|
103
|
+
for (const surface of ["skills", "agents", "commands"]) {
|
|
104
|
+
const dir = (0, node_path_1.join)(root, surface);
|
|
105
|
+
if (!(0, node_fs_1.existsSync)(dir) || !(0, node_fs_1.statSync)(dir).isDirectory())
|
|
106
|
+
continue;
|
|
107
|
+
const tree = readTree(dir, root);
|
|
108
|
+
for (const [rel, content] of Object.entries(tree)) {
|
|
109
|
+
files[(0, node_path_1.join)(".claude", rel)] = content;
|
|
110
|
+
}
|
|
111
|
+
counts[surface] = Object.keys(tree).length;
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
settings: resolvedHooks ? { hooks: resolvedHooks } : {},
|
|
115
|
+
files,
|
|
116
|
+
warnings: pluginWarnings(root, counts, resolvedHooks, files),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Flag surfaces present-but-not-deterministically-exercisable. Subagents
|
|
121
|
+
* (`agents/`) and slash commands (`commands/`) are materialized into the sandbox
|
|
122
|
+
* but only run under a real model (Task / slash invocation), so they belong to
|
|
123
|
+
* the eval tier. MCP servers aren't wired by the loader at all. And a plugin
|
|
124
|
+
* that yields neither hooks nor files would otherwise be a silent empty machine.
|
|
125
|
+
*/
|
|
126
|
+
function pluginWarnings(root, counts, hooks, files) {
|
|
127
|
+
const warnings = [];
|
|
128
|
+
if (counts.agents) {
|
|
129
|
+
warnings.push(`plugin defines ${String(counts.agents)} subagent file(s) under agents/ — these run only under a real model; test them at the eval tier (runEval), not the deterministic mock.`);
|
|
130
|
+
}
|
|
131
|
+
if (counts.commands) {
|
|
132
|
+
warnings.push(`plugin defines ${String(counts.commands)} slash-command file(s) under commands/ — slash-command invocation needs a real model; test at the eval tier.`);
|
|
133
|
+
}
|
|
134
|
+
if (hasMcp(root)) {
|
|
135
|
+
warnings.push(`plugin declares MCP server(s) (mcpServers / .mcp.json) — the loader does not wire MCP; bring the server up yourself if your test needs it.`);
|
|
136
|
+
}
|
|
137
|
+
if (!hooks && Object.keys(files).length === 0) {
|
|
138
|
+
warnings.push(`nothing was loaded (no hooks, CLAUDE.md, skills, agents, or commands) — the deterministic harness would run an effectively empty machine.`);
|
|
139
|
+
}
|
|
140
|
+
return warnings;
|
|
141
|
+
}
|
|
142
|
+
/** Whether the plugin declares any MCP servers (manifest field or .mcp.json). */
|
|
143
|
+
function hasMcp(root) {
|
|
144
|
+
if ((0, node_fs_1.existsSync)((0, node_path_1.join)(root, ".mcp.json")))
|
|
145
|
+
return true;
|
|
146
|
+
const manifestPath = (0, node_path_1.join)(root, ".claude-plugin", "plugin.json");
|
|
147
|
+
if (!(0, node_fs_1.existsSync)(manifestPath))
|
|
148
|
+
return false;
|
|
149
|
+
try {
|
|
150
|
+
const m = JSON.parse((0, node_fs_1.readFileSync)(manifestPath, "utf-8"));
|
|
151
|
+
return m.mcpServers !== undefined;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Merge a loaded plugin's settings with inline settings. Inline wins; when both
|
|
159
|
+
* declare hooks, the per-event arrays are concatenated (plugin hooks first), so
|
|
160
|
+
* a test can layer an extra hook on top of the real plugin. Returns `undefined`
|
|
161
|
+
* when neither side has hooks (so the caller skips `--settings`).
|
|
162
|
+
*/
|
|
163
|
+
function mergeSettings(base, override) {
|
|
164
|
+
const baseHasHooks = base.hooks !== undefined;
|
|
165
|
+
if (override === undefined)
|
|
166
|
+
return baseHasHooks ? base : undefined;
|
|
167
|
+
if (!baseHasHooks)
|
|
168
|
+
return override;
|
|
169
|
+
const b = base;
|
|
170
|
+
const o = override;
|
|
171
|
+
const events = new Set([
|
|
172
|
+
...Object.keys(b.hooks ?? {}),
|
|
173
|
+
...Object.keys(o.hooks ?? {}),
|
|
174
|
+
]);
|
|
175
|
+
const hooks = {};
|
|
176
|
+
for (const e of events) {
|
|
177
|
+
hooks[e] = [...(b.hooks?.[e] ?? []), ...(o.hooks?.[e] ?? [])];
|
|
178
|
+
}
|
|
179
|
+
return { ...o, hooks };
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Resolve the effective harness for a test/eval (arm): load the plugin if given,
|
|
183
|
+
* then layer inline settings + files on top. Shared by `runHarnessTest` and
|
|
184
|
+
* `runEval` so both test the assembled machine the same way.
|
|
185
|
+
*/
|
|
186
|
+
function resolveHarness(opts) {
|
|
187
|
+
const loaded = opts.plugin
|
|
188
|
+
? loadPlugin(opts.plugin)
|
|
189
|
+
: { settings: {}, files: {} };
|
|
190
|
+
return {
|
|
191
|
+
files: { ...loaded.files, ...opts.files },
|
|
192
|
+
settings: mergeSettings(loaded.settings, opts.settings),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=plugin-loader.js.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/** A hook event payload (the JSON Claude Code writes to the hook's stdin). */
|
|
2
|
+
export interface HookInput {
|
|
3
|
+
/** e.g. "PreToolUse", "PostToolUse", "Stop", "SessionStart", "PreCompact". */
|
|
4
|
+
readonly hook_event_name?: string;
|
|
5
|
+
/** PreToolUse/PostToolUse. */
|
|
6
|
+
readonly tool_name?: string;
|
|
7
|
+
readonly tool_input?: unknown;
|
|
8
|
+
readonly tool_response?: unknown;
|
|
9
|
+
/** UserPromptSubmit. */
|
|
10
|
+
readonly prompt?: string;
|
|
11
|
+
/** SessionStart. */
|
|
12
|
+
readonly source?: string;
|
|
13
|
+
/** Stop / SubagentStop. */
|
|
14
|
+
readonly stop_hook_active?: boolean;
|
|
15
|
+
/** Any other event-specific fields. */
|
|
16
|
+
readonly [k: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
/** The JSON a hook may print on stdout (all fields optional). */
|
|
19
|
+
export interface HookOutput {
|
|
20
|
+
readonly decision?: "approve" | "block";
|
|
21
|
+
readonly reason?: string;
|
|
22
|
+
readonly continue?: boolean;
|
|
23
|
+
readonly stopReason?: string;
|
|
24
|
+
readonly suppressOutput?: boolean;
|
|
25
|
+
readonly systemMessage?: string;
|
|
26
|
+
readonly hookSpecificOutput?: {
|
|
27
|
+
readonly hookEventName?: string;
|
|
28
|
+
readonly permissionDecision?: "allow" | "deny" | "ask";
|
|
29
|
+
readonly permissionDecisionReason?: string;
|
|
30
|
+
readonly additionalContext?: string;
|
|
31
|
+
};
|
|
32
|
+
readonly [k: string]: unknown;
|
|
33
|
+
}
|
|
34
|
+
export interface RunHookOptions {
|
|
35
|
+
/** Working directory for the hook process. Default: a value won't be set. */
|
|
36
|
+
readonly cwd?: string;
|
|
37
|
+
/** Extra env vars (merged over process.env). `{cwd}` in values is left as-is. */
|
|
38
|
+
readonly env?: Record<string, string>;
|
|
39
|
+
/** Per-run timeout ms. Default 10000. */
|
|
40
|
+
readonly timeoutMs?: number;
|
|
41
|
+
}
|
|
42
|
+
export interface HookRunResult {
|
|
43
|
+
readonly exitCode: number;
|
|
44
|
+
readonly stdout: string;
|
|
45
|
+
readonly stderr: string;
|
|
46
|
+
/** Parsed stdout JSON if the hook emitted a JSON decision, else null. */
|
|
47
|
+
readonly json: HookOutput | null;
|
|
48
|
+
/**
|
|
49
|
+
* Normalized decision: a deny/block via exit 2, `decision:"block"`, or
|
|
50
|
+
* `permissionDecision:"deny"` all set `blocked = true`.
|
|
51
|
+
*/
|
|
52
|
+
readonly blocked: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* The decision the hook expressed, preferring the structured
|
|
55
|
+
* `permissionDecision` ("allow"|"deny"|"ask") then legacy `decision`
|
|
56
|
+
* ("approve"|"block"), else undefined.
|
|
57
|
+
*/
|
|
58
|
+
readonly decision: HookOutput["decision"] | "allow" | "deny" | "ask" | undefined;
|
|
59
|
+
}
|
|
60
|
+
/** Parse stdout as a hook JSON decision (pure, testable without a process). */
|
|
61
|
+
export declare function parseHookOutput(stdout: string): HookOutput | null;
|
|
62
|
+
/**
|
|
63
|
+
* Decide whether a hook result blocked, and the normalized decision. Pure, so
|
|
64
|
+
* the policy is unit-testable independent of spawning anything.
|
|
65
|
+
*/
|
|
66
|
+
export declare function decideHook(exitCode: number, json: HookOutput | null): {
|
|
67
|
+
blocked: boolean;
|
|
68
|
+
decision: HookRunResult["decision"];
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Run a hook command, piping `input` as JSON to its stdin, and report the exit
|
|
72
|
+
* code + parsed decision. Synchronous (so it can be used inside an eval's
|
|
73
|
+
* `measure` too). `command` is run through a shell, so the same command string a
|
|
74
|
+
* plugin ships (with args / env refs) works verbatim.
|
|
75
|
+
*/
|
|
76
|
+
export declare function runHook(command: string, input: HookInput, opts?: RunHookOptions): HookRunResult;
|
|
77
|
+
//# sourceMappingURL=run-hook.d.ts.map
|
package/dist/run-hook.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseHookOutput = parseHookOutput;
|
|
4
|
+
exports.decideHook = decideHook;
|
|
5
|
+
exports.runHook = runHook;
|
|
6
|
+
/**
|
|
7
|
+
* vigiles — the *unit* tier for Claude Code hooks.
|
|
8
|
+
*
|
|
9
|
+
* A hook is just a process: Claude Code pipes a JSON event to its stdin and
|
|
10
|
+
* reads back an exit code (0 ok, 2 = block) and, optionally, a JSON decision on
|
|
11
|
+
* stdout. `runHook` exercises exactly that contract directly — no `claude`
|
|
12
|
+
* binary, no model, no sandbox — so a hook's logic can be unit-tested in
|
|
13
|
+
* milliseconds:
|
|
14
|
+
*
|
|
15
|
+
* const r = runHook('"$GUARD" ', {
|
|
16
|
+
* hook_event_name: "PreToolUse",
|
|
17
|
+
* tool_name: "Bash",
|
|
18
|
+
* tool_input: { command: "git commit --no-verify" },
|
|
19
|
+
* }, { env: { GUARD: guardPath } });
|
|
20
|
+
* assert.ok(r.blocked); // exit 2 / decision:block / permission:deny
|
|
21
|
+
*
|
|
22
|
+
* Why this exists alongside `runHarnessTest`:
|
|
23
|
+
* - It is the cheap base of the pyramid — no CLI dependency, runs anywhere.
|
|
24
|
+
* - It reaches every event. The deterministic `runHarnessTest` mock can drive
|
|
25
|
+
* SessionStart/Stop/UserPromptSubmit/Bash PreToolUse|PostToolUse, but NOT
|
|
26
|
+
* Edit/Write tool events (headless-gated), PreCompact, Notification,
|
|
27
|
+
* SessionEnd, or SubagentStop. At this tier you hand the hook the event
|
|
28
|
+
* JSON yourself, so all of them are testable.
|
|
29
|
+
*
|
|
30
|
+
* It does NOT prove the hook is *wired* into the harness (that the settings
|
|
31
|
+
* point at it, that `${CLAUDE_PLUGIN_ROOT}` resolves) — that is what the
|
|
32
|
+
* `plugin:` loader + `runHarnessTest` cover. Use both: unit-test the hook's
|
|
33
|
+
* logic here, then assert it fires in the assembled machine there.
|
|
34
|
+
*/
|
|
35
|
+
const node_child_process_1 = require("node:child_process");
|
|
36
|
+
/** Parse stdout as a hook JSON decision (pure, testable without a process). */
|
|
37
|
+
function parseHookOutput(stdout) {
|
|
38
|
+
const s = stdout.trim();
|
|
39
|
+
if (!s.startsWith("{"))
|
|
40
|
+
return null;
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(s);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Decide whether a hook result blocked, and the normalized decision. Pure, so
|
|
50
|
+
* the policy is unit-testable independent of spawning anything.
|
|
51
|
+
*/
|
|
52
|
+
function decideHook(exitCode, json) {
|
|
53
|
+
const permission = json?.hookSpecificOutput?.permissionDecision;
|
|
54
|
+
const decision = permission ?? json?.decision;
|
|
55
|
+
const blocked = exitCode === 2 || decision === "block" || decision === "deny";
|
|
56
|
+
return { blocked, decision };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Run a hook command, piping `input` as JSON to its stdin, and report the exit
|
|
60
|
+
* code + parsed decision. Synchronous (so it can be used inside an eval's
|
|
61
|
+
* `measure` too). `command` is run through a shell, so the same command string a
|
|
62
|
+
* plugin ships (with args / env refs) works verbatim.
|
|
63
|
+
*/
|
|
64
|
+
function runHook(command, input, opts = {}) {
|
|
65
|
+
const res = (0, node_child_process_1.spawnSync)(command, {
|
|
66
|
+
shell: true,
|
|
67
|
+
cwd: opts.cwd,
|
|
68
|
+
env: { ...process.env, ...opts.env },
|
|
69
|
+
input: JSON.stringify(input),
|
|
70
|
+
encoding: "utf-8",
|
|
71
|
+
timeout: opts.timeoutMs ?? 10000,
|
|
72
|
+
});
|
|
73
|
+
const exitCode = res.status ?? (res.signal ? 1 : 0);
|
|
74
|
+
const stdout = res.stdout ?? "";
|
|
75
|
+
const stderr = res.stderr ?? "";
|
|
76
|
+
const json = parseHookOutput(stdout);
|
|
77
|
+
const { blocked, decision } = decideHook(exitCode, json);
|
|
78
|
+
return { exitCode, stdout, stderr, json, blocked, decision };
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=run-hook.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface ScriptRunResult {
|
|
2
|
+
readonly file: string;
|
|
3
|
+
readonly code: number;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Expand the given path/glob patterns into concrete script files. A pattern
|
|
7
|
+
* that is an existing file passes through unchanged; anything else is treated
|
|
8
|
+
* as a glob. Falls back to `defaultGlob` when no patterns are given. Results
|
|
9
|
+
* are deduped and sorted; `node_modules` and `dist` are always ignored.
|
|
10
|
+
*/
|
|
11
|
+
export declare function discoverScripts(patterns: readonly string[], defaultGlob: string, cwd: string): string[];
|
|
12
|
+
/**
|
|
13
|
+
* Run each script as `node <file>`, inheriting stdio so the script's own report
|
|
14
|
+
* streams to the console. `env` is merged over `process.env` for every child
|
|
15
|
+
* (e.g. `VIGILES_TRIALS`). Returns the per-file exit codes.
|
|
16
|
+
*/
|
|
17
|
+
export declare function runScripts(files: readonly string[], cwd: string, env?: NodeJS.ProcessEnv): ScriptRunResult[];
|
|
18
|
+
/** Format a one-line-per-file run summary with a pass/fail tally. */
|
|
19
|
+
export declare function formatScriptSummary(results: readonly ScriptRunResult[]): string;
|
|
20
|
+
//# sourceMappingURL=run-scripts.d.ts.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.discoverScripts = discoverScripts;
|
|
4
|
+
exports.runScripts = runScripts;
|
|
5
|
+
exports.formatScriptSummary = formatScriptSummary;
|
|
6
|
+
/**
|
|
7
|
+
* vigiles — run harness-test / eval script files via the CLI.
|
|
8
|
+
*
|
|
9
|
+
* `vigiles test` and `vigiles eval` discover `*.harness.mjs` / `*.eval.mjs`
|
|
10
|
+
* scripts and run each as a child `node` process, so the two-tier
|
|
11
|
+
* harness-testing API (`src/harness-test.ts`, `src/eval.ts`) works as a CI
|
|
12
|
+
* command, not just `node x.mjs`. The scripts stay plain Node modules (they
|
|
13
|
+
* import from the built `dist/`), so they also run standalone — the CLI just
|
|
14
|
+
* discovers, runs, and aggregates exit codes.
|
|
15
|
+
*/
|
|
16
|
+
const node_child_process_1 = require("node:child_process");
|
|
17
|
+
const node_path_1 = require("node:path");
|
|
18
|
+
const node_fs_1 = require("node:fs");
|
|
19
|
+
const glob_1 = require("glob");
|
|
20
|
+
/**
|
|
21
|
+
* Expand the given path/glob patterns into concrete script files. A pattern
|
|
22
|
+
* that is an existing file passes through unchanged; anything else is treated
|
|
23
|
+
* as a glob. Falls back to `defaultGlob` when no patterns are given. Results
|
|
24
|
+
* are deduped and sorted; `node_modules` and `dist` are always ignored.
|
|
25
|
+
*/
|
|
26
|
+
function discoverScripts(patterns, defaultGlob, cwd) {
|
|
27
|
+
const globs = patterns.length > 0 ? patterns : [defaultGlob];
|
|
28
|
+
const found = new Set();
|
|
29
|
+
for (const p of globs) {
|
|
30
|
+
if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, p))) {
|
|
31
|
+
found.add(p);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
for (const m of (0, glob_1.globSync)(p, {
|
|
35
|
+
cwd,
|
|
36
|
+
ignore: ["node_modules/**", "dist/**"],
|
|
37
|
+
})) {
|
|
38
|
+
found.add(m);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return [...found].sort();
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Run each script as `node <file>`, inheriting stdio so the script's own report
|
|
45
|
+
* streams to the console. `env` is merged over `process.env` for every child
|
|
46
|
+
* (e.g. `VIGILES_TRIALS`). Returns the per-file exit codes.
|
|
47
|
+
*/
|
|
48
|
+
function runScripts(files, cwd, env = {}) {
|
|
49
|
+
const results = [];
|
|
50
|
+
for (const file of files) {
|
|
51
|
+
const res = (0, node_child_process_1.spawnSync)("node", [file], {
|
|
52
|
+
cwd,
|
|
53
|
+
stdio: "inherit",
|
|
54
|
+
env: { ...process.env, ...env },
|
|
55
|
+
});
|
|
56
|
+
results.push({ file, code: res.status ?? 1 });
|
|
57
|
+
}
|
|
58
|
+
return results;
|
|
59
|
+
}
|
|
60
|
+
/** Format a one-line-per-file run summary with a pass/fail tally. */
|
|
61
|
+
function formatScriptSummary(results) {
|
|
62
|
+
const lines = results.map((r) => ` ${r.code === 0 ? "✓" : "✗"} ${r.file}` +
|
|
63
|
+
(r.code === 0 ? "" : ` (exit ${String(r.code)})`));
|
|
64
|
+
const failed = results.filter((r) => r.code !== 0).length;
|
|
65
|
+
lines.push(failed === 0
|
|
66
|
+
? `\n${String(results.length)} passed.`
|
|
67
|
+
: `\n${String(failed)}/${String(results.length)} failed.`);
|
|
68
|
+
return lines.join("\n");
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=run-scripts.js.map
|
package/dist/vitest.mjs
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/* eslint-disable max-params, @typescript-eslint/no-explicit-any --
|
|
2
|
+
The matcher signatures mirror the runtime vigilesMatchers (positional args),
|
|
3
|
+
and `Matchers<T = any>` must match @vitest/expect's generic default to merge. */
|
|
4
|
+
/**
|
|
5
|
+
* vigiles — vitest integration (opt-in). ESM, because vitest is ESM-only.
|
|
6
|
+
*
|
|
7
|
+
* Importing this entry registers the vigiles matchers AND augments vitest's
|
|
8
|
+
* types so `toHaveCreated` / `toBeatBaseline` type-check.
|
|
9
|
+
*
|
|
10
|
+
* // vitest.config.ts → test: { setupFiles: ["vigiles/vitest"] }
|
|
11
|
+
* // …or at the top of a test file:
|
|
12
|
+
* import "vigiles/vitest";
|
|
13
|
+
*
|
|
14
|
+
* expect(result).toHaveCreated("DONE");
|
|
15
|
+
* expect(report).toBeatBaseline("vanilla", "gated", "caught");
|
|
16
|
+
*
|
|
17
|
+
* vitest is an optional peer dependency — only vitest users load this entry.
|
|
18
|
+
*/
|
|
19
|
+
import { expect } from "vitest";
|
|
20
|
+
import { vigilesMatchers } from "./harness-assert.js";
|
|
21
|
+
expect.extend(vigilesMatchers);
|
|
22
|
+
//# sourceMappingURL=vitest.mjs.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vigiles",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Compile .spec.ts files to instruction files (CLAUDE.md, AGENTS.md) with linter cross-referencing",
|
|
5
5
|
"bin": {
|
|
6
6
|
"vigiles": "dist/cli.js"
|
|
@@ -14,11 +14,23 @@
|
|
|
14
14
|
"./linters": "./dist/linters.js",
|
|
15
15
|
"./eval": "./dist/eval.js",
|
|
16
16
|
"./harness-test": "./dist/harness-test.js",
|
|
17
|
-
"./
|
|
17
|
+
"./harness-assert": "./dist/harness-assert.js",
|
|
18
|
+
"./run-hook": "./dist/run-hook.js",
|
|
19
|
+
"./plugin-loader": "./dist/plugin-loader.js",
|
|
20
|
+
"./mcp": "./dist/mcp.js",
|
|
21
|
+
"./judge": "./dist/judge.js",
|
|
22
|
+
"./mock-model": "./dist/mock-model.js",
|
|
23
|
+
"./vitest": {
|
|
24
|
+
"types": "./dist/vitest.d.mts",
|
|
25
|
+
"default": "./dist/vitest.mjs"
|
|
26
|
+
},
|
|
27
|
+
"./jest": "./dist/jest.js"
|
|
18
28
|
},
|
|
19
29
|
"files": [
|
|
20
30
|
"dist/**/*.js",
|
|
31
|
+
"dist/**/*.mjs",
|
|
21
32
|
"dist/**/*.d.ts",
|
|
33
|
+
"dist/**/*.d.mts",
|
|
22
34
|
"!dist/**/*.test.js",
|
|
23
35
|
"!dist/**/*.test.d.ts",
|
|
24
36
|
"action.yml",
|
|
@@ -28,11 +40,17 @@
|
|
|
28
40
|
],
|
|
29
41
|
"scripts": {
|
|
30
42
|
"build": "tsc",
|
|
31
|
-
"test": "npm run build && node --test dist/spec.test.js dist/validate.test.js dist/cli.test.js dist/proofs.test.js dist/inline.test.js dist/sidecar.test.js dist/coverage.test.js dist/session.test.js dist/orphans.test.js dist/cedar.test.js dist/doc-refs.test.js dist/frontmatter.test.js dist/skill-pipeline.test.js dist/skill-runtime.test.js dist/skill-driver.test.js dist/skill-test.test.js dist/compile-generator.test.js dist/community-skills.test.js dist/action-gate.test.js dist/symbols.test.js dist/refs.test.js dist/harness-test.test.js dist/eval.test.js",
|
|
43
|
+
"test": "npm run build && node --test dist/spec.test.js dist/validate.test.js dist/cli.test.js dist/proofs.test.js dist/inline.test.js dist/sidecar.test.js dist/coverage.test.js dist/session.test.js dist/orphans.test.js dist/cedar.test.js dist/doc-refs.test.js dist/frontmatter.test.js dist/skill-pipeline.test.js dist/skill-runtime.test.js dist/skill-driver.test.js dist/skill-test.test.js dist/compile-generator.test.js dist/community-skills.test.js dist/action-gate.test.js dist/symbols.test.js dist/refs.test.js dist/harness-test.test.js dist/eval.test.js dist/run-scripts.test.js dist/plugin-loader.test.js dist/harness-assert.test.js dist/judge.test.js dist/run-hook.test.js dist/mcp.test.js",
|
|
32
44
|
"lint": "eslint src/",
|
|
33
45
|
"fmt": "prettier --write .",
|
|
34
46
|
"fmt:check": "prettier --check .",
|
|
35
|
-
"
|
|
47
|
+
"demo": "npm run build && bash examples/demo/run.sh",
|
|
48
|
+
"test:e2e": "bash test/e2e/run.sh",
|
|
49
|
+
"test:harness": "npm run build && node dist/cli.js test",
|
|
50
|
+
"test:eval": "npm run build && node dist/cli.js eval",
|
|
51
|
+
"test:vitest": "npm run build && vitest run",
|
|
52
|
+
"test:jest": "npm run build && jest",
|
|
53
|
+
"test:types": "npm run build && tsc --noEmit -p test/types/tsconfig.json"
|
|
36
54
|
},
|
|
37
55
|
"devDependencies": {
|
|
38
56
|
"@eslint/js": "^10.0.1",
|
|
@@ -44,9 +62,23 @@
|
|
|
44
62
|
"eslint": "^10.1.0",
|
|
45
63
|
"eslint-plugin-sonarjs": "^4.0.2",
|
|
46
64
|
"globals": "^17.4.0",
|
|
65
|
+
"jest": "^30.4.2",
|
|
47
66
|
"prettier": "^3.8.1",
|
|
48
67
|
"tsx": "^4.21.0",
|
|
49
|
-
"typescript": "^5.9.3"
|
|
68
|
+
"typescript": "^5.9.3",
|
|
69
|
+
"vitest": "^4.1.8"
|
|
70
|
+
},
|
|
71
|
+
"peerDependencies": {
|
|
72
|
+
"jest": ">=28",
|
|
73
|
+
"vitest": ">=1"
|
|
74
|
+
},
|
|
75
|
+
"peerDependenciesMeta": {
|
|
76
|
+
"jest": {
|
|
77
|
+
"optional": true
|
|
78
|
+
},
|
|
79
|
+
"vitest": {
|
|
80
|
+
"optional": true
|
|
81
|
+
}
|
|
50
82
|
},
|
|
51
83
|
"dependencies": {
|
|
52
84
|
"@ast-grep/lang-python": "^0.0.6",
|