@factiii/runner 0.2.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +744 -271
  2. package/package.json +6 -4
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 path7 = require("node:path");
967
- var fs6 = require("node:fs");
966
+ var path8 = require("node:path");
967
+ var fs7 = require("node:fs");
968
968
  var process2 = require("node:process");
969
969
  var { Argument: Argument2, humanReadableArgName } = require_argument();
970
970
  var { CommanderError: CommanderError2 } = require_error();
@@ -1896,11 +1896,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
1896
1896
  let launchWithNode = false;
1897
1897
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
1898
1898
  function findFile(baseDir, baseName) {
1899
- const localBin = path7.resolve(baseDir, baseName);
1900
- if (fs6.existsSync(localBin)) return localBin;
1901
- if (sourceExt.includes(path7.extname(baseName))) return void 0;
1899
+ const localBin = path8.resolve(baseDir, baseName);
1900
+ if (fs7.existsSync(localBin)) return localBin;
1901
+ if (sourceExt.includes(path8.extname(baseName))) return void 0;
1902
1902
  const foundExt = sourceExt.find(
1903
- (ext) => fs6.existsSync(`${localBin}${ext}`)
1903
+ (ext) => fs7.existsSync(`${localBin}${ext}`)
1904
1904
  );
1905
1905
  if (foundExt) return `${localBin}${foundExt}`;
1906
1906
  return void 0;
@@ -1912,21 +1912,21 @@ Expecting one of '${allowedValues.join("', '")}'`);
1912
1912
  if (this._scriptPath) {
1913
1913
  let resolvedScriptPath;
1914
1914
  try {
1915
- resolvedScriptPath = fs6.realpathSync(this._scriptPath);
1915
+ resolvedScriptPath = fs7.realpathSync(this._scriptPath);
1916
1916
  } catch (err) {
1917
1917
  resolvedScriptPath = this._scriptPath;
1918
1918
  }
1919
- executableDir = path7.resolve(
1920
- path7.dirname(resolvedScriptPath),
1919
+ executableDir = path8.resolve(
1920
+ path8.dirname(resolvedScriptPath),
1921
1921
  executableDir
1922
1922
  );
1923
1923
  }
1924
1924
  if (executableDir) {
1925
1925
  let localFile = findFile(executableDir, executableFile);
1926
1926
  if (!localFile && !subcommand._executableFile && this._scriptPath) {
1927
- const legacyName = path7.basename(
1927
+ const legacyName = path8.basename(
1928
1928
  this._scriptPath,
1929
- path7.extname(this._scriptPath)
1929
+ path8.extname(this._scriptPath)
1930
1930
  );
1931
1931
  if (legacyName !== this._name) {
1932
1932
  localFile = findFile(
@@ -1937,7 +1937,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1937
1937
  }
1938
1938
  executableFile = localFile || executableFile;
1939
1939
  }
1940
- launchWithNode = sourceExt.includes(path7.extname(executableFile));
1940
+ launchWithNode = sourceExt.includes(path8.extname(executableFile));
1941
1941
  let proc;
1942
1942
  if (process2.platform !== "win32") {
1943
1943
  if (launchWithNode) {
@@ -2777,7 +2777,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2777
2777
  * @return {Command}
2778
2778
  */
2779
2779
  nameFromFilename(filename) {
2780
- this._name = path7.basename(filename, path7.extname(filename));
2780
+ this._name = path8.basename(filename, path8.extname(filename));
2781
2781
  return this;
2782
2782
  }
2783
2783
  /**
@@ -2791,9 +2791,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
2791
2791
  * @param {string} [path]
2792
2792
  * @return {(string|null|Command)}
2793
2793
  */
2794
- executableDir(path8) {
2795
- if (path8 === void 0) return this._executableDir;
2796
- this._executableDir = path8;
2794
+ executableDir(path9) {
2795
+ if (path9 === void 0) return this._executableDir;
2796
+ this._executableDir = path9;
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,7 +3026,7 @@ var require_commander = __commonJS({
3026
3026
  // ../../node_modules/xmlhttprequest-ssl/lib/XMLHttpRequest.js
3027
3027
  var require_XMLHttpRequest = __commonJS({
3028
3028
  "../../node_modules/xmlhttprequest-ssl/lib/XMLHttpRequest.js"(exports2, module2) {
3029
- var fs6 = require("fs");
3029
+ var fs7 = require("fs");
3030
3030
  var Url = require("url");
3031
3031
  var spawn3 = require("child_process").spawn;
3032
3032
  module2.exports = XMLHttpRequest3;
@@ -3184,7 +3184,7 @@ var require_XMLHttpRequest = __commonJS({
3184
3184
  throw new Error("XMLHttpRequest: Only GET method is supported");
3185
3185
  }
3186
3186
  if (settings.async) {
3187
- fs6.readFile(unescape(url2.pathname), function(error, data2) {
3187
+ fs7.readFile(unescape(url2.pathname), function(error, data2) {
3188
3188
  if (error) {
3189
3189
  self.handleError(error, error.errno || -1);
3190
3190
  } else {
@@ -3196,7 +3196,7 @@ var require_XMLHttpRequest = __commonJS({
3196
3196
  });
3197
3197
  } else {
3198
3198
  try {
3199
- this.response = fs6.readFileSync(unescape(url2.pathname));
3199
+ this.response = fs7.readFileSync(unescape(url2.pathname));
3200
3200
  this.responseText = this.response.toString("utf8");
3201
3201
  this.status = 200;
3202
3202
  setState(self.DONE);
@@ -3322,15 +3322,15 @@ var require_XMLHttpRequest = __commonJS({
3322
3322
  } else {
3323
3323
  var contentFile = ".node-xmlhttprequest-content-" + process.pid;
3324
3324
  var syncFile = ".node-xmlhttprequest-sync-" + process.pid;
3325
- fs6.writeFileSync(syncFile, "", "utf8");
3325
+ fs7.writeFileSync(syncFile, "", "utf8");
3326
3326
  var execString = "var http = require('http'), https = require('https'), fs = require('fs');var doRequest = http" + (ssl ? "s" : "") + ".request;var options = " + JSON.stringify(options) + ";var responseText = '';var responseData = Buffer.alloc(0);var req = doRequest(options, function(response) {response.on('data', function(chunk) { var data = Buffer.from(chunk); responseText += data.toString('utf8'); responseData = Buffer.concat([responseData, data]);});response.on('end', function() {fs.writeFileSync('" + contentFile + "', JSON.stringify({err: null, data: {statusCode: response.statusCode, headers: response.headers, text: responseText, data: responseData.toString('base64')}}), 'utf8');fs.unlinkSync('" + syncFile + "');});response.on('error', function(error) {fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');fs.unlinkSync('" + syncFile + "');});}).on('error', function(error) {fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');fs.unlinkSync('" + syncFile + "');});" + (data ? "req.write('" + JSON.stringify(data).slice(1, -1).replace(/'/g, "\\'") + "');" : "") + "req.end();";
3327
3327
  var syncProc = spawn3(process.argv[0], ["-e", execString]);
3328
3328
  var statusText;
3329
- while (fs6.existsSync(syncFile)) {
3329
+ while (fs7.existsSync(syncFile)) {
3330
3330
  }
3331
- self.responseText = fs6.readFileSync(contentFile, "utf8");
3331
+ self.responseText = fs7.readFileSync(contentFile, "utf8");
3332
3332
  syncProc.stdin.end();
3333
- fs6.unlinkSync(contentFile);
3333
+ fs7.unlinkSync(contentFile);
3334
3334
  if (self.responseText.match(/^NODE-XMLHTTPREQUEST-ERROR:/)) {
3335
3335
  var errorObj = JSON.parse(self.responseText.replace(/^NODE-XMLHTTPREQUEST-ERROR:/, ""));
3336
3336
  self.handleError(errorObj, 503);
@@ -4260,9 +4260,9 @@ var require_src = __commonJS({
4260
4260
  }
4261
4261
  });
4262
4262
 
4263
- // ../../node_modules/engine.io-client/node_modules/ws/lib/constants.js
4263
+ // ../../node_modules/ws/lib/constants.js
4264
4264
  var require_constants = __commonJS({
4265
- "../../node_modules/engine.io-client/node_modules/ws/lib/constants.js"(exports2, module2) {
4265
+ "../../node_modules/ws/lib/constants.js"(exports2, module2) {
4266
4266
  "use strict";
4267
4267
  var BINARY_TYPES = ["nodebuffer", "arraybuffer", "fragments"];
4268
4268
  var hasBlob = typeof Blob !== "undefined";
@@ -4283,9 +4283,9 @@ var require_constants = __commonJS({
4283
4283
  }
4284
4284
  });
4285
4285
 
4286
- // ../../node_modules/engine.io-client/node_modules/ws/lib/buffer-util.js
4286
+ // ../../node_modules/ws/lib/buffer-util.js
4287
4287
  var require_buffer_util = __commonJS({
4288
- "../../node_modules/engine.io-client/node_modules/ws/lib/buffer-util.js"(exports2, module2) {
4288
+ "../../node_modules/ws/lib/buffer-util.js"(exports2, module2) {
4289
4289
  "use strict";
4290
4290
  var { EMPTY_BUFFER } = require_constants();
4291
4291
  var FastBuffer = Buffer[Symbol.species];
@@ -4358,9 +4358,9 @@ var require_buffer_util = __commonJS({
4358
4358
  }
4359
4359
  });
4360
4360
 
4361
- // ../../node_modules/engine.io-client/node_modules/ws/lib/limiter.js
4361
+ // ../../node_modules/ws/lib/limiter.js
4362
4362
  var require_limiter = __commonJS({
4363
- "../../node_modules/engine.io-client/node_modules/ws/lib/limiter.js"(exports2, module2) {
4363
+ "../../node_modules/ws/lib/limiter.js"(exports2, module2) {
4364
4364
  "use strict";
4365
4365
  var kDone = Symbol("kDone");
4366
4366
  var kRun = Symbol("kRun");
@@ -4408,9 +4408,9 @@ var require_limiter = __commonJS({
4408
4408
  }
4409
4409
  });
4410
4410
 
4411
- // ../../node_modules/engine.io-client/node_modules/ws/lib/permessage-deflate.js
4411
+ // ../../node_modules/ws/lib/permessage-deflate.js
4412
4412
  var require_permessage_deflate = __commonJS({
4413
- "../../node_modules/engine.io-client/node_modules/ws/lib/permessage-deflate.js"(exports2, module2) {
4413
+ "../../node_modules/ws/lib/permessage-deflate.js"(exports2, module2) {
4414
4414
  "use strict";
4415
4415
  var zlib = require("zlib");
4416
4416
  var bufferUtil = require_buffer_util();
@@ -4791,9 +4791,9 @@ var require_permessage_deflate = __commonJS({
4791
4791
  }
4792
4792
  });
4793
4793
 
4794
- // ../../node_modules/engine.io-client/node_modules/ws/lib/validation.js
4794
+ // ../../node_modules/ws/lib/validation.js
4795
4795
  var require_validation = __commonJS({
4796
- "../../node_modules/engine.io-client/node_modules/ws/lib/validation.js"(exports2, module2) {
4796
+ "../../node_modules/ws/lib/validation.js"(exports2, module2) {
4797
4797
  "use strict";
4798
4798
  var { isUtf8 } = require("buffer");
4799
4799
  var { hasBlob } = require_constants();
@@ -4992,9 +4992,9 @@ var require_validation = __commonJS({
4992
4992
  }
4993
4993
  });
4994
4994
 
4995
- // ../../node_modules/engine.io-client/node_modules/ws/lib/receiver.js
4995
+ // ../../node_modules/ws/lib/receiver.js
4996
4996
  var require_receiver = __commonJS({
4997
- "../../node_modules/engine.io-client/node_modules/ws/lib/receiver.js"(exports2, module2) {
4997
+ "../../node_modules/ws/lib/receiver.js"(exports2, module2) {
4998
4998
  "use strict";
4999
4999
  var { Writable } = require("stream");
5000
5000
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -5027,6 +5027,10 @@ var require_receiver = __commonJS({
5027
5027
  * extensions
5028
5028
  * @param {Boolean} [options.isServer=false] Specifies whether to operate in
5029
5029
  * client or server mode
5030
+ * @param {Number} [options.maxBufferedChunks=0] The maximum number of
5031
+ * buffered data chunks
5032
+ * @param {Number} [options.maxFragments=0] The maximum number of message
5033
+ * fragments
5030
5034
  * @param {Number} [options.maxPayload=0] The maximum allowed message length
5031
5035
  * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
5032
5036
  * not to skip UTF-8 validation for text and close messages
@@ -5037,6 +5041,8 @@ var require_receiver = __commonJS({
5037
5041
  this._binaryType = options.binaryType || BINARY_TYPES[0];
5038
5042
  this._extensions = options.extensions || {};
5039
5043
  this._isServer = !!options.isServer;
5044
+ this._maxBufferedChunks = options.maxBufferedChunks | 0;
5045
+ this._maxFragments = options.maxFragments | 0;
5040
5046
  this._maxPayload = options.maxPayload | 0;
5041
5047
  this._skipUTF8Validation = !!options.skipUTF8Validation;
5042
5048
  this[kWebSocket] = void 0;
@@ -5066,6 +5072,18 @@ var require_receiver = __commonJS({
5066
5072
  */
5067
5073
  _write(chunk, encoding, cb) {
5068
5074
  if (this._opcode === 8 && this._state == GET_INFO) return cb();
5075
+ if (this._maxBufferedChunks > 0 && this._buffers.length >= this._maxBufferedChunks) {
5076
+ cb(
5077
+ this.createError(
5078
+ RangeError,
5079
+ "Too many buffered chunks",
5080
+ false,
5081
+ 1008,
5082
+ "WS_ERR_TOO_MANY_BUFFERED_PARTS"
5083
+ )
5084
+ );
5085
+ return;
5086
+ }
5069
5087
  this._bufferedBytes += chunk.length;
5070
5088
  this._buffers.push(chunk);
5071
5089
  this.startLoop(cb);
@@ -5395,6 +5413,17 @@ var require_receiver = __commonJS({
5395
5413
  return;
5396
5414
  }
5397
5415
  if (data.length) {
5416
+ if (this._maxFragments > 0 && this._fragments.length >= this._maxFragments) {
5417
+ const error = this.createError(
5418
+ RangeError,
5419
+ "Too many message fragments",
5420
+ false,
5421
+ 1008,
5422
+ "WS_ERR_TOO_MANY_BUFFERED_PARTS"
5423
+ );
5424
+ cb(error);
5425
+ return;
5426
+ }
5398
5427
  this._messageLength = this._totalPayloadLength;
5399
5428
  this._fragments.push(data);
5400
5429
  }
@@ -5424,6 +5453,17 @@ var require_receiver = __commonJS({
5424
5453
  cb(error);
5425
5454
  return;
5426
5455
  }
5456
+ if (this._maxFragments > 0 && this._fragments.length >= this._maxFragments) {
5457
+ const error = this.createError(
5458
+ RangeError,
5459
+ "Too many message fragments",
5460
+ false,
5461
+ 1008,
5462
+ "WS_ERR_TOO_MANY_BUFFERED_PARTS"
5463
+ );
5464
+ cb(error);
5465
+ return;
5466
+ }
5427
5467
  this._fragments.push(buf);
5428
5468
  }
5429
5469
  this.dataMessage(cb);
@@ -5584,9 +5624,9 @@ var require_receiver = __commonJS({
5584
5624
  }
5585
5625
  });
5586
5626
 
5587
- // ../../node_modules/engine.io-client/node_modules/ws/lib/sender.js
5627
+ // ../../node_modules/ws/lib/sender.js
5588
5628
  var require_sender = __commonJS({
5589
- "../../node_modules/engine.io-client/node_modules/ws/lib/sender.js"(exports2, module2) {
5629
+ "../../node_modules/ws/lib/sender.js"(exports2, module2) {
5590
5630
  "use strict";
5591
5631
  var { Duplex } = require("stream");
5592
5632
  var { randomFillSync } = require("crypto");
@@ -6077,9 +6117,9 @@ var require_sender = __commonJS({
6077
6117
  }
6078
6118
  });
6079
6119
 
6080
- // ../../node_modules/engine.io-client/node_modules/ws/lib/event-target.js
6120
+ // ../../node_modules/ws/lib/event-target.js
6081
6121
  var require_event_target = __commonJS({
6082
- "../../node_modules/engine.io-client/node_modules/ws/lib/event-target.js"(exports2, module2) {
6122
+ "../../node_modules/ws/lib/event-target.js"(exports2, module2) {
6083
6123
  "use strict";
6084
6124
  var { kForOnEventAttribute, kListener } = require_constants();
6085
6125
  var kCode = Symbol("kCode");
@@ -6306,9 +6346,9 @@ var require_event_target = __commonJS({
6306
6346
  }
6307
6347
  });
6308
6348
 
6309
- // ../../node_modules/engine.io-client/node_modules/ws/lib/extension.js
6349
+ // ../../node_modules/ws/lib/extension.js
6310
6350
  var require_extension = __commonJS({
6311
- "../../node_modules/engine.io-client/node_modules/ws/lib/extension.js"(exports2, module2) {
6351
+ "../../node_modules/ws/lib/extension.js"(exports2, module2) {
6312
6352
  "use strict";
6313
6353
  var { tokenChars } = require_validation();
6314
6354
  function push(dest, name, elem) {
@@ -6459,9 +6499,9 @@ var require_extension = __commonJS({
6459
6499
  }
6460
6500
  });
6461
6501
 
6462
- // ../../node_modules/engine.io-client/node_modules/ws/lib/websocket.js
6502
+ // ../../node_modules/ws/lib/websocket.js
6463
6503
  var require_websocket = __commonJS({
6464
- "../../node_modules/engine.io-client/node_modules/ws/lib/websocket.js"(exports2, module2) {
6504
+ "../../node_modules/ws/lib/websocket.js"(exports2, module2) {
6465
6505
  "use strict";
6466
6506
  var EventEmitter = require("events");
6467
6507
  var https = require("https");
@@ -6630,6 +6670,10 @@ var require_websocket = __commonJS({
6630
6670
  * multiple times in the same tick
6631
6671
  * @param {Function} [options.generateMask] The function used to generate the
6632
6672
  * masking key
6673
+ * @param {Number} [options.maxBufferedChunks=0] The maximum number of
6674
+ * buffered data chunks
6675
+ * @param {Number} [options.maxFragments=0] The maximum number of message
6676
+ * fragments
6633
6677
  * @param {Number} [options.maxPayload=0] The maximum allowed message size
6634
6678
  * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
6635
6679
  * not to skip UTF-8 validation for text and close messages
@@ -6641,6 +6685,8 @@ var require_websocket = __commonJS({
6641
6685
  binaryType: this.binaryType,
6642
6686
  extensions: this._extensions,
6643
6687
  isServer: this._isServer,
6688
+ maxBufferedChunks: options.maxBufferedChunks,
6689
+ maxFragments: options.maxFragments,
6644
6690
  maxPayload: options.maxPayload,
6645
6691
  skipUTF8Validation: options.skipUTF8Validation
6646
6692
  });
@@ -6940,6 +6986,8 @@ var require_websocket = __commonJS({
6940
6986
  autoPong: true,
6941
6987
  closeTimeout: CLOSE_TIMEOUT,
6942
6988
  protocolVersion: protocolVersions[1],
6989
+ maxBufferedChunks: 1024 * 1024,
6990
+ maxFragments: 128 * 1024,
6943
6991
  maxPayload: 100 * 1024 * 1024,
6944
6992
  skipUTF8Validation: false,
6945
6993
  perMessageDeflate: true,
@@ -7182,6 +7230,8 @@ var require_websocket = __commonJS({
7182
7230
  websocket.setSocket(socket, head, {
7183
7231
  allowSynchronousEvents: opts.allowSynchronousEvents,
7184
7232
  generateMask: opts.generateMask,
7233
+ maxBufferedChunks: opts.maxBufferedChunks,
7234
+ maxFragments: opts.maxFragments,
7185
7235
  maxPayload: opts.maxPayload,
7186
7236
  skipUTF8Validation: opts.skipUTF8Validation
7187
7237
  });
@@ -7345,9 +7395,9 @@ var require_websocket = __commonJS({
7345
7395
  }
7346
7396
  });
7347
7397
 
7348
- // ../../node_modules/engine.io-client/node_modules/ws/lib/stream.js
7398
+ // ../../node_modules/ws/lib/stream.js
7349
7399
  var require_stream = __commonJS({
7350
- "../../node_modules/engine.io-client/node_modules/ws/lib/stream.js"(exports2, module2) {
7400
+ "../../node_modules/ws/lib/stream.js"(exports2, module2) {
7351
7401
  "use strict";
7352
7402
  var WebSocket2 = require_websocket();
7353
7403
  var { Duplex } = require("stream");
@@ -7443,9 +7493,9 @@ var require_stream = __commonJS({
7443
7493
  }
7444
7494
  });
7445
7495
 
7446
- // ../../node_modules/engine.io-client/node_modules/ws/lib/subprotocol.js
7496
+ // ../../node_modules/ws/lib/subprotocol.js
7447
7497
  var require_subprotocol = __commonJS({
7448
- "../../node_modules/engine.io-client/node_modules/ws/lib/subprotocol.js"(exports2, module2) {
7498
+ "../../node_modules/ws/lib/subprotocol.js"(exports2, module2) {
7449
7499
  "use strict";
7450
7500
  var { tokenChars } = require_validation();
7451
7501
  function parse3(header) {
@@ -7488,9 +7538,9 @@ var require_subprotocol = __commonJS({
7488
7538
  }
7489
7539
  });
7490
7540
 
7491
- // ../../node_modules/engine.io-client/node_modules/ws/lib/websocket-server.js
7541
+ // ../../node_modules/ws/lib/websocket-server.js
7492
7542
  var require_websocket_server = __commonJS({
7493
- "../../node_modules/engine.io-client/node_modules/ws/lib/websocket-server.js"(exports2, module2) {
7543
+ "../../node_modules/ws/lib/websocket-server.js"(exports2, module2) {
7494
7544
  "use strict";
7495
7545
  var EventEmitter = require("events");
7496
7546
  var http = require("http");
@@ -7524,6 +7574,10 @@ var require_websocket_server = __commonJS({
7524
7574
  * called
7525
7575
  * @param {Function} [options.handleProtocols] A hook to handle protocols
7526
7576
  * @param {String} [options.host] The hostname where to bind the server
7577
+ * @param {Number} [options.maxBufferedChunks=1048576] The maximum number of
7578
+ * buffered data chunks
7579
+ * @param {Number} [options.maxFragments=131072] The maximum number of message
7580
+ * fragments
7527
7581
  * @param {Number} [options.maxPayload=104857600] The maximum allowed message
7528
7582
  * size
7529
7583
  * @param {Boolean} [options.noServer=false] Enable no server mode
@@ -7545,6 +7599,8 @@ var require_websocket_server = __commonJS({
7545
7599
  options = {
7546
7600
  allowSynchronousEvents: true,
7547
7601
  autoPong: true,
7602
+ maxBufferedChunks: 1024 * 1024,
7603
+ maxFragments: 128 * 1024,
7548
7604
  maxPayload: 100 * 1024 * 1024,
7549
7605
  skipUTF8Validation: false,
7550
7606
  perMessageDeflate: false,
@@ -7824,6 +7880,8 @@ var require_websocket_server = __commonJS({
7824
7880
  socket.removeListener("error", socketOnError);
7825
7881
  ws.setSocket(socket, head, {
7826
7882
  allowSynchronousEvents: this.options.allowSynchronousEvents,
7883
+ maxBufferedChunks: this.options.maxBufferedChunks,
7884
+ maxFragments: this.options.maxFragments,
7827
7885
  maxPayload: this.options.maxPayload,
7828
7886
  skipUTF8Validation: this.options.skipUTF8Validation
7829
7887
  });
@@ -7884,7 +7942,7 @@ var require_websocket_server = __commonJS({
7884
7942
  // src/cli.ts
7885
7943
  var import_node_dns = __toESM(require("node:dns"));
7886
7944
 
7887
- // ../../node_modules/commander/esm.mjs
7945
+ // node_modules/commander/esm.mjs
7888
7946
  var import_index = __toESM(require_commander(), 1);
7889
7947
  var {
7890
7948
  program,
@@ -7979,8 +8037,144 @@ function configExists() {
7979
8037
  return import_fs.default.existsSync(CONFIG_FILE);
7980
8038
  }
7981
8039
 
7982
- // ../../shared/all/helpers/board-agent-core/claude.ts
8040
+ // src/setup.ts
7983
8041
  var import_child_process2 = require("child_process");
8042
+ var import_crypto = __toESM(require("crypto"));
8043
+ var import_fs2 = __toESM(require("fs"));
8044
+ var import_path2 = __toESM(require("path"));
8045
+ var DOCKERFILE_LABEL = "factiii.dockerfile-sha";
8046
+ function run(bin, args, opts) {
8047
+ try {
8048
+ return (0, import_child_process2.execFileSync)(bin, args, {
8049
+ encoding: "utf-8",
8050
+ stdio: opts?.silent ? "pipe" : "inherit",
8051
+ // Avoids a cmd.exe window flash on Windows during silent checks.
8052
+ windowsHide: true
8053
+ });
8054
+ } catch {
8055
+ return "";
8056
+ }
8057
+ }
8058
+ function check(cmd) {
8059
+ const lookup2 = process.platform === "win32" ? "where" : "which";
8060
+ try {
8061
+ (0, import_child_process2.execFileSync)(lookup2, [cmd], { stdio: "pipe", windowsHide: true });
8062
+ return true;
8063
+ } catch {
8064
+ return false;
8065
+ }
8066
+ }
8067
+ async function setup(serverUrl) {
8068
+ console.log("\n=== Factiii Runner \u2014 Setup ===\n");
8069
+ console.log("[1/3] Checking Docker...");
8070
+ if (!check("docker")) {
8071
+ console.error(
8072
+ " Docker is not installed. Install it from https://docs.docker.com/get-docker/ and re-run setup."
8073
+ );
8074
+ process.exit(1);
8075
+ }
8076
+ if (!run("docker", ["info"], { silent: true })) {
8077
+ console.error(
8078
+ " Docker is installed but the daemon is not running. Start Docker and re-run setup."
8079
+ );
8080
+ process.exit(1);
8081
+ }
8082
+ console.log(" Docker is ready.");
8083
+ console.log("\n[2/3] Building factiii-claude Docker image...");
8084
+ const dockerfilePath = findDockerfile();
8085
+ if (!dockerfilePath) {
8086
+ console.error(
8087
+ " Dockerfile.claude not found. Reinstall the package and re-run setup."
8088
+ );
8089
+ process.exit(1);
8090
+ }
8091
+ const wantedHash = import_crypto.default.createHash("sha256").update(import_fs2.default.readFileSync(dockerfilePath)).digest("hex");
8092
+ const existingHash = run(
8093
+ "docker",
8094
+ [
8095
+ "image",
8096
+ "inspect",
8097
+ "factiii-claude",
8098
+ "--format",
8099
+ `{{ index .Config.Labels "${DOCKERFILE_LABEL}" }}`
8100
+ ],
8101
+ { silent: true }
8102
+ ).trim();
8103
+ if (existingHash === wantedHash) {
8104
+ console.log(" Image is up to date.");
8105
+ } else {
8106
+ console.log(
8107
+ existingHash ? ` Dockerfile changed since last build, rebuilding from ${dockerfilePath}...` : ` Building from ${dockerfilePath}...`
8108
+ );
8109
+ run("docker", [
8110
+ "build",
8111
+ "-t",
8112
+ "factiii-claude",
8113
+ "-f",
8114
+ dockerfilePath,
8115
+ "--label",
8116
+ `${DOCKERFILE_LABEL}=${wantedHash}`,
8117
+ "."
8118
+ ]);
8119
+ console.log(" Image built.");
8120
+ }
8121
+ run("docker", ["volume", "create", "factiii-card-cache"], { silent: true });
8122
+ console.log("\n[3/3] Pairing this runner with a Factiii account...");
8123
+ const { token, runnerName } = await pairWithBrowser(serverUrl);
8124
+ writeRunnerConfig({ serverUrl, authToken: token, runnerName });
8125
+ console.log(" Paired and config saved.");
8126
+ console.log("\n=== Setup complete ===\n");
8127
+ console.log("Start the runner with: factiii-runner start\n");
8128
+ }
8129
+ function findDockerfile() {
8130
+ const p = import_path2.default.join(__dirname, "..", "Dockerfile.claude");
8131
+ return import_fs2.default.existsSync(p) ? p : null;
8132
+ }
8133
+
8134
+ // src/selfUpdate.ts
8135
+ var import_node_child_process = require("node:child_process");
8136
+ var import_node_fs = __toESM(require("node:fs"));
8137
+ var import_node_path = __toESM(require("node:path"));
8138
+ var PACKAGE_NAME = "@factiii/runner";
8139
+ var RESTART_FOR_UPDATE = 75;
8140
+ var WORKER_ENV = "FACTIII_RUNNER_WORKER";
8141
+ function markWorkerEnv(env) {
8142
+ return { ...env, [WORKER_ENV]: "1" };
8143
+ }
8144
+ function isWorker() {
8145
+ return process.env[WORKER_ENV] === "1";
8146
+ }
8147
+ function currentVersion() {
8148
+ try {
8149
+ const pkg = JSON.parse(
8150
+ import_node_fs.default.readFileSync(import_node_path.default.join(__dirname, "..", "package.json"), "utf-8")
8151
+ );
8152
+ return pkg.version ?? "0.0.0";
8153
+ } catch {
8154
+ return "0.0.0";
8155
+ }
8156
+ }
8157
+ function installLatest() {
8158
+ console.log(`[update] installing ${PACKAGE_NAME}@latest...`);
8159
+ const res = (0, import_node_child_process.spawnSync)("npm", ["install", "-g", `${PACKAGE_NAME}@latest`], {
8160
+ stdio: "inherit",
8161
+ // On Windows npm is npm.cmd, which spawnSync can't exec without a shell.
8162
+ shell: process.platform === "win32"
8163
+ });
8164
+ if (res.status !== 0) {
8165
+ console.error(
8166
+ `[update] npm install failed (exit ${res.status ?? "signal"}). Staying on ${currentVersion()}.`
8167
+ );
8168
+ return false;
8169
+ }
8170
+ return true;
8171
+ }
8172
+
8173
+ // src/supervisor.ts
8174
+ var import_node_child_process2 = require("node:child_process");
8175
+
8176
+ // ../../shared/all/helpers/board-agent-core/claude.ts
8177
+ var import_child_process3 = require("child_process");
7984
8178
 
7985
8179
  // ../../shared/all/helpers/board-agent-core/claude-stream.ts
7986
8180
  function truncate(s, max) {
@@ -8210,6 +8404,12 @@ function pipeClaudeStream(proc, callbacks) {
8210
8404
  for (const line of lines) {
8211
8405
  const trimmed = line.trim();
8212
8406
  if (!trimmed) continue;
8407
+ if (trimmed[0] !== "{" && trimmed[0] !== "[") {
8408
+ state.isError = true;
8409
+ state.fullText = state.fullText ? `${state.fullText}
8410
+ ${trimmed}` : trimmed;
8411
+ continue;
8412
+ }
8213
8413
  try {
8214
8414
  const raw = JSON.parse(trimmed);
8215
8415
  if (processStreamEvent(raw, state, callbacks)) continue;
@@ -8450,7 +8650,7 @@ function spawnClaude(opts) {
8450
8650
  if (opts.resumeSessionId) {
8451
8651
  args.push("--resume", opts.resumeSessionId);
8452
8652
  }
8453
- const proc = (0, import_child_process2.spawn)("docker", args);
8653
+ const proc = (0, import_child_process3.spawn)("docker", args);
8454
8654
  const result = new Promise(
8455
8655
  (resolve, reject) => {
8456
8656
  let errorOutput = "";
@@ -8482,7 +8682,7 @@ function spawnClaude(opts) {
8482
8682
  }
8483
8683
  function generateCommitMessage(containerName, diff) {
8484
8684
  return new Promise((resolve) => {
8485
- const proc = (0, import_child_process2.spawn)("docker", [
8685
+ const proc = (0, import_child_process3.spawn)("docker", [
8486
8686
  "exec",
8487
8687
  "-i",
8488
8688
  "--user",
@@ -8511,16 +8711,16 @@ ${diff.slice(0, 8e3)}`);
8511
8711
  }
8512
8712
 
8513
8713
  // ../../shared/all/helpers/board-agent-core/docker.ts
8514
- var import_child_process3 = require("child_process");
8515
- var import_crypto = __toESM(require("crypto"));
8714
+ var import_child_process4 = require("child_process");
8715
+ var import_crypto2 = __toESM(require("crypto"));
8516
8716
  var import_promises = __toESM(require("fs/promises"));
8517
8717
  var import_net = __toESM(require("net"));
8518
8718
  var import_util = require("util");
8519
- var execFileAsync = (0, import_util.promisify)(import_child_process3.execFile);
8520
- var DOCKERFILE_LABEL = "factiii.dockerfile-sha";
8719
+ var execFileAsync = (0, import_util.promisify)(import_child_process4.execFile);
8720
+ var DOCKERFILE_LABEL2 = "factiii.dockerfile-sha";
8521
8721
  async function hashDockerfile(dockerfilePath) {
8522
8722
  const buf = await import_promises.default.readFile(dockerfilePath);
8523
- return import_crypto.default.createHash("sha256").update(buf).digest("hex");
8723
+ return import_crypto2.default.createHash("sha256").update(buf).digest("hex");
8524
8724
  }
8525
8725
  async function getFreePort() {
8526
8726
  return new Promise((resolve, reject) => {
@@ -8559,7 +8759,7 @@ async function ensureDockerImage(dockerfilePath, onLog) {
8559
8759
  "inspect",
8560
8760
  "factiii-claude",
8561
8761
  "--format",
8562
- `{{ index .Config.Labels "${DOCKERFILE_LABEL}" }}`
8762
+ `{{ index .Config.Labels "${DOCKERFILE_LABEL2}" }}`
8563
8763
  ],
8564
8764
  {}
8565
8765
  );
@@ -8585,7 +8785,7 @@ async function resetNixVolume() {
8585
8785
  }
8586
8786
  function buildImageStreaming(dockerfilePath, wantedHash, onLog) {
8587
8787
  return new Promise((resolve, reject) => {
8588
- const child = (0, import_child_process3.spawn)(
8788
+ const child = (0, import_child_process4.spawn)(
8589
8789
  "docker",
8590
8790
  [
8591
8791
  "build",
@@ -8595,7 +8795,7 @@ function buildImageStreaming(dockerfilePath, wantedHash, onLog) {
8595
8795
  "-f",
8596
8796
  dockerfilePath,
8597
8797
  "--label",
8598
- `${DOCKERFILE_LABEL}=${wantedHash}`,
8798
+ `${DOCKERFILE_LABEL2}=${wantedHash}`,
8599
8799
  "."
8600
8800
  ],
8601
8801
  { env: { ...process.env, DOCKER_BUILDKIT: "1" } }
@@ -8654,6 +8854,44 @@ async function killContainer(containerName) {
8654
8854
  } catch {
8655
8855
  }
8656
8856
  }
8857
+ async function listContainersByPrefix(namePrefix) {
8858
+ try {
8859
+ const { stdout } = await execFileAsync(
8860
+ "docker",
8861
+ [
8862
+ "ps",
8863
+ "-a",
8864
+ "--no-trunc",
8865
+ "--filter",
8866
+ `name=${namePrefix}`,
8867
+ "--format",
8868
+ "{{.Names}} {{.State}} {{.Status}}"
8869
+ ],
8870
+ { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 }
8871
+ );
8872
+ const rows = [];
8873
+ for (const line of stdout.split("\n")) {
8874
+ if (!line.trim()) continue;
8875
+ const [name, state, ...statusParts] = line.split(" ");
8876
+ if (!name || !name.startsWith(namePrefix)) continue;
8877
+ rows.push({
8878
+ name,
8879
+ running: state === "running",
8880
+ status: statusParts.join(" ").trim() || state || ""
8881
+ });
8882
+ }
8883
+ return rows;
8884
+ } catch {
8885
+ return [];
8886
+ }
8887
+ }
8888
+ async function renameContainer(oldName, newName) {
8889
+ if (oldName === newName) return;
8890
+ try {
8891
+ await execFileAsync("docker", ["rename", oldName, newName], {});
8892
+ } catch {
8893
+ }
8894
+ }
8657
8895
  async function spawnContainer(containerName, claudeToken, opts) {
8658
8896
  const NUM_PORTS = 6;
8659
8897
  const ports = [];
@@ -8728,7 +8966,7 @@ async function execInContainer(containerName, cmd) {
8728
8966
  return stdout;
8729
8967
  }
8730
8968
  function streamExecInContainer(containerName, cmd, onLine) {
8731
- const child = (0, import_child_process3.spawn)("docker", [
8969
+ const child = (0, import_child_process4.spawn)("docker", [
8732
8970
  "exec",
8733
8971
  "--user",
8734
8972
  "claude",
@@ -8811,9 +9049,9 @@ async function getContainerPort(containerName, containerPort) {
8811
9049
  }
8812
9050
 
8813
9051
  // ../../shared/all/helpers/board-agent-core/git.ts
8814
- var import_child_process4 = require("child_process");
9052
+ var import_child_process5 = require("child_process");
8815
9053
  var import_util2 = require("util");
8816
- var execFileAsync2 = (0, import_util2.promisify)(import_child_process4.execFile);
9054
+ var execFileAsync2 = (0, import_util2.promisify)(import_child_process5.execFile);
8817
9055
  function buildCloneUrl(repoUrl, githubToken) {
8818
9056
  const url2 = new URL(repoUrl);
8819
9057
  url2.username = githubToken;
@@ -9254,6 +9492,27 @@ function withTimeout(work, ms, label) {
9254
9492
  });
9255
9493
  return Promise.race([work.finally(() => clearTimeout(timer)), timeout]);
9256
9494
  }
9495
+ function computeInstancePrefix(instanceId) {
9496
+ const raw = (instanceId || "shared").toLowerCase();
9497
+ return raw.replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "shared";
9498
+ }
9499
+ function parseFactiiiContainerName(name, instancePrefix) {
9500
+ const head = `factiii-${instancePrefix}-`;
9501
+ if (!name.startsWith(head)) return null;
9502
+ const rest = name.slice(head.length);
9503
+ const withId = rest.match(/^(card|util)-(\d+)-(.+)$/);
9504
+ if (withId) {
9505
+ const kind = withId[1];
9506
+ const spaceId = Number(withId[2]);
9507
+ return kind === "card" ? { kind, spaceId, taskId: withId[3] } : { kind, spaceId, cacheKey: withId[3] };
9508
+ }
9509
+ const legacy = rest.match(/^(card|util)-(.+)$/);
9510
+ if (legacy) {
9511
+ const kind = legacy[1];
9512
+ return kind === "card" ? { kind, spaceId: null, taskId: legacy[2] } : { kind, spaceId: null, cacheKey: legacy[2] };
9513
+ }
9514
+ return null;
9515
+ }
9257
9516
  var ContainerCore = class {
9258
9517
  constructor(options) {
9259
9518
  // Utility container state.
@@ -9261,12 +9520,17 @@ var ContainerCore = class {
9261
9520
  this.lastConfigKey = "";
9262
9521
  this.setupPromise = null;
9263
9522
  this.spaceSlug = options.spaceSlug;
9523
+ this.spaceId = options.spaceId ?? null;
9264
9524
  this.emitter = options.emitter;
9265
9525
  this.configProvider = options.configProvider;
9266
9526
  this.dockerfilePath = options.dockerfilePath;
9267
9527
  this.runnerToken = options.runnerToken;
9268
- const raw = (options.instanceId || "shared").toLowerCase();
9269
- this.instancePrefix = raw.replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "shared";
9528
+ this.instancePrefix = computeInstancePrefix(options.instanceId);
9529
+ }
9530
+ /** The space segment used in container names: the stable id when we have it,
9531
+ * else the slug (legacy / id-less callers). */
9532
+ get spaceSegment() {
9533
+ return this.spaceId != null ? String(this.spaceId) : this.spaceSlug;
9270
9534
  }
9271
9535
  readConfig() {
9272
9536
  return this.configProvider.readConfig(this.spaceSlug);
@@ -9366,7 +9630,11 @@ var ContainerCore = class {
9366
9630
  }
9367
9631
  const configKey = onedrive ? `onedrive|${this.spaceSlug}|${config.claudeToken}` : `${config.repoUrl}|${config.claudeToken}|${config.githubToken}`;
9368
9632
  const cacheKey = onedrive ? `onedrive-${this.spaceSlug}`.replace(/[^a-zA-Z0-9._-]/g, "_") : repoCacheKey(config.repoUrl);
9369
- const name = `factiii-${this.instancePrefix}-util-${this.spaceSlug}-${cacheKey}`;
9633
+ const name = `factiii-${this.instancePrefix}-util-${this.spaceSegment}-${cacheKey}`;
9634
+ if (this.spaceId != null) {
9635
+ const legacyName = `factiii-${this.instancePrefix}-util-${this.spaceSlug}-${cacheKey}`;
9636
+ await renameContainer(legacyName, name);
9637
+ }
9370
9638
  if (this.containerName && this.lastConfigKey !== configKey) {
9371
9639
  await killContainer(this.containerName);
9372
9640
  this.containerName = null;
@@ -9597,9 +9865,9 @@ function createEventSink(send, spaceSlug, notify) {
9597
9865
  }
9598
9866
 
9599
9867
  // ../../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"));
9868
+ var import_child_process6 = require("child_process");
9869
+ var import_fs3 = require("fs");
9870
+ var import_path3 = __toESM(require("path"));
9603
9871
  var import_util3 = require("util");
9604
9872
 
9605
9873
  // ../../shared/all/helpers/card-ai/scripts.ts
@@ -9653,15 +9921,15 @@ fi
9653
9921
  `;
9654
9922
 
9655
9923
  // ../../shared/all/helpers/card-ai/frpc.ts
9656
- var execFileAsync3 = (0, import_util3.promisify)(import_child_process5.execFile);
9924
+ var execFileAsync3 = (0, import_util3.promisify)(import_child_process6.execFile);
9657
9925
  var FRP_SERVER_ADDR = "dev.factiii.com";
9658
9926
  var FRP_SERVER_PORT = 7443;
9659
9927
  var FRP_DOMAIN_SUFFIX = "dev.factiii.com";
9660
- async function writeFileAsRoot(containerName, path7, contents) {
9928
+ async function writeFileAsRoot(containerName, path8, contents) {
9661
9929
  const b64 = Buffer.from(contents, "utf-8").toString("base64");
9662
9930
  await execInContainerAsRoot(
9663
9931
  containerName,
9664
- `echo '${b64}' | base64 -d > '${path7}'`
9932
+ `echo '${b64}' | base64 -d > '${path8}'`
9665
9933
  );
9666
9934
  }
9667
9935
  async function installFrpcBinary(containerName) {
@@ -9674,8 +9942,8 @@ async function installFrpcBinary(containerName) {
9674
9942
  if (!binaryName) {
9675
9943
  throw new Error(`Unsupported container arch for frpc: ${arch}`);
9676
9944
  }
9677
- const src = import_path2.default.join(__dirname, "..", "vendor", binaryName);
9678
- if (!(0, import_fs2.existsSync)(src)) {
9945
+ const src = import_path3.default.join(__dirname, "..", "vendor", binaryName);
9946
+ if (!(0, import_fs3.existsSync)(src)) {
9679
9947
  throw new Error(
9680
9948
  `Vendored frpc missing (${src}). Run apps/runner/scripts/fetch-frpc.sh.`
9681
9949
  );
@@ -9766,6 +10034,7 @@ var ClaudeModeEngine = class _ClaudeModeEngine {
9766
10034
  );
9767
10035
  }
9768
10036
  const containerName = this.core.containerName;
10037
+ await execInContainer(containerName, "mkdir -p /home/claude/workspace");
9769
10038
  this.board.messages.push({ role: "user", content: params.question });
9770
10039
  let prompt2;
9771
10040
  if (this.board.sessionId) {
@@ -10418,10 +10687,10 @@ var TerminalModeEngine = class {
10418
10687
  }
10419
10688
  async bareFsList({
10420
10689
  postId,
10421
- path: path7
10690
+ path: path8
10422
10691
  }) {
10423
10692
  const container = this.getBareContainer(postId);
10424
- const abs = this.resolveBarePath(path7);
10693
+ const abs = this.resolveBarePath(path8);
10425
10694
  const relRoot = abs.slice(BARE_REPO_ROOT.length) || "/";
10426
10695
  const out = await execInContainer(
10427
10696
  container,
@@ -10471,21 +10740,21 @@ var TerminalModeEngine = class {
10471
10740
  }
10472
10741
  async bareFsRead({
10473
10742
  postId,
10474
- path: path7
10743
+ path: path8
10475
10744
  }) {
10476
10745
  const container = this.getBareContainer(postId);
10477
- const abs = this.resolveBarePath(path7);
10746
+ const abs = this.resolveBarePath(path8);
10478
10747
  const b64 = (await execInContainer(container, `base64 -w 0 ${shEscape(abs)}`)).trim();
10479
10748
  return { content: b64, encoding: "base64" };
10480
10749
  }
10481
10750
  async bareFsWrite({
10482
10751
  postId,
10483
- path: path7,
10752
+ path: path8,
10484
10753
  content,
10485
10754
  encoding
10486
10755
  }) {
10487
10756
  const container = this.getBareContainer(postId);
10488
- const abs = this.resolveBarePath(path7);
10757
+ const abs = this.resolveBarePath(path8);
10489
10758
  const b64 = encoding === "base64" ? content : Buffer.from(content, "utf-8").toString("base64");
10490
10759
  if (!/^[A-Za-z0-9+/=\n\r]*$/.test(b64)) {
10491
10760
  throw new Error("Invalid base64 payload.");
@@ -10495,7 +10764,7 @@ var TerminalModeEngine = class {
10495
10764
  `mkdir -p ${shEscape(posixDirname(abs))} && printf %s ${shEscape(b64)} | base64 -d > ${shEscape(abs)}`
10496
10765
  );
10497
10766
  const session = this.sessions.get(String(postId));
10498
- if (session) delete session.buffers[path7];
10767
+ if (session) delete session.buffers[path8];
10499
10768
  }
10500
10769
  bareEditorState({
10501
10770
  postId: rawPostId
@@ -10512,38 +10781,38 @@ var TerminalModeEngine = class {
10512
10781
  }
10513
10782
  bareOpenFile({
10514
10783
  postId: rawPostId,
10515
- path: path7
10784
+ path: path8
10516
10785
  }) {
10517
10786
  const session = this.sessions.get(String(rawPostId));
10518
10787
  if (!session || session.mode !== "bare") return;
10519
- if (!session.openedFiles.includes(path7)) {
10520
- session.openedFiles.push(path7);
10788
+ if (!session.openedFiles.includes(path8)) {
10789
+ session.openedFiles.push(path8);
10521
10790
  }
10522
10791
  }
10523
10792
  bareCloseFile({
10524
10793
  postId: rawPostId,
10525
- path: path7
10794
+ path: path8
10526
10795
  }) {
10527
10796
  const session = this.sessions.get(String(rawPostId));
10528
10797
  if (!session || session.mode !== "bare") return;
10529
- session.openedFiles = session.openedFiles.filter((p) => p !== path7);
10530
- delete session.buffers[path7];
10798
+ session.openedFiles = session.openedFiles.filter((p) => p !== path8);
10799
+ delete session.buffers[path8];
10531
10800
  }
10532
10801
  bareBufferSet({
10533
10802
  postId: rawPostId,
10534
- path: path7,
10803
+ path: path8,
10535
10804
  content
10536
10805
  }) {
10537
10806
  const session = this.sessions.get(String(rawPostId));
10538
10807
  if (!session || session.mode !== "bare") return;
10539
- session.buffers[path7] = content;
10808
+ session.buffers[path8] = content;
10540
10809
  }
10541
10810
  async bareFsDelete({
10542
10811
  postId,
10543
- path: path7
10812
+ path: path8
10544
10813
  }) {
10545
10814
  const container = this.getBareContainer(postId);
10546
- const abs = this.resolveBarePath(path7);
10815
+ const abs = this.resolveBarePath(path8);
10547
10816
  if (abs === BARE_REPO_ROOT) {
10548
10817
  throw new Error("Refusing to delete the repo root.");
10549
10818
  }
@@ -10551,10 +10820,10 @@ var TerminalModeEngine = class {
10551
10820
  }
10552
10821
  async bareFsMkdir({
10553
10822
  postId,
10554
- path: path7
10823
+ path: path8
10555
10824
  }) {
10556
10825
  const container = this.getBareContainer(postId);
10557
- const abs = this.resolveBarePath(path7);
10826
+ const abs = this.resolveBarePath(path8);
10558
10827
  await execInContainer(container, `mkdir -p ${shEscape(abs)}`);
10559
10828
  }
10560
10829
  async bareReview({
@@ -10967,7 +11236,9 @@ var BoardAgentEngine = class {
10967
11236
  targetBranch: targetBranch || "",
10968
11237
  effort: effort || "high",
10969
11238
  todos: [],
10970
- containerName: `factiii-${this.core.instancePrefix}-card-${taskId}`
11239
+ // Name by stable spaceId when we have it; fall back to the id-less legacy
11240
+ // form for old callers that only send the slug.
11241
+ containerName: this.core.spaceId != null ? `factiii-${this.core.instancePrefix}-card-${this.core.spaceId}-${taskId}` : `factiii-${this.core.instancePrefix}-card-${taskId}`
10971
11242
  };
10972
11243
  if (mode === "bare") {
10973
11244
  this.terminal.start(base, {
@@ -10996,6 +11267,57 @@ var BoardAgentEngine = class {
10996
11267
  cardSessions() {
10997
11268
  return { ...this.terminal.listSummaries(), ...this.claude.listSummaries() };
10998
11269
  }
11270
+ /** The stable board id this engine runs, if the caller provided one. */
11271
+ get spaceId() {
11272
+ return this.core.spaceId;
11273
+ }
11274
+ /**
11275
+ * Every factiii container/VM on this host (running + stopped) for this
11276
+ * runner's instance prefix - card and util, across ALL boards, since the
11277
+ * prefix is shared by every engine on the host. Returns them decoded; the
11278
+ * daemon scope-filters before handing them to a remote member. Electron
11279
+ * (single owner) gets the raw list straight through.
11280
+ */
11281
+ async listContainers() {
11282
+ const rows = await listContainersByPrefix(
11283
+ `factiii-${this.core.instancePrefix}-`
11284
+ );
11285
+ const out = [];
11286
+ for (const row of rows) {
11287
+ const parsed = parseFactiiiContainerName(
11288
+ row.name,
11289
+ this.core.instancePrefix
11290
+ );
11291
+ if (!parsed) continue;
11292
+ out.push({
11293
+ containerName: row.name,
11294
+ kind: parsed.kind,
11295
+ spaceId: parsed.spaceId,
11296
+ // This engine only knows its own board's slug; the daemon fills in
11297
+ // slugs for other boards (cross-board) when scope-filtering.
11298
+ spaceSlug: parsed.spaceId != null && parsed.spaceId === this.core.spaceId ? this.core.spaceSlug : void 0,
11299
+ taskId: parsed.taskId,
11300
+ cacheKey: parsed.cacheKey,
11301
+ running: row.running,
11302
+ status: row.status
11303
+ });
11304
+ }
11305
+ return out;
11306
+ }
11307
+ /**
11308
+ * Force-remove a container/VM by name. Guards the name against this runner's
11309
+ * prefix so a peer can't `docker rm` an arbitrary container on the host. The
11310
+ * daemon adds the per-board scope check on top for remote members.
11311
+ */
11312
+ async removeContainer({
11313
+ containerName
11314
+ }) {
11315
+ if (!containerName.startsWith(`factiii-${this.core.instancePrefix}-`)) {
11316
+ throw new Error("Refusing to remove a container outside this runner.");
11317
+ }
11318
+ await killContainer(containerName);
11319
+ return { removed: true };
11320
+ }
10999
11321
  cardSession({ postId }) {
11000
11322
  return this.claude.sessionWithLogs(String(postId)) ?? this.terminal.sessionWithLogs(String(postId));
11001
11323
  }
@@ -11240,6 +11562,12 @@ var BoardAgentEngine = class {
11240
11562
  return this.bareListExposed(
11241
11563
  payload
11242
11564
  );
11565
+ case "listContainers":
11566
+ return await this.listContainers();
11567
+ case "removeContainer":
11568
+ return await this.removeContainer(
11569
+ payload
11570
+ );
11243
11571
  default:
11244
11572
  throw new Error(`Unknown board-agent action: ${String(action)}`);
11245
11573
  }
@@ -11296,7 +11624,7 @@ function makeChunkReassembler() {
11296
11624
 
11297
11625
  // src/daemon.ts
11298
11626
  var import_node_datachannel = require("node-datachannel");
11299
- var import_path5 = __toESM(require("path"));
11627
+ var import_path6 = __toESM(require("path"));
11300
11628
 
11301
11629
  // ../../node_modules/engine.io-client/build/esm-debug/transports/polling-xhr.node.js
11302
11630
  var XMLHttpRequestModule = __toESM(require_XMLHttpRequest(), 1);
@@ -12229,7 +12557,7 @@ var XHR = class extends BaseXHR {
12229
12557
  }
12230
12558
  };
12231
12559
 
12232
- // ../../node_modules/engine.io-client/node_modules/ws/wrapper.mjs
12560
+ // ../../node_modules/ws/wrapper.mjs
12233
12561
  var import_stream = __toESM(require_stream(), 1);
12234
12562
  var import_extension = __toESM(require_extension(), 1);
12235
12563
  var import_permessage_deflate = __toESM(require_permessage_deflate(), 1);
@@ -12476,12 +12804,12 @@ function parse2(str) {
12476
12804
  uri.queryKey = queryKey(uri, uri["query"]);
12477
12805
  return uri;
12478
12806
  }
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) {
12807
+ function pathNames(obj, path8) {
12808
+ const regx = /\/{2,9}/g, names = path8.replace(regx, "/").split("/");
12809
+ if (path8.slice(0, 1) == "/" || path8.length === 0) {
12482
12810
  names.splice(0, 1);
12483
12811
  }
12484
- if (path7.slice(-1) == "/") {
12812
+ if (path8.slice(-1) == "/") {
12485
12813
  names.splice(names.length - 1, 1);
12486
12814
  }
12487
12815
  return names;
@@ -13085,11 +13413,12 @@ var SocketWithUpgrade = class extends SocketWithoutUpgrade {
13085
13413
  };
13086
13414
  var Socket = class extends SocketWithUpgrade {
13087
13415
  constructor(uri, opts = {}) {
13088
- const o = typeof uri === "object" ? uri : opts;
13416
+ const isOptionsOnly = typeof uri === "object";
13417
+ const o = isOptionsOnly ? { ...uri } : { ...opts };
13089
13418
  if (!o.transports || o.transports && typeof o.transports[0] === "string") {
13090
13419
  o.transports = (o.transports || ["polling", "websocket", "webtransport"]).map((transportName) => transports[transportName]).filter((t) => !!t);
13091
13420
  }
13092
- super(uri, o);
13421
+ super(isOptionsOnly ? o : uri, o);
13093
13422
  }
13094
13423
  };
13095
13424
 
@@ -13099,7 +13428,7 @@ var protocol2 = Socket.protocol;
13099
13428
  // ../../node_modules/socket.io-client/build/esm-debug/url.js
13100
13429
  var import_debug7 = __toESM(require_src(), 1);
13101
13430
  var debug7 = (0, import_debug7.default)("socket.io-client:url");
13102
- function url(uri, path7 = "", loc) {
13431
+ function url(uri, path8 = "", loc) {
13103
13432
  let obj = uri;
13104
13433
  loc = loc || typeof location !== "undefined" && location;
13105
13434
  if (null == uri)
@@ -13133,7 +13462,7 @@ function url(uri, path7 = "", loc) {
13133
13462
  obj.path = obj.path || "/";
13134
13463
  const ipv6 = obj.host.indexOf(":") !== -1;
13135
13464
  const host = ipv6 ? "[" + obj.host + "]" : obj.host;
13136
- obj.id = obj.protocol + "://" + host + ":" + obj.port + path7;
13465
+ obj.id = obj.protocol + "://" + host + ":" + obj.port + path8;
13137
13466
  obj.href = obj.protocol + "://" + host + (loc && loc.port === obj.port ? "" : ":" + obj.port);
13138
13467
  return obj;
13139
13468
  }
@@ -14767,8 +15096,8 @@ function lookup(uri, opts) {
14767
15096
  const parsed = url(uri, opts.path || "/socket.io");
14768
15097
  const source = parsed.source;
14769
15098
  const id = parsed.id;
14770
- const path7 = parsed.path;
14771
- const sameNamespace = cache[id] && path7 in cache[id]["nsps"];
15099
+ const path8 = parsed.path;
15100
+ const sameNamespace = cache[id] && path8 in cache[id]["nsps"];
14772
15101
  const newConnection = opts.forceNew || opts["force new connection"] || false === opts.multiplex || sameNamespace;
14773
15102
  let io;
14774
15103
  if (newConnection) {
@@ -14794,22 +15123,22 @@ Object.assign(lookup, {
14794
15123
  });
14795
15124
 
14796
15125
  // ../../shared/all/helpers/terminal-ai/terminal.ts
14797
- var import_child_process6 = require("child_process");
15126
+ var import_child_process7 = require("child_process");
14798
15127
  var import_dockerode = __toESM(require("dockerode"));
14799
- var import_fs3 = __toESM(require("fs"));
15128
+ var import_fs4 = __toESM(require("fs"));
14800
15129
  var import_os2 = __toESM(require("os"));
14801
- var import_path3 = __toESM(require("path"));
15130
+ var import_path4 = __toESM(require("path"));
14802
15131
  function resolveDocker() {
14803
15132
  if (process.env.DOCKER_HOST) return new import_dockerode.default();
14804
15133
  try {
14805
- const out = (0, import_child_process6.execFileSync)(
15134
+ const out = (0, import_child_process7.execFileSync)(
14806
15135
  "docker",
14807
15136
  ["context", "inspect", "--format", "{{.Endpoints.docker.Host}}"],
14808
15137
  { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
14809
15138
  ).trim();
14810
15139
  if (out.startsWith("unix://")) {
14811
15140
  const p = out.slice("unix://".length);
14812
- if (import_fs3.default.existsSync(p)) return new import_dockerode.default({ socketPath: p });
15141
+ if (import_fs4.default.existsSync(p)) return new import_dockerode.default({ socketPath: p });
14813
15142
  } else if (out) {
14814
15143
  process.env.DOCKER_HOST = out;
14815
15144
  return new import_dockerode.default();
@@ -14818,15 +15147,15 @@ function resolveDocker() {
14818
15147
  }
14819
15148
  const candidates = [
14820
15149
  "/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")
15150
+ import_path4.default.join(import_os2.default.homedir(), ".docker/run/docker.sock"),
15151
+ import_path4.default.join(import_os2.default.homedir(), ".docker/desktop/docker.sock"),
15152
+ import_path4.default.join(import_os2.default.homedir(), ".orbstack/run/docker.sock"),
15153
+ import_path4.default.join(import_os2.default.homedir(), ".colima/default/docker.sock"),
15154
+ import_path4.default.join(import_os2.default.homedir(), ".rd/docker.sock")
14826
15155
  ];
14827
15156
  for (const p of candidates) {
14828
15157
  try {
14829
- import_fs3.default.statSync(p);
15158
+ import_fs4.default.statSync(p);
14830
15159
  return new import_dockerode.default({ socketPath: p });
14831
15160
  } catch {
14832
15161
  }
@@ -15027,50 +15356,119 @@ var TerminalManager = class {
15027
15356
  };
15028
15357
 
15029
15358
  // src/agent-adapter.ts
15030
- var import_fs4 = __toESM(require("fs"));
15359
+ var import_fs5 = __toESM(require("fs"));
15031
15360
  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");
15361
+ var import_path5 = __toESM(require("path"));
15362
+ var CONFIG_DIR2 = import_path5.default.join(import_os3.default.homedir(), ".factiii-runner");
15363
+ var RUNNER_CONFIG_PATH = import_path5.default.join(CONFIG_DIR2, "runner-config.json");
15034
15364
  function safeSlug(spaceSlug) {
15035
15365
  return spaceSlug.replace(/[^a-zA-Z0-9_.-]/g, "_");
15036
15366
  }
15037
- function configPath(spaceSlug) {
15038
- return import_path4.default.join(CONFIG_DIR2, `space-${safeSlug(spaceSlug)}.json`);
15039
- }
15040
15367
  function connectionPath(spaceSlug) {
15041
- return import_path4.default.join(CONFIG_DIR2, `space-${safeSlug(spaceSlug)}.onedrive.json`);
15368
+ return import_path5.default.join(CONFIG_DIR2, `space-${safeSlug(spaceSlug)}.onedrive.json`);
15042
15369
  }
15043
15370
  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);
15371
+ import_fs5.default.mkdirSync(CONFIG_DIR2, { recursive: true });
15372
+ import_fs5.default.writeFileSync(filePath, JSON.stringify(value2, null, 2));
15373
+ import_fs5.default.chmodSync(filePath, 384);
15047
15374
  }
15048
- var localConfigProvider = {
15049
- readConfig(spaceSlug) {
15050
- let config = {};
15375
+ var CONFIG_FIELDS = [
15376
+ "repoUrl",
15377
+ "mainBranch",
15378
+ "gitName",
15379
+ "gitEmail",
15380
+ "claudeToken",
15381
+ "githubToken",
15382
+ "storageBackend"
15383
+ ];
15384
+ function fillDefaults(config) {
15385
+ return {
15386
+ repoUrl: config.repoUrl || "",
15387
+ mainBranch: config.mainBranch || "main",
15388
+ gitName: config.gitName || "",
15389
+ gitEmail: config.gitEmail || "",
15390
+ claudeToken: config.claudeToken || "",
15391
+ githubToken: config.githubToken || "",
15392
+ storageBackend: config.storageBackend || "github"
15393
+ };
15394
+ }
15395
+ function migrateLegacyConfigs() {
15396
+ const file = { boards: {} };
15397
+ let legacy = [];
15398
+ try {
15399
+ legacy = import_fs5.default.readdirSync(CONFIG_DIR2).filter((f) => f.startsWith("space-") && f.endsWith(".json")).filter((f) => !f.endsWith(".onedrive.json"));
15400
+ } catch {
15401
+ return file;
15402
+ }
15403
+ for (const name of legacy) {
15404
+ const slug = name.slice("space-".length, -".json".length);
15405
+ let parsed = {};
15051
15406
  try {
15052
- config = JSON.parse(
15053
- import_fs4.default.readFileSync(configPath(spaceSlug), "utf-8")
15407
+ parsed = JSON.parse(
15408
+ import_fs5.default.readFileSync(import_path5.default.join(CONFIG_DIR2, name), "utf-8")
15054
15409
  );
15055
15410
  } catch {
15411
+ continue;
15056
15412
  }
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
- };
15413
+ if (!file.defaultSlug) {
15414
+ file.defaultSlug = slug;
15415
+ file.default = parsed;
15416
+ } else {
15417
+ file.boards[slug] = parsed;
15418
+ }
15419
+ }
15420
+ if (file.defaultSlug) writeJson(RUNNER_CONFIG_PATH, file);
15421
+ return file;
15422
+ }
15423
+ function readRunnerConfig2() {
15424
+ try {
15425
+ return JSON.parse(
15426
+ import_fs5.default.readFileSync(RUNNER_CONFIG_PATH, "utf-8")
15427
+ );
15428
+ } catch {
15429
+ return migrateLegacyConfigs();
15430
+ }
15431
+ }
15432
+ var localConfigProvider = {
15433
+ readConfig(spaceSlug) {
15434
+ const file = readRunnerConfig2();
15435
+ const base = file.default ?? {};
15436
+ const over = file.boards?.[spaceSlug] ?? {};
15437
+ return fillDefaults({
15438
+ repoUrl: over.repoUrl || base.repoUrl,
15439
+ mainBranch: over.mainBranch || base.mainBranch,
15440
+ gitName: over.gitName || base.gitName,
15441
+ gitEmail: over.gitEmail || base.gitEmail,
15442
+ claudeToken: over.claudeToken || base.claudeToken,
15443
+ githubToken: over.githubToken || base.githubToken,
15444
+ storageBackend: over.storageBackend || base.storageBackend
15445
+ });
15066
15446
  },
15067
15447
  writeConfig(spaceSlug, config) {
15068
- writeJson(configPath(spaceSlug), config);
15448
+ const file = readRunnerConfig2();
15449
+ file.boards ??= {};
15450
+ if (!file.defaultSlug) {
15451
+ file.defaultSlug = spaceSlug;
15452
+ file.default = config;
15453
+ } else if (spaceSlug === file.defaultSlug) {
15454
+ file.default = config;
15455
+ } else {
15456
+ const base = file.default ?? {};
15457
+ const diff = {};
15458
+ for (const f of CONFIG_FIELDS) {
15459
+ if (config[f] && config[f] !== base[f]) {
15460
+ diff[f] = config[f];
15461
+ }
15462
+ }
15463
+ if (Object.keys(diff).length) file.boards[spaceSlug] = diff;
15464
+ else delete file.boards[spaceSlug];
15465
+ }
15466
+ writeJson(RUNNER_CONFIG_PATH, file);
15069
15467
  },
15070
15468
  readOneDriveConnection(spaceSlug) {
15071
15469
  try {
15072
15470
  return JSON.parse(
15073
- import_fs4.default.readFileSync(connectionPath(spaceSlug), "utf-8")
15471
+ import_fs5.default.readFileSync(connectionPath(spaceSlug), "utf-8")
15074
15472
  );
15075
15473
  } catch {
15076
15474
  return null;
@@ -15079,7 +15477,7 @@ var localConfigProvider = {
15079
15477
  writeOneDriveConnection(spaceSlug, connection) {
15080
15478
  const filePath = connectionPath(spaceSlug);
15081
15479
  if (!connection) {
15082
- import_fs4.default.rmSync(filePath, { force: true });
15480
+ import_fs5.default.rmSync(filePath, { force: true });
15083
15481
  return;
15084
15482
  }
15085
15483
  writeJson(filePath, connection);
@@ -15087,6 +15485,13 @@ var localConfigProvider = {
15087
15485
  };
15088
15486
 
15089
15487
  // src/daemon.ts
15488
+ function toChannelScope(raw) {
15489
+ if (!raw || raw.role === "owner") return { role: "owner" };
15490
+ return { role: "member", spaceSlugs: new Set(raw.spaceSlugs) };
15491
+ }
15492
+ function scopeAllows(scope, spaceSlug) {
15493
+ return scope.role === "owner" || scope.spaceSlugs.has(spaceSlug);
15494
+ }
15090
15495
  async function startDaemon(config) {
15091
15496
  console.log(`Connecting to ${config.serverUrl} as "${config.runnerName}"...`);
15092
15497
  const socket = lookup(config.serverUrl, {
@@ -15096,10 +15501,11 @@ async function startDaemon(config) {
15096
15501
  autoConnect: false
15097
15502
  });
15098
15503
  const engines = /* @__PURE__ */ new Map();
15099
- const openChannels = /* @__PURE__ */ new Set();
15504
+ const channelScopes = /* @__PURE__ */ new Map();
15100
15505
  const terminalByChannel = /* @__PURE__ */ new WeakMap();
15101
- const findCardContainer = (postId) => {
15102
- for (const engine of engines.values()) {
15506
+ const findCardContainerInScope = (scope, postId) => {
15507
+ for (const [spaceSlug, engine] of engines) {
15508
+ if (!scopeAllows(scope, spaceSlug)) continue;
15103
15509
  const name = engine.getCardContainerName(postId);
15104
15510
  if (name) return name;
15105
15511
  }
@@ -15107,7 +15513,19 @@ async function startDaemon(config) {
15107
15513
  };
15108
15514
  const broadcastToAll = (msg) => {
15109
15515
  const frames = chunkWire(msg);
15110
- for (const dc of openChannels) {
15516
+ for (const dc of channelScopes.keys()) {
15517
+ for (const frame of frames) {
15518
+ try {
15519
+ dc.sendMessage(frame);
15520
+ } catch {
15521
+ }
15522
+ }
15523
+ }
15524
+ };
15525
+ const broadcastToBoard = (spaceSlug, msg) => {
15526
+ const frames = chunkWire(msg);
15527
+ for (const [dc, scope] of channelScopes) {
15528
+ if (!scopeAllows(scope, spaceSlug)) continue;
15111
15529
  for (const frame of frames) {
15112
15530
  try {
15113
15531
  dc.sendMessage(frame);
@@ -15116,7 +15534,7 @@ async function startDaemon(config) {
15116
15534
  }
15117
15535
  }
15118
15536
  };
15119
- function getEngine(spaceSlug) {
15537
+ function getEngine(spaceSlug, spaceId) {
15120
15538
  let engine = engines.get(spaceSlug);
15121
15539
  if (!engine) {
15122
15540
  const send = (event, data) => {
@@ -15126,10 +15544,13 @@ async function startDaemon(config) {
15126
15544
  event,
15127
15545
  payload: data
15128
15546
  };
15129
- broadcastToAll(JSON.stringify(msg));
15547
+ broadcastToBoard(spaceSlug, JSON.stringify(msg));
15130
15548
  };
15131
15549
  engine = new BoardAgentEngine({
15132
15550
  spaceSlug,
15551
+ // Stable id for durable container naming (slugs can change); the engine
15552
+ // falls back to the slug when absent.
15553
+ spaceId,
15133
15554
  emitter: createEventSink(send, spaceSlug),
15134
15555
  configProvider: localConfigProvider,
15135
15556
  dockerfilePath: getDockerfilePath(),
@@ -15143,6 +15564,7 @@ async function startDaemon(config) {
15143
15564
  }
15144
15565
  return engine;
15145
15566
  }
15567
+ const instancePrefix = computeInstancePrefix(`runner-${config.runnerName}`);
15146
15568
  socket.on("connect", () => {
15147
15569
  console.log(`Connected. Runner "${config.runnerName}" is online.`);
15148
15570
  console.log("Waiting for connections from UI clients...\n");
@@ -15155,6 +15577,7 @@ async function startDaemon(config) {
15155
15577
  });
15156
15578
  socket.on("runnerOffer", (data) => {
15157
15579
  console.log(`Received WebRTC offer from ${data.fromSocketId}`);
15580
+ const scope = toChannelScope(data.scope);
15158
15581
  try {
15159
15582
  const peer = new import_node_datachannel.PeerConnection("runner", {
15160
15583
  iceServers: ["stun:stun.l.google.com:19302"]
@@ -15202,7 +15625,7 @@ async function startDaemon(config) {
15202
15625
  console.log(
15203
15626
  `Data channel "${dc.getLabel()}" opened with ${data.fromSocketId}`
15204
15627
  );
15205
- openChannels.add(dc);
15628
+ channelScopes.set(dc, scope);
15206
15629
  const reassembler = makeChunkReassembler();
15207
15630
  const sendToChannel = (msg) => {
15208
15631
  for (const frame of chunkWire(msg)) {
@@ -15220,10 +15643,17 @@ async function startDaemon(config) {
15220
15643
  };
15221
15644
  terminalByChannel.set(
15222
15645
  dc,
15223
- new TerminalManager(sendRawToChannel, findCardContainer)
15646
+ new TerminalManager(
15647
+ sendRawToChannel,
15648
+ (postId) => findCardContainerInScope(scope, postId)
15649
+ )
15224
15650
  );
15225
15651
  dc.onOpen(() => {
15652
+ sendToChannel(
15653
+ JSON.stringify({ type: "runner-info", version: currentVersion() })
15654
+ );
15226
15655
  for (const [spaceSlug, engine] of engines.entries()) {
15656
+ if (!scopeAllows(scope, spaceSlug)) continue;
15227
15657
  const sessions = engine.cardSessions();
15228
15658
  for (const summary of Object.values(sessions)) {
15229
15659
  const msg = JSON.stringify({
@@ -15251,15 +15681,21 @@ async function startDaemon(config) {
15251
15681
  }
15252
15682
  return;
15253
15683
  }
15684
+ if (parsed.type === "runner-control" && parsed.action === "update") {
15685
+ applyUpdateAndRestart();
15686
+ return;
15687
+ }
15254
15688
  if (parsed.type === "broadcast" && parsed.spaceSlug && parsed.event) {
15689
+ if (!scopeAllows(scope, parsed.spaceSlug)) return;
15255
15690
  const eventMsg = JSON.stringify({
15256
15691
  type: "event",
15257
15692
  event: `board-agent:${parsed.spaceSlug}:${parsed.event}`,
15258
15693
  payload: parsed.payload
15259
15694
  });
15260
15695
  const frames = chunkWire(eventMsg);
15261
- for (const other of openChannels) {
15696
+ for (const [other, otherScope] of channelScopes) {
15262
15697
  if (other === dc) continue;
15698
+ if (!scopeAllows(otherScope, parsed.spaceSlug)) continue;
15263
15699
  for (const frame of frames) {
15264
15700
  try {
15265
15701
  other.sendMessage(frame);
@@ -15274,14 +15710,17 @@ async function startDaemon(config) {
15274
15710
  }
15275
15711
  handleDataChannelMessage(
15276
15712
  msgStr,
15713
+ scope,
15277
15714
  sendToChannel,
15278
- broadcastToAll,
15279
- getEngine
15715
+ broadcastToBoard,
15716
+ getEngine,
15717
+ engines,
15718
+ instancePrefix
15280
15719
  );
15281
15720
  });
15282
15721
  dc.onClosed(() => {
15283
15722
  console.log(`Data channel closed with ${data.fromSocketId}`);
15284
- openChannels.delete(dc);
15723
+ channelScopes.delete(dc);
15285
15724
  const mgr = terminalByChannel.get(dc);
15286
15725
  if (mgr) {
15287
15726
  mgr.destroyAll();
@@ -15307,6 +15746,25 @@ async function startDaemon(config) {
15307
15746
  }
15308
15747
  socket.disconnect();
15309
15748
  };
15749
+ const applyUpdateAndRestart = () => {
15750
+ console.log("[update] update approved by a client \u2014 installing latest...");
15751
+ const status = (state) => broadcastToAll(JSON.stringify({ type: "runner-update", state }));
15752
+ status("installing");
15753
+ if (!installLatest()) {
15754
+ status("failed");
15755
+ return;
15756
+ }
15757
+ status("restarting");
15758
+ cleanup();
15759
+ process.exit(RESTART_FOR_UPDATE);
15760
+ };
15761
+ socket.on("runnerRevoked", () => {
15762
+ console.log(
15763
+ "\nThis runner was disconnected from your Factiii account. Stopping all jobs and removing containers..."
15764
+ );
15765
+ cleanup();
15766
+ process.exit(0);
15767
+ });
15310
15768
  process.on("SIGINT", () => {
15311
15769
  console.log("\nShutting down...");
15312
15770
  cleanup();
@@ -15316,8 +15774,27 @@ async function startDaemon(config) {
15316
15774
  cleanup();
15317
15775
  process.exit(0);
15318
15776
  });
15777
+ await new Promise(() => {
15778
+ });
15779
+ }
15780
+ function spaceIdToSlug(engines) {
15781
+ const map = /* @__PURE__ */ new Map();
15782
+ for (const [slug, engine] of engines) {
15783
+ if (engine.spaceId != null) map.set(engine.spaceId, slug);
15784
+ }
15785
+ return map;
15319
15786
  }
15320
- async function handleDataChannelMessage(raw, sendToChannel, broadcastToAll, getEngine) {
15787
+ function scopeFilterContainers(all, scope, engines) {
15788
+ const idToSlug = spaceIdToSlug(engines);
15789
+ return all.flatMap((c) => {
15790
+ const slug = c.spaceId != null ? idToSlug.get(c.spaceId) : void 0;
15791
+ if (scope.role !== "owner") {
15792
+ if (!slug || !scopeAllows(scope, slug)) return [];
15793
+ }
15794
+ return [{ ...c, spaceId: slug ? c.spaceId : null, spaceSlug: slug }];
15795
+ });
15796
+ }
15797
+ async function handleDataChannelMessage(raw, scope, sendToChannel, broadcastToBoard, getEngine, engines, instancePrefix) {
15321
15798
  let msg;
15322
15799
  try {
15323
15800
  msg = JSON.parse(raw);
@@ -15325,9 +15802,76 @@ async function handleDataChannelMessage(raw, sendToChannel, broadcastToAll, getE
15325
15802
  return;
15326
15803
  }
15327
15804
  if (msg.type !== "dispatch") return;
15328
- const engine = getEngine(msg.spaceSlug);
15329
15805
  const { action, payload, requestId } = msg;
15330
15806
  const ts = () => (/* @__PURE__ */ new Date()).toLocaleTimeString();
15807
+ const sendResult = (res) => sendToChannel(JSON.stringify(res));
15808
+ if (action === "listContainers") {
15809
+ try {
15810
+ const rows = await listContainersByPrefix(`factiii-${instancePrefix}-`);
15811
+ const all = rows.flatMap((row) => {
15812
+ const parsed = parseFactiiiContainerName(row.name, instancePrefix);
15813
+ if (!parsed) return [];
15814
+ return [
15815
+ {
15816
+ containerName: row.name,
15817
+ kind: parsed.kind,
15818
+ spaceId: parsed.spaceId,
15819
+ taskId: parsed.taskId,
15820
+ cacheKey: parsed.cacheKey,
15821
+ running: row.running,
15822
+ status: row.status
15823
+ }
15824
+ ];
15825
+ });
15826
+ sendResult({
15827
+ type: "result",
15828
+ requestId,
15829
+ result: scopeFilterContainers(all, scope, engines)
15830
+ });
15831
+ } catch (err) {
15832
+ sendResult({
15833
+ type: "result",
15834
+ requestId,
15835
+ error: err instanceof Error ? err.message : String(err)
15836
+ });
15837
+ }
15838
+ return;
15839
+ }
15840
+ if (action === "removeContainer") {
15841
+ const { containerName } = payload;
15842
+ try {
15843
+ if (!containerName.startsWith(`factiii-${instancePrefix}-`)) {
15844
+ throw new Error("Refusing to remove a container outside this runner.");
15845
+ }
15846
+ if (scope.role !== "owner") {
15847
+ const parsed = parseFactiiiContainerName(containerName, instancePrefix);
15848
+ const slug = parsed?.spaceId != null ? spaceIdToSlug(engines).get(parsed.spaceId) : void 0;
15849
+ if (!slug || !scopeAllows(scope, slug)) {
15850
+ throw new Error("Not authorized to remove this container");
15851
+ }
15852
+ }
15853
+ await killContainer(containerName);
15854
+ sendResult({ type: "result", requestId, result: { removed: true } });
15855
+ } catch (err) {
15856
+ sendResult({
15857
+ type: "result",
15858
+ requestId,
15859
+ error: err instanceof Error ? err.message : String(err)
15860
+ });
15861
+ }
15862
+ return;
15863
+ }
15864
+ if (!scopeAllows(scope, msg.spaceSlug)) {
15865
+ console.warn(`[${ts()}] \u20E0 ${action} denied for board "${msg.spaceSlug}"`);
15866
+ const denied = {
15867
+ type: "result",
15868
+ requestId,
15869
+ error: "Not authorized for this board on this runner"
15870
+ };
15871
+ sendToChannel(JSON.stringify(denied));
15872
+ return;
15873
+ }
15874
+ const engine = getEngine(msg.spaceSlug, msg.spaceId);
15331
15875
  console.log(`[${ts()}] \u2190 ${action} (${requestId})`);
15332
15876
  const askLogSender = (entry) => {
15333
15877
  const event = {
@@ -15336,7 +15880,7 @@ async function handleDataChannelMessage(raw, sendToChannel, broadcastToAll, getE
15336
15880
  event: `board-agent:${msg.spaceSlug}:log`,
15337
15881
  payload: entry
15338
15882
  };
15339
- broadcastToAll(JSON.stringify(event));
15883
+ broadcastToBoard(msg.spaceSlug, JSON.stringify(event));
15340
15884
  };
15341
15885
  try {
15342
15886
  const result = await engine.dispatch(
@@ -15359,107 +15903,41 @@ async function handleDataChannelMessage(raw, sendToChannel, broadcastToAll, getE
15359
15903
  }
15360
15904
  }
15361
15905
  function getDockerfilePath() {
15362
- return import_path5.default.join(__dirname, "..", "Dockerfile.claude");
15906
+ return import_path6.default.join(__dirname, "..", "Dockerfile.claude");
15363
15907
  }
15364
15908
 
15365
- // src/setup.ts
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";
15371
- function run(bin, args, opts) {
15372
- try {
15373
- return (0, import_child_process7.execFileSync)(bin, args, {
15374
- encoding: "utf-8",
15375
- stdio: opts?.silent ? "pipe" : "inherit",
15376
- // Avoids a cmd.exe window flash on Windows during silent checks.
15377
- windowsHide: true
15378
- });
15379
- } catch {
15380
- return "";
15381
- }
15382
- }
15383
- function check(cmd) {
15384
- const lookup2 = process.platform === "win32" ? "where" : "which";
15385
- try {
15386
- (0, import_child_process7.execFileSync)(lookup2, [cmd], { stdio: "pipe", windowsHide: true });
15387
- return true;
15388
- } catch {
15389
- return false;
15390
- }
15391
- }
15392
- async function setup(serverUrl) {
15393
- console.log("\n=== Factiii Runner \u2014 Setup ===\n");
15394
- console.log("[1/3] Checking Docker...");
15395
- if (!check("docker")) {
15396
- console.error(
15397
- " Docker is not installed. Install it from https://docs.docker.com/get-docker/ and re-run setup."
15398
- );
15399
- process.exit(1);
15400
- }
15401
- if (!run("docker", ["info"], { silent: true })) {
15402
- console.error(
15403
- " Docker is installed but the daemon is not running. Start Docker and re-run setup."
15404
- );
15405
- process.exit(1);
15406
- }
15407
- console.log(" Docker is ready.");
15408
- console.log("\n[2/3] Building factiii-claude Docker image...");
15409
- const dockerfilePath = findDockerfile();
15410
- if (!dockerfilePath) {
15411
- console.error(
15412
- " Dockerfile.claude not found. Reinstall the package and re-run setup."
15413
- );
15414
- process.exit(1);
15909
+ // src/supervisor.ts
15910
+ async function runSupervised(workerArgs) {
15911
+ if (isWorker()) {
15912
+ const config = readRunnerConfig();
15913
+ if (!config) {
15914
+ console.error('Failed to read config. Run "factiii-runner setup" again.');
15915
+ process.exit(1);
15916
+ }
15917
+ await startDaemon(config);
15918
+ process.exit(0);
15415
15919
  }
15416
- const wantedHash = import_crypto2.default.createHash("sha256").update(import_fs5.default.readFileSync(dockerfilePath)).digest("hex");
15417
- const existingHash = run(
15418
- "docker",
15419
- [
15420
- "image",
15421
- "inspect",
15422
- "factiii-claude",
15423
- "--format",
15424
- `{{ index .Config.Labels "${DOCKERFILE_LABEL2}" }}`
15425
- ],
15426
- { silent: true }
15427
- ).trim();
15428
- if (existingHash === wantedHash) {
15429
- console.log(" Image is up to date.");
15430
- } else {
15431
- console.log(
15432
- existingHash ? ` Dockerfile changed since last build, rebuilding from ${dockerfilePath}...` : ` Building from ${dockerfilePath}...`
15920
+ for (; ; ) {
15921
+ const child = (0, import_node_child_process2.spawnSync)(
15922
+ process.execPath,
15923
+ [...process.execArgv, ...workerArgs],
15924
+ {
15925
+ stdio: "inherit",
15926
+ env: markWorkerEnv(process.env)
15927
+ }
15433
15928
  );
15434
- run("docker", [
15435
- "build",
15436
- "-t",
15437
- "factiii-claude",
15438
- "-f",
15439
- dockerfilePath,
15440
- "--label",
15441
- `${DOCKERFILE_LABEL2}=${wantedHash}`,
15442
- "."
15443
- ]);
15444
- console.log(" Image built.");
15929
+ if (child.status === RESTART_FOR_UPDATE) {
15930
+ console.log("[update] relaunching the runner on the new version...");
15931
+ continue;
15932
+ }
15933
+ process.exit(child.signal ? 0 : child.status ?? 0);
15445
15934
  }
15446
- run("docker", ["volume", "create", "factiii-card-cache"], { silent: true });
15447
- console.log("\n[3/3] Pairing this runner with a Factiii account...");
15448
- const { token, runnerName } = await pairWithBrowser(serverUrl);
15449
- writeRunnerConfig({ serverUrl, authToken: token, runnerName });
15450
- console.log(" Paired and config saved.");
15451
- console.log("\n=== Setup complete ===\n");
15452
- console.log("Start the runner with: factiii-runner start\n");
15453
- }
15454
- function findDockerfile() {
15455
- const p = import_path6.default.join(__dirname, "..", "Dockerfile.claude");
15456
- return import_fs5.default.existsSync(p) ? p : null;
15457
15935
  }
15458
15936
 
15459
15937
  // src/cli.ts
15460
15938
  import_node_dns.default.setDefaultResultOrder("ipv4first");
15461
15939
  var program2 = new Command();
15462
- program2.name("factiii-runner").description("Factiii Runner - run Board AI agents on remote servers").version("0.0.1");
15940
+ program2.name("factiii-runner").description("Factiii Runner - run Board AI agents on remote servers").version(currentVersion());
15463
15941
  program2.command("setup").description("Verify Docker, build the Claude image, and pair this runner").option("-s, --server <url>", "API server URL", "https://api.factiii.com").action(async (opts) => {
15464
15942
  try {
15465
15943
  await setup(opts.server);
@@ -15494,12 +15972,7 @@ program2.command("start").description("Start the runner daemon").action(async ()
15494
15972
  console.error('No config found. Run "factiii-runner setup" first.');
15495
15973
  process.exit(1);
15496
15974
  }
15497
- const config = readRunnerConfig();
15498
- if (!config) {
15499
- console.error('Failed to read config. Run "factiii-runner setup" again.');
15500
- process.exit(1);
15501
- }
15502
- await startDaemon(config);
15975
+ await runSupervised(process.argv.slice(1));
15503
15976
  });
15504
15977
  program2.command("status").description("Show current runner configuration").action(() => {
15505
15978
  if (!configExists()) {