@thinremote/thinr-cli 1.2.0-alpha.1 → 1.2.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.
- package/CHANGELOG.md +45 -1
- package/README.md +192 -16
- package/bin/thinr.js +23 -24
- package/commands/_shared.js +154 -0
- package/commands/dashboard.js +17 -0
- package/commands/device/_shared.js +17 -34
- package/commands/device/console.js +1 -0
- package/commands/device/delete.js +53 -0
- package/commands/device/exec.js +7 -6
- package/commands/device/fs.js +193 -0
- package/commands/device/index.js +9 -0
- package/commands/device/list.js +35 -23
- package/commands/device/logs.js +178 -0
- package/commands/device/property.js +30 -28
- package/commands/device/proxy.js +3 -0
- package/commands/device/resource.js +30 -29
- package/commands/device/status.js +35 -14
- package/commands/device/transfer.js +236 -0
- package/commands/device/update.js +15 -10
- package/commands/fleet/index.js +17 -0
- package/commands/fleet/upgrade.js +275 -0
- package/commands/logout.js +10 -11
- package/commands/playbook/_shared.js +12 -0
- package/commands/playbook/index.js +18 -0
- package/commands/playbook/run.js +302 -0
- package/commands/playbook/validate.js +38 -0
- package/commands/product/_shared.js +8 -0
- package/commands/product/exec.js +243 -0
- package/commands/product/fs.js +218 -0
- package/commands/product/index.js +35 -0
- package/commands/product/list.js +86 -0
- package/commands/product/logs.js +289 -0
- package/commands/product/metric.js +124 -0
- package/commands/product/playbook.js +911 -0
- package/commands/product/property.js +76 -0
- package/commands/product/props.js +125 -0
- package/commands/product/push.js +184 -0
- package/commands/product/resource.js +76 -0
- package/commands/product/status.js +213 -0
- package/commands/profile.js +76 -8
- package/dist/dashboard.js +5644 -0
- package/lib/agent-releases.js +59 -0
- package/lib/agent-versions.js +73 -0
- package/lib/alarms.js +252 -0
- package/lib/api.js +8 -2
- package/lib/auth.js +30 -17
- package/lib/bucket.js +87 -0
- package/lib/command-stream.js +199 -0
- package/lib/config.js +5 -3
- package/lib/console.js +12 -4
- package/lib/constants.js +76 -0
- package/lib/dashboard/event-stream.js +239 -0
- package/lib/dashboard/metric-aggregate.js +64 -0
- package/lib/dashboard/metric-filter.js +39 -0
- package/lib/debug-log.js +93 -0
- package/lib/device-api.js +109 -147
- package/lib/devices.js +18 -21
- package/lib/fleet-upgrade.js +106 -0
- package/lib/format.js +114 -0
- package/lib/mcp/registry.js +14 -0
- package/lib/mcp/tools-alarms.js +510 -0
- package/lib/mcp/tools-buckets.js +103 -0
- package/lib/mcp/tools-devices.js +33 -3
- package/lib/mcp/tools-exec.js +30 -17
- package/lib/mcp/tools-filesystem.js +102 -6
- package/lib/mcp/tools-install.js +153 -0
- package/lib/mcp/tools-logs.js +225 -0
- package/lib/mcp/tools-playbook.js +211 -0
- package/lib/mcp/tools-products.js +1226 -105
- package/lib/mcp/tools-profile.js +634 -0
- package/lib/mcp/tools-tokens.js +371 -0
- package/lib/monitoring.js +26 -1
- package/lib/playbook/actions.js +447 -0
- package/lib/playbook/expression.js +302 -0
- package/lib/playbook/fleet.js +175 -0
- package/lib/playbook/loader.js +247 -0
- package/lib/playbook/runner.js +300 -0
- package/lib/playbook/schema.js +209 -0
- package/lib/playbook/vars.js +209 -0
- package/lib/product/log-presets.js +196 -0
- package/lib/product/logs.js +485 -0
- package/lib/product-orchestrator.js +124 -0
- package/lib/product.js +523 -20
- package/lib/profile.js +665 -0
- package/lib/property.js +7 -40
- package/lib/proxy.js +9 -8
- package/lib/resource.js +10 -14
- package/lib/status.js +78 -37
- package/lib/tokens.js +374 -0
- package/package.json +14 -3
- package/commands/product.js +0 -407
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [1.2.0] - 2026-
|
|
3
|
+
## [1.2.0] - 2026-06-10
|
|
4
4
|
|
|
5
5
|
### Added
|
|
6
6
|
|
|
7
|
+
- `thinr` with no arguments now launches the interactive dashboard
|
|
8
|
+
directly when stdout is a TTY (and a configuration exists), so the
|
|
9
|
+
fleet view is one keystroke away. Non-TTY contexts (pipes, scripts)
|
|
10
|
+
fall back to `--help`. First-time users still get the auth flow,
|
|
11
|
+
then drop straight into the dashboard.
|
|
12
|
+
- TUI dashboard: alarms panel on the overview tab, with rule- and
|
|
13
|
+
instance-level views, severity colouring and live updates over the
|
|
14
|
+
same event stream the rest of the dashboard already uses.
|
|
15
|
+
- `thinr device delete <deviceId>` — remove a device record from the
|
|
16
|
+
platform. Interactive `confirm` by default, `--yes` to skip in
|
|
17
|
+
scripts, idempotent on a missing device. Useful after re-provisioning
|
|
18
|
+
an agent under a new hostname leaves the old `ip-…` record offline.
|
|
19
|
+
- MCP: `thinr_device_delete` — same operation for AI clients.
|
|
20
|
+
- MCP: `thinr_push` and `thinr_pull` — upload a local file to a
|
|
21
|
+
device and download a remote file to local disk, respectively.
|
|
22
|
+
Rounds out filesystem parity with the CLI so an agent can move
|
|
23
|
+
binary or large payloads that don't fit inline in `thinr_write`.
|
|
24
|
+
- MCP: alarms tools (`thinr_alarm_instances`,
|
|
25
|
+
`thinr_alarm_instance_get`, `thinr_alarm_instance_stats`,
|
|
26
|
+
`thinr_alarm_instance_update`, `thinr_alarm_instance_delete`,
|
|
27
|
+
`thinr_alarm_rules`, `thinr_alarm_rule_read`, `thinr_alarm_rule_write`,
|
|
28
|
+
`thinr_alarm_rule_delete`) — list, inspect and manage alarm
|
|
29
|
+
instances and rules from AI clients, with state/severity parsing
|
|
30
|
+
shared with the dashboard.
|
|
31
|
+
- MCP: product profile config tools (buckets, properties, API
|
|
32
|
+
resources) and access-token tools (user-level and device-level).
|
|
7
33
|
- `thinr product exec <productId> <command...>` — run a shell command
|
|
8
34
|
in parallel on every active device of a product, with bounded
|
|
9
35
|
concurrency (`-c, --concurrency`, default 10), per-device timeout
|
|
@@ -23,9 +49,27 @@
|
|
|
23
49
|
|
|
24
50
|
### Changed
|
|
25
51
|
|
|
52
|
+
- Unified filesystem verbs across CLI, MCP and playbooks so the same
|
|
53
|
+
eight nouns mean the same thing everywhere: `read` / `write`
|
|
54
|
+
(inline content) · `push` / `pull` (local ↔ remote file transfer)
|
|
55
|
+
· `ls` · `mkdir` · `rm` · `mv`.
|
|
56
|
+
- CLI: `thinr device cat` is now `thinr device read` (`cat` kept as
|
|
57
|
+
a hidden alias). New `thinr device write <deviceId> <path>
|
|
58
|
+
[content]` accepts the payload as an argument or on stdin.
|
|
59
|
+
- MCP: `thinr_delete` renamed to `thinr_rm`, `thinr_move` renamed to
|
|
60
|
+
`thinr_mv`. No backwards-compatibility aliases.
|
|
61
|
+
- Playbook actions: `delete` → `rm`, `move` → `mv`. The `write`
|
|
62
|
+
action now only accepts inline `content`; use the new `push`
|
|
63
|
+
action to upload a local file (`source`, `destination`). New
|
|
64
|
+
`pull` action downloads a remote file to the local disk.
|
|
26
65
|
- `cli-table3` added as a direct dependency to render the
|
|
27
66
|
product-exec summary.
|
|
28
67
|
|
|
68
|
+
### Removed
|
|
69
|
+
|
|
70
|
+
- `thinr dashboard` subcommand. The dashboard is now the default for
|
|
71
|
+
bare `thinr`; the explicit subcommand had not shipped to anyone yet.
|
|
72
|
+
|
|
29
73
|
## [1.1.0] - 2026-04-16
|
|
30
74
|
|
|
31
75
|
Major release: MCP server for AI clients, subcommand-first CLI, JSON
|
package/README.md
CHANGED
|
@@ -30,11 +30,13 @@ task at hand.
|
|
|
30
30
|
- [JSON output](#json-output)
|
|
31
31
|
- [`thinr device <action> [<deviceId>]`](#thinr-device-action-deviceid)
|
|
32
32
|
- [`thinr product <action> <productId>`](#thinr-product-action-productid)
|
|
33
|
+
- [`thinr playbook <action>`](#thinr-playbook-action)
|
|
33
34
|
- [`thinr profile`](#thinr-profile)
|
|
34
35
|
- [`thinr logout`](#thinr-logout)
|
|
35
36
|
- [Part 2 — MCP server](#part-2--mcp-server)
|
|
36
37
|
- [Starting the server](#starting-the-server)
|
|
37
38
|
- [Tool catalog](#tool-catalog)
|
|
39
|
+
- [Authoring playbooks from natural language](#authoring-playbooks-from-natural-language)
|
|
38
40
|
- [Per-call controls](#per-call-controls)
|
|
39
41
|
- [Integrating with Claude Code](#integrating-with-claude-code)
|
|
40
42
|
- [Profiles and multi-account use](#profiles-and-multi-account-use)
|
|
@@ -337,6 +339,69 @@ Also exposed as an MCP tool (`thinr_product_exec`) with the same
|
|
|
337
339
|
parameters, so agents can drive a fleet operation with a single call
|
|
338
340
|
instead of looping `thinr_exec` per device.
|
|
339
341
|
|
|
342
|
+
#### `playbook`
|
|
343
|
+
|
|
344
|
+
Manage playbooks stored on a product and run them against one device
|
|
345
|
+
or the whole fleet:
|
|
346
|
+
|
|
347
|
+
```bash
|
|
348
|
+
thinr product playbook list <productId> [--json]
|
|
349
|
+
thinr product playbook upload <productId> <name> [file] [--description <text>] [--skip-validation] [--json]
|
|
350
|
+
thinr product playbook download <productId> <name> [-o <file>] [--json]
|
|
351
|
+
thinr product playbook delete <productId> <name> [--json]
|
|
352
|
+
thinr product playbook run <productId> <name> (--device <id> | --fleet) [options]
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
`upload` reads the YAML from a file, or from stdin when the path is
|
|
356
|
+
omitted or `-`. The document is validated against the playbook schema
|
|
357
|
+
before being written to the product storage; an invalid document is
|
|
358
|
+
rejected with a descriptive error and nothing is uploaded. Pass
|
|
359
|
+
`--skip-validation` only to work around a schema-runtime gap (do not
|
|
360
|
+
use it to bypass real errors).
|
|
361
|
+
|
|
362
|
+
`run` targets either a single device (`--device <id>`) or the whole
|
|
363
|
+
fleet (`--fleet`, progressive batch rollout):
|
|
364
|
+
|
|
365
|
+
- `-v, --var <key=value>` — override an overridable playbook variable
|
|
366
|
+
(repeatable; values are coerced to the declared type).
|
|
367
|
+
- `--vars-file <path>` — load variable overrides from a YAML/JSON file.
|
|
368
|
+
- `--dry-run` — print the resolved plan without contacting any device.
|
|
369
|
+
- `--check` — contact devices read-only and report what each step would
|
|
370
|
+
change.
|
|
371
|
+
- `--yes` / `-y` — skip the interactive confirmation prompt.
|
|
372
|
+
|
|
373
|
+
Fleet-only options:
|
|
374
|
+
|
|
375
|
+
- `--batch-size <n>` — devices attempted in parallel per batch
|
|
376
|
+
(default: 5).
|
|
377
|
+
- `--failure-threshold <p>` — abort rollout when cumulative failure
|
|
378
|
+
rate reaches P% (default: 25).
|
|
379
|
+
- `-g, --group <id>` — restrict to an asset group.
|
|
380
|
+
- `--filter <key=value>` — extra server-side filter (repeatable).
|
|
381
|
+
- `--include-offline` — attempt offline devices too (default: active
|
|
382
|
+
only).
|
|
383
|
+
|
|
384
|
+
Each fleet rollout writes a persistent JSON report to
|
|
385
|
+
`playbooks/runs/<timestamp>-<name>-<user>.json` in the product's file
|
|
386
|
+
storage, capturing resolved variables, per-device outcomes, and any
|
|
387
|
+
abort reason.
|
|
388
|
+
|
|
389
|
+
### `thinr playbook <action>`
|
|
390
|
+
|
|
391
|
+
Run or validate a local playbook YAML file (not stored on a product).
|
|
392
|
+
Useful for one-shot or ad-hoc runs:
|
|
393
|
+
|
|
394
|
+
```bash
|
|
395
|
+
thinr playbook validate <file>
|
|
396
|
+
thinr playbook run <file> [--dry-run] [--check] [options]
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
`validate` parses the YAML and reports any schema errors. `run`
|
|
400
|
+
resolves the target block declared inside the playbook and executes
|
|
401
|
+
it; the same variable-override flags as `thinr product playbook run`
|
|
402
|
+
apply. For playbooks that should be reusable across users, upload
|
|
403
|
+
them to their product with `thinr product playbook upload` instead.
|
|
404
|
+
|
|
340
405
|
### `thinr profile`
|
|
341
406
|
|
|
342
407
|
Manage the profile store (see [Profiles and multi-account
|
|
@@ -384,25 +449,86 @@ passed through (via the global flag) to impersonate another account.
|
|
|
384
449
|
|
|
385
450
|
### Tool catalog
|
|
386
451
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
| Area | Tools
|
|
392
|
-
| --------------------- |
|
|
393
|
-
| Discovery | `thinr_devices` (with optional `query` for regex filtering), `thinr_device_info`, `thinr_profiles`
|
|
394
|
-
| Shell | `thinr_exec` (buffered), with streaming stdout/stderr
|
|
395
|
-
| Filesystem | `thinr_read`, `thinr_write`, `thinr_ls`, `thinr_mkdir`, `
|
|
396
|
-
| Resources | `thinr_resource_list` (with `in`/`out` schemas), `thinr_resource_call`
|
|
397
|
-
| Properties | `thinr_property_get`, `thinr_property_set`
|
|
398
|
-
| Scripts (device) | `thinr_script_list`, `thinr_script_write`, `thinr_script_delete`
|
|
399
|
-
| Monitoring and update | `thinr_monitoring`, `thinr_update`
|
|
400
|
-
| Products | `thinr_products`, `thinr_product_delete`, `thinr_device_set_product`
|
|
401
|
-
| Product scripts | `thinr_product_script_list`, `thinr_product_script_read`, `thinr_product_script_write`, `thinr_product_script_delete`
|
|
452
|
+
Grouped by capability. Every tool accepts optional `device`, `user`,
|
|
453
|
+
and `profile` arguments, so a single session can target any
|
|
454
|
+
device/account/environment without restart.
|
|
455
|
+
|
|
456
|
+
| Area | Tools |
|
|
457
|
+
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
458
|
+
| Discovery | `thinr_devices` (with optional `query` for regex filtering), `thinr_device_info`, `thinr_profiles` |
|
|
459
|
+
| Shell | `thinr_exec` (buffered), with streaming stdout/stderr |
|
|
460
|
+
| Filesystem | `thinr_read`, `thinr_write`, `thinr_push`, `thinr_pull`, `thinr_ls`, `thinr_mkdir`, `thinr_rm`, `thinr_mv` |
|
|
461
|
+
| Resources | `thinr_resource_list` (with `in`/`out` schemas), `thinr_resource_call` |
|
|
462
|
+
| Properties | `thinr_property_get`, `thinr_property_set` |
|
|
463
|
+
| Scripts (device) | `thinr_script_list`, `thinr_script_write`, `thinr_script_delete` |
|
|
464
|
+
| Monitoring and update | `thinr_monitoring`, `thinr_bucket_read`, `thinr_update` |
|
|
465
|
+
| Products | `thinr_products`, `thinr_product_delete`, `thinr_device_set_product`, `thinr_product_exec`, `thinr_product_write` |
|
|
466
|
+
| Product scripts | `thinr_product_script_list`, `thinr_product_script_read`, `thinr_product_script_write`, `thinr_product_script_delete` |
|
|
467
|
+
| Playbooks (authoring) | `thinr_playbook_schema`, `thinr_playbook_validate`, `thinr_playbook_run` (ad-hoc) |
|
|
468
|
+
| Product playbooks | `thinr_product_playbook_list`, `thinr_product_playbook_read`, `thinr_product_playbook_write`, `thinr_product_playbook_run` (single device), `thinr_product_playbook_rollout` (fleet), `thinr_product_playbook_delete` |
|
|
402
469
|
|
|
403
470
|
Full input/output schemas are published via standard MCP
|
|
404
471
|
`list_tools`; the client will show them when you connect.
|
|
405
472
|
|
|
473
|
+
### Authoring playbooks from natural language
|
|
474
|
+
|
|
475
|
+
Playbooks are declarative YAML documents — steps like `exec`, `write`,
|
|
476
|
+
`install_package`, `service`, plus a `target` block and a `vars`
|
|
477
|
+
block — that the runner applies to a device or a fleet. With the MCP
|
|
478
|
+
server connected, an AI assistant can turn a natural-language request
|
|
479
|
+
into a validated playbook and store it on the relevant product
|
|
480
|
+
without the user ever writing YAML by hand.
|
|
481
|
+
|
|
482
|
+
The intended authoring loop is:
|
|
483
|
+
|
|
484
|
+
1. **Fetch the schema.** Call `thinr_playbook_schema` once at the
|
|
485
|
+
start of the session to get the authoritative list of top-level
|
|
486
|
+
fields, target block, step actions with their parameters, and the
|
|
487
|
+
two accepted forms of the `vars` block (plain and extended).
|
|
488
|
+
2. **Draft the YAML.** Translate the user's request into a playbook
|
|
489
|
+
draft. Use the extended `vars` form
|
|
490
|
+
(`default` / `description` / `type` / `overridable` / `required`)
|
|
491
|
+
whenever the user hints at knobs the caller may need to tune at
|
|
492
|
+
run time.
|
|
493
|
+
3. **Validate and iterate.** Call `thinr_playbook_validate` with the
|
|
494
|
+
draft. On failure, read the consolidated error message, fix the
|
|
495
|
+
YAML, and call validate again. Never skip this step.
|
|
496
|
+
4. **Upload.** Once validation is clean, call
|
|
497
|
+
`thinr_product_playbook_write` with the product id, a short name
|
|
498
|
+
(letters / digits / underscore / dash), and the validated YAML.
|
|
499
|
+
The tool re-validates server-side; never pass `skip_validation` to
|
|
500
|
+
mask a real error.
|
|
501
|
+
5. **Pre-flight on one device.** Call
|
|
502
|
+
`thinr_product_playbook_run` with a single `device` (plus
|
|
503
|
+
`dry_run: true` or `check: true` for a read-only preview) to
|
|
504
|
+
confirm the steps behave as expected.
|
|
505
|
+
6. **Roll out.** Finally, `thinr_product_playbook_rollout` rolls the
|
|
506
|
+
playbook across the fleet in batches with a failure-rate kill
|
|
507
|
+
switch, and writes a persistent report to `playbooks/runs/` in the
|
|
508
|
+
product file storage.
|
|
509
|
+
|
|
510
|
+
Example prompt the user can give the assistant:
|
|
511
|
+
|
|
512
|
+
> "On product `edge-gateways`, create a playbook called
|
|
513
|
+
> `nginx-upgrade` that updates the nginx package, renders
|
|
514
|
+
> `/etc/nginx/nginx.conf` from a template, and restarts nginx only
|
|
515
|
+
> if the config check passes. Make `worker_connections` an
|
|
516
|
+
> overridable integer variable, default 1024. Then try it on
|
|
517
|
+
> `edge-gw-17` in check mode."
|
|
518
|
+
|
|
519
|
+
A well-behaved assistant translates this into:
|
|
520
|
+
|
|
521
|
+
- `thinr_playbook_schema` → read the authoring contract.
|
|
522
|
+
- Draft YAML → `thinr_playbook_validate` until clean.
|
|
523
|
+
- `thinr_product_playbook_write` with `product="edge-gateways"`,
|
|
524
|
+
`name="nginx-upgrade"`, and the validated content.
|
|
525
|
+
- `thinr_product_playbook_run` with `device="edge-gw-17"` and
|
|
526
|
+
`check: true` to preview what would change.
|
|
527
|
+
|
|
528
|
+
After that a second message like "looks good, roll it out to staging"
|
|
529
|
+
maps to `thinr_product_playbook_rollout` with the matching
|
|
530
|
+
`group` / `filter` and a conservative `batch_size`.
|
|
531
|
+
|
|
406
532
|
### Per-call controls
|
|
407
533
|
|
|
408
534
|
The server starts with no per-session pin: every tool call must say
|
|
@@ -485,7 +611,57 @@ npm link # makes `thinr` available on $PATH
|
|
|
485
611
|
node bin/thinr.js # or run directly without linking
|
|
486
612
|
```
|
|
487
613
|
|
|
488
|
-
|
|
614
|
+
Unit tests live under `tests/` and run offline:
|
|
615
|
+
|
|
616
|
+
```bash
|
|
617
|
+
npm test
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
### Integration tests
|
|
621
|
+
|
|
622
|
+
The product-playbook end-to-end script at
|
|
623
|
+
[`tests/integration/playbook-e2e.js`](tests/integration/playbook-e2e.js)
|
|
624
|
+
drives a real ThinRemote cloud through the full playbook lifecycle
|
|
625
|
+
(create product → upload flat/extended/internal playbooks → list →
|
|
626
|
+
download → replace → reject invalid inputs → optionally run in
|
|
627
|
+
`check` mode against a device → delete). Cleanup of the temporary
|
|
628
|
+
product and its file storage is unconditional (`try / finally`),
|
|
629
|
+
even if the run fails halfway.
|
|
630
|
+
|
|
631
|
+
Prerequisites:
|
|
632
|
+
|
|
633
|
+
- An authenticated CLI profile (`thinr` once, or `THINR_PROFILE=<name>`
|
|
634
|
+
pointing at an existing entry in `~/.config/thinr-cli/config.json`).
|
|
635
|
+
- The account must be allowed to create products and file storages.
|
|
636
|
+
|
|
637
|
+
Invocations:
|
|
638
|
+
|
|
639
|
+
```bash
|
|
640
|
+
# Offline checks only — parses fixtures, exercises resolveVarScope,
|
|
641
|
+
# asserts the dry-run plan. No network access. Safe in CI without
|
|
642
|
+
# credentials.
|
|
643
|
+
node tests/integration/playbook-e2e.js --offline
|
|
644
|
+
|
|
645
|
+
# Full flow against the active profile. Creates a throw-away product
|
|
646
|
+
# named "playbook-e2e-<timestamp>-<rand>" and deletes it on exit.
|
|
647
|
+
node tests/integration/playbook-e2e.js
|
|
648
|
+
|
|
649
|
+
# Add a test device to exercise `check`-mode execution with and
|
|
650
|
+
# without variable overrides. The device must belong to the active
|
|
651
|
+
# user; it does NOT need to be bound to the temporary product.
|
|
652
|
+
THINR_E2E_DEVICE=<device-id> node tests/integration/playbook-e2e.js
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
The script only touches resources it creates itself (identified by
|
|
656
|
+
the `playbook-e2e-` product-id prefix). If a run is interrupted
|
|
657
|
+
before the `finally` cleanup executes — e.g. the process is killed
|
|
658
|
+
— list leftovers with `thinr product list --json` and drop each one
|
|
659
|
+
through the MCP `thinr_product_delete` tool (from an AI client) or
|
|
660
|
+
via a Node one-liner reusing `deleteProductWithStorage` from
|
|
661
|
+
`lib/product.js`.
|
|
662
|
+
|
|
663
|
+
No credentials or tokens are ever written to the repository; the
|
|
664
|
+
script reads them from the standard CLI profile store.
|
|
489
665
|
|
|
490
666
|
## License
|
|
491
667
|
|
package/bin/thinr.js
CHANGED
|
@@ -4,9 +4,8 @@ import { readFileSync } from 'fs';
|
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { dirname, resolve } from 'path';
|
|
6
6
|
import { Command } from 'commander';
|
|
7
|
-
import chalk from 'chalk';
|
|
8
7
|
import figlet from 'figlet';
|
|
9
|
-
import
|
|
8
|
+
import { accent, info } from '../lib/format.js';
|
|
10
9
|
import { configExists, readConfig, setActiveProfile } from '../lib/config.js';
|
|
11
10
|
import { detectJsonModeFromArgv } from '../lib/output.js';
|
|
12
11
|
|
|
@@ -34,22 +33,23 @@ for (let i = 2; i < process.argv.length; i++) {
|
|
|
34
33
|
detectJsonModeFromArgv();
|
|
35
34
|
import { authenticate } from '../lib/auth.js';
|
|
36
35
|
import { deviceCommand } from '../commands/device/index.js';
|
|
36
|
+
import { fleetCommand } from '../commands/fleet/index.js';
|
|
37
37
|
import { logoutCommand } from '../commands/logout.js';
|
|
38
|
-
import {
|
|
38
|
+
import { playbookCommand } from '../commands/playbook/index.js';
|
|
39
|
+
import { productCommand } from '../commands/product/index.js';
|
|
39
40
|
import { profileCommand } from '../commands/profile.js';
|
|
41
|
+
import { runDashboard } from '../commands/dashboard.js';
|
|
40
42
|
import { setBaseURL } from '../lib/api.js';
|
|
41
43
|
import { startMCPServer } from '../lib/mcp/server.js';
|
|
42
44
|
|
|
43
|
-
// Function to display the banner
|
|
44
45
|
function displayBanner() {
|
|
45
|
-
console.log(
|
|
46
|
-
console.log(
|
|
46
|
+
console.log(accent(figlet.textSync('thinr-cli', { horizontalLayout: 'full' })));
|
|
47
|
+
console.log(info('Thin Remote CLI - Remote management for IoT devices'));
|
|
47
48
|
console.log();
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
const program = new Command();
|
|
51
52
|
|
|
52
|
-
// Set up CLI information
|
|
53
53
|
program
|
|
54
54
|
.name('thinr')
|
|
55
55
|
.description('CLI for ThinRemote - Remote management for IoT devices')
|
|
@@ -57,14 +57,16 @@ program
|
|
|
57
57
|
.option('-u, --user <username>', 'API user override (admin impersonation)')
|
|
58
58
|
.option('--profile <name>', 'Configuration profile to use (defaults to the saved default)');
|
|
59
59
|
|
|
60
|
-
// Register commands
|
|
61
60
|
deviceCommand(program);
|
|
61
|
+
fleetCommand(program);
|
|
62
62
|
productCommand(program);
|
|
63
|
+
playbookCommand(program);
|
|
63
64
|
profileCommand(program);
|
|
64
65
|
logoutCommand(program);
|
|
65
66
|
|
|
66
67
|
program
|
|
67
68
|
.command('mcp')
|
|
69
|
+
.helpGroup('Integration:')
|
|
68
70
|
.description('Start the MCP server (stdio) for AI tool integration')
|
|
69
71
|
.action(async () => {
|
|
70
72
|
try {
|
|
@@ -91,27 +93,24 @@ if (configExists()) {
|
|
|
91
93
|
}
|
|
92
94
|
}
|
|
93
95
|
|
|
94
|
-
// Handle no command (just "thinr")
|
|
96
|
+
// Handle no command (just "thinr"): launch the dashboard when running
|
|
97
|
+
// interactively. Non-TTY contexts (pipes, scripts, `thinr | cat`) fall
|
|
98
|
+
// back to `--help` so they get something useful instead of an Ink crash.
|
|
95
99
|
if (process.argv.length <= 2) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
100
|
+
if (!process.stdout.isTTY) {
|
|
101
|
+
program.help();
|
|
102
|
+
} else {
|
|
99
103
|
if (!configExists()) {
|
|
100
|
-
// Display banner only if configuration doesn't exist
|
|
101
104
|
displayBanner();
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
program.help();
|
|
105
|
+
try {
|
|
106
|
+
await authenticate();
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error(`Configuration error: ${error.message}`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
109
111
|
}
|
|
110
|
-
|
|
111
|
-
spinner.fail(`Configuration error: ${error.message}`);
|
|
112
|
-
process.exit(1);
|
|
112
|
+
await runDashboard();
|
|
113
113
|
}
|
|
114
|
-
spinner.succeed('Configuration found');
|
|
115
114
|
} else {
|
|
116
115
|
// Parse arguments - don't show banner for regular commands
|
|
117
116
|
program.parse(process.argv);
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { InvalidArgumentError } from 'commander';
|
|
3
|
+
import { configExists } from '../lib/config.js';
|
|
4
|
+
import {
|
|
5
|
+
setJsonMode,
|
|
6
|
+
isJsonMode,
|
|
7
|
+
printOk,
|
|
8
|
+
printErr,
|
|
9
|
+
createSpinner,
|
|
10
|
+
classifyError,
|
|
11
|
+
} from '../lib/output.js';
|
|
12
|
+
|
|
13
|
+
export function ensureConfigured() {
|
|
14
|
+
if (!configExists()) {
|
|
15
|
+
printErr('Not configured. Run thinr without parameters to set up.', {
|
|
16
|
+
code: 'not_configured',
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function applyJsonFlag(opts) {
|
|
22
|
+
if (opts.json) setJsonMode(true);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Walks up to the root program so subcommands can read `--user` regardless
|
|
26
|
+
// of nesting depth.
|
|
27
|
+
export function getGlobalUser(cmd) {
|
|
28
|
+
let root = cmd;
|
|
29
|
+
while (root.parent) root = root.parent;
|
|
30
|
+
return root.opts().user || null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function parsePositiveInt(label) {
|
|
34
|
+
return (value) => {
|
|
35
|
+
const n = Number(value);
|
|
36
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
37
|
+
throw new InvalidArgumentError(`${label} must be a positive integer`);
|
|
38
|
+
}
|
|
39
|
+
return n;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Collect repeatable `-i key=value` / `-v key=value` pairs into a single
|
|
44
|
+
// object. Uses Commander's InvalidArgumentError so a bad value prints a
|
|
45
|
+
// clean "error: option … argument is invalid" line instead of a stack trace.
|
|
46
|
+
export function collectKeyValue(value, previous = {}) {
|
|
47
|
+
const idx = value.indexOf('=');
|
|
48
|
+
if (idx === -1) throw new InvalidArgumentError('must be key=value');
|
|
49
|
+
return { ...previous, [value.slice(0, idx)]: value.slice(idx + 1) };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Historical aliases kept so existing subcommands don't need renaming at
|
|
53
|
+
// the call sites. Same function, different names where it reads better.
|
|
54
|
+
export const collectInput = collectKeyValue;
|
|
55
|
+
export const collectVar = collectKeyValue;
|
|
56
|
+
|
|
57
|
+
// Resolve a dot-path against a nested object. Returns `undefined` as soon
|
|
58
|
+
// as any intermediate hop is missing — matching the previous inline
|
|
59
|
+
// `reduce((o, k) => o && o[k], obj)` behaviour across the CLI.
|
|
60
|
+
export function extractField(obj, path) {
|
|
61
|
+
if (!path) return obj;
|
|
62
|
+
return path.split('.').reduce((o, k) => (o == null ? o : o[k]), obj);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Live counter for fan-out spinners. Wraps an ora spinner and rewrites
|
|
67
|
+
* its text as work enters/leaves the pool and completes. Use it to avoid
|
|
68
|
+
* reimplementing the `done/in flight` bookkeeping in every fan-out
|
|
69
|
+
* command (product exec, product fs, product push, playbook run…).
|
|
70
|
+
*
|
|
71
|
+
* const progress = new ProgressSpinner(spinner, devices.length,
|
|
72
|
+
* ({ done, total, inFlight }) =>
|
|
73
|
+
* `Running on ${total} devices — ${done}/${total} done · ${inFlight} in flight`);
|
|
74
|
+
* progress.render();
|
|
75
|
+
* // inside worker:
|
|
76
|
+
* progress.startItem();
|
|
77
|
+
* // …
|
|
78
|
+
* progress.finishItem();
|
|
79
|
+
*/
|
|
80
|
+
export class ProgressSpinner {
|
|
81
|
+
constructor(spinner, total, formatText) {
|
|
82
|
+
this.spinner = spinner;
|
|
83
|
+
this.total = total;
|
|
84
|
+
this.formatText = formatText;
|
|
85
|
+
this.done = 0;
|
|
86
|
+
this.inFlight = 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
render() {
|
|
90
|
+
this.spinner.text = this.formatText({
|
|
91
|
+
done: this.done,
|
|
92
|
+
total: this.total,
|
|
93
|
+
inFlight: this.inFlight,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
startItem() {
|
|
98
|
+
this.inFlight++;
|
|
99
|
+
this.render();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
finishItem() {
|
|
103
|
+
this.inFlight--;
|
|
104
|
+
this.done++;
|
|
105
|
+
this.render();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
stop() {
|
|
109
|
+
this.spinner.stop();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* One-shot command skeleton shared by read-only device subcommands
|
|
115
|
+
* (status, list, property, resource…). Handles the spinner start,
|
|
116
|
+
* try/catch, `classifyError`+`printErr` on failure, and the
|
|
117
|
+
* `isJsonMode() ? printOk : console.log` default for the success path.
|
|
118
|
+
*
|
|
119
|
+
* Shape:
|
|
120
|
+
* await runDeviceCommand({
|
|
121
|
+
* start: 'Fetching devices…', // spinner label
|
|
122
|
+
* fn: () => getDevices(filter), // async unit of work
|
|
123
|
+
* success: n => `Found ${n} device(s)`,// string | (result) => string | null
|
|
124
|
+
* failure: 'Failed to list devices', // string on failure
|
|
125
|
+
* onSuccess: (result) => { … }, // optional full render override
|
|
126
|
+
* });
|
|
127
|
+
*
|
|
128
|
+
* When `success` is `null` the spinner just stops (no checkmark), which
|
|
129
|
+
* matches the idiom used by commands whose output block already speaks
|
|
130
|
+
* for the "found" state. When `onSuccess` is omitted the result is
|
|
131
|
+
* printed verbatim (`printOk` in JSON mode, `console.log` otherwise).
|
|
132
|
+
*/
|
|
133
|
+
export async function runDeviceCommand({ start, fn, success, failure, onSuccess }) {
|
|
134
|
+
const spinner = createSpinner(start).start();
|
|
135
|
+
try {
|
|
136
|
+
const result = await fn();
|
|
137
|
+
if (success === null) {
|
|
138
|
+
spinner.stop();
|
|
139
|
+
} else {
|
|
140
|
+
spinner.succeed(typeof success === 'function' ? success(result) : success);
|
|
141
|
+
}
|
|
142
|
+
if (onSuccess) {
|
|
143
|
+
await onSuccess(result);
|
|
144
|
+
} else if (isJsonMode()) {
|
|
145
|
+
printOk(result);
|
|
146
|
+
} else {
|
|
147
|
+
console.log(result);
|
|
148
|
+
}
|
|
149
|
+
} catch (error) {
|
|
150
|
+
spinner.fail(typeof failure === 'function' ? failure(error) : failure);
|
|
151
|
+
const { message, code } = classifyError(error);
|
|
152
|
+
printErr(message, { code });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { ensureConfigured } from './device/_shared.js';
|
|
3
|
+
|
|
4
|
+
export async function runDashboard() {
|
|
5
|
+
ensureConfigured();
|
|
6
|
+
// React's package entry switches between react.development.js and
|
|
7
|
+
// react.production.js based on NODE_ENV at first load. The dev
|
|
8
|
+
// build has a known leak under frequent setState (facebook/react
|
|
9
|
+
// #34770) — heap grows from ~100MB to >1GB in minutes when WS
|
|
10
|
+
// events drive the UI. Force production unless the user has
|
|
11
|
+
// explicitly asked for the dev build (e.g. while debugging).
|
|
12
|
+
// Set BEFORE the dynamic import so the bundle resolves React with
|
|
13
|
+
// the correct env baked in.
|
|
14
|
+
if (!process.env.NODE_ENV) process.env.NODE_ENV = 'production';
|
|
15
|
+
const { run } = await import('../dist/dashboard.js');
|
|
16
|
+
await run();
|
|
17
|
+
}
|
|
@@ -1,48 +1,31 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import { configExists } from '../../lib/config.js';
|
|
5
|
-
import { setJsonMode, classifyError, printErr } from '../../lib/output.js';
|
|
2
|
+
import { classifyError } from '../../lib/output.js';
|
|
3
|
+
import { error as errorStyle } from '../../lib/format.js';
|
|
6
4
|
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// Walks up to the root program so subcommands can read `--user` regardless
|
|
20
|
-
// of nesting depth.
|
|
21
|
-
export function getGlobalUser(cmd) {
|
|
22
|
-
let root = cmd;
|
|
23
|
-
while (root.parent) root = root.parent;
|
|
24
|
-
return root.opts().user || null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Collect repeatable `-i key=value` pairs into a single object. Uses
|
|
28
|
-
// Commander's InvalidArgumentError so a bad value prints a clean
|
|
29
|
-
// "error: option … argument is invalid" line instead of a stack trace.
|
|
30
|
-
export function collectInput(value, previous = {}) {
|
|
31
|
-
const idx = value.indexOf('=');
|
|
32
|
-
if (idx === -1) throw new InvalidArgumentError('must be key=value');
|
|
33
|
-
return { ...previous, [value.slice(0, idx)]: value.slice(idx + 1) };
|
|
34
|
-
}
|
|
5
|
+
export {
|
|
6
|
+
ensureConfigured,
|
|
7
|
+
applyJsonFlag,
|
|
8
|
+
getGlobalUser,
|
|
9
|
+
parsePositiveInt,
|
|
10
|
+
collectKeyValue,
|
|
11
|
+
collectInput,
|
|
12
|
+
collectVar,
|
|
13
|
+
extractField,
|
|
14
|
+
ProgressSpinner,
|
|
15
|
+
runDeviceCommand,
|
|
16
|
+
} from '../_shared.js';
|
|
35
17
|
|
|
36
18
|
// Wrapper for the proxy / console handlers, which return a Promise that
|
|
37
19
|
// resolves with an exit code or rejects with a tagged Error (instead of
|
|
38
|
-
// calling process.exit themselves).
|
|
20
|
+
// calling process.exit themselves). Kept local to `device/` because only
|
|
21
|
+
// the interactive device subcommands use it.
|
|
39
22
|
export const runInteractive = async (fn) => {
|
|
40
23
|
try {
|
|
41
24
|
const exitCode = await fn();
|
|
42
25
|
process.exit(exitCode ?? 0);
|
|
43
26
|
} catch (error) {
|
|
44
27
|
const { message, code } = classifyError(error);
|
|
45
|
-
console.error(
|
|
28
|
+
console.error(errorStyle(`Error [${code}]: ${message}`));
|
|
46
29
|
process.exit(1);
|
|
47
30
|
}
|
|
48
31
|
};
|
|
@@ -5,6 +5,7 @@ import { ensureConfigured, runInteractive } from './_shared.js';
|
|
|
5
5
|
export function registerConsoleCommand(device) {
|
|
6
6
|
device
|
|
7
7
|
.command('console <deviceId>')
|
|
8
|
+
.helpGroup('Remote access:')
|
|
8
9
|
.description('Open an interactive terminal on the device')
|
|
9
10
|
.action((deviceId) =>
|
|
10
11
|
runInteractive(() => {
|