@codesona/runner 0.1.0 → 0.2.0
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 +34 -2
- package/dist/cli.js +671 -116
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2276,7 +2276,7 @@ var require_websocket = __commonJS({
|
|
|
2276
2276
|
var http2 = __require("http");
|
|
2277
2277
|
var net2 = __require("net");
|
|
2278
2278
|
var tls = __require("tls");
|
|
2279
|
-
var { randomBytes:
|
|
2279
|
+
var { randomBytes: randomBytes9, createHash: createHash5 } = __require("crypto");
|
|
2280
2280
|
var { Duplex, Readable } = __require("stream");
|
|
2281
2281
|
var { URL: URL2 } = __require("url");
|
|
2282
2282
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
@@ -2827,7 +2827,7 @@ var require_websocket = __commonJS({
|
|
|
2827
2827
|
}
|
|
2828
2828
|
}
|
|
2829
2829
|
const defaultPort = isSecure ? 443 : 80;
|
|
2830
|
-
const key =
|
|
2830
|
+
const key = randomBytes9(16).toString("base64");
|
|
2831
2831
|
const request = isSecure ? https2.request : http2.request;
|
|
2832
2832
|
const protocolSet = /* @__PURE__ */ new Set();
|
|
2833
2833
|
let perMessageDeflate;
|
|
@@ -3722,9 +3722,9 @@ var require_websocket_server = __commonJS({
|
|
|
3722
3722
|
|
|
3723
3723
|
// src/runner/doctor.ts
|
|
3724
3724
|
import { randomUUID as randomUUID8 } from "node:crypto";
|
|
3725
|
-
import
|
|
3725
|
+
import fs12 from "node:fs";
|
|
3726
3726
|
import os6 from "node:os";
|
|
3727
|
-
import
|
|
3727
|
+
import path19 from "node:path";
|
|
3728
3728
|
import { parseArgs as parseArgs2 } from "node:util";
|
|
3729
3729
|
|
|
3730
3730
|
// src/harness/host.ts
|
|
@@ -5383,12 +5383,12 @@ var NativeUsageReader = class {
|
|
|
5383
5383
|
cursor = -1;
|
|
5384
5384
|
childTrajectories = false;
|
|
5385
5385
|
async read() {
|
|
5386
|
-
const
|
|
5386
|
+
const path30 = join5(this.home, "antigravity-acp", "conversations", `${this.sessionId}.db`);
|
|
5387
5387
|
let cursor = this.cursor;
|
|
5388
5388
|
let complete = false;
|
|
5389
5389
|
const result = [];
|
|
5390
5390
|
for (let batch = 0; batch < 16; batch++) {
|
|
5391
|
-
const rows = await this.readRows(
|
|
5391
|
+
const rows = await this.readRows(path30, cursor);
|
|
5392
5392
|
for (const row of rows) {
|
|
5393
5393
|
if (!Number.isSafeInteger(row.idx) || row.idx <= cursor || !/^(?:[a-f0-9]{2})*$/i.test(row.data) || row.data.length > 8 * 1024 * 1024) throw new Error("Invalid native usage row");
|
|
5394
5394
|
if (row.has_children) this.childTrajectories = true;
|
|
@@ -5406,11 +5406,11 @@ var NativeUsageReader = class {
|
|
|
5406
5406
|
return { samples: result, complete, ...this.childTrajectories ? { incompleteScope: true } : {} };
|
|
5407
5407
|
}
|
|
5408
5408
|
};
|
|
5409
|
-
async function queryNativeRowsWithCli(
|
|
5410
|
-
const output = await readLocalCommand("sqlite3", ["-readonly", "-json",
|
|
5409
|
+
async function queryNativeRowsWithCli(path30, cursor) {
|
|
5410
|
+
const output = await readLocalCommand("sqlite3", ["-readonly", "-json", path30, QUERY.replace("idx>?", `idx>${cursor}`)], 16 * 1024 * 1024);
|
|
5411
5411
|
return output.trim() ? JSON.parse(output) : [];
|
|
5412
5412
|
}
|
|
5413
|
-
async function queryNativeRows(
|
|
5413
|
+
async function queryNativeRows(path30, cursor, loadSqlite = async () => {
|
|
5414
5414
|
const module = "node:sqlite";
|
|
5415
5415
|
return await import(module);
|
|
5416
5416
|
}) {
|
|
@@ -5418,9 +5418,9 @@ async function queryNativeRows(path27, cursor, loadSqlite = async () => {
|
|
|
5418
5418
|
try {
|
|
5419
5419
|
sqlite = await loadSqlite();
|
|
5420
5420
|
} catch {
|
|
5421
|
-
return queryNativeRowsWithCli(
|
|
5421
|
+
return queryNativeRowsWithCli(path30, cursor);
|
|
5422
5422
|
}
|
|
5423
|
-
const db = new sqlite.DatabaseSync(
|
|
5423
|
+
const db = new sqlite.DatabaseSync(path30, { readOnly: true });
|
|
5424
5424
|
try {
|
|
5425
5425
|
return db.prepare(QUERY).all(cursor);
|
|
5426
5426
|
} finally {
|
|
@@ -7309,14 +7309,14 @@ function normalizeFileChangeInput(item) {
|
|
|
7309
7309
|
const changePaths = Array.isArray(item?.changes) ? item.changes.map((change) => change?.path) : [];
|
|
7310
7310
|
for (const candidate of changePaths) {
|
|
7311
7311
|
if (typeof candidate !== "string") continue;
|
|
7312
|
-
const
|
|
7313
|
-
if (!
|
|
7314
|
-
seen.add(
|
|
7315
|
-
paths.push(
|
|
7312
|
+
const path30 = candidate.trim();
|
|
7313
|
+
if (!path30 || seen.has(path30)) continue;
|
|
7314
|
+
seen.add(path30);
|
|
7315
|
+
paths.push(path30);
|
|
7316
7316
|
}
|
|
7317
7317
|
if (paths.length === 0 && typeof item?.path === "string") {
|
|
7318
|
-
const
|
|
7319
|
-
if (
|
|
7318
|
+
const path30 = item.path.trim();
|
|
7319
|
+
if (path30) paths.push(path30);
|
|
7320
7320
|
}
|
|
7321
7321
|
return paths.length > 0 ? { path: paths.join(", ") } : {};
|
|
7322
7322
|
}
|
|
@@ -10842,7 +10842,7 @@ var OpenCodeServiceLane = class {
|
|
|
10842
10842
|
const document = record4(body);
|
|
10843
10843
|
const paths = record4(document?.paths);
|
|
10844
10844
|
const required = this.requiredApiPaths;
|
|
10845
|
-
if (!paths || required.some((
|
|
10845
|
+
if (!paths || required.some((path30) => !record4(paths[path30]))) {
|
|
10846
10846
|
throw new OpenCodeIncompatibleError("Installed OpenCode service is missing required session capabilities");
|
|
10847
10847
|
}
|
|
10848
10848
|
}
|
|
@@ -10932,8 +10932,8 @@ var OpenCodeServiceLane = class {
|
|
|
10932
10932
|
const controller = new AbortController();
|
|
10933
10933
|
this.durableAbort = controller;
|
|
10934
10934
|
try {
|
|
10935
|
-
const
|
|
10936
|
-
const response = await this.openStream(
|
|
10935
|
+
const path30 = `/api/session/${encodeURIComponent(this.sessionId)}/event?after=${this.lastDurableSeq}`;
|
|
10936
|
+
const response = await this.openStream(path30, controller, "durable event stream");
|
|
10937
10937
|
await this.catchUpHistory(true);
|
|
10938
10938
|
if (!this.isIdentity(identity)) return;
|
|
10939
10939
|
this.durableGap = false;
|
|
@@ -11000,12 +11000,12 @@ var OpenCodeServiceLane = class {
|
|
|
11000
11000
|
if (this.stopping) return;
|
|
11001
11001
|
this.protocolFailure(`OpenCode ${kind} stream stopped unexpectedly`, error);
|
|
11002
11002
|
}
|
|
11003
|
-
async openStream(
|
|
11003
|
+
async openStream(path30, lifecycle, operation) {
|
|
11004
11004
|
const deadline = new AbortController();
|
|
11005
11005
|
const timeout = setTimeout(() => deadline.abort(), this.options.requestTimeoutMs ?? REQUEST_TIMEOUT_MS);
|
|
11006
11006
|
timeout.unref?.();
|
|
11007
11007
|
try {
|
|
11008
|
-
const response = await this.fetchFn(`${this.baseUrl}${
|
|
11008
|
+
const response = await this.fetchFn(`${this.baseUrl}${path30}`, {
|
|
11009
11009
|
method: "GET",
|
|
11010
11010
|
headers: this.headers({ accept: "text/event-stream" }),
|
|
11011
11011
|
signal: AbortSignal.any([lifecycle.signal, deadline.signal])
|
|
@@ -11437,8 +11437,8 @@ var OpenCodeServiceLane = class {
|
|
|
11437
11437
|
turnOwner: root.receipt
|
|
11438
11438
|
});
|
|
11439
11439
|
}
|
|
11440
|
-
async requestNoContent(
|
|
11441
|
-
const response = await this.fetchWithTimeout(
|
|
11440
|
+
async requestNoContent(path30, method, body, operation) {
|
|
11441
|
+
const response = await this.fetchWithTimeout(path30, {
|
|
11442
11442
|
method,
|
|
11443
11443
|
...body ? {
|
|
11444
11444
|
headers: { "content-type": "application/json" },
|
|
@@ -11447,14 +11447,14 @@ var OpenCodeServiceLane = class {
|
|
|
11447
11447
|
}, operation);
|
|
11448
11448
|
if (!response.ok) throw new OpenCodeHttpError(response.status, operation);
|
|
11449
11449
|
}
|
|
11450
|
-
async fetchWithTimeout(
|
|
11450
|
+
async fetchWithTimeout(path30, init, operation, timeoutMs = this.options.requestTimeoutMs ?? REQUEST_TIMEOUT_MS) {
|
|
11451
11451
|
if (this.stopping) throw new Error("OpenCode service lane is stopping");
|
|
11452
11452
|
const controller = new AbortController();
|
|
11453
11453
|
this.requestControllers.add(controller);
|
|
11454
11454
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
11455
11455
|
timer.unref?.();
|
|
11456
11456
|
try {
|
|
11457
|
-
return await this.fetchFn(`${this.baseUrl}${
|
|
11457
|
+
return await this.fetchFn(`${this.baseUrl}${path30}`, {
|
|
11458
11458
|
...init,
|
|
11459
11459
|
headers: this.headers(init.headers),
|
|
11460
11460
|
signal: controller.signal
|
|
@@ -11466,7 +11466,7 @@ var OpenCodeServiceLane = class {
|
|
|
11466
11466
|
this.requestControllers.delete(controller);
|
|
11467
11467
|
}
|
|
11468
11468
|
}
|
|
11469
|
-
async fetchJsonWithTimeout(
|
|
11469
|
+
async fetchJsonWithTimeout(path30, init, operation, timeoutMs = this.options.requestTimeoutMs ?? REQUEST_TIMEOUT_MS) {
|
|
11470
11470
|
if (this.stopping) throw new OpenCodeStoppedError("OpenCode service lane is stopping");
|
|
11471
11471
|
const controller = new AbortController();
|
|
11472
11472
|
this.requestControllers.add(controller);
|
|
@@ -11477,7 +11477,7 @@ var OpenCodeServiceLane = class {
|
|
|
11477
11477
|
}, timeoutMs);
|
|
11478
11478
|
timer.unref?.();
|
|
11479
11479
|
try {
|
|
11480
|
-
const response = await this.fetchFn(`${this.baseUrl}${
|
|
11480
|
+
const response = await this.fetchFn(`${this.baseUrl}${path30}`, {
|
|
11481
11481
|
...init,
|
|
11482
11482
|
headers: this.headers(init.headers),
|
|
11483
11483
|
signal: controller.signal
|
|
@@ -14608,9 +14608,9 @@ function parseBffUrl(value) {
|
|
|
14608
14608
|
const loopback = url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "[::1]";
|
|
14609
14609
|
return url.protocol === "http:" && loopback ? url : void 0;
|
|
14610
14610
|
}
|
|
14611
|
-
function bffEndpoint(base,
|
|
14611
|
+
function bffEndpoint(base, path30) {
|
|
14612
14612
|
const url = new URL(base.href);
|
|
14613
|
-
url.pathname = `${url.pathname.replace(/\/+$/, "")}/${
|
|
14613
|
+
url.pathname = `${url.pathname.replace(/\/+$/, "")}/${path30.replace(/^\/+/, "")}`;
|
|
14614
14614
|
url.search = "";
|
|
14615
14615
|
url.hash = "";
|
|
14616
14616
|
return url;
|
|
@@ -14700,7 +14700,7 @@ function parseRunBundle(response, runId) {
|
|
|
14700
14700
|
if (typeof response.sessionId !== "string" || !UUID.test(response.sessionId)) throw new BundleError("the bundle's session id is not a uuid");
|
|
14701
14701
|
const raw = response.bundle;
|
|
14702
14702
|
if (!raw || typeof raw !== "object") throw new BundleError("the run has no bundle");
|
|
14703
|
-
if (raw.v !== 1) throw new BundleError(`bundle version ${String(raw.v)} is not one this Runner reads; update Codesona Runner`);
|
|
14703
|
+
if (raw.v !== 1 && raw.v !== 2) throw new BundleError(`bundle version ${String(raw.v)} is not one this Runner reads; update Codesona Runner`);
|
|
14704
14704
|
if (raw.harness !== Harness.ClaudeCode) throw new BundleError(`the bundle is for ${String(raw.harness)}, which this Runner does not start`);
|
|
14705
14705
|
if (typeof raw.instructions !== "string") throw new BundleError("the bundle has no instructions");
|
|
14706
14706
|
const files = stringRecord(raw.files ?? {}, "files");
|
|
@@ -14716,7 +14716,7 @@ function parseRunBundle(response, runId) {
|
|
|
14716
14716
|
sessionId: response.sessionId,
|
|
14717
14717
|
resume: response.resume === true,
|
|
14718
14718
|
bundle: {
|
|
14719
|
-
v:
|
|
14719
|
+
v: raw.v,
|
|
14720
14720
|
harness: Harness.ClaudeCode,
|
|
14721
14721
|
instructions: raw.instructions,
|
|
14722
14722
|
files,
|
|
@@ -14726,10 +14726,71 @@ function parseRunBundle(response, runId) {
|
|
|
14726
14726
|
limits: {
|
|
14727
14727
|
wallSeconds: seconds(limits.wallSeconds, DEFAULT_LIMITS.wallSeconds),
|
|
14728
14728
|
idleSeconds: seconds(limits.idleSeconds, DEFAULT_LIMITS.idleSeconds)
|
|
14729
|
-
}
|
|
14729
|
+
},
|
|
14730
|
+
// A v1 bundle has none, whatever it carries.
|
|
14731
|
+
prepare: raw.v === 2 && raw.prepare != null ? parsePrepare(raw.prepare) : null
|
|
14730
14732
|
}
|
|
14731
14733
|
};
|
|
14732
14734
|
}
|
|
14735
|
+
var SHA = /^[0-9a-f]{40}$/i;
|
|
14736
|
+
function parsePrepare(value) {
|
|
14737
|
+
if (typeof value !== "object" || Array.isArray(value)) throw new BundleError("the bundle's prepare is not an object");
|
|
14738
|
+
const list = value.repos ?? [];
|
|
14739
|
+
if (!Array.isArray(list)) throw new BundleError("the bundle's prepare.repos is not a list");
|
|
14740
|
+
const repos = list.map((item, index) => parseRepo(item, index));
|
|
14741
|
+
for (const key of ["repoId", "name"]) {
|
|
14742
|
+
const seen = /* @__PURE__ */ new Set();
|
|
14743
|
+
for (const repo of repos) {
|
|
14744
|
+
const folded = repo[key].toLowerCase();
|
|
14745
|
+
if (seen.has(folded)) throw new BundleError(`the bundle names the repository ${key} ${JSON.stringify(repo[key])} twice`);
|
|
14746
|
+
seen.add(folded);
|
|
14747
|
+
}
|
|
14748
|
+
}
|
|
14749
|
+
return { repos };
|
|
14750
|
+
}
|
|
14751
|
+
function parseRepo(value, index) {
|
|
14752
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new BundleError(`the bundle's repository ${index} is not an object`);
|
|
14753
|
+
const raw = value;
|
|
14754
|
+
const text = (field) => {
|
|
14755
|
+
const item = raw[field];
|
|
14756
|
+
if (typeof item !== "string" || !item) throw new BundleError(`the bundle's repository ${index} has no ${field}`);
|
|
14757
|
+
return item;
|
|
14758
|
+
};
|
|
14759
|
+
const repoId = text("repoId");
|
|
14760
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(repoId)) throw new BundleError(`the bundle's repository ${index} has an id the Runner cannot name a directory after`);
|
|
14761
|
+
const name = text("name");
|
|
14762
|
+
if (!isPathSegment(name)) throw new BundleError(`the bundle's repository name ${JSON.stringify(name.slice(0, 80))} is not one plain directory name`);
|
|
14763
|
+
const cloneUrl = text("cloneUrl");
|
|
14764
|
+
let url;
|
|
14765
|
+
try {
|
|
14766
|
+
url = new URL(cloneUrl);
|
|
14767
|
+
} catch {
|
|
14768
|
+
throw new BundleError(`the clone URL of ${name} is not a URL`);
|
|
14769
|
+
}
|
|
14770
|
+
if (url.protocol !== "https:") throw new BundleError(`the clone URL of ${name} is not https`);
|
|
14771
|
+
if (url.username || url.password || /^https:\/\/[^/]*@/i.test(cloneUrl)) throw new BundleError(`the clone URL of ${name} carries a user name or password`);
|
|
14772
|
+
const defaultBranch = branchName(text("defaultBranch"), "default branch", name);
|
|
14773
|
+
const taskBranch = branchName(text("taskBranch"), "task branch", name);
|
|
14774
|
+
const baseSha = text("baseSha");
|
|
14775
|
+
if (!SHA.test(baseSha)) throw new BundleError(`the base commit of ${name} is not a 40-character SHA`);
|
|
14776
|
+
const holding = raw.holdingRef ?? null;
|
|
14777
|
+
if (holding !== null && typeof holding !== "string") throw new BundleError(`the holding ref of ${name} is not a string`);
|
|
14778
|
+
const holdingRef = holding ? refName(holding, "holding ref", name) : null;
|
|
14779
|
+
return { repoId, name, cloneUrl, defaultBranch, taskBranch, baseSha: baseSha.toLowerCase(), holdingRef };
|
|
14780
|
+
}
|
|
14781
|
+
function isPathSegment(name) {
|
|
14782
|
+
return name.length <= 100 && name !== "." && name !== ".." && !name.startsWith("-") && !/[/\\\x00-\x1f\x7f]/.test(name) && name.trim() === name;
|
|
14783
|
+
}
|
|
14784
|
+
function refName(ref, what, repo) {
|
|
14785
|
+
const bad = ref.length > 255 || ref.startsWith("-") || ref.startsWith("/") || ref.endsWith("/") || ref.endsWith(".") || ref.endsWith(".lock") || ref.includes("..") || ref.includes("//") || ref.includes("@{") || /[\s~^:?*[\\\x00-\x1f\x7f]/.test(ref) || ref.split("/").some((part) => part.startsWith("."));
|
|
14786
|
+
if (bad) throw new BundleError(`the ${what} of ${repo} is not a ref name git takes`);
|
|
14787
|
+
return ref;
|
|
14788
|
+
}
|
|
14789
|
+
function branchName(branch, what, repo) {
|
|
14790
|
+
refName(branch, what, repo);
|
|
14791
|
+
if (branch.startsWith("refs/") || branch === "HEAD") throw new BundleError(`the ${what} of ${repo} is not a branch's short name`);
|
|
14792
|
+
return branch;
|
|
14793
|
+
}
|
|
14733
14794
|
function stringRecord(value, what) {
|
|
14734
14795
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new BundleError(`the bundle's ${what} are not an object`);
|
|
14735
14796
|
for (const [key, item] of Object.entries(value)) {
|
|
@@ -14741,7 +14802,7 @@ function seconds(value, fallback) {
|
|
|
14741
14802
|
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
14742
14803
|
return Math.min(86400, Math.max(60, Math.round(value)));
|
|
14743
14804
|
}
|
|
14744
|
-
var RESERVED = /* @__PURE__ */ new Set(["AGENTS.md", "CLAUDE.md", ".codesona"]);
|
|
14805
|
+
var RESERVED = /* @__PURE__ */ new Set(["AGENTS.md", "CLAUDE.md", ".codesona", "repos"]);
|
|
14745
14806
|
function bundleFilePath(name) {
|
|
14746
14807
|
const segments = name.split("/");
|
|
14747
14808
|
if (!name || path14.isAbsolute(name) || name.includes("\\") || name.includes("\0") || segments.some((segment) => !segment || segment === "." || segment === "..") || RESERVED.has(segments[0])) {
|
|
@@ -14941,6 +15002,8 @@ function runnerFiles(home) {
|
|
|
14941
15002
|
spool: path16.join(home, "spool"),
|
|
14942
15003
|
/** One working directory per agent: the same one every session, so `--resume` finds its transcript. */
|
|
14943
15004
|
agents: path16.join(home, "agents"),
|
|
15005
|
+
/** One bare clone per repository (`<repoId>.git`), shared by the agents' worktrees of it (KT2 §4). */
|
|
15006
|
+
repos: path16.join(home, "repos"),
|
|
14944
15007
|
/** The background service's log (`run --service`), turned over at 10 MB. */
|
|
14945
15008
|
log: path16.join(state, "runner.log"),
|
|
14946
15009
|
/** What the background service's process wrote outside its log (a crash), as launchd keeps it. */
|
|
@@ -15009,7 +15072,7 @@ var ExitCode = {
|
|
|
15009
15072
|
Failed: 1,
|
|
15010
15073
|
Usage: 2
|
|
15011
15074
|
};
|
|
15012
|
-
var runnerVersion = true ? "0.
|
|
15075
|
+
var runnerVersion = true ? "0.2.0" : "0.0.0-dev";
|
|
15013
15076
|
|
|
15014
15077
|
// src/runner/log.ts
|
|
15015
15078
|
var LogLevel = {
|
|
@@ -15411,7 +15474,7 @@ var LaunchdError = class extends Error {
|
|
|
15411
15474
|
};
|
|
15412
15475
|
function runCommand(file, args, options = {}) {
|
|
15413
15476
|
return new Promise((resolve4) => {
|
|
15414
|
-
execFile(file, [...args], { timeout: options.timeoutMs ?? 15e3, encoding: "utf8", maxBuffer: 4 * 1024 * 1024, env: options.env }, (error, stdout, stderr) => {
|
|
15477
|
+
execFile(file, [...args], { timeout: options.timeoutMs ?? 15e3, encoding: "utf8", maxBuffer: 4 * 1024 * 1024, env: options.env, cwd: options.cwd }, (error, stdout, stderr) => {
|
|
15415
15478
|
const exit = error?.code;
|
|
15416
15479
|
const code = !error ? 0 : typeof exit === "number" ? exit : -1;
|
|
15417
15480
|
resolve4({ code, stdout: stdout ?? "", stderr: stderr || (code === -1 && error ? error.message : "") });
|
|
@@ -15757,6 +15820,68 @@ async function statusCommand(args, io, env = process.env) {
|
|
|
15757
15820
|
return ExitCode.Ok;
|
|
15758
15821
|
}
|
|
15759
15822
|
|
|
15823
|
+
// src/runner/tools.ts
|
|
15824
|
+
import fs11 from "node:fs";
|
|
15825
|
+
import path18 from "node:path";
|
|
15826
|
+
var TOOL_TIMEOUT_MS = 1e4;
|
|
15827
|
+
function parseGitVersion(output) {
|
|
15828
|
+
return /git version (\S+)/.exec(output)?.[1] ?? null;
|
|
15829
|
+
}
|
|
15830
|
+
function parseDotnetSdks(output) {
|
|
15831
|
+
return output.split("\n").map((line) => /^(\d\S*)\s+\[/.exec(line.trim())?.[1]).filter((version) => version !== void 0);
|
|
15832
|
+
}
|
|
15833
|
+
function parseNodeVersion(output) {
|
|
15834
|
+
return /^v?(\d+\.\d+\.\d+\S*)/.exec(output.trim())?.[1] ?? null;
|
|
15835
|
+
}
|
|
15836
|
+
async function readMachineTools(environment = process.env, run = runCommand) {
|
|
15837
|
+
const options = { env: agentEnvironment(environment), timeoutMs: TOOL_TIMEOUT_MS, cwd: "/" };
|
|
15838
|
+
const look = (file, args) => run(file, args, options).catch(() => void 0);
|
|
15839
|
+
const [git, dotnet, node] = await Promise.all([look("git", ["--version"]), look("dotnet", ["--list-sdks"]), look("node", ["--version"])]);
|
|
15840
|
+
return {
|
|
15841
|
+
git: git?.code === 0 ? parseGitVersion(git.stdout) : null,
|
|
15842
|
+
dotnet: dotnet?.code === 0 ? parseDotnetSdks(dotnet.stdout) : null,
|
|
15843
|
+
node: node?.code === 0 ? parseNodeVersion(node.stdout) : null
|
|
15844
|
+
};
|
|
15845
|
+
}
|
|
15846
|
+
async function readGitSetup(environment = process.env, run = runCommand) {
|
|
15847
|
+
const options = { env: agentEnvironment(environment), timeoutMs: TOOL_TIMEOUT_MS, cwd: "/" };
|
|
15848
|
+
const look = (args) => run("git", args, options).catch(() => void 0);
|
|
15849
|
+
const version = await look(["--version"]);
|
|
15850
|
+
if (version?.code !== 0) return { version: null, credentialHelpers: [], commitSigning: false };
|
|
15851
|
+
const [helpers, signing] = await Promise.all([look(["config", "--get-all", "credential.helper"]), look(["config", "--get", "--type=bool", "commit.gpgsign"])]);
|
|
15852
|
+
return {
|
|
15853
|
+
version: parseGitVersion(version.stdout),
|
|
15854
|
+
// An empty value resets the list before it: what is after the last one counts.
|
|
15855
|
+
credentialHelpers: helpers?.code === 0 ? helpers.stdout.replace(/\n$/, "").split("\n").reduce((list, line) => line.trim() ? [...list, line.trim()] : [], []) : [],
|
|
15856
|
+
commitSigning: signing?.code === 0 && signing.stdout.trim() === "true"
|
|
15857
|
+
};
|
|
15858
|
+
}
|
|
15859
|
+
function isSshUrl(url) {
|
|
15860
|
+
if (/^(ssh|git\+ssh|ssh\+git):\/\//i.test(url)) return true;
|
|
15861
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(url) || url.startsWith("/")) return false;
|
|
15862
|
+
return /^[^/:]+:/.test(url);
|
|
15863
|
+
}
|
|
15864
|
+
async function cachedSshRemotes(cacheDirectory, environment = process.env, run = runCommand) {
|
|
15865
|
+
let names;
|
|
15866
|
+
try {
|
|
15867
|
+
names = fs11.readdirSync(cacheDirectory).filter((name) => name.endsWith(".git"));
|
|
15868
|
+
} catch {
|
|
15869
|
+
return [];
|
|
15870
|
+
}
|
|
15871
|
+
const options = { env: agentEnvironment(environment), timeoutMs: TOOL_TIMEOUT_MS, cwd: "/" };
|
|
15872
|
+
const found = [];
|
|
15873
|
+
for (const name of names) {
|
|
15874
|
+
const directory = path18.join(cacheDirectory, name);
|
|
15875
|
+
for (const args of [["remote", "get-url", "--all", "origin"], ["remote", "get-url", "--push", "--all", "origin"]]) {
|
|
15876
|
+
const result = await run("git", ["-C", directory, ...args], options).catch(() => void 0);
|
|
15877
|
+
for (const url of result?.code === 0 ? result.stdout.split("\n").map((line) => line.trim()).filter(Boolean) : []) {
|
|
15878
|
+
if (isSshUrl(url) && !found.some((remote) => remote.repository === name && remote.url === url)) found.push({ repository: name, url });
|
|
15879
|
+
}
|
|
15880
|
+
}
|
|
15881
|
+
}
|
|
15882
|
+
return found;
|
|
15883
|
+
}
|
|
15884
|
+
|
|
15760
15885
|
// src/runner/versions.ts
|
|
15761
15886
|
function isOlder(version, minimum) {
|
|
15762
15887
|
if (!minimum) return false;
|
|
@@ -15850,18 +15975,20 @@ function machineEnvironment(env = process.env, launchAgent, machine = {}) {
|
|
|
15850
15975
|
// A Runner run by hand gives its sessions this terminal's environment, less Codesona's names.
|
|
15851
15976
|
terminalClaudeLogin: service ? () => readClaudeLogin((file, args) => run(file, args, { env: agentEnvironment(env), timeoutMs: 2e4 })) : void 0,
|
|
15852
15977
|
claudeSettings: () => readClaudeSettings(claudeConfigDir(sessions())),
|
|
15853
|
-
terminalClaudeSettings: service ? () =>
|
|
15978
|
+
terminalClaudeSettings: service ? () => path19.resolve(claudeConfigDir(env)) === path19.resolve(claudeConfigDir(sessions())) ? void 0 : readClaudeSettings(claudeConfigDir(env)) : void 0,
|
|
15854
15979
|
shellClaudeSettings: service ? () => shellOnlyClaudeSettings(env, userHome) : void 0,
|
|
15855
15980
|
powerSettings: () => readPowerSettings(run, platform),
|
|
15856
15981
|
probeEdge: (baseUrl) => probeEdge(baseUrl, { userAgent: runnerUserAgent() }),
|
|
15857
15982
|
launchAgent: service ? launchAgent ?? new LaunchAgent() : void 0,
|
|
15858
|
-
claudeOnServicePath: () => currentLaunchAgentSpec(env, userHome).path.split(":").some((directory) => isExecutable(
|
|
15983
|
+
claudeOnServicePath: () => currentLaunchAgentSpec(env, userHome).path.split(":").some((directory) => isExecutable(path19.join(directory, "claude"))),
|
|
15984
|
+
gitSetup: () => readGitSetup(sessions(), run),
|
|
15985
|
+
sshRemotes: () => cachedSshRemotes(runnerFiles(runnerHome(env)).repos, sessions(), run)
|
|
15859
15986
|
};
|
|
15860
15987
|
}
|
|
15861
15988
|
function isExecutable(file) {
|
|
15862
15989
|
try {
|
|
15863
|
-
|
|
15864
|
-
return
|
|
15990
|
+
fs12.accessSync(file, fs12.constants.X_OK);
|
|
15991
|
+
return fs12.statSync(file).isFile();
|
|
15865
15992
|
} catch {
|
|
15866
15993
|
return false;
|
|
15867
15994
|
}
|
|
@@ -15974,6 +16101,35 @@ ${setting}`)).map(({ file, setting }) => {
|
|
|
15974
16101
|
}
|
|
15975
16102
|
});
|
|
15976
16103
|
}
|
|
16104
|
+
function gitChecks(setup, sshRemotes) {
|
|
16105
|
+
if (!setup.version) {
|
|
16106
|
+
return [{
|
|
16107
|
+
status: CheckStatus.Warning,
|
|
16108
|
+
message: "git is not installed, or not on the sessions' PATH: a Developer's sessions need it (a Tester's do not)",
|
|
16109
|
+
hint: "Install it: xcode-select --install, or brew install git"
|
|
16110
|
+
}];
|
|
16111
|
+
}
|
|
16112
|
+
const checks = [{ status: CheckStatus.Ok, message: `git ${setup.version}` }];
|
|
16113
|
+
checks.push(setup.credentialHelpers.length > 0 ? { status: CheckStatus.Ok, message: `git has a credential helper (${setup.credentialHelpers.join(", ")})` } : {
|
|
16114
|
+
status: CheckStatus.Warning,
|
|
16115
|
+
message: "git has no credential helper: a Developer's sessions could not fetch or push over https",
|
|
16116
|
+
hint: "Set one up and sign in once in a terminal: Git Credential Manager (brew install --cask git-credential-manager), or gh auth setup-git"
|
|
16117
|
+
});
|
|
16118
|
+
if (setup.commitSigning) {
|
|
16119
|
+
checks.push({
|
|
16120
|
+
status: CheckStatus.Warning,
|
|
16121
|
+
message: "commit.gpgsign is true in your git config: sessions run with commit signing off, so the agents' commits are not signed with your key"
|
|
16122
|
+
});
|
|
16123
|
+
}
|
|
16124
|
+
for (const remote of sshRemotes) {
|
|
16125
|
+
checks.push({
|
|
16126
|
+
status: CheckStatus.Warning,
|
|
16127
|
+
message: `The cached repository ${remote.repository} reaches its remote over SSH (${remote.url}): the background service has no SSH agent, so its git cannot use your SSH key`,
|
|
16128
|
+
hint: "Use https with a credential helper; a url.<base>.insteadOf in your git config that turns https into SSH does the same"
|
|
16129
|
+
});
|
|
16130
|
+
}
|
|
16131
|
+
return checks;
|
|
16132
|
+
}
|
|
15977
16133
|
function addressCheck(url, paired) {
|
|
15978
16134
|
const which = paired ? "This machine's Codesona" : "Codesona (the default; no machine paired yet)";
|
|
15979
16135
|
if (url.protocol === "https:") return { status: CheckStatus.Ok, message: `${which} is ${url.origin}, over https` };
|
|
@@ -16051,7 +16207,7 @@ function permissionChecks(home) {
|
|
|
16051
16207
|
const files = runnerFiles(home);
|
|
16052
16208
|
const wide = [home, files.config].filter((file) => {
|
|
16053
16209
|
try {
|
|
16054
|
-
return (
|
|
16210
|
+
return (fs12.statSync(file).mode & 63) !== 0;
|
|
16055
16211
|
} catch {
|
|
16056
16212
|
return false;
|
|
16057
16213
|
}
|
|
@@ -16094,6 +16250,7 @@ async function runChecks(environment, options = {}) {
|
|
|
16094
16250
|
if (sessionSettings) checks.push(...hookChecks(sessionSettings));
|
|
16095
16251
|
const terminalSettings = environment.terminalClaudeSettings && service && !service.installed ? environment.terminalClaudeSettings() : void 0;
|
|
16096
16252
|
if (terminalSettings) checks.push(...hookChecks(terminalSettings, SESSIONS_WHERE[SessionPlace.Terminal], stopHookBlocks(sessionSettings ?? [])));
|
|
16253
|
+
if (environment.gitSetup) checks.push(...gitChecks(await environment.gitSetup(), await environment.sshRemotes?.() ?? []));
|
|
16097
16254
|
if (unreadable) {
|
|
16098
16255
|
checks.push({ status: CheckStatus.Failed, message: unreadable.message, hint: "Pair this machine again: codesona-runner logout --local, then the command Factory shows." });
|
|
16099
16256
|
}
|
|
@@ -16148,8 +16305,8 @@ async function applyFix(fix, environment) {
|
|
|
16148
16305
|
switch (fix) {
|
|
16149
16306
|
case DoctorFix.Permissions: {
|
|
16150
16307
|
const files = runnerFiles(environment.runnerHome);
|
|
16151
|
-
|
|
16152
|
-
if (
|
|
16308
|
+
fs12.chmodSync(environment.runnerHome, 448);
|
|
16309
|
+
if (fs12.existsSync(files.config)) fs12.chmodSync(files.config, 384);
|
|
16153
16310
|
return `${environment.runnerHome} and its config can be read by this user only`;
|
|
16154
16311
|
}
|
|
16155
16312
|
case DoctorFix.InstallService:
|
|
@@ -16193,8 +16350,8 @@ async function doctorCommand(args, io, environment = machineEnvironment()) {
|
|
|
16193
16350
|
}
|
|
16194
16351
|
|
|
16195
16352
|
// src/runner/logout.ts
|
|
16196
|
-
import
|
|
16197
|
-
import
|
|
16353
|
+
import fs14 from "node:fs";
|
|
16354
|
+
import path21 from "node:path";
|
|
16198
16355
|
import { parseArgs as parseArgs3 } from "node:util";
|
|
16199
16356
|
|
|
16200
16357
|
// src/runner/bff.ts
|
|
@@ -16262,13 +16419,13 @@ var BffClient = class {
|
|
|
16262
16419
|
async logout(connectionKey) {
|
|
16263
16420
|
await this.request("POST", "v1/machine-auth/logout", connectionKey, void 0, { emptyAnswer: true });
|
|
16264
16421
|
}
|
|
16265
|
-
async request(method,
|
|
16422
|
+
async request(method, path30, bearer, body, options = {}) {
|
|
16266
16423
|
const headers = { "User-Agent": this.options.userAgent, Accept: "application/json" };
|
|
16267
16424
|
if (bearer) headers.Authorization = `Bearer ${bearer}`;
|
|
16268
16425
|
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
16269
16426
|
let response;
|
|
16270
16427
|
try {
|
|
16271
|
-
response = await this.fetchImpl(bffEndpoint(this.options.baseUrl,
|
|
16428
|
+
response = await this.fetchImpl(bffEndpoint(this.options.baseUrl, path30), {
|
|
16272
16429
|
method,
|
|
16273
16430
|
headers,
|
|
16274
16431
|
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
@@ -16276,7 +16433,7 @@ var BffClient = class {
|
|
|
16276
16433
|
redirect: "error"
|
|
16277
16434
|
});
|
|
16278
16435
|
} catch (error) {
|
|
16279
|
-
throw new BffError(`${method} /${
|
|
16436
|
+
throw new BffError(`${method} /${path30}: ${describeFetchError(error)}`, 0);
|
|
16280
16437
|
}
|
|
16281
16438
|
const serverTime = Date.parse(response.headers.get("date") ?? "");
|
|
16282
16439
|
if (Number.isFinite(serverTime)) {
|
|
@@ -16289,7 +16446,7 @@ var BffClient = class {
|
|
|
16289
16446
|
const code = typeof problem?.code === "string" ? problem.code : void 0;
|
|
16290
16447
|
const detail = typeof problem?.detail === "string" ? problem.detail : void 0;
|
|
16291
16448
|
throw new BffError(
|
|
16292
|
-
`${method} /${
|
|
16449
|
+
`${method} /${path30}: ${response.status}${code ? ` ${code}` : ""}${detail ? `: ${detail}` : ""}`,
|
|
16293
16450
|
response.status,
|
|
16294
16451
|
code,
|
|
16295
16452
|
retryAfter(response.headers.get("retry-after"))
|
|
@@ -16298,7 +16455,7 @@ var BffClient = class {
|
|
|
16298
16455
|
if (options.emptyAnswer) return void 0;
|
|
16299
16456
|
const parsed = parseJson(text);
|
|
16300
16457
|
if (parsed === void 0 || parsed === null || typeof parsed !== "object") {
|
|
16301
|
-
throw new BffError(`${method} /${
|
|
16458
|
+
throw new BffError(`${method} /${path30}: ${response.status} with a body that is not JSON`, 502);
|
|
16302
16459
|
}
|
|
16303
16460
|
return parsed;
|
|
16304
16461
|
}
|
|
@@ -16343,6 +16500,9 @@ var AuthRefusalCode = {
|
|
|
16343
16500
|
AlreadyConnected: "already_connected",
|
|
16344
16501
|
InvalidRequest: "invalid_request"
|
|
16345
16502
|
};
|
|
16503
|
+
var LaunchErrorKind = {
|
|
16504
|
+
PrepareFailed: "PrepareFailed"
|
|
16505
|
+
};
|
|
16346
16506
|
var EndResult = {
|
|
16347
16507
|
Written: "Written",
|
|
16348
16508
|
AlreadyEnded: "AlreadyEnded",
|
|
@@ -16386,6 +16546,11 @@ var StartRefusal = {
|
|
|
16386
16546
|
BundleInvalid: "BundleInvalid",
|
|
16387
16547
|
/** The run's bundle could not be read in time (the BFF away, or not this machine's run any more). */
|
|
16388
16548
|
BundleUnavailable: "BundleUnavailable",
|
|
16549
|
+
/**
|
|
16550
|
+
* A repository the run works in cannot be reached with this machine's git (`git ls-remote` failed or did not answer in
|
|
16551
|
+
* time): holds until a person acts (the machine's credentials, its network, the repository's access).
|
|
16552
|
+
*/
|
|
16553
|
+
RepoUnavailable: "RepoUnavailable",
|
|
16389
16554
|
UpdateRequired: "UpdateRequired",
|
|
16390
16555
|
ShuttingDown: "ShuttingDown"
|
|
16391
16556
|
};
|
|
@@ -16498,14 +16663,14 @@ function refusalMessage(reason) {
|
|
|
16498
16663
|
}
|
|
16499
16664
|
|
|
16500
16665
|
// src/runner/lock.ts
|
|
16501
|
-
import
|
|
16666
|
+
import fs13 from "node:fs";
|
|
16502
16667
|
import net from "node:net";
|
|
16503
|
-
import
|
|
16668
|
+
import path20 from "node:path";
|
|
16504
16669
|
var TAKEOVER_STALE_MS = 3e4;
|
|
16505
16670
|
var TAKEOVER_WAIT_MS = 50;
|
|
16506
16671
|
var ATTEMPTS = 40;
|
|
16507
16672
|
async function acquireInstanceLock(file) {
|
|
16508
|
-
ensurePrivateDirectory(
|
|
16673
|
+
ensurePrivateDirectory(path20.dirname(file));
|
|
16509
16674
|
const takeover = `${file}.takeover`;
|
|
16510
16675
|
for (let attempt = 0; attempt < ATTEMPTS; attempt++) {
|
|
16511
16676
|
const lock = await listen(file);
|
|
@@ -16517,10 +16682,10 @@ async function acquireInstanceLock(file) {
|
|
|
16517
16682
|
}
|
|
16518
16683
|
try {
|
|
16519
16684
|
if (await isHeld(file)) return void 0;
|
|
16520
|
-
|
|
16685
|
+
fs13.rmSync(file, { force: true });
|
|
16521
16686
|
return await listen(file);
|
|
16522
16687
|
} finally {
|
|
16523
|
-
|
|
16688
|
+
fs13.rmSync(takeover, { recursive: true, force: true });
|
|
16524
16689
|
}
|
|
16525
16690
|
}
|
|
16526
16691
|
return void 0;
|
|
@@ -16542,13 +16707,13 @@ async function listen(file) {
|
|
|
16542
16707
|
}
|
|
16543
16708
|
function enterTakeover(directory) {
|
|
16544
16709
|
try {
|
|
16545
|
-
|
|
16710
|
+
fs13.mkdirSync(directory, { mode: 448 });
|
|
16546
16711
|
return true;
|
|
16547
16712
|
} catch (error) {
|
|
16548
16713
|
if (error.code !== "EEXIST") throw error;
|
|
16549
16714
|
}
|
|
16550
16715
|
try {
|
|
16551
|
-
if (Date.now() -
|
|
16716
|
+
if (Date.now() - fs13.statSync(directory).mtimeMs > TAKEOVER_STALE_MS) fs13.rmSync(directory, { recursive: true, force: true });
|
|
16552
16717
|
} catch {
|
|
16553
16718
|
}
|
|
16554
16719
|
return false;
|
|
@@ -16630,7 +16795,7 @@ async function logoutCommand(args, io, environment = {}) {
|
|
|
16630
16795
|
removeRunnerFiles(home);
|
|
16631
16796
|
} finally {
|
|
16632
16797
|
await lock.release();
|
|
16633
|
-
|
|
16798
|
+
fs14.rmSync(files.state, { recursive: true, force: true });
|
|
16634
16799
|
removeIfEmpty(home);
|
|
16635
16800
|
}
|
|
16636
16801
|
const name = config?.machineName ? `"${config.machineName}"` : "This machine";
|
|
@@ -16671,30 +16836,378 @@ async function tellCodesona(store, config, fetchImpl) {
|
|
|
16671
16836
|
}
|
|
16672
16837
|
function removeRunnerFiles(home) {
|
|
16673
16838
|
const files = runnerFiles(home);
|
|
16674
|
-
|
|
16675
|
-
for (const directory of [files.outbox, files.spool, files.agents])
|
|
16839
|
+
fs14.rmSync(files.config, { force: true });
|
|
16840
|
+
for (const directory of [files.outbox, files.spool, files.agents]) fs14.rmSync(directory, { recursive: true, force: true });
|
|
16676
16841
|
for (const name of safeList(files.state)) {
|
|
16677
|
-
if (name !==
|
|
16842
|
+
if (name !== path21.basename(files.lock)) fs14.rmSync(path21.join(files.state, name), { recursive: true, force: true });
|
|
16678
16843
|
}
|
|
16679
16844
|
}
|
|
16680
16845
|
function safeList(directory) {
|
|
16681
16846
|
try {
|
|
16682
|
-
return
|
|
16847
|
+
return fs14.readdirSync(directory);
|
|
16683
16848
|
} catch {
|
|
16684
16849
|
return [];
|
|
16685
16850
|
}
|
|
16686
16851
|
}
|
|
16687
16852
|
function removeIfEmpty(directory) {
|
|
16688
16853
|
try {
|
|
16689
|
-
|
|
16854
|
+
fs14.rmdirSync(directory);
|
|
16690
16855
|
} catch {
|
|
16691
16856
|
}
|
|
16692
16857
|
}
|
|
16693
16858
|
|
|
16694
|
-
// src/runner/
|
|
16859
|
+
// src/runner/prune.ts
|
|
16860
|
+
import fs16 from "node:fs";
|
|
16861
|
+
import path23 from "node:path";
|
|
16695
16862
|
import { parseArgs as parseArgs4 } from "node:util";
|
|
16863
|
+
|
|
16864
|
+
// src/runner/repos.ts
|
|
16865
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
16866
|
+
import { randomBytes as randomBytes8 } from "node:crypto";
|
|
16867
|
+
import fs15 from "node:fs";
|
|
16868
|
+
import path22 from "node:path";
|
|
16869
|
+
function gitWith(environment = process.env) {
|
|
16870
|
+
const base = { ...agentEnvironment(environment), GIT_TERMINAL_PROMPT: "0" };
|
|
16871
|
+
return (args, options = {}) => new Promise((resolve4) => {
|
|
16872
|
+
execFile2("git", [...args], {
|
|
16873
|
+
cwd: options.cwd ?? "/",
|
|
16874
|
+
env: withGitEnvironment(base, options.env),
|
|
16875
|
+
timeout: options.timeoutMs ?? COMMAND_TIMEOUT_MS,
|
|
16876
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
16877
|
+
encoding: "utf8",
|
|
16878
|
+
signal: options.signal
|
|
16879
|
+
}, (error, stdout, stderr) => {
|
|
16880
|
+
if (!error) return resolve4({ code: 0, stdout, stderr });
|
|
16881
|
+
const failure = error;
|
|
16882
|
+
if (failure.name === "AbortError") return resolve4({ code: -1, stdout: stdout ?? "", stderr: stderr ?? "", aborted: true });
|
|
16883
|
+
const code = typeof failure.code === "number" ? failure.code : -1;
|
|
16884
|
+
resolve4({ code, stdout: stdout ?? "", stderr: stderr || (code === -1 ? failure.message : ""), timedOut: failure.killed === true && code === -1 });
|
|
16885
|
+
});
|
|
16886
|
+
});
|
|
16887
|
+
}
|
|
16888
|
+
function withGitEnvironment(base, extra = {}) {
|
|
16889
|
+
const result = {};
|
|
16890
|
+
for (const [name, value] of Object.entries(base)) if (value !== void 0) result[name] = value;
|
|
16891
|
+
const extraCount = Number(extra.GIT_CONFIG_COUNT ?? 0);
|
|
16892
|
+
for (const [name, value] of Object.entries(extra)) if (!/^GIT_CONFIG_(COUNT|KEY_\d+|VALUE_\d+)$/.test(name)) result[name] = value;
|
|
16893
|
+
if (Number.isSafeInteger(extraCount) && extraCount > 0) {
|
|
16894
|
+
const baseCount = Number(result.GIT_CONFIG_COUNT ?? 0);
|
|
16895
|
+
const offset = Number.isSafeInteger(baseCount) && baseCount > 0 ? baseCount : 0;
|
|
16896
|
+
let added = 0;
|
|
16897
|
+
for (let index = 0; index < extraCount; index++) {
|
|
16898
|
+
const key = extra[`GIT_CONFIG_KEY_${index}`];
|
|
16899
|
+
const value = extra[`GIT_CONFIG_VALUE_${index}`];
|
|
16900
|
+
if (key === void 0 || value === void 0) continue;
|
|
16901
|
+
result[`GIT_CONFIG_KEY_${offset + added}`] = key;
|
|
16902
|
+
result[`GIT_CONFIG_VALUE_${offset + added}`] = value;
|
|
16903
|
+
added++;
|
|
16904
|
+
}
|
|
16905
|
+
result.GIT_CONFIG_COUNT = String(offset + added);
|
|
16906
|
+
}
|
|
16907
|
+
return result;
|
|
16908
|
+
}
|
|
16909
|
+
function sessionGitEnvironment(env) {
|
|
16910
|
+
return Object.fromEntries(Object.entries(env).filter(([name]) => /^GIT_(AUTHOR|COMMITTER)_(NAME|EMAIL)$|^GIT_CONFIG_(COUNT|KEY_\d+|VALUE_\d+)$/.test(name)));
|
|
16911
|
+
}
|
|
16912
|
+
var COMMAND_TIMEOUT_MS = 2 * 60 * 1e3;
|
|
16913
|
+
var FETCH_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
16914
|
+
var LS_REMOTE_TIMEOUT_MS = 15e3;
|
|
16915
|
+
var KEPT_BUILD_CACHES = ["node_modules", "bin", "obj", ".nuget", ".gradle", "target", "dist"];
|
|
16916
|
+
function withoutUserinfo(text) {
|
|
16917
|
+
return text.replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^/@\s]*@/gi, "$1");
|
|
16918
|
+
}
|
|
16919
|
+
function shortGitError(result, timeoutMs) {
|
|
16920
|
+
if (result.timedOut) return timeoutMs ? `git did not answer within ${Math.round(timeoutMs / 1e3)}s` : "git did not answer in time";
|
|
16921
|
+
const lines = result.stderr.split("\n").map((line2) => line2.trim()).filter(Boolean);
|
|
16922
|
+
const line = [...lines].reverse().find((candidate) => /^(fatal|error):/i.test(candidate)) ?? lines.at(-1) ?? `git exited with ${result.code}`;
|
|
16923
|
+
const clean = redactCredentials(withoutUserinfo(line)).replace(/^(fatal|error):\s*/i, "");
|
|
16924
|
+
return clean.length > 300 ? `${clean.slice(0, 297)}\u2026` : clean;
|
|
16925
|
+
}
|
|
16926
|
+
var PrepareError = class extends Error {
|
|
16927
|
+
};
|
|
16928
|
+
var PrepareAborted = class extends Error {
|
|
16929
|
+
};
|
|
16930
|
+
var repoLocks = /* @__PURE__ */ new Map();
|
|
16931
|
+
async function withRepoLock(key, work) {
|
|
16932
|
+
const before = repoLocks.get(key) ?? Promise.resolve();
|
|
16933
|
+
const current = before.catch(() => void 0).then(work);
|
|
16934
|
+
const settled = current.catch(() => void 0);
|
|
16935
|
+
repoLocks.set(key, settled);
|
|
16936
|
+
try {
|
|
16937
|
+
return await current;
|
|
16938
|
+
} finally {
|
|
16939
|
+
if (repoLocks.get(key) === settled) repoLocks.delete(key);
|
|
16940
|
+
}
|
|
16941
|
+
}
|
|
16942
|
+
var RepoPreparer = class {
|
|
16943
|
+
constructor(options) {
|
|
16944
|
+
this.options = options;
|
|
16945
|
+
}
|
|
16946
|
+
options;
|
|
16947
|
+
/**
|
|
16948
|
+
* Whether this machine's git reaches every repository (`git ls-remote` of its task and default branches), within
|
|
16949
|
+
* `withinMs` at most; undefined when it does, else a sentence for a person naming the first that it does not.
|
|
16950
|
+
*/
|
|
16951
|
+
async checkAccess(repos, withinMs) {
|
|
16952
|
+
const timeoutMs = Math.max(1e3, Math.min(LS_REMOTE_TIMEOUT_MS, withinMs));
|
|
16953
|
+
const answers = await Promise.all(repos.map(async (repo) => {
|
|
16954
|
+
const result = await this.options.git(["ls-remote", "--quiet", repo.cloneUrl, `refs/heads/${repo.taskBranch}`, `refs/heads/${repo.defaultBranch}`], { timeoutMs });
|
|
16955
|
+
return result.code === 0 ? void 0 : `This Mac cannot reach the repository ${repo.name} (${withoutUserinfo(repo.cloneUrl)}) with its git: ${shortGitError(result, timeoutMs)}`;
|
|
16956
|
+
}));
|
|
16957
|
+
return answers.find((answer) => answer !== void 0);
|
|
16958
|
+
}
|
|
16959
|
+
/** Each repository's worktree under `<agentDirectory>/repos/`, on its task branch. Throws PrepareError, or PrepareAborted. */
|
|
16960
|
+
async prepare(agentDirectory, repos, options = {}) {
|
|
16961
|
+
for (const repo of repos) {
|
|
16962
|
+
const cache = path22.join(this.options.cacheDirectory, `${repo.repoId}.git`);
|
|
16963
|
+
await withRepoLock(cache, () => new RepoWork(this.options, repo, cache, path22.join(agentDirectory, "repos", repo.name), options).run());
|
|
16964
|
+
}
|
|
16965
|
+
}
|
|
16966
|
+
};
|
|
16967
|
+
var RepoWork = class {
|
|
16968
|
+
constructor(options, repo, cache, worktree, prepare) {
|
|
16969
|
+
this.options = options;
|
|
16970
|
+
this.repo = repo;
|
|
16971
|
+
this.cache = cache;
|
|
16972
|
+
this.worktree = worktree;
|
|
16973
|
+
this.prepare = prepare;
|
|
16974
|
+
}
|
|
16975
|
+
options;
|
|
16976
|
+
repo;
|
|
16977
|
+
cache;
|
|
16978
|
+
worktree;
|
|
16979
|
+
prepare;
|
|
16980
|
+
async run() {
|
|
16981
|
+
await this.updateCache();
|
|
16982
|
+
const start = await this.startPoint();
|
|
16983
|
+
const state = this.worktreeState(await this.commonDirectory());
|
|
16984
|
+
if (state !== "ours") {
|
|
16985
|
+
if (state === "other") {
|
|
16986
|
+
this.options.log.warn(`${this.repo.name}: ${this.worktree} is not a worktree of the repository's cache; it is made again`);
|
|
16987
|
+
fs15.rmSync(this.worktree, { recursive: true, force: true });
|
|
16988
|
+
}
|
|
16989
|
+
fs15.mkdirSync(path22.dirname(this.worktree), { recursive: true, mode: 448 });
|
|
16990
|
+
await this.git(["worktree", "prune"], "the cache's worktrees could not be pruned", { cwd: this.cache });
|
|
16991
|
+
await this.git(["worktree", "add", "--quiet", "-B", this.repo.taskBranch, this.worktree, start.ref], `the worktree could not be made on ${this.repo.taskBranch}`, { cwd: this.cache });
|
|
16992
|
+
if (start.local) await this.fastForward();
|
|
16993
|
+
} else {
|
|
16994
|
+
const current = (await this.exec(["symbolic-ref", "--quiet", "--short", "HEAD"], { cwd: this.worktree })).stdout.trim();
|
|
16995
|
+
if (current !== this.repo.taskBranch) {
|
|
16996
|
+
await this.git(["reset", "--hard", "--quiet"], "the worktree could not be reset", { cwd: this.worktree });
|
|
16997
|
+
await this.git(["clean", "-f", "-d", "-x", "-q", ...KEPT_BUILD_CACHES.flatMap((name) => ["-e", name])], "the worktree could not be cleaned", { cwd: this.worktree });
|
|
16998
|
+
await this.git(["checkout", "--quiet", "-B", this.repo.taskBranch, start.ref], `the worktree could not be switched to ${this.repo.taskBranch}`, { cwd: this.worktree });
|
|
16999
|
+
if (start.local) await this.fastForward();
|
|
17000
|
+
} else {
|
|
17001
|
+
await this.fastForward();
|
|
17002
|
+
}
|
|
17003
|
+
}
|
|
17004
|
+
if (this.repo.holdingRef) await this.mergeHoldingRef(this.repo.holdingRef);
|
|
17005
|
+
}
|
|
17006
|
+
/**
|
|
17007
|
+
* The cache made (a bare repository with origin's branches under refs/remotes/origin, the layout `git clone --bare`
|
|
17008
|
+
* would give with that refspec, minus local copies of every branch) or brought up to date. The fetch prunes only
|
|
17009
|
+
* refs/remotes/origin: a local branch is never pruned, so work pushed to a holding ref and never to its feature
|
|
17010
|
+
* branch keeps its local branch.
|
|
17011
|
+
*/
|
|
17012
|
+
async updateCache() {
|
|
17013
|
+
if (!fs15.existsSync(path22.join(this.cache, "HEAD"))) {
|
|
17014
|
+
ensurePrivateDirectory(path22.dirname(this.cache));
|
|
17015
|
+
const making = `${this.cache}.${process.pid}.${randomBytes8(4).toString("hex")}.tmp`;
|
|
17016
|
+
try {
|
|
17017
|
+
await this.git(["init", "--bare", "--quiet", making], "the repository's cache could not be made");
|
|
17018
|
+
await this.git(["remote", "add", "origin", this.repo.cloneUrl], "the repository's cache could not be made", { cwd: making });
|
|
17019
|
+
await this.fetch(making);
|
|
17020
|
+
fs15.renameSync(making, this.cache);
|
|
17021
|
+
} finally {
|
|
17022
|
+
fs15.rmSync(making, { recursive: true, force: true });
|
|
17023
|
+
}
|
|
17024
|
+
return;
|
|
17025
|
+
}
|
|
17026
|
+
await this.git(["config", "remote.origin.url", this.repo.cloneUrl], "the repository's cache could not be set up", { cwd: this.cache });
|
|
17027
|
+
await this.git(["config", "--replace-all", "remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*"], "the repository's cache could not be set up", { cwd: this.cache });
|
|
17028
|
+
await this.fetch(this.cache);
|
|
17029
|
+
}
|
|
17030
|
+
async fetch(directory) {
|
|
17031
|
+
await this.git(["fetch", "--prune", "--quiet", "origin"], "fetching from origin failed", { cwd: directory, timeoutMs: FETCH_TIMEOUT_MS });
|
|
17032
|
+
}
|
|
17033
|
+
/** Where the task branch starts: the machine's own branch, else origin's, else the planned base commit. */
|
|
17034
|
+
async startPoint() {
|
|
17035
|
+
const branch = this.repo.taskBranch;
|
|
17036
|
+
if (await this.exists(`refs/heads/${branch}`)) return { ref: branch, local: true };
|
|
17037
|
+
if (await this.exists(`refs/remotes/origin/${branch}`)) return { ref: `refs/remotes/origin/${branch}`, local: false };
|
|
17038
|
+
if (!await this.exists(this.repo.baseSha)) {
|
|
17039
|
+
await this.exec(["fetch", "--quiet", "origin", this.repo.baseSha], { cwd: this.cache, timeoutMs: FETCH_TIMEOUT_MS });
|
|
17040
|
+
if (!await this.exists(this.repo.baseSha)) throw this.failure(`its base commit ${this.repo.baseSha.slice(0, 12)} is not in the repository`);
|
|
17041
|
+
}
|
|
17042
|
+
return { ref: this.repo.baseSha, local: false };
|
|
17043
|
+
}
|
|
17044
|
+
async exists(ref) {
|
|
17045
|
+
return (await this.exec(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], { cwd: this.cache })).code === 0;
|
|
17046
|
+
}
|
|
17047
|
+
/** The repository the directory at the worktree's path belongs to, when it is a worktree at all. */
|
|
17048
|
+
async commonDirectory() {
|
|
17049
|
+
if (!fs15.existsSync(path22.join(this.worktree, ".git"))) return void 0;
|
|
17050
|
+
const result = await this.exec(["rev-parse", "--git-common-dir"], { cwd: this.worktree });
|
|
17051
|
+
return result.code === 0 && result.stdout.trim() ? path22.resolve(this.worktree, result.stdout.trim()) : void 0;
|
|
17052
|
+
}
|
|
17053
|
+
worktreeState(common) {
|
|
17054
|
+
if (!fs15.existsSync(this.worktree)) return "missing";
|
|
17055
|
+
if (!common) return "other";
|
|
17056
|
+
try {
|
|
17057
|
+
return fs15.realpathSync(common) === fs15.realpathSync(this.cache) ? "ours" : "other";
|
|
17058
|
+
} catch {
|
|
17059
|
+
return "other";
|
|
17060
|
+
}
|
|
17061
|
+
}
|
|
17062
|
+
/** The task branch brought up to origin's, only when it has no commit origin lacks: unpushed work is never dropped. */
|
|
17063
|
+
async fastForward() {
|
|
17064
|
+
const remote = `refs/remotes/origin/${this.repo.taskBranch}`;
|
|
17065
|
+
if (!await this.exists(remote)) return;
|
|
17066
|
+
const behind = await this.exec(["merge-base", "--is-ancestor", "HEAD", remote], { cwd: this.worktree });
|
|
17067
|
+
if (behind.code === 1) {
|
|
17068
|
+
this.options.log.info(`${this.repo.name}: ${this.repo.taskBranch} has commits origin does not; left as it is`);
|
|
17069
|
+
return;
|
|
17070
|
+
}
|
|
17071
|
+
if (behind.code !== 0) throw this.failure(`its branch could not be compared with origin's: ${shortGitError(behind)}`);
|
|
17072
|
+
const merged = await this.exec(["merge", "--ff-only", "--quiet", remote], { cwd: this.worktree });
|
|
17073
|
+
if (merged.code !== 0) this.options.log.warn(`${this.repo.name}: ${this.repo.taskBranch} could not be brought up to origin's (${shortGitError(merged)}); left as it is`);
|
|
17074
|
+
}
|
|
17075
|
+
/** The holding ref's work brought into the task branch: a fast-forward when it can be, else a merge; a conflict fails. */
|
|
17076
|
+
async mergeHoldingRef(ref) {
|
|
17077
|
+
await this.git(["fetch", "--quiet", "origin", ref], `the holding ref ${ref} could not be fetched`, { cwd: this.worktree, timeoutMs: FETCH_TIMEOUT_MS });
|
|
17078
|
+
if ((await this.exec(["merge-base", "--is-ancestor", "FETCH_HEAD", "HEAD"], { cwd: this.worktree })).code === 0) return;
|
|
17079
|
+
if ((await this.exec(["merge", "--ff-only", "--quiet", "FETCH_HEAD"], { cwd: this.worktree })).code === 0) return;
|
|
17080
|
+
const merged = await this.exec(["-c", "commit.gpgsign=false", "merge", "--no-edit", "--quiet", "FETCH_HEAD"], { cwd: this.worktree });
|
|
17081
|
+
if (merged.code === 0) return;
|
|
17082
|
+
await this.exec(["merge", "--abort"], { cwd: this.worktree });
|
|
17083
|
+
throw this.failure(`the holding ref ${ref} does not merge into ${this.repo.taskBranch}: ${shortGitError(merged)}`);
|
|
17084
|
+
}
|
|
17085
|
+
failure(what) {
|
|
17086
|
+
return new PrepareError(`${this.repo.name}: ${what}`);
|
|
17087
|
+
}
|
|
17088
|
+
/** git, failing the preparation with `what` and git's own words when it fails. */
|
|
17089
|
+
async git(args, what, options = {}) {
|
|
17090
|
+
const result = await this.exec(args, options);
|
|
17091
|
+
if (result.code !== 0) throw this.failure(`${what}: ${shortGitError(result, options.timeoutMs ?? COMMAND_TIMEOUT_MS)}`);
|
|
17092
|
+
return result;
|
|
17093
|
+
}
|
|
17094
|
+
async exec(args, options = {}) {
|
|
17095
|
+
if (this.prepare.signal?.aborted) throw new PrepareAborted("stopped");
|
|
17096
|
+
let result;
|
|
17097
|
+
try {
|
|
17098
|
+
result = await this.options.git(args, { ...options, env: { ...this.prepare.gitEnvironment, ...options.env }, signal: this.prepare.signal });
|
|
17099
|
+
} catch (error) {
|
|
17100
|
+
throw this.failure(`git could not be run: ${describeError(error)}`);
|
|
17101
|
+
}
|
|
17102
|
+
if (result.aborted) throw new PrepareAborted("stopped");
|
|
17103
|
+
return result;
|
|
17104
|
+
}
|
|
17105
|
+
};
|
|
17106
|
+
|
|
17107
|
+
// src/runner/prune.ts
|
|
17108
|
+
var PRUNE_AFTER_MS = 14 * 24 * 60 * 60 * 1e3;
|
|
17109
|
+
async function pruneCommand(args, io, environment = {}) {
|
|
17110
|
+
parseArgs4({ args, options: {}, strict: true, allowPositionals: false });
|
|
17111
|
+
const env = environment.env ?? process.env;
|
|
17112
|
+
const home = runnerHome(env);
|
|
17113
|
+
const darwin = (environment.platform ?? process.platform) === "darwin";
|
|
17114
|
+
const busy = await runningSessions(home, environment.launchAgent ?? (darwin ? new LaunchAgent() : void 0));
|
|
17115
|
+
if (busy.count > 0) {
|
|
17116
|
+
io.err(`Not pruned: Codesona Runner is running ${busy.count === 1 ? "a session" : `${busy.count} sessions`}. Run codesona-runner prune again once it is idle (codesona-runner status shows its sessions).`);
|
|
17117
|
+
return ExitCode.Failed;
|
|
17118
|
+
}
|
|
17119
|
+
const report = await pruneRepositories(runnerFiles(home), environment.git ?? gitWith(env), (environment.now ?? Date.now)());
|
|
17120
|
+
for (const worktree of report.removedWorktrees) io.out(`removed ${worktree}`);
|
|
17121
|
+
for (const branch of report.deletedBranches) io.out(`deleted branch ${branch}`);
|
|
17122
|
+
for (const sentence of report.kept) io.out(`kept ${sentence}`);
|
|
17123
|
+
if (report.removedWorktrees.length === 0 && report.deletedBranches.length === 0) io.out("Nothing to prune.");
|
|
17124
|
+
return ExitCode.Ok;
|
|
17125
|
+
}
|
|
17126
|
+
async function pruneRepositories(files, git, now, olderThanMs = PRUNE_AFTER_MS) {
|
|
17127
|
+
const report = { removedWorktrees: [], deletedBranches: [], kept: [] };
|
|
17128
|
+
let caches;
|
|
17129
|
+
try {
|
|
17130
|
+
caches = fs16.readdirSync(files.repos).filter((name) => name.endsWith(".git")).map((name) => path23.join(files.repos, name));
|
|
17131
|
+
} catch {
|
|
17132
|
+
return report;
|
|
17133
|
+
}
|
|
17134
|
+
for (const cache of caches) await pruneRepository(cache, git, now - olderThanMs, report);
|
|
17135
|
+
return report;
|
|
17136
|
+
}
|
|
17137
|
+
async function pruneRepository(cache, git, before, report) {
|
|
17138
|
+
const name = path23.basename(cache, ".git");
|
|
17139
|
+
const fetched = await git(["fetch", "--prune", "--quiet", "origin"], { cwd: cache, timeoutMs: 5 * 60 * 1e3 });
|
|
17140
|
+
if (fetched.code !== 0) {
|
|
17141
|
+
report.kept.push(`everything of ${name}: origin could not be asked (${shortGitError(fetched)})`);
|
|
17142
|
+
return;
|
|
17143
|
+
}
|
|
17144
|
+
await git(["worktree", "prune"], { cwd: cache });
|
|
17145
|
+
const onOrigin = async (branch) => (await git(["merge-base", "--is-ancestor", `refs/heads/${branch}`, `refs/remotes/origin/${branch}`], { cwd: cache })).code === 0;
|
|
17146
|
+
const worktrees = await listWorktrees(cache, git);
|
|
17147
|
+
const checkedOut = /* @__PURE__ */ new Set();
|
|
17148
|
+
for (const worktree of worktrees) {
|
|
17149
|
+
if (!worktree.branch) continue;
|
|
17150
|
+
checkedOut.add(worktree.branch);
|
|
17151
|
+
if (lastTouched(worktree.path) > before) continue;
|
|
17152
|
+
const status = await git(["status", "--porcelain"], { cwd: worktree.path });
|
|
17153
|
+
if (status.code !== 0 || status.stdout.trim()) {
|
|
17154
|
+
report.kept.push(`${worktree.path}: it has changes that were never committed`);
|
|
17155
|
+
continue;
|
|
17156
|
+
}
|
|
17157
|
+
if (!await onOrigin(worktree.branch)) {
|
|
17158
|
+
report.kept.push(`${worktree.path}: ${worktree.branch} has commits origin does not`);
|
|
17159
|
+
continue;
|
|
17160
|
+
}
|
|
17161
|
+
if ((await git(["worktree", "remove", "--force", worktree.path], { cwd: cache })).code !== 0) continue;
|
|
17162
|
+
report.removedWorktrees.push(worktree.path);
|
|
17163
|
+
checkedOut.delete(worktree.branch);
|
|
17164
|
+
}
|
|
17165
|
+
const refs = await git(["for-each-ref", "--format=%(refname:short)%09%(committerdate:unix)", "refs/heads"], { cwd: cache });
|
|
17166
|
+
for (const line of refs.code === 0 ? refs.stdout.split("\n").filter(Boolean) : []) {
|
|
17167
|
+
const [branch = "", committed = "0"] = line.split(" ");
|
|
17168
|
+
if (!branch || checkedOut.has(branch) || Number(committed) * 1e3 > before) continue;
|
|
17169
|
+
if (!await onOrigin(branch)) {
|
|
17170
|
+
report.kept.push(`branch ${branch} of ${name}: it has commits origin does not`);
|
|
17171
|
+
continue;
|
|
17172
|
+
}
|
|
17173
|
+
if ((await git(["branch", "-D", "--quiet", branch], { cwd: cache })).code === 0) report.deletedBranches.push(`${branch} (${name})`);
|
|
17174
|
+
}
|
|
17175
|
+
}
|
|
17176
|
+
async function listWorktrees(cache, git) {
|
|
17177
|
+
const listed = await git(["worktree", "list", "--porcelain"], { cwd: cache });
|
|
17178
|
+
if (listed.code !== 0) return [];
|
|
17179
|
+
const entries = [];
|
|
17180
|
+
for (const block of listed.stdout.split("\n\n")) {
|
|
17181
|
+
const lines = block.split("\n");
|
|
17182
|
+
const at = lines.find((line) => line.startsWith("worktree "))?.slice("worktree ".length);
|
|
17183
|
+
if (!at || lines.includes("bare")) continue;
|
|
17184
|
+
const branch = lines.find((line) => line.startsWith("branch refs/heads/"))?.slice("branch refs/heads/".length);
|
|
17185
|
+
entries.push({ path: at, branch });
|
|
17186
|
+
}
|
|
17187
|
+
return entries;
|
|
17188
|
+
}
|
|
17189
|
+
function lastTouched(worktree) {
|
|
17190
|
+
let gitDirectory;
|
|
17191
|
+
try {
|
|
17192
|
+
const pointer = /^gitdir:\s*(.+)$/m.exec(fs16.readFileSync(path23.join(worktree, ".git"), "utf8"))?.[1]?.trim();
|
|
17193
|
+
if (pointer) gitDirectory = path23.resolve(worktree, pointer);
|
|
17194
|
+
} catch {
|
|
17195
|
+
}
|
|
17196
|
+
const candidates = [worktree, ...gitDirectory ? ["index", "HEAD", path23.join("logs", "HEAD")].map((file) => path23.join(gitDirectory, file)) : []];
|
|
17197
|
+
let latest = 0;
|
|
17198
|
+
for (const file of candidates) {
|
|
17199
|
+
try {
|
|
17200
|
+
latest = Math.max(latest, fs16.statSync(file).mtimeMs);
|
|
17201
|
+
} catch {
|
|
17202
|
+
}
|
|
17203
|
+
}
|
|
17204
|
+
return latest;
|
|
17205
|
+
}
|
|
17206
|
+
|
|
17207
|
+
// src/runner/restart.ts
|
|
17208
|
+
import { parseArgs as parseArgs5 } from "node:util";
|
|
16696
17209
|
async function restartCommand(args, io, environment = {}) {
|
|
16697
|
-
const { values } =
|
|
17210
|
+
const { values } = parseArgs5({ args, options: { force: { type: "boolean" } }, strict: true, allowPositionals: false });
|
|
16698
17211
|
const byHand = "A Runner run by hand (codesona-runner run) is restarted by stopping it (Ctrl-C) and running it again.";
|
|
16699
17212
|
if ((environment.platform ?? process.platform) !== "darwin") {
|
|
16700
17213
|
io.err(`codesona-runner restart restarts the background service, which Codesona Runner sets up on macOS only. ${byHand}`);
|
|
@@ -16750,11 +17263,11 @@ async function restartCommand(args, io, environment = {}) {
|
|
|
16750
17263
|
}
|
|
16751
17264
|
|
|
16752
17265
|
// src/runner/run.ts
|
|
16753
|
-
import { parseArgs as
|
|
17266
|
+
import { parseArgs as parseArgs6 } from "node:util";
|
|
16754
17267
|
|
|
16755
17268
|
// src/runner/logfile.ts
|
|
16756
|
-
import
|
|
16757
|
-
import
|
|
17269
|
+
import fs17 from "node:fs";
|
|
17270
|
+
import path24 from "node:path";
|
|
16758
17271
|
var LOG_MAX_BYTES = 10 * 1024 * 1024;
|
|
16759
17272
|
var RotatingLogFile = class {
|
|
16760
17273
|
constructor(file, maxBytes = LOG_MAX_BYTES) {
|
|
@@ -16772,36 +17285,36 @@ var RotatingLogFile = class {
|
|
|
16772
17285
|
const bytes = Buffer.byteLength(data);
|
|
16773
17286
|
if (this.fd === void 0) this.open();
|
|
16774
17287
|
if (this.size > 0 && this.size + bytes > this.maxBytes) this.turnOver();
|
|
16775
|
-
|
|
17288
|
+
fs17.writeSync(this.fd, data);
|
|
16776
17289
|
this.size += bytes;
|
|
16777
17290
|
} catch {
|
|
16778
17291
|
}
|
|
16779
17292
|
}
|
|
16780
17293
|
close() {
|
|
16781
|
-
if (this.fd !== void 0)
|
|
17294
|
+
if (this.fd !== void 0) fs17.closeSync(this.fd);
|
|
16782
17295
|
this.fd = void 0;
|
|
16783
17296
|
}
|
|
16784
17297
|
open() {
|
|
16785
|
-
ensurePrivateDirectory(
|
|
16786
|
-
this.fd =
|
|
16787
|
-
this.size =
|
|
17298
|
+
ensurePrivateDirectory(path24.dirname(this.file));
|
|
17299
|
+
this.fd = fs17.openSync(this.file, "a", 384);
|
|
17300
|
+
this.size = fs17.fstatSync(this.fd).size;
|
|
16788
17301
|
}
|
|
16789
17302
|
turnOver() {
|
|
16790
17303
|
this.close();
|
|
16791
|
-
|
|
17304
|
+
fs17.renameSync(this.file, `${this.file}.1`);
|
|
16792
17305
|
this.open();
|
|
16793
17306
|
}
|
|
16794
17307
|
};
|
|
16795
17308
|
function trimFile(file, maxBytes = LOG_MAX_BYTES) {
|
|
16796
17309
|
try {
|
|
16797
|
-
if (
|
|
17310
|
+
if (fs17.statSync(file).size > maxBytes) fs17.truncateSync(file, 0);
|
|
16798
17311
|
} catch {
|
|
16799
17312
|
}
|
|
16800
17313
|
}
|
|
16801
17314
|
|
|
16802
17315
|
// src/runner/events.ts
|
|
16803
|
-
import
|
|
16804
|
-
import
|
|
17316
|
+
import fs18 from "node:fs";
|
|
17317
|
+
import path25 from "node:path";
|
|
16805
17318
|
var MAX_TEXT = 2 * 1024;
|
|
16806
17319
|
var MAX_BATCH_EVENTS = 32;
|
|
16807
17320
|
var MAX_BATCH_BYTES = 24 * 1024;
|
|
@@ -16892,7 +17405,7 @@ var ActivityChannel = class {
|
|
|
16892
17405
|
}
|
|
16893
17406
|
try {
|
|
16894
17407
|
ensurePrivateDirectory(this.options.spoolDirectory);
|
|
16895
|
-
|
|
17408
|
+
fs18.appendFileSync(path25.join(this.options.spoolDirectory, `${safeFileStem(runId)}.ndjson`), lines, { mode: 384 });
|
|
16896
17409
|
this.spooled += bytes;
|
|
16897
17410
|
} catch (error) {
|
|
16898
17411
|
this.options.log.warn(`Run ${runId}: activity could not be spooled (${describeError(error)})`);
|
|
@@ -16909,7 +17422,7 @@ var ActivityChannel = class {
|
|
|
16909
17422
|
async replayFiles() {
|
|
16910
17423
|
for (const name of listSpool(this.options.spoolDirectory)) {
|
|
16911
17424
|
if (!this.online) return;
|
|
16912
|
-
const file =
|
|
17425
|
+
const file = path25.join(this.options.spoolDirectory, name);
|
|
16913
17426
|
const runId = name.slice(0, -".ndjson".length);
|
|
16914
17427
|
let events;
|
|
16915
17428
|
try {
|
|
@@ -16926,7 +17439,7 @@ var ActivityChannel = class {
|
|
|
16926
17439
|
}
|
|
16927
17440
|
}
|
|
16928
17441
|
if (!sentAll) return;
|
|
16929
|
-
|
|
17442
|
+
fs18.rmSync(file, { force: true });
|
|
16930
17443
|
}
|
|
16931
17444
|
this.spooled = spoolSize(this.options.spoolDirectory);
|
|
16932
17445
|
this.droppedReported = false;
|
|
@@ -16954,7 +17467,7 @@ function batchBytes(events) {
|
|
|
16954
17467
|
}
|
|
16955
17468
|
function listSpool(directory) {
|
|
16956
17469
|
try {
|
|
16957
|
-
return
|
|
17470
|
+
return fs18.readdirSync(directory).filter((name) => name.endsWith(".ndjson")).sort();
|
|
16958
17471
|
} catch {
|
|
16959
17472
|
return [];
|
|
16960
17473
|
}
|
|
@@ -16962,7 +17475,7 @@ function listSpool(directory) {
|
|
|
16962
17475
|
function spoolSize(directory) {
|
|
16963
17476
|
return listSpool(directory).reduce((total, name) => {
|
|
16964
17477
|
try {
|
|
16965
|
-
return total +
|
|
17478
|
+
return total + fs18.statSync(path25.join(directory, name)).size;
|
|
16966
17479
|
} catch {
|
|
16967
17480
|
return total;
|
|
16968
17481
|
}
|
|
@@ -16970,7 +17483,7 @@ function spoolSize(directory) {
|
|
|
16970
17483
|
}
|
|
16971
17484
|
function readSpool(file) {
|
|
16972
17485
|
const events = [];
|
|
16973
|
-
for (const line of
|
|
17486
|
+
for (const line of fs18.readFileSync(file, "utf8").split("\n")) {
|
|
16974
17487
|
if (!line) continue;
|
|
16975
17488
|
try {
|
|
16976
17489
|
events.push(JSON.parse(line));
|
|
@@ -20029,7 +20542,7 @@ var HubClient = class {
|
|
|
20029
20542
|
}
|
|
20030
20543
|
this.stateValue = HubState.Connected;
|
|
20031
20544
|
this.lastError = void 0;
|
|
20032
|
-
this.factsTold = { connection, facts: { harnesses: hello.harnesses, sleepPrevented: hello.sleepPrevented } };
|
|
20545
|
+
this.factsTold = { connection, facts: { harnesses: hello.harnesses, sleepPrevented: hello.sleepPrevented, tools: hello.tools } };
|
|
20033
20546
|
this.options.log.info(`Connected to Codesona as ${reply.machineName ?? reply.machineId} (${reply.agents?.length ?? 0} agent(s) bound)`);
|
|
20034
20547
|
await this.accept(connection, reply, credential);
|
|
20035
20548
|
const error = await closed;
|
|
@@ -20188,7 +20701,8 @@ var HubClient = class {
|
|
|
20188
20701
|
});
|
|
20189
20702
|
return {
|
|
20190
20703
|
...sameHarnesses(harnesses, told.facts.harnesses) ? {} : { harnesses },
|
|
20191
|
-
...now.sleepPrevented == null || now.sleepPrevented === told.facts.sleepPrevented ? {} : { sleepPrevented: now.sleepPrevented }
|
|
20704
|
+
...now.sleepPrevented == null || now.sleepPrevented === told.facts.sleepPrevented ? {} : { sleepPrevented: now.sleepPrevented },
|
|
20705
|
+
...now.tools == null || JSON.stringify(now.tools) === JSON.stringify(told.facts.tools ?? null) ? {} : { tools: now.tools }
|
|
20192
20706
|
};
|
|
20193
20707
|
}
|
|
20194
20708
|
scheduleRenewal(credential) {
|
|
@@ -20296,11 +20810,11 @@ var SignalRLog = class {
|
|
|
20296
20810
|
};
|
|
20297
20811
|
|
|
20298
20812
|
// src/runner/leftovers.ts
|
|
20299
|
-
import { execFile as
|
|
20300
|
-
import
|
|
20301
|
-
import
|
|
20813
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
20814
|
+
import fs19 from "node:fs";
|
|
20815
|
+
import path26 from "node:path";
|
|
20302
20816
|
var listProcesses = () => new Promise((resolve4) => {
|
|
20303
|
-
|
|
20817
|
+
execFile3("ps", ["-axww", "-o", "pid=,ppid=,pgid=,command="], { timeout: 5e3, maxBuffer: 16 * 1024 * 1024 }, (error, stdout) => {
|
|
20304
20818
|
if (error) {
|
|
20305
20819
|
resolve4([]);
|
|
20306
20820
|
return;
|
|
@@ -20335,7 +20849,7 @@ var LiveRuns = class {
|
|
|
20335
20849
|
/** Never throws: it is called on the way out of a run, and a run that does not get out holds the service's stop. */
|
|
20336
20850
|
remove(runId) {
|
|
20337
20851
|
try {
|
|
20338
|
-
|
|
20852
|
+
fs19.rmSync(this.file(runId), { force: true });
|
|
20339
20853
|
} catch (error) {
|
|
20340
20854
|
this.log.warn(`Run ${runId}: its state file could not be removed (${describeError(error)})`);
|
|
20341
20855
|
}
|
|
@@ -20351,11 +20865,11 @@ var LiveRuns = class {
|
|
|
20351
20865
|
const records = [];
|
|
20352
20866
|
for (const name of listJsonFiles(this.directory)) {
|
|
20353
20867
|
try {
|
|
20354
|
-
const record6 = readJsonFile(
|
|
20868
|
+
const record6 = readJsonFile(path26.join(this.directory, name));
|
|
20355
20869
|
if (record6?.runId) records.push(record6);
|
|
20356
20870
|
} catch {
|
|
20357
20871
|
}
|
|
20358
|
-
|
|
20872
|
+
fs19.rmSync(path26.join(this.directory, name), { force: true });
|
|
20359
20873
|
}
|
|
20360
20874
|
const withGroups = records.filter((record6) => Number.isSafeInteger(record6.processGroup) && record6.processGroup > 1);
|
|
20361
20875
|
if (withGroups.length === 0) return 0;
|
|
@@ -20378,13 +20892,13 @@ var LiveRuns = class {
|
|
|
20378
20892
|
}
|
|
20379
20893
|
}
|
|
20380
20894
|
file(runId) {
|
|
20381
|
-
return
|
|
20895
|
+
return path26.join(this.directory, `${safeFileStem(runId)}.json`);
|
|
20382
20896
|
}
|
|
20383
20897
|
};
|
|
20384
20898
|
|
|
20385
20899
|
// src/runner/outbox.ts
|
|
20386
|
-
import
|
|
20387
|
-
import
|
|
20900
|
+
import fs20 from "node:fs";
|
|
20901
|
+
import path27 from "node:path";
|
|
20388
20902
|
var OUTBOX_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
20389
20903
|
var END_BODY_MAX_BYTES = 1024 * 1024;
|
|
20390
20904
|
var END_BODY_RETRY_BYTES = 64 * 1024;
|
|
@@ -20446,26 +20960,26 @@ var Outbox = class {
|
|
|
20446
20960
|
list(log) {
|
|
20447
20961
|
const entries = [];
|
|
20448
20962
|
for (const name of listJsonFiles(this.directory)) {
|
|
20449
|
-
const file =
|
|
20963
|
+
const file = path27.join(this.directory, name);
|
|
20450
20964
|
try {
|
|
20451
20965
|
const entry = readJsonFile(file);
|
|
20452
20966
|
if (!entry?.end?.runId) throw new Error("no run id");
|
|
20453
20967
|
entries.push({ end: entry.end, recordedAt: Date.parse(entry.recordedAt) || 0 });
|
|
20454
20968
|
} catch (error) {
|
|
20455
20969
|
log.error(`Outbox entry ${name} cannot be read (${describeError(error)}); moved aside`);
|
|
20456
|
-
|
|
20970
|
+
fs20.renameSync(file, `${file}.unreadable`);
|
|
20457
20971
|
}
|
|
20458
20972
|
}
|
|
20459
20973
|
return entries.sort((a, b) => a.recordedAt - b.recordedAt);
|
|
20460
20974
|
}
|
|
20461
20975
|
has(runId) {
|
|
20462
|
-
return
|
|
20976
|
+
return fs20.existsSync(this.file(runId));
|
|
20463
20977
|
}
|
|
20464
20978
|
remove(runId) {
|
|
20465
|
-
|
|
20979
|
+
fs20.rmSync(this.file(runId), { force: true });
|
|
20466
20980
|
}
|
|
20467
20981
|
file(runId) {
|
|
20468
|
-
return
|
|
20982
|
+
return path27.join(this.directory, `${safeFileStem(runId)}.json`);
|
|
20469
20983
|
}
|
|
20470
20984
|
};
|
|
20471
20985
|
function deliveryOutcomeOf(result) {
|
|
@@ -20598,8 +21112,8 @@ var EndReporter = class {
|
|
|
20598
21112
|
};
|
|
20599
21113
|
|
|
20600
21114
|
// src/runner/supervisor.ts
|
|
20601
|
-
import
|
|
20602
|
-
import
|
|
21115
|
+
import fs21 from "node:fs";
|
|
21116
|
+
import path28 from "node:path";
|
|
20603
21117
|
|
|
20604
21118
|
// src/harness/claude-result.ts
|
|
20605
21119
|
var ZERO_USAGE = { costUsd: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0 };
|
|
@@ -20856,6 +21370,7 @@ var Supervisor = class {
|
|
|
20856
21370
|
constructor(options) {
|
|
20857
21371
|
this.options = options;
|
|
20858
21372
|
this.hubWatchdog = new HubWatchdog(() => this.hubLost(), options.hubOfflineLimitMs, options.clock);
|
|
21373
|
+
this.repos = options.repos ?? new RepoPreparer({ cacheDirectory: options.files.repos, git: gitWith(process.env), log: options.log });
|
|
20859
21374
|
}
|
|
20860
21375
|
options;
|
|
20861
21376
|
sessions = /* @__PURE__ */ new Map();
|
|
@@ -20867,6 +21382,7 @@ var Supervisor = class {
|
|
|
20867
21382
|
machinePaused = false;
|
|
20868
21383
|
updateRequired = false;
|
|
20869
21384
|
shuttingDown = false;
|
|
21385
|
+
repos;
|
|
20870
21386
|
// ── What the hub says ──
|
|
20871
21387
|
/** The machine's bindings, from Hello or AgentsChanged. Attachment is kept for an agent the hub still lists. */
|
|
20872
21388
|
setAgents(assignments) {
|
|
@@ -20946,6 +21462,11 @@ var Supervisor = class {
|
|
|
20946
21462
|
if (harness) return this.refuse(runId, harness.reason, harness.detail);
|
|
20947
21463
|
const bundle = await this.readBundle(runId, Math.max(0, deadline - Date.now()));
|
|
20948
21464
|
if ("reason" in bundle) return this.refuse(runId, bundle.reason, bundle.detail);
|
|
21465
|
+
const repos = bundle.bundle.prepare?.repos ?? [];
|
|
21466
|
+
if (repos.length > 0) {
|
|
21467
|
+
const unreachable = await this.repoRefusal(repos, Math.max(0, deadline - Date.now()));
|
|
21468
|
+
if (unreachable) return this.refuse(runId, StartRefusal.RepoUnavailable, unreachable);
|
|
21469
|
+
}
|
|
20949
21470
|
const refusal = this.stateRefusal(message);
|
|
20950
21471
|
if (refusal) return this.refuse(runId, refusal);
|
|
20951
21472
|
const session = new RunSession(this, message);
|
|
@@ -21013,6 +21534,14 @@ var Supervisor = class {
|
|
|
21013
21534
|
});
|
|
21014
21535
|
return Promise.race([reading, late]).finally(() => clearTimeout(timer));
|
|
21015
21536
|
}
|
|
21537
|
+
/** A sentence naming a repository this machine's git cannot reach, within `withinMs`; undefined when it reaches them all. */
|
|
21538
|
+
async repoRefusal(repos, withinMs) {
|
|
21539
|
+
try {
|
|
21540
|
+
return await this.repos.checkAccess(repos, withinMs);
|
|
21541
|
+
} catch (error) {
|
|
21542
|
+
return `This Mac could not check its access to the run's repositories: ${describeError(error)}`;
|
|
21543
|
+
}
|
|
21544
|
+
}
|
|
21016
21545
|
stopSession(message, why = "Codesona stopped the run") {
|
|
21017
21546
|
const session = this.sessions.get(message?.runId);
|
|
21018
21547
|
if (!session) return { known: this.ended.has(message?.runId) };
|
|
@@ -21128,6 +21657,8 @@ var RunSession = class {
|
|
|
21128
21657
|
turnEnded;
|
|
21129
21658
|
cwd;
|
|
21130
21659
|
over = false;
|
|
21660
|
+
/** Stops the git that prepares the run's repositories, when the run is stopped before its session opens. */
|
|
21661
|
+
preparing = new AbortController();
|
|
21131
21662
|
done;
|
|
21132
21663
|
resolveDone;
|
|
21133
21664
|
get options() {
|
|
@@ -21154,6 +21685,7 @@ var RunSession = class {
|
|
|
21154
21685
|
if (this.stop) return;
|
|
21155
21686
|
this.stop = stop;
|
|
21156
21687
|
this.options.log.info(`Run ${this.runId}: stopping (${why})`);
|
|
21688
|
+
this.preparing.abort();
|
|
21157
21689
|
void this.stopDriver();
|
|
21158
21690
|
}
|
|
21159
21691
|
async reportStarted() {
|
|
@@ -21182,7 +21714,7 @@ var RunSession = class {
|
|
|
21182
21714
|
async execute(bundle) {
|
|
21183
21715
|
if (this.stop) return this.stoppedBeforeStart();
|
|
21184
21716
|
const route = this.options.proxy.register(this.runId, this.ticket, (refusal) => this.requestStop(gatewayStop(refusal), `the gateway answered ${refusal}`));
|
|
21185
|
-
const cwd =
|
|
21717
|
+
const cwd = path28.join(this.options.files.agents, safeFileStem(this.agentId));
|
|
21186
21718
|
this.cwd = cwd;
|
|
21187
21719
|
let workspace;
|
|
21188
21720
|
try {
|
|
@@ -21190,6 +21722,18 @@ var RunSession = class {
|
|
|
21190
21722
|
} catch (error) {
|
|
21191
21723
|
return this.endOf({ launchError: `The session's files could not be written: ${describeError(error)}` });
|
|
21192
21724
|
}
|
|
21725
|
+
const repos = bundle.bundle.prepare?.repos ?? [];
|
|
21726
|
+
if (repos.length > 0) {
|
|
21727
|
+
try {
|
|
21728
|
+
await this.supervisor.repos.prepare(cwd, repos, { signal: this.preparing.signal, gitEnvironment: sessionGitEnvironment(bundle.bundle.env) });
|
|
21729
|
+
this.options.log.info(`Run ${this.runId}: ${repos.length} repositor${repos.length === 1 ? "y" : "ies"} prepared under ${path28.join(cwd, "repos")}`);
|
|
21730
|
+
} catch (error) {
|
|
21731
|
+
if (this.stop || error instanceof PrepareAborted) return this.stoppedBeforeStart();
|
|
21732
|
+
const why = error instanceof PrepareError ? error.message : `git failed: ${describeError(error)}`;
|
|
21733
|
+
return this.endOf({ launchError: `The repositories could not be prepared: ${why}`, launchErrorKind: LaunchErrorKind.PrepareFailed });
|
|
21734
|
+
}
|
|
21735
|
+
if (this.stop) return this.stoppedBeforeStart();
|
|
21736
|
+
}
|
|
21193
21737
|
let resume = bundle.resume && this.transcriptExists(cwd, bundle.sessionId);
|
|
21194
21738
|
if (bundle.resume && !resume) this.options.log.info(`Run ${this.runId}: no transcript of session ${bundle.sessionId} on this machine; starting it as a new conversation`);
|
|
21195
21739
|
this.options.liveRuns.record({
|
|
@@ -21216,10 +21760,10 @@ var RunSession = class {
|
|
|
21216
21760
|
transcriptExists(cwd, sessionId) {
|
|
21217
21761
|
const candidates = /* @__PURE__ */ new Set([cwd]);
|
|
21218
21762
|
try {
|
|
21219
|
-
candidates.add(
|
|
21763
|
+
candidates.add(fs21.realpathSync(cwd));
|
|
21220
21764
|
} catch {
|
|
21221
21765
|
}
|
|
21222
|
-
return [...candidates].some((directory) =>
|
|
21766
|
+
return [...candidates].some((directory) => fs21.existsSync(claudeTranscriptPath(this.options.claudeConfigDir, directory, sessionId)));
|
|
21223
21767
|
}
|
|
21224
21768
|
async attempt(number3, bundle, workspace, resume) {
|
|
21225
21769
|
const reader = new ClaudeResultReader();
|
|
@@ -21430,6 +21974,7 @@ var RunSession = class {
|
|
|
21430
21974
|
harnessSessionId: this.harnessSessionId ?? null,
|
|
21431
21975
|
stopReason: parts.stopReason ?? null,
|
|
21432
21976
|
launchError: parts.launchError ?? null,
|
|
21977
|
+
launchErrorKind: parts.launchError ? parts.launchErrorKind ?? null : null,
|
|
21433
21978
|
result
|
|
21434
21979
|
};
|
|
21435
21980
|
}
|
|
@@ -21448,7 +21993,7 @@ function sleep2(ms) {
|
|
|
21448
21993
|
}
|
|
21449
21994
|
|
|
21450
21995
|
// src/runner/usage.ts
|
|
21451
|
-
import
|
|
21996
|
+
import path29 from "node:path";
|
|
21452
21997
|
var SessionUsageLedger = class {
|
|
21453
21998
|
constructor(directory, log) {
|
|
21454
21999
|
this.directory = directory;
|
|
@@ -21480,7 +22025,7 @@ var SessionUsageLedger = class {
|
|
|
21480
22025
|
`);
|
|
21481
22026
|
}
|
|
21482
22027
|
file(sessionId) {
|
|
21483
|
-
return
|
|
22028
|
+
return path29.join(this.directory, `${safeFileStem(sessionId)}.json`);
|
|
21484
22029
|
}
|
|
21485
22030
|
};
|
|
21486
22031
|
function number2(value) {
|
|
@@ -21519,7 +22064,8 @@ function sleep3(ms) {
|
|
|
21519
22064
|
function setupFacts(harnesses, setup) {
|
|
21520
22065
|
return {
|
|
21521
22066
|
harnesses: harnesses.map((found) => found.harness === Harness.ClaudeCode && setup.claudeLogin ? { ...found, loggedIn: setup.claudeLogin.loggedIn } : found),
|
|
21522
|
-
...setup.sleepPrevented === void 0 ? {} : { sleepPrevented: setup.sleepPrevented }
|
|
22067
|
+
...setup.sleepPrevented === void 0 ? {} : { sleepPrevented: setup.sleepPrevented },
|
|
22068
|
+
...setup.tools === void 0 ? {} : { tools: setup.tools }
|
|
21523
22069
|
};
|
|
21524
22070
|
}
|
|
21525
22071
|
function until(work, deadline) {
|
|
@@ -21570,6 +22116,8 @@ var RunnerService = class _RunnerService {
|
|
|
21570
22116
|
this.supervisor = new Supervisor({
|
|
21571
22117
|
files: this.files,
|
|
21572
22118
|
host,
|
|
22119
|
+
// The owner's git, with their credential helper: the sessions' environment, as the session's own git has it.
|
|
22120
|
+
repos: new RepoPreparer({ cacheDirectory: this.files.repos, git: gitWith(options.environment ?? process.env), log }),
|
|
21573
22121
|
sdk: createHarnessSdk(host),
|
|
21574
22122
|
proxy: this.proxy,
|
|
21575
22123
|
fetchBundle: async (runId) => {
|
|
@@ -21850,8 +22398,9 @@ var RunnerService = class _RunnerService {
|
|
|
21850
22398
|
}
|
|
21851
22399
|
}
|
|
21852
22400
|
/**
|
|
21853
|
-
* Claude Code's login, as the sessions' environment runs `claude auth status` (the CLI the sessions run),
|
|
21854
|
-
*
|
|
22401
|
+
* Claude Code's login, as the sessions' environment runs `claude auth status` (the CLI the sessions run), `pmset -g`, and
|
|
22402
|
+
* `git --version`, `dotnet --list-sdks` and `node --version` in that environment; looked at again after ten minutes, as
|
|
22403
|
+
* the CLIs are. Neither holds Hello up for long, and neither fails it.
|
|
21855
22404
|
*/
|
|
21856
22405
|
machineSetup() {
|
|
21857
22406
|
if (this.setup && Date.now() - this.setup.at < this.factsTtlMs) return Promise.resolve(this.setup.value);
|
|
@@ -21861,11 +22410,12 @@ var RunnerService = class _RunnerService {
|
|
|
21861
22410
|
async lookAtSetup() {
|
|
21862
22411
|
const environment = this.options.environment ?? process.env;
|
|
21863
22412
|
const login = this.options.claudeLogin ?? (() => readClaudeLogin((file, args) => runCommand(file, args, { env: agentEnvironment(environment), timeoutMs: CLAUDE_LOGIN_TIMEOUT_MS }), this.options.claudeCommand));
|
|
21864
|
-
const [claudeLogin, power] = await Promise.all([
|
|
22413
|
+
const [claudeLogin, power, tools] = await Promise.all([
|
|
21865
22414
|
login().catch(() => void 0),
|
|
21866
|
-
(this.options.powerSettings ?? (() => readPowerSettings()))().catch(() => void 0)
|
|
22415
|
+
(this.options.powerSettings ?? (() => readPowerSettings()))().catch(() => void 0),
|
|
22416
|
+
(this.options.machineTools ?? (() => readMachineTools(environment)))().catch(() => void 0)
|
|
21867
22417
|
]);
|
|
21868
|
-
const value = { claudeLogin, sleepPrevented: power === void 0 ? void 0 : sleepPrevented(power) };
|
|
22418
|
+
const value = { claudeLogin, sleepPrevented: power === void 0 ? void 0 : sleepPrevented(power), tools };
|
|
21869
22419
|
this.setup = { at: Date.now(), value };
|
|
21870
22420
|
return value;
|
|
21871
22421
|
}
|
|
@@ -21929,7 +22479,7 @@ var RunnerService = class _RunnerService {
|
|
|
21929
22479
|
|
|
21930
22480
|
// src/runner/run.ts
|
|
21931
22481
|
async function runCommand2(args, io, env = process.env) {
|
|
21932
|
-
const { values } =
|
|
22482
|
+
const { values } = parseArgs6({ args, options: { verbose: { type: "boolean" }, service: { type: "boolean" } }, strict: true, allowPositionals: false });
|
|
21933
22483
|
const home = runnerHome(env);
|
|
21934
22484
|
const asService = values.service === true;
|
|
21935
22485
|
let logFile;
|
|
@@ -21977,7 +22527,7 @@ async function serve(home, io, log, asService) {
|
|
|
21977
22527
|
|
|
21978
22528
|
// src/runner/start.ts
|
|
21979
22529
|
import os7 from "node:os";
|
|
21980
|
-
import { parseArgs as
|
|
22530
|
+
import { parseArgs as parseArgs7 } from "node:util";
|
|
21981
22531
|
|
|
21982
22532
|
// src/runner/pair.ts
|
|
21983
22533
|
async function pairMachine(store, client, input) {
|
|
@@ -22064,7 +22614,7 @@ function machineKeyKind(key) {
|
|
|
22064
22614
|
return KEY_PREFIXES[key.slice(0, 4)];
|
|
22065
22615
|
}
|
|
22066
22616
|
function parseStart(args) {
|
|
22067
|
-
const { values } =
|
|
22617
|
+
const { values } = parseArgs7({
|
|
22068
22618
|
args,
|
|
22069
22619
|
options: {
|
|
22070
22620
|
"machine-key": { type: "string" },
|
|
@@ -22176,6 +22726,7 @@ var Command = {
|
|
|
22176
22726
|
Doctor: "doctor",
|
|
22177
22727
|
Status: "status",
|
|
22178
22728
|
Restart: "restart",
|
|
22729
|
+
Prune: "prune",
|
|
22179
22730
|
Logout: "logout"
|
|
22180
22731
|
};
|
|
22181
22732
|
var COMMANDS = {
|
|
@@ -22184,6 +22735,7 @@ var COMMANDS = {
|
|
|
22184
22735
|
[Command.Doctor]: doctorCommand,
|
|
22185
22736
|
[Command.Status]: statusCommand,
|
|
22186
22737
|
[Command.Restart]: restartCommand,
|
|
22738
|
+
[Command.Prune]: (args, io) => pruneCommand(args, io),
|
|
22187
22739
|
[Command.Logout]: logoutCommand
|
|
22188
22740
|
};
|
|
22189
22741
|
var HELP = `Codesona Runner ${runnerVersion}
|
|
@@ -22202,6 +22754,9 @@ Commands:
|
|
|
22202
22754
|
restart the background service, so it connects anew and runs
|
|
22203
22755
|
the Codesona Runner installed now; not while it runs sessions
|
|
22204
22756
|
(--force: restart it all the same, ending them)
|
|
22757
|
+
prune remove the agents' repository worktrees and local task
|
|
22758
|
+
branches untouched for 14 days that origin already has;
|
|
22759
|
+
never unpushed work
|
|
22205
22760
|
logout [--local]
|
|
22206
22761
|
disconnect this machine from Codesona and remove the service
|
|
22207
22762
|
(--local: remove this machine's side even if Codesona
|