@thinremote/thinr-cli 1.3.4 → 1.4.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 +86 -0
- package/README.md +117 -123
- package/bin/thinr.js +43 -105
- package/commands/_shared.js +10 -90
- package/commands/device/_shared.js +2 -2
- package/commands/device/exec.js +2 -2
- package/commands/device/fs.js +2 -2
- package/commands/device/index.js +5 -19
- package/commands/device/logs.js +4 -4
- package/commands/device/status.js +1 -1
- package/commands/device/transfer.js +2 -2
- package/commands/device/update.js +8 -2
- package/commands/fleet/upgrade.js +3 -3
- package/commands/playbook/run.js +2 -2
- package/commands/playbook/validate.js +2 -2
- package/commands/product/_shared.js +1 -1
- package/commands/product/exec.js +2 -2
- package/commands/product/fs.js +2 -2
- package/commands/product/index.js +4 -11
- package/commands/product/logs.js +2 -2
- package/commands/product/metric.js +2 -2
- package/commands/product/playbook.js +5 -5
- package/commands/product/property.js +4 -4
- package/commands/product/push.js +2 -2
- package/commands/product/resource.js +4 -4
- package/commands/product/status.js +3 -3
- package/dist/dashboard.js +12 -12
- package/docs/guides/thinremote-metrics-logs.md +59 -0
- package/docs/guides/thinremote-playbooks.md +133 -0
- package/docs/guides/thinremote-scripts.md +115 -0
- package/docs/guides/thinremote.md +116 -0
- package/lib/command-stream.js +3 -2
- package/lib/console.js +6 -6
- package/lib/constants.js +1 -19
- package/lib/dashboard/event-stream.js +5 -5
- package/lib/device-api.js +4 -4
- package/lib/guides.js +41 -0
- package/lib/identity.js +75 -0
- package/lib/local-files.js +54 -0
- package/lib/mcp/{tools-install.js → agent-install.js} +12 -61
- package/lib/mcp/helpers.js +5 -25
- package/lib/mcp/playbook-ops.js +393 -0
- package/lib/mcp/registry.js +9 -43
- package/lib/mcp/resource-hooks.js +53 -0
- package/lib/mcp/resource-types.js +857 -0
- package/lib/mcp/tools-exec.js +217 -177
- package/lib/monitoring.js +3 -3
- package/lib/playbook/actions.js +30 -18
- package/lib/playbook/expression.js +1 -4
- package/lib/playbook/fleet.js +4 -1
- package/lib/playbook/loader.js +16 -8
- package/lib/playbook/runner.js +40 -8
- package/lib/playbook/schema.js +108 -34
- package/lib/product/log-presets.js +1 -1
- package/lib/product/logs.js +2 -2
- package/lib/product-orchestrator.js +14 -9
- package/lib/product.js +29 -227
- package/lib/proxy.js +4 -4
- package/lib/status.js +13 -5
- package/package.json +4 -2
- package/commands/device/delete.js +0 -53
- package/commands/device/list.js +0 -55
- package/commands/device/property.js +0 -52
- package/commands/device/resource.js +0 -52
- package/commands/logout.js +0 -47
- package/commands/product/list.js +0 -86
- package/commands/product/props.js +0 -125
- package/commands/profile.js +0 -173
- package/lib/alarms.js +0 -223
- package/lib/api.js +0 -89
- package/lib/auth.js +0 -428
- package/lib/bucket.js +0 -82
- package/lib/concurrency.js +0 -52
- package/lib/config.js +0 -230
- package/lib/devices.js +0 -60
- package/lib/errors.js +0 -65
- package/lib/format.js +0 -114
- package/lib/mcp/server.js +0 -67
- package/lib/mcp/tools-alarms.js +0 -507
- package/lib/mcp/tools-buckets.js +0 -99
- package/lib/mcp/tools-devices.js +0 -235
- package/lib/mcp/tools-filesystem.js +0 -281
- package/lib/mcp/tools-logs.js +0 -225
- package/lib/mcp/tools-playbook.js +0 -210
- package/lib/mcp/tools-products.js +0 -1557
- package/lib/mcp/tools-profile.js +0 -635
- package/lib/mcp/tools-profiles.js +0 -30
- package/lib/mcp/tools-properties.js +0 -79
- package/lib/mcp/tools-resources.js +0 -78
- package/lib/mcp/tools-scripts.js +0 -212
- package/lib/mcp/tools-tokens.js +0 -369
- package/lib/output.js +0 -114
- package/lib/paths.js +0 -32
- package/lib/profile.js +0 -657
- package/lib/property.js +0 -75
- package/lib/resource.js +0 -151
- package/lib/storage.js +0 -117
- package/lib/tokens.js +0 -366
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# ThinRemote fleet metrics and log sources
|
|
2
|
+
|
|
3
|
+
Two per-product settings for operating a fleet from **thinr**: the metrics
|
|
4
|
+
its terminal dashboard (`thinr` with no arguments) shows for a product, and
|
|
5
|
+
the log sources an operator can stream from a device (in that dashboard or
|
|
6
|
+
with `thinr device logs`). Today only thinr reads them; they are not the web
|
|
7
|
+
console's dashboards (for those, see the server's `dashboard-sdk` guide and
|
|
8
|
+
`thinr_dashboard_publish`). Both live on the server as product properties,
|
|
9
|
+
so the devices need nothing and other clients could reuse them.
|
|
10
|
+
|
|
11
|
+
Tool names below omit the `thinr_` prefix.
|
|
12
|
+
|
|
13
|
+
## Metrics
|
|
14
|
+
|
|
15
|
+
A metric is a value thinr's dashboard asks every device of the product for,
|
|
16
|
+
and combines across the fleet: free disk, connected clients, service version…
|
|
17
|
+
They live in the `dashboard_metrics` product property; edit them as the
|
|
18
|
+
resource type `product_metric` (parent the product, id the metric's name:
|
|
19
|
+
`resource_list`, `_get`, `_create`, `_update`, `_delete`), not by writing the
|
|
20
|
+
property by hand.
|
|
21
|
+
|
|
22
|
+
Fields of a metric:
|
|
23
|
+
|
|
24
|
+
| Field | Meaning |
|
|
25
|
+
| --------------- | -------------------------------------------------------------------------------------------------------- |
|
|
26
|
+
| `name` | Stable id (letters, digits, `_`, `-`): the resource id |
|
|
27
|
+
| `label` | Title in the panel |
|
|
28
|
+
| `resource` | Product API resource called on each device, usually a product script |
|
|
29
|
+
| `field` | Dot path to the value in the response (`disk.used_pct`); omit for scalar responses |
|
|
30
|
+
| `aggregation` | `sum`, `avg`, `max`, `min` (numeric), `count` (devices reporting a value), `top`, `distribution`, `none` |
|
|
31
|
+
| `visualization` | `kpi` (default: value and its age), `sparkline` (value and recent trend), `bar`, `list` |
|
|
32
|
+
| `interval` | Refresh in seconds (≥ 1); 0 or omitted relays only what devices push |
|
|
33
|
+
| `unit` | Display suffix (`%`, `devices`) |
|
|
34
|
+
|
|
35
|
+
Recipe: write a product script that prints the value as JSON (see
|
|
36
|
+
`thinremote-scripts`), check it with `device_resource_call` on one device,
|
|
37
|
+
then register the metric pointing at the script's resource name.
|
|
38
|
+
|
|
39
|
+
## Log sources
|
|
40
|
+
|
|
41
|
+
A log source is a named shell command the agent streams when an operator
|
|
42
|
+
opens a device's logs in thinr (`journalctl -f`, `docker logs -f app`,
|
|
43
|
+
`tail -F /var/log/nginx/access.log`). They live in the `logs` product
|
|
44
|
+
property; without it, products show a single `system` source running
|
|
45
|
+
journalctl (listed with `fallback: true`). They are the resource type
|
|
46
|
+
`product_log_source`, parent the product, id the source's name.
|
|
47
|
+
|
|
48
|
+
- `resource_create`: a source by `name` (≤ 32 chars, letters, digits,
|
|
49
|
+
`_`, `-`; up to 32 sources). The `command` is stored and run as is.
|
|
50
|
+
`default: true` makes it the one shown first (and the one
|
|
51
|
+
`thinr device logs` uses without `--source`).
|
|
52
|
+
- Structured rendering: add a `preset` (`resource_describe` type
|
|
53
|
+
`product_log_source` lists them:
|
|
54
|
+
journalctl, spdlog, nginx…) or your own `pattern`, a regex with named
|
|
55
|
+
groups `time`, `level` and `msg`. With a `level` group, operators can filter
|
|
56
|
+
by severity. `preset` and `pattern` are mutually exclusive.
|
|
57
|
+
- `resource_update` with `{"default": true}` picks the default;
|
|
58
|
+
`resource_delete` drops a source (removing the last one deletes the
|
|
59
|
+
property, back to the fallback).
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# ThinRemote playbooks
|
|
2
|
+
|
|
3
|
+
A playbook is a YAML document with ordered steps the agent executes on each
|
|
4
|
+
device: write a file, run a command, install a script, restart a service…
|
|
5
|
+
It is the safe way to change many machines: validated before it runs,
|
|
6
|
+
testable on one device, rolled out in batches with a failure threshold, and
|
|
7
|
+
recorded.
|
|
8
|
+
|
|
9
|
+
Tool names below omit the `thinr_` prefix.
|
|
10
|
+
|
|
11
|
+
## Authoring flow
|
|
12
|
+
|
|
13
|
+
A saved playbook is a resource of type `product_playbook`, with the product
|
|
14
|
+
as parent and its name as id; validating, running and rolling it out are
|
|
15
|
+
its actions (`resource_call`).
|
|
16
|
+
|
|
17
|
+
1. `resource_describe` type `product_playbook` once: its `playbook_format`
|
|
18
|
+
is the full schema (document, `vars`, step fields, every action with its
|
|
19
|
+
parameters, the `when` syntax). Write against it.
|
|
20
|
+
2. Draft the YAML, then action `validate` with the YAML in `body.content`,
|
|
21
|
+
and fix what it reports. Repeat until it is clean.
|
|
22
|
+
3. Save it: `resource_create` type `product_playbook`, body
|
|
23
|
+
`{"content": "<yaml>", "description"?: "…"}` (validated again: nothing is
|
|
24
|
+
saved if invalid; it writes `playbooks/<name>.yaml` in the product storage
|
|
25
|
+
and registers it in the `playbooks` product property). Change it later
|
|
26
|
+
with `resource_update`.
|
|
27
|
+
4. Try it on one device: action `run` with the playbook's `id` and
|
|
28
|
+
`body.device`, first with `dry_run: true` (the resolved plan, no device
|
|
29
|
+
contacted), then `check: true` (contacts the device read-only: each step
|
|
30
|
+
says changed / unchanged / unknown), then for real.
|
|
31
|
+
5. Roll it out: action `rollout` (below).
|
|
32
|
+
|
|
33
|
+
For a one-off run that is not worth saving, action `run` without `id` takes
|
|
34
|
+
the YAML in `body.content`, with the same `dry_run` and `check`; it targets
|
|
35
|
+
`body.device`, `body.devices`, `body.group` or every device of the product.
|
|
36
|
+
|
|
37
|
+
`resource_list` and `resource_get` show what a product has (`get` includes
|
|
38
|
+
the YAML); `resource_delete` removes one (index entry, then file).
|
|
39
|
+
|
|
40
|
+
## Document
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
name: nginx-config
|
|
44
|
+
description: Deploy nginx.conf and reload
|
|
45
|
+
target:
|
|
46
|
+
product: web_gateways # or devices: [gw1, gw2]; group: madrid narrows
|
|
47
|
+
concurrency: 10 # parallel devices (action run)
|
|
48
|
+
fail_fast: false
|
|
49
|
+
vars:
|
|
50
|
+
port:
|
|
51
|
+
default: 8080
|
|
52
|
+
type: number
|
|
53
|
+
description: Listening port
|
|
54
|
+
release:
|
|
55
|
+
type: string
|
|
56
|
+
required: true # must be given at run time
|
|
57
|
+
steps:
|
|
58
|
+
- name: Write config to a temp file
|
|
59
|
+
action: write
|
|
60
|
+
path: /tmp/nginx.conf
|
|
61
|
+
content: |
|
|
62
|
+
server { listen {{ port }}; }
|
|
63
|
+
- name: Check it
|
|
64
|
+
action: exec
|
|
65
|
+
command: nginx -t -c /tmp/nginx.conf
|
|
66
|
+
register: check
|
|
67
|
+
- name: Move it into place
|
|
68
|
+
action: move
|
|
69
|
+
source: /tmp/nginx.conf
|
|
70
|
+
destination: /etc/nginx/nginx.conf
|
|
71
|
+
overwrite: true
|
|
72
|
+
when: check.ok
|
|
73
|
+
- name: Reload
|
|
74
|
+
action: exec
|
|
75
|
+
command: systemctl reload nginx
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- `vars`: plain (`port: 8080`) or extended (`default`, `type` string |
|
|
79
|
+
number | boolean | object, `description`, `overridable: false` for
|
|
80
|
+
internal ones, `required: true`). Used in any parameter as `{{ name }}`.
|
|
81
|
+
Overrides passed at run time (`vars`) are validated against the declared
|
|
82
|
+
type; unknown, non-overridable or missing required ones are errors, and
|
|
83
|
+
they never change the stored playbook.
|
|
84
|
+
- Step fields: `name`, `action`, the action's parameters, and optionally
|
|
85
|
+
`when` (skip unless true), `register` (expose `{ ok, result | error,
|
|
86
|
+
duration_ms }` to later steps), `pause_after` (seconds).
|
|
87
|
+
- `when` syntax: `==`, `!=`, `>`, `<`, `>=`, `<=`, `and`, `or`, `not`,
|
|
88
|
+
parentheses, quoted strings, numbers, `true` / `false` / `null`, dotted
|
|
89
|
+
paths (`check.ok`, `agent.version`).
|
|
90
|
+
|
|
91
|
+
Actions:
|
|
92
|
+
|
|
93
|
+
| Action | Required | Optional | Does |
|
|
94
|
+
| ---------------------------------- | ----------------------- | --------------------------- | ---------------------------------------------------------- |
|
|
95
|
+
| `exec` | `command` | `timeout` | runs a shell command; non-zero exit fails the step |
|
|
96
|
+
| `write` | `path`, `content` | | writes inline content |
|
|
97
|
+
| `push` / `pull` | `source`, `destination` | | copies a file from / to the machine running the playbook |
|
|
98
|
+
| `mkdir` | `path` | | creates a directory |
|
|
99
|
+
| `move` | `source`, `destination` | `overwrite` | moves; write to a temp file then `move` for atomic deploys |
|
|
100
|
+
| `delete` | `path` | `recursive` | deletes |
|
|
101
|
+
| `property_set` | `property`, `value` | | sets a device property |
|
|
102
|
+
| `resource` | `resource` | `inputs` | calls a device resource |
|
|
103
|
+
| `script_install` / `script_delete` | `name` | `content` or `content_file` | installs or removes a device script |
|
|
104
|
+
| `update` | `op` | `channel` | checks for or applies an agent update |
|
|
105
|
+
| `sleep` | `seconds` | | waits |
|
|
106
|
+
|
|
107
|
+
Run from the MCP server, `push`, `pull` and `content_file` only reach files
|
|
108
|
+
inside its local folder (`THINR_MCP_LOCAL_DIR`, or where it started).
|
|
109
|
+
|
|
110
|
+
`check` cannot predict `exec` or `resource` steps: they report `unknown`.
|
|
111
|
+
|
|
112
|
+
## Rolling out
|
|
113
|
+
|
|
114
|
+
Action `rollout` (`resource_call` type `product_playbook`, the playbook's
|
|
115
|
+
`id`) runs a saved playbook on every matching device of the product, in batches of `body.batch_size` (default 5). A batch finishes before
|
|
116
|
+
the next starts; after each one the cumulative failure rate is compared
|
|
117
|
+
with `body.failure_threshold` (default 10 %), and reaching it stops the rollout
|
|
118
|
+
without touching the remaining devices. Narrow the targets with `group` or
|
|
119
|
+
`filter`; offline devices are skipped unless `include_offline`. `dry_run`
|
|
120
|
+
lists the plan and the devices; `check` reports what would change.
|
|
121
|
+
|
|
122
|
+
Each rollout writes a report to `playbooks/runs/<timestamp>-<name>-<user>.json`
|
|
123
|
+
in the product storage (read it with `storage_file_read`, storage named like
|
|
124
|
+
the product): resolved variables, per-device outcome, and why it stopped if
|
|
125
|
+
it did. A rollout that stops does not undo the devices it already changed:
|
|
126
|
+
write files to a temporary path and `move` them into place, so a device is
|
|
127
|
+
never left with half a change.
|
|
128
|
+
|
|
129
|
+
Writing the same file to every device is a playbook too: a `write` step
|
|
130
|
+
(and a `move`), rolled out.
|
|
131
|
+
|
|
132
|
+
Rollouts change real machines: confirm with the user, test on one device
|
|
133
|
+
first, and start with small batches.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# ThinRemote scripts
|
|
2
|
+
|
|
3
|
+
A script turns an operation (read a sensor, rotate a log, run a backup,
|
|
4
|
+
collect a health report) into a **resource**: something with a name and an
|
|
5
|
+
input/output shape that tools, dashboards and other devices can call. Use one
|
|
6
|
+
instead of `thinr_device_exec` whenever the operation will be repeated.
|
|
7
|
+
|
|
8
|
+
Both kinds are resource types: `resource_list` / `_get` / `_create` /
|
|
9
|
+
`_update` / `_delete` with type `device_script` (parent the device) or
|
|
10
|
+
`product_script` (parent the product), the file name as id.
|
|
11
|
+
|
|
12
|
+
Tool names below omit the `thinr_` prefix.
|
|
13
|
+
|
|
14
|
+
## Device scripts or product scripts
|
|
15
|
+
|
|
16
|
+
| | Device script | Product script |
|
|
17
|
+
| ----------- | ------------------------------------- | --------------------------------------------------- |
|
|
18
|
+
| Lives in | the device: `<agent base>/scripts/` | the product's file storage: `scripts/` |
|
|
19
|
+
| Reaches | that device | every device of the product, current and future |
|
|
20
|
+
| Changing it | one write per device | one write, effective on the next call everywhere |
|
|
21
|
+
| Type | `device_script`, parent the device | `product_script`, parent the product |
|
|
22
|
+
| Called with | `device_resource_call` on that device | `device_resource_call` on any device of the product |
|
|
23
|
+
|
|
24
|
+
Prefer **product scripts** for fleets: the code is downloaded on each call,
|
|
25
|
+
so there is nothing to deploy and no drift between devices. Use a device
|
|
26
|
+
script for something specific to one machine.
|
|
27
|
+
|
|
28
|
+
The resource name is the file name without extension: `scripts/backup.sh` is
|
|
29
|
+
the resource `backup`. The extension is kept only for editor highlighting.
|
|
30
|
+
|
|
31
|
+
Scripts run with the agent's privileges, root for a system install:
|
|
32
|
+
installing one grants permanent remote execution of that code. Treat it like
|
|
33
|
+
adding a service.
|
|
34
|
+
|
|
35
|
+
## Product scripts
|
|
36
|
+
|
|
37
|
+
Creating one (`resource_create` type `product_script`, body `{content, input?, output?}`)
|
|
38
|
+
does everything needed: it creates the
|
|
39
|
+
product's storage if missing, writes the `index.js` wrapper the first time,
|
|
40
|
+
links it as the product's code, uploads the script and creates or updates
|
|
41
|
+
the product API resource, so the script is callable at once. The product must
|
|
42
|
+
exist (`resource_create` type `product`).
|
|
43
|
+
|
|
44
|
+
How a call works: the product API resource targets the agent's `cmd`
|
|
45
|
+
resource; the product code reads the script from the storage, sends it with
|
|
46
|
+
the caller's input on stdin, and parses stdout as JSON (`parse_stdout`). A
|
|
47
|
+
non-zero exit returns `{error, retcode}`; invalid JSON returns
|
|
48
|
+
`{error: "invalid JSON in stdout", stdout}`.
|
|
49
|
+
|
|
50
|
+
Contract:
|
|
51
|
+
|
|
52
|
+
- Any executable source. With a shebang (`#!/usr/bin/env python3`,
|
|
53
|
+
`#!/bin/bash`…) it runs with that interpreter; without one it runs through
|
|
54
|
+
the shell (pipes, redirects and `&&` work).
|
|
55
|
+
- The object the caller sends arrives on **stdin** as JSON: parse it with
|
|
56
|
+
`jq`, `json.load`, `JSON.parse`.
|
|
57
|
+
- With `output` true (the default), **stdout must be valid JSON**: the
|
|
58
|
+
resource returns the decoded object.
|
|
59
|
+
|
|
60
|
+
Choose the resource shape with `input` and `output`:
|
|
61
|
+
|
|
62
|
+
| Shape | `input` | `output` | Example | Console shows |
|
|
63
|
+
| ---------------- | ------------------ | -------- | ---------------- | ------------------------------------- |
|
|
64
|
+
| output only | omit | true | stats, status | the output |
|
|
65
|
+
| input and output | `{field: default}` | true | transform, query | an input form and the output |
|
|
66
|
+
| input only | `{field: default}` | false | notify, log | an input form; the call replies empty |
|
|
67
|
+
| run | omit | false | ping, restart | a button |
|
|
68
|
+
|
|
69
|
+
`input` maps field names (`^[A-Za-z_][A-Za-z0-9_]*$`) to default values
|
|
70
|
+
(string, number or boolean), e.g. `{"threshold": 80, "label": "warm"}`: the
|
|
71
|
+
console renders typed fields and callers may override each one. Omit it for
|
|
72
|
+
a free-form payload passed to stdin as is.
|
|
73
|
+
|
|
74
|
+
Example, output only (`scripts/disk.sh`):
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
#!/bin/bash
|
|
78
|
+
df -P / | awk 'NR==2 {printf "{\"used_pct\": %d, \"avail_kb\": %d}\n", $5, $4}'
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Example, input and output (`scripts/ping.py`, `input: {"host": "8.8.8.8"}`):
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
#!/usr/bin/env python3
|
|
85
|
+
import json, subprocess, sys
|
|
86
|
+
host = json.load(sys.stdin).get("host", "8.8.8.8")
|
|
87
|
+
ok = subprocess.run(["ping", "-c", "1", "-W", "2", host], capture_output=True).returncode == 0
|
|
88
|
+
print(json.dumps({"host": host, "reachable": ok}))
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Device scripts
|
|
92
|
+
|
|
93
|
+
Creating one (`resource_create` type `device_script`, body `{content}`)
|
|
94
|
+
writes the file in the agent's scripts folder, makes it executable and
|
|
95
|
+
reloads the scripts extension (`$scripts/reload`), so the resource is
|
|
96
|
+
callable at once. `resource_list` shows each script with the schema it
|
|
97
|
+
describes; `resource_get` adds its source.
|
|
98
|
+
|
|
99
|
+
Contract:
|
|
100
|
+
|
|
101
|
+
- Executable with a shebang.
|
|
102
|
+
- Called with `--describe`, it prints a JSON object with `input` and/or
|
|
103
|
+
`output` sample objects, e.g. `{"input": {"path": ""}, "output": {"size": 0}}`:
|
|
104
|
+
that becomes the resource's schema.
|
|
105
|
+
- Called normally, the input object arrives on stdin as JSON; stdout is
|
|
106
|
+
parsed as JSON. An object's keys become the output fields; any other value
|
|
107
|
+
appears under `output`. A non-zero exit is an error.
|
|
108
|
+
|
|
109
|
+
## Writing JSON safely
|
|
110
|
+
|
|
111
|
+
Build JSON with a tool, never by pasting values into a string: a hostname or
|
|
112
|
+
a log line with a quote or a backslash breaks it. Use `jq -n --arg v "$x"
|
|
113
|
+
'{v: $v}'`, or Python's `json.dumps`. Output landing under `output` (device
|
|
114
|
+
scripts) or an `invalid JSON` error (product scripts) means the JSON was
|
|
115
|
+
malformed.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# ThinRemote: devices, agent and products
|
|
2
|
+
|
|
3
|
+
ThinRemote manages fleets of machines (gateways, servers, edge boxes) on a
|
|
4
|
+
Thinger.io server. Each machine runs **thinr-agent**, which connects to the
|
|
5
|
+
server as a device and lets you operate it remotely: files, commands,
|
|
6
|
+
scripts, monitoring, logs.
|
|
7
|
+
|
|
8
|
+
Almost everything is a **resource type** behind the generic tools
|
|
9
|
+
`resource_list` / `_get` / `_create` / `_update` / `_delete` and
|
|
10
|
+
`resource_call` (its actions), described by `resource_describe`: the
|
|
11
|
+
server's types (device, product, bucket, property, token, alarm…; read the
|
|
12
|
+
server's `concepts` guide with `doc_read`) and ThinRemote's, below. The one
|
|
13
|
+
tool of its own is `device_exec`, for shell commands.
|
|
14
|
+
|
|
15
|
+
Tool names below omit the `thinr_` prefix.
|
|
16
|
+
|
|
17
|
+
## The device and its agent
|
|
18
|
+
|
|
19
|
+
A ThinRemote device is a Generic (IOTMP) device whose firmware is the agent.
|
|
20
|
+
The agent runs as an OS user (root for a system install), so what you can do
|
|
21
|
+
on the machine is what that user can do. `resource_get` with type `device`
|
|
22
|
+
adds what the agent reports when it is connected: `agent.system_info`
|
|
23
|
+
(hostname, OS, architecture, user…), `agent.fs_base_path`, and `agent.where`,
|
|
24
|
+
a map of where everything below is. Check them before assuming write access
|
|
25
|
+
to system paths.
|
|
26
|
+
|
|
27
|
+
What the agent offers, and how to reach it:
|
|
28
|
+
|
|
29
|
+
| Need | How |
|
|
30
|
+
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
31
|
+
| Run a shell command | `device_exec` with `device` (output and exit code; `timeout` in s) |
|
|
32
|
+
| List a folder | `resource_list` type `device_fs`, parent the device, `filter: {"path": "/var/log"}` |
|
|
33
|
+
| Read a file | `resource_get` type `device_fs`, parent the device, id the path |
|
|
34
|
+
| Write a file / create a folder | `resource_create` (or `_update`) type `device_fs`, body `{"content": "…"}` / `{"type": "directory"}` |
|
|
35
|
+
| Delete a file or folder | `resource_delete` type `device_fs` |
|
|
36
|
+
| Move, or copy binaries from/to this machine | `resource_call` type `device_fs`, action `move`, `push` or `pull` |
|
|
37
|
+
| CPU, memory, disk, network, load: now | `device_resource_call` resource `monitoring` |
|
|
38
|
+
| The same, as history | `bucket_data_read` bucket `monitoring` with `device` (fields such as `cpu.usage`, `memory.usage`, `disk.root.usage`, `load.1m`) |
|
|
39
|
+
| A reusable, typed operation on one device | type `device_script` (see `thinremote-scripts`) |
|
|
40
|
+
| Call what the agent or a script exposes | `device_resource_list`, `device_resource_call` |
|
|
41
|
+
| Update the agent | `resource_call` type `device`, action `agent_update`, body `{"action": "check"}` or `{"action": "apply"}` |
|
|
42
|
+
| Install the agent on a new machine | `resource_call` type `device`, action `agent_install` (a shell one-liner; no id) |
|
|
43
|
+
|
|
44
|
+
Paths are absolute on the device. `/` on its own lists the agent's working
|
|
45
|
+
directory (normally the root, reported as `fs_base_path`); nothing is
|
|
46
|
+
confined to it.
|
|
47
|
+
|
|
48
|
+
Built-in agent resources (`cmd`, `monitoring`, `system`, `update`, `agent`)
|
|
49
|
+
keep those names: a device script with one of them would hide the real one,
|
|
50
|
+
so creating it is refused unless forced.
|
|
51
|
+
|
|
52
|
+
## Products: one configuration for a fleet
|
|
53
|
+
|
|
54
|
+
A product groups the devices of a fleet and holds what they share. ThinRemote
|
|
55
|
+
products are created with `resource_create` type `product` and come with
|
|
56
|
+
`config.type: "thinremote"` and a `monitoring` bucket that stores each
|
|
57
|
+
device's `monitoring` resource every 60 s (the history above; a device of
|
|
58
|
+
another kind of product has no history unless its product gets that bucket). On top of the
|
|
59
|
+
server's product, ThinRemote keeps these types, all with the product as
|
|
60
|
+
parent:
|
|
61
|
+
|
|
62
|
+
| Type | What it is | Where it lives | Guide |
|
|
63
|
+
| -------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ------------------------- |
|
|
64
|
+
| `product_script` | Code run on any device of the product, kept in one place | file storage named like the product, `scripts/`; one product API resource | `thinremote-scripts` |
|
|
65
|
+
| `product_playbook` | Ordered steps rolled out across devices; actions `validate`, `run`, `rollout` | same storage, `playbooks/<name>.yaml`, indexed in the `playbooks` property | `thinremote-playbooks` |
|
|
66
|
+
| `product_metric` | Metrics for thinr's terminal dashboard | `dashboard_metrics` product property | `thinremote-metrics-logs` |
|
|
67
|
+
| `product_log_source` | Log sources for thinr (dashboard, `thinr device logs`) | `logs` product property | `thinremote-metrics-logs` |
|
|
68
|
+
|
|
69
|
+
Creating one that already exists fails (update it instead), and updating one
|
|
70
|
+
that does not exist fails too, as with the server's types.
|
|
71
|
+
|
|
72
|
+
Deleting a product with `resource_delete` also deletes its file storage (its
|
|
73
|
+
scripts and playbooks).
|
|
74
|
+
|
|
75
|
+
Devices join a product when they are created with `product`, with
|
|
76
|
+
`resource_update` (`{"product": "<id>"}`), or by autoprovisioning (type
|
|
77
|
+
`product_profile_autoprovision`).
|
|
78
|
+
|
|
79
|
+
## Fleet operations
|
|
80
|
+
|
|
81
|
+
| Need | How |
|
|
82
|
+
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------- |
|
|
83
|
+
| Same command on many devices, once | `device_exec` with `devices`, `product` (and `group`) or `all: true`; `dry_run` lists the targets first |
|
|
84
|
+
| Same file on every device | a playbook with a `write` step (to a temporary file, then `move`), rolled out |
|
|
85
|
+
| A repeatable, versioned change with checks | a playbook: action `run` on one device, then action `rollout` |
|
|
86
|
+
| A value from every device | a product script + `device_resource_call` per device, or a metric |
|
|
87
|
+
| Which devices are online | `resource_list` type `device` with `filter: {"product": "<id>"}` |
|
|
88
|
+
| Fleet health at a glance | `bucket_data_read` bucket `monitoring` without `device`, `group_by` device |
|
|
89
|
+
|
|
90
|
+
Offline devices are skipped unless asked (`include_offline`). Commands on
|
|
91
|
+
several devices and rollouts act at once: confirm with the user first.
|
|
92
|
+
|
|
93
|
+
## Recipes
|
|
94
|
+
|
|
95
|
+
**Bring a new machine in.** `resource_create` type `product` (if the fleet
|
|
96
|
+
has none) → `resource_call` type `device` action `agent_install` → run the
|
|
97
|
+
command on the machine → the device appears (autoprovisioned or created) →
|
|
98
|
+
`resource_update` its `product` if needed → `resource_get` to confirm
|
|
99
|
+
`agent.system_info`.
|
|
100
|
+
|
|
101
|
+
**Expose a health check for the whole fleet.** Write it once as a product
|
|
102
|
+
script (`resource_create` type `product_script`, output-only, JSON on
|
|
103
|
+
stdout) → call it on a device with `device_resource_call` to check it →
|
|
104
|
+
register a metric over it (`resource_create` type `product_metric`) so
|
|
105
|
+
thinr's dashboard aggregates it across the fleet.
|
|
106
|
+
|
|
107
|
+
**Change a config file on every machine, safely.** `resource_describe` type
|
|
108
|
+
`product_playbook` (the format) → draft a playbook that writes the file and
|
|
109
|
+
restarts the service → action `validate` → `resource_create` to save it →
|
|
110
|
+
action `run` on one device with `check: true`, then for real → action
|
|
111
|
+
`rollout` with a small `batch_size`.
|
|
112
|
+
|
|
113
|
+
**Investigate one machine.** `resource_get` type `device` (agent info) →
|
|
114
|
+
`device_resource_call` resource `monitoring` → `device_exec` for specifics →
|
|
115
|
+
`resource_get` type `device_fs` for log files, or the product's log sources
|
|
116
|
+
with `thinr device logs`.
|
package/lib/command-stream.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import WebSocket from 'ws';
|
|
3
3
|
import { randomUUID } from 'crypto';
|
|
4
4
|
import { TIMEOUTS } from './constants.js';
|
|
5
|
-
import {
|
|
5
|
+
import { serverWebSocketURL } from '@thinger-io/cli-core/config';
|
|
6
|
+
import { device } from '@thinger-io/cli-core/paths';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* One-shot execution of a shell command on a device over the IOTMP
|
|
@@ -80,7 +81,7 @@ export class CommandStream {
|
|
|
80
81
|
const { server, apiUser, deviceId, cmd, timeout } = this.opts;
|
|
81
82
|
const params = new URLSearchParams({ cmd });
|
|
82
83
|
if (timeout && timeout > 0) params.set('timeout', String(timeout));
|
|
83
|
-
return
|
|
84
|
+
return `${serverWebSocketURL(server)}${device(apiUser, deviceId)}/resources/${this.resource}?${params}`;
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
_openSocket() {
|
package/lib/console.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import WebSocket from 'ws';
|
|
2
|
-
import { requireConfig } from '
|
|
3
|
-
import { ensureFreshToken } from '
|
|
4
|
-
import api from '
|
|
5
|
-
import { warning, error as errorStyle } from '
|
|
6
|
-
import { device } from '
|
|
2
|
+
import { requireConfig, serverWebSocketURL } from '@thinger-io/cli-core/config';
|
|
3
|
+
import { ensureFreshToken } from '@thinger-io/cli-core/auth';
|
|
4
|
+
import api from '@thinger-io/cli-core/api';
|
|
5
|
+
import { warning, error as errorStyle } from '@thinger-io/cli-core/format';
|
|
6
|
+
import { device } from '@thinger-io/cli-core/paths';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Creates a WebSocket connection to a device terminal. Returns a
|
|
@@ -27,7 +27,7 @@ export async function connectToDeviceConsole(deviceId, user) {
|
|
|
27
27
|
const token = (await ensureFreshToken()) || config.token;
|
|
28
28
|
|
|
29
29
|
const sessionId = createUUID();
|
|
30
|
-
const wsUrl =
|
|
30
|
+
const wsUrl = `${serverWebSocketURL(config.server)}${device(user, deviceId)}/resources/$terminal/${sessionId}?raw=1`;
|
|
31
31
|
const ws = new WebSocket(wsUrl, {
|
|
32
32
|
headers: { Authorization: `Bearer ${token}` },
|
|
33
33
|
});
|
package/lib/constants.js
CHANGED
|
@@ -4,21 +4,11 @@
|
|
|
4
4
|
* Central home for the numeric knobs the CLI scatters otherwise.
|
|
5
5
|
* Keep every "magic number" that has meaning beyond its immediate call
|
|
6
6
|
* site here so we can tune them in one place (and grep for them when
|
|
7
|
-
* debugging a stuck transfer
|
|
7
|
+
* debugging a stuck transfer).
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
/** Timeouts. All values in ms unless the constant name ends in `_SECONDS`. */
|
|
11
11
|
export const TIMEOUTS = {
|
|
12
|
-
/** Default axios request timeout for control-plane HTTP calls. */
|
|
13
|
-
API_DEFAULT_MS: 10_000,
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Refresh a JWT access token this close to (or past) its `exp`
|
|
17
|
-
* before opening a WebSocket. WS handshakes lack the
|
|
18
|
-
* refresh-on-401 retry that api.js gives HTTP requests.
|
|
19
|
-
*/
|
|
20
|
-
TOKEN_EXPIRY_SLACK_MS: 30_000,
|
|
21
|
-
|
|
22
12
|
/** POSTs to device resources (cmd, update, etc.) — mid-weight calls. */
|
|
23
13
|
DEVICE_RESOURCE_CALL_MS: 30_000,
|
|
24
14
|
|
|
@@ -43,9 +33,6 @@ export const TIMEOUTS = {
|
|
|
43
33
|
*/
|
|
44
34
|
EXEC_EXTRA_GRACE_MS: 5_000,
|
|
45
35
|
|
|
46
|
-
/** Hard ceiling for the OAuth device-code flow — 10 minutes. */
|
|
47
|
-
OAUTH_DEVICE_CODE_MS: 10 * 60 * 1000,
|
|
48
|
-
|
|
49
36
|
/**
|
|
50
37
|
* Default command timeout (in *seconds*) for user-facing exec
|
|
51
38
|
* flows: CLI `device exec`, `product exec`, playbook `exec`, MCP
|
|
@@ -76,8 +63,3 @@ export const PROXY_PORTS = {
|
|
|
76
63
|
MIN: 50_000,
|
|
77
64
|
MAX: 51_000,
|
|
78
65
|
};
|
|
79
|
-
|
|
80
|
-
/** OAuth device-flow client identity registered for this CLI. */
|
|
81
|
-
export const OAUTH = {
|
|
82
|
-
CLIENT_ID: '3b40164d28730e416cbd',
|
|
83
|
-
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
import WebSocket from 'ws';
|
|
3
|
-
import { readConfig } from '
|
|
4
|
-
import { refreshToken } from '
|
|
3
|
+
import { readConfig, serverWebSocketURL } from '@thinger-io/cli-core/config';
|
|
4
|
+
import { refreshToken } from '@thinger-io/cli-core/auth';
|
|
5
5
|
import { debugCount, debugLog } from '../debug-log.js';
|
|
6
|
-
import { v2 } from '
|
|
6
|
+
import { v2 } from '@thinger-io/cli-core/paths';
|
|
7
7
|
|
|
8
8
|
// Single shared websocket to /v2/users/{user}/events. Both fleet
|
|
9
9
|
// monitoring and product metrics live on the same endpoint with the
|
|
@@ -109,9 +109,9 @@ class EventStream {
|
|
|
109
109
|
if (this.disposed) return;
|
|
110
110
|
const config = readConfig();
|
|
111
111
|
if (!config?.server || !config?.token || !config?.username) return;
|
|
112
|
-
const url =
|
|
112
|
+
const url = `${serverWebSocketURL(config.server)}${v2(config.username)}/events?authorization=${config.token}`;
|
|
113
113
|
debugLog('ws:events', 'connecting', {
|
|
114
|
-
endpoint:
|
|
114
|
+
endpoint: `${serverWebSocketURL(config.server)}${v2(config.username)}/events`,
|
|
115
115
|
pending_subs: this.subs.size,
|
|
116
116
|
});
|
|
117
117
|
const ws = new WebSocket(url);
|
package/lib/device-api.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import api from '
|
|
2
|
-
import { requireConfig, resolveUser } from '
|
|
1
|
+
import api from '@thinger-io/cli-core/api';
|
|
2
|
+
import { requireConfig, resolveUser } from '@thinger-io/cli-core/config';
|
|
3
3
|
import { BYTES, TIMEOUTS } from './constants.js';
|
|
4
4
|
import { CommandStream } from './command-stream.js';
|
|
5
|
-
import { ensureFreshToken, refreshToken } from '
|
|
5
|
+
import { ensureFreshToken, refreshToken } from '@thinger-io/cli-core/auth';
|
|
6
6
|
import { Readable } from 'node:stream';
|
|
7
|
-
import { device } from '
|
|
7
|
+
import { device } from '@thinger-io/cli-core/paths';
|
|
8
8
|
|
|
9
9
|
// Directory endpoints on the agent expect a trailing slash so it can
|
|
10
10
|
// distinguish "list this directory" from "read a file with this name".
|
package/lib/guides.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The guides thinr ships for agents (served by `thinr_doc_read` and as MCP
|
|
5
|
+
* resources, next to the server's documentation). They describe what thinr
|
|
6
|
+
* implements on top of the server — the agent, product scripts, playbooks,
|
|
7
|
+
* the product dashboard — so they live and change with this code. Tool
|
|
8
|
+
* descriptions stay short and point here for the how-to.
|
|
9
|
+
*/
|
|
10
|
+
const guide = (name) => new URL(`../docs/guides/${name}.md`, import.meta.url);
|
|
11
|
+
|
|
12
|
+
export const guides = [
|
|
13
|
+
{
|
|
14
|
+
id: 'thinremote',
|
|
15
|
+
title: 'ThinRemote: devices, agent and products',
|
|
16
|
+
description:
|
|
17
|
+
'Start here: what the agent exposes and the tool for each need, what a ThinRemote product holds, fleet operations and recipes.',
|
|
18
|
+
file: guide('thinremote'),
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'thinremote-scripts',
|
|
22
|
+
title: 'ThinRemote scripts',
|
|
23
|
+
description:
|
|
24
|
+
'Device and product scripts: when to use each, the stdin/stdout JSON contract, resource shapes and examples.',
|
|
25
|
+
file: guide('thinremote-scripts'),
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'thinremote-playbooks',
|
|
29
|
+
title: 'ThinRemote playbooks',
|
|
30
|
+
description:
|
|
31
|
+
'Write, validate, test on one device and roll out playbooks: document format, variables, actions, batches and reports.',
|
|
32
|
+
file: guide('thinremote-playbooks'),
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 'thinremote-metrics-logs',
|
|
36
|
+
title: 'ThinRemote fleet metrics and log sources',
|
|
37
|
+
description:
|
|
38
|
+
"Per-product metrics and log sources read by thinr's terminal dashboard and `thinr device logs` (not the web console's dashboards): fields, aggregations, presets.",
|
|
39
|
+
file: guide('thinremote-metrics-logs'),
|
|
40
|
+
},
|
|
41
|
+
];
|