Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
b51b67b
feat: add WizardSigninAuthClient for browser-mediated login flow
viadezo1er May 11, 2026
a3bd5c4
fix: match full-block logo characters when PNG is unavailable
viadezo1er May 11, 2026
bddfc24
chore: max 3min rate limit for polling
viadezo1er May 11, 2026
bfff9ac
chore: add request timeout
viadezo1er May 12, 2026
b77521c
feat: Braintrust API client
viadezo1er May 11, 2026
ca8c7c0
feat: expand wizard-copy with new prompts and helpers
viadezo1er May 11, 2026
ec4fabb
feat: use open to open web page
viadezo1er May 11, 2026
d0ab852
feat: use open to open web page
viadezo1er May 11, 2026
8e021d0
feat: add cleanup message helpers
viadezo1er May 11, 2026
285eb99
feat: add fuzzy search prompt helper
viadezo1er May 11, 2026
e3627c2
feat: add git repo detection and .env writing helpers
viadezo1er May 11, 2026
4fc525b
feat: add fuzzy search prompt helper
viadezo1er May 11, 2026
8102508
feat: add language/framework detection
viadezo1er May 11, 2026
184a949
feat: add CLI argument parser
viadezo1er May 11, 2026
0adc6a0
feat: add LLM providers list
viadezo1er May 11, 2026
9dfc5e2
feat: add instrumentation prompt template
viadezo1er May 11, 2026
28c7501
feat: implement clack wizard flow
viadezo1er May 11, 2026
a61246b
feat: collect multi-credential provider fields in wizard flow
viadezo1er May 13, 2026
d22a82a
feat: add git repo detection and .env writing helpers
viadezo1er May 11, 2026
474205e
chore: use ignore for .gitignore parsing instead of manual implementa…
viadezo1er May 12, 2026
4797dab
chore: use yargs to generate the help
viadezo1er May 13, 2026
1939000
fix: remove manual help catching
viadezo1er May 13, 2026
c53aaf0
feat: azure, vertex, bedrock api key support
viadezo1er May 13, 2026
3418d75
refactor: tighten LlmProvider type and test quality
viadezo1er May 13, 2026
06c40d0
chore: use yargs to generate the help
viadezo1er May 13, 2026
56dc57e
chore: formatting
viadezo1er May 14, 2026
063ec6d
chore: prompt comment
viadezo1er May 14, 2026
5db0c08
feat: collect multi-credential provider fields in wizard flow
viadezo1er May 13, 2026
f0e9217
feat: scaffold bt-wizard-harness workspace package
viadezo1er May 11, 2026
3562b8a
chore: move wizard code into packages/ and wire up harness tooling
viadezo1er May 14, 2026
20d930f
fix: await parseArgs in cli.ts and drop dead helpText export
viadezo1er May 14, 2026
0847d33
fix: await async git helpers in clack-wizard
viadezo1er May 14, 2026
071d5ea
fix: clean up harness scaffold and fix typings errors
viadezo1er May 14, 2026
83effa0
chore: drop phantom harness dep from braintrust-wizard
viadezo1er May 14, 2026
8e7e1c4
chore: migrate pi deps to @earendil-works/* namespace
viadezo1er May 14, 2026
ecf776f
chore: move wizard code into packages/ and wire up harness tooling
viadezo1er May 14, 2026
7a6cd2e
feat: add path-guard extension
viadezo1er May 11, 2026
8257160
feat: add bt, curl, git, request-command tool extensions
viadezo1er May 11, 2026
02ebeea
feat: add package-manager tool extension
viadezo1er May 11, 2026
7a4116d
feat: add bt-wizard-harness launcher binary
viadezo1er May 11, 2026
9c1b83f
feat: add instrument.ts harness bridge
viadezo1er May 11, 2026
31052d3
feat: accept providerCredentials map in runHarness
viadezo1er May 13, 2026
b4d57e6
feat: wire PI instrumentation into wizard flow
viadezo1er May 11, 2026
ac95e58
feat: pass providerCredentials map to runInstrumentation
viadezo1er May 13, 2026
f9a4cb2
feat: wire PI instrumentation into wizard flow
viadezo1er May 11, 2026
5a112dc
feat: pass providerCredentials map to runInstrumentation
viadezo1er May 13, 2026
3db886d
feat: implement clack wizard flow
viadezo1er May 11, 2026
b86d2c5
feat: single executable (SEA)
viadezo1er May 11, 2026
048ec1c
feat: bun as js runtime
viadezo1er May 14, 2026
f7ec5a7
feat: pass providerCredentials map to runInstrumentation
viadezo1er May 13, 2026
a29f9bf
feat: implement clack wizard flow
viadezo1er May 11, 2026
a030d11
feat: telemetry
viadezo1er May 11, 2026
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
3 changes: 2 additions & 1 deletion .mise.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
[tools]
node = "24.15.0"
node = "26.1.0"
bun= "1.3.14"
pnpm = "10.33.3"
21 changes: 15 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,21 @@ Default implementation work should go into the Clack implementation. Do not work
- Keep beau Ink UI components focused on rendering and input handling.
- Keep backend/API request logic out of presentation components; expose it through query or mutation hooks.

## Workspace Layout

This is a pnpm workspace. All packages live under `packages/`.

- `packages/braintrust-wizard/` — the main wizard CLI (Clack + beau variants).
- `packages/bt-wizard-harness/` — the pi-coding-agent harness scaffold.

Root-level scripts (`pnpm build`, `pnpm lint`, etc.) delegate to the packages via `--filter` or `-r`.

## Implementation Notes

- Source files live under `src/`.
- Tests live under `test/`.
- The default CLI entrypoint is `src/cli.ts`; Rolldown emits `dist/cli.js`.
- The beau CLI entrypoint is `src/beau/cli.tsx`; Rolldown emits `dist/cli.beau.js`.
- Shared wizard copy lives in `src/wizard-copy.ts`; keep both variants using it.
- `src/query-client.ts` owns QueryClient creation and should remain the central place for query defaults.
- Wizard source files live under `packages/braintrust-wizard/src/`.
- Wizard tests live under `packages/braintrust-wizard/test/`.
- The default CLI entrypoint is `packages/braintrust-wizard/src/cli.ts`; Rolldown emits `packages/braintrust-wizard/dist/cli.js`.
- The beau CLI entrypoint is `packages/braintrust-wizard/src/beau/cli.tsx`; Rolldown emits `packages/braintrust-wizard/dist/cli.beau.js`.
- Shared wizard copy lives in `packages/braintrust-wizard/src/wizard-copy.ts`; keep both variants using it.
- `packages/braintrust-wizard/src/query-client.ts` owns QueryClient creation and should remain the central place for query defaults.
- Do not add SEA packaging yet; the current build targets are JavaScript bundles.
42 changes: 10 additions & 32 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,45 +1,23 @@
{
"name": "braintrust-wizard",
"name": "crank",
"version": "0.0.0",
"private": true,
"description": "CLI wizard to get your project set up with Braintrust",
"description": "Braintrust wizard monorepo",
"license": "MIT",
"type": "module",
"bin": {
"braintrust-wizard": "./dist/cli.js"
},
"packageManager": "pnpm@10.33.3",
"scripts": {
"start": "pnpm build && node dist/cli.js",
"build": "rolldown -c",
"start:beau": "pnpm build:beau && node dist/cli.beau.js",
"build:beau": "rolldown -c rolldown.beau.config.mjs",
"typings": "tsc --noEmit",
"test": "vitest run",
"lint": "eslint .",
"start": "pnpm --filter braintrust-wizard start",
"build": "pnpm --filter braintrust-wizard build",
"start:beau": "pnpm --filter braintrust-wizard start:beau",
"build:beau": "pnpm --filter braintrust-wizard build:beau",
"typings": "pnpm -r run typings",
"test": "pnpm -r run test",
"lint": "pnpm -r run lint",
"format": "prettier --write .",
"format:check": "prettier --check ."
},
"dependencies": {
"@clack/prompts": "1.3.0",
"@tanstack/react-query": "5.100.9",
"ink": "7.0.2",
"react": "19.2.5",
"react-devtools-core": "7.0.1"
},
"devDependencies": {
"@eslint/js": "10.0.1",
"@types/node": "24.12.2",
"@types/react": "19.2.14",
"eslint": "10.3.0",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-react": "7.37.5",
"eslint-plugin-react-hooks": "7.1.1",
"globals": "17.6.0",
"prettier": "3.8.3",
"rolldown": "1.0.0-rc.18",
"typescript": "6.0.3",
"typescript-eslint": "8.59.2",
"vitest": "4.1.5"
"prettier": "3.8.3"
}
}
File renamed without changes.
51 changes: 51 additions & 0 deletions packages/braintrust-wizard/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"name": "braintrust-wizard",
"version": "0.0.0",
"private": true,
"description": "CLI wizard to get your project set up with Braintrust",
"license": "MIT",
"type": "module",
"bin": {
"braintrust-wizard": "./dist/cli.js"
},
"scripts": {
"start": "pnpm build && node dist/cli.js",
"build": "rolldown -c",
"start:beau": "pnpm build:beau && node dist/cli.beau.js",
"build:beau": "rolldown -c rolldown.beau.config.mjs",
"typings": "tsc --noEmit",
"test": "vitest run",
"lint": "eslint .",
"format": "prettier --write .",
"format:check": "prettier --check ."
},
"dependencies": {
"@braintrust/bt-wizard-harness": "workspace:*",
"@inquirer/search": "4.1.8",
"@clack/prompts": "1.3.0",
"@tanstack/react-query": "5.100.9",
"ignore": "^7.0.5",
"ink": "7.0.2",
"open": "^11.0.0",
"react": "19.2.5",
"react-devtools-core": "7.0.1",
"yargs": "^18.0.0"
},
"devDependencies": {
"@braintrust/proxy": "0.0.9",
"@eslint/js": "10.0.1",
"@types/node": "24.12.2",
"@types/react": "19.2.14",
"@types/yargs": "^17.0.35",
"eslint": "10.3.0",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-react": "7.37.5",
"eslint-plugin-react-hooks": "7.1.1",
"globals": "17.6.0",
"prettier": "3.8.3",
"rolldown": "1.0.0-rc.18",
"typescript": "6.0.3",
"typescript-eslint": "8.59.2",
"vitest": "4.1.5"
}
}
File renamed without changes.
27 changes: 27 additions & 0 deletions packages/braintrust-wizard/rolldown.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { defineConfig } from "rolldown";

export default defineConfig([
{
input: "src/cli.ts",
output: {
banner: "#!/usr/bin/env node",
codeSplitting: false,
file: "dist/cli.mjs",
format: "esm",
sourcemap: true,
},
platform: "node",
external: [/^node:/],
},
{
input: "src/crank-telemetry.ts",
output: {
codeSplitting: false,
file: "dist/crank-telemetry.mjs",
format: "esm",
sourcemap: true,
},
platform: "node",
external: [/^node:/],
},
]);
228 changes: 228 additions & 0 deletions packages/braintrust-wizard/src/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
export type WizardSigninCreateResponse = {
readonly id: string;
readonly poll_token: string;
readonly login_path: string;
readonly login_url: string;
readonly expires_at: string;
};

export type WizardSigninOrgInfo = {
readonly id: string;
readonly name: string;
readonly api_url?: string | null;
readonly proxy_url?: string | null;
readonly realtime_url?: string | null;
readonly is_universal_api?: boolean | null;
readonly git_metadata?: unknown;
};

export type WizardSigninProject = {
readonly id: string;
readonly name: string;
readonly org_id: string;
readonly description?: string | null;
};

export type WizardSigninCompleteResult = {
readonly apiKey: string;
readonly orgInfo: WizardSigninOrgInfo;
readonly project: WizardSigninProject;
};

export type WizardSigninEvents = {
readonly onLoginUrl: (info: {
readonly loginUrl: string;
readonly expiresAt: string;
}) => void;
readonly onTryOpenBrowser: (url: string) => Promise<boolean>;
};

const POLL_INTERVAL_MS = 2000;
const SLOW_DOWN_INCREMENT_MS = 1000;
const MAX_POLL_INTERVAL_MS = 30_000;
const POLL_HARD_TIMEOUT_MS = 3 * 60 * 1000;
const CREATE_REQUEST_TIMEOUT_MS = 15_000;
const POLL_REQUEST_TIMEOUT_MS = 30_000;

/**
* Browser-mediated wizard sign-in.
*
* Endpoints (added by the braintrust-wizard-login-flow PR):
* POST {appUrl}/api/cli/wizard-signin/create
* GET {appUrl}/api/cli/wizard-signin/poll?id=...
* (Authorization: Bearer <poll_token>)
*
* The poll response is one of:
* { status: "pending", expires_at }
* { status: "expired" }
* { status: "claimed" }
* { status: "complete", api_key, org_info, project }
*/
export class WizardSigninAuthClient {
constructor(
private readonly appUrl: string,
private readonly clientName: string = "crank",
) {}

async createSession(): Promise<WizardSigninCreateResponse> {
const res = await fetch(`${this.appUrl}/api/cli/wizard-signin/create`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({ client_name: this.clientName }),
signal: AbortSignal.timeout(CREATE_REQUEST_TIMEOUT_MS),
});
if (!res.ok) {
throw new Error(
`Wizard sign-in create failed: ${res.status} ${await res.text()}`,
);
}
return (await res.json()) as WizardSigninCreateResponse;
}

async pollSession(args: {
readonly id: string;
readonly pollToken: string;
readonly sleep?: (ms: number) => Promise<void>;
}): Promise<WizardSigninCompleteResult> {
const sleep = args.sleep ?? defaultSleep;
let interval = POLL_INTERVAL_MS;
const deadline = Date.now() + POLL_HARD_TIMEOUT_MS;
while (Date.now() < deadline) {
await sleep(interval);
const url = `${this.appUrl}/api/cli/wizard-signin/poll?id=${encodeURIComponent(args.id)}`;
const res = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${args.pollToken}`,
Accept: "application/json",
},
signal: AbortSignal.timeout(POLL_REQUEST_TIMEOUT_MS),
});
if (res.status === 429) {
interval = Math.min(
interval + SLOW_DOWN_INCREMENT_MS,
MAX_POLL_INTERVAL_MS,
);
res.body?.cancel();
continue;
}
const json = (await res.json().catch(() => ({}))) as Record<
string,
unknown
>;
if (!res.ok) {
throw new Error(
`Wizard sign-in poll failed: ${res.status} ${JSON.stringify(json)}`,
);
}
const status = json["status"];
switch (status) {
case "pending":
continue;
case "expired":
throw new Error("Wizard sign-in session expired before approval.");
case "claimed":
throw new Error(
"Wizard sign-in session was already claimed by another client.",
);
case "complete":
return parseCompleteResponse(json);
default:
throw new Error(
`Unexpected wizard sign-in status: ${JSON.stringify(json)}`,
);
}
}
throw new Error("Wizard sign-in session timed out.");
}

async login(events: WizardSigninEvents): Promise<WizardSigninCompleteResult> {
const session = await this.createSession();
events.onLoginUrl({
loginUrl: session.login_url,
expiresAt: session.expires_at,
});
await events.onTryOpenBrowser(session.login_url);
return this.pollSession({
id: session.id,
pollToken: session.poll_token,
});
}
}

function parseCompleteResponse(
json: Record<string, unknown>,
): WizardSigninCompleteResult {
const apiKey = json["api_key"];
if (typeof apiKey !== "string" || apiKey.length === 0) {
throw new Error("Wizard sign-in completed without an api_key");
}
const orgInfo = parseOrgInfo(json["org_info"]);
const project = parseProject(json["project"]);
return { apiKey, orgInfo, project };
}

function parseOrgInfo(value: unknown): WizardSigninOrgInfo {
if (!isObject(value)) {
throw new Error("Wizard sign-in completed without org_info");
}
const id = value["id"];
const name = value["name"];
if (typeof id !== "string" || typeof name !== "string") {
throw new Error("Wizard sign-in org_info missing id/name");
}
return {
id,
name,
api_url: optionalString(value["api_url"]),
proxy_url: optionalString(value["proxy_url"]),
realtime_url: optionalString(value["realtime_url"]),
is_universal_api:
typeof value["is_universal_api"] === "boolean"
? value["is_universal_api"]
: null,
git_metadata: value["git_metadata"],
};
}

function parseProject(value: unknown): WizardSigninProject {
if (!isObject(value)) {
throw new Error("Wizard sign-in completed without project");
}
const id = value["id"];
const name = value["name"];
const orgId = value["org_id"];
if (
typeof id !== "string" ||
typeof name !== "string" ||
typeof orgId !== "string"
) {
throw new Error("Wizard sign-in project missing id/name/org_id");
}
return {
id,
name,
org_id: orgId,
description: optionalString(value["description"]),
};
}

function optionalString(value: unknown): string | null {
if (typeof value === "string") {
return value;
}
return null;
}

function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}

function defaultSleep(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading