@effected/cli 0.25.0 → 0.27.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 +5 -208
- package/bin/effected.js +7 -0
- package/commands/root.js +14 -0
- package/commands/schema/LiveProgress.js +114 -0
- package/commands/schema/build.js +9 -0
- package/commands/schema/check.js +12 -0
- package/commands/schema/errors.js +121 -0
- package/commands/schema/execute.js +141 -0
- package/commands/schema/flags.js +24 -0
- package/commands/schema/index.js +14 -0
- package/commands/schema/validate.js +58 -0
- package/index.d.ts +85 -3906
- package/index.js +4 -26
- package/main.d.ts +51 -0
- package/main.js +61 -0
- package/package.json +18 -50
- package/render/DiffDoc.js +51 -0
- package/tsdoc-metadata.json +1 -1
- package/version.js +11 -0
- package/Cancelled.js +0 -43
- package/CliAudience.js +0 -177
- package/CliColor.js +0 -59
- package/CliEnv.js +0 -101
- package/CliExit.js +0 -64
- package/CliFailure.js +0 -287
- package/CliFormat.js +0 -124
- package/CliInteractive.js +0 -94
- package/CliLinks.js +0 -153
- package/CliLog.js +0 -367
- package/CliLogger.js +0 -91
- package/CliMessage.js +0 -79
- package/CliPrompt.js +0 -110
- package/CliRuntime.js +0 -254
- package/CliTest.js +0 -206
- package/CliTheme.js +0 -142
- package/ConfigIssueRenderer.js +0 -63
- package/Doc.js +0 -548
- package/Fmt.js +0 -335
- package/GithubAnnotation.js +0 -39
- package/Glyphs.js +0 -82
- package/NotInteractive.js +0 -41
- package/Render.js +0 -280
- package/SchemaIssueRenderer.js +0 -47
- package/Status.js +0 -165
- package/TestTerminal.js +0 -101
- package/Token.js +0 -68
- package/internal/ExitRequested.js +0 -21
- package/internal/HelpRouting.js +0 -35
- package/internal/ansi.js +0 -229
- package/internal/autoFormat.js +0 -33
- package/internal/canPrompt.js +0 -14
- package/internal/counts.js +0 -83
- package/internal/diagnostics.js +0 -31
- package/internal/displayPath.js +0 -46
- package/internal/displayWidth.js +0 -42
- package/internal/failureTarget.js +0 -195
- package/internal/fallbackAnswer.js +0 -17
- package/internal/fileSink.js +0 -61
- package/internal/format.js +0 -175
- package/internal/inspect.js +0 -40
- package/internal/isExitCode.js +0 -11
- package/internal/layout.js +0 -249
- package/internal/linkScheme.js +0 -29
- package/internal/linkTarget.js +0 -49
- package/internal/logSafety.js +0 -45
- package/internal/renderAnsi.js +0 -51
- package/internal/renderDoc.js +0 -322
- package/internal/renderGithubLog.js +0 -59
- package/internal/renderMarkdown.js +0 -367
- package/internal/renderPlain.js +0 -49
- package/internal/scanAudience.js +0 -105
- package/internal/splitFrame.js +0 -55
- package/internal/terminalCapabilities.js +0 -23
- package/internal/wizardGate.js +0 -17
- package/testing.d.ts +0 -246
- package/testing.js +0 -4
- package/ui/CliUi.js +0 -507
- package/ui/CliUiDetached.js +0 -36
- package/ui/CliUiLive.js +0 -436
- package/ui/CliUiRouteStream.js +0 -72
- package/ui/Confirm.js +0 -244
- package/ui/DocView.js +0 -76
- package/ui/KeyHelp.js +0 -61
- package/ui/KeyTable.js +0 -198
- package/ui/MultiSelect.js +0 -259
- package/ui/Select.js +0 -229
- package/ui/Tabs.js +0 -201
- package/ui/TextInput.js +0 -289
- package/ui/Toggle.js +0 -31
- package/ui/UiKey.js +0 -48
- package/ui/UiProvider.js +0 -62
- package/ui/UiStreams.js +0 -17
- package/ui/UiTheme.js +0 -147
- package/ui/Viewport.js +0 -203
- package/ui/internal/ErrorBoundary.js +0 -29
- package/ui/internal/Holder.js +0 -73
- package/ui/internal/ScreenContext.js +0 -51
- package/ui/internal/UiProviders.js +0 -38
- package/ui/internal/ink.js +0 -121
- package/ui/internal/inkChalk.js +0 -47
- package/ui/internal/inkConsole.js +0 -150
- package/ui/internal/lazyView.js +0 -73
- package/ui/internal/lineRouter.js +0 -70
- package/ui/internal/lineText.js +0 -18
- package/ui/internal/mountPermit.js +0 -15
- package/ui/internal/processStreams.js +0 -18
- package/ui/internal/renderOptions.js +0 -12
- package/ui/testing/CliUiTest.js +0 -792
- package/ui/testing/fakeStreams.js +0 -104
- package/ui/testing/terminalModel.js +0 -67
- package/ui-testing-serializer.d.ts +0 -12
- package/ui-testing-serializer.js +0 -32
- package/ui-testing.d.ts +0 -558
- package/ui-testing.js +0 -3
- package/ui.d.ts +0 -1990
- package/ui.js +0 -17
package/README.md
CHANGED
|
@@ -1,216 +1,13 @@
|
|
|
1
1
|
# @effected/cli
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[](https://nodejs.org/)
|
|
6
|
-
[](https://www.typescriptlang.org/)
|
|
3
|
+
The `effected` command: the tool's command tree on the kit's front-end runtime (`@effected/cli-kit`).
|
|
7
4
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
> **Pre-`1.0.0`.** This package is part of the `@effected/*` kit, built on stable
|
|
11
|
-
> Effect v4 (`effect` `~4.1.0`) and still in `0.x` development. Stable Effect
|
|
12
|
-
> makes a kit `1.0.0` possible, not automatic. To keep your `effect` and
|
|
13
|
-
> `@effect/*` versions on the line the kit is built and tested against, install
|
|
14
|
-
> [`@effected/pnpm-plugin-effected`](https://www.npmjs.com/package/@effected/pnpm-plugin-effected).
|
|
15
|
-
>
|
|
16
|
-
> **Stability: unstable.** This package's API surface is not yet considered
|
|
17
|
-
> complete and may change across `0.x` releases. Pin an exact version — even a
|
|
18
|
-
> package marked *stable* before `1.0.0` can introduce a breaking change by
|
|
19
|
-
> accident, and an exact pin turns that into a type-check error rather than a
|
|
20
|
-
> runtime surprise. Full policy: [release strategy](https://github.com/spencerbeggs/effected#release-strategy).
|
|
21
|
-
|
|
22
|
-
## Why @effected/cli
|
|
23
|
-
|
|
24
|
-
Everything here shares one property: **you only discover you needed it by shipping bad output to a person or a machine.** None of it fails a type-check, a test, or a review of the code in isolation.
|
|
25
|
-
|
|
26
|
-
Effect's default logger emits `[00:33:56.619] INFO (#2): message`, which is right for a service and noise for a tool someone is watching. A platform `runMain` reports an unhandled failure through that same default logger, outside the layers your program was given, so it prints on **stdout**, the one stream errors must not use. Colour codes end up in an agent's context window. A prompt fires inside a pipe and hangs. A file name containing `::error::` becomes a workflow command in GitHub Actions. And a decode failure arrives as a structured tree when a user needs a sentence naming the key they got wrong.
|
|
27
|
-
|
|
28
|
-
This package makes those decisions once, at the edge of the program, from the environment it actually runs in.
|
|
29
|
-
|
|
30
|
-
## Install
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
npm install @effected/cli @effected/env @effected/glob @effected/walker effect
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
pnpm add @effected/cli @effected/env @effected/glob @effected/walker effect
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
Requires Node.js >=24.11.0. `effect` v4, `@effected/env` (the audience and terminal services), and `@effected/walker` with its `@effected/glob` peer (the project root for editor links) are peer dependencies. The one runtime dependency is `@effected/github-commands`. The package never imports a platform package, so it runs unchanged on Node, Bun and Deno.
|
|
41
|
-
|
|
42
|
-
Optional peers:
|
|
43
|
-
|
|
44
|
-
- **`ink` and `react`**, plus **`@types/react`** for TypeScript, for the interactive screens and live views in `@effected/cli/ui`. The root never reaches them, and `./ui` loads neither until a screen first mounts. Without `@types/react`, a program compiled with `skipLibCheck` silently types every screen as `any`.
|
|
45
|
-
|
|
46
|
-
```bash
|
|
47
|
-
npm install ink react @types/react
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
- **`@effected/config-file`**, only for `ConfigIssueRenderer`. It is imported as a type, so nothing at runtime reaches for it.
|
|
51
|
-
|
|
52
|
-
All `@effected/*` packages are ESM-only: the exports maps publish only `import` conditions, so `require()` fails with Node's `ERR_PACKAGE_PATH_NOT_EXPORTED`. Import from an ES module.
|
|
53
|
-
|
|
54
|
-
## Quick start
|
|
55
|
-
|
|
56
|
-
One wiring serves every program: share the audience flags on the root command, run it through `CliAudience.run`, and hand that to `CliRuntime.main` with an `env`.
|
|
57
|
-
|
|
58
|
-
```ts
|
|
59
|
-
import { CliAudience, CliExit, CliMessage, CliRuntime, Doc } from "@effected/cli";
|
|
60
|
-
import { NodeRuntime, NodeServices } from "@effect/platform-node";
|
|
61
|
-
import { Effect } from "effect";
|
|
62
|
-
import { Command } from "effect/cli";
|
|
63
|
-
|
|
64
|
-
const sync = Command.make("sync", {}, () =>
|
|
65
|
-
Effect.gen(function* () {
|
|
66
|
-
yield* CliMessage.info("syncing 2 repositories");
|
|
67
|
-
yield* Doc.print([
|
|
68
|
-
Doc.table(
|
|
69
|
-
[{ header: "Repository" }, { header: "Files", align: "right" }],
|
|
70
|
-
[
|
|
71
|
-
["acme/widgets", "12"],
|
|
72
|
-
["acme/gadgets", "3"],
|
|
73
|
-
],
|
|
74
|
-
),
|
|
75
|
-
]);
|
|
76
|
-
yield* CliMessage.warning("acme/gadgets has no default branch");
|
|
77
|
-
// A finding, not a crash: the handler succeeds and the run still exits 1.
|
|
78
|
-
yield* CliExit.set(1);
|
|
79
|
-
}),
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
const root = Command.make("tool").pipe(Command.withSharedFlags(CliAudience.flags()), Command.withSubcommands([sync]));
|
|
83
|
-
|
|
84
|
-
NodeRuntime.runMain(
|
|
85
|
-
CliRuntime.main(CliAudience.run(root, { version: "1.0.0" }), {
|
|
86
|
-
platform: NodeServices.layer,
|
|
87
|
-
env: {
|
|
88
|
-
audienceEnvVar: "TOOL_AUDIENCE",
|
|
89
|
-
stderrIsTerminal: Effect.sync(() => process.stderr.isTTY === true),
|
|
90
|
-
},
|
|
91
|
-
}),
|
|
92
|
-
);
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
```text
|
|
96
|
-
$ tool sync
|
|
97
|
-
ℹ syncing 2 repositories
|
|
98
|
-
Repository Files
|
|
99
|
-
------------ -----
|
|
100
|
-
acme/widgets 12
|
|
101
|
-
acme/gadgets 3
|
|
102
|
-
⚠ acme/gadgets has no default branch
|
|
103
|
-
$ echo $?
|
|
104
|
-
1
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
At a colour terminal the glyphs are painted and the header is bold. For an agent (`--agent`, `TOOL_AUDIENCE=agent`, or an agent detected from the environment), in a pipe, or under `NO_COLOR`, the same program writes no escape sequence of any kind. Under GitHub Actions, anything the runner could read as a workflow command is neutralized. A failure anywhere renders as a short report on stderr and exits non-zero.
|
|
108
|
-
|
|
109
|
-
## Audiences
|
|
110
|
-
|
|
111
|
-
The audience is decided once per run: an audience flag (`--audience <human|agent|ci>`, `--human`, `--agent`, `--ci`), then the override variable you name, then an agent detected from the environment, then CI, else a human. It decides:
|
|
112
|
-
|
|
113
|
-
| | Human | Agent | CI |
|
|
114
|
-
| --- | --- | --- | --- |
|
|
115
|
-
| Colour | When the stream has it | Never, even with `FORCE_COLOR` | When the stream has it (messages) |
|
|
116
|
-
| `Doc.print` | `ansi`: painted, OSC 8 links | `plain` | `githubLog` under GitHub Actions, else `plain` |
|
|
117
|
-
| Width | The terminal's | Unbounded | Unbounded |
|
|
118
|
-
| Diagnostics (`CliLog`) | Pretty lines | NDJSON | NDJSON |
|
|
119
|
-
| Prompts and screens | When interactive | Never | Never |
|
|
120
|
-
|
|
121
|
-
Colour follows Node's precedence, per stream: `FORCE_COLOR` decides first and beats `NO_COLOR` (`1`–`3` on, even in a pipe; `0` off). Otherwise there is no colour without a terminal. On a terminal, a non-empty `NO_COLOR`, `NODE_DISABLE_COLORS` or `TERM=dumb` turns it off. `TERM=dumb` also switches to ASCII glyphs and makes the run non-interactive.
|
|
122
|
-
|
|
123
|
-
## Output
|
|
124
|
-
|
|
125
|
-
- **`CliMessage`**: `success`, `info`, `warning`, `failure` and `status(vocab, name, text)`. One themed line each, through `Console` rather than the logger, so no log level silences them. Warnings and failures go to stderr.
|
|
126
|
-
- **`Doc` and `Render`**: a document IR (headings, paragraphs, lists, tables, trees, counts, count tables, collapsibles, callouts, code blocks, diffs, GitHub annotations) and pure `plain`, `ansi`, `markdown` and `githubLog` renderers. `Doc.print` picks the renderer for the audience, and lays out at the terminal's width only when the stream is a terminal: piped output (`tool | grep`) never wraps. `Doc.line(content, { wrap: false })` keeps one line whole at any width, glyph and colour included. `Render.contextOf` renders outside Effect, for example markdown for a step summary.
|
|
127
|
-
- **`CliTheme`, `Token`, `Status`, `Glyphs`**: semantic tokens (`success`, `failure`, `warning`, `info`, `error`, `muted`, `accent`, `emphasis`), an extendable status vocabulary with glyphs and ranks, and Unicode or ASCII glyph sets. Override tokens with `env.theme`.
|
|
128
|
-
- **`CliLinks`**: file links that open in VS Code (`vscode://file/…`) or as `file://` URLs, as OSC 8 hyperlinks where the terminal renders them, never for an agent.
|
|
129
|
-
- **`Fmt`**: `sanitize`, `width`, `truncate`, `duration`, `percent` and `plural`.
|
|
130
|
-
|
|
131
|
-
Every string that enters a document or a message is sanitised: escape sequences and control characters are removed, so data cannot paint the terminal or plant a link.
|
|
132
|
-
|
|
133
|
-
## Failures and exit codes
|
|
134
|
-
|
|
135
|
-
`CliRuntime.main` reports a failure as a document on stderr, through the audience's renderer: a status line for a typed failure, a tree of rejected values for a schema failure, and a defect's message with a collapsible stack of your own frames (Effect's, Node's and `node_modules` frames hidden). Give an error class a `[CliDoc]()` method to draw itself, or pass a `render` option. Its `details.lines({ status: false })` keeps the run's colour and paths behind your own prefix. The `in: outer › inner` span trail after a failure names only your own spans by default; `env.spans` (`"app"`, `"all"` or `"off"`) chooses, as `env.stackFrames` does for a defect's frames. `"app"` leaves out spans defined in files under `node_modules/@effected/` or `node_modules/effect/`, and fails open: a kit package linked into a workspace, or a bundled program, shows more, never less. A program that is itself installed under `node_modules/@effected/` passes its bin's `import.meta.url` as `env.appModule` to keep its own spans. `env.spansEnvVar` names a variable (say `TOOL_SPANS`) that sets it at run time, as `log.envVar` sets the level. An `Effect.fn` call and its definition are one entry in the trail.
|
|
136
|
-
|
|
137
|
-
- `CliExit.set(code)` records a findings exit code from a handler that still succeeds. Do not provide `CliExit.layer` yourself under `main`, or the code goes to a second, unread cell.
|
|
138
|
-
- `Cancelled` (a prompt quit) exits `130`, and `NotInteractive` (a prompt with nobody to ask) exits `64`, each as one fixed line.
|
|
139
|
-
- A usage error exits `64`. `helpOnUsageError: "stderr"` keeps stdout clean for a caller piping it into `jq`.
|
|
140
|
-
- Without `main`: `CliRuntime.reportFailures()` is the combinator to apply inside your program, and `CliRuntime.reported(error, code)` marks an error you printed yourself.
|
|
141
|
-
|
|
142
|
-
`SchemaIssueRenderer.render(issue)` and `ConfigIssueRenderer.render(error)` turn an issue tree into lines like `unknown key at groups.g.rulesetz`.
|
|
143
|
-
|
|
144
|
-
## Logging
|
|
145
|
-
|
|
146
|
-
`CliLogger` writes plain lines, with no timestamp, level or fiber id, and routes every level to stderr by default (`stderrFrom` narrows it), so stdout carries only the program's output. Pass `env.log` to `main` for **`CliLog`**: a diagnostics level of its own (`level`, or `envVar` such as `TOOL_LOG_LEVEL`, with core's `--log-level` beating both), pretty lines for a person and NDJSON for an agent or CI, an optional NDJSON log file, and `CliLog.component(name)` tags. `CliLog.status(vocab, name, text)` logs a diagnostic with a painted status glyph (its text still sanitised), and `CliTheme.forAudience` applies the kit's "an agent never gets an escape" rule to a theme you paint with yourself.
|
|
147
|
-
|
|
148
|
-
## Prompts and screens
|
|
149
|
-
|
|
150
|
-
A prompt fires only when `CliInteractive` is true: a human audience, a terminal on stdin and stdout, and `TERM` not `dumb`. Otherwise it answers a default you supply, or fails cleanly.
|
|
151
|
-
|
|
152
|
-
```ts
|
|
153
|
-
import { CliPrompt } from "@effected/cli";
|
|
154
|
-
import { Flag, Prompt } from "effect/cli";
|
|
155
|
-
|
|
156
|
-
// Core's prompt as a flag fallback: asks at a terminal, uses "library" in a pipe.
|
|
157
|
-
const profile = Flag.String("profile").pipe(
|
|
158
|
-
Flag.withFallbackPrompt(
|
|
159
|
-
CliPrompt.fallback(
|
|
160
|
-
Prompt.Select({ message: "Profile", choices: [{ title: "library", value: "library" }, { title: "application", value: "application" }] }),
|
|
161
|
-
{ flag: "profile", otherwise: "library" },
|
|
162
|
-
),
|
|
163
|
-
),
|
|
164
|
-
);
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
`@effected/cli/ui` adds Ink screens: `CliUi.run`, `prompt` (with an `otherwise`) and `fallback` (for a flag), over the widgets `Select`, `TextInput` (with a `mask` for secrets, always, or from the moment a predicate spots one anywhere in the value, latched until the value is cleared: its `validate` message is drawn unmasked, so never echo the value in it), `MultiSelect`, `Confirm` (with toggles), `Toggle`, `Tabs` and `Viewport`. Your own screens use the key layer (`KeyTable`, `useKeys`, `KeyHelp`) and the theme bridge (`Styled`, `useTheme`, `useGlyphs`, `useTerminalSize`).
|
|
168
|
-
|
|
169
|
-
```tsx
|
|
170
|
-
import { CliUi, Select } from "@effected/cli/ui";
|
|
171
|
-
|
|
172
|
-
const pickProfile = Select.screen({
|
|
173
|
-
message: "Profile",
|
|
174
|
-
choices: [
|
|
175
|
-
{ label: "library", value: "library" },
|
|
176
|
-
{ label: "application", value: "application" },
|
|
177
|
-
],
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
const profile = CliUi.prompt(pickProfile, { otherwise: "library" });
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
`CliUi.map(screen, f)` maps a screen's answer and leaves a cancel alone, so a `Confirm` can back a boolean flag ("confirm, or `--yes`"):
|
|
5
|
+
This release answers `--version` and `--help` only. `effected --version` prints `effected <version>`; when a carrier package launched the bin, it ends with "via" and the carrier's name and version.
|
|
184
6
|
|
|
185
7
|
```ts
|
|
186
|
-
import {
|
|
187
|
-
import { Flag } from "effect/cli";
|
|
8
|
+
import { main } from "@effected/cli/main";
|
|
188
9
|
|
|
189
|
-
|
|
190
|
-
Flag.withFallbackPrompt(
|
|
191
|
-
CliUi.fallback(
|
|
192
|
-
CliUi.map(Confirm.screen({ message: "Publish?" }), (result) => result.confirmed),
|
|
193
|
-
{ flag: "yes", otherwise: false },
|
|
194
|
-
),
|
|
195
|
-
),
|
|
196
|
-
);
|
|
10
|
+
main({ distribution: { name: "@effected/plugin", version: "1.0.0" } });
|
|
197
11
|
```
|
|
198
12
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
`CliUi.live` folds a stream or a `PubSub` subscription of events into state, and draws **runs** with Ink while they are going: a run starts at `isStart`, redraws on a tick, and commits its final frame at `isTerminal`. Log lines go above the frame through `handle.logConsole`. End with `handle.close`, which folds everything still queued. When nobody is watching (a pipe, an agent, CI), each run's final frame prints once instead: give the view a `final: (state) => Document` and that run prints the document with no Ink or React loaded at all; `render` is then never called on such a run, not even to build an unused string. `render: CliUi.lazyView(() => import("./view.js"))` keeps the view's module, and React, off every run until one draws. `DocView` draws a `Doc` document inside a view byte for byte as `Doc.print` would, and `UiProvider` with `CliUi.context` gives an Ink tree you mount yourself the same theme.
|
|
202
|
-
|
|
203
|
-
## Testing
|
|
204
|
-
|
|
205
|
-
- **`@effected/cli/testing`**: `CliTest.sandbox` and `CliTest.run` spawn a built bin hermetically and return `{ exitCode, stdout, stderr }` as data. `TestTerminal` drives core's prompts.
|
|
206
|
-
- **`@effected/cli/ui/testing`**: `CliUiTest.render` mounts a screen on in-memory streams (`press`, `type`, `chunk`, `frame`, `result`). `view` mounts a display-only element, `session` drives a whole command's screens (its `transcript` shows what reached the terminal, a live view's `logConsole` lines included, `stdoutWritten`/`stderrWritten` each stream alone as raw bytes, `stdoutTranscript`/`stderrTranscript` each stream alone as plain text, and `renderPath: "production"` makes `clear` observable), and `live` mounts a live view on the production render path with a `TestClock` tick. `CliUiTest.serializer` prints frames as token markup in snapshots: register it in the Vitest config with `snapshotSerializers: ["@effected/cli/ui/testing/serializer"]`, or with `expect.addSnapshotSerializer`. Snapshots are the one place a test needs `expect`, since `assert` has no snapshot form.
|
|
207
|
-
|
|
208
|
-
In-process, provide `layerTest`s from `@effected/env` and `CliTheme.layerTest`, swap in a capturing `Console`, and assert on both streams. Neither testing entrypoint is reachable from a CLI's runtime imports.
|
|
209
|
-
|
|
210
|
-
## Documentation
|
|
211
|
-
|
|
212
|
-
Guides for every part, and the full API reference, are at [effected.spencerbeg.gs/cli](https://effected.spencerbeg.gs/cli): [getting started](https://effected.spencerbeg.gs/cli/getting-started), [audiences and output](https://effected.spencerbeg.gs/cli/output), [prompts and screens](https://effected.spencerbeg.gs/cli/prompts), [live views](https://effected.spencerbeg.gs/cli/live-views), [testing](https://effected.spencerbeg.gs/cli/testing) and [advanced](https://effected.spencerbeg.gs/cli/advanced).
|
|
213
|
-
|
|
214
|
-
## License
|
|
215
|
-
|
|
216
|
-
[MIT](LICENSE)
|
|
13
|
+
> **Pre-`1.0.0`.** Part of the `@effected/*` kit; unstable across `0.x` releases. Pin an exact version.
|
package/bin/effected.js
ADDED
package/commands/root.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { makeSchemaCommand } from "./schema/index.js";
|
|
2
|
+
import { CliAudience } from "@effected/cli-kit";
|
|
3
|
+
import { Command } from "effect/cli";
|
|
4
|
+
|
|
5
|
+
// src/commands/root.ts
|
|
6
|
+
/**
|
|
7
|
+
* The `effected` command tree, closed over the process boundary it runs against: `main` hands in the process values, a
|
|
8
|
+
* test its own. The audience flags are shared on the root so every subcommand accepts them.
|
|
9
|
+
*
|
|
10
|
+
* @public
|
|
11
|
+
*/
|
|
12
|
+
const makeRootCommand = (deps) => Command.make("effected", {}).pipe(Command.withDescription("The effected tool: a toolchain for Effect v4 applications."), Command.withSharedFlags(CliAudience.flags()), Command.withSubcommands([makeSchemaCommand(deps)]));
|
|
13
|
+
|
|
14
|
+
export { makeRootCommand };
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Console, Effect, PubSub } from "effect";
|
|
2
|
+
import { CliUi, useGlyphs, useStyle } from "@effected/cli-kit/ui";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
import { createElement } from "react";
|
|
5
|
+
|
|
6
|
+
// src/commands/schema/LiveProgress.ts
|
|
7
|
+
const SETTLED = {
|
|
8
|
+
written: {
|
|
9
|
+
label: "written",
|
|
10
|
+
token: "success"
|
|
11
|
+
},
|
|
12
|
+
unchanged: {
|
|
13
|
+
label: "unchanged",
|
|
14
|
+
token: "muted"
|
|
15
|
+
},
|
|
16
|
+
"would-write": {
|
|
17
|
+
label: "would write",
|
|
18
|
+
token: "warning"
|
|
19
|
+
},
|
|
20
|
+
drift: {
|
|
21
|
+
label: "drift",
|
|
22
|
+
token: "error"
|
|
23
|
+
},
|
|
24
|
+
held: {
|
|
25
|
+
label: "held",
|
|
26
|
+
token: "muted"
|
|
27
|
+
},
|
|
28
|
+
"gate-failed": {
|
|
29
|
+
label: "gate failed",
|
|
30
|
+
token: "error"
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const reduce = (state, event) => {
|
|
34
|
+
const update = (name, row) => ({ rows: state.rows.map((current) => current.name === name ? {
|
|
35
|
+
name,
|
|
36
|
+
...row
|
|
37
|
+
} : current) });
|
|
38
|
+
switch (event._tag) {
|
|
39
|
+
case "Started": return { rows: event.names.map((name) => ({
|
|
40
|
+
name,
|
|
41
|
+
pending: true,
|
|
42
|
+
label: "checking",
|
|
43
|
+
token: "muted"
|
|
44
|
+
})) };
|
|
45
|
+
case "SchemaChecked": return update(event.name, event.blocked ? {
|
|
46
|
+
pending: true,
|
|
47
|
+
label: "gate failed",
|
|
48
|
+
token: "error"
|
|
49
|
+
} : event.verdict === "drift" ? {
|
|
50
|
+
pending: true,
|
|
51
|
+
label: "drift",
|
|
52
|
+
token: "error"
|
|
53
|
+
} : event.change === "none" ? {
|
|
54
|
+
pending: true,
|
|
55
|
+
label: "clean",
|
|
56
|
+
token: "muted"
|
|
57
|
+
} : {
|
|
58
|
+
pending: true,
|
|
59
|
+
label: event.change,
|
|
60
|
+
token: "warning"
|
|
61
|
+
});
|
|
62
|
+
case "SchemaDone": return update(event.report.name, {
|
|
63
|
+
pending: false,
|
|
64
|
+
...SETTLED[event.report.outcome]
|
|
65
|
+
});
|
|
66
|
+
case "Ended": return state;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const RowView = ({ row, frame }) => {
|
|
70
|
+
const glyphs = useGlyphs();
|
|
71
|
+
const name = useStyle("emphasis");
|
|
72
|
+
const label = useStyle(row.token);
|
|
73
|
+
const mark = row.pending ? glyphs.spinner[frame % glyphs.spinner.length] ?? "" : glyphs.bullet;
|
|
74
|
+
return createElement(Text, null, createElement(Text, label, `${mark} `), createElement(Text, name, row.name), createElement(Text, label, ` ${row.label}`));
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* The view's options, less the events: one value, so the test drives what the command runs.
|
|
78
|
+
*
|
|
79
|
+
* @internal
|
|
80
|
+
*/
|
|
81
|
+
const liveProgress = {
|
|
82
|
+
initial: { rows: [] },
|
|
83
|
+
reduce,
|
|
84
|
+
render: (state, frame) => createElement(Box, { flexDirection: "column" }, ...state.rows.map((row) => createElement(RowView, {
|
|
85
|
+
key: row.name,
|
|
86
|
+
row,
|
|
87
|
+
frame
|
|
88
|
+
}))),
|
|
89
|
+
isStart: (event) => event._tag === "Started",
|
|
90
|
+
isTerminal: (event) => event._tag === "Ended"
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Run the walk under the view: its progress feeds the rows, and its log lines write above the frame.
|
|
94
|
+
*
|
|
95
|
+
* @remarks
|
|
96
|
+
* The frame is committed before the walk's answer is handed back, whichever way it ended, so what follows prints
|
|
97
|
+
* below it and a walk that failed keeps the rows it reached.
|
|
98
|
+
*
|
|
99
|
+
* @internal
|
|
100
|
+
*/
|
|
101
|
+
const withLiveProgress = (walk) => Effect.gen(function* () {
|
|
102
|
+
const pubsub = yield* PubSub.unbounded();
|
|
103
|
+
const events = yield* PubSub.subscribe(pubsub);
|
|
104
|
+
const view = yield* CliUi.live({
|
|
105
|
+
...liveProgress,
|
|
106
|
+
events
|
|
107
|
+
});
|
|
108
|
+
const exit = yield* Effect.exit(walk((event) => Effect.asVoid(PubSub.publish(pubsub, event))).pipe(Effect.provideService(Console.Console, view.logConsole)));
|
|
109
|
+
yield* PubSub.publish(pubsub, { _tag: "Ended" });
|
|
110
|
+
yield* view.close;
|
|
111
|
+
return yield* exit;
|
|
112
|
+
}).pipe(Effect.scoped);
|
|
113
|
+
|
|
114
|
+
export { liveProgress, withLiveProgress };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { execute } from "./execute.js";
|
|
2
|
+
import { commandFlags } from "./flags.js";
|
|
3
|
+
import { Command } from "effect/cli";
|
|
4
|
+
|
|
5
|
+
// src/commands/schema/build.ts
|
|
6
|
+
/** `effected schema build`: generate, gate and write every schema and catalog entry the config declares. */
|
|
7
|
+
const makeBuildCommand = (deps) => Command.make("build", commandFlags, (input) => execute("build", input, deps)).pipe(Command.withDescription("Generate, gate and write every schema the config declares, its catalog slice, and the merged catalog"));
|
|
8
|
+
|
|
9
|
+
export { makeBuildCommand };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { execute } from "./execute.js";
|
|
2
|
+
import { commandFlags } from "./flags.js";
|
|
3
|
+
import { Command } from "effect/cli";
|
|
4
|
+
|
|
5
|
+
// src/commands/schema/check.ts
|
|
6
|
+
/**
|
|
7
|
+
* `effected schema check`: the same walk as `build`, reported and never written. The CI drift gate: it also fails when
|
|
8
|
+
* the committed documents are stale, and on outputs nothing claims, which `build` never deletes.
|
|
9
|
+
*/
|
|
10
|
+
const makeCheckCommand = (deps) => Command.make("check", commandFlags, (input) => execute("check", input, deps)).pipe(Command.withDescription("Report what build would do, fail when it would write anything or refuse to, or when a catalog file or document is orphaned or the merged catalog is blocked, and write nothing"));
|
|
11
|
+
|
|
12
|
+
export { makeCheckCommand };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
// src/commands/schema/errors.ts
|
|
4
|
+
const DriftedSchema = Schema.Struct({
|
|
5
|
+
$id: Schema.String,
|
|
6
|
+
change: Schema.Literals([
|
|
7
|
+
"none",
|
|
8
|
+
"created",
|
|
9
|
+
"annotations",
|
|
10
|
+
"contract"
|
|
11
|
+
]),
|
|
12
|
+
version: Schema.optionalKey(Schema.String),
|
|
13
|
+
nextVersion: Schema.optionalKey(Schema.String)
|
|
14
|
+
});
|
|
15
|
+
/**
|
|
16
|
+
* A published schema drifted under `onDrift: "error"`, so nothing was written. Exit `1`.
|
|
17
|
+
*
|
|
18
|
+
* @public
|
|
19
|
+
*/
|
|
20
|
+
var DriftError = class extends Schema.TaggedError()("DriftError", { drifted: Schema.Array(DriftedSchema) }) {
|
|
21
|
+
get count() {
|
|
22
|
+
return this.drifted.length;
|
|
23
|
+
}
|
|
24
|
+
get message() {
|
|
25
|
+
const lines = this.drifted.map((s) => ` ${s.$id}: ${s.change}${s.version !== void 0 ? ` at published ${s.version}` : ""}${s.nextVersion !== void 0 ? ` → suggest ${s.nextVersion}` : ""}`);
|
|
26
|
+
return `${this.count} published schema(s) drifted; nothing was written.\n${lines.join("\n")}\nBump the drifting versions in the config, or re-run with --force to write anyway.`;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* A schema failed the lint/validation gate, so nothing was written. Exit `1`.
|
|
31
|
+
*
|
|
32
|
+
* @public
|
|
33
|
+
*/
|
|
34
|
+
var GateError = class extends Schema.TaggedError()("GateError", { count: Schema.Number }) {
|
|
35
|
+
get message() {
|
|
36
|
+
return `${this.count} schema(s) failed the lint/validation gate; nothing was written.`;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* The merged catalog could not be assembled: a catalog URL or entry name is advertised more than once across the
|
|
41
|
+
* slices, or a slice is not a catalog entry array. Nothing is merged silently, so the merged catalog was left as it
|
|
42
|
+
* is. Exit `1` under both `build` and `check`: only an edit to the slices or the configs clears it.
|
|
43
|
+
*
|
|
44
|
+
* @public
|
|
45
|
+
*/
|
|
46
|
+
var CatalogMergeError = class extends Schema.TaggedError()("CatalogMergeError", {
|
|
47
|
+
path: Schema.String,
|
|
48
|
+
conflicts: Schema.Array(Schema.Union([Schema.Struct({
|
|
49
|
+
kind: Schema.Literal("url"),
|
|
50
|
+
url: Schema.String,
|
|
51
|
+
slices: Schema.Array(Schema.String)
|
|
52
|
+
}), Schema.Struct({
|
|
53
|
+
kind: Schema.Literal("name"),
|
|
54
|
+
name: Schema.String,
|
|
55
|
+
slices: Schema.Array(Schema.String)
|
|
56
|
+
})])),
|
|
57
|
+
invalid: Schema.Array(Schema.Struct({
|
|
58
|
+
path: Schema.String,
|
|
59
|
+
reason: Schema.String
|
|
60
|
+
}))
|
|
61
|
+
}) {
|
|
62
|
+
get message() {
|
|
63
|
+
const lines = [...this.conflicts.map((conflict) => ` ${conflict.kind === "url" ? `url ${conflict.url}` : `name ${conflict.name}`} is advertised by ${conflict.slices.join(", ")}`), ...this.invalid.map(({ path, reason }) => ` ${path} is invalid: ${reason}`)];
|
|
64
|
+
return `The merged catalog ${this.path} was not written.\n${lines.join("\n")}\nGive each catalog URL and name to exactly one config, and fix or delete every invalid slice.`;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* `check` found committed documents that differ from what the config generates, or outputs nothing claims, which
|
|
69
|
+
* `build` never deletes. `check` is the CI drift gate, so a stale tree fails it. Exit `1`. `count` is every finding;
|
|
70
|
+
* `orphaned` the part of it a build cannot clear, so the message names both remedies.
|
|
71
|
+
*
|
|
72
|
+
* @public
|
|
73
|
+
*/
|
|
74
|
+
var StaleError = class extends Schema.TaggedError()("StaleError", {
|
|
75
|
+
count: Schema.Number,
|
|
76
|
+
orphaned: Schema.optionalKey(Schema.Number)
|
|
77
|
+
}) {
|
|
78
|
+
get message() {
|
|
79
|
+
const orphaned = this.orphaned ?? 0;
|
|
80
|
+
const stale = this.count - orphaned;
|
|
81
|
+
return [...stale > 0 ? [`${stale} document(s) are stale; run \`effected schema build\` and commit the result.`] : [], ...orphaned > 0 ? [`${orphaned} orphaned output(s) must be deleted by hand; build never will.`] : []].join(" ");
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* The payload does not conform to the resolved document: the engine answered findings, each already reported. Exit
|
|
86
|
+
* `1`: a validation verdict, the reason CI runs the command.
|
|
87
|
+
*
|
|
88
|
+
* @public
|
|
89
|
+
*/
|
|
90
|
+
var ValidationFailedError = class extends Schema.TaggedError()("ValidationFailedError", {
|
|
91
|
+
count: Schema.Number,
|
|
92
|
+
payload: Schema.String,
|
|
93
|
+
schema: Schema.String
|
|
94
|
+
}) {
|
|
95
|
+
get message() {
|
|
96
|
+
return `${this.count} finding(s): ${this.payload} does not conform to ${this.schema}`;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* No schemastore config was found. Exit `2`: the command could not run, where exit `1` means it ran and found a
|
|
101
|
+
* problem. The engine's error says what was searched; this adds the way out, which is the front end's to state because
|
|
102
|
+
* it shows the command that was run.
|
|
103
|
+
*
|
|
104
|
+
* @public
|
|
105
|
+
*/
|
|
106
|
+
var NoConfigError = class extends Schema.TaggedError()("NoConfigError", {
|
|
107
|
+
/** The file names looked for. */
|
|
108
|
+
names: Schema.Array(Schema.String),
|
|
109
|
+
/** The directories walked, or the one explicit path checked. */
|
|
110
|
+
searched: Schema.Array(Schema.String),
|
|
111
|
+
/** Whether `searched` is the one path the user named. */
|
|
112
|
+
explicit: Schema.Boolean,
|
|
113
|
+
/** What follows `effected schema` in the example: the subcommand and its positionals, with a config path. */
|
|
114
|
+
usage: Schema.String
|
|
115
|
+
}) {
|
|
116
|
+
get message() {
|
|
117
|
+
return `${this.explicit ? `The schemastore config ${this.searched.join(", ")} does not exist.` : `No schemastore config found. Looked for ${this.names.join(", ")} in: ${this.searched.join(", ")}.`}\nRun next to a config, or pass its path, for example: effected schema ${this.usage}`;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export { CatalogMergeError, DriftError, GateError, NoConfigError, StaleError, ValidationFailedError };
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { DiffDoc } from "../../render/DiffDoc.js";
|
|
2
|
+
import { CatalogMergeError, DriftError, GateError, NoConfigError, StaleError } from "./errors.js";
|
|
3
|
+
import { CliInteractive, CliRuntime, Doc } from "@effected/cli-kit";
|
|
4
|
+
import { ConfigLoadError, ConfigLoader, Report, SchemaRun, StepSummary } from "@effected/engine/schema";
|
|
5
|
+
import { Audience } from "@effected/env";
|
|
6
|
+
import { Console, Effect, Option } from "effect";
|
|
7
|
+
|
|
8
|
+
// src/commands/schema/execute.ts
|
|
9
|
+
/** `--format` as given, else what the audience reads: JSON for an agent, human text for a person and for CI. */
|
|
10
|
+
const resolveFormat = (format) => Effect.map(Audience, (audience) => Option.getOrElse(format, () => audience.kind === "agent" ? "json" : "human"));
|
|
11
|
+
/** Warned before the walk, because an error from the walk does not carry the mismatch. */
|
|
12
|
+
const warnVersionMismatch = (mismatch) => Effect.logWarning(`VERSION MISMATCH IGNORED (--ignore-version-mismatch): ${mismatch.message}`);
|
|
13
|
+
/** A missing config, with the way out; a load failure's first line, with the rest (a stack) kept at debug. */
|
|
14
|
+
const configFailures = (usage, config) => ({
|
|
15
|
+
ConfigNotFoundError: (error) => Effect.fail(CliRuntime.reported(new NoConfigError({
|
|
16
|
+
names: ConfigLoader.CONFIG_NAMES,
|
|
17
|
+
searched: error.searched,
|
|
18
|
+
explicit: Option.isSome(config),
|
|
19
|
+
usage
|
|
20
|
+
}), 2)),
|
|
21
|
+
ConfigLoadError: (error) => Effect.gen(function* () {
|
|
22
|
+
const [first = "", ...rest] = error.reason.split("\n");
|
|
23
|
+
if (rest.length > 0) yield* Effect.logDebug(rest.join("\n"));
|
|
24
|
+
return yield* Effect.fail(CliRuntime.reported(new ConfigLoadError({
|
|
25
|
+
path: error.path,
|
|
26
|
+
reason: first
|
|
27
|
+
}), 2));
|
|
28
|
+
})
|
|
29
|
+
});
|
|
30
|
+
const LINE_TOKENS = [
|
|
31
|
+
["DRIFT", "error"],
|
|
32
|
+
["GATE FAILED", "error"],
|
|
33
|
+
["CATALOG BLOCKED", "error"],
|
|
34
|
+
["would write", "warning"],
|
|
35
|
+
["orphaned", "warning"],
|
|
36
|
+
["written", "success"],
|
|
37
|
+
["unchanged", "muted"],
|
|
38
|
+
["held", "muted"]
|
|
39
|
+
];
|
|
40
|
+
const paintedLine = (line) => {
|
|
41
|
+
const token = LINE_TOKENS.find(([prefix]) => line.startsWith(prefix))?.[1];
|
|
42
|
+
return token === void 0 ? Doc.text(line) : Doc.text(line, token);
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* The human report: the outcome lines, each changed document's diff, then the summary, which `Report.human` writes
|
|
46
|
+
* last and which stays at the bottom where a person's eye lands. `Doc.text` is data to every renderer, so a path or a
|
|
47
|
+
* diff line cannot repaint the terminal.
|
|
48
|
+
*/
|
|
49
|
+
const humanDocument = (report) => {
|
|
50
|
+
const lines = Report.human(report);
|
|
51
|
+
const outcomes = lines.slice(0, -1);
|
|
52
|
+
const summary = lines.slice(-1);
|
|
53
|
+
return Doc.section(void 0, [
|
|
54
|
+
...outcomes.length > 0 ? [Doc.lines(outcomes.map(paintedLine), { wrap: false })] : [],
|
|
55
|
+
...report.schemas.flatMap((schema) => schema.diff !== void 0 ? [DiffDoc.render(schema.path, schema.diff)] : []),
|
|
56
|
+
Doc.lines(summary.map(paintedLine), { wrap: false })
|
|
57
|
+
]);
|
|
58
|
+
};
|
|
59
|
+
const emit = (report, format, versionMismatch) => Effect.gen(function* () {
|
|
60
|
+
if (format === "json") {
|
|
61
|
+
yield* Console.log(Report.json(report, versionMismatch));
|
|
62
|
+
for (const line of Report.human(report)) yield* Effect.logInfo(line);
|
|
63
|
+
} else yield* Doc.print([humanDocument(report)]);
|
|
64
|
+
for (const line of Report.warnings(report)) yield* Effect.logWarning(line);
|
|
65
|
+
});
|
|
66
|
+
const failOnVerdict = (result) => {
|
|
67
|
+
const { report, verdict } = result;
|
|
68
|
+
const merged = report.catalog?.merged;
|
|
69
|
+
switch (verdict) {
|
|
70
|
+
case "ok": return Effect.void;
|
|
71
|
+
case "gate-failed": {
|
|
72
|
+
const count = report.schemas.filter((schema) => schema.outcome === "gate-failed").length;
|
|
73
|
+
return Effect.fail(CliRuntime.reported(new GateError({ count }), 1));
|
|
74
|
+
}
|
|
75
|
+
case "drift": {
|
|
76
|
+
const drifted = report.schemas.filter((schema) => schema.verdict === "drift").map((schema) => ({
|
|
77
|
+
$id: schema.$id,
|
|
78
|
+
change: schema.change,
|
|
79
|
+
...schema.version !== void 0 ? { version: schema.version } : {},
|
|
80
|
+
...schema.nextVersion !== void 0 ? { nextVersion: schema.nextVersion } : {}
|
|
81
|
+
}));
|
|
82
|
+
return Effect.fail(CliRuntime.reported(new DriftError({ drifted }), 1));
|
|
83
|
+
}
|
|
84
|
+
case "catalog-blocked": return Effect.fail(CliRuntime.reported(new CatalogMergeError({
|
|
85
|
+
path: merged?.path ?? "",
|
|
86
|
+
conflicts: merged?.conflicts ?? [],
|
|
87
|
+
invalid: merged?.invalid ?? []
|
|
88
|
+
}), 1));
|
|
89
|
+
case "stale": {
|
|
90
|
+
const catalogFiles = [report.catalog?.slice?.outcome, merged?.outcome];
|
|
91
|
+
const orphaned = catalogFiles.filter((outcome) => outcome === "orphaned").length + (report.orphaned?.length ?? 0);
|
|
92
|
+
const count = report.schemas.filter((schema) => schema.outcome === "would-write").length + catalogFiles.filter((outcome) => outcome === "would-write").length + orphaned;
|
|
93
|
+
return Effect.fail(CliRuntime.reported(new StaleError({
|
|
94
|
+
count,
|
|
95
|
+
...orphaned > 0 ? { orphaned } : {}
|
|
96
|
+
}), 1));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Run one `build` or `check`.
|
|
102
|
+
*
|
|
103
|
+
* @remarks
|
|
104
|
+
* The live view is for a person watching, so it needs human output and an interactive run, which `CliInteractive` only
|
|
105
|
+
* ever answers `true` for a human audience with terminals. That module, and Ink and React with it, is imported only
|
|
106
|
+
* then: an agent, CI or piped run never loads them. The report is emitted first and the failure follows, so a run that
|
|
107
|
+
* fails has still said why.
|
|
108
|
+
*/
|
|
109
|
+
const execute = Effect.fn("effected.schema.execute")(function* (mode, input, deps) {
|
|
110
|
+
const format = yield* resolveFormat(input.format);
|
|
111
|
+
const interactive = yield* CliInteractive;
|
|
112
|
+
const announce = (event) => Effect.gen(function* () {
|
|
113
|
+
if (event._tag !== "Started") return;
|
|
114
|
+
if (input.force) yield* Effect.logWarning(`--force: drift policy is allow for this run; a published document ${mode === "check" ? "would be" : "may be"} rewritten in place, which breaks every consumer pinned to its URL.`);
|
|
115
|
+
if (event.versionMismatch !== void 0) yield* warnVersionMismatch(event.versionMismatch);
|
|
116
|
+
});
|
|
117
|
+
const walk = (onProgress) => SchemaRun.run({
|
|
118
|
+
mode,
|
|
119
|
+
force: input.force,
|
|
120
|
+
versionMismatch: input.ignoreVersionMismatch ? "warn" : "fail",
|
|
121
|
+
...Option.isSome(input.config) ? { config: input.config.value } : {},
|
|
122
|
+
...Option.isSome(input.drift) ? { drift: input.drift.value } : {},
|
|
123
|
+
...Option.isSome(input.onDrift) ? { onDrift: input.onDrift.value } : {}
|
|
124
|
+
}, {
|
|
125
|
+
...deps,
|
|
126
|
+
onProgress: (event) => Effect.andThen(announce(event), onProgress(event))
|
|
127
|
+
});
|
|
128
|
+
const result = yield* (format === "human" && interactive ? Effect.flatMap(Effect.promise(() => import("./LiveProgress.js")), ({ withLiveProgress }) => withLiveProgress(walk)) : walk(() => Effect.void)).pipe(Effect.catchTags({
|
|
129
|
+
...configFailures(`${mode} lib/schemastore.config.ts`, input.config),
|
|
130
|
+
ConflictingFlagsError: (error) => Effect.fail(CliRuntime.reported(error, 64)),
|
|
131
|
+
VersionMismatchError: (error) => Effect.fail(CliRuntime.reported(error, 2)),
|
|
132
|
+
FrozenVersionMissingError: (error) => Effect.fail(CliRuntime.reported(error, 1)),
|
|
133
|
+
FrozenVersionIdMismatchError: (error) => Effect.fail(CliRuntime.reported(error, 1)),
|
|
134
|
+
CatalogDirError: (error) => Effect.fail(CliRuntime.reported(error, 2))
|
|
135
|
+
}));
|
|
136
|
+
yield* emit(result.report, format, result.versionMismatch);
|
|
137
|
+
yield* StepSummary.append(Report.markdown(result.report));
|
|
138
|
+
yield* failOnVerdict(result);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
export { configFailures, execute, resolveFormat, warnVersionMismatch };
|