@hegemonart/get-design-done 1.53.0 → 1.54.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 (45) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +41 -0
  4. package/README.md +2 -0
  5. package/SKILL.md +2 -1
  6. package/agents/component-taxonomy-mapper.md +3 -0
  7. package/agents/motion-mapper.md +1 -0
  8. package/agents/token-mapper.md +3 -0
  9. package/dist/claude-code/.claude/skills/new-addendum/SKILL.md +81 -0
  10. package/package.json +1 -1
  11. package/reference/frameworks/astro.md +43 -0
  12. package/reference/frameworks/nextjs.md +44 -0
  13. package/reference/frameworks/remix.md +44 -0
  14. package/reference/frameworks/storybook.md +44 -0
  15. package/reference/frameworks/sveltekit.md +43 -0
  16. package/reference/frameworks/vite-react.md +43 -0
  17. package/reference/interaction.md +1 -0
  18. package/reference/motion/framer-motion.md +45 -0
  19. package/reference/motion/gsap.md +45 -0
  20. package/reference/motion/motion-one.md +44 -0
  21. package/reference/motion/react-spring.md +44 -0
  22. package/reference/motion.md +1 -0
  23. package/reference/registry.json +163 -1
  24. package/reference/registry.schema.json +18 -1
  25. package/reference/skill-graph.md +2 -1
  26. package/reference/systems/chakra.md +44 -0
  27. package/reference/systems/css-modules.md +44 -0
  28. package/reference/systems/mui.md +44 -0
  29. package/reference/systems/radix-themes.md +43 -0
  30. package/reference/systems/shadcn.md +45 -0
  31. package/reference/systems/styled-components.md +44 -0
  32. package/reference/systems/tailwind.md +44 -0
  33. package/reference/systems/vanilla-extract.md +44 -0
  34. package/scripts/lib/detect/stack.cjs +455 -0
  35. package/scripts/lib/detect/stack.d.cts +44 -0
  36. package/scripts/lib/explore-parallel-runner/index.ts +138 -1
  37. package/scripts/lib/explore-parallel-runner/types.ts +27 -0
  38. package/scripts/lib/health-mirror/index.cjs +73 -1
  39. package/scripts/lib/manifest/skills.json +8 -0
  40. package/scripts/lib/mapper-spawn.cjs +257 -0
  41. package/scripts/lib/mapper-spawn.d.cts +60 -0
  42. package/scripts/lib/new-addendum.cjs +204 -0
  43. package/sdk/cli/index.js +1135 -1
  44. package/sdk/mcp/gdd-mcp/server.js +1047 -0
  45. package/skills/new-addendum/SKILL.md +81 -0
@@ -0,0 +1,44 @@
1
+ ---
2
+ name: vanilla-extract
3
+ kind: system
4
+ composes_into: [token-mapper, component-taxonomy-mapper]
5
+ phase: 54
6
+ ---
7
+ <!-- Vendor docs: https://vanilla-extract.style/documentation/theming/. -->
8
+
9
+ # vanilla-extract
10
+
11
+ ## Conventions
12
+
13
+ - Zero-runtime CSS-in-TS: `*.css.ts` files are extracted to static CSS at build time.
14
+ - `createTheme(tokens)` returns `[themeClass, vars]`. `createThemeContract(shape)` declares a typed token CONTRACT (null leaves) that several `createTheme(contract, values)` implement, the canonical light and dark multi-theme pattern.
15
+ - `style({})` defines a class; `styleVariants({})` and `recipe()` produce variant APIs; sprinkles add atomic utilities.
16
+
17
+ ## File patterns
18
+
19
+ - `*.css.ts` such as `theme.css.ts`, `contract.css.ts`, `sprinkles.css.ts`.
20
+ - `@vanilla-extract/vite-plugin` (or webpack) in config.
21
+ - Identify via: @vanilla-extract/css in deps plus a `*.css.ts` file.
22
+
23
+ ## Gotchas
24
+
25
+ - Tokens are the vars from createTheme or contract keys, NOT the literal values in the generated CSS.
26
+ - The contract is the source of truth; each theme mirrors or extends it (model with mirrors or extends edges).
27
+ - styleVariants and recipe are variants. Read the `.css.ts` source, not the generated `.css`.
28
+
29
+ ## Example output
30
+
31
+ ```json
32
+ {
33
+ "schema_version": "52.0",
34
+ "nodes": [
35
+ { "id": "tok.color.brand", "type": "token", "subtype": "color", "name": "vars.color.brand", "summary": "Brand color slot declared in the theme contract.", "complexity": "simple", "tags": ["color", "brand", "theme"] },
36
+ { "id": "tok.space.md", "type": "token", "subtype": "spacing", "name": "vars.space.md", "summary": "Medium spacing scale step.", "complexity": "simple", "tags": ["spacing", "gap"] },
37
+ { "id": "cmp.button", "type": "component", "name": "Button", "summary": "Recipe-driven button consuming contract vars.", "complexity": "moderate", "tags": ["interactive", "atom"] }
38
+ ],
39
+ "edges": [
40
+ { "source": "cmp.button", "target": "tok.color.brand", "type": "uses-token", "direction": "forward", "weight": 0.9 },
41
+ { "source": "cmp.button", "target": "tok.space.md", "type": "uses-token", "direction": "forward", "weight": 0.5 }
42
+ ]
43
+ }
44
+ ```
@@ -0,0 +1,455 @@
1
+ 'use strict';
2
+ // Phase 54 — gdd stack fingerprint. Pure, dep-free. Reads a project root and emits the detected
3
+ // design-system / framework / motion libraries so the mapper-spawn composer (scripts/lib/mapper-spawn.cjs)
4
+ // can pull the matching reference addendums. Reuses the Phase 41 engine's walk() + SKIP_DIRS so the
5
+ // file-signature scans honor the same node_modules/.git/dist/... exclusions and never wander into
6
+ // vendored trees. Never touches the network or any optional dependency (SC#10 network-isolation
7
+ // scan stays clean), and NEVER throws — an absent / malformed package.json simply yields all-null
8
+ // with an `evidence` note explaining why.
9
+ //
10
+ // detectStack(root) -> { ds: string|null, framework: string|null, motion_libs: string[], evidence: {ds?, framework?, motion?[]} }
11
+ //
12
+ // Detection trust (ROADMAP open-q default): ANY presence in `dependencies` OR `devDependencies`
13
+ // counts. Config files + import signatures are secondary probes that promote weaker file-pattern
14
+ // hits. Priority within a category (CONTEXT R1): explicit dep > config file > file-pattern.
15
+
16
+ const fs = require('node:fs');
17
+ const path = require('node:path');
18
+ const { walk, SKIP_DIRS } = require('./engine.cjs');
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // package.json deps reader. Merges dependencies + devDependencies (+ peer/optional
22
+ // for completeness). Never throws: a missing file yields {}, a malformed JSON yields
23
+ // {} with the parse error surfaced to the caller via `readDeps().error`.
24
+ // ---------------------------------------------------------------------------
25
+
26
+ /**
27
+ * Read + merge the dependency maps from a root package.json.
28
+ * @param {string} root project directory (or a path to package.json directly)
29
+ * @returns {{ deps: Record<string,string>, present: boolean, error: string|null }}
30
+ */
31
+ function readDeps(root) {
32
+ const pkgPath = path.basename(String(root || '')) === 'package.json'
33
+ ? root
34
+ : path.join(root || '.', 'package.json');
35
+ let raw;
36
+ try {
37
+ raw = fs.readFileSync(pkgPath, 'utf8');
38
+ } catch {
39
+ return { deps: {}, present: false, error: null }; // absent package.json is a normal case
40
+ }
41
+ let pkg;
42
+ try {
43
+ pkg = JSON.parse(raw);
44
+ } catch (e) {
45
+ return { deps: {}, present: true, error: 'package.json is not valid JSON' + (e && e.message ? `: ${e.message}` : '') };
46
+ }
47
+ if (!pkg || typeof pkg !== 'object') {
48
+ return { deps: {}, present: true, error: 'package.json did not parse to an object' };
49
+ }
50
+ const deps = {};
51
+ for (const field of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
52
+ const m = pkg[field];
53
+ if (m && typeof m === 'object' && !Array.isArray(m)) {
54
+ for (const k of Object.keys(m)) deps[k] = m[k];
55
+ }
56
+ }
57
+ return { deps, present: true, error: null };
58
+ }
59
+
60
+ /** True if `name` (exact) is present in the merged dep map. */
61
+ function hasDep(deps, name) {
62
+ return Object.prototype.hasOwnProperty.call(deps, name);
63
+ }
64
+
65
+ /** True if any dep name starts with `prefix` (scoped families, e.g. '@radix-ui/'). */
66
+ function hasDepPrefix(deps, prefix) {
67
+ for (const k of Object.keys(deps)) {
68
+ if (k === prefix || k.startsWith(prefix)) return true;
69
+ }
70
+ return false;
71
+ }
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // Filesystem probes. All are SKIP_DIRS-aware (top-level config checks bypass the
75
+ // walk; deep pattern scans go through engine.walk so vendored trees are excluded).
76
+ // Bounded: we stop scanning a category once we have a hit (first-match wins).
77
+ // ---------------------------------------------------------------------------
78
+
79
+ /** True if a top-level file matching one of `names` exists directly under root. */
80
+ function hasTopLevelFile(root, names) {
81
+ let entries;
82
+ try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { return false; }
83
+ const set = new Set(entries.filter((e) => e.isFile()).map((e) => e.name));
84
+ for (const n of names) if (set.has(n)) return true;
85
+ return false;
86
+ }
87
+
88
+ /** True if a top-level config file whose basename starts with `stem` + '.' exists (e.g. tailwind.config.*). */
89
+ function hasTopLevelConfig(root, stem) {
90
+ let entries;
91
+ try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { return false; }
92
+ const prefix = stem + '.';
93
+ for (const e of entries) {
94
+ if (e.isFile() && e.name.startsWith(prefix)) return true;
95
+ }
96
+ return false;
97
+ }
98
+
99
+ /** True if a top-level directory `name` exists directly under root (e.g. app/ vs pages/). */
100
+ function hasTopLevelDir(root, name) {
101
+ try {
102
+ const st = fs.statSync(path.join(root, name));
103
+ return st.isDirectory();
104
+ } catch { return false; }
105
+ }
106
+
107
+ /**
108
+ * Scan walkable files for the first whose basename matches `re` (e.g. *.css.ts, *.module.css,
109
+ * *.stories.*). Returns the project-relative path of the first match, or null. Bounded by walk()'s
110
+ * SKIP_DIRS. We additionally cap at the first hit so this stays cheap on large trees.
111
+ */
112
+ function findFileMatching(root, re) {
113
+ let files;
114
+ try { files = walk(root); } catch { return null; }
115
+ for (const abs of files) {
116
+ if (re.test(path.basename(abs))) return relish(root, abs);
117
+ }
118
+ return null;
119
+ }
120
+
121
+ /**
122
+ * Scan walkable file *contents* for the first whose text matches `re` (an import / token signature
123
+ * like `@theme` or `cn(`). Bounded by walk() + a per-file read-failure skip. Returns
124
+ * { file, match } for the first hit or null.
125
+ */
126
+ function findContentMatching(root, re, fileFilter) {
127
+ let files;
128
+ try { files = walk(root); } catch { return null; }
129
+ for (const abs of files) {
130
+ if (fileFilter && !fileFilter(abs)) continue;
131
+ let text;
132
+ try { text = fs.readFileSync(abs, 'utf8'); } catch { continue; }
133
+ const m = re.exec(text);
134
+ if (m) return { file: relish(root, abs), match: m[0] };
135
+ }
136
+ return null;
137
+ }
138
+
139
+ /** project-relative, forward-slashed path (stable across OSes for evidence strings). */
140
+ function relish(root, abs) {
141
+ const rel = path.relative(root, abs);
142
+ return (rel || abs).split(path.sep).join('/');
143
+ }
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // Design-system detection. Priority: explicit dep > config file > file-pattern.
147
+ // Returns the STRONGEST single ds (one winner) + the evidence string for it.
148
+ // The probe order encodes the cross-system priority (tailwind/shadcn first since
149
+ // they are the most common + have the strongest signals; the rest follow).
150
+ // ---------------------------------------------------------------------------
151
+
152
+ const DS_PROBES = [
153
+ {
154
+ id: 'shadcn',
155
+ // shadcn is a tailwind super-set: detect it FIRST so a shadcn project (which also
156
+ // ships tailwind) is labeled shadcn rather than the more generic tailwind.
157
+ detect(root, deps) {
158
+ const hasComponentsJson = hasTopLevelFile(root, ['components.json']);
159
+ // lib/utils.ts `cn(` is shadcn's canonical helper signature.
160
+ const cnHit = hasComponentsJson
161
+ ? null
162
+ : findContentMatching(root, /\bcn\s*\(/, (abs) => /utils\.(t|j)sx?$/.test(abs.split(path.sep).join('/')));
163
+ if (hasComponentsJson) return { ev: 'components.json present (shadcn/ui)' };
164
+ if (cnHit) return { ev: `cn() helper in ${cnHit.file} (shadcn/ui)` };
165
+ return null;
166
+ },
167
+ },
168
+ {
169
+ id: 'tailwind',
170
+ detect(root, deps) {
171
+ if (hasDep(deps, 'tailwindcss')) return { ev: 'tailwindcss in dependencies' };
172
+ if (hasTopLevelConfig(root, 'tailwind.config')) return { ev: 'tailwind.config.* present' };
173
+ const themeHit = findContentMatching(root, /@theme\b/, (abs) => /\.css$/.test(abs));
174
+ if (themeHit) return { ev: `@theme directive in ${themeHit.file} (tailwind v4)` };
175
+ return null;
176
+ },
177
+ },
178
+ {
179
+ id: 'radix-themes',
180
+ detect(root, deps) {
181
+ if (hasDep(deps, '@radix-ui/themes')) return { ev: '@radix-ui/themes in dependencies' };
182
+ return null;
183
+ },
184
+ },
185
+ {
186
+ id: 'mui',
187
+ detect(root, deps) {
188
+ if (hasDep(deps, '@mui/material')) return { ev: '@mui/material in dependencies' };
189
+ return null;
190
+ },
191
+ },
192
+ {
193
+ id: 'chakra',
194
+ detect(root, deps) {
195
+ if (hasDep(deps, '@chakra-ui/react')) return { ev: '@chakra-ui/react in dependencies' };
196
+ return null;
197
+ },
198
+ },
199
+ {
200
+ id: 'vanilla-extract',
201
+ detect(root, deps) {
202
+ if (hasDep(deps, '@vanilla-extract/css')) return { ev: '@vanilla-extract/css in dependencies' };
203
+ const cssTs = findFileMatching(root, /\.css\.ts$/);
204
+ if (cssTs) return { ev: `*.css.ts file ${cssTs} (vanilla-extract)` };
205
+ return null;
206
+ },
207
+ },
208
+ {
209
+ id: 'styled-components',
210
+ detect(root, deps) {
211
+ if (hasDep(deps, 'styled-components')) return { ev: 'styled-components in dependencies' };
212
+ return null;
213
+ },
214
+ },
215
+ {
216
+ id: 'css-modules',
217
+ // Weakest signal (a plain file pattern, no dep). Last so any explicit DS wins over it.
218
+ detect(root, deps) {
219
+ const mod = findFileMatching(root, /\.module\.css$/);
220
+ if (mod) return { ev: `*.module.css file ${mod} (CSS Modules)` };
221
+ return null;
222
+ },
223
+ },
224
+ ];
225
+
226
+ function detectDs(root, deps) {
227
+ for (const probe of DS_PROBES) {
228
+ let res = null;
229
+ try { res = probe.detect(root, deps); } catch { res = null; }
230
+ if (res) return { ds: probe.id, evidence: res.ev };
231
+ }
232
+ return { ds: null, evidence: 'no design-system signal (no known DS dep, config file, or file pattern)' };
233
+ }
234
+
235
+ // ---------------------------------------------------------------------------
236
+ // Framework detection. Single winner. Priority: explicit dep > config file. Next
237
+ // vs Remix vs Vite-React are disambiguated by their unique deps; vite-react is the
238
+ // fallback only when vite+react are present WITHOUT a higher-level framework.
239
+ // ---------------------------------------------------------------------------
240
+
241
+ function detectFramework(root, deps) {
242
+ // Next.js: `next` dep. app/ vs pages/ noted as router-style evidence (does not change the label).
243
+ if (hasDep(deps, 'next')) {
244
+ const router = hasTopLevelDir(root, 'app') ? 'app-router'
245
+ : hasTopLevelDir(root, 'pages') ? 'pages-router'
246
+ : (hasTopLevelDir(root, 'src') && hasTopLevelDir(path.join(root, 'src'), 'app')) ? 'app-router (src/)'
247
+ : 'router undetermined';
248
+ return { framework: 'nextjs', evidence: `next in dependencies (${router})` };
249
+ }
250
+ // Remix: the @remix-run/* family (run/react, run/node, run/dev, ...).
251
+ if (hasDepPrefix(deps, '@remix-run/')) {
252
+ return { framework: 'remix', evidence: '@remix-run/* in dependencies' };
253
+ }
254
+ // Astro.
255
+ if (hasDep(deps, 'astro') || hasTopLevelConfig(root, 'astro.config')) {
256
+ return { framework: 'astro', evidence: hasDep(deps, 'astro') ? 'astro in dependencies' : 'astro.config.* present' };
257
+ }
258
+ // SvelteKit.
259
+ if (hasDep(deps, '@sveltejs/kit') || hasTopLevelConfig(root, 'svelte.config')) {
260
+ return {
261
+ framework: 'sveltekit',
262
+ evidence: hasDep(deps, '@sveltejs/kit') ? '@sveltejs/kit in dependencies' : 'svelte.config.* present',
263
+ };
264
+ }
265
+ // Storybook: the `storybook` package or any @storybook/* addon/framework.
266
+ if (hasDep(deps, 'storybook') || hasDepPrefix(deps, '@storybook/')) {
267
+ return { framework: 'storybook', evidence: 'storybook / @storybook/* in dependencies' };
268
+ }
269
+ // Vite + React, with no higher-level framework above -> the SPA fallback.
270
+ if ((hasDep(deps, 'vite') || hasTopLevelConfig(root, 'vite.config')) && (hasDep(deps, 'react') || hasDep(deps, 'react-dom'))) {
271
+ return { framework: 'vite-react', evidence: 'vite + react in dependencies (no next/remix/astro/sveltekit)' };
272
+ }
273
+ return { framework: null, evidence: 'no framework signal (no next/remix/vite-react/astro/sveltekit/storybook)' };
274
+ }
275
+
276
+ // ---------------------------------------------------------------------------
277
+ // Motion library detection. MULTIPLE allowed (a project can use framer-motion +
278
+ // gsap). All driven by deps. Note: the bare `motion` package is shared by
279
+ // framer-motion (its new name) and motion-one (its umbrella). We attribute a bare
280
+ // `motion` dep to framer-motion (the dominant React usage) and ALSO surface
281
+ // motion-one only when an explicit @motionone/* scope is present, so the two never
282
+ // silently collide.
283
+ // ---------------------------------------------------------------------------
284
+
285
+ const MOTION_PROBES = [
286
+ {
287
+ id: 'framer-motion',
288
+ detect(deps) {
289
+ if (hasDep(deps, 'framer-motion')) return 'framer-motion in dependencies';
290
+ if (hasDep(deps, 'motion')) return 'motion in dependencies (framer-motion v11+)';
291
+ return null;
292
+ },
293
+ },
294
+ {
295
+ id: 'gsap',
296
+ detect(deps) {
297
+ if (hasDep(deps, 'gsap')) return 'gsap in dependencies';
298
+ return null;
299
+ },
300
+ },
301
+ {
302
+ id: 'motion-one',
303
+ detect(deps) {
304
+ if (hasDepPrefix(deps, '@motionone/')) return '@motionone/* in dependencies';
305
+ // Bare `motion` already attributed to framer-motion above; only the scoped
306
+ // @motionone/* packages uniquely identify Motion One.
307
+ return null;
308
+ },
309
+ },
310
+ {
311
+ id: 'react-spring',
312
+ detect(deps) {
313
+ if (hasDep(deps, 'react-spring') || hasDepPrefix(deps, '@react-spring/')) return 'react-spring / @react-spring/* in dependencies';
314
+ return null;
315
+ },
316
+ },
317
+ ];
318
+
319
+ function detectMotion(deps) {
320
+ const libs = [];
321
+ const evidence = [];
322
+ for (const probe of MOTION_PROBES) {
323
+ let ev = null;
324
+ try { ev = probe.detect(deps); } catch { ev = null; }
325
+ if (ev) { libs.push(probe.id); evidence.push(`${probe.id}: ${ev}`); }
326
+ }
327
+ return { motion_libs: libs, evidence };
328
+ }
329
+
330
+ // ---------------------------------------------------------------------------
331
+ // Public API.
332
+ // ---------------------------------------------------------------------------
333
+
334
+ /**
335
+ * Detect the design-system / framework / motion stack of a project.
336
+ * @param {string} root project directory (defaults to cwd)
337
+ * @returns {{ ds: string|null, framework: string|null, motion_libs: string[], evidence: { ds?: string, framework?: string, motion?: string[], note?: string } }}
338
+ */
339
+ function detectStack(root) {
340
+ const dir = root || process.cwd();
341
+ const evidence = {};
342
+
343
+ let exists = false;
344
+ try { exists = fs.existsSync(dir); } catch { exists = false; }
345
+ if (!exists) {
346
+ return {
347
+ ds: null,
348
+ framework: null,
349
+ motion_libs: [],
350
+ evidence: { note: `root path does not exist: ${dir}` },
351
+ };
352
+ }
353
+
354
+ const { deps, present, error } = readDeps(dir);
355
+ if (!present) evidence.note = 'no package.json at root — relying on config-file + file-pattern probes only';
356
+ else if (error) evidence.note = `${error} — relying on config-file + file-pattern probes only`;
357
+
358
+ let ds = null;
359
+ let framework = null;
360
+ let motion_libs = [];
361
+ try {
362
+ const dsr = detectDs(dir, deps);
363
+ ds = dsr.ds;
364
+ evidence.ds = dsr.evidence;
365
+ } catch (e) { evidence.ds = 'ds detection error: ' + (e && e.message ? e.message : String(e)); }
366
+ try {
367
+ const fwr = detectFramework(dir, deps);
368
+ framework = fwr.framework;
369
+ evidence.framework = fwr.evidence;
370
+ } catch (e) { evidence.framework = 'framework detection error: ' + (e && e.message ? e.message : String(e)); }
371
+ try {
372
+ const mr = detectMotion(deps);
373
+ motion_libs = mr.motion_libs;
374
+ evidence.motion = mr.evidence;
375
+ } catch (e) { evidence.motion = ['motion detection error: ' + (e && e.message ? e.message : String(e))]; }
376
+
377
+ return { ds, framework, motion_libs, evidence };
378
+ }
379
+
380
+ // ---------------------------------------------------------------------------
381
+ // CLI. `gdd-detect-stack <root> [--json]` — prints the fingerprint. JSON by default
382
+ // for machine consumption (mapper-spawn / health-mirror); --pretty for a human read.
383
+ // Exit codes: 0 always (detection is non-judgmental — absence is not an error).
384
+ // ---------------------------------------------------------------------------
385
+
386
+ const HELP = `gdd stack detection — fingerprint a project's design-system / framework / motion stack.
387
+
388
+ Usage:
389
+ detect-stack [root] [options]
390
+
391
+ Arguments:
392
+ [root] Project directory to scan (defaults to the current directory).
393
+
394
+ Options:
395
+ --json Machine-readable JSON (default).
396
+ --pretty Pretty-printed human summary.
397
+ -h, --help This help.
398
+
399
+ Always exits 0 — an undetected stack is reported, not an error.`;
400
+
401
+ function parseArgs(argv) {
402
+ const opts = { root: null, json: true, pretty: false, help: false };
403
+ for (let i = 0; i < argv.length; i++) {
404
+ const a = argv[i];
405
+ if (a === '--json') opts.json = true;
406
+ else if (a === '--pretty') { opts.pretty = true; opts.json = false; }
407
+ else if (a === '-h' || a === '--help') opts.help = true;
408
+ else if (!a.startsWith('-') && opts.root === null) opts.root = a;
409
+ }
410
+ return opts;
411
+ }
412
+
413
+ function renderPretty(res) {
414
+ const lines = [];
415
+ lines.push('gdd stack:');
416
+ lines.push(` design-system : ${res.ds || '(none detected)'}`);
417
+ lines.push(` framework : ${res.framework || '(none detected)'}`);
418
+ lines.push(` motion : ${res.motion_libs.length ? res.motion_libs.join(', ') : '(none detected)'}`);
419
+ if (res.evidence && res.evidence.note) lines.push(` note : ${res.evidence.note}`);
420
+ return lines.join('\n');
421
+ }
422
+
423
+ /**
424
+ * @param {string[]} argv process.argv.slice(2)
425
+ * @param {{ cwd?: string, log?: fn, err?: fn }} [io] injectable for tests
426
+ * @returns {number} exit code (always 0 unless --help on no args)
427
+ */
428
+ function main(argv, io) {
429
+ const o = io || {};
430
+ const log = o.log || ((s) => process.stdout.write(s + '\n'));
431
+ const opts = parseArgs(argv);
432
+ if (opts.help) { log(HELP); return 0; }
433
+ const root = opts.root || o.cwd || process.cwd();
434
+ const res = detectStack(root);
435
+ if (opts.pretty) log(renderPretty(res));
436
+ else log(JSON.stringify(res, null, 2));
437
+ return 0;
438
+ }
439
+
440
+ module.exports = {
441
+ detectStack,
442
+ main,
443
+ // internals exported for unit reuse / introspection (kept stable for executors B & F).
444
+ readDeps,
445
+ hasDep,
446
+ hasDepPrefix,
447
+ detectDs,
448
+ detectFramework,
449
+ detectMotion,
450
+ parseArgs,
451
+ HELP,
452
+ SKIP_DIRS,
453
+ };
454
+
455
+ if (require.main === module) process.exit(main(process.argv.slice(2)));
@@ -0,0 +1,44 @@
1
+ // scripts/lib/detect/stack.d.cts — types for detect/stack.cjs (Phase 54 DETECT-01).
2
+
3
+ export interface StackEvidence {
4
+ ds?: string;
5
+ framework?: string;
6
+ motion?: string[];
7
+ note?: string;
8
+ }
9
+
10
+ export interface DetectedStack {
11
+ /** Detected design-system id (tailwind / shadcn / mui / ...), or null. */
12
+ ds: string | null;
13
+ /** Detected framework id (nextjs / remix / vite-react / ...), or null. */
14
+ framework: string | null;
15
+ /** Detected motion library ids (framer-motion / gsap / ...). Possibly empty. */
16
+ motion_libs: string[];
17
+ /** Per-category evidence strings (why each value was chosen). */
18
+ evidence: StackEvidence;
19
+ }
20
+
21
+ export interface ReadDepsResult {
22
+ deps: Record<string, string>;
23
+ present: boolean;
24
+ error: string | null;
25
+ }
26
+
27
+ /**
28
+ * Fingerprint a project's design-system / framework / motion stack. Pure,
29
+ * dependency-free, NEVER throws — an absent/malformed package.json yields
30
+ * all-null with an evidence note.
31
+ * @param root project directory (defaults to process.cwd()).
32
+ */
33
+ export function detectStack(root?: string): DetectedStack;
34
+
35
+ export function readDeps(root: string): ReadDepsResult;
36
+ export function hasDep(deps: Record<string, string>, name: string): boolean;
37
+ export function hasDepPrefix(deps: Record<string, string>, prefix: string): boolean;
38
+ export function detectDs(root: string, deps: Record<string, string>): { ds: string | null; evidence: string };
39
+ export function detectFramework(root: string, deps: Record<string, string>): { framework: string | null; evidence: string };
40
+ export function detectMotion(deps: Record<string, string>): { motion_libs: string[]; evidence: string[] };
41
+ export function main(argv: string[], io?: { cwd?: string; log?: (s: string) => void; err?: (s: string) => void }): number;
42
+ export function parseArgs(argv: string[]): { root: string | null; json: boolean; pretty: boolean; help: boolean };
43
+ export const HELP: string;
44
+ export const SKIP_DIRS: ReadonlySet<string> | string[];