Compare commits
11 Commits
cfb9ed6172
...
b46511efe3
| Author | SHA1 | Date | |
|---|---|---|---|
| b46511efe3 | |||
| 33e47a802c | |||
| 6114540764 | |||
| 6ec203e67f | |||
| 5b9998b138 | |||
| 2eba71ce87 | |||
| 2d0940f7c6 | |||
| 4cde0c2086 | |||
| 10ebfcf754 | |||
| c2cf0518e7 | |||
| 89eb5e382b |
4
bun.lock
4
bun.lock
|
|
@ -5,7 +5,7 @@
|
||||||
"": {
|
"": {
|
||||||
"name": "sandlot",
|
"name": "sandlot",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"commander": "^13.1.0",
|
"commander": "^14.0.3",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.3.9",
|
"@types/bun": "^1.3.9",
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.9", "https://npm.nose.space/bun-types/-/bun-types-1.3.9.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
|
"bun-types": ["bun-types@1.3.9", "https://npm.nose.space/bun-types/-/bun-types-1.3.9.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
|
||||||
|
|
||||||
"commander": ["commander@13.1.0", "https://npm.nose.space/commander/-/commander-13.1.0.tgz", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="],
|
"commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@5.26.5", "https://npm.nose.space/undici-types/-/undici-types-5.26.5.tgz", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
"undici-types": ["undici-types@5.26.5", "https://npm.nose.space/undici-types/-/undici-types-5.26.5.tgz", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
"sandlot": "./src/cli.ts"
|
"sandlot": "./src/cli.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"commander": "^13.1.0"
|
"commander": "^14.0.3"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test:markdown": "bun src/test-markdown.ts"
|
"test:markdown": "bun src/test-markdown.ts"
|
||||||
|
|
|
||||||
144
src/cli.ts
144
src/cli.ts
|
|
@ -1,6 +1,7 @@
|
||||||
#!/usr/bin/env bun
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
import { Command } from "commander"
|
import { Command, Option } from "commander"
|
||||||
|
import { yellow, reset } from "./fmt.ts"
|
||||||
import * as git from "./git.ts"
|
import * as git from "./git.ts"
|
||||||
import * as state from "./state.ts"
|
import * as state from "./state.ts"
|
||||||
import { action as newAction } from "./commands/new.ts"
|
import { action as newAction } from "./commands/new.ts"
|
||||||
|
|
@ -25,9 +26,26 @@ const pkg = await Bun.file(new URL("../package.json", import.meta.url)).json()
|
||||||
|
|
||||||
const program = new Command()
|
const program = new Command()
|
||||||
|
|
||||||
program.name("sandlot").description("Branch-based development with git worktrees and Apple Container").version(pkg.version)
|
program
|
||||||
|
.name("sandlot")
|
||||||
|
.description("Sandboxed development with Claude.")
|
||||||
|
.configureHelp({ styleTitle: (str) => `${yellow}${str}${reset}` })
|
||||||
|
.helpOption(false)
|
||||||
|
.addOption(new Option("-h, --help").hideHelp())
|
||||||
|
.on("option:help", () => program.help())
|
||||||
|
.addOption(new Option("-V, --version").hideHelp())
|
||||||
|
.on("option:version", () => {
|
||||||
|
console.log(pkg.version)
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
|
||||||
// ── sandlot new ──────────────────────────────────────────────────────
|
// ── Sessions ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("list")
|
||||||
|
.description("Show all active sessions")
|
||||||
|
.option("--json", "Output as JSON")
|
||||||
|
.action(listAction)
|
||||||
|
|
||||||
program
|
program
|
||||||
.command("new")
|
.command("new")
|
||||||
|
|
@ -38,44 +56,15 @@ program
|
||||||
.description("Create a new session and launch Claude")
|
.description("Create a new session and launch Claude")
|
||||||
.action(newAction)
|
.action(newAction)
|
||||||
|
|
||||||
// ── sandlot list ─────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
program
|
|
||||||
.command("list")
|
|
||||||
.description("Show all active sessions (◌ idle · ◯ working · ◎ unsaved · ● saved)")
|
|
||||||
.option("--json", "Output as JSON")
|
|
||||||
.action(listAction)
|
|
||||||
|
|
||||||
// ── sandlot open ─────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
program
|
program
|
||||||
.command("open")
|
.command("open")
|
||||||
.argument("<branch>", "branch name")
|
.argument("<branch>", "branch name")
|
||||||
.argument("[prompt]", "initial prompt for Claude")
|
.argument("[prompt]", "initial prompt for Claude")
|
||||||
.option("-p, --print <prompt>", "run Claude in non-interactive mode with -p")
|
.option("-p, --print <prompt>", "run Claude in non-interactive mode with -p")
|
||||||
.option("-n, --no-save", "skip auto-save after Claude exits")
|
.option("-n, --no-save", "skip auto-save after Claude exits")
|
||||||
.description("Re-enter an existing session")
|
.description("Open an existing Claude session")
|
||||||
.action(openAction)
|
.action(openAction)
|
||||||
|
|
||||||
// ── sandlot review ───────────────────────────────────────────────────
|
|
||||||
|
|
||||||
program
|
|
||||||
.command("review")
|
|
||||||
.argument("<branch>", "branch name")
|
|
||||||
.option("-p, --print", "print the review to stdout instead of launching interactive mode")
|
|
||||||
.description("Launch an interactive grumpy code review for a branch")
|
|
||||||
.action(reviewAction)
|
|
||||||
|
|
||||||
// ── sandlot shell ────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
program
|
|
||||||
.command("shell")
|
|
||||||
.argument("[branch]", "branch name (omit for a plain VM shell)")
|
|
||||||
.description("Open a shell in the VM (at the session's worktree if branch given)")
|
|
||||||
.action(shellAction)
|
|
||||||
|
|
||||||
// ── sandlot close ────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
program
|
program
|
||||||
.command("close")
|
.command("close")
|
||||||
.argument("<branch>", "branch name")
|
.argument("<branch>", "branch name")
|
||||||
|
|
@ -90,31 +79,27 @@ program
|
||||||
.description("Remove a session (alias for close)")
|
.description("Remove a session (alias for close)")
|
||||||
.action((branch: string, opts: { force?: boolean }) => closeAction(branch, opts))
|
.action((branch: string, opts: { force?: boolean }) => closeAction(branch, opts))
|
||||||
|
|
||||||
// ── sandlot merge ────────────────────────────────────────────────────
|
// ── Branch ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
program.commandsGroup("Branch Commands:")
|
||||||
|
|
||||||
program
|
program
|
||||||
.command("merge")
|
.command("diff")
|
||||||
.argument("<branch>", "branch name")
|
.argument("<branch>", "branch name")
|
||||||
.description("Merge a branch into main and close the session")
|
.description("Show uncommitted changes, or full branch diff vs main")
|
||||||
.action(mergeAction)
|
.action(diffAction)
|
||||||
|
|
||||||
// ── sandlot squash ───────────────────────────────────────────────────
|
|
||||||
|
|
||||||
program
|
program
|
||||||
.command("squash")
|
.command("log")
|
||||||
.argument("<branch>", "branch name")
|
.argument("<branch>", "branch name")
|
||||||
.description("Squash-merge a branch into main and close the session")
|
.description("Show commits on a branch that are not on main")
|
||||||
.action(squashAction)
|
.action(logAction)
|
||||||
|
|
||||||
// ── sandlot rebase ───────────────────────────────────────────────────
|
|
||||||
|
|
||||||
program
|
program
|
||||||
.command("rebase")
|
.command("show")
|
||||||
.argument("<branch>", "branch name")
|
.argument("<branch>", "branch name")
|
||||||
.description("Rebase a branch onto the latest main")
|
.description("Show the prompt and full diff for a branch")
|
||||||
.action(rebaseAction)
|
.action(showAction)
|
||||||
|
|
||||||
// ── sandlot save ─────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
program
|
program
|
||||||
.command("save")
|
.command("save")
|
||||||
|
|
@ -123,31 +108,36 @@ program
|
||||||
.description("Stage all changes and commit")
|
.description("Stage all changes and commit")
|
||||||
.action(saveAction)
|
.action(saveAction)
|
||||||
|
|
||||||
// ── sandlot diff ─────────────────────────────────────────────────────
|
program
|
||||||
|
.command("merge")
|
||||||
|
.argument("<branch>", "branch name")
|
||||||
|
.description("Merge a branch into main and close the session")
|
||||||
|
.action(mergeAction)
|
||||||
|
|
||||||
program
|
program
|
||||||
.command("diff")
|
.command("squash")
|
||||||
.argument("<branch>", "branch name")
|
.argument("<branch>", "branch name")
|
||||||
.description("Show uncommitted changes, or full branch diff vs main")
|
.description("Squash-merge a branch into main and close the session")
|
||||||
.action(diffAction)
|
.action(squashAction)
|
||||||
|
|
||||||
// ── sandlot show ─────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
program
|
program
|
||||||
.command("show")
|
.command("rebase")
|
||||||
.argument("<branch>", "branch name")
|
.argument("<branch>", "branch name")
|
||||||
.description("Show the prompt and full diff for a branch (for code review)")
|
.description("Rebase a branch onto the latest main")
|
||||||
.action(showAction)
|
.action(rebaseAction)
|
||||||
|
|
||||||
// ── sandlot log ──────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
program
|
program
|
||||||
.command("log")
|
.command("review")
|
||||||
.argument("<branch>", "branch name")
|
.argument("<branch>", "branch name")
|
||||||
.description("Show commits on a branch that are not on main")
|
.option("-p, --print", "print the review to stdout instead of launching interactive mode")
|
||||||
.action(logAction)
|
.description("Launch an interactive grumpy code review for a branch")
|
||||||
|
.action(reviewAction)
|
||||||
|
|
||||||
// ── sandlot dir ──────────────────────────────────────────────────────
|
program
|
||||||
|
.command("shell")
|
||||||
|
.argument("[branch]", "branch name (omit for a plain VM shell)")
|
||||||
|
.description("Open a shell in the VM")
|
||||||
|
.action(shellAction)
|
||||||
|
|
||||||
program
|
program
|
||||||
.command("dir")
|
.command("dir")
|
||||||
|
|
@ -155,18 +145,23 @@ program
|
||||||
.description("Print the worktree path for a session")
|
.description("Print the worktree path for a session")
|
||||||
.action(dirAction)
|
.action(dirAction)
|
||||||
|
|
||||||
// ── sandlot cleanup ──────────────────────────────────────────────────
|
// ── Admin ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
program.commandsGroup("Admin Commands:")
|
||||||
|
|
||||||
program
|
program
|
||||||
.command("cleanup")
|
.command("cleanup")
|
||||||
.description("Remove stale sessions whose worktrees no longer exist")
|
.description("Remove stale sessions whose worktrees no longer exist")
|
||||||
.action(cleanupAction)
|
.action(cleanupAction)
|
||||||
|
|
||||||
// ── sandlot vm ───────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
registerVmCommands(program)
|
registerVmCommands(program)
|
||||||
|
|
||||||
// ── sandlot completions ──────────────────────────────────────────────
|
program
|
||||||
|
.command("version")
|
||||||
|
.description("Print the version number")
|
||||||
|
.action(() => {
|
||||||
|
console.log(pkg.version)
|
||||||
|
})
|
||||||
|
|
||||||
program
|
program
|
||||||
.command("completions")
|
.command("completions")
|
||||||
|
|
@ -174,17 +169,10 @@ program
|
||||||
.description("Output fish shell completions")
|
.description("Output fish shell completions")
|
||||||
.action((opts: { install?: boolean }) => completionsAction(program, opts))
|
.action((opts: { install?: boolean }) => completionsAction(program, opts))
|
||||||
|
|
||||||
// ── Default: show list if sessions exist, otherwise help ─────────────
|
// ── Default: `sandlot` → `sandlot list` ─────────────────────────────
|
||||||
|
|
||||||
const args = process.argv.slice(2)
|
if (process.argv.length === 2) {
|
||||||
if (args.length === 0) {
|
process.argv.push("list")
|
||||||
try {
|
|
||||||
const root = await git.repoRoot()
|
|
||||||
const st = await state.load(root)
|
|
||||||
if (Object.keys(st.sessions).length > 0) {
|
|
||||||
process.argv.push("list")
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
program.parseAsync().catch((err) => {
|
program.parseAsync().catch((err) => {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { $ } from "bun"
|
import { $ } from "bun"
|
||||||
import * as git from "../git.ts"
|
import * as git from "../git.ts"
|
||||||
import { pager } from "../fmt.ts"
|
|
||||||
import { requireSession } from "./helpers.ts"
|
import { requireSession } from "./helpers.ts"
|
||||||
|
|
||||||
export async function action(branch: string) {
|
export async function action(branch: string) {
|
||||||
|
|
@ -13,27 +12,26 @@ export async function action(branch: string) {
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
let diff: string
|
let args: string[]
|
||||||
if (status.text().trim().length > 0) {
|
if (status.text().trim().length > 0) {
|
||||||
// Show uncommitted changes (both staged and unstaged)
|
// Show uncommitted changes (both staged and unstaged)
|
||||||
const result = await $`git -C ${session.worktree} diff --color=always HEAD`.nothrow().quiet()
|
const hasHead = await $`git -C ${session.worktree} rev-parse --verify HEAD`.nothrow().quiet()
|
||||||
if (result.exitCode !== 0) {
|
args = hasHead.exitCode === 0 ? ["diff", "HEAD"] : ["diff"]
|
||||||
// HEAD may not exist yet (no commits); fall back to showing all tracked + untracked
|
|
||||||
const fallback = await $`git -C ${session.worktree} diff --color=always`.nothrow().quiet()
|
|
||||||
diff = fallback.text()
|
|
||||||
} else {
|
|
||||||
diff = result.text()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// No uncommitted changes — show full branch diff vs main
|
// No uncommitted changes — show full branch diff vs main
|
||||||
const main = await git.mainBranch(session.worktree)
|
const main = await git.mainBranch(session.worktree)
|
||||||
const result = await $`git -C ${session.worktree} diff --color=always ${main}...${branch}`.nothrow().quiet()
|
args = ["diff", `${main}...${branch}`]
|
||||||
if (result.exitCode !== 0) {
|
|
||||||
console.error("✖ git diff failed")
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
diff = result.text()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await pager(diff)
|
// Run git diff with inherited stdio so external diff tools (e.g. difftastic)
|
||||||
|
// see a real TTY and git can use its own pager
|
||||||
|
const proc = Bun.spawn(["git", "-C", session.worktree, ...args], {
|
||||||
|
stdin: "inherit",
|
||||||
|
stdout: "inherit",
|
||||||
|
stderr: "inherit",
|
||||||
|
})
|
||||||
|
const exitCode = await proc.exited
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
process.exit(exitCode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -105,10 +105,10 @@ export async function action(
|
||||||
|
|
||||||
if (opts.print) {
|
if (opts.print) {
|
||||||
spin.text = "Running prompt…"
|
spin.text = "Running prompt…"
|
||||||
const output = await vm.claude(worktreeAbs, { prompt, print: opts.print })
|
const result = await vm.claude(worktreeAbs, { prompt, print: opts.print })
|
||||||
if (output) {
|
if (result.output) {
|
||||||
spin.stop()
|
spin.stop()
|
||||||
process.stdout.write(renderMarkdown(output) + "\n")
|
process.stdout.write(renderMarkdown(result.output) + "\n")
|
||||||
} else {
|
} else {
|
||||||
spin.succeed("Done")
|
spin.succeed("Done")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,10 @@ export async function action(
|
||||||
|
|
||||||
if (opts.print) {
|
if (opts.print) {
|
||||||
spin.text = "Running prompt…"
|
spin.text = "Running prompt…"
|
||||||
const output = await vm.claude(session.worktree, { prompt, print: opts.print })
|
const result = await vm.claude(session.worktree, { prompt, print: opts.print, continue: true })
|
||||||
if (output) {
|
if (result.output) {
|
||||||
spin.stop()
|
spin.stop()
|
||||||
process.stdout.write(renderMarkdown(output) + "\n")
|
process.stdout.write(renderMarkdown(result.output) + "\n")
|
||||||
} else {
|
} else {
|
||||||
spin.succeed("Done")
|
spin.succeed("Done")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,9 @@ export async function action(branch: string, opts: { print?: boolean }) {
|
||||||
|
|
||||||
if (opts.print) {
|
if (opts.print) {
|
||||||
spin.text = "Running review…"
|
spin.text = "Running review…"
|
||||||
const output = await vm.claude(session.worktree, { print: prompt })
|
const result = await vm.claude(session.worktree, { print: prompt })
|
||||||
spin.stop()
|
spin.stop()
|
||||||
if (output) process.stdout.write(output + "\n")
|
if (result.output) process.stdout.write(result.output + "\n")
|
||||||
} else {
|
} else {
|
||||||
spin.succeed("Session ready")
|
spin.succeed("Session ready")
|
||||||
await vm.claude(session.worktree, { prompt })
|
await vm.claude(session.worktree, { prompt })
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,24 @@
|
||||||
import { $ } from "bun"
|
|
||||||
import * as git from "../git.ts"
|
import * as git from "../git.ts"
|
||||||
import { pager } from "../fmt.ts"
|
|
||||||
import { requireSession } from "./helpers.ts"
|
import { requireSession } from "./helpers.ts"
|
||||||
|
|
||||||
export async function action(branch: string) {
|
export async function action(branch: string) {
|
||||||
const { session } = await requireSession(branch)
|
const { session } = await requireSession(branch)
|
||||||
|
|
||||||
const main = await git.mainBranch(session.worktree)
|
|
||||||
const result = await $`git -C ${session.worktree} diff --color=always ${main}...${branch}`.nothrow().quiet()
|
|
||||||
if (result.exitCode !== 0) {
|
|
||||||
console.error("git diff failed")
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
let output = ""
|
|
||||||
if (session.prompt) {
|
if (session.prompt) {
|
||||||
output += `PROMPT: ${session.prompt}\n\n`
|
process.stderr.write(`PROMPT: ${session.prompt}\n\n`)
|
||||||
}
|
}
|
||||||
output += result.text()
|
|
||||||
|
|
||||||
await pager(output)
|
const main = await git.mainBranch(session.worktree)
|
||||||
|
|
||||||
|
// Run git diff with inherited stdio so external diff tools (e.g. difftastic)
|
||||||
|
// see a real TTY and git can use its own pager
|
||||||
|
const proc = Bun.spawn(["git", "-C", session.worktree, "diff", `${main}...${branch}`], {
|
||||||
|
stdin: "inherit",
|
||||||
|
stdout: "inherit",
|
||||||
|
stderr: "inherit",
|
||||||
|
})
|
||||||
|
const exitCode = await proc.exited
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
process.exit(exitCode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ export function register(program: Command) {
|
||||||
.description("Show VM system info (via neofetch)")
|
.description("Show VM system info (via neofetch)")
|
||||||
.action(async () => {
|
.action(async () => {
|
||||||
await vm.ensure()
|
await vm.ensure()
|
||||||
await vm.info()
|
await vm.neofetch()
|
||||||
})
|
})
|
||||||
|
|
||||||
vmCmd
|
vmCmd
|
||||||
|
|
|
||||||
22
src/vm.ts
22
src/vm.ts
|
|
@ -2,6 +2,7 @@ import { $ } from "bun"
|
||||||
import { homedir } from "os"
|
import { homedir } from "os"
|
||||||
import { dirname, join } from "path"
|
import { dirname, join } from "path"
|
||||||
import { getApiKey } from "./env.ts"
|
import { getApiKey } from "./env.ts"
|
||||||
|
import { info } from "./fmt.ts"
|
||||||
|
|
||||||
const CONTAINER_NAME = "sandlot"
|
const CONTAINER_NAME = "sandlot"
|
||||||
const USER = "ubuntu"
|
const USER = "ubuntu"
|
||||||
|
|
@ -201,7 +202,7 @@ export async function start(): Promise<void> {
|
||||||
const s = await status()
|
const s = await status()
|
||||||
if (s === "running") return
|
if (s === "running") return
|
||||||
if (s === "missing") throw new Error("Container does not exist. Use 'sandlot vm create' first.")
|
if (s === "missing") throw new Error("Container does not exist. Use 'sandlot vm create' first.")
|
||||||
await $`container start ${CONTAINER_NAME}`.quiet()
|
await run($`container start ${CONTAINER_NAME}`, "Container start")
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Ensure the sandlot container exists and is running. Creates and provisions on first use. */
|
/** Ensure the sandlot container exists and is running. Creates and provisions on first use. */
|
||||||
|
|
@ -237,7 +238,7 @@ export async function status(): Promise<"running" | "stopped" | "missing"> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Launch claude in the container at the given workdir. */
|
/** Launch claude in the container at the given workdir. */
|
||||||
export async function claude(workdir: string, opts?: { prompt?: string; print?: string; continue?: boolean }): Promise<string | void> {
|
export async function claude(workdir: string, opts?: { prompt?: string; print?: string; continue?: boolean }): Promise<{ exitCode: number; output?: string }> {
|
||||||
const cwd = containerPath(workdir)
|
const cwd = containerPath(workdir)
|
||||||
const systemPromptLines = [
|
const systemPromptLines = [
|
||||||
"You are running inside a sandlot container (Apple Container, ubuntu:24.04).",
|
"You are running inside a sandlot container (Apple Container, ubuntu:24.04).",
|
||||||
|
|
@ -260,12 +261,21 @@ export async function claude(workdir: string, opts?: { prompt?: string; print?:
|
||||||
if (opts?.print) {
|
if (opts?.print) {
|
||||||
const proc = Bun.spawn(args, { stdin: "inherit", stdout: "pipe", stderr: "inherit" })
|
const proc = Bun.spawn(args, { stdin: "inherit", stdout: "pipe", stderr: "inherit" })
|
||||||
const output = await new Response(proc.stdout).text()
|
const output = await new Response(proc.stdout).text()
|
||||||
await proc.exited
|
const exitCode = await proc.exited
|
||||||
return output
|
if (exitCode !== 0 && opts?.continue) {
|
||||||
|
info("Retrying without --continue")
|
||||||
|
return claude(workdir, { ...opts, continue: false })
|
||||||
|
}
|
||||||
|
return { exitCode, output }
|
||||||
}
|
}
|
||||||
|
|
||||||
const proc = Bun.spawn(args, { stdin: "inherit", stdout: "inherit", stderr: "inherit" })
|
const proc = Bun.spawn(args, { stdin: "inherit", stdout: "inherit", stderr: "inherit" })
|
||||||
await proc.exited
|
const exitCode = await proc.exited
|
||||||
|
if (exitCode !== 0 && opts?.continue) {
|
||||||
|
info("Retrying without --continue")
|
||||||
|
return claude(workdir, { ...opts, continue: false })
|
||||||
|
}
|
||||||
|
return { exitCode }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Open an interactive fish shell in the container, optionally in a specific directory. */
|
/** Open an interactive fish shell in the container, optionally in a specific directory. */
|
||||||
|
|
@ -278,7 +288,7 @@ export async function shell(workdir?: string): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Run neofetch in the container. */
|
/** Run neofetch in the container. */
|
||||||
export async function info(): Promise<void> {
|
export async function neofetch(): Promise<void> {
|
||||||
const proc = Bun.spawn(
|
const proc = Bun.spawn(
|
||||||
["container", "exec", "--user", USER, CONTAINER_NAME, "env", `PATH=${CONTAINER_PATH}`, "neofetch"],
|
["container", "exec", "--user", USER, CONTAINER_NAME, "env", `PATH=${CONTAINER_PATH}`, "neofetch"],
|
||||||
{ stdin: "inherit", stdout: "inherit", stderr: "inherit" },
|
{ stdin: "inherit", stdout: "inherit", stderr: "inherit" },
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user