@nick848/fet 1.1.4 → 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 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
  | 命令 | 用法 | 说明 |
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 |
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 createInterface2 } from "readline/promises";
8
+ import { createInterface as createInterface3 } from "readline/promises";
9
9
  import { Command } from "commander";
10
10
 
11
11
  // src/commands/doctor.ts
@@ -2679,60 +2679,16 @@ async function assertVerifiedChange(ctx, changeId) {
2679
2679
  }
2680
2680
  }
2681
2681
 
2682
- // src/commands/update.ts
2682
+ // src/update/npm.ts
2683
2683
  import { spawn } from "child_process";
2684
- var DEFAULT_PACKAGE_NAME = "@nick848/fet";
2685
- async function updateCommand(ctx) {
2686
- const packageName = process.env.FET_UPDATE_PACKAGE_NAME ?? DEFAULT_PACKAGE_NAME;
2687
- const npmExecutable = process.env.FET_UPDATE_NPM_EXECUTABLE ?? defaultNpmExecutable();
2688
- const latestVersion = await resolveLatestVersion(packageName, npmExecutable);
2689
- const currentVersion = ctx.fetVersion;
2690
- if (compareVersions(currentVersion, latestVersion) >= 0) {
2691
- ctx.output.result({
2692
- ok: true,
2693
- command: "update",
2694
- summary: ctx.language === "en" ? `FET is already up to date (${currentVersion}).` : `FET \u5DF2\u662F\u6700\u65B0\u7248 (${currentVersion})\u3002`,
2695
- data: {
2696
- packageName,
2697
- currentVersion,
2698
- latestVersion,
2699
- updated: false
2700
- }
2701
- });
2702
- return;
2703
- }
2704
- if (!ctx.json) {
2705
- ctx.output.info(
2706
- ctx.language === "en" ? `Updating FET from ${currentVersion} to ${latestVersion}...` : `\u6B63\u5728\u5C06 FET \u4ECE ${currentVersion} \u5347\u7EA7\u5230 ${latestVersion}...`
2707
- );
2708
- }
2709
- const installArgs = ["install", "-g", `${packageName}@latest`];
2710
- const result = await runNpm(npmExecutable, installArgs, {
2711
- cwd: ctx.cwd,
2712
- stdio: ctx.json ? "pipe" : "inherit"
2713
- });
2714
- if (result.exitCode !== 0) {
2715
- throw new FetError({
2716
- code: "UPDATE_FAILED" /* UpdateFailed */,
2717
- message: ctx.language === "en" ? "FET update failed." : "FET \u5347\u7EA7\u5931\u8D25\u3002",
2718
- details: result,
2719
- suggestedCommand: `${npmExecutable} ${installArgs.join(" ")}`
2720
- });
2721
- }
2722
- ctx.output.result({
2723
- ok: true,
2724
- command: "update",
2725
- summary: ctx.language === "en" ? `FET updated from ${currentVersion} to ${latestVersion}.` : `FET \u5DF2\u4ECE ${currentVersion} \u5347\u7EA7\u5230 ${latestVersion}\u3002`,
2726
- data: {
2727
- packageName,
2728
- currentVersion,
2729
- latestVersion,
2730
- updated: true,
2731
- installCommand: `${npmExecutable} ${installArgs.join(" ")}`
2732
- }
2733
- });
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;
2687
+ }
2688
+ function getNpmExecutable(env = process.env) {
2689
+ return env.FET_UPDATE_NPM_EXECUTABLE?.trim() || (process.platform === "win32" ? "npm.cmd" : "npm");
2734
2690
  }
2735
- async function resolveLatestVersion(packageName, npmExecutable) {
2691
+ async function resolveLatestFetVersion(packageName = getFetPackageName(), npmExecutable = getNpmExecutable()) {
2736
2692
  const override = process.env.FET_UPDATE_LATEST_VERSION?.trim();
2737
2693
  if (override) {
2738
2694
  return override;
@@ -2759,6 +2715,32 @@ async function resolveLatestVersion(packageName, npmExecutable) {
2759
2715
  }
2760
2716
  return version;
2761
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
+ }
2762
2744
  function parseNpmVersion(stdout) {
2763
2745
  const trimmed = stdout.trim();
2764
2746
  if (!trimmed) {
@@ -2805,9 +2787,6 @@ function runNpm(command, args, options) {
2805
2787
  });
2806
2788
  });
2807
2789
  }
2808
- function defaultNpmExecutable() {
2809
- return process.platform === "win32" ? "npm.cmd" : "npm";
2810
- }
2811
2790
  function compareVersions(left, right) {
2812
2791
  const leftVersion = parseVersion(left);
2813
2792
  const rightVersion = parseVersion(right);
@@ -2868,6 +2847,45 @@ function comparePrereleasePart(left, right) {
2868
2847
  return left.localeCompare(right);
2869
2848
  }
2870
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
+
2871
2889
  // src/commands/verify.ts
2872
2890
  import { createHash } from "crypto";
2873
2891
  import { mkdir as mkdir7, readFile as readFile12, stat as stat5 } from "fs/promises";
@@ -5023,6 +5041,173 @@ async function createCommandContext(command, options) {
5023
5041
  };
5024
5042
  }
5025
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
+
5026
5211
  // src/cli/index.ts
5027
5212
  var program = new Command();
5028
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");
@@ -5066,6 +5251,7 @@ function wrap(command, handler) {
5066
5251
  const opts = isCommandLike(maybeCommand) ? { ...maybeCommand.parent?.opts(), ...maybeCommand.opts() } : program.opts();
5067
5252
  const ctx = await createCommandContext(command, { ...opts, ...extractGlobalOptions(args) });
5068
5253
  try {
5254
+ await handleFetUpdateCheck(ctx);
5069
5255
  await handleModelPolicyRecommendation(ctx);
5070
5256
  await warnIfContextPlaceholdersRemain(ctx);
5071
5257
  await handler(ctx, ...args);
@@ -5087,7 +5273,7 @@ async function handleModelPolicyRecommendation(ctx) {
5087
5273
  if (policyMode !== "confirm" || ctx.yes || ctx.json || !process.stdin.isTTY || !process.stderr.isTTY) {
5088
5274
  return;
5089
5275
  }
5090
- const rl = createInterface2({ input: process.stdin, output: process.stderr });
5276
+ const rl = createInterface3({ input: process.stdin, output: process.stderr });
5091
5277
  try {
5092
5278
  const question = ctx.language === "en" ? "Continue anyway? [y/N] " : "\u4ECD\u7136\u7EE7\u7EED\uFF1F[y/N] ";
5093
5279
  const answer = (await rl.question(question)).trim().toLowerCase();