-
Notifications
You must be signed in to change notification settings - Fork 0
@wave-av/cli 1.0.11: current banner, unserved commands hidden, release-drift fixed #84
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,8 +6,45 @@ All notable changes to this project are documented here. The format is based on | |
|
|
||
| ## [Unreleased] | ||
|
|
||
| ## [1.0.11] - 2026-09-28 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: The new Prompt for AI agents |
||
|
|
||
| ### Changed | ||
| - **Banner no longer says "Enterprise Streaming Platform."** `wave --help`'s ASCII banner now | ||
| prints the current positioning line, `Media infrastructure for the agentic internet` — the | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Custom agent: Flag AI Slop and Fabricated Changes The changelog attributes this banner change to Prompt for AI agents |
||
| same line `package.json`'s own `description` field already carried — instead of the retired | ||
| "Enterprise Streaming Platform" tagline. Graded clean by `governance/voice/voice-gate.mjs` | ||
| (WAVE's voice-eval gate) before landing. | ||
| - **`wave --help` no longer lists command groups the gateway does not serve today.** Per | ||
| `dec-unserved-families (b)` (WAVE Core go-live, decided 2026-09): `stream`, `studio`, | ||
| `editor`, `phone`, `collab` and `podcast` are hidden from the default top-level help listing. | ||
| Each is still fully registered — `wave <group> --help` shows its real, SDK-backed | ||
| subcommands exactly as before, and `wave --all` (new flag) shows every group, tagging the | ||
| hidden six `(not yet served)`. This was verified live on 2026-09-28: every route under these | ||
| six prefixes returns `404 ROUTE_NOT_FOUND` from `api.wave.online`, with the same body pointing | ||
| at the gateway's own free capability index. (The go-live definition's "camera/production" | ||
| family has no corresponding top-level `wave camera`/`wave production` command group to hide — | ||
| that gap is gateway/OpenAPI-only.) | ||
| - **Running a now-hidden group's command fails before any network call.** Instead of a caller | ||
| discovering a raw 404 after a real HTTP round-trip, every subcommand inside `stream`, | ||
| `studio`, `editor`, `phone`, `collab` and `podcast` now exits `1` immediately (JSON shape | ||
| when the environment prefers JSON, colored stderr otherwise) with a message pointing at | ||
| `https://gateway.wave.online/.well-known/wave-skills.json` — the gateway's own list of what | ||
| IS served right now — instead of surfacing `ROUTE_NOT_FOUND`/`ROUTE_NOT_MAPPED` from a call | ||
| that was always going to fail. | ||
|
|
||
| ### Fixed | ||
|
|
||
| - **"Release drift check" (`.github/workflows/release-drift.yml`), reported failing daily since | ||
| 2026-09-11.** Reproduced by running `scripts/release/check-drift.sh` locally against a clean | ||
| `origin/main` checkout: it reports `RESULT: IN SYNC (exit 0)` — tag `v1.0.10`, `package.json` | ||
| `1.0.10`, npm registry latest `1.0.10`, GitHub Release present, provenance present. The CI | ||
| failures are not release drift: every failing run's annotation reads "The job was not started | ||
| because your account is locked due to a billing issue" (confirmed across 25 consecutive daily | ||
| runs, 2026-09-11 through 2026-09-28, via `gh run view <id>` — the last *executed* run, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: The run count and date range contradict each other: 2026-09-11 through 2026-09-28 is 18 days, not 25 consecutive daily runs. Fix the number or the date range so the documented investigation receipt is internally consistent. Prompt for AI agents |
||
| 2026-09-10, was green). No code change fixes an org-wide Actions billing lock from inside | ||
| this repo; the fix is running the check locally (as this entry's receipt does) until the | ||
| scheduled Action is replaced with an unblocked CI plane. | ||
|
|
||
| - `pr-agent` lane: fork-triggered `/` commands are now refused, and the AI | ||
| call's budget fits inside its step. Three defects, one of them only visible | ||
| once the first was fixed. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| { | ||
| "name": "@wave-av/cli", | ||
| "version": "1.0.10", | ||
| "version": "1.0.11", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: The version bump in package.json is not mirrored in the committed package-lock.json, whose root package version still reads 1.0.10. For a repo that already tracks release drift, this leaves a stale lockfile after release: Prompt for AI agents |
||
| "description": "WAVE CLI: the terminal client for WAVE, media infrastructure for the agentic internet. Manage live streams, productions, and media routes from your terminal.", | ||
| "main": "./dist/index.js", | ||
| "type": "module", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -56,6 +56,7 @@ import { registerLinkCommands } from "./commands/link/index.js"; | |
| import { registerComposeCommands } from "./commands/compose/index.js"; | ||
| import { detectEnvironment } from "./lib/environment.js"; | ||
| import { CLI_VERSION } from "./lib/version.js"; | ||
| import { applyUnservedGroups } from "./lib/unserved.js"; | ||
|
|
||
| function printBanner(): void { | ||
| // WAVE brand gradient: blue (#3366FF) -> purple (#7B41E8) -> cyan (#33BBCC) | ||
|
|
@@ -72,7 +73,7 @@ function printBanner(): void { | |
| console.log(` ${b("╚███╔███╔╝")} ${p("██║ ██║")} ${p(" ╚████╔╝ ")} ${c("███████╗")}`); | ||
| console.log(` ${b(" ╚══╝╚══╝ ")} ${p("╚═╝ ╚═╝")} ${p(" ╚═══╝ ")} ${c("╚══════╝")}`); | ||
| console.log(""); | ||
| console.log(` ${d("Enterprise Streaming Platform")} ${chalk.hex("#555")(`v${CLI_VERSION}`)}`); | ||
| console.log(` ${d("Media infrastructure for the agentic internet")} ${chalk.hex("#555")(`v${CLI_VERSION}`)}`); | ||
| console.log(` ${d("─".repeat(45))}`); | ||
| console.log(""); | ||
| } | ||
|
|
@@ -89,7 +90,8 @@ export function createProgram(): Command { | |
| .option("--org <id>", "Override organization") | ||
| .option("-c, --confirm", "Skip confirmation prompts") | ||
| .option("--no-color", "Disable colored output") | ||
| .option("--debug", "Verbose debug logging"); | ||
| .option("--debug", "Verbose debug logging") | ||
| .option("--all", "Show every command group, including ones not yet served by the WAVE API"); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: The new global Prompt for AI agents |
||
|
|
||
| // Auth & Config | ||
| registerAuthCommands(program); | ||
|
|
@@ -170,6 +172,12 @@ export function createProgram(): Command { | |
| registerCompletionCommands(program); | ||
| registerApiCommands(program); | ||
|
|
||
| // Hide command groups the gateway does not serve yet from the default --help listing | ||
| // (dec-unserved-families (b), WAVE Core go-live). `wave --all` still shows them, tagged | ||
| // "(not yet served)", and every action in them fails BEFORE the network call with the | ||
| // gateway's own doc_url instead of a raw 404. | ||
| applyUnservedGroups(program); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: In an interactive terminal, Prompt for AI agents |
||
|
|
||
| // Skip banner for AI agents and CI (they prefer clean output) | ||
| const env = detectEnvironment(); | ||
| if (!env.isAgent && !env.isCI) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,203 @@ | ||
| import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; | ||
|
|
||
| /** | ||
| * dec-unserved-families (b), WAVE Core go-live (2026-09): `stream`, `studio`, `editor`, `phone`, | ||
| * `collab` and `podcast` all hit gateway routes that return 404 ROUTE_NOT_FOUND today. These tests | ||
| * prove the three-part contract this module implements: | ||
| * | ||
| * 1. The default `wave --help` listing omits all six groups. | ||
| * 2. `wave --all` shows all six, each tagged "(not yet served)", and a served group (e.g. | ||
| * `auth`) is never mis-tagged or hidden. | ||
| * 3. Invoking ANY subcommand inside an unserved group exits 1 with the gateway's real doc_url | ||
| * BEFORE any network call — never a raw 404 surfaced from a request that was always going to | ||
| * fail. | ||
| * | ||
| * `wave <group> --help` (the group's OWN help, not root) is intentionally untouched — verified by | ||
| * the "group help unaffected" case below — because that's what lets `wave --all` communicate real, | ||
| * SDK-backed subcommands underneath a "(not yet served)" tag rather than an empty stub. | ||
| */ | ||
|
|
||
| vi.mock("../lib/api-client.js", () => ({ | ||
| getClient: vi.fn(), | ||
| })); | ||
|
|
||
| import { getClient } from "./api-client.js"; | ||
| import { createProgram } from "../cli.js"; | ||
| import { GATEWAY_DOC_URL, UNSERVED_GROUPS, unservedMessage } from "./unserved.js"; | ||
|
|
||
| const ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g"); | ||
| const stripAnsi = (s: string): string => s.replace(ANSI, ""); | ||
|
|
||
| /** Env vars that flip detectEnvironment() into non-interactive / agent / CI mode. */ | ||
| const ENV_KEYS = [ | ||
| "CI", | ||
| "GITHUB_ACTIONS", | ||
| "VERCEL", | ||
| "BUILDKITE", | ||
| "GITLAB_CI", | ||
| "CIRCLECI", | ||
| "WAVE_AGENT", | ||
| "CLAUDE_CODE", | ||
| "CURSOR_SESSION", | ||
| "AIDER_SESSION", | ||
| "CONTINUE_SESSION", | ||
| "WAVE_OUTPUT_FORMAT", | ||
| ] as const; | ||
|
|
||
| describe("wave --help: unserved command groups are hidden by default", () => { | ||
| let savedArgv: string[]; | ||
| let savedEnv: Record<string, string | undefined>; | ||
|
|
||
| beforeEach(() => { | ||
| savedArgv = process.argv; | ||
| savedEnv = {}; | ||
| for (const key of ENV_KEYS) { | ||
| savedEnv[key] = process.env[key]; | ||
| delete process.env[key]; | ||
| } | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| process.argv = savedArgv; | ||
| for (const [key, value] of Object.entries(savedEnv)) { | ||
| if (value === undefined) delete process.env[key]; | ||
| else process.env[key] = value; | ||
| } | ||
| vi.restoreAllMocks(); | ||
| }); | ||
|
|
||
| it("omits every UNSERVED_GROUPS name from the default top-level listing", () => { | ||
| process.argv = ["node", "wave"]; | ||
| const program = createProgram(); | ||
| const help = stripAnsi(program.helpInformation()); | ||
|
|
||
| for (const group of UNSERVED_GROUPS) { | ||
| expect(help, `expected "${group}" to be hidden from default --help`).not.toMatch( | ||
| new RegExp(`^\\s*${group}\\b`, "m"), | ||
| ); | ||
| } | ||
| }); | ||
|
|
||
| it("still lists a served group (auth) in the default listing", () => { | ||
| process.argv = ["node", "wave"]; | ||
| const program = createProgram(); | ||
| const help = stripAnsi(program.helpInformation()); | ||
| expect(help).toMatch(/\bauth\b/); | ||
| }); | ||
|
|
||
| it("`wave --all` shows every UNSERVED_GROUPS name tagged \"(not yet served)\"", () => { | ||
| process.argv = ["node", "wave", "--all"]; | ||
| const program = createProgram(); | ||
| const help = stripAnsi(program.helpInformation()); | ||
|
|
||
| for (const group of UNSERVED_GROUPS) { | ||
| const line = new RegExp(`^\\s*${group}\\b.*\\(not yet served\\)`, "m"); | ||
| expect(help, `expected "${group}" tagged "(not yet served)" under --all:\n${help}`).toMatch( | ||
| line, | ||
| ); | ||
| } | ||
| // A served group must never pick up the tag. | ||
| expect(help).not.toMatch(/\bauth\b.*\(not yet served\)/); | ||
| }); | ||
|
|
||
| it("`wave <group> --help` (the group's own help) is unaffected and lists real subcommands", () => { | ||
| process.argv = ["node", "wave", "stream", "--help"]; | ||
| const program = createProgram(); | ||
| const streamGroup = program.commands.find((c) => c.name() === "stream"); | ||
| expect(streamGroup).toBeDefined(); | ||
| const groupHelp = stripAnsi(streamGroup!.helpInformation()); | ||
| // The group's own listing is untouched — real subcommands still show (list is one of them). | ||
| expect(groupHelp).toMatch(/\blist\b/); | ||
| }); | ||
| }); | ||
|
|
||
| /** Real `process.exit()` never returns control to the caller — mock it the same way, or code | ||
| * after the call (here: commander invoking the real, network-calling action) keeps running, | ||
| * which would mask the exact bug this guard exists to prevent. */ | ||
| class ProcessExit extends Error { | ||
| constructor(public readonly code: number | undefined) { | ||
| super(`process.exit(${code})`); | ||
| } | ||
| } | ||
|
|
||
| describe("wave <unserved-group> <subcommand>: fails before any network call", () => { | ||
| let exitSpy: ReturnType<typeof vi.spyOn>; | ||
| let errorSpy: ReturnType<typeof vi.spyOn>; | ||
| let savedEnv: Record<string, string | undefined>; | ||
|
|
||
| const CASES: Array<{ group: string; args: string[] }> = [ | ||
| { group: "stream", args: ["stream", "list"] }, | ||
| { group: "studio", args: ["studio", "list"] }, | ||
| { group: "editor", args: ["editor", "list"] }, | ||
| { group: "phone", args: ["phone", "call", "--to", "+15551234567", "--from", "+15557654321"] }, | ||
| { group: "collab", args: ["collab", "room", "list"] }, | ||
| { group: "podcast", args: ["podcast", "episodes", "list", "--podcast-id", "p_1"] }, | ||
| ]; | ||
|
|
||
| beforeEach(() => { | ||
| vi.mocked(getClient).mockReset(); | ||
| savedEnv = {}; | ||
| for (const key of ENV_KEYS) { | ||
| savedEnv[key] = process.env[key]; | ||
| delete process.env[key]; | ||
| } | ||
| exitSpy = vi.spyOn(process, "exit").mockImplementation(((code?: number) => { | ||
| throw new ProcessExit(code); | ||
| }) as unknown as typeof process.exit); | ||
| errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| for (const [key, value] of Object.entries(savedEnv)) { | ||
| if (value === undefined) delete process.env[key]; | ||
| else process.env[key] = value; | ||
| } | ||
| vi.restoreAllMocks(); | ||
| }); | ||
|
|
||
| for (const { group, args } of CASES) { | ||
| it(`\`wave ${args.join(" ")}\` exits 1 with the gateway doc_url, never calling getClient`, async () => { | ||
| const program = createProgram(); | ||
| program.exitOverride(); | ||
|
|
||
| await expect(program.parseAsync(["node", "wave", ...args])).rejects.toBeInstanceOf( | ||
| ProcessExit, | ||
| ); | ||
|
|
||
| expect(getClient, `${group}: getClient must never be called`).not.toHaveBeenCalled(); | ||
| expect(exitSpy).toHaveBeenCalledWith(1); // EXIT_CODES.GENERAL_ERROR | ||
|
|
||
| const printed = errorSpy.mock.calls.map((c: unknown[]) => String(c[0])).join("\n"); | ||
| expect(stripAnsi(printed)).toContain(GATEWAY_DOC_URL); | ||
| expect(stripAnsi(printed)).not.toMatch(/ROUTE_NOT_MAPPED|ROUTE_NOT_FOUND.*404|^\s*at\s+\S+:\d+:\d+/m); | ||
| }); | ||
| } | ||
|
|
||
| it("emits a structured JSON error (code, exit_code, doc_url suggestion) when JSON is preferred", async () => { | ||
| process.env["WAVE_OUTPUT_FORMAT"] = "json"; | ||
| const program = createProgram(); | ||
| program.exitOverride(); | ||
|
|
||
| await expect( | ||
| program.parseAsync(["node", "wave", "stream", "list"]), | ||
| ).rejects.toBeInstanceOf(ProcessExit); | ||
|
|
||
| expect(getClient).not.toHaveBeenCalled(); | ||
| const printed = errorSpy.mock.calls.map((c: unknown[]) => String(c[0])).join("\n"); | ||
| const parsed = JSON.parse(printed) as { | ||
| error: { code: string; exit_code: number; message: string; suggestions: Array<{ docs?: string }> }; | ||
| }; | ||
| expect(parsed.error.code).toBe("ROUTE_NOT_FOUND"); | ||
| expect(parsed.error.exit_code).toBe(1); | ||
| expect(parsed.error.suggestions.some((s) => s.docs === GATEWAY_DOC_URL)).toBe(true); | ||
| }); | ||
| }); | ||
|
|
||
| describe("unservedMessage()", () => { | ||
| it("names the group, the doc_url, and the --all escape hatch", () => { | ||
| const msg = unservedMessage("stream"); | ||
| expect(msg).toContain("wave stream"); | ||
| expect(msg).toContain(GATEWAY_DOC_URL); | ||
| expect(msg).toContain("--all"); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add the user-facing changes to
Unreleased.These notes are under
[1.0.11]. Add the user-facing changes to theUnreleasedsection as required.As per coding guidelines, “update
CHANGELOG.md(Unreleased) for user-facing changes.”🤖 Prompt for AI Agents
Source: Coding guidelines