basepad 0.0.0-stage → 0.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aysnc Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,84 @@
1
- # Temporary Holding Version
1
+ <p align="center">
2
+ <img src="docs/rocket.svg" width="220" alt="basepad's rocket on its launch pad">
3
+ </p>
2
4
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
5
+ <h1 align="center">basepad</h1>
6
+
7
+ <p align="center">
8
+ A local environment for coding agents.<br>
9
+ On your machine, or in a cloud sandbox.
10
+ </p>
11
+
12
+ ---
13
+
14
+ Tell basepad what your project runs:
15
+
16
+ ```ts
17
+ // basepad.config.ts
18
+ import { defineConfig, mailpit, postgres, redis } from "basepad";
19
+
20
+ export default defineConfig({
21
+ name: "shop",
22
+ services: { db: postgres(), cache: redis(), mail: mailpit() },
23
+ apps: {
24
+ web: { run: "next dev" },
25
+ api: { run: "php artisan serve --port=$PORT" },
26
+ },
27
+ env: ({ db, cache, mail, web }) => ({
28
+ ".env.local": {
29
+ DATABASE_URL: db.url,
30
+ REDIS_URL: cache.url,
31
+ MAIL_URL: mail.url,
32
+ APP_URL: web.url,
33
+ },
34
+ }),
35
+ });
36
+ ```
37
+
38
+ `basepad up` starts it. Every agent working in a worktree gets its own copy, with its own data and its own URLs:
39
+
40
+ ```
41
+ https://web.shop.localhost you, on main
42
+ https://web.worktree-1.shop.localhost the agent fixing checkout
43
+ https://web.worktree-2.shop.localhost the agent rewriting search
44
+ ```
45
+
46
+ Mail it sends lands in a local inbox at `https://mail.shop.localhost`, and `basepad db save` snapshots its database before it tries something risky.
47
+
48
+ ## Try it
49
+
50
+ ```sh
51
+ curl https://mise.run | sh # basepad installs everything else through mise
52
+ npm install --save-dev basepad
53
+ npx basepad init # writes a starter config
54
+ npx basepad up
55
+ ```
56
+
57
+ Run `npx basepad setup` once per machine and your browser will trust the local HTTPS domains. It's worth it for the rocket alone.
58
+
59
+ Works on macOS (Apple Silicon) and Linux (x64 and ARM).
60
+
61
+ ## Commands
62
+
63
+ | | |
64
+ | --- | --- |
65
+ | `basepad up` | Start everything and open mission control (`-d` skips it) |
66
+ | `basepad down` | Stop everything. Data and env files stay put |
67
+ | `basepad status` | Every app's URL and state |
68
+ | `basepad worktree add` | A new branch with its own environment |
69
+ | `basepad worktree rm <name>` | Throw one away. The branch survives |
70
+ | `basepad db save`, `db load` | Snapshot this environment's database, and put it back |
71
+ | `basepad setup` | Once per machine, for trusted URLs with no port |
72
+
73
+ The [guide](docs/guide.md) has the rest: every config option, how worktrees copy data, the Claude Code hooks, CI and sandboxes, and every key in mission control.
74
+
75
+ ## Hacking on basepad
76
+
77
+ ```sh
78
+ mise install && npm ci
79
+ npm run check
80
+ ```
81
+
82
+ The tests run the real thing against real mise, pitchfork and Postgres. [`AGENTS.md`](AGENTS.md) explains how to keep a test run away from your own setup.
83
+
84
+ MIT licensed.
@@ -0,0 +1,82 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { z } from "zod";
4
+ import { BasepadError, shellQuote } from "../internal/exec.js";
5
+ import { answers, freePort, waitFor } from "../internal/ports.js";
6
+ import { recordRunning, runningPort } from "../internal/processes.js";
7
+ import { install } from "../tools.js";
8
+ import { MAILPIT_VERSION } from "../versions.js";
9
+ /**
10
+ * Mailpit: one inbox per project, which catches every message the project's environments send.
11
+ * Its inbox is its domain; its SMTP server is on loopback. Main and every worktree share it, and
12
+ * each sends as its own SMTP user, which Mailpit tags each message with. Its messages are kept in
13
+ * the service's folder, so they survive a restart.
14
+ */
15
+ const HOST = "127.0.0.1";
16
+ /** Mailpit accepts any password, but some mailers authenticate only with one. */
17
+ const PASSWORD = "basepad";
18
+ /** How long `up` waits for its SMTP server to answer before it gives up. */
19
+ const READY_MS = 30_000;
20
+ /**
21
+ * Catches all mail the project sends. Its inbox is at `https://<key>.<name>.localhost`; each
22
+ * message is tagged with the environment that sent it: `main`, or a worktree's suffix.
23
+ */
24
+ export function mailpit() {
25
+ return { kind: "mailpit" };
26
+ }
27
+ export const mailpitKind = {
28
+ schema: z.strictObject({ kind: z.literal("mailpit") }),
29
+ web: true,
30
+ names(_service, { dir, suffix }) {
31
+ return { user: suffix ?? "main", data: join(dir, "mailpit.db"), pid: join(dir, "mailpit.pid") };
32
+ },
33
+ async prepare(_service, place) {
34
+ const bin = (await install("mailpit", MAILPIT_VERSION, place.machine.home)).bin("mailpit");
35
+ await mkdir(place.dir, { recursive: true });
36
+ const smtp = (await runningPort(place.pid)) ?? (await freePort(place.port));
37
+ const argv = [
38
+ bin,
39
+ "--smtp",
40
+ `${HOST}:${smtp}`,
41
+ "--database",
42
+ place.data,
43
+ // Any user and password, over plain SMTP on loopback: the user becomes the message's tag.
44
+ "--smtp-auth-accept-any",
45
+ "--smtp-auth-allow-insecure",
46
+ "--tags-username",
47
+ "--smtp-disable-rdns",
48
+ "--disable-version-check",
49
+ ];
50
+ return {
51
+ run: `${recordRunning(place.pid, smtp)} && exec ${argv.map(shellQuote).join(" ")} --listen "${HOST}:$PORT"`,
52
+ dir: place.dir,
53
+ port: webOf(place).port,
54
+ };
55
+ },
56
+ async provision(_service, place, { stopped }) {
57
+ const port = await waitFor(async () => {
58
+ const port = await runningPort(place.pid);
59
+ if (port === undefined)
60
+ throw new Error("It has not started.");
61
+ if (!(await answers(port, [HOST])))
62
+ throw new Error(`Nothing answers on port ${port}.`);
63
+ return port;
64
+ }, `Mailpit for \`${place.key}\` didn't answer within ${READY_MS / 1000} s.`, READY_MS, stopped);
65
+ return {
66
+ url: `smtp://${place.user}:${PASSWORD}@${HOST}:${port}`,
67
+ host: HOST,
68
+ port,
69
+ user: place.user,
70
+ password: PASSWORD,
71
+ inbox: webOf(place).url,
72
+ };
73
+ },
74
+ async address(_service, place) {
75
+ return webOf(place).url;
76
+ },
77
+ };
78
+ function webOf(place) {
79
+ if (place.web === undefined)
80
+ throw new BasepadError(`\`${place.key}\` has no domain.`);
81
+ return place.web;
82
+ }