@seeed-studio/solution-cli 0.0.0-stage → 0.1.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/README.md +77 -2
- package/bin/cli.js +136 -0
- package/bin/runner.js +39 -0
- package/lib/agent.js +160 -0
- package/lib/detector.js +321 -0
- package/lib/doctor.js +179 -0
- package/lib/downloader.js +219 -0
- package/lib/installer.js +375 -0
- package/lib/ledger.js +302 -0
- package/lib/platform.js +113 -0
- package/package.json +51 -4
- package/skills/sensecraft/SKILL.md +176 -0
- package/skills/sensecraft/references/app-only.md +273 -0
- package/skills/sensecraft/references/deploy.md +300 -0
- package/skills/sensecraft/references/diagnose.md +133 -0
- package/skills/sensecraft/references/error-catalog.md +33 -0
- package/skills/sensecraft/references/offline.md +78 -0
- package/skills/sensecraft/references/verify.md +136 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# Desktop-app-only capabilities
|
|
2
|
+
|
|
3
|
+
> **Requires the SenseCraft Solution desktop app** running on this computer.
|
|
4
|
+
> Everything here talks to the app's local HTTP API with `curl`. The engine
|
|
5
|
+
> CLI path in `SKILL.md` does not need any of it. Use this file when the user
|
|
6
|
+
> has the app open and wants to change a solution's configuration, deployed
|
|
7
|
+
> from the app, or asks what the app is showing.
|
|
8
|
+
|
|
9
|
+
Sections: 1. Connect to the app · 2. Deploy through the app · 3. Diagnose
|
|
10
|
+
through the app · 4. Change a solution's configuration (edit copy).
|
|
11
|
+
|
|
12
|
+
> **Windows PowerShell:** `curl` there is an alias for `Invoke-WebRequest`.
|
|
13
|
+
> Always type `curl.exe`. Send JSON bodies from a file (`--data @body.json`)
|
|
14
|
+
> instead of inline strings to avoid quoting problems.
|
|
15
|
+
|
|
16
|
+
## 1. Connect to the app (every app task)
|
|
17
|
+
|
|
18
|
+
1. Read the runtime file:
|
|
19
|
+
- macOS / Linux: `~/.sensecraft/runtime.json`
|
|
20
|
+
- Windows: `%USERPROFILE%\.sensecraft\runtime.json`
|
|
21
|
+
|
|
22
|
+
It holds `base_url`, `contract_version`, `logs_dir`, `solutions_dir` and
|
|
23
|
+
`solutions_edits_dir`.
|
|
24
|
+
2. Check the app is really there: `curl -s <base_url>/api/health` must return
|
|
25
|
+
`"status": "healthy"`. The file can be left over after a crash.
|
|
26
|
+
3. If the file is missing or the check fails, the app is not running: ask the
|
|
27
|
+
user to open **SenseCraft Solution** and try again. (Developers running
|
|
28
|
+
`./dev.sh` from source: `base_url` is `http://127.0.0.1:3260`.)
|
|
29
|
+
4. If `contract_version` is below 6, deploying and diagnosing still work, but
|
|
30
|
+
config editing is not available: tell the user to update the app.
|
|
31
|
+
5. The `sensecraft-engine` CLI writes the same file for its own instance. A
|
|
32
|
+
file with `"schema_version": 2` and `"python_backend": null` points at an
|
|
33
|
+
engine instance started by the CLI, not at the app: the app is not running.
|
|
34
|
+
|
|
35
|
+
Below, `BASE` means the `base_url` value. Add `lang=zh` to API calls that take
|
|
36
|
+
`lang` when the user writes in Chinese, `lang=en` otherwise.
|
|
37
|
+
|
|
38
|
+
## 2. Deploy through the app
|
|
39
|
+
|
|
40
|
+
`BASE` comes from §1. Examples use `lang=en`; use `lang=zh`
|
|
41
|
+
for a Chinese-speaking user.
|
|
42
|
+
|
|
43
|
+
### 2.1 Find the solution
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
curl -s "BASE/api/solutions?lang=en"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Match the user's wording against `name` / `summary`. Confirm the `id` with the
|
|
50
|
+
user if more than one fits.
|
|
51
|
+
|
|
52
|
+
### 2.2 Read what the deployment needs
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
curl -s "BASE/api/solutions/<id>/deploy-info?lang=en"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
- `presets[]` — deployment variants. If there is more than one, ask the user which
|
|
59
|
+
one, then call again with `&preset_id=<preset>`.
|
|
60
|
+
- `steps[]` — each step's `device_id`, `type`, `required`, parameters, and for
|
|
61
|
+
`docker_deploy` steps the `targets` (local = this computer's Docker, remote =
|
|
62
|
+
a device over SSH).
|
|
63
|
+
- `request_template` — a ready request body. Values shaped like
|
|
64
|
+
`<REQUIRED: ...>` must be filled in; everything else has a working default.
|
|
65
|
+
|
|
66
|
+
Never make up step or device IDs: the keys of `device_connections` and the
|
|
67
|
+
entries of `selected_devices` are exactly the ones in `request_template`.
|
|
68
|
+
|
|
69
|
+
Optional device discovery, to pre-fill hosts:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
curl -s "BASE/api/devices/detect/<id>?preset=<preset>" # USB / serial
|
|
73
|
+
curl -s "BASE/api/devices/scan-mdns" # SSH devices on the LAN
|
|
74
|
+
curl -s "BASE/api/docker-devices/local/check" # local Docker
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 2.3 Fill in and confirm
|
|
78
|
+
|
|
79
|
+
Ask the user for every `<REQUIRED: ...>` value (device IP, username, password,
|
|
80
|
+
and any user inputs). For `docker_deploy` steps pick the target with
|
|
81
|
+
`"target": "<target id>"` **and set `"target_type"` to that target's own type**
|
|
82
|
+
(`local` / `remote`, from deploy-info `targets`) — or remove `target_type`. The
|
|
83
|
+
template carries the default target's type; leaving it while switching
|
|
84
|
+
`target` is rejected with a 400. Remote targets also need `host`, `username`,
|
|
85
|
+
`password`. Add `"auto_replace_containers": true` only if the user agrees to
|
|
86
|
+
replace existing containers with the same name.
|
|
87
|
+
|
|
88
|
+
Write the filled body to a file, e.g. `body.json`:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"solution_id": "<id>",
|
|
93
|
+
"preset_id": "<preset>",
|
|
94
|
+
"selected_devices": ["<step_id>"],
|
|
95
|
+
"device_connections": {
|
|
96
|
+
"<step_id>": {"target": "<target_id>", "host": "192.168.1.50",
|
|
97
|
+
"username": "<user>", "password": "<password>"}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Show the user the body with the password as `<REDACTED>` before starting.
|
|
103
|
+
|
|
104
|
+
### 2.4 Start and wait
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
curl -s -X POST "BASE/api/deployments/start" -H "Content-Type: application/json" --data @body.json
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Keep the returned `deployment_id`. The response also lists each device with
|
|
111
|
+
`status`: `deploying`, or `skipped` with a `reason` (manual / info-only steps
|
|
112
|
+
are always skipped — that is normal, not an error).
|
|
113
|
+
|
|
114
|
+
Poll every 3–5 seconds:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
curl -s "BASE/api/deployments/<deployment_id>/summary"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`status` is `running` while in progress and ends as `completed`, `failed` or
|
|
121
|
+
`cancelled`. Each `devices[].steps[]` entry has its own `status` (`pending`,
|
|
122
|
+
`running`, `completed`, `failed`) and a `message`. The JSON is compact
|
|
123
|
+
(`"status":"failed"`, no spaces) — parse it rather than grepping. The user sees the same
|
|
124
|
+
deployment live in the app. Delete `body.json` afterwards (it holds a password).
|
|
125
|
+
|
|
126
|
+
### 2.5 Verify
|
|
127
|
+
|
|
128
|
+
`completed` is not enough. Check the result is actually usable:
|
|
129
|
+
|
|
130
|
+
- Docker steps: on the target (`docker ps` locally, or over SSH) the
|
|
131
|
+
solution's containers are `Up` and, if they define a health check, `(healthy)`.
|
|
132
|
+
- Services with a port: `curl -s -o /dev/null -w "%{http_code}" http://<host>:<port>/`
|
|
133
|
+
returns 2xx/3xx.
|
|
134
|
+
- Tell the user the URL to open and anything the guide says to do next.
|
|
135
|
+
|
|
136
|
+
### 2.6 If anything failed
|
|
137
|
+
|
|
138
|
+
Go straight to §3 below with the `deployment_id`. Do not stop to ask.
|
|
139
|
+
|
|
140
|
+
To retry after a fix, send the same body again — same `preset_id` and
|
|
141
|
+
`device_connections` — with `selected_devices` listing only the device(s) that
|
|
142
|
+
failed; the others are already done. Leaving manual steps in is harmless (they
|
|
143
|
+
are skipped).
|
|
144
|
+
|
|
145
|
+
## 3. Diagnose through the app
|
|
146
|
+
|
|
147
|
+
The general method (classify, retry, report, issue form) is in `diagnose.md`; these are the app-specific evidence sources.
|
|
148
|
+
|
|
149
|
+
### 3.1 Find the deployment
|
|
150
|
+
|
|
151
|
+
If you started it, you already have the `deployment_id`. Otherwise:
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
curl -s "BASE/api/deployments" # newest first: id, solution_id, status
|
|
155
|
+
curl -s "BASE/api/ai/context" # what is running right now, and what the user is looking at in the app
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### 3.2 Collect evidence
|
|
159
|
+
|
|
160
|
+
`/summary` and `/logs` only know deployments made since the app last started;
|
|
161
|
+
an older id from the list returns 404. In that case read the engine log (below)
|
|
162
|
+
for that time, or reproduce the failure by deploying again.
|
|
163
|
+
|
|
164
|
+
Start with the summary. **The real error is usually in the failing step's
|
|
165
|
+
`message`** (`devices[].steps[]` with `status: failed`); the top-level `errors`
|
|
166
|
+
list and the error-level logs may only say "Device X deployment failed".
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
curl -s "BASE/api/deployments/<deployment_id>/summary"
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
For more context around the failure, the full device log:
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
curl -s "BASE/api/deployments/<deployment_id>/logs?device_id=<device_id>&limit=300"
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
If the deployment logs are empty or end abruptly, read the engine log:
|
|
179
|
+
`<logs_dir>/provisioning-station.log` (last ~200 lines) and, if present,
|
|
180
|
+
`<logs_dir>/provisioning-station-crash.log`. `logs_dir` is in `runtime.json`.
|
|
181
|
+
|
|
182
|
+
Note the failing device, step and the first error — later errors are usually
|
|
183
|
+
consequences of the first.
|
|
184
|
+
|
|
185
|
+
### 3.3 Device checks, classification, retry, report
|
|
186
|
+
|
|
187
|
+
As in `diagnose.md` §3–6, with these differences: the solution folder is
|
|
188
|
+
`<solutions_dir>/<id>/` from `runtime.json`, or the edit copy if one exists;
|
|
189
|
+
configuration fixes go through §4; the issue form's `app_version` is
|
|
190
|
+
`app_version` from `runtime.json`, and its `diff` field is the output of
|
|
191
|
+
`/api/editor/solutions/<id>/diff`.
|
|
192
|
+
|
|
193
|
+
## 4. Change a solution's configuration
|
|
194
|
+
|
|
195
|
+
Needs `contract_version` 6 or later (see §1).
|
|
196
|
+
|
|
197
|
+
The app never changes its bundled solutions. Edits go into an **edit copy** of
|
|
198
|
+
one solution; while the edit copy exists, the app uses it instead of the
|
|
199
|
+
bundled one. Deleting it ("discard") restores the original. The app marks
|
|
200
|
+
the solution as "locally modified", shows the changes, and offers "restore
|
|
201
|
+
official version" — so the user can always see and undo what you changed.
|
|
202
|
+
|
|
203
|
+
### 4.1 Open the edit copy
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
curl -s -X POST "BASE/api/editor/solutions/<id>/enter"
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Returns `path` — the edit copy's folder. Calling it again is safe; it keeps
|
|
210
|
+
existing edits and returns the same `path`.
|
|
211
|
+
|
|
212
|
+
### 4.2 Edit files in `path`
|
|
213
|
+
|
|
214
|
+
| File | Holds |
|
|
215
|
+
|---|---|
|
|
216
|
+
| `solution.yaml` | Name, presets, intro metadata |
|
|
217
|
+
| `guide.md`, `guide_zh.md` | Deployment steps; each `## Step` header has `config=devices/<x>.yaml` |
|
|
218
|
+
| `devices/<x>.yaml` | One step's settings: `docker.compose_file`, `docker.environment`, `docker.services[].port` / `health_check_endpoint`, `pre_checks`, `ssh.default_user`, firmware source, ... |
|
|
219
|
+
| `assets/...` | What gets deployed: compose files, Node-RED flows, scripts, firmware |
|
|
220
|
+
|
|
221
|
+
Field definitions:
|
|
222
|
+
https://raw.githubusercontent.com/suharvest/sensecraft-solutions/main/spec/device.schema.json
|
|
223
|
+
and `.../spec/solution.schema.json`.
|
|
224
|
+
|
|
225
|
+
Keep changes minimal: change what the evidence points at, nothing else. When a
|
|
226
|
+
value appears in several places (a port in compose, in `services[].port` and in
|
|
227
|
+
`pre_checks`), change all of them.
|
|
228
|
+
|
|
229
|
+
**Device YAMLs and assets are read from disk at deploy time**, so they take
|
|
230
|
+
effect as soon as you save them. Always validate before redeploying.
|
|
231
|
+
|
|
232
|
+
### 4.3 Validate
|
|
233
|
+
|
|
234
|
+
```
|
|
235
|
+
curl -s -X POST "BASE/api/editor/solutions/<id>/validate"
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
`ok: false` → fix each entry in `errors` (`file` is relative to `path`, `line`
|
|
239
|
+
is 1-based or null) and validate again. `warnings` do not block; leave
|
|
240
|
+
existing ones (e.g. "missing Chinese translation") alone unless the user asked
|
|
241
|
+
for guide changes.
|
|
242
|
+
|
|
243
|
+
### 4.4 Apply
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
curl -s -X POST "BASE/api/editor/solutions/<id>/apply"
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
- `200` with `active_path` equal to `path` → the edit copy is live, including
|
|
250
|
+
changes to `solution.yaml` and the guides.
|
|
251
|
+
- `422` → `{"detail": <the same report as validate>}`; nothing was reloaded.
|
|
252
|
+
Fix the `detail.errors` entries and retry.
|
|
253
|
+
|
|
254
|
+
Then redeploy the affected device (§2.6, or `deploy.md` §6 on the engine path).
|
|
255
|
+
|
|
256
|
+
### 4.5 Show what changed
|
|
257
|
+
|
|
258
|
+
```
|
|
259
|
+
curl -s "BASE/api/editor/solutions/<id>/diff"
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Returns `files[]` with `path`, `status` (`modified` / `added` / `removed`) and a
|
|
263
|
+
unified `diff` for text files. Show the user these diffs. The same changes are
|
|
264
|
+
visible in the app.
|
|
265
|
+
|
|
266
|
+
### 4.6 Undo, share
|
|
267
|
+
|
|
268
|
+
- Undo all edits: `curl -s -X POST "BASE/api/editor/solutions/<id>/discard"`
|
|
269
|
+
- Package the edit copy as a zip (returns `zip_path`) to send to the solution
|
|
270
|
+
maintainers or turn into a pull request:
|
|
271
|
+
`curl -s -X POST "BASE/api/editor/solutions/<id>/export"`
|
|
272
|
+
- An app content update does not overwrite the edit copy. If the user later
|
|
273
|
+
wants the updated official version, discard the edit copy.
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# Deploy a solution
|
|
2
|
+
|
|
3
|
+
Run Step 0 in `SKILL.md` first (engine installed, solutions pulled). Examples
|
|
4
|
+
use `--lang en`; use `--lang zh` for a Chinese-speaking user.
|
|
5
|
+
|
|
6
|
+
## 1. Find the solution
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
sensecraft-engine solution list --json
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Match the user's wording against `name` / `summary`. Confirm the `id` with the
|
|
13
|
+
user if more than one fits. `sensecraft-engine solution show <id> --lang en`
|
|
14
|
+
prints the full solution if you need more detail.
|
|
15
|
+
|
|
16
|
+
## 2. Read what the deployment needs
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
sensecraft-engine deploy-info <id> --lang en
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- `presets[]` — deployment variants. If there is more than one, ask the user which
|
|
23
|
+
one, then run again with `--preset <preset>`. Without `--preset` the template
|
|
24
|
+
lists the steps of every preset.
|
|
25
|
+
- `steps[]` — each step's `device_id`, `type`, `required`, `connection_type`
|
|
26
|
+
(`ssh` / `serial` / `none` / ...), `parameters`, `category` (`deploy` or
|
|
27
|
+
`verify`) and `interactive`. Steps with `has_targets: true` (e.g.
|
|
28
|
+
`docker_deploy`) list `targets`: `local` = this computer's Docker, `remote` =
|
|
29
|
+
a device over SSH.
|
|
30
|
+
- Verify steps that need the user carry a ready command list:
|
|
31
|
+
`counting_cli`, `face_cli`, `wizard_cli` or `robot_cli` (see `verify.md`).
|
|
32
|
+
- `request_template` — a ready request body. Values shaped like
|
|
33
|
+
`<REQUIRED: ...>` must be filled in; everything else has a working default.
|
|
34
|
+
|
|
35
|
+
Never make up step or device IDs: the keys of `device_connections` and the
|
|
36
|
+
entries of `selected_devices` are exactly the ones in `request_template`.
|
|
37
|
+
|
|
38
|
+
Optional discovery, to pre-fill values:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
sensecraft-engine device ports # serial ports (USB devices)
|
|
42
|
+
sensecraft-engine device scan --timeout 5 # SSH devices on the LAN (mDNS, IPv4)
|
|
43
|
+
sensecraft-engine device list # device catalog
|
|
44
|
+
sensecraft-engine doctor --json # this computer: serial permission, Docker, disk, network
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## 3. Fill in and confirm
|
|
48
|
+
|
|
49
|
+
Ask the user for every `<REQUIRED: ...>` value (device IP, username, password,
|
|
50
|
+
serial port, and any user inputs). Serial steps carry both `port` and
|
|
51
|
+
`serial_port` placeholders; fill both with the port name from `device ports`.
|
|
52
|
+
|
|
53
|
+
For `docker_deploy` steps pick the target with `"target": "<target id>"` **and
|
|
54
|
+
set `"target_type"` to that target's own type** (`local` / `remote`) — or
|
|
55
|
+
remove `target_type`. Leaving the template's type while switching `target` is
|
|
56
|
+
rejected. Each deploy-info target carries `type` (`local` / `remote`) and,
|
|
57
|
+
when the target is tied to a board, `device` / `device_name` /
|
|
58
|
+
`device_class` (for example `rk3576`, `RK3576`): pick the target whose
|
|
59
|
+
`type` and device match the user's hardware, and copy its `type` into
|
|
60
|
+
`target_type`. `device` is null for targets that run on any Docker host. On
|
|
61
|
+
engine builds whose targets have no `type` yet, the target id suffix (for
|
|
62
|
+
example `_local`, `_remote`) is only a hint; confirm the type with `validate`
|
|
63
|
+
(§3, dry run) before starting. Remote targets need `host`, `username`
|
|
64
|
+
and either `password` or `key_file`:
|
|
65
|
+
|
|
66
|
+
Windows native `local` Docker is unsupported by both deployers and returns the
|
|
67
|
+
platform check failure; use a `remote` target over SSH to a Linux Docker host.
|
|
68
|
+
WSL must use the Linux engine binary and is Linux-host evidence, not Windows
|
|
69
|
+
native local Docker support. On macOS, the local target uses the literal
|
|
70
|
+
`docker` executable visible to the new engine process. If Docker Desktop's CLI
|
|
71
|
+
is only in its application bundle, provide that bundle's `bin` directory in a
|
|
72
|
+
temporary engine-process `PATH`; do not alter global shell configuration or
|
|
73
|
+
assume an already-running instance inherits the path. Docker Desktop being
|
|
74
|
+
running is a prerequisite check, not a deployment result.
|
|
75
|
+
|
|
76
|
+
- Password: `"password": "<the user's password>"`.
|
|
77
|
+
- SSH key: `"key_file": "~/.ssh/id_ed25519"` and no `password` field. The key
|
|
78
|
+
must be a regular file inside `~/.ssh` (or the engine's data directory); a
|
|
79
|
+
relative path is resolved under `~/.ssh`; other locations are refused.
|
|
80
|
+
|
|
81
|
+
Add `"auto_replace_containers": true` (or the `--replace-existing` flag) only if
|
|
82
|
+
the user agrees to replace existing containers with the same name.
|
|
83
|
+
|
|
84
|
+
Write the filled body to `./.sensecraft-work/body.json` in the current working
|
|
85
|
+
directory, not `/tmp` (a file keeps the password out of the process list and
|
|
86
|
+
avoids shell quoting problems, notably in PowerShell):
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"solution_id": "<id>",
|
|
91
|
+
"preset_id": "<preset>",
|
|
92
|
+
"selected_devices": ["<step_id>"],
|
|
93
|
+
"device_connections": {
|
|
94
|
+
"<step_id>": {"target": "<target_id>", "target_type": "remote",
|
|
95
|
+
"host": "192.168.1.50", "username": "<user>",
|
|
96
|
+
"password": "<password>"}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Show the user the body with the password as `<REDACTED>` before starting.
|
|
102
|
+
|
|
103
|
+
On Windows PowerShell 5.1, write the JSON as UTF-8 **without a BOM**. The
|
|
104
|
+
default `Set-Content -Encoding UTF8` adds a BOM, which the Rust request reader
|
|
105
|
+
rejects. Keep the request file under the current workspace and do not print it
|
|
106
|
+
when it contains a password:
|
|
107
|
+
|
|
108
|
+
```powershell
|
|
109
|
+
$RequestPath = [System.IO.Path]::GetFullPath('.sensecraft-work/body.json')
|
|
110
|
+
$RequestDir = [System.IO.Path]::GetDirectoryName($RequestPath)
|
|
111
|
+
New-Item -ItemType Directory -Force -Path $RequestDir | Out-Null
|
|
112
|
+
$body = @{
|
|
113
|
+
solution_id = '<id>'
|
|
114
|
+
preset_id = '<preset>'
|
|
115
|
+
selected_devices = @('<step_id>')
|
|
116
|
+
device_connections = @{
|
|
117
|
+
'<step_id>' = @{
|
|
118
|
+
target = '<target_id>'
|
|
119
|
+
target_type = 'remote'
|
|
120
|
+
host = '192.168.1.50'
|
|
121
|
+
username = '<user>'
|
|
122
|
+
password = '<password>'
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
$json = $body | ConvertTo-Json -Depth 50
|
|
127
|
+
[System.IO.File]::WriteAllText($RequestPath, $json, [System.Text.UTF8Encoding]::new($false))
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`-Depth 50` preserves nested `device_connections`; PowerShell's default depth
|
|
131
|
+
of 2 is insufficient. Replace placeholders with user-provided values and keep
|
|
132
|
+
the file protected; never print the password.
|
|
133
|
+
|
|
134
|
+
Before a remote deploy, run the dry run (no changes on any device):
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
sensecraft-engine validate --params ./.sensecraft-work/body.json
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
It reports parameter errors and previews which verify steps will need the user
|
|
141
|
+
(`verify_required`). It also lists the changes the deployment may make on each
|
|
142
|
+
target beyond the solution's own files and containers, as
|
|
143
|
+
`side_effects: {"<step_id>": [{"kind", "what", "when", "decided_in"}, ...]}`:
|
|
144
|
+
|
|
145
|
+
| `kind` | Change on the target |
|
|
146
|
+
|---|---|
|
|
147
|
+
| `sudo` | Commands run with `sudo` (Docker through `sudo docker`, actions marked `sudo`) |
|
|
148
|
+
| `daemon_json_write` | Merges `registry-mirrors` into `/etc/docker/daemon.json` (other keys kept) |
|
|
149
|
+
| `docker_daemon_restart` | Restarts the Docker daemon (all containers on the device restart with it) |
|
|
150
|
+
| `package_install` | Installs packages (Docker / Compose, `.deb`, apt / pip in an action) |
|
|
151
|
+
| `system_config` | Changes system settings (`usermod`, `update-alternatives`, services) |
|
|
152
|
+
| `reboot` | Reboots the device |
|
|
153
|
+
| `firmware_flash` | Flashes firmware |
|
|
154
|
+
| `local_script` | Runs a script on this computer |
|
|
155
|
+
|
|
156
|
+
`what` says what happens in plain words, `when` the condition: `always`, a
|
|
157
|
+
condition the engine checks during the deploy (for example "the device cannot
|
|
158
|
+
reach Docker Hub"), or "the action's script decides". `decided_in` points at
|
|
159
|
+
engine code; it is for bug reports, not something to read.
|
|
160
|
+
|
|
161
|
+
Tell the user what the list contains. If the user forbade a kind that is
|
|
162
|
+
listed (Docker restart, daemon.json changes, reboot, package installs) and
|
|
163
|
+
has not agreed to it, stop and ask before starting, also when `when` is a
|
|
164
|
+
condition: the deploy may meet it. Exception: when the action's script is in
|
|
165
|
+
the solution's step file (`devices/<x>.yaml` under `actions` or
|
|
166
|
+
`remote_overrides.actions` in `~/.sensecraft/content/solutions/<id>/`, or
|
|
167
|
+
`$SC_HOME/content/solutions/<id>/` when `SC_HOME` is set), read
|
|
168
|
+
it; if its condition can be checked read-only on the device and is not met,
|
|
169
|
+
say so and continue. Actions that come from the engine's built-in device
|
|
170
|
+
profile are not in that file: treat them as possible. Builds without a
|
|
171
|
+
`side_effects` field: assume a remote Docker deploy may write
|
|
172
|
+
`/etc/docker/daemon.json` and ask when the user restricted that.
|
|
173
|
+
|
|
174
|
+
Before the deploy, save the target's current Docker daemon config so changes
|
|
175
|
+
can be shown and restored:
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
ssh <user>@<ip> cat /etc/docker/daemon.json > ./.sensecraft-work/daemon.json.before
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
(a missing file is fine: note that it did not exist). After the deploy, `cat`
|
|
182
|
+
it again and show the user any difference; restoring it is a change on the
|
|
183
|
+
device and needs the user's OK.
|
|
184
|
+
|
|
185
|
+
## 4. Start and follow
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
sensecraft-engine deploy --params ./.sensecraft-work/body.json --yes --detach --lang en
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`--detach` prints `{"deployment_id": "...", "log": "..."}` and returns. Keep the
|
|
192
|
+
`deployment_id`. Exit code 2 means the request or arguments are invalid. When
|
|
193
|
+
the JSON error contains a `missing[]` list, ask for those fields
|
|
194
|
+
(`preset_id`, `device_connections.<id>.<key>`); for `bad_request` without
|
|
195
|
+
`missing[]`, inspect the solution ID, JSON shape, and parameter spelling before
|
|
196
|
+
asking for another value.
|
|
197
|
+
Exit code 3 with a 409 means the device is busy with another deployment.
|
|
198
|
+
|
|
199
|
+
Follow progress in one of two ways:
|
|
200
|
+
|
|
201
|
+
- **Poll** (works with any agent's command timeout): every 3–5 seconds run
|
|
202
|
+
`sensecraft-engine status <deployment_id> --json`. `status` is `running`
|
|
203
|
+
while in progress and ends as `completed`, `failed`, `cancelled` or
|
|
204
|
+
`interrupted`; each device and step has its own `status` and `message`.
|
|
205
|
+
Exit code: 0 while running or completed, 1 for failed / cancelled /
|
|
206
|
+
interrupted.
|
|
207
|
+
- **Event stream:** `sensecraft-engine logs <deployment_id> --since <N>`
|
|
208
|
+
prints NDJSON events (`{seq, type, ...}`) with `seq > N` and exits; add
|
|
209
|
+
`--follow` to keep streaming until the deployment ends (run it in the
|
|
210
|
+
background if your command timeout is short). Remember the last `seq` and
|
|
211
|
+
pass it as `--since` next time, so you never read the same events twice.
|
|
212
|
+
|
|
213
|
+
Event types to act on:
|
|
214
|
+
|
|
215
|
+
| `type` | Meaning |
|
|
216
|
+
|---|---|
|
|
217
|
+
| `log` | `level`, `message`; `error`-level lines are evidence for `diagnose.md` |
|
|
218
|
+
| `step_skipped` with `reason: verify_required` | The step needs the user — the `verify_required` event before it has the details |
|
|
219
|
+
| `verify_required` | Go to `verify.md` with this event |
|
|
220
|
+
| `device_completed` | One device finished |
|
|
221
|
+
| `deployment_completed` | Last event; `status` is the final result |
|
|
222
|
+
|
|
223
|
+
Read the JSON directly instead of grepping it (no interpreter, see `SKILL.md`
|
|
224
|
+
rule 5). Delete `./.sensecraft-work/` afterwards (`body.json` holds a
|
|
225
|
+
password).
|
|
226
|
+
|
|
227
|
+
Without `--detach`, `deploy --json` streams the same events on stdout and ends
|
|
228
|
+
with one `{"type": "result", "deployment_id", "status", "devices"}` line; the
|
|
229
|
+
exit code is the deployment's result. Ctrl-C or a killed agent command only
|
|
230
|
+
detaches: the deployment keeps running in the engine instance; continue with
|
|
231
|
+
`status` / `logs`.
|
|
232
|
+
|
|
233
|
+
If `--detach` prints `"backend": "python"`, the desktop app's backend is
|
|
234
|
+
running this deployment: `logs` then has `log` and `deployment_completed`
|
|
235
|
+
events only, no `verify_required`. Do the guide's verify steps with the user
|
|
236
|
+
as described in `verify.md` §1.
|
|
237
|
+
|
|
238
|
+
## 5. Verify
|
|
239
|
+
|
|
240
|
+
`completed` is not enough. Check the result is actually usable:
|
|
241
|
+
|
|
242
|
+
- `verify_required` events: handle each one with `verify.md`.
|
|
243
|
+
- Docker steps: on the target the solution's containers have `status`
|
|
244
|
+
`running` (and are `(healthy)` in `docker ps` when they define a health
|
|
245
|
+
check):
|
|
246
|
+
`sensecraft-engine docker ps --json` (this computer),
|
|
247
|
+
`sensecraft-engine docker ps --host <ip> --username <user> --key-file ~/.ssh/id_ed25519 --json`
|
|
248
|
+
(remote with an SSH key; same path rules as `key_file` in §3), or
|
|
249
|
+
`SC_SSH_PASSWORD=<password> sensecraft-engine docker ps --host <ip> --username <user> --json`
|
|
250
|
+
(remote with a password; `--password-stdin` reads it from stdin instead).
|
|
251
|
+
`docker logs <container> --host ... --key-file ...` takes the same options.
|
|
252
|
+
Older engines without `--key-file` accept a password only; with a key-only
|
|
253
|
+
device use `ssh <user>@<ip> docker ps`.
|
|
254
|
+
- Services with a port: `curl -s -o /dev/null -w "%{http_code}" http://<host>:<port>/`
|
|
255
|
+
returns 2xx/3xx.
|
|
256
|
+
- Stay running: `completed` can precede a crash loop. CLI deploys already
|
|
257
|
+
watch each Docker service for about 60 seconds after its health check
|
|
258
|
+
answers and fail the step with `Service <name> is not healthy: ...` when a
|
|
259
|
+
container restarts, exits or disappears. Check once more yourself: run `docker ps` once,
|
|
260
|
+
wait about 60 seconds, then run it again and read the container restart
|
|
261
|
+
count (`docker inspect -f '{{.Name}} {{.RestartCount}} {{.State.Status}}' <container>`
|
|
262
|
+
over SSH, or the `Restarting` / `Up Less than a second` status in `docker ps`).
|
|
263
|
+
The counts must stay 0 and the verify endpoint (the step's `url`, or the
|
|
264
|
+
port check above) must still answer after the wait. If a container restarts
|
|
265
|
+
or the endpoint stops answering, report a crash loop, not success, and go to
|
|
266
|
+
`diagnose.md`.
|
|
267
|
+
- Tell the user the URL to open and anything the guide says to do next.
|
|
268
|
+
|
|
269
|
+
## 6. If anything failed
|
|
270
|
+
|
|
271
|
+
Go straight to `diagnose.md` with the `deployment_id`. Do not stop to ask.
|
|
272
|
+
|
|
273
|
+
To retry after a fix, send the same body again — same `preset_id` and
|
|
274
|
+
`device_connections` — with `selected_devices` listing only the device(s) that
|
|
275
|
+
failed (or `--device <step_id>` for one device); the others are already done.
|
|
276
|
+
Leaving manual steps in is harmless (they are skipped).
|
|
277
|
+
|
|
278
|
+
Two kinds of failure, two policies:
|
|
279
|
+
|
|
280
|
+
- **Transient registry or network errors** (image pull answered 401 / 429 /
|
|
281
|
+
500 / 502 / 503 / 504, `TLS handshake timeout`, `i/o timeout`,
|
|
282
|
+
`connection reset`, a mirror that flaps): retry the same body without
|
|
283
|
+
asking, up to 5 times, waiting 15, 30, 60, 120, 240 seconds between tries.
|
|
284
|
+
These retries do not count as fix rounds. If the engine log shows the
|
|
285
|
+
mirror changing between tries, that is the same transient case. After the
|
|
286
|
+
5th failure, stop and report the error text, the waits and the registry host.
|
|
287
|
+
- **Deterministic failures** (bad parameter or credentials, port taken, disk
|
|
288
|
+
full, missing file, a container that exits on start, the same error with the
|
|
289
|
+
same text after a fix): stop retrying blindly. First retry after a fix
|
|
290
|
+
without asking; later retries need the user's OK; at most 3 rounds, then
|
|
291
|
+
report.
|
|
292
|
+
|
|
293
|
+
If you cannot tell which kind it is, treat the second occurrence of the same
|
|
294
|
+
error after a wait as deterministic.
|
|
295
|
+
|
|
296
|
+
## Cancel
|
|
297
|
+
|
|
298
|
+
Only with the user's OK: `sensecraft-engine cancel <deployment_id>`. Then check
|
|
299
|
+
`status` until it is `cancelled`, and tell the user which steps had already run:
|
|
300
|
+
the device may be half-configured.
|