skill-rules 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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +438 -0
  3. package/dist/index.js +1125 -0
  4. package/package.json +79 -0
package/dist/index.js ADDED
@@ -0,0 +1,1125 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.jsx
4
+ import { Command } from "commander";
5
+ import { readFileSync as readFileSync5 } from "fs";
6
+ import { fileURLToPath } from "url";
7
+ import { join as join11, dirname } from "path";
8
+
9
+ // src/commands/run.jsx
10
+ import React2, { useEffect, useState as useState2 } from "react";
11
+ import { render, Box as Box2, Text as Text2, useApp } from "ink";
12
+ import { Spinner } from "@inkjs/ui";
13
+
14
+ // src/lib/ides.js
15
+ import { existsSync } from "fs";
16
+ import { join } from "path";
17
+ var IDES = {
18
+ claude: {
19
+ name: "Claude Code",
20
+ detectDir: ".claude",
21
+ skillsDir: ".claude/skills"
22
+ },
23
+ cursor: {
24
+ name: "Cursor",
25
+ detectDir: ".cursor",
26
+ skillsDir: ".cursor/skills"
27
+ },
28
+ windsurf: {
29
+ name: "Windsurf",
30
+ detectDir: ".windsurf",
31
+ skillsDir: ".windsurf/skills"
32
+ },
33
+ openhands: {
34
+ name: "OpenHands",
35
+ detectDir: ".openhands",
36
+ skillsDir: ".openhands/skills"
37
+ },
38
+ agents: {
39
+ // Shared by: GitHub Copilot, Cline, VS Code, OpenCode, Codex, Kiro, and others
40
+ name: "Agents (Copilot, Cline, VS Code, OpenCode, Codex, Kiro)",
41
+ detectDir: ".agents",
42
+ skillsDir: ".agents/skills"
43
+ }
44
+ };
45
+ function detectIDEs(cwd = process.cwd()) {
46
+ return Object.entries(IDES).filter(([, ide]) => existsSync(join(cwd, ide.detectDir))).map(([id, ide]) => ({ id, ...ide }));
47
+ }
48
+
49
+ // src/lib/lock.js
50
+ import { readFileSync, writeFileSync, existsSync as existsSync2 } from "fs";
51
+ import { join as join2 } from "path";
52
+ var LOCK_FILE = "skills-lock.json";
53
+ function readLock(cwd = process.cwd()) {
54
+ const lockPath = join2(cwd, LOCK_FILE);
55
+ if (!existsSync2(lockPath)) {
56
+ return { version: 1, skills: {} };
57
+ }
58
+ try {
59
+ return JSON.parse(readFileSync(lockPath, "utf8"));
60
+ } catch {
61
+ throw new Error(`Failed to parse ${LOCK_FILE}. Delete it and run: skill-rules init`);
62
+ }
63
+ }
64
+ function writeLock(lock, cwd = process.cwd()) {
65
+ const lockPath = join2(cwd, LOCK_FILE);
66
+ writeFileSync(lockPath, JSON.stringify(lock, null, 2) + "\n");
67
+ }
68
+ function setSkillTracked(lock, skillName, tracked) {
69
+ if (!lock.skills[skillName]) return lock;
70
+ if (tracked) {
71
+ lock.skills[skillName].track = true;
72
+ } else {
73
+ delete lock.skills[skillName].track;
74
+ }
75
+ return lock;
76
+ }
77
+
78
+ // src/lib/rules.js
79
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync3 } from "fs";
80
+ import { join as join3 } from "path";
81
+ var RULES_FILE = "skills.rules";
82
+ function readRules(cwd = process.cwd()) {
83
+ const rulesPath = join3(cwd, RULES_FILE);
84
+ if (!existsSync3(rulesPath)) return null;
85
+ try {
86
+ return JSON.parse(readFileSync2(rulesPath, "utf8"));
87
+ } catch {
88
+ throw new Error(`Failed to parse ${RULES_FILE}. Delete it and run: skill-rules init`);
89
+ }
90
+ }
91
+ function writeRules(rules, cwd = process.cwd()) {
92
+ const rulesPath = join3(cwd, RULES_FILE);
93
+ writeFileSync2(rulesPath, JSON.stringify(rules, null, 2) + "\n");
94
+ }
95
+ function createEmptyRules() {
96
+ return { version: 1, stages: {} };
97
+ }
98
+ function getActiveSkills(rules, stage) {
99
+ if (!rules) return [];
100
+ const { stages } = rules;
101
+ if (stage) {
102
+ return stages[stage] ?? [];
103
+ }
104
+ const seen = /* @__PURE__ */ new Set();
105
+ for (const skills of Object.values(stages)) {
106
+ for (const s of skills) seen.add(s);
107
+ }
108
+ return [...seen];
109
+ }
110
+ function addSkillToStage(rules, skillName, stage) {
111
+ if (!rules.stages[stage]) rules.stages[stage] = [];
112
+ if (!rules.stages[stage].includes(skillName)) {
113
+ rules.stages[stage].push(skillName);
114
+ }
115
+ return rules;
116
+ }
117
+ function removeSkillFromStage(rules, skillName, stage) {
118
+ if (stage) {
119
+ rules.stages[stage] = (rules.stages[stage] ?? []).filter((s) => s !== skillName);
120
+ if (rules.stages[stage].length === 0) delete rules.stages[stage];
121
+ } else {
122
+ for (const key of Object.keys(rules.stages)) {
123
+ rules.stages[key] = rules.stages[key].filter((s) => s !== skillName);
124
+ if (rules.stages[key].length === 0) delete rules.stages[key];
125
+ }
126
+ }
127
+ return rules;
128
+ }
129
+ function listStages(rules) {
130
+ return Object.keys(rules?.stages ?? {});
131
+ }
132
+
133
+ // src/lib/ignorer.js
134
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync4 } from "fs";
135
+ import { join as join4 } from "path";
136
+ var MARKER_START = "# skill-rules [start]";
137
+ var MARKER_END = "# skill-rules [end]";
138
+ function updateGitignore(cwd, patterns) {
139
+ const gitignorePath = join4(cwd, ".gitignore");
140
+ let content = existsSync4(gitignorePath) ? readFileSync3(gitignorePath, "utf8") : "";
141
+ const escapedStart = escapeRegex(MARKER_START);
142
+ const escapedEnd = escapeRegex(MARKER_END);
143
+ content = content.replace(new RegExp(`\\n?${escapedStart}[\\s\\S]*?${escapedEnd}\\n?`, "g"), "");
144
+ if (patterns.length === 0) {
145
+ writeFileSync3(gitignorePath, content);
146
+ return;
147
+ }
148
+ const normalized = patterns.map((p) => p.replace(/\\/g, "/"));
149
+ const section = `
150
+ ${MARKER_START}
151
+ ${normalized.join("\n")}
152
+ ${MARKER_END}
153
+ `;
154
+ writeFileSync3(gitignorePath, content.trimEnd() + section);
155
+ }
156
+ function buildIgnorePatterns(ides, trackedSkillNames = []) {
157
+ const patterns = [".skill-rules/"];
158
+ for (const ide of ides) {
159
+ patterns.push(ide.skillsDir);
160
+ for (const skill of trackedSkillNames) {
161
+ patterns.push(`!${ide.skillsDir}/${skill}`);
162
+ }
163
+ }
164
+ return patterns;
165
+ }
166
+ function syncGitignore(cwd, ides, lock) {
167
+ const tracked = Object.entries(lock?.skills ?? {}).filter(([, info]) => info.track).map(([name]) => name);
168
+ updateGitignore(cwd, buildIgnorePatterns(ides, tracked));
169
+ }
170
+ function escapeRegex(str) {
171
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
172
+ }
173
+
174
+ // src/lib/syncer.js
175
+ import { existsSync as existsSync5, statSync as statSync2 } from "fs";
176
+ import { join as join6 } from "path";
177
+
178
+ // src/lib/copy.js
179
+ import { readdirSync, statSync, copyFileSync, mkdirSync } from "fs";
180
+ import { join as join5 } from "path";
181
+ function copyDirSync(src, dest) {
182
+ mkdirSync(dest, { recursive: true });
183
+ for (const entry of readdirSync(src)) {
184
+ const srcPath = join5(src, entry);
185
+ const destPath = join5(dest, entry);
186
+ if (statSync(srcPath).isDirectory()) {
187
+ copyDirSync(srcPath, destPath);
188
+ } else {
189
+ copyFileSync(srcPath, destPath);
190
+ }
191
+ }
192
+ }
193
+
194
+ // src/lib/syncer.js
195
+ function findSkillSources(cwd, ides, skillName) {
196
+ return ides.filter((ide) => {
197
+ const p = join6(cwd, ide.skillsDir, skillName);
198
+ return existsSync5(p) && statSync2(p).isDirectory();
199
+ });
200
+ }
201
+ function syncSkillToMissingIDEs(cwd, ides, skillName, sourceIde) {
202
+ const sourcePath = join6(cwd, sourceIde.skillsDir, skillName);
203
+ let copied = 0;
204
+ for (const ide of ides) {
205
+ const targetPath = join6(cwd, ide.skillsDir, skillName);
206
+ if (!existsSync5(targetPath) || !statSync2(targetPath).isDirectory()) {
207
+ try {
208
+ copyDirSync(sourcePath, targetPath);
209
+ copied++;
210
+ } catch (err) {
211
+ throw new Error(`Could not sync "${skillName}" to ${ide.name}: ${err.message}`, {
212
+ cause: err
213
+ });
214
+ }
215
+ }
216
+ }
217
+ return copied;
218
+ }
219
+
220
+ // src/lib/state.js
221
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync6, mkdirSync as mkdirSync2 } from "fs";
222
+ import { join as join7 } from "path";
223
+ var STATE_DIR = ".skill-rules";
224
+ var STATE_FILE = "state.json";
225
+ function statePath(cwd) {
226
+ return join7(cwd, STATE_DIR, STATE_FILE);
227
+ }
228
+ function readState(cwd) {
229
+ const p = statePath(cwd);
230
+ if (!existsSync6(p)) return {};
231
+ try {
232
+ return JSON.parse(readFileSync4(p, "utf8"));
233
+ } catch {
234
+ return {};
235
+ }
236
+ }
237
+ function writeState(state, cwd) {
238
+ mkdirSync2(join7(cwd, STATE_DIR), { recursive: true });
239
+ writeFileSync4(statePath(cwd), JSON.stringify(state, null, 2) + "\n");
240
+ }
241
+ function getActiveStage(cwd = process.cwd()) {
242
+ return readState(cwd).activeStage ?? null;
243
+ }
244
+ function setActiveStage(stage, cwd = process.cwd()) {
245
+ const state = readState(cwd);
246
+ state.activeStage = stage;
247
+ writeState(state, cwd);
248
+ }
249
+ function clearActiveStage(cwd = process.cwd()) {
250
+ const state = readState(cwd);
251
+ delete state.activeStage;
252
+ writeState(state, cwd);
253
+ }
254
+
255
+ // src/ui.jsx
256
+ import React, { useState } from "react";
257
+ import { Box, Text, useInput } from "ink";
258
+ import figures from "figures";
259
+ var STATUS_ICON = {
260
+ ok: figures.tick,
261
+ success: figures.tick,
262
+ missing: figures.cross,
263
+ error: figures.cross,
264
+ synced: figures.bullet,
265
+ warning: figures.warning,
266
+ info: figures.info
267
+ };
268
+ var STATUS_COLOR = {
269
+ ok: "green",
270
+ success: "green",
271
+ missing: "red",
272
+ error: "red",
273
+ synced: "yellow",
274
+ warning: "yellow",
275
+ info: "cyan"
276
+ };
277
+ function Header({ children }) {
278
+ return /* @__PURE__ */ React.createElement(Box, { marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true }, children));
279
+ }
280
+ function IDEItem({ ide }) {
281
+ return /* @__PURE__ */ React.createElement(Box, { gap: 2 }, /* @__PURE__ */ React.createElement(Text, { color: "green" }, figures.tick), /* @__PURE__ */ React.createElement(Text, null, ide.name), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, ide.skillsDir));
282
+ }
283
+ function SkillStatus({ name, status, detail }) {
284
+ return /* @__PURE__ */ React.createElement(Box, { gap: 1 }, /* @__PURE__ */ React.createElement(Text, { color: STATUS_COLOR[status] ?? "white" }, STATUS_ICON[status] ?? figures.bullet), /* @__PURE__ */ React.createElement(Text, { bold: true }, name), detail && /* @__PURE__ */ React.createElement(Text, { dimColor: true }, detail));
285
+ }
286
+ function StatusLine({ variant, children }) {
287
+ return /* @__PURE__ */ React.createElement(Box, { gap: 1 }, /* @__PURE__ */ React.createElement(Text, { color: STATUS_COLOR[variant] ?? "white" }, STATUS_ICON[variant] ?? figures.bullet), /* @__PURE__ */ React.createElement(Text, null, children));
288
+ }
289
+ function Hint({ children }) {
290
+ return /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " ", children);
291
+ }
292
+ function ListSelect({ options, onSelect }) {
293
+ const [index, setIndex] = useState(0);
294
+ useInput((_, key) => {
295
+ if (key.upArrow) setIndex((i) => Math.max(0, i - 1));
296
+ else if (key.downArrow) setIndex((i) => Math.min(options.length - 1, i + 1));
297
+ else if (key.return) onSelect(options[index].value);
298
+ });
299
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, options.map((opt, i) => /* @__PURE__ */ React.createElement(Box, { key: opt.value, gap: 1 }, /* @__PURE__ */ React.createElement(Text, { color: i === index ? "cyan" : void 0 }, i === index ? figures.pointerSmall : " "), /* @__PURE__ */ React.createElement(Text, { color: i === index ? "cyan" : void 0 }, opt.label))));
300
+ }
301
+
302
+ // src/commands/run.jsx
303
+ async function run(options = {}) {
304
+ const cwd = process.cwd();
305
+ const explicitStage = options.stage ?? null;
306
+ const activeStage = getActiveStage(cwd);
307
+ const stage = explicitStage ?? activeStage;
308
+ const { waitUntilExit } = render(
309
+ /* @__PURE__ */ React2.createElement(RunUI, { stage, stageFromState: !explicitStage && !!activeStage })
310
+ );
311
+ await waitUntilExit();
312
+ }
313
+ function RunUI({ stage, stageFromState }) {
314
+ const { exit } = useApp();
315
+ const [phase, setPhase] = useState2("init");
316
+ const [state, setState] = useState2(null);
317
+ useEffect(() => {
318
+ try {
319
+ const cwd = process.cwd();
320
+ const ides2 = detectIDEs(cwd);
321
+ const lock = readLock(cwd);
322
+ const rules = readRules(cwd);
323
+ if (ides2.length === 0) {
324
+ setState({
325
+ error: "No IDE directories detected.\nExpected: .claude/ .cursor/ .windsurf/ .agents/ .openhands/"
326
+ });
327
+ setPhase("error");
328
+ return;
329
+ }
330
+ if (stage && rules && !(stage in (rules.stages ?? {}))) {
331
+ const available = listStages(rules);
332
+ const hint = stageFromState ? "\nActive stage is stale \u2014 run: skill-rules use --off" : "";
333
+ setState({
334
+ error: `Stage "${stage}" not found.${available.length ? `
335
+ Available: ${available.join(", ")}` : "\nNo stages defined yet."}${hint}`
336
+ });
337
+ setPhase("error");
338
+ return;
339
+ }
340
+ syncGitignore(cwd, ides2, lock);
341
+ const activeSkills = getActiveSkills(rules, stage);
342
+ const lockSkills = Object.keys(lock.skills);
343
+ const skillsToCheck = stage ? activeSkills : lockSkills;
344
+ if (skillsToCheck.length === 0) {
345
+ setState({ ides: ides2, results: [], stage });
346
+ setPhase("done");
347
+ return;
348
+ }
349
+ const results2 = [];
350
+ for (const skillName of skillsToCheck) {
351
+ const sources = findSkillSources(cwd, ides2, skillName);
352
+ if (sources.length === 0) {
353
+ results2.push({ name: skillName, status: "missing" });
354
+ } else if (sources.length < ides2.length) {
355
+ const copied = syncSkillToMissingIDEs(cwd, ides2, skillName, sources[0]);
356
+ results2.push({
357
+ name: skillName,
358
+ status: "synced",
359
+ detail: `synced to ${copied} ${copied === 1 ? "IDE" : "IDEs"} from ${sources[0].name}`
360
+ });
361
+ } else {
362
+ results2.push({ name: skillName, status: "ok" });
363
+ }
364
+ }
365
+ setState({ ides: ides2, results: results2, stage });
366
+ setPhase("done");
367
+ } catch (err) {
368
+ setState({ error: err.message });
369
+ setPhase("error");
370
+ }
371
+ }, []);
372
+ useEffect(() => {
373
+ if (phase === "done" || phase === "error") exit();
374
+ }, [phase]);
375
+ if (phase === "init") return /* @__PURE__ */ React2.createElement(Spinner, { label: "Scanning IDEs and skills\u2026" });
376
+ if (phase === "error")
377
+ return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: "red" }, state.error));
378
+ const { ides, results, stage: activeStage } = state;
379
+ if (!results || results.length === 0) {
380
+ const msg = activeStage ? `Stage [${activeStage}] has no skills. Run: skill-rules add --stage ${activeStage}` : "No skills yet. Run: skill-rules add";
381
+ return /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, msg);
382
+ }
383
+ const ok = results.filter((r) => r.status === "ok");
384
+ const synced = results.filter((r) => r.status === "synced");
385
+ const missing = results.filter((r) => r.status === "missing");
386
+ return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React2.createElement(Header, null, "skill-rules", activeStage ? ` [${activeStage}]` : ""), /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column" }, /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "IDEs (", ides.length, ")"), ides.map((ide) => /* @__PURE__ */ React2.createElement(IDEItem, { key: ide.id, ide }))), /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column" }, /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "Skills (", results.length, ")"), results.map((r) => /* @__PURE__ */ React2.createElement(SkillStatus, { key: r.name, name: r.name, status: r.status, detail: r.detail }))), /* @__PURE__ */ React2.createElement(Box2, { gap: 3 }, ok.length > 0 && /* @__PURE__ */ React2.createElement(Text2, { color: "green" }, ok.length, " up to date"), synced.length > 0 && /* @__PURE__ */ React2.createElement(Text2, { color: "yellow" }, synced.length, " synced"), missing.length > 0 && /* @__PURE__ */ React2.createElement(Text2, { color: "red" }, missing.length, " missing \u2014 install via ", /* @__PURE__ */ React2.createElement(Text2, { bold: true }, "skills.sh"), " or", " ", /* @__PURE__ */ React2.createElement(Text2, { bold: true }, "autoskill"))));
387
+ }
388
+
389
+ // src/commands/init.jsx
390
+ import React3 from "react";
391
+ import { render as render2, Box as Box3, Text as Text3 } from "ink";
392
+ import { existsSync as existsSync7 } from "fs";
393
+ import { join as join8 } from "path";
394
+ async function init() {
395
+ const cwd = process.cwd();
396
+ const ides = detectIDEs(cwd);
397
+ if (ides.length === 0) {
398
+ const { unmount: unmount2 } = render2(
399
+ /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React3.createElement(StatusLine, { variant: "warning" }, "No IDE directories detected."), /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "Create one of: .claude/ .cursor/ .windsurf/ .agents/ .openhands/"))
400
+ );
401
+ unmount2();
402
+ return;
403
+ }
404
+ const lockPath = join8(cwd, "skills-lock.json");
405
+ const lockExists = existsSync7(lockPath);
406
+ const rulesPath = join8(cwd, "skills.rules");
407
+ const rulesExists = existsSync7(rulesPath);
408
+ if (!lockExists) writeLock({ version: 1, skills: {} }, cwd);
409
+ if (!rulesExists) writeRules(createEmptyRules(), cwd);
410
+ const lock = readLock(cwd);
411
+ const rules = readRules(cwd);
412
+ syncGitignore(cwd, ides, lock);
413
+ const skillCount = Object.keys(lock.skills).length;
414
+ const stages = listStages(rules);
415
+ const { unmount } = render2(
416
+ /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React3.createElement(Header, null, "skill-rules init"), /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "IDEs detected (", ides.length, ")"), ides.map((ide) => /* @__PURE__ */ React3.createElement(IDEItem, { key: ide.id, ide }))), /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(StatusLine, { variant: lockExists ? "info" : "success" }, "skills-lock.json", lockExists ? ` \u2014 already exists (${skillCount} skill${skillCount !== 1 ? "s" : ""})` : " \u2014 created"), /* @__PURE__ */ React3.createElement(StatusLine, { variant: rulesExists ? "info" : "success" }, "skills.rules", rulesExists ? ` \u2014 already exists (stages: ${stages.join(", ") || "none"})` : " \u2014 created (empty)"), /* @__PURE__ */ React3.createElement(StatusLine, { variant: "success" }, ".gitignore \u2014 updated")), /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Hint, null, "1. Install skills: skills.sh or autoskill"), /* @__PURE__ */ React3.createElement(Hint, null, "2. Assign stages: skill-rules add"), /* @__PURE__ */ React3.createElement(Hint, null, "3. Sync IDEs: npx skill-rules")))
417
+ );
418
+ unmount();
419
+ }
420
+
421
+ // src/commands/add.jsx
422
+ import React4, { useState as useState3, useEffect as useEffect2 } from "react";
423
+ import { render as render3, Box as Box4, Text as Text4, useApp as useApp2 } from "ink";
424
+ import { MultiSelect, TextInput } from "@inkjs/ui";
425
+ var NEW_STAGE = "__new__";
426
+ var STAGE_RE = /^[a-z0-9_-]+$/;
427
+ function normalizeSkillName(skill) {
428
+ const base = skill.startsWith("@") ? skill.split("/")[1] : skill;
429
+ return base.replace(/^skill-/, "");
430
+ }
431
+ async function add(skill, options = {}) {
432
+ const cwd = process.cwd();
433
+ const stage = options.stage ?? null;
434
+ const track = options.track ?? false;
435
+ if (stage) {
436
+ if (!skill) {
437
+ const { unmount } = render3(
438
+ /* @__PURE__ */ React4.createElement(Text4, { color: "red" }, "Usage: skill-rules add ", "<skill>", " --stage ", "<name>")
439
+ );
440
+ unmount();
441
+ return;
442
+ }
443
+ runNonInteractive(cwd, normalizeSkillName(skill), stage, track);
444
+ } else {
445
+ const preselected = skill ? normalizeSkillName(skill) : null;
446
+ const { waitUntilExit } = render3(/* @__PURE__ */ React4.createElement(AddWizard, { cwd, preselected }));
447
+ await waitUntilExit();
448
+ }
449
+ }
450
+ function runNonInteractive(cwd, skillName, stage, track) {
451
+ const lock = readLock(cwd);
452
+ const ides = detectIDEs(cwd);
453
+ const inLock = !!lock.skills[skillName];
454
+ const rules = readRules(cwd) ?? createEmptyRules();
455
+ const existingStages = listStages(rules);
456
+ const isNewStage = !existingStages.includes(stage);
457
+ const alreadyInStage = (rules.stages[stage] ?? []).includes(skillName);
458
+ if (!alreadyInStage) {
459
+ addSkillToStage(rules, skillName, stage);
460
+ writeRules(rules, cwd);
461
+ }
462
+ if (track && inLock) {
463
+ setSkillTracked(lock, skillName, true);
464
+ writeLock(lock, cwd);
465
+ }
466
+ if (ides.length > 0) syncGitignore(cwd, ides, lock);
467
+ const { unmount } = render3(
468
+ /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React4.createElement(Header, null, "skill-rules add ", skillName), /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column" }, !inLock && /* @__PURE__ */ React4.createElement(StatusLine, { variant: "warning" }, skillName, " not in skills-lock.json \u2014 install via skills.sh or autoskill first"), isNewStage && !alreadyInStage && /* @__PURE__ */ React4.createElement(StatusLine, { variant: "info" }, "new stage [", stage, "] created"), /* @__PURE__ */ React4.createElement(StatusLine, { variant: alreadyInStage ? "info" : "success" }, "skills.rules [", stage, "] \u2014 ", alreadyInStage ? "already present" : `added ${skillName}`), track && /* @__PURE__ */ React4.createElement(StatusLine, { variant: inLock ? "success" : "warning" }, "git tracking \u2014 ", inLock ? "now tracked" : "skipped (not in lock)"), ides.length > 0 && /* @__PURE__ */ React4.createElement(StatusLine, { variant: "success" }, ".gitignore \u2014 updated")))
469
+ );
470
+ unmount();
471
+ }
472
+ function AddWizard({ cwd, preselected }) {
473
+ const { exit } = useApp2();
474
+ const [step, setStep] = useState3("select-skills");
475
+ const [selectedSkills, setSelectedSkills] = useState3([]);
476
+ const [stageName, setStageName] = useState3("");
477
+ const [stageError, setStageError] = useState3("");
478
+ const [summary, setSummary] = useState3(null);
479
+ const lock = readLock(cwd);
480
+ const ides = detectIDEs(cwd);
481
+ const skillNames = Object.keys(lock.skills);
482
+ const rules = readRules(cwd) ?? createEmptyRules();
483
+ const stages = listStages(rules);
484
+ useEffect2(() => {
485
+ if (step === "done") exit();
486
+ }, [step]);
487
+ if (skillNames.length === 0) {
488
+ setTimeout(exit, 0);
489
+ return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React4.createElement(StatusLine, { variant: "warning" }, "No skills installed yet."), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "Install via skills.sh or autoskill, then run skill-rules add."));
490
+ }
491
+ const preselectedMissing = preselected && !skillNames.includes(preselected);
492
+ if (step === "select-skills") {
493
+ const options = skillNames.map((name) => {
494
+ const skillStages = stages.filter((s) => (rules?.stages[s] ?? []).includes(name));
495
+ return {
496
+ label: `${name.padEnd(20)} ${skillStages.length ? skillStages.join(", ") : "\u2014"}`,
497
+ value: name
498
+ };
499
+ });
500
+ return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React4.createElement(Header, null, "Add skills to stage"), preselectedMissing && /* @__PURE__ */ React4.createElement(StatusLine, { variant: "warning" }, '"', preselected, '" not in skills-lock.json \u2014 install it first'), /* @__PURE__ */ React4.createElement(Box4, { gap: 4 }, /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "SKILL".padEnd(20)), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "CURRENT STAGES")), /* @__PURE__ */ React4.createElement(
501
+ MultiSelect,
502
+ {
503
+ options,
504
+ defaultValue: preselected && skillNames.includes(preselected) ? [preselected] : [],
505
+ onSubmit: (values) => {
506
+ if (values.length === 0) {
507
+ exit();
508
+ return;
509
+ }
510
+ setSelectedSkills(values);
511
+ setStep("select-stage");
512
+ }
513
+ }
514
+ ), /* @__PURE__ */ React4.createElement(Hint, null, "Space toggle \xB7 Enter confirm \xB7 0 selected = cancel"));
515
+ }
516
+ if (step === "select-stage") {
517
+ const stageOptions = [
518
+ ...stages.map((s) => ({ label: s, value: s })),
519
+ { label: "+ Create new stage\u2026", value: NEW_STAGE }
520
+ ];
521
+ return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React4.createElement(Header, null, "Select stage"), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "Skills: ", /* @__PURE__ */ React4.createElement(Text4, { color: "cyan" }, selectedSkills.join(", "))), /* @__PURE__ */ React4.createElement(
522
+ ListSelect,
523
+ {
524
+ options: stageOptions,
525
+ onSelect: (value) => {
526
+ if (value === NEW_STAGE) {
527
+ setStep("create-stage");
528
+ } else {
529
+ setStageName(value);
530
+ setStep("confirm-track");
531
+ }
532
+ }
533
+ }
534
+ ), /* @__PURE__ */ React4.createElement(Hint, null, "\u2191\u2193 navigate \xB7 Enter confirm"));
535
+ }
536
+ if (step === "create-stage") {
537
+ return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React4.createElement(Header, null, "New stage name"), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "Allowed: letters, numbers, hyphens, underscores"), /* @__PURE__ */ React4.createElement(
538
+ TextInput,
539
+ {
540
+ placeholder: "e.g. dev, qa, staging\u2026",
541
+ onSubmit: (value) => {
542
+ const trimmed = value.trim().toLowerCase();
543
+ if (!trimmed) return;
544
+ if (!STAGE_RE.test(trimmed)) {
545
+ setStageError(`"${trimmed}" is invalid \u2014 use only letters, numbers, - and _`);
546
+ return;
547
+ }
548
+ setStageError("");
549
+ setStageName(trimmed);
550
+ setStep("confirm-track");
551
+ }
552
+ }
553
+ ), stageError && /* @__PURE__ */ React4.createElement(Text4, { color: "red" }, stageError));
554
+ }
555
+ if (step === "confirm-track") {
556
+ return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React4.createElement(Header, null, "Git tracking"), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "Commit skills to git? By default each dev installs their own copy."), /* @__PURE__ */ React4.createElement(
557
+ ListSelect,
558
+ {
559
+ options: [
560
+ { label: "No \u2014 keep gitignored (recommended)", value: "no" },
561
+ { label: "Yes \u2014 commit to repo", value: "yes" }
562
+ ],
563
+ onSelect: (value) => {
564
+ const track = value === "yes";
565
+ applyChanges(cwd, selectedSkills, stageName, track, lock, rules, ides);
566
+ setSummary({ skills: selectedSkills, stage: stageName, track });
567
+ setStep("done");
568
+ }
569
+ }
570
+ ), /* @__PURE__ */ React4.createElement(Hint, null, "\u2191\u2193 navigate \xB7 Enter confirm"));
571
+ }
572
+ if (step === "done" && summary) {
573
+ return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React4.createElement(Header, null, "Done"), summary.skills.map((name) => /* @__PURE__ */ React4.createElement(StatusLine, { key: name, variant: "success" }, name, " \u2192 [", summary.stage, "]", summary.track ? " \xB7 tracked in git" : "")), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "Run npx skill-rules to sync across IDEs."));
574
+ }
575
+ return null;
576
+ }
577
+ function applyChanges(cwd, skillNames, stage, track, lock, rules, ides) {
578
+ for (const name of skillNames) {
579
+ addSkillToStage(rules, name, stage);
580
+ if (track && lock.skills[name] !== void 0) {
581
+ setSkillTracked(lock, name, true);
582
+ }
583
+ }
584
+ writeRules(rules, cwd);
585
+ if (track) writeLock(lock, cwd);
586
+ if (ides.length > 0) syncGitignore(cwd, ides, lock);
587
+ }
588
+
589
+ // src/commands/list.jsx
590
+ import React5, { useState as useState4, useEffect as useEffect3 } from "react";
591
+ import { render as render4, Box as Box5, Text as Text5, useApp as useApp3 } from "ink";
592
+ import { MultiSelect as MultiSelect2 } from "@inkjs/ui";
593
+ async function list(options = {}) {
594
+ const { waitUntilExit } = render4(/* @__PURE__ */ React5.createElement(ListUI, { track: options.track, untrack: options.untrack }));
595
+ await waitUntilExit();
596
+ }
597
+ function ListUI({ track, untrack }) {
598
+ const { exit } = useApp3();
599
+ const cwd = process.cwd();
600
+ const lock = readLock(cwd);
601
+ const rules = readRules(cwd);
602
+ const ides = detectIDEs(cwd);
603
+ const skills = Object.entries(lock.skills);
604
+ const stages = listStages(rules);
605
+ if (track || untrack) {
606
+ const skillName = track ?? untrack;
607
+ const exists = !!lock.skills[skillName];
608
+ if (!exists) {
609
+ setTimeout(exit, 0);
610
+ return /* @__PURE__ */ React5.createElement(Text5, { color: "red" }, '"', skillName, '" not found in skills-lock.json');
611
+ }
612
+ setSkillTracked(lock, skillName, !!track);
613
+ writeLock(lock, cwd);
614
+ syncGitignore(cwd, ides, lock);
615
+ setTimeout(exit, 0);
616
+ return /* @__PURE__ */ React5.createElement(StatusLine, { variant: track ? "success" : "warning" }, track ? "Tracking" : "Ignoring", " ", skillName, track ? " \u2014 will be committed to git" : " \u2014 excluded from git");
617
+ }
618
+ if (skills.length === 0) {
619
+ setTimeout(exit, 0);
620
+ return /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "No skills yet. Run: skill-rules add");
621
+ }
622
+ return /* @__PURE__ */ React5.createElement(
623
+ InteractiveList,
624
+ {
625
+ skills,
626
+ stages,
627
+ rules,
628
+ ides,
629
+ lock,
630
+ cwd
631
+ }
632
+ );
633
+ }
634
+ function InteractiveList({ skills, stages, rules, ides, lock, cwd }) {
635
+ const { exit } = useApp3();
636
+ const [done, setDone] = useState4(false);
637
+ const [updated, setUpdated] = useState4([]);
638
+ const initialTracked = skills.filter(([, i]) => i.track).map(([n]) => n);
639
+ const options = skills.map(([name, info]) => {
640
+ const sources = findSkillSources(cwd, ides, name);
641
+ const skillStages = stages.filter((s) => (rules?.stages[s] ?? []).includes(name));
642
+ const installedLabel = sources.length === 0 ? "not installed" : sources.length === ides.length ? "all IDEs" : sources.map((s) => s.name).join(", ");
643
+ const stageLabel = skillStages.length > 0 ? skillStages.join(", ") : "\u2014";
644
+ return {
645
+ label: `${name.padEnd(18)} ${stageLabel.padEnd(14)} ${installedLabel}`,
646
+ value: name
647
+ };
648
+ });
649
+ const handleSubmit = (selected) => {
650
+ const changed = [];
651
+ for (const [name] of skills) {
652
+ const wasTracked = !!lock.skills[name].track;
653
+ const isNowTracked = selected.includes(name);
654
+ if (wasTracked !== isNowTracked) {
655
+ setSkillTracked(lock, name, isNowTracked);
656
+ changed.push({ name, tracked: isNowTracked });
657
+ }
658
+ }
659
+ writeLock(lock, cwd);
660
+ syncGitignore(cwd, ides, lock);
661
+ setUpdated(changed);
662
+ setDone(true);
663
+ };
664
+ useEffect3(() => {
665
+ if (done) exit();
666
+ }, [done]);
667
+ if (done) {
668
+ if (updated.length === 0) return /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "No changes.");
669
+ return /* @__PURE__ */ React5.createElement(Box5, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React5.createElement(Header, null, "Updated git tracking"), updated.map(({ name, tracked }) => /* @__PURE__ */ React5.createElement(StatusLine, { key: name, variant: tracked ? "success" : "warning" }, name, " \u2014 ", tracked ? "now tracked (committed to git)" : "now gitignored")));
670
+ }
671
+ return /* @__PURE__ */ React5.createElement(Box5, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React5.createElement(Header, null, "Skills"), /* @__PURE__ */ React5.createElement(Box5, { gap: 4 }, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "SKILL".padEnd(18)), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "STAGES".padEnd(14)), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "INSTALLED")), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "Select skills to ", /* @__PURE__ */ React5.createElement(Text5, { color: "green" }, "track in git"), " (checked = committed, unchecked = gitignored)"), /* @__PURE__ */ React5.createElement(MultiSelect2, { options, defaultValue: initialTracked, onSubmit: handleSubmit }), /* @__PURE__ */ React5.createElement(Hint, null, "Space to toggle \xB7 Enter to confirm \xB7 tracked skills won't be in .gitignore"));
672
+ }
673
+
674
+ // src/commands/ignore.jsx
675
+ import React6 from "react";
676
+ import { render as render5, Box as Box6, Text as Text6 } from "ink";
677
+ import figures2 from "figures";
678
+ async function ignore() {
679
+ const cwd = process.cwd();
680
+ const ides = detectIDEs(cwd);
681
+ if (ides.length === 0) {
682
+ const { unmount: unmount2 } = render5(
683
+ /* @__PURE__ */ React6.createElement(StatusLine, { variant: "warning" }, "No IDE directories detected. Nothing to ignore.")
684
+ );
685
+ unmount2();
686
+ return;
687
+ }
688
+ const lock = readLock(cwd);
689
+ const tracked = Object.entries(lock.skills).filter(([, i]) => i.track).map(([n]) => n);
690
+ const patterns = buildIgnorePatterns(ides, tracked);
691
+ updateGitignore(cwd, patterns);
692
+ const { unmount } = render5(
693
+ /* @__PURE__ */ React6.createElement(Box6, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React6.createElement(Header, null, ".gitignore updated"), /* @__PURE__ */ React6.createElement(Box6, { flexDirection: "column" }, patterns.map((p) => /* @__PURE__ */ React6.createElement(Box6, { key: p, gap: 2 }, /* @__PURE__ */ React6.createElement(Text6, { color: p.startsWith("!") ? "green" : "yellow" }, p.startsWith("!") ? figures2.tick : figures2.pointerSmall), /* @__PURE__ */ React6.createElement(Text6, null, p)))))
694
+ );
695
+ unmount();
696
+ }
697
+
698
+ // src/commands/help.jsx
699
+ import React7 from "react";
700
+ import { render as render6, Box as Box7, Text as Text7 } from "ink";
701
+ var COMMANDS = [
702
+ { cmd: "sr", desc: "Sync all active skills across every detected IDE" },
703
+ { cmd: "sr init", desc: "Create config files and update .gitignore" },
704
+ { cmd: "sr add [skill]", desc: "Assign skills to stages" },
705
+ { cmd: "sr remove [skill]", desc: "Remove skill-to-stage assignments" },
706
+ { cmd: "sr use [stage]", desc: "Activate a stage \u2014 stash / restore skills" },
707
+ { cmd: "sr use --off", desc: "Restore all stashed skills, clear active stage" },
708
+ { cmd: "sr list", desc: "Manage git tracking for skills" },
709
+ { cmd: "sr ignore", desc: "Regenerate .gitignore skill-rules block" },
710
+ { cmd: "sr help", desc: "Show this help" }
711
+ ];
712
+ var OPTIONS = [
713
+ { flag: "--stage <name>", desc: "Limit sync to a specific stage" },
714
+ { flag: "--version", desc: "Show version number" },
715
+ { flag: "<command> --help", desc: "Show detailed help for a command" }
716
+ ];
717
+ var CMD_W = Math.max(...COMMANDS.map((c) => c.cmd.length)) + 2;
718
+ var OPT_W = Math.max(...OPTIONS.map((o) => o.flag.length)) + 2;
719
+ async function help() {
720
+ const { unmount } = render6(/* @__PURE__ */ React7.createElement(HelpUI, null));
721
+ unmount();
722
+ }
723
+ function HelpUI() {
724
+ return /* @__PURE__ */ React7.createElement(Box7, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React7.createElement(Header, null, "skill-rules"), /* @__PURE__ */ React7.createElement(Text7, { dimColor: true }, "Sync AI agent skills across IDEs with per-stage rules"), /* @__PURE__ */ React7.createElement(Box7, { flexDirection: "column" }, /* @__PURE__ */ React7.createElement(Text7, { bold: true }, "Commands"), COMMANDS.map(({ cmd, desc }) => /* @__PURE__ */ React7.createElement(Box7, { key: cmd, gap: 2 }, /* @__PURE__ */ React7.createElement(Text7, { color: "cyan" }, cmd.padEnd(CMD_W)), /* @__PURE__ */ React7.createElement(Text7, null, desc)))), /* @__PURE__ */ React7.createElement(Box7, { flexDirection: "column" }, /* @__PURE__ */ React7.createElement(Text7, { bold: true }, "Options"), OPTIONS.map(({ flag, desc }) => /* @__PURE__ */ React7.createElement(Box7, { key: flag, gap: 2 }, /* @__PURE__ */ React7.createElement(Text7, { color: "yellow" }, flag.padEnd(OPT_W)), /* @__PURE__ */ React7.createElement(Text7, { dimColor: true }, desc)))), /* @__PURE__ */ React7.createElement(Hint, null, "https://github.com/carrilloapps/skill-rules"));
725
+ }
726
+
727
+ // src/commands/remove.jsx
728
+ import React8, { useState as useState5, useEffect as useEffect4 } from "react";
729
+ import { render as render7, Box as Box8, Text as Text8, useApp as useApp4 } from "ink";
730
+ import { MultiSelect as MultiSelect3 } from "@inkjs/ui";
731
+ var ALL_STAGES = "__all__";
732
+ async function remove(skill, options = {}) {
733
+ const cwd = process.cwd();
734
+ const stage = options.stage ?? null;
735
+ if (skill) {
736
+ runNonInteractive2(cwd, skill, stage);
737
+ } else {
738
+ const { waitUntilExit } = render7(/* @__PURE__ */ React8.createElement(RemoveWizard, { cwd }));
739
+ await waitUntilExit();
740
+ }
741
+ }
742
+ function runNonInteractive2(cwd, skillName, stage) {
743
+ const rules = readRules(cwd);
744
+ if (!rules) {
745
+ const { unmount: unmount2 } = render7(
746
+ /* @__PURE__ */ React8.createElement(StatusLine, { variant: "warning" }, "No skills.rules found. Nothing to remove.")
747
+ );
748
+ unmount2();
749
+ return;
750
+ }
751
+ const stages = listStages(rules);
752
+ const inAnyStage = stages.some((s) => (rules.stages[s] ?? []).includes(skillName));
753
+ if (!inAnyStage) {
754
+ const { unmount: unmount2 } = render7(
755
+ /* @__PURE__ */ React8.createElement(StatusLine, { variant: "warning" }, '"', skillName, '" is not assigned to any stage.')
756
+ );
757
+ unmount2();
758
+ return;
759
+ }
760
+ if (stage && !(rules.stages[stage] ?? []).includes(skillName)) {
761
+ const assignedTo = stages.filter((s) => (rules.stages[s] ?? []).includes(skillName));
762
+ const { unmount: unmount2 } = render7(
763
+ /* @__PURE__ */ React8.createElement(StatusLine, { variant: "warning" }, '"', skillName, '" is not in [', stage, "]. Assigned to: ", assignedTo.join(", "))
764
+ );
765
+ unmount2();
766
+ return;
767
+ }
768
+ removeSkillFromStage(rules, skillName, stage ?? null);
769
+ writeRules(rules, cwd);
770
+ const lock = readLock(cwd);
771
+ const ides = detectIDEs(cwd);
772
+ if (ides.length > 0) syncGitignore(cwd, ides, lock);
773
+ const { unmount } = render7(
774
+ /* @__PURE__ */ React8.createElement(Box8, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React8.createElement(Header, null, "skill-rules remove ", skillName), /* @__PURE__ */ React8.createElement(StatusLine, { variant: "success" }, "Removed from ", stage ? `[${stage}]` : "all stages"), ides.length > 0 && /* @__PURE__ */ React8.createElement(StatusLine, { variant: "success" }, ".gitignore \u2014 updated"))
775
+ );
776
+ unmount();
777
+ }
778
+ function RemoveWizard({ cwd }) {
779
+ const { exit } = useApp4();
780
+ const [step, setStep] = useState5("select-skills");
781
+ const [selectedSkills, setSelectedSkills] = useState5([]);
782
+ const [summary, setSummary] = useState5(null);
783
+ const lock = readLock(cwd);
784
+ const ides = detectIDEs(cwd);
785
+ const rules = readRules(cwd) ?? { version: 1, stages: {} };
786
+ const stages = listStages(rules);
787
+ const stagedSkills = [...new Set(Object.values(rules.stages).flat())];
788
+ useEffect4(() => {
789
+ if (step === "done") exit();
790
+ }, [step]);
791
+ if (stagedSkills.length === 0) {
792
+ setTimeout(exit, 0);
793
+ return /* @__PURE__ */ React8.createElement(Box8, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React8.createElement(StatusLine, { variant: "warning" }, "No skills are assigned to any stage."), /* @__PURE__ */ React8.createElement(Text8, { dimColor: true }, "Use skill-rules add to assign skills to stages first."));
794
+ }
795
+ if (step === "select-skills") {
796
+ const options = stagedSkills.map((name) => {
797
+ const skillStages = stages.filter((s) => (rules.stages[s] ?? []).includes(name));
798
+ return {
799
+ label: `${name.padEnd(20)} ${skillStages.join(", ")}`,
800
+ value: name
801
+ };
802
+ });
803
+ return /* @__PURE__ */ React8.createElement(Box8, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React8.createElement(Header, null, "Remove from stage"), /* @__PURE__ */ React8.createElement(Box8, { gap: 4 }, /* @__PURE__ */ React8.createElement(Text8, { dimColor: true }, "SKILL".padEnd(20)), /* @__PURE__ */ React8.createElement(Text8, { dimColor: true }, "ASSIGNED STAGES")), /* @__PURE__ */ React8.createElement(
804
+ MultiSelect3,
805
+ {
806
+ options,
807
+ onSubmit: (values) => {
808
+ if (values.length === 0) {
809
+ exit();
810
+ return;
811
+ }
812
+ setSelectedSkills(values);
813
+ setStep("select-stage");
814
+ }
815
+ }
816
+ ), /* @__PURE__ */ React8.createElement(Hint, null, "Space toggle \xB7 Enter confirm \xB7 0 selected = cancel"));
817
+ }
818
+ if (step === "select-stage") {
819
+ const relevantStages = stages.filter(
820
+ (s) => selectedSkills.some((skill) => (rules.stages[s] ?? []).includes(skill))
821
+ );
822
+ const stageOptions = [
823
+ ...relevantStages.map((s) => ({ label: s, value: s })),
824
+ { label: "All stages", value: ALL_STAGES }
825
+ ];
826
+ return /* @__PURE__ */ React8.createElement(Box8, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React8.createElement(Header, null, "Remove from which stage?"), /* @__PURE__ */ React8.createElement(Text8, { dimColor: true }, "Skills: ", /* @__PURE__ */ React8.createElement(Text8, { color: "cyan" }, selectedSkills.join(", "))), /* @__PURE__ */ React8.createElement(
827
+ ListSelect,
828
+ {
829
+ options: stageOptions,
830
+ onSelect: (value) => {
831
+ const stage = value === ALL_STAGES ? null : value;
832
+ applyRemove(cwd, selectedSkills, stage, rules, lock, ides);
833
+ setSummary({ skills: selectedSkills, stage: value });
834
+ setStep("done");
835
+ }
836
+ }
837
+ ), /* @__PURE__ */ React8.createElement(Hint, null, "\u2191\u2193 navigate \xB7 Enter confirm"));
838
+ }
839
+ if (step === "done" && summary) {
840
+ return /* @__PURE__ */ React8.createElement(Box8, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React8.createElement(Header, null, "Done"), summary.skills.map((name) => /* @__PURE__ */ React8.createElement(StatusLine, { key: name, variant: "success" }, name, " removed from ", summary.stage === ALL_STAGES ? "all stages" : `[${summary.stage}]`)), /* @__PURE__ */ React8.createElement(Text8, { dimColor: true }, "Run skill-rules list to manage git tracking."));
841
+ }
842
+ return null;
843
+ }
844
+ function applyRemove(cwd, skillNames, stage, rules, lock, ides) {
845
+ for (const name of skillNames) {
846
+ removeSkillFromStage(rules, name, stage);
847
+ }
848
+ writeRules(rules, cwd);
849
+ if (ides.length > 0) syncGitignore(cwd, ides, lock);
850
+ }
851
+
852
+ // src/commands/use.jsx
853
+ import React9, { useState as useState6, useEffect as useEffect5 } from "react";
854
+ import { render as render8, Box as Box9, Text as Text9, useApp as useApp5 } from "ink";
855
+ import { existsSync as existsSync9 } from "fs";
856
+ import { join as join10 } from "path";
857
+
858
+ // src/lib/stash.js
859
+ import { existsSync as existsSync8, statSync as statSync3, readdirSync as readdirSync2, mkdirSync as mkdirSync3, rmSync } from "fs";
860
+ import { join as join9 } from "path";
861
+ var STASH_DIR = ".skill-rules/stash";
862
+ function stashSkillPath(cwd, skillName) {
863
+ return join9(cwd, STASH_DIR, skillName);
864
+ }
865
+ function isStashed(cwd, skillName) {
866
+ const p = stashSkillPath(cwd, skillName);
867
+ return existsSync8(p) && statSync3(p).isDirectory();
868
+ }
869
+ function listStashed(cwd) {
870
+ const stashDir = join9(cwd, STASH_DIR);
871
+ if (!existsSync8(stashDir)) return [];
872
+ return readdirSync2(stashDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
873
+ }
874
+ function stashSkill(cwd, ides, skillName) {
875
+ const dest = stashSkillPath(cwd, skillName);
876
+ if (existsSync8(dest)) return;
877
+ const source = ides.find((ide) => {
878
+ const p = join9(cwd, ide.skillsDir, skillName);
879
+ return existsSync8(p) && statSync3(p).isDirectory();
880
+ });
881
+ if (source) {
882
+ mkdirSync3(join9(cwd, STASH_DIR), { recursive: true });
883
+ copyDirSync(join9(cwd, source.skillsDir, skillName), dest);
884
+ }
885
+ for (const ide of ides) {
886
+ const p = join9(cwd, ide.skillsDir, skillName);
887
+ if (existsSync8(p)) rmSync(p, { recursive: true, force: true });
888
+ }
889
+ }
890
+ function restoreSkill(cwd, ides, skillName) {
891
+ const src = stashSkillPath(cwd, skillName);
892
+ if (!existsSync8(src)) return;
893
+ for (const ide of ides) {
894
+ const ideSkillsDir = join9(cwd, ide.skillsDir);
895
+ const targetPath = join9(ideSkillsDir, skillName);
896
+ if (!existsSync8(targetPath)) {
897
+ mkdirSync3(ideSkillsDir, { recursive: true });
898
+ copyDirSync(src, targetPath);
899
+ }
900
+ }
901
+ rmSync(src, { recursive: true, force: true });
902
+ }
903
+
904
+ // src/commands/use.jsx
905
+ async function use(stage, options = {}) {
906
+ const cwd = process.cwd();
907
+ const { waitUntilExit } = render8(
908
+ /* @__PURE__ */ React9.createElement(UseWizard, { cwd, stage: stage ?? null, off: options.off ?? false })
909
+ );
910
+ await waitUntilExit();
911
+ }
912
+ function buildPlan(cwd, stage, ides, lock, rules) {
913
+ const targetSkills = new Set(rules.stages[stage] ?? []);
914
+ const allStagedSkills = new Set(getActiveSkills(rules, null));
915
+ const trackedSkills = new Set(
916
+ Object.entries(lock.skills ?? {}).filter(([, info]) => info.track).map(([name]) => name)
917
+ );
918
+ const toActivate = [];
919
+ const toRestore = [];
920
+ const toMissing = [];
921
+ const toStash = [];
922
+ const toSkip = [];
923
+ for (const skill of allStagedSkills) {
924
+ if (trackedSkills.has(skill)) {
925
+ toSkip.push(skill);
926
+ continue;
927
+ }
928
+ if (targetSkills.has(skill)) {
929
+ if (isStashed(cwd, skill)) {
930
+ toRestore.push(skill);
931
+ } else {
932
+ const inIDEs = ides.some((ide) => existsSync9(join10(cwd, ide.skillsDir, skill)));
933
+ if (inIDEs) toActivate.push(skill);
934
+ else toMissing.push(skill);
935
+ }
936
+ } else {
937
+ if (!isStashed(cwd, skill)) {
938
+ const inIDEs = ides.some((ide) => existsSync9(join10(cwd, ide.skillsDir, skill)));
939
+ if (inIDEs) toStash.push(skill);
940
+ }
941
+ }
942
+ }
943
+ return { toActivate, toRestore, toMissing, toStash, toSkip };
944
+ }
945
+ function executePlan(cwd, stage, ides, lock, plan) {
946
+ for (const skill of plan.toStash) stashSkill(cwd, ides, skill);
947
+ for (const skill of plan.toRestore) restoreSkill(cwd, ides, skill);
948
+ setActiveStage(stage, cwd);
949
+ syncGitignore(cwd, ides, lock);
950
+ }
951
+ function UseWizard({ cwd, stage, off }) {
952
+ const { exit } = useApp5();
953
+ const [step, setStep] = useState6("init");
954
+ const [data, setData] = useState6(null);
955
+ useEffect5(() => {
956
+ const ides = detectIDEs(cwd);
957
+ const lock = readLock(cwd);
958
+ const currentStage = getActiveStage(cwd);
959
+ if (!stage && !off) {
960
+ const stashed = listStashed(cwd);
961
+ const rules2 = readRules(cwd);
962
+ setData({ mode: "status", currentStage, stashed, rules: rules2 });
963
+ setStep("done");
964
+ return;
965
+ }
966
+ if (off) {
967
+ const stashed = listStashed(cwd);
968
+ if (stashed.length === 0) {
969
+ clearActiveStage(cwd);
970
+ setData({ mode: "off", stashed: [] });
971
+ setStep("done");
972
+ return;
973
+ }
974
+ if (ides.length === 0) {
975
+ setData({
976
+ mode: "error",
977
+ error: "No IDE directories detected \u2014 cannot restore stashed skills."
978
+ });
979
+ setStep("done");
980
+ return;
981
+ }
982
+ try {
983
+ for (const skill of stashed) restoreSkill(cwd, ides, skill);
984
+ clearActiveStage(cwd);
985
+ syncGitignore(cwd, ides, lock);
986
+ setData({ mode: "off", stashed });
987
+ } catch (err) {
988
+ setData({ mode: "error", error: err.message });
989
+ }
990
+ setStep("done");
991
+ return;
992
+ }
993
+ if (ides.length === 0) {
994
+ setData({
995
+ mode: "error",
996
+ error: "No IDE directories detected.\nExpected: .claude/ .cursor/ .windsurf/ .agents/ .openhands/"
997
+ });
998
+ setStep("done");
999
+ return;
1000
+ }
1001
+ const rules = readRules(cwd);
1002
+ if (!rules) {
1003
+ setData({ mode: "error", error: "No skills.rules found. Run: skill-rules init" });
1004
+ setStep("done");
1005
+ return;
1006
+ }
1007
+ const available = listStages(rules);
1008
+ if (!available.includes(stage)) {
1009
+ setData({
1010
+ mode: "error",
1011
+ error: `Stage "${stage}" not found.${available.length ? `
1012
+ Available: ${available.join(", ")}` : "\nNo stages defined yet."}`
1013
+ });
1014
+ setStep("done");
1015
+ return;
1016
+ }
1017
+ if (currentStage === stage) {
1018
+ setData({ mode: "already", stage });
1019
+ setStep("done");
1020
+ return;
1021
+ }
1022
+ const plan = buildPlan(cwd, stage, ides, lock, rules);
1023
+ if (plan.toStash.length === 0) {
1024
+ try {
1025
+ executePlan(cwd, stage, ides, lock, plan);
1026
+ setData({ mode: "use", stage, plan });
1027
+ } catch (err) {
1028
+ setData({ mode: "error", error: err.message });
1029
+ }
1030
+ setStep("done");
1031
+ return;
1032
+ }
1033
+ setData({ mode: "use", stage, plan, ides, lock });
1034
+ setStep("confirm");
1035
+ }, []);
1036
+ useEffect5(() => {
1037
+ if (step === "done") exit();
1038
+ }, [step]);
1039
+ if (step === "confirm" && data) {
1040
+ const { plan, stage: targetStage, ides: ideList, lock: lockData } = data;
1041
+ return /* @__PURE__ */ React9.createElement(Box9, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React9.createElement(Header, null, "skill-rules use ", targetStage), /* @__PURE__ */ React9.createElement(PlanPreview, { plan }), /* @__PURE__ */ React9.createElement(Text9, { dimColor: true }, 'Skills marked "stash" will be removed from IDEs and saved locally.'), /* @__PURE__ */ React9.createElement(
1042
+ ListSelect,
1043
+ {
1044
+ options: [
1045
+ { label: "Yes, activate stage", value: "yes" },
1046
+ { label: "Cancel", value: "no" }
1047
+ ],
1048
+ onSelect: (value) => {
1049
+ if (value === "no") {
1050
+ exit();
1051
+ return;
1052
+ }
1053
+ try {
1054
+ executePlan(cwd, targetStage, ideList, lockData, plan);
1055
+ setStep("done");
1056
+ } catch (err) {
1057
+ setData((d) => ({ ...d, mode: "error", error: err.message }));
1058
+ setStep("done");
1059
+ }
1060
+ }
1061
+ }
1062
+ ), /* @__PURE__ */ React9.createElement(Hint, null, "\u2191\u2193 navigate \xB7 Enter confirm"));
1063
+ }
1064
+ if (step === "done" && data) {
1065
+ return /* @__PURE__ */ React9.createElement(DoneView, { data });
1066
+ }
1067
+ return null;
1068
+ }
1069
+ function PlanPreview({ plan }) {
1070
+ const { toActivate, toRestore, toMissing, toStash, toSkip } = plan;
1071
+ return /* @__PURE__ */ React9.createElement(Box9, { flexDirection: "column" }, toActivate.map((s) => /* @__PURE__ */ React9.createElement(StatusLine, { key: s, variant: "ok" }, "keep ", " ", s)), toRestore.map((s) => /* @__PURE__ */ React9.createElement(StatusLine, { key: s, variant: "success" }, "restore ", " ", s, /* @__PURE__ */ React9.createElement(Text9, { dimColor: true }, " (from stash)"))), toMissing.map((s) => /* @__PURE__ */ React9.createElement(StatusLine, { key: s, variant: "warning" }, "missing ", " ", s, /* @__PURE__ */ React9.createElement(Text9, { dimColor: true }, " (not installed)"))), toStash.map((s) => /* @__PURE__ */ React9.createElement(StatusLine, { key: s, variant: "warning" }, "stash ", " ", s)), toSkip.map((s) => /* @__PURE__ */ React9.createElement(StatusLine, { key: s, variant: "info" }, "skip ", " ", s, /* @__PURE__ */ React9.createElement(Text9, { dimColor: true }, " (tracked in git)"))));
1072
+ }
1073
+ function DoneView({ data }) {
1074
+ const { mode } = data;
1075
+ if (mode === "error") {
1076
+ return /* @__PURE__ */ React9.createElement(Box9, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React9.createElement(Text9, { color: "red" }, data.error));
1077
+ }
1078
+ if (mode === "already") {
1079
+ return /* @__PURE__ */ React9.createElement(StatusLine, { variant: "info" }, "Stage [", data.stage, "] is already active.");
1080
+ }
1081
+ if (mode === "off") {
1082
+ const { stashed } = data;
1083
+ if (stashed.length === 0) {
1084
+ return /* @__PURE__ */ React9.createElement(StatusLine, { variant: "info" }, "Nothing stashed \u2014 no active stage to clear.");
1085
+ }
1086
+ return /* @__PURE__ */ React9.createElement(Box9, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React9.createElement(Header, null, "Done"), stashed.map((s) => /* @__PURE__ */ React9.createElement(StatusLine, { key: s, variant: "success" }, "restored ", s)), /* @__PURE__ */ React9.createElement(Text9, { dimColor: true }, "No active stage \u2014 all skills available."));
1087
+ }
1088
+ if (mode === "status") {
1089
+ const { currentStage, stashed, rules } = data;
1090
+ const stages = listStages(rules ?? { stages: {} });
1091
+ if (!currentStage) {
1092
+ return /* @__PURE__ */ React9.createElement(Box9, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React9.createElement(StatusLine, { variant: "info" }, "No active stage \u2014 all skills available."), stages.length > 0 && /* @__PURE__ */ React9.createElement(Text9, { dimColor: true }, "Run: skill-rules use ", "<stage>", " to activate one"));
1093
+ }
1094
+ return /* @__PURE__ */ React9.createElement(Box9, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React9.createElement(Header, null, "skill-rules use"), /* @__PURE__ */ React9.createElement(StatusLine, { variant: "success" }, "Active stage: [", currentStage, "]"), stashed.length > 0 && /* @__PURE__ */ React9.createElement(Text9, { dimColor: true }, "Stashed (", stashed.length, "): ", stashed.join(", ")), /* @__PURE__ */ React9.createElement(Text9, { dimColor: true }, "Run: skill-rules use --off to restore all"));
1095
+ }
1096
+ if (mode === "use") {
1097
+ const { plan, stage } = data;
1098
+ const { toActivate, toRestore, toMissing, toStash, toSkip } = plan;
1099
+ return /* @__PURE__ */ React9.createElement(Box9, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React9.createElement(Header, null, "Done"), toActivate.map((s) => /* @__PURE__ */ React9.createElement(StatusLine, { key: s, variant: "ok" }, "active ", s)), toRestore.map((s) => /* @__PURE__ */ React9.createElement(StatusLine, { key: s, variant: "success" }, "restored ", s)), toStash.map((s) => /* @__PURE__ */ React9.createElement(StatusLine, { key: s, variant: "warning" }, "stashed ", s)), toMissing.map((s) => /* @__PURE__ */ React9.createElement(StatusLine, { key: s, variant: "warning" }, "missing ", s, /* @__PURE__ */ React9.createElement(Text9, { dimColor: true }, " \u2014 install via skills.sh or autoskill"))), toSkip.map((s) => /* @__PURE__ */ React9.createElement(StatusLine, { key: s, variant: "info" }, "tracked ", s)), /* @__PURE__ */ React9.createElement(Text9, { dimColor: true }, "Active stage: [", stage, "] \xB7 Run skill-rules to sync IDEs"));
1100
+ }
1101
+ return null;
1102
+ }
1103
+
1104
+ // src/cli.jsx
1105
+ var __dirname = dirname(fileURLToPath(import.meta.url));
1106
+ var { version } = JSON.parse(readFileSync5(join11(__dirname, "../package.json"), "utf8"));
1107
+ var stageOption = ["-s, --stage <stage>", "limit to a specific stage (e.g. dev, qa)"];
1108
+ function createCli() {
1109
+ const program = new Command();
1110
+ program.name("skill-rules").description("Sync AI agent skills across IDEs and manage per-stage skill rules").version(version).enablePositionalOptions().addHelpCommand(false).option(...stageOption).action((options) => run(options));
1111
+ program.command("init").description("Create skills-lock.json, skills.rules, and update .gitignore").action(init);
1112
+ program.command("add [skill]").description("Assign skills to stages \u2014 interactive when called with no arguments").option(...stageOption).option("--track", "mark skill as tracked in git (default: gitignored)").action(add);
1113
+ program.command("remove [skill]").description("Remove skills from stages \u2014 interactive when called with no arguments").option(...stageOption).action(remove);
1114
+ program.command("list").description("Show and manage skills \u2014 track/untrack from git interactively").option("--track <skill>", "commit a skill to git (remove from .gitignore)").option("--untrack <skill>", "exclude a skill from git (back to .gitignore)").action(list);
1115
+ program.command("ignore").description("Regenerate .gitignore for detected IDE skill directories").action(ignore);
1116
+ program.command("use [stage]").description("Activate a stage \u2014 stashes skills not in it, restores those that are").option("--off", "restore all stashed skills and clear the active stage").action(use);
1117
+ program.command("help").description("Show help for all commands").action(help);
1118
+ return program;
1119
+ }
1120
+
1121
+ // src/index.jsx
1122
+ createCli().parseAsync(process.argv).catch((err) => {
1123
+ console.error(`\x1B[31mError:\x1B[0m ${err.message}`);
1124
+ process.exit(1);
1125
+ });