@junoflow/cli 0.3.0 → 0.9.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 (59) hide show
  1. package/README.md +693 -52
  2. package/app-client.js +306 -0
  3. package/bin/juno.js +7 -0
  4. package/eslint.app.config.js +34 -0
  5. package/eslint.flow.config.js +35 -0
  6. package/package.json +12 -4
  7. package/src/api.js +541 -30
  8. package/src/app-dev/build.js +134 -0
  9. package/src/app-dev/harness.js +108 -0
  10. package/src/app-dev/panel.html +336 -0
  11. package/src/app-dev/panel.js +290 -0
  12. package/src/app-dev/server.js +347 -0
  13. package/src/app-dev/stream.js +224 -0
  14. package/src/app-dev/watch.js +47 -0
  15. package/src/app-project.js +133 -0
  16. package/src/authoring-skills.js +43 -0
  17. package/src/commands/agent-skill-install.js +64 -0
  18. package/src/commands/app-check.js +75 -0
  19. package/src/commands/app-deploy.js +102 -0
  20. package/src/commands/app-dev.js +141 -0
  21. package/src/commands/app-gen-types.js +37 -0
  22. package/src/commands/app-init.js +46 -0
  23. package/src/commands/app-pull.js +106 -0
  24. package/src/commands/ask.js +60 -0
  25. package/src/commands/check.js +71 -27
  26. package/src/commands/config.js +26 -0
  27. package/src/commands/credential-set.js +68 -0
  28. package/src/commands/deploy.js +95 -51
  29. package/src/commands/flow-check.js +75 -0
  30. package/src/commands/flow-deploy.js +110 -0
  31. package/src/commands/flow-fixture.js +67 -0
  32. package/src/commands/flow-gen-types.js +37 -0
  33. package/src/commands/flow-init.js +59 -0
  34. package/src/commands/flow-pull.js +115 -0
  35. package/src/commands/{types.js → gen-types.js} +12 -5
  36. package/src/commands/init.js +28 -16
  37. package/src/commands/pull.js +111 -63
  38. package/src/commands/session-approve.js +73 -0
  39. package/src/commands/skill.js +73 -0
  40. package/src/commands/tool.js +249 -0
  41. package/src/commands/verify-types.js +4 -2
  42. package/src/config.js +34 -0
  43. package/src/diagnostics.js +14 -0
  44. package/src/errors.js +40 -0
  45. package/src/exit.js +22 -0
  46. package/src/flow-project.js +78 -0
  47. package/src/help.js +199 -0
  48. package/src/index.js +567 -75
  49. package/src/installed-skill.js +55 -0
  50. package/src/project.js +76 -8
  51. package/src/prompt.js +196 -0
  52. package/src/stamp.js +10 -9
  53. package/src/suggest.js +67 -0
  54. package/src/target.js +22 -7
  55. package/src/verbs.js +34 -0
  56. package/src/watch.js +87 -0
  57. package/src/working-copy.js +262 -0
  58. package/tsconfig.page.json +15 -0
  59. package/verbs.json +276 -0
package/README.md CHANGED
@@ -1,52 +1,691 @@
1
1
  # @junoflow/cli
2
2
 
3
- Develop [Juno](https://github.com/boivie/juno) integrations locally: scaffold a
4
- project, generate types from your manifest, check it, and deploy it.
3
+ Develop [Juno](https://github.com/boivie/juno) flows and integrations, and
4
+ operate a Juno instance, from the command line.
5
5
 
6
6
  ```bash
7
- npm install --save-dev @junoflow/cli
8
- npx juno init my-integration --url http://juno.local:8080
7
+ npm install --save-dev @junoflow/cli # in a flow or integration working copy
8
+ npm install --global @junoflow/cli # to operate an instance from anywhere
9
9
  ```
10
10
 
11
+ The package installs one command, `juno`, with two sets of commands:
12
+
13
+ - **Operating an instance**: `juno <group> <verb> [id]` puts a named command in
14
+ front of every tool Juno's in-app assistant uses, so a coding agent (Claude
15
+ Code and similar), a shell script or a person can find things and act on them.
16
+ `juno ask` talks to the assistant itself.
17
+ - **Working copies**: `juno flow init`, `juno integration init` and the commands
18
+ beside them write a flow or an integration to disk as files, check it, and
19
+ deploy it. See [Working copies](#working-copies).
20
+
21
+ Juno has **no authentication on any endpoint**. An instance is a trusted-LAN
22
+ appliance; do not expose one to the internet.
23
+
24
+ ## Operating an instance
25
+
26
+ ```bash
27
+ juno config set url juno.local:8080 # once, outside a project
28
+ juno discover porch --kind state
29
+ juno state get porch-dark --jq .value
30
+ juno related endpoint:ha:light.hallway --degree 2
31
+ juno state preview-layers porch-dark --input - <<'EOF'
32
+ {"ops":[{"op":"insert", …}]}
33
+ EOF
34
+ juno ha call-service --input '{"domain":"light","service":"turn_off"}'
35
+ ```
36
+
37
+ The grammar is `juno <group> <verb> [id]`. The group is the kind of thing a
38
+ command acts on, so a `discover` hit's `{kind, id}` leads straight to
39
+ `juno <kind> <verb> <id>` and `juno related <kind>:<id>` (or `<kind> <id>`). A
40
+ verb may also be spelled with `_` (`juno state get_history`).
41
+
42
+ Every tool command sends the tool's own JSON input, and prints the tool's JSON
43
+ answer. The CLI holds no tool semantics: the instance validates the input,
44
+ writes the descriptions and classifies each command's effect.
45
+
46
+ ### Help
47
+
48
+ | Command | Shows |
49
+ | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
50
+ | `juno --help` | the groups and top-level commands. Offline. |
51
+ | `juno <group> --help` | the group's commands, each with its effect and the first sentence of its description. Offline, it lists the commands alone. |
52
+ | `juno <group> <verb> --help` | the tool's full description, its effect, its input schema, and what the positional and flags fill in. Needs the instance. |
53
+
54
+ `juno <group>` on its own prints the group's help and exits 2. An unknown
55
+ command suggests the closest one: `juno stat get` answers
56
+ `did you mean "juno state get"?`.
57
+
58
+ ### Effects
59
+
60
+ Every command has one effect:
61
+
62
+ - **read** changes nothing.
63
+ - **change** writes stored configuration that keeps a version timeline, so it
64
+ can be reverted.
65
+ - **act** does something now (calls a service, presses a button, sets a value,
66
+ starts a run, executes code), with nothing to roll back to.
67
+
68
+ `config set` and `agent-skill install` are **local**: they write a file on your
69
+ machine and change nothing on the instance. So is `app dev`, which serves an app
70
+ working copy on localhost and never connects to an instance.
71
+
72
+ No command of a lower effect is a string prefix of a command of a higher one.
73
+ Claude Code matches a Bash permission rule by prefix, so an allowlist can list
74
+ the read commands, and optionally the change commands, and every act still
75
+ prompts.
76
+
77
+ ### Commands
78
+
79
+ The positional fills the named input field. A dash means the command takes no
80
+ positional; its input is a whole document (`device write`) or has no id
81
+ (`run list`).
82
+
83
+ **Top level**
84
+
85
+ | Command | Tool | Positional | Effect |
86
+ | --------------------------------------- | ------------- | ----------------------------------- | ------ |
87
+ | `juno discover <query> [--kind k]…` | `discover` | `query`; `--kind` → `kinds` | read |
88
+ | `juno related <kind>:<id> [--degree n]` | `get_related` | `kind`, `id`; `--degree` → `degree` | read |
89
+ | `juno skill [<name>] [--json]` | none | skill name | read |
90
+ | `juno ask ["<prompt>"]` | none | the prompt | act |
91
+ | `juno agent-skill install` | none | none | local |
92
+
93
+ `related` splits its positional on the first `:` only, so
94
+ `endpoint:ha:light.hallway` is kind `endpoint`, id `ha:light.hallway`.
95
+
96
+ **`state`**
97
+
98
+ | Command | Tool | Positional | Effect |
99
+ | --------------------------- | ------------------------------------- | ---------- | ------ |
100
+ | `juno state get` | `get_state` | `id` | read |
101
+ | `juno state get-history` | `get_state_history` | `id` | read |
102
+ | `juno state get-layers` | `get_state_layers` | `id` | read |
103
+ | `juno state evaluate-level` | `evaluate_level` | — | read |
104
+ | `juno state preview-layers` | `edit_state_layers`, `dry_run: true` | `id` | read |
105
+ | `juno state create` | `create_state` | `id` | change |
106
+ | `juno state edit-layers` | `edit_state_layers`, `dry_run: false` | `id` | change |
107
+ | `juno state set-value` | `set_state_value` | `id` | act |
108
+ | `juno state claim` | `claim_state` | `id` | act |
109
+ | `juno state release` | `release_state` | `id` | act |
110
+
111
+ `preview-layers` and `edit-layers` call the same tool. The preview always sends
112
+ `dry_run: true`, so a permission rule can allow the preview and not the edit; an
113
+ `--input` that says `"dry_run": false` is refused.
114
+
115
+ **`flow`**
116
+
117
+ | Command | Tool | Positional | Effect |
118
+ | ------------------------ | ------------------ | ---------- | ------ |
119
+ | `juno flow get` | `get_flow` | `id` | read |
120
+ | `juno flow set-enabled` | `set_flow_enabled` | `flow_id` | change |
121
+ | `juno flow trigger` | `trigger_flow` | `flow_id` | act |
122
+ | `juno flow fire-trigger` | `fire_trigger` | `flow_id` | act |
123
+
124
+ The group's working-copy commands (`flow init`, `gen-types`, `check`, `deploy`,
125
+ `pull`) are in [Flows as files](#flows-as-files).
126
+
127
+ **`run`**
128
+
129
+ | Command | Tool | Positional | Effect |
130
+ | --------------- | ----------- | ---------- | ------ |
131
+ | `juno run list` | `list_runs` | — | read |
132
+ | `juno run get` | `get_run` | `run_id` | read |
133
+
134
+ **`integration`**
135
+
136
+ | Command | Tool | Positional | Effect |
137
+ | ------------------------------- | -------------------------- | ------------- | ------ |
138
+ | `juno integration get` | `get_integration` | `id` | read |
139
+ | `juno integration configure` | `configure_integration` | `id` | change |
140
+ | `juno integration install` | `install_integration` | — | act |
141
+ | `juno integration run-schedule` | `run_integration_schedule` | `integration` | act |
142
+
143
+ The group's working-copy commands (`integration init`, `gen-types`,
144
+ `verify-types`, `check`, `deploy`, `pull`) are in
145
+ [Developing integrations](#developing-integrations).
146
+
147
+ **`device`**
148
+
149
+ | Command | Tool | Positional | Effect |
150
+ | --------------------- | ---------------- | ---------- | ------ |
151
+ | `juno device list` | `list_devices` | — | read |
152
+ | `juno device get` | `get_device` | `id` | read |
153
+ | `juno device preview` | `preview_device` | — | read |
154
+ | `juno device write` | `write_device` | — | change |
155
+ | `juno device suspend` | `suspend_device` | `id` | act |
156
+ | `juno device release` | `release_device` | `id` | act |
157
+
158
+ **`control`**
159
+
160
+ | Command | Tool | Positional | Effect |
161
+ | ---------------------- | ----------------- | ---------- | ------ |
162
+ | `juno control write` | `write_control` | — | change |
163
+ | `juno control use` | `use_control` | `id` | act |
164
+ | `juno control release` | `release_control` | `id` | act |
165
+
166
+ **`finding`**
167
+
168
+ | Command | Tool | Positional | Effect |
169
+ | ------------------ | ------------- | ---------- | ------ |
170
+ | `juno finding get` | `get_finding` | `slug` | read |
171
+
172
+ **`webhook`**
173
+
174
+ | Command | Tool | Positional | Effect |
175
+ | ----------------------------- | ------------------------ | ---------- | ------ |
176
+ | `juno webhook list` | `list_webhooks` | — | read |
177
+ | `juno webhook get-deliveries` | `get_webhook_deliveries` | `id` | read |
178
+ | `juno webhook write` | `write_webhook` | `id` | change |
179
+ | `juno webhook fire` | `fire_webhook` | `id` | act |
180
+
181
+ **`credential`**
182
+
183
+ | Command | Tool | Positional | Effect |
184
+ | -------------------------- | ------------------ | ------------- | ------ |
185
+ | `juno credential list` | `list_credentials` | — | read |
186
+ | `juno credential set <id>` | none | credential id | act |
187
+
188
+ **`ha`** (Home Assistant)
189
+
190
+ | Command | Tool | Positional | Effect |
191
+ | ---------------------------- | -------------------- | ----------- | ------ |
192
+ | `juno ha get-entity` | `get_entity` | `entity_id` | read |
193
+ | `juno ha get-entity-history` | `get_entity_history` | `entity_id` | read |
194
+ | `juno ha list-services` | `list_services` | — | read |
195
+ | `juno ha call-service` | `call_service` | — | act |
196
+
197
+ **`script`**
198
+
199
+ | Command | Tool | Positional | Effect |
200
+ | ------------------------------------- | ------------ | ----------------------------------------------------- | ------ |
201
+ | `juno script run <file> [--cred id]…` | `run_script` | `<file>` → `files`, `entry`; `--cred` → `credentials` | act |
202
+
203
+ `script run` reads the file relative to the working directory, puts its content
204
+ under `files` by its basename and makes it the `entry`.
205
+
206
+ **`session`** and **`config`**
207
+
208
+ | Command | Tool | Positional | Effect |
209
+ | --------------------------- | ---- | ----------- | ------ |
210
+ | `juno session approve <id>` | none | session id | act |
211
+ | `juno config set url <url>` | none | `url <url>` | local |
212
+
213
+ There is no command that calls a tool by name. One permission rule for such a
214
+ command would allow every act, so each tool has its own command instead, and a
215
+ tool added to the server needs a CLI release to reach.
216
+
217
+ ### Input
218
+
219
+ The id, when the command has one, is the single positional argument. Everything
220
+ else is the tool's input JSON, given with `--input`:
221
+
222
+ | Form | Reads |
223
+ | ------------------ | ----------------------------------------- |
224
+ | `--input '<json>'` | the JSON itself |
225
+ | `--input -` | standard input |
226
+ | `--input @<path>` | a file, relative to the working directory |
227
+
228
+ The input must be a JSON object. Without `--input` it is `{}`. The CLI never
229
+ reads standard input unless `--input -` asks for it, because an open, empty
230
+ stdin would hang an agent, and it never prompts off a terminal.
231
+
232
+ The input is built in this order: the `--input` object, then the positional into
233
+ its field, then the command's flags, then the fixed input (`preview-layers`'
234
+ `dry_run`). A positional or flag that disagrees with a value already in
235
+ `--input` is a usage error naming both. The CLI checks nothing else: the
236
+ instance validates the input against the tool's schema and answers with the
237
+ failing field (`/id`, `/ops/0/when`, `/` for the input as a whole).
238
+
239
+ ### Output
240
+
241
+ Standard output carries the tool's answer and nothing else:
242
+
243
+ - off a terminal, the instance's JSON byte for byte;
244
+ - on a terminal, the same JSON indented by two spaces. Never coloured.
245
+
246
+ The choice follows whether stdout is a terminal, so a script and an agent see
247
+ the same bytes. A tool that fails in its own terms answers `{"error": "…"}`;
248
+ that is printed on stdout like any answer, and the exit code is 1. Warnings,
249
+ progress and notes go to standard error.
250
+
251
+ `--jq <expr>` runs the answer through [`jq`](https://jqlang.org), which must be
252
+ on `PATH` (`jq -c` off a terminal, plain `jq` on one). A filter given this way
253
+ stays inside the command's own permission rule, where a pipe to `jq` would need
254
+ its own. A missing `jq` or an expression `jq` rejects exits 2.
255
+
256
+ ### Exit codes
257
+
258
+ One table for the whole CLI, the working-copy commands included:
259
+
260
+ | Code | When |
261
+ | ---- | ---------------------------------------------------------------------------------------------------- |
262
+ | 0 | Success |
263
+ | 1 | A tool answer with a top-level string `error`, a failed check or deploy, or any other failure |
264
+ | 2 | Usage: an unknown command or flag, bad `--input`, input the instance rejects, a bad `--jq` |
265
+ | 3 | The instance is unreachable |
266
+ | 4 | A working-copy `deploy` or `check`, any kind: the instance changed since the working copy was pulled |
267
+ | 5 | `juno ask` / `session approve`: the turn is still running |
268
+ | 6 | `juno ask` / `session approve`: the assistant is waiting for an answer or an approval |
269
+
270
+ An instance that does not serve a tool the CLI names is older than the CLI; that
271
+ is also exit 2, and the message says so.
272
+
273
+ ### `juno ask` and `juno session approve`
274
+
275
+ `juno ask` sends a prompt to Juno's in-app assistant through the
276
+ [Agent Prompt API](https://boivie.github.io/juno/reference/agent-api/) and
277
+ prints the turn's final event. It is for scripts, chat bridges and a person at a
278
+ terminal. Its effect is **act**, because the assistant can do anything its tools
279
+ allow.
280
+
281
+ ```bash
282
+ juno ask "Why is the porch lamp on?"
283
+ juno ask --session <id> "And the hallway?" # continue a conversation
284
+ juno ask --session <id> # keep waiting on a running turn
285
+ juno session approve <id> # let a pending tool call through
286
+ juno ask --session <id> "not while I'm away" # deny it, with a reason
287
+ ```
288
+
289
+ | Option | Meaning |
290
+ | ------------------ | -------------------------------------------------------------------------------- |
291
+ | `--session <id>` | resume that session. With no prompt, observe it: wait for its turn, send nothing |
292
+ | `--wait <seconds>` | off a terminal, how long one call waits before answering pending (default 60) |
293
+
294
+ Off a terminal each call is one request that waits up to `--wait` seconds, and
295
+ the final event is printed unchanged. On a terminal the answer streams: progress
296
+ and the session id go to standard error, and the final event is printed
297
+ indented. The exit code is the outcome:
298
+
299
+ | Event | Exit |
300
+ | ----------------------------------------------- | ---- |
301
+ | `message`, or `done` with `completed` | 0 |
302
+ | `pending` (still running) | 5 |
303
+ | `awaiting_input` (question or approval) | 6 |
304
+ | `done` with `failed` or `abandoned`, or `error` | 1 |
305
+
306
+ So "keep waiting" is `juno ask --session <id>` until the exit code is not 5. A
307
+ new conversation's session id is minted by the CLI, so a retry after a dropped
308
+ connection resumes the same session; a failed connection is retried twice before
309
+ exit 3. When the session is busy with another turn, `juno ask` waits for that
310
+ turn to end and sends again (up to three times), unless the turn ends waiting
311
+ for input, since the message would then be taken as the answer.
312
+
313
+ `juno ask approve` is refused: approving is `juno session approve <id>`, a
314
+ separate command so that a permission rule allowing `juno ask` does not also let
315
+ an agent approve the assistant's `call_service` on your behalf.
316
+ `session approve` first checks that the session is waiting for an approval and
317
+ exits 1 without sending anything otherwise. Any other message sent to a session
318
+ waiting for an approval denies it, and the assistant sees the message as the
319
+ reason.
320
+
321
+ ### `juno credential set`
322
+
323
+ ```bash
324
+ juno credential set pushover_token # update an existing one
325
+ juno credential set pushover_token --name Pushover --description "App token"
326
+ ```
327
+
328
+ It reads the secret from the terminal with echo off and stores it. An existing
329
+ credential keeps its name and description unless `--name` or `--description`
330
+ replaces them; a new one needs both. It never prints the value.
331
+
332
+ It **refuses to run when standard input is not a terminal** (exit 2), so an
333
+ agent cannot pipe a value into it: a secret an agent types lands in its
334
+ transcript. A person runs it, directly or as `! juno credential set <id>` inside
335
+ Claude Code. For the same reason there is no tool that writes a credential.
336
+
337
+ ### `juno skill`
338
+
339
+ `juno skill` lists the skills the assistant can load, one
340
+ `<name> — <description>` line each. `juno skill <name>` prints one skill's body.
341
+ `--json` prints the instance's JSON instead. The skills are the in-app
342
+ assistant's, rendered for a reader outside Juno: where the assistant reads a
343
+ tool name (`edit_state_layers`), you read the command in front of it
344
+ (`juno state edit-layers`), or what to do in a working copy instead
345
+ (`npm run deploy` for `submit_code`).
346
+
347
+ ### `juno agent-skill install`
348
+
349
+ ```bash
350
+ juno agent-skill install # .claude/skills/juno/SKILL.md
351
+ juno agent-skill install --dir ~/.claude/skills/juno # for every project
352
+ juno agent-skill install --allow-change # + every change command
353
+ juno agent-skill install --working-copy # + the npm scripts (run it in a working copy)
354
+ juno agent-skill install --agents-md >> AGENTS.md # for a harness that reads AGENTS.md
355
+ ```
356
+
357
+ Writes the skill that teaches a coding agent to use Juno — `SKILL.md` in the
358
+ Agent Skills format, rendered by the instance (`GET /api/agent-skill`) — and
359
+ prints a Claude Code permission block on stdout:
360
+
361
+ ```json
362
+ {"permissions":{"allow":["Bash(juno discover:*)","Bash(juno related:*)","Bash(juno state get:*)",…]}}
363
+ ```
364
+
365
+ The block is generated from the command table and each command's effect. By
366
+ default it allows every `read` command. `--allow-change` adds every `change`
367
+ command (layer stacks, devices, controls, `flow deploy`, …), for when you want
368
+ the agent to reconfigure freely and use version history to review or undo.
369
+ `--working-copy` adds the working copy's npm scripts of the same tiers
370
+ (`npm test`, `npm run check`, and with `--allow-change` `npm run deploy`); those
371
+ trust whatever that `package.json` says the script runs, so put them in the
372
+ working copy's `.claude/settings.json`, never in user-wide settings. **An `act`
373
+ command is never in the block**: it always prompts. Add one by hand if you want
374
+ to, for example, let the agent press a test button. A command added in a later
375
+ version prompts until you regenerate the block.
376
+
377
+ The skill is short and stable; what changes per instance comes from `juno skill`
378
+ and `--help` at run time. It records the version it was rendered at, and
379
+ `juno skill`, `juno flow init` and `juno integration init` say on stderr when
380
+ the instance serves a newer one. `--agents-md` prints the same text as an
381
+ `AGENTS.md` section and writes nothing; `--json` prints the instance's answer.
382
+
383
+ ## Choosing an instance
384
+
385
+ In order:
386
+
387
+ 1. `--url <url>`, before or after the command words;
388
+ 2. `$JUNO_URL`;
389
+ 3. `"juno": { "target": … }` in `package.json` in the working directory;
390
+ 4. the user config file, `$XDG_CONFIG_HOME/juno/config.json`
391
+ (`~/.config/juno/config.json` when `XDG_CONFIG_HOME` is unset).
392
+
393
+ A project downloaded from a Juno already has the third, so a fresh clone deploys
394
+ with no setup. `juno config set url <url>` writes the fourth, which is what
395
+ makes `juno` useful outside a project. A bare host such as `juno.local:8080`
396
+ means `http://juno.local:8080`.
397
+
398
+ Every request carries an `X-Juno-Client` header: `cli`, or `cli/claude-code`
399
+ when `CLAUDECODE=1` is set, as Claude Code sets it in the shells it starts. Juno
400
+ records a write made through a tool as `operator:<client>`. The header names who
401
+ made a change; it is not a security control.
402
+
403
+ ## Working copies
404
+
405
+ A working copy is a directory on your machine holding one flow or one
406
+ integration as files. Juno keeps every version, so the instance is the source of
407
+ truth and a working copy is where you edit the next version. Its `package.json`
408
+ names the instance (`juno.target`) and the flow or install it belongs to, so a
409
+ fresh clone deploys with no setup.
410
+
411
+ Both kinds have the same loop and the same npm scripts, and so does an app
412
+ (`juno app init`), whose `check` stores a draft to preview at its `~draft/` URL:
413
+
414
+ ```bash
415
+ npm install
416
+ npm test # local tsc, eslint and vitest
417
+ npm run check # ... then verify against the instance, which writes nothing
418
+ npm run deploy # verify, commit and activate the next version
419
+ npm run pull # bring the working copy up to the active version
420
+ ```
421
+
422
+ | Script | Flow working copy | Integration working copy | App working copy |
423
+ | -------------- | ------------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------- |
424
+ | `typecheck` | `tsc --noEmit` | `tsc --noEmit` | `tsc --noEmit && tsc --noEmit -p page` |
425
+ | `lint` | `eslint .` | `eslint .` | `eslint .` |
426
+ | `verify-types` | (absent) | `juno integration verify-types` | (absent) |
427
+ | `test` | `npm run typecheck && npm run lint && vitest run` | `npm run verify-types && npm run typecheck && npm run lint && vitest run` | `npm run typecheck && npm run lint && vitest run` |
428
+ | `types` | `juno flow gen-types` | `juno integration gen-types` | `juno app gen-types` |
429
+ | `check` | `juno flow check` | `juno integration check` | `juno app check` |
430
+ | `deploy` | `juno flow deploy` | `juno integration deploy` | `juno app deploy` |
431
+ | `watch` | `juno flow deploy --watch` | `juno integration deploy --watch` | `juno app deploy --watch` |
432
+ | `pull` | `juno flow pull` | `juno integration pull` | `juno app pull` |
433
+ | `dev` | (absent) | (absent) | `juno app dev` |
434
+
435
+ `dev` exists only in an app working copy, because flows and integrations have no
436
+ page to serve. `npm run dev` serves the app on `http://localhost:8090/`
437
+ (`--port <n>` picks another) with no instance, no credentials and no network:
438
+ the page is built from `page/` on each request, the handlers run in Node through
439
+ the testkit's `appHarness`, and live values come from a scenario file
440
+ (`--scenario test/fixtures/<name>.json`). A panel beside the page sets values,
441
+ presses controls and switches scenarios, and saving a file rebuilds and reloads.
442
+ It uses the `esbuild` and `@junoflow/testkit` the working copy installs.
443
+
444
+ `init` writes these lines as the instance renders them. After that they are
445
+ yours: `pull` never changes `scripts`. To see the current lines, run `init` into
446
+ a fresh directory and compare.
447
+
448
+ The working-copy commands sit in the `flow` and `integration` groups beside the
449
+ commands that act on the instance, and `juno flow --help` and
450
+ `juno integration --help` list them under their own heading. Every one except
451
+ `init` acts on the working copy in the current directory and refuses an
452
+ argument: `juno flow deploy porch-light` exits 2 and tells you to run
453
+ `juno flow init porch-light` and deploy from inside it.
454
+
455
+ | Effect | Commands |
456
+ | -------- | -------------------------------------------------------------------- |
457
+ | `read` | `init`, `gen-types`, `verify-types`, `check`, `pull`, `flow fixture` |
458
+ | `change` | `deploy`: it adds a version to the flow's or install's history |
459
+ | `local` | `app dev`: a server on localhost that touches no instance |
460
+
461
+ `init`, `pull` and `flow fixture` write local files, but change nothing in Juno,
462
+ so their effect is `read`.
463
+
464
+ After writing a working copy, `init` prints the skills for writing that kind,
465
+ one `<name> — <description>` line each, the same index Juno's in-app flow or
466
+ integration author is given; load one with `juno skill <name>`.
467
+ `juno flow --help` and `juno integration --help` list them too.
468
+
469
+ ### What Juno stores, and what `pull` writes
470
+
471
+ A working copy holds three kinds of file:
472
+
473
+ | Kind | Files | Stored by Juno |
474
+ | --------- | ------------------------------------------------------------------------------------------------- | -------------------------------------- |
475
+ | artifact | `manifest.json`, `src/**`, `test/**` | yes, with every version, byte for byte |
476
+ | generated | `types/**` | no; generated from the manifest |
477
+ | project | everything else: `tsconfig.json`, `eslint.config.js`, `README.md`, `.gitignore`, `package.json` … | no |
478
+
479
+ `check` and `deploy` send the artifact files. Your tests travel with the
480
+ version, so a copy pulled elsewhere gets them back. Juno stores `test/**` and
481
+ nothing more: it never type-checks, lints or runs a test. `npm test` runs them
482
+ in your checkout.
483
+
484
+ `pull` makes the artifact files the active version's, exactly as Juno stores
485
+ them, and regenerates `types/`. It deletes a local file only when Juno deleted
486
+ it: the version recorded in `juno.base_version` had the file and the pulled
487
+ version does not. A file neither version has is one you have not deployed yet,
488
+ and `pull` leaves it alone. A working copy with no `base_version` has nothing to
489
+ compare against, so `pull` deletes nothing. It prints what it wrote and, on a
490
+ second line, what it removed:
491
+
492
+ ```text
493
+ pulled porch-light v7 from http://juno.local:8080 (manifest.json, package.json, src/flow.ts)
494
+ removed src/old.ts
495
+ ```
496
+
497
+ `pull` never writes or deletes a project file. In `package.json` it sets two
498
+ keys, `juno.base_version` and `dependencies` (from the manifest), and leaves the
499
+ rest as you wrote it. An integration whose manifest declares no dependencies
500
+ keeps them in `package.json` alone, so there `pull` leaves `dependencies` as it
501
+ is. It refuses to overwrite uncommitted changes in a git working tree unless you
502
+ pass `--force`.
503
+
504
+ ## Flows as files
505
+
506
+ ```bash
507
+ juno flow init porch-light # the flow's active version, or a stub for a new id
508
+ cd porch-light && npm install
509
+ $EDITOR src/flow.ts manifest.json
510
+ npm test
511
+ npm run check
512
+ npm run deploy
513
+ ```
514
+
515
+ `juno flow init <id>` writes `./<id>` (or the current directory, with `--here`).
516
+ For a flow that has an active version, it is that version. For a new id it is a
517
+ stub whose only trigger is a `call` trigger, so a stub deployed by accident
518
+ never fires on its own. When the version stores no `test/**`, `init` also writes
519
+ a starter `test/flow.test.ts`: a conformance test against `@junoflow/testkit`'s
520
+ `flowHarness`, and one `it.todo` per trigger. A working copy has the same shape
521
+ as an integration's:
522
+
523
+ | File | What it is |
524
+ | --------------------- | ------------------------------------------------------------------------------------------ |
525
+ | `manifest.json` | the flow's manifest |
526
+ | `src/flow.ts` | the flow's entry |
527
+ | `src/**` | any other `.ts` or `.json` module the entry imports |
528
+ | `test/**` | your tests and their `.ts` or `.json` fixtures; stored with the flow, never compiled |
529
+ | `types/flow-env.d.ts` | the module the source imports its types from, generated from the manifest and the instance |
530
+ | `types/flow-api.d.ts` | the SDK's generic declarations, which `flow-env.d.ts` fills in; never import it directly |
531
+ | `tsconfig.json` | extends `@junoflow/cli/tsconfig.base.json` |
532
+ | `eslint.config.js` | `@junoflow/cli/eslint.flow.config.js`: the rules Juno's verify step lints flows by |
533
+ | `package.json` | the scripts, the dependencies, and `juno.target` / `flow_id` / `base_version` |
534
+ | `.gitignore` | `node_modules/` |
535
+
536
+ Every source file imports its types from the generated module:
537
+
538
+ ```ts
539
+ // src/flow.ts
540
+ import type { Flow } from '../types/flow-env';
541
+ import { describe } from './describe';
542
+
543
+ export default {
544
+ triggers: {
545
+ onCall: async ({ status }) => status.ok(describe()),
546
+ },
547
+ } satisfies Flow;
548
+ ```
549
+
550
+ `src/` and `test/` hold `.ts` and `.json` files only, at most 64 of them
551
+ together, each at most 512 KiB. Every one is sent on `check` and `deploy`. The
552
+ files under `src/` are type-checked and linted, and only `src/flow.ts` must
553
+ contain `satisfies Flow`. The files under `test/` are stored as they are; the
554
+ flow's `eslint.config.js` skips them too. `types/` is generated and is never
555
+ sent.
556
+
557
+ | Command | What it does |
558
+ | ----------------------------------------------- | -------------------------------------------------------------------------------------- |
559
+ | `juno flow init <id> [--here]` | write a working copy from the instance |
560
+ | `juno flow gen-types` | regenerate `types/flow-env.d.ts` and `types/flow-api.d.ts` from `manifest.json` |
561
+ | `juno flow check [--skip-local] [--json]` | local `tsc --noEmit` and `eslint .`, then verify against the instance, writing nothing |
562
+ | `juno flow deploy [--watch] [--force] [--json]` | verify, commit and activate; write back the manifest, the types and `base_version` |
563
+ | `juno flow pull [--force]` | bring the working copy up to the active version |
564
+ | `juno flow fixture --run <seq> [--force]` | write run `seq` as a test fixture, `test/fixtures/<flow-id>-<seq>.json` |
565
+
566
+ `pull` writes `manifest.json`, `src/**`, `test/**` and both `types/` files, and
567
+ deletes what the flow deleted (see
568
+ [What Juno stores](#what-juno-stores-and-what-pull-writes)).
569
+
570
+ `deploy` writes back what Juno stored: the canonical `manifest.json`, freshly
571
+ generated types, and the new version number. `--watch` redeploys whenever a file
572
+ under `src/` or `manifest.json` is saved (a deploy sends `test/` too, but saving
573
+ a test does not start one), skips a save that changed nothing, and keeps
574
+ watching after a failure. `--json` on `check` and `deploy` prints the instance's
575
+ JSON answer (or, when verification fails, its diagnostics) on stdout instead,
576
+ for CI; the integration commands take it too.
577
+
578
+ `fixture` turns a recorded run into a scenario a test loads with
579
+ `h.scenario(fixture)` from `@junoflow/testkit`: the event the run received, the
580
+ world it saw as far as Juno recorded it, and, under `expect`, the status, steps
581
+ and target calls it produced. It writes the file the instance builds, prints
582
+ `wrote test/fixtures/porch-light-42.json`, and lists on stderr each thing the
583
+ run did not record, such as a state value reconstructed from history or a
584
+ credential to fill in. It refuses to overwrite an existing fixture without
585
+ `--force` (exit 1), warns when the run ran another version than
586
+ `juno.base_version`, and exits 1 with the instance's reason for a run it cannot
587
+ replay: one still going, one that slept or waited for an event, or one that was
588
+ cancelled.
589
+
590
+ ### The flow id and `base_version`
591
+
592
+ `package.json`'s `juno.flow_id` binds the working copy to one flow. The
593
+ manifest's `id` is the flow's identity, so a manifest whose `id` differs from
594
+ `flow_id` is refused, naming both. To change what a flow is called, edit `name`;
595
+ renaming a flow is done in the UI.
596
+
597
+ `juno.base_version` is the version the working copy came from. `deploy` sends
598
+ it, and when the flow's active version is anything else (someone edited the flow
599
+ in the UI, asked the assistant to change it, or rolled it back), Juno refuses
600
+ the deploy and the CLI exits **4**, naming both versions. Run `npm run pull` to
601
+ start from the active version, or `juno flow deploy --force` to replace it.
602
+ `check` makes the same comparison, so a stale working copy shows up before you
603
+ deploy. A new flow's working copy has no `base_version`. `juno flow pull` on a
604
+ flow with no active version exits 1: there is nothing to pull.
605
+
606
+ ### Dependencies
607
+
608
+ `manifest.dependencies` is the list Juno installs when it verifies the flow.
609
+ `init` copies it into `package.json`'s `dependencies` so `npm install` gives
610
+ `tsc` the same modules. Add a package to the manifest, then to `package.json`
611
+ (or deploy and `npm run pull`). `juno flow check` warns when the two lists
612
+ differ, naming the packages.
613
+
614
+ ### Why there is no `flow verify-types`
615
+
616
+ `types/flow-env.d.ts` describes the instance as well as the manifest: the states
617
+ the flow binds, the integrations and controls it calls. It goes stale whenever
618
+ the instance changes, and Juno regenerates it on every check and deploy, so
619
+ stale local types never ship. `npm run types` refreshes your copy when the
620
+ editor disagrees with the instance.
621
+
622
+ ## Developing integrations
623
+
11
624
  An integration is a git repository holding `manifest.json` and TypeScript. Juno
12
625
  fetches it at a tag, compiles it, and runs it in a sandbox. There is no
13
626
  packaging format and nothing is published to npm.
14
627
 
15
- ## Commands
628
+ ```bash
629
+ npm install --global @junoflow/cli
630
+ juno integration init my-integration --url http://juno.local:8080
631
+ ```
16
632
 
17
- | Command | What it does |
18
- | ----------------------- | ------------------------------------------------------------------------------ |
19
- | `juno init <name>` | fetch a starting project from a Juno and write it down |
20
- | `juno types` | POST `manifest.json`, write `types/` |
21
- | `juno verify-types` | check `types/` still matches `manifest.json` — talks to nothing |
22
- | `juno check` | local `verify-types` + `tsc --noEmit` + `eslint`, then validate against a Juno |
23
- | `juno deploy [--watch]` | upload, verify, activate — and write back the returned types |
24
- | `juno pull` | write back the manifest and sources the instance holds |
633
+ ### Commands
634
+
635
+ | Command | What it does |
636
+ | ------------------------------------------------------------------ | ------------------------------------------------------------------------------ |
637
+ | `juno integration init <id> [--name <name>] [--here]` | write the install's project from a Juno, or a starter for a new id |
638
+ | `juno integration gen-types` | POST `manifest.json`, write `types/` |
639
+ | `juno integration verify-types` | check `types/` still matches `manifest.json`; talks to nothing |
640
+ | `juno integration check [--skip-local] [--id <id>] [--json]` | local `verify-types` + `tsc --noEmit` + `eslint`, then validate against a Juno |
641
+ | `juno integration deploy [--watch] [--force] [--id <id>] [--json]` | upload, verify, activate, and write back the returned types and `base_version` |
642
+ | `juno integration pull [--force] [--id <id>]` | write back the manifest, `src/`, `test/`, `types/` and `base_version` |
643
+
644
+ They exit as the [exit-code table](#exit-codes) says: 1 for a failed check or
645
+ deploy, 2 for a usage error, 3 when the instance is unreachable, 4 for a stale
646
+ working copy (see [`base_version`](#base_version)).
25
647
 
26
648
  ### Options
27
649
 
28
- | Flag | Meaning |
29
- | --------------- | -------------------------------------------------------------------- |
30
- | `--url <url>` | the instance to talk to |
31
- | `--name <name>` | address an install under a different name than the manifest declares |
32
- | `--watch` | redeploy on every save (`deploy`) |
33
- | `--force` | overwrite a dirty working tree (`pull`) |
34
- | `--here` | scaffold into the current directory (`init`) |
35
- | `--skip-local` | skip the local verify-types/tsc/eslint pass (`check`) |
650
+ | Flag | Meaning |
651
+ | --------------- | ----------------------------------------------------------------------------------------------------------- |
652
+ | `--url <url>` | the instance to talk to |
653
+ | `--id <id>` | address an install under a different id than the manifest declares (`check`, `deploy`, `pull`) |
654
+ | `--name <name>` | display name for a new integration (`init`) |
655
+ | `--watch` | redeploy on every save (`deploy`) |
656
+ | `--force` | overwrite a dirty working tree (`pull`); replace the active version whatever the base (`deploy`) |
657
+ | `--here` | scaffold into the current directory (`init`) |
658
+ | `--skip-local` | skip the local verify-types/tsc/eslint pass (`check`) |
659
+ | `--json` | print the instance's JSON answer (or the failed verify's diagnostics) on stdout, for CI (`check`, `deploy`) |
36
660
 
37
- ## Choosing an instance
661
+ ### `base_version`
38
662
 
39
- In order: `--url`, then `$JUNO_URL`, then `"juno": { "target": … }` in
40
- `package.json`. A project downloaded from a Juno already has the third, so a
41
- fresh clone deploys with no setup.
663
+ `package.json`'s `juno.base_version` is the version the working copy was pulled
664
+ from or last deployed. `init` of an installed id, the **Download project** zip,
665
+ `pull` and every successful `deploy` write it. `deploy` and `check` send it, and
666
+ when the install's active version is anything else (someone deployed another
667
+ copy, asked the assistant to change the integration, or rolled it back), Juno
668
+ refuses and the CLI exits **4**:
42
669
 
43
- Juno has **no authentication on any endpoint**. An instance is a trusted-LAN
44
- appliance; do not expose one to the internet.
670
+ ```text
671
+ integration "pushover": active version is v4, but this working copy was pulled from v3 — pull it again, or deploy with --force to replace it
672
+ ```
673
+
674
+ Run `npm run pull` to start from the active version, or
675
+ `juno integration deploy --force` to replace it. A working copy without a
676
+ `base_version` sends none; its first deploy or pull records one.
677
+
678
+ The base belongs to the install `manifest.json` names. `--id <other>` deploys
679
+ the same code to a different install, whose versions this copy has never seen,
680
+ so it sends no base, writes none back, and warns
681
+ `no stale-base check: deploying to <other>, not <manifest id>`. It needs no
682
+ `--force`. `pull --id <other>` rewrites `manifest.json` to that install, so
683
+ later deploys are checked against it.
45
684
 
46
- ## The loop
685
+ ### The loop
47
686
 
48
687
  ```bash
49
- npm run watch # juno deploy --watch
688
+ npm run watch # juno integration deploy --watch
50
689
  ```
51
690
 
52
691
  Add an action to `manifest.json`. The types regenerate, and your editor
@@ -54,7 +693,7 @@ immediately red-squiggles `src/integration.ts` because `IntegrationActions` now
54
693
  requires a method you have not written — with the parameter and return types
55
694
  your JSON Schema implies. The manifest drives the types drive the compiler.
56
695
 
57
- ## Why the types are committed
696
+ ### Why the types are committed
58
697
 
59
698
  `types/` is generated and checked in. CI needs no Juno, a fresh clone compiles
60
699
  offline, and the diff in a pull request shows that adding an action changed the
@@ -69,40 +708,42 @@ contract you no longer declare. `tsc` compiles happily against stale types,
69
708
  with your _module_, never with `types/` — so the mismatch surfaces at deploy, or
70
709
  later, as a flow author calling an action whose shape changed.
71
710
 
72
- `juno verify-types` closes it. `juno types` records a fingerprint of the
73
- `manifest.json` it generated from, as a comment in `types/integration-env.d.ts`:
711
+ `juno integration verify-types` closes it. `juno integration gen-types` records
712
+ a fingerprint of the `manifest.json` it generated from, as a comment in
713
+ `types/integration-env.d.ts`:
74
714
 
75
715
  ```ts
76
716
  // integration-env.d.ts for 'pushover'
77
- // DO NOT EDIT — generated by 'juno types' from 'manifest.json'.
717
+ // DO NOT EDIT — generated by 'juno integration gen-types' from 'manifest.json'.
78
718
  // juno:manifest-sha256 5c5ca8eabe12cf44
79
719
  ```
80
720
 
81
721
  `verify-types` recomputes it and compares. It runs first in `npm test` and
82
- inside `juno check`, and it contacts nothing — which is the point: the mistake
83
- is caught in a contributor's CI, and that CI deliberately has no instance to
84
- ask. Reformatting `manifest.json` also invalidates the stamp; the fix is the
85
- same `npm run types` either way.
722
+ inside `juno integration check`, and it contacts nothing — which is the point:
723
+ the mistake is caught in a contributor's CI, and that CI deliberately has no
724
+ instance to ask. Reformatting `manifest.json` also invalidates the stamp; the
725
+ fix is the same `npm run types` either way.
86
726
 
87
- ## What runs where
727
+ ### What runs where
88
728
 
89
- | | offline | needs an instance |
90
- | --------------------------------------------------------------- | ------- | ----------------- |
91
- | `tsc --noEmit`, `eslint`, `vitest` | ✅ | |
92
- | `juno verify-types` (is `types/` current?) | ✅ | |
93
- | structural manifest check via `"$schema"` in your editor | ✅ | |
94
- | reserved names, cross-surface collisions, JSON Schema compiling | | `juno check` |
95
- | `juno init`, `juno types`, `juno deploy` | | ✅ |
729
+ | | offline | needs an instance |
730
+ | --------------------------------------------------------------- | ------- | ------------------------ |
731
+ | `tsc --noEmit`, `eslint`, `vitest` | ✅ | |
732
+ | `juno integration verify-types` (is `types/` current?) | ✅ | |
733
+ | structural manifest check via `"$schema"` in your editor | ✅ | |
734
+ | reserved names, cross-surface collisions, JSON Schema compiling | | `juno integration check` |
735
+ | `juno integration init`, `gen-types`, `deploy` | | ✅ |
96
736
 
97
737
  Everything semantic lives in the Go server. This package knows some HTTP
98
738
  endpoints and a config preset — that is deliberate, so there is one answer to
99
739
  "is this manifest valid" rather than two that can disagree.
100
740
 
101
- ## `init` vs. downloading a project
741
+ ### `init` vs. downloading a project
102
742
 
103
- Both produce the same project, from the same generator on the instance: `init`
104
- starts from a stub manifest, the **Develop locally → Download project** button
105
- starts from an integration that already exists there. Neither is a local
106
- template — the CLI writes the files it is handed, so a scaffold cannot drift
107
- from what the server ejects. To refresh an existing checkout use `juno pull`; a
108
- second download would clobber your working tree.
743
+ Both produce the same project, from the same generator on the instance. `init`
744
+ of an id that is already installed writes that install's project, the one the
745
+ **Develop locally → Download project** button gives you; `init` of a new id
746
+ writes a starter. Neither is a local template: the CLI writes the files it is
747
+ handed, so a scaffold cannot drift from what the server ejects. To refresh an
748
+ existing checkout use `juno integration pull`, which touches only the files Juno
749
+ stores; a second download would clobber your working tree.