@hegemonart/get-design-done 1.53.0 → 1.55.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 (56) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +88 -0
  4. package/README.md +4 -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/bin/gdd-dashboard +91 -0
  10. package/dist/claude-code/.claude/skills/new-addendum/SKILL.md +81 -0
  11. package/package.json +2 -1
  12. package/reference/frameworks/astro.md +43 -0
  13. package/reference/frameworks/nextjs.md +44 -0
  14. package/reference/frameworks/remix.md +44 -0
  15. package/reference/frameworks/storybook.md +44 -0
  16. package/reference/frameworks/sveltekit.md +43 -0
  17. package/reference/frameworks/vite-react.md +43 -0
  18. package/reference/interaction.md +1 -0
  19. package/reference/motion/framer-motion.md +45 -0
  20. package/reference/motion/gsap.md +45 -0
  21. package/reference/motion/motion-one.md +44 -0
  22. package/reference/motion/react-spring.md +44 -0
  23. package/reference/motion.md +1 -0
  24. package/reference/registry.json +163 -1
  25. package/reference/registry.schema.json +18 -1
  26. package/reference/skill-graph.md +2 -1
  27. package/reference/systems/chakra.md +44 -0
  28. package/reference/systems/css-modules.md +44 -0
  29. package/reference/systems/mui.md +44 -0
  30. package/reference/systems/radix-themes.md +43 -0
  31. package/reference/systems/shadcn.md +45 -0
  32. package/reference/systems/styled-components.md +44 -0
  33. package/reference/systems/tailwind.md +44 -0
  34. package/reference/systems/vanilla-extract.md +44 -0
  35. package/scripts/lib/dashboard/graph-html.cjs +0 -0
  36. package/scripts/lib/detect/stack.cjs +455 -0
  37. package/scripts/lib/detect/stack.d.cts +44 -0
  38. package/scripts/lib/explore-parallel-runner/index.ts +138 -1
  39. package/scripts/lib/explore-parallel-runner/types.ts +27 -0
  40. package/scripts/lib/health-mirror/index.cjs +218 -1
  41. package/scripts/lib/manifest/skills.json +8 -0
  42. package/scripts/lib/mapper-spawn.cjs +257 -0
  43. package/scripts/lib/mapper-spawn.d.cts +60 -0
  44. package/scripts/lib/new-addendum.cjs +204 -0
  45. package/sdk/cli/commands/dashboard.ts +419 -0
  46. package/sdk/cli/index.js +1388 -3
  47. package/sdk/cli/index.ts +7 -0
  48. package/sdk/dashboard/data/_pkg-root.cjs +92 -0
  49. package/sdk/dashboard/data/cost-aggregator.cjs +187 -0
  50. package/sdk/dashboard/data/discovery.cjs +297 -0
  51. package/sdk/dashboard/data/risk-surface.cjs +136 -0
  52. package/sdk/dashboard/data/source.cjs +576 -0
  53. package/sdk/dashboard/tui/ansi.cjs +355 -0
  54. package/sdk/dashboard/tui/index.cjs +778 -0
  55. package/sdk/mcp/gdd-mcp/server.js +1117 -0
  56. package/skills/new-addendum/SKILL.md +81 -0
@@ -0,0 +1,45 @@
1
+ ---
2
+ name: shadcn
3
+ kind: system
4
+ composes_into: [token-mapper, component-taxonomy-mapper]
5
+ phase: 54
6
+ ---
7
+ <!-- Vendor docs: https://ui.shadcn.com/docs/theming. -->
8
+
9
+ # shadcn/ui
10
+
11
+ ## Conventions
12
+
13
+ - Not an npm dependency: components are copied INTO the repo (you own the source), built on Radix primitives plus Tailwind.
14
+ - Color tokens are CSS vars in `globals.css`, semantic and paired: `--background`/`--foreground`, `--primary`, `--muted`, `--border`, `--ring` with `-foreground` partners. Values are HSL channel triplets (newer installs use OKLCH).
15
+ - Variants come from cva with variant and size axes.
16
+
17
+ ## File patterns
18
+
19
+ - `components.json` (style, tailwind.baseColor, cssVariables, aliases).
20
+ - `lib/utils.ts` exporting the cn() helper (clsx plus tailwind-merge).
21
+ - `components/ui/` source files; `app/globals.css` with `:root` and `.dark`.
22
+ - Identify via: components.json plus lib/utils.ts cn().
23
+
24
+ ## Gotchas
25
+
26
+ - Components are first-party source: model relationships to Radix as composes or extends, NOT an external depends-on.
27
+ - Bare channel triplets like `240 10% 4%` are token values; capture them as tokens.
28
+ - cn() is plumbing, not a component node.
29
+
30
+ ## Example output
31
+
32
+ ```json
33
+ {
34
+ "schema_version": "52.0",
35
+ "nodes": [
36
+ { "id": "tok.color.primary", "type": "token", "subtype": "color", "name": "--primary", "summary": "Semantic primary color, HSL channel triplet.", "complexity": "simple", "tags": ["color", "brand", "theme"] },
37
+ { "id": "cmp.button", "type": "component", "name": "Button", "summary": "Owned button source wrapping Radix Slot.", "complexity": "moderate", "tags": ["interactive", "atom"] },
38
+ { "id": "var.button.destructive", "type": "variant", "name": "destructive", "summary": "cva destructive variant of Button.", "complexity": "simple", "tags": ["destructive", "state"] }
39
+ ],
40
+ "edges": [
41
+ { "source": "cmp.button", "target": "tok.color.primary", "type": "uses-token", "direction": "forward", "weight": 0.8 },
42
+ { "source": "var.button.destructive", "target": "cmp.button", "type": "extends", "direction": "forward", "weight": 0.7 }
43
+ ]
44
+ }
45
+ ```
@@ -0,0 +1,44 @@
1
+ ---
2
+ name: styled-components
3
+ kind: system
4
+ composes_into: [token-mapper, component-taxonomy-mapper]
5
+ phase: 54
6
+ ---
7
+ <!-- Vendor docs: https://styled-components.com/docs/advanced#theming. -->
8
+
9
+ # styled-components
10
+
11
+ ## Conventions
12
+
13
+ - Runtime CSS-in-JS via tagged templates: ``styled.div`...` `` and ``styled(Base)`...` ``.
14
+ - Tokens live in a plain JS theme passed through `<ThemeProvider theme={}>` and read by interpolation: `${p => p.theme.colors.primary}`.
15
+ - Variants are prop-conditional interpolation (`${p => p.$variant === 'primary' && css`...`}`); ``css`...` `` composes reusable blocks; transient `$`-props drive styles but are not forwarded to the DOM.
16
+
17
+ ## File patterns
18
+
19
+ - `theme.ts` with the theme object; ThemeProvider at the root.
20
+ - `*.styles.ts` co-located; ``styled.X`...` ``, ``css`...` ``, ``createGlobalStyle`...` ``.
21
+ - Identify via: styled-components in deps plus ThemeProvider.
22
+
23
+ ## Gotchas
24
+
25
+ - There is no token file format: infer subtype from the key path (`theme.colors.*` to color, `theme.space.*` to spacing).
26
+ - Variants are prop-conditional CSS; derive variant and state from the prop union plus `&:hover` and `&:disabled`.
27
+ - v6 adds createGlobalStyle and RSC createTheme emitting CSS vars; transient `$`-props never reach the DOM.
28
+
29
+ ## Example output
30
+
31
+ ```json
32
+ {
33
+ "schema_version": "52.0",
34
+ "nodes": [
35
+ { "id": "tok.color.primary", "type": "token", "subtype": "color", "name": "theme.colors.primary", "summary": "Primary brand color read via theme interpolation.", "complexity": "simple", "tags": ["color", "brand", "theme"] },
36
+ { "id": "cmp.button", "type": "component", "name": "Button", "summary": "Styled button reading theme.colors.", "complexity": "moderate", "tags": ["interactive", "atom"] },
37
+ { "id": "st.button.hover", "type": "state", "name": "hover", "summary": "Hover state via the &:hover selector.", "complexity": "simple", "tags": ["hover", "state"] }
38
+ ],
39
+ "edges": [
40
+ { "source": "cmp.button", "target": "tok.color.primary", "type": "uses-token", "direction": "forward", "weight": 0.9 },
41
+ { "source": "cmp.button", "target": "st.button.hover", "type": "transitions-to", "direction": "forward", "weight": 0.5 }
42
+ ]
43
+ }
44
+ ```
@@ -0,0 +1,44 @@
1
+ ---
2
+ name: tailwind
3
+ kind: system
4
+ composes_into: [token-mapper, component-taxonomy-mapper]
5
+ phase: 54
6
+ ---
7
+ <!-- Vendor docs: https://tailwindcss.com/docs/theme. -->
8
+
9
+ # Tailwind
10
+
11
+ ## Conventions
12
+
13
+ - v4 inverts the model: tokens are namespaced CSS custom properties in an `@theme {}` block in the CSS entry. Tailwind generates utilities from token names.
14
+ - Namespaces map to subtypes: `--color-*` to color (drives bg/text/border), `--spacing-*` to spacing, `--font-*` to typography, `--radius-*` to radius.
15
+ - v3 defines tokens in `tailwind.config.{js,ts}` under `theme` or `theme.extend`.
16
+ - Utility-class clusters on one element are that component's variants.
17
+
18
+ ## File patterns
19
+
20
+ - v4: `app.css` or `globals.css` with `@import "tailwindcss"` plus an `@theme` block. May ship NO js config.
21
+ - v3: `tailwind.config.{js,ts,cjs,mjs}`.
22
+ - Identify via: tailwindcss in deps plus either config file or `@theme`.
23
+
24
+ ## Gotchas
25
+
26
+ - The resolved `--color-*` and `--spacing-*` values are the tokens; utility classes are usage, not token nodes.
27
+ - Arbitrary values `bg-[#1da1f2]` and `p-[3px]` bypass tokens. Flag them as an anti-pattern node, not a token.
28
+ - `@apply` inlines utilities; it is not a token definition.
29
+
30
+ ## Example output
31
+
32
+ ```json
33
+ {
34
+ "schema_version": "52.0",
35
+ "nodes": [
36
+ { "id": "tok.color.primary", "type": "token", "subtype": "color", "name": "--color-primary", "summary": "Brand primary color token from @theme.", "complexity": "simple", "tags": ["color", "brand", "theme"] },
37
+ { "id": "cmp.button", "type": "component", "name": "Button", "summary": "Primary action button styled with utility clusters.", "complexity": "moderate", "tags": ["interactive", "atom"] },
38
+ { "id": "ap.arbitrary-bg", "type": "anti-pattern", "name": "Arbitrary bg-[#1da1f2]", "summary": "Hardcoded color bypasses the token system.", "complexity": "simple", "tags": ["anti-pattern", "color"] }
39
+ ],
40
+ "edges": [
41
+ { "source": "cmp.button", "target": "tok.color.primary", "type": "uses-token", "direction": "forward", "weight": 0.9 }
42
+ ]
43
+ }
44
+ ```
@@ -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)));