@rafter-security/cli 0.7.0 → 0.7.2

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 (56) hide show
  1. package/README.md +20 -1
  2. package/dist/commands/agent/audit-skill.js +2 -1
  3. package/dist/commands/agent/audit.js +27 -0
  4. package/dist/commands/agent/components.js +800 -0
  5. package/dist/commands/agent/disable.js +47 -0
  6. package/dist/commands/agent/enable.js +50 -0
  7. package/dist/commands/agent/index.js +6 -0
  8. package/dist/commands/agent/init.js +162 -164
  9. package/dist/commands/agent/list.js +72 -0
  10. package/dist/commands/brief.js +20 -0
  11. package/dist/commands/docs/index.js +18 -0
  12. package/dist/commands/docs/list.js +37 -0
  13. package/dist/commands/docs/show.js +64 -0
  14. package/dist/commands/mcp/server.js +84 -0
  15. package/dist/commands/skill/index.js +14 -0
  16. package/dist/commands/skill/install.js +89 -0
  17. package/dist/commands/skill/list.js +79 -0
  18. package/dist/commands/skill/registry.js +273 -0
  19. package/dist/commands/skill/remote.js +333 -0
  20. package/dist/commands/skill/review.js +975 -0
  21. package/dist/commands/skill/uninstall.js +65 -0
  22. package/dist/core/audit-logger.js +262 -21
  23. package/dist/core/config-manager.js +3 -0
  24. package/dist/core/docs-loader.js +148 -0
  25. package/dist/core/policy-loader.js +72 -1
  26. package/dist/index.js +6 -0
  27. package/package.json +1 -1
  28. package/resources/skills/rafter/SKILL.md +76 -96
  29. package/resources/skills/rafter/docs/backend.md +106 -0
  30. package/resources/skills/rafter/docs/cli-reference.md +199 -0
  31. package/resources/skills/rafter/docs/finding-triage.md +79 -0
  32. package/resources/skills/rafter/docs/guardrails.md +91 -0
  33. package/resources/skills/rafter/docs/shift-left.md +64 -0
  34. package/resources/skills/rafter-code-review/SKILL.md +91 -0
  35. package/resources/skills/rafter-code-review/docs/api.md +90 -0
  36. package/resources/skills/rafter-code-review/docs/asvs.md +120 -0
  37. package/resources/skills/rafter-code-review/docs/cwe-top25.md +78 -0
  38. package/resources/skills/rafter-code-review/docs/investigation-playbook.md +101 -0
  39. package/resources/skills/rafter-code-review/docs/llm.md +87 -0
  40. package/resources/skills/rafter-code-review/docs/web-app.md +84 -0
  41. package/resources/skills/rafter-secure-design/SKILL.md +103 -0
  42. package/resources/skills/rafter-secure-design/docs/api-design.md +97 -0
  43. package/resources/skills/rafter-secure-design/docs/auth.md +67 -0
  44. package/resources/skills/rafter-secure-design/docs/data-storage.md +90 -0
  45. package/resources/skills/rafter-secure-design/docs/dependencies.md +101 -0
  46. package/resources/skills/rafter-secure-design/docs/deployment.md +104 -0
  47. package/resources/skills/rafter-secure-design/docs/ingestion.md +98 -0
  48. package/resources/skills/rafter-secure-design/docs/standards-pointers.md +102 -0
  49. package/resources/skills/rafter-secure-design/docs/threat-modeling.md +128 -0
  50. package/resources/skills/rafter-skill-review/SKILL.md +106 -0
  51. package/resources/skills/rafter-skill-review/docs/authorship-provenance.md +82 -0
  52. package/resources/skills/rafter-skill-review/docs/changelog-review.md +99 -0
  53. package/resources/skills/rafter-skill-review/docs/data-practices.md +88 -0
  54. package/resources/skills/rafter-skill-review/docs/malware-indicators.md +79 -0
  55. package/resources/skills/rafter-skill-review/docs/prompt-injection.md +85 -0
  56. package/resources/skills/rafter-skill-review/docs/telemetry.md +78 -0
@@ -0,0 +1,72 @@
1
+ import { Command } from "commander";
2
+ import { snapshotComponents } from "./components.js";
3
+ import { fmt } from "../../utils/formatter.js";
4
+ /**
5
+ * `rafter agent list` — machine-readable inventory of everything rafter has touched
6
+ * (or could touch) on this machine. One row per (platform, component).
7
+ *
8
+ * Exit codes: 0 on success.
9
+ */
10
+ export function createListCommand() {
11
+ return new Command("list")
12
+ .description("List agent integration components and their state")
13
+ .option("--json", "Output machine-readable JSON")
14
+ .option("--installed", "Only show components that are currently installed")
15
+ .option("--detected", "Only show components whose platform is detected")
16
+ .action((opts) => {
17
+ let rows = snapshotComponents();
18
+ if (opts.installed)
19
+ rows = rows.filter((r) => r.installed);
20
+ if (opts.detected)
21
+ rows = rows.filter((r) => r.detected);
22
+ if (opts.json) {
23
+ const payload = {
24
+ components: rows.map((r) => ({
25
+ id: r.id,
26
+ platform: r.platform,
27
+ kind: r.kind,
28
+ description: r.description,
29
+ path: r.path,
30
+ state: r.state,
31
+ installed: r.installed,
32
+ detected: r.detected,
33
+ configEnabled: r.configEnabled,
34
+ })),
35
+ };
36
+ console.log(JSON.stringify(payload, null, 2));
37
+ return;
38
+ }
39
+ const byPlatform = new Map();
40
+ for (const r of rows) {
41
+ const arr = byPlatform.get(r.platform) ?? [];
42
+ arr.push(r);
43
+ byPlatform.set(r.platform, arr);
44
+ }
45
+ console.log(fmt.header("Rafter agent components"));
46
+ console.log(fmt.divider());
47
+ for (const [platform, list] of byPlatform) {
48
+ const detected = list[0]?.detected ?? false;
49
+ const suffix = detected ? "" : " (not detected)";
50
+ console.log(`\n${platform}${suffix}`);
51
+ for (const r of list) {
52
+ const label = r.id.padEnd(28);
53
+ let marker;
54
+ switch (r.state) {
55
+ case "installed":
56
+ marker = "● installed";
57
+ break;
58
+ case "not-installed":
59
+ marker = "○ not installed";
60
+ break;
61
+ case "not-detected":
62
+ default:
63
+ marker = "· platform not detected";
64
+ break;
65
+ }
66
+ console.log(` ${label} ${marker}`);
67
+ }
68
+ }
69
+ console.log();
70
+ console.log(fmt.info("Use `rafter agent enable <id>` or `rafter agent disable <id>` to toggle individual components."));
71
+ });
72
+ }
@@ -9,6 +9,16 @@ function loadSkill(name) {
9
9
  // Strip YAML frontmatter
10
10
  return raw.replace(/^---[\s\S]*?---\n*/, "").trim();
11
11
  }
12
+ function loadSkillDoc(skill, doc) {
13
+ return readFileSync(join(RESOURCES_DIR, skill, "docs", `${doc}.md`), "utf-8").trim();
14
+ }
15
+ const RAFTER_SUBDOCS = [
16
+ { slug: "cli-reference", desc: "Full rafter CLI tree by category" },
17
+ { slug: "guardrails", desc: "PreToolUse hooks, risk tiers, overrides" },
18
+ { slug: "backend", desc: "Remote fast vs plus, setup, cost/latency" },
19
+ { slug: "shift-left", desc: "Pointers to secure-design & code-review skills" },
20
+ { slug: "finding-triage", desc: "How to read a finding and decide next steps" },
21
+ ];
12
22
  function extractSections(content, headings) {
13
23
  const lines = content.split("\n");
14
24
  const sections = [];
@@ -165,6 +175,13 @@ function buildTopics() {
165
175
  "the tools developers use every day.",
166
176
  ].join("\n"),
167
177
  },
178
+ ...Object.fromEntries(RAFTER_SUBDOCS.map(({ slug, desc }) => [
179
+ slug,
180
+ {
181
+ description: desc,
182
+ render: () => loadSkillDoc("rafter", slug),
183
+ },
184
+ ])),
168
185
  all: {
169
186
  description: "Everything — full security + scanning + setup briefing",
170
187
  render: () => {
@@ -503,6 +520,9 @@ function renderTopicList(topics) {
503
520
  lines.push(" rafter brief commands # full command reference");
504
521
  lines.push(" rafter brief setup/claude-code # Claude Code setup guide");
505
522
  lines.push(" rafter brief setup/generic # setup for any agent");
523
+ lines.push(" rafter brief cli-reference # full CLI tree");
524
+ lines.push(" rafter brief guardrails # hooks + risk tiers");
525
+ lines.push(" rafter brief finding-triage # interpret findings");
506
526
  lines.push(" rafter brief all # everything");
507
527
  return lines.join("\n");
508
528
  }
@@ -0,0 +1,18 @@
1
+ import { Command } from "commander";
2
+ import { createDocsListCommand } from "./list.js";
3
+ import { createDocsShowCommand } from "./show.js";
4
+ export function createDocsCommand() {
5
+ const docs = new Command("docs")
6
+ .description("Repo-specific security docs declared in .rafter.yml")
7
+ .addHelpText("after", [
8
+ "",
9
+ "Examples:",
10
+ " $ rafter docs list",
11
+ " $ rafter docs list --tag owasp",
12
+ " $ rafter docs show secure-coding",
13
+ " $ rafter docs show secure-coding --refresh",
14
+ ].join("\n"));
15
+ docs.addCommand(createDocsListCommand());
16
+ docs.addCommand(createDocsShowCommand());
17
+ return docs;
18
+ }
@@ -0,0 +1,37 @@
1
+ import { Command } from "commander";
2
+ import { listDocs } from "../../core/docs-loader.js";
3
+ export function createDocsListCommand() {
4
+ return new Command("list")
5
+ .description("List security docs declared in .rafter.yml")
6
+ .option("--tag <tag>", "Filter to docs matching this tag")
7
+ .option("--json", "Output as JSON")
8
+ .action((opts) => {
9
+ const entries = listDocs().filter(d => !opts.tag || (Array.isArray(d.tags) && d.tags.includes(opts.tag)));
10
+ if (entries.length === 0) {
11
+ if (opts.json) {
12
+ process.stdout.write("[]\n");
13
+ }
14
+ else {
15
+ process.stderr.write("No docs configured in .rafter.yml\n");
16
+ }
17
+ process.exit(3);
18
+ }
19
+ if (opts.json) {
20
+ process.stdout.write(JSON.stringify(entries.map(e => ({
21
+ id: e.id,
22
+ source: e.source,
23
+ source_kind: e.sourceKind,
24
+ description: e.description || "",
25
+ tags: e.tags || [],
26
+ cache_status: e.cacheStatus,
27
+ })), null, 2) + "\n");
28
+ return;
29
+ }
30
+ for (const e of entries) {
31
+ const tags = (e.tags && e.tags.length) ? ` [${e.tags.join(", ")}]` : "";
32
+ const cache = e.sourceKind === "url" ? ` (${e.cacheStatus})` : "";
33
+ const desc = e.description ? ` — ${e.description}` : "";
34
+ process.stdout.write(`${e.id} ${e.source}${cache}${tags}${desc}\n`);
35
+ }
36
+ });
37
+ }
@@ -0,0 +1,64 @@
1
+ import { Command } from "commander";
2
+ import { resolveDocSelector, fetchDoc } from "../../core/docs-loader.js";
3
+ export function createDocsShowCommand() {
4
+ return new Command("show")
5
+ .description("Print the content of a doc by id or tag")
6
+ .argument("<id-or-tag>", "Doc id or tag selector")
7
+ .option("--refresh", "Force re-fetch for URL-backed docs (bypass cache)")
8
+ .option("--json", "Output as JSON (array of { id, source, content })")
9
+ .action(async (selector, opts) => {
10
+ const entries = resolveDocSelector(selector);
11
+ if (entries.length === 0) {
12
+ const { loadPolicy } = await import("../../core/policy-loader.js");
13
+ const policy = loadPolicy();
14
+ if (!policy || !policy.docs || policy.docs.length === 0) {
15
+ process.stderr.write("No docs configured in .rafter.yml\n");
16
+ process.exit(3);
17
+ }
18
+ process.stderr.write(`No doc matched id or tag: ${selector}\n`);
19
+ process.exit(2);
20
+ }
21
+ const results = [];
22
+ let anyError = false;
23
+ for (const entry of entries) {
24
+ try {
25
+ const fetched = await fetchDoc(entry, { refresh: opts.refresh });
26
+ results.push({
27
+ id: entry.id,
28
+ source: fetched.source,
29
+ source_kind: fetched.sourceKind,
30
+ stale: fetched.stale,
31
+ content: fetched.content,
32
+ });
33
+ if (fetched.stale) {
34
+ process.stderr.write(`Warning: ${entry.id} served from stale cache (fetch failed)\n`);
35
+ }
36
+ }
37
+ catch (err) {
38
+ anyError = true;
39
+ process.stderr.write(`Error: failed to fetch ${entry.id}: ${err.message || err}\n`);
40
+ }
41
+ }
42
+ if (results.length === 0) {
43
+ process.exit(1);
44
+ }
45
+ if (opts.json) {
46
+ process.stdout.write(JSON.stringify(results, null, 2) + "\n");
47
+ }
48
+ else if (results.length === 1) {
49
+ process.stdout.write(results[0].content);
50
+ if (!results[0].content.endsWith("\n"))
51
+ process.stdout.write("\n");
52
+ }
53
+ else {
54
+ for (const r of results) {
55
+ process.stdout.write(`\n===== ${r.id} (${r.source}) =====\n`);
56
+ process.stdout.write(r.content);
57
+ if (!r.content.endsWith("\n"))
58
+ process.stdout.write("\n");
59
+ }
60
+ }
61
+ if (anyError)
62
+ process.exit(1);
63
+ });
64
+ }
@@ -7,6 +7,7 @@ import { GitleaksScanner } from "../../scanners/gitleaks.js";
7
7
  import { CommandInterceptor } from "../../core/command-interceptor.js";
8
8
  import { AuditLogger } from "../../core/audit-logger.js";
9
9
  import { ConfigManager } from "../../core/config-manager.js";
10
+ import { listDocs, resolveDocSelector, fetchDoc } from "../../core/docs-loader.js";
10
11
  import { createRequire } from "module";
11
12
  const _require = createRequire(import.meta.url);
12
13
  const { version: CLI_VERSION } = _require("../../../package.json");
@@ -87,6 +88,28 @@ export function createServer() {
87
88
  },
88
89
  },
89
90
  },
91
+ {
92
+ name: "list_docs",
93
+ description: "List repo-specific security docs declared in .rafter.yml. Call this early in any security-relevant task to discover project-specific rules, threat models, or compliance policies the user expects agents to follow.",
94
+ inputSchema: {
95
+ type: "object",
96
+ properties: {
97
+ tag: { type: "string", description: "Filter to docs whose tags include this value" },
98
+ },
99
+ },
100
+ },
101
+ {
102
+ name: "get_doc",
103
+ description: "Return the content of a repo-specific security doc by id or tag. Use after list_docs to read a specific document.",
104
+ inputSchema: {
105
+ type: "object",
106
+ properties: {
107
+ id_or_tag: { type: "string", description: "Doc id or tag selector" },
108
+ refresh: { type: "boolean", description: "Force re-fetch for URL-backed docs (bypass cache)" },
109
+ },
110
+ required: ["id_or_tag"],
111
+ },
112
+ },
90
113
  ],
91
114
  }));
92
115
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -149,6 +172,44 @@ export function createServer() {
149
172
  const value = key ? manager.get(key) : manager.load();
150
173
  return textResult(value);
151
174
  }
175
+ case "list_docs": {
176
+ const tag = args?.tag;
177
+ const entries = listDocs().filter(d => !tag || (Array.isArray(d.tags) && d.tags.includes(tag)));
178
+ return textResult(entries.map(e => ({
179
+ id: e.id,
180
+ source: e.source,
181
+ source_kind: e.sourceKind,
182
+ description: e.description || "",
183
+ tags: e.tags || [],
184
+ cache_status: e.cacheStatus,
185
+ })));
186
+ }
187
+ case "get_doc": {
188
+ const selector = args?.id_or_tag;
189
+ if (!selector)
190
+ return errorResult("id_or_tag is required");
191
+ const matches = resolveDocSelector(selector);
192
+ if (matches.length === 0)
193
+ return errorResult(`No doc matched id or tag: ${selector}`);
194
+ const refresh = Boolean(args?.refresh);
195
+ const results = [];
196
+ for (const entry of matches) {
197
+ try {
198
+ const fetched = await fetchDoc(entry, { refresh });
199
+ results.push({
200
+ id: entry.id,
201
+ source: fetched.source,
202
+ source_kind: fetched.sourceKind,
203
+ stale: fetched.stale,
204
+ content: fetched.content,
205
+ });
206
+ }
207
+ catch (err) {
208
+ return errorResult(`Failed to fetch ${entry.id}: ${err.message || err}`);
209
+ }
210
+ }
211
+ return textResult(results);
212
+ }
152
213
  default:
153
214
  return errorResult(`Unknown tool: ${name}`);
154
215
  }
@@ -168,6 +229,12 @@ export function createServer() {
168
229
  description: "Active security policy (merged .rafter.yml + config)",
169
230
  mimeType: "application/json",
170
231
  },
232
+ {
233
+ uri: "rafter://docs",
234
+ name: "Rafter Docs",
235
+ description: "Repo-specific security docs declared in .rafter.yml (metadata only, no content)",
236
+ mimeType: "application/json",
237
+ },
171
238
  ],
172
239
  }));
173
240
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
@@ -190,6 +257,23 @@ export function createServer() {
190
257
  text: JSON.stringify(manager.loadWithPolicy(), null, 2),
191
258
  }],
192
259
  };
260
+ case "rafter://docs": {
261
+ const entries = listDocs().map(e => ({
262
+ id: e.id,
263
+ source: e.source,
264
+ source_kind: e.sourceKind,
265
+ description: e.description || "",
266
+ tags: e.tags || [],
267
+ cache_status: e.cacheStatus,
268
+ }));
269
+ return {
270
+ contents: [{
271
+ uri: "rafter://docs",
272
+ mimeType: "application/json",
273
+ text: JSON.stringify(entries, null, 2),
274
+ }],
275
+ };
276
+ }
193
277
  default:
194
278
  throw new Error(`Unknown resource: ${uri}`);
195
279
  }
@@ -0,0 +1,14 @@
1
+ import { Command } from "commander";
2
+ import { createListCommand } from "./list.js";
3
+ import { createInstallCommand } from "./install.js";
4
+ import { createUninstallCommand } from "./uninstall.js";
5
+ import { createReviewCommand } from "./review.js";
6
+ export function createSkillCommand() {
7
+ const skill = new Command("skill")
8
+ .description("Manage rafter-authored skills (list / install / uninstall / review)");
9
+ skill.addCommand(createListCommand());
10
+ skill.addCommand(createInstallCommand());
11
+ skill.addCommand(createUninstallCommand());
12
+ skill.addCommand(createReviewCommand());
13
+ return skill;
14
+ }
@@ -0,0 +1,89 @@
1
+ import { Command } from "commander";
2
+ import fs from "fs";
3
+ import { resolveSkill, listBundledSkills, skillDestPath, skillDetectDir, resolveExplicitDest, writeSkillTo, recordSkillState, SKILL_PLATFORMS, } from "./registry.js";
4
+ import { fmt } from "../../utils/formatter.js";
5
+ /**
6
+ * `rafter skill install <name>` — install a rafter-authored skill to one or
7
+ * more platforms (or an explicit --to path).
8
+ *
9
+ * Exit codes:
10
+ * 0 — installed successfully (or already installed — copy is idempotent)
11
+ * 1 — unknown skill, unknown platform, or install failure
12
+ * 2 — no detected platform found and --force was not passed
13
+ */
14
+ export function createInstallCommand() {
15
+ return new Command("install")
16
+ .description("Install a rafter-authored skill to detected platform(s) or an explicit path")
17
+ .argument("<name>", "Skill name (e.g. rafter, rafter-secure-design)")
18
+ .option("--platform <platform...>", `Target platform(s). One or more of: ${SKILL_PLATFORMS.join(", ")}. Default: all detected.`)
19
+ .option("--to <path>", "Explicit destination. If it ends in .md/.mdc, used as-is; otherwise treated as a skills-base directory.")
20
+ .option("--force", "Install even if no target platform is detected")
21
+ .action((name, opts) => {
22
+ const skill = resolveSkill(name);
23
+ if (!skill) {
24
+ console.error(fmt.error(`Unknown skill: ${name}`));
25
+ console.error(fmt.info(`Available: ${listBundledSkills().map((s) => s.name).join(", ") || "(none)"}`));
26
+ process.exit(1);
27
+ }
28
+ // --to overrides platform-based resolution.
29
+ if (opts.to) {
30
+ const destPath = resolveExplicitDest(opts.to, skill.name);
31
+ try {
32
+ writeSkillTo(skill, destPath);
33
+ console.log(fmt.success(`Installed ${skill.name} v${skill.version} → ${destPath}`));
34
+ process.exit(0);
35
+ }
36
+ catch (e) {
37
+ console.error(fmt.error(`Failed to install ${skill.name} to ${destPath}: ${e}`));
38
+ process.exit(1);
39
+ }
40
+ }
41
+ // Resolve target platforms: either explicit --platform list, or "all detected".
42
+ let targets;
43
+ if (Array.isArray(opts.platform) && opts.platform.length > 0) {
44
+ targets = [];
45
+ for (const raw of opts.platform) {
46
+ const p = raw.trim();
47
+ if (!SKILL_PLATFORMS.includes(p)) {
48
+ console.error(fmt.error(`Unknown platform: ${raw}. Known: ${SKILL_PLATFORMS.join(", ")}`));
49
+ process.exit(1);
50
+ }
51
+ targets.push(p);
52
+ }
53
+ }
54
+ else {
55
+ targets = SKILL_PLATFORMS.filter((p) => fs.existsSync(skillDetectDir(p)));
56
+ if (targets.length === 0) {
57
+ if (!opts.force) {
58
+ console.error(fmt.warning(`No supported platform detected. Re-run with --platform <name> or --force to install to all known platforms.`));
59
+ process.exit(2);
60
+ }
61
+ targets = [...SKILL_PLATFORMS];
62
+ }
63
+ }
64
+ let exitCode = 0;
65
+ for (const platform of targets) {
66
+ const detected = fs.existsSync(skillDetectDir(platform));
67
+ if (!detected && !opts.force && !(Array.isArray(opts.platform) && opts.platform.length > 0)) {
68
+ // Shouldn't hit this — we pre-filtered to detected — but defensive.
69
+ continue;
70
+ }
71
+ if (!detected && !opts.force && Array.isArray(opts.platform) && opts.platform.length > 0) {
72
+ console.error(fmt.warning(`${platform}: not detected (${skillDetectDir(platform)}). Re-run with --force to install anyway.`));
73
+ exitCode = exitCode || 2;
74
+ continue;
75
+ }
76
+ const destPath = skillDestPath(platform, skill.name);
77
+ try {
78
+ writeSkillTo(skill, destPath);
79
+ recordSkillState(platform, skill.name, true, skill.version);
80
+ console.log(fmt.success(`Installed ${skill.name} v${skill.version} → ${destPath} (${platform})`));
81
+ }
82
+ catch (e) {
83
+ console.error(fmt.error(`Failed to install ${skill.name} for ${platform}: ${e}`));
84
+ exitCode = 1;
85
+ }
86
+ }
87
+ process.exit(exitCode);
88
+ });
89
+ }
@@ -0,0 +1,79 @@
1
+ import { Command } from "commander";
2
+ import { listBundledSkills, snapshotSkills, SKILL_PLATFORMS, } from "./registry.js";
3
+ import { fmt } from "../../utils/formatter.js";
4
+ /**
5
+ * `rafter skill list` — show rafter-authored skills available in this CLI and
6
+ * whether each is installed for each supported platform (claude-code, codex,
7
+ * openclaw, cursor).
8
+ *
9
+ * Exit code: 0 on success.
10
+ */
11
+ export function createListCommand() {
12
+ return new Command("list")
13
+ .description("List rafter-authored skills and their install state per platform")
14
+ .option("--json", "Output machine-readable JSON")
15
+ .option("--installed", "Only show (skill, platform) pairs where the skill is installed")
16
+ .option("--platform <platform>", "Limit to one platform")
17
+ .action((opts) => {
18
+ const bundled = listBundledSkills();
19
+ let rows = snapshotSkills();
20
+ const platformFilter = opts.platform;
21
+ if (platformFilter) {
22
+ if (!SKILL_PLATFORMS.includes(platformFilter)) {
23
+ console.error(fmt.error(`Unknown platform: ${platformFilter}. Known: ${SKILL_PLATFORMS.join(", ")}`));
24
+ process.exit(1);
25
+ }
26
+ rows = rows.filter((r) => r.platform === platformFilter);
27
+ }
28
+ if (opts.installed)
29
+ rows = rows.filter((r) => r.installed);
30
+ if (opts.json) {
31
+ const payload = {
32
+ skills: bundled.map((s) => ({
33
+ name: s.name,
34
+ version: s.version,
35
+ description: s.description,
36
+ })),
37
+ installations: rows.map((r) => ({
38
+ name: r.name,
39
+ platform: r.platform,
40
+ path: r.path,
41
+ detected: r.detected,
42
+ installed: r.installed,
43
+ version: r.version,
44
+ })),
45
+ };
46
+ console.log(JSON.stringify(payload, null, 2));
47
+ return;
48
+ }
49
+ console.log(fmt.header("Rafter-authored skills"));
50
+ for (const s of bundled) {
51
+ console.log(` ${s.name.padEnd(24)} v${s.version}`);
52
+ }
53
+ console.log();
54
+ console.log(fmt.header("Installations by platform"));
55
+ const byPlatform = new Map();
56
+ for (const r of rows) {
57
+ const arr = byPlatform.get(r.platform) ?? [];
58
+ arr.push(r);
59
+ byPlatform.set(r.platform, arr);
60
+ }
61
+ for (const [platform, list] of byPlatform) {
62
+ const detected = list[0]?.detected ?? false;
63
+ const suffix = detected ? "" : " (not detected)";
64
+ console.log(`\n${platform}${suffix}`);
65
+ for (const r of list) {
66
+ const label = r.name.padEnd(24);
67
+ if (r.installed) {
68
+ const ver = r.version ? ` v${r.version}` : "";
69
+ console.log(` ${label} ● installed${ver} (${r.path})`);
70
+ }
71
+ else {
72
+ console.log(` ${label} ○ not installed`);
73
+ }
74
+ }
75
+ }
76
+ console.log();
77
+ console.log(fmt.info("Use `rafter skill install <name>` / `rafter skill uninstall <name>` to toggle skills."));
78
+ });
79
+ }