@lesscap/baton-cli 0.8.3 → 0.8.4
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/README.md +32 -17
- package/dist/baton.mjs +825 -443
- package/package.json +1 -1
- package/skills/baton-publish-artifact/SKILL.md +1 -1
package/dist/baton.mjs
CHANGED
|
@@ -2281,7 +2281,7 @@ var require_websocket = __commonJS({
|
|
|
2281
2281
|
var http = __require("http");
|
|
2282
2282
|
var net = __require("net");
|
|
2283
2283
|
var tls = __require("tls");
|
|
2284
|
-
var { randomBytes, createHash } = __require("crypto");
|
|
2284
|
+
var { randomBytes, createHash: createHash2 } = __require("crypto");
|
|
2285
2285
|
var { Duplex, Readable: Readable2 } = __require("stream");
|
|
2286
2286
|
var { URL: URL2 } = __require("url");
|
|
2287
2287
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
@@ -2949,7 +2949,7 @@ var require_websocket = __commonJS({
|
|
|
2949
2949
|
abortHandshake(websocket, socket, "Invalid Upgrade header");
|
|
2950
2950
|
return;
|
|
2951
2951
|
}
|
|
2952
|
-
const digest =
|
|
2952
|
+
const digest = createHash2("sha1").update(key + GUID).digest("base64");
|
|
2953
2953
|
if (res.headers["sec-websocket-accept"] !== digest) {
|
|
2954
2954
|
abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
|
|
2955
2955
|
return;
|
|
@@ -3230,7 +3230,7 @@ var require_stream = __commonJS({
|
|
|
3230
3230
|
};
|
|
3231
3231
|
duplex._final = function(callback) {
|
|
3232
3232
|
if (ws.readyState === ws.CONNECTING) {
|
|
3233
|
-
ws.once("open", function
|
|
3233
|
+
ws.once("open", function open2() {
|
|
3234
3234
|
duplex._final(callback);
|
|
3235
3235
|
});
|
|
3236
3236
|
return;
|
|
@@ -3251,7 +3251,7 @@ var require_stream = __commonJS({
|
|
|
3251
3251
|
};
|
|
3252
3252
|
duplex._write = function(chunk, encoding, callback) {
|
|
3253
3253
|
if (ws.readyState === ws.CONNECTING) {
|
|
3254
|
-
ws.once("open", function
|
|
3254
|
+
ws.once("open", function open2() {
|
|
3255
3255
|
duplex._write(chunk, encoding, callback);
|
|
3256
3256
|
});
|
|
3257
3257
|
return;
|
|
@@ -3318,7 +3318,7 @@ var require_websocket_server = __commonJS({
|
|
|
3318
3318
|
var EventEmitter = __require("events");
|
|
3319
3319
|
var http = __require("http");
|
|
3320
3320
|
var { Duplex } = __require("stream");
|
|
3321
|
-
var { createHash } = __require("crypto");
|
|
3321
|
+
var { createHash: createHash2 } = __require("crypto");
|
|
3322
3322
|
var extension2 = require_extension();
|
|
3323
3323
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
3324
3324
|
var subprotocol2 = require_subprotocol();
|
|
@@ -3625,7 +3625,7 @@ var require_websocket_server = __commonJS({
|
|
|
3625
3625
|
);
|
|
3626
3626
|
}
|
|
3627
3627
|
if (this._state > RUNNING) return abortHandshake(socket, 503);
|
|
3628
|
-
const digest =
|
|
3628
|
+
const digest = createHash2("sha1").update(key + GUID).digest("base64");
|
|
3629
3629
|
const headers = [
|
|
3630
3630
|
"HTTP/1.1 101 Switching Protocols",
|
|
3631
3631
|
"Upgrade: websocket",
|
|
@@ -5370,11 +5370,13 @@ var contentTypeForPath = (path) => src_default.getType(path) ?? "application/oct
|
|
|
5370
5370
|
// src/output.ts
|
|
5371
5371
|
var toJson = (data) => JSON.stringify(data, null, 2);
|
|
5372
5372
|
var fmtWorkspace = (w) => `${w.id} ${w.name}`;
|
|
5373
|
+
var fmtWorkerTemplate = (t) => `${t.id} ${t.name} [${t.available ? "available" : "unavailable"}] ${t.agentKinds.join(", ")}`;
|
|
5374
|
+
var fmtWorkerCreation = (c) => `${c.id} [${c.status}/${c.step ?? "pending"}] ${c.name} ${c.agentKind}${c.workerId === null ? "" : ` W-${c.workerId}`}${c.error ? ` ${c.error}` : ""}`;
|
|
5373
5375
|
var fmtProject = (p) => `${p.id} ${p.name}`;
|
|
5374
5376
|
var fmtRequirement = (r) => `${r.code} [${r.status}] ${r.title}`;
|
|
5375
5377
|
var fmtTask = (t) => `${t.code} [${t.status}] ${t.title}`;
|
|
5376
5378
|
var fmtComment = (c) => `${c.workerId !== void 0 ? `worker#${c.workerId}` : "you"} ${c.body}`;
|
|
5377
|
-
var fmtSession = (s) => `${s.id} ${s.name} ${s.attached ? "[active]" : "[inactive]"} ${s.
|
|
5379
|
+
var fmtSession = (s) => `${s.id} ${s.name} ${s.attached ? "[active]" : "[inactive]"} ${s.cwd ?? "(pending)"}`;
|
|
5378
5380
|
var fmtWorker = (w) => {
|
|
5379
5381
|
const offline = w.connected === false ? " [offline]" : "";
|
|
5380
5382
|
return `W-${w.id} ${w.name} ${w.hostname} ${w.agentKind}${offline}`;
|
|
@@ -5550,7 +5552,7 @@ var servicesClient = (baseUrl) => {
|
|
|
5550
5552
|
};
|
|
5551
5553
|
|
|
5552
5554
|
// src/client/retry.ts
|
|
5553
|
-
var sleep = (ms) => new Promise((
|
|
5555
|
+
var sleep = (ms) => new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
5554
5556
|
var withRetry = async (fn, opts = {}) => {
|
|
5555
5557
|
const tries = Math.max(1, opts.tries ?? 4);
|
|
5556
5558
|
const baseMs = opts.baseMs ?? 300;
|
|
@@ -5685,6 +5687,14 @@ var taskClient = (baseUrl) => {
|
|
|
5685
5687
|
};
|
|
5686
5688
|
};
|
|
5687
5689
|
|
|
5690
|
+
// src/client/worker-creations.ts
|
|
5691
|
+
var workerCreationsClient = (baseUrl) => ({
|
|
5692
|
+
templates: (projectId) => request(`${baseUrl}/projects/${projectId}/worker-templates`, { method: "GET" }),
|
|
5693
|
+
create: (projectId, input) => request(`${baseUrl}/projects/${projectId}/worker-creations`, { method: "POST", body: input }),
|
|
5694
|
+
listByProject: (projectId) => request(`${baseUrl}/projects/${projectId}/worker-creations`, { method: "GET" }),
|
|
5695
|
+
get: (id) => request(`${baseUrl}/worker-creations/${id}`, { method: "GET" })
|
|
5696
|
+
});
|
|
5697
|
+
|
|
5688
5698
|
// src/client/workers.ts
|
|
5689
5699
|
var workersClient = (baseUrl) => {
|
|
5690
5700
|
const u = (p) => `${baseUrl}${p}`;
|
|
@@ -5725,6 +5735,7 @@ var createClient = (baseUrl, opts) => {
|
|
|
5725
5735
|
return clientFromBase(baseUrl);
|
|
5726
5736
|
};
|
|
5727
5737
|
var clientFromBase = (baseUrl) => ({
|
|
5738
|
+
workerCreations: workerCreationsClient(baseUrl),
|
|
5728
5739
|
artifacts: artifactsClient(baseUrl),
|
|
5729
5740
|
workspaces: workspaceClient(baseUrl),
|
|
5730
5741
|
projects: projectClient(baseUrl),
|
|
@@ -5783,7 +5794,6 @@ var viewWorker = (cfg) => {
|
|
|
5783
5794
|
return {
|
|
5784
5795
|
server,
|
|
5785
5796
|
projectId: project2,
|
|
5786
|
-
baseBranch: cfg.baseBranch,
|
|
5787
5797
|
workerId: worker2.id,
|
|
5788
5798
|
agentKind: worker2.agentKind ?? "claude-code",
|
|
5789
5799
|
name: worker2.name,
|
|
@@ -5791,29 +5801,40 @@ var viewWorker = (cfg) => {
|
|
|
5791
5801
|
apiToken: worker2.apiToken
|
|
5792
5802
|
};
|
|
5793
5803
|
};
|
|
5794
|
-
var
|
|
5804
|
+
var workerProjectConfig = (cfg) => ({
|
|
5795
5805
|
server: cfg.server,
|
|
5796
5806
|
project: cfg.projectId,
|
|
5797
|
-
baseBranch: cfg.baseBranch,
|
|
5798
5807
|
worker: {
|
|
5799
5808
|
id: cfg.workerId,
|
|
5800
5809
|
name: cfg.name,
|
|
5801
5810
|
agentKind: cfg.agentKind,
|
|
5802
5811
|
machineId: cfg.machineId,
|
|
5803
5812
|
apiToken: cfg.apiToken
|
|
5804
|
-
}
|
|
5805
|
-
|
|
5813
|
+
}
|
|
5814
|
+
});
|
|
5815
|
+
var workerEnv = (cfg) => ({
|
|
5816
|
+
...Object.fromEntries(
|
|
5817
|
+
Object.entries(process.env).filter(
|
|
5818
|
+
(entry) => entry[1] !== void 0
|
|
5819
|
+
)
|
|
5820
|
+
),
|
|
5821
|
+
BATON_SERVER: cfg.server,
|
|
5822
|
+
BATON_PROJECT_ID: String(cfg.projectId),
|
|
5823
|
+
BATON_WORKER_ID: String(cfg.workerId),
|
|
5824
|
+
BATON_WORKER_TOKEN: cfg.apiToken
|
|
5806
5825
|
});
|
|
5807
5826
|
|
|
5808
5827
|
// src/config.ts
|
|
5809
5828
|
var resolveBaseUrl = (urlArg, env = process.env) => {
|
|
5810
5829
|
if (urlArg) return urlArg;
|
|
5830
|
+
if (env.BATON_SERVER) return env.BATON_SERVER;
|
|
5811
5831
|
const cfg = loadProjectConfigOrNull(projectConfigPath());
|
|
5812
5832
|
if (cfg?.server) return cfg.server;
|
|
5813
5833
|
return env.BATON_URL ?? "http://localhost:3280";
|
|
5814
5834
|
};
|
|
5815
5835
|
var resolveBaseUrlOrNull = (urlArg, env = process.env) => {
|
|
5816
5836
|
if (urlArg) return urlArg;
|
|
5837
|
+
if (env.BATON_SERVER) return env.BATON_SERVER;
|
|
5817
5838
|
const cfg = loadProjectConfigOrNull(projectConfigPath());
|
|
5818
5839
|
if (cfg?.server) return cfg.server;
|
|
5819
5840
|
return env.BATON_URL ?? null;
|
|
@@ -5823,7 +5844,7 @@ var resolveBaseUrlOrNull = (urlArg, env = process.env) => {
|
|
|
5823
5844
|
var common = {
|
|
5824
5845
|
url: {
|
|
5825
5846
|
type: "string",
|
|
5826
|
-
description: "baton server url (--url > .baton.json > BATON_URL > http://localhost:3280)"
|
|
5847
|
+
description: "baton server url (--url > BATON_SERVER > .baton.json > BATON_URL > http://localhost:3280)"
|
|
5827
5848
|
},
|
|
5828
5849
|
json: { type: "boolean", description: "output JSON" }
|
|
5829
5850
|
};
|
|
@@ -5846,6 +5867,8 @@ var resolveBearer = () => {
|
|
|
5846
5867
|
};
|
|
5847
5868
|
var resolveProjectId = (args) => {
|
|
5848
5869
|
if (args.project !== void 0 && args.project !== "") return Number(args.project);
|
|
5870
|
+
if (process.env.BATON_PROJECT_ID !== void 0)
|
|
5871
|
+
return contextId(process.env.BATON_PROJECT_ID, "BATON_PROJECT_ID");
|
|
5849
5872
|
const cfg = loadProjectConfigOrNull(projectConfigPath());
|
|
5850
5873
|
if (cfg?.project !== void 0) return cfg.project;
|
|
5851
5874
|
throw new Error("no project in scope. pass --project <id> or run `baton init` in this directory.");
|
|
@@ -5870,6 +5893,18 @@ var parseDuration = (s) => {
|
|
|
5870
5893
|
const mult = m[2] === "d" ? 86400 : m[2] === "h" ? 3600 : m[2] === "m" ? 60 : 1;
|
|
5871
5894
|
return n * mult;
|
|
5872
5895
|
};
|
|
5896
|
+
var contextId = (value, source) => {
|
|
5897
|
+
const id = Number(value);
|
|
5898
|
+
if (!Number.isSafeInteger(id) || id <= 0) throw new Error(`invalid ${source}`);
|
|
5899
|
+
return id;
|
|
5900
|
+
};
|
|
5901
|
+
var resolveWorkerId = () => {
|
|
5902
|
+
if (process.env.BATON_WORKER_ID !== void 0)
|
|
5903
|
+
return contextId(process.env.BATON_WORKER_ID, "BATON_WORKER_ID");
|
|
5904
|
+
const id = loadProjectConfigOrNull(projectConfigPath())?.worker?.id;
|
|
5905
|
+
if (id === void 0) throw new Error("no worker in scope");
|
|
5906
|
+
return id;
|
|
5907
|
+
};
|
|
5873
5908
|
|
|
5874
5909
|
// src/commands/artifact.ts
|
|
5875
5910
|
var formatArtifact = (artifact2) => `${artifact2.id} ${artifact2.size} B ${artifact2.filename} ${artifact2.url}`;
|
|
@@ -6227,11 +6262,11 @@ var EventSource = class extends EventTarget {
|
|
|
6227
6262
|
return;
|
|
6228
6263
|
}
|
|
6229
6264
|
const decoder = new TextDecoder(), reader = body.getReader();
|
|
6230
|
-
let
|
|
6265
|
+
let open2 = true;
|
|
6231
6266
|
do {
|
|
6232
6267
|
const { done, value } = await reader.read();
|
|
6233
|
-
value && __privateGet(this, _parser).feed(decoder.decode(value, { stream: !done })), done && (
|
|
6234
|
-
} while (
|
|
6268
|
+
value && __privateGet(this, _parser).feed(decoder.decode(value, { stream: !done })), done && (open2 = false, __privateGet(this, _parser).reset(), __privateMethod(this, _EventSource_instances, scheduleReconnect_fn).call(this));
|
|
6269
|
+
} while (open2);
|
|
6235
6270
|
}), __privateAdd(this, _onFetchError, (err) => {
|
|
6236
6271
|
__privateSet(this, _controller, void 0), !(err.name === "AbortError" || err.type === "aborted") && __privateMethod(this, _EventSource_instances, scheduleReconnect_fn).call(this, flattenError(err));
|
|
6237
6272
|
}), __privateAdd(this, _onEvent, (event) => {
|
|
@@ -6776,10 +6811,10 @@ var runListen = async (o) => {
|
|
|
6776
6811
|
onError: (e) => emit({ type: "channel.error", error: describe(e) })
|
|
6777
6812
|
});
|
|
6778
6813
|
emit({ type: "channel.listening", channel: o.channel, from: o.from, url: o.url });
|
|
6779
|
-
await new Promise((
|
|
6814
|
+
await new Promise((resolve4) => {
|
|
6780
6815
|
const stop = () => {
|
|
6781
6816
|
close();
|
|
6782
|
-
|
|
6817
|
+
resolve4();
|
|
6783
6818
|
};
|
|
6784
6819
|
process.on("SIGINT", stop);
|
|
6785
6820
|
process.on("SIGTERM", stop);
|
|
@@ -7034,10 +7069,6 @@ var init = defineCommand({
|
|
|
7034
7069
|
});
|
|
7035
7070
|
|
|
7036
7071
|
// src/commands/session/shared.ts
|
|
7037
|
-
import { homedir } from "node:os";
|
|
7038
|
-
import { join as join3 } from "node:path";
|
|
7039
|
-
var defaultWorktreeDir = (env = process.env) => env.BATON_WORKTREE_DIR ?? join3(env.XDG_DATA_HOME ?? join3(env.HOME ?? homedir(), ".local/share"), "baton", "worktrees");
|
|
7040
|
-
var slug = (s) => s.toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
|
|
7041
7072
|
var resolveSession = async (client, projectId, handle) => {
|
|
7042
7073
|
const asInt = Number(handle);
|
|
7043
7074
|
if (Number.isInteger(asInt) && asInt > 0) {
|
|
@@ -7264,10 +7295,466 @@ var project = defineCommand({
|
|
|
7264
7295
|
}
|
|
7265
7296
|
});
|
|
7266
7297
|
|
|
7298
|
+
// src/provisioner/config.ts
|
|
7299
|
+
import { execFile } from "node:child_process";
|
|
7300
|
+
import { constants } from "node:fs";
|
|
7301
|
+
import { access, readFile, stat as stat2 } from "node:fs/promises";
|
|
7302
|
+
import { isAbsolute } from "node:path";
|
|
7303
|
+
import { promisify } from "node:util";
|
|
7304
|
+
var forbiddenEnv = /* @__PURE__ */ new Set([
|
|
7305
|
+
"BATON_PROVISIONER_TOKEN",
|
|
7306
|
+
"BATON_TOKEN",
|
|
7307
|
+
"BATON_WORKER_TOKEN",
|
|
7308
|
+
"BATON_USER",
|
|
7309
|
+
"BATON_PASS",
|
|
7310
|
+
"HOME",
|
|
7311
|
+
"XDG_DATA_HOME"
|
|
7312
|
+
]);
|
|
7313
|
+
var object = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7314
|
+
var absolutePath = (value, field) => {
|
|
7315
|
+
if (typeof value !== "string" || !isAbsolute(value) || value.includes("\0"))
|
|
7316
|
+
throw new Error(`${field} must be an absolute path`);
|
|
7317
|
+
return value;
|
|
7318
|
+
};
|
|
7319
|
+
var commandEnvironment = (config) => ({
|
|
7320
|
+
...process.env,
|
|
7321
|
+
PATH: config.workerEnv.PATH ?? process.env.PATH ?? "/usr/bin:/bin"
|
|
7322
|
+
});
|
|
7323
|
+
var loadProvisionerConfig = async (path) => {
|
|
7324
|
+
if (process.platform !== "darwin") throw new Error("provisioner requires macOS");
|
|
7325
|
+
absolutePath(path, "config");
|
|
7326
|
+
if ((await stat2(path)).mode & 63) throw new Error("config must be private: chmod 600 <config>");
|
|
7327
|
+
const text = await readFile(path, "utf8");
|
|
7328
|
+
let raw;
|
|
7329
|
+
try {
|
|
7330
|
+
raw = JSON.parse(text);
|
|
7331
|
+
} catch (error) {
|
|
7332
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
7333
|
+
throw new Error("invalid provisioner JSON config");
|
|
7334
|
+
}
|
|
7335
|
+
if (!object(raw) || typeof raw.server !== "string" || typeof raw.token !== "string")
|
|
7336
|
+
throw new Error("server and token required");
|
|
7337
|
+
let server;
|
|
7338
|
+
try {
|
|
7339
|
+
server = new URL(raw.server);
|
|
7340
|
+
} catch (error) {
|
|
7341
|
+
if (!(error instanceof TypeError)) throw error;
|
|
7342
|
+
throw new Error("server must be a valid HTTP(S) URL");
|
|
7343
|
+
}
|
|
7344
|
+
if (!["http:", "https:"].includes(server.protocol) || server.username || server.password || server.search || server.hash)
|
|
7345
|
+
throw new Error("server must be an HTTP(S) URL without credentials, query, or fragment");
|
|
7346
|
+
if (!raw.token.trim() || /[\r\n]/.test(raw.token))
|
|
7347
|
+
throw new Error("valid provisioner token required");
|
|
7348
|
+
if (!object(raw.workerEnv)) throw new Error("workerEnv must be an object");
|
|
7349
|
+
const workerEnv2 = Object.fromEntries(
|
|
7350
|
+
Object.entries(raw.workerEnv).map(([key, value]) => {
|
|
7351
|
+
if (forbiddenEnv.has(key) || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || typeof value !== "string" || value.includes("\0"))
|
|
7352
|
+
throw new Error(`invalid worker environment key: ${key}`);
|
|
7353
|
+
return [key, value];
|
|
7354
|
+
})
|
|
7355
|
+
);
|
|
7356
|
+
const config = {
|
|
7357
|
+
server: server.toString().replace(/\/$/, ""),
|
|
7358
|
+
token: raw.token.trim(),
|
|
7359
|
+
projectsRoot: absolutePath(raw.projectsRoot, "projectsRoot"),
|
|
7360
|
+
cliPath: absolutePath(raw.cliPath, "cliPath"),
|
|
7361
|
+
nodePath: absolutePath(raw.nodePath ?? process.execPath, "nodePath"),
|
|
7362
|
+
workerEnv: workerEnv2
|
|
7363
|
+
};
|
|
7364
|
+
await access(config.cliPath, constants.R_OK);
|
|
7365
|
+
await access(config.nodePath ?? process.execPath, constants.X_OK);
|
|
7366
|
+
const env = commandEnvironment(config);
|
|
7367
|
+
await promisify(execFile)("git", ["--version"], { env, timeout: 1e4 });
|
|
7368
|
+
await promisify(execFile)("launchctl", ["help"], { env, timeout: 1e4 });
|
|
7369
|
+
await promisify(execFile)("/usr/bin/plutil", ["-help"], { timeout: 1e4 });
|
|
7370
|
+
return config;
|
|
7371
|
+
};
|
|
7372
|
+
|
|
7373
|
+
// src/provisioner/client.ts
|
|
7374
|
+
var createProvisionerClient = (config) => {
|
|
7375
|
+
const request2 = async (path, token, body, signal) => {
|
|
7376
|
+
const timeout = AbortSignal.timeout(1e4);
|
|
7377
|
+
const response = await fetch(`${config.server}${path}`, {
|
|
7378
|
+
method: body === void 0 ? "GET" : "POST",
|
|
7379
|
+
headers: {
|
|
7380
|
+
authorization: `Bearer ${token}`,
|
|
7381
|
+
...body === void 0 ? {} : { "content-type": "application/json" }
|
|
7382
|
+
},
|
|
7383
|
+
signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
|
|
7384
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
7385
|
+
});
|
|
7386
|
+
if (!response.ok) {
|
|
7387
|
+
await response.body?.cancel();
|
|
7388
|
+
throw new HttpError(response.status, `provisioner request ${path}: HTTP ${response.status}`);
|
|
7389
|
+
}
|
|
7390
|
+
return await response.json();
|
|
7391
|
+
};
|
|
7392
|
+
return {
|
|
7393
|
+
get: (id, signal) => request2(`/provisioner/worker-creations/${id}`, config.token, void 0, signal),
|
|
7394
|
+
report: (id, report, signal) => request2(`/provisioner/worker-creations/${id}/progress`, config.token, report, signal),
|
|
7395
|
+
register: (id, hostname2, signal) => request2(`/provisioner/worker-creations/${id}/worker`, config.token, { hostname: hostname2 }, signal),
|
|
7396
|
+
worker: (id, token, signal) => request2(`/workers/${id}`, token, void 0, signal)
|
|
7397
|
+
};
|
|
7398
|
+
};
|
|
7399
|
+
|
|
7400
|
+
// src/provisioner/executor.ts
|
|
7401
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
7402
|
+
import { lstat, mkdir as mkdir2, realpath, writeFile } from "node:fs/promises";
|
|
7403
|
+
import { hostname } from "node:os";
|
|
7404
|
+
import { join as join4 } from "node:path";
|
|
7405
|
+
import { setTimeout as setTimeout2 } from "node:timers/promises";
|
|
7406
|
+
import { promisify as promisify3 } from "node:util";
|
|
7407
|
+
|
|
7408
|
+
// src/provisioner/launchd.ts
|
|
7409
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
7410
|
+
import { mkdir, open } from "node:fs/promises";
|
|
7411
|
+
import { homedir } from "node:os";
|
|
7412
|
+
import { dirname, join as join3 } from "node:path";
|
|
7413
|
+
import { promisify as promisify2 } from "node:util";
|
|
7414
|
+
var workerStatePath = (config, creation) => join3(
|
|
7415
|
+
config.projectsRoot,
|
|
7416
|
+
".worker-state",
|
|
7417
|
+
`project-${creation.projectId}`,
|
|
7418
|
+
`creation-${creation.id}`
|
|
7419
|
+
);
|
|
7420
|
+
var workerLogPath = (creation) => join3(homedir(), "Library/Logs", `baton-worker-creation-${creation.id}.log`);
|
|
7421
|
+
var startAgent = async (config, agent, signal) => {
|
|
7422
|
+
signal?.throwIfAborted();
|
|
7423
|
+
const plist = join3(homedir(), "Library/LaunchAgents", `${agent.label}.plist`);
|
|
7424
|
+
await mkdir(dirname(plist), { recursive: true });
|
|
7425
|
+
await mkdir(dirname(agent.log), { recursive: true });
|
|
7426
|
+
const domain = `gui/${process.getuid?.()}`;
|
|
7427
|
+
const env = commandEnvironment(config);
|
|
7428
|
+
try {
|
|
7429
|
+
await promisify2(execFile2)("launchctl", ["print", `${domain}/${agent.label}`], {
|
|
7430
|
+
env,
|
|
7431
|
+
timeout: 1e4,
|
|
7432
|
+
signal
|
|
7433
|
+
});
|
|
7434
|
+
throw new Error(
|
|
7435
|
+
`launchd service already exists: ${agent.label}; stop it manually before installing`
|
|
7436
|
+
);
|
|
7437
|
+
} catch (error) {
|
|
7438
|
+
if (!(error instanceof Error && "code" in error && error.code === 113 && "stderr" in error && typeof error.stderr === "string" && error.stderr.includes("Could not find service")))
|
|
7439
|
+
throw error;
|
|
7440
|
+
}
|
|
7441
|
+
const handle = await open(plist, "wx", 384);
|
|
7442
|
+
try {
|
|
7443
|
+
const payload = {
|
|
7444
|
+
Label: agent.label,
|
|
7445
|
+
ProgramArguments: agent.args,
|
|
7446
|
+
WorkingDirectory: agent.cwd,
|
|
7447
|
+
EnvironmentVariables: agent.env,
|
|
7448
|
+
RunAtLoad: true,
|
|
7449
|
+
KeepAlive: true,
|
|
7450
|
+
ThrottleInterval: 10,
|
|
7451
|
+
StandardOutPath: agent.log,
|
|
7452
|
+
StandardErrorPath: agent.log
|
|
7453
|
+
};
|
|
7454
|
+
await handle.writeFile(JSON.stringify(payload));
|
|
7455
|
+
} finally {
|
|
7456
|
+
await handle.close();
|
|
7457
|
+
}
|
|
7458
|
+
const log = await open(agent.log, "a", 384);
|
|
7459
|
+
await log.close();
|
|
7460
|
+
try {
|
|
7461
|
+
await promisify2(execFile2)("/usr/bin/plutil", ["-convert", "xml1", plist], {
|
|
7462
|
+
timeout: 1e4,
|
|
7463
|
+
signal
|
|
7464
|
+
});
|
|
7465
|
+
await promisify2(execFile2)("/usr/bin/plutil", ["-lint", plist], { timeout: 1e4, signal });
|
|
7466
|
+
await promisify2(execFile2)("launchctl", ["bootstrap", domain, plist], {
|
|
7467
|
+
env,
|
|
7468
|
+
timeout: 1e4,
|
|
7469
|
+
signal
|
|
7470
|
+
});
|
|
7471
|
+
} catch {
|
|
7472
|
+
throw new Error(`launchd start failed; inspect ${plist} and ${agent.log}`);
|
|
7473
|
+
}
|
|
7474
|
+
};
|
|
7475
|
+
var launchWorkerAgent = async (config, creation, repoPath, signal) => startAgent(
|
|
7476
|
+
config,
|
|
7477
|
+
{
|
|
7478
|
+
label: `dev.fmap.baton-worker-creation-${creation.id}`,
|
|
7479
|
+
args: [config.nodePath ?? process.execPath, config.cliPath, "worker", "run"],
|
|
7480
|
+
cwd: repoPath,
|
|
7481
|
+
env: {
|
|
7482
|
+
PATH: config.workerEnv.PATH ?? process.env.PATH ?? "/usr/bin:/bin",
|
|
7483
|
+
...config.workerEnv,
|
|
7484
|
+
HOME: homedir(),
|
|
7485
|
+
XDG_DATA_HOME: workerStatePath(config, creation)
|
|
7486
|
+
},
|
|
7487
|
+
log: workerLogPath(creation)
|
|
7488
|
+
},
|
|
7489
|
+
signal
|
|
7490
|
+
);
|
|
7491
|
+
var installProvisionerAgent = async (config, configPath) => startAgent(config, {
|
|
7492
|
+
label: "dev.fmap.baton-provisioner",
|
|
7493
|
+
args: [
|
|
7494
|
+
config.nodePath ?? process.execPath,
|
|
7495
|
+
config.cliPath,
|
|
7496
|
+
"provisioner",
|
|
7497
|
+
"run",
|
|
7498
|
+
"--config",
|
|
7499
|
+
configPath
|
|
7500
|
+
],
|
|
7501
|
+
cwd: dirname(configPath),
|
|
7502
|
+
env: { HOME: homedir(), PATH: config.workerEnv.PATH ?? process.env.PATH ?? "/usr/bin:/bin" },
|
|
7503
|
+
log: join3(homedir(), "Library/Logs/baton-provisioner.log")
|
|
7504
|
+
});
|
|
7505
|
+
|
|
7506
|
+
// src/provisioner/executor.ts
|
|
7507
|
+
var directory = async (path) => {
|
|
7508
|
+
try {
|
|
7509
|
+
await mkdir2(path, { mode: 448 });
|
|
7510
|
+
} catch (error) {
|
|
7511
|
+
if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
|
|
7512
|
+
}
|
|
7513
|
+
const info = await lstat(path);
|
|
7514
|
+
if (!info.isDirectory() || info.isSymbolicLink()) throw new Error(`unsafe directory: ${path}`);
|
|
7515
|
+
};
|
|
7516
|
+
var provisionWorker = async (config, creation, client, signal) => {
|
|
7517
|
+
const repoPath = join4(
|
|
7518
|
+
config.projectsRoot,
|
|
7519
|
+
`project-${creation.projectId}`,
|
|
7520
|
+
`creation-${creation.id}`
|
|
7521
|
+
);
|
|
7522
|
+
const progress = (step) => client.report(creation.id, { status: "running", step, repoPath }, signal);
|
|
7523
|
+
await progress("directory");
|
|
7524
|
+
signal.throwIfAborted();
|
|
7525
|
+
await mkdir2(config.projectsRoot, { recursive: true, mode: 448 });
|
|
7526
|
+
const root = await realpath(config.projectsRoot);
|
|
7527
|
+
await directory(join4(root, `project-${creation.projectId}`));
|
|
7528
|
+
await mkdir2(join4(root, `project-${creation.projectId}`, `creation-${creation.id}`), {
|
|
7529
|
+
mode: 448
|
|
7530
|
+
});
|
|
7531
|
+
await progress("git");
|
|
7532
|
+
const git = async (args) => {
|
|
7533
|
+
try {
|
|
7534
|
+
await promisify3(execFile3)("git", ["-C", repoPath, ...args], {
|
|
7535
|
+
env: commandEnvironment(config),
|
|
7536
|
+
signal,
|
|
7537
|
+
timeout: 1e4
|
|
7538
|
+
});
|
|
7539
|
+
} catch (error) {
|
|
7540
|
+
signal.throwIfAborted();
|
|
7541
|
+
throw new Error("Git repository initialization failed", { cause: error });
|
|
7542
|
+
}
|
|
7543
|
+
};
|
|
7544
|
+
await git(["init", "-b", "main"]);
|
|
7545
|
+
await writeFile(join4(repoPath, ".git/info/exclude"), "\n.baton.json\n", { flag: "a" });
|
|
7546
|
+
await git([
|
|
7547
|
+
"-c",
|
|
7548
|
+
"user.name=Baton",
|
|
7549
|
+
"-c",
|
|
7550
|
+
"user.email=baton@localhost",
|
|
7551
|
+
"commit",
|
|
7552
|
+
"--allow-empty",
|
|
7553
|
+
"-m",
|
|
7554
|
+
"chore: initialize repository"
|
|
7555
|
+
]);
|
|
7556
|
+
await progress("register");
|
|
7557
|
+
const result = await client.register(creation.id, hostname(), signal);
|
|
7558
|
+
const state = workerStatePath(config, creation);
|
|
7559
|
+
await directory(join4(root, ".worker-state"));
|
|
7560
|
+
await directory(join4(root, ".worker-state", `project-${creation.projectId}`));
|
|
7561
|
+
await mkdir2(state, { mode: 448 });
|
|
7562
|
+
await mkdir2(join4(state, "baton"), { mode: 448 });
|
|
7563
|
+
await writeFile(join4(state, "baton/machine-id"), `${creation.machineId}
|
|
7564
|
+
`, {
|
|
7565
|
+
flag: "wx",
|
|
7566
|
+
mode: 384
|
|
7567
|
+
});
|
|
7568
|
+
const cfg = workerProjectConfig({
|
|
7569
|
+
server: config.server,
|
|
7570
|
+
projectId: creation.projectId,
|
|
7571
|
+
workerId: result.worker.id,
|
|
7572
|
+
name: result.worker.name,
|
|
7573
|
+
agentKind: result.worker.agentKind,
|
|
7574
|
+
machineId: result.worker.machineId,
|
|
7575
|
+
apiToken: result.apiToken
|
|
7576
|
+
});
|
|
7577
|
+
await writeFile(join4(repoPath, ".baton.json"), `${JSON.stringify(cfg, null, 2)}
|
|
7578
|
+
`, {
|
|
7579
|
+
flag: "wx",
|
|
7580
|
+
mode: 384
|
|
7581
|
+
});
|
|
7582
|
+
await progress("start");
|
|
7583
|
+
signal.throwIfAborted();
|
|
7584
|
+
await launchWorkerAgent(config, creation, repoPath, signal);
|
|
7585
|
+
await progress("connect");
|
|
7586
|
+
const deadline = Date.now() + 6e4;
|
|
7587
|
+
while (Date.now() < deadline) {
|
|
7588
|
+
const worker2 = await client.worker(result.worker.id, result.apiToken, signal);
|
|
7589
|
+
if (worker2.connected) {
|
|
7590
|
+
await client.report(creation.id, { status: "succeeded" }, signal);
|
|
7591
|
+
return;
|
|
7592
|
+
}
|
|
7593
|
+
await setTimeout2(1e3, void 0, { signal });
|
|
7594
|
+
}
|
|
7595
|
+
throw new Error("Worker did not connect within 60 seconds");
|
|
7596
|
+
};
|
|
7597
|
+
|
|
7598
|
+
// src/provisioner/daemon.ts
|
|
7599
|
+
var taskError = (error, config) => {
|
|
7600
|
+
const message = error instanceof Error ? error.message : "unknown creation error";
|
|
7601
|
+
return [config.token, ...Object.values(config.workerEnv)].filter((value) => value.length > 0).reduce((safe, secret) => safe.replaceAll(secret, "[redacted]"), message);
|
|
7602
|
+
};
|
|
7603
|
+
var processCreation = async (id, config, client, signal) => {
|
|
7604
|
+
let creation;
|
|
7605
|
+
try {
|
|
7606
|
+
creation = await client.get(id, signal);
|
|
7607
|
+
} catch (error) {
|
|
7608
|
+
if (error instanceof HttpError && error.status === 404) {
|
|
7609
|
+
console.error(`[provisioner] creation ${id} no longer exists`);
|
|
7610
|
+
return;
|
|
7611
|
+
}
|
|
7612
|
+
throw error;
|
|
7613
|
+
}
|
|
7614
|
+
if (creation.status === "succeeded" || creation.status === "failed") return;
|
|
7615
|
+
if (creation.status === "running") {
|
|
7616
|
+
await client.report(
|
|
7617
|
+
id,
|
|
7618
|
+
{
|
|
7619
|
+
status: "failed",
|
|
7620
|
+
error: "Provisioner interrupted; inspect retained resources before creating again"
|
|
7621
|
+
},
|
|
7622
|
+
signal
|
|
7623
|
+
);
|
|
7624
|
+
return;
|
|
7625
|
+
}
|
|
7626
|
+
try {
|
|
7627
|
+
await provisionWorker(config, creation, client, signal);
|
|
7628
|
+
} catch (error) {
|
|
7629
|
+
if (signal.aborted) {
|
|
7630
|
+
console.error(`[provisioner] creation ${id} interrupted: ${taskError(error, config)}`);
|
|
7631
|
+
return;
|
|
7632
|
+
}
|
|
7633
|
+
if (!(error instanceof HttpError) || ![401, 403].includes(error.status)) {
|
|
7634
|
+
const message = `${taskError(error, config)}; worker log: ${workerLogPath(creation)}`;
|
|
7635
|
+
console.error(`[provisioner] creation ${id}: ${message}`);
|
|
7636
|
+
await client.report(id, { status: "failed", error: message }, signal);
|
|
7637
|
+
return;
|
|
7638
|
+
}
|
|
7639
|
+
throw error;
|
|
7640
|
+
}
|
|
7641
|
+
};
|
|
7642
|
+
var runProvisioner = async (config, signal) => {
|
|
7643
|
+
const client = createProvisionerClient(config);
|
|
7644
|
+
const abort = new AbortController();
|
|
7645
|
+
const queued = /* @__PURE__ */ new Set();
|
|
7646
|
+
let tail = Promise.resolve();
|
|
7647
|
+
let fatal;
|
|
7648
|
+
let finish = () => {
|
|
7649
|
+
};
|
|
7650
|
+
const ended = new Promise((resolve4) => {
|
|
7651
|
+
finish = resolve4;
|
|
7652
|
+
});
|
|
7653
|
+
const stream = new EventSource(`${config.server}/provisioner/stream`, {
|
|
7654
|
+
fetch: (url, init2) => fetch(url, {
|
|
7655
|
+
...init2,
|
|
7656
|
+
headers: { ...init2?.headers, authorization: `Bearer ${config.token}` },
|
|
7657
|
+
signal: abort.signal
|
|
7658
|
+
})
|
|
7659
|
+
});
|
|
7660
|
+
const stop = () => {
|
|
7661
|
+
stream.close();
|
|
7662
|
+
abort.abort();
|
|
7663
|
+
finish();
|
|
7664
|
+
};
|
|
7665
|
+
const fail = (error) => {
|
|
7666
|
+
fatal = error;
|
|
7667
|
+
console.error(`[provisioner] ${taskError(error, config)}`);
|
|
7668
|
+
stop();
|
|
7669
|
+
};
|
|
7670
|
+
stream.onmessage = (event) => {
|
|
7671
|
+
try {
|
|
7672
|
+
const message = JSON.parse(event.data);
|
|
7673
|
+
if (!message || !Number.isSafeInteger(message.creationId) || message.creationId <= 0 || !["create", "inspect"].includes(message.action))
|
|
7674
|
+
throw new Error("invalid provisioner signal");
|
|
7675
|
+
if (queued.has(message.creationId)) return;
|
|
7676
|
+
queued.add(message.creationId);
|
|
7677
|
+
tail = tail.then(async () => {
|
|
7678
|
+
try {
|
|
7679
|
+
if (!abort.signal.aborted)
|
|
7680
|
+
await processCreation(message.creationId, config, client, abort.signal);
|
|
7681
|
+
} finally {
|
|
7682
|
+
queued.delete(message.creationId);
|
|
7683
|
+
}
|
|
7684
|
+
}).catch((error) => {
|
|
7685
|
+
if (!abort.signal.aborted) fail(error);
|
|
7686
|
+
else console.error(`[provisioner] stopped: ${taskError(error, config)}`);
|
|
7687
|
+
});
|
|
7688
|
+
} catch (error) {
|
|
7689
|
+
fail(error);
|
|
7690
|
+
}
|
|
7691
|
+
};
|
|
7692
|
+
stream.onerror = (event) => {
|
|
7693
|
+
if (stream.readyState === EventSource.CLOSED || [401, 403, 409].includes(event.code ?? 0))
|
|
7694
|
+
fail(new Error(`provisioner stream rejected: HTTP ${event.code}`));
|
|
7695
|
+
else
|
|
7696
|
+
console.error(`[provisioner] stream disconnected (${event.code ?? "network"}); reconnecting`);
|
|
7697
|
+
};
|
|
7698
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
7699
|
+
if (signal.aborted) stop();
|
|
7700
|
+
console.log("[provisioner] waiting for worker creation requests");
|
|
7701
|
+
try {
|
|
7702
|
+
await ended;
|
|
7703
|
+
await tail;
|
|
7704
|
+
} finally {
|
|
7705
|
+
stop();
|
|
7706
|
+
signal.removeEventListener("abort", stop);
|
|
7707
|
+
}
|
|
7708
|
+
if (fatal !== void 0) throw new Error(taskError(fatal, config));
|
|
7709
|
+
};
|
|
7710
|
+
|
|
7711
|
+
// src/commands/provisioner.ts
|
|
7712
|
+
var configArg = {
|
|
7713
|
+
config: {
|
|
7714
|
+
type: "string",
|
|
7715
|
+
required: true,
|
|
7716
|
+
description: "absolute path to private provisioner JSON config"
|
|
7717
|
+
}
|
|
7718
|
+
};
|
|
7719
|
+
var provisioner = defineCommand({
|
|
7720
|
+
meta: {
|
|
7721
|
+
name: "provisioner",
|
|
7722
|
+
description: "prepare local repositories and launch workers on macOS"
|
|
7723
|
+
},
|
|
7724
|
+
subCommands: {
|
|
7725
|
+
run: defineCommand({
|
|
7726
|
+
args: configArg,
|
|
7727
|
+
run: async ({ args }) => {
|
|
7728
|
+
const config = await loadProvisionerConfig(args.config);
|
|
7729
|
+
const abort = new AbortController();
|
|
7730
|
+
const stop = () => abort.abort();
|
|
7731
|
+
process.on("SIGINT", stop);
|
|
7732
|
+
process.on("SIGTERM", stop);
|
|
7733
|
+
try {
|
|
7734
|
+
await runProvisioner(config, abort.signal);
|
|
7735
|
+
} finally {
|
|
7736
|
+
process.off("SIGINT", stop);
|
|
7737
|
+
process.off("SIGTERM", stop);
|
|
7738
|
+
}
|
|
7739
|
+
}
|
|
7740
|
+
}),
|
|
7741
|
+
install: defineCommand({
|
|
7742
|
+
args: configArg,
|
|
7743
|
+
run: async ({ args }) => {
|
|
7744
|
+
const config = await loadProvisionerConfig(args.config);
|
|
7745
|
+
await installProvisionerAgent(config, args.config);
|
|
7746
|
+
console.log(
|
|
7747
|
+
"installed dev.fmap.baton-provisioner; inspect ~/Library/Logs/baton-provisioner.log"
|
|
7748
|
+
);
|
|
7749
|
+
}
|
|
7750
|
+
})
|
|
7751
|
+
}
|
|
7752
|
+
});
|
|
7753
|
+
|
|
7267
7754
|
// src/commands/relay.ts
|
|
7268
7755
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
7269
7756
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
7270
|
-
import { join as
|
|
7757
|
+
import { join as join5 } from "node:path";
|
|
7271
7758
|
|
|
7272
7759
|
// src/client/relay.ts
|
|
7273
7760
|
var relayClient = (baseUrl) => {
|
|
@@ -7395,11 +7882,11 @@ var listenCommand2 = defineCommand({
|
|
|
7395
7882
|
process.stdout.write(`${JSON.stringify(o)}
|
|
7396
7883
|
`);
|
|
7397
7884
|
};
|
|
7398
|
-
const spillDir =
|
|
7885
|
+
const spillDir = join5(tmpdir2(), "hotline", args.channel);
|
|
7399
7886
|
mkdirSync2(spillDir, { recursive: true });
|
|
7400
7887
|
const surface = (m) => {
|
|
7401
7888
|
const text = typeof m.text === "string" ? m.text : "";
|
|
7402
|
-
const file =
|
|
7889
|
+
const file = join5(spillDir, `${m.seq}.txt`);
|
|
7403
7890
|
writeFileSync4(file, text);
|
|
7404
7891
|
const truncated = text.length > PREVIEW2;
|
|
7405
7892
|
emit({
|
|
@@ -7426,10 +7913,10 @@ var listenCommand2 = defineCommand({
|
|
|
7426
7913
|
onError: (e) => emit({ type: "relay.error", error: describe2(e) })
|
|
7427
7914
|
});
|
|
7428
7915
|
emit({ type: "relay.listening", channel: args.channel, from: me, url });
|
|
7429
|
-
await new Promise((
|
|
7916
|
+
await new Promise((resolve4) => {
|
|
7430
7917
|
const stop = () => {
|
|
7431
7918
|
close();
|
|
7432
|
-
|
|
7919
|
+
resolve4();
|
|
7433
7920
|
};
|
|
7434
7921
|
process.on("SIGINT", stop);
|
|
7435
7922
|
process.on("SIGTERM", stop);
|
|
@@ -7553,6 +8040,9 @@ var requirement = defineCommand({
|
|
|
7553
8040
|
}
|
|
7554
8041
|
});
|
|
7555
8042
|
|
|
8043
|
+
// src/commands/service.ts
|
|
8044
|
+
import { resolve as resolve2 } from "node:path";
|
|
8045
|
+
|
|
7556
8046
|
// src/worker/service-child.ts
|
|
7557
8047
|
import { spawn } from "node:child_process";
|
|
7558
8048
|
|
|
@@ -7633,25 +8123,15 @@ var formatUptime = (startedAt, now = Date.now()) => {
|
|
|
7633
8123
|
if (seconds >= 60) return `${Math.floor(seconds / 60)}m`;
|
|
7634
8124
|
return `${seconds}s`;
|
|
7635
8125
|
};
|
|
7636
|
-
var currentContext = () => {
|
|
7637
|
-
const config = loadProjectConfig(projectConfigPath());
|
|
7638
|
-
if (!config.worker || !config.session)
|
|
7639
|
-
throw new Error("service run requires a Baton session worktree");
|
|
7640
|
-
return { workerId: config.worker.id, sessionId: config.session };
|
|
7641
|
-
};
|
|
7642
|
-
var currentWorker = () => {
|
|
7643
|
-
const worker2 = loadProjectConfig(projectConfigPath()).worker;
|
|
7644
|
-
if (!worker2) throw new Error("no worker in scope");
|
|
7645
|
-
return worker2.id;
|
|
7646
|
-
};
|
|
7647
8126
|
var parseTarget = (target) => {
|
|
7648
8127
|
const explicit = target.match(/^W-(\d+)\/(.+)$/i);
|
|
7649
|
-
return explicit ? { workerId: Number(explicit[1]), name: explicit[2] ?? "" } : { workerId:
|
|
8128
|
+
return explicit ? { workerId: Number(explicit[1]), name: explicit[2] ?? "" } : { workerId: resolveWorkerId(), name: target };
|
|
7650
8129
|
};
|
|
7651
8130
|
var runCommand2 = defineCommand({
|
|
7652
8131
|
meta: { name: "run", description: "run a service on the current worker" },
|
|
7653
8132
|
args: {
|
|
7654
8133
|
name: { type: "positional", required: true, description: "service name" },
|
|
8134
|
+
cwd: { type: "string", required: true, description: "service working directory on the Worker" },
|
|
7655
8135
|
"public-url": { type: "string", description: "public HTTP(S) URL for this service" },
|
|
7656
8136
|
note: { type: "string", description: "short note describing this service" },
|
|
7657
8137
|
...common
|
|
@@ -7666,9 +8146,9 @@ var runCommand2 = defineCommand({
|
|
|
7666
8146
|
throw new Error("--public-url must be an absolute HTTP(S) URL");
|
|
7667
8147
|
if (note && !isServiceNote(note))
|
|
7668
8148
|
throw new Error("service note must be 500 characters or fewer");
|
|
7669
|
-
const
|
|
7670
|
-
const result = await clientFor(args).services.run(
|
|
7671
|
-
|
|
8149
|
+
const workerId = resolveWorkerId();
|
|
8150
|
+
const result = await clientFor(args).services.run(workerId, {
|
|
8151
|
+
cwd: resolve2(args.cwd),
|
|
7672
8152
|
name: args.name,
|
|
7673
8153
|
argv,
|
|
7674
8154
|
...publicUrl ? { publicUrl } : {},
|
|
@@ -7676,7 +8156,7 @@ var runCommand2 = defineCommand({
|
|
|
7676
8156
|
});
|
|
7677
8157
|
if (!result.ok) throw new Error(result.error);
|
|
7678
8158
|
if (args.json) return console.log(toJson(result));
|
|
7679
|
-
console.log(`running W-${
|
|
8159
|
+
console.log(`running W-${workerId}/${args.name}`);
|
|
7680
8160
|
if (publicUrl) console.log(`public: ${publicUrl}`);
|
|
7681
8161
|
if (result.logPath) console.log(`logs: ${result.logPath}`);
|
|
7682
8162
|
}
|
|
@@ -7693,7 +8173,7 @@ var lsCommand = defineCommand({
|
|
|
7693
8173
|
const workers = await client.workers.listByProject(projectId);
|
|
7694
8174
|
const names = new Map(workers.map((worker2) => [worker2.id, worker2.name]));
|
|
7695
8175
|
services.sort((a, b) => a.workerId - b.workerId || a.name.localeCompare(b.name)).map(
|
|
7696
|
-
(item) => `${serviceHandle(item)} ${names.get(item.workerId) ?? "unknown"}
|
|
8176
|
+
(item) => `${serviceHandle(item)} ${names.get(item.workerId) ?? "unknown"} cwd=${item.cwd} uptime=${formatUptime(item.startedAt)}`
|
|
7697
8177
|
).forEach((line) => {
|
|
7698
8178
|
console.log(line);
|
|
7699
8179
|
});
|
|
@@ -7743,7 +8223,7 @@ var sessionAbortCommand = defineCommand({
|
|
|
7743
8223
|
var sessionClearCommand = defineCommand({
|
|
7744
8224
|
meta: {
|
|
7745
8225
|
name: "clear",
|
|
7746
|
-
description: "reset the conversation context (keeps the session +
|
|
8226
|
+
description: "reset the conversation context (keeps the session + directory)"
|
|
7747
8227
|
},
|
|
7748
8228
|
args: {
|
|
7749
8229
|
session: { type: "positional", required: true, description: "session int id or name" },
|
|
@@ -7852,7 +8332,7 @@ var sessionResumeCommand = defineCommand({
|
|
|
7852
8332
|
|
|
7853
8333
|
// src/commands/session/rm.ts
|
|
7854
8334
|
var sessionRmCommand = defineCommand({
|
|
7855
|
-
meta: { name: "rm", description: "delete a session (irreversible; removes its
|
|
8335
|
+
meta: { name: "rm", description: "delete a session (irreversible; removes its runtime files)" },
|
|
7856
8336
|
args: {
|
|
7857
8337
|
session: { type: "positional", required: true, description: "session int id or name" },
|
|
7858
8338
|
project: { type: "string", description: "project id (overrides .baton.json)" },
|
|
@@ -7863,7 +8343,9 @@ var sessionRmCommand = defineCommand({
|
|
|
7863
8343
|
const c = clientFor(args);
|
|
7864
8344
|
const handle = await resolveSession(c, resolveProjectId(args), args.session);
|
|
7865
8345
|
if (!args.confirm) {
|
|
7866
|
-
console.log(
|
|
8346
|
+
console.log(
|
|
8347
|
+
`[dry-run] would delete session ${handle.name} (#${handle.id}) + its runtime files`
|
|
8348
|
+
);
|
|
7867
8349
|
console.log("re-run with --confirm to proceed.");
|
|
7868
8350
|
return;
|
|
7869
8351
|
}
|
|
@@ -7879,12 +8361,12 @@ import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
|
7879
8361
|
// src/session/runner/transcript.ts
|
|
7880
8362
|
import { readdirSync, readFileSync as readFileSync4, statSync } from "node:fs";
|
|
7881
8363
|
import { homedir as homedir2 } from "node:os";
|
|
7882
|
-
import { join as
|
|
8364
|
+
import { join as join6 } from "node:path";
|
|
7883
8365
|
var findTranscriptPath = (agentSessionId) => {
|
|
7884
|
-
const root =
|
|
8366
|
+
const root = join6(homedir2(), ".claude", "projects");
|
|
7885
8367
|
try {
|
|
7886
8368
|
for (const dir of readdirSync(root)) {
|
|
7887
|
-
const candidate =
|
|
8369
|
+
const candidate = join6(root, dir, `${agentSessionId}.jsonl`);
|
|
7888
8370
|
try {
|
|
7889
8371
|
if (statSync(candidate).isFile()) return candidate;
|
|
7890
8372
|
} catch {
|
|
@@ -7962,16 +8444,15 @@ var readFirstExchange = (agentSessionId) => {
|
|
|
7962
8444
|
};
|
|
7963
8445
|
|
|
7964
8446
|
// src/session/runner/turn.ts
|
|
7965
|
-
import { mkdir as
|
|
7966
|
-
import { join as
|
|
8447
|
+
import { mkdir as mkdir4, writeFile as writeFile2 } from "node:fs/promises";
|
|
8448
|
+
import { join as join10 } from "node:path";
|
|
7967
8449
|
|
|
7968
8450
|
// src/session/runner/attachments.ts
|
|
7969
8451
|
import { createWriteStream } from "node:fs";
|
|
7970
|
-
import { mkdir
|
|
7971
|
-
import { join as
|
|
8452
|
+
import { mkdir as mkdir3 } from "node:fs/promises";
|
|
8453
|
+
import { join as join7 } from "node:path";
|
|
7972
8454
|
import { Readable } from "node:stream";
|
|
7973
8455
|
import { pipeline } from "node:stream/promises";
|
|
7974
|
-
var ATTACH_DIR = "attachments";
|
|
7975
8456
|
var dedupe = (name, taken) => {
|
|
7976
8457
|
if (!taken.has(name)) {
|
|
7977
8458
|
taken.add(name);
|
|
@@ -7991,25 +8472,24 @@ var dedupe = (name, taken) => {
|
|
|
7991
8472
|
};
|
|
7992
8473
|
var materializeAttachments = async (opts) => {
|
|
7993
8474
|
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
7994
|
-
const dir =
|
|
7995
|
-
await
|
|
7996
|
-
|
|
7997
|
-
const
|
|
7998
|
-
const relPaths = [];
|
|
8475
|
+
const dir = opts.directory;
|
|
8476
|
+
await mkdir3(dir, { recursive: true });
|
|
8477
|
+
const taken = /* @__PURE__ */ new Set();
|
|
8478
|
+
const paths = [];
|
|
7999
8479
|
for (const att of opts.attachments) {
|
|
8000
8480
|
const name = dedupe(att.filename, taken);
|
|
8001
8481
|
const res = await fetchImpl(`${opts.serverBase}${att.url}`);
|
|
8002
8482
|
if (!res.ok || !res.body) throw new Error(`attachment ${att.id} \u2192 ${res.status}`);
|
|
8003
|
-
await pipeline(Readable.fromWeb(res.body), createWriteStream(
|
|
8004
|
-
|
|
8483
|
+
await pipeline(Readable.fromWeb(res.body), createWriteStream(join7(dir, name)));
|
|
8484
|
+
paths.push(join7(dir, name));
|
|
8005
8485
|
}
|
|
8006
|
-
return
|
|
8486
|
+
return paths;
|
|
8007
8487
|
};
|
|
8008
|
-
var augmentPrompt = (text,
|
|
8009
|
-
if (
|
|
8488
|
+
var augmentPrompt = (text, paths, labels) => {
|
|
8489
|
+
if (paths.length === 0) return text;
|
|
8010
8490
|
const header = [
|
|
8011
|
-
"[Attached files \u2014 already saved
|
|
8012
|
-
...
|
|
8491
|
+
"[Attached files \u2014 already saved locally. Reference them by label:]",
|
|
8492
|
+
...paths.map((p, i) => `- {${labels[i]}} ${p}`)
|
|
8013
8493
|
].join("\n");
|
|
8014
8494
|
return text.length > 0 ? `${header}
|
|
8015
8495
|
|
|
@@ -8017,18 +8497,18 @@ ${text}` : header;
|
|
|
8017
8497
|
};
|
|
8018
8498
|
|
|
8019
8499
|
// src/session/runner/codex.ts
|
|
8500
|
+
import { join as join8 } from "node:path";
|
|
8020
8501
|
import { Codex } from "@openai/codex-sdk";
|
|
8021
8502
|
|
|
8022
8503
|
// src/session/runner/sdk-env.ts
|
|
8023
8504
|
import { existsSync as existsSync3 } from "node:fs";
|
|
8024
|
-
import { isAbsolute } from "node:path";
|
|
8505
|
+
import { isAbsolute as isAbsolute2 } from "node:path";
|
|
8025
8506
|
var buildSdkEnv = (overlay) => overlay ? { ...process.env, ...overlay } : void 0;
|
|
8026
8507
|
var claudeExecutable = () => process.env.BATON_CLAUDE_BIN;
|
|
8027
|
-
var additionalDirs = () => {
|
|
8508
|
+
var additionalDirs = (required = []) => {
|
|
8028
8509
|
const raw = process.env.BATON_ADD_DIRS;
|
|
8029
|
-
|
|
8030
|
-
|
|
8031
|
-
return dirs.length > 0 ? dirs : void 0;
|
|
8510
|
+
const dirs = [...required, ...(raw ?? "").split(":")].map((s) => s.trim()).filter((s) => s !== "" && isAbsolute2(s) && existsSync3(s));
|
|
8511
|
+
return dirs.length > 0 ? [...new Set(dirs)] : void 0;
|
|
8032
8512
|
};
|
|
8033
8513
|
var truthy = (value) => {
|
|
8034
8514
|
if (value === void 0) return void 0;
|
|
@@ -8188,7 +8668,7 @@ async function* startCodexEvents(config, input, worker2, opts) {
|
|
|
8188
8668
|
const env = compactEnv(buildSdkEnv(opts.envOverlay));
|
|
8189
8669
|
const codex = new Codex(env ? { env } : void 0);
|
|
8190
8670
|
const resuming = !config.agentSessionId.startsWith("pending:");
|
|
8191
|
-
const addDirs = additionalDirs();
|
|
8671
|
+
const addDirs = additionalDirs([join8(config.runtimeDir, "attachments")]);
|
|
8192
8672
|
const sandboxMode = codexSandboxMode(opts.planMode ?? false);
|
|
8193
8673
|
const approvalPolicy = codexApprovalPolicy();
|
|
8194
8674
|
const networkAccessEnabled = codexNetworkAccess();
|
|
@@ -8199,7 +8679,7 @@ async function* startCodexEvents(config, input, worker2, opts) {
|
|
|
8199
8679
|
if (opts.model) opts.log(`[model] ${opts.model}`);
|
|
8200
8680
|
if (reasoningEffort) opts.log(`[effort] ${reasoningEffort}`);
|
|
8201
8681
|
const threadOptions = {
|
|
8202
|
-
workingDirectory: config.
|
|
8682
|
+
workingDirectory: config.cwd,
|
|
8203
8683
|
skipGitRepoCheck: true,
|
|
8204
8684
|
sandboxMode,
|
|
8205
8685
|
approvalPolicy,
|
|
@@ -8218,8 +8698,7 @@ async function* startCodexEvents(config, input, worker2, opts) {
|
|
|
8218
8698
|
if (event.type === "thread.started" && !resuming && event.sessionId) {
|
|
8219
8699
|
config.agentSessionId = event.sessionId;
|
|
8220
8700
|
await worker2.materialize({
|
|
8221
|
-
agentSessionId: event.sessionId
|
|
8222
|
-
worktreePath: config.worktreePath
|
|
8701
|
+
agentSessionId: event.sessionId
|
|
8223
8702
|
});
|
|
8224
8703
|
opts.log(`[codex] thread ${event.sessionId}`);
|
|
8225
8704
|
}
|
|
@@ -8228,17 +8707,18 @@ async function* startCodexEvents(config, input, worker2, opts) {
|
|
|
8228
8707
|
}
|
|
8229
8708
|
|
|
8230
8709
|
// src/session/runner/query.ts
|
|
8710
|
+
import { join as join9 } from "node:path";
|
|
8231
8711
|
var startQuery = (config, text, resuming, queryFn, abortController, log, { envOverlay, planMode = false, model, effort } = {}) => {
|
|
8232
8712
|
const env = buildSdkEnv(envOverlay);
|
|
8233
8713
|
const exe = claudeExecutable();
|
|
8234
|
-
const addDirs = additionalDirs();
|
|
8714
|
+
const addDirs = additionalDirs([join9(config.runtimeDir, "attachments")]);
|
|
8235
8715
|
const level = claudeEffort(effort);
|
|
8236
8716
|
if (addDirs) log(`[add-dir] ${addDirs.join(", ")}`);
|
|
8237
8717
|
if (planMode) log("[plan] read-only planning turn");
|
|
8238
8718
|
if (model) log(`[model] ${model}`);
|
|
8239
8719
|
if (level) log(`[effort] ${level}`);
|
|
8240
8720
|
const options = {
|
|
8241
|
-
cwd: config.
|
|
8721
|
+
cwd: config.cwd,
|
|
8242
8722
|
permissionMode: planMode ? "plan" : "bypassPermissions",
|
|
8243
8723
|
// Headless relay: no TTY/TUI can answer an interactive ask, so AskUserQuestion
|
|
8244
8724
|
// would fail the whole turn. Block it — the model asks in plain text instead,
|
|
@@ -8482,15 +8962,19 @@ var UnsettledExecution = class extends Error {
|
|
|
8482
8962
|
var materializeImages = async (config, execution) => {
|
|
8483
8963
|
const images = execution.message.payload.images ?? [];
|
|
8484
8964
|
if (!images.length) return [];
|
|
8485
|
-
const
|
|
8486
|
-
|
|
8487
|
-
|
|
8965
|
+
const directory2 = join10(
|
|
8966
|
+
config.runtimeDir,
|
|
8967
|
+
"attachments",
|
|
8968
|
+
`message-${execution.message.id}`,
|
|
8969
|
+
"pasted"
|
|
8970
|
+
);
|
|
8971
|
+
await mkdir4(directory2, { recursive: true });
|
|
8488
8972
|
return Promise.all(
|
|
8489
8973
|
images.map(async (image, index) => {
|
|
8490
8974
|
const match = /^data:image\/(png|jpeg|webp|gif);base64,([A-Za-z0-9+/=\r\n]+)$/.exec(image);
|
|
8491
8975
|
if (!match) throw new Error("unsupported pasted image");
|
|
8492
|
-
const path = `
|
|
8493
|
-
await writeFile2(
|
|
8976
|
+
const path = join10(directory2, `pasted-${index + 1}.${match[1]}`);
|
|
8977
|
+
await writeFile2(path, Buffer.from(match[2] ?? "", "base64"));
|
|
8494
8978
|
return path;
|
|
8495
8979
|
})
|
|
8496
8980
|
);
|
|
@@ -8498,7 +8982,12 @@ var materializeImages = async (config, execution) => {
|
|
|
8498
8982
|
var buildPrompt = async (config, execution, fetchImpl) => {
|
|
8499
8983
|
const { text: rawText, attachments = [] } = execution.message.payload;
|
|
8500
8984
|
const paths = attachments.length ? await materializeAttachments({
|
|
8501
|
-
|
|
8985
|
+
directory: join10(
|
|
8986
|
+
config.runtimeDir,
|
|
8987
|
+
"attachments",
|
|
8988
|
+
`message-${execution.message.id}`,
|
|
8989
|
+
"files"
|
|
8990
|
+
),
|
|
8502
8991
|
serverBase: config.server,
|
|
8503
8992
|
attachments,
|
|
8504
8993
|
fetchImpl
|
|
@@ -8515,7 +9004,7 @@ var buildPrompt = async (config, execution, fetchImpl) => {
|
|
|
8515
9004
|
];
|
|
8516
9005
|
const images = imagePaths.map((path) => ({
|
|
8517
9006
|
type: "local_image",
|
|
8518
|
-
path
|
|
9007
|
+
path
|
|
8519
9008
|
}));
|
|
8520
9009
|
return { text, input: images.length ? [{ type: "text", text }, ...images] : text };
|
|
8521
9010
|
};
|
|
@@ -8632,8 +9121,8 @@ var runDaemon = async (config, deps, signal) => {
|
|
|
8632
9121
|
let lastActivity = Date.now();
|
|
8633
9122
|
let stop = () => {
|
|
8634
9123
|
};
|
|
8635
|
-
const stopped = new Promise((
|
|
8636
|
-
stop =
|
|
9124
|
+
const stopped = new Promise((resolve4) => {
|
|
9125
|
+
stop = resolve4;
|
|
8637
9126
|
});
|
|
8638
9127
|
const drain = async () => {
|
|
8639
9128
|
if (draining || closed || stranded) return;
|
|
@@ -8745,6 +9234,19 @@ var runDaemon = async (config, deps, signal) => {
|
|
|
8745
9234
|
}
|
|
8746
9235
|
};
|
|
8747
9236
|
|
|
9237
|
+
// src/worker/runtime-paths.ts
|
|
9238
|
+
import { createHash } from "node:crypto";
|
|
9239
|
+
import { homedir as homedir3 } from "node:os";
|
|
9240
|
+
import { join as join11 } from "node:path";
|
|
9241
|
+
var workerRuntimeDir = (owner, env = process.env) => join11(
|
|
9242
|
+
env.XDG_DATA_HOME ?? join11(env.HOME ?? homedir3(), ".local/share"),
|
|
9243
|
+
"baton",
|
|
9244
|
+
"runtime",
|
|
9245
|
+
createHash("sha256").update(owner.server).digest("hex"),
|
|
9246
|
+
`worker-${owner.workerId}`
|
|
9247
|
+
);
|
|
9248
|
+
var sessionRuntimeDir = (owner, sessionId, env = process.env) => join11(workerRuntimeDir(owner, env), "sessions", String(sessionId));
|
|
9249
|
+
|
|
8748
9250
|
// src/commands/session/run.ts
|
|
8749
9251
|
var bearerFetch = (token) => (input, init2) => fetch(input, {
|
|
8750
9252
|
...init2,
|
|
@@ -8774,13 +9276,13 @@ var sessionRunCommand = defineCommand({
|
|
|
8774
9276
|
},
|
|
8775
9277
|
run: async ({ args }) => {
|
|
8776
9278
|
if (!process.connected) throw new Error("session runner requires supervisor IPC");
|
|
8777
|
-
await new Promise((
|
|
9279
|
+
await new Promise((resolve4, reject) => {
|
|
8778
9280
|
const onMessage = (message) => {
|
|
8779
9281
|
if (typeof message !== "object" || message === null || !("type" in message) || message.type !== "execute")
|
|
8780
9282
|
return;
|
|
8781
9283
|
process.off("message", onMessage);
|
|
8782
9284
|
process.off("disconnect", onDisconnect);
|
|
8783
|
-
|
|
9285
|
+
resolve4();
|
|
8784
9286
|
};
|
|
8785
9287
|
const onDisconnect = () => {
|
|
8786
9288
|
process.off("message", onMessage);
|
|
@@ -8799,7 +9301,7 @@ var sessionRunCommand = defineCommand({
|
|
|
8799
9301
|
throw new Error(`invalid session id: ${args.session}`);
|
|
8800
9302
|
const client = createClient(server, { bearer: workerToken });
|
|
8801
9303
|
const s = await client.sessions.get(sessionId);
|
|
8802
|
-
if (!s.agentSessionId || !s.
|
|
9304
|
+
if (!s.agentSessionId || !s.cwd)
|
|
8803
9305
|
throw new Error(`session #${sessionId} is not materialized yet`);
|
|
8804
9306
|
const config = {
|
|
8805
9307
|
server,
|
|
@@ -8807,7 +9309,8 @@ var sessionRunCommand = defineCommand({
|
|
|
8807
9309
|
name: s.name,
|
|
8808
9310
|
agentKind: s.agentKind,
|
|
8809
9311
|
agentSessionId: s.agentSessionId,
|
|
8810
|
-
|
|
9312
|
+
cwd: s.cwd,
|
|
9313
|
+
runtimeDir: sessionRuntimeDir({ server, workerId: s.workerId }, sessionId)
|
|
8811
9314
|
};
|
|
8812
9315
|
const worker2 = createWorkerClient(server, workerToken, sessionId);
|
|
8813
9316
|
const ac = new AbortController();
|
|
@@ -9100,184 +9603,60 @@ var task = defineCommand({
|
|
|
9100
9603
|
// src/commands/worker/index.ts
|
|
9101
9604
|
import { hostname as osHostname } from "node:os";
|
|
9102
9605
|
|
|
9103
|
-
// src/
|
|
9104
|
-
import {
|
|
9606
|
+
// src/worker/bundled-skills.ts
|
|
9607
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
9608
|
+
import { cpSync, existsSync as existsSync5 } from "node:fs";
|
|
9609
|
+
import { dirname as dirname3, join as join12 } from "node:path";
|
|
9610
|
+
import { fileURLToPath } from "node:url";
|
|
9611
|
+
|
|
9612
|
+
// src/worker/git-exclude.ts
|
|
9613
|
+
import { spawnSync } from "node:child_process";
|
|
9105
9614
|
import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync5 } from "node:fs";
|
|
9106
|
-
import { dirname, isAbsolute as
|
|
9107
|
-
|
|
9108
|
-
|
|
9109
|
-
|
|
9110
|
-
|
|
9111
|
-
});
|
|
9112
|
-
var commandError = (result) => {
|
|
9113
|
-
const raw = result.stderr || result.stdout || result.error?.message || `exit ${result.status}`;
|
|
9114
|
-
return raw.trim().replace(/(https?:\/\/)[^/@\s]+@/g, "$1***@");
|
|
9115
|
-
};
|
|
9116
|
-
var execFileAsync = promisify(execFile);
|
|
9117
|
-
var DEFAULT_SYNC_TIMEOUT_MS = 6e4;
|
|
9118
|
-
var asyncCommandError = (error, timeoutMs) => {
|
|
9119
|
-
const failure = error;
|
|
9120
|
-
if (failure.killed || failure.code === "ETIMEDOUT") return `timed out after ${timeoutMs}ms`;
|
|
9121
|
-
const raw = failure.stderr || failure.stdout || failure.message || String(error);
|
|
9122
|
-
return raw.trim().replace(/(https?:\/\/)[^/@\s]+@/g, "$1***@");
|
|
9123
|
-
};
|
|
9124
|
-
var validateBaseBranch = (branch) => {
|
|
9125
|
-
const normalized = branch.trim();
|
|
9126
|
-
const valid = spawnSync("git", ["check-ref-format", "--branch", normalized], {
|
|
9127
|
-
stdio: "pipe",
|
|
9128
|
-
encoding: "utf8"
|
|
9129
|
-
});
|
|
9130
|
-
if (valid.status !== 0)
|
|
9131
|
-
throw new Error(`invalid base branch "${normalized}": ${commandError(valid)}`);
|
|
9132
|
-
return normalized;
|
|
9133
|
-
};
|
|
9134
|
-
var createWorktree = (input) => {
|
|
9135
|
-
if (existsSync4(input.worktreePath))
|
|
9136
|
-
throw new Error(`worktree path already exists: ${input.worktreePath}`);
|
|
9137
|
-
const head = spawnSync("git", ["-C", input.repo, "rev-parse", "--verify", "-q", "HEAD"], {
|
|
9138
|
-
stdio: "pipe",
|
|
9139
|
-
encoding: "utf8"
|
|
9140
|
-
});
|
|
9141
|
-
if (head.status !== 0)
|
|
9142
|
-
throw new Error(
|
|
9143
|
-
`repo has no commits (unborn HEAD) \u2014 create an initial commit before starting sessions`
|
|
9144
|
-
);
|
|
9145
|
-
mkdirSync3(dirname(input.worktreePath), { recursive: true });
|
|
9146
|
-
const branch = `baton/${input.sessionCode}`;
|
|
9147
|
-
const r = spawnSync(
|
|
9148
|
-
"git",
|
|
9149
|
-
["-C", input.repo, "worktree", "add", "-b", branch, input.worktreePath, input.base],
|
|
9150
|
-
{ stdio: "pipe", encoding: "utf8" }
|
|
9151
|
-
);
|
|
9152
|
-
if (r.status !== 0) {
|
|
9153
|
-
throw new Error(`git worktree add failed: ${r.stderr || r.stdout || `exit ${r.status}`}`);
|
|
9154
|
-
}
|
|
9155
|
-
};
|
|
9156
|
-
var repoHeadBranch = (repo) => {
|
|
9157
|
-
const r = git(repo, ["symbolic-ref", "--short", "HEAD"]);
|
|
9158
|
-
const branch = r.status === 0 ? r.stdout.trim() : "";
|
|
9159
|
-
return branch || "main";
|
|
9160
|
-
};
|
|
9161
|
-
var syncBaseBranch = async (repo, branch, options = {}) => {
|
|
9162
|
-
const checkedBranch = validateBaseBranch(branch);
|
|
9163
|
-
const env = options.env ?? process.env;
|
|
9164
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_SYNC_TIMEOUT_MS;
|
|
9165
|
-
const origin = git(repo, ["remote", "get-url", "origin"]);
|
|
9166
|
-
if (origin.status !== 0) {
|
|
9167
|
-
const localRef = `refs/heads/${checkedBranch}`;
|
|
9168
|
-
const verified2 = git(repo, ["rev-parse", "--verify", "-q", localRef]);
|
|
9169
|
-
if (verified2.status !== 0) throw new Error(`local base branch does not exist: ${checkedBranch}`);
|
|
9170
|
-
return localRef;
|
|
9171
|
-
}
|
|
9172
|
-
const helper = env.BATON_GIT_SYNC_BIN?.trim();
|
|
9173
|
-
const command = helper ?? "git";
|
|
9174
|
-
const args = helper ? [repo, checkedBranch] : [
|
|
9175
|
-
"-C",
|
|
9176
|
-
repo,
|
|
9177
|
-
"fetch",
|
|
9178
|
-
"--no-tags",
|
|
9179
|
-
"origin",
|
|
9180
|
-
`+refs/heads/${checkedBranch}:refs/remotes/origin/${checkedBranch}`
|
|
9181
|
-
];
|
|
9182
|
-
let synced;
|
|
9183
|
-
try {
|
|
9184
|
-
synced = await execFileAsync(command, args, {
|
|
9185
|
-
encoding: "utf8",
|
|
9186
|
-
env: { ...env, GIT_TERMINAL_PROMPT: "0" },
|
|
9187
|
-
timeout: timeoutMs
|
|
9188
|
-
});
|
|
9189
|
-
} catch (error) {
|
|
9190
|
-
const via = helper ? ` via ${helper}` : "";
|
|
9191
|
-
throw new Error(
|
|
9192
|
-
`git sync failed for origin/${checkedBranch}${via}: ${asyncCommandError(error, timeoutMs)}`
|
|
9193
|
-
);
|
|
9194
|
-
}
|
|
9195
|
-
const remoteRef = `refs/remotes/origin/${checkedBranch}`;
|
|
9196
|
-
const verified = git(repo, ["rev-parse", "--verify", "-q", remoteRef]);
|
|
9197
|
-
if (verified.status !== 0) throw new Error(`git sync succeeded but ${remoteRef} does not exist`);
|
|
9198
|
-
if (helper) {
|
|
9199
|
-
const reportedCommit = synced.stdout.trim();
|
|
9200
|
-
const actualCommit = verified.stdout.trim();
|
|
9201
|
-
if (!/^[0-9a-f]{40,64}$/i.test(reportedCommit))
|
|
9202
|
-
throw new Error("git sync helper did not report the synced commit on stdout");
|
|
9203
|
-
if (reportedCommit !== actualCommit)
|
|
9204
|
-
throw new Error(`git sync helper reported a commit that does not match ${remoteRef}`);
|
|
9205
|
-
}
|
|
9206
|
-
return remoteRef;
|
|
9207
|
-
};
|
|
9208
|
-
var ensureExcluded = (repo, pattern) => {
|
|
9209
|
-
try {
|
|
9210
|
-
const r = spawnSync("git", ["-C", repo, "rev-parse", "--git-path", "info/exclude"], {
|
|
9211
|
-
stdio: "pipe",
|
|
9212
|
-
encoding: "utf8"
|
|
9213
|
-
});
|
|
9214
|
-
if (r.status !== 0) return;
|
|
9215
|
-
const rel = r.stdout.trim();
|
|
9216
|
-
const path = isAbsolute2(rel) ? rel : resolve2(repo, rel);
|
|
9217
|
-
const current = existsSync4(path) ? readFileSync5(path, "utf8") : "";
|
|
9218
|
-
if (current.split("\n").includes(pattern)) return;
|
|
9219
|
-
mkdirSync3(dirname(path), { recursive: true });
|
|
9220
|
-
const sep = current === "" || current.endsWith("\n") ? "" : "\n";
|
|
9221
|
-
appendFileSync(path, `${sep}${pattern}
|
|
9222
|
-
`, "utf8");
|
|
9223
|
-
} catch {
|
|
9224
|
-
}
|
|
9225
|
-
};
|
|
9226
|
-
var removeWorktree = (repo, worktreePath) => {
|
|
9227
|
-
if (!existsSync4(worktreePath)) return;
|
|
9228
|
-
try {
|
|
9229
|
-
execFileSync("git", ["-C", repo, "worktree", "remove", "--force", worktreePath], {
|
|
9230
|
-
stdio: "pipe"
|
|
9231
|
-
});
|
|
9232
|
-
} catch {
|
|
9233
|
-
}
|
|
9234
|
-
};
|
|
9235
|
-
var restoreWorktree = async (repo, worktreePath, sessionCode, baseBranch = repoHeadBranch(repo)) => {
|
|
9236
|
-
spawnSync("git", ["-C", repo, "worktree", "prune"], { stdio: "pipe" });
|
|
9237
|
-
mkdirSync3(dirname(worktreePath), { recursive: true });
|
|
9238
|
-
const branch = `baton/${sessionCode}`;
|
|
9239
|
-
const reuse = spawnSync("git", ["-C", repo, "worktree", "add", worktreePath, branch], {
|
|
9240
|
-
stdio: "pipe",
|
|
9241
|
-
encoding: "utf8"
|
|
9242
|
-
});
|
|
9243
|
-
if (reuse.status === 0) return;
|
|
9244
|
-
createWorktree({
|
|
9245
|
-
repo,
|
|
9246
|
-
worktreePath,
|
|
9247
|
-
sessionCode,
|
|
9248
|
-
base: await syncBaseBranch(repo, baseBranch)
|
|
9615
|
+
import { dirname as dirname2, isAbsolute as isAbsolute3, resolve as resolve3 } from "node:path";
|
|
9616
|
+
var ensureExcluded = (cwd, pattern) => {
|
|
9617
|
+
const result = spawnSync("git", ["-C", cwd, "rev-parse", "--git-path", "info/exclude"], {
|
|
9618
|
+
encoding: "utf8",
|
|
9619
|
+
env: { ...process.env, LC_ALL: "C" }
|
|
9249
9620
|
});
|
|
9621
|
+
if (result.error) throw result.error;
|
|
9622
|
+
if (result.status !== 0) {
|
|
9623
|
+
if (result.stderr.includes("not a git repository")) return;
|
|
9624
|
+
throw new Error(`cannot locate Git exclude file: ${result.stderr.trim()}`);
|
|
9625
|
+
}
|
|
9626
|
+
const location = result.stdout.trim();
|
|
9627
|
+
const path = isAbsolute3(location) ? location : resolve3(cwd, location);
|
|
9628
|
+
const current = existsSync4(path) ? readFileSync5(path, "utf8") : "";
|
|
9629
|
+
if (current.split("\n").includes(pattern)) return;
|
|
9630
|
+
mkdirSync3(dirname2(path), { recursive: true });
|
|
9631
|
+
appendFileSync(path, `${current && !current.endsWith("\n") ? "\n" : ""}${pattern}
|
|
9632
|
+
`);
|
|
9250
9633
|
};
|
|
9251
9634
|
|
|
9252
9635
|
// src/worker/bundled-skills.ts
|
|
9253
|
-
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
9254
|
-
import { cpSync, existsSync as existsSync5 } from "node:fs";
|
|
9255
|
-
import { dirname as dirname2, join as join8 } from "node:path";
|
|
9256
|
-
import { fileURLToPath } from "node:url";
|
|
9257
9636
|
var SKILL_NAME = "baton-publish-artifact";
|
|
9258
9637
|
var TARGETS = [`.claude/skills/${SKILL_NAME}`, `.agents/skills/${SKILL_NAME}`];
|
|
9259
9638
|
var bundledSkillPath = () => {
|
|
9260
|
-
const here =
|
|
9639
|
+
const here = dirname3(fileURLToPath(import.meta.url));
|
|
9261
9640
|
const candidates = [
|
|
9262
|
-
|
|
9263
|
-
|
|
9641
|
+
join12(here, "..", "skills", SKILL_NAME),
|
|
9642
|
+
join12(here, "..", "..", "skills", SKILL_NAME)
|
|
9264
9643
|
];
|
|
9265
9644
|
const found = candidates.find(existsSync5);
|
|
9266
9645
|
if (!found) throw new Error(`bundled skill missing: ${SKILL_NAME}`);
|
|
9267
9646
|
return found;
|
|
9268
9647
|
};
|
|
9269
|
-
var isTracked = (
|
|
9270
|
-
const result = spawnSync2("git", ["-C",
|
|
9648
|
+
var isTracked = (cwd, relativePath) => {
|
|
9649
|
+
const result = spawnSync2("git", ["-C", cwd, "ls-files", "--", relativePath], {
|
|
9271
9650
|
encoding: "utf8",
|
|
9272
9651
|
stdio: "pipe"
|
|
9273
9652
|
});
|
|
9274
9653
|
return result.status === 0 && result.stdout.trim().length > 0;
|
|
9275
9654
|
};
|
|
9276
|
-
var syncBundledArtifactSkill = (
|
|
9655
|
+
var syncBundledArtifactSkill = (cwd) => {
|
|
9277
9656
|
const source = bundledSkillPath();
|
|
9278
|
-
TARGETS.filter((target) => !isTracked(
|
|
9279
|
-
ensureExcluded(
|
|
9280
|
-
cpSync(source,
|
|
9657
|
+
TARGETS.filter((target) => !isTracked(cwd, target)).forEach((target) => {
|
|
9658
|
+
ensureExcluded(cwd, `${target}/`);
|
|
9659
|
+
cpSync(source, join12(cwd, target), { recursive: true, force: true });
|
|
9281
9660
|
});
|
|
9282
9661
|
};
|
|
9283
9662
|
|
|
@@ -9339,12 +9718,13 @@ var createLiveness = (deps) => {
|
|
|
9339
9718
|
|
|
9340
9719
|
// src/worker/service-supervisor.ts
|
|
9341
9720
|
import { mkdirSync as mkdirSync4 } from "node:fs";
|
|
9342
|
-
import {
|
|
9721
|
+
import { stat as stat3 } from "node:fs/promises";
|
|
9722
|
+
import { join as join14 } from "node:path";
|
|
9343
9723
|
|
|
9344
9724
|
// src/worker/service-process.ts
|
|
9345
9725
|
import { spawn as spawn2 } from "node:child_process";
|
|
9346
9726
|
import { closeSync, existsSync as existsSync6, openSync } from "node:fs";
|
|
9347
|
-
import { dirname as
|
|
9727
|
+
import { dirname as dirname4, join as join13 } from "node:path";
|
|
9348
9728
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
9349
9729
|
var START_TIMEOUT_MS = 1e4;
|
|
9350
9730
|
var isWrapperMessage = (message) => {
|
|
@@ -9354,8 +9734,8 @@ var isWrapperMessage = (message) => {
|
|
|
9354
9734
|
return value.type === "error" && typeof value.error === "string";
|
|
9355
9735
|
};
|
|
9356
9736
|
var binPath = () => {
|
|
9357
|
-
const here =
|
|
9358
|
-
const devShim =
|
|
9737
|
+
const here = dirname4(fileURLToPath2(import.meta.url));
|
|
9738
|
+
const devShim = join13(here, "..", "..", "bin", "baton.mjs");
|
|
9359
9739
|
return existsSync6(devShim) ? devShim : fileURLToPath2(import.meta.url);
|
|
9360
9740
|
};
|
|
9361
9741
|
var startServiceProcess = (input) => {
|
|
@@ -9370,7 +9750,7 @@ var startServiceProcess = (input) => {
|
|
|
9370
9750
|
} finally {
|
|
9371
9751
|
closeSync(logFd);
|
|
9372
9752
|
}
|
|
9373
|
-
const started = new Promise((
|
|
9753
|
+
const started = new Promise((resolve4, reject) => {
|
|
9374
9754
|
let settled = false;
|
|
9375
9755
|
let timer;
|
|
9376
9756
|
const cleanup = () => {
|
|
@@ -9384,7 +9764,7 @@ var startServiceProcess = (input) => {
|
|
|
9384
9764
|
settled = true;
|
|
9385
9765
|
cleanup();
|
|
9386
9766
|
if (result instanceof Error) reject(result);
|
|
9387
|
-
else
|
|
9767
|
+
else resolve4(result);
|
|
9388
9768
|
};
|
|
9389
9769
|
const onMessage = (message) => {
|
|
9390
9770
|
if (!isWrapperMessage(message)) return;
|
|
@@ -9409,13 +9789,13 @@ var startServiceProcess = (input) => {
|
|
|
9409
9789
|
};
|
|
9410
9790
|
|
|
9411
9791
|
// src/worker/service-supervisor.ts
|
|
9412
|
-
var waitForExit = (child) => child.exitCode !== null ? Promise.resolve() : new Promise((
|
|
9792
|
+
var waitForExit = (child) => child.exitCode !== null ? Promise.resolve() : new Promise((resolve4) => child.once("exit", () => resolve4()));
|
|
9413
9793
|
var createServiceSupervisor = (deps) => {
|
|
9414
|
-
const {
|
|
9794
|
+
const { sendReport, cfg, log } = deps;
|
|
9415
9795
|
const entries = /* @__PURE__ */ new Map();
|
|
9416
9796
|
let reportQueue = Promise.resolve();
|
|
9417
|
-
const snapshot = () => [...entries.values()].filter((entry) => entry.appPid !== void 0).map(({
|
|
9418
|
-
|
|
9797
|
+
const snapshot = () => [...entries.values()].filter((entry) => entry.appPid !== void 0).map(({ cwd, name, startedAt, publicUrl, note }) => ({
|
|
9798
|
+
cwd,
|
|
9419
9799
|
name,
|
|
9420
9800
|
startedAt,
|
|
9421
9801
|
...publicUrl ? { publicUrl } : {},
|
|
@@ -9425,15 +9805,13 @@ var createServiceSupervisor = (deps) => {
|
|
|
9425
9805
|
const body = { services: snapshot(), ...result ? { result } : {} };
|
|
9426
9806
|
const request2 = reportQueue.then(() => sendReport(body)).then(() => {
|
|
9427
9807
|
});
|
|
9428
|
-
reportQueue = request2.catch(() => {
|
|
9429
|
-
});
|
|
9808
|
+
reportQueue = request2.catch((error) => log(`service report failed: ${String(error)}`));
|
|
9430
9809
|
return request2;
|
|
9431
9810
|
};
|
|
9432
9811
|
const remove = (entry) => {
|
|
9433
9812
|
if (entries.get(entry.name) !== entry) return;
|
|
9434
9813
|
entries.delete(entry.name);
|
|
9435
|
-
void report().catch(() => {
|
|
9436
|
-
});
|
|
9814
|
+
void report().catch((error) => log(`service removal report failed: ${String(error)}`));
|
|
9437
9815
|
};
|
|
9438
9816
|
const stopEntry = (entry) => {
|
|
9439
9817
|
if (entry.wrapper.connected) {
|
|
@@ -9442,7 +9820,18 @@ var createServiceSupervisor = (deps) => {
|
|
|
9442
9820
|
}
|
|
9443
9821
|
if (entry.appPid) killPidGroup(entry.appPid, "SIGKILL");
|
|
9444
9822
|
};
|
|
9445
|
-
const runOne = async (requestId,
|
|
9823
|
+
const runOne = async (requestId, cwd, name, argv, publicUrl, note) => {
|
|
9824
|
+
try {
|
|
9825
|
+
if (!(await stat3(cwd)).isDirectory())
|
|
9826
|
+
return report({ requestId, ok: false, error: `not a directory: ${cwd}`, status: 400 });
|
|
9827
|
+
} catch (error) {
|
|
9828
|
+
const code = error.code;
|
|
9829
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
|
|
9830
|
+
return report({ requestId, ok: false, error: `directory not found: ${cwd}`, status: 400 });
|
|
9831
|
+
}
|
|
9832
|
+
const logDir = join14(workerRuntimeDir(cfg), "services");
|
|
9833
|
+
const logPath = join14(logDir, `${name}.log`);
|
|
9834
|
+
mkdirSync4(logDir, { recursive: true, mode: 448 });
|
|
9446
9835
|
if (entries.has(name))
|
|
9447
9836
|
return report({
|
|
9448
9837
|
requestId,
|
|
@@ -9450,21 +9839,10 @@ var createServiceSupervisor = (deps) => {
|
|
|
9450
9839
|
error: `service "${name}" is already running`,
|
|
9451
9840
|
status: 409
|
|
9452
9841
|
});
|
|
9453
|
-
const
|
|
9454
|
-
if (session2.workerId !== cfg.workerId || !session2.worktreePath)
|
|
9455
|
-
return report({
|
|
9456
|
-
requestId,
|
|
9457
|
-
ok: false,
|
|
9458
|
-
error: "session is not materialized on this worker",
|
|
9459
|
-
status: 400
|
|
9460
|
-
});
|
|
9461
|
-
const logDir = join10(session2.worktreePath, ".baton-services");
|
|
9462
|
-
const logPath = join10(logDir, `${name}.log`);
|
|
9463
|
-
mkdirSync4(logDir, { recursive: true });
|
|
9464
|
-
const { wrapper, started } = startServiceProcess({ argv, cwd: session2.worktreePath, logPath });
|
|
9842
|
+
const { wrapper, started } = startServiceProcess({ argv, cwd, logPath });
|
|
9465
9843
|
const entry = {
|
|
9466
9844
|
wrapper,
|
|
9467
|
-
|
|
9845
|
+
cwd,
|
|
9468
9846
|
name,
|
|
9469
9847
|
startedAt: Date.now(),
|
|
9470
9848
|
publicUrl,
|
|
@@ -9483,19 +9861,20 @@ var createServiceSupervisor = (deps) => {
|
|
|
9483
9861
|
}
|
|
9484
9862
|
const service2 = {
|
|
9485
9863
|
workerId: cfg.workerId,
|
|
9486
|
-
|
|
9864
|
+
cwd,
|
|
9487
9865
|
name,
|
|
9488
9866
|
startedAt: entry.startedAt,
|
|
9489
9867
|
...publicUrl ? { publicUrl } : {},
|
|
9490
9868
|
...note ? { note } : {}
|
|
9491
9869
|
};
|
|
9492
|
-
log(`service ${name} started in ${
|
|
9870
|
+
log(`service ${name} started in ${cwd}`);
|
|
9493
9871
|
await report({ requestId, ok: true, service: service2, logPath });
|
|
9494
9872
|
};
|
|
9495
|
-
const run2 = async (requestId,
|
|
9873
|
+
const run2 = async (requestId, cwd, name, argv, publicUrl, note) => {
|
|
9496
9874
|
try {
|
|
9497
|
-
await runOne(requestId,
|
|
9875
|
+
await runOne(requestId, cwd, name, argv, publicUrl, note);
|
|
9498
9876
|
} catch (error) {
|
|
9877
|
+
log(`service ${name} failed: ${String(error)}`);
|
|
9499
9878
|
await report({ requestId, ok: false, error: String(error), status: 500 });
|
|
9500
9879
|
}
|
|
9501
9880
|
};
|
|
@@ -9508,21 +9887,16 @@ var createServiceSupervisor = (deps) => {
|
|
|
9508
9887
|
remove(entry);
|
|
9509
9888
|
await report({ requestId, ok: true });
|
|
9510
9889
|
};
|
|
9511
|
-
const stopSession = async (sessionId) => {
|
|
9512
|
-
const matches = [...entries.values()].filter((entry) => entry.sessionId === sessionId);
|
|
9513
|
-
matches.forEach(stopEntry);
|
|
9514
|
-
await Promise.all(matches.map((entry) => waitForExit(entry.wrapper)));
|
|
9515
|
-
matches.forEach(remove);
|
|
9516
|
-
};
|
|
9517
9890
|
const killAll = () => entries.forEach(stopEntry);
|
|
9518
|
-
return { run: run2, stop,
|
|
9891
|
+
return { run: run2, stop, report, killAll };
|
|
9519
9892
|
};
|
|
9520
9893
|
|
|
9521
9894
|
// src/worker/session-supervisor.ts
|
|
9522
9895
|
import { spawn as spawn3 } from "node:child_process";
|
|
9523
9896
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
9524
9897
|
import { existsSync as existsSync7 } from "node:fs";
|
|
9525
|
-
import {
|
|
9898
|
+
import { mkdir as mkdir5, rm as rm2, stat as stat4 } from "node:fs/promises";
|
|
9899
|
+
import { dirname as dirname5, join as join17 } from "node:path";
|
|
9526
9900
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
9527
9901
|
import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
|
|
9528
9902
|
|
|
@@ -9559,7 +9933,7 @@ var buildTitlePrompt = (userText, assistantText) => {
|
|
|
9559
9933
|
].join("\n");
|
|
9560
9934
|
};
|
|
9561
9935
|
var generateTitle = async (input) => {
|
|
9562
|
-
const {
|
|
9936
|
+
const { cwd, userText, assistantText, queryFn } = input;
|
|
9563
9937
|
if (`${userText}${assistantText}`.trim().length < 4) return { kind: "declined" };
|
|
9564
9938
|
const exe = claudeExecutable();
|
|
9565
9939
|
const abort = new AbortController();
|
|
@@ -9572,7 +9946,7 @@ var generateTitle = async (input) => {
|
|
|
9572
9946
|
const messages = queryFn({
|
|
9573
9947
|
prompt: buildTitlePrompt(userText, assistantText),
|
|
9574
9948
|
options: {
|
|
9575
|
-
cwd
|
|
9949
|
+
cwd,
|
|
9576
9950
|
// A one-shot summarizer, not an agent: replace the ~18k-token Claude
|
|
9577
9951
|
// Code preset system prompt. Inside the agent harness the model
|
|
9578
9952
|
// routinely answers NONE to this ask (verified against reclaude);
|
|
@@ -9616,7 +9990,7 @@ var generateTitle = async (input) => {
|
|
|
9616
9990
|
// src/session/runner/title-codex.ts
|
|
9617
9991
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
9618
9992
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
9619
|
-
import { join as
|
|
9993
|
+
import { join as join15 } from "node:path";
|
|
9620
9994
|
import { Codex as Codex2 } from "@openai/codex-sdk";
|
|
9621
9995
|
var generateTitleWithCodex = async (input) => {
|
|
9622
9996
|
const { userText, assistantText } = input;
|
|
@@ -9625,7 +9999,7 @@ var generateTitleWithCodex = async (input) => {
|
|
|
9625
9999
|
const timer = setTimeout(() => abort.abort(), TITLE_TIMEOUT_MS);
|
|
9626
10000
|
let cwd = "";
|
|
9627
10001
|
try {
|
|
9628
|
-
cwd = await mkdtemp(
|
|
10002
|
+
cwd = await mkdtemp(join15(tmpdir3(), "baton-title-"));
|
|
9629
10003
|
const client = input.client ?? new Codex2();
|
|
9630
10004
|
const thread = client.startThread({
|
|
9631
10005
|
workingDirectory: cwd,
|
|
@@ -9652,18 +10026,18 @@ var generateTitleWithCodex = async (input) => {
|
|
|
9652
10026
|
};
|
|
9653
10027
|
|
|
9654
10028
|
// src/worker/session-process.ts
|
|
9655
|
-
import { execFile as
|
|
9656
|
-
import { readFile, rename, writeFile as writeFile3 } from "node:fs/promises";
|
|
9657
|
-
import { join as
|
|
9658
|
-
import { promisify as
|
|
9659
|
-
var run =
|
|
10029
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
10030
|
+
import { readFile as readFile2, rename, writeFile as writeFile3 } from "node:fs/promises";
|
|
10031
|
+
import { join as join16 } from "node:path";
|
|
10032
|
+
import { promisify as promisify4 } from "node:util";
|
|
10033
|
+
var run = promisify4(execFile4);
|
|
9660
10034
|
var RUNNER_RECORD = ".baton-runner.json";
|
|
9661
|
-
var recordPath = (
|
|
10035
|
+
var recordPath = (cwd) => join16(cwd, RUNNER_RECORD);
|
|
9662
10036
|
var codeOf = (error) => typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
|
|
9663
|
-
var readRunnerRecord = async (
|
|
10037
|
+
var readRunnerRecord = async (cwd) => {
|
|
9664
10038
|
let raw;
|
|
9665
10039
|
try {
|
|
9666
|
-
raw = await
|
|
10040
|
+
raw = await readFile2(recordPath(cwd), "utf8");
|
|
9667
10041
|
} catch (error) {
|
|
9668
10042
|
if (codeOf(error) === "ENOENT") return null;
|
|
9669
10043
|
throw error;
|
|
@@ -9673,8 +10047,8 @@ var readRunnerRecord = async (worktreePath) => {
|
|
|
9673
10047
|
throw new Error("invalid session process record");
|
|
9674
10048
|
return value;
|
|
9675
10049
|
};
|
|
9676
|
-
var writeRunnerRecord = async (
|
|
9677
|
-
const path = recordPath(
|
|
10050
|
+
var writeRunnerRecord = async (cwd, record) => {
|
|
10051
|
+
const path = recordPath(cwd);
|
|
9678
10052
|
const temporary = `${path}.${record.runnerToken}.tmp`;
|
|
9679
10053
|
await writeFile3(temporary, JSON.stringify(record), { mode: 384, flag: "wx" });
|
|
9680
10054
|
await rename(temporary, path);
|
|
@@ -9714,37 +10088,23 @@ var stopRecordedRunner = async (record, expected, graceMs = 5e3) => {
|
|
|
9714
10088
|
signalGroup(record.pgid, "SIGTERM");
|
|
9715
10089
|
const deadline = Date.now() + graceMs;
|
|
9716
10090
|
while (groupExists(record.pgid) && Date.now() < deadline)
|
|
9717
|
-
await new Promise((
|
|
10091
|
+
await new Promise((resolve4) => setTimeout(resolve4, 50));
|
|
9718
10092
|
if (!groupExists(record.pgid)) return;
|
|
9719
10093
|
signalGroup(record.pgid, "SIGKILL");
|
|
9720
10094
|
const killDeadline = Date.now() + graceMs;
|
|
9721
10095
|
while (groupExists(record.pgid) && Date.now() < killDeadline)
|
|
9722
|
-
await new Promise((
|
|
10096
|
+
await new Promise((resolve4) => setTimeout(resolve4, 50));
|
|
9723
10097
|
if (groupExists(record.pgid)) throw new Error("session process group has not exited");
|
|
9724
10098
|
};
|
|
9725
10099
|
|
|
9726
10100
|
// src/worker/session-supervisor.ts
|
|
9727
10101
|
var binPath2 = () => {
|
|
9728
|
-
const here =
|
|
9729
|
-
const devShim =
|
|
10102
|
+
const here = dirname5(fileURLToPath3(import.meta.url));
|
|
10103
|
+
const devShim = join17(here, "..", "..", "bin", "baton.mjs");
|
|
9730
10104
|
return existsSync7(devShim) ? devShim : fileURLToPath3(import.meta.url);
|
|
9731
10105
|
};
|
|
9732
10106
|
var createSessionSupervisor = (deps) => {
|
|
9733
|
-
const { client, cfg,
|
|
9734
|
-
const worktreeDir = defaultWorktreeDir();
|
|
9735
|
-
const baseBranch = cfg.baseBranch ?? repoHeadBranch(repo);
|
|
9736
|
-
const syncBase = deps.syncBase ?? syncBaseBranch;
|
|
9737
|
-
let syncInFlight = null;
|
|
9738
|
-
const syncedBase = () => {
|
|
9739
|
-
if (syncInFlight) return syncInFlight;
|
|
9740
|
-
const pending = syncBase(repo, baseBranch);
|
|
9741
|
-
syncInFlight = pending;
|
|
9742
|
-
const clear = () => {
|
|
9743
|
-
if (syncInFlight === pending) syncInFlight = null;
|
|
9744
|
-
};
|
|
9745
|
-
void pending.then(clear, clear);
|
|
9746
|
-
return pending;
|
|
9747
|
-
};
|
|
10107
|
+
const { client, cfg, cwd, log, hasTerminal, closeTerminal } = deps;
|
|
9748
10108
|
const children = /* @__PURE__ */ new Map();
|
|
9749
10109
|
const lifecycle = /* @__PURE__ */ new Map();
|
|
9750
10110
|
const enqueue = (id, action) => {
|
|
@@ -9763,8 +10123,8 @@ var createSessionSupervisor = (deps) => {
|
|
|
9763
10123
|
const cancelStart = (sessionId) => {
|
|
9764
10124
|
startEpochs.set(sessionId, currentStartEpoch(sessionId) + 1);
|
|
9765
10125
|
};
|
|
9766
|
-
const stopRecord = async (sessionId,
|
|
9767
|
-
const record = await readRunnerRecord(
|
|
10126
|
+
const stopRecord = async (sessionId, token) => {
|
|
10127
|
+
const record = await readRunnerRecord(sessionRuntimeDir(cfg, sessionId));
|
|
9768
10128
|
if (!record) {
|
|
9769
10129
|
if (token) throw new Error("missing record for an unfinished execution");
|
|
9770
10130
|
return;
|
|
@@ -9773,9 +10133,9 @@ var createSessionSupervisor = (deps) => {
|
|
|
9773
10133
|
throw new Error("runner identity does not match the attempt");
|
|
9774
10134
|
await stopRecordedRunner(record, { server: cfg.server, workerId: cfg.workerId, sessionId });
|
|
9775
10135
|
};
|
|
9776
|
-
const spawnChild = async (sessionId,
|
|
10136
|
+
const spawnChild = async (sessionId, cwd2, wasCanceled) => {
|
|
9777
10137
|
if (children.has(sessionId)) return;
|
|
9778
|
-
await stopRecord(sessionId
|
|
10138
|
+
await stopRecord(sessionId);
|
|
9779
10139
|
const runnerToken = randomUUID2();
|
|
9780
10140
|
const child = spawn3(
|
|
9781
10141
|
process.execPath,
|
|
@@ -9783,16 +10143,17 @@ var createSessionSupervisor = (deps) => {
|
|
|
9783
10143
|
{
|
|
9784
10144
|
detached: true,
|
|
9785
10145
|
stdio: ["inherit", "inherit", "inherit", "ipc"],
|
|
9786
|
-
|
|
10146
|
+
cwd: cwd2,
|
|
10147
|
+
env: workerEnv(cfg)
|
|
9787
10148
|
}
|
|
9788
10149
|
);
|
|
9789
|
-
const entry = { child
|
|
10150
|
+
const entry = { child };
|
|
9790
10151
|
children.set(sessionId, entry);
|
|
9791
10152
|
let readyTimer;
|
|
9792
|
-
const ready = new Promise((
|
|
10153
|
+
const ready = new Promise((resolve4, reject) => {
|
|
9793
10154
|
child.on("message", (message) => {
|
|
9794
10155
|
if (typeof message === "object" && message !== null && "type" in message && message.type === "ready")
|
|
9795
|
-
|
|
10156
|
+
resolve4();
|
|
9796
10157
|
});
|
|
9797
10158
|
child.once("error", reject);
|
|
9798
10159
|
child.once("exit", () => reject(new Error("session child exited before ready")));
|
|
@@ -9812,8 +10173,8 @@ var createSessionSupervisor = (deps) => {
|
|
|
9812
10173
|
void stop(sessionId).catch((error) => log(`stuck execution cleanup failed: ${String(error)}`));
|
|
9813
10174
|
});
|
|
9814
10175
|
try {
|
|
9815
|
-
await new Promise((
|
|
9816
|
-
child.once("spawn",
|
|
10176
|
+
await new Promise((resolve4, reject) => {
|
|
10177
|
+
child.once("spawn", resolve4);
|
|
9817
10178
|
child.once("error", reject);
|
|
9818
10179
|
});
|
|
9819
10180
|
if (!child.pid) throw new Error("session child has no PID");
|
|
@@ -9825,11 +10186,11 @@ var createSessionSupervisor = (deps) => {
|
|
|
9825
10186
|
pid: child.pid,
|
|
9826
10187
|
pgid: child.pid
|
|
9827
10188
|
};
|
|
9828
|
-
await writeRunnerRecord(
|
|
10189
|
+
await writeRunnerRecord(sessionRuntimeDir(cfg, sessionId), record);
|
|
9829
10190
|
await readiness;
|
|
9830
10191
|
if (wasCanceled()) return;
|
|
9831
10192
|
await new Promise(
|
|
9832
|
-
(
|
|
10193
|
+
(resolve4, reject) => child.send({ type: "execute" }, (error) => error ? reject(error) : resolve4())
|
|
9833
10194
|
);
|
|
9834
10195
|
} catch (error) {
|
|
9835
10196
|
killProcessGroup(child);
|
|
@@ -9847,37 +10208,20 @@ var createSessionSupervisor = (deps) => {
|
|
|
9847
10208
|
if (execution.paused || execution.contextResetRequested || execution.attempt?.status === "running" || execution.attempt?.status === "stopping")
|
|
9848
10209
|
return;
|
|
9849
10210
|
if (wasCanceled()) return log(`session #${sessionId} start canceled before materializing`);
|
|
9850
|
-
|
|
9851
|
-
if (!
|
|
9852
|
-
|
|
9853
|
-
|
|
9854
|
-
|
|
9855
|
-
const
|
|
9856
|
-
|
|
9857
|
-
|
|
9858
|
-
|
|
9859
|
-
|
|
9860
|
-
sessionCode: sessionCode.slice(0, 8),
|
|
9861
|
-
base
|
|
9862
|
-
});
|
|
9863
|
-
await client.sessions.materialize(sessionId, { agentSessionId, worktreePath }, cfg.apiToken);
|
|
9864
|
-
if (wasCanceled()) return log(`session #${sessionId} start canceled after materializing`);
|
|
9865
|
-
log(`materialized session #${sessionId} \u2192 ${worktreePath}`);
|
|
9866
|
-
} else if (!existsSync7(worktreePath)) {
|
|
9867
|
-
await restoreWorktree(repo, worktreePath, session2.agentSessionId.slice(0, 8), baseBranch);
|
|
9868
|
-
if (wasCanceled()) return log(`session #${sessionId} start canceled during worktree restore`);
|
|
9869
|
-
log(`recreated session #${sessionId} worktree (was missing) \u2192 ${worktreePath}`);
|
|
9870
|
-
}
|
|
9871
|
-
ensureExcluded(repo, PROJECT_CONFIG_NAME);
|
|
9872
|
-
ensureExcluded(repo, ".baton-services/");
|
|
9873
|
-
ensureExcluded(repo, RUNNER_RECORD);
|
|
9874
|
-
ensureExcluded(repo, ".baton-runner.json.*.tmp");
|
|
9875
|
-
saveProjectConfig(join13(worktreePath, PROJECT_CONFIG_NAME), worktreeConfig(cfg, sessionId));
|
|
9876
|
-
syncBundledArtifactSkill(repo, worktreePath);
|
|
10211
|
+
const directory2 = session2.cwd ?? cwd;
|
|
10212
|
+
if (!(await stat4(directory2)).isDirectory()) throw new Error(`not a directory: ${directory2}`);
|
|
10213
|
+
if (wasCanceled()) return;
|
|
10214
|
+
if (!session2.agentSessionId || !session2.cwd) {
|
|
10215
|
+
const id = randomUUID2();
|
|
10216
|
+
const agentSessionId = session2.agentKind === "codex" ? `pending:${id}` : id;
|
|
10217
|
+
await client.sessions.materialize(sessionId, { agentSessionId, cwd: directory2 }, cfg.apiToken);
|
|
10218
|
+
log(`materialized session #${sessionId} (${name}) \u2192 ${directory2}`);
|
|
10219
|
+
}
|
|
10220
|
+
await mkdir5(sessionRuntimeDir(cfg, sessionId), { recursive: true, mode: 448 });
|
|
9877
10221
|
if (hasTerminal(sessionId))
|
|
9878
10222
|
return log(`session #${sessionId} terminal opened mid-start \u2014 skipping headless start`);
|
|
9879
10223
|
if (wasCanceled()) return;
|
|
9880
|
-
await spawnChild(sessionId,
|
|
10224
|
+
await spawnChild(sessionId, directory2, wasCanceled);
|
|
9881
10225
|
};
|
|
9882
10226
|
const start = (sessionId, name) => {
|
|
9883
10227
|
const epoch = currentStartEpoch(sessionId);
|
|
@@ -9895,25 +10239,21 @@ var createSessionSupervisor = (deps) => {
|
|
|
9895
10239
|
cancelStart(sessionId);
|
|
9896
10240
|
return enqueue(sessionId, async () => {
|
|
9897
10241
|
const entry = children.get(sessionId);
|
|
9898
|
-
|
|
9899
|
-
if (session2.worktreePath) await stopRecord(sessionId, session2.worktreePath);
|
|
10242
|
+
await stopRecord(sessionId);
|
|
9900
10243
|
if (entry && children.get(sessionId) === entry) children.delete(sessionId);
|
|
9901
10244
|
});
|
|
9902
10245
|
};
|
|
9903
|
-
const remove = (sessionId
|
|
10246
|
+
const remove = (sessionId) => {
|
|
9904
10247
|
cancelStart(sessionId);
|
|
9905
10248
|
closeTerminal(sessionId);
|
|
9906
10249
|
return enqueue(sessionId, async () => {
|
|
9907
10250
|
const entry = children.get(sessionId);
|
|
9908
|
-
|
|
9909
|
-
|
|
9910
|
-
await stopRecord(sessionId, path);
|
|
9911
|
-
removeWorktree(repo, path);
|
|
9912
|
-
}
|
|
10251
|
+
await stopRecord(sessionId);
|
|
10252
|
+
await rm2(sessionRuntimeDir(cfg, sessionId), { recursive: true, force: true });
|
|
9913
10253
|
if (children.get(sessionId) === entry) children.delete(sessionId);
|
|
9914
10254
|
});
|
|
9915
10255
|
};
|
|
9916
|
-
const title = async (sessionId, agentSessionId,
|
|
10256
|
+
const title = async (sessionId, agentSessionId, cwd2) => {
|
|
9917
10257
|
const session2 = await client.sessions.get(sessionId);
|
|
9918
10258
|
const events = await client.sessions.listEvents(sessionId);
|
|
9919
10259
|
const exchange = parseFirstExchangeFromEvents(events) ?? (session2.agentKind === "claude-code" ? readFirstExchange(agentSessionId) : null);
|
|
@@ -9922,7 +10262,7 @@ var createSessionSupervisor = (deps) => {
|
|
|
9922
10262
|
userText: exchange.userText,
|
|
9923
10263
|
assistantText: exchange.assistantText
|
|
9924
10264
|
}) : await generateTitle({
|
|
9925
|
-
|
|
10265
|
+
cwd: cwd2,
|
|
9926
10266
|
userText: exchange.userText,
|
|
9927
10267
|
assistantText: exchange.assistantText,
|
|
9928
10268
|
queryFn: query2
|
|
@@ -9942,10 +10282,10 @@ var createSessionSupervisor = (deps) => {
|
|
|
9942
10282
|
const session2 = await client.sessions.get(candidate.id);
|
|
9943
10283
|
let execution = await client.sessions.execution(session2.id);
|
|
9944
10284
|
if (execution.attempt?.status === "stopping") {
|
|
9945
|
-
if (!session2.
|
|
10285
|
+
if (!session2.cwd) throw new Error("unfinished execution without cwd");
|
|
9946
10286
|
if (children.has(session2.id) && Date.now() - (execution.attempt.stopRequestedAt ?? 0) < 1e4)
|
|
9947
10287
|
return;
|
|
9948
|
-
await stopRecord(session2.id,
|
|
10288
|
+
await stopRecord(session2.id, execution.attempt.runnerToken);
|
|
9949
10289
|
children.delete(session2.id);
|
|
9950
10290
|
await client.sessions.stopped(
|
|
9951
10291
|
session2.id,
|
|
@@ -9955,12 +10295,12 @@ var createSessionSupervisor = (deps) => {
|
|
|
9955
10295
|
execution = await client.sessions.execution(session2.id);
|
|
9956
10296
|
}
|
|
9957
10297
|
if (execution.paused) {
|
|
9958
|
-
if (session2.
|
|
10298
|
+
if (session2.cwd) await stopRecord(session2.id);
|
|
9959
10299
|
return;
|
|
9960
10300
|
}
|
|
9961
10301
|
if (execution.attempt?.status === "running") {
|
|
9962
|
-
if (!children.has(session2.id) && session2.
|
|
9963
|
-
await stopRecord(session2.id,
|
|
10302
|
+
if (!children.has(session2.id) && session2.cwd)
|
|
10303
|
+
await stopRecord(session2.id, execution.attempt.runnerToken);
|
|
9964
10304
|
return;
|
|
9965
10305
|
}
|
|
9966
10306
|
if (execution.pendingCount || execution.turn?.status === "recovering")
|
|
@@ -9971,7 +10311,7 @@ var createSessionSupervisor = (deps) => {
|
|
|
9971
10311
|
for (const [id, entry] of children) {
|
|
9972
10312
|
if (!live.has(id))
|
|
9973
10313
|
await enqueue(id, async () => {
|
|
9974
|
-
await stopRecord(id
|
|
10314
|
+
await stopRecord(id);
|
|
9975
10315
|
if (children.get(id) === entry) children.delete(id);
|
|
9976
10316
|
});
|
|
9977
10317
|
}
|
|
@@ -9991,7 +10331,7 @@ var createSessionSupervisor = (deps) => {
|
|
|
9991
10331
|
};
|
|
9992
10332
|
|
|
9993
10333
|
// src/worker/terminal-manager.ts
|
|
9994
|
-
import {
|
|
10334
|
+
import { statSync as statSync2 } from "node:fs";
|
|
9995
10335
|
import { spawn as spawn4 } from "node-pty";
|
|
9996
10336
|
|
|
9997
10337
|
// ../../node_modules/.pnpm/ws@8.21.0/node_modules/ws/wrapper.mjs
|
|
@@ -10007,8 +10347,8 @@ var import_websocket_server = __toESM(require_websocket_server(), 1);
|
|
|
10007
10347
|
// src/worker/pty.ts
|
|
10008
10348
|
import { existsSync as existsSync8, readdirSync as readdirSync2 } from "node:fs";
|
|
10009
10349
|
import { createRequire as createModuleRequire } from "node:module";
|
|
10010
|
-
import { homedir as
|
|
10011
|
-
import { join as
|
|
10350
|
+
import { homedir as homedir4 } from "node:os";
|
|
10351
|
+
import { join as join18 } from "node:path";
|
|
10012
10352
|
var MAX_TERMINALS = 10;
|
|
10013
10353
|
var claudePtyCommand = (agentSessionId, hasJsonl, env = process.env) => ({
|
|
10014
10354
|
file: env.BATON_CLAUDE_BIN?.trim() || "claude",
|
|
@@ -10024,10 +10364,10 @@ var codexPtyCommand = (agentSessionId, env = process.env) => {
|
|
|
10024
10364
|
};
|
|
10025
10365
|
};
|
|
10026
10366
|
var hasSessionJsonl = (agentSessionId) => {
|
|
10027
|
-
const root =
|
|
10367
|
+
const root = join18(homedir4(), ".claude", "projects");
|
|
10028
10368
|
if (!existsSync8(root)) return false;
|
|
10029
10369
|
for (const dir of readdirSync2(root))
|
|
10030
|
-
if (existsSync8(
|
|
10370
|
+
if (existsSync8(join18(root, dir, `${agentSessionId}.jsonl`))) return true;
|
|
10031
10371
|
return false;
|
|
10032
10372
|
};
|
|
10033
10373
|
var serverTerminalWsUrl = (server, sessionId) => `${server.replace(/^http/, "ws")}/workers/me/terminal/ws?sessionId=${sessionId}`;
|
|
@@ -10035,27 +10375,21 @@ var isPositiveDim = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
|
|
|
10035
10375
|
|
|
10036
10376
|
// src/worker/terminal-manager.ts
|
|
10037
10377
|
var createTerminalManager = (deps) => {
|
|
10038
|
-
const { cfg, log, hasChild
|
|
10378
|
+
const { cfg, log, hasChild } = deps;
|
|
10039
10379
|
const terminals = /* @__PURE__ */ new Map();
|
|
10040
|
-
const
|
|
10041
|
-
const why = terminals.has(sessionId) ? "already open" : hasChild(sessionId) ? "headless child running" : !
|
|
10380
|
+
const open2 = (sessionId, agentSessionId, cwd) => {
|
|
10381
|
+
const why = terminals.has(sessionId) ? "already open" : hasChild(sessionId) ? "headless child running" : !statSync2(cwd, { throwIfNoEntry: false })?.isDirectory() ? `directory missing (${cwd})` : terminals.size >= MAX_TERMINALS ? `cap ${MAX_TERMINALS} reached` : null;
|
|
10042
10382
|
if (why) {
|
|
10043
10383
|
log(`terminal #${sessionId}: ${why} \u2014 refusing`);
|
|
10044
10384
|
return;
|
|
10045
10385
|
}
|
|
10046
|
-
try {
|
|
10047
|
-
prepareWorktree(worktreePath);
|
|
10048
|
-
} catch (error) {
|
|
10049
|
-
log(`terminal #${sessionId}: worktree preparation failed: ${String(error)}`);
|
|
10050
|
-
return;
|
|
10051
|
-
}
|
|
10052
10386
|
const command = cfg.agentKind === "codex" ? codexPtyCommand(agentSessionId) : claudePtyCommand(agentSessionId, hasSessionJsonl(agentSessionId));
|
|
10053
10387
|
const term = spawn4(command.file, command.args, {
|
|
10054
10388
|
name: "xterm-color",
|
|
10055
10389
|
cols: 80,
|
|
10056
10390
|
rows: 24,
|
|
10057
|
-
cwd
|
|
10058
|
-
env:
|
|
10391
|
+
cwd,
|
|
10392
|
+
env: workerEnv(cfg)
|
|
10059
10393
|
});
|
|
10060
10394
|
const ws = new import_websocket.default(serverTerminalWsUrl(cfg.server, sessionId), {
|
|
10061
10395
|
headers: { authorization: `Bearer ${cfg.apiToken}` }
|
|
@@ -10101,7 +10435,7 @@ var createTerminalManager = (deps) => {
|
|
|
10101
10435
|
}
|
|
10102
10436
|
};
|
|
10103
10437
|
return {
|
|
10104
|
-
open,
|
|
10438
|
+
open: open2,
|
|
10105
10439
|
close,
|
|
10106
10440
|
has: (sessionId) => terminals.has(sessionId),
|
|
10107
10441
|
killAll: () => {
|
|
@@ -10122,12 +10456,14 @@ var createTerminalManager = (deps) => {
|
|
|
10122
10456
|
// src/worker/daemon.ts
|
|
10123
10457
|
var ES_OPEN = 1;
|
|
10124
10458
|
var runWorkerDaemon = async (cfg, client, signal) => {
|
|
10125
|
-
const
|
|
10459
|
+
const cwd = process.cwd();
|
|
10460
|
+
ensureExcluded(cwd, PROJECT_CONFIG_NAME);
|
|
10461
|
+
syncBundledArtifactSkill(cwd);
|
|
10126
10462
|
const log = (m) => console.log(`[worker #${cfg.workerId} ${cfg.name}] ${m}`);
|
|
10127
10463
|
const supervisor = createSessionSupervisor({
|
|
10128
10464
|
client,
|
|
10129
10465
|
cfg,
|
|
10130
|
-
|
|
10466
|
+
cwd,
|
|
10131
10467
|
log,
|
|
10132
10468
|
hasTerminal: (id) => terminals.has(id),
|
|
10133
10469
|
closeTerminal: (id) => terminals.close(id)
|
|
@@ -10135,11 +10471,9 @@ var runWorkerDaemon = async (cfg, client, signal) => {
|
|
|
10135
10471
|
const terminals = createTerminalManager({
|
|
10136
10472
|
cfg,
|
|
10137
10473
|
log,
|
|
10138
|
-
hasChild: (id) => supervisor.has(id)
|
|
10139
|
-
prepareWorktree: (worktreePath) => syncBundledArtifactSkill(repo, worktreePath)
|
|
10474
|
+
hasChild: (id) => supervisor.has(id)
|
|
10140
10475
|
});
|
|
10141
10476
|
const services = createServiceSupervisor({
|
|
10142
|
-
getSession: (sessionId) => client.sessions.get(sessionId),
|
|
10143
10477
|
sendReport: (report) => client.services.report(report),
|
|
10144
10478
|
cfg,
|
|
10145
10479
|
log
|
|
@@ -10184,15 +10518,14 @@ var runWorkerDaemon = async (cfg, client, signal) => {
|
|
|
10184
10518
|
else if (cmd.cmd === "session.reconcile")
|
|
10185
10519
|
void supervisor.reconcile(cmd.sessionId).catch((error) => log(`reconcile failed: ${String(error)}`));
|
|
10186
10520
|
else if (cmd.cmd === "session.delete")
|
|
10187
|
-
void
|
|
10521
|
+
void supervisor.remove(cmd.sessionId).catch((err) => log(`session cleanup failed: ${String(err)}`));
|
|
10188
10522
|
else if (cmd.cmd === "session.title")
|
|
10189
|
-
void supervisor.title(cmd.sessionId, cmd.agentSessionId, cmd.
|
|
10523
|
+
void supervisor.title(cmd.sessionId, cmd.agentSessionId, cmd.cwd).catch((err) => log(`title failed: ${String(err)}`));
|
|
10190
10524
|
else if (cmd.cmd === "session.terminal") {
|
|
10191
|
-
if (cmd.action === "open")
|
|
10192
|
-
terminals.open(cmd.sessionId, cmd.agentSessionId, cmd.worktreePath);
|
|
10525
|
+
if (cmd.action === "open") terminals.open(cmd.sessionId, cmd.agentSessionId, cmd.cwd);
|
|
10193
10526
|
else terminals.close(cmd.sessionId);
|
|
10194
10527
|
} else if (cmd.cmd === "service.run")
|
|
10195
|
-
void services.run(cmd.requestId, cmd.
|
|
10528
|
+
void services.run(cmd.requestId, cmd.cwd, cmd.name, cmd.argv, cmd.publicUrl, cmd.note).catch((err) => log(`service run failed: ${String(err)}`));
|
|
10196
10529
|
else if (cmd.cmd === "service.stop")
|
|
10197
10530
|
void services.stop(cmd.requestId, cmd.name).catch((err) => log(`service stop failed: ${String(err)}`));
|
|
10198
10531
|
} catch (error) {
|
|
@@ -10200,12 +10533,12 @@ var runWorkerDaemon = async (cfg, client, signal) => {
|
|
|
10200
10533
|
}
|
|
10201
10534
|
};
|
|
10202
10535
|
es.onerror = () => log("command stream error (eventsource will retry)");
|
|
10203
|
-
log(`listening for commands (server ${cfg.server},
|
|
10536
|
+
log(`listening for commands (server ${cfg.server}, cwd ${cwd})`);
|
|
10204
10537
|
liveness.start();
|
|
10205
|
-
await new Promise((
|
|
10206
|
-
stop =
|
|
10207
|
-
if (signal.aborted) return
|
|
10208
|
-
signal.addEventListener("abort", () =>
|
|
10538
|
+
await new Promise((resolve4) => {
|
|
10539
|
+
stop = resolve4;
|
|
10540
|
+
if (signal.aborted) return resolve4();
|
|
10541
|
+
signal.addEventListener("abort", () => resolve4(), { once: true });
|
|
10209
10542
|
});
|
|
10210
10543
|
es.close();
|
|
10211
10544
|
liveness.stop();
|
|
@@ -10217,22 +10550,87 @@ var runWorkerDaemon = async (cfg, client, signal) => {
|
|
|
10217
10550
|
|
|
10218
10551
|
// src/worker/machine-id.ts
|
|
10219
10552
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
10220
|
-
import { existsSync as
|
|
10221
|
-
import { homedir as
|
|
10222
|
-
import { dirname as
|
|
10223
|
-
var machineIdPath = (env = process.env) =>
|
|
10553
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
|
|
10554
|
+
import { homedir as homedir5 } from "node:os";
|
|
10555
|
+
import { dirname as dirname6, join as join19 } from "node:path";
|
|
10556
|
+
var machineIdPath = (env = process.env) => join19(env.XDG_DATA_HOME ?? join19(env.HOME ?? homedir5(), ".local/share"), "baton", "machine-id");
|
|
10224
10557
|
var readOrCreateMachineId = (path = machineIdPath()) => {
|
|
10225
|
-
if (
|
|
10558
|
+
if (existsSync9(path)) {
|
|
10226
10559
|
const s = readFileSync6(path, "utf8").trim();
|
|
10227
10560
|
if (s) return s;
|
|
10228
10561
|
}
|
|
10229
10562
|
const id = randomUUID3();
|
|
10230
|
-
mkdirSync5(
|
|
10563
|
+
mkdirSync5(dirname6(path), { recursive: true });
|
|
10231
10564
|
writeFileSync5(path, `${id}
|
|
10232
10565
|
`, "utf8");
|
|
10233
10566
|
return id;
|
|
10234
10567
|
};
|
|
10235
10568
|
|
|
10569
|
+
// src/commands/worker/provision.ts
|
|
10570
|
+
var projectArg = {
|
|
10571
|
+
project: { type: "string", description: "project id (overrides .baton.json)" }
|
|
10572
|
+
};
|
|
10573
|
+
var positiveId = (raw) => {
|
|
10574
|
+
const id = Number(raw);
|
|
10575
|
+
if (!Number.isSafeInteger(id) || id <= 0) throw new Error("positive id required");
|
|
10576
|
+
return id;
|
|
10577
|
+
};
|
|
10578
|
+
var workerTemplatesCommand = defineCommand({
|
|
10579
|
+
meta: { name: "templates", description: "list worker creation templates for a project" },
|
|
10580
|
+
args: { ...projectArg, ...common },
|
|
10581
|
+
run: async ({ args }) => {
|
|
10582
|
+
const list = await clientFor(args).workerCreations.templates(positiveId(resolveProjectId(args)));
|
|
10583
|
+
console.log(renderList(list, fmtWorkerTemplate, Boolean(args.json)));
|
|
10584
|
+
}
|
|
10585
|
+
});
|
|
10586
|
+
var workerCreateCommand = defineCommand({
|
|
10587
|
+
meta: { name: "create", description: "request a worker from the mini template (asynchronous)" },
|
|
10588
|
+
args: {
|
|
10589
|
+
...projectArg,
|
|
10590
|
+
...common,
|
|
10591
|
+
template: { type: "string", required: true, description: "mini-local" },
|
|
10592
|
+
name: { type: "string", required: true, description: "worker name" },
|
|
10593
|
+
agentKind: { type: "string", default: "claude-code", description: "claude-code or codex" }
|
|
10594
|
+
},
|
|
10595
|
+
run: async ({ args }) => {
|
|
10596
|
+
if (args.template !== "mini-local") throw new Error("template must be mini-local");
|
|
10597
|
+
if (args.agentKind !== "claude-code" && args.agentKind !== "codex")
|
|
10598
|
+
throw new Error("invalid agentKind");
|
|
10599
|
+
const name = args.name.trim();
|
|
10600
|
+
if (!name) throw new Error("worker name required");
|
|
10601
|
+
const creation = await clientFor(args).workerCreations.create(
|
|
10602
|
+
positiveId(resolveProjectId(args)),
|
|
10603
|
+
{
|
|
10604
|
+
templateId: "mini-local",
|
|
10605
|
+
name,
|
|
10606
|
+
agentKind: args.agentKind
|
|
10607
|
+
}
|
|
10608
|
+
);
|
|
10609
|
+
console.log(renderOne(creation, fmtWorkerCreation, Boolean(args.json)));
|
|
10610
|
+
}
|
|
10611
|
+
});
|
|
10612
|
+
var workerCreationCommand = defineCommand({
|
|
10613
|
+
meta: { name: "creation", description: "inspect worker creation records" },
|
|
10614
|
+
subCommands: {
|
|
10615
|
+
ls: defineCommand({
|
|
10616
|
+
args: { ...projectArg, ...common },
|
|
10617
|
+
run: async ({ args }) => {
|
|
10618
|
+
const list = await clientFor(args).workerCreations.listByProject(
|
|
10619
|
+
positiveId(resolveProjectId(args))
|
|
10620
|
+
);
|
|
10621
|
+
console.log(renderList(list, fmtWorkerCreation, Boolean(args.json)));
|
|
10622
|
+
}
|
|
10623
|
+
}),
|
|
10624
|
+
get: defineCommand({
|
|
10625
|
+
args: { ...common, id: { type: "positional", required: true } },
|
|
10626
|
+
run: async ({ args }) => {
|
|
10627
|
+
const creation = await clientFor(args).workerCreations.get(positiveId(args.id));
|
|
10628
|
+
console.log(renderOne(creation, fmtWorkerCreation, Boolean(args.json)));
|
|
10629
|
+
}
|
|
10630
|
+
})
|
|
10631
|
+
}
|
|
10632
|
+
});
|
|
10633
|
+
|
|
10236
10634
|
// src/commands/worker/index.ts
|
|
10237
10635
|
var resolveWorker = async (client, projectId, handle) => {
|
|
10238
10636
|
const asId = parseWorkerHandle(handle);
|
|
@@ -10249,11 +10647,6 @@ var parseAgentKind = (value) => {
|
|
|
10249
10647
|
if (raw === "claude-code" || raw === "codex") return raw;
|
|
10250
10648
|
throw new Error(`invalid agent kind "${raw}" (expected claude-code or codex)`);
|
|
10251
10649
|
};
|
|
10252
|
-
var nonEmptyString = (value) => typeof value === "string" && value.trim() ? value.trim() : null;
|
|
10253
|
-
var resolveBaseBranch = (input) => {
|
|
10254
|
-
const branch = nonEmptyString(input.flag) ?? nonEmptyString(input.env) ?? nonEmptyString(input.saved) ?? repoHeadBranch(input.repo ?? process.cwd());
|
|
10255
|
-
return validateBaseBranch(branch);
|
|
10256
|
-
};
|
|
10257
10650
|
var registerWorker = async (client, input, cfgPath = projectConfigPath()) => {
|
|
10258
10651
|
const out = await client.workers.register({
|
|
10259
10652
|
projectId: input.projectId,
|
|
@@ -10267,7 +10660,6 @@ var registerWorker = async (client, input, cfgPath = projectConfigPath()) => {
|
|
|
10267
10660
|
...existing,
|
|
10268
10661
|
server: input.server,
|
|
10269
10662
|
project: input.projectId,
|
|
10270
|
-
baseBranch: input.baseBranch,
|
|
10271
10663
|
worker: {
|
|
10272
10664
|
id: out.worker.id,
|
|
10273
10665
|
name: out.worker.name,
|
|
@@ -10281,6 +10673,9 @@ var registerWorker = async (client, input, cfgPath = projectConfigPath()) => {
|
|
|
10281
10673
|
var worker = defineCommand({
|
|
10282
10674
|
meta: { name: "worker", description: "register / inspect / close workers for a project" },
|
|
10283
10675
|
subCommands: {
|
|
10676
|
+
create: workerCreateCommand,
|
|
10677
|
+
creation: workerCreationCommand,
|
|
10678
|
+
templates: workerTemplatesCommand,
|
|
10284
10679
|
register: defineCommand({
|
|
10285
10680
|
meta: {
|
|
10286
10681
|
name: "register",
|
|
@@ -10297,10 +10692,6 @@ var worker = defineCommand({
|
|
|
10297
10692
|
type: "string",
|
|
10298
10693
|
description: "agent engine for new sessions: claude-code or codex"
|
|
10299
10694
|
},
|
|
10300
|
-
baseBranch: {
|
|
10301
|
-
type: "string",
|
|
10302
|
-
description: "branch new session worktrees start from (default: current branch, then main)"
|
|
10303
|
-
},
|
|
10304
10695
|
...common
|
|
10305
10696
|
},
|
|
10306
10697
|
run: async ({ args }) => {
|
|
@@ -10315,14 +10706,9 @@ var worker = defineCommand({
|
|
|
10315
10706
|
const fileToken = savedConfig?.worker?.apiToken;
|
|
10316
10707
|
const auth = resolveAuth(process.env, fileToken);
|
|
10317
10708
|
const c = createClient(server, auth && auth !== "cookie" ? auth : void 0);
|
|
10318
|
-
const
|
|
10319
|
-
flag: args.baseBranch,
|
|
10320
|
-
env: process.env.BATON_BASE_BRANCH,
|
|
10321
|
-
saved: savedConfig?.baseBranch
|
|
10322
|
-
});
|
|
10323
|
-
const hostname = osHostname();
|
|
10709
|
+
const hostname2 = osHostname();
|
|
10324
10710
|
const machineId = readOrCreateMachineId();
|
|
10325
|
-
const name = args.name ??
|
|
10711
|
+
const name = args.name ?? hostname2;
|
|
10326
10712
|
const agentKind = parseAgentKind(args.agentKind ?? process.env.BATON_AGENT_KIND);
|
|
10327
10713
|
const { out, configPath } = await registerWorker(
|
|
10328
10714
|
c,
|
|
@@ -10330,20 +10716,18 @@ var worker = defineCommand({
|
|
|
10330
10716
|
projectId: resolveProjectId(args),
|
|
10331
10717
|
name,
|
|
10332
10718
|
server,
|
|
10333
|
-
hostname,
|
|
10719
|
+
hostname: hostname2,
|
|
10334
10720
|
machineId,
|
|
10335
|
-
agentKind
|
|
10336
|
-
baseBranch
|
|
10721
|
+
agentKind
|
|
10337
10722
|
},
|
|
10338
10723
|
cfgPath
|
|
10339
10724
|
);
|
|
10340
10725
|
if (args.json) {
|
|
10341
|
-
console.log(toJson({ ...out,
|
|
10726
|
+
console.log(toJson({ ...out, configPath, machineIdPath: machineIdPath() }));
|
|
10342
10727
|
return;
|
|
10343
10728
|
}
|
|
10344
10729
|
console.log(`worker #${out.worker.id} (${out.worker.name}) \u2014 ${out.outcome}`);
|
|
10345
10730
|
console.log(` agentKind: ${out.worker.agentKind}`);
|
|
10346
|
-
console.log(` baseBranch: ${baseBranch}`);
|
|
10347
10731
|
console.log(` hostname: ${out.worker.hostname}`);
|
|
10348
10732
|
console.log(` machineId: ${out.worker.machineId}`);
|
|
10349
10733
|
console.log(` machineId file: ${machineIdPath()}`);
|
|
@@ -10452,14 +10836,12 @@ var worker = defineCommand({
|
|
|
10452
10836
|
console.log("(no worker registered for this project \u2014 run `baton worker register`)");
|
|
10453
10837
|
return;
|
|
10454
10838
|
}
|
|
10455
|
-
const baseBranch = resolveBaseBranch({ saved: cfg.baseBranch });
|
|
10456
10839
|
if (args.json) {
|
|
10457
10840
|
console.log(
|
|
10458
10841
|
toJson({
|
|
10459
10842
|
...worker2,
|
|
10460
10843
|
server: cfg.server,
|
|
10461
|
-
projectId: cfg.project
|
|
10462
|
-
baseBranch
|
|
10844
|
+
projectId: cfg.project
|
|
10463
10845
|
})
|
|
10464
10846
|
);
|
|
10465
10847
|
return;
|
|
@@ -10467,7 +10849,6 @@ var worker = defineCommand({
|
|
|
10467
10849
|
console.log(`worker #${worker2.id} (${worker2.name})`);
|
|
10468
10850
|
console.log(` machineId: ${worker2.machineId}`);
|
|
10469
10851
|
console.log(` agentKind: ${worker2.agentKind ?? "claude-code"}`);
|
|
10470
|
-
console.log(` baseBranch: ${baseBranch}`);
|
|
10471
10852
|
console.log(` server: ${cfg?.server}`);
|
|
10472
10853
|
}
|
|
10473
10854
|
})
|
|
@@ -10541,6 +10922,7 @@ var main = defineCommand({
|
|
|
10541
10922
|
subCommands: {
|
|
10542
10923
|
init,
|
|
10543
10924
|
worker,
|
|
10925
|
+
provisioner,
|
|
10544
10926
|
session,
|
|
10545
10927
|
service,
|
|
10546
10928
|
artifact,
|