@lesscap/baton-cli 0.7.5 → 0.7.7
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/baton.mjs +438 -53
- package/package.json +1 -1
package/dist/baton.mjs
CHANGED
|
@@ -4815,7 +4815,7 @@ var viewWorker = (cfg) => {
|
|
|
4815
4815
|
apiToken: worker2.apiToken
|
|
4816
4816
|
};
|
|
4817
4817
|
};
|
|
4818
|
-
var worktreeConfig = (cfg) => ({
|
|
4818
|
+
var worktreeConfig = (cfg, session2) => ({
|
|
4819
4819
|
server: cfg.server,
|
|
4820
4820
|
project: cfg.projectId,
|
|
4821
4821
|
baseBranch: cfg.baseBranch,
|
|
@@ -4825,7 +4825,8 @@ var worktreeConfig = (cfg) => ({
|
|
|
4825
4825
|
agentKind: cfg.agentKind,
|
|
4826
4826
|
machineId: cfg.machineId,
|
|
4827
4827
|
apiToken: cfg.apiToken
|
|
4828
|
-
}
|
|
4828
|
+
},
|
|
4829
|
+
...session2 === void 0 ? {} : { session: session2 }
|
|
4829
4830
|
});
|
|
4830
4831
|
|
|
4831
4832
|
// src/config.ts
|
|
@@ -4990,6 +4991,19 @@ var withRetry = async (fn, opts = {}) => {
|
|
|
4990
4991
|
throw lastErr;
|
|
4991
4992
|
};
|
|
4992
4993
|
|
|
4994
|
+
// src/client/services.ts
|
|
4995
|
+
var servicesClient = (baseUrl) => {
|
|
4996
|
+
const u = (path) => `${baseUrl}${path}`;
|
|
4997
|
+
return {
|
|
4998
|
+
listByProject: (projectId) => request(u(`/projects/${projectId}/services`), { method: "GET" }),
|
|
4999
|
+
run: (workerId, input) => request(u(`/workers/${workerId}/services`), { method: "POST", body: input }),
|
|
5000
|
+
stop: (workerId, name) => request(u(`/workers/${workerId}/services/${encodeURIComponent(name)}`), {
|
|
5001
|
+
method: "DELETE"
|
|
5002
|
+
}),
|
|
5003
|
+
report: (input) => request(u("/workers/me/services"), { method: "PUT", body: input })
|
|
5004
|
+
};
|
|
5005
|
+
};
|
|
5006
|
+
|
|
4993
5007
|
// src/client/sessions.ts
|
|
4994
5008
|
var sessionsClient = (baseUrl) => {
|
|
4995
5009
|
const u = (p) => `${baseUrl}${p}`;
|
|
@@ -5113,6 +5127,7 @@ var clientFromBase = (baseUrl) => ({
|
|
|
5113
5127
|
requirements: requirementClient(baseUrl),
|
|
5114
5128
|
tasks: taskClient(baseUrl),
|
|
5115
5129
|
sessions: sessionsClient(baseUrl),
|
|
5130
|
+
services: servicesClient(baseUrl),
|
|
5116
5131
|
loops: loopsClient(baseUrl),
|
|
5117
5132
|
workers: workersClient(baseUrl)
|
|
5118
5133
|
});
|
|
@@ -5375,6 +5390,20 @@ var labelAttachments = (attachments) => {
|
|
|
5375
5390
|
// ../shared/src/domain/channel.ts
|
|
5376
5391
|
var isMessageFor = (msg, name) => !msg.to || msg.to.length === 0 || msg.to.includes(name);
|
|
5377
5392
|
|
|
5393
|
+
// ../shared/src/domain/service.ts
|
|
5394
|
+
var SERVICE_PUBLIC_URL_MAX_LENGTH = 2048;
|
|
5395
|
+
var SERVICE_NOTE_MAX_LENGTH = 500;
|
|
5396
|
+
var isServicePublicUrl = (value) => {
|
|
5397
|
+
if (value.length === 0 || value.length > SERVICE_PUBLIC_URL_MAX_LENGTH) return false;
|
|
5398
|
+
try {
|
|
5399
|
+
const url = new URL(value);
|
|
5400
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
5401
|
+
} catch {
|
|
5402
|
+
return false;
|
|
5403
|
+
}
|
|
5404
|
+
};
|
|
5405
|
+
var isServiceNote = (value) => value.length > 0 && value.length <= SERVICE_NOTE_MAX_LENGTH;
|
|
5406
|
+
|
|
5378
5407
|
// ../shared/src/domain/session-event.ts
|
|
5379
5408
|
var agentMessageText = (payload) => {
|
|
5380
5409
|
if (typeof payload !== "object" || payload === null) return null;
|
|
@@ -6251,6 +6280,173 @@ var requirement = defineCommand({
|
|
|
6251
6280
|
}
|
|
6252
6281
|
});
|
|
6253
6282
|
|
|
6283
|
+
// src/worker/service-child.ts
|
|
6284
|
+
import { spawn } from "node:child_process";
|
|
6285
|
+
|
|
6286
|
+
// src/worker/proc.ts
|
|
6287
|
+
var killProcessGroup = (proc) => {
|
|
6288
|
+
if (proc.pid === void 0) return;
|
|
6289
|
+
killPidGroup(proc.pid);
|
|
6290
|
+
};
|
|
6291
|
+
var killPidGroup = (pid, signal = "SIGTERM") => {
|
|
6292
|
+
try {
|
|
6293
|
+
process.kill(-pid, signal);
|
|
6294
|
+
} catch {
|
|
6295
|
+
}
|
|
6296
|
+
};
|
|
6297
|
+
|
|
6298
|
+
// src/worker/service-child.ts
|
|
6299
|
+
var isStart = (message) => {
|
|
6300
|
+
if (typeof message !== "object" || message === null) return false;
|
|
6301
|
+
const value = message;
|
|
6302
|
+
return value.type === "start" && typeof value.cwd === "string" && Array.isArray(value.argv) && value.argv.length > 0 && value.argv.every((arg) => typeof arg === "string");
|
|
6303
|
+
};
|
|
6304
|
+
var isStop = (message) => typeof message === "object" && message !== null && message.type === "stop";
|
|
6305
|
+
var send = (message) => {
|
|
6306
|
+
if (process.connected) process.send?.(message);
|
|
6307
|
+
};
|
|
6308
|
+
var runServiceChild = () => {
|
|
6309
|
+
let app = null;
|
|
6310
|
+
let killTimer = null;
|
|
6311
|
+
let stopping = false;
|
|
6312
|
+
const stop = () => {
|
|
6313
|
+
if (stopping) return;
|
|
6314
|
+
stopping = true;
|
|
6315
|
+
if (!app?.pid) {
|
|
6316
|
+
process.exit(0);
|
|
6317
|
+
return;
|
|
6318
|
+
}
|
|
6319
|
+
killPidGroup(app.pid);
|
|
6320
|
+
killTimer = setTimeout(() => {
|
|
6321
|
+
if (app?.pid) killPidGroup(app.pid, "SIGKILL");
|
|
6322
|
+
}, 5e3);
|
|
6323
|
+
killTimer.unref();
|
|
6324
|
+
};
|
|
6325
|
+
process.on("disconnect", stop);
|
|
6326
|
+
process.on("SIGINT", stop);
|
|
6327
|
+
process.on("SIGTERM", stop);
|
|
6328
|
+
process.on("message", (message) => {
|
|
6329
|
+
if (isStop(message)) return stop();
|
|
6330
|
+
if (!isStart(message) || app) return;
|
|
6331
|
+
const [command, ...args] = message.argv;
|
|
6332
|
+
app = spawn(command ?? "", args, {
|
|
6333
|
+
cwd: message.cwd,
|
|
6334
|
+
detached: true,
|
|
6335
|
+
stdio: "inherit",
|
|
6336
|
+
env: process.env
|
|
6337
|
+
});
|
|
6338
|
+
app.once("spawn", () => send({ type: "started", pid: app?.pid }));
|
|
6339
|
+
app.once("error", (error) => {
|
|
6340
|
+
send({ type: "error", error: error.message });
|
|
6341
|
+
process.exit(1);
|
|
6342
|
+
});
|
|
6343
|
+
app.once("exit", (code, signal) => {
|
|
6344
|
+
if (killTimer) clearTimeout(killTimer);
|
|
6345
|
+
process.exit(code ?? (signal ? 1 : 0));
|
|
6346
|
+
});
|
|
6347
|
+
});
|
|
6348
|
+
};
|
|
6349
|
+
var serviceChildCommand = defineCommand({
|
|
6350
|
+
meta: { name: "child", description: "internal service process wrapper", hidden: true },
|
|
6351
|
+
run: runServiceChild
|
|
6352
|
+
});
|
|
6353
|
+
|
|
6354
|
+
// src/commands/service.ts
|
|
6355
|
+
var serviceHandle = (service2) => `W-${service2.workerId}/${service2.name}`;
|
|
6356
|
+
var formatUptime = (startedAt, now = Date.now()) => {
|
|
6357
|
+
const seconds = Math.max(0, Math.floor((now - startedAt) / 1e3));
|
|
6358
|
+
if (seconds >= 86400) return `${Math.floor(seconds / 86400)}d`;
|
|
6359
|
+
if (seconds >= 3600) return `${Math.floor(seconds / 3600)}h`;
|
|
6360
|
+
if (seconds >= 60) return `${Math.floor(seconds / 60)}m`;
|
|
6361
|
+
return `${seconds}s`;
|
|
6362
|
+
};
|
|
6363
|
+
var currentContext = () => {
|
|
6364
|
+
const config = loadProjectConfig(projectConfigPath());
|
|
6365
|
+
if (!config.worker || !config.session)
|
|
6366
|
+
throw new Error("service run requires a Baton session worktree");
|
|
6367
|
+
return { workerId: config.worker.id, sessionId: config.session };
|
|
6368
|
+
};
|
|
6369
|
+
var currentWorker = () => {
|
|
6370
|
+
const worker2 = loadProjectConfig(projectConfigPath()).worker;
|
|
6371
|
+
if (!worker2) throw new Error("no worker in scope");
|
|
6372
|
+
return worker2.id;
|
|
6373
|
+
};
|
|
6374
|
+
var parseTarget = (target) => {
|
|
6375
|
+
const explicit = target.match(/^W-(\d+)\/(.+)$/i);
|
|
6376
|
+
return explicit ? { workerId: Number(explicit[1]), name: explicit[2] ?? "" } : { workerId: currentWorker(), name: target };
|
|
6377
|
+
};
|
|
6378
|
+
var runCommand2 = defineCommand({
|
|
6379
|
+
meta: { name: "run", description: "run a service on the current worker" },
|
|
6380
|
+
args: {
|
|
6381
|
+
name: { type: "positional", required: true, description: "service name" },
|
|
6382
|
+
"public-url": { type: "string", description: "public HTTP(S) URL for this service" },
|
|
6383
|
+
note: { type: "string", description: "short note describing this service" },
|
|
6384
|
+
...common
|
|
6385
|
+
},
|
|
6386
|
+
run: async ({ args, rawArgs }) => {
|
|
6387
|
+
const separator = rawArgs.indexOf("--");
|
|
6388
|
+
const argv = separator === -1 ? [] : rawArgs.slice(separator + 1);
|
|
6389
|
+
if (argv.length === 0) throw new Error("command required after `--`");
|
|
6390
|
+
const publicUrl = args["public-url"]?.trim() || void 0;
|
|
6391
|
+
const note = args.note?.trim() || void 0;
|
|
6392
|
+
if (publicUrl && !isServicePublicUrl(publicUrl))
|
|
6393
|
+
throw new Error("--public-url must be an absolute HTTP(S) URL");
|
|
6394
|
+
if (note && !isServiceNote(note))
|
|
6395
|
+
throw new Error("service note must be 500 characters or fewer");
|
|
6396
|
+
const context = currentContext();
|
|
6397
|
+
const result = await clientFor(args).services.run(context.workerId, {
|
|
6398
|
+
sessionId: context.sessionId,
|
|
6399
|
+
name: args.name,
|
|
6400
|
+
argv,
|
|
6401
|
+
...publicUrl ? { publicUrl } : {},
|
|
6402
|
+
...note ? { note } : {}
|
|
6403
|
+
});
|
|
6404
|
+
if (!result.ok) throw new Error(result.error);
|
|
6405
|
+
if (args.json) return console.log(toJson(result));
|
|
6406
|
+
console.log(`running W-${context.workerId}/${args.name}`);
|
|
6407
|
+
if (publicUrl) console.log(`public: ${publicUrl}`);
|
|
6408
|
+
if (result.logPath) console.log(`logs: ${result.logPath}`);
|
|
6409
|
+
}
|
|
6410
|
+
});
|
|
6411
|
+
var lsCommand = defineCommand({
|
|
6412
|
+
meta: { name: "ls", description: "list live services in the current project" },
|
|
6413
|
+
args: common,
|
|
6414
|
+
run: async ({ args }) => {
|
|
6415
|
+
const client = clientFor(args);
|
|
6416
|
+
const projectId = resolveProjectId({});
|
|
6417
|
+
const services = await client.services.listByProject(projectId);
|
|
6418
|
+
if (args.json) return console.log(toJson(services));
|
|
6419
|
+
if (services.length === 0) return console.log("(none)");
|
|
6420
|
+
const workers = await client.workers.listByProject(projectId);
|
|
6421
|
+
const names = new Map(workers.map((worker2) => [worker2.id, worker2.name]));
|
|
6422
|
+
services.sort((a, b) => a.workerId - b.workerId || a.name.localeCompare(b.name)).map(
|
|
6423
|
+
(item) => `${serviceHandle(item)} ${names.get(item.workerId) ?? "unknown"} session=${item.sessionId} uptime=${formatUptime(item.startedAt)}`
|
|
6424
|
+
).forEach((line) => {
|
|
6425
|
+
console.log(line);
|
|
6426
|
+
});
|
|
6427
|
+
}
|
|
6428
|
+
});
|
|
6429
|
+
var stopCommand = defineCommand({
|
|
6430
|
+
meta: { name: "stop", description: "stop and remove a live service" },
|
|
6431
|
+
args: {
|
|
6432
|
+
service: {
|
|
6433
|
+
type: "positional",
|
|
6434
|
+
required: true,
|
|
6435
|
+
description: "service name, or W-N/name"
|
|
6436
|
+
},
|
|
6437
|
+
...common
|
|
6438
|
+
},
|
|
6439
|
+
run: async ({ args }) => {
|
|
6440
|
+
const target = parseTarget(args.service);
|
|
6441
|
+
await clientFor(args).services.stop(target.workerId, target.name);
|
|
6442
|
+
console.log(args.json ? toJson({ ok: true, service: args.service }) : `stopped ${args.service}`);
|
|
6443
|
+
}
|
|
6444
|
+
});
|
|
6445
|
+
var service = defineCommand({
|
|
6446
|
+
meta: { name: "service", description: "run and stop worker-managed development services" },
|
|
6447
|
+
subCommands: { run: runCommand2, ls: lsCommand, stop: stopCommand, child: serviceChildCommand }
|
|
6448
|
+
});
|
|
6449
|
+
|
|
6254
6450
|
// src/commands/session/abort.ts
|
|
6255
6451
|
var sessionAbortCommand = defineCommand({
|
|
6256
6452
|
meta: {
|
|
@@ -7777,21 +7973,26 @@ var HEARTBEAT_TIMEOUT_MS = 15e3;
|
|
|
7777
7973
|
var HEARTBEAT_DEAD_MS = 15e4;
|
|
7778
7974
|
var STREAM_STALE_MS = 9e4;
|
|
7779
7975
|
var createLiveness = (deps) => {
|
|
7780
|
-
const {
|
|
7976
|
+
const { heartbeat, log, isStreamOpen, reportRuntime, onTrip } = deps;
|
|
7781
7977
|
let heartbeatOkAt = Date.now();
|
|
7782
7978
|
let streamOkAt = Date.now();
|
|
7783
7979
|
let timer = null;
|
|
7784
7980
|
const ping = async () => {
|
|
7981
|
+
if (reportRuntime)
|
|
7982
|
+
void reportRuntime().catch((error) => log(`service report failed: ${String(error)}`));
|
|
7983
|
+
let timeout;
|
|
7785
7984
|
try {
|
|
7786
7985
|
await Promise.race([
|
|
7787
|
-
|
|
7788
|
-
new Promise(
|
|
7789
|
-
|
|
7790
|
-
)
|
|
7986
|
+
heartbeat(),
|
|
7987
|
+
new Promise((_, reject) => {
|
|
7988
|
+
timeout = setTimeout(() => reject(new Error("heartbeat timeout")), HEARTBEAT_TIMEOUT_MS);
|
|
7989
|
+
})
|
|
7791
7990
|
]);
|
|
7792
7991
|
heartbeatOkAt = Date.now();
|
|
7793
7992
|
} catch (e) {
|
|
7794
7993
|
log(`heartbeat failed: ${String(e)}`);
|
|
7994
|
+
} finally {
|
|
7995
|
+
if (timeout) clearTimeout(timeout);
|
|
7795
7996
|
}
|
|
7796
7997
|
};
|
|
7797
7998
|
const tick = () => {
|
|
@@ -7818,20 +8019,195 @@ var createLiveness = (deps) => {
|
|
|
7818
8019
|
};
|
|
7819
8020
|
};
|
|
7820
8021
|
|
|
8022
|
+
// src/worker/service-supervisor.ts
|
|
8023
|
+
import { mkdirSync as mkdirSync4 } from "node:fs";
|
|
8024
|
+
import { join as join8 } from "node:path";
|
|
8025
|
+
|
|
8026
|
+
// src/worker/service-process.ts
|
|
8027
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
8028
|
+
import { closeSync, existsSync as existsSync5, openSync } from "node:fs";
|
|
8029
|
+
import { dirname as dirname2, join as join7 } from "node:path";
|
|
8030
|
+
import { fileURLToPath } from "node:url";
|
|
8031
|
+
var START_TIMEOUT_MS = 1e4;
|
|
8032
|
+
var isWrapperMessage = (message) => {
|
|
8033
|
+
if (typeof message !== "object" || message === null) return false;
|
|
8034
|
+
const value = message;
|
|
8035
|
+
if (value.type === "started") return typeof value.pid === "number";
|
|
8036
|
+
return value.type === "error" && typeof value.error === "string";
|
|
8037
|
+
};
|
|
8038
|
+
var binPath = () => {
|
|
8039
|
+
const here = dirname2(fileURLToPath(import.meta.url));
|
|
8040
|
+
const devShim = join7(here, "..", "..", "bin", "baton.mjs");
|
|
8041
|
+
return existsSync5(devShim) ? devShim : fileURLToPath(import.meta.url);
|
|
8042
|
+
};
|
|
8043
|
+
var startServiceProcess = (input) => {
|
|
8044
|
+
const logFd = openSync(input.logPath, "a");
|
|
8045
|
+
let wrapper;
|
|
8046
|
+
try {
|
|
8047
|
+
wrapper = spawn2(process.execPath, [binPath(), "service", "child"], {
|
|
8048
|
+
detached: false,
|
|
8049
|
+
stdio: ["ignore", logFd, logFd, "ipc"],
|
|
8050
|
+
env: process.env
|
|
8051
|
+
});
|
|
8052
|
+
} finally {
|
|
8053
|
+
closeSync(logFd);
|
|
8054
|
+
}
|
|
8055
|
+
const started = new Promise((resolve2, reject) => {
|
|
8056
|
+
let settled = false;
|
|
8057
|
+
let timer;
|
|
8058
|
+
const cleanup = () => {
|
|
8059
|
+
clearTimeout(timer);
|
|
8060
|
+
wrapper.off("message", onMessage);
|
|
8061
|
+
wrapper.off("error", onError);
|
|
8062
|
+
wrapper.off("exit", onExit);
|
|
8063
|
+
};
|
|
8064
|
+
const finish = (result) => {
|
|
8065
|
+
if (settled) return;
|
|
8066
|
+
settled = true;
|
|
8067
|
+
cleanup();
|
|
8068
|
+
if (result instanceof Error) reject(result);
|
|
8069
|
+
else resolve2(result);
|
|
8070
|
+
};
|
|
8071
|
+
const onMessage = (message) => {
|
|
8072
|
+
if (!isWrapperMessage(message)) return;
|
|
8073
|
+
if (message.type === "started") finish(message.pid);
|
|
8074
|
+
else finish(new Error(message.error));
|
|
8075
|
+
};
|
|
8076
|
+
const onError = (error) => finish(error);
|
|
8077
|
+
const onExit = (code) => finish(new Error(`service wrapper exited before start (code=${code ?? -1})`));
|
|
8078
|
+
wrapper.on("message", onMessage);
|
|
8079
|
+
wrapper.once("error", onError);
|
|
8080
|
+
wrapper.once("exit", onExit);
|
|
8081
|
+
timer = setTimeout(() => {
|
|
8082
|
+
if (wrapper.connected) wrapper.send({ type: "stop" });
|
|
8083
|
+
finish(new Error("service wrapper did not start in time"));
|
|
8084
|
+
}, START_TIMEOUT_MS);
|
|
8085
|
+
timer.unref();
|
|
8086
|
+
wrapper.send({ type: "start", argv: input.argv, cwd: input.cwd }, (error) => {
|
|
8087
|
+
if (error) finish(error);
|
|
8088
|
+
});
|
|
8089
|
+
});
|
|
8090
|
+
return { wrapper, started };
|
|
8091
|
+
};
|
|
8092
|
+
|
|
8093
|
+
// src/worker/service-supervisor.ts
|
|
8094
|
+
var waitForExit = (child) => child.exitCode !== null ? Promise.resolve() : new Promise((resolve2) => child.once("exit", () => resolve2()));
|
|
8095
|
+
var createServiceSupervisor = (deps) => {
|
|
8096
|
+
const { getSession, sendReport, cfg, log } = deps;
|
|
8097
|
+
const entries = /* @__PURE__ */ new Map();
|
|
8098
|
+
let reportQueue = Promise.resolve();
|
|
8099
|
+
const snapshot = () => [...entries.values()].filter((entry) => entry.appPid !== void 0).map(({ sessionId, name, startedAt, publicUrl, note }) => ({
|
|
8100
|
+
sessionId,
|
|
8101
|
+
name,
|
|
8102
|
+
startedAt,
|
|
8103
|
+
...publicUrl ? { publicUrl } : {},
|
|
8104
|
+
...note ? { note } : {}
|
|
8105
|
+
}));
|
|
8106
|
+
const report = (result) => {
|
|
8107
|
+
const body = { services: snapshot(), ...result ? { result } : {} };
|
|
8108
|
+
const request2 = reportQueue.then(() => sendReport(body)).then(() => {
|
|
8109
|
+
});
|
|
8110
|
+
reportQueue = request2.catch(() => {
|
|
8111
|
+
});
|
|
8112
|
+
return request2;
|
|
8113
|
+
};
|
|
8114
|
+
const remove = (entry) => {
|
|
8115
|
+
if (entries.get(entry.name) !== entry) return;
|
|
8116
|
+
entries.delete(entry.name);
|
|
8117
|
+
void report().catch(() => {
|
|
8118
|
+
});
|
|
8119
|
+
};
|
|
8120
|
+
const stopEntry = (entry) => {
|
|
8121
|
+
if (entry.wrapper.connected) {
|
|
8122
|
+
entry.wrapper.send({ type: "stop" });
|
|
8123
|
+
return;
|
|
8124
|
+
}
|
|
8125
|
+
if (entry.appPid) killPidGroup(entry.appPid, "SIGKILL");
|
|
8126
|
+
};
|
|
8127
|
+
const runOne = async (requestId, sessionId, name, argv, publicUrl, note) => {
|
|
8128
|
+
if (entries.has(name))
|
|
8129
|
+
return report({
|
|
8130
|
+
requestId,
|
|
8131
|
+
ok: false,
|
|
8132
|
+
error: `service "${name}" is already running`,
|
|
8133
|
+
status: 409
|
|
8134
|
+
});
|
|
8135
|
+
const session2 = await getSession(sessionId);
|
|
8136
|
+
if (session2.workerId !== cfg.workerId || !session2.worktreePath)
|
|
8137
|
+
return report({
|
|
8138
|
+
requestId,
|
|
8139
|
+
ok: false,
|
|
8140
|
+
error: "session is not materialized on this worker",
|
|
8141
|
+
status: 400
|
|
8142
|
+
});
|
|
8143
|
+
const logDir = join8(session2.worktreePath, ".baton-services");
|
|
8144
|
+
const logPath = join8(logDir, `${name}.log`);
|
|
8145
|
+
mkdirSync4(logDir, { recursive: true });
|
|
8146
|
+
const { wrapper, started } = startServiceProcess({ argv, cwd: session2.worktreePath, logPath });
|
|
8147
|
+
const entry = {
|
|
8148
|
+
wrapper,
|
|
8149
|
+
sessionId,
|
|
8150
|
+
name,
|
|
8151
|
+
startedAt: Date.now(),
|
|
8152
|
+
publicUrl,
|
|
8153
|
+
note
|
|
8154
|
+
};
|
|
8155
|
+
entries.set(name, entry);
|
|
8156
|
+
wrapper.once("exit", (code) => {
|
|
8157
|
+
log(`service ${name} exited (code=${code ?? -1})`);
|
|
8158
|
+
remove(entry);
|
|
8159
|
+
});
|
|
8160
|
+
try {
|
|
8161
|
+
entry.appPid = await started;
|
|
8162
|
+
} catch (error) {
|
|
8163
|
+
remove(entry);
|
|
8164
|
+
return report({ requestId, ok: false, error: String(error), status: 500 });
|
|
8165
|
+
}
|
|
8166
|
+
const service2 = {
|
|
8167
|
+
workerId: cfg.workerId,
|
|
8168
|
+
sessionId,
|
|
8169
|
+
name,
|
|
8170
|
+
startedAt: entry.startedAt,
|
|
8171
|
+
...publicUrl ? { publicUrl } : {},
|
|
8172
|
+
...note ? { note } : {}
|
|
8173
|
+
};
|
|
8174
|
+
log(`service ${name} started in ${session2.worktreePath}`);
|
|
8175
|
+
await report({ requestId, ok: true, service: service2, logPath });
|
|
8176
|
+
};
|
|
8177
|
+
const run = async (requestId, sessionId, name, argv, publicUrl, note) => {
|
|
8178
|
+
try {
|
|
8179
|
+
await runOne(requestId, sessionId, name, argv, publicUrl, note);
|
|
8180
|
+
} catch (error) {
|
|
8181
|
+
await report({ requestId, ok: false, error: String(error), status: 500 });
|
|
8182
|
+
}
|
|
8183
|
+
};
|
|
8184
|
+
const stop = async (requestId, name) => {
|
|
8185
|
+
const entry = entries.get(name);
|
|
8186
|
+
if (!entry)
|
|
8187
|
+
return report({ requestId, ok: false, error: `service "${name}" not found`, status: 404 });
|
|
8188
|
+
stopEntry(entry);
|
|
8189
|
+
await waitForExit(entry.wrapper);
|
|
8190
|
+
remove(entry);
|
|
8191
|
+
await report({ requestId, ok: true });
|
|
8192
|
+
};
|
|
8193
|
+
const stopSession = async (sessionId) => {
|
|
8194
|
+
const matches = [...entries.values()].filter((entry) => entry.sessionId === sessionId);
|
|
8195
|
+
matches.forEach(stopEntry);
|
|
8196
|
+
await Promise.all(matches.map((entry) => waitForExit(entry.wrapper)));
|
|
8197
|
+
matches.forEach(remove);
|
|
8198
|
+
};
|
|
8199
|
+
const killAll = () => entries.forEach(stopEntry);
|
|
8200
|
+
return { run, stop, stopSession, report, killAll };
|
|
8201
|
+
};
|
|
8202
|
+
|
|
7821
8203
|
// src/worker/session-supervisor.ts
|
|
7822
|
-
import { spawn } from "node:child_process";
|
|
8204
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
7823
8205
|
import { randomUUID } from "node:crypto";
|
|
7824
|
-
import { existsSync as
|
|
7825
|
-
import { dirname as
|
|
7826
|
-
import { fileURLToPath } from "node:url";
|
|
8206
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
8207
|
+
import { dirname as dirname3, join as join10 } from "node:path";
|
|
8208
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
7827
8209
|
import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
|
|
7828
8210
|
|
|
7829
|
-
// src/session/runner/title-codex.ts
|
|
7830
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
7831
|
-
import { tmpdir as tmpdir3 } from "node:os";
|
|
7832
|
-
import { join as join7 } from "node:path";
|
|
7833
|
-
import { Codex as Codex2 } from "@openai/codex-sdk";
|
|
7834
|
-
|
|
7835
8211
|
// src/session/runner/title.ts
|
|
7836
8212
|
var clip = (s, max) => s.length > max ? s.slice(0, max) : s;
|
|
7837
8213
|
var isDeclined = (title) => title.replace(/[^a-z]/gi, "").toUpperCase() === "NONE";
|
|
@@ -7920,6 +8296,10 @@ var generateTitle = async (input) => {
|
|
|
7920
8296
|
};
|
|
7921
8297
|
|
|
7922
8298
|
// src/session/runner/title-codex.ts
|
|
8299
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
8300
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
8301
|
+
import { join as join9 } from "node:path";
|
|
8302
|
+
import { Codex as Codex2 } from "@openai/codex-sdk";
|
|
7923
8303
|
var generateTitleWithCodex = async (input) => {
|
|
7924
8304
|
const { userText, assistantText } = input;
|
|
7925
8305
|
if (`${userText}${assistantText}`.trim().length < 4) return { kind: "declined" };
|
|
@@ -7927,7 +8307,7 @@ var generateTitleWithCodex = async (input) => {
|
|
|
7927
8307
|
const timer = setTimeout(() => abort.abort(), TITLE_TIMEOUT_MS);
|
|
7928
8308
|
let cwd = "";
|
|
7929
8309
|
try {
|
|
7930
|
-
cwd = await mkdtemp(
|
|
8310
|
+
cwd = await mkdtemp(join9(tmpdir3(), "baton-title-"));
|
|
7931
8311
|
const client = input.client ?? new Codex2();
|
|
7932
8312
|
const thread = client.startThread({
|
|
7933
8313
|
workingDirectory: cwd,
|
|
@@ -7953,20 +8333,11 @@ var generateTitleWithCodex = async (input) => {
|
|
|
7953
8333
|
}
|
|
7954
8334
|
};
|
|
7955
8335
|
|
|
7956
|
-
// src/worker/proc.ts
|
|
7957
|
-
var killProcessGroup = (proc) => {
|
|
7958
|
-
if (proc.pid === void 0) return;
|
|
7959
|
-
try {
|
|
7960
|
-
process.kill(-proc.pid, "SIGTERM");
|
|
7961
|
-
} catch {
|
|
7962
|
-
}
|
|
7963
|
-
};
|
|
7964
|
-
|
|
7965
8336
|
// src/worker/session-supervisor.ts
|
|
7966
|
-
var
|
|
7967
|
-
const here =
|
|
7968
|
-
const devShim =
|
|
7969
|
-
return
|
|
8337
|
+
var binPath2 = () => {
|
|
8338
|
+
const here = dirname3(fileURLToPath2(import.meta.url));
|
|
8339
|
+
const devShim = join10(here, "..", "..", "bin", "baton.mjs");
|
|
8340
|
+
return existsSync6(devShim) ? devShim : fileURLToPath2(import.meta.url);
|
|
7970
8341
|
};
|
|
7971
8342
|
var createSessionSupervisor = (deps) => {
|
|
7972
8343
|
const { client, cfg, repo, log, hasTerminal, closeTerminal } = deps;
|
|
@@ -7993,7 +8364,7 @@ var createSessionSupervisor = (deps) => {
|
|
|
7993
8364
|
};
|
|
7994
8365
|
const spawnChild = (sessionId, worktreePath) => {
|
|
7995
8366
|
if (children.has(sessionId)) return;
|
|
7996
|
-
const child =
|
|
8367
|
+
const child = spawn3(process.execPath, [binPath2(), "session", "run", String(sessionId)], {
|
|
7997
8368
|
detached: true,
|
|
7998
8369
|
stdio: "inherit",
|
|
7999
8370
|
env: { ...process.env, BATON_SERVER: cfg.server, BATON_WORKER_TOKEN: cfg.apiToken }
|
|
@@ -8019,7 +8390,7 @@ var createSessionSupervisor = (deps) => {
|
|
|
8019
8390
|
if (!session2.agentSessionId || !worktreePath) {
|
|
8020
8391
|
const sessionCode = randomUUID();
|
|
8021
8392
|
const agentSessionId = session2.agentKind === "codex" ? `pending:${sessionCode}` : sessionCode;
|
|
8022
|
-
worktreePath =
|
|
8393
|
+
worktreePath = join10(worktreeDir, slug(`${name}-${sessionCode.slice(0, 8)}`));
|
|
8023
8394
|
const base = await syncedBase();
|
|
8024
8395
|
if (wasCanceled()) return log(`session #${sessionId} start canceled during git sync`);
|
|
8025
8396
|
createWorktree({
|
|
@@ -8031,13 +8402,14 @@ var createSessionSupervisor = (deps) => {
|
|
|
8031
8402
|
await client.sessions.materialize(sessionId, { agentSessionId, worktreePath }, cfg.apiToken);
|
|
8032
8403
|
if (wasCanceled()) return log(`session #${sessionId} start canceled after materializing`);
|
|
8033
8404
|
log(`materialized session #${sessionId} \u2192 ${worktreePath}`);
|
|
8034
|
-
} else if (!
|
|
8405
|
+
} else if (!existsSync6(worktreePath)) {
|
|
8035
8406
|
await restoreWorktree(repo, worktreePath, session2.agentSessionId.slice(0, 8), baseBranch);
|
|
8036
8407
|
if (wasCanceled()) return log(`session #${sessionId} start canceled during worktree restore`);
|
|
8037
8408
|
log(`recreated session #${sessionId} worktree (was missing) \u2192 ${worktreePath}`);
|
|
8038
8409
|
}
|
|
8039
8410
|
ensureExcluded(repo, PROJECT_CONFIG_NAME);
|
|
8040
|
-
|
|
8411
|
+
ensureExcluded(repo, ".baton-services/");
|
|
8412
|
+
saveProjectConfig(join10(worktreePath, PROJECT_CONFIG_NAME), worktreeConfig(cfg, sessionId));
|
|
8041
8413
|
if (hasTerminal(sessionId))
|
|
8042
8414
|
return log(`session #${sessionId} terminal opened mid-start \u2014 skipping headless start`);
|
|
8043
8415
|
spawnChild(sessionId, worktreePath);
|
|
@@ -8125,8 +8497,8 @@ var createSessionSupervisor = (deps) => {
|
|
|
8125
8497
|
};
|
|
8126
8498
|
|
|
8127
8499
|
// src/worker/terminal-manager.ts
|
|
8128
|
-
import { existsSync as
|
|
8129
|
-
import { spawn as
|
|
8500
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
8501
|
+
import { spawn as spawn4 } from "node-pty";
|
|
8130
8502
|
|
|
8131
8503
|
// ../../node_modules/.pnpm/ws@8.21.0/node_modules/ws/wrapper.mjs
|
|
8132
8504
|
var import_stream2 = __toESM(require_stream(), 1);
|
|
@@ -8139,16 +8511,16 @@ var import_websocket = __toESM(require_websocket(), 1);
|
|
|
8139
8511
|
var import_websocket_server = __toESM(require_websocket_server(), 1);
|
|
8140
8512
|
|
|
8141
8513
|
// src/worker/pty.ts
|
|
8142
|
-
import { existsSync as
|
|
8514
|
+
import { existsSync as existsSync7, readdirSync as readdirSync2 } from "node:fs";
|
|
8143
8515
|
import { homedir as homedir3 } from "node:os";
|
|
8144
|
-
import { join as
|
|
8516
|
+
import { join as join11 } from "node:path";
|
|
8145
8517
|
var MAX_TERMINALS = 10;
|
|
8146
8518
|
var ptyArgs = (agentSessionId, hasJsonl) => hasJsonl ? ["--resume", agentSessionId] : ["--session-id", agentSessionId];
|
|
8147
8519
|
var hasSessionJsonl = (agentSessionId) => {
|
|
8148
|
-
const root =
|
|
8149
|
-
if (!
|
|
8520
|
+
const root = join11(homedir3(), ".claude", "projects");
|
|
8521
|
+
if (!existsSync7(root)) return false;
|
|
8150
8522
|
for (const dir of readdirSync2(root))
|
|
8151
|
-
if (
|
|
8523
|
+
if (existsSync7(join11(root, dir, `${agentSessionId}.jsonl`))) return true;
|
|
8152
8524
|
return false;
|
|
8153
8525
|
};
|
|
8154
8526
|
var serverTerminalWsUrl = (server, sessionId) => `${server.replace(/^http/, "ws")}/workers/me/terminal/ws?sessionId=${sessionId}`;
|
|
@@ -8159,13 +8531,13 @@ var createTerminalManager = (deps) => {
|
|
|
8159
8531
|
const { cfg, log, hasChild } = deps;
|
|
8160
8532
|
const terminals = /* @__PURE__ */ new Map();
|
|
8161
8533
|
const open = (sessionId, agentSessionId, worktreePath) => {
|
|
8162
|
-
const why = terminals.has(sessionId) ? "already open" : hasChild(sessionId) ? "headless child running" : !
|
|
8534
|
+
const why = terminals.has(sessionId) ? "already open" : hasChild(sessionId) ? "headless child running" : !existsSync8(worktreePath) ? `worktree missing (${worktreePath})` : terminals.size >= MAX_TERMINALS ? `cap ${MAX_TERMINALS} reached` : null;
|
|
8163
8535
|
if (why) {
|
|
8164
8536
|
log(`terminal #${sessionId}: ${why} \u2014 refusing`);
|
|
8165
8537
|
return;
|
|
8166
8538
|
}
|
|
8167
8539
|
const claudeBin = process.env.BATON_CLAUDE_BIN ?? "claude";
|
|
8168
|
-
const term =
|
|
8540
|
+
const term = spawn4(claudeBin, ptyArgs(agentSessionId, hasSessionJsonl(agentSessionId)), {
|
|
8169
8541
|
name: "xterm-color",
|
|
8170
8542
|
cols: 80,
|
|
8171
8543
|
rows: 24,
|
|
@@ -8251,14 +8623,20 @@ var runWorkerDaemon = async (cfg, client, signal) => {
|
|
|
8251
8623
|
log,
|
|
8252
8624
|
hasChild: (id) => supervisor.has(id)
|
|
8253
8625
|
});
|
|
8626
|
+
const services = createServiceSupervisor({
|
|
8627
|
+
getSession: (sessionId) => client.sessions.get(sessionId),
|
|
8628
|
+
sendReport: (report) => client.services.report(report),
|
|
8629
|
+
cfg,
|
|
8630
|
+
log
|
|
8631
|
+
});
|
|
8254
8632
|
let fatal = false;
|
|
8255
8633
|
let stop = () => {
|
|
8256
8634
|
};
|
|
8257
8635
|
const liveness = createLiveness({
|
|
8258
|
-
client,
|
|
8259
|
-
cfg,
|
|
8636
|
+
heartbeat: () => client.workers.heartbeat(cfg.machineId),
|
|
8260
8637
|
log,
|
|
8261
8638
|
isStreamOpen: () => es.readyState === ES_OPEN,
|
|
8639
|
+
reportRuntime: () => services.report(),
|
|
8262
8640
|
onTrip: (reason) => {
|
|
8263
8641
|
if (fatal) return;
|
|
8264
8642
|
log(`${reason} \u2014 exiting so the supervisor restarts a clean worker`);
|
|
@@ -8278,6 +8656,7 @@ var runWorkerDaemon = async (cfg, client, signal) => {
|
|
|
8278
8656
|
es.onopen = () => {
|
|
8279
8657
|
liveness.markStreamOk();
|
|
8280
8658
|
void supervisor.reconcile().catch((err) => log(`reconcile failed: ${String(err)}`));
|
|
8659
|
+
void services.report().catch((err) => log(`service report failed: ${String(err)}`));
|
|
8281
8660
|
};
|
|
8282
8661
|
es.onmessage = (e) => {
|
|
8283
8662
|
liveness.markStreamOk();
|
|
@@ -8286,14 +8665,18 @@ var runWorkerDaemon = async (cfg, client, signal) => {
|
|
|
8286
8665
|
if (cmd.cmd === "session.start")
|
|
8287
8666
|
void supervisor.start(cmd.sessionId, cmd.name).catch((err) => log(`start failed: ${String(err)}`));
|
|
8288
8667
|
else if (cmd.cmd === "session.stop") supervisor.stop(cmd.sessionId);
|
|
8289
|
-
else if (cmd.cmd === "session.delete")
|
|
8668
|
+
else if (cmd.cmd === "session.delete")
|
|
8669
|
+
void services.stopSession(cmd.sessionId).then(() => supervisor.remove(cmd.sessionId, cmd.worktreePath)).catch((err) => log(`service cleanup failed: ${String(err)}`));
|
|
8290
8670
|
else if (cmd.cmd === "session.title")
|
|
8291
8671
|
void supervisor.title(cmd.sessionId, cmd.agentSessionId, cmd.worktreePath).catch((err) => log(`title failed: ${String(err)}`));
|
|
8292
8672
|
else if (cmd.cmd === "session.terminal") {
|
|
8293
8673
|
if (cmd.action === "open")
|
|
8294
8674
|
terminals.open(cmd.sessionId, cmd.agentSessionId, cmd.worktreePath);
|
|
8295
8675
|
else terminals.close(cmd.sessionId);
|
|
8296
|
-
}
|
|
8676
|
+
} else if (cmd.cmd === "service.run")
|
|
8677
|
+
void services.run(cmd.requestId, cmd.sessionId, cmd.name, cmd.argv, cmd.publicUrl, cmd.note).catch((err) => log(`service run failed: ${String(err)}`));
|
|
8678
|
+
else if (cmd.cmd === "service.stop")
|
|
8679
|
+
void services.stop(cmd.requestId, cmd.name).catch((err) => log(`service stop failed: ${String(err)}`));
|
|
8297
8680
|
} catch {
|
|
8298
8681
|
}
|
|
8299
8682
|
};
|
|
@@ -8307,6 +8690,7 @@ var runWorkerDaemon = async (cfg, client, signal) => {
|
|
|
8307
8690
|
});
|
|
8308
8691
|
es.close();
|
|
8309
8692
|
liveness.stop();
|
|
8693
|
+
services.killAll();
|
|
8310
8694
|
supervisor.killAll();
|
|
8311
8695
|
terminals.killAll();
|
|
8312
8696
|
if (fatal) process.exit(1);
|
|
@@ -8314,17 +8698,17 @@ var runWorkerDaemon = async (cfg, client, signal) => {
|
|
|
8314
8698
|
|
|
8315
8699
|
// src/worker/machine-id.ts
|
|
8316
8700
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
8317
|
-
import { existsSync as
|
|
8701
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
|
|
8318
8702
|
import { homedir as homedir4 } from "node:os";
|
|
8319
|
-
import { dirname as
|
|
8320
|
-
var machineIdPath = (env = process.env) =>
|
|
8703
|
+
import { dirname as dirname4, join as join12 } from "node:path";
|
|
8704
|
+
var machineIdPath = (env = process.env) => join12(env.XDG_DATA_HOME ?? join12(env.HOME ?? homedir4(), ".local/share"), "baton", "machine-id");
|
|
8321
8705
|
var readOrCreateMachineId = (path = machineIdPath()) => {
|
|
8322
|
-
if (
|
|
8706
|
+
if (existsSync9(path)) {
|
|
8323
8707
|
const s = readFileSync6(path, "utf8").trim();
|
|
8324
8708
|
if (s) return s;
|
|
8325
8709
|
}
|
|
8326
8710
|
const id = randomUUID2();
|
|
8327
|
-
|
|
8711
|
+
mkdirSync5(dirname4(path), { recursive: true });
|
|
8328
8712
|
writeFileSync5(path, `${id}
|
|
8329
8713
|
`, "utf8");
|
|
8330
8714
|
return id;
|
|
@@ -8639,6 +9023,7 @@ var main = defineCommand({
|
|
|
8639
9023
|
init,
|
|
8640
9024
|
worker,
|
|
8641
9025
|
session,
|
|
9026
|
+
service,
|
|
8642
9027
|
loop,
|
|
8643
9028
|
relay,
|
|
8644
9029
|
channel,
|