FN-8452: reject invalid update flags and announce beta releases
Make update commands fail clearly on invalid arguments while helping stable users discover newer beta releases. - Parse update and upgrade options strictly, rejecting unknown, duplicate, and malformed flags before running an update - Show a live-registry beta availability notice for stable human-readable output without affecting JSON or cached results - Add CLI coverage, beta bootstrap documentation, and a patch changeset Files changed: .changeset/fn-8452-update-unknown-flags.md | 7 + RELEASING.md | 2 +- docs/cli-reference.md | 4 + docs/getting-started.md | 2 + packages/cli/src/__tests__/bin-update-args.test.ts | 75 ++++++++++ packages/cli/src/bin.ts | 25 +--- packages/cli/src/commands/__tests__/update.test.ts | 44 ++++++ packages/cli/src/commands/update.ts | 162 +++++++++++++++++++-- 8 files changed, 285 insertions(+), 36 deletions(-) Fusion-Task-Id: FN-8452 Fusion-Task-Lineage: b29a1ce5-5a40-40ec-ac18-07107fa18344 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8452-update-unknown-flags.md
Normal file
7
.changeset/fn-8452-update-unknown-flags.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Reject unknown `fn update` flags and document the beta install bootstrap.
|
||||||
|
category: fix
|
||||||
|
dev: Strict argv allow-list for update/upgrade; duplicate options rejected; optional stable-channel beta availability notice; docs for npm @beta bootstrap (FN-8452 / #2368).
|
||||||
@@ -64,7 +64,7 @@ When you merge the Version Packages PR:
|
|||||||
|
|
||||||
## Release tracks: beta and stable
|
## Release tracks: beta and stable
|
||||||
|
|
||||||
Fusion ships on two tracks. Users pick theirs with the `updateChannel` global setting (Settings → General → Release channel) or `fn update --channel <stable|beta>`.
|
Fusion ships on two tracks. Users pick theirs with the `updateChannel` global setting (Settings → General → Release channel) or `fn update --channel <stable|beta>`. Older global installs that predate `--channel` can bootstrap with `npm install -g @runfusion/fusion@beta`.
|
||||||
|
|
||||||
| Track | Cut from | Version shape | npm dist-tag | GitHub Release | Homebrew |
|
| Track | Cut from | Version shape | npm dist-tag | GitHub Release | Homebrew |
|
||||||
|-------|----------|---------------|--------------|----------------|----------|
|
|-------|----------|---------------|--------------|----------------|----------|
|
||||||
|
|||||||
@@ -153,6 +153,10 @@ fn upgrade
|
|||||||
| `--channel <stable\|beta>` | Select the release track and persist it to global settings (`updateChannel`), shared with the dashboard and desktop updater. `stable` follows the npm `latest` dist-tag; `beta` follows the newer of `latest` and `beta`. |
|
| `--channel <stable\|beta>` | Select the release track and persist it to global settings (`updateChannel`), shared with the dashboard and desktop updater. `stable` follows the npm `latest` dist-tag; `beta` follows the newer of `latest` and `beta`. |
|
||||||
| `--force` | Install the resolved channel target even when it is not newer than the current version — the explicit beta → stable downgrade path. |
|
| `--force` | Install the resolved channel target even when it is not newer than the current version — the explicit beta → stable downgrade path. |
|
||||||
|
|
||||||
|
Unknown options and positional arguments are rejected with an error and non-zero exit code. Repeating any option, including `--channel`, is also rejected rather than silently choosing a value.
|
||||||
|
|
||||||
|
If your installed CLI predates `--channel`, bootstrap onto beta with `npm install -g @runfusion/fusion@beta`. Once installed, use `fn update --channel beta` to persist the beta track.
|
||||||
|
|
||||||
`fn upgrade` is an alias for `fn update`. Installs always pin the exact resolved version rather than a dist-tag, so a beta-channel install can never silently land on stable (or vice versa).
|
`fn upgrade` is an alias for `fn update`. Installs always pin the exact resolved version rather than a dist-tag, so a beta-channel install can never silently land on stable (or vice versa).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ fusion --help
|
|||||||
|
|
||||||
If `fn update` fails with an npm bin-link collision (for example `EEXIST` / `File exists` mentioning `fn` or `fusion`), Fusion now retries once with `--force` automatically.
|
If `fn update` fails with an npm bin-link collision (for example `EEXIST` / `File exists` mentioning `fn` or `fusion`), Fusion now retries once with `--force` automatically.
|
||||||
|
|
||||||
|
Older CLI installations may predate `fn update --channel beta`. Bootstrap directly onto the beta dist-tag with `npm install -g @runfusion/fusion@beta`, then use `fn update --channel beta` to persist that track. `fn update` rejects unknown or duplicate options with a non-zero exit code, so check spelling when it reports an option error.
|
||||||
|
|
||||||
If update still fails, run the manual recovery commands:
|
If update still fails, run the manual recovery commands:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
75
packages/cli/src/__tests__/bin-update-args.test.ts
Normal file
75
packages/cli/src/__tests__/bin-update-args.test.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { dispatchUpdateCliArgs, parseUpdateCliArgs } from "../commands/update.js";
|
||||||
|
|
||||||
|
describe("parseUpdateCliArgs", () => {
|
||||||
|
it("parses every supported option without changing channel semantics", () => {
|
||||||
|
expect(parseUpdateCliArgs(["--check", "--global", "--json", "--channel", "beta", "--force"])).toEqual({
|
||||||
|
options: { check: true, global: true, json: true, channel: "beta", force: true },
|
||||||
|
});
|
||||||
|
expect(parseUpdateCliArgs(["--channel", "nightly"])).toEqual({ options: { channel: "nightly" } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["--totally-bogus-flag-xyz"],
|
||||||
|
["--beta"],
|
||||||
|
["-x"],
|
||||||
|
["something"],
|
||||||
|
])("rejects unknown update argv token %s", (token) => {
|
||||||
|
expect(parseUpdateCliArgs([token])).toEqual({
|
||||||
|
error: `Error: unknown option '${token}'. Valid options: --check, --global, --json, --channel <stable|beta>, --force.`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a missing channel value and duplicate options", () => {
|
||||||
|
expect(parseUpdateCliArgs(["--channel"])).toEqual({
|
||||||
|
error: "Error: --channel requires a value: stable or beta.",
|
||||||
|
});
|
||||||
|
expect(parseUpdateCliArgs(["--channel", "--check"])).toEqual({
|
||||||
|
error: "Error: --channel requires a value: stable or beta.",
|
||||||
|
});
|
||||||
|
expect(parseUpdateCliArgs(["--channel", "beta", "--channel", "stable"])).toEqual({
|
||||||
|
error: "Error: duplicate option '--channel'.",
|
||||||
|
});
|
||||||
|
expect(parseUpdateCliArgs(["--check", "--check"])).toEqual({
|
||||||
|
error: "Error: duplicate option '--check'.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("bin update/upgrade argv dispatch", () => {
|
||||||
|
it.each(["update", "upgrade"])("does not run an update for an unknown %s flag", async (command) => {
|
||||||
|
const runUpdate = vi.fn(async () => {
|
||||||
|
throw new Error("Already up to date.");
|
||||||
|
});
|
||||||
|
const writeError = vi.fn();
|
||||||
|
const exit = vi.fn();
|
||||||
|
|
||||||
|
await dispatchUpdateCliArgs(["--totally-bogus-flag-xyz"], { runUpdate, writeError, exit });
|
||||||
|
|
||||||
|
expect(exit).toHaveBeenCalledWith(1);
|
||||||
|
expect(writeError).toHaveBeenCalledWith(expect.stringContaining("--totally-bogus-flag-xyz"));
|
||||||
|
expect(runUpdate, `${command} must not reach success/status handling`).not.toHaveBeenCalled();
|
||||||
|
expect(writeError.mock.calls.flat().join("\n")).not.toContain("Already up to date.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("blocks a stray positional without printing a successful JSON status", async () => {
|
||||||
|
const runUpdate = vi.fn(async () => undefined);
|
||||||
|
const writeError = vi.fn();
|
||||||
|
const exit = vi.fn();
|
||||||
|
|
||||||
|
await dispatchUpdateCliArgs(["--check", "--json", "something"], { runUpdate, writeError, exit });
|
||||||
|
|
||||||
|
expect(exit).toHaveBeenCalledWith(1);
|
||||||
|
expect(writeError).toHaveBeenCalledWith(expect.stringContaining("something"));
|
||||||
|
expect(runUpdate).not.toHaveBeenCalled();
|
||||||
|
expect(writeError.mock.calls.flat().join("\n")).not.toContain('"updated":false');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes valid argv through the same dispatch used by bin.ts", async () => {
|
||||||
|
const runUpdate = vi.fn(async () => undefined);
|
||||||
|
|
||||||
|
await dispatchUpdateCliArgs(["--check", "--json", "--channel", "beta"], { runUpdate });
|
||||||
|
|
||||||
|
expect(runUpdate).toHaveBeenCalledWith({ check: true, json: true, channel: "beta" });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -153,7 +153,7 @@ async function loadCommandHandlers() {
|
|||||||
const { runSkillsSearch, runSkillsInstall } = await import("./commands/skills.js");
|
const { runSkillsSearch, runSkillsInstall } = await import("./commands/skills.js");
|
||||||
const { runResearchCreate, runResearchList, runResearchShow, runResearchExport, runResearchCancel, runResearchRetry } = await import("./commands/research.js");
|
const { runResearchCreate, runResearchList, runResearchShow, runResearchExport, runResearchCancel, runResearchRetry } = await import("./commands/research.js");
|
||||||
const { runExperimentFinalize } = await import("./commands/experiment-finalize.js");
|
const { runExperimentFinalize } = await import("./commands/experiment-finalize.js");
|
||||||
const { runUpdate } = await import("./commands/update.js");
|
const { dispatchUpdateCliArgs } = await import("./commands/update.js");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
runDashboard,
|
runDashboard,
|
||||||
@@ -289,7 +289,7 @@ async function loadCommandHandlers() {
|
|||||||
runResearchCancel,
|
runResearchCancel,
|
||||||
runResearchRetry,
|
runResearchRetry,
|
||||||
runExperimentFinalize,
|
runExperimentFinalize,
|
||||||
runUpdate,
|
dispatchUpdateCliArgs,
|
||||||
runChatInteractive,
|
runChatInteractive,
|
||||||
parseChatCliArgs,
|
parseChatCliArgs,
|
||||||
};
|
};
|
||||||
@@ -816,7 +816,7 @@ async function main() {
|
|||||||
runResearchCancel,
|
runResearchCancel,
|
||||||
runResearchRetry,
|
runResearchRetry,
|
||||||
runExperimentFinalize,
|
runExperimentFinalize,
|
||||||
runUpdate,
|
dispatchUpdateCliArgs,
|
||||||
runChatInteractive,
|
runChatInteractive,
|
||||||
parseChatCliArgs,
|
parseChatCliArgs,
|
||||||
} = await loadCommandHandlers();
|
} = await loadCommandHandlers();
|
||||||
@@ -938,24 +938,7 @@ async function main() {
|
|||||||
|
|
||||||
case "update":
|
case "update":
|
||||||
case "upgrade": {
|
case "upgrade": {
|
||||||
// FNXC:UpdateChannels 2026-07-19-13:05: --channel <stable|beta> selects
|
await dispatchUpdateCliArgs(args.slice(1));
|
||||||
// and persists the release track; --force installs the channel target
|
|
||||||
// even when not newer (the explicit beta → stable downgrade path).
|
|
||||||
// A bare trailing --channel (or one followed by another flag) errors
|
|
||||||
// instead of being silently ignored (PR #2345 review).
|
|
||||||
const channelFlagIndex = args.indexOf("--channel");
|
|
||||||
const channelValue = channelFlagIndex !== -1 ? args[channelFlagIndex + 1] : undefined;
|
|
||||||
if (channelFlagIndex !== -1 && (channelValue === undefined || channelValue.startsWith("--"))) {
|
|
||||||
console.error("Error: --channel requires a value: stable or beta.");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
await runUpdate({
|
|
||||||
check: args.includes("--check"),
|
|
||||||
global: args.includes("--global") ? true : undefined,
|
|
||||||
json: args.includes("--json"),
|
|
||||||
channel: channelValue,
|
|
||||||
force: args.includes("--force"),
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -432,5 +432,49 @@ describe("runUpdate", () => {
|
|||||||
await expect(runUpdate({ check: true })).rejects.toThrow("process.exit:1");
|
await expect(runUpdate({ check: true })).rejects.toThrow("process.exit:1");
|
||||||
expect(errorSpy).toHaveBeenCalledWith("Error checking for updates: network down");
|
expect(errorSpy).toHaveBeenCalledWith("Error checking for updates: network down");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[{ latest: "1.2.3", beta: "1.3.0-beta.1" }, "1.2.3", undefined, true],
|
||||||
|
[{ latest: "1.4.0", beta: "1.3.0-beta.1" }, "1.2.3", undefined, false],
|
||||||
|
[{ latest: "1.2.3", beta: "1.3.0-beta.1" }, "1.3.0-beta.1", undefined, false],
|
||||||
|
[{ latest: "1.2.3" }, "1.2.3", undefined, false],
|
||||||
|
[{ latest: "1.2.3", beta: "1.3.0-beta.1" }, "1.2.3", "beta", false],
|
||||||
|
])("shows a stable beta notice only for the strict live-registry predicate", async (distTags, currentVersion, channel, expectNotice) => {
|
||||||
|
if (channel) getConfiguredUpdateChannelMock.mockResolvedValue(channel);
|
||||||
|
readFileSyncMock.mockReturnValue(JSON.stringify({ name: "@runfusion/fusion", version: currentVersion }));
|
||||||
|
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ json: vi.fn().mockResolvedValue({ "dist-tags": distTags }) }));
|
||||||
|
|
||||||
|
await runUpdate({ check: true });
|
||||||
|
|
||||||
|
expect(logSpy.mock.calls.flat().join("\n").includes("A newer beta")).toBe(expectNotice);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("never announces beta from a cached registry fallback or JSON output", async () => {
|
||||||
|
vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("network down")));
|
||||||
|
getCachedUpdateStatusMock.mockReturnValue({
|
||||||
|
updateAvailable: true,
|
||||||
|
currentVersion: "1.2.3",
|
||||||
|
latestVersion: "1.2.4",
|
||||||
|
channel: "stable",
|
||||||
|
});
|
||||||
|
|
||||||
|
await runUpdate({ check: true });
|
||||||
|
expect(logSpy.mock.calls.flat().join("\n")).not.toContain("A newer beta");
|
||||||
|
|
||||||
|
logSpy.mockClear();
|
||||||
|
process.exitCode = 0;
|
||||||
|
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ json: vi.fn().mockResolvedValue({ "dist-tags": { latest: "1.2.3", beta: "1.3.0-beta.1" } }) }));
|
||||||
|
await runUpdate({ check: true, json: true });
|
||||||
|
|
||||||
|
const output = logSpy.mock.calls[0]?.[0] as string;
|
||||||
|
expect(JSON.parse(output)).toEqual({
|
||||||
|
currentVersion: "1.2.3",
|
||||||
|
latestVersion: "1.2.3",
|
||||||
|
updateAvailable: false,
|
||||||
|
updated: false,
|
||||||
|
channel: "stable",
|
||||||
|
});
|
||||||
|
expect(logSpy.mock.calls.flat().join("\n")).not.toContain("A newer beta");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,6 +35,91 @@ export type RunUpdateOptions = {
|
|||||||
force?: boolean;
|
force?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const UPDATE_CLI_OPTIONS = "--check, --global, --json, --channel <stable|beta>, --force";
|
||||||
|
|
||||||
|
type UpdateCliParseResult =
|
||||||
|
| { options: RunUpdateOptions; error?: never }
|
||||||
|
| { options?: never; error: string };
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:UpdateArgumentHonesty 2026-07-21-12:00:
|
||||||
|
Update and upgrade must fail closed for unknown or duplicate options. A documented
|
||||||
|
flag that ships only in a newer CLI must never become a false “Already up to date.”
|
||||||
|
success on an older build, and typos must not silently no-op (FN-8452 / #2368).
|
||||||
|
This parser owns argv structure only: it requires a non-flag --channel value but
|
||||||
|
passes that raw value to runUpdate for semantic stable/beta validation. Reject
|
||||||
|
repeated options rather than silently choosing first or last for the same
|
||||||
|
honesty guarantee.
|
||||||
|
*/
|
||||||
|
export function parseUpdateCliArgs(args: string[]): UpdateCliParseResult {
|
||||||
|
const options: RunUpdateOptions = {};
|
||||||
|
const seen = new Set<string>();
|
||||||
|
|
||||||
|
for (let index = 0; index < args.length; index += 1) {
|
||||||
|
const arg = args[index]!;
|
||||||
|
if (seen.has(arg)) {
|
||||||
|
return { error: `Error: duplicate option '${arg}'.` };
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (arg) {
|
||||||
|
case "--check":
|
||||||
|
seen.add(arg);
|
||||||
|
options.check = true;
|
||||||
|
break;
|
||||||
|
case "--global":
|
||||||
|
seen.add(arg);
|
||||||
|
options.global = true;
|
||||||
|
break;
|
||||||
|
case "--json":
|
||||||
|
seen.add(arg);
|
||||||
|
options.json = true;
|
||||||
|
break;
|
||||||
|
case "--force":
|
||||||
|
seen.add(arg);
|
||||||
|
options.force = true;
|
||||||
|
break;
|
||||||
|
case "--channel": {
|
||||||
|
seen.add(arg);
|
||||||
|
const channel = args[index + 1];
|
||||||
|
if (channel === undefined || channel.startsWith("-")) {
|
||||||
|
return { error: "Error: --channel requires a value: stable or beta." };
|
||||||
|
}
|
||||||
|
options.channel = channel;
|
||||||
|
index += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return { error: `Error: unknown option '${arg}'. Valid options: ${UPDATE_CLI_OPTIONS}.` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { options };
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateCommandDependencies = {
|
||||||
|
runUpdate?: (options: RunUpdateOptions) => Promise<void>;
|
||||||
|
writeError?: (message: string) => void;
|
||||||
|
exit?: (code: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch the strict argv parser used by both `fn update` and `fn upgrade`.
|
||||||
|
* Kept injectable so the bin wiring can be tested without a registry request.
|
||||||
|
*/
|
||||||
|
export async function dispatchUpdateCliArgs(
|
||||||
|
args: string[],
|
||||||
|
dependencies: UpdateCommandDependencies = {},
|
||||||
|
): Promise<void> {
|
||||||
|
const parsed = parseUpdateCliArgs(args);
|
||||||
|
if ("error" in parsed) {
|
||||||
|
(dependencies.writeError ?? console.error)(parsed.error);
|
||||||
|
(dependencies.exit ?? process.exit)(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await (dependencies.runUpdate ?? runUpdate)(parsed.options);
|
||||||
|
}
|
||||||
|
|
||||||
type UpdateStatus = {
|
type UpdateStatus = {
|
||||||
currentVersion: string;
|
currentVersion: string;
|
||||||
latestVersion: string;
|
latestVersion: string;
|
||||||
@@ -74,24 +159,26 @@ function readOwnCliVersion(): string | undefined {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchChannelTargetVersion(channel: UpdateChannel): Promise<string> {
|
type UpdateDistTags = {
|
||||||
const response = await fetch(REGISTRY_URL);
|
latest?: string;
|
||||||
const payload = (await response.json()) as {
|
beta?: string;
|
||||||
"dist-tags"?: {
|
};
|
||||||
latest?: string;
|
|
||||||
beta?: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const targetVersion = resolveUpdateTargetVersion(channel, {
|
type ChannelTarget = {
|
||||||
latest: payload?.["dist-tags"]?.latest,
|
targetVersion: string;
|
||||||
beta: payload?.["dist-tags"]?.beta,
|
distTags: UpdateDistTags;
|
||||||
});
|
};
|
||||||
|
|
||||||
|
async function fetchChannelTargetVersion(channel: UpdateChannel): Promise<ChannelTarget> {
|
||||||
|
const response = await fetch(REGISTRY_URL);
|
||||||
|
const payload = (await response.json()) as { "dist-tags"?: UpdateDistTags };
|
||||||
|
const distTags = payload?.["dist-tags"] ?? {};
|
||||||
|
const targetVersion = resolveUpdateTargetVersion(channel, distTags);
|
||||||
if (typeof targetVersion !== "string" || targetVersion.length === 0) {
|
if (typeof targetVersion !== "string" || targetVersion.length === 0) {
|
||||||
throw new Error(`Could not determine ${channel} version from npm registry response.`);
|
throw new Error(`Could not determine ${channel} version from npm registry response.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return targetVersion;
|
return { targetVersion, distTags };
|
||||||
}
|
}
|
||||||
|
|
||||||
// FNXC:UpdateChannels 2026-07-19-16:20: the version comes from the npm
|
// FNXC:UpdateChannels 2026-07-19-16:20: the version comes from the npm
|
||||||
@@ -239,6 +326,45 @@ function printJson(status: UpdateStatus): void {
|
|||||||
console.log(JSON.stringify(status));
|
console.log(JSON.stringify(status));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:UpdateBetaNotice 2026-07-21-12:15:
|
||||||
|
Stable, human-readable output may mention but never install a newer beta so
|
||||||
|
release-note readers can discover it without changing stable resolution
|
||||||
|
(FN-8452 / #2368 suggested fix #3). Show the notice only when channel is stable,
|
||||||
|
JSON is off, this run fetched the live registry, beta is non-empty, and beta is
|
||||||
|
strictly newer than both current and stable target. Cache-only and registry
|
||||||
|
failure paths must never invent a beta notice.
|
||||||
|
*/
|
||||||
|
function printBetaAvailabilityNotice({
|
||||||
|
channel,
|
||||||
|
jsonOutput,
|
||||||
|
registrySucceeded,
|
||||||
|
betaTag,
|
||||||
|
currentVersion,
|
||||||
|
stableTarget,
|
||||||
|
}: {
|
||||||
|
channel: UpdateChannel;
|
||||||
|
jsonOutput: boolean;
|
||||||
|
registrySucceeded: boolean;
|
||||||
|
betaTag: string | undefined;
|
||||||
|
currentVersion: string;
|
||||||
|
stableTarget: string;
|
||||||
|
}): void {
|
||||||
|
if (
|
||||||
|
channel !== "stable" ||
|
||||||
|
jsonOutput ||
|
||||||
|
!registrySucceeded ||
|
||||||
|
typeof betaTag !== "string" ||
|
||||||
|
betaTag.length === 0 ||
|
||||||
|
!isVersionNewer(betaTag, currentVersion) ||
|
||||||
|
!isVersionNewer(betaTag, stableTarget)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`A newer beta (${betaTag}) is available. To opt in, run \`fn update --channel beta\` or \`npm install -g @runfusion/fusion@beta\`.`);
|
||||||
|
}
|
||||||
|
|
||||||
function getLatestVersionFallback(currentVersion: string, channel: UpdateChannel): string | null {
|
function getLatestVersionFallback(currentVersion: string, channel: UpdateChannel): string | null {
|
||||||
const cached = getCachedUpdateStatus(currentVersion);
|
const cached = getCachedUpdateStatus(currentVersion);
|
||||||
if (!cached) return null;
|
if (!cached) return null;
|
||||||
@@ -287,8 +413,13 @@ export async function runUpdate(options: RunUpdateOptions = {}): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let latestVersion: string;
|
let latestVersion: string;
|
||||||
|
let betaTag: string | undefined;
|
||||||
|
let registrySucceeded = false;
|
||||||
try {
|
try {
|
||||||
latestVersion = await fetchChannelTargetVersion(channel);
|
const target = await fetchChannelTargetVersion(channel);
|
||||||
|
latestVersion = target.targetVersion;
|
||||||
|
betaTag = target.distTags.beta;
|
||||||
|
registrySucceeded = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const fallbackVersion = getLatestVersionFallback(currentVersion, channel);
|
const fallbackVersion = getLatestVersionFallback(currentVersion, channel);
|
||||||
if (!fallbackVersion) {
|
if (!fallbackVersion) {
|
||||||
@@ -322,6 +453,7 @@ export async function runUpdate(options: RunUpdateOptions = {}): Promise<void> {
|
|||||||
printJson(checkStatus);
|
printJson(checkStatus);
|
||||||
} else {
|
} else {
|
||||||
printStatus(checkStatus, true);
|
printStatus(checkStatus, true);
|
||||||
|
printBetaAvailabilityNotice({ channel, jsonOutput, registrySucceeded, betaTag, currentVersion, stableTarget: latestVersion });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updateAvailable) {
|
if (updateAvailable) {
|
||||||
@@ -343,6 +475,7 @@ export async function runUpdate(options: RunUpdateOptions = {}): Promise<void> {
|
|||||||
printJson(status);
|
printJson(status);
|
||||||
} else {
|
} else {
|
||||||
printStatus(status, false);
|
printStatus(status, false);
|
||||||
|
printBetaAvailabilityNotice({ channel, jsonOutput, registrySucceeded, betaTag, currentVersion, stableTarget: latestVersion });
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -370,4 +503,5 @@ export async function runUpdate(options: RunUpdateOptions = {}): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
printStatus(updatedStatus, false);
|
printStatus(updatedStatus, false);
|
||||||
|
printBetaAvailabilityNotice({ channel, jsonOutput, registrySucceeded, betaTag, currentVersion, stableTarget: latestVersion });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user