@rajat-rastogi/maestro 0.1.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1805 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (let key of __getOwnPropNames(from))
11
+ if (!__hasOwnProp.call(to, key) && key !== except)
12
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
+ }
14
+ return to;
15
+ };
16
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
+ // If the importer is in node compatibility mode or this is not an ESM
18
+ // file that has been converted to a CommonJS file using a Babel-
19
+ // compatible transform (i.e. "__esModule" has not been set), then set
20
+ // "default" to the CommonJS "module.exports" for node compatibility.
21
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
+ mod
23
+ ));
24
+ const electron = require("electron");
25
+ const path = require("path");
26
+ const fs = require("fs");
27
+ const child_process = require("child_process");
28
+ const net = require("net");
29
+ const os = require("os");
30
+ const ini = require("ini");
31
+ const crypto = require("crypto");
32
+ const WebSocket = require("ws");
33
+ const util = require("util");
34
+ const pty = require("node-pty");
35
+ const ssh2 = require("ssh2");
36
+ function _interopNamespaceDefault(e) {
37
+ const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
38
+ if (e) {
39
+ for (const k in e) {
40
+ if (k !== "default") {
41
+ const d = Object.getOwnPropertyDescriptor(e, k);
42
+ Object.defineProperty(n, k, d.get ? d : {
43
+ enumerable: true,
44
+ get: () => e[k]
45
+ });
46
+ }
47
+ }
48
+ }
49
+ n.default = e;
50
+ return Object.freeze(n);
51
+ }
52
+ const path__namespace = /* @__PURE__ */ _interopNamespaceDefault(path);
53
+ const fs__namespace = /* @__PURE__ */ _interopNamespaceDefault(fs);
54
+ const net__namespace = /* @__PURE__ */ _interopNamespaceDefault(net);
55
+ const os__namespace = /* @__PURE__ */ _interopNamespaceDefault(os);
56
+ const pty__namespace = /* @__PURE__ */ _interopNamespaceDefault(pty);
57
+ function extractPreview(content) {
58
+ const lines = content.split("\n");
59
+ for (const line of lines) {
60
+ const trimmed = line.trim();
61
+ if (trimmed && !trimmed.startsWith("#") && !trimmed.startsWith("---")) {
62
+ return trimmed.slice(0, 120);
63
+ }
64
+ }
65
+ return "";
66
+ }
67
+ function registerPlanIpc() {
68
+ electron.ipcMain.handle("plan:list", async (_event, dir) => {
69
+ try {
70
+ const entries = fs__namespace.readdirSync(dir, { withFileTypes: true });
71
+ const plans = [];
72
+ for (const entry of entries) {
73
+ if (entry.isFile() && entry.name.endsWith(".md")) {
74
+ const filePath = path__namespace.join(dir, entry.name);
75
+ try {
76
+ const stat = fs__namespace.statSync(filePath);
77
+ const content = fs__namespace.readFileSync(filePath, "utf8");
78
+ plans.push({
79
+ name: entry.name.replace(/\.md$/, ""),
80
+ filePath,
81
+ mtime: stat.mtimeMs,
82
+ preview: extractPreview(content)
83
+ });
84
+ } catch {
85
+ }
86
+ }
87
+ }
88
+ return plans.sort((a, b) => b.mtime - a.mtime);
89
+ } catch {
90
+ return [];
91
+ }
92
+ });
93
+ electron.ipcMain.handle("plan:openDialog", async (_event) => {
94
+ const win = electron.BrowserWindow.getFocusedWindow();
95
+ const result = await electron.dialog.showOpenDialog(win, {
96
+ title: "Open Plan File",
97
+ filters: [{ name: "Markdown Plans", extensions: ["md"] }],
98
+ properties: ["openFile"]
99
+ });
100
+ return result.canceled ? null : result.filePaths[0];
101
+ });
102
+ electron.ipcMain.handle("plan:read", async (_event, filePath) => {
103
+ try {
104
+ return fs__namespace.readFileSync(filePath, "utf8");
105
+ } catch {
106
+ return null;
107
+ }
108
+ });
109
+ electron.ipcMain.handle("plan:defaultDir", async (_event) => {
110
+ const cwd = process.cwd();
111
+ const candidates = [
112
+ path__namespace.join(cwd, "docs", "plans"),
113
+ path__namespace.join(cwd, "plans"),
114
+ cwd
115
+ ];
116
+ for (const c of candidates) {
117
+ if (fs__namespace.existsSync(c)) return c;
118
+ }
119
+ return cwd;
120
+ });
121
+ }
122
+ function resolveMaestro() {
123
+ const repoRoot = path__namespace.resolve(__dirname, "..", "..", "..");
124
+ const distMain = path__namespace.join(repoRoot, "dist", "main.js");
125
+ if (fs__namespace.existsSync(distMain)) {
126
+ return { cmd: process.execPath, leadArgs: [distMain] };
127
+ }
128
+ const binDir = path__namespace.join(repoRoot, "node_modules", ".bin");
129
+ const winBin = path__namespace.join(binDir, "maestro.cmd");
130
+ const unixBin = path__namespace.join(binDir, "maestro");
131
+ if (process.platform === "win32" && fs__namespace.existsSync(winBin)) {
132
+ return { cmd: winBin, leadArgs: [] };
133
+ }
134
+ if (process.platform !== "win32" && fs__namespace.existsSync(unixBin)) {
135
+ return { cmd: unixBin, leadArgs: [] };
136
+ }
137
+ const parentDist = path__namespace.join(repoRoot, "..", "dist", "main.js");
138
+ if (fs__namespace.existsSync(parentDist)) {
139
+ return { cmd: process.execPath, leadArgs: [path__namespace.resolve(parentDist)] };
140
+ }
141
+ throw new Error(
142
+ `Cannot find the maestro CLI.
143
+ Expected: ${distMain}
144
+ Run "npm run build" in the repo root first, or install maestro globally.`
145
+ );
146
+ }
147
+ function allocateFreePort() {
148
+ return new Promise((resolve, reject) => {
149
+ const server = net__namespace.createServer();
150
+ server.listen(0, "127.0.0.1", () => {
151
+ const addr = server.address();
152
+ if (!addr || typeof addr === "string") {
153
+ server.close();
154
+ reject(new Error("Failed to get port"));
155
+ return;
156
+ }
157
+ const port = addr.port;
158
+ server.close(() => resolve(port));
159
+ });
160
+ server.on("error", reject);
161
+ });
162
+ }
163
+ const registry = /* @__PURE__ */ new Map();
164
+ function getActiveProcess(role) {
165
+ return registry.get(role);
166
+ }
167
+ function killProcess(role) {
168
+ const entry = registry.get(role);
169
+ if (entry) {
170
+ if (!entry.process.killed) {
171
+ entry.process.kill("SIGTERM");
172
+ setTimeout(() => {
173
+ if (!entry.process.killed) entry.process.kill("SIGKILL");
174
+ }, 3e3);
175
+ }
176
+ registry.delete(role);
177
+ }
178
+ }
179
+ function spawnMaestro(opts) {
180
+ killProcess(opts.role);
181
+ const { cmd, leadArgs } = resolveMaestro();
182
+ const allArgs = [...leadArgs, ...opts.args];
183
+ const child = child_process.spawn(cmd, allArgs, {
184
+ stdio: ["pipe", "pipe", "pipe"],
185
+ // Inherit the shell PATH so subprocesses can find tools
186
+ env: { ...process.env },
187
+ // Use cmd.exe shell on Windows only when running a .cmd file
188
+ shell: cmd.endsWith(".cmd")
189
+ });
190
+ const handle = {
191
+ process: child,
192
+ port: opts.port,
193
+ kill() {
194
+ if (!child.killed) {
195
+ child.kill("SIGTERM");
196
+ setTimeout(() => {
197
+ if (!child.killed) child.kill("SIGKILL");
198
+ }, 3e3);
199
+ }
200
+ }
201
+ };
202
+ registry.set(opts.role, handle);
203
+ if (opts.onData) {
204
+ const handler = opts.onData;
205
+ let buf = "";
206
+ const processChunk = (chunk) => {
207
+ buf += chunk.toString();
208
+ const lines = buf.split("\n");
209
+ buf = lines.pop() ?? "";
210
+ lines.forEach((l) => handler(l));
211
+ };
212
+ child.stdout?.on("data", processChunk);
213
+ child.stderr?.on("data", processChunk);
214
+ }
215
+ if (opts.onExit) {
216
+ child.on("exit", opts.onExit);
217
+ }
218
+ child.on("exit", () => {
219
+ if (registry.get(opts.role) === handle) {
220
+ registry.delete(opts.role);
221
+ }
222
+ });
223
+ return handle;
224
+ }
225
+ const ROLE$2 = "run";
226
+ function registerRunIpc(getMainWindow) {
227
+ electron.ipcMain.handle("run:start", async (_event, planFile, opts) => {
228
+ const port = await allocateFreePort();
229
+ const args = [planFile, "--serve", "-p", String(port)];
230
+ if (opts.provider) args.push("--provider", opts.provider);
231
+ if (opts.maxIterations) args.push("-m", String(opts.maxIterations));
232
+ if (opts.worktree) args.push("--worktree");
233
+ if (opts.tasksOnly) args.push("--tasks-only");
234
+ if (opts.reviewOnly) args.push("--review");
235
+ spawnMaestro({
236
+ role: ROLE$2,
237
+ args,
238
+ port,
239
+ onData(line) {
240
+ const win = getMainWindow();
241
+ win?.webContents.send("run:log", line);
242
+ },
243
+ onExit(code) {
244
+ const win = getMainWindow();
245
+ win?.webContents.send("run:exit", code);
246
+ }
247
+ });
248
+ return port;
249
+ });
250
+ electron.ipcMain.handle("run:stop", async () => {
251
+ killProcess(ROLE$2);
252
+ });
253
+ electron.ipcMain.handle("run:status", async () => {
254
+ const proc = getActiveProcess(ROLE$2);
255
+ return proc ? "running" : "idle";
256
+ });
257
+ electron.ipcMain.handle("run:stdin", async (_event, data) => {
258
+ const proc = getActiveProcess(ROLE$2);
259
+ if (proc?.process.stdin) {
260
+ proc.process.stdin.write(data + "\n");
261
+ }
262
+ });
263
+ }
264
+ const ROLE$1 = "replay";
265
+ function registerReplayIpc() {
266
+ electron.ipcMain.handle("replay:start", async () => {
267
+ const port = await allocateFreePort();
268
+ spawnMaestro({
269
+ role: ROLE$1,
270
+ args: ["--replay", "-p", String(port)],
271
+ port
272
+ });
273
+ await new Promise((resolve) => setTimeout(resolve, 1500));
274
+ return port;
275
+ });
276
+ electron.ipcMain.handle("replay:stop", async () => {
277
+ killProcess(ROLE$1);
278
+ });
279
+ electron.ipcMain.handle("replay:status", async () => {
280
+ return getActiveProcess(ROLE$1) ? "running" : "idle";
281
+ });
282
+ }
283
+ const DEFAULTS = {
284
+ ai_provider: "copilot",
285
+ claude_command: "claude",
286
+ claude_args: "--dangerously-skip-permissions --output-format stream-json --verbose",
287
+ claude_error_patterns: ["You've hit your limit", "API Error:", "cannot be launched inside another Claude Code session"],
288
+ copilot_command: "copilot",
289
+ copilot_args: "--autopilot --allow-all",
290
+ copilot_error_patterns: ["authentication required", "Copilot is not enabled", "rate limit exceeded"],
291
+ max_iterations: 50,
292
+ max_external_iterations: 0,
293
+ max_diff_chars: 5e4,
294
+ iteration_delay_ms: 2e3,
295
+ task_retry_count: 1,
296
+ validation_timeout_ms: 6e4,
297
+ finalize_enabled: false,
298
+ use_worktree: false,
299
+ plans_dir: "docs/plans",
300
+ default_branch: "",
301
+ vcs_command: "git",
302
+ push_on_complete: false,
303
+ watch_dirs: [],
304
+ port: 8080,
305
+ color_task: "#00ff00",
306
+ color_review: "#00ffff",
307
+ color_claude_eval: "#64c8ff",
308
+ color_warn: "#ffff00",
309
+ color_error: "#ff0000",
310
+ color_signal: "#ff6464",
311
+ color_timestamp: "#8a8a8a",
312
+ color_info: "#b4b4b4"
313
+ };
314
+ const BOOLEAN_KEYS = /* @__PURE__ */ new Set(["finalize_enabled", "use_worktree", "push_on_complete"]);
315
+ const NUMBER_KEYS = /* @__PURE__ */ new Set([
316
+ "max_iterations",
317
+ "max_external_iterations",
318
+ "max_diff_chars",
319
+ "iteration_delay_ms",
320
+ "task_retry_count",
321
+ "validation_timeout_ms",
322
+ "port"
323
+ ]);
324
+ const ARRAY_KEYS = /* @__PURE__ */ new Set(["claude_error_patterns", "copilot_error_patterns", "watch_dirs"]);
325
+ function resolveConfigDir() {
326
+ return process.env["MAESTRO_CONFIG_DIR"] ?? path__namespace.join(os__namespace.homedir(), ".config", "maestro");
327
+ }
328
+ function parseConfigFile(filePath) {
329
+ if (!fs__namespace.existsSync(filePath)) return {};
330
+ try {
331
+ const raw = fs__namespace.readFileSync(filePath, "utf8");
332
+ const parsed = ini.parse(raw);
333
+ const result = {};
334
+ for (const [key, raw2] of Object.entries(parsed)) {
335
+ if (!(key in DEFAULTS)) continue;
336
+ if (BOOLEAN_KEYS.has(key)) {
337
+ result[key] = raw2 === "true";
338
+ } else if (NUMBER_KEYS.has(key)) {
339
+ result[key] = parseInt(raw2) || 0;
340
+ } else if (ARRAY_KEYS.has(key)) {
341
+ result[key] = String(raw2).split(",").map((s) => s.trim()).filter(Boolean);
342
+ } else {
343
+ result[key] = raw2;
344
+ }
345
+ }
346
+ return result;
347
+ } catch {
348
+ return {};
349
+ }
350
+ }
351
+ function mergeConfig(...layers) {
352
+ const result = { ...DEFAULTS };
353
+ for (const layer of layers) {
354
+ for (const [k, v] of Object.entries(layer)) {
355
+ if (v !== void 0 && v !== null) result[k] = v;
356
+ }
357
+ }
358
+ return result;
359
+ }
360
+ function buildState(globalPath, projectPath) {
361
+ const globalOverrides = parseConfigFile(globalPath);
362
+ const projectOverrides = parseConfigFile(projectPath);
363
+ const merged = mergeConfig(globalOverrides, projectOverrides);
364
+ return {
365
+ merged,
366
+ defaults: DEFAULTS,
367
+ globalOverrides,
368
+ projectOverrides,
369
+ globalConfigPath: globalPath,
370
+ projectConfigPath: projectPath,
371
+ globalExists: fs__namespace.existsSync(globalPath),
372
+ projectExists: fs__namespace.existsSync(projectPath)
373
+ };
374
+ }
375
+ function configToIni(overrides) {
376
+ const lines = [];
377
+ for (const [key, value] of Object.entries(overrides)) {
378
+ if (value === void 0 || value === null) continue;
379
+ if (Array.isArray(value)) {
380
+ lines.push(`${key} = ${value.join(", ")}`);
381
+ } else {
382
+ lines.push(`${key} = ${String(value)}`);
383
+ }
384
+ }
385
+ return lines.join("\n") + "\n";
386
+ }
387
+ function registerConfigIpc() {
388
+ electron.ipcMain.handle("config:load", async () => {
389
+ const configDir = resolveConfigDir();
390
+ const globalPath = path__namespace.join(configDir, "config");
391
+ const projectPath = path__namespace.join(process.cwd(), ".maestro", "config");
392
+ return buildState(globalPath, projectPath);
393
+ });
394
+ electron.ipcMain.handle("config:save", async (_event, overrides, target) => {
395
+ const configDir = resolveConfigDir();
396
+ const targetPath = target === "project" ? path__namespace.join(process.cwd(), ".maestro", "config") : path__namespace.join(configDir, "config");
397
+ fs__namespace.mkdirSync(path__namespace.dirname(targetPath), { recursive: true });
398
+ const existing = parseConfigFile(targetPath);
399
+ const merged = { ...existing, ...overrides };
400
+ const toWrite = {};
401
+ for (const [k, v] of Object.entries(merged)) {
402
+ const def = DEFAULTS[k];
403
+ if (Array.isArray(def) && Array.isArray(v)) {
404
+ if (JSON.stringify(v) !== JSON.stringify(def)) toWrite[k] = v;
405
+ } else if (v !== def) {
406
+ toWrite[k] = v;
407
+ }
408
+ }
409
+ fs__namespace.writeFileSync(targetPath, configToIni(toWrite), "utf8");
410
+ return { ok: true };
411
+ });
412
+ }
413
+ const ROLE = "creator";
414
+ function registerCreatorIpc(getMainWindow) {
415
+ electron.ipcMain.handle("creator:start", async (_event, description) => {
416
+ const port = await allocateFreePort();
417
+ const args = ["--plan", description, "--serve", "-p", String(port)];
418
+ spawnMaestro({
419
+ role: ROLE,
420
+ args,
421
+ port,
422
+ onData(line) {
423
+ const win = getMainWindow();
424
+ win?.webContents.send("creator:log", line);
425
+ },
426
+ onExit(code) {
427
+ const win = getMainWindow();
428
+ win?.webContents.send("creator:exit", code);
429
+ }
430
+ });
431
+ return port;
432
+ });
433
+ electron.ipcMain.handle("creator:stdin", async (_event, data) => {
434
+ const proc = getActiveProcess(ROLE);
435
+ if (proc?.process.stdin) {
436
+ proc.process.stdin.write(data + "\n");
437
+ }
438
+ });
439
+ electron.ipcMain.handle("creator:stop", async () => {
440
+ killProcess(ROLE);
441
+ });
442
+ electron.ipcMain.handle("creator:status", async () => {
443
+ return getActiveProcess(ROLE) ? "running" : "idle";
444
+ });
445
+ }
446
+ function registerMachinesIpc(machineManager2) {
447
+ electron.ipcMain.handle("machines:list", async () => {
448
+ return machineManager2.list();
449
+ });
450
+ electron.ipcMain.handle("machines:add", async (_e, partial) => {
451
+ return machineManager2.add(partial);
452
+ });
453
+ electron.ipcMain.handle("machines:remove", async (_e, id) => {
454
+ machineManager2.remove(id);
455
+ });
456
+ electron.ipcMain.handle("machines:ping", async (_e, id) => {
457
+ return machineManager2.ping(id);
458
+ });
459
+ electron.ipcMain.handle("machines:update", async (_e, id, partial) => {
460
+ return machineManager2.update(id, partial);
461
+ });
462
+ electron.ipcMain.handle("machines:updateAddress", async (_e, id, address) => {
463
+ return machineManager2.updateAddress(id, address);
464
+ });
465
+ electron.ipcMain.handle("machines:discoverDevBoxes", async () => {
466
+ return machineManager2.discoverDevBoxes();
467
+ });
468
+ electron.ipcMain.handle("machines:launchRdp", async (_e, id) => {
469
+ return machineManager2.launchRdp(id);
470
+ });
471
+ }
472
+ function registerSessionsIpc(sessionManager2) {
473
+ electron.ipcMain.handle("sessions:list", async () => {
474
+ return sessionManager2.list();
475
+ });
476
+ electron.ipcMain.handle("sessions:getBuffer", async (_e, id) => {
477
+ return sessionManager2.getBuffer(id);
478
+ });
479
+ electron.ipcMain.handle("sessions:create", async (_e, opts) => {
480
+ return sessionManager2.create(opts);
481
+ });
482
+ electron.ipcMain.handle("sessions:destroy", async (_e, id) => {
483
+ sessionManager2.destroy(id);
484
+ });
485
+ electron.ipcMain.handle("sessions:rename", async (_e, id, name) => {
486
+ sessionManager2.rename(id, name);
487
+ });
488
+ electron.ipcMain.handle("sessions:updateTags", async (_e, id, tags) => {
489
+ sessionManager2.updateTags(id, tags);
490
+ });
491
+ electron.ipcMain.handle("sessions:sendInput", async (_e, id, data) => {
492
+ sessionManager2.sendInput(id, data);
493
+ });
494
+ electron.ipcMain.handle("sessions:resize", async (_e, id, cols, rows) => {
495
+ sessionManager2.resize(id, cols, rows);
496
+ });
497
+ }
498
+ class AgentConnectionPool {
499
+ static instance = new AgentConnectionPool();
500
+ static getInstance() {
501
+ return AgentConnectionPool.instance;
502
+ }
503
+ pool = /* @__PURE__ */ new Map();
504
+ wingetEnv() {
505
+ const env = { ...process.env };
506
+ const local = env.LOCALAPPDATA ?? "";
507
+ if (local) env.PATH = `${local}\\Microsoft\\WinGet\\Links;${env.PATH ?? ""}`;
508
+ return env;
509
+ }
510
+ /** Get a ready WebSocket for tunnelId, spawning devtunnel connect if needed. */
511
+ async getConnection(tunnelId) {
512
+ const existing = this.pool.get(tunnelId);
513
+ if (existing) {
514
+ if (existing.ready && existing.ws?.readyState === WebSocket.OPEN) {
515
+ return existing.ws;
516
+ }
517
+ return new Promise((resolve) => existing.queue.push(resolve));
518
+ }
519
+ const entry = { proc: null, ws: null, ready: false, queue: [] };
520
+ this.pool.set(tunnelId, entry);
521
+ entry.proc = child_process.spawn("devtunnel", ["connect", tunnelId], { shell: true, env: this.wingetEnv() });
522
+ entry.proc.on("error", () => this.teardown(tunnelId));
523
+ entry.proc.on("exit", () => this.teardown(tunnelId));
524
+ await new Promise((r) => setTimeout(r, 4e3));
525
+ const ws = new WebSocket("ws://127.0.0.1:9741");
526
+ entry.ws = ws;
527
+ return new Promise((resolve, reject) => {
528
+ ws.on("open", () => {
529
+ entry.ready = true;
530
+ entry.queue.forEach((cb) => cb(ws));
531
+ entry.queue = [];
532
+ resolve(ws);
533
+ });
534
+ ws.on("error", () => {
535
+ this.teardown(tunnelId);
536
+ reject(new Error("agent ws error"));
537
+ });
538
+ ws.on("close", () => this.teardown(tunnelId));
539
+ });
540
+ }
541
+ /** Ping the agent via WebSocket, return true if pong received within 5s. */
542
+ async ping(tunnelId) {
543
+ try {
544
+ const ws = await this.getConnection(tunnelId);
545
+ return new Promise((resolve) => {
546
+ const t = setTimeout(() => {
547
+ ws.removeListener("message", handler);
548
+ resolve(false);
549
+ }, 5e3);
550
+ const handler = (data) => {
551
+ try {
552
+ const msg = JSON.parse(data.toString());
553
+ if (msg.type === "pong") {
554
+ clearTimeout(t);
555
+ ws.removeListener("message", handler);
556
+ resolve(true);
557
+ }
558
+ } catch {
559
+ }
560
+ };
561
+ ws.on("message", handler);
562
+ ws.send(JSON.stringify({ type: "ping" }));
563
+ });
564
+ } catch {
565
+ return false;
566
+ }
567
+ }
568
+ teardown(tunnelId) {
569
+ const entry = this.pool.get(tunnelId);
570
+ if (entry) {
571
+ try {
572
+ entry.proc?.kill();
573
+ } catch {
574
+ }
575
+ try {
576
+ entry.ws?.close();
577
+ } catch {
578
+ }
579
+ this.pool.delete(tunnelId);
580
+ }
581
+ }
582
+ releaseAll() {
583
+ for (const id of Array.from(this.pool.keys())) {
584
+ this.teardown(id);
585
+ }
586
+ }
587
+ }
588
+ const agentConnectionPool = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
589
+ __proto__: null,
590
+ AgentConnectionPool
591
+ }, Symbol.toStringTag, { value: "Module" }));
592
+ function registerFsIpc(machineManager2) {
593
+ electron.ipcMain.handle("fs:listDir", async (_e, machineId, dirPath) => {
594
+ const machine = machineManager2.getById(machineId);
595
+ if (!machine || machineId === "local") {
596
+ return listLocalDir(dirPath);
597
+ }
598
+ if (machine.authMethod === "dev-tunnel") {
599
+ return listRemoteDirViaTunnel(machine.tunnelId ?? "", dirPath);
600
+ }
601
+ return listRemoteDir(machine.address, dirPath, machine.authMethod, machine.keyPath);
602
+ });
603
+ electron.ipcMain.handle("fs:homeDir", async (_e, machineId) => {
604
+ const machine = machineManager2.getById(machineId);
605
+ if (!machine || machineId === "local") {
606
+ return os__namespace.homedir();
607
+ }
608
+ return "C:\\Users";
609
+ });
610
+ }
611
+ function listLocalDir(dirPath) {
612
+ try {
613
+ const entries = fs__namespace.readdirSync(dirPath, { withFileTypes: true });
614
+ return entries.filter((e) => e.isDirectory() || e.isFile()).map((e) => ({ name: e.name, isDir: e.isDirectory() })).sort((a, b) => {
615
+ if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
616
+ return a.name.localeCompare(b.name);
617
+ });
618
+ } catch {
619
+ return [];
620
+ }
621
+ }
622
+ function listRemoteDir(host, dirPath, authMethod, keyPath) {
623
+ return new Promise((resolve) => {
624
+ if (authMethod === "ssh-key" && keyPath) {
625
+ import("ssh2").then(({ Client }) => {
626
+ const conn = new Client();
627
+ conn.on("ready", () => {
628
+ const psCmd = `powershell -Command "Get-ChildItem -LiteralPath '${dirPath.replace(/'/g, "''")}' | Select-Object Name,@{N='IsDir';E={$_.PSIsContainer}} | ConvertTo-Json -Compress"`;
629
+ conn.exec(psCmd, (err, stream) => {
630
+ if (err) {
631
+ conn.end();
632
+ resolve([]);
633
+ return;
634
+ }
635
+ let output = "";
636
+ stream.on("data", (chunk) => {
637
+ output += chunk.toString();
638
+ });
639
+ stream.on("close", () => {
640
+ conn.end();
641
+ try {
642
+ const parsed = JSON.parse(output);
643
+ const items = Array.isArray(parsed) ? parsed : [parsed];
644
+ resolve(
645
+ items.map((i) => ({ name: i.Name, isDir: i.IsDir })).sort((a, b) => {
646
+ if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
647
+ return a.name.localeCompare(b.name);
648
+ })
649
+ );
650
+ } catch {
651
+ resolve([]);
652
+ }
653
+ });
654
+ });
655
+ });
656
+ conn.on("error", () => resolve([]));
657
+ import("fs").then((fsModule) => {
658
+ import("os").then((osModule) => {
659
+ conn.connect({
660
+ host,
661
+ port: 22,
662
+ username: osModule.userInfo().username,
663
+ privateKey: fsModule.readFileSync(keyPath),
664
+ readyTimeout: 8e3
665
+ });
666
+ });
667
+ });
668
+ }).catch(() => resolve([]));
669
+ } else {
670
+ resolve([]);
671
+ }
672
+ });
673
+ }
674
+ async function listRemoteDirViaTunnel(tunnelId, dirPath) {
675
+ if (!tunnelId) return [];
676
+ try {
677
+ const ws = await AgentConnectionPool.getInstance().getConnection(tunnelId);
678
+ const id = crypto.randomUUID();
679
+ return new Promise((resolve) => {
680
+ const t = setTimeout(() => {
681
+ ws.removeListener("message", handler);
682
+ resolve([]);
683
+ }, 8e3);
684
+ const handler = (raw) => {
685
+ try {
686
+ const msg = JSON.parse(raw.toString());
687
+ if (msg.id === id && msg.type === "dirList") {
688
+ clearTimeout(t);
689
+ ws.removeListener("message", handler);
690
+ resolve(msg.entries ?? []);
691
+ }
692
+ } catch {
693
+ }
694
+ };
695
+ ws.on("message", handler);
696
+ ws.send(JSON.stringify({ type: "listDir", id, path: dirPath }));
697
+ });
698
+ } catch {
699
+ return [];
700
+ }
701
+ }
702
+ const execAsync$1 = util.promisify(child_process.exec);
703
+ async function runAz(args) {
704
+ const { stdout } = await execAsync$1(`az ${args} --output json`, {
705
+ timeout: 3e4
706
+ });
707
+ return JSON.parse(stdout);
708
+ }
709
+ function powerStateToStatus(powerState) {
710
+ switch (powerState?.toLowerCase()) {
711
+ case "running":
712
+ return "online";
713
+ case "hibernated":
714
+ return "hibernated";
715
+ case "stopped":
716
+ case "deallocated":
717
+ return "offline";
718
+ default:
719
+ return "unknown";
720
+ }
721
+ }
722
+ async function discoverDevBoxes() {
723
+ try {
724
+ await execAsync$1("az account show --output none", { timeout: 1e4 });
725
+ } catch (err) {
726
+ const msg = String(err?.message ?? "");
727
+ if (msg.includes("ENOENT") || msg.includes("not found") || msg.includes("not recognized")) {
728
+ return { machines: [], error: "az-not-found" };
729
+ }
730
+ return { machines: [], error: "not-logged-in", errorMessage: msg };
731
+ }
732
+ let endpoints = [];
733
+ try {
734
+ const graphResult = await runAz(
735
+ `graph query -q "Resources | where type =~ 'microsoft.devcenter/projects' | project properties.devCenterUri"`
736
+ );
737
+ endpoints = [
738
+ ...new Set(
739
+ graphResult.data.map((p) => p.properties_devCenterUri).filter(Boolean)
740
+ )
741
+ ];
742
+ } catch {
743
+ return { machines: [], error: "unknown", errorMessage: "resource-graph query failed" };
744
+ }
745
+ if (endpoints.length === 0) return { machines: [] };
746
+ const discovered = [];
747
+ for (const endpoint of endpoints) {
748
+ try {
749
+ const boxes = await runAz(
750
+ `devcenter dev dev-box list --endpoint "${endpoint}" --user-id me`
751
+ );
752
+ if (!Array.isArray(boxes)) continue;
753
+ for (const box of boxes) {
754
+ discovered.push({
755
+ name: box.name ?? "Dev Box",
756
+ type: "devbox",
757
+ status: powerStateToStatus(box.powerState),
758
+ onboarded: false,
759
+ // SSH not yet verified
760
+ address: "",
761
+ // unknown until user sets it up
762
+ authMethod: "az-ssh",
763
+ discoverySource: "azure-devbox",
764
+ projectName: box.projectName,
765
+ endpoint,
766
+ specs: box.hardwareProfile ? {
767
+ cpus: box.hardwareProfile.vCPUs ?? 0,
768
+ memoryGB: box.hardwareProfile.memoryGB ?? 0,
769
+ storageGB: box.storageProfile?.osDisk?.diskSizeGB ?? 0
770
+ } : void 0,
771
+ powerState: box.powerState
772
+ });
773
+ }
774
+ } catch {
775
+ }
776
+ }
777
+ return { machines: discovered };
778
+ }
779
+ async function discoverCloudPCs() {
780
+ return { machines: [] };
781
+ }
782
+ const execAsync = util.promisify(child_process.exec);
783
+ const LOCAL_MACHINE = {
784
+ id: "local",
785
+ name: "This Machine",
786
+ type: "host",
787
+ status: "online",
788
+ onboarded: true,
789
+ address: "127.0.0.1",
790
+ authMethod: "az-ssh"
791
+ };
792
+ class MachineManager {
793
+ machines = /* @__PURE__ */ new Map();
794
+ statusCallbacks = [];
795
+ pingInterval = null;
796
+ persistPath;
797
+ constructor() {
798
+ this.persistPath = path__namespace.join(electron.app.getPath("userData"), "machines.json");
799
+ this.load();
800
+ }
801
+ load() {
802
+ this.machines.set("local", { ...LOCAL_MACHINE });
803
+ try {
804
+ const data = fs__namespace.readFileSync(this.persistPath, "utf8");
805
+ const saved = JSON.parse(data);
806
+ for (const m of saved) {
807
+ if (m.id !== "local") {
808
+ this.machines.set(m.id, m);
809
+ }
810
+ }
811
+ } catch {
812
+ }
813
+ }
814
+ save() {
815
+ try {
816
+ const data = JSON.stringify(
817
+ Array.from(this.machines.values()).filter((m) => m.id !== "local"),
818
+ null,
819
+ 2
820
+ );
821
+ fs__namespace.writeFileSync(this.persistPath, data, "utf8");
822
+ } catch {
823
+ }
824
+ }
825
+ list() {
826
+ return Array.from(this.machines.values());
827
+ }
828
+ getById(id) {
829
+ return this.machines.get(id);
830
+ }
831
+ add(partial) {
832
+ const machine = { ...partial, id: crypto.randomUUID(), status: "unknown" };
833
+ this.machines.set(machine.id, machine);
834
+ this.save();
835
+ return machine;
836
+ }
837
+ remove(id) {
838
+ if (id === "local") return;
839
+ this.machines.delete(id);
840
+ this.save();
841
+ }
842
+ update(id, partial) {
843
+ const machine = this.machines.get(id);
844
+ if (!machine) return void 0;
845
+ Object.assign(machine, partial);
846
+ if ("address" in partial && !partial.address) machine.onboarded = false;
847
+ this.save();
848
+ return machine;
849
+ }
850
+ updateAddress(id, address) {
851
+ const machine = this.machines.get(id);
852
+ if (!machine) return void 0;
853
+ machine.address = address;
854
+ if (!address) machine.onboarded = false;
855
+ this.save();
856
+ return machine;
857
+ }
858
+ async discoverDevBoxes() {
859
+ const [devBoxResult, cloudPcResult] = await Promise.all([
860
+ discoverDevBoxes(),
861
+ discoverCloudPCs()
862
+ ]);
863
+ const allDiscovered = [...devBoxResult.machines, ...cloudPcResult.machines];
864
+ for (const partial of allDiscovered) {
865
+ const existing = Array.from(this.machines.values()).find((m) => {
866
+ if (partial.discoverySource === "azure-devbox") {
867
+ return m.discoverySource === "azure-devbox" && m.name === partial.name && m.projectName === partial.projectName;
868
+ }
869
+ if (partial.discoverySource === "cloud-pc") {
870
+ return m.discoverySource === "cloud-pc" && m.cloudPcId === partial.cloudPcId;
871
+ }
872
+ return false;
873
+ });
874
+ if (existing) {
875
+ existing.status = partial.status;
876
+ existing.powerState = partial.powerState;
877
+ if (partial.specs) existing.specs = partial.specs;
878
+ } else {
879
+ const machine = { ...partial, id: crypto.randomUUID() };
880
+ this.machines.set(machine.id, machine);
881
+ }
882
+ }
883
+ this.save();
884
+ const error = devBoxResult.error ?? cloudPcResult.error;
885
+ const errorMessage = devBoxResult.errorMessage ?? cloudPcResult.errorMessage;
886
+ return { machines: this.list(), error, errorMessage };
887
+ }
888
+ async launchRdp(id) {
889
+ const machine = this.machines.get(id);
890
+ if (!machine) throw new Error("Machine not found");
891
+ if (machine.discoverySource === "cloud-pc" && machine.cloudPcId) {
892
+ const upn = machine.cloudPcUpn ?? "";
893
+ const params = new URLSearchParams({ cpcid: machine.cloudPcId, version: "1.0" });
894
+ if (upn) params.set("username", upn);
895
+ await electron.shell.openExternal(`ms-cloudpc:connect?${params.toString()}`);
896
+ return;
897
+ }
898
+ if (machine.discoverySource === "azure-devbox" && machine.endpoint && machine.projectName) {
899
+ try {
900
+ const { stdout } = await execAsync(
901
+ `az devcenter dev dev-box show-remote-connection --endpoint "${machine.endpoint}" --user-id me --dev-box-name "${machine.name}" --project-name "${machine.projectName}" --output json`,
902
+ { timeout: 3e4 }
903
+ );
904
+ const conn = JSON.parse(stdout);
905
+ const url = conn.rdpConnectionUrl ?? conn.webUrl;
906
+ if (url) {
907
+ await electron.shell.openExternal(url);
908
+ return;
909
+ }
910
+ } catch {
911
+ }
912
+ }
913
+ if (machine.address) {
914
+ child_process.exec(`mstsc /v:${machine.address}`);
915
+ } else {
916
+ throw new Error("No RDP connection method available for this machine");
917
+ }
918
+ }
919
+ async ping(id) {
920
+ const machine = this.machines.get(id);
921
+ if (!machine) return "unknown";
922
+ if (id === "local") return "online";
923
+ const status = machine.authMethod === "az-ssh" ? await azSshPing(machine.address) : machine.authMethod === "dev-tunnel" ? await devTunnelPing(machine.tunnelId ?? "") : await tcpPing(machine.address, 22, 5e3);
924
+ machine.status = status;
925
+ if (status === "online") machine.onboarded = true;
926
+ this.save();
927
+ this.emitStatusChanged({ machineId: id, status });
928
+ return status;
929
+ }
930
+ onStatusChanged(cb) {
931
+ this.statusCallbacks.push(cb);
932
+ }
933
+ startPingLoop() {
934
+ this.pingInterval = setInterval(() => {
935
+ for (const machine of this.machines.values()) {
936
+ if (machine.id !== "local") {
937
+ this.ping(machine.id).catch(() => {
938
+ });
939
+ }
940
+ }
941
+ }, 3e4);
942
+ }
943
+ stopPingLoop() {
944
+ if (this.pingInterval) {
945
+ clearInterval(this.pingInterval);
946
+ this.pingInterval = null;
947
+ }
948
+ AgentConnectionPool.getInstance().releaseAll();
949
+ }
950
+ emitStatusChanged(ev) {
951
+ for (const cb of this.statusCallbacks) cb(ev);
952
+ }
953
+ }
954
+ function azSshPing(address) {
955
+ return new Promise((resolve) => {
956
+ const timer = setTimeout(() => {
957
+ child.kill();
958
+ resolve("offline");
959
+ }, 2e4);
960
+ const child = child_process.exec(
961
+ `az ssh vm --ip "${address}" -- exit`,
962
+ { timeout: 2e4 },
963
+ (err) => {
964
+ clearTimeout(timer);
965
+ resolve(err ? "offline" : "online");
966
+ }
967
+ );
968
+ });
969
+ }
970
+ async function devTunnelPing(tunnelId) {
971
+ if (!tunnelId) return "offline";
972
+ const ok = await AgentConnectionPool.getInstance().ping(tunnelId);
973
+ return ok ? "online" : "offline";
974
+ }
975
+ function tcpPing(host, port, timeoutMs) {
976
+ return new Promise((resolve) => {
977
+ const socket = new net__namespace.Socket();
978
+ const timer = setTimeout(() => {
979
+ socket.destroy();
980
+ resolve("offline");
981
+ }, timeoutMs);
982
+ socket.connect(port, host, () => {
983
+ clearTimeout(timer);
984
+ socket.destroy();
985
+ resolve("online");
986
+ });
987
+ socket.on("error", () => {
988
+ clearTimeout(timer);
989
+ resolve("offline");
990
+ });
991
+ });
992
+ }
993
+ class LocalPtyAdapter {
994
+ ptyProcess;
995
+ dataHandlers = [];
996
+ exitHandlers = [];
997
+ constructor(cwd, cols, rows) {
998
+ this.ptyProcess = pty__namespace.spawn("powershell.exe", [], {
999
+ name: "xterm-256color",
1000
+ cols,
1001
+ rows,
1002
+ cwd,
1003
+ env: { ...process.env }
1004
+ });
1005
+ this.ptyProcess.onData((data) => {
1006
+ for (const h of this.dataHandlers) h(data);
1007
+ });
1008
+ this.ptyProcess.onExit(({ exitCode }) => {
1009
+ for (const h of this.exitHandlers) h(exitCode);
1010
+ });
1011
+ }
1012
+ onData(cb) {
1013
+ this.dataHandlers.push(cb);
1014
+ }
1015
+ onExit(cb) {
1016
+ this.exitHandlers.push(cb);
1017
+ }
1018
+ write(data) {
1019
+ this.ptyProcess.write(data);
1020
+ }
1021
+ resize(cols, rows) {
1022
+ try {
1023
+ this.ptyProcess.resize(cols, rows);
1024
+ } catch {
1025
+ }
1026
+ }
1027
+ kill() {
1028
+ try {
1029
+ this.ptyProcess.kill();
1030
+ } catch {
1031
+ }
1032
+ }
1033
+ autoLaunch(provider) {
1034
+ if (provider === "claude") {
1035
+ setTimeout(() => this.write("claude\r"), 800);
1036
+ } else if (provider === "copilot") {
1037
+ setTimeout(() => this.write("copilot --output-format json\r"), 800);
1038
+ }
1039
+ }
1040
+ }
1041
+ class SshPtyAdapter {
1042
+ callbacks = { onData: [], onExit: [] };
1043
+ ptyProcess = null;
1044
+ // for az-ssh path
1045
+ sshClient = null;
1046
+ // for ssh-key / dev-tunnel path
1047
+ sshStream = null;
1048
+ devtunnelProc = null;
1049
+ // for dev-tunnel path
1050
+ cols;
1051
+ rows;
1052
+ constructor(cols, rows) {
1053
+ this.cols = cols;
1054
+ this.rows = rows;
1055
+ }
1056
+ async connect(machine, workingDirectory) {
1057
+ if (machine.authMethod === "az-ssh") {
1058
+ await this.connectAzSsh(machine, workingDirectory);
1059
+ } else if (machine.authMethod === "dev-tunnel") {
1060
+ await this.connectDevTunnel(machine, workingDirectory);
1061
+ } else {
1062
+ await this.connectSshKey(machine, workingDirectory);
1063
+ }
1064
+ }
1065
+ connectDevTunnel(machine, workingDirectory) {
1066
+ return new Promise((resolve, reject) => {
1067
+ if (!machine.tunnelId) {
1068
+ reject(new Error("No tunnelId configured for this machine"));
1069
+ return;
1070
+ }
1071
+ const localAppData = process.env.LOCALAPPDATA ?? "";
1072
+ const env = { ...process.env };
1073
+ if (localAppData) env.PATH = `${localAppData}\\Microsoft\\WinGet\\Links;${env.PATH ?? ""}`;
1074
+ const child = child_process.spawn("devtunnel", ["connect", machine.tunnelId], { shell: true, env });
1075
+ this.devtunnelProc = child;
1076
+ let portReady = false;
1077
+ child.on("error", (err) => {
1078
+ if (!portReady) reject(err);
1079
+ });
1080
+ child.on("close", (code) => {
1081
+ if (!portReady) {
1082
+ reject(new Error(`devtunnel connect exited with code ${code}`));
1083
+ } else {
1084
+ for (const h of this.callbacks.onExit) h(code ?? void 0);
1085
+ }
1086
+ });
1087
+ setTimeout(() => {
1088
+ if (!portReady) {
1089
+ portReady = true;
1090
+ this.connectSshKey({ ...machine, address: "127.0.0.1" }, workingDirectory).then(resolve).catch((err) => {
1091
+ child.kill();
1092
+ reject(err);
1093
+ });
1094
+ }
1095
+ }, 4e3);
1096
+ setTimeout(() => {
1097
+ if (!portReady) {
1098
+ portReady = true;
1099
+ child.kill();
1100
+ reject(new Error("devtunnel connect timed out after 20s"));
1101
+ }
1102
+ }, 2e4);
1103
+ });
1104
+ }
1105
+ connectAzSsh(machine, workingDirectory) {
1106
+ return new Promise((resolve, reject) => {
1107
+ try {
1108
+ const cdCmd = workingDirectory ? `powershell.exe -NoExit -Command "Set-Location '${workingDirectory.replace(/'/g, "''")}'"` : "powershell.exe";
1109
+ this.ptyProcess = pty__namespace.spawn(
1110
+ "az",
1111
+ ["ssh", "vm", "--ip", machine.address, "--", cdCmd],
1112
+ {
1113
+ name: "xterm-256color",
1114
+ cols: this.cols,
1115
+ rows: this.rows,
1116
+ env: { ...process.env }
1117
+ }
1118
+ );
1119
+ this.ptyProcess.onData((data) => {
1120
+ for (const h of this.callbacks.onData) h(data);
1121
+ resolve();
1122
+ });
1123
+ this.ptyProcess.onExit(({ exitCode }) => {
1124
+ for (const h of this.callbacks.onExit) h(exitCode);
1125
+ });
1126
+ setTimeout(() => reject(new Error("az ssh connection timeout")), 3e4);
1127
+ } catch (err) {
1128
+ reject(err);
1129
+ }
1130
+ });
1131
+ }
1132
+ connectSshKey(machine, workingDirectory) {
1133
+ return new Promise((resolve, reject) => {
1134
+ if (!machine.keyPath) {
1135
+ reject(new Error("No keyPath set for ssh-key auth"));
1136
+ return;
1137
+ }
1138
+ let privateKey;
1139
+ try {
1140
+ privateKey = fs__namespace.readFileSync(machine.keyPath);
1141
+ } catch (err) {
1142
+ reject(new Error(`Cannot read SSH key at ${machine.keyPath}: ${err}`));
1143
+ return;
1144
+ }
1145
+ this.sshClient = new ssh2.Client();
1146
+ this.sshClient.on("ready", () => {
1147
+ const initialCmd = workingDirectory ? `powershell.exe -NoExit -Command "Set-Location '${workingDirectory.replace(/'/g, "''")}'; $Host.UI.RawUI.WindowSize = New-Object Management.Automation.Host.Size(${this.cols},${this.rows})"` : "powershell.exe";
1148
+ this.sshClient.exec(
1149
+ initialCmd,
1150
+ { pty: { term: "xterm-256color", cols: this.cols, rows: this.rows } },
1151
+ (err, stream) => {
1152
+ if (err) {
1153
+ reject(err);
1154
+ return;
1155
+ }
1156
+ this.sshStream = stream;
1157
+ stream.on("data", (chunk) => {
1158
+ const data = chunk.toString();
1159
+ for (const h of this.callbacks.onData) h(data);
1160
+ });
1161
+ stream.stderr.on("data", (chunk) => {
1162
+ const data = chunk.toString();
1163
+ for (const h of this.callbacks.onData) h(data);
1164
+ });
1165
+ stream.on("close", (code) => {
1166
+ for (const h of this.callbacks.onExit) h(code);
1167
+ });
1168
+ resolve();
1169
+ }
1170
+ );
1171
+ });
1172
+ this.sshClient.on("error", (err) => reject(err));
1173
+ this.sshClient.connect({
1174
+ host: machine.address,
1175
+ port: 22,
1176
+ username: os__namespace.userInfo().username,
1177
+ privateKey,
1178
+ readyTimeout: 15e3
1179
+ });
1180
+ });
1181
+ }
1182
+ onData(cb) {
1183
+ this.callbacks.onData.push(cb);
1184
+ }
1185
+ onExit(cb) {
1186
+ this.callbacks.onExit.push(cb);
1187
+ }
1188
+ write(data) {
1189
+ if (this.ptyProcess) {
1190
+ this.ptyProcess.write(data);
1191
+ } else if (this.sshStream) {
1192
+ this.sshStream.stdin.write(data);
1193
+ }
1194
+ }
1195
+ resize(cols, rows) {
1196
+ this.cols = cols;
1197
+ this.rows = rows;
1198
+ if (this.ptyProcess) {
1199
+ try {
1200
+ this.ptyProcess.resize(cols, rows);
1201
+ } catch {
1202
+ }
1203
+ } else if (this.sshStream) {
1204
+ try {
1205
+ this.sshStream.setWindow(rows, cols, 0, 0);
1206
+ } catch {
1207
+ }
1208
+ }
1209
+ }
1210
+ kill() {
1211
+ try {
1212
+ this.ptyProcess?.kill();
1213
+ } catch {
1214
+ }
1215
+ try {
1216
+ this.sshStream?.close();
1217
+ } catch {
1218
+ }
1219
+ try {
1220
+ this.sshClient?.end();
1221
+ } catch {
1222
+ }
1223
+ try {
1224
+ this.devtunnelProc?.kill();
1225
+ } catch {
1226
+ }
1227
+ this.ptyProcess = null;
1228
+ this.sshStream = null;
1229
+ this.sshClient = null;
1230
+ this.devtunnelProc = null;
1231
+ }
1232
+ autoLaunch(provider) {
1233
+ if (provider === "claude") {
1234
+ setTimeout(() => this.write("claude\r"), 1200);
1235
+ } else if (provider === "copilot") {
1236
+ setTimeout(() => this.write("copilot --output-format json\r"), 1200);
1237
+ }
1238
+ }
1239
+ }
1240
+ class AgentPtyAdapter {
1241
+ constructor(tunnelId, sessionId) {
1242
+ this.tunnelId = tunnelId;
1243
+ this.sessionId = sessionId;
1244
+ }
1245
+ ws = null;
1246
+ dataCallback = null;
1247
+ exitCallback = null;
1248
+ messageHandler = null;
1249
+ async connect(cwd, cols, rows) {
1250
+ this.ws = await AgentConnectionPool.getInstance().getConnection(this.tunnelId);
1251
+ this.messageHandler = (raw) => {
1252
+ try {
1253
+ const msg = JSON.parse(raw.toString());
1254
+ if (msg.id !== this.sessionId) return;
1255
+ if (msg.type === "data" && this.dataCallback) {
1256
+ this.dataCallback(Buffer.from(msg.data ?? "", "base64").toString());
1257
+ } else if (msg.type === "exit" && this.exitCallback) {
1258
+ this.exitCallback(msg.code ?? null);
1259
+ }
1260
+ } catch {
1261
+ }
1262
+ };
1263
+ this.ws.on("message", this.messageHandler);
1264
+ this.ws.send(JSON.stringify({ type: "create", id: this.sessionId, shell: "powershell", cwd, cols, rows }));
1265
+ await new Promise((resolve) => {
1266
+ const t = setTimeout(resolve, 5e3);
1267
+ const ackHandler = (raw) => {
1268
+ try {
1269
+ const msg = JSON.parse(raw.toString());
1270
+ if (msg.id === this.sessionId && msg.type === "created") {
1271
+ clearTimeout(t);
1272
+ this.ws.removeListener("message", ackHandler);
1273
+ resolve();
1274
+ }
1275
+ } catch {
1276
+ }
1277
+ };
1278
+ this.ws.on("message", ackHandler);
1279
+ });
1280
+ }
1281
+ onData(cb) {
1282
+ this.dataCallback = cb;
1283
+ }
1284
+ onExit(cb) {
1285
+ this.exitCallback = cb;
1286
+ }
1287
+ write(data) {
1288
+ this.ws?.send(JSON.stringify({ type: "input", id: this.sessionId, data }));
1289
+ }
1290
+ resize(cols, rows) {
1291
+ this.ws?.send(JSON.stringify({ type: "resize", id: this.sessionId, cols, rows }));
1292
+ }
1293
+ kill() {
1294
+ if (this.ws && this.messageHandler) {
1295
+ this.ws.removeListener("message", this.messageHandler);
1296
+ }
1297
+ this.ws?.send(JSON.stringify({ type: "destroy", id: this.sessionId }));
1298
+ }
1299
+ autoLaunch(provider) {
1300
+ if (provider === "claude") {
1301
+ setTimeout(() => this.write("claude\r"), 600);
1302
+ } else if (provider === "copilot") {
1303
+ setTimeout(() => this.write("copilot --output-format json\r"), 600);
1304
+ }
1305
+ }
1306
+ }
1307
+ class StateMonitor {
1308
+ opts;
1309
+ fileWatcher = null;
1310
+ silenceTimer = null;
1311
+ stopped = false;
1312
+ jsonlBuf = "";
1313
+ // Set to true once we see the copilot banner — ignores PowerShell startup noise
1314
+ copilotReady = false;
1315
+ // Track last emitted state to avoid duplicate emissions from TUI redraws
1316
+ lastEmittedState = null;
1317
+ // Regex to strip ANSI escape sequences:
1318
+ // - CSI sequences: \x1b[ (with optional ? > = prefix) digits/semicolons, ending in letter or ~
1319
+ // - OSC sequences: \x1b] ... BEL
1320
+ // - Simple escapes: \x1b followed by single char
1321
+ static ANSI_RE = /\x1b\[[\?>=!]?[0-9;]*[a-zA-Z~]|\x1b\][^\x07]*\x07|\x1b[^\[]/g;
1322
+ constructor(opts) {
1323
+ this.opts = opts;
1324
+ }
1325
+ start() {
1326
+ if (this.opts.provider === "claude") {
1327
+ this.startClaudeHookMonitor();
1328
+ } else if (this.opts.provider !== "copilot") {
1329
+ this.resetSilenceTimer();
1330
+ }
1331
+ }
1332
+ /** Called with every PTY data chunk from the adapter */
1333
+ onPtyData(data) {
1334
+ if (this.stopped) return;
1335
+ if (this.opts.provider === "copilot") {
1336
+ if (!this.copilotReady && data.includes("GitHub Copilot")) {
1337
+ this.copilotReady = true;
1338
+ console.log(`[StateMonitor] ${this.opts.sessionId.slice(0, 8)} copilotReady=true (banner detected)`);
1339
+ }
1340
+ this.jsonlBuf += data;
1341
+ const lines = this.jsonlBuf.split("\n");
1342
+ this.jsonlBuf = lines.pop() ?? "";
1343
+ if (lines.length > 0) {
1344
+ console.log(`[StateMonitor] ${this.opts.sessionId.slice(0, 8)} JSONL: ${lines.length} lines to parse, buf remainder=${this.jsonlBuf.length} chars`);
1345
+ }
1346
+ for (const line of lines) {
1347
+ const trimmed = line.trim();
1348
+ if (trimmed.length > 0) {
1349
+ const stripped = trimmed.replace(/\x1b\[[\?>=!]?[0-9;]*[a-zA-Z~]/g, "").trim();
1350
+ const startsWithBrace = stripped.startsWith("{");
1351
+ console.log(`[StateMonitor] ${this.opts.sessionId.slice(0, 8)} JSONL line: startsWithBrace=${startsWithBrace} len=${trimmed.length} preview=${JSON.stringify(stripped.slice(0, 120))}`);
1352
+ }
1353
+ this.parseJsonlLine(line.trim());
1354
+ }
1355
+ if (this.copilotReady && data.length >= 50) {
1356
+ if (data.includes("Enter to select")) {
1357
+ this.emitState("permission", "heuristic", '"Enter to select" — input required');
1358
+ } else if (data.includes("Esc to cancel") || data.includes("Esc to stop")) {
1359
+ this.emitState("working", "heuristic", '"Esc to cancel/stop" — copilot working');
1360
+ } else if (data.length >= 200) {
1361
+ this.emitState("waiting", "heuristic", "no working/input signals — at prompt");
1362
+ }
1363
+ }
1364
+ if (this.jsonlBuf.length > 1e4) {
1365
+ this.jsonlBuf = this.jsonlBuf.slice(-2e3);
1366
+ }
1367
+ } else if (this.opts.provider !== "claude") {
1368
+ this.resetSilenceTimer();
1369
+ }
1370
+ }
1371
+ stop() {
1372
+ this.stopped = true;
1373
+ this.fileWatcher?.close();
1374
+ this.fileWatcher = null;
1375
+ const localFile = path__namespace.join(os__namespace.homedir(), ".claude", "session-status.txt");
1376
+ fs__namespace.unwatchFile(localFile);
1377
+ if (this.claudePollingInterval) {
1378
+ clearInterval(this.claudePollingInterval);
1379
+ this.claudePollingInterval = null;
1380
+ }
1381
+ if (this.silenceTimer) clearTimeout(this.silenceTimer);
1382
+ }
1383
+ lastClaudeFileContent = null;
1384
+ claudePollingInterval = null;
1385
+ startClaudeHookMonitor() {
1386
+ const isRemote = this.opts.machineId !== "local" && !!this.opts.tunnelId;
1387
+ const statusFilePath = isRemote ? "$HOME/.claude/session-status.txt" : path__namespace.join(os__namespace.homedir(), ".claude", "session-status.txt");
1388
+ console.log(`[StateMonitor] ${this.opts.sessionId.slice(0, 8)} Claude hook monitor starting (${isRemote ? "remote via agent" : "local"})`);
1389
+ console.log(`[StateMonitor] ${this.opts.sessionId.slice(0, 8)} file: ${statusFilePath}`);
1390
+ if (isRemote) {
1391
+ this.startRemoteClaudePolling();
1392
+ } else {
1393
+ const localFile = path__namespace.join(os__namespace.homedir(), ".claude", "session-status.txt");
1394
+ this.applyClaudeStatus(localFile);
1395
+ fs__namespace.watchFile(localFile, { interval: 500 }, (curr, prev) => {
1396
+ if (curr.mtimeMs !== prev.mtimeMs) {
1397
+ this.applyClaudeStatus(localFile);
1398
+ }
1399
+ });
1400
+ }
1401
+ }
1402
+ startRemoteClaudePolling() {
1403
+ const pollRemote = async () => {
1404
+ if (this.stopped) return;
1405
+ try {
1406
+ const { AgentConnectionPool: AgentConnectionPool2 } = await Promise.resolve().then(() => agentConnectionPool);
1407
+ const ws = await AgentConnectionPool2.getInstance().getConnection(this.opts.tunnelId);
1408
+ const id = `status-${Date.now()}`;
1409
+ return new Promise((resolve) => {
1410
+ const timeout = setTimeout(() => {
1411
+ resolve();
1412
+ }, 3e3);
1413
+ const handler = (raw) => {
1414
+ try {
1415
+ const msg = JSON.parse(raw.toString());
1416
+ if (msg.id === id && msg.type === "fileContent") {
1417
+ clearTimeout(timeout);
1418
+ ws.removeListener("message", handler);
1419
+ const content = (msg.content ?? "").trim().toUpperCase();
1420
+ if (content !== this.lastClaudeFileContent) {
1421
+ console.log(`[StateMonitor] ${this.opts.sessionId.slice(0, 8)} remote Claude status: "${content}" (was: "${this.lastClaudeFileContent}")`);
1422
+ this.lastClaudeFileContent = content;
1423
+ if (content === "WAITING") {
1424
+ this.opts.onStateChange("waiting", "high");
1425
+ } else if (content === "RUNNING") {
1426
+ this.opts.onStateChange("working", "high");
1427
+ }
1428
+ }
1429
+ resolve();
1430
+ }
1431
+ } catch {
1432
+ }
1433
+ };
1434
+ ws.on("message", handler);
1435
+ ws.send(JSON.stringify({
1436
+ type: "readFile",
1437
+ id,
1438
+ path: "~/.claude/session-status.txt"
1439
+ }));
1440
+ });
1441
+ } catch (err) {
1442
+ console.log(`[StateMonitor] ${this.opts.sessionId.slice(0, 8)} remote poll error: ${err}`);
1443
+ }
1444
+ };
1445
+ setTimeout(() => {
1446
+ pollRemote();
1447
+ this.claudePollingInterval = setInterval(pollRemote, 1e3);
1448
+ }, 5e3);
1449
+ }
1450
+ applyClaudeStatus(localFile) {
1451
+ if (this.stopped) return;
1452
+ try {
1453
+ const raw = fs__namespace.readFileSync(localFile, "utf8");
1454
+ const content = raw.trim().toUpperCase();
1455
+ if (content !== this.lastClaudeFileContent) {
1456
+ console.log(`[StateMonitor] ${this.opts.sessionId.slice(0, 8)} Claude status: "${content}" (was: "${this.lastClaudeFileContent}")`);
1457
+ this.lastClaudeFileContent = content;
1458
+ if (content === "WAITING") {
1459
+ this.opts.onStateChange("waiting", "high");
1460
+ } else if (content === "RUNNING") {
1461
+ this.opts.onStateChange("working", "high");
1462
+ }
1463
+ }
1464
+ } catch {
1465
+ }
1466
+ }
1467
+ parseJsonlLine(line) {
1468
+ const clean = line.replace(/\x1b\[[\?>=!]?[0-9;]*[a-zA-Z~]|\x1b\][^\x07]*\x07|\x1b[^\[]/g, "").trim();
1469
+ if (!clean.startsWith("{")) return;
1470
+ try {
1471
+ const obj = JSON.parse(clean);
1472
+ const type = obj["type"];
1473
+ if (type === "turn_start" || type === "tool_call") {
1474
+ this.emitState("working", "high", `JSONL: ${type}`);
1475
+ this.resetSilenceTimer();
1476
+ } else if (type === "turn_end" || type === "response_end") {
1477
+ this.emitState("waiting", "high", `JSONL: ${type}`);
1478
+ if (this.silenceTimer) {
1479
+ clearTimeout(this.silenceTimer);
1480
+ this.silenceTimer = null;
1481
+ }
1482
+ } else {
1483
+ console.log(`[StateMonitor] ${this.opts.sessionId.slice(0, 8)} JSONL parsed type="${type}" (no state change)`);
1484
+ }
1485
+ } catch {
1486
+ }
1487
+ }
1488
+ /** Emit state change, deduplicating consecutive identical states (from TUI redraws) */
1489
+ emitState(state, confidence, reason) {
1490
+ const key = `${state}:${confidence}`;
1491
+ if (key !== this.lastEmittedState) {
1492
+ console.log(`[StateMonitor] ${this.opts.sessionId.slice(0, 8)} → ${state.toUpperCase()} (${confidence}, ${reason})`);
1493
+ this.lastEmittedState = key;
1494
+ this.opts.onStateChange(state, confidence);
1495
+ }
1496
+ }
1497
+ resetSilenceTimer() {
1498
+ if (this.silenceTimer) clearTimeout(this.silenceTimer);
1499
+ const isCopilot = this.opts.provider === "copilot";
1500
+ this.silenceTimer = setTimeout(() => {
1501
+ if (!this.stopped) {
1502
+ console.log(`[StateMonitor] ${this.opts.sessionId.slice(0, 8)} → ${isCopilot ? "WAITING" : "IDLE"} (silence timer fired, ${isCopilot ? "5s" : "15s"})`);
1503
+ this.opts.onStateChange(isCopilot ? "waiting" : "idle", "heuristic");
1504
+ }
1505
+ }, isCopilot ? 5e3 : 15e3);
1506
+ }
1507
+ }
1508
+ const CLAUDE_DIR = path__namespace.join(os__namespace.homedir(), ".claude");
1509
+ const SETTINGS_FILE = path__namespace.join(CLAUDE_DIR, "settings.json");
1510
+ const HOOK_SCRIPT = path__namespace.join(CLAUDE_DIR, "maestro-hook.js");
1511
+ const HOOK_MARKER = "maestro-status-hook";
1512
+ const HOOK_SCRIPT_CONTENT = `// ${HOOK_MARKER} — auto-generated by Maestro
1513
+ const fs = require('fs');
1514
+ const path = require('path');
1515
+ const os = require('os');
1516
+
1517
+ const logFile = path.join(os.homedir(), '.claude', 'maestro-hook.log');
1518
+ function log(msg) {
1519
+ try {
1520
+ fs.appendFileSync(logFile, new Date().toISOString() + ' ' + msg + '\\n');
1521
+ } catch {}
1522
+ }
1523
+
1524
+ log('hook invoked, reading stdin...');
1525
+
1526
+ let input = '';
1527
+ process.stdin.on('data', d => input += d);
1528
+ process.stdin.on('end', () => {
1529
+ log('stdin received: ' + input.length + ' chars');
1530
+ try {
1531
+ const data = JSON.parse(input);
1532
+ const event = data.hook_event_name;
1533
+ log('event: ' + event);
1534
+ let status;
1535
+ if (event === 'UserPromptSubmit' || event === 'PreToolUse') {
1536
+ status = 'RUNNING';
1537
+ } else if (event === 'Stop' || event === 'Notification') {
1538
+ status = 'WAITING';
1539
+ }
1540
+ if (status) {
1541
+ const file = path.join(os.homedir(), '.claude', 'session-status.txt');
1542
+ fs.writeFileSync(file, status);
1543
+ log('wrote status: ' + status + ' to ' + file);
1544
+ } else {
1545
+ log('no status mapped for event: ' + event);
1546
+ }
1547
+ } catch (e) {
1548
+ log('ERROR: ' + e.message);
1549
+ }
1550
+ });
1551
+ `;
1552
+ function ensureClaudeHooks() {
1553
+ try {
1554
+ if (!fs__namespace.existsSync(CLAUDE_DIR)) {
1555
+ fs__namespace.mkdirSync(CLAUDE_DIR, { recursive: true });
1556
+ }
1557
+ const hookScriptPath = HOOK_SCRIPT.replace(/\\/g, "/");
1558
+ fs__namespace.writeFileSync(HOOK_SCRIPT, HOOK_SCRIPT_CONTENT, "utf8");
1559
+ console.log(`[ClaudeHooks] Hook script written to ${HOOK_SCRIPT}`);
1560
+ const hookCommand = `node "${hookScriptPath}"`;
1561
+ let settings = {};
1562
+ if (fs__namespace.existsSync(SETTINGS_FILE)) {
1563
+ const raw = fs__namespace.readFileSync(SETTINGS_FILE, "utf8");
1564
+ settings = JSON.parse(raw);
1565
+ }
1566
+ if (!settings.hooks) {
1567
+ settings.hooks = {};
1568
+ }
1569
+ const hookEvents = ["UserPromptSubmit", "PreToolUse", "Stop", "Notification"];
1570
+ let changed = false;
1571
+ for (const event of hookEvents) {
1572
+ if (!settings.hooks[event]) {
1573
+ settings.hooks[event] = [];
1574
+ }
1575
+ const before = settings.hooks[event].length;
1576
+ settings.hooks[event] = settings.hooks[event].filter((rule) => {
1577
+ const flat = rule;
1578
+ if (flat.command?.includes(HOOK_MARKER)) return false;
1579
+ if (rule.hooks?.some((h) => h.command?.includes(HOOK_MARKER) || h.command?.includes("maestro-hook"))) return false;
1580
+ return true;
1581
+ });
1582
+ if (settings.hooks[event].length !== before) changed = true;
1583
+ settings.hooks[event].push({
1584
+ matcher: "",
1585
+ hooks: [{
1586
+ type: "command",
1587
+ command: hookCommand
1588
+ }]
1589
+ });
1590
+ changed = true;
1591
+ }
1592
+ if (changed) {
1593
+ fs__namespace.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), "utf8");
1594
+ console.log(`[ClaudeHooks] Registered hooks in ${SETTINGS_FILE}`);
1595
+ console.log(`[ClaudeHooks] Hook command: ${hookCommand}`);
1596
+ }
1597
+ } catch (err) {
1598
+ console.error("[ClaudeHooks] Failed to register hooks:", err);
1599
+ }
1600
+ }
1601
+ const OUTPUT_BUFFER_MAX = 20 * 1024;
1602
+ class SessionManager {
1603
+ entries = /* @__PURE__ */ new Map();
1604
+ machineManager;
1605
+ push;
1606
+ constructor(machineManager2, push) {
1607
+ this.machineManager = machineManager2;
1608
+ this.push = push;
1609
+ }
1610
+ list() {
1611
+ return Array.from(this.entries.values()).map((e) => ({
1612
+ ...e.session,
1613
+ outputBuffer: e.outputBuffer
1614
+ }));
1615
+ }
1616
+ getBuffer(id) {
1617
+ const buf = this.entries.get(id)?.outputBuffer ?? "";
1618
+ console.log(`[SessionManager] getBuffer(${id}) → ${buf.length} chars`);
1619
+ return buf;
1620
+ }
1621
+ async create(opts) {
1622
+ if (opts.provider === "claude") {
1623
+ ensureClaudeHooks();
1624
+ }
1625
+ const cols = opts.cols ?? 120;
1626
+ const rows = opts.rows ?? 30;
1627
+ const machine = this.machineManager.getById(opts.machineId);
1628
+ const session = {
1629
+ id: crypto.randomUUID(),
1630
+ name: opts.name,
1631
+ machineId: opts.machineId,
1632
+ provider: opts.provider,
1633
+ workingDirectory: opts.workingDirectory,
1634
+ state: "idle",
1635
+ stateConfidence: "heuristic",
1636
+ stateEnteredAt: (/* @__PURE__ */ new Date()).toISOString(),
1637
+ tags: {
1638
+ ...opts.tags,
1639
+ "machine": opts.machineId,
1640
+ "provider": opts.provider
1641
+ },
1642
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1643
+ };
1644
+ let adapter;
1645
+ if (!machine || opts.machineId === "local") {
1646
+ adapter = new LocalPtyAdapter(opts.workingDirectory || process.cwd(), cols, rows);
1647
+ } else if (machine.authMethod === "dev-tunnel") {
1648
+ const agentAdapter = new AgentPtyAdapter(machine.tunnelId, session.id);
1649
+ await agentAdapter.connect(opts.workingDirectory, cols, rows);
1650
+ adapter = agentAdapter;
1651
+ } else {
1652
+ const sshAdapter = new SshPtyAdapter(cols, rows);
1653
+ await sshAdapter.connect(machine, opts.workingDirectory);
1654
+ adapter = sshAdapter;
1655
+ }
1656
+ const monitor = new StateMonitor({
1657
+ sessionId: session.id,
1658
+ provider: opts.provider,
1659
+ machineId: opts.machineId,
1660
+ tunnelId: machine?.tunnelId,
1661
+ onStateChange: (state, confidence) => {
1662
+ this.updateState(session.id, state, confidence);
1663
+ }
1664
+ });
1665
+ const entry = { session, adapter, monitor, outputBuffer: "" };
1666
+ this.entries.set(session.id, entry);
1667
+ adapter.onData((data) => {
1668
+ const e = this.entries.get(session.id);
1669
+ if (!e) return;
1670
+ e.outputBuffer += data;
1671
+ if (e.outputBuffer.length > OUTPUT_BUFFER_MAX) {
1672
+ e.outputBuffer = e.outputBuffer.slice(e.outputBuffer.length - OUTPUT_BUFFER_MAX);
1673
+ }
1674
+ this.push("sessions:data", { sessionId: session.id, data });
1675
+ monitor.onPtyData(data);
1676
+ });
1677
+ adapter.onExit((code) => {
1678
+ const e = this.entries.get(session.id);
1679
+ if (e) {
1680
+ const state = code === 0 || code == null ? "done" : "error";
1681
+ this.updateState(session.id, state, "high");
1682
+ }
1683
+ this.push("sessions:destroyed", session.id);
1684
+ });
1685
+ monitor.start();
1686
+ if (opts.provider !== "none") {
1687
+ adapter.autoLaunch(opts.provider);
1688
+ }
1689
+ return { ...session };
1690
+ }
1691
+ destroy(id) {
1692
+ const entry = this.entries.get(id);
1693
+ if (!entry) return;
1694
+ entry.monitor.stop();
1695
+ entry.adapter.kill();
1696
+ this.entries.delete(id);
1697
+ }
1698
+ rename(id, name) {
1699
+ const entry = this.entries.get(id);
1700
+ if (entry) entry.session.name = name;
1701
+ }
1702
+ updateTags(id, tags) {
1703
+ const entry = this.entries.get(id);
1704
+ if (entry) entry.session.tags = tags;
1705
+ }
1706
+ sendInput(id, data) {
1707
+ this.entries.get(id)?.adapter.write(data);
1708
+ }
1709
+ resize(id, cols, rows) {
1710
+ this.entries.get(id)?.adapter.resize(cols, rows);
1711
+ }
1712
+ destroyAll() {
1713
+ for (const id of Array.from(this.entries.keys())) {
1714
+ this.destroy(id);
1715
+ }
1716
+ }
1717
+ updateState(id, state, confidence) {
1718
+ const entry = this.entries.get(id);
1719
+ if (!entry) return;
1720
+ entry.session.state = state;
1721
+ entry.session.stateConfidence = confidence;
1722
+ entry.session.stateEnteredAt = (/* @__PURE__ */ new Date()).toISOString();
1723
+ entry.session.tags["state"] = state;
1724
+ const ev = {
1725
+ sessionId: id,
1726
+ state,
1727
+ confidence,
1728
+ enteredAt: entry.session.stateEnteredAt
1729
+ };
1730
+ this.push("sessions:stateChanged", ev);
1731
+ }
1732
+ }
1733
+ const startupArgs = process.argv.slice(2);
1734
+ let startupPlanFile = null;
1735
+ const pfIdx = startupArgs.indexOf("--plan-file");
1736
+ if (pfIdx !== -1 && startupArgs[pfIdx + 1]) {
1737
+ startupPlanFile = startupArgs[pfIdx + 1];
1738
+ }
1739
+ let mainWindow = null;
1740
+ function sendToRenderer(channel, payload) {
1741
+ mainWindow?.webContents.send(channel, payload);
1742
+ }
1743
+ const machineManager = new MachineManager();
1744
+ const sessionManager = new SessionManager(machineManager, sendToRenderer);
1745
+ function createWindow() {
1746
+ mainWindow = new electron.BrowserWindow({
1747
+ width: 1280,
1748
+ height: 840,
1749
+ minWidth: 900,
1750
+ minHeight: 600,
1751
+ title: "Maestro",
1752
+ backgroundColor: "#1a1a2e",
1753
+ show: false,
1754
+ webPreferences: {
1755
+ preload: path.join(__dirname, "../preload/preload.js"),
1756
+ contextIsolation: true,
1757
+ nodeIntegration: false,
1758
+ sandbox: false
1759
+ }
1760
+ });
1761
+ mainWindow.once("ready-to-show", () => {
1762
+ mainWindow?.show();
1763
+ });
1764
+ mainWindow.on("closed", () => {
1765
+ for (const role of ["run", "replay", "creator"]) {
1766
+ killProcess(role);
1767
+ }
1768
+ sessionManager.destroyAll();
1769
+ machineManager.stopPingLoop();
1770
+ mainWindow = null;
1771
+ });
1772
+ if (process.env["ELECTRON_RENDERER_URL"]) {
1773
+ mainWindow.loadURL(process.env["ELECTRON_RENDERER_URL"]);
1774
+ mainWindow.webContents.openDevTools({ mode: "detach" });
1775
+ } else {
1776
+ mainWindow.loadFile(path.join(__dirname, "../renderer/index.html"));
1777
+ }
1778
+ }
1779
+ electron.app.whenReady().then(() => {
1780
+ electron.Menu.setApplicationMenu(null);
1781
+ registerPlanIpc();
1782
+ registerRunIpc(() => mainWindow);
1783
+ registerReplayIpc();
1784
+ registerConfigIpc();
1785
+ registerCreatorIpc(() => mainWindow);
1786
+ registerMachinesIpc(machineManager);
1787
+ registerSessionsIpc(sessionManager);
1788
+ registerFsIpc(machineManager);
1789
+ machineManager.onStatusChanged((ev) => sendToRenderer("machines:statusChanged", ev));
1790
+ machineManager.startPingLoop();
1791
+ electron.ipcMain.handle("app:getStartupArgs", () => ({
1792
+ planFile: startupPlanFile
1793
+ }));
1794
+ createWindow();
1795
+ electron.app.on("activate", () => {
1796
+ if (electron.BrowserWindow.getAllWindows().length === 0) {
1797
+ createWindow();
1798
+ }
1799
+ });
1800
+ });
1801
+ electron.app.on("window-all-closed", () => {
1802
+ if (process.platform !== "darwin") {
1803
+ electron.app.quit();
1804
+ }
1805
+ });