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
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// `bottleship review <pr>`: a box with the PR's head and base, the review server running, and an agent building a guided tour of the PR.
|
|
2
|
+
import { ROOT } from "../paths.js";
|
|
3
|
+
import { BsError } from "../config.js";
|
|
4
|
+
import { createBox, putFiles, putTar, runIn, startAgent } from "../boxes.js";
|
|
5
|
+
import { dotenvFiles, parseRepoEnv, readEnvFiles, tarFiles } from "../repo.js";
|
|
6
|
+
import { fetchPr, prEnvFiles } from "./pr.js";
|
|
7
|
+
import { reviewPrompt } from "./prompt.js";
|
|
8
|
+
import { localChanges } from "./local.js";
|
|
9
|
+
import { shq } from "../util.js";
|
|
10
|
+
import { templateFiles, templatePackage } from "./template.js";
|
|
11
|
+
// the review server's port in the box, and ports for instances besides the app's own, e.g. base; all published
|
|
12
|
+
export const REVIEW_PORT = Number(process.env.BOTTLESHIP_REVIEW_PORT ?? 7777);
|
|
13
|
+
export const INSTANCE_PORTS = [7781, 7782, 7783, 7784];
|
|
14
|
+
// containers publish what listens on all interfaces; the provider decides who can reach it (docker: this machine only)
|
|
15
|
+
const REVIEW_HOST = process.env.BOTTLESHIP_REVIEW_HOST ?? "0.0.0.0";
|
|
16
|
+
// a headless browser the agent drives to see and check the UI; require("playwright") resolves through NODE_PATH
|
|
17
|
+
const PLAYWRIGHT = "1.64.1";
|
|
18
|
+
const BROWSER = `USER root
|
|
19
|
+
ENV PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright NODE_PATH=/opt/playwright/lib/node_modules
|
|
20
|
+
RUN npm install -g --prefix /opt/playwright playwright@${PLAYWRIGHT} && /opt/playwright/bin/playwright install --with-deps chromium && rm -rf /var/lib/apt/lists/* /root/.npm
|
|
21
|
+
USER box
|
|
22
|
+
`;
|
|
23
|
+
// the review app's dependencies, installed once into the image; a review links them into its copy of the template
|
|
24
|
+
const REVIEW_APP = () => `USER root
|
|
25
|
+
RUN mkdir -p /opt/review-app && echo ${Buffer.from(templatePackage()).toString("base64")} | base64 -d > /opt/review-app/package.json \\
|
|
26
|
+
&& cd /opt/review-app && npm install --no-audit --no-fund && rm -rf /root/.npm && chown -R box:box /opt/review-app
|
|
27
|
+
USER box
|
|
28
|
+
`;
|
|
29
|
+
// what the box needs to run the review server, and the package.json that marks it as ES modules
|
|
30
|
+
const SERVER_FILES = ["package.json", "dist/src/review/server.js", "dist/src/review/contract.js", "dist/src/review/serve.js", "dist/src/review/github.js", "dist/src/review/comments.js", "dist/src/review/setting-up.js", "dist/src/util.js"];
|
|
31
|
+
// onPhase: what is happening now, for a person watching in a terminal
|
|
32
|
+
// local: the user's checkout of the same repo, for its .env files and its .bottleship/ when the PR has none
|
|
33
|
+
export async function startReview(cfg, opts) {
|
|
34
|
+
const phase = opts.onPhase ?? (() => { });
|
|
35
|
+
const src = opts.source;
|
|
36
|
+
const localEnv = opts.local ? readEnvFiles(opts.local) : undefined;
|
|
37
|
+
const dotenv = opts.local ? dotenvFiles(opts.local) : {};
|
|
38
|
+
let subject;
|
|
39
|
+
let workspace, pr, prEnv, baseTree, baseSha;
|
|
40
|
+
if ("pr" in src) {
|
|
41
|
+
phase(`Reading ${src.pr.repo.label}#${src.pr.number} from GitHub`);
|
|
42
|
+
const p = await fetchPr(src.pr.repo, src.pr.number);
|
|
43
|
+
prEnv = await prEnvFiles(src.pr.repo, p.headRefOid);
|
|
44
|
+
subject = { raw: p.raw, title: p.title, url: p.url, diff: p.diff, reviewComments: p.reviewComments, base: p.baseRefName, label: String(src.pr.number), prompt: { repo: src.pr.repo.label, number: p.number, title: p.title, base: p.baseRefName } };
|
|
45
|
+
workspace = { clone: { url: src.pr.repo.clone, label: src.pr.repo.label } };
|
|
46
|
+
pr = { number: src.pr.number, base: p.baseRefName };
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
phase("Reading the local changes");
|
|
50
|
+
const c = await localChanges(src.changes.root, { ...src.changes, exclude: Object.keys(dotenv) });
|
|
51
|
+
const base = c.raw.baseRefName;
|
|
52
|
+
subject = { raw: c.raw, title: c.branch, url: c.branch, diff: c.diff, reviewComments: [], base, label: "local", prompt: { repo: c.raw.repo, title: c.branch, base, changes: { branch: c.branch, note: src.changes.note } } };
|
|
53
|
+
workspace = { tar: c.head };
|
|
54
|
+
baseTree = c.baseTree;
|
|
55
|
+
baseSha = c.base;
|
|
56
|
+
}
|
|
57
|
+
const env = prEnv ? "pr" : localEnv ? "local" : "none";
|
|
58
|
+
const repoEnv = parseRepoEnv((prEnv ?? localEnv)?.Dockerfile);
|
|
59
|
+
const reviewUrl = `http://127.0.0.1:${REVIEW_PORT}`;
|
|
60
|
+
phase(`Creating the box for "${subject.title}" (the first box builds the image, which takes minutes)`);
|
|
61
|
+
const box = await createBox(cfg, {
|
|
62
|
+
provider: opts.provider,
|
|
63
|
+
label: `review-${subject.label}`,
|
|
64
|
+
agent: opts.agent,
|
|
65
|
+
workspace,
|
|
66
|
+
pr,
|
|
67
|
+
repoEnv,
|
|
68
|
+
dotenv,
|
|
69
|
+
env: { BOTTLESHIP_REVIEW_URL: reviewUrl },
|
|
70
|
+
dockerfile: `${BROWSER}${REVIEW_APP()}EXPOSE ${REVIEW_PORT} ${INSTANCE_PORTS.join(" ")}\n`,
|
|
71
|
+
});
|
|
72
|
+
try {
|
|
73
|
+
phase("Copying the changes and the review app into the box");
|
|
74
|
+
if (baseTree) {
|
|
75
|
+
// local changes: base from git, head from the working tree, each its own repo so the agent can diff its hooks;
|
|
76
|
+
// .env files stay out of both
|
|
77
|
+
await putTar(box, "base", baseTree);
|
|
78
|
+
const exclude = Object.keys(dotenv).map((n) => `/${n}`).join("\n");
|
|
79
|
+
const id = "-c user.name=bottleship -c user.email=review@bottleship.local";
|
|
80
|
+
const repos = await runIn(box, `set -e
|
|
81
|
+
cd "$HOME/base" && git init -q && printf '%s\\n' ${shq(exclude)} >> .git/info/exclude && git add -A && git ${id} commit -qm base
|
|
82
|
+
cp -R "$HOME/base/.git" "$HOME/workspace/.git" && cd "$HOME/workspace" && git add -A && git ${id} commit -qm ${shq(`local changes on ${subject.title}`)} --allow-empty
|
|
83
|
+
[ -d "$HOME/.bottleship/dotenv" ] && cp -R "$HOME/.bottleship/dotenv/." "$HOME/base/" || true`);
|
|
84
|
+
if (repos.code !== 0)
|
|
85
|
+
throw new BsError("setup_failed", `could not set up the checkouts in ${box.id}`, "see stderr", { stderr: repos.stderr.slice(-2000) });
|
|
86
|
+
}
|
|
87
|
+
// the review app: the template, the changes in src/data, and the dependencies from the image
|
|
88
|
+
await putTar(box, ".bottleship/review-template", await tarFiles(ROOT, templateFiles()));
|
|
89
|
+
const shas = await runIn(box, 'git -C "$HOME/base" rev-parse HEAD && git -C "$HOME/workspace" rev-parse HEAD');
|
|
90
|
+
const [base, head] = baseSha ? [baseSha, "working tree"] : shas.stdout.trim().split("\n");
|
|
91
|
+
const files = {
|
|
92
|
+
"review/app/src/data/pr.json": JSON.stringify(subject.raw, null, 2),
|
|
93
|
+
"review/app/src/data/review-comments.json": JSON.stringify(subject.reviewComments, null, 2),
|
|
94
|
+
"review/app/src/data/pr.diff": subject.diff,
|
|
95
|
+
"review/app/src/data/box.json": JSON.stringify({ base, head }),
|
|
96
|
+
};
|
|
97
|
+
// the reviewer's local, uncommitted environment, when the change itself has none
|
|
98
|
+
if (env === "local")
|
|
99
|
+
for (const [name, content] of Object.entries(localEnv))
|
|
100
|
+
for (const dir of ["workspace", "base"])
|
|
101
|
+
files[`${dir}/.bottleship/${name}`] = content;
|
|
102
|
+
const app = await runIn(box, `set -e
|
|
103
|
+
mkdir -p "$HOME/review/steps" "$HOME/review/shots"
|
|
104
|
+
cp -R "$HOME/.bottleship/review-template/review-app/." "$HOME/review/app"
|
|
105
|
+
rm -f "$HOME/review/app/src/data/"*
|
|
106
|
+
ln -sfn /opt/review-app/node_modules "$HOME/review/app/node_modules"`);
|
|
107
|
+
if (app.code !== 0)
|
|
108
|
+
throw new BsError("review_app_failed", `could not set up the review app in ${box.id}`, "see stderr", { stderr: app.stderr.slice(-2000) });
|
|
109
|
+
await putFiles(box, files);
|
|
110
|
+
await putTar(box, ".bottleship/review-server", await tarFiles(ROOT, SERVER_FILES));
|
|
111
|
+
// where this machine reaches each port the box publishes, for the review UI's links and frames
|
|
112
|
+
const urls = {};
|
|
113
|
+
if (box.provider.url)
|
|
114
|
+
for (const port of [REVIEW_PORT, ...INSTANCE_PORTS, ...repoEnv.ports]) {
|
|
115
|
+
try {
|
|
116
|
+
urls[port] = await box.provider.url(box.box, port);
|
|
117
|
+
}
|
|
118
|
+
catch { }
|
|
119
|
+
}
|
|
120
|
+
await putFiles(box, { ".bottleship/urls.json": JSON.stringify(urls) });
|
|
121
|
+
phase("Starting the review server");
|
|
122
|
+
// with the box's env, so posting a review uses its GH_TOKEN
|
|
123
|
+
const server = `. "$HOME/.bottleship/env.sh"
|
|
124
|
+
S="$HOME/.bottleship/review-server/dist/src/review/serve.js"
|
|
125
|
+
nohup node "$S" --dir "$HOME/review" --port ${REVIEW_PORT} --host ${shq(REVIEW_HOST)} --urls "$HOME/.bottleship/urls.json" --ask "$HOME/.bottleship/ask.sh" --state "$HOME/.bottleship/agent-state.sh" > "$HOME/.bottleship/review-server.log" 2>&1 < /dev/null &
|
|
126
|
+
echo $! > "$HOME/.bottleship/review-server.pid"
|
|
127
|
+
for i in $(seq 1 50); do curl -s -o /dev/null ${shq(`${reviewUrl}/api/status`)} && exit 0; sleep 0.2; done
|
|
128
|
+
cat "$HOME/.bottleship/review-server.log" >&2; exit 1`;
|
|
129
|
+
const r = await runIn(box, server);
|
|
130
|
+
if (r.code !== 0)
|
|
131
|
+
throw new BsError("review_server_failed", `the review server did not start in ${box.id}`, "see stderr", { stderr: r.stderr.slice(-2000) });
|
|
132
|
+
phase("Starting the agent");
|
|
133
|
+
await startAgent(box, reviewPrompt({ ...subject.prompt, reviewUrl, instancePorts: INSTANCE_PORTS, appPorts: repoEnv.ports, envSource: env }));
|
|
134
|
+
return { box, url: urls[REVIEW_PORT] ?? reviewUrl, pr: subject.url, title: subject.title, env };
|
|
135
|
+
}
|
|
136
|
+
catch (e) {
|
|
137
|
+
await box.provider.destroy(box.box).catch(() => { });
|
|
138
|
+
throw e;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface GhRepo {
|
|
2
|
+
host: string;
|
|
3
|
+
owner: string;
|
|
4
|
+
name: string;
|
|
5
|
+
}
|
|
6
|
+
export interface PrRepo {
|
|
7
|
+
label: string;
|
|
8
|
+
api: string;
|
|
9
|
+
hostname?: string;
|
|
10
|
+
clone: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function parseRemote(s: string): GhRepo | undefined;
|
|
13
|
+
export declare function parseRepoArg(s: string): GhRepo;
|
|
14
|
+
export declare function parsePrRef(s: string): {
|
|
15
|
+
number: number;
|
|
16
|
+
repo?: GhRepo;
|
|
17
|
+
};
|
|
18
|
+
export declare const sameRepo: (a: GhRepo, b: GhRepo) => boolean;
|
|
19
|
+
export declare function prRepo(r: GhRepo): PrRepo;
|
|
20
|
+
export declare function prRepoFromRemote(remote: string): PrRepo;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Which PR to review: a number, a shorthand, or a link, on github.com or a GitHub Enterprise host.
|
|
2
|
+
import { BsError } from "../config.js";
|
|
3
|
+
const FORMS = "17, #17, owner/name#17, host/owner/name#17, or a PR link like https://github.com/owner/name/pull/17";
|
|
4
|
+
const bad = (what) => new BsError("bad_request", `"${what}" is not a PR`, `use ${FORMS}`);
|
|
5
|
+
const strip = (n) => n.replace(/\.git$/, "");
|
|
6
|
+
const SEG = "[A-Za-z0-9_.-]+";
|
|
7
|
+
// a git remote or repo link: https://host/owner/name(.git), git@host:owner/name(.git), ssh://git@host[:port]/owner/name(.git)
|
|
8
|
+
export function parseRemote(s) {
|
|
9
|
+
const scp = s.match(new RegExp(`^[\\w.-]+@([^:/]+):(${SEG})/(${SEG})/?$`));
|
|
10
|
+
if (scp)
|
|
11
|
+
return { host: scp[1].toLowerCase(), owner: scp[2], name: strip(scp[3]) };
|
|
12
|
+
let u;
|
|
13
|
+
try {
|
|
14
|
+
u = new URL(s);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
if (!["https:", "http:", "ssh:", "git:"].includes(u.protocol))
|
|
20
|
+
return undefined;
|
|
21
|
+
const parts = u.pathname.replace(/^\/+|\/+$/g, "").split("/");
|
|
22
|
+
if (parts.length !== 2 || !parts.every((p) => new RegExp(`^${SEG}$`).test(p)))
|
|
23
|
+
return undefined;
|
|
24
|
+
return { host: u.hostname.toLowerCase(), owner: parts[0], name: strip(parts[1]) };
|
|
25
|
+
}
|
|
26
|
+
// --repo: owner/name (github.com), host/owner/name, or a link or remote
|
|
27
|
+
export function parseRepoArg(s) {
|
|
28
|
+
const short = s.match(new RegExp(`^(?:(${SEG}\\.${SEG})/)?(${SEG})/(${SEG})$`));
|
|
29
|
+
if (short)
|
|
30
|
+
return { host: (short[1] ?? "github.com").toLowerCase(), owner: short[2], name: strip(short[3]) };
|
|
31
|
+
const r = parseRemote(s);
|
|
32
|
+
if (!r)
|
|
33
|
+
throw new BsError("bad_request", `"${s}" is not a repo`, "use owner/name, host/owner/name for GitHub Enterprise, or the repo's URL");
|
|
34
|
+
return r;
|
|
35
|
+
}
|
|
36
|
+
export function parsePrRef(s) {
|
|
37
|
+
const arg = s.trim();
|
|
38
|
+
const n = arg.match(/^#?(\d+)$/);
|
|
39
|
+
if (n)
|
|
40
|
+
return { number: Number(n[1]) };
|
|
41
|
+
const short = arg.match(new RegExp(`^(?:(${SEG}\\.${SEG})/)?(${SEG})/(${SEG})#(\\d+)$`));
|
|
42
|
+
if (short)
|
|
43
|
+
return { number: Number(short[4]), repo: { host: (short[1] ?? "github.com").toLowerCase(), owner: short[2], name: strip(short[3]) } };
|
|
44
|
+
// a link to the PR or any page under it: /files, /commits, #comment anchors, query strings
|
|
45
|
+
let u;
|
|
46
|
+
try {
|
|
47
|
+
u = new URL(/^[a-z]+:\/\//i.test(arg) ? arg : `https://${arg}`);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
throw bad(s);
|
|
51
|
+
}
|
|
52
|
+
const p = u.pathname.split("/").filter(Boolean);
|
|
53
|
+
if (!u.hostname.includes(".") || p.length < 4 || p[2] !== "pull" || !/^\d+$/.test(p[3]))
|
|
54
|
+
throw bad(s);
|
|
55
|
+
return { number: Number(p[3]), repo: { host: u.hostname.toLowerCase(), owner: p[0], name: strip(p[1]) } };
|
|
56
|
+
}
|
|
57
|
+
export const sameRepo = (a, b) => a.host === b.host && a.owner.toLowerCase() === b.owner.toLowerCase() && a.name.toLowerCase() === b.name.toLowerCase();
|
|
58
|
+
export function prRepo(r) {
|
|
59
|
+
const github = r.host === "github.com";
|
|
60
|
+
return { label: github ? `${r.owner}/${r.name}` : `${r.host}/${r.owner}/${r.name}`, api: `repos/${r.owner}/${r.name}`, ...(!github && { hostname: r.host }), clone: `https://${r.host}/${r.owner}/${r.name}.git` };
|
|
61
|
+
}
|
|
62
|
+
// a remote that is no GitHub repo link, e.g. a path, goes to gh and git as it is
|
|
63
|
+
export function prRepoFromRemote(remote) {
|
|
64
|
+
const r = parseRemote(remote);
|
|
65
|
+
return r ? prRepo(r) : { label: remote, api: `repos/${remote}`, clone: remote };
|
|
66
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// The review app template: a Vite and React project with the review kit, which the box agent fills in for each PR.
|
|
2
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join, relative } from "node:path";
|
|
4
|
+
import { ROOT } from "../paths.js";
|
|
5
|
+
export const TEMPLATE = join(ROOT, "review-app");
|
|
6
|
+
// the playground is for working on the kit, not part of a review
|
|
7
|
+
const SKIP = new Set(["node_modules", "dist", ".DS_Store", "playground"]);
|
|
8
|
+
// every template file, relative to the package root
|
|
9
|
+
export function templateFiles(dir = TEMPLATE) {
|
|
10
|
+
return readdirSync(dir, { withFileTypes: true }).flatMap((e) => SKIP.has(e.name) ? [] : e.isDirectory() ? templateFiles(join(dir, e.name)) : [relative(ROOT, join(dir, e.name))]);
|
|
11
|
+
}
|
|
12
|
+
// the template's package.json, installed once into the box image so a review builds without a network
|
|
13
|
+
export const templatePackage = () => readFileSync(join(TEMPLATE, "package.json"), "utf8");
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// `bottleship review` for a person in a terminal: each phase as it happens, the review UI opened in the browser,
|
|
2
|
+
// then the box watched until the agent is done or the person stops watching. The review keeps going either way.
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { agentState } from "../boxes.js";
|
|
5
|
+
import { startReview } from "./start.js";
|
|
6
|
+
import { c } from "../ui.js";
|
|
7
|
+
const ENV = {
|
|
8
|
+
pr: "the PR's own .bottleship/",
|
|
9
|
+
local: "your local .bottleship/",
|
|
10
|
+
none: "nothing: neither the PR nor your checkout has a .bottleship/, so the agent works out how to run the app",
|
|
11
|
+
};
|
|
12
|
+
function openInBrowser(url) {
|
|
13
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "linux" ? "xdg-open" : undefined;
|
|
14
|
+
if (!cmd)
|
|
15
|
+
return false;
|
|
16
|
+
try {
|
|
17
|
+
spawn(cmd, [url], { stdio: "ignore", detached: true }).on("error", () => { }).unref();
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const elapsed = (since) => {
|
|
25
|
+
const s = Math.round((Date.now() - since) / 1000);
|
|
26
|
+
return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`;
|
|
27
|
+
};
|
|
28
|
+
export async function reviewInTerminal(ui, cfg, opts) {
|
|
29
|
+
const pr = "pr" in opts.source ? opts.source.pr : undefined;
|
|
30
|
+
ui.intro("review");
|
|
31
|
+
const setup = ui.spin(pr ? `Reading ${pr.repo.label}#${pr.number} from GitHub` : "Reading the local changes");
|
|
32
|
+
let r;
|
|
33
|
+
try {
|
|
34
|
+
r = await startReview(cfg, { ...opts, onPhase: (t) => setup.message(t) });
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
setup.stop("Could not start the review", true);
|
|
38
|
+
throw e;
|
|
39
|
+
}
|
|
40
|
+
setup.stop(pr ? `${c.bold(`#${pr.number}`)} ${r.title}` : `${c.bold("Local changes")} on ${r.title}`);
|
|
41
|
+
const opened = openInBrowser(r.url);
|
|
42
|
+
ui.note("Review", `${r.url}${opened ? " (opened in your browser)" : ""}\n\nIt shows the tour as soon as the agent has built it. Box ${r.box.id}, environment from ${ENV[r.env]}.`);
|
|
43
|
+
// watch until the agent is done; Ctrl-C only stops watching
|
|
44
|
+
const started = Date.now();
|
|
45
|
+
let watch = ui.spin("Setting up the environment");
|
|
46
|
+
let stopped = false;
|
|
47
|
+
const stop = () => (stopped = true);
|
|
48
|
+
process.once("SIGINT", stop);
|
|
49
|
+
let envSeen = "";
|
|
50
|
+
let built = false;
|
|
51
|
+
let last = { agent: "running", log: "" };
|
|
52
|
+
try {
|
|
53
|
+
while (!stopped) {
|
|
54
|
+
last = await agentState(r.box, 12);
|
|
55
|
+
if (last.env && last.env !== envSeen && ["ready", "failed"].includes(last.env)) {
|
|
56
|
+
envSeen = last.env;
|
|
57
|
+
watch.stop(last.env === "ready" ? "The app's environment is up" : "The app's environment failed to set up; the agent will work around it", last.env === "failed");
|
|
58
|
+
if (last.env === "failed")
|
|
59
|
+
ui.say(c.dim(` see it: bottleship agent exec ${r.box.id} -- tail -40 ~/.bottleship/setup.log ~/.bottleship/app.log`));
|
|
60
|
+
watch = ui.spin("Agent is building the tour");
|
|
61
|
+
}
|
|
62
|
+
if (!built) {
|
|
63
|
+
built = await fetch(`${r.url}/api/status`).then((x) => x.json()).then((s) => !!s.app, () => false);
|
|
64
|
+
if (built) {
|
|
65
|
+
watch.stop("The tour is built and open in the review; the agent is checking it");
|
|
66
|
+
watch = ui.spin("Agent is checking the tour");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (last.agent !== "running")
|
|
70
|
+
break;
|
|
71
|
+
watch.message(`${built ? "Agent is checking the tour" : envSeen ? "Agent is building the tour" : "Setting up the environment"} ${c.dim(elapsed(started))}`);
|
|
72
|
+
await new Promise((res) => setTimeout(res, 2000));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
process.off("SIGINT", stop);
|
|
77
|
+
}
|
|
78
|
+
if (stopped) {
|
|
79
|
+
watch.stop("Stopped watching; the review keeps going in the box");
|
|
80
|
+
ui.say(" The tour shows how far it got.");
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
const failed = last.agent !== "exited" || ("exitCode" in last && last.exitCode !== 0);
|
|
84
|
+
watch.stop(failed ? `The agent stopped after ${elapsed(started)}` : `The agent is done after ${elapsed(started)}`, failed);
|
|
85
|
+
const summary = last.log.trim().split("\n").slice(-3).join("\n");
|
|
86
|
+
if (summary)
|
|
87
|
+
ui.note(failed ? "Last output" : "Agent's summary", summary);
|
|
88
|
+
}
|
|
89
|
+
ui.say(` ${c.dim(`bottleship box rm ${r.box.id}`)} when the review is over`);
|
|
90
|
+
ui.close(built ? `Review at ${r.url}` : "Done");
|
|
91
|
+
}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -13,33 +13,12 @@ export interface AgentSpec {
|
|
|
13
13
|
resume: string;
|
|
14
14
|
env?: EnvMap;
|
|
15
15
|
unset?: string[];
|
|
16
|
-
login?:
|
|
17
|
-
}
|
|
18
|
-
export type LoginRoute = "token" | "device" | "none";
|
|
19
|
-
export interface AgentLogin {
|
|
20
|
-
routes: LoginRoute[];
|
|
21
|
-
use: LoginRoute;
|
|
22
|
-
token?: {
|
|
16
|
+
login?: {
|
|
23
17
|
env: string;
|
|
24
18
|
store: string;
|
|
25
19
|
command?: string;
|
|
26
20
|
hint?: string;
|
|
27
21
|
};
|
|
28
|
-
tokens?: {
|
|
29
|
-
env: string;
|
|
30
|
-
store: string;
|
|
31
|
-
hint?: string;
|
|
32
|
-
}[];
|
|
33
|
-
device?: {
|
|
34
|
-
command: string;
|
|
35
|
-
status: string;
|
|
36
|
-
setup?: string;
|
|
37
|
-
timeout?: number;
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
export interface ProviderSpec {
|
|
41
|
-
module: string;
|
|
42
|
-
[option: string]: unknown;
|
|
43
22
|
}
|
|
44
23
|
export interface Config {
|
|
45
24
|
provider: string;
|
|
@@ -49,12 +28,7 @@ export interface Config {
|
|
|
49
28
|
email: string;
|
|
50
29
|
};
|
|
51
30
|
env: EnvMap;
|
|
52
|
-
providers: Record<string, ProviderSpec>;
|
|
53
31
|
agents: Record<string, AgentSpec>;
|
|
54
|
-
templates: Record<string, {
|
|
55
|
-
name: string;
|
|
56
|
-
ref: string;
|
|
57
|
-
}>;
|
|
58
32
|
}
|
|
59
33
|
export interface ExecResult {
|
|
60
34
|
code: number | null;
|
|
@@ -68,50 +42,6 @@ export interface BoxStream {
|
|
|
68
42
|
close(): void;
|
|
69
43
|
onClose(cb: (code?: number | null) => void): void;
|
|
70
44
|
}
|
|
71
|
-
export type BoxStatus = "running" | "gone";
|
|
72
|
-
export interface TemplateSpec {
|
|
73
|
-
name: string;
|
|
74
|
-
dockerfile: string;
|
|
75
|
-
}
|
|
76
|
-
export interface Provider {
|
|
77
|
-
template?(spec: TemplateSpec): Promise<string>;
|
|
78
|
-
create(opts: {
|
|
79
|
-
id: string;
|
|
80
|
-
source?: string;
|
|
81
|
-
template?: string;
|
|
82
|
-
}): Promise<{
|
|
83
|
-
id: string;
|
|
84
|
-
}>;
|
|
85
|
-
exec(id: string, script: string, opts?: {
|
|
86
|
-
stdin?: string | Buffer;
|
|
87
|
-
}): Promise<ExecResult>;
|
|
88
|
-
destroy(id: string): Promise<unknown>;
|
|
89
|
-
openStream?(id: string, script: string): BoxStream | Promise<BoxStream>;
|
|
90
|
-
status?(id: string): Promise<BoxStatus>;
|
|
91
|
-
}
|
|
92
|
-
export interface LoadedProvider extends Provider {
|
|
93
|
-
name: string;
|
|
94
|
-
}
|
|
95
|
-
export interface RunOptions {
|
|
96
|
-
stdin?: string | Buffer;
|
|
97
|
-
check?: boolean;
|
|
98
|
-
env?: Record<string, string>;
|
|
99
|
-
}
|
|
100
|
-
export interface ProviderKit {
|
|
101
|
-
run(cmd: string, args: string[], opts?: RunOptions): Promise<ExecResult>;
|
|
102
|
-
shq(s: unknown): string;
|
|
103
|
-
fill(template: string, values: Record<string, unknown>): string;
|
|
104
|
-
spawnStream(cmd: string, args: string[]): BoxStream;
|
|
105
|
-
wsStream(url: string, headers?: Record<string, string>): BoxStream;
|
|
106
|
-
}
|
|
107
|
-
export interface ProviderCtx {
|
|
108
|
-
name: string;
|
|
109
|
-
options: Record<string, any>;
|
|
110
|
-
secret(source: SecretSource): string;
|
|
111
|
-
log(...args: unknown[]): void;
|
|
112
|
-
kit: ProviderKit;
|
|
113
|
-
}
|
|
114
|
-
export type ProviderFactory = (ctx: ProviderCtx) => Provider | Promise<Provider>;
|
|
115
45
|
export interface BsErrorJSON {
|
|
116
46
|
error: string;
|
|
117
47
|
message: string;
|
package/dist/src/ui.d.ts
CHANGED
|
@@ -9,8 +9,12 @@ export declare const c: {
|
|
|
9
9
|
white: (s: string) => string;
|
|
10
10
|
};
|
|
11
11
|
export declare function header(): string;
|
|
12
|
+
export interface Spinner {
|
|
13
|
+
message(text: string): void;
|
|
14
|
+
stop(text: string, failed?: boolean): void;
|
|
15
|
+
}
|
|
12
16
|
export interface UI {
|
|
13
|
-
intro(): void;
|
|
17
|
+
intro(title?: string): void;
|
|
14
18
|
say(line?: string): void;
|
|
15
19
|
section(title: string): void;
|
|
16
20
|
ok(label: string, detail?: string): void;
|
|
@@ -25,6 +29,7 @@ export interface UI {
|
|
|
25
29
|
confirm(question: string): Promise<boolean>;
|
|
26
30
|
handOver(command: string): Promise<number | null>;
|
|
27
31
|
step<T>(label: string, fn: () => Promise<T>): Promise<T>;
|
|
32
|
+
spin(label: string): Spinner;
|
|
28
33
|
fail(message: string, fix?: string): void;
|
|
29
34
|
close(message?: string): void;
|
|
30
35
|
}
|
package/dist/src/ui.js
CHANGED
|
@@ -13,35 +13,29 @@ export const c = {
|
|
|
13
13
|
blue: sgr("34"),
|
|
14
14
|
white: sgr("97"),
|
|
15
15
|
};
|
|
16
|
-
//
|
|
16
|
+
// the sail on two waves, as on the review's pages, with a mask per line: w the sail and the near wave, d the far one
|
|
17
17
|
const ART = [
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
" \\ ~~~~~~\\__o___o___o___o___o__/~~~~~~ /",
|
|
31
|
-
" c bbbbbbrrryrrryrrryrrryrrryrrrbbbbbb c",
|
|
32
|
-
" \\ ~ ~ ~ ~ ~ ~ ~ ~ /",
|
|
33
|
-
" c b b b b b b b b c",
|
|
34
|
-
" '-----------------------------------------------'",
|
|
35
|
-
" ccccccccccccccccccccccccccccccccccccccccccccccccc",
|
|
18
|
+
" │╲",
|
|
19
|
+
" ww",
|
|
20
|
+
" │ ╲",
|
|
21
|
+
" w ww",
|
|
22
|
+
" │ ╲",
|
|
23
|
+
" w w",
|
|
24
|
+
" │___╲",
|
|
25
|
+
" wwwww",
|
|
26
|
+
" ∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿",
|
|
27
|
+
" ddddddddddddddd",
|
|
28
|
+
" ∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿",
|
|
29
|
+
" wwwwwwwwwwwwwwwww",
|
|
36
30
|
];
|
|
37
|
-
const paint = {
|
|
31
|
+
const paint = { w: c.white, d: c.dim };
|
|
38
32
|
export function header() {
|
|
39
33
|
const rows = [];
|
|
40
34
|
for (let i = 0; i < ART.length; i += 2) {
|
|
41
35
|
const [text, mask] = [ART[i], ART[i + 1]];
|
|
42
36
|
rows.push([...text].map((ch, j) => (paint[mask[j]] ?? String)(ch)).join(""));
|
|
43
37
|
}
|
|
44
|
-
return `\n${rows.join("\n")}\n\n
|
|
38
|
+
return `\n${rows.join("\n")}\n\n ${c.bold("bottleship")}\n`;
|
|
45
39
|
}
|
|
46
40
|
// soft-wraps each line to width, keeping a list item's hanging indent
|
|
47
41
|
function wrap(text, width) {
|
|
@@ -71,9 +65,9 @@ function answered(v) {
|
|
|
71
65
|
export function terminalUI() {
|
|
72
66
|
let closed = false;
|
|
73
67
|
return {
|
|
74
|
-
intro() {
|
|
68
|
+
intro(title = "setup") {
|
|
75
69
|
process.stdout.write(header() + "\n");
|
|
76
|
-
p.intro(c.bold(
|
|
70
|
+
p.intro(c.bold(` ${title} `));
|
|
77
71
|
},
|
|
78
72
|
say: (line = "") => p.log.message(line.replace(/^ +/gm, "")),
|
|
79
73
|
section: (title) => p.log.step(c.bold(title)),
|
|
@@ -112,6 +106,11 @@ export function terminalUI() {
|
|
|
112
106
|
throw e;
|
|
113
107
|
}
|
|
114
108
|
},
|
|
109
|
+
spin(label) {
|
|
110
|
+
const s = p.spinner();
|
|
111
|
+
s.start(label);
|
|
112
|
+
return { message: (t) => s.message(t), stop: (t, failed) => s.stop(failed ? c.red(t) : t, failed ? 1 : 0) };
|
|
113
|
+
},
|
|
115
114
|
fail(message, fix) {
|
|
116
115
|
p.log.error(`${message}${fix ? "\n" + c.dim(fix) : ""}`);
|
|
117
116
|
p.outro(c.red("Setup did not finish."));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { BoxStream, ExecResult } from "./types.ts";
|
|
2
|
+
export interface RunOptions {
|
|
3
|
+
stdin?: string | Buffer;
|
|
4
|
+
check?: boolean;
|
|
5
|
+
env?: Record<string, string>;
|
|
6
|
+
cwd?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function run(cmd: string, args: string[], opts?: RunOptions): Promise<ExecResult>;
|
|
9
|
+
export declare function spawnStream(cmd: string, args: string[]): BoxStream;
|
|
10
|
+
export declare const shq: (s: unknown) => string;
|
|
11
|
+
export declare function fill(template: string, values: Record<string, unknown>): string;
|
package/dist/src/util.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Process helpers: run a command to completion, or keep one open as a stream.
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
function spawnPiped(cmd, args, { env, cwd } = {}) {
|
|
4
|
+
return spawn(cmd, args, { stdio: ["pipe", "pipe", "pipe"], cwd, env: env ? { ...process.env, ...env } : process.env });
|
|
5
|
+
}
|
|
6
|
+
// resolves { code, stdout, stderr }; with check (default true) a non-zero exit rejects
|
|
7
|
+
export function run(cmd, args, opts = {}) {
|
|
8
|
+
const { stdin, check = true } = opts;
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const child = spawnPiped(cmd, args, opts);
|
|
11
|
+
let stdout = "";
|
|
12
|
+
let stderr = "";
|
|
13
|
+
child.stdout.on("data", (d) => (stdout += d));
|
|
14
|
+
child.stderr.on("data", (d) => (stderr += d));
|
|
15
|
+
child.on("error", reject);
|
|
16
|
+
child.on("close", (code) => {
|
|
17
|
+
if (check && code !== 0)
|
|
18
|
+
reject(new Error(`${cmd} ${args.slice(0, 3).join(" ")}... exited ${code}: ${stderr.trim().slice(-500)}`));
|
|
19
|
+
else
|
|
20
|
+
resolve({ code, stdout, stderr });
|
|
21
|
+
});
|
|
22
|
+
child.stdin.on("error", () => { });
|
|
23
|
+
child.stdin.end(stdin ?? "");
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
// a long-lived child process as a BoxStream
|
|
27
|
+
export function spawnStream(cmd, args) {
|
|
28
|
+
const child = spawnPiped(cmd, args);
|
|
29
|
+
child.stdin.on("error", () => { });
|
|
30
|
+
return { stdin: child.stdin, stdout: child.stdout, stderr: child.stderr, close: () => child.kill(), onClose: (cb) => child.on("close", (code) => cb(code)) };
|
|
31
|
+
}
|
|
32
|
+
// single-quote for sh
|
|
33
|
+
export const shq = (s) => `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
34
|
+
// fills {name} with the shell-quoted value, missing values with ""
|
|
35
|
+
export function fill(template, values) {
|
|
36
|
+
return template.replace(/\{(\w+)\}/g, (_, k) => {
|
|
37
|
+
const v = values[k];
|
|
38
|
+
return v === undefined || v === null ? "" : shq(v);
|
|
39
|
+
});
|
|
40
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bottleship",
|
|
3
|
-
"version": "0.1.0-prototype.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.0-prototype.2",
|
|
4
|
+
"description": "Coding agents in boxes: isolated dev environments for your repo, run by your local agent.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Joshua Knauber",
|
|
7
7
|
"type": "module",
|
|
@@ -11,21 +11,17 @@
|
|
|
11
11
|
"files": [
|
|
12
12
|
"bin",
|
|
13
13
|
"dist/src",
|
|
14
|
-
"dist/presets",
|
|
15
14
|
"presets",
|
|
15
|
+
"review-app",
|
|
16
|
+
"!review-app/node_modules",
|
|
17
|
+
"!review-app/dist",
|
|
16
18
|
"README.md",
|
|
17
19
|
"CHANGELOG.md",
|
|
18
20
|
"LICENSE"
|
|
19
21
|
],
|
|
20
|
-
"exports": {
|
|
21
|
-
".": {
|
|
22
|
-
"types": "./dist/src/types.d.ts",
|
|
23
|
-
"default": "./dist/src/types.js"
|
|
24
|
-
}
|
|
25
|
-
},
|
|
26
22
|
"scripts": {
|
|
27
23
|
"build": "tsc -p tsconfig.json",
|
|
28
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
24
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p review-app/tsconfig.json",
|
|
29
25
|
"test": "npm run build && vitest run",
|
|
30
26
|
"prepare": "npm run build",
|
|
31
27
|
"release": "node scripts/release.mjs"
|
|
@@ -39,10 +35,7 @@
|
|
|
39
35
|
"vitest": "^3.2.7"
|
|
40
36
|
},
|
|
41
37
|
"dependencies": {
|
|
42
|
-
"@clack/prompts": "^0.11.0"
|
|
43
|
-
"@daytonaio/sdk": "^0.231.1",
|
|
44
|
-
"e2b": "^2.63.0",
|
|
45
|
-
"modal": "^0.11.1"
|
|
38
|
+
"@clack/prompts": "^0.11.0"
|
|
46
39
|
},
|
|
47
40
|
"publishConfig": {
|
|
48
41
|
"access": "public"
|
|
@@ -4,9 +4,5 @@
|
|
|
4
4
|
"resume": "claude -p {prompt} --continue --dangerously-skip-permissions",
|
|
5
5
|
"env": { "IS_SANDBOX": "1" },
|
|
6
6
|
"unset": ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"],
|
|
7
|
-
"login": {
|
|
8
|
-
"routes": ["token"],
|
|
9
|
-
"use": "token",
|
|
10
|
-
"token": { "env": "CLAUDE_CODE_OAUTH_TOKEN", "store": "claude", "command": "claude setup-token", "hint": "paste the long-lived token `claude setup-token` prints; it uses your Claude subscription" }
|
|
11
|
-
}
|
|
7
|
+
"login": { "env": "CLAUDE_CODE_OAUTH_TOKEN", "store": "claude", "command": "claude setup-token", "hint": "paste the long-lived token `claude setup-token` prints; it uses your Claude subscription" }
|
|
12
8
|
}
|