@rajat-rastogi/maestro 0.2.5 → 0.3.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.
@@ -488,6 +488,9 @@ function registerSessionsIpc(sessionManager2) {
488
488
  electron.ipcMain.handle("sessions:updateTags", async (_e, id, tags) => {
489
489
  sessionManager2.updateTags(id, tags);
490
490
  });
491
+ electron.ipcMain.handle("sessions:interrupt", async (_e, id) => {
492
+ sessionManager2.interrupt(id);
493
+ });
491
494
  electron.ipcMain.handle("sessions:sendInput", async (_e, id, data) => {
492
495
  sessionManager2.sendInput(id, data);
493
496
  });
@@ -507,56 +510,77 @@ class AgentConnectionPool {
507
510
  if (local) env.PATH = `${local}\\Microsoft\\WinGet\\Links;${env.PATH ?? ""}`;
508
511
  return env;
509
512
  }
510
- /** Get a ready WebSocket for tunnelId, spawning devtunnel connect if needed. */
513
+ /**
514
+ * Discover the WSS URL for a tunnel by running `devtunnel show`.
515
+ * Parses the port line to find the HTTPS URL, converts to WSS.
516
+ */
517
+ discoverWssUrl(tunnelId) {
518
+ console.log(`[AgentPool] discovering WSS URL for "${tunnelId}"...`);
519
+ const output = child_process.execSync(`devtunnel show ${tunnelId}`, {
520
+ encoding: "utf8",
521
+ env: this.wingetEnv(),
522
+ timeout: 1e4
523
+ });
524
+ const portLine = output.split("\n").find((l) => l.includes("9741") && l.includes("https://"));
525
+ if (!portLine) {
526
+ throw new Error(`No port 9741 URL found in devtunnel show output for "${tunnelId}"`);
527
+ }
528
+ const match = portLine.match(/https:\/\/[^\s]+/);
529
+ if (!match) {
530
+ throw new Error(`Could not parse URL from: ${portLine.trim()}`);
531
+ }
532
+ const wssUrl = match[0].replace("https://", "wss://").replace(/\/$/, "");
533
+ console.log(`[AgentPool] discovered: ${wssUrl}`);
534
+ return wssUrl;
535
+ }
536
+ /** Get a ready WebSocket for tunnelId, connecting via WSS URL. */
511
537
  async getConnection(tunnelId) {
512
538
  const existing = this.pool.get(tunnelId);
513
539
  if (existing) {
514
- if (existing.ready && existing.ws?.readyState === WebSocket.OPEN) {
540
+ if (existing.ready && existing.ws.readyState === WebSocket.OPEN) {
515
541
  return existing.ws;
516
542
  }
517
- console.log(`[AgentPool] getConnection: "${tunnelId}" — queuing (connection in progress)`);
518
- return new Promise((resolve) => existing.queue.push(resolve));
543
+ if (!existing.ready) {
544
+ console.log(`[AgentPool] getConnection: "${tunnelId}" — queuing (connection in progress)`);
545
+ return new Promise((resolve) => existing.queue.push(resolve));
546
+ }
547
+ this.pool.delete(tunnelId);
519
548
  }
520
- const entry = { proc: null, ws: null, ready: false, queue: [] };
549
+ let wssUrl;
550
+ try {
551
+ wssUrl = this.discoverWssUrl(tunnelId);
552
+ } catch (err) {
553
+ console.log(`[AgentPool] getConnection failed: ${err instanceof Error ? err.message : String(err)}`);
554
+ throw err;
555
+ }
556
+ const entry = { ws: null, wssUrl, ready: false, queue: [] };
521
557
  this.pool.set(tunnelId, entry);
522
- console.log(`[AgentPool] getConnection: spawning "devtunnel connect ${tunnelId}"...`);
523
- entry.proc = child_process.spawn("devtunnel", ["connect", tunnelId], { shell: true, env: this.wingetEnv() });
524
- entry.proc.stderr?.on("data", (data) => {
525
- console.log(`[AgentPool] devtunnel stderr: ${data.toString().trim()}`);
526
- });
527
- entry.proc.on("error", (err) => {
528
- console.log(`[AgentPool] devtunnel process error: ${err.message}`);
529
- this.teardown(tunnelId);
530
- });
531
- entry.proc.on("exit", (code) => {
532
- console.log(`[AgentPool] devtunnel exited with code ${code}`);
533
- this.teardown(tunnelId);
534
- });
535
- console.log(`[AgentPool] getConnection: waiting 4s for devtunnel to establish port forwarding...`);
536
- await new Promise((r) => setTimeout(r, 4e3));
537
- console.log(`[AgentPool] getConnection: opening WebSocket to ws://127.0.0.1:9741...`);
538
- const ws = new WebSocket("ws://127.0.0.1:9741");
558
+ console.log(`[AgentPool] getConnection: opening WebSocket to ${wssUrl}...`);
559
+ const ws = new WebSocket(wssUrl);
539
560
  entry.ws = ws;
540
561
  return new Promise((resolve, reject) => {
541
562
  ws.on("open", () => {
542
- console.log(`[AgentPool] getConnection: WebSocket connected to agent`);
563
+ console.log(`[AgentPool] getConnection: connected to agent via ${wssUrl}`);
543
564
  entry.ready = true;
544
565
  entry.queue.forEach((cb) => cb(ws));
545
566
  entry.queue = [];
546
567
  resolve(ws);
547
568
  });
548
569
  ws.on("error", (err) => {
549
- console.log(`[AgentPool] getConnection: WebSocket error: ${err.message}`);
570
+ console.log(`[AgentPool] WebSocket error for "${tunnelId}": ${err.message}`);
550
571
  this.teardown(tunnelId);
551
572
  reject(new Error(`Agent WebSocket error: ${err.message}`));
552
573
  });
553
- ws.on("close", () => this.teardown(tunnelId));
574
+ ws.on("close", () => {
575
+ console.log(`[AgentPool] WebSocket closed for "${tunnelId}"`);
576
+ this.teardown(tunnelId);
577
+ });
554
578
  });
555
579
  }
556
580
  /** Ping the agent via WebSocket, return true if pong received within 5s. */
557
581
  async ping(tunnelId) {
558
582
  try {
559
- console.log(`[AgentPool] ping: connecting to tunnel "${tunnelId}"...`);
583
+ console.log(`[AgentPool] ping: connecting to "${tunnelId}"...`);
560
584
  const ws = await this.getConnection(tunnelId);
561
585
  console.log(`[AgentPool] ping: connected, sending ping...`);
562
586
  return new Promise((resolve) => {
@@ -588,10 +612,6 @@ class AgentConnectionPool {
588
612
  teardown(tunnelId) {
589
613
  const entry = this.pool.get(tunnelId);
590
614
  if (entry) {
591
- try {
592
- entry.proc?.kill();
593
- } catch {
594
- }
595
615
  try {
596
616
  entry.ws?.close();
597
617
  } catch {
@@ -1656,7 +1676,7 @@ class SessionManager {
1656
1676
  stateEnteredAt: (/* @__PURE__ */ new Date()).toISOString(),
1657
1677
  tags: {
1658
1678
  ...opts.tags,
1659
- "machine": opts.machineId,
1679
+ "machine": machine?.name ?? opts.machineId,
1660
1680
  "provider": opts.provider
1661
1681
  },
1662
1682
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -1723,6 +1743,13 @@ class SessionManager {
1723
1743
  const entry = this.entries.get(id);
1724
1744
  if (entry) entry.session.tags = tags;
1725
1745
  }
1746
+ /** Send Ctrl+C to interrupt the running process, then clear the screen */
1747
+ interrupt(id) {
1748
+ const entry = this.entries.get(id);
1749
+ if (!entry) return;
1750
+ entry.adapter.write("");
1751
+ setTimeout(() => entry.adapter.write("\r"), 200);
1752
+ }
1726
1753
  sendInput(id, data) {
1727
1754
  this.entries.get(id)?.adapter.write(data);
1728
1755
  }
@@ -79,6 +79,7 @@ electron.contextBridge.exposeInMainWorld("maestro", {
79
79
  rename: (id, name) => electron.ipcRenderer.invoke("sessions:rename", id, name),
80
80
  updateTags: (id, tags) => electron.ipcRenderer.invoke("sessions:updateTags", id, tags),
81
81
  getBuffer: (id) => electron.ipcRenderer.invoke("sessions:getBuffer", id),
82
+ interrupt: (id) => electron.ipcRenderer.invoke("sessions:interrupt", id),
82
83
  sendInput: (id, data) => electron.ipcRenderer.invoke("sessions:sendInput", id, data),
83
84
  resize: (id, cols, rows) => electron.ipcRenderer.invoke("sessions:resize", id, cols, rows),
84
85
  onData: (cb) => {