@devrik-tools/claude-gates 0.1.2 → 0.2.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.
Files changed (106) hide show
  1. package/.claude-plugin/marketplace.json +7 -1
  2. package/cli/__tests__/config.test.mjs +113 -101
  3. package/cli/__tests__/install.test.mjs +102 -0
  4. package/cli/__tests__/materialize.test.mjs +95 -0
  5. package/cli/__tests__/registry-gates-consistency.test.mjs +52 -44
  6. package/cli/__tests__/task.test.mjs +124 -0
  7. package/cli/__tests__/version-consistency.test.mjs +44 -0
  8. package/cli/config.mjs +124 -113
  9. package/cli/index.mjs +100 -97
  10. package/cli/init.mjs +259 -256
  11. package/cli/install.mjs +150 -80
  12. package/cli/materialize.mjs +102 -51
  13. package/cli/registry.mjs +136 -127
  14. package/cli/task.mjs +140 -0
  15. package/package.json +2 -1
  16. package/plugins/gates/.claude-plugin/plugin.json +8 -2
  17. package/plugins/gates/hooks/__tests__/ask-adoption.test.mjs +83 -0
  18. package/plugins/gates/hooks/__tests__/doctor.test.mjs +85 -0
  19. package/plugins/gates/hooks/__tests__/wiring-check.test.mjs +65 -0
  20. package/plugins/gates/hooks/ask-adoption.mjs +147 -0
  21. package/plugins/gates/hooks/doctor.mjs +207 -0
  22. package/plugins/gates/hooks/gates/audit-before-build/audit-before-build.edge.test.mjs +83 -0
  23. package/plugins/gates/hooks/gates/audit-before-build/index.mjs +88 -88
  24. package/plugins/gates/hooks/gates/audit-before-build/test.mjs +117 -92
  25. package/plugins/gates/hooks/gates/autonomous-mode/autonomous.edge.test.mjs +104 -0
  26. package/plugins/gates/hooks/gates/autonomous-mode/index.mjs +48 -45
  27. package/plugins/gates/hooks/gates/bash-commands/bash-commands.edge.test.mjs +165 -0
  28. package/plugins/gates/hooks/gates/bash-commands/index.mjs +285 -268
  29. package/plugins/gates/hooks/gates/brief-before-delegate/brief-before-delegate.edge.test.mjs +151 -0
  30. package/plugins/gates/hooks/gates/brief-before-delegate/index.mjs +265 -177
  31. package/plugins/gates/hooks/gates/circuit-breaker/circuit-breaker.edge.test.mjs +207 -0
  32. package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +501 -456
  33. package/plugins/gates/hooks/gates/circuit-breaker/test.mjs +161 -143
  34. package/plugins/gates/hooks/gates/dependency-skills/dependency-skills.edge.test.mjs +69 -0
  35. package/plugins/gates/hooks/gates/dependency-skills/index.mjs +126 -118
  36. package/plugins/gates/hooks/gates/diagnosis-before-patch/diagnosis-before-patch.edge.test.mjs +68 -0
  37. package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +43 -49
  38. package/plugins/gates/hooks/gates/feature-catalog/edge.edge.test.mjs +83 -0
  39. package/plugins/gates/hooks/gates/feature-catalog/index.mjs +83 -100
  40. package/plugins/gates/hooks/gates/force-parallel/index.mjs +119 -0
  41. package/plugins/gates/hooks/gates/force-parallel/test.mjs +88 -0
  42. package/plugins/gates/hooks/gates/forge-flow/forge-flow.edge.test.mjs +155 -0
  43. package/plugins/gates/hooks/gates/forge-flow/index.mjs +134 -112
  44. package/plugins/gates/hooks/gates/implementation-pipeline/edge.edge.test.mjs +66 -0
  45. package/plugins/gates/hooks/gates/implementation-pipeline/index.mjs +187 -186
  46. package/plugins/gates/hooks/gates/intent-flow/index.mjs +260 -238
  47. package/plugins/gates/hooks/gates/intent-flow/intent-flow.edge.test.mjs +96 -0
  48. package/plugins/gates/hooks/gates/intent-flow/test.mjs +165 -136
  49. package/plugins/gates/hooks/gates/lint-commit/index.mjs +149 -0
  50. package/plugins/gates/hooks/gates/lint-commit/test.mjs +85 -0
  51. package/plugins/gates/hooks/gates/mandatory-flow/edge.edge.test.mjs +100 -0
  52. package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +180 -166
  53. package/plugins/gates/hooks/gates/neutral-spanish/index.mjs +79 -85
  54. package/plugins/gates/hooks/gates/neutral-spanish/neutral-spanish.edge.test.mjs +80 -0
  55. package/plugins/gates/hooks/gates/never-assume/index.mjs +58 -55
  56. package/plugins/gates/hooks/gates/never-assume/never-assume.edge.test.mjs +71 -0
  57. package/plugins/gates/hooks/gates/never-assume/test.mjs +95 -78
  58. package/plugins/gates/hooks/gates/no-blocking/index.mjs +148 -142
  59. package/plugins/gates/hooks/gates/no-blocking/no-blocking.edge.test.mjs +109 -0
  60. package/plugins/gates/hooks/gates/no-memory-dependency/index.mjs +123 -120
  61. package/plugins/gates/hooks/gates/no-memory-dependency/no-memory-dependency.edge.test.mjs +83 -0
  62. package/plugins/gates/hooks/gates/no-reconfirm/no-reconfirm.edge.test.mjs +91 -0
  63. package/plugins/gates/hooks/gates/protected-paths/index.mjs +144 -147
  64. package/plugins/gates/hooks/gates/protected-paths/protected-paths.edge.test.mjs +126 -0
  65. package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +89 -64
  66. package/plugins/gates/hooks/gates/recurrence-lock/recurrence-lock.edge.test.mjs +102 -0
  67. package/plugins/gates/hooks/gates/reuse-before-build/edge.edge.test.mjs +94 -0
  68. package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +159 -150
  69. package/plugins/gates/hooks/gates/risk-level/index.mjs +263 -203
  70. package/plugins/gates/hooks/gates/risk-level/risk-level.edge.test.mjs +116 -0
  71. package/plugins/gates/hooks/gates/risk-level/test.mjs +153 -125
  72. package/plugins/gates/hooks/gates/root-cause-first/index.mjs +56 -50
  73. package/plugins/gates/hooks/gates/root-cause-first/root-cause-first.edge.test.mjs +58 -0
  74. package/plugins/gates/hooks/gates/root-whitelist/index.mjs +116 -111
  75. package/plugins/gates/hooks/gates/root-whitelist/root-whitelist.edge.test.mjs +97 -0
  76. package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +184 -99
  77. package/plugins/gates/hooks/gates/rule-skill-autodiscovery/rule-skill-autodiscovery.edge.test.mjs +126 -0
  78. package/plugins/gates/hooks/gates/sdd-specs/edge.edge.test.mjs +130 -0
  79. package/plugins/gates/hooks/gates/sdd-specs/index.mjs +256 -251
  80. package/plugins/gates/hooks/gates/stop-pending/index.mjs +159 -0
  81. package/plugins/gates/hooks/gates/stop-pending/test.mjs +114 -0
  82. package/plugins/gates/hooks/gates/test-after-implementation/index.mjs +101 -101
  83. package/plugins/gates/hooks/gates/test-after-implementation/test-after-implementation.edge.test.mjs +72 -0
  84. package/plugins/gates/hooks/gates/test-matrix/edge.edge.test.mjs +63 -0
  85. package/plugins/gates/hooks/gates/test-matrix/index.mjs +187 -181
  86. package/plugins/gates/hooks/gates/test-matrix/test.mjs +114 -87
  87. package/plugins/gates/hooks/gates/tool-map/edge.edge.test.mjs +62 -0
  88. package/plugins/gates/hooks/gates/tool-map/index.mjs +143 -140
  89. package/plugins/gates/hooks/hooks.json +346 -286
  90. package/plugins/gates/hooks/lib/__tests__/config.test.mjs +173 -154
  91. package/plugins/gates/hooks/lib/__tests__/hook-io.test.mjs +210 -154
  92. package/plugins/gates/hooks/lib/__tests__/signals.test.mjs +114 -0
  93. package/plugins/gates/hooks/lib/config.mjs +172 -165
  94. package/plugins/gates/hooks/lib/hook-io-config.edge.test.mjs +189 -0
  95. package/plugins/gates/hooks/lib/hook-io.mjs +318 -208
  96. package/plugins/gates/hooks/lib/signals.mjs +127 -0
  97. package/plugins/gates/hooks/wiring-check.mjs +227 -0
  98. package/plugins/tasks/.claude-plugin/plugin.json +14 -0
  99. package/plugins/tasks/hooks/__tests__/register-requests.test.mjs +100 -0
  100. package/plugins/tasks/hooks/__tests__/session-tasks.test.mjs +95 -0
  101. package/plugins/tasks/hooks/hooks.json +26 -0
  102. package/plugins/tasks/hooks/lib/__tests__/task-store.test.mjs +180 -132
  103. package/plugins/tasks/hooks/lib/task-store.mjs +197 -159
  104. package/plugins/tasks/hooks/register-requests.mjs +141 -108
  105. package/plugins/tasks/hooks/session-tasks.mjs +108 -0
  106. package/registry.json +793 -686
@@ -0,0 +1,44 @@
1
+ // Version coherence guard. This exists because of a real incident: the npm package was
2
+ // bumped to 0.1.3 and published, but plugin.json / marketplace.json stayed at 0.1.0. Claude
3
+ // Code versions a plugin by its plugin.json/marketplace version, so a machine that already
4
+ // had 0.1.0 cached NEVER updated the code on reinstall — it ran the old, pre-migration gates
5
+ // while the freshly written config.json said 0.1.3. The gates looked "enabled but broken".
6
+ //
7
+ // The rule this enforces: the npm package version, both plugin.json versions, and every
8
+ // marketplace.json plugin entry must be the SAME string. Bump them together, every release.
9
+
10
+ import assert from 'node:assert/strict';
11
+ import { readFileSync } from 'node:fs';
12
+ import { dirname, join } from 'node:path';
13
+ import { test } from 'node:test';
14
+ import { fileURLToPath } from 'node:url';
15
+
16
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
17
+
18
+ function readJson(relativePath) {
19
+ return JSON.parse(readFileSync(join(ROOT, relativePath), 'utf8').replace(/^/, ''));
20
+ }
21
+
22
+ test('npm, both plugin.json, and every marketplace entry share one version', () => {
23
+ const versions = {
24
+ 'package.json': readJson('package.json').version,
25
+ 'plugins/gates/.claude-plugin/plugin.json': readJson(
26
+ 'plugins/gates/.claude-plugin/plugin.json',
27
+ ).version,
28
+ 'plugins/tasks/.claude-plugin/plugin.json': readJson(
29
+ 'plugins/tasks/.claude-plugin/plugin.json',
30
+ ).version,
31
+ };
32
+ for (const plugin of readJson('.claude-plugin/marketplace.json').plugins) {
33
+ versions[`marketplace.json:${plugin.name}`] = plugin.version;
34
+ }
35
+
36
+ const distinct = [...new Set(Object.values(versions))];
37
+ assert.equal(
38
+ distinct.length,
39
+ 1,
40
+ `all versions must match; found ${JSON.stringify(versions)}. ` +
41
+ 'A plugin.json/marketplace version behind the npm version means Claude Code keeps ' +
42
+ 'serving the OLD cached plugin code on reinstall. Bump them together.',
43
+ );
44
+ });
package/cli/config.mjs CHANGED
@@ -1,113 +1,124 @@
1
- // Where the selection is persisted and how it merges with what is already there.
2
- //
3
- // Scope "project": `<project root>/.ai/config.json` — the file the hooks read
4
- // per project. Other keys the project already has (autoCommit, etc.) are kept.
5
- // Scope "global": `~/.claude/claude-gates/config.json` — the fallback the hooks
6
- // use when a project has no answer of its own.
7
-
8
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
9
- import { dirname, join } from 'node:path';
10
- import { findUpSync } from 'find-up';
11
- import {
12
- CLAUDE_USER_DIRECTORY,
13
- CONFIG_FILE,
14
- GLOBAL_STATE_DIRECTORY,
15
- HOME_DIRECTORY,
16
- JSON_INDENT,
17
- PROJECT_ROOT_MARKERS,
18
- PROJECT_STATE_DIRECTORY,
19
- } from './constants.mjs';
20
-
21
- export const SCOPES = Object.freeze({ GLOBAL: 'global', PROJECT: 'project' });
22
-
23
- /**
24
- * Climbs from `startDirectory` to the nearest project marker (`.git`/`.ai`).
25
- * The home directory (and anything above it) is never a project root: a stray
26
- * `~/.ai` would otherwise turn every folder under home into "the project" and the
27
- * config would land in the wrong place silently. `.git` may be a file (worktrees),
28
- * so the matcher checks existence, not type.
29
- */
30
- export function findProjectRoot(
31
- startDirectory,
32
- { home = HOME_DIRECTORY } = {},
33
- ) {
34
- if (startDirectory === home) return startDirectory;
35
- const markerDirectory = findUpSync(
36
- (directory) => {
37
- if (directory === home) return findUpSync.stop;
38
- const hasMarker = PROJECT_ROOT_MARKERS.some((marker) =>
39
- existsSync(join(directory, marker)),
40
- );
41
- return hasMarker ? directory : undefined;
42
- },
43
- { cwd: startDirectory, stopAt: home, type: 'directory' },
44
- );
45
- return markerDirectory ?? startDirectory;
46
- }
47
-
48
- export function configPathFor(
49
- scope,
50
- { cwd = process.cwd(), home = HOME_DIRECTORY } = {},
51
- ) {
52
- if (scope === SCOPES.GLOBAL)
53
- return join(
54
- home,
55
- CLAUDE_USER_DIRECTORY,
56
- GLOBAL_STATE_DIRECTORY,
57
- CONFIG_FILE,
58
- );
59
- if (scope === SCOPES.PROJECT)
60
- return join(
61
- findProjectRoot(cwd, { home }),
62
- PROJECT_STATE_DIRECTORY,
63
- CONFIG_FILE,
64
- );
65
- throw new Error(`Unknown scope: ${scope}`);
66
- }
67
-
68
- export function readConfig(path) {
69
- if (!existsSync(path)) return { exists: false, data: {}, corrupt: false };
70
- try {
71
- return {
72
- exists: true,
73
- data: JSON.parse(readFileSync(path, 'utf8')),
74
- corrupt: false,
75
- };
76
- } catch {
77
- return { exists: true, data: {}, corrupt: true };
78
- }
79
- }
80
-
81
- /**
82
- * Merges one gate's new value onto whatever the project already had, preserving the user's
83
- * edits. A gate with params arrives as `{ enabled, ...defaults }`; if the user already
84
- * tuned those params, their values win and only `enabled` follows the new selection. A
85
- * paramless gate is a plain boolean. This is what keeps a re-run of `init` additive: it
86
- * never overwrites a whitelist or pattern list the user changed by hand.
87
- */
88
- function mergeGate(existingValue, newValue) {
89
- if (typeof newValue === 'boolean') return newValue;
90
- if (existingValue && typeof existingValue === 'object') {
91
- return { ...newValue, ...existingValue, enabled: newValue.enabled };
92
- }
93
- return newValue;
94
- }
95
-
96
- /**
97
- * Merges the new selection into an existing config without touching unrelated keys. Gates
98
- * are merged key by key: a gate absent from the new map keeps its old value (a registry
99
- * that dropped a gate must not silently flip it), and a gate the user configured keeps its
100
- * params (see mergeGate).
101
- */
102
- export function mergeConfig(existing, { adopted, gates, gateVersion }) {
103
- const mergedGates = { ...(existing.gates ?? {}) };
104
- for (const [key, value] of Object.entries(gates)) {
105
- mergedGates[key] = mergeGate(mergedGates[key], value);
106
- }
107
- return { ...existing, adopted, gateVersion, gates: mergedGates };
108
- }
109
-
110
- export function writeConfig(path, data) {
111
- mkdirSync(dirname(path), { recursive: true });
112
- writeFileSync(path, `${JSON.stringify(data, null, JSON_INDENT)}\n`, 'utf8');
113
- }
1
+ // Where the selection is persisted and how it merges with what is already there.
2
+ //
3
+ // Scope "project": `<project root>/.ai/config.json` — the file the hooks read
4
+ // per project. Other keys the project already has (autoCommit, etc.) are kept.
5
+ // Scope "global": `~/.claude/claude-gates/config.json` — the fallback the hooks
6
+ // use when a project has no answer of its own.
7
+
8
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
9
+ import { dirname, join } from 'node:path';
10
+ import { findUpSync } from 'find-up';
11
+ import {
12
+ CLAUDE_USER_DIRECTORY,
13
+ CONFIG_FILE,
14
+ GLOBAL_STATE_DIRECTORY,
15
+ HOME_DIRECTORY,
16
+ JSON_INDENT,
17
+ PROJECT_ROOT_MARKERS,
18
+ PROJECT_STATE_DIRECTORY,
19
+ } from './constants.mjs';
20
+
21
+ export const SCOPES = Object.freeze({ GLOBAL: 'global', PROJECT: 'project' });
22
+
23
+ /**
24
+ * Climbs from `startDirectory` to the nearest project marker (`.git`/`.ai`).
25
+ * The home directory (and anything above it) is never a project root: a stray
26
+ * `~/.ai` would otherwise turn every folder under home into "the project" and the
27
+ * config would land in the wrong place silently. `.git` may be a file (worktrees),
28
+ * so the matcher checks existence, not type.
29
+ */
30
+ export function findProjectRoot(
31
+ startDirectory,
32
+ { home = HOME_DIRECTORY } = {},
33
+ ) {
34
+ if (startDirectory === home) return startDirectory;
35
+ const markerDirectory = findUpSync(
36
+ (directory) => {
37
+ if (directory === home) return findUpSync.stop;
38
+ const hasMarker = PROJECT_ROOT_MARKERS.some((marker) =>
39
+ existsSync(join(directory, marker)),
40
+ );
41
+ return hasMarker ? directory : undefined;
42
+ },
43
+ { cwd: startDirectory, stopAt: home, type: 'directory' },
44
+ );
45
+ return markerDirectory ?? startDirectory;
46
+ }
47
+
48
+ export function configPathFor(
49
+ scope,
50
+ { cwd = process.cwd(), home = HOME_DIRECTORY } = {},
51
+ ) {
52
+ if (scope === SCOPES.GLOBAL)
53
+ return join(
54
+ home,
55
+ CLAUDE_USER_DIRECTORY,
56
+ GLOBAL_STATE_DIRECTORY,
57
+ CONFIG_FILE,
58
+ );
59
+ if (scope === SCOPES.PROJECT)
60
+ return join(
61
+ findProjectRoot(cwd, { home }),
62
+ PROJECT_STATE_DIRECTORY,
63
+ CONFIG_FILE,
64
+ );
65
+ throw new Error(`Unknown scope: ${scope}`);
66
+ }
67
+
68
+ // A leading UTF-8 BOM (EF BB BF, decoded as U+FEFF) is not stripped by readFileSync('utf8'),
69
+ // and JSON.parse rejects a string that starts with it. Without stripping it, a config saved
70
+ // by a BOM-adding editor or `PowerShell Set-Content -Encoding utf8` would read back as
71
+ // `corrupt: true` — a valid, user-edited config (with its gate overrides) mistaken for
72
+ // unreadable. init.mjs bails loudly on `corrupt`, but a caller relying on `data` alone (as
73
+ // `mergeConfig` does) would otherwise merge onto `{}` and silently drop every existing gate
74
+ // override the user made. This is the same failure mode fixed in the gates' own config.mjs.
75
+ function stripBom(text) {
76
+ return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
77
+ }
78
+
79
+ export function readConfig(path) {
80
+ if (!existsSync(path)) return { exists: false, data: {}, corrupt: false };
81
+ try {
82
+ return {
83
+ exists: true,
84
+ data: JSON.parse(stripBom(readFileSync(path, 'utf8'))),
85
+ corrupt: false,
86
+ };
87
+ } catch {
88
+ return { exists: true, data: {}, corrupt: true };
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Merges one gate's new value onto whatever the project already had, preserving the user's
94
+ * edits. A gate with params arrives as `{ enabled, ...defaults }`; if the user already
95
+ * tuned those params, their values win and only `enabled` follows the new selection. A
96
+ * paramless gate is a plain boolean. This is what keeps a re-run of `init` additive: it
97
+ * never overwrites a whitelist or pattern list the user changed by hand.
98
+ */
99
+ function mergeGate(existingValue, newValue) {
100
+ if (typeof newValue === 'boolean') return newValue;
101
+ if (existingValue && typeof existingValue === 'object') {
102
+ return { ...newValue, ...existingValue, enabled: newValue.enabled };
103
+ }
104
+ return newValue;
105
+ }
106
+
107
+ /**
108
+ * Merges the new selection into an existing config without touching unrelated keys. Gates
109
+ * are merged key by key: a gate absent from the new map keeps its old value (a registry
110
+ * that dropped a gate must not silently flip it), and a gate the user configured keeps its
111
+ * params (see mergeGate).
112
+ */
113
+ export function mergeConfig(existing, { adopted, gates, gateVersion }) {
114
+ const mergedGates = { ...(existing.gates ?? {}) };
115
+ for (const [key, value] of Object.entries(gates)) {
116
+ mergedGates[key] = mergeGate(mergedGates[key], value);
117
+ }
118
+ return { ...existing, adopted, gateVersion, gates: mergedGates };
119
+ }
120
+
121
+ export function writeConfig(path, data) {
122
+ mkdirSync(dirname(path), { recursive: true });
123
+ writeFileSync(path, `${JSON.stringify(data, null, JSON_INDENT)}\n`, 'utf8');
124
+ }
package/cli/index.mjs CHANGED
@@ -1,97 +1,100 @@
1
- #!/usr/bin/env node
2
- // Entry point (commander). Commands:
3
- // init interactive (or flag-driven) selection of gates, per project or globally
4
- // registry --check validates registry.json; --list prints the catalog
5
-
6
- import { readFileSync } from 'node:fs';
7
- import { join } from 'node:path';
8
- import { Command } from 'commander';
9
- import {
10
- EXIT_CODE,
11
- LIST_SEPARATOR,
12
- REGISTRY_PATH,
13
- REPOSITORY_ROOT,
14
- } from './constants.mjs';
15
- import { loadRegistry, validateRegistry } from './registry.mjs';
16
-
17
- const packageManifest = JSON.parse(
18
- readFileSync(join(REPOSITORY_ROOT, 'package.json'), 'utf8'),
19
- );
20
-
21
- function fail(message) {
22
- process.stderr.write(`${message}\n`);
23
- process.exit(EXIT_CODE.FAILURE);
24
- }
25
-
26
- function splitList(value) {
27
- return value
28
- .split(LIST_SEPARATOR)
29
- .map((item) => item.trim())
30
- .filter(Boolean);
31
- }
32
-
33
- function registryCheck() {
34
- const problems = validateRegistry(
35
- JSON.parse(readFileSync(REGISTRY_PATH, 'utf8')),
36
- );
37
- if (problems.length > 0) {
38
- fail(`registry.json is invalid:\n- ${problems.join('\n- ')}`);
39
- }
40
- process.stdout.write('registry.json OK\n');
41
- }
42
-
43
- function registryList() {
44
- const registry = loadRegistry();
45
- for (const family of registry.families) {
46
- process.stdout.write(`${family.name} (${family.id})\n`);
47
- for (const gate of family.gates) {
48
- const marker = gate.default ? '*' : ' ';
49
- process.stdout.write(` ${marker} ${gate.id} ${gate.configKey}\n`);
50
- }
51
- }
52
- process.stdout.write('\n* = recommended default\n');
53
- }
54
-
55
- const program = new Command()
56
- .name(packageManifest.name)
57
- .description(packageManifest.description)
58
- .version(packageManifest.version);
59
-
60
- program
61
- .command('init')
62
- .description('Choose which gates to adopt and write the config.')
63
- .option('--project', 'apply to this project (<root>/.ai/config.json)')
64
- .option('--global', 'apply globally (~/.claude/claude-gates/config.json)')
65
- .option('--defaults', 'enable the recommended defaults')
66
- .option('--all', 'enable every gate')
67
- .option('--none', "record a 'no' so you are not asked again")
68
- .option(
69
- '--families <ids>',
70
- 'enable whole families, comma-separated',
71
- splitList,
72
- )
73
- .option(
74
- '--gates <ids>',
75
- 'enable individual gates, comma-separated',
76
- splitList,
77
- )
78
- .option('-y, --yes', 'never prompt; use flags and defaults')
79
- .option('--no-install', 'write the config but do not install the plugin')
80
- .option('--dry-run', 'show the selection without writing')
81
- .action(async (options) => {
82
- const { runInit } = await import('./init.mjs');
83
- await runInit(options);
84
- });
85
-
86
- program
87
- .command('registry')
88
- .description('Inspect registry.json.')
89
- .option('--check', 'validate the registry')
90
- .option('--list', 'print families and gates')
91
- .action((options) => {
92
- if (options.check) return registryCheck();
93
- if (options.list) return registryList();
94
- return fail('registry needs --check or --list');
95
- });
96
-
97
- program.parseAsync(process.argv).catch((error) => fail(error.message));
1
+ #!/usr/bin/env node
2
+ // Entry point (commander). Commands:
3
+ // init interactive (or flag-driven) selection of gates, per project or globally
4
+ // registry --check validates registry.json; --list prints the catalog
5
+
6
+ import { readFileSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { Command } from 'commander';
9
+ import {
10
+ EXIT_CODE,
11
+ LIST_SEPARATOR,
12
+ REGISTRY_PATH,
13
+ REPOSITORY_ROOT,
14
+ } from './constants.mjs';
15
+ import { loadRegistry, validateRegistry } from './registry.mjs';
16
+ import { registerTaskCommand } from './task.mjs';
17
+
18
+ const packageManifest = JSON.parse(
19
+ readFileSync(join(REPOSITORY_ROOT, 'package.json'), 'utf8'),
20
+ );
21
+
22
+ function fail(message) {
23
+ process.stderr.write(`${message}\n`);
24
+ process.exit(EXIT_CODE.FAILURE);
25
+ }
26
+
27
+ function splitList(value) {
28
+ return value
29
+ .split(LIST_SEPARATOR)
30
+ .map((item) => item.trim())
31
+ .filter(Boolean);
32
+ }
33
+
34
+ function registryCheck() {
35
+ const problems = validateRegistry(
36
+ JSON.parse(readFileSync(REGISTRY_PATH, 'utf8')),
37
+ );
38
+ if (problems.length > 0) {
39
+ fail(`registry.json is invalid:\n- ${problems.join('\n- ')}`);
40
+ }
41
+ process.stdout.write('registry.json OK\n');
42
+ }
43
+
44
+ function registryList() {
45
+ const registry = loadRegistry();
46
+ for (const family of registry.families) {
47
+ process.stdout.write(`${family.name} (${family.id})\n`);
48
+ for (const gate of family.gates) {
49
+ const marker = gate.default ? '*' : ' ';
50
+ process.stdout.write(` ${marker} ${gate.id} ${gate.configKey}\n`);
51
+ }
52
+ }
53
+ process.stdout.write('\n* = recommended default\n');
54
+ }
55
+
56
+ const program = new Command()
57
+ .name(packageManifest.name)
58
+ .description(packageManifest.description)
59
+ .version(packageManifest.version);
60
+
61
+ program
62
+ .command('init')
63
+ .description('Choose which gates to adopt and write the config.')
64
+ .option('--project', 'apply to this project (<root>/.ai/config.json)')
65
+ .option('--global', 'apply globally (~/.claude/claude-gates/config.json)')
66
+ .option('--defaults', 'enable the recommended defaults')
67
+ .option('--all', 'enable every gate')
68
+ .option('--none', "record a 'no' so you are not asked again")
69
+ .option(
70
+ '--families <ids>',
71
+ 'enable whole families, comma-separated',
72
+ splitList,
73
+ )
74
+ .option(
75
+ '--gates <ids>',
76
+ 'enable individual gates, comma-separated',
77
+ splitList,
78
+ )
79
+ .option('-y, --yes', 'never prompt; use flags and defaults')
80
+ .option('--no-install', 'write the config but do not install the plugin')
81
+ .option('--dry-run', 'show the selection without writing')
82
+ .action(async (options) => {
83
+ const { runInit } = await import('./init.mjs');
84
+ await runInit(options);
85
+ });
86
+
87
+ program
88
+ .command('registry')
89
+ .description('Inspect registry.json.')
90
+ .option('--check', 'validate the registry')
91
+ .option('--list', 'print families and gates')
92
+ .action((options) => {
93
+ if (options.check) return registryCheck();
94
+ if (options.list) return registryList();
95
+ return fail('registry needs --check or --list');
96
+ });
97
+
98
+ registerTaskCommand(program);
99
+
100
+ program.parseAsync(process.argv).catch((error) => fail(error.message));