@seeed-studio/solution-cli 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,113 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import fs from 'node:fs';
4
+
5
+ export function getPlatformInfo() {
6
+ const p = os.platform();
7
+ const a = os.arch();
8
+
9
+ let target = null;
10
+ let supported = true;
11
+ let note = '';
12
+
13
+ if (p === 'darwin') {
14
+ if (a === 'arm64') {
15
+ target = 'aarch64-apple-darwin';
16
+ } else {
17
+ supported = false;
18
+ note = 'macOS x86_64 (Intel) is not supported for standalone binary releases. Under Rosetta, run from a native ARM shell.';
19
+ }
20
+ } else if (p === 'linux') {
21
+ if (a === 'x64') {
22
+ target = 'x86_64-unknown-linux-musl';
23
+ } else if (a === 'arm64') {
24
+ target = 'aarch64-unknown-linux-musl';
25
+ } else {
26
+ supported = false;
27
+ note = `Linux architecture ${a} is not supported. Supported: x64, arm64.`;
28
+ }
29
+ } else if (p === 'win32') {
30
+ if (a === 'x64') {
31
+ target = 'x86_64-pc-windows-msvc';
32
+ } else {
33
+ supported = false;
34
+ note = `Windows architecture ${a} is not supported. Supported: x64.`;
35
+ }
36
+ } else {
37
+ supported = false;
38
+ note = `Operating system ${p} is not supported.`;
39
+ }
40
+
41
+ const isWindows = p === 'win32';
42
+ const binaryName = isWindows ? 'sensecraft-engine.exe' : 'sensecraft-engine';
43
+
44
+ return {
45
+ platform: p,
46
+ arch: a,
47
+ target,
48
+ supported,
49
+ note,
50
+ isWindows,
51
+ binaryName
52
+ };
53
+ }
54
+
55
+ export function getScHome() {
56
+ return process.env.SC_HOME || path.join(os.homedir(), '.sensecraft');
57
+ }
58
+
59
+ export function getEngineBinDir() {
60
+ return path.join(getScHome(), 'bin');
61
+ }
62
+
63
+ export function getEngineBinaryPath() {
64
+ const { binaryName } = getPlatformInfo();
65
+ return path.join(getEngineBinDir(), binaryName);
66
+ }
67
+
68
+ export function getContentDir() {
69
+ return path.join(getScHome(), 'content');
70
+ }
71
+
72
+ export function getConfiguredEnginePath() {
73
+ const pointerFile = path.join(getScHome(), 'engine.json');
74
+ if (fs.existsSync(pointerFile)) {
75
+ try {
76
+ const data = JSON.parse(fs.readFileSync(pointerFile, 'utf8'));
77
+ if (data && data.bin && fs.existsSync(data.bin) && isExecutable(data.bin)) {
78
+ return path.resolve(data.bin);
79
+ }
80
+ } catch {}
81
+ }
82
+ return path.resolve(getEngineBinaryPath());
83
+ }
84
+
85
+ export function setConfiguredEnginePath(binPath, version = null, source = null) {
86
+ const pointerFile = path.join(getScHome(), 'engine.json');
87
+ const dir = path.dirname(pointerFile);
88
+ fs.mkdirSync(dir, { recursive: true });
89
+ const payload = {
90
+ schema_version: 1,
91
+ bin: path.resolve(binPath),
92
+ version: version || 'unknown',
93
+ source: source || 'unknown',
94
+ updated_at: new Date().toISOString()
95
+ };
96
+ const tempFile = path.join(dir, `.engine.tmp-${Date.now()}`);
97
+ fs.writeFileSync(tempFile, JSON.stringify(payload, null, 2), 'utf8');
98
+ fs.renameSync(tempFile, pointerFile);
99
+ }
100
+
101
+ export function isExecutable(filePath) {
102
+ try {
103
+ const stat = fs.statSync(filePath);
104
+ if (!stat.isFile()) return false;
105
+ if (os.platform() === 'win32') {
106
+ return filePath.toLowerCase().endsWith('.exe') || filePath.toLowerCase().endsWith('.cmd');
107
+ }
108
+ // Unix: check execute permissions (owner/group/others)
109
+ return Boolean(stat.mode & 0o111);
110
+ } catch {
111
+ return false;
112
+ }
113
+ }
package/package.json CHANGED
@@ -1,6 +1,54 @@
1
1
  {
2
2
  "name": "@seeed-studio/solution-cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Zero-friction installer and CLI runner for SenseCraft Engine and AI Agent Skills (Work Buddy, Claude Code, Codex, CodeBuddy)",
5
+ "type": "module",
6
+ "main": "./lib/installer.js",
7
+ "bin": {
8
+ "solution-cli": "bin/cli.js",
9
+ "sensecraft-solution-cli": "bin/cli.js",
10
+ "sensecraft": "bin/cli.js",
11
+ "sensecraft-engine": "bin/runner.js",
12
+ "sensecraft-skill": "bin/cli.js"
13
+ },
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "files": [
18
+ "bin",
19
+ "lib",
20
+ "catalog",
21
+ "skills",
22
+ "README.md"
23
+ ],
24
+ "scripts": {
25
+ "test": "node --test tests/cli.test.mjs",
26
+ "install:self": "node bin/cli.js install",
27
+ "status": "node bin/cli.js status",
28
+ "doctor": "node bin/cli.js doctor"
29
+ },
30
+ "keywords": [
31
+ "seeed-studio",
32
+ "sensecraft",
33
+ "sensecraft-solution",
34
+ "iot",
35
+ "workbuddy",
36
+ "claude-code",
37
+ "codex",
38
+ "codebuddy",
39
+ "skills",
40
+ "edge-ai",
41
+ "jetson",
42
+ "raspberry-pi"
43
+ ],
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/Seeed-Studio/app_collaboration.git",
47
+ "directory": "packages/solution-cli"
48
+ },
49
+ "author": "Seeed Studio",
50
+ "license": "Apache-2.0",
51
+ "engines": {
52
+ "node": ">=18.0.0"
53
+ }
54
+ }
@@ -0,0 +1,183 @@
1
+ ---
2
+ name: sensecraft
3
+ description: Consult, select, deploy, manage, and troubleshoot SenseCraft / Seeed Studio edge IoT solutions (智慧农业, 摔倒检测, 边缘安全, 智慧仓库, 语音助手, people counting, face recognition 等) and hardware products (reComputer Jetson, reCamera, SenseCAP, Grove Vision AI, ESP32) using the `sensecraft-engine` and `solution-cli` toolset. Use whenever the user asks for edge IoT solutions, hardware recommendations, accessory compatibility, deployment, or troubleshooting.
4
+ ---
5
+
6
+ # SenseCraft Solution
7
+
8
+ This skill drives the `sensecraft-engine` execution engine and `solution-cli` toolset. It operates across the complete lifecycle: **Solution Consultation ➔ Hardware & Accessory BOM Selection ➔ Local Deployment ➔ Self-Healing Diagnostics**.
9
+
10
+ ## Workflow Routing & Progressive Disclosure
11
+
12
+ Do NOT read all reference files at once. Determine the user's workflow state and intent, then load **at most ONE** reference file on demand:
13
+
14
+ | User State & Intent | Reference to Load | Primary CLI Tool to Call | Guardrails & Forbidden Actions |
15
+ |---|---|---|---|
16
+ | **DISCOVERY / CONSULT**<br>“我想做防跌倒监控”、“有智慧农场方案吗” | `references/consult-and-select.md` | `solution-cli hub search "<keyword>" --json` | **DO NOT run deploy**; user is exploring scenarios and has not confirmed hardware. |
17
+ | **PROCUREMENT / VERIFY_BOM**<br>“买什么硬件?”、“J4012 需要配什么配件?” | `references/hardware-bom.md` | `solution-cli product accessories <id> --json` | **DO NOT fabricate** third-party SKUs or prices. Always distinguish REQUIRED vs OPTIONAL accessories. |
18
+ | **ONBOARDING / PRECHECK**<br>“设备到了,插上了网线/USB”、“环境自检” | `references/diagnose.md` | `solution-cli doctor` / `sensecraft-engine device scan` | Freeze hardware requirements into DeploymentLockfile before proceeding to apply. |
19
+ | **DEPLOYING / APPLY**<br>“开始安装”、“部署 fall_detection” | `references/deploy.md` | `sensecraft-engine deploy --solution <id>` | **DEPLOYMENT CRITICAL SECTION**: Zero external commercial queries allowed during apply. |
20
+ | **TROUBLESHOOTING**<br>“部署报错”、“黑屏”、“无视频流” | `references/error-catalog.md` | `sensecraft-engine logs` / `solution-cli product verify-compatibility` | Max 3 fix-and-retry rounds. Follow local auto-patching rule. |
21
+
22
+ Capabilities that need the **SenseCraft Solution desktop app** (editing a
23
+ solution's configuration, the app's `/api/ai/context`, diagnosing through the
24
+ app's logs) are in `references/app-only.md`. Use them only when the user has
25
+ the app running.
26
+
27
+ ## Host platform boundary
28
+
29
+ The standalone engine supports four host platforms: macOS ARM
30
+ (`aarch64-apple-darwin`), Linux ARM (`aarch64-unknown-linux-musl`), Linux
31
+ x86_64 (`x86_64-unknown-linux-musl`), and Windows x86_64
32
+ (`x86_64-pc-windows-msvc`). macOS x86_64 is not maintained; under Rosetta,
33
+ run the installer from a native ARM shell.
34
+
35
+ `docker_deploy` target `local` requires a supported Docker host. Windows native
36
+ `docker_local` is intentionally unsupported by both the Python and Rust
37
+ deployers; a Windows CLI can still use a `remote` target over SSH to a Linux
38
+ Docker host. WSL uses the Linux engine binary and Linux host behavior; do not
39
+ describe a WSL run as Windows-native local Docker support. USB device steps
40
+ remain hardware-dependent and require the user to connect and confirm the
41
+ device.
42
+
43
+ On macOS, local Docker uses the literal `docker` command found by the engine's
44
+ process environment. If Docker Desktop's CLI is only inside its application
45
+ bundle, start a new engine process with a temporary `PATH` containing that
46
+ bundle's `bin` directory; do not change global shell configuration or assume an
47
+ existing engine daemon inherits the new path. Verify the new engine instance
48
+ with `doctor --json` before selecting a local target. A running Docker Desktop
49
+ daemon is a host prerequisite, not evidence that a deployment has passed.
50
+
51
+ ## Rules
52
+
53
+ 1. **Credentials and secrets:** never invent a host, username or password —
54
+ ask the user. Replace passwords, tokens, API keys and any other secret a
55
+ deployment prints (for example a gateway token) with `<REDACTED>` in
56
+ anything you show or log. Tell the user where to find the value (on the
57
+ device, in the solution's page, in the file that holds it) instead of
58
+ printing it.
59
+ 2. **What you may do without asking, and what needs the user's OK:**
60
+
61
+ | Action | Ask first? |
62
+ |---|---|
63
+ | Read deployment status and logs (`status`, `logs`), engine log files, `doctor`, `device list/ports/scan` | No |
64
+ | Read-only checks on a device over SSH (`docker ps`, `docker logs`, `df -h`, `ss -ltnp`, `ping`) | No |
65
+ | Edit the solution's edit copy (desktop app only, see `references/app-only.md`) | No — show the diff afterwards; the user sees it in the app as "locally modified" and can restore the official version in one click |
66
+ | Anything a person does with their hands: plug in or swap a cable, connect a servo, sit in front of a camera, walk across a counting line, move or stand near a robot arm | **Yes** — tell the user what to do and wait for them to confirm it is done |
67
+ | Commands that move hardware (`robot torque`, `robot actions test`) | **Yes** — the user confirms nobody is within reach first |
68
+ | Change anything on a device: stop/remove containers, delete files, install packages, change system settings; `--replace-existing` | **Yes** |
69
+ | Redeploy | First retry: no. Every later retry: **yes** |
70
+ | Cancel a running deployment | **Yes** — it can leave a device half-configured |
71
+ | Prepare an issue report (the user submits it) or open a pull request | **Yes** |
72
+
73
+ 3. **At most 3 fix-and-retry rounds** per problem for deterministic failures
74
+ (wrong parameter, missing file, port taken, bad credentials, a container
75
+ that exits). After that, stop and give the user a report (what failed, what
76
+ you checked, what you changed). Transient registry or network errors follow
77
+ the backoff rule in `references/deploy.md` §6 and do not use up a round.
78
+ 4. **Preserving official cache & Local auto-patching**: Never edit files directly under the official content cache directory (`~/.sensecraft/content/`). When fixing a solution-level bug (timeout too short, wrong port/env var, minor syntax/transfer glitch, broken health check):
79
+ - In CLI / Engine mode: copy the solution from `~/.sensecraft/content/solutions/<id>` into `./.sensecraft-work/solutions/<id>`, apply the minimal fix in that copy. **Crucial engine switch step**: Because an existing engine daemon instance retains its initial solution directory in memory, you MUST stop the running instance first: `sensecraft-engine daemon stop`. Then re-run deployment with `--solutions-dir ./.sensecraft-work/solutions` so the fresh engine instance picks up the patched files. Show the user the patch diff.
80
+ - If running with the Desktop App: use the app's edit copy mechanism (`references/app-only.md` §4).
81
+ 5. **Reading output:** read the CLI's JSON output directly; it is small. If a
82
+ filter is needed, use `jq` only when it is already installed. Do not start
83
+ python, node or any other interpreter to parse output. Where a command has
84
+ a flag that prints just what you need (`--json`, `--preset`, `--device`,
85
+ `--since`), use it.
86
+ 6. **Temp files:** keep request bodies and scratch files in the current
87
+ working directory under `./.sensecraft-work/`, never in `/tmp`. Remove the
88
+ directory when the task ends, also after a failure.
89
+
90
+ ## Step 0 — Install the engine and the solutions (every task)
91
+
92
+ 1. Check whether it is installed: `sensecraft-engine --version`. If the command
93
+ is not found, also try `~/.sensecraft/bin/sensecraft-engine --version`
94
+ (Windows: `%USERPROFILE%\.sensecraft\bin\sensecraft-engine.cmd`); the
95
+ installer puts it there and prints the `PATH` line to add.
96
+ 2. Before running an installer, check its local prerequisites. On Unix run
97
+ `command -v curl || command -v wget`, `command -v sha256sum || command -v shasum`,
98
+ `command -v tar`, `command -v jq`, and `command -v minisign`. If wget is the
99
+ selected downloader, also require `wget --help | grep -- --compression` to
100
+ succeed; otherwise stop because the CDN may return gzip content. On Windows
101
+ PowerShell run `Get-Command irm`, `Get-Command Invoke-WebRequest`,
102
+ `Get-Command Expand-Archive`, `Get-Command ConvertFrom-Json`, and
103
+ `Get-Command minisign`. Windows uses PowerShell/.NET ZIP and JSON support;
104
+ it does not require `tar` or `jq`. If any check fails, report the missing
105
+ command and stop; do not install packages or start another interpreter.
106
+ `minisign` is required to verify the signed release manifest; Unix `jq` is
107
+ required by the shell installer to read it.
108
+ 3. Not installed → install it together with the solutions (ask the user first,
109
+ it downloads a program):
110
+
111
+ **Recommended (Zero-friction via solution-cli, auto-detects existing app & agents)**:
112
+ ```bash
113
+ npx @seeed-studio/solution-cli install -y
114
+ ```
115
+
116
+ **Fallback (Standalone shell script when Node.js is not present)**:
117
+ ```bash
118
+ curl --compressed -fsSL https://sensecraft-statics.seeed.cc/solution-app/engine/install.sh | sh -s -- --with-content
119
+ ```
120
+
121
+ If `curl` is unavailable but `wget` passed the prerequisite check, use the
122
+ equivalent command:
123
+
124
+ ```
125
+ wget --compression=auto -qO- https://sensecraft-statics.seeed.cc/solution-app/engine/install.sh | sh -s -- --with-content
126
+ ```
127
+
128
+ Windows PowerShell:
129
+
130
+ ```
131
+ & ([scriptblock]::Create((irm https://sensecraft-statics.seeed.cc/solution-app/engine/install.ps1))) -WithContent
132
+ ```
133
+
134
+ 4. Solutions are downloaded separately from the engine:
135
+
136
+ | Command | Does |
137
+ |---|---|
138
+ | `sensecraft-engine content status --offline --json` | Reads the local content store only; an empty store is a normal result with `installed: 0` and exit code 0 |
139
+ | `sensecraft-engine content status --json` | Compares local solutions with the online list |
140
+ | `sensecraft-engine content pull` | Downloads every solution (first run), or only `--solution <id>` |
141
+ | `sensecraft-engine content update` | Re-downloads installed solutions that changed online |
142
+
143
+ Run the offline status check before deciding whether content is installed.
144
+ If it returns exit code 0 with `installed: 0`, run `content pull`; do not
145
+ treat that status result as an error. A solution query such as `solution
146
+ list` or `deploy-info` exits with code 3 when no usable resource/solution
147
+ directory is available and tells you to run `content pull`. If a usable
148
+ directory exists but the requested solution ID is unknown, the query exits
149
+ with code 2. After `content pull` / `update`, if an engine instance is
150
+ already running it keeps the solutions it loaded at start: run
151
+ `sensecraft-engine daemon stop` when no deployment is running.
152
+ 5. Device pre-checks on this computer: `sensecraft-engine doctor --json`. Each
153
+ check has `status` (`ok` / `warn` / `fail`) and a `fix` command. Show the
154
+ user any `fail` and its `fix`; running a `fix` changes this computer and
155
+ needs their OK.
156
+
157
+ Use `--lang zh` on commands that take `--lang` when the user writes in Chinese,
158
+ `--lang en` otherwise.
159
+
160
+ ## Exit codes (all commands)
161
+
162
+ | Code | Meaning | Next step |
163
+ |---|---|---|
164
+ | 0 | Success / accepted | Continue |
165
+ | 1 | Deployment failed, cancelled or interrupted; for `counting` / `face` / `wizard` / `robot`: the device did not confirm | `references/diagnose.md` |
166
+ | 2 | Invalid arguments or request. A `missing_params` error includes `missing[]`; an unknown ID or malformed JSON is also code 2 and may have no `missing[]` | Ask for each field only when `missing[]` is present; otherwise inspect the ID, JSON shape and parameter spelling, then correct or ask for the specific value |
167
+ | 3 | Environment or conflict: a solution query has no usable resource/solution directory (`content pull`), device busy with another deployment (409), engine instance shutting down or incompatible, serial port busy | Read the message; pull content or fix the environment, then run again. An empty `content status --offline` store is exit 0, not this case |
168
+ | 4 | This step needs the desktop app (deployer not in the engine) | Tell the user to deploy this solution from the SenseCraft Solution app |
169
+ | 5 | Internal engine error | Engine bug: `references/diagnose.md` §6 |
170
+
171
+ ## Pick the task
172
+
173
+ | The user wants to... | Read |
174
+ |---|---|
175
+ | Deploy a solution | `references/deploy.md` |
176
+ | Finish a step that reported `verify_required` (dashboards, counting lines, face enrollment, servo setup, robot arm) | `references/verify.md` |
177
+ | Find out why a deployment failed or a deployed solution misbehaves | `references/diagnose.md` |
178
+ | Deploy on a site without internet (prepare an offline package first) | `references/offline.md` |
179
+ | Change a solution's configuration, or use the desktop app's API | `references/app-only.md` (requires the desktop app) |
180
+
181
+ They connect: **a failed deployment goes straight to `diagnose.md`** without
182
+ stopping to ask; a `verify_required` event goes to `verify.md`; the retry goes
183
+ back to `deploy.md` for the failed device only.
@@ -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.