bayma-repl 0.0.0-stage → 0.12.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/dist/bayma.js +984 -0
- package/package.json +20 -4
- package/README.md +0 -3
package/dist/bayma.js
ADDED
|
@@ -0,0 +1,984 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// packages/cli/src/main.ts
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
// packages/cli/src/docker.ts
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import {
|
|
10
|
+
request as httpRequest,
|
|
11
|
+
STATUS_CODES
|
|
12
|
+
} from "node:http";
|
|
13
|
+
import { buffer } from "node:stream/consumers";
|
|
14
|
+
var API_VERSION = "v1.44";
|
|
15
|
+
var DEFAULT_SOCKET = "/var/run/docker.sock";
|
|
16
|
+
|
|
17
|
+
class DockerError extends Error {
|
|
18
|
+
status;
|
|
19
|
+
constructor(message, status) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "DockerError";
|
|
22
|
+
this.status = status;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function dockerSocket(env) {
|
|
26
|
+
const host = env.DOCKER_HOST;
|
|
27
|
+
if (!host)
|
|
28
|
+
return DEFAULT_SOCKET;
|
|
29
|
+
if (host.startsWith("unix://"))
|
|
30
|
+
return host.slice("unix://".length);
|
|
31
|
+
throw new DockerError(`DOCKER_HOST is ${host}, but bayma reaches Docker only over a Unix socket: set it to unix://PATH, or unset it for ${DEFAULT_SOCKET}`);
|
|
32
|
+
}
|
|
33
|
+
function unreachableReason(socketPath, cause) {
|
|
34
|
+
switch (cause.code) {
|
|
35
|
+
case "EACCES":
|
|
36
|
+
case "EPERM":
|
|
37
|
+
return `A Docker Engine runs here, but only root and the docker group may use ${socketPath}: add yourself, with sudo usermod -aG docker $USER, and sign in again`;
|
|
38
|
+
case "ECONNREFUSED":
|
|
39
|
+
return `Nothing answers on ${socketPath}: start Docker, with sudo systemctl start docker`;
|
|
40
|
+
case "ENOENT":
|
|
41
|
+
return existsSync("/etc/debian_version") ? `No Docker Engine runs here (${socketPath} is missing): install one, with sudo apt-get install -y docker.io && sudo usermod -aG docker $USER, and sign in again; or set DOCKER_HOST to unix://PATH of the one you run` : `No Docker Engine runs here (${socketPath} is missing): install one, as https://docs.docker.com/engine/install/ describes; or set DOCKER_HOST to unix://PATH of the one you run`;
|
|
42
|
+
default:
|
|
43
|
+
return `Docker could not be reached at ${socketPath}: ${cause.message}`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function describe(call) {
|
|
47
|
+
return `${call.method} ${call.path}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
class DockerEngine {
|
|
51
|
+
socketPath;
|
|
52
|
+
constructor(socketPath) {
|
|
53
|
+
this.socketPath = socketPath;
|
|
54
|
+
}
|
|
55
|
+
send(call) {
|
|
56
|
+
const query = new URLSearchParams(call.query);
|
|
57
|
+
const body = call.body === undefined ? undefined : JSON.stringify(call.body);
|
|
58
|
+
const request = httpRequest({
|
|
59
|
+
socketPath: this.socketPath,
|
|
60
|
+
agent: false,
|
|
61
|
+
method: call.method,
|
|
62
|
+
path: `/${API_VERSION}${call.path}${query.size ? `?${query}` : ""}`,
|
|
63
|
+
headers: body === undefined ? call.headers : {
|
|
64
|
+
...call.headers,
|
|
65
|
+
"Content-Type": "application/json",
|
|
66
|
+
"Content-Length": Buffer.byteLength(body)
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
request.end(body);
|
|
70
|
+
return request;
|
|
71
|
+
}
|
|
72
|
+
unreachable(call, cause) {
|
|
73
|
+
const errno = cause;
|
|
74
|
+
return new DockerError(errno.code ? unreachableReason(this.socketPath, errno) : `Docker failed ${describe(call)}: ${errno.message ?? String(cause)}`);
|
|
75
|
+
}
|
|
76
|
+
async refusal(call, response) {
|
|
77
|
+
const status = response.statusCode ?? 0;
|
|
78
|
+
const text = (await buffer(response)).toString("utf8").trim();
|
|
79
|
+
let reason = text || STATUS_CODES[status] || "no message";
|
|
80
|
+
try {
|
|
81
|
+
const parsed = JSON.parse(text);
|
|
82
|
+
if (typeof parsed.message === "string")
|
|
83
|
+
reason = parsed.message;
|
|
84
|
+
} catch {}
|
|
85
|
+
return new DockerError(`Docker answered ${status} to ${describe(call)}: ${reason}`, status);
|
|
86
|
+
}
|
|
87
|
+
open(call) {
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
const request = this.send(call);
|
|
90
|
+
request.on("response", (response) => {
|
|
91
|
+
if ((response.statusCode ?? 0) < 400)
|
|
92
|
+
resolve(response);
|
|
93
|
+
else
|
|
94
|
+
this.refusal(call, response).then(reject, reject);
|
|
95
|
+
});
|
|
96
|
+
request.on("error", (cause) => reject(this.unreachable(call, cause)));
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
async body(call) {
|
|
100
|
+
const response = await this.open(call);
|
|
101
|
+
try {
|
|
102
|
+
return await buffer(response);
|
|
103
|
+
} catch (cause) {
|
|
104
|
+
throw this.unreachable(call, cause);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async json(call) {
|
|
108
|
+
return JSON.parse((await this.body(call)).toString("utf8"));
|
|
109
|
+
}
|
|
110
|
+
version() {
|
|
111
|
+
return this.json({ method: "GET", path: "/version" });
|
|
112
|
+
}
|
|
113
|
+
async inspectImage(reference) {
|
|
114
|
+
try {
|
|
115
|
+
return await this.json({
|
|
116
|
+
method: "GET",
|
|
117
|
+
path: `/images/${reference}/json`
|
|
118
|
+
});
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (error instanceof DockerError && error.status === 404)
|
|
121
|
+
return null;
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
listImages() {
|
|
126
|
+
return this.json({ method: "GET", path: "/images/json" });
|
|
127
|
+
}
|
|
128
|
+
async pullImage(repository, tag, onEvent) {
|
|
129
|
+
const call = {
|
|
130
|
+
method: "POST",
|
|
131
|
+
path: "/images/create",
|
|
132
|
+
query: { fromImage: repository, tag }
|
|
133
|
+
};
|
|
134
|
+
const response = await this.open(call);
|
|
135
|
+
let pending = "";
|
|
136
|
+
let failure;
|
|
137
|
+
response.setEncoding("utf8");
|
|
138
|
+
try {
|
|
139
|
+
for await (const chunk of response) {
|
|
140
|
+
pending += chunk;
|
|
141
|
+
const lines = pending.split(`
|
|
142
|
+
`);
|
|
143
|
+
pending = lines.pop();
|
|
144
|
+
for (const line of lines) {
|
|
145
|
+
if (!line.trim())
|
|
146
|
+
continue;
|
|
147
|
+
const event = JSON.parse(line);
|
|
148
|
+
if (event.error)
|
|
149
|
+
failure = event.error;
|
|
150
|
+
else
|
|
151
|
+
onEvent(event);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} catch (cause) {
|
|
155
|
+
throw this.unreachable(call, cause);
|
|
156
|
+
}
|
|
157
|
+
if (failure !== undefined)
|
|
158
|
+
throw new DockerError(`Docker could not pull ${repository}: ${failure}`);
|
|
159
|
+
}
|
|
160
|
+
async removeImage(reference) {
|
|
161
|
+
try {
|
|
162
|
+
await this.body({ method: "DELETE", path: `/images/${reference}` });
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (!(error instanceof DockerError && error.status === 404))
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
listContainers(ancestor) {
|
|
169
|
+
return this.json({
|
|
170
|
+
method: "GET",
|
|
171
|
+
path: "/containers/json",
|
|
172
|
+
query: { all: "true", filters: JSON.stringify({ ancestor: [ancestor] }) }
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
async createContainer(spec) {
|
|
176
|
+
const { Id } = await this.json({
|
|
177
|
+
method: "POST",
|
|
178
|
+
path: "/containers/create",
|
|
179
|
+
body: spec
|
|
180
|
+
});
|
|
181
|
+
return Id;
|
|
182
|
+
}
|
|
183
|
+
attachContainer(id, stdin) {
|
|
184
|
+
const call = {
|
|
185
|
+
method: "POST",
|
|
186
|
+
path: `/containers/${id}/attach`,
|
|
187
|
+
query: {
|
|
188
|
+
stream: "1",
|
|
189
|
+
stdin: stdin ? "1" : "0",
|
|
190
|
+
stdout: "1",
|
|
191
|
+
stderr: "1"
|
|
192
|
+
},
|
|
193
|
+
headers: { Connection: "Upgrade", Upgrade: "tcp" }
|
|
194
|
+
};
|
|
195
|
+
return new Promise((resolve, reject) => {
|
|
196
|
+
const request = this.send(call);
|
|
197
|
+
request.on("upgrade", (_response, socket, head) => {
|
|
198
|
+
if (head.length > 0)
|
|
199
|
+
socket.unshift(head);
|
|
200
|
+
resolve(socket);
|
|
201
|
+
});
|
|
202
|
+
request.on("response", (response) => this.refusal(call, response).then(reject, reject));
|
|
203
|
+
request.on("error", (cause) => reject(this.unreachable(call, cause)));
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
watchExit(id) {
|
|
207
|
+
const call = {
|
|
208
|
+
method: "POST",
|
|
209
|
+
path: `/containers/${id}/wait`,
|
|
210
|
+
query: { condition: "removed" }
|
|
211
|
+
};
|
|
212
|
+
return this.open(call).then((response) => ({
|
|
213
|
+
status: buffer(response).then((body) => {
|
|
214
|
+
const { StatusCode, Error: error } = JSON.parse(body.toString("utf8"));
|
|
215
|
+
if (error?.Message)
|
|
216
|
+
throw new DockerError(`Docker lost track of the container: ${error.Message}`);
|
|
217
|
+
return StatusCode;
|
|
218
|
+
}, (cause) => {
|
|
219
|
+
throw this.unreachable(call, cause);
|
|
220
|
+
})
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
async startContainer(id) {
|
|
224
|
+
await this.body({ method: "POST", path: `/containers/${id}/start` });
|
|
225
|
+
}
|
|
226
|
+
async killContainer(id, signal) {
|
|
227
|
+
try {
|
|
228
|
+
await this.body({
|
|
229
|
+
method: "POST",
|
|
230
|
+
path: `/containers/${id}/kill`,
|
|
231
|
+
query: { signal }
|
|
232
|
+
});
|
|
233
|
+
} catch (error) {
|
|
234
|
+
if (!(error instanceof DockerError && [404, 409].includes(error.status)))
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async removeContainer(id) {
|
|
239
|
+
try {
|
|
240
|
+
await this.body({
|
|
241
|
+
method: "DELETE",
|
|
242
|
+
path: `/containers/${id}`,
|
|
243
|
+
query: { force: "true" }
|
|
244
|
+
});
|
|
245
|
+
} catch (error) {
|
|
246
|
+
if (!(error instanceof DockerError && error.status === 404))
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function demultiplexer() {
|
|
252
|
+
let pending = Buffer.alloc(0);
|
|
253
|
+
return (chunk) => {
|
|
254
|
+
pending = pending.length > 0 ? Buffer.concat([pending, chunk]) : chunk;
|
|
255
|
+
const frames = [];
|
|
256
|
+
let offset = 0;
|
|
257
|
+
while (offset + 8 <= pending.length) {
|
|
258
|
+
const size = pending.readUInt32BE(offset + 4);
|
|
259
|
+
if (offset + 8 + size > pending.length)
|
|
260
|
+
break;
|
|
261
|
+
frames.push({
|
|
262
|
+
stream: pending[offset] === 2 ? "stderr" : "stdout",
|
|
263
|
+
data: pending.subarray(offset + 8, offset + 8 + size)
|
|
264
|
+
});
|
|
265
|
+
offset += 8 + size;
|
|
266
|
+
}
|
|
267
|
+
pending = pending.subarray(offset);
|
|
268
|
+
return frames;
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// packages/cli/src/context.ts
|
|
273
|
+
function connectDocker(context) {
|
|
274
|
+
return new DockerEngine(dockerSocket(context.env));
|
|
275
|
+
}
|
|
276
|
+
function say(context, text) {
|
|
277
|
+
context.stdio.stdout.write(`${text}
|
|
278
|
+
`);
|
|
279
|
+
}
|
|
280
|
+
// packages/cli/package.json
|
|
281
|
+
var package_default = {
|
|
282
|
+
name: "bayma-repl",
|
|
283
|
+
version: "0.12.2",
|
|
284
|
+
description: "Set up bayma, a durable engine for REPL sessions, and launch its MCP servers in Docker: npx bayma-repl init",
|
|
285
|
+
type: "module",
|
|
286
|
+
license: "MIT",
|
|
287
|
+
bin: {
|
|
288
|
+
bayma: "dist/bayma.js"
|
|
289
|
+
},
|
|
290
|
+
files: [
|
|
291
|
+
"dist/bayma.js"
|
|
292
|
+
],
|
|
293
|
+
engines: {
|
|
294
|
+
node: ">=22"
|
|
295
|
+
},
|
|
296
|
+
repository: {
|
|
297
|
+
type: "git",
|
|
298
|
+
url: "git+https://github.com/eaucoin/bayma.git",
|
|
299
|
+
directory: "packages/cli"
|
|
300
|
+
},
|
|
301
|
+
homepage: "https://github.com/eaucoin/bayma"
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
// packages/cli/src/release.ts
|
|
305
|
+
var VERSION = package_default.version;
|
|
306
|
+
var IMAGE_REPOSITORY = "ghcr.io/eaucoin/bayma";
|
|
307
|
+
var IMAGE_DIGEST = "sha256:654750dd6a1ddcc36aaf46bf5676c58a3438fab3545da454c05aeb296f42a4e6";
|
|
308
|
+
|
|
309
|
+
// packages/cli/src/image.ts
|
|
310
|
+
var IMAGE_OVERRIDE = "BAYMA_IMAGE";
|
|
311
|
+
function pinnedImage(digest = IMAGE_DIGEST) {
|
|
312
|
+
return digest ? `${IMAGE_REPOSITORY}@${digest}` : undefined;
|
|
313
|
+
}
|
|
314
|
+
function baymaImage(env) {
|
|
315
|
+
const override = env[IMAGE_OVERRIDE];
|
|
316
|
+
if (override)
|
|
317
|
+
return { reference: override, pinned: false };
|
|
318
|
+
const pinned = pinnedImage();
|
|
319
|
+
if (!pinned)
|
|
320
|
+
throw new Error(`this bayma ${VERSION} is a development build, with no image pinned: set ${IMAGE_OVERRIDE} to an image such as bayma:${VERSION}, which bun run image builds`);
|
|
321
|
+
return { reference: pinned, pinned: true };
|
|
322
|
+
}
|
|
323
|
+
function missingImage(image) {
|
|
324
|
+
return image.pinned ? `bayma's image ${image.reference} is not here: run npx bayma-repl@${VERSION} init` : `${IMAGE_OVERRIDE} names ${image.reference}, which Docker does not have`;
|
|
325
|
+
}
|
|
326
|
+
async function ensureImage(engine, image, report) {
|
|
327
|
+
if (await engine.inspectImage(image.reference))
|
|
328
|
+
return "present";
|
|
329
|
+
if (!image.pinned)
|
|
330
|
+
throw new Error(missingImage(image));
|
|
331
|
+
await engine.pullImage(IMAGE_REPOSITORY, IMAGE_DIGEST, report);
|
|
332
|
+
return "pulled";
|
|
333
|
+
}
|
|
334
|
+
var MB = 1e6;
|
|
335
|
+
function pullReporter(stream) {
|
|
336
|
+
const layers = new Map;
|
|
337
|
+
const done = new Set;
|
|
338
|
+
const line = () => {
|
|
339
|
+
let current = 0;
|
|
340
|
+
let total = 0;
|
|
341
|
+
for (const layer of layers.values()) {
|
|
342
|
+
current += layer.current;
|
|
343
|
+
total += layer.total;
|
|
344
|
+
}
|
|
345
|
+
return `pulled ${done.size} of ${layers.size} layers, ${Math.round(current / MB)} of ${Math.round(total / MB)} MB downloaded`;
|
|
346
|
+
};
|
|
347
|
+
return {
|
|
348
|
+
event({ id, status, progressDetail }) {
|
|
349
|
+
if (id === undefined || status === undefined)
|
|
350
|
+
return;
|
|
351
|
+
const layer = layers.get(id) ?? { current: 0, total: 0 };
|
|
352
|
+
layers.set(id, layer);
|
|
353
|
+
if (status === "Downloading" && progressDetail?.total) {
|
|
354
|
+
layer.current = progressDetail.current ?? 0;
|
|
355
|
+
layer.total = progressDetail.total;
|
|
356
|
+
}
|
|
357
|
+
if (status === "Download complete")
|
|
358
|
+
layer.current = layer.total;
|
|
359
|
+
const finished = (status === "Pull complete" || status === "Already exists") && !done.has(id);
|
|
360
|
+
if (finished)
|
|
361
|
+
done.add(id);
|
|
362
|
+
if (stream.isTTY)
|
|
363
|
+
stream.write(`\r\x1B[K${line()}`);
|
|
364
|
+
else if (finished)
|
|
365
|
+
stream.write(`${line()}
|
|
366
|
+
`);
|
|
367
|
+
},
|
|
368
|
+
end() {
|
|
369
|
+
if (stream.isTTY && layers.size > 0)
|
|
370
|
+
stream.write(`
|
|
371
|
+
`);
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// packages/cli/src/launcher.ts
|
|
377
|
+
import { constants } from "node:os";
|
|
378
|
+
|
|
379
|
+
// packages/cli/src/paths.ts
|
|
380
|
+
import { isAbsolute, join, relative, sep } from "node:path";
|
|
381
|
+
var XDG_DIRECTORIES = [
|
|
382
|
+
"XDG_DATA_HOME",
|
|
383
|
+
"XDG_STATE_HOME",
|
|
384
|
+
"XDG_CACHE_HOME"
|
|
385
|
+
];
|
|
386
|
+
function pathEnvironment(home, env) {
|
|
387
|
+
const paths = { HOME: home };
|
|
388
|
+
for (const variable of XDG_DIRECTORIES) {
|
|
389
|
+
const value = env[variable];
|
|
390
|
+
if (!value)
|
|
391
|
+
continue;
|
|
392
|
+
const within = relative(home, value);
|
|
393
|
+
if (!isAbsolute(value) || within === ".." || within.startsWith(`..${sep}`) || isAbsolute(within))
|
|
394
|
+
throw new Error(`${variable} is ${value}, outside HOME (${home}), the one directory bayma's container is given: unset it, or set it to a directory under HOME`);
|
|
395
|
+
paths[variable] = value;
|
|
396
|
+
}
|
|
397
|
+
return paths;
|
|
398
|
+
}
|
|
399
|
+
function xdgRoot(env, variable, fallback) {
|
|
400
|
+
const configured = env[variable];
|
|
401
|
+
return configured ? join(configured, "bayma") : join(env.HOME, ...fallback, "bayma");
|
|
402
|
+
}
|
|
403
|
+
function dataRoot(env) {
|
|
404
|
+
return xdgRoot(env, "XDG_DATA_HOME", [".local", "share"]);
|
|
405
|
+
}
|
|
406
|
+
function stateRoot(env) {
|
|
407
|
+
return xdgRoot(env, "XDG_STATE_HOME", [".local", "state"]);
|
|
408
|
+
}
|
|
409
|
+
function cacheRoot(env) {
|
|
410
|
+
return xdgRoot(env, "XDG_CACHE_HOME", [".cache"]);
|
|
411
|
+
}
|
|
412
|
+
function toolbeltDir(env) {
|
|
413
|
+
return join(dataRoot(env), "toolbelt");
|
|
414
|
+
}
|
|
415
|
+
var TOOLBELT_VERSION_FILE = ".bayma-toolbelt-version";
|
|
416
|
+
function cliRoot(env) {
|
|
417
|
+
return join(dataRoot(env), "cli");
|
|
418
|
+
}
|
|
419
|
+
function installedCli(env, version) {
|
|
420
|
+
return join(cliRoot(env), version, "bayma.js");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// packages/cli/src/launcher.ts
|
|
424
|
+
var SNAPSHOT_CAPABILITIES = ["CHECKPOINT_RESTORE", "SYS_PTRACE"];
|
|
425
|
+
var SNAPSHOT_SECURITY = ["seccomp=unconfined"];
|
|
426
|
+
function telemetryEnvironment(env) {
|
|
427
|
+
return Object.entries(env).filter(([name, value]) => name.startsWith("OTEL_") && value).map(([name, value]) => `${name}=${value}`).sort();
|
|
428
|
+
}
|
|
429
|
+
function containerSpec(image, command, args, invocation) {
|
|
430
|
+
const stdin = command === "mcp-stdio";
|
|
431
|
+
return {
|
|
432
|
+
Image: image,
|
|
433
|
+
Cmd: [command, ...args],
|
|
434
|
+
User: `${invocation.uid}:${invocation.gid}`,
|
|
435
|
+
Env: [
|
|
436
|
+
...Object.entries(pathEnvironment(invocation.home, invocation.env)).map(([name, value]) => `${name}=${value}`),
|
|
437
|
+
...telemetryEnvironment(invocation.env)
|
|
438
|
+
],
|
|
439
|
+
WorkingDir: invocation.cwd,
|
|
440
|
+
AttachStdin: stdin,
|
|
441
|
+
AttachStdout: true,
|
|
442
|
+
AttachStderr: true,
|
|
443
|
+
OpenStdin: stdin,
|
|
444
|
+
StdinOnce: stdin,
|
|
445
|
+
Tty: false,
|
|
446
|
+
HostConfig: {
|
|
447
|
+
AutoRemove: true,
|
|
448
|
+
Binds: [`${invocation.home}:${invocation.home}`],
|
|
449
|
+
CapAdd: SNAPSHOT_CAPABILITIES,
|
|
450
|
+
SecurityOpt: SNAPSHOT_SECURITY,
|
|
451
|
+
NetworkMode: "host"
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
var FORWARDED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
456
|
+
async function runContainer(engine, image, spec, stdio) {
|
|
457
|
+
let running;
|
|
458
|
+
let cancelled;
|
|
459
|
+
const onSignal = (signal) => {
|
|
460
|
+
if (running === undefined) {
|
|
461
|
+
cancelled ??= signal;
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
engine.killContainer(running, signal).catch((error) => stdio.stderr.write(`bayma: could not forward ${signal}: ${error instanceof Error ? error.message : String(error)}
|
|
465
|
+
`));
|
|
466
|
+
};
|
|
467
|
+
for (const signal of FORWARDED_SIGNALS)
|
|
468
|
+
process.on(signal, onSignal);
|
|
469
|
+
try {
|
|
470
|
+
let id;
|
|
471
|
+
try {
|
|
472
|
+
id = await engine.createContainer(spec);
|
|
473
|
+
} catch (error) {
|
|
474
|
+
if (error instanceof DockerError && error.status === 404)
|
|
475
|
+
throw new Error(missingImage(image));
|
|
476
|
+
throw error;
|
|
477
|
+
}
|
|
478
|
+
let socket;
|
|
479
|
+
let exit;
|
|
480
|
+
let started = false;
|
|
481
|
+
try {
|
|
482
|
+
socket = await engine.attachContainer(id, spec.OpenStdin);
|
|
483
|
+
exit = await engine.watchExit(id);
|
|
484
|
+
if (cancelled === undefined) {
|
|
485
|
+
await engine.startContainer(id);
|
|
486
|
+
started = true;
|
|
487
|
+
}
|
|
488
|
+
} catch (error) {
|
|
489
|
+
await discard(engine, id, socket, exit);
|
|
490
|
+
throw error;
|
|
491
|
+
}
|
|
492
|
+
if (!started) {
|
|
493
|
+
await discard(engine, id, socket, exit);
|
|
494
|
+
return 128 + constants.signals[cancelled];
|
|
495
|
+
}
|
|
496
|
+
running = id;
|
|
497
|
+
if (cancelled !== undefined)
|
|
498
|
+
onSignal(cancelled);
|
|
499
|
+
return await forwardUntilExit(socket, exit, spec.OpenStdin, stdio);
|
|
500
|
+
} finally {
|
|
501
|
+
for (const signal of FORWARDED_SIGNALS)
|
|
502
|
+
process.off(signal, onSignal);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
async function discard(engine, id, socket, exit) {
|
|
506
|
+
socket?.destroy();
|
|
507
|
+
exit?.status.catch(() => {
|
|
508
|
+
return;
|
|
509
|
+
});
|
|
510
|
+
await engine.removeContainer(id).catch(() => {
|
|
511
|
+
return;
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
async function forwardUntilExit(socket, exit, stdin, stdio) {
|
|
515
|
+
const frames = demultiplexer();
|
|
516
|
+
const output = new Promise((resolve) => {
|
|
517
|
+
socket.on("data", (chunk) => {
|
|
518
|
+
for (const { stream, data } of frames(chunk)) {
|
|
519
|
+
const target = stdio[stream];
|
|
520
|
+
if (!target.write(data)) {
|
|
521
|
+
socket.pause();
|
|
522
|
+
target.once("drain", () => socket.resume());
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
socket.on("error", () => {
|
|
527
|
+
return;
|
|
528
|
+
});
|
|
529
|
+
socket.on("close", () => resolve());
|
|
530
|
+
});
|
|
531
|
+
if (stdin)
|
|
532
|
+
stdio.stdin.pipe(socket);
|
|
533
|
+
try {
|
|
534
|
+
const [status] = await Promise.all([exit.status, output]);
|
|
535
|
+
return status;
|
|
536
|
+
} finally {
|
|
537
|
+
if (stdin)
|
|
538
|
+
stdio.stdin.unpipe(socket);
|
|
539
|
+
socket.destroy();
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// packages/cli/src/setup.ts
|
|
544
|
+
import {
|
|
545
|
+
chmodSync,
|
|
546
|
+
copyFileSync,
|
|
547
|
+
existsSync as existsSync3,
|
|
548
|
+
mkdirSync,
|
|
549
|
+
readdirSync,
|
|
550
|
+
renameSync
|
|
551
|
+
} from "node:fs";
|
|
552
|
+
import { dirname } from "node:path";
|
|
553
|
+
|
|
554
|
+
// packages/cli/src/clients.ts
|
|
555
|
+
import { spawn } from "node:child_process";
|
|
556
|
+
import { accessSync, constants as constants2, existsSync as existsSync2, readFileSync } from "node:fs";
|
|
557
|
+
import { delimiter, isAbsolute as isAbsolute2, join as join2 } from "node:path";
|
|
558
|
+
var SERVER_NAME = "bayma";
|
|
559
|
+
function claudeRegistration(home, env) {
|
|
560
|
+
const path = join2(env.CLAUDE_CONFIG_DIR || home, ".claude.json");
|
|
561
|
+
if (!existsSync2(path))
|
|
562
|
+
return;
|
|
563
|
+
const config = JSON.parse(readFileSync(path, "utf8"));
|
|
564
|
+
const server = config.mcpServers?.[SERVER_NAME];
|
|
565
|
+
if (server?.command === undefined)
|
|
566
|
+
return;
|
|
567
|
+
return { command: server.command, args: server.args ?? [] };
|
|
568
|
+
}
|
|
569
|
+
function tomlStrings(text) {
|
|
570
|
+
return [...text.matchAll(/"(?:[^"\\]|\\.)*"|'[^']*'/g)].map(([value]) => value.startsWith("'") ? value.slice(1, -1) : JSON.parse(value));
|
|
571
|
+
}
|
|
572
|
+
function codexRegistration(home, env) {
|
|
573
|
+
const path = join2(env.CODEX_HOME || join2(home, ".codex"), "config.toml");
|
|
574
|
+
if (!existsSync2(path))
|
|
575
|
+
return;
|
|
576
|
+
const lines = readFileSync(path, "utf8").split(`
|
|
577
|
+
`);
|
|
578
|
+
const header = lines.findIndex((line) => /^\s*\[\s*mcp_servers\s*\.\s*(bayma|"bayma")\s*\]\s*$/.test(line));
|
|
579
|
+
if (header === -1)
|
|
580
|
+
return;
|
|
581
|
+
const end = lines.findIndex((line, index) => index > header && /^\s*\[/.test(line));
|
|
582
|
+
const table = lines.slice(header + 1, end === -1 ? undefined : end).join(`
|
|
583
|
+
`);
|
|
584
|
+
const command = /^\s*command\s*=\s*(.*)$/m.exec(table)?.[1];
|
|
585
|
+
const args = /^\s*args\s*=\s*\[([^\]]*)\]/m.exec(table)?.[1];
|
|
586
|
+
if (command === undefined)
|
|
587
|
+
return;
|
|
588
|
+
const [executable] = tomlStrings(command);
|
|
589
|
+
if (executable === undefined)
|
|
590
|
+
return;
|
|
591
|
+
return {
|
|
592
|
+
command: executable,
|
|
593
|
+
args: args === undefined ? [] : tomlStrings(args)
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
var MCP_CLIENTS = [
|
|
597
|
+
{
|
|
598
|
+
name: "Claude Code",
|
|
599
|
+
executable: "claude",
|
|
600
|
+
add: ({ command, args }) => [
|
|
601
|
+
"mcp",
|
|
602
|
+
"add",
|
|
603
|
+
"--scope",
|
|
604
|
+
"user",
|
|
605
|
+
SERVER_NAME,
|
|
606
|
+
"--",
|
|
607
|
+
command,
|
|
608
|
+
...args
|
|
609
|
+
],
|
|
610
|
+
remove: ["mcp", "remove", "--scope", "user", SERVER_NAME],
|
|
611
|
+
registered: claudeRegistration
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
name: "Codex",
|
|
615
|
+
executable: "codex",
|
|
616
|
+
add: ({ command, args }) => [
|
|
617
|
+
"mcp",
|
|
618
|
+
"add",
|
|
619
|
+
SERVER_NAME,
|
|
620
|
+
"--",
|
|
621
|
+
command,
|
|
622
|
+
...args
|
|
623
|
+
],
|
|
624
|
+
remove: ["mcp", "remove", SERVER_NAME],
|
|
625
|
+
registered: codexRegistration
|
|
626
|
+
}
|
|
627
|
+
];
|
|
628
|
+
function isExecutable(path) {
|
|
629
|
+
try {
|
|
630
|
+
accessSync(path, constants2.X_OK);
|
|
631
|
+
return true;
|
|
632
|
+
} catch {
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
function findExecutable(name, env) {
|
|
637
|
+
if (name.includes("/"))
|
|
638
|
+
return isExecutable(name) ? name : undefined;
|
|
639
|
+
for (const directory of (env.PATH ?? "").split(delimiter)) {
|
|
640
|
+
if (!directory)
|
|
641
|
+
continue;
|
|
642
|
+
const candidate = join2(directory, name);
|
|
643
|
+
if (isExecutable(candidate))
|
|
644
|
+
return candidate;
|
|
645
|
+
}
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
function runCommand(file, args, env) {
|
|
649
|
+
return new Promise((resolve, reject) => {
|
|
650
|
+
const child = spawn(file, args, {
|
|
651
|
+
env,
|
|
652
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
653
|
+
});
|
|
654
|
+
let output = "";
|
|
655
|
+
child.stdout.setEncoding("utf8").on("data", (chunk) => output += chunk);
|
|
656
|
+
child.stderr.setEncoding("utf8").on("data", (chunk) => output += chunk);
|
|
657
|
+
child.on("error", reject);
|
|
658
|
+
child.on("close", (code) => resolve({ status: code ?? 1, output }));
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
async function register(client, executable, launch, env) {
|
|
662
|
+
await runCommand(executable, client.remove, env);
|
|
663
|
+
return runCommand(executable, client.add(launch), env);
|
|
664
|
+
}
|
|
665
|
+
function registrationProblem(launch, env) {
|
|
666
|
+
const [cli, command] = launch.args;
|
|
667
|
+
if (!findExecutable(launch.command, env))
|
|
668
|
+
return isAbsolute2(launch.command) ? `${launch.command} is not there to run` : `${launch.command} is not on PATH`;
|
|
669
|
+
if (cli === undefined || !existsSync2(cli))
|
|
670
|
+
return `${cli ?? "no command"} is not there`;
|
|
671
|
+
if (command !== "mcp-stdio")
|
|
672
|
+
return `it does not run mcp-stdio`;
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// packages/cli/src/setup.ts
|
|
677
|
+
function installCli(paths, bundle) {
|
|
678
|
+
const target = installedCli(paths, VERSION);
|
|
679
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
680
|
+
const staging = `${target}.${process.pid}`;
|
|
681
|
+
copyFileSync(bundle, staging);
|
|
682
|
+
chmodSync(staging, 493);
|
|
683
|
+
renameSync(staging, target);
|
|
684
|
+
return target;
|
|
685
|
+
}
|
|
686
|
+
function launchCommand(node, cli) {
|
|
687
|
+
return { command: node, args: [cli, "mcp-stdio"] };
|
|
688
|
+
}
|
|
689
|
+
function mcpServersJson(launch) {
|
|
690
|
+
return JSON.stringify({ mcpServers: { [SERVER_NAME]: launch } }, null, 2);
|
|
691
|
+
}
|
|
692
|
+
function otherVersions(paths) {
|
|
693
|
+
const root = cliRoot(paths);
|
|
694
|
+
if (!existsSync3(root))
|
|
695
|
+
return [];
|
|
696
|
+
return readdirSync(root).filter((version) => version !== VERSION);
|
|
697
|
+
}
|
|
698
|
+
function runInImage(context, engine, image, command, output) {
|
|
699
|
+
return runContainer(engine, image, containerSpec(image.reference, command, [], context), { ...context.stdio, stdout: output });
|
|
700
|
+
}
|
|
701
|
+
async function setUp(context, upgrade) {
|
|
702
|
+
const paths = pathEnvironment(context.home, context.env);
|
|
703
|
+
if (upgrade) {
|
|
704
|
+
const others = otherVersions(paths);
|
|
705
|
+
say(context, `Upgrading to bayma ${VERSION}${others.length > 0 ? ` from ${others.join(", ")}` : ""}. MCP clients running now keep their bayma until they restart; once they do, a REPL session another version snapshotted is restored from its checkpoints rather than resumed live, since a process snapshot is restored only by the version that took it.`);
|
|
706
|
+
}
|
|
707
|
+
const image = baymaImage(context.env);
|
|
708
|
+
const engine = connectDocker(context);
|
|
709
|
+
const docker = await engine.version();
|
|
710
|
+
say(context, `Docker ${docker.Version} answers at ${engine.socketPath}`);
|
|
711
|
+
const progress = pullReporter(context.stdio.stderr);
|
|
712
|
+
if (image.pinned)
|
|
713
|
+
say(context, `pulling bayma ${VERSION}'s image, ${image.reference}`);
|
|
714
|
+
const pulled = await ensureImage(engine, image, (event) => progress.event(event));
|
|
715
|
+
progress.end();
|
|
716
|
+
say(context, `image ${image.reference}: ${pulled}`);
|
|
717
|
+
say(context, "installing the toolbelt bundled with bayma");
|
|
718
|
+
const installed = await runInImage(context, engine, image, "install-toolbelt", context.stdio.stderr);
|
|
719
|
+
if (installed !== 0)
|
|
720
|
+
throw new Error(`bayma install-toolbelt failed with status ${installed}, as it says above; bayma is not registered`);
|
|
721
|
+
say(context, "checking each runtime with bayma doctor");
|
|
722
|
+
const doctor = await runInImage(context, engine, image, "doctor", context.stdio.stdout);
|
|
723
|
+
const cli = installCli(paths, context.bundle);
|
|
724
|
+
say(context, `installed bayma ${VERSION} at ${cli}`);
|
|
725
|
+
const launch = launchCommand(context.node, cli);
|
|
726
|
+
let failed = false;
|
|
727
|
+
for (const client of MCP_CLIENTS) {
|
|
728
|
+
const executable = findExecutable(client.executable, context.env);
|
|
729
|
+
if (!executable) {
|
|
730
|
+
say(context, `${client.name}: no ${client.executable} on PATH, so not registered`);
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
const result = await register(client, executable, launch, context.env);
|
|
734
|
+
const ran = [client.executable, ...client.add(launch)].join(" ");
|
|
735
|
+
if (result.status === 0)
|
|
736
|
+
say(context, `${client.name}: registered bayma, with ${ran}`);
|
|
737
|
+
else {
|
|
738
|
+
failed = true;
|
|
739
|
+
say(context, `${client.name}: ${ran} failed with status ${result.status}: ${result.output.trim()}`);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
say(context, `For any other MCP client:
|
|
743
|
+
${mcpServersJson(launch)}`);
|
|
744
|
+
if (doctor !== 0)
|
|
745
|
+
say(context, `warning: bayma doctor failed with status ${doctor}, so a runtime its report above names as failing does not work here; bayma is registered all the same, for the runtimes that do. npx bayma-repl doctor checks them again.`);
|
|
746
|
+
return failed ? 1 : 0;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// packages/cli/src/status.ts
|
|
750
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
751
|
+
import { join as join3 } from "node:path";
|
|
752
|
+
function message(error) {
|
|
753
|
+
return error instanceof Error ? error.message : String(error);
|
|
754
|
+
}
|
|
755
|
+
async function readStatus(context) {
|
|
756
|
+
const paths = pathEnvironment(context.home, context.env);
|
|
757
|
+
const status = {
|
|
758
|
+
version: VERSION,
|
|
759
|
+
docker: {},
|
|
760
|
+
image: {},
|
|
761
|
+
toolbelt: { path: toolbeltDir(paths) },
|
|
762
|
+
installed: existsSync4(cliRoot(paths)) ? readdirSync2(cliRoot(paths)).sort() : [],
|
|
763
|
+
registrations: MCP_CLIENTS.map((client) => {
|
|
764
|
+
const launch = client.registered(context.home, context.env);
|
|
765
|
+
return {
|
|
766
|
+
client: client.name,
|
|
767
|
+
launch,
|
|
768
|
+
problem: launch && registrationProblem(launch, context.env)
|
|
769
|
+
};
|
|
770
|
+
})
|
|
771
|
+
};
|
|
772
|
+
const versionFile = join3(status.toolbelt.path, TOOLBELT_VERSION_FILE);
|
|
773
|
+
if (existsSync4(versionFile))
|
|
774
|
+
status.toolbelt.version = readFileSync2(versionFile, "utf8").trim();
|
|
775
|
+
try {
|
|
776
|
+
status.image.image = baymaImage(context.env);
|
|
777
|
+
} catch (error) {
|
|
778
|
+
status.image.problem = message(error);
|
|
779
|
+
}
|
|
780
|
+
try {
|
|
781
|
+
const engine = connectDocker(context);
|
|
782
|
+
status.docker.socket = engine.socketPath;
|
|
783
|
+
status.docker.version = (await engine.version()).Version;
|
|
784
|
+
if (status.image.image !== undefined)
|
|
785
|
+
status.image.present = await engine.inspectImage(status.image.image.reference) !== null;
|
|
786
|
+
} catch (error) {
|
|
787
|
+
status.docker.problem = message(error);
|
|
788
|
+
}
|
|
789
|
+
return status;
|
|
790
|
+
}
|
|
791
|
+
function renderStatus(status) {
|
|
792
|
+
const lines = [`bayma ${status.version}`];
|
|
793
|
+
lines.push(status.docker.problem === undefined ? `Docker: ${status.docker.version} at ${status.docker.socket}` : `Docker: ${status.docker.problem}`);
|
|
794
|
+
const { image, present, problem } = status.image;
|
|
795
|
+
lines.push(image === undefined ? `image: ${problem}` : present === undefined ? `image: ${image.reference}, not known to be here without Docker` : present ? `image: ${image.reference}` : `image: ${missingImage(image)}`);
|
|
796
|
+
lines.push(status.toolbelt.version === undefined ? `toolbelt: not installed at ${status.toolbelt.path}` : `toolbelt: ${status.toolbelt.version} at ${status.toolbelt.path}`);
|
|
797
|
+
lines.push(`installed for MCP clients: ${status.installed.length > 0 ? status.installed.join(", ") : "none"}`);
|
|
798
|
+
for (const { client, launch, problem } of status.registrations) {
|
|
799
|
+
if (launch === undefined) {
|
|
800
|
+
lines.push(`${client}: bayma is not registered`);
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
const command = [launch.command, ...launch.args].join(" ");
|
|
804
|
+
lines.push(problem === undefined ? `${client}: ${command}` : `${client}: ${command}, which is broken: ${problem}; npx bayma-repl init registers bayma again`);
|
|
805
|
+
}
|
|
806
|
+
return lines.join(`
|
|
807
|
+
`);
|
|
808
|
+
}
|
|
809
|
+
async function showStatus(context) {
|
|
810
|
+
say(context, renderStatus(await readStatus(context)));
|
|
811
|
+
return 0;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// packages/cli/src/uninstall.ts
|
|
815
|
+
import { existsSync as existsSync5, readdirSync as readdirSync3, rmSync } from "node:fs";
|
|
816
|
+
async function pulledImages(engine) {
|
|
817
|
+
return (await engine.listImages()).flatMap(({ RepoDigests }) => (RepoDigests ?? []).filter((reference) => reference.startsWith(`${IMAGE_REPOSITORY}@`)));
|
|
818
|
+
}
|
|
819
|
+
async function running(engine, references) {
|
|
820
|
+
let count = 0;
|
|
821
|
+
for (const reference of references)
|
|
822
|
+
count += (await engine.listContainers(reference)).filter(({ State }) => State === "running").length;
|
|
823
|
+
return count;
|
|
824
|
+
}
|
|
825
|
+
function remove(context, path, what) {
|
|
826
|
+
if (!existsSync5(path))
|
|
827
|
+
return;
|
|
828
|
+
rmSync(path, { recursive: true, force: true });
|
|
829
|
+
say(context, `removed ${what}, ${path}`);
|
|
830
|
+
}
|
|
831
|
+
async function uninstall(context, purge) {
|
|
832
|
+
const paths = pathEnvironment(context.home, context.env);
|
|
833
|
+
let docker;
|
|
834
|
+
try {
|
|
835
|
+
const engine = connectDocker(context);
|
|
836
|
+
docker = { engine, images: await pulledImages(engine) };
|
|
837
|
+
} catch (error) {
|
|
838
|
+
say(context, `${error instanceof Error ? error.message : String(error)}; bayma's images are left as they are`);
|
|
839
|
+
}
|
|
840
|
+
if (docker !== undefined) {
|
|
841
|
+
const override = context.env[IMAGE_OVERRIDE];
|
|
842
|
+
const servers = await running(docker.engine, override ? [...docker.images, override] : docker.images);
|
|
843
|
+
if (servers > 0)
|
|
844
|
+
throw new Error(`bayma runs in ${servers} container${servers === 1 ? "" : "s"} now, serving MCP clients: close them, then uninstall`);
|
|
845
|
+
}
|
|
846
|
+
for (const client of MCP_CLIENTS) {
|
|
847
|
+
const executable = findExecutable(client.executable, context.env);
|
|
848
|
+
if (!executable)
|
|
849
|
+
continue;
|
|
850
|
+
const result = await runCommand(executable, client.remove, context.env);
|
|
851
|
+
say(context, result.status === 0 ? `${client.name}: removed bayma` : `${client.name}: removed nothing: ${result.output.trim()}`);
|
|
852
|
+
}
|
|
853
|
+
if (docker !== undefined)
|
|
854
|
+
for (const reference of docker.images) {
|
|
855
|
+
await docker.engine.removeImage(reference);
|
|
856
|
+
say(context, `removed the image ${reference}`);
|
|
857
|
+
}
|
|
858
|
+
remove(context, dataRoot(paths), "the toolbelt bundled with bayma and bayma's commands for MCP clients");
|
|
859
|
+
remove(context, cacheRoot(paths), "bayma's cache");
|
|
860
|
+
const state = stateRoot(paths);
|
|
861
|
+
if (!existsSync5(state))
|
|
862
|
+
return 0;
|
|
863
|
+
if (!purge) {
|
|
864
|
+
say(context, `kept REPL sessions' state, in ${state}; uninstall --purge removes it`);
|
|
865
|
+
return 0;
|
|
866
|
+
}
|
|
867
|
+
const directories = readdirSync3(state).length;
|
|
868
|
+
say(context, `removing REPL sessions' state, of the ${directories} director${directories === 1 ? "y" : "ies"} MCP clients launched bayma from: every REPL session's catalog entry, execution history, checkpoints, and process snapshots`);
|
|
869
|
+
remove(context, state, "REPL sessions' state");
|
|
870
|
+
return 0;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// packages/cli/src/main.ts
|
|
874
|
+
function usage() {
|
|
875
|
+
return `Usage: npx bayma-repl <command>
|
|
876
|
+
|
|
877
|
+
bayma is a durable engine for REPL sessions, served to agents by its MCP
|
|
878
|
+
servers. This command sets it up on Linux (x64) with Docker, and runs it in
|
|
879
|
+
bayma's image of its own version.
|
|
880
|
+
|
|
881
|
+
Commands:
|
|
882
|
+
init
|
|
883
|
+
pull bayma's image, install the toolbelt bundled with bayma, check
|
|
884
|
+
each runtime with bayma's doctor, and register bayma with Claude Code
|
|
885
|
+
and Codex, as an MCP server they launch over stdio
|
|
886
|
+
upgrade
|
|
887
|
+
set up this version of bayma in place of the one set up now; run it
|
|
888
|
+
as npx bayma-repl@latest upgrade
|
|
889
|
+
status
|
|
890
|
+
show what is set up of bayma here, and what of it would not work
|
|
891
|
+
mcp-stdio [OPTIONS]
|
|
892
|
+
serve MCP over stdio, as MCP clients launch bayma
|
|
893
|
+
mcp-http [OPTIONS]
|
|
894
|
+
serve MCP over Streamable HTTP; its default host, 127.0.0.1, is this
|
|
895
|
+
machine's loopback
|
|
896
|
+
doctor [OPTIONS]
|
|
897
|
+
check that each runtime works here
|
|
898
|
+
uninstall [--purge]
|
|
899
|
+
remove bayma's registrations, its commands for MCP clients, its
|
|
900
|
+
images, its toolbelt, and its cache; with --purge, REPL sessions'
|
|
901
|
+
state too
|
|
902
|
+
version
|
|
903
|
+
print the version
|
|
904
|
+
help
|
|
905
|
+
show this help
|
|
906
|
+
|
|
907
|
+
OPTIONS are those of the same command in bayma's image, passed to it as
|
|
908
|
+
given. bayma's servers export OpenTelemetry traces, metrics, and logs where
|
|
909
|
+
the OTEL_* variables set here say.`;
|
|
910
|
+
}
|
|
911
|
+
var LAUNCHED = [
|
|
912
|
+
"mcp-stdio",
|
|
913
|
+
"mcp-http",
|
|
914
|
+
"doctor"
|
|
915
|
+
];
|
|
916
|
+
async function launch(context, command, args) {
|
|
917
|
+
const image = baymaImage(context.env);
|
|
918
|
+
return runContainer(connectDocker(context), image, containerSpec(image.reference, command, args, context), context.stdio);
|
|
919
|
+
}
|
|
920
|
+
function expectArgs(command, args, allowed) {
|
|
921
|
+
const unknown = args.find((arg) => !allowed.includes(arg));
|
|
922
|
+
if (unknown !== undefined)
|
|
923
|
+
throw new Error(`${command} takes no ${unknown}; see npx bayma-repl help`);
|
|
924
|
+
}
|
|
925
|
+
function processContext() {
|
|
926
|
+
return {
|
|
927
|
+
uid: process.getuid(),
|
|
928
|
+
gid: process.getgid(),
|
|
929
|
+
home: process.env.HOME || homedir(),
|
|
930
|
+
cwd: process.cwd(),
|
|
931
|
+
env: process.env,
|
|
932
|
+
node: process.execPath,
|
|
933
|
+
bundle: fileURLToPath(import.meta.url),
|
|
934
|
+
stdio: {
|
|
935
|
+
stdin: process.stdin,
|
|
936
|
+
stdout: process.stdout,
|
|
937
|
+
stderr: process.stderr
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
async function run(argv) {
|
|
942
|
+
const [command = "help", ...args] = argv;
|
|
943
|
+
if (command === "help" || command === "--help" || command === "-h") {
|
|
944
|
+
process.stdout.write(`${usage()}
|
|
945
|
+
`);
|
|
946
|
+
return 0;
|
|
947
|
+
}
|
|
948
|
+
if (command === "version" || command === "--version" || command === "-V") {
|
|
949
|
+
process.stdout.write(`${VERSION}
|
|
950
|
+
`);
|
|
951
|
+
return 0;
|
|
952
|
+
}
|
|
953
|
+
const platform = `${process.platform}-${process.arch}`;
|
|
954
|
+
if (platform !== "linux-x64")
|
|
955
|
+
throw new Error(`bayma runs on Linux (x64) with Docker, and this is ${platform}`);
|
|
956
|
+
const context = processContext();
|
|
957
|
+
if (LAUNCHED.includes(command))
|
|
958
|
+
return launch(context, command, args);
|
|
959
|
+
switch (command) {
|
|
960
|
+
case "init":
|
|
961
|
+
case "upgrade":
|
|
962
|
+
expectArgs(command, args, []);
|
|
963
|
+
return setUp(context, command === "upgrade");
|
|
964
|
+
case "status":
|
|
965
|
+
expectArgs(command, args, []);
|
|
966
|
+
return showStatus(context);
|
|
967
|
+
case "uninstall":
|
|
968
|
+
expectArgs(command, args, ["--purge"]);
|
|
969
|
+
return uninstall(context, args.includes("--purge"));
|
|
970
|
+
default:
|
|
971
|
+
process.stderr.write(`${usage()}
|
|
972
|
+
`);
|
|
973
|
+
return 2;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
var status;
|
|
977
|
+
try {
|
|
978
|
+
status = await run(process.argv.slice(2));
|
|
979
|
+
} catch (error) {
|
|
980
|
+
process.stderr.write(`bayma: ${error instanceof Error ? error.message : String(error)}
|
|
981
|
+
`);
|
|
982
|
+
status = 1;
|
|
983
|
+
}
|
|
984
|
+
process.exit(status);
|
package/package.json
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bayma-repl",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.12.2",
|
|
4
|
+
"description": "Set up bayma, a durable engine for REPL sessions, and launch its MCP servers in Docker: npx bayma-repl init",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"bayma": "dist/bayma.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/bayma.js"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=22"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/eaucoin/bayma.git",
|
|
19
|
+
"directory": "packages/cli"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/eaucoin/bayma"
|
|
22
|
+
}
|
package/README.md
DELETED