@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
package/cli/init.mjs CHANGED
@@ -1,256 +1,259 @@
1
- // `claude-gates init` — the interactive flow: scope → mode → picks → confirm → write.
2
- // Every decision can also be passed as a flag so CI and scripts can run it without a TTY.
3
-
4
- import * as prompts from '@clack/prompts';
5
- import {
6
- SCOPES,
7
- configPathFor,
8
- readConfig,
9
- mergeConfig,
10
- writeConfig,
11
- } from './config.mjs';
12
- import { EXIT_CODE } from './constants.mjs';
13
- import { installPlugin, pluginInstallCommand } from './install.mjs';
14
- import { materializeGates } from './materialize.mjs';
15
- import { loadRegistry, allGates } from './registry.mjs';
16
- import {
17
- MODES,
18
- resolveSelection,
19
- adoptionOf,
20
- summarize,
21
- } from './selection.mjs';
22
-
23
- const MODE_OPTIONS = [
24
- {
25
- value: MODES.DEFAULTS,
26
- label: 'Recommended defaults',
27
- hint: 'gates marked default in the registry',
28
- },
29
- { value: MODES.ALL, label: 'Everything', hint: 'every gate in every family' },
30
- { value: MODES.FAMILIES, label: 'By family', hint: 'pick whole families' },
31
- { value: MODES.GRANULAR, label: 'Granular', hint: 'pick individual gates' },
32
- {
33
- value: MODES.NONE,
34
- label: 'Nothing',
35
- hint: "record a 'no' so you are not asked again",
36
- },
37
- ];
38
-
39
- /** commander option name → selection mode */
40
- const MODE_BY_OPTION = {
41
- defaults: MODES.DEFAULTS,
42
- all: MODES.ALL,
43
- none: MODES.NONE,
44
- families: MODES.FAMILIES,
45
- gates: MODES.GRANULAR,
46
- };
47
-
48
- /**
49
- * Turns commander's options object into the decisions `runInit` needs.
50
- * `scope`/`mode` stay `null` when no flag decided them (prompted later).
51
- */
52
- export function normalizeOptions(options = {}) {
53
- const modeFlags = Object.keys(MODE_BY_OPTION).filter((name) => options[name]);
54
- if (modeFlags.length > 1)
55
- throw new Error(`Pick one of --${modeFlags.join(', --')}`);
56
- if (options.project && options.global)
57
- throw new Error('Pick one of --project, --global');
58
-
59
- let scope = null;
60
- if (options.project) scope = SCOPES.PROJECT;
61
- if (options.global) scope = SCOPES.GLOBAL;
62
-
63
- return {
64
- scope,
65
- mode: modeFlags.length === 1 ? MODE_BY_OPTION[modeFlags[0]] : null,
66
- families: Array.isArray(options.families) ? options.families : [],
67
- gates: Array.isArray(options.gates) ? options.gates : [],
68
- yes: Boolean(options.yes),
69
- dryRun: Boolean(options.dryRun),
70
- install: options.install,
71
- };
72
- }
73
-
74
- function bail(value) {
75
- if (prompts.isCancel(value)) {
76
- prompts.cancel('Nothing written.');
77
- process.exit(EXIT_CODE.FAILURE);
78
- }
79
- return value;
80
- }
81
-
82
- async function askScope(cwd) {
83
- return bail(
84
- await prompts.select({
85
- message: 'Where should these gates apply?',
86
- options: [
87
- {
88
- value: SCOPES.PROJECT,
89
- label: 'This project',
90
- hint: configPathFor(SCOPES.PROJECT, { cwd }),
91
- },
92
- {
93
- value: SCOPES.GLOBAL,
94
- label: 'Globally (fallback for every project)',
95
- hint: configPathFor(SCOPES.GLOBAL),
96
- },
97
- ],
98
- }),
99
- );
100
- }
101
-
102
- async function askMode() {
103
- return bail(
104
- await prompts.select({
105
- message: 'What do you want to adopt?',
106
- options: MODE_OPTIONS,
107
- }),
108
- );
109
- }
110
-
111
- async function askFamilies(registry) {
112
- return bail(
113
- await prompts.multiselect({
114
- message: 'Pick families (space to toggle, enter to confirm)',
115
- options: registry.families.map((family) => ({
116
- value: family.id,
117
- label: family.name,
118
- hint: family.description,
119
- })),
120
- initialValues: registry.families
121
- .filter((family) => family.gates.some((gate) => gate.default))
122
- .map((family) => family.id),
123
- required: false,
124
- }),
125
- );
126
- }
127
-
128
- async function askGates(registry) {
129
- const options = {};
130
- for (const family of registry.families) {
131
- options[family.name] = family.gates.map((gate) => ({
132
- value: gate.id,
133
- label: gate.id,
134
- hint: gate.description,
135
- }));
136
- }
137
- return bail(
138
- await prompts.groupMultiselect({
139
- message: 'Pick gates (space to toggle, enter to confirm)',
140
- options,
141
- initialValues: allGates(registry)
142
- .filter((gate) => gate.default)
143
- .map((gate) => gate.id),
144
- required: false,
145
- }),
146
- );
147
- }
148
-
149
- /** `claude plugin install <plugin>@<marketplace>`, read from the marketplace manifest, never hard-coded. */
150
-
151
- /** Fills in whatever the flags left undecided, asking only when there is a TTY. */
152
- async function decide(flags, registry, cwd, interactive) {
153
- const scope =
154
- flags.scope ?? (interactive ? await askScope(cwd) : SCOPES.PROJECT);
155
- const mode = flags.mode ?? (interactive ? await askMode() : MODES.DEFAULTS);
156
- const picks = { families: flags.families, gates: flags.gates };
157
- if (interactive && mode === MODES.FAMILIES && picks.families.length === 0) {
158
- picks.families = await askFamilies(registry);
159
- }
160
- if (interactive && mode === MODES.GRANULAR && picks.gates.length === 0) {
161
- picks.gates = await askGates(registry);
162
- }
163
- return { scope, mode, picks };
164
- }
165
-
166
- function renderSummary(registry, gatesMap) {
167
- return summarize(registry, gatesMap)
168
- .map((row) => {
169
- const list = row.enabled.length > 0 ? ` — ${row.enabled.join(', ')}` : '';
170
- return `${row.name}: ${row.enabled.length}/${row.total}${list}`;
171
- })
172
- .join('\n');
173
- }
174
-
175
- async function confirmWrite(io, fileExists) {
176
- const confirmed = bail(
177
- await io.confirm({
178
- message: fileExists
179
- ? 'File exists. Merge this selection into it?'
180
- : 'Write this file?',
181
- }),
182
- );
183
- if (!confirmed) {
184
- io.cancel('Nothing written.');
185
- process.exit(EXIT_CODE.FAILURE);
186
- }
187
- }
188
-
189
- export async function runInit(
190
- options,
191
- { cwd = process.cwd(), io = prompts } = {},
192
- ) {
193
- const flags = normalizeOptions(options);
194
- const registry = loadRegistry();
195
- const interactive = !flags.yes && Boolean(process.stdin.isTTY);
196
-
197
- if (interactive) io.intro('claude-gates');
198
-
199
- const { scope, mode, picks } = await decide(
200
- flags,
201
- registry,
202
- cwd,
203
- interactive,
204
- );
205
- const gates = resolveSelection(registry, mode, picks);
206
- const path = configPathFor(scope, { cwd });
207
- const existing = readConfig(path);
208
-
209
- if (existing.corrupt) {
210
- io.log.error(
211
- `${path} exists but is not valid JSON. Fix or remove it first; nothing written.`,
212
- );
213
- process.exit(EXIT_CODE.FAILURE);
214
- }
215
-
216
- const merged = mergeConfig(existing.data, {
217
- adopted: adoptionOf(gates),
218
- gates: materializeGates(registry, gates),
219
- gateVersion: registry.gateVersion,
220
- });
221
-
222
- io.note(renderSummary(registry, gates), `Selection (${scope}) → ${path}`);
223
-
224
- if (flags.dryRun) {
225
- io.outro('Dry run: nothing written.');
226
- return { path, config: merged, written: false };
227
- }
228
-
229
- if (interactive) await confirmWrite(io, existing.exists);
230
-
231
- writeConfig(path, merged);
232
-
233
- // Install based on the selection, unless the user turned everything off or opted out.
234
- // Additive: `claude plugin install` merges the gate hooks next to whatever is already
235
- // configured. If it cannot run, we fall back to printing the manual command.
236
- const wantsInstall = flags.install !== false && adoptionOf(gates) !== false;
237
- if (!wantsInstall) {
238
- io.outro(`Written ${path}.`);
239
- return { path, config: merged, written: true, installed: false };
240
- }
241
-
242
- const result = installPlugin(scope, { cwd });
243
- if (result.installed) {
244
- io.outro(
245
- `Written ${path} and installed the plugin (${result.scope} scope). ` +
246
- 'Restart the session (or run /plugin) for the gates to load.',
247
- );
248
- } else {
249
- io.log.warn(
250
- `Config written, but the plugin was not installed automatically (${result.reason}). ` +
251
- `Install it yourself with: ${pluginInstallCommand()}`,
252
- );
253
- io.outro(`Written ${path}.`);
254
- }
255
- return { path, config: merged, written: true, installed: result.installed };
256
- }
1
+ // `claude-gates init` — the interactive flow: scope → mode → picks → confirm → write.
2
+ // Every decision can also be passed as a flag so CI and scripts can run it without a TTY.
3
+
4
+ import * as prompts from '@clack/prompts';
5
+ import {
6
+ SCOPES,
7
+ configPathFor,
8
+ readConfig,
9
+ mergeConfig,
10
+ writeConfig,
11
+ } from './config.mjs';
12
+ import { EXIT_CODE } from './constants.mjs';
13
+ import { installPlugin, pluginInstallCommands } from './install.mjs';
14
+ import { materializeGates } from './materialize.mjs';
15
+ import { loadRegistry, allGates } from './registry.mjs';
16
+ import {
17
+ MODES,
18
+ resolveSelection,
19
+ adoptionOf,
20
+ summarize,
21
+ } from './selection.mjs';
22
+
23
+ const MODE_OPTIONS = [
24
+ {
25
+ value: MODES.DEFAULTS,
26
+ label: 'Recommended defaults',
27
+ hint: 'gates marked default in the registry',
28
+ },
29
+ { value: MODES.ALL, label: 'Everything', hint: 'every gate in every family' },
30
+ { value: MODES.FAMILIES, label: 'By family', hint: 'pick whole families' },
31
+ { value: MODES.GRANULAR, label: 'Granular', hint: 'pick individual gates' },
32
+ {
33
+ value: MODES.NONE,
34
+ label: 'Nothing',
35
+ hint: "record a 'no' so you are not asked again",
36
+ },
37
+ ];
38
+
39
+ /** commander option name → selection mode */
40
+ const MODE_BY_OPTION = {
41
+ defaults: MODES.DEFAULTS,
42
+ all: MODES.ALL,
43
+ none: MODES.NONE,
44
+ families: MODES.FAMILIES,
45
+ gates: MODES.GRANULAR,
46
+ };
47
+
48
+ /**
49
+ * Turns commander's options object into the decisions `runInit` needs.
50
+ * `scope`/`mode` stay `null` when no flag decided them (prompted later).
51
+ */
52
+ export function normalizeOptions(options = {}) {
53
+ const modeFlags = Object.keys(MODE_BY_OPTION).filter((name) => options[name]);
54
+ if (modeFlags.length > 1)
55
+ throw new Error(`Pick one of --${modeFlags.join(', --')}`);
56
+ if (options.project && options.global)
57
+ throw new Error('Pick one of --project, --global');
58
+
59
+ let scope = null;
60
+ if (options.project) scope = SCOPES.PROJECT;
61
+ if (options.global) scope = SCOPES.GLOBAL;
62
+
63
+ return {
64
+ scope,
65
+ mode: modeFlags.length === 1 ? MODE_BY_OPTION[modeFlags[0]] : null,
66
+ families: Array.isArray(options.families) ? options.families : [],
67
+ gates: Array.isArray(options.gates) ? options.gates : [],
68
+ yes: Boolean(options.yes),
69
+ dryRun: Boolean(options.dryRun),
70
+ install: options.install,
71
+ };
72
+ }
73
+
74
+ function bail(value) {
75
+ if (prompts.isCancel(value)) {
76
+ prompts.cancel('Nothing written.');
77
+ process.exit(EXIT_CODE.FAILURE);
78
+ }
79
+ return value;
80
+ }
81
+
82
+ async function askScope(cwd) {
83
+ return bail(
84
+ await prompts.select({
85
+ message: 'Where should these gates apply?',
86
+ options: [
87
+ {
88
+ value: SCOPES.PROJECT,
89
+ label: 'This project',
90
+ hint: configPathFor(SCOPES.PROJECT, { cwd }),
91
+ },
92
+ {
93
+ value: SCOPES.GLOBAL,
94
+ label: 'Globally (fallback for every project)',
95
+ hint: configPathFor(SCOPES.GLOBAL),
96
+ },
97
+ ],
98
+ }),
99
+ );
100
+ }
101
+
102
+ async function askMode() {
103
+ return bail(
104
+ await prompts.select({
105
+ message: 'What do you want to adopt?',
106
+ options: MODE_OPTIONS,
107
+ }),
108
+ );
109
+ }
110
+
111
+ async function askFamilies(registry) {
112
+ return bail(
113
+ await prompts.multiselect({
114
+ message: 'Pick families (space to toggle, enter to confirm)',
115
+ options: registry.families.map((family) => ({
116
+ value: family.id,
117
+ label: family.name,
118
+ hint: family.description,
119
+ })),
120
+ initialValues: registry.families
121
+ .filter((family) => family.gates.some((gate) => gate.default))
122
+ .map((family) => family.id),
123
+ required: false,
124
+ }),
125
+ );
126
+ }
127
+
128
+ async function askGates(registry) {
129
+ const options = {};
130
+ for (const family of registry.families) {
131
+ options[family.name] = family.gates.map((gate) => ({
132
+ value: gate.id,
133
+ label: gate.id,
134
+ hint: gate.description,
135
+ }));
136
+ }
137
+ return bail(
138
+ await prompts.groupMultiselect({
139
+ message: 'Pick gates (space to toggle, enter to confirm)',
140
+ options,
141
+ initialValues: allGates(registry)
142
+ .filter((gate) => gate.default)
143
+ .map((gate) => gate.id),
144
+ required: false,
145
+ }),
146
+ );
147
+ }
148
+
149
+ /** `claude plugin install <plugin>@<marketplace>`, read from the marketplace manifest, never hard-coded. */
150
+
151
+ /** Fills in whatever the flags left undecided, asking only when there is a TTY. */
152
+ async function decide(flags, registry, cwd, interactive) {
153
+ const scope =
154
+ flags.scope ?? (interactive ? await askScope(cwd) : SCOPES.PROJECT);
155
+ const mode = flags.mode ?? (interactive ? await askMode() : MODES.DEFAULTS);
156
+ const picks = { families: flags.families, gates: flags.gates };
157
+ if (interactive && mode === MODES.FAMILIES && picks.families.length === 0) {
158
+ picks.families = await askFamilies(registry);
159
+ }
160
+ if (interactive && mode === MODES.GRANULAR && picks.gates.length === 0) {
161
+ picks.gates = await askGates(registry);
162
+ }
163
+ return { scope, mode, picks };
164
+ }
165
+
166
+ function renderSummary(registry, gatesMap) {
167
+ return summarize(registry, gatesMap)
168
+ .map((row) => {
169
+ const list = row.enabled.length > 0 ? ` — ${row.enabled.join(', ')}` : '';
170
+ return `${row.name}: ${row.enabled.length}/${row.total}${list}`;
171
+ })
172
+ .join('\n');
173
+ }
174
+
175
+ async function confirmWrite(io, fileExists) {
176
+ const confirmed = bail(
177
+ await io.confirm({
178
+ message: fileExists
179
+ ? 'File exists. Merge this selection into it?'
180
+ : 'Write this file?',
181
+ }),
182
+ );
183
+ if (!confirmed) {
184
+ io.cancel('Nothing written.');
185
+ process.exit(EXIT_CODE.FAILURE);
186
+ }
187
+ }
188
+
189
+ export async function runInit(
190
+ options,
191
+ { cwd = process.cwd(), io = prompts } = {},
192
+ ) {
193
+ const flags = normalizeOptions(options);
194
+ const registry = loadRegistry();
195
+ const interactive = !flags.yes && Boolean(process.stdin.isTTY);
196
+
197
+ if (interactive) io.intro('claude-gates');
198
+
199
+ const { scope, mode, picks } = await decide(
200
+ flags,
201
+ registry,
202
+ cwd,
203
+ interactive,
204
+ );
205
+ const gates = resolveSelection(registry, mode, picks);
206
+ const path = configPathFor(scope, { cwd });
207
+ const existing = readConfig(path);
208
+
209
+ if (existing.corrupt) {
210
+ io.log.error(
211
+ `${path} exists but is not valid JSON. Fix or remove it first; nothing written.`,
212
+ );
213
+ process.exit(EXIT_CODE.FAILURE);
214
+ }
215
+
216
+ const merged = mergeConfig(existing.data, {
217
+ adopted: adoptionOf(gates),
218
+ gates: materializeGates(registry, gates, existing.data.gates ?? {}, mode),
219
+ gateVersion: registry.gateVersion,
220
+ });
221
+
222
+ io.note(renderSummary(registry, gates), `Selection (${scope}) → ${path}`);
223
+
224
+ if (flags.dryRun) {
225
+ io.outro('Dry run: nothing written.');
226
+ return { path, config: merged, written: false };
227
+ }
228
+
229
+ if (interactive) await confirmWrite(io, existing.exists);
230
+
231
+ writeConfig(path, merged);
232
+
233
+ // Install based on the selection, unless the user turned everything off or opted out.
234
+ // Additive: `claude plugin install` merges the gate hooks next to whatever is already
235
+ // configured. If it cannot run, we fall back to printing the manual command.
236
+ const wantsInstall = flags.install !== false && adoptionOf(gates) !== false;
237
+ if (!wantsInstall) {
238
+ io.outro(`Written ${path}.`);
239
+ return { path, config: merged, written: true, installed: false };
240
+ }
241
+
242
+ const result = installPlugin(scope, { cwd });
243
+ if (result.installed) {
244
+ io.outro(
245
+ `Written ${path} and installed all plugins (${result.scope} scope). ` +
246
+ 'Restart the session (or run /plugin) for the gates to load.',
247
+ );
248
+ } else {
249
+ const manualCommands = pluginInstallCommands()
250
+ .filter((_command, index) => !result.results[index].installed)
251
+ .join('\n ');
252
+ io.log.warn(
253
+ `Config written, but not every plugin installed automatically (${result.reason}). ` +
254
+ `Install the rest yourself with:\n ${manualCommands}`,
255
+ );
256
+ io.outro(`Written ${path}.`);
257
+ }
258
+ return { path, config: merged, written: true, installed: result.installed };
259
+ }