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.
- package/README.md +273 -100
- package/agents-content/000_communication-writing.block.md +11 -0
- package/agents-content/001_external-actions.block.md +9 -0
- package/agents-content/AGENTS.md +11 -0
- package/package.json +6 -3
- package/skills-content/architecture-scanner/SKILL.md +274 -0
- package/skills-content/architecture-scanner/files.json +10 -0
- package/skills-content/architecture-scanner/scan.sh +823 -0
- package/skills-content/change-review/SKILL.md +76 -0
- package/skills-content/change-review/agents/openai.yaml +4 -0
- package/skills-content/changelog/SKILL.md +99 -0
- package/skills-content/changeset/changeset.md +80 -0
- package/skills-content/code-buddy/SKILL.md +136 -0
- package/skills-content/design-system-catalog/SKILL.md +106 -0
- package/skills-content/design-system-catalog/extract-components.mjs +441 -0
- package/skills-content/design-system-catalog/generate-catalog.sh +255 -0
- package/skills-content/flare/SKILL.md +96 -0
- package/skills-content/grilling/SKILL.md +31 -0
- package/skills-content/install-sigmap/SKILL.md +97 -0
- package/skills-content/one-shot/SKILL.md +51 -0
- package/skills-content/release/SKILL.md +128 -0
- package/skills-content/shadow-clone/SKILL.md +228 -0
- package/skills-content/shadow-clone/extract-signatures.mjs +202 -0
- package/skills-content/shadow-clone/scan-repo.mjs +343 -0
- package/skills-content/ship/SKILL.md +95 -0
- package/skills-content/shots/SKILL.md +89 -0
- package/skills-content/shots/agents/openai.yaml +4 -0
- package/skills-content/skillshop/SKILL.md +157 -0
- package/skills-content/skillz/SKILL.md +15 -0
- package/skills-content/transcript-clean/SKILL.md +55 -0
- package/skills-content/transcript-edit/SKILL.md +80 -0
- package/skills-content/vitest/GENERATION.md +5 -0
- package/skills-content/vitest/SKILL.md +52 -0
- package/skills-content/vitest/references/advanced-environments.md +264 -0
- package/skills-content/vitest/references/advanced-projects.md +300 -0
- package/skills-content/vitest/references/advanced-type-testing.md +237 -0
- package/skills-content/vitest/references/advanced-vi.md +249 -0
- package/skills-content/vitest/references/core-cli.md +166 -0
- package/skills-content/vitest/references/core-config.md +174 -0
- package/skills-content/vitest/references/core-describe.md +193 -0
- package/skills-content/vitest/references/core-expect.md +219 -0
- package/skills-content/vitest/references/core-hooks.md +244 -0
- package/skills-content/vitest/references/core-test-api.md +233 -0
- package/skills-content/vitest/references/features-concurrency.md +250 -0
- package/skills-content/vitest/references/features-context.md +238 -0
- package/skills-content/vitest/references/features-coverage.md +207 -0
- package/skills-content/vitest/references/features-filtering.md +211 -0
- package/skills-content/vitest/references/features-mocking.md +265 -0
- package/skills-content/vitest/references/features-snapshots.md +207 -0
- package/skills-content/workleaf/SKILL.md +77 -0
- package/skills-content/worklog/SKILL.md +145 -0
- package/skills-content/worklog/references/summary-format.md +114 -0
- package/skills-content/worktree/SKILL.md +71 -0
- package/src/agents-instructions.js +256 -0
- package/src/cli.js +87 -23
- package/src/import.js +12 -7
- package/src/index.js +1 -0
- package/src/legacy-agents-content.js +36 -0
- package/src/legacy-cli.js +1822 -0
- package/src/library.js +9 -0
- package/src/specifier.js +11 -0
- 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}"
|