Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
65 changes: 62 additions & 3 deletions cli/src/commands/update/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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");
}
100 changes: 84 additions & 16 deletions cli/src/lib/updater.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down Expand Up @@ -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");
});

Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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.");
});
});
Expand All @@ -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"),
);
Expand All @@ -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"),
);
Expand All @@ -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}`,
);
});

Expand Down Expand Up @@ -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", () => {
Expand All @@ -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;
Expand All @@ -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", () => {
Expand Down
14 changes: 7 additions & 7 deletions cli/src/lib/updater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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}`,
});
Expand Down Expand Up @@ -893,7 +893,7 @@ export async function installGitHubBinaryRelease(
release.version,
tempPath,
options.spawnImpl,
"Downloaded altertable",
"Downloaded Altertable CLI",
);
replaceFileAtomically(targetPath, tempPath);

Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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 "),
Expand All @@ -1177,15 +1177,15 @@ 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"),
]);
}

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"),
]),
Expand Down
Loading