@effected/cli-kit 0.0.0-stage → 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.
Files changed (103) hide show
  1. package/Cancelled.js +43 -0
  2. package/CliAudience.js +177 -0
  3. package/CliColor.js +66 -0
  4. package/CliEnv.js +101 -0
  5. package/CliExit.js +117 -0
  6. package/CliFailure.js +287 -0
  7. package/CliFormat.js +124 -0
  8. package/CliInteractive.js +94 -0
  9. package/CliLinks.js +153 -0
  10. package/CliLog.js +367 -0
  11. package/CliLogger.js +91 -0
  12. package/CliMessage.js +79 -0
  13. package/CliPrompt.js +110 -0
  14. package/CliRuntime.js +254 -0
  15. package/CliTest.js +240 -0
  16. package/CliTheme.js +142 -0
  17. package/ConfigIssueRenderer.js +62 -0
  18. package/Doc.js +548 -0
  19. package/Fmt.js +295 -0
  20. package/GithubAnnotation.js +39 -0
  21. package/Glyphs.js +82 -0
  22. package/LICENSE +21 -0
  23. package/NotInteractive.js +41 -0
  24. package/README.md +213 -2
  25. package/Render.js +280 -0
  26. package/SchemaIssueRenderer.js +47 -0
  27. package/Status.js +165 -0
  28. package/TestTerminal.js +107 -0
  29. package/Token.js +70 -0
  30. package/index.d.ts +4035 -0
  31. package/index.js +27 -0
  32. package/internal/ExitRequested.js +21 -0
  33. package/internal/HelpRouting.js +63 -0
  34. package/internal/ansi.js +229 -0
  35. package/internal/autoFormat.js +33 -0
  36. package/internal/canPrompt.js +14 -0
  37. package/internal/counts.js +83 -0
  38. package/internal/diagnostics.js +31 -0
  39. package/internal/displayPath.js +46 -0
  40. package/internal/displayWidth.js +42 -0
  41. package/internal/failureTarget.js +195 -0
  42. package/internal/fallbackAnswer.js +17 -0
  43. package/internal/fileSink.js +61 -0
  44. package/internal/format.js +173 -0
  45. package/internal/inspect.js +40 -0
  46. package/internal/isExitCode.js +11 -0
  47. package/internal/layout.js +249 -0
  48. package/internal/linkScheme.js +29 -0
  49. package/internal/linkTarget.js +49 -0
  50. package/internal/logSafety.js +45 -0
  51. package/internal/renderAnsi.js +51 -0
  52. package/internal/renderDoc.js +322 -0
  53. package/internal/renderGithubLog.js +59 -0
  54. package/internal/renderMarkdown.js +367 -0
  55. package/internal/renderPlain.js +49 -0
  56. package/internal/scanAudience.js +105 -0
  57. package/internal/splitFrame.js +55 -0
  58. package/internal/terminalCapabilities.js +23 -0
  59. package/internal/wizardGate.js +17 -0
  60. package/package.json +87 -5
  61. package/testing.d.ts +283 -0
  62. package/testing.js +4 -0
  63. package/tsdoc-metadata.json +11 -0
  64. package/ui/CliUi.js +509 -0
  65. package/ui/CliUiDetached.js +36 -0
  66. package/ui/CliUiLive.js +436 -0
  67. package/ui/CliUiRouteStream.js +72 -0
  68. package/ui/Confirm.js +244 -0
  69. package/ui/DocView.js +76 -0
  70. package/ui/KeyHelp.js +61 -0
  71. package/ui/KeyTable.js +198 -0
  72. package/ui/MultiSelect.js +259 -0
  73. package/ui/Select.js +229 -0
  74. package/ui/Tabs.js +201 -0
  75. package/ui/TextInput.js +289 -0
  76. package/ui/Toggle.js +31 -0
  77. package/ui/UiKey.js +48 -0
  78. package/ui/UiProvider.js +62 -0
  79. package/ui/UiStreams.js +17 -0
  80. package/ui/UiTheme.js +166 -0
  81. package/ui/Viewport.js +203 -0
  82. package/ui/internal/ErrorBoundary.js +29 -0
  83. package/ui/internal/Holder.js +73 -0
  84. package/ui/internal/ScreenContext.js +51 -0
  85. package/ui/internal/UiProviders.js +38 -0
  86. package/ui/internal/ink.js +121 -0
  87. package/ui/internal/inkChalk.js +47 -0
  88. package/ui/internal/inkConsole.js +150 -0
  89. package/ui/internal/lazyView.js +73 -0
  90. package/ui/internal/lineRouter.js +70 -0
  91. package/ui/internal/lineText.js +18 -0
  92. package/ui/internal/mountPermit.js +15 -0
  93. package/ui/internal/processStreams.js +18 -0
  94. package/ui/internal/renderOptions.js +12 -0
  95. package/ui/testing/CliUiTest.js +792 -0
  96. package/ui/testing/fakeStreams.js +104 -0
  97. package/ui/testing/terminalModel.js +67 -0
  98. package/ui-testing-serializer.d.ts +12 -0
  99. package/ui-testing-serializer.js +32 -0
  100. package/ui-testing.d.ts +558 -0
  101. package/ui-testing.js +3 -0
  102. package/ui.d.ts +2013 -0
  103. package/ui.js +17 -0
package/package.json CHANGED
@@ -1,6 +1,88 @@
1
1
  {
2
- "name": "@effected/cli-kit",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@effected/cli-kit",
3
+ "version": "0.27.0",
4
+ "private": false,
5
+ "description": "The presentation boundary of an effect/cli program: audience-aware output, a document IR and renderers, editor links, failure reports and logging, plus opt-in Ink screens, widgets and a live view",
6
+ "keywords": [
7
+ "effect",
8
+ "cli",
9
+ "logger",
10
+ "terminal",
11
+ "ink",
12
+ "tui"
13
+ ],
14
+ "homepage": "https://github.com/spencerbeggs/effected/tree/main/packages/cli-kit#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/spencerbeggs/effected/issues"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/spencerbeggs/effected.git",
21
+ "directory": "packages/cli-kit"
22
+ },
23
+ "license": "MIT",
24
+ "author": {
25
+ "name": "C. Spencer Beggs",
26
+ "email": "spencer@beggs.codes",
27
+ "url": "https://spencerbeg.gs"
28
+ },
29
+ "sideEffects": false,
30
+ "type": "module",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./index.d.ts",
34
+ "import": "./index.js",
35
+ "default": "./index.js"
36
+ },
37
+ "./testing": {
38
+ "types": "./testing.d.ts",
39
+ "import": "./testing.js",
40
+ "default": "./testing.js"
41
+ },
42
+ "./ui": {
43
+ "types": "./ui.d.ts",
44
+ "import": "./ui.js",
45
+ "default": "./ui.js"
46
+ },
47
+ "./ui/testing": {
48
+ "types": "./ui-testing.d.ts",
49
+ "import": "./ui-testing.js",
50
+ "default": "./ui-testing.js"
51
+ },
52
+ "./ui/testing/serializer": {
53
+ "types": "./ui-testing-serializer.d.ts",
54
+ "import": "./ui-testing-serializer.js",
55
+ "default": "./ui-testing-serializer.js"
56
+ },
57
+ "./package.json": "./package.json"
58
+ },
59
+ "dependencies": {
60
+ "@effected/commands": "^0.20.0",
61
+ "@effected/github-commands": "^0.9.0",
62
+ "@effected/kit": "^0.13.0",
63
+ "@effected/source": "^0.4.0"
64
+ },
65
+ "peerDependencies": {
66
+ "@effected/env": "^0.11.0",
67
+ "@effected/glob": "^0.19.0",
68
+ "@effected/walker": "^0.25.1",
69
+ "@types/react": "^19.3.0",
70
+ "effect": "~4.1.0",
71
+ "ink": "^8.0.0",
72
+ "react": "^19.3.0"
73
+ },
74
+ "peerDependenciesMeta": {
75
+ "@types/react": {
76
+ "optional": true
77
+ },
78
+ "ink": {
79
+ "optional": true
80
+ },
81
+ "react": {
82
+ "optional": true
83
+ }
84
+ },
85
+ "engines": {
86
+ "node": ">=24.11.0"
87
+ }
88
+ }
package/testing.d.ts ADDED
@@ -0,0 +1,283 @@
1
+ import { Duration, Effect, FileSystem, Layer, Path, PlatformError, Schema, Scope, Stream, Terminal } from "effect";
2
+ import { ChildProcess, ChildProcessSpawner } from "effect/process";
3
+ /**
4
+ * A hermetic temp directory minted by {@link CliTest.sandbox}, removed when its
5
+ * scope closes.
6
+ *
7
+ * @public
8
+ */
9
+ interface Sandbox {
10
+ /** The temp directory itself; the default working directory of {@link CliTest.run}. */
11
+ readonly root: string;
12
+ /** The fresh `HOME` inside `root`; the XDG base directories live under it. */
13
+ readonly home: string;
14
+ /**
15
+ * The complete child environment: `HOME`, the four `XDG_*_HOME` variables,
16
+ * the injected `PATH` and `NO_COLOR=1`. Nothing is inherited from the host.
17
+ */
18
+ readonly env: Readonly<Record<string, string>>;
19
+ }
20
+ /**
21
+ * How {@link CliTest.run} spawns a bin.
22
+ *
23
+ * @public
24
+ */
25
+ interface RunOptions {
26
+ /** The sandbox whose environment and root the child runs in. */
27
+ readonly sandbox: Sandbox;
28
+ /** The node binary — pass `process.execPath` from the test file, never a PATH lookup. */
29
+ readonly execPath: string;
30
+ /** The child's working directory. Defaults to the sandbox `root`. */
31
+ readonly cwd?: string | undefined;
32
+ /**
33
+ * Extra environment variables, merged over the sandbox environment — the
34
+ * way to override `PATH` for one run.
35
+ */
36
+ readonly env?: Readonly<Record<string, string>> | undefined;
37
+ /**
38
+ * Text written to the child's stdin, which is then closed. Omitted or `""`,
39
+ * the child gets an already-ended empty input, never an open pipe.
40
+ */
41
+ readonly stdin?: string | undefined;
42
+ /**
43
+ * How long a child has to exit after `SIGTERM` (or the signal `kill` sends), when its scope closes or
44
+ * {@link SpawnedBin.kill} is called, before it is sent `SIGKILL`; `2 seconds` by default. A finite duration: a
45
+ * child that ignores the signal would otherwise keep the scope from closing forever, and leave the process behind.
46
+ */
47
+ readonly forceKillAfter?: Duration.Input | undefined;
48
+ /**
49
+ * The exit code the run must end with. Any other ending, a signal included, fails with a {@link CliTestFailure}
50
+ * carrying the code, the signal and the child's stderr in its message. Omitted, a non-zero exit is data.
51
+ */
52
+ readonly expectExit?: number | undefined;
53
+ }
54
+ declare const CliTestFailure_base: Schema.Class<CliTestFailure, Schema.TaggedStruct<"CliTestFailure", {
55
+ readonly expected: Schema.Number;
56
+ readonly exitCode: Schema.NullOr<Schema.Number>;
57
+ readonly signal: Schema.NullOr<Schema.String>;
58
+ readonly stderr: Schema.String;
59
+ }>, import("effect/Cause").YieldableError>;
60
+ /**
61
+ * A run ended otherwise than {@link RunOptions.expectExit} required.
62
+ *
63
+ * @remarks
64
+ * Audience: the person reading a failed test. The message holds what the child did and its stderr, so a suite that
65
+ * only cares about the happy path needs no hand-rolled helper to see why it did not get it.
66
+ *
67
+ * @public
68
+ */
69
+ export declare class CliTestFailure extends CliTestFailure_base {
70
+ /**
71
+ * What was expected, what happened, and the child's stderr.
72
+ *
73
+ * @remarks
74
+ * Assigning to it is ignored: a runner or logger that rewrites `error.message` must not make this error throw,
75
+ * which a getter-only property does in strict mode.
76
+ */
77
+ get message(): string;
78
+ set message(_value: string);
79
+ }
80
+ /**
81
+ * What a spawned bin did, as data: a non-zero exit is a result, not a failure.
82
+ *
83
+ * @public
84
+ */
85
+ interface RunResult {
86
+ /** The child's exit code, or `null` when a signal ended it. */
87
+ readonly exitCode: number | null;
88
+ /** The signal that ended the child, or `null` when it exited with a code. */
89
+ readonly signal: ChildProcess.Signal | null;
90
+ /** Everything the child wrote to stdout, decoded as UTF-8. */
91
+ readonly stdout: string;
92
+ /** Everything the child wrote to stderr, decoded as UTF-8. */
93
+ readonly stderr: string;
94
+ }
95
+ /**
96
+ * A bin {@link CliTest.spawn} started and has not waited for: its output as it arrives, a way to signal it, and its
97
+ * exit.
98
+ *
99
+ * @public
100
+ */
101
+ interface SpawnedBin {
102
+ /**
103
+ * The child's stdout as lines, each without its line break (`\r\n` included), from the first byte it wrote,
104
+ * ending when the child closes stdout; a last line with no line break comes when it does. Every subscription replays
105
+ * from the start, so a test may read it more than once, and output a test never reads is still drained, so the
106
+ * child never blocks on a full pipe.
107
+ */
108
+ readonly stdout: Stream.Stream<string>;
109
+ /** The child's stderr as lines, as `stdout` gives stdout. */
110
+ readonly stderr: Stream.Stream<string>;
111
+ /**
112
+ * Send `signal` (`SIGTERM` by default) and wait for the child to exit. A child still running after
113
+ * {@link RunOptions.forceKillAfter} is sent `SIGKILL`, so this always ends. A child that has already exited, on its
114
+ * own or at the same moment, is not an error: this returns normally, and `exitCode` and `result` keep reporting how
115
+ * it ended. A kill that fails on a child still running fails with that `PlatformError`.
116
+ */
117
+ readonly kill: (signal?: ChildProcess.Signal) => Effect.Effect<void, PlatformError.PlatformError>;
118
+ /**
119
+ * The child's exit code once it exits, or `null` when a signal ended it (the default `kill` included): `result`
120
+ * names the signal. Re-reading it after the exit answers at once.
121
+ */
122
+ readonly exitCode: Effect.Effect<number | null, PlatformError.PlatformError>;
123
+ /** Wait for the exit and the end of both streams, and read everything, as {@link CliTest.run} returns it. */
124
+ readonly result: Effect.Effect<RunResult, PlatformError.PlatformError>;
125
+ }
126
+ /**
127
+ * Spawn a built CLI bin hermetically and read its exit code and streams as data.
128
+ *
129
+ * @example
130
+ * ```ts
131
+ * import * as NodeServices from "@effect/platform-node/NodeServices"
132
+ * import { assert, it } from "@effect/vitest"
133
+ * import { CliTest } from "@effected/cli-kit/testing"
134
+ * import { Effect } from "effect"
135
+ *
136
+ * it.effect("prints its version", () =>
137
+ * Effect.gen(function* () {
138
+ * const sandbox = yield* CliTest.sandbox({ path: process.env.PATH ?? "" })
139
+ * const result = yield* CliTest.run("dist/bin.js", ["--version"], { sandbox, execPath: process.execPath })
140
+ * assert.strictEqual(result.exitCode, 0)
141
+ * }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
142
+ * )
143
+ * ```
144
+ *
145
+ * @public
146
+ */
147
+ export declare class CliTest {
148
+ private constructor();
149
+ /**
150
+ * A scoped temp directory with a fresh `HOME` and XDG tree, `NO_COLOR=1`,
151
+ * and the `PATH` you pass — the host environment is never inherited.
152
+ *
153
+ * @remarks
154
+ * Each call mints a fresh temp directory; bind the result to a `const`
155
+ * within one test rather than calling this more than once per assertion.
156
+ */
157
+ static readonly sandbox: (options: {
158
+ /**
159
+ * The `PATH` the child sees, passed explicitly because nothing else is
160
+ * inherited — `process.env.PATH` when the bin shells out to host tools,
161
+ * a narrower list to prove it does not.
162
+ */
163
+ readonly path: string;
164
+ }) => Effect.Effect<Sandbox, PlatformError.PlatformError, FileSystem.FileSystem | Path.Path | Scope.Scope>;
165
+ /**
166
+ * Run `execPath bin ...args`; a non-zero exit is returned, never failed, unless `expectExit` is set.
167
+ *
168
+ * @remarks
169
+ * `stdin` is never left as an inherited open pipe: when omitted or `""`
170
+ * the child gets an already-ended empty input (`Stream.empty`), so a
171
+ * stdin-reading bin exits instead of hanging on `effect/process`'s
172
+ * default `"pipe"` stdio, which stays open until something writes to and
173
+ * ends it.
174
+ */
175
+ static readonly run: (bin: string, args: ReadonlyArray<string>, options: RunOptions) => Effect.Effect<RunResult, PlatformError.PlatformError | CliTestFailure, ChildProcessSpawner.ChildProcessSpawner>;
176
+ /**
177
+ * Start `execPath bin ...args` and return at once with a handle on it: its output as lines while it runs, a signal,
178
+ * and its exit, for a long-running command (a `--follow`, a watch mode, a server) that {@link CliTest.run} would wait
179
+ * on forever.
180
+ *
181
+ * @remarks
182
+ * The child lives in the caller's scope: when the scope closes first, the child is sent `SIGTERM`, then `SIGKILL`
183
+ * after `forceKillAfter` (`2 seconds` by default), so a test that ends early leaves nothing running and never waits
184
+ * on a child that ignores the signal. A child a signal ended reports `exitCode: null` and the signal in `result`. Both streams are drained from the start whether or not the test reads them, so a
185
+ * chatty child never blocks on a full pipe, and each stream replays from its first line to every reader. The
186
+ * options are {@link CliTest.run}'s, `stdin` included: the child gets that text, then an ended input.
187
+ *
188
+ * ```ts
189
+ * const child = yield* CliTest.spawn("dist/bin.js", ["logs", "--follow"], { sandbox, execPath: process.execPath })
190
+ * yield* Stream.runHead(child.stdout) // the first line: it is following
191
+ * yield* child.kill("SIGINT")
192
+ * assert.strictEqual(yield* child.exitCode, 0)
193
+ * ```
194
+ *
195
+ * @param bin - the built bin to run
196
+ * @param args - its arguments
197
+ * @param options - the sandbox, the node binary, and the working directory, environment and stdin
198
+ */
199
+ static readonly spawn: (bin: string, args: ReadonlyArray<string>, options: RunOptions) => Effect.Effect<SpawnedBin, PlatformError.PlatformError, ChildProcessSpawner.ChildProcessSpawner | Scope.Scope>;
200
+ }
201
+ /**
202
+ * One key press for {@link TestTerminal}.
203
+ *
204
+ * @public
205
+ */
206
+ interface KeyInput {
207
+ /** The key name the prompts switch on: `down`, `up`, `enter`, `space`, `escape`, or a character. */
208
+ readonly name: string;
209
+ /** Whether Ctrl is held. */
210
+ readonly ctrl?: boolean | undefined;
211
+ /** Whether Meta is held. */
212
+ readonly meta?: boolean | undefined;
213
+ /** Whether Shift is held. */
214
+ readonly shift?: boolean | undefined;
215
+ }
216
+ /**
217
+ * What {@link TestTerminal.make} builds: the `Terminal` layer and the means to drive and inspect it.
218
+ *
219
+ * @public
220
+ */
221
+ interface TestTerminalHandle {
222
+ /** Provides `Terminal` backed by this double. */
223
+ readonly layer: Layer.Layer<Terminal.Terminal>;
224
+ /** Queue key presses, as if the user had typed them. */
225
+ readonly input: (keys: ReadonlyArray<KeyInput>) => Effect.Effect<void>;
226
+ /** Queue `text` one character at a time. */
227
+ readonly type: (text: string) => Effect.Effect<void>;
228
+ /** End the input, as Ctrl-C or end-of-file does: a prompt waiting for a key is quit. */
229
+ readonly end: Effect.Effect<void>;
230
+ /** Everything written to the terminal so far, prompt frames and escape codes included. */
231
+ readonly output: Effect.Effect<string>;
232
+ /**
233
+ * Resize the terminal: `columns` and `rows` report the new size from now on, and every open `size`
234
+ * subscription receives it.
235
+ */
236
+ readonly resize: (size: Terminal.Size) => Effect.Effect<void>;
237
+ /** How many queued key presses nobody has taken yet. */
238
+ readonly pending: Effect.Effect<number>;
239
+ /**
240
+ * What the program did with the input: `keys` taken from it, `lines` read with `readLine`, and
241
+ * `subscriptions` to `readInput`. All `0` proves a code path never touched the terminal's input. Counting the
242
+ * subscription matters: on a real terminal merely subscribing attaches a reader to stdin, even when no key is
243
+ * ever taken.
244
+ */
245
+ readonly reads: Effect.Effect<{
246
+ readonly keys: number;
247
+ readonly lines: number;
248
+ readonly subscriptions: number;
249
+ }>;
250
+ }
251
+ /**
252
+ * A scripted `Terminal` for testing prompts and anything that reads the terminal.
253
+ *
254
+ * @remarks
255
+ * Queue keys with `input` or `type`, run the program under `layer`, then read `output`. `size` behaves as a real
256
+ * terminal's does: each subscription starts with the current size, receives every `resize`, keeps only the latest,
257
+ * and ends when its scope closes. To assert that a code path
258
+ * did NOT touch the terminal, queue some keys first and check `reads` is all zero (no subscription, no key, no
259
+ * line) and `pending` is unchanged afterwards. Only
260
+ * available from `@effected/cli-kit/testing`.
261
+ *
262
+ * @public
263
+ */
264
+ export declare class TestTerminal {
265
+ private constructor();
266
+ /**
267
+ * Build a test terminal.
268
+ *
269
+ * @remarks
270
+ * With `forbidInput`, `readInput` and `readLine` die with a message saying this path must not read the terminal,
271
+ * where the default double hands back an empty queue (a stray read hangs) and quits `readLine`. `size`, `columns`,
272
+ * `rows`, `display` and `resize` behave as ever, so a "this path never prompts" platform keeps a real terminal size.
273
+ *
274
+ * @param options - the reported size, 80 by 24 by default; `forbidInput` to die on any input read
275
+ */
276
+ static readonly make: (options?: {
277
+ readonly columns?: number | undefined;
278
+ readonly rows?: number | undefined;
279
+ readonly forbidInput?: boolean | undefined;
280
+ }) => Effect.Effect<TestTerminalHandle>;
281
+ }
282
+ export type { KeyInput, RunOptions, RunResult, Sandbox, SpawnedBin, TestTerminalHandle };
283
+ //# sourceMappingURL=testing.d.ts.map
package/testing.js ADDED
@@ -0,0 +1,4 @@
1
+ import { CliTest, CliTestFailure } from "./CliTest.js";
2
+ import { TestTerminal } from "./TestTerminal.js";
3
+
4
+ export { CliTest, CliTestFailure, TestTerminal };
@@ -0,0 +1,11 @@
1
+ // This file is read by tools that parse documentation comments conforming to the TSDoc standard.
2
+ // It should be published with your NPM package. It should not be tracked by Git.
3
+ {
4
+ "tsdocVersion": "0.12",
5
+ "toolPackages": [
6
+ {
7
+ "packageName": "@microsoft/api-extractor",
8
+ "packageVersion": "7.60.1"
9
+ }
10
+ ]
11
+ }