@beeos-ai/cli 1.1.2 → 1.1.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.
Files changed (2) hide show
  1. package/dist/index.js +245 -1
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6583,6 +6583,60 @@ function printFleetNotRunningHint() {
6583
6583
  console.log(" device-agent fleet start # foreground (debug-friendly)");
6584
6584
  console.log("");
6585
6585
  }
6586
+ async function getFleetDeviceSnapshotBestEffort(serial, baseUrl = FLEET_STATUS_BASE_URL) {
6587
+ let res;
6588
+ try {
6589
+ res = await fetch(
6590
+ `${baseUrl}/fleet/devices/${encodeURIComponent(serial)}`,
6591
+ {
6592
+ method: "GET",
6593
+ signal: AbortSignal.timeout(FLEET_NOTIFY_TIMEOUT_MS)
6594
+ }
6595
+ );
6596
+ } catch {
6597
+ return null;
6598
+ }
6599
+ if (!res.ok) {
6600
+ return null;
6601
+ }
6602
+ let body;
6603
+ try {
6604
+ body = await res.json();
6605
+ } catch {
6606
+ return null;
6607
+ }
6608
+ return parseFleetDeviceSnapshot(body);
6609
+ }
6610
+ function parseFleetDeviceSnapshot(raw) {
6611
+ if (typeof raw !== "object" || raw === null) return null;
6612
+ const r = raw;
6613
+ if (typeof r.serial !== "string" || r.serial.length === 0) return null;
6614
+ if (!isFleetDeviceState(r.state)) return null;
6615
+ return {
6616
+ serial: r.serial,
6617
+ state: r.state,
6618
+ instanceId: typeof r.instanceId === "string" ? r.instanceId : null,
6619
+ mcpPid: typeof r.mcpPid === "number" ? r.mcpPid : null,
6620
+ daemonPid: typeof r.daemonPid === "number" ? r.daemonPid : null,
6621
+ sinceMs: typeof r.sinceMs === "number" ? r.sinceMs : 0
6622
+ };
6623
+ }
6624
+ function isFleetDeviceState(v) {
6625
+ switch (v) {
6626
+ case "adb_offline":
6627
+ case "adb_unauthorized":
6628
+ case "awaiting_bind":
6629
+ case "spawning":
6630
+ case "healthy":
6631
+ case "grace":
6632
+ case "degraded":
6633
+ case "gone":
6634
+ case "crashed":
6635
+ return true;
6636
+ default:
6637
+ return false;
6638
+ }
6639
+ }
6586
6640
  var FLEET_STATUS_BASE_URL, FLEET_NOTIFY_TIMEOUT_MS, FLEET_SHUTDOWN_TIMEOUT_MS;
6587
6641
  var init_fleet_notify = __esm({
6588
6642
  "src/commands/device/fleet-notify.ts"() {
@@ -6855,6 +6909,169 @@ var init_instance_picker = __esm({
6855
6909
  }
6856
6910
  });
6857
6911
 
6912
+ // src/commands/device/wait-for-fleet-healthy.ts
6913
+ import ora2 from "ora";
6914
+ async function waitForFleetHealthy(opts) {
6915
+ const timeoutMs = resolveTimeoutMs(opts.timeoutMs);
6916
+ if (timeoutMs <= 0) {
6917
+ return { kind: "skipped", reason: "opt_out" };
6918
+ }
6919
+ const intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS;
6920
+ const useSpinner = (opts.spinner ?? true) && Boolean(process.stdout?.isTTY);
6921
+ const reporter = useSpinner ? new SpinnerReporter() : new BareReporter();
6922
+ reporter.start(opts.serial, timeoutMs);
6923
+ const startedAt = Date.now();
6924
+ let nullStreak = 0;
6925
+ let lastSnapshot = null;
6926
+ try {
6927
+ while (true) {
6928
+ const snapshot = await getFleetDeviceSnapshotBestEffort(
6929
+ opts.serial,
6930
+ opts.fleetBaseUrl
6931
+ );
6932
+ const elapsedMs = Date.now() - startedAt;
6933
+ if (snapshot === null) {
6934
+ nullStreak += 1;
6935
+ if (nullStreak >= FLEET_DEAD_NULL_THRESHOLD) {
6936
+ reporter.skipFleetDown();
6937
+ return { kind: "skipped", reason: "fleet_not_running" };
6938
+ }
6939
+ } else {
6940
+ nullStreak = 0;
6941
+ lastSnapshot = snapshot;
6942
+ if (snapshot.state === "healthy") {
6943
+ reporter.online(snapshot, elapsedMs);
6944
+ return { kind: "online", elapsedMs, snapshot };
6945
+ }
6946
+ reporter.update(snapshot, elapsedMs);
6947
+ }
6948
+ if (elapsedMs + intervalMs >= timeoutMs) {
6949
+ reporter.timeout(lastSnapshot, elapsedMs);
6950
+ return { kind: "timeout", elapsedMs, lastSnapshot };
6951
+ }
6952
+ await sleep6(intervalMs);
6953
+ }
6954
+ } finally {
6955
+ reporter.dispose();
6956
+ }
6957
+ }
6958
+ function resolveTimeoutMs(optsTimeoutMs) {
6959
+ const envRaw = process.env.BEEOS_AGENT_WAIT_TIMEOUT_MS;
6960
+ if (envRaw !== void 0 && envRaw !== "") {
6961
+ const parsed = Number(envRaw);
6962
+ if (Number.isFinite(parsed) && parsed >= 0) return parsed;
6963
+ }
6964
+ if (process.env.BEEOS_NO_WAIT_AGENT === "1") return 0;
6965
+ if (optsTimeoutMs !== void 0) return optsTimeoutMs;
6966
+ return DEFAULT_TIMEOUT_MS2;
6967
+ }
6968
+ function sleep6(ms) {
6969
+ return new Promise((resolve) => setTimeout(resolve, ms));
6970
+ }
6971
+ function formatSec(ms) {
6972
+ return `${(ms / 1e3).toFixed(1)}s`;
6973
+ }
6974
+ function formatOnline(snapshot, elapsedMs) {
6975
+ const mcp = snapshot.mcpPid ?? "?";
6976
+ const daemon = snapshot.daemonPid ?? "?";
6977
+ return `Device fleet healthy: mcp_pid=${mcp} daemon_pid=${daemon} (${formatSec(elapsedMs)})`;
6978
+ }
6979
+ function formatTimeout(serial, lastSnapshot, elapsedMs) {
6980
+ const state = lastSnapshot?.state ?? "unknown";
6981
+ const daemonPid = lastSnapshot?.daemonPid ?? null;
6982
+ return `Waiting for ${serial} healthy timed out after ${formatSec(elapsedMs)} \u2014 current state: ${state}, daemon_pid=${daemonPid ?? "null"}`;
6983
+ }
6984
+ var DEFAULT_TIMEOUT_MS2, DEFAULT_INTERVAL_MS, FLEET_DEAD_NULL_THRESHOLD, SpinnerReporter, BareReporter;
6985
+ var init_wait_for_fleet_healthy = __esm({
6986
+ "src/commands/device/wait-for-fleet-healthy.ts"() {
6987
+ "use strict";
6988
+ init_fleet_notify();
6989
+ DEFAULT_TIMEOUT_MS2 = 3e4;
6990
+ DEFAULT_INTERVAL_MS = 1e3;
6991
+ FLEET_DEAD_NULL_THRESHOLD = 3;
6992
+ SpinnerReporter = class {
6993
+ spinner = null;
6994
+ serial = "";
6995
+ start(serial, timeoutMs) {
6996
+ this.serial = serial;
6997
+ this.spinner = ora2({
6998
+ text: `Waiting for device-agent fleet to mark ${serial} healthy (timeout=${formatSec(timeoutMs)})...`,
6999
+ stream: process.stderr
7000
+ }).start();
7001
+ }
7002
+ update(snapshot, elapsedMs) {
7003
+ if (!this.spinner) return;
7004
+ this.spinner.text = `Waiting for ${this.serial} healthy \u2014 current state=${snapshot.state} (${formatSec(elapsedMs)})`;
7005
+ }
7006
+ online(snapshot, elapsedMs) {
7007
+ if (!this.spinner) return;
7008
+ this.spinner.succeed(formatOnline(snapshot, elapsedMs));
7009
+ this.spinner = null;
7010
+ console.log(
7011
+ " Agent may take ~1-2s more to register on dashboard."
7012
+ );
7013
+ }
7014
+ timeout(lastSnapshot, elapsedMs) {
7015
+ if (!this.spinner) return;
7016
+ this.spinner.warn(formatTimeout(this.serial, lastSnapshot, elapsedMs));
7017
+ this.spinner = null;
7018
+ console.log(
7019
+ ` See ~/.beeos/logs/services/device-agent-${this.serial}.log for details.`
7020
+ );
7021
+ }
7022
+ skipFleetDown() {
7023
+ if (!this.spinner) return;
7024
+ this.spinner.info(
7025
+ "Skipped fleet wait \u2014 supervisor not running. Run `beeos start` or `beeos doctor`."
7026
+ );
7027
+ this.spinner = null;
7028
+ }
7029
+ dispose() {
7030
+ if (this.spinner) {
7031
+ this.spinner.stop();
7032
+ this.spinner = null;
7033
+ }
7034
+ }
7035
+ };
7036
+ BareReporter = class {
7037
+ serial = "";
7038
+ lastState = null;
7039
+ start(serial, timeoutMs) {
7040
+ this.serial = serial;
7041
+ console.log(
7042
+ `Waiting for device-agent fleet to mark ${serial} healthy (timeout=${formatSec(timeoutMs)})...`
7043
+ );
7044
+ }
7045
+ update(snapshot, elapsedMs) {
7046
+ if (snapshot.state === this.lastState) return;
7047
+ this.lastState = snapshot.state;
7048
+ console.log(
7049
+ `[${formatSec(elapsedMs)}] ${this.serial} state=${snapshot.state}`
7050
+ );
7051
+ }
7052
+ online(snapshot, elapsedMs) {
7053
+ console.log(`\u2713 ${formatOnline(snapshot, elapsedMs)}`);
7054
+ console.log(
7055
+ " Agent may take ~1-2s more to register on dashboard."
7056
+ );
7057
+ }
7058
+ timeout(lastSnapshot, elapsedMs) {
7059
+ console.log(`\u26A0 ${formatTimeout(this.serial, lastSnapshot, elapsedMs)}`);
7060
+ console.log(
7061
+ ` See ~/.beeos/logs/services/device-agent-${this.serial}.log for details.`
7062
+ );
7063
+ }
7064
+ skipFleetDown() {
7065
+ console.log(
7066
+ "Skipped fleet wait \u2014 supervisor not running. Run `beeos start` or `beeos doctor`."
7067
+ );
7068
+ }
7069
+ dispose() {
7070
+ }
7071
+ };
7072
+ }
7073
+ });
7074
+
6858
7075
  // src/commands/device/state.ts
6859
7076
  import lockfile from "proper-lockfile";
6860
7077
  function deviceAgentTargetId(serial) {
@@ -7101,10 +7318,17 @@ async function attach(options) {
7101
7318
  const outcome = await notifyFleetRestartDeviceBestEffort(serial);
7102
7319
  if (outcome === "not_running") {
7103
7320
  await maybeNotifyFleetWithHint(cfg);
7321
+ } else {
7322
+ await maybeWaitForFleetHealthy(serial, options);
7104
7323
  }
7105
7324
  return;
7106
7325
  }
7107
7326
  await maybeNotifyFleetWithHint(cfg);
7327
+ await maybeWaitForFleetHealthy(serial, options);
7328
+ }
7329
+ async function maybeWaitForFleetHealthy(serial, options) {
7330
+ if (options.wait === false) return;
7331
+ await waitForFleetHealthy({ serial });
7108
7332
  }
7109
7333
  async function attachAll(cfg, reporter, withVideo, options) {
7110
7334
  const all = await deviceRuntime.listAdbDevices();
@@ -7230,6 +7454,7 @@ async function attachAll(cfg, reporter, withVideo, options) {
7230
7454
  }
7231
7455
  console.log(`Attached ${committed.length} device(s); supervision via device-agent fleet.`);
7232
7456
  });
7457
+ const committedSerials = commits.filter((c) => c.ok).map((c) => c.serial);
7233
7458
  if (reboundSerials.length > 0) {
7234
7459
  let anyNotRunning = false;
7235
7460
  for (const serial of reboundSerials) {
@@ -7238,12 +7463,27 @@ async function attachAll(cfg, reporter, withVideo, options) {
7238
7463
  }
7239
7464
  if (anyNotRunning) {
7240
7465
  await maybeNotifyFleetWithHint(cfg);
7241
- } else if (reboundSerials.length < commits.filter((c) => c.ok).length) {
7466
+ } else if (reboundSerials.length < committedSerials.length) {
7242
7467
  await maybeNotifyFleetWithHint(cfg);
7243
7468
  }
7469
+ await maybeWaitForFleetHealthyMany(committedSerials, options);
7244
7470
  return;
7245
7471
  }
7246
7472
  await maybeNotifyFleetWithHint(cfg);
7473
+ await maybeWaitForFleetHealthyMany(committedSerials, options);
7474
+ }
7475
+ async function maybeWaitForFleetHealthyMany(serials, options) {
7476
+ if (options.wait === false || serials.length === 0) return;
7477
+ await Promise.allSettled(
7478
+ serials.map(
7479
+ (s) => (
7480
+ // Bare mode: ora doesn't compose with parallel callers cleanly,
7481
+ // so we route every serial through the line-per-state-change
7482
+ // reporter even on a TTY.
7483
+ waitForFleetHealthy({ serial: s, spinner: false })
7484
+ )
7485
+ )
7486
+ );
7247
7487
  }
7248
7488
  function commitAttachResults(state, commits) {
7249
7489
  const committed = [];
@@ -7558,6 +7798,7 @@ var init_attach = __esm({
7558
7798
  init_instance_picker();
7559
7799
  init_fallback_banner();
7560
7800
  init_fleet_notify();
7801
+ init_wait_for_fleet_healthy();
7561
7802
  init_state2();
7562
7803
  }
7563
7804
  });
@@ -9932,6 +10173,9 @@ deviceCmd.command("attach").description("Attach an ADB-connected device as an AI
9932
10173
  ).option(
9933
10174
  "--agent-gateway-url <url>",
9934
10175
  "Override the Agent Gateway URL for THIS device only (advanced; multi-region staging). Persists to the device entry so the fleet supervisor reuses it on every restart. Falls back to the global config when omitted."
10176
+ ).option(
10177
+ "--no-wait",
10178
+ "Skip waiting for the fleet supervisor to mark the device healthy after bind. By default, attach polls fleet on 127.0.0.1:7950 for up to 30s (BEEOS_AGENT_WAIT_TIMEOUT_MS overrides) so terminal exit aligns with dashboard agent-online status. Set this for CI/scripts that don't care about post-bind dashboard sync."
9935
10179
  ).action(attach);
9936
10180
  deviceCmd.command("detach").description("Detach a device agent").option("--serial <serial>", "ADB device serial number").option("--all", "Detach all devices", false).option(
9937
10181
  "--rotate-identity",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beeos-ai/cli",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "type": "module",
5
5
  "description": "BeeOS CLI — run AI agents from your desktop",
6
6
  "bin": {