@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.
- package/.claude-plugin/marketplace.json +7 -1
- package/cli/__tests__/config.test.mjs +113 -101
- package/cli/__tests__/install.test.mjs +102 -0
- package/cli/__tests__/materialize.test.mjs +95 -0
- package/cli/__tests__/registry-gates-consistency.test.mjs +52 -44
- package/cli/__tests__/task.test.mjs +124 -0
- package/cli/__tests__/version-consistency.test.mjs +44 -0
- package/cli/config.mjs +124 -113
- package/cli/index.mjs +100 -97
- package/cli/init.mjs +259 -256
- package/cli/install.mjs +150 -80
- package/cli/materialize.mjs +102 -51
- package/cli/registry.mjs +136 -127
- package/cli/task.mjs +140 -0
- package/package.json +2 -1
- package/plugins/gates/.claude-plugin/plugin.json +8 -2
- package/plugins/gates/hooks/__tests__/ask-adoption.test.mjs +83 -0
- package/plugins/gates/hooks/__tests__/doctor.test.mjs +85 -0
- package/plugins/gates/hooks/__tests__/wiring-check.test.mjs +65 -0
- package/plugins/gates/hooks/ask-adoption.mjs +147 -0
- package/plugins/gates/hooks/doctor.mjs +207 -0
- package/plugins/gates/hooks/gates/audit-before-build/audit-before-build.edge.test.mjs +83 -0
- package/plugins/gates/hooks/gates/audit-before-build/index.mjs +88 -88
- package/plugins/gates/hooks/gates/audit-before-build/test.mjs +117 -92
- package/plugins/gates/hooks/gates/autonomous-mode/autonomous.edge.test.mjs +104 -0
- package/plugins/gates/hooks/gates/autonomous-mode/index.mjs +48 -45
- package/plugins/gates/hooks/gates/bash-commands/bash-commands.edge.test.mjs +165 -0
- package/plugins/gates/hooks/gates/bash-commands/index.mjs +285 -268
- package/plugins/gates/hooks/gates/brief-before-delegate/brief-before-delegate.edge.test.mjs +151 -0
- package/plugins/gates/hooks/gates/brief-before-delegate/index.mjs +265 -177
- package/plugins/gates/hooks/gates/circuit-breaker/circuit-breaker.edge.test.mjs +207 -0
- package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +501 -456
- package/plugins/gates/hooks/gates/circuit-breaker/test.mjs +161 -143
- package/plugins/gates/hooks/gates/dependency-skills/dependency-skills.edge.test.mjs +69 -0
- package/plugins/gates/hooks/gates/dependency-skills/index.mjs +126 -118
- package/plugins/gates/hooks/gates/diagnosis-before-patch/diagnosis-before-patch.edge.test.mjs +68 -0
- package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +43 -49
- package/plugins/gates/hooks/gates/feature-catalog/edge.edge.test.mjs +83 -0
- package/plugins/gates/hooks/gates/feature-catalog/index.mjs +83 -100
- package/plugins/gates/hooks/gates/force-parallel/index.mjs +119 -0
- package/plugins/gates/hooks/gates/force-parallel/test.mjs +88 -0
- package/plugins/gates/hooks/gates/forge-flow/forge-flow.edge.test.mjs +155 -0
- package/plugins/gates/hooks/gates/forge-flow/index.mjs +134 -112
- package/plugins/gates/hooks/gates/implementation-pipeline/edge.edge.test.mjs +66 -0
- package/plugins/gates/hooks/gates/implementation-pipeline/index.mjs +187 -186
- package/plugins/gates/hooks/gates/intent-flow/index.mjs +260 -238
- package/plugins/gates/hooks/gates/intent-flow/intent-flow.edge.test.mjs +96 -0
- package/plugins/gates/hooks/gates/intent-flow/test.mjs +165 -136
- package/plugins/gates/hooks/gates/lint-commit/index.mjs +149 -0
- package/plugins/gates/hooks/gates/lint-commit/test.mjs +85 -0
- package/plugins/gates/hooks/gates/mandatory-flow/edge.edge.test.mjs +100 -0
- package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +180 -166
- package/plugins/gates/hooks/gates/neutral-spanish/index.mjs +79 -85
- package/plugins/gates/hooks/gates/neutral-spanish/neutral-spanish.edge.test.mjs +80 -0
- package/plugins/gates/hooks/gates/never-assume/index.mjs +58 -55
- package/plugins/gates/hooks/gates/never-assume/never-assume.edge.test.mjs +71 -0
- package/plugins/gates/hooks/gates/never-assume/test.mjs +95 -78
- package/plugins/gates/hooks/gates/no-blocking/index.mjs +148 -142
- package/plugins/gates/hooks/gates/no-blocking/no-blocking.edge.test.mjs +109 -0
- package/plugins/gates/hooks/gates/no-memory-dependency/index.mjs +123 -120
- package/plugins/gates/hooks/gates/no-memory-dependency/no-memory-dependency.edge.test.mjs +83 -0
- package/plugins/gates/hooks/gates/no-reconfirm/no-reconfirm.edge.test.mjs +91 -0
- package/plugins/gates/hooks/gates/protected-paths/index.mjs +144 -147
- package/plugins/gates/hooks/gates/protected-paths/protected-paths.edge.test.mjs +126 -0
- package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +89 -64
- package/plugins/gates/hooks/gates/recurrence-lock/recurrence-lock.edge.test.mjs +102 -0
- package/plugins/gates/hooks/gates/reuse-before-build/edge.edge.test.mjs +94 -0
- package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +159 -150
- package/plugins/gates/hooks/gates/risk-level/index.mjs +263 -203
- package/plugins/gates/hooks/gates/risk-level/risk-level.edge.test.mjs +116 -0
- package/plugins/gates/hooks/gates/risk-level/test.mjs +153 -125
- package/plugins/gates/hooks/gates/root-cause-first/index.mjs +56 -50
- package/plugins/gates/hooks/gates/root-cause-first/root-cause-first.edge.test.mjs +58 -0
- package/plugins/gates/hooks/gates/root-whitelist/index.mjs +116 -111
- package/plugins/gates/hooks/gates/root-whitelist/root-whitelist.edge.test.mjs +97 -0
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +184 -99
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/rule-skill-autodiscovery.edge.test.mjs +126 -0
- package/plugins/gates/hooks/gates/sdd-specs/edge.edge.test.mjs +130 -0
- package/plugins/gates/hooks/gates/sdd-specs/index.mjs +256 -251
- package/plugins/gates/hooks/gates/stop-pending/index.mjs +159 -0
- package/plugins/gates/hooks/gates/stop-pending/test.mjs +114 -0
- package/plugins/gates/hooks/gates/test-after-implementation/index.mjs +101 -101
- package/plugins/gates/hooks/gates/test-after-implementation/test-after-implementation.edge.test.mjs +72 -0
- package/plugins/gates/hooks/gates/test-matrix/edge.edge.test.mjs +63 -0
- package/plugins/gates/hooks/gates/test-matrix/index.mjs +187 -181
- package/plugins/gates/hooks/gates/test-matrix/test.mjs +114 -87
- package/plugins/gates/hooks/gates/tool-map/edge.edge.test.mjs +62 -0
- package/plugins/gates/hooks/gates/tool-map/index.mjs +143 -140
- package/plugins/gates/hooks/hooks.json +346 -286
- package/plugins/gates/hooks/lib/__tests__/config.test.mjs +173 -154
- package/plugins/gates/hooks/lib/__tests__/hook-io.test.mjs +210 -154
- package/plugins/gates/hooks/lib/__tests__/signals.test.mjs +114 -0
- package/plugins/gates/hooks/lib/config.mjs +172 -165
- package/plugins/gates/hooks/lib/hook-io-config.edge.test.mjs +189 -0
- package/plugins/gates/hooks/lib/hook-io.mjs +318 -208
- package/plugins/gates/hooks/lib/signals.mjs +127 -0
- package/plugins/gates/hooks/wiring-check.mjs +227 -0
- package/plugins/tasks/.claude-plugin/plugin.json +14 -0
- package/plugins/tasks/hooks/__tests__/register-requests.test.mjs +100 -0
- package/plugins/tasks/hooks/__tests__/session-tasks.test.mjs +95 -0
- package/plugins/tasks/hooks/hooks.json +26 -0
- package/plugins/tasks/hooks/lib/__tests__/task-store.test.mjs +180 -132
- package/plugins/tasks/hooks/lib/task-store.mjs +197 -159
- package/plugins/tasks/hooks/register-requests.mjs +141 -108
- package/plugins/tasks/hooks/session-tasks.mjs +108 -0
- 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,
|
|
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
|
|
246
|
-
'Restart the session (or run /plugin) for the gates to load.',
|
|
247
|
-
);
|
|
248
|
-
} else {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
255
|
-
|
|
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
|
+
}
|