itwillsync 1.2.1 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hub/daemon.js +4523 -0
- package/dist/hub/daemon.js.map +1 -0
- package/dist/hub/dashboard/assets/index-DgUZUPW_.js +2 -0
- package/dist/hub/dashboard/assets/index-Erqx_a0N.css +1 -0
- package/dist/hub/dashboard/index.html +31 -0
- package/dist/index.js +428 -28
- package/dist/index.js.map +1 -1
- package/dist/web-client/assets/{index-sjFayRPB.js → index-CpcOfuy-.js} +17 -16
- package/dist/web-client/assets/index-viCDtpt6.css +1 -0
- package/dist/web-client/index.html +3 -2
- package/package.json +1 -1
- package/dist/web-client/assets/index-Cj7rG9re.css +0 -1
package/dist/index.js
CHANGED
|
@@ -2766,7 +2766,7 @@ var require_websocket = __commonJS({
|
|
|
2766
2766
|
}
|
|
2767
2767
|
const defaultPort = isSecure ? 443 : 80;
|
|
2768
2768
|
const key = randomBytes2(16).toString("base64");
|
|
2769
|
-
const
|
|
2769
|
+
const request2 = isSecure ? https.request : http.request;
|
|
2770
2770
|
const protocolSet = /* @__PURE__ */ new Set();
|
|
2771
2771
|
let perMessageDeflate;
|
|
2772
2772
|
opts.createConnection = opts.createConnection || (isSecure ? tlsConnect : netConnect);
|
|
@@ -2843,12 +2843,12 @@ var require_websocket = __commonJS({
|
|
|
2843
2843
|
if (opts.auth && !options.headers.authorization) {
|
|
2844
2844
|
options.headers.authorization = "Basic " + Buffer.from(opts.auth).toString("base64");
|
|
2845
2845
|
}
|
|
2846
|
-
req = websocket._req =
|
|
2846
|
+
req = websocket._req = request2(opts);
|
|
2847
2847
|
if (websocket._redirects) {
|
|
2848
2848
|
websocket.emit("redirect", websocket.url, req);
|
|
2849
2849
|
}
|
|
2850
2850
|
} else {
|
|
2851
|
-
req = websocket._req =
|
|
2851
|
+
req = websocket._req = request2(opts);
|
|
2852
2852
|
}
|
|
2853
2853
|
if (opts.timeout) {
|
|
2854
2854
|
req.on("timeout", () => {
|
|
@@ -3683,6 +3683,14 @@ function ensureSpawnHelperPermissions() {
|
|
|
3683
3683
|
}
|
|
3684
3684
|
var PtyManager = class {
|
|
3685
3685
|
ptyProcess;
|
|
3686
|
+
_cols = 80;
|
|
3687
|
+
_rows = 24;
|
|
3688
|
+
get cols() {
|
|
3689
|
+
return this._cols;
|
|
3690
|
+
}
|
|
3691
|
+
get rows() {
|
|
3692
|
+
return this._rows;
|
|
3693
|
+
}
|
|
3686
3694
|
/** The process ID of the spawned PTY process. */
|
|
3687
3695
|
pid;
|
|
3688
3696
|
constructor(command, args) {
|
|
@@ -3725,6 +3733,8 @@ var PtyManager = class {
|
|
|
3725
3733
|
resize(cols, rows) {
|
|
3726
3734
|
try {
|
|
3727
3735
|
this.ptyProcess.resize(cols, rows);
|
|
3736
|
+
this._cols = cols;
|
|
3737
|
+
this._rows = rows;
|
|
3728
3738
|
} catch {
|
|
3729
3739
|
}
|
|
3730
3740
|
}
|
|
@@ -3929,7 +3939,7 @@ async function serveStaticFile(webClientPath, filePath, req, res) {
|
|
|
3929
3939
|
var PING_INTERVAL_MS = 3e4;
|
|
3930
3940
|
var SCROLLBACK_BUFFER_SIZE = 5e4;
|
|
3931
3941
|
function createSyncServer(options) {
|
|
3932
|
-
const { ptyManager, token, webClientPath, host, port } = options;
|
|
3942
|
+
const { ptyManager, token, webClientPath, host, port, localTerminalOwnsResize = false } = options;
|
|
3933
3943
|
const clients = /* @__PURE__ */ new Set();
|
|
3934
3944
|
const aliveMap = /* @__PURE__ */ new WeakMap();
|
|
3935
3945
|
let scrollbackBuffer = "";
|
|
@@ -3972,6 +3982,7 @@ function createSyncServer(options) {
|
|
|
3972
3982
|
if (scrollbackBuffer.length > 0) {
|
|
3973
3983
|
ws.send(JSON.stringify({ type: "data", data: scrollbackBuffer, seq }));
|
|
3974
3984
|
}
|
|
3985
|
+
ws.send(JSON.stringify({ type: "resize", cols: ptyManager.cols, rows: ptyManager.rows }));
|
|
3975
3986
|
ws.on("pong", () => {
|
|
3976
3987
|
aliveMap.set(ws, true);
|
|
3977
3988
|
});
|
|
@@ -3981,7 +3992,9 @@ function createSyncServer(options) {
|
|
|
3981
3992
|
if (message.type === "input" && typeof message.data === "string") {
|
|
3982
3993
|
ptyManager.write(message.data);
|
|
3983
3994
|
} else if (message.type === "resize" && typeof message.cols === "number" && typeof message.rows === "number") {
|
|
3984
|
-
|
|
3995
|
+
if (!localTerminalOwnsResize) {
|
|
3996
|
+
ptyManager.resize(message.cols, message.rows);
|
|
3997
|
+
}
|
|
3985
3998
|
} else if (message.type === "resume" && typeof message.lastSeq === "number") {
|
|
3986
3999
|
const missed = seq - message.lastSeq;
|
|
3987
4000
|
if (missed > 0 && scrollbackBuffer.length > 0) {
|
|
@@ -4024,6 +4037,14 @@ function createSyncServer(options) {
|
|
|
4024
4037
|
clients.clear();
|
|
4025
4038
|
wssServer.close();
|
|
4026
4039
|
httpServer.close();
|
|
4040
|
+
},
|
|
4041
|
+
broadcastResize(cols, rows) {
|
|
4042
|
+
const msg = JSON.stringify({ type: "resize", cols, rows });
|
|
4043
|
+
for (const client of clients) {
|
|
4044
|
+
if (client.readyState === client.OPEN) {
|
|
4045
|
+
client.send(msg);
|
|
4046
|
+
}
|
|
4047
|
+
}
|
|
4027
4048
|
}
|
|
4028
4049
|
};
|
|
4029
4050
|
}
|
|
@@ -4149,8 +4170,233 @@ async function runSetupWizard() {
|
|
|
4149
4170
|
return config;
|
|
4150
4171
|
}
|
|
4151
4172
|
|
|
4173
|
+
// src/hub-client.ts
|
|
4174
|
+
import { spawn as spawn2 } from "child_process";
|
|
4175
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
4176
|
+
import { homedir as homedir2 } from "os";
|
|
4177
|
+
import { join as join4, dirname as dirname2 } from "path";
|
|
4178
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4179
|
+
import { request } from "http";
|
|
4180
|
+
var HUB_INTERNAL_PORT = 7963;
|
|
4181
|
+
var HUB_EXTERNAL_PORT = 7962;
|
|
4182
|
+
var SESSION_PORT_START = 7964;
|
|
4183
|
+
function getHubDir() {
|
|
4184
|
+
return process.env.ITWILLSYNC_CONFIG_DIR || join4(homedir2(), ".itwillsync");
|
|
4185
|
+
}
|
|
4186
|
+
function getHubConfigPath() {
|
|
4187
|
+
return join4(getHubDir(), "hub.json");
|
|
4188
|
+
}
|
|
4189
|
+
async function discoverHub() {
|
|
4190
|
+
return new Promise((resolve) => {
|
|
4191
|
+
const req = request(
|
|
4192
|
+
{
|
|
4193
|
+
hostname: "127.0.0.1",
|
|
4194
|
+
port: HUB_INTERNAL_PORT,
|
|
4195
|
+
path: "/api/health",
|
|
4196
|
+
method: "GET",
|
|
4197
|
+
timeout: 2e3
|
|
4198
|
+
},
|
|
4199
|
+
(res) => {
|
|
4200
|
+
let data = "";
|
|
4201
|
+
res.on("data", (chunk) => {
|
|
4202
|
+
data += chunk;
|
|
4203
|
+
});
|
|
4204
|
+
res.on("end", () => {
|
|
4205
|
+
try {
|
|
4206
|
+
const json = JSON.parse(data);
|
|
4207
|
+
resolve(json.status === "ok");
|
|
4208
|
+
} catch {
|
|
4209
|
+
resolve(false);
|
|
4210
|
+
}
|
|
4211
|
+
});
|
|
4212
|
+
}
|
|
4213
|
+
);
|
|
4214
|
+
req.on("error", () => resolve(false));
|
|
4215
|
+
req.on("timeout", () => {
|
|
4216
|
+
req.destroy();
|
|
4217
|
+
resolve(false);
|
|
4218
|
+
});
|
|
4219
|
+
req.end();
|
|
4220
|
+
});
|
|
4221
|
+
}
|
|
4222
|
+
async function spawnHub() {
|
|
4223
|
+
const __dirname = dirname2(fileURLToPath2(import.meta.url));
|
|
4224
|
+
const hubPath = join4(__dirname, "hub", "daemon.js");
|
|
4225
|
+
return new Promise((resolve, reject) => {
|
|
4226
|
+
const child = spawn2("node", [hubPath], {
|
|
4227
|
+
detached: true,
|
|
4228
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
4229
|
+
env: {
|
|
4230
|
+
...process.env,
|
|
4231
|
+
// Pass config dir to hub
|
|
4232
|
+
ITWILLSYNC_CONFIG_DIR: process.env.ITWILLSYNC_CONFIG_DIR || ""
|
|
4233
|
+
}
|
|
4234
|
+
});
|
|
4235
|
+
child.unref();
|
|
4236
|
+
let output = "";
|
|
4237
|
+
const timeout = setTimeout(() => {
|
|
4238
|
+
reject(new Error("Hub daemon startup timed out"));
|
|
4239
|
+
}, 1e4);
|
|
4240
|
+
child.stdout?.on("data", (data) => {
|
|
4241
|
+
output += data.toString();
|
|
4242
|
+
if (output.includes("hub:ready:")) {
|
|
4243
|
+
clearTimeout(timeout);
|
|
4244
|
+
child.stdout?.destroy();
|
|
4245
|
+
resolve();
|
|
4246
|
+
}
|
|
4247
|
+
});
|
|
4248
|
+
child.on("error", (err) => {
|
|
4249
|
+
clearTimeout(timeout);
|
|
4250
|
+
reject(new Error(`Failed to spawn hub daemon: ${err.message}`));
|
|
4251
|
+
});
|
|
4252
|
+
child.on("exit", (code) => {
|
|
4253
|
+
clearTimeout(timeout);
|
|
4254
|
+
if (code !== null && code !== 0) {
|
|
4255
|
+
reject(new Error(`Hub daemon exited with code ${code}`));
|
|
4256
|
+
}
|
|
4257
|
+
});
|
|
4258
|
+
});
|
|
4259
|
+
}
|
|
4260
|
+
function getHubConfig() {
|
|
4261
|
+
const configPath = getHubConfigPath();
|
|
4262
|
+
if (!existsSync2(configPath)) {
|
|
4263
|
+
return null;
|
|
4264
|
+
}
|
|
4265
|
+
try {
|
|
4266
|
+
const raw = readFileSync2(configPath, "utf-8");
|
|
4267
|
+
return JSON.parse(raw);
|
|
4268
|
+
} catch {
|
|
4269
|
+
return null;
|
|
4270
|
+
}
|
|
4271
|
+
}
|
|
4272
|
+
async function registerSession(registration) {
|
|
4273
|
+
const body = JSON.stringify(registration);
|
|
4274
|
+
return new Promise((resolve, reject) => {
|
|
4275
|
+
const req = request(
|
|
4276
|
+
{
|
|
4277
|
+
hostname: "127.0.0.1",
|
|
4278
|
+
port: HUB_INTERNAL_PORT,
|
|
4279
|
+
path: "/api/sessions",
|
|
4280
|
+
method: "POST",
|
|
4281
|
+
headers: {
|
|
4282
|
+
"Content-Type": "application/json",
|
|
4283
|
+
"Content-Length": Buffer.byteLength(body)
|
|
4284
|
+
},
|
|
4285
|
+
timeout: 5e3
|
|
4286
|
+
},
|
|
4287
|
+
(res) => {
|
|
4288
|
+
let data = "";
|
|
4289
|
+
res.on("data", (chunk) => {
|
|
4290
|
+
data += chunk;
|
|
4291
|
+
});
|
|
4292
|
+
res.on("end", () => {
|
|
4293
|
+
try {
|
|
4294
|
+
const json = JSON.parse(data);
|
|
4295
|
+
if (res.statusCode === 201 && json.session) {
|
|
4296
|
+
resolve(json.session);
|
|
4297
|
+
} else {
|
|
4298
|
+
reject(new Error(`Registration failed: ${json.error || "Unknown error"}`));
|
|
4299
|
+
}
|
|
4300
|
+
} catch {
|
|
4301
|
+
reject(new Error("Invalid response from hub"));
|
|
4302
|
+
}
|
|
4303
|
+
});
|
|
4304
|
+
}
|
|
4305
|
+
);
|
|
4306
|
+
req.on("error", (err) => reject(new Error(`Failed to register with hub: ${err.message}`)));
|
|
4307
|
+
req.on("timeout", () => {
|
|
4308
|
+
req.destroy();
|
|
4309
|
+
reject(new Error("Registration request timed out"));
|
|
4310
|
+
});
|
|
4311
|
+
req.end(body);
|
|
4312
|
+
});
|
|
4313
|
+
}
|
|
4314
|
+
async function unregisterSession(sessionId) {
|
|
4315
|
+
return new Promise((resolve) => {
|
|
4316
|
+
const req = request(
|
|
4317
|
+
{
|
|
4318
|
+
hostname: "127.0.0.1",
|
|
4319
|
+
port: HUB_INTERNAL_PORT,
|
|
4320
|
+
path: `/api/sessions/${sessionId}`,
|
|
4321
|
+
method: "DELETE",
|
|
4322
|
+
timeout: 3e3
|
|
4323
|
+
},
|
|
4324
|
+
() => resolve()
|
|
4325
|
+
);
|
|
4326
|
+
req.on("error", () => resolve());
|
|
4327
|
+
req.on("timeout", () => {
|
|
4328
|
+
req.destroy();
|
|
4329
|
+
resolve();
|
|
4330
|
+
});
|
|
4331
|
+
req.end();
|
|
4332
|
+
});
|
|
4333
|
+
}
|
|
4334
|
+
async function listSessions() {
|
|
4335
|
+
return new Promise((resolve) => {
|
|
4336
|
+
const req = request(
|
|
4337
|
+
{
|
|
4338
|
+
hostname: "127.0.0.1",
|
|
4339
|
+
port: HUB_INTERNAL_PORT,
|
|
4340
|
+
path: "/api/sessions",
|
|
4341
|
+
method: "GET",
|
|
4342
|
+
timeout: 3e3
|
|
4343
|
+
},
|
|
4344
|
+
(res) => {
|
|
4345
|
+
let data = "";
|
|
4346
|
+
res.on("data", (chunk) => {
|
|
4347
|
+
data += chunk;
|
|
4348
|
+
});
|
|
4349
|
+
res.on("end", () => {
|
|
4350
|
+
try {
|
|
4351
|
+
const json = JSON.parse(data);
|
|
4352
|
+
resolve(json.sessions || []);
|
|
4353
|
+
} catch {
|
|
4354
|
+
resolve([]);
|
|
4355
|
+
}
|
|
4356
|
+
});
|
|
4357
|
+
}
|
|
4358
|
+
);
|
|
4359
|
+
req.on("error", () => resolve([]));
|
|
4360
|
+
req.on("timeout", () => {
|
|
4361
|
+
req.destroy();
|
|
4362
|
+
resolve([]);
|
|
4363
|
+
});
|
|
4364
|
+
req.end();
|
|
4365
|
+
});
|
|
4366
|
+
}
|
|
4367
|
+
function stopHub() {
|
|
4368
|
+
const config = getHubConfig();
|
|
4369
|
+
if (!config) return false;
|
|
4370
|
+
try {
|
|
4371
|
+
process.kill(config.pid, "SIGTERM");
|
|
4372
|
+
return true;
|
|
4373
|
+
} catch {
|
|
4374
|
+
return false;
|
|
4375
|
+
}
|
|
4376
|
+
}
|
|
4377
|
+
async function sendHeartbeat(sessionId) {
|
|
4378
|
+
return new Promise((resolve) => {
|
|
4379
|
+
const req = request(
|
|
4380
|
+
{
|
|
4381
|
+
hostname: "127.0.0.1",
|
|
4382
|
+
port: HUB_INTERNAL_PORT,
|
|
4383
|
+
path: `/api/sessions/${sessionId}/heartbeat`,
|
|
4384
|
+
method: "PUT",
|
|
4385
|
+
timeout: 2e3
|
|
4386
|
+
},
|
|
4387
|
+
() => resolve()
|
|
4388
|
+
);
|
|
4389
|
+
req.on("error", () => resolve());
|
|
4390
|
+
req.on("timeout", () => {
|
|
4391
|
+
req.destroy();
|
|
4392
|
+
resolve();
|
|
4393
|
+
});
|
|
4394
|
+
req.end();
|
|
4395
|
+
});
|
|
4396
|
+
}
|
|
4397
|
+
|
|
4152
4398
|
// src/cli-options.ts
|
|
4153
|
-
var DEFAULT_PORT =
|
|
4399
|
+
var DEFAULT_PORT = SESSION_PORT_START;
|
|
4154
4400
|
function parseArgs(argv) {
|
|
4155
4401
|
const options = {
|
|
4156
4402
|
port: DEFAULT_PORT,
|
|
@@ -4159,13 +4405,24 @@ function parseArgs(argv) {
|
|
|
4159
4405
|
command: [],
|
|
4160
4406
|
subcommand: null,
|
|
4161
4407
|
tailscale: false,
|
|
4162
|
-
local: false
|
|
4408
|
+
local: false,
|
|
4409
|
+
hubInfo: false,
|
|
4410
|
+
hubStop: false,
|
|
4411
|
+
hubStatus: false
|
|
4163
4412
|
};
|
|
4164
4413
|
const args = argv.slice(2);
|
|
4165
4414
|
if (args.length > 0 && args[0] === "setup") {
|
|
4166
4415
|
options.subcommand = "setup";
|
|
4167
4416
|
return options;
|
|
4168
4417
|
}
|
|
4418
|
+
if (args.length > 0 && args[0] === "hub") {
|
|
4419
|
+
const hubAction = args[1] || "info";
|
|
4420
|
+
if (hubAction === "info") options.hubInfo = true;
|
|
4421
|
+
else if (hubAction === "stop") options.hubStop = true;
|
|
4422
|
+
else if (hubAction === "status") options.hubStatus = true;
|
|
4423
|
+
else options.hubInfo = true;
|
|
4424
|
+
return options;
|
|
4425
|
+
}
|
|
4169
4426
|
let i = 0;
|
|
4170
4427
|
while (i < args.length) {
|
|
4171
4428
|
const arg = args[i];
|
|
@@ -4187,6 +4444,15 @@ function parseArgs(argv) {
|
|
|
4187
4444
|
} else if (arg === "--no-qr") {
|
|
4188
4445
|
options.noQr = true;
|
|
4189
4446
|
i++;
|
|
4447
|
+
} else if (arg === "--hub-info") {
|
|
4448
|
+
options.hubInfo = true;
|
|
4449
|
+
i++;
|
|
4450
|
+
} else if (arg === "--hub-stop") {
|
|
4451
|
+
options.hubStop = true;
|
|
4452
|
+
i++;
|
|
4453
|
+
} else if (arg === "--hub-status") {
|
|
4454
|
+
options.hubStatus = true;
|
|
4455
|
+
i++;
|
|
4190
4456
|
} else if (arg === "--help" || arg === "-h") {
|
|
4191
4457
|
printHelp();
|
|
4192
4458
|
process.exit(0);
|
|
@@ -4208,6 +4474,7 @@ Usage:
|
|
|
4208
4474
|
itwillsync [options] -- <command> [args...]
|
|
4209
4475
|
itwillsync [options] <command> [args...]
|
|
4210
4476
|
itwillsync setup
|
|
4477
|
+
itwillsync hub [info|stop|status]
|
|
4211
4478
|
|
|
4212
4479
|
Examples:
|
|
4213
4480
|
itwillsync -- claude
|
|
@@ -4216,9 +4483,14 @@ Examples:
|
|
|
4216
4483
|
itwillsync --port 8080 -- claude
|
|
4217
4484
|
itwillsync --tailscale -- claude
|
|
4218
4485
|
itwillsync setup
|
|
4486
|
+
itwillsync hub info
|
|
4487
|
+
itwillsync hub stop
|
|
4219
4488
|
|
|
4220
4489
|
Commands:
|
|
4221
4490
|
setup Run the setup wizard (configure networking mode)
|
|
4491
|
+
hub info Show dashboard URL, QR code, and hub status
|
|
4492
|
+
hub stop Stop the hub daemon and all sessions
|
|
4493
|
+
hub status List all active sessions
|
|
4222
4494
|
|
|
4223
4495
|
Options:
|
|
4224
4496
|
--port <number> Port to listen on (default: ${DEFAULT_PORT})
|
|
@@ -4228,24 +4500,29 @@ Options:
|
|
|
4228
4500
|
--no-qr Don't display QR code
|
|
4229
4501
|
-h, --help Show this help
|
|
4230
4502
|
-v, --version Show version
|
|
4503
|
+
|
|
4504
|
+
Hub Management:
|
|
4505
|
+
--hub-info Show dashboard URL, QR code, and hub status
|
|
4506
|
+
--hub-stop Stop the hub daemon and all sessions
|
|
4507
|
+
--hub-status List all active sessions
|
|
4231
4508
|
`);
|
|
4232
4509
|
}
|
|
4233
4510
|
|
|
4234
4511
|
// src/index.ts
|
|
4235
|
-
import { fileURLToPath as
|
|
4236
|
-
import { join as
|
|
4237
|
-
import { spawn as
|
|
4512
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4513
|
+
import { join as join5, dirname as dirname3 } from "path";
|
|
4514
|
+
import { spawn as spawn3 } from "child_process";
|
|
4238
4515
|
function preventSleep() {
|
|
4239
4516
|
try {
|
|
4240
4517
|
if (process.platform === "darwin") {
|
|
4241
|
-
const child =
|
|
4518
|
+
const child = spawn3("caffeinate", ["-i", "-w", String(process.pid)], {
|
|
4242
4519
|
stdio: "ignore",
|
|
4243
4520
|
detached: true
|
|
4244
4521
|
});
|
|
4245
4522
|
child.unref();
|
|
4246
4523
|
return child;
|
|
4247
4524
|
} else if (process.platform === "linux") {
|
|
4248
|
-
return
|
|
4525
|
+
return spawn3("systemd-inhibit", [
|
|
4249
4526
|
"--what=idle",
|
|
4250
4527
|
"--who=itwillsync",
|
|
4251
4528
|
"--why=Terminal sync session active",
|
|
@@ -4257,12 +4534,96 @@ function preventSleep() {
|
|
|
4257
4534
|
}
|
|
4258
4535
|
return null;
|
|
4259
4536
|
}
|
|
4537
|
+
async function ensureHub() {
|
|
4538
|
+
const hubRunning = await discoverHub();
|
|
4539
|
+
if (hubRunning) {
|
|
4540
|
+
return false;
|
|
4541
|
+
}
|
|
4542
|
+
try {
|
|
4543
|
+
await spawnHub();
|
|
4544
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
4545
|
+
return true;
|
|
4546
|
+
} catch (err) {
|
|
4547
|
+
console.warn(`
|
|
4548
|
+
Warning: Could not start hub daemon: ${err.message}`);
|
|
4549
|
+
console.warn(" Running in standalone mode (no dashboard).\n");
|
|
4550
|
+
return true;
|
|
4551
|
+
}
|
|
4552
|
+
}
|
|
4553
|
+
async function handleHubCommand(options) {
|
|
4554
|
+
const hubConfig = getHubConfig();
|
|
4555
|
+
const hubRunning = await discoverHub();
|
|
4556
|
+
if (options.hubStop) {
|
|
4557
|
+
if (!hubRunning || !hubConfig) {
|
|
4558
|
+
console.log("\n No hub daemon is running.\n");
|
|
4559
|
+
return;
|
|
4560
|
+
}
|
|
4561
|
+
const stopped = stopHub();
|
|
4562
|
+
if (stopped) {
|
|
4563
|
+
console.log("\n Hub daemon stopped.\n");
|
|
4564
|
+
} else {
|
|
4565
|
+
console.log("\n Failed to stop hub daemon.\n");
|
|
4566
|
+
}
|
|
4567
|
+
return;
|
|
4568
|
+
}
|
|
4569
|
+
if (options.hubStatus) {
|
|
4570
|
+
if (!hubRunning) {
|
|
4571
|
+
console.log("\n No hub daemon is running.\n");
|
|
4572
|
+
return;
|
|
4573
|
+
}
|
|
4574
|
+
const sessions = await listSessions();
|
|
4575
|
+
console.log(`
|
|
4576
|
+
Hub is running. ${sessions.length} active session(s).
|
|
4577
|
+
`);
|
|
4578
|
+
if (sessions.length > 0) {
|
|
4579
|
+
for (const s of sessions) {
|
|
4580
|
+
const uptime = Math.floor((Date.now() - s.connectedAt) / 6e4);
|
|
4581
|
+
console.log(` ${s.name || s.agent} (${s.status}, ${uptime}m, port ${s.port})`);
|
|
4582
|
+
}
|
|
4583
|
+
console.log("");
|
|
4584
|
+
}
|
|
4585
|
+
return;
|
|
4586
|
+
}
|
|
4587
|
+
if (options.hubInfo) {
|
|
4588
|
+
if (!hubRunning || !hubConfig) {
|
|
4589
|
+
console.log("\n No hub daemon is running.");
|
|
4590
|
+
console.log(" Start a session with: itwillsync -- <agent>\n");
|
|
4591
|
+
return;
|
|
4592
|
+
}
|
|
4593
|
+
let networkingMode = "local";
|
|
4594
|
+
if (options.tailscale) {
|
|
4595
|
+
networkingMode = "tailscale";
|
|
4596
|
+
} else if (options.local) {
|
|
4597
|
+
networkingMode = "local";
|
|
4598
|
+
} else if (configExists()) {
|
|
4599
|
+
networkingMode = loadConfig().networkingMode;
|
|
4600
|
+
}
|
|
4601
|
+
const ip = await resolveSessionIP(networkingMode, false);
|
|
4602
|
+
const dashboardUrl = `http://${ip}:${HUB_EXTERNAL_PORT}?token=${hubConfig.masterToken}`;
|
|
4603
|
+
displayQR(dashboardUrl);
|
|
4604
|
+
console.log(` Dashboard: ${dashboardUrl}`);
|
|
4605
|
+
const sessions = await listSessions();
|
|
4606
|
+
console.log(` Sessions: ${sessions.length} active`);
|
|
4607
|
+
if (sessions.length > 0) {
|
|
4608
|
+
for (const s of sessions) {
|
|
4609
|
+
const uptime = Math.floor((Date.now() - s.connectedAt) / 6e4);
|
|
4610
|
+
console.log(` ${s.name || s.agent} (${s.status}, ${uptime}m)`);
|
|
4611
|
+
}
|
|
4612
|
+
}
|
|
4613
|
+
console.log("");
|
|
4614
|
+
return;
|
|
4615
|
+
}
|
|
4616
|
+
}
|
|
4260
4617
|
async function main() {
|
|
4261
4618
|
const options = parseArgs(process.argv);
|
|
4262
4619
|
if (options.subcommand === "setup") {
|
|
4263
4620
|
await runSetupWizard();
|
|
4264
4621
|
return;
|
|
4265
4622
|
}
|
|
4623
|
+
if (options.hubInfo || options.hubStop || options.hubStatus) {
|
|
4624
|
+
await handleHubCommand(options);
|
|
4625
|
+
return;
|
|
4626
|
+
}
|
|
4266
4627
|
if (options.tailscale && options.local) {
|
|
4267
4628
|
console.error("Error: Cannot use both --tailscale and --local.\n");
|
|
4268
4629
|
process.exit(1);
|
|
@@ -4285,27 +4646,59 @@ async function main() {
|
|
|
4285
4646
|
networkingMode = config.networkingMode;
|
|
4286
4647
|
}
|
|
4287
4648
|
const [cmd, ...cmdArgs] = options.command;
|
|
4649
|
+
const isFirstSession = await ensureHub();
|
|
4650
|
+
const hubConfig = getHubConfig();
|
|
4288
4651
|
const token = generateToken();
|
|
4289
4652
|
const port = await findAvailablePort(options.port);
|
|
4290
4653
|
const host = options.localhost ? "127.0.0.1" : "0.0.0.0";
|
|
4291
4654
|
const ip = await resolveSessionIP(networkingMode, options.localhost);
|
|
4292
|
-
const
|
|
4293
|
-
const
|
|
4294
|
-
const webClientPath = join4(__dirname, "web-client");
|
|
4655
|
+
const __dirname = dirname3(fileURLToPath3(import.meta.url));
|
|
4656
|
+
const webClientPath = join5(__dirname, "web-client");
|
|
4295
4657
|
const ptyManager = new PtyManager(cmd, cmdArgs);
|
|
4296
4658
|
const server = createSyncServer({
|
|
4297
4659
|
ptyManager,
|
|
4298
4660
|
token,
|
|
4299
4661
|
webClientPath,
|
|
4300
4662
|
host,
|
|
4301
|
-
port
|
|
4663
|
+
port,
|
|
4664
|
+
localTerminalOwnsResize: true
|
|
4302
4665
|
});
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4666
|
+
let registeredSession = null;
|
|
4667
|
+
let heartbeatInterval = null;
|
|
4668
|
+
if (hubConfig) {
|
|
4669
|
+
try {
|
|
4670
|
+
registeredSession = await registerSession({
|
|
4671
|
+
name: cmd,
|
|
4672
|
+
port,
|
|
4673
|
+
token,
|
|
4674
|
+
agent: cmd,
|
|
4675
|
+
cwd: process.cwd(),
|
|
4676
|
+
pid: ptyManager.pid
|
|
4677
|
+
});
|
|
4678
|
+
heartbeatInterval = setInterval(() => {
|
|
4679
|
+
if (registeredSession) {
|
|
4680
|
+
sendHeartbeat(registeredSession.id);
|
|
4681
|
+
}
|
|
4682
|
+
}, 1e4);
|
|
4683
|
+
} catch (err) {
|
|
4684
|
+
console.warn(` Warning: Failed to register with hub: ${err.message}`);
|
|
4685
|
+
}
|
|
4686
|
+
}
|
|
4687
|
+
const dashboardUrl = hubConfig ? `http://${ip}:${HUB_EXTERNAL_PORT}?token=${hubConfig.masterToken}` : null;
|
|
4688
|
+
if (isFirstSession && dashboardUrl && !options.noQr) {
|
|
4689
|
+
displayQR(dashboardUrl);
|
|
4690
|
+
console.log(` Dashboard: ${dashboardUrl}`);
|
|
4691
|
+
} else if (isFirstSession && !options.noQr) {
|
|
4692
|
+
const directUrl = `http://${ip}:${port}?token=${token}`;
|
|
4693
|
+
displayQR(directUrl);
|
|
4694
|
+
} else if (dashboardUrl) {
|
|
4306
4695
|
console.log(`
|
|
4307
|
-
|
|
4308
|
-
`);
|
|
4696
|
+
Session "${cmd}" registered with hub.`);
|
|
4697
|
+
console.log(` Dashboard: ${dashboardUrl}`);
|
|
4698
|
+
console.log("");
|
|
4699
|
+
} else if (!options.noQr) {
|
|
4700
|
+
const directUrl = `http://${ip}:${port}?token=${token}`;
|
|
4701
|
+
displayQR(directUrl);
|
|
4309
4702
|
}
|
|
4310
4703
|
const sleepGuard = preventSleep();
|
|
4311
4704
|
console.log(` Server listening on ${host}:${port}`);
|
|
@@ -4327,30 +4720,37 @@ async function main() {
|
|
|
4327
4720
|
function handleResize() {
|
|
4328
4721
|
if (process.stdout.columns && process.stdout.rows) {
|
|
4329
4722
|
ptyManager.resize(process.stdout.columns, process.stdout.rows);
|
|
4723
|
+
server.broadcastResize(process.stdout.columns, process.stdout.rows);
|
|
4330
4724
|
}
|
|
4331
4725
|
}
|
|
4332
4726
|
process.stdout.on("resize", handleResize);
|
|
4333
4727
|
handleResize();
|
|
4334
|
-
function cleanup() {
|
|
4728
|
+
async function cleanup() {
|
|
4335
4729
|
if (process.stdin.isTTY) {
|
|
4336
4730
|
process.stdin.setRawMode(false);
|
|
4337
4731
|
}
|
|
4732
|
+
if (heartbeatInterval) {
|
|
4733
|
+
clearInterval(heartbeatInterval);
|
|
4734
|
+
}
|
|
4735
|
+
if (registeredSession) {
|
|
4736
|
+
await unregisterSession(registeredSession.id);
|
|
4737
|
+
}
|
|
4338
4738
|
sleepGuard?.kill();
|
|
4339
4739
|
server.close();
|
|
4340
4740
|
ptyManager.kill();
|
|
4341
4741
|
}
|
|
4342
|
-
ptyManager.onExit((exitCode) => {
|
|
4742
|
+
ptyManager.onExit(async (exitCode) => {
|
|
4343
4743
|
console.log(`
|
|
4344
4744
|
Agent exited with code ${exitCode}`);
|
|
4345
|
-
cleanup();
|
|
4745
|
+
await cleanup();
|
|
4346
4746
|
process.exit(exitCode);
|
|
4347
4747
|
});
|
|
4348
|
-
process.on("SIGINT", () => {
|
|
4349
|
-
cleanup();
|
|
4748
|
+
process.on("SIGINT", async () => {
|
|
4749
|
+
await cleanup();
|
|
4350
4750
|
process.exit(0);
|
|
4351
4751
|
});
|
|
4352
|
-
process.on("SIGTERM", () => {
|
|
4353
|
-
cleanup();
|
|
4752
|
+
process.on("SIGTERM", async () => {
|
|
4753
|
+
await cleanup();
|
|
4354
4754
|
process.exit(0);
|
|
4355
4755
|
});
|
|
4356
4756
|
}
|