From 90865b24b321b69c4f3becaac9778fc53c3a83f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Wed, 14 Oct 2026 14:06:29 +0200 Subject: [PATCH] feat(update): clarify installation progress and release guidance Make Altertable CLI self-updates easier to scan with styled current and target versions, explicit upgrade/reinstall/downgrade outcomes, and release notes, issue-reporting, and commit-log links. Use Altertable CLI consistently in update messages. Keep JSON results free of human progress output and omit version comparisons when reinstalling the same release. --- CHANGELOG.md | 2 + cli/src/commands/update/index.ts | 65 +++++++++++++++++++- cli/src/lib/updater.test.ts | 100 ++++++++++++++++++++++++++----- cli/src/lib/updater.ts | 14 ++--- 4 files changed, 155 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9155cf9d..e8420b05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Make CLI updates easier to scan with styled version summaries, explicit upgrade/reinstall/downgrade messages, and release notes, issue, and commit-log links. + - Generate and upgrade local data apps with `@altertable/data-app` 0.68.0, using app declarations and registered SQL parameters. ## [1.11.0](https://github.com/altertable-ai/altertable-cli/compare/v1.10.1...v1.11.0) (2026-10-13) diff --git a/cli/src/commands/update/index.ts b/cli/src/commands/update/index.ts index ac176bb9..48e2df75 100644 --- a/cli/src/commands/update/index.ts +++ b/cli/src/commands/update/index.ts @@ -1,3 +1,6 @@ +import { CLI_PACKAGE_METADATA } from "@/package-metadata.ts"; +import { span } from "@/ui/document.ts"; +import { renderDisplayText } from "@/ui/terminal/styles.ts"; import { VERSION } from "@/version.ts"; import { asCliArgString } from "@/lib/cli-args.ts"; import { writeCommandOutput } from "@/lib/command-output.ts"; @@ -119,16 +122,72 @@ export async function executeUpdateCommand( return; } - sink.writeMetadata([`Updating altertable to ${result.latest_version}`]); + if (!sink.json) { + sink.writeMetadata([formatUpdateAnnouncement(result)]); + } const installed = await dependencies.installCliUpdate(result.latest_version, { stdio: sink.json ? "pipe" : "inherit", }); await writeCommandOutput( { - kind: "ack", + kind: "normalized", data: installed, - metadataMessage: `altertable ${installed.verified_version} installed.`, + humanText: formatInstalledUpdate(result, installed.verified_version), }, sink, ); } + +function formatUpdateAnnouncement(result: UpdateCheckResult): string { + if (result.update_available) { + return renderDisplayText([ + span("Altertable CLI ", "strong"), + span(`v${result.latest_version}`, "accent"), + span(" is out! You're on "), + span(`v${result.current_version}`, "string"), + ]); + } + + if (compareVersions(result.latest_version, result.current_version) === 0) { + return renderDisplayText([ + span("Reinstalling Altertable CLI ", "strong"), + span(`v${result.current_version}`, "accent"), + span("."), + ]); + } + + return renderDisplayText([ + span("Downgrading Altertable CLI to ", "strong"), + span(`v${result.latest_version}`, "accent"), + span(". You're on "), + span(`v${result.current_version}`, "string"), + ]); +} + +function formatInstalledUpdate(result: UpdateCheckResult, installedVersion: string): string { + const repositoryUrl = `https://github.com/${CLI_PACKAGE_METADATA.repositorySlug}`; + const version = `v${normalizeVersion(installedVersion)}`; + const currentVersion = `v${normalizeVersion(result.current_version)}`; + const comparison = compareVersions(installedVersion, result.current_version); + let outcome = "Reinstalled."; + if (comparison > 0) { + outcome = "Upgraded."; + } else if (comparison < 0) { + outcome = "Downgraded."; + } + const lines = [ + outcome, + "", + `Release notes for Altertable CLI ${version}:`, + "", + renderDisplayText([span(` ${repositoryUrl}/releases/tag/${version}`, "accent")]), + "", + "Report any bugs:", + "", + ` ${repositoryUrl}/issues`, + ]; + if (comparison !== 0) { + lines.push("", "Commit log:", "", ` ${repositoryUrl}/compare/${currentVersion}...${version}`); + } + return lines.join("\n"); +} diff --git a/cli/src/lib/updater.test.ts b/cli/src/lib/updater.test.ts index dea5ce59..09ac5c7d 100644 --- a/cli/src/lib/updater.test.ts +++ b/cli/src/lib/updater.test.ts @@ -511,7 +511,7 @@ describe("binary self-update", () => { throw new Error("Expected downloaded binary verification to fail."); } catch (error) { expect(error).toBeInstanceOf(Error); - expect((error as Error).message).toContain("Downloaded altertable version is 9.9.9"); + expect((error as Error).message).toContain("Downloaded Altertable CLI version is 9.9.9"); } expect(readFileSync(targetPath, "utf8")).toBe(originalBinary); }); @@ -707,7 +707,7 @@ describe("automatic update checks", () => { }); }); - expect(stderr.join("\n")).toContain(`Update available: altertable ${UPDATE_TEST_VERSION}`); + expect(stderr.join("\n")).toContain(`Update available: Altertable CLI ${UPDATE_TEST_VERSION}`); expect(stderr.join("\n")).toContain("altertable update"); }); @@ -739,7 +739,7 @@ describe("automatic update checks", () => { }); expect(fetchCalls).toBe(0); - expect(stderr.join("\n")).toContain(`Update available: altertable ${UPDATE_TEST_VERSION}`); + expect(stderr.join("\n")).toContain(`Update available: Altertable CLI ${UPDATE_TEST_VERSION}`); }); test("falls back to a cached notice when a scheduled refresh fails", async () => { @@ -769,7 +769,7 @@ describe("automatic update checks", () => { }, }); - expect(stderr.join("\n")).toContain(`Update available: altertable ${UPDATE_TEST_VERSION}`); + expect(stderr.join("\n")).toContain(`Update available: Altertable CLI ${UPDATE_TEST_VERSION}`); expect(readUpdateState().last_error).toBe("Unable to check for CLI updates."); }); }); @@ -780,7 +780,7 @@ describe("update command", () => { expect(output.stdout[0]).toBe( [ - `A new version of altertable is available: v${UPDATE_TEST_VERSION} (current v${VERSION})`, + `A new version of Altertable CLI is available: v${UPDATE_TEST_VERSION} (current v${VERSION})`, `Run ${UpdaterConfig.commands.selfUpdate} to install it.`, ].join("\n"), ); @@ -800,7 +800,7 @@ describe("update command", () => { expect(output.stdout[0]).toBe( [ - `Target version v${targetVersion} is older than installed altertable v${VERSION}.`, + `Target version v${targetVersion} is older than installed Altertable CLI v${VERSION}.`, `Run altertable update ${targetVersion} --force to install it anyway.`, ].join("\n"), ); @@ -810,7 +810,7 @@ describe("update command", () => { const output = await runUpdateCommand(["upgrade", UPDATE_TEST_VERSION, "--check"]); expect(output.stdout[0]).toContain( - `A new version of altertable is available: v${UPDATE_TEST_VERSION}`, + `A new version of Altertable CLI is available: v${UPDATE_TEST_VERSION}`, ); }); @@ -838,8 +838,57 @@ describe("update command", () => { await executeUpdateCommand({}, runtime.output, dependencies); expect(installedVersion).toBe(UPDATE_TEST_VERSION); - expect(output.stderr).toContain(`Updating altertable to ${UPDATE_TEST_VERSION}`); - expect(output.stderr).toContain(`altertable ${UPDATE_TEST_VERSION} installed.`); + expect(output.stderr).toEqual([ + `Altertable CLI v${UPDATE_TEST_VERSION} is out! You're on v${VERSION}`, + ]); + const summary = output.stdout.join("\n"); + expect(summary).toStartWith("Upgraded.\n"); + expect(summary).not.toContain("Welcome"); + expect(summary).toContain(`Release notes for Altertable CLI v${UPDATE_TEST_VERSION}:`); + expect(summary).toContain(`/releases/tag/v${UPDATE_TEST_VERSION}`); + expect(summary).toContain("https://github.com/altertable-ai/altertable-cli/issues"); + expect(summary).toContain(`/compare/v${VERSION}...v${UPDATE_TEST_VERSION}`); + }); + + test("keeps installation JSON free of human output", async () => { + const runtime = createCliRuntime({ debug: false, json: true, agent: false }); + const humanOutput: string[] = []; + const jsonOutput: unknown[] = []; + runtime.output.writeHuman = (text) => humanOutput.push(text); + runtime.output.writeMetadata = (lines) => humanOutput.push(...lines); + runtime.output.writeJson = (data) => jsonOutput.push(data); + const installed = { + installed_version: UPDATE_TEST_VERSION, + verified_version: UPDATE_TEST_VERSION, + method: "github-binary" as const, + target_path: "/tmp/altertable", + }; + await executeUpdateCommand({ version: UPDATE_TEST_VERSION }, runtime.output, { + checkForUpdate, + async installCliUpdate(_version, options) { + expect(options?.stdio).toBe("pipe"); + return installed; + }, + }); + expect(humanOutput).toEqual([]); + expect(jsonOutput).toEqual([installed]); + }); + + test("does not announce success when installation fails", async () => { + const runtime = createCliRuntime({ debug: false, json: false, agent: false }); + const humanOutput: string[] = []; + runtime.output.writeHuman = (text) => humanOutput.push(text); + runtime.output.writeMetadata = () => {}; + const rejected = expect( + executeUpdateCommand({ version: UPDATE_TEST_VERSION }, runtime.output, { + checkForUpdate, + async installCliUpdate() { + throw new Error("Installation failed"); + }, + }), + ).rejects.toThrow("Installation failed"); + expect(humanOutput).toEqual([]); + return rejected; }); test("rejects a downgrade unless forced", () => { @@ -848,10 +897,25 @@ describe("update command", () => { ); }); - test("installs an explicit downgrade when forced", async () => { + test.each([ + { + version: "1.1.0", + announcement: `Downgrading Altertable CLI to v1.1.0. You're on v${VERSION}`, + outcome: "Downgraded.", + }, + { + version: VERSION, + announcement: `Reinstalling Altertable CLI v${VERSION}.`, + outcome: "Reinstalled.", + }, + ])("describes a forced $outcome accurately", async ({ version, announcement, outcome }) => { const runtime = createCliRuntime({ debug: false, json: false, agent: false }); + const stdout: string[] = []; + const stderr: string[] = []; + runtime.output.writeHuman = (text) => stdout.push(text); + runtime.output.writeMetadata = (lines) => stderr.push(...lines); let installedVersion = ""; - const dependencies: UpdateCommandDependencies = { + await executeUpdateCommand({ version, force: true }, runtime.output, { checkForUpdate, async installCliUpdate(version) { installedVersion = version; @@ -862,11 +926,15 @@ describe("update command", () => { target_path: "/tmp/altertable", }; }, - }; - - await executeUpdateCommand({ version: "1.1.0", force: true }, runtime.output, dependencies); - - expect(installedVersion).toBe("1.1.0"); + }); + expect(installedVersion).toBe(version); + expect(stderr).toEqual([announcement]); + expect(stdout[0]).toStartWith(`${outcome}\n`); + expect(stdout[0]).toContain(`Release notes for Altertable CLI v${version}:`); + expect(stdout[0]).not.toContain("What's new"); + if (version === VERSION) { + expect(stdout[0]).not.toContain("Commit log:"); + } }); test("rejects the removed install positional", () => { diff --git a/cli/src/lib/updater.ts b/cli/src/lib/updater.ts index 7e0920df..a4baf247 100644 --- a/cli/src/lib/updater.ts +++ b/cli/src/lib/updater.ts @@ -767,7 +767,7 @@ export function verifyInstalledCliVersion( expectedVersion: string, command = "altertable", spawnImpl: SpawnSyncImpl = spawnSync, - description = "Installed altertable", + description = "Installed Altertable CLI", ): string { const result = spawnImpl(command, ["--version"], { encoding: "utf8" }); if (result.error) { @@ -810,7 +810,7 @@ function replaceFileAtomically(targetPath: string, replacementPath: string): voi } } rmSync(replacementPath, { force: true }); - throw new CliError("Unable to replace the altertable binary.", { + throw new CliError("Unable to replace the Altertable CLI binary.", { cause: error, details: `Backup path: ${backupPath}`, }); @@ -893,7 +893,7 @@ export async function installGitHubBinaryRelease( release.version, tempPath, options.spawnImpl, - "Downloaded altertable", + "Downloaded Altertable CLI", ); replaceFileAtomically(targetPath, tempPath); @@ -1108,7 +1108,7 @@ function writeAutomaticUpdateNotice(sink: OutputSink, latestVersion: string): vo sink.writeMetadata([ "", renderDisplayText([ - span(`Update available: altertable ${latestVersion} (current ${VERSION}).`, "subtle"), + span(`Update available: Altertable CLI ${latestVersion} (current ${VERSION}).`, "subtle"), ]), renderDisplayText([ span(`Run ${recommendedInstallCommand(latestVersion)} to install it.`, "subtle"), @@ -1160,7 +1160,7 @@ export function formatUpdateResult(result: UpdateCheckResult, targetVersion?: st if (comparison < 0) { return [ renderDisplayText([ - span(`Target version v${target} is older than installed altertable v${current}.`), + span(`Target version v${target} is older than installed Altertable CLI v${current}.`), ]), renderDisplayText([ span("Run "), @@ -1177,7 +1177,7 @@ export function formatUpdateResult(result: UpdateCheckResult, targetVersion?: st if (!result.update_available) { return renderDisplayText([ span("Congrats!", "success"), - span(" You're already on the latest version of altertable "), + span(" You're already on the latest version of Altertable CLI "), span(`(which is v${normalizeVersion(result.current_version)})`, "subtle"), ]); } @@ -1185,7 +1185,7 @@ export function formatUpdateResult(result: UpdateCheckResult, targetVersion?: st return [ renderDisplayText([ span( - `A new version of altertable is available: v${normalizeVersion(result.latest_version)} `, + `A new version of Altertable CLI is available: v${normalizeVersion(result.latest_version)} `, ), span(`(current v${normalizeVersion(result.current_version)})`, "subtle"), ]),