@thinremote/thinr-cli 1.2.0-alpha.1 → 1.3.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.
Files changed (92) hide show
  1. package/CHANGELOG.md +63 -1
  2. package/README.md +235 -30
  3. package/bin/thinr.js +43 -25
  4. package/commands/_shared.js +154 -0
  5. package/commands/dashboard.js +17 -0
  6. package/commands/device/_shared.js +17 -34
  7. package/commands/device/console.js +1 -0
  8. package/commands/device/delete.js +53 -0
  9. package/commands/device/exec.js +7 -6
  10. package/commands/device/fs.js +193 -0
  11. package/commands/device/index.js +9 -0
  12. package/commands/device/list.js +35 -23
  13. package/commands/device/logs.js +178 -0
  14. package/commands/device/property.js +30 -28
  15. package/commands/device/proxy.js +3 -0
  16. package/commands/device/resource.js +30 -29
  17. package/commands/device/status.js +35 -14
  18. package/commands/device/transfer.js +236 -0
  19. package/commands/device/update.js +15 -10
  20. package/commands/fleet/index.js +17 -0
  21. package/commands/fleet/upgrade.js +275 -0
  22. package/commands/logout.js +10 -11
  23. package/commands/playbook/_shared.js +12 -0
  24. package/commands/playbook/index.js +18 -0
  25. package/commands/playbook/run.js +302 -0
  26. package/commands/playbook/validate.js +38 -0
  27. package/commands/product/_shared.js +8 -0
  28. package/commands/product/exec.js +243 -0
  29. package/commands/product/fs.js +218 -0
  30. package/commands/product/index.js +35 -0
  31. package/commands/product/list.js +86 -0
  32. package/commands/product/logs.js +289 -0
  33. package/commands/product/metric.js +124 -0
  34. package/commands/product/playbook.js +911 -0
  35. package/commands/product/property.js +76 -0
  36. package/commands/product/props.js +125 -0
  37. package/commands/product/push.js +184 -0
  38. package/commands/product/resource.js +76 -0
  39. package/commands/product/status.js +213 -0
  40. package/commands/profile.js +76 -8
  41. package/dist/dashboard.js +5644 -0
  42. package/lib/agent-releases.js +59 -0
  43. package/lib/agent-versions.js +73 -0
  44. package/lib/alarms.js +252 -0
  45. package/lib/api.js +8 -2
  46. package/lib/auth.js +68 -20
  47. package/lib/bucket.js +87 -0
  48. package/lib/command-stream.js +213 -0
  49. package/lib/config.js +5 -3
  50. package/lib/console.js +19 -5
  51. package/lib/constants.js +83 -0
  52. package/lib/dashboard/event-stream.js +239 -0
  53. package/lib/dashboard/metric-aggregate.js +64 -0
  54. package/lib/dashboard/metric-filter.js +39 -0
  55. package/lib/debug-log.js +93 -0
  56. package/lib/device-api.js +131 -148
  57. package/lib/devices.js +18 -21
  58. package/lib/errors.js +12 -0
  59. package/lib/fleet-upgrade.js +106 -0
  60. package/lib/format.js +114 -0
  61. package/lib/mcp/registry.js +14 -0
  62. package/lib/mcp/tools-alarms.js +510 -0
  63. package/lib/mcp/tools-buckets.js +103 -0
  64. package/lib/mcp/tools-devices.js +33 -3
  65. package/lib/mcp/tools-exec.js +30 -17
  66. package/lib/mcp/tools-filesystem.js +102 -6
  67. package/lib/mcp/tools-install.js +153 -0
  68. package/lib/mcp/tools-logs.js +225 -0
  69. package/lib/mcp/tools-playbook.js +211 -0
  70. package/lib/mcp/tools-products.js +1226 -105
  71. package/lib/mcp/tools-profile.js +634 -0
  72. package/lib/mcp/tools-tokens.js +371 -0
  73. package/lib/monitoring.js +26 -1
  74. package/lib/playbook/actions.js +447 -0
  75. package/lib/playbook/expression.js +302 -0
  76. package/lib/playbook/fleet.js +175 -0
  77. package/lib/playbook/loader.js +247 -0
  78. package/lib/playbook/runner.js +300 -0
  79. package/lib/playbook/schema.js +209 -0
  80. package/lib/playbook/vars.js +209 -0
  81. package/lib/product/log-presets.js +196 -0
  82. package/lib/product/logs.js +485 -0
  83. package/lib/product-orchestrator.js +124 -0
  84. package/lib/product.js +523 -20
  85. package/lib/profile.js +665 -0
  86. package/lib/property.js +7 -40
  87. package/lib/proxy.js +9 -8
  88. package/lib/resource.js +10 -14
  89. package/lib/status.js +78 -37
  90. package/lib/tokens.js +374 -0
  91. package/package.json +14 -3
  92. package/commands/product.js +0 -407
package/CHANGELOG.md CHANGED
@@ -1,9 +1,53 @@
1
1
  # Changelog
2
2
 
3
- ## [1.2.0] - 2026-04-16
3
+ ## [1.3.0] - 2026-09-13
4
4
 
5
5
  ### Added
6
6
 
7
+ - `thinr login` — re-authenticate the current profile in place, without
8
+ deleting and recreating it. Renews an expired session in one step and
9
+ defaults the server prompt to the profile's existing server.
10
+
11
+ ### Changed
12
+
13
+ - Session-expiry errors now point to the working command: "Session
14
+ expired. Run `thinr login` to sign in again." (previously suggested
15
+ `thinr`, which opens the dashboard instead of re-authenticating).
16
+ - More robust token refresh for WebSocket sessions: the interactive
17
+ console and streaming `exec` now refresh an expired access token
18
+ before the handshake and retry once on a 401, matching the
19
+ refresh-on-401 behaviour of HTTP calls.
20
+
21
+ ## [1.2.0] - 2026-06-10
22
+
23
+ ### Added
24
+
25
+ - `thinr` with no arguments now launches the interactive dashboard
26
+ directly when stdout is a TTY (and a configuration exists), so the
27
+ fleet view is one keystroke away. Non-TTY contexts (pipes, scripts)
28
+ fall back to `--help`. First-time users still get the auth flow,
29
+ then drop straight into the dashboard.
30
+ - TUI dashboard: alarms panel on the overview tab, with rule- and
31
+ instance-level views, severity colouring and live updates over the
32
+ same event stream the rest of the dashboard already uses.
33
+ - `thinr device delete <deviceId>` — remove a device record from the
34
+ platform. Interactive `confirm` by default, `--yes` to skip in
35
+ scripts, idempotent on a missing device. Useful after re-provisioning
36
+ an agent under a new hostname leaves the old `ip-…` record offline.
37
+ - MCP: `thinr_device_delete` — same operation for AI clients.
38
+ - MCP: `thinr_push` and `thinr_pull` — upload a local file to a
39
+ device and download a remote file to local disk, respectively.
40
+ Rounds out filesystem parity with the CLI so an agent can move
41
+ binary or large payloads that don't fit inline in `thinr_write`.
42
+ - MCP: alarms tools (`thinr_alarm_instances`,
43
+ `thinr_alarm_instance_get`, `thinr_alarm_instance_stats`,
44
+ `thinr_alarm_instance_update`, `thinr_alarm_instance_delete`,
45
+ `thinr_alarm_rules`, `thinr_alarm_rule_read`, `thinr_alarm_rule_write`,
46
+ `thinr_alarm_rule_delete`) — list, inspect and manage alarm
47
+ instances and rules from AI clients, with state/severity parsing
48
+ shared with the dashboard.
49
+ - MCP: product profile config tools (buckets, properties, API
50
+ resources) and access-token tools (user-level and device-level).
7
51
  - `thinr product exec <productId> <command...>` — run a shell command
8
52
  in parallel on every active device of a product, with bounded
9
53
  concurrency (`-c, --concurrency`, default 10), per-device timeout
@@ -23,9 +67,27 @@
23
67
 
24
68
  ### Changed
25
69
 
70
+ - Unified filesystem verbs across CLI, MCP and playbooks so the same
71
+ eight nouns mean the same thing everywhere: `read` / `write`
72
+ (inline content) · `push` / `pull` (local ↔ remote file transfer)
73
+ · `ls` · `mkdir` · `rm` · `mv`.
74
+ - CLI: `thinr device cat` is now `thinr device read` (`cat` kept as
75
+ a hidden alias). New `thinr device write <deviceId> <path>
76
+ [content]` accepts the payload as an argument or on stdin.
77
+ - MCP: `thinr_delete` renamed to `thinr_rm`, `thinr_move` renamed to
78
+ `thinr_mv`. No backwards-compatibility aliases.
79
+ - Playbook actions: `delete` → `rm`, `move` → `mv`. The `write`
80
+ action now only accepts inline `content`; use the new `push`
81
+ action to upload a local file (`source`, `destination`). New
82
+ `pull` action downloads a remote file to the local disk.
26
83
  - `cli-table3` added as a direct dependency to render the
27
84
  product-exec summary.
28
85
 
86
+ ### Removed
87
+
88
+ - `thinr dashboard` subcommand. The dashboard is now the default for
89
+ bare `thinr`; the explicit subcommand had not shipped to anyone yet.
90
+
29
91
  ## [1.1.0] - 2026-04-16
30
92
 
31
93
  Major release: MCP server for AI clients, subcommand-first CLI, JSON
package/README.md CHANGED
@@ -12,8 +12,8 @@ Two operating modes:
12
12
 
13
13
  - **CLI** — interactive or scripted device management from a shell.
14
14
  - **MCP server** — a stdio [Model Context Protocol][mcp] server that
15
- exposes the same operations as typed tools for AI clients like
16
- Claude Code.
15
+ exposes the same operations as 80+ typed tools for AI clients like
16
+ Claude Code, Claude Desktop and Cursor.
17
17
 
18
18
  Both modes share the same auth, profiles, and device API layer, so
19
19
  you configure the tool once and use it in whichever mode fits the
@@ -30,13 +30,16 @@ 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)
42
+ - [Integrating with Claude Desktop, Cursor and other MCP hosts](#integrating-with-claude-desktop-cursor-and-other-mcp-hosts)
40
43
  - [Profiles and multi-account use](#profiles-and-multi-account-use)
41
44
  - [Development](#development)
42
45
  - [License](#license)
@@ -337,6 +340,69 @@ Also exposed as an MCP tool (`thinr_product_exec`) with the same
337
340
  parameters, so agents can drive a fleet operation with a single call
338
341
  instead of looping `thinr_exec` per device.
339
342
 
343
+ #### `playbook`
344
+
345
+ Manage playbooks stored on a product and run them against one device
346
+ or the whole fleet:
347
+
348
+ ```bash
349
+ thinr product playbook list <productId> [--json]
350
+ thinr product playbook upload <productId> <name> [file] [--description <text>] [--skip-validation] [--json]
351
+ thinr product playbook download <productId> <name> [-o <file>] [--json]
352
+ thinr product playbook delete <productId> <name> [--json]
353
+ thinr product playbook run <productId> <name> (--device <id> | --fleet) [options]
354
+ ```
355
+
356
+ `upload` reads the YAML from a file, or from stdin when the path is
357
+ omitted or `-`. The document is validated against the playbook schema
358
+ before being written to the product storage; an invalid document is
359
+ rejected with a descriptive error and nothing is uploaded. Pass
360
+ `--skip-validation` only to work around a schema-runtime gap (do not
361
+ use it to bypass real errors).
362
+
363
+ `run` targets either a single device (`--device <id>`) or the whole
364
+ fleet (`--fleet`, progressive batch rollout):
365
+
366
+ - `-v, --var <key=value>` — override an overridable playbook variable
367
+ (repeatable; values are coerced to the declared type).
368
+ - `--vars-file <path>` — load variable overrides from a YAML/JSON file.
369
+ - `--dry-run` — print the resolved plan without contacting any device.
370
+ - `--check` — contact devices read-only and report what each step would
371
+ change.
372
+ - `--yes` / `-y` — skip the interactive confirmation prompt.
373
+
374
+ Fleet-only options:
375
+
376
+ - `--batch-size <n>` — devices attempted in parallel per batch
377
+ (default: 5).
378
+ - `--failure-threshold <p>` — abort rollout when cumulative failure
379
+ rate reaches P% (default: 25).
380
+ - `-g, --group <id>` — restrict to an asset group.
381
+ - `--filter <key=value>` — extra server-side filter (repeatable).
382
+ - `--include-offline` — attempt offline devices too (default: active
383
+ only).
384
+
385
+ Each fleet rollout writes a persistent JSON report to
386
+ `playbooks/runs/<timestamp>-<name>-<user>.json` in the product's file
387
+ storage, capturing resolved variables, per-device outcomes, and any
388
+ abort reason.
389
+
390
+ ### `thinr playbook <action>`
391
+
392
+ Run or validate a local playbook YAML file (not stored on a product).
393
+ Useful for one-shot or ad-hoc runs:
394
+
395
+ ```bash
396
+ thinr playbook validate <file>
397
+ thinr playbook run <file> [--dry-run] [--check] [options]
398
+ ```
399
+
400
+ `validate` parses the YAML and reports any schema errors. `run`
401
+ resolves the target block declared inside the playbook and executes
402
+ it; the same variable-override flags as `thinr product playbook run`
403
+ apply. For playbooks that should be reusable across users, upload
404
+ them to their product with `thinr product playbook upload` instead.
405
+
340
406
  ### `thinr profile`
341
407
 
342
408
  Manage the profile store (see [Profiles and multi-account
@@ -363,46 +429,116 @@ Other profiles are left untouched.
363
429
 
364
430
  ## Part 2 — MCP server
365
431
 
366
- `thinr` ships an [MCP][mcp] server that exposes the CLI's device
367
- operations as a set of typed tools. AI clients (Claude Code, Claude
368
- Desktop, and any other MCP-compatible host) can then list devices,
369
- run shell commands, read/write files, call resources, and manage
370
- products through the same API the CLI uses — with stable schemas and
371
- a consistent error contract.
432
+ `thinr` ships an [MCP][mcp] server that exposes the CLI's device and
433
+ fleet operations as a set of 80+ typed tools. AI clients (Claude Code,
434
+ Claude Desktop, Cursor, and any other MCP-compatible host) can list
435
+ devices, run shell commands, read/write files, call resources, manage
436
+ products, author and roll out playbooks, triage alarms and rotate
437
+ access tokens through the same API the CLI uses — with stable schemas
438
+ and a consistent error contract.
372
439
 
373
440
  ### Starting the server
374
441
 
375
442
  ```bash
376
- thinr mcp [-d, --device <deviceId>]
443
+ thinr mcp
377
444
  ```
378
445
 
379
- The server speaks [MCP over stdio][mcp-stdio]. `-d/--device` sets a
380
- default device that tools can omit in each call; `--user` can be
381
- passed through (via the global flag) to impersonate another account.
446
+ The server speaks [MCP over stdio][mcp-stdio]. Every tool call carries
447
+ its own `device`, `user` and `profile` arguments, so a single running
448
+ server handles any device of any configured environment without a
449
+ restart.
382
450
 
383
451
  [mcp-stdio]: https://modelcontextprotocol.io/specification/server/transport#stdio
384
452
 
385
453
  ### Tool catalog
386
454
 
387
- Roughly 28 tools, grouped by capability. Every tool accepts optional
388
- `device`, `user`, and `profile` arguments, so a single session can
389
- target any device/account/environment without restart.
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`, `thinr_delete`, `thinr_move` |
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` |
455
+ Grouped by capability. Every tool accepts optional `device` (when
456
+ relevant), `user` and `profile` arguments, so a single session can
457
+ target any device, account or environment without restart.
458
+
459
+ | Area | Tools |
460
+ | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
461
+ | Discovery | `thinr_devices` (with optional `query` for regex filtering), `thinr_device_info`, `thinr_profiles` |
462
+ | Provisioning and lifecycle | `thinr_agent_install_command`, `thinr_device_set_product`, `thinr_device_delete` |
463
+ | Shell | `thinr_exec` (buffered), with streaming stdout/stderr |
464
+ | Filesystem | `thinr_read`, `thinr_write`, `thinr_push`, `thinr_pull`, `thinr_ls`, `thinr_mkdir`, `thinr_rm`, `thinr_mv` |
465
+ | Resources | `thinr_resource_list` (with `in`/`out` schemas), `thinr_resource_call` |
466
+ | Properties (device) | `thinr_property_get`, `thinr_property_set` |
467
+ | Scripts (device) | `thinr_script_list`, `thinr_script_write`, `thinr_script_delete` |
468
+ | Monitoring and agent updates | `thinr_monitoring`, `thinr_bucket_read`, `thinr_update` |
469
+ | Alarms | `thinr_alarm_instances`, `thinr_alarm_instance_stats`, `thinr_alarm_instance_get`, `thinr_alarm_instance_update`, `thinr_alarm_instance_delete`, `thinr_alarm_rules`, `thinr_alarm_rule_read/write/delete` |
470
+ | Access tokens | User: `thinr_token_list`, `thinr_token_get`, `thinr_token_create`, `thinr_token_update`, `thinr_token_delete`. Device: `thinr_device_token_list`, `thinr_device_token_create`, `thinr_device_token_delete` |
471
+ | Products | `thinr_products`, `thinr_product_create`, `thinr_product_update`, `thinr_product_delete`, `thinr_product_exec`, `thinr_product_write` |
472
+ | Product scripts | `thinr_product_script_list`, `thinr_product_script_read`, `thinr_product_script_write`, `thinr_product_script_delete` |
473
+ | Product properties | `thinr_product_property_get`, `thinr_product_property_set`, `thinr_product_property_delete` |
474
+ | Product profile schema | `thinr_product_profile_api_{list,get,set,delete}`, `thinr_product_profile_bucket_{list,get,set,delete}`, `thinr_product_profile_property_{list,get,set,delete}` |
475
+ | Product log sources | `thinr_product_logs_list`, `thinr_product_logs_add`, `thinr_product_logs_remove`, `thinr_product_logs_presets`, `thinr_product_logs_set_default` |
476
+ | Product metrics | `thinr_product_metric_list`, `thinr_product_metric_set`, `thinr_product_metric_delete` |
477
+ | Playbooks (authoring) | `thinr_playbook_schema`, `thinr_playbook_validate`, `thinr_playbook_run` (ad-hoc) |
478
+ | 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
479
 
403
480
  Full input/output schemas are published via standard MCP
404
481
  `list_tools`; the client will show them when you connect.
405
482
 
483
+ ### Authoring playbooks from natural language
484
+
485
+ Playbooks are declarative YAML documents — steps like `exec`, `write`,
486
+ `install_package`, `service`, plus a `target` block and a `vars`
487
+ block — that the runner applies to a device or a fleet. With the MCP
488
+ server connected, an AI assistant can turn a natural-language request
489
+ into a validated playbook and store it on the relevant product
490
+ without the user ever writing YAML by hand.
491
+
492
+ The intended authoring loop is:
493
+
494
+ 1. **Fetch the schema.** Call `thinr_playbook_schema` once at the
495
+ start of the session to get the authoritative list of top-level
496
+ fields, target block, step actions with their parameters, and the
497
+ two accepted forms of the `vars` block (plain and extended).
498
+ 2. **Draft the YAML.** Translate the user's request into a playbook
499
+ draft. Use the extended `vars` form
500
+ (`default` / `description` / `type` / `overridable` / `required`)
501
+ whenever the user hints at knobs the caller may need to tune at
502
+ run time.
503
+ 3. **Validate and iterate.** Call `thinr_playbook_validate` with the
504
+ draft. On failure, read the consolidated error message, fix the
505
+ YAML, and call validate again. Never skip this step.
506
+ 4. **Upload.** Once validation is clean, call
507
+ `thinr_product_playbook_write` with the product id, a short name
508
+ (letters / digits / underscore / dash), and the validated YAML.
509
+ The tool re-validates server-side; never pass `skip_validation` to
510
+ mask a real error.
511
+ 5. **Pre-flight on one device.** Call
512
+ `thinr_product_playbook_run` with a single `device` (plus
513
+ `dry_run: true` or `check: true` for a read-only preview) to
514
+ confirm the steps behave as expected.
515
+ 6. **Roll out.** Finally, `thinr_product_playbook_rollout` rolls the
516
+ playbook across the fleet in batches with a failure-rate kill
517
+ switch, and writes a persistent report to `playbooks/runs/` in the
518
+ product file storage.
519
+
520
+ Example prompt the user can give the assistant:
521
+
522
+ > "On product `edge-gateways`, create a playbook called
523
+ > `nginx-upgrade` that updates the nginx package, renders
524
+ > `/etc/nginx/nginx.conf` from a template, and restarts nginx only
525
+ > if the config check passes. Make `worker_connections` an
526
+ > overridable integer variable, default 1024. Then try it on
527
+ > `edge-gw-17` in check mode."
528
+
529
+ A well-behaved assistant translates this into:
530
+
531
+ - `thinr_playbook_schema` → read the authoring contract.
532
+ - Draft YAML → `thinr_playbook_validate` until clean.
533
+ - `thinr_product_playbook_write` with `product="edge-gateways"`,
534
+ `name="nginx-upgrade"`, and the validated content.
535
+ - `thinr_product_playbook_run` with `device="edge-gw-17"` and
536
+ `check: true` to preview what would change.
537
+
538
+ After that a second message like "looks good, roll it out to staging"
539
+ maps to `thinr_product_playbook_rollout` with the matching
540
+ `group` / `filter` and a conservative `batch_size`.
541
+
406
542
  ### Per-call controls
407
543
 
408
544
  The server starts with no per-session pin: every tool call must say
@@ -431,8 +567,27 @@ That's it — a single MCP server entry handles every device of the
431
567
  active profile. Tool calls pass `device` (and optionally `user` /
432
568
  `profile`) per call.
433
569
 
434
- Other MCP hosts follow the same pattern — they just need the command
435
- `thinr mcp` (optionally with `-d <deviceId>`) over stdio.
570
+ ### Integrating with Claude Desktop, Cursor and other MCP hosts
571
+
572
+ Other MCP hosts use a JSON config file (`claude_desktop_config.json`,
573
+ `.cursor/mcp.json`, etc.). The entry shape is the same on every host:
574
+
575
+ ```json
576
+ {
577
+ "mcpServers": {
578
+ "thinr": {
579
+ "command": "thinr",
580
+ "args": ["mcp"]
581
+ }
582
+ }
583
+ }
584
+ ```
585
+
586
+ The `thinr` binary must be on `PATH` for the host to spawn it —
587
+ `npm install -g @thinremote/thinr-cli` puts it there. If the host
588
+ runs under a shell that doesn't load your global npm bin
589
+ (common on macOS GUI apps), point `command` at the absolute path
590
+ (`which thinr`) instead.
436
591
 
437
592
  ---
438
593
 
@@ -485,7 +640,57 @@ npm link # makes `thinr` available on $PATH
485
640
  node bin/thinr.js # or run directly without linking
486
641
  ```
487
642
 
488
- There are no automated tests yet — contributions welcome.
643
+ Unit tests live under `tests/` and run offline:
644
+
645
+ ```bash
646
+ npm test
647
+ ```
648
+
649
+ ### Integration tests
650
+
651
+ The product-playbook end-to-end script at
652
+ [`tests/integration/playbook-e2e.js`](tests/integration/playbook-e2e.js)
653
+ drives a real ThinRemote cloud through the full playbook lifecycle
654
+ (create product → upload flat/extended/internal playbooks → list →
655
+ download → replace → reject invalid inputs → optionally run in
656
+ `check` mode against a device → delete). Cleanup of the temporary
657
+ product and its file storage is unconditional (`try / finally`),
658
+ even if the run fails halfway.
659
+
660
+ Prerequisites:
661
+
662
+ - An authenticated CLI profile (`thinr` once, or `THINR_PROFILE=<name>`
663
+ pointing at an existing entry in `~/.config/thinr-cli/config.json`).
664
+ - The account must be allowed to create products and file storages.
665
+
666
+ Invocations:
667
+
668
+ ```bash
669
+ # Offline checks only — parses fixtures, exercises resolveVarScope,
670
+ # asserts the dry-run plan. No network access. Safe in CI without
671
+ # credentials.
672
+ node tests/integration/playbook-e2e.js --offline
673
+
674
+ # Full flow against the active profile. Creates a throw-away product
675
+ # named "playbook-e2e-<timestamp>-<rand>" and deletes it on exit.
676
+ node tests/integration/playbook-e2e.js
677
+
678
+ # Add a test device to exercise `check`-mode execution with and
679
+ # without variable overrides. The device must belong to the active
680
+ # user; it does NOT need to be bound to the temporary product.
681
+ THINR_E2E_DEVICE=<device-id> node tests/integration/playbook-e2e.js
682
+ ```
683
+
684
+ The script only touches resources it creates itself (identified by
685
+ the `playbook-e2e-` product-id prefix). If a run is interrupted
686
+ before the `finally` cleanup executes — e.g. the process is killed
687
+ — list leftovers with `thinr product list --json` and drop each one
688
+ through the MCP `thinr_product_delete` tool (from an AI client) or
689
+ via a Node one-liner reusing `deleteProductWithStorage` from
690
+ `lib/product.js`.
691
+
692
+ No credentials or tokens are ever written to the repository; the
693
+ script reads them from the standard CLI profile store.
489
694
 
490
695
  ## License
491
696
 
package/bin/thinr.js CHANGED
@@ -4,10 +4,9 @@ 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 ora from 'ora';
10
- import { configExists, readConfig, setActiveProfile } from '../lib/config.js';
8
+ import { accent, info } from '../lib/format.js';
9
+ import { configExists, getActiveProfile, readConfig, setActiveProfile } from '../lib/config.js';
11
10
  import { detectJsonModeFromArgv } from '../lib/output.js';
12
11
 
13
12
  // Single source of truth for the version: package.json. Avoids the drift
@@ -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 { productCommand } from '../commands/product.js';
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(chalk.cyan(figlet.textSync('thinr-cli', { horizontalLayout: 'full' })));
46
- console.log(chalk.blue('Thin Remote CLI - Remote management for IoT devices'));
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,35 @@ 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
 
67
+ program
68
+ .command('login')
69
+ .helpGroup('Profile:')
70
+ .description('Re-authenticate the current profile (renew an expired session)')
71
+ .action(async () => {
72
+ try {
73
+ // Pin the target to the active/default profile by name so the
74
+ // refreshed credentials overwrite it in place, even when the
75
+ // profile name differs from the server hostname.
76
+ const name = getActiveProfile();
77
+ if (name) setActiveProfile(name);
78
+ await authenticate();
79
+ console.log(info('Signed in.'));
80
+ } catch (error) {
81
+ console.error(`Error: ${error.message}`);
82
+ process.exit(1);
83
+ }
84
+ });
85
+
66
86
  program
67
87
  .command('mcp')
88
+ .helpGroup('Integration:')
68
89
  .description('Start the MCP server (stdio) for AI tool integration')
69
90
  .action(async () => {
70
91
  try {
@@ -91,27 +112,24 @@ if (configExists()) {
91
112
  }
92
113
  }
93
114
 
94
- // Handle no command (just "thinr")
115
+ // Handle no command (just "thinr"): launch the dashboard when running
116
+ // interactively. Non-TTY contexts (pipes, scripts, `thinr | cat`) fall
117
+ // back to `--help` so they get something useful instead of an Ink crash.
95
118
  if (process.argv.length <= 2) {
96
- const spinner = ora('Checking configuration...').start();
97
-
98
- try {
119
+ if (!process.stdout.isTTY) {
120
+ program.help();
121
+ } else {
99
122
  if (!configExists()) {
100
- // Display banner only if configuration doesn't exist
101
123
  displayBanner();
102
- spinner.succeed('No configuration found. Starting setup...');
103
- await authenticate();
104
- program.help();
105
- } else {
106
- spinner.succeed('Configuration found');
107
- // Show help if already configured, but don't display banner twice
108
- program.help();
124
+ try {
125
+ await authenticate();
126
+ } catch (error) {
127
+ console.error(`Configuration error: ${error.message}`);
128
+ process.exit(1);
129
+ }
109
130
  }
110
- } catch (error) {
111
- spinner.fail(`Configuration error: ${error.message}`);
112
- process.exit(1);
131
+ await runDashboard();
113
132
  }
114
- spinner.succeed('Configuration found');
115
133
  } else {
116
134
  // Parse arguments - don't show banner for regular commands
117
135
  program.parse(process.argv);