@stage-labs/metro 0.1.0-beta.78 → 0.1.0-beta.79

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage-labs/metro",
3
- "version": "0.1.0-beta.78",
3
+ "version": "0.1.0-beta.79",
4
4
  "description": "The metro command line. Sign in once per machine, then hand your MCP connector list to Claude Code without the credentials touching disk, argv or shell history.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -12,6 +12,8 @@ import { localOwner } from '../agents/file-admin.js';
12
12
  const RESTART_DELAY_MS = 2_000;
13
13
  const RESTART_DELAY_MAX_MS = 30_000;
14
14
  const TAKEN_RE = /listener already exists/i;
15
+ const ADOPTED_MISSES = 2;
16
+ const adoptedCheckMs = (): number => Number(process.env.METRO_TUNNEL_WATCH_MS) || 30_000;
15
17
  const FUNNEL_ON_RE = /\(Funnel on\)/i;
16
18
 
17
19
  export const tunnelWanted = (): boolean => process.env.METRO_TUNNEL?.trim() === 'tailscale';
@@ -25,6 +27,7 @@ export const funnelUrlIn = (text: string): string | null =>
25
27
  export interface Adopted {
26
28
  url: string | null;
27
29
  hint: string;
30
+ alive?: () => Promise<boolean>;
28
31
  }
29
32
 
30
33
  export interface TunnelDriver {
@@ -106,11 +109,17 @@ export function nodeNameIn(statusJson: string): string | null {
106
109
 
107
110
  async function adoptFunnel(bin: string, port: number, probe: Probe, owner: () => string | null): Promise<Adopted> {
108
111
  const name = nodeNameIn(await runText(bin, ['status', '--json']));
112
+ const watched = (url: string): Adopted => ({
113
+ url,
114
+ hint: `a Funnel already publishes this daemon at ${url}; using it`,
115
+ alive: () => probe(url, owner()),
116
+ });
109
117
  if (name !== null) {
110
118
  const url = `https://${name}`;
111
- if (await probe(url, owner())) return { url, hint: `a Funnel already publishes this daemon at ${url}; using it` };
119
+ if (await probe(url, owner())) return watched(url);
112
120
  }
113
- return funnelAlreadyServing(await runText(bin, ['funnel', 'status']), port);
121
+ const found = funnelAlreadyServing(await runText(bin, ['funnel', 'status']), port);
122
+ return found.url === null ? found : watched(found.url);
114
123
  }
115
124
 
116
125
  export function funnelAlreadyServing(status: string, port: number): Adopted {
@@ -240,6 +249,7 @@ export class Tunnel {
240
249
  private closed = false;
241
250
  private output: string[] = [];
242
251
  private restartDelay = RESTART_DELAY_MS;
252
+ private watch: ReturnType<typeof setInterval> | null = null;
243
253
 
244
254
  constructor(
245
255
  private driver: TunnelDriver,
@@ -330,6 +340,7 @@ export class Tunnel {
330
340
  if (found.url !== null) {
331
341
  log.info({ url: found.url }, `${this.driver.name}: ${found.hint}`);
332
342
  this.noticeUrl(found.url);
343
+ if (found.alive !== undefined) this.watchAdopted(found.url, found.alive);
333
344
  return;
334
345
  }
335
346
  log.error({ output }, `${this.driver.name}: ${found.hint}`);
@@ -340,8 +351,38 @@ export class Tunnel {
340
351
  this.restartDelay = Math.min(this.restartDelay * 2, RESTART_DELAY_MAX_MS);
341
352
  }
342
353
 
354
+ private watchAdopted(url: string, alive: () => Promise<boolean>): void {
355
+ let misses = 0;
356
+ const tick = async (): Promise<void> => {
357
+ if (this.closed || liveUrl !== url) {
358
+ this.unwatch();
359
+ return;
360
+ }
361
+ misses = (await alive()) ? 0 : misses + 1;
362
+ if (misses < ADOPTED_MISSES) return;
363
+ this.unwatch();
364
+ log.warn({ url }, `${this.driver.name}: the Funnel this daemon adopted no longer answers; publishing our own`);
365
+ liveUrl = null;
366
+ this.restartDelay = RESTART_DELAY_MS;
367
+ this.start();
368
+ };
369
+ this.unwatch();
370
+ this.watch = setInterval(() => {
371
+ tick().catch((err: unknown) => {
372
+ log.warn({ err: errMsg(err) }, `${this.driver.name}: adopted-funnel check failed`);
373
+ });
374
+ }, adoptedCheckMs());
375
+ this.watch.unref();
376
+ }
377
+
378
+ private unwatch(): void {
379
+ if (this.watch !== null) clearInterval(this.watch);
380
+ this.watch = null;
381
+ }
382
+
343
383
  stop(): void {
344
384
  this.closed = true;
385
+ this.unwatch();
345
386
  this.child?.kill('SIGINT');
346
387
  this.child = null;
347
388
  }
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.1.0-beta.78"
2
+ "version": "0.1.0-beta.79"
3
3
  }