vigiles 38.2.0 → 39.0.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/dist/adapters/claude-code/event-capability.js +24 -8
- package/dist/adapters/claude-code/hook-protocol.js +2 -1
- package/dist/adapters/codex/hook-protocol.js +2 -1
- package/dist/cli-flag-check.d.ts +36 -0
- package/dist/cli-flag-check.js +72 -1
- package/dist/cli-main.d.ts +2 -2
- package/dist/cli-main.js +548 -379
- package/dist/core/bash-effects.d.ts +0 -57
- package/dist/core/bash-effects.js +0 -147
- package/dist/core/compile-generator.d.ts +6 -6
- package/dist/core/compile-generator.js +4 -4
- package/dist/core/compile.d.ts +72 -8
- package/dist/core/compile.js +276 -133
- package/dist/core/config-schema.d.ts +40 -10
- package/dist/core/config-schema.js +139 -16
- package/dist/core/coverage.d.ts +16 -0
- package/dist/core/coverage.js +22 -0
- package/dist/core/doc-refs.d.ts +3 -2
- package/dist/core/doc-refs.js +2 -2
- package/dist/core/enforce-marker.d.ts +28 -0
- package/dist/core/enforce-marker.js +34 -0
- package/dist/core/eslint-inventory.d.ts +96 -0
- package/dist/core/eslint-inventory.js +251 -0
- package/dist/core/event-capability.d.ts +5 -1
- package/dist/core/frame.d.ts +43 -2
- package/dist/core/frame.js +85 -2
- package/dist/core/generate-schema.d.ts +3 -2
- package/dist/core/generate-schema.js +2 -2
- package/dist/core/generate-types.d.ts +11 -2
- package/dist/core/generate-types.js +12 -5
- package/dist/core/hook-command.d.ts +172 -0
- package/dist/core/hook-command.js +277 -0
- package/dist/core/hook-delivery.d.ts +133 -0
- package/dist/core/hook-delivery.js +150 -0
- package/dist/core/hook-paths.d.ts +14 -0
- package/dist/core/hook-paths.js +22 -0
- package/dist/core/hook-program.d.ts +4 -4
- package/dist/core/hook-program.js +13 -25
- package/dist/core/hook-state.d.ts +1 -1
- package/dist/core/hook-state.js +1 -1
- package/dist/core/instruction-remedies.d.ts +35 -0
- package/dist/core/instruction-remedies.js +70 -1
- package/dist/core/integrity-header.d.ts +54 -0
- package/dist/core/integrity-header.js +90 -0
- package/dist/core/integrity.d.ts +1 -25
- package/dist/core/integrity.js +16 -74
- package/dist/core/linter-adapter.d.ts +27 -3
- package/dist/core/linters.d.ts +8 -1
- package/dist/core/linters.js +116 -81
- package/dist/core/orphans.d.ts +3 -5
- package/dist/core/orphans.js +9 -10
- package/dist/core/presets.d.ts +19 -7
- package/dist/core/presets.js +19 -6
- package/dist/core/rule-catalog.d.ts +11 -12
- package/dist/core/rule-catalog.js +25 -94
- package/dist/core/rule-meta.d.ts +12 -0
- package/dist/core/rule-meta.js +47 -10
- package/dist/core/source-refs.d.ts +3 -23
- package/dist/core/source-refs.js +3 -27
- package/dist/core/test-utils.d.ts +13 -0
- package/dist/core/test-utils.js +40 -1
- package/dist/core/types.d.ts +18 -26
- package/dist/core/validate.d.ts +1 -2
- package/dist/core/validate.js +60 -47
- package/dist/coverage-evidence.js +2 -3
- package/dist/deprecated-key-files.d.ts +6 -0
- package/dist/deprecated-key-files.js +7 -6
- package/dist/exclude.d.ts +3 -1
- package/dist/exclude.js +9 -33
- package/dist/harness-assert.d.ts +18 -0
- package/dist/harness-assert.js +38 -0
- package/dist/hook-check-install.js +15 -6
- package/dist/hook-check.d.ts +1 -3
- package/dist/hook-check.js +3 -5
- package/dist/hook-install.d.ts +7 -41
- package/dist/hook-install.js +29 -96
- package/dist/hook-runtime.js +2 -1
- package/dist/load-hook.d.ts +3 -2
- package/dist/load-hook.js +28 -6
- package/dist/overrides.d.ts +25 -0
- package/dist/overrides.js +51 -0
- package/dist/plugin-loader.d.ts +23 -0
- package/dist/plugin-loader.js +42 -12
- package/dist/root-globs.d.ts +9 -0
- package/dist/root-globs.js +48 -0
- package/dist/rule-routing.js +6 -3
- package/dist/run-hook.d.ts +26 -2
- package/dist/run-hook.js +48 -12
- package/dist/scan-core.d.ts +96 -20
- package/dist/scan-core.js +171 -176
- package/dist/scan-files.d.ts +0 -8
- package/dist/scan-files.js +78 -20
- package/dist/scan.d.ts +12 -12
- package/dist/scan.js +33 -10
- package/dist/setup-plan.d.ts +15 -9
- package/dist/setup-plan.js +26 -18
- package/dist/test-coverage.d.ts +3 -8
- package/dist/test-coverage.js +6 -6
- package/dist/test.d.ts +1 -1
- package/dist/vigilesrc.schema.json +1329 -118
- package/package.json +1 -1
- package/skills/adopt-spec/SKILL.md +2 -1
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The ONE answer to "which ESLint rules exist in this repo, which are enabled
|
|
4
|
+
* where, and did the config load" — read from ESLint itself.
|
|
5
|
+
*
|
|
6
|
+
* Three callers used to ask ESLint three different ways, and each saw a
|
|
7
|
+
* different repo: `generate types` probed one representative path per
|
|
8
|
+
* (directory, extension) (#352), `enforce()` verification probed one path per
|
|
9
|
+
* config block `files` glob (#351), and the routing catalog asked about
|
|
10
|
+
* `src/index.ts` alone. The same rule could be enabled for one caller and
|
|
11
|
+
* absent for another. This module owns the probe; every caller reads the same
|
|
12
|
+
* {@link EslintInventory}.
|
|
13
|
+
*
|
|
14
|
+
* This module is the pure half (the probe's script, its wire schema, and every
|
|
15
|
+
* question asked of the answer); `eslintInventory` in `linters.ts` runs the
|
|
16
|
+
* probe beside the other linter subprocesses, cached per project.
|
|
17
|
+
*
|
|
18
|
+
* HOW: a child `node -e` process runs in the project's directory, so `eslint`
|
|
19
|
+
* and the config's own imports resolve from the project exactly as `npx eslint`
|
|
20
|
+
* would, and nothing of the project's config loads into our process. The child
|
|
21
|
+
* lists the config's blocks (plugins they register, rules they mention, a probe
|
|
22
|
+
* path synthesized from each `files` glob), adds one representative path per
|
|
23
|
+
* (directory, name shape) of the project's files, and asks
|
|
24
|
+
* `calculateConfigForFile` for each. A rule is `enabled` if any probe has it on,
|
|
25
|
+
* `disabled` if every probe that sets it has it off.
|
|
26
|
+
*
|
|
27
|
+
* LIMITS, named: a block naming one exact file that shares directory and
|
|
28
|
+
* extension with siblings is reached only if its glob synthesizes that path; a
|
|
29
|
+
* config in a SUBDIRECTORY (v10 looks configs up per file) is not listed; a
|
|
30
|
+
* `files` entry given as a function is not probed. A rule that a block mentions
|
|
31
|
+
* but no probe reached reads `unknown`, never `unconfigured`.
|
|
32
|
+
*/
|
|
33
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
|
+
exports.ESLINT_PROBE_SCRIPT = exports.PROJECT_FILES_GLOB = void 0;
|
|
35
|
+
exports.representativePaths = representativePaths;
|
|
36
|
+
exports.parseEslintInventory = parseEslintInventory;
|
|
37
|
+
exports.configView = configView;
|
|
38
|
+
exports.enabledRuleIds = enabledRuleIds;
|
|
39
|
+
exports.eslintRuleState = eslintRuleState;
|
|
40
|
+
exports.registeredPluginHasRule = registeredPluginHasRule;
|
|
41
|
+
const node_path_1 = require("node:path");
|
|
42
|
+
const zod_1 = require("zod");
|
|
43
|
+
/** Upper bound on the (directory, name-shape) probes handed to ESLint. */
|
|
44
|
+
const PROBE_CAP = 2000;
|
|
45
|
+
/** `src/a.test.ts` → `.test.ts`; a name with no extension (`Makefile`) is its own shape. */
|
|
46
|
+
function nameShape(file) {
|
|
47
|
+
const name = (0, node_path_1.basename)(file);
|
|
48
|
+
const dot = name.indexOf(".", 1);
|
|
49
|
+
return dot === -1 ? name : name.slice(dot);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* One representative path per distinct (directory, name shape) pair of the
|
|
53
|
+
* project's files. A flat-config `files` glob selects on the directory and the
|
|
54
|
+
* name's extension chain, so one file per pair sees every block any file of
|
|
55
|
+
* that pair could match — without linting a single file.
|
|
56
|
+
*/
|
|
57
|
+
function representativePaths(files) {
|
|
58
|
+
// A later duplicate key replaces an earlier one: one representative per pair.
|
|
59
|
+
const byPair = new Map(files.map((f) => [`${(0, node_path_1.dirname)(f)}\0${nameShape(f)}`, f]));
|
|
60
|
+
return [...byPair.values()].slice(0, PROBE_CAP);
|
|
61
|
+
}
|
|
62
|
+
/** The files `representativePaths` picks from: everything but dependencies and git. */
|
|
63
|
+
exports.PROJECT_FILES_GLOB = {
|
|
64
|
+
pattern: "**/*",
|
|
65
|
+
ignore: ["**/node_modules/**", "**/.git/**"],
|
|
66
|
+
};
|
|
67
|
+
// Plain CommonJS for `node -e`, run with cwd = the project. Reads the
|
|
68
|
+
// representative paths as JSON on stdin; prints one JSON object (the wire
|
|
69
|
+
// schema below). `messageTemplate` is ESLint's own error id, so "no config" is
|
|
70
|
+
// read from a field, not from the wording of a message.
|
|
71
|
+
exports.ESLINT_PROBE_SCRIPT = String.raw `
|
|
72
|
+
const { pathToFileURL } = require("node:url");
|
|
73
|
+
const path = require("node:path");
|
|
74
|
+
const PROBE = "__vigiles_probe__";
|
|
75
|
+
const NO_CONFIG = new Set(["no-config-found", "config-file-missing"]);
|
|
76
|
+
function probeFor(pattern, base) {
|
|
77
|
+
if (Array.isArray(pattern)) pattern = pattern[0];
|
|
78
|
+
if (typeof pattern !== "string" || pattern.startsWith("!")) return null;
|
|
79
|
+
let p = pattern
|
|
80
|
+
.replace(/\{([^,}]*)[^}]*\}/g, "$1")
|
|
81
|
+
.replace(/\[!?\^?([^\]])[^\]]*\]/g, "$1")
|
|
82
|
+
.replace(/(^|\/)\*\*\//g, "$1")
|
|
83
|
+
.replace(/\/\*\*$/, "/" + PROBE + ".js")
|
|
84
|
+
.replace(/\*\*/g, PROBE)
|
|
85
|
+
.replace(/\*/g, PROBE)
|
|
86
|
+
.replace(/\?/g, "x");
|
|
87
|
+
if (p.startsWith("./")) p = p.slice(2);
|
|
88
|
+
if (p === "" || path.isAbsolute(p)) return null;
|
|
89
|
+
if (p.endsWith("/")) p += PROBE + ".js";
|
|
90
|
+
return typeof base === "string" ? path.posix.join(base, p) : p;
|
|
91
|
+
}
|
|
92
|
+
function severityOn(v) {
|
|
93
|
+
const sev = Array.isArray(v) ? v[0] : v;
|
|
94
|
+
return !(sev === 0 || sev === "off");
|
|
95
|
+
}
|
|
96
|
+
const print = (o) => console.log(JSON.stringify(o));
|
|
97
|
+
let input = "";
|
|
98
|
+
process.stdin.on("data", (c) => (input += c));
|
|
99
|
+
process.stdin.on("end", async () => {
|
|
100
|
+
let api;
|
|
101
|
+
try { api = require("eslint"); } catch (e) { return print({ kind: "not-installed" }); }
|
|
102
|
+
const plugins = new Map();
|
|
103
|
+
const addPlugins = (map) => {
|
|
104
|
+
if (!map || typeof map !== "object" || Array.isArray(map)) return;
|
|
105
|
+
for (const [ns, plugin] of Object.entries(map)) {
|
|
106
|
+
if (ns === "@" || !plugin || typeof plugin !== "object") continue;
|
|
107
|
+
const set = plugins.get(ns) || new Set();
|
|
108
|
+
for (const r of Object.keys(plugin.rules || {})) set.add(r);
|
|
109
|
+
plugins.set(ns, set);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
try {
|
|
113
|
+
const ESLint = await api.loadESLint();
|
|
114
|
+
const eslint = new ESLint({ cwd: process.cwd() });
|
|
115
|
+
const probes = new Set(["dummy.js", "dummy.mjs", "dummy.cjs", ...JSON.parse(input || "[]")]);
|
|
116
|
+
const mentioned = new Set();
|
|
117
|
+
let complete = false;
|
|
118
|
+
if (typeof eslint.findConfigFile === "function") {
|
|
119
|
+
const file = await eslint.findConfigFile();
|
|
120
|
+
if (!file) return print({ kind: "no-config" });
|
|
121
|
+
const mod = await import(pathToFileURL(file).href);
|
|
122
|
+
const blocks = [];
|
|
123
|
+
const walk = (x) => { if (Array.isArray(x)) x.forEach(walk); else if (x && typeof x === "object") blocks.push(x); };
|
|
124
|
+
walk(await mod.default);
|
|
125
|
+
for (const b of blocks) {
|
|
126
|
+
addPlugins(b.plugins);
|
|
127
|
+
for (const id of Object.keys(b.rules || {})) mentioned.add(id);
|
|
128
|
+
for (const f of Array.isArray(b.files) ? b.files : []) {
|
|
129
|
+
const probe = probeFor(f, b.basePath);
|
|
130
|
+
if (probe) probes.add(probe);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
complete = true;
|
|
134
|
+
}
|
|
135
|
+
const rules = {};
|
|
136
|
+
for (const probe of probes) {
|
|
137
|
+
const cfg = await eslint.calculateConfigForFile(probe);
|
|
138
|
+
// undefined: the config ignores this path.
|
|
139
|
+
if (!cfg) continue;
|
|
140
|
+
addPlugins(cfg.plugins);
|
|
141
|
+
for (const [id, v] of Object.entries(cfg.rules || {})) {
|
|
142
|
+
rules[id] = rules[id] === "enabled" || severityOn(v) ? "enabled" : "disabled";
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
let core = [];
|
|
146
|
+
try { core = [...require("eslint/use-at-your-own-risk").builtinRules.keys()]; } catch (e) {}
|
|
147
|
+
const pluginsOut = {};
|
|
148
|
+
for (const [ns, set] of plugins) pluginsOut[ns] = [...set];
|
|
149
|
+
print({ kind: "loaded", core, plugins: pluginsOut, rules, mentioned: [...mentioned], complete });
|
|
150
|
+
} catch (e) {
|
|
151
|
+
if (e && NO_CONFIG.has(e.messageTemplate)) return print({ kind: "no-config" });
|
|
152
|
+
print({ kind: "failed", error: String((e && e.message) || e).split("\n")[0] });
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
`;
|
|
156
|
+
const ruleStateSchema = zod_1.z.enum(["enabled", "disabled"]);
|
|
157
|
+
/** The child's answer. Parsed, never cast: a malformed answer is a failure. */
|
|
158
|
+
const wireSchema = zod_1.z.discriminatedUnion("kind", [
|
|
159
|
+
zod_1.z.object({ kind: zod_1.z.literal("not-installed") }),
|
|
160
|
+
zod_1.z.object({ kind: zod_1.z.literal("no-config") }),
|
|
161
|
+
zod_1.z.object({ kind: zod_1.z.literal("failed"), error: zod_1.z.string() }),
|
|
162
|
+
zod_1.z.object({
|
|
163
|
+
kind: zod_1.z.literal("loaded"),
|
|
164
|
+
core: zod_1.z.array(zod_1.z.string()),
|
|
165
|
+
plugins: zod_1.z.record(zod_1.z.string(), zod_1.z.array(zod_1.z.string())),
|
|
166
|
+
rules: zod_1.z.record(zod_1.z.string(), ruleStateSchema),
|
|
167
|
+
mentioned: zod_1.z.array(zod_1.z.string()),
|
|
168
|
+
complete: zod_1.z.boolean(),
|
|
169
|
+
}),
|
|
170
|
+
]);
|
|
171
|
+
/**
|
|
172
|
+
* Turn the child's stdout into an inventory. Pure, so every shape of answer is
|
|
173
|
+
* testable without spawning ESLint. @internal
|
|
174
|
+
*/
|
|
175
|
+
function parseEslintInventory(stdout) {
|
|
176
|
+
const line = stdout.trim().split("\n").at(-1) ?? "";
|
|
177
|
+
const json = (() => {
|
|
178
|
+
try {
|
|
179
|
+
return JSON.parse(line);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
})();
|
|
185
|
+
const parsed = wireSchema.safeParse(json);
|
|
186
|
+
if (!parsed.success) {
|
|
187
|
+
return { kind: "failed", error: `unreadable ESLint probe output: ${line}` };
|
|
188
|
+
}
|
|
189
|
+
const wire = parsed.data;
|
|
190
|
+
if (wire.kind !== "loaded")
|
|
191
|
+
return wire;
|
|
192
|
+
const view = {
|
|
193
|
+
core: new Set(wire.core),
|
|
194
|
+
plugins: new Map(Object.entries(wire.plugins).map(([ns, r]) => [ns, new Set(r)])),
|
|
195
|
+
rules: new Map(Object.entries(wire.rules)),
|
|
196
|
+
mentioned: new Set(wire.mentioned),
|
|
197
|
+
complete: wire.complete,
|
|
198
|
+
};
|
|
199
|
+
const anyEnabled = [...view.rules.values()].includes("enabled");
|
|
200
|
+
return anyEnabled ? { kind: "loaded", view } : { kind: "empty", view };
|
|
201
|
+
}
|
|
202
|
+
/** The view, when the config loaded (with or without enabled rules). */
|
|
203
|
+
function configView(inv) {
|
|
204
|
+
return inv.kind === "loaded" || inv.kind === "empty" ? inv.view : null;
|
|
205
|
+
}
|
|
206
|
+
/** Ids of every rule the config switches on somewhere, sorted. */
|
|
207
|
+
function enabledRuleIds(view) {
|
|
208
|
+
return [...view.rules]
|
|
209
|
+
.filter(([, state]) => state === "enabled")
|
|
210
|
+
.map(([id]) => id)
|
|
211
|
+
.sort();
|
|
212
|
+
}
|
|
213
|
+
/** Enabled state of one rule id across every block of the config. */
|
|
214
|
+
function eslintRuleState(inv, ruleId) {
|
|
215
|
+
switch (inv.kind) {
|
|
216
|
+
// No config: ESLint runs no rule anywhere.
|
|
217
|
+
case "no-config":
|
|
218
|
+
return "unconfigured";
|
|
219
|
+
case "not-installed":
|
|
220
|
+
case "failed":
|
|
221
|
+
return "unknown";
|
|
222
|
+
case "empty":
|
|
223
|
+
case "loaded": {
|
|
224
|
+
const state = inv.view.rules.get(ruleId);
|
|
225
|
+
if (state)
|
|
226
|
+
return state;
|
|
227
|
+
// Set somewhere we could not reach, or the block list was never read: we
|
|
228
|
+
// do not know. Set nowhere in a config we read whole: it runs nowhere.
|
|
229
|
+
if (inv.view.mentioned.has(ruleId) || !inv.view.complete) {
|
|
230
|
+
return "unknown";
|
|
231
|
+
}
|
|
232
|
+
return "unconfigured";
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Does the plugin the config REGISTERS under `ruleId`'s namespace define the
|
|
238
|
+
* rule? null when the config registers no plugin under that namespace (or did
|
|
239
|
+
* not load). The namespace is everything before the LAST slash — ESLint's own
|
|
240
|
+
* split, so `@scope/plugin/rule` works.
|
|
241
|
+
*/
|
|
242
|
+
function registeredPluginHasRule(inv, ruleId) {
|
|
243
|
+
const slash = ruleId.lastIndexOf("/");
|
|
244
|
+
if (slash <= 0)
|
|
245
|
+
return null;
|
|
246
|
+
const rules = configView(inv)?.plugins.get(ruleId.substring(0, slash));
|
|
247
|
+
if (!rules)
|
|
248
|
+
return null;
|
|
249
|
+
return rules.has(ruleId.substring(slash + 1));
|
|
250
|
+
}
|
|
251
|
+
//# sourceMappingURL=eslint-inventory.js.map
|
|
@@ -44,8 +44,12 @@
|
|
|
44
44
|
* - `inject` — `additionalContext` reaches the MODEL.
|
|
45
45
|
* - `feedback` — a non-zero exit reaches the model as text, but stops nothing.
|
|
46
46
|
* - `halt` — the whole turn can be ended (Claude Code's `continue: false`).
|
|
47
|
+
* - `stdout-context` — PLAIN (non-JSON) stdout at exit 0 reaches the model as
|
|
48
|
+
* context. Distinct from `inject`: on most events plain stdout goes to the
|
|
49
|
+
* debug log while `additionalContext` still lands, so the two are separate
|
|
50
|
+
* facts (read by `core/hook-delivery.ts`).
|
|
47
51
|
*/
|
|
48
|
-
export type HookChannel = "veto" | "ask" | "inject" | "feedback" | "halt";
|
|
52
|
+
export type HookChannel = "veto" | "ask" | "inject" | "feedback" | "halt" | "stdout-context";
|
|
49
53
|
/** What the event hands the hook — and therefore what a decide() can read. */
|
|
50
54
|
export type EventPayload = "tool" | "prompt" | "stop" | "session" | "none";
|
|
51
55
|
/**
|
package/dist/core/frame.d.ts
CHANGED
|
@@ -75,8 +75,8 @@ export interface Frame {
|
|
|
75
75
|
/**
|
|
76
76
|
* The frame for a command run from `cwd` against `target` (absolute).
|
|
77
77
|
*
|
|
78
|
-
* The root is `cwd` —
|
|
79
|
-
*
|
|
78
|
+
* The root is `cwd` — the project root the CLI resolved once with
|
|
79
|
+
* {@link findProjectRoot} and runs from — UNLESS the target lies outside it. A foreign
|
|
80
80
|
* target (`vigiles lint ../other`) is somebody else's repository, and its paths
|
|
81
81
|
* mean something from ITS root; relative to `cwd` they would all start with
|
|
82
82
|
* `../`, and a foreign lint must never let the caller's own files satisfy the
|
|
@@ -87,4 +87,45 @@ export interface Frame {
|
|
|
87
87
|
export declare function frameFor(cwd: string, target: string): Frame;
|
|
88
88
|
/** The frame rooted at an absolute directory. */
|
|
89
89
|
export declare function frameAt(root: string): Frame;
|
|
90
|
+
/**
|
|
91
|
+
* The target of a markdown link written INTO the file `from` that points at
|
|
92
|
+
* `to`; both are repo-relative. A link resolves from the directory of the file
|
|
93
|
+
* it sits in, so a compiled `plugin/CLAUDE.md` must link `../docs/x.md`, not
|
|
94
|
+
* `docs/x.md` (#371). The ONE place a repo path becomes a link: every renderer
|
|
95
|
+
* that writes a path as a link calls this.
|
|
96
|
+
*/
|
|
97
|
+
export declare function linkFrom(from: string, to: string): string;
|
|
98
|
+
/**
|
|
99
|
+
* The repo path of a spec's secondary target `target`, which the spec names
|
|
100
|
+
* relative to its primary output `primary`'s directory: `docs/CLAUDE.md` with
|
|
101
|
+
* `../AGENTS.md` is `AGENTS.md`.
|
|
102
|
+
*/
|
|
103
|
+
export declare function targetBeside(primary: string, target: string): string;
|
|
104
|
+
/**
|
|
105
|
+
* The project root for a command started in `start` (absolute): the ONE answer
|
|
106
|
+
* to "relative to which directory", resolved once by the CLI, so running
|
|
107
|
+
* `vigiles lint` from `src/` gives the same findings as from the root (#371).
|
|
108
|
+
*
|
|
109
|
+
* From `start` upward:
|
|
110
|
+
* 1. the nearest directory holding `.vigilesrc.json` — a package in a
|
|
111
|
+
* monorepo that has its own config is its own project;
|
|
112
|
+
* 2. else the git work-tree top (the nearest directory holding `.git`, a
|
|
113
|
+
* directory or a worktree's file);
|
|
114
|
+
* 3. else `start` itself.
|
|
115
|
+
*
|
|
116
|
+
* Inside a git checkout the config search stops at the work-tree top, so a
|
|
117
|
+
* config ABOVE the checkout (a home directory) is never read. Outside any
|
|
118
|
+
* checkout it climbs to the filesystem root. `exists` is the filesystem port;
|
|
119
|
+
* the caller passes `existsSync`.
|
|
120
|
+
*/
|
|
121
|
+
export declare function findProjectRoot(start: string, exists: (abs: string) => boolean): string;
|
|
122
|
+
/**
|
|
123
|
+
* A path argument typed in `from`, re-anchored for a command that runs at
|
|
124
|
+
* `root` (both absolute) — so `vigiles lint CLAUDE.md` typed in `pkg/` reads
|
|
125
|
+
* `pkg/CLAUDE.md`. Typed AT the root it is returned exactly as typed, so a
|
|
126
|
+
* message quotes what the user wrote. Absolute (a Windows drive-letter or UNC
|
|
127
|
+
* path included), or leaving the root, it is absolute: `../../etc/passwd`
|
|
128
|
+
* names the same file and reads as a different one.
|
|
129
|
+
*/
|
|
130
|
+
export declare function rebaseArg(typed: string, from: string, root: string): string;
|
|
90
131
|
//# sourceMappingURL=frame.d.ts.map
|
package/dist/core/frame.js
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.frameFor = frameFor;
|
|
4
4
|
exports.frameAt = frameAt;
|
|
5
|
+
exports.linkFrom = linkFrom;
|
|
6
|
+
exports.targetBeside = targetBeside;
|
|
7
|
+
exports.findProjectRoot = findProjectRoot;
|
|
8
|
+
exports.rebaseArg = rebaseArg;
|
|
9
|
+
const config_schema_js_1 = require("./config-schema.js");
|
|
5
10
|
const posix_path_js_1 = require("../posix-path.js");
|
|
6
11
|
/**
|
|
7
12
|
* Node on Windows hands out `C:\\repo`, and `../posix-path.js` recognises only a leading `/`
|
|
@@ -21,8 +26,8 @@ function isBelow(rel) {
|
|
|
21
26
|
/**
|
|
22
27
|
* The frame for a command run from `cwd` against `target` (absolute).
|
|
23
28
|
*
|
|
24
|
-
* The root is `cwd` —
|
|
25
|
-
*
|
|
29
|
+
* The root is `cwd` — the project root the CLI resolved once with
|
|
30
|
+
* {@link findProjectRoot} and runs from — UNLESS the target lies outside it. A foreign
|
|
26
31
|
* target (`vigiles lint ../other`) is somebody else's repository, and its paths
|
|
27
32
|
* mean something from ITS root; relative to `cwd` they would all start with
|
|
28
33
|
* `../`, and a foreign lint must never let the caller's own files satisfy the
|
|
@@ -55,4 +60,82 @@ function frameAt(root) {
|
|
|
55
60
|
};
|
|
56
61
|
return { root: base, repo, bundle };
|
|
57
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* The target of a markdown link written INTO the file `from` that points at
|
|
65
|
+
* `to`; both are repo-relative. A link resolves from the directory of the file
|
|
66
|
+
* it sits in, so a compiled `plugin/CLAUDE.md` must link `../docs/x.md`, not
|
|
67
|
+
* `docs/x.md` (#371). The ONE place a repo path becomes a link: every renderer
|
|
68
|
+
* that writes a path as a link calls this.
|
|
69
|
+
*/
|
|
70
|
+
function linkFrom(from, to) {
|
|
71
|
+
const dir = (0, posix_path_js_1.dirname)((0, posix_path_js_1.normalize)(slashed(from)));
|
|
72
|
+
return (0, posix_path_js_1.relative)((0, posix_path_js_1.join)("/", dir), (0, posix_path_js_1.join)("/", (0, posix_path_js_1.normalize)(slashed(to)))) || ".";
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The repo path of a spec's secondary target `target`, which the spec names
|
|
76
|
+
* relative to its primary output `primary`'s directory: `docs/CLAUDE.md` with
|
|
77
|
+
* `../AGENTS.md` is `AGENTS.md`.
|
|
78
|
+
*/
|
|
79
|
+
function targetBeside(primary, target) {
|
|
80
|
+
return (0, posix_path_js_1.join)((0, posix_path_js_1.dirname)((0, posix_path_js_1.normalize)(slashed(primary))), slashed(target));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The project root for a command started in `start` (absolute): the ONE answer
|
|
84
|
+
* to "relative to which directory", resolved once by the CLI, so running
|
|
85
|
+
* `vigiles lint` from `src/` gives the same findings as from the root (#371).
|
|
86
|
+
*
|
|
87
|
+
* From `start` upward:
|
|
88
|
+
* 1. the nearest directory holding `.vigilesrc.json` — a package in a
|
|
89
|
+
* monorepo that has its own config is its own project;
|
|
90
|
+
* 2. else the git work-tree top (the nearest directory holding `.git`, a
|
|
91
|
+
* directory or a worktree's file);
|
|
92
|
+
* 3. else `start` itself.
|
|
93
|
+
*
|
|
94
|
+
* Inside a git checkout the config search stops at the work-tree top, so a
|
|
95
|
+
* config ABOVE the checkout (a home directory) is never read. Outside any
|
|
96
|
+
* checkout it climbs to the filesystem root. `exists` is the filesystem port;
|
|
97
|
+
* the caller passes `existsSync`.
|
|
98
|
+
*/
|
|
99
|
+
function findProjectRoot(start, exists) {
|
|
100
|
+
const from = (0, posix_path_js_1.normalize)(slashed(start));
|
|
101
|
+
const ancestors = [from, ...ancestorsOf(from)];
|
|
102
|
+
const top = ancestors.findIndex((d) => exists((0, posix_path_js_1.join)(d, ".git")));
|
|
103
|
+
const searched = top === -1 ? ancestors : ancestors.slice(0, top + 1);
|
|
104
|
+
const configured = searched.find((d) => exists((0, posix_path_js_1.join)(d, config_schema_js_1.PROJECT_CONFIG_FILE)));
|
|
105
|
+
return configured ?? (top === -1 ? from : searched[top]) ?? from;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* A path argument typed in `from`, re-anchored for a command that runs at
|
|
109
|
+
* `root` (both absolute) — so `vigiles lint CLAUDE.md` typed in `pkg/` reads
|
|
110
|
+
* `pkg/CLAUDE.md`. Typed AT the root it is returned exactly as typed, so a
|
|
111
|
+
* message quotes what the user wrote. Absolute (a Windows drive-letter or UNC
|
|
112
|
+
* path included), or leaving the root, it is absolute: `../../etc/passwd`
|
|
113
|
+
* names the same file and reads as a different one.
|
|
114
|
+
*/
|
|
115
|
+
function rebaseArg(typed, from, root) {
|
|
116
|
+
if (isAbs(slashed(typed)))
|
|
117
|
+
return typed;
|
|
118
|
+
const [volume, here] = splitVolume(slashed(from));
|
|
119
|
+
const [, top] = splitVolume(slashed(root));
|
|
120
|
+
if ((0, posix_path_js_1.normalize)(here) === (0, posix_path_js_1.normalize)(top))
|
|
121
|
+
return typed;
|
|
122
|
+
const abs = (0, posix_path_js_1.resolve)(here, slashed(typed));
|
|
123
|
+
const rel = (0, posix_path_js_1.relative)(top, abs) || ".";
|
|
124
|
+
return isBelow(rel) ? rel : `${volume}${abs}`;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* A `/`-separated absolute path split into its Windows volume (`C:`, or
|
|
128
|
+
* `//server/share`; `""` on POSIX) and the rooted path below it, which the
|
|
129
|
+
* POSIX ops can climb without eating the volume. `from` and `root` share one:
|
|
130
|
+
* the root is an ancestor of where the command was typed.
|
|
131
|
+
*/
|
|
132
|
+
function splitVolume(abs) {
|
|
133
|
+
const volume = /^(?:[A-Za-z]:|\/\/[^/]+\/[^/]+)(?=\/|$)/.exec(abs)?.[0] ?? "";
|
|
134
|
+
return [volume, abs.slice(volume.length) || "/"];
|
|
135
|
+
}
|
|
136
|
+
/** Every proper ancestor of an absolute directory, nearest first. */
|
|
137
|
+
function ancestorsOf(dir) {
|
|
138
|
+
const parent = (0, posix_path_js_1.dirname)(dir);
|
|
139
|
+
return parent === dir ? [] : [parent, ...ancestorsOf(parent)];
|
|
140
|
+
}
|
|
58
141
|
//# sourceMappingURL=frame.js.map
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
* what is really enforceable, not a static catalog.
|
|
18
18
|
*/
|
|
19
19
|
export interface GenerateSchemaOptions {
|
|
20
|
-
|
|
20
|
+
/** The repo root. Required: no cwd fallback. */
|
|
21
|
+
readonly basePath: string;
|
|
21
22
|
/**
|
|
22
23
|
* Custom linters from `.vigilesrc.json` (`rulesDir`-backed). These aren't
|
|
23
24
|
* auto-discovered by `generate-types`, but `vigiles lint` resolves their
|
|
@@ -47,5 +48,5 @@ export interface GenerateSchemaResult {
|
|
|
47
48
|
* `rule` field falls back to a freeform string so the schema never
|
|
48
49
|
* false-flags a valid reference.
|
|
49
50
|
*/
|
|
50
|
-
export declare function generateSchema(options
|
|
51
|
+
export declare function generateSchema(options: Readonly<GenerateSchemaOptions>): GenerateSchemaResult;
|
|
51
52
|
//# sourceMappingURL=generate-schema.d.ts.map
|
|
@@ -51,8 +51,8 @@ function customRuleRefs(basePath, linters) {
|
|
|
51
51
|
* `rule` field falls back to a freeform string so the schema never
|
|
52
52
|
* false-flags a valid reference.
|
|
53
53
|
*/
|
|
54
|
-
function generateSchema(options
|
|
55
|
-
const basePath = options.basePath
|
|
54
|
+
function generateSchema(options) {
|
|
55
|
+
const basePath = options.basePath;
|
|
56
56
|
const { linters } = (0, generate_types_js_1.generateTypes)({ basePath });
|
|
57
57
|
const ruleNames = new Set();
|
|
58
58
|
for (const { linter, rules } of linters) {
|
|
@@ -11,12 +11,21 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import type { DiscoveredRules } from "./linter-adapter.js";
|
|
13
13
|
export interface GenerateTypesOptions {
|
|
14
|
-
|
|
14
|
+
/** The repo root. Required: no cwd fallback. */
|
|
15
|
+
readonly basePath: string;
|
|
15
16
|
fileGlobs?: string[];
|
|
16
17
|
}
|
|
17
18
|
export interface GenerateTypesResult {
|
|
18
19
|
dts: string;
|
|
19
20
|
linters: DiscoveredRules[];
|
|
21
|
+
/**
|
|
22
|
+
* Linters that are set up here but whose config could not be read: their
|
|
23
|
+
* rules are missing from `dts`, and the caller must say so (#371).
|
|
24
|
+
*/
|
|
25
|
+
unreadable: readonly {
|
|
26
|
+
readonly linter: string;
|
|
27
|
+
readonly reason: string;
|
|
28
|
+
}[];
|
|
20
29
|
scripts: string[];
|
|
21
30
|
files: string[];
|
|
22
31
|
}
|
|
@@ -25,5 +34,5 @@ export interface GenerateTypesResult {
|
|
|
25
34
|
*
|
|
26
35
|
* Scans all available linters, package.json scripts, and project files.
|
|
27
36
|
*/
|
|
28
|
-
export declare function generateTypes(options
|
|
37
|
+
export declare function generateTypes(options: Readonly<GenerateTypesOptions>): GenerateTypesResult;
|
|
29
38
|
//# sourceMappingURL=generate-types.d.ts.map
|
|
@@ -60,20 +60,26 @@ function formatUnion(items, indent = " ") {
|
|
|
60
60
|
}
|
|
61
61
|
return ("\n" + items.map((i) => `${indent}| "${escapeForUnion(i)}"`).join("\n"));
|
|
62
62
|
}
|
|
63
|
+
/** Every linter's enabled rules, and the linters whose config could not be read. */
|
|
64
|
+
function discoverLinters(basePath) {
|
|
65
|
+
const discoveries = Object.values(linters_js_1.LINTERS).flatMap((adapter) => adapter.discoverEnabled ? [adapter.discoverEnabled(basePath)] : []);
|
|
66
|
+
return {
|
|
67
|
+
linters: discoveries.flatMap((d) => (d.kind === "found" ? [d.rules] : [])),
|
|
68
|
+
unreadable: discoveries.flatMap((d) => d.kind === "unreadable" ? [{ linter: d.linter, reason: d.reason }] : []),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
63
71
|
/**
|
|
64
72
|
* Generate .d.ts content with type unions from actual project state.
|
|
65
73
|
*
|
|
66
74
|
* Scans all available linters, package.json scripts, and project files.
|
|
67
75
|
*/
|
|
68
|
-
function generateTypes(options
|
|
69
|
-
const basePath = options.basePath
|
|
76
|
+
function generateTypes(options) {
|
|
77
|
+
const basePath = options.basePath;
|
|
70
78
|
const fileGlobs = options.fileGlobs ?? ["src/**/*"];
|
|
71
79
|
// Discover every linter's enabled rules through the single LINTERS registry
|
|
72
80
|
// (each adapter's discoverEnabled) — no separate discoverers list to keep in
|
|
73
81
|
// step with the registry.
|
|
74
|
-
const linters =
|
|
75
|
-
.map((adapter) => adapter.discoverEnabled?.(basePath))
|
|
76
|
-
.filter((found) => found != null);
|
|
82
|
+
const { linters, unreadable } = discoverLinters(basePath);
|
|
77
83
|
const scripts = discoverNpmScripts(basePath);
|
|
78
84
|
const files = discoverProjectFiles(basePath, fileGlobs);
|
|
79
85
|
// Generate .d.ts
|
|
@@ -148,6 +154,7 @@ function generateTypes(options = {}) {
|
|
|
148
154
|
return {
|
|
149
155
|
dts: sections.join("\n"),
|
|
150
156
|
linters,
|
|
157
|
+
unreadable,
|
|
151
158
|
scripts,
|
|
152
159
|
files,
|
|
153
160
|
};
|