@nick848/fet 1.1.12 → 1.1.13
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/dist/{chunk-J5WB4KAL.js → chunk-YKRUVIJ6.js} +0 -1
- package/dist/cli/index.js +1009 -208
- package/dist/index.js +1 -2
- package/package.json +2 -1
- package/dist/chunk-J5WB4KAL.js.map +0 -1
- package/dist/cli/index.js.map +0 -1
- package/dist/index.js.map +0 -1
package/dist/cli/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
FetError,
|
|
4
4
|
toFetError
|
|
5
|
-
} from "../chunk-
|
|
5
|
+
} from "../chunk-YKRUVIJ6.js";
|
|
6
6
|
|
|
7
7
|
// src/cli/index.ts
|
|
8
8
|
import { createInterface as createInterface3 } from "readline/promises";
|
|
@@ -2080,7 +2080,13 @@ function renderFetConfig(scan, language = "zh-CN") {
|
|
|
2080
2080
|
figmaGuard: {
|
|
2081
2081
|
enabled: true,
|
|
2082
2082
|
mode: "require_before_ui",
|
|
2083
|
-
onUncertainty: "stop_and_ask"
|
|
2083
|
+
onUncertainty: "stop_and_ask",
|
|
2084
|
+
sync: {
|
|
2085
|
+
enabled: true,
|
|
2086
|
+
apiKeyEnv: "FIGMA_ACCESS_TOKEN",
|
|
2087
|
+
inlineImageMaxBytes: 32768,
|
|
2088
|
+
requireBeforeUi: true
|
|
2089
|
+
}
|
|
2084
2090
|
},
|
|
2085
2091
|
uiDisplayContract: {
|
|
2086
2092
|
enabled: true
|
|
@@ -2530,17 +2536,27 @@ function figmaStopHandoffRelativePath(changeId) {
|
|
|
2530
2536
|
function figmaApplyInstructionsRelativePath(changeId) {
|
|
2531
2537
|
return `openspec/changes/${changeId}/.fet/figma-apply-instructions.md`;
|
|
2532
2538
|
}
|
|
2539
|
+
function figmaDesignManifestRelativePath(changeId) {
|
|
2540
|
+
return `openspec/changes/${changeId}/.fet/figma-design-manifest.yaml`;
|
|
2541
|
+
}
|
|
2542
|
+
function figmaSyncInstructionsRelativePath(changeId) {
|
|
2543
|
+
return `openspec/changes/${changeId}/.fet/figma-sync-instructions.md`;
|
|
2544
|
+
}
|
|
2533
2545
|
function renderFigmaRequireBeforeUiBody(language, changeId) {
|
|
2534
2546
|
const stopPath = figmaStopHandoffRelativePath(changeId);
|
|
2547
|
+
const syncPath = figmaSyncInstructionsRelativePath(changeId);
|
|
2548
|
+
const manifestPath3 = figmaDesignManifestRelativePath(changeId);
|
|
2535
2549
|
if (language === "en") {
|
|
2536
2550
|
return `## Mandatory before any UI implementation
|
|
2537
2551
|
|
|
2538
2552
|
Complete these steps **before** writing or editing UI code (components, pages, styles, layout):
|
|
2539
2553
|
|
|
2540
|
-
1.
|
|
2541
|
-
2.
|
|
2542
|
-
3.
|
|
2543
|
-
4.
|
|
2554
|
+
1. Run \`fet figma sync --change ${changeId}\` (requires \`FIGMA_ACCESS_TOKEN\` or \`--token\`) so FET downloads **all nodes** and **image assets** into \`.fet/figma-assets/\`.
|
|
2555
|
+
2. Read \`${syncPath}\`, \`${manifestPath3}\`, and \`figma-nodes.json\` in the same folder\u2014use on-disk asset paths for backgrounds and large images (not base64 in source).
|
|
2556
|
+
3. Read \`${stopPath}\` for detected Figma links and stop rules.
|
|
2557
|
+
4. If sync is impossible, use **Figma MCP/API** to read every linked frame/node; do not skip image fills or exportable layers.
|
|
2558
|
+
5. In your reply, briefly list design facts you confirmed (frames, colors, typography, spacing, components, states, asset paths).
|
|
2559
|
+
6. Only then implement UI tasks from \`tasks.md\`.
|
|
2544
2560
|
|
|
2545
2561
|
## Forbidden
|
|
2546
2562
|
|
|
@@ -2556,10 +2572,12 @@ Follow the stop rules in \`${stopPath}\`. Pause implementation, explain what fai
|
|
|
2556
2572
|
|
|
2557
2573
|
\u5728\u7F16\u5199\u6216\u4FEE\u6539 UI \u4EE3\u7801\uFF08\u7EC4\u4EF6\u3001\u9875\u9762\u3001\u6837\u5F0F\u3001\u5E03\u5C40\uFF09**\u4E4B\u524D**\uFF0C\u6309\u987A\u5E8F\u5B8C\u6210\uFF1A
|
|
2558
2574
|
|
|
2559
|
-
1. \
|
|
2560
|
-
2. \
|
|
2561
|
-
3. \
|
|
2562
|
-
4. \
|
|
2575
|
+
1. \u6267\u884C \`fet figma sync --change ${changeId}\`\uFF08\u9700 \`FIGMA_ACCESS_TOKEN\` \u6216 \`--token\`\uFF09\uFF0C\u7531 FET \u62C9\u53D6**\u5168\u90E8\u8282\u70B9**\u5E76\u5C06\u56FE\u7247\u7D20\u6750\u843D\u76D8\u5230 \`.fet/figma-assets/\`\u3002
|
|
2576
|
+
2. \u9605\u8BFB\u540C\u76EE\u5F55 \`${syncPath}\`\u3001\`${manifestPath3}\`\u3001\`figma-nodes.json\`\uFF1B\u5927\u56FE\u7528\u6587\u4EF6\u8DEF\u5F84\u5F15\u7528\uFF0C\u7981\u6B62\u5728\u6E90\u7801\u4E2D\u5185\u8054\u5927\u4F53\u79EF base64\u3002
|
|
2577
|
+
3. \u9605\u8BFB \`${stopPath}\` \u4E2D\u7684 Figma \u94FE\u63A5\u4E0E\u505C\u6B62\u89C4\u5219\u3002
|
|
2578
|
+
4. \u82E5\u65E0\u6CD5 sync\uFF0C\u5219\u7528 **Figma MCP/API** \u8BFB\u53D6\u6BCF\u4E2A\u753B\u677F/\u8282\u70B9\uFF0C\u4E0D\u5F97\u9057\u6F0F\u56FE\u7247\u586B\u5145\u4E0E\u53EF\u5BFC\u51FA\u56FE\u5C42\u3002
|
|
2579
|
+
5. \u5728\u56DE\u590D\u4E2D\u7B80\u8981\u5217\u51FA\u5DF2\u786E\u8BA4\u7684\u8BBE\u8BA1\u4E8B\u5B9E\uFF08\u753B\u677F\u3001\u989C\u8272\u3001\u5B57\u53F7\u3001\u95F4\u8DDD\u3001\u7EC4\u4EF6\u3001\u72B6\u6001\u3001\u7D20\u6750\u8DEF\u5F84\uFF09\u3002
|
|
2580
|
+
6. \u5B8C\u6210\u4EE5\u4E0A\u6B65\u9AA4\u540E\uFF0C\u518D\u6309 \`tasks.md\` \u5B9E\u65BD UI \u4EFB\u52A1\u3002
|
|
2563
2581
|
|
|
2564
2582
|
## \u7981\u6B62
|
|
2565
2583
|
|
|
@@ -2702,16 +2720,21 @@ function renderFigmaStopNextStep(changeId, language) {
|
|
|
2702
2720
|
const path = figmaStopHandoffRelativePath(changeId);
|
|
2703
2721
|
return language === "en" ? `Before UI implementation, read ${path}. If Figma access fails or design details are unclear, stop and ask the user\u2014do not guess styles.` : `\u5B9E\u65BD UI \u524D\u9605\u8BFB ${path}\u3002Figma \u8BBF\u95EE\u5931\u8D25\u6216\u8BBE\u8BA1\u7EC6\u8282\u4E0D\u660E\u786E\u65F6\u7ACB\u5373\u505C\u6B62\u5E76\u5411\u7528\u6237\u63D0\u95EE\uFF0C\u4E0D\u8981\u731C\u6D4B\u6837\u5F0F\u3002`;
|
|
2704
2722
|
}
|
|
2705
|
-
function renderFigmaApplyNextSteps(changeId, language, mode) {
|
|
2723
|
+
function renderFigmaApplyNextSteps(changeId, language, mode, options) {
|
|
2706
2724
|
const applyPath = figmaApplyInstructionsRelativePath(changeId);
|
|
2707
2725
|
const stopPath = figmaStopHandoffRelativePath(changeId);
|
|
2726
|
+
const syncPath = figmaSyncInstructionsRelativePath(changeId);
|
|
2727
|
+
const apiKeyEnv = options?.apiKeyEnv ?? "FIGMA_ACCESS_TOKEN";
|
|
2708
2728
|
if (mode === "require_before_ui") {
|
|
2729
|
+
const syncStep = options?.syncDone === true ? language === "en" ? `Figma design data is synced. Read ${syncPath} and figma-design-manifest.yaml before UI work.` : `Figma \u8BBE\u8BA1\u6570\u636E\u5DF2\u540C\u6B65\u3002\u5B9E\u65BD UI \u524D\u9605\u8BFB ${syncPath} \u4E0E figma-design-manifest.yaml\u3002` : options?.syncMissingToken ? language === "en" ? `Run fet figma sync --change ${changeId} with ${apiKeyEnv} set to fetch all nodes and assets before UI work.` : `\u5B9E\u65BD UI \u524D\u6267\u884C fet figma sync --change ${changeId}\uFF08\u914D\u7F6E ${apiKeyEnv}\uFF09\uFF0C\u62C9\u53D6\u5168\u90E8\u8282\u70B9\u4E0E\u7D20\u6750\u3002` : language === "en" ? `Run fet figma sync --change ${changeId} if not already synced; then read ${syncPath}.` : `\u82E5\u5C1A\u672A\u540C\u6B65\uFF0C\u6267\u884C fet figma sync --change ${changeId}\uFF0C\u5E76\u9605\u8BFB ${syncPath}\u3002`;
|
|
2709
2730
|
return language === "en" ? [
|
|
2710
|
-
|
|
2731
|
+
syncStep,
|
|
2732
|
+
`Before any UI task: read and follow ${applyPath} (mandatory). Use synced assets and node tree\u2014do not invent styles.`,
|
|
2711
2733
|
`If Figma access fails or design details are unclear, stop per ${stopPath} and ask the user before continuing.`,
|
|
2712
2734
|
`After Figma is confirmed, read openspec/changes/${changeId}/tasks.md and implement pending tasks.`
|
|
2713
2735
|
] : [
|
|
2714
|
-
|
|
2736
|
+
syncStep,
|
|
2737
|
+
`\u5B9E\u65BD\u4EFB\u4F55 UI \u4EFB\u52A1\u524D\uFF1A\u5FC5\u987B\u9605\u8BFB\u5E76\u9075\u5B88 ${applyPath}\uFF1B\u4F7F\u7528\u5DF2\u540C\u6B65\u7684\u8282\u70B9\u6811\u4E0E\u7D20\u6750\u8DEF\u5F84\uFF0C\u7981\u6B62\u81EA\u521B\u6837\u5F0F\u3002`,
|
|
2715
2738
|
`Figma \u8BBF\u95EE\u5931\u8D25\u6216\u8BBE\u8BA1\u7EC6\u8282\u4E0D\u6E05\uFF1A\u6309 ${stopPath} \u7ACB\u5373\u505C\u6B62\u5E76\u5411\u7528\u6237\u63D0\u95EE\uFF0C\u786E\u8BA4\u540E\u518D\u7EE7\u7EED\u3002`,
|
|
2716
2739
|
`\u8BBE\u8BA1\u786E\u8BA4\u540E\uFF0C\u518D\u9605\u8BFB openspec/changes/${changeId}/tasks.md \u5E76\u5B9E\u65BD\u5F85\u529E\u4EFB\u52A1\u3002`
|
|
2717
2740
|
];
|
|
@@ -3545,8 +3568,8 @@ async function exists4(path) {
|
|
|
3545
3568
|
}
|
|
3546
3569
|
|
|
3547
3570
|
// src/commands/proxy.ts
|
|
3548
|
-
import { readFile as
|
|
3549
|
-
import { join as
|
|
3571
|
+
import { readFile as readFile19 } from "fs/promises";
|
|
3572
|
+
import { join as join26 } from "path";
|
|
3550
3573
|
|
|
3551
3574
|
// src/figma-guard.ts
|
|
3552
3575
|
import { readdir as readdir3, readFile as readFile10, stat as stat7 } from "fs/promises";
|
|
@@ -3701,11 +3724,757 @@ async function readOptional3(path) {
|
|
|
3701
3724
|
}
|
|
3702
3725
|
}
|
|
3703
3726
|
|
|
3727
|
+
// src/figma/config.ts
|
|
3728
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
3729
|
+
import { join as join17 } from "path";
|
|
3730
|
+
import { parseDocument as parseDocument3 } from "yaml";
|
|
3731
|
+
var DEFAULT_FIGMA_SYNC_CONFIG = {
|
|
3732
|
+
enabled: true,
|
|
3733
|
+
apiKeyEnv: "FIGMA_ACCESS_TOKEN",
|
|
3734
|
+
inlineImageMaxBytes: 32768,
|
|
3735
|
+
requireBeforeUi: true
|
|
3736
|
+
};
|
|
3737
|
+
async function loadFigmaSyncConfig(projectRoot) {
|
|
3738
|
+
try {
|
|
3739
|
+
const raw = await readFile11(join17(projectRoot, "openspec", "config.yaml"), "utf8");
|
|
3740
|
+
const doc = parseDocument3(raw);
|
|
3741
|
+
const fetNode = doc.get("fet", true);
|
|
3742
|
+
const guardNode = fetNode?.get?.("figmaGuard");
|
|
3743
|
+
const syncNode = guardNode?.get?.("sync");
|
|
3744
|
+
if (!syncNode || typeof syncNode.get !== "function") {
|
|
3745
|
+
return DEFAULT_FIGMA_SYNC_CONFIG;
|
|
3746
|
+
}
|
|
3747
|
+
const enabled = syncNode.get("enabled");
|
|
3748
|
+
const apiKeyEnv = syncNode.get("apiKeyEnv");
|
|
3749
|
+
const inlineImageMaxBytes = syncNode.get("inlineImageMaxBytes");
|
|
3750
|
+
const requireBeforeUi = syncNode.get("requireBeforeUi");
|
|
3751
|
+
return {
|
|
3752
|
+
enabled: enabled === void 0 ? true : Boolean(enabled),
|
|
3753
|
+
apiKeyEnv: typeof apiKeyEnv === "string" && apiKeyEnv.trim() ? apiKeyEnv.trim() : DEFAULT_FIGMA_SYNC_CONFIG.apiKeyEnv,
|
|
3754
|
+
inlineImageMaxBytes: typeof inlineImageMaxBytes === "number" && inlineImageMaxBytes > 0 ? inlineImageMaxBytes : DEFAULT_FIGMA_SYNC_CONFIG.inlineImageMaxBytes,
|
|
3755
|
+
requireBeforeUi: requireBeforeUi === void 0 ? true : Boolean(requireBeforeUi)
|
|
3756
|
+
};
|
|
3757
|
+
} catch {
|
|
3758
|
+
return DEFAULT_FIGMA_SYNC_CONFIG;
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3761
|
+
function resolveFigmaAccessToken(config, explicitToken) {
|
|
3762
|
+
if (explicitToken?.trim()) {
|
|
3763
|
+
return explicitToken.trim();
|
|
3764
|
+
}
|
|
3765
|
+
const fromEnv = process.env[config.apiKeyEnv];
|
|
3766
|
+
return fromEnv?.trim() ? fromEnv.trim() : null;
|
|
3767
|
+
}
|
|
3768
|
+
|
|
3769
|
+
// src/figma/sync.ts
|
|
3770
|
+
import { mkdir as mkdir7, writeFile as writeFile2 } from "fs/promises";
|
|
3771
|
+
import { join as join21 } from "path";
|
|
3772
|
+
|
|
3773
|
+
// src/tdd/fingerprint.ts
|
|
3774
|
+
import { createHash } from "crypto";
|
|
3775
|
+
import { readdir as readdir4, readFile as readFile12, stat as stat8 } from "fs/promises";
|
|
3776
|
+
import { join as join18, relative as relative3 } from "path";
|
|
3777
|
+
async function collectPlanningSources(projectRoot, changeId) {
|
|
3778
|
+
const changeRoot = join18(projectRoot, "openspec", "changes", changeId);
|
|
3779
|
+
const sources = [];
|
|
3780
|
+
const rootFiles = ["proposal.md", "tasks.md", "design.md"];
|
|
3781
|
+
for (const name of rootFiles) {
|
|
3782
|
+
const path = join18(changeRoot, name);
|
|
3783
|
+
if (await exists5(path)) {
|
|
3784
|
+
sources.push(relative3(projectRoot, path).replace(/\\/g, "/"));
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3787
|
+
const specsDir = join18(changeRoot, "specs");
|
|
3788
|
+
if (await exists5(specsDir)) {
|
|
3789
|
+
for (const file of await walkFiles(specsDir)) {
|
|
3790
|
+
if (file.endsWith(".md")) {
|
|
3791
|
+
sources.push(relative3(projectRoot, file).replace(/\\/g, "/"));
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
return sources.sort();
|
|
3796
|
+
}
|
|
3797
|
+
async function computePlanningFingerprint(projectRoot, changeId) {
|
|
3798
|
+
const sources = await collectPlanningSources(projectRoot, changeId);
|
|
3799
|
+
const hash = createHash("sha256");
|
|
3800
|
+
for (const source of sources) {
|
|
3801
|
+
const content = await readFile12(join18(projectRoot, source), "utf8");
|
|
3802
|
+
hash.update(source);
|
|
3803
|
+
hash.update("\0");
|
|
3804
|
+
hash.update(content);
|
|
3805
|
+
hash.update("\0");
|
|
3806
|
+
}
|
|
3807
|
+
return `sha256:${hash.digest("hex")}`;
|
|
3808
|
+
}
|
|
3809
|
+
async function walkFiles(dir) {
|
|
3810
|
+
const entries = await readdir4(dir, { withFileTypes: true });
|
|
3811
|
+
const files = [];
|
|
3812
|
+
for (const entry of entries) {
|
|
3813
|
+
const path = join18(dir, entry.name);
|
|
3814
|
+
if (entry.isDirectory()) {
|
|
3815
|
+
files.push(...await walkFiles(path));
|
|
3816
|
+
} else if (entry.isFile()) {
|
|
3817
|
+
files.push(path);
|
|
3818
|
+
}
|
|
3819
|
+
}
|
|
3820
|
+
return files;
|
|
3821
|
+
}
|
|
3822
|
+
async function exists5(path) {
|
|
3823
|
+
try {
|
|
3824
|
+
await stat8(path);
|
|
3825
|
+
return true;
|
|
3826
|
+
} catch {
|
|
3827
|
+
return false;
|
|
3828
|
+
}
|
|
3829
|
+
}
|
|
3830
|
+
|
|
3831
|
+
// src/figma/api.ts
|
|
3832
|
+
var FIGMA_API_BASE = "https://api.figma.com/v1";
|
|
3833
|
+
async function fetchFigmaFile(fileKey, accessToken, nodeIds) {
|
|
3834
|
+
const params = new URLSearchParams();
|
|
3835
|
+
if (nodeIds?.length) {
|
|
3836
|
+
params.set("ids", nodeIds.join(","));
|
|
3837
|
+
params.set("depth", "8");
|
|
3838
|
+
}
|
|
3839
|
+
const query = params.size ? `?${params.toString()}` : "";
|
|
3840
|
+
const url = nodeIds?.length ? `${FIGMA_API_BASE}/files/${fileKey}/nodes${query}` : `${FIGMA_API_BASE}/files/${fileKey}${query}`;
|
|
3841
|
+
const response = await fetch(url, {
|
|
3842
|
+
headers: { "X-Figma-Token": accessToken }
|
|
3843
|
+
});
|
|
3844
|
+
if (!response.ok) {
|
|
3845
|
+
throw new FetError({
|
|
3846
|
+
code: "OPENSPEC_COMMAND_FAILED" /* OpenSpecCommandFailed */,
|
|
3847
|
+
message: `Figma API request failed (${response.status})`,
|
|
3848
|
+
details: { fileKey, status: response.status, statusText: response.statusText },
|
|
3849
|
+
recoverable: true,
|
|
3850
|
+
suggestedCommand: "Check FIGMA_ACCESS_TOKEN and file permissions, then rerun fet figma sync."
|
|
3851
|
+
});
|
|
3852
|
+
}
|
|
3853
|
+
const body = await response.json();
|
|
3854
|
+
if (body.nodes && nodeIds?.length) {
|
|
3855
|
+
const first = nodeIds.map((id) => body.nodes?.[id]?.document).find(Boolean);
|
|
3856
|
+
if (!first) {
|
|
3857
|
+
throw new FetError({
|
|
3858
|
+
code: "OPENSPEC_COMMAND_FAILED" /* OpenSpecCommandFailed */,
|
|
3859
|
+
message: "Figma API returned no nodes for the requested node-id.",
|
|
3860
|
+
details: { fileKey, nodeIds },
|
|
3861
|
+
recoverable: true
|
|
3862
|
+
});
|
|
3863
|
+
}
|
|
3864
|
+
return {
|
|
3865
|
+
name: body.name ?? fileKey,
|
|
3866
|
+
document: first,
|
|
3867
|
+
components: body.components,
|
|
3868
|
+
styles: body.styles
|
|
3869
|
+
};
|
|
3870
|
+
}
|
|
3871
|
+
if (!body.document) {
|
|
3872
|
+
throw new FetError({
|
|
3873
|
+
code: "OPENSPEC_COMMAND_FAILED" /* OpenSpecCommandFailed */,
|
|
3874
|
+
message: "Figma API returned an empty document.",
|
|
3875
|
+
details: { fileKey },
|
|
3876
|
+
recoverable: true
|
|
3877
|
+
});
|
|
3878
|
+
}
|
|
3879
|
+
return {
|
|
3880
|
+
name: body.name ?? fileKey,
|
|
3881
|
+
document: body.document,
|
|
3882
|
+
components: body.components,
|
|
3883
|
+
styles: body.styles
|
|
3884
|
+
};
|
|
3885
|
+
}
|
|
3886
|
+
async function fetchFigmaImageUrls(fileKey, accessToken, imageRefs) {
|
|
3887
|
+
if (!imageRefs.length) {
|
|
3888
|
+
return {};
|
|
3889
|
+
}
|
|
3890
|
+
const response = await fetch(`${FIGMA_API_BASE}/files/${fileKey}/images`, {
|
|
3891
|
+
headers: { "X-Figma-Token": accessToken }
|
|
3892
|
+
});
|
|
3893
|
+
if (!response.ok) {
|
|
3894
|
+
throw new FetError({
|
|
3895
|
+
code: "OPENSPEC_COMMAND_FAILED" /* OpenSpecCommandFailed */,
|
|
3896
|
+
message: `Figma image metadata request failed (${response.status})`,
|
|
3897
|
+
details: { fileKey, status: response.status },
|
|
3898
|
+
recoverable: true
|
|
3899
|
+
});
|
|
3900
|
+
}
|
|
3901
|
+
const body = await response.json();
|
|
3902
|
+
const result = {};
|
|
3903
|
+
for (const ref of imageRefs) {
|
|
3904
|
+
const url = body.images?.[ref];
|
|
3905
|
+
if (url) {
|
|
3906
|
+
result[ref] = url;
|
|
3907
|
+
}
|
|
3908
|
+
}
|
|
3909
|
+
return result;
|
|
3910
|
+
}
|
|
3911
|
+
async function fetchFigmaNodeExports(fileKey, accessToken, nodeIds, format = "png") {
|
|
3912
|
+
if (!nodeIds.length) {
|
|
3913
|
+
return {};
|
|
3914
|
+
}
|
|
3915
|
+
const params = new URLSearchParams({
|
|
3916
|
+
ids: nodeIds.join(","),
|
|
3917
|
+
format,
|
|
3918
|
+
scale: "2"
|
|
3919
|
+
});
|
|
3920
|
+
const response = await fetch(`${FIGMA_API_BASE}/images/${fileKey}?${params.toString()}`, {
|
|
3921
|
+
headers: { "X-Figma-Token": accessToken }
|
|
3922
|
+
});
|
|
3923
|
+
if (!response.ok) {
|
|
3924
|
+
return {};
|
|
3925
|
+
}
|
|
3926
|
+
const body = await response.json();
|
|
3927
|
+
const result = {};
|
|
3928
|
+
for (const id of nodeIds) {
|
|
3929
|
+
const url = body.images?.[id];
|
|
3930
|
+
if (url) {
|
|
3931
|
+
result[id] = url;
|
|
3932
|
+
}
|
|
3933
|
+
}
|
|
3934
|
+
return result;
|
|
3935
|
+
}
|
|
3936
|
+
async function downloadBinary(url) {
|
|
3937
|
+
const response = await fetch(url);
|
|
3938
|
+
if (!response.ok) {
|
|
3939
|
+
throw new FetError({
|
|
3940
|
+
code: "FILE_SYSTEM_ERROR" /* FileSystemError */,
|
|
3941
|
+
message: `Failed to download Figma asset (${response.status})`,
|
|
3942
|
+
details: { url },
|
|
3943
|
+
recoverable: true
|
|
3944
|
+
});
|
|
3945
|
+
}
|
|
3946
|
+
const buffer = new Uint8Array(await response.arrayBuffer());
|
|
3947
|
+
const mimeType = response.headers.get("content-type")?.split(";")[0]?.trim() || "application/octet-stream";
|
|
3948
|
+
return { bytes: buffer, mimeType };
|
|
3949
|
+
}
|
|
3950
|
+
|
|
3951
|
+
// src/figma/download-assets.ts
|
|
3952
|
+
import { mkdir as mkdir6, writeFile } from "fs/promises";
|
|
3953
|
+
import { join as join19 } from "path";
|
|
3954
|
+
|
|
3955
|
+
// src/figma/paths.ts
|
|
3956
|
+
function figmaFetDirRelative(changeId) {
|
|
3957
|
+
return `openspec/changes/${changeId}/.fet`;
|
|
3958
|
+
}
|
|
3959
|
+
function figmaDesignManifestRelativePath2(changeId) {
|
|
3960
|
+
return `${figmaFetDirRelative(changeId)}/figma-design-manifest.yaml`;
|
|
3961
|
+
}
|
|
3962
|
+
function figmaNodesRelativePath(changeId) {
|
|
3963
|
+
return `${figmaFetDirRelative(changeId)}/figma-nodes.json`;
|
|
3964
|
+
}
|
|
3965
|
+
function figmaSyncInstructionsRelativePath2(changeId) {
|
|
3966
|
+
return `${figmaFetDirRelative(changeId)}/figma-sync-instructions.md`;
|
|
3967
|
+
}
|
|
3968
|
+
function figmaAssetsDirRelative(changeId) {
|
|
3969
|
+
return `${figmaFetDirRelative(changeId)}/figma-assets`;
|
|
3970
|
+
}
|
|
3971
|
+
function figmaAssetFileRelative(changeId, fileKey, assetId, ext) {
|
|
3972
|
+
const safeId = assetId.replace(/:/g, "-");
|
|
3973
|
+
return `${figmaAssetsDirRelative(changeId)}/${fileKey}/${safeId}.${ext}`;
|
|
3974
|
+
}
|
|
3975
|
+
|
|
3976
|
+
// src/figma/download-assets.ts
|
|
3977
|
+
function extensionForMime(mimeType) {
|
|
3978
|
+
if (mimeType.includes("svg")) {
|
|
3979
|
+
return "svg";
|
|
3980
|
+
}
|
|
3981
|
+
if (mimeType.includes("jpeg") || mimeType.includes("jpg")) {
|
|
3982
|
+
return "jpg";
|
|
3983
|
+
}
|
|
3984
|
+
if (mimeType.includes("webp")) {
|
|
3985
|
+
return "webp";
|
|
3986
|
+
}
|
|
3987
|
+
return "png";
|
|
3988
|
+
}
|
|
3989
|
+
function toBase64(bytes) {
|
|
3990
|
+
return Buffer.from(bytes).toString("base64");
|
|
3991
|
+
}
|
|
3992
|
+
async function downloadFigmaAssets(options) {
|
|
3993
|
+
const assets = [];
|
|
3994
|
+
const assetsRoot = join19(options.projectRoot, "openspec", "changes", options.changeId, ".fet", "figma-assets", options.fileKey);
|
|
3995
|
+
await mkdir6(assetsRoot, { recursive: true });
|
|
3996
|
+
const refUrls = await fetchFigmaImageUrls(options.fileKey, options.accessToken, [
|
|
3997
|
+
...new Set(options.imageRefs.map((item) => item.ref))
|
|
3998
|
+
]);
|
|
3999
|
+
for (const item of options.imageRefs) {
|
|
4000
|
+
const url = refUrls[item.ref];
|
|
4001
|
+
if (!url) {
|
|
4002
|
+
continue;
|
|
4003
|
+
}
|
|
4004
|
+
const { bytes, mimeType } = await downloadBinary(url);
|
|
4005
|
+
const ext = extensionForMime(mimeType);
|
|
4006
|
+
const assetId = `ref-${item.ref}`;
|
|
4007
|
+
const relativePath = figmaAssetFileRelative(options.changeId, options.fileKey, assetId, ext);
|
|
4008
|
+
const entry = await persistAsset({
|
|
4009
|
+
projectRoot: options.projectRoot,
|
|
4010
|
+
relativePath,
|
|
4011
|
+
assetId,
|
|
4012
|
+
name: item.name,
|
|
4013
|
+
nodeId: item.nodeId,
|
|
4014
|
+
imageRef: item.ref,
|
|
4015
|
+
kind: "image-ref",
|
|
4016
|
+
bytes,
|
|
4017
|
+
mimeType,
|
|
4018
|
+
config: options.config
|
|
4019
|
+
});
|
|
4020
|
+
assets.push(entry);
|
|
4021
|
+
}
|
|
4022
|
+
const exportIds = options.exportNodeIds.map((item) => item.nodeId).filter((id) => !assets.some((asset) => asset.nodeId === id));
|
|
4023
|
+
if (!exportIds.length) {
|
|
4024
|
+
return assets;
|
|
4025
|
+
}
|
|
4026
|
+
const exportUrls = await fetchFigmaNodeExports(options.fileKey, options.accessToken, exportIds.slice(0, 30));
|
|
4027
|
+
for (const item of options.exportNodeIds) {
|
|
4028
|
+
const url = exportUrls[item.nodeId];
|
|
4029
|
+
if (!url) {
|
|
4030
|
+
continue;
|
|
4031
|
+
}
|
|
4032
|
+
const { bytes, mimeType } = await downloadBinary(url);
|
|
4033
|
+
const ext = extensionForMime(mimeType);
|
|
4034
|
+
const relativePath = figmaAssetFileRelative(options.changeId, options.fileKey, item.nodeId, ext);
|
|
4035
|
+
const entry = await persistAsset({
|
|
4036
|
+
projectRoot: options.projectRoot,
|
|
4037
|
+
relativePath,
|
|
4038
|
+
assetId: item.nodeId,
|
|
4039
|
+
name: item.name,
|
|
4040
|
+
nodeId: item.nodeId,
|
|
4041
|
+
kind: "node-export",
|
|
4042
|
+
bytes,
|
|
4043
|
+
mimeType,
|
|
4044
|
+
config: options.config
|
|
4045
|
+
});
|
|
4046
|
+
assets.push(entry);
|
|
4047
|
+
}
|
|
4048
|
+
return assets;
|
|
4049
|
+
}
|
|
4050
|
+
async function persistAsset(options) {
|
|
4051
|
+
const absolutePath = join19(options.projectRoot, options.relativePath);
|
|
4052
|
+
await mkdir6(join19(absolutePath, ".."), { recursive: true });
|
|
4053
|
+
if (options.bytes.byteLength <= options.config.inlineImageMaxBytes) {
|
|
4054
|
+
const inlinePath = `${options.relativePath}.inline.json`;
|
|
4055
|
+
await writeFile(
|
|
4056
|
+
join19(options.projectRoot, inlinePath),
|
|
4057
|
+
JSON.stringify({
|
|
4058
|
+
mimeType: options.mimeType,
|
|
4059
|
+
base64: toBase64(options.bytes)
|
|
4060
|
+
}),
|
|
4061
|
+
"utf8"
|
|
4062
|
+
);
|
|
4063
|
+
return {
|
|
4064
|
+
id: options.assetId,
|
|
4065
|
+
kind: options.kind,
|
|
4066
|
+
name: options.name,
|
|
4067
|
+
nodeId: options.nodeId,
|
|
4068
|
+
imageRef: options.imageRef,
|
|
4069
|
+
storage: "inline",
|
|
4070
|
+
relativePath: inlinePath,
|
|
4071
|
+
mimeType: options.mimeType,
|
|
4072
|
+
bytes: options.bytes.byteLength
|
|
4073
|
+
};
|
|
4074
|
+
}
|
|
4075
|
+
await writeFile(absolutePath, options.bytes);
|
|
4076
|
+
return {
|
|
4077
|
+
id: options.assetId,
|
|
4078
|
+
kind: options.kind,
|
|
4079
|
+
name: options.name,
|
|
4080
|
+
nodeId: options.nodeId,
|
|
4081
|
+
imageRef: options.imageRef,
|
|
4082
|
+
storage: "file",
|
|
4083
|
+
relativePath: options.relativePath,
|
|
4084
|
+
mimeType: options.mimeType,
|
|
4085
|
+
bytes: options.bytes.byteLength
|
|
4086
|
+
};
|
|
4087
|
+
}
|
|
4088
|
+
|
|
4089
|
+
// src/figma/manifest.ts
|
|
4090
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
4091
|
+
import { join as join20 } from "path";
|
|
4092
|
+
import { parseDocument as parseDocument4, stringify as stringify3 } from "yaml";
|
|
4093
|
+
async function readFigmaDesignManifest(projectRoot, changeId) {
|
|
4094
|
+
try {
|
|
4095
|
+
const raw = await readFile13(join20(projectRoot, figmaDesignManifestRelativePath2(changeId)), "utf8");
|
|
4096
|
+
const doc = parseDocument4(raw).toJSON();
|
|
4097
|
+
if (!doc?.schemaVersion) {
|
|
4098
|
+
return null;
|
|
4099
|
+
}
|
|
4100
|
+
return doc;
|
|
4101
|
+
} catch {
|
|
4102
|
+
return null;
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
function renderFigmaDesignManifestYaml(manifest) {
|
|
4106
|
+
return stringify3(manifest);
|
|
4107
|
+
}
|
|
4108
|
+
|
|
4109
|
+
// src/templates/figma-sync.ts
|
|
4110
|
+
function renderFigmaSyncInstructions(options) {
|
|
4111
|
+
const title = options.language === "en" ? "Figma sync (this change)" : "Figma \u540C\u6B65\uFF08\u672C change\uFF09";
|
|
4112
|
+
const intro = options.language === "en" ? `FET synced **${options.nodeCount}** nodes and **${options.assetCount}** assets (${options.fileAssetCount} saved as files on disk). Use this data before UI implementation\u2014do not guess visuals.` : `FET \u5DF2\u540C\u6B65 **${options.nodeCount}** \u4E2A\u8282\u70B9\u3001**${options.assetCount}** \u4E2A\u7D20\u6750\uFF08\u5176\u4E2D **${options.fileAssetCount}** \u4E2A\u5927\u56FE\u5DF2\u843D\u76D8\uFF09\u3002\u5B9E\u65BD UI \u524D\u5FC5\u987B\u57FA\u4E8E\u8FD9\u4E9B\u6570\u636E\uFF0C\u7981\u6B62\u731C\u6D4B\u89C6\u89C9\u6548\u679C\u3002`;
|
|
4113
|
+
const readSteps = options.language === "en" ? `## Required reading order (before UI code)
|
|
4114
|
+
|
|
4115
|
+
1. \`${options.manifestPath}\` \u2014 node summaries, colors, typography, asset paths.
|
|
4116
|
+
2. \`${options.nodesPath}\` \u2014 full Figma node tree (JSON) for layout and nested structure.
|
|
4117
|
+
3. \`openspec/changes/${options.changeId}/.fet/figma-assets/\` \u2014 background images and exports (use file paths in code, not base64 for large assets).
|
|
4118
|
+
4. \`figma-apply-instructions.md\` and \`figma-stop.md\` in the same folder \u2014 guard rules.
|
|
4119
|
+
|
|
4120
|
+
## Asset usage in code
|
|
4121
|
+
|
|
4122
|
+
- **storage: file** \u2192 import or reference the \`relativePath\` under the project root (e.g. \`public/\` or \`src/assets/\` after copy).
|
|
4123
|
+
- **storage: inline** \u2192 small icons only; read \`.inline.json\` if needed.
|
|
4124
|
+
- Never embed multi\u2011hundred\u2011KB images as base64 in source.` : `## \u5B9E\u65BD UI \u524D\u5FC5\u8BFB\uFF08\u987A\u5E8F\uFF09
|
|
4125
|
+
|
|
4126
|
+
1. \`${options.manifestPath}\` \u2014 \u8282\u70B9\u6458\u8981\u3001\u989C\u8272\u3001\u5B57\u53F7\u3001\u7D20\u6750\u8DEF\u5F84\u3002
|
|
4127
|
+
2. \`${options.nodesPath}\` \u2014 \u5B8C\u6574 Figma \u8282\u70B9\u6811\uFF08JSON\uFF09\uFF0C\u7528\u4E8E\u5E03\u5C40\u4E0E\u5D4C\u5957\u7ED3\u6784\u3002
|
|
4128
|
+
3. \`openspec/changes/${options.changeId}/.fet/figma-assets/\` \u2014 \u80CC\u666F\u56FE\u4E0E\u5BFC\u51FA\u56FE\uFF08\u4EE3\u7801\u4E2D\u7528\u6587\u4EF6\u8DEF\u5F84\uFF0C\u5927\u56FE\u7981\u6B62 base64\uFF09\u3002
|
|
4129
|
+
4. \u540C\u76EE\u5F55 \`figma-apply-instructions.md\`\u3001\`figma-stop.md\` \u2014 \u5B88\u536B\u89C4\u5219\u3002
|
|
4130
|
+
|
|
4131
|
+
## \u4EE3\u7801\u4E2D\u5982\u4F55\u4F7F\u7528\u7D20\u6750
|
|
4132
|
+
|
|
4133
|
+
- **storage: file** \u2192 \u5C06 \`relativePath\` \u590D\u5236\u5230 \`public/\` \u6216 \`src/assets/\` \u540E\u5F15\u7528\u3002
|
|
4134
|
+
- **storage: inline** \u2192 \u4EC5\u7528\u4E8E\u5C0F\u56FE\u6807\uFF1B\u5FC5\u8981\u65F6\u8BFB\u53D6 \`.inline.json\`\u3002
|
|
4135
|
+
- \u7981\u6B62\u5728\u6E90\u7801\u4E2D\u5185\u8054\u6570\u767E KB \u4EE5\u4E0A\u7684 base64 \u56FE\u7247\u3002`;
|
|
4136
|
+
return `---
|
|
4137
|
+
schemaVersion: 1
|
|
4138
|
+
fetVersion: ${FET_VERSION}
|
|
4139
|
+
generatedAt: ${options.generatedAt}
|
|
4140
|
+
changeId: ${options.changeId}
|
|
4141
|
+
purpose: figma-sync
|
|
4142
|
+
---
|
|
4143
|
+
|
|
4144
|
+
# ${title}
|
|
4145
|
+
|
|
4146
|
+
${intro}
|
|
4147
|
+
|
|
4148
|
+
${readSteps}
|
|
4149
|
+
`;
|
|
4150
|
+
}
|
|
4151
|
+
function renderFigmaSyncNextSteps(changeId, language, apiKeyEnv) {
|
|
4152
|
+
if (language === "en") {
|
|
4153
|
+
return [
|
|
4154
|
+
`Run fet figma sync --change ${changeId} (set ${apiKeyEnv}) to fetch all Figma nodes and download assets before UI work.`,
|
|
4155
|
+
`Read openspec/changes/${changeId}/.fet/figma-sync-instructions.md and figma-design-manifest.yaml after sync.`
|
|
4156
|
+
];
|
|
4157
|
+
}
|
|
4158
|
+
return [
|
|
4159
|
+
`\u5B9E\u65BD UI \u524D\u6267\u884C fet figma sync --change ${changeId}\uFF08\u914D\u7F6E ${apiKeyEnv}\uFF09\uFF0C\u62C9\u53D6\u5168\u90E8\u8282\u70B9\u5E76\u4E0B\u8F7D\u7D20\u6750\u3002`,
|
|
4160
|
+
`\u540C\u6B65\u540E\u9605\u8BFB openspec/changes/${changeId}/.fet/figma-sync-instructions.md \u4E0E figma-design-manifest.yaml\u3002`
|
|
4161
|
+
];
|
|
4162
|
+
}
|
|
4163
|
+
|
|
4164
|
+
// src/figma/url.ts
|
|
4165
|
+
var FILE_KEY_PATTERN = /figma\.com\/(?:file|design|proto)\/([a-zA-Z0-9]+)/i;
|
|
4166
|
+
var NODE_ID_PATTERN = /[?&]node-id=([\d]+)-([\d]+)/i;
|
|
4167
|
+
function parseFigmaUrl(url) {
|
|
4168
|
+
const fileMatch = url.match(FILE_KEY_PATTERN);
|
|
4169
|
+
if (!fileMatch?.[1]) {
|
|
4170
|
+
return null;
|
|
4171
|
+
}
|
|
4172
|
+
const nodeMatch = url.match(NODE_ID_PATTERN);
|
|
4173
|
+
return {
|
|
4174
|
+
fileKey: fileMatch[1],
|
|
4175
|
+
nodeId: nodeMatch ? `${nodeMatch[1]}:${nodeMatch[2]}` : void 0,
|
|
4176
|
+
url
|
|
4177
|
+
};
|
|
4178
|
+
}
|
|
4179
|
+
function parseFigmaUrls(urls) {
|
|
4180
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4181
|
+
const parsed = [];
|
|
4182
|
+
for (const url of urls) {
|
|
4183
|
+
const item = parseFigmaUrl(url);
|
|
4184
|
+
if (!item) {
|
|
4185
|
+
continue;
|
|
4186
|
+
}
|
|
4187
|
+
const key = `${item.fileKey}:${item.nodeId ?? ""}`;
|
|
4188
|
+
if (seen.has(key)) {
|
|
4189
|
+
continue;
|
|
4190
|
+
}
|
|
4191
|
+
seen.add(key);
|
|
4192
|
+
parsed.push(item);
|
|
4193
|
+
}
|
|
4194
|
+
return parsed;
|
|
4195
|
+
}
|
|
4196
|
+
|
|
4197
|
+
// src/figma/walk.ts
|
|
4198
|
+
function rgbaFill(fill) {
|
|
4199
|
+
if (fill.type !== "SOLID" || !fill.color) {
|
|
4200
|
+
return null;
|
|
4201
|
+
}
|
|
4202
|
+
const { r, g, b, a = 1 } = fill.color;
|
|
4203
|
+
const alpha = Math.round(a * 100) / 100;
|
|
4204
|
+
return `rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${alpha})`;
|
|
4205
|
+
}
|
|
4206
|
+
function summarizeFills(fills) {
|
|
4207
|
+
if (!fills?.length) {
|
|
4208
|
+
return void 0;
|
|
4209
|
+
}
|
|
4210
|
+
const out = [];
|
|
4211
|
+
for (const fill of fills) {
|
|
4212
|
+
if (fill.visible === false) {
|
|
4213
|
+
continue;
|
|
4214
|
+
}
|
|
4215
|
+
if (fill.type === "IMAGE" && fill.imageRef) {
|
|
4216
|
+
out.push(`image:${fill.imageRef}`);
|
|
4217
|
+
} else {
|
|
4218
|
+
const color = rgbaFill(fill);
|
|
4219
|
+
if (color) {
|
|
4220
|
+
out.push(color);
|
|
4221
|
+
}
|
|
4222
|
+
}
|
|
4223
|
+
}
|
|
4224
|
+
return out.length ? out : void 0;
|
|
4225
|
+
}
|
|
4226
|
+
function summarizeNode(node) {
|
|
4227
|
+
const summary = {
|
|
4228
|
+
id: node.id,
|
|
4229
|
+
name: node.name,
|
|
4230
|
+
type: node.type,
|
|
4231
|
+
visible: node.visible !== false
|
|
4232
|
+
};
|
|
4233
|
+
if (node.absoluteBoundingBox) {
|
|
4234
|
+
summary.width = node.absoluteBoundingBox.width;
|
|
4235
|
+
summary.height = node.absoluteBoundingBox.height;
|
|
4236
|
+
}
|
|
4237
|
+
if (node.layoutMode) {
|
|
4238
|
+
summary.layoutMode = node.layoutMode;
|
|
4239
|
+
}
|
|
4240
|
+
const fills = summarizeFills(node.fills);
|
|
4241
|
+
if (fills) {
|
|
4242
|
+
summary.fills = fills;
|
|
4243
|
+
}
|
|
4244
|
+
if (node.characters) {
|
|
4245
|
+
summary.characters = node.characters.slice(0, 200);
|
|
4246
|
+
}
|
|
4247
|
+
if (node.style) {
|
|
4248
|
+
summary.textStyle = {
|
|
4249
|
+
fontFamily: node.style.fontFamily,
|
|
4250
|
+
fontSize: node.style.fontSize,
|
|
4251
|
+
fontWeight: node.style.fontWeight,
|
|
4252
|
+
lineHeight: node.style.lineHeightPx
|
|
4253
|
+
};
|
|
4254
|
+
}
|
|
4255
|
+
if (node.children?.length) {
|
|
4256
|
+
summary.children = node.children.map((child) => child.id);
|
|
4257
|
+
}
|
|
4258
|
+
return summary;
|
|
4259
|
+
}
|
|
4260
|
+
function walkFigmaDocument(root) {
|
|
4261
|
+
const nodes = [];
|
|
4262
|
+
const imageRefs = [];
|
|
4263
|
+
const exportNodeIds = [];
|
|
4264
|
+
const seenRefs = /* @__PURE__ */ new Set();
|
|
4265
|
+
function visit(node) {
|
|
4266
|
+
nodes.push(summarizeNode(node));
|
|
4267
|
+
for (const fill of node.fills ?? []) {
|
|
4268
|
+
if (fill.type === "IMAGE" && fill.imageRef && fill.visible !== false && !seenRefs.has(fill.imageRef)) {
|
|
4269
|
+
seenRefs.add(fill.imageRef);
|
|
4270
|
+
imageRefs.push({ ref: fill.imageRef, nodeId: node.id, name: node.name });
|
|
4271
|
+
}
|
|
4272
|
+
}
|
|
4273
|
+
const hasImageFill = node.fills?.some((fill) => fill.type === "IMAGE" && fill.visible !== false);
|
|
4274
|
+
if (hasImageFill && (node.type === "RECTANGLE" || node.type === "FRAME" || node.type === "COMPONENT" || node.type === "INSTANCE")) {
|
|
4275
|
+
exportNodeIds.push({ nodeId: node.id, name: node.name });
|
|
4276
|
+
}
|
|
4277
|
+
for (const child of node.children ?? []) {
|
|
4278
|
+
visit(child);
|
|
4279
|
+
}
|
|
4280
|
+
}
|
|
4281
|
+
visit(root);
|
|
4282
|
+
return { nodes, imageRefs, exportNodeIds };
|
|
4283
|
+
}
|
|
4284
|
+
|
|
4285
|
+
// src/figma/sync.ts
|
|
4286
|
+
async function syncFigmaAssets(options) {
|
|
4287
|
+
const syncConfig = await loadFigmaSyncConfig(options.projectRoot);
|
|
4288
|
+
if (!syncConfig.enabled) {
|
|
4289
|
+
return null;
|
|
4290
|
+
}
|
|
4291
|
+
const { urls, sources } = await collectFigmaUrlsFromChange(options.projectRoot, options.changeId);
|
|
4292
|
+
if (!urls.length) {
|
|
4293
|
+
throw new FetError({
|
|
4294
|
+
code: "INVALID_ARGUMENTS" /* InvalidArguments */,
|
|
4295
|
+
message: "No Figma URLs found in this change.",
|
|
4296
|
+
details: { changeId: options.changeId },
|
|
4297
|
+
recoverable: true,
|
|
4298
|
+
suggestedCommand: `Add a Figma link to openspec/changes/${options.changeId}/proposal.md or design.md`
|
|
4299
|
+
});
|
|
4300
|
+
}
|
|
4301
|
+
const token = resolveFigmaAccessToken(syncConfig, options.accessToken);
|
|
4302
|
+
if (!token) {
|
|
4303
|
+
throw new FetError({
|
|
4304
|
+
code: "INVALID_ARGUMENTS" /* InvalidArguments */,
|
|
4305
|
+
message: `Figma access token missing. Set ${syncConfig.apiKeyEnv} or pass --token.`,
|
|
4306
|
+
details: { apiKeyEnv: syncConfig.apiKeyEnv },
|
|
4307
|
+
recoverable: true,
|
|
4308
|
+
suggestedCommand: `FIGMA_ACCESS_TOKEN=*** fet figma sync --change ${options.changeId}`
|
|
4309
|
+
});
|
|
4310
|
+
}
|
|
4311
|
+
const parsed = parseFigmaUrls(urls);
|
|
4312
|
+
if (!parsed.length) {
|
|
4313
|
+
throw new FetError({
|
|
4314
|
+
code: "INVALID_ARGUMENTS" /* InvalidArguments */,
|
|
4315
|
+
message: "Could not parse any Figma file keys from the detected URLs.",
|
|
4316
|
+
details: { urls },
|
|
4317
|
+
recoverable: true
|
|
4318
|
+
});
|
|
4319
|
+
}
|
|
4320
|
+
const planningFingerprint = await computePlanningFingerprint(options.projectRoot, options.changeId);
|
|
4321
|
+
const existing = await readFigmaDesignManifest(options.projectRoot, options.changeId);
|
|
4322
|
+
if (!options.force && existing && existing.planningFingerprint === planningFingerprint && existing.figmaUrls.join(",") === urls.join(",")) {
|
|
4323
|
+
return {
|
|
4324
|
+
manifestPath: figmaDesignManifestRelativePath2(options.changeId),
|
|
4325
|
+
nodesPath: figmaNodesRelativePath(options.changeId),
|
|
4326
|
+
instructionsPath: figmaSyncInstructionsRelativePath2(options.changeId),
|
|
4327
|
+
assetsDir: join21("openspec", "changes", options.changeId, ".fet", "figma-assets"),
|
|
4328
|
+
manifest: existing,
|
|
4329
|
+
written: false
|
|
4330
|
+
};
|
|
4331
|
+
}
|
|
4332
|
+
if (options.plan) {
|
|
4333
|
+
return {
|
|
4334
|
+
manifestPath: figmaDesignManifestRelativePath2(options.changeId),
|
|
4335
|
+
nodesPath: figmaNodesRelativePath(options.changeId),
|
|
4336
|
+
instructionsPath: figmaSyncInstructionsRelativePath2(options.changeId),
|
|
4337
|
+
assetsDir: join21("openspec", "changes", options.changeId, ".fet", "figma-assets"),
|
|
4338
|
+
manifest: {
|
|
4339
|
+
schemaVersion: 1,
|
|
4340
|
+
fetVersion: FET_VERSION,
|
|
4341
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4342
|
+
changeId: options.changeId,
|
|
4343
|
+
planningFingerprint,
|
|
4344
|
+
figmaUrls: urls,
|
|
4345
|
+
figmaSources: sources,
|
|
4346
|
+
files: parsed.map((item) => ({
|
|
4347
|
+
fileKey: item.fileKey,
|
|
4348
|
+
url: item.url,
|
|
4349
|
+
rootNodeId: item.nodeId,
|
|
4350
|
+
nodeCount: 0,
|
|
4351
|
+
assetCount: 0
|
|
4352
|
+
})),
|
|
4353
|
+
nodes: [],
|
|
4354
|
+
assets: []
|
|
4355
|
+
},
|
|
4356
|
+
written: false
|
|
4357
|
+
};
|
|
4358
|
+
}
|
|
4359
|
+
const allNodes = [];
|
|
4360
|
+
const allAssets = [];
|
|
4361
|
+
const files = [];
|
|
4362
|
+
const nodesByFile = {};
|
|
4363
|
+
for (const item of parsed) {
|
|
4364
|
+
const file = await fetchFigmaFile(item.fileKey, token, item.nodeId ? [item.nodeId] : void 0);
|
|
4365
|
+
const walked = walkFigmaDocument(file.document);
|
|
4366
|
+
allNodes.push(...walked.nodes);
|
|
4367
|
+
nodesByFile[item.fileKey] = {
|
|
4368
|
+
name: file.name,
|
|
4369
|
+
rootNodeId: item.nodeId ?? file.document.id,
|
|
4370
|
+
document: file.document,
|
|
4371
|
+
components: file.components ?? {},
|
|
4372
|
+
styles: file.styles ?? {}
|
|
4373
|
+
};
|
|
4374
|
+
const assets = await downloadFigmaAssets({
|
|
4375
|
+
projectRoot: options.projectRoot,
|
|
4376
|
+
changeId: options.changeId,
|
|
4377
|
+
fileKey: item.fileKey,
|
|
4378
|
+
accessToken: token,
|
|
4379
|
+
config: syncConfig,
|
|
4380
|
+
imageRefs: walked.imageRefs,
|
|
4381
|
+
exportNodeIds: walked.exportNodeIds.slice(0, 20)
|
|
4382
|
+
});
|
|
4383
|
+
allAssets.push(...assets);
|
|
4384
|
+
files.push({
|
|
4385
|
+
fileKey: item.fileKey,
|
|
4386
|
+
url: item.url,
|
|
4387
|
+
rootNodeId: item.nodeId,
|
|
4388
|
+
nodeCount: walked.nodes.length,
|
|
4389
|
+
assetCount: assets.length
|
|
4390
|
+
});
|
|
4391
|
+
}
|
|
4392
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4393
|
+
const manifest = {
|
|
4394
|
+
schemaVersion: 1,
|
|
4395
|
+
fetVersion: FET_VERSION,
|
|
4396
|
+
generatedAt,
|
|
4397
|
+
changeId: options.changeId,
|
|
4398
|
+
planningFingerprint,
|
|
4399
|
+
figmaUrls: urls,
|
|
4400
|
+
figmaSources: sources,
|
|
4401
|
+
files,
|
|
4402
|
+
nodes: allNodes,
|
|
4403
|
+
assets: allAssets
|
|
4404
|
+
};
|
|
4405
|
+
const fetDir = join21(options.projectRoot, "openspec", "changes", options.changeId, ".fet");
|
|
4406
|
+
await mkdir7(fetDir, { recursive: true });
|
|
4407
|
+
const manifestPath3 = figmaDesignManifestRelativePath2(options.changeId);
|
|
4408
|
+
const nodesPath = figmaNodesRelativePath(options.changeId);
|
|
4409
|
+
const instructionsPath = figmaSyncInstructionsRelativePath2(options.changeId);
|
|
4410
|
+
await atomicWrite(join21(options.projectRoot, manifestPath3), renderFigmaDesignManifestYaml(manifest));
|
|
4411
|
+
await writeFile2(join21(options.projectRoot, nodesPath), JSON.stringify(nodesByFile, null, 2), "utf8");
|
|
4412
|
+
await atomicWrite(
|
|
4413
|
+
join21(options.projectRoot, instructionsPath),
|
|
4414
|
+
renderFigmaSyncInstructions({
|
|
4415
|
+
changeId: options.changeId,
|
|
4416
|
+
generatedAt,
|
|
4417
|
+
manifestPath: manifestPath3,
|
|
4418
|
+
nodesPath,
|
|
4419
|
+
language: options.language,
|
|
4420
|
+
nodeCount: allNodes.length,
|
|
4421
|
+
assetCount: allAssets.length,
|
|
4422
|
+
fileAssetCount: allAssets.filter((asset) => asset.storage === "file").length
|
|
4423
|
+
})
|
|
4424
|
+
);
|
|
4425
|
+
return {
|
|
4426
|
+
manifestPath: manifestPath3,
|
|
4427
|
+
nodesPath,
|
|
4428
|
+
instructionsPath,
|
|
4429
|
+
assetsDir: join21("openspec", "changes", options.changeId, ".fet", "figma-assets"),
|
|
4430
|
+
manifest,
|
|
4431
|
+
written: true
|
|
4432
|
+
};
|
|
4433
|
+
}
|
|
4434
|
+
async function tryAutoSyncFigmaAssets(options) {
|
|
4435
|
+
const syncConfig = await loadFigmaSyncConfig(options.projectRoot);
|
|
4436
|
+
if (!syncConfig.enabled) {
|
|
4437
|
+
return { synced: false, skipped: true, missingToken: false };
|
|
4438
|
+
}
|
|
4439
|
+
const { urls } = await collectFigmaUrlsFromChange(options.projectRoot, options.changeId);
|
|
4440
|
+
if (!urls.length) {
|
|
4441
|
+
return { synced: false, skipped: true, missingToken: false };
|
|
4442
|
+
}
|
|
4443
|
+
const token = resolveFigmaAccessToken(syncConfig);
|
|
4444
|
+
if (!token) {
|
|
4445
|
+
return { synced: false, skipped: false, missingToken: true };
|
|
4446
|
+
}
|
|
4447
|
+
const existing = await readFigmaDesignManifest(options.projectRoot, options.changeId);
|
|
4448
|
+
const planningFingerprint = await computePlanningFingerprint(options.projectRoot, options.changeId);
|
|
4449
|
+
if (existing && existing.planningFingerprint === planningFingerprint && existing.figmaUrls.join(",") === urls.join(",")) {
|
|
4450
|
+
return {
|
|
4451
|
+
synced: false,
|
|
4452
|
+
skipped: true,
|
|
4453
|
+
missingToken: false,
|
|
4454
|
+
result: {
|
|
4455
|
+
manifestPath: figmaDesignManifestRelativePath2(options.changeId),
|
|
4456
|
+
nodesPath: figmaNodesRelativePath(options.changeId),
|
|
4457
|
+
instructionsPath: figmaSyncInstructionsRelativePath2(options.changeId),
|
|
4458
|
+
assetsDir: join21("openspec", "changes", options.changeId, ".fet", "figma-assets"),
|
|
4459
|
+
manifest: existing,
|
|
4460
|
+
written: false
|
|
4461
|
+
}
|
|
4462
|
+
};
|
|
4463
|
+
}
|
|
4464
|
+
const result = await syncFigmaAssets({
|
|
4465
|
+
projectRoot: options.projectRoot,
|
|
4466
|
+
changeId: options.changeId,
|
|
4467
|
+
language: options.language,
|
|
4468
|
+
accessToken: token
|
|
4469
|
+
});
|
|
4470
|
+
return { synced: Boolean(result?.written), skipped: false, missingToken: false, result: result ?? void 0 };
|
|
4471
|
+
}
|
|
4472
|
+
|
|
3704
4473
|
// src/ui-display-contract.ts
|
|
3705
4474
|
import { existsSync as existsSync2 } from "fs";
|
|
3706
|
-
import { readdir as
|
|
3707
|
-
import { join as
|
|
3708
|
-
import { parse as parse3, parseDocument as
|
|
4475
|
+
import { readdir as readdir5, readFile as readFile14, stat as stat9 } from "fs/promises";
|
|
4476
|
+
import { join as join22, relative as relative4 } from "path";
|
|
4477
|
+
import { parse as parse3, parseDocument as parseDocument5 } from "yaml";
|
|
3709
4478
|
var DEFAULT_CONFIG2 = {
|
|
3710
4479
|
enabled: true
|
|
3711
4480
|
};
|
|
@@ -3715,8 +4484,8 @@ var BACKTICK_PATH_PATTERN = /`([^`]+\.(?:ya?ml|json))`/gi;
|
|
|
3715
4484
|
var OPENAPI_BARE_PATTERN = /\b(openapi\.ya?ml|swagger\.ya?ml|swagger\.json)\b/gi;
|
|
3716
4485
|
async function loadUiDisplayContractConfig(projectRoot) {
|
|
3717
4486
|
try {
|
|
3718
|
-
const raw = await
|
|
3719
|
-
const doc =
|
|
4487
|
+
const raw = await readFile14(join22(projectRoot, "openspec", "config.yaml"), "utf8");
|
|
4488
|
+
const doc = parseDocument5(raw);
|
|
3720
4489
|
const fetNode = doc.get("fet", true);
|
|
3721
4490
|
const node = fetNode?.get?.("uiDisplayContract");
|
|
3722
4491
|
if (!node || typeof node.get !== "function") {
|
|
@@ -3731,12 +4500,12 @@ async function loadUiDisplayContractConfig(projectRoot) {
|
|
|
3731
4500
|
}
|
|
3732
4501
|
}
|
|
3733
4502
|
async function collectApiSourcesFromChange(projectRoot, changeId) {
|
|
3734
|
-
const changePath =
|
|
4503
|
+
const changePath = join22(projectRoot, "openspec", "changes", changeId);
|
|
3735
4504
|
const paths = /* @__PURE__ */ new Set();
|
|
3736
4505
|
const sources = [];
|
|
3737
4506
|
const candidates = ["proposal.md", "tasks.md", "design.md"];
|
|
3738
4507
|
for (const name of candidates) {
|
|
3739
|
-
const filePath =
|
|
4508
|
+
const filePath = join22(changePath, name);
|
|
3740
4509
|
const content = await readOptional4(filePath);
|
|
3741
4510
|
if (!content) {
|
|
3742
4511
|
continue;
|
|
@@ -3749,7 +4518,7 @@ async function collectApiSourcesFromChange(projectRoot, changeId) {
|
|
|
3749
4518
|
}
|
|
3750
4519
|
}
|
|
3751
4520
|
}
|
|
3752
|
-
const specsPath =
|
|
4521
|
+
const specsPath = join22(changePath, "specs");
|
|
3753
4522
|
for (const filePath of await listMarkdownFiles2(specsPath)) {
|
|
3754
4523
|
const content = await readOptional4(filePath);
|
|
3755
4524
|
if (!content) {
|
|
@@ -3757,7 +4526,7 @@ async function collectApiSourcesFromChange(projectRoot, changeId) {
|
|
|
3757
4526
|
}
|
|
3758
4527
|
const found = extractApiDocPaths(content, projectRoot);
|
|
3759
4528
|
if (found.length) {
|
|
3760
|
-
sources.push(
|
|
4529
|
+
sources.push(relative4(projectRoot, filePath).replaceAll("\\", "/"));
|
|
3761
4530
|
for (const path of found) {
|
|
3762
4531
|
paths.add(path);
|
|
3763
4532
|
}
|
|
@@ -3783,7 +4552,7 @@ function extractApiDocPaths(content, projectRoot) {
|
|
|
3783
4552
|
const common = ["openapi.yaml", "openapi.yml", "docs/openapi.yaml", "swagger.yaml", "swagger.json"];
|
|
3784
4553
|
for (const candidate of common) {
|
|
3785
4554
|
const normalized = normalizeRepoPath(candidate, projectRoot);
|
|
3786
|
-
if (normalized && existsSync2(
|
|
4555
|
+
if (normalized && existsSync2(join22(projectRoot, normalized))) {
|
|
3787
4556
|
found.add(normalized);
|
|
3788
4557
|
}
|
|
3789
4558
|
}
|
|
@@ -3793,7 +4562,7 @@ function extractApiDocPaths(content, projectRoot) {
|
|
|
3793
4562
|
async function extractOpenApiSchemas(projectRoot, relativePaths) {
|
|
3794
4563
|
const schemas = [];
|
|
3795
4564
|
for (const rel of relativePaths) {
|
|
3796
|
-
const absolute =
|
|
4565
|
+
const absolute = join22(projectRoot, rel);
|
|
3797
4566
|
const content = await readOptional4(absolute);
|
|
3798
4567
|
if (!content) {
|
|
3799
4568
|
continue;
|
|
@@ -3869,9 +4638,9 @@ async function ensureChangeUiDisplayContract(options) {
|
|
|
3869
4638
|
if (!config.enabled) {
|
|
3870
4639
|
return null;
|
|
3871
4640
|
}
|
|
3872
|
-
const
|
|
4641
|
+
const figma2 = await collectFigmaUrlsFromChange(options.projectRoot, options.changeId);
|
|
3873
4642
|
const api = await collectApiSourcesFromChange(options.projectRoot, options.changeId);
|
|
3874
|
-
const hasFigma =
|
|
4643
|
+
const hasFigma = figma2.urls.length > 0;
|
|
3875
4644
|
const hasApi = api.paths.length > 0;
|
|
3876
4645
|
if (!hasFigma && !hasApi) {
|
|
3877
4646
|
return null;
|
|
@@ -3884,14 +4653,14 @@ async function ensureChangeUiDisplayContract(options) {
|
|
|
3884
4653
|
changeId: options.changeId,
|
|
3885
4654
|
generatedAt,
|
|
3886
4655
|
fetVersion: options.fetVersion,
|
|
3887
|
-
figmaUrls:
|
|
3888
|
-
figmaSources:
|
|
4656
|
+
figmaUrls: figma2.urls,
|
|
4657
|
+
figmaSources: figma2.sources,
|
|
3889
4658
|
apiPaths: api.paths,
|
|
3890
4659
|
apiSources: api.sources,
|
|
3891
4660
|
apiSchemas
|
|
3892
4661
|
});
|
|
3893
4662
|
const contractContent = renderUiDisplayContractYaml(doc);
|
|
3894
|
-
const contractAbsolutePath =
|
|
4663
|
+
const contractAbsolutePath = join22(options.projectRoot, contractRelativePath);
|
|
3895
4664
|
const existingContract = await readOptional4(contractAbsolutePath);
|
|
3896
4665
|
let written = false;
|
|
3897
4666
|
if (existingContract !== contractContent) {
|
|
@@ -3906,7 +4675,7 @@ async function ensureChangeUiDisplayContract(options) {
|
|
|
3906
4675
|
hasFigma,
|
|
3907
4676
|
hasApi
|
|
3908
4677
|
});
|
|
3909
|
-
const applyAbsolutePath =
|
|
4678
|
+
const applyAbsolutePath = join22(options.projectRoot, applyRelativePath);
|
|
3910
4679
|
const existingApply = await readOptional4(applyAbsolutePath);
|
|
3911
4680
|
if (existingApply !== applyContent) {
|
|
3912
4681
|
await atomicWrite(applyAbsolutePath, applyContent);
|
|
@@ -3973,9 +4742,9 @@ async function listMarkdownFiles2(root) {
|
|
|
3973
4742
|
}
|
|
3974
4743
|
async function walk2(dir, files) {
|
|
3975
4744
|
try {
|
|
3976
|
-
const entries = await
|
|
4745
|
+
const entries = await readdir5(dir, { withFileTypes: true });
|
|
3977
4746
|
for (const entry of entries) {
|
|
3978
|
-
const fullPath =
|
|
4747
|
+
const fullPath = join22(dir, entry.name);
|
|
3979
4748
|
if (entry.isDirectory()) {
|
|
3980
4749
|
await walk2(fullPath, files);
|
|
3981
4750
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
@@ -3988,8 +4757,8 @@ async function walk2(dir, files) {
|
|
|
3988
4757
|
}
|
|
3989
4758
|
async function readOptional4(path) {
|
|
3990
4759
|
try {
|
|
3991
|
-
await
|
|
3992
|
-
return await
|
|
4760
|
+
await stat9(path);
|
|
4761
|
+
return await readFile14(path, "utf8");
|
|
3993
4762
|
} catch {
|
|
3994
4763
|
return null;
|
|
3995
4764
|
}
|
|
@@ -4034,9 +4803,9 @@ async function assertChangeExists(ctx, changeId) {
|
|
|
4034
4803
|
}
|
|
4035
4804
|
|
|
4036
4805
|
// src/tdd/config.ts
|
|
4037
|
-
import { readFile as
|
|
4038
|
-
import { join as
|
|
4039
|
-
import { parseDocument as
|
|
4806
|
+
import { readFile as readFile15 } from "fs/promises";
|
|
4807
|
+
import { join as join23 } from "path";
|
|
4808
|
+
import { parseDocument as parseDocument6 } from "yaml";
|
|
4040
4809
|
var DEFAULT_CONFIG3 = {
|
|
4041
4810
|
enabled: true,
|
|
4042
4811
|
mode: "require_before_apply",
|
|
@@ -4044,8 +4813,8 @@ var DEFAULT_CONFIG3 = {
|
|
|
4044
4813
|
};
|
|
4045
4814
|
async function loadTddConfig(projectRoot) {
|
|
4046
4815
|
try {
|
|
4047
|
-
const raw = await
|
|
4048
|
-
const doc =
|
|
4816
|
+
const raw = await readFile15(join23(projectRoot, "openspec", "config.yaml"), "utf8");
|
|
4817
|
+
const doc = parseDocument6(raw);
|
|
4049
4818
|
const fetNode = doc.get("fet", true);
|
|
4050
4819
|
const node = fetNode?.get?.("tdd");
|
|
4051
4820
|
if (!node || typeof node.get !== "function") {
|
|
@@ -4079,70 +4848,12 @@ function isTddRequired(config) {
|
|
|
4079
4848
|
return config.enabled && config.mode === "require_before_apply";
|
|
4080
4849
|
}
|
|
4081
4850
|
|
|
4082
|
-
// src/tdd/fingerprint.ts
|
|
4083
|
-
import { createHash } from "crypto";
|
|
4084
|
-
import { readdir as readdir5, readFile as readFile13, stat as stat9 } from "fs/promises";
|
|
4085
|
-
import { join as join19, relative as relative4 } from "path";
|
|
4086
|
-
async function collectPlanningSources(projectRoot, changeId) {
|
|
4087
|
-
const changeRoot = join19(projectRoot, "openspec", "changes", changeId);
|
|
4088
|
-
const sources = [];
|
|
4089
|
-
const rootFiles = ["proposal.md", "tasks.md", "design.md"];
|
|
4090
|
-
for (const name of rootFiles) {
|
|
4091
|
-
const path = join19(changeRoot, name);
|
|
4092
|
-
if (await exists5(path)) {
|
|
4093
|
-
sources.push(relative4(projectRoot, path).replace(/\\/g, "/"));
|
|
4094
|
-
}
|
|
4095
|
-
}
|
|
4096
|
-
const specsDir = join19(changeRoot, "specs");
|
|
4097
|
-
if (await exists5(specsDir)) {
|
|
4098
|
-
for (const file of await walkFiles(specsDir)) {
|
|
4099
|
-
if (file.endsWith(".md")) {
|
|
4100
|
-
sources.push(relative4(projectRoot, file).replace(/\\/g, "/"));
|
|
4101
|
-
}
|
|
4102
|
-
}
|
|
4103
|
-
}
|
|
4104
|
-
return sources.sort();
|
|
4105
|
-
}
|
|
4106
|
-
async function computePlanningFingerprint(projectRoot, changeId) {
|
|
4107
|
-
const sources = await collectPlanningSources(projectRoot, changeId);
|
|
4108
|
-
const hash = createHash("sha256");
|
|
4109
|
-
for (const source of sources) {
|
|
4110
|
-
const content = await readFile13(join19(projectRoot, source), "utf8");
|
|
4111
|
-
hash.update(source);
|
|
4112
|
-
hash.update("\0");
|
|
4113
|
-
hash.update(content);
|
|
4114
|
-
hash.update("\0");
|
|
4115
|
-
}
|
|
4116
|
-
return `sha256:${hash.digest("hex")}`;
|
|
4117
|
-
}
|
|
4118
|
-
async function walkFiles(dir) {
|
|
4119
|
-
const entries = await readdir5(dir, { withFileTypes: true });
|
|
4120
|
-
const files = [];
|
|
4121
|
-
for (const entry of entries) {
|
|
4122
|
-
const path = join19(dir, entry.name);
|
|
4123
|
-
if (entry.isDirectory()) {
|
|
4124
|
-
files.push(...await walkFiles(path));
|
|
4125
|
-
} else if (entry.isFile()) {
|
|
4126
|
-
files.push(path);
|
|
4127
|
-
}
|
|
4128
|
-
}
|
|
4129
|
-
return files;
|
|
4130
|
-
}
|
|
4131
|
-
async function exists5(path) {
|
|
4132
|
-
try {
|
|
4133
|
-
await stat9(path);
|
|
4134
|
-
return true;
|
|
4135
|
-
} catch {
|
|
4136
|
-
return false;
|
|
4137
|
-
}
|
|
4138
|
-
}
|
|
4139
|
-
|
|
4140
4851
|
// src/tdd/manifest.ts
|
|
4141
|
-
import { mkdir as
|
|
4142
|
-
import { dirname as dirname8, join as
|
|
4143
|
-
import { parse as parse4, stringify as
|
|
4852
|
+
import { mkdir as mkdir8, readFile as readFile16, stat as stat10 } from "fs/promises";
|
|
4853
|
+
import { dirname as dirname8, join as join24 } from "path";
|
|
4854
|
+
import { parse as parse4, stringify as stringify4 } from "yaml";
|
|
4144
4855
|
function tddManifestPath(projectRoot, changeId) {
|
|
4145
|
-
return
|
|
4856
|
+
return join24(projectRoot, tddManifestRelativePath(changeId));
|
|
4146
4857
|
}
|
|
4147
4858
|
async function readTddManifest(projectRoot, changeId) {
|
|
4148
4859
|
const path = tddManifestPath(projectRoot, changeId);
|
|
@@ -4151,7 +4862,7 @@ async function readTddManifest(projectRoot, changeId) {
|
|
|
4151
4862
|
} catch {
|
|
4152
4863
|
return null;
|
|
4153
4864
|
}
|
|
4154
|
-
const doc = parse4(await
|
|
4865
|
+
const doc = parse4(await readFile16(path, "utf8"));
|
|
4155
4866
|
if (!doc || doc.schemaVersion !== 1 || doc.changeId !== changeId) {
|
|
4156
4867
|
return null;
|
|
4157
4868
|
}
|
|
@@ -4159,15 +4870,15 @@ async function readTddManifest(projectRoot, changeId) {
|
|
|
4159
4870
|
}
|
|
4160
4871
|
async function writeTddManifest(projectRoot, manifest) {
|
|
4161
4872
|
const relative6 = tddManifestRelativePath(manifest.changeId);
|
|
4162
|
-
const path =
|
|
4163
|
-
await
|
|
4164
|
-
await atomicWrite(path,
|
|
4873
|
+
const path = join24(projectRoot, relative6);
|
|
4874
|
+
await mkdir8(dirname8(path), { recursive: true });
|
|
4875
|
+
await atomicWrite(path, stringify4(manifest));
|
|
4165
4876
|
return relative6;
|
|
4166
4877
|
}
|
|
4167
4878
|
async function writeTddResults(projectRoot, results) {
|
|
4168
4879
|
const relative6 = tddResultsRelativePath(results.changeId);
|
|
4169
|
-
const path =
|
|
4170
|
-
await
|
|
4880
|
+
const path = join24(projectRoot, relative6);
|
|
4881
|
+
await mkdir8(dirname8(path), { recursive: true });
|
|
4171
4882
|
await atomicWrite(path, `${JSON.stringify(results, null, 2)}
|
|
4172
4883
|
`);
|
|
4173
4884
|
return relative6;
|
|
@@ -4275,8 +4986,8 @@ async function git(cwd, args) {
|
|
|
4275
4986
|
}
|
|
4276
4987
|
|
|
4277
4988
|
// src/state/store.ts
|
|
4278
|
-
import { mkdir as
|
|
4279
|
-
import { join as
|
|
4989
|
+
import { mkdir as mkdir9, readFile as readFile17 } from "fs/promises";
|
|
4990
|
+
import { join as join25 } from "path";
|
|
4280
4991
|
|
|
4281
4992
|
// src/language.ts
|
|
4282
4993
|
var DEFAULT_LANGUAGE = "zh-CN";
|
|
@@ -4398,7 +5109,7 @@ var StateStore = class {
|
|
|
4398
5109
|
project;
|
|
4399
5110
|
async readGlobal() {
|
|
4400
5111
|
try {
|
|
4401
|
-
const value = JSON.parse(await
|
|
5112
|
+
const value = JSON.parse(await readFile17(this.globalPath(), "utf8"));
|
|
4402
5113
|
assertGlobalState(value);
|
|
4403
5114
|
return value;
|
|
4404
5115
|
} catch (error) {
|
|
@@ -4413,13 +5124,13 @@ var StateStore = class {
|
|
|
4413
5124
|
}
|
|
4414
5125
|
async writeGlobal(state) {
|
|
4415
5126
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4416
|
-
await
|
|
5127
|
+
await mkdir9(join25(this.projectRoot, "openspec"), { recursive: true });
|
|
4417
5128
|
await atomicWrite(this.globalPath(), `${JSON.stringify(state, null, 2)}
|
|
4418
5129
|
`);
|
|
4419
5130
|
}
|
|
4420
5131
|
async readChange(changeId) {
|
|
4421
5132
|
try {
|
|
4422
|
-
const value = JSON.parse(await
|
|
5133
|
+
const value = JSON.parse(await readFile17(this.changePath(changeId), "utf8"));
|
|
4423
5134
|
assertChangeState(value);
|
|
4424
5135
|
return value;
|
|
4425
5136
|
} catch (error) {
|
|
@@ -4434,15 +5145,15 @@ var StateStore = class {
|
|
|
4434
5145
|
}
|
|
4435
5146
|
async writeChange(state) {
|
|
4436
5147
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4437
|
-
await
|
|
5148
|
+
await mkdir9(join25(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
|
|
4438
5149
|
await atomicWrite(this.changePath(state.changeId), `${JSON.stringify(state, null, 2)}
|
|
4439
5150
|
`);
|
|
4440
5151
|
}
|
|
4441
5152
|
globalPath() {
|
|
4442
|
-
return
|
|
5153
|
+
return join25(this.projectRoot, "openspec", "fet-state.json");
|
|
4443
5154
|
}
|
|
4444
5155
|
changePath(changeId) {
|
|
4445
|
-
return
|
|
5156
|
+
return join25(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
|
|
4446
5157
|
}
|
|
4447
5158
|
};
|
|
4448
5159
|
function isNotFound(error) {
|
|
@@ -4450,11 +5161,11 @@ function isNotFound(error) {
|
|
|
4450
5161
|
}
|
|
4451
5162
|
|
|
4452
5163
|
// src/state/tasks.ts
|
|
4453
|
-
import { readFile as
|
|
5164
|
+
import { readFile as readFile18 } from "fs/promises";
|
|
4454
5165
|
async function readCompletedTaskIds(tasksPath) {
|
|
4455
5166
|
let content;
|
|
4456
5167
|
try {
|
|
4457
|
-
content = await
|
|
5168
|
+
content = await readFile18(tasksPath, "utf8");
|
|
4458
5169
|
} catch {
|
|
4459
5170
|
return [];
|
|
4460
5171
|
}
|
|
@@ -4617,6 +5328,8 @@ async function applyWorkflowCommand(ctx, args) {
|
|
|
4617
5328
|
fetVersion: ctx.fetVersion
|
|
4618
5329
|
})
|
|
4619
5330
|
]);
|
|
5331
|
+
const figmaSyncConfig = await loadFigmaSyncConfig(ctx.projectRoot);
|
|
5332
|
+
const figmaAutoSync = figmaGuard ? await tryAutoSyncFigmaAssets({ projectRoot: ctx.projectRoot, changeId, language: ctx.language }) : null;
|
|
4620
5333
|
const applyNextSteps = [
|
|
4621
5334
|
`Read openspec/changes/${changeId}/tasks.md and the instructions output.`,
|
|
4622
5335
|
"Implement pending tasks and update task checkboxes only after the work is done.",
|
|
@@ -4627,7 +5340,13 @@ async function applyWorkflowCommand(ctx, args) {
|
|
|
4627
5340
|
applyNextSteps.unshift(...renderUiDisplayContractApplyNextSteps(changeId, ctx.language));
|
|
4628
5341
|
}
|
|
4629
5342
|
if (figmaGuard) {
|
|
4630
|
-
applyNextSteps.unshift(
|
|
5343
|
+
applyNextSteps.unshift(
|
|
5344
|
+
...renderFigmaApplyNextSteps(changeId, ctx.language, figmaGuard.mode, {
|
|
5345
|
+
syncMissingToken: figmaAutoSync?.missingToken,
|
|
5346
|
+
syncDone: figmaAutoSync?.synced || figmaAutoSync?.skipped && Boolean(figmaAutoSync.result),
|
|
5347
|
+
apiKeyEnv: figmaSyncConfig.apiKeyEnv
|
|
5348
|
+
})
|
|
5349
|
+
);
|
|
4631
5350
|
applyNextSteps.splice(applyNextSteps.length - 1, 0, ...renderVisualVerifyNextSteps(changeId, ctx.language));
|
|
4632
5351
|
}
|
|
4633
5352
|
ctx.output.result({
|
|
@@ -4642,6 +5361,7 @@ async function applyWorkflowCommand(ctx, args) {
|
|
|
4642
5361
|
status,
|
|
4643
5362
|
graphContext: runState.graphContext,
|
|
4644
5363
|
figmaGuard: figmaGuard ?? void 0,
|
|
5364
|
+
figmaSync: figmaAutoSync ?? void 0,
|
|
4645
5365
|
uiDisplayContract: uiContract ?? void 0
|
|
4646
5366
|
}
|
|
4647
5367
|
});
|
|
@@ -5040,7 +5760,7 @@ async function createChangelogEntry(projectRoot, changeId) {
|
|
|
5040
5760
|
};
|
|
5041
5761
|
}
|
|
5042
5762
|
async function appendChangelog(projectRoot, entry) {
|
|
5043
|
-
const changelogPath =
|
|
5763
|
+
const changelogPath = join26(projectRoot, "CHANGELOG.md");
|
|
5044
5764
|
const existing = await readOptional5(changelogPath);
|
|
5045
5765
|
const legacyContentLabel = "\u66F4\u65B0\u5185\u5BB9";
|
|
5046
5766
|
const block = `updateTime: ${entry.updateTime}
|
|
@@ -5053,12 +5773,12 @@ ${block}` : block;
|
|
|
5053
5773
|
await atomicWrite(changelogPath, next);
|
|
5054
5774
|
}
|
|
5055
5775
|
async function readChangeRequirement(projectRoot, changeId) {
|
|
5056
|
-
const changeRoot =
|
|
5057
|
-
const proposal = await readOptional5(
|
|
5776
|
+
const changeRoot = join26(projectRoot, "openspec", "changes", changeId);
|
|
5777
|
+
const proposal = await readOptional5(join26(changeRoot, "proposal.md"));
|
|
5058
5778
|
if (proposal) {
|
|
5059
5779
|
return summarizeMarkdown(proposal);
|
|
5060
5780
|
}
|
|
5061
|
-
const readme = await readOptional5(
|
|
5781
|
+
const readme = await readOptional5(join26(changeRoot, "README.md"));
|
|
5062
5782
|
if (readme) {
|
|
5063
5783
|
return summarizeMarkdown(readme);
|
|
5064
5784
|
}
|
|
@@ -5070,7 +5790,7 @@ function summarizeMarkdown(content) {
|
|
|
5070
5790
|
}
|
|
5071
5791
|
async function readOptional5(path) {
|
|
5072
5792
|
try {
|
|
5073
|
-
return await
|
|
5793
|
+
return await readFile19(path, "utf8");
|
|
5074
5794
|
} catch {
|
|
5075
5795
|
return null;
|
|
5076
5796
|
}
|
|
@@ -5425,17 +6145,17 @@ async function updateCommand(ctx) {
|
|
|
5425
6145
|
}
|
|
5426
6146
|
|
|
5427
6147
|
// src/commands/tdd.ts
|
|
5428
|
-
import { mkdir as
|
|
5429
|
-
import { join as
|
|
6148
|
+
import { mkdir as mkdir10 } from "fs/promises";
|
|
6149
|
+
import { join as join28 } from "path";
|
|
5430
6150
|
|
|
5431
6151
|
// src/tdd/extract-cases.ts
|
|
5432
|
-
import { readFile as
|
|
5433
|
-
import { join as
|
|
6152
|
+
import { readFile as readFile20 } from "fs/promises";
|
|
6153
|
+
import { join as join27 } from "path";
|
|
5434
6154
|
async function extractCasesFromChange(projectRoot, changeId, sources) {
|
|
5435
6155
|
const cases = [];
|
|
5436
6156
|
const seen = /* @__PURE__ */ new Set();
|
|
5437
6157
|
for (const source of sources) {
|
|
5438
|
-
const content = await
|
|
6158
|
+
const content = await readFile20(join27(projectRoot, source), "utf8");
|
|
5439
6159
|
if (source.endsWith("tasks.md")) {
|
|
5440
6160
|
for (const item of extractTaskCases(content, changeId)) {
|
|
5441
6161
|
if (!seen.has(item.id)) {
|
|
@@ -5569,12 +6289,12 @@ async function tddCommand(ctx) {
|
|
|
5569
6289
|
testCommand: testCommand2
|
|
5570
6290
|
});
|
|
5571
6291
|
const manifestPath3 = await writeTddManifest(ctx.projectRoot, manifest);
|
|
5572
|
-
const fetDir =
|
|
5573
|
-
await
|
|
6292
|
+
const fetDir = join28(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
|
|
6293
|
+
await mkdir10(fetDir, { recursive: true });
|
|
5574
6294
|
const instructionsPath = tddInstructionsRelativePath(changeId);
|
|
5575
6295
|
const specPath = tddSpecRelativePath(changeId);
|
|
5576
|
-
await atomicWrite(
|
|
5577
|
-
await atomicWrite(
|
|
6296
|
+
await atomicWrite(join28(ctx.projectRoot, instructionsPath), renderTddInstructions(changeId, manifest, ctx.language));
|
|
6297
|
+
await atomicWrite(join28(ctx.projectRoot, specPath), renderTddSpec(changeId, manifest, ctx.language));
|
|
5578
6298
|
const changeState = await ctx.stateStore.getOrCreateChange(changeId, "implement");
|
|
5579
6299
|
changeState.tdd = {
|
|
5580
6300
|
status: "ready",
|
|
@@ -5816,14 +6536,81 @@ function truncate(value, max = 2e3) {
|
|
|
5816
6536
|
return value.length > max ? `${value.slice(0, max)}\u2026` : value;
|
|
5817
6537
|
}
|
|
5818
6538
|
|
|
6539
|
+
// src/commands/figma.ts
|
|
6540
|
+
import { mkdir as mkdir11 } from "fs/promises";
|
|
6541
|
+
import { join as join29 } from "path";
|
|
6542
|
+
async function figmaSyncCommand(ctx, options) {
|
|
6543
|
+
await withProjectLock(ctx.projectRoot, { command: "figma", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
|
|
6544
|
+
const changeId = await resolveChangeId(ctx);
|
|
6545
|
+
await assertChangeExists(ctx, changeId);
|
|
6546
|
+
const syncConfig = await loadFigmaSyncConfig(ctx.projectRoot);
|
|
6547
|
+
const result = await syncFigmaAssets({
|
|
6548
|
+
projectRoot: ctx.projectRoot,
|
|
6549
|
+
changeId,
|
|
6550
|
+
language: ctx.language,
|
|
6551
|
+
accessToken: options.token,
|
|
6552
|
+
force: Boolean(options.force),
|
|
6553
|
+
plan: Boolean(options.plan)
|
|
6554
|
+
});
|
|
6555
|
+
if (!result) {
|
|
6556
|
+
ctx.output.result({
|
|
6557
|
+
ok: true,
|
|
6558
|
+
command: "figma",
|
|
6559
|
+
summary: msg(ctx.language, "Figma \u540C\u6B65\u5DF2\u7981\u7528\uFF08fet.figmaGuard.sync.enabled: false\uFF09\u3002", "Figma sync is disabled (fet.figmaGuard.sync.enabled: false)."),
|
|
6560
|
+
data: { changeId, enabled: false }
|
|
6561
|
+
});
|
|
6562
|
+
return;
|
|
6563
|
+
}
|
|
6564
|
+
if (options.plan) {
|
|
6565
|
+
ctx.output.result({
|
|
6566
|
+
ok: true,
|
|
6567
|
+
command: "figma",
|
|
6568
|
+
summary: msg(ctx.language, "Figma \u540C\u6B65\u8BA1\u5212\u5DF2\u751F\u6210\u3002", "Figma sync plan ready."),
|
|
6569
|
+
data: {
|
|
6570
|
+
changeId,
|
|
6571
|
+
files: result.manifest.files,
|
|
6572
|
+
figmaUrls: result.manifest.figmaUrls
|
|
6573
|
+
},
|
|
6574
|
+
nextSteps: [`fet figma sync --change ${changeId}`]
|
|
6575
|
+
});
|
|
6576
|
+
return;
|
|
6577
|
+
}
|
|
6578
|
+
await mkdir11(join29(ctx.projectRoot, "openspec", "changes", changeId, ".fet"), { recursive: true });
|
|
6579
|
+
ctx.output.result({
|
|
6580
|
+
ok: true,
|
|
6581
|
+
command: "figma",
|
|
6582
|
+
summary: result.written ? msg(
|
|
6583
|
+
ctx.language,
|
|
6584
|
+
`\u5DF2\u540C\u6B65 ${result.manifest.nodes.length} \u4E2A\u8282\u70B9\u3001${result.manifest.assets.length} \u4E2A\u7D20\u6750\u3002`,
|
|
6585
|
+
`Synced ${result.manifest.nodes.length} node(s) and ${result.manifest.assets.length} asset(s).`
|
|
6586
|
+
) : msg(ctx.language, "Figma \u8BBE\u8BA1\u6570\u636E\u5DF2\u662F\u6700\u65B0\uFF0C\u8DF3\u8FC7\u91CD\u65B0\u4E0B\u8F7D\u3002", "Figma design data is up to date; skipped re-download."),
|
|
6587
|
+
data: {
|
|
6588
|
+
changeId,
|
|
6589
|
+
written: result.written,
|
|
6590
|
+
manifestPath: result.manifestPath,
|
|
6591
|
+
nodesPath: result.nodesPath,
|
|
6592
|
+
instructionsPath: result.instructionsPath,
|
|
6593
|
+
assetsDir: result.assetsDir,
|
|
6594
|
+
nodeCount: result.manifest.nodes.length,
|
|
6595
|
+
assetCount: result.manifest.assets.length,
|
|
6596
|
+
fileAssetCount: result.manifest.assets.filter((asset) => asset.storage === "file").length
|
|
6597
|
+
},
|
|
6598
|
+
nextSteps: result.written ? [
|
|
6599
|
+
`Read ${result.instructionsPath} before UI implementation.`,
|
|
6600
|
+
...renderFigmaSyncNextSteps(changeId, ctx.language, syncConfig.apiKeyEnv).slice(1)
|
|
6601
|
+
] : [`Read ${result.instructionsPath} before UI implementation.`]
|
|
6602
|
+
});
|
|
6603
|
+
});
|
|
6604
|
+
}
|
|
6605
|
+
|
|
5819
6606
|
// src/commands/visual.ts
|
|
5820
|
-
import { mkdir as
|
|
5821
|
-
import { join as
|
|
6607
|
+
import { mkdir as mkdir13 } from "fs/promises";
|
|
6608
|
+
import { join as join33 } from "path";
|
|
5822
6609
|
|
|
5823
6610
|
// src/visual/config.ts
|
|
5824
|
-
import { readFile as
|
|
5825
|
-
import { join as
|
|
5826
|
-
import { parseDocument as
|
|
6611
|
+
import { readFile as readFile21 } from "fs/promises";
|
|
6612
|
+
import { join as join30 } from "path";
|
|
6613
|
+
import { parseDocument as parseDocument7 } from "yaml";
|
|
5827
6614
|
var DEFAULT_CONFIG4 = {
|
|
5828
6615
|
enabled: true,
|
|
5829
6616
|
compareMode: "layout-only",
|
|
@@ -5832,8 +6619,8 @@ var DEFAULT_CONFIG4 = {
|
|
|
5832
6619
|
};
|
|
5833
6620
|
async function loadVisualConfig(projectRoot) {
|
|
5834
6621
|
try {
|
|
5835
|
-
const raw = await
|
|
5836
|
-
const doc =
|
|
6622
|
+
const raw = await readFile21(join30(projectRoot, "openspec", "config.yaml"), "utf8");
|
|
6623
|
+
const doc = parseDocument7(raw);
|
|
5837
6624
|
const fetNode = doc.get("fet", true);
|
|
5838
6625
|
const node = fetNode?.get?.("visual");
|
|
5839
6626
|
if (!node || typeof node.get !== "function") {
|
|
@@ -5879,10 +6666,10 @@ function isVisualRequiredForVerify(config, hasFigma) {
|
|
|
5879
6666
|
|
|
5880
6667
|
// src/visual/playwright.ts
|
|
5881
6668
|
import { createRequire } from "module";
|
|
5882
|
-
import { dirname as dirname9, join as
|
|
6669
|
+
import { dirname as dirname9, join as join31 } from "path";
|
|
5883
6670
|
import { pathToFileURL } from "url";
|
|
5884
6671
|
async function resolvePlaywright(projectRoot) {
|
|
5885
|
-
const require2 = createRequire(
|
|
6672
|
+
const require2 = createRequire(join31(projectRoot, "package.json"));
|
|
5886
6673
|
const candidates = ["playwright", "@playwright/test"];
|
|
5887
6674
|
for (const name of candidates) {
|
|
5888
6675
|
try {
|
|
@@ -5914,11 +6701,11 @@ async function capturePagesWithPlaywright(projectRoot, manifest, baseUrl) {
|
|
|
5914
6701
|
return results;
|
|
5915
6702
|
}
|
|
5916
6703
|
async function captureSinglePage(projectRoot, browser, changeId, pageDef, baseUrl) {
|
|
5917
|
-
const { mkdir:
|
|
6704
|
+
const { mkdir: mkdir18 } = await import("fs/promises");
|
|
5918
6705
|
const { join: joinPath } = await import("path");
|
|
5919
6706
|
const screenshotRelative = visualPageScreenshotRelative(changeId, pageDef.id);
|
|
5920
6707
|
const screenshotAbsolute = joinPath(projectRoot, screenshotRelative);
|
|
5921
|
-
await
|
|
6708
|
+
await mkdir18(dirname9(screenshotAbsolute), { recursive: true });
|
|
5922
6709
|
const url = new URL(pageDef.route, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).toString();
|
|
5923
6710
|
const pwPage = await browser.newPage();
|
|
5924
6711
|
try {
|
|
@@ -5983,11 +6770,11 @@ async function extractRegions(page, pageDef) {
|
|
|
5983
6770
|
}
|
|
5984
6771
|
|
|
5985
6772
|
// src/visual/manifest.ts
|
|
5986
|
-
import { mkdir as
|
|
5987
|
-
import { dirname as dirname10, join as
|
|
5988
|
-
import { parse as parse5, stringify as
|
|
6773
|
+
import { mkdir as mkdir12, readFile as readFile22, stat as stat11 } from "fs/promises";
|
|
6774
|
+
import { dirname as dirname10, join as join32 } from "path";
|
|
6775
|
+
import { parse as parse5, stringify as stringify5 } from "yaml";
|
|
5989
6776
|
function visualManifestPath(projectRoot, changeId) {
|
|
5990
|
-
return
|
|
6777
|
+
return join32(projectRoot, visualManifestRelativePath(changeId));
|
|
5991
6778
|
}
|
|
5992
6779
|
async function readVisualManifest(projectRoot, changeId) {
|
|
5993
6780
|
const path = visualManifestPath(projectRoot, changeId);
|
|
@@ -5996,7 +6783,7 @@ async function readVisualManifest(projectRoot, changeId) {
|
|
|
5996
6783
|
} catch {
|
|
5997
6784
|
return null;
|
|
5998
6785
|
}
|
|
5999
|
-
const doc = parse5(await
|
|
6786
|
+
const doc = parse5(await readFile22(path, "utf8"));
|
|
6000
6787
|
if (!doc || doc.schemaVersion !== 1 || doc.changeId !== changeId) {
|
|
6001
6788
|
return null;
|
|
6002
6789
|
}
|
|
@@ -6004,15 +6791,15 @@ async function readVisualManifest(projectRoot, changeId) {
|
|
|
6004
6791
|
}
|
|
6005
6792
|
async function writeVisualManifest(projectRoot, manifest) {
|
|
6006
6793
|
const relative6 = visualManifestRelativePath(manifest.changeId);
|
|
6007
|
-
const path =
|
|
6008
|
-
await
|
|
6009
|
-
await atomicWrite(path,
|
|
6794
|
+
const path = join32(projectRoot, relative6);
|
|
6795
|
+
await mkdir12(dirname10(path), { recursive: true });
|
|
6796
|
+
await atomicWrite(path, stringify5(manifest));
|
|
6010
6797
|
return relative6;
|
|
6011
6798
|
}
|
|
6012
6799
|
async function readVisualCapture(projectRoot, changeId) {
|
|
6013
|
-
const path =
|
|
6800
|
+
const path = join32(projectRoot, visualCaptureRelativePath(changeId));
|
|
6014
6801
|
try {
|
|
6015
|
-
const doc = JSON.parse(await
|
|
6802
|
+
const doc = JSON.parse(await readFile22(path, "utf8"));
|
|
6016
6803
|
if (doc?.schemaVersion === 1 && doc.changeId === changeId) {
|
|
6017
6804
|
return doc;
|
|
6018
6805
|
}
|
|
@@ -6023,16 +6810,16 @@ async function readVisualCapture(projectRoot, changeId) {
|
|
|
6023
6810
|
}
|
|
6024
6811
|
async function writeVisualCapture(projectRoot, capture) {
|
|
6025
6812
|
const relative6 = visualCaptureRelativePath(capture.changeId);
|
|
6026
|
-
const path =
|
|
6027
|
-
await
|
|
6813
|
+
const path = join32(projectRoot, relative6);
|
|
6814
|
+
await mkdir12(dirname10(path), { recursive: true });
|
|
6028
6815
|
await atomicWrite(path, `${JSON.stringify(capture, null, 2)}
|
|
6029
6816
|
`);
|
|
6030
6817
|
return relative6;
|
|
6031
6818
|
}
|
|
6032
6819
|
async function writeVisualResults(projectRoot, results) {
|
|
6033
6820
|
const relative6 = visualResultsRelativePath(results.changeId);
|
|
6034
|
-
const path =
|
|
6035
|
-
await
|
|
6821
|
+
const path = join32(projectRoot, relative6);
|
|
6822
|
+
await mkdir12(dirname10(path), { recursive: true });
|
|
6036
6823
|
await atomicWrite(path, `${JSON.stringify(results, null, 2)}
|
|
6037
6824
|
`);
|
|
6038
6825
|
return relative6;
|
|
@@ -6426,10 +7213,10 @@ async function ensureManifest(ctx, changeId, baseUrl) {
|
|
|
6426
7213
|
baseUrl
|
|
6427
7214
|
);
|
|
6428
7215
|
await writeVisualManifest(ctx.projectRoot, manifest);
|
|
6429
|
-
const fetDir =
|
|
6430
|
-
await
|
|
6431
|
-
await atomicWrite(
|
|
6432
|
-
await atomicWrite(
|
|
7216
|
+
const fetDir = join33(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
|
|
7217
|
+
await mkdir13(fetDir, { recursive: true });
|
|
7218
|
+
await atomicWrite(join33(ctx.projectRoot, visualInstructionsRelativePath(changeId)), renderVisualInstructions(changeId, manifest, ctx.language));
|
|
7219
|
+
await atomicWrite(join33(ctx.projectRoot, visualSpecRelativePath(changeId)), renderVisualSpec(changeId, manifest, ctx.language));
|
|
6433
7220
|
const changeState = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
6434
7221
|
changeState.visual = {
|
|
6435
7222
|
status: "ready",
|
|
@@ -6482,8 +7269,8 @@ async function recordVisualFailure(ctx, changeId, planningFingerprint, steps, ca
|
|
|
6482
7269
|
|
|
6483
7270
|
// src/commands/verify.ts
|
|
6484
7271
|
import { createHash as createHash2 } from "crypto";
|
|
6485
|
-
import { mkdir as
|
|
6486
|
-
import { join as
|
|
7272
|
+
import { mkdir as mkdir14, readFile as readFile23, stat as stat12 } from "fs/promises";
|
|
7273
|
+
import { join as join34 } from "path";
|
|
6487
7274
|
async function verifyCommand(ctx, options) {
|
|
6488
7275
|
if (options.auto) {
|
|
6489
7276
|
const scan = await ctx.scanner.scan(ctx.projectRoot, {});
|
|
@@ -6552,9 +7339,9 @@ async function writeInstructions(ctx, changeId) {
|
|
|
6552
7339
|
await assertTestPassed(ctx, changeId);
|
|
6553
7340
|
await assertVisualPassed(ctx, changeId);
|
|
6554
7341
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6555
|
-
const dir =
|
|
6556
|
-
const instructionsPath =
|
|
6557
|
-
await
|
|
7342
|
+
const dir = join34(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
|
|
7343
|
+
const instructionsPath = join34(dir, "verify-instructions.md");
|
|
7344
|
+
await mkdir14(dir, { recursive: true });
|
|
6558
7345
|
await atomicWrite(instructionsPath, renderVerifyInstructions(changeId, generatedAt));
|
|
6559
7346
|
const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
6560
7347
|
state.currentPhase = "verify";
|
|
@@ -6572,7 +7359,7 @@ async function markDone(ctx, changeId) {
|
|
|
6572
7359
|
await assertTestPassed(ctx, changeId);
|
|
6573
7360
|
await assertVisualPassed(ctx, changeId);
|
|
6574
7361
|
const declaredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6575
|
-
const instructionsPath =
|
|
7362
|
+
const instructionsPath = join34(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
|
|
6576
7363
|
const instructions = await readInstructions(ctx, instructionsPath, changeId);
|
|
6577
7364
|
const instructionsGeneratedAt = readFrontMatterValue(instructions, "generatedAt") ?? declaredAt;
|
|
6578
7365
|
const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
@@ -6597,7 +7384,7 @@ async function markDone(ctx, changeId) {
|
|
|
6597
7384
|
async function readInstructions(ctx, path, changeId) {
|
|
6598
7385
|
try {
|
|
6599
7386
|
await stat12(path);
|
|
6600
|
-
const content = await
|
|
7387
|
+
const content = await readFile23(path, "utf8");
|
|
6601
7388
|
const fileChangeId = readFrontMatterValue(content, "changeId");
|
|
6602
7389
|
if (fileChangeId !== changeId) {
|
|
6603
7390
|
throw new FetError({
|
|
@@ -6720,9 +7507,9 @@ function renderIdeModelPolicy(command, language = "zh-CN") {
|
|
|
6720
7507
|
import { resolve } from "path";
|
|
6721
7508
|
|
|
6722
7509
|
// src/adapters/codex/index.ts
|
|
6723
|
-
import { mkdir as
|
|
7510
|
+
import { mkdir as mkdir15, readFile as readFile24, stat as stat13 } from "fs/promises";
|
|
6724
7511
|
import { homedir } from "os";
|
|
6725
|
-
import { dirname as dirname11, join as
|
|
7512
|
+
import { dirname as dirname11, join as join35 } from "path";
|
|
6726
7513
|
|
|
6727
7514
|
// src/adapters/commands.ts
|
|
6728
7515
|
var FET_STANDALONE_COMMANDS = ["tdd", "test", "visual"];
|
|
@@ -7383,7 +8170,7 @@ Steps:
|
|
|
7383
8170
|
3. Follow the native apply output. If JSON output is unavailable, read the files referenced by the terminal output and the artifacts under openspec/changes/<change-id>/.
|
|
7384
8171
|
4. If \`openspec/changes/<change-id>/.fet/figma-apply-instructions.md\` exists (or apply nextSteps mention Figma):
|
|
7385
8172
|
- Read it and \`figma-stop.md\` in the same folder before any UI code.
|
|
7386
|
-
-
|
|
8173
|
+
- Run \`fet figma sync\` when possible; read \`figma-design-manifest.yaml\`, \`figma-nodes.json\`, and \`.fet/figma-assets/\` (file paths for large images). Otherwise use Figma MCP/API for every linked frame; briefly confirm design facts in your reply.
|
|
7387
8174
|
- If Figma access fails or design details are unclear, stop and ask the user\u2014do not guess styles or implement UI.
|
|
7388
8175
|
5. If \`openspec/changes/<change-id>/.fet/ui-display-contract.yaml\` exists (or apply nextSteps mention UI display contract):
|
|
7389
8176
|
- Read it and \`ui-field-apply-instructions.md\` in the same folder before UI that binds API data.
|
|
@@ -7929,7 +8716,7 @@ var CodexAdapter = class {
|
|
|
7929
8716
|
adapterVersion = 1;
|
|
7930
8717
|
async detect(projectRoot) {
|
|
7931
8718
|
return {
|
|
7932
|
-
detected: await exists6(
|
|
8719
|
+
detected: await exists6(join35(projectRoot, ".codex")) || await exists6(join35(projectRoot, "AGENTS.md")),
|
|
7933
8720
|
reason: "Codex adapter is available for projects that use AGENTS.md"
|
|
7934
8721
|
};
|
|
7935
8722
|
}
|
|
@@ -7968,7 +8755,7 @@ var CodexAdapter = class {
|
|
|
7968
8755
|
if (existing && !existing.includes("FET:MANAGED") && force) {
|
|
7969
8756
|
await createBackup(target);
|
|
7970
8757
|
}
|
|
7971
|
-
await
|
|
8758
|
+
await mkdir15(dirname11(target), { recursive: true });
|
|
7972
8759
|
await atomicWrite(target, file.content);
|
|
7973
8760
|
written.push(displayPath);
|
|
7974
8761
|
}
|
|
@@ -7995,9 +8782,9 @@ var CodexAdapter = class {
|
|
|
7995
8782
|
};
|
|
7996
8783
|
function resolveTarget(projectRoot, file) {
|
|
7997
8784
|
if (file.root === "codex-home") {
|
|
7998
|
-
return
|
|
8785
|
+
return join35(resolveCodexHome(), file.path);
|
|
7999
8786
|
}
|
|
8000
|
-
return
|
|
8787
|
+
return join35(projectRoot, file.path);
|
|
8001
8788
|
}
|
|
8002
8789
|
function displayPathFor(file) {
|
|
8003
8790
|
if (file.root === "codex-home") {
|
|
@@ -8006,11 +8793,11 @@ function displayPathFor(file) {
|
|
|
8006
8793
|
return file.path;
|
|
8007
8794
|
}
|
|
8008
8795
|
function resolveCodexHome() {
|
|
8009
|
-
return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ??
|
|
8796
|
+
return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join35(homedir(), ".codex");
|
|
8010
8797
|
}
|
|
8011
8798
|
async function readExisting(path) {
|
|
8012
8799
|
try {
|
|
8013
|
-
return await
|
|
8800
|
+
return await readFile24(path, "utf8");
|
|
8014
8801
|
} catch {
|
|
8015
8802
|
return null;
|
|
8016
8803
|
}
|
|
@@ -8025,8 +8812,8 @@ async function exists6(path) {
|
|
|
8025
8812
|
}
|
|
8026
8813
|
|
|
8027
8814
|
// src/adapters/cursor/index.ts
|
|
8028
|
-
import { mkdir as
|
|
8029
|
-
import { dirname as dirname12, join as
|
|
8815
|
+
import { mkdir as mkdir16, readFile as readFile25, stat as stat14 } from "fs/promises";
|
|
8816
|
+
import { dirname as dirname12, join as join36 } from "path";
|
|
8030
8817
|
|
|
8031
8818
|
// src/adapters/cursor/templates.ts
|
|
8032
8819
|
function cursorFigmaStopRuleFile(language = DEFAULT_LANGUAGE) {
|
|
@@ -8272,12 +9059,14 @@ function renderApplySkill(usage, language) {
|
|
|
8272
9059
|
const figmaBlock = language === "en" ? `If \`fet apply\` output or the change has \`openspec/changes/<change-id>/.fet/figma-apply-instructions.md\`:
|
|
8273
9060
|
|
|
8274
9061
|
1. Read that file and \`figma-stop.md\` in the same folder before any UI code.
|
|
8275
|
-
2.
|
|
9062
|
+
2. Run \`fet figma sync\` when possible; read \`figma-design-manifest.yaml\`, \`figma-nodes.json\`, and \`.fet/figma-assets/\` (use file paths for large images, not base64).
|
|
9063
|
+
3. Otherwise use Figma MCP/API for every linked frame; do not invent colors, spacing, or layout.
|
|
8276
9064
|
3. If Figma fails or design is unclear, stop and ask the user\u2014do not guess styles or mark UI tasks done.
|
|
8277
9065
|
4. After design is confirmed, implement tasks from \`tasks.md\`.` : `\u82E5 \`fet apply\` \u8F93\u51FA\u6216 change \u4E2D\u5B58\u5728 \`openspec/changes/<change-id>/.fet/figma-apply-instructions.md\`\uFF1A
|
|
8278
9066
|
|
|
8279
9067
|
1. \u5728\u6539\u4EFB\u4F55 UI \u4EE3\u7801\u524D\uFF0C\u5148\u9605\u8BFB\u8BE5\u6587\u4EF6\u4E0E\u540C\u76EE\u5F55 \`figma-stop.md\`\u3002
|
|
8280
|
-
2. \
|
|
9068
|
+
2. \u4F18\u5148\u6267\u884C \`fet figma sync\`\uFF1B\u9605\u8BFB \`figma-design-manifest.yaml\`\u3001\`figma-nodes.json\` \u4E0E \`.fet/figma-assets/\`\uFF08\u5927\u56FE\u7528\u6587\u4EF6\u8DEF\u5F84\uFF0C\u7981\u6B62 base64\uFF09\u3002
|
|
9069
|
+
3. \u5426\u5219\u7528 Figma MCP/API \u8BFB\u53D6\u6BCF\u4E2A\u753B\u677F\uFF1B\u7981\u6B62\u81EA\u521B\u989C\u8272\u3001\u95F4\u8DDD\u3001\u5E03\u5C40\u3002
|
|
8281
9070
|
3. Figma \u5931\u8D25\u6216\u8BBE\u8BA1\u4E0D\u6E05\u65F6\u7ACB\u5373\u505C\u6B62\u5E76\u5411\u7528\u6237\u63D0\u95EE\uFF0C\u4E0D\u8981\u731C\u6837\u5F0F\uFF0C\u4E0D\u8981\u628A UI \u4EFB\u52A1\u6807\u4E3A\u5B8C\u6210\u3002
|
|
8282
9071
|
4. \u8BBE\u8BA1\u786E\u8BA4\u540E\uFF0C\u518D\u6309 \`tasks.md\` \u5B9E\u65BD\u4EFB\u52A1\u3002`;
|
|
8283
9072
|
return `<!-- FET:MANAGED
|
|
@@ -8381,7 +9170,7 @@ var CursorAdapter = class {
|
|
|
8381
9170
|
adapterVersion = 1;
|
|
8382
9171
|
async detect(projectRoot) {
|
|
8383
9172
|
return {
|
|
8384
|
-
detected: await exists7(
|
|
9173
|
+
detected: await exists7(join36(projectRoot, ".cursor")),
|
|
8385
9174
|
reason: "Cursor adapter is available for any project"
|
|
8386
9175
|
};
|
|
8387
9176
|
}
|
|
@@ -8398,10 +9187,10 @@ var CursorAdapter = class {
|
|
|
8398
9187
|
const written = [];
|
|
8399
9188
|
const skipped = [];
|
|
8400
9189
|
for (const file of plan.files) {
|
|
8401
|
-
const target =
|
|
9190
|
+
const target = join36(projectRoot, file.path);
|
|
8402
9191
|
const existing = await readExisting2(target);
|
|
8403
9192
|
if (file.path === ".cursor/hooks.json") {
|
|
8404
|
-
await
|
|
9193
|
+
await mkdir16(dirname12(target), { recursive: true });
|
|
8405
9194
|
await atomicWrite(target, mergeCursorHooksJson(existing, file.content));
|
|
8406
9195
|
written.push(file.path);
|
|
8407
9196
|
continue;
|
|
@@ -8417,7 +9206,7 @@ var CursorAdapter = class {
|
|
|
8417
9206
|
if (existing && !existing.includes("FET:MANAGED") && force) {
|
|
8418
9207
|
await createBackup(target);
|
|
8419
9208
|
}
|
|
8420
|
-
await
|
|
9209
|
+
await mkdir16(dirname12(target), { recursive: true });
|
|
8421
9210
|
await atomicWrite(target, file.content);
|
|
8422
9211
|
written.push(file.path);
|
|
8423
9212
|
}
|
|
@@ -8427,7 +9216,7 @@ var CursorAdapter = class {
|
|
|
8427
9216
|
const plan = await this.planInstall(projectRoot);
|
|
8428
9217
|
const checks = [];
|
|
8429
9218
|
for (const file of plan.files) {
|
|
8430
|
-
const target =
|
|
9219
|
+
const target = join36(projectRoot, file.path);
|
|
8431
9220
|
const content = await readExisting2(target);
|
|
8432
9221
|
const hooksManaged = file.path === ".cursor/hooks.json" && Boolean(content?.includes('"writeBoundary"'));
|
|
8433
9222
|
const hookScript = file.path.startsWith(".cursor/hooks/fet-guard-") && file.path.endsWith(".mjs");
|
|
@@ -8445,7 +9234,7 @@ var CursorAdapter = class {
|
|
|
8445
9234
|
};
|
|
8446
9235
|
async function readExisting2(path) {
|
|
8447
9236
|
try {
|
|
8448
|
-
return await
|
|
9237
|
+
return await readFile25(path, "utf8");
|
|
8449
9238
|
} catch {
|
|
8450
9239
|
return null;
|
|
8451
9240
|
}
|
|
@@ -8465,12 +9254,12 @@ import { promisify as promisify4 } from "util";
|
|
|
8465
9254
|
|
|
8466
9255
|
// src/openspec/inspector.ts
|
|
8467
9256
|
import { readdir as readdir6, stat as stat15 } from "fs/promises";
|
|
8468
|
-
import { join as
|
|
9257
|
+
import { join as join37 } from "path";
|
|
8469
9258
|
async function inspectOpenSpecProject(projectRoot) {
|
|
8470
|
-
const openspecPath =
|
|
8471
|
-
const changesPath =
|
|
8472
|
-
const legacyArchivePath =
|
|
8473
|
-
const changesArchivePath =
|
|
9259
|
+
const openspecPath = join37(projectRoot, "openspec");
|
|
9260
|
+
const changesPath = join37(openspecPath, "changes");
|
|
9261
|
+
const legacyArchivePath = join37(openspecPath, "archive");
|
|
9262
|
+
const changesArchivePath = join37(changesPath, "archive");
|
|
8474
9263
|
return {
|
|
8475
9264
|
exists: await exists8(openspecPath),
|
|
8476
9265
|
changes: await listDirectories(changesPath, { exclude: ["archive"] }),
|
|
@@ -8478,13 +9267,13 @@ async function inspectOpenSpecProject(projectRoot) {
|
|
|
8478
9267
|
};
|
|
8479
9268
|
}
|
|
8480
9269
|
async function inspectOpenSpecChange(projectRoot, changeId) {
|
|
8481
|
-
const changePath =
|
|
8482
|
-
const tasksPath =
|
|
8483
|
-
const specsPath =
|
|
9270
|
+
const changePath = join37(projectRoot, "openspec", "changes", changeId);
|
|
9271
|
+
const tasksPath = join37(changePath, "tasks.md");
|
|
9272
|
+
const specsPath = join37(changePath, "specs");
|
|
8484
9273
|
return {
|
|
8485
9274
|
changeId,
|
|
8486
9275
|
exists: await exists8(changePath),
|
|
8487
|
-
hasProposal: await exists8(
|
|
9276
|
+
hasProposal: await exists8(join37(changePath, "proposal.md")),
|
|
8488
9277
|
hasTasks: await exists8(tasksPath),
|
|
8489
9278
|
hasSpecs: await exists8(specsPath),
|
|
8490
9279
|
tasksPath,
|
|
@@ -8686,12 +9475,12 @@ function escapeRegExp(value) {
|
|
|
8686
9475
|
|
|
8687
9476
|
// src/scanner/routes.ts
|
|
8688
9477
|
import { readdir as readdir7, stat as stat16 } from "fs/promises";
|
|
8689
|
-
import { join as
|
|
9478
|
+
import { join as join38, relative as relative5, sep } from "path";
|
|
8690
9479
|
async function scanRoutes(projectRoot) {
|
|
8691
9480
|
const candidates = ["src/routes", "src/pages", "app", "pages"];
|
|
8692
9481
|
const routes = [];
|
|
8693
9482
|
for (const candidate of candidates) {
|
|
8694
|
-
const root =
|
|
9483
|
+
const root = join38(projectRoot, candidate);
|
|
8695
9484
|
if (!await exists9(root)) {
|
|
8696
9485
|
continue;
|
|
8697
9486
|
}
|
|
@@ -8719,7 +9508,7 @@ async function listFiles(root) {
|
|
|
8719
9508
|
const entries = await readdir7(root, { withFileTypes: true });
|
|
8720
9509
|
const files = [];
|
|
8721
9510
|
for (const entry of entries) {
|
|
8722
|
-
const path =
|
|
9511
|
+
const path = join38(root, entry.name);
|
|
8723
9512
|
if (entry.isDirectory()) {
|
|
8724
9513
|
files.push(...await listFiles(path));
|
|
8725
9514
|
} else {
|
|
@@ -8887,9 +9676,9 @@ async function createCommandContext(command, options) {
|
|
|
8887
9676
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
8888
9677
|
|
|
8889
9678
|
// src/update/check.ts
|
|
8890
|
-
import { mkdir as
|
|
9679
|
+
import { mkdir as mkdir17, readFile as readFile26, writeFile as writeFile3 } from "fs/promises";
|
|
8891
9680
|
import { homedir as homedir2 } from "os";
|
|
8892
|
-
import { dirname as dirname13, join as
|
|
9681
|
+
import { dirname as dirname13, join as join39 } from "path";
|
|
8893
9682
|
var DEFAULT_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
8894
9683
|
function getFetUpdateCheckMode(env = process.env) {
|
|
8895
9684
|
const value = env.FET_UPDATE_CHECK?.trim().toLowerCase();
|
|
@@ -8962,11 +9751,11 @@ function formatFetUpdateWarning(availability, language) {
|
|
|
8962
9751
|
}
|
|
8963
9752
|
function cachePath() {
|
|
8964
9753
|
const home = process.env.FET_UPDATE_CHECK_CACHE_HOME?.trim() || homedir2();
|
|
8965
|
-
return
|
|
9754
|
+
return join39(home, ".fet", "update-check-cache.json");
|
|
8966
9755
|
}
|
|
8967
9756
|
async function readUpdateCheckCache() {
|
|
8968
9757
|
try {
|
|
8969
|
-
const raw = await
|
|
9758
|
+
const raw = await readFile26(cachePath(), "utf8");
|
|
8970
9759
|
const parsed = JSON.parse(raw);
|
|
8971
9760
|
if (typeof parsed.latestVersion !== "string" || typeof parsed.checkedAt !== "string") {
|
|
8972
9761
|
return null;
|
|
@@ -8982,8 +9771,8 @@ async function readUpdateCheckCache() {
|
|
|
8982
9771
|
}
|
|
8983
9772
|
async function writeUpdateCheckCache(cache) {
|
|
8984
9773
|
const path = cachePath();
|
|
8985
|
-
await
|
|
8986
|
-
await
|
|
9774
|
+
await mkdir17(dirname13(path), { recursive: true });
|
|
9775
|
+
await writeFile3(path, `${JSON.stringify(cache, null, 2)}
|
|
8987
9776
|
`, "utf8");
|
|
8988
9777
|
}
|
|
8989
9778
|
|
|
@@ -9070,6 +9859,19 @@ addGlobalOptions(program.command("doctor").description("\u8BCA\u65AD\u72B6\u6001
|
|
|
9070
9859
|
wrap("doctor", (ctx, options) => doctorCommand(ctx, { fixLock: Boolean(options.fixLock) }))
|
|
9071
9860
|
);
|
|
9072
9861
|
addGlobalOptions(program.command("tdd").description("\u6839\u636E\u89C4\u5212\u4EA7\u7269\u751F\u6210 change \u7EA7 TDD \u6E05\u5355\u4E0E\u6D4B\u8BD5\u6307\u5F15")).action(wrap("tdd", tddCommand));
|
|
9862
|
+
var figma = addGlobalOptions(program.command("figma").description("Figma \u8BBE\u8BA1\u7A3F\u540C\u6B65\uFF08\u62C9\u53D6\u8282\u70B9\u6811\u5E76\u4E0B\u8F7D\u7D20\u6750\uFF09"));
|
|
9863
|
+
addGlobalOptions(
|
|
9864
|
+
figma.command("sync").description("\u4ECE Figma API \u540C\u6B65\u5168\u90E8\u8282\u70B9\u4E0E\u56FE\u7247\u7D20\u6750\u5230 change/.fet/").option("--token <token>", "Figma Personal Access Token\uFF08\u4E5F\u53EF\u7528 FIGMA_ACCESS_TOKEN\uFF09").option("--force", "\u5F3A\u5236\u91CD\u65B0\u4E0B\u8F7D\uFF0C\u5FFD\u7565\u5DF2\u6709\u540C\u6B65\u7ED3\u679C").option("--plan", "\u4EC5\u8F93\u51FA\u540C\u6B65\u8BA1\u5212\uFF0C\u4E0D\u8C03\u7528 Figma API")
|
|
9865
|
+
).action(
|
|
9866
|
+
wrap(
|
|
9867
|
+
"figma",
|
|
9868
|
+
(ctx, options) => figmaSyncCommand(ctx, {
|
|
9869
|
+
token: options.token,
|
|
9870
|
+
force: Boolean(options.force),
|
|
9871
|
+
plan: Boolean(options.plan)
|
|
9872
|
+
})
|
|
9873
|
+
)
|
|
9874
|
+
);
|
|
9073
9875
|
addGlobalOptions(program.command("test").description("\u6309 change TDD \u6E05\u5355\u8FD0\u884C\u5355\u6D4B\u5E76\u8BB0\u5F55\u7EFF\u706F\u72B6\u6001").option("--plan", "\u4EC5\u8F93\u51FA\u5C06\u6267\u884C\u7684\u6D4B\u8BD5\u547D\u4EE4\uFF0C\u4E0D\u8FD0\u884C")).action(
|
|
9074
9876
|
wrap("test", (ctx, options) => testCommand(ctx, { plan: Boolean(options.plan) }))
|
|
9075
9877
|
);
|
|
@@ -9202,4 +10004,3 @@ function extractGlobalOptions(args) {
|
|
|
9202
10004
|
}
|
|
9203
10005
|
return options;
|
|
9204
10006
|
}
|
|
9205
|
-
//# sourceMappingURL=index.js.map
|