Compare commits

...

2 Commits

Author SHA1 Message Date
Chris Wanstrath
9b5b0bc1b6 9 2026-02-25 15:53:24 -08:00
Chris Wanstrath
1d55cf427e no oauth 2026-02-25 15:52:22 -08:00
4 changed files with 22 additions and 22 deletions

View File

@ -1,6 +1,6 @@
{
"name": "@because/sandlot",
"version": "0.0.8",
"version": "0.0.9",
"description": "Sandboxed, branch-based development with Claude",
"type": "module",
"bin": {

View File

@ -6,7 +6,7 @@ import * as vm from "../vm.ts"
import * as state from "../state.ts"
import { spinner } from "../spinner.ts"
import { die } from "../fmt.ts"
import { getApiKey } from "../env.ts"
import { requireApiKey } from "../env.ts"
import { renderMarkdown } from "../markdown.ts"
import { saveChanges } from "./helpers.ts"
@ -21,8 +21,7 @@ function fallbackBranchName(text: string): string {
}
async function branchFromPrompt(text: string): Promise<string> {
const apiKey = await getApiKey()
if (!apiKey) return fallbackBranchName(text)
const apiKey = await requireApiKey()
try {
const res = await fetch("https://api.anthropic.com/v1/messages", {

View File

@ -1,5 +1,6 @@
import { homedir } from "os"
import { join } from "path"
import { die } from "./fmt.ts"
/** Read the ANTHROPIC_API_KEY from ~/.env. Returns undefined if not found. */
export async function getApiKey(): Promise<string | undefined> {
@ -9,3 +10,10 @@ export async function getApiKey(): Promise<string | undefined> {
const envContent = await envFile.text()
return envContent.match(/^(?:export\s+)?ANTHROPIC_API_KEY=["']?([^"'\s]+)["']?/m)?.[1]
}
/** Read the ANTHROPIC_API_KEY from ~/.env, dying if not found. */
export async function requireApiKey(): Promise<string> {
const key = await getApiKey()
if (!key) die("ANTHROPIC_API_KEY not found in ~/.env")
return key
}

View File

@ -2,7 +2,7 @@ import { $ } from "bun"
import { existsSync } from "fs"
import { homedir } from "os"
import { dirname, join } from "path"
import { getApiKey } from "./env.ts"
import { requireApiKey } from "./env.ts"
import { info } from "./fmt.ts"
const DEBUG = !!process.env.DEBUG
@ -145,35 +145,27 @@ async function installTooling(cached: boolean, log?: (msg: string) => void): Pro
}
/** Configure git identity, API key helper, activity hook, and Claude settings. */
async function configureEnvironment(home: string, log?: (msg: string) => void, apiKey?: string): Promise<void> {
async function configureEnvironment(home: string, apiKey: string): Promise<void> {
const gitName = (await $`git config user.name`.quiet().text()).trim()
const gitEmail = (await $`git config user.email`.quiet().text()).trim()
if (gitName) await $`container exec --user ${USER} ${CONTAINER_NAME} git config --global user.name ${gitName}`.quiet()
if (gitEmail) await $`container exec --user ${USER} ${CONTAINER_NAME} git config --global user.email ${gitEmail}`.quiet()
if (!apiKey) {
log?.("Warning: ANTHROPIC_API_KEY not found in ~/.env — claude will require manual login")
}
const activityBin = `/home/${USER}/.local/bin/sandlot-activity`
const hooks = {
UserPromptSubmit: [{ hooks: [{ type: "command", command: `${activityBin} active` }] }],
Stop: [{ hooks: [{ type: "command", command: `${activityBin} idle` }] }],
}
const settingsJson = JSON.stringify(apiKey
? { apiKeyHelper: "~/.claude/api-key-helper.sh", skipDangerousModePermissionPrompt: true, hooks }
: { skipDangerousModePermissionPrompt: true, hooks })
const settingsJson = JSON.stringify({ apiKeyHelper: "~/.claude/api-key-helper.sh", skipDangerousModePermissionPrompt: true, hooks })
const claudeJson = JSON.stringify({ hasCompletedOnboarding: true, effortCalloutDismissed: true, projects: { "/": { hasTrustDialogAccepted: true } } })
// Write the helper script to a temp file and copy it in so the key
// never appears in a process argument visible in `ps`.
if (apiKey) {
const tmp = `${home}/.sandlot/.api-key-helper.tmp`
await Bun.write(tmp, `#!/bin/sh\necho '${apiKey.replace(/'/g, "'\\''")}'\n`)
await $`chmod +x ${tmp}`.quiet()
await $`container exec --user ${USER} ${CONTAINER_NAME} bash -c ${"mkdir -p ~/.claude && cp /sandlot/.api-key-helper.tmp ~/.claude/api-key-helper.sh"}`.quiet()
await Bun.file(tmp).unlink()
}
// Install activity tracking hook script
const activityTmp = `${home}/.sandlot/.sandlot-activity.tmp`
@ -199,6 +191,7 @@ echo '${claudeJson}' > ~/.claude.json
/** Create and provision the container from scratch. Fails if it already exists. */
export async function create(log?: (msg: string) => void): Promise<void> {
requireContainer()
const apiKey = await requireApiKey()
const s = await status()
if (s !== "missing") {
@ -219,8 +212,7 @@ export async function create(log?: (msg: string) => void): Promise<void> {
await installTooling(cached, log)
log?.("Configuring environment")
const apiKey = await getApiKey()
await configureEnvironment(home, log, apiKey)
await configureEnvironment(home, apiKey)
}
/** Start a stopped container. */
@ -235,6 +227,7 @@ export async function start(): Promise<void> {
/** Ensure the sandlot container exists and is running. Creates and provisions on first use. */
export async function ensure(log?: (msg: string) => void): Promise<void> {
requireContainer()
await requireApiKey()
// Ensure the container daemon is running (--enable-kernel-install skips interactive prompt)
if (DEBUG) await $`container system start --enable-kernel-install`.nothrow()