mirror of
https://github.com/robcholz/vibebox.git
synced 2026-07-02 12:25:29 +02:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4cb1162ca3 | |||
| 4c9f517107 | |||
| 15245fd245 | |||
| 1c5a464a68 | |||
| 5659f2a538 | |||
| 9cf33359f9 | |||
| a1056ba5cb | |||
| ad8fb139de | |||
| 5e008c9471 | |||
| 935e0cd566 | |||
| c53a6eff51 | |||
| 4dd9c31f74 | |||
| 9dd88dd304 | |||
| 4311168fdd | |||
| a4b9d62873 | |||
| 727adffb4c | |||
| 75adb1696a | |||
| 57ebe7ffee | |||
| f4aa223ddb |
@@ -6,6 +6,8 @@ on:
|
|||||||
- "**"
|
- "**"
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- "README.md"
|
- "README.md"
|
||||||
|
- "CONTRIBUTING.md"
|
||||||
|
- "README.zh.md"
|
||||||
- "docs/**"
|
- "docs/**"
|
||||||
- "install"
|
- "install"
|
||||||
pull_request:
|
pull_request:
|
||||||
@@ -54,3 +56,15 @@ jobs:
|
|||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
- name: cargo build
|
- name: cargo build
|
||||||
run: cargo build --locked
|
run: cargo build --locked
|
||||||
|
|
||||||
|
test:
|
||||||
|
name: Test
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- name: Install Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
- name: Cache Rust
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
- name: cargo test
|
||||||
|
run: cargo test --locked
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# Contributing to Vibebox
|
||||||
|
|
||||||
|
Thanks for your interest in contributing! This guide keeps PRs small, reviewable, and consistent with the project’s
|
||||||
|
development workflow.
|
||||||
|
|
||||||
|
**Prerequisites**
|
||||||
|
|
||||||
|
- macOS on Apple Silicon (required for the virtualization backend)
|
||||||
|
- Rust `1.91.1` or newer (see `Cargo.toml`)
|
||||||
|
|
||||||
|
**Getting Started**
|
||||||
|
|
||||||
|
1. Fork the repo and create a feature branch.
|
||||||
|
2. Build once to validate your toolchain:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo build --locked
|
||||||
|
```
|
||||||
|
|
||||||
|
**Development Commands**
|
||||||
|
|
||||||
|
- Format: `cargo fmt --all -- --check`
|
||||||
|
- Lint: `cargo clippy --all-targets --all-features -- -D warnings`
|
||||||
|
- Test: `cargo test --locked`
|
||||||
|
- Build: `cargo build --locked`
|
||||||
|
|
||||||
|
**Submitting Changes**
|
||||||
|
|
||||||
|
- Keep changes focused and scoped to one problem.
|
||||||
|
- Update or add tests when behavior changes.
|
||||||
|
- If you change user-facing behavior, update docs or help text.
|
||||||
|
- Avoid adding heavy dependencies without a clear reason.
|
||||||
|
|
||||||
|
**Reporting Issues**
|
||||||
|
Please include:
|
||||||
|
|
||||||
|
- macOS version and hardware (Apple Silicon model)
|
||||||
|
- Vibebox version (`vibebox --version`)
|
||||||
|
- Steps to reproduce
|
||||||
|
- Logs from `.vibebox/cli.log`, `.vibebox/vm_root.log` and `.vibebox/vm_manager.log`
|
||||||
|
|
||||||
|
**Security**
|
||||||
|
If you believe you’ve found a security issue, please avoid public disclosure. Open a private report via GitHub Security
|
||||||
|
Advisories instead.
|
||||||
Generated
+1
-1
@@ -1238,7 +1238,7 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vibebox"
|
name = "vibebox"
|
||||||
version = "0.1.0"
|
version = "0.2.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block2",
|
"block2",
|
||||||
"clap",
|
"clap",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "vibebox"
|
name = "vibebox"
|
||||||
version = "0.1.0"
|
version = "0.2.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors = ["Finn Sheng"]
|
authors = ["Finn Sheng"]
|
||||||
description = "Ultrafast CLI on Apple Silicon macOS for fast, sandboxed development and LLM agents."
|
description = "Ultrafast CLI on Apple Silicon macOS for fast, sandboxed development and LLM agents."
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="https://vibebox.robcholz.com">
|
||||||
|
<picture>
|
||||||
|
<img src="docs/banner.png" alt="VibeBox logo">
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p align="center">Your ultrafast open source AI sandbox.</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://crates.io/crates/vibebox">
|
||||||
|
<img alt="Crates.io" src="https://img.shields.io/crates/v/vibebox.svg">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/robcholz/vibebox/blob/main/LICENSE">
|
||||||
|
<img alt="MIT licensed" src="https://img.shields.io/badge/license-MIT-blue.svg">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/robcholz/vibebox/actions?query=workflow%3ACI+branch%3Amain">
|
||||||
|
<img alt="Build Status" src="https://github.com/robcholz/vibebox/workflows/CI/badge.svg">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="README.md">English</a> |
|
||||||
|
<a href="README.zh.md">简体中文</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
[](https://vibebox.robcholz.com)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# YOLO
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/robcholz/vibebox/main/install | bash
|
||||||
|
|
||||||
|
# Package managers
|
||||||
|
cargo install vibebox
|
||||||
|
|
||||||
|
# Or manually (bad)
|
||||||
|
curl -LO https://github.com/robcholz/vibebox/releases/download/latest/vibebox-macos-arm64.zip
|
||||||
|
unzip vibebox-macos-arm64.zip
|
||||||
|
mkdir -p ~/.local/bin
|
||||||
|
mv vibe ~/.local/bin
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> We truly recommend you to use `YOLO` to install.
|
||||||
|
|
||||||
|
**Requirements**
|
||||||
|
|
||||||
|
- macOS on Apple Silicon (Vibebox uses Apple's virtualization APIs).
|
||||||
|
|
||||||
|
**First Run**
|
||||||
|
|
||||||
|
The first `vibebox` run downloads a Debian base image and provisions it. After that, per-project instances reuse the
|
||||||
|
cached base image for much faster startups.
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
**Quick Start**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/your/project
|
||||||
|
vibebox
|
||||||
|
```
|
||||||
|
|
||||||
|
On first run, Vibebox creates `vibebox.toml` in your project (if missing) and a `.vibebox/` directory for instance data.
|
||||||
|
|
||||||
|
**Configuration (`vibebox.toml`)**
|
||||||
|
|
||||||
|
`vibebox.toml` lives in your project root by default. You can override it with `vibebox -c path/to/vibebox.toml` or the
|
||||||
|
`VIBEBOX_CONFIG_PATH` env var, but the path must stay inside the project directory.
|
||||||
|
|
||||||
|
Default config (auto-created when missing):
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[box]
|
||||||
|
cpu_count = 2
|
||||||
|
ram_mb = 2048
|
||||||
|
disk_gb = 5
|
||||||
|
mounts = [
|
||||||
|
"~/.codex:~/.codex:read-write",
|
||||||
|
"~/.claude:~/.claude:read-write",
|
||||||
|
]
|
||||||
|
|
||||||
|
[supervisor]
|
||||||
|
auto_shutdown_ms = 20000
|
||||||
|
```
|
||||||
|
|
||||||
|
`disk_gb` is only applied when the instance disk is first created. If you change it later, run `vibebox reset` to
|
||||||
|
recreate the disk.
|
||||||
|
|
||||||
|
**Mounts**
|
||||||
|
|
||||||
|
- Your project is mounted read-write at `~/<project-name>`, and the shell starts there.
|
||||||
|
- If a `.git` directory exists, it is masked with a tmpfs mount inside the VM to discourage accidental edits from the
|
||||||
|
guest.
|
||||||
|
- Extra mounts come from `box.mounts` with the format `host:guest[:read-only|read-write]`.
|
||||||
|
- Host paths support `~` expansion. Relative guest paths are treated as `/root/<path>`.
|
||||||
|
- Guest paths that use `~` are linked into `/home/<ssh-user>` for convenience. Run `vibebox explain` to see the resolved
|
||||||
|
host/guest mappings.
|
||||||
|
|
||||||
|
**CLI Commands**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
vibebox # start or attach to the current project VM
|
||||||
|
vibebox list # list known project sessions
|
||||||
|
vibebox reset # delete .vibebox for this project and recreate on next run
|
||||||
|
vibebox purge-cache # delete the global cache (~/.cache/vibebox)
|
||||||
|
vibebox explain # show mounts and network info
|
||||||
|
```
|
||||||
|
|
||||||
|
**Inside the VM**
|
||||||
|
|
||||||
|
- Default SSH user: `vibecoder`
|
||||||
|
- Hostname: `vibebox`
|
||||||
|
- Base image provisioning installs: build tools, `git`, `curl`, `ripgrep`, `openssh-server`, and `sudo`.
|
||||||
|
- On first login, Vibebox installs `mise` and configures tools like `uv`, `node`, `@openai/codex`, and
|
||||||
|
`@anthropic-ai/claude-code` (best-effort).
|
||||||
|
- Shell aliases: `:help` and `:exit`.
|
||||||
|
|
||||||
|
**State & Cache**
|
||||||
|
|
||||||
|
- Project state lives in `.vibebox/` (instance disk, SSH keys, logs, manager socket/pid). `vibebox reset` removes it.
|
||||||
|
- Global cache lives in `~/.cache/vibebox` (base image + shared guest cache). `vibebox purge-cache` clears it.
|
||||||
|
- Session index lives in `~/.vibebox/sessions` and is shown by `vibebox list`.
|
||||||
|
|
||||||
|
### Contributing
|
||||||
|
|
||||||
|
If you're interested in contributing to VibeBox, please read our [contributing docs](CONTRIBUTING.md) before
|
||||||
|
submitting a pull request.
|
||||||
|
|
||||||
|
### Using VibeBox
|
||||||
|
|
||||||
|
Feel free to use, but remember to promote VibeBox as well!
|
||||||
|
|
||||||
|
### FAQ
|
||||||
|
|
||||||
|
#### How is this different from other Sandboxes?
|
||||||
|
|
||||||
|
Vibebox is built for fast, repeatable local sandboxes with minimal ceremony. What’s different here:
|
||||||
|
|
||||||
|
- Warm startup is typically under **6 seconds** on my M3, so you can jump back in quickly.
|
||||||
|
- One simple command — `vibebox` — drops you into the sandbox from your project.
|
||||||
|
- Configuration lives in `vibebox.toml`, where you can set CPU, RAM, disk size, and mounts.
|
||||||
|
|
||||||
|
### Special Thank
|
||||||
|
|
||||||
|
[vibe](https://github.com/lynaghk/vibe) by lynaghk.
|
||||||
|
|
||||||
|
And amazing Rust community, without your rich crates and fantastic toolchain like [crates.io](https://crates.io), this
|
||||||
|
wouldn't be possible!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Follow me on X** [X.com](https://x.com/robcholz)
|
||||||
|
|||||||
+156
@@ -0,0 +1,156 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="https://vibebox.robcholz.com">
|
||||||
|
<picture>
|
||||||
|
<img src="docs/banner.png" alt="VibeBox logo">
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p align="center">超高速、开源的 AI 沙盒。</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://crates.io/crates/vibebox">
|
||||||
|
<img alt="Crates.io" src="https://img.shields.io/crates/v/vibebox.svg">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/robcholz/vibebox/blob/main/LICENSE">
|
||||||
|
<img alt="MIT licensed" src="https://img.shields.io/badge/license-MIT-blue.svg">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/robcholz/vibebox/actions?query=workflow%3ACI+branch%3Amain">
|
||||||
|
<img alt="Build Status" src="https://github.com/robcholz/vibebox/workflows/CI/badge.svg">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="README.zh.md">简体中文</a> |
|
||||||
|
<a href="README.md">English</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
[](https://vibebox.robcholz.com)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# YOLO:一键安装(推荐)
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/robcholz/vibebox/main/install | bash
|
||||||
|
|
||||||
|
# Cargo
|
||||||
|
cargo install vibebox
|
||||||
|
|
||||||
|
# 或者手动安装(不推荐)
|
||||||
|
curl -LO https://github.com/robcholz/vibebox/releases/download/latest/vibebox-macos-arm64.zip
|
||||||
|
unzip vibebox-macos-arm64.zip
|
||||||
|
mkdir -p ~/.local/bin
|
||||||
|
mv vibe ~/.local/bin
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> 强烈建议直接用 `YOLO` 方式安装,省事且更不容易踩坑。
|
||||||
|
|
||||||
|
**系统要求**
|
||||||
|
|
||||||
|
- Apple Silicon 的 macOS(VibeBox 使用了 Apple 的虚拟化 API)。
|
||||||
|
|
||||||
|
**首次运行**
|
||||||
|
|
||||||
|
第一次执行 `vibebox` 会下载 Debian 基础镜像并完成初始化。之后每个项目的实例会复用缓存的基础镜像,
|
||||||
|
启动会快很多。
|
||||||
|
|
||||||
|
### 文档
|
||||||
|
|
||||||
|
**快速开始**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/your/project
|
||||||
|
vibebox
|
||||||
|
```
|
||||||
|
|
||||||
|
第一次运行时,如果项目目录里缺少配置,VibeBox 会自动创建 `vibebox.toml`(放在项目根目录),并创建
|
||||||
|
`.vibebox/` 用来保存实例数据。
|
||||||
|
|
||||||
|
**配置(`vibebox.toml`)**
|
||||||
|
|
||||||
|
默认情况下,`vibebox.toml` 位于项目根目录。你可以用 `vibebox -c path/to/vibebox.toml` 或设置
|
||||||
|
`VIBEBOX_CONFIG_PATH` 环境变量来覆盖路径,但配置文件必须仍然位于项目目录内部。
|
||||||
|
|
||||||
|
默认配置(缺失时会自动生成):
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[box]
|
||||||
|
cpu_count = 2
|
||||||
|
ram_mb = 2048
|
||||||
|
disk_gb = 5
|
||||||
|
mounts = [
|
||||||
|
"~/.codex:~/.codex:read-write",
|
||||||
|
"~/.claude:~/.claude:read-write",
|
||||||
|
]
|
||||||
|
|
||||||
|
[supervisor]
|
||||||
|
auto_shutdown_ms = 20000
|
||||||
|
```
|
||||||
|
|
||||||
|
注意:`disk_gb` 只在「首次创建实例磁盘」时生效。之后如果你改了它,需要运行 `vibebox reset` 重新创建磁盘。
|
||||||
|
|
||||||
|
**挂载(Mounts)**
|
||||||
|
|
||||||
|
- 你的项目会以读写方式挂载到 `~/<project-name>`,并且 shell 会默认从那里启动。
|
||||||
|
- 如果项目里存在 `.git` 目录,VM 内会用 tmpfs 把它遮住,避免你在 guest 里误操作改到 Git 元数据。
|
||||||
|
- 额外挂载通过 `box.mounts` 配置,格式为 `host:guest[:read-only|read-write]`。
|
||||||
|
- Host 路径支持 `~` 展开;guest 的相对路径会被视为 `/root/<path>`。
|
||||||
|
- guest 路径如果用了 `~`,会为了方便被链接到 `/home/<ssh-user>` 下。你可以运行 `vibebox explain`
|
||||||
|
查看最终解析后的 host/guest 映射关系。
|
||||||
|
|
||||||
|
**CLI 命令**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
vibebox # 启动或连接当前项目的 VM
|
||||||
|
vibebox list # 列出已知的项目会话
|
||||||
|
vibebox reset # 删除当前项目的 .vibebox,下一次运行会重新创建
|
||||||
|
vibebox purge-cache # 删除全局缓存(~/.cache/vibebox)
|
||||||
|
vibebox explain # 显示挂载与网络信息
|
||||||
|
```
|
||||||
|
|
||||||
|
**在 VM 内部**
|
||||||
|
|
||||||
|
- 默认 SSH 用户:`vibecoder`
|
||||||
|
- 主机名:`vibebox`
|
||||||
|
- 基础镜像初始化会安装:构建工具、`git`、`curl`、`ripgrep`、`openssh-server`、`sudo`
|
||||||
|
- 首次登录时,VibeBox 会安装 `mise`,并尽力配置 `uv`、`node`、`@openai/codex`、
|
||||||
|
`@anthropic-ai/claude-code` 等工具(best-effort,视网络和环境而定)
|
||||||
|
- Shell 里有两个别名:`:help` 和 `:exit`
|
||||||
|
|
||||||
|
**状态与缓存**
|
||||||
|
|
||||||
|
- 项目级状态在 `.vibebox/`(实例磁盘、SSH key、日志、manager socket/pid)。`vibebox reset` 会移除它。
|
||||||
|
- 全局缓存在 `~/.cache/vibebox`(基础镜像 + 共享 guest 缓存)。`vibebox purge-cache` 会清空它。
|
||||||
|
- 会话索引在 `~/.vibebox/sessions`,可以通过 `vibebox list` 查看。
|
||||||
|
|
||||||
|
### 参与贡献
|
||||||
|
|
||||||
|
如果你想参与贡献 VibeBox,请先阅读 [贡献指南](CONTRIBUTING.md),再提交 Pull Request。
|
||||||
|
|
||||||
|
### 使用 VibeBox
|
||||||
|
|
||||||
|
欢迎使用,也别忘了顺手帮 VibeBox 做点宣传!
|
||||||
|
|
||||||
|
### FAQ
|
||||||
|
|
||||||
|
#### 它和其它 Sandboxes 有什么不同?
|
||||||
|
|
||||||
|
Vibebox 追求的是:本地、可复现、启动快、流程简单。主要差异点:
|
||||||
|
|
||||||
|
- 在我的 M3 上,热启动通常 **6 秒以内**,可以非常快地回到工作状态。
|
||||||
|
- 一个命令——`vibebox`——直接把你带进沙盒(从你的项目目录启动)。
|
||||||
|
- 配置集中在 `vibebox.toml`,CPU / 内存 / 磁盘大小 / 挂载都能一眼看懂、随手改。
|
||||||
|
|
||||||
|
### 特别鸣谢
|
||||||
|
|
||||||
|
[vibe](https://github.com/lynaghk/vibe) by lynaghk。
|
||||||
|
|
||||||
|
以及 Rust 社区。没有你们丰富的 crates 生态和优秀的工具链(比如 [crates.io](https://crates.io)),
|
||||||
|
这个项目不可能这么顺利!Rust教。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**在 X 上关注我** [X.com](https://x.com/robcholz)
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
@@ -47,36 +47,40 @@ ensure_cargo() {
|
|||||||
|
|
||||||
print_message warning "Rust (cargo) is required but not found."
|
print_message warning "Rust (cargo) is required but not found."
|
||||||
print_message info "You should review and approve the Rust installer before proceeding."
|
print_message info "You should review and approve the Rust installer before proceeding."
|
||||||
|
reply=""
|
||||||
if [[ -t 0 ]]; then
|
if [[ -t 0 ]]; then
|
||||||
read -r -p "Install Rust using rustup now? (y/N) " reply
|
read -r -p "Install Rust using rustup now? (y/N) " reply
|
||||||
case "${reply}" in
|
elif [[ -r /dev/tty ]]; then
|
||||||
y|Y)
|
read -r -p "Install Rust using rustup now? (y/N) " reply </dev/tty
|
||||||
if command -v curl >/dev/null 2>&1; then
|
|
||||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
|
||||||
elif command -v wget >/dev/null 2>&1; then
|
|
||||||
wget -qO- https://sh.rustup.rs | sh -s -- -y
|
|
||||||
else
|
|
||||||
print_message error "Missing required command: curl or wget"
|
|
||||||
print_message info "Install Rust manually from https://rustup.rs and retry."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
# shellcheck source=/dev/null
|
|
||||||
if [[ -f "$HOME/.cargo/env" ]]; then
|
|
||||||
# shellcheck disable=SC1090
|
|
||||||
source "$HOME/.cargo/env"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
print_message info "Install Rust manually from https://rustup.rs and retry."
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
else
|
else
|
||||||
print_message error "Non-interactive shell: cannot prompt to install Rust."
|
print_message error "Non-interactive shell: cannot prompt to install Rust."
|
||||||
print_message info "Install Rust manually from https://rustup.rs and retry."
|
print_message info "Install Rust manually from https://rustup.rs and retry."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
case "${reply}" in
|
||||||
|
y|Y)
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||||
|
elif command -v wget >/dev/null 2>&1; then
|
||||||
|
wget -qO- https://sh.rustup.rs | sh -s -- -y
|
||||||
|
else
|
||||||
|
print_message error "Missing required command: curl or wget"
|
||||||
|
print_message info "Install Rust manually from https://rustup.rs and retry."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
if [[ -f "$HOME/.cargo/env" ]]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$HOME/.cargo/env"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
print_message info "Install Rust manually from https://rustup.rs and retry."
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
if ! command -v cargo >/dev/null 2>&1; then
|
if ! command -v cargo >/dev/null 2>&1; then
|
||||||
print_message error "Cargo still not available after installation."
|
print_message error "Cargo still not available after installation."
|
||||||
print_message info "Open a new shell or run: source \"$HOME/.cargo/env\""
|
print_message info "Open a new shell or run: source \"$HOME/.cargo/env\""
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ fn main() -> Result<()> {
|
|||||||
let args = vm::VmArg {
|
let args = vm::VmArg {
|
||||||
cpu_count: config.box_cfg.cpu_count,
|
cpu_count: config.box_cfg.cpu_count,
|
||||||
ram_bytes: config.box_cfg.ram_mb.saturating_mul(1024 * 1024),
|
ram_bytes: config.box_cfg.ram_mb.saturating_mul(1024 * 1024),
|
||||||
|
disk_bytes: config.box_cfg.disk_gb.saturating_mul(1024 * 1024 * 1024),
|
||||||
no_default_mounts: false,
|
no_default_mounts: false,
|
||||||
mounts: config.box_cfg.mounts.clone(),
|
mounts: config.box_cfg.mounts.clone(),
|
||||||
};
|
};
|
||||||
|
|||||||
+27
-3
@@ -49,7 +49,7 @@ fn main() -> Result<()> {
|
|||||||
let stderr_handle = init_tracing(&cwd);
|
let stderr_handle = init_tracing(&cwd);
|
||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
tracing::info!(cwd = %cwd.display(), "starting vibebox cli");
|
tracing::debug!(cwd = %cwd.display(), "starting vibebox cli");
|
||||||
if let Some(command) = cli.command {
|
if let Some(command) = cli.command {
|
||||||
return handle_command(command, &cwd, cli.config.as_deref());
|
return handle_command(command, &cwd, cli.config.as_deref());
|
||||||
}
|
}
|
||||||
@@ -63,6 +63,7 @@ fn main() -> Result<()> {
|
|||||||
let args = vm::VmArg {
|
let args = vm::VmArg {
|
||||||
cpu_count: config.box_cfg.cpu_count,
|
cpu_count: config.box_cfg.cpu_count,
|
||||||
ram_bytes: config.box_cfg.ram_mb.saturating_mul(1024 * 1024),
|
ram_bytes: config.box_cfg.ram_mb.saturating_mul(1024 * 1024),
|
||||||
|
disk_bytes: config.box_cfg.disk_gb.saturating_mul(1024 * 1024 * 1024),
|
||||||
no_default_mounts: false,
|
no_default_mounts: false,
|
||||||
mounts: config.box_cfg.mounts.clone(),
|
mounts: config.box_cfg.mounts.clone(),
|
||||||
};
|
};
|
||||||
@@ -80,14 +81,15 @@ fn main() -> Result<()> {
|
|||||||
let vm_args = vm::VmArg {
|
let vm_args = vm::VmArg {
|
||||||
cpu_count: config.box_cfg.cpu_count,
|
cpu_count: config.box_cfg.cpu_count,
|
||||||
ram_bytes: config.box_cfg.ram_mb.saturating_mul(1024 * 1024),
|
ram_bytes: config.box_cfg.ram_mb.saturating_mul(1024 * 1024),
|
||||||
|
disk_bytes: config.box_cfg.disk_gb.saturating_mul(1024 * 1024 * 1024),
|
||||||
no_default_mounts: false,
|
no_default_mounts: false,
|
||||||
mounts: config.box_cfg.mounts.clone(),
|
mounts: config.box_cfg.mounts.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let auto_shutdown_ms = config.supervisor.auto_shutdown_ms;
|
let auto_shutdown_ms = config.supervisor.auto_shutdown_ms;
|
||||||
let vm_info = VmInfo {
|
let vm_info = VmInfo {
|
||||||
max_memory_mb: vm_args.ram_bytes / (1024 * 1024),
|
max_memory_mb: vm_args.ram_bytes / (1024 * 1024),
|
||||||
cpu_cores: vm_args.cpu_count,
|
cpu_cores: vm_args.cpu_count,
|
||||||
|
max_disk_gb: (vm_args.disk_bytes as f32) / 1024.0 / 1024.0 / 1024.0,
|
||||||
system_name: "Debian".to_string(), // TODO: read system name from the VM.
|
system_name: "Debian".to_string(), // TODO: read system name from the VM.
|
||||||
auto_shutdown_ms,
|
auto_shutdown_ms,
|
||||||
};
|
};
|
||||||
@@ -110,6 +112,7 @@ fn main() -> Result<()> {
|
|||||||
writeln!(stdout)?;
|
writeln!(stdout)?;
|
||||||
stdout.flush()?;
|
stdout.flush()?;
|
||||||
}
|
}
|
||||||
|
warn_disk_size_mismatch(&cwd, vm_args.disk_bytes);
|
||||||
if let Some(handle) = stderr_handle {
|
if let Some(handle) = stderr_handle {
|
||||||
let _ = handle.modify(|filter| *filter = LevelFilter::INFO);
|
let _ = handle.modify(|filter| *filter = LevelFilter::INFO);
|
||||||
}
|
}
|
||||||
@@ -351,6 +354,27 @@ fn format_last_active(value: Option<&str>) -> String {
|
|||||||
format!("{} day{} ago", days, if days == 1 { "" } else { "s" })
|
format!("{} day{} ago", days, if days == 1 { "" } else { "s" })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn warn_disk_size_mismatch(cwd: &Path, configured_bytes: u64) {
|
||||||
|
let instance_raw = cwd
|
||||||
|
.join(session_manager::INSTANCE_DIR_NAME)
|
||||||
|
.join("instance.raw");
|
||||||
|
let Ok(meta) = fs::metadata(&instance_raw) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let current_bytes = meta.len();
|
||||||
|
if current_bytes == configured_bytes {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let current_gb = current_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
|
||||||
|
let target_gb = configured_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
|
||||||
|
tracing::warn!(
|
||||||
|
"instance disk size does not match config (current {:.2} GB, config {:.2} GB). \
|
||||||
|
disk_gb applies only on init. Run `vibebox reset` to recreate or set disk_gb to match; using the existing disk.",
|
||||||
|
current_gb,
|
||||||
|
target_gb
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type StderrHandle = reload::Handle<LevelFilter, Registry>;
|
type StderrHandle = reload::Handle<LevelFilter, Registry>;
|
||||||
|
|
||||||
fn init_tracing(cwd: &Path) -> Option<StderrHandle> {
|
fn init_tracing(cwd: &Path) -> Option<StderrHandle> {
|
||||||
@@ -371,7 +395,7 @@ fn init_tracing(cwd: &Path) -> Option<StderrHandle> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if stderr_is_tty {
|
if stderr_is_tty {
|
||||||
let (stderr_filter, handle) = reload::Layer::new(LevelFilter::OFF);
|
let (stderr_filter, handle) = reload::Layer::new(LevelFilter::INFO);
|
||||||
let stderr_layer = fmt::layer()
|
let stderr_layer = fmt::layer()
|
||||||
.with_target(false)
|
.with_target(false)
|
||||||
.with_ansi(ansi)
|
.with_ansi(ansi)
|
||||||
|
|||||||
+16
-2
@@ -13,6 +13,7 @@ pub const CONFIG_PATH_ENV: &str = "VIBEBOX_CONFIG_PATH";
|
|||||||
const DEFAULT_CPU_COUNT: usize = 2;
|
const DEFAULT_CPU_COUNT: usize = 2;
|
||||||
const DEFAULT_RAM_MB: u64 = 2048;
|
const DEFAULT_RAM_MB: u64 = 2048;
|
||||||
const DEFAULT_AUTO_SHUTDOWN_MS: u64 = 20000;
|
const DEFAULT_AUTO_SHUTDOWN_MS: u64 = 20000;
|
||||||
|
const DEFAULT_DISK_GB: u64 = 5;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
@@ -25,6 +26,7 @@ pub struct Config {
|
|||||||
pub struct BoxConfig {
|
pub struct BoxConfig {
|
||||||
pub cpu_count: usize,
|
pub cpu_count: usize,
|
||||||
pub ram_mb: u64,
|
pub ram_mb: u64,
|
||||||
|
pub disk_gb: u64,
|
||||||
pub mounts: Vec<String>,
|
pub mounts: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +35,7 @@ impl Default for BoxConfig {
|
|||||||
Self {
|
Self {
|
||||||
cpu_count: default_cpu_count(),
|
cpu_count: default_cpu_count(),
|
||||||
ram_mb: default_ram_mb(),
|
ram_mb: default_ram_mb(),
|
||||||
|
disk_gb: default_disk_gb(),
|
||||||
mounts: default_mounts(),
|
mounts: default_mounts(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,7 +67,14 @@ fn default_auto_shutdown_ms() -> u64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn default_mounts() -> Vec<String> {
|
fn default_mounts() -> Vec<String> {
|
||||||
Vec::new()
|
vec![
|
||||||
|
"~/.codex:~/.codex:read-write".into(),
|
||||||
|
"~/.claude:~/.claude:read-write".into(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_disk_gb() -> u64 {
|
||||||
|
DEFAULT_DISK_GB
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn config_path(project_root: &Path) -> PathBuf {
|
pub fn config_path(project_root: &Path) -> PathBuf {
|
||||||
@@ -102,7 +112,7 @@ pub fn load_config_with_path(project_root: &Path, override_path: Option<&Path>)
|
|||||||
tracing::debug!(path = %path.display(), bytes = raw.len(), "loaded vibebox config");
|
tracing::debug!(path = %path.display(), bytes = raw.len(), "loaded vibebox config");
|
||||||
if trimmed.is_empty() {
|
if trimmed.is_empty() {
|
||||||
die(&format!(
|
die(&format!(
|
||||||
"config file ({}) is empty. Required fields: [box].cpu_count (integer), [box].ram_mb (integer), [box].mounts (array of strings), [supervisor].auto_shutdown_ms (integer)",
|
"config file ({}) is empty. Required fields: [box].cpu_count (integer), [box].ram_mb (integer), [box].disk_gb (integer), [box].mounts (array of strings), [supervisor].auto_shutdown_ms (integer)",
|
||||||
path.display()
|
path.display()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -193,6 +203,7 @@ fn validate_schema(value: &toml::Value) -> Vec<String> {
|
|||||||
Some(table) => {
|
Some(table) => {
|
||||||
validate_int(table, "cpu_count", "[box].cpu_count (integer)", &mut errors);
|
validate_int(table, "cpu_count", "[box].cpu_count (integer)", &mut errors);
|
||||||
validate_int(table, "ram_mb", "[box].ram_mb (integer)", &mut errors);
|
validate_int(table, "ram_mb", "[box].ram_mb (integer)", &mut errors);
|
||||||
|
validate_int(table, "disk_gb", "[box].disk_gb (integer)", &mut errors);
|
||||||
validate_string_array(
|
validate_string_array(
|
||||||
table,
|
table,
|
||||||
"mounts",
|
"mounts",
|
||||||
@@ -259,6 +270,9 @@ fn validate_or_exit(config: &Config) {
|
|||||||
if config.box_cfg.ram_mb == 0 {
|
if config.box_cfg.ram_mb == 0 {
|
||||||
die("box.ram_mb must be >= 1");
|
die("box.ram_mb must be >= 1");
|
||||||
}
|
}
|
||||||
|
if config.box_cfg.disk_gb == 0 {
|
||||||
|
die("box.disk_gb must be >= 1");
|
||||||
|
}
|
||||||
if config.supervisor.auto_shutdown_ms == 0 {
|
if config.supervisor.auto_shutdown_ms == 0 {
|
||||||
die("supervisor.auto_shutdown_ms must be >= 1");
|
die("supervisor.auto_shutdown_ms must be >= 1");
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -46,7 +46,7 @@ fn default_mounts(cwd: &Path) -> Result<Vec<tui::MountListRow>, Box<dyn Error +
|
|||||||
.file_name()
|
.file_name()
|
||||||
.and_then(|name| name.to_str())
|
.and_then(|name| name.to_str())
|
||||||
.unwrap_or("project");
|
.unwrap_or("project");
|
||||||
let project_guest = format!("/root/{project_name}");
|
let project_guest = format!("~/{project_name}");
|
||||||
let project_host = display_path(cwd);
|
let project_host = display_path(cwd);
|
||||||
let mut rows = vec![tui::MountListRow {
|
let mut rows = vec![tui::MountListRow {
|
||||||
host: project_host,
|
host: project_host,
|
||||||
|
|||||||
@@ -394,6 +394,7 @@ fn ssh_port_open(ip: &str) -> bool {
|
|||||||
pub(crate) fn build_ssh_login_actions(
|
pub(crate) fn build_ssh_login_actions(
|
||||||
config: &Arc<Mutex<InstanceConfig>>,
|
config: &Arc<Mutex<InstanceConfig>>,
|
||||||
project_name: &str,
|
project_name: &str,
|
||||||
|
project_guest_dir: &str,
|
||||||
guest_dir: &str,
|
guest_dir: &str,
|
||||||
key_name: &str,
|
key_name: &str,
|
||||||
home_links_script: &str,
|
home_links_script: &str,
|
||||||
@@ -409,6 +410,7 @@ pub(crate) fn build_ssh_login_actions(
|
|||||||
.replace("__SSH_USER__", &ssh_user)
|
.replace("__SSH_USER__", &ssh_user)
|
||||||
.replace("__SUDO_PASSWORD__", &sudo_password)
|
.replace("__SUDO_PASSWORD__", &sudo_password)
|
||||||
.replace("__PROJECT_NAME__", project_name)
|
.replace("__PROJECT_NAME__", project_name)
|
||||||
|
.replace("__PROJECT_GUEST_DIR__", project_guest_dir)
|
||||||
.replace("__KEY_PATH__", &key_path)
|
.replace("__KEY_PATH__", &key_path)
|
||||||
.replace("__VIBEBOX_SHELL_SCRIPT__", &commands::render_shell_script())
|
.replace("__VIBEBOX_SHELL_SCRIPT__", &commands::render_shell_script())
|
||||||
.replace("__VIBEBOX_HOME_LINKS__", home_links_script);
|
.replace("__VIBEBOX_HOME_LINKS__", home_links_script);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ apt-get install -y --no-install-recommends \
|
|||||||
curl \
|
curl \
|
||||||
git \
|
git \
|
||||||
ripgrep \
|
ripgrep \
|
||||||
|
cloud-guest-utils \
|
||||||
openssh-server \
|
openssh-server \
|
||||||
sudo
|
sudo
|
||||||
|
|
||||||
@@ -42,6 +43,11 @@ systemctl restart ssh
|
|||||||
# Set this env var so claude doesn't complain about running as root.'
|
# Set this env var so claude doesn't complain about running as root.'
|
||||||
echo "export IS_SANDBOX=1" >> .bashrc
|
echo "export IS_SANDBOX=1" >> .bashrc
|
||||||
|
|
||||||
|
# Ensure cloned instances generate unique machine-id on first boot.
|
||||||
|
truncate -s 0 /etc/machine-id
|
||||||
|
rm -f /var/lib/dbus/machine-id
|
||||||
|
ln -sf /etc/machine-id /var/lib/dbus/machine-id
|
||||||
|
|
||||||
# Shutdown the VM when you logout
|
# Shutdown the VM when you logout
|
||||||
cat > .bash_logout <<EOF
|
cat > .bash_logout <<EOF
|
||||||
systemctl poweroff
|
systemctl poweroff
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DEV="$(findmnt / -n -o SOURCE || true)"
|
||||||
|
ROOT_FSTYPE="$(findmnt / -n -o FSTYPE || true)"
|
||||||
|
|
||||||
|
if [ -z "$ROOT_DEV" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
DISK_DEV=""
|
||||||
|
PART_NUM=""
|
||||||
|
|
||||||
|
if command -v lsblk >/dev/null 2>&1; then
|
||||||
|
DISK_DEV="$(lsblk -no pkname "$ROOT_DEV" 2>/dev/null | head -n1 || true)"
|
||||||
|
PART_NUM="$(lsblk -no PARTNUM "$ROOT_DEV" 2>/dev/null | head -n1 || true)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$DISK_DEV" ] || [ -z "$PART_NUM" ]; then
|
||||||
|
ROOT_BASENAME="$(basename "$ROOT_DEV")"
|
||||||
|
if echo "$ROOT_BASENAME" | grep -Eq '^nvme.+p[0-9]+$'; then
|
||||||
|
DISK_DEV="/dev/${ROOT_BASENAME%p[0-9]*}"
|
||||||
|
PART_NUM="${ROOT_BASENAME##*p}"
|
||||||
|
elif echo "$ROOT_BASENAME" | grep -Eq '^[a-z]+[0-9]+$'; then
|
||||||
|
DISK_DEV="/dev/${ROOT_BASENAME%%[0-9]*}"
|
||||||
|
PART_NUM="${ROOT_BASENAME##*[a-z]}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$DISK_DEV" ] && [ -n "$PART_NUM" ]; then
|
||||||
|
if command -v growpart >/dev/null 2>&1; then
|
||||||
|
growpart "$DISK_DEV" "$PART_NUM" || true
|
||||||
|
elif command -v sfdisk >/dev/null 2>&1; then
|
||||||
|
sfdisk -N "$PART_NUM" --force "$DISK_DEV" <<'EOF' || true
|
||||||
|
,,
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v partprobe >/dev/null 2>&1; then
|
||||||
|
partprobe "$DISK_DEV" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$ROOT_FSTYPE" in
|
||||||
|
ext4|ext3|ext2)
|
||||||
|
if command -v resize2fs >/dev/null 2>&1; then
|
||||||
|
resize2fs "$ROOT_DEV" || true
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
xfs)
|
||||||
|
if command -v xfs_growfs >/dev/null 2>&1; then
|
||||||
|
xfs_growfs / || true
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -117,7 +117,7 @@ impl SessionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if removed > 0 || added {
|
if removed > 0 || added {
|
||||||
tracing::info!(
|
tracing::debug!(
|
||||||
path = %self.sessions_dir.display(),
|
path = %self.sessions_dir.display(),
|
||||||
removed,
|
removed,
|
||||||
added,
|
added,
|
||||||
@@ -196,7 +196,7 @@ impl SessionManager {
|
|||||||
let path = self.session_path_for(&record.id);
|
let path = self.session_path_for(&record.id);
|
||||||
let content = toml::to_string_pretty(record)?;
|
let content = toml::to_string_pretty(record)?;
|
||||||
atomic_write(&path, content.as_bytes())?;
|
atomic_write(&path, content.as_bytes())?;
|
||||||
tracing::info!(
|
tracing::debug!(
|
||||||
path = %path.display(),
|
path = %path.display(),
|
||||||
"wrote session record"
|
"wrote session record"
|
||||||
);
|
);
|
||||||
|
|||||||
+51
-14
@@ -3,6 +3,7 @@ set -eu
|
|||||||
|
|
||||||
SSH_USER="__SSH_USER__"
|
SSH_USER="__SSH_USER__"
|
||||||
PROJECT_NAME="__PROJECT_NAME__"
|
PROJECT_NAME="__PROJECT_NAME__"
|
||||||
|
PROJECT_GUEST_DIR="__PROJECT_GUEST_DIR__"
|
||||||
KEY_PATH="__KEY_PATH__"
|
KEY_PATH="__KEY_PATH__"
|
||||||
|
|
||||||
diag() { echo "[vibebox][diag] $*" >&2; }
|
diag() { echo "[vibebox][diag] $*" >&2; }
|
||||||
@@ -49,7 +50,7 @@ dump_diag() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# 1) tmpfs mount
|
# 1) tmpfs mount
|
||||||
TARGET="/root/${PROJECT_NAME}/.vibebox"
|
TARGET="${PROJECT_GUEST_DIR}/.vibebox"
|
||||||
if [ -d "$TARGET" ] && ! mountpoint -q "$TARGET"; then
|
if [ -d "$TARGET" ] && ! mountpoint -q "$TARGET"; then
|
||||||
mount -t tmpfs tmpfs "$TARGET"
|
mount -t tmpfs tmpfs "$TARGET"
|
||||||
fi
|
fi
|
||||||
@@ -78,6 +79,21 @@ __VIBEBOX_SHELL_SCRIPT__
|
|||||||
VIBEBOX_SHELL_EOF
|
VIBEBOX_SHELL_EOF
|
||||||
chmod 644 /etc/profile.d/vibebox.sh
|
chmod 644 /etc/profile.d/vibebox.sh
|
||||||
|
|
||||||
|
# Auto-cd into project for interactive shells
|
||||||
|
cat > /etc/profile.d/vibebox-project.sh <<'VIBEBOX_PROJECT_EOF'
|
||||||
|
case "$-" in
|
||||||
|
*i*)
|
||||||
|
project_home="${HOME}/__PROJECT_NAME__"
|
||||||
|
if [ "$USER" = "__SSH_USER__" ] && [ -d "$project_home" ]; then
|
||||||
|
cd "$project_home"
|
||||||
|
elif [ "$USER" = "__SSH_USER__" ] && [ -d "__PROJECT_GUEST_DIR__" ]; then
|
||||||
|
cd "__PROJECT_GUEST_DIR__"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
VIBEBOX_PROJECT_EOF
|
||||||
|
chmod 644 /etc/profile.d/vibebox-project.sh
|
||||||
|
|
||||||
if ! grep -q "vibebox-aliases" "${USER_HOME}/.bashrc" 2>/dev/null; then
|
if ! grep -q "vibebox-aliases" "${USER_HOME}/.bashrc" 2>/dev/null; then
|
||||||
{
|
{
|
||||||
echo ""
|
echo ""
|
||||||
@@ -88,16 +104,22 @@ fi
|
|||||||
|
|
||||||
# Install Mise
|
# Install Mise
|
||||||
MISE_BIN="${USER_HOME}/.local/bin/mise"
|
MISE_BIN="${USER_HOME}/.local/bin/mise"
|
||||||
if [ ! -x "$MISE_BIN" ] && ! command -v mise >/dev/null 2>&1; then
|
mise_warn() { echo "[mise] $*" >&2; }
|
||||||
curl https://mise.run | HOME="$USER_HOME" sh
|
mise_ok() { command -v mise >/dev/null 2>&1 || [ -x "$MISE_BIN" ]; }
|
||||||
fi
|
mise_install() {
|
||||||
echo 'eval "$(~/.local/bin/mise activate bash)"' >> "${USER_HOME}/.bashrc"
|
if [ ! -x "$MISE_BIN" ] && ! command -v mise >/dev/null 2>&1; then
|
||||||
|
if ! curl https://mise.run | HOME="$USER_HOME" sh; then
|
||||||
|
mise_warn "mise install script failed (continuing)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo 'eval "$(~/.local/bin/mise activate bash)"' >> "${USER_HOME}/.bashrc"
|
||||||
|
|
||||||
export PATH="${USER_HOME}/.local/bin:/usr/local/bin:$PATH"
|
export PATH="${USER_HOME}/.local/bin:/usr/local/bin:$PATH"
|
||||||
|
|
||||||
mkdir -p "${USER_HOME}/.config/mise"
|
mkdir -p "${USER_HOME}/.config/mise"
|
||||||
|
|
||||||
cat > "${USER_HOME}/.config/mise/config.toml" <<MISE
|
cat > "${USER_HOME}/.config/mise/config.toml" <<MISE
|
||||||
[settings]
|
[settings]
|
||||||
# Always use the venv created by uv, if available in directory
|
# Always use the venv created by uv, if available in directory
|
||||||
python.uv_venv_auto = true
|
python.uv_venv_auto = true
|
||||||
@@ -111,12 +133,21 @@ cat > "${USER_HOME}/.config/mise/config.toml" <<MISE
|
|||||||
"npm:@anthropic-ai/claude-code" = "latest"
|
"npm:@anthropic-ai/claude-code" = "latest"
|
||||||
MISE
|
MISE
|
||||||
|
|
||||||
touch "${USER_HOME}/.config/mise/mise.lock"
|
touch "${USER_HOME}/.config/mise/mise.lock"
|
||||||
if [ -x "$MISE_BIN" ]; then
|
if [ -x "$MISE_BIN" ]; then
|
||||||
HOME="$USER_HOME" "$MISE_BIN" install
|
if ! HOME="$USER_HOME" "$MISE_BIN" install; then
|
||||||
else
|
mise_warn "mise install failed (continuing)"
|
||||||
HOME="$USER_HOME" mise install
|
return 0
|
||||||
fi
|
fi
|
||||||
|
else
|
||||||
|
if ! HOME="$USER_HOME" mise install; then
|
||||||
|
mise_warn "mise install failed (continuing)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
mise_install || true
|
||||||
|
|
||||||
# 3) start ssh (don't swallow failures)
|
# 3) start ssh (don't swallow failures)
|
||||||
# If ssh is already active, don't force start/restart.
|
# If ssh is already active, don't force start/restart.
|
||||||
@@ -184,5 +215,11 @@ if ! listens_ok; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
ip a
|
||||||
|
ip link
|
||||||
|
curl -s https://api.ipify.org ; echo
|
||||||
|
|
||||||
|
cat /etc/machine-id
|
||||||
|
|
||||||
echo VIBEBOX_SSH_READY
|
echo VIBEBOX_SSH_READY
|
||||||
echo "VIBEBOX_IPV4=$ip"
|
echo "VIBEBOX_IPV4=$ip"
|
||||||
|
|||||||
+5
-3
@@ -25,6 +25,7 @@ use ratatui::{
|
|||||||
|
|
||||||
use crate::vm;
|
use crate::vm;
|
||||||
|
|
||||||
|
// https://patorjk.com/software/taag/#p=display&f=ANSI+Shadow&t=VIBEBOX&x=none&v=4&h=4&w=80&we=false
|
||||||
const ASCII_BANNER: [&str; 7] = [
|
const ASCII_BANNER: [&str; 7] = [
|
||||||
"██╗ ██╗██╗██████╗ ███████╗██████╗ ██████╗ ██╗ ██╗",
|
"██╗ ██╗██╗██████╗ ███████╗██████╗ ██████╗ ██╗ ██╗",
|
||||||
"██║ ██║██║██╔══██╗██╔════╝██╔══██╗██╔═══██╗╚██╗██╔╝",
|
"██║ ██║██║██╔══██╗██╔════╝██╔══██╗██╔═══██╗╚██╗██╔╝",
|
||||||
@@ -40,6 +41,7 @@ const INFO_LINE_COUNT: u16 = 5;
|
|||||||
pub struct VmInfo {
|
pub struct VmInfo {
|
||||||
pub max_memory_mb: u64,
|
pub max_memory_mb: u64,
|
||||||
pub cpu_cores: usize,
|
pub cpu_cores: usize,
|
||||||
|
pub max_disk_gb: f32,
|
||||||
pub system_name: String,
|
pub system_name: String,
|
||||||
pub auto_shutdown_ms: u64,
|
pub auto_shutdown_ms: u64,
|
||||||
}
|
}
|
||||||
@@ -569,11 +571,11 @@ fn render_header(buffer: &mut Buffer, area: Rect, app: &AppState) {
|
|||||||
Span::styled(&app.vm_info.system_name, Style::default().fg(Color::Green)),
|
Span::styled(&app.vm_info.system_name, Style::default().fg(Color::Green)),
|
||||||
]),
|
]),
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::raw("CPU / Memory: "),
|
Span::raw("CPU / Memory / Disk: "),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
format!(
|
format!(
|
||||||
"{} cores / {} MB",
|
"{} cores / {} MB / {} GB",
|
||||||
app.vm_info.cpu_cores, app.vm_info.max_memory_mb
|
app.vm_info.cpu_cores, app.vm_info.max_memory_mb, app.vm_info.max_disk_gb
|
||||||
),
|
),
|
||||||
Style::default().fg(Color::Green),
|
Style::default().fg(Color::Green),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const DEBIAN_COMPRESSED_DISK_URL: &str = "https://cloud.debian.org/images/cloud/
|
|||||||
const DEBIAN_COMPRESSED_SHA: &str = "6ab9be9e6834adc975268367f2f0235251671184345c34ee13031749fdfbf66fe4c3aafd949a2d98550426090e9ac645e79009c51eb0eefc984c15786570bb38";
|
const DEBIAN_COMPRESSED_SHA: &str = "6ab9be9e6834adc975268367f2f0235251671184345c34ee13031749fdfbf66fe4c3aafd949a2d98550426090e9ac645e79009c51eb0eefc984c15786570bb38";
|
||||||
const DEBIAN_COMPRESSED_SIZE_BYTES: u64 = 280901576;
|
const DEBIAN_COMPRESSED_SIZE_BYTES: u64 = 280901576;
|
||||||
const SHARED_DIRECTORIES_TAG: &str = "shared";
|
const SHARED_DIRECTORIES_TAG: &str = "shared";
|
||||||
|
pub const PROJECT_GUEST_BASE: &str = "/usr/local/vibebox-mounts";
|
||||||
|
|
||||||
const BYTES_PER_MB: u64 = 1024 * 1024;
|
const BYTES_PER_MB: u64 = 1024 * 1024;
|
||||||
const DEFAULT_CPU_COUNT: usize = 2;
|
const DEFAULT_CPU_COUNT: usize = 2;
|
||||||
@@ -74,6 +75,7 @@ impl Drop for StatusFile {
|
|||||||
}
|
}
|
||||||
const PROVISION_SCRIPT: &str = include_str!("provision.sh");
|
const PROVISION_SCRIPT: &str = include_str!("provision.sh");
|
||||||
const PROVISION_SCRIPT_NAME: &str = "provision.sh";
|
const PROVISION_SCRIPT_NAME: &str = "provision.sh";
|
||||||
|
const RESIZE_DISK_SCRIPT: &str = include_str!("resize_disk.sh");
|
||||||
const DEFAULT_RAW_NAME: &str = "default.raw";
|
const DEFAULT_RAW_NAME: &str = "default.raw";
|
||||||
const INSTANCE_RAW_NAME: &str = "instance.raw";
|
const INSTANCE_RAW_NAME: &str = "instance.raw";
|
||||||
const BASE_DISK_RAW_NAME: &str = "disk.raw";
|
const BASE_DISK_RAW_NAME: &str = "disk.raw";
|
||||||
@@ -166,6 +168,7 @@ fn expand_tilde_path(value: &str) -> PathBuf {
|
|||||||
pub struct VmArg {
|
pub struct VmArg {
|
||||||
pub cpu_count: usize,
|
pub cpu_count: usize,
|
||||||
pub ram_bytes: u64,
|
pub ram_bytes: u64,
|
||||||
|
pub disk_bytes: u64,
|
||||||
pub no_default_mounts: bool,
|
pub no_default_mounts: bool,
|
||||||
pub mounts: Vec<String>,
|
pub mounts: Vec<String>,
|
||||||
}
|
}
|
||||||
@@ -230,14 +233,23 @@ where
|
|||||||
std::slice::from_ref(&mise_directory_share),
|
std::slice::from_ref(&mise_directory_share),
|
||||||
Some(&status_file),
|
Some(&status_file),
|
||||||
)?;
|
)?;
|
||||||
ensure_instance_disk(&instance_raw, &default_raw, Some(&status_file))?;
|
let _ = ensure_instance_disk(
|
||||||
|
&instance_raw,
|
||||||
|
&default_raw,
|
||||||
|
args.disk_bytes,
|
||||||
|
Some(&status_file),
|
||||||
|
)?;
|
||||||
|
let base_size = fs::metadata(&default_raw)?.len();
|
||||||
|
let instance_size = fs::metadata(&instance_raw)?.len();
|
||||||
|
let needs_resize = instance_size > base_size;
|
||||||
let disk_path = instance_raw;
|
let disk_path = instance_raw;
|
||||||
|
|
||||||
let mut login_actions = Vec::new();
|
let mut login_actions = Vec::new();
|
||||||
let mut directory_shares = Vec::new();
|
let mut directory_shares = Vec::new();
|
||||||
|
|
||||||
if !args.no_default_mounts {
|
if !args.no_default_mounts {
|
||||||
login_actions.push(Send(format!("cd {project_name}")));
|
let project_guest_dir = PathBuf::from(PROJECT_GUEST_BASE).join(project_name);
|
||||||
|
login_actions.push(Send(format!("cd {}", project_guest_dir.display())));
|
||||||
|
|
||||||
// discourage read/write of .git folder from within the VM. note that this isn't secure, since the VM runs as root and could unmount this.
|
// discourage read/write of .git folder from within the VM. note that this isn't secure, since the VM runs as root and could unmount this.
|
||||||
// I couldn't find an alternative way to do this --- the MacOS sandbox doesn't apply to the Apple Virtualization system
|
// I couldn't find an alternative way to do this --- the MacOS sandbox doesn't apply to the Apple Virtualization system
|
||||||
@@ -245,15 +257,6 @@ where
|
|||||||
login_actions.push(Send(r"mount -t tmpfs tmpfs .git/".into()));
|
login_actions.push(Send(r"mount -t tmpfs tmpfs .git/".into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
directory_shares.push(
|
|
||||||
DirectoryShare::new(
|
|
||||||
project_root,
|
|
||||||
PathBuf::from("/root/").join(project_name),
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.expect("Project directory must exist"),
|
|
||||||
);
|
|
||||||
|
|
||||||
directory_shares.push(mise_directory_share);
|
directory_shares.push(mise_directory_share);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,6 +266,11 @@ where
|
|||||||
directory_shares.push(DirectoryShare::from_mount_spec(spec)?);
|
directory_shares.push(DirectoryShare::from_mount_spec(spec)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if needs_resize {
|
||||||
|
let resize_cmd = script_command_from_content("resize_disk", RESIZE_DISK_SCRIPT)?;
|
||||||
|
login_actions.push(Send(resize_cmd));
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(motd_action) = motd_login_action(&directory_shares) {
|
if let Some(motd_action) = motd_login_action(&directory_shares) {
|
||||||
login_actions.push(motd_action);
|
login_actions.push(motd_action);
|
||||||
}
|
}
|
||||||
@@ -559,19 +567,51 @@ fn ensure_default_image(
|
|||||||
fn ensure_instance_disk(
|
fn ensure_instance_disk(
|
||||||
instance_raw: &Path,
|
instance_raw: &Path,
|
||||||
template_raw: &Path,
|
template_raw: &Path,
|
||||||
|
target_bytes: u64,
|
||||||
status: Option<&StatusFile>,
|
status: Option<&StatusFile>,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<bool, Box<dyn std::error::Error>> {
|
||||||
if instance_raw.exists() {
|
if instance_raw.exists() {
|
||||||
return Ok(());
|
let current_size = fs::metadata(instance_raw)?.len();
|
||||||
|
if current_size != target_bytes {
|
||||||
|
let current_gb = current_size as f64 / (1024.0 * 1024.0 * 1024.0);
|
||||||
|
let target_gb = target_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
|
||||||
|
tracing::warn!(
|
||||||
|
current_bytes = current_size,
|
||||||
|
target_bytes,
|
||||||
|
"instance disk size does not match config (current {:.2} GB, config {:.2} GB); disk_gb applies only on init. Run `vibebox reset` to recreate or set disk_gb to match; using existing disk.",
|
||||||
|
current_gb,
|
||||||
|
target_gb
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let template_size = fs::metadata(template_raw)?.len();
|
||||||
|
if target_bytes < template_size {
|
||||||
|
return Err(format!(
|
||||||
|
"Requested disk size {} bytes is smaller than base image size {} bytes",
|
||||||
|
target_bytes, template_size
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
let target_size = target_bytes;
|
||||||
|
let needs_resize = target_size > template_size;
|
||||||
|
|
||||||
if let Some(status) = status {
|
if let Some(status) = status {
|
||||||
status.update("creating instance disk...");
|
status.update("creating instance disk...");
|
||||||
}
|
}
|
||||||
tracing::info!(path = %template_raw.display(), "creating instance disk");
|
tracing::info!(path = %template_raw.display(), "creating instance disk");
|
||||||
std::fs::create_dir_all(instance_raw.parent().unwrap())?;
|
std::fs::create_dir_all(instance_raw.parent().unwrap())?;
|
||||||
fs::copy(template_raw, instance_raw)?;
|
if target_size == template_size {
|
||||||
Ok(())
|
fs::copy(template_raw, instance_raw)?;
|
||||||
|
return Ok(needs_resize);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut dst = std::fs::File::create(instance_raw)?;
|
||||||
|
dst.set_len(target_size)?;
|
||||||
|
let mut src = std::fs::File::open(template_raw)?;
|
||||||
|
std::io::copy(&mut src, &mut dst)?;
|
||||||
|
Ok(needs_resize)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct IoContext {
|
pub struct IoContext {
|
||||||
|
|||||||
+66
-7
@@ -25,11 +25,12 @@ use crate::{
|
|||||||
session_manager::{
|
session_manager::{
|
||||||
GLOBAL_DIR_NAME, INSTANCE_FILENAME, VM_MANAGER_PID_NAME, VM_MANAGER_SOCKET_NAME,
|
GLOBAL_DIR_NAME, INSTANCE_FILENAME, VM_MANAGER_PID_NAME, VM_MANAGER_SOCKET_NAME,
|
||||||
},
|
},
|
||||||
vm::{self, DirectoryShare, LoginAction, VmInput},
|
vm::{self, DirectoryShare, LoginAction, PROJECT_GUEST_BASE, VmInput},
|
||||||
};
|
};
|
||||||
|
|
||||||
const VM_MANAGER_LOCK_NAME: &str = "vm.lock";
|
const VM_MANAGER_LOCK_NAME: &str = "vm.lock";
|
||||||
const VM_MANAGER_LOG_NAME: &str = "vm_manager.log";
|
const VM_MANAGER_LOG_NAME: &str = "vm_manager.log";
|
||||||
|
const HARD_SHUTDOWN_TIMEOUT_MS: u64 = 12_000;
|
||||||
|
|
||||||
pub fn ensure_manager(
|
pub fn ensure_manager(
|
||||||
raw_args: &[std::ffi::OsString],
|
raw_args: &[std::ffi::OsString],
|
||||||
@@ -200,6 +201,30 @@ fn cleanup_stale_manager(instance_dir: &Path) {
|
|||||||
let _ = fs::remove_file(&pid_path);
|
let _ = fs::remove_file(&pid_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn inject_project_mount(
|
||||||
|
mounts: &mut Vec<String>,
|
||||||
|
project_root: &Path,
|
||||||
|
ssh_user: &str,
|
||||||
|
project_name: &str,
|
||||||
|
) {
|
||||||
|
let guest_tilde = format!("~/{project_name}");
|
||||||
|
let guest_home = format!("/home/{ssh_user}/{project_name}");
|
||||||
|
let guest_base = format!("{PROJECT_GUEST_BASE}/{project_name}");
|
||||||
|
let already_mapped = mounts.iter().any(|spec| {
|
||||||
|
let parts: Vec<&str> = spec.split(':').collect();
|
||||||
|
if parts.len() < 2 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let guest = parts[1];
|
||||||
|
guest == guest_tilde || guest == guest_home || guest == guest_base
|
||||||
|
});
|
||||||
|
if already_mapped {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let host = project_root.display();
|
||||||
|
mounts.insert(0, format!("{host}:{guest_tilde}:read-write"));
|
||||||
|
}
|
||||||
|
|
||||||
fn is_socket_path(path: &Path) -> bool {
|
fn is_socket_path(path: &Path) -> bool {
|
||||||
fs::metadata(path)
|
fs::metadata(path)
|
||||||
.map(|meta| meta.file_type().is_socket())
|
.map(|meta| meta.file_type().is_socket())
|
||||||
@@ -252,7 +277,7 @@ fn rewrite_mount_spec(spec: &str, ssh_user: &str) -> (String, Option<HomeLink>)
|
|||||||
return (spec.to_string(), None);
|
return (spec.to_string(), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
let root_base = "/usr/local/vibebox-mounts";
|
let root_base = PROJECT_GUEST_BASE;
|
||||||
let root_path = if rel.is_empty() {
|
let root_path = if rel.is_empty() {
|
||||||
root_base.to_string()
|
root_base.to_string()
|
||||||
} else {
|
} else {
|
||||||
@@ -563,7 +588,7 @@ impl VmExecutor for RealVmExecutor {
|
|||||||
|
|
||||||
fn run_manager_with(
|
fn run_manager_with(
|
||||||
project_root: &Path,
|
project_root: &Path,
|
||||||
args: vm::VmArg,
|
mut args: vm::VmArg,
|
||||||
auto_shutdown_ms: u64,
|
auto_shutdown_ms: u64,
|
||||||
executor: &dyn VmExecutor,
|
executor: &dyn VmExecutor,
|
||||||
options: ManagerOptions,
|
options: ManagerOptions,
|
||||||
@@ -602,8 +627,12 @@ fn run_manager_with(
|
|||||||
.lock()
|
.lock()
|
||||||
.map(|cfg| cfg.ssh_user_display())
|
.map(|cfg| cfg.ssh_user_display())
|
||||||
.unwrap_or_else(|_| DEFAULT_SSH_USER.to_string());
|
.unwrap_or_else(|_| DEFAULT_SSH_USER.to_string());
|
||||||
|
if !args.no_default_mounts {
|
||||||
|
inject_project_mount(&mut args.mounts, project_root, &ssh_user, &project_name);
|
||||||
|
}
|
||||||
let (args, home_links_script) = prepare_mounts_and_links(args, &ssh_user);
|
let (args, home_links_script) = prepare_mounts_and_links(args, &ssh_user);
|
||||||
|
|
||||||
|
let project_guest_dir = format!("{PROJECT_GUEST_BASE}/{project_name}");
|
||||||
let ssh_guest_dir = format!("/root/{}", GLOBAL_DIR_NAME);
|
let ssh_guest_dir = format!("/root/{}", GLOBAL_DIR_NAME);
|
||||||
let extra_shares = vec![DirectoryShare::new(
|
let extra_shares = vec![DirectoryShare::new(
|
||||||
instance_dir.clone(),
|
instance_dir.clone(),
|
||||||
@@ -613,6 +642,7 @@ fn run_manager_with(
|
|||||||
let extra_login_actions = build_ssh_login_actions(
|
let extra_login_actions = build_ssh_login_actions(
|
||||||
&config,
|
&config,
|
||||||
&project_name,
|
&project_name,
|
||||||
|
&project_guest_dir,
|
||||||
ssh_guest_dir.as_str(),
|
ssh_guest_dir.as_str(),
|
||||||
"ssh_key",
|
"ssh_key",
|
||||||
&home_links_script,
|
&home_links_script,
|
||||||
@@ -692,7 +722,9 @@ fn manager_event_loop(
|
|||||||
let mut ref_count: usize = 0;
|
let mut ref_count: usize = 0;
|
||||||
let mut shutdown_deadline: Option<Instant> = None;
|
let mut shutdown_deadline: Option<Instant> = None;
|
||||||
let mut shutdown_sent = false;
|
let mut shutdown_sent = false;
|
||||||
|
let mut hard_deadline: Option<Instant> = None;
|
||||||
let grace = Duration::from_millis(auto_shutdown_ms.max(1));
|
let grace = Duration::from_millis(auto_shutdown_ms.max(1));
|
||||||
|
let hard_timeout = Duration::from_millis(HARD_SHUTDOWN_TIMEOUT_MS);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let timeout = match shutdown_deadline {
|
let timeout = match shutdown_deadline {
|
||||||
@@ -711,6 +743,7 @@ fn manager_event_loop(
|
|||||||
);
|
);
|
||||||
shutdown_deadline = None;
|
shutdown_deadline = None;
|
||||||
shutdown_sent = false;
|
shutdown_sent = false;
|
||||||
|
hard_deadline = None;
|
||||||
}
|
}
|
||||||
Ok(ManagerEvent::Dec(pid)) => {
|
Ok(ManagerEvent::Dec(pid)) => {
|
||||||
ref_count = ref_count.saturating_sub(1);
|
ref_count = ref_count.saturating_sub(1);
|
||||||
@@ -736,12 +769,38 @@ fn manager_event_loop(
|
|||||||
&& Instant::now() >= deadline
|
&& Instant::now() >= deadline
|
||||||
&& !shutdown_sent
|
&& !shutdown_sent
|
||||||
{
|
{
|
||||||
|
let mut sent = false;
|
||||||
if let Some(tx) = vm_input_tx.lock().unwrap().clone() {
|
if let Some(tx) = vm_input_tx.lock().unwrap().clone() {
|
||||||
let _ = tx.send(VmInput::Bytes(b"systemctl poweroff\n".to_vec()));
|
if tx
|
||||||
|
.send(VmInput::Bytes(b"systemctl poweroff\n".to_vec()))
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
sent = true;
|
||||||
|
} else {
|
||||||
|
tracing::warn!("shutdown command failed to send");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tracing::warn!("shutdown command deferred; vm input not ready");
|
||||||
}
|
}
|
||||||
tracing::info!("shutdown command sent");
|
if sent {
|
||||||
shutdown_sent = true;
|
tracing::info!("shutdown command sent");
|
||||||
shutdown_deadline = None;
|
shutdown_sent = true;
|
||||||
|
shutdown_deadline = None;
|
||||||
|
hard_deadline = Some(Instant::now() + hard_timeout);
|
||||||
|
} else {
|
||||||
|
shutdown_deadline = Some(Instant::now() + Duration::from_millis(500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ref_count == 0
|
||||||
|
&& shutdown_sent
|
||||||
|
&& let Some(deadline) = hard_deadline
|
||||||
|
&& Instant::now() >= deadline
|
||||||
|
{
|
||||||
|
tracing::warn!(
|
||||||
|
timeout_ms = HARD_SHUTDOWN_TIMEOUT_MS,
|
||||||
|
"force exiting: VM did not stop after shutdown timeout"
|
||||||
|
);
|
||||||
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(mpsc::RecvTimeoutError::Disconnected) => break,
|
Err(mpsc::RecvTimeoutError::Disconnected) => break,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[box]
|
[box]
|
||||||
cpu_count = 2
|
cpu_count = 2
|
||||||
ram_mb = 2048
|
ram_mb = 2048
|
||||||
|
disk_gb = 5
|
||||||
mounts = [
|
mounts = [
|
||||||
"~/.codex:~/.codex:read-write",
|
"~/.codex:~/.codex:read-write",
|
||||||
"~/.claude:~/.claude:read-write",
|
"~/.claude:~/.claude:read-write",
|
||||||
|
|||||||
Reference in New Issue
Block a user