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