@nick848/fet 1.1.11 → 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 +1607 -263
- 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
|
];
|
|
@@ -3000,6 +3023,390 @@ When the artifact is \`specs/<capability>/spec.md\` (or you edit spec files in t
|
|
|
3000
3023
|
3. \u82F1\u6587\u89C4\u8303\u53E5\u6709\u4EFB\u4F55\u53D8\u52A8\u65F6\uFF0C**\u540C\u4E00\u6B21\u7F16\u8F91**\u5FC5\u987B\u540C\u6B65\u66F4\u65B0\u5BF9\u5E94\u4E2D\u6587\u6CE8\u91CA\u3002${uiBlock}`;
|
|
3001
3024
|
}
|
|
3002
3025
|
|
|
3026
|
+
// src/templates/write-boundary.ts
|
|
3027
|
+
var WRITE_BOUNDARY_ALLOW_PREFIXES = ["src/", "openspec/"];
|
|
3028
|
+
var WRITE_BOUNDARY_ROOT_CONFIG_EXACT = [
|
|
3029
|
+
".gitignore",
|
|
3030
|
+
".gitattributes",
|
|
3031
|
+
".stylelintignore",
|
|
3032
|
+
".eslintignore",
|
|
3033
|
+
".prettierignore",
|
|
3034
|
+
".editorconfig",
|
|
3035
|
+
".npmrc",
|
|
3036
|
+
".nvmrc",
|
|
3037
|
+
".node-version",
|
|
3038
|
+
".checkrc.js",
|
|
3039
|
+
"package.json",
|
|
3040
|
+
"package-lock.json",
|
|
3041
|
+
"pnpm-lock.yaml",
|
|
3042
|
+
"yarn.lock",
|
|
3043
|
+
"bun.lockb",
|
|
3044
|
+
"npm-shrinkwrap.json"
|
|
3045
|
+
];
|
|
3046
|
+
var WRITE_BOUNDARY_ROOT_CONFIG_BASENAME_PREFIXES = [
|
|
3047
|
+
".eslintrc",
|
|
3048
|
+
".stylelintrc",
|
|
3049
|
+
".prettierrc",
|
|
3050
|
+
".stylelint",
|
|
3051
|
+
"eslint.config",
|
|
3052
|
+
"stylelint.config",
|
|
3053
|
+
"prettier.config",
|
|
3054
|
+
"tsconfig",
|
|
3055
|
+
"vitest.config",
|
|
3056
|
+
"vite.config",
|
|
3057
|
+
"tsup.config",
|
|
3058
|
+
"jest.config",
|
|
3059
|
+
"rollup.config",
|
|
3060
|
+
"webpack.config",
|
|
3061
|
+
"babel.config",
|
|
3062
|
+
"biome.json",
|
|
3063
|
+
"deno.json",
|
|
3064
|
+
"deno.jsonc"
|
|
3065
|
+
];
|
|
3066
|
+
function renderRootConfigPathList(language) {
|
|
3067
|
+
const samples = language === "en" ? "`.gitignore`, `.stylelintignore`, `.stylelintrc*`, `.eslintrc*`, `eslint.config.*`, `.checkrc.js`, `package.json`, `package-lock.json`, `tsconfig*.json`, `.prettierrc*`, tool configs at repo root (`vitest.config.*`, `vite.config.*`, \u2026)" : "`.gitignore`\u3001`.stylelintignore`\u3001`.stylelintrc*`\u3001`.eslintrc*`\u3001`eslint.config.*`\u3001`.checkrc.js`\u3001`package.json`\u3001`package-lock.json`\u3001`tsconfig*.json`\u3001`.prettierrc*`\u3001\u4EE5\u53CA\u4ED3\u5E93\u6839\u76EE\u5F55\u4E0B\u7684 `vitest.config.*`\u3001`vite.config.*` \u7B49\u5DE5\u5177\u914D\u7F6E";
|
|
3068
|
+
return samples;
|
|
3069
|
+
}
|
|
3070
|
+
function renderWriteBoundaryGuardrail(language) {
|
|
3071
|
+
const rootList = renderRootConfigPathList(language);
|
|
3072
|
+
if (language === "en") {
|
|
3073
|
+
return `- Default write scope: \`src/**\`, \`openspec/**\`, and any \`**/.fet/**\`. All other paths need explicit user approval first.
|
|
3074
|
+
- **Repo-root config is high risk**: ${rootList} \u2014 list each file and why before editing; never change these silently.`;
|
|
3075
|
+
}
|
|
3076
|
+
return `- \u9ED8\u8BA4\u53EA\u5141\u8BB8\u4FEE\u6539 \`src/**\`\u3001\`openspec/**\` \u53CA\u4EFB\u610F \`**/.fet/**\`\uFF1B\u5176\u4F59\u8DEF\u5F84\u987B\u5148\u83B7\u7528\u6237\u660E\u786E\u540C\u610F\u3002
|
|
3077
|
+
- **\u4ED3\u5E93\u6839\u76EE\u5F55\u914D\u7F6E\u6587\u4EF6\u9AD8\u98CE\u9669**\uFF1A${rootList} \u2014 \u4FEE\u6539\u524D\u987B\u5217\u51FA\u6587\u4EF6\u4E0E\u539F\u56E0\uFF0C\u7981\u6B62\u64C5\u81EA\u6539\u52A8\u3002`;
|
|
3078
|
+
}
|
|
3079
|
+
function renderWriteBoundaryPolicyBody(language) {
|
|
3080
|
+
if (language === "en") {
|
|
3081
|
+
return `## Default allowed write scope
|
|
3082
|
+
|
|
3083
|
+
- \`src/**\` \u2014 application/library source
|
|
3084
|
+
- \`openspec/**\` \u2014 OpenSpec specs and change artifacts
|
|
3085
|
+
- \`**/.fet/**\` \u2014 per-change FET handoff files (including \`openspec/changes/<id>/.fet/\`)
|
|
3086
|
+
|
|
3087
|
+
## Ask the user first
|
|
3088
|
+
|
|
3089
|
+
Before creating, editing, or deleting files **outside** the allowed scope:
|
|
3090
|
+
|
|
3091
|
+
1. List every path you need to touch and why.
|
|
3092
|
+
2. Wait for explicit user approval (do not assume silence means yes).
|
|
3093
|
+
3. Prefer minimal diffs.
|
|
3094
|
+
|
|
3095
|
+
Common paths that need approval: \`tests/**\`, \`.github/**\`, \`.workflow/**\`, \`.cursor/**\`, \`.codex/**\`, \`AGENTS.md\`, \`README*\`, \`.env*\`.
|
|
3096
|
+
|
|
3097
|
+
## Repo-root config (always ask \u2014 never silent edit)
|
|
3098
|
+
|
|
3099
|
+
These live at the **repository root** (no subdirectory). Treat every change as infrastructure impact:
|
|
3100
|
+
|
|
3101
|
+
${renderRootConfigPathList(language)}
|
|
3102
|
+
|
|
3103
|
+
Workflow:
|
|
3104
|
+
|
|
3105
|
+
1. State **exact filenames** (e.g. \`package.json\`, \`.eslintrc.js\`, \`.gitignore\`).
|
|
3106
|
+
2. Explain **why** each file must change.
|
|
3107
|
+
3. Wait for **explicit user approval** before writing.
|
|
3108
|
+
4. Do not \u201Cfix lint/format\u201D by editing root configs unless the user requested that scope.
|
|
3109
|
+
|
|
3110
|
+
## Forbidden without approval
|
|
3111
|
+
|
|
3112
|
+
- Dependency or lockfile changes (\`package.json\`, \`package-lock.json\`, etc.) unless the user asked
|
|
3113
|
+
- Lint/format/tooling config at repo root (\`.eslintrc*\`, \`.stylelint*\`, \`.checkrc.js\`, \`.gitignore\`, \u2026) unless the user asked
|
|
3114
|
+
- Secrets or credential files
|
|
3115
|
+
- Using shell redirects or destructive git commands to bypass this policy
|
|
3116
|
+
|
|
3117
|
+
## Cursor enforcement
|
|
3118
|
+
|
|
3119
|
+
When \`.cursor/hooks/fet-guard-write-paths.mjs\` is installed, out-of-scope **write tools** trigger an approval prompt (\`permission: ask\`). Shell writes are gated by \`fet-guard-shell-writes.mjs\`. Rules alone are not sufficient\u2014keep hooks enabled after \`fet init\`.`;
|
|
3120
|
+
}
|
|
3121
|
+
return `## \u9ED8\u8BA4\u5141\u8BB8\u4FEE\u6539\u7684\u8303\u56F4
|
|
3122
|
+
|
|
3123
|
+
- \`src/**\` \u2014 \u4E1A\u52A1/\u5E93\u6E90\u7801
|
|
3124
|
+
- \`openspec/**\` \u2014 OpenSpec \u89C4\u8303\u4E0E change \u4EA7\u7269
|
|
3125
|
+
- \`**/.fet/**\` \u2014 \u5404 change \u7684 FET \u4EA4\u63A5\u6587\u4EF6\uFF08\u542B \`openspec/changes/<id>/.fet/\`\uFF09
|
|
3126
|
+
|
|
3127
|
+
## \u987B\u5148\u5F81\u5F97\u7528\u6237\u540C\u610F
|
|
3128
|
+
|
|
3129
|
+
\u5728**\u5141\u8BB8\u8303\u56F4\u5916**\u521B\u5EFA\u3001\u4FEE\u6539\u6216\u5220\u9664\u6587\u4EF6\u4E4B\u524D\uFF1A
|
|
3130
|
+
|
|
3131
|
+
1. \u5217\u51FA\u5C06\u8981\u4FEE\u6539\u7684\u8DEF\u5F84\u53CA\u539F\u56E0\u3002
|
|
3132
|
+
2. \u7B49\u5F85\u7528\u6237\u660E\u786E\u540C\u610F\uFF08\u4E0D\u8981\u9ED8\u8BA4\u6C89\u9ED8\u5373\u540C\u610F\uFF09\u3002
|
|
3133
|
+
3. \u4FDD\u6301\u6700\u5C0F\u6539\u52A8\u3002
|
|
3134
|
+
|
|
3135
|
+
\u5E38\u89C1\u9700\u5BA1\u6279\u8DEF\u5F84\uFF1A\`tests/**\`\u3001\`.github/**\`\u3001\`.workflow/**\`\u3001\`.cursor/**\`\u3001\`.codex/**\`\u3001\`AGENTS.md\`\u3001\`README*\`\u3001\`.env*\`\u3002
|
|
3136
|
+
|
|
3137
|
+
## \u4ED3\u5E93\u6839\u76EE\u5F55\u914D\u7F6E\uFF08\u59CB\u7EC8\u987B\u8BE2\u95EE\uFF0C\u7981\u6B62\u9759\u9ED8\u4FEE\u6539\uFF09
|
|
3138
|
+
|
|
3139
|
+
\u4EE5\u4E0B\u6587\u4EF6\u4F4D\u4E8E**\u9879\u76EE\u6839\u76EE\u5F55**\uFF08\u8DEF\u5F84\u4E2D\u65E0\u5B50\u76EE\u5F55\uFF09\uFF0C\u4E00\u5F8B\u89C6\u4E3A\u57FA\u7840\u8BBE\u65BD\u7EA7\u6539\u52A8\uFF1A
|
|
3140
|
+
|
|
3141
|
+
${renderRootConfigPathList(language)}
|
|
3142
|
+
|
|
3143
|
+
\u6D41\u7A0B\uFF1A
|
|
3144
|
+
|
|
3145
|
+
1. \u660E\u786E\u5217\u51FA**\u5B8C\u6574\u6587\u4EF6\u540D**\uFF08\u5982 \`package.json\`\u3001\`.eslintrc.js\`\u3001\`.gitignore\`\uFF09\u3002
|
|
3146
|
+
2. \u8BF4\u660E**\u6BCF\u9879\u4FEE\u6539\u7684\u539F\u56E0**\u3002
|
|
3147
|
+
3. \u83B7\u5F97\u7528\u6237**\u660E\u786E\u540C\u610F**\u540E\u518D\u5199\u5165\u3002
|
|
3148
|
+
4. \u4E0D\u8981\u4EE5\u300C\u987A\u4FBF\u4FEE lint/\u683C\u5F0F\u300D\u4E3A\u7531\u64C5\u81EA\u6539\u6839\u76EE\u5F55\u914D\u7F6E\u3002
|
|
3149
|
+
|
|
3150
|
+
## \u672A\u7ECF\u540C\u610F\u7981\u6B62
|
|
3151
|
+
|
|
3152
|
+
- \u64C5\u81EA\u6539\u4F9D\u8D56\u6216\u9501\u6587\u4EF6\uFF08\`package.json\`\u3001\`package-lock.json\` \u7B49\uFF09\uFF0C\u9664\u975E\u7528\u6237\u660E\u786E\u8981\u6C42
|
|
3153
|
+
- \u64C5\u81EA\u6539\u6839\u76EE\u5F55 lint/\u683C\u5F0F\u5316/\u5DE5\u5177\u94FE\u914D\u7F6E\uFF08\`.eslintrc*\`\u3001\`.stylelint*\`\u3001\`.checkrc.js\`\u3001\`.gitignore\` \u7B49\uFF09\uFF0C\u9664\u975E\u7528\u6237\u660E\u786E\u8981\u6C42
|
|
3154
|
+
- \u4FEE\u6539\u5BC6\u94A5\u6216\u51ED\u8BC1\u6587\u4EF6
|
|
3155
|
+
- \u7528 shell \u91CD\u5B9A\u5411\u6216\u7834\u574F\u6027 git \u547D\u4EE4\u7ED5\u8FC7\u672C\u7B56\u7565
|
|
3156
|
+
|
|
3157
|
+
## Cursor \u786C\u95E8\u7981
|
|
3158
|
+
|
|
3159
|
+
\u5B89\u88C5 \`.cursor/hooks/fet-guard-write-paths.mjs\` \u540E\uFF0C\u8D85\u51FA\u8303\u56F4\u7684**\u5199\u5DE5\u5177**\u4F1A\u5F39\u51FA\u5BA1\u6279\uFF08\`permission: ask\`\uFF09\uFF1B\`fet-guard-shell-writes.mjs\` \u7EA6\u675F\u53EF\u80FD\u5199\u6587\u4EF6\u7684 shell\u3002\u4EC5\u89C4\u5219\u4E0D\u591F\u2014\u2014\`fet init\` \u540E\u8BF7\u4FDD\u6301 hooks \u542F\u7528\u3002`;
|
|
3160
|
+
}
|
|
3161
|
+
function renderCursorWriteBoundaryRule(language) {
|
|
3162
|
+
const description = language === "en" ? "FET write boundary: default edits only under src/, openspec/, and .fet/" : "FET \u5199\u8DEF\u5F84\u8FB9\u754C\uFF1A\u9ED8\u8BA4\u4EC5\u53EF\u6539 src/\u3001openspec/\u3001.fet/";
|
|
3163
|
+
return `<!-- FET:MANAGED
|
|
3164
|
+
schemaVersion: 1
|
|
3165
|
+
fetVersion: ${FET_VERSION}
|
|
3166
|
+
generator: cursor-adapter
|
|
3167
|
+
adapterVersion: 1
|
|
3168
|
+
FET:END -->
|
|
3169
|
+
|
|
3170
|
+
---
|
|
3171
|
+
description: ${description}
|
|
3172
|
+
alwaysApply: true
|
|
3173
|
+
---
|
|
3174
|
+
|
|
3175
|
+
${renderWriteBoundaryPolicyBody(language)}
|
|
3176
|
+
|
|
3177
|
+
${renderWriteBoundaryGuardrail(language)}
|
|
3178
|
+
`;
|
|
3179
|
+
}
|
|
3180
|
+
function renderCodexWriteBoundaryGuide(language) {
|
|
3181
|
+
return `<!-- FET:MANAGED
|
|
3182
|
+
schemaVersion: 1
|
|
3183
|
+
fetVersion: ${FET_VERSION}
|
|
3184
|
+
generator: codex-adapter
|
|
3185
|
+
adapterVersion: 1
|
|
3186
|
+
FET:END -->
|
|
3187
|
+
|
|
3188
|
+
# ${language === "en" ? "Write path boundary (Codex)" : "\u5199\u8DEF\u5F84\u8FB9\u754C\uFF08Codex\uFF09"}
|
|
3189
|
+
|
|
3190
|
+
${renderWriteBoundaryPolicyBody(language)}
|
|
3191
|
+
|
|
3192
|
+
${renderWriteBoundaryGuardrail(language)}
|
|
3193
|
+
`;
|
|
3194
|
+
}
|
|
3195
|
+
function renderCursorWritePathsHookMjs() {
|
|
3196
|
+
const allowPrefixes = [...WRITE_BOUNDARY_ALLOW_PREFIXES];
|
|
3197
|
+
const rootExact = [...WRITE_BOUNDARY_ROOT_CONFIG_EXACT];
|
|
3198
|
+
const rootPrefixes = [...WRITE_BOUNDARY_ROOT_CONFIG_BASENAME_PREFIXES];
|
|
3199
|
+
return `#!/usr/bin/env node
|
|
3200
|
+
/**
|
|
3201
|
+
* FET:MANAGED
|
|
3202
|
+
* adapterVersion: 1
|
|
3203
|
+
* write-path guard for Cursor preToolUse (Write/StrReplace/EditNotebook/Delete).
|
|
3204
|
+
*/
|
|
3205
|
+
import { readFileSync } from "node:fs";
|
|
3206
|
+
|
|
3207
|
+
const ALLOW_PREFIXES = ${JSON.stringify(allowPrefixes)};
|
|
3208
|
+
const ROOT_CONFIG_EXACT = ${JSON.stringify(rootExact)};
|
|
3209
|
+
const ROOT_CONFIG_PREFIXES = ${JSON.stringify(rootPrefixes)};
|
|
3210
|
+
|
|
3211
|
+
function normalizePath(path) {
|
|
3212
|
+
return String(path ?? "")
|
|
3213
|
+
.replaceAll("\\\\", "/")
|
|
3214
|
+
.replace(/^\\.\\/+/, "")
|
|
3215
|
+
.replace(/^\\/+/u, "");
|
|
3216
|
+
}
|
|
3217
|
+
|
|
3218
|
+
function isRootConfigPath(path) {
|
|
3219
|
+
const normalized = normalizePath(path);
|
|
3220
|
+
if (!normalized || normalized.includes("/")) return false;
|
|
3221
|
+
const base = normalized.toLowerCase();
|
|
3222
|
+
if (ROOT_CONFIG_EXACT.includes(base)) return true;
|
|
3223
|
+
return ROOT_CONFIG_PREFIXES.some((prefix) => base === prefix || base.startsWith(prefix));
|
|
3224
|
+
}
|
|
3225
|
+
|
|
3226
|
+
function classifyPath(path) {
|
|
3227
|
+
const normalized = normalizePath(path);
|
|
3228
|
+
if (!normalized) return "ask";
|
|
3229
|
+
for (const prefix of ALLOW_PREFIXES) {
|
|
3230
|
+
const bare = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
3231
|
+
if (normalized === bare || normalized.startsWith(prefix)) return "allow";
|
|
3232
|
+
}
|
|
3233
|
+
if (normalized.startsWith(".fet/") || normalized.includes("/.fet/")) return "allow";
|
|
3234
|
+
if (isRootConfigPath(normalized)) return "root_config";
|
|
3235
|
+
return "ask";
|
|
3236
|
+
}
|
|
3237
|
+
|
|
3238
|
+
function extractPaths(payload) {
|
|
3239
|
+
const paths = [];
|
|
3240
|
+
const tool = payload?.tool_name ?? payload?.toolName ?? "";
|
|
3241
|
+
const input = payload?.tool_input ?? payload?.toolInput ?? payload?.input ?? {};
|
|
3242
|
+
if (tool === "Write" || tool === "StrReplace" || tool === "Delete") {
|
|
3243
|
+
if (input.path) paths.push(input.path);
|
|
3244
|
+
}
|
|
3245
|
+
if (tool === "EditNotebook" && input.target_notebook) {
|
|
3246
|
+
paths.push(input.target_notebook);
|
|
3247
|
+
}
|
|
3248
|
+
return paths;
|
|
3249
|
+
}
|
|
3250
|
+
|
|
3251
|
+
function respond(decision, paths) {
|
|
3252
|
+
if (decision === "allow") {
|
|
3253
|
+
process.stdout.write(JSON.stringify({ permission: "allow" }));
|
|
3254
|
+
return;
|
|
3255
|
+
}
|
|
3256
|
+
const list = paths.map((p) => normalizePath(p)).filter(Boolean).join(", ") || "(unknown path)";
|
|
3257
|
+
const hasRootConfig = paths.some((p) => classifyPath(p) === "root_config");
|
|
3258
|
+
const msg = hasRootConfig
|
|
3259
|
+
? "FET: repo-root config file(s) require your explicit approval (.gitignore, package.json, .eslintrc*, .stylelint*, .checkrc.js, lockfiles, etc.). Approve only if you requested this tooling change."
|
|
3260
|
+
: "FET write boundary: this edit is outside src/, openspec/, or **/.fet/. Approve only if you intend to modify protected paths.";
|
|
3261
|
+
process.stdout.write(
|
|
3262
|
+
JSON.stringify({
|
|
3263
|
+
permission: "ask",
|
|
3264
|
+
user_message: msg + " Paths: " + list,
|
|
3265
|
+
agent_message: hasRootConfig
|
|
3266
|
+
? "Repo-root config edit blocked. Explain why each root config file must change and wait for user approval."
|
|
3267
|
+
: "Out-of-scope write blocked pending user approval. List why each path is needed, then retry after approval."
|
|
3268
|
+
})
|
|
3269
|
+
);
|
|
3270
|
+
}
|
|
3271
|
+
|
|
3272
|
+
const raw = readFileSync(0, "utf8");
|
|
3273
|
+
let payload = {};
|
|
3274
|
+
try {
|
|
3275
|
+
payload = JSON.parse(raw || "{}");
|
|
3276
|
+
} catch {
|
|
3277
|
+
process.stdout.write(JSON.stringify({ permission: "ask", user_message: "FET write guard: invalid hook payload." }));
|
|
3278
|
+
process.exit(0);
|
|
3279
|
+
}
|
|
3280
|
+
|
|
3281
|
+
const paths = extractPaths(payload);
|
|
3282
|
+
if (paths.length === 0) {
|
|
3283
|
+
respond("allow", paths);
|
|
3284
|
+
process.exit(0);
|
|
3285
|
+
}
|
|
3286
|
+
|
|
3287
|
+
const decisions = paths.map((p) => classifyPath(p));
|
|
3288
|
+
if (decisions.every((d) => d === "allow")) {
|
|
3289
|
+
respond("allow", paths);
|
|
3290
|
+
} else {
|
|
3291
|
+
respond("ask", paths);
|
|
3292
|
+
}
|
|
3293
|
+
process.exit(0);
|
|
3294
|
+
`;
|
|
3295
|
+
}
|
|
3296
|
+
function renderCursorShellWriteHookMjs() {
|
|
3297
|
+
return `#!/usr/bin/env node
|
|
3298
|
+
/**
|
|
3299
|
+
* FET:MANAGED
|
|
3300
|
+
* adapterVersion: 1
|
|
3301
|
+
* shell guard for commands that may write outside the FET write boundary.
|
|
3302
|
+
*/
|
|
3303
|
+
import { readFileSync } from "node:fs";
|
|
3304
|
+
|
|
3305
|
+
const REDIRECT = />\\s*[^\\s|&;]+/;
|
|
3306
|
+
const GIT_WRITE = /\\bgit\\s+(checkout|restore|reset|clean|apply)\\b/i;
|
|
3307
|
+
const PKG_WRITE = /\\b(npm|pnpm|yarn|bun)\\s+(install|ci|add|remove|update)\\b/i;
|
|
3308
|
+
const STREAM_EDIT = /\\b(sed|perl)\\s+[^\\n]*-i\\b/i;
|
|
3309
|
+
const TEE = /\\btee\\s+[^|;&\\n]+/i;
|
|
3310
|
+
|
|
3311
|
+
function respondAsk(command, reason) {
|
|
3312
|
+
process.stdout.write(
|
|
3313
|
+
JSON.stringify({
|
|
3314
|
+
permission: "ask",
|
|
3315
|
+
user_message: "FET shell guard: " + reason,
|
|
3316
|
+
agent_message: "Shell command may modify files outside src/openspec/.fet. Command: " + command
|
|
3317
|
+
})
|
|
3318
|
+
);
|
|
3319
|
+
}
|
|
3320
|
+
|
|
3321
|
+
const raw = readFileSync(0, "utf8");
|
|
3322
|
+
let payload = {};
|
|
3323
|
+
try {
|
|
3324
|
+
payload = JSON.parse(raw || "{}");
|
|
3325
|
+
} catch {
|
|
3326
|
+
respondAsk("", "invalid hook payload");
|
|
3327
|
+
process.exit(0);
|
|
3328
|
+
}
|
|
3329
|
+
|
|
3330
|
+
const command = String(payload?.command ?? "");
|
|
3331
|
+
if (!command.trim()) {
|
|
3332
|
+
process.stdout.write(JSON.stringify({ permission: "allow" }));
|
|
3333
|
+
process.exit(0);
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
if (
|
|
3337
|
+
REDIRECT.test(command) ||
|
|
3338
|
+
GIT_WRITE.test(command) ||
|
|
3339
|
+
PKG_WRITE.test(command) ||
|
|
3340
|
+
STREAM_EDIT.test(command) ||
|
|
3341
|
+
TEE.test(command)
|
|
3342
|
+
) {
|
|
3343
|
+
respondAsk(command, "command may write or replace project files outside the default FET scope");
|
|
3344
|
+
process.exit(0);
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
process.stdout.write(JSON.stringify({ permission: "allow" }));
|
|
3348
|
+
process.exit(0);
|
|
3349
|
+
`;
|
|
3350
|
+
}
|
|
3351
|
+
function renderCursorHooksJson() {
|
|
3352
|
+
return JSON.stringify(
|
|
3353
|
+
{
|
|
3354
|
+
version: 1,
|
|
3355
|
+
_fet: {
|
|
3356
|
+
writeBoundary: true,
|
|
3357
|
+
fetVersion: FET_VERSION
|
|
3358
|
+
},
|
|
3359
|
+
hooks: {
|
|
3360
|
+
preToolUse: [
|
|
3361
|
+
{
|
|
3362
|
+
command: ".cursor/hooks/fet-guard-write-paths.mjs",
|
|
3363
|
+
matcher: "Write|StrReplace|EditNotebook|Delete",
|
|
3364
|
+
failClosed: true
|
|
3365
|
+
}
|
|
3366
|
+
],
|
|
3367
|
+
beforeShellExecution: [
|
|
3368
|
+
{
|
|
3369
|
+
command: ".cursor/hooks/fet-guard-shell-writes.mjs",
|
|
3370
|
+
failClosed: true
|
|
3371
|
+
}
|
|
3372
|
+
]
|
|
3373
|
+
}
|
|
3374
|
+
},
|
|
3375
|
+
null,
|
|
3376
|
+
2
|
|
3377
|
+
);
|
|
3378
|
+
}
|
|
3379
|
+
function mergeCursorHooksJson(existingContent, fetContent) {
|
|
3380
|
+
const fet = JSON.parse(fetContent);
|
|
3381
|
+
if (!existingContent?.trim()) {
|
|
3382
|
+
return fetContent;
|
|
3383
|
+
}
|
|
3384
|
+
let existing;
|
|
3385
|
+
try {
|
|
3386
|
+
existing = JSON.parse(existingContent);
|
|
3387
|
+
} catch {
|
|
3388
|
+
return fetContent;
|
|
3389
|
+
}
|
|
3390
|
+
const merged = {
|
|
3391
|
+
version: existing.version ?? fet.version ?? 1,
|
|
3392
|
+
_fet: { ...existing._fet, ...fet._fet, writeBoundary: true },
|
|
3393
|
+
hooks: { ...existing.hooks }
|
|
3394
|
+
};
|
|
3395
|
+
for (const [event, fetEntries] of Object.entries(fet.hooks ?? {})) {
|
|
3396
|
+
const current = [...merged.hooks?.[event] ?? []];
|
|
3397
|
+
for (const entry of fetEntries) {
|
|
3398
|
+
const duplicate = current.some((item) => item.command === entry.command);
|
|
3399
|
+
if (!duplicate) {
|
|
3400
|
+
current.push(entry);
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
merged.hooks = merged.hooks ?? {};
|
|
3404
|
+
merged.hooks[event] = current;
|
|
3405
|
+
}
|
|
3406
|
+
return `${JSON.stringify(merged, null, 2)}
|
|
3407
|
+
`;
|
|
3408
|
+
}
|
|
3409
|
+
|
|
3003
3410
|
// src/commands/update-context.ts
|
|
3004
3411
|
async function updateContextCommand(ctx) {
|
|
3005
3412
|
let contextResult = { warnings: [] };
|
|
@@ -3161,8 +3568,8 @@ async function exists4(path) {
|
|
|
3161
3568
|
}
|
|
3162
3569
|
|
|
3163
3570
|
// src/commands/proxy.ts
|
|
3164
|
-
import { readFile as
|
|
3165
|
-
import { join as
|
|
3571
|
+
import { readFile as readFile19 } from "fs/promises";
|
|
3572
|
+
import { join as join26 } from "path";
|
|
3166
3573
|
|
|
3167
3574
|
// src/figma-guard.ts
|
|
3168
3575
|
import { readdir as readdir3, readFile as readFile10, stat as stat7 } from "fs/promises";
|
|
@@ -3262,66 +3669,812 @@ async function ensureChangeFigmaStopHandoff(options) {
|
|
|
3262
3669
|
await atomicWrite(stopAbsolutePath, stopContent);
|
|
3263
3670
|
written = true;
|
|
3264
3671
|
}
|
|
3265
|
-
let applyInstructionsPath;
|
|
3266
|
-
if (config.mode === "require_before_ui") {
|
|
3267
|
-
applyInstructionsPath = figmaApplyInstructionsRelativePath(options.changeId);
|
|
3268
|
-
const applyAbsolutePath = join16(options.projectRoot, applyInstructionsPath);
|
|
3269
|
-
const applyContent = renderChangeFigmaApplyInstructions({
|
|
3672
|
+
let applyInstructionsPath;
|
|
3673
|
+
if (config.mode === "require_before_ui") {
|
|
3674
|
+
applyInstructionsPath = figmaApplyInstructionsRelativePath(options.changeId);
|
|
3675
|
+
const applyAbsolutePath = join16(options.projectRoot, applyInstructionsPath);
|
|
3676
|
+
const applyContent = renderChangeFigmaApplyInstructions({
|
|
3677
|
+
changeId: options.changeId,
|
|
3678
|
+
generatedAt,
|
|
3679
|
+
urls,
|
|
3680
|
+
sources,
|
|
3681
|
+
language: options.language
|
|
3682
|
+
});
|
|
3683
|
+
const existingApply = await readOptional3(applyAbsolutePath);
|
|
3684
|
+
if (existingApply !== applyContent) {
|
|
3685
|
+
await atomicWrite(applyAbsolutePath, applyContent);
|
|
3686
|
+
written = true;
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
return {
|
|
3690
|
+
path: stopRelativePath,
|
|
3691
|
+
applyInstructionsPath,
|
|
3692
|
+
written,
|
|
3693
|
+
urls,
|
|
3694
|
+
sources,
|
|
3695
|
+
mode: config.mode
|
|
3696
|
+
};
|
|
3697
|
+
}
|
|
3698
|
+
async function listMarkdownFiles(root) {
|
|
3699
|
+
const files = [];
|
|
3700
|
+
await walk(root, files);
|
|
3701
|
+
return files;
|
|
3702
|
+
}
|
|
3703
|
+
async function walk(dir, files) {
|
|
3704
|
+
try {
|
|
3705
|
+
const entries = await readdir3(dir, { withFileTypes: true });
|
|
3706
|
+
for (const entry of entries) {
|
|
3707
|
+
const fullPath = join16(dir, entry.name);
|
|
3708
|
+
if (entry.isDirectory()) {
|
|
3709
|
+
await walk(fullPath, files);
|
|
3710
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
3711
|
+
files.push(fullPath);
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
} catch {
|
|
3715
|
+
return;
|
|
3716
|
+
}
|
|
3717
|
+
}
|
|
3718
|
+
async function readOptional3(path) {
|
|
3719
|
+
try {
|
|
3720
|
+
await stat7(path);
|
|
3721
|
+
return await readFile10(path, "utf8");
|
|
3722
|
+
} catch {
|
|
3723
|
+
return null;
|
|
3724
|
+
}
|
|
3725
|
+
}
|
|
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,
|
|
3270
4376
|
changeId: options.changeId,
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
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
|
|
3275
4390
|
});
|
|
3276
|
-
const existingApply = await readOptional3(applyAbsolutePath);
|
|
3277
|
-
if (existingApply !== applyContent) {
|
|
3278
|
-
await atomicWrite(applyAbsolutePath, applyContent);
|
|
3279
|
-
written = true;
|
|
3280
|
-
}
|
|
3281
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
|
+
);
|
|
3282
4425
|
return {
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
4426
|
+
manifestPath: manifestPath3,
|
|
4427
|
+
nodesPath,
|
|
4428
|
+
instructionsPath,
|
|
4429
|
+
assetsDir: join21("openspec", "changes", options.changeId, ".fet", "figma-assets"),
|
|
4430
|
+
manifest,
|
|
4431
|
+
written: true
|
|
3289
4432
|
};
|
|
3290
4433
|
}
|
|
3291
|
-
async function
|
|
3292
|
-
const
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
}
|
|
3296
|
-
async function walk(dir, files) {
|
|
3297
|
-
try {
|
|
3298
|
-
const entries = await readdir3(dir, { withFileTypes: true });
|
|
3299
|
-
for (const entry of entries) {
|
|
3300
|
-
const fullPath = join16(dir, entry.name);
|
|
3301
|
-
if (entry.isDirectory()) {
|
|
3302
|
-
await walk(fullPath, files);
|
|
3303
|
-
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
3304
|
-
files.push(fullPath);
|
|
3305
|
-
}
|
|
3306
|
-
}
|
|
3307
|
-
} catch {
|
|
3308
|
-
return;
|
|
4434
|
+
async function tryAutoSyncFigmaAssets(options) {
|
|
4435
|
+
const syncConfig = await loadFigmaSyncConfig(options.projectRoot);
|
|
4436
|
+
if (!syncConfig.enabled) {
|
|
4437
|
+
return { synced: false, skipped: true, missingToken: false };
|
|
3309
4438
|
}
|
|
3310
|
-
}
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
return
|
|
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
|
+
};
|
|
3317
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 };
|
|
3318
4471
|
}
|
|
3319
4472
|
|
|
3320
4473
|
// src/ui-display-contract.ts
|
|
3321
4474
|
import { existsSync as existsSync2 } from "fs";
|
|
3322
|
-
import { readdir as
|
|
3323
|
-
import { join as
|
|
3324
|
-
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";
|
|
3325
4478
|
var DEFAULT_CONFIG2 = {
|
|
3326
4479
|
enabled: true
|
|
3327
4480
|
};
|
|
@@ -3331,8 +4484,8 @@ var BACKTICK_PATH_PATTERN = /`([^`]+\.(?:ya?ml|json))`/gi;
|
|
|
3331
4484
|
var OPENAPI_BARE_PATTERN = /\b(openapi\.ya?ml|swagger\.ya?ml|swagger\.json)\b/gi;
|
|
3332
4485
|
async function loadUiDisplayContractConfig(projectRoot) {
|
|
3333
4486
|
try {
|
|
3334
|
-
const raw = await
|
|
3335
|
-
const doc =
|
|
4487
|
+
const raw = await readFile14(join22(projectRoot, "openspec", "config.yaml"), "utf8");
|
|
4488
|
+
const doc = parseDocument5(raw);
|
|
3336
4489
|
const fetNode = doc.get("fet", true);
|
|
3337
4490
|
const node = fetNode?.get?.("uiDisplayContract");
|
|
3338
4491
|
if (!node || typeof node.get !== "function") {
|
|
@@ -3347,12 +4500,12 @@ async function loadUiDisplayContractConfig(projectRoot) {
|
|
|
3347
4500
|
}
|
|
3348
4501
|
}
|
|
3349
4502
|
async function collectApiSourcesFromChange(projectRoot, changeId) {
|
|
3350
|
-
const changePath =
|
|
4503
|
+
const changePath = join22(projectRoot, "openspec", "changes", changeId);
|
|
3351
4504
|
const paths = /* @__PURE__ */ new Set();
|
|
3352
4505
|
const sources = [];
|
|
3353
4506
|
const candidates = ["proposal.md", "tasks.md", "design.md"];
|
|
3354
4507
|
for (const name of candidates) {
|
|
3355
|
-
const filePath =
|
|
4508
|
+
const filePath = join22(changePath, name);
|
|
3356
4509
|
const content = await readOptional4(filePath);
|
|
3357
4510
|
if (!content) {
|
|
3358
4511
|
continue;
|
|
@@ -3365,7 +4518,7 @@ async function collectApiSourcesFromChange(projectRoot, changeId) {
|
|
|
3365
4518
|
}
|
|
3366
4519
|
}
|
|
3367
4520
|
}
|
|
3368
|
-
const specsPath =
|
|
4521
|
+
const specsPath = join22(changePath, "specs");
|
|
3369
4522
|
for (const filePath of await listMarkdownFiles2(specsPath)) {
|
|
3370
4523
|
const content = await readOptional4(filePath);
|
|
3371
4524
|
if (!content) {
|
|
@@ -3373,7 +4526,7 @@ async function collectApiSourcesFromChange(projectRoot, changeId) {
|
|
|
3373
4526
|
}
|
|
3374
4527
|
const found = extractApiDocPaths(content, projectRoot);
|
|
3375
4528
|
if (found.length) {
|
|
3376
|
-
sources.push(
|
|
4529
|
+
sources.push(relative4(projectRoot, filePath).replaceAll("\\", "/"));
|
|
3377
4530
|
for (const path of found) {
|
|
3378
4531
|
paths.add(path);
|
|
3379
4532
|
}
|
|
@@ -3399,7 +4552,7 @@ function extractApiDocPaths(content, projectRoot) {
|
|
|
3399
4552
|
const common = ["openapi.yaml", "openapi.yml", "docs/openapi.yaml", "swagger.yaml", "swagger.json"];
|
|
3400
4553
|
for (const candidate of common) {
|
|
3401
4554
|
const normalized = normalizeRepoPath(candidate, projectRoot);
|
|
3402
|
-
if (normalized && existsSync2(
|
|
4555
|
+
if (normalized && existsSync2(join22(projectRoot, normalized))) {
|
|
3403
4556
|
found.add(normalized);
|
|
3404
4557
|
}
|
|
3405
4558
|
}
|
|
@@ -3409,7 +4562,7 @@ function extractApiDocPaths(content, projectRoot) {
|
|
|
3409
4562
|
async function extractOpenApiSchemas(projectRoot, relativePaths) {
|
|
3410
4563
|
const schemas = [];
|
|
3411
4564
|
for (const rel of relativePaths) {
|
|
3412
|
-
const absolute =
|
|
4565
|
+
const absolute = join22(projectRoot, rel);
|
|
3413
4566
|
const content = await readOptional4(absolute);
|
|
3414
4567
|
if (!content) {
|
|
3415
4568
|
continue;
|
|
@@ -3485,9 +4638,9 @@ async function ensureChangeUiDisplayContract(options) {
|
|
|
3485
4638
|
if (!config.enabled) {
|
|
3486
4639
|
return null;
|
|
3487
4640
|
}
|
|
3488
|
-
const
|
|
4641
|
+
const figma2 = await collectFigmaUrlsFromChange(options.projectRoot, options.changeId);
|
|
3489
4642
|
const api = await collectApiSourcesFromChange(options.projectRoot, options.changeId);
|
|
3490
|
-
const hasFigma =
|
|
4643
|
+
const hasFigma = figma2.urls.length > 0;
|
|
3491
4644
|
const hasApi = api.paths.length > 0;
|
|
3492
4645
|
if (!hasFigma && !hasApi) {
|
|
3493
4646
|
return null;
|
|
@@ -3500,14 +4653,14 @@ async function ensureChangeUiDisplayContract(options) {
|
|
|
3500
4653
|
changeId: options.changeId,
|
|
3501
4654
|
generatedAt,
|
|
3502
4655
|
fetVersion: options.fetVersion,
|
|
3503
|
-
figmaUrls:
|
|
3504
|
-
figmaSources:
|
|
4656
|
+
figmaUrls: figma2.urls,
|
|
4657
|
+
figmaSources: figma2.sources,
|
|
3505
4658
|
apiPaths: api.paths,
|
|
3506
4659
|
apiSources: api.sources,
|
|
3507
4660
|
apiSchemas
|
|
3508
4661
|
});
|
|
3509
4662
|
const contractContent = renderUiDisplayContractYaml(doc);
|
|
3510
|
-
const contractAbsolutePath =
|
|
4663
|
+
const contractAbsolutePath = join22(options.projectRoot, contractRelativePath);
|
|
3511
4664
|
const existingContract = await readOptional4(contractAbsolutePath);
|
|
3512
4665
|
let written = false;
|
|
3513
4666
|
if (existingContract !== contractContent) {
|
|
@@ -3522,7 +4675,7 @@ async function ensureChangeUiDisplayContract(options) {
|
|
|
3522
4675
|
hasFigma,
|
|
3523
4676
|
hasApi
|
|
3524
4677
|
});
|
|
3525
|
-
const applyAbsolutePath =
|
|
4678
|
+
const applyAbsolutePath = join22(options.projectRoot, applyRelativePath);
|
|
3526
4679
|
const existingApply = await readOptional4(applyAbsolutePath);
|
|
3527
4680
|
if (existingApply !== applyContent) {
|
|
3528
4681
|
await atomicWrite(applyAbsolutePath, applyContent);
|
|
@@ -3589,9 +4742,9 @@ async function listMarkdownFiles2(root) {
|
|
|
3589
4742
|
}
|
|
3590
4743
|
async function walk2(dir, files) {
|
|
3591
4744
|
try {
|
|
3592
|
-
const entries = await
|
|
4745
|
+
const entries = await readdir5(dir, { withFileTypes: true });
|
|
3593
4746
|
for (const entry of entries) {
|
|
3594
|
-
const fullPath =
|
|
4747
|
+
const fullPath = join22(dir, entry.name);
|
|
3595
4748
|
if (entry.isDirectory()) {
|
|
3596
4749
|
await walk2(fullPath, files);
|
|
3597
4750
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
@@ -3604,8 +4757,8 @@ async function walk2(dir, files) {
|
|
|
3604
4757
|
}
|
|
3605
4758
|
async function readOptional4(path) {
|
|
3606
4759
|
try {
|
|
3607
|
-
await
|
|
3608
|
-
return await
|
|
4760
|
+
await stat9(path);
|
|
4761
|
+
return await readFile14(path, "utf8");
|
|
3609
4762
|
} catch {
|
|
3610
4763
|
return null;
|
|
3611
4764
|
}
|
|
@@ -3650,9 +4803,9 @@ async function assertChangeExists(ctx, changeId) {
|
|
|
3650
4803
|
}
|
|
3651
4804
|
|
|
3652
4805
|
// src/tdd/config.ts
|
|
3653
|
-
import { readFile as
|
|
3654
|
-
import { join as
|
|
3655
|
-
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";
|
|
3656
4809
|
var DEFAULT_CONFIG3 = {
|
|
3657
4810
|
enabled: true,
|
|
3658
4811
|
mode: "require_before_apply",
|
|
@@ -3660,8 +4813,8 @@ var DEFAULT_CONFIG3 = {
|
|
|
3660
4813
|
};
|
|
3661
4814
|
async function loadTddConfig(projectRoot) {
|
|
3662
4815
|
try {
|
|
3663
|
-
const raw = await
|
|
3664
|
-
const doc =
|
|
4816
|
+
const raw = await readFile15(join23(projectRoot, "openspec", "config.yaml"), "utf8");
|
|
4817
|
+
const doc = parseDocument6(raw);
|
|
3665
4818
|
const fetNode = doc.get("fet", true);
|
|
3666
4819
|
const node = fetNode?.get?.("tdd");
|
|
3667
4820
|
if (!node || typeof node.get !== "function") {
|
|
@@ -3695,70 +4848,12 @@ function isTddRequired(config) {
|
|
|
3695
4848
|
return config.enabled && config.mode === "require_before_apply";
|
|
3696
4849
|
}
|
|
3697
4850
|
|
|
3698
|
-
// src/tdd/fingerprint.ts
|
|
3699
|
-
import { createHash } from "crypto";
|
|
3700
|
-
import { readdir as readdir5, readFile as readFile13, stat as stat9 } from "fs/promises";
|
|
3701
|
-
import { join as join19, relative as relative4 } from "path";
|
|
3702
|
-
async function collectPlanningSources(projectRoot, changeId) {
|
|
3703
|
-
const changeRoot = join19(projectRoot, "openspec", "changes", changeId);
|
|
3704
|
-
const sources = [];
|
|
3705
|
-
const rootFiles = ["proposal.md", "tasks.md", "design.md"];
|
|
3706
|
-
for (const name of rootFiles) {
|
|
3707
|
-
const path = join19(changeRoot, name);
|
|
3708
|
-
if (await exists5(path)) {
|
|
3709
|
-
sources.push(relative4(projectRoot, path).replace(/\\/g, "/"));
|
|
3710
|
-
}
|
|
3711
|
-
}
|
|
3712
|
-
const specsDir = join19(changeRoot, "specs");
|
|
3713
|
-
if (await exists5(specsDir)) {
|
|
3714
|
-
for (const file of await walkFiles(specsDir)) {
|
|
3715
|
-
if (file.endsWith(".md")) {
|
|
3716
|
-
sources.push(relative4(projectRoot, file).replace(/\\/g, "/"));
|
|
3717
|
-
}
|
|
3718
|
-
}
|
|
3719
|
-
}
|
|
3720
|
-
return sources.sort();
|
|
3721
|
-
}
|
|
3722
|
-
async function computePlanningFingerprint(projectRoot, changeId) {
|
|
3723
|
-
const sources = await collectPlanningSources(projectRoot, changeId);
|
|
3724
|
-
const hash = createHash("sha256");
|
|
3725
|
-
for (const source of sources) {
|
|
3726
|
-
const content = await readFile13(join19(projectRoot, source), "utf8");
|
|
3727
|
-
hash.update(source);
|
|
3728
|
-
hash.update("\0");
|
|
3729
|
-
hash.update(content);
|
|
3730
|
-
hash.update("\0");
|
|
3731
|
-
}
|
|
3732
|
-
return `sha256:${hash.digest("hex")}`;
|
|
3733
|
-
}
|
|
3734
|
-
async function walkFiles(dir) {
|
|
3735
|
-
const entries = await readdir5(dir, { withFileTypes: true });
|
|
3736
|
-
const files = [];
|
|
3737
|
-
for (const entry of entries) {
|
|
3738
|
-
const path = join19(dir, entry.name);
|
|
3739
|
-
if (entry.isDirectory()) {
|
|
3740
|
-
files.push(...await walkFiles(path));
|
|
3741
|
-
} else if (entry.isFile()) {
|
|
3742
|
-
files.push(path);
|
|
3743
|
-
}
|
|
3744
|
-
}
|
|
3745
|
-
return files;
|
|
3746
|
-
}
|
|
3747
|
-
async function exists5(path) {
|
|
3748
|
-
try {
|
|
3749
|
-
await stat9(path);
|
|
3750
|
-
return true;
|
|
3751
|
-
} catch {
|
|
3752
|
-
return false;
|
|
3753
|
-
}
|
|
3754
|
-
}
|
|
3755
|
-
|
|
3756
4851
|
// src/tdd/manifest.ts
|
|
3757
|
-
import { mkdir as
|
|
3758
|
-
import { dirname as dirname8, join as
|
|
3759
|
-
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";
|
|
3760
4855
|
function tddManifestPath(projectRoot, changeId) {
|
|
3761
|
-
return
|
|
4856
|
+
return join24(projectRoot, tddManifestRelativePath(changeId));
|
|
3762
4857
|
}
|
|
3763
4858
|
async function readTddManifest(projectRoot, changeId) {
|
|
3764
4859
|
const path = tddManifestPath(projectRoot, changeId);
|
|
@@ -3767,7 +4862,7 @@ async function readTddManifest(projectRoot, changeId) {
|
|
|
3767
4862
|
} catch {
|
|
3768
4863
|
return null;
|
|
3769
4864
|
}
|
|
3770
|
-
const doc = parse4(await
|
|
4865
|
+
const doc = parse4(await readFile16(path, "utf8"));
|
|
3771
4866
|
if (!doc || doc.schemaVersion !== 1 || doc.changeId !== changeId) {
|
|
3772
4867
|
return null;
|
|
3773
4868
|
}
|
|
@@ -3775,15 +4870,15 @@ async function readTddManifest(projectRoot, changeId) {
|
|
|
3775
4870
|
}
|
|
3776
4871
|
async function writeTddManifest(projectRoot, manifest) {
|
|
3777
4872
|
const relative6 = tddManifestRelativePath(manifest.changeId);
|
|
3778
|
-
const path =
|
|
3779
|
-
await
|
|
3780
|
-
await atomicWrite(path,
|
|
4873
|
+
const path = join24(projectRoot, relative6);
|
|
4874
|
+
await mkdir8(dirname8(path), { recursive: true });
|
|
4875
|
+
await atomicWrite(path, stringify4(manifest));
|
|
3781
4876
|
return relative6;
|
|
3782
4877
|
}
|
|
3783
4878
|
async function writeTddResults(projectRoot, results) {
|
|
3784
4879
|
const relative6 = tddResultsRelativePath(results.changeId);
|
|
3785
|
-
const path =
|
|
3786
|
-
await
|
|
4880
|
+
const path = join24(projectRoot, relative6);
|
|
4881
|
+
await mkdir8(dirname8(path), { recursive: true });
|
|
3787
4882
|
await atomicWrite(path, `${JSON.stringify(results, null, 2)}
|
|
3788
4883
|
`);
|
|
3789
4884
|
return relative6;
|
|
@@ -3891,8 +4986,8 @@ async function git(cwd, args) {
|
|
|
3891
4986
|
}
|
|
3892
4987
|
|
|
3893
4988
|
// src/state/store.ts
|
|
3894
|
-
import { mkdir as
|
|
3895
|
-
import { join as
|
|
4989
|
+
import { mkdir as mkdir9, readFile as readFile17 } from "fs/promises";
|
|
4990
|
+
import { join as join25 } from "path";
|
|
3896
4991
|
|
|
3897
4992
|
// src/language.ts
|
|
3898
4993
|
var DEFAULT_LANGUAGE = "zh-CN";
|
|
@@ -4014,7 +5109,7 @@ var StateStore = class {
|
|
|
4014
5109
|
project;
|
|
4015
5110
|
async readGlobal() {
|
|
4016
5111
|
try {
|
|
4017
|
-
const value = JSON.parse(await
|
|
5112
|
+
const value = JSON.parse(await readFile17(this.globalPath(), "utf8"));
|
|
4018
5113
|
assertGlobalState(value);
|
|
4019
5114
|
return value;
|
|
4020
5115
|
} catch (error) {
|
|
@@ -4029,13 +5124,13 @@ var StateStore = class {
|
|
|
4029
5124
|
}
|
|
4030
5125
|
async writeGlobal(state) {
|
|
4031
5126
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4032
|
-
await
|
|
5127
|
+
await mkdir9(join25(this.projectRoot, "openspec"), { recursive: true });
|
|
4033
5128
|
await atomicWrite(this.globalPath(), `${JSON.stringify(state, null, 2)}
|
|
4034
5129
|
`);
|
|
4035
5130
|
}
|
|
4036
5131
|
async readChange(changeId) {
|
|
4037
5132
|
try {
|
|
4038
|
-
const value = JSON.parse(await
|
|
5133
|
+
const value = JSON.parse(await readFile17(this.changePath(changeId), "utf8"));
|
|
4039
5134
|
assertChangeState(value);
|
|
4040
5135
|
return value;
|
|
4041
5136
|
} catch (error) {
|
|
@@ -4050,15 +5145,15 @@ var StateStore = class {
|
|
|
4050
5145
|
}
|
|
4051
5146
|
async writeChange(state) {
|
|
4052
5147
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4053
|
-
await
|
|
5148
|
+
await mkdir9(join25(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
|
|
4054
5149
|
await atomicWrite(this.changePath(state.changeId), `${JSON.stringify(state, null, 2)}
|
|
4055
5150
|
`);
|
|
4056
5151
|
}
|
|
4057
5152
|
globalPath() {
|
|
4058
|
-
return
|
|
5153
|
+
return join25(this.projectRoot, "openspec", "fet-state.json");
|
|
4059
5154
|
}
|
|
4060
5155
|
changePath(changeId) {
|
|
4061
|
-
return
|
|
5156
|
+
return join25(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
|
|
4062
5157
|
}
|
|
4063
5158
|
};
|
|
4064
5159
|
function isNotFound(error) {
|
|
@@ -4066,11 +5161,11 @@ function isNotFound(error) {
|
|
|
4066
5161
|
}
|
|
4067
5162
|
|
|
4068
5163
|
// src/state/tasks.ts
|
|
4069
|
-
import { readFile as
|
|
5164
|
+
import { readFile as readFile18 } from "fs/promises";
|
|
4070
5165
|
async function readCompletedTaskIds(tasksPath) {
|
|
4071
5166
|
let content;
|
|
4072
5167
|
try {
|
|
4073
|
-
content = await
|
|
5168
|
+
content = await readFile18(tasksPath, "utf8");
|
|
4074
5169
|
} catch {
|
|
4075
5170
|
return [];
|
|
4076
5171
|
}
|
|
@@ -4233,6 +5328,8 @@ async function applyWorkflowCommand(ctx, args) {
|
|
|
4233
5328
|
fetVersion: ctx.fetVersion
|
|
4234
5329
|
})
|
|
4235
5330
|
]);
|
|
5331
|
+
const figmaSyncConfig = await loadFigmaSyncConfig(ctx.projectRoot);
|
|
5332
|
+
const figmaAutoSync = figmaGuard ? await tryAutoSyncFigmaAssets({ projectRoot: ctx.projectRoot, changeId, language: ctx.language }) : null;
|
|
4236
5333
|
const applyNextSteps = [
|
|
4237
5334
|
`Read openspec/changes/${changeId}/tasks.md and the instructions output.`,
|
|
4238
5335
|
"Implement pending tasks and update task checkboxes only after the work is done.",
|
|
@@ -4243,7 +5340,13 @@ async function applyWorkflowCommand(ctx, args) {
|
|
|
4243
5340
|
applyNextSteps.unshift(...renderUiDisplayContractApplyNextSteps(changeId, ctx.language));
|
|
4244
5341
|
}
|
|
4245
5342
|
if (figmaGuard) {
|
|
4246
|
-
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
|
+
);
|
|
4247
5350
|
applyNextSteps.splice(applyNextSteps.length - 1, 0, ...renderVisualVerifyNextSteps(changeId, ctx.language));
|
|
4248
5351
|
}
|
|
4249
5352
|
ctx.output.result({
|
|
@@ -4258,6 +5361,7 @@ async function applyWorkflowCommand(ctx, args) {
|
|
|
4258
5361
|
status,
|
|
4259
5362
|
graphContext: runState.graphContext,
|
|
4260
5363
|
figmaGuard: figmaGuard ?? void 0,
|
|
5364
|
+
figmaSync: figmaAutoSync ?? void 0,
|
|
4261
5365
|
uiDisplayContract: uiContract ?? void 0
|
|
4262
5366
|
}
|
|
4263
5367
|
});
|
|
@@ -4656,7 +5760,7 @@ async function createChangelogEntry(projectRoot, changeId) {
|
|
|
4656
5760
|
};
|
|
4657
5761
|
}
|
|
4658
5762
|
async function appendChangelog(projectRoot, entry) {
|
|
4659
|
-
const changelogPath =
|
|
5763
|
+
const changelogPath = join26(projectRoot, "CHANGELOG.md");
|
|
4660
5764
|
const existing = await readOptional5(changelogPath);
|
|
4661
5765
|
const legacyContentLabel = "\u66F4\u65B0\u5185\u5BB9";
|
|
4662
5766
|
const block = `updateTime: ${entry.updateTime}
|
|
@@ -4669,12 +5773,12 @@ ${block}` : block;
|
|
|
4669
5773
|
await atomicWrite(changelogPath, next);
|
|
4670
5774
|
}
|
|
4671
5775
|
async function readChangeRequirement(projectRoot, changeId) {
|
|
4672
|
-
const changeRoot =
|
|
4673
|
-
const proposal = await readOptional5(
|
|
5776
|
+
const changeRoot = join26(projectRoot, "openspec", "changes", changeId);
|
|
5777
|
+
const proposal = await readOptional5(join26(changeRoot, "proposal.md"));
|
|
4674
5778
|
if (proposal) {
|
|
4675
5779
|
return summarizeMarkdown(proposal);
|
|
4676
5780
|
}
|
|
4677
|
-
const readme = await readOptional5(
|
|
5781
|
+
const readme = await readOptional5(join26(changeRoot, "README.md"));
|
|
4678
5782
|
if (readme) {
|
|
4679
5783
|
return summarizeMarkdown(readme);
|
|
4680
5784
|
}
|
|
@@ -4686,7 +5790,7 @@ function summarizeMarkdown(content) {
|
|
|
4686
5790
|
}
|
|
4687
5791
|
async function readOptional5(path) {
|
|
4688
5792
|
try {
|
|
4689
|
-
return await
|
|
5793
|
+
return await readFile19(path, "utf8");
|
|
4690
5794
|
} catch {
|
|
4691
5795
|
return null;
|
|
4692
5796
|
}
|
|
@@ -5041,17 +6145,17 @@ async function updateCommand(ctx) {
|
|
|
5041
6145
|
}
|
|
5042
6146
|
|
|
5043
6147
|
// src/commands/tdd.ts
|
|
5044
|
-
import { mkdir as
|
|
5045
|
-
import { join as
|
|
6148
|
+
import { mkdir as mkdir10 } from "fs/promises";
|
|
6149
|
+
import { join as join28 } from "path";
|
|
5046
6150
|
|
|
5047
6151
|
// src/tdd/extract-cases.ts
|
|
5048
|
-
import { readFile as
|
|
5049
|
-
import { join as
|
|
6152
|
+
import { readFile as readFile20 } from "fs/promises";
|
|
6153
|
+
import { join as join27 } from "path";
|
|
5050
6154
|
async function extractCasesFromChange(projectRoot, changeId, sources) {
|
|
5051
6155
|
const cases = [];
|
|
5052
6156
|
const seen = /* @__PURE__ */ new Set();
|
|
5053
6157
|
for (const source of sources) {
|
|
5054
|
-
const content = await
|
|
6158
|
+
const content = await readFile20(join27(projectRoot, source), "utf8");
|
|
5055
6159
|
if (source.endsWith("tasks.md")) {
|
|
5056
6160
|
for (const item of extractTaskCases(content, changeId)) {
|
|
5057
6161
|
if (!seen.has(item.id)) {
|
|
@@ -5185,12 +6289,12 @@ async function tddCommand(ctx) {
|
|
|
5185
6289
|
testCommand: testCommand2
|
|
5186
6290
|
});
|
|
5187
6291
|
const manifestPath3 = await writeTddManifest(ctx.projectRoot, manifest);
|
|
5188
|
-
const fetDir =
|
|
5189
|
-
await
|
|
6292
|
+
const fetDir = join28(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
|
|
6293
|
+
await mkdir10(fetDir, { recursive: true });
|
|
5190
6294
|
const instructionsPath = tddInstructionsRelativePath(changeId);
|
|
5191
6295
|
const specPath = tddSpecRelativePath(changeId);
|
|
5192
|
-
await atomicWrite(
|
|
5193
|
-
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));
|
|
5194
6298
|
const changeState = await ctx.stateStore.getOrCreateChange(changeId, "implement");
|
|
5195
6299
|
changeState.tdd = {
|
|
5196
6300
|
status: "ready",
|
|
@@ -5432,14 +6536,81 @@ function truncate(value, max = 2e3) {
|
|
|
5432
6536
|
return value.length > max ? `${value.slice(0, max)}\u2026` : value;
|
|
5433
6537
|
}
|
|
5434
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
|
+
|
|
5435
6606
|
// src/commands/visual.ts
|
|
5436
|
-
import { mkdir as
|
|
5437
|
-
import { join as
|
|
6607
|
+
import { mkdir as mkdir13 } from "fs/promises";
|
|
6608
|
+
import { join as join33 } from "path";
|
|
5438
6609
|
|
|
5439
6610
|
// src/visual/config.ts
|
|
5440
|
-
import { readFile as
|
|
5441
|
-
import { join as
|
|
5442
|
-
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";
|
|
5443
6614
|
var DEFAULT_CONFIG4 = {
|
|
5444
6615
|
enabled: true,
|
|
5445
6616
|
compareMode: "layout-only",
|
|
@@ -5448,8 +6619,8 @@ var DEFAULT_CONFIG4 = {
|
|
|
5448
6619
|
};
|
|
5449
6620
|
async function loadVisualConfig(projectRoot) {
|
|
5450
6621
|
try {
|
|
5451
|
-
const raw = await
|
|
5452
|
-
const doc =
|
|
6622
|
+
const raw = await readFile21(join30(projectRoot, "openspec", "config.yaml"), "utf8");
|
|
6623
|
+
const doc = parseDocument7(raw);
|
|
5453
6624
|
const fetNode = doc.get("fet", true);
|
|
5454
6625
|
const node = fetNode?.get?.("visual");
|
|
5455
6626
|
if (!node || typeof node.get !== "function") {
|
|
@@ -5495,10 +6666,10 @@ function isVisualRequiredForVerify(config, hasFigma) {
|
|
|
5495
6666
|
|
|
5496
6667
|
// src/visual/playwright.ts
|
|
5497
6668
|
import { createRequire } from "module";
|
|
5498
|
-
import { dirname as dirname9, join as
|
|
6669
|
+
import { dirname as dirname9, join as join31 } from "path";
|
|
5499
6670
|
import { pathToFileURL } from "url";
|
|
5500
6671
|
async function resolvePlaywright(projectRoot) {
|
|
5501
|
-
const require2 = createRequire(
|
|
6672
|
+
const require2 = createRequire(join31(projectRoot, "package.json"));
|
|
5502
6673
|
const candidates = ["playwright", "@playwright/test"];
|
|
5503
6674
|
for (const name of candidates) {
|
|
5504
6675
|
try {
|
|
@@ -5530,11 +6701,11 @@ async function capturePagesWithPlaywright(projectRoot, manifest, baseUrl) {
|
|
|
5530
6701
|
return results;
|
|
5531
6702
|
}
|
|
5532
6703
|
async function captureSinglePage(projectRoot, browser, changeId, pageDef, baseUrl) {
|
|
5533
|
-
const { mkdir:
|
|
6704
|
+
const { mkdir: mkdir18 } = await import("fs/promises");
|
|
5534
6705
|
const { join: joinPath } = await import("path");
|
|
5535
6706
|
const screenshotRelative = visualPageScreenshotRelative(changeId, pageDef.id);
|
|
5536
6707
|
const screenshotAbsolute = joinPath(projectRoot, screenshotRelative);
|
|
5537
|
-
await
|
|
6708
|
+
await mkdir18(dirname9(screenshotAbsolute), { recursive: true });
|
|
5538
6709
|
const url = new URL(pageDef.route, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).toString();
|
|
5539
6710
|
const pwPage = await browser.newPage();
|
|
5540
6711
|
try {
|
|
@@ -5599,11 +6770,11 @@ async function extractRegions(page, pageDef) {
|
|
|
5599
6770
|
}
|
|
5600
6771
|
|
|
5601
6772
|
// src/visual/manifest.ts
|
|
5602
|
-
import { mkdir as
|
|
5603
|
-
import { dirname as dirname10, join as
|
|
5604
|
-
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";
|
|
5605
6776
|
function visualManifestPath(projectRoot, changeId) {
|
|
5606
|
-
return
|
|
6777
|
+
return join32(projectRoot, visualManifestRelativePath(changeId));
|
|
5607
6778
|
}
|
|
5608
6779
|
async function readVisualManifest(projectRoot, changeId) {
|
|
5609
6780
|
const path = visualManifestPath(projectRoot, changeId);
|
|
@@ -5612,7 +6783,7 @@ async function readVisualManifest(projectRoot, changeId) {
|
|
|
5612
6783
|
} catch {
|
|
5613
6784
|
return null;
|
|
5614
6785
|
}
|
|
5615
|
-
const doc = parse5(await
|
|
6786
|
+
const doc = parse5(await readFile22(path, "utf8"));
|
|
5616
6787
|
if (!doc || doc.schemaVersion !== 1 || doc.changeId !== changeId) {
|
|
5617
6788
|
return null;
|
|
5618
6789
|
}
|
|
@@ -5620,15 +6791,15 @@ async function readVisualManifest(projectRoot, changeId) {
|
|
|
5620
6791
|
}
|
|
5621
6792
|
async function writeVisualManifest(projectRoot, manifest) {
|
|
5622
6793
|
const relative6 = visualManifestRelativePath(manifest.changeId);
|
|
5623
|
-
const path =
|
|
5624
|
-
await
|
|
5625
|
-
await atomicWrite(path,
|
|
6794
|
+
const path = join32(projectRoot, relative6);
|
|
6795
|
+
await mkdir12(dirname10(path), { recursive: true });
|
|
6796
|
+
await atomicWrite(path, stringify5(manifest));
|
|
5626
6797
|
return relative6;
|
|
5627
6798
|
}
|
|
5628
6799
|
async function readVisualCapture(projectRoot, changeId) {
|
|
5629
|
-
const path =
|
|
6800
|
+
const path = join32(projectRoot, visualCaptureRelativePath(changeId));
|
|
5630
6801
|
try {
|
|
5631
|
-
const doc = JSON.parse(await
|
|
6802
|
+
const doc = JSON.parse(await readFile22(path, "utf8"));
|
|
5632
6803
|
if (doc?.schemaVersion === 1 && doc.changeId === changeId) {
|
|
5633
6804
|
return doc;
|
|
5634
6805
|
}
|
|
@@ -5639,16 +6810,16 @@ async function readVisualCapture(projectRoot, changeId) {
|
|
|
5639
6810
|
}
|
|
5640
6811
|
async function writeVisualCapture(projectRoot, capture) {
|
|
5641
6812
|
const relative6 = visualCaptureRelativePath(capture.changeId);
|
|
5642
|
-
const path =
|
|
5643
|
-
await
|
|
6813
|
+
const path = join32(projectRoot, relative6);
|
|
6814
|
+
await mkdir12(dirname10(path), { recursive: true });
|
|
5644
6815
|
await atomicWrite(path, `${JSON.stringify(capture, null, 2)}
|
|
5645
6816
|
`);
|
|
5646
6817
|
return relative6;
|
|
5647
6818
|
}
|
|
5648
6819
|
async function writeVisualResults(projectRoot, results) {
|
|
5649
6820
|
const relative6 = visualResultsRelativePath(results.changeId);
|
|
5650
|
-
const path =
|
|
5651
|
-
await
|
|
6821
|
+
const path = join32(projectRoot, relative6);
|
|
6822
|
+
await mkdir12(dirname10(path), { recursive: true });
|
|
5652
6823
|
await atomicWrite(path, `${JSON.stringify(results, null, 2)}
|
|
5653
6824
|
`);
|
|
5654
6825
|
return relative6;
|
|
@@ -6042,10 +7213,10 @@ async function ensureManifest(ctx, changeId, baseUrl) {
|
|
|
6042
7213
|
baseUrl
|
|
6043
7214
|
);
|
|
6044
7215
|
await writeVisualManifest(ctx.projectRoot, manifest);
|
|
6045
|
-
const fetDir =
|
|
6046
|
-
await
|
|
6047
|
-
await atomicWrite(
|
|
6048
|
-
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));
|
|
6049
7220
|
const changeState = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
6050
7221
|
changeState.visual = {
|
|
6051
7222
|
status: "ready",
|
|
@@ -6098,8 +7269,8 @@ async function recordVisualFailure(ctx, changeId, planningFingerprint, steps, ca
|
|
|
6098
7269
|
|
|
6099
7270
|
// src/commands/verify.ts
|
|
6100
7271
|
import { createHash as createHash2 } from "crypto";
|
|
6101
|
-
import { mkdir as
|
|
6102
|
-
import { join as
|
|
7272
|
+
import { mkdir as mkdir14, readFile as readFile23, stat as stat12 } from "fs/promises";
|
|
7273
|
+
import { join as join34 } from "path";
|
|
6103
7274
|
async function verifyCommand(ctx, options) {
|
|
6104
7275
|
if (options.auto) {
|
|
6105
7276
|
const scan = await ctx.scanner.scan(ctx.projectRoot, {});
|
|
@@ -6168,9 +7339,9 @@ async function writeInstructions(ctx, changeId) {
|
|
|
6168
7339
|
await assertTestPassed(ctx, changeId);
|
|
6169
7340
|
await assertVisualPassed(ctx, changeId);
|
|
6170
7341
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6171
|
-
const dir =
|
|
6172
|
-
const instructionsPath =
|
|
6173
|
-
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 });
|
|
6174
7345
|
await atomicWrite(instructionsPath, renderVerifyInstructions(changeId, generatedAt));
|
|
6175
7346
|
const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
6176
7347
|
state.currentPhase = "verify";
|
|
@@ -6188,7 +7359,7 @@ async function markDone(ctx, changeId) {
|
|
|
6188
7359
|
await assertTestPassed(ctx, changeId);
|
|
6189
7360
|
await assertVisualPassed(ctx, changeId);
|
|
6190
7361
|
const declaredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6191
|
-
const instructionsPath =
|
|
7362
|
+
const instructionsPath = join34(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
|
|
6192
7363
|
const instructions = await readInstructions(ctx, instructionsPath, changeId);
|
|
6193
7364
|
const instructionsGeneratedAt = readFrontMatterValue(instructions, "generatedAt") ?? declaredAt;
|
|
6194
7365
|
const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
@@ -6213,7 +7384,7 @@ async function markDone(ctx, changeId) {
|
|
|
6213
7384
|
async function readInstructions(ctx, path, changeId) {
|
|
6214
7385
|
try {
|
|
6215
7386
|
await stat12(path);
|
|
6216
|
-
const content = await
|
|
7387
|
+
const content = await readFile23(path, "utf8");
|
|
6217
7388
|
const fileChangeId = readFrontMatterValue(content, "changeId");
|
|
6218
7389
|
if (fileChangeId !== changeId) {
|
|
6219
7390
|
throw new FetError({
|
|
@@ -6336,9 +7507,9 @@ function renderIdeModelPolicy(command, language = "zh-CN") {
|
|
|
6336
7507
|
import { resolve } from "path";
|
|
6337
7508
|
|
|
6338
7509
|
// src/adapters/codex/index.ts
|
|
6339
|
-
import { mkdir as
|
|
7510
|
+
import { mkdir as mkdir15, readFile as readFile24, stat as stat13 } from "fs/promises";
|
|
6340
7511
|
import { homedir } from "os";
|
|
6341
|
-
import { dirname as dirname11, join as
|
|
7512
|
+
import { dirname as dirname11, join as join35 } from "path";
|
|
6342
7513
|
|
|
6343
7514
|
// src/adapters/commands.ts
|
|
6344
7515
|
var FET_STANDALONE_COMMANDS = ["tdd", "test", "visual"];
|
|
@@ -6385,6 +7556,7 @@ Before doing FET or OpenSpec work in Codex, read:
|
|
|
6385
7556
|
- .codex/fet/spec-language.md when writing or updating OpenSpec specs
|
|
6386
7557
|
- openspec/changes/<change-id>/.fet/figma-apply-instructions.md before UI work when FET apply reports Figma links
|
|
6387
7558
|
- .codex/fet/ui-display-contract.md when UI binds API data; openspec/changes/<change-id>/.fet/ui-display-contract.yaml when present
|
|
7559
|
+
- .codex/fet/write-boundary.md for default edit scope (src/, openspec/, .fet/ only; ask before other paths)
|
|
6388
7560
|
- the active change files under openspec/changes/<change-id>/, when a change is selected
|
|
6389
7561
|
|
|
6390
7562
|
If GitNexus code graph context is available in the IDE or MCP tools, prefer it before broad repository scans. Use it to identify relevant modules, dependencies, and insertion points, then read only the concrete source files needed. If GitNexus is unavailable, continue with the normal FET/OpenSpec workflow.
|
|
@@ -6406,6 +7578,7 @@ ${languageInstruction(language)}
|
|
|
6406
7578
|
- \u6309 Figma \u5B9E\u73B0 UI \u65F6\u9605\u8BFB .codex/fet/figma-stop.md\uFF1B\u6709 figma-apply-instructions.md \u65F6\u5FC5\u987B\u5728\u6539 UI \u524D\u5B8C\u6574\u6267\u884C
|
|
6407
7579
|
- \u7F16\u5199\u6216\u66F4\u65B0 spec \u65F6\u9605\u8BFB .codex/fet/spec-language.md\uFF08\u82F1\u6587\u89C4\u8303 + \u540C\u6B21\u7F16\u8F91\u7EF4\u62A4\u4E2D\u6587\u6CE8\u91CA\uFF09
|
|
6408
7580
|
- \u7ED1\u5B9A\u63A5\u53E3\u7684 UI \u9605\u8BFB .codex/fet/ui-display-contract.md\uFF1B\u5B58\u5728 ui-display-contract.yaml \u65F6\u5B9E\u65BD\u524D\u987B\u786E\u8BA4 displayFields
|
|
7581
|
+
- \u4FEE\u6539\u6587\u4EF6\u524D\u9605\u8BFB .codex/fet/write-boundary.md\uFF08\u9ED8\u8BA4\u4EC5 src/\u3001openspec/\u3001.fet/\uFF1B\u5176\u5B83\u8DEF\u5F84\u987B\u7528\u6237\u660E\u786E\u540C\u610F\uFF09
|
|
6409
7582
|
- \u5982\u679C\u5DF2\u9009\u62E9 change\uFF0C\u9605\u8BFB openspec/changes/<change-id>/ \u4E0B\u7684\u5F53\u524D\u4EA7\u7269
|
|
6410
7583
|
|
|
6411
7584
|
\u5982\u679C IDE \u6216 MCP \u5DE5\u5177\u4E2D\u53EF\u7528 GitNexus \u4EE3\u7801\u56FE\u4E0A\u4E0B\u6587\uFF0C\u5148\u7528\u5B83\u7F29\u5C0F\u4ED3\u5E93\u626B\u63CF\u8303\u56F4\uFF1B\u7528\u56FE\u8BC6\u522B\u76F8\u5173\u6A21\u5757\u3001\u4F9D\u8D56\u548C\u63D2\u5165\u70B9\uFF0C\u518D\u53EA\u8BFB\u53D6\u9700\u8981\u786E\u8BA4\u884C\u4E3A\u7684\u5177\u4F53\u6E90\u7801\u6587\u4EF6\u3002GitNexus \u4E0D\u53EF\u7528\u65F6\uFF0C\u6309\u666E\u901A FET/OpenSpec \u5DE5\u4F5C\u6D41\u7EE7\u7EED\u3002
|
|
@@ -6444,12 +7617,19 @@ function codexUiDisplayContractFile(language = DEFAULT_LANGUAGE) {
|
|
|
6444
7617
|
content: renderCodexUiDisplayContractGuide(language)
|
|
6445
7618
|
};
|
|
6446
7619
|
}
|
|
7620
|
+
function codexWriteBoundaryFile(language = DEFAULT_LANGUAGE) {
|
|
7621
|
+
return {
|
|
7622
|
+
path: ".codex/fet/write-boundary.md",
|
|
7623
|
+
content: renderCodexWriteBoundaryGuide(language)
|
|
7624
|
+
};
|
|
7625
|
+
}
|
|
6447
7626
|
function codexCommandFiles(language = DEFAULT_LANGUAGE) {
|
|
6448
7627
|
return [
|
|
6449
7628
|
codexKarpathyGuidelinesFile(language),
|
|
6450
7629
|
codexFigmaStopFile(language),
|
|
6451
7630
|
codexUiDisplayContractFile(language),
|
|
6452
7631
|
codexSpecLanguageFile(language),
|
|
7632
|
+
codexWriteBoundaryFile(language),
|
|
6453
7633
|
...FET_ADAPTER_COMMANDS.map((command) => ({
|
|
6454
7634
|
path: `.codex/fet/commands/${command}.md`,
|
|
6455
7635
|
content: renderCommand(command, language)
|
|
@@ -6491,6 +7671,9 @@ function renderCommand(command, language) {
|
|
|
6491
7671
|
if (command.startsWith("graph-")) {
|
|
6492
7672
|
return renderGraphCommand(command, language);
|
|
6493
7673
|
}
|
|
7674
|
+
if (command === "tdd" || command === "test" || command === "visual") {
|
|
7675
|
+
return renderStandaloneWorkflowCommand(command, language);
|
|
7676
|
+
}
|
|
6494
7677
|
const usage = renderFetAdapterUsage(command, "");
|
|
6495
7678
|
return `<!-- FET:MANAGED
|
|
6496
7679
|
schemaVersion: 1
|
|
@@ -6521,11 +7704,16 @@ ${usage}
|
|
|
6521
7704
|
If the command needs a change id, pass it with \`--change <change-id>\` or use the active OpenSpec change from the user's request.
|
|
6522
7705
|
|
|
6523
7706
|
After the command completes, report the important next steps from the FET output and keep any generated OpenSpec artifacts in the normal project workflow.
|
|
7707
|
+
|
|
7708
|
+
${renderWriteBoundaryGuardrail(language)}
|
|
6524
7709
|
`;
|
|
6525
7710
|
}
|
|
6526
7711
|
function renderCommandZh(command) {
|
|
6527
7712
|
const usage = renderFetAdapterUsage(command, command === "fill-context" ? "" : command === "passthrough" ? "<openspec-command> [...args]" : "");
|
|
6528
7713
|
const title = commandTitleZh(command);
|
|
7714
|
+
if (command === "tdd" || command === "test" || command === "visual") {
|
|
7715
|
+
return renderStandaloneWorkflowCommand(command, "zh-CN");
|
|
7716
|
+
}
|
|
6529
7717
|
if (command === "graph-setup") {
|
|
6530
7718
|
return `<!-- FET:MANAGED
|
|
6531
7719
|
schemaVersion: 1
|
|
@@ -6583,6 +7771,8 @@ ${usage}
|
|
|
6583
7771
|
\u5982\u679C\u547D\u4EE4\u9700\u8981 change id\uFF0C\u4F18\u5148\u4F7F\u7528\u7528\u6237\u8F93\u5165\u3001\`--change <change-id>\`\u3001FET active change \u6216\u552F\u4E00\u6253\u5F00\u7684 change\u3002\u5B58\u5728\u6B67\u4E49\u65F6\u5148\u8BE2\u95EE\u7528\u6237\u3002
|
|
6584
7772
|
|
|
6585
7773
|
\u6267\u884C\u5B8C\u6210\u540E\uFF0C\u7528\u4E2D\u6587\u603B\u7ED3\u5173\u952E\u8F93\u51FA\u3001\u751F\u6210\u6216\u66F4\u65B0\u7684\u6587\u4EF6\uFF0C\u4EE5\u53CA\u4E0B\u4E00\u6B65\u5EFA\u8BAE\u3002
|
|
7774
|
+
|
|
7775
|
+
${renderWriteBoundaryGuardrail("zh-CN")}
|
|
6586
7776
|
`;
|
|
6587
7777
|
}
|
|
6588
7778
|
function renderPassthroughCommand(language) {
|
|
@@ -6699,6 +7889,9 @@ function renderSlashPrompt(command, language) {
|
|
|
6699
7889
|
if (command === "apply") {
|
|
6700
7890
|
return renderApplySlashPrompt(language);
|
|
6701
7891
|
}
|
|
7892
|
+
if (command === "tdd" || command === "test" || command === "visual") {
|
|
7893
|
+
return renderStandaloneWorkflowSlashPrompt(command, language);
|
|
7894
|
+
}
|
|
6702
7895
|
if (command === "verify") {
|
|
6703
7896
|
return renderVerifySlashPrompt(language);
|
|
6704
7897
|
}
|
|
@@ -6760,6 +7953,9 @@ function renderSlashPromptZh(command) {
|
|
|
6760
7953
|
if (command === "graph-setup") {
|
|
6761
7954
|
return renderGraphSetupSlashPrompt("zh-CN");
|
|
6762
7955
|
}
|
|
7956
|
+
if (command === "tdd" || command === "test" || command === "visual") {
|
|
7957
|
+
return renderStandaloneWorkflowSlashPrompt(command, "zh-CN");
|
|
7958
|
+
}
|
|
6763
7959
|
const usage = renderFetAdapterUsage(command, command === "fill-context" ? "" : command === "passthrough" ? "<openspec-command> [...args]" : "[...args]");
|
|
6764
7960
|
const argumentHint = command === "passthrough" ? "openspec-command [...args]" : void 0;
|
|
6765
7961
|
const argumentHintLine = argumentHint ? `argument-hint: ${argumentHint}
|
|
@@ -6800,9 +7996,10 @@ ${commandGoalZh(command)}
|
|
|
6800
7996
|
- \u9ED8\u8BA4\u4F7F\u7528\u4E2D\u6587\u4EA7\u51FA\u3002
|
|
6801
7997
|
- \u4E0D\u8981\u7ED5\u8FC7 FET \u76F4\u63A5\u8C03\u7528 openspec\uFF0C\u9664\u975E FET \u547D\u4EE4\u672C\u8EAB\u4E0D\u53EF\u7528\u3002
|
|
6802
7998
|
- change \u4E0D\u660E\u786E\u65F6\u5148\u8BE2\u95EE\u7528\u6237\u3002
|
|
6803
|
-
${
|
|
7999
|
+
${renderWriteBoundaryGuardrail("zh-CN")}
|
|
8000
|
+
${command === "fill-context" ? "- fet fill-context \u53EF\u80FD\u5DF2\u5199\u5165\u5C0F\u7A0B\u5E8F\u5305\u4F53\u79EF\u4E0E 2MB \u7EA6\u675F\uFF0C\u4E0D\u8981\u8986\u76D6\u8BE5\u8282\u4E2D\u7684\u626B\u63CF\u7ED3\u679C\uFF0C\u9664\u975E\u4ED3\u5E93\u7ED3\u6784\u5DF2\u53D8\u3002\n- \u66FF\u6362 AGENTS.md \u4E2D\u5176\u4F59 [NEEDS LLM INPUT] \u5360\u4F4D\u7B26\uFF0C\u4FDD\u7559 FET \u6258\u7BA1\u6807\u8BB0\uFF0C\u4E0D\u8981\u4FEE\u6539\u4E1A\u52A1\u4EE3\u7801\uFF1B\u4FEE\u6539 AGENTS.md \u5C5E\u4E8E\u5141\u8BB8\u8303\u56F4\u5916\u8DEF\u5F84\uFF0C\u987B\u5728\u672C\u8F6E\u5DF2\u83B7\u5F97\u7528\u6237\u540C\u610F\u3002\n" : ""}${command === "propose" || command === "continue" || command === "ff" ? "- \u4E00\u6B21\u53EA\u521B\u5EFA\u4E00\u4E2A ready artifact\uFF0C\u5E76\u5728\u5199\u5165\u524D\u9605\u8BFB\u4F9D\u8D56\u6587\u4EF6\u3002\n" : ""}${command === "propose" || command === "continue" || command === "ff" ? "- \u4E0D\u8981\u5728\u7528\u6237\u5BA1\u9605\u5F53\u524D\u4EA7\u7269\u524D\u81EA\u52A8\u8FD0\u884C fet continue\u3001fet ff \u6216\u5FAA\u73AF\u751F\u6210\u540E\u7EED\u4EA7\u7269\uFF1B\u9700\u8981\u7528\u6237\u660E\u786E\u786E\u8BA4\u540E\u518D\u63A8\u8FDB\u3002\n" : ""}${command === "propose" || command === "continue" || command === "ff" || command === "sync" ? `${renderSpecArtifactGuardrail("zh-CN")}
|
|
6804
8001
|
` : ""}${command === "propose" || command === "continue" || command === "ff" ? `${renderUiDisplayContractGuardrail("zh-CN")}
|
|
6805
|
-
` : ""}${command === "apply" ? "- \u4E0D\u8981\u5728\u672A\u5B8C\u6210\u771F\u5B9E\u5B9E\u73B0\u524D\u52FE\u9009 tasks.md\uFF1B\u4E0D\u8981\u4ECE apply \u9636\u6BB5\u76F4\u63A5 sync \u6216 archive\u3002\n- \u82E5\u5B58\u5728 openspec/changes/<change-id>/.fet/figma-apply-instructions.md\uFF0C\u5FC5\u987B\u5148\u8BFB Figma\uFF08MCP/API\uFF09\u518D\u6539 UI\uFF1B\u5931\u8D25\u5219\u505C\u4E0B\u95EE\u7528\u6237\uFF0C\u7981\u6B62\u731C\u6837\u5F0F\u3002\n- \u82E5\u5B58\u5728 openspec/changes/<change-id>/.fet/ui-display-contract.yaml\uFF0C\u5B9E\u65BD\u7ED1\u5B9A\u63A5\u53E3\u7684 UI \u524D\u987B\u786E\u8BA4 displayFields\uFF0C\u7981\u6B62\u6309 OpenAPI \u5168\u91CF\u5C55\u793A\u3002\n" : ""}`;
|
|
8002
|
+
` : ""}${command === "apply" ? "- \u4E0D\u8981\u5728\u672A\u5B8C\u6210\u771F\u5B9E\u5B9E\u73B0\u524D\u52FE\u9009 tasks.md\uFF1B\u4E0D\u8981\u4ECE apply \u9636\u6BB5\u76F4\u63A5 sync \u6216 archive\u3002\n- \u5B9E\u65BD\u524D\u987B\u5DF2\u8FD0\u884C fet tdd\uFF1B\u5B58\u5728 tdd-manifest.yaml \u65F6\u6309\u6E05\u5355\u7F16\u5199\u6D4B\u8BD5\u4E0E\u5B9E\u73B0\uFF0Cfet verify \u524D\u5148 fet test\u3002\n- \u82E5\u5B58\u5728 openspec/changes/<change-id>/.fet/figma-apply-instructions.md\uFF0C\u5FC5\u987B\u5148\u8BFB Figma\uFF08MCP/API\uFF09\u518D\u6539 UI\uFF1B\u5931\u8D25\u5219\u505C\u4E0B\u95EE\u7528\u6237\uFF0C\u7981\u6B62\u731C\u6837\u5F0F\u3002\n- \u82E5\u5B58\u5728 openspec/changes/<change-id>/.fet/ui-display-contract.yaml\uFF0C\u5B9E\u65BD\u7ED1\u5B9A\u63A5\u53E3\u7684 UI \u524D\u987B\u786E\u8BA4 displayFields\uFF0C\u7981\u6B62\u6309 OpenAPI \u5168\u91CF\u5C55\u793A\u3002\n" : ""}`;
|
|
6806
8003
|
}
|
|
6807
8004
|
function commandTitleZh(command) {
|
|
6808
8005
|
const titles = {
|
|
@@ -6876,7 +8073,9 @@ First run:
|
|
|
6876
8073
|
fet fill-context
|
|
6877
8074
|
\`\`\`
|
|
6878
8075
|
|
|
6879
|
-
Then read AGENTS.md and openspec/config.yaml, inspect the project, and replace every [NEEDS LLM INPUT] or [NEED LLM INPUT] placeholder in AGENTS.md with concrete project-specific content. Preserve FET managed markers and do not modify business code.
|
|
8076
|
+
Then read AGENTS.md and openspec/config.yaml, inspect the project, and replace every [NEEDS LLM INPUT] or [NEED LLM INPUT] placeholder in AGENTS.md with concrete project-specific content. Preserve FET managed markers and do not modify business code. Editing AGENTS.md is outside the default write scope\u2014confirm user approval first.
|
|
8077
|
+
|
|
8078
|
+
${renderWriteBoundaryGuardrail(language)}
|
|
6880
8079
|
`;
|
|
6881
8080
|
}
|
|
6882
8081
|
function renderFillContextSlashPrompt(language) {
|
|
@@ -6912,7 +8111,9 @@ Steps:
|
|
|
6912
8111
|
Guardrails:
|
|
6913
8112
|
- Do not invent facts that cannot be inferred from the repo.
|
|
6914
8113
|
- Use [UNKNOWN] only when the repository does not contain enough evidence.
|
|
6915
|
-
- Keep generated context stable and useful for future AI coding sessions
|
|
8114
|
+
- Keep generated context stable and useful for future AI coding sessions.
|
|
8115
|
+
- Editing AGENTS.md is outside the default write scope; confirm user approval first (see .codex/fet/write-boundary.md).
|
|
8116
|
+
${renderWriteBoundaryGuardrail(language)}`,
|
|
6916
8117
|
void 0,
|
|
6917
8118
|
language
|
|
6918
8119
|
);
|
|
@@ -6969,7 +8170,7 @@ Steps:
|
|
|
6969
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>/.
|
|
6970
8171
|
4. If \`openspec/changes/<change-id>/.fet/figma-apply-instructions.md\` exists (or apply nextSteps mention Figma):
|
|
6971
8172
|
- Read it and \`figma-stop.md\` in the same folder before any UI code.
|
|
6972
|
-
-
|
|
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.
|
|
6973
8174
|
- If Figma access fails or design details are unclear, stop and ask the user\u2014do not guess styles or implement UI.
|
|
6974
8175
|
5. If \`openspec/changes/<change-id>/.fet/ui-display-contract.yaml\` exists (or apply nextSteps mention UI display contract):
|
|
6975
8176
|
- Read it and \`ui-field-apply-instructions.md\` in the same folder before UI that binds API data.
|
|
@@ -6981,14 +8182,16 @@ Steps:
|
|
|
6981
8182
|
- Follow proposal, specs, design, and tasks.
|
|
6982
8183
|
- Mark each completed task checkbox in tasks.md from \`- [ ]\` to \`- [x]\`.
|
|
6983
8184
|
- Pause and ask if a task is ambiguous or reveals a design conflict.
|
|
6984
|
-
8.
|
|
8185
|
+
8. Run \`fet tdd --change <change-id>\` before implementation when no \`tdd-manifest.yaml\` exists yet; after coding run \`fet test --change <change-id>\` before \`fet verify\`.
|
|
8186
|
+
9. After completing or pausing, summarize completed tasks, remaining tasks, and blockers.
|
|
6985
8187
|
|
|
6986
8188
|
Guardrails:
|
|
6987
8189
|
- Never skip reading OpenSpec artifacts before implementation.
|
|
6988
8190
|
- When Figma links exist for this change, never implement or restyle UI without reading Figma first.
|
|
6989
8191
|
- When ui-display-contract.yaml exists, API schemas are not UI checklists\u2014only displayFields may render.
|
|
6990
8192
|
- Do not mark a task complete until the code change is actually done.
|
|
6991
|
-
- Do not run sync or archive from apply
|
|
8193
|
+
- Do not run sync or archive from apply.
|
|
8194
|
+
${renderWriteBoundaryGuardrail(language)}`,
|
|
6992
8195
|
void 0,
|
|
6993
8196
|
language
|
|
6994
8197
|
);
|
|
@@ -7415,8 +8618,97 @@ ${languageInstruction(language)}
|
|
|
7415
8618
|
${graphContextInstruction}
|
|
7416
8619
|
|
|
7417
8620
|
${body}
|
|
8621
|
+
|
|
8622
|
+
${renderWriteBoundaryGuardrail(language)}
|
|
8623
|
+
`;
|
|
8624
|
+
}
|
|
8625
|
+
function renderStandaloneWorkflowCommand(command, language) {
|
|
8626
|
+
const usage = renderFetAdapterUsage(command, "");
|
|
8627
|
+
const { title, body } = standaloneWorkflowCopy(command, language);
|
|
8628
|
+
return `<!-- FET:MANAGED
|
|
8629
|
+
schemaVersion: 1
|
|
8630
|
+
fetVersion: ${FET_VERSION}
|
|
8631
|
+
generator: codex-adapter
|
|
8632
|
+
adapterVersion: 1
|
|
8633
|
+
command: ${usage}
|
|
8634
|
+
FET:END -->
|
|
8635
|
+
|
|
8636
|
+
# ${usage}
|
|
8637
|
+
|
|
8638
|
+
${renderIdeModelPolicy(command, language)}
|
|
8639
|
+
|
|
8640
|
+
${languageInstruction(language)}
|
|
8641
|
+
|
|
8642
|
+
## ${language === "en" ? "Purpose" : "\u7528\u9014"}
|
|
8643
|
+
|
|
8644
|
+
${title}
|
|
8645
|
+
|
|
8646
|
+
## ${language === "en" ? "Workflow" : "\u5DE5\u4F5C\u6D41"}
|
|
8647
|
+
|
|
8648
|
+
${body}
|
|
8649
|
+
|
|
8650
|
+
${renderWriteBoundaryGuardrail(language)}
|
|
7418
8651
|
`;
|
|
7419
8652
|
}
|
|
8653
|
+
function renderStandaloneWorkflowSlashPrompt(command, language) {
|
|
8654
|
+
const usage = renderFetAdapterUsage(command, "[...args]");
|
|
8655
|
+
const { title, body, description } = standaloneWorkflowCopy(command, language);
|
|
8656
|
+
return renderManagedSlashPrompt(
|
|
8657
|
+
usage,
|
|
8658
|
+
description,
|
|
8659
|
+
`${title}
|
|
8660
|
+
|
|
8661
|
+
Steps:
|
|
8662
|
+
|
|
8663
|
+
1. Read AGENTS.md, openspec/config.yaml, .codex/fet/karpathy-guidelines.md, and .codex/fet/write-boundary.md.
|
|
8664
|
+
2. Resolve the change id when needed (\`--change <change-id>\` or active change).
|
|
8665
|
+
3. Run:
|
|
8666
|
+
\`\`\`sh
|
|
8667
|
+
${renderFetAdapterUsage(command, "")}
|
|
8668
|
+
\`\`\`
|
|
8669
|
+
4. ${body}
|
|
8670
|
+
5. Summarize outputs, paths written under openspec/changes/<change-id>/.fet/, and next steps.
|
|
8671
|
+
|
|
8672
|
+
Guardrails:
|
|
8673
|
+
- ${body.split("\n")[0]}
|
|
8674
|
+
${renderWriteBoundaryGuardrail(language)}`,
|
|
8675
|
+
void 0,
|
|
8676
|
+
language
|
|
8677
|
+
);
|
|
8678
|
+
}
|
|
8679
|
+
function standaloneWorkflowCopy(command, language) {
|
|
8680
|
+
if (command === "tdd") {
|
|
8681
|
+
return language === "en" ? {
|
|
8682
|
+
description: "Generate per-change TDD manifest and test instructions",
|
|
8683
|
+
title: "Generate the TDD manifest before implementation.",
|
|
8684
|
+
body: "After planning artifacts exist, run this before `fet apply`. It writes `openspec/changes/<change-id>/.fet/tdd-manifest.yaml`, `tdd-spec.md`, and `tdd-instructions.md`. Then add failing tests in the repo before implementation (tests/ edits need user approval per write-boundary)."
|
|
8685
|
+
} : {
|
|
8686
|
+
description: "\u751F\u6210\u672C change \u7684 TDD \u6E05\u5355\u4E0E\u6D4B\u8BD5\u6307\u5F15",
|
|
8687
|
+
title: "\u5728\u5B9E\u65BD\u524D\u751F\u6210 TDD \u6E05\u5355\u3002",
|
|
8688
|
+
body: "\u89C4\u5212\u4EA7\u7269\u5C31\u7EEA\u540E\u3001\u6267\u884C `fet apply` \u4E4B\u524D\u8FD0\u884C\u3002\u4F1A\u5199\u5165 `openspec/changes/<change-id>/.fet/tdd-manifest.yaml`\u3001`tdd-spec.md`\u3001`tdd-instructions.md`\uFF0C\u518D\u5728\u4ED3\u5E93\u4E2D\u7F16\u5199\u9884\u671F\u5931\u8D25\u7684\u6D4B\u8BD5\uFF08\u4FEE\u6539 tests/ \u987B\u7B26\u5408 write-boundary\uFF0C\u5148\u83B7\u7528\u6237\u540C\u610F\uFF09\u3002"
|
|
8689
|
+
};
|
|
8690
|
+
}
|
|
8691
|
+
if (command === "test") {
|
|
8692
|
+
return language === "en" ? {
|
|
8693
|
+
description: "Run unit tests scoped to the change TDD manifest",
|
|
8694
|
+
title: "Run change-scoped tests after implementation.",
|
|
8695
|
+
body: "Requires `tdd-manifest.yaml`. Records pass/fail in FET state; `fet verify` is blocked until this passes (unless configured to skip)."
|
|
8696
|
+
} : {
|
|
8697
|
+
description: "\u6309 change TDD \u6E05\u5355\u8FD0\u884C\u5355\u6D4B",
|
|
8698
|
+
title: "\u5B9E\u73B0\u5B8C\u6210\u540E\u6309 change \u8FD0\u884C\u6D4B\u8BD5\u3002",
|
|
8699
|
+
body: "\u9700\u8981\u6709\u6548\u7684 `tdd-manifest.yaml`\u3002\u7ED3\u679C\u5199\u5165 FET \u72B6\u6001\uFF1B\u672A\u901A\u8FC7\u524D `fet verify` \u4F1A\u88AB\u62E6\u622A\uFF08\u9664\u975E\u914D\u7F6E\u4E3A skip\uFF09\u3002"
|
|
8700
|
+
};
|
|
8701
|
+
}
|
|
8702
|
+
return language === "en" ? {
|
|
8703
|
+
description: "Layout-only visual verification for a change",
|
|
8704
|
+
title: "Run layout-only visual checks for the change.",
|
|
8705
|
+
body: "Default `fet visual` refreshes the manifest, captures with Playwright (`--base-url` required), and runs layout-only checks (no pixel match on dynamic API content). Use `--plan`, `--capture-only`, or `--check-layout-only` only when debugging."
|
|
8706
|
+
} : {
|
|
8707
|
+
description: "change \u7EA7 layout-only \u89C6\u89C9\u9A8C\u6536",
|
|
8708
|
+
title: "\u5BF9 change \u505A layout-only \u89C6\u89C9\u9A8C\u6536\u3002",
|
|
8709
|
+
body: "\u9ED8\u8BA4 `fet visual` \u4F1A\u66F4\u65B0\u6E05\u5355\u3001Playwright \u622A\u56FE\uFF08\u9700 `--base-url`\uFF09\u5E76\u505A layout-only \u68C0\u67E5\uFF08\u4E0D\u5BF9\u52A8\u6001\u63A5\u53E3\u5185\u5BB9\u505A\u50CF\u7D20\u5BF9\u6BD4\uFF09\u3002\u4EC5\u5728\u8C03\u8BD5\u65F6\u4F7F\u7528 `--plan`\u3001`--capture-only`\u3001`--check-layout-only`\u3002"
|
|
8710
|
+
};
|
|
8711
|
+
}
|
|
7420
8712
|
|
|
7421
8713
|
// src/adapters/codex/index.ts
|
|
7422
8714
|
var CodexAdapter = class {
|
|
@@ -7424,7 +8716,7 @@ var CodexAdapter = class {
|
|
|
7424
8716
|
adapterVersion = 1;
|
|
7425
8717
|
async detect(projectRoot) {
|
|
7426
8718
|
return {
|
|
7427
|
-
detected: await exists6(
|
|
8719
|
+
detected: await exists6(join35(projectRoot, ".codex")) || await exists6(join35(projectRoot, "AGENTS.md")),
|
|
7428
8720
|
reason: "Codex adapter is available for projects that use AGENTS.md"
|
|
7429
8721
|
};
|
|
7430
8722
|
}
|
|
@@ -7463,7 +8755,7 @@ var CodexAdapter = class {
|
|
|
7463
8755
|
if (existing && !existing.includes("FET:MANAGED") && force) {
|
|
7464
8756
|
await createBackup(target);
|
|
7465
8757
|
}
|
|
7466
|
-
await
|
|
8758
|
+
await mkdir15(dirname11(target), { recursive: true });
|
|
7467
8759
|
await atomicWrite(target, file.content);
|
|
7468
8760
|
written.push(displayPath);
|
|
7469
8761
|
}
|
|
@@ -7490,9 +8782,9 @@ var CodexAdapter = class {
|
|
|
7490
8782
|
};
|
|
7491
8783
|
function resolveTarget(projectRoot, file) {
|
|
7492
8784
|
if (file.root === "codex-home") {
|
|
7493
|
-
return
|
|
8785
|
+
return join35(resolveCodexHome(), file.path);
|
|
7494
8786
|
}
|
|
7495
|
-
return
|
|
8787
|
+
return join35(projectRoot, file.path);
|
|
7496
8788
|
}
|
|
7497
8789
|
function displayPathFor(file) {
|
|
7498
8790
|
if (file.root === "codex-home") {
|
|
@@ -7501,11 +8793,11 @@ function displayPathFor(file) {
|
|
|
7501
8793
|
return file.path;
|
|
7502
8794
|
}
|
|
7503
8795
|
function resolveCodexHome() {
|
|
7504
|
-
return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ??
|
|
8796
|
+
return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join35(homedir(), ".codex");
|
|
7505
8797
|
}
|
|
7506
8798
|
async function readExisting(path) {
|
|
7507
8799
|
try {
|
|
7508
|
-
return await
|
|
8800
|
+
return await readFile24(path, "utf8");
|
|
7509
8801
|
} catch {
|
|
7510
8802
|
return null;
|
|
7511
8803
|
}
|
|
@@ -7520,8 +8812,8 @@ async function exists6(path) {
|
|
|
7520
8812
|
}
|
|
7521
8813
|
|
|
7522
8814
|
// src/adapters/cursor/index.ts
|
|
7523
|
-
import { mkdir as
|
|
7524
|
-
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";
|
|
7525
8817
|
|
|
7526
8818
|
// src/adapters/cursor/templates.ts
|
|
7527
8819
|
function cursorFigmaStopRuleFile(language = DEFAULT_LANGUAGE) {
|
|
@@ -7536,6 +8828,28 @@ function cursorSpecLanguageRuleFile(language = DEFAULT_LANGUAGE) {
|
|
|
7536
8828
|
content: renderCursorSpecLanguageRule(language)
|
|
7537
8829
|
};
|
|
7538
8830
|
}
|
|
8831
|
+
function cursorWriteBoundaryRuleFile(language = DEFAULT_LANGUAGE) {
|
|
8832
|
+
return {
|
|
8833
|
+
path: ".cursor/rules/fet-write-boundary.mdc",
|
|
8834
|
+
content: renderCursorWriteBoundaryRule(language)
|
|
8835
|
+
};
|
|
8836
|
+
}
|
|
8837
|
+
function cursorHookFiles() {
|
|
8838
|
+
return [
|
|
8839
|
+
{
|
|
8840
|
+
path: ".cursor/hooks/fet-guard-write-paths.mjs",
|
|
8841
|
+
content: renderCursorWritePathsHookMjs()
|
|
8842
|
+
},
|
|
8843
|
+
{
|
|
8844
|
+
path: ".cursor/hooks/fet-guard-shell-writes.mjs",
|
|
8845
|
+
content: renderCursorShellWriteHookMjs()
|
|
8846
|
+
},
|
|
8847
|
+
{
|
|
8848
|
+
path: ".cursor/hooks.json",
|
|
8849
|
+
content: renderCursorHooksJson()
|
|
8850
|
+
}
|
|
8851
|
+
];
|
|
8852
|
+
}
|
|
7539
8853
|
function cursorUiDisplayContractRuleFile(language = DEFAULT_LANGUAGE) {
|
|
7540
8854
|
return {
|
|
7541
8855
|
path: ".cursor/rules/fet-ui-display-contract.mdc",
|
|
@@ -7545,6 +8859,7 @@ function cursorUiDisplayContractRuleFile(language = DEFAULT_LANGUAGE) {
|
|
|
7545
8859
|
function cursorRuleFiles(language = DEFAULT_LANGUAGE) {
|
|
7546
8860
|
return [
|
|
7547
8861
|
cursorRuleFile(language),
|
|
8862
|
+
cursorWriteBoundaryRuleFile(language),
|
|
7548
8863
|
cursorFigmaStopRuleFile(language),
|
|
7549
8864
|
cursorUiDisplayContractRuleFile(language),
|
|
7550
8865
|
cursorSpecLanguageRuleFile(language)
|
|
@@ -7582,6 +8897,7 @@ ${languageInstruction(language)}
|
|
|
7582
8897
|
- \u6309 Figma \u5B9E\u73B0 UI \u65F6\u9075\u5B88 \`.cursor/rules/fet-figma-stop.mdc\`\uFF1B\u82E5\u5B58\u5728 \`openspec/changes/<change-id>/.fet/figma-apply-instructions.md\` \u5FC5\u987B\u5728\u6539 UI \u524D\u5B8C\u6574\u6267\u884C\uFF1B\u540C\u76EE\u5F55 \`figma-stop.md\` \u542B\u94FE\u63A5\u4E0E\u505C\u6B62\u89C4\u5219\u3002
|
|
7583
8898
|
- \u7F16\u5199\u6216\u4FEE\u6539 OpenSpec \`specs/**/spec.md\` \u65F6\u9075\u5B88 \`.cursor/rules/fet-spec-language.mdc\`\uFF08\u82F1\u6587\u89C4\u8303 + \`<!-- \u4E2D\u6587\uFF1A... -->\`\uFF0C\u540C\u6B21\u7F16\u8F91\u540C\u6B65\u66F4\u65B0\u4E2D\u6587\u8BF4\u660E\uFF09\u3002
|
|
7584
8899
|
- \u7ED1\u5B9A\u63A5\u53E3\u7684 UI \u9075\u5B88 \`.cursor/rules/fet-ui-display-contract.mdc\`\uFF1B\u5B58\u5728 \`openspec/changes/<change-id>/.fet/ui-display-contract.yaml\` \u65F6\u5B9E\u65BD\u524D\u987B\u786E\u8BA4 displayFields\u3002
|
|
8900
|
+
- \u4FEE\u6539\u6587\u4EF6\u65F6\u9075\u5B88 \`.cursor/rules/fet-write-boundary.mdc\`\uFF08\u9ED8\u8BA4\u4EC5 \`src/**\`\u3001\`openspec/**\`\u3001\`**/.fet/**\`\uFF1B\u5176\u5B83\u8DEF\u5F84\u987B\u7528\u6237\u660E\u786E\u540C\u610F\uFF09\u3002Cursor hooks \u4F1A\u5BF9\u8D8A\u754C\u5199\u5165\u5F39\u51FA\u5BA1\u6279\u3002
|
|
7585
8901
|
|
|
7586
8902
|
\u5982\u679C\u7528\u6237\u8F93\u5165\u7C7B\u4F3C \`/fet apply\` \u7684\u8BF7\u6C42\uFF0C\u8BF7\u628A\u5B83\u89C6\u4E3A\u5DE5\u4F5C\u6D41\u610F\u56FE\uFF0C\u5E76\u63D0\u793A\u6216\u6267\u884C\u5BF9\u5E94\u7684\u7EC8\u7AEF\u547D\u4EE4 \`fet <cmd>\`\u3002
|
|
7587
8903
|
`
|
|
@@ -7617,7 +8933,9 @@ ${languageInstruction(language)}
|
|
|
7617
8933
|
|
|
7618
8934
|
\`fet fill-context\` \u53EF\u80FD\u5DF2\u5199\u5165\u5C0F\u7A0B\u5E8F\u5305\u4F53\u79EF\u8868\u4E0E 2MB \u5F00\u53D1\u7EA6\u675F\uFF0C\u4E0D\u8981\u8986\u76D6 AGENTS.md\u300C\u5C0F\u7A0B\u5E8F\u300D\u8282\u4E2D\u7684\u626B\u63CF\u7ED3\u679C\uFF0C\u9664\u975E\u4ED3\u5E93\u7ED3\u6784\u5DF2\u53D8\u3002
|
|
7619
8935
|
|
|
7620
|
-
\u68C0\u67E5 README\u3001package scripts\u3001\u8DEF\u7531\u3001\u6D4B\u8BD5\u3001\u6E90\u7801\u7ED3\u6784\u548C\u73B0\u6709\u7EA6\u5B9A\u540E\uFF0C\u628A AGENTS.md \u4E2D**\u5176\u4F59** \`[NEEDS LLM INPUT]\` \u6216 \`[NEED LLM INPUT]\` \u5360\u4F4D\u7B26\u66FF\u6362\u4E3A\u5177\u4F53\u3001\u7B80\u6D01\u3001\u9879\u76EE\u76F8\u5173\u7684\u5185\u5BB9\u3002\u4FDD\u7559 FET \u6258\u7BA1\u6807\u8BB0\uFF0C\u4E0D\u8981\u4FEE\u6539\u4E1A\u52A1\u4EE3\u7801\u3002
|
|
8936
|
+
\u68C0\u67E5 README\u3001package scripts\u3001\u8DEF\u7531\u3001\u6D4B\u8BD5\u3001\u6E90\u7801\u7ED3\u6784\u548C\u73B0\u6709\u7EA6\u5B9A\u540E\uFF0C\u628A AGENTS.md \u4E2D**\u5176\u4F59** \`[NEEDS LLM INPUT]\` \u6216 \`[NEED LLM INPUT]\` \u5360\u4F4D\u7B26\u66FF\u6362\u4E3A\u5177\u4F53\u3001\u7B80\u6D01\u3001\u9879\u76EE\u76F8\u5173\u7684\u5185\u5BB9\u3002\u4FDD\u7559 FET \u6258\u7BA1\u6807\u8BB0\uFF0C\u4E0D\u8981\u4FEE\u6539\u4E1A\u52A1\u4EE3\u7801\u3002\u4FEE\u6539 \`AGENTS.md\` \u4E0D\u5728\u9ED8\u8BA4\u5199\u8DEF\u5F84\u5185\uFF0C\u987B\u786E\u8BA4\u7528\u6237\u5DF2\u540C\u610F\uFF08\u6216\u7531 hooks \u5BA1\u6279\uFF09\u3002
|
|
8937
|
+
|
|
8938
|
+
${renderWriteBoundaryGuardrail(language)}
|
|
7621
8939
|
`;
|
|
7622
8940
|
}
|
|
7623
8941
|
if (command === "graph-setup") {
|
|
@@ -7723,6 +9041,8 @@ ${renderSpecArtifactGuardrail(language)}
|
|
|
7723
9041
|
|
|
7724
9042
|
${renderUiDisplayContractGuardrail(language)}
|
|
7725
9043
|
|
|
9044
|
+
${renderWriteBoundaryGuardrail(language)}
|
|
9045
|
+
|
|
7726
9046
|
\u6267\u884C\u524D\u8BF7\u9605\u8BFB AGENTS.md\u3001openspec/config.yaml\uFF08\u542B \`fet.specLanguage\`\uFF09\u4E0E\u5F53\u524D change \u5DF2\u6709\u4EA7\u7269\u3002
|
|
7727
9047
|
`;
|
|
7728
9048
|
}
|
|
@@ -7739,12 +9059,14 @@ function renderApplySkill(usage, language) {
|
|
|
7739
9059
|
const figmaBlock = language === "en" ? `If \`fet apply\` output or the change has \`openspec/changes/<change-id>/.fet/figma-apply-instructions.md\`:
|
|
7740
9060
|
|
|
7741
9061
|
1. Read that file and \`figma-stop.md\` in the same folder before any UI code.
|
|
7742
|
-
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.
|
|
7743
9064
|
3. If Figma fails or design is unclear, stop and ask the user\u2014do not guess styles or mark UI tasks done.
|
|
7744
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
|
|
7745
9066
|
|
|
7746
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
|
|
7747
|
-
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
|
|
7748
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
|
|
7749
9071
|
4. \u8BBE\u8BA1\u786E\u8BA4\u540E\uFF0C\u518D\u6309 \`tasks.md\` \u5B9E\u65BD\u4EFB\u52A1\u3002`;
|
|
7750
9072
|
return `<!-- FET:MANAGED
|
|
@@ -7778,6 +9100,8 @@ ${uiContractBlock}
|
|
|
7778
9100
|
\u6267\u884C\u524D\u8BF7\u9605\u8BFB AGENTS.md\u3001openspec/config.yaml \u4E0E\u5F53\u524D change \u4E0B\u7684 OpenSpec \u4EA7\u7269\u3002
|
|
7779
9101
|
|
|
7780
9102
|
\u5B9E\u65BD\u524D\u987B\u5DF2\u8FD0\u884C \`fet tdd\`\uFF1B\u5B58\u5728 \`openspec/changes/<change-id>/.fet/tdd-manifest.yaml\` \u65F6\u6309\u6E05\u5355\u7F16\u5199\u6D4B\u8BD5\u4E0E\u5B9E\u73B0\uFF0C\u5E76\u5728 \`fet verify\` \u524D\u5148 \`fet test\`\u3002
|
|
9103
|
+
|
|
9104
|
+
${renderWriteBoundaryGuardrail(language)}
|
|
7781
9105
|
`;
|
|
7782
9106
|
}
|
|
7783
9107
|
function renderVisualSkill(usage, language) {
|
|
@@ -7846,14 +9170,14 @@ var CursorAdapter = class {
|
|
|
7846
9170
|
adapterVersion = 1;
|
|
7847
9171
|
async detect(projectRoot) {
|
|
7848
9172
|
return {
|
|
7849
|
-
detected: await exists7(
|
|
9173
|
+
detected: await exists7(join36(projectRoot, ".cursor")),
|
|
7850
9174
|
reason: "Cursor adapter is available for any project"
|
|
7851
9175
|
};
|
|
7852
9176
|
}
|
|
7853
9177
|
async planInstall(_projectRoot, language) {
|
|
7854
9178
|
return {
|
|
7855
9179
|
tool: this.tool,
|
|
7856
|
-
files: [...cursorSkillFiles(language), ...cursorRuleFiles(language)].map((file) => ({
|
|
9180
|
+
files: [...cursorSkillFiles(language), ...cursorRuleFiles(language), ...cursorHookFiles()].map((file) => ({
|
|
7857
9181
|
...file,
|
|
7858
9182
|
managed: true
|
|
7859
9183
|
}))
|
|
@@ -7863,8 +9187,14 @@ var CursorAdapter = class {
|
|
|
7863
9187
|
const written = [];
|
|
7864
9188
|
const skipped = [];
|
|
7865
9189
|
for (const file of plan.files) {
|
|
7866
|
-
const target =
|
|
9190
|
+
const target = join36(projectRoot, file.path);
|
|
7867
9191
|
const existing = await readExisting2(target);
|
|
9192
|
+
if (file.path === ".cursor/hooks.json") {
|
|
9193
|
+
await mkdir16(dirname12(target), { recursive: true });
|
|
9194
|
+
await atomicWrite(target, mergeCursorHooksJson(existing, file.content));
|
|
9195
|
+
written.push(file.path);
|
|
9196
|
+
continue;
|
|
9197
|
+
}
|
|
7868
9198
|
if (existing && !existing.includes("FET:MANAGED") && !force) {
|
|
7869
9199
|
throw new FetError({
|
|
7870
9200
|
code: "TOOL_ADAPTER_CONFLICT" /* ToolAdapterConflict */,
|
|
@@ -7876,7 +9206,7 @@ var CursorAdapter = class {
|
|
|
7876
9206
|
if (existing && !existing.includes("FET:MANAGED") && force) {
|
|
7877
9207
|
await createBackup(target);
|
|
7878
9208
|
}
|
|
7879
|
-
await
|
|
9209
|
+
await mkdir16(dirname12(target), { recursive: true });
|
|
7880
9210
|
await atomicWrite(target, file.content);
|
|
7881
9211
|
written.push(file.path);
|
|
7882
9212
|
}
|
|
@@ -7886,14 +9216,16 @@ var CursorAdapter = class {
|
|
|
7886
9216
|
const plan = await this.planInstall(projectRoot);
|
|
7887
9217
|
const checks = [];
|
|
7888
9218
|
for (const file of plan.files) {
|
|
7889
|
-
const target =
|
|
9219
|
+
const target = join36(projectRoot, file.path);
|
|
7890
9220
|
const content = await readExisting2(target);
|
|
7891
|
-
const
|
|
7892
|
-
const
|
|
9221
|
+
const hooksManaged = file.path === ".cursor/hooks.json" && Boolean(content?.includes('"writeBoundary"'));
|
|
9222
|
+
const hookScript = file.path.startsWith(".cursor/hooks/fet-guard-") && file.path.endsWith(".mjs");
|
|
9223
|
+
const managed = hooksManaged || hookScript || Boolean(content?.includes("FET:MANAGED"));
|
|
9224
|
+
const versionMatches = file.path === ".cursor/hooks.json" ? Boolean(content?.includes('"writeBoundary"')) : hookScript ? Boolean(content?.includes(`adapterVersion: ${this.adapterVersion}`)) : Boolean(content?.includes(`adapterVersion: ${this.adapterVersion}`));
|
|
7893
9225
|
checks.push({
|
|
7894
9226
|
id: `cursor:${file.path}`,
|
|
7895
9227
|
status: !content ? "warn" : managed && versionMatches ? "pass" : "warn",
|
|
7896
|
-
message: !content ? `${file.path} \u7F3A\u5931` : !managed ? `${file.path} \u5DF2\u5B58\u5728\uFF0C\u4F46\u4E0D\u7531 FET \u7BA1\u7406` : !versionMatches ? `${file.path}
|
|
9228
|
+
message: !content ? `${file.path} \u7F3A\u5931` : !managed ? `${file.path} \u5DF2\u5B58\u5728\uFF0C\u4F46\u4E0D\u7531 FET \u7BA1\u7406` : !versionMatches ? `${file.path} \u7248\u672C\u5DF2\u8FC7\u671F\uFF08\u8BF7\u8FD0\u884C fet init \u66F4\u65B0\uFF09` : `${file.path} \u5DF2\u5B58\u5728\u4E14\u7248\u672C\u5339\u914D`,
|
|
7897
9229
|
suggestedCommand: !content || !managed || !versionMatches ? "fet init" : void 0
|
|
7898
9230
|
});
|
|
7899
9231
|
}
|
|
@@ -7902,7 +9234,7 @@ var CursorAdapter = class {
|
|
|
7902
9234
|
};
|
|
7903
9235
|
async function readExisting2(path) {
|
|
7904
9236
|
try {
|
|
7905
|
-
return await
|
|
9237
|
+
return await readFile25(path, "utf8");
|
|
7906
9238
|
} catch {
|
|
7907
9239
|
return null;
|
|
7908
9240
|
}
|
|
@@ -7922,12 +9254,12 @@ import { promisify as promisify4 } from "util";
|
|
|
7922
9254
|
|
|
7923
9255
|
// src/openspec/inspector.ts
|
|
7924
9256
|
import { readdir as readdir6, stat as stat15 } from "fs/promises";
|
|
7925
|
-
import { join as
|
|
9257
|
+
import { join as join37 } from "path";
|
|
7926
9258
|
async function inspectOpenSpecProject(projectRoot) {
|
|
7927
|
-
const openspecPath =
|
|
7928
|
-
const changesPath =
|
|
7929
|
-
const legacyArchivePath =
|
|
7930
|
-
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");
|
|
7931
9263
|
return {
|
|
7932
9264
|
exists: await exists8(openspecPath),
|
|
7933
9265
|
changes: await listDirectories(changesPath, { exclude: ["archive"] }),
|
|
@@ -7935,13 +9267,13 @@ async function inspectOpenSpecProject(projectRoot) {
|
|
|
7935
9267
|
};
|
|
7936
9268
|
}
|
|
7937
9269
|
async function inspectOpenSpecChange(projectRoot, changeId) {
|
|
7938
|
-
const changePath =
|
|
7939
|
-
const tasksPath =
|
|
7940
|
-
const specsPath =
|
|
9270
|
+
const changePath = join37(projectRoot, "openspec", "changes", changeId);
|
|
9271
|
+
const tasksPath = join37(changePath, "tasks.md");
|
|
9272
|
+
const specsPath = join37(changePath, "specs");
|
|
7941
9273
|
return {
|
|
7942
9274
|
changeId,
|
|
7943
9275
|
exists: await exists8(changePath),
|
|
7944
|
-
hasProposal: await exists8(
|
|
9276
|
+
hasProposal: await exists8(join37(changePath, "proposal.md")),
|
|
7945
9277
|
hasTasks: await exists8(tasksPath),
|
|
7946
9278
|
hasSpecs: await exists8(specsPath),
|
|
7947
9279
|
tasksPath,
|
|
@@ -8143,12 +9475,12 @@ function escapeRegExp(value) {
|
|
|
8143
9475
|
|
|
8144
9476
|
// src/scanner/routes.ts
|
|
8145
9477
|
import { readdir as readdir7, stat as stat16 } from "fs/promises";
|
|
8146
|
-
import { join as
|
|
9478
|
+
import { join as join38, relative as relative5, sep } from "path";
|
|
8147
9479
|
async function scanRoutes(projectRoot) {
|
|
8148
9480
|
const candidates = ["src/routes", "src/pages", "app", "pages"];
|
|
8149
9481
|
const routes = [];
|
|
8150
9482
|
for (const candidate of candidates) {
|
|
8151
|
-
const root =
|
|
9483
|
+
const root = join38(projectRoot, candidate);
|
|
8152
9484
|
if (!await exists9(root)) {
|
|
8153
9485
|
continue;
|
|
8154
9486
|
}
|
|
@@ -8176,7 +9508,7 @@ async function listFiles(root) {
|
|
|
8176
9508
|
const entries = await readdir7(root, { withFileTypes: true });
|
|
8177
9509
|
const files = [];
|
|
8178
9510
|
for (const entry of entries) {
|
|
8179
|
-
const path =
|
|
9511
|
+
const path = join38(root, entry.name);
|
|
8180
9512
|
if (entry.isDirectory()) {
|
|
8181
9513
|
files.push(...await listFiles(path));
|
|
8182
9514
|
} else {
|
|
@@ -8344,9 +9676,9 @@ async function createCommandContext(command, options) {
|
|
|
8344
9676
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
8345
9677
|
|
|
8346
9678
|
// src/update/check.ts
|
|
8347
|
-
import { mkdir as
|
|
9679
|
+
import { mkdir as mkdir17, readFile as readFile26, writeFile as writeFile3 } from "fs/promises";
|
|
8348
9680
|
import { homedir as homedir2 } from "os";
|
|
8349
|
-
import { dirname as dirname13, join as
|
|
9681
|
+
import { dirname as dirname13, join as join39 } from "path";
|
|
8350
9682
|
var DEFAULT_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
8351
9683
|
function getFetUpdateCheckMode(env = process.env) {
|
|
8352
9684
|
const value = env.FET_UPDATE_CHECK?.trim().toLowerCase();
|
|
@@ -8419,11 +9751,11 @@ function formatFetUpdateWarning(availability, language) {
|
|
|
8419
9751
|
}
|
|
8420
9752
|
function cachePath() {
|
|
8421
9753
|
const home = process.env.FET_UPDATE_CHECK_CACHE_HOME?.trim() || homedir2();
|
|
8422
|
-
return
|
|
9754
|
+
return join39(home, ".fet", "update-check-cache.json");
|
|
8423
9755
|
}
|
|
8424
9756
|
async function readUpdateCheckCache() {
|
|
8425
9757
|
try {
|
|
8426
|
-
const raw = await
|
|
9758
|
+
const raw = await readFile26(cachePath(), "utf8");
|
|
8427
9759
|
const parsed = JSON.parse(raw);
|
|
8428
9760
|
if (typeof parsed.latestVersion !== "string" || typeof parsed.checkedAt !== "string") {
|
|
8429
9761
|
return null;
|
|
@@ -8439,8 +9771,8 @@ async function readUpdateCheckCache() {
|
|
|
8439
9771
|
}
|
|
8440
9772
|
async function writeUpdateCheckCache(cache) {
|
|
8441
9773
|
const path = cachePath();
|
|
8442
|
-
await
|
|
8443
|
-
await
|
|
9774
|
+
await mkdir17(dirname13(path), { recursive: true });
|
|
9775
|
+
await writeFile3(path, `${JSON.stringify(cache, null, 2)}
|
|
8444
9776
|
`, "utf8");
|
|
8445
9777
|
}
|
|
8446
9778
|
|
|
@@ -8527,6 +9859,19 @@ addGlobalOptions(program.command("doctor").description("\u8BCA\u65AD\u72B6\u6001
|
|
|
8527
9859
|
wrap("doctor", (ctx, options) => doctorCommand(ctx, { fixLock: Boolean(options.fixLock) }))
|
|
8528
9860
|
);
|
|
8529
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
|
+
);
|
|
8530
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(
|
|
8531
9876
|
wrap("test", (ctx, options) => testCommand(ctx, { plan: Boolean(options.plan) }))
|
|
8532
9877
|
);
|
|
@@ -8659,4 +10004,3 @@ function extractGlobalOptions(args) {
|
|
|
8659
10004
|
}
|
|
8660
10005
|
return options;
|
|
8661
10006
|
}
|
|
8662
|
-
//# sourceMappingURL=index.js.map
|