@junoflow/cli 0.6.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.
package/README.md CHANGED
@@ -66,7 +66,8 @@ Every command has one effect:
66
66
  starts a run, executes code), with nothing to roll back to.
67
67
 
68
68
  `config set` and `agent-skill install` are **local**: they write a file on your
69
- machine and change nothing on the instance.
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.
70
71
 
71
72
  No command of a lower effect is a string prefix of a command of a higher one.
72
73
  Claude Code matches a Bash permission rule by prefix, so an allowlist can list
@@ -256,15 +257,15 @@ its own. A missing `jq` or an expression `jq` rejects exits 2.
256
257
 
257
258
  One table for the whole CLI, the working-copy commands included:
258
259
 
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 |
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 |
268
269
 
269
270
  An instance that does not serve a tool the CLI names is older than the CLI; that
270
271
  is also exit 2, and the message says so.
@@ -407,30 +408,42 @@ truth and a working copy is where you edit the next version. Its `package.json`
407
408
  names the instance (`juno.target`) and the flow or install it belongs to, so a
408
409
  fresh clone deploys with no setup.
409
410
 
410
- Both kinds have the same loop and the same npm scripts:
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:
411
413
 
412
414
  ```bash
413
415
  npm install
414
- npm test # local tsc and eslint
416
+ npm test # local tsc, eslint and vitest
415
417
  npm run check # ... then verify against the instance, which writes nothing
416
418
  npm run deploy # verify, commit and activate the next version
417
- npm run pull # overwrite the working copy with the active version
419
+ npm run pull # bring the working copy up to the active version
418
420
  ```
419
421
 
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`.
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.
434
447
 
435
448
  The working-copy commands sit in the `flow` and `integration` groups beside the
436
449
  commands that act on the instance, and `juno flow --help` and
@@ -439,27 +452,61 @@ commands that act on the instance, and `juno flow --help` and
439
452
  argument: `juno flow deploy porch-light` exits 2 and tells you to run
440
453
  `juno flow init porch-light` and deploy from inside it.
441
454
 
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 |
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 |
446
460
 
447
- `init` and `pull` write local files, but change nothing in Juno, so their effect
448
- is `read`.
461
+ `init`, `pull` and `flow fixture` write local files, but change nothing in Juno,
462
+ so their effect is `read`.
449
463
 
450
464
  After writing a working copy, `init` prints the skills for writing that kind,
451
465
  one `<name> — <description>` line each, the same index Juno's in-app flow or
452
466
  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`.
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`.
456
503
 
457
504
  ## Flows as files
458
505
 
459
506
  ```bash
460
507
  juno flow init porch-light # the flow's active version, or a stub for a new id
461
508
  cd porch-light && npm install
462
- $EDITOR flow.ts manifest.json
509
+ $EDITOR src/flow.ts manifest.json
463
510
  npm test
464
511
  npm run check
465
512
  npm run deploy
@@ -468,33 +515,77 @@ npm run deploy
468
515
  `juno flow init <id>` writes `./<id>` (or the current directory, with `--here`).
469
516
  For a flow that has an active version, it is that version. For a new id it is a
470
517
  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/` |
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.
483
556
 
484
557
  | Command | What it does |
485
558
  | ----------------------------------------------- | -------------------------------------------------------------------------------------- |
486
559
  | `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` |
560
+ | `juno flow gen-types` | regenerate `types/flow-env.d.ts` and `types/flow-api.d.ts` from `manifest.json` |
488
561
  | `juno flow check [--skip-local] [--json]` | local `tsc --noEmit` and `eslint .`, then verify against the instance, writing nothing |
489
562
  | `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 |
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)).
491
569
 
492
570
  `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.
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.
498
589
 
499
590
  ### The flow id and `base_version`
500
591
 
@@ -522,11 +613,11 @@ differ, naming the packages.
522
613
 
523
614
  ### Why there is no `flow verify-types`
524
615
 
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.
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.
530
621
 
531
622
  ## Developing integrations
532
623
 
@@ -541,17 +632,18 @@ juno integration init my-integration --url http://juno.local:8080
541
632
 
542
633
  ### Commands
543
634
 
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 |
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` |
552
643
 
553
644
  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.
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)).
555
647
 
556
648
  ### Options
557
649
 
@@ -561,11 +653,35 @@ deploy, 2 for a usage error, 3 when the instance is unreachable.
561
653
  | `--id <id>` | address an install under a different id than the manifest declares (`check`, `deploy`, `pull`) |
562
654
  | `--name <name>` | display name for a new integration (`init`) |
563
655
  | `--watch` | redeploy on every save (`deploy`) |
564
- | `--force` | overwrite a dirty working tree (`pull`) |
656
+ | `--force` | overwrite a dirty working tree (`pull`); replace the active version whatever the base (`deploy`) |
565
657
  | `--here` | scaffold into the current directory (`init`) |
566
658
  | `--skip-local` | skip the local verify-types/tsc/eslint pass (`check`) |
567
659
  | `--json` | print the instance's JSON answer (or the failed verify's diagnostics) on stdout, for CI (`check`, `deploy`) |
568
660
 
661
+ ### `base_version`
662
+
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**:
669
+
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.
684
+
569
685
  ### The loop
570
686
 
571
687
  ```bash
@@ -629,5 +745,5 @@ of an id that is already installed writes that install's project, the one the
629
745
  **Develop locally → Download project** button gives you; `init` of a new id
630
746
  writes a starter. Neither is a local template: the CLI writes the files it is
631
747
  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.
748
+ existing checkout use `juno integration pull`, which touches only the files Juno
749
+ stores; a second download would clobber your working tree.