@factiii/runner 0.0.1 → 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/Dockerfile.claude +25 -3
- package/dist/cli.js +2633 -838
- package/package.json +3 -1
package/dist/cli.js
CHANGED
|
@@ -30,9 +30,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
30
30
|
mod
|
|
31
31
|
));
|
|
32
32
|
|
|
33
|
-
// node_modules/commander/lib/error.js
|
|
33
|
+
// ../../node_modules/commander/lib/error.js
|
|
34
34
|
var require_error = __commonJS({
|
|
35
|
-
"node_modules/commander/lib/error.js"(exports2) {
|
|
35
|
+
"../../node_modules/commander/lib/error.js"(exports2) {
|
|
36
36
|
var CommanderError2 = class extends Error {
|
|
37
37
|
/**
|
|
38
38
|
* Constructs the CommanderError class
|
|
@@ -65,9 +65,9 @@ var require_error = __commonJS({
|
|
|
65
65
|
}
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
// node_modules/commander/lib/argument.js
|
|
68
|
+
// ../../node_modules/commander/lib/argument.js
|
|
69
69
|
var require_argument = __commonJS({
|
|
70
|
-
"node_modules/commander/lib/argument.js"(exports2) {
|
|
70
|
+
"../../node_modules/commander/lib/argument.js"(exports2) {
|
|
71
71
|
var { InvalidArgumentError: InvalidArgumentError2 } = require_error();
|
|
72
72
|
var Argument2 = class {
|
|
73
73
|
/**
|
|
@@ -192,9 +192,9 @@ var require_argument = __commonJS({
|
|
|
192
192
|
}
|
|
193
193
|
});
|
|
194
194
|
|
|
195
|
-
// node_modules/commander/lib/help.js
|
|
195
|
+
// ../../node_modules/commander/lib/help.js
|
|
196
196
|
var require_help = __commonJS({
|
|
197
|
-
"node_modules/commander/lib/help.js"(exports2) {
|
|
197
|
+
"../../node_modules/commander/lib/help.js"(exports2) {
|
|
198
198
|
var { humanReadableArgName } = require_argument();
|
|
199
199
|
var Help2 = class {
|
|
200
200
|
constructor() {
|
|
@@ -606,9 +606,9 @@ var require_help = __commonJS({
|
|
|
606
606
|
}
|
|
607
607
|
});
|
|
608
608
|
|
|
609
|
-
// node_modules/commander/lib/option.js
|
|
609
|
+
// ../../node_modules/commander/lib/option.js
|
|
610
610
|
var require_option = __commonJS({
|
|
611
|
-
"node_modules/commander/lib/option.js"(exports2) {
|
|
611
|
+
"../../node_modules/commander/lib/option.js"(exports2) {
|
|
612
612
|
var { InvalidArgumentError: InvalidArgumentError2 } = require_error();
|
|
613
613
|
var Option2 = class {
|
|
614
614
|
/**
|
|
@@ -878,9 +878,9 @@ var require_option = __commonJS({
|
|
|
878
878
|
}
|
|
879
879
|
});
|
|
880
880
|
|
|
881
|
-
// node_modules/commander/lib/suggestSimilar.js
|
|
881
|
+
// ../../node_modules/commander/lib/suggestSimilar.js
|
|
882
882
|
var require_suggestSimilar = __commonJS({
|
|
883
|
-
"node_modules/commander/lib/suggestSimilar.js"(exports2) {
|
|
883
|
+
"../../node_modules/commander/lib/suggestSimilar.js"(exports2) {
|
|
884
884
|
var maxDistance = 3;
|
|
885
885
|
function editDistance(a, b) {
|
|
886
886
|
if (Math.abs(a.length - b.length) > maxDistance)
|
|
@@ -958,13 +958,13 @@ var require_suggestSimilar = __commonJS({
|
|
|
958
958
|
}
|
|
959
959
|
});
|
|
960
960
|
|
|
961
|
-
// node_modules/commander/lib/command.js
|
|
961
|
+
// ../../node_modules/commander/lib/command.js
|
|
962
962
|
var require_command = __commonJS({
|
|
963
|
-
"node_modules/commander/lib/command.js"(exports2) {
|
|
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 path7 = require("node:path");
|
|
967
|
+
var fs6 = 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 = path7.resolve(baseDir, baseName);
|
|
1900
|
+
if (fs6.existsSync(localBin)) return localBin;
|
|
1901
|
+
if (sourceExt.includes(path7.extname(baseName))) return void 0;
|
|
1902
1902
|
const foundExt = sourceExt.find(
|
|
1903
|
-
(ext) =>
|
|
1903
|
+
(ext) => fs6.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 = fs6.realpathSync(this._scriptPath);
|
|
1916
1916
|
} catch (err) {
|
|
1917
1917
|
resolvedScriptPath = this._scriptPath;
|
|
1918
1918
|
}
|
|
1919
|
-
executableDir =
|
|
1920
|
-
|
|
1919
|
+
executableDir = path7.resolve(
|
|
1920
|
+
path7.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 = path7.basename(
|
|
1928
1928
|
this._scriptPath,
|
|
1929
|
-
|
|
1929
|
+
path7.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(path7.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 = path7.basename(filename, path7.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(path8) {
|
|
2795
|
+
if (path8 === void 0) return this._executableDir;
|
|
2796
|
+
this._executableDir = path8;
|
|
2797
2797
|
return this;
|
|
2798
2798
|
}
|
|
2799
2799
|
/**
|
|
@@ -3001,9 +3001,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3001
3001
|
}
|
|
3002
3002
|
});
|
|
3003
3003
|
|
|
3004
|
-
// node_modules/commander/index.js
|
|
3004
|
+
// ../../node_modules/commander/index.js
|
|
3005
3005
|
var require_commander = __commonJS({
|
|
3006
|
-
"node_modules/commander/index.js"(exports2) {
|
|
3006
|
+
"../../node_modules/commander/index.js"(exports2) {
|
|
3007
3007
|
var { Argument: Argument2 } = require_argument();
|
|
3008
3008
|
var { Command: Command2 } = require_command();
|
|
3009
3009
|
var { CommanderError: CommanderError2, InvalidArgumentError: InvalidArgumentError2 } = require_error();
|
|
@@ -3026,9 +3026,9 @@ 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 fs6 = require("fs");
|
|
3030
3030
|
var Url = require("url");
|
|
3031
|
-
var
|
|
3031
|
+
var spawn3 = require("child_process").spawn;
|
|
3032
3032
|
module2.exports = XMLHttpRequest3;
|
|
3033
3033
|
XMLHttpRequest3.XMLHttpRequest = XMLHttpRequest3;
|
|
3034
3034
|
function XMLHttpRequest3(opts) {
|
|
@@ -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
|
+
fs6.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 = fs6.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
|
+
fs6.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
|
-
var syncProc =
|
|
3327
|
+
var syncProc = spawn3(process.argv[0], ["-e", execString]);
|
|
3328
3328
|
var statusText;
|
|
3329
|
-
while (
|
|
3329
|
+
while (fs6.existsSync(syncFile)) {
|
|
3330
3330
|
}
|
|
3331
|
-
self.responseText =
|
|
3331
|
+
self.responseText = fs6.readFileSync(contentFile, "utf8");
|
|
3332
3332
|
syncProc.stdin.end();
|
|
3333
|
-
|
|
3333
|
+
fs6.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);
|
|
@@ -3984,7 +3984,7 @@ var require_has_flag = __commonJS({
|
|
|
3984
3984
|
var require_supports_color = __commonJS({
|
|
3985
3985
|
"../../node_modules/debug/node_modules/supports-color/index.js"(exports2, module2) {
|
|
3986
3986
|
"use strict";
|
|
3987
|
-
var
|
|
3987
|
+
var os4 = require("os");
|
|
3988
3988
|
var hasFlag = require_has_flag();
|
|
3989
3989
|
var env = process.env;
|
|
3990
3990
|
var forceColor;
|
|
@@ -4022,7 +4022,7 @@ var require_supports_color = __commonJS({
|
|
|
4022
4022
|
}
|
|
4023
4023
|
const min = forceColor ? 1 : 0;
|
|
4024
4024
|
if (process.platform === "win32") {
|
|
4025
|
-
const osRelease =
|
|
4025
|
+
const osRelease = os4.release().split(".");
|
|
4026
4026
|
if (Number(process.versions.node.split(".")[0]) >= 8 && Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) {
|
|
4027
4027
|
return Number(osRelease[2]) >= 14931 ? 3 : 2;
|
|
4028
4028
|
}
|
|
@@ -4269,6 +4269,7 @@ var require_constants = __commonJS({
|
|
|
4269
4269
|
if (hasBlob) BINARY_TYPES.push("blob");
|
|
4270
4270
|
module2.exports = {
|
|
4271
4271
|
BINARY_TYPES,
|
|
4272
|
+
CLOSE_TIMEOUT: 3e4,
|
|
4272
4273
|
EMPTY_BUFFER: Buffer.alloc(0),
|
|
4273
4274
|
GUID: "258EAFA5-E914-47DA-95CA-C5AB0DC85B11",
|
|
4274
4275
|
hasBlob,
|
|
@@ -4423,7 +4424,7 @@ var require_permessage_deflate = __commonJS({
|
|
|
4423
4424
|
var kBuffers = Symbol("buffers");
|
|
4424
4425
|
var kError = Symbol("error");
|
|
4425
4426
|
var zlibLimiter;
|
|
4426
|
-
var
|
|
4427
|
+
var PerMessageDeflate2 = class {
|
|
4427
4428
|
/**
|
|
4428
4429
|
* Creates a PerMessageDeflate instance.
|
|
4429
4430
|
*
|
|
@@ -4434,6 +4435,9 @@ var require_permessage_deflate = __commonJS({
|
|
|
4434
4435
|
* acknowledge disabling of client context takeover
|
|
4435
4436
|
* @param {Number} [options.concurrencyLimit=10] The number of concurrent
|
|
4436
4437
|
* calls to zlib
|
|
4438
|
+
* @param {Boolean} [options.isServer=false] Create the instance in either
|
|
4439
|
+
* server or client mode
|
|
4440
|
+
* @param {Number} [options.maxPayload=0] The maximum allowed message length
|
|
4437
4441
|
* @param {(Boolean|Number)} [options.serverMaxWindowBits] Request/confirm the
|
|
4438
4442
|
* use of a custom server window size
|
|
4439
4443
|
* @param {Boolean} [options.serverNoContextTakeover=false] Request/accept
|
|
@@ -4444,15 +4448,12 @@ var require_permessage_deflate = __commonJS({
|
|
|
4444
4448
|
* deflate
|
|
4445
4449
|
* @param {Object} [options.zlibInflateOptions] Options to pass to zlib on
|
|
4446
4450
|
* inflate
|
|
4447
|
-
* @param {Boolean} [isServer=false] Create the instance in either server or
|
|
4448
|
-
* client mode
|
|
4449
|
-
* @param {Number} [maxPayload=0] The maximum allowed message length
|
|
4450
4451
|
*/
|
|
4451
|
-
constructor(options
|
|
4452
|
-
this._maxPayload = maxPayload | 0;
|
|
4452
|
+
constructor(options) {
|
|
4453
4453
|
this._options = options || {};
|
|
4454
4454
|
this._threshold = this._options.threshold !== void 0 ? this._options.threshold : 1024;
|
|
4455
|
-
this.
|
|
4455
|
+
this._maxPayload = this._options.maxPayload | 0;
|
|
4456
|
+
this._isServer = !!this._options.isServer;
|
|
4456
4457
|
this._deflate = null;
|
|
4457
4458
|
this._inflate = null;
|
|
4458
4459
|
this.params = null;
|
|
@@ -4761,7 +4762,7 @@ var require_permessage_deflate = __commonJS({
|
|
|
4761
4762
|
});
|
|
4762
4763
|
}
|
|
4763
4764
|
};
|
|
4764
|
-
module2.exports =
|
|
4765
|
+
module2.exports = PerMessageDeflate2;
|
|
4765
4766
|
function deflateOnData(chunk) {
|
|
4766
4767
|
this[kBuffers].push(chunk);
|
|
4767
4768
|
this[kTotalLength] += chunk.length;
|
|
@@ -4996,7 +4997,7 @@ var require_receiver = __commonJS({
|
|
|
4996
4997
|
"../../node_modules/engine.io-client/node_modules/ws/lib/receiver.js"(exports2, module2) {
|
|
4997
4998
|
"use strict";
|
|
4998
4999
|
var { Writable } = require("stream");
|
|
4999
|
-
var
|
|
5000
|
+
var PerMessageDeflate2 = require_permessage_deflate();
|
|
5000
5001
|
var {
|
|
5001
5002
|
BINARY_TYPES,
|
|
5002
5003
|
EMPTY_BUFFER,
|
|
@@ -5163,7 +5164,7 @@ var require_receiver = __commonJS({
|
|
|
5163
5164
|
return;
|
|
5164
5165
|
}
|
|
5165
5166
|
const compressed = (buf[0] & 64) === 64;
|
|
5166
|
-
if (compressed && !this._extensions[
|
|
5167
|
+
if (compressed && !this._extensions[PerMessageDeflate2.extensionName]) {
|
|
5167
5168
|
const error = this.createError(
|
|
5168
5169
|
RangeError,
|
|
5169
5170
|
"RSV1 must be clear",
|
|
@@ -5407,7 +5408,7 @@ var require_receiver = __commonJS({
|
|
|
5407
5408
|
* @private
|
|
5408
5409
|
*/
|
|
5409
5410
|
decompress(data, cb) {
|
|
5410
|
-
const perMessageDeflate = this._extensions[
|
|
5411
|
+
const perMessageDeflate = this._extensions[PerMessageDeflate2.extensionName];
|
|
5411
5412
|
perMessageDeflate.decompress(data, this._fin, (err, buf) => {
|
|
5412
5413
|
if (err) return cb(err);
|
|
5413
5414
|
if (buf.length) {
|
|
@@ -5589,7 +5590,10 @@ var require_sender = __commonJS({
|
|
|
5589
5590
|
"use strict";
|
|
5590
5591
|
var { Duplex } = require("stream");
|
|
5591
5592
|
var { randomFillSync } = require("crypto");
|
|
5592
|
-
var
|
|
5593
|
+
var {
|
|
5594
|
+
types: { isUint8Array }
|
|
5595
|
+
} = require("util");
|
|
5596
|
+
var PerMessageDeflate2 = require_permessage_deflate();
|
|
5593
5597
|
var { EMPTY_BUFFER, kWebSocket, NOOP } = require_constants();
|
|
5594
5598
|
var { isBlob, isValidStatusCode } = require_validation();
|
|
5595
5599
|
var { mask: applyMask, toBuffer: toBuffer2 } = require_buffer_util();
|
|
@@ -5742,8 +5746,10 @@ var require_sender = __commonJS({
|
|
|
5742
5746
|
buf.writeUInt16BE(code, 0);
|
|
5743
5747
|
if (typeof data === "string") {
|
|
5744
5748
|
buf.write(data, 2);
|
|
5745
|
-
} else {
|
|
5749
|
+
} else if (isUint8Array(data)) {
|
|
5746
5750
|
buf.set(data, 2);
|
|
5751
|
+
} else {
|
|
5752
|
+
throw new TypeError("Second argument must be a string or a Uint8Array");
|
|
5747
5753
|
}
|
|
5748
5754
|
}
|
|
5749
5755
|
const options = {
|
|
@@ -5873,7 +5879,7 @@ var require_sender = __commonJS({
|
|
|
5873
5879
|
* @public
|
|
5874
5880
|
*/
|
|
5875
5881
|
send(data, options, cb) {
|
|
5876
|
-
const perMessageDeflate = this._extensions[
|
|
5882
|
+
const perMessageDeflate = this._extensions[PerMessageDeflate2.extensionName];
|
|
5877
5883
|
let opcode = options.binary ? 2 : 1;
|
|
5878
5884
|
let rsv1 = options.compress;
|
|
5879
5885
|
let byteLength2;
|
|
@@ -5997,7 +6003,7 @@ var require_sender = __commonJS({
|
|
|
5997
6003
|
this.sendFrame(_Sender.frame(data, options), cb);
|
|
5998
6004
|
return;
|
|
5999
6005
|
}
|
|
6000
|
-
const perMessageDeflate = this._extensions[
|
|
6006
|
+
const perMessageDeflate = this._extensions[PerMessageDeflate2.extensionName];
|
|
6001
6007
|
this._bufferedBytes += options[kByteLength];
|
|
6002
6008
|
this._state = DEFLATING;
|
|
6003
6009
|
perMessageDeflate.compress(data, options.fin, (_, buf) => {
|
|
@@ -6435,11 +6441,11 @@ var require_extension = __commonJS({
|
|
|
6435
6441
|
return offers;
|
|
6436
6442
|
}
|
|
6437
6443
|
function format(extensions) {
|
|
6438
|
-
return Object.keys(extensions).map((
|
|
6439
|
-
let configurations = extensions[
|
|
6444
|
+
return Object.keys(extensions).map((extension2) => {
|
|
6445
|
+
let configurations = extensions[extension2];
|
|
6440
6446
|
if (!Array.isArray(configurations)) configurations = [configurations];
|
|
6441
6447
|
return configurations.map((params) => {
|
|
6442
|
-
return [
|
|
6448
|
+
return [extension2].concat(
|
|
6443
6449
|
Object.keys(params).map((k) => {
|
|
6444
6450
|
let values = params[k];
|
|
6445
6451
|
if (!Array.isArray(values)) values = [values];
|
|
@@ -6465,12 +6471,13 @@ var require_websocket = __commonJS({
|
|
|
6465
6471
|
var { randomBytes, createHash } = require("crypto");
|
|
6466
6472
|
var { Duplex, Readable } = require("stream");
|
|
6467
6473
|
var { URL: URL2 } = require("url");
|
|
6468
|
-
var
|
|
6474
|
+
var PerMessageDeflate2 = require_permessage_deflate();
|
|
6469
6475
|
var Receiver2 = require_receiver();
|
|
6470
6476
|
var Sender2 = require_sender();
|
|
6471
6477
|
var { isBlob } = require_validation();
|
|
6472
6478
|
var {
|
|
6473
6479
|
BINARY_TYPES,
|
|
6480
|
+
CLOSE_TIMEOUT,
|
|
6474
6481
|
EMPTY_BUFFER,
|
|
6475
6482
|
GUID,
|
|
6476
6483
|
kForOnEventAttribute,
|
|
@@ -6484,7 +6491,6 @@ var require_websocket = __commonJS({
|
|
|
6484
6491
|
} = require_event_target();
|
|
6485
6492
|
var { format, parse: parse3 } = require_extension();
|
|
6486
6493
|
var { toBuffer: toBuffer2 } = require_buffer_util();
|
|
6487
|
-
var closeTimeout = 30 * 1e3;
|
|
6488
6494
|
var kAborted = Symbol("kAborted");
|
|
6489
6495
|
var protocolVersions = [8, 13];
|
|
6490
6496
|
var readyStates = ["CONNECTING", "OPEN", "CLOSING", "CLOSED"];
|
|
@@ -6530,6 +6536,7 @@ var require_websocket = __commonJS({
|
|
|
6530
6536
|
initAsClient(this, address, protocols, options);
|
|
6531
6537
|
} else {
|
|
6532
6538
|
this._autoPong = options.autoPong;
|
|
6539
|
+
this._closeTimeout = options.closeTimeout;
|
|
6533
6540
|
this._isServer = true;
|
|
6534
6541
|
}
|
|
6535
6542
|
}
|
|
@@ -6672,8 +6679,8 @@ var require_websocket = __commonJS({
|
|
|
6672
6679
|
this.emit("close", this._closeCode, this._closeMessage);
|
|
6673
6680
|
return;
|
|
6674
6681
|
}
|
|
6675
|
-
if (this._extensions[
|
|
6676
|
-
this._extensions[
|
|
6682
|
+
if (this._extensions[PerMessageDeflate2.extensionName]) {
|
|
6683
|
+
this._extensions[PerMessageDeflate2.extensionName].cleanup();
|
|
6677
6684
|
}
|
|
6678
6685
|
this._receiver.removeAllListeners();
|
|
6679
6686
|
this._readyState = _WebSocket.CLOSED;
|
|
@@ -6835,7 +6842,7 @@ var require_websocket = __commonJS({
|
|
|
6835
6842
|
fin: true,
|
|
6836
6843
|
...options
|
|
6837
6844
|
};
|
|
6838
|
-
if (!this._extensions[
|
|
6845
|
+
if (!this._extensions[PerMessageDeflate2.extensionName]) {
|
|
6839
6846
|
opts.compress = false;
|
|
6840
6847
|
}
|
|
6841
6848
|
this._sender.send(data || EMPTY_BUFFER, opts, cb);
|
|
@@ -6931,6 +6938,7 @@ var require_websocket = __commonJS({
|
|
|
6931
6938
|
const opts = {
|
|
6932
6939
|
allowSynchronousEvents: true,
|
|
6933
6940
|
autoPong: true,
|
|
6941
|
+
closeTimeout: CLOSE_TIMEOUT,
|
|
6934
6942
|
protocolVersion: protocolVersions[1],
|
|
6935
6943
|
maxPayload: 100 * 1024 * 1024,
|
|
6936
6944
|
skipUTF8Validation: false,
|
|
@@ -6948,6 +6956,7 @@ var require_websocket = __commonJS({
|
|
|
6948
6956
|
port: void 0
|
|
6949
6957
|
};
|
|
6950
6958
|
websocket._autoPong = opts.autoPong;
|
|
6959
|
+
websocket._closeTimeout = opts.closeTimeout;
|
|
6951
6960
|
if (!protocolVersions.includes(opts.protocolVersion)) {
|
|
6952
6961
|
throw new RangeError(
|
|
6953
6962
|
`Unsupported protocol version: ${opts.protocolVersion} (supported versions: ${protocolVersions.join(", ")})`
|
|
@@ -6959,7 +6968,7 @@ var require_websocket = __commonJS({
|
|
|
6959
6968
|
} else {
|
|
6960
6969
|
try {
|
|
6961
6970
|
parsedUrl = new URL2(address);
|
|
6962
|
-
} catch
|
|
6971
|
+
} catch {
|
|
6963
6972
|
throw new SyntaxError(`Invalid URL: ${address}`);
|
|
6964
6973
|
}
|
|
6965
6974
|
}
|
|
@@ -7007,13 +7016,13 @@ var require_websocket = __commonJS({
|
|
|
7007
7016
|
opts.path = parsedUrl.pathname + parsedUrl.search;
|
|
7008
7017
|
opts.timeout = opts.handshakeTimeout;
|
|
7009
7018
|
if (opts.perMessageDeflate) {
|
|
7010
|
-
perMessageDeflate = new
|
|
7011
|
-
opts.perMessageDeflate
|
|
7012
|
-
false,
|
|
7013
|
-
opts.maxPayload
|
|
7014
|
-
);
|
|
7019
|
+
perMessageDeflate = new PerMessageDeflate2({
|
|
7020
|
+
...opts.perMessageDeflate,
|
|
7021
|
+
isServer: false,
|
|
7022
|
+
maxPayload: opts.maxPayload
|
|
7023
|
+
});
|
|
7015
7024
|
opts.headers["Sec-WebSocket-Extensions"] = format({
|
|
7016
|
-
[
|
|
7025
|
+
[PerMessageDeflate2.extensionName]: perMessageDeflate.offer()
|
|
7017
7026
|
});
|
|
7018
7027
|
}
|
|
7019
7028
|
if (protocols.length) {
|
|
@@ -7156,19 +7165,19 @@ var require_websocket = __commonJS({
|
|
|
7156
7165
|
return;
|
|
7157
7166
|
}
|
|
7158
7167
|
const extensionNames = Object.keys(extensions);
|
|
7159
|
-
if (extensionNames.length !== 1 || extensionNames[0] !==
|
|
7168
|
+
if (extensionNames.length !== 1 || extensionNames[0] !== PerMessageDeflate2.extensionName) {
|
|
7160
7169
|
const message = "Server indicated an extension that was not requested";
|
|
7161
7170
|
abortHandshake(websocket, socket, message);
|
|
7162
7171
|
return;
|
|
7163
7172
|
}
|
|
7164
7173
|
try {
|
|
7165
|
-
perMessageDeflate.accept(extensions[
|
|
7174
|
+
perMessageDeflate.accept(extensions[PerMessageDeflate2.extensionName]);
|
|
7166
7175
|
} catch (err) {
|
|
7167
7176
|
const message = "Invalid Sec-WebSocket-Extensions header";
|
|
7168
7177
|
abortHandshake(websocket, socket, message);
|
|
7169
7178
|
return;
|
|
7170
7179
|
}
|
|
7171
|
-
websocket._extensions[
|
|
7180
|
+
websocket._extensions[PerMessageDeflate2.extensionName] = perMessageDeflate;
|
|
7172
7181
|
}
|
|
7173
7182
|
websocket.setSocket(socket, head, {
|
|
7174
7183
|
allowSynchronousEvents: opts.allowSynchronousEvents,
|
|
@@ -7290,7 +7299,7 @@ var require_websocket = __commonJS({
|
|
|
7290
7299
|
function setCloseTimer(websocket) {
|
|
7291
7300
|
websocket._closeTimer = setTimeout(
|
|
7292
7301
|
websocket._socket.destroy.bind(websocket._socket),
|
|
7293
|
-
|
|
7302
|
+
websocket._closeTimeout
|
|
7294
7303
|
);
|
|
7295
7304
|
}
|
|
7296
7305
|
function socketOnClose() {
|
|
@@ -7299,8 +7308,8 @@ var require_websocket = __commonJS({
|
|
|
7299
7308
|
this.removeListener("data", socketOnData);
|
|
7300
7309
|
this.removeListener("end", socketOnEnd);
|
|
7301
7310
|
websocket._readyState = WebSocket2.CLOSING;
|
|
7302
|
-
|
|
7303
|
-
|
|
7311
|
+
if (!this._readableState.endEmitted && !websocket._closeFrameReceived && !websocket._receiver._writableState.errorEmitted && this._readableState.length !== 0) {
|
|
7312
|
+
const chunk = this.read(this._readableState.length);
|
|
7304
7313
|
websocket._receiver.write(chunk);
|
|
7305
7314
|
}
|
|
7306
7315
|
websocket._receiver.end();
|
|
@@ -7487,11 +7496,11 @@ var require_websocket_server = __commonJS({
|
|
|
7487
7496
|
var http = require("http");
|
|
7488
7497
|
var { Duplex } = require("stream");
|
|
7489
7498
|
var { createHash } = require("crypto");
|
|
7490
|
-
var
|
|
7491
|
-
var
|
|
7492
|
-
var
|
|
7499
|
+
var extension2 = require_extension();
|
|
7500
|
+
var PerMessageDeflate2 = require_permessage_deflate();
|
|
7501
|
+
var subprotocol2 = require_subprotocol();
|
|
7493
7502
|
var WebSocket2 = require_websocket();
|
|
7494
|
-
var { GUID, kWebSocket } = require_constants();
|
|
7503
|
+
var { CLOSE_TIMEOUT, GUID, kWebSocket } = require_constants();
|
|
7495
7504
|
var keyRegex = /^[+/0-9A-Za-z]{22}==$/;
|
|
7496
7505
|
var RUNNING = 0;
|
|
7497
7506
|
var CLOSING = 1;
|
|
@@ -7510,6 +7519,9 @@ var require_websocket_server = __commonJS({
|
|
|
7510
7519
|
* pending connections
|
|
7511
7520
|
* @param {Boolean} [options.clientTracking=true] Specifies whether or not to
|
|
7512
7521
|
* track clients
|
|
7522
|
+
* @param {Number} [options.closeTimeout=30000] Duration in milliseconds to
|
|
7523
|
+
* wait for the closing handshake to finish after `websocket.close()` is
|
|
7524
|
+
* called
|
|
7513
7525
|
* @param {Function} [options.handleProtocols] A hook to handle protocols
|
|
7514
7526
|
* @param {String} [options.host] The hostname where to bind the server
|
|
7515
7527
|
* @param {Number} [options.maxPayload=104857600] The maximum allowed message
|
|
@@ -7538,6 +7550,7 @@ var require_websocket_server = __commonJS({
|
|
|
7538
7550
|
perMessageDeflate: false,
|
|
7539
7551
|
handleProtocols: null,
|
|
7540
7552
|
clientTracking: true,
|
|
7553
|
+
closeTimeout: CLOSE_TIMEOUT,
|
|
7541
7554
|
verifyClient: null,
|
|
7542
7555
|
noServer: false,
|
|
7543
7556
|
backlog: null,
|
|
@@ -7708,7 +7721,7 @@ var require_websocket_server = __commonJS({
|
|
|
7708
7721
|
let protocols = /* @__PURE__ */ new Set();
|
|
7709
7722
|
if (secWebSocketProtocol !== void 0) {
|
|
7710
7723
|
try {
|
|
7711
|
-
protocols =
|
|
7724
|
+
protocols = subprotocol2.parse(secWebSocketProtocol);
|
|
7712
7725
|
} catch (err) {
|
|
7713
7726
|
const message = "Invalid Sec-WebSocket-Protocol header";
|
|
7714
7727
|
abortHandshakeOrEmitwsClientError(this, req, socket, 400, message);
|
|
@@ -7718,16 +7731,16 @@ var require_websocket_server = __commonJS({
|
|
|
7718
7731
|
const secWebSocketExtensions = req.headers["sec-websocket-extensions"];
|
|
7719
7732
|
const extensions = {};
|
|
7720
7733
|
if (this.options.perMessageDeflate && secWebSocketExtensions !== void 0) {
|
|
7721
|
-
const perMessageDeflate = new
|
|
7722
|
-
this.options.perMessageDeflate,
|
|
7723
|
-
true,
|
|
7724
|
-
this.options.maxPayload
|
|
7725
|
-
);
|
|
7734
|
+
const perMessageDeflate = new PerMessageDeflate2({
|
|
7735
|
+
...this.options.perMessageDeflate,
|
|
7736
|
+
isServer: true,
|
|
7737
|
+
maxPayload: this.options.maxPayload
|
|
7738
|
+
});
|
|
7726
7739
|
try {
|
|
7727
|
-
const offers =
|
|
7728
|
-
if (offers[
|
|
7729
|
-
perMessageDeflate.accept(offers[
|
|
7730
|
-
extensions[
|
|
7740
|
+
const offers = extension2.parse(secWebSocketExtensions);
|
|
7741
|
+
if (offers[PerMessageDeflate2.extensionName]) {
|
|
7742
|
+
perMessageDeflate.accept(offers[PerMessageDeflate2.extensionName]);
|
|
7743
|
+
extensions[PerMessageDeflate2.extensionName] = perMessageDeflate;
|
|
7731
7744
|
}
|
|
7732
7745
|
} catch (err) {
|
|
7733
7746
|
const message = "Invalid or unacceptable Sec-WebSocket-Extensions header";
|
|
@@ -7798,10 +7811,10 @@ var require_websocket_server = __commonJS({
|
|
|
7798
7811
|
ws._protocol = protocol4;
|
|
7799
7812
|
}
|
|
7800
7813
|
}
|
|
7801
|
-
if (extensions[
|
|
7802
|
-
const params = extensions[
|
|
7803
|
-
const value2 =
|
|
7804
|
-
[
|
|
7814
|
+
if (extensions[PerMessageDeflate2.extensionName]) {
|
|
7815
|
+
const params = extensions[PerMessageDeflate2.extensionName].params;
|
|
7816
|
+
const value2 = extension2.format({
|
|
7817
|
+
[PerMessageDeflate2.extensionName]: [params]
|
|
7805
7818
|
});
|
|
7806
7819
|
headers.push(`Sec-WebSocket-Extensions: ${value2}`);
|
|
7807
7820
|
ws._extensions = extensions;
|
|
@@ -7868,7 +7881,10 @@ var require_websocket_server = __commonJS({
|
|
|
7868
7881
|
}
|
|
7869
7882
|
});
|
|
7870
7883
|
|
|
7871
|
-
//
|
|
7884
|
+
// src/cli.ts
|
|
7885
|
+
var import_node_dns = __toESM(require("node:dns"));
|
|
7886
|
+
|
|
7887
|
+
// ../../node_modules/commander/esm.mjs
|
|
7872
7888
|
var import_index = __toESM(require_commander(), 1);
|
|
7873
7889
|
var {
|
|
7874
7890
|
program,
|
|
@@ -7963,10 +7979,10 @@ function configExists() {
|
|
|
7963
7979
|
return import_fs.default.existsSync(CONFIG_FILE);
|
|
7964
7980
|
}
|
|
7965
7981
|
|
|
7966
|
-
// ../../shared/all/helpers/
|
|
7982
|
+
// ../../shared/all/helpers/board-agent-core/claude.ts
|
|
7967
7983
|
var import_child_process2 = require("child_process");
|
|
7968
7984
|
|
|
7969
|
-
// ../../shared/all/helpers/
|
|
7985
|
+
// ../../shared/all/helpers/board-agent-core/claude-stream.ts
|
|
7970
7986
|
function truncate(s, max) {
|
|
7971
7987
|
return s.length > max ? s.slice(0, max) + "..." : s;
|
|
7972
7988
|
}
|
|
@@ -8221,7 +8237,7 @@ function pipeClaudeStream(proc, callbacks) {
|
|
|
8221
8237
|
};
|
|
8222
8238
|
}
|
|
8223
8239
|
|
|
8224
|
-
// ../../shared/all/helpers/
|
|
8240
|
+
// ../../shared/all/helpers/board-agent-core/prompts.json
|
|
8225
8241
|
var prompts_default = {
|
|
8226
8242
|
system: [
|
|
8227
8243
|
"You are a coding agent working inside a Docker container on a task from a project management board.",
|
|
@@ -8231,7 +8247,7 @@ var prompts_default = {
|
|
|
8231
8247
|
"2. **Implement** \u2014 Execute the plan. You gain Edit, Write, and Bash tools. Make the code changes.",
|
|
8232
8248
|
"3. **Review** \u2014 Validate your changes, fix issues, generate reproduction steps for the developer.",
|
|
8233
8249
|
"",
|
|
8234
|
-
"
|
|
8250
|
+
"After Review, the developer may request an optional **Preview** where you set up a live dev environment on demand.",
|
|
8235
8251
|
"",
|
|
8236
8252
|
"## Task Tracking",
|
|
8237
8253
|
"Use the TodoWrite tool to track your progress. Create todos at the start of each phase with your planned steps,",
|
|
@@ -8394,7 +8410,7 @@ var prompts_default = {
|
|
|
8394
8410
|
commitMessage: 'Generate a single-line conventional commit message (e.g. "feat: ...", "fix: ...", "refactor: ...") for these changes. Output ONLY the commit message, nothing else.'
|
|
8395
8411
|
};
|
|
8396
8412
|
|
|
8397
|
-
// ../../shared/all/helpers/
|
|
8413
|
+
// ../../shared/all/helpers/board-agent-core/claude.ts
|
|
8398
8414
|
function spawnClaude(opts) {
|
|
8399
8415
|
const args = [
|
|
8400
8416
|
"exec",
|
|
@@ -8494,11 +8510,18 @@ ${diff.slice(0, 8e3)}`);
|
|
|
8494
8510
|
});
|
|
8495
8511
|
}
|
|
8496
8512
|
|
|
8497
|
-
// ../../shared/all/helpers/
|
|
8513
|
+
// ../../shared/all/helpers/board-agent-core/docker.ts
|
|
8498
8514
|
var import_child_process3 = require("child_process");
|
|
8515
|
+
var import_crypto = __toESM(require("crypto"));
|
|
8516
|
+
var import_promises = __toESM(require("fs/promises"));
|
|
8499
8517
|
var import_net = __toESM(require("net"));
|
|
8500
8518
|
var import_util = require("util");
|
|
8501
8519
|
var execFileAsync = (0, import_util.promisify)(import_child_process3.execFile);
|
|
8520
|
+
var DOCKERFILE_LABEL = "factiii.dockerfile-sha";
|
|
8521
|
+
async function hashDockerfile(dockerfilePath) {
|
|
8522
|
+
const buf = await import_promises.default.readFile(dockerfilePath);
|
|
8523
|
+
return import_crypto.default.createHash("sha256").update(buf).digest("hex");
|
|
8524
|
+
}
|
|
8502
8525
|
async function getFreePort() {
|
|
8503
8526
|
return new Promise((resolve, reject) => {
|
|
8504
8527
|
const srv = import_net.default.createServer();
|
|
@@ -8524,28 +8547,105 @@ async function assertDockerRunning() {
|
|
|
8524
8547
|
);
|
|
8525
8548
|
}
|
|
8526
8549
|
}
|
|
8527
|
-
|
|
8550
|
+
var BUILD_TIMEOUT_MS = 9e5;
|
|
8551
|
+
async function ensureDockerImage(dockerfilePath, onLog) {
|
|
8528
8552
|
await assertDockerRunning();
|
|
8553
|
+
const wantedHash = await hashDockerfile(dockerfilePath);
|
|
8529
8554
|
try {
|
|
8530
|
-
|
|
8531
|
-
|
|
8555
|
+
const { stdout } = await execFileAsync(
|
|
8556
|
+
"docker",
|
|
8557
|
+
[
|
|
8558
|
+
"image",
|
|
8559
|
+
"inspect",
|
|
8560
|
+
"factiii-claude",
|
|
8561
|
+
"--format",
|
|
8562
|
+
`{{ index .Config.Labels "${DOCKERFILE_LABEL}" }}`
|
|
8563
|
+
],
|
|
8564
|
+
{}
|
|
8565
|
+
);
|
|
8566
|
+
if (stdout.trim() === wantedHash) return;
|
|
8532
8567
|
} catch {
|
|
8533
8568
|
}
|
|
8534
|
-
await
|
|
8535
|
-
|
|
8536
|
-
["build", "-t", "factiii-claude", "-f", dockerfilePath, "."],
|
|
8537
|
-
{ timeout: 3e5 }
|
|
8538
|
-
);
|
|
8569
|
+
await buildImageStreaming(dockerfilePath, wantedHash, onLog);
|
|
8570
|
+
await resetNixVolume();
|
|
8539
8571
|
}
|
|
8540
|
-
async function
|
|
8572
|
+
async function resetNixVolume() {
|
|
8541
8573
|
try {
|
|
8542
|
-
await execFileAsync(
|
|
8574
|
+
const { stdout } = await execFileAsync("docker", [
|
|
8575
|
+
"ps",
|
|
8576
|
+
"-aq",
|
|
8577
|
+
"--filter",
|
|
8578
|
+
"volume=factiii-nix"
|
|
8579
|
+
]);
|
|
8580
|
+
const ids = stdout.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
8581
|
+
if (ids.length) await execFileAsync("docker", ["rm", "-f", ...ids]);
|
|
8582
|
+
await execFileAsync("docker", ["volume", "rm", "factiii-nix"]);
|
|
8583
|
+
} catch {
|
|
8584
|
+
}
|
|
8585
|
+
}
|
|
8586
|
+
function buildImageStreaming(dockerfilePath, wantedHash, onLog) {
|
|
8587
|
+
return new Promise((resolve, reject) => {
|
|
8588
|
+
const child = (0, import_child_process3.spawn)(
|
|
8543
8589
|
"docker",
|
|
8544
|
-
[
|
|
8545
|
-
|
|
8590
|
+
[
|
|
8591
|
+
"build",
|
|
8592
|
+
"--progress=plain",
|
|
8593
|
+
"-t",
|
|
8594
|
+
"factiii-claude",
|
|
8595
|
+
"-f",
|
|
8596
|
+
dockerfilePath,
|
|
8597
|
+
"--label",
|
|
8598
|
+
`${DOCKERFILE_LABEL}=${wantedHash}`,
|
|
8599
|
+
"."
|
|
8600
|
+
],
|
|
8601
|
+
{ env: { ...process.env, DOCKER_BUILDKIT: "1" } }
|
|
8546
8602
|
);
|
|
8547
|
-
|
|
8548
|
-
|
|
8603
|
+
let timer;
|
|
8604
|
+
const arm = () => {
|
|
8605
|
+
clearTimeout(timer);
|
|
8606
|
+
timer = setTimeout(() => {
|
|
8607
|
+
child.kill("SIGKILL");
|
|
8608
|
+
reject(
|
|
8609
|
+
new Error(
|
|
8610
|
+
"Building container image timed out. Check Docker and your connection, then Try Again."
|
|
8611
|
+
)
|
|
8612
|
+
);
|
|
8613
|
+
}, BUILD_TIMEOUT_MS);
|
|
8614
|
+
};
|
|
8615
|
+
arm();
|
|
8616
|
+
const makeLineEmitter = () => {
|
|
8617
|
+
let buffer = "";
|
|
8618
|
+
return (buf) => {
|
|
8619
|
+
arm();
|
|
8620
|
+
buffer += buf.toString();
|
|
8621
|
+
const parts2 = buffer.split("\n");
|
|
8622
|
+
buffer = parts2.pop() ?? "";
|
|
8623
|
+
for (const line of parts2) {
|
|
8624
|
+
const trimmed = line.trimEnd();
|
|
8625
|
+
if (trimmed) onLog?.(trimmed);
|
|
8626
|
+
}
|
|
8627
|
+
};
|
|
8628
|
+
};
|
|
8629
|
+
child.stdout.on("data", makeLineEmitter());
|
|
8630
|
+
child.stderr.on("data", makeLineEmitter());
|
|
8631
|
+
child.on("error", (err) => {
|
|
8632
|
+
clearTimeout(timer);
|
|
8633
|
+
reject(err);
|
|
8634
|
+
});
|
|
8635
|
+
child.on("close", (code) => {
|
|
8636
|
+
clearTimeout(timer);
|
|
8637
|
+
if (code === 0) resolve();
|
|
8638
|
+
else reject(new Error(`docker build failed (exit code ${code}).`));
|
|
8639
|
+
});
|
|
8640
|
+
});
|
|
8641
|
+
}
|
|
8642
|
+
async function ensureCacheVolume() {
|
|
8643
|
+
for (const name of ["factiii-card-cache", "factiii-nix"]) {
|
|
8644
|
+
try {
|
|
8645
|
+
await execFileAsync("docker", ["volume", "inspect", name], {});
|
|
8646
|
+
} catch {
|
|
8647
|
+
await execFileAsync("docker", ["volume", "create", name]);
|
|
8648
|
+
}
|
|
8549
8649
|
}
|
|
8550
8650
|
}
|
|
8551
8651
|
async function killContainer(containerName) {
|
|
@@ -8554,7 +8654,7 @@ async function killContainer(containerName) {
|
|
|
8554
8654
|
} catch {
|
|
8555
8655
|
}
|
|
8556
8656
|
}
|
|
8557
|
-
async function spawnContainer(containerName, claudeToken) {
|
|
8657
|
+
async function spawnContainer(containerName, claudeToken, opts) {
|
|
8558
8658
|
const NUM_PORTS = 6;
|
|
8559
8659
|
const ports = [];
|
|
8560
8660
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -8575,10 +8675,22 @@ async function spawnContainer(containerName, claudeToken) {
|
|
|
8575
8675
|
containerName,
|
|
8576
8676
|
"--user",
|
|
8577
8677
|
"claude",
|
|
8678
|
+
// SYS_PTRACE: gdb/strace/valgrind (Docker seccomp gates ptrace on it).
|
|
8679
|
+
// NET_RAW: raw-socket tools (tcpdump/nmap/ping). Scoped to the container.
|
|
8680
|
+
"--cap-add",
|
|
8681
|
+
"SYS_PTRACE",
|
|
8682
|
+
"--cap-add",
|
|
8683
|
+
"NET_RAW",
|
|
8684
|
+
// FUSE for rclone mount on OneDrive spaces (gated - GitHub stays unprivileged).
|
|
8685
|
+
...opts?.fuse ? ["--cap-add", "SYS_ADMIN", "--device", "/dev/fuse"] : [],
|
|
8578
8686
|
"-e",
|
|
8579
8687
|
"HOME=/home/claude",
|
|
8688
|
+
...claudeToken ? ["-e", `CLAUDE_CODE_OAUTH_TOKEN=${claudeToken}`] : [],
|
|
8689
|
+
// Redirect the otherwise-`~/.claude.json` config file into the .claude
|
|
8690
|
+
// dir, which is symlinked to /cache/claude. Keeps all claude state
|
|
8691
|
+
// inside one path the CLI already understands.
|
|
8580
8692
|
"-e",
|
|
8581
|
-
|
|
8693
|
+
"CLAUDE_CONFIG_DIR=/home/claude/.claude",
|
|
8582
8694
|
"-e",
|
|
8583
8695
|
"NODE_OPTIONS=--max-old-space-size=4096",
|
|
8584
8696
|
"-e",
|
|
@@ -8597,6 +8709,8 @@ async function spawnContainer(containerName, claudeToken) {
|
|
|
8597
8709
|
`FACTIII_PORTS=${ports.join(",")}`,
|
|
8598
8710
|
"-v",
|
|
8599
8711
|
"factiii-card-cache:/cache",
|
|
8712
|
+
"-v",
|
|
8713
|
+
"factiii-nix:/nix",
|
|
8600
8714
|
...portArgs,
|
|
8601
8715
|
"factiii-claude",
|
|
8602
8716
|
"tail",
|
|
@@ -8613,6 +8727,37 @@ async function execInContainer(containerName, cmd) {
|
|
|
8613
8727
|
);
|
|
8614
8728
|
return stdout;
|
|
8615
8729
|
}
|
|
8730
|
+
function streamExecInContainer(containerName, cmd, onLine) {
|
|
8731
|
+
const child = (0, import_child_process3.spawn)("docker", [
|
|
8732
|
+
"exec",
|
|
8733
|
+
"--user",
|
|
8734
|
+
"claude",
|
|
8735
|
+
containerName,
|
|
8736
|
+
"sh",
|
|
8737
|
+
"-c",
|
|
8738
|
+
cmd
|
|
8739
|
+
]);
|
|
8740
|
+
let buffer = "";
|
|
8741
|
+
child.stdout.on("data", (buf) => {
|
|
8742
|
+
buffer += buf.toString();
|
|
8743
|
+
const parts2 = buffer.split("\n");
|
|
8744
|
+
buffer = parts2.pop() ?? "";
|
|
8745
|
+
for (const line of parts2) {
|
|
8746
|
+
const trimmed = line.trimEnd();
|
|
8747
|
+
if (trimmed) onLine(trimmed);
|
|
8748
|
+
}
|
|
8749
|
+
});
|
|
8750
|
+
child.on("error", () => {
|
|
8751
|
+
});
|
|
8752
|
+
let stopped = false;
|
|
8753
|
+
return {
|
|
8754
|
+
stop: () => {
|
|
8755
|
+
if (stopped) return;
|
|
8756
|
+
stopped = true;
|
|
8757
|
+
child.kill("SIGKILL");
|
|
8758
|
+
}
|
|
8759
|
+
};
|
|
8760
|
+
}
|
|
8616
8761
|
async function execInContainerAsRoot(containerName, cmd) {
|
|
8617
8762
|
const { stdout } = await execFileAsync(
|
|
8618
8763
|
"docker",
|
|
@@ -8624,15 +8769,19 @@ async function execInContainerAsRoot(containerName, cmd) {
|
|
|
8624
8769
|
async function setupContainerCache(containerName) {
|
|
8625
8770
|
await execInContainer(
|
|
8626
8771
|
containerName,
|
|
8627
|
-
"mkdir -p /cache/repos /cache/claude /cache/pnpm-store /cache/pnpm-vstore /cache/pip-cache /cache/npm-cache /cache/
|
|
8772
|
+
"mkdir -p /cache/repos /cache/claude /cache/pnpm-store /cache/pnpm-vstore /cache/pip-cache /cache/npm-cache /cache/apt /cache/xdg-cache"
|
|
8628
8773
|
);
|
|
8629
8774
|
await execInContainer(
|
|
8630
8775
|
containerName,
|
|
8631
8776
|
"ln -sfn /cache/claude /home/claude/.claude"
|
|
8632
8777
|
);
|
|
8778
|
+
await execInContainer(
|
|
8779
|
+
containerName,
|
|
8780
|
+
"printf 'set -g mouse on\\nset -g history-limit 50000\\nset -g set-clipboard on\\nset -ga terminal-features ,*:clipboard\\nbind -T copy-mode MouseDragEnd1Pane send -X copy-selection-and-cancel\\nbind -T copy-mode-vi MouseDragEnd1Pane send -X copy-selection-and-cancel\\n' > /home/claude/.tmux.conf"
|
|
8781
|
+
);
|
|
8633
8782
|
await execInContainerAsRoot(
|
|
8634
8783
|
containerName,
|
|
8635
|
-
"ln -sfn /cache/
|
|
8784
|
+
"rm -rf /var/cache/apt/archives && ln -sfn /cache/apt /var/cache/apt/archives && mkdir -p /cache/apt/partial"
|
|
8636
8785
|
);
|
|
8637
8786
|
}
|
|
8638
8787
|
async function isContainerRunning(containerName) {
|
|
@@ -8661,136 +8810,10 @@ async function getContainerPort(containerName, containerPort) {
|
|
|
8661
8810
|
}
|
|
8662
8811
|
}
|
|
8663
8812
|
|
|
8664
|
-
// ../../shared/all/helpers/
|
|
8813
|
+
// ../../shared/all/helpers/board-agent-core/git.ts
|
|
8665
8814
|
var import_child_process4 = require("child_process");
|
|
8666
|
-
var import_fs2 = require("fs");
|
|
8667
|
-
var import_path2 = __toESM(require("path"));
|
|
8668
8815
|
var import_util2 = require("util");
|
|
8669
|
-
|
|
8670
|
-
// ../../shared/all/helpers/card-ai/scripts.ts
|
|
8671
|
-
var FRPC_EXPOSE_SCRIPT = `#!/bin/sh
|
|
8672
|
-
set -e
|
|
8673
|
-
set -a; . /etc/environment.factiii 2>/dev/null || true; set +a
|
|
8674
|
-
PORT="$1"
|
|
8675
|
-
NAME="$2"
|
|
8676
|
-
if [ -z "$PORT" ] || [ -z "$NAME" ]; then
|
|
8677
|
-
echo "usage: factiii-expose <port> <name>" >&2
|
|
8678
|
-
echo " e.g. factiii-expose 3000 app" >&2
|
|
8679
|
-
exit 1
|
|
8680
|
-
fi
|
|
8681
|
-
case "$NAME" in *[!a-zA-Z0-9-]*|"") echo "name must be [a-zA-Z0-9-]" >&2; exit 1 ;; esac
|
|
8682
|
-
PROXY="\${FACTIII_TASK_ID}-\${NAME}"
|
|
8683
|
-
DOMAIN="\${PROXY}.\${FACTIII_FRP_DOMAIN}"
|
|
8684
|
-
TOML="/tmp/frpc-\${NAME}.toml"
|
|
8685
|
-
cat > "$TOML" <<EOF
|
|
8686
|
-
serverAddr = "\${FACTIII_FRP_SERVER}"
|
|
8687
|
-
serverPort = \${FACTIII_FRP_PORT}
|
|
8688
|
-
transport.protocol = "kcp"
|
|
8689
|
-
|
|
8690
|
-
[metadatas]
|
|
8691
|
-
factiii_token = "\${FACTIII_FRP_TOKEN}"
|
|
8692
|
-
|
|
8693
|
-
[[proxies]]
|
|
8694
|
-
name = "\${PROXY}"
|
|
8695
|
-
type = "http"
|
|
8696
|
-
localPort = \${PORT}
|
|
8697
|
-
subdomain = "\${PROXY}"
|
|
8698
|
-
EOF
|
|
8699
|
-
setsid frpc -c "$TOML" >/tmp/frpc-\${NAME}.log 2>&1 < /dev/null &
|
|
8700
|
-
echo $! > /tmp/frpc-\${NAME}.pid
|
|
8701
|
-
echo "http://\${DOMAIN}"
|
|
8702
|
-
`;
|
|
8703
|
-
var FRPC_UNEXPOSE_SCRIPT = `#!/bin/sh
|
|
8704
|
-
NAME="$1"
|
|
8705
|
-
if [ -z "$NAME" ]; then
|
|
8706
|
-
echo "usage: factiii-unexpose <name>" >&2
|
|
8707
|
-
exit 1
|
|
8708
|
-
fi
|
|
8709
|
-
PID_FILE="/tmp/frpc-\${NAME}.pid"
|
|
8710
|
-
if [ -f "$PID_FILE" ]; then
|
|
8711
|
-
kill "$(cat "$PID_FILE")" 2>/dev/null || true
|
|
8712
|
-
rm -f "$PID_FILE" "/tmp/frpc-\${NAME}.toml" "/tmp/frpc-\${NAME}.log"
|
|
8713
|
-
echo "stopped \${NAME}"
|
|
8714
|
-
else
|
|
8715
|
-
echo "no tunnel named \${NAME}" >&2
|
|
8716
|
-
exit 1
|
|
8717
|
-
fi
|
|
8718
|
-
`;
|
|
8719
|
-
|
|
8720
|
-
// ../../shared/all/helpers/card-ai/frpc.ts
|
|
8721
8816
|
var execFileAsync2 = (0, import_util2.promisify)(import_child_process4.execFile);
|
|
8722
|
-
var FRP_SERVER_ADDR = "dev.factiii.com";
|
|
8723
|
-
var FRP_SERVER_PORT = 7443;
|
|
8724
|
-
var FRP_DOMAIN_SUFFIX = "dev.factiii.com";
|
|
8725
|
-
async function writeFileAsRoot(containerName, path6, contents) {
|
|
8726
|
-
const b64 = Buffer.from(contents, "utf-8").toString("base64");
|
|
8727
|
-
await execInContainerAsRoot(
|
|
8728
|
-
containerName,
|
|
8729
|
-
`echo '${b64}' | base64 -d > '${path6}'`
|
|
8730
|
-
);
|
|
8731
|
-
}
|
|
8732
|
-
async function installFrpcBinary(containerName) {
|
|
8733
|
-
const arch = (await execInContainer(containerName, "uname -m")).trim();
|
|
8734
|
-
const map = {
|
|
8735
|
-
x86_64: "frpc-linux-amd64",
|
|
8736
|
-
aarch64: "frpc-linux-arm64"
|
|
8737
|
-
};
|
|
8738
|
-
const binaryName = map[arch];
|
|
8739
|
-
if (!binaryName) {
|
|
8740
|
-
throw new Error(`Unsupported container arch for frpc: ${arch}`);
|
|
8741
|
-
}
|
|
8742
|
-
const src = import_path2.default.join(__dirname, "..", "vendor", binaryName);
|
|
8743
|
-
if (!(0, import_fs2.existsSync)(src)) {
|
|
8744
|
-
throw new Error(
|
|
8745
|
-
`Vendored frpc missing (${src}). Run apps/runner/scripts/fetch-frpc.sh.`
|
|
8746
|
-
);
|
|
8747
|
-
}
|
|
8748
|
-
await execFileAsync2("docker", [
|
|
8749
|
-
"cp",
|
|
8750
|
-
src,
|
|
8751
|
-
`${containerName}:/usr/local/bin/frpc`
|
|
8752
|
-
]);
|
|
8753
|
-
await execInContainerAsRoot(containerName, "chmod +x /usr/local/bin/frpc");
|
|
8754
|
-
}
|
|
8755
|
-
async function installFrpcHarness(params) {
|
|
8756
|
-
const { containerName, taskId, runnerToken } = params;
|
|
8757
|
-
await installFrpcBinary(containerName);
|
|
8758
|
-
const envLines = [
|
|
8759
|
-
`FACTIII_FRP_SERVER=${FRP_SERVER_ADDR}`,
|
|
8760
|
-
`FACTIII_FRP_PORT=${FRP_SERVER_PORT}`,
|
|
8761
|
-
`FACTIII_FRP_TOKEN=${runnerToken}`,
|
|
8762
|
-
`FACTIII_FRP_DOMAIN=${FRP_DOMAIN_SUFFIX}`,
|
|
8763
|
-
`FACTIII_TASK_ID=${taskId}`
|
|
8764
|
-
];
|
|
8765
|
-
await writeFileAsRoot(
|
|
8766
|
-
containerName,
|
|
8767
|
-
"/usr/local/bin/factiii-expose",
|
|
8768
|
-
FRPC_EXPOSE_SCRIPT
|
|
8769
|
-
);
|
|
8770
|
-
await writeFileAsRoot(
|
|
8771
|
-
containerName,
|
|
8772
|
-
"/usr/local/bin/factiii-unexpose",
|
|
8773
|
-
FRPC_UNEXPOSE_SCRIPT
|
|
8774
|
-
);
|
|
8775
|
-
await execInContainerAsRoot(
|
|
8776
|
-
containerName,
|
|
8777
|
-
"chmod +x /usr/local/bin/factiii-expose /usr/local/bin/factiii-unexpose"
|
|
8778
|
-
);
|
|
8779
|
-
await writeFileAsRoot(
|
|
8780
|
-
containerName,
|
|
8781
|
-
"/etc/environment.factiii",
|
|
8782
|
-
envLines.join("\n") + "\n"
|
|
8783
|
-
);
|
|
8784
|
-
await execInContainerAsRoot(
|
|
8785
|
-
containerName,
|
|
8786
|
-
`grep -q 'environment.factiii' /home/claude/.profile 2>/dev/null || echo 'set -a; . /etc/environment.factiii; set +a' >> /home/claude/.profile && chown claude:claude /home/claude/.profile`
|
|
8787
|
-
);
|
|
8788
|
-
}
|
|
8789
|
-
|
|
8790
|
-
// ../../shared/all/helpers/card-ai/git.ts
|
|
8791
|
-
var import_child_process5 = require("child_process");
|
|
8792
|
-
var import_util3 = require("util");
|
|
8793
|
-
var execFileAsync3 = (0, import_util3.promisify)(import_child_process5.execFile);
|
|
8794
8817
|
function buildCloneUrl(repoUrl, githubToken) {
|
|
8795
8818
|
const url2 = new URL(repoUrl);
|
|
8796
8819
|
url2.username = githubToken;
|
|
@@ -8804,7 +8827,7 @@ function repoCacheKey(repoUrl) {
|
|
|
8804
8827
|
return repoUrl.replace(/^https?:\/\//, "").replace(/\/\/.*@/, "").replace(/\.git$/, "").replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
8805
8828
|
}
|
|
8806
8829
|
async function gitInContainer(containerName, ...args) {
|
|
8807
|
-
const { stdout } = await
|
|
8830
|
+
const { stdout } = await execFileAsync2(
|
|
8808
8831
|
"docker",
|
|
8809
8832
|
[
|
|
8810
8833
|
"exec",
|
|
@@ -8851,7 +8874,7 @@ async function cloneRepo(containerName, cloneUrl, branch, cacheKey) {
|
|
|
8851
8874
|
await gitInContainer(containerName, "clean", "-fd");
|
|
8852
8875
|
return { fromCache: true };
|
|
8853
8876
|
}
|
|
8854
|
-
await
|
|
8877
|
+
await execFileAsync2(
|
|
8855
8878
|
"docker",
|
|
8856
8879
|
[
|
|
8857
8880
|
"exec",
|
|
@@ -8894,6 +8917,7 @@ async function setGitIdentity(containerName, name, email) {
|
|
|
8894
8917
|
);
|
|
8895
8918
|
}
|
|
8896
8919
|
async function extractDiff(containerName) {
|
|
8920
|
+
await gitInContainer(containerName, "add", "--intent-to-add", ".");
|
|
8897
8921
|
const diff = await gitInContainer(containerName, "diff", "HEAD");
|
|
8898
8922
|
const changedFilesRaw = (await gitInContainer(containerName, "diff", "HEAD", "--name-only")).trim();
|
|
8899
8923
|
return {
|
|
@@ -8915,6 +8939,13 @@ async function listRemoteBranches(containerName) {
|
|
|
8915
8939
|
return [];
|
|
8916
8940
|
}
|
|
8917
8941
|
}
|
|
8942
|
+
var NoChangesError = class extends Error {
|
|
8943
|
+
constructor(message = "No changes to commit.") {
|
|
8944
|
+
super(message);
|
|
8945
|
+
this.code = "NO_CHANGES";
|
|
8946
|
+
this.name = "NoChangesError";
|
|
8947
|
+
}
|
|
8948
|
+
};
|
|
8918
8949
|
async function commitAndPush(containerName, branchName, commitMessage, gitConfig) {
|
|
8919
8950
|
if (gitConfig?.gitName)
|
|
8920
8951
|
await gitInContainer(
|
|
@@ -8930,8 +8961,8 @@ async function commitAndPush(containerName, branchName, commitMessage, gitConfig
|
|
|
8930
8961
|
"user.email",
|
|
8931
8962
|
gitConfig.gitEmail
|
|
8932
8963
|
);
|
|
8933
|
-
const
|
|
8934
|
-
if (!
|
|
8964
|
+
const status = (await gitInContainer(containerName, "status", "--porcelain")).trim();
|
|
8965
|
+
if (!status) throw new NoChangesError();
|
|
8935
8966
|
let branchExists = false;
|
|
8936
8967
|
try {
|
|
8937
8968
|
await gitInContainer(
|
|
@@ -8946,7 +8977,7 @@ async function commitAndPush(containerName, branchName, commitMessage, gitConfig
|
|
|
8946
8977
|
} catch {
|
|
8947
8978
|
}
|
|
8948
8979
|
if (branchExists) {
|
|
8949
|
-
await gitInContainer(containerName, "stash");
|
|
8980
|
+
await gitInContainer(containerName, "stash", "--include-untracked");
|
|
8950
8981
|
await gitInContainer(containerName, "fetch", "origin", branchName);
|
|
8951
8982
|
await gitInContainer(
|
|
8952
8983
|
containerName,
|
|
@@ -8962,128 +8993,273 @@ async function commitAndPush(containerName, branchName, commitMessage, gitConfig
|
|
|
8962
8993
|
await gitInContainer(containerName, "add", "-A");
|
|
8963
8994
|
await gitInContainer(containerName, "commit", "-m", commitMessage);
|
|
8964
8995
|
await gitInContainer(containerName, "push", "origin", branchName);
|
|
8996
|
+
await gitInContainer(containerName, "fetch", "origin", branchName);
|
|
8997
|
+
const remoteSha = (await gitInContainer(containerName, "rev-parse", `origin/${branchName}`)).trim();
|
|
8998
|
+
const localSha = (await gitInContainer(containerName, "rev-parse", "HEAD")).trim();
|
|
8999
|
+
if (remoteSha !== localSha) {
|
|
9000
|
+
throw new Error(
|
|
9001
|
+
`Push verification failed: local HEAD ${localSha.slice(0, 8)} does not match origin/${branchName} ${remoteSha.slice(0, 8)}`
|
|
9002
|
+
);
|
|
9003
|
+
}
|
|
9004
|
+
return { pushedSha: localSha, remoteSha, branch: branchName };
|
|
8965
9005
|
}
|
|
8966
9006
|
|
|
8967
|
-
// ../../shared/all/helpers/
|
|
8968
|
-
|
|
9007
|
+
// ../../shared/all/helpers/board-agent-core/onedrive-auth.ts
|
|
9008
|
+
var DEVICECODE_URL = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode";
|
|
9009
|
+
var TOKEN_URL = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
|
|
9010
|
+
var GRAPH_BASE = "https://graph.microsoft.com/v1.0";
|
|
9011
|
+
var DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
9012
|
+
var ONEDRIVE_SCOPES = "offline_access Files.ReadWrite.AppFolder User.Read";
|
|
9013
|
+
async function startDeviceCode(clientId) {
|
|
9014
|
+
const res = await fetch(DEVICECODE_URL, {
|
|
9015
|
+
method: "POST",
|
|
9016
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
9017
|
+
body: new URLSearchParams({ client_id: clientId, scope: ONEDRIVE_SCOPES })
|
|
9018
|
+
});
|
|
9019
|
+
if (!res.ok) {
|
|
9020
|
+
throw new Error(`OneDrive device-code start failed: ${await res.text()}`);
|
|
9021
|
+
}
|
|
9022
|
+
const data = await res.json();
|
|
8969
9023
|
return {
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
8974
|
-
|
|
8975
|
-
changedFiles: session.changedFiles,
|
|
8976
|
-
commitMessage: session.commitMessage,
|
|
8977
|
-
previewUrl: session.previewUrl,
|
|
8978
|
-
reproSteps: session.reproSteps,
|
|
8979
|
-
pendingQuestion: session.pendingQuestion,
|
|
8980
|
-
error: session.error,
|
|
8981
|
-
startedAt: session.startedAt,
|
|
8982
|
-
targetBranch: session.targetBranch,
|
|
8983
|
-
taskTitle: session.taskTitle,
|
|
8984
|
-
todos: session.todos
|
|
9024
|
+
deviceCode: data.device_code,
|
|
9025
|
+
userCode: data.user_code,
|
|
9026
|
+
verificationUri: data.verification_uri,
|
|
9027
|
+
expiresIn: data.expires_in,
|
|
9028
|
+
interval: data.interval
|
|
8985
9029
|
};
|
|
8986
9030
|
}
|
|
8987
|
-
function
|
|
8988
|
-
const
|
|
8989
|
-
|
|
8990
|
-
"
|
|
8991
|
-
|
|
8992
|
-
|
|
8993
|
-
|
|
8994
|
-
|
|
8995
|
-
|
|
9031
|
+
async function refreshAccessToken(clientId, refreshToken) {
|
|
9032
|
+
const res = await fetch(TOKEN_URL, {
|
|
9033
|
+
method: "POST",
|
|
9034
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
9035
|
+
body: new URLSearchParams({
|
|
9036
|
+
grant_type: "refresh_token",
|
|
9037
|
+
client_id: clientId,
|
|
9038
|
+
refresh_token: refreshToken,
|
|
9039
|
+
scope: ONEDRIVE_SCOPES
|
|
9040
|
+
})
|
|
9041
|
+
});
|
|
9042
|
+
if (!res.ok) {
|
|
9043
|
+
throw new Error(`OneDrive token refresh failed: ${await res.text()}`);
|
|
9044
|
+
}
|
|
9045
|
+
const tok = await res.json();
|
|
9046
|
+
return {
|
|
9047
|
+
accessToken: tok.access_token,
|
|
9048
|
+
// Microsoft rotates the refresh token on each refresh; fall back to the old
|
|
9049
|
+
// one if a new one wasn't returned.
|
|
9050
|
+
refreshToken: tok.refresh_token || refreshToken,
|
|
9051
|
+
expiry: new Date(Date.now() + tok.expires_in * 1e3).toISOString()
|
|
8996
9052
|
};
|
|
8997
|
-
|
|
9053
|
+
}
|
|
9054
|
+
async function pollDeviceCode(clientId, deviceCode) {
|
|
9055
|
+
const res = await fetch(TOKEN_URL, {
|
|
9056
|
+
method: "POST",
|
|
9057
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
9058
|
+
body: new URLSearchParams({
|
|
9059
|
+
grant_type: DEVICE_CODE_GRANT,
|
|
9060
|
+
client_id: clientId,
|
|
9061
|
+
device_code: deviceCode
|
|
9062
|
+
})
|
|
9063
|
+
});
|
|
9064
|
+
if (res.ok) {
|
|
9065
|
+
const tok = await res.json();
|
|
9066
|
+
if (!tok.refresh_token) {
|
|
9067
|
+
return { status: "error", message: "No refresh token returned." };
|
|
9068
|
+
}
|
|
9069
|
+
const appFolder = await getAppFolder(tok.access_token);
|
|
9070
|
+
return {
|
|
9071
|
+
status: "complete",
|
|
9072
|
+
refreshToken: tok.refresh_token,
|
|
9073
|
+
appFolder
|
|
9074
|
+
};
|
|
9075
|
+
}
|
|
9076
|
+
const err = await res.json().catch(() => ({}));
|
|
9077
|
+
switch (err.error) {
|
|
9078
|
+
case "authorization_pending":
|
|
9079
|
+
return { status: "pending" };
|
|
9080
|
+
case "slow_down":
|
|
9081
|
+
return { status: "slow_down" };
|
|
9082
|
+
case "expired_token":
|
|
9083
|
+
case "code_expired":
|
|
9084
|
+
return { status: "expired" };
|
|
9085
|
+
case "authorization_declined":
|
|
9086
|
+
case "access_denied":
|
|
9087
|
+
return { status: "declined" };
|
|
9088
|
+
default:
|
|
9089
|
+
return {
|
|
9090
|
+
status: "error",
|
|
9091
|
+
message: err.error_description || err.error || "Device-code poll failed"
|
|
9092
|
+
};
|
|
9093
|
+
}
|
|
9094
|
+
}
|
|
9095
|
+
async function getAppFolder(accessToken) {
|
|
9096
|
+
const res = await fetch(
|
|
9097
|
+
`${GRAPH_BASE}/me/drive/special/approot?$select=id,parentReference`,
|
|
9098
|
+
{ headers: { Authorization: `Bearer ${accessToken}` } }
|
|
9099
|
+
);
|
|
9100
|
+
if (!res.ok) {
|
|
9101
|
+
throw new Error(
|
|
9102
|
+
`Failed to resolve OneDrive app folder: ${res.status} - ${await res.text()}`
|
|
9103
|
+
);
|
|
9104
|
+
}
|
|
9105
|
+
const data = await res.json();
|
|
9106
|
+
if (!data.id || !data.parentReference?.driveId) {
|
|
9107
|
+
throw new Error("OneDrive app folder response was incomplete.");
|
|
9108
|
+
}
|
|
8998
9109
|
return {
|
|
8999
|
-
|
|
9000
|
-
|
|
9001
|
-
|
|
9002
|
-
if (label && notify) {
|
|
9003
|
-
notify(
|
|
9004
|
-
`Board AI \u2014 ${label}`,
|
|
9005
|
-
`${session.taskTitle || "Card"} (step ${session.step}/3)`,
|
|
9006
|
-
session.phase !== "complete" && session.phase !== "error" && session.phase !== "awaiting-input"
|
|
9007
|
-
);
|
|
9008
|
-
}
|
|
9009
|
-
},
|
|
9010
|
-
emitSessionRemoved(postId) {
|
|
9011
|
-
send(channel("session-update"), {
|
|
9012
|
-
postId,
|
|
9013
|
-
phase: null,
|
|
9014
|
-
step: 1,
|
|
9015
|
-
taskId: "",
|
|
9016
|
-
diff: "",
|
|
9017
|
-
changedFiles: [],
|
|
9018
|
-
commitMessage: "",
|
|
9019
|
-
previewUrl: "",
|
|
9020
|
-
reproSteps: "",
|
|
9021
|
-
pendingQuestion: "",
|
|
9022
|
-
error: "",
|
|
9023
|
-
startedAt: 0,
|
|
9024
|
-
targetBranch: "",
|
|
9025
|
-
taskTitle: "",
|
|
9026
|
-
todos: []
|
|
9027
|
-
});
|
|
9028
|
-
},
|
|
9029
|
-
emitLog(session, entry) {
|
|
9030
|
-
send(channel(`log:${session.postId}`), entry);
|
|
9031
|
-
},
|
|
9032
|
-
addLog(session, type, content) {
|
|
9033
|
-
const entry = { time: Date.now(), type, content };
|
|
9034
|
-
session.logs.push(entry);
|
|
9035
|
-
this.emitLog(session, entry);
|
|
9036
|
-
},
|
|
9037
|
-
appendTextLog(session, text) {
|
|
9038
|
-
const last = session.logs[session.logs.length - 1];
|
|
9039
|
-
if (last && last.type === "text") {
|
|
9040
|
-
last.content += text;
|
|
9041
|
-
this.emitLog(session, {
|
|
9042
|
-
time: last.time,
|
|
9043
|
-
type: "text-append",
|
|
9044
|
-
content: text
|
|
9045
|
-
});
|
|
9046
|
-
} else {
|
|
9047
|
-
const entry = {
|
|
9048
|
-
time: Date.now(),
|
|
9049
|
-
type: "text",
|
|
9050
|
-
content: text
|
|
9051
|
-
};
|
|
9052
|
-
session.logs.push(entry);
|
|
9053
|
-
this.emitLog(session, entry);
|
|
9054
|
-
}
|
|
9055
|
-
},
|
|
9056
|
-
emitSetupStatus(message) {
|
|
9057
|
-
send(channel("setup-status"), message);
|
|
9058
|
-
}
|
|
9110
|
+
rootFolderId: data.id,
|
|
9111
|
+
driveId: data.parentReference.driveId,
|
|
9112
|
+
driveType: data.parentReference.driveType || "personal"
|
|
9059
9113
|
};
|
|
9060
9114
|
}
|
|
9061
9115
|
|
|
9062
|
-
// ../../shared/all/helpers/
|
|
9063
|
-
|
|
9064
|
-
|
|
9065
|
-
|
|
9066
|
-
|
|
9116
|
+
// ../../shared/all/helpers/board-agent-core/onedrive.ts
|
|
9117
|
+
var WORKSPACE = "/home/claude/workspace";
|
|
9118
|
+
var RCLONE_CONF = "/home/claude/.config/rclone/rclone.conf";
|
|
9119
|
+
var REMOTE = "onedrive";
|
|
9120
|
+
var SYNC_LOG = "/tmp/rclone-onedrive.log";
|
|
9121
|
+
function onedriveRemotePath(spaceSlug) {
|
|
9122
|
+
const safe = spaceSlug.replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
9123
|
+
return `${REMOTE}:${safe}`;
|
|
9124
|
+
}
|
|
9125
|
+
async function writeRcloneMountConfig(containerName, conn) {
|
|
9126
|
+
const fresh = await refreshAccessToken(conn.clientId, conn.refreshToken);
|
|
9127
|
+
const tokenJson = JSON.stringify({
|
|
9128
|
+
access_token: fresh.accessToken,
|
|
9129
|
+
refresh_token: fresh.refreshToken,
|
|
9130
|
+
token_type: "Bearer",
|
|
9131
|
+
expiry: fresh.expiry
|
|
9132
|
+
});
|
|
9133
|
+
const conf = [
|
|
9134
|
+
`[${REMOTE}]`,
|
|
9135
|
+
"type = onedrive",
|
|
9136
|
+
`client_id = ${conn.clientId}`,
|
|
9137
|
+
`drive_id = ${conn.driveId}`,
|
|
9138
|
+
`drive_type = ${conn.driveType}`,
|
|
9139
|
+
`root_folder_id = ${conn.rootFolderId}`,
|
|
9140
|
+
// App is consumer-only, so rclone must refresh against /consumers (not the
|
|
9141
|
+
// default /common, which rejects it with AADSTS9002346).
|
|
9142
|
+
"auth_url = https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize",
|
|
9143
|
+
"token_url = https://login.microsoftonline.com/consumers/oauth2/v2.0/token",
|
|
9144
|
+
`token = ${tokenJson}`,
|
|
9145
|
+
""
|
|
9146
|
+
].join("\n");
|
|
9147
|
+
const b64 = Buffer.from(conf, "utf-8").toString("base64");
|
|
9148
|
+
await execInContainer(
|
|
9149
|
+
containerName,
|
|
9150
|
+
`mkdir -p /home/claude/.config/rclone && printf %s '${b64}' | base64 -d > ${RCLONE_CONF} && chmod 600 ${RCLONE_CONF}`
|
|
9151
|
+
);
|
|
9152
|
+
return fresh.refreshToken;
|
|
9153
|
+
}
|
|
9154
|
+
async function mountWorkspace(containerName, remotePath) {
|
|
9155
|
+
await execInContainer(
|
|
9156
|
+
containerName,
|
|
9157
|
+
`mkdir -p ${WORKSPACE} && rclone mkdir '${remotePath}'`
|
|
9158
|
+
);
|
|
9159
|
+
const out = await execInContainer(
|
|
9160
|
+
containerName,
|
|
9161
|
+
`nohup rclone mount '${remotePath}' ${WORKSPACE} --vfs-cache-mode full --vfs-write-back 1s --dir-cache-time 30s -v --rc --rc-addr ${RC_ADDR} --rc-no-auth >> ${SYNC_LOG} 2>&1 & for i in $(seq 1 40); do mountpoint -q ${WORKSPACE} && break; sleep 0.25; done; mountpoint -q ${WORKSPACE} && echo mounted || (tail -n 20 ${SYNC_LOG} 2>/dev/null; echo mount-failed)`
|
|
9162
|
+
);
|
|
9163
|
+
if (!out.trim().endsWith("mounted")) {
|
|
9164
|
+
throw new Error(`OneDrive mount failed:
|
|
9165
|
+
${out.trim()}`);
|
|
9067
9166
|
}
|
|
9068
|
-
return text;
|
|
9069
9167
|
}
|
|
9070
|
-
|
|
9071
|
-
|
|
9072
|
-
|
|
9073
|
-
|
|
9168
|
+
var syncWatchers = /* @__PURE__ */ new Map();
|
|
9169
|
+
var RC_ADDR = "127.0.0.1:5572";
|
|
9170
|
+
async function onedriveQueueNow(containerName) {
|
|
9171
|
+
const out = await execInContainer(
|
|
9172
|
+
containerName,
|
|
9173
|
+
`rclone rc --url http://${RC_ADDR}/ vfs/queue 2>/dev/null || echo '{}'`
|
|
9174
|
+
);
|
|
9175
|
+
try {
|
|
9176
|
+
const parsed = JSON.parse(out);
|
|
9177
|
+
return (parsed.queue ?? []).map((i) => i.name).filter((n) => !!n);
|
|
9178
|
+
} catch {
|
|
9179
|
+
return [];
|
|
9180
|
+
}
|
|
9181
|
+
}
|
|
9182
|
+
function startOneDriveSyncWatcher(containerName, onPending) {
|
|
9183
|
+
stopOneDriveSyncWatcher(containerName);
|
|
9184
|
+
let chain = Promise.resolve();
|
|
9185
|
+
const refresh = () => {
|
|
9186
|
+
chain = chain.then(async () => {
|
|
9187
|
+
const watcher = syncWatchers.get(containerName);
|
|
9188
|
+
if (!watcher) return;
|
|
9189
|
+
const files = await onedriveQueueNow(containerName);
|
|
9190
|
+
if (watcher.files.join("\n") !== files.join("\n")) {
|
|
9191
|
+
watcher.files = files;
|
|
9192
|
+
onPending(files.length, files);
|
|
9193
|
+
}
|
|
9194
|
+
});
|
|
9195
|
+
};
|
|
9196
|
+
const handle = streamExecInContainer(
|
|
9197
|
+
containerName,
|
|
9198
|
+
`tail -n0 -F ${SYNC_LOG} 2>/dev/null | grep --line-buffered -E 'vfs cache: (upload|starting|failed|cancel|queuing)'`,
|
|
9199
|
+
refresh
|
|
9200
|
+
);
|
|
9201
|
+
syncWatchers.set(containerName, { handle, files: [] });
|
|
9202
|
+
}
|
|
9203
|
+
function stopOneDriveSyncWatcher(containerName) {
|
|
9204
|
+
const watcher = syncWatchers.get(containerName);
|
|
9205
|
+
if (watcher) {
|
|
9206
|
+
watcher.handle.stop();
|
|
9207
|
+
syncWatchers.delete(containerName);
|
|
9208
|
+
}
|
|
9209
|
+
}
|
|
9210
|
+
function onedrivePendingUploads(containerName) {
|
|
9211
|
+
return syncWatchers.get(containerName)?.files.length ?? 0;
|
|
9212
|
+
}
|
|
9213
|
+
function onedriveUploadingFiles(containerName) {
|
|
9214
|
+
return syncWatchers.get(containerName)?.files ?? [];
|
|
9215
|
+
}
|
|
9216
|
+
async function unmountWorkspace(containerName) {
|
|
9217
|
+
await execInContainer(
|
|
9218
|
+
containerName,
|
|
9219
|
+
`fusermount -u ${WORKSPACE} 2>/dev/null || fusermount3 -u ${WORKSPACE} 2>/dev/null || rclone unmount ${WORKSPACE} 2>/dev/null || true`
|
|
9220
|
+
);
|
|
9221
|
+
}
|
|
9222
|
+
async function readbackRefreshToken(containerName) {
|
|
9223
|
+
let conf = "";
|
|
9224
|
+
try {
|
|
9225
|
+
conf = await execInContainer(
|
|
9226
|
+
containerName,
|
|
9227
|
+
`cat ${RCLONE_CONF} 2>/dev/null || true`
|
|
9228
|
+
);
|
|
9229
|
+
} catch {
|
|
9230
|
+
return null;
|
|
9231
|
+
}
|
|
9232
|
+
const match = conf.match(/token\s*=\s*(\{.*\})/);
|
|
9233
|
+
if (!match) return null;
|
|
9234
|
+
try {
|
|
9235
|
+
const parsed = JSON.parse(match[1]);
|
|
9236
|
+
return parsed.refresh_token ?? null;
|
|
9237
|
+
} catch {
|
|
9238
|
+
return null;
|
|
9239
|
+
}
|
|
9240
|
+
}
|
|
9241
|
+
|
|
9242
|
+
// ../../shared/all/helpers/board-agent-core/container.ts
|
|
9243
|
+
function withTimeout(work, ms, label) {
|
|
9244
|
+
let timer;
|
|
9245
|
+
const timeout = new Promise((_, reject) => {
|
|
9246
|
+
timer = setTimeout(
|
|
9247
|
+
() => reject(
|
|
9248
|
+
new Error(
|
|
9249
|
+
`${label} timed out. Check Docker and your connection, then Try Again.`
|
|
9250
|
+
)
|
|
9251
|
+
),
|
|
9252
|
+
ms
|
|
9253
|
+
);
|
|
9254
|
+
});
|
|
9255
|
+
return Promise.race([work.finally(() => clearTimeout(timer)), timeout]);
|
|
9074
9256
|
}
|
|
9075
|
-
var
|
|
9257
|
+
var ContainerCore = class {
|
|
9076
9258
|
constructor(options) {
|
|
9077
|
-
// Utility container state
|
|
9259
|
+
// Utility container state.
|
|
9078
9260
|
this.containerName = null;
|
|
9079
9261
|
this.lastConfigKey = "";
|
|
9080
9262
|
this.setupPromise = null;
|
|
9081
|
-
// Board AI session id + active process
|
|
9082
|
-
this.boardSessionId = null;
|
|
9083
|
-
this.boardProcess = null;
|
|
9084
|
-
// Board AI state
|
|
9085
|
-
this.sessions = /* @__PURE__ */ new Map();
|
|
9086
|
-
this.cardProcesses = /* @__PURE__ */ new Map();
|
|
9087
9263
|
this.spaceSlug = options.spaceSlug;
|
|
9088
9264
|
this.emitter = options.emitter;
|
|
9089
9265
|
this.configProvider = options.configProvider;
|
|
@@ -9092,6 +9268,49 @@ var BoardAgentEngine = class _BoardAgentEngine {
|
|
|
9092
9268
|
const raw = (options.instanceId || "shared").toLowerCase();
|
|
9093
9269
|
this.instancePrefix = raw.replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "shared";
|
|
9094
9270
|
}
|
|
9271
|
+
readConfig() {
|
|
9272
|
+
return this.configProvider.readConfig(this.spaceSlug);
|
|
9273
|
+
}
|
|
9274
|
+
isReady() {
|
|
9275
|
+
return this.containerName !== null;
|
|
9276
|
+
}
|
|
9277
|
+
// ── Storage backend ──
|
|
9278
|
+
isOneDrive(config) {
|
|
9279
|
+
return config.storageBackend === "onedrive";
|
|
9280
|
+
}
|
|
9281
|
+
/** Per-space OneDrive remote path inside the app folder. */
|
|
9282
|
+
remotePath() {
|
|
9283
|
+
return onedriveRemotePath(this.spaceSlug);
|
|
9284
|
+
}
|
|
9285
|
+
isOneDriveConnected() {
|
|
9286
|
+
return this.configProvider.readOneDriveConnection(this.spaceSlug) !== null;
|
|
9287
|
+
}
|
|
9288
|
+
/** The stored OneDrive credential, or fail with a connect hint. */
|
|
9289
|
+
getOneDriveConnection() {
|
|
9290
|
+
const conn = this.configProvider.readOneDriveConnection(this.spaceSlug);
|
|
9291
|
+
if (!conn) {
|
|
9292
|
+
throw new Error(
|
|
9293
|
+
"OneDrive is not connected. Connect it in Board AI settings."
|
|
9294
|
+
);
|
|
9295
|
+
}
|
|
9296
|
+
return conn;
|
|
9297
|
+
}
|
|
9298
|
+
saveOneDriveConnection(connection) {
|
|
9299
|
+
this.configProvider.writeOneDriveConnection(this.spaceSlug, connection);
|
|
9300
|
+
}
|
|
9301
|
+
clearOneDriveConnection() {
|
|
9302
|
+
this.configProvider.writeOneDriveConnection(this.spaceSlug, null);
|
|
9303
|
+
}
|
|
9304
|
+
/** Persist a rotated refresh token onto the existing connection. */
|
|
9305
|
+
updateOneDriveRefreshToken(refreshToken) {
|
|
9306
|
+
const conn = this.configProvider.readOneDriveConnection(this.spaceSlug);
|
|
9307
|
+
if (conn) {
|
|
9308
|
+
this.configProvider.writeOneDriveConnection(this.spaceSlug, {
|
|
9309
|
+
...conn,
|
|
9310
|
+
refreshToken
|
|
9311
|
+
});
|
|
9312
|
+
}
|
|
9313
|
+
}
|
|
9095
9314
|
// ── Config ──
|
|
9096
9315
|
getConfig() {
|
|
9097
9316
|
const config = this.configProvider.readConfig(this.spaceSlug);
|
|
@@ -9103,7 +9322,11 @@ var BoardAgentEngine = class _BoardAgentEngine {
|
|
|
9103
9322
|
hasToken: Boolean(config.claudeToken),
|
|
9104
9323
|
hasGithubToken: Boolean(config.githubToken),
|
|
9105
9324
|
tokenHint: config.claudeToken ? `\xB7\xB7\xB7\xB7${config.claudeToken.slice(-4)}` : "",
|
|
9106
|
-
githubTokenHint: config.githubToken ? `\xB7\xB7\xB7\xB7${config.githubToken.slice(-4)}` : ""
|
|
9325
|
+
githubTokenHint: config.githubToken ? `\xB7\xB7\xB7\xB7${config.githubToken.slice(-4)}` : "",
|
|
9326
|
+
storageBackend: config.storageBackend ?? "github",
|
|
9327
|
+
onedrive: {
|
|
9328
|
+
connected: this.isOneDriveConnected()
|
|
9329
|
+
}
|
|
9107
9330
|
};
|
|
9108
9331
|
}
|
|
9109
9332
|
setConfig(partial) {
|
|
@@ -9114,7 +9337,8 @@ var BoardAgentEngine = class _BoardAgentEngine {
|
|
|
9114
9337
|
gitName: partial.gitName ?? existing.gitName,
|
|
9115
9338
|
gitEmail: partial.gitEmail ?? existing.gitEmail,
|
|
9116
9339
|
claudeToken: partial.claudeToken?.replace(/\s/g, "") || existing.claudeToken,
|
|
9117
|
-
githubToken: partial.githubToken?.replace(/\s/g, "") || existing.githubToken
|
|
9340
|
+
githubToken: partial.githubToken?.replace(/\s/g, "") || existing.githubToken,
|
|
9341
|
+
storageBackend: partial.storageBackend ?? existing.storageBackend
|
|
9118
9342
|
};
|
|
9119
9343
|
this.configProvider.writeConfig(this.spaceSlug, updated);
|
|
9120
9344
|
return { success: true };
|
|
@@ -9129,10 +9353,19 @@ var BoardAgentEngine = class _BoardAgentEngine {
|
|
|
9129
9353
|
async ensureContainer(onStatus) {
|
|
9130
9354
|
const config = this.configProvider.readConfig(this.spaceSlug);
|
|
9131
9355
|
if (!config.claudeToken) throw new Error("No Claude token configured.");
|
|
9132
|
-
|
|
9133
|
-
if (
|
|
9134
|
-
|
|
9135
|
-
|
|
9356
|
+
const onedrive = this.isOneDrive(config);
|
|
9357
|
+
if (onedrive) {
|
|
9358
|
+
if (!this.isOneDriveConnected()) {
|
|
9359
|
+
throw new Error(
|
|
9360
|
+
"OneDrive is not connected. Connect it in Board AI settings, then retry."
|
|
9361
|
+
);
|
|
9362
|
+
}
|
|
9363
|
+
} else {
|
|
9364
|
+
if (!config.githubToken) throw new Error("No GitHub token configured.");
|
|
9365
|
+
if (!config.repoUrl) throw new Error("No repository URL configured.");
|
|
9366
|
+
}
|
|
9367
|
+
const configKey = onedrive ? `onedrive|${this.spaceSlug}|${config.claudeToken}` : `${config.repoUrl}|${config.claudeToken}|${config.githubToken}`;
|
|
9368
|
+
const cacheKey = onedrive ? `onedrive-${this.spaceSlug}`.replace(/[^a-zA-Z0-9._-]/g, "_") : repoCacheKey(config.repoUrl);
|
|
9136
9369
|
const name = `factiii-${this.instancePrefix}-util-${this.spaceSlug}-${cacheKey}`;
|
|
9137
9370
|
if (this.containerName && this.lastConfigKey !== configKey) {
|
|
9138
9371
|
await killContainer(this.containerName);
|
|
@@ -9154,16 +9387,40 @@ var BoardAgentEngine = class _BoardAgentEngine {
|
|
|
9154
9387
|
this.emitter.emitSetupStatus(msg);
|
|
9155
9388
|
};
|
|
9156
9389
|
try {
|
|
9157
|
-
status("
|
|
9158
|
-
await
|
|
9390
|
+
status("Step 1/4 \xB7 Checking Docker\u2026");
|
|
9391
|
+
await withTimeout(assertDockerRunning(), 15e3, "Checking Docker");
|
|
9392
|
+
status("Step 2/4 \xB7 Building container image\u2026");
|
|
9393
|
+
await ensureDockerImage(this.dockerfilePath, (line) => status(line));
|
|
9159
9394
|
await ensureCacheVolume();
|
|
9160
9395
|
await killContainer(name);
|
|
9161
|
-
status("Starting container
|
|
9162
|
-
await
|
|
9163
|
-
|
|
9164
|
-
|
|
9165
|
-
|
|
9166
|
-
|
|
9396
|
+
status("Step 3/4 \xB7 Starting container\u2026");
|
|
9397
|
+
await withTimeout(
|
|
9398
|
+
spawnContainer(name, config.claudeToken),
|
|
9399
|
+
9e4,
|
|
9400
|
+
"Starting container"
|
|
9401
|
+
);
|
|
9402
|
+
if (onedrive) {
|
|
9403
|
+
status("Step 4/4 \xB7 Verifying OneDrive\u2026");
|
|
9404
|
+
const rotated = await writeRcloneMountConfig(
|
|
9405
|
+
name,
|
|
9406
|
+
this.getOneDriveConnection()
|
|
9407
|
+
);
|
|
9408
|
+
this.updateOneDriveRefreshToken(rotated);
|
|
9409
|
+
await withTimeout(
|
|
9410
|
+
execInContainer(name, `rclone lsd 'onedrive:'`),
|
|
9411
|
+
6e4,
|
|
9412
|
+
"Verifying OneDrive"
|
|
9413
|
+
);
|
|
9414
|
+
} else {
|
|
9415
|
+
status("Step 4/4 \xB7 Cloning repository\u2026");
|
|
9416
|
+
const cloneUrl = buildCloneUrl(config.repoUrl, config.githubToken);
|
|
9417
|
+
await withTimeout(
|
|
9418
|
+
cloneRepo(name, cloneUrl, config.mainBranch || "main", cacheKey),
|
|
9419
|
+
18e4,
|
|
9420
|
+
"Cloning repository"
|
|
9421
|
+
);
|
|
9422
|
+
await setGitIdentity(name, config.gitName, config.gitEmail);
|
|
9423
|
+
}
|
|
9167
9424
|
status("");
|
|
9168
9425
|
this.containerName = name;
|
|
9169
9426
|
this.lastConfigKey = configKey;
|
|
@@ -9175,324 +9432,1152 @@ var BoardAgentEngine = class _BoardAgentEngine {
|
|
|
9175
9432
|
})();
|
|
9176
9433
|
return this.setupPromise;
|
|
9177
9434
|
}
|
|
9178
|
-
|
|
9179
|
-
|
|
9435
|
+
setupStatus() {
|
|
9436
|
+
if (this.containerName) return { phase: "ready" };
|
|
9437
|
+
if (this.setupPromise) return { phase: "running" };
|
|
9438
|
+
return { phase: "idle" };
|
|
9439
|
+
}
|
|
9440
|
+
async ensureSetup() {
|
|
9441
|
+
await this.ensureContainer();
|
|
9442
|
+
return { ready: true };
|
|
9443
|
+
}
|
|
9444
|
+
async cardRemoteBranches() {
|
|
9445
|
+
const config = this.configProvider.readConfig(this.spaceSlug);
|
|
9446
|
+
if (this.isOneDrive(config)) return [];
|
|
9447
|
+
if (!config.githubToken || !config.repoUrl) return [];
|
|
9180
9448
|
if (!this.isReady()) {
|
|
9181
9449
|
throw new Error(
|
|
9182
9450
|
'Environment not initialized. Open Board AI Settings and click "Initialize Environment".'
|
|
9183
9451
|
);
|
|
9184
9452
|
}
|
|
9185
|
-
|
|
9186
|
-
let prompt2;
|
|
9187
|
-
if (this.boardSessionId) {
|
|
9188
|
-
prompt2 = params.question;
|
|
9189
|
-
} else {
|
|
9190
|
-
const parts2 = ["Here is the board data:", "", params.boardContext || ""];
|
|
9191
|
-
parts2.push("", params.question);
|
|
9192
|
-
prompt2 = parts2.join("\n");
|
|
9193
|
-
}
|
|
9194
|
-
const systemPrompt = [params.componentSchema, buildPrompt(prompts_default.boardAI)].filter(Boolean).join("\n\n");
|
|
9195
|
-
const { proc, result } = spawnClaude({
|
|
9196
|
-
containerName,
|
|
9197
|
-
prompt: prompt2,
|
|
9198
|
-
allowedTools: "Read,Grep,Glob,Agent",
|
|
9199
|
-
model: "claude-opus-4-6",
|
|
9200
|
-
includePartialMessages: true,
|
|
9201
|
-
appendSystemPrompt: systemPrompt,
|
|
9202
|
-
resumeSessionId: this.boardSessionId || void 0,
|
|
9203
|
-
callbacks: {
|
|
9204
|
-
onLog: (entry) => sendLog(entry)
|
|
9205
|
-
}
|
|
9206
|
-
});
|
|
9207
|
-
this.boardProcess = proc;
|
|
9208
|
-
try {
|
|
9209
|
-
const { text, sessionId } = await result;
|
|
9210
|
-
if (sessionId) this.boardSessionId = sessionId;
|
|
9211
|
-
return text;
|
|
9212
|
-
} finally {
|
|
9213
|
-
this.boardProcess = null;
|
|
9214
|
-
}
|
|
9215
|
-
}
|
|
9216
|
-
resetBoardSession() {
|
|
9217
|
-
if (this.boardProcess) {
|
|
9218
|
-
try {
|
|
9219
|
-
this.boardProcess.kill("SIGTERM");
|
|
9220
|
-
} catch {
|
|
9221
|
-
}
|
|
9222
|
-
this.boardProcess = null;
|
|
9223
|
-
}
|
|
9224
|
-
this.boardSessionId = null;
|
|
9453
|
+
return listRemoteBranches(this.containerName);
|
|
9225
9454
|
}
|
|
9226
|
-
|
|
9227
|
-
|
|
9228
|
-
|
|
9229
|
-
containerName
|
|
9230
|
-
|
|
9231
|
-
allowedTools: tools,
|
|
9232
|
-
disallowedTools: options?.disallowedTools,
|
|
9233
|
-
permissionMode: options?.permissionMode,
|
|
9234
|
-
appendSystemPrompt: buildPrompt(prompts_default.system),
|
|
9235
|
-
effort: session.effort,
|
|
9236
|
-
model: "opus",
|
|
9237
|
-
resumeSessionId,
|
|
9238
|
-
callbacks: {
|
|
9239
|
-
onLog: (entry) => {
|
|
9240
|
-
if (entry.type === "text-append") {
|
|
9241
|
-
this.emitter.appendTextLog(session, entry.content);
|
|
9242
|
-
} else {
|
|
9243
|
-
this.emitter.addLog(session, entry.type, entry.content);
|
|
9244
|
-
}
|
|
9245
|
-
},
|
|
9246
|
-
onTodos: (todos) => {
|
|
9247
|
-
session.todos = todos;
|
|
9248
|
-
this.emitter.emitSessionUpdate(session);
|
|
9249
|
-
}
|
|
9250
|
-
}
|
|
9251
|
-
});
|
|
9252
|
-
this.cardProcesses.set(session.postId, proc);
|
|
9253
|
-
try {
|
|
9254
|
-
return await result;
|
|
9255
|
-
} finally {
|
|
9256
|
-
this.cardProcesses.delete(session.postId);
|
|
9455
|
+
destroy() {
|
|
9456
|
+
if (this.containerName) {
|
|
9457
|
+
void killContainer(this.containerName);
|
|
9458
|
+
this.containerName = null;
|
|
9459
|
+
this.setupPromise = null;
|
|
9257
9460
|
}
|
|
9258
9461
|
}
|
|
9259
|
-
|
|
9260
|
-
|
|
9261
|
-
|
|
9262
|
-
|
|
9263
|
-
|
|
9264
|
-
|
|
9265
|
-
|
|
9462
|
+
};
|
|
9463
|
+
|
|
9464
|
+
// ../../shared/all/helpers/board-agent-core/runtimes.ts
|
|
9465
|
+
async function installRuntimes(emitter, session, attrs) {
|
|
9466
|
+
const clean = attrs.map((a) => a.trim()).filter((a) => a && /^[a-zA-Z0-9._-]+$/.test(a));
|
|
9467
|
+
if (!clean.length) return;
|
|
9468
|
+
emitter.addLog(
|
|
9469
|
+
session,
|
|
9470
|
+
"system",
|
|
9471
|
+
`Installing ${clean.join(", ")} via Nix...`
|
|
9472
|
+
);
|
|
9473
|
+
try {
|
|
9474
|
+
const refs = clean.map((a) => `nixpkgs#${a}`).join(" ");
|
|
9475
|
+
const wrapped = `nix profile install ${refs} 2>&1; echo "__FACTIII_NIX_EXIT__:$?"`;
|
|
9476
|
+
const out = await execInContainer(session.containerName, wrapped);
|
|
9477
|
+
const marker = out.lastIndexOf("__FACTIII_NIX_EXIT__:");
|
|
9478
|
+
const body = (marker >= 0 ? out.slice(0, marker) : out).trimEnd();
|
|
9479
|
+
const code = marker >= 0 ? parseInt(out.slice(marker).split(":")[1], 10) || 0 : 0;
|
|
9480
|
+
if (body) emitter.addLog(session, "text", body);
|
|
9481
|
+
emitter.addLog(
|
|
9266
9482
|
session,
|
|
9267
|
-
|
|
9268
|
-
"
|
|
9269
|
-
resumeSessionId,
|
|
9270
|
-
{ permissionMode: "plan", disallowedTools: "AskUserQuestion" }
|
|
9483
|
+
code === 0 ? "system" : "error",
|
|
9484
|
+
code === 0 ? "Environment ready." : `Install exited with code ${code}. Continuing \u2014 install by hand from the terminal if needed.`
|
|
9271
9485
|
);
|
|
9272
|
-
|
|
9273
|
-
|
|
9274
|
-
const markerIdx = text.lastIndexOf(_BoardAgentEngine.QUESTION_MARKER);
|
|
9275
|
-
if (markerIdx === -1) return;
|
|
9276
|
-
session.phase = "awaiting-input";
|
|
9277
|
-
session.pendingQuestion = text.slice(markerIdx + _BoardAgentEngine.QUESTION_MARKER.length).trim();
|
|
9278
|
-
this.emitter.emitSessionUpdate(session);
|
|
9279
|
-
this.emitter.addLog(
|
|
9486
|
+
} catch (err) {
|
|
9487
|
+
emitter.addLog(
|
|
9280
9488
|
session,
|
|
9281
|
-
"
|
|
9282
|
-
|
|
9489
|
+
"error",
|
|
9490
|
+
`Install failed: ${err instanceof Error ? err.message : String(err)}`
|
|
9283
9491
|
);
|
|
9284
|
-
const answer = await new Promise((resolve) => {
|
|
9285
|
-
session.answerResolver = resolve;
|
|
9286
|
-
});
|
|
9287
|
-
if (!this.sessions.has(session.postId)) return;
|
|
9288
|
-
session.phase = "research";
|
|
9289
|
-
session.pendingQuestion = "";
|
|
9290
|
-
session.answerResolver = null;
|
|
9291
|
-
this.emitter.emitSessionUpdate(session);
|
|
9292
|
-
this.emitter.addLog(session, "system", "Resuming research...");
|
|
9293
|
-
return this.runResearch(session, answer, sessionId);
|
|
9294
9492
|
}
|
|
9295
|
-
|
|
9296
|
-
|
|
9297
|
-
|
|
9298
|
-
|
|
9299
|
-
|
|
9300
|
-
|
|
9301
|
-
|
|
9302
|
-
|
|
9303
|
-
|
|
9304
|
-
|
|
9305
|
-
|
|
9306
|
-
|
|
9307
|
-
|
|
9308
|
-
|
|
9309
|
-
|
|
9310
|
-
|
|
9311
|
-
|
|
9312
|
-
|
|
9313
|
-
|
|
9314
|
-
|
|
9315
|
-
|
|
9316
|
-
|
|
9317
|
-
|
|
9318
|
-
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
|
|
9322
|
-
|
|
9323
|
-
|
|
9324
|
-
|
|
9325
|
-
|
|
9326
|
-
|
|
9327
|
-
|
|
9328
|
-
|
|
9329
|
-
|
|
9330
|
-
|
|
9331
|
-
session,
|
|
9493
|
+
}
|
|
9494
|
+
|
|
9495
|
+
// ../../shared/all/helpers/board-agent-core/session.ts
|
|
9496
|
+
function toSummary(session) {
|
|
9497
|
+
return {
|
|
9498
|
+
taskId: session.taskId,
|
|
9499
|
+
postId: session.postId,
|
|
9500
|
+
phase: session.phase,
|
|
9501
|
+
mode: session.mode,
|
|
9502
|
+
step: session.step,
|
|
9503
|
+
diff: session.diff,
|
|
9504
|
+
changedFiles: session.changedFiles,
|
|
9505
|
+
commitMessage: session.commitMessage,
|
|
9506
|
+
previewUrl: session.previewUrl,
|
|
9507
|
+
previewRunning: session.previewRunning,
|
|
9508
|
+
reproSteps: session.reproSteps,
|
|
9509
|
+
pendingQuestion: session.pendingQuestion,
|
|
9510
|
+
error: session.error,
|
|
9511
|
+
startedAt: session.startedAt,
|
|
9512
|
+
targetBranch: session.targetBranch,
|
|
9513
|
+
taskTitle: session.taskTitle,
|
|
9514
|
+
todos: session.todos
|
|
9515
|
+
};
|
|
9516
|
+
}
|
|
9517
|
+
function createEventSink(send, spaceSlug, notify) {
|
|
9518
|
+
const PHASE_LABELS = {
|
|
9519
|
+
research: "Research started",
|
|
9520
|
+
"awaiting-input": "Input required",
|
|
9521
|
+
implement: "Implementation started",
|
|
9522
|
+
review: "Review started",
|
|
9523
|
+
complete: "Task complete",
|
|
9524
|
+
error: "Task failed"
|
|
9525
|
+
};
|
|
9526
|
+
const channel = (event) => `board-agent:${spaceSlug}:${event}`;
|
|
9527
|
+
return {
|
|
9528
|
+
emitSessionUpdate(session) {
|
|
9529
|
+
send(channel("session-update"), toSummary(session));
|
|
9530
|
+
const label = PHASE_LABELS[session.phase];
|
|
9531
|
+
if (label && notify) {
|
|
9532
|
+
notify(
|
|
9533
|
+
`Board AI - ${label}`,
|
|
9534
|
+
`${session.taskTitle || "Card"} (step ${session.step}/3)`,
|
|
9535
|
+
session.phase !== "complete" && session.phase !== "error" && session.phase !== "awaiting-input"
|
|
9536
|
+
);
|
|
9537
|
+
}
|
|
9538
|
+
},
|
|
9539
|
+
emitSessionRemoved(postId) {
|
|
9540
|
+
send(channel("session-update"), {
|
|
9541
|
+
postId,
|
|
9542
|
+
phase: null,
|
|
9543
|
+
mode: "claude",
|
|
9544
|
+
step: 1,
|
|
9545
|
+
taskId: "",
|
|
9546
|
+
diff: "",
|
|
9547
|
+
changedFiles: [],
|
|
9548
|
+
commitMessage: "",
|
|
9549
|
+
previewUrl: "",
|
|
9550
|
+
previewRunning: false,
|
|
9551
|
+
reproSteps: "",
|
|
9552
|
+
pendingQuestion: "",
|
|
9553
|
+
error: "",
|
|
9554
|
+
startedAt: 0,
|
|
9555
|
+
targetBranch: "",
|
|
9556
|
+
taskTitle: "",
|
|
9557
|
+
todos: []
|
|
9558
|
+
});
|
|
9559
|
+
},
|
|
9560
|
+
emitLog(session, entry) {
|
|
9561
|
+
send(channel(`log:${session.postId}`), entry);
|
|
9562
|
+
},
|
|
9563
|
+
addLog(session, type, content) {
|
|
9564
|
+
const entry = { time: Date.now(), type, content };
|
|
9565
|
+
session.logs.push(entry);
|
|
9566
|
+
this.emitLog(session, entry);
|
|
9567
|
+
},
|
|
9568
|
+
appendTextLog(session, text) {
|
|
9569
|
+
const last = session.logs[session.logs.length - 1];
|
|
9570
|
+
if (last && last.type === "text") {
|
|
9571
|
+
last.content += text;
|
|
9572
|
+
this.emitLog(session, {
|
|
9573
|
+
time: last.time,
|
|
9574
|
+
type: "text-append",
|
|
9575
|
+
content: text
|
|
9576
|
+
});
|
|
9577
|
+
} else {
|
|
9578
|
+
const entry = {
|
|
9579
|
+
time: Date.now(),
|
|
9580
|
+
type: "text",
|
|
9581
|
+
content: text
|
|
9582
|
+
};
|
|
9583
|
+
session.logs.push(entry);
|
|
9584
|
+
this.emitLog(session, entry);
|
|
9585
|
+
}
|
|
9586
|
+
},
|
|
9587
|
+
emitSetupStatus(message) {
|
|
9588
|
+
send(channel("setup-status"), message);
|
|
9589
|
+
},
|
|
9590
|
+
emitOneDriveAuth(payload) {
|
|
9591
|
+
send(channel("onedrive-auth"), payload);
|
|
9592
|
+
},
|
|
9593
|
+
emitOneDriveSync(payload) {
|
|
9594
|
+
send(channel("onedrive-sync"), payload);
|
|
9595
|
+
}
|
|
9596
|
+
};
|
|
9597
|
+
}
|
|
9598
|
+
|
|
9599
|
+
// ../../shared/all/helpers/card-ai/frpc.ts
|
|
9600
|
+
var import_child_process5 = require("child_process");
|
|
9601
|
+
var import_fs2 = require("fs");
|
|
9602
|
+
var import_path2 = __toESM(require("path"));
|
|
9603
|
+
var import_util3 = require("util");
|
|
9604
|
+
|
|
9605
|
+
// ../../shared/all/helpers/card-ai/scripts.ts
|
|
9606
|
+
var FRPC_EXPOSE_SCRIPT = `#!/bin/sh
|
|
9607
|
+
set -e
|
|
9608
|
+
set -a; . /etc/environment.factiii 2>/dev/null || true; set +a
|
|
9609
|
+
PORT="$1"
|
|
9610
|
+
NAME="$2"
|
|
9611
|
+
if [ -z "$PORT" ] || [ -z "$NAME" ]; then
|
|
9612
|
+
echo "usage: factiii-expose <port> <name>" >&2
|
|
9613
|
+
echo " e.g. factiii-expose 3000 app" >&2
|
|
9614
|
+
exit 1
|
|
9615
|
+
fi
|
|
9616
|
+
case "$NAME" in *[!a-zA-Z0-9-]*|"") echo "name must be [a-zA-Z0-9-]" >&2; exit 1 ;; esac
|
|
9617
|
+
PROXY="\${FACTIII_TASK_ID}-\${NAME}"
|
|
9618
|
+
DOMAIN="\${PROXY}.\${FACTIII_FRP_DOMAIN}"
|
|
9619
|
+
TOML="/tmp/frpc-\${NAME}.toml"
|
|
9620
|
+
cat > "$TOML" <<EOF
|
|
9621
|
+
serverAddr = "\${FACTIII_FRP_SERVER}"
|
|
9622
|
+
serverPort = \${FACTIII_FRP_PORT}
|
|
9623
|
+
transport.protocol = "tcp"
|
|
9624
|
+
|
|
9625
|
+
[metadatas]
|
|
9626
|
+
factiii_token = "\${FACTIII_FRP_TOKEN}"
|
|
9627
|
+
|
|
9628
|
+
[[proxies]]
|
|
9629
|
+
name = "\${PROXY}"
|
|
9630
|
+
type = "http"
|
|
9631
|
+
localPort = \${PORT}
|
|
9632
|
+
subdomain = "\${PROXY}"
|
|
9633
|
+
EOF
|
|
9634
|
+
setsid frpc -c "$TOML" >/tmp/frpc-\${NAME}.log 2>&1 < /dev/null &
|
|
9635
|
+
echo $! > /tmp/frpc-\${NAME}.pid
|
|
9636
|
+
echo "http://\${DOMAIN}"
|
|
9637
|
+
`;
|
|
9638
|
+
var FRPC_UNEXPOSE_SCRIPT = `#!/bin/sh
|
|
9639
|
+
NAME="$1"
|
|
9640
|
+
if [ -z "$NAME" ]; then
|
|
9641
|
+
echo "usage: factiii-unexpose <name>" >&2
|
|
9642
|
+
exit 1
|
|
9643
|
+
fi
|
|
9644
|
+
PID_FILE="/tmp/frpc-\${NAME}.pid"
|
|
9645
|
+
if [ -f "$PID_FILE" ]; then
|
|
9646
|
+
kill "$(cat "$PID_FILE")" 2>/dev/null || true
|
|
9647
|
+
rm -f "$PID_FILE" "/tmp/frpc-\${NAME}.toml" "/tmp/frpc-\${NAME}.log"
|
|
9648
|
+
echo "stopped \${NAME}"
|
|
9649
|
+
else
|
|
9650
|
+
echo "no tunnel named \${NAME}" >&2
|
|
9651
|
+
exit 1
|
|
9652
|
+
fi
|
|
9653
|
+
`;
|
|
9654
|
+
|
|
9655
|
+
// ../../shared/all/helpers/card-ai/frpc.ts
|
|
9656
|
+
var execFileAsync3 = (0, import_util3.promisify)(import_child_process5.execFile);
|
|
9657
|
+
var FRP_SERVER_ADDR = "dev.factiii.com";
|
|
9658
|
+
var FRP_SERVER_PORT = 7443;
|
|
9659
|
+
var FRP_DOMAIN_SUFFIX = "dev.factiii.com";
|
|
9660
|
+
async function writeFileAsRoot(containerName, path7, contents) {
|
|
9661
|
+
const b64 = Buffer.from(contents, "utf-8").toString("base64");
|
|
9662
|
+
await execInContainerAsRoot(
|
|
9663
|
+
containerName,
|
|
9664
|
+
`echo '${b64}' | base64 -d > '${path7}'`
|
|
9665
|
+
);
|
|
9666
|
+
}
|
|
9667
|
+
async function installFrpcBinary(containerName) {
|
|
9668
|
+
const arch = (await execInContainer(containerName, "uname -m")).trim();
|
|
9669
|
+
const map = {
|
|
9670
|
+
x86_64: "frpc-linux-amd64",
|
|
9671
|
+
aarch64: "frpc-linux-arm64"
|
|
9672
|
+
};
|
|
9673
|
+
const binaryName = map[arch];
|
|
9674
|
+
if (!binaryName) {
|
|
9675
|
+
throw new Error(`Unsupported container arch for frpc: ${arch}`);
|
|
9676
|
+
}
|
|
9677
|
+
const src = import_path2.default.join(__dirname, "..", "vendor", binaryName);
|
|
9678
|
+
if (!(0, import_fs2.existsSync)(src)) {
|
|
9679
|
+
throw new Error(
|
|
9680
|
+
`Vendored frpc missing (${src}). Run apps/runner/scripts/fetch-frpc.sh.`
|
|
9681
|
+
);
|
|
9682
|
+
}
|
|
9683
|
+
await execFileAsync3("docker", [
|
|
9684
|
+
"cp",
|
|
9685
|
+
src,
|
|
9686
|
+
`${containerName}:/usr/local/bin/frpc`
|
|
9687
|
+
]);
|
|
9688
|
+
await execInContainerAsRoot(containerName, "chmod +x /usr/local/bin/frpc");
|
|
9689
|
+
}
|
|
9690
|
+
async function installFrpcHarness(params) {
|
|
9691
|
+
const { containerName, taskId, runnerToken } = params;
|
|
9692
|
+
await installFrpcBinary(containerName);
|
|
9693
|
+
const envLines = [
|
|
9694
|
+
`FACTIII_FRP_SERVER=${FRP_SERVER_ADDR}`,
|
|
9695
|
+
`FACTIII_FRP_PORT=${FRP_SERVER_PORT}`,
|
|
9696
|
+
`FACTIII_FRP_TOKEN=${runnerToken}`,
|
|
9697
|
+
`FACTIII_FRP_DOMAIN=${FRP_DOMAIN_SUFFIX}`,
|
|
9698
|
+
`FACTIII_TASK_ID=${taskId}`
|
|
9699
|
+
];
|
|
9700
|
+
await writeFileAsRoot(
|
|
9701
|
+
containerName,
|
|
9702
|
+
"/usr/local/bin/factiii-expose",
|
|
9703
|
+
FRPC_EXPOSE_SCRIPT
|
|
9704
|
+
);
|
|
9705
|
+
await writeFileAsRoot(
|
|
9706
|
+
containerName,
|
|
9707
|
+
"/usr/local/bin/factiii-unexpose",
|
|
9708
|
+
FRPC_UNEXPOSE_SCRIPT
|
|
9709
|
+
);
|
|
9710
|
+
await execInContainerAsRoot(
|
|
9711
|
+
containerName,
|
|
9712
|
+
"chmod +x /usr/local/bin/factiii-expose /usr/local/bin/factiii-unexpose"
|
|
9713
|
+
);
|
|
9714
|
+
await writeFileAsRoot(
|
|
9715
|
+
containerName,
|
|
9716
|
+
"/etc/environment.factiii",
|
|
9717
|
+
envLines.join("\n") + "\n"
|
|
9718
|
+
);
|
|
9719
|
+
await execInContainerAsRoot(
|
|
9720
|
+
containerName,
|
|
9721
|
+
`grep -q 'environment.factiii' /home/claude/.profile 2>/dev/null || echo 'set -a; . /etc/environment.factiii; set +a' >> /home/claude/.profile && chown claude:claude /home/claude/.profile`
|
|
9722
|
+
);
|
|
9723
|
+
}
|
|
9724
|
+
|
|
9725
|
+
// ../../shared/all/helpers/card-ai/claude-mode.ts
|
|
9726
|
+
function buildPrompt(lines, vars = {}) {
|
|
9727
|
+
let text = lines.join("\n");
|
|
9728
|
+
for (const [key, value2] of Object.entries(vars)) {
|
|
9729
|
+
text = text.replaceAll(`{{${key}}}`, value2);
|
|
9730
|
+
}
|
|
9731
|
+
return text;
|
|
9732
|
+
}
|
|
9733
|
+
var ONEDRIVE_PROMPT_SWAPS = [
|
|
9734
|
+
[
|
|
9735
|
+
"- Full clone with complete git history available.",
|
|
9736
|
+
"- Your file changes are saved automatically when the task finishes."
|
|
9737
|
+
],
|
|
9738
|
+
[
|
|
9739
|
+
"1. Review the diff of your changes (git diff HEAD)",
|
|
9740
|
+
"1. Review the changes you made"
|
|
9741
|
+
]
|
|
9742
|
+
];
|
|
9743
|
+
function gitFree(text) {
|
|
9744
|
+
let out = text;
|
|
9745
|
+
for (const [from, to] of ONEDRIVE_PROMPT_SWAPS) out = out.replace(from, to);
|
|
9746
|
+
return out;
|
|
9747
|
+
}
|
|
9748
|
+
var ClaudeModeEngine = class _ClaudeModeEngine {
|
|
9749
|
+
constructor(core, emitter) {
|
|
9750
|
+
this.core = core;
|
|
9751
|
+
this.emitter = emitter;
|
|
9752
|
+
this.sessions = /* @__PURE__ */ new Map();
|
|
9753
|
+
this.board = { sessionId: null, process: null, messages: [] };
|
|
9754
|
+
}
|
|
9755
|
+
static {
|
|
9756
|
+
this.QUESTION_MARKER = "AWAITING_INPUT";
|
|
9757
|
+
}
|
|
9758
|
+
static {
|
|
9759
|
+
this.FULL_TOOLS = "Read,Grep,Glob,Agent,Edit,Write,Bash,TodoWrite";
|
|
9760
|
+
}
|
|
9761
|
+
// ── Board AI ("Ask AI") ──
|
|
9762
|
+
async ask(params, sendLog) {
|
|
9763
|
+
if (!this.core.isReady()) {
|
|
9764
|
+
throw new Error(
|
|
9765
|
+
'Environment not initialized. Open Board AI Settings and click "Initialize Environment".'
|
|
9766
|
+
);
|
|
9767
|
+
}
|
|
9768
|
+
const containerName = this.core.containerName;
|
|
9769
|
+
this.board.messages.push({ role: "user", content: params.question });
|
|
9770
|
+
let prompt2;
|
|
9771
|
+
if (this.board.sessionId) {
|
|
9772
|
+
prompt2 = params.question;
|
|
9773
|
+
} else {
|
|
9774
|
+
const parts2 = ["Here is the board data:", "", params.boardContext || ""];
|
|
9775
|
+
parts2.push("", params.question);
|
|
9776
|
+
prompt2 = parts2.join("\n");
|
|
9777
|
+
}
|
|
9778
|
+
const systemPrompt = [params.componentSchema, buildPrompt(prompts_default.boardAI)].filter(Boolean).join("\n\n");
|
|
9779
|
+
const { proc, result } = spawnClaude({
|
|
9780
|
+
containerName,
|
|
9781
|
+
prompt: prompt2,
|
|
9782
|
+
allowedTools: "Read,Grep,Glob,Agent",
|
|
9783
|
+
model: "claude-opus-4-6",
|
|
9784
|
+
includePartialMessages: true,
|
|
9785
|
+
appendSystemPrompt: systemPrompt,
|
|
9786
|
+
resumeSessionId: this.board.sessionId || void 0,
|
|
9787
|
+
callbacks: {
|
|
9788
|
+
onLog: (entry) => sendLog(entry)
|
|
9789
|
+
}
|
|
9790
|
+
});
|
|
9791
|
+
this.board.process = proc;
|
|
9792
|
+
try {
|
|
9793
|
+
const { text, sessionId } = await result;
|
|
9794
|
+
if (sessionId) this.board.sessionId = sessionId;
|
|
9795
|
+
this.board.messages.push({ role: "assistant", content: text });
|
|
9796
|
+
return text;
|
|
9797
|
+
} finally {
|
|
9798
|
+
this.board.process = null;
|
|
9799
|
+
}
|
|
9800
|
+
}
|
|
9801
|
+
/** The persisted Ask AI conversation, for the client to hydrate on open. */
|
|
9802
|
+
boardSession() {
|
|
9803
|
+
return { messages: this.board.messages.slice() };
|
|
9804
|
+
}
|
|
9805
|
+
resetBoardSession() {
|
|
9806
|
+
if (this.board.process) {
|
|
9807
|
+
try {
|
|
9808
|
+
this.board.process.kill("SIGTERM");
|
|
9809
|
+
} catch {
|
|
9810
|
+
}
|
|
9811
|
+
}
|
|
9812
|
+
this.board = { sessionId: null, process: null, messages: [] };
|
|
9813
|
+
}
|
|
9814
|
+
// ── Claude runner ──
|
|
9815
|
+
/** Strip git mentions from an agent prompt on OneDrive spaces; pass-through otherwise. */
|
|
9816
|
+
agentPrompt(text) {
|
|
9817
|
+
return this.core.isOneDrive(this.core.readConfig()) ? gitFree(text) : text;
|
|
9818
|
+
}
|
|
9819
|
+
async runClaudeInContainer(session, prompt2, tools, resumeSessionId, options) {
|
|
9820
|
+
const { proc, result } = spawnClaude({
|
|
9821
|
+
containerName: session.containerName,
|
|
9822
|
+
prompt: prompt2,
|
|
9823
|
+
allowedTools: tools,
|
|
9824
|
+
disallowedTools: options?.disallowedTools,
|
|
9825
|
+
permissionMode: options?.permissionMode,
|
|
9826
|
+
appendSystemPrompt: this.agentPrompt(buildPrompt(prompts_default.system)),
|
|
9827
|
+
effort: session.effort,
|
|
9828
|
+
model: "opus",
|
|
9829
|
+
resumeSessionId,
|
|
9830
|
+
callbacks: {
|
|
9831
|
+
onLog: (entry) => {
|
|
9832
|
+
if (entry.type === "text-append") {
|
|
9833
|
+
this.emitter.appendTextLog(session, entry.content);
|
|
9834
|
+
} else {
|
|
9835
|
+
this.emitter.addLog(session, entry.type, entry.content);
|
|
9836
|
+
}
|
|
9837
|
+
},
|
|
9838
|
+
onTodos: (todos) => {
|
|
9839
|
+
session.todos = todos;
|
|
9840
|
+
this.emitter.emitSessionUpdate(session);
|
|
9841
|
+
}
|
|
9842
|
+
}
|
|
9843
|
+
});
|
|
9844
|
+
session.proc = proc;
|
|
9845
|
+
try {
|
|
9846
|
+
return await result;
|
|
9847
|
+
} finally {
|
|
9848
|
+
session.proc = null;
|
|
9849
|
+
}
|
|
9850
|
+
}
|
|
9851
|
+
// ── Research with Q&A ──
|
|
9852
|
+
async runResearch(session, prompt2, resumeSessionId) {
|
|
9853
|
+
if (!this.sessions.has(session.postId)) return;
|
|
9854
|
+
const { text, sessionId } = await this.runClaudeInContainer(
|
|
9855
|
+
session,
|
|
9856
|
+
prompt2,
|
|
9857
|
+
"Read,Grep,Glob,Agent,TodoWrite",
|
|
9858
|
+
resumeSessionId,
|
|
9859
|
+
{ permissionMode: "plan", disallowedTools: "AskUserQuestion" }
|
|
9860
|
+
);
|
|
9861
|
+
session.claudeSessionId = sessionId;
|
|
9862
|
+
if (!this.sessions.has(session.postId)) return;
|
|
9863
|
+
const markerIdx = text.lastIndexOf(_ClaudeModeEngine.QUESTION_MARKER);
|
|
9864
|
+
if (markerIdx === -1) return;
|
|
9865
|
+
session.phase = "awaiting-input";
|
|
9866
|
+
session.pendingQuestion = text.slice(markerIdx + _ClaudeModeEngine.QUESTION_MARKER.length).trim();
|
|
9867
|
+
this.emitter.emitSessionUpdate(session);
|
|
9868
|
+
this.emitter.addLog(
|
|
9869
|
+
session,
|
|
9870
|
+
"system",
|
|
9871
|
+
"Claude has a question - waiting for your input."
|
|
9872
|
+
);
|
|
9873
|
+
const answer = await new Promise((resolve) => {
|
|
9874
|
+
session.answerResolver = resolve;
|
|
9875
|
+
});
|
|
9876
|
+
if (!this.sessions.has(session.postId)) return;
|
|
9877
|
+
session.phase = "research";
|
|
9878
|
+
session.pendingQuestion = "";
|
|
9879
|
+
session.answerResolver = null;
|
|
9880
|
+
this.emitter.emitSessionUpdate(session);
|
|
9881
|
+
this.emitter.addLog(session, "system", "Resuming research...");
|
|
9882
|
+
return this.runResearch(session, answer, sessionId);
|
|
9883
|
+
}
|
|
9884
|
+
// ── Pipeline ──
|
|
9885
|
+
async runPipeline(session, prompt2, baseBranch, runtimes) {
|
|
9886
|
+
if (!this.core.isReady()) {
|
|
9887
|
+
throw new Error(
|
|
9888
|
+
'Environment not initialized. Open Board AI Settings and click "Initialize Environment".'
|
|
9889
|
+
);
|
|
9890
|
+
}
|
|
9891
|
+
const config = this.core.readConfig();
|
|
9892
|
+
this.emitter.addLog(session, "system", "Starting container...");
|
|
9893
|
+
const onedrive = this.core.isOneDrive(config);
|
|
9894
|
+
await killContainer(session.containerName);
|
|
9895
|
+
await spawnContainer(session.containerName, config.claudeToken, {
|
|
9896
|
+
fuse: onedrive
|
|
9897
|
+
});
|
|
9898
|
+
if (onedrive) {
|
|
9899
|
+
const rotated = await writeRcloneMountConfig(
|
|
9900
|
+
session.containerName,
|
|
9901
|
+
this.core.getOneDriveConnection()
|
|
9902
|
+
);
|
|
9903
|
+
this.core.updateOneDriveRefreshToken(rotated);
|
|
9904
|
+
await mountWorkspace(session.containerName, this.core.remotePath());
|
|
9905
|
+
startOneDriveSyncWatcher(
|
|
9906
|
+
session.containerName,
|
|
9907
|
+
(pending, files) => this.emitter.emitOneDriveSync({
|
|
9908
|
+
postId: session.postId,
|
|
9909
|
+
pending,
|
|
9910
|
+
files
|
|
9911
|
+
})
|
|
9912
|
+
);
|
|
9913
|
+
this.emitter.addLog(session, "system", "OneDrive folder mounted.");
|
|
9914
|
+
} else {
|
|
9915
|
+
const cloneUrl = buildCloneUrl(config.repoUrl, config.githubToken);
|
|
9916
|
+
const branch = baseBranch || config.mainBranch || "main";
|
|
9917
|
+
const cacheKey = repoCacheKey(config.repoUrl);
|
|
9918
|
+
const { fromCache } = await cloneRepo(
|
|
9919
|
+
session.containerName,
|
|
9920
|
+
cloneUrl,
|
|
9921
|
+
branch,
|
|
9922
|
+
cacheKey
|
|
9923
|
+
);
|
|
9924
|
+
this.emitter.addLog(
|
|
9925
|
+
session,
|
|
9926
|
+
"system",
|
|
9927
|
+
fromCache ? "Repository updated from cache." : "Repository cloned."
|
|
9928
|
+
);
|
|
9929
|
+
await setGitIdentity(
|
|
9930
|
+
session.containerName,
|
|
9931
|
+
config.gitName,
|
|
9932
|
+
config.gitEmail
|
|
9933
|
+
);
|
|
9934
|
+
}
|
|
9935
|
+
await installRuntimes(this.emitter, session, runtimes);
|
|
9936
|
+
this.emitter.addLog(session, "system", "Container ready.");
|
|
9937
|
+
session.phase = "research";
|
|
9938
|
+
session.step = 1;
|
|
9939
|
+
this.emitter.emitSessionUpdate(session);
|
|
9940
|
+
this.emitter.addLog(session, "system", "\u2500\u2500 Step 1: Research \u2500\u2500");
|
|
9941
|
+
await this.runResearch(
|
|
9942
|
+
session,
|
|
9332
9943
|
buildPrompt(prompts_default.research, { task: prompt2 })
|
|
9333
9944
|
);
|
|
9334
9945
|
if (!this.sessions.has(session.postId)) return;
|
|
9335
9946
|
await this.runImplementation(
|
|
9336
9947
|
session,
|
|
9337
9948
|
buildPrompt(prompts_default.implement),
|
|
9338
|
-
"\u2500\u2500 Step"
|
|
9339
|
-
|
|
9949
|
+
"\u2500\u2500 Step"
|
|
9950
|
+
);
|
|
9951
|
+
}
|
|
9952
|
+
async runImplementation(session, implementPrompt, logPrefix) {
|
|
9953
|
+
const tools = _ClaudeModeEngine.FULL_TOOLS;
|
|
9954
|
+
session.phase = "implement";
|
|
9955
|
+
session.step = 2;
|
|
9956
|
+
this.emitter.emitSessionUpdate(session);
|
|
9957
|
+
this.emitter.addLog(session, "system", `${logPrefix} 2: Implement \u2500\u2500`);
|
|
9958
|
+
const implResult = await this.runClaudeInContainer(
|
|
9959
|
+
session,
|
|
9960
|
+
implementPrompt,
|
|
9961
|
+
tools,
|
|
9962
|
+
session.claudeSessionId
|
|
9963
|
+
);
|
|
9964
|
+
session.claudeSessionId = implResult.sessionId || session.claudeSessionId;
|
|
9965
|
+
if (!this.sessions.has(session.postId)) return;
|
|
9966
|
+
session.phase = "review";
|
|
9967
|
+
session.step = 3;
|
|
9968
|
+
this.emitter.emitSessionUpdate(session);
|
|
9969
|
+
this.emitter.addLog(session, "system", `${logPrefix} 3: Review \u2500\u2500`);
|
|
9970
|
+
const reviewResult = await this.runClaudeInContainer(
|
|
9971
|
+
session,
|
|
9972
|
+
this.agentPrompt(buildPrompt(prompts_default.review)),
|
|
9973
|
+
tools,
|
|
9974
|
+
session.claudeSessionId
|
|
9340
9975
|
);
|
|
9976
|
+
session.claudeSessionId = reviewResult.sessionId || session.claudeSessionId;
|
|
9977
|
+
if (!this.sessions.has(session.postId)) return;
|
|
9978
|
+
session.reproSteps = reviewResult.text.trim();
|
|
9979
|
+
await this.extractAndComplete(session);
|
|
9980
|
+
}
|
|
9981
|
+
// On-demand dev-server preview: reuses the live container + Claude session,
|
|
9982
|
+
// maps the port (or a public frpc tunnel), stores the URL. Stays in `complete`.
|
|
9983
|
+
async runPreview(session) {
|
|
9984
|
+
const tools = _ClaudeModeEngine.FULL_TOOLS;
|
|
9985
|
+
session.previewRunning = true;
|
|
9986
|
+
this.emitter.addLog(session, "system", "Setting up live preview...");
|
|
9987
|
+
this.emitter.emitSessionUpdate(session);
|
|
9988
|
+
const portsEnv = (await execInContainer(session.containerName, "echo -n $FACTIII_PORTS")).trim();
|
|
9989
|
+
const publicPreviewEnabled = !!this.core.runnerToken;
|
|
9990
|
+
if (publicPreviewEnabled && this.core.runnerToken) {
|
|
9991
|
+
await installFrpcHarness({
|
|
9992
|
+
containerName: session.containerName,
|
|
9993
|
+
taskId: session.taskId,
|
|
9994
|
+
runnerToken: this.core.runnerToken
|
|
9995
|
+
});
|
|
9996
|
+
}
|
|
9997
|
+
const previewResult = await this.runClaudeInContainer(
|
|
9998
|
+
session,
|
|
9999
|
+
buildPrompt(prompts_default.preview, {
|
|
10000
|
+
ports: portsEnv,
|
|
10001
|
+
publicPreviewBlock: publicPreviewEnabled ? prompts_default.previewPublic.join("\n") : ""
|
|
10002
|
+
}),
|
|
10003
|
+
tools,
|
|
10004
|
+
session.claudeSessionId
|
|
10005
|
+
);
|
|
10006
|
+
session.claudeSessionId = previewResult.sessionId || session.claudeSessionId;
|
|
10007
|
+
if (!this.sessions.has(session.postId)) return;
|
|
10008
|
+
const portMatch = previewResult.text.match(/PREVIEW_PORT:\s*(\d+)/);
|
|
10009
|
+
const previewPort = portMatch ? portMatch[1] : "3000";
|
|
10010
|
+
const urlMatch = previewResult.text.match(/PREVIEW_URL:\s*(\S+)/);
|
|
10011
|
+
if (publicPreviewEnabled && urlMatch) {
|
|
10012
|
+
session.previewUrl = urlMatch[1];
|
|
10013
|
+
this.emitter.addLog(
|
|
10014
|
+
session,
|
|
10015
|
+
"system",
|
|
10016
|
+
`Public preview available at ${session.previewUrl}`
|
|
10017
|
+
);
|
|
10018
|
+
} else {
|
|
10019
|
+
const previewUrl = await getContainerPort(
|
|
10020
|
+
session.containerName,
|
|
10021
|
+
previewPort
|
|
10022
|
+
);
|
|
10023
|
+
if (previewUrl) {
|
|
10024
|
+
session.previewUrl = previewUrl;
|
|
10025
|
+
this.emitter.addLog(
|
|
10026
|
+
session,
|
|
10027
|
+
"system",
|
|
10028
|
+
`Preview available at ${session.previewUrl}`
|
|
10029
|
+
);
|
|
10030
|
+
} else {
|
|
10031
|
+
this.emitter.addLog(
|
|
10032
|
+
session,
|
|
10033
|
+
"system",
|
|
10034
|
+
"Could not map preview port. Server may not be running."
|
|
10035
|
+
);
|
|
10036
|
+
}
|
|
10037
|
+
}
|
|
10038
|
+
this.emitter.emitSessionUpdate(session);
|
|
9341
10039
|
}
|
|
9342
|
-
|
|
9343
|
-
|
|
9344
|
-
|
|
9345
|
-
|
|
9346
|
-
|
|
9347
|
-
|
|
9348
|
-
|
|
9349
|
-
|
|
9350
|
-
|
|
9351
|
-
}) {
|
|
9352
|
-
const postId = String(rawPostId);
|
|
9353
|
-
const existing = this.sessions.get(postId);
|
|
9354
|
-
if (existing && (existing.phase === "starting" || existing.phase === "research" || existing.phase === "implement")) {
|
|
9355
|
-
throw new Error("A session is already running for this card.");
|
|
10040
|
+
async extractAndComplete(session) {
|
|
10041
|
+
const config = this.core.readConfig();
|
|
10042
|
+
if (this.core.isOneDrive(config)) {
|
|
10043
|
+
session.diff = "";
|
|
10044
|
+
session.changedFiles = [];
|
|
10045
|
+
session.commitMessage = "";
|
|
10046
|
+
session.phase = "complete";
|
|
10047
|
+
this.emitter.emitSessionUpdate(session);
|
|
10048
|
+
return;
|
|
9356
10049
|
}
|
|
9357
|
-
|
|
9358
|
-
|
|
9359
|
-
|
|
10050
|
+
this.emitter.addLog(session, "system", "Extracting changes...");
|
|
10051
|
+
const { diff, changedFiles } = await extractDiff(session.containerName);
|
|
10052
|
+
session.diff = diff;
|
|
10053
|
+
session.changedFiles = changedFiles;
|
|
10054
|
+
if (changedFiles.length > 0) {
|
|
10055
|
+
this.emitter.addLog(
|
|
10056
|
+
session,
|
|
10057
|
+
"system",
|
|
10058
|
+
`${changedFiles.length} file${changedFiles.length !== 1 ? "s" : ""} changed.`
|
|
10059
|
+
);
|
|
10060
|
+
this.emitter.addLog(session, "system", "Generating commit message...");
|
|
10061
|
+
session.commitMessage = await generateCommitMessage(
|
|
10062
|
+
session.containerName,
|
|
10063
|
+
diff
|
|
10064
|
+
);
|
|
10065
|
+
} else {
|
|
10066
|
+
this.emitter.addLog(session, "system", "No files changed.");
|
|
9360
10067
|
}
|
|
9361
|
-
|
|
10068
|
+
session.phase = "complete";
|
|
10069
|
+
this.emitter.emitSessionUpdate(session);
|
|
10070
|
+
}
|
|
10071
|
+
// ── Lifecycle / ModeEngine ──
|
|
10072
|
+
start(base, args) {
|
|
9362
10073
|
const session = {
|
|
9363
|
-
|
|
9364
|
-
postId,
|
|
9365
|
-
phase: "starting",
|
|
9366
|
-
step: 1,
|
|
9367
|
-
logs: [],
|
|
9368
|
-
diff: "",
|
|
9369
|
-
changedFiles: [],
|
|
9370
|
-
commitMessage: "",
|
|
9371
|
-
previewUrl: "",
|
|
9372
|
-
reproSteps: "",
|
|
9373
|
-
pendingQuestion: "",
|
|
9374
|
-
error: "",
|
|
9375
|
-
startedAt: Date.now(),
|
|
9376
|
-
taskTitle: taskTitle || "",
|
|
9377
|
-
targetBranch: targetBranch || "",
|
|
9378
|
-
effort: effort || "high",
|
|
9379
|
-
todos: [],
|
|
9380
|
-
containerName: `factiii-${this.instancePrefix}-card-${taskId}`,
|
|
10074
|
+
...base,
|
|
9381
10075
|
claudeSessionId: "",
|
|
9382
10076
|
answerResolver: null,
|
|
9383
|
-
|
|
10077
|
+
proc: null
|
|
9384
10078
|
};
|
|
9385
|
-
this.sessions.set(postId, session);
|
|
10079
|
+
this.sessions.set(session.postId, session);
|
|
9386
10080
|
this.emitter.emitSessionUpdate(session);
|
|
10081
|
+
const onError = (err) => {
|
|
10082
|
+
if (!this.sessions.has(session.postId)) return;
|
|
10083
|
+
session.phase = "error";
|
|
10084
|
+
session.error = err instanceof Error ? err.message : String(err);
|
|
10085
|
+
this.emitter.addLog(session, "error", session.error);
|
|
10086
|
+
this.emitter.emitSessionUpdate(session);
|
|
10087
|
+
};
|
|
9387
10088
|
this.runPipeline(
|
|
9388
10089
|
session,
|
|
9389
|
-
|
|
9390
|
-
|
|
9391
|
-
|
|
9392
|
-
).catch(
|
|
10090
|
+
args.prompt,
|
|
10091
|
+
args.baseBranch,
|
|
10092
|
+
args.runtimes
|
|
10093
|
+
).catch(onError);
|
|
10094
|
+
}
|
|
10095
|
+
// Triggered from the review (complete) phase; returns the live preview URL.
|
|
10096
|
+
async cardStartPreview({
|
|
10097
|
+
postId: rawPostId
|
|
10098
|
+
}) {
|
|
10099
|
+
const postId = String(rawPostId);
|
|
10100
|
+
const session = this.sessions.get(postId);
|
|
10101
|
+
if (!session) throw new Error("No active session for this card.");
|
|
10102
|
+
if (session.phase !== "complete")
|
|
10103
|
+
throw new Error("Preview is only available once changes are ready.");
|
|
10104
|
+
if (!session.claudeSessionId)
|
|
10105
|
+
throw new Error("No Claude session to resume for preview.");
|
|
10106
|
+
if (session.previewRunning || session.proc)
|
|
10107
|
+
return { previewUrl: session.previewUrl };
|
|
10108
|
+
try {
|
|
10109
|
+
await this.runPreview(session);
|
|
10110
|
+
} finally {
|
|
10111
|
+
session.previewRunning = false;
|
|
10112
|
+
if (this.sessions.has(postId)) this.emitter.emitSessionUpdate(session);
|
|
10113
|
+
}
|
|
10114
|
+
return { previewUrl: session.previewUrl };
|
|
10115
|
+
}
|
|
10116
|
+
cardAnswerQuestion({
|
|
10117
|
+
postId: rawPostId,
|
|
10118
|
+
answer
|
|
10119
|
+
}) {
|
|
10120
|
+
const postId = String(rawPostId);
|
|
10121
|
+
const session = this.sessions.get(postId);
|
|
10122
|
+
if (!session || session.phase !== "awaiting-input" || !session.answerResolver)
|
|
10123
|
+
return;
|
|
10124
|
+
session.answerResolver(answer);
|
|
10125
|
+
}
|
|
10126
|
+
cardFeedback({
|
|
10127
|
+
postId: rawPostId,
|
|
10128
|
+
feedback
|
|
10129
|
+
}) {
|
|
10130
|
+
const postId = String(rawPostId);
|
|
10131
|
+
const session = this.sessions.get(postId);
|
|
10132
|
+
if (!session || session.phase !== "complete") return;
|
|
10133
|
+
if (!session.claudeSessionId) return;
|
|
10134
|
+
session.diff = "";
|
|
10135
|
+
session.changedFiles = [];
|
|
10136
|
+
session.commitMessage = "";
|
|
10137
|
+
session.reproSteps = "";
|
|
10138
|
+
const prompt2 = `The user reviewed your changes and has the following feedback:
|
|
10139
|
+
|
|
10140
|
+
${feedback}
|
|
10141
|
+
|
|
10142
|
+
${buildPrompt(prompts_default.implement)}`;
|
|
10143
|
+
this.runImplementation(session, prompt2, "\u2500\u2500 Follow-up").catch((err) => {
|
|
9393
10144
|
if (!this.sessions.has(postId)) return;
|
|
9394
10145
|
session.phase = "error";
|
|
9395
10146
|
session.error = err instanceof Error ? err.message : String(err);
|
|
9396
10147
|
this.emitter.addLog(session, "error", session.error);
|
|
9397
10148
|
this.emitter.emitSessionUpdate(session);
|
|
9398
10149
|
});
|
|
9399
|
-
return { taskId, postId };
|
|
9400
10150
|
}
|
|
9401
|
-
|
|
9402
|
-
|
|
10151
|
+
listSummaries() {
|
|
10152
|
+
const result = {};
|
|
10153
|
+
for (const [postId, session] of this.sessions) {
|
|
10154
|
+
result[postId] = toSummary(session);
|
|
10155
|
+
}
|
|
10156
|
+
return result;
|
|
9403
10157
|
}
|
|
9404
|
-
|
|
9405
|
-
|
|
9406
|
-
|
|
9407
|
-
|
|
10158
|
+
get(postId) {
|
|
10159
|
+
return this.sessions.get(postId);
|
|
10160
|
+
}
|
|
10161
|
+
sessionWithLogs(postId) {
|
|
10162
|
+
const session = this.sessions.get(postId);
|
|
10163
|
+
if (!session) return null;
|
|
10164
|
+
return { ...toSummary(session), logs: session.logs };
|
|
10165
|
+
}
|
|
10166
|
+
has(postId) {
|
|
10167
|
+
return this.sessions.has(postId);
|
|
10168
|
+
}
|
|
10169
|
+
isRunning(postId) {
|
|
10170
|
+
const s = this.sessions.get(postId);
|
|
10171
|
+
return !!s && (s.phase === "starting" || s.phase === "research" || s.phase === "implement");
|
|
10172
|
+
}
|
|
10173
|
+
remove(postId) {
|
|
10174
|
+
this.sessions.delete(postId);
|
|
10175
|
+
}
|
|
10176
|
+
async kill(postId) {
|
|
10177
|
+
let session = this.sessions.get(postId);
|
|
10178
|
+
if (!session) {
|
|
10179
|
+
for (const [, s] of this.sessions) {
|
|
10180
|
+
if (s.taskId === postId) {
|
|
10181
|
+
session = s;
|
|
10182
|
+
break;
|
|
10183
|
+
}
|
|
10184
|
+
}
|
|
10185
|
+
}
|
|
10186
|
+
if (!session) return;
|
|
10187
|
+
if (session.answerResolver) {
|
|
10188
|
+
session.answerResolver("");
|
|
10189
|
+
session.answerResolver = null;
|
|
10190
|
+
}
|
|
10191
|
+
if (session.proc) {
|
|
10192
|
+
try {
|
|
10193
|
+
session.proc.kill("SIGTERM");
|
|
10194
|
+
} catch {
|
|
10195
|
+
}
|
|
10196
|
+
session.proc = null;
|
|
10197
|
+
}
|
|
10198
|
+
await killContainer(session.containerName);
|
|
10199
|
+
const removedPostId = session.postId;
|
|
10200
|
+
this.sessions.delete(removedPostId);
|
|
10201
|
+
this.emitter.emitSessionRemoved(removedPostId);
|
|
10202
|
+
}
|
|
10203
|
+
destroy() {
|
|
10204
|
+
this.resetBoardSession();
|
|
10205
|
+
for (const [, session] of this.sessions) {
|
|
10206
|
+
if (session.proc) {
|
|
10207
|
+
try {
|
|
10208
|
+
session.proc.kill("SIGTERM");
|
|
10209
|
+
} catch {
|
|
10210
|
+
}
|
|
10211
|
+
}
|
|
10212
|
+
void killContainer(session.containerName);
|
|
10213
|
+
}
|
|
10214
|
+
this.sessions.clear();
|
|
10215
|
+
}
|
|
10216
|
+
};
|
|
10217
|
+
|
|
10218
|
+
// ../../shared/all/helpers/terminal-ai/terminal-mode.ts
|
|
10219
|
+
var BARE_REPO_ROOT = "/home/claude/workspace";
|
|
10220
|
+
function posixNormalize(p) {
|
|
10221
|
+
const parts2 = p.split("/").filter(Boolean);
|
|
10222
|
+
const stack = [];
|
|
10223
|
+
for (const part of parts2) {
|
|
10224
|
+
if (part === ".") continue;
|
|
10225
|
+
if (part === "..") {
|
|
10226
|
+
if (stack.length) stack.pop();
|
|
10227
|
+
continue;
|
|
10228
|
+
}
|
|
10229
|
+
stack.push(part);
|
|
10230
|
+
}
|
|
10231
|
+
return "/" + stack.join("/");
|
|
10232
|
+
}
|
|
10233
|
+
function posixDirname(p) {
|
|
10234
|
+
const i = p.lastIndexOf("/");
|
|
10235
|
+
if (i <= 0) return "/";
|
|
10236
|
+
return p.slice(0, i);
|
|
10237
|
+
}
|
|
10238
|
+
function shEscape(s) {
|
|
10239
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
10240
|
+
}
|
|
10241
|
+
var TerminalModeEngine = class {
|
|
10242
|
+
constructor(core, emitter) {
|
|
10243
|
+
this.core = core;
|
|
10244
|
+
this.emitter = emitter;
|
|
10245
|
+
this.sessions = /* @__PURE__ */ new Map();
|
|
10246
|
+
}
|
|
10247
|
+
// ── Lifecycle / ModeEngine ──
|
|
10248
|
+
start(base, args) {
|
|
10249
|
+
const session = {
|
|
10250
|
+
...base,
|
|
10251
|
+
openedFiles: [],
|
|
10252
|
+
buffers: {},
|
|
10253
|
+
exposed: []
|
|
10254
|
+
};
|
|
10255
|
+
this.sessions.set(session.postId, session);
|
|
9408
10256
|
this.emitter.emitSessionUpdate(session);
|
|
9409
|
-
|
|
9410
|
-
|
|
9411
|
-
session
|
|
9412
|
-
|
|
9413
|
-
|
|
9414
|
-
session.claudeSessionId
|
|
9415
|
-
);
|
|
9416
|
-
session.claudeSessionId = implResult.sessionId || session.claudeSessionId;
|
|
9417
|
-
if (!this.sessions.has(session.postId)) return;
|
|
9418
|
-
if (enablePreview) {
|
|
9419
|
-
session.phase = "preview";
|
|
9420
|
-
session.step = 3;
|
|
10257
|
+
const onError = (err) => {
|
|
10258
|
+
if (!this.sessions.has(session.postId)) return;
|
|
10259
|
+
session.phase = "error";
|
|
10260
|
+
session.error = err instanceof Error ? err.message : String(err);
|
|
10261
|
+
this.emitter.addLog(session, "error", session.error);
|
|
9421
10262
|
this.emitter.emitSessionUpdate(session);
|
|
9422
|
-
|
|
9423
|
-
|
|
9424
|
-
|
|
9425
|
-
|
|
10263
|
+
};
|
|
10264
|
+
this.runBareSession(session, args.baseBranch, args.runtimes ?? []).catch(
|
|
10265
|
+
onError
|
|
10266
|
+
);
|
|
10267
|
+
}
|
|
10268
|
+
listSummaries() {
|
|
10269
|
+
const result = {};
|
|
10270
|
+
for (const [postId, session] of this.sessions) {
|
|
10271
|
+
result[postId] = toSummary(session);
|
|
10272
|
+
}
|
|
10273
|
+
return result;
|
|
10274
|
+
}
|
|
10275
|
+
get(postId) {
|
|
10276
|
+
return this.sessions.get(postId);
|
|
10277
|
+
}
|
|
10278
|
+
sessionWithLogs(postId) {
|
|
10279
|
+
const session = this.sessions.get(postId);
|
|
10280
|
+
if (!session) return null;
|
|
10281
|
+
return { ...toSummary(session), logs: session.logs };
|
|
10282
|
+
}
|
|
10283
|
+
has(postId) {
|
|
10284
|
+
return this.sessions.has(postId);
|
|
10285
|
+
}
|
|
10286
|
+
isRunning(postId) {
|
|
10287
|
+
const s = this.sessions.get(postId);
|
|
10288
|
+
return !!s && (s.phase === "starting" || s.phase === "bare-active");
|
|
10289
|
+
}
|
|
10290
|
+
remove(postId) {
|
|
10291
|
+
this.sessions.delete(postId);
|
|
10292
|
+
}
|
|
10293
|
+
async kill(postId) {
|
|
10294
|
+
let session = this.sessions.get(postId);
|
|
10295
|
+
if (!session) {
|
|
10296
|
+
for (const [, s] of this.sessions) {
|
|
10297
|
+
if (s.taskId === postId) {
|
|
10298
|
+
session = s;
|
|
10299
|
+
break;
|
|
10300
|
+
}
|
|
10301
|
+
}
|
|
10302
|
+
}
|
|
10303
|
+
if (!session) return;
|
|
10304
|
+
await killContainer(session.containerName);
|
|
10305
|
+
const removedPostId = session.postId;
|
|
10306
|
+
this.sessions.delete(removedPostId);
|
|
10307
|
+
this.emitter.emitSessionRemoved(removedPostId);
|
|
10308
|
+
}
|
|
10309
|
+
destroy() {
|
|
10310
|
+
for (const [, session] of this.sessions) {
|
|
10311
|
+
void killContainer(session.containerName);
|
|
10312
|
+
}
|
|
10313
|
+
this.sessions.clear();
|
|
10314
|
+
}
|
|
10315
|
+
/** Active container for a card, or null when none / terminated. */
|
|
10316
|
+
getCardContainerName(postId) {
|
|
10317
|
+
const session = this.sessions.get(String(postId));
|
|
10318
|
+
if (!session) return null;
|
|
10319
|
+
if (session.phase === "error" || session.phase === "done") return null;
|
|
10320
|
+
return session.containerName;
|
|
10321
|
+
}
|
|
10322
|
+
// ── container + Claude CLI, user drives ──
|
|
10323
|
+
async runBareSession(session, baseBranch, runtimes) {
|
|
10324
|
+
if (!this.core.isReady()) {
|
|
10325
|
+
throw new Error(
|
|
10326
|
+
'Environment not initialized. Open Board AI Settings and click "Initialize Environment".'
|
|
10327
|
+
);
|
|
10328
|
+
}
|
|
10329
|
+
const config = this.core.readConfig();
|
|
10330
|
+
this.emitter.addLog(session, "system", "Starting container...");
|
|
10331
|
+
const onedrive = this.core.isOneDrive(config);
|
|
10332
|
+
await killContainer(session.containerName);
|
|
10333
|
+
await spawnContainer(session.containerName, void 0, { fuse: onedrive });
|
|
10334
|
+
if (onedrive) {
|
|
10335
|
+
const rotated = await writeRcloneMountConfig(
|
|
10336
|
+
session.containerName,
|
|
10337
|
+
this.core.getOneDriveConnection()
|
|
10338
|
+
);
|
|
10339
|
+
this.core.updateOneDriveRefreshToken(rotated);
|
|
10340
|
+
await mountWorkspace(session.containerName, this.core.remotePath());
|
|
10341
|
+
startOneDriveSyncWatcher(
|
|
10342
|
+
session.containerName,
|
|
10343
|
+
(pending, files) => this.emitter.emitOneDriveSync({
|
|
10344
|
+
postId: session.postId,
|
|
10345
|
+
pending,
|
|
10346
|
+
files
|
|
10347
|
+
})
|
|
10348
|
+
);
|
|
10349
|
+
this.emitter.addLog(session, "system", "OneDrive folder mounted.");
|
|
10350
|
+
} else {
|
|
10351
|
+
const cloneUrl = buildCloneUrl(config.repoUrl, config.githubToken);
|
|
10352
|
+
const branch = baseBranch || config.mainBranch || "main";
|
|
10353
|
+
const cacheKey = repoCacheKey(config.repoUrl);
|
|
10354
|
+
const { fromCache } = await cloneRepo(
|
|
10355
|
+
session.containerName,
|
|
10356
|
+
cloneUrl,
|
|
10357
|
+
branch,
|
|
10358
|
+
cacheKey
|
|
10359
|
+
);
|
|
10360
|
+
this.emitter.addLog(
|
|
10361
|
+
session,
|
|
10362
|
+
"system",
|
|
10363
|
+
fromCache ? "Repository updated from cache." : "Repository cloned."
|
|
10364
|
+
);
|
|
10365
|
+
await setGitIdentity(
|
|
10366
|
+
session.containerName,
|
|
10367
|
+
config.gitName,
|
|
10368
|
+
config.gitEmail
|
|
10369
|
+
);
|
|
10370
|
+
}
|
|
10371
|
+
if (this.core.runnerToken) {
|
|
10372
|
+
try {
|
|
9426
10373
|
await installFrpcHarness({
|
|
9427
10374
|
containerName: session.containerName,
|
|
9428
10375
|
taskId: session.taskId,
|
|
9429
|
-
runnerToken: this.runnerToken
|
|
10376
|
+
runnerToken: this.core.runnerToken
|
|
9430
10377
|
});
|
|
9431
|
-
}
|
|
9432
|
-
const previewResult = await this.runClaudeInContainer(
|
|
9433
|
-
session,
|
|
9434
|
-
buildPrompt(prompts_default.preview, {
|
|
9435
|
-
ports: portsEnv,
|
|
9436
|
-
publicPreviewBlock: publicPreviewEnabled ? prompts_default.previewPublic.join("\n") : ""
|
|
9437
|
-
}),
|
|
9438
|
-
tools,
|
|
9439
|
-
session.claudeSessionId
|
|
9440
|
-
);
|
|
9441
|
-
session.claudeSessionId = previewResult.sessionId || session.claudeSessionId;
|
|
9442
|
-
if (!this.sessions.has(session.postId)) return;
|
|
9443
|
-
const portMatch = previewResult.text.match(/PREVIEW_PORT:\s*(\d+)/);
|
|
9444
|
-
const previewPort = portMatch ? portMatch[1] : "3000";
|
|
9445
|
-
const urlMatch = previewResult.text.match(/PREVIEW_URL:\s*(\S+)/);
|
|
9446
|
-
if (publicPreviewEnabled && urlMatch) {
|
|
9447
|
-
session.previewUrl = urlMatch[1];
|
|
10378
|
+
} catch (err) {
|
|
9448
10379
|
this.emitter.addLog(
|
|
9449
10380
|
session,
|
|
9450
10381
|
"system",
|
|
9451
|
-
`
|
|
9452
|
-
);
|
|
9453
|
-
} else {
|
|
9454
|
-
const previewUrl = await getContainerPort(
|
|
9455
|
-
session.containerName,
|
|
9456
|
-
previewPort
|
|
10382
|
+
`Port forwarding unavailable: ${err instanceof Error ? err.message : String(err)}`
|
|
9457
10383
|
);
|
|
9458
|
-
if (previewUrl) {
|
|
9459
|
-
session.previewUrl = previewUrl;
|
|
9460
|
-
this.emitter.addLog(
|
|
9461
|
-
session,
|
|
9462
|
-
"system",
|
|
9463
|
-
`Preview available at ${session.previewUrl}`
|
|
9464
|
-
);
|
|
9465
|
-
} else {
|
|
9466
|
-
this.emitter.addLog(
|
|
9467
|
-
session,
|
|
9468
|
-
"system",
|
|
9469
|
-
"Could not map preview port. Server may not be running."
|
|
9470
|
-
);
|
|
9471
|
-
}
|
|
9472
10384
|
}
|
|
9473
|
-
this.emitter.emitSessionUpdate(session);
|
|
9474
|
-
await new Promise((resolve) => {
|
|
9475
|
-
session.previewResolver = resolve;
|
|
9476
|
-
});
|
|
9477
|
-
session.previewResolver = null;
|
|
9478
|
-
if (!this.sessions.has(session.postId)) return;
|
|
9479
10385
|
}
|
|
9480
|
-
|
|
9481
|
-
session.
|
|
10386
|
+
await installRuntimes(this.emitter, session, runtimes);
|
|
10387
|
+
session.phase = "bare-active";
|
|
9482
10388
|
this.emitter.emitSessionUpdate(session);
|
|
9483
|
-
this.emitter.addLog(
|
|
9484
|
-
const reviewResult = await this.runClaudeInContainer(
|
|
10389
|
+
this.emitter.addLog(
|
|
9485
10390
|
session,
|
|
9486
|
-
|
|
9487
|
-
|
|
9488
|
-
session.claudeSessionId
|
|
10391
|
+
"system",
|
|
10392
|
+
"Container ready. Drive it from the terminal."
|
|
9489
10393
|
);
|
|
9490
|
-
session.claudeSessionId = reviewResult.sessionId || session.claudeSessionId;
|
|
9491
|
-
if (!this.sessions.has(session.postId)) return;
|
|
9492
|
-
session.reproSteps = reviewResult.text.trim();
|
|
9493
|
-
await this.extractAndComplete(session);
|
|
9494
10394
|
}
|
|
9495
|
-
|
|
10395
|
+
// ── file ops ──
|
|
10396
|
+
getBareContainer(postId) {
|
|
10397
|
+
const session = this.sessions.get(String(postId));
|
|
10398
|
+
if (!session) throw new Error("No active session for this card.");
|
|
10399
|
+
if (session.mode !== "bare") {
|
|
10400
|
+
throw new Error("File ops are only available in Bare mode.");
|
|
10401
|
+
}
|
|
10402
|
+
if (session.phase !== "bare-active" && session.phase !== "bare-review") {
|
|
10403
|
+
throw new Error("Container is not ready.");
|
|
10404
|
+
}
|
|
10405
|
+
return session.containerName;
|
|
10406
|
+
}
|
|
10407
|
+
/**
|
|
10408
|
+
* Resolve a repo-relative path to an absolute container path, rejecting
|
|
10409
|
+
* anything that escapes the repo root.
|
|
10410
|
+
*/
|
|
10411
|
+
resolveBarePath(rel) {
|
|
10412
|
+
const normalized = posixNormalize("/" + (rel || "").replace(/^\/+/, ""));
|
|
10413
|
+
const abs = `${BARE_REPO_ROOT}${normalized === "/" ? "" : normalized}`;
|
|
10414
|
+
if (abs !== BARE_REPO_ROOT && !abs.startsWith(BARE_REPO_ROOT + "/")) {
|
|
10415
|
+
throw new Error("Path escapes repo root.");
|
|
10416
|
+
}
|
|
10417
|
+
return abs;
|
|
10418
|
+
}
|
|
10419
|
+
async bareFsList({
|
|
10420
|
+
postId,
|
|
10421
|
+
path: path7
|
|
10422
|
+
}) {
|
|
10423
|
+
const container = this.getBareContainer(postId);
|
|
10424
|
+
const abs = this.resolveBarePath(path7);
|
|
10425
|
+
const relRoot = abs.slice(BARE_REPO_ROOT.length) || "/";
|
|
10426
|
+
const out = await execInContainer(
|
|
10427
|
+
container,
|
|
10428
|
+
`cd ${shEscape(abs)} && for f in .* *; do
|
|
10429
|
+
case "$f" in
|
|
10430
|
+
"."|".."|".git"|"*") continue ;;
|
|
10431
|
+
esac
|
|
10432
|
+
[ -e "$f" ] || continue
|
|
10433
|
+
if [ -d "$f" ]; then echo "d/$f"; else echo "f/$f"; fi
|
|
10434
|
+
done | LC_ALL=C sort`
|
|
10435
|
+
);
|
|
10436
|
+
const entries = [];
|
|
10437
|
+
for (const line of out.split("\n")) {
|
|
10438
|
+
if (!line) continue;
|
|
10439
|
+
const sep = line.indexOf("/");
|
|
10440
|
+
if (sep < 0) continue;
|
|
10441
|
+
const ty = line.slice(0, sep);
|
|
10442
|
+
const name = line.slice(sep + 1);
|
|
10443
|
+
const type = ty === "d" ? "dir" : "file";
|
|
10444
|
+
entries.push({
|
|
10445
|
+
name,
|
|
10446
|
+
path: relRoot === "/" ? `/${name}` : `${relRoot}/${name}`,
|
|
10447
|
+
type
|
|
10448
|
+
});
|
|
10449
|
+
}
|
|
10450
|
+
entries.sort((a, b) => {
|
|
10451
|
+
if (a.type !== b.type) return a.type === "dir" ? -1 : 1;
|
|
10452
|
+
return a.name.localeCompare(b.name);
|
|
10453
|
+
});
|
|
10454
|
+
return entries;
|
|
10455
|
+
}
|
|
10456
|
+
async bareFsSearch({
|
|
10457
|
+
postId
|
|
10458
|
+
}) {
|
|
10459
|
+
const container = this.getBareContainer(postId);
|
|
10460
|
+
const root = BARE_REPO_ROOT;
|
|
10461
|
+
const out = await execInContainer(
|
|
10462
|
+
container,
|
|
10463
|
+
`cd ${shEscape(root)} && (git ls-files --cached --others --exclude-standard 2>/dev/null || find . -type f -not -path './.git/*' | sed 's|^\\./||') | head -n 5000`
|
|
10464
|
+
);
|
|
10465
|
+
const files = [];
|
|
10466
|
+
for (const line of out.split("\n")) {
|
|
10467
|
+
const p = line.trim();
|
|
10468
|
+
if (p) files.push("/" + p);
|
|
10469
|
+
}
|
|
10470
|
+
return files;
|
|
10471
|
+
}
|
|
10472
|
+
async bareFsRead({
|
|
10473
|
+
postId,
|
|
10474
|
+
path: path7
|
|
10475
|
+
}) {
|
|
10476
|
+
const container = this.getBareContainer(postId);
|
|
10477
|
+
const abs = this.resolveBarePath(path7);
|
|
10478
|
+
const b64 = (await execInContainer(container, `base64 -w 0 ${shEscape(abs)}`)).trim();
|
|
10479
|
+
return { content: b64, encoding: "base64" };
|
|
10480
|
+
}
|
|
10481
|
+
async bareFsWrite({
|
|
10482
|
+
postId,
|
|
10483
|
+
path: path7,
|
|
10484
|
+
content,
|
|
10485
|
+
encoding
|
|
10486
|
+
}) {
|
|
10487
|
+
const container = this.getBareContainer(postId);
|
|
10488
|
+
const abs = this.resolveBarePath(path7);
|
|
10489
|
+
const b64 = encoding === "base64" ? content : Buffer.from(content, "utf-8").toString("base64");
|
|
10490
|
+
if (!/^[A-Za-z0-9+/=\n\r]*$/.test(b64)) {
|
|
10491
|
+
throw new Error("Invalid base64 payload.");
|
|
10492
|
+
}
|
|
10493
|
+
await execInContainer(
|
|
10494
|
+
container,
|
|
10495
|
+
`mkdir -p ${shEscape(posixDirname(abs))} && printf %s ${shEscape(b64)} | base64 -d > ${shEscape(abs)}`
|
|
10496
|
+
);
|
|
10497
|
+
const session = this.sessions.get(String(postId));
|
|
10498
|
+
if (session) delete session.buffers[path7];
|
|
10499
|
+
}
|
|
10500
|
+
bareEditorState({
|
|
10501
|
+
postId: rawPostId
|
|
10502
|
+
}) {
|
|
10503
|
+
const postId = String(rawPostId);
|
|
10504
|
+
const session = this.sessions.get(postId);
|
|
10505
|
+
if (!session || session.mode !== "bare") {
|
|
10506
|
+
return { openedFiles: [], buffers: {} };
|
|
10507
|
+
}
|
|
10508
|
+
return {
|
|
10509
|
+
openedFiles: session.openedFiles.slice(),
|
|
10510
|
+
buffers: { ...session.buffers }
|
|
10511
|
+
};
|
|
10512
|
+
}
|
|
10513
|
+
bareOpenFile({
|
|
10514
|
+
postId: rawPostId,
|
|
10515
|
+
path: path7
|
|
10516
|
+
}) {
|
|
10517
|
+
const session = this.sessions.get(String(rawPostId));
|
|
10518
|
+
if (!session || session.mode !== "bare") return;
|
|
10519
|
+
if (!session.openedFiles.includes(path7)) {
|
|
10520
|
+
session.openedFiles.push(path7);
|
|
10521
|
+
}
|
|
10522
|
+
}
|
|
10523
|
+
bareCloseFile({
|
|
10524
|
+
postId: rawPostId,
|
|
10525
|
+
path: path7
|
|
10526
|
+
}) {
|
|
10527
|
+
const session = this.sessions.get(String(rawPostId));
|
|
10528
|
+
if (!session || session.mode !== "bare") return;
|
|
10529
|
+
session.openedFiles = session.openedFiles.filter((p) => p !== path7);
|
|
10530
|
+
delete session.buffers[path7];
|
|
10531
|
+
}
|
|
10532
|
+
bareBufferSet({
|
|
10533
|
+
postId: rawPostId,
|
|
10534
|
+
path: path7,
|
|
10535
|
+
content
|
|
10536
|
+
}) {
|
|
10537
|
+
const session = this.sessions.get(String(rawPostId));
|
|
10538
|
+
if (!session || session.mode !== "bare") return;
|
|
10539
|
+
session.buffers[path7] = content;
|
|
10540
|
+
}
|
|
10541
|
+
async bareFsDelete({
|
|
10542
|
+
postId,
|
|
10543
|
+
path: path7
|
|
10544
|
+
}) {
|
|
10545
|
+
const container = this.getBareContainer(postId);
|
|
10546
|
+
const abs = this.resolveBarePath(path7);
|
|
10547
|
+
if (abs === BARE_REPO_ROOT) {
|
|
10548
|
+
throw new Error("Refusing to delete the repo root.");
|
|
10549
|
+
}
|
|
10550
|
+
await execInContainer(container, `rm -rf ${shEscape(abs)}`);
|
|
10551
|
+
}
|
|
10552
|
+
async bareFsMkdir({
|
|
10553
|
+
postId,
|
|
10554
|
+
path: path7
|
|
10555
|
+
}) {
|
|
10556
|
+
const container = this.getBareContainer(postId);
|
|
10557
|
+
const abs = this.resolveBarePath(path7);
|
|
10558
|
+
await execInContainer(container, `mkdir -p ${shEscape(abs)}`);
|
|
10559
|
+
}
|
|
10560
|
+
async bareReview({
|
|
10561
|
+
postId: rawPostId
|
|
10562
|
+
}) {
|
|
10563
|
+
const postId = String(rawPostId);
|
|
10564
|
+
const session = this.sessions.get(postId);
|
|
10565
|
+
if (!session) throw new Error("No active session for this card.");
|
|
10566
|
+
if (session.mode !== "bare") {
|
|
10567
|
+
throw new Error("bareReview is only valid in Bare mode.");
|
|
10568
|
+
}
|
|
10569
|
+
if (session.phase !== "bare-active" && session.phase !== "bare-review") {
|
|
10570
|
+
throw new Error("Container is not ready.");
|
|
10571
|
+
}
|
|
10572
|
+
const config = this.core.readConfig();
|
|
10573
|
+
if (this.core.isOneDrive(config)) {
|
|
10574
|
+
session.diff = "";
|
|
10575
|
+
session.changedFiles = [];
|
|
10576
|
+
session.commitMessage = "";
|
|
10577
|
+
session.phase = "bare-review";
|
|
10578
|
+
this.emitter.emitSessionUpdate(session);
|
|
10579
|
+
return { diff: "", changedFiles: [], commitMessage: "" };
|
|
10580
|
+
}
|
|
9496
10581
|
this.emitter.addLog(session, "system", "Extracting changes...");
|
|
9497
10582
|
const { diff, changedFiles } = await extractDiff(session.containerName);
|
|
9498
10583
|
session.diff = diff;
|
|
@@ -9504,68 +10589,424 @@ var BoardAgentEngine = class _BoardAgentEngine {
|
|
|
9504
10589
|
`${changedFiles.length} file${changedFiles.length !== 1 ? "s" : ""} changed.`
|
|
9505
10590
|
);
|
|
9506
10591
|
this.emitter.addLog(session, "system", "Generating commit message...");
|
|
9507
|
-
|
|
9508
|
-
session.
|
|
9509
|
-
|
|
9510
|
-
|
|
10592
|
+
try {
|
|
10593
|
+
session.commitMessage = await generateCommitMessage(
|
|
10594
|
+
session.containerName,
|
|
10595
|
+
diff
|
|
10596
|
+
);
|
|
10597
|
+
} catch {
|
|
10598
|
+
session.commitMessage = "WIP: changes from bare session";
|
|
10599
|
+
}
|
|
9511
10600
|
} else {
|
|
10601
|
+
session.commitMessage = "";
|
|
9512
10602
|
this.emitter.addLog(session, "system", "No files changed.");
|
|
9513
10603
|
}
|
|
9514
|
-
session.phase = "
|
|
10604
|
+
session.phase = "bare-review";
|
|
9515
10605
|
this.emitter.emitSessionUpdate(session);
|
|
10606
|
+
return {
|
|
10607
|
+
diff: session.diff,
|
|
10608
|
+
changedFiles: session.changedFiles,
|
|
10609
|
+
commitMessage: session.commitMessage
|
|
10610
|
+
};
|
|
9516
10611
|
}
|
|
9517
|
-
|
|
10612
|
+
async bareGitStatus({
|
|
10613
|
+
postId: rawPostId
|
|
10614
|
+
}) {
|
|
9518
10615
|
const postId = String(rawPostId);
|
|
9519
10616
|
const session = this.sessions.get(postId);
|
|
9520
10617
|
if (!session) throw new Error("No active session for this card.");
|
|
9521
|
-
if (session.
|
|
9522
|
-
throw new Error("
|
|
9523
|
-
|
|
9524
|
-
|
|
9525
|
-
|
|
10618
|
+
if (session.mode !== "bare") {
|
|
10619
|
+
throw new Error("bareGitStatus is only valid in Bare mode.");
|
|
10620
|
+
}
|
|
10621
|
+
if (session.phase !== "bare-active" && session.phase !== "bare-review") {
|
|
10622
|
+
throw new Error("Container is not ready.");
|
|
10623
|
+
}
|
|
10624
|
+
if (this.core.isOneDrive(this.core.readConfig())) {
|
|
10625
|
+
return { diff: "", changedFiles: [] };
|
|
10626
|
+
}
|
|
10627
|
+
return await extractDiff(session.containerName);
|
|
9526
10628
|
}
|
|
9527
|
-
|
|
9528
|
-
|
|
9529
|
-
|
|
9530
|
-
|
|
10629
|
+
async bareListTerminals({
|
|
10630
|
+
postId: rawPostId
|
|
10631
|
+
}) {
|
|
10632
|
+
const postId = String(rawPostId);
|
|
10633
|
+
const session = this.sessions.get(postId);
|
|
10634
|
+
if (!session) return [];
|
|
10635
|
+
if (session.mode !== "bare") return [];
|
|
10636
|
+
if (session.phase !== "bare-active" && session.phase !== "bare-review") {
|
|
10637
|
+
return [];
|
|
10638
|
+
}
|
|
10639
|
+
const sanitizedPostId = postId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
10640
|
+
const prefix = `terminal-${sanitizedPostId}-`;
|
|
10641
|
+
try {
|
|
10642
|
+
const stdout = await execInContainer(
|
|
10643
|
+
session.containerName,
|
|
10644
|
+
`tmux list-panes -a -F '#{session_name}|#{pane_current_command}' 2>/dev/null || true`
|
|
10645
|
+
);
|
|
10646
|
+
const byId = /* @__PURE__ */ new Map();
|
|
10647
|
+
for (const raw of stdout.split("\n")) {
|
|
10648
|
+
const line = raw.trim();
|
|
10649
|
+
if (!line.startsWith(prefix)) continue;
|
|
10650
|
+
const rest = line.slice(prefix.length);
|
|
10651
|
+
const sep = rest.indexOf("|");
|
|
10652
|
+
if (sep === -1) continue;
|
|
10653
|
+
const id = rest.slice(0, sep);
|
|
10654
|
+
const cmd = rest.slice(sep + 1).trim() || "bash";
|
|
10655
|
+
if (!byId.has(id)) byId.set(id, cmd);
|
|
10656
|
+
}
|
|
10657
|
+
return Array.from(byId, ([id, name]) => ({ id, name })).sort(
|
|
10658
|
+
(a, b) => (parseInt(a.id, 10) || 0) - (parseInt(b.id, 10) || 0)
|
|
10659
|
+
);
|
|
10660
|
+
} catch {
|
|
10661
|
+
return [];
|
|
9531
10662
|
}
|
|
9532
|
-
return result;
|
|
9533
10663
|
}
|
|
9534
|
-
|
|
9535
|
-
|
|
10664
|
+
// ── port forwarding (factiii-expose / factiii-unexpose) ──
|
|
10665
|
+
async bareExpose({
|
|
10666
|
+
postId,
|
|
10667
|
+
port,
|
|
10668
|
+
name
|
|
9536
10669
|
}) {
|
|
10670
|
+
const container = this.getBareContainer(postId);
|
|
9537
10671
|
const session = this.sessions.get(String(postId));
|
|
9538
|
-
|
|
9539
|
-
|
|
10672
|
+
const safeName = name.trim();
|
|
10673
|
+
if (!/^[a-zA-Z0-9-]+$/.test(safeName)) {
|
|
10674
|
+
throw new Error("Name must be letters, numbers, or dashes.");
|
|
10675
|
+
}
|
|
10676
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
10677
|
+
throw new Error("Port must be between 1 and 65535.");
|
|
10678
|
+
}
|
|
10679
|
+
const out = (await execInContainer(
|
|
10680
|
+
container,
|
|
10681
|
+
`factiii-expose ${port} ${shEscape(safeName)} 2>&1`
|
|
10682
|
+
)).trim();
|
|
10683
|
+
const url2 = out.split("\n").filter(Boolean).pop() || "";
|
|
10684
|
+
if (!/^https?:\/\//.test(url2)) {
|
|
10685
|
+
throw new Error(out || "Failed to expose port.");
|
|
10686
|
+
}
|
|
10687
|
+
const entry = { name: safeName, port, url: url2 };
|
|
10688
|
+
session.exposed = session.exposed.filter((e) => e.name !== safeName);
|
|
10689
|
+
session.exposed.push(entry);
|
|
10690
|
+
return entry;
|
|
10691
|
+
}
|
|
10692
|
+
async bareUnexpose({
|
|
10693
|
+
postId,
|
|
10694
|
+
name
|
|
10695
|
+
}) {
|
|
10696
|
+
const container = this.getBareContainer(postId);
|
|
10697
|
+
const session = this.sessions.get(String(postId));
|
|
10698
|
+
const safeName = name.trim();
|
|
10699
|
+
if (!/^[a-zA-Z0-9-]+$/.test(safeName)) {
|
|
10700
|
+
throw new Error("Invalid tunnel name.");
|
|
10701
|
+
}
|
|
10702
|
+
await execInContainer(
|
|
10703
|
+
container,
|
|
10704
|
+
`factiii-unexpose ${shEscape(safeName)} 2>&1 || true`
|
|
10705
|
+
);
|
|
10706
|
+
session.exposed = session.exposed.filter((e) => e.name !== safeName);
|
|
9540
10707
|
}
|
|
9541
|
-
|
|
10708
|
+
bareListExposed({
|
|
10709
|
+
postId
|
|
10710
|
+
}) {
|
|
9542
10711
|
const session = this.sessions.get(String(postId));
|
|
9543
|
-
if (!session) return;
|
|
9544
|
-
|
|
9545
|
-
|
|
9546
|
-
|
|
10712
|
+
if (!session || session.mode !== "bare") return [];
|
|
10713
|
+
return session.exposed.slice();
|
|
10714
|
+
}
|
|
10715
|
+
};
|
|
10716
|
+
|
|
10717
|
+
// ../../shared/all/helpers/board-agent-core/engine.ts
|
|
10718
|
+
function sanitizeTaskId(taskId) {
|
|
10719
|
+
const sanitized = String(taskId).replace(/[^a-zA-Z0-9_.-]/g, "");
|
|
10720
|
+
if (!sanitized) throw new Error("Invalid taskId");
|
|
10721
|
+
return sanitized;
|
|
10722
|
+
}
|
|
10723
|
+
var BoardAgentEngine = class {
|
|
10724
|
+
constructor(options) {
|
|
10725
|
+
this.core = new ContainerCore(options);
|
|
10726
|
+
this.emitter = options.emitter;
|
|
10727
|
+
this.claude = new ClaudeModeEngine(this.core, this.emitter);
|
|
10728
|
+
this.terminal = new TerminalModeEngine(this.core, this.emitter);
|
|
10729
|
+
}
|
|
10730
|
+
/** The mode engine that currently owns this postId, if any. */
|
|
10731
|
+
owner(postId) {
|
|
10732
|
+
if (this.claude.has(postId)) return this.claude;
|
|
10733
|
+
if (this.terminal.has(postId)) return this.terminal;
|
|
10734
|
+
return null;
|
|
10735
|
+
}
|
|
10736
|
+
// ── Config / setup (→ core) ──
|
|
10737
|
+
getConfig() {
|
|
10738
|
+
return this.core.getConfig();
|
|
10739
|
+
}
|
|
10740
|
+
setConfig(partial) {
|
|
10741
|
+
return this.core.setConfig(partial);
|
|
10742
|
+
}
|
|
10743
|
+
clearToken(key) {
|
|
10744
|
+
return this.core.clearToken(key);
|
|
9547
10745
|
}
|
|
9548
10746
|
setupStatus() {
|
|
9549
|
-
|
|
9550
|
-
if (this.setupPromise) return { phase: "running" };
|
|
9551
|
-
return { phase: "idle" };
|
|
10747
|
+
return this.core.setupStatus();
|
|
9552
10748
|
}
|
|
9553
|
-
|
|
9554
|
-
|
|
9555
|
-
return { ready: true };
|
|
10749
|
+
ensureSetup() {
|
|
10750
|
+
return this.core.ensureSetup();
|
|
9556
10751
|
}
|
|
9557
|
-
|
|
9558
|
-
return this.
|
|
10752
|
+
cardRemoteBranches() {
|
|
10753
|
+
return this.core.cardRemoteBranches();
|
|
9559
10754
|
}
|
|
9560
|
-
|
|
9561
|
-
|
|
9562
|
-
|
|
9563
|
-
|
|
9564
|
-
|
|
9565
|
-
|
|
9566
|
-
|
|
10755
|
+
// Start the host-run device-code flow and return the code to show the user.
|
|
10756
|
+
// The host polls Microsoft and pushes one `onedrive-auth` event when done.
|
|
10757
|
+
async onedriveStartDeviceCode(p) {
|
|
10758
|
+
const start = await startDeviceCode(p.clientId);
|
|
10759
|
+
console.log(
|
|
10760
|
+
`[onedrive] device-code started: userCode=${start.userCode} expiresIn=${start.expiresIn}s interval=${start.interval}s`
|
|
10761
|
+
);
|
|
10762
|
+
void this.pollDeviceCodeUntilDone(
|
|
10763
|
+
p.clientId,
|
|
10764
|
+
start.deviceCode,
|
|
10765
|
+
start.interval,
|
|
10766
|
+
start.expiresIn
|
|
10767
|
+
);
|
|
10768
|
+
return {
|
|
10769
|
+
userCode: start.userCode,
|
|
10770
|
+
verificationUri: start.verificationUri,
|
|
10771
|
+
expiresIn: start.expiresIn
|
|
10772
|
+
};
|
|
10773
|
+
}
|
|
10774
|
+
async pollDeviceCodeUntilDone(clientId, deviceCode, interval, expiresIn) {
|
|
10775
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
10776
|
+
const deadline = Date.now() + expiresIn * 1e3;
|
|
10777
|
+
let wait = Math.max(interval, 5);
|
|
10778
|
+
while (Date.now() < deadline) {
|
|
10779
|
+
await sleep(wait * 1e3);
|
|
10780
|
+
let res;
|
|
10781
|
+
try {
|
|
10782
|
+
res = await pollDeviceCode(clientId, deviceCode);
|
|
10783
|
+
} catch (err) {
|
|
10784
|
+
console.log(`[onedrive] poll threw (will retry): ${String(err)}`);
|
|
10785
|
+
continue;
|
|
10786
|
+
}
|
|
10787
|
+
console.log(`[onedrive] poll status=${res.status}`);
|
|
10788
|
+
if (res.status === "pending") continue;
|
|
10789
|
+
if (res.status === "slow_down") {
|
|
10790
|
+
wait += 5;
|
|
10791
|
+
continue;
|
|
10792
|
+
}
|
|
10793
|
+
if (res.status === "complete") {
|
|
10794
|
+
try {
|
|
10795
|
+
this.core.saveOneDriveConnection({
|
|
10796
|
+
refreshToken: res.refreshToken,
|
|
10797
|
+
clientId,
|
|
10798
|
+
...res.appFolder
|
|
10799
|
+
});
|
|
10800
|
+
console.log("[onedrive] connection saved locally");
|
|
10801
|
+
} catch (err) {
|
|
10802
|
+
console.log(
|
|
10803
|
+
`[onedrive] saveOneDriveConnection failed: ${String(err)}`
|
|
10804
|
+
);
|
|
10805
|
+
this.emitter.emitOneDriveAuth({
|
|
10806
|
+
status: "error",
|
|
10807
|
+
message: "Failed to store the OneDrive connection on the host."
|
|
10808
|
+
});
|
|
10809
|
+
return;
|
|
10810
|
+
}
|
|
10811
|
+
this.emitter.emitOneDriveAuth({ status: "complete" });
|
|
10812
|
+
return;
|
|
10813
|
+
}
|
|
10814
|
+
this.emitter.emitOneDriveAuth({
|
|
10815
|
+
status: res.status,
|
|
10816
|
+
message: res.status === "error" ? res.message : void 0
|
|
10817
|
+
});
|
|
10818
|
+
return;
|
|
9567
10819
|
}
|
|
9568
|
-
|
|
10820
|
+
console.log("[onedrive] device code reached deadline without completion");
|
|
10821
|
+
this.emitter.emitOneDriveAuth({ status: "expired" });
|
|
10822
|
+
}
|
|
10823
|
+
onedriveDisconnect() {
|
|
10824
|
+
this.core.clearOneDriveConnection();
|
|
10825
|
+
return { success: true };
|
|
10826
|
+
}
|
|
10827
|
+
// Unmount the workspace (flushing writes) and persist the rotated refresh
|
|
10828
|
+
// token. No-op for non-OneDrive spaces or when nothing is mounted.
|
|
10829
|
+
async onedriveTeardown(p) {
|
|
10830
|
+
const postId = String(p.postId);
|
|
10831
|
+
const session = this.owner(postId)?.get(postId);
|
|
10832
|
+
if (!session) return { success: true };
|
|
10833
|
+
stopOneDriveSyncWatcher(session.containerName);
|
|
10834
|
+
if (!this.core.isOneDrive(this.core.readConfig())) {
|
|
10835
|
+
return { success: true };
|
|
10836
|
+
}
|
|
10837
|
+
try {
|
|
10838
|
+
await unmountWorkspace(session.containerName);
|
|
10839
|
+
} catch {
|
|
10840
|
+
}
|
|
10841
|
+
const refreshToken = await readbackRefreshToken(
|
|
10842
|
+
session.containerName
|
|
10843
|
+
).catch(() => null);
|
|
10844
|
+
if (refreshToken) this.core.updateOneDriveRefreshToken(refreshToken);
|
|
10845
|
+
return { success: true };
|
|
10846
|
+
}
|
|
10847
|
+
// Pending upload count to seed the UI (0 = synced); live updates come via the
|
|
10848
|
+
// onedrive-sync push. Synced for non-OneDrive spaces or idle sessions.
|
|
10849
|
+
onedriveStatus(p) {
|
|
10850
|
+
const postId = String(p.postId);
|
|
10851
|
+
const session = this.owner(postId)?.get(postId);
|
|
10852
|
+
if (!session || !this.core.isOneDrive(this.core.readConfig())) {
|
|
10853
|
+
return { syncing: false, pending: 0, files: [] };
|
|
10854
|
+
}
|
|
10855
|
+
const pending = onedrivePendingUploads(session.containerName);
|
|
10856
|
+
const files = onedriveUploadingFiles(session.containerName);
|
|
10857
|
+
return { syncing: pending > 0, pending, files };
|
|
10858
|
+
}
|
|
10859
|
+
// ── Ask AI + claude-only controls (→ claude) ──
|
|
10860
|
+
ask(params, sendLog) {
|
|
10861
|
+
return this.claude.ask(params, sendLog);
|
|
10862
|
+
}
|
|
10863
|
+
resetBoardSession() {
|
|
10864
|
+
this.claude.resetBoardSession();
|
|
10865
|
+
}
|
|
10866
|
+
boardSession() {
|
|
10867
|
+
return this.claude.boardSession();
|
|
10868
|
+
}
|
|
10869
|
+
cardStartPreview(payload) {
|
|
10870
|
+
return this.claude.cardStartPreview(payload);
|
|
10871
|
+
}
|
|
10872
|
+
cardAnswerQuestion(payload) {
|
|
10873
|
+
this.claude.cardAnswerQuestion(payload);
|
|
10874
|
+
}
|
|
10875
|
+
cardFeedback(payload) {
|
|
10876
|
+
this.claude.cardFeedback(payload);
|
|
10877
|
+
}
|
|
10878
|
+
// ── Terminal ops (→ terminal) ──
|
|
10879
|
+
bareFsList(p) {
|
|
10880
|
+
return this.terminal.bareFsList(p);
|
|
10881
|
+
}
|
|
10882
|
+
bareFsSearch(p) {
|
|
10883
|
+
return this.terminal.bareFsSearch(p);
|
|
10884
|
+
}
|
|
10885
|
+
bareFsRead(p) {
|
|
10886
|
+
return this.terminal.bareFsRead(p);
|
|
10887
|
+
}
|
|
10888
|
+
bareFsWrite(p) {
|
|
10889
|
+
return this.terminal.bareFsWrite(p);
|
|
10890
|
+
}
|
|
10891
|
+
bareFsDelete(p) {
|
|
10892
|
+
return this.terminal.bareFsDelete(p);
|
|
10893
|
+
}
|
|
10894
|
+
bareFsMkdir(p) {
|
|
10895
|
+
return this.terminal.bareFsMkdir(p);
|
|
10896
|
+
}
|
|
10897
|
+
bareReview(p) {
|
|
10898
|
+
return this.terminal.bareReview(p);
|
|
10899
|
+
}
|
|
10900
|
+
bareGitStatus(p) {
|
|
10901
|
+
return this.terminal.bareGitStatus(p);
|
|
10902
|
+
}
|
|
10903
|
+
bareListTerminals(p) {
|
|
10904
|
+
return this.terminal.bareListTerminals(p);
|
|
10905
|
+
}
|
|
10906
|
+
bareEditorState(p) {
|
|
10907
|
+
return this.terminal.bareEditorState(p);
|
|
10908
|
+
}
|
|
10909
|
+
bareOpenFile(p) {
|
|
10910
|
+
this.terminal.bareOpenFile(p);
|
|
10911
|
+
}
|
|
10912
|
+
bareCloseFile(p) {
|
|
10913
|
+
this.terminal.bareCloseFile(p);
|
|
10914
|
+
}
|
|
10915
|
+
bareBufferSet(p) {
|
|
10916
|
+
this.terminal.bareBufferSet(p);
|
|
10917
|
+
}
|
|
10918
|
+
bareExpose(p) {
|
|
10919
|
+
return this.terminal.bareExpose(p);
|
|
10920
|
+
}
|
|
10921
|
+
bareUnexpose(p) {
|
|
10922
|
+
return this.terminal.bareUnexpose(p);
|
|
10923
|
+
}
|
|
10924
|
+
bareListExposed(p) {
|
|
10925
|
+
return this.terminal.bareListExposed(p);
|
|
10926
|
+
}
|
|
10927
|
+
// ── Cross-mode coordinators ──
|
|
10928
|
+
async cardExecute(payload) {
|
|
10929
|
+
const {
|
|
10930
|
+
postId: rawPostId,
|
|
10931
|
+
prompt: prompt2,
|
|
10932
|
+
baseBranch,
|
|
10933
|
+
targetBranch,
|
|
10934
|
+
taskTitle,
|
|
10935
|
+
effort,
|
|
10936
|
+
mode,
|
|
10937
|
+
runtimes
|
|
10938
|
+
} = payload;
|
|
10939
|
+
const postId = String(rawPostId);
|
|
10940
|
+
if (this.claude.isRunning(postId) || this.terminal.isRunning(postId)) {
|
|
10941
|
+
throw new Error("A session is already running for this card.");
|
|
10942
|
+
}
|
|
10943
|
+
const stale = this.owner(postId);
|
|
10944
|
+
if (stale) {
|
|
10945
|
+
const s = stale.get(postId);
|
|
10946
|
+
await killContainer(s.containerName);
|
|
10947
|
+
stale.remove(postId);
|
|
10948
|
+
}
|
|
10949
|
+
const taskId = sanitizeTaskId(`${postId}-${Date.now()}`);
|
|
10950
|
+
const base = {
|
|
10951
|
+
taskId,
|
|
10952
|
+
postId,
|
|
10953
|
+
phase: "starting",
|
|
10954
|
+
mode,
|
|
10955
|
+
step: 1,
|
|
10956
|
+
logs: [],
|
|
10957
|
+
diff: "",
|
|
10958
|
+
changedFiles: [],
|
|
10959
|
+
commitMessage: "",
|
|
10960
|
+
previewUrl: "",
|
|
10961
|
+
previewRunning: false,
|
|
10962
|
+
reproSteps: "",
|
|
10963
|
+
pendingQuestion: "",
|
|
10964
|
+
error: "",
|
|
10965
|
+
startedAt: Date.now(),
|
|
10966
|
+
taskTitle: taskTitle || "",
|
|
10967
|
+
targetBranch: targetBranch || "",
|
|
10968
|
+
effort: effort || "high",
|
|
10969
|
+
todos: [],
|
|
10970
|
+
containerName: `factiii-${this.core.instancePrefix}-card-${taskId}`
|
|
10971
|
+
};
|
|
10972
|
+
if (mode === "bare") {
|
|
10973
|
+
this.terminal.start(base, {
|
|
10974
|
+
baseBranch: baseBranch ?? "",
|
|
10975
|
+
runtimes: runtimes ?? []
|
|
10976
|
+
});
|
|
10977
|
+
} else {
|
|
10978
|
+
this.claude.start(base, {
|
|
10979
|
+
prompt: prompt2,
|
|
10980
|
+
baseBranch: baseBranch ?? "",
|
|
10981
|
+
runtimes: runtimes ?? []
|
|
10982
|
+
});
|
|
10983
|
+
}
|
|
10984
|
+
return { taskId, postId };
|
|
10985
|
+
}
|
|
10986
|
+
/**
|
|
10987
|
+
* Look up the active container for a card. Used by the terminal bridge to
|
|
10988
|
+
* map postId to the docker container it should attach a shell to.
|
|
10989
|
+
*/
|
|
10990
|
+
getCardContainerName(postId) {
|
|
10991
|
+
const session = this.claude.get(String(postId)) ?? this.terminal.get(String(postId));
|
|
10992
|
+
if (!session) return null;
|
|
10993
|
+
if (session.phase === "error" || session.phase === "done") return null;
|
|
10994
|
+
return session.containerName;
|
|
10995
|
+
}
|
|
10996
|
+
cardSessions() {
|
|
10997
|
+
return { ...this.terminal.listSummaries(), ...this.claude.listSummaries() };
|
|
10998
|
+
}
|
|
10999
|
+
cardSession({ postId }) {
|
|
11000
|
+
return this.claude.sessionWithLogs(String(postId)) ?? this.terminal.sessionWithLogs(String(postId));
|
|
11001
|
+
}
|
|
11002
|
+
async cardDismiss({ postId }) {
|
|
11003
|
+
const owner = this.owner(String(postId));
|
|
11004
|
+
if (!owner) return;
|
|
11005
|
+
const session = owner.get(String(postId));
|
|
11006
|
+
await this.onedriveTeardown({ postId: String(postId) });
|
|
11007
|
+
await killContainer(session.containerName);
|
|
11008
|
+
owner.remove(String(postId));
|
|
11009
|
+
this.emitter.emitSessionRemoved(String(postId));
|
|
9569
11010
|
}
|
|
9570
11011
|
async cardFinalize({
|
|
9571
11012
|
postId: rawPostId,
|
|
@@ -9574,128 +11015,79 @@ var BoardAgentEngine = class _BoardAgentEngine {
|
|
|
9574
11015
|
commitMessage
|
|
9575
11016
|
}) {
|
|
9576
11017
|
const postId = String(rawPostId);
|
|
9577
|
-
const
|
|
9578
|
-
if (!
|
|
11018
|
+
const owner = this.owner(postId);
|
|
11019
|
+
if (!owner) throw new Error("No active session for this card.");
|
|
11020
|
+
const session = owner.get(postId);
|
|
11021
|
+
const config = this.core.readConfig();
|
|
11022
|
+
if (this.core.isOneDrive(config)) {
|
|
11023
|
+
if (apply) {
|
|
11024
|
+
session.phase = "done";
|
|
11025
|
+
this.emitter.addLog(session, "system", "Changes are live on OneDrive.");
|
|
11026
|
+
this.emitter.emitSessionUpdate(session);
|
|
11027
|
+
return { applied: true, branchName: "" };
|
|
11028
|
+
}
|
|
11029
|
+
await this.onedriveTeardown({ postId });
|
|
11030
|
+
await killContainer(session.containerName);
|
|
11031
|
+
owner.remove(postId);
|
|
11032
|
+
this.emitter.emitSessionRemoved(postId);
|
|
11033
|
+
return { applied: false, branchName: "" };
|
|
11034
|
+
}
|
|
9579
11035
|
if (apply && branchName) {
|
|
9580
|
-
|
|
9581
|
-
|
|
9582
|
-
|
|
9583
|
-
|
|
9584
|
-
|
|
9585
|
-
|
|
9586
|
-
|
|
9587
|
-
|
|
11036
|
+
let pushResult;
|
|
11037
|
+
try {
|
|
11038
|
+
pushResult = await commitAndPush(
|
|
11039
|
+
session.containerName,
|
|
11040
|
+
branchName,
|
|
11041
|
+
commitMessage || `Board AI: ${branchName}`,
|
|
11042
|
+
{
|
|
11043
|
+
gitName: config.gitName,
|
|
11044
|
+
gitEmail: config.gitEmail
|
|
11045
|
+
}
|
|
11046
|
+
);
|
|
11047
|
+
} catch (err) {
|
|
11048
|
+
if (err instanceof NoChangesError) {
|
|
11049
|
+
this.emitter.addLog(
|
|
11050
|
+
session,
|
|
11051
|
+
"system",
|
|
11052
|
+
"No changes to push - container kept alive for inspection."
|
|
11053
|
+
);
|
|
11054
|
+
} else {
|
|
11055
|
+
this.emitter.addLog(
|
|
11056
|
+
session,
|
|
11057
|
+
"error",
|
|
11058
|
+
`Push failed: ${err instanceof Error ? err.message : String(err)}`
|
|
11059
|
+
);
|
|
9588
11060
|
}
|
|
9589
|
-
|
|
9590
|
-
|
|
9591
|
-
|
|
9592
|
-
if (apply) {
|
|
11061
|
+
this.emitter.emitSessionUpdate(session);
|
|
11062
|
+
throw err;
|
|
11063
|
+
}
|
|
9593
11064
|
session.phase = "done";
|
|
9594
11065
|
this.emitter.addLog(
|
|
9595
11066
|
session,
|
|
9596
11067
|
"system",
|
|
9597
|
-
`
|
|
11068
|
+
`Pushed ${pushResult.pushedSha.slice(0, 8)} to origin/${pushResult.branch}`
|
|
9598
11069
|
);
|
|
9599
11070
|
this.emitter.emitSessionUpdate(session);
|
|
9600
|
-
|
|
9601
|
-
if (this.sessions.get(postId)?.phase === "done") {
|
|
9602
|
-
this.sessions.delete(postId);
|
|
9603
|
-
this.emitter.emitSessionRemoved(postId);
|
|
9604
|
-
}
|
|
9605
|
-
}, 1e4);
|
|
9606
|
-
} else {
|
|
9607
|
-
this.sessions.delete(postId);
|
|
9608
|
-
this.emitter.emitSessionRemoved(postId);
|
|
11071
|
+
return { applied: true, branchName: pushResult.branch };
|
|
9609
11072
|
}
|
|
11073
|
+
await killContainer(session.containerName);
|
|
11074
|
+
owner.remove(postId);
|
|
11075
|
+
this.emitter.emitSessionRemoved(postId);
|
|
9610
11076
|
return { applied: apply, branchName: branchName || "" };
|
|
9611
11077
|
}
|
|
9612
|
-
cardAnswerQuestion({
|
|
9613
|
-
postId: rawPostId,
|
|
9614
|
-
answer
|
|
9615
|
-
}) {
|
|
9616
|
-
const postId = String(rawPostId);
|
|
9617
|
-
const session = this.sessions.get(postId);
|
|
9618
|
-
if (!session || session.phase !== "awaiting-input" || !session.answerResolver)
|
|
9619
|
-
return;
|
|
9620
|
-
session.answerResolver(answer);
|
|
9621
|
-
}
|
|
9622
|
-
cardFeedback({
|
|
9623
|
-
postId: rawPostId,
|
|
9624
|
-
feedback
|
|
9625
|
-
}) {
|
|
9626
|
-
const postId = String(rawPostId);
|
|
9627
|
-
const session = this.sessions.get(postId);
|
|
9628
|
-
if (!session || session.phase !== "complete") return;
|
|
9629
|
-
if (!session.claudeSessionId) return;
|
|
9630
|
-
session.diff = "";
|
|
9631
|
-
session.changedFiles = [];
|
|
9632
|
-
session.commitMessage = "";
|
|
9633
|
-
session.reproSteps = "";
|
|
9634
|
-
const prompt2 = `The user reviewed your changes and has the following feedback:
|
|
9635
|
-
|
|
9636
|
-
${feedback}
|
|
9637
|
-
|
|
9638
|
-
${buildPrompt(prompts_default.implement)}`;
|
|
9639
|
-
this.runImplementation(session, prompt2, "\u2500\u2500 Follow-up").catch((err) => {
|
|
9640
|
-
if (!this.sessions.has(postId)) return;
|
|
9641
|
-
session.phase = "error";
|
|
9642
|
-
session.error = err instanceof Error ? err.message : String(err);
|
|
9643
|
-
this.emitter.addLog(session, "error", session.error);
|
|
9644
|
-
this.emitter.emitSessionUpdate(session);
|
|
9645
|
-
});
|
|
9646
|
-
}
|
|
9647
11078
|
async cardKill({ postId: rawPostId }) {
|
|
9648
11079
|
const postId = String(rawPostId);
|
|
9649
|
-
|
|
9650
|
-
if (
|
|
9651
|
-
|
|
9652
|
-
|
|
9653
|
-
|
|
9654
|
-
break;
|
|
9655
|
-
}
|
|
9656
|
-
}
|
|
9657
|
-
}
|
|
9658
|
-
if (!session) return;
|
|
9659
|
-
if (session.answerResolver) {
|
|
9660
|
-
session.answerResolver("");
|
|
9661
|
-
session.answerResolver = null;
|
|
9662
|
-
}
|
|
9663
|
-
if (session.previewResolver) {
|
|
9664
|
-
session.previewResolver();
|
|
9665
|
-
session.previewResolver = null;
|
|
9666
|
-
}
|
|
9667
|
-
const proc = this.cardProcesses.get(session.postId);
|
|
9668
|
-
if (proc) {
|
|
9669
|
-
try {
|
|
9670
|
-
proc.kill("SIGTERM");
|
|
9671
|
-
} catch {
|
|
9672
|
-
}
|
|
9673
|
-
this.cardProcesses.delete(session.postId);
|
|
9674
|
-
}
|
|
9675
|
-
await killContainer(session.containerName);
|
|
9676
|
-
const removedPostId = session.postId;
|
|
9677
|
-
this.sessions.delete(removedPostId);
|
|
9678
|
-
this.emitter.emitSessionRemoved(removedPostId);
|
|
11080
|
+
await this.onedriveTeardown({ postId });
|
|
11081
|
+
if (this.claude.has(postId)) return this.claude.kill(postId);
|
|
11082
|
+
if (this.terminal.has(postId)) return this.terminal.kill(postId);
|
|
11083
|
+
await this.claude.kill(postId);
|
|
11084
|
+
await this.terminal.kill(postId);
|
|
9679
11085
|
}
|
|
9680
11086
|
// ── Lifecycle ──
|
|
9681
11087
|
destroy() {
|
|
9682
|
-
this.
|
|
9683
|
-
|
|
9684
|
-
|
|
9685
|
-
proc.kill("SIGTERM");
|
|
9686
|
-
} catch {
|
|
9687
|
-
}
|
|
9688
|
-
}
|
|
9689
|
-
this.cardProcesses.clear();
|
|
9690
|
-
for (const [, session] of this.sessions) {
|
|
9691
|
-
void killContainer(session.containerName);
|
|
9692
|
-
}
|
|
9693
|
-
this.sessions.clear();
|
|
9694
|
-
if (this.containerName) {
|
|
9695
|
-
void killContainer(this.containerName);
|
|
9696
|
-
this.containerName = null;
|
|
9697
|
-
this.setupPromise = null;
|
|
9698
|
-
}
|
|
11088
|
+
this.claude.destroy();
|
|
11089
|
+
this.terminal.destroy();
|
|
11090
|
+
this.core.destroy();
|
|
9699
11091
|
}
|
|
9700
11092
|
/**
|
|
9701
11093
|
* Route a single action/payload pair to the right engine method.
|
|
@@ -9725,13 +11117,16 @@ ${buildPrompt(prompts_default.implement)}`;
|
|
|
9725
11117
|
case "resetBoardSession":
|
|
9726
11118
|
this.resetBoardSession();
|
|
9727
11119
|
return void 0;
|
|
11120
|
+
case "boardSession":
|
|
11121
|
+
return this.boardSession();
|
|
9728
11122
|
case "cardExecute":
|
|
9729
11123
|
return await this.cardExecute(
|
|
9730
11124
|
payload
|
|
9731
11125
|
);
|
|
9732
|
-
case "
|
|
9733
|
-
this.
|
|
9734
|
-
|
|
11126
|
+
case "cardStartPreview":
|
|
11127
|
+
return await this.cardStartPreview(
|
|
11128
|
+
payload
|
|
11129
|
+
);
|
|
9735
11130
|
case "cardSessions":
|
|
9736
11131
|
return this.cardSessions();
|
|
9737
11132
|
case "cardSession":
|
|
@@ -9747,6 +11142,20 @@ ${buildPrompt(prompts_default.implement)}`;
|
|
|
9747
11142
|
return await this.ensureSetup();
|
|
9748
11143
|
case "cardRemoteBranches":
|
|
9749
11144
|
return await this.cardRemoteBranches();
|
|
11145
|
+
case "onedriveStartDeviceCode":
|
|
11146
|
+
return await this.onedriveStartDeviceCode(
|
|
11147
|
+
payload
|
|
11148
|
+
);
|
|
11149
|
+
case "onedriveDisconnect":
|
|
11150
|
+
return this.onedriveDisconnect();
|
|
11151
|
+
case "onedriveTeardown":
|
|
11152
|
+
return await this.onedriveTeardown(
|
|
11153
|
+
payload
|
|
11154
|
+
);
|
|
11155
|
+
case "onedriveStatus":
|
|
11156
|
+
return this.onedriveStatus(
|
|
11157
|
+
payload
|
|
11158
|
+
);
|
|
9750
11159
|
case "cardFinalize":
|
|
9751
11160
|
return await this.cardFinalize(
|
|
9752
11161
|
payload
|
|
@@ -9760,6 +11169,77 @@ ${buildPrompt(prompts_default.implement)}`;
|
|
|
9760
11169
|
case "cardKill":
|
|
9761
11170
|
await this.cardKill(payload);
|
|
9762
11171
|
return void 0;
|
|
11172
|
+
case "bareFsList":
|
|
11173
|
+
return await this.bareFsList(
|
|
11174
|
+
payload
|
|
11175
|
+
);
|
|
11176
|
+
case "bareFsSearch":
|
|
11177
|
+
return await this.bareFsSearch(
|
|
11178
|
+
payload
|
|
11179
|
+
);
|
|
11180
|
+
case "bareFsRead":
|
|
11181
|
+
return await this.bareFsRead(
|
|
11182
|
+
payload
|
|
11183
|
+
);
|
|
11184
|
+
case "bareFsWrite":
|
|
11185
|
+
await this.bareFsWrite(
|
|
11186
|
+
payload
|
|
11187
|
+
);
|
|
11188
|
+
return void 0;
|
|
11189
|
+
case "bareFsDelete":
|
|
11190
|
+
await this.bareFsDelete(
|
|
11191
|
+
payload
|
|
11192
|
+
);
|
|
11193
|
+
return void 0;
|
|
11194
|
+
case "bareFsMkdir":
|
|
11195
|
+
await this.bareFsMkdir(
|
|
11196
|
+
payload
|
|
11197
|
+
);
|
|
11198
|
+
return void 0;
|
|
11199
|
+
case "bareReview":
|
|
11200
|
+
return await this.bareReview(
|
|
11201
|
+
payload
|
|
11202
|
+
);
|
|
11203
|
+
case "bareGitStatus":
|
|
11204
|
+
return await this.bareGitStatus(
|
|
11205
|
+
payload
|
|
11206
|
+
);
|
|
11207
|
+
case "bareListTerminals":
|
|
11208
|
+
return await this.bareListTerminals(
|
|
11209
|
+
payload
|
|
11210
|
+
);
|
|
11211
|
+
case "bareEditorState":
|
|
11212
|
+
return this.bareEditorState(
|
|
11213
|
+
payload
|
|
11214
|
+
);
|
|
11215
|
+
case "bareOpenFile":
|
|
11216
|
+
this.bareOpenFile(
|
|
11217
|
+
payload
|
|
11218
|
+
);
|
|
11219
|
+
return void 0;
|
|
11220
|
+
case "bareCloseFile":
|
|
11221
|
+
this.bareCloseFile(
|
|
11222
|
+
payload
|
|
11223
|
+
);
|
|
11224
|
+
return void 0;
|
|
11225
|
+
case "bareBufferSet":
|
|
11226
|
+
this.bareBufferSet(
|
|
11227
|
+
payload
|
|
11228
|
+
);
|
|
11229
|
+
return void 0;
|
|
11230
|
+
case "bareExpose":
|
|
11231
|
+
return await this.bareExpose(
|
|
11232
|
+
payload
|
|
11233
|
+
);
|
|
11234
|
+
case "bareUnexpose":
|
|
11235
|
+
await this.bareUnexpose(
|
|
11236
|
+
payload
|
|
11237
|
+
);
|
|
11238
|
+
return void 0;
|
|
11239
|
+
case "bareListExposed":
|
|
11240
|
+
return this.bareListExposed(
|
|
11241
|
+
payload
|
|
11242
|
+
);
|
|
9763
11243
|
default:
|
|
9764
11244
|
throw new Error(`Unknown board-agent action: ${String(action)}`);
|
|
9765
11245
|
}
|
|
@@ -9816,7 +11296,7 @@ function makeChunkReassembler() {
|
|
|
9816
11296
|
|
|
9817
11297
|
// src/daemon.ts
|
|
9818
11298
|
var import_node_datachannel = require("node-datachannel");
|
|
9819
|
-
var
|
|
11299
|
+
var import_path5 = __toESM(require("path"));
|
|
9820
11300
|
|
|
9821
11301
|
// ../../node_modules/engine.io-client/build/esm-debug/transports/polling-xhr.node.js
|
|
9822
11302
|
var XMLHttpRequestModule = __toESM(require_XMLHttpRequest(), 1);
|
|
@@ -10530,8 +12010,8 @@ var BaseXHR = class extends Polling {
|
|
|
10530
12010
|
/**
|
|
10531
12011
|
* Sends data.
|
|
10532
12012
|
*
|
|
10533
|
-
* @param {String} data to send.
|
|
10534
|
-
* @param {Function} called upon flush.
|
|
12013
|
+
* @param {String} data - data to send.
|
|
12014
|
+
* @param {Function} fn - called upon flush.
|
|
10535
12015
|
* @private
|
|
10536
12016
|
*/
|
|
10537
12017
|
doWrite(data, fn) {
|
|
@@ -10751,8 +12231,11 @@ var XHR = class extends BaseXHR {
|
|
|
10751
12231
|
|
|
10752
12232
|
// ../../node_modules/engine.io-client/node_modules/ws/wrapper.mjs
|
|
10753
12233
|
var import_stream = __toESM(require_stream(), 1);
|
|
12234
|
+
var import_extension = __toESM(require_extension(), 1);
|
|
12235
|
+
var import_permessage_deflate = __toESM(require_permessage_deflate(), 1);
|
|
10754
12236
|
var import_receiver = __toESM(require_receiver(), 1);
|
|
10755
12237
|
var import_sender = __toESM(require_sender(), 1);
|
|
12238
|
+
var import_subprotocol = __toESM(require_subprotocol(), 1);
|
|
10756
12239
|
var import_websocket = __toESM(require_websocket(), 1);
|
|
10757
12240
|
var import_websocket_server = __toESM(require_websocket_server(), 1);
|
|
10758
12241
|
|
|
@@ -10993,12 +12476,12 @@ function parse2(str) {
|
|
|
10993
12476
|
uri.queryKey = queryKey(uri, uri["query"]);
|
|
10994
12477
|
return uri;
|
|
10995
12478
|
}
|
|
10996
|
-
function pathNames(obj,
|
|
10997
|
-
const regx = /\/{2,9}/g, names =
|
|
10998
|
-
if (
|
|
12479
|
+
function pathNames(obj, path7) {
|
|
12480
|
+
const regx = /\/{2,9}/g, names = path7.replace(regx, "/").split("/");
|
|
12481
|
+
if (path7.slice(0, 1) == "/" || path7.length === 0) {
|
|
10999
12482
|
names.splice(0, 1);
|
|
11000
12483
|
}
|
|
11001
|
-
if (
|
|
12484
|
+
if (path7.slice(-1) == "/") {
|
|
11002
12485
|
names.splice(names.length - 1, 1);
|
|
11003
12486
|
}
|
|
11004
12487
|
return names;
|
|
@@ -11353,7 +12836,7 @@ var SocketWithoutUpgrade = class _SocketWithoutUpgrade extends import_component_
|
|
|
11353
12836
|
/**
|
|
11354
12837
|
* Sends a packet.
|
|
11355
12838
|
*
|
|
11356
|
-
* @param {String} type
|
|
12839
|
+
* @param {String} type - packet type.
|
|
11357
12840
|
* @param {String} data.
|
|
11358
12841
|
* @param {Object} options.
|
|
11359
12842
|
* @param {Function} fn - callback function.
|
|
@@ -11616,7 +13099,7 @@ var protocol2 = Socket.protocol;
|
|
|
11616
13099
|
// ../../node_modules/socket.io-client/build/esm-debug/url.js
|
|
11617
13100
|
var import_debug7 = __toESM(require_src(), 1);
|
|
11618
13101
|
var debug7 = (0, import_debug7.default)("socket.io-client:url");
|
|
11619
|
-
function url(uri,
|
|
13102
|
+
function url(uri, path7 = "", loc) {
|
|
11620
13103
|
let obj = uri;
|
|
11621
13104
|
loc = loc || typeof location !== "undefined" && location;
|
|
11622
13105
|
if (null == uri)
|
|
@@ -11650,7 +13133,7 @@ function url(uri, path6 = "", loc) {
|
|
|
11650
13133
|
obj.path = obj.path || "/";
|
|
11651
13134
|
const ipv6 = obj.host.indexOf(":") !== -1;
|
|
11652
13135
|
const host = ipv6 ? "[" + obj.host + "]" : obj.host;
|
|
11653
|
-
obj.id = obj.protocol + "://" + host + ":" + obj.port +
|
|
13136
|
+
obj.id = obj.protocol + "://" + host + ":" + obj.port + path7;
|
|
11654
13137
|
obj.href = obj.protocol + "://" + host + (loc && loc.port === obj.port ? "" : ":" + obj.port);
|
|
11655
13138
|
return obj;
|
|
11656
13139
|
}
|
|
@@ -13284,8 +14767,8 @@ function lookup(uri, opts) {
|
|
|
13284
14767
|
const parsed = url(uri, opts.path || "/socket.io");
|
|
13285
14768
|
const source = parsed.source;
|
|
13286
14769
|
const id = parsed.id;
|
|
13287
|
-
const
|
|
13288
|
-
const sameNamespace = cache[id] &&
|
|
14770
|
+
const path7 = parsed.path;
|
|
14771
|
+
const sameNamespace = cache[id] && path7 in cache[id]["nsps"];
|
|
13289
14772
|
const newConnection = opts.forceNew || opts["force new connection"] || false === opts.multiplex || sameNamespace;
|
|
13290
14773
|
let io;
|
|
13291
14774
|
if (newConnection) {
|
|
@@ -13310,44 +14793,296 @@ Object.assign(lookup, {
|
|
|
13310
14793
|
connect: lookup
|
|
13311
14794
|
});
|
|
13312
14795
|
|
|
13313
|
-
//
|
|
14796
|
+
// ../../shared/all/helpers/terminal-ai/terminal.ts
|
|
14797
|
+
var import_child_process6 = require("child_process");
|
|
14798
|
+
var import_dockerode = __toESM(require("dockerode"));
|
|
13314
14799
|
var import_fs3 = __toESM(require("fs"));
|
|
13315
14800
|
var import_os2 = __toESM(require("os"));
|
|
13316
14801
|
var import_path3 = __toESM(require("path"));
|
|
13317
|
-
|
|
14802
|
+
function resolveDocker() {
|
|
14803
|
+
if (process.env.DOCKER_HOST) return new import_dockerode.default();
|
|
14804
|
+
try {
|
|
14805
|
+
const out = (0, import_child_process6.execFileSync)(
|
|
14806
|
+
"docker",
|
|
14807
|
+
["context", "inspect", "--format", "{{.Endpoints.docker.Host}}"],
|
|
14808
|
+
{ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
|
|
14809
|
+
).trim();
|
|
14810
|
+
if (out.startsWith("unix://")) {
|
|
14811
|
+
const p = out.slice("unix://".length);
|
|
14812
|
+
if (import_fs3.default.existsSync(p)) return new import_dockerode.default({ socketPath: p });
|
|
14813
|
+
} else if (out) {
|
|
14814
|
+
process.env.DOCKER_HOST = out;
|
|
14815
|
+
return new import_dockerode.default();
|
|
14816
|
+
}
|
|
14817
|
+
} catch {
|
|
14818
|
+
}
|
|
14819
|
+
const candidates = [
|
|
14820
|
+
"/var/run/docker.sock",
|
|
14821
|
+
import_path3.default.join(import_os2.default.homedir(), ".docker/run/docker.sock"),
|
|
14822
|
+
import_path3.default.join(import_os2.default.homedir(), ".docker/desktop/docker.sock"),
|
|
14823
|
+
import_path3.default.join(import_os2.default.homedir(), ".orbstack/run/docker.sock"),
|
|
14824
|
+
import_path3.default.join(import_os2.default.homedir(), ".colima/default/docker.sock"),
|
|
14825
|
+
import_path3.default.join(import_os2.default.homedir(), ".rd/docker.sock")
|
|
14826
|
+
];
|
|
14827
|
+
for (const p of candidates) {
|
|
14828
|
+
try {
|
|
14829
|
+
import_fs3.default.statSync(p);
|
|
14830
|
+
return new import_dockerode.default({ socketPath: p });
|
|
14831
|
+
} catch {
|
|
14832
|
+
}
|
|
14833
|
+
}
|
|
14834
|
+
return new import_dockerode.default();
|
|
14835
|
+
}
|
|
14836
|
+
var docker = resolveDocker();
|
|
14837
|
+
var TERMINAL_INIT_SCRIPT = [
|
|
14838
|
+
`export PS1='\\[\\e[1;32m\\]user@bare\\[\\e[0m\\]:\\w\\$ '`,
|
|
14839
|
+
`exec bash --norc -i`
|
|
14840
|
+
].join("; ");
|
|
14841
|
+
function tmuxName(terminalKey) {
|
|
14842
|
+
const safe = terminalKey.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80);
|
|
14843
|
+
return `terminal-${safe}`;
|
|
14844
|
+
}
|
|
14845
|
+
var TerminalManager = class {
|
|
14846
|
+
constructor(send, getCardContainerName) {
|
|
14847
|
+
this.send = send;
|
|
14848
|
+
this.getCardContainerName = getCardContainerName;
|
|
14849
|
+
this.sessions = /* @__PURE__ */ new Map();
|
|
14850
|
+
}
|
|
14851
|
+
handle(msg) {
|
|
14852
|
+
switch (msg.op) {
|
|
14853
|
+
case "open":
|
|
14854
|
+
void this.open(msg);
|
|
14855
|
+
return;
|
|
14856
|
+
case "stdin":
|
|
14857
|
+
this.stdin(msg.sessionId, msg.data);
|
|
14858
|
+
return;
|
|
14859
|
+
case "resize":
|
|
14860
|
+
void this.resize(msg.sessionId, msg.cols, msg.rows);
|
|
14861
|
+
return;
|
|
14862
|
+
case "close":
|
|
14863
|
+
this.close(msg.sessionId);
|
|
14864
|
+
return;
|
|
14865
|
+
case "kill":
|
|
14866
|
+
void this.killTmuxSession(msg.postId, msg.terminalKey);
|
|
14867
|
+
return;
|
|
14868
|
+
}
|
|
14869
|
+
}
|
|
14870
|
+
async open(msg) {
|
|
14871
|
+
if (this.sessions.has(msg.sessionId)) {
|
|
14872
|
+
this.emit({
|
|
14873
|
+
type: "terminal",
|
|
14874
|
+
op: "error",
|
|
14875
|
+
sessionId: msg.sessionId,
|
|
14876
|
+
message: "Session already open."
|
|
14877
|
+
});
|
|
14878
|
+
return;
|
|
14879
|
+
}
|
|
14880
|
+
const containerName = this.getCardContainerName(msg.postId);
|
|
14881
|
+
if (!containerName) {
|
|
14882
|
+
this.emit({
|
|
14883
|
+
type: "terminal",
|
|
14884
|
+
op: "error",
|
|
14885
|
+
sessionId: msg.sessionId,
|
|
14886
|
+
message: `No active card session for postId=${msg.postId}.`
|
|
14887
|
+
});
|
|
14888
|
+
return;
|
|
14889
|
+
}
|
|
14890
|
+
const tmuxSession = tmuxName(msg.terminalKey);
|
|
14891
|
+
try {
|
|
14892
|
+
const exec = await docker.getContainer(containerName).exec({
|
|
14893
|
+
Cmd: [
|
|
14894
|
+
"tmux",
|
|
14895
|
+
// -u forces UTF-8 regardless of the locale. Slim base images often
|
|
14896
|
+
// ship without a UTF-8 locale, so tmux's auto-detect falls through to
|
|
14897
|
+
// ASCII line-draw chars (the "qqqq" mess in place of ─).
|
|
14898
|
+
"-u",
|
|
14899
|
+
"new-session",
|
|
14900
|
+
"-A",
|
|
14901
|
+
"-s",
|
|
14902
|
+
tmuxSession,
|
|
14903
|
+
"--",
|
|
14904
|
+
"bash",
|
|
14905
|
+
"-c",
|
|
14906
|
+
TERMINAL_INIT_SCRIPT
|
|
14907
|
+
],
|
|
14908
|
+
AttachStdin: true,
|
|
14909
|
+
AttachStdout: true,
|
|
14910
|
+
AttachStderr: true,
|
|
14911
|
+
Tty: true,
|
|
14912
|
+
// Drop the user into the cloned repo so claude/git/etc. just work.
|
|
14913
|
+
WorkingDir: "/home/claude/workspace",
|
|
14914
|
+
Env: ["TERM=xterm-256color"],
|
|
14915
|
+
// The client follows up with a resize op carrying real dimensions.
|
|
14916
|
+
ConsoleSize: [msg.rows, msg.cols]
|
|
14917
|
+
});
|
|
14918
|
+
const stream = await exec.start({
|
|
14919
|
+
hijack: true,
|
|
14920
|
+
stdin: true,
|
|
14921
|
+
Tty: true
|
|
14922
|
+
});
|
|
14923
|
+
this.sessions.set(msg.sessionId, { exec, stream, containerName });
|
|
14924
|
+
stream.on("data", (chunk) => {
|
|
14925
|
+
this.emit({
|
|
14926
|
+
type: "terminal",
|
|
14927
|
+
op: "stdout",
|
|
14928
|
+
sessionId: msg.sessionId,
|
|
14929
|
+
data: chunk.toString("base64")
|
|
14930
|
+
});
|
|
14931
|
+
});
|
|
14932
|
+
stream.on("error", (err) => {
|
|
14933
|
+
this.cleanup(msg.sessionId);
|
|
14934
|
+
this.emit({
|
|
14935
|
+
type: "terminal",
|
|
14936
|
+
op: "error",
|
|
14937
|
+
sessionId: msg.sessionId,
|
|
14938
|
+
message: err.message
|
|
14939
|
+
});
|
|
14940
|
+
});
|
|
14941
|
+
stream.on("end", () => {
|
|
14942
|
+
void this.finalize(msg.sessionId, exec);
|
|
14943
|
+
});
|
|
14944
|
+
stream.on("close", () => {
|
|
14945
|
+
void this.finalize(msg.sessionId, exec);
|
|
14946
|
+
});
|
|
14947
|
+
this.emit({
|
|
14948
|
+
type: "terminal",
|
|
14949
|
+
op: "opened",
|
|
14950
|
+
sessionId: msg.sessionId,
|
|
14951
|
+
containerName
|
|
14952
|
+
});
|
|
14953
|
+
} catch (err) {
|
|
14954
|
+
this.emit({
|
|
14955
|
+
type: "terminal",
|
|
14956
|
+
op: "error",
|
|
14957
|
+
sessionId: msg.sessionId,
|
|
14958
|
+
message: err instanceof Error ? err.message : String(err)
|
|
14959
|
+
});
|
|
14960
|
+
}
|
|
14961
|
+
}
|
|
14962
|
+
stdin(sessionId, b64) {
|
|
14963
|
+
const session = this.sessions.get(sessionId);
|
|
14964
|
+
if (!session) return;
|
|
14965
|
+
try {
|
|
14966
|
+
session.stream.write(Buffer.from(b64, "base64"));
|
|
14967
|
+
} catch {
|
|
14968
|
+
}
|
|
14969
|
+
}
|
|
14970
|
+
async resize(sessionId, cols, rows) {
|
|
14971
|
+
const session = this.sessions.get(sessionId);
|
|
14972
|
+
if (!session) return;
|
|
14973
|
+
try {
|
|
14974
|
+
await session.exec.resize({ h: rows, w: cols });
|
|
14975
|
+
} catch {
|
|
14976
|
+
}
|
|
14977
|
+
}
|
|
14978
|
+
close(sessionId) {
|
|
14979
|
+
const session = this.sessions.get(sessionId);
|
|
14980
|
+
if (!session) return;
|
|
14981
|
+
try {
|
|
14982
|
+
session.stream.end();
|
|
14983
|
+
} catch {
|
|
14984
|
+
}
|
|
14985
|
+
}
|
|
14986
|
+
/** Tear down the tmux session for a tab. Used when the user explicitly
|
|
14987
|
+
* removes a tab (the X button), not on transient detach. */
|
|
14988
|
+
async killTmuxSession(postId, terminalKey) {
|
|
14989
|
+
const containerName = this.getCardContainerName(postId);
|
|
14990
|
+
if (!containerName) return;
|
|
14991
|
+
const session = tmuxName(terminalKey);
|
|
14992
|
+
try {
|
|
14993
|
+
const exec = await docker.getContainer(containerName).exec({
|
|
14994
|
+
Cmd: ["tmux", "kill-session", "-t", session],
|
|
14995
|
+
AttachStdout: false,
|
|
14996
|
+
AttachStderr: false,
|
|
14997
|
+
Tty: false
|
|
14998
|
+
});
|
|
14999
|
+
await exec.start({});
|
|
15000
|
+
} catch {
|
|
15001
|
+
}
|
|
15002
|
+
}
|
|
15003
|
+
destroyAll() {
|
|
15004
|
+
for (const sessionId of [...this.sessions.keys()]) {
|
|
15005
|
+
this.close(sessionId);
|
|
15006
|
+
}
|
|
15007
|
+
}
|
|
15008
|
+
// Called only once even if both 'end' and 'close' fire.
|
|
15009
|
+
async finalize(sessionId, exec) {
|
|
15010
|
+
if (!this.sessions.has(sessionId)) return;
|
|
15011
|
+
this.cleanup(sessionId);
|
|
15012
|
+
let code = null;
|
|
15013
|
+
try {
|
|
15014
|
+
const info = await exec.inspect();
|
|
15015
|
+
code = typeof info.ExitCode === "number" ? info.ExitCode : null;
|
|
15016
|
+
} catch {
|
|
15017
|
+
}
|
|
15018
|
+
this.emit({ type: "terminal", op: "exit", sessionId, code });
|
|
15019
|
+
}
|
|
15020
|
+
cleanup(sessionId) {
|
|
15021
|
+
this.sessions.delete(sessionId);
|
|
15022
|
+
}
|
|
15023
|
+
emit(msg) {
|
|
15024
|
+
const wire = JSON.stringify(msg);
|
|
15025
|
+
for (const frame of chunkWire(wire)) this.send(frame);
|
|
15026
|
+
}
|
|
15027
|
+
};
|
|
15028
|
+
|
|
15029
|
+
// src/agent-adapter.ts
|
|
15030
|
+
var import_fs4 = __toESM(require("fs"));
|
|
15031
|
+
var import_os3 = __toESM(require("os"));
|
|
15032
|
+
var import_path4 = __toESM(require("path"));
|
|
15033
|
+
var CONFIG_DIR2 = import_path4.default.join(import_os3.default.homedir(), ".factiii-runner");
|
|
15034
|
+
function safeSlug(spaceSlug) {
|
|
15035
|
+
return spaceSlug.replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
15036
|
+
}
|
|
13318
15037
|
function configPath(spaceSlug) {
|
|
13319
|
-
|
|
13320
|
-
|
|
15038
|
+
return import_path4.default.join(CONFIG_DIR2, `space-${safeSlug(spaceSlug)}.json`);
|
|
15039
|
+
}
|
|
15040
|
+
function connectionPath(spaceSlug) {
|
|
15041
|
+
return import_path4.default.join(CONFIG_DIR2, `space-${safeSlug(spaceSlug)}.onedrive.json`);
|
|
15042
|
+
}
|
|
15043
|
+
function writeJson(filePath, value2) {
|
|
15044
|
+
import_fs4.default.mkdirSync(CONFIG_DIR2, { recursive: true });
|
|
15045
|
+
import_fs4.default.writeFileSync(filePath, JSON.stringify(value2, null, 2));
|
|
15046
|
+
import_fs4.default.chmodSync(filePath, 384);
|
|
13321
15047
|
}
|
|
13322
15048
|
var localConfigProvider = {
|
|
13323
15049
|
readConfig(spaceSlug) {
|
|
15050
|
+
let config = {};
|
|
13324
15051
|
try {
|
|
13325
|
-
|
|
13326
|
-
|
|
13327
|
-
|
|
13328
|
-
repoUrl: config.repoUrl || "",
|
|
13329
|
-
mainBranch: config.mainBranch || "main",
|
|
13330
|
-
gitName: config.gitName || "",
|
|
13331
|
-
gitEmail: config.gitEmail || "",
|
|
13332
|
-
claudeToken: config.claudeToken || "",
|
|
13333
|
-
githubToken: config.githubToken || ""
|
|
13334
|
-
};
|
|
15052
|
+
config = JSON.parse(
|
|
15053
|
+
import_fs4.default.readFileSync(configPath(spaceSlug), "utf-8")
|
|
15054
|
+
);
|
|
13335
15055
|
} catch {
|
|
13336
|
-
return {
|
|
13337
|
-
repoUrl: "",
|
|
13338
|
-
mainBranch: "main",
|
|
13339
|
-
gitName: "",
|
|
13340
|
-
gitEmail: "",
|
|
13341
|
-
claudeToken: "",
|
|
13342
|
-
githubToken: ""
|
|
13343
|
-
};
|
|
13344
15056
|
}
|
|
15057
|
+
return {
|
|
15058
|
+
repoUrl: config.repoUrl || "",
|
|
15059
|
+
mainBranch: config.mainBranch || "main",
|
|
15060
|
+
gitName: config.gitName || "",
|
|
15061
|
+
gitEmail: config.gitEmail || "",
|
|
15062
|
+
claudeToken: config.claudeToken || "",
|
|
15063
|
+
githubToken: config.githubToken || "",
|
|
15064
|
+
storageBackend: config.storageBackend || "github"
|
|
15065
|
+
};
|
|
13345
15066
|
},
|
|
13346
15067
|
writeConfig(spaceSlug, config) {
|
|
13347
|
-
|
|
13348
|
-
|
|
13349
|
-
|
|
13350
|
-
|
|
15068
|
+
writeJson(configPath(spaceSlug), config);
|
|
15069
|
+
},
|
|
15070
|
+
readOneDriveConnection(spaceSlug) {
|
|
15071
|
+
try {
|
|
15072
|
+
return JSON.parse(
|
|
15073
|
+
import_fs4.default.readFileSync(connectionPath(spaceSlug), "utf-8")
|
|
15074
|
+
);
|
|
15075
|
+
} catch {
|
|
15076
|
+
return null;
|
|
15077
|
+
}
|
|
15078
|
+
},
|
|
15079
|
+
writeOneDriveConnection(spaceSlug, connection) {
|
|
15080
|
+
const filePath = connectionPath(spaceSlug);
|
|
15081
|
+
if (!connection) {
|
|
15082
|
+
import_fs4.default.rmSync(filePath, { force: true });
|
|
15083
|
+
return;
|
|
15084
|
+
}
|
|
15085
|
+
writeJson(filePath, connection);
|
|
13351
15086
|
}
|
|
13352
15087
|
};
|
|
13353
15088
|
|
|
@@ -13362,6 +15097,14 @@ async function startDaemon(config) {
|
|
|
13362
15097
|
});
|
|
13363
15098
|
const engines = /* @__PURE__ */ new Map();
|
|
13364
15099
|
const openChannels = /* @__PURE__ */ new Set();
|
|
15100
|
+
const terminalByChannel = /* @__PURE__ */ new WeakMap();
|
|
15101
|
+
const findCardContainer = (postId) => {
|
|
15102
|
+
for (const engine of engines.values()) {
|
|
15103
|
+
const name = engine.getCardContainerName(postId);
|
|
15104
|
+
if (name) return name;
|
|
15105
|
+
}
|
|
15106
|
+
return null;
|
|
15107
|
+
};
|
|
13365
15108
|
const broadcastToAll = (msg) => {
|
|
13366
15109
|
const frames = chunkWire(msg);
|
|
13367
15110
|
for (const dc of openChannels) {
|
|
@@ -13469,12 +15212,45 @@ async function startDaemon(config) {
|
|
|
13469
15212
|
}
|
|
13470
15213
|
}
|
|
13471
15214
|
};
|
|
15215
|
+
const sendRawToChannel = (frame) => {
|
|
15216
|
+
try {
|
|
15217
|
+
dc.sendMessage(frame);
|
|
15218
|
+
} catch {
|
|
15219
|
+
}
|
|
15220
|
+
};
|
|
15221
|
+
terminalByChannel.set(
|
|
15222
|
+
dc,
|
|
15223
|
+
new TerminalManager(sendRawToChannel, findCardContainer)
|
|
15224
|
+
);
|
|
15225
|
+
dc.onOpen(() => {
|
|
15226
|
+
for (const [spaceSlug, engine] of engines.entries()) {
|
|
15227
|
+
const sessions = engine.cardSessions();
|
|
15228
|
+
for (const summary of Object.values(sessions)) {
|
|
15229
|
+
const msg = JSON.stringify({
|
|
15230
|
+
type: "event",
|
|
15231
|
+
spaceSlug,
|
|
15232
|
+
event: `board-agent:${spaceSlug}:session-update`,
|
|
15233
|
+
payload: summary
|
|
15234
|
+
});
|
|
15235
|
+
sendToChannel(msg);
|
|
15236
|
+
}
|
|
15237
|
+
}
|
|
15238
|
+
});
|
|
13472
15239
|
dc.onMessage((raw) => {
|
|
13473
15240
|
const rawStr = typeof raw === "string" ? raw : raw.toString("utf-8");
|
|
13474
15241
|
const msgStr = reassembler.feed(rawStr);
|
|
13475
15242
|
if (msgStr === null) return;
|
|
13476
15243
|
try {
|
|
13477
15244
|
const parsed = JSON.parse(msgStr);
|
|
15245
|
+
if (parsed.type === "terminal") {
|
|
15246
|
+
const mgr = terminalByChannel.get(dc);
|
|
15247
|
+
if (mgr) {
|
|
15248
|
+
mgr.handle(
|
|
15249
|
+
parsed
|
|
15250
|
+
);
|
|
15251
|
+
}
|
|
15252
|
+
return;
|
|
15253
|
+
}
|
|
13478
15254
|
if (parsed.type === "broadcast" && parsed.spaceSlug && parsed.event) {
|
|
13479
15255
|
const eventMsg = JSON.stringify({
|
|
13480
15256
|
type: "event",
|
|
@@ -13506,6 +15282,11 @@ async function startDaemon(config) {
|
|
|
13506
15282
|
dc.onClosed(() => {
|
|
13507
15283
|
console.log(`Data channel closed with ${data.fromSocketId}`);
|
|
13508
15284
|
openChannels.delete(dc);
|
|
15285
|
+
const mgr = terminalByChannel.get(dc);
|
|
15286
|
+
if (mgr) {
|
|
15287
|
+
mgr.destroyAll();
|
|
15288
|
+
terminalByChannel.delete(dc);
|
|
15289
|
+
}
|
|
13509
15290
|
detachIceHandler();
|
|
13510
15291
|
});
|
|
13511
15292
|
});
|
|
@@ -13578,16 +15359,18 @@ async function handleDataChannelMessage(raw, sendToChannel, broadcastToAll, getE
|
|
|
13578
15359
|
}
|
|
13579
15360
|
}
|
|
13580
15361
|
function getDockerfilePath() {
|
|
13581
|
-
return
|
|
15362
|
+
return import_path5.default.join(__dirname, "..", "Dockerfile.claude");
|
|
13582
15363
|
}
|
|
13583
15364
|
|
|
13584
15365
|
// src/setup.ts
|
|
13585
|
-
var
|
|
13586
|
-
var
|
|
13587
|
-
var
|
|
15366
|
+
var import_child_process7 = require("child_process");
|
|
15367
|
+
var import_crypto2 = __toESM(require("crypto"));
|
|
15368
|
+
var import_fs5 = __toESM(require("fs"));
|
|
15369
|
+
var import_path6 = __toESM(require("path"));
|
|
15370
|
+
var DOCKERFILE_LABEL2 = "factiii.dockerfile-sha";
|
|
13588
15371
|
function run(bin, args, opts) {
|
|
13589
15372
|
try {
|
|
13590
|
-
return (0,
|
|
15373
|
+
return (0, import_child_process7.execFileSync)(bin, args, {
|
|
13591
15374
|
encoding: "utf-8",
|
|
13592
15375
|
stdio: opts?.silent ? "pipe" : "inherit",
|
|
13593
15376
|
// Avoids a cmd.exe window flash on Windows during silent checks.
|
|
@@ -13600,7 +15383,7 @@ function run(bin, args, opts) {
|
|
|
13600
15383
|
function check(cmd) {
|
|
13601
15384
|
const lookup2 = process.platform === "win32" ? "where" : "which";
|
|
13602
15385
|
try {
|
|
13603
|
-
(0,
|
|
15386
|
+
(0, import_child_process7.execFileSync)(lookup2, [cmd], { stdio: "pipe", windowsHide: true });
|
|
13604
15387
|
return true;
|
|
13605
15388
|
} catch {
|
|
13606
15389
|
return false;
|
|
@@ -13630,21 +15413,32 @@ async function setup(serverUrl) {
|
|
|
13630
15413
|
);
|
|
13631
15414
|
process.exit(1);
|
|
13632
15415
|
}
|
|
13633
|
-
const
|
|
15416
|
+
const wantedHash = import_crypto2.default.createHash("sha256").update(import_fs5.default.readFileSync(dockerfilePath)).digest("hex");
|
|
15417
|
+
const existingHash = run(
|
|
13634
15418
|
"docker",
|
|
13635
|
-
[
|
|
15419
|
+
[
|
|
15420
|
+
"image",
|
|
15421
|
+
"inspect",
|
|
15422
|
+
"factiii-claude",
|
|
15423
|
+
"--format",
|
|
15424
|
+
`{{ index .Config.Labels "${DOCKERFILE_LABEL2}" }}`
|
|
15425
|
+
],
|
|
13636
15426
|
{ silent: true }
|
|
13637
|
-
);
|
|
13638
|
-
if (
|
|
13639
|
-
console.log(" Image
|
|
15427
|
+
).trim();
|
|
15428
|
+
if (existingHash === wantedHash) {
|
|
15429
|
+
console.log(" Image is up to date.");
|
|
13640
15430
|
} else {
|
|
13641
|
-
console.log(
|
|
15431
|
+
console.log(
|
|
15432
|
+
existingHash ? ` Dockerfile changed since last build, rebuilding from ${dockerfilePath}...` : ` Building from ${dockerfilePath}...`
|
|
15433
|
+
);
|
|
13642
15434
|
run("docker", [
|
|
13643
15435
|
"build",
|
|
13644
15436
|
"-t",
|
|
13645
15437
|
"factiii-claude",
|
|
13646
15438
|
"-f",
|
|
13647
15439
|
dockerfilePath,
|
|
15440
|
+
"--label",
|
|
15441
|
+
`${DOCKERFILE_LABEL2}=${wantedHash}`,
|
|
13648
15442
|
"."
|
|
13649
15443
|
]);
|
|
13650
15444
|
console.log(" Image built.");
|
|
@@ -13658,13 +15452,14 @@ async function setup(serverUrl) {
|
|
|
13658
15452
|
console.log("Start the runner with: factiii-runner start\n");
|
|
13659
15453
|
}
|
|
13660
15454
|
function findDockerfile() {
|
|
13661
|
-
const p =
|
|
13662
|
-
return
|
|
15455
|
+
const p = import_path6.default.join(__dirname, "..", "Dockerfile.claude");
|
|
15456
|
+
return import_fs5.default.existsSync(p) ? p : null;
|
|
13663
15457
|
}
|
|
13664
15458
|
|
|
13665
15459
|
// src/cli.ts
|
|
15460
|
+
import_node_dns.default.setDefaultResultOrder("ipv4first");
|
|
13666
15461
|
var program2 = new Command();
|
|
13667
|
-
program2.name("factiii-runner").description("Factiii Runner
|
|
15462
|
+
program2.name("factiii-runner").description("Factiii Runner - run Board AI agents on remote servers").version("0.0.1");
|
|
13668
15463
|
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) => {
|
|
13669
15464
|
try {
|
|
13670
15465
|
await setup(opts.server);
|