rulebookguard 0.8.0 → 0.10.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 +175 -56
- package/dist/{anchors-C2j9c5MI.mjs → anchors-jf2bKfyg.mjs} +1 -1
- package/dist/cli.mjs +519 -147
- package/dist/index.d.mts +44 -8
- package/dist/index.mjs +3 -3
- package/dist/{lint-DfibQDW3.mjs → lint-BPdk6XxH.mjs} +209 -16
- package/dist/{site-DmY5YVte.mjs → site-B7g8lbjx.mjs} +16 -2
- package/package.json +3 -3
- package/schemas/v1/rule.schema.json +4 -0
package/dist/cli.mjs
CHANGED
|
@@ -1,23 +1,433 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { t as rulesForPath } from "./anchors-
|
|
4
|
-
import { access, mkdir, readdir, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { B as FormatError, I as CONFIG_FILE_NAME, M as CONTEXT_FILE_NAME, S as toRepoPath, V as formatIssue, _ as loadCatalog, b as expandGlobs, c as gitFallbackWarning, d as writeJson, f as writeLines, g as findRulebookRoot, h as scanTestTags, i as lintCommand, k as isRuleId, l as openCatalog, o as CliError, p as scanImplementations, s as findRule, t as changedFiles, u as shownPath, z as messages } from "./lint-BPdk6XxH.mjs";
|
|
3
|
+
import { t as rulesForPath } from "./anchors-jf2bKfyg.mjs";
|
|
4
|
+
import { access, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
5
5
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { parseArgs } from "node:util";
|
|
7
7
|
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
8
8
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
9
9
|
import * as z from "zod/v4";
|
|
10
|
+
// src/commands/export-agents.ts
|
|
11
|
+
const RULE_FILE_TARGETS = {
|
|
12
|
+
cursor: {
|
|
13
|
+
dir: ".cursor/rules",
|
|
14
|
+
extension: ".mdc",
|
|
15
|
+
frontmatter: (patterns) => [`globs: ${patterns.join(", ")}`, "alwaysApply: false"]
|
|
16
|
+
},
|
|
17
|
+
copilot: {
|
|
18
|
+
dir: ".github/instructions",
|
|
19
|
+
extension: ".instructions.md",
|
|
20
|
+
frontmatter: (patterns) => [`applyTo: ${JSON.stringify(patterns.join(","))}`]
|
|
21
|
+
},
|
|
22
|
+
kiro: {
|
|
23
|
+
dir: ".kiro/steering",
|
|
24
|
+
extension: ".md",
|
|
25
|
+
frontmatter: (patterns) => ["inclusion: fileMatch", `fileMatchPattern: ${JSON.stringify(patterns.length === 1 ? patterns[0] : patterns)}`]
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const AGENTS_MD_FORMAT = "agents-md";
|
|
29
|
+
const AGENTS_MD_FILE = "AGENTS.md";
|
|
30
|
+
const BLOCK_START = "<!-- rulebookguard:start -->";
|
|
31
|
+
const BLOCK_END = "<!-- rulebookguard:end -->";
|
|
32
|
+
const RULE_FILE_PREFIX = "rulebookguard-";
|
|
33
|
+
const AGENT_FORMATS = [...Object.keys(RULE_FILE_TARGETS), AGENTS_MD_FORMAT];
|
|
34
|
+
async function exportAgentRules(catalog, io, format, out) {
|
|
35
|
+
const rules = catalog.rules.filter((rule) => rule.status === "active").sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
36
|
+
const target = RULE_FILE_TARGETS[format];
|
|
37
|
+
const outDir = (dir) => out === void 0 ? join(catalog.root, dir) : resolve(io.cwd, out);
|
|
38
|
+
writeLines(io, target === void 0 ? await writeAgentsMd(catalog, io, rules, outDir(".")) : await writeRuleFiles(catalog, io, rules, format, target, outDir(target.dir)));
|
|
39
|
+
}
|
|
40
|
+
function anchoredRules(rules, io, text) {
|
|
41
|
+
const unanchored = rules.filter((rule) => rule.anchors.length === 0);
|
|
42
|
+
if (unanchored.length > 0) io.stderr(unanchored.map((rule) => `${text.unanchoredRuleSkipped(rule.id)}\n`).join(""));
|
|
43
|
+
return rules.filter((rule) => rule.anchors.length > 0);
|
|
44
|
+
}
|
|
45
|
+
async function writeRuleFiles(catalog, io, active, format, target, dir) {
|
|
46
|
+
const text = messages(catalog.config.language);
|
|
47
|
+
const rules = anchoredRules(active, io, text);
|
|
48
|
+
const nothing = active.length === 0 ? text.noActiveRules : text.noAnchoredRules;
|
|
49
|
+
const lines = rules.length === 0 ? [nothing] : [];
|
|
50
|
+
const fileName = (rule) => `${RULE_FILE_PREFIX}${rule.id}${target.extension}`;
|
|
51
|
+
const handWritten = await handWrittenFiles(dir, format, rules.map(fileName));
|
|
52
|
+
if (handWritten.length > 0) throw new CliError(handWritten.map((name) => text.handWrittenExport(toRepoPath(shownPath(io.cwd, join(dir, name))), format)).join("\n"));
|
|
53
|
+
const current = /* @__PURE__ */ new Set();
|
|
54
|
+
for (const rule of rules) {
|
|
55
|
+
const name = fileName(rule);
|
|
56
|
+
current.add(name);
|
|
57
|
+
await mkdir(dir, { recursive: true });
|
|
58
|
+
await writeFile(join(dir, name), ruleFile(rule, format, target, text));
|
|
59
|
+
lines.push(toRepoPath(shownPath(io.cwd, join(dir, name))));
|
|
60
|
+
}
|
|
61
|
+
for (const name of await staleRuleFiles(dir, format, target, current)) {
|
|
62
|
+
await rm(join(dir, name));
|
|
63
|
+
lines.push(text.removedExport(toRepoPath(shownPath(io.cwd, join(dir, name)))));
|
|
64
|
+
}
|
|
65
|
+
return lines;
|
|
66
|
+
}
|
|
67
|
+
function ruleFile(rule, format, target, text) {
|
|
68
|
+
return [
|
|
69
|
+
"---",
|
|
70
|
+
...target.frontmatter(patternsOf(rule)),
|
|
71
|
+
"---",
|
|
72
|
+
"",
|
|
73
|
+
`<!-- ${text.generatedRule(format, rule.file)} -->`,
|
|
74
|
+
"",
|
|
75
|
+
`**${rule.id}:** ${rule.summary}`,
|
|
76
|
+
"",
|
|
77
|
+
`${normalizedBody(rule)}\n`
|
|
78
|
+
].join("\n");
|
|
79
|
+
}
|
|
80
|
+
async function handWrittenFiles(dir, format, names) {
|
|
81
|
+
const existing = new Set(await listNames(dir));
|
|
82
|
+
const handWritten = [];
|
|
83
|
+
for (const name of names) if (existing.has(name) && !hasGeneratedLine(await readFile(join(dir, name), "utf8"), format)) handWritten.push(name);
|
|
84
|
+
return handWritten;
|
|
85
|
+
}
|
|
86
|
+
async function staleRuleFiles(dir, format, target, current) {
|
|
87
|
+
const stale = [];
|
|
88
|
+
for (const name of await listNames(dir)) if (!current.has(name) && name.startsWith(RULE_FILE_PREFIX) && name.endsWith(target.extension) && isRuleId(name.slice(14, -target.extension.length)) && hasGeneratedLine(await readFile(join(dir, name), "utf8"), format)) stale.push(name);
|
|
89
|
+
return stale.sort();
|
|
90
|
+
}
|
|
91
|
+
function hasGeneratedLine(content, format) {
|
|
92
|
+
const lines = content.split(/\r?\n/);
|
|
93
|
+
const closing = lines.indexOf("---", 1);
|
|
94
|
+
const line = lines[closing + 2] ?? "";
|
|
95
|
+
return lines[0] === "---" && closing !== -1 && lines[closing + 1] === "" && line.startsWith("<!-- ") && line.includes(` "rulebookguard export ${format}" `) && line.endsWith(" -->");
|
|
96
|
+
}
|
|
97
|
+
async function listNames(dir) {
|
|
98
|
+
try {
|
|
99
|
+
return await readdir(dir);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (error.code === "ENOENT") return [];
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function writeAgentsMd(catalog, io, rules, dir) {
|
|
106
|
+
const text = messages(catalog.config.language);
|
|
107
|
+
const file = join(dir, AGENTS_MD_FILE);
|
|
108
|
+
const shown = toRepoPath(shownPath(io.cwd, file));
|
|
109
|
+
const existing = await readOptional(file);
|
|
110
|
+
const start = existing.indexOf(BLOCK_START);
|
|
111
|
+
const end = existing.indexOf(BLOCK_END);
|
|
112
|
+
const starts = existing.split(BLOCK_START).length - 1;
|
|
113
|
+
if (starts !== existing.split(BLOCK_END).length - 1 || starts > 1 || end < start) throw new CliError(text.invalidAgentsMdMarkers(shown, BLOCK_START, BLOCK_END));
|
|
114
|
+
const hasBlock = starts === 1;
|
|
115
|
+
if (rules.length === 0 && !hasBlock) return [text.noActiveRules];
|
|
116
|
+
const before = hasBlock ? existing.slice(0, start) : existing;
|
|
117
|
+
const after = hasBlock ? existing.slice(end + 26) : "";
|
|
118
|
+
if (rules.length === 0) {
|
|
119
|
+
const rest = withoutBlock(before, after);
|
|
120
|
+
if (rest === "") {
|
|
121
|
+
await rm(file);
|
|
122
|
+
return [text.noActiveRules, text.removedExport(shown)];
|
|
123
|
+
}
|
|
124
|
+
await writeFile(file, rest);
|
|
125
|
+
return [text.noActiveRules, shown];
|
|
126
|
+
}
|
|
127
|
+
await mkdir(dir, { recursive: true });
|
|
128
|
+
await writeFile(file, withBlock(before, after, agentsBlock(rules, text), hasBlock));
|
|
129
|
+
return [shown];
|
|
130
|
+
}
|
|
131
|
+
function withBlock(before, after, block, hasBlock) {
|
|
132
|
+
if (hasBlock) return `${before}${block}${after}`;
|
|
133
|
+
return before.trim() === "" ? `${block}\n` : `${before.trimEnd()}\n\n${block}\n`;
|
|
134
|
+
}
|
|
135
|
+
function withoutBlock(before, after) {
|
|
136
|
+
const joined = `${before}${after.replace(/^(?:\r?\n)+/, "")}`;
|
|
137
|
+
return joined.trim() === "" ? "" : `${joined.trimEnd()}\n`;
|
|
138
|
+
}
|
|
139
|
+
function agentsBlock(rules, text) {
|
|
140
|
+
return [
|
|
141
|
+
BLOCK_START,
|
|
142
|
+
`<!-- ${text.generatedAgentsMd} -->`,
|
|
143
|
+
`## ${text.agentsMdHeading}`,
|
|
144
|
+
"",
|
|
145
|
+
text.agentsMdIntro,
|
|
146
|
+
"",
|
|
147
|
+
...rules.map((rule) => {
|
|
148
|
+
const anchors = rule.anchors.map((anchor) => `\`${toRepoPath(anchor)}\``).join(", ") || text.agentsMdNoAnchors;
|
|
149
|
+
return `- \`${rule.id}\` (${anchors}): ${rule.summary} ${text.agentsMdSeeFile(rule.file)}`;
|
|
150
|
+
}),
|
|
151
|
+
BLOCK_END
|
|
152
|
+
].join("\n");
|
|
153
|
+
}
|
|
154
|
+
async function readOptional(file) {
|
|
155
|
+
try {
|
|
156
|
+
return await readFile(file, "utf8");
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (error.code === "ENOENT") return "";
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function patternsOf(rule) {
|
|
163
|
+
return [...new Set(rule.anchors.flatMap((anchor) => expandBraces(toRepoPath(anchor))))];
|
|
164
|
+
}
|
|
165
|
+
function expandBraces(glob) {
|
|
166
|
+
const match = /\{([^{}]*,[^{}]*)\}/.exec(glob);
|
|
167
|
+
if (match === null) return [glob];
|
|
168
|
+
const head = glob.slice(0, match.index);
|
|
169
|
+
const tail = glob.slice(match.index + match[0].length);
|
|
170
|
+
return (match[1] ?? "").split(",").flatMap((option) => expandBraces(`${head}${option}${tail}`));
|
|
171
|
+
}
|
|
172
|
+
function normalizedBody(rule) {
|
|
173
|
+
return rule.body.replaceAll("\r\n", "\n").trim();
|
|
174
|
+
}
|
|
175
|
+
// src/commands/export.ts
|
|
176
|
+
const EXPORT_USAGE = "rulebookguard export <format> [--out <dir>]";
|
|
177
|
+
const GHERKIN_BLOCK = /^```gherkin[^\n]*\n([\s\S]*?)^```/gm;
|
|
178
|
+
const LANGUAGE_LINE = /^\s*#\s*language:.*$/;
|
|
179
|
+
async function exportCommand(catalog, io, format, out) {
|
|
180
|
+
if (AGENT_FORMATS.includes(format)) {
|
|
181
|
+
await exportAgentRules(catalog, io, format, out);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (format !== "gherkin") {
|
|
185
|
+
const supported = ["gherkin", ...AGENT_FORMATS].join(", ");
|
|
186
|
+
throw new CliError(`Unknown export format "${format}"; supported: ${supported}.\nUsage: ${EXPORT_USAGE}`, 2);
|
|
187
|
+
}
|
|
188
|
+
const language = catalog.config.language ?? "en";
|
|
189
|
+
const text = messages(language);
|
|
190
|
+
const outDir = resolve(io.cwd, out ?? "features/rules");
|
|
191
|
+
const written = [];
|
|
192
|
+
const rules = catalog.rules.filter((rule) => rule.status !== "deprecated").sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
193
|
+
for (const rule of rules) {
|
|
194
|
+
const scenarios = gherkinBlocks(rule.body);
|
|
195
|
+
if (scenarios.length === 0) continue;
|
|
196
|
+
const file = join(outDir, rule.context, `${rule.id}.feature`);
|
|
197
|
+
await mkdir(dirname(file), { recursive: true });
|
|
198
|
+
await writeFile(file, featureFile(rule, scenarios, language === "es", text));
|
|
199
|
+
written.push(shownPath(io.cwd, file));
|
|
200
|
+
}
|
|
201
|
+
writeLines(io, written.length === 0 ? [text.noGherkinScenarios] : written);
|
|
202
|
+
}
|
|
203
|
+
function gherkinBlocks(body) {
|
|
204
|
+
return [...body.matchAll(GHERKIN_BLOCK)].map((match) => (match[1] ?? "").split("\n").filter((line) => !LANGUAGE_LINE.test(line)).join("\n").trim()).filter((block) => block !== "");
|
|
205
|
+
}
|
|
206
|
+
function featureFile(rule, scenarios, spanish, text) {
|
|
207
|
+
const indented = scenarios.map((block) => block.split("\n").map((line) => line.trim() === "" ? "" : ` ${line}`).join("\n"));
|
|
208
|
+
return [
|
|
209
|
+
...spanish ? ["# language: es"] : [],
|
|
210
|
+
`# ${text.generatedFeature(rule.file)}`,
|
|
211
|
+
`@${rule.id}`,
|
|
212
|
+
`${text.featureKeyword}: ${rule.id} ${rule.summary}`,
|
|
213
|
+
"",
|
|
214
|
+
`${indented.join("\n\n")}\n`
|
|
215
|
+
].join("\n");
|
|
216
|
+
}
|
|
217
|
+
// src/commands/read.ts
|
|
218
|
+
const RULE_STATUSES = [
|
|
219
|
+
"proposed",
|
|
220
|
+
"active",
|
|
221
|
+
"deprecated"
|
|
222
|
+
];
|
|
223
|
+
function contextsCommand(catalog, io, json) {
|
|
224
|
+
const contexts = [...catalog.contexts].sort((a, b) => compare(a.context, b.context)).map(({ context, prefix, summary, file }) => ({
|
|
225
|
+
context,
|
|
226
|
+
prefix,
|
|
227
|
+
summary,
|
|
228
|
+
rules: catalog.rules.filter((rule) => rule.context === context).length,
|
|
229
|
+
file
|
|
230
|
+
}));
|
|
231
|
+
if (json) {
|
|
232
|
+
writeJson(io, { contexts });
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (contexts.length === 0) {
|
|
236
|
+
writeLines(io, [messages(catalog.config.language).noContexts]);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
writeLines(io, contexts.map((item) => `${item.context} (${item.prefix}, ${messages(catalog.config.language).ruleCount(item.rules)}): ${item.summary}`));
|
|
240
|
+
}
|
|
241
|
+
function listCommand(catalog, io, options) {
|
|
242
|
+
const { context, status } = options;
|
|
243
|
+
const text = messages(catalog.config.language);
|
|
244
|
+
if (status !== void 0 && !RULE_STATUSES.includes(status)) throw new CliError(text.invalidStatus(status, RULE_STATUSES.join(", ")), 2);
|
|
245
|
+
if (context !== void 0 && !catalog.contexts.some((item) => item.context === context)) throw new CliError(text.unknownContext(context));
|
|
246
|
+
const rules = sortById(catalog.rules).filter((rule) => context === void 0 || rule.context === context).filter((rule) => status === void 0 || rule.status === status).map(toSummary);
|
|
247
|
+
if (options.json) {
|
|
248
|
+
writeJson(io, { rules });
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (rules.length === 0) {
|
|
252
|
+
writeLines(io, [text.noRules]);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
writeLines(io, rules.map((rule) => `${rule.id} [${rule.status}] ${rule.summary}`));
|
|
256
|
+
}
|
|
257
|
+
function searchCommand(catalog, io, query, options) {
|
|
258
|
+
const { context } = options;
|
|
259
|
+
const text = messages(catalog.config.language);
|
|
260
|
+
if (context !== void 0 && !catalog.contexts.some((item) => item.context === context)) throw new CliError(text.unknownContext(context));
|
|
261
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
262
|
+
const rules = sortById(catalog.rules).filter((rule) => context === void 0 || rule.context === context).filter((rule) => {
|
|
263
|
+
const haystack = [
|
|
264
|
+
rule.id,
|
|
265
|
+
rule.summary,
|
|
266
|
+
...rule.entities,
|
|
267
|
+
rule.body
|
|
268
|
+
].join("\n").toLowerCase();
|
|
269
|
+
return terms.every((term) => haystack.includes(term));
|
|
270
|
+
}).map(toSummary);
|
|
271
|
+
if (options.json) {
|
|
272
|
+
writeJson(io, { rules });
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
writeLines(io, rules.length === 0 ? [text.noRules] : rules.map((rule) => `${rule.id} [${rule.status}] ${rule.summary}`));
|
|
276
|
+
}
|
|
277
|
+
async function showCommand(catalog, io, id, json) {
|
|
278
|
+
const rule = findRule(catalog, id);
|
|
279
|
+
const files = await expandGlobs(catalog.root, rule.anchors, gitFallbackWarning(io, catalog.config.language));
|
|
280
|
+
const implementations = (await scanImplementations(catalog.root, files)).regions.get(rule.id) ?? [];
|
|
281
|
+
if (json) {
|
|
282
|
+
writeJson(io, { rule: {
|
|
283
|
+
...toSummary(rule),
|
|
284
|
+
entities: rule.entities,
|
|
285
|
+
anchors: rule.anchors,
|
|
286
|
+
supersedes: rule.supersedes ?? null,
|
|
287
|
+
dependsOn: rule.dependsOn ?? [],
|
|
288
|
+
conflictsWith: rule.conflictsWith ?? [],
|
|
289
|
+
origin: rule.origin ?? null,
|
|
290
|
+
evidence: rule.evidence ?? [],
|
|
291
|
+
implementations,
|
|
292
|
+
file: rule.file,
|
|
293
|
+
body: rule.body
|
|
294
|
+
} });
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const none = messages(catalog.config.language).none;
|
|
298
|
+
const header = [
|
|
299
|
+
`${rule.id}: ${rule.summary}`,
|
|
300
|
+
`context: ${rule.context}`,
|
|
301
|
+
`status: ${rule.status}`,
|
|
302
|
+
`entities: ${rule.entities.length === 0 ? none : rule.entities.join(", ")}`,
|
|
303
|
+
"anchors:",
|
|
304
|
+
...indent(rule.anchors, none),
|
|
305
|
+
...rule.supersedes === void 0 ? [] : [`supersedes: ${rule.supersedes}`],
|
|
306
|
+
...rule.dependsOn?.length ? [`dependsOn: ${rule.dependsOn.join(", ")}`] : [],
|
|
307
|
+
...rule.conflictsWith?.length ? [`conflictsWith: ${rule.conflictsWith.join(", ")}`] : [],
|
|
308
|
+
...rule.origin === void 0 ? [] : [`origin: ${rule.origin}`],
|
|
309
|
+
...rule.evidence?.length ? ["evidence:", ...indent(rule.evidence, none)] : [],
|
|
310
|
+
...implementations.length ? ["implementations:", ...indent(implementations.map((region) => `${region.file}:${region.line}-${region.endLine}`), none)] : [],
|
|
311
|
+
`file: ${rule.file}`
|
|
312
|
+
];
|
|
313
|
+
io.stdout(`${header.join("\n")}\n\n${rule.body}`);
|
|
314
|
+
}
|
|
315
|
+
async function forCommand(catalog, io, input, json) {
|
|
316
|
+
const text = messages(catalog.config.language);
|
|
317
|
+
const path = toRootPath(catalog, resolve(io.cwd, input), input);
|
|
318
|
+
const regions = await regionsIn(catalog.root, path);
|
|
319
|
+
const rules = rulesForPath(catalog.rules, path).filter((rule) => rule.status === "active").map((rule) => ({
|
|
320
|
+
...toSummary(rule),
|
|
321
|
+
file: rule.file,
|
|
322
|
+
regions: regions(rule.id)
|
|
323
|
+
}));
|
|
324
|
+
if (json) {
|
|
325
|
+
writeJson(io, {
|
|
326
|
+
path,
|
|
327
|
+
rules
|
|
328
|
+
});
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (rules.length === 0) {
|
|
332
|
+
writeLines(io, [text.noActiveRulesFor(path)]);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
writeLines(io, rules.map((rule) => `${rule.id} (${rule.context}): ${rule.summary}${rule.regions.length === 0 ? "" : ` (${text.lines(formatSpans(rule.regions))})`}`));
|
|
336
|
+
}
|
|
337
|
+
async function forChangedCommand(catalog, io, base, json) {
|
|
338
|
+
const text = messages(catalog.config.language);
|
|
339
|
+
const { files } = await changedFiles(catalog.root, base, text, "for --changed", { bothRenamePaths: true });
|
|
340
|
+
const touched = /* @__PURE__ */ new Map();
|
|
341
|
+
for (const file of files) for (const rule of rulesForPath(catalog.rules, file)) touched.set(rule.id, [...touched.get(rule.id) ?? [], file]);
|
|
342
|
+
const rules = sortById(catalog.rules.filter((rule) => rule.status === "active")).flatMap((rule) => {
|
|
343
|
+
const ruleFiles = touched.get(rule.id);
|
|
344
|
+
return ruleFiles === void 0 ? [] : [{
|
|
345
|
+
...toSummary(rule),
|
|
346
|
+
file: rule.file,
|
|
347
|
+
files: ruleFiles
|
|
348
|
+
}];
|
|
349
|
+
});
|
|
350
|
+
if (json) {
|
|
351
|
+
writeJson(io, {
|
|
352
|
+
base,
|
|
353
|
+
files,
|
|
354
|
+
rules
|
|
355
|
+
});
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (rules.length === 0) {
|
|
359
|
+
writeLines(io, [text.noActiveRulesForChanged(base)]);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
writeLines(io, rules.flatMap((rule) => [`${rule.id} (${rule.context}): ${rule.summary}`, ...indent(rule.files, text.none)]));
|
|
363
|
+
}
|
|
364
|
+
async function regionsIn(root, path) {
|
|
365
|
+
const found = (await scanImplementations(root, [path])).regions;
|
|
366
|
+
return (id) => (found.get(id) ?? []).map(({ line, endLine }) => ({
|
|
367
|
+
line,
|
|
368
|
+
endLine
|
|
369
|
+
}));
|
|
370
|
+
}
|
|
371
|
+
function formatSpans(spans) {
|
|
372
|
+
return spans.map(({ line, endLine }) => `${line}-${endLine}`).join(", ");
|
|
373
|
+
}
|
|
374
|
+
async function impactCommand(catalog, io, id, json) {
|
|
375
|
+
const rule = findRule(catalog, id);
|
|
376
|
+
const text = messages(catalog.config.language);
|
|
377
|
+
const warn = gitFallbackWarning(io, catalog.config.language);
|
|
378
|
+
const files = await expandGlobs(catalog.root, rule.anchors, warn);
|
|
379
|
+
const tests = (await scanTestTags(catalog.root, catalog.config.testGlobs, warn)).get(id) ?? [];
|
|
380
|
+
if (json) {
|
|
381
|
+
writeJson(io, {
|
|
382
|
+
id,
|
|
383
|
+
files,
|
|
384
|
+
tests
|
|
385
|
+
});
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
writeLines(io, [
|
|
389
|
+
`${rule.id}: ${rule.summary}`,
|
|
390
|
+
text.filesMatched(files.length),
|
|
391
|
+
...indent(files, text.none),
|
|
392
|
+
text.testsTagging(rule.id, tests.length),
|
|
393
|
+
...indent(tests.map((test) => `${test.file}:${test.line}`), text.none)
|
|
394
|
+
]);
|
|
395
|
+
}
|
|
396
|
+
function toRootPath(catalog, absolute, input) {
|
|
397
|
+
const path = toRepoPath(relative(catalog.root, absolute));
|
|
398
|
+
if (path === "" || path === ".." || path.startsWith("../") || isAbsolute(path)) throw new CliError(messages(catalog.config.language).outsideRoot(input, catalog.root));
|
|
399
|
+
return path;
|
|
400
|
+
}
|
|
401
|
+
function toSummary(rule) {
|
|
402
|
+
return {
|
|
403
|
+
id: rule.id,
|
|
404
|
+
context: rule.context,
|
|
405
|
+
status: rule.status,
|
|
406
|
+
summary: rule.summary
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
function indent(lines, none) {
|
|
410
|
+
return lines.length === 0 ? [` ${none}`] : lines.map((line) => ` ${line}`);
|
|
411
|
+
}
|
|
412
|
+
function sortById(rules) {
|
|
413
|
+
return [...rules].sort((a, b) => compare(a.id, b.id));
|
|
414
|
+
}
|
|
415
|
+
function compare(a, b) {
|
|
416
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
417
|
+
}
|
|
10
418
|
// src/commands/hook.ts
|
|
11
419
|
const HOOK_BODY_LIMIT = 1e4;
|
|
12
420
|
const TEXT = {
|
|
13
421
|
en: {
|
|
14
422
|
header: (path) => `Business rules anchored to ${path} (rulebookguard). They are the source of truth: keep any change to this file consistent with them, and change the rule first if a change alters its behavior.`,
|
|
15
423
|
ruleFile: "Rule file",
|
|
424
|
+
lines: "Lines",
|
|
16
425
|
omitted: `Rule texts omitted (over ${HOOK_BODY_LIMIT} characters); run \`rulebookguard show <id>\` for the full text.`
|
|
17
426
|
},
|
|
18
427
|
es: {
|
|
19
428
|
header: (path) => `Reglas de negocio ancladas a ${path} (rulebookguard). Son la fuente de verdad: mantén cualquier cambio a este archivo coherente con ellas y, si un cambio altera su comportamiento, cambia primero la regla.`,
|
|
20
429
|
ruleFile: "Archivo de la regla",
|
|
430
|
+
lines: "Líneas",
|
|
21
431
|
omitted: `Se omitió el texto de las reglas (más de ${HOOK_BODY_LIMIT} caracteres); ejecuta \`rulebookguard show <id>\` para verlo completo.`
|
|
22
432
|
}
|
|
23
433
|
};
|
|
@@ -42,7 +452,7 @@ async function hookContext(event, cwd) {
|
|
|
42
452
|
const repoPath = path.split(sep).join("/");
|
|
43
453
|
const rules = rulesForPath(catalog.rules, repoPath).filter((rule) => rule.status === "active");
|
|
44
454
|
if (rules.length === 0) return;
|
|
45
|
-
return render(repoPath, rules, TEXT[catalog.config.language ?? "en"]);
|
|
455
|
+
return render(repoPath, rules, await regionsIn(root, repoPath).catch(() => () => []), TEXT[catalog.config.language ?? "en"]);
|
|
46
456
|
}
|
|
47
457
|
function parseEvent(input) {
|
|
48
458
|
let event;
|
|
@@ -65,10 +475,15 @@ function parseEvent(input) {
|
|
|
65
475
|
filePath
|
|
66
476
|
};
|
|
67
477
|
}
|
|
68
|
-
function render(path, rules, text) {
|
|
478
|
+
function render(path, rules, regions, text) {
|
|
69
479
|
const withBodies = rules.reduce((total, rule) => total + rule.body.trim().length, 0) <= HOOK_BODY_LIMIT;
|
|
70
480
|
const sections = rules.map((rule) => {
|
|
71
|
-
const
|
|
481
|
+
const spans = regions(rule.id);
|
|
482
|
+
const lines = [
|
|
483
|
+
`## ${rule.id} (${rule.context}): ${rule.summary}`,
|
|
484
|
+
`${text.ruleFile}: ${rule.file}`,
|
|
485
|
+
...spans.length === 0 ? [] : [`${text.lines}: ${formatSpans(spans)}`]
|
|
486
|
+
];
|
|
72
487
|
return withBodies ? [
|
|
73
488
|
...lines,
|
|
74
489
|
"",
|
|
@@ -80,7 +495,7 @@ function render(path, rules, text) {
|
|
|
80
495
|
return parts.join("\n\n");
|
|
81
496
|
}
|
|
82
497
|
// package.json
|
|
83
|
-
var version = "0.
|
|
498
|
+
var version = "0.10.0";
|
|
84
499
|
// src/commands/new.ts
|
|
85
500
|
const MAX_NUMBER = 999;
|
|
86
501
|
const CONTEXT_NAME = /^[a-z0-9][a-z0-9_-]*$/;
|
|
@@ -271,9 +686,53 @@ function createRulesServer(projectDir) {
|
|
|
271
686
|
id,
|
|
272
687
|
"--json"
|
|
273
688
|
]));
|
|
689
|
+
server.registerTool("contexts", {
|
|
690
|
+
description: "L0 map of the rulebook: each context with its prefix, summary and number of rules. Start here to find the contexts a change touches.",
|
|
691
|
+
inputSchema: z.object({}),
|
|
692
|
+
annotations: { readOnlyHint: true }
|
|
693
|
+
}, () => cli(projectDir, ["contexts", "--json"]));
|
|
694
|
+
server.registerTool("rules_list", {
|
|
695
|
+
description: "L1 list of rules: id, context, status and summary, optionally narrowed to one context and one status. Read the full text of the relevant ones with rules_show.",
|
|
696
|
+
inputSchema: z.object({
|
|
697
|
+
context: z.string().optional().describe("Context name, for example clients"),
|
|
698
|
+
status: z.enum([
|
|
699
|
+
"proposed",
|
|
700
|
+
"active",
|
|
701
|
+
"deprecated"
|
|
702
|
+
]).optional()
|
|
703
|
+
}),
|
|
704
|
+
annotations: { readOnlyHint: true }
|
|
705
|
+
}, ({ context, status }) => cli(projectDir, [
|
|
706
|
+
"list",
|
|
707
|
+
...context === void 0 ? [] : ["--context", context],
|
|
708
|
+
...status === void 0 ? [] : ["--status", status],
|
|
709
|
+
"--json"
|
|
710
|
+
]));
|
|
711
|
+
server.registerTool("rules_search", {
|
|
712
|
+
description: "Rules in any status whose id, summary, entities or text hold every word of the text, ignoring case (plain text search, no embeddings). Use it to find existing rules before proposing a new one.",
|
|
713
|
+
inputSchema: z.object({
|
|
714
|
+
text: z.string().describe("Words to look for, for example \"invoice number\""),
|
|
715
|
+
context: z.string().optional().describe("Context name to search in")
|
|
716
|
+
}),
|
|
717
|
+
annotations: { readOnlyHint: true }
|
|
718
|
+
}, ({ text, context }) => cli(projectDir, [
|
|
719
|
+
"search",
|
|
720
|
+
text,
|
|
721
|
+
...context === void 0 ? [] : ["--context", context],
|
|
722
|
+
"--json"
|
|
723
|
+
]));
|
|
724
|
+
server.registerTool("lint", {
|
|
725
|
+
description: "Checks the whole rulebook (schema, anchors, test tags, relations between rules) and answers the findings with a hint each; with base, rules changed since that git ref also need a changed test. Findings are data: the tool fails only when lint cannot run.",
|
|
726
|
+
inputSchema: z.object({ base: z.string().optional().describe("Git ref to compare with, for example origin/main") }),
|
|
727
|
+
annotations: { readOnlyHint: true }
|
|
728
|
+
}, ({ base }) => cli(projectDir, [
|
|
729
|
+
"lint",
|
|
730
|
+
...base === void 0 ? [] : ["--base", base],
|
|
731
|
+
"--json"
|
|
732
|
+
], { findingsAreData: true }));
|
|
274
733
|
return server;
|
|
275
734
|
}
|
|
276
|
-
async function cli(cwd, args) {
|
|
735
|
+
async function cli(cwd, args, { findingsAreData = false } = {}) {
|
|
277
736
|
let stdout = "";
|
|
278
737
|
let stderr = "";
|
|
279
738
|
if (await runCli(args, {
|
|
@@ -284,7 +743,7 @@ async function cli(cwd, args) {
|
|
|
284
743
|
stderr: (text) => {
|
|
285
744
|
stderr += text;
|
|
286
745
|
}
|
|
287
|
-
}) === 0) return { content: [{
|
|
746
|
+
}) === 0 || findingsAreData && stdout !== "") return { content: [{
|
|
288
747
|
type: "text",
|
|
289
748
|
text: stdout
|
|
290
749
|
}] };
|
|
@@ -300,136 +759,6 @@ async function cli(cwd, args) {
|
|
|
300
759
|
async function mcpCommand(io, project) {
|
|
301
760
|
await createRulesServer(resolve(io.cwd, project ?? ".")).connect(new StdioServerTransport());
|
|
302
761
|
}
|
|
303
|
-
// src/commands/read.ts
|
|
304
|
-
const RULE_STATUSES = [
|
|
305
|
-
"proposed",
|
|
306
|
-
"active",
|
|
307
|
-
"deprecated"
|
|
308
|
-
];
|
|
309
|
-
function contextsCommand(catalog, io, json) {
|
|
310
|
-
const contexts = [...catalog.contexts].sort((a, b) => compare(a.context, b.context)).map(({ context, prefix, summary, file }) => ({
|
|
311
|
-
context,
|
|
312
|
-
prefix,
|
|
313
|
-
summary,
|
|
314
|
-
rules: catalog.rules.filter((rule) => rule.context === context).length,
|
|
315
|
-
file
|
|
316
|
-
}));
|
|
317
|
-
if (json) {
|
|
318
|
-
writeJson(io, { contexts });
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
if (contexts.length === 0) {
|
|
322
|
-
writeLines(io, [messages(catalog.config.language).noContexts]);
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
writeLines(io, contexts.map((item) => `${item.context} (${item.prefix}, ${messages(catalog.config.language).ruleCount(item.rules)}): ${item.summary}`));
|
|
326
|
-
}
|
|
327
|
-
function listCommand(catalog, io, options) {
|
|
328
|
-
const { context, status } = options;
|
|
329
|
-
const text = messages(catalog.config.language);
|
|
330
|
-
if (status !== void 0 && !RULE_STATUSES.includes(status)) throw new CliError(text.invalidStatus(status, RULE_STATUSES.join(", ")), 2);
|
|
331
|
-
if (context !== void 0 && !catalog.contexts.some((item) => item.context === context)) throw new CliError(text.unknownContext(context));
|
|
332
|
-
const rules = sortById(catalog.rules).filter((rule) => context === void 0 || rule.context === context).filter((rule) => status === void 0 || rule.status === status).map(toSummary);
|
|
333
|
-
if (options.json) {
|
|
334
|
-
writeJson(io, { rules });
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
if (rules.length === 0) {
|
|
338
|
-
writeLines(io, [text.noRules]);
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
writeLines(io, rules.map((rule) => `${rule.id} [${rule.status}] ${rule.summary}`));
|
|
342
|
-
}
|
|
343
|
-
function showCommand(catalog, io, id, json) {
|
|
344
|
-
const rule = findRule(catalog, id);
|
|
345
|
-
if (json) {
|
|
346
|
-
writeJson(io, { rule: {
|
|
347
|
-
...toSummary(rule),
|
|
348
|
-
entities: rule.entities,
|
|
349
|
-
anchors: rule.anchors,
|
|
350
|
-
supersedes: rule.supersedes ?? null,
|
|
351
|
-
file: rule.file,
|
|
352
|
-
body: rule.body
|
|
353
|
-
} });
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
const none = messages(catalog.config.language).none;
|
|
357
|
-
const header = [
|
|
358
|
-
`${rule.id}: ${rule.summary}`,
|
|
359
|
-
`context: ${rule.context}`,
|
|
360
|
-
`status: ${rule.status}`,
|
|
361
|
-
`entities: ${rule.entities.length === 0 ? none : rule.entities.join(", ")}`,
|
|
362
|
-
"anchors:",
|
|
363
|
-
...indent(rule.anchors, none),
|
|
364
|
-
...rule.supersedes === void 0 ? [] : [`supersedes: ${rule.supersedes}`],
|
|
365
|
-
`file: ${rule.file}`
|
|
366
|
-
];
|
|
367
|
-
io.stdout(`${header.join("\n")}\n\n${rule.body}`);
|
|
368
|
-
}
|
|
369
|
-
function forCommand(catalog, io, input, json) {
|
|
370
|
-
const text = messages(catalog.config.language);
|
|
371
|
-
const path = toRootPath(catalog, resolve(io.cwd, input), input);
|
|
372
|
-
const rules = rulesForPath(catalog.rules, path).filter((rule) => rule.status === "active").map((rule) => ({
|
|
373
|
-
...toSummary(rule),
|
|
374
|
-
file: rule.file
|
|
375
|
-
}));
|
|
376
|
-
if (json) {
|
|
377
|
-
writeJson(io, {
|
|
378
|
-
path,
|
|
379
|
-
rules
|
|
380
|
-
});
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
if (rules.length === 0) {
|
|
384
|
-
writeLines(io, [text.noActiveRulesFor(path)]);
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
writeLines(io, rules.map((rule) => `${rule.id} (${rule.context}): ${rule.summary}`));
|
|
388
|
-
}
|
|
389
|
-
async function impactCommand(catalog, io, id, json) {
|
|
390
|
-
const rule = findRule(catalog, id);
|
|
391
|
-
const text = messages(catalog.config.language);
|
|
392
|
-
const warn = gitFallbackWarning(io, catalog.config.language);
|
|
393
|
-
const files = await expandGlobs(catalog.root, rule.anchors, warn);
|
|
394
|
-
const tests = (await scanTestTags(catalog.root, catalog.config.testGlobs, warn)).get(id) ?? [];
|
|
395
|
-
if (json) {
|
|
396
|
-
writeJson(io, {
|
|
397
|
-
id,
|
|
398
|
-
files,
|
|
399
|
-
tests
|
|
400
|
-
});
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
writeLines(io, [
|
|
404
|
-
`${rule.id}: ${rule.summary}`,
|
|
405
|
-
text.filesMatched(files.length),
|
|
406
|
-
...indent(files, text.none),
|
|
407
|
-
text.testsTagging(rule.id, tests.length),
|
|
408
|
-
...indent(tests.map((test) => `${test.file}:${test.line}`), text.none)
|
|
409
|
-
]);
|
|
410
|
-
}
|
|
411
|
-
function toRootPath(catalog, absolute, input) {
|
|
412
|
-
const path = toRepoPath(relative(catalog.root, absolute));
|
|
413
|
-
if (path === "" || path === ".." || path.startsWith("../") || isAbsolute(path)) throw new CliError(messages(catalog.config.language).outsideRoot(input, catalog.root));
|
|
414
|
-
return path;
|
|
415
|
-
}
|
|
416
|
-
function toSummary(rule) {
|
|
417
|
-
return {
|
|
418
|
-
id: rule.id,
|
|
419
|
-
context: rule.context,
|
|
420
|
-
status: rule.status,
|
|
421
|
-
summary: rule.summary
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
function indent(lines, none) {
|
|
425
|
-
return lines.length === 0 ? [` ${none}`] : lines.map((line) => ` ${line}`);
|
|
426
|
-
}
|
|
427
|
-
function sortById(rules) {
|
|
428
|
-
return [...rules].sort((a, b) => compare(a.id, b.id));
|
|
429
|
-
}
|
|
430
|
-
function compare(a, b) {
|
|
431
|
-
return a < b ? -1 : a > b ? 1 : 0;
|
|
432
|
-
}
|
|
433
762
|
// src/commands/run.ts
|
|
434
763
|
const USAGE = `Usage: rulebookguard <command> [options]
|
|
435
764
|
|
|
@@ -442,17 +771,31 @@ Commands:
|
|
|
442
771
|
contexts [--json] L0: list the contexts
|
|
443
772
|
list [--context <c>] [--status <s>] [--json] L1: id and summary of each rule
|
|
444
773
|
show <id> [--json] L2: the full rule
|
|
774
|
+
search <text> [--context <c>] [--json] rules whose id, summary, entities or text hold every
|
|
775
|
+
word of <text>, ignoring case
|
|
445
776
|
for <path> [--json] active rules anchored to a path
|
|
777
|
+
for --changed <ref> [--json] active rules anchored to the files changed since <ref>,
|
|
778
|
+
each with those files
|
|
446
779
|
impact <id> [--json] files matched by a rule's anchors and tests tagging it
|
|
447
780
|
new <context> [--prefix <P>] scaffold the next free rule id as a proposed rule;
|
|
448
781
|
creates the context first when it does not exist,
|
|
449
782
|
--prefix: its id prefix (default: first 3 letters)
|
|
450
783
|
lint [--base <ref>] [--json] check the catalog, anchors and test tags;
|
|
451
784
|
--base: rules changed since <ref> need a changed test
|
|
785
|
+
export gherkin [--out <dir>] write the Gherkin blocks of each non-deprecated rule as
|
|
786
|
+
<out>/<context>/<id>.feature tagged @<id>;
|
|
787
|
+
--out: output directory (default: features/rules)
|
|
788
|
+
export cursor|copilot|kiro [--out <dir>] write each active rule as a rule file of the tool, scoped
|
|
789
|
+
to its anchors, in .cursor/rules, .github/instructions
|
|
790
|
+
or .kiro/steering, and remove those of rules no longer
|
|
791
|
+
active; --out: output directory
|
|
792
|
+
export agents-md [--out <dir>] keep an index of the active rules in a marked block of
|
|
793
|
+
AGENTS.md; --out: its directory (default: rulebook root)
|
|
452
794
|
hook Claude Code hook for PreToolUse and PostToolUse: reads
|
|
453
795
|
the event on stdin and prints the rules anchored to
|
|
454
796
|
the file of the tool (Read, Edit, Write...)
|
|
455
|
-
mcp [--project <dir>] MCP server over stdio with rules_for
|
|
797
|
+
mcp [--project <dir>] MCP server over stdio with rules_for, rules_show, contexts,
|
|
798
|
+
rules_list, rules_search and lint;
|
|
456
799
|
--project: directory to find the rulebook from (default: cwd)
|
|
457
800
|
site:generate [--out <dir>] [--base <path>] build the read-only static site of the rulebook (alias: site);
|
|
458
801
|
--out: output directory (default: site), --base: URL path
|
|
@@ -495,10 +838,22 @@ const COMMANDS = {
|
|
|
495
838
|
argument: "id",
|
|
496
839
|
options: JSON_OPTION
|
|
497
840
|
},
|
|
841
|
+
search: {
|
|
842
|
+
usage: "rulebookguard search <text> [--context <c>] [--json]",
|
|
843
|
+
argument: "text",
|
|
844
|
+
options: {
|
|
845
|
+
...JSON_OPTION,
|
|
846
|
+
context: { type: "string" }
|
|
847
|
+
}
|
|
848
|
+
},
|
|
498
849
|
for: {
|
|
499
|
-
usage: "rulebookguard for <path> [--json]",
|
|
850
|
+
usage: "rulebookguard for <path>|--changed <ref> [--json]",
|
|
500
851
|
argument: "path",
|
|
501
|
-
|
|
852
|
+
argumentUnless: "changed",
|
|
853
|
+
options: {
|
|
854
|
+
...JSON_OPTION,
|
|
855
|
+
changed: { type: "string" }
|
|
856
|
+
}
|
|
502
857
|
},
|
|
503
858
|
impact: {
|
|
504
859
|
usage: "rulebookguard impact <id> [--json]",
|
|
@@ -510,6 +865,11 @@ const COMMANDS = {
|
|
|
510
865
|
argument: "context",
|
|
511
866
|
options: { prefix: { type: "string" } }
|
|
512
867
|
},
|
|
868
|
+
export: {
|
|
869
|
+
usage: EXPORT_USAGE,
|
|
870
|
+
argument: "format",
|
|
871
|
+
options: { out: { type: "string" } }
|
|
872
|
+
},
|
|
513
873
|
hook: {
|
|
514
874
|
usage: "rulebookguard hook",
|
|
515
875
|
options: {}
|
|
@@ -575,7 +935,7 @@ async function dispatch(argv, io) {
|
|
|
575
935
|
return 0;
|
|
576
936
|
}
|
|
577
937
|
if (name === "site" || name === "site:generate" || name === "site:run") {
|
|
578
|
-
const { siteCommand } = await import("./site-
|
|
938
|
+
const { siteCommand } = await import("./site-B7g8lbjx.mjs");
|
|
579
939
|
await siteCommand(io, {
|
|
580
940
|
out: values.out,
|
|
581
941
|
base: values.base,
|
|
@@ -596,6 +956,7 @@ async function dispatch(argv, io) {
|
|
|
596
956
|
base: values.base,
|
|
597
957
|
json
|
|
598
958
|
});
|
|
959
|
+
if (name === "search" && argument.trim() === "") throw new CliError(`Search text is empty. Usage: ${spec.usage}`, 2);
|
|
599
960
|
const catalog = await openCatalog(io);
|
|
600
961
|
switch (name) {
|
|
601
962
|
case "contexts":
|
|
@@ -609,15 +970,25 @@ async function dispatch(argv, io) {
|
|
|
609
970
|
});
|
|
610
971
|
break;
|
|
611
972
|
case "show":
|
|
612
|
-
showCommand(catalog, io, argument, json);
|
|
973
|
+
await showCommand(catalog, io, argument, json);
|
|
974
|
+
break;
|
|
975
|
+
case "search":
|
|
976
|
+
searchCommand(catalog, io, argument, {
|
|
977
|
+
context: values.context,
|
|
978
|
+
json
|
|
979
|
+
});
|
|
613
980
|
break;
|
|
614
981
|
case "for":
|
|
615
|
-
forCommand(catalog, io, argument, json);
|
|
982
|
+
if (values.changed === void 0) await forCommand(catalog, io, argument, json);
|
|
983
|
+
else await forChangedCommand(catalog, io, values.changed, json);
|
|
616
984
|
break;
|
|
617
985
|
case "impact":
|
|
618
986
|
await impactCommand(catalog, io, argument, json);
|
|
619
987
|
break;
|
|
620
|
-
case "new":
|
|
988
|
+
case "new":
|
|
989
|
+
await newCommand(catalog, io, argument, values.prefix);
|
|
990
|
+
break;
|
|
991
|
+
case "export": await exportCommand(catalog, io, argument, values.out);
|
|
621
992
|
}
|
|
622
993
|
return 0;
|
|
623
994
|
}
|
|
@@ -643,7 +1014,8 @@ function parse(spec, args) {
|
|
|
643
1014
|
} catch (error) {
|
|
644
1015
|
throw new CliError(`${error.message}\nUsage: ${spec.usage}`, 2);
|
|
645
1016
|
}
|
|
646
|
-
const
|
|
1017
|
+
const replaced = spec.argumentUnless !== void 0 && parsed.values[spec.argumentUnless] !== void 0;
|
|
1018
|
+
const expected = spec.argument === void 0 || replaced ? 0 : 1;
|
|
647
1019
|
if (parsed.positionals.length < expected) throw new CliError(`Missing argument <${spec.argument}>. Usage: ${spec.usage}`, 2);
|
|
648
1020
|
if (parsed.positionals.length > expected) throw new CliError(`Unexpected argument "${parsed.positionals[expected]}". Usage: ${spec.usage}`, 2);
|
|
649
1021
|
return {
|