babavoss 0.0.1 → 0.0.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/NOTICE +7 -0
- package/bin/voss.ts +48 -0
- package/gui/babavoss-web.js +234 -0
- package/gui/chunk-5hpp1ypv.js +11710 -0
- package/gui/chunk-9rq662fd.js +8627 -0
- package/gui/chunk-k3cm8j6r.js +433 -0
- package/gui/chunk-pk09y98y.js +50 -0
- package/gui/chunk-smz02qa6.js +186 -0
- package/gui/chunk-wwqypxre.js +49 -0
- package/gui/gui.js +2015 -0
- package/gui/react-compiler-runtime.js +39 -0
- package/gui/react-dom-client.js +21 -0
- package/gui/react-dom.js +44 -0
- package/gui/react-jsx-runtime.js +19 -0
- package/gui/react.js +105 -0
- package/gui/theme.css +3053 -0
- package/index.ts +15 -0
- package/package.json +50 -4
- package/src/baba/check.ts +90 -0
- package/src/baba/config.ts +261 -0
- package/src/baba/find.ts +12 -0
- package/src/baba/init.ts +176 -0
- package/src/baba/node.ts +483 -0
- package/src/baba/project.ts +63 -0
- package/src/baba/registry.ts +35 -0
- package/src/baba/worker.ts +55 -0
- package/src/bench/index.ts +6 -0
- package/src/bench/measure.ts +132 -0
- package/src/bench/scenarios.ts +136 -0
- package/src/build/builder.ts +74 -0
- package/src/build/failure.ts +78 -0
- package/src/build/guard.ts +85 -0
- package/src/build/mdx-register.ts +3 -0
- package/src/build/mdx.ts +38 -0
- package/src/build/project.ts +38 -0
- package/src/build/views.ts +146 -0
- package/src/builder/main.ts +29 -0
- package/src/desktop/bob.ts +76 -0
- package/src/desktop/desktop.css +111 -0
- package/src/desktop/icons.ts +50 -0
- package/src/desktop/index.ts +323 -0
- package/src/desktop/routes.ts +98 -0
- package/src/desktop/view.tsx +673 -0
- package/src/door/core.ts +384 -0
- package/src/ecs/baba.ts +431 -0
- package/src/ecs/codec.ts +334 -0
- package/src/ecs/handles.ts +91 -0
- package/src/ecs/replica.ts +150 -0
- package/src/ecs/runtime.ts +603 -0
- package/src/ecs/scheduler.ts +75 -0
- package/src/ecs/snapshot.ts +102 -0
- package/src/ecs/state.ts +759 -0
- package/src/ecs/system.ts +256 -0
- package/src/ecs/table.ts +420 -0
- package/src/ecs/testbed.ts +97 -0
- package/src/exec/host.ts +177 -0
- package/src/exec/main.ts +98 -0
- package/src/exec/watch.ts +7 -0
- package/src/exec/wire.ts +29 -0
- package/src/generated/build.ts +4 -0
- package/src/gui/css.d.ts +1 -0
- package/src/gui/gui.tsx +245 -0
- package/src/gui/index.ts +51 -0
- package/src/gui/inspector.tsx +47 -0
- package/src/gui/levels.tsx +73 -0
- package/src/gui/promptware.tsx +68 -0
- package/src/gui/runner.tsx +118 -0
- package/src/gui/theme.css +498 -0
- package/src/gui/theme.ts +25 -0
- package/src/gui/wizard.tsx +227 -0
- package/src/guide/add-a-desktop.mdx +100 -0
- package/src/guide/compose-an-interface.mdx +84 -0
- package/src/guide/index.ts +13 -0
- package/src/guide/reach-outside.mdx +112 -0
- package/src/guide/spec-a-system.mdx +93 -0
- package/src/guide/systems-together.mdx +72 -0
- package/src/guide/write-a-system.mdx +183 -0
- package/src/guide/write-promptware.mdx +90 -0
- package/src/http/server.ts +310 -0
- package/src/kernel/build.ts +21 -0
- package/src/kernel/builder.ts +105 -0
- package/src/kernel/children.ts +117 -0
- package/src/kernel/context.ts +90 -0
- package/src/kernel/lock.ts +46 -0
- package/src/kernel/names.ts +14 -0
- package/src/kernel/schema.ts +130 -0
- package/src/kernel/where.ts +12 -0
- package/src/kit/index.ts +232 -0
- package/src/maker/system.ts +213 -0
- package/src/mcp/daemon.ts +61 -0
- package/src/mcp/main.ts +208 -0
- package/src/mcp/rpc.ts +64 -0
- package/src/mcp/tools.ts +125 -0
- package/src/prompt/evals.ts +42 -0
- package/src/prompt/index.ts +242 -0
- package/src/prompt/jsx-dev-runtime.ts +1 -0
- package/src/prompt/jsx-runtime.ts +49 -0
- package/src/prompt/mdx.d.ts +1 -0
- package/src/promptware/compile.ts +183 -0
- package/src/promptware/define.ts +16 -0
- package/src/promptware/disk.ts +72 -0
- package/src/promptware/markdown.d.ts +6 -0
- package/src/promptware/sync.ts +437 -0
- package/src/promptware/system.ts +215 -0
- package/src/runtime/bridge.ts +85 -0
- package/src/runtime/connect.ts +54 -0
- package/src/runtime/env.ts +35 -0
- package/src/runtime/harness.ts +80 -0
- package/src/runtime/main.ts +119 -0
- package/src/runtime/worker.ts +33 -0
- package/src/server/edge.ts +332 -0
- package/src/server/main.ts +45 -0
- package/src/server/messages.ts +97 -0
- package/src/server/protocol.ts +37 -0
- package/src/services/args.ts +45 -0
- package/src/services/exec.ts +69 -0
- package/src/services/fs.ts +139 -0
- package/src/services/http.ts +30 -0
- package/src/services/index.ts +113 -0
- package/src/services/secrets.ts +18 -0
- package/src/shell/address.ts +21 -0
- package/src/shell/args.ts +219 -0
- package/src/shell/client.ts +107 -0
- package/src/shell/codes.ts +26 -0
- package/src/shell/positional.ts +20 -0
- package/src/shell/run.ts +470 -0
- package/src/shell/service.ts +167 -0
- package/src/shell/state.ts +204 -0
- package/src/spec/adapters.ts +72 -0
- package/src/spec/diff.ts +26 -0
- package/src/spec/files.ts +17 -0
- package/src/spec/index.ts +155 -0
- package/src/spec/run.ts +97 -0
- package/src/spec/take.ts +54 -0
- package/src/test/index.ts +8 -0
- package/src/test/prove.ts +56 -0
- package/src/test/records.ts +23 -0
- package/src/test/specs.ts +56 -0
- package/src/test/steps.ts +100 -0
- package/src/test/voss-dir.ts +17 -0
- package/src/transport/messages.ts +110 -0
- package/src/transport/transport.ts +62 -0
- package/src/wall/probe.ts +67 -0
- package/src/wall/profile.ts +103 -0
- package/src/wall/spawn.ts +59 -0
- package/src/web/app.tsx +53 -0
- package/src/web/core.tsx +140 -0
- package/src/web/form.ts +155 -0
- package/src/web/hooks.ts +135 -0
- package/src/web/index.tsx +17 -0
- package/src/web/list.ts +19 -0
- package/src/web/maker.tsx +766 -0
- package/src/web/objects.tsx +213 -0
- package/src/web/socket.ts +84 -0
- package/src/web/state.tsx +69 -0
- package/src/web/store.ts +221 -0
- package/src/web/ui.tsx +135 -0
- package/README.md +0 -5
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
// The HTTP edge, kernel side: the projects' Nodes, the GUI's build, the
|
|
2
|
+
// page token and the address. The registry says what runs; a refresh
|
|
3
|
+
// re-reads it. The server itself is a child process (src/server/) that holds
|
|
4
|
+
// no files; this side fills its memory and answers what it forwards.
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import { mkdir, writeFile, chmod } from "node:fs/promises";
|
|
7
|
+
import { dirname } from "node:path";
|
|
8
|
+
import { readFileSync, unlinkSync } from "node:fs";
|
|
9
|
+
import { openProject, exists } from "../baba/project.ts";
|
|
10
|
+
import { init } from "../baba/init.ts";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { readdir } from "node:fs/promises";
|
|
13
|
+
import { Node, NodeError } from "../baba/node.ts";
|
|
14
|
+
import type { BuildOutput } from "../build/builder.ts";
|
|
15
|
+
import type { Registry } from "../baba/registry.ts";
|
|
16
|
+
import { page, buildGui } from "../gui/index.ts";
|
|
17
|
+
import type { Transport } from "../transport/transport.ts";
|
|
18
|
+
import type { ViewIndex, Asset, Failure, FromServer, ProjectEntry, ToServer, Refreshed, Browsed } from "../server/messages.ts";
|
|
19
|
+
import { connectFor, builderFor, serverSpawn } from "../kernel/children.ts";
|
|
20
|
+
import { dev, framework } from "../kernel/where.ts";
|
|
21
|
+
import { takeLock, LockError } from "../kernel/lock.ts";
|
|
22
|
+
import { thisBuild, buildOf, buildName, tellApart, type Build } from "../kernel/build.ts";
|
|
23
|
+
import type { Voss } from "../server/messages.ts";
|
|
24
|
+
import { stat } from "node:fs/promises";
|
|
25
|
+
|
|
26
|
+
export type ServerTransport = Transport<ToServer, FromServer>;
|
|
27
|
+
|
|
28
|
+
export interface ServeOptions {
|
|
29
|
+
/** Starts the server child from its command line; by default behind the wall with loopback. */
|
|
30
|
+
spawn?: (cmd: string[]) => ServerTransport;
|
|
31
|
+
/** Runs the projects in process workers instead of walled runtimes: for tests. */
|
|
32
|
+
inProcess?: boolean;
|
|
33
|
+
/** false: no DIR/address and no DIR/lock, so the CLI does not take this server for the root's daemon: `serve` for one project. */
|
|
34
|
+
address?: boolean;
|
|
35
|
+
/** Under the service (launchd, systemd): a clean exit is a restart. */
|
|
36
|
+
kept?: boolean;
|
|
37
|
+
/** The package.json to watch for a newer install: the framework's own, installed and the root's daemon; false for none; given, for tests. */
|
|
38
|
+
binary?: { path: string; every?: number; version?: (path: string) => Promise<Build | null> } | false;
|
|
39
|
+
/** What a restart does once answered: by default stops everything and exits, re-running itself when not kept. */
|
|
40
|
+
restart?: () => Promise<void> | void;
|
|
41
|
+
/** What a baba made from the picker depends on, as `voss init` would write it; absent, the picker can only remember a baba that exists. */
|
|
42
|
+
init?: { framework: string; extra?: Record<string, string>; install?: boolean };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface Served { hostname: string; port: number; token: string; nodes: Map<string, Node>; voss: Voss; stop(): Promise<void> }
|
|
46
|
+
|
|
47
|
+
/** The command that starts the GUI server: Bun on bin/voss.ts in the server role. */
|
|
48
|
+
export function serverCommand(): string[] {
|
|
49
|
+
return [process.execPath, join(framework, "bin", "voss.ts"), "--server"];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** $VOSS_TOKEN, else 32 random bytes written to DIR/http-token, readable by the user alone. */
|
|
53
|
+
async function pageToken(dir: string, log: (l: string) => void): Promise<string> {
|
|
54
|
+
if (process.env.VOSS_TOKEN) return process.env.VOSS_TOKEN;
|
|
55
|
+
const token = Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString("hex");
|
|
56
|
+
const path = join(dir, "http-token");
|
|
57
|
+
await mkdir(dir, { recursive: true });
|
|
58
|
+
await writeFile(path, token + "\n", { mode: 0o600 });
|
|
59
|
+
await chmod(path, 0o600);
|
|
60
|
+
log(`token in ${path}`);
|
|
61
|
+
return token;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const attr = (s: string) => s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
65
|
+
const withToken = (html: string, token: string) => html.replace("<title>", `<meta name="voss-token" content="${attr(token)}"><title>`);
|
|
66
|
+
|
|
67
|
+
async function readTree(dir: string): Promise<Record<string, Asset>> {
|
|
68
|
+
const out: Record<string, Asset> = {};
|
|
69
|
+
for await (const path of new Bun.Glob("**/*").scan({ cwd: dir, onlyFiles: true })) {
|
|
70
|
+
const f = Bun.file(join(dir, path));
|
|
71
|
+
out[path] = { mime: f.type, bytes: new Uint8Array(await f.arrayBuffer()) };
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The entry to import and the stylesheets to attach: the entry's own, and any chunk's, in build order. */
|
|
77
|
+
function indexOf(outputs: BuildOutput[]): ViewIndex {
|
|
78
|
+
return {
|
|
79
|
+
entry: outputs.find((o) => o.kind === "entry-point")!.path,
|
|
80
|
+
css: outputs.filter((o) => o.path.endsWith(".css")).map((o) => o.path),
|
|
81
|
+
paths: outputs.map((o) => o.path),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** A directory and the directories under it, the hidden and node_modules left out, each marked when it holds a baba or is already remembered. */
|
|
86
|
+
async function browse(path: string, known: string[]): Promise<Browsed> {
|
|
87
|
+
path = resolve(path);
|
|
88
|
+
const remembered = new Set(known.map((d) => resolve(d)));
|
|
89
|
+
const hasBaba = (dir: string) => exists(join(dir, ".baba", "package.json"));
|
|
90
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
91
|
+
const dirs: Browsed["dirs"] = [];
|
|
92
|
+
for (const e of entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").sort((a, b) => a.name.localeCompare(b.name))) {
|
|
93
|
+
const full = join(path, e.name);
|
|
94
|
+
dirs.push({ name: e.name, path: full, baba: await hasBaba(full), known: remembered.has(full) });
|
|
95
|
+
}
|
|
96
|
+
const parent = dirname(path);
|
|
97
|
+
return { path, home: homedir(), parent: parent === path ? null : parent, baba: await hasBaba(path), known: remembered.has(path), dirs };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function failure(err: unknown): Failure {
|
|
101
|
+
return { ok: false, code: err instanceof NodeError ? err.code : "failed", message: err instanceof Error ? err.message : String(err) };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Where a root's daemon says it listens: DIR/address, written on listen and removed on stop. */
|
|
105
|
+
export const addressPath = (rootDir: string) => join(rootDir, "address");
|
|
106
|
+
export interface Address { hostname: string; port: number; pid: number }
|
|
107
|
+
/** DIR/lock: the daemon's, beside its address and token. */
|
|
108
|
+
export const lockPath = (dir: string) => join(dir, "lock");
|
|
109
|
+
|
|
110
|
+
export async function serve(reg: Registry, hostname: string, port: number, log = (l: string) => console.error(`voss: ${l}`), options: ServeOptions = {}): Promise<Served> {
|
|
111
|
+
const nodes = new Map<string, Node>();
|
|
112
|
+
// The server child, once it runs: every round's frame and every generation go to it, for the windows.
|
|
113
|
+
let child: ServerTransport | null = null;
|
|
114
|
+
const hook = (n: Node) => {
|
|
115
|
+
n.onRound = (frame, round) => child?.send({ type: "round", name: n.name, round, frame });
|
|
116
|
+
n.onGeneration = (gen) => child?.send({ type: "generation", name: n.name, gen });
|
|
117
|
+
};
|
|
118
|
+
// One daemon per root: a second one would overwrite the address and the token and run every project twice.
|
|
119
|
+
let lock: { release(): void } | null = null;
|
|
120
|
+
if (options.address !== false) {
|
|
121
|
+
await mkdir(reg.dir, { recursive: true });
|
|
122
|
+
try { lock = takeLock(lockPath(reg.dir), { role: "daemon" }, (h) => `a voss daemon already runs this root, pid ${h.pid}`); }
|
|
123
|
+
catch (err) { if (err instanceof LockError) throw new NodeError("busy", err.message); throw err; }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// The registry is the list of what runs: a refresh opens what it gained and retires what it lost, one at a time.
|
|
127
|
+
let refreshing: Promise<unknown> = Promise.resolve();
|
|
128
|
+
const reread = async (): Promise<Refreshed> => {
|
|
129
|
+
const dirs = (await reg.list()).map((d) => resolve(d));
|
|
130
|
+
const errors: Refreshed["errors"] = [];
|
|
131
|
+
for (const [name, n] of nodes) {
|
|
132
|
+
if (dirs.includes(n.project.dir)) continue;
|
|
133
|
+
nodes.delete(name);
|
|
134
|
+
await n.stop();
|
|
135
|
+
log(`${name}: stopped`);
|
|
136
|
+
}
|
|
137
|
+
for (const dir of dirs) {
|
|
138
|
+
if ([...nodes.values()].some((n) => n.project.dir === dir)) continue;
|
|
139
|
+
try {
|
|
140
|
+
if (voss.stale) throw new Error(`not opened: voss ${buildName(voss.installed!)} is installed and this one is stale; restart to update`);
|
|
141
|
+
const p = await openProject(dir);
|
|
142
|
+
const other = nodes.get(p.name);
|
|
143
|
+
if (other) throw new Error(`name ${p.name} is already taken by ${other.project.dir}`);
|
|
144
|
+
const builder = options.inProcess ? undefined : builderFor(p, reg.dir, log);
|
|
145
|
+
const n = options.inProcess ? new Node(p, log) : new Node(p, log, connectFor(p, reg.dir, builder!), undefined, builder);
|
|
146
|
+
hook(n);
|
|
147
|
+
await n.start();
|
|
148
|
+
nodes.set(p.name, n);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
151
|
+
log(`${dir}: ${error}`);
|
|
152
|
+
errors.push({ dir, error });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return { projects: listing(), errors };
|
|
156
|
+
};
|
|
157
|
+
const refresh = (): Promise<Refreshed> => { const r = refreshing.then(reread); refreshing = r.catch(() => {}); return r; };
|
|
158
|
+
const stopNodes = async () => { await refreshing; await Promise.all([...nodes.values()].map((n) => n.stop())); };
|
|
159
|
+
|
|
160
|
+
// Which voss this is. A newer package installed over it makes this one stale: it serves what it has,
|
|
161
|
+
// loads no change and opens no project, until the restart brings the new one up.
|
|
162
|
+
const voss: Voss = { ...thisBuild, pid: process.pid, kept: options.kept ?? false, stale: false, installed: null };
|
|
163
|
+
const stale = (b: Build) => {
|
|
164
|
+
voss.stale = true;
|
|
165
|
+
voss.installed = { version: b.version, sha: b.sha, at: b.at };
|
|
166
|
+
const [installed, running] = tellApart(b, thisBuild);
|
|
167
|
+
const why = `voss ${installed} is installed and this one is ${running}; restart to update`;
|
|
168
|
+
for (const n of nodes.values()) n.freeze(why);
|
|
169
|
+
log(why);
|
|
170
|
+
child?.send({ type: "voss", voss });
|
|
171
|
+
};
|
|
172
|
+
const watched = options.binary === false ? null : options.binary ?? (!dev && options.address !== false ? { path: join(framework, "package.json") } : null);
|
|
173
|
+
let watcher: ReturnType<typeof setInterval> | null = null;
|
|
174
|
+
const watchBinary = async () => {
|
|
175
|
+
if (!watched) return;
|
|
176
|
+
const first = await stat(watched.path).catch(() => null);
|
|
177
|
+
if (!first) return;
|
|
178
|
+
const read = watched.version ?? buildOf;
|
|
179
|
+
let asking = false;
|
|
180
|
+
watcher = setInterval(() => {
|
|
181
|
+
if (voss.stale || asking) return;
|
|
182
|
+
asking = true;
|
|
183
|
+
void (async () => {
|
|
184
|
+
const now = await stat(watched.path).catch(() => null); // gone for a moment: an install under way
|
|
185
|
+
if (now && now.ino !== first.ino) {
|
|
186
|
+
const b = await read(watched.path); // not yet a package that answers: ask again next time
|
|
187
|
+
if (b && (b.version !== thisBuild.version || b.sha !== thisBuild.sha || b.at !== thisBuild.at)) stale(b);
|
|
188
|
+
}
|
|
189
|
+
})().finally(() => { asking = false; });
|
|
190
|
+
}, watched.every ?? 5_000);
|
|
191
|
+
watcher.unref();
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// The GUI, packed with a release under the framework's gui/; built here from a checkout, or a package without one.
|
|
195
|
+
// Either way it goes into memory for the child, which reads no files.
|
|
196
|
+
const packed = join(framework, "gui");
|
|
197
|
+
let built = !dev && await exists(join(packed, "gui.js")) ? packed : null;
|
|
198
|
+
if (!built) { built = join(reg.dir, "gui"); await buildGui(built); }
|
|
199
|
+
const assets = await readTree(built);
|
|
200
|
+
const token = await pageToken(reg.dir, log);
|
|
201
|
+
function listing(): ProjectEntry[] {
|
|
202
|
+
const all = [...nodes.values()];
|
|
203
|
+
const parentOf = (n: Node) => all
|
|
204
|
+
.filter((o) => o !== n && n.project.dir.startsWith(o.project.dir + "/"))
|
|
205
|
+
.sort((a, b) => b.project.dir.length - a.project.dir.length)[0]?.name ?? null;
|
|
206
|
+
return all.map((n) => ({ name: n.name, title: n.project.title, dir: n.project.dir, parent: parentOf(n) }));
|
|
207
|
+
}
|
|
208
|
+
const pagesOf = () => {
|
|
209
|
+
const pages: Record<string, string> = { "": withToken(page(null), token) };
|
|
210
|
+
for (const name of nodes.keys()) pages[name] = withToken(page(name), token);
|
|
211
|
+
return pages;
|
|
212
|
+
};
|
|
213
|
+
await refresh();
|
|
214
|
+
|
|
215
|
+
child = (options.spawn ?? serverSpawn(reg.dir))(serverCommand());
|
|
216
|
+
|
|
217
|
+
// A restart: the answer goes out, then everything stops and the process ends. Kept by the service, it is started again
|
|
218
|
+
// as the installed voss; otherwise it runs the same command again itself, once the lock is free.
|
|
219
|
+
let shutdown: () => Promise<void> = async () => {};
|
|
220
|
+
const restart = options.restart ?? (async () => {
|
|
221
|
+
log(`restarting${voss.installed ? ` as voss ${buildName(voss.installed)}` : ""}`);
|
|
222
|
+
await shutdown();
|
|
223
|
+
if (!voss.kept) Bun.spawn([process.execPath, ...process.argv.slice(1)], { stdio: ["ignore", "inherit", "inherit"], env: process.env }).unref();
|
|
224
|
+
process.exit(0);
|
|
225
|
+
});
|
|
226
|
+
async function answer(m: FromServer): Promise<ToServer | null> {
|
|
227
|
+
if (m.type === "ready" || m.type === "listening" || m.type === "failed") return null;
|
|
228
|
+
if (m.type === "restart") {
|
|
229
|
+
setTimeout(() => void restart(), 300);
|
|
230
|
+
return { type: "restart-answer", id: m.id, ok: true, value: { restarting: true, kept: voss.kept } };
|
|
231
|
+
}
|
|
232
|
+
if (m.type === "refresh") {
|
|
233
|
+
try {
|
|
234
|
+
const value = await refresh();
|
|
235
|
+
child?.send({ type: "projects", projects: value.projects, pages: pagesOf() });
|
|
236
|
+
return { type: "refresh-answer", id: m.id, ok: true, value };
|
|
237
|
+
} catch (err) { return { type: "refresh-answer", id: m.id, ...failure(err) }; }
|
|
238
|
+
}
|
|
239
|
+
if (m.type === "browse") {
|
|
240
|
+
try { return { type: "browse-answer", id: m.id, ok: true, value: await browse(m.path || homedir(), await reg.list()) }; }
|
|
241
|
+
catch (err) { return { type: "browse-answer", id: m.id, ...failure(err) }; }
|
|
242
|
+
}
|
|
243
|
+
if (m.type === "add") {
|
|
244
|
+
try {
|
|
245
|
+
const dir = resolve(m.dir);
|
|
246
|
+
if (m.init) {
|
|
247
|
+
if (!options.init) throw new NodeError("failed", "this voss cannot make a baba; `voss init` does");
|
|
248
|
+
await init(dir, { name: m.name, title: m.title, framework: options.init.framework, extra: options.init.extra, install: options.init.install });
|
|
249
|
+
}
|
|
250
|
+
const p = await openProject(dir);
|
|
251
|
+
await reg.add(p.dir);
|
|
252
|
+
const value = await refresh();
|
|
253
|
+
child?.send({ type: "projects", projects: value.projects, pages: pagesOf() });
|
|
254
|
+
const own = value.errors.find((e) => e.dir === p.dir);
|
|
255
|
+
if (own) throw new NodeError("failed", own.error);
|
|
256
|
+
return { type: "add-answer", id: m.id, ok: true, value: { ...value, name: p.name, dir: p.dir } };
|
|
257
|
+
} catch (err) { return { type: "add-answer", id: m.id, ...failure(err) }; }
|
|
258
|
+
}
|
|
259
|
+
const n = nodes.get(m.name);
|
|
260
|
+
try {
|
|
261
|
+
if (!n) throw new NodeError("nokey", `no project ${m.name}`);
|
|
262
|
+
if (m.type === "describe") {
|
|
263
|
+
if (m.since) await n.waitFor(m.since, 25_000);
|
|
264
|
+
return { type: "describe-answer", id: m.id, ok: true, value: n.describe() };
|
|
265
|
+
}
|
|
266
|
+
if (m.type === "view") {
|
|
267
|
+
const b = await n.bundle();
|
|
268
|
+
if (m.path === undefined) return { type: "view-answer", id: m.id, ok: true, gen: b.gen, index: indexOf(b.outputs) };
|
|
269
|
+
const o = b.outputs.find((x) => x.path === m.path);
|
|
270
|
+
if (!o) throw new NodeError("notfound", `the view has no ${m.path}`);
|
|
271
|
+
return { type: "view-answer", id: m.id, ok: true, gen: b.gen, file: { type: o.type, bytes: o.bytes } };
|
|
272
|
+
}
|
|
273
|
+
let value: unknown, round: number | undefined;
|
|
274
|
+
if (m.kind === "state") value = m.parts ? await n.parts(m.parts) : m.frame ? await n.frame(m.since, m.wait !== false) : await n.state(m.since);
|
|
275
|
+
else if (m.kind === "raw") ({ value, round } = await n.rawIn(m.raw));
|
|
276
|
+
else if (m.kind === "fire") ({ value, round } = await n.fireIn(m.target, m.args, m.coerce, m.key));
|
|
277
|
+
else value = await n.read(m.target, m.args, m.coerce);
|
|
278
|
+
return { type: "call-answer", id: m.id, ok: true, value, ...(round !== undefined ? { round } : {}) };
|
|
279
|
+
} catch (err) {
|
|
280
|
+
return { type: `${m.type}-answer`, id: m.id, ...failure(err) };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return new Promise<Served>((resolve, reject) => {
|
|
285
|
+
let up = false;
|
|
286
|
+
child.onClose((reason) => {
|
|
287
|
+
if (!up) { lock?.release(); void stopNodes(); reject(new Error(`the GUI server ${reason}`)); return; }
|
|
288
|
+
log(`the GUI server ${reason}`);
|
|
289
|
+
});
|
|
290
|
+
child.onMessage((m) => {
|
|
291
|
+
if (m.type === "ready") {
|
|
292
|
+
child.send({ type: "serve", hostname, port, token, voss, projects: listing(), home: homedir(), pages: pagesOf(), assets });
|
|
293
|
+
} else if (m.type === "listening") {
|
|
294
|
+
up = true;
|
|
295
|
+
// The address goes next to the token, for the CLI to find the daemon by; it goes when the daemon does.
|
|
296
|
+
const path = addressPath(reg.dir);
|
|
297
|
+
const text = JSON.stringify({ hostname: m.hostname, port: m.port, pid: process.pid } satisfies Address) + "\n";
|
|
298
|
+
const forget = () => { try { if (readFileSync(path, "utf8") === text) unlinkSync(path); } catch { /* already gone */ } };
|
|
299
|
+
const written = options.address === false ? Promise.resolve() : writeFile(path, text, { mode: 0o600 }).then(() => { process.once("exit", forget); });
|
|
300
|
+
shutdown = async () => { if (watcher) clearInterval(watcher); process.off("exit", forget); forget(); child.close(); await stopNodes(); lock?.release(); };
|
|
301
|
+
written.then(() => { void watchBinary(); resolve({ hostname: m.hostname, port: m.port, token, nodes, voss, stop: shutdown }); }, reject);
|
|
302
|
+
} else if (m.type === "failed") {
|
|
303
|
+
child.close(); void stopNodes(); lock?.release();
|
|
304
|
+
reject(new Error(`the GUI server could not listen on ${hostname}:${port}: ${m.error}`));
|
|
305
|
+
} else {
|
|
306
|
+
answer(m).then((a) => a && child.send(a));
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Which voss this is: the package version, the commit and time a release
|
|
2
|
+
// was packed from, and whether it runs from a developer checkout. What
|
|
3
|
+
// `voss --version` prints, what the daemon reports, what a newer install
|
|
4
|
+
// is compared by.
|
|
5
|
+
import { build } from "../generated/build.ts";
|
|
6
|
+
import { dev } from "./where.ts";
|
|
7
|
+
import pkg from "../../package.json";
|
|
8
|
+
|
|
9
|
+
export interface Build { version: string; sha: string; at: string; dev: boolean }
|
|
10
|
+
|
|
11
|
+
export const thisBuild: Build = { version: (pkg as { version: string }).version, sha: build.sha, at: build.at, dev };
|
|
12
|
+
|
|
13
|
+
export { buildName, tellApart } from "./names.ts";
|
|
14
|
+
|
|
15
|
+
/** The build of a package on disk, from its package.json; null when there is none. */
|
|
16
|
+
export async function buildOf(packageJson: string): Promise<Build | null> {
|
|
17
|
+
try {
|
|
18
|
+
const b = JSON.parse(await Bun.file(packageJson).text()) as Partial<Build>;
|
|
19
|
+
return typeof b.version === "string" ? { version: b.version, sha: "", at: "", dev: false } : null;
|
|
20
|
+
} catch { return null; }
|
|
21
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// The kernel's builder for a project: a walled process started when a
|
|
2
|
+
// build is asked for, kept while builds keep coming, gone after a quiet
|
|
3
|
+
// spell. A build that outruns its time is killed, and whoever waited hears
|
|
4
|
+
// a failure; the old bundle stays, as with any failed build.
|
|
5
|
+
import type { Transport } from "../transport/transport.ts";
|
|
6
|
+
import { PROTOCOL, type ToBuilder, type FromBuilder, type Built } from "../transport/messages.ts";
|
|
7
|
+
import { ViewFailed, type ViewFailure } from "../build/failure.ts";
|
|
8
|
+
import { validateOutputs, entryOf, type Builder, type BuildOutput, type ViewSpec } from "../build/builder.ts";
|
|
9
|
+
import type { Project } from "../baba/project.ts";
|
|
10
|
+
|
|
11
|
+
export interface BuilderOptions { idleMs?: number; timeoutMs?: number }
|
|
12
|
+
|
|
13
|
+
interface Pending { resolve: (b: Extract<Built, { ok: true }>) => void; reject: (e: Error) => void }
|
|
14
|
+
|
|
15
|
+
export class ProcessBuilder implements Builder {
|
|
16
|
+
private t: Transport<ToBuilder, FromBuilder> | null = null;
|
|
17
|
+
private pending = new Map<number, Pending>();
|
|
18
|
+
private nextId = 1;
|
|
19
|
+
private idle: ReturnType<typeof setTimeout> | null = null;
|
|
20
|
+
private timer: ReturnType<typeof setTimeout> | null = null;
|
|
21
|
+
private readonly idleMs: number;
|
|
22
|
+
private readonly timeoutMs: number;
|
|
23
|
+
|
|
24
|
+
constructor(private spawn: () => Transport<ToBuilder, FromBuilder>, private log: (line: string) => void = () => {}, o: BuilderOptions = {}) {
|
|
25
|
+
this.idleMs = o.idleMs ?? 30_000;
|
|
26
|
+
this.timeoutMs = o.timeoutMs ?? 60_000;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async project(p: Project): Promise<string> {
|
|
30
|
+
const b = await this.ask({ type: "build-project", id: 0, project: p });
|
|
31
|
+
return entryOf(b.outputs);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async view(spec: ViewSpec): Promise<BuildOutput[]> {
|
|
35
|
+
const b = await this.ask({ type: "build-view", id: 0, spec });
|
|
36
|
+
return validateOutputs(b.outputs);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
stop(): void {
|
|
40
|
+
this.fail("the builder was stopped");
|
|
41
|
+
if (this.idle) { clearTimeout(this.idle); this.idle = null; }
|
|
42
|
+
this.t?.close();
|
|
43
|
+
this.t = null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private transport(): Transport<ToBuilder, FromBuilder> {
|
|
47
|
+
if (this.t) return this.t;
|
|
48
|
+
const t = this.spawn();
|
|
49
|
+
t.onMessage((m) => {
|
|
50
|
+
if (m.type === "hello") {
|
|
51
|
+
if (m.protocol !== PROTOCOL) { this.log(`the builder speaks protocol ${m.protocol}, this kernel ${PROTOCOL}`); this.stop(); }
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (m.type !== "built") return;
|
|
55
|
+
const p = this.pending.get(m.id);
|
|
56
|
+
if (!p) return;
|
|
57
|
+
this.pending.delete(m.id);
|
|
58
|
+
this.armTimer();
|
|
59
|
+
if (m.ok) p.resolve(m); else p.reject(new ViewFailed(m.failure));
|
|
60
|
+
});
|
|
61
|
+
t.onClose((reason) => {
|
|
62
|
+
if (this.t === t) this.t = null;
|
|
63
|
+
this.fail(`the builder exited: ${reason}`);
|
|
64
|
+
});
|
|
65
|
+
t.send({ type: "hello", protocol: PROTOCOL, role: "kernel" });
|
|
66
|
+
this.t = t;
|
|
67
|
+
return t;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private ask(m: Exclude<ToBuilder, { type: "hello" }>): Promise<Extract<Built, { ok: true }>> {
|
|
71
|
+
const id = this.nextId++;
|
|
72
|
+
if (this.idle) { clearTimeout(this.idle); this.idle = null; }
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
this.pending.set(id, { resolve, reject });
|
|
75
|
+
this.transport().send({ ...m, id });
|
|
76
|
+
this.armTimer();
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** While builds are pending the clock runs for the oldest; with none, the idle clock runs. */
|
|
81
|
+
private armTimer(): void {
|
|
82
|
+
if (this.timer) { clearTimeout(this.timer); this.timer = null; }
|
|
83
|
+
if (this.pending.size > 0) {
|
|
84
|
+
this.timer = setTimeout(() => this.kill(`the build took longer than ${this.timeoutMs / 1000} s and was stopped`), this.timeoutMs);
|
|
85
|
+
} else if (this.t) {
|
|
86
|
+
this.idle = setTimeout(() => { this.t?.close(); this.t = null; }, this.idleMs);
|
|
87
|
+
this.idle.unref?.();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private kill(why: string): void {
|
|
92
|
+
this.log(why);
|
|
93
|
+
const t = this.t;
|
|
94
|
+
this.t = null;
|
|
95
|
+
t?.close();
|
|
96
|
+
this.fail(why);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private fail(why: string): void {
|
|
100
|
+
if (this.timer) { clearTimeout(this.timer); this.timer = null; }
|
|
101
|
+
const failure: ViewFailure = { stage: "build", message: why };
|
|
102
|
+
for (const p of this.pending.values()) p.reject(new ViewFailed(failure));
|
|
103
|
+
this.pending.clear();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// How the kernel starts its children: a runtime per project and the GUI
|
|
2
|
+
// server, each voss in a role, behind the wall. The child reads the
|
|
3
|
+
// framework's tree and its dependencies, nothing else of the machine.
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { readdirSync, lstatSync, realpathSync, readlinkSync, readFileSync } from "node:fs";
|
|
6
|
+
import { resolve } from "node:path";
|
|
7
|
+
import { framework } from "./where.ts";
|
|
8
|
+
import { seatbeltProfile } from "../wall/profile.ts";
|
|
9
|
+
import { spawnWalled } from "../wall/spawn.ts";
|
|
10
|
+
import { runtimeConnect, roleCommand, type Spawn } from "../runtime/connect.ts";
|
|
11
|
+
import { ProcessBuilder } from "./builder.ts";
|
|
12
|
+
import type { Builder } from "../build/builder.ts";
|
|
13
|
+
import type { ToBuilder, FromBuilder } from "../transport/messages.ts";
|
|
14
|
+
import type { Project } from "../baba/project.ts";
|
|
15
|
+
import type { Connect } from "../baba/node.ts";
|
|
16
|
+
import type { Transport } from "../transport/transport.ts";
|
|
17
|
+
import type { ServerTransport } from "../http/server.ts";
|
|
18
|
+
import type { ToServer, FromServer } from "../server/messages.ts";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Where the framework's dependencies really are: its own node_modules, or the
|
|
22
|
+
* node_modules a workspace installs them into above it, `.bun/` and all, when the framework is a member.
|
|
23
|
+
*/
|
|
24
|
+
function dependencies(): string[] {
|
|
25
|
+
const out = new Set<string>();
|
|
26
|
+
const pkg = JSON.parse(readFileSync(join(framework, "package.json"), "utf8")) as { dependencies?: Record<string, string> };
|
|
27
|
+
for (const name of Object.keys(pkg.dependencies ?? {})) {
|
|
28
|
+
const target = real(join(framework, "node_modules", name));
|
|
29
|
+
if (target === framework || target.startsWith(framework + "/")) continue;
|
|
30
|
+
const at = target.indexOf("/node_modules/");
|
|
31
|
+
if (at >= 0) out.add(target.slice(0, at + "/node_modules".length));
|
|
32
|
+
}
|
|
33
|
+
return [...out];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** What a child may read besides Bun: the framework's tree and its dependencies. */
|
|
37
|
+
let sourceTree: string[] | null = null;
|
|
38
|
+
const readable = () => (sourceTree ??= [framework, ...dependencies()]);
|
|
39
|
+
|
|
40
|
+
/** A project's runtime, walled: no files, no network, no programs. */
|
|
41
|
+
export function runtimeSpawn(rootDir: string): Spawn {
|
|
42
|
+
return (cmd, o) => spawnWalled(cmd, {
|
|
43
|
+
profile: seatbeltProfile({ binary: cmd[0]!, readable: readable() }),
|
|
44
|
+
env: o.env, dir: join(rootDir, "wall"), stdout: o.stdout, stderr: o.stderr,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The Node's connect for a project: a walled runtime process, its bundle from `builder`. */
|
|
49
|
+
export function connectFor(project: Project, rootDir: string, builder: Builder): Connect {
|
|
50
|
+
return runtimeConnect(project, runtimeSpawn(rootDir), builder);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const real = (p: string) => { try { return realpathSync(p); } catch { return p; } };
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Where a project's node_modules points outside the project: a `link:`
|
|
57
|
+
* dependency or a workspace member elsewhere. The wall grants by real path,
|
|
58
|
+
* so each target is granted on its own; the framework already is.
|
|
59
|
+
* `links` are the symbolic links passed through on the way, outside the
|
|
60
|
+
* project: a global install's link to a package, say. The wall must read
|
|
61
|
+
* each as the link it is, or the path cannot be followed.
|
|
62
|
+
*/
|
|
63
|
+
export function linkTargets(baba: string): { targets: string[]; links: string[] } {
|
|
64
|
+
const home = real(baba);
|
|
65
|
+
const inside = (p: string) => p === home || p.startsWith(home + "/");
|
|
66
|
+
const out = new Set<string>();
|
|
67
|
+
const links = new Set<string>();
|
|
68
|
+
const dir = join(baba, "node_modules");
|
|
69
|
+
let names: string[] = [];
|
|
70
|
+
try { names = readdirSync(dir); } catch { return { targets: [], links: [] }; }
|
|
71
|
+
const entries: string[] = [];
|
|
72
|
+
for (const n of names) {
|
|
73
|
+
if (n.startsWith(".")) continue;
|
|
74
|
+
if (n.startsWith("@")) { try { for (const s of readdirSync(join(dir, n))) entries.push(join(dir, n, s)); } catch {} }
|
|
75
|
+
else entries.push(join(dir, n));
|
|
76
|
+
}
|
|
77
|
+
for (const e of entries) {
|
|
78
|
+
try { if (!lstatSync(e).isSymbolicLink()) continue; } catch { continue; }
|
|
79
|
+
// Every link on the chain after the first, which is inside the project.
|
|
80
|
+
for (let p = e, hops = 0; hops < 32; hops++) {
|
|
81
|
+
let next: string;
|
|
82
|
+
try { if (!lstatSync(p).isSymbolicLink()) break; next = resolve(dirname(p), readlinkSync(p)); } catch { break; }
|
|
83
|
+
if (p !== e && !inside(p)) links.add(p);
|
|
84
|
+
p = next;
|
|
85
|
+
}
|
|
86
|
+
const target = real(e);
|
|
87
|
+
if (!inside(target) && !readable().some((r) => target === r || target.startsWith(r + "/"))) out.add(target);
|
|
88
|
+
}
|
|
89
|
+
return { targets: [...out], links: [...links] };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A project's builder, walled: it may read its project's `.baba` and what
|
|
94
|
+
* the project links to, starts there, and may read, write or reach nothing
|
|
95
|
+
* else. The kernel's own state under `rootDir` stays out even when it lies
|
|
96
|
+
* inside a granted tree, as `.voss-dev` does in a checkout.
|
|
97
|
+
*/
|
|
98
|
+
export function builderFor(project: Project, rootDir: string, log: (line: string) => void = () => {}): Builder {
|
|
99
|
+
const spawn = (): Transport<ToBuilder, FromBuilder> => {
|
|
100
|
+
const { targets, links } = linkTargets(project.baba);
|
|
101
|
+
for (const t of targets) log(`${project.name}: the builder may read ${t}, linked from node_modules`);
|
|
102
|
+
const cmd = roleCommand("--builder");
|
|
103
|
+
return spawnWalled<ToBuilder, FromBuilder>(cmd, {
|
|
104
|
+
profile: seatbeltProfile({ binary: cmd[0]!, readable: [...readable(), project.baba, ...targets], links, unreadable: [rootDir] }),
|
|
105
|
+
env: {}, dir: join(rootDir, "wall"), cwd: project.baba, stdout: "inherit", stderr: "inherit",
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
return new ProcessBuilder(spawn, (l) => log(`${project.name}: ${l}`));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The GUI server, walled with loopback only. */
|
|
112
|
+
export function serverSpawn(rootDir: string): (cmd: string[]) => ServerTransport {
|
|
113
|
+
return (cmd) => spawnWalled<ToServer, FromServer>(cmd, {
|
|
114
|
+
profile: seatbeltProfile({ binary: cmd[0]!, loopback: true, readable: readable() }),
|
|
115
|
+
env: {}, dir: join(rootDir, "wall"), stdout: "inherit", stderr: "inherit",
|
|
116
|
+
});
|
|
117
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// The context: an effect's whole reach beyond computation. Every part is a
|
|
2
|
+
// message to the kernel, which serves it and logs every reach beyond the
|
|
3
|
+
// project, attributed to the system, the effect and the job that asked.
|
|
4
|
+
|
|
5
|
+
export interface Context {
|
|
6
|
+
/** The project directory, as a path the kernel understands. */
|
|
7
|
+
project: string;
|
|
8
|
+
/** Files, scoped to the project: a path is relative to it, or absolute inside it. */
|
|
9
|
+
fs: Fs;
|
|
10
|
+
/** The network, through the kernel: every request observed and attributed. */
|
|
11
|
+
http: Http;
|
|
12
|
+
/** Programs on the machine, owned by the job and supervised by the kernel. */
|
|
13
|
+
exec: Exec;
|
|
14
|
+
/** Secrets from the keychain, through the kernel. */
|
|
15
|
+
secrets: Secrets;
|
|
16
|
+
/** The environment: the project's .env files. */
|
|
17
|
+
env: Readonly<Record<string, string>>;
|
|
18
|
+
/** Aborted when the job settles or is cancelled: its entity despawned, its request removed, the baba retired. */
|
|
19
|
+
signal: AbortSignal;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface Fs {
|
|
23
|
+
read(path: string): Promise<string>;
|
|
24
|
+
readBytes(path: string): Promise<Uint8Array>;
|
|
25
|
+
write(path: string, content: string | Uint8Array): Promise<void>;
|
|
26
|
+
list(path: string): Promise<Entry[]>;
|
|
27
|
+
/** Every change under `path` from now on, as it happens: the entry as it is, or `{ path, gone: true }`; `{ resync: true }` when too many arrived to tell. */
|
|
28
|
+
watch(path: string): AsyncIterable<Change>;
|
|
29
|
+
stat(path: string): Promise<Entry | null>;
|
|
30
|
+
remove(path: string): Promise<void>;
|
|
31
|
+
mkdir(path: string): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
export interface Entry { name: string; path: string; dir: boolean; size: number; updated: string }
|
|
34
|
+
export type Change = Entry | { path: string; gone: true } | { resync: true };
|
|
35
|
+
|
|
36
|
+
export interface Http {
|
|
37
|
+
fetch(url: string, init?: { method?: string; headers?: Record<string, string>; body?: string }): Promise<{ status: number; headers: Record<string, string>; body: string }>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ExecOptions {
|
|
41
|
+
cwd?: string; input?: string; env?: Record<string, string>;
|
|
42
|
+
/** Milliseconds; null explicitly requests an unlimited lifetime. Default: ten minutes. */
|
|
43
|
+
timeout?: number | null;
|
|
44
|
+
/** Keep stdin open for exec.write. stream only. */
|
|
45
|
+
interactive?: boolean;
|
|
46
|
+
}
|
|
47
|
+
export type ExecEvent =
|
|
48
|
+
| { type: "started"; process: string; pid: number }
|
|
49
|
+
| { type: "stdout" | "stderr"; data: Uint8Array }
|
|
50
|
+
| { type: "exit"; code: number; signal: string | null };
|
|
51
|
+
export interface Exec {
|
|
52
|
+
run(program: string, args?: string[], options?: ExecOptions): Promise<{ code: number; stdout: string; stderr: string }>;
|
|
53
|
+
stream(program: string, args?: string[], options?: ExecOptions): AsyncIterable<ExecEvent>;
|
|
54
|
+
/** null closes stdin. Acknowledges bytes accepted, not application completion. */
|
|
55
|
+
write(process: string, input: string | null): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface Secrets {
|
|
59
|
+
get(name: string): Promise<string | null>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** What a project's package.json may say under `baba`. */
|
|
63
|
+
export interface BabaConfig {
|
|
64
|
+
/** The baba's name when the package's own must differ, as in a workspace that has a package of that name. */
|
|
65
|
+
name?: string;
|
|
66
|
+
title?: string;
|
|
67
|
+
harnesses?: string[];
|
|
68
|
+
/** What the build needs: bundler plugins from the project's node_modules, directories served beside the view. */
|
|
69
|
+
build?: { plugins?: string[]; assets?: string[] };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* What the kernel implements, one method per service operation. A request
|
|
74
|
+
* names the system, the effect and the job it belongs to, so the kernel can
|
|
75
|
+
* attribute what it serves. Every method may throw a ServiceError.
|
|
76
|
+
*/
|
|
77
|
+
/** A service's answer: once, or many times for a streaming operation. `signal` is the job's: aborted, the service lets go. */
|
|
78
|
+
export interface Services {
|
|
79
|
+
fs(who: Who, op: keyof Fs, args: unknown[], signal?: AbortSignal): Promise<unknown> | AsyncIterable<unknown>;
|
|
80
|
+
http(who: Who, op: keyof Http, args: unknown[], signal?: AbortSignal): Promise<unknown> | AsyncIterable<unknown>;
|
|
81
|
+
exec(who: Who, op: keyof Exec, args: unknown[], signal?: AbortSignal): Promise<unknown> | AsyncIterable<unknown>;
|
|
82
|
+
secrets(who: Who, op: keyof Secrets, args: unknown[], signal?: AbortSignal): Promise<unknown> | AsyncIterable<unknown>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Who asks: the project, the system, the effect, and the job the request belongs to. */
|
|
86
|
+
export interface Who { project: string; system: string; effect: string; job: number; generation?: string; entity?: number | null }
|
|
87
|
+
|
|
88
|
+
export class ServiceError extends Error {
|
|
89
|
+
constructor(readonly code: "denied" | "notfound" | "failed" | "args", msg: string) { super(msg); }
|
|
90
|
+
}
|