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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
10 changes: 5 additions & 5 deletions src/commands/builtins/usage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
});
Expand All @@ -42,23 +42,23 @@ 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",
});
});

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",
});
});

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,
});
});
Expand All @@ -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",
});
Expand Down
10 changes: 5 additions & 5 deletions src/commands/builtins/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -68,10 +68,10 @@ export async function fetchUsageReport(): Promise<string> {
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) {
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
15 changes: 14 additions & 1 deletion src/projects/cli.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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 <id> [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" },
Expand Down
15 changes: 15 additions & 0 deletions src/projects/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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<number> {
const projects = sortProjects([...(await client.list())]);
if (projects.length === 0) {
Expand Down Expand Up @@ -628,6 +636,13 @@ function printProjectHelp(out: (msg: string) => void): void {
out(" cancel <id> cancel a running web build (`latest` is accepted)");
}

function printPullHelp(out: (msg: string) => void): void {
out("usage: codebase project pull <id> [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] <prompt>");
out("alias: codebase web-build [--wait] [--model MODEL] [--scaffold ID] [--project ID] <prompt>");
Expand Down
Loading