@nick848/fet 1.1.3 → 1.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -1
- package/README_en.md +14 -1
- package/dist/cli/index.js +309 -74
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,6 +50,7 @@ fet --help
|
|
|
50
50
|
|
|
51
51
|
```sh
|
|
52
52
|
fet init
|
|
53
|
+
fet fill-context
|
|
53
54
|
fet doctor
|
|
54
55
|
```
|
|
55
56
|
|
|
@@ -95,6 +96,18 @@ fet init --lang en
|
|
|
95
96
|
| `--verbose` | 输出更多诊断信息。 | `fet doctor --verbose` |
|
|
96
97
|
| `--no-color` | 禁用终端颜色。 | `fet --no-color doctor` |
|
|
97
98
|
|
|
99
|
+
### 版本更新检查
|
|
100
|
+
|
|
101
|
+
除 `fet update` 外,FET 在执行其他命令前会检查 npm 上是否有新版本(默认缓存 6 小时)。发现新版本时会提示当前版本与最新版本;在交互式终端中可选择立即升级、跳过并继续当前任务,或取消命令。
|
|
102
|
+
|
|
103
|
+
| 环境变量 | 说明 |
|
|
104
|
+
|----------|------|
|
|
105
|
+
| `FET_UPDATE_CHECK` | `off` 关闭检查;`warn` 仅警告并继续;`confirm` 在 TTY 中交互询问(默认)。 |
|
|
106
|
+
| `FET_SKIP_UPDATE_CHECK` | 设为 `1` 时等同于 `FET_UPDATE_CHECK=off`。 |
|
|
107
|
+
| `FET_UPDATE_CHECK_TTL_MS` | 版本检查缓存时长(毫秒),默认 `21600000`(6 小时)。 |
|
|
108
|
+
|
|
109
|
+
使用 `--yes` 或 `--json` 时会跳过交互式升级询问;跳过某版本后,同一最新版本不会重复提示,直到检测到更高的新版本。
|
|
110
|
+
|
|
98
111
|
## 命令列表
|
|
99
112
|
|
|
100
113
|
| 命令 | 用法 | 说明 |
|
|
@@ -104,7 +117,7 @@ fet init --lang en
|
|
|
104
117
|
| `fet fill-context` | `fet fill-context [--yes]` | 刷新 IDE 交接命令,引导 AI 替换 `AGENTS.md` 占位符。 |
|
|
105
118
|
| `fet doctor` | `fet doctor [--fix-lock]` | 诊断 OpenSpec、FET 状态、上下文文件、工具集成和锁文件。 |
|
|
106
119
|
| `fet explore` | `fet explore [...args] [--change <id>]` | 通过 FET 执行 OpenSpec 原生 explore,保留交互式需求澄清。 |
|
|
107
|
-
| `fet propose` | `fet propose <change-id>` |
|
|
120
|
+
| `fet propose` | `fet propose <change-id-or-description>` | 创建 OpenSpec change,并生成第一个 ready 规划产物;后续产物用 `continue` 逐步推进。 |
|
|
108
121
|
| `fet new` | `fet new <change-id>` | 创建新的 OpenSpec change。 |
|
|
109
122
|
| `fet continue` | `fet continue [...args] [--change <id>]` | 通过 FET 执行 OpenSpec 原生 continue。 |
|
|
110
123
|
| `fet ff` | `fet ff [...args] [--change <id>]` | 通过 FET 执行 OpenSpec 原生 ff。 |
|
package/README_en.md
CHANGED
|
@@ -51,6 +51,7 @@ Run this in a project root that should use OpenSpec:
|
|
|
51
51
|
|
|
52
52
|
```sh
|
|
53
53
|
fet init
|
|
54
|
+
fet fill-context
|
|
54
55
|
fet doctor
|
|
55
56
|
```
|
|
56
57
|
|
|
@@ -96,6 +97,18 @@ fet init --lang en
|
|
|
96
97
|
| `--verbose` | Print more diagnostics. | `fet doctor --verbose` |
|
|
97
98
|
| `--no-color` | Disable terminal colors. | `fet --no-color doctor` |
|
|
98
99
|
|
|
100
|
+
### Version update check
|
|
101
|
+
|
|
102
|
+
Before most commands (except `fet update`), FET checks npm for a newer release (cached for 6 hours by default). When an update is available, it prints the current and latest versions. In an interactive terminal you can upgrade now, skip and continue the current task, or cancel the command.
|
|
103
|
+
|
|
104
|
+
| Variable | Description |
|
|
105
|
+
|----------|-------------|
|
|
106
|
+
| `FET_UPDATE_CHECK` | `off` disables checks; `warn` prints a warning and continues; `confirm` prompts in a TTY (default). |
|
|
107
|
+
| `FET_SKIP_UPDATE_CHECK` | Set to `1` to disable checks (same as `FET_UPDATE_CHECK=off`). |
|
|
108
|
+
| `FET_UPDATE_CHECK_TTL_MS` | Cache TTL in milliseconds; default `21600000` (6 hours). |
|
|
109
|
+
|
|
110
|
+
`--yes` and `--json` skip the interactive upgrade prompt. If you skip a specific latest version, FET will not prompt again for that version until a newer release appears.
|
|
111
|
+
|
|
99
112
|
## Commands
|
|
100
113
|
|
|
101
114
|
| Command | Usage | Description |
|
|
@@ -106,7 +119,7 @@ fet init --lang en
|
|
|
106
119
|
| `fet fill-context` | `fet fill-context [--yes]` | Refresh IDE handoff commands that ask AI to replace `AGENTS.md` placeholders. |
|
|
107
120
|
| `fet doctor` | `fet doctor [--fix-lock]` | Diagnose OpenSpec, FET state, context files, tool integration, and lock files. |
|
|
108
121
|
| `fet explore` | `fet explore [...args] [--change <id>]` | Run native OpenSpec explore through FET so clarification prompts remain interactive. |
|
|
109
|
-
| `fet propose` | `fet propose <change-id>` | Create
|
|
122
|
+
| `fet propose` | `fet propose <change-id-or-description>` | Create an OpenSpec change and the first ready planning artifact; use `continue` for each subsequent artifact after review. |
|
|
110
123
|
| `fet new` | `fet new <change-id>` | Create a new OpenSpec change. |
|
|
111
124
|
| `fet continue` | `fet continue [...args] [--change <id>]` | Run native OpenSpec continue through FET. |
|
|
112
125
|
| `fet ff` | `fet ff [...args] [--change <id>]` | Run native OpenSpec ff through FET. |
|
package/dist/cli/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
} from "../chunk-J5WB4KAL.js";
|
|
6
6
|
|
|
7
7
|
// src/cli/index.ts
|
|
8
|
-
import { createInterface as
|
|
8
|
+
import { createInterface as createInterface3 } from "readline/promises";
|
|
9
9
|
import { Command } from "commander";
|
|
10
10
|
|
|
11
11
|
// src/commands/doctor.ts
|
|
@@ -2287,8 +2287,9 @@ async function artifactWorkflowCommand(ctx, command, args) {
|
|
|
2287
2287
|
warnings: runState.graphContext?.warnings,
|
|
2288
2288
|
nextSteps: [
|
|
2289
2289
|
`Create or update openspec/changes/${changeId}/${resolveOutputPath(status, artifactId)}`,
|
|
2290
|
+
"Review the artifact with the user before generating the next planning file.",
|
|
2290
2291
|
`Run fet passthrough status --change ${changeId}`,
|
|
2291
|
-
status.isComplete ? `Run fet apply --change ${changeId}` : `Run fet continue --change ${changeId}`
|
|
2292
|
+
status.isComplete ? `Run fet apply --change ${changeId}` : `Run fet continue --change ${changeId} when ready for the next artifact`
|
|
2292
2293
|
],
|
|
2293
2294
|
data: {
|
|
2294
2295
|
changeId,
|
|
@@ -2678,60 +2679,16 @@ async function assertVerifiedChange(ctx, changeId) {
|
|
|
2678
2679
|
}
|
|
2679
2680
|
}
|
|
2680
2681
|
|
|
2681
|
-
// src/
|
|
2682
|
+
// src/update/npm.ts
|
|
2682
2683
|
import { spawn } from "child_process";
|
|
2683
|
-
var
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
const npmExecutable = process.env.FET_UPDATE_NPM_EXECUTABLE ?? defaultNpmExecutable();
|
|
2687
|
-
const latestVersion = await resolveLatestVersion(packageName, npmExecutable);
|
|
2688
|
-
const currentVersion = ctx.fetVersion;
|
|
2689
|
-
if (compareVersions(currentVersion, latestVersion) >= 0) {
|
|
2690
|
-
ctx.output.result({
|
|
2691
|
-
ok: true,
|
|
2692
|
-
command: "update",
|
|
2693
|
-
summary: ctx.language === "en" ? `FET is already up to date (${currentVersion}).` : `FET \u5DF2\u662F\u6700\u65B0\u7248 (${currentVersion})\u3002`,
|
|
2694
|
-
data: {
|
|
2695
|
-
packageName,
|
|
2696
|
-
currentVersion,
|
|
2697
|
-
latestVersion,
|
|
2698
|
-
updated: false
|
|
2699
|
-
}
|
|
2700
|
-
});
|
|
2701
|
-
return;
|
|
2702
|
-
}
|
|
2703
|
-
if (!ctx.json) {
|
|
2704
|
-
ctx.output.info(
|
|
2705
|
-
ctx.language === "en" ? `Updating FET from ${currentVersion} to ${latestVersion}...` : `\u6B63\u5728\u5C06 FET \u4ECE ${currentVersion} \u5347\u7EA7\u5230 ${latestVersion}...`
|
|
2706
|
-
);
|
|
2707
|
-
}
|
|
2708
|
-
const installArgs = ["install", "-g", `${packageName}@latest`];
|
|
2709
|
-
const result = await runNpm(npmExecutable, installArgs, {
|
|
2710
|
-
cwd: ctx.cwd,
|
|
2711
|
-
stdio: ctx.json ? "pipe" : "inherit"
|
|
2712
|
-
});
|
|
2713
|
-
if (result.exitCode !== 0) {
|
|
2714
|
-
throw new FetError({
|
|
2715
|
-
code: "UPDATE_FAILED" /* UpdateFailed */,
|
|
2716
|
-
message: ctx.language === "en" ? "FET update failed." : "FET \u5347\u7EA7\u5931\u8D25\u3002",
|
|
2717
|
-
details: result,
|
|
2718
|
-
suggestedCommand: `${npmExecutable} ${installArgs.join(" ")}`
|
|
2719
|
-
});
|
|
2720
|
-
}
|
|
2721
|
-
ctx.output.result({
|
|
2722
|
-
ok: true,
|
|
2723
|
-
command: "update",
|
|
2724
|
-
summary: ctx.language === "en" ? `FET updated from ${currentVersion} to ${latestVersion}.` : `FET \u5DF2\u4ECE ${currentVersion} \u5347\u7EA7\u5230 ${latestVersion}\u3002`,
|
|
2725
|
-
data: {
|
|
2726
|
-
packageName,
|
|
2727
|
-
currentVersion,
|
|
2728
|
-
latestVersion,
|
|
2729
|
-
updated: true,
|
|
2730
|
-
installCommand: `${npmExecutable} ${installArgs.join(" ")}`
|
|
2731
|
-
}
|
|
2732
|
-
});
|
|
2684
|
+
var DEFAULT_FET_PACKAGE_NAME = "@nick848/fet";
|
|
2685
|
+
function getFetPackageName(env = process.env) {
|
|
2686
|
+
return env.FET_UPDATE_PACKAGE_NAME?.trim() || DEFAULT_FET_PACKAGE_NAME;
|
|
2733
2687
|
}
|
|
2734
|
-
|
|
2688
|
+
function getNpmExecutable(env = process.env) {
|
|
2689
|
+
return env.FET_UPDATE_NPM_EXECUTABLE?.trim() || (process.platform === "win32" ? "npm.cmd" : "npm");
|
|
2690
|
+
}
|
|
2691
|
+
async function resolveLatestFetVersion(packageName = getFetPackageName(), npmExecutable = getNpmExecutable()) {
|
|
2735
2692
|
const override = process.env.FET_UPDATE_LATEST_VERSION?.trim();
|
|
2736
2693
|
if (override) {
|
|
2737
2694
|
return override;
|
|
@@ -2758,6 +2715,32 @@ async function resolveLatestVersion(packageName, npmExecutable) {
|
|
|
2758
2715
|
}
|
|
2759
2716
|
return version;
|
|
2760
2717
|
}
|
|
2718
|
+
async function performFetUpdate(currentVersion, latestVersion, options) {
|
|
2719
|
+
const packageName = getFetPackageName();
|
|
2720
|
+
const npmExecutable = getNpmExecutable();
|
|
2721
|
+
const installArgs = ["install", "-g", `${packageName}@latest`];
|
|
2722
|
+
if (!options.json) {
|
|
2723
|
+
options.info(
|
|
2724
|
+
options.language === "en" ? `Updating FET from ${currentVersion} to ${latestVersion}...` : `\u6B63\u5728\u5C06 FET \u4ECE ${currentVersion} \u5347\u7EA7\u5230 ${latestVersion}...`
|
|
2725
|
+
);
|
|
2726
|
+
}
|
|
2727
|
+
const result = await runNpm(npmExecutable, installArgs, {
|
|
2728
|
+
cwd: options.cwd,
|
|
2729
|
+
stdio: options.json ? "pipe" : "inherit"
|
|
2730
|
+
});
|
|
2731
|
+
if (result.exitCode !== 0) {
|
|
2732
|
+
throw new FetError({
|
|
2733
|
+
code: "UPDATE_FAILED" /* UpdateFailed */,
|
|
2734
|
+
message: options.language === "en" ? "FET update failed." : "FET \u5347\u7EA7\u5931\u8D25\u3002",
|
|
2735
|
+
details: result,
|
|
2736
|
+
suggestedCommand: `${npmExecutable} ${installArgs.join(" ")}`
|
|
2737
|
+
});
|
|
2738
|
+
}
|
|
2739
|
+
return {
|
|
2740
|
+
packageName,
|
|
2741
|
+
installCommand: `${npmExecutable} ${installArgs.join(" ")}`
|
|
2742
|
+
};
|
|
2743
|
+
}
|
|
2761
2744
|
function parseNpmVersion(stdout) {
|
|
2762
2745
|
const trimmed = stdout.trim();
|
|
2763
2746
|
if (!trimmed) {
|
|
@@ -2804,9 +2787,6 @@ function runNpm(command, args, options) {
|
|
|
2804
2787
|
});
|
|
2805
2788
|
});
|
|
2806
2789
|
}
|
|
2807
|
-
function defaultNpmExecutable() {
|
|
2808
|
-
return process.platform === "win32" ? "npm.cmd" : "npm";
|
|
2809
|
-
}
|
|
2810
2790
|
function compareVersions(left, right) {
|
|
2811
2791
|
const leftVersion = parseVersion(left);
|
|
2812
2792
|
const rightVersion = parseVersion(right);
|
|
@@ -2867,6 +2847,45 @@ function comparePrereleasePart(left, right) {
|
|
|
2867
2847
|
return left.localeCompare(right);
|
|
2868
2848
|
}
|
|
2869
2849
|
|
|
2850
|
+
// src/commands/update.ts
|
|
2851
|
+
async function updateCommand(ctx) {
|
|
2852
|
+
const packageName = getFetPackageName();
|
|
2853
|
+
const latestVersion = await resolveLatestFetVersion(packageName);
|
|
2854
|
+
const currentVersion = ctx.fetVersion;
|
|
2855
|
+
if (compareVersions(currentVersion, latestVersion) >= 0) {
|
|
2856
|
+
ctx.output.result({
|
|
2857
|
+
ok: true,
|
|
2858
|
+
command: "update",
|
|
2859
|
+
summary: ctx.language === "en" ? `FET is already up to date (${currentVersion}).` : `FET \u5DF2\u662F\u6700\u65B0\u7248 (${currentVersion})\u3002`,
|
|
2860
|
+
data: {
|
|
2861
|
+
packageName,
|
|
2862
|
+
currentVersion,
|
|
2863
|
+
latestVersion,
|
|
2864
|
+
updated: false
|
|
2865
|
+
}
|
|
2866
|
+
});
|
|
2867
|
+
return;
|
|
2868
|
+
}
|
|
2869
|
+
const install = await performFetUpdate(currentVersion, latestVersion, {
|
|
2870
|
+
cwd: ctx.cwd,
|
|
2871
|
+
json: ctx.json,
|
|
2872
|
+
language: ctx.language,
|
|
2873
|
+
info: (message) => ctx.output.info(message)
|
|
2874
|
+
});
|
|
2875
|
+
ctx.output.result({
|
|
2876
|
+
ok: true,
|
|
2877
|
+
command: "update",
|
|
2878
|
+
summary: ctx.language === "en" ? `FET updated from ${currentVersion} to ${latestVersion}.` : `FET \u5DF2\u4ECE ${currentVersion} \u5347\u7EA7\u5230 ${latestVersion}\u3002`,
|
|
2879
|
+
data: {
|
|
2880
|
+
packageName,
|
|
2881
|
+
currentVersion,
|
|
2882
|
+
latestVersion,
|
|
2883
|
+
updated: true,
|
|
2884
|
+
installCommand: install.installCommand
|
|
2885
|
+
}
|
|
2886
|
+
});
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2870
2889
|
// src/commands/verify.ts
|
|
2871
2890
|
import { createHash } from "crypto";
|
|
2872
2891
|
import { mkdir as mkdir7, readFile as readFile12, stat as stat5 } from "fs/promises";
|
|
@@ -3442,12 +3461,15 @@ After the command completes, report the GitNexus state, generated handoff files,
|
|
|
3442
3461
|
`;
|
|
3443
3462
|
}
|
|
3444
3463
|
function renderSlashPrompt(command, language) {
|
|
3445
|
-
if (command === "ff"
|
|
3446
|
-
return renderFastForwardSlashPrompt(
|
|
3464
|
+
if (command === "ff") {
|
|
3465
|
+
return renderFastForwardSlashPrompt(language);
|
|
3447
3466
|
}
|
|
3448
3467
|
if (language !== "en") {
|
|
3449
3468
|
return renderSlashPromptZh(command);
|
|
3450
3469
|
}
|
|
3470
|
+
if (command === "propose") {
|
|
3471
|
+
return renderProposeSlashPrompt(language);
|
|
3472
|
+
}
|
|
3451
3473
|
if (command === "continue") {
|
|
3452
3474
|
return renderContinueSlashPrompt(language);
|
|
3453
3475
|
}
|
|
@@ -3561,12 +3583,12 @@ ${commandGoalZh(command)}
|
|
|
3561
3583
|
- \u9ED8\u8BA4\u4F7F\u7528\u4E2D\u6587\u4EA7\u51FA\u3002
|
|
3562
3584
|
- \u4E0D\u8981\u7ED5\u8FC7 FET \u76F4\u63A5\u8C03\u7528 openspec\uFF0C\u9664\u975E FET \u547D\u4EE4\u672C\u8EAB\u4E0D\u53EF\u7528\u3002
|
|
3563
3585
|
- change \u4E0D\u660E\u786E\u65F6\u5148\u8BE2\u95EE\u7528\u6237\u3002
|
|
3564
|
-
${command === "fill-context" ? "- \u66FF\u6362 AGENTS.md \u4E2D\u6BCF\u4E2A `[NEEDS LLM INPUT]` \u6216 `[NEED LLM INPUT]` \u5360\u4F4D\u7B26\uFF0C\u4FDD\u7559 FET \u6258\u7BA1\u6807\u8BB0\uFF0C\u4E0D\u8981\u4FEE\u6539\u4E1A\u52A1\u4EE3\u7801\u3002\n" : ""}${command === "continue" ? "- \u4E00\u6B21\u53EA\u521B\u5EFA\u4E00\u4E2A ready artifact\uFF0C\u5E76\u5728\u5199\u5165\u524D\u9605\u8BFB\u4F9D\u8D56\u6587\u4EF6\u3002\n" : ""}${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" : ""}`;
|
|
3586
|
+
${command === "fill-context" ? "- \u66FF\u6362 AGENTS.md \u4E2D\u6BCF\u4E2A `[NEEDS LLM INPUT]` \u6216 `[NEED LLM INPUT]` \u5360\u4F4D\u7B26\uFF0C\u4FDD\u7559 FET \u6258\u7BA1\u6807\u8BB0\uFF0C\u4E0D\u8981\u4FEE\u6539\u4E1A\u52A1\u4EE3\u7801\u3002\n" : ""}${command === "propose" || command === "continue" ? "- \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" ? "- \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 === "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" : ""}`;
|
|
3565
3587
|
}
|
|
3566
3588
|
function commandTitleZh(command) {
|
|
3567
3589
|
const titles = {
|
|
3568
3590
|
explore: "\u63A2\u7D22 FET/OpenSpec \u9700\u6C42",
|
|
3569
|
-
propose: "\u521B\u5EFA
|
|
3591
|
+
propose: "\u521B\u5EFA FET/OpenSpec change \u5E76\u751F\u6210\u9996\u4E2A\u89C4\u5212\u4EA7\u7269",
|
|
3570
3592
|
new: "\u521B\u5EFA\u65B0\u7684 FET/OpenSpec change \u9AA8\u67B6",
|
|
3571
3593
|
continue: "\u63A8\u8FDB\u5F53\u524D FET/OpenSpec change \u7684\u4E0B\u4E00\u4E2A\u4EA7\u7269",
|
|
3572
3594
|
ff: "\u5FEB\u901F\u751F\u6210 FET/OpenSpec \u6240\u9700\u4EA7\u7269",
|
|
@@ -3591,9 +3613,15 @@ function commandGoalZh(command) {
|
|
|
3591
3613
|
if (command === "fill-context") {
|
|
3592
3614
|
return "\u8865\u9F50 FET \u81EA\u52A8\u751F\u6210\u7684\u9879\u76EE\u4E0A\u4E0B\u6587\uFF0C\u8BA9\u540E\u7EED AI \u7F16\u7801\u548C OpenSpec \u5DE5\u4F5C\u6D41\u62E5\u6709\u7A33\u5B9A\u7684\u9879\u76EE\u4E8B\u5B9E\u3002";
|
|
3593
3615
|
}
|
|
3616
|
+
if (command === "propose") {
|
|
3617
|
+
return "\u6839\u636E change id \u6216\u63CF\u8FF0\u521B\u5EFA OpenSpec change\uFF08\u5982\u4E0D\u5B58\u5728\uFF09\uFF0C\u5E76\u53EA\u751F\u6210\u5F53\u524D\u7B2C\u4E00\u4E2A ready \u7684\u89C4\u5212\u4EA7\u7269\u3002";
|
|
3618
|
+
}
|
|
3594
3619
|
if (command === "continue") {
|
|
3595
3620
|
return "\u57FA\u4E8E active change \u6216\u7528\u6237\u6307\u5B9A\u7684 change\uFF0C\u521B\u5EFA\u4E0B\u4E00\u4E2A ready \u7684 OpenSpec \u89C4\u5212\u4EA7\u7269\u3002";
|
|
3596
3621
|
}
|
|
3622
|
+
if (command === "ff") {
|
|
3623
|
+
return "\u5728\u7528\u6237\u660E\u786E\u8981\u6C42\u5FEB\u8FDB\u65F6\uFF0C\u4E00\u6B21\u6027\u751F\u6210 change \u6240\u9700\u7684\u5168\u90E8\u89C4\u5212\u4EA7\u7269\u3002";
|
|
3624
|
+
}
|
|
3597
3625
|
if (command === "apply") {
|
|
3598
3626
|
return "\u8BFB\u53D6 OpenSpec \u4EA7\u7269\u5E76\u6309 tasks.md \u5B9E\u65BD\u4EE3\u7801\u53D8\u66F4\u3002";
|
|
3599
3627
|
}
|
|
@@ -4007,6 +4035,45 @@ Guardrails:
|
|
|
4007
4035
|
language
|
|
4008
4036
|
);
|
|
4009
4037
|
}
|
|
4038
|
+
function renderProposeSlashPrompt(language) {
|
|
4039
|
+
return renderManagedSlashPrompt(
|
|
4040
|
+
"fet propose [...args]",
|
|
4041
|
+
"Create a FET/OpenSpec change and its first ready artifact",
|
|
4042
|
+
`Start proposal planning for a FET-managed OpenSpec change by creating exactly one ready artifact.
|
|
4043
|
+
|
|
4044
|
+
Input after the slash command may be a change id or a short description of what the user wants to build. If omitted, ask the user.
|
|
4045
|
+
|
|
4046
|
+
Steps:
|
|
4047
|
+
|
|
4048
|
+
1. Load project context from AGENTS.md and openspec/config.yaml.
|
|
4049
|
+
2. Derive a kebab-case change id when the user provided a description. If ambiguous, ask the user.
|
|
4050
|
+
3. Run the native OpenSpec propose flow through FET. This creates the change when needed and returns instructions for only the next ready artifact:
|
|
4051
|
+
\`\`\`sh
|
|
4052
|
+
fet propose <change-id-or-description> --json
|
|
4053
|
+
\`\`\`
|
|
4054
|
+
4. Follow the native output. When it provides template, instruction, dependencies, and outputPath, use those fields.
|
|
4055
|
+
5. Read dependency files before writing.
|
|
4056
|
+
6. Create only that artifact file at outputPath. Do not copy context/rules wrapper text into the artifact.
|
|
4057
|
+
7. Verify the file exists, then run:
|
|
4058
|
+
\`\`\`sh
|
|
4059
|
+
fet passthrough status --change <change-id>
|
|
4060
|
+
\`\`\`
|
|
4061
|
+
|
|
4062
|
+
Output:
|
|
4063
|
+
- Change id and location.
|
|
4064
|
+
- Which single artifact was created and its file path.
|
|
4065
|
+
- Current status.
|
|
4066
|
+
- Tell the user to review the artifact and run /prompts:fet-continue <change-id> when ready for the next artifact. Mention /prompts:fet-ff <change-id> only if they explicitly want to fast-forward all remaining artifacts.
|
|
4067
|
+
|
|
4068
|
+
Guardrails:
|
|
4069
|
+
- Create one artifact per invocation. Do not run fet continue or fet ff in the same session unless the user explicitly asks after reviewing the artifact just created.
|
|
4070
|
+
- Never skip dependency order.
|
|
4071
|
+
- Do not substitute one FET/OpenSpec workflow command for another after a command-not-found or unsupported-version error.
|
|
4072
|
+
- Ask the user if instructions are ambiguous enough that a useful artifact cannot be written.`,
|
|
4073
|
+
void 0,
|
|
4074
|
+
language
|
|
4075
|
+
);
|
|
4076
|
+
}
|
|
4010
4077
|
function renderContinueSlashPrompt(language) {
|
|
4011
4078
|
return renderManagedSlashPrompt(
|
|
4012
4079
|
"fet continue [...args]",
|
|
@@ -4049,27 +4116,27 @@ Guardrails:
|
|
|
4049
4116
|
language
|
|
4050
4117
|
);
|
|
4051
4118
|
}
|
|
4052
|
-
function renderFastForwardSlashPrompt(
|
|
4053
|
-
const title = language === "en" ? command === "propose" ? "Propose a new FET/OpenSpec change" : "Fast-forward FET/OpenSpec artifact creation" : command === "propose" ? "\u521B\u5EFA\u5E76\u8865\u9F50 FET/OpenSpec \u63D0\u6848\u4EA7\u7269" : "\u5FEB\u901F\u751F\u6210 FET/OpenSpec \u6240\u9700\u4EA7\u7269";
|
|
4054
|
-
const commandLine = command === "propose" ? "fet propose <change-id-or-description>" : "fet ff --change <change-id>";
|
|
4119
|
+
function renderFastForwardSlashPrompt(language) {
|
|
4055
4120
|
return renderManagedSlashPrompt(
|
|
4056
|
-
|
|
4057
|
-
language === "en" ?
|
|
4058
|
-
`${
|
|
4121
|
+
"fet ff [...args]",
|
|
4122
|
+
language === "en" ? "Fast-forward FET/OpenSpec artifact creation" : "\u5FEB\u901F\u751F\u6210 FET/OpenSpec \u6240\u9700\u4EA7\u7269",
|
|
4123
|
+
`${language === "en" ? "Fast-forward FET/OpenSpec artifact creation" : "\u5FEB\u901F\u751F\u6210 FET/OpenSpec \u6240\u9700\u4EA7\u7269"}.
|
|
4059
4124
|
|
|
4060
|
-
|
|
4125
|
+
Use this command only when the user explicitly wants to generate all remaining planning artifacts in one session without reviewing each artifact between steps. For step-by-step review, use /prompts:fet-propose or /prompts:fet-continue instead.
|
|
4126
|
+
|
|
4127
|
+
Input after the slash command may be a change id. It may be omitted when the active OpenSpec change is unambiguous.
|
|
4061
4128
|
|
|
4062
4129
|
Steps:
|
|
4063
4130
|
|
|
4064
4131
|
1. Load project context from AGENTS.md and openspec/config.yaml.
|
|
4065
|
-
2. Resolve the change id
|
|
4066
|
-
3. Run the native OpenSpec
|
|
4132
|
+
2. Resolve the change id. If ambiguous, ask the user.
|
|
4133
|
+
3. Run the native OpenSpec ff flow through FET:
|
|
4067
4134
|
\`\`\`sh
|
|
4068
|
-
|
|
4135
|
+
fet ff --change <change-id>
|
|
4069
4136
|
\`\`\`
|
|
4070
4137
|
4. Follow the native output. If it asks for clarification, ask the user rather than inventing details.
|
|
4071
|
-
5. If the native output includes artifact paths or templates to write, create
|
|
4072
|
-
6. If FET reports that the OpenSpec CLI does not expose the requested command, stop immediately. Do not run \`fet
|
|
4138
|
+
5. If the native output includes artifact paths or templates to write, create those files and preserve OpenSpec structure.
|
|
4139
|
+
6. If FET reports that the OpenSpec CLI does not expose the requested command, stop immediately. Do not run \`fet continue\`, \`openspec change\`, or any alternative workflow command unless the user explicitly chooses that fallback after seeing the error.
|
|
4073
4140
|
|
|
4074
4141
|
Artifact rules:
|
|
4075
4142
|
- Follow the instruction field from OpenSpec/FET for each artifact.
|
|
@@ -4974,6 +5041,173 @@ async function createCommandContext(command, options) {
|
|
|
4974
5041
|
};
|
|
4975
5042
|
}
|
|
4976
5043
|
|
|
5044
|
+
// src/cli/update-check.ts
|
|
5045
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
5046
|
+
|
|
5047
|
+
// src/update/check.ts
|
|
5048
|
+
import { mkdir as mkdir10, readFile as readFile16, writeFile } from "fs/promises";
|
|
5049
|
+
import { homedir as homedir2 } from "os";
|
|
5050
|
+
import { dirname as dirname10, join as join21 } from "path";
|
|
5051
|
+
var DEFAULT_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
5052
|
+
function getFetUpdateCheckMode(env = process.env) {
|
|
5053
|
+
const value = env.FET_UPDATE_CHECK?.trim().toLowerCase();
|
|
5054
|
+
if (value === "off" || env.FET_SKIP_UPDATE_CHECK === "1") {
|
|
5055
|
+
return "off";
|
|
5056
|
+
}
|
|
5057
|
+
if (value === "warn") {
|
|
5058
|
+
return "warn";
|
|
5059
|
+
}
|
|
5060
|
+
if (value === "confirm") {
|
|
5061
|
+
return "confirm";
|
|
5062
|
+
}
|
|
5063
|
+
return "confirm";
|
|
5064
|
+
}
|
|
5065
|
+
function shouldCheckFetUpdateForCommand(command) {
|
|
5066
|
+
return command !== "update";
|
|
5067
|
+
}
|
|
5068
|
+
async function resolveFetUpdateAvailability(currentVersion) {
|
|
5069
|
+
const packageName = getFetPackageName();
|
|
5070
|
+
const cache = await readUpdateCheckCache();
|
|
5071
|
+
const ttlMs = Number.parseInt(process.env.FET_UPDATE_CHECK_TTL_MS ?? "", 10);
|
|
5072
|
+
const cacheTtlMs = Number.isFinite(ttlMs) && ttlMs > 0 ? ttlMs : DEFAULT_CACHE_TTL_MS;
|
|
5073
|
+
let latestVersion = cache?.latestVersion;
|
|
5074
|
+
const cacheFresh = cache?.checkedAt ? Date.now() - Date.parse(cache.checkedAt) < cacheTtlMs : false;
|
|
5075
|
+
if (!latestVersion || !cacheFresh) {
|
|
5076
|
+
try {
|
|
5077
|
+
latestVersion = await resolveLatestFetVersion(packageName);
|
|
5078
|
+
await writeUpdateCheckCache({
|
|
5079
|
+
latestVersion,
|
|
5080
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5081
|
+
dismissedLatestVersion: cache?.dismissedLatestVersion
|
|
5082
|
+
});
|
|
5083
|
+
} catch {
|
|
5084
|
+
if (cache?.latestVersion) {
|
|
5085
|
+
latestVersion = cache.latestVersion;
|
|
5086
|
+
} else {
|
|
5087
|
+
return null;
|
|
5088
|
+
}
|
|
5089
|
+
}
|
|
5090
|
+
}
|
|
5091
|
+
if (compareVersions(currentVersion, latestVersion) >= 0) {
|
|
5092
|
+
return null;
|
|
5093
|
+
}
|
|
5094
|
+
if (cache?.dismissedLatestVersion === latestVersion) {
|
|
5095
|
+
return null;
|
|
5096
|
+
}
|
|
5097
|
+
return {
|
|
5098
|
+
packageName,
|
|
5099
|
+
currentVersion,
|
|
5100
|
+
latestVersion
|
|
5101
|
+
};
|
|
5102
|
+
}
|
|
5103
|
+
async function rememberDismissedFetUpdate(latestVersion) {
|
|
5104
|
+
const cache = await readUpdateCheckCache() ?? {
|
|
5105
|
+
latestVersion,
|
|
5106
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5107
|
+
};
|
|
5108
|
+
await writeUpdateCheckCache({
|
|
5109
|
+
...cache,
|
|
5110
|
+
latestVersion,
|
|
5111
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5112
|
+
dismissedLatestVersion: latestVersion
|
|
5113
|
+
});
|
|
5114
|
+
}
|
|
5115
|
+
function formatFetUpdateWarning(availability, language) {
|
|
5116
|
+
if (language === "en") {
|
|
5117
|
+
return `A newer FET release is available (${availability.currentVersion} -> ${availability.latestVersion}). Run \`fet update\` or choose update when prompted.`;
|
|
5118
|
+
}
|
|
5119
|
+
return `\u68C0\u6D4B\u5230 FET \u6709\u65B0\u7248\u672C\uFF08\u5F53\u524D ${availability.currentVersion}\uFF0C\u6700\u65B0 ${availability.latestVersion}\uFF09\u3002\u53EF\u8FD0\u884C \`fet update\`\uFF0C\u6216\u5728\u63D0\u793A\u65F6\u9009\u62E9\u5347\u7EA7\u3002`;
|
|
5120
|
+
}
|
|
5121
|
+
function cachePath() {
|
|
5122
|
+
const home = process.env.FET_UPDATE_CHECK_CACHE_HOME?.trim() || homedir2();
|
|
5123
|
+
return join21(home, ".fet", "update-check-cache.json");
|
|
5124
|
+
}
|
|
5125
|
+
async function readUpdateCheckCache() {
|
|
5126
|
+
try {
|
|
5127
|
+
const raw = await readFile16(cachePath(), "utf8");
|
|
5128
|
+
const parsed = JSON.parse(raw);
|
|
5129
|
+
if (typeof parsed.latestVersion !== "string" || typeof parsed.checkedAt !== "string") {
|
|
5130
|
+
return null;
|
|
5131
|
+
}
|
|
5132
|
+
return {
|
|
5133
|
+
latestVersion: parsed.latestVersion,
|
|
5134
|
+
checkedAt: parsed.checkedAt,
|
|
5135
|
+
dismissedLatestVersion: typeof parsed.dismissedLatestVersion === "string" ? parsed.dismissedLatestVersion : void 0
|
|
5136
|
+
};
|
|
5137
|
+
} catch {
|
|
5138
|
+
return null;
|
|
5139
|
+
}
|
|
5140
|
+
}
|
|
5141
|
+
async function writeUpdateCheckCache(cache) {
|
|
5142
|
+
const path = cachePath();
|
|
5143
|
+
await mkdir10(dirname10(path), { recursive: true });
|
|
5144
|
+
await writeFile(path, `${JSON.stringify(cache, null, 2)}
|
|
5145
|
+
`, "utf8");
|
|
5146
|
+
}
|
|
5147
|
+
|
|
5148
|
+
// src/cli/update-check.ts
|
|
5149
|
+
async function handleFetUpdateCheck(ctx) {
|
|
5150
|
+
if (!shouldCheckFetUpdateForCommand(ctx.command)) {
|
|
5151
|
+
return;
|
|
5152
|
+
}
|
|
5153
|
+
const mode = getFetUpdateCheckMode();
|
|
5154
|
+
if (mode === "off") {
|
|
5155
|
+
return;
|
|
5156
|
+
}
|
|
5157
|
+
const availability = await resolveFetUpdateAvailability(ctx.fetVersion);
|
|
5158
|
+
if (!availability) {
|
|
5159
|
+
return;
|
|
5160
|
+
}
|
|
5161
|
+
const warning = formatFetUpdateWarning(availability, ctx.language);
|
|
5162
|
+
ctx.output.warn(warning);
|
|
5163
|
+
const forceConfirm = process.env.FET_UPDATE_CHECK_FORCE_CONFIRM === "1";
|
|
5164
|
+
const interactive = mode === "confirm" && !ctx.json && !ctx.yes && (forceConfirm || process.stdin.isTTY && process.stderr.isTTY);
|
|
5165
|
+
if (!interactive) {
|
|
5166
|
+
return;
|
|
5167
|
+
}
|
|
5168
|
+
const choice = await promptFetUpdateChoice(availability, ctx.language);
|
|
5169
|
+
if (choice === "skip") {
|
|
5170
|
+
await rememberDismissedFetUpdate(availability.latestVersion);
|
|
5171
|
+
return;
|
|
5172
|
+
}
|
|
5173
|
+
if (choice === "cancel") {
|
|
5174
|
+
throw new FetError({
|
|
5175
|
+
code: "USER_CANCELLED" /* UserCancelled */,
|
|
5176
|
+
message: ctx.language === "en" ? "Command cancelled before FET update." : "\u547D\u4EE4\u5DF2\u53D6\u6D88\uFF0C\u672A\u6267\u884C FET \u5347\u7EA7\u3002",
|
|
5177
|
+
details: availability,
|
|
5178
|
+
suggestedCommand: ctx.language === "en" ? `fet update` : "fet update"
|
|
5179
|
+
});
|
|
5180
|
+
}
|
|
5181
|
+
await performFetUpdate(availability.currentVersion, availability.latestVersion, {
|
|
5182
|
+
cwd: ctx.cwd,
|
|
5183
|
+
json: ctx.json,
|
|
5184
|
+
language: ctx.language,
|
|
5185
|
+
info: (message) => ctx.output.info(message)
|
|
5186
|
+
});
|
|
5187
|
+
throw new FetError({
|
|
5188
|
+
code: "USER_CANCELLED" /* UserCancelled */,
|
|
5189
|
+
message: ctx.language === "en" ? `FET updated to ${availability.latestVersion}. Rerun this command to use the new version.` : `FET \u5DF2\u5347\u7EA7\u5230 ${availability.latestVersion}\u3002\u8BF7\u91CD\u65B0\u8FD0\u884C\u5F53\u524D\u547D\u4EE4\u4EE5\u4F7F\u7528\u65B0\u7248\u672C\u3002`,
|
|
5190
|
+
details: availability,
|
|
5191
|
+
suggestedCommand: `fet ${ctx.command}`
|
|
5192
|
+
});
|
|
5193
|
+
}
|
|
5194
|
+
async function promptFetUpdateChoice(availability, language) {
|
|
5195
|
+
const rl = createInterface2({ input: process.stdin, output: process.stderr });
|
|
5196
|
+
try {
|
|
5197
|
+
const question = language === "en" ? `Update FET now (${availability.currentVersion} -> ${availability.latestVersion})? [U]pdate / [S]kip / [C]ancel [s]: ` : `\u662F\u5426\u73B0\u5728\u5347\u7EA7 FET\uFF08${availability.currentVersion} -> ${availability.latestVersion}\uFF09\uFF1F[U]\u5347\u7EA7 / [S]\u8DF3\u8FC7 / [C]\u53D6\u6D88 [s]: `;
|
|
5198
|
+
const answer = (await rl.question(question)).trim().toLowerCase();
|
|
5199
|
+
if (answer === "u" || answer === "update" || answer === "\u5347\u7EA7") {
|
|
5200
|
+
return "update";
|
|
5201
|
+
}
|
|
5202
|
+
if (answer === "c" || answer === "cancel" || answer === "\u53D6\u6D88") {
|
|
5203
|
+
return "cancel";
|
|
5204
|
+
}
|
|
5205
|
+
return "skip";
|
|
5206
|
+
} finally {
|
|
5207
|
+
rl.close();
|
|
5208
|
+
}
|
|
5209
|
+
}
|
|
5210
|
+
|
|
4977
5211
|
// src/cli/index.ts
|
|
4978
5212
|
var program = new Command();
|
|
4979
5213
|
program.name("fet").description("\u56F4\u7ED5 OpenSpec \u7684\u524D\u7AEF\u5F00\u53D1\u5DE5\u4F5C\u6D41\u7F16\u6392\u5DE5\u5177\u3002").enablePositionalOptions().version(FET_VERSION).option("--cwd <path>", "\u6307\u5B9A\u9879\u76EE\u6839\u76EE\u5F55").option("--change <id>", "\u6307\u5B9A OpenSpec change").option("--lang <language>", "\u6307\u5B9A FET \u4EA4\u4E92\u4FE1\u606F\u548C\u751F\u6210\u4EA7\u7269\u8BED\u8A00\uFF0C\u9ED8\u8BA4 zh-CN").option("--yes", "\u5BF9\u4F4E\u98CE\u9669\u786E\u8BA4\u4F7F\u7528\u9ED8\u8BA4\u540C\u610F").option("--json", "\u8F93\u51FA\u673A\u5668\u53EF\u8BFB JSON").option("--verbose", "\u8F93\u51FA\u8BCA\u65AD\u7EC6\u8282").option("--no-color", "\u7981\u7528\u7EC8\u7AEF\u989C\u8272");
|
|
@@ -5017,6 +5251,7 @@ function wrap(command, handler) {
|
|
|
5017
5251
|
const opts = isCommandLike(maybeCommand) ? { ...maybeCommand.parent?.opts(), ...maybeCommand.opts() } : program.opts();
|
|
5018
5252
|
const ctx = await createCommandContext(command, { ...opts, ...extractGlobalOptions(args) });
|
|
5019
5253
|
try {
|
|
5254
|
+
await handleFetUpdateCheck(ctx);
|
|
5020
5255
|
await handleModelPolicyRecommendation(ctx);
|
|
5021
5256
|
await warnIfContextPlaceholdersRemain(ctx);
|
|
5022
5257
|
await handler(ctx, ...args);
|
|
@@ -5038,7 +5273,7 @@ async function handleModelPolicyRecommendation(ctx) {
|
|
|
5038
5273
|
if (policyMode !== "confirm" || ctx.yes || ctx.json || !process.stdin.isTTY || !process.stderr.isTTY) {
|
|
5039
5274
|
return;
|
|
5040
5275
|
}
|
|
5041
|
-
const rl =
|
|
5276
|
+
const rl = createInterface3({ input: process.stdin, output: process.stderr });
|
|
5042
5277
|
try {
|
|
5043
5278
|
const question = ctx.language === "en" ? "Continue anyway? [y/N] " : "\u4ECD\u7136\u7EE7\u7EED\uFF1F[y/N] ";
|
|
5044
5279
|
const answer = (await rl.question(question)).trim().toLowerCase();
|