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
8 changes: 8 additions & 0 deletions .changeset/plain-version-output.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"cf": minor
---

Print a parseable version from `cf --version`

Write the package version to stdout for `--version` and `-v`, so scripts can
parse it directly. Interactive use keeps the branded banner on stderr.
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,7 @@ hand-written design or remain deliberately unsupported.

These have stabilised this repo's voice; preserve them.

- **Banner:** `renderPromptIntro(version)` supplies the compact branded headline. `openSession` prints it once for an interactive command; bare cf and `--version` use the compact presentation directly.
- **Banner:** `renderPromptIntro(version)` supplies the compact branded headline. `openSession` prints it once to stderr for an interactive command, including `--version`; bare cf uses the compact presentation directly. `--version` also prints the plain version to stdout.
- **Terminal styling:** Use the semantic helpers in `lib/ui/theme.ts`
(`theme.brand`, `theme.info`, `theme.muted`, etc.) rather than importing
Chalk directly, calling explicit color methods, or hard-coding RGB/ANSI
Expand Down Expand Up @@ -615,7 +615,7 @@ Live globals (`packages/cli/src/index.ts:buildCli`):
| Flag | Alias | Purpose |
| -------------- | ----- | --------------------------------------------------------------- |
| `--help` | `-h` | Show help |
| `--version` | `-v` | Show version (branded banner) |
| `--version` | `-v` | Show plain version on stdout (branded banner on stderr) |
| `--quiet` | `-q` | Suppress non-essential output |
| `--zone` | `-z` | Zone ID or domain (overrides `CLOUDFLARE_ZONE_ID`) |
| `--profile` | — | Use a specific auth profile |
Expand Down
38 changes: 31 additions & 7 deletions packages/cli/src/__tests__/update-check-runtime.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
import * as updateCheck from "../lib/update-check.js";
import { VERSION } from "../version.js";
import { captureOutput } from "./helpers/capture-output.js";
import { runCf } from "./helpers/run-cf.js";

Expand Down Expand Up @@ -59,7 +60,7 @@ describe("update check runtime", () => {
isMajor: false,
});

await runCf(["--version"]);
await runCf([]);

expect(output.stdout()).toContain("update available: v1.3.0");
expect(updateCheck.getUpdateNotice).toHaveBeenCalledOnce();
Expand All @@ -70,20 +71,43 @@ describe("update check runtime", () => {
setTTY(false, false);
const output = captureOutput();

await runCf(["--version"]);
await runCf([]);

expect(output.stdout()).not.toContain("update available");
expect(updateCheck.getUpdateNotice).not.toHaveBeenCalled();
expect(updateCheck.maybeStartBackgroundUpdateCheck).not.toHaveBeenCalled();
});

it("does not load or refresh update state when quiet", async () => {
it("prints the interactive version banner to stderr and the number to stdout", async () => {
setTTY(true, true);
const output = captureOutput();
vi.mocked(updateCheck.getUpdateNotice).mockReturnValue({
latestVersion: "1.3.0",
isMajor: false,
});

await runCf(["--version", "--quiet"]);
await runCf(["--version"]);

expect(output.stdout()).not.toContain("update available");
expect(output.stdout()).toBe(VERSION);
expect(output.stderr()).toContain("🍊☁️");
expect(output.stderr()).toContain(`v${VERSION}`);
expect(output.stderr()).toContain("update available: v1.3.0");
expect(updateCheck.getUpdateNotice).toHaveBeenCalledOnce();
expect(updateCheck.maybeStartBackgroundUpdateCheck).toHaveBeenCalledOnce();
});

it.each([
["-v", ["-v"], false],
["--version --quiet", ["--version", "--quiet"], true],
["-q -v", ["-q", "-v"], true],
])("keeps stdout parseable for %s", async (_label, args, tty) => {
setTTY(tty, tty);
const output = captureOutput();

await runCf(args);

expect(output.stdout()).toBe(VERSION);
expect(output.stderr()).toBe("");
expect(updateCheck.getUpdateNotice).not.toHaveBeenCalled();
expect(updateCheck.maybeStartBackgroundUpdateCheck).not.toHaveBeenCalled();
});
Expand All @@ -93,7 +117,7 @@ describe("update check runtime", () => {
setTTY(true, true);
const output = captureOutput();

await runCf(["--version"]);
await runCf([]);

expect(output.stdout()).not.toContain("update available");
expect(updateCheck.getUpdateNotice).not.toHaveBeenCalled();
Expand All @@ -115,7 +139,7 @@ describe("update check runtime", () => {
setTTY(true, true);
captureOutput();

await runCf(["--profile", "complete", "--version"]);
await runCf(["--profile", "complete", "--help"]);

expect(updateCheck.getUpdateNotice).toHaveBeenCalledOnce();
expect(updateCheck.maybeStartBackgroundUpdateCheck).toHaveBeenCalledOnce();
Expand Down
22 changes: 7 additions & 15 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,22 +339,16 @@ async function reportHelpShown(command: string | undefined): Promise<void> {
}
}

/**
* Check if --version flag is present and show branded version.
*
* Yargs has its own `--version` handling but renders monochrome
* `0.0.5` with no branding. The slim headline (orange + emoji) is
* the same banner cf uses everywhere else, so intercepting `-v` /
* `--version` here keeps that consistent.
*/
/** Print the branded banner to stderr and the parseable version to stdout. */
function maybeHandleVersionEarly(update?: UpdateNotice): boolean {
const rawArgs = process.argv.slice(2);
const separator = rawArgs.indexOf("--");
const args = separator === -1 ? rawArgs : rawArgs.slice(0, separator);
if (!args.includes("--version") && !args.includes("-v")) {
return false;
}
console.log(renderPromptIntro(version, update));
openSession(version, { quiet: hasQuietFlag(args), update });
console.log(version);
return true;
}

Expand Down Expand Up @@ -401,9 +395,8 @@ function maybeShowSplash(update?: UpdateNotice): boolean {
* must be parseable, no decoration). Covers both the init-script
* form (`cf complete bash`) and the runtime callback form
* (`cf complete -- <words…>`).
* - the command is bare `cf` or `cf --version` (those render the
* slim `🍊☁️ cf · v…` headline themselves via `renderPromptIntro`
* and we'd double up)
* - the command is bare `cf` (the splash renders its own headline)
* or `cf --version` (handled before normal command dispatch)
*/
function maybeOpenSession(quiet = false, update?: UpdateNotice): void {
const rawArgs = process.argv.slice(2);
Expand Down Expand Up @@ -444,8 +437,7 @@ async function prepareUpdateCheck(): Promise<PreparedUpdateCheck | undefined> {
if (commandName(args) === "complete") {
return undefined;
}
const bannerWritesToStdout =
args.length === 0 || args.includes("--version") || args.includes("-v");
const bannerWritesToStdout = args.length === 0;
if (
bannerWritesToStdout
? process.stdout.isTTY !== true
Expand Down Expand Up @@ -489,7 +481,7 @@ async function prepareUpdateCheck(): Promise<PreparedUpdateCheck | undefined> {
* ## Global flags (live)
*
* --help, -h Show help
* --version, -v Show version (branded banner)
* --version, -v Show plain version on stdout (banner on stderr)
* --quiet, -q Suppress non-essential output
* --zone, -z Zone ID or domain
* --profile Use a specific auth profile
Expand Down
31 changes: 8 additions & 23 deletions packages/cli/src/lib/ui/banner.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
/**
* Banner module — the slim `🍊☁️ cf · v…` headline cf renders at the
* top of every interactive invocation, plus the `cf <subcommand>`
* command-line formatter shared by splash / help screens.
*
* Previously this module also shipped a full ASCII cloud logo plus
* gradient dividers for `cf --version` / `cf` splash. Both surfaces
* now use the slim headline (see `renderPromptIntro`) so the splash
* art was removed along with its `gradient-string` dependency.
* Formats the slim `🍊☁️ cf · v…` headline used by interactive commands
* and the bare `cf` splash, plus `cf <subcommand>` labels for command lists.
*/

import { stripVTControlCharacters } from "node:util";
Expand All @@ -16,34 +10,25 @@ import { theme } from "./theme.js";
import type { UpdateNotice } from "../update-check.js";

/**
* Slim wrangler-style intro banner with a plain horizontal underline:
* Slim cf banner with a horizontal underline:
*
* 🍊☁️ cf · v0.0.5
* ──────────────────
*
* When this process was reached by a *global* cf delegating to a
* project-pinned copy (Wrangler-2 style), a dim `· delegated` tag is
* appended to the headline so it's clear why the running version may
* differ from the global cf the user invoked:
* A project-pinned cf reached through global cf delegation adds a dim
* `· delegated` tag so the running version is clear:
*
* 🍊☁️ cf · v1.2.3 · delegated
Comment thread
penalosa marked this conversation as resolved.
* ─────────────────────────────
*
* Underline length tracks the visible width of the headline (ANSI
* stripped). Mirrors wrangler's `── ` rule below `⛅️ wrangler vX.Y.Z`
* without trying to connect down into a prompt gutter — earlier
* iterations bent a `┌` corner into the gutter via `clack.intro()`,
* but the bare corner above an emoji headline read as a stray glyph,
* not a frame.
*
* Color gracefully degrades to plain ASCII when NO_COLOR/non-TTY.
* Underline length tracks the visible width of the headline after ANSI
* styling is stripped. Color styling follows the terminal settings.
*/
export function renderPromptIntro(
version: string,
update?: UpdateNotice
): string {
// 🍊☁️ — "orange cloud" is an old internal nickname for Cloudflare.
// U+FE0F after ☁ forces emoji presentation rather than monochrome glyph.
// U+FE0F after ☁ keeps the mark in emoji presentation.
const mark = "🍊☁️ ";

// A delegated child is spawned with DELEGATION_SENTINEL set (see
Expand Down
Loading