diff --git a/package-lock.json b/package-lock.json index a8690b8..444e384 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "codebase-cli", - "version": "2.0.0-pre.76", + "version": "2.0.0-pre.77", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codebase-cli", - "version": "2.0.0-pre.76", + "version": "2.0.0-pre.77", "license": "MIT", "dependencies": { "@earendil-works/pi-agent-core": "0.74.0", diff --git a/package.json b/package.json index ac74083..65e1d11 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codebase-cli", - "version": "2.0.0-pre.76", + "version": "2.0.0-pre.77", "description": "Codebase CLI — a TypeScript coding agent on the pi-mono runtime. OAuth-aware, any LLM provider, single install.", "keywords": [ "ai", diff --git a/src/commands/builtins/usage.test.ts b/src/commands/builtins/usage.test.ts index bb97b0a..844c0b9 100644 --- a/src/commands/builtins/usage.test.ts +++ b/src/commands/builtins/usage.test.ts @@ -29,7 +29,7 @@ describe("formatUsageBalance", () => { plan: "pro", }), ).toMatchObject({ - creditLine: "Credits: 250 / 1,000 used · 750 left", + creditLine: "Metered credits: 250 / 1,000 used · 750 left", pct: 25, planName: "pro", }); @@ -42,7 +42,7 @@ describe("formatUsageBalance", () => { plan: { name: "team", monthlyCredits: 40 }, }), ).toMatchObject({ - creditLine: "Credits: 30 / 40 used · 10 left", + creditLine: "Metered credits: 30 / 40 used · 10 left", pct: 75, planName: "team", }); @@ -50,7 +50,7 @@ describe("formatUsageBalance", () => { it("uses the known free allowance when the live endpoint omits it", () => { expect(formatUsageBalance({ creditsRemaining: 25, planId: "free" })).toMatchObject({ - creditLine: "Credits: 25 / 50 used · 25 left", + creditLine: "Metered credits: 25 / 50 used · 25 left", pct: 50, planName: "free", }); @@ -58,7 +58,7 @@ describe("formatUsageBalance", () => { it("accepts snake-case allowance fields", () => { expect(formatUsageBalance({ creditsRemaining: 30, monthly_credits: 50, planId: "free" })).toMatchObject({ - creditLine: "Credits: 20 / 50 used · 30 left", + creditLine: "Metered credits: 20 / 50 used · 30 left", pct: 40, }); }); @@ -72,7 +72,7 @@ describe("formatUsageBalance", () => { planName: "pro", }), ).toMatchObject({ - creditLine: "Credits: 100 / 1,000 used · 900 left", + creditLine: "Metered credits: 100 / 1,000 used · 900 left", pct: 10, planName: "pro", }); diff --git a/src/commands/builtins/usage.ts b/src/commands/builtins/usage.ts index 38343d2..5d1c076 100644 --- a/src/commands/builtins/usage.ts +++ b/src/commands/builtins/usage.ts @@ -24,7 +24,7 @@ interface Balance { */ export const usage: Command = { name: "usage", - description: "Show your Codebase plan usage — credits used, remaining, and reset date.", + description: "Show metered credits and included Codebase turn allowances.", handler: async (_args, ctx) => { ctx.emit(await fetchUsageReport()); return { handled: true }; @@ -68,10 +68,10 @@ export async function fetchUsageReport(): Promise { lines.push("Monthly allowance was not returned yet; showing remaining credits only."); } if (typeof b.anyBuildsRemaining === "number" && b.anyBuildsRemaining >= 0) { - lines.push(`Build turns remaining: ${b.anyBuildsRemaining.toLocaleString()}`); + lines.push(`Included web-build turns remaining: ${b.anyBuildsRemaining.toLocaleString()}`); } if (typeof b.cheapBuildsRemaining === "number" && b.cheapBuildsRemaining >= 0) { - lines.push(`Fast turns remaining: ${b.cheapBuildsRemaining.toLocaleString()}`); + lines.push(`Included fast coding turns remaining: ${b.cheapBuildsRemaining.toLocaleString()}`); } return lines.join("\n"); } catch (err) { @@ -99,7 +99,7 @@ export function formatUsageBalance(b: Balance): { b.planName ?? plan?.name ?? (typeof b.plan === "string" ? b.plan : undefined) ?? b.planId ?? "Codebase"; if (!allowance || allowance <= 0) { return { - creditLine: `Credits left: ${remaining.toLocaleString()}`, + creditLine: `Metered credits left: ${remaining.toLocaleString()}`, days, pct: null, planName, @@ -108,7 +108,7 @@ export function formatUsageBalance(b: Balance): { const used = Math.max(0, allowance - remaining); const pct = Math.max(0, Math.min(100, Math.round((used / allowance) * 100))); return { - creditLine: `Credits: ${used.toLocaleString()} / ${allowance.toLocaleString()} used · ${remaining.toLocaleString()} left`, + creditLine: `Metered credits: ${used.toLocaleString()} / ${allowance.toLocaleString()} used · ${remaining.toLocaleString()} left`, days, pct, planName, diff --git a/src/projects/cli.test.ts b/src/projects/cli.test.ts index 3606c8e..1bba8d9 100644 --- a/src/projects/cli.test.ts +++ b/src/projects/cli.test.ts @@ -1,7 +1,7 @@ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { runProjectSubcommand } from "./cli.js"; import { type ProjectClient, ProjectClientError } from "./client.js"; import { BuildHandoffStore } from "./handoff.js"; @@ -102,6 +102,19 @@ describe("runProjectSubcommand", () => { expect(result.stdout.join("\n")).toContain("alias: codebase web-build"); }); + it("prints pull help without treating the flag as a project id", async () => { + const client = fakeClient(); + client.pull = vi.fn(() => { + throw new Error("pull should not run for help"); + }); + + const result = await runProject(["project", "pull", "--help"], client); + + expect(result.code).toBe(0); + expect(result.stdout.join("\n")).toContain("usage: codebase project pull [dest]"); + expect(client.pull).not.toHaveBeenCalled(); + }); + it("defaults project list to 25 entries and puts titled indexed projects first", async () => { const projects: PlatformProject[] = [ { id: "storage-a", source: "storage-only" }, diff --git a/src/projects/cli.ts b/src/projects/cli.ts index 2467c1b..aa71fdc 100644 --- a/src/projects/cli.ts +++ b/src/projects/cli.ts @@ -44,6 +44,10 @@ export async function runProjectSubcommand(argv: string[], options: ProjectCliOp printProjectHelp(out); return 0; } + if (subcommand === "pull" && isHelpArg(argv[2])) { + printPullHelp(out); + return 0; + } if (subcommand === "pull") return await pullCmd(client, argv[2], argv[3], out, err); if (subcommand === "build") return await buildCmd(client, argv.slice(2), out, err, sleep, handoffStore); if (subcommand === "status") return await statusCmd(client, argv[2], out, err, handoffStore); @@ -127,6 +131,10 @@ function isListFlag(arg: string): boolean { return arg === "--all" || arg === "--limit" || arg.startsWith("--limit="); } +function isHelpArg(arg: string | undefined): boolean { + return arg === "--help" || arg === "-h" || arg === "help"; +} + async function listCmd(client: ProjectClient, opts: ListOptions, out: (msg: string) => void): Promise { const projects = sortProjects([...(await client.list())]); if (projects.length === 0) { @@ -628,6 +636,13 @@ function printProjectHelp(out: (msg: string) => void): void { out(" cancel cancel a running web build (`latest` is accepted)"); } +function printPullHelp(out: (msg: string) => void): void { + out("usage: codebase project pull [dest]"); + out(""); + out("Download a codebase.design project as a ZIP archive."); + out("When dest is omitted, the ZIP is saved under ~/.codebase/pulls/."); +} + function printBuildHelp(out: (msg: string) => void): void { out("usage: codebase project build [--wait] [--model MODEL] [--scaffold ID] [--project ID] "); out("alias: codebase web-build [--wait] [--model MODEL] [--scaffold ID] [--project ID] ");