@balazsbarta/mp-skills 0.1.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/.agents/skills/brainstorming/SKILL.md +201 -0
- package/.agents/skills/brainstorming/references/option-evaluation.md +64 -0
- package/.agents/skills/brainstorming/references/questioning-playbook.md +57 -0
- package/.agents/skills/brainstorming/references/repo-analysis.md +60 -0
- package/.agents/skills/conventional-commits/SKILL.md +124 -0
- package/.agents/skills/conventional-commits/references/commit-types-scopes.md +75 -0
- package/.agents/skills/conventional-commits/references/semantic-release.md +71 -0
- package/.agents/skills/jest/SKILL.md +219 -0
- package/.agents/skills/jest/references/common-errors.md +274 -0
- package/.agents/skills/jest/references/configuration.md +175 -0
- package/.agents/skills/jest/references/embedded-docs.md +44 -0
- package/.agents/skills/jest/references/mocking.md +206 -0
- package/.agents/skills/jest/references/remote-docs.md +19 -0
- package/.agents/skills/jest/references/snapshot-testing.md +181 -0
- package/.agents/skills/jest/references/transforms.md +216 -0
- package/.agents/skills/maestro/SKILL.md +230 -0
- package/.agents/skills/maestro/references/assertions-commands.md +259 -0
- package/.agents/skills/maestro/references/common-errors.md +273 -0
- package/.agents/skills/maestro/references/eas-ci-integration.md +219 -0
- package/.agents/skills/maestro/references/flow-authoring.md +224 -0
- package/.agents/skills/maestro/references/remote-docs.md +23 -0
- package/.agents/skills/mastra/SKILL.md +159 -0
- package/.agents/skills/mastra/references/common-errors.md +535 -0
- package/.agents/skills/mastra/references/create-mastra.md +220 -0
- package/.agents/skills/mastra/references/embedded-docs.md +123 -0
- package/.agents/skills/mastra/references/migration-guide.md +180 -0
- package/.agents/skills/mastra/references/remote-docs.md +193 -0
- package/.agents/skills/next-js/SKILL.md +209 -0
- package/.agents/skills/next-js/references/api-routes.md +213 -0
- package/.agents/skills/next-js/references/app-router.md +206 -0
- package/.agents/skills/next-js/references/caching-revalidation.md +211 -0
- package/.agents/skills/next-js/references/common-errors.md +251 -0
- package/.agents/skills/next-js/references/embedded-docs.md +43 -0
- package/.agents/skills/next-js/references/metadata-seo.md +257 -0
- package/.agents/skills/next-js/references/remote-docs.md +22 -0
- package/.agents/skills/playwright/SKILL.md +218 -0
- package/.agents/skills/playwright/references/ci-configuration.md +208 -0
- package/.agents/skills/playwright/references/common-errors.md +258 -0
- package/.agents/skills/playwright/references/embedded-docs.md +41 -0
- package/.agents/skills/playwright/references/fixtures-assertions.md +208 -0
- package/.agents/skills/playwright/references/page-objects.md +167 -0
- package/.agents/skills/playwright/references/remote-docs.md +23 -0
- package/.agents/skills/playwright/references/visual-regression.md +206 -0
- package/.agents/skills/pull-request-lifecycle/SKILL.md +116 -0
- package/.agents/skills/pull-request-lifecycle/references/changelog-versioning.md +72 -0
- package/.agents/skills/pull-request-lifecycle/references/merge-strategies.md +33 -0
- package/.agents/skills/pull-request-lifecycle/references/pr-description-template.md +72 -0
- package/.agents/skills/pull-request-lifecycle/references/review-process.md +54 -0
- package/.agents/skills/pull-request-lifecycle/scripts/code_review.py +220 -0
- package/.agents/skills/react-native-expo/SKILL.md +212 -0
- package/.agents/skills/react-native-expo/references/common-errors.md +251 -0
- package/.agents/skills/react-native-expo/references/eas-build-submit.md +238 -0
- package/.agents/skills/react-native-expo/references/embedded-docs.md +42 -0
- package/.agents/skills/react-native-expo/references/native-modules.md +181 -0
- package/.agents/skills/react-native-expo/references/navigation-setup.md +229 -0
- package/.agents/skills/react-native-expo/references/remote-docs.md +23 -0
- package/.agents/skills/supabase/SKILL.md +216 -0
- package/.agents/skills/supabase/references/auth-setup.md +206 -0
- package/.agents/skills/supabase/references/common-errors.md +285 -0
- package/.agents/skills/supabase/references/edge-functions.md +178 -0
- package/.agents/skills/supabase/references/embedded-docs.md +43 -0
- package/.agents/skills/supabase/references/migrations.md +193 -0
- package/.agents/skills/supabase/references/remote-docs.md +24 -0
- package/.agents/skills/supabase/references/rls-policies.md +187 -0
- package/.agents/skills/supabase/references/storage.md +182 -0
- package/.agents/skills/task-breakdown/SKILL.md +179 -0
- package/.agents/skills/task-breakdown/references/acceptance-criteria.md +165 -0
- package/.agents/skills/task-breakdown/references/epic-story-format.md +209 -0
- package/.agents/skills/task-breakdown/references/estimation-guide.md +140 -0
- package/.agents/skills/vitest/SKILL.md +219 -0
- package/.agents/skills/vitest/references/common-errors.md +271 -0
- package/.agents/skills/vitest/references/component-testing.md +182 -0
- package/.agents/skills/vitest/references/configuration.md +184 -0
- package/.agents/skills/vitest/references/coverage.md +179 -0
- package/.agents/skills/vitest/references/embedded-docs.md +43 -0
- package/.agents/skills/vitest/references/mocking.md +182 -0
- package/.agents/skills/vitest/references/remote-docs.md +22 -0
- package/README.md +235 -0
- package/package.json +20 -0
- package/scripts/skills.mjs +849 -0
|
@@ -0,0 +1,849 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { constants as fsConstants } from "node:fs";
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import readline from "node:readline/promises";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
|
|
11
|
+
const TARGETS = ["codex", "claude-code", "opencode", "gemini-cli", "cursor", "generic"];
|
|
12
|
+
const ALL_TARGETS = ["codex", "claude-code", "opencode", "gemini-cli", "cursor"];
|
|
13
|
+
const TARGET_ALIASES = new Map([
|
|
14
|
+
["codex", "codex"],
|
|
15
|
+
["claude", "claude-code"],
|
|
16
|
+
["claude-code", "claude-code"],
|
|
17
|
+
["opencode", "opencode"],
|
|
18
|
+
["gemini", "gemini-cli"],
|
|
19
|
+
["gemini-cli", "gemini-cli"],
|
|
20
|
+
["cursor", "cursor"],
|
|
21
|
+
["cursor-cli", "cursor"],
|
|
22
|
+
["generic", "generic"],
|
|
23
|
+
["other", "generic"],
|
|
24
|
+
["others", "generic"],
|
|
25
|
+
]);
|
|
26
|
+
const SOURCE_ALIASES = new Map([
|
|
27
|
+
["mp-skills", "balazsbarta/mp-skills"],
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const HELP_TEXT = `
|
|
31
|
+
skills - Install skill packs for coding assistants
|
|
32
|
+
|
|
33
|
+
Usage:
|
|
34
|
+
skills add [source] [options]
|
|
35
|
+
skills select [source] [options] # alias for interactive add
|
|
36
|
+
skills targets
|
|
37
|
+
skills --help
|
|
38
|
+
|
|
39
|
+
Examples:
|
|
40
|
+
skills add mp-skills
|
|
41
|
+
skills add mp-skills --mode copy
|
|
42
|
+
skills add . --target codex --mode symlink
|
|
43
|
+
skills add . --target cursor --cursor-project /path/to/project
|
|
44
|
+
skills add . --target generic --dest /path/to/agent/skills
|
|
45
|
+
skills select mp-skills
|
|
46
|
+
|
|
47
|
+
Arguments:
|
|
48
|
+
source Optional local path or GitHub repo slug (owner/repo)
|
|
49
|
+
"mp-skills" maps to "balazsbarta/mp-skills"
|
|
50
|
+
|
|
51
|
+
Options:
|
|
52
|
+
--target <name> (add non-interactive mode) codex | claude-code | opencode | gemini-cli | cursor | generic | all
|
|
53
|
+
--mode <mode> (add/select) copy | symlink
|
|
54
|
+
--cursor-project <dir> (add/select) Project root for Cursor rules
|
|
55
|
+
--dest <dir> (add/select) Destination folder used by target=generic
|
|
56
|
+
-h, --help Show help
|
|
57
|
+
|
|
58
|
+
Interactive install:
|
|
59
|
+
"skills add" starts guided prompts by default (targets, skills, mode).
|
|
60
|
+
"skills select" behaves the same (compatibility alias).
|
|
61
|
+
Unselected already-installed skills are kept.
|
|
62
|
+
|
|
63
|
+
Non-interactive install:
|
|
64
|
+
Provide --target to run "skills add" without prompts.
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
const TARGET_PROMPT_OPTIONS = [
|
|
68
|
+
{ value: "codex", label: "codex" },
|
|
69
|
+
{ value: "claude-code", label: "claude-code" },
|
|
70
|
+
{ value: "opencode", label: "opencode" },
|
|
71
|
+
{ value: "gemini-cli", label: "gemini-cli" },
|
|
72
|
+
{ value: "cursor", label: "cursor" },
|
|
73
|
+
{ value: "generic", label: "generic" },
|
|
74
|
+
{ value: "all", label: "all (codex, claude-code, opencode, gemini-cli, cursor)" },
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
const MODE_PROMPT_OPTIONS = [
|
|
78
|
+
{ value: "copy", label: "copy" },
|
|
79
|
+
{ value: "symlink", label: "symlink" },
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
async function main() {
|
|
83
|
+
const argv = process.argv.slice(2);
|
|
84
|
+
if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) {
|
|
85
|
+
print(HELP_TEXT.trim());
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const command = argv[0];
|
|
90
|
+
const args = argv.slice(1);
|
|
91
|
+
|
|
92
|
+
if (command === "targets") {
|
|
93
|
+
print("Supported targets:");
|
|
94
|
+
for (const target of TARGETS) {
|
|
95
|
+
print(`- ${target}`);
|
|
96
|
+
}
|
|
97
|
+
print("- all");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (command === "add") {
|
|
102
|
+
await runAddCommand(args);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (command === "select") {
|
|
107
|
+
await runSelectCommand(args);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
fatal(`Unknown command: ${command}\n\n${HELP_TEXT.trim()}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function runAddCommand(args) {
|
|
115
|
+
const options = parseAddArgs(args);
|
|
116
|
+
if (!options.hasExplicitTarget) {
|
|
117
|
+
await runInteractiveInstall(options);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const cleanupPaths = [];
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const context = await loadInstallContext(options.sourceArg, cleanupPaths);
|
|
125
|
+
const targets = resolveTargets(options.targetArg);
|
|
126
|
+
const effectiveMode = normalizeModeForSource(options.mode || "copy", context.source);
|
|
127
|
+
const summary = await installToTargets({
|
|
128
|
+
targets,
|
|
129
|
+
skills: context.skills,
|
|
130
|
+
mode: effectiveMode,
|
|
131
|
+
cursorProject: options.cursorProject || process.cwd(),
|
|
132
|
+
dest: options.dest,
|
|
133
|
+
});
|
|
134
|
+
printInstallSummary(summary);
|
|
135
|
+
} finally {
|
|
136
|
+
await cleanupTemporaryPaths(cleanupPaths);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function runSelectCommand(args) {
|
|
141
|
+
const options = parseSelectArgs(args);
|
|
142
|
+
await runInteractiveInstall(options);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function runInteractiveInstall(options) {
|
|
146
|
+
ensureInteractiveTerminal();
|
|
147
|
+
|
|
148
|
+
const cleanupPaths = [];
|
|
149
|
+
try {
|
|
150
|
+
const context = await loadInstallContext(options.sourceArg, cleanupPaths);
|
|
151
|
+
const selection = await promptInstallSelection(context.skills, options);
|
|
152
|
+
const selectedSkills = selectSkillsByName(context.skills, selection.skillNames);
|
|
153
|
+
const effectiveMode = normalizeModeForSource(selection.mode, context.source);
|
|
154
|
+
|
|
155
|
+
const summary = await installToTargets({
|
|
156
|
+
targets: selection.targets,
|
|
157
|
+
skills: selectedSkills,
|
|
158
|
+
mode: effectiveMode,
|
|
159
|
+
cursorProject: selection.cursorProject,
|
|
160
|
+
dest: selection.dest,
|
|
161
|
+
});
|
|
162
|
+
printInstallSummary(summary);
|
|
163
|
+
} finally {
|
|
164
|
+
await cleanupTemporaryPaths(cleanupPaths);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function loadInstallContext(sourceArg, cleanupPaths) {
|
|
169
|
+
const source = await resolveSource(sourceArg, cleanupPaths);
|
|
170
|
+
const skillsRoot = await findSkillsRoot(source.root);
|
|
171
|
+
const skills = await readSkills(skillsRoot);
|
|
172
|
+
if (skills.length === 0) {
|
|
173
|
+
fatal(`No skills found in: ${skillsRoot}`);
|
|
174
|
+
}
|
|
175
|
+
return { source, skillsRoot, skills };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function normalizeModeForSource(mode, source) {
|
|
179
|
+
if (source.isTemporary && mode === "symlink") {
|
|
180
|
+
print("Source is temporary (GitHub clone), falling back to copy mode.");
|
|
181
|
+
return "copy";
|
|
182
|
+
}
|
|
183
|
+
return mode;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function installToTargets({ targets, skills, mode, cursorProject, dest }) {
|
|
187
|
+
const summary = [];
|
|
188
|
+
|
|
189
|
+
for (const target of targets) {
|
|
190
|
+
if (target === "codex") {
|
|
191
|
+
const installed = await installCodex(skills, mode);
|
|
192
|
+
summary.push(`codex: ${installed} skills`);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (target === "claude-code") {
|
|
196
|
+
const installed = await installClaudeCode(skills, mode);
|
|
197
|
+
summary.push(`claude-code: ${installed} skills`);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (target === "opencode") {
|
|
201
|
+
const installed = await installOpenCode(skills, mode);
|
|
202
|
+
summary.push(`opencode: ${installed} skills`);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (target === "gemini-cli") {
|
|
206
|
+
const installed = await installGemini(skills, mode);
|
|
207
|
+
summary.push(`gemini-cli: ${installed} commands`);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (target === "cursor") {
|
|
211
|
+
const effectiveCursorProject = cursorProject || process.cwd();
|
|
212
|
+
const installed = await installCursor(skills, mode, effectiveCursorProject);
|
|
213
|
+
summary.push(`cursor: ${installed} rules`);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (target === "generic") {
|
|
217
|
+
const installed = await installGeneric(skills, mode, dest);
|
|
218
|
+
summary.push(`generic: ${installed} skills`);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
fatal(`Unsupported target: ${target}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return summary;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function printInstallSummary(summary) {
|
|
228
|
+
print("\nInstall complete:");
|
|
229
|
+
for (const item of summary) {
|
|
230
|
+
print(`- ${item}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function cleanupTemporaryPaths(cleanupPaths) {
|
|
235
|
+
for (const tempPath of cleanupPaths) {
|
|
236
|
+
await fs.rm(tempPath, { recursive: true, force: true });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function ensureInteractiveTerminal() {
|
|
241
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
fatal(
|
|
245
|
+
'Interactive mode requires a TTY terminal. Use "skills add --target <name>" for non-interactive installs.',
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function promptInstallSelection(skills, options) {
|
|
250
|
+
const rl = readline.createInterface({
|
|
251
|
+
input: process.stdin,
|
|
252
|
+
output: process.stdout,
|
|
253
|
+
terminal: true,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
print("");
|
|
258
|
+
print("Interactive skill installer");
|
|
259
|
+
print("===========================");
|
|
260
|
+
|
|
261
|
+
const rawTargets = await promptMultiChoice({
|
|
262
|
+
rl,
|
|
263
|
+
title: "Select target agent(s):",
|
|
264
|
+
prompt: "Enter target number(s), separated by commas",
|
|
265
|
+
options: TARGET_PROMPT_OPTIONS,
|
|
266
|
+
required: true,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const targets = normalizeInteractiveTargets(rawTargets);
|
|
270
|
+
const skillOptions = [
|
|
271
|
+
...skills.map((skill) => ({ value: skill.name, label: skill.name })),
|
|
272
|
+
{ value: "__all__", label: "all (install every discovered skill)" },
|
|
273
|
+
];
|
|
274
|
+
|
|
275
|
+
const rawSkillNames = await promptMultiChoice({
|
|
276
|
+
rl,
|
|
277
|
+
title: "Select skill(s) to install:",
|
|
278
|
+
prompt: "Enter skill number(s), separated by commas",
|
|
279
|
+
options: skillOptions,
|
|
280
|
+
required: true,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const skillNames =
|
|
284
|
+
rawSkillNames.includes("__all__") ? skills.map((skill) => skill.name) : rawSkillNames;
|
|
285
|
+
if (skillNames.length === 0) {
|
|
286
|
+
throw new Error("At least one skill must be selected.");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const mode =
|
|
290
|
+
options.mode ||
|
|
291
|
+
(await promptSingleChoice({
|
|
292
|
+
rl,
|
|
293
|
+
title: "Select install mode:",
|
|
294
|
+
prompt: "Enter mode number",
|
|
295
|
+
options: MODE_PROMPT_OPTIONS,
|
|
296
|
+
}));
|
|
297
|
+
|
|
298
|
+
let cursorProject = options.cursorProject;
|
|
299
|
+
let dest = options.dest;
|
|
300
|
+
|
|
301
|
+
if (targets.includes("cursor") && !cursorProject) {
|
|
302
|
+
cursorProject = await promptRequiredText({
|
|
303
|
+
rl,
|
|
304
|
+
label: "Cursor project path",
|
|
305
|
+
defaultValue: process.cwd(),
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (targets.includes("generic") && !dest) {
|
|
310
|
+
dest = await promptRequiredText({
|
|
311
|
+
rl,
|
|
312
|
+
label: "Destination path for generic target",
|
|
313
|
+
defaultValue: "",
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
print("");
|
|
318
|
+
print("Selection summary:");
|
|
319
|
+
print(`- targets: ${targets.join(", ")}`);
|
|
320
|
+
print(`- skills: ${skillNames.join(", ")}`);
|
|
321
|
+
print(`- mode: ${mode}`);
|
|
322
|
+
if (targets.includes("cursor")) {
|
|
323
|
+
print(`- cursor project: ${path.resolve(cursorProject || process.cwd())}`);
|
|
324
|
+
}
|
|
325
|
+
if (targets.includes("generic")) {
|
|
326
|
+
print(`- generic destination: ${path.resolve(dest || "")}`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const confirmed = await promptYesNo({
|
|
330
|
+
rl,
|
|
331
|
+
question: "Continue with installation?",
|
|
332
|
+
defaultYes: true,
|
|
333
|
+
});
|
|
334
|
+
if (!confirmed) {
|
|
335
|
+
throw new Error("Installation canceled.");
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
targets,
|
|
340
|
+
skillNames,
|
|
341
|
+
mode,
|
|
342
|
+
cursorProject: cursorProject || process.cwd(),
|
|
343
|
+
dest,
|
|
344
|
+
};
|
|
345
|
+
} finally {
|
|
346
|
+
rl.close();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function normalizeInteractiveTargets(rawTargets) {
|
|
351
|
+
if (rawTargets.includes("all")) {
|
|
352
|
+
return [...ALL_TARGETS];
|
|
353
|
+
}
|
|
354
|
+
return rawTargets;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function selectSkillsByName(skills, names) {
|
|
358
|
+
const uniqueNames = [...new Set(names)];
|
|
359
|
+
const selected = skills.filter((skill) => uniqueNames.includes(skill.name));
|
|
360
|
+
if (selected.length === 0) {
|
|
361
|
+
fatal("No valid skills selected.");
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const selectedNames = new Set(selected.map((skill) => skill.name));
|
|
365
|
+
const missing = uniqueNames.filter((name) => !selectedNames.has(name));
|
|
366
|
+
if (missing.length > 0) {
|
|
367
|
+
fatal(`Unknown selected skill(s): ${missing.join(", ")}`);
|
|
368
|
+
}
|
|
369
|
+
return selected;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function promptSingleChoice({ rl, title, prompt, options }) {
|
|
373
|
+
while (true) {
|
|
374
|
+
print("");
|
|
375
|
+
print(title);
|
|
376
|
+
printNumberedOptions(options);
|
|
377
|
+
const answer = (await rl.question(`${prompt}: `)).trim();
|
|
378
|
+
|
|
379
|
+
const indexes = parseIndexInput(answer, options.length, false);
|
|
380
|
+
if (indexes) {
|
|
381
|
+
return options[indexes[0] - 1].value;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const retry = await promptRetry({ rl, reason: "Invalid selection." });
|
|
385
|
+
if (!retry) {
|
|
386
|
+
throw new Error("Installation canceled.");
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function promptMultiChoice({ rl, title, prompt, options, required }) {
|
|
392
|
+
while (true) {
|
|
393
|
+
print("");
|
|
394
|
+
print(title);
|
|
395
|
+
printNumberedOptions(options);
|
|
396
|
+
const answer = (await rl.question(`${prompt}: `)).trim();
|
|
397
|
+
|
|
398
|
+
const indexes = parseIndexInput(answer, options.length, true);
|
|
399
|
+
if (indexes && (!required || indexes.length > 0)) {
|
|
400
|
+
const values = indexes.map((index) => options[index - 1].value);
|
|
401
|
+
return dedupe(values);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const retry = await promptRetry({ rl, reason: "Invalid selection." });
|
|
405
|
+
if (!retry) {
|
|
406
|
+
throw new Error("Installation canceled.");
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function printNumberedOptions(options) {
|
|
412
|
+
for (let i = 0; i < options.length; i += 1) {
|
|
413
|
+
print(` ${i + 1}) ${options[i].label}`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function parseIndexInput(raw, max, allowMultiple) {
|
|
418
|
+
if (!raw) {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const tokens = raw.split(",").map((token) => token.trim()).filter(Boolean);
|
|
423
|
+
if (tokens.length === 0) {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
if (!allowMultiple && tokens.length !== 1) {
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const indexes = [];
|
|
431
|
+
for (const token of tokens) {
|
|
432
|
+
if (!/^\d+$/.test(token)) {
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const index = Number(token);
|
|
437
|
+
if (!Number.isInteger(index) || index < 1 || index > max) {
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
indexes.push(index);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return dedupe(indexes);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function dedupe(values) {
|
|
447
|
+
const seen = new Set();
|
|
448
|
+
const result = [];
|
|
449
|
+
for (const value of values) {
|
|
450
|
+
if (seen.has(value)) {
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
seen.add(value);
|
|
454
|
+
result.push(value);
|
|
455
|
+
}
|
|
456
|
+
return result;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async function promptRequiredText({ rl, label, defaultValue }) {
|
|
460
|
+
while (true) {
|
|
461
|
+
const suffix = defaultValue ? ` (${defaultValue})` : "";
|
|
462
|
+
const answer = (await rl.question(`${label}${suffix}: `)).trim();
|
|
463
|
+
const value = answer || defaultValue;
|
|
464
|
+
if (value && value.trim().length > 0) {
|
|
465
|
+
return value.trim();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const retry = await promptRetry({ rl, reason: "Value cannot be empty." });
|
|
469
|
+
if (!retry) {
|
|
470
|
+
throw new Error("Installation canceled.");
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function promptRetry({ rl, reason }) {
|
|
476
|
+
print(reason);
|
|
477
|
+
return promptYesNo({
|
|
478
|
+
rl,
|
|
479
|
+
question: "Try again?",
|
|
480
|
+
defaultYes: true,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async function promptYesNo({ rl, question, defaultYes }) {
|
|
485
|
+
while (true) {
|
|
486
|
+
const suffix = defaultYes ? " [Y/n]" : " [y/N]";
|
|
487
|
+
const answer = (await rl.question(`${question}${suffix}: `)).trim().toLowerCase();
|
|
488
|
+
if (!answer) {
|
|
489
|
+
return defaultYes;
|
|
490
|
+
}
|
|
491
|
+
if (answer === "y" || answer === "yes") {
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
if (answer === "n" || answer === "no") {
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
print('Please answer "y" or "n".');
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function parseAddArgs(args) {
|
|
502
|
+
let sourceArg = null;
|
|
503
|
+
let targetArg = "";
|
|
504
|
+
let mode = "";
|
|
505
|
+
let cursorProject = "";
|
|
506
|
+
let dest = "";
|
|
507
|
+
let hasExplicitTarget = false;
|
|
508
|
+
|
|
509
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
510
|
+
const token = args[i];
|
|
511
|
+
if (!token.startsWith("-") && !sourceArg) {
|
|
512
|
+
sourceArg = token;
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
if (token === "--target") {
|
|
516
|
+
hasExplicitTarget = true;
|
|
517
|
+
targetArg = readValue(args, i, token);
|
|
518
|
+
i += 1;
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
if (token === "--mode") {
|
|
522
|
+
mode = readValue(args, i, token);
|
|
523
|
+
i += 1;
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
if (token === "--cursor-project") {
|
|
527
|
+
cursorProject = readValue(args, i, token);
|
|
528
|
+
i += 1;
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
if (token === "--dest") {
|
|
532
|
+
dest = readValue(args, i, token);
|
|
533
|
+
i += 1;
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
fatal(`Unknown argument: ${token}`);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (mode && !["copy", "symlink"].includes(mode)) {
|
|
540
|
+
fatal(`Invalid --mode value: ${mode}. Use copy or symlink.`);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
return { sourceArg, targetArg, mode, cursorProject, dest, hasExplicitTarget };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function parseSelectArgs(args) {
|
|
547
|
+
let sourceArg = null;
|
|
548
|
+
let mode = "";
|
|
549
|
+
let cursorProject = "";
|
|
550
|
+
let dest = "";
|
|
551
|
+
|
|
552
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
553
|
+
const token = args[i];
|
|
554
|
+
if (!token.startsWith("-") && !sourceArg) {
|
|
555
|
+
sourceArg = token;
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
if (token === "--mode") {
|
|
559
|
+
mode = readValue(args, i, token);
|
|
560
|
+
i += 1;
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
if (token === "--cursor-project") {
|
|
564
|
+
cursorProject = readValue(args, i, token);
|
|
565
|
+
i += 1;
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
if (token === "--dest") {
|
|
569
|
+
dest = readValue(args, i, token);
|
|
570
|
+
i += 1;
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
fatal(`Unknown argument: ${token}`);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (mode && !["copy", "symlink"].includes(mode)) {
|
|
577
|
+
fatal(`Invalid --mode value: ${mode}. Use copy or symlink.`);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return { sourceArg, mode, cursorProject, dest };
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function readValue(args, index, flag) {
|
|
584
|
+
const value = args[index + 1];
|
|
585
|
+
if (!value || value.startsWith("-")) {
|
|
586
|
+
fatal(`Missing value for ${flag}`);
|
|
587
|
+
}
|
|
588
|
+
return value;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function resolveTargets(rawTarget) {
|
|
592
|
+
if (rawTarget === "all") {
|
|
593
|
+
return [...ALL_TARGETS];
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const names = rawTarget.split(",").map((item) => item.trim()).filter(Boolean);
|
|
597
|
+
if (names.length === 0) {
|
|
598
|
+
fatal(`Invalid --target value: ${rawTarget}`);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const targets = [];
|
|
602
|
+
for (const name of names) {
|
|
603
|
+
const mapped = TARGET_ALIASES.get(name);
|
|
604
|
+
if (!mapped) {
|
|
605
|
+
fatal(`Unknown target: ${name}. Run "skills targets" for valid values.`);
|
|
606
|
+
}
|
|
607
|
+
if (!targets.includes(mapped)) {
|
|
608
|
+
targets.push(mapped);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return targets;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
async function resolveSource(sourceArg, cleanupPaths) {
|
|
615
|
+
if (!sourceArg) {
|
|
616
|
+
return {
|
|
617
|
+
root: path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."),
|
|
618
|
+
isTemporary: false,
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (SOURCE_ALIASES.has(sourceArg)) {
|
|
623
|
+
const mappedSource = SOURCE_ALIASES.get(sourceArg);
|
|
624
|
+
print(`Using source alias "${sourceArg}" -> "${mappedSource}"`);
|
|
625
|
+
return cloneGithubSource(mappedSource, cleanupPaths);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const localPath = path.resolve(sourceArg);
|
|
629
|
+
if (await pathExists(localPath)) {
|
|
630
|
+
return { root: localPath, isTemporary: false };
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (isGithubSlug(sourceArg)) {
|
|
634
|
+
return cloneGithubSource(sourceArg, cleanupPaths);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
fatal(`Source not found: ${sourceArg}`);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
async function cloneGithubSource(slug, cleanupPaths) {
|
|
641
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "skills-source-"));
|
|
642
|
+
cleanupPaths.push(tempRoot);
|
|
643
|
+
|
|
644
|
+
const cloneUrl = `https://github.com/${slug}.git`;
|
|
645
|
+
print(`Cloning ${cloneUrl} ...`);
|
|
646
|
+
execFileSync("git", ["clone", "--depth", "1", cloneUrl, tempRoot], {
|
|
647
|
+
stdio: "inherit",
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
return { root: tempRoot, isTemporary: true };
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function isGithubSlug(value) {
|
|
654
|
+
return /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(value);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
async function findSkillsRoot(rootPath) {
|
|
658
|
+
const candidates = [path.join(rootPath, ".agents", "skills"), path.join(rootPath, "skills")];
|
|
659
|
+
|
|
660
|
+
for (const candidate of candidates) {
|
|
661
|
+
if (!(await pathExists(candidate))) {
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const entries = await fs.readdir(candidate, { withFileTypes: true });
|
|
666
|
+
for (const entry of entries) {
|
|
667
|
+
if (!entry.isDirectory()) {
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
const skillPath = path.join(candidate, entry.name, "SKILL.md");
|
|
671
|
+
if (await pathExists(skillPath)) {
|
|
672
|
+
return candidate;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
fatal(
|
|
678
|
+
`Could not find skills under ${rootPath}. Expected ".agents/skills" or "skills" with SKILL.md files.`,
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
async function readSkills(skillsRoot) {
|
|
683
|
+
const entries = await fs.readdir(skillsRoot, { withFileTypes: true });
|
|
684
|
+
const skills = [];
|
|
685
|
+
|
|
686
|
+
for (const entry of entries) {
|
|
687
|
+
if (!entry.isDirectory()) {
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const dir = path.join(skillsRoot, entry.name);
|
|
692
|
+
const skillFile = path.join(dir, "SKILL.md");
|
|
693
|
+
if (!(await pathExists(skillFile))) {
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const content = await fs.readFile(skillFile, "utf8");
|
|
698
|
+
skills.push({
|
|
699
|
+
name: entry.name,
|
|
700
|
+
dir,
|
|
701
|
+
skillFile,
|
|
702
|
+
content,
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
async function installCodex(skills, mode) {
|
|
710
|
+
const codexHome = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
|
|
711
|
+
const targetRoot = path.join(codexHome, "skills");
|
|
712
|
+
return installSkillDirectories(skills, targetRoot, mode, "Codex");
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
async function installClaudeCode(skills, mode) {
|
|
716
|
+
const claudeHome = process.env.CLAUDE_HOME || path.join(os.homedir(), ".claude");
|
|
717
|
+
const targetRoot = path.join(claudeHome, "skills");
|
|
718
|
+
return installSkillDirectories(skills, targetRoot, mode, "Claude Code");
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
async function installOpenCode(skills, mode) {
|
|
722
|
+
const opencodeHome =
|
|
723
|
+
process.env.OPENCODE_HOME ||
|
|
724
|
+
process.env.OPENCODE_CONFIG_DIR ||
|
|
725
|
+
path.join(os.homedir(), ".config", "opencode");
|
|
726
|
+
const targetRoot = path.join(opencodeHome, "skills");
|
|
727
|
+
return installSkillDirectories(skills, targetRoot, mode, "OpenCode");
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
async function installGeneric(skills, mode, destRaw) {
|
|
731
|
+
if (!destRaw) {
|
|
732
|
+
fatal(`target=generic requires --dest /path/to/skills`);
|
|
733
|
+
}
|
|
734
|
+
const targetRoot = path.resolve(destRaw);
|
|
735
|
+
return installSkillDirectories(skills, targetRoot, mode, "Generic");
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
async function installSkillDirectories(skills, targetRoot, mode, label) {
|
|
739
|
+
await fs.mkdir(targetRoot, { recursive: true });
|
|
740
|
+
let installed = 0;
|
|
741
|
+
|
|
742
|
+
for (const skill of skills) {
|
|
743
|
+
const targetPath = path.join(targetRoot, skill.name);
|
|
744
|
+
await replacePath(targetPath);
|
|
745
|
+
|
|
746
|
+
if (mode === "symlink") {
|
|
747
|
+
await fs.symlink(skill.dir, targetPath, "dir");
|
|
748
|
+
} else {
|
|
749
|
+
await fs.cp(skill.dir, targetPath, { recursive: true });
|
|
750
|
+
}
|
|
751
|
+
installed += 1;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
print(`Installed ${label} skills to ${targetRoot}`);
|
|
755
|
+
return installed;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
async function installGemini(skills, mode) {
|
|
759
|
+
const geminiHome = process.env.GEMINI_HOME || path.join(os.homedir(), ".gemini");
|
|
760
|
+
const targetRoot = path.join(geminiHome, "commands", "mp-skills");
|
|
761
|
+
await fs.mkdir(targetRoot, { recursive: true });
|
|
762
|
+
|
|
763
|
+
if (mode === "symlink") {
|
|
764
|
+
print("Gemini CLI uses generated TOML wrappers; using direct install for Gemini.");
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
let installed = 0;
|
|
768
|
+
for (const skill of skills) {
|
|
769
|
+
const targetPath = path.join(targetRoot, `${skill.name}.toml`);
|
|
770
|
+
const toml = buildGeminiToml(skill);
|
|
771
|
+
await replacePath(targetPath);
|
|
772
|
+
await fs.writeFile(targetPath, toml, "utf8");
|
|
773
|
+
installed += 1;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
print(`Installed Gemini CLI commands to ${targetRoot}`);
|
|
777
|
+
return installed;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function buildGeminiToml(skill) {
|
|
781
|
+
const prompt = escapeTomlMultiline(skill.content.trimEnd());
|
|
782
|
+
return `description = "mp-skills: ${skill.name}"\n\nprompt = """\n${prompt}\n"""\n`;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function escapeTomlMultiline(value) {
|
|
786
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
async function installCursor(skills, mode, cursorProjectRaw) {
|
|
790
|
+
const cursorProject = path.resolve(cursorProjectRaw);
|
|
791
|
+
const targetRoot = path.join(cursorProject, ".cursor", "rules", "mp-skills");
|
|
792
|
+
await fs.mkdir(targetRoot, { recursive: true });
|
|
793
|
+
|
|
794
|
+
if (mode === "symlink") {
|
|
795
|
+
print("Cursor uses generated .mdc rules; using direct install for Cursor.");
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
let installed = 0;
|
|
799
|
+
for (const skill of skills) {
|
|
800
|
+
const targetPath = path.join(targetRoot, `${skill.name}.mdc`);
|
|
801
|
+
const content = buildCursorRule(skill);
|
|
802
|
+
await replacePath(targetPath);
|
|
803
|
+
await fs.writeFile(targetPath, content, "utf8");
|
|
804
|
+
installed += 1;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
print(`Installed Cursor rules to ${targetRoot}`);
|
|
808
|
+
return installed;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function buildCursorRule(skill) {
|
|
812
|
+
return `---
|
|
813
|
+
description: "mp-skills: ${skill.name}"
|
|
814
|
+
alwaysApply: false
|
|
815
|
+
---
|
|
816
|
+
# ${skill.name}
|
|
817
|
+
|
|
818
|
+
Use this rule when you want to run the \`${skill.name}\` workflow from the shared mp-skills library.
|
|
819
|
+
|
|
820
|
+
${skill.content.trimEnd()}
|
|
821
|
+
`;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
async function replacePath(targetPath) {
|
|
825
|
+
await fs.rm(targetPath, { recursive: true, force: true });
|
|
826
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
async function pathExists(candidatePath) {
|
|
830
|
+
try {
|
|
831
|
+
await fs.access(candidatePath, fsConstants.F_OK);
|
|
832
|
+
return true;
|
|
833
|
+
} catch {
|
|
834
|
+
return false;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function print(message) {
|
|
839
|
+
process.stdout.write(`${message}\n`);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function fatal(message) {
|
|
843
|
+
process.stderr.write(`${message}\n`);
|
|
844
|
+
process.exit(1);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
main().catch((error) => {
|
|
848
|
+
fatal(error instanceof Error ? error.message : String(error));
|
|
849
|
+
});
|