@nick848/fet 1.0.9 → 1.1.0

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_en.md CHANGED
@@ -42,6 +42,7 @@ Check the installation:
42
42
  ```sh
43
43
  fet --version
44
44
  fet --help
45
+ fet update
45
46
  ```
46
47
 
47
48
  ## Quick Start
@@ -100,6 +101,7 @@ fet init --lang en
100
101
  | Command | Usage | Description |
101
102
  |---------|-------|-------------|
102
103
  | `fet init` | `fet init [--yes] [--lang <language>]` | Initialize FET and OpenSpec; generate context, state, Cursor integration, and Codex workflow guides. |
104
+ | `fet update` | `fet update` | Check whether FET is the latest published version and automatically upgrade when a newer version is available. |
103
105
  | `fet update-context` | `fet update-context [--yes]` | Rescan the project and update FET-managed regions in `AGENTS.md` and `openspec/config.yaml`. |
104
106
  | `fet fill-context` | `fet fill-context [--yes]` | Refresh IDE handoff commands that ask AI to replace `AGENTS.md` placeholders. |
105
107
  | `fet doctor` | `fet doctor [--fix-lock]` | Diagnose OpenSpec, FET state, context files, tool integration, and lock files. |
@@ -15,6 +15,7 @@ var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
15
15
  ErrorCode2["LockHeld"] = "LOCK_HELD";
16
16
  ErrorCode2["UserCancelled"] = "USER_CANCELLED";
17
17
  ErrorCode2["UnsafeScriptApprovalRequired"] = "UNSAFE_SCRIPT_APPROVAL_REQUIRED";
18
+ ErrorCode2["UpdateFailed"] = "UPDATE_FAILED";
18
19
  ErrorCode2["ToolAdapterConflict"] = "TOOL_ADAPTER_CONFLICT";
19
20
  ErrorCode2["ConfigInvalid"] = "CONFIG_INVALID";
20
21
  ErrorCode2["FileSystemError"] = "FILE_SYSTEM_ERROR";
@@ -31,6 +32,7 @@ function exitCodeForError(code) {
31
32
  return 3;
32
33
  case "OPENSPEC_COMMAND_FAILED" /* OpenSpecCommandFailed */:
33
34
  case "GRAPH_COMMAND_FAILED" /* GraphCommandFailed */:
35
+ case "UPDATE_FAILED" /* UpdateFailed */:
34
36
  return 4;
35
37
  case "OPENSPEC_STRUCTURE_UNKNOWN" /* OpenSpecStructureUnknown */:
36
38
  case "STATE_SCHEMA_UNSUPPORTED" /* StateSchemaUnsupported */:
@@ -105,4 +107,4 @@ export {
105
107
  FetError,
106
108
  toFetError
107
109
  };
108
- //# sourceMappingURL=chunk-V4ZRBF5L.js.map
110
+ //# sourceMappingURL=chunk-J5WB4KAL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors/codes.ts","../src/errors/fet-error.ts"],"sourcesContent":["export enum ErrorCode {\n Unknown = \"UNKNOWN\",\n InvalidArguments = \"INVALID_ARGUMENTS\",\n OpenSpecNotFound = \"OPENSPEC_NOT_FOUND\",\n OpenSpecUnsupportedVersion = \"OPENSPEC_UNSUPPORTED_VERSION\",\n OpenSpecCommandFailed = \"OPENSPEC_COMMAND_FAILED\",\n OpenSpecStructureUnknown = \"OPENSPEC_STRUCTURE_UNKNOWN\",\n GraphProviderNotFound = \"GRAPH_PROVIDER_NOT_FOUND\",\n GraphCommandFailed = \"GRAPH_COMMAND_FAILED\",\n StateSchemaUnsupported = \"STATE_SCHEMA_UNSUPPORTED\",\n StateCorrupted = \"STATE_CORRUPTED\",\n LockHeld = \"LOCK_HELD\",\n UserCancelled = \"USER_CANCELLED\",\n UnsafeScriptApprovalRequired = \"UNSAFE_SCRIPT_APPROVAL_REQUIRED\",\n UpdateFailed = \"UPDATE_FAILED\",\n ToolAdapterConflict = \"TOOL_ADAPTER_CONFLICT\",\n ConfigInvalid = \"CONFIG_INVALID\",\n FileSystemError = \"FILE_SYSTEM_ERROR\"\n}\n\nexport function exitCodeForError(code: ErrorCode): number {\n switch (code) {\n case ErrorCode.InvalidArguments:\n case ErrorCode.ConfigInvalid:\n return 2;\n case ErrorCode.OpenSpecNotFound:\n case ErrorCode.OpenSpecUnsupportedVersion:\n case ErrorCode.GraphProviderNotFound:\n return 3;\n case ErrorCode.OpenSpecCommandFailed:\n case ErrorCode.GraphCommandFailed:\n case ErrorCode.UpdateFailed:\n return 4;\n case ErrorCode.OpenSpecStructureUnknown:\n case ErrorCode.StateSchemaUnsupported:\n case ErrorCode.StateCorrupted:\n case ErrorCode.ToolAdapterConflict:\n case ErrorCode.FileSystemError:\n return 5;\n case ErrorCode.LockHeld:\n return 6;\n case ErrorCode.UserCancelled:\n return 7;\n case ErrorCode.UnsafeScriptApprovalRequired:\n return 8;\n case ErrorCode.Unknown:\n default:\n return 1;\n }\n}\n","import { ErrorCode, exitCodeForError } from \"./codes.js\";\n\nexport interface FetErrorOptions {\n code: ErrorCode;\n message: string;\n details?: unknown;\n recoverable?: boolean;\n suggestedCommand?: string;\n cause?: unknown;\n}\n\nexport class FetError extends Error {\n readonly code: ErrorCode;\n readonly exitCode: number;\n readonly details?: unknown;\n readonly recoverable: boolean;\n readonly suggestedCommand?: string;\n override readonly cause?: unknown;\n\n constructor(options: FetErrorOptions) {\n super(options.message);\n this.name = \"FetError\";\n this.code = options.code;\n this.exitCode = exitCodeForError(options.code);\n this.details = options.details;\n this.recoverable = options.recoverable ?? true;\n this.suggestedCommand = options.suggestedCommand;\n this.cause = options.cause;\n }\n\n toJSON() {\n return {\n code: this.code,\n exitCode: this.exitCode,\n message: this.message,\n details: this.details,\n recoverable: this.recoverable,\n suggestedCommand: this.suggestedCommand\n };\n }\n}\n\nexport function toFetError(error: unknown): FetError {\n if (error instanceof FetError) {\n return error;\n }\n\n if (error instanceof Error) {\n return new FetError({\n code: ErrorCode.Unknown,\n message: error.message,\n recoverable: false,\n cause: error\n });\n }\n\n return new FetError({\n code: ErrorCode.Unknown,\n message: \"Unknown error\",\n details: error,\n recoverable: false\n });\n}\n"],"mappings":";;;AAAO,IAAK,YAAL,kBAAKA,eAAL;AACL,EAAAA,WAAA,aAAU;AACV,EAAAA,WAAA,sBAAmB;AACnB,EAAAA,WAAA,sBAAmB;AACnB,EAAAA,WAAA,gCAA6B;AAC7B,EAAAA,WAAA,2BAAwB;AACxB,EAAAA,WAAA,8BAA2B;AAC3B,EAAAA,WAAA,2BAAwB;AACxB,EAAAA,WAAA,wBAAqB;AACrB,EAAAA,WAAA,4BAAyB;AACzB,EAAAA,WAAA,oBAAiB;AACjB,EAAAA,WAAA,cAAW;AACX,EAAAA,WAAA,mBAAgB;AAChB,EAAAA,WAAA,kCAA+B;AAC/B,EAAAA,WAAA,kBAAe;AACf,EAAAA,WAAA,yBAAsB;AACtB,EAAAA,WAAA,mBAAgB;AAChB,EAAAA,WAAA,qBAAkB;AAjBR,SAAAA;AAAA,GAAA;AAoBL,SAAS,iBAAiB,MAAyB;AACxD,UAAQ,MAAM;AAAA,IACZ,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL;AACE,aAAO;AAAA,EACX;AACF;;;ACtCO,IAAM,WAAN,cAAuB,MAAM;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACS;AAAA,EAElB,YAAY,SAA0B;AACpC,UAAM,QAAQ,OAAO;AACrB,SAAK,OAAO;AACZ,SAAK,OAAO,QAAQ;AACpB,SAAK,WAAW,iBAAiB,QAAQ,IAAI;AAC7C,SAAK,UAAU,QAAQ;AACvB,SAAK,cAAc,QAAQ,eAAe;AAC1C,SAAK,mBAAmB,QAAQ;AAChC,SAAK,QAAQ,QAAQ;AAAA,EACvB;AAAA,EAEA,SAAS;AACP,WAAO;AAAA,MACL,MAAM,KAAK;AAAA,MACX,UAAU,KAAK;AAAA,MACf,SAAS,KAAK;AAAA,MACd,SAAS,KAAK;AAAA,MACd,aAAa,KAAK;AAAA,MAClB,kBAAkB,KAAK;AAAA,IACzB;AAAA,EACF;AACF;AAEO,SAAS,WAAW,OAA0B;AACnD,MAAI,iBAAiB,UAAU;AAC7B,WAAO;AAAA,EACT;AAEA,MAAI,iBAAiB,OAAO;AAC1B,WAAO,IAAI,SAAS;AAAA,MAClB;AAAA,MACA,SAAS,MAAM;AAAA,MACf,aAAa;AAAA,MACb,OAAO;AAAA,IACT,CAAC;AAAA,EACH;AAEA,SAAO,IAAI,SAAS;AAAA,IAClB;AAAA,IACA,SAAS;AAAA,IACT,SAAS;AAAA,IACT,aAAa;AAAA,EACf,CAAC;AACH;","names":["ErrorCode"]}
package/dist/cli/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  FetError,
4
4
  toFetError
5
- } from "../chunk-V4ZRBF5L.js";
5
+ } from "../chunk-J5WB4KAL.js";
6
6
 
7
7
  // src/cli/index.ts
8
8
  import { createInterface } from "readline/promises";
@@ -1989,6 +1989,7 @@ async function proxyCommand(ctx, command, args) {
1989
1989
  await assertVerified(ctx);
1990
1990
  }
1991
1991
  const mapped = await mapOpenSpecCommand(ctx, command, openSpecArgs);
1992
+ await assertOpenSpecCommandSupported(ctx, mapped.command, command);
1992
1993
  const mappedChangeId = extractChangeId(mapped.args);
1993
1994
  const targetChangeId = command === "archive" ? mapped.args[0] ?? ctx.changeId ?? mappedChangeId : ctx.changeId ?? mappedChangeId;
1994
1995
  runState.graphContext = await buildWorkflowGraphContext(ctx, {
@@ -2055,6 +2056,23 @@ async function proxyCommand(ctx, command, args) {
2055
2056
  data: graphContext ? { graphContext } : void 0
2056
2057
  });
2057
2058
  }
2059
+ async function assertOpenSpecCommandSupported(ctx, openSpecCommand, fetCommand) {
2060
+ const capabilities = await ctx.openSpec.getCapabilities();
2061
+ if (capabilities.commands.includes(openSpecCommand)) {
2062
+ return;
2063
+ }
2064
+ throw new FetError({
2065
+ code: "OPENSPEC_UNSUPPORTED_VERSION" /* OpenSpecUnsupportedVersion */,
2066
+ message: `OpenSpec CLI ${capabilities.version} does not expose command "${openSpecCommand}" required by "fet ${fetCommand}". FET will not substitute another workflow command automatically.`,
2067
+ details: {
2068
+ openSpecVersion: capabilities.version,
2069
+ requiredCommand: openSpecCommand,
2070
+ availableCommands: capabilities.commands,
2071
+ supported: capabilities.supported
2072
+ },
2073
+ suggestedCommand: "Upgrade OpenSpec to a version that supports this command, then rerun FET. Try: npm install -g @fission-ai/openspec@latest && fet doctor. If your OpenSpec version intentionally removed this command, pause and choose a compatible FET workflow instead of running ff automatically."
2074
+ });
2075
+ }
2058
2076
  async function createChangelogEntry(projectRoot, changeId) {
2059
2077
  return {
2060
2078
  updateTime: formatLocalTimestamp(/* @__PURE__ */ new Date()),
@@ -2236,6 +2254,195 @@ async function assertVerified(ctx) {
2236
2254
  }
2237
2255
  }
2238
2256
 
2257
+ // src/commands/update.ts
2258
+ import { spawn } from "child_process";
2259
+ var DEFAULT_PACKAGE_NAME = "@nick848/fet";
2260
+ async function updateCommand(ctx) {
2261
+ const packageName = process.env.FET_UPDATE_PACKAGE_NAME ?? DEFAULT_PACKAGE_NAME;
2262
+ const npmExecutable = process.env.FET_UPDATE_NPM_EXECUTABLE ?? defaultNpmExecutable();
2263
+ const latestVersion = await resolveLatestVersion(packageName, npmExecutable);
2264
+ const currentVersion = ctx.fetVersion;
2265
+ if (compareVersions(currentVersion, latestVersion) >= 0) {
2266
+ ctx.output.result({
2267
+ ok: true,
2268
+ command: "update",
2269
+ summary: ctx.language === "en" ? `FET is already up to date (${currentVersion}).` : `FET \u5DF2\u662F\u6700\u65B0\u7248 (${currentVersion})\u3002`,
2270
+ data: {
2271
+ packageName,
2272
+ currentVersion,
2273
+ latestVersion,
2274
+ updated: false
2275
+ }
2276
+ });
2277
+ return;
2278
+ }
2279
+ if (!ctx.json) {
2280
+ ctx.output.info(
2281
+ ctx.language === "en" ? `Updating FET from ${currentVersion} to ${latestVersion}...` : `\u6B63\u5728\u5C06 FET \u4ECE ${currentVersion} \u5347\u7EA7\u5230 ${latestVersion}...`
2282
+ );
2283
+ }
2284
+ const installArgs = ["install", "-g", `${packageName}@latest`];
2285
+ const result = await runNpm(npmExecutable, installArgs, {
2286
+ cwd: ctx.cwd,
2287
+ stdio: ctx.json ? "pipe" : "inherit"
2288
+ });
2289
+ if (result.exitCode !== 0) {
2290
+ throw new FetError({
2291
+ code: "UPDATE_FAILED" /* UpdateFailed */,
2292
+ message: ctx.language === "en" ? "FET update failed." : "FET \u5347\u7EA7\u5931\u8D25\u3002",
2293
+ details: result,
2294
+ suggestedCommand: `${npmExecutable} ${installArgs.join(" ")}`
2295
+ });
2296
+ }
2297
+ ctx.output.result({
2298
+ ok: true,
2299
+ command: "update",
2300
+ summary: ctx.language === "en" ? `FET updated from ${currentVersion} to ${latestVersion}.` : `FET \u5DF2\u4ECE ${currentVersion} \u5347\u7EA7\u5230 ${latestVersion}\u3002`,
2301
+ data: {
2302
+ packageName,
2303
+ currentVersion,
2304
+ latestVersion,
2305
+ updated: true,
2306
+ installCommand: `${npmExecutable} ${installArgs.join(" ")}`
2307
+ }
2308
+ });
2309
+ }
2310
+ async function resolveLatestVersion(packageName, npmExecutable) {
2311
+ const override = process.env.FET_UPDATE_LATEST_VERSION?.trim();
2312
+ if (override) {
2313
+ return override;
2314
+ }
2315
+ const result = await runNpm(npmExecutable, ["view", packageName, "version", "--json"], {
2316
+ cwd: process.cwd(),
2317
+ stdio: "pipe"
2318
+ });
2319
+ if (result.exitCode !== 0) {
2320
+ throw new FetError({
2321
+ code: "UPDATE_FAILED" /* UpdateFailed */,
2322
+ message: "Unable to check the latest FET version from npm.",
2323
+ details: result,
2324
+ suggestedCommand: `${npmExecutable} view ${packageName} version`
2325
+ });
2326
+ }
2327
+ const version = parseNpmVersion(result.stdout ?? "");
2328
+ if (!version) {
2329
+ throw new FetError({
2330
+ code: "UPDATE_FAILED" /* UpdateFailed */,
2331
+ message: "npm returned an invalid FET version.",
2332
+ details: { stdout: result.stdout }
2333
+ });
2334
+ }
2335
+ return version;
2336
+ }
2337
+ function parseNpmVersion(stdout) {
2338
+ const trimmed = stdout.trim();
2339
+ if (!trimmed) {
2340
+ return null;
2341
+ }
2342
+ try {
2343
+ const parsed = JSON.parse(trimmed);
2344
+ return typeof parsed === "string" && parsed.length > 0 ? parsed : null;
2345
+ } catch {
2346
+ return trimmed;
2347
+ }
2348
+ }
2349
+ function runNpm(command, args, options) {
2350
+ return new Promise((resolve2, reject) => {
2351
+ const stdout = [];
2352
+ const stderr = [];
2353
+ const child = spawn(command, args, {
2354
+ cwd: options.cwd,
2355
+ stdio: options.stdio,
2356
+ shell: process.platform === "win32"
2357
+ });
2358
+ child.stdout?.on("data", (chunk) => stdout.push(chunk));
2359
+ child.stderr?.on("data", (chunk) => stderr.push(chunk));
2360
+ child.on("error", (error) => {
2361
+ reject(
2362
+ new FetError({
2363
+ code: "UPDATE_FAILED" /* UpdateFailed */,
2364
+ message: "Unable to run npm for FET update.",
2365
+ details: { command, args },
2366
+ cause: error,
2367
+ suggestedCommand: `${command} ${args.join(" ")}`
2368
+ })
2369
+ );
2370
+ });
2371
+ child.on("close", (exitCode, signal) => {
2372
+ resolve2({
2373
+ command,
2374
+ args,
2375
+ exitCode: exitCode ?? 1,
2376
+ signal,
2377
+ stdout: stdout.length ? Buffer.concat(stdout).toString("utf8") : void 0,
2378
+ stderr: stderr.length ? Buffer.concat(stderr).toString("utf8") : void 0
2379
+ });
2380
+ });
2381
+ });
2382
+ }
2383
+ function defaultNpmExecutable() {
2384
+ return process.platform === "win32" ? "npm.cmd" : "npm";
2385
+ }
2386
+ function compareVersions(left, right) {
2387
+ const leftVersion = parseVersion(left);
2388
+ const rightVersion = parseVersion(right);
2389
+ for (let index = 0; index < 3; index += 1) {
2390
+ const diff = leftVersion.main[index] - rightVersion.main[index];
2391
+ if (diff !== 0) {
2392
+ return Math.sign(diff);
2393
+ }
2394
+ }
2395
+ if (!leftVersion.prerelease.length && rightVersion.prerelease.length) {
2396
+ return 1;
2397
+ }
2398
+ if (leftVersion.prerelease.length && !rightVersion.prerelease.length) {
2399
+ return -1;
2400
+ }
2401
+ const length = Math.max(leftVersion.prerelease.length, rightVersion.prerelease.length);
2402
+ for (let index = 0; index < length; index += 1) {
2403
+ const leftPart = leftVersion.prerelease[index];
2404
+ const rightPart = rightVersion.prerelease[index];
2405
+ if (leftPart === void 0) {
2406
+ return -1;
2407
+ }
2408
+ if (rightPart === void 0) {
2409
+ return 1;
2410
+ }
2411
+ const diff = comparePrereleasePart(leftPart, rightPart);
2412
+ if (diff !== 0) {
2413
+ return diff;
2414
+ }
2415
+ }
2416
+ return 0;
2417
+ }
2418
+ function parseVersion(version) {
2419
+ const [withoutBuild] = version.trim().replace(/^v/i, "").split("+");
2420
+ const [mainValue = "", prereleaseValue = ""] = withoutBuild.split("-");
2421
+ const mainParts = mainValue.split(".").map((part) => Number.parseInt(part, 10));
2422
+ return {
2423
+ main: [
2424
+ Number.isFinite(mainParts[0]) ? mainParts[0] : 0,
2425
+ Number.isFinite(mainParts[1]) ? mainParts[1] : 0,
2426
+ Number.isFinite(mainParts[2]) ? mainParts[2] : 0
2427
+ ],
2428
+ prerelease: prereleaseValue ? prereleaseValue.split(".") : []
2429
+ };
2430
+ }
2431
+ function comparePrereleasePart(left, right) {
2432
+ const leftNumber = /^\d+$/.test(left) ? Number.parseInt(left, 10) : null;
2433
+ const rightNumber = /^\d+$/.test(right) ? Number.parseInt(right, 10) : null;
2434
+ if (leftNumber !== null && rightNumber !== null) {
2435
+ return Math.sign(leftNumber - rightNumber);
2436
+ }
2437
+ if (leftNumber !== null) {
2438
+ return -1;
2439
+ }
2440
+ if (rightNumber !== null) {
2441
+ return 1;
2442
+ }
2443
+ return left.localeCompare(right);
2444
+ }
2445
+
2239
2446
  // src/commands/verify.ts
2240
2447
  import { createHash } from "crypto";
2241
2448
  import { mkdir as mkdir7, readFile as readFile12, stat as stat5 } from "fs/promises";
@@ -2811,15 +3018,15 @@ After the command completes, report the GitNexus state, generated handoff files,
2811
3018
  `;
2812
3019
  }
2813
3020
  function renderSlashPrompt(command, language) {
3021
+ if (command === "ff" || command === "propose") {
3022
+ return renderFastForwardSlashPrompt(command, language);
3023
+ }
2814
3024
  if (language !== "en") {
2815
3025
  return renderSlashPromptZh(command);
2816
3026
  }
2817
3027
  if (command === "continue") {
2818
3028
  return renderContinueSlashPrompt(language);
2819
3029
  }
2820
- if (command === "ff" || command === "propose") {
2821
- return renderFastForwardSlashPrompt(command, language);
2822
- }
2823
3030
  if (command === "explore") {
2824
3031
  return renderExploreSlashPrompt(language);
2825
3032
  }
@@ -3419,11 +3626,11 @@ Guardrails:
3419
3626
  );
3420
3627
  }
3421
3628
  function renderFastForwardSlashPrompt(command, language) {
3422
- const title = command === "propose" ? "Propose a new FET/OpenSpec change" : "Fast-forward FET/OpenSpec artifact creation";
3629
+ 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";
3423
3630
  const commandLine = command === "propose" ? "fet propose <change-id-or-description>" : "fet ff --change <change-id>";
3424
3631
  return renderManagedSlashPrompt(
3425
3632
  `fet ${command} [...args]`,
3426
- command === "propose" ? "Create a change and generate required OpenSpec artifacts" : "Generate required OpenSpec artifacts for a change",
3633
+ language === "en" ? command === "propose" ? "Create a change and generate required OpenSpec artifacts" : "Generate required OpenSpec artifacts for a change" : command === "propose" ? "\u521B\u5EFA\u5E76\u8865\u9F50 FET/OpenSpec \u63D0\u6848\u4EA7\u7269" : "\u5FEB\u901F\u751F\u6210 FET/OpenSpec \u6240\u9700\u4EA7\u7269",
3427
3634
  `${title}.
3428
3635
 
3429
3636
  Input after the slash command may be a change id or a description of what the user wants to build. For ff, it may be omitted when the active OpenSpec change is unambiguous.
@@ -3438,6 +3645,7 @@ Steps:
3438
3645
  \`\`\`
3439
3646
  4. Follow the native output. If it asks for clarification, ask the user rather than inventing details.
3440
3647
  5. If the native output includes artifact paths or templates to write, create only those files and preserve OpenSpec structure.
3648
+ 6. If FET reports that the OpenSpec CLI does not expose the requested command, stop immediately. Do not run \`fet ff\`, \`openspec ff\`, \`openspec change\`, or any alternative workflow command unless the user explicitly chooses that fallback after seeing the error.
3441
3649
 
3442
3650
  Artifact rules:
3443
3651
  - Follow the instruction field from OpenSpec/FET for each artifact.
@@ -3449,7 +3657,11 @@ Output:
3449
3657
  - Change id and location.
3450
3658
  - Artifacts created.
3451
3659
  - Current status.
3452
- - Next recommended command, usually /prompts:fet-apply <change-id>.`,
3660
+ - Next recommended command, usually /prompts:fet-apply <change-id>.
3661
+
3662
+ Guardrails:
3663
+ - Do not substitute one FET/OpenSpec workflow command for another after a command-not-found or unsupported-version error.
3664
+ - If OpenSpec appears outdated or incompatible, report the detected version and suggest \`npm install -g @fission-ai/openspec@latest\` or \`fet doctor\`, then wait for the user's decision.`,
3453
3665
  void 0,
3454
3666
  language
3455
3667
  );
@@ -3855,7 +4067,7 @@ async function findExecutable() {
3855
4067
  const command = process.platform === "win32" ? "where.exe" : "which";
3856
4068
  try {
3857
4069
  const { stdout } = await exec(command, ["openspec"]);
3858
- const first = stdout.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
4070
+ const first = stdout.split(/\r?\n/).map((line) => line.trim()).sort((left, right) => executablePreference(left) - executablePreference(right)).find(Boolean);
3859
4071
  if (first) {
3860
4072
  return first;
3861
4073
  }
@@ -3872,6 +4084,12 @@ async function findExecutable() {
3872
4084
  });
3873
4085
  }
3874
4086
  }
4087
+ function executablePreference(path) {
4088
+ if (process.platform === "win32" && path.toLowerCase().endsWith(".cmd")) {
4089
+ return 0;
4090
+ }
4091
+ return 1;
4092
+ }
3875
4093
  async function readVersion(executablePath) {
3876
4094
  const command = executablePath === "npx openspec" ? "npx" : executablePath;
3877
4095
  const args = executablePath === "npx openspec" ? ["openspec", "--version"] : ["--version"];
@@ -3892,14 +4110,14 @@ function exec(command, args) {
3892
4110
  }
3893
4111
 
3894
4112
  // src/openspec/runner.ts
3895
- import { spawn } from "child_process";
4113
+ import { spawn as spawn2 } from "child_process";
3896
4114
  async function runOpenSpec(executablePath, command, args, options) {
3897
4115
  const spawnCommand = executablePath === "npx openspec" ? "npx" : executablePath;
3898
4116
  const spawnArgs = executablePath === "npx openspec" ? ["openspec", command, ...args] : [command, ...args];
3899
4117
  return new Promise((resolve2, reject) => {
3900
4118
  const stdout = [];
3901
4119
  const stderr = [];
3902
- const child = spawn(spawnCommand, spawnArgs, {
4120
+ const child = spawn2(spawnCommand, spawnArgs, {
3903
4121
  cwd: options.cwd,
3904
4122
  stdio: options.stdio ?? "inherit",
3905
4123
  shell: process.platform === "win32"
@@ -3986,7 +4204,10 @@ function parseCommands(help) {
3986
4204
  "bulk-archive",
3987
4205
  "onboard"
3988
4206
  ];
3989
- return known.filter((command) => help.includes(command));
4207
+ return known.filter((command) => new RegExp(`\\b${escapeRegExp(command)}\\b`).test(help));
4208
+ }
4209
+ function escapeRegExp(value) {
4210
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3990
4211
  }
3991
4212
 
3992
4213
  // src/scanner/package.ts
@@ -4318,6 +4539,7 @@ async function createCommandContext(command, options) {
4318
4539
  var program = new Command();
4319
4540
  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");
4320
4541
  addGlobalOptions(program.command("init")).description("\u521D\u59CB\u5316 FET + OpenSpec").action(wrap("init", initCommand));
4542
+ addGlobalOptions(program.command("update")).description("\u68C0\u67E5 FET \u662F\u5426\u4E3A\u6700\u65B0\u7248\uFF0C\u5E76\u5728\u9700\u8981\u65F6\u81EA\u52A8\u5347\u7EA7").action(wrap("update", updateCommand));
4321
4543
  addGlobalOptions(program.command("update-context")).description("\u66F4\u65B0\u9879\u76EE\u4E0A\u4E0B\u6587").action(wrap("update-context", updateContextCommand));
4322
4544
  addGlobalOptions(program.command("fill-context")).description("\u5237\u65B0 IDE \u586B\u5145 AGENTS.md \u5360\u4F4D\u7B26\u7684\u63D0\u793A\u6587\u4EF6").action(wrap("fill-context", fillContextCommand));
4323
4545
  var graph = addGlobalOptions(program.command("graph").description("\u7BA1\u7406\u53EF\u9009\u7684 GitNexus \u4EE3\u7801\u56FE\u652F\u6301"));
@@ -4400,7 +4622,7 @@ function renderModelPolicyActionHint(policyMode, language) {
4400
4622
  return language === "en" ? "This is advisory because FET_MODEL_POLICY=warn; the command will continue." : "\u5F53\u524D\u8BBE\u7F6E FET_MODEL_POLICY=warn\uFF0C\u8BE5\u63D0\u9192\u4EC5\u4F5C\u4E3A\u5EFA\u8BAE\uFF0C\u547D\u4EE4\u4F1A\u7EE7\u7EED\u6267\u884C\u3002";
4401
4623
  }
4402
4624
  async function warnIfContextPlaceholdersRemain(ctx) {
4403
- if (["init", "update-context", "fill-context", "doctor"].includes(ctx.command)) {
4625
+ if (["init", "update", "update-context", "fill-context", "doctor"].includes(ctx.command)) {
4404
4626
  return;
4405
4627
  }
4406
4628
  const count2 = await countAgentsLlmPlaceholders(ctx.projectRoot);