@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.
@@ -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.