@esp-vision/device-cli 0.1.0 → 0.1.1

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  `@esp-vision/device-cli` installs the `esp-vision-device` command for device discovery, inspection, configuration, control, firmware updates, Python execution, device file operations, JPEG capture, sequential batch plans, and bounded event watches. It is an optional host interface backed by the same `@esp-vision/device-core` package used by ESP-VISION clients.
4
4
 
5
- Requires Node.js 22 or newer. Serial access requires Python with `pyserial`; firmware flashing requires Python `esptool>=5.3,<6`. LAN discovery uses the bundled multicast-DNS client.
5
+ Requires Node.js 22 or newer. Serial access requires Python with `pyserial`; firmware flashing requires Python `esptool==5.6.0`. LAN discovery uses the bundled multicast-DNS client. Check the selected interpreter with `PYTHON --version` and `PYTHON -c "import serial"`; install pyserial there with `PYTHON -m pip install pyserial` (replace `PYTHON` with that executable). Windows Python Launcher users can use `--python py` and `py -m pip install pyserial`. A failed serial setup gets a bounded environment diagnosis after connection cleanup; successful connections, TCP operations, help, and offline firmware checks do not start it. Busy/access-denied errors keep their port-specific guidance; an unknown adapter failure is not proof of missing Python or pyserial.
6
6
 
7
7
  Run `esp-vision-device --help` for the command reference, or `esp-vision-device --help --json` for the versioned machine-readable command, output, and limit capabilities envelope.
8
8
 
@@ -26,7 +26,7 @@ For a continuous app, finish directory operations and uploads before starting it
26
26
 
27
27
  `files list`, `files read`, `files write`, `files remove`, and `files mkdir` operate on device paths. `files read DEVICE_PATH --output LOCAL_FILE` writes binary content to a local file. `files write DEVICE_PATH --file LOCAL_FILE` sends local bytes to the device. Reads are capped at 16 MiB by default; `--max-bytes` can lower that limit.
28
28
 
29
- `capture camera|screen --output LOCAL.jpg` writes one JPEG and returns its path, byte count, source, MIME type, and capture metadata. Images are never embedded as base64 in JSON output.
29
+ `capture camera|screen --output LOCAL.jpg` writes one JPEG and returns its path, byte count, source, MIME type, and capture metadata. Images are never embedded as base64 in JSON output. The `source` field identifies the requested source; it does not certify whether annotations are present. `camera` observes the application's latest camera frame, not the LCD image. Application drawing depends on firmware publication behavior; firmware advertising `debug.capture_frame.observed.v1` supplies an independent copy made at acquisition, before later drawing, without triggering a new acquisition. `screen` captures the current LCD framebuffer, including drawing rendered there; use it to verify LCD overlays and UI. It requires an initialized display, and camera/display dimensions may differ. Neither source starts the application or initializes the sensor/display for you.
30
30
 
31
31
  ## Bounded event watch
32
32
 
@@ -58,3 +58,5 @@ For a continuous app, finish directory operations and uploads before starting it
58
58
  Status, Python output, firmware phases, cancellation, errors, and duration are NDJSON events on stderr. SIGINT exits with code 130, SIGTERM with 143, argument/plan errors with 2, other failures with 1, and success with 0. Busy or uncertain operations are never retried automatically.
59
59
 
60
60
  Inspect JSON output uses the same versioned envelope as other commands. Human-readable inspect output remains a terminal table.
61
+
62
+ Device paths such as `/models` stay absolute device paths on Windows. Git Bash/MSYS may rewrite them; use PowerShell or prefix the individual command with `MSYS2_ARG_CONV_EXCL="*"`, supplying local paths in Windows form. Quoting the device path alone does not prevent conversion. Invalid device paths now fail before connecting, with a shell hint for Windows host paths.
@@ -120,7 +120,7 @@ Bundled from: `node_modules/@esp-vision/device-core/src/firmware_feed.js`, `node
120
120
 
121
121
  ### Source notice 7
122
122
 
123
- Bundled from: `node_modules/@esp-vision/device-core/src/firmware_io.js`, `node_modules/@esp-vision/device-core/src/firmware_release.js`, `node_modules/@esp-vision/device-core/src/node.js`
123
+ Bundled from: `node_modules/@esp-vision/device-core/src/firmware_io.js`, `node_modules/@esp-vision/device-core/src/firmware_release.js`, `node_modules/@esp-vision/device-core/src/node.js`, `src/serial_diagnostics.js`
124
124
 
125
125
  ```text
126
126
  // SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
@@ -128,7 +128,7 @@ Bundled from: `node_modules/@esp-vision/device-core/src/firmware_io.js`, `node_m
128
128
 
129
129
  ### Source notice 8
130
130
 
131
- Bundled from: `node_modules/@esp-vision/device-core/src/device_files.js`, `node_modules/@esp-vision/device-core/src/device_python.js`, `node_modules/@esp-vision/device-core/src/firmware_feed.js`, `node_modules/@esp-vision/device-core/src/firmware_flash.js`, `node_modules/@esp-vision/device-core/src/firmware_io.js`, `node_modules/@esp-vision/device-core/src/firmware_release.js`, `node_modules/@esp-vision/device-core/src/index.js`, `node_modules/@esp-vision/device-core/src/network_discovery.js`, `node_modules/@esp-vision/device-core/src/network_discovery_node.js`, `node_modules/@esp-vision/device-core/src/node.js`, `node_modules/@esp-vision/device-core/src/node_inventory.js`, `node_modules/@esp-vision/device-core/src/node_rom.js`, `node_modules/@esp-vision/device-core/src/node_transport.js`, `src/cli.js`
131
+ Bundled from: `node_modules/@esp-vision/device-core/src/device_files.js`, `node_modules/@esp-vision/device-core/src/device_python.js`, `node_modules/@esp-vision/device-core/src/firmware_feed.js`, `node_modules/@esp-vision/device-core/src/firmware_flash.js`, `node_modules/@esp-vision/device-core/src/firmware_io.js`, `node_modules/@esp-vision/device-core/src/firmware_release.js`, `node_modules/@esp-vision/device-core/src/index.js`, `node_modules/@esp-vision/device-core/src/network_discovery.js`, `node_modules/@esp-vision/device-core/src/network_discovery_node.js`, `node_modules/@esp-vision/device-core/src/node.js`, `node_modules/@esp-vision/device-core/src/node_inventory.js`, `node_modules/@esp-vision/device-core/src/node_rom.js`, `node_modules/@esp-vision/device-core/src/node_transport.js`, `src/cli.js`, `src/serial_diagnostics.js`
132
132
 
133
133
  ```text
134
134
  // SPDX-License-Identifier: Apache-2.0
@@ -3795,6 +3795,60 @@ function stringWidth(string, options2 = {}) {
3795
3795
  return width;
3796
3796
  }
3797
3797
 
3798
+ // src/serial_diagnostics.js
3799
+ import { execFile } from "node:child_process";
3800
+ import { promisify } from "node:util";
3801
+ var check = `import sys
3802
+ try:
3803
+ import serial
3804
+ except ModuleNotFoundError as error:
3805
+ sys.exit(71 if error.name == 'serial' else 73)
3806
+ except Exception:
3807
+ sys.exit(73)
3808
+ sys.exit(0 if callable(getattr(serial, 'Serial', None)) else 72)
3809
+ `;
3810
+ async function explainSerialFailure(failure3, options2, dependencies = {}) {
3811
+ if (failure3?.code === "SERIAL_BUSY") {
3812
+ failure3.message = "Serial port is busy. Close the IDE, serial monitor or other process using this port before retrying.";
3813
+ return failure3;
3814
+ }
3815
+ if (failure3?.code === "SERIAL_ACCESS_DENIED") {
3816
+ failure3.message = "Serial port access was denied. Check port permissions and close other applications using it; Windows may report an occupied port as access denied.";
3817
+ return failure3;
3818
+ }
3819
+ if (!["SERIAL_ERROR", "SERIAL_UNAVAILABLE"].includes(failure3?.code)) return failure3;
3820
+ if (dependencies.signal?.aborted) return failure3;
3821
+ if (["read", "write", "stdout", "stdin"].includes(failure3.details?.stage)) return failure3;
3822
+ let reason = "unknown";
3823
+ try {
3824
+ await (dependencies.execFile ?? promisify(execFile))(options2.python, ["-c", check], {
3825
+ signal: dependencies.signal,
3826
+ timeout: Math.min(options2.timeout ?? 3e3, 3e3),
3827
+ maxBuffer: 4096,
3828
+ windowsHide: true
3829
+ });
3830
+ reason = "available";
3831
+ } catch (cause) {
3832
+ if (dependencies.signal?.aborted) return failure3;
3833
+ if (cause.code === "ENOENT") reason = "missing-python";
3834
+ else if (cause.code === "EACCES") reason = "python-permission";
3835
+ else if (cause.code === 71) reason = "missing-pyserial";
3836
+ else if (cause.code === 72) reason = "invalid-pyserial";
3837
+ else if (cause.code === 73) reason = "import-failed";
3838
+ }
3839
+ const messages = {
3840
+ "missing-python": "The configured Python executable was not found. Check python3 --version (Windows: python --version or py --version), then select an installed interpreter with --python PATH; Windows Python Launcher users can use --python py.",
3841
+ "python-permission": "The configured Python executable could not be started because access was denied. Check executable permissions or select an accessible interpreter with --python PATH.",
3842
+ "missing-pyserial": 'The configured Python interpreter has no pyserial module. Install it in that same interpreter with PYTHON -m pip install pyserial (Windows Launcher: py -m pip install pyserial and --python py), then verify PYTHON -c "import serial". Replace PYTHON with the executable selected by --python.',
3843
+ "invalid-pyserial": "The configured Python imported a serial module without the pyserial Serial API. Check for a conflicting serial.py or serial package; verify pyserial in the interpreter selected by --python.",
3844
+ "import-failed": 'The configured Python could not import pyserial successfully. Run PYTHON -c "import serial" in the same interpreter to inspect the environment; select it with --python PATH.',
3845
+ available: "Python and the pyserial Serial API are available. Check the selected port, cable, OS permissions and whether another application owns the port. The serial failure does not establish a missing dependency.",
3846
+ unknown: 'Serial adapter failed; Python/pyserial availability could not be established. Check PYTHON --version and PYTHON -c "import serial", select that interpreter with --python PATH (Windows Launcher: --python py), and check port permissions and other applications using the port.'
3847
+ };
3848
+ failure3.message = messages[reason];
3849
+ return failure3;
3850
+ }
3851
+
3798
3852
  // node_modules/@esp-vision/device-core/src/device_session.js
3799
3853
  var EV_MUX_VERSION = 4;
3800
3854
  var INLINE_PYTHON_REPL_TIMEOUT_MS = 5e3;
@@ -7807,8 +7861,8 @@ import { createInterface } from "node:readline";
7807
7861
  import { fileURLToPath as fileURLToPath3 } from "node:url";
7808
7862
 
7809
7863
  // node_modules/@esp-vision/device-core/src/node_inventory.js
7810
- import { execFile } from "node:child_process";
7811
- import { promisify } from "node:util";
7864
+ import { execFile as execFile2 } from "node:child_process";
7865
+ import { promisify as promisify2 } from "node:util";
7812
7866
  import { fileURLToPath as fileURLToPath2 } from "node:url";
7813
7867
 
7814
7868
  // node_modules/@esp-vision/device-core/src/network_discovery_node.js
@@ -7949,7 +8003,7 @@ var defaultPython = process.platform === "win32" ? "python" : "python3";
7949
8003
  async function listSerial({ python = defaultPython, signal, timeout = 1e4 } = {}, dependencies = {}) {
7950
8004
  if (!Number.isFinite(timeout) || timeout <= 0) throw new RangeError("Serial enumeration timeout must be positive");
7951
8005
  try {
7952
- const run = dependencies.execFile ?? promisify(execFile);
8006
+ const run = dependencies.execFile ?? promisify2(execFile2);
7953
8007
  const { stdout } = await run(python, [fileURLToPath2(new URL("./node_serial.py", import.meta.url)), "--list"], { signal, timeout, maxBuffer: 1024 * 1024, windowsHide: true });
7954
8008
  const ports = JSON.parse(stdout);
7955
8009
  if (!Array.isArray(ports)) throw new Error("Invalid serial list");
@@ -8174,9 +8228,12 @@ esp-vision-device files write DEVICE_PATH --file LOCAL_FILE [--no-overwrite]
8174
8228
  esp-vision-device files remove DEVICE_PATH [--type file|directory]
8175
8229
  esp-vision-device files mkdir DEVICE_DIR [--recursive]
8176
8230
  esp-vision-device capture camera|screen --output LOCAL.jpg [--quality N] [--max-bytes N]
8231
+ camera: Observe the application's camera-frame source, not the LCD image. Freshness and application drawing depend on firmware publication behavior.
8232
+ screen: Capture the current LCD framebuffer, including drawing rendered there. Use this to verify LCD overlays and UI; display initialization is required.
8177
8233
  esp-vision-device batch PLAN.json [--port PATH | --tcp HOST[:PORT]]
8178
8234
  esp-vision-device watch --duration MS [--port PATH | --tcp HOST[:PORT]]
8179
8235
  Common: --timeout MS, --python PYTHON (default python3; Windows python), --json, --help
8236
+ Serial setup: check PYTHON --version and PYTHON -c "import serial" with the selected interpreter. Install pyserial there with PYTHON -m pip install pyserial. Windows Launcher: --python py and py -m pip install pyserial.
8180
8237
  All commands that operate on a device require --port or --tcp, except inspect and offline firmware check.
8181
8238
  Python --completion start accepts only --path DEVICE.py. It acknowledges the start, disconnects, and exits; it does not retain a daemon or resumable job.
8182
8239
  Python --timeout is an execution deadline for --completion wait only; omit it with --completion start.
@@ -8187,7 +8244,7 @@ Batch plans use schemaVersion 1 with an operations array. Operations run sequent
8187
8244
  In JSON mode stdout contains one schemaVersion 1 JSON envelope. Python output, status, logs, and errors are NDJSON events on stderr.
8188
8245
  Binary file reads and JPEG captures always write to --output. Local output is capped at 16 MiB by default and --max-bytes may lower that cap.
8189
8246
  Firmware versions come from dl.espressif.com by default; local catalogs are advanced overrides.
8190
- Firmware update uses the built-in serial flasher (Python esptool>=5.3,<6). TCP is not OTA.
8247
+ Firmware update uses the built-in serial flasher (Python esptool==5.6.0). TCP is not OTA.
8191
8248
  Default mode update preserves the existing layout/data; explicit mode install writes a complete image, replacing data in its written range. Blank boards require --board and a ROM-capable USJ/UART port, not an application USB-OTG interface.`;
8192
8249
  var cliCapabilities = Object.freeze({
8193
8250
  commands: {
@@ -8197,7 +8254,7 @@ var cliCapabilities = Object.freeze({
8197
8254
  firmware: ["check", "update"],
8198
8255
  python: ["--source CODE", "--file LOCAL.py", "--path DEVICE.py", "live output, log and state NDJSON events"],
8199
8256
  files: ["list", "read", "write", "remove", "mkdir"],
8200
- capture: ["camera JPEG", "screen JPEG"],
8257
+ capture: ["camera JPEG: latest application camera frame, not the LCD image; drawing depends on firmware publication behavior", "screen JPEG: LCD framebuffer with drawing rendered there; requires display initialization"],
8201
8258
  batch: ["sequential schemaVersion 1 JSON plan", "one connection", "stop at first failure"],
8202
8259
  watch: ["bounded output, log, state and diagnostic stream", "one connection", "no script launch or reconnect"]
8203
8260
  },
@@ -8215,6 +8272,13 @@ var error = (code, message, options2 = {}) => Object.assign(new Error(message),
8215
8272
  var optionError = (message) => error("ARGUMENT", message);
8216
8273
  var knownDeviceCommands = ["configure", "control", "firmware", "python", "files", "capture", "batch", "watch"];
8217
8274
  var commonOptions = ["port", "tcp", "python", "timeout", "json", "role"];
8275
+ function devicePath(value, { root = false, fail: fail2 = optionError } = {}) {
8276
+ if (typeof value !== "string" || !value.startsWith("/") || !root && value === "/" || new TextEncoder().encode(value).length > 123 || /[\x00-\x1f\x7f\\"]/.test(value) || value.includes("..") || value.includes("//") || value.length > 1 && value.endsWith("/")) {
8277
+ const shellHint = typeof value === "string" && /^[A-Za-z]:[\\/]/.test(value) ? ' Received a Windows host path; Git Bash/MSYS may have converted the device path. Use PowerShell or prefix this command with MSYS2_ARG_CONV_EXCL="*" and keep local paths in Windows form.' : "";
8278
+ throw fail2("Expected an absolute device path (for example /models), at most 123 UTF-8 bytes, without traversal, trailing slash or forbidden characters." + shellHint);
8279
+ }
8280
+ return value;
8281
+ }
8218
8282
  function parseTcpAddress(value) {
8219
8283
  const match = /^(?:\[([0-9a-fA-F:]+)\]|([^\s:/@?#]+))(?::([0-9]+))?$/.exec(value);
8220
8284
  if (!match || Number(match[3] ?? 3232) < 1 || Number(match[3] ?? 3232) > 65535) {
@@ -8367,6 +8431,7 @@ function parseCli(args) {
8367
8431
  if (!endpoint) throw optionError("Specify --port or --tcp");
8368
8432
  const forms = ["source", "file", "path"].filter((key) => v[key] !== void 0);
8369
8433
  if (forms.length !== 1) throw optionError("Specify exactly one of --source, --file or --path");
8434
+ if (v.path !== void 0) devicePath(v.path);
8370
8435
  options2.completion = v.completion ?? "wait";
8371
8436
  if (!["wait", "start"].includes(options2.completion)) throw optionError("--completion must be wait or start");
8372
8437
  if (options2.completion === "start" && v.path === void 0) throw optionError("--completion start requires device --path");
@@ -8390,6 +8455,7 @@ function parseCli(args) {
8390
8455
  if (v.path !== void 0) throw optionError(`files ${subcommand} uses a positional device path`);
8391
8456
  options2.path = pathPositional;
8392
8457
  }
8458
+ devicePath(options2.path, { root: subcommand === "list" });
8393
8459
  if (subcommand === "read") {
8394
8460
  if (!v.output) throw optionError("files read requires --output LOCAL_FILE");
8395
8461
  options2.maxBytes = parsePositiveInteger(v["max-bytes"] ?? String(MAX_FILE_OUTPUT), "--max-bytes", { max: MAX_FILE_OUTPUT });
@@ -8438,7 +8504,27 @@ async function passwordFromInput(options2) {
8438
8504
  }
8439
8505
  function errorInfo(failure3) {
8440
8506
  if (!failure3) return void 0;
8441
- return { code: failure3.code ?? "COMMAND_FAILED", message: failure3.message ?? "Command failed" };
8507
+ const info = { code: failure3.code ?? "COMMAND_FAILED", message: failure3.message ?? "Command failed" };
8508
+ if (failure3.code === "ROM_OPERATION_FAILED") {
8509
+ if (typeof failure3.operation === "string") info.operation = failure3.operation.slice(0, 64);
8510
+ if (typeof failure3.cause === "string") info.detail = failure3.cause.slice(0, 1024);
8511
+ }
8512
+ if (failure3.reset?.acknowledged === false && failure3.reset.outcome === "unknown") {
8513
+ info.reset = { acknowledged: false, outcome: "unknown" };
8514
+ }
8515
+ if (String(failure3.code).startsWith("ROM_") && typeof failure3.transition?.command?.attempted === "boolean") {
8516
+ const transition = failure3.transition;
8517
+ info.transition = {
8518
+ command: {
8519
+ attempted: transition.command.attempted,
8520
+ acknowledged: transition.command.acknowledged === true,
8521
+ outcome: ["unknown", "not_attempted"].includes(transition.command.outcome) ? transition.command.outcome : "unknown"
8522
+ },
8523
+ observed: transition.observed === "rom" ? "rom" : null,
8524
+ cleanup: typeof transition.cleanup?.ok === "boolean" ? { ok: transition.cleanup.ok } : null
8525
+ };
8526
+ }
8527
+ return info;
8442
8528
  }
8443
8529
  function attachCleanupFailure(primary, cleanupFailure) {
8444
8530
  if (primary && cleanupFailure) {
@@ -8458,12 +8544,7 @@ function validatePlan(plan, planFile) {
8458
8544
  if (typeof value !== "string" || value.length === 0 || value.includes("\0")) throw planError("Batch local paths must be nonempty strings");
8459
8545
  return resolve(base, value);
8460
8546
  };
8461
- const devicePath = (value, { root = false } = {}) => {
8462
- if (typeof value !== "string" || !root && value === "/" || !value.startsWith("/") || value.length > 123 || /[\x00-\x1f\x7f\\"]/.test(value) || value.includes("..") || value.includes("//") || value.length > 1 && value.endsWith("/")) {
8463
- throw planError("Batch device paths must be absolute valid device paths");
8464
- }
8465
- return value;
8466
- };
8547
+ const planDevicePath = (value, options2 = {}) => devicePath(value, { ...options2, fail: planError });
8467
8548
  return plan.operations.map((operation, index) => {
8468
8549
  const fail2 = (message) => {
8469
8550
  throw planError(`Batch operation ${index + 1}: ${message}`);
@@ -8484,27 +8565,27 @@ function validatePlan(plan, planFile) {
8484
8565
  const forms = ["source", "file", "path"].filter((key) => Object.hasOwn(operation, key));
8485
8566
  if (forms.length !== 1) fail2("python requires exactly one of source, file or path");
8486
8567
  if (operation.source !== void 0 && typeof operation.source !== "string") fail2("source must be a string");
8487
- if (operation.path !== void 0) copy.path = devicePath(operation.path);
8568
+ if (operation.path !== void 0) copy.path = planDevicePath(operation.path);
8488
8569
  if (operation.file !== void 0) copy.file = local(operation.file);
8489
8570
  } else if (operation.op === "files.list") {
8490
- copy.path = devicePath(operation.path ?? "/", { root: true });
8571
+ copy.path = planDevicePath(operation.path ?? "/", { root: true });
8491
8572
  if (operation.recursive !== void 0 && typeof operation.recursive !== "boolean") fail2("recursive must be boolean");
8492
8573
  } else if (operation.op === "files.read") {
8493
- copy.path = devicePath(operation.path);
8574
+ copy.path = planDevicePath(operation.path);
8494
8575
  if (operation.output === void 0) fail2("files.read requires output");
8495
8576
  copy.output = local(operation.output);
8496
8577
  copy.maxBytes = operation.maxBytes ?? MAX_FILE_OUTPUT;
8497
8578
  if (!Number.isSafeInteger(copy.maxBytes) || copy.maxBytes < 1 || copy.maxBytes > MAX_FILE_OUTPUT) fail2("maxBytes must be 1..16 MiB");
8498
8579
  } else if (operation.op === "files.write") {
8499
- copy.path = devicePath(operation.path);
8580
+ copy.path = planDevicePath(operation.path);
8500
8581
  if (operation.file === void 0) fail2("files.write requires file");
8501
8582
  copy.file = local(operation.file);
8502
8583
  if (operation.overwrite !== void 0 && typeof operation.overwrite !== "boolean") fail2("overwrite must be boolean");
8503
8584
  } else if (operation.op === "files.remove") {
8504
- copy.path = devicePath(operation.path);
8585
+ copy.path = planDevicePath(operation.path);
8505
8586
  if (operation.type !== void 0 && !["file", "directory"].includes(operation.type)) fail2("type must be file or directory");
8506
8587
  } else if (operation.op === "files.mkdir") {
8507
- copy.path = devicePath(operation.path);
8588
+ copy.path = planDevicePath(operation.path);
8508
8589
  if (operation.recursive !== void 0 && typeof operation.recursive !== "boolean") fail2("recursive must be boolean");
8509
8590
  } else if (operation.op === "capture") {
8510
8591
  if (!["camera", "screen"].includes(operation.source)) fail2("capture source must be camera or screen");
@@ -8742,6 +8823,24 @@ function createWatchObserver(device, options2, dependencies) {
8742
8823
  }
8743
8824
  };
8744
8825
  }
8826
+ async function cliInventory(options2, dependencies) {
8827
+ let serialFailure;
8828
+ const listing = await inventory(options2, {
8829
+ ...dependencies,
8830
+ async listSerial(request) {
8831
+ try {
8832
+ return await (dependencies.listSerial ?? listSerial)(request);
8833
+ } catch (failure3) {
8834
+ serialFailure = await explainSerialFailure(failure3, options2, dependencies);
8835
+ throw serialFailure;
8836
+ }
8837
+ }
8838
+ });
8839
+ if (serialFailure) for (const warning of listing.warnings) {
8840
+ if (warning.source === "serial") warning.message = serialFailure.message;
8841
+ }
8842
+ return listing;
8843
+ }
8745
8844
  async function runCli(options2, dependencies = {}) {
8746
8845
  if (options2.help) return { help: usage };
8747
8846
  const startedAt = Date.now();
@@ -8756,13 +8855,13 @@ async function runCli(options2, dependencies = {}) {
8756
8855
  }
8757
8856
  }
8758
8857
  if (options2.command === "inspect" && !endpoint && !options2.select) {
8759
- const listing = await (dependencies.inventory ?? inventory)({ ...options2, signal: dependencies.signal, timeout: Math.min(options2.timeout, 1e4) }, dependencies);
8760
- if (listing.sourcesAvailable === 0) throw error("ENUMERATION_UNAVAILABLE", "No inventory source succeeded; check Python/pyserial, multicast-dns and permissions");
8858
+ const listing = await (dependencies.inventory ?? cliInventory)({ ...options2, signal: dependencies.signal, timeout: Math.min(options2.timeout, 1e4) }, dependencies);
8859
+ if (listing.sourcesAvailable === 0) throw error("ENUMERATION_UNAVAILABLE", `No inventory source succeeded. ${listing.warnings.map((warning) => warning.message).join(" ")}`);
8761
8860
  emit(dependencies, { event: "duration", milliseconds: Date.now() - startedAt });
8762
8861
  return listing;
8763
8862
  }
8764
8863
  if (options2.command === "inspect" && !endpoint && options2.select) {
8765
- const listing = await (dependencies.inventory ?? inventory)({ ...options2, signal: dependencies.signal, timeout: Math.min(options2.timeout, 1e4) }, dependencies);
8864
+ const listing = await (dependencies.inventory ?? cliInventory)({ ...options2, signal: dependencies.signal, timeout: Math.min(options2.timeout, 1e4) }, dependencies);
8766
8865
  endpoint = listing.devices.find((row) => row.number === options2.select);
8767
8866
  if (!endpoint) {
8768
8867
  emit(dependencies, { event: "duration", milliseconds: Date.now() - startedAt });
@@ -8845,6 +8944,9 @@ async function runCli(options2, dependencies = {}) {
8845
8944
  primary = attachCleanupFailure(primary, cleanupFailure);
8846
8945
  }
8847
8946
  emit(dependencies, { event: "state", state: "disconnected" });
8947
+ if (primary && endpoint?.interface !== "tcp" && (endpoint || options2.port)) {
8948
+ primary = await explainSerialFailure(primary, options2, dependencies);
8949
+ }
8848
8950
  const durationMs = Date.now() - startedAt;
8849
8951
  emit(dependencies, { event: "duration", milliseconds: durationMs });
8850
8952
  if (primary) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@esp-vision/device-cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Standalone command line interface for ESP-VISION devices.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,7 +36,7 @@
36
36
  "cli"
37
37
  ],
38
38
  "devDependencies": {
39
- "@esp-vision/device-core": "0.5.1",
39
+ "@esp-vision/device-core": "~0.5.0",
40
40
  "esbuild": "0.25.12",
41
41
  "multicast-dns": "7.2.5",
42
42
  "string-width": "7.2.0"
@@ -45,5 +45,12 @@
45
45
  "type": "git",
46
46
  "url": "git+https://github.com/espressif/esp-vision.git",
47
47
  "directory": "host/device-cli"
48
+ },
49
+ "espVisionReleaseType": "patch",
50
+ "espVision": {
51
+ "deviceCore": "~0.5.0",
52
+ "resolved": {
53
+ "deviceCore": "0.5.1"
54
+ }
48
55
  }
49
56
  }