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 +21 -0
- package/README.md +83 -2
- package/dist/catalogue/mailpit.js +82 -0
- package/dist/catalogue/postgres.js +632 -0
- package/dist/catalogue/redis.js +135 -0
- package/dist/catalogue.js +13 -0
- package/dist/claude-hooks.js +33 -0
- package/dist/cli.js +182 -0
- package/dist/commands.js +989 -0
- package/dist/config.js +217 -0
- package/dist/env-files.js +123 -0
- package/dist/forward.js +53 -0
- package/dist/hosts.js +79 -0
- package/dist/index.js +11 -0
- package/dist/init.js +88 -0
- package/dist/internal/exec.js +58 -0
- package/dist/internal/ports.js +63 -0
- package/dist/internal/processes.js +42 -0
- package/dist/internal/service-user.js +53 -0
- package/dist/internal/table.js +7 -0
- package/dist/launcher.js +53 -0
- package/dist/layout.js +234 -0
- package/dist/mission-control/act.js +91 -0
- package/dist/mission-control/feed.js +254 -0
- package/dist/mission-control/frame.js +799 -0
- package/dist/mission-control/launch.js +362 -0
- package/dist/mission-control/model.js +325 -0
- package/dist/mission-control/run.js +211 -0
- package/dist/mission-control/work.js +162 -0
- package/dist/setup.js +280 -0
- package/dist/supervisor.js +477 -0
- package/dist/tls.js +99 -0
- package/dist/tools.js +32 -0
- package/dist/versions.js +19 -0
- package/dist/worktrees.js +186 -0
- package/package.json +50 -5
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, readdir, realpath, stat } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, join, relative } from "node:path";
|
|
4
|
+
import { CONFIG_FILE } from "./config.js";
|
|
5
|
+
import { BasepadError, exec, must } from "./internal/exec.js";
|
|
6
|
+
import { nextSuffix, worktreeFolder } from "./layout.js";
|
|
7
|
+
/**
|
|
8
|
+
* Whether `root` (a config's folder) is in a linked git worktree, and if so its suffix and main's
|
|
9
|
+
* matching folder. Outside git, or in git's main checkout, it is main.
|
|
10
|
+
*/
|
|
11
|
+
export async function checkoutOf(root) {
|
|
12
|
+
const top = await toplevel(root);
|
|
13
|
+
if (top === undefined)
|
|
14
|
+
return {};
|
|
15
|
+
const [first] = await checkoutsOf(top);
|
|
16
|
+
if (first === undefined || first.path === top)
|
|
17
|
+
return {};
|
|
18
|
+
return { suffix: basename(top), main: join(first.path, relative(top, root)) };
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* What `worktree add` prints as soon as its git worktree is there, before its setup runs; its errors
|
|
22
|
+
* after that end with it too. An addition killed partway is known to have made `suffix` by it.
|
|
23
|
+
*/
|
|
24
|
+
export function checkedOut(suffix, root) {
|
|
25
|
+
return `Worktree ${suffix} is checked out at ${root}`;
|
|
26
|
+
}
|
|
27
|
+
/** The suffix `lines` (`worktree add`'s output) say was checked out, if any. */
|
|
28
|
+
export function checkedOutIn(lines) {
|
|
29
|
+
for (const line of lines) {
|
|
30
|
+
const suffix = /^Worktree (\S+) is checked out at /.exec(line)?.[1];
|
|
31
|
+
if (suffix !== undefined)
|
|
32
|
+
return suffix;
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Checks out a new worktree of `main` (a project in git's main checkout), named `<prefix>-<n>` with
|
|
38
|
+
* the lowest number no worktree, branch or folder has. It is on `branch` when that exists, here or
|
|
39
|
+
* on a remote; else on a new branch of that name, or of the suffix, from main's `HEAD`. Returns the
|
|
40
|
+
* suffix, the branch and the worktree's folder for the config.
|
|
41
|
+
*/
|
|
42
|
+
export async function checkOutWorktree(main, branch) {
|
|
43
|
+
const top = await toplevel(main.root);
|
|
44
|
+
if (top === undefined) {
|
|
45
|
+
throw new BasepadError(`A worktree is a git worktree, and ${main.root} is not in a git repository.`);
|
|
46
|
+
}
|
|
47
|
+
const dir = worktreeFolder(main, top);
|
|
48
|
+
const branches = (await git(top, ["for-each-ref", "--format=%(refname:short)", "refs/heads"]))
|
|
49
|
+
.split("\n")
|
|
50
|
+
.filter(Boolean);
|
|
51
|
+
const folders = existsSync(dir) ? await readdir(dir) : [];
|
|
52
|
+
const checkouts = (await checkoutsOf(top)).map(({ path }) => basename(path));
|
|
53
|
+
const suffix = nextSuffix(main.worktrees.prefix, [...branches, ...folders, ...checkouts]);
|
|
54
|
+
const path = worktreeFolder(main, top, suffix);
|
|
55
|
+
const on = branch ?? suffix;
|
|
56
|
+
// For a branch only a remote has, git makes a local one tracking it.
|
|
57
|
+
const exists = branch !== undefined &&
|
|
58
|
+
(await git(top, [
|
|
59
|
+
"for-each-ref",
|
|
60
|
+
"--format=%(refname)",
|
|
61
|
+
`refs/heads/${on}`,
|
|
62
|
+
`refs/remotes/*/${on}`,
|
|
63
|
+
])) !== "";
|
|
64
|
+
await git(top, [
|
|
65
|
+
"worktree",
|
|
66
|
+
"add",
|
|
67
|
+
"--quiet",
|
|
68
|
+
...(exists ? [path, on] : ["-b", on, path, "HEAD"]),
|
|
69
|
+
]);
|
|
70
|
+
const root = join(await realpath(path), relative(top, main.root));
|
|
71
|
+
if (!existsSync(join(root, CONFIG_FILE))) {
|
|
72
|
+
throw new BasepadError(`The worktree at ${path} has no ${relative(top, join(main.root, CONFIG_FILE))}: commit it in main, then remove this worktree with \`git worktree remove ${path}\` and try again.`);
|
|
73
|
+
}
|
|
74
|
+
return { suffix, branch: on, root };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Copies each of `paths` (relative to both folders) from main's folder to a worktree's, file by
|
|
78
|
+
* file, copy-on-write where the filesystem can (APFS, btrfs, XFS) and plainly otherwise. Never a
|
|
79
|
+
* link: a worktree's writes must not reach main's files. Returns a note for each path main lacks.
|
|
80
|
+
* The system's `cp` does it, which is faster than Node's own on a large `node_modules`.
|
|
81
|
+
*/
|
|
82
|
+
export async function clonePaths(from, to, paths) {
|
|
83
|
+
const notes = [];
|
|
84
|
+
for (const path of paths) {
|
|
85
|
+
const source = join(from, path);
|
|
86
|
+
const target = join(to, path);
|
|
87
|
+
if (!existsSync(source)) {
|
|
88
|
+
notes.push(`Skipped \`${path}\` from \`worktrees.clone\`: main has none.`);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
// A folder's contents go into the target folder, which may exist (with tracked files).
|
|
92
|
+
const folder = (await stat(source)).isDirectory();
|
|
93
|
+
await mkdir(folder ? target : dirname(target), { recursive: true });
|
|
94
|
+
const args = folder ? [`${source}/.`, `${target}/`] : [source, target];
|
|
95
|
+
await copyOnWrite(args);
|
|
96
|
+
}
|
|
97
|
+
return notes;
|
|
98
|
+
}
|
|
99
|
+
/** `cp -R` with copy-on-write where the filesystem can; symlinks are copied as they are. */
|
|
100
|
+
async function copyOnWrite(args) {
|
|
101
|
+
if (process.platform !== "darwin") {
|
|
102
|
+
await must("cp", ["-R", "--reflink=auto", ...args]);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// macOS's `cp -c` clones, and fails on a volume that cannot.
|
|
106
|
+
const cloned = await exec("cp", ["-c", "-R", ...args]);
|
|
107
|
+
if (cloned.code !== 0)
|
|
108
|
+
await must("cp", ["-R", ...args]);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Runs each setup command in order in `dir` with the developer's own PATH, saying each before it
|
|
112
|
+
* runs. The first that fails throws with its output, and the rest do not run.
|
|
113
|
+
*/
|
|
114
|
+
export async function runSetup(dir, commands, say) {
|
|
115
|
+
const env = { ...process.env };
|
|
116
|
+
// The launcher moved basepad onto mise's Node; the project's commands get the PATH they had.
|
|
117
|
+
if (env.BASEPAD_PATH)
|
|
118
|
+
env.PATH = env.BASEPAD_PATH;
|
|
119
|
+
delete env.BASEPAD_PATH;
|
|
120
|
+
for (const command of commands) {
|
|
121
|
+
say(`Running \`${command}\``);
|
|
122
|
+
const out = await exec("sh", ["-c", command], { cwd: dir, env });
|
|
123
|
+
if (out.code === 0)
|
|
124
|
+
continue;
|
|
125
|
+
const output = `${out.stdout}${out.stderr}`.trimEnd();
|
|
126
|
+
throw new BasepadError(`Setup command \`${command}\` failed (exit ${out.code}). Its output:\n${(output || "(none)").replace(/^/gm, " ")}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** Every worktree of `main` (a project in git's main checkout), as git lists them; none outside git. */
|
|
130
|
+
export async function listWorktrees(main) {
|
|
131
|
+
const top = await toplevel(main.root);
|
|
132
|
+
if (top === undefined)
|
|
133
|
+
return [];
|
|
134
|
+
// Main's own checkout comes first.
|
|
135
|
+
return (await checkoutsOf(top)).slice(1).map(({ path, branch }) => ({
|
|
136
|
+
suffix: basename(path),
|
|
137
|
+
path,
|
|
138
|
+
root: join(path, relative(top, main.root)),
|
|
139
|
+
...(branch === undefined ? {} : { branch }),
|
|
140
|
+
present: existsSync(path),
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
/** Main's own branch, as git lists it; none when detached or outside git. */
|
|
144
|
+
export async function mainBranch(main) {
|
|
145
|
+
const top = await toplevel(main.root);
|
|
146
|
+
if (top === undefined)
|
|
147
|
+
return undefined;
|
|
148
|
+
return (await checkoutsOf(top))[0]?.branch;
|
|
149
|
+
}
|
|
150
|
+
/** Whether a git worktree's folder has changes git would lose: modified, staged or untracked files. */
|
|
151
|
+
export async function hasChanges(path) {
|
|
152
|
+
return (await git(path, ["status", "--porcelain"])).trim() !== "";
|
|
153
|
+
}
|
|
154
|
+
/** Removes a git worktree's folder and git's record of it, changes and all; its branch stays. */
|
|
155
|
+
export async function removeCheckout(main, path) {
|
|
156
|
+
const top = await toplevel(main.root);
|
|
157
|
+
if (top === undefined)
|
|
158
|
+
return;
|
|
159
|
+
await git(top, ["worktree", "remove", "--force", path]);
|
|
160
|
+
}
|
|
161
|
+
/** The git repository's top folder containing `dir`, real; none outside git. */
|
|
162
|
+
async function toplevel(dir) {
|
|
163
|
+
const out = await exec("git", ["rev-parse", "--show-toplevel"], { cwd: dir }).catch(() => undefined);
|
|
164
|
+
if (out === undefined || out.code !== 0)
|
|
165
|
+
return undefined;
|
|
166
|
+
return realpath(out.stdout.trim());
|
|
167
|
+
}
|
|
168
|
+
/** Every checkout of the repository as git lists it, main's first: its real folder and branch. */
|
|
169
|
+
async function checkoutsOf(top) {
|
|
170
|
+
const out = await git(top, ["worktree", "list", "--porcelain", "-z"]);
|
|
171
|
+
const checkouts = [];
|
|
172
|
+
// Each checkout is a run of fields, ended by an empty one.
|
|
173
|
+
for (const record of out.split("\0\0")) {
|
|
174
|
+
const fields = record.split("\0");
|
|
175
|
+
const path = fields.find((f) => f.startsWith("worktree "))?.slice("worktree ".length);
|
|
176
|
+
const branch = fields.find((f) => f.startsWith("branch "))?.slice("branch refs/heads/".length);
|
|
177
|
+
if (path === undefined)
|
|
178
|
+
continue;
|
|
179
|
+
const real = await realpath(path).catch(() => path);
|
|
180
|
+
checkouts.push({ path: real, ...(branch === undefined ? {} : { branch }) });
|
|
181
|
+
}
|
|
182
|
+
return checkouts;
|
|
183
|
+
}
|
|
184
|
+
function git(cwd, args) {
|
|
185
|
+
return must("git", args, { cwd });
|
|
186
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "basepad",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Local environments for a project, described in one committed config and run natively through mise.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Aysnc-Labs/basepad.git"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"basepad": "dist/launcher.js"
|
|
13
|
+
},
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"os": [
|
|
25
|
+
"darwin",
|
|
26
|
+
"linux"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc -p tsconfig.build.json",
|
|
30
|
+
"check": "biome check . && tsc --noEmit && node scripts/check-feature-map.ts && node --test scripts/check-feature-map.test.ts && node scripts/rocket-svg.ts --check && npm test",
|
|
31
|
+
"lint": "biome check .",
|
|
32
|
+
"prepare": "npm run build",
|
|
33
|
+
"test": "vitest run",
|
|
34
|
+
"changeset": "changeset"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"chalk": "^6.0.1",
|
|
38
|
+
"ink": "^8.0.0",
|
|
39
|
+
"react": "^19.3.1",
|
|
40
|
+
"zod": "^4.6.7"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@biomejs/biome": "2.6.6",
|
|
44
|
+
"@changesets/changelog-github": "^1.0.1",
|
|
45
|
+
"@changesets/cli": "^3.0.3",
|
|
46
|
+
"@types/node": "^24.19.2",
|
|
47
|
+
"@types/react": "^19.3.0",
|
|
48
|
+
"typescript": "^7.0.2",
|
|
49
|
+
"vitest": "^5.1.0"
|
|
50
|
+
}
|
|
51
|
+
}
|