@sun-asterisk/sungen 2.4.6 → 2.5.1

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 (206) hide show
  1. package/README.md +88 -7
  2. package/dist/cli/commands/add.d.ts.map +1 -1
  3. package/dist/cli/commands/add.js +109 -9
  4. package/dist/cli/commands/add.js.map +1 -1
  5. package/dist/cli/commands/figma.d.ts +11 -0
  6. package/dist/cli/commands/figma.d.ts.map +1 -0
  7. package/dist/cli/commands/figma.js +178 -0
  8. package/dist/cli/commands/figma.js.map +1 -0
  9. package/dist/cli/commands/generate.d.ts.map +1 -1
  10. package/dist/cli/commands/generate.js +2 -0
  11. package/dist/cli/commands/generate.js.map +1 -1
  12. package/dist/cli/index.js +4 -2
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/generators/gherkin-parser/index.d.ts +1 -0
  15. package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
  16. package/dist/generators/gherkin-parser/index.js +3 -0
  17. package/dist/generators/gherkin-parser/index.js.map +1 -1
  18. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +29 -1
  19. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  20. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +21 -1
  21. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
  22. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js +11 -2
  23. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
  24. package/dist/generators/test-generator/adapters/playwright/templates/after-all.hbs +8 -0
  25. package/dist/generators/test-generator/adapters/playwright/templates/after-each.hbs +8 -0
  26. package/dist/generators/test-generator/adapters/playwright/templates/before-all.hbs +8 -0
  27. package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  28. package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +24 -0
  29. package/dist/generators/test-generator/code-generator.d.ts +2 -0
  30. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  31. package/dist/generators/test-generator/code-generator.js +109 -12
  32. package/dist/generators/test-generator/code-generator.js.map +1 -1
  33. package/dist/generators/test-generator/step-mapper.d.ts +1 -0
  34. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  35. package/dist/generators/test-generator/step-mapper.js +1 -1
  36. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  37. package/dist/generators/test-generator/template-engine.d.ts +29 -1
  38. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  39. package/dist/generators/test-generator/template-engine.js +11 -2
  40. package/dist/generators/test-generator/template-engine.js.map +1 -1
  41. package/dist/generators/test-generator/utils/data-resolver.d.ts +11 -2
  42. package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
  43. package/dist/generators/test-generator/utils/data-resolver.js +36 -25
  44. package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
  45. package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts +7 -0
  46. package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts.map +1 -0
  47. package/dist/generators/test-generator/utils/runtime-data-transformer.js +42 -0
  48. package/dist/generators/test-generator/utils/runtime-data-transformer.js.map +1 -0
  49. package/dist/generators/types.d.ts +1 -0
  50. package/dist/generators/types.d.ts.map +1 -1
  51. package/dist/generators/types.js.map +1 -1
  52. package/dist/orchestrator/ai-rules-updater.d.ts.map +1 -1
  53. package/dist/orchestrator/ai-rules-updater.js +2 -0
  54. package/dist/orchestrator/ai-rules-updater.js.map +1 -1
  55. package/dist/orchestrator/figma/figma-scaffolder-helpers.d.ts +33 -0
  56. package/dist/orchestrator/figma/figma-scaffolder-helpers.d.ts.map +1 -0
  57. package/dist/orchestrator/figma/figma-scaffolder-helpers.js +135 -0
  58. package/dist/orchestrator/figma/figma-scaffolder-helpers.js.map +1 -0
  59. package/dist/orchestrator/figma/figma-scaffolder-types.d.ts +25 -0
  60. package/dist/orchestrator/figma/figma-scaffolder-types.d.ts.map +1 -0
  61. package/dist/orchestrator/figma/figma-scaffolder-types.js +7 -0
  62. package/dist/orchestrator/figma/figma-scaffolder-types.js.map +1 -0
  63. package/dist/orchestrator/figma/figma-scaffolder.d.ts +23 -0
  64. package/dist/orchestrator/figma/figma-scaffolder.d.ts.map +1 -0
  65. package/dist/orchestrator/figma/figma-scaffolder.js +212 -0
  66. package/dist/orchestrator/figma/figma-scaffolder.js.map +1 -0
  67. package/dist/orchestrator/figma/node-path-collapser.d.ts +16 -0
  68. package/dist/orchestrator/figma/node-path-collapser.d.ts.map +1 -0
  69. package/dist/orchestrator/figma/node-path-collapser.js +37 -0
  70. package/dist/orchestrator/figma/node-path-collapser.js.map +1 -0
  71. package/dist/orchestrator/figma/spec-figma-renderer.d.ts +44 -0
  72. package/dist/orchestrator/figma/spec-figma-renderer.d.ts.map +1 -0
  73. package/dist/orchestrator/figma/spec-figma-renderer.js +45 -0
  74. package/dist/orchestrator/figma/spec-figma-renderer.js.map +1 -0
  75. package/dist/orchestrator/figma/spec-figma-section-renderers.d.ts +23 -0
  76. package/dist/orchestrator/figma/spec-figma-section-renderers.d.ts.map +1 -0
  77. package/dist/orchestrator/figma/spec-figma-section-renderers.js +47 -0
  78. package/dist/orchestrator/figma/spec-figma-section-renderers.js.map +1 -0
  79. package/dist/orchestrator/project-initializer.d.ts +9 -0
  80. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  81. package/dist/orchestrator/project-initializer.js +74 -10
  82. package/dist/orchestrator/project-initializer.js.map +1 -1
  83. package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +56 -11
  84. package/dist/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +30 -17
  85. package/dist/orchestrator/templates/ai-instructions/claude-cmd-review.md +4 -3
  86. package/dist/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +34 -2
  87. package/dist/orchestrator/templates/ai-instructions/claude-config.md +12 -2
  88. package/dist/orchestrator/templates/ai-instructions/claude-skill-figma-source.md +151 -0
  89. package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +66 -13
  90. package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +93 -23
  91. package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-review.md +2 -0
  92. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +53 -9
  93. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +21 -16
  94. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-review.md +4 -3
  95. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +34 -2
  96. package/dist/orchestrator/templates/ai-instructions/copilot-config.md +12 -2
  97. package/dist/orchestrator/templates/ai-instructions/copilot-skill-figma-source.md +151 -0
  98. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-figma-source.md +151 -0
  99. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +86 -13
  100. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +61 -0
  101. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +105 -28
  102. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-review.md +20 -0
  103. package/dist/orchestrator/templates/specs-base.d.ts +12 -1
  104. package/dist/orchestrator/templates/specs-base.d.ts.map +1 -1
  105. package/dist/orchestrator/templates/specs-base.js +47 -5
  106. package/dist/orchestrator/templates/specs-base.js.map +1 -1
  107. package/dist/orchestrator/templates/specs-base.ts +65 -7
  108. package/dist/orchestrator/templates/specs-test-data.d.ts +14 -0
  109. package/dist/orchestrator/templates/specs-test-data.d.ts.map +1 -0
  110. package/dist/orchestrator/templates/specs-test-data.js +100 -0
  111. package/dist/orchestrator/templates/specs-test-data.js.map +1 -0
  112. package/dist/orchestrator/templates/specs-test-data.ts +66 -0
  113. package/dist/tools/figma/figma-auth.d.ts +36 -0
  114. package/dist/tools/figma/figma-auth.d.ts.map +1 -0
  115. package/dist/tools/figma/figma-auth.js +182 -0
  116. package/dist/tools/figma/figma-auth.js.map +1 -0
  117. package/dist/tools/figma/figma-cache.d.ts +45 -0
  118. package/dist/tools/figma/figma-cache.d.ts.map +1 -0
  119. package/dist/tools/figma/figma-cache.js +191 -0
  120. package/dist/tools/figma/figma-cache.js.map +1 -0
  121. package/dist/tools/figma/figma-client-types.d.ts +112 -0
  122. package/dist/tools/figma/figma-client-types.d.ts.map +1 -0
  123. package/dist/tools/figma/figma-client-types.js +7 -0
  124. package/dist/tools/figma/figma-client-types.js.map +1 -0
  125. package/dist/tools/figma/figma-errors.d.ts +49 -0
  126. package/dist/tools/figma/figma-errors.d.ts.map +1 -0
  127. package/dist/tools/figma/figma-errors.js +105 -0
  128. package/dist/tools/figma/figma-errors.js.map +1 -0
  129. package/dist/tools/figma/figma-image-downloader.d.ts +25 -0
  130. package/dist/tools/figma/figma-image-downloader.d.ts.map +1 -0
  131. package/dist/tools/figma/figma-image-downloader.js +128 -0
  132. package/dist/tools/figma/figma-image-downloader.js.map +1 -0
  133. package/dist/tools/figma/figma-node-filter.d.ts +26 -0
  134. package/dist/tools/figma/figma-node-filter.d.ts.map +1 -0
  135. package/dist/tools/figma/figma-node-filter.js +164 -0
  136. package/dist/tools/figma/figma-node-filter.js.map +1 -0
  137. package/dist/tools/figma/figma-rest-client.d.ts +24 -0
  138. package/dist/tools/figma/figma-rest-client.d.ts.map +1 -0
  139. package/dist/tools/figma/figma-rest-client.js +154 -0
  140. package/dist/tools/figma/figma-rest-client.js.map +1 -0
  141. package/dist/tools/figma/figma-url-parser.d.ts +18 -0
  142. package/dist/tools/figma/figma-url-parser.d.ts.map +1 -0
  143. package/dist/tools/figma/figma-url-parser.js +51 -0
  144. package/dist/tools/figma/figma-url-parser.js.map +1 -0
  145. package/dist/utils/exec-file-no-throw.d.ts +20 -0
  146. package/dist/utils/exec-file-no-throw.d.ts.map +1 -0
  147. package/dist/utils/exec-file-no-throw.js +36 -0
  148. package/dist/utils/exec-file-no-throw.js.map +1 -0
  149. package/package.json +1 -1
  150. package/src/cli/commands/add.ts +80 -9
  151. package/src/cli/commands/figma.ts +162 -0
  152. package/src/cli/commands/generate.ts +2 -0
  153. package/src/cli/index.ts +4 -2
  154. package/src/generators/gherkin-parser/index.ts +4 -0
  155. package/src/generators/test-generator/adapters/adapter-interface.ts +12 -1
  156. package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +14 -2
  157. package/src/generators/test-generator/adapters/playwright/templates/after-all.hbs +8 -0
  158. package/src/generators/test-generator/adapters/playwright/templates/after-each.hbs +8 -0
  159. package/src/generators/test-generator/adapters/playwright/templates/before-all.hbs +8 -0
  160. package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  161. package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +24 -0
  162. package/src/generators/test-generator/code-generator.ts +122 -13
  163. package/src/generators/test-generator/step-mapper.ts +2 -2
  164. package/src/generators/test-generator/template-engine.ts +28 -2
  165. package/src/generators/test-generator/utils/data-resolver.ts +45 -27
  166. package/src/generators/test-generator/utils/runtime-data-transformer.ts +51 -0
  167. package/src/generators/types.ts +1 -0
  168. package/src/orchestrator/ai-rules-updater.ts +2 -0
  169. package/src/orchestrator/figma/figma-scaffolder-helpers.ts +126 -0
  170. package/src/orchestrator/figma/figma-scaffolder-types.ts +26 -0
  171. package/src/orchestrator/figma/figma-scaffolder.ts +209 -0
  172. package/src/orchestrator/figma/node-path-collapser.ts +38 -0
  173. package/src/orchestrator/figma/spec-figma-renderer.ts +80 -0
  174. package/src/orchestrator/figma/spec-figma-section-renderers.ts +46 -0
  175. package/src/orchestrator/project-initializer.ts +84 -10
  176. package/src/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +56 -11
  177. package/src/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +30 -17
  178. package/src/orchestrator/templates/ai-instructions/claude-cmd-review.md +4 -3
  179. package/src/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +34 -2
  180. package/src/orchestrator/templates/ai-instructions/claude-config.md +12 -2
  181. package/src/orchestrator/templates/ai-instructions/claude-skill-figma-source.md +151 -0
  182. package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +66 -13
  183. package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +93 -23
  184. package/src/orchestrator/templates/ai-instructions/claude-skill-tc-review.md +2 -0
  185. package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +53 -9
  186. package/src/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +21 -16
  187. package/src/orchestrator/templates/ai-instructions/copilot-cmd-review.md +4 -3
  188. package/src/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +34 -2
  189. package/src/orchestrator/templates/ai-instructions/copilot-config.md +12 -2
  190. package/src/orchestrator/templates/ai-instructions/copilot-skill-figma-source.md +151 -0
  191. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-figma-source.md +151 -0
  192. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +86 -13
  193. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +61 -0
  194. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +105 -28
  195. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-review.md +20 -0
  196. package/src/orchestrator/templates/specs-base.ts +65 -7
  197. package/src/orchestrator/templates/specs-test-data.ts +66 -0
  198. package/src/tools/figma/figma-auth.ts +161 -0
  199. package/src/tools/figma/figma-cache.ts +184 -0
  200. package/src/tools/figma/figma-client-types.ts +125 -0
  201. package/src/tools/figma/figma-errors.ts +127 -0
  202. package/src/tools/figma/figma-image-downloader.ts +112 -0
  203. package/src/tools/figma/figma-node-filter.ts +198 -0
  204. package/src/tools/figma/figma-rest-client.ts +183 -0
  205. package/src/tools/figma/figma-url-parser.ts +55 -0
  206. package/src/utils/exec-file-no-throw.ts +45 -0
@@ -0,0 +1,209 @@
1
+ /**
2
+ * FigmaScaffolder: orchestrates Figma URL → spec_figma.md + ui/*.png.
3
+ *
4
+ * Flow: parseFigmaUrl → assertSafeToUse → loadPat → cache check
5
+ * → getFileNodes → filterFigmaNode → getImageUrls → downloadImages
6
+ * → renderSpecFigma → write files
7
+ *
8
+ * Idempotent: re-running overwrites spec_figma.md and PNGs.
9
+ * spec.md: written only if absent (never overwritten).
10
+ * Cache: keyed by file+version+node; bypassed when refresh=true.
11
+ *
12
+ * Helpers (image download, name utils, stubs) live in figma-scaffolder-helpers.ts.
13
+ */
14
+
15
+ import * as fs from 'node:fs';
16
+ import * as path from 'node:path';
17
+ import ora from 'ora';
18
+
19
+ import { parseFigmaUrl } from '../../tools/figma/figma-url-parser';
20
+ import { assertSafeToUse, loadPat } from '../../tools/figma/figma-auth';
21
+ import { getFileNodes, getImageUrls } from '../../tools/figma/figma-rest-client';
22
+ import { filterFigmaNode, extractTextLabels, extractVariants } from '../../tools/figma/figma-node-filter';
23
+ import * as FigmaCache from '../../tools/figma/figma-cache';
24
+ import { renderSpecFigma } from './spec-figma-renderer';
25
+ import {
26
+ sanitizeName,
27
+ readGeneratorVersion,
28
+ minimalSpecMd,
29
+ downloadImages,
30
+ } from './figma-scaffolder-helpers';
31
+ import type { FigmaScaffolderOptions, FigmaScaffolderResult } from './figma-scaffolder-types';
32
+
33
+ // Max variant images to download in addition to the main frame.
34
+ const MAX_VARIANT_IMAGES = 5;
35
+
36
+ export class FigmaScaffolder {
37
+ /**
38
+ * Orchestrate the full Figma → screen files flow.
39
+ *
40
+ * @throws Error with remediation instructions on auth / parse / network failure.
41
+ */
42
+ static async run(options: FigmaScaffolderOptions): Promise<FigmaScaffolderResult> {
43
+ const cwd = options.cwd ?? process.cwd();
44
+ const refresh = options.refresh ?? false;
45
+ const scale = options.scale ?? 2;
46
+ const spinner = ora({ stream: process.stderr });
47
+
48
+ // 1. Parse and validate Figma URL
49
+ const ref = parseFigmaUrl(options.figmaUrl);
50
+ if (!ref) {
51
+ throw new Error(
52
+ `Invalid Figma URL: "${options.figmaUrl}"\n` +
53
+ 'Expected format: https://www.figma.com/design/<KEY>/<name>?node-id=1-23',
54
+ );
55
+ }
56
+ if (!ref.nodeId) {
57
+ throw new Error(
58
+ 'Figma URL must include a node-id query parameter.\n' +
59
+ 'Select a specific frame in Figma, right-click → "Copy link to selection".',
60
+ );
61
+ }
62
+
63
+ // 2. Security scan + PAT load
64
+ spinner.start('Checking Figma auth…');
65
+ await assertSafeToUse(cwd);
66
+
67
+ const pat = loadPat(cwd);
68
+ if (!pat) {
69
+ spinner.fail('Figma PAT not found.');
70
+ throw new Error('Figma PAT is not configured.\nRun: sungen figma auth set');
71
+ }
72
+ spinner.succeed('Figma auth OK');
73
+
74
+ // 3. Ensure screen directories exist
75
+ const screenDir = path.join(cwd, 'qa', 'screens', options.screenName);
76
+ const requirementsDir = path.join(screenDir, 'requirements');
77
+ const uiDir = path.join(requirementsDir, 'ui');
78
+ fs.mkdirSync(uiDir, { recursive: true });
79
+
80
+ // 4. Resolve node: prefer raw-JSON cache to spare Starter-tier REST quota.
81
+ // On Figma Starter, `/v1/files/:key/nodes` counts against a tiny daily
82
+ // bucket (observed Retry-After up to 4+ days). If we already have a
83
+ // cached raw response, reuse it and skip the call entirely.
84
+ let versionId: string;
85
+ let nodeDocument: unknown;
86
+ let fromCache = false;
87
+
88
+ if (!refresh) {
89
+ const cachedVersion = FigmaCache.findLatestCachedVersion(cwd, ref.fileKey, ref.nodeId);
90
+ if (cachedVersion) {
91
+ const cachedRaw = FigmaCache.get(cwd, ref.fileKey, cachedVersion, ref.nodeId, 'raw');
92
+ if (cachedRaw) {
93
+ try {
94
+ nodeDocument = JSON.parse(cachedRaw.toString('utf8'));
95
+ versionId = cachedVersion;
96
+ fromCache = true;
97
+ spinner.info(`Reusing cached Figma node (version ${versionId}) — pass --refresh to re-fetch`);
98
+ } catch {
99
+ // Corrupt cache → fall through to API fetch
100
+ }
101
+ }
102
+ }
103
+ }
104
+
105
+ if (!fromCache) {
106
+ spinner.start(`Fetching Figma node ${ref.nodeId}…`);
107
+ let nodesResponse: Awaited<ReturnType<typeof getFileNodes>>;
108
+ try {
109
+ nodesResponse = await getFileNodes(pat, ref.fileKey, [ref.nodeId]);
110
+ } catch (err) {
111
+ spinner.fail('Failed to fetch Figma node.');
112
+ throw err;
113
+ }
114
+
115
+ versionId = nodesResponse.version;
116
+ const nodeEntry = nodesResponse.nodes[ref.nodeId];
117
+ if (!nodeEntry) {
118
+ spinner.fail(`Node ${ref.nodeId} not found in Figma file.`);
119
+ throw new Error(
120
+ `Node "${ref.nodeId}" was not found in file "${ref.fileKey}".\n` +
121
+ 'Check the Figma URL — the node-id may be incorrect.',
122
+ );
123
+ }
124
+ spinner.succeed(`Fetched node: ${nodeEntry.document.name}`);
125
+ nodeDocument = nodeEntry.document;
126
+
127
+ // Persist raw (unfiltered) node JSON for downstream LLM synthesis + future
128
+ // cache-first runs. Re-runs overwrite atomically.
129
+ FigmaCache.put(
130
+ cwd,
131
+ ref.fileKey,
132
+ versionId,
133
+ ref.nodeId,
134
+ 'raw',
135
+ JSON.stringify(nodeDocument),
136
+ );
137
+ }
138
+
139
+ // nodeEntry.document shape — trust the cached bytes we wrote earlier.
140
+ const nodeEntry = { document: nodeDocument as Parameters<typeof filterFigmaNode>[0] };
141
+
142
+ // 5. Filter node tree + extract metadata
143
+ const filteredNode = filterFigmaNode(nodeEntry.document);
144
+ const textLabels = extractTextLabels(filteredNode);
145
+ const variants = extractVariants(filteredNode);
146
+
147
+ // 6. Resolve image node IDs (main + up to MAX_VARIANT_IMAGES variants)
148
+ const imageNodeIds = [ref.nodeId, ...variants.slice(0, MAX_VARIANT_IMAGES).map((v) => v.nodeId)];
149
+ const scaleKind = `${scale}x` as `${number}x`;
150
+
151
+ // 7. Warm image cache (skipped when refresh=true)
152
+ const cachedImages = new Map<string, Buffer>();
153
+ if (!refresh) {
154
+ for (const nodeId of imageNodeIds) {
155
+ const cached = FigmaCache.get(cwd, ref.fileKey, versionId, nodeId, scaleKind);
156
+ if (cached) cachedImages.set(nodeId, cached);
157
+ }
158
+ }
159
+
160
+ // 8. Fetch image URLs for uncached nodes
161
+ const uncachedIds = imageNodeIds.filter((id) => !cachedImages.has(id));
162
+ let imageUrls: Record<string, string | null> = {};
163
+ if (uncachedIds.length > 0) {
164
+ spinner.start(`Fetching image URLs for ${uncachedIds.length} node(s)…`);
165
+ try {
166
+ const urlsResponse = await getImageUrls(pat, ref.fileKey, uncachedIds, { scale, format: 'png' });
167
+ imageUrls = urlsResponse.images;
168
+ } catch (err) {
169
+ spinner.fail('Failed to fetch image URLs.');
170
+ throw err;
171
+ }
172
+ spinner.succeed('Image URLs fetched');
173
+ }
174
+
175
+ // 9. Download (or copy from cache) images
176
+ const frameBaseName = sanitizeName(filteredNode.name);
177
+ const variantNames = variants.slice(0, MAX_VARIANT_IMAGES).map((v) => v.name);
178
+ const imagePaths = await downloadImages({
179
+ cwd, fileKey: ref.fileKey, versionId, uiDir,
180
+ imageNodeIds, mainNodeId: ref.nodeId, variantNames,
181
+ cachedImages, imageUrls, frameBaseName, scaleKind, spinner,
182
+ });
183
+
184
+ // 10. Render and write spec_figma.md (always overwritten)
185
+ spinner.start('Rendering spec_figma.md…');
186
+ const markdown = renderSpecFigma({
187
+ filteredNode, variants, textLabels, imagePaths,
188
+ fileKey: ref.fileKey, nodeId: ref.nodeId, versionId,
189
+ fetchedAt: new Date().toISOString(),
190
+ generatorVersion: readGeneratorVersion(cwd),
191
+ });
192
+ const specFigmaPath = path.join(requirementsDir, 'spec_figma.md');
193
+ fs.writeFileSync(specFigmaPath, markdown, 'utf8');
194
+ spinner.succeed('spec_figma.md written');
195
+
196
+ // 11. Write spec.md stub only if absent
197
+ const specMdPath = path.join(requirementsDir, 'spec.md');
198
+ const specMdCreated = !fs.existsSync(specMdPath);
199
+ if (specMdCreated) {
200
+ fs.writeFileSync(specMdPath, minimalSpecMd(options.screenName), 'utf8');
201
+ spinner.info('spec.md created (stub — fill in your requirements)');
202
+ }
203
+
204
+ // 12. Evict old cache versions (keep last 3)
205
+ FigmaCache.bustOldVersions(cwd, ref.fileKey, versionId);
206
+
207
+ return { specFigmaPath, imagePaths, specMdCreated };
208
+ }
209
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Pure helper: collapse consecutive repeated segments in a Figma node path.
3
+ *
4
+ * Delimiter: " > " (space-angle-space).
5
+ * Collapsed runs use the multiplication sign × (U+00D7).
6
+ *
7
+ * Examples:
8
+ * "A > B > C" → "A > B > C" (unchanged)
9
+ * "A > B > B > C" → "A > B×2 > C"
10
+ * "Container > Container > Container > X" → "Container×3 > X"
11
+ * "A > A > A > A > A > B > C" → "A×5 > B > C"
12
+ * "A" → "A" (single)
13
+ * "" → "" (empty)
14
+ */
15
+ export function collapseNodePath(path: string): string {
16
+ if (!path) return path;
17
+
18
+ const DELIMITER = ' > ';
19
+ const segments = path.split(DELIMITER);
20
+
21
+ const collapsed: string[] = [];
22
+ let i = 0;
23
+
24
+ while (i < segments.length) {
25
+ const current = segments[i];
26
+ let count = 1;
27
+
28
+ // Count consecutive identical segments
29
+ while (i + count < segments.length && segments[i + count] === current) {
30
+ count++;
31
+ }
32
+
33
+ collapsed.push(count > 1 ? `${current}×${count}` : current);
34
+ i += count;
35
+ }
36
+
37
+ return collapsed.join(DELIMITER);
38
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Pure function: filtered Figma node → spec_figma.md envelope string.
3
+ *
4
+ * The envelope is deterministic and stable across runs: frontmatter,
5
+ * auto-gen banner, Frame metadata, Screenshots, and a SYNTHESIS marker.
6
+ * Narrative sections (Purpose, ASCII Layout, Regions, Actions, Form Fields,
7
+ * Data Columns, Navigation) are appended BELOW the marker by the
8
+ * sungen-figma-source skill, which reads the raw cached node JSON.
9
+ *
10
+ * Section renderers live in spec-figma-section-renderers.ts.
11
+ *
12
+ * This module never reads from disk or makes network calls.
13
+ */
14
+
15
+ import type {
16
+ FilteredFigmaNode,
17
+ FigmaTextLabel,
18
+ FigmaVariantDefinition,
19
+ } from '../../tools/figma/figma-client-types';
20
+ import {
21
+ renderFrontmatter,
22
+ renderFrame,
23
+ renderScreenshots,
24
+ SYNTHESIS_MARKER,
25
+ } from './spec-figma-section-renderers';
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Types
29
+ // ---------------------------------------------------------------------------
30
+
31
+ export interface RenderSpecFigmaInput {
32
+ /** Root filtered node (the requested frame / component). */
33
+ filteredNode: FilteredFigmaNode;
34
+ /** Variant definitions from a COMPONENT_SET parent, or [] if none. Kept for compatibility. */
35
+ variants: FigmaVariantDefinition[];
36
+ /** Flat text labels extracted from the node tree. Kept for compatibility. */
37
+ textLabels: FigmaTextLabel[];
38
+ /** Relative paths of downloaded image files (e.g., ["ui/home.png", "ui/home-dark.png"]). */
39
+ imagePaths: string[];
40
+ /** Figma file key. */
41
+ fileKey: string;
42
+ /** Node ID in API form (e.g. "1:23"). */
43
+ nodeId: string;
44
+ /** Figma file version ID at fetch time. */
45
+ versionId: string;
46
+ /** ISO timestamp of when data was fetched. */
47
+ fetchedAt: string;
48
+ /** Generator version string (e.g., "sungen v2.4.5"). */
49
+ generatorVersion: string;
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Public API
54
+ // ---------------------------------------------------------------------------
55
+
56
+ /**
57
+ * Render the deterministic envelope for spec_figma.md.
58
+ * The LLM synthesis step (sungen-figma-source skill) appends the narrative
59
+ * sections AFTER the SYNTHESIS_MARKER comment. Re-synthesis replaces
60
+ * everything from the marker to EOF.
61
+ *
62
+ * Pure function — no I/O side effects.
63
+ */
64
+ export function renderSpecFigma(input: RenderSpecFigmaInput): string {
65
+ const sections = [
66
+ renderFrontmatter(input),
67
+ '',
68
+ '> Envelope is AUTO-GENERATED. Narrative sections below the marker are LLM-synthesized.',
69
+ '> To refine, copy useful parts into ../spec.md (authoritative).',
70
+ '',
71
+ renderFrame(input.filteredNode),
72
+ '',
73
+ renderScreenshots(input.imagePaths),
74
+ '',
75
+ SYNTHESIS_MARKER,
76
+ '',
77
+ ];
78
+
79
+ return sections.join('\n') + '\n';
80
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Section-render helpers for spec-figma-renderer.ts.
3
+ *
4
+ * After the shift to LLM-synthesized narrative sections, the envelope only
5
+ * needs: frontmatter, Frame metadata, Screenshots, and the SYNTHESIS marker.
6
+ * All prose sections (Purpose, ASCII Layout, Regions, Actions, Form Fields,
7
+ * Data Columns, Navigation) are appended below the marker by the
8
+ * sungen-figma-source skill.
9
+ *
10
+ * Pure — no I/O.
11
+ */
12
+
13
+ import type { FilteredFigmaNode } from '../../tools/figma/figma-client-types';
14
+ import type { RenderSpecFigmaInput } from './spec-figma-renderer';
15
+
16
+ /**
17
+ * Sentinel comment that separates the deterministic envelope (above) from
18
+ * the LLM-synthesized narrative (below). Re-synthesis replaces everything
19
+ * from this marker to EOF.
20
+ */
21
+ export const SYNTHESIS_MARKER = '<!-- SYNTHESIS-BELOW -->';
22
+
23
+ export function renderFrontmatter(input: RenderSpecFigmaInput): string {
24
+ return [
25
+ '---',
26
+ 'source: figma',
27
+ `file_key: ${input.fileKey}`,
28
+ `node_id: ${input.nodeId}`,
29
+ `figma_version_id: ${input.versionId}`,
30
+ `fetched_at: ${input.fetchedAt}`,
31
+ `generator: ${input.generatorVersion}`,
32
+ '---',
33
+ ].join('\n');
34
+ }
35
+
36
+ export function renderFrame(node: FilteredFigmaNode): string {
37
+ const bb = node.boundingBox;
38
+ const dims = bb ? `${bb.w}x${bb.h}` : '—';
39
+ return ['## Frame', `- Name: ${node.name}`, `- Dimensions: ${dims}`, `- Type: ${node.type}`].join('\n');
40
+ }
41
+
42
+ export function renderScreenshots(imagePaths: string[]): string {
43
+ if (imagePaths.length === 0) return '## Screenshots\n- (no images downloaded)';
44
+ const items = imagePaths.map((p, i) => `- \`${p}\` — ${i === 0 ? 'default state' : `variant ${i}`}`);
45
+ return ['## Screenshots', ...items].join('\n');
46
+ }
@@ -48,8 +48,9 @@ export class ProjectInitializer {
48
48
  // Create tsconfig.json if doesn't exist
49
49
  this.createTsConfig();
50
50
 
51
- // Create specs/base.ts for shared context
51
+ // Create specs/base.ts for shared context and specs/test-data.ts for runtime data
52
52
  this.createSpecsBase();
53
+ this.createSpecsTestData();
53
54
 
54
55
  // Create/update .gitignore
55
56
  this.updateGitignore();
@@ -383,6 +384,27 @@ export class ProjectInitializer {
383
384
  this.createdItems.push('specs/generated/base.ts');
384
385
  }
385
386
 
387
+ /**
388
+ * Create specs/test-data.ts for runtime YAML loading
389
+ */
390
+ private createSpecsTestData(): void {
391
+ const testDataPath = path.join(this.cwd, 'specs', 'generated', 'test-data.ts');
392
+
393
+ if (fs.existsSync(testDataPath)) {
394
+ this.skippedItems.push('specs/generated/test-data.ts');
395
+ return;
396
+ }
397
+
398
+ const baseDir = path.dirname(testDataPath);
399
+ if (!fs.existsSync(baseDir)) {
400
+ fs.mkdirSync(baseDir, { recursive: true });
401
+ }
402
+
403
+ const content = this.readTemplate('specs-test-data.ts');
404
+ fs.writeFileSync(testDataPath, content, 'utf-8');
405
+ this.createdItems.push('specs/generated/test-data.ts');
406
+ }
407
+
386
408
  /**
387
409
  * Read a template file from the templates directory
388
410
  */
@@ -405,26 +427,78 @@ export class ProjectInitializer {
405
427
  this.createdItems.push('package.json');
406
428
  }
407
429
 
408
- // Check if @playwright/test is already installed
430
+ // Ensure standard scripts exist in package.json
431
+ this.ensurePackageScripts(packageJsonPath);
432
+
433
+ // Check which dependencies are missing
434
+ const requiredDeps = ['@playwright/test', '@types/node', 'yaml'];
435
+ let missingDeps: string[] = requiredDeps;
409
436
  try {
410
437
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
411
438
  const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
412
- if (deps['@playwright/test']) {
413
- console.log('✓ @playwright/test already installed\n');
414
- return;
415
- }
439
+ missingDeps = requiredDeps.filter(d => !deps[d]);
416
440
  } catch {
417
- // package.json just created, proceed with install
441
+ // package.json just created, install all
442
+ }
443
+
444
+ if (missingDeps.length === 0) {
445
+ console.log('✓ All dependencies already installed\n');
446
+ return;
418
447
  }
419
448
 
420
- // Install Playwright and TypeScript types
421
- console.log('📦 Installing @playwright/test and @types/node...\n');
422
- execSync('npm install -D @playwright/test @types/node', execOpts);
449
+ console.log(`📦 Installing ${missingDeps.join(', ')}...\n`);
450
+ execSync(`npm install -D ${missingDeps.join(' ')}`, execOpts);
423
451
 
424
452
  console.log('\n🎭 Installing Playwright browsers...\n');
425
453
  execSync('npx playwright install', execOpts);
426
454
  }
427
455
 
456
+ /**
457
+ * Ensure package.json has standard Playwright + Sungen scripts.
458
+ * Only adds missing scripts — never overwrites user customizations.
459
+ */
460
+ private ensurePackageScripts(packageJsonPath: string): void {
461
+ let packageJson: Record<string, any>;
462
+ try {
463
+ packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
464
+ } catch {
465
+ return;
466
+ }
467
+
468
+ if (!packageJson.scripts || typeof packageJson.scripts !== 'object') {
469
+ packageJson.scripts = {};
470
+ }
471
+
472
+ const standardScripts: Record<string, string> = {
473
+ 'test': 'playwright test specs/generated/',
474
+ 'test:headed': 'playwright test specs/generated/ --headed',
475
+ 'test:debug': 'playwright test specs/generated/ --debug',
476
+ 'test:ui': 'playwright test specs/generated/ --ui',
477
+ 'report': 'playwright show-report',
478
+ 'generate': 'sungen generate --all',
479
+ 'install:browsers': 'npx playwright install chromium',
480
+ };
481
+
482
+ let added = 0;
483
+ for (const [name, command] of Object.entries(standardScripts)) {
484
+ // Skip if user already has this script (don't overwrite)
485
+ // Exception: overwrite the npm init default test script
486
+ const existing = packageJson.scripts[name];
487
+ if (existing && existing !== 'echo "Error: no test specified" && exit 1') {
488
+ continue;
489
+ }
490
+ packageJson.scripts[name] = command;
491
+ added++;
492
+ }
493
+
494
+ if (added > 0) {
495
+ // Also mark as private (test projects should not be published)
496
+ packageJson.private = true;
497
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf-8');
498
+ console.log(`📝 Added ${added} standard script(s) to package.json\n`);
499
+ }
500
+ }
501
+
428
502
  /**
429
503
  * Get Playwright configuration template
430
504
  */
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: add-screen
3
3
  description: 'Add a new Sungen screen — scaffolds directories, helps fill spec.md, and can capture visuals from Figma (pre-launch) or live page via the capture skills'
4
- argument-hint: [screen-name] [url-path]
4
+ argument-hint: [screen-name] [url-path] [--figma <url>]
5
5
  allowed-tools: Read, Grep, Bash, Glob, Edit, Write, AskUserQuestion, mcp__playwright__browser_navigate, mcp__playwright__browser_take_screenshot, mcp__playwright__browser_snapshot, mcp__figma__get_design_context, mcp__figma__get_variable_defs, mcp__figma__get_screenshot
6
6
  ---
7
7
 
@@ -11,37 +11,82 @@ You are adding a new Sungen screen for test generation.
11
11
 
12
12
  Parse from `$ARGUMENTS`:
13
13
  - **screen** — screen name (e.g., `login`, `dashboard`, `settings`)
14
- - **path** — URL path (e.g., `/login`, `/dashboard`, `/settings`)
14
+ - **path** — URL path (e.g., `/login`, `/dashboard`, `/settings`) - optional when `--figma` is provided
15
+ - **--figma \<url\>** — Figma share URL (optional)
16
+ - **--refresh** — bypass Figma cache and re-fetch (optional, use with `--figma`)
17
+ - **--scale \<n\>** — PNG export scale factor, default 2 (optional)
18
+ - **--hi-res** — export at 4× scale, shorthand for `--scale 4` (optional)
15
19
 
16
20
  If **screen** is missing, ask: "What is the screen name? (e.g., `login`, `dashboard`)"
17
- If **path** is missing, ask: "What is the URL path? (e.g., `/login`, `/dashboard`)"
21
+ If **path** is missing and `--figma` was NOT provided, ask: "What is the URL path? (e.g., `/login`, `/dashboard`)"
18
22
 
19
23
  ## Steps
20
24
 
21
25
  ### 1. Scaffold the screen
22
26
 
23
- Run:
27
+ **Standard path (no `--figma`):**
24
28
  ```bash
25
29
  sungen add --screen <screen> --path <path>
26
30
  ```
27
31
 
28
- ### 2. Fill spec.md
32
+ **Figma branch (when `--figma <url>` is in `$ARGUMENTS`):**
29
33
 
30
- Use `AskUserQuestion`: *"Fill `spec.md` now?"* offer **Yes, fill now (Recommended)** / **Skip, fill later**.
34
+ Invoke the `sungen-figma-source` skill by running:
35
+ ```bash
36
+ sungen add --screen <screen> --figma '<url>' [--path <path>] [--refresh] [--scale <n>]
37
+ # Single-quote the URL — Figma links contain `&` which bash otherwise treats as a background operator.
38
+ ```
39
+
40
+ This CLI command automatically:
41
+ 1. Parses the Figma URL and fetches the design node.
42
+ 2. Downloads frame + variant PNGs to `qa/screens/<screen>/requirements/ui/`.
43
+ 3. Writes `qa/screens/<screen>/requirements/spec_figma.md` (auto-generated, always overwritten on re-run).
44
+ 4. Creates `qa/screens/<screen>/requirements/spec.md` stub **only if the file does not already exist** — never overwrites existing human content.
45
+
46
+ **Important coexistence rules:**
47
+ - `--figma` and `--path` can be passed together: `--path` provides the live URL for `run-test`; `--figma` provides design context.
48
+ - `spec.md` is the human-authored source of truth. Never modify it automatically.
49
+ - `spec_figma.md` is auto-generated — always overwritten; tell the user to copy useful sections into `spec.md`.
50
+ - `--capture` (Playwright screenshot) and `--figma` can be used simultaneously; they produce different outputs.
51
+
52
+ **If Figma auth is missing**, the command will fail with a message like:
53
+ > Figma PAT is not configured. Run: sungen figma auth set
54
+
55
+ In that case, guide the user to run `sungen figma auth set` and retry.
31
56
 
32
- If yes → open `qa/screens/<screen>/requirements/spec.md` and help the user fill sections, fields, validation rules, business rules, and states. Especially prompt for the optional **Figma URL** and **Live URL** fields in Overview — those unlock auto-capture without re-asking next run.
57
+ ### 1a. Synthesize narrative sections (Figma branch only)
33
58
 
34
- ### 3. Capture visual source
59
+ After `sungen add --figma` succeeds, the envelope of `spec_figma.md` is deterministic but the narrative below the `<!-- SYNTHESIS-BELOW -->` marker is empty. Invoke the `sungen-figma-source` skill to fill it:
35
60
 
36
- Use `AskUserQuestion`: *"Capture a visual reference for this screen?"*always offer all three so pre-launch projects work:
61
+ 1. Read `qa/screens/<screen>/requirements/spec_figma.md`note `file_key`, `node_id`, `figma_version_id` from frontmatter.
62
+ 2. Read the cached raw node JSON at `.sungen/figma-cache/<file_key>/<figma_version_id>/<safe_node_id>-raw.json` (colons in node_id become underscores).
63
+ 3. Follow the skill's 7-section template (Purpose / ASCII Layout / Regions / Actions / Form Fields / Data Columns / Navigation) and **replace** everything from the marker to EOF.
64
+ 4. Preserve the envelope above the marker byte-for-byte.
37
65
 
66
+ **Review gate.** Before moving on, show the user the synthesized narrative and use `AskUserQuestion`:
67
+
68
+ - **Approve** — narrative looks right, continue to Step 2
69
+ - **Edit** — user will tweak `spec_figma.md` now; wait for confirmation before continuing
70
+ - **Cancel** — abort; advise the user that `spec.md` was NOT modified and they can re-run `sungen add --figma --refresh` later
71
+
72
+ ### 2. Capture visual source
73
+
74
+ **If Figma branch (Step 1) already downloaded PNGs** → visuals already exist. Use `AskUserQuestion` to offer:
75
+ - **Continue** — Figma visuals are enough (Recommended)
76
+ - **Also capture live page** — supplement Figma with real page scan (invoke `sungen-capture-live` skill)
77
+
78
+ **If standard path (no `--figma`)** → go straight to source selection. Use `AskUserQuestion`: *"Pick a visual source for this screen:"*
38
79
  - **Figma design** (Recommended for pre-launch) — invoke `sungen-capture-figma` skill
39
80
  - **Live page scan** (dev/staging is up) — invoke `sungen-capture-live` skill
40
- - **Skip** — user will drop images manually into `requirements/ui/` later, or rely on `/sungen:create-test` to prompt again
81
+ - **Skip** — user will drop images manually into `requirements/ui/` later
41
82
 
42
83
  Each capture skill writes outputs into `qa/screens/<screen>/requirements/ui/` and reports back a summary. Do not inline capture logic here — always delegate to the skill so behavior stays consistent with `/sungen:create-test`.
43
84
 
44
- If the user has additional UI designs (mockups, hand-drawn sketches), suggest copying them to `requirements/ui/` — `sungen-capture-local` will pick them up during `/sungen:create-test`.
85
+ ### 3. Fill spec.md
86
+
87
+ Use `AskUserQuestion`: *"Fill `spec.md` now? (You can reference the captured visuals)"* — offer **Yes, fill now (Recommended)** / **Skip, fill later**.
88
+
89
+ If yes → open `qa/screens/<screen>/requirements/spec.md` and help the user fill sections, fields, validation rules, business rules, and states. Reference the captured visuals from Step 2 to suggest field names, form elements, and UI states. Especially prompt for the optional **Figma URL** and **Live URL** fields in Overview — those unlock auto-capture without re-asking next run.
45
90
 
46
91
  ### 4. Next steps
47
92