@factiii/runner 0.2.1 → 0.6.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/dist/cli.js +634 -227
- package/package.json +4 -2
package/dist/cli.js
CHANGED
|
@@ -963,8 +963,8 @@ var require_command = __commonJS({
|
|
|
963
963
|
"node_modules/commander/lib/command.js"(exports2) {
|
|
964
964
|
var EventEmitter = require("node:events").EventEmitter;
|
|
965
965
|
var childProcess = require("node:child_process");
|
|
966
|
-
var
|
|
967
|
-
var
|
|
966
|
+
var path8 = require("node:path");
|
|
967
|
+
var fs7 = require("node:fs");
|
|
968
968
|
var process2 = require("node:process");
|
|
969
969
|
var { Argument: Argument2, humanReadableArgName } = require_argument();
|
|
970
970
|
var { CommanderError: CommanderError2 } = require_error();
|
|
@@ -1896,11 +1896,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
1896
1896
|
let launchWithNode = false;
|
|
1897
1897
|
const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
|
|
1898
1898
|
function findFile(baseDir, baseName) {
|
|
1899
|
-
const localBin =
|
|
1900
|
-
if (
|
|
1901
|
-
if (sourceExt.includes(
|
|
1899
|
+
const localBin = path8.resolve(baseDir, baseName);
|
|
1900
|
+
if (fs7.existsSync(localBin)) return localBin;
|
|
1901
|
+
if (sourceExt.includes(path8.extname(baseName))) return void 0;
|
|
1902
1902
|
const foundExt = sourceExt.find(
|
|
1903
|
-
(ext) =>
|
|
1903
|
+
(ext) => fs7.existsSync(`${localBin}${ext}`)
|
|
1904
1904
|
);
|
|
1905
1905
|
if (foundExt) return `${localBin}${foundExt}`;
|
|
1906
1906
|
return void 0;
|
|
@@ -1912,21 +1912,21 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
1912
1912
|
if (this._scriptPath) {
|
|
1913
1913
|
let resolvedScriptPath;
|
|
1914
1914
|
try {
|
|
1915
|
-
resolvedScriptPath =
|
|
1915
|
+
resolvedScriptPath = fs7.realpathSync(this._scriptPath);
|
|
1916
1916
|
} catch (err) {
|
|
1917
1917
|
resolvedScriptPath = this._scriptPath;
|
|
1918
1918
|
}
|
|
1919
|
-
executableDir =
|
|
1920
|
-
|
|
1919
|
+
executableDir = path8.resolve(
|
|
1920
|
+
path8.dirname(resolvedScriptPath),
|
|
1921
1921
|
executableDir
|
|
1922
1922
|
);
|
|
1923
1923
|
}
|
|
1924
1924
|
if (executableDir) {
|
|
1925
1925
|
let localFile = findFile(executableDir, executableFile);
|
|
1926
1926
|
if (!localFile && !subcommand._executableFile && this._scriptPath) {
|
|
1927
|
-
const legacyName =
|
|
1927
|
+
const legacyName = path8.basename(
|
|
1928
1928
|
this._scriptPath,
|
|
1929
|
-
|
|
1929
|
+
path8.extname(this._scriptPath)
|
|
1930
1930
|
);
|
|
1931
1931
|
if (legacyName !== this._name) {
|
|
1932
1932
|
localFile = findFile(
|
|
@@ -1937,7 +1937,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
1937
1937
|
}
|
|
1938
1938
|
executableFile = localFile || executableFile;
|
|
1939
1939
|
}
|
|
1940
|
-
launchWithNode = sourceExt.includes(
|
|
1940
|
+
launchWithNode = sourceExt.includes(path8.extname(executableFile));
|
|
1941
1941
|
let proc;
|
|
1942
1942
|
if (process2.platform !== "win32") {
|
|
1943
1943
|
if (launchWithNode) {
|
|
@@ -2777,7 +2777,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2777
2777
|
* @return {Command}
|
|
2778
2778
|
*/
|
|
2779
2779
|
nameFromFilename(filename) {
|
|
2780
|
-
this._name =
|
|
2780
|
+
this._name = path8.basename(filename, path8.extname(filename));
|
|
2781
2781
|
return this;
|
|
2782
2782
|
}
|
|
2783
2783
|
/**
|
|
@@ -2791,9 +2791,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2791
2791
|
* @param {string} [path]
|
|
2792
2792
|
* @return {(string|null|Command)}
|
|
2793
2793
|
*/
|
|
2794
|
-
executableDir(
|
|
2795
|
-
if (
|
|
2796
|
-
this._executableDir =
|
|
2794
|
+
executableDir(path9) {
|
|
2795
|
+
if (path9 === void 0) return this._executableDir;
|
|
2796
|
+
this._executableDir = path9;
|
|
2797
2797
|
return this;
|
|
2798
2798
|
}
|
|
2799
2799
|
/**
|
|
@@ -3026,7 +3026,7 @@ var require_commander = __commonJS({
|
|
|
3026
3026
|
// ../../node_modules/xmlhttprequest-ssl/lib/XMLHttpRequest.js
|
|
3027
3027
|
var require_XMLHttpRequest = __commonJS({
|
|
3028
3028
|
"../../node_modules/xmlhttprequest-ssl/lib/XMLHttpRequest.js"(exports2, module2) {
|
|
3029
|
-
var
|
|
3029
|
+
var fs7 = require("fs");
|
|
3030
3030
|
var Url = require("url");
|
|
3031
3031
|
var spawn3 = require("child_process").spawn;
|
|
3032
3032
|
module2.exports = XMLHttpRequest3;
|
|
@@ -3184,7 +3184,7 @@ var require_XMLHttpRequest = __commonJS({
|
|
|
3184
3184
|
throw new Error("XMLHttpRequest: Only GET method is supported");
|
|
3185
3185
|
}
|
|
3186
3186
|
if (settings.async) {
|
|
3187
|
-
|
|
3187
|
+
fs7.readFile(unescape(url2.pathname), function(error, data2) {
|
|
3188
3188
|
if (error) {
|
|
3189
3189
|
self.handleError(error, error.errno || -1);
|
|
3190
3190
|
} else {
|
|
@@ -3196,7 +3196,7 @@ var require_XMLHttpRequest = __commonJS({
|
|
|
3196
3196
|
});
|
|
3197
3197
|
} else {
|
|
3198
3198
|
try {
|
|
3199
|
-
this.response =
|
|
3199
|
+
this.response = fs7.readFileSync(unescape(url2.pathname));
|
|
3200
3200
|
this.responseText = this.response.toString("utf8");
|
|
3201
3201
|
this.status = 200;
|
|
3202
3202
|
setState(self.DONE);
|
|
@@ -3322,15 +3322,15 @@ var require_XMLHttpRequest = __commonJS({
|
|
|
3322
3322
|
} else {
|
|
3323
3323
|
var contentFile = ".node-xmlhttprequest-content-" + process.pid;
|
|
3324
3324
|
var syncFile = ".node-xmlhttprequest-sync-" + process.pid;
|
|
3325
|
-
|
|
3325
|
+
fs7.writeFileSync(syncFile, "", "utf8");
|
|
3326
3326
|
var execString = "var http = require('http'), https = require('https'), fs = require('fs');var doRequest = http" + (ssl ? "s" : "") + ".request;var options = " + JSON.stringify(options) + ";var responseText = '';var responseData = Buffer.alloc(0);var req = doRequest(options, function(response) {response.on('data', function(chunk) { var data = Buffer.from(chunk); responseText += data.toString('utf8'); responseData = Buffer.concat([responseData, data]);});response.on('end', function() {fs.writeFileSync('" + contentFile + "', JSON.stringify({err: null, data: {statusCode: response.statusCode, headers: response.headers, text: responseText, data: responseData.toString('base64')}}), 'utf8');fs.unlinkSync('" + syncFile + "');});response.on('error', function(error) {fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');fs.unlinkSync('" + syncFile + "');});}).on('error', function(error) {fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');fs.unlinkSync('" + syncFile + "');});" + (data ? "req.write('" + JSON.stringify(data).slice(1, -1).replace(/'/g, "\\'") + "');" : "") + "req.end();";
|
|
3327
3327
|
var syncProc = spawn3(process.argv[0], ["-e", execString]);
|
|
3328
3328
|
var statusText;
|
|
3329
|
-
while (
|
|
3329
|
+
while (fs7.existsSync(syncFile)) {
|
|
3330
3330
|
}
|
|
3331
|
-
self.responseText =
|
|
3331
|
+
self.responseText = fs7.readFileSync(contentFile, "utf8");
|
|
3332
3332
|
syncProc.stdin.end();
|
|
3333
|
-
|
|
3333
|
+
fs7.unlinkSync(contentFile);
|
|
3334
3334
|
if (self.responseText.match(/^NODE-XMLHTTPREQUEST-ERROR:/)) {
|
|
3335
3335
|
var errorObj = JSON.parse(self.responseText.replace(/^NODE-XMLHTTPREQUEST-ERROR:/, ""));
|
|
3336
3336
|
self.handleError(errorObj, 503);
|
|
@@ -8037,8 +8037,144 @@ function configExists() {
|
|
|
8037
8037
|
return import_fs.default.existsSync(CONFIG_FILE);
|
|
8038
8038
|
}
|
|
8039
8039
|
|
|
8040
|
-
//
|
|
8040
|
+
// src/setup.ts
|
|
8041
8041
|
var import_child_process2 = require("child_process");
|
|
8042
|
+
var import_crypto = __toESM(require("crypto"));
|
|
8043
|
+
var import_fs2 = __toESM(require("fs"));
|
|
8044
|
+
var import_path2 = __toESM(require("path"));
|
|
8045
|
+
var DOCKERFILE_LABEL = "factiii.dockerfile-sha";
|
|
8046
|
+
function run(bin, args, opts) {
|
|
8047
|
+
try {
|
|
8048
|
+
return (0, import_child_process2.execFileSync)(bin, args, {
|
|
8049
|
+
encoding: "utf-8",
|
|
8050
|
+
stdio: opts?.silent ? "pipe" : "inherit",
|
|
8051
|
+
// Avoids a cmd.exe window flash on Windows during silent checks.
|
|
8052
|
+
windowsHide: true
|
|
8053
|
+
});
|
|
8054
|
+
} catch {
|
|
8055
|
+
return "";
|
|
8056
|
+
}
|
|
8057
|
+
}
|
|
8058
|
+
function check(cmd) {
|
|
8059
|
+
const lookup2 = process.platform === "win32" ? "where" : "which";
|
|
8060
|
+
try {
|
|
8061
|
+
(0, import_child_process2.execFileSync)(lookup2, [cmd], { stdio: "pipe", windowsHide: true });
|
|
8062
|
+
return true;
|
|
8063
|
+
} catch {
|
|
8064
|
+
return false;
|
|
8065
|
+
}
|
|
8066
|
+
}
|
|
8067
|
+
async function setup(serverUrl) {
|
|
8068
|
+
console.log("\n=== Factiii Runner \u2014 Setup ===\n");
|
|
8069
|
+
console.log("[1/3] Checking Docker...");
|
|
8070
|
+
if (!check("docker")) {
|
|
8071
|
+
console.error(
|
|
8072
|
+
" Docker is not installed. Install it from https://docs.docker.com/get-docker/ and re-run setup."
|
|
8073
|
+
);
|
|
8074
|
+
process.exit(1);
|
|
8075
|
+
}
|
|
8076
|
+
if (!run("docker", ["info"], { silent: true })) {
|
|
8077
|
+
console.error(
|
|
8078
|
+
" Docker is installed but the daemon is not running. Start Docker and re-run setup."
|
|
8079
|
+
);
|
|
8080
|
+
process.exit(1);
|
|
8081
|
+
}
|
|
8082
|
+
console.log(" Docker is ready.");
|
|
8083
|
+
console.log("\n[2/3] Building factiii-claude Docker image...");
|
|
8084
|
+
const dockerfilePath = findDockerfile();
|
|
8085
|
+
if (!dockerfilePath) {
|
|
8086
|
+
console.error(
|
|
8087
|
+
" Dockerfile.claude not found. Reinstall the package and re-run setup."
|
|
8088
|
+
);
|
|
8089
|
+
process.exit(1);
|
|
8090
|
+
}
|
|
8091
|
+
const wantedHash = import_crypto.default.createHash("sha256").update(import_fs2.default.readFileSync(dockerfilePath)).digest("hex");
|
|
8092
|
+
const existingHash = run(
|
|
8093
|
+
"docker",
|
|
8094
|
+
[
|
|
8095
|
+
"image",
|
|
8096
|
+
"inspect",
|
|
8097
|
+
"factiii-claude",
|
|
8098
|
+
"--format",
|
|
8099
|
+
`{{ index .Config.Labels "${DOCKERFILE_LABEL}" }}`
|
|
8100
|
+
],
|
|
8101
|
+
{ silent: true }
|
|
8102
|
+
).trim();
|
|
8103
|
+
if (existingHash === wantedHash) {
|
|
8104
|
+
console.log(" Image is up to date.");
|
|
8105
|
+
} else {
|
|
8106
|
+
console.log(
|
|
8107
|
+
existingHash ? ` Dockerfile changed since last build, rebuilding from ${dockerfilePath}...` : ` Building from ${dockerfilePath}...`
|
|
8108
|
+
);
|
|
8109
|
+
run("docker", [
|
|
8110
|
+
"build",
|
|
8111
|
+
"-t",
|
|
8112
|
+
"factiii-claude",
|
|
8113
|
+
"-f",
|
|
8114
|
+
dockerfilePath,
|
|
8115
|
+
"--label",
|
|
8116
|
+
`${DOCKERFILE_LABEL}=${wantedHash}`,
|
|
8117
|
+
"."
|
|
8118
|
+
]);
|
|
8119
|
+
console.log(" Image built.");
|
|
8120
|
+
}
|
|
8121
|
+
run("docker", ["volume", "create", "factiii-card-cache"], { silent: true });
|
|
8122
|
+
console.log("\n[3/3] Pairing this runner with a Factiii account...");
|
|
8123
|
+
const { token, runnerName } = await pairWithBrowser(serverUrl);
|
|
8124
|
+
writeRunnerConfig({ serverUrl, authToken: token, runnerName });
|
|
8125
|
+
console.log(" Paired and config saved.");
|
|
8126
|
+
console.log("\n=== Setup complete ===\n");
|
|
8127
|
+
console.log("Start the runner with: factiii-runner start\n");
|
|
8128
|
+
}
|
|
8129
|
+
function findDockerfile() {
|
|
8130
|
+
const p = import_path2.default.join(__dirname, "..", "Dockerfile.claude");
|
|
8131
|
+
return import_fs2.default.existsSync(p) ? p : null;
|
|
8132
|
+
}
|
|
8133
|
+
|
|
8134
|
+
// src/selfUpdate.ts
|
|
8135
|
+
var import_node_child_process = require("node:child_process");
|
|
8136
|
+
var import_node_fs = __toESM(require("node:fs"));
|
|
8137
|
+
var import_node_path = __toESM(require("node:path"));
|
|
8138
|
+
var PACKAGE_NAME = "@factiii/runner";
|
|
8139
|
+
var RESTART_FOR_UPDATE = 75;
|
|
8140
|
+
var WORKER_ENV = "FACTIII_RUNNER_WORKER";
|
|
8141
|
+
function markWorkerEnv(env) {
|
|
8142
|
+
return { ...env, [WORKER_ENV]: "1" };
|
|
8143
|
+
}
|
|
8144
|
+
function isWorker() {
|
|
8145
|
+
return process.env[WORKER_ENV] === "1";
|
|
8146
|
+
}
|
|
8147
|
+
function currentVersion() {
|
|
8148
|
+
try {
|
|
8149
|
+
const pkg = JSON.parse(
|
|
8150
|
+
import_node_fs.default.readFileSync(import_node_path.default.join(__dirname, "..", "package.json"), "utf-8")
|
|
8151
|
+
);
|
|
8152
|
+
return pkg.version ?? "0.0.0";
|
|
8153
|
+
} catch {
|
|
8154
|
+
return "0.0.0";
|
|
8155
|
+
}
|
|
8156
|
+
}
|
|
8157
|
+
function installLatest() {
|
|
8158
|
+
console.log(`[update] installing ${PACKAGE_NAME}@latest...`);
|
|
8159
|
+
const res = (0, import_node_child_process.spawnSync)("npm", ["install", "-g", `${PACKAGE_NAME}@latest`], {
|
|
8160
|
+
stdio: "inherit",
|
|
8161
|
+
// On Windows npm is npm.cmd, which spawnSync can't exec without a shell.
|
|
8162
|
+
shell: process.platform === "win32"
|
|
8163
|
+
});
|
|
8164
|
+
if (res.status !== 0) {
|
|
8165
|
+
console.error(
|
|
8166
|
+
`[update] npm install failed (exit ${res.status ?? "signal"}). Staying on ${currentVersion()}.`
|
|
8167
|
+
);
|
|
8168
|
+
return false;
|
|
8169
|
+
}
|
|
8170
|
+
return true;
|
|
8171
|
+
}
|
|
8172
|
+
|
|
8173
|
+
// src/supervisor.ts
|
|
8174
|
+
var import_node_child_process2 = require("node:child_process");
|
|
8175
|
+
|
|
8176
|
+
// ../../shared/all/helpers/board-agent-core/claude.ts
|
|
8177
|
+
var import_child_process3 = require("child_process");
|
|
8042
8178
|
|
|
8043
8179
|
// ../../shared/all/helpers/board-agent-core/claude-stream.ts
|
|
8044
8180
|
function truncate(s, max) {
|
|
@@ -8514,7 +8650,7 @@ function spawnClaude(opts) {
|
|
|
8514
8650
|
if (opts.resumeSessionId) {
|
|
8515
8651
|
args.push("--resume", opts.resumeSessionId);
|
|
8516
8652
|
}
|
|
8517
|
-
const proc = (0,
|
|
8653
|
+
const proc = (0, import_child_process3.spawn)("docker", args);
|
|
8518
8654
|
const result = new Promise(
|
|
8519
8655
|
(resolve, reject) => {
|
|
8520
8656
|
let errorOutput = "";
|
|
@@ -8546,7 +8682,7 @@ function spawnClaude(opts) {
|
|
|
8546
8682
|
}
|
|
8547
8683
|
function generateCommitMessage(containerName, diff) {
|
|
8548
8684
|
return new Promise((resolve) => {
|
|
8549
|
-
const proc = (0,
|
|
8685
|
+
const proc = (0, import_child_process3.spawn)("docker", [
|
|
8550
8686
|
"exec",
|
|
8551
8687
|
"-i",
|
|
8552
8688
|
"--user",
|
|
@@ -8575,16 +8711,16 @@ ${diff.slice(0, 8e3)}`);
|
|
|
8575
8711
|
}
|
|
8576
8712
|
|
|
8577
8713
|
// ../../shared/all/helpers/board-agent-core/docker.ts
|
|
8578
|
-
var
|
|
8579
|
-
var
|
|
8714
|
+
var import_child_process4 = require("child_process");
|
|
8715
|
+
var import_crypto2 = __toESM(require("crypto"));
|
|
8580
8716
|
var import_promises = __toESM(require("fs/promises"));
|
|
8581
8717
|
var import_net = __toESM(require("net"));
|
|
8582
8718
|
var import_util = require("util");
|
|
8583
|
-
var execFileAsync = (0, import_util.promisify)(
|
|
8584
|
-
var
|
|
8719
|
+
var execFileAsync = (0, import_util.promisify)(import_child_process4.execFile);
|
|
8720
|
+
var DOCKERFILE_LABEL2 = "factiii.dockerfile-sha";
|
|
8585
8721
|
async function hashDockerfile(dockerfilePath) {
|
|
8586
8722
|
const buf = await import_promises.default.readFile(dockerfilePath);
|
|
8587
|
-
return
|
|
8723
|
+
return import_crypto2.default.createHash("sha256").update(buf).digest("hex");
|
|
8588
8724
|
}
|
|
8589
8725
|
async function getFreePort() {
|
|
8590
8726
|
return new Promise((resolve, reject) => {
|
|
@@ -8623,7 +8759,7 @@ async function ensureDockerImage(dockerfilePath, onLog) {
|
|
|
8623
8759
|
"inspect",
|
|
8624
8760
|
"factiii-claude",
|
|
8625
8761
|
"--format",
|
|
8626
|
-
`{{ index .Config.Labels "${
|
|
8762
|
+
`{{ index .Config.Labels "${DOCKERFILE_LABEL2}" }}`
|
|
8627
8763
|
],
|
|
8628
8764
|
{}
|
|
8629
8765
|
);
|
|
@@ -8649,7 +8785,7 @@ async function resetNixVolume() {
|
|
|
8649
8785
|
}
|
|
8650
8786
|
function buildImageStreaming(dockerfilePath, wantedHash, onLog) {
|
|
8651
8787
|
return new Promise((resolve, reject) => {
|
|
8652
|
-
const child = (0,
|
|
8788
|
+
const child = (0, import_child_process4.spawn)(
|
|
8653
8789
|
"docker",
|
|
8654
8790
|
[
|
|
8655
8791
|
"build",
|
|
@@ -8659,7 +8795,7 @@ function buildImageStreaming(dockerfilePath, wantedHash, onLog) {
|
|
|
8659
8795
|
"-f",
|
|
8660
8796
|
dockerfilePath,
|
|
8661
8797
|
"--label",
|
|
8662
|
-
`${
|
|
8798
|
+
`${DOCKERFILE_LABEL2}=${wantedHash}`,
|
|
8663
8799
|
"."
|
|
8664
8800
|
],
|
|
8665
8801
|
{ env: { ...process.env, DOCKER_BUILDKIT: "1" } }
|
|
@@ -8718,6 +8854,44 @@ async function killContainer(containerName) {
|
|
|
8718
8854
|
} catch {
|
|
8719
8855
|
}
|
|
8720
8856
|
}
|
|
8857
|
+
async function listContainersByPrefix(namePrefix) {
|
|
8858
|
+
try {
|
|
8859
|
+
const { stdout } = await execFileAsync(
|
|
8860
|
+
"docker",
|
|
8861
|
+
[
|
|
8862
|
+
"ps",
|
|
8863
|
+
"-a",
|
|
8864
|
+
"--no-trunc",
|
|
8865
|
+
"--filter",
|
|
8866
|
+
`name=${namePrefix}`,
|
|
8867
|
+
"--format",
|
|
8868
|
+
"{{.Names}} {{.State}} {{.Status}}"
|
|
8869
|
+
],
|
|
8870
|
+
{ encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 }
|
|
8871
|
+
);
|
|
8872
|
+
const rows = [];
|
|
8873
|
+
for (const line of stdout.split("\n")) {
|
|
8874
|
+
if (!line.trim()) continue;
|
|
8875
|
+
const [name, state, ...statusParts] = line.split(" ");
|
|
8876
|
+
if (!name || !name.startsWith(namePrefix)) continue;
|
|
8877
|
+
rows.push({
|
|
8878
|
+
name,
|
|
8879
|
+
running: state === "running",
|
|
8880
|
+
status: statusParts.join(" ").trim() || state || ""
|
|
8881
|
+
});
|
|
8882
|
+
}
|
|
8883
|
+
return rows;
|
|
8884
|
+
} catch {
|
|
8885
|
+
return [];
|
|
8886
|
+
}
|
|
8887
|
+
}
|
|
8888
|
+
async function renameContainer(oldName, newName) {
|
|
8889
|
+
if (oldName === newName) return;
|
|
8890
|
+
try {
|
|
8891
|
+
await execFileAsync("docker", ["rename", oldName, newName], {});
|
|
8892
|
+
} catch {
|
|
8893
|
+
}
|
|
8894
|
+
}
|
|
8721
8895
|
async function spawnContainer(containerName, claudeToken, opts) {
|
|
8722
8896
|
const NUM_PORTS = 6;
|
|
8723
8897
|
const ports = [];
|
|
@@ -8792,7 +8966,7 @@ async function execInContainer(containerName, cmd) {
|
|
|
8792
8966
|
return stdout;
|
|
8793
8967
|
}
|
|
8794
8968
|
function streamExecInContainer(containerName, cmd, onLine) {
|
|
8795
|
-
const child = (0,
|
|
8969
|
+
const child = (0, import_child_process4.spawn)("docker", [
|
|
8796
8970
|
"exec",
|
|
8797
8971
|
"--user",
|
|
8798
8972
|
"claude",
|
|
@@ -8875,9 +9049,9 @@ async function getContainerPort(containerName, containerPort) {
|
|
|
8875
9049
|
}
|
|
8876
9050
|
|
|
8877
9051
|
// ../../shared/all/helpers/board-agent-core/git.ts
|
|
8878
|
-
var
|
|
9052
|
+
var import_child_process5 = require("child_process");
|
|
8879
9053
|
var import_util2 = require("util");
|
|
8880
|
-
var execFileAsync2 = (0, import_util2.promisify)(
|
|
9054
|
+
var execFileAsync2 = (0, import_util2.promisify)(import_child_process5.execFile);
|
|
8881
9055
|
function buildCloneUrl(repoUrl, githubToken) {
|
|
8882
9056
|
const url2 = new URL(repoUrl);
|
|
8883
9057
|
url2.username = githubToken;
|
|
@@ -9318,6 +9492,27 @@ function withTimeout(work, ms, label) {
|
|
|
9318
9492
|
});
|
|
9319
9493
|
return Promise.race([work.finally(() => clearTimeout(timer)), timeout]);
|
|
9320
9494
|
}
|
|
9495
|
+
function computeInstancePrefix(instanceId) {
|
|
9496
|
+
const raw = (instanceId || "shared").toLowerCase();
|
|
9497
|
+
return raw.replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "shared";
|
|
9498
|
+
}
|
|
9499
|
+
function parseFactiiiContainerName(name, instancePrefix) {
|
|
9500
|
+
const head = `factiii-${instancePrefix}-`;
|
|
9501
|
+
if (!name.startsWith(head)) return null;
|
|
9502
|
+
const rest = name.slice(head.length);
|
|
9503
|
+
const withId = rest.match(/^(card|util)-(\d+)-(.+)$/);
|
|
9504
|
+
if (withId) {
|
|
9505
|
+
const kind = withId[1];
|
|
9506
|
+
const spaceId = Number(withId[2]);
|
|
9507
|
+
return kind === "card" ? { kind, spaceId, taskId: withId[3] } : { kind, spaceId, cacheKey: withId[3] };
|
|
9508
|
+
}
|
|
9509
|
+
const legacy = rest.match(/^(card|util)-(.+)$/);
|
|
9510
|
+
if (legacy) {
|
|
9511
|
+
const kind = legacy[1];
|
|
9512
|
+
return kind === "card" ? { kind, spaceId: null, taskId: legacy[2] } : { kind, spaceId: null, cacheKey: legacy[2] };
|
|
9513
|
+
}
|
|
9514
|
+
return null;
|
|
9515
|
+
}
|
|
9321
9516
|
var ContainerCore = class {
|
|
9322
9517
|
constructor(options) {
|
|
9323
9518
|
// Utility container state.
|
|
@@ -9325,12 +9520,17 @@ var ContainerCore = class {
|
|
|
9325
9520
|
this.lastConfigKey = "";
|
|
9326
9521
|
this.setupPromise = null;
|
|
9327
9522
|
this.spaceSlug = options.spaceSlug;
|
|
9523
|
+
this.spaceId = options.spaceId ?? null;
|
|
9328
9524
|
this.emitter = options.emitter;
|
|
9329
9525
|
this.configProvider = options.configProvider;
|
|
9330
9526
|
this.dockerfilePath = options.dockerfilePath;
|
|
9331
9527
|
this.runnerToken = options.runnerToken;
|
|
9332
|
-
|
|
9333
|
-
|
|
9528
|
+
this.instancePrefix = computeInstancePrefix(options.instanceId);
|
|
9529
|
+
}
|
|
9530
|
+
/** The space segment used in container names: the stable id when we have it,
|
|
9531
|
+
* else the slug (legacy / id-less callers). */
|
|
9532
|
+
get spaceSegment() {
|
|
9533
|
+
return this.spaceId != null ? String(this.spaceId) : this.spaceSlug;
|
|
9334
9534
|
}
|
|
9335
9535
|
readConfig() {
|
|
9336
9536
|
return this.configProvider.readConfig(this.spaceSlug);
|
|
@@ -9430,7 +9630,11 @@ var ContainerCore = class {
|
|
|
9430
9630
|
}
|
|
9431
9631
|
const configKey = onedrive ? `onedrive|${this.spaceSlug}|${config.claudeToken}` : `${config.repoUrl}|${config.claudeToken}|${config.githubToken}`;
|
|
9432
9632
|
const cacheKey = onedrive ? `onedrive-${this.spaceSlug}`.replace(/[^a-zA-Z0-9._-]/g, "_") : repoCacheKey(config.repoUrl);
|
|
9433
|
-
const name = `factiii-${this.instancePrefix}-util-${this.
|
|
9633
|
+
const name = `factiii-${this.instancePrefix}-util-${this.spaceSegment}-${cacheKey}`;
|
|
9634
|
+
if (this.spaceId != null) {
|
|
9635
|
+
const legacyName = `factiii-${this.instancePrefix}-util-${this.spaceSlug}-${cacheKey}`;
|
|
9636
|
+
await renameContainer(legacyName, name);
|
|
9637
|
+
}
|
|
9434
9638
|
if (this.containerName && this.lastConfigKey !== configKey) {
|
|
9435
9639
|
await killContainer(this.containerName);
|
|
9436
9640
|
this.containerName = null;
|
|
@@ -9661,9 +9865,9 @@ function createEventSink(send, spaceSlug, notify) {
|
|
|
9661
9865
|
}
|
|
9662
9866
|
|
|
9663
9867
|
// ../../shared/all/helpers/card-ai/frpc.ts
|
|
9664
|
-
var
|
|
9665
|
-
var
|
|
9666
|
-
var
|
|
9868
|
+
var import_child_process6 = require("child_process");
|
|
9869
|
+
var import_fs3 = require("fs");
|
|
9870
|
+
var import_path3 = __toESM(require("path"));
|
|
9667
9871
|
var import_util3 = require("util");
|
|
9668
9872
|
|
|
9669
9873
|
// ../../shared/all/helpers/card-ai/scripts.ts
|
|
@@ -9717,15 +9921,15 @@ fi
|
|
|
9717
9921
|
`;
|
|
9718
9922
|
|
|
9719
9923
|
// ../../shared/all/helpers/card-ai/frpc.ts
|
|
9720
|
-
var execFileAsync3 = (0, import_util3.promisify)(
|
|
9924
|
+
var execFileAsync3 = (0, import_util3.promisify)(import_child_process6.execFile);
|
|
9721
9925
|
var FRP_SERVER_ADDR = "dev.factiii.com";
|
|
9722
9926
|
var FRP_SERVER_PORT = 7443;
|
|
9723
9927
|
var FRP_DOMAIN_SUFFIX = "dev.factiii.com";
|
|
9724
|
-
async function writeFileAsRoot(containerName,
|
|
9928
|
+
async function writeFileAsRoot(containerName, path8, contents) {
|
|
9725
9929
|
const b64 = Buffer.from(contents, "utf-8").toString("base64");
|
|
9726
9930
|
await execInContainerAsRoot(
|
|
9727
9931
|
containerName,
|
|
9728
|
-
`echo '${b64}' | base64 -d > '${
|
|
9932
|
+
`echo '${b64}' | base64 -d > '${path8}'`
|
|
9729
9933
|
);
|
|
9730
9934
|
}
|
|
9731
9935
|
async function installFrpcBinary(containerName) {
|
|
@@ -9738,8 +9942,8 @@ async function installFrpcBinary(containerName) {
|
|
|
9738
9942
|
if (!binaryName) {
|
|
9739
9943
|
throw new Error(`Unsupported container arch for frpc: ${arch}`);
|
|
9740
9944
|
}
|
|
9741
|
-
const src =
|
|
9742
|
-
if (!(0,
|
|
9945
|
+
const src = import_path3.default.join(__dirname, "..", "vendor", binaryName);
|
|
9946
|
+
if (!(0, import_fs3.existsSync)(src)) {
|
|
9743
9947
|
throw new Error(
|
|
9744
9948
|
`Vendored frpc missing (${src}). Run apps/runner/scripts/fetch-frpc.sh.`
|
|
9745
9949
|
);
|
|
@@ -10483,10 +10687,10 @@ var TerminalModeEngine = class {
|
|
|
10483
10687
|
}
|
|
10484
10688
|
async bareFsList({
|
|
10485
10689
|
postId,
|
|
10486
|
-
path:
|
|
10690
|
+
path: path8
|
|
10487
10691
|
}) {
|
|
10488
10692
|
const container = this.getBareContainer(postId);
|
|
10489
|
-
const abs = this.resolveBarePath(
|
|
10693
|
+
const abs = this.resolveBarePath(path8);
|
|
10490
10694
|
const relRoot = abs.slice(BARE_REPO_ROOT.length) || "/";
|
|
10491
10695
|
const out = await execInContainer(
|
|
10492
10696
|
container,
|
|
@@ -10536,21 +10740,21 @@ var TerminalModeEngine = class {
|
|
|
10536
10740
|
}
|
|
10537
10741
|
async bareFsRead({
|
|
10538
10742
|
postId,
|
|
10539
|
-
path:
|
|
10743
|
+
path: path8
|
|
10540
10744
|
}) {
|
|
10541
10745
|
const container = this.getBareContainer(postId);
|
|
10542
|
-
const abs = this.resolveBarePath(
|
|
10746
|
+
const abs = this.resolveBarePath(path8);
|
|
10543
10747
|
const b64 = (await execInContainer(container, `base64 -w 0 ${shEscape(abs)}`)).trim();
|
|
10544
10748
|
return { content: b64, encoding: "base64" };
|
|
10545
10749
|
}
|
|
10546
10750
|
async bareFsWrite({
|
|
10547
10751
|
postId,
|
|
10548
|
-
path:
|
|
10752
|
+
path: path8,
|
|
10549
10753
|
content,
|
|
10550
10754
|
encoding
|
|
10551
10755
|
}) {
|
|
10552
10756
|
const container = this.getBareContainer(postId);
|
|
10553
|
-
const abs = this.resolveBarePath(
|
|
10757
|
+
const abs = this.resolveBarePath(path8);
|
|
10554
10758
|
const b64 = encoding === "base64" ? content : Buffer.from(content, "utf-8").toString("base64");
|
|
10555
10759
|
if (!/^[A-Za-z0-9+/=\n\r]*$/.test(b64)) {
|
|
10556
10760
|
throw new Error("Invalid base64 payload.");
|
|
@@ -10560,7 +10764,7 @@ var TerminalModeEngine = class {
|
|
|
10560
10764
|
`mkdir -p ${shEscape(posixDirname(abs))} && printf %s ${shEscape(b64)} | base64 -d > ${shEscape(abs)}`
|
|
10561
10765
|
);
|
|
10562
10766
|
const session = this.sessions.get(String(postId));
|
|
10563
|
-
if (session) delete session.buffers[
|
|
10767
|
+
if (session) delete session.buffers[path8];
|
|
10564
10768
|
}
|
|
10565
10769
|
bareEditorState({
|
|
10566
10770
|
postId: rawPostId
|
|
@@ -10577,38 +10781,38 @@ var TerminalModeEngine = class {
|
|
|
10577
10781
|
}
|
|
10578
10782
|
bareOpenFile({
|
|
10579
10783
|
postId: rawPostId,
|
|
10580
|
-
path:
|
|
10784
|
+
path: path8
|
|
10581
10785
|
}) {
|
|
10582
10786
|
const session = this.sessions.get(String(rawPostId));
|
|
10583
10787
|
if (!session || session.mode !== "bare") return;
|
|
10584
|
-
if (!session.openedFiles.includes(
|
|
10585
|
-
session.openedFiles.push(
|
|
10788
|
+
if (!session.openedFiles.includes(path8)) {
|
|
10789
|
+
session.openedFiles.push(path8);
|
|
10586
10790
|
}
|
|
10587
10791
|
}
|
|
10588
10792
|
bareCloseFile({
|
|
10589
10793
|
postId: rawPostId,
|
|
10590
|
-
path:
|
|
10794
|
+
path: path8
|
|
10591
10795
|
}) {
|
|
10592
10796
|
const session = this.sessions.get(String(rawPostId));
|
|
10593
10797
|
if (!session || session.mode !== "bare") return;
|
|
10594
|
-
session.openedFiles = session.openedFiles.filter((p) => p !==
|
|
10595
|
-
delete session.buffers[
|
|
10798
|
+
session.openedFiles = session.openedFiles.filter((p) => p !== path8);
|
|
10799
|
+
delete session.buffers[path8];
|
|
10596
10800
|
}
|
|
10597
10801
|
bareBufferSet({
|
|
10598
10802
|
postId: rawPostId,
|
|
10599
|
-
path:
|
|
10803
|
+
path: path8,
|
|
10600
10804
|
content
|
|
10601
10805
|
}) {
|
|
10602
10806
|
const session = this.sessions.get(String(rawPostId));
|
|
10603
10807
|
if (!session || session.mode !== "bare") return;
|
|
10604
|
-
session.buffers[
|
|
10808
|
+
session.buffers[path8] = content;
|
|
10605
10809
|
}
|
|
10606
10810
|
async bareFsDelete({
|
|
10607
10811
|
postId,
|
|
10608
|
-
path:
|
|
10812
|
+
path: path8
|
|
10609
10813
|
}) {
|
|
10610
10814
|
const container = this.getBareContainer(postId);
|
|
10611
|
-
const abs = this.resolveBarePath(
|
|
10815
|
+
const abs = this.resolveBarePath(path8);
|
|
10612
10816
|
if (abs === BARE_REPO_ROOT) {
|
|
10613
10817
|
throw new Error("Refusing to delete the repo root.");
|
|
10614
10818
|
}
|
|
@@ -10616,10 +10820,10 @@ var TerminalModeEngine = class {
|
|
|
10616
10820
|
}
|
|
10617
10821
|
async bareFsMkdir({
|
|
10618
10822
|
postId,
|
|
10619
|
-
path:
|
|
10823
|
+
path: path8
|
|
10620
10824
|
}) {
|
|
10621
10825
|
const container = this.getBareContainer(postId);
|
|
10622
|
-
const abs = this.resolveBarePath(
|
|
10826
|
+
const abs = this.resolveBarePath(path8);
|
|
10623
10827
|
await execInContainer(container, `mkdir -p ${shEscape(abs)}`);
|
|
10624
10828
|
}
|
|
10625
10829
|
async bareReview({
|
|
@@ -11032,7 +11236,9 @@ var BoardAgentEngine = class {
|
|
|
11032
11236
|
targetBranch: targetBranch || "",
|
|
11033
11237
|
effort: effort || "high",
|
|
11034
11238
|
todos: [],
|
|
11035
|
-
|
|
11239
|
+
// Name by stable spaceId when we have it; fall back to the id-less legacy
|
|
11240
|
+
// form for old callers that only send the slug.
|
|
11241
|
+
containerName: this.core.spaceId != null ? `factiii-${this.core.instancePrefix}-card-${this.core.spaceId}-${taskId}` : `factiii-${this.core.instancePrefix}-card-${taskId}`
|
|
11036
11242
|
};
|
|
11037
11243
|
if (mode === "bare") {
|
|
11038
11244
|
this.terminal.start(base, {
|
|
@@ -11061,6 +11267,57 @@ var BoardAgentEngine = class {
|
|
|
11061
11267
|
cardSessions() {
|
|
11062
11268
|
return { ...this.terminal.listSummaries(), ...this.claude.listSummaries() };
|
|
11063
11269
|
}
|
|
11270
|
+
/** The stable board id this engine runs, if the caller provided one. */
|
|
11271
|
+
get spaceId() {
|
|
11272
|
+
return this.core.spaceId;
|
|
11273
|
+
}
|
|
11274
|
+
/**
|
|
11275
|
+
* Every factiii container/VM on this host (running + stopped) for this
|
|
11276
|
+
* runner's instance prefix - card and util, across ALL boards, since the
|
|
11277
|
+
* prefix is shared by every engine on the host. Returns them decoded; the
|
|
11278
|
+
* daemon scope-filters before handing them to a remote member. Electron
|
|
11279
|
+
* (single owner) gets the raw list straight through.
|
|
11280
|
+
*/
|
|
11281
|
+
async listContainers() {
|
|
11282
|
+
const rows = await listContainersByPrefix(
|
|
11283
|
+
`factiii-${this.core.instancePrefix}-`
|
|
11284
|
+
);
|
|
11285
|
+
const out = [];
|
|
11286
|
+
for (const row of rows) {
|
|
11287
|
+
const parsed = parseFactiiiContainerName(
|
|
11288
|
+
row.name,
|
|
11289
|
+
this.core.instancePrefix
|
|
11290
|
+
);
|
|
11291
|
+
if (!parsed) continue;
|
|
11292
|
+
out.push({
|
|
11293
|
+
containerName: row.name,
|
|
11294
|
+
kind: parsed.kind,
|
|
11295
|
+
spaceId: parsed.spaceId,
|
|
11296
|
+
// This engine only knows its own board's slug; the daemon fills in
|
|
11297
|
+
// slugs for other boards (cross-board) when scope-filtering.
|
|
11298
|
+
spaceSlug: parsed.spaceId != null && parsed.spaceId === this.core.spaceId ? this.core.spaceSlug : void 0,
|
|
11299
|
+
taskId: parsed.taskId,
|
|
11300
|
+
cacheKey: parsed.cacheKey,
|
|
11301
|
+
running: row.running,
|
|
11302
|
+
status: row.status
|
|
11303
|
+
});
|
|
11304
|
+
}
|
|
11305
|
+
return out;
|
|
11306
|
+
}
|
|
11307
|
+
/**
|
|
11308
|
+
* Force-remove a container/VM by name. Guards the name against this runner's
|
|
11309
|
+
* prefix so a peer can't `docker rm` an arbitrary container on the host. The
|
|
11310
|
+
* daemon adds the per-board scope check on top for remote members.
|
|
11311
|
+
*/
|
|
11312
|
+
async removeContainer({
|
|
11313
|
+
containerName
|
|
11314
|
+
}) {
|
|
11315
|
+
if (!containerName.startsWith(`factiii-${this.core.instancePrefix}-`)) {
|
|
11316
|
+
throw new Error("Refusing to remove a container outside this runner.");
|
|
11317
|
+
}
|
|
11318
|
+
await killContainer(containerName);
|
|
11319
|
+
return { removed: true };
|
|
11320
|
+
}
|
|
11064
11321
|
cardSession({ postId }) {
|
|
11065
11322
|
return this.claude.sessionWithLogs(String(postId)) ?? this.terminal.sessionWithLogs(String(postId));
|
|
11066
11323
|
}
|
|
@@ -11305,6 +11562,12 @@ var BoardAgentEngine = class {
|
|
|
11305
11562
|
return this.bareListExposed(
|
|
11306
11563
|
payload
|
|
11307
11564
|
);
|
|
11565
|
+
case "listContainers":
|
|
11566
|
+
return await this.listContainers();
|
|
11567
|
+
case "removeContainer":
|
|
11568
|
+
return await this.removeContainer(
|
|
11569
|
+
payload
|
|
11570
|
+
);
|
|
11308
11571
|
default:
|
|
11309
11572
|
throw new Error(`Unknown board-agent action: ${String(action)}`);
|
|
11310
11573
|
}
|
|
@@ -11361,7 +11624,7 @@ function makeChunkReassembler() {
|
|
|
11361
11624
|
|
|
11362
11625
|
// src/daemon.ts
|
|
11363
11626
|
var import_node_datachannel = require("node-datachannel");
|
|
11364
|
-
var
|
|
11627
|
+
var import_path6 = __toESM(require("path"));
|
|
11365
11628
|
|
|
11366
11629
|
// ../../node_modules/engine.io-client/build/esm-debug/transports/polling-xhr.node.js
|
|
11367
11630
|
var XMLHttpRequestModule = __toESM(require_XMLHttpRequest(), 1);
|
|
@@ -12541,12 +12804,12 @@ function parse2(str) {
|
|
|
12541
12804
|
uri.queryKey = queryKey(uri, uri["query"]);
|
|
12542
12805
|
return uri;
|
|
12543
12806
|
}
|
|
12544
|
-
function pathNames(obj,
|
|
12545
|
-
const regx = /\/{2,9}/g, names =
|
|
12546
|
-
if (
|
|
12807
|
+
function pathNames(obj, path8) {
|
|
12808
|
+
const regx = /\/{2,9}/g, names = path8.replace(regx, "/").split("/");
|
|
12809
|
+
if (path8.slice(0, 1) == "/" || path8.length === 0) {
|
|
12547
12810
|
names.splice(0, 1);
|
|
12548
12811
|
}
|
|
12549
|
-
if (
|
|
12812
|
+
if (path8.slice(-1) == "/") {
|
|
12550
12813
|
names.splice(names.length - 1, 1);
|
|
12551
12814
|
}
|
|
12552
12815
|
return names;
|
|
@@ -13165,7 +13428,7 @@ var protocol2 = Socket.protocol;
|
|
|
13165
13428
|
// ../../node_modules/socket.io-client/build/esm-debug/url.js
|
|
13166
13429
|
var import_debug7 = __toESM(require_src(), 1);
|
|
13167
13430
|
var debug7 = (0, import_debug7.default)("socket.io-client:url");
|
|
13168
|
-
function url(uri,
|
|
13431
|
+
function url(uri, path8 = "", loc) {
|
|
13169
13432
|
let obj = uri;
|
|
13170
13433
|
loc = loc || typeof location !== "undefined" && location;
|
|
13171
13434
|
if (null == uri)
|
|
@@ -13199,7 +13462,7 @@ function url(uri, path7 = "", loc) {
|
|
|
13199
13462
|
obj.path = obj.path || "/";
|
|
13200
13463
|
const ipv6 = obj.host.indexOf(":") !== -1;
|
|
13201
13464
|
const host = ipv6 ? "[" + obj.host + "]" : obj.host;
|
|
13202
|
-
obj.id = obj.protocol + "://" + host + ":" + obj.port +
|
|
13465
|
+
obj.id = obj.protocol + "://" + host + ":" + obj.port + path8;
|
|
13203
13466
|
obj.href = obj.protocol + "://" + host + (loc && loc.port === obj.port ? "" : ":" + obj.port);
|
|
13204
13467
|
return obj;
|
|
13205
13468
|
}
|
|
@@ -14833,8 +15096,8 @@ function lookup(uri, opts) {
|
|
|
14833
15096
|
const parsed = url(uri, opts.path || "/socket.io");
|
|
14834
15097
|
const source = parsed.source;
|
|
14835
15098
|
const id = parsed.id;
|
|
14836
|
-
const
|
|
14837
|
-
const sameNamespace = cache[id] &&
|
|
15099
|
+
const path8 = parsed.path;
|
|
15100
|
+
const sameNamespace = cache[id] && path8 in cache[id]["nsps"];
|
|
14838
15101
|
const newConnection = opts.forceNew || opts["force new connection"] || false === opts.multiplex || sameNamespace;
|
|
14839
15102
|
let io;
|
|
14840
15103
|
if (newConnection) {
|
|
@@ -14860,22 +15123,22 @@ Object.assign(lookup, {
|
|
|
14860
15123
|
});
|
|
14861
15124
|
|
|
14862
15125
|
// ../../shared/all/helpers/terminal-ai/terminal.ts
|
|
14863
|
-
var
|
|
15126
|
+
var import_child_process7 = require("child_process");
|
|
14864
15127
|
var import_dockerode = __toESM(require("dockerode"));
|
|
14865
|
-
var
|
|
15128
|
+
var import_fs4 = __toESM(require("fs"));
|
|
14866
15129
|
var import_os2 = __toESM(require("os"));
|
|
14867
|
-
var
|
|
15130
|
+
var import_path4 = __toESM(require("path"));
|
|
14868
15131
|
function resolveDocker() {
|
|
14869
15132
|
if (process.env.DOCKER_HOST) return new import_dockerode.default();
|
|
14870
15133
|
try {
|
|
14871
|
-
const out = (0,
|
|
15134
|
+
const out = (0, import_child_process7.execFileSync)(
|
|
14872
15135
|
"docker",
|
|
14873
15136
|
["context", "inspect", "--format", "{{.Endpoints.docker.Host}}"],
|
|
14874
15137
|
{ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
|
|
14875
15138
|
).trim();
|
|
14876
15139
|
if (out.startsWith("unix://")) {
|
|
14877
15140
|
const p = out.slice("unix://".length);
|
|
14878
|
-
if (
|
|
15141
|
+
if (import_fs4.default.existsSync(p)) return new import_dockerode.default({ socketPath: p });
|
|
14879
15142
|
} else if (out) {
|
|
14880
15143
|
process.env.DOCKER_HOST = out;
|
|
14881
15144
|
return new import_dockerode.default();
|
|
@@ -14884,15 +15147,15 @@ function resolveDocker() {
|
|
|
14884
15147
|
}
|
|
14885
15148
|
const candidates = [
|
|
14886
15149
|
"/var/run/docker.sock",
|
|
14887
|
-
|
|
14888
|
-
|
|
14889
|
-
|
|
14890
|
-
|
|
14891
|
-
|
|
15150
|
+
import_path4.default.join(import_os2.default.homedir(), ".docker/run/docker.sock"),
|
|
15151
|
+
import_path4.default.join(import_os2.default.homedir(), ".docker/desktop/docker.sock"),
|
|
15152
|
+
import_path4.default.join(import_os2.default.homedir(), ".orbstack/run/docker.sock"),
|
|
15153
|
+
import_path4.default.join(import_os2.default.homedir(), ".colima/default/docker.sock"),
|
|
15154
|
+
import_path4.default.join(import_os2.default.homedir(), ".rd/docker.sock")
|
|
14892
15155
|
];
|
|
14893
15156
|
for (const p of candidates) {
|
|
14894
15157
|
try {
|
|
14895
|
-
|
|
15158
|
+
import_fs4.default.statSync(p);
|
|
14896
15159
|
return new import_dockerode.default({ socketPath: p });
|
|
14897
15160
|
} catch {
|
|
14898
15161
|
}
|
|
@@ -15093,50 +15356,119 @@ var TerminalManager = class {
|
|
|
15093
15356
|
};
|
|
15094
15357
|
|
|
15095
15358
|
// src/agent-adapter.ts
|
|
15096
|
-
var
|
|
15359
|
+
var import_fs5 = __toESM(require("fs"));
|
|
15097
15360
|
var import_os3 = __toESM(require("os"));
|
|
15098
|
-
var
|
|
15099
|
-
var CONFIG_DIR2 =
|
|
15361
|
+
var import_path5 = __toESM(require("path"));
|
|
15362
|
+
var CONFIG_DIR2 = import_path5.default.join(import_os3.default.homedir(), ".factiii-runner");
|
|
15363
|
+
var RUNNER_CONFIG_PATH = import_path5.default.join(CONFIG_DIR2, "runner-config.json");
|
|
15100
15364
|
function safeSlug(spaceSlug) {
|
|
15101
15365
|
return spaceSlug.replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
15102
15366
|
}
|
|
15103
|
-
function configPath(spaceSlug) {
|
|
15104
|
-
return import_path4.default.join(CONFIG_DIR2, `space-${safeSlug(spaceSlug)}.json`);
|
|
15105
|
-
}
|
|
15106
15367
|
function connectionPath(spaceSlug) {
|
|
15107
|
-
return
|
|
15368
|
+
return import_path5.default.join(CONFIG_DIR2, `space-${safeSlug(spaceSlug)}.onedrive.json`);
|
|
15108
15369
|
}
|
|
15109
15370
|
function writeJson(filePath, value2) {
|
|
15110
|
-
|
|
15111
|
-
|
|
15112
|
-
|
|
15371
|
+
import_fs5.default.mkdirSync(CONFIG_DIR2, { recursive: true });
|
|
15372
|
+
import_fs5.default.writeFileSync(filePath, JSON.stringify(value2, null, 2));
|
|
15373
|
+
import_fs5.default.chmodSync(filePath, 384);
|
|
15113
15374
|
}
|
|
15114
|
-
var
|
|
15115
|
-
|
|
15116
|
-
|
|
15375
|
+
var CONFIG_FIELDS = [
|
|
15376
|
+
"repoUrl",
|
|
15377
|
+
"mainBranch",
|
|
15378
|
+
"gitName",
|
|
15379
|
+
"gitEmail",
|
|
15380
|
+
"claudeToken",
|
|
15381
|
+
"githubToken",
|
|
15382
|
+
"storageBackend"
|
|
15383
|
+
];
|
|
15384
|
+
function fillDefaults(config) {
|
|
15385
|
+
return {
|
|
15386
|
+
repoUrl: config.repoUrl || "",
|
|
15387
|
+
mainBranch: config.mainBranch || "main",
|
|
15388
|
+
gitName: config.gitName || "",
|
|
15389
|
+
gitEmail: config.gitEmail || "",
|
|
15390
|
+
claudeToken: config.claudeToken || "",
|
|
15391
|
+
githubToken: config.githubToken || "",
|
|
15392
|
+
storageBackend: config.storageBackend || "github"
|
|
15393
|
+
};
|
|
15394
|
+
}
|
|
15395
|
+
function migrateLegacyConfigs() {
|
|
15396
|
+
const file = { boards: {} };
|
|
15397
|
+
let legacy = [];
|
|
15398
|
+
try {
|
|
15399
|
+
legacy = import_fs5.default.readdirSync(CONFIG_DIR2).filter((f) => f.startsWith("space-") && f.endsWith(".json")).filter((f) => !f.endsWith(".onedrive.json"));
|
|
15400
|
+
} catch {
|
|
15401
|
+
return file;
|
|
15402
|
+
}
|
|
15403
|
+
for (const name of legacy) {
|
|
15404
|
+
const slug = name.slice("space-".length, -".json".length);
|
|
15405
|
+
let parsed = {};
|
|
15117
15406
|
try {
|
|
15118
|
-
|
|
15119
|
-
|
|
15407
|
+
parsed = JSON.parse(
|
|
15408
|
+
import_fs5.default.readFileSync(import_path5.default.join(CONFIG_DIR2, name), "utf-8")
|
|
15120
15409
|
);
|
|
15121
15410
|
} catch {
|
|
15411
|
+
continue;
|
|
15122
15412
|
}
|
|
15123
|
-
|
|
15124
|
-
|
|
15125
|
-
|
|
15126
|
-
|
|
15127
|
-
|
|
15128
|
-
|
|
15129
|
-
|
|
15130
|
-
|
|
15131
|
-
|
|
15413
|
+
if (!file.defaultSlug) {
|
|
15414
|
+
file.defaultSlug = slug;
|
|
15415
|
+
file.default = parsed;
|
|
15416
|
+
} else {
|
|
15417
|
+
file.boards[slug] = parsed;
|
|
15418
|
+
}
|
|
15419
|
+
}
|
|
15420
|
+
if (file.defaultSlug) writeJson(RUNNER_CONFIG_PATH, file);
|
|
15421
|
+
return file;
|
|
15422
|
+
}
|
|
15423
|
+
function readRunnerConfig2() {
|
|
15424
|
+
try {
|
|
15425
|
+
return JSON.parse(
|
|
15426
|
+
import_fs5.default.readFileSync(RUNNER_CONFIG_PATH, "utf-8")
|
|
15427
|
+
);
|
|
15428
|
+
} catch {
|
|
15429
|
+
return migrateLegacyConfigs();
|
|
15430
|
+
}
|
|
15431
|
+
}
|
|
15432
|
+
var localConfigProvider = {
|
|
15433
|
+
readConfig(spaceSlug) {
|
|
15434
|
+
const file = readRunnerConfig2();
|
|
15435
|
+
const base = file.default ?? {};
|
|
15436
|
+
const over = file.boards?.[spaceSlug] ?? {};
|
|
15437
|
+
return fillDefaults({
|
|
15438
|
+
repoUrl: over.repoUrl || base.repoUrl,
|
|
15439
|
+
mainBranch: over.mainBranch || base.mainBranch,
|
|
15440
|
+
gitName: over.gitName || base.gitName,
|
|
15441
|
+
gitEmail: over.gitEmail || base.gitEmail,
|
|
15442
|
+
claudeToken: over.claudeToken || base.claudeToken,
|
|
15443
|
+
githubToken: over.githubToken || base.githubToken,
|
|
15444
|
+
storageBackend: over.storageBackend || base.storageBackend
|
|
15445
|
+
});
|
|
15132
15446
|
},
|
|
15133
15447
|
writeConfig(spaceSlug, config) {
|
|
15134
|
-
|
|
15448
|
+
const file = readRunnerConfig2();
|
|
15449
|
+
file.boards ??= {};
|
|
15450
|
+
if (!file.defaultSlug) {
|
|
15451
|
+
file.defaultSlug = spaceSlug;
|
|
15452
|
+
file.default = config;
|
|
15453
|
+
} else if (spaceSlug === file.defaultSlug) {
|
|
15454
|
+
file.default = config;
|
|
15455
|
+
} else {
|
|
15456
|
+
const base = file.default ?? {};
|
|
15457
|
+
const diff = {};
|
|
15458
|
+
for (const f of CONFIG_FIELDS) {
|
|
15459
|
+
if (config[f] && config[f] !== base[f]) {
|
|
15460
|
+
diff[f] = config[f];
|
|
15461
|
+
}
|
|
15462
|
+
}
|
|
15463
|
+
if (Object.keys(diff).length) file.boards[spaceSlug] = diff;
|
|
15464
|
+
else delete file.boards[spaceSlug];
|
|
15465
|
+
}
|
|
15466
|
+
writeJson(RUNNER_CONFIG_PATH, file);
|
|
15135
15467
|
},
|
|
15136
15468
|
readOneDriveConnection(spaceSlug) {
|
|
15137
15469
|
try {
|
|
15138
15470
|
return JSON.parse(
|
|
15139
|
-
|
|
15471
|
+
import_fs5.default.readFileSync(connectionPath(spaceSlug), "utf-8")
|
|
15140
15472
|
);
|
|
15141
15473
|
} catch {
|
|
15142
15474
|
return null;
|
|
@@ -15145,7 +15477,7 @@ var localConfigProvider = {
|
|
|
15145
15477
|
writeOneDriveConnection(spaceSlug, connection) {
|
|
15146
15478
|
const filePath = connectionPath(spaceSlug);
|
|
15147
15479
|
if (!connection) {
|
|
15148
|
-
|
|
15480
|
+
import_fs5.default.rmSync(filePath, { force: true });
|
|
15149
15481
|
return;
|
|
15150
15482
|
}
|
|
15151
15483
|
writeJson(filePath, connection);
|
|
@@ -15153,6 +15485,13 @@ var localConfigProvider = {
|
|
|
15153
15485
|
};
|
|
15154
15486
|
|
|
15155
15487
|
// src/daemon.ts
|
|
15488
|
+
function toChannelScope(raw) {
|
|
15489
|
+
if (!raw || raw.role === "owner") return { role: "owner" };
|
|
15490
|
+
return { role: "member", spaceSlugs: new Set(raw.spaceSlugs) };
|
|
15491
|
+
}
|
|
15492
|
+
function scopeAllows(scope, spaceSlug) {
|
|
15493
|
+
return scope.role === "owner" || scope.spaceSlugs.has(spaceSlug);
|
|
15494
|
+
}
|
|
15156
15495
|
async function startDaemon(config) {
|
|
15157
15496
|
console.log(`Connecting to ${config.serverUrl} as "${config.runnerName}"...`);
|
|
15158
15497
|
const socket = lookup(config.serverUrl, {
|
|
@@ -15162,10 +15501,11 @@ async function startDaemon(config) {
|
|
|
15162
15501
|
autoConnect: false
|
|
15163
15502
|
});
|
|
15164
15503
|
const engines = /* @__PURE__ */ new Map();
|
|
15165
|
-
const
|
|
15504
|
+
const channelScopes = /* @__PURE__ */ new Map();
|
|
15166
15505
|
const terminalByChannel = /* @__PURE__ */ new WeakMap();
|
|
15167
|
-
const
|
|
15168
|
-
for (const engine of engines
|
|
15506
|
+
const findCardContainerInScope = (scope, postId) => {
|
|
15507
|
+
for (const [spaceSlug, engine] of engines) {
|
|
15508
|
+
if (!scopeAllows(scope, spaceSlug)) continue;
|
|
15169
15509
|
const name = engine.getCardContainerName(postId);
|
|
15170
15510
|
if (name) return name;
|
|
15171
15511
|
}
|
|
@@ -15173,7 +15513,7 @@ async function startDaemon(config) {
|
|
|
15173
15513
|
};
|
|
15174
15514
|
const broadcastToAll = (msg) => {
|
|
15175
15515
|
const frames = chunkWire(msg);
|
|
15176
|
-
for (const dc of
|
|
15516
|
+
for (const dc of channelScopes.keys()) {
|
|
15177
15517
|
for (const frame of frames) {
|
|
15178
15518
|
try {
|
|
15179
15519
|
dc.sendMessage(frame);
|
|
@@ -15182,7 +15522,19 @@ async function startDaemon(config) {
|
|
|
15182
15522
|
}
|
|
15183
15523
|
}
|
|
15184
15524
|
};
|
|
15185
|
-
|
|
15525
|
+
const broadcastToBoard = (spaceSlug, msg) => {
|
|
15526
|
+
const frames = chunkWire(msg);
|
|
15527
|
+
for (const [dc, scope] of channelScopes) {
|
|
15528
|
+
if (!scopeAllows(scope, spaceSlug)) continue;
|
|
15529
|
+
for (const frame of frames) {
|
|
15530
|
+
try {
|
|
15531
|
+
dc.sendMessage(frame);
|
|
15532
|
+
} catch {
|
|
15533
|
+
}
|
|
15534
|
+
}
|
|
15535
|
+
}
|
|
15536
|
+
};
|
|
15537
|
+
function getEngine(spaceSlug, spaceId) {
|
|
15186
15538
|
let engine = engines.get(spaceSlug);
|
|
15187
15539
|
if (!engine) {
|
|
15188
15540
|
const send = (event, data) => {
|
|
@@ -15192,10 +15544,13 @@ async function startDaemon(config) {
|
|
|
15192
15544
|
event,
|
|
15193
15545
|
payload: data
|
|
15194
15546
|
};
|
|
15195
|
-
|
|
15547
|
+
broadcastToBoard(spaceSlug, JSON.stringify(msg));
|
|
15196
15548
|
};
|
|
15197
15549
|
engine = new BoardAgentEngine({
|
|
15198
15550
|
spaceSlug,
|
|
15551
|
+
// Stable id for durable container naming (slugs can change); the engine
|
|
15552
|
+
// falls back to the slug when absent.
|
|
15553
|
+
spaceId,
|
|
15199
15554
|
emitter: createEventSink(send, spaceSlug),
|
|
15200
15555
|
configProvider: localConfigProvider,
|
|
15201
15556
|
dockerfilePath: getDockerfilePath(),
|
|
@@ -15209,6 +15564,7 @@ async function startDaemon(config) {
|
|
|
15209
15564
|
}
|
|
15210
15565
|
return engine;
|
|
15211
15566
|
}
|
|
15567
|
+
const instancePrefix = computeInstancePrefix(`runner-${config.runnerName}`);
|
|
15212
15568
|
socket.on("connect", () => {
|
|
15213
15569
|
console.log(`Connected. Runner "${config.runnerName}" is online.`);
|
|
15214
15570
|
console.log("Waiting for connections from UI clients...\n");
|
|
@@ -15221,6 +15577,7 @@ async function startDaemon(config) {
|
|
|
15221
15577
|
});
|
|
15222
15578
|
socket.on("runnerOffer", (data) => {
|
|
15223
15579
|
console.log(`Received WebRTC offer from ${data.fromSocketId}`);
|
|
15580
|
+
const scope = toChannelScope(data.scope);
|
|
15224
15581
|
try {
|
|
15225
15582
|
const peer = new import_node_datachannel.PeerConnection("runner", {
|
|
15226
15583
|
iceServers: ["stun:stun.l.google.com:19302"]
|
|
@@ -15268,7 +15625,7 @@ async function startDaemon(config) {
|
|
|
15268
15625
|
console.log(
|
|
15269
15626
|
`Data channel "${dc.getLabel()}" opened with ${data.fromSocketId}`
|
|
15270
15627
|
);
|
|
15271
|
-
|
|
15628
|
+
channelScopes.set(dc, scope);
|
|
15272
15629
|
const reassembler = makeChunkReassembler();
|
|
15273
15630
|
const sendToChannel = (msg) => {
|
|
15274
15631
|
for (const frame of chunkWire(msg)) {
|
|
@@ -15286,10 +15643,17 @@ async function startDaemon(config) {
|
|
|
15286
15643
|
};
|
|
15287
15644
|
terminalByChannel.set(
|
|
15288
15645
|
dc,
|
|
15289
|
-
new TerminalManager(
|
|
15646
|
+
new TerminalManager(
|
|
15647
|
+
sendRawToChannel,
|
|
15648
|
+
(postId) => findCardContainerInScope(scope, postId)
|
|
15649
|
+
)
|
|
15290
15650
|
);
|
|
15291
15651
|
dc.onOpen(() => {
|
|
15652
|
+
sendToChannel(
|
|
15653
|
+
JSON.stringify({ type: "runner-info", version: currentVersion() })
|
|
15654
|
+
);
|
|
15292
15655
|
for (const [spaceSlug, engine] of engines.entries()) {
|
|
15656
|
+
if (!scopeAllows(scope, spaceSlug)) continue;
|
|
15293
15657
|
const sessions = engine.cardSessions();
|
|
15294
15658
|
for (const summary of Object.values(sessions)) {
|
|
15295
15659
|
const msg = JSON.stringify({
|
|
@@ -15317,15 +15681,21 @@ async function startDaemon(config) {
|
|
|
15317
15681
|
}
|
|
15318
15682
|
return;
|
|
15319
15683
|
}
|
|
15684
|
+
if (parsed.type === "runner-control" && parsed.action === "update") {
|
|
15685
|
+
applyUpdateAndRestart();
|
|
15686
|
+
return;
|
|
15687
|
+
}
|
|
15320
15688
|
if (parsed.type === "broadcast" && parsed.spaceSlug && parsed.event) {
|
|
15689
|
+
if (!scopeAllows(scope, parsed.spaceSlug)) return;
|
|
15321
15690
|
const eventMsg = JSON.stringify({
|
|
15322
15691
|
type: "event",
|
|
15323
15692
|
event: `board-agent:${parsed.spaceSlug}:${parsed.event}`,
|
|
15324
15693
|
payload: parsed.payload
|
|
15325
15694
|
});
|
|
15326
15695
|
const frames = chunkWire(eventMsg);
|
|
15327
|
-
for (const other of
|
|
15696
|
+
for (const [other, otherScope] of channelScopes) {
|
|
15328
15697
|
if (other === dc) continue;
|
|
15698
|
+
if (!scopeAllows(otherScope, parsed.spaceSlug)) continue;
|
|
15329
15699
|
for (const frame of frames) {
|
|
15330
15700
|
try {
|
|
15331
15701
|
other.sendMessage(frame);
|
|
@@ -15340,14 +15710,17 @@ async function startDaemon(config) {
|
|
|
15340
15710
|
}
|
|
15341
15711
|
handleDataChannelMessage(
|
|
15342
15712
|
msgStr,
|
|
15713
|
+
scope,
|
|
15343
15714
|
sendToChannel,
|
|
15344
|
-
|
|
15345
|
-
getEngine
|
|
15715
|
+
broadcastToBoard,
|
|
15716
|
+
getEngine,
|
|
15717
|
+
engines,
|
|
15718
|
+
instancePrefix
|
|
15346
15719
|
);
|
|
15347
15720
|
});
|
|
15348
15721
|
dc.onClosed(() => {
|
|
15349
15722
|
console.log(`Data channel closed with ${data.fromSocketId}`);
|
|
15350
|
-
|
|
15723
|
+
channelScopes.delete(dc);
|
|
15351
15724
|
const mgr = terminalByChannel.get(dc);
|
|
15352
15725
|
if (mgr) {
|
|
15353
15726
|
mgr.destroyAll();
|
|
@@ -15373,6 +15746,25 @@ async function startDaemon(config) {
|
|
|
15373
15746
|
}
|
|
15374
15747
|
socket.disconnect();
|
|
15375
15748
|
};
|
|
15749
|
+
const applyUpdateAndRestart = () => {
|
|
15750
|
+
console.log("[update] update approved by a client \u2014 installing latest...");
|
|
15751
|
+
const status = (state) => broadcastToAll(JSON.stringify({ type: "runner-update", state }));
|
|
15752
|
+
status("installing");
|
|
15753
|
+
if (!installLatest()) {
|
|
15754
|
+
status("failed");
|
|
15755
|
+
return;
|
|
15756
|
+
}
|
|
15757
|
+
status("restarting");
|
|
15758
|
+
cleanup();
|
|
15759
|
+
process.exit(RESTART_FOR_UPDATE);
|
|
15760
|
+
};
|
|
15761
|
+
socket.on("runnerRevoked", () => {
|
|
15762
|
+
console.log(
|
|
15763
|
+
"\nThis runner was disconnected from your Factiii account. Stopping all jobs and removing containers..."
|
|
15764
|
+
);
|
|
15765
|
+
cleanup();
|
|
15766
|
+
process.exit(0);
|
|
15767
|
+
});
|
|
15376
15768
|
process.on("SIGINT", () => {
|
|
15377
15769
|
console.log("\nShutting down...");
|
|
15378
15770
|
cleanup();
|
|
@@ -15382,8 +15774,27 @@ async function startDaemon(config) {
|
|
|
15382
15774
|
cleanup();
|
|
15383
15775
|
process.exit(0);
|
|
15384
15776
|
});
|
|
15777
|
+
await new Promise(() => {
|
|
15778
|
+
});
|
|
15385
15779
|
}
|
|
15386
|
-
|
|
15780
|
+
function spaceIdToSlug(engines) {
|
|
15781
|
+
const map = /* @__PURE__ */ new Map();
|
|
15782
|
+
for (const [slug, engine] of engines) {
|
|
15783
|
+
if (engine.spaceId != null) map.set(engine.spaceId, slug);
|
|
15784
|
+
}
|
|
15785
|
+
return map;
|
|
15786
|
+
}
|
|
15787
|
+
function scopeFilterContainers(all, scope, engines) {
|
|
15788
|
+
const idToSlug = spaceIdToSlug(engines);
|
|
15789
|
+
return all.flatMap((c) => {
|
|
15790
|
+
const slug = c.spaceId != null ? idToSlug.get(c.spaceId) : void 0;
|
|
15791
|
+
if (scope.role !== "owner") {
|
|
15792
|
+
if (!slug || !scopeAllows(scope, slug)) return [];
|
|
15793
|
+
}
|
|
15794
|
+
return [{ ...c, spaceId: slug ? c.spaceId : null, spaceSlug: slug }];
|
|
15795
|
+
});
|
|
15796
|
+
}
|
|
15797
|
+
async function handleDataChannelMessage(raw, scope, sendToChannel, broadcastToBoard, getEngine, engines, instancePrefix) {
|
|
15387
15798
|
let msg;
|
|
15388
15799
|
try {
|
|
15389
15800
|
msg = JSON.parse(raw);
|
|
@@ -15391,9 +15802,76 @@ async function handleDataChannelMessage(raw, sendToChannel, broadcastToAll, getE
|
|
|
15391
15802
|
return;
|
|
15392
15803
|
}
|
|
15393
15804
|
if (msg.type !== "dispatch") return;
|
|
15394
|
-
const engine = getEngine(msg.spaceSlug);
|
|
15395
15805
|
const { action, payload, requestId } = msg;
|
|
15396
15806
|
const ts = () => (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
15807
|
+
const sendResult = (res) => sendToChannel(JSON.stringify(res));
|
|
15808
|
+
if (action === "listContainers") {
|
|
15809
|
+
try {
|
|
15810
|
+
const rows = await listContainersByPrefix(`factiii-${instancePrefix}-`);
|
|
15811
|
+
const all = rows.flatMap((row) => {
|
|
15812
|
+
const parsed = parseFactiiiContainerName(row.name, instancePrefix);
|
|
15813
|
+
if (!parsed) return [];
|
|
15814
|
+
return [
|
|
15815
|
+
{
|
|
15816
|
+
containerName: row.name,
|
|
15817
|
+
kind: parsed.kind,
|
|
15818
|
+
spaceId: parsed.spaceId,
|
|
15819
|
+
taskId: parsed.taskId,
|
|
15820
|
+
cacheKey: parsed.cacheKey,
|
|
15821
|
+
running: row.running,
|
|
15822
|
+
status: row.status
|
|
15823
|
+
}
|
|
15824
|
+
];
|
|
15825
|
+
});
|
|
15826
|
+
sendResult({
|
|
15827
|
+
type: "result",
|
|
15828
|
+
requestId,
|
|
15829
|
+
result: scopeFilterContainers(all, scope, engines)
|
|
15830
|
+
});
|
|
15831
|
+
} catch (err) {
|
|
15832
|
+
sendResult({
|
|
15833
|
+
type: "result",
|
|
15834
|
+
requestId,
|
|
15835
|
+
error: err instanceof Error ? err.message : String(err)
|
|
15836
|
+
});
|
|
15837
|
+
}
|
|
15838
|
+
return;
|
|
15839
|
+
}
|
|
15840
|
+
if (action === "removeContainer") {
|
|
15841
|
+
const { containerName } = payload;
|
|
15842
|
+
try {
|
|
15843
|
+
if (!containerName.startsWith(`factiii-${instancePrefix}-`)) {
|
|
15844
|
+
throw new Error("Refusing to remove a container outside this runner.");
|
|
15845
|
+
}
|
|
15846
|
+
if (scope.role !== "owner") {
|
|
15847
|
+
const parsed = parseFactiiiContainerName(containerName, instancePrefix);
|
|
15848
|
+
const slug = parsed?.spaceId != null ? spaceIdToSlug(engines).get(parsed.spaceId) : void 0;
|
|
15849
|
+
if (!slug || !scopeAllows(scope, slug)) {
|
|
15850
|
+
throw new Error("Not authorized to remove this container");
|
|
15851
|
+
}
|
|
15852
|
+
}
|
|
15853
|
+
await killContainer(containerName);
|
|
15854
|
+
sendResult({ type: "result", requestId, result: { removed: true } });
|
|
15855
|
+
} catch (err) {
|
|
15856
|
+
sendResult({
|
|
15857
|
+
type: "result",
|
|
15858
|
+
requestId,
|
|
15859
|
+
error: err instanceof Error ? err.message : String(err)
|
|
15860
|
+
});
|
|
15861
|
+
}
|
|
15862
|
+
return;
|
|
15863
|
+
}
|
|
15864
|
+
if (!scopeAllows(scope, msg.spaceSlug)) {
|
|
15865
|
+
console.warn(`[${ts()}] \u20E0 ${action} denied for board "${msg.spaceSlug}"`);
|
|
15866
|
+
const denied = {
|
|
15867
|
+
type: "result",
|
|
15868
|
+
requestId,
|
|
15869
|
+
error: "Not authorized for this board on this runner"
|
|
15870
|
+
};
|
|
15871
|
+
sendToChannel(JSON.stringify(denied));
|
|
15872
|
+
return;
|
|
15873
|
+
}
|
|
15874
|
+
const engine = getEngine(msg.spaceSlug, msg.spaceId);
|
|
15397
15875
|
console.log(`[${ts()}] \u2190 ${action} (${requestId})`);
|
|
15398
15876
|
const askLogSender = (entry) => {
|
|
15399
15877
|
const event = {
|
|
@@ -15402,7 +15880,7 @@ async function handleDataChannelMessage(raw, sendToChannel, broadcastToAll, getE
|
|
|
15402
15880
|
event: `board-agent:${msg.spaceSlug}:log`,
|
|
15403
15881
|
payload: entry
|
|
15404
15882
|
};
|
|
15405
|
-
|
|
15883
|
+
broadcastToBoard(msg.spaceSlug, JSON.stringify(event));
|
|
15406
15884
|
};
|
|
15407
15885
|
try {
|
|
15408
15886
|
const result = await engine.dispatch(
|
|
@@ -15425,107 +15903,41 @@ async function handleDataChannelMessage(raw, sendToChannel, broadcastToAll, getE
|
|
|
15425
15903
|
}
|
|
15426
15904
|
}
|
|
15427
15905
|
function getDockerfilePath() {
|
|
15428
|
-
return
|
|
15906
|
+
return import_path6.default.join(__dirname, "..", "Dockerfile.claude");
|
|
15429
15907
|
}
|
|
15430
15908
|
|
|
15431
|
-
// src/
|
|
15432
|
-
|
|
15433
|
-
|
|
15434
|
-
|
|
15435
|
-
|
|
15436
|
-
|
|
15437
|
-
|
|
15438
|
-
|
|
15439
|
-
|
|
15440
|
-
|
|
15441
|
-
stdio: opts?.silent ? "pipe" : "inherit",
|
|
15442
|
-
// Avoids a cmd.exe window flash on Windows during silent checks.
|
|
15443
|
-
windowsHide: true
|
|
15444
|
-
});
|
|
15445
|
-
} catch {
|
|
15446
|
-
return "";
|
|
15447
|
-
}
|
|
15448
|
-
}
|
|
15449
|
-
function check(cmd) {
|
|
15450
|
-
const lookup2 = process.platform === "win32" ? "where" : "which";
|
|
15451
|
-
try {
|
|
15452
|
-
(0, import_child_process7.execFileSync)(lookup2, [cmd], { stdio: "pipe", windowsHide: true });
|
|
15453
|
-
return true;
|
|
15454
|
-
} catch {
|
|
15455
|
-
return false;
|
|
15456
|
-
}
|
|
15457
|
-
}
|
|
15458
|
-
async function setup(serverUrl) {
|
|
15459
|
-
console.log("\n=== Factiii Runner \u2014 Setup ===\n");
|
|
15460
|
-
console.log("[1/3] Checking Docker...");
|
|
15461
|
-
if (!check("docker")) {
|
|
15462
|
-
console.error(
|
|
15463
|
-
" Docker is not installed. Install it from https://docs.docker.com/get-docker/ and re-run setup."
|
|
15464
|
-
);
|
|
15465
|
-
process.exit(1);
|
|
15466
|
-
}
|
|
15467
|
-
if (!run("docker", ["info"], { silent: true })) {
|
|
15468
|
-
console.error(
|
|
15469
|
-
" Docker is installed but the daemon is not running. Start Docker and re-run setup."
|
|
15470
|
-
);
|
|
15471
|
-
process.exit(1);
|
|
15472
|
-
}
|
|
15473
|
-
console.log(" Docker is ready.");
|
|
15474
|
-
console.log("\n[2/3] Building factiii-claude Docker image...");
|
|
15475
|
-
const dockerfilePath = findDockerfile();
|
|
15476
|
-
if (!dockerfilePath) {
|
|
15477
|
-
console.error(
|
|
15478
|
-
" Dockerfile.claude not found. Reinstall the package and re-run setup."
|
|
15479
|
-
);
|
|
15480
|
-
process.exit(1);
|
|
15909
|
+
// src/supervisor.ts
|
|
15910
|
+
async function runSupervised(workerArgs) {
|
|
15911
|
+
if (isWorker()) {
|
|
15912
|
+
const config = readRunnerConfig();
|
|
15913
|
+
if (!config) {
|
|
15914
|
+
console.error('Failed to read config. Run "factiii-runner setup" again.');
|
|
15915
|
+
process.exit(1);
|
|
15916
|
+
}
|
|
15917
|
+
await startDaemon(config);
|
|
15918
|
+
process.exit(0);
|
|
15481
15919
|
}
|
|
15482
|
-
|
|
15483
|
-
|
|
15484
|
-
|
|
15485
|
-
|
|
15486
|
-
|
|
15487
|
-
|
|
15488
|
-
|
|
15489
|
-
|
|
15490
|
-
`{{ index .Config.Labels "${DOCKERFILE_LABEL2}" }}`
|
|
15491
|
-
],
|
|
15492
|
-
{ silent: true }
|
|
15493
|
-
).trim();
|
|
15494
|
-
if (existingHash === wantedHash) {
|
|
15495
|
-
console.log(" Image is up to date.");
|
|
15496
|
-
} else {
|
|
15497
|
-
console.log(
|
|
15498
|
-
existingHash ? ` Dockerfile changed since last build, rebuilding from ${dockerfilePath}...` : ` Building from ${dockerfilePath}...`
|
|
15920
|
+
for (; ; ) {
|
|
15921
|
+
const child = (0, import_node_child_process2.spawnSync)(
|
|
15922
|
+
process.execPath,
|
|
15923
|
+
[...process.execArgv, ...workerArgs],
|
|
15924
|
+
{
|
|
15925
|
+
stdio: "inherit",
|
|
15926
|
+
env: markWorkerEnv(process.env)
|
|
15927
|
+
}
|
|
15499
15928
|
);
|
|
15500
|
-
|
|
15501
|
-
"
|
|
15502
|
-
|
|
15503
|
-
|
|
15504
|
-
|
|
15505
|
-
dockerfilePath,
|
|
15506
|
-
"--label",
|
|
15507
|
-
`${DOCKERFILE_LABEL2}=${wantedHash}`,
|
|
15508
|
-
"."
|
|
15509
|
-
]);
|
|
15510
|
-
console.log(" Image built.");
|
|
15929
|
+
if (child.status === RESTART_FOR_UPDATE) {
|
|
15930
|
+
console.log("[update] relaunching the runner on the new version...");
|
|
15931
|
+
continue;
|
|
15932
|
+
}
|
|
15933
|
+
process.exit(child.signal ? 0 : child.status ?? 0);
|
|
15511
15934
|
}
|
|
15512
|
-
run("docker", ["volume", "create", "factiii-card-cache"], { silent: true });
|
|
15513
|
-
console.log("\n[3/3] Pairing this runner with a Factiii account...");
|
|
15514
|
-
const { token, runnerName } = await pairWithBrowser(serverUrl);
|
|
15515
|
-
writeRunnerConfig({ serverUrl, authToken: token, runnerName });
|
|
15516
|
-
console.log(" Paired and config saved.");
|
|
15517
|
-
console.log("\n=== Setup complete ===\n");
|
|
15518
|
-
console.log("Start the runner with: factiii-runner start\n");
|
|
15519
|
-
}
|
|
15520
|
-
function findDockerfile() {
|
|
15521
|
-
const p = import_path6.default.join(__dirname, "..", "Dockerfile.claude");
|
|
15522
|
-
return import_fs5.default.existsSync(p) ? p : null;
|
|
15523
15935
|
}
|
|
15524
15936
|
|
|
15525
15937
|
// src/cli.ts
|
|
15526
15938
|
import_node_dns.default.setDefaultResultOrder("ipv4first");
|
|
15527
15939
|
var program2 = new Command();
|
|
15528
|
-
program2.name("factiii-runner").description("Factiii Runner - run Board AI agents on remote servers").version(
|
|
15940
|
+
program2.name("factiii-runner").description("Factiii Runner - run Board AI agents on remote servers").version(currentVersion());
|
|
15529
15941
|
program2.command("setup").description("Verify Docker, build the Claude image, and pair this runner").option("-s, --server <url>", "API server URL", "https://api.factiii.com").action(async (opts) => {
|
|
15530
15942
|
try {
|
|
15531
15943
|
await setup(opts.server);
|
|
@@ -15560,12 +15972,7 @@ program2.command("start").description("Start the runner daemon").action(async ()
|
|
|
15560
15972
|
console.error('No config found. Run "factiii-runner setup" first.');
|
|
15561
15973
|
process.exit(1);
|
|
15562
15974
|
}
|
|
15563
|
-
|
|
15564
|
-
if (!config) {
|
|
15565
|
-
console.error('Failed to read config. Run "factiii-runner setup" again.');
|
|
15566
|
-
process.exit(1);
|
|
15567
|
-
}
|
|
15568
|
-
await startDaemon(config);
|
|
15975
|
+
await runSupervised(process.argv.slice(1));
|
|
15569
15976
|
});
|
|
15570
15977
|
program2.command("status").description("Show current runner configuration").action(() => {
|
|
15571
15978
|
if (!configExists()) {
|