skillshop 2.0.0 → 2.2.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 (62) hide show
  1. package/README.md +273 -100
  2. package/agents-content/000_communication-writing.block.md +11 -0
  3. package/agents-content/001_external-actions.block.md +9 -0
  4. package/agents-content/AGENTS.md +11 -0
  5. package/package.json +6 -3
  6. package/skills-content/architecture-scanner/SKILL.md +274 -0
  7. package/skills-content/architecture-scanner/files.json +10 -0
  8. package/skills-content/architecture-scanner/scan.sh +823 -0
  9. package/skills-content/change-review/SKILL.md +76 -0
  10. package/skills-content/change-review/agents/openai.yaml +4 -0
  11. package/skills-content/changelog/SKILL.md +99 -0
  12. package/skills-content/changeset/changeset.md +80 -0
  13. package/skills-content/code-buddy/SKILL.md +136 -0
  14. package/skills-content/design-system-catalog/SKILL.md +106 -0
  15. package/skills-content/design-system-catalog/extract-components.mjs +441 -0
  16. package/skills-content/design-system-catalog/generate-catalog.sh +255 -0
  17. package/skills-content/flare/SKILL.md +96 -0
  18. package/skills-content/grilling/SKILL.md +31 -0
  19. package/skills-content/install-sigmap/SKILL.md +97 -0
  20. package/skills-content/one-shot/SKILL.md +51 -0
  21. package/skills-content/release/SKILL.md +128 -0
  22. package/skills-content/shadow-clone/SKILL.md +228 -0
  23. package/skills-content/shadow-clone/extract-signatures.mjs +202 -0
  24. package/skills-content/shadow-clone/scan-repo.mjs +343 -0
  25. package/skills-content/ship/SKILL.md +95 -0
  26. package/skills-content/shots/SKILL.md +89 -0
  27. package/skills-content/shots/agents/openai.yaml +4 -0
  28. package/skills-content/skillshop/SKILL.md +157 -0
  29. package/skills-content/skillz/SKILL.md +15 -0
  30. package/skills-content/transcript-clean/SKILL.md +55 -0
  31. package/skills-content/transcript-edit/SKILL.md +80 -0
  32. package/skills-content/vitest/GENERATION.md +5 -0
  33. package/skills-content/vitest/SKILL.md +52 -0
  34. package/skills-content/vitest/references/advanced-environments.md +264 -0
  35. package/skills-content/vitest/references/advanced-projects.md +300 -0
  36. package/skills-content/vitest/references/advanced-type-testing.md +237 -0
  37. package/skills-content/vitest/references/advanced-vi.md +249 -0
  38. package/skills-content/vitest/references/core-cli.md +166 -0
  39. package/skills-content/vitest/references/core-config.md +174 -0
  40. package/skills-content/vitest/references/core-describe.md +193 -0
  41. package/skills-content/vitest/references/core-expect.md +219 -0
  42. package/skills-content/vitest/references/core-hooks.md +244 -0
  43. package/skills-content/vitest/references/core-test-api.md +233 -0
  44. package/skills-content/vitest/references/features-concurrency.md +250 -0
  45. package/skills-content/vitest/references/features-context.md +238 -0
  46. package/skills-content/vitest/references/features-coverage.md +207 -0
  47. package/skills-content/vitest/references/features-filtering.md +211 -0
  48. package/skills-content/vitest/references/features-mocking.md +265 -0
  49. package/skills-content/vitest/references/features-snapshots.md +207 -0
  50. package/skills-content/workleaf/SKILL.md +77 -0
  51. package/skills-content/worklog/SKILL.md +145 -0
  52. package/skills-content/worklog/references/summary-format.md +114 -0
  53. package/skills-content/worktree/SKILL.md +71 -0
  54. package/src/agents-instructions.js +256 -0
  55. package/src/cli.js +87 -23
  56. package/src/import.js +12 -7
  57. package/src/index.js +1 -0
  58. package/src/legacy-agents-content.js +36 -0
  59. package/src/legacy-cli.js +1822 -0
  60. package/src/library.js +9 -0
  61. package/src/specifier.js +11 -0
  62. package/src/user-install.js +48 -0
@@ -0,0 +1,441 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Universal design system component extractor.
5
+ *
6
+ * Extracts React components from any installed design system package.
7
+ * Primary strategy: parse TypeScript declarations (.d.ts).
8
+ * Secondary: optional runtime import for sub-component enrichment.
9
+ *
10
+ * Usage: node extract-components.mjs <package-name>
11
+ * Output: JSON to stdout
12
+ */
13
+
14
+ import { readFileSync, existsSync } from "fs";
15
+ import { join, dirname, relative, sep } from "path";
16
+ import { createRequire } from "module";
17
+
18
+ // ── Args ────────────────────────────────────────────────────
19
+ const packageName = process.argv[2];
20
+ if (!packageName) {
21
+ process.stderr.write("Usage: node extract-components.mjs <package-name>\n");
22
+ process.exit(1);
23
+ }
24
+
25
+ // ── Resolve package ─────────────────────────────────────────
26
+ const require_ = createRequire(
27
+ join(process.env.REPO_ROOT || process.cwd(), "package.json")
28
+ );
29
+
30
+ let pkgRoot;
31
+ try {
32
+ const p = require_.resolve(`${packageName}/package.json`);
33
+ pkgRoot = dirname(p);
34
+ } catch {
35
+ const fallback = join(
36
+ process.env.REPO_ROOT || process.cwd(),
37
+ "node_modules",
38
+ ...packageName.split("/")
39
+ );
40
+ if (existsSync(join(fallback, "package.json"))) {
41
+ pkgRoot = fallback;
42
+ } else {
43
+ process.stderr.write(`Error: package "${packageName}" not found.\n`);
44
+ process.exit(1);
45
+ }
46
+ }
47
+
48
+ const pkgJson = JSON.parse(
49
+ readFileSync(join(pkgRoot, "package.json"), "utf-8")
50
+ );
51
+ const version = pkgJson.version;
52
+
53
+ // ── Skip lists ──────────────────────────────────────────────
54
+
55
+ const SKIP_NAMES = new Set([
56
+ "Omit", "Pick", "Partial", "Record", "React", "HTMLElement",
57
+ "PropsWithChildren", "ForwardRefExoticComponent", "FCWithSlotMarker",
58
+ "Provider", "Consumer", "Fragment", "Suspense", "StrictMode",
59
+ "Component", "PureComponent", "Element",
60
+ ]);
61
+
62
+ const SKIP_PROPS = new Set([
63
+ "children", "className", "style", "ref", "key", "as",
64
+ "dangerouslySetInnerHTML", "suppressHydrationWarning",
65
+ ]);
66
+
67
+ // ── TypeScript declaration parsing (primary) ────────────────
68
+
69
+ function findTypesEntryPoint() {
70
+ const candidates = [
71
+ pkgJson.types,
72
+ pkgJson.typings,
73
+ ];
74
+
75
+ // Crawl the exports map for type entries
76
+ if (pkgJson.exports) {
77
+ const root = pkgJson.exports["."];
78
+ if (typeof root === "string" && root.endsWith(".d.ts")) {
79
+ candidates.push(root);
80
+ } else if (typeof root === "object" && root !== null) {
81
+ candidates.push(
82
+ root.types,
83
+ root.import?.types,
84
+ root.require?.types,
85
+ root.node?.types,
86
+ root.default?.types
87
+ );
88
+ }
89
+ }
90
+
91
+ // typesVersions (e.g. {"*": {"*": ["dist/types/*"]}})
92
+ if (pkgJson.typesVersions) {
93
+ for (const range of Object.values(pkgJson.typesVersions)) {
94
+ if (range["*"]) {
95
+ for (const pattern of range["*"]) {
96
+ const resolved = pattern.replace("*", "index");
97
+ candidates.push(resolved);
98
+ }
99
+ }
100
+ if (range["."]) {
101
+ for (const pattern of range["."]) {
102
+ candidates.push(pattern.replace("*", "index"));
103
+ }
104
+ }
105
+ }
106
+ }
107
+
108
+ // Common fallback locations
109
+ candidates.push(
110
+ "dist/index.d.ts", "dist/index.d.mts", "dist/index.d.cts",
111
+ "lib/index.d.ts", "index.d.ts", "types/index.d.ts",
112
+ "dist/cjs/index.d.ts", "dist/esm/index.d.ts"
113
+ );
114
+
115
+ for (const c of candidates.filter(Boolean)) {
116
+ const resolved = join(pkgRoot, c);
117
+ if (existsSync(resolved)) return resolved;
118
+ }
119
+
120
+ // Try DefinitelyTyped (@types/*)
121
+ const typesPackage = packageName.startsWith("@")
122
+ ? `@types/${packageName.slice(1).replace("/", "__")}`
123
+ : `@types/${packageName}`;
124
+ try {
125
+ return require_.resolve(`${typesPackage}/index.d.ts`);
126
+ } catch {
127
+ /* not found */
128
+ }
129
+
130
+ return null;
131
+ }
132
+
133
+ function resolveImportPath(fromFile, specifier) {
134
+ const dir = dirname(fromFile);
135
+ for (const ext of [
136
+ ".d.ts", ".d.mts", ".d.cts",
137
+ "/index.d.ts", "/index.d.mts", "/index.d.cts",
138
+ ]) {
139
+ const candidate = join(dir, specifier + ext);
140
+ if (existsSync(candidate)) return candidate;
141
+ }
142
+ return null;
143
+ }
144
+
145
+ /**
146
+ * Walk the export tree starting from entryFile.
147
+ * Returns Map<name, { name, sourceFile }>.
148
+ */
149
+ function parseExports(entryFile, depth = 0, visited = new Set()) {
150
+ if (depth > 8 || visited.has(entryFile) || !existsSync(entryFile))
151
+ return new Map();
152
+ visited.add(entryFile);
153
+
154
+ const content = readFileSync(entryFile, "utf-8");
155
+ const results = new Map();
156
+
157
+ for (const line of content.split("\n")) {
158
+ if (!line.startsWith("export ")) continue;
159
+ if (line.startsWith("export type ")) continue;
160
+
161
+ // export * from './path'
162
+ const starMatch = line.match(/export\s*\*\s*from\s*['"]([^'"]+)['"]/);
163
+ if (starMatch) {
164
+ const resolved = resolveImportPath(entryFile, starMatch[1]);
165
+ if (resolved) {
166
+ for (const [k, v] of parseExports(resolved, depth + 1, visited)) {
167
+ if (!results.has(k)) results.set(k, v);
168
+ }
169
+ }
170
+ continue;
171
+ }
172
+
173
+ // export { Foo, Bar, default as Baz } from './path'
174
+ const namedFrom = line.match(
175
+ /export\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/
176
+ );
177
+ if (namedFrom) {
178
+ const resolved = resolveImportPath(entryFile, namedFrom[2]);
179
+ for (const part of namedFrom[1].split(",").map((s) => s.trim())) {
180
+ if (part.startsWith("type ")) continue;
181
+ let name;
182
+ const asDefault = part.match(/default\s+as\s+(\w+)/);
183
+ const asRename = part.match(/(\w+)\s+as\s+(\w+)/);
184
+ const plain = part.match(/^(\w+)$/);
185
+ if (asDefault) name = asDefault[1];
186
+ else if (asRename) name = asRename[2];
187
+ else if (plain) name = plain[1];
188
+ if (name && /^[A-Z]/.test(name) && !SKIP_NAMES.has(name)) {
189
+ results.set(name, { name, sourceFile: resolved || entryFile });
190
+ }
191
+ }
192
+ continue;
193
+ }
194
+
195
+ // export declare const/function/class Foo
196
+ const decl = line.match(
197
+ /export\s+declare\s+(?:const|function|class)\s+([A-Z]\w+)/
198
+ );
199
+ if (decl && !SKIP_NAMES.has(decl[1])) {
200
+ results.set(decl[1], { name: decl[1], sourceFile: entryFile });
201
+ }
202
+ }
203
+
204
+ return results;
205
+ }
206
+
207
+ /**
208
+ * Extract sub-components from .d.ts content for a compound component.
209
+ * Handles both property declarations and intersection types.
210
+ */
211
+ function extractSubComponentsDts(dtsContent, compName) {
212
+ const subs = [];
213
+ const seen = new Set();
214
+
215
+ // Pattern 1: indented property — ` SubName: ...`
216
+ for (const line of dtsContent.split("\n")) {
217
+ const m = line.match(/^\s{2,8}([A-Z][a-zA-Z]+)\s*:/);
218
+ if (m && !SKIP_NAMES.has(m[1]) && !seen.has(m[1])) {
219
+ seen.add(m[1]);
220
+ subs.push(m[1]);
221
+ }
222
+ }
223
+
224
+ // Pattern 2: intersection — `typeof X & { Cell: ...; Header: ... }`
225
+ const intersectionRe = /&\s*\{([^}]+)\}/g;
226
+ let im;
227
+ while ((im = intersectionRe.exec(dtsContent)) !== null) {
228
+ const block = im[1];
229
+ const propRe = /([A-Z][a-zA-Z]+)\s*:/g;
230
+ let pm;
231
+ while ((pm = propRe.exec(block)) !== null) {
232
+ if (!SKIP_NAMES.has(pm[1]) && !seen.has(pm[1])) {
233
+ seen.add(pm[1]);
234
+ subs.push(pm[1]);
235
+ }
236
+ }
237
+ }
238
+
239
+ return subs;
240
+ }
241
+
242
+ /**
243
+ * Extract props from *Props type/interface definitions.
244
+ */
245
+ function extractPropsDts(dtsContent, compName) {
246
+ const props = [];
247
+ const seen = new Set();
248
+
249
+ // Match type/interface blocks named {CompName}Props or {CompName}BaseProps
250
+ const patterns = [
251
+ new RegExp(
252
+ `(?:export\\s+)?(?:type|interface)\\s+${compName}(?:Base)?Props[^{]*\\{([^}]+(?:\\{[^}]*\\}[^}]*)*)\\}`,
253
+ "gs"
254
+ ),
255
+ ];
256
+
257
+ for (const pattern of patterns) {
258
+ let match;
259
+ while ((match = pattern.exec(dtsContent)) !== null) {
260
+ const block = match[1];
261
+ const propRe =
262
+ /(?:\/\*\*[\s\S]*?\*\/\s*)?['"]?(\w+)['"]?\s*\??\s*:\s*([^;\n]+)/g;
263
+ let pm;
264
+ while ((pm = propRe.exec(block)) !== null) {
265
+ const propName = pm[1];
266
+ let propType = pm[2].trim();
267
+ if (SKIP_PROPS.has(propName)) continue;
268
+ if (propName.startsWith("_")) continue;
269
+ if (seen.has(propName)) continue;
270
+ if (
271
+ /^[A-Z]/.test(propName) &&
272
+ /(?:ForwardRef|FCWith|ExoticComponent|typeof)/.test(propType)
273
+ )
274
+ continue;
275
+
276
+ propType = propType.replace(/import\([^)]+\)\./g, "");
277
+ if (propType.length > 80) propType = propType.substring(0, 77) + "...";
278
+
279
+ seen.add(propName);
280
+ props.push({ name: propName, type: propType });
281
+ }
282
+ }
283
+ }
284
+
285
+ return props;
286
+ }
287
+
288
+ /**
289
+ * Read all relevant .d.ts content for a component.
290
+ */
291
+ function readComponentDts(sourceFile, compName) {
292
+ if (!sourceFile || !existsSync(sourceFile)) return "";
293
+
294
+ const sources = [];
295
+ sources.push(readFileSync(sourceFile, "utf-8"));
296
+
297
+ const dir = dirname(sourceFile);
298
+
299
+ // Also check for types.d.ts in same directory
300
+ for (const extra of ["types.d.ts", `${compName}.d.ts`]) {
301
+ const f = join(dir, extra);
302
+ if (existsSync(f) && f !== sourceFile) {
303
+ sources.push(readFileSync(f, "utf-8"));
304
+ }
305
+ }
306
+
307
+ return sources.join("\n");
308
+ }
309
+
310
+ // ── Auto-categorization ─────────────────────────────────────
311
+
312
+ const CATEGORY_MAP = {
313
+ layout: "Layout", layouts: "Layout", grid: "Layout",
314
+ navigation: "Navigation", nav: "Navigation",
315
+ action: "Actions", actions: "Actions", menu: "Actions",
316
+ menus: "Actions", button: "Actions", buttons: "Actions",
317
+ form: "Forms", forms: "Forms", input: "Forms", inputs: "Forms",
318
+ data: "Data Display", display: "Data Display", "data-display": "Data Display",
319
+ feedback: "Feedback",
320
+ overlay: "Overlays", overlays: "Overlays", modal: "Overlays", dialog: "Overlays",
321
+ content: "Content", typography: "Typography",
322
+ utility: "Utilities", utilities: "Utilities",
323
+ general: "General", common: "General",
324
+ };
325
+
326
+ function categorizeFromPath(sourceFile) {
327
+ if (!sourceFile) return "Components";
328
+
329
+ const rel = relative(pkgRoot, sourceFile);
330
+ const parts = rel
331
+ .split(sep)
332
+ .filter((p) => !["dist", "lib", "src", "cjs", "esm", "types"].includes(p));
333
+
334
+ for (const part of parts) {
335
+ const lower = part.toLowerCase();
336
+ if (CATEGORY_MAP[lower]) return CATEGORY_MAP[lower];
337
+ }
338
+
339
+ if (parts.length >= 2) {
340
+ const parentDir = parts[parts.length - 2].toLowerCase();
341
+ if (CATEGORY_MAP[parentDir]) return CATEGORY_MAP[parentDir];
342
+ }
343
+
344
+ return "Components";
345
+ }
346
+
347
+ // ── Runtime enrichment (optional, best-effort) ──────────────
348
+
349
+ async function tryRuntimeEnrichment(components) {
350
+ try {
351
+ const mod = await import(packageName);
352
+
353
+ for (const [name, comp] of components) {
354
+ const value = mod[name];
355
+ if (!value) continue;
356
+
357
+ // Enrich sub-components from runtime (most reliable for compound components)
358
+ try {
359
+ const runtimeSubs = [];
360
+ for (const [subName, subValue] of Object.entries(value)) {
361
+ if (!/^[A-Z]/.test(subName)) continue;
362
+ if (SKIP_NAMES.has(subName)) continue;
363
+ const isSub =
364
+ typeof subValue === "function" ||
365
+ (typeof subValue === "object" && subValue?.$$typeof != null) ||
366
+ (typeof subValue === "object" &&
367
+ typeof subValue?.render === "function");
368
+ if (isSub) runtimeSubs.push(subName);
369
+ }
370
+
371
+ if (runtimeSubs.length > 0) {
372
+ const existing = new Set(comp.subComponents);
373
+ for (const sub of runtimeSubs) {
374
+ if (!existing.has(sub)) comp.subComponents.push(sub);
375
+ }
376
+ }
377
+ } catch {
378
+ /* non-iterable export */
379
+ }
380
+ }
381
+
382
+ process.stderr.write("Runtime enrichment: success\n");
383
+ } catch {
384
+ process.stderr.write(
385
+ "Runtime enrichment: skipped (import failed, using .d.ts data only)\n"
386
+ );
387
+ }
388
+ }
389
+
390
+ // ── Build component list ────────────────────────────────────
391
+
392
+ const typesEntry = findTypesEntryPoint();
393
+
394
+ if (!typesEntry) {
395
+ process.stderr.write(
396
+ `Error: no TypeScript declarations found for "${packageName}".\n` +
397
+ "Ensure the package ships .d.ts files or install @types/* for it.\n"
398
+ );
399
+ process.exit(1);
400
+ }
401
+
402
+ process.stderr.write(`Types entry: ${relative(pkgRoot, typesEntry)}\n`);
403
+
404
+ const tsExports = parseExports(typesEntry);
405
+
406
+ process.stderr.write(
407
+ `Found ${tsExports.size} PascalCase exports in declarations\n`
408
+ );
409
+
410
+ // Build component map from TypeScript exports
411
+ const components = new Map();
412
+
413
+ for (const [name, info] of tsExports) {
414
+ if (name.startsWith("use") || !/^[A-Z]/.test(name)) continue;
415
+
416
+ const dtsContent = readComponentDts(info.sourceFile, name);
417
+
418
+ components.set(name, {
419
+ name,
420
+ importPath: packageName,
421
+ subComponents: extractSubComponentsDts(dtsContent, name),
422
+ props: extractPropsDts(dtsContent, name),
423
+ category: categorizeFromPath(info.sourceFile),
424
+ });
425
+ }
426
+
427
+ // Try runtime enrichment for sub-components (best-effort)
428
+ await tryRuntimeEnrichment(components);
429
+
430
+ // ── Output ──────────────────────────────────────────────────
431
+
432
+ const output = {
433
+ packageName,
434
+ version,
435
+ totalComponents: components.size,
436
+ components: [...components.values()].sort((a, b) =>
437
+ a.name.localeCompare(b.name)
438
+ ),
439
+ };
440
+
441
+ process.stdout.write(JSON.stringify(output, null, 2) + "\n");
@@ -0,0 +1,255 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # generate-catalog.sh — Generate a component catalog skill for any design system
4
+ #
5
+ # Usage:
6
+ # .agents/skills/design-system-catalog/generate-catalog.sh <package-name> [display-name]
7
+ #
8
+ # Examples:
9
+ # .agents/skills/design-system-catalog/generate-catalog.sh @primer/react "Primer React"
10
+ # .agents/skills/design-system-catalog/generate-catalog.sh @chakra-ui/react "Chakra UI"
11
+ # .agents/skills/design-system-catalog/generate-catalog.sh antd "Ant Design"
12
+
13
+ set -euo pipefail
14
+
15
+ REPO_ROOT="$(git rev-parse --show-toplevel)"
16
+
17
+ # Resolve skills base directory — priority: .agents → .github → .claude
18
+ SKILLS_BASE=""
19
+ for candidate in .agents/skills .github/skills .claude/skills; do
20
+ if [ -d "$REPO_ROOT/$candidate/design-system-catalog" ]; then
21
+ SKILLS_BASE="$candidate"
22
+ break
23
+ fi
24
+ done
25
+
26
+ if [ -z "$SKILLS_BASE" ]; then
27
+ echo "Error: design-system-catalog skill not found in .agents/skills, .github/skills, or .claude/skills" >&2
28
+ exit 1
29
+ fi
30
+
31
+ SKILL_DIR="$REPO_ROOT/$SKILLS_BASE/design-system-catalog"
32
+ EXTRACTOR="$SKILL_DIR/extract-components.mjs"
33
+
34
+ PACKAGE_NAME="${1:?Usage: generate-catalog.sh <package-name> [display-name]}"
35
+ DISPLAY_NAME="${2:-}"
36
+
37
+ # ── Derive names ─────────────────────────────────────────────
38
+
39
+ # Skill name: strip @, replace / with -, lowercase
40
+ SKILL_NAME="${PACKAGE_NAME#@}"
41
+ SKILL_NAME="${SKILL_NAME////-}"
42
+ SKILL_NAME=$(echo "$SKILL_NAME" | tr '[:upper:]' '[:lower:]')
43
+ CATALOG_SKILL_NAME="${SKILL_NAME}-components-catalog"
44
+
45
+ # Display name: auto-derive if not provided
46
+ if [ -z "$DISPLAY_NAME" ]; then
47
+ # @primer/react → Primer React, antd → Antd, @chakra-ui/react → Chakra Ui React
48
+ DISPLAY_NAME=$(echo "$SKILL_NAME" | sed 's/-/ /g' | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) tolower(substr($i,2))}1')
49
+ fi
50
+
51
+ OUTPUT_DIR="$REPO_ROOT/$SKILLS_BASE/$CATALOG_SKILL_NAME"
52
+ OUTPUT_FILE="$OUTPUT_DIR/SKILL.md"
53
+
54
+ echo "Package: $PACKAGE_NAME"
55
+ echo "Display name: $DISPLAY_NAME"
56
+ echo "Skill name: $CATALOG_SKILL_NAME"
57
+ echo "Output dir: $OUTPUT_DIR"
58
+ echo ""
59
+
60
+ # ── Validate ─────────────────────────────────────────────────
61
+
62
+ if [ ! -f "$EXTRACTOR" ]; then
63
+ echo "Error: extractor not found at $EXTRACTOR" >&2
64
+ exit 1
65
+ fi
66
+
67
+ PKG_DIR="$REPO_ROOT/node_modules/${PACKAGE_NAME}"
68
+ if [ ! -d "$PKG_DIR" ]; then
69
+ echo "Error: $PACKAGE_NAME not installed. Run npm install first." >&2
70
+ exit 1
71
+ fi
72
+
73
+ VERSION=$(node -e "console.log(require('$PKG_DIR/package.json').version)" 2>/dev/null || echo "unknown")
74
+
75
+ # ── Extract components ───────────────────────────────────────
76
+
77
+ TMP_DIR=$(mktemp -d)
78
+ trap "rm -rf $TMP_DIR" EXIT
79
+
80
+ echo "Extracting components from $PACKAGE_NAME@$VERSION ..."
81
+ REPO_ROOT="$REPO_ROOT" node "$EXTRACTOR" "$PACKAGE_NAME" > "$TMP_DIR/components.json" 2>"$TMP_DIR/extract.log"
82
+
83
+ cat "$TMP_DIR/extract.log" >&2
84
+
85
+ TOTAL=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$TMP_DIR/components.json','utf-8')).totalComponents)")
86
+
87
+ if [ "$TOTAL" -eq 0 ]; then
88
+ echo "Error: no components found in $PACKAGE_NAME. Check that the package has TypeScript declarations." >&2
89
+ exit 1
90
+ fi
91
+
92
+ echo "Found $TOTAL components."
93
+
94
+ # ── Format catalog markdown ──────────────────────────────────
95
+
96
+ COMPONENTS_JSON="$TMP_DIR/components.json" node --input-type=module << 'FORMATTER' > "$TMP_DIR/catalog.md"
97
+ import { readFileSync } from 'fs';
98
+
99
+ const data = JSON.parse(readFileSync(process.env.COMPONENTS_JSON, 'utf-8'));
100
+ const { packageName, version, components } = data;
101
+
102
+ // Group components by category
103
+ const groups = {};
104
+ for (const comp of components) {
105
+ const cat = comp.category || 'Components';
106
+ if (!groups[cat]) groups[cat] = [];
107
+ groups[cat].push(comp);
108
+ }
109
+
110
+ // Sort categories: known categories first, then alphabetical
111
+ const knownOrder = [
112
+ 'Layout', 'Navigation', 'Actions', 'Actions & Menus', 'Forms',
113
+ 'Data Display', 'Feedback', 'Overlays', 'Content', 'Typography',
114
+ 'Utilities', 'General', 'Components',
115
+ ];
116
+ const allCategories = Object.keys(groups);
117
+ const sorted = [
118
+ ...knownOrder.filter(c => allCategories.includes(c)),
119
+ ...allCategories.filter(c => !knownOrder.includes(c)).sort(),
120
+ ];
121
+
122
+ let md = '';
123
+ md += `## Component Index\n\n`;
124
+
125
+ let totalCount = 0;
126
+
127
+ for (const cat of sorted) {
128
+ const comps = groups[cat];
129
+ if (!comps || comps.length === 0) continue;
130
+
131
+ comps.sort((a, b) => a.name.localeCompare(b.name));
132
+
133
+ md += `### ${cat}\n\n`;
134
+
135
+ for (const comp of comps) {
136
+ totalCount++;
137
+ md += `#### \`${comp.name}\`\n\n`;
138
+ md += `\`\`\`jsx\nimport { ${comp.name} } from '${comp.importPath}'\n\`\`\`\n\n`;
139
+
140
+ if (comp.subComponents.length > 0) {
141
+ md += '**Sub-components:** ';
142
+ md += comp.subComponents.map(s => `\`${comp.name}.${s}\``).join(', ');
143
+ md += '\n\n';
144
+ }
145
+
146
+ if (comp.props.length > 0) {
147
+ md += '**Props:**\n\n';
148
+ md += '| Prop | Type |\n';
149
+ md += '|------|------|\n';
150
+ for (const prop of comp.props) {
151
+ const safeType = prop.type.replace(/\|/g, '\\|').replace(/\n/g, ' ');
152
+ md += `| \`${prop.name}\` | \`${safeType}\` |\n`;
153
+ }
154
+ md += '\n';
155
+ }
156
+ }
157
+ }
158
+
159
+ md += '---\n\n';
160
+ md += `_${totalCount} components total from ${packageName}@${version}._\n`;
161
+
162
+ process.stdout.write(md);
163
+ FORMATTER
164
+
165
+ # ── Create output skill directory ────────────────────────────
166
+
167
+ mkdir -p "$OUTPUT_DIR"
168
+
169
+ # ── Write SKILL.md ───────────────────────────────────────────
170
+
171
+ cat > "$OUTPUT_FILE" << SKILLEOF
172
+ # ${DISPLAY_NAME} Components Catalog
173
+
174
+ > Triggered by: ANY code that imports from \`${PACKAGE_NAME}\`, "what ${DISPLAY_NAME} components are available", "${DISPLAY_NAME,,} component list", "${DISPLAY_NAME,,} components", when building UI with ${DISPLAY_NAME}
175
+
176
+ > Auto-update trigger: When \`package.json\` is modified and \`${PACKAGE_NAME}\` version changes, regenerate this catalog.
177
+
178
+ ## What This Does
179
+
180
+ Provides a complete catalog of all \`${PACKAGE_NAME}\` components available in this codebase. Each entry includes the component name, import path, sub-components (for compound components), and props with types.
181
+
182
+ ## When This Applies
183
+
184
+ - Building any new page or component that uses ${DISPLAY_NAME}
185
+ - Deciding which component to use for a UI pattern
186
+ - Looking up props or sub-components for a specific component
187
+
188
+ ## How to Use
189
+
190
+ 1. **Find the right component** — Browse by category or search alphabetically
191
+ 2. **Check sub-components** — Compound components list their sub-components (e.g. \`Table.Cell\`)
192
+ 3. **Check props** — Each component lists its specific props with types
193
+ 4. **Import** — Use the import path shown (always \`${PACKAGE_NAME}\`)
194
+
195
+ ### Regenerating This Catalog
196
+
197
+ When \`${PACKAGE_NAME}\` is upgraded, regenerate with:
198
+
199
+ \`\`\`bash
200
+ ${SKILLS_BASE}/${CATALOG_SKILL_NAME}/extract-components.sh
201
+ \`\`\`
202
+
203
+ <!-- CATALOG -->
204
+ <!-- Generated from ${PACKAGE_NAME}@${VERSION} — do not edit manually -->
205
+ <!-- Run ${SKILLS_BASE}/${CATALOG_SKILL_NAME}/extract-components.sh to regenerate -->
206
+
207
+ ---
208
+
209
+ SKILLEOF
210
+
211
+ # Append the formatted catalog
212
+ cat "$TMP_DIR/catalog.md" >> "$OUTPUT_FILE"
213
+
214
+ # ── Write regeneration script ────────────────────────────────
215
+
216
+ REGEN_SCRIPT="$OUTPUT_DIR/extract-components.sh"
217
+ cat > "$REGEN_SCRIPT" << 'REGENEOF'
218
+ #!/usr/bin/env bash
219
+ #
220
+ # Regenerate the component catalog for PACKAGE_PLACEHOLDER
221
+ #
222
+ # This is a thin wrapper around the shared design-system-catalog extractor.
223
+ # Run this script when the package is upgraded to refresh the catalog.
224
+
225
+ set -euo pipefail
226
+
227
+ REPO_ROOT="$(git rev-parse --show-toplevel)"
228
+
229
+ GENERATOR=""
230
+ for candidate in .agents/skills .github/skills .claude/skills; do
231
+ if [ -f "$REPO_ROOT/$candidate/design-system-catalog/generate-catalog.sh" ]; then
232
+ GENERATOR="$REPO_ROOT/$candidate/design-system-catalog/generate-catalog.sh"
233
+ break
234
+ fi
235
+ done
236
+
237
+ if [ -z "$GENERATOR" ]; then
238
+ echo "Error: design-system-catalog skill not found in .agents/skills, .github/skills, or .claude/skills" >&2
239
+ echo "The shared extractor is required to regenerate this catalog." >&2
240
+ exit 1
241
+ fi
242
+
243
+ exec "$GENERATOR" "PACKAGE_PLACEHOLDER" "DISPLAY_PLACEHOLDER"
244
+ REGENEOF
245
+
246
+ # Replace placeholders
247
+ sed -i '' "s|PACKAGE_PLACEHOLDER|${PACKAGE_NAME}|g" "$REGEN_SCRIPT"
248
+ sed -i '' "s|DISPLAY_PLACEHOLDER|${DISPLAY_NAME}|g" "$REGEN_SCRIPT"
249
+ chmod +x "$REGEN_SCRIPT"
250
+
251
+ echo ""
252
+ echo "✅ Generated skill: $CATALOG_SKILL_NAME"
253
+ echo " Skill file: $OUTPUT_FILE"
254
+ echo " Regenerate: $REGEN_SCRIPT"
255
+ echo " Components: $TOTAL from ${PACKAGE_NAME}@${VERSION}"