@thinremote/thinr-cli 1.1.0 → 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 +71 -0
- package/README.md +230 -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/concurrency.js +52 -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 +1343 -10
- 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 +15 -3
- package/commands/product.js +0 -167
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,75 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.2.0] - 2026-06-10
|
|
4
|
+
|
|
5
|
+
### Added
|
|
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).
|
|
33
|
+
- `thinr product exec <productId> <command...>` — run a shell command
|
|
34
|
+
in parallel on every active device of a product, with bounded
|
|
35
|
+
concurrency (`-c, --concurrency`, default 10), per-device timeout
|
|
36
|
+
(`--timeout`, default 30 s), `--fail-fast`, asset-group filter
|
|
37
|
+
(`-g, --group`) and an `-a, --all` switch to include offline
|
|
38
|
+
devices. Live progress shows a single spinner with done/total
|
|
39
|
+
counters; the final render uses a `cli-table3` table per device
|
|
40
|
+
plus a compact `Output:` section and a one-line totals bar.
|
|
41
|
+
- `thinr_product_exec` MCP tool — the same fan-out as the CLI
|
|
42
|
+
subcommand, returning a consolidated text report so an AI agent
|
|
43
|
+
can roll out a change across a whole fleet in a single call
|
|
44
|
+
instead of looping `thinr_exec` per device.
|
|
45
|
+
- `lib/concurrency.js#runPool` — tiny helper used by both entry
|
|
46
|
+
points; keeps at most N workers in flight and returns a parallel
|
|
47
|
+
results array. `runPool` plus an internal try/catch in each worker
|
|
48
|
+
keeps fleet-wide errors localised per device.
|
|
49
|
+
|
|
50
|
+
### Changed
|
|
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.
|
|
65
|
+
- `cli-table3` added as a direct dependency to render the
|
|
66
|
+
product-exec summary.
|
|
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
|
+
|
|
3
73
|
## [1.1.0] - 2026-04-16
|
|
4
74
|
|
|
5
75
|
Major release: MCP server for AI clients, subcommand-first CLI, JSON
|
|
@@ -128,6 +198,7 @@ consistent error taxonomy across both interfaces.
|
|
|
128
198
|
- Property listing and retrieval
|
|
129
199
|
- Device resource listing and execution
|
|
130
200
|
|
|
201
|
+
[1.2.0]: https://github.com/Thin-Remote/thinr-cli/compare/1.1.0...1.2.0
|
|
131
202
|
[1.1.0]: https://github.com/Thin-Remote/thinr-cli/compare/1.0.2...1.1.0
|
|
132
203
|
[1.0.2]: https://github.com/Thin-Remote/thinr-cli/compare/1.0.1...1.0.2
|
|
133
204
|
[1.0.1]: https://github.com/Thin-Remote/thinr-cli/compare/1.0.0...1.0.1
|
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)
|
|
@@ -299,6 +301,107 @@ Options:
|
|
|
299
301
|
Offline devices are skipped for `resource` — the server would reject
|
|
300
302
|
the call anyway.
|
|
301
303
|
|
|
304
|
+
#### `exec`
|
|
305
|
+
|
|
306
|
+
Run a shell command in parallel across every active device of the
|
|
307
|
+
product and report a per-device status table plus each device's
|
|
308
|
+
output:
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
thinr product exec <productId> <command...> [options]
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Options:
|
|
315
|
+
|
|
316
|
+
- `-j, --json`: Output as JSON. Emits a single envelope with a
|
|
317
|
+
`summary` block and a `results[]` array carrying stdout, stderr,
|
|
318
|
+
exit code, `timedOut` flag and per-device duration.
|
|
319
|
+
- `-c, --concurrency <n>`: Max parallel executions (default: 10)
|
|
320
|
+
- `--timeout <seconds>`: Per-device timeout (default: 30)
|
|
321
|
+
- `--fail-fast`: Stop dequeueing new devices as soon as one fails
|
|
322
|
+
- `-g, --group <group>`: Filter devices by asset group
|
|
323
|
+
- `-a, --all`: Include offline devices (default: only active)
|
|
324
|
+
|
|
325
|
+
Examples:
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
# hostname on every active device
|
|
329
|
+
thinr product exec my-product hostname
|
|
330
|
+
|
|
331
|
+
# apt upgrade across the fleet with 5 parallel slots and a long timeout
|
|
332
|
+
thinr product exec my-product "apt-get -y upgrade" -c 5 --timeout 600
|
|
333
|
+
|
|
334
|
+
# target only a subset of the product
|
|
335
|
+
thinr product exec my-product "systemctl restart agent" -g staging
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Also exposed as an MCP tool (`thinr_product_exec`) with the same
|
|
339
|
+
parameters, so agents can drive a fleet operation with a single call
|
|
340
|
+
instead of looping `thinr_exec` per device.
|
|
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
|
+
|
|
302
405
|
### `thinr profile`
|
|
303
406
|
|
|
304
407
|
Manage the profile store (see [Profiles and multi-account
|
|
@@ -346,25 +449,86 @@ passed through (via the global flag) to impersonate another account.
|
|
|
346
449
|
|
|
347
450
|
### Tool catalog
|
|
348
451
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
| Area | Tools
|
|
354
|
-
| --------------------- |
|
|
355
|
-
| Discovery | `thinr_devices` (with optional `query` for regex filtering), `thinr_device_info`, `thinr_profiles`
|
|
356
|
-
| Shell | `thinr_exec` (buffered), with streaming stdout/stderr
|
|
357
|
-
| Filesystem | `thinr_read`, `thinr_write`, `thinr_ls`, `thinr_mkdir`, `
|
|
358
|
-
| Resources | `thinr_resource_list` (with `in`/`out` schemas), `thinr_resource_call`
|
|
359
|
-
| Properties | `thinr_property_get`, `thinr_property_set`
|
|
360
|
-
| Scripts (device) | `thinr_script_list`, `thinr_script_write`, `thinr_script_delete`
|
|
361
|
-
| Monitoring and update | `thinr_monitoring`, `thinr_update`
|
|
362
|
-
| Products | `thinr_products`, `thinr_product_delete`, `thinr_device_set_product`
|
|
363
|
-
| 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` |
|
|
364
469
|
|
|
365
470
|
Full input/output schemas are published via standard MCP
|
|
366
471
|
`list_tools`; the client will show them when you connect.
|
|
367
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
|
+
|
|
368
532
|
### Per-call controls
|
|
369
533
|
|
|
370
534
|
The server starts with no per-session pin: every tool call must say
|
|
@@ -447,7 +611,57 @@ npm link # makes `thinr` available on $PATH
|
|
|
447
611
|
node bin/thinr.js # or run directly without linking
|
|
448
612
|
```
|
|
449
613
|
|
|
450
|
-
|
|
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.
|
|
451
665
|
|
|
452
666
|
## License
|
|
453
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
|
+
}
|