cue-ai 0.5.0 → 0.7.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 (154) hide show
  1. package/README.md +757 -110
  2. package/package.json +5 -5
  3. package/profiles/README.md +12 -12
  4. package/profiles/SCHEMA.md +31 -3
  5. package/profiles/_cache/README.md +1 -1
  6. package/profiles/_types.ts +26 -1
  7. package/profiles/backend/profile.yaml +1 -0
  8. package/profiles/career/profile.yaml +13 -0
  9. package/profiles/core/profile.yaml +76 -9
  10. package/profiles/creative-media/README.md +1 -1
  11. package/profiles/cybersecurity/profile.yaml +779 -756
  12. package/profiles/ecc/profile.yaml +39 -0
  13. package/profiles/event-design/profile.yaml +10 -0
  14. package/profiles/fleet-control/README.md +1 -1
  15. package/profiles/frontend/profile.yaml +14 -0
  16. package/profiles/full/README.md +1 -1
  17. package/profiles/go-api/profile.yaml +1 -0
  18. package/profiles/marketing/profile.yaml +12 -1
  19. package/profiles/predict-everything/profile.yaml +9 -0
  20. package/profiles/rust/profile.yaml +22 -3
  21. package/profiles/rust-cli/profile.yaml +14 -0
  22. package/profiles/rust-core/profile.yaml +35 -0
  23. package/profiles/rust-embedded/profile.yaml +11 -0
  24. package/profiles/rust-ffi/profile.yaml +13 -0
  25. package/profiles/rust-game/profile.yaml +11 -0
  26. package/profiles/rust-wasm/profile.yaml +11 -0
  27. package/profiles/rust-web/profile.yaml +17 -0
  28. package/profiles/schema.json +44 -4
  29. package/profiles/trendradar/profile.yaml +11 -0
  30. package/resources/mcps/README.md +39 -164
  31. package/resources/mcps/configs/claude.sanitized.json +55 -0
  32. package/resources/mcps/configs/claude_runtime.sanitized.json +47 -0
  33. package/resources/skills/README.md +70 -113
  34. package/resources/skills/skills/event-design/wedding-invitations/SKILL.md +43 -0
  35. package/resources/skills/skills/meta/acpx/SKILL.md +78 -0
  36. package/resources/skills/skills/meta/cue-usage/SKILL.md +24 -0
  37. package/resources/skills/skills/meta/profile-fit-monitor/SKILL.md +24 -0
  38. package/resources/skills/skills/predict-everything/mirofish/SKILL.md +75 -0
  39. package/resources/skills/skills/research/trendradar/SKILL.md +88 -0
  40. package/resources/skills/skills/rust/async-tokio/SKILL.md +27 -0
  41. package/resources/skills/skills/rust/axum-api/SKILL.md +38 -0
  42. package/resources/skills/skills/rust/bacon-watch/SKILL.md +24 -0
  43. package/resources/skills/skills/rust/bevy/SKILL.md +43 -0
  44. package/resources/skills/skills/rust/bindgen/SKILL.md +39 -0
  45. package/resources/skills/skills/rust/cargo-audit/SKILL.md +26 -0
  46. package/resources/skills/skills/rust/cargo-basics/SKILL.md +28 -0
  47. package/resources/skills/skills/rust/cargo-chef/SKILL.md +43 -0
  48. package/resources/skills/skills/rust/cargo-edit/SKILL.md +26 -0
  49. package/resources/skills/skills/rust/cargo-expand/SKILL.md +24 -0
  50. package/resources/skills/skills/rust/cargo-flamegraph/SKILL.md +26 -0
  51. package/resources/skills/skills/rust/cargo-fuzz/SKILL.md +34 -0
  52. package/resources/skills/skills/rust/cargo-hack/SKILL.md +26 -0
  53. package/resources/skills/skills/rust/cargo-msrv/SKILL.md +30 -0
  54. package/resources/skills/skills/rust/cargo-mutants/SKILL.md +26 -0
  55. package/resources/skills/skills/rust/cargo-nextest/SKILL.md +24 -0
  56. package/resources/skills/skills/rust/cargo-readme/SKILL.md +36 -0
  57. package/resources/skills/skills/rust/cbindgen/SKILL.md +41 -0
  58. package/resources/skills/skills/rust/chisel-tool/SKILL.md +32 -0
  59. package/resources/skills/skills/rust/clap-cli/SKILL.md +44 -0
  60. package/resources/skills/skills/rust/clippy-and-fmt/SKILL.md +25 -0
  61. package/resources/skills/skills/rust/cross-compile/SKILL.md +26 -0
  62. package/resources/skills/skills/rust/embedded/SKILL.md +33 -0
  63. package/resources/skills/skills/rust/error-handling/SKILL.md +32 -0
  64. package/resources/skills/skills/rust/just-runner/SKILL.md +26 -0
  65. package/resources/skills/skills/rust/mdbook/SKILL.md +25 -0
  66. package/resources/skills/skills/rust/napi-rs/SKILL.md +32 -0
  67. package/resources/skills/skills/rust/no-std/SKILL.md +42 -0
  68. package/resources/skills/skills/rust/property-testing/SKILL.md +35 -0
  69. package/resources/skills/skills/rust/pyo3/SKILL.md +40 -0
  70. package/resources/skills/skills/rust/ratatui-tui/SKILL.md +36 -0
  71. package/resources/skills/skills/rust/release-plz/SKILL.md +27 -0
  72. package/resources/skills/skills/rust/reqwest/SKILL.md +37 -0
  73. package/resources/skills/skills/rust/sccache/SKILL.md +28 -0
  74. package/resources/skills/skills/rust/serde/SKILL.md +30 -0
  75. package/resources/skills/skills/rust/snapshot-testing/SKILL.md +30 -0
  76. package/resources/skills/skills/rust/sqlx-cli/SKILL.md +33 -0
  77. package/resources/skills/skills/rust/tracing/SKILL.md +36 -0
  78. package/resources/skills/skills/rust/typos-spellcheck/SKILL.md +31 -0
  79. package/resources/skills/skills/rust/uniffi/SKILL.md +38 -0
  80. package/resources/skills/skills/rust/wasm-rust/SKILL.md +27 -0
  81. package/resources/skills/skills/security/agentshield/SKILL.md +119 -0
  82. package/src/commands/_index.ts +47 -3
  83. package/src/commands/cli.test.ts +192 -0
  84. package/src/commands/cli.ts +303 -0
  85. package/src/commands/current.ts +1 -1
  86. package/src/commands/debug.test.ts +62 -0
  87. package/src/commands/debug.ts +212 -0
  88. package/src/commands/discover.scoring.test.ts +216 -0
  89. package/src/commands/discover.test.ts +145 -0
  90. package/src/commands/discover.ts +2618 -0
  91. package/src/commands/eval-behavior.test.ts +56 -0
  92. package/src/commands/eval-behavior.ts +189 -0
  93. package/src/commands/eval.test.ts +102 -0
  94. package/src/commands/eval.ts +348 -0
  95. package/src/commands/evolve.ts +291 -0
  96. package/src/commands/failures.test.ts +78 -0
  97. package/src/commands/failures.ts +393 -0
  98. package/src/commands/feedback.ts +219 -0
  99. package/src/commands/init.ts +26 -0
  100. package/src/commands/launch.e2e.test.ts +9 -1
  101. package/src/commands/launch.ts +174 -11
  102. package/src/commands/lint-skill.ts +157 -0
  103. package/src/commands/marketplace.ts +763 -2
  104. package/src/commands/new.ts +1 -1
  105. package/src/commands/optimizer.ts +92 -28
  106. package/src/commands/profile-draft-skill.test.ts +96 -0
  107. package/src/commands/profile-draft-skill.ts +287 -0
  108. package/src/commands/profile-evolve.test.ts +126 -0
  109. package/src/commands/profile-evolve.ts +0 -0
  110. package/src/commands/profile-suggest.ts +223 -0
  111. package/src/commands/profile.ts +41 -0
  112. package/src/commands/quick.ts +2 -17
  113. package/src/commands/scan.ts +2 -2
  114. package/src/commands/score.ts +1 -1
  115. package/src/commands/share.ts +1 -1
  116. package/src/commands/sources.ts +2 -2
  117. package/src/commands/submit-profile.ts +262 -0
  118. package/src/commands/upgrade.ts +1 -1
  119. package/src/commands/use.ts +35 -5
  120. package/src/commands/validate.ts +1 -1
  121. package/src/index.ts +66 -0
  122. package/src/lib/analytics.ts +48 -2
  123. package/src/lib/claude-binary.ts +39 -0
  124. package/src/lib/cli-extractor.ts +77 -0
  125. package/src/lib/cluster-skills.test.ts +268 -0
  126. package/src/lib/cluster-skills.ts +290 -0
  127. package/src/lib/credentials-sync.test.ts +208 -0
  128. package/src/lib/credentials-sync.ts +205 -0
  129. package/src/lib/mcp-materializer.test.ts +1 -1
  130. package/src/lib/persona-playbooks.test.ts +111 -0
  131. package/src/lib/pr-poster.test.ts +243 -0
  132. package/src/lib/pr-poster.ts +285 -0
  133. package/src/lib/pr-throttle.test.ts +148 -0
  134. package/src/lib/pr-throttle.ts +209 -0
  135. package/src/lib/profile-generator.test.ts +1 -1
  136. package/src/lib/profile-generator.ts +2 -2
  137. package/src/lib/profile-linter.test.ts +6 -3
  138. package/src/lib/profile-linter.ts +71 -8
  139. package/src/lib/profile-loader.test.ts +1 -1
  140. package/src/lib/profile-loader.ts +16 -0
  141. package/src/lib/resolver-local.test.ts +1 -1
  142. package/src/lib/resolver-npx.test.ts +76 -1
  143. package/src/lib/resolver-npx.ts +35 -3
  144. package/src/lib/resolver-plugins.test.ts +1 -1
  145. package/src/lib/runtime-materializer.test.ts +191 -7
  146. package/src/lib/runtime-materializer.ts +310 -42
  147. package/src/lib/scan-plugins.test.ts +1 -1
  148. package/src/lib/skill-linter.test.ts +174 -0
  149. package/src/lib/skill-linter.ts +507 -0
  150. package/src/lib/skill-subset.test.ts +95 -0
  151. package/src/lib/skill-subset.ts +166 -0
  152. package/src/lib/star-prompt.ts +1 -1
  153. package/src/lib/uvx-installer.test.ts +229 -0
  154. package/src/lib/uvx-installer.ts +278 -0
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Scorer regression fixtures for `cue discover`.
3
+ *
4
+ * Each case is hand-labeled with a known-good repo (or known dump). We assert
5
+ * on a *range*, not an exact value — the scoring formula is allowed to drift,
6
+ * but real gems must stay well above the default --min-score (3) and dumps
7
+ * must stay at 0.
8
+ *
9
+ * If you tweak the formula in discover.ts and these break: re-rank the
10
+ * fixtures by hand first, then adjust the ranges here.
11
+ */
12
+
13
+ import { describe, expect, test } from "bun:test";
14
+ import { scoreGem, isLikelySpam, type GemRepo } from "./discover";
15
+
16
+ // ---- Fixture helpers ------------------------------------------------------
17
+
18
+ const DAY = 86_400_000;
19
+ const iso = (daysAgo: number) => new Date(Date.now() - daysAgo * DAY).toISOString();
20
+
21
+ function gem(partial: Partial<GemRepo>): GemRepo {
22
+ return {
23
+ full_name: "",
24
+ owner: "",
25
+ name: "",
26
+ description: "",
27
+ stars: 0,
28
+ forks: 0,
29
+ created_at: iso(180),
30
+ pushed_at: iso(7),
31
+ topics: [],
32
+ language: "",
33
+ has_skill_md: false,
34
+ has_claude_dir: false,
35
+ has_mcp_sdk: false,
36
+ gem_score: 0,
37
+ suggested_profiles: [],
38
+ suggested_mcps: [],
39
+ suggested_clis: [],
40
+ quality: 0,
41
+ url: "",
42
+ ...partial,
43
+ };
44
+ }
45
+
46
+ // ---- Real-world labeled fixtures -----------------------------------------
47
+
48
+ const FIXTURES: Array<{ label: string; repo: GemRepo; min: number; max: number; spam?: boolean }> = [
49
+ {
50
+ label: "elementalsouls/Claude-OSINT — 1362★ curated, top-tier gem",
51
+ repo: gem({
52
+ owner: "elementalsouls", name: "Claude-OSINT",
53
+ description: "Two paired Claude skills · 90+ recon modules · 48 secret-regex patterns · 80+ dorks",
54
+ stars: 1362, forks: 120, created_at: iso(365), pushed_at: iso(7),
55
+ topics: ["claude-skill", "mcp-server"], language: "Python",
56
+ has_skill_md: true, has_claude_dir: true,
57
+ }),
58
+ min: 18, max: 30,
59
+ },
60
+ {
61
+ label: "fallow-rs/fallow-skills — 48★ proven small skill repo",
62
+ repo: gem({
63
+ owner: "fallow-rs", name: "fallow-skills",
64
+ description: "Agent skills for fallow, codebase intelligence for JavaScript and TypeScript",
65
+ stars: 48, forks: 6, created_at: iso(60), pushed_at: iso(7),
66
+ topics: ["agent-skill"], language: "Rust",
67
+ has_skill_md: true,
68
+ }),
69
+ min: 10, max: 18,
70
+ },
71
+ {
72
+ label: "Lupynow/math-modeling-skills — 21★ mature & active",
73
+ repo: gem({
74
+ owner: "Lupynow", name: "math-modeling-skills",
75
+ description: "Math modeling competition guidance Skill — covers CUMCM and MCM/ICM",
76
+ stars: 21, forks: 3, created_at: iso(365), pushed_at: iso(30),
77
+ topics: ["claude-skill"], language: "Python",
78
+ has_skill_md: true,
79
+ }),
80
+ min: 9, max: 16,
81
+ },
82
+ {
83
+ label: "Zandereins/hydra — 1★ hydra council, has SKILL.md, fresh",
84
+ repo: gem({
85
+ owner: "Zandereins", name: "hydra",
86
+ description: "Multi-perspective code review council for Claude Code. 3 advisors by default, 10 agents in deep mode",
87
+ stars: 1, forks: 0, created_at: iso(30), pushed_at: iso(7),
88
+ topics: ["claude-skill"], language: "TypeScript",
89
+ has_skill_md: true,
90
+ }),
91
+ min: 8, max: 14,
92
+ },
93
+
94
+ // --- Spam / AI dumps ---
95
+ {
96
+ label: "ditakebede1/personal-knowledge-nexus — AI slop (Library OS opener)",
97
+ repo: gem({
98
+ owner: "ditakebede1", name: "personal-knowledge-nexus",
99
+ description: "Library OS 2026: Auto-Publish Your Deep Book Notes to Your Website",
100
+ stars: 0, forks: 0, created_at: iso(7), pushed_at: iso(7),
101
+ }),
102
+ min: 0, max: 0, spam: true,
103
+ },
104
+ {
105
+ label: "Meizu1330/neural-context-archive — AI slop (year-stamped name)",
106
+ repo: gem({
107
+ owner: "Meizu1330", name: "neural-context-archive-2026",
108
+ description: "GitHub Memory Snapshot 2026 – Auto Recall & Prune Project Context",
109
+ stars: 0, forks: 0, created_at: iso(7), pushed_at: iso(7),
110
+ }),
111
+ min: 0, max: 0, spam: true,
112
+ },
113
+ {
114
+ label: "j28rawat/support-sentinel — AI slop (Production-grade opener)",
115
+ repo: gem({
116
+ owner: "j28rawat", name: "support-sentinel",
117
+ description: "Production-grade multi-agent AI customer support system",
118
+ stars: 0, forks: 0, created_at: iso(5), pushed_at: iso(5),
119
+ }),
120
+ min: 0, max: 0, spam: true,
121
+ },
122
+
123
+ // --- Edge cases ---
124
+ {
125
+ label: "Edge: fresh repo with year-stamped name BUT has SKILL.md → not spam",
126
+ repo: gem({
127
+ owner: "real-author", name: "skill-2026",
128
+ description: "",
129
+ stars: 0, forks: 0, created_at: iso(7), pushed_at: iso(7),
130
+ has_skill_md: true,
131
+ }),
132
+ min: 5, max: 12, spam: false,
133
+ },
134
+ {
135
+ label: "Edge: stale repo (push 500d ago) — penalty applies, low but nonzero",
136
+ repo: gem({
137
+ owner: "abandoned", name: "old-skill",
138
+ description: "An older skill that used to work great — long since unmaintained",
139
+ stars: 5, forks: 1, created_at: iso(700), pushed_at: iso(500),
140
+ has_skill_md: true,
141
+ }),
142
+ min: 2, max: 7,
143
+ },
144
+ {
145
+ label: "Edge: just an MCP SDK user, no SKILL.md — modest signal",
146
+ repo: gem({
147
+ owner: "someone", name: "mcp-tool",
148
+ description: "Tiny MCP server for X — uses the model context protocol SDK",
149
+ stars: 3, forks: 0, created_at: iso(120), pushed_at: iso(20),
150
+ has_mcp_sdk: true,
151
+ }),
152
+ min: 3, max: 8,
153
+ },
154
+ ];
155
+
156
+ // ---- Tests ----------------------------------------------------------------
157
+
158
+ describe("scoreGem — labeled real-world fixtures", () => {
159
+ for (const fx of FIXTURES) {
160
+ test(fx.label, () => {
161
+ const score = scoreGem(fx.repo);
162
+ expect(score).toBeGreaterThanOrEqual(fx.min);
163
+ expect(score).toBeLessThanOrEqual(fx.max);
164
+ if (fx.spam !== undefined) {
165
+ expect(isLikelySpam(fx.repo)).toBe(fx.spam);
166
+ }
167
+ });
168
+ }
169
+ });
170
+
171
+ describe("scoreGem — ordering invariants", () => {
172
+ test("curated 1000★ repo outranks 1★ fresh repo (both have SKILL.md)", () => {
173
+ const curated = FIXTURES.find(f => f.label.startsWith("elementalsouls"))!.repo;
174
+ const fresh = FIXTURES.find(f => f.label.startsWith("Zandereins"))!.repo;
175
+ expect(scoreGem(curated)).toBeGreaterThan(scoreGem(fresh));
176
+ });
177
+
178
+ test("any AI dump scores below the default --min-score=3", () => {
179
+ for (const fx of FIXTURES.filter(f => f.spam === true)) {
180
+ expect(scoreGem(fx.repo)).toBeLessThan(3);
181
+ }
182
+ });
183
+
184
+ test("SKILL.md is the load-bearing signal: adding it lifts an otherwise-borderline repo", () => {
185
+ const without = gem({
186
+ owner: "x", name: "y", description: "Reasonable description of an MCP server here",
187
+ stars: 5, forks: 0, created_at: iso(100), pushed_at: iso(20),
188
+ });
189
+ const withIt = { ...without, has_skill_md: true };
190
+ // Allow tiny fp epsilon; the contract is "SKILL.md contributes the full +5 bonus".
191
+ expect(scoreGem(withIt) - scoreGem(without)).toBeGreaterThan(4.95);
192
+ });
193
+ });
194
+
195
+ describe("isLikelySpam — guard conditions", () => {
196
+ test("does NOT mark spam if has_skill_md is true (load-bearing exemption)", () => {
197
+ expect(isLikelySpam(gem({
198
+ owner: "spammy1", name: "foo-2026", description: "",
199
+ created_at: iso(2), has_skill_md: true,
200
+ }))).toBe(false);
201
+ });
202
+
203
+ test("does NOT mark spam if repo has any engagement (forks ≥ 1)", () => {
204
+ expect(isLikelySpam(gem({
205
+ owner: "spammy1", name: "foo-2026", description: "",
206
+ stars: 0, forks: 1, created_at: iso(2),
207
+ }))).toBe(false);
208
+ });
209
+
210
+ test("does NOT mark spam if repo is older than 14 days", () => {
211
+ expect(isLikelySpam(gem({
212
+ owner: "spammy1", name: "foo-2026", description: "",
213
+ stars: 0, forks: 0, created_at: iso(30),
214
+ }))).toBe(false);
215
+ });
216
+ });
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Tests for `cue discover` export modes. Writes a synthetic gems cache, runs
3
+ * the command against a tmp dir, and asserts on the file shape. No network.
4
+ */
5
+
6
+ import { describe, expect, test, beforeEach, afterEach } from "bun:test";
7
+ import { mkdirSync, writeFileSync, rmSync, existsSync, readFileSync, readdirSync } from "node:fs";
8
+ import { tmpdir, homedir } from "node:os";
9
+ import { join } from "node:path";
10
+
11
+ import { run as discoverRun } from "./discover";
12
+
13
+ let tmp: string;
14
+ let originalXdg: string | undefined;
15
+
16
+ beforeEach(() => {
17
+ tmp = `${tmpdir()}/cue-discover-test-${Date.now()}-${Math.random().toString(36).slice(2)}`;
18
+ mkdirSync(tmp, { recursive: true });
19
+ // Redirect XDG_CONFIG_HOME so the cache lives in tmp and we don't trample the user's real cache.
20
+ originalXdg = process.env.XDG_CONFIG_HOME;
21
+ process.env.XDG_CONFIG_HOME = join(tmp, "config");
22
+
23
+ // Seed a synthetic gem cache the export command will consume.
24
+ const cacheDir = join(tmp, "config", "cue", "discover");
25
+ mkdirSync(cacheDir, { recursive: true });
26
+ writeFileSync(join(cacheDir, "gems.json"), JSON.stringify({
27
+ updated: "2026-05-24T00:00:00.000Z",
28
+ gems: [
29
+ {
30
+ full_name: "octocat/alpha", owner: "octocat", name: "alpha",
31
+ description: "Alpha skill for backend devs.", stars: 12, forks: 1,
32
+ created_at: "2026-01-01", pushed_at: "2026-05-01",
33
+ topics: ["claude-skill"], language: "TypeScript",
34
+ has_skill_md: true, has_claude_dir: false, has_mcp_sdk: false,
35
+ gem_score: 9, suggested_profiles: ["backend"], suggested_mcps: [], suggested_clis: ["node"],
36
+ quality: 8, url: "https://github.com/octocat/alpha",
37
+ },
38
+ {
39
+ full_name: "octocat/beta", owner: "octocat", name: "beta",
40
+ description: "Beta skill for marketing folks.", stars: 4, forks: 0,
41
+ created_at: "2026-02-01", pushed_at: "2026-05-10",
42
+ topics: [], language: "JavaScript",
43
+ has_skill_md: true, has_claude_dir: false, has_mcp_sdk: false,
44
+ gem_score: 6, suggested_profiles: ["marketing", "backend"], suggested_mcps: [], suggested_clis: [],
45
+ quality: 5, url: "https://github.com/octocat/beta",
46
+ },
47
+ ],
48
+ }, null, 2));
49
+ });
50
+
51
+ afterEach(() => {
52
+ if (originalXdg === undefined) delete process.env.XDG_CONFIG_HOME;
53
+ else process.env.XDG_CONFIG_HOME = originalXdg;
54
+ try { rmSync(tmp, { recursive: true, force: true }); } catch {}
55
+ });
56
+
57
+ // Silence stdout in tests
58
+ function silent<T>(fn: () => Promise<T>): Promise<T> {
59
+ const orig = process.stdout.write.bind(process.stdout);
60
+ (process.stdout as any).write = () => true;
61
+ return fn().finally(() => { (process.stdout as any).write = orig; });
62
+ }
63
+
64
+ describe("cue discover --export (legacy single-file)", () => {
65
+ test("writes a single markdown file with both gems grouped by profile", async () => {
66
+ const out = join(tmp, "out.md");
67
+ await silent(() => discoverRun(["--export", out]));
68
+ expect(existsSync(out)).toBe(true);
69
+ const content = readFileSync(out, "utf8");
70
+ expect(content).toMatch(/# 🎯 Discovered Skills/);
71
+ expect(content).toContain("octocat/alpha");
72
+ expect(content).toContain("octocat/beta");
73
+ expect(content).toMatch(/^##\s.*backend/m);
74
+ expect(content).toMatch(/^##\s.*marketing/m);
75
+ });
76
+ });
77
+
78
+ describe("cue discover --export --site (per-profile pages)", () => {
79
+ test("emits index + per-profile pages with frontmatter", async () => {
80
+ const dir = join(tmp, "site");
81
+ await silent(() => discoverRun(["--export", dir, "--site"]));
82
+
83
+ expect(existsSync(join(dir, "index.md"))).toBe(true);
84
+ expect(existsSync(join(dir, "backend.md"))).toBe(true);
85
+ expect(existsSync(join(dir, "marketing.md"))).toBe(true);
86
+
87
+ const indexMd = readFileSync(join(dir, "index.md"), "utf8");
88
+ expect(indexMd).toMatch(/^---\n/); // frontmatter
89
+ expect(indexMd).toMatch(/title: "Discovered/);
90
+ expect(indexMd).toContain("[**backend**](./backend.md)");
91
+ expect(indexMd).toContain("[**marketing**](./marketing.md)");
92
+
93
+ const backendMd = readFileSync(join(dir, "backend.md"), "utf8");
94
+ expect(backendMd).toMatch(/title: "Claude Code Skills for backend"/);
95
+ expect(backendMd).toContain("octocat/alpha");
96
+ expect(backendMd).toContain("octocat/beta"); // beta also fits backend
97
+ expect(backendMd).toContain("cue skills add octocat/alpha --profile backend");
98
+ expect(backendMd).toMatch(/<a id="octocat-alpha"><\/a>/); // stable per-repo anchor
99
+ });
100
+
101
+ test("--site --html also emits .html files with JSON-LD schema", async () => {
102
+ const dir = join(tmp, "site-html");
103
+ await silent(() => discoverRun(["--export", dir, "--site", "--html"]));
104
+
105
+ expect(existsSync(join(dir, "index.html"))).toBe(true);
106
+ expect(existsSync(join(dir, "backend.html"))).toBe(true);
107
+
108
+ const indexHtml = readFileSync(join(dir, "index.html"), "utf8");
109
+ expect(indexHtml).toContain('application/ld+json');
110
+ expect(indexHtml).toContain('"@type": "ItemList"');
111
+ expect(indexHtml).toContain('"@type": "SoftwareApplication"');
112
+ expect(indexHtml).toContain('"name": "octocat/alpha"');
113
+ expect(indexHtml).toContain('property="og:title"'); // social meta
114
+ expect(indexHtml).toContain('rel="canonical"'); // SEO canonical
115
+
116
+ const backendHtml = readFileSync(join(dir, "backend.html"), "utf8");
117
+ expect(backendHtml).toContain('Claude Code Skills for backend');
118
+ expect(backendHtml).toContain('id="octocat-alpha"'); // anchor for deep-links
119
+ });
120
+
121
+ test("total file count matches: 1 index + N profiles (× 2 if html)", async () => {
122
+ const dir = join(tmp, "site-count");
123
+ await silent(() => discoverRun(["--export", dir, "--site", "--html"]));
124
+ const files = readdirSync(dir);
125
+ // 1 (index) + 2 (backend, marketing) = 3, times 2 for html = 6
126
+ expect(files.filter((f) => f.endsWith(".md")).length).toBe(3);
127
+ expect(files.filter((f) => f.endsWith(".html")).length).toBe(3);
128
+ });
129
+ });
130
+
131
+ describe("cue discover --export with missing cache", () => {
132
+ test("exits 1 with helpful error", async () => {
133
+ rmSync(join(tmp, "config", "cue", "discover", "gems.json"));
134
+ const origErr = process.stderr.write.bind(process.stderr);
135
+ let stderr = "";
136
+ (process.stderr as any).write = (c: string | Uint8Array) => { stderr += String(c); return true; };
137
+ try {
138
+ const code = await silent(() => discoverRun(["--export", join(tmp, "out.md")]));
139
+ expect(code).toBe(1);
140
+ expect(stderr).toContain("No cached gems");
141
+ } finally {
142
+ (process.stderr as any).write = origErr;
143
+ }
144
+ });
145
+ });