@lesscap/baton-cli 0.7.5 → 0.7.6
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 +403 -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
|
});
|
|
@@ -6251,6 +6266,162 @@ var requirement = defineCommand({
|
|
|
6251
6266
|
}
|
|
6252
6267
|
});
|
|
6253
6268
|
|
|
6269
|
+
// src/worker/service-child.ts
|
|
6270
|
+
import { spawn } from "node:child_process";
|
|
6271
|
+
|
|
6272
|
+
// src/worker/proc.ts
|
|
6273
|
+
var killProcessGroup = (proc) => {
|
|
6274
|
+
if (proc.pid === void 0) return;
|
|
6275
|
+
killPidGroup(proc.pid);
|
|
6276
|
+
};
|
|
6277
|
+
var killPidGroup = (pid, signal = "SIGTERM") => {
|
|
6278
|
+
try {
|
|
6279
|
+
process.kill(-pid, signal);
|
|
6280
|
+
} catch {
|
|
6281
|
+
}
|
|
6282
|
+
};
|
|
6283
|
+
|
|
6284
|
+
// src/worker/service-child.ts
|
|
6285
|
+
var isStart = (message) => {
|
|
6286
|
+
if (typeof message !== "object" || message === null) return false;
|
|
6287
|
+
const value = message;
|
|
6288
|
+
return value.type === "start" && typeof value.cwd === "string" && Array.isArray(value.argv) && value.argv.length > 0 && value.argv.every((arg) => typeof arg === "string");
|
|
6289
|
+
};
|
|
6290
|
+
var isStop = (message) => typeof message === "object" && message !== null && message.type === "stop";
|
|
6291
|
+
var send = (message) => {
|
|
6292
|
+
if (process.connected) process.send?.(message);
|
|
6293
|
+
};
|
|
6294
|
+
var runServiceChild = () => {
|
|
6295
|
+
let app = null;
|
|
6296
|
+
let killTimer = null;
|
|
6297
|
+
let stopping = false;
|
|
6298
|
+
const stop = () => {
|
|
6299
|
+
if (stopping) return;
|
|
6300
|
+
stopping = true;
|
|
6301
|
+
if (!app?.pid) {
|
|
6302
|
+
process.exit(0);
|
|
6303
|
+
return;
|
|
6304
|
+
}
|
|
6305
|
+
killPidGroup(app.pid);
|
|
6306
|
+
killTimer = setTimeout(() => {
|
|
6307
|
+
if (app?.pid) killPidGroup(app.pid, "SIGKILL");
|
|
6308
|
+
}, 5e3);
|
|
6309
|
+
killTimer.unref();
|
|
6310
|
+
};
|
|
6311
|
+
process.on("disconnect", stop);
|
|
6312
|
+
process.on("SIGINT", stop);
|
|
6313
|
+
process.on("SIGTERM", stop);
|
|
6314
|
+
process.on("message", (message) => {
|
|
6315
|
+
if (isStop(message)) return stop();
|
|
6316
|
+
if (!isStart(message) || app) return;
|
|
6317
|
+
const [command, ...args] = message.argv;
|
|
6318
|
+
app = spawn(command ?? "", args, {
|
|
6319
|
+
cwd: message.cwd,
|
|
6320
|
+
detached: true,
|
|
6321
|
+
stdio: "inherit",
|
|
6322
|
+
env: process.env
|
|
6323
|
+
});
|
|
6324
|
+
app.once("spawn", () => send({ type: "started", pid: app?.pid }));
|
|
6325
|
+
app.once("error", (error) => {
|
|
6326
|
+
send({ type: "error", error: error.message });
|
|
6327
|
+
process.exit(1);
|
|
6328
|
+
});
|
|
6329
|
+
app.once("exit", (code, signal) => {
|
|
6330
|
+
if (killTimer) clearTimeout(killTimer);
|
|
6331
|
+
process.exit(code ?? (signal ? 1 : 0));
|
|
6332
|
+
});
|
|
6333
|
+
});
|
|
6334
|
+
};
|
|
6335
|
+
var serviceChildCommand = defineCommand({
|
|
6336
|
+
meta: { name: "child", description: "internal service process wrapper", hidden: true },
|
|
6337
|
+
run: runServiceChild
|
|
6338
|
+
});
|
|
6339
|
+
|
|
6340
|
+
// src/commands/service.ts
|
|
6341
|
+
var serviceHandle = (service2) => `W-${service2.workerId}/${service2.name}`;
|
|
6342
|
+
var formatUptime = (startedAt, now = Date.now()) => {
|
|
6343
|
+
const seconds = Math.max(0, Math.floor((now - startedAt) / 1e3));
|
|
6344
|
+
if (seconds >= 86400) return `${Math.floor(seconds / 86400)}d`;
|
|
6345
|
+
if (seconds >= 3600) return `${Math.floor(seconds / 3600)}h`;
|
|
6346
|
+
if (seconds >= 60) return `${Math.floor(seconds / 60)}m`;
|
|
6347
|
+
return `${seconds}s`;
|
|
6348
|
+
};
|
|
6349
|
+
var currentContext = () => {
|
|
6350
|
+
const config = loadProjectConfig(projectConfigPath());
|
|
6351
|
+
if (!config.worker || !config.session)
|
|
6352
|
+
throw new Error("service run requires a Baton session worktree");
|
|
6353
|
+
return { workerId: config.worker.id, sessionId: config.session };
|
|
6354
|
+
};
|
|
6355
|
+
var currentWorker = () => {
|
|
6356
|
+
const worker2 = loadProjectConfig(projectConfigPath()).worker;
|
|
6357
|
+
if (!worker2) throw new Error("no worker in scope");
|
|
6358
|
+
return worker2.id;
|
|
6359
|
+
};
|
|
6360
|
+
var parseTarget = (target) => {
|
|
6361
|
+
const explicit = target.match(/^W-(\d+)\/(.+)$/i);
|
|
6362
|
+
return explicit ? { workerId: Number(explicit[1]), name: explicit[2] ?? "" } : { workerId: currentWorker(), name: target };
|
|
6363
|
+
};
|
|
6364
|
+
var runCommand2 = defineCommand({
|
|
6365
|
+
meta: { name: "run", description: "run a service on the current worker" },
|
|
6366
|
+
args: {
|
|
6367
|
+
name: { type: "positional", required: true, description: "service name" },
|
|
6368
|
+
...common
|
|
6369
|
+
},
|
|
6370
|
+
run: async ({ args, rawArgs }) => {
|
|
6371
|
+
const separator = rawArgs.indexOf("--");
|
|
6372
|
+
const argv = separator === -1 ? [] : rawArgs.slice(separator + 1);
|
|
6373
|
+
if (argv.length === 0) throw new Error("command required after `--`");
|
|
6374
|
+
const context = currentContext();
|
|
6375
|
+
const result = await clientFor(args).services.run(context.workerId, {
|
|
6376
|
+
sessionId: context.sessionId,
|
|
6377
|
+
name: args.name,
|
|
6378
|
+
argv
|
|
6379
|
+
});
|
|
6380
|
+
if (!result.ok) throw new Error(result.error);
|
|
6381
|
+
if (args.json) return console.log(toJson(result));
|
|
6382
|
+
console.log(`running W-${context.workerId}/${args.name}`);
|
|
6383
|
+
if (result.logPath) console.log(`logs: ${result.logPath}`);
|
|
6384
|
+
}
|
|
6385
|
+
});
|
|
6386
|
+
var lsCommand = defineCommand({
|
|
6387
|
+
meta: { name: "ls", description: "list live services in the current project" },
|
|
6388
|
+
args: common,
|
|
6389
|
+
run: async ({ args }) => {
|
|
6390
|
+
const client = clientFor(args);
|
|
6391
|
+
const projectId = resolveProjectId({});
|
|
6392
|
+
const services = await client.services.listByProject(projectId);
|
|
6393
|
+
if (args.json) return console.log(toJson(services));
|
|
6394
|
+
if (services.length === 0) return console.log("(none)");
|
|
6395
|
+
const workers = await client.workers.listByProject(projectId);
|
|
6396
|
+
const names = new Map(workers.map((worker2) => [worker2.id, worker2.name]));
|
|
6397
|
+
services.sort((a, b) => a.workerId - b.workerId || a.name.localeCompare(b.name)).map(
|
|
6398
|
+
(item) => `${serviceHandle(item)} ${names.get(item.workerId) ?? "unknown"} session=${item.sessionId} uptime=${formatUptime(item.startedAt)}`
|
|
6399
|
+
).forEach((line) => {
|
|
6400
|
+
console.log(line);
|
|
6401
|
+
});
|
|
6402
|
+
}
|
|
6403
|
+
});
|
|
6404
|
+
var stopCommand = defineCommand({
|
|
6405
|
+
meta: { name: "stop", description: "stop and remove a live service" },
|
|
6406
|
+
args: {
|
|
6407
|
+
service: {
|
|
6408
|
+
type: "positional",
|
|
6409
|
+
required: true,
|
|
6410
|
+
description: "service name, or W-N/name"
|
|
6411
|
+
},
|
|
6412
|
+
...common
|
|
6413
|
+
},
|
|
6414
|
+
run: async ({ args }) => {
|
|
6415
|
+
const target = parseTarget(args.service);
|
|
6416
|
+
await clientFor(args).services.stop(target.workerId, target.name);
|
|
6417
|
+
console.log(args.json ? toJson({ ok: true, service: args.service }) : `stopped ${args.service}`);
|
|
6418
|
+
}
|
|
6419
|
+
});
|
|
6420
|
+
var service = defineCommand({
|
|
6421
|
+
meta: { name: "service", description: "run and stop worker-managed development services" },
|
|
6422
|
+
subCommands: { run: runCommand2, ls: lsCommand, stop: stopCommand, child: serviceChildCommand }
|
|
6423
|
+
});
|
|
6424
|
+
|
|
6254
6425
|
// src/commands/session/abort.ts
|
|
6255
6426
|
var sessionAbortCommand = defineCommand({
|
|
6256
6427
|
meta: {
|
|
@@ -7777,21 +7948,26 @@ var HEARTBEAT_TIMEOUT_MS = 15e3;
|
|
|
7777
7948
|
var HEARTBEAT_DEAD_MS = 15e4;
|
|
7778
7949
|
var STREAM_STALE_MS = 9e4;
|
|
7779
7950
|
var createLiveness = (deps) => {
|
|
7780
|
-
const {
|
|
7951
|
+
const { heartbeat, log, isStreamOpen, reportRuntime, onTrip } = deps;
|
|
7781
7952
|
let heartbeatOkAt = Date.now();
|
|
7782
7953
|
let streamOkAt = Date.now();
|
|
7783
7954
|
let timer = null;
|
|
7784
7955
|
const ping = async () => {
|
|
7956
|
+
if (reportRuntime)
|
|
7957
|
+
void reportRuntime().catch((error) => log(`service report failed: ${String(error)}`));
|
|
7958
|
+
let timeout;
|
|
7785
7959
|
try {
|
|
7786
7960
|
await Promise.race([
|
|
7787
|
-
|
|
7788
|
-
new Promise(
|
|
7789
|
-
|
|
7790
|
-
)
|
|
7961
|
+
heartbeat(),
|
|
7962
|
+
new Promise((_, reject) => {
|
|
7963
|
+
timeout = setTimeout(() => reject(new Error("heartbeat timeout")), HEARTBEAT_TIMEOUT_MS);
|
|
7964
|
+
})
|
|
7791
7965
|
]);
|
|
7792
7966
|
heartbeatOkAt = Date.now();
|
|
7793
7967
|
} catch (e) {
|
|
7794
7968
|
log(`heartbeat failed: ${String(e)}`);
|
|
7969
|
+
} finally {
|
|
7970
|
+
if (timeout) clearTimeout(timeout);
|
|
7795
7971
|
}
|
|
7796
7972
|
};
|
|
7797
7973
|
const tick = () => {
|
|
@@ -7818,20 +7994,185 @@ var createLiveness = (deps) => {
|
|
|
7818
7994
|
};
|
|
7819
7995
|
};
|
|
7820
7996
|
|
|
7997
|
+
// src/worker/service-supervisor.ts
|
|
7998
|
+
import { mkdirSync as mkdirSync4 } from "node:fs";
|
|
7999
|
+
import { join as join8 } from "node:path";
|
|
8000
|
+
|
|
8001
|
+
// src/worker/service-process.ts
|
|
8002
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
8003
|
+
import { closeSync, existsSync as existsSync5, openSync } from "node:fs";
|
|
8004
|
+
import { dirname as dirname2, join as join7 } from "node:path";
|
|
8005
|
+
import { fileURLToPath } from "node:url";
|
|
8006
|
+
var START_TIMEOUT_MS = 1e4;
|
|
8007
|
+
var isWrapperMessage = (message) => {
|
|
8008
|
+
if (typeof message !== "object" || message === null) return false;
|
|
8009
|
+
const value = message;
|
|
8010
|
+
if (value.type === "started") return typeof value.pid === "number";
|
|
8011
|
+
return value.type === "error" && typeof value.error === "string";
|
|
8012
|
+
};
|
|
8013
|
+
var binPath = () => {
|
|
8014
|
+
const here = dirname2(fileURLToPath(import.meta.url));
|
|
8015
|
+
const devShim = join7(here, "..", "..", "bin", "baton.mjs");
|
|
8016
|
+
return existsSync5(devShim) ? devShim : fileURLToPath(import.meta.url);
|
|
8017
|
+
};
|
|
8018
|
+
var startServiceProcess = (input) => {
|
|
8019
|
+
const logFd = openSync(input.logPath, "a");
|
|
8020
|
+
let wrapper;
|
|
8021
|
+
try {
|
|
8022
|
+
wrapper = spawn2(process.execPath, [binPath(), "service", "child"], {
|
|
8023
|
+
detached: false,
|
|
8024
|
+
stdio: ["ignore", logFd, logFd, "ipc"],
|
|
8025
|
+
env: process.env
|
|
8026
|
+
});
|
|
8027
|
+
} finally {
|
|
8028
|
+
closeSync(logFd);
|
|
8029
|
+
}
|
|
8030
|
+
const started = new Promise((resolve2, reject) => {
|
|
8031
|
+
let settled = false;
|
|
8032
|
+
let timer;
|
|
8033
|
+
const cleanup = () => {
|
|
8034
|
+
clearTimeout(timer);
|
|
8035
|
+
wrapper.off("message", onMessage);
|
|
8036
|
+
wrapper.off("error", onError);
|
|
8037
|
+
wrapper.off("exit", onExit);
|
|
8038
|
+
};
|
|
8039
|
+
const finish = (result) => {
|
|
8040
|
+
if (settled) return;
|
|
8041
|
+
settled = true;
|
|
8042
|
+
cleanup();
|
|
8043
|
+
if (result instanceof Error) reject(result);
|
|
8044
|
+
else resolve2(result);
|
|
8045
|
+
};
|
|
8046
|
+
const onMessage = (message) => {
|
|
8047
|
+
if (!isWrapperMessage(message)) return;
|
|
8048
|
+
if (message.type === "started") finish(message.pid);
|
|
8049
|
+
else finish(new Error(message.error));
|
|
8050
|
+
};
|
|
8051
|
+
const onError = (error) => finish(error);
|
|
8052
|
+
const onExit = (code) => finish(new Error(`service wrapper exited before start (code=${code ?? -1})`));
|
|
8053
|
+
wrapper.on("message", onMessage);
|
|
8054
|
+
wrapper.once("error", onError);
|
|
8055
|
+
wrapper.once("exit", onExit);
|
|
8056
|
+
timer = setTimeout(() => {
|
|
8057
|
+
if (wrapper.connected) wrapper.send({ type: "stop" });
|
|
8058
|
+
finish(new Error("service wrapper did not start in time"));
|
|
8059
|
+
}, START_TIMEOUT_MS);
|
|
8060
|
+
timer.unref();
|
|
8061
|
+
wrapper.send({ type: "start", argv: input.argv, cwd: input.cwd }, (error) => {
|
|
8062
|
+
if (error) finish(error);
|
|
8063
|
+
});
|
|
8064
|
+
});
|
|
8065
|
+
return { wrapper, started };
|
|
8066
|
+
};
|
|
8067
|
+
|
|
8068
|
+
// src/worker/service-supervisor.ts
|
|
8069
|
+
var waitForExit = (child) => child.exitCode !== null ? Promise.resolve() : new Promise((resolve2) => child.once("exit", () => resolve2()));
|
|
8070
|
+
var createServiceSupervisor = (deps) => {
|
|
8071
|
+
const { getSession, sendReport, cfg, log } = deps;
|
|
8072
|
+
const entries = /* @__PURE__ */ new Map();
|
|
8073
|
+
let reportQueue = Promise.resolve();
|
|
8074
|
+
const snapshot = () => [...entries.values()].filter((entry) => entry.appPid !== void 0).map(({ sessionId, name, startedAt }) => ({ sessionId, name, startedAt }));
|
|
8075
|
+
const report = (result) => {
|
|
8076
|
+
const body = { services: snapshot(), ...result ? { result } : {} };
|
|
8077
|
+
const request2 = reportQueue.then(() => sendReport(body)).then(() => {
|
|
8078
|
+
});
|
|
8079
|
+
reportQueue = request2.catch(() => {
|
|
8080
|
+
});
|
|
8081
|
+
return request2;
|
|
8082
|
+
};
|
|
8083
|
+
const remove = (entry) => {
|
|
8084
|
+
if (entries.get(entry.name) !== entry) return;
|
|
8085
|
+
entries.delete(entry.name);
|
|
8086
|
+
void report().catch(() => {
|
|
8087
|
+
});
|
|
8088
|
+
};
|
|
8089
|
+
const stopEntry = (entry) => {
|
|
8090
|
+
if (entry.wrapper.connected) {
|
|
8091
|
+
entry.wrapper.send({ type: "stop" });
|
|
8092
|
+
return;
|
|
8093
|
+
}
|
|
8094
|
+
if (entry.appPid) killPidGroup(entry.appPid, "SIGKILL");
|
|
8095
|
+
};
|
|
8096
|
+
const runOne = async (requestId, sessionId, name, argv) => {
|
|
8097
|
+
if (entries.has(name))
|
|
8098
|
+
return report({
|
|
8099
|
+
requestId,
|
|
8100
|
+
ok: false,
|
|
8101
|
+
error: `service "${name}" is already running`,
|
|
8102
|
+
status: 409
|
|
8103
|
+
});
|
|
8104
|
+
const session2 = await getSession(sessionId);
|
|
8105
|
+
if (session2.workerId !== cfg.workerId || !session2.worktreePath)
|
|
8106
|
+
return report({
|
|
8107
|
+
requestId,
|
|
8108
|
+
ok: false,
|
|
8109
|
+
error: "session is not materialized on this worker",
|
|
8110
|
+
status: 400
|
|
8111
|
+
});
|
|
8112
|
+
const logDir = join8(session2.worktreePath, ".baton-services");
|
|
8113
|
+
const logPath = join8(logDir, `${name}.log`);
|
|
8114
|
+
mkdirSync4(logDir, { recursive: true });
|
|
8115
|
+
const { wrapper, started } = startServiceProcess({ argv, cwd: session2.worktreePath, logPath });
|
|
8116
|
+
const entry = {
|
|
8117
|
+
wrapper,
|
|
8118
|
+
sessionId,
|
|
8119
|
+
name,
|
|
8120
|
+
startedAt: Date.now()
|
|
8121
|
+
};
|
|
8122
|
+
entries.set(name, entry);
|
|
8123
|
+
wrapper.once("exit", (code) => {
|
|
8124
|
+
log(`service ${name} exited (code=${code ?? -1})`);
|
|
8125
|
+
remove(entry);
|
|
8126
|
+
});
|
|
8127
|
+
try {
|
|
8128
|
+
entry.appPid = await started;
|
|
8129
|
+
} catch (error) {
|
|
8130
|
+
remove(entry);
|
|
8131
|
+
return report({ requestId, ok: false, error: String(error), status: 500 });
|
|
8132
|
+
}
|
|
8133
|
+
const service2 = {
|
|
8134
|
+
workerId: cfg.workerId,
|
|
8135
|
+
sessionId,
|
|
8136
|
+
name,
|
|
8137
|
+
startedAt: entry.startedAt
|
|
8138
|
+
};
|
|
8139
|
+
log(`service ${name} started in ${session2.worktreePath}`);
|
|
8140
|
+
await report({ requestId, ok: true, service: service2, logPath });
|
|
8141
|
+
};
|
|
8142
|
+
const run = async (requestId, sessionId, name, argv) => {
|
|
8143
|
+
try {
|
|
8144
|
+
await runOne(requestId, sessionId, name, argv);
|
|
8145
|
+
} catch (error) {
|
|
8146
|
+
await report({ requestId, ok: false, error: String(error), status: 500 });
|
|
8147
|
+
}
|
|
8148
|
+
};
|
|
8149
|
+
const stop = async (requestId, name) => {
|
|
8150
|
+
const entry = entries.get(name);
|
|
8151
|
+
if (!entry)
|
|
8152
|
+
return report({ requestId, ok: false, error: `service "${name}" not found`, status: 404 });
|
|
8153
|
+
stopEntry(entry);
|
|
8154
|
+
await waitForExit(entry.wrapper);
|
|
8155
|
+
remove(entry);
|
|
8156
|
+
await report({ requestId, ok: true });
|
|
8157
|
+
};
|
|
8158
|
+
const stopSession = async (sessionId) => {
|
|
8159
|
+
const matches = [...entries.values()].filter((entry) => entry.sessionId === sessionId);
|
|
8160
|
+
matches.forEach(stopEntry);
|
|
8161
|
+
await Promise.all(matches.map((entry) => waitForExit(entry.wrapper)));
|
|
8162
|
+
matches.forEach(remove);
|
|
8163
|
+
};
|
|
8164
|
+
const killAll = () => entries.forEach(stopEntry);
|
|
8165
|
+
return { run, stop, stopSession, report, killAll };
|
|
8166
|
+
};
|
|
8167
|
+
|
|
7821
8168
|
// src/worker/session-supervisor.ts
|
|
7822
|
-
import { spawn } from "node:child_process";
|
|
8169
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
7823
8170
|
import { randomUUID } from "node:crypto";
|
|
7824
|
-
import { existsSync as
|
|
7825
|
-
import { dirname as
|
|
7826
|
-
import { fileURLToPath } from "node:url";
|
|
8171
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
8172
|
+
import { dirname as dirname3, join as join10 } from "node:path";
|
|
8173
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
7827
8174
|
import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
|
|
7828
8175
|
|
|
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
8176
|
// src/session/runner/title.ts
|
|
7836
8177
|
var clip = (s, max) => s.length > max ? s.slice(0, max) : s;
|
|
7837
8178
|
var isDeclined = (title) => title.replace(/[^a-z]/gi, "").toUpperCase() === "NONE";
|
|
@@ -7920,6 +8261,10 @@ var generateTitle = async (input) => {
|
|
|
7920
8261
|
};
|
|
7921
8262
|
|
|
7922
8263
|
// src/session/runner/title-codex.ts
|
|
8264
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
8265
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
8266
|
+
import { join as join9 } from "node:path";
|
|
8267
|
+
import { Codex as Codex2 } from "@openai/codex-sdk";
|
|
7923
8268
|
var generateTitleWithCodex = async (input) => {
|
|
7924
8269
|
const { userText, assistantText } = input;
|
|
7925
8270
|
if (`${userText}${assistantText}`.trim().length < 4) return { kind: "declined" };
|
|
@@ -7927,7 +8272,7 @@ var generateTitleWithCodex = async (input) => {
|
|
|
7927
8272
|
const timer = setTimeout(() => abort.abort(), TITLE_TIMEOUT_MS);
|
|
7928
8273
|
let cwd = "";
|
|
7929
8274
|
try {
|
|
7930
|
-
cwd = await mkdtemp(
|
|
8275
|
+
cwd = await mkdtemp(join9(tmpdir3(), "baton-title-"));
|
|
7931
8276
|
const client = input.client ?? new Codex2();
|
|
7932
8277
|
const thread = client.startThread({
|
|
7933
8278
|
workingDirectory: cwd,
|
|
@@ -7953,20 +8298,11 @@ var generateTitleWithCodex = async (input) => {
|
|
|
7953
8298
|
}
|
|
7954
8299
|
};
|
|
7955
8300
|
|
|
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
8301
|
// src/worker/session-supervisor.ts
|
|
7966
|
-
var
|
|
7967
|
-
const here =
|
|
7968
|
-
const devShim =
|
|
7969
|
-
return
|
|
8302
|
+
var binPath2 = () => {
|
|
8303
|
+
const here = dirname3(fileURLToPath2(import.meta.url));
|
|
8304
|
+
const devShim = join10(here, "..", "..", "bin", "baton.mjs");
|
|
8305
|
+
return existsSync6(devShim) ? devShim : fileURLToPath2(import.meta.url);
|
|
7970
8306
|
};
|
|
7971
8307
|
var createSessionSupervisor = (deps) => {
|
|
7972
8308
|
const { client, cfg, repo, log, hasTerminal, closeTerminal } = deps;
|
|
@@ -7993,7 +8329,7 @@ var createSessionSupervisor = (deps) => {
|
|
|
7993
8329
|
};
|
|
7994
8330
|
const spawnChild = (sessionId, worktreePath) => {
|
|
7995
8331
|
if (children.has(sessionId)) return;
|
|
7996
|
-
const child =
|
|
8332
|
+
const child = spawn3(process.execPath, [binPath2(), "session", "run", String(sessionId)], {
|
|
7997
8333
|
detached: true,
|
|
7998
8334
|
stdio: "inherit",
|
|
7999
8335
|
env: { ...process.env, BATON_SERVER: cfg.server, BATON_WORKER_TOKEN: cfg.apiToken }
|
|
@@ -8019,7 +8355,7 @@ var createSessionSupervisor = (deps) => {
|
|
|
8019
8355
|
if (!session2.agentSessionId || !worktreePath) {
|
|
8020
8356
|
const sessionCode = randomUUID();
|
|
8021
8357
|
const agentSessionId = session2.agentKind === "codex" ? `pending:${sessionCode}` : sessionCode;
|
|
8022
|
-
worktreePath =
|
|
8358
|
+
worktreePath = join10(worktreeDir, slug(`${name}-${sessionCode.slice(0, 8)}`));
|
|
8023
8359
|
const base = await syncedBase();
|
|
8024
8360
|
if (wasCanceled()) return log(`session #${sessionId} start canceled during git sync`);
|
|
8025
8361
|
createWorktree({
|
|
@@ -8031,13 +8367,14 @@ var createSessionSupervisor = (deps) => {
|
|
|
8031
8367
|
await client.sessions.materialize(sessionId, { agentSessionId, worktreePath }, cfg.apiToken);
|
|
8032
8368
|
if (wasCanceled()) return log(`session #${sessionId} start canceled after materializing`);
|
|
8033
8369
|
log(`materialized session #${sessionId} \u2192 ${worktreePath}`);
|
|
8034
|
-
} else if (!
|
|
8370
|
+
} else if (!existsSync6(worktreePath)) {
|
|
8035
8371
|
await restoreWorktree(repo, worktreePath, session2.agentSessionId.slice(0, 8), baseBranch);
|
|
8036
8372
|
if (wasCanceled()) return log(`session #${sessionId} start canceled during worktree restore`);
|
|
8037
8373
|
log(`recreated session #${sessionId} worktree (was missing) \u2192 ${worktreePath}`);
|
|
8038
8374
|
}
|
|
8039
8375
|
ensureExcluded(repo, PROJECT_CONFIG_NAME);
|
|
8040
|
-
|
|
8376
|
+
ensureExcluded(repo, ".baton-services/");
|
|
8377
|
+
saveProjectConfig(join10(worktreePath, PROJECT_CONFIG_NAME), worktreeConfig(cfg, sessionId));
|
|
8041
8378
|
if (hasTerminal(sessionId))
|
|
8042
8379
|
return log(`session #${sessionId} terminal opened mid-start \u2014 skipping headless start`);
|
|
8043
8380
|
spawnChild(sessionId, worktreePath);
|
|
@@ -8125,8 +8462,8 @@ var createSessionSupervisor = (deps) => {
|
|
|
8125
8462
|
};
|
|
8126
8463
|
|
|
8127
8464
|
// src/worker/terminal-manager.ts
|
|
8128
|
-
import { existsSync as
|
|
8129
|
-
import { spawn as
|
|
8465
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
8466
|
+
import { spawn as spawn4 } from "node-pty";
|
|
8130
8467
|
|
|
8131
8468
|
// ../../node_modules/.pnpm/ws@8.21.0/node_modules/ws/wrapper.mjs
|
|
8132
8469
|
var import_stream2 = __toESM(require_stream(), 1);
|
|
@@ -8139,16 +8476,16 @@ var import_websocket = __toESM(require_websocket(), 1);
|
|
|
8139
8476
|
var import_websocket_server = __toESM(require_websocket_server(), 1);
|
|
8140
8477
|
|
|
8141
8478
|
// src/worker/pty.ts
|
|
8142
|
-
import { existsSync as
|
|
8479
|
+
import { existsSync as existsSync7, readdirSync as readdirSync2 } from "node:fs";
|
|
8143
8480
|
import { homedir as homedir3 } from "node:os";
|
|
8144
|
-
import { join as
|
|
8481
|
+
import { join as join11 } from "node:path";
|
|
8145
8482
|
var MAX_TERMINALS = 10;
|
|
8146
8483
|
var ptyArgs = (agentSessionId, hasJsonl) => hasJsonl ? ["--resume", agentSessionId] : ["--session-id", agentSessionId];
|
|
8147
8484
|
var hasSessionJsonl = (agentSessionId) => {
|
|
8148
|
-
const root =
|
|
8149
|
-
if (!
|
|
8485
|
+
const root = join11(homedir3(), ".claude", "projects");
|
|
8486
|
+
if (!existsSync7(root)) return false;
|
|
8150
8487
|
for (const dir of readdirSync2(root))
|
|
8151
|
-
if (
|
|
8488
|
+
if (existsSync7(join11(root, dir, `${agentSessionId}.jsonl`))) return true;
|
|
8152
8489
|
return false;
|
|
8153
8490
|
};
|
|
8154
8491
|
var serverTerminalWsUrl = (server, sessionId) => `${server.replace(/^http/, "ws")}/workers/me/terminal/ws?sessionId=${sessionId}`;
|
|
@@ -8159,13 +8496,13 @@ var createTerminalManager = (deps) => {
|
|
|
8159
8496
|
const { cfg, log, hasChild } = deps;
|
|
8160
8497
|
const terminals = /* @__PURE__ */ new Map();
|
|
8161
8498
|
const open = (sessionId, agentSessionId, worktreePath) => {
|
|
8162
|
-
const why = terminals.has(sessionId) ? "already open" : hasChild(sessionId) ? "headless child running" : !
|
|
8499
|
+
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
8500
|
if (why) {
|
|
8164
8501
|
log(`terminal #${sessionId}: ${why} \u2014 refusing`);
|
|
8165
8502
|
return;
|
|
8166
8503
|
}
|
|
8167
8504
|
const claudeBin = process.env.BATON_CLAUDE_BIN ?? "claude";
|
|
8168
|
-
const term =
|
|
8505
|
+
const term = spawn4(claudeBin, ptyArgs(agentSessionId, hasSessionJsonl(agentSessionId)), {
|
|
8169
8506
|
name: "xterm-color",
|
|
8170
8507
|
cols: 80,
|
|
8171
8508
|
rows: 24,
|
|
@@ -8251,14 +8588,20 @@ var runWorkerDaemon = async (cfg, client, signal) => {
|
|
|
8251
8588
|
log,
|
|
8252
8589
|
hasChild: (id) => supervisor.has(id)
|
|
8253
8590
|
});
|
|
8591
|
+
const services = createServiceSupervisor({
|
|
8592
|
+
getSession: (sessionId) => client.sessions.get(sessionId),
|
|
8593
|
+
sendReport: (report) => client.services.report(report),
|
|
8594
|
+
cfg,
|
|
8595
|
+
log
|
|
8596
|
+
});
|
|
8254
8597
|
let fatal = false;
|
|
8255
8598
|
let stop = () => {
|
|
8256
8599
|
};
|
|
8257
8600
|
const liveness = createLiveness({
|
|
8258
|
-
client,
|
|
8259
|
-
cfg,
|
|
8601
|
+
heartbeat: () => client.workers.heartbeat(cfg.machineId),
|
|
8260
8602
|
log,
|
|
8261
8603
|
isStreamOpen: () => es.readyState === ES_OPEN,
|
|
8604
|
+
reportRuntime: () => services.report(),
|
|
8262
8605
|
onTrip: (reason) => {
|
|
8263
8606
|
if (fatal) return;
|
|
8264
8607
|
log(`${reason} \u2014 exiting so the supervisor restarts a clean worker`);
|
|
@@ -8278,6 +8621,7 @@ var runWorkerDaemon = async (cfg, client, signal) => {
|
|
|
8278
8621
|
es.onopen = () => {
|
|
8279
8622
|
liveness.markStreamOk();
|
|
8280
8623
|
void supervisor.reconcile().catch((err) => log(`reconcile failed: ${String(err)}`));
|
|
8624
|
+
void services.report().catch((err) => log(`service report failed: ${String(err)}`));
|
|
8281
8625
|
};
|
|
8282
8626
|
es.onmessage = (e) => {
|
|
8283
8627
|
liveness.markStreamOk();
|
|
@@ -8286,14 +8630,18 @@ var runWorkerDaemon = async (cfg, client, signal) => {
|
|
|
8286
8630
|
if (cmd.cmd === "session.start")
|
|
8287
8631
|
void supervisor.start(cmd.sessionId, cmd.name).catch((err) => log(`start failed: ${String(err)}`));
|
|
8288
8632
|
else if (cmd.cmd === "session.stop") supervisor.stop(cmd.sessionId);
|
|
8289
|
-
else if (cmd.cmd === "session.delete")
|
|
8633
|
+
else if (cmd.cmd === "session.delete")
|
|
8634
|
+
void services.stopSession(cmd.sessionId).then(() => supervisor.remove(cmd.sessionId, cmd.worktreePath)).catch((err) => log(`service cleanup failed: ${String(err)}`));
|
|
8290
8635
|
else if (cmd.cmd === "session.title")
|
|
8291
8636
|
void supervisor.title(cmd.sessionId, cmd.agentSessionId, cmd.worktreePath).catch((err) => log(`title failed: ${String(err)}`));
|
|
8292
8637
|
else if (cmd.cmd === "session.terminal") {
|
|
8293
8638
|
if (cmd.action === "open")
|
|
8294
8639
|
terminals.open(cmd.sessionId, cmd.agentSessionId, cmd.worktreePath);
|
|
8295
8640
|
else terminals.close(cmd.sessionId);
|
|
8296
|
-
}
|
|
8641
|
+
} else if (cmd.cmd === "service.run")
|
|
8642
|
+
void services.run(cmd.requestId, cmd.sessionId, cmd.name, cmd.argv).catch((err) => log(`service run failed: ${String(err)}`));
|
|
8643
|
+
else if (cmd.cmd === "service.stop")
|
|
8644
|
+
void services.stop(cmd.requestId, cmd.name).catch((err) => log(`service stop failed: ${String(err)}`));
|
|
8297
8645
|
} catch {
|
|
8298
8646
|
}
|
|
8299
8647
|
};
|
|
@@ -8307,6 +8655,7 @@ var runWorkerDaemon = async (cfg, client, signal) => {
|
|
|
8307
8655
|
});
|
|
8308
8656
|
es.close();
|
|
8309
8657
|
liveness.stop();
|
|
8658
|
+
services.killAll();
|
|
8310
8659
|
supervisor.killAll();
|
|
8311
8660
|
terminals.killAll();
|
|
8312
8661
|
if (fatal) process.exit(1);
|
|
@@ -8314,17 +8663,17 @@ var runWorkerDaemon = async (cfg, client, signal) => {
|
|
|
8314
8663
|
|
|
8315
8664
|
// src/worker/machine-id.ts
|
|
8316
8665
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
8317
|
-
import { existsSync as
|
|
8666
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
|
|
8318
8667
|
import { homedir as homedir4 } from "node:os";
|
|
8319
|
-
import { dirname as
|
|
8320
|
-
var machineIdPath = (env = process.env) =>
|
|
8668
|
+
import { dirname as dirname4, join as join12 } from "node:path";
|
|
8669
|
+
var machineIdPath = (env = process.env) => join12(env.XDG_DATA_HOME ?? join12(env.HOME ?? homedir4(), ".local/share"), "baton", "machine-id");
|
|
8321
8670
|
var readOrCreateMachineId = (path = machineIdPath()) => {
|
|
8322
|
-
if (
|
|
8671
|
+
if (existsSync9(path)) {
|
|
8323
8672
|
const s = readFileSync6(path, "utf8").trim();
|
|
8324
8673
|
if (s) return s;
|
|
8325
8674
|
}
|
|
8326
8675
|
const id = randomUUID2();
|
|
8327
|
-
|
|
8676
|
+
mkdirSync5(dirname4(path), { recursive: true });
|
|
8328
8677
|
writeFileSync5(path, `${id}
|
|
8329
8678
|
`, "utf8");
|
|
8330
8679
|
return id;
|
|
@@ -8639,6 +8988,7 @@ var main = defineCommand({
|
|
|
8639
8988
|
init,
|
|
8640
8989
|
worker,
|
|
8641
8990
|
session,
|
|
8991
|
+
service,
|
|
8642
8992
|
loop,
|
|
8643
8993
|
relay,
|
|
8644
8994
|
channel,
|