@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.
- package/LICENSE +15 -0
- package/README.md +230 -0
- package/assets/agent-guide.md +408 -0
- package/bin/miakapp.js +10 -0
- package/dist/agent-pack.d.ts +78 -0
- package/dist/agent-pack.d.ts.map +1 -0
- package/dist/agent-pack.js +193 -0
- package/dist/agent-pack.js.map +1 -0
- package/dist/artifact.d.ts +32 -0
- package/dist/artifact.d.ts.map +1 -0
- package/dist/artifact.js +328 -0
- package/dist/artifact.js.map +1 -0
- package/dist/control-plane.d.ts +37 -0
- package/dist/control-plane.d.ts.map +1 -0
- package/dist/control-plane.js +127 -0
- package/dist/control-plane.js.map +1 -0
- package/dist/discovery.d.ts +95 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +389 -0
- package/dist/discovery.js.map +1 -0
- package/dist/errors.d.ts +37 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +56 -0
- package/dist/errors.js.map +1 -0
- package/dist/internal/http.d.ts +35 -0
- package/dist/internal/http.d.ts.map +1 -0
- package/dist/internal/http.js +125 -0
- package/dist/internal/http.js.map +1 -0
- package/dist/internal/json.d.ts +15 -0
- package/dist/internal/json.d.ts.map +1 -0
- package/dist/internal/json.js +227 -0
- package/dist/internal/json.js.map +1 -0
- package/dist/internal/names.d.ts +45 -0
- package/dist/internal/names.d.ts.map +1 -0
- package/dist/internal/names.js +98 -0
- package/dist/internal/names.js.map +1 -0
- package/dist/internal/requirements.d.ts +10 -0
- package/dist/internal/requirements.d.ts.map +1 -0
- package/dist/internal/requirements.js +50 -0
- package/dist/internal/requirements.js.map +1 -0
- package/dist/internal/yaml.d.ts +26 -0
- package/dist/internal/yaml.d.ts.map +1 -0
- package/dist/internal/yaml.js +184 -0
- package/dist/internal/yaml.js.map +1 -0
- package/dist/main.d.ts +86 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +666 -0
- package/dist/main.js.map +1 -0
- package/dist/mcp.d.ts +93 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +524 -0
- package/dist/mcp.js.map +1 -0
- package/dist/project.d.ts +25 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +90 -0
- package/dist/project.js.map +1 -0
- package/dist/publication.d.ts +95 -0
- package/dist/publication.d.ts.map +1 -0
- package/dist/publication.js +408 -0
- package/dist/publication.js.map +1 -0
- 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
|
+
});
|