@sun-asterisk/sungen 2.5.0 → 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.
- package/README.md +88 -7
- package/dist/cli/commands/add.d.ts.map +1 -1
- package/dist/cli/commands/add.js +109 -9
- package/dist/cli/commands/add.js.map +1 -1
- package/dist/cli/commands/figma.d.ts +11 -0
- package/dist/cli/commands/figma.d.ts.map +1 -0
- package/dist/cli/commands/figma.js +178 -0
- package/dist/cli/commands/figma.js.map +1 -0
- package/dist/cli/index.js +4 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/orchestrator/ai-rules-updater.d.ts.map +1 -1
- package/dist/orchestrator/ai-rules-updater.js +2 -0
- package/dist/orchestrator/ai-rules-updater.js.map +1 -1
- package/dist/orchestrator/figma/figma-scaffolder-helpers.d.ts +33 -0
- package/dist/orchestrator/figma/figma-scaffolder-helpers.d.ts.map +1 -0
- package/dist/orchestrator/figma/figma-scaffolder-helpers.js +135 -0
- package/dist/orchestrator/figma/figma-scaffolder-helpers.js.map +1 -0
- package/dist/orchestrator/figma/figma-scaffolder-types.d.ts +25 -0
- package/dist/orchestrator/figma/figma-scaffolder-types.d.ts.map +1 -0
- package/dist/orchestrator/figma/figma-scaffolder-types.js +7 -0
- package/dist/orchestrator/figma/figma-scaffolder-types.js.map +1 -0
- package/dist/orchestrator/figma/figma-scaffolder.d.ts +23 -0
- package/dist/orchestrator/figma/figma-scaffolder.d.ts.map +1 -0
- package/dist/orchestrator/figma/figma-scaffolder.js +212 -0
- package/dist/orchestrator/figma/figma-scaffolder.js.map +1 -0
- package/dist/orchestrator/figma/node-path-collapser.d.ts +16 -0
- package/dist/orchestrator/figma/node-path-collapser.d.ts.map +1 -0
- package/dist/orchestrator/figma/node-path-collapser.js +37 -0
- package/dist/orchestrator/figma/node-path-collapser.js.map +1 -0
- package/dist/orchestrator/figma/spec-figma-renderer.d.ts +44 -0
- package/dist/orchestrator/figma/spec-figma-renderer.d.ts.map +1 -0
- package/dist/orchestrator/figma/spec-figma-renderer.js +45 -0
- package/dist/orchestrator/figma/spec-figma-renderer.js.map +1 -0
- package/dist/orchestrator/figma/spec-figma-section-renderers.d.ts +23 -0
- package/dist/orchestrator/figma/spec-figma-section-renderers.d.ts.map +1 -0
- package/dist/orchestrator/figma/spec-figma-section-renderers.js +47 -0
- package/dist/orchestrator/figma/spec-figma-section-renderers.js.map +1 -0
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +56 -11
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +30 -17
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-review.md +4 -3
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +33 -1
- package/dist/orchestrator/templates/ai-instructions/claude-config.md +1 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-figma-source.md +151 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +39 -20
- package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-review.md +2 -0
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +53 -9
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +21 -16
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-review.md +4 -3
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +33 -1
- package/dist/orchestrator/templates/ai-instructions/copilot-config.md +1 -0
- package/dist/orchestrator/templates/ai-instructions/copilot-skill-figma-source.md +151 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-figma-source.md +151 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +61 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +51 -25
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-review.md +20 -0
- package/dist/tools/figma/figma-auth.d.ts +36 -0
- package/dist/tools/figma/figma-auth.d.ts.map +1 -0
- package/dist/tools/figma/figma-auth.js +182 -0
- package/dist/tools/figma/figma-auth.js.map +1 -0
- package/dist/tools/figma/figma-cache.d.ts +45 -0
- package/dist/tools/figma/figma-cache.d.ts.map +1 -0
- package/dist/tools/figma/figma-cache.js +191 -0
- package/dist/tools/figma/figma-cache.js.map +1 -0
- package/dist/tools/figma/figma-client-types.d.ts +112 -0
- package/dist/tools/figma/figma-client-types.d.ts.map +1 -0
- package/dist/tools/figma/figma-client-types.js +7 -0
- package/dist/tools/figma/figma-client-types.js.map +1 -0
- package/dist/tools/figma/figma-errors.d.ts +49 -0
- package/dist/tools/figma/figma-errors.d.ts.map +1 -0
- package/dist/tools/figma/figma-errors.js +105 -0
- package/dist/tools/figma/figma-errors.js.map +1 -0
- package/dist/tools/figma/figma-image-downloader.d.ts +25 -0
- package/dist/tools/figma/figma-image-downloader.d.ts.map +1 -0
- package/dist/tools/figma/figma-image-downloader.js +128 -0
- package/dist/tools/figma/figma-image-downloader.js.map +1 -0
- package/dist/tools/figma/figma-node-filter.d.ts +26 -0
- package/dist/tools/figma/figma-node-filter.d.ts.map +1 -0
- package/dist/tools/figma/figma-node-filter.js +164 -0
- package/dist/tools/figma/figma-node-filter.js.map +1 -0
- package/dist/tools/figma/figma-rest-client.d.ts +24 -0
- package/dist/tools/figma/figma-rest-client.d.ts.map +1 -0
- package/dist/tools/figma/figma-rest-client.js +154 -0
- package/dist/tools/figma/figma-rest-client.js.map +1 -0
- package/dist/tools/figma/figma-url-parser.d.ts +18 -0
- package/dist/tools/figma/figma-url-parser.d.ts.map +1 -0
- package/dist/tools/figma/figma-url-parser.js +51 -0
- package/dist/tools/figma/figma-url-parser.js.map +1 -0
- package/dist/utils/exec-file-no-throw.d.ts +20 -0
- package/dist/utils/exec-file-no-throw.d.ts.map +1 -0
- package/dist/utils/exec-file-no-throw.js +36 -0
- package/dist/utils/exec-file-no-throw.js.map +1 -0
- package/package.json +1 -1
- package/src/cli/commands/add.ts +80 -9
- package/src/cli/commands/figma.ts +162 -0
- package/src/cli/index.ts +4 -2
- package/src/orchestrator/ai-rules-updater.ts +2 -0
- package/src/orchestrator/figma/figma-scaffolder-helpers.ts +126 -0
- package/src/orchestrator/figma/figma-scaffolder-types.ts +26 -0
- package/src/orchestrator/figma/figma-scaffolder.ts +209 -0
- package/src/orchestrator/figma/node-path-collapser.ts +38 -0
- package/src/orchestrator/figma/spec-figma-renderer.ts +80 -0
- package/src/orchestrator/figma/spec-figma-section-renderers.ts +46 -0
- package/src/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +56 -11
- package/src/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +30 -17
- package/src/orchestrator/templates/ai-instructions/claude-cmd-review.md +4 -3
- package/src/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +33 -1
- package/src/orchestrator/templates/ai-instructions/claude-config.md +1 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-figma-source.md +151 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +39 -20
- package/src/orchestrator/templates/ai-instructions/claude-skill-tc-review.md +2 -0
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +53 -9
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +21 -16
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-review.md +4 -3
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +33 -1
- package/src/orchestrator/templates/ai-instructions/copilot-config.md +1 -0
- package/src/orchestrator/templates/ai-instructions/copilot-skill-figma-source.md +151 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-figma-source.md +151 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +61 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +51 -25
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-review.md +20 -0
- package/src/tools/figma/figma-auth.ts +161 -0
- package/src/tools/figma/figma-cache.ts +184 -0
- package/src/tools/figma/figma-client-types.ts +125 -0
- package/src/tools/figma/figma-errors.ts +127 -0
- package/src/tools/figma/figma-image-downloader.ts +112 -0
- package/src/tools/figma/figma-node-filter.ts +198 -0
- package/src/tools/figma/figma-rest-client.ts +183 -0
- package/src/tools/figma/figma-url-parser.ts +55 -0
- package/src/utils/exec-file-no-throw.ts +45 -0
|
@@ -41,6 +41,7 @@ export const AI_RULES_FILE_MAPPING: [string, string][] = [
|
|
|
41
41
|
['claude-skill-capture-figma.md', '.claude/skills/sungen-capture-figma/SKILL.md'],
|
|
42
42
|
['claude-skill-capture-local.md', '.claude/skills/sungen-capture-local/SKILL.md'],
|
|
43
43
|
['claude-skill-capture-live.md', '.claude/skills/sungen-capture-live/SKILL.md'],
|
|
44
|
+
['claude-skill-figma-source.md', '.claude/skills/sungen-figma-source/SKILL.md'],
|
|
44
45
|
|
|
45
46
|
// Skills — GitHub Copilot
|
|
46
47
|
['github-skill-sungen-gherkin-syntax.md', '.github/skills/sungen-gherkin-syntax/SKILL.md'],
|
|
@@ -55,6 +56,7 @@ export const AI_RULES_FILE_MAPPING: [string, string][] = [
|
|
|
55
56
|
['github-skill-sungen-capture-figma.md', '.github/skills/sungen-capture-figma/SKILL.md'],
|
|
56
57
|
['github-skill-sungen-capture-local.md', '.github/skills/sungen-capture-local/SKILL.md'],
|
|
57
58
|
['github-skill-sungen-capture-live.md', '.github/skills/sungen-capture-live/SKILL.md'],
|
|
59
|
+
['github-skill-sungen-figma-source.md', '.github/skills/sungen-figma-source/SKILL.md'],
|
|
58
60
|
];
|
|
59
61
|
|
|
60
62
|
export class AIRulesUpdater {
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal helpers for figma-scaffolder.ts.
|
|
3
|
+
* Extracted to keep the main orchestration file under 200 LOC.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from 'node:fs';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
import type { Ora } from 'ora';
|
|
9
|
+
import { downloadToPath } from '../../tools/figma/figma-image-downloader';
|
|
10
|
+
import * as FigmaCache from '../../tools/figma/figma-cache';
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Name utilities
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
/** Sanitize a Figma frame/variant name to a safe filename (lowercase, dashes). */
|
|
17
|
+
export function sanitizeName(name: string): string {
|
|
18
|
+
return name
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
21
|
+
.replace(/^-+|-+$/g, '');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Version / stub helpers
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
/** Read generator version from root package.json. Falls back to "sungen". */
|
|
29
|
+
export function readGeneratorVersion(cwd: string): string {
|
|
30
|
+
try {
|
|
31
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
32
|
+
if (fs.existsSync(pkgPath)) {
|
|
33
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')) as { version?: string };
|
|
34
|
+
if (pkg.version) return `sungen v${pkg.version}`;
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
// fall through
|
|
38
|
+
}
|
|
39
|
+
return 'sungen';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Minimal spec.md stub pointing users to spec_figma.md. */
|
|
43
|
+
export function minimalSpecMd(screenName: string): string {
|
|
44
|
+
return `# ${screenName} Screen Specification
|
|
45
|
+
|
|
46
|
+
<!-- This file is the human-authored source of truth. -->
|
|
47
|
+
<!-- spec_figma.md contains auto-generated Figma data — copy useful sections here. -->
|
|
48
|
+
|
|
49
|
+
## Overview
|
|
50
|
+
- **URL Path:** /${screenName}
|
|
51
|
+
- **Auth Required:** no
|
|
52
|
+
- **Platform:** web
|
|
53
|
+
|
|
54
|
+
## Sections
|
|
55
|
+
|
|
56
|
+
<!-- Add sections, fields, validation rules, and business rules here. -->
|
|
57
|
+
`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Image download orchestration
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
export interface DownloadImagesOptions {
|
|
65
|
+
cwd: string;
|
|
66
|
+
fileKey: string;
|
|
67
|
+
versionId: string;
|
|
68
|
+
uiDir: string;
|
|
69
|
+
imageNodeIds: string[];
|
|
70
|
+
mainNodeId: string;
|
|
71
|
+
variantNames: string[];
|
|
72
|
+
/** Pre-warmed cache entries (nodeId → bytes). */
|
|
73
|
+
cachedImages: Map<string, Buffer>;
|
|
74
|
+
/** URL map from getImageUrls response (nodeId → signed URL). */
|
|
75
|
+
imageUrls: Record<string, string | null>;
|
|
76
|
+
frameBaseName: string;
|
|
77
|
+
scaleKind: `${number}x`;
|
|
78
|
+
spinner: Ora;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Download (or copy from cache) images for each node ID.
|
|
83
|
+
* Returns relative paths (e.g. "ui/login-frame.png") for each successfully written file.
|
|
84
|
+
*/
|
|
85
|
+
export async function downloadImages(opts: DownloadImagesOptions): Promise<string[]> {
|
|
86
|
+
const imagePaths: string[] = [];
|
|
87
|
+
|
|
88
|
+
for (let i = 0; i < opts.imageNodeIds.length; i++) {
|
|
89
|
+
const nodeId = opts.imageNodeIds[i];
|
|
90
|
+
const isMain = nodeId === opts.mainNodeId;
|
|
91
|
+
const nameSuffix = isMain
|
|
92
|
+
? opts.frameBaseName
|
|
93
|
+
: sanitizeName(opts.variantNames[i - 1] ?? `variant-${i}`);
|
|
94
|
+
const destPath = path.join(opts.uiDir, `${nameSuffix}.png`);
|
|
95
|
+
const relPath = `ui/${nameSuffix}.png`;
|
|
96
|
+
|
|
97
|
+
// Cache hit path
|
|
98
|
+
if (opts.cachedImages.has(nodeId)) {
|
|
99
|
+
opts.spinner.start(`Writing cached image: ${nameSuffix}.png`);
|
|
100
|
+
fs.writeFileSync(destPath, opts.cachedImages.get(nodeId)!);
|
|
101
|
+
imagePaths.push(relPath);
|
|
102
|
+
opts.spinner.succeed(` ui/${nameSuffix}.png (cached)`);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const url = opts.imageUrls[nodeId];
|
|
107
|
+
if (!url) {
|
|
108
|
+
opts.spinner.warn(` No image URL for node ${nodeId} — skipping`);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
opts.spinner.start(`Downloading ui/${nameSuffix}.png…`);
|
|
113
|
+
try {
|
|
114
|
+
await downloadToPath(url, destPath);
|
|
115
|
+
// Cache downloaded bytes for future runs
|
|
116
|
+
const bytes = fs.readFileSync(destPath);
|
|
117
|
+
FigmaCache.put(opts.cwd, opts.fileKey, opts.versionId, nodeId, opts.scaleKind, bytes);
|
|
118
|
+
imagePaths.push(relPath);
|
|
119
|
+
opts.spinner.succeed(` ui/${nameSuffix}.png`);
|
|
120
|
+
} catch {
|
|
121
|
+
opts.spinner.warn(` Failed to download ui/${nameSuffix}.png — skipping`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return imagePaths;
|
|
126
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared input/output types for FigmaScaffolder.
|
|
3
|
+
* Kept separate to avoid growing figma-client-types.ts beyond its scope.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface FigmaScaffolderOptions {
|
|
7
|
+
/** Screen name (e.g. "login", "dashboard"). */
|
|
8
|
+
screenName: string;
|
|
9
|
+
/** Full Figma share URL. */
|
|
10
|
+
figmaUrl: string;
|
|
11
|
+
/** Absolute or cwd-relative working directory. Default: process.cwd(). */
|
|
12
|
+
cwd?: string;
|
|
13
|
+
/** When true, bypass cache and re-fetch from Figma API. Default: false. */
|
|
14
|
+
refresh?: boolean;
|
|
15
|
+
/** Render scale for PNG export. Default: 2. */
|
|
16
|
+
scale?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface FigmaScaffolderResult {
|
|
20
|
+
/** Absolute path to the written spec_figma.md. */
|
|
21
|
+
specFigmaPath: string;
|
|
22
|
+
/** Absolute paths to downloaded PNG files. */
|
|
23
|
+
imagePaths: string[];
|
|
24
|
+
/** True when spec.md was newly created (was not already present). */
|
|
25
|
+
specMdCreated: boolean;
|
|
26
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
-
|
|
27
|
+
**Standard path (no `--figma`):**
|
|
24
28
|
```bash
|
|
25
29
|
sungen add --screen <screen> --path <path>
|
|
26
30
|
```
|
|
27
31
|
|
|
28
|
-
|
|
32
|
+
**Figma branch (when `--figma <url>` is in `$ARGUMENTS`):**
|
|
29
33
|
|
|
30
|
-
|
|
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
|
-
|
|
57
|
+
### 1a. Synthesize narrative sections (Figma branch only)
|
|
33
58
|
|
|
34
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|