@devrik-tools/claude-gates 0.3.1 → 0.6.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/.claude-plugin/marketplace.json +2 -2
- package/README.es.md +25 -4
- package/README.md +75 -43
- package/cli/config.mjs +126 -124
- package/cli/index.mjs +154 -104
- package/cli/init.mjs +303 -276
- package/cli/install.mjs +281 -175
- package/cli/materialize.mjs +103 -102
- package/cli/registry.mjs +139 -136
- package/cli/smoke-fixtures.json +441 -0
- package/cli/smoke.mjs +129 -0
- package/cli/task.mjs +140 -140
- package/package.json +1 -1
- package/plugins/gates/.claude-plugin/plugin.json +1 -1
- package/plugins/gates/hooks/ask-adoption.mjs +147 -147
- package/plugins/gates/hooks/doctor.mjs +207 -207
- package/plugins/gates/hooks/gates/atomic-commit/index.mjs +229 -0
- package/plugins/gates/hooks/gates/audit-before-build/index.mjs +110 -88
- package/plugins/gates/hooks/gates/autonomous-mode/index.mjs +50 -50
- package/plugins/gates/hooks/gates/bash-commands/index.mjs +215 -215
- package/plugins/gates/hooks/gates/brief-before-delegate/index.mjs +269 -265
- package/plugins/gates/hooks/gates/capability-map/index.mjs +377 -0
- package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +527 -501
- package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +48 -43
- package/plugins/gates/hooks/gates/feature-catalog/index.mjs +83 -83
- package/plugins/gates/hooks/gates/force-parallel/index.mjs +134 -119
- package/plugins/gates/hooks/gates/forge-flow/index.mjs +134 -134
- package/plugins/gates/hooks/gates/implementation-pipeline/index.mjs +187 -187
- package/plugins/gates/hooks/gates/intent-flow/index.mjs +260 -260
- package/plugins/gates/hooks/gates/lint-commit/index.mjs +152 -149
- package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +180 -180
- package/plugins/gates/hooks/gates/never-assume/index.mjs +59 -58
- package/plugins/gates/hooks/gates/no-blocking/index.mjs +163 -148
- package/plugins/gates/hooks/gates/no-coauthor/index.mjs +127 -0
- package/plugins/gates/hooks/gates/no-lint-suppression/index.mjs +183 -0
- package/plugins/gates/hooks/gates/protected-paths/index.mjs +149 -144
- package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +91 -89
- package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +263 -159
- package/plugins/gates/hooks/gates/risk-level/index.mjs +265 -263
- package/plugins/gates/hooks/gates/root-cause-first/index.mjs +57 -56
- package/plugins/gates/hooks/gates/root-whitelist/index.mjs +211 -131
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +181 -184
- package/plugins/gates/hooks/gates/sdd-specs/index.mjs +256 -256
- package/plugins/gates/hooks/gates/staged-lint/index.mjs +187 -0
- package/plugins/gates/hooks/gates/stop-pending/index.mjs +169 -164
- package/plugins/gates/hooks/gates/test-matrix/index.mjs +187 -187
- package/plugins/gates/hooks/gates/tool-map/index.mjs +168 -143
- package/plugins/gates/hooks/hooks.json +51 -0
- package/plugins/gates/hooks/lib/config.mjs +179 -172
- package/plugins/gates/hooks/lib/hook-io.mjs +367 -357
- package/plugins/gates/hooks/lib/signals.mjs +172 -127
- package/plugins/gates/hooks/wiring-check.mjs +227 -227
- package/plugins/tasks/.claude-plugin/plugin.json +1 -1
- package/plugins/tasks/hooks/hooks.json +26 -26
- package/plugins/tasks/hooks/lib/task-store.mjs +217 -197
- package/plugins/tasks/hooks/register-requests.mjs +145 -145
- package/plugins/tasks/hooks/session-tasks.mjs +108 -108
- package/registry.json +171 -1
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
// capability-map — UserPromptSubmit hook. Surfaces the REAL, current catalog of the project's
|
|
2
|
+
// AI capabilities — skills, agents/subagents, and commands — as compact data ("caveman"
|
|
3
|
+
// format: `name — first clause`, one line each, grouped by kind), read fresh from disk on
|
|
4
|
+
// every message. It also PERSISTS that catalog to .ai/capability-map.json so the map is easy
|
|
5
|
+
// to follow and read the same way the tool map (.ai/tool-map.json) is, and so another tool or
|
|
6
|
+
// a human can consult it without re-scanning. Autosynced by construction: add or remove a
|
|
7
|
+
// skill/agent/command file and the next message reflects it, because the map IS the directory
|
|
8
|
+
// listing, never a hardcoded copy.
|
|
9
|
+
//
|
|
10
|
+
// It does NOT tell the model to obey a reminder — that would be prose the model may ignore,
|
|
11
|
+
// the exact antipattern guard-no-model-reliance forbids. It injects a fact (which capabilities
|
|
12
|
+
// exist) and lets the assistant decide to use them.
|
|
13
|
+
//
|
|
14
|
+
// justification: no existing gate covers this. rule-skill-autodiscovery EXECUTES sub-gate
|
|
15
|
+
// scripts found under skills; dependency-skills cross-checks package.json deps against skill
|
|
16
|
+
// dirs; tool-map records built TOOLS (scripts) into .ai/tool-map.json. None enumerate the
|
|
17
|
+
// skill/agent/command catalog or inject it as context. This is the read+inject counterpart to
|
|
18
|
+
// tool-map for the capability catalog.
|
|
19
|
+
//
|
|
20
|
+
// A UserPromptSubmit hook's stdout is appended to the assistant's context as plain text, so
|
|
21
|
+
// the catalog is simply written to stdout (no PreToolUse JSON shape). Self-contained: Node
|
|
22
|
+
// built-ins only.
|
|
23
|
+
//
|
|
24
|
+
// ── What it discovers (every root: ~/.claude and <project>/.claude, plus config extras) ──
|
|
25
|
+
// skills <root>/skills/<name>/SKILL.md front matter name + description
|
|
26
|
+
// agents <root>/agents/*.md front matter name (falls back to file basename)
|
|
27
|
+
// + description; subagents nested deeper too
|
|
28
|
+
// commands <root>/commands/*.{md,toml} front matter description; name is the basename
|
|
29
|
+
// Each injected line is `name — first clause` (up to the first '. ' or a hard char cap), so
|
|
30
|
+
// the whole catalog stays cheap even at dozens of entries.
|
|
31
|
+
//
|
|
32
|
+
// ── What a project can configure (params) — everything is customizable ───────────────
|
|
33
|
+
// kinds which capability kinds to include, e.g. ["skills","agents","commands"].
|
|
34
|
+
// Drop one to stop scanning it entirely.
|
|
35
|
+
// maxClauseChars hard cap on each entry's one-line blurb (default 90).
|
|
36
|
+
// extraSkillsDirs / extraAgentsDirs / extraCommandsDirs additional roots per kind
|
|
37
|
+
// (relative to project or absolute), added on top of the built-in ones.
|
|
38
|
+
// persist whether to write .ai/capability-map.json (default true).
|
|
39
|
+
// mapFile path to the persisted map, relative to project root. Default
|
|
40
|
+
// .ai/capability-map.json.
|
|
41
|
+
//
|
|
42
|
+
// ── Fail-safe shape ──────────────────────────────────────────────────────────────────
|
|
43
|
+
// Nothing found anywhere, gate disabled, or unreadable input: inject nothing (silent, no
|
|
44
|
+
// wasted context) — and never write an empty map over a good one. Persistence failure is
|
|
45
|
+
// swallowed: the injection is the job, the file is a convenience. Never blocks —
|
|
46
|
+
// UserPromptSubmit cannot deny; it only adds context.
|
|
47
|
+
|
|
48
|
+
import {
|
|
49
|
+
existsSync,
|
|
50
|
+
mkdirSync,
|
|
51
|
+
readFileSync,
|
|
52
|
+
readdirSync,
|
|
53
|
+
writeFileSync,
|
|
54
|
+
} from 'node:fs';
|
|
55
|
+
import { homedir } from 'node:os';
|
|
56
|
+
import { basename, dirname, extname, isAbsolute, join } from 'node:path';
|
|
57
|
+
|
|
58
|
+
const STDIN_FILE_DESCRIPTOR = 0;
|
|
59
|
+
const CONFIG_KEY = 'injectCapabilityMap';
|
|
60
|
+
const DUMP_ENV = 'CLAUDE_GATES_DUMP_DEFAULTS';
|
|
61
|
+
const DEFAULT_MAX_CLAUSE_CHARS = 90;
|
|
62
|
+
const DEFAULT_KINDS = ['skills', 'agents', 'commands'];
|
|
63
|
+
const DEFAULT_MAP_FILE = join('.ai', 'capability-map.json');
|
|
64
|
+
const JSON_INDENT = 2;
|
|
65
|
+
|
|
66
|
+
// Config lookup mirrors config.mjs (project → global), kept local so a gate stays runnable on
|
|
67
|
+
// its own alongside the other event-scripts (doctor, wiring-check).
|
|
68
|
+
const PROJECT_ROOT_MARKERS = ['.git', '.ai'];
|
|
69
|
+
const PROJECT_CONFIG = join('.ai', 'config.json');
|
|
70
|
+
const GLOBAL_CONFIG = join('.claude', 'claude-gates', 'config.json');
|
|
71
|
+
|
|
72
|
+
function readJson(path) {
|
|
73
|
+
try {
|
|
74
|
+
return JSON.parse(readFileSync(path, 'utf8').replace(/^\uFEFF/, ''));
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function projectRootOf(startDirectory) {
|
|
81
|
+
let current = startDirectory;
|
|
82
|
+
while (true) {
|
|
83
|
+
if (
|
|
84
|
+
PROJECT_ROOT_MARKERS.some((marker) => existsSync(join(current, marker)))
|
|
85
|
+
) {
|
|
86
|
+
return current;
|
|
87
|
+
}
|
|
88
|
+
const parent = dirname(current);
|
|
89
|
+
if (parent === current) return null;
|
|
90
|
+
current = parent;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The capability-map gate config entry (project overrides global), or null. */
|
|
95
|
+
function gateConfig(startDirectory) {
|
|
96
|
+
const root = projectRootOf(startDirectory);
|
|
97
|
+
const projectData = root ? readJson(join(root, PROJECT_CONFIG)) : null;
|
|
98
|
+
const globalData = readJson(join(homedir(), GLOBAL_CONFIG));
|
|
99
|
+
const layer = projectData?.gates ?? globalData?.gates ?? {};
|
|
100
|
+
const entry = layer[CONFIG_KEY];
|
|
101
|
+
if (typeof entry === 'boolean') return { enabled: entry };
|
|
102
|
+
if (entry && typeof entry === 'object') return entry;
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function readPayload() {
|
|
107
|
+
try {
|
|
108
|
+
return JSON.parse(readFileSync(STDIN_FILE_DESCRIPTOR, 'utf8'));
|
|
109
|
+
} catch {
|
|
110
|
+
return {};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The `name` and `description` from a markdown-style front matter block (skills, agents,
|
|
116
|
+
* commands all use `---`-fenced YAML-ish front matter). Parsed line-by-line (no multi-line
|
|
117
|
+
* regex) so a large file body can never trigger catastrophic backtracking. Returns
|
|
118
|
+
* { name, description } with either possibly ''.
|
|
119
|
+
*/
|
|
120
|
+
function parseFrontMatter(fileText) {
|
|
121
|
+
const lines = fileText.split(/\r?\n/);
|
|
122
|
+
if (lines[0]?.trim() !== '---') return { name: '', description: '' };
|
|
123
|
+
|
|
124
|
+
let name = '';
|
|
125
|
+
let description = '';
|
|
126
|
+
for (let index = 1; index < lines.length; index += 1) {
|
|
127
|
+
const line = lines[index];
|
|
128
|
+
if (line.trim() === '---') break; // end of front matter
|
|
129
|
+
const separator = line.indexOf(':');
|
|
130
|
+
if (separator < 0) continue;
|
|
131
|
+
const key = line.slice(0, separator).trim();
|
|
132
|
+
const value = line
|
|
133
|
+
.slice(separator + 1)
|
|
134
|
+
.trim()
|
|
135
|
+
.replace(/^["']|["']$/g, ''); // toml/yaml quoting around the value
|
|
136
|
+
if (key === 'name') name = value;
|
|
137
|
+
else if (key === 'description') description = value;
|
|
138
|
+
}
|
|
139
|
+
return { name, description };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** The description's first clause, capped — the caveman blurb. */
|
|
143
|
+
function firstClause(description, maxClauseChars) {
|
|
144
|
+
if (!description) return '';
|
|
145
|
+
const sentenceEnd = description.indexOf('. ');
|
|
146
|
+
const clause =
|
|
147
|
+
sentenceEnd > 0 ? description.slice(0, sentenceEnd) : description;
|
|
148
|
+
return clause.length > maxClauseChars
|
|
149
|
+
? `${clause.slice(0, maxClauseChars - 1).trimEnd()}…`
|
|
150
|
+
: clause;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Recursively lists files under a directory whose extension is in `extensions`. */
|
|
154
|
+
function filesUnder(directory, extensions) {
|
|
155
|
+
if (!existsSync(directory)) return [];
|
|
156
|
+
let entries;
|
|
157
|
+
try {
|
|
158
|
+
entries = readdirSync(directory, { withFileTypes: true });
|
|
159
|
+
} catch {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
const files = [];
|
|
163
|
+
for (const entry of entries) {
|
|
164
|
+
const full = join(directory, entry.name);
|
|
165
|
+
if (entry.isDirectory()) {
|
|
166
|
+
files.push(...filesUnder(full, extensions));
|
|
167
|
+
} else if (extensions.includes(extname(entry.name).toLowerCase())) {
|
|
168
|
+
files.push(full);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return files;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Skill capabilities under one root: <root>/skills/<name>/SKILL.md. */
|
|
175
|
+
function skillEntriesUnder(skillsRoot, maxClauseChars) {
|
|
176
|
+
if (!existsSync(skillsRoot)) return [];
|
|
177
|
+
let skillDirectories;
|
|
178
|
+
try {
|
|
179
|
+
skillDirectories = readdirSync(skillsRoot, { withFileTypes: true });
|
|
180
|
+
} catch {
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
const entries = [];
|
|
184
|
+
for (const skillDirectory of skillDirectories) {
|
|
185
|
+
if (!skillDirectory.isDirectory()) continue;
|
|
186
|
+
const skillFile = join(skillsRoot, skillDirectory.name, 'SKILL.md');
|
|
187
|
+
if (!existsSync(skillFile)) continue;
|
|
188
|
+
let content;
|
|
189
|
+
try {
|
|
190
|
+
content = readFileSync(skillFile, 'utf8');
|
|
191
|
+
} catch {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const { name, description } = parseFrontMatter(content);
|
|
195
|
+
entries.push({
|
|
196
|
+
name: name || skillDirectory.name,
|
|
197
|
+
blurb: firstClause(description, maxClauseChars),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
return entries;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Agent/command capabilities: flat or nested files whose front matter carries a description. */
|
|
204
|
+
function fileEntriesUnder(directory, extensions, maxClauseChars) {
|
|
205
|
+
const entries = [];
|
|
206
|
+
for (const file of filesUnder(directory, extensions)) {
|
|
207
|
+
let content;
|
|
208
|
+
try {
|
|
209
|
+
content = readFileSync(file, 'utf8');
|
|
210
|
+
} catch {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const { name, description } = parseFrontMatter(content);
|
|
214
|
+
entries.push({
|
|
215
|
+
name: name || basename(file, extname(file)),
|
|
216
|
+
blurb: firstClause(description, maxClauseChars),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
return entries;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function baseRoots(cwd) {
|
|
223
|
+
return [join(homedir(), '.claude'), join(cwd, '.claude')];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function resolveExtra(cwd, directory) {
|
|
227
|
+
return isAbsolute(directory) ? directory : join(cwd, directory);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Every entry for one kind across all roots, de-duplicated by name. */
|
|
231
|
+
function entriesForKind(kind, cwd, config, maxClauseChars) {
|
|
232
|
+
const perKind = {
|
|
233
|
+
skills: {
|
|
234
|
+
subdir: 'skills',
|
|
235
|
+
extra: config.extraSkillsDirs,
|
|
236
|
+
collect: (root) =>
|
|
237
|
+
skillEntriesUnder(join(root, 'skills'), maxClauseChars),
|
|
238
|
+
collectExtra: (directory) => skillEntriesUnder(directory, maxClauseChars),
|
|
239
|
+
},
|
|
240
|
+
agents: {
|
|
241
|
+
extra: config.extraAgentsDirs,
|
|
242
|
+
collect: (root) =>
|
|
243
|
+
fileEntriesUnder(join(root, 'agents'), ['.md'], maxClauseChars),
|
|
244
|
+
collectExtra: (directory) =>
|
|
245
|
+
fileEntriesUnder(directory, ['.md'], maxClauseChars),
|
|
246
|
+
},
|
|
247
|
+
commands: {
|
|
248
|
+
extra: config.extraCommandsDirs,
|
|
249
|
+
collect: (root) =>
|
|
250
|
+
fileEntriesUnder(
|
|
251
|
+
join(root, 'commands'),
|
|
252
|
+
['.md', '.toml'],
|
|
253
|
+
maxClauseChars,
|
|
254
|
+
),
|
|
255
|
+
collectExtra: (directory) =>
|
|
256
|
+
fileEntriesUnder(directory, ['.md', '.toml'], maxClauseChars),
|
|
257
|
+
},
|
|
258
|
+
}[kind];
|
|
259
|
+
if (!perKind) return [];
|
|
260
|
+
|
|
261
|
+
const collected = [];
|
|
262
|
+
for (const root of baseRoots(cwd)) collected.push(...perKind.collect(root));
|
|
263
|
+
const extra = Array.isArray(perKind.extra) ? perKind.extra : [];
|
|
264
|
+
for (const directory of extra) {
|
|
265
|
+
collected.push(...perKind.collectExtra(resolveExtra(cwd, directory)));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const seen = new Set();
|
|
269
|
+
const unique = [];
|
|
270
|
+
for (const entry of collected) {
|
|
271
|
+
if (seen.has(entry.name)) continue; // a capability present in two roots is listed once
|
|
272
|
+
seen.add(entry.name);
|
|
273
|
+
unique.push(entry);
|
|
274
|
+
}
|
|
275
|
+
unique.sort((a, b) => a.name.localeCompare(b.name));
|
|
276
|
+
return unique;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Persists the map to .ai/capability-map.json, best-effort — never throws, never blocks. */
|
|
280
|
+
function persistMap(cwd, mapFile, catalog) {
|
|
281
|
+
const root = projectRootOf(cwd);
|
|
282
|
+
if (!root) return;
|
|
283
|
+
const mapPath = join(root, mapFile);
|
|
284
|
+
const payload = {
|
|
285
|
+
generatedAt: new Date().toISOString(),
|
|
286
|
+
capabilities: catalog,
|
|
287
|
+
};
|
|
288
|
+
try {
|
|
289
|
+
mkdirSync(dirname(mapPath), { recursive: true });
|
|
290
|
+
writeFileSync(
|
|
291
|
+
mapPath,
|
|
292
|
+
`${JSON.stringify(payload, null, JSON_INDENT)}\n`,
|
|
293
|
+
'utf8',
|
|
294
|
+
);
|
|
295
|
+
} catch {
|
|
296
|
+
// The injection is the job; the persisted file is a convenience. A write failure must
|
|
297
|
+
// not break the turn.
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const KIND_LABELS = {
|
|
302
|
+
skills: 'skills',
|
|
303
|
+
agents: 'agents',
|
|
304
|
+
commands: 'commands',
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
function dumpDefaults() {
|
|
308
|
+
process.stdout.write(
|
|
309
|
+
JSON.stringify({
|
|
310
|
+
id: 'capability-map',
|
|
311
|
+
configKey: CONFIG_KEY,
|
|
312
|
+
enabledByDefault: false,
|
|
313
|
+
defaultParams: {
|
|
314
|
+
kinds: DEFAULT_KINDS,
|
|
315
|
+
maxClauseChars: DEFAULT_MAX_CLAUSE_CHARS,
|
|
316
|
+
extraSkillsDirs: [],
|
|
317
|
+
extraAgentsDirs: [],
|
|
318
|
+
extraCommandsDirs: [],
|
|
319
|
+
persist: true,
|
|
320
|
+
mapFile: DEFAULT_MAP_FILE,
|
|
321
|
+
},
|
|
322
|
+
}),
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** The non-empty catalog keyed by kind, in the order `kinds` lists them. */
|
|
327
|
+
function buildCatalog(kinds, cwd, config, maxClauseChars) {
|
|
328
|
+
const catalog = {};
|
|
329
|
+
for (const kind of kinds) {
|
|
330
|
+
const entries = entriesForKind(kind, cwd, config, maxClauseChars);
|
|
331
|
+
if (entries.length > 0) catalog[kind] = entries;
|
|
332
|
+
}
|
|
333
|
+
return catalog;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** The injected text for a catalog, grouped and captioned per kind. */
|
|
337
|
+
function renderCatalog(catalog, kinds) {
|
|
338
|
+
const sections = [];
|
|
339
|
+
for (const kind of kinds) {
|
|
340
|
+
const entries = catalog[kind];
|
|
341
|
+
if (!entries) continue;
|
|
342
|
+
const label = KIND_LABELS[kind] || kind;
|
|
343
|
+
const lines = entries.map((entry) =>
|
|
344
|
+
entry.blurb ? ` ${entry.name} — ${entry.blurb}` : ` ${entry.name}`,
|
|
345
|
+
);
|
|
346
|
+
sections.push(`${label}:\n${lines.join('\n')}`);
|
|
347
|
+
}
|
|
348
|
+
return `[capabilities] available (check before improvising something one of these covers):\n${sections.join('\n')}\n`;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function main() {
|
|
352
|
+
if (process.env[DUMP_ENV]) {
|
|
353
|
+
dumpDefaults();
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const payload = readPayload();
|
|
358
|
+
const cwd = payload.cwd || process.cwd();
|
|
359
|
+
|
|
360
|
+
const config = gateConfig(cwd);
|
|
361
|
+
if (!config || config.enabled !== true) return; // opt-in: off unless explicitly enabled
|
|
362
|
+
|
|
363
|
+
const maxClauseChars =
|
|
364
|
+
Number(config.maxClauseChars) || DEFAULT_MAX_CLAUSE_CHARS;
|
|
365
|
+
const kinds = Array.isArray(config.kinds) ? config.kinds : DEFAULT_KINDS;
|
|
366
|
+
|
|
367
|
+
const catalog = buildCatalog(kinds, cwd, config, maxClauseChars);
|
|
368
|
+
if (Object.keys(catalog).length === 0) return; // nothing to surface: never overwrite a good map
|
|
369
|
+
|
|
370
|
+
if (config.persist !== false) {
|
|
371
|
+
persistMap(cwd, config.mapFile || DEFAULT_MAP_FILE, catalog);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
process.stdout.write(renderCatalog(catalog, kinds));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
main();
|