bottleship 0.1.0-prototype.1 → 0.1.0-prototype.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +118 -117
- package/dist/src/boxes.d.ts +41 -39
- package/dist/src/boxes.js +181 -179
- package/dist/src/cli.js +298 -84
- package/dist/src/config.d.ts +3 -6
- package/dist/src/config.js +19 -85
- package/dist/src/init.d.ts +2 -4
- package/dist/src/init.js +92 -228
- package/dist/src/local.d.ts +8 -0
- package/dist/src/local.js +24 -0
- package/dist/src/onboard.d.ts +5 -0
- package/dist/src/onboard.js +25 -0
- package/dist/src/paths.d.ts +0 -1
- package/dist/src/paths.js +0 -1
- package/dist/src/provider.d.ts +16 -0
- package/dist/src/provider.js +81 -0
- package/dist/src/repo.d.ts +21 -0
- package/dist/src/repo.js +75 -0
- package/dist/src/review/comments.d.ts +66 -0
- package/dist/src/review/comments.js +344 -0
- package/dist/src/review/contract.d.ts +82 -0
- package/dist/src/review/contract.js +189 -0
- package/dist/src/review/github.d.ts +120 -0
- package/dist/src/review/github.js +160 -0
- package/dist/src/review/local.d.ts +14 -0
- package/dist/src/review/local.js +70 -0
- package/dist/src/review/loop.d.ts +12 -0
- package/dist/src/review/loop.js +119 -0
- package/dist/src/review/pr.d.ts +14 -0
- package/dist/src/review/pr.js +68 -0
- package/dist/src/review/prompt.d.ts +15 -0
- package/dist/src/review/prompt.js +48 -0
- package/dist/src/review/serve.d.ts +1 -0
- package/dist/src/review/serve.js +8 -0
- package/dist/src/review/server.d.ts +14 -0
- package/dist/src/review/server.js +484 -0
- package/dist/src/review/setting-up.d.ts +4 -0
- package/dist/src/review/setting-up.js +74 -0
- package/dist/src/review/start.d.ts +32 -0
- package/dist/src/review/start.js +140 -0
- package/dist/src/review/target.d.ts +20 -0
- package/dist/src/review/target.js +66 -0
- package/dist/src/review/template.d.ts +3 -0
- package/dist/src/review/template.js +13 -0
- package/dist/src/review/terminal.d.ts +4 -0
- package/dist/src/review/terminal.js +91 -0
- package/dist/src/types.d.ts +1 -71
- package/dist/src/ui.d.ts +6 -1
- package/dist/src/ui.js +22 -23
- package/dist/src/util.d.ts +11 -0
- package/dist/src/util.js +40 -0
- package/package.json +7 -14
- package/presets/agents/claude.json +1 -5
- package/presets/agents/codex.json +1 -10
- package/presets/agents/cursor.json +1 -9
- package/presets/agents/opencode.json +1 -14
- package/presets/providers/docker/create +12 -0
- package/presets/providers/docker/destroy +2 -0
- package/presets/providers/docker/exec +2 -0
- package/presets/providers/docker/list +4 -0
- package/presets/providers/docker/status +2 -0
- package/presets/providers/docker/url +4 -0
- package/review-app/KIT.md +106 -0
- package/review-app/WRITING.md +93 -0
- package/review-app/examples/rply-16.tsx +98 -0
- package/review-app/index.html +15 -0
- package/review-app/package-lock.json +2539 -0
- package/review-app/package.json +24 -0
- package/review-app/playground/fixtures/excalidraw-12602/data/box.json +1 -0
- package/review-app/playground/fixtures/excalidraw-12602/data/pr.diff +91 -0
- package/review-app/playground/fixtures/excalidraw-12602/data/pr.json +62 -0
- package/review-app/playground/fixtures/excalidraw-12602/data/review-comments.json +44 -0
- package/review-app/playground/fixtures/excalidraw-12602/events.json +1 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/base-ar-zoom.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/base-en-US-zoom.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/base-he-IL-zoom.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/base-he-dark.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/base-he-hover.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/base-he-page.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/base-he-tip.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/base-he-zen.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/head-ar-zoom.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/head-en-US-zoom.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/head-he-IL-zoom.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/head-he-dark.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/head-he-hover.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/head-he-page.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/head-he-tip.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/files/shots/head-he-zen.png +0 -0
- package/review-app/playground/fixtures/excalidraw-12602/instances.json +1 -0
- package/review-app/playground/fixtures/excalidraw-12602/review.tsx +103 -0
- package/review-app/playground/fixtures/rply-16/data/box.json +1 -0
- package/review-app/playground/fixtures/rply-16/data/hooks.diff +28 -0
- package/review-app/playground/fixtures/rply-16/data/pr.diff +114 -0
- package/review-app/playground/fixtures/rply-16/data/pr.json +68 -0
- package/review-app/playground/fixtures/rply-16/data/review-comments.json +1 -0
- package/review-app/playground/fixtures/rply-16/events.json +156 -0
- package/review-app/playground/fixtures/rply-16/instances.json +1 -0
- package/review-app/playground/fixtures/rply-16/review.tsx +98 -0
- package/review-app/playground/gallery.tsx +127 -0
- package/review-app/playground/index.html +15 -0
- package/review-app/playground/main.tsx +26 -0
- package/review-app/playground/mock.ts +215 -0
- package/review-app/playground/playground.css +18 -0
- package/review-app/playground/vite.config.ts +32 -0
- package/review-app/scripts/check.cjs +47 -0
- package/review-app/scripts/clip.cjs +104 -0
- package/review-app/scripts/shot.cjs +66 -0
- package/review-app/src/App.tsx +132 -0
- package/review-app/src/data/box.json +1 -0
- package/review-app/src/data/hooks.diff +28 -0
- package/review-app/src/data/pr.diff +114 -0
- package/review-app/src/data/pr.json +68 -0
- package/review-app/src/data/review-comments.json +1 -0
- package/review-app/src/kit/agent.tsx +287 -0
- package/review-app/src/kit/chapter.tsx +178 -0
- package/review-app/src/kit/code.tsx +217 -0
- package/review-app/src/kit/comments.tsx +377 -0
- package/review-app/src/kit/finish.tsx +274 -0
- package/review-app/src/kit/flow.tsx +184 -0
- package/review-app/src/kit/index.ts +10 -0
- package/review-app/src/kit/kit.css +696 -0
- package/review-app/src/kit/live.ts +91 -0
- package/review-app/src/kit/media.tsx +348 -0
- package/review-app/src/kit/overview.tsx +161 -0
- package/review-app/src/kit/pr.ts +100 -0
- package/review-app/src/kit/pullrequest.tsx +298 -0
- package/review-app/src/kit/review.ts +74 -0
- package/review-app/src/kit/theme.ts +30 -0
- package/review-app/src/kit/ui.tsx +127 -0
- package/review-app/src/main.tsx +6 -0
- package/review-app/src/review.tsx +22 -0
- package/review-app/tsconfig.json +17 -0
- package/review-app/vite.config.ts +12 -0
- package/dist/presets/providers/command.d.ts +0 -3
- package/dist/presets/providers/command.js +0 -39
- package/dist/presets/providers/daytona.d.ts +0 -3
- package/dist/presets/providers/daytona.js +0 -173
- package/dist/presets/providers/docker.d.ts +0 -3
- package/dist/presets/providers/docker.js +0 -31
- package/dist/presets/providers/e2b.d.ts +0 -3
- package/dist/presets/providers/e2b.js +0 -124
- package/dist/presets/providers/exe.d.ts +0 -3
- package/dist/presets/providers/exe.js +0 -28
- package/dist/presets/providers/http.example.d.ts +0 -3
- package/dist/presets/providers/http.example.js +0 -47
- package/dist/presets/providers/local.d.ts +0 -3
- package/dist/presets/providers/local.js +0 -52
- package/dist/presets/providers/modal.d.ts +0 -3
- package/dist/presets/providers/modal.js +0 -117
- package/dist/presets/providers/ssh.d.ts +0 -3
- package/dist/presets/providers/ssh.js +0 -49
- package/dist/presets/providers/util.d.ts +0 -13
- package/dist/presets/providers/util.js +0 -102
- package/presets/providers/command.ts +0 -43
- package/presets/providers/daytona.ts +0 -181
- package/presets/providers/docker.ts +0 -38
- package/presets/providers/e2b.ts +0 -130
- package/presets/providers/exe.ts +0 -35
- package/presets/providers/http.example.ts +0 -64
- package/presets/providers/local.ts +0 -56
- package/presets/providers/modal.ts +0 -121
- package/presets/providers/ssh.ts +0 -54
- package/presets/providers/util.ts +0 -112
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
Add a section on top with a new version to release it: pushing it to main publishes that version to npm.
|
|
4
4
|
Prerelease versions like `0.1.0-prototype.1` go to the dist-tag named after them (`prototype`), plain versions to `latest`.
|
|
5
5
|
|
|
6
|
+
## 0.1.0-prototype.2
|
|
7
|
+
|
|
8
|
+
- `bottleship review <pr>` brings a PR up in a box, with its base and head running side by side. An agent there builds a guided tour that opens in your browser: an overview, chapters that show the change in the running app, and the diff in tour order. Your comments go into your pending review on GitHub, and you submit it from the tour with a summary and a verdict. The Pull request page shows the checks and merges when you are allowed to.
|
|
9
|
+
- `bottleship review` with no PR does the same for your checkout's local changes, and leaves the checkout untouched.
|
|
10
|
+
- The tour has a chat with an agent. It knows the page you are on and takes selections, comments and screenshots as context. In a PR review the box agent answers, and commits fixes to the PR's branch when you ask. In a review of local changes your own agent answers, changes your checkout, and the tour updates.
|
|
11
|
+
- `bottleship init` also sets up the repo you run it in: your local agent works out how the repo runs in a box and writes `.bottleship/`, checking its work in real boxes. On a machine that is already set up, it goes straight to the repo.
|
|
12
|
+
- `bottleship box list` shows the boxes on this machine with their reviews, and `bottleship box rm` removes one.
|
|
13
|
+
- Breaking: the commands for coding agents moved under `bottleship agent`: `new`, `status`, `ask`, `exec` and `rm`, plus the new `list`, `review`, `review next` and `onboard`. `bottleship agent help` is their reference. To update your agent, replace the "## Remote agents with bottleship" section in its instructions file with the one in the README.
|
|
14
|
+
- Providers can have a `list` script, which `box list` uses. The builtin `docker` provider has one.
|
|
15
|
+
|
|
6
16
|
## 0.1.0-prototype.1
|
|
7
17
|
|
|
8
18
|
- The last step of `bottleship init` opens your local agent (Claude Code, Codex, opencode or Cursor) to add bottleship to its global instructions, instead of showing text to copy.
|
package/README.md
CHANGED
|
@@ -1,138 +1,92 @@
|
|
|
1
1
|
# bottleship (prototype)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Review a pull request by watching it run. `bottleship review 17` starts the PR in a box, an isolated dev environment with base and head running side by side. An agent there builds a guided tour that opens in your browser: what the PR does, shown in the running app, then the diff in tour order. You comment, chat with the agent, and submit your review to GitHub from the tour.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The same boxes let your coding agent hand work to other agents, like subagents that each have their own copy of the app.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Quick start
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
```sh
|
|
12
|
-
npm install -g <path or git URL of this package> # will be `npm i -g bottleship` once published
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
Needs Node >= 22.18 on this machine. Installing from a git URL or a checkout builds the TypeScript sources (`npm install` runs `npm run build`).
|
|
16
|
-
|
|
17
|
-
## Onboarding
|
|
18
|
-
|
|
19
|
-
Once per machine, in a terminal:
|
|
9
|
+
You need Node 22.18 or newer, Docker, and the GitHub CLI logged in (`gh auth login`).
|
|
20
10
|
|
|
21
11
|
```sh
|
|
12
|
+
npm install -g bottleship@prototype
|
|
13
|
+
cd your-repo
|
|
22
14
|
bottleship init
|
|
15
|
+
bottleship review 17
|
|
23
16
|
```
|
|
24
17
|
|
|
25
|
-
|
|
18
|
+
`bottleship init` asks a few questions, checks that a box can run an agent, then has your local coding agent set up the repo with you. The first box builds the base image, which takes a few minutes.
|
|
26
19
|
|
|
27
|
-
|
|
20
|
+
## Commands
|
|
28
21
|
|
|
29
22
|
```sh
|
|
30
|
-
bottleship init --
|
|
23
|
+
bottleship init [--reset] # set up this machine, then the repo you are in
|
|
24
|
+
bottleship review [<pr>] # review a PR, or this checkout's changes without a PR
|
|
25
|
+
bottleship review --base <ref> # review the changes against another base
|
|
26
|
+
bottleship box list # the boxes on this machine, with their reviews
|
|
27
|
+
bottleship box rm [<id>] # remove a box; without an id, pick one
|
|
28
|
+
bottleship help
|
|
31
29
|
```
|
|
32
30
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
For an agent on the device login route (codex by default), the live check stops at the login with `login_needs_human`, since someone has to approve it in a browser. Ask the user to run `bottleship init` in their terminal.
|
|
36
|
-
|
|
37
|
-
At the end, init opens the user's own agent on this machine (Claude Code, Codex, opencode or Cursor, whichever is installed) with a prompt to add the instructions below to its global instructions (for Cursor, a user rule file `~/.cursor/rules/bottleship.mdc`; Cursor documents that folder, not whether its CLI reads it), so the user only approves the change. With `--yes`, the JSON's `next` asks the calling agent to add `instructions` to its own global instructions file and show the user the change.
|
|
38
|
-
|
|
39
|
-
Rerunning `bottleship init` only asks what is missing. `bottleship init --reset` starts over: it sets `~/.bottleship/config.json` aside as `config.json.bak` and asks everything again; on a terminal it also offers to forget the saved logins. With `--yes` it never deletes saved logins.
|
|
40
|
-
|
|
41
|
-
If a secret is missing it fails with `secret_missing`. Then ask the user to run `bottleship init` in their own terminal, where it asks with a hidden prompt. Never ask the user to paste a secret to you.
|
|
42
|
-
|
|
43
|
-
### Secrets
|
|
44
|
-
|
|
45
|
-
Box env vars are configured under `env` in `~/.bottleship/config.json` (and per agent). A value is a plain string or a secret source:
|
|
46
|
-
|
|
47
|
-
| Source | Meaning |
|
|
48
|
-
|---|---|
|
|
49
|
-
| `{ "store": "claude" }` | bottleship's store: macOS Keychain, or a 0600 file under `~/.bottleship/secrets` |
|
|
50
|
-
| `{ "command": "gh auth token" }` | the output of a command on this machine |
|
|
51
|
-
| `{ "env": "CORP_KEY" }` | an environment variable of the bottleship process |
|
|
31
|
+
A PR is a number in the current repo, `owner/name#17`, or a link to the PR.
|
|
52
32
|
|
|
53
|
-
|
|
33
|
+
Your coding agent has its own commands under `bottleship agent`. See [For your coding agent](#for-your-coding-agent).
|
|
54
34
|
|
|
55
|
-
|
|
35
|
+
## Reviewing a PR
|
|
56
36
|
|
|
57
|
-
|
|
37
|
+
`bottleship review 17` reads the PR from GitHub, starts a box, opens the review in your browser, and shows progress in the terminal until the tour is ready. Ctrl-C stops watching, and the review keeps going.
|
|
58
38
|
|
|
59
|
-
|
|
60
|
-
- `device`: the agent logs in inside each box. `new` (and `ask`, if the box's login expired) starts the login and returns its URL and code for the user to approve in a browser; the task starts once they do. Nothing is stored on this machine.
|
|
61
|
-
- `none`: no login.
|
|
39
|
+
The tour has four parts, in the rail on the left:
|
|
62
40
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
| `cursor` (no reply tested yet) | token: `CURSOR_API_KEY` from https://cursor.com/dashboard/api (bills to your Cursor plan); device: `cursor-agent login` per box | API keys are Cursor's documented headless login ([docs](https://cursor.com/docs/cli/reference/authentication)). |
|
|
68
|
-
| `opencode` | none: free models; device: a ChatGPT plan via `opencode auth login openai --method chatgpt-headless` per box; token: `OPENCODE_API_KEY` (OpenCode Zen), `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` | Per-box logins and provider API keys ([docs](https://opencode.ai/docs/)). Never a Claude subscription: Anthropic allows those only in Claude Code. |
|
|
69
|
-
|
|
70
|
-
On `new`, the `.env*` files at the root of the current repo (not `*.example`, `*.sample`, `*.template`) are copied into the box workspace at the same paths.
|
|
41
|
+
- **Overview.** What the PR does in one sentence, before and after, where it happens, and what deserves a close look. Every claim in the description, comments and reviews is checked in the box.
|
|
42
|
+
- **Chapters.** The change in the running app. Each chapter says what should happen on base and on head, runs a step, and shows the evidence: live instances, screenshots side by side, and flows traced through the code.
|
|
43
|
+
- **Changes.** The diff grouped by chapter, each group with why it exists, then the files no chapter covers.
|
|
44
|
+
- **Finish review** and **Pull request.** Your comments, a summary and a verdict, then the PR's checks and a merge button when you are allowed to merge.
|
|
71
45
|
|
|
72
|
-
|
|
46
|
+
Comments work as on GitHub, and land there as you write them. Press the + next to a line, or drag over several lines, to comment or suggest a change. Add single comment posts at once. Start a review puts the comment in your pending review, which you submit from Finish review.
|
|
73
47
|
|
|
74
|
-
|
|
48
|
+
The Agent button in the footer opens a chat with the agent in the box. It knows the page you are on, and takes what you select, any comment, and any screenshot as context. It answers questions, and commits fixes to the PR's branch when you ask. It leaves comments on GitHub only when you ask, and only in your pending review.
|
|
75
49
|
|
|
76
|
-
|
|
50
|
+
Only review PRs you trust. The box runs the PR's code with your GitHub token, so code in the PR could push, comment or approve as you, in every repo the token reaches.
|
|
77
51
|
|
|
78
|
-
|
|
79
|
-
- `exe`: [exe.dev](https://exe.dev) VMs over ssh; needs `ssh exe.dev` working. No box template: exe.dev boots VMs only from registry images, so boxes install the agent at start.
|
|
80
|
-
- `e2b` (untested): [e2b.dev](https://e2b.dev) cloud sandboxes; API key from https://e2b.dev/dashboard.
|
|
81
|
-
- `daytona` (untested): [daytona.io](https://daytona.io) cloud sandboxes; API key from https://app.daytona.io/dashboard/keys.
|
|
82
|
-
- `modal` (untested): [modal.com](https://modal.com) cloud sandboxes; token id and secret from `modal token new` or https://modal.com/settings/tokens.
|
|
83
|
-
- `ssh` (untested on a real host): any machine you can `ssh` into without a password prompt; each box is a directory `~/bottleship-boxes/<id>` there, used as its `$HOME`. No isolation: agents run as that user with permission prompts off. Options: `host` (required, anything ssh accepts, e.g. a `~/.ssh/config` alias; `init --host`), `root`.
|
|
84
|
-
- `local`: git worktrees on this machine. The box workspace is a `git worktree` of your local repo on branch `bottleship/<id>` (from your `HEAD` commit, without uncommitted changes); `rm` removes the worktree and keeps the branch. No isolation: agents run as you, with your files and logins, with permission prompts off. Boxes stop when the laptop sleeps.
|
|
85
|
-
- `custom`: your company's sandboxes, see below.
|
|
52
|
+
## Reviewing local changes
|
|
86
53
|
|
|
87
|
-
`
|
|
54
|
+
Run `bottleship review` without a PR to review your checkout's changes: commits, uncommitted and untracked files, against where your branch left the default branch. The box gets a copy, and your checkout stays as it is. `.env` files never go into the diff.
|
|
88
55
|
|
|
89
|
-
|
|
56
|
+
Your coding agent starts these reviews too, after a big change it made. Then the chat in the tour goes to that agent: it changes your checkout, the box picks up the new state, and the tour updates. When you press Looks good, your agent does what your note says.
|
|
90
57
|
|
|
91
|
-
|
|
58
|
+
## Boxes
|
|
92
59
|
|
|
93
|
-
|
|
60
|
+
A box is a Docker container by default. Each one has the repo, its dependencies and services, and the app running. `bottleship box list` shows every box with its age and, for reviews, the review link. Boxes stay until you remove them with `bottleship box rm`.
|
|
94
61
|
|
|
95
|
-
|
|
62
|
+
## Setting up a repo
|
|
96
63
|
|
|
97
|
-
|
|
98
|
-
2. Put the module's options in its config entry, with secrets as secret sources, never as plain values:
|
|
64
|
+
A repo describes its box in `.bottleship/`, committed with the repo. `bottleship init` in the repo has your coding agent write it with you and check it in real boxes.
|
|
99
65
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
66
|
+
| File | What it does |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `.bottleship/Dockerfile` | Optional. Lines added to bottleship's base image (Debian bookworm, Node 22, git, gh, curl, ripgrep, a user `box` without sudo), run as root. No `FROM`, `COPY` or `ADD`. `EXPOSE <port>` marks the ports to open; bottleship publishes them on 127.0.0.1 and waits for them. |
|
|
69
|
+
| `.bottleship/setup.sh` | Runs once as `box` in `~/workspace`: dependencies, databases, seed data, build. Must not prompt. |
|
|
70
|
+
| `.bottleship/start.sh` | Starts the services, then the app in the foreground. It must listen on 0.0.0.0. The log is in `~/.bottleship/app.log`. |
|
|
105
71
|
|
|
106
|
-
|
|
72
|
+
Boxes also get the `.env` files at the root of your checkout, but not `.env.example` and the like. The image is built once per Dockerfile and reused after that.
|
|
107
73
|
|
|
108
|
-
|
|
74
|
+
## Configuration
|
|
109
75
|
|
|
110
|
-
|
|
76
|
+
`bottleship init` writes `~/.bottleship/config.json`. Run it again to fill in anything missing, or with `--reset` to start over. The old config is kept as `config.json.bak`.
|
|
111
77
|
|
|
112
|
-
|
|
78
|
+
### Agents
|
|
113
79
|
|
|
114
|
-
|
|
115
|
-
bottleship new "<task>" [--repo owner/name | --no-repo] [--provider P] [--agent A]
|
|
116
|
-
# { "id": "docker:bs-fix-login-bug-3fa2", "provider": "docker", "agent": "claude", "repo": "acme/web", "next": "..." }
|
|
117
|
-
# device login route: { "id", "provider", "agent", "repo", "login": { "url", "code"?, "agent" }, "next": "show the user this URL and code; ..." }
|
|
118
|
-
|
|
119
|
-
bottleship status <id> [--wait 600] [--lines 40]
|
|
120
|
-
# { "id", "box": "running", "agent": "running" | "exited" | "stopped", "exitCode"?, "log": "<last lines>", "next" }
|
|
121
|
-
# { "id", "box": "running", "agent": "waiting_for_login", "login": { "url", "code"?, "agent" }, "log", "next" }
|
|
122
|
-
# { "id", "box": "running", "agent": "login_failed", "exitCode", "log": "<the login's output>", "next" }
|
|
123
|
-
# { "id", "box": "gone", "next" }
|
|
124
|
-
|
|
125
|
-
bottleship ask <id> "<follow-up>" # continues the same conversation; refused while the agent runs
|
|
126
|
-
# { "id", "agent": "running", "next" }, or with an expired device login { "id", "agent": "waiting_for_login", "login", "next" }
|
|
127
|
-
bottleship exec <id> -- git log --oneline -5
|
|
128
|
-
bottleship exec <id> -- "npm test 2>&1 | tail -20" # one argument is a script, several are argv
|
|
129
|
-
bottleship exec -i <id> -- <cmd> # streams stdin/stdout, e.g. for an IDE attaching over ACP
|
|
130
|
-
bottleship rm <id>
|
|
131
|
-
```
|
|
80
|
+
The agent in a box is `claude`, `codex`, `cursor` or `opencode`. Each gets one long-lived login, which init asks for:
|
|
132
81
|
|
|
133
|
-
|
|
82
|
+
| Agent | Env var | Where it comes from |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| `claude` | `CLAUDE_CODE_OAUTH_TOKEN` | `claude setup-token`, to use a Claude subscription headless ([docs](https://code.claude.com/docs/en/authentication)). Boxes never get `ANTHROPIC_API_KEY` unless you configure it, since it would win over the token. |
|
|
85
|
+
| `codex` | `CODEX_API_KEY` | an OpenAI API key, billed to the API, not a ChatGPT plan |
|
|
86
|
+
| `cursor` | `CURSOR_API_KEY` | an API key from https://cursor.com/dashboard/api, billed to your Cursor plan |
|
|
87
|
+
| `opencode` | `OPENCODE_API_KEY` | an OpenCode Zen key from https://opencode.ai/console |
|
|
134
88
|
|
|
135
|
-
|
|
89
|
+
An agent is a set of shell templates. The presets are in [presets/agents](presets/agents), and an entry under `agents` in the config overrides only the fields it names:
|
|
136
90
|
|
|
137
91
|
```json
|
|
138
92
|
"agents": {
|
|
@@ -142,47 +96,94 @@ Agents are configured as shell templates (`{prompt}` is filled in shell-quoted):
|
|
|
142
96
|
"resume": "claude -p {prompt} --continue --dangerously-skip-permissions",
|
|
143
97
|
"env": { "IS_SANDBOX": "1" },
|
|
144
98
|
"unset": ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"],
|
|
145
|
-
"login": { "
|
|
99
|
+
"login": { "env": "CLAUDE_CODE_OAUTH_TOKEN", "store": "claude", "command": "claude setup-token" }
|
|
146
100
|
}
|
|
147
101
|
}
|
|
148
102
|
```
|
|
149
103
|
|
|
150
|
-
|
|
104
|
+
### Secrets and tokens
|
|
151
105
|
|
|
152
|
-
|
|
106
|
+
Boxes get `GH_TOKEN`, which git and gh both use, and the agent's login. Other env vars go under `env` in the config, as a plain string or one of these sources:
|
|
153
107
|
|
|
154
|
-
|
|
108
|
+
| Source | Meaning |
|
|
109
|
+
|---|---|
|
|
110
|
+
| `{ "store": "claude" }` | bottleship's store: the macOS Keychain, or a 0600 file under `~/.bottleship/secrets` |
|
|
111
|
+
| `{ "command": "gh auth token" }` | the output of a command on this machine |
|
|
112
|
+
| `{ "env": "CORP_KEY" }` | an environment variable of the bottleship process |
|
|
155
113
|
|
|
156
|
-
|
|
114
|
+
Init asks for a [fine-grained personal access token](https://github.com/settings/personal-access-tokens/new) limited to the repos boxes work on, with Contents and Pull requests set to read and write. Your full gh login (`gh auth token`) is used only if you pick it and confirm a warning. Either way, protect `main` so a box reaches it only through a reviewed PR.
|
|
157
115
|
|
|
158
|
-
The
|
|
116
|
+
The agent in a box can do anything its tokens allow, so keep them narrow. A box that only runs the repo's scripts, such as a setup check, gets no secrets. A proxy that keeps tokens out of boxes is not built yet.
|
|
159
117
|
|
|
160
|
-
|
|
118
|
+
## Providers
|
|
161
119
|
|
|
162
|
-
|
|
120
|
+
A provider decides where boxes run. It is a directory of bash scripts. `docker` ships in [presets/providers/docker](presets/providers/docker). For anything else, such as a company's sandboxes, a cloud sandbox service or a VM over ssh, put the same scripts in `~/.bottleship/providers/<name>/` and pick it in `bottleship init`. A provider there with a builtin's name replaces the builtin.
|
|
163
121
|
|
|
164
|
-
|
|
122
|
+
| Script | Contract |
|
|
123
|
+
|---|---|
|
|
124
|
+
| `create <id> <dockerfile> <image>` | Build the box from the Dockerfile, cached as `<image>`, which changes whenever the Dockerfile does. Start it with every `EXPOSE`d port reachable from this machine, and exit 0 once it runs commands. The box stays up until `destroy`. |
|
|
125
|
+
| `exec <id> <script>` | Run `bash -c <script>` in the box as its non-root user, with `$HOME` its home. Pass stdin, stdout, stderr and the exit code through, binary-clean and streaming. |
|
|
126
|
+
| `status <id>` | Print `running` or `gone`. |
|
|
127
|
+
| `destroy <id>` | Remove the box and everything it runs. |
|
|
128
|
+
| `url <id> <port>` | Optional. Print a URL on this machine for a port the box exposes. |
|
|
129
|
+
| `list` | Optional. Print the boxes bottleship made, one per line: the id, then optionally a space and the creation time in ISO 8601. Without it, `box list` shows none of this provider's boxes. |
|
|
130
|
+
|
|
131
|
+
The scripts run on this machine with bottleship's environment. They read the credentials they need themselves, from an env var, the keychain or a CLI's own login, and never hardcode them.
|
|
132
|
+
|
|
133
|
+
## For your coding agent
|
|
134
|
+
|
|
135
|
+
`bottleship init` adds this section to your agent's global instructions, such as `~/.claude/CLAUDE.md` or `~/.codex/AGENTS.md`. If you skipped that step, add it yourself:
|
|
165
136
|
|
|
166
137
|
```md
|
|
167
138
|
## Remote agents with bottleship
|
|
168
139
|
|
|
169
|
-
|
|
140
|
+
The `bottleship` CLI runs coding agents in boxes, isolated dev environments for a repo (Docker containers by default), and builds guided reviews of PRs and of local changes, which the user opens in their browser. Your commands are under `bottleship agent`; run `bottleship agent help` before you first use them in a session, for what each does and how to use it.
|
|
170
141
|
|
|
171
|
-
-
|
|
172
|
-
-
|
|
173
|
-
-
|
|
174
|
-
-
|
|
175
|
-
- `bottleship ask <id> "<follow-up>"` continues the same conversation once the agent has finished.
|
|
176
|
-
- `bottleship exec <id> -- <cmd>` inspects the box (git log, test output, files).
|
|
177
|
-
- `bottleship rm <id>` when the work is merged or abandoned. If a box is gone, start a new one on the pushed branch.
|
|
178
|
-
- If the user wants to watch or steer a box from their IDE, find out how their IDE attaches to an agent over a command (ACP) and give it `bottleship exec -i <id> -- <the agent's ACP command>`.
|
|
142
|
+
- Use a box for work that can run on its own while you do something else, and treat it as a subagent you manage.
|
|
143
|
+
- When the user wants to review a PR, start the review for them and give them its url.
|
|
144
|
+
- Reviews of your own work: after a big change you made on your own (many files, or behaviour that is hard to see in the diff), start a local review before you hand over and give the user its url, unless the user has said otherwise. Skip it for small changes.
|
|
145
|
+
- If the user stops or interrupts you while you start a review, or turns one down, ask them once when they want reviews (after every big change, only when they ask, never, or something else). Then rewrite the line above in this section of your instructions file to say exactly what they chose, and follow it from then on.
|
|
179
146
|
- Secrets: never ask the user to paste a secret to you. If bottleship reports one missing, ask them to run `bottleship init` in their terminal.
|
|
180
147
|
```
|
|
181
148
|
|
|
149
|
+
`bottleship agent help` is the full reference for agents. Each command prints JSON, errors are `{ "error", "message", "fix" }` with exit code 1, and most results say in `next` what to do next. In short:
|
|
150
|
+
|
|
151
|
+
- `agent new`, `status`, `ask`, `exec`, `list` and `rm` run another agent in a box and manage it.
|
|
152
|
+
- `agent review` starts a review and returns its link. For local changes, `agent review next` waits for what you send from the tour's chat and sends back the agent's updates.
|
|
153
|
+
- `agent onboard` gives the instructions for setting up a repo, and `agent onboard --check` tries them in a box.
|
|
154
|
+
- `agent init --yes` sets up the machine without prompts.
|
|
155
|
+
|
|
156
|
+
## How a review works
|
|
157
|
+
|
|
158
|
+
These details are for working on bottleship or on a tour.
|
|
159
|
+
|
|
160
|
+
In a review box, `~/workspace` is the PR's head and `~/base` is the commit it branched off. The box environment is the PR's `.bottleship/` if it has one, otherwise your local checkout's, otherwise none and the agent works it out.
|
|
161
|
+
|
|
162
|
+
The review server listens on port 7777 from the start, so the link works at once and shows that the review is being set up. Ports 7781 to 7784 are free for extra instances, such as base. The server keeps the review's state in the box: `~/review/comments.json`, `verdict.json` and `updates.json`.
|
|
163
|
+
|
|
164
|
+
`~/review/app` is a copy of [review-app](review-app), a Vite and React app with the PR's data in `src/data`. The agent writes `src/review.tsx`, step scripts in `~/review/steps/<id>.sh`, and screenshots in `~/review/shots`. [review-app/KIT.md](review-app/KIT.md) tells it how to build a tour from the kit, and [review-app/WRITING.md](review-app/WRITING.md) how to write one. `examples/rply-16.tsx` is a complete tour.
|
|
165
|
+
|
|
166
|
+
Steps and review hooks, which are temporary code in the box's copy of the app, report what happens by posting events to `$BOTTLESHIP_REVIEW_URL/api/events`:
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
{ "instance": "base", "kind": "hook", "at": "push/index.ts:47", "text": "setPresence key=…/ada-7f3c",
|
|
170
|
+
"flow": { "id": "presence", "edge": ["sync", "pres"], "node": "pres", "status": "fail", "label": "…/ada-7f3c → deploys" },
|
|
171
|
+
"overlay": { "title": "No push for Ada", "status": "fail" } }
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`kind` is `request`, `hook`, `ok` or `fail`. `flow` moves a dot along an edge and sets a node's state and label. `overlay` shows a note over that instance. The server tags each event with the step that was running, and answers 400 with the problems when an event is malformed.
|
|
175
|
+
|
|
176
|
+
For GitHub Enterprise, log gh in to that host (`gh auth login --hostname <host>`) and make `GH_TOKEN` a token for it.
|
|
177
|
+
|
|
182
178
|
## Development
|
|
183
179
|
|
|
184
180
|
```sh
|
|
185
181
|
npm test # build, then all tests
|
|
186
|
-
npx vitest run test/cli.test.ts # one file
|
|
187
|
-
grep -rn 'ponytail:' src
|
|
182
|
+
npx vitest run test/cli.test.ts # one file, after a build
|
|
183
|
+
grep -rn 'ponytail:' src # deliberate shortcuts and their upgrade paths
|
|
184
|
+
cd review-app && npm run playground # the review kit against recorded reviews, gallery at #/gallery
|
|
188
185
|
```
|
|
186
|
+
|
|
187
|
+
Tests run boxes with the `dir` provider in [test/fixtures/providers/dir](test/fixtures/providers/dir), which uses directories on this machine, so no Docker is needed.
|
|
188
|
+
|
|
189
|
+
To release, add a section with the new version on top of [CHANGELOG.md](CHANGELOG.md) and set the same version in `package.json`. Merging it to `main` publishes it to npm.
|
package/dist/src/boxes.d.ts
CHANGED
|
@@ -1,64 +1,66 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
url: string;
|
|
5
|
-
label: string;
|
|
6
|
-
}
|
|
1
|
+
import { type Provider } from "./provider.ts";
|
|
2
|
+
import type { Repo, RepoEnv } from "./repo.ts";
|
|
3
|
+
import type { Config } from "./types.ts";
|
|
7
4
|
export interface Box {
|
|
8
5
|
id: string;
|
|
9
|
-
provider:
|
|
6
|
+
provider: Provider;
|
|
10
7
|
box: string;
|
|
11
|
-
warning?: string;
|
|
12
|
-
login?: Login;
|
|
13
|
-
}
|
|
14
|
-
export interface Login {
|
|
15
|
-
url: string;
|
|
16
|
-
code?: string;
|
|
17
|
-
agent: string;
|
|
18
8
|
}
|
|
19
9
|
export type AgentState = {
|
|
20
10
|
agent: "running";
|
|
11
|
+
env?: string;
|
|
21
12
|
log: string;
|
|
22
13
|
} | {
|
|
23
14
|
agent: "exited";
|
|
24
15
|
exitCode: number;
|
|
16
|
+
env?: string;
|
|
25
17
|
log: string;
|
|
26
18
|
} | {
|
|
27
19
|
agent: "stopped";
|
|
20
|
+
env?: string;
|
|
28
21
|
log: string;
|
|
29
|
-
} | {
|
|
30
|
-
agent: "waiting_for_login";
|
|
31
|
-
login?: Login;
|
|
32
|
-
log: string;
|
|
33
|
-
} | {
|
|
34
|
-
agent: "login_failed";
|
|
35
|
-
exitCode: number;
|
|
36
|
-
log: string;
|
|
37
22
|
};
|
|
38
|
-
export declare const goneFix = "the box is gone. Start a new one with `bottleship new \"<task>\"` and tell its agent to continue on the branch the old box pushed";
|
|
39
|
-
export declare function
|
|
40
|
-
|
|
23
|
+
export declare const goneFix = "the box is gone. Start a new one with `bottleship agent new \"<task>\"` and tell its agent to continue on the branch the old box pushed";
|
|
24
|
+
export declare function boxImage(cfg: Config, agentName: string, repoEnv?: RepoEnv, extra?: string): {
|
|
25
|
+
dockerfile: string;
|
|
26
|
+
image: string;
|
|
27
|
+
};
|
|
41
28
|
export declare function parseId(cfg: Config, handle: string): {
|
|
42
29
|
provider: string;
|
|
43
30
|
box: string;
|
|
44
31
|
};
|
|
45
|
-
export declare function newBoxId(
|
|
32
|
+
export declare function newBoxId(label: string): string;
|
|
46
33
|
export declare function openBox(cfg: Config, handle: string): Promise<Box>;
|
|
47
|
-
export interface
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
34
|
+
export interface BoxOptions {
|
|
35
|
+
provider: string;
|
|
36
|
+
label: string;
|
|
37
|
+
agent?: string;
|
|
38
|
+
workspace?: {
|
|
39
|
+
clone: Repo;
|
|
40
|
+
} | {
|
|
41
|
+
tar: Buffer;
|
|
42
|
+
};
|
|
43
|
+
repoEnv?: RepoEnv;
|
|
51
44
|
dotenv?: Record<string, string>;
|
|
45
|
+
pr?: {
|
|
46
|
+
number: number;
|
|
47
|
+
base: string;
|
|
48
|
+
};
|
|
49
|
+
env?: Record<string, string>;
|
|
50
|
+
dockerfile?: string;
|
|
52
51
|
}
|
|
53
|
-
export declare function
|
|
54
|
-
export declare function
|
|
55
|
-
export declare function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
52
|
+
export declare function createBox(cfg: Config, opts: BoxOptions): Promise<Box>;
|
|
53
|
+
export declare function putTar(b: Box, dir: string, tar: Buffer): Promise<void>;
|
|
54
|
+
export declare function putFiles(b: Box, files: Record<string, string>): Promise<void>;
|
|
55
|
+
export declare const runIn: (b: Box, script: string) => Promise<import("./types.ts").ExecResult>;
|
|
56
|
+
export type Stage = "setup" | "start" | "ready" | "failed";
|
|
57
|
+
export declare function prepare(b: Box, seconds?: number): Promise<{
|
|
58
|
+
stage: Stage;
|
|
59
|
+
message?: string;
|
|
60
|
+
log?: string;
|
|
61
|
+
}>;
|
|
62
|
+
export declare function startAgent(b: Box, prompt: string): Promise<void>;
|
|
60
63
|
export declare function agentState(b: Box, lines?: number): Promise<AgentState>;
|
|
61
|
-
export declare function loginState(b: Box, setup?: string, seconds?: number): Promise<AgentState>;
|
|
62
64
|
export declare function waitAgent(b: Box, seconds: number, lines?: number): Promise<AgentState>;
|
|
63
|
-
export declare function ask(b: Box, prompt: string): Promise<
|
|
65
|
+
export declare function ask(b: Box, prompt: string): Promise<void>;
|
|
64
66
|
export declare const inBox: (script: string) => string;
|