@miakapp/cli 4.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +230 -0
  3. package/assets/agent-guide.md +408 -0
  4. package/bin/miakapp.js +10 -0
  5. package/dist/agent-pack.d.ts +78 -0
  6. package/dist/agent-pack.d.ts.map +1 -0
  7. package/dist/agent-pack.js +193 -0
  8. package/dist/agent-pack.js.map +1 -0
  9. package/dist/artifact.d.ts +32 -0
  10. package/dist/artifact.d.ts.map +1 -0
  11. package/dist/artifact.js +328 -0
  12. package/dist/artifact.js.map +1 -0
  13. package/dist/control-plane.d.ts +37 -0
  14. package/dist/control-plane.d.ts.map +1 -0
  15. package/dist/control-plane.js +127 -0
  16. package/dist/control-plane.js.map +1 -0
  17. package/dist/discovery.d.ts +95 -0
  18. package/dist/discovery.d.ts.map +1 -0
  19. package/dist/discovery.js +389 -0
  20. package/dist/discovery.js.map +1 -0
  21. package/dist/errors.d.ts +37 -0
  22. package/dist/errors.d.ts.map +1 -0
  23. package/dist/errors.js +56 -0
  24. package/dist/errors.js.map +1 -0
  25. package/dist/internal/http.d.ts +35 -0
  26. package/dist/internal/http.d.ts.map +1 -0
  27. package/dist/internal/http.js +125 -0
  28. package/dist/internal/http.js.map +1 -0
  29. package/dist/internal/json.d.ts +15 -0
  30. package/dist/internal/json.d.ts.map +1 -0
  31. package/dist/internal/json.js +227 -0
  32. package/dist/internal/json.js.map +1 -0
  33. package/dist/internal/names.d.ts +45 -0
  34. package/dist/internal/names.d.ts.map +1 -0
  35. package/dist/internal/names.js +98 -0
  36. package/dist/internal/names.js.map +1 -0
  37. package/dist/internal/requirements.d.ts +10 -0
  38. package/dist/internal/requirements.d.ts.map +1 -0
  39. package/dist/internal/requirements.js +50 -0
  40. package/dist/internal/requirements.js.map +1 -0
  41. package/dist/internal/yaml.d.ts +26 -0
  42. package/dist/internal/yaml.d.ts.map +1 -0
  43. package/dist/internal/yaml.js +184 -0
  44. package/dist/internal/yaml.js.map +1 -0
  45. package/dist/main.d.ts +86 -0
  46. package/dist/main.d.ts.map +1 -0
  47. package/dist/main.js +666 -0
  48. package/dist/main.js.map +1 -0
  49. package/dist/mcp.d.ts +93 -0
  50. package/dist/mcp.d.ts.map +1 -0
  51. package/dist/mcp.js +524 -0
  52. package/dist/mcp.js.map +1 -0
  53. package/dist/project.d.ts +25 -0
  54. package/dist/project.d.ts.map +1 -0
  55. package/dist/project.js +90 -0
  56. package/dist/project.js.map +1 -0
  57. package/dist/publication.d.ts +95 -0
  58. package/dist/publication.d.ts.map +1 -0
  59. package/dist/publication.js +408 -0
  60. package/dist/publication.js.map +1 -0
  61. package/package.json +49 -0
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 Mathieu Colmon
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,230 @@
1
+ # @miakapp/cli
2
+
3
+ Build, validate, publish and roll back one Miakapp home component.
4
+
5
+ The CLI is the deployment mechanism, not the source of truth. **Git belongs to
6
+ you**: this tool never commits, never rewrites sources it did not generate and
7
+ never bundles for you. It reads a project file, verifies the artifact bytes you
8
+ built, and talks to the control plane over the closed RFC 0004 §13.2 surface.
9
+
10
+ It is written for a coding agent first and a person second. Every outcome is one
11
+ stable exit code and one stable failure kind, and `--json` prints exactly one
12
+ object on stdout.
13
+
14
+ ```bash
15
+ bunx @miakapp/cli init --home my-home --control-plane https://control.miakapp.app
16
+ bunx @miakapp/cli check
17
+ bunx @miakapp/cli publish --expected-generation 0
18
+ ```
19
+
20
+ ## The project file
21
+
22
+ `miakapp.yaml` sits at the root of your repository:
23
+
24
+ ```yaml
25
+ schema: miakapp.project/1
26
+ home: my-home
27
+ control_plane: https://control.miakapp.app
28
+
29
+ component:
30
+ artifact: dist/component.js
31
+ release: 2026-09-13.1
32
+ requires:
33
+ state_read:
34
+ - climate.living_room.temperature
35
+ event_subscribe: []
36
+ event_publish: []
37
+ call:
38
+ - lighting.set
39
+ presentation: []
40
+
41
+ coordinator:
42
+ entry: coordinator/main.ts
43
+ ```
44
+
45
+ `requires` is the closed RFC 0002 capability object. Lists are de-duplicated and
46
+ sorted before they are bound into an upload capability, so the value the CLI
47
+ sends is byte-identical on every later reconciliation read.
48
+
49
+ The parser is a deliberately small YAML subset: block mappings, block sequences
50
+ of scalars, two-space indentation, comments, quoted and plain scalars, and `[]`
51
+ for an empty list. Anything else — tabs, anchors, tags, multi-line scalars,
52
+ duplicate keys — is rejected with the offending line rather than guessed at.
53
+
54
+ ## Commands
55
+
56
+ | Command | What it does |
57
+ | --- | --- |
58
+ | `init` | Writes `miakapp.yaml`. Never overwrites an existing one. |
59
+ | `agent-pack` | Offline. Installs the guide and the MCP wiring into a repository. |
60
+ | `discover` | Offline. Inventories a Node-RED installation from its flows export. |
61
+ | `check` | Offline. Parses the project, verifies the artifact, prints the digest. |
62
+ | `publish` | Capability → delivery → finalization → activation, in one run. |
63
+ | `activate` | Activates an already finalized digest at a new generation. |
64
+ | `rollback` | Alias of `activate`, for returning to a known-good digest. |
65
+ | `release <sha256>` | Reads one finalized release record. |
66
+ | `upload <uploadId>` | Reads one upload status, to reconcile a lost request. |
67
+ | `mcp` | Serves every command above over MCP on stdio. |
68
+
69
+ `check` is the command to run in CI and before every publication. It costs
70
+ nothing, touches no network and catches the four artifact rules the broker's
71
+ pinned parser would reject anyway: module syntax, dynamic `import`, a source-map
72
+ directive and the ABI 1 token ceiling.
73
+
74
+ `discover` is the command to run *before* `init`, on a house that already exists:
75
+
76
+ ```
77
+ miakapp discover --flows ~/node-red/flows.json --json
78
+ ```
79
+
80
+ It needs no project file and no Home Key. It reads the bytes it was given —
81
+ opening no socket, contacting no broker, writing nothing back — and reports the
82
+ flows, the MQTT brokers with the topics their nodes actually reach, the v3
83
+ MiakAPI surface as V4 state and function candidates, and every node type it does
84
+ not model, so the reader knows what the inventory missed. It reports that a
85
+ coordinator secret is present in the export; it never prints the secret itself.
86
+ `docs/agent-guide.md` §3 explains what to do with each finding.
87
+
88
+ ## The agent pack
89
+
90
+ `agent-pack` is the command to run *once* in a home repository, so that the next
91
+ agent to open it arrives already knowing the rules:
92
+
93
+ ```
94
+ miakapp agent-pack # or --dir /path/to/the/repository
95
+ ```
96
+
97
+ It writes four files and reports what it did to each one:
98
+
99
+ | File | Why |
100
+ | --- | --- |
101
+ | `.miakapp/agent-guide.md` | The full guide, copied out of this package. No network, no stale bookmark. |
102
+ | `AGENTS.md` | The instruction file Codex reads. |
103
+ | `CLAUDE.md` | The instruction file Claude Code reads. |
104
+ | `.mcp.json` | Project-scope MCP configuration, registering `miakapp mcp`. |
105
+
106
+ The repository is yours, so the pack edits rather than replaces. The guide is a
107
+ file it owns outright. The instruction files are touched only between
108
+ `<!-- miakapp:begin -->` and `<!-- miakapp:end -->`: prose above and below the
109
+ markers is copied through byte for byte, and a second run rewrites the block in
110
+ place instead of appending another copy. `.mcp.json` is merged as a structure —
111
+ one key, by name — so every other server in it survives, and a file that does
112
+ not parse is refused rather than replaced with a valid one.
113
+
114
+ The server is registered as the bare `miakapp` command rather than an absolute
115
+ path, because the file is committed and the next machine to check it out will
116
+ not have this one's directory layout.
117
+
118
+ Run it again whenever the CLI is upgraded: an unchanged file is reported
119
+ `unchanged`, and a guide that moved on is reported `updated`.
120
+
121
+ ## MCP
122
+
123
+ An agent that already runs a shell does not need this. An agent that speaks the
124
+ Model Context Protocol natively does: `miakapp mcp` serves the same commands as
125
+ tools over newline-delimited JSON-RPC on stdio.
126
+
127
+ ```json
128
+ {
129
+ "mcpServers": {
130
+ "miakapp": {
131
+ "command": "bunx",
132
+ "args": ["@miakapp/cli", "mcp"],
133
+ "env": { "MIAKAPP_HOME_KEY": "${MIAKAPP_HOME_KEY}" }
134
+ }
135
+ }
136
+ }
137
+ ```
138
+
139
+ | Tool | Command | |
140
+ | --- | --- | --- |
141
+ | `miakapp_discover` | `discover` | read-only, offline |
142
+ | `miakapp_check` | `check` | read-only, offline |
143
+ | `miakapp_release` | `release` | read-only |
144
+ | `miakapp_upload` | `upload` | read-only |
145
+ | `miakapp_init` | `init` | writes `miakapp.yaml`, never overwrites |
146
+ | `miakapp_agent_pack` | `agent-pack` | offline, writes the pack into a repository |
147
+ | `miakapp_publish` | `publish` | **moves the pointer — needs `confirm: true`** |
148
+ | `miakapp_activate` | `activate` | **moves the pointer — needs `confirm: true`** |
149
+ | `miakapp_rollback` | `rollback` | **moves the pointer — needs `confirm: true`** |
150
+
151
+ The server is a translation layer: a tool call becomes the exact argv a person
152
+ would have typed and runs the same dispatch, so a tool and a command line cannot
153
+ drift apart. A tool argument is the option name with `_` for `-`
154
+ (`expected_generation` → `--expected-generation`); an argument the tool does not
155
+ declare is refused rather than ignored.
156
+
157
+ The three pointer-moving tools additionally require `confirm: true`. It is
158
+ checked before anything else and never reaches the command line, so a model that
159
+ hallucinated a publication spends the mistake on an argument check instead of on
160
+ a generation.
161
+
162
+ A command that fails comes back as a tool result carrying `isError: true` and
163
+ the same closed object the CLI prints — `kind`, `exit_code`, `message` and a
164
+ remedy — not as a JSON-RPC error. That distinction matters: a protocol error
165
+ means the call never happened, while a publication that reached the control
166
+ plane and failed did happen, and only `kind` says whether to reconcile.
167
+
168
+ ## Authorization
169
+
170
+ The Home Key is read from `MIAKAPP_HOME_KEY` and from nowhere else. No command
171
+ accepts it as an argument, because an argument lands in shell history, in a
172
+ process listing and in most CI logs.
173
+
174
+ ```bash
175
+ export MIAKAPP_HOME_KEY="$(op read op://home/miakapp/home-key)" # or your own vault
176
+ bunx @miakapp/cli publish --expected-generation 4
177
+ ```
178
+
179
+ The key is exchanged for a five-minute `components:publish` token before every
180
+ run. Publication endpoints never accept the Home Key itself, and the CLI never
181
+ prints either credential.
182
+
183
+ ## Generations
184
+
185
+ Activation is a compare-and-set: `--expected-generation` is the generation you
186
+ believe the pointer holds, and `--generation` (default: expected + 1) is the one
187
+ you are publishing. A stale expectation fails with `generation_conflict` and
188
+ exit code 6 rather than last-write-wins.
189
+
190
+ `--expected-generation` is required rather than discovered, because RFC 0004
191
+ §13.2 publishes no read for the current pointer: the pointer lives in
192
+ `components/{homeID}` and reaches clients as authenticated platform data. Until
193
+ that surface exists, the CLI asks you for the number instead of guessing one —
194
+ inventing an endpoint would be worse than an explicit flag.
195
+
196
+ ## Exit codes
197
+
198
+ | Code | Kind | Meaning |
199
+ | --- | --- | --- |
200
+ | 0 | — | success |
201
+ | 1 | `usage` | the invocation is malformed |
202
+ | 2 | `project` | `miakapp.yaml` is missing or invalid |
203
+ | 3 | `artifact` | the artifact is missing, malformed or not publishable |
204
+ | 4 | `authorization` | no Home Key, or the control plane refused it |
205
+ | 5 | `contract` | the control plane answered outside its own schema |
206
+ | 6 | `conflict` | the generation precondition failed |
207
+ | 7 | `unknown_outcome` | the effect on the control plane is undetermined |
208
+
209
+ Code 7 is the one that matters. It means a mutating request may already have
210
+ committed, so the next step is a read — `miakapp upload <id>` or
211
+ `miakapp release <sha256>` — and never a blind retry with a fresh capability.
212
+
213
+ ## Failure output
214
+
215
+ ```console
216
+ $ miakapp publish --expected-generation 0
217
+ miakapp: conflict: Activation failed with HTTP 409 generation_conflict (request Zq1...)
218
+ Another publication advanced the pointer. Read the active generation and retry
219
+ the activation with the observed expected_generation.
220
+ ```
221
+
222
+ ```console
223
+ $ miakapp publish --expected-generation 0 --json
224
+ {"ok":false,"kind":"conflict","exit_code":6,"message":"...","remedy":"..."}
225
+ ```
226
+
227
+ ## Status
228
+
229
+ Alpha, tracking Miakapp 4. The package is `private` until the control plane it
230
+ talks to is deployed; publishing it to npm is a deliberate, separate step.
@@ -0,0 +1,408 @@
1
+ # Building a Miakapp home
2
+
3
+ This guide is written for a coding agent — Claude Code, Codex, or any successor —
4
+ that has been handed someone's house and asked to make it work. You are expected
5
+ to read the existing installation, write a coordinator, write an interface, test
6
+ both, and publish. The owner is not a home-automation developer and should not
7
+ have to become one.
8
+
9
+ Everything below is true of the code in this repository. Where a rule exists for
10
+ a reason that is not obvious, the reason is given, because a rule whose purpose
11
+ you cannot see is one you will optimize away.
12
+
13
+ ## 1. What you are building
14
+
15
+ Three artifacts, and no more:
16
+
17
+ | Artifact | Where it runs | What it owns |
18
+ | --- | --- | --- |
19
+ | Coordinator | the owner's machine, under Bun | state, events, functions, **authorization** |
20
+ | Component | a sandboxed Worker in the household's browser | the interface |
21
+ | `miakapp.yaml` | neither; it is the contract | what the component may ask for |
22
+
23
+ The coordinator is trusted. The component is not. The relay between them is
24
+ platform-untrusted but not blind: it terminates TLS, stores plaintext state and
25
+ enforces routing, so self-hosting it does not give end-to-end confidentiality.
26
+ Do not tell the owner otherwise.
27
+
28
+ Start from `templates/home`. It is a complete working home — one lamp, one
29
+ temperature — with the three files already in the right relationship, and its
30
+ own `README.md` covering the mechanics of copying it out. This guide covers the
31
+ judgment the template cannot.
32
+
33
+ ### Leave the repository readable by the next agent
34
+
35
+ You are probably not the last agent to open this repository, and the next one
36
+ will not have this conversation. Run the pack once, in the repository root:
37
+
38
+ ```bash
39
+ miakapp agent-pack
40
+ ```
41
+
42
+ It copies this guide to `.miakapp/agent-guide.md`, points `AGENTS.md` and
43
+ `CLAUDE.md` at it, and registers `miakapp mcp` in `.mcp.json` so the tools are
44
+ wired rather than described. It edits instead of replacing: prose outside the
45
+ `<!-- miakapp:begin -->` markers is kept, other MCP servers are kept by name,
46
+ and running it twice changes nothing.
47
+
48
+ If you are reading this file *as* `.miakapp/agent-guide.md`, someone already
49
+ ran it. Run it again after upgrading the CLI, and commit what changes — a guide
50
+ that contradicts the CLI installed beside it is worse than no guide.
51
+
52
+ ## 2. The division of responsibility
53
+
54
+ **The coordinator authorizes everything.** The relay proves *who* is calling and
55
+ attaches non-spoofable caller metadata. Deciding whether that person *may* act is
56
+ the coordinator's job and nobody else's. In the template that decision is the
57
+ first line of the function body:
58
+
59
+ ```ts
60
+ if (call.source.kind !== 'user' || call.source.id !== options.ownerUserId) {
61
+ throw new ApplicationCallError(2003, 'Only the owner may drive the lights');
62
+ }
63
+ ```
64
+
65
+ Removing that check does not produce an error anywhere. It produces a home that
66
+ anyone enrolled can drive. There is no second layer that will catch it for you.
67
+
68
+ **The component trusts nothing it was not given.** It has no network, no storage
69
+ and no DOM. It cannot reach the lamp except through a call the coordinator
70
+ declared, and it cannot read a state path the coordinator did not grant. This is
71
+ why it is safe to let a component be rewritten often and reviewed lightly, and
72
+ why it is not safe to move a decision into it.
73
+
74
+ **You own the UI; the coordinator owns the facts.** The coordinator exposes state
75
+ and actions. What the household actually sees — the layout, the wording, the
76
+ language, which controls are prominent — is yours to design from the semantic
77
+ vocabulary in §5. Do not push presentation choices into the coordinator, and do
78
+ not push authorization into the component.
79
+
80
+ ## 3. Before you write anything: characterize the house
81
+
82
+ You are almost never starting from an empty building. Read what is already there
83
+ before you design anything:
84
+
85
+ - existing hubs and brokers — Node-RED, MQTT, Zigbee/Z-Wave coordinators, an
86
+ HTTP-speaking hub;
87
+ - what each device actually reports, and how often;
88
+ - which values are *measurements* (temperature, power) and which are
89
+ *commanded* (a lamp, a valve) — they have different failure modes;
90
+ - which actions are physically consequential: anything that heats, locks,
91
+ unlocks, opens or closes.
92
+
93
+ Write down what you found before you write the configuration. The state paths you
94
+ choose become a disclosure boundary and a public interface at the same time, and
95
+ renaming one after the household has used it is not free.
96
+
97
+ ### Reading a V3 house you inherited
98
+
99
+ Most houses arriving at V4 already run Node-RED with the v3 MiakAPI nodes. Ask
100
+ the owner for the `flows.json` Node-RED writes, or for an *Export > All flows*
101
+ download, and read it before you read anything else:
102
+
103
+ ```
104
+ miakapp discover --flows ~/node-red/flows.json
105
+ miakapp discover --flows ~/node-red/flows.json --json
106
+ ```
107
+
108
+ The command is offline and read-only: it opens no socket, contacts no broker and
109
+ never writes back into the export. It reports the tabs, the MQTT brokers with the
110
+ topics their nodes actually reach, the `initMiakapi` home bindings, every
111
+ `commitVariables` path as a state candidate, every `onUserAction` id as a
112
+ function candidate with the groups allowed to invoke it, and every node type it
113
+ does not model — so you know what the inventory missed rather than assuming it
114
+ missed nothing.
115
+
116
+ Four of its findings decide work you would otherwise discover late:
117
+
118
+ - **`secret_in_export`.** The v3 `initMiakapi` node declares `coordSecret` in its
119
+ `defaults`, not in its `credentials`, so Node-RED stores that secret in
120
+ cleartext in `flows.json` rather than in the encrypted `flows_cred.json`. If
121
+ the export has one, treat it as leaked: rotate it, and keep the file out of
122
+ Git. §9 is the V4 rule that replaces it.
123
+ - **`unrestricted_action`.** The v3 handler allows an action outright when its
124
+ node lists no group, so an empty `allowedGroups` is a grant to every signed-in
125
+ user, not a deny. Each one needs a deliberate V4 rule before you port it.
126
+ - **`name_needs_rename`.** A v3 variable path or action id that is not a legal V4
127
+ dotted name has to be renamed now, while nobody depends on it.
128
+ - **`wildcard_subscription`.** A topic holding `#` or `+` is a subscription
129
+ pattern, not one device. Enumerate what it actually matches.
130
+
131
+ The command deliberately does not tell you which actions are physically
132
+ consequential. It lists every action it found; deciding which of them heats,
133
+ locks, unlocks, opens or closes is a judgement you make with the owner, and no
134
+ keyword list should make it for you.
135
+
136
+ ## 4. The coordinator
137
+
138
+ `templates/home/coordinator/home.ts` is the shape to copy: the configuration is a
139
+ **pure function of its options**, so it can be tested without a relay, a control
140
+ plane or a network. `coordinator/main.ts` is the only file that touches the
141
+ outside world. Keep that split. It is what makes `test/home.test.ts` possible,
142
+ and the authorization rules are exactly the thing you want under test.
143
+
144
+ Four declarations:
145
+
146
+ ```ts
147
+ {
148
+ state: { 'zone.salon.light.on': false }, // paths and initial values
149
+ stateAccess: [{ userId, patterns: ['zone.salon.*'] }],
150
+ events: [{ topic, directions: EventDirection.publishToUsers }],
151
+ eventAccess: [{ userId, publish: [], subscribe: [topic] }],
152
+ functions: { async 'lighting.set'(call) { /* authorize, act, return */ } },
153
+ }
154
+ ```
155
+
156
+ `stateAccess[].patterns` is the disclosure boundary. A user sees exactly those
157
+ paths and nothing else. Widen it deliberately, one path at a time, and never with
158
+ a wildcard that happens to be convenient.
159
+
160
+ Three ordering and failure rules that are easy to get wrong:
161
+
162
+ **State first, event second.** Write the state, *then* publish the event:
163
+
164
+ ```ts
165
+ await coordinator.state.set([{ path: STATE.lightOn, value: on }]);
166
+ await coordinator.events.publish(EVENT_LIGHT_CHANGED, { on });
167
+ ```
168
+
169
+ A subscriber that reacts to the event and immediately reads the state must never
170
+ see the old value. The reverse order is a race that will reproduce once a month
171
+ and waste a day.
172
+
173
+ **Reject bad arguments with a typed application error.** `ApplicationCallError`
174
+ carries a numeric code the component can branch on. Do not throw a bare `Error`
175
+ for a caller mistake; the distinction between "you asked wrongly" and "something
176
+ broke" is the one the interface needs most.
177
+
178
+ **A home has several coordinators.** Names are namespace sharding by convention.
179
+ The relay keeps ownership tables for topics, state paths and functions, detects
180
+ collisions and rejects with `4409`. If you claim `lighting.set` for the whole
181
+ house, you have taken a name another integration may need; scope what you own.
182
+
183
+ ## 5. The component
184
+
185
+ Import from `@miakapp/component`. The whole public surface is one module, and the
186
+ whole rendering vocabulary is `ui.*`:
187
+
188
+ ```
189
+ screen stack grid section layout
190
+ text status progress media output
191
+ button toggle input select interaction
192
+ ```
193
+
194
+ You return one complete semantic tree per render; the trusted host draws it with
195
+ its own components. You do not ship CSS, you do not ship a DOM, and you cannot
196
+ style your way around the host. This is a constraint worth accepting rather than
197
+ fighting: it is what lets the host stay accessible, themed and localized without
198
+ auditing your code.
199
+
200
+ Handlers are functions, not identifiers to wire up by hand:
201
+
202
+ ```ts
203
+ ui.toggle({
204
+ id: 'salon-light',
205
+ label: 'Lampe du salon',
206
+ value: asBoolean(home.state.get(LIGHT_ON)),
207
+ disabled: home.staging || !healthy,
208
+ pending,
209
+ onChange: (next) => void setLight(next),
210
+ })
211
+ ```
212
+
213
+ Handlers are registered for the render that created them, so a stale tree cannot
214
+ fire an action against new state.
215
+
216
+ The limits are real and enforced: 1 024 nodes, depth 32, 30 renders per second,
217
+ 32 outstanding calls, 8 KiB per text node. If you are approaching any of them you
218
+ are building a dashboard the household will not read.
219
+
220
+ ### Two states the interface must never hide
221
+
222
+ **Staleness.** `home.state.stale` means the snapshot may no longer reflect the
223
+ house. Show it. Per RFC 0002 §12.2 it is exposed, never hidden — a thermostat
224
+ reading that is silently forty minutes old is worse than one labelled uncertain.
225
+
226
+ **Staging.** `home.staging` is true while a release is staged: rendering works,
227
+ calls and events do not. Disable the controls and say why, as the template does.
228
+ An interface that looks live and silently does nothing is the worst outcome
229
+ available.
230
+
231
+ ### An unknown outcome is never retried
232
+
233
+ This is the rule that will most tempt you to break it, so it is stated plainly:
234
+
235
+ ```ts
236
+ try {
237
+ await home.call('lighting.set', { on }, { deadlineMs: 10_000 });
238
+ } catch (error) {
239
+ // Deliberately not retried. The call may already have reached the lamp,
240
+ // and the next state snapshot settles the question.
241
+ failure = error instanceof Error ? error.message : 'La commande a échoué';
242
+ }
243
+ ```
244
+
245
+ A failed call is not a call that did not happen. `CallOutcomeUnknownError` exists
246
+ precisely to name the case where the effect is undetermined, and the physical
247
+ world does not have a rollback. Surface it, let the next state snapshot settle
248
+ it, and let the person decide. The same principle has a CLI counterpart: exit
249
+ code 7.
250
+
251
+ ## 6. The intersection rule
252
+
253
+ Every name a component may touch appears in **two** places, and the effective
254
+ grant is the intersection:
255
+
256
+ | `miakapp.yaml` → `requires` | `coordinator/home.ts` |
257
+ | --- | --- |
258
+ | `state_read` | `stateAccess[].patterns` |
259
+ | `event_subscribe` | `eventAccess[].subscribe` |
260
+ | `event_publish` | `eventAccess[].publish` |
261
+ | `call` | `functions` |
262
+
263
+ Asking for more than the coordinator grants **does not fail loudly at
264
+ publication**. The component simply never receives that path, and the interface
265
+ renders a hole — an empty card, a control that does nothing, a temperature that
266
+ is permanently unavailable. This is the single most common way a Miakapp home
267
+ breaks, and it breaks quietly.
268
+
269
+ So: assert the correspondence in a test. `templates/home/test/home.test.ts` does
270
+ exactly this, and it is the reason a mismatch fails in CI rather than in
271
+ someone's living room. When you add a path, change four things in one commit —
272
+ the state declaration, the access pattern, the `requires` entry, and the test.
273
+
274
+ ## 7. The loop
275
+
276
+ ```bash
277
+ bun run check # typecheck → bundle → test → validate the artifact offline
278
+ ```
279
+
280
+ Run it before every publication and in CI. It costs nothing and catches the
281
+ artifact rules the runtime would reject anyway.
282
+
283
+ Do not treat a check that exists as a check that runs. On 2026-09-14 this
284
+ repository had a template and a component example that were both verified
285
+ locally and built by nobody, because the workflow called `bun run check` and not
286
+ `check:packages`; when CI finally exercised them they failed three times in a
287
+ row on defects that had been sitting there invisibly. If you add a package, add
288
+ it to the job that runs in CI, then watch one run go green before believing it.
289
+
290
+ Reproduce CI with the toolchain version it pins, not the one you have. Bun 1.4
291
+ self-references the root package and resolves `bunx` from `node_modules`; the
292
+ pinned 1.2.23 does neither, so a green local run proves nothing about the runner.
293
+ Install the pinned version alongside yours:
294
+
295
+ ```bash
296
+ BUN_INSTALL=/tmp/bun1223 curl -fsSL https://bun.sh/install | bash -s bun-v1.2.23
297
+ ```
298
+
299
+ Note also that `bun test foo/` is a **substring filter**, not a directory scope.
300
+
301
+ ## 8. Publishing
302
+
303
+ The CLI builds, validates, publishes and rolls back. **It never owns Git.** It
304
+ writes no history, rewrites no source it did not generate, and invents no
305
+ control-plane endpoint. The repository is the owner's.
306
+
307
+ ```bash
308
+ miakapp agent-pack # once, per repository
309
+ miakapp init --home <homeId> --control-plane <https url>
310
+ miakapp check
311
+ miakapp publish --expected-generation <n>
312
+ miakapp activate --sha256 <digest> --expected-generation <n>
313
+ miakapp rollback --sha256 <digest> --expected-generation <n> # alias of activate
314
+ miakapp release <sha256> # read one finalized release
315
+ miakapp upload <uploadId> # reconcile a lost request
316
+ ```
317
+
318
+ `--expected-generation` is a compare-and-swap on the home's component pointer: the
319
+ generation you believe it currently holds. It is `0` for a home that has never
320
+ published. It is required, and it is what stops two agents from silently
321
+ overwriting each other.
322
+
323
+ Rollback is `activate` pointed at a digest you already trust. Keep the digest of
324
+ every release you shipped; a rollback you can perform in one command is worth
325
+ more than an incident you can explain.
326
+
327
+ ### Driving the CLI as a program
328
+
329
+ Every failure maps to exactly one stable exit code and one stable
330
+ machine-readable kind. New kinds may be added; existing codes never change
331
+ meaning. Pass `--json` and you get exactly one closed object on stdout.
332
+
333
+ | Code | Kind | Meaning |
334
+ | --- | --- | --- |
335
+ | 0 | `success` | |
336
+ | 1 | `usage` | the invocation was wrong |
337
+ | 2 | `project` | `miakapp.yaml` is missing or invalid |
338
+ | 3 | `artifact` | the built bytes violate an artifact rule |
339
+ | 4 | `authorization` | the Home Key is missing, wrong or unscoped |
340
+ | 5 | `contract` | the control plane rejected the request |
341
+ | 6 | `conflict` | `--expected-generation` did not match; re-read, do not retry |
342
+ | 7 | `unknown_outcome` | **effect undetermined — reconcile with a read** |
343
+
344
+ Branch on the code, not on the prose. On 7, call `miakapp upload <uploadId>` or
345
+ `miakapp release <sha256>` and reconcile before acting again. Never retry a 7
346
+ with a fresh capability.
347
+
348
+ ### If you speak MCP instead of shell
349
+
350
+ `miakapp mcp` serves the same commands as tools over JSON-RPC on stdio. It is the
351
+ same code: a tool call becomes the argv a person would have typed and runs the
352
+ same dispatch, so everything above still holds — the same defaults, the same
353
+ validation, the same `kind` on every failure.
354
+
355
+ Three differences are worth knowing before you call anything:
356
+
357
+ - `miakapp_publish`, `miakapp_activate` and `miakapp_rollback` refuse to run
358
+ without `confirm: true`. Set it when the owner asked for that publication, and
359
+ not to get past an error;
360
+ - a failure arrives as a tool result with `isError: true`, carrying the same
361
+ closed object, not as a JSON-RPC error. A JSON-RPC error means your call never
362
+ happened; `isError` means it ran and failed, and `kind` says what to do next;
363
+ - a tool argument is the option name with `_` instead of `-`. An argument the
364
+ tool does not declare is refused, never ignored.
365
+
366
+ `packages/cli/README.md` lists the tools. The exit codes above are the
367
+ `exit_code` field in every result, so branch on the same table either way.
368
+
369
+ ## 9. Secrets
370
+
371
+ `MIAKAPP_HOME_KEY` comes from the environment. It is never a command-line
372
+ argument, never printed, never written into a project file. No command accepts
373
+ one, deliberately: an argument lands in shell history, in a process listing and
374
+ in most CI logs.
375
+
376
+ ```bash
377
+ export MIAKAPP_HOME_KEY="$(your-secret-manager read miakapp/home-key)"
378
+ ```
379
+
380
+ If you are about to write a secret into `miakapp.yaml` so something works, stop:
381
+ that is the failure this design exists to prevent.
382
+
383
+ ## 10. Before you tell the owner you are done
384
+
385
+ - [ ] Every physically consequential function authorizes its caller on its first
386
+ line, and a test proves an unauthorized caller is refused.
387
+ - [ ] Every name in `miakapp.yaml` is covered by the coordinator, and a test
388
+ asserts the correspondence.
389
+ - [ ] `home.state.stale` and `home.staging` are visible in the interface.
390
+ - [ ] No call path retries an unknown outcome.
391
+ - [ ] State is written before its event is published.
392
+ - [ ] `bun run check` is green, and CI ran it — not just you.
393
+ - [ ] The Home Key exists only in the environment.
394
+ - [ ] You recorded the digest of what you published, so rollback is one command.
395
+
396
+ ## Where to read further
397
+
398
+ - `packages/cli/README.md` — every command and every tool, including what the
399
+ pack writes and what it refuses to overwrite.
400
+ - `templates/home/README.md` — the mechanics of the template itself.
401
+ - `docs/rfcs/0001` (Miakapp-V3) — wire protocol: ownership, collisions, `4409`.
402
+ - `docs/rfcs/0002` — component runtime and the staleness rule. **The broker is
403
+ the authority for the guest ABI, not the RFC**: `component-runtime/src/runtime-broker.ts`
404
+ and `contract.ts` define the exact payloads, and the broker terminates the
405
+ instance rather than answering when a field is wrong.
406
+ - `docs/rfcs/0003` — the coordinator SDK surface.
407
+ - `docs/rfcs/0004` — Home Key bootstrap, scopes, publication.
408
+ - `docs/rfcs/0005` — the trusted browser client.
package/bin/miakapp.js ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { run } from '../dist/main.js';
3
+
4
+ process.exitCode = await run(process.argv.slice(2), {
5
+ write: (text) => void process.stdout.write(text),
6
+ writeError: (text) => void process.stderr.write(text),
7
+ cwd: () => process.cwd(),
8
+ env: (name) => process.env[name],
9
+ input: process.stdin,
10
+ });