auvezy-terminal-remote 0.7.1 → 0.7.3

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/README.md CHANGED
@@ -15,22 +15,36 @@ Remote-control any terminal program on your PC from a phone or tablet
15
15
  browser over LAN. Start a broker once at boot — open the browser any
16
16
  time to log in, create instances, and run Claude / your shell / any TUI.
17
17
 
18
- <img src="./frontend/public/screenshots/desktop.png" alt="Webapp running Claude Code in a browser tab" width="720">
18
+ <img src="./frontend/public/screenshots/desktop.png" alt="Webapp running Claude Code in a browser tab" width="960">
19
19
 
20
- </div>
20
+ <img src="./frontend/public/screenshots/mobile.png" alt="Webapp on a phone screen" width="400">
21
21
 
22
- ## ✨ Features
22
+ </div>
23
23
 
24
- - **PTY bridge** — node-pty + xterm.js 5, full ANSI, alt-screen TUI safe
25
- - **Claude Code / TUI tuned** — Ink/Yoga reflow fix on resize, alt-screen blocklist, "adapt to current device" PTY sizing
26
- - **Multi-instance** every instance is its own subprocess; one tab bar shows them all, URL is the instance (`/i/<id>/`)
27
- - **Multi-client** many browsers / `attach` clients on one instance, with master arbitration
28
- - **Mobile-first PWA** IME guard, long-press, swipe scroll, viewport-aware fit, install to home screen
29
- - **Custom shortcuts & commands** — define on-screen keys and saved command snippets in the settings panel
30
- - **Reconnect with replay** — scrollback rehydrated on every reconnect, alt-screen TUIs protected
31
- - **LAN-only design** token + shared sessions, `timingSafeEqual`, `/api/hook` is loopback-only, workers bind to 127.0.0.1
32
- - **WSL aware** auto-detects mirrored vs NAT mode; prints a one-liner PowerShell port-forward snippet when needed
33
- - **Boot autostart** — `atr install` writes the systemd / launchd config in one shot
24
+ ## Highlights
25
+
26
+ - **Mobile browser as a first-class terminal client** full-fidelity PTY
27
+ for any program (`claude`, `vim`, `htop`, your shell). The mobile UI ships
28
+ with on-screen shortcut keys, IME-safe input handling, swipe-to-scroll,
29
+ viewport-aware sizing, and an installable PWA manifest.
30
+ - **TUI / Claude Code adaptation** — handles Ink/Yoga reflow on resize so
31
+ Claude does not blank on device rotation; an alt-screen blocklist keeps
32
+ full-screen TUIs (`claude`, `tmux`, `lazygit`, …) clean across reconnects.
33
+ - **Reconnect with replay** — scrollback is rehydrated on every reconnect,
34
+ so transient network drops, lock-screen, or sleeping the device do not
35
+ lose context.
36
+ - **Multi-instance with unified tab bar** — each `atr <program>` runs as an
37
+ independent subprocess at its own URL (`/i/<id>/`); the webapp surfaces
38
+ every active instance in a single tab strip.
39
+ - **Configurable from the settings panel** — on-screen shortcut keys,
40
+ saved command snippets, per-device font size, terminal theme, scrollback
41
+ size, and hook integrations are user-configurable. All preferences are
42
+ persisted to `~/.atrrc`.
43
+ - **LAN-only architecture** — a single shared token (timing-safe comparison),
44
+ workers bound to `127.0.0.1`, and the broker as the sole outward-facing
45
+ process. No public server, no third-party relay.
46
+ - **One-step boot autostart** — `atr install` generates the systemd /
47
+ launchd unit; the service comes up automatically on reboot.
34
48
 
35
49
  Full list: [`docs/FEATURES.md`](./docs/FEATURES.md).
36
50
 
@@ -40,23 +54,23 @@ Full list: [`docs/FEATURES.md`](./docs/FEATURES.md).
40
54
  **worker** is a single PTY instance.
41
55
 
42
56
  ```
43
- Browser / phone Your PC
44
- ┌──────────────┐ ┌──────────────────────────────────┐
45
- │ │ ws://host │ broker (LAN: 0.0.0.0:3000) │
46
- │ webapp PWA │ ──────────────►│ ├─ /api/* (auth / instances / │
47
- │ │ │ │ push / config / …) │
48
- │ │ │ ├─ /i/<id>/ → SPA + base href │
49
- │ │ │ ├─ /i/<id>/api/* → proxy worker │
50
- │ │ │ └─ /i/<id>/ws → proxy worker │
51
- └──────────────┘ │ │ │
52
- │ ▼ │
53
- │ worker A worker B … │
54
- │ 127.0.0.1: 127.0.0.1: │
55
- │ 3001 3002 … │
56
- │ ├─ PTY (claude / shell / TUI) │
57
- │ ├─ /api/health /api/hook │
58
- │ └─ /ws (PTY IO) │
59
- └──────────────────────────────────┘
57
+ Browser / phone Your PC
58
+ ┌──────────────┐ ┌──────────────────────────────────┐
59
+ │ │ ws://host │ broker (LAN: 0.0.0.0:3000) │
60
+ │ webapp PWA │ ──────────────►│ ├─ /api/* (auth / instances / │
61
+ │ │ │ │ push / config / …) │
62
+ │ │ │ ├─ /i/<id>/ → SPA + base href │
63
+ │ │ │ ├─ /i/<id>/api/* → proxy worker │
64
+ │ │ │ └─ /i/<id>/ws → proxy worker │
65
+ └──────────────┘ │ │ │
66
+ │ ▼ │
67
+ │ worker A worker B … │
68
+ │ 127.0.0.1: 127.0.0.1: │
69
+ │ 3001 3002 … │
70
+ │ ├─ PTY (claude / shell / TUI) │
71
+ │ ├─ /api/health /api/hook │
72
+ │ └─ /ws (PTY IO) │
73
+ └──────────────────────────────────┘
60
74
  ```
61
75
 
62
76
  - **broker** (LAN entry point): the only outward-facing process, listens on
package/dist/cli.js CHANGED
@@ -14,7 +14,7 @@ var DEFAULT_PORT, DEFAULT_SESSION_TTL_MS, DEFAULT_AUTH_RATE_LIMIT, DEFAULT_MAX_B
14
14
  var init_constants = __esm({
15
15
  "shared/dist/constants.js"() {
16
16
  "use strict";
17
- DEFAULT_PORT = 3e3;
17
+ DEFAULT_PORT = 3737;
18
18
  DEFAULT_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
19
19
  DEFAULT_AUTH_RATE_LIMIT = 20;
20
20
  DEFAULT_MAX_BUFFER_LINES = 1e4;
@@ -608,6 +608,9 @@ function assignFlag(out, key, value) {
608
608
  case "--strict-port":
609
609
  out.strictPort = value === true || value === "true";
610
610
  return;
611
+ case "--foreground":
612
+ out.foreground = value === true || value === "true";
613
+ return;
611
614
  case "--help":
612
615
  out.help = true;
613
616
  return;
@@ -719,7 +722,8 @@ var init_cli_utils = __esm({
719
722
  "--version",
720
723
  "--no-open",
721
724
  "--wait-confirm",
722
- "--strict-port"
725
+ "--strict-port",
726
+ "--foreground"
723
727
  ]);
724
728
  KNOWN_FLAGS_VALUE = /* @__PURE__ */ new Set([
725
729
  "--port",
@@ -774,8 +778,9 @@ Usage:
774
778
  atr <subcommand> [...] manage the broker / instances (see below)
775
779
 
776
780
  Subcommands:
777
- start [--port n] [--host ip] start the background service (broker) in the foreground
778
- (Ctrl+C to quit). --port / --host override 3000 / 0.0.0.0.
781
+ start [--port n] [--host ip] start the background service (broker); the command
782
+ returns immediately once the broker is healthy.
783
+ add --foreground to keep it attached (systemd / Docker).
779
784
  stop stop the background service
780
785
  status one-shot view: process, token, entry URLs, instances
781
786
  list list all live instances
@@ -804,7 +809,7 @@ Strict argument order:
804
809
  atr -- --weird '--' forces split; default shell with '--weird'
805
810
 
806
811
  Run options (for atr [program]):
807
- -p, --port <n> Background service (broker) port (default 3000). If broker is
812
+ -p, --port <n> Background service (broker) port (default 3737). If broker is
808
813
  already running and on a different port, atr will refuse to
809
814
  start \u2014 run 'atr stop' first if you want to switch.
810
815
  Worker ports are internal and auto-assigned; you don't set them.
@@ -842,7 +847,7 @@ Run options (for atr [program]):
842
847
  passes through automatically)
843
848
 
844
849
  Multi-instance:
845
- The background service (broker) runs once on port 3000 and is shared by all
850
+ The background service (broker) runs once on port 3737 and is shared by all
846
851
  instances. Running atr [program] in different terminals all connect to the
847
852
  same service; PTY children are independent. Click the tab bar in the browser
848
853
  to switch between them. If the service isn't running, the first atr will
@@ -4257,9 +4262,9 @@ async function bindAvailablePort(opts) {
4257
4262
  return { port: actualPort };
4258
4263
  }
4259
4264
  if (strict) {
4260
- throw new InstanceError(ErrorCode.PORT_UNAVAILABLE, `\u7AEF\u53E3 ${preferred} \u5DF2\u88AB\u5360\u7528\uFF08--strict-port \u542F\u7528\uFF0C\u672A\u5C1D\u8BD5\u81EA\u9002\u5E94\uFF09`, 503);
4265
+ throw new InstanceError(ErrorCode.PORT_UNAVAILABLE, `port ${preferred} is in use (--strict-port set; not auto-incrementing)`, 503);
4261
4266
  }
4262
- throw new InstanceError(ErrorCode.PORT_UNAVAILABLE, `\u4ECE\u7AEF\u53E3 ${preferred} \u8D77\u63A2\u6D4B ${maxAttempts} \u4E2A\u5747\u4E0D\u53EF\u7528\uFF08\u6700\u540E EADDRINUSE \u7AEF\u53E3\uFF1A${lastEaddrPort ?? preferred}\uFF09`, 503);
4267
+ throw new InstanceError(ErrorCode.PORT_UNAVAILABLE, `none of ports ${preferred}..${preferred + maxAttempts - 1} are available (last EADDRINUSE: ${lastEaddrPort ?? preferred})`, 503);
4263
4268
  }
4264
4269
  function readListenedPort(server) {
4265
4270
  try {
@@ -4437,7 +4442,7 @@ async function renderQrCode(url, opts = {}) {
4437
4442
  try {
4438
4443
  return await QRCode.toString(url, {
4439
4444
  type: "utf8",
4440
- errorCorrectionLevel: opts.errorCorrectionLevel ?? "L",
4445
+ errorCorrectionLevel: opts.errorCorrectionLevel ?? "M",
4441
4446
  // utf8 模式本身就是半字符垂直压缩,体积约 qrcode-terminal small=true 的 1/2。
4442
4447
  // margin: utf8 渲染器在 margin=1 时有"Invalid array length" bug,避开它;
4443
4448
  // margin=2 视觉上仍然紧凑,且扫码识别率更高
@@ -5444,21 +5449,13 @@ async function startBrokerServer(opts) {
5444
5449
  socket.destroy();
5445
5450
  });
5446
5451
  }
5447
- await new Promise((resolveListen, rejectListen) => {
5448
- const onError = (err) => {
5449
- httpServer.removeListener("listening", onListening);
5450
- rejectListen(err);
5451
- };
5452
- const onListening = () => {
5453
- httpServer.removeListener("error", onError);
5454
- resolveListen();
5455
- };
5456
- httpServer.once("error", onError);
5457
- httpServer.once("listening", onListening);
5458
- httpServer.listen(port, host);
5452
+ const bound = await bindAvailablePort({
5453
+ preferred: port,
5454
+ host,
5455
+ server: httpServer,
5456
+ strict: opts.strictPort ?? false
5459
5457
  });
5460
- const addr = httpServer.address();
5461
- const actualPort = typeof addr === "object" && addr ? addr.port : port;
5458
+ const actualPort = bound.port;
5462
5459
  writeBrokerState({
5463
5460
  pid: process.pid,
5464
5461
  port: actualPort,
@@ -5519,7 +5516,8 @@ var init_broker_server = __esm({
5519
5516
  init_broker_state();
5520
5517
  init_instance_router();
5521
5518
  init_router();
5522
- DEFAULT_BROKER_PORT = 3e3;
5519
+ init_port_finder();
5520
+ DEFAULT_BROKER_PORT = 3737;
5523
5521
  DEFAULT_BROKER_HOST = "0.0.0.0";
5524
5522
  }
5525
5523
  });
@@ -5546,7 +5544,9 @@ async function ensureBroker(opts) {
5546
5544
  if (isBrokerAlive(existing)) {
5547
5545
  if (existing && await probeHealth(existing, fetchImpl, probeTimeoutMs)) {
5548
5546
  if (opts.brokerPort !== void 0 && existing.port !== opts.brokerPort) {
5549
- throw new AppError(ErrorCode.INTERNAL_ERROR, `broker is already running on port ${existing.port}; cannot honor requested port ${opts.brokerPort}. Run 'atr stop' first if you want to switch to ${opts.brokerPort}.`);
5547
+ throw new AppError(ErrorCode.INTERNAL_ERROR, `broker is already running on port ${existing.port}; cannot honor requested port ${opts.brokerPort}.
5548
+ - to use the running broker: drop -p ${opts.brokerPort}
5549
+ - to switch broker: 'atr stop' then 'atr -p ${opts.brokerPort} ...' again`);
5550
5550
  }
5551
5551
  return { state: existing, forked: false };
5552
5552
  }
@@ -5696,6 +5696,41 @@ var init_broker = __esm({
5696
5696
  }
5697
5697
  });
5698
5698
 
5699
+ // backend/dist/utils/wsl-detect.js
5700
+ import { readFileSync as readFileSync9 } from "node:fs";
5701
+ function isWsl(deps) {
5702
+ if (cached !== void 0 && deps === void 0)
5703
+ return cached;
5704
+ const platform2 = deps?.platform ?? process.platform;
5705
+ if (platform2 !== "linux") {
5706
+ if (deps === void 0)
5707
+ cached = false;
5708
+ return false;
5709
+ }
5710
+ let content;
5711
+ try {
5712
+ content = (deps?.readProcVersion ?? defaultReadProcVersion)();
5713
+ } catch {
5714
+ if (deps === void 0)
5715
+ cached = false;
5716
+ return false;
5717
+ }
5718
+ const lower = content.toLowerCase();
5719
+ const result = lower.includes("microsoft") || lower.includes("wsl");
5720
+ if (deps === void 0)
5721
+ cached = result;
5722
+ return result;
5723
+ }
5724
+ function defaultReadProcVersion() {
5725
+ return readFileSync9("/proc/version", "utf-8");
5726
+ }
5727
+ var cached;
5728
+ var init_wsl_detect = __esm({
5729
+ "backend/dist/utils/wsl-detect.js"() {
5730
+ "use strict";
5731
+ }
5732
+ });
5733
+
5699
5734
  // backend/dist/broker/entry-discovery.js
5700
5735
  var entry_discovery_exports = {};
5701
5736
  __export(entry_discovery_exports, {
@@ -5742,13 +5777,8 @@ function discoverEntries(opts) {
5742
5777
  url: buildEntryUrl("127.0.0.1", brokerPort, instanceId, urlOpts)
5743
5778
  });
5744
5779
  }
5745
- const order = {
5746
- tailscale: 0,
5747
- lan: 1,
5748
- ipv6: 2,
5749
- other: 3,
5750
- loopback: 4
5751
- };
5780
+ const wsl = isWsl();
5781
+ const order = wsl ? { lan: 0, ipv6: 1, tailscale: 2, other: 3, loopback: 4 } : { tailscale: 0, lan: 1, ipv6: 2, other: 3, loopback: 4 };
5752
5782
  out.sort((a, b) => {
5753
5783
  if (a.kind !== b.kind)
5754
5784
  return order[a.kind] - order[b.kind];
@@ -5809,6 +5839,7 @@ var init_entry_discovery = __esm({
5809
5839
  "backend/dist/broker/entry-discovery.js"() {
5810
5840
  "use strict";
5811
5841
  init_network();
5842
+ init_wsl_detect();
5812
5843
  }
5813
5844
  });
5814
5845
 
@@ -6306,17 +6337,17 @@ hint: check whether ~/.atr/broker.json is held by a stale process; set ATR_DEBUG
6306
6337
  setTimeout(() => triggerSpawn("timeout"), cfg.spawnTimeoutSec * 1e3).unref();
6307
6338
  }
6308
6339
  }
6340
+ const brokerHost = brokerState.host === "0.0.0.0" || brokerState.host === "::" ? displayIp : brokerState.host;
6309
6341
  void registry.register({
6310
6342
  instanceId,
6311
6343
  name: cfg.instanceName,
6312
- // 0.7.0:host 改为 worker 实际监听地址(loopback)。broker 阶段 3 反代时
6313
- // 直接用这个 host:port 连 worker。displayIp 已不参与 worker 注册
6314
6344
  host: "127.0.0.1",
6315
6345
  port: cfg.port,
6316
6346
  pid: process.pid,
6317
6347
  cwd: cfg.claudeCwd,
6318
6348
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
6319
- headless: cfg.noTerminal
6349
+ headless: cfg.noTerminal,
6350
+ ...brokerHost ? { brokerHost } : {}
6320
6351
  }).catch((err) => logger.warn({ err }, "\u6CE8\u518C\u5B9E\u4F8B\u5931\u8D25"));
6321
6352
  logger.info({
6322
6353
  port: cfg.port,
@@ -7391,7 +7422,7 @@ __export(service_installer_exports, {
7391
7422
  renderSystemdUnit: () => renderSystemdUnit,
7392
7423
  uninstall: () => uninstall
7393
7424
  });
7394
- import { existsSync as existsSync15, mkdirSync as mkdirSync12, readFileSync as readFileSync9, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "node:fs";
7425
+ import { existsSync as existsSync15, mkdirSync as mkdirSync12, readFileSync as readFileSync10, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "node:fs";
7395
7426
  import { resolve as resolve15, dirname as dirname8 } from "node:path";
7396
7427
  import { homedir as homedir9, platform } from "node:os";
7397
7428
  function detectPlatform(env = process.env, plat = platform()) {
@@ -7413,7 +7444,7 @@ After=network.target
7413
7444
 
7414
7445
  [Service]
7415
7446
  Type=simple
7416
- ExecStart=${opts.nodeBin} ${opts.cliPath} start
7447
+ ExecStart=${opts.nodeBin} ${opts.cliPath} start --foreground
7417
7448
  Restart=on-failure
7418
7449
  RestartSec=5s
7419
7450
  ${portEnv}[Install]
@@ -7438,6 +7469,7 @@ function renderLaunchdPlist(opts) {
7438
7469
  <string>${opts.nodeBin}</string>
7439
7470
  <string>${opts.cliPath}</string>
7440
7471
  <string>start</string>
7472
+ <string>--foreground</string>
7441
7473
  </array>
7442
7474
  <key>RunAtLoad</key><true/>
7443
7475
  <key>KeepAlive</key><true/>
@@ -7553,7 +7585,7 @@ var init_service_installer = __esm({
7553
7585
  mkdirSync12(p, o);
7554
7586
  },
7555
7587
  writeFileSync: (p, d, o) => writeFileSync5(p, d, { encoding: "utf-8", ...o ?? {} }),
7556
- readFileSync: (p) => readFileSync9(p, "utf-8"),
7588
+ readFileSync: (p) => readFileSync10(p, "utf-8"),
7557
7589
  rmSync: rmSync3
7558
7590
  };
7559
7591
  ServicePlatformUnsupportedError = class extends AppError {
@@ -7610,8 +7642,8 @@ var cli_exports = {};
7610
7642
  __export(cli_exports, {
7611
7643
  runServiceCli: () => runServiceCli
7612
7644
  });
7613
- import { execSync as execSync2 } from "node:child_process";
7614
- import { readFileSync as readFileSync10 } from "node:fs";
7645
+ import { execSync as execSync2, spawn as spawn4 } from "node:child_process";
7646
+ import { existsSync as existsSync16, openSync as openSync3, readFileSync as readFileSync11 } from "node:fs";
7615
7647
  import { resolve as resolve16, dirname as dirname9 } from "node:path";
7616
7648
  import { fileURLToPath as fileURLToPath3 } from "node:url";
7617
7649
  import { homedir as homedir10 } from "node:os";
@@ -7624,7 +7656,7 @@ function getBrokerVersion() {
7624
7656
  ];
7625
7657
  for (const pkgPath of candidates) {
7626
7658
  try {
7627
- const pkg = JSON.parse(readFileSync10(pkgPath, "utf-8"));
7659
+ const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
7628
7660
  if (pkg.name === "auvezy-terminal-remote" && typeof pkg.version === "string") {
7629
7661
  return pkg.version;
7630
7662
  }
@@ -7636,18 +7668,19 @@ function getBrokerVersion() {
7636
7668
  }
7637
7669
  function getCliPath() {
7638
7670
  const __dirname2 = dirname9(fileURLToPath3(import.meta.url));
7639
- const candidates = [
7640
- resolve16(__dirname2, "cli.js"),
7641
- resolve16(__dirname2, "..", "cli.js")
7642
- ];
7643
- for (const p of candidates) {
7644
- try {
7645
- readFileSync10(p);
7646
- return p;
7647
- } catch {
7648
- }
7671
+ const parentDir = resolve16(__dirname2, "..", "cli.js");
7672
+ const sameDir = resolve16(__dirname2, "cli.js");
7673
+ try {
7674
+ readFileSync11(parentDir);
7675
+ return parentDir;
7676
+ } catch {
7677
+ }
7678
+ try {
7679
+ readFileSync11(sameDir);
7680
+ return sameDir;
7681
+ } catch {
7649
7682
  }
7650
- return candidates[0];
7683
+ return parentDir;
7651
7684
  }
7652
7685
  async function runServiceCli(action, cli) {
7653
7686
  switch (action) {
@@ -7668,6 +7701,10 @@ async function runServiceCli(action, cli) {
7668
7701
  }
7669
7702
  }
7670
7703
  async function runBrokerStart(cli) {
7704
+ const wantForeground = cli.foreground === true || process.env["ATR_BROKER_FOREGROUND"] === "1";
7705
+ if (!wantForeground) {
7706
+ return runBrokerStartDaemonize(cli);
7707
+ }
7671
7708
  const port = cli.port ?? parseEnvPort(process.env["ATR_BROKER_PORT"]) ?? DEFAULT_BROKER_PORT;
7672
7709
  const host = cli.host ?? process.env["ATR_BROKER_HOST"] ?? DEFAULT_BROKER_HOST;
7673
7710
  const brokerVersion = getBrokerVersion();
@@ -7710,7 +7747,7 @@ async function runBrokerStart(cli) {
7710
7747
  const pushService = new PushService();
7711
7748
  await pushService.init();
7712
7749
  startInstanceWatcher(registry.filePath);
7713
- const cliJsPath = resolve16(__dirname2, "cli.js");
7750
+ const cliJsPath = getCliPath();
7714
7751
  const workdirAllow = currentUserConfig.workdirAllow;
7715
7752
  const workdirDeny = currentUserConfig.workdirDeny;
7716
7753
  const spawner = new DefaultInstanceSpawner({
@@ -7738,14 +7775,20 @@ async function runBrokerStart(cli) {
7738
7775
  brokerVersion,
7739
7776
  registry,
7740
7777
  frontendDist,
7741
- brokerApi
7778
+ brokerApi,
7779
+ strictPort: cli.strictPort ?? false
7742
7780
  });
7743
7781
  } catch (err) {
7744
- process.stderr.write(`[atr] startup failed: ${err instanceof Error ? err.message : String(err)}
7782
+ process.stderr.write(`${c.red("[atr]")} startup failed: ${err instanceof Error ? err.message : String(err)}
7745
7783
  `);
7746
7784
  return 1;
7747
7785
  }
7748
- process.stderr.write(`[atr] listening on http://${host}:${handle.port}
7786
+ if (handle.port !== port) {
7787
+ process.stderr.write(`${c.yellow("[atr]")} preferred port ${port} was busy; bound to ${handle.port} instead
7788
+ ` + c.dim(` pass --strict-port if you want atr to refuse to start when ${port} is taken
7789
+ `));
7790
+ }
7791
+ process.stderr.write(`${c.cyan("[atr]")} listening on http://${host}:${handle.port}
7749
7792
  `);
7750
7793
  let stopping = false;
7751
7794
  const stop = async (signal) => {
@@ -7779,6 +7822,83 @@ function installBrokerLogRotator() {
7779
7822
  });
7780
7823
  return rotator;
7781
7824
  }
7825
+ async function runBrokerStartDaemonize(cli) {
7826
+ const tag = c.cyan("[atr]");
7827
+ const port = cli.port ?? parseEnvPort(process.env["ATR_BROKER_PORT"]) ?? DEFAULT_BROKER_PORT;
7828
+ const existing = readBrokerState();
7829
+ if (existing && isBrokerAlive(existing)) {
7830
+ process.stderr.write(`${tag} broker already running on ${existing.host}:${existing.port} (pid=${existing.pid})
7831
+ `);
7832
+ return 0;
7833
+ }
7834
+ const cliJsPath = getCliPath();
7835
+ const entry = resolveDaemonEntry(cliJsPath);
7836
+ const logFd = process.env["ATR_DEBUG_SPAWN"] ? openSync3(`/tmp/atr-broker-${Date.now()}.log`, "a") : "ignore";
7837
+ const childEnv = {
7838
+ ...process.env,
7839
+ ATR_BROKER_FOREGROUND: "1"
7840
+ };
7841
+ if (cli.port !== void 0)
7842
+ childEnv["ATR_BROKER_PORT"] = String(cli.port);
7843
+ if (cli.host !== void 0)
7844
+ childEnv["ATR_BROKER_HOST"] = cli.host;
7845
+ if (cli.strictPort)
7846
+ childEnv["ATR_BROKER_STRICT_PORT"] = "1";
7847
+ const child = spawn4(entry.execPath, [...entry.args, "start", "--foreground"], {
7848
+ env: childEnv,
7849
+ detached: true,
7850
+ stdio: ["ignore", logFd, logFd]
7851
+ });
7852
+ if (typeof child.pid !== "number") {
7853
+ process.stderr.write(`${tag} failed to spawn broker subprocess (no pid)
7854
+ `);
7855
+ return 1;
7856
+ }
7857
+ const earlyExit = [];
7858
+ const earlyError = [];
7859
+ child.once("error", (e) => {
7860
+ earlyError.push(e);
7861
+ });
7862
+ child.once("exit", (code, signal) => {
7863
+ earlyExit.push({ code, signal });
7864
+ });
7865
+ child.unref();
7866
+ const t0 = Date.now();
7867
+ const statePath = defaultBrokerStatePath();
7868
+ while (Date.now() - t0 < DAEMONIZE_TIMEOUT_MS) {
7869
+ const st = readBrokerState(statePath);
7870
+ if (st && isBrokerAlive(st) && st.pid === child.pid) {
7871
+ process.stdout.write(`${tag} broker started on ${c.green(`${st.host}:${st.port}`)} (pid=${st.pid})
7872
+ `);
7873
+ return 0;
7874
+ }
7875
+ await sleep4(DAEMONIZE_POLL_INTERVAL_MS);
7876
+ }
7877
+ let detail = "";
7878
+ if (earlyError[0]) {
7879
+ detail = `
7880
+ - spawn error: ${earlyError[0].message}`;
7881
+ } else if (earlyExit[0]) {
7882
+ detail = `
7883
+ - child exited early (code=${earlyExit[0].code}, signal=${earlyExit[0].signal})`;
7884
+ }
7885
+ process.stderr.write(`${tag} broker did not become ready within ${DAEMONIZE_TIMEOUT_MS}ms.${detail}
7886
+ - check ~/.auvezy/terminal-remote/broker-*.log for errors
7887
+ - or set ATR_DEBUG_SPAWN=1 and retry to capture /tmp/atr-broker-*.log
7888
+ - port ${port} may be busy; try '--port <other>' or '--strict-port' to fail fast
7889
+ `);
7890
+ return 1;
7891
+ }
7892
+ function resolveDaemonEntry(cliJsPath) {
7893
+ if (existsSync16(cliJsPath)) {
7894
+ return { execPath: process.execPath, args: [cliJsPath] };
7895
+ }
7896
+ const tsPath = cliJsPath.replace(/\.js$/, ".ts");
7897
+ if (existsSync16(tsPath)) {
7898
+ return { execPath: process.execPath, args: ["--import", "tsx", tsPath] };
7899
+ }
7900
+ return { execPath: process.execPath, args: [cliJsPath] };
7901
+ }
7782
7902
  async function runBrokerStop() {
7783
7903
  const tag = c.cyan("[atr]");
7784
7904
  const state = readBrokerState();
@@ -7936,12 +8056,12 @@ function writeServiceInstallSection() {
7936
8056
  async function writeTokenSection() {
7937
8057
  process.stdout.write(c.bold("=== Token ===\n"));
7938
8058
  try {
7939
- const { readFileSync: readFileSync11, statSync: statSync6 } = await import("node:fs");
8059
+ const { readFileSync: readFileSync12, statSync: statSync6 } = await import("node:fs");
7940
8060
  const { resolve: pathResolve2 } = await import("node:path");
7941
8061
  const { homedir: homedir11 } = await import("node:os");
7942
8062
  const path = pathResolve2(homedir11(), ".atrrc");
7943
8063
  const stat = statSync6(path);
7944
- const cfg = JSON.parse(readFileSync11(path, "utf-8"));
8064
+ const cfg = JSON.parse(readFileSync12(path, "utf-8"));
7945
8065
  if (typeof cfg.token === "string" && cfg.token.length > 0) {
7946
8066
  process.stdout.write(` token: ${cfg.token}
7947
8067
  `);
@@ -7963,10 +8083,10 @@ async function writeEntriesSection(brokerPort) {
7963
8083
  const { discoverEntries: discoverEntries2, kindLabel: kindLabel2 } = await Promise.resolve().then(() => (init_entry_discovery(), entry_discovery_exports));
7964
8084
  let token;
7965
8085
  try {
7966
- const { readFileSync: readFileSync11 } = await import("node:fs");
8086
+ const { readFileSync: readFileSync12 } = await import("node:fs");
7967
8087
  const { resolve: pathResolve2 } = await import("node:path");
7968
8088
  const { homedir: homedir11 } = await import("node:os");
7969
- const cfg = JSON.parse(readFileSync11(pathResolve2(homedir11(), ".atrrc"), "utf-8"));
8089
+ const cfg = JSON.parse(readFileSync12(pathResolve2(homedir11(), ".atrrc"), "utf-8"));
7970
8090
  if (typeof cfg.token === "string" && cfg.token.length > 0)
7971
8091
  token = cfg.token;
7972
8092
  } catch {
@@ -8025,12 +8145,12 @@ async function runListInstances() {
8025
8145
  async function runShowLogs() {
8026
8146
  const { homedir: homedir11 } = await import("node:os");
8027
8147
  const { resolve: pathResolve2 } = await import("node:path");
8028
- const { existsSync: existsSync16 } = await import("node:fs");
8029
- const { spawn: spawn4 } = await import("node:child_process");
8148
+ const { existsSync: existsSync17 } = await import("node:fs");
8149
+ const { spawn: spawn5 } = await import("node:child_process");
8030
8150
  const today = /* @__PURE__ */ new Date();
8031
8151
  const day = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`;
8032
8152
  const logPath = pathResolve2(homedir11(), ".atr", `broker-${day}.log`);
8033
- if (!existsSync16(logPath)) {
8153
+ if (!existsSync17(logPath)) {
8034
8154
  process.stderr.write(`[atr] today's log not found: ${logPath}
8035
8155
  hint: is the service running? try atr status, or atr start to launch.
8036
8156
  `);
@@ -8038,14 +8158,14 @@ hint: is the service running? try atr status, or atr start to launch.
8038
8158
  }
8039
8159
  process.stderr.write(`[atr] tail -F ${logPath} (Ctrl+C to quit)
8040
8160
  `);
8041
- const tail = spawn4("tail", ["-F", logPath], { stdio: "inherit" });
8161
+ const tail = spawn5("tail", ["-F", logPath], { stdio: "inherit" });
8042
8162
  return new Promise((resolveExit) => {
8043
8163
  tail.on("error", (err) => {
8044
8164
  process.stderr.write(`[atr] cannot spawn tail (${err.message}); falling back to one-shot output:
8045
8165
  `);
8046
- void import("node:fs").then(({ readFileSync: readFileSync11 }) => {
8166
+ void import("node:fs").then(({ readFileSync: readFileSync12 }) => {
8047
8167
  try {
8048
- process.stdout.write(readFileSync11(logPath, "utf-8"));
8168
+ process.stdout.write(readFileSync12(logPath, "utf-8"));
8049
8169
  resolveExit(0);
8050
8170
  } catch (e) {
8051
8171
  process.stderr.write(`[atr] failed to read log: ${e.message}
@@ -8131,7 +8251,7 @@ function parseEnvPort(raw) {
8131
8251
  function sleep4(ms) {
8132
8252
  return new Promise((r) => setTimeout(r, ms));
8133
8253
  }
8134
- var STOP_GRACE_MS, STOP_POLL_INTERVAL_MS;
8254
+ var DAEMONIZE_TIMEOUT_MS, DAEMONIZE_POLL_INTERVAL_MS, STOP_GRACE_MS, STOP_POLL_INTERVAL_MS;
8135
8255
  var init_cli = __esm({
8136
8256
  "backend/dist/broker/cli.js"() {
8137
8257
  "use strict";
@@ -8153,6 +8273,8 @@ var init_cli = __esm({
8153
8273
  init_broker_state();
8154
8274
  init_service_installer();
8155
8275
  init_colors();
8276
+ DAEMONIZE_TIMEOUT_MS = 8e3;
8277
+ DAEMONIZE_POLL_INTERVAL_MS = 100;
8156
8278
  STOP_GRACE_MS = 5e3;
8157
8279
  STOP_POLL_INTERVAL_MS = 100;
8158
8280
  }
@@ -8221,11 +8343,11 @@ void (async () => {
8221
8343
  process.exit(0);
8222
8344
  }
8223
8345
  if (cli.version) {
8224
- const { readFileSync: readFileSync11 } = await import("node:fs");
8346
+ const { readFileSync: readFileSync12 } = await import("node:fs");
8225
8347
  const { resolve: resolve17, dirname: dirname10 } = await import("node:path");
8226
8348
  const { fileURLToPath: fileURLToPath4 } = await import("node:url");
8227
8349
  const __dirname2 = dirname10(fileURLToPath4(import.meta.url));
8228
- const pkg = JSON.parse(readFileSync11(resolve17(__dirname2, "..", "package.json"), "utf-8"));
8350
+ const pkg = JSON.parse(readFileSync12(resolve17(__dirname2, "..", "package.json"), "utf-8"));
8229
8351
  process.stdout.write(`${pkg.version}
8230
8352
  `);
8231
8353
  process.exit(0);