@spardutti/claude-skills 2.32.0 → 2.32.1
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/bin/cli.mjs +4 -158
- package/lib/post-install.mjs +168 -0
- package/package.json +1 -1
package/bin/cli.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { confirm
|
|
3
|
+
import { confirm } from "@inquirer/prompts";
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { readFileSync } from "node:fs";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -8,16 +8,8 @@ import { dirname, join } from "node:path";
|
|
|
8
8
|
import { fetchSkills, fetchCommands, fetchAgents } from "../lib/github.mjs";
|
|
9
9
|
import { promptSkillSelection, promptCommandSelection, promptRemoval } from "../lib/prompt.mjs";
|
|
10
10
|
import { installSkills, installCommands, installRequiredAgents } from "../lib/install.mjs";
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
detectStack,
|
|
14
|
-
writeGauntletConf,
|
|
15
|
-
autoModeRuleStatus,
|
|
16
|
-
writeAutoModeRule,
|
|
17
|
-
} from "../lib/setup-hook.mjs";
|
|
18
|
-
import { makeLocalSource, reportToolNeeds } from "../lib/local.mjs";
|
|
19
|
-
import { scaffoldStryker } from "../lib/scaffold-stryker.mjs";
|
|
20
|
-
import { setupClaudeMd } from "../lib/setup-claude-md.mjs";
|
|
11
|
+
import { makeLocalSource } from "../lib/local.mjs";
|
|
12
|
+
import { runPostInstall } from "../lib/post-install.mjs";
|
|
21
13
|
import {
|
|
22
14
|
readManifest, writeManifest, computeOrphans, computeRemovals, scanInstalled, removeArtifacts,
|
|
23
15
|
MANIFEST_FILE,
|
|
@@ -165,153 +157,7 @@ async function main() {
|
|
|
165
157
|
process.exit(0);
|
|
166
158
|
}
|
|
167
159
|
|
|
168
|
-
|
|
169
|
-
// Offered whenever anything was installed: the skill gates need skills, but the
|
|
170
|
-
// gauntlet guards any repo with a test runner, commands-only installs included.
|
|
171
|
-
if (selectedSkills.length > 0 || selectedCommands.length > 0) {
|
|
172
|
-
console.log();
|
|
173
|
-
const shouldSetup = await confirm({
|
|
174
|
-
message: "Set up hooks — skill evaluation before edits, verification after? (Recommended)",
|
|
175
|
-
default: true,
|
|
176
|
-
});
|
|
177
|
-
if (shouldSetup) {
|
|
178
|
-
console.log();
|
|
179
|
-
await setupHook();
|
|
180
|
-
await setupClaudeMd();
|
|
181
|
-
|
|
182
|
-
// In auto mode the classifier denies the gate's ack, and it only reads
|
|
183
|
-
// autoMode.allow from the user's own ~/.claude/settings.json. Without this
|
|
184
|
-
// the gate cannot be satisfied at all, and the session cannot fix it
|
|
185
|
-
// either — an agent editing permission settings is denied by the same
|
|
186
|
-
// classifier. The installer is the only thing here running as the user.
|
|
187
|
-
const autoModeStatus = await autoModeRuleStatus();
|
|
188
|
-
if (autoModeStatus === "unreadable") {
|
|
189
|
-
console.log();
|
|
190
|
-
console.log(` ${chalk.yellow("!")} ${chalk.dim("~/.claude/settings.json is not valid JSON — skipping the auto mode rule.")}`);
|
|
191
|
-
} else if (autoModeStatus !== "current") {
|
|
192
|
-
console.log();
|
|
193
|
-
console.log(chalk.dim(" Auto mode's classifier denies the gate's acknowledgement files, and"));
|
|
194
|
-
console.log(chalk.dim(" clearing that needs a rule in your own ~/.claude/settings.json. Claude"));
|
|
195
|
-
console.log(chalk.dim(" cannot add it — editing permission settings is denied to agents too."));
|
|
196
|
-
const addRule = await confirm({
|
|
197
|
-
message:
|
|
198
|
-
autoModeStatus === "stale"
|
|
199
|
-
? "Update the auto mode allow rule in ~/.claude/settings.json? (Recommended)"
|
|
200
|
-
: "Add an auto mode allow rule to ~/.claude/settings.json? (Recommended)",
|
|
201
|
-
default: true,
|
|
202
|
-
});
|
|
203
|
-
if (addRule) {
|
|
204
|
-
try {
|
|
205
|
-
const written = await writeAutoModeRule();
|
|
206
|
-
console.log(` Settings updated: ${written}`);
|
|
207
|
-
} catch (err) {
|
|
208
|
-
console.log(` ${chalk.yellow("!")} ${chalk.dim(err.message)}`);
|
|
209
|
-
}
|
|
210
|
-
} else {
|
|
211
|
-
console.log(` ${chalk.dim("Skipped. In auto mode the gate will block until you add it via /permissions.")}`);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Report what the gauntlet will actually run, so a silent no-op is visible.
|
|
216
|
-
// This asks the installed hook itself — never a second copy of its logic.
|
|
217
|
-
const gates = await detectStack(CWD);
|
|
218
|
-
if (gates) {
|
|
219
|
-
for (const gate of gates.split(";").map((g) => g.trim()).filter(Boolean)) {
|
|
220
|
-
console.log(` ${chalk.dim(`Gauntlet gate: ${gate}`)}`);
|
|
221
|
-
}
|
|
222
|
-
} else {
|
|
223
|
-
console.log(` ${chalk.yellow("!")} ${chalk.dim("Gauntlet found no test runner here — it will stay asleep.")}`);
|
|
224
|
-
console.log();
|
|
225
|
-
const wantConf = await confirm({
|
|
226
|
-
message: "Point it at your test command now?",
|
|
227
|
-
default: false,
|
|
228
|
-
});
|
|
229
|
-
if (wantConf) {
|
|
230
|
-
const cmd = (await input({ message: "Test command:" })).trim();
|
|
231
|
-
if (cmd) {
|
|
232
|
-
const written = await writeGauntletConf(CWD, cmd);
|
|
233
|
-
console.log(` Config written: ${written.replace(CWD + "/", "")}`);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// --- What the installed commands need that this project does not have ---
|
|
241
|
-
// /ship's mutation check is per project, so a monorepo needs a tool per project.
|
|
242
|
-
if (selectedCommands.some((c) => c.fileName === "ship.md")) {
|
|
243
|
-
const needs = await reportToolNeeds(CWD);
|
|
244
|
-
console.log();
|
|
245
|
-
if (needs.length === 0) {
|
|
246
|
-
console.log(` ${chalk.dim("/ship's mutation check has a tool in every project here.")}`);
|
|
247
|
-
} else {
|
|
248
|
-
console.log(` ${chalk.yellow("!")} ${chalk.bold("/ship needs a mutation tool per project")} ${chalk.dim("— without one it reports UNPROVEN, and never blocks.")}`);
|
|
249
|
-
if (!selectedSkills.some((s) => s.dirName === "testing-best-practices")) {
|
|
250
|
-
console.log(` ${chalk.dim("Setting these up has traps — install the testing-best-practices skill for MUTATION-TESTING.md.")}`);
|
|
251
|
-
}
|
|
252
|
-
for (const n of needs.filter((n) => !n.ignorerOnly)) {
|
|
253
|
-
console.log(` ${chalk.bold(n.label)} needs ${n.tool}`);
|
|
254
|
-
console.log(` ${chalk.cyan(n.install)}`);
|
|
255
|
-
console.log(` ${chalk.dim(n.config)}`);
|
|
256
|
-
for (const line of n.also ?? []) console.log(` ${chalk.dim(line)}`);
|
|
257
|
-
}
|
|
258
|
-
for (const n of needs.filter((n) => n.ignorerOnly)) {
|
|
259
|
-
console.log(` ${chalk.bold(n.label)} runs ${n.tool} without the class-name ignorer`);
|
|
260
|
-
console.log(` ${chalk.dim("~40% of a first React run is mutants on className strings, which no test can honestly kill")}`);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Printing five steps meant every project was set up by hand, and the one
|
|
264
|
-
// step people skipped was the class-name ignorer — the reason a first run
|
|
265
|
-
// comes back with dozens of mutants on className strings that no test can
|
|
266
|
-
// honestly kill. Write the two files instead of describing them.
|
|
267
|
-
const stryker = needs.filter((n) => n.tool === "Stryker");
|
|
268
|
-
if (stryker.length > 0) {
|
|
269
|
-
console.log();
|
|
270
|
-
const doScaffold = await confirm({
|
|
271
|
-
message: `Write Stryker's config and the class-name ignorer for ${stryker.length} project(s)?`,
|
|
272
|
-
default: true,
|
|
273
|
-
});
|
|
274
|
-
if (doScaffold) {
|
|
275
|
-
const installs = [];
|
|
276
|
-
const apiInstalls = [];
|
|
277
|
-
const vitest = [];
|
|
278
|
-
for (const n of stryker) {
|
|
279
|
-
const r = await scaffoldStryker(CWD, n.rel);
|
|
280
|
-
for (const f of r.wrote) console.log(` Wrote: ${f}`);
|
|
281
|
-
for (const f of r.patched) console.log(` Added the ignorer to: ${f}`);
|
|
282
|
-
for (const f of r.kept) console.log(` ${chalk.dim(`Kept yours: ${f}`)}`);
|
|
283
|
-
for (const f of r.unreadable) {
|
|
284
|
-
console.log(` ${chalk.yellow("!")} ${f} is not readable as JSON — left alone, add the ignorer by hand`);
|
|
285
|
-
}
|
|
286
|
-
// A project that already runs Stryker needs neither the install nor
|
|
287
|
-
// the vitest exclude repeated at it; it only lacked the ignorer.
|
|
288
|
-
if (n.ignorerOnly) {
|
|
289
|
-
apiInstalls.push(r.apiInstall);
|
|
290
|
-
} else {
|
|
291
|
-
installs.push(r.install);
|
|
292
|
-
vitest.push(r.vitest);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
// Left to the reader on purpose: installing touches the lockfile and
|
|
296
|
-
// the network, and editing an existing vitest config means parsing
|
|
297
|
-
// someone's plugins and aliases, where being wrong breaks their tests.
|
|
298
|
-
if (installs.length > 0) {
|
|
299
|
-
console.log(`\n ${chalk.yellow("!")} ${chalk.bold("Two steps left, per project:")}`);
|
|
300
|
-
for (const c of installs) console.log(` ${chalk.cyan(c)}`);
|
|
301
|
-
for (const v of vitest) console.log(` ${chalk.dim(v)}`);
|
|
302
|
-
}
|
|
303
|
-
// The ignorer imports @stryker-mutator/api, which npm hoists by
|
|
304
|
-
// accident and pnpm does not. Missing, it prints one WARN PluginLoader
|
|
305
|
-
// line, loads nothing, and every className mutant comes back survived.
|
|
306
|
-
if (apiInstalls.length > 0) {
|
|
307
|
-
console.log(`\n ${chalk.yellow("!")} ${chalk.bold("The ignorer needs its own dependency:")}`);
|
|
308
|
-
for (const c of apiInstalls) console.log(` ${chalk.cyan(c)}`);
|
|
309
|
-
console.log(` ${chalk.dim("without it the ignorer silently does not load")}`);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
160
|
+
await runPostInstall({ CWD, selectedSkills, selectedCommands });
|
|
315
161
|
|
|
316
162
|
// --- Record what is now installed ---
|
|
317
163
|
await writeManifest(CWD, {
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// Everything the installer does AFTER the user has chosen what to install:
|
|
2
|
+
// offer the hooks, then report what the installed commands need that this
|
|
3
|
+
// project does not have. Lifted out of a 270-line main() — these two are a
|
|
4
|
+
// phase of their own, they run only when something was installed, and neither
|
|
5
|
+
// decides what gets installed.
|
|
6
|
+
import { confirm, input } from "@inquirer/prompts";
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
import {
|
|
9
|
+
setupHook,
|
|
10
|
+
detectStack,
|
|
11
|
+
writeGauntletConf,
|
|
12
|
+
autoModeRuleStatus,
|
|
13
|
+
writeAutoModeRule,
|
|
14
|
+
} from "./setup-hook.mjs";
|
|
15
|
+
import { reportToolNeeds } from "./local.mjs";
|
|
16
|
+
import { scaffoldStryker } from "./scaffold-stryker.mjs";
|
|
17
|
+
import { setupClaudeMd } from "./setup-claude-md.mjs";
|
|
18
|
+
|
|
19
|
+
export async function runPostInstall({ CWD, selectedSkills, selectedCommands }) {
|
|
20
|
+
// --- Hook setup ---
|
|
21
|
+
// Offered whenever anything was installed: the skill gates need skills, but the
|
|
22
|
+
// gauntlet guards any repo with a test runner, commands-only installs included.
|
|
23
|
+
if (selectedSkills.length > 0 || selectedCommands.length > 0) {
|
|
24
|
+
console.log();
|
|
25
|
+
const shouldSetup = await confirm({
|
|
26
|
+
message: "Set up hooks — skill evaluation before edits, verification after? (Recommended)",
|
|
27
|
+
default: true,
|
|
28
|
+
});
|
|
29
|
+
if (shouldSetup) {
|
|
30
|
+
console.log();
|
|
31
|
+
await setupHook();
|
|
32
|
+
await setupClaudeMd();
|
|
33
|
+
|
|
34
|
+
// In auto mode the classifier denies the gate's ack, and it only reads
|
|
35
|
+
// autoMode.allow from the user's own ~/.claude/settings.json. Without this
|
|
36
|
+
// the gate cannot be satisfied at all, and the session cannot fix it
|
|
37
|
+
// either — an agent editing permission settings is denied by the same
|
|
38
|
+
// classifier. The installer is the only thing here running as the user.
|
|
39
|
+
const autoModeStatus = await autoModeRuleStatus();
|
|
40
|
+
if (autoModeStatus === "unreadable") {
|
|
41
|
+
console.log();
|
|
42
|
+
console.log(` ${chalk.yellow("!")} ${chalk.dim("~/.claude/settings.json is not valid JSON — skipping the auto mode rule.")}`);
|
|
43
|
+
} else if (autoModeStatus !== "current") {
|
|
44
|
+
console.log();
|
|
45
|
+
console.log(chalk.dim(" Auto mode's classifier denies the gate's acknowledgement files, and"));
|
|
46
|
+
console.log(chalk.dim(" clearing that needs a rule in your own ~/.claude/settings.json. Claude"));
|
|
47
|
+
console.log(chalk.dim(" cannot add it — editing permission settings is denied to agents too."));
|
|
48
|
+
const addRule = await confirm({
|
|
49
|
+
message:
|
|
50
|
+
autoModeStatus === "stale"
|
|
51
|
+
? "Update the auto mode allow rule in ~/.claude/settings.json? (Recommended)"
|
|
52
|
+
: "Add an auto mode allow rule to ~/.claude/settings.json? (Recommended)",
|
|
53
|
+
default: true,
|
|
54
|
+
});
|
|
55
|
+
if (addRule) {
|
|
56
|
+
try {
|
|
57
|
+
const written = await writeAutoModeRule();
|
|
58
|
+
console.log(` Settings updated: ${written}`);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.log(` ${chalk.yellow("!")} ${chalk.dim(err.message)}`);
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
console.log(` ${chalk.dim("Skipped. In auto mode the gate will block until you add it via /permissions.")}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Report what the gauntlet will actually run, so a silent no-op is visible.
|
|
68
|
+
// This asks the installed hook itself — never a second copy of its logic.
|
|
69
|
+
const gates = await detectStack(CWD);
|
|
70
|
+
if (gates) {
|
|
71
|
+
for (const gate of gates.split(";").map((g) => g.trim()).filter(Boolean)) {
|
|
72
|
+
console.log(` ${chalk.dim(`Gauntlet gate: ${gate}`)}`);
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
console.log(` ${chalk.yellow("!")} ${chalk.dim("Gauntlet found no test runner here — it will stay asleep.")}`);
|
|
76
|
+
console.log();
|
|
77
|
+
const wantConf = await confirm({
|
|
78
|
+
message: "Point it at your test command now?",
|
|
79
|
+
default: false,
|
|
80
|
+
});
|
|
81
|
+
if (wantConf) {
|
|
82
|
+
const cmd = (await input({ message: "Test command:" })).trim();
|
|
83
|
+
if (cmd) {
|
|
84
|
+
const written = await writeGauntletConf(CWD, cmd);
|
|
85
|
+
console.log(` Config written: ${written.replace(CWD + "/", "")}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// --- What the installed commands need that this project does not have ---
|
|
93
|
+
// /ship's mutation check is per project, so a monorepo needs a tool per project.
|
|
94
|
+
if (selectedCommands.some((c) => c.fileName === "ship.md")) {
|
|
95
|
+
const needs = await reportToolNeeds(CWD);
|
|
96
|
+
console.log();
|
|
97
|
+
if (needs.length === 0) {
|
|
98
|
+
console.log(` ${chalk.dim("/ship's mutation check has a tool in every project here.")}`);
|
|
99
|
+
} else {
|
|
100
|
+
console.log(` ${chalk.yellow("!")} ${chalk.bold("/ship needs a mutation tool per project")} ${chalk.dim("— without one it reports UNPROVEN, and never blocks.")}`);
|
|
101
|
+
if (!selectedSkills.some((s) => s.dirName === "testing-best-practices")) {
|
|
102
|
+
console.log(` ${chalk.dim("Setting these up has traps — install the testing-best-practices skill for MUTATION-TESTING.md.")}`);
|
|
103
|
+
}
|
|
104
|
+
for (const n of needs.filter((n) => !n.ignorerOnly)) {
|
|
105
|
+
console.log(` ${chalk.bold(n.label)} needs ${n.tool}`);
|
|
106
|
+
console.log(` ${chalk.cyan(n.install)}`);
|
|
107
|
+
console.log(` ${chalk.dim(n.config)}`);
|
|
108
|
+
for (const line of n.also ?? []) console.log(` ${chalk.dim(line)}`);
|
|
109
|
+
}
|
|
110
|
+
for (const n of needs.filter((n) => n.ignorerOnly)) {
|
|
111
|
+
console.log(` ${chalk.bold(n.label)} runs ${n.tool} without the class-name ignorer`);
|
|
112
|
+
console.log(` ${chalk.dim("~40% of a first React run is mutants on className strings, which no test can honestly kill")}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Printing five steps meant every project was set up by hand, and the one
|
|
116
|
+
// step people skipped was the class-name ignorer — the reason a first run
|
|
117
|
+
// comes back with dozens of mutants on className strings that no test can
|
|
118
|
+
// honestly kill. Write the two files instead of describing them.
|
|
119
|
+
const stryker = needs.filter((n) => n.tool === "Stryker");
|
|
120
|
+
if (stryker.length > 0) {
|
|
121
|
+
console.log();
|
|
122
|
+
const doScaffold = await confirm({
|
|
123
|
+
message: `Write Stryker's config and the class-name ignorer for ${stryker.length} project(s)?`,
|
|
124
|
+
default: true,
|
|
125
|
+
});
|
|
126
|
+
if (doScaffold) {
|
|
127
|
+
const installs = [];
|
|
128
|
+
const apiInstalls = [];
|
|
129
|
+
const vitest = [];
|
|
130
|
+
for (const n of stryker) {
|
|
131
|
+
const r = await scaffoldStryker(CWD, n.rel);
|
|
132
|
+
for (const f of r.wrote) console.log(` Wrote: ${f}`);
|
|
133
|
+
for (const f of r.patched) console.log(` Added the ignorer to: ${f}`);
|
|
134
|
+
for (const f of r.kept) console.log(` ${chalk.dim(`Kept yours: ${f}`)}`);
|
|
135
|
+
for (const f of r.unreadable) {
|
|
136
|
+
console.log(` ${chalk.yellow("!")} ${f} is not readable as JSON — left alone, add the ignorer by hand`);
|
|
137
|
+
}
|
|
138
|
+
// A project that already runs Stryker needs neither the install nor
|
|
139
|
+
// the vitest exclude repeated at it; it only lacked the ignorer.
|
|
140
|
+
if (n.ignorerOnly) {
|
|
141
|
+
apiInstalls.push(r.apiInstall);
|
|
142
|
+
} else {
|
|
143
|
+
installs.push(r.install);
|
|
144
|
+
vitest.push(r.vitest);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Left to the reader on purpose: installing touches the lockfile and
|
|
148
|
+
// the network, and editing an existing vitest config means parsing
|
|
149
|
+
// someone's plugins and aliases, where being wrong breaks their tests.
|
|
150
|
+
if (installs.length > 0) {
|
|
151
|
+
console.log(`\n ${chalk.yellow("!")} ${chalk.bold("Two steps left, per project:")}`);
|
|
152
|
+
for (const c of installs) console.log(` ${chalk.cyan(c)}`);
|
|
153
|
+
for (const v of vitest) console.log(` ${chalk.dim(v)}`);
|
|
154
|
+
}
|
|
155
|
+
// The ignorer imports @stryker-mutator/api, which npm hoists by
|
|
156
|
+
// accident and pnpm does not. Missing, it prints one WARN PluginLoader
|
|
157
|
+
// line, loads nothing, and every className mutant comes back survived.
|
|
158
|
+
if (apiInstalls.length > 0) {
|
|
159
|
+
console.log(`\n ${chalk.yellow("!")} ${chalk.bold("The ignorer needs its own dependency:")}`);
|
|
160
|
+
for (const c of apiInstalls) console.log(` ${chalk.cyan(c)}`);
|
|
161
|
+
console.log(` ${chalk.dim("without it the ignorer silently does not load")}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
}
|