apple container
This commit is contained in:
parent
f90cd3a38d
commit
0f18f1e5ff
31
README.md
31
README.md
|
|
@ -1,14 +1,18 @@
|
||||||
# sandlot
|
# sandlot
|
||||||
|
|
||||||
A CLI for branch-based development using git worktrees and [Lima](https://github.com/lima-vm/lima) VMs. Each branch gets its own worktree and isolated VM.
|
A CLI for branch-based development using git worktrees and [Apple Container](https://github.com/apple/container). Each branch gets its own worktree and isolated container.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- macOS on Apple Silicon
|
- macOS on Apple Silicon
|
||||||
- [Bun](https://bun.sh)
|
- [Bun](https://bun.sh)
|
||||||
- [Lima](https://github.com/lima-vm/lima) (`brew install lima`)
|
- [Apple Container](https://github.com/apple/container) (`brew install container`)
|
||||||
- Git
|
- Git
|
||||||
|
|
||||||
|
### First-time setup
|
||||||
|
|
||||||
|
After installing Apple Container, run `container system start` in your terminal. It will prompt to install the Kata kernel — say yes. This is a one-time step.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -28,30 +32,15 @@ Run all commands from inside a cloned git repo.
|
||||||
sandlot new fix-POST
|
sandlot new fix-POST
|
||||||
```
|
```
|
||||||
|
|
||||||
Creates a worktree at `.sandlot/fix-POST/`, boots a Lima VM mapped to it, and drops you into a shell.
|
Creates a worktree, boots a container, and launches Claude Code inside it.
|
||||||
|
|
||||||
### Other commands
|
### Other commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sandlot list # show all sessions
|
sandlot list # show all sessions
|
||||||
sandlot open <branch> # re-enter a session's VM
|
sandlot open <branch> # re-enter a session
|
||||||
sandlot stop <branch> # stop a VM without destroying it
|
sandlot stop <branch> # stop a container without destroying it
|
||||||
sandlot rm <branch> # tear down session (VM, worktree, local branch)
|
sandlot rm <branch> # tear down session (container, worktree, local branch)
|
||||||
```
|
```
|
||||||
|
|
||||||
Use git directly for commits, pushes, merges, etc. The worktree is a normal git checkout.
|
Use git directly for commits, pushes, merges, etc. The worktree is a normal git checkout.
|
||||||
|
|
||||||
## Project config
|
|
||||||
|
|
||||||
Optionally add a `sandlot.json` to your repo root:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"vm": {
|
|
||||||
"cpus": 4,
|
|
||||||
"memory": "8GB",
|
|
||||||
"image": "ubuntu:24.04",
|
|
||||||
"mounts": { "/path/to/deps": "/deps" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
|
||||||
24
SPEC.md
24
SPEC.md
|
|
@ -1,18 +1,18 @@
|
||||||
# sandlot
|
# sandlot
|
||||||
|
|
||||||
A CLI for branch-based development using git worktrees and Lima VMs. Each branch gets its own worktree and isolated VM.
|
A CLI for branch-based development using git worktrees and Apple Container. Each branch gets its own worktree and isolated container.
|
||||||
|
|
||||||
## Concepts
|
## Concepts
|
||||||
|
|
||||||
**Sandlot** is a thin workflow layer over two things: git worktrees and [Lima](https://github.com/lima-vm/lima). The idea is that spinning up a branch should give you a fully isolated environment — filesystem and runtime — with zero setup. When you're done, use git to merge and `sandlot rm` to clean up.
|
**Sandlot** is a thin workflow layer over two things: git worktrees and [Apple Container](https://github.com/apple/container). The idea is that spinning up a branch should give you a fully isolated environment — filesystem and runtime — with zero setup. When you're done, use git to merge and `sandlot rm` to clean up.
|
||||||
|
|
||||||
A sandlot **session** is a (worktree, VM) pair tied to a branch. Sessions are created with `sandlot new <branch>` and destroyed with `sandlot rm <branch>`.
|
A sandlot **session** is a (worktree, container) pair tied to a branch. Sessions are created with `sandlot new <branch>` and destroyed with `sandlot rm <branch>`.
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
- [Bun](https://bun.sh) runtime
|
- [Bun](https://bun.sh) runtime
|
||||||
- [Commander](https://github.com/tj/commander.js) for CLI parsing
|
- [Commander](https://github.com/tj/commander.js) for CLI parsing
|
||||||
- [Lima](https://github.com/lima-vm/lima) for VMs
|
- [Apple Container](https://github.com/apple/container) for containers
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
|
|
@ -21,7 +21,7 @@ A sandlot **session** is a (worktree, VM) pair tied to a branch. Sessions are cr
|
||||||
- macOS on Apple Silicon
|
- macOS on Apple Silicon
|
||||||
- [Bun](https://bun.sh) installed
|
- [Bun](https://bun.sh) installed
|
||||||
- Git installed
|
- Git installed
|
||||||
- [Lima](https://github.com/lima-vm/lima) installed (`brew install lima`)
|
- [Apple Container](https://github.com/apple/container) installed (`brew install container`)
|
||||||
|
|
||||||
### Install
|
### Install
|
||||||
|
|
||||||
|
|
@ -42,8 +42,8 @@ Create a new session. This:
|
||||||
|
|
||||||
1. Checks out the branch if it exists (local or remote), or creates a new branch from current HEAD
|
1. Checks out the branch if it exists (local or remote), or creates a new branch from current HEAD
|
||||||
2. Creates a git worktree at `.sandlot/<branch>/` (relative to the repo root)
|
2. Creates a git worktree at `.sandlot/<branch>/` (relative to the repo root)
|
||||||
3. Boots a Lima VM mapped to that worktree
|
3. Boots a container mapped to that worktree
|
||||||
4. Drops you into the VM shell
|
4. Drops you into the container shell
|
||||||
|
|
||||||
```
|
```
|
||||||
$ sandlot new fix-POST
|
$ sandlot new fix-POST
|
||||||
|
|
@ -65,7 +65,7 @@ refactor-auth Stopped .sandlot/refactor-auth/
|
||||||
|
|
||||||
### `sandlot open <branch>`
|
### `sandlot open <branch>`
|
||||||
|
|
||||||
Re-enter an existing session's VM. If the VM is stopped, boots it first.
|
Re-enter an existing session's container. If the container is stopped, starts it first.
|
||||||
|
|
||||||
```
|
```
|
||||||
$ sandlot open fix-POST
|
$ sandlot open fix-POST
|
||||||
|
|
@ -75,11 +75,11 @@ root@fix-POST:~#
|
||||||
|
|
||||||
### `sandlot stop <branch>`
|
### `sandlot stop <branch>`
|
||||||
|
|
||||||
Stop a session's VM without destroying it. The worktree and branch remain.
|
Stop a session's container without destroying it. The worktree and branch remain.
|
||||||
|
|
||||||
### `sandlot rm <branch>`
|
### `sandlot rm <branch>`
|
||||||
|
|
||||||
Tear down a session. Stops the VM, removes the worktree, deletes the local branch. Does not touch the remote branch.
|
Tear down a session. Stops the container, removes the worktree, deletes the local branch. Does not touch the remote branch.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
|
|
@ -120,12 +120,12 @@ Sandlot tracks sessions in `.sandlot/state.json` at the repo root:
|
||||||
|
|
||||||
## Edge Cases
|
## Edge Cases
|
||||||
|
|
||||||
- **Stale VMs**: If a VM crashes, `sandlot open` detects the dead VM and reboots it.
|
- **Stale containers**: If a container crashes, `sandlot open` detects the dead container and restarts it.
|
||||||
- **Multiple repos**: State is per-repo. No global daemon.
|
- **Multiple repos**: State is per-repo. No global daemon.
|
||||||
- **Branch name conflicts**: If `.sandlot/<branch>/` already exists as a directory but the session state is missing, prompt to clean up or recover.
|
- **Branch name conflicts**: If `.sandlot/<branch>/` already exists as a directory but the session state is missing, prompt to clean up or recover.
|
||||||
|
|
||||||
## Non-Goals
|
## Non-Goals
|
||||||
|
|
||||||
- Not a CI/CD tool. No pipelines, no test runners.
|
- Not a CI/CD tool. No pipelines, no test runners.
|
||||||
- Not a replacement for git. All git state lives in the real repo. Sandlot manages worktrees and VMs only.
|
- Not a replacement for git. All git state lives in the real repo. Sandlot manages worktrees and containers only.
|
||||||
- No multi-user collaboration features. This is a single-developer workflow tool.
|
- No multi-user collaboration features. This is a single-developer workflow tool.
|
||||||
|
|
|
||||||
41
src/cli.ts
41
src/cli.ts
|
|
@ -13,7 +13,7 @@ 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 Lima VMs").version(pkg.version)
|
program.name("sandlot").description("Branch-based development with git worktrees and Apple Container").version(pkg.version)
|
||||||
|
|
||||||
// ── sandlot new <branch> ──────────────────────────────────────────────
|
// ── sandlot new <branch> ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -33,13 +33,21 @@ program
|
||||||
}
|
}
|
||||||
|
|
||||||
const spin = spinner("Creating worktree")
|
const spin = spinner("Creating worktree")
|
||||||
await git.createWorktree(branch, worktreeAbs, root)
|
try {
|
||||||
await mkdir(join(root, '.sandlot'), { recursive: true })
|
await git.createWorktree(branch, worktreeAbs, root)
|
||||||
await symlink(worktreeAbs, join(root, '.sandlot', branch))
|
await mkdir(join(root, '.sandlot'), { recursive: true })
|
||||||
|
await symlink(worktreeAbs, join(root, '.sandlot', branch))
|
||||||
|
|
||||||
spin.text = "Starting VM"
|
spin.text = "Starting container"
|
||||||
await vm.ensure()
|
await vm.ensure((msg) => { spin.text = msg })
|
||||||
spin.succeed("Session ready")
|
spin.succeed("Session ready")
|
||||||
|
} catch (err) {
|
||||||
|
spin.fail(String((err as Error).message ?? err))
|
||||||
|
await git.removeWorktree(worktreeAbs, root).catch(() => {})
|
||||||
|
await git.deleteLocalBranch(branch, root).catch(() => {})
|
||||||
|
await unlink(join(root, '.sandlot', branch)).catch(() => {})
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
await state.setSession(root, {
|
await state.setSession(root, {
|
||||||
branch,
|
branch,
|
||||||
|
|
@ -93,8 +101,8 @@ program
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
const spin = spinner("Starting VM")
|
const spin = spinner("Starting container")
|
||||||
await vm.ensure()
|
await vm.ensure((msg) => { spin.text = msg })
|
||||||
spin.succeed("Session ready")
|
spin.succeed("Session ready")
|
||||||
|
|
||||||
await vm.claude(session.worktree)
|
await vm.claude(session.worktree)
|
||||||
|
|
@ -108,11 +116,16 @@ const closeAction = async (branch: string) => {
|
||||||
const worktreeAbs = session?.worktree ?? join(homedir(), '.sandlot', basename(root), branch)
|
const worktreeAbs = session?.worktree ?? join(homedir(), '.sandlot', basename(root), branch)
|
||||||
|
|
||||||
await git.removeWorktree(worktreeAbs, root)
|
await git.removeWorktree(worktreeAbs, root)
|
||||||
await unlink(join(root, '.sandlot', branch)).catch(() => {})
|
.then(() => console.log(`Removed worktree ${worktreeAbs}/`))
|
||||||
console.log(`Removed worktree ${worktreeAbs}/`)
|
.catch((e) => console.warn(`Failed to remove worktree ${worktreeAbs}: ${e.message}`))
|
||||||
|
|
||||||
|
await unlink(join(root, '.sandlot', branch))
|
||||||
|
.then(() => console.log(`Removed symlink .sandlot/${branch}`))
|
||||||
|
.catch(() => {}) // symlink may not exist
|
||||||
|
|
||||||
await git.deleteLocalBranch(branch, root)
|
await git.deleteLocalBranch(branch, root)
|
||||||
console.log(`Deleted local branch ${branch}`)
|
.then(() => console.log(`Deleted local branch ${branch}`))
|
||||||
|
.catch((e) => console.warn(`Failed to delete local branch ${branch}: ${e.message}`))
|
||||||
|
|
||||||
if (session) {
|
if (session) {
|
||||||
await state.removeSession(root, branch)
|
await state.removeSession(root, branch)
|
||||||
|
|
@ -162,8 +175,8 @@ program
|
||||||
|
|
||||||
const worktreeAbs = session.worktree
|
const worktreeAbs = session.worktree
|
||||||
|
|
||||||
const spin = spinner("Starting VM")
|
const spin = spinner("Starting container")
|
||||||
await vm.ensure()
|
await vm.ensure((msg) => { spin.text = msg })
|
||||||
|
|
||||||
spin.text = "Staging changes"
|
spin.text = "Staging changes"
|
||||||
await vm.exec(worktreeAbs, "git add .")
|
await vm.exec(worktreeAbs, "git add .")
|
||||||
|
|
|
||||||
139
src/vm.ts
139
src/vm.ts
|
|
@ -1,98 +1,129 @@
|
||||||
import { $ } from "bun"
|
import { $ } from "bun"
|
||||||
import { homedir } from "os"
|
import { homedir } from "os"
|
||||||
|
|
||||||
const VM_NAME = "sandlot"
|
const CONTAINER_NAME = "sandlot"
|
||||||
|
const USER = "ubuntu"
|
||||||
|
const CLAUDE_BIN = `/home/${USER}/.local/bin/claude`
|
||||||
|
|
||||||
|
function requireContainer(): void {
|
||||||
|
if (!Bun.which("container")) {
|
||||||
|
console.error('Apple Container is not installed. Install it with: brew install container')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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()
|
||||||
|
|
||||||
|
// Ensure the container daemon is running
|
||||||
|
await $`container system start`.nothrow().quiet()
|
||||||
|
|
||||||
/** Ensure the sandlot VM exists and is running. Creates and provisions on first use. */
|
|
||||||
export async function ensure(): Promise<void> {
|
|
||||||
const s = await status()
|
const s = await status()
|
||||||
if (s === "running") return
|
if (s === "running") return
|
||||||
|
|
||||||
if (s === "stopped") {
|
if (s === "stopped") {
|
||||||
await $`limactl start ${VM_NAME}`.quiet()
|
await $`container start ${CONTAINER_NAME}`.quiet()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create from scratch
|
// Create from scratch
|
||||||
const home = homedir()
|
const home = homedir()
|
||||||
await $`limactl create --name=${VM_NAME} --mount=${home}/dev --mount=${home}/.sandlot:w template:ubuntu-24.04`.quiet()
|
log?.("Pulling image & creating container")
|
||||||
await $`limactl start ${VM_NAME}`.quiet()
|
await $`container run -d --name ${CONTAINER_NAME} -m 4G -v ${home}/dev:${home}/dev -v ${home}/.sandlot:${home}/.sandlot ubuntu:24.04 sleep infinity`.quiet()
|
||||||
|
|
||||||
// Provision
|
// Provision (as root)
|
||||||
await $`limactl shell ${VM_NAME} -- bash -c "curl -fsSL https://claude.ai/install.sh | bash"`.quiet()
|
log?.("Installing packages")
|
||||||
|
await $`container exec ${CONTAINER_NAME} bash -c ${"apt update && apt install -y curl git neofetch fish"}`.quiet()
|
||||||
|
|
||||||
await $`limactl shell ${VM_NAME} -- sudo apt install -y neofetch fish`.quiet()
|
// Install Claude Code and configure (as ubuntu user)
|
||||||
|
log?.("Installing Claude Code")
|
||||||
|
await $`container exec --user ${USER} ${CONTAINER_NAME} bash -c ${"curl -fsSL https://claude.ai/install.sh | bash"}`.quiet()
|
||||||
|
|
||||||
// Configure git from host settings
|
log?.("Configuring environment")
|
||||||
const gitName = (await $`git config user.name`.quiet().text()).trim()
|
const gitName = (await $`git config user.name`.quiet().text()).trim()
|
||||||
const gitEmail = (await $`git config user.email`.quiet().text()).trim()
|
const gitEmail = (await $`git config user.email`.quiet().text()).trim()
|
||||||
if (gitName) await $`limactl shell ${VM_NAME} -- git config --global user.name ${gitName}`.quiet()
|
if (gitName) await $`container exec --user ${USER} ${CONTAINER_NAME} git config --global user.name ${gitName}`.quiet()
|
||||||
if (gitEmail) await $`limactl shell ${VM_NAME} -- git config --global user.email ${gitEmail}`.quiet()
|
if (gitEmail) await $`container exec --user ${USER} ${CONTAINER_NAME} git config --global user.email ${gitEmail}`.quiet()
|
||||||
|
|
||||||
// Configure claude to use API key from host ~/.env (skip login prompt)
|
// Configure claude to use API key from host ~/.env (skip login prompt)
|
||||||
const helperScript = `#!/bin/sh\n. ${home}/.env 2>/dev/null\necho "$ANTHROPIC_API_KEY"\n`
|
let apiKey: string | undefined
|
||||||
const settingsJson = JSON.stringify({ apiKeyHelper: "~/.claude/api-key-helper.sh", skipDangerousModePermissionPrompt: true })
|
const envFile = Bun.file(`${home}/.env`)
|
||||||
const tmpHelper = `${home}/.sandlot-tmp-helper.sh`
|
if (await envFile.exists()) {
|
||||||
const tmpSettings = `${home}/.sandlot-tmp-settings.json`
|
const envContent = await envFile.text()
|
||||||
await Bun.write(tmpHelper, helperScript, { mode: 0o755 })
|
apiKey = envContent.match(/^(?:export\s+)?ANTHROPIC_API_KEY=["']?([^"'\s]+)["']?/m)?.[1]
|
||||||
await Bun.write(tmpSettings, settingsJson)
|
|
||||||
const claudeJson = JSON.stringify({ hasCompletedOnboarding: true })
|
|
||||||
const tmpClaudeJson = `${home}/.sandlot-tmp-claude.json`
|
|
||||||
await Bun.write(tmpClaudeJson, claudeJson)
|
|
||||||
await $`limactl shell ${VM_NAME} -- bash -c "mkdir -p ~/.claude && cp ${tmpHelper} ~/.claude/api-key-helper.sh && chmod +x ~/.claude/api-key-helper.sh && cp ${tmpSettings} ~/.claude/settings.json && cp ${tmpClaudeJson} ~/.claude.json"`.quiet()
|
|
||||||
await Bun.file(tmpHelper).delete()
|
|
||||||
await Bun.file(tmpSettings).delete()
|
|
||||||
await Bun.file(tmpClaudeJson).delete()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Check VM status. */
|
|
||||||
export async function status(): Promise<"running" | "stopped" | "missing"> {
|
|
||||||
const result = await $`limactl list --json`.nothrow().quiet().text()
|
|
||||||
|
|
||||||
for (const line of result.trim().split("\n")) {
|
|
||||||
if (!line) continue
|
|
||||||
try {
|
|
||||||
const instance = JSON.parse(line)
|
|
||||||
if (instance.name === VM_NAME) {
|
|
||||||
return instance.status?.toLowerCase() === "running" ? "running" : "stopped"
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return "missing"
|
if (!apiKey) {
|
||||||
|
log?.("Warning: ANTHROPIC_API_KEY not found in ~/.env — claude will require manual login")
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingsJson = JSON.stringify(apiKey
|
||||||
|
? { apiKeyHelper: "~/.claude/api-key-helper.sh", skipDangerousModePermissionPrompt: true }
|
||||||
|
: { skipDangerousModePermissionPrompt: true })
|
||||||
|
const claudeJson = JSON.stringify({ hasCompletedOnboarding: 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} mkdir -p /home/${USER}/.claude`.quiet()
|
||||||
|
await $`container cp ${tmp} ${CONTAINER_NAME}:/home/${USER}/.claude/api-key-helper.sh`.quiet()
|
||||||
|
await Bun.file(tmp).unlink()
|
||||||
|
}
|
||||||
|
|
||||||
|
await $`container exec --user ${USER} ${CONTAINER_NAME} bash -c ${`
|
||||||
|
mkdir -p ~/.claude
|
||||||
|
echo '${settingsJson}' > ~/.claude/settings.json
|
||||||
|
echo '${claudeJson}' > ~/.claude.json
|
||||||
|
`}`.quiet()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Launch claude in the VM at the given workdir. */
|
/** Check container status. */
|
||||||
|
export async function status(): Promise<"running" | "stopped" | "missing"> {
|
||||||
|
const result = await $`container list --format json --all`.nothrow().quiet().text()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const containers = JSON.parse(result.trim())
|
||||||
|
const container = containers.find((c: any) => c.configuration?.id === CONTAINER_NAME)
|
||||||
|
if (!container) return "missing"
|
||||||
|
return container.status?.toLowerCase() === "running" ? "running" : "stopped"
|
||||||
|
} catch {
|
||||||
|
return "missing"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Launch claude in the container at the given workdir. */
|
||||||
export async function claude(workdir: string, prompt?: string): Promise<void> {
|
export async function claude(workdir: string, prompt?: string): Promise<void> {
|
||||||
const args = ["limactl", "shell", `--workdir=${workdir}`, VM_NAME, "claude", "--dangerously-skip-permissions"]
|
const args = ["container", "exec", "-it", "--user", USER, "--workdir", workdir, CONTAINER_NAME, CLAUDE_BIN, "--dangerously-skip-permissions"]
|
||||||
if (prompt) args.push(prompt)
|
if (prompt) args.push(prompt)
|
||||||
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
|
await proc.exited
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Open an interactive fish shell in the VM. */
|
/** Open an interactive fish shell in the container. */
|
||||||
export async function shell(): Promise<void> {
|
export async function shell(): Promise<void> {
|
||||||
const proc = Bun.spawn(
|
const proc = Bun.spawn(
|
||||||
["limactl", "shell", VM_NAME, "--", "env", "TERM=xterm-256color", "fish", "--login"],
|
["container", "exec", "-it", "--user", USER, CONTAINER_NAME, "env", "TERM=xterm-256color", "fish", "--login"],
|
||||||
{ stdin: "inherit", stdout: "inherit", stderr: "inherit" },
|
{ stdin: "inherit", stdout: "inherit", stderr: "inherit" },
|
||||||
)
|
)
|
||||||
await proc.exited
|
await proc.exited
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Run neofetch in the VM. */
|
/** Run neofetch in the container. */
|
||||||
export async function info(): Promise<void> {
|
export async function info(): Promise<void> {
|
||||||
const proc = Bun.spawn(
|
const proc = Bun.spawn(
|
||||||
["limactl", "shell", VM_NAME, "--", "neofetch"],
|
["container", "exec", "--user", USER, CONTAINER_NAME, "neofetch"],
|
||||||
{ stdin: "inherit", stdout: "inherit", stderr: "inherit" },
|
{ stdin: "inherit", stdout: "inherit", stderr: "inherit" },
|
||||||
)
|
)
|
||||||
await proc.exited
|
await proc.exited
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Run a bash command in the VM at the given workdir, capturing output. */
|
/** Run a bash command in the container at the given workdir, capturing output. */
|
||||||
export async function exec(workdir: string, command: string): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
export async function exec(workdir: string, command: string): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
||||||
const result = await $`limactl shell --workdir=${workdir} ${VM_NAME} -- bash -c ${command}`.nothrow().quiet()
|
const result = await $`container exec --user ${USER} --workdir ${workdir} ${CONTAINER_NAME} bash -c ${"export PATH=$HOME/.local/bin:$PATH; " + command}`.nothrow().quiet()
|
||||||
return {
|
return {
|
||||||
exitCode: result.exitCode,
|
exitCode: result.exitCode,
|
||||||
stdout: result.stdout.toString().trim(),
|
stdout: result.stdout.toString().trim(),
|
||||||
|
|
@ -100,13 +131,13 @@ export async function exec(workdir: string, command: string): Promise<{ exitCode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Stop the VM. */
|
/** Stop the container. */
|
||||||
export async function stop(): Promise<void> {
|
export async function stop(): Promise<void> {
|
||||||
await $`limactl stop ${VM_NAME}`.nothrow().quiet()
|
await $`container stop ${CONTAINER_NAME}`.nothrow().quiet()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Stop and delete the VM. */
|
/** Stop and delete the container. */
|
||||||
export async function destroy(): Promise<void> {
|
export async function destroy(): Promise<void> {
|
||||||
await stop()
|
await stop()
|
||||||
await $`limactl delete ${VM_NAME}`.nothrow().quiet()
|
await $`container delete ${CONTAINER_NAME}`.nothrow().quiet()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user