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
1 change: 1 addition & 0 deletions packages/spark/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@clack/prompts": "1.3.0",
"@inquirer/search": "4.1.8",
"@tanstack/react-query": "5.100.9",
"chalk": "^5.6.2",
"clipboardy": "^5.3.1",
"ignore": "^7.0.5",
"ink": "7.0.2",
Expand Down
124 changes: 57 additions & 67 deletions packages/spark/src/clack-copy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { BraintrustCliContext } from "./braintrust-cli";
import chalk from "chalk";

const BRAINTRUST_CLI_CONTEXT_FALLBACKS = {
profile: "no profile",
Expand All @@ -18,9 +19,7 @@ export const CLACK_WIZARD_COPY = {
},

gitRepository: {
outsideRepoWarning:
"Warning: You are running this wizard inside a folder that is not a git repository. The wizard may edit files.",
continueOutsideRepoQuestion: "Continue without a git repository?",
outsideRepoWarning: `${chalk.yellow.bold("Warning:")} You are running this wizard inside a folder that is not a git repository. The wizard may edit files. ${chalk.bold("Continue without a git repository?")}`,
continueOutsideRepoChoices: {
yes: {
label: "Yes",
Expand Down Expand Up @@ -50,10 +49,16 @@ export const CLACK_WIZARD_COPY = {
readonly verificationCode: string;
}) =>
[
`Sign in: ${args.loginLink}`,
chalk.bold(
"Sign in to continue the setup. Your browser should have opened automatically.",
),
"",
"If your browser didn't open automatically, open the link above to sign in.",
`Verification code: ${args.verificationCode}`,
"",
chalk.dim(
"If your browser didn't open automatically, open the link below to sign in:\n",
),
chalk.dim(args.loginLink),
].join("\n"),
waitingForBrowser: "Waiting for you to sign in via the browser...",
browserSetupComplete: (args: {
Expand Down Expand Up @@ -88,18 +93,16 @@ export const CLACK_WIZARD_COPY = {
},
installing: "Installing Braintrust CLI...",
updating: "Updating Braintrust CLI...",
checkingContext: "Checking Braintrust CLI context...",
configuringContext: "Configuring Braintrust CLI context...",
checkingContext: "Checking Braintrust CLI login state...",
configuringContext: "Configuring Braintrust CLI login state...",
updateFailed: (message: string) =>
`Could not update Braintrust CLI: ${message}`,
installFailed: (message: string) =>
`Could not install Braintrust CLI: ${message}`,
configureFailed: (message: string) =>
`Could not configure Braintrust CLI: ${message}`,
installedButNotFound:
"Braintrust CLI was installed, but the wizard could not find `bt` in PATH or the default install location. Open a new shell and run `bt status` to verify it.",
statusFailed: (message: string) =>
`Could not inspect Braintrust CLI status; leaving existing CLI context unchanged. ${message}`,
"Braintrust CLI was installed, but the wizard could not find `bt` in PATH or the default install location. Install the CLI manually:\nhttps://www.braintrust.dev/docs/reference/cli/quickstart",
switchContextQuestion: (args: {
readonly currentContext: BraintrustCliContext;
readonly targetContext: BraintrustCliContext;
Expand Down Expand Up @@ -137,10 +140,9 @@ export const CLACK_WIZARD_COPY = {
},
},
builtIn: {
determiningAvailable: "Scanning for available coding agents...",
determiningAvailable: "Searching for available coding agents...",
running: (label: string) => `Running ${label}...`,
proceedQuestion:
"This setup wizard will now invoke a coding agent with full permissions. Proceed?",
proceedQuestion: `This setup wizard will now invoke a coding agent ${chalk.bold("with full permissions")}. Proceed?`,
proceedChoices: {
yes: {
label: "Confirm",
Expand Down Expand Up @@ -176,92 +178,80 @@ export const CLACK_WIZARD_COPY = {
complete: "Instrumentation complete.",
toolFinished: (toolLabel: string) => `${toolLabel} finished.`,
},
localToken: {
title: "Local application token",
notice:
"The wizard will now create .env.braintrust and .braintrust.json files that are used to authenticate your application to Braintrust. They will be used for local testing.",
existingNotice:
"A local Braintrust token file already exists. The wizard can replace local token files with the API key for this Braintrust project.",
replaceQuestion: "Replace local Braintrust token files?",
replaceChoices: {
yes: {
label: "Yes (recommended)",
hint: "Use this project key",
},
no: {
label: "No",
hint: "Keep existing file",
},
},
outsideGitRepo: (apiKey: string) =>
`BRAINTRUST_API_KEY=${apiKey}\nNot in a git repo — set this in your environment manually.`,
keptTokenFiles: () =>
"Kept existing local Braintrust token files unchanged.",
gitignoreNote: (args: {
readonly added: boolean;
readonly alreadyCovered: boolean;
}) => {
if (args.added) {
return "Updated .gitignore for local Braintrust token files.";
}
if (args.alreadyCovered) {
return undefined;
}
return ".gitignore unchanged.";
},
},
manual: {
title: "Manual instrumentation",
note: (docsLink: string) =>
completedQuestion: (docsLink: string) =>
[
"Follow the Braintrust instrumentation docs for your project.",
"Follow the Braintrust instrumentation docs for your project:",
chalk.cyanBright(docsLink),
"",
docsLink,
chalk.bold(
"Did you complete setting up Braintrust by following the docs?\n",
),
].join("\n"),
completedQuestion: "Braintrust instrumentation completed?",
completedChoices: {
confirm: {
label: "confirm",
hint: "Continue setup",
label: "Confirm",
hint: "Press Enter to continue",
},
},
},
ownAgent: {
deliveryQuestion:
"How should Braintrust Setup deliver the instrumentation prompt?",
"How do you want to receive the prompt for your coding agent?",
copyToClipboard: "Copy to clipboard",
printToTerminal: "Print to terminal",
copiedToClipboard: "Copied instrumentation prompt to clipboard.",
clipboardFailed: (message: string) =>
`Could not copy the instrumentation prompt to the clipboard: ${message}`,
completedQuestion:
"Give the above prompt to your coding agent and proceed when the agent has completed the task.",
"Paste the above prompt into your coding agent. Press enter and proceed when the agent has completed the task.",
completedChoices: {
confirm: {
label: "Confirm and proceed",
hint: "Continue setup",
hint: "Press Enter to continue",
},
},
},
},

logs: {
projectLogsUrl: (url: string) => `Check your Braintrust logs: ${url}`,
checkQuestion: (url: string) =>
[
"Your application should now be instrumented with Braintrust tracing.",
"",
chalk.bold(
"Please run your app locally now, and invoke AI functionality to confirm whether AI calls are logged and traced.",
),
"",
`If everything is set up correctly, traces will appear in your Braintrust logs:\n${chalk.cyanBright(url)}`,
"",
chalk.dim(
`If traces are not showing up, visit the troubleshooting guide:\nhttps://www.braintrust.dev/docs/kb/troubleshooting-guides\n`,
),
].join("\n"),
checked: "I've confirmed my application is sending traces.",
hint: "Press Enter to continue",
},

productionToken: {
title: "Production token",
noteWithEnvFile: (envFilePath: string) =>
`The local Braintrust token files contain a BRAINTRUST_API_KEY token. Add that token to your deployment platform's environment variables so tracing works in production.\n\nEnv file: ${envFilePath}`,
noteWithoutEnvFile:
"Add the BRAINTRUST_API_KEY token to your deployment platform's environment variables so tracing works in production.",
question: "Have you added BRAINTRUST_API_KEY to your deployment platform?",
understood: "Understood",
question: `Production Setup: Add the ${chalk.cyanBright("BRAINTRUST_API_KEY")} token from your local ${chalk.bold("./.env.braintrust")} file to your production environment as environment variable.\n`,
confirmed: `I have added ${chalk.bold("BRAINTRUST_API_KEY")} to my production env.`,
hint: "Press Enter to continue",
},

outro: {
complete: (docsUrl: string) =>
["Setup complete.", "", `Docs: ${docsUrl}`].join("\n"),
complete: [
chalk.dim("Braintrust setup complete."),
"",
"You can now use Braintrust in production.",
"",
"If you encountered any issues during setup, please open an issue at https://github.com/braintrustdata/spark/issues/new.",
"",
chalk.dim("- Contact support: https://www.braintrust.dev/contact"),
chalk.dim(
"- Further documentation: https://www.braintrust.dev/docs/instrument",
),
].join("\n"),
},
} as const;

Expand Down
104 changes: 32 additions & 72 deletions packages/spark/src/clack-wizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,11 @@ import {
type CodingToolRunResult,
type CodingToolStatus,
} from "./coding-tools";
import {
braintrustTokenFilesExist,
ensureEnvBraintrustIgnored,
isGitRepo,
writeEnvBraintrust,
} from "./git";
import { isGitRepo, writeEnvBraintrust } from "./git";
import { allocateResultFile, readResultFile } from "./instrument";
import type { WizardOptions } from "./options";
import { renderPrompt } from "./prompt";
import { ClackToolRenderer } from "./tool-ui";
import { terminalHyperlink } from "./wizard-utils";

const COPY = CLACK_WIZARD_COPY;
const WIZARD_CANCEL_MESSAGE = COPY.shared.cancelMessage;
Expand Down Expand Up @@ -196,9 +190,8 @@ export async function runClackWizard(deps: WizardDeps): Promise<WizardResult> {
clack.intro(COPY.welcome.intro);

if (!(await isGitRepo(deps.cwd))) {
clack.log.warn(COPY.gitRepository.outsideRepoWarning);
const continueOutsideGit = await selectBoolean({
message: COPY.gitRepository.continueOutsideRepoQuestion,
message: COPY.gitRepository.outsideRepoWarning,
choices: COPY.gitRepository.continueOutsideRepoChoices,
yesFirst: false,
});
Expand All @@ -219,7 +212,7 @@ export async function runClackWizard(deps: WizardDeps): Promise<WizardResult> {
authMode: (await hasBraintrustAccount()) ? "signin" : "signup",
});

const envFilePath = await writeLocalEnvBraintrust(deps, session.apiKey);
await writeLocalEnvBraintrust(deps, session.apiKey);

const setupSpinner = new WizardStepSpinner();
let codingToolStatuses: readonly CodingToolStatus[];
Expand Down Expand Up @@ -286,11 +279,11 @@ export async function runClackWizard(deps: WizardDeps): Promise<WizardResult> {
}

const projectLogsUrl = `${deps.options.appUrl}/app/${encodeURIComponent(session.orgName)}/p/${encodeURIComponent(session.projectName)}/logs`;
clack.log.info(COPY.logs.projectLogsUrl(projectLogsUrl));
await confirmTraceLogs(projectLogsUrl);

await confirmProductionApiKey(envFilePath);
await confirmProductionApiKey();

clack.outro(COPY.outro.complete(COPY.shared.instrumentationDocsUrl));
clack.outro(COPY.outro.complete);

return {
orgName: session.orgName,
Expand Down Expand Up @@ -460,11 +453,8 @@ async function handleBraintrustCliSetup(
spinner.update(COPY.braintrustCli.checkingContext);
try {
currentContext = await deps.braintrustCli.status(commandPath);
} catch (error) {
} catch {
spinner.clear();
clack.log.warn(
COPY.braintrustCli.statusFailed(summarizeBraintrustCliError(error)),
);
return;
}

Expand Down Expand Up @@ -604,55 +594,17 @@ function warnNoUsableCodingTools(statuses: readonly CodingToolStatus[]): void {
async function writeLocalEnvBraintrust(
deps: WizardDeps,
apiKey: string,
): Promise<string | undefined> {
const targetDirectory = deps.cwd;
if (await braintrustTokenFilesExist(targetDirectory)) {
clack.note(
COPY.instrumentation.localToken.existingNotice,
COPY.instrumentation.localToken.title,
);
const shouldReplace = await selectBoolean({
message: COPY.instrumentation.localToken.replaceQuestion,
choices: COPY.instrumentation.localToken.replaceChoices,
yesFirst: true,
});
if (!shouldReplace) {
const gitignoreResult = await ensureEnvBraintrustIgnored(targetDirectory);
const gitignoreNote = COPY.instrumentation.localToken.gitignoreNote({
added: gitignoreResult.addedToGitignore,
alreadyCovered: gitignoreResult.alreadyCovered,
});
clack.log.info(COPY.instrumentation.localToken.keptTokenFiles());
if (gitignoreNote) clack.log.info(gitignoreNote);
return undefined;
}
} else {
clack.note(
COPY.instrumentation.localToken.notice,
COPY.instrumentation.localToken.title,
);
}

const result = await writeEnvBraintrust(targetDirectory, apiKey);
const envFilePath = relative(targetDirectory, result.envFilePath);
const gitignoreNote = COPY.instrumentation.localToken.gitignoreNote({
added: result.addedToGitignore,
alreadyCovered: result.alreadyCovered,
});
if (gitignoreNote) clack.log.info(gitignoreNote);
return envFilePath;
): Promise<string> {
const result = await writeEnvBraintrust(deps.cwd, apiKey);
return relative(deps.cwd, result.envFilePath);
}

async function confirmManualInstrumentation(): Promise<void> {
clack.note(
COPY.instrumentation.manual.note(
terminalHyperlink(COPY.shared.instrumentationDocsUrl),
),
COPY.instrumentation.manual.title,
);
unwrap(
await clack.select<"confirm">({
message: COPY.instrumentation.manual.completedQuestion,
message: COPY.instrumentation.manual.completedQuestion(
COPY.shared.instrumentationDocsUrl,
),
options: [
{
label: COPY.instrumentation.manual.completedChoices.confirm.label,
Expand Down Expand Up @@ -722,22 +674,30 @@ function printInstrumentationPrompt(promptText: string): void {
process.stdout.write(`\n${promptText}\n\n`);
}

async function confirmProductionApiKey(
envFilePath: string | undefined,
): Promise<void> {
clack.note(
envFilePath
? COPY.productionToken.noteWithEnvFile(envFilePath)
: COPY.productionToken.noteWithoutEnvFile,
COPY.productionToken.title,
async function confirmTraceLogs(projectLogsUrl: string): Promise<void> {
unwrap(
await clack.select<"checked">({
message: COPY.logs.checkQuestion(projectLogsUrl),
options: [
{
label: COPY.logs.checked,
value: "checked",
hint: COPY.logs.hint,
},
],
}),
);
}

async function confirmProductionApiKey(): Promise<void> {
unwrap(
await clack.select<"understood">({
await clack.select<"confirmed">({
message: COPY.productionToken.question,
options: [
{
label: COPY.productionToken.understood,
value: "understood",
label: COPY.productionToken.confirmed,
value: "confirmed",
hint: COPY.productionToken.hint,
},
],
}),
Expand Down
2 changes: 1 addition & 1 deletion packages/spark/src/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export async function isGitRepo(cwd: string): Promise<boolean> {
const ENV_FILENAME = ".env.braintrust";
const BRAINTRUST_JSON_FILENAME = ".braintrust.json";
const GENERATED_FILE_COMMENT =
"This file was generated by the Braintrust CLI wizard. This file contains sensitive information. Do not commit this file to version control! The file can be safely deleted after confirming your application sends traces.";
"This file was generated by the Braintrust wizard. This file contains sensitive information. Do not commit this file to version control! The file can be safely deleted after confirming your application sends traces.";
const LOCAL_TOKEN_FILENAMES = [ENV_FILENAME, BRAINTRUST_JSON_FILENAME] as const;

export function envBraintrustPath(directory: string): string {
Expand Down
Loading
Loading