@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.
Files changed (116) hide show
  1. package/README.md +5 -208
  2. package/bin/effected.js +7 -0
  3. package/commands/root.js +14 -0
  4. package/commands/schema/LiveProgress.js +114 -0
  5. package/commands/schema/build.js +9 -0
  6. package/commands/schema/check.js +12 -0
  7. package/commands/schema/errors.js +121 -0
  8. package/commands/schema/execute.js +141 -0
  9. package/commands/schema/flags.js +24 -0
  10. package/commands/schema/index.js +14 -0
  11. package/commands/schema/validate.js +58 -0
  12. package/index.d.ts +85 -3906
  13. package/index.js +4 -26
  14. package/main.d.ts +51 -0
  15. package/main.js +61 -0
  16. package/package.json +18 -50
  17. package/render/DiffDoc.js +51 -0
  18. package/tsdoc-metadata.json +1 -1
  19. package/version.js +11 -0
  20. package/Cancelled.js +0 -43
  21. package/CliAudience.js +0 -177
  22. package/CliColor.js +0 -59
  23. package/CliEnv.js +0 -101
  24. package/CliExit.js +0 -64
  25. package/CliFailure.js +0 -287
  26. package/CliFormat.js +0 -124
  27. package/CliInteractive.js +0 -94
  28. package/CliLinks.js +0 -153
  29. package/CliLog.js +0 -367
  30. package/CliLogger.js +0 -91
  31. package/CliMessage.js +0 -79
  32. package/CliPrompt.js +0 -110
  33. package/CliRuntime.js +0 -254
  34. package/CliTest.js +0 -206
  35. package/CliTheme.js +0 -142
  36. package/ConfigIssueRenderer.js +0 -63
  37. package/Doc.js +0 -548
  38. package/Fmt.js +0 -335
  39. package/GithubAnnotation.js +0 -39
  40. package/Glyphs.js +0 -82
  41. package/NotInteractive.js +0 -41
  42. package/Render.js +0 -280
  43. package/SchemaIssueRenderer.js +0 -47
  44. package/Status.js +0 -165
  45. package/TestTerminal.js +0 -101
  46. package/Token.js +0 -68
  47. package/internal/ExitRequested.js +0 -21
  48. package/internal/HelpRouting.js +0 -35
  49. package/internal/ansi.js +0 -229
  50. package/internal/autoFormat.js +0 -33
  51. package/internal/canPrompt.js +0 -14
  52. package/internal/counts.js +0 -83
  53. package/internal/diagnostics.js +0 -31
  54. package/internal/displayPath.js +0 -46
  55. package/internal/displayWidth.js +0 -42
  56. package/internal/failureTarget.js +0 -195
  57. package/internal/fallbackAnswer.js +0 -17
  58. package/internal/fileSink.js +0 -61
  59. package/internal/format.js +0 -175
  60. package/internal/inspect.js +0 -40
  61. package/internal/isExitCode.js +0 -11
  62. package/internal/layout.js +0 -249
  63. package/internal/linkScheme.js +0 -29
  64. package/internal/linkTarget.js +0 -49
  65. package/internal/logSafety.js +0 -45
  66. package/internal/renderAnsi.js +0 -51
  67. package/internal/renderDoc.js +0 -322
  68. package/internal/renderGithubLog.js +0 -59
  69. package/internal/renderMarkdown.js +0 -367
  70. package/internal/renderPlain.js +0 -49
  71. package/internal/scanAudience.js +0 -105
  72. package/internal/splitFrame.js +0 -55
  73. package/internal/terminalCapabilities.js +0 -23
  74. package/internal/wizardGate.js +0 -17
  75. package/testing.d.ts +0 -246
  76. package/testing.js +0 -4
  77. package/ui/CliUi.js +0 -507
  78. package/ui/CliUiDetached.js +0 -36
  79. package/ui/CliUiLive.js +0 -436
  80. package/ui/CliUiRouteStream.js +0 -72
  81. package/ui/Confirm.js +0 -244
  82. package/ui/DocView.js +0 -76
  83. package/ui/KeyHelp.js +0 -61
  84. package/ui/KeyTable.js +0 -198
  85. package/ui/MultiSelect.js +0 -259
  86. package/ui/Select.js +0 -229
  87. package/ui/Tabs.js +0 -201
  88. package/ui/TextInput.js +0 -289
  89. package/ui/Toggle.js +0 -31
  90. package/ui/UiKey.js +0 -48
  91. package/ui/UiProvider.js +0 -62
  92. package/ui/UiStreams.js +0 -17
  93. package/ui/UiTheme.js +0 -147
  94. package/ui/Viewport.js +0 -203
  95. package/ui/internal/ErrorBoundary.js +0 -29
  96. package/ui/internal/Holder.js +0 -73
  97. package/ui/internal/ScreenContext.js +0 -51
  98. package/ui/internal/UiProviders.js +0 -38
  99. package/ui/internal/ink.js +0 -121
  100. package/ui/internal/inkChalk.js +0 -47
  101. package/ui/internal/inkConsole.js +0 -150
  102. package/ui/internal/lazyView.js +0 -73
  103. package/ui/internal/lineRouter.js +0 -70
  104. package/ui/internal/lineText.js +0 -18
  105. package/ui/internal/mountPermit.js +0 -15
  106. package/ui/internal/processStreams.js +0 -18
  107. package/ui/internal/renderOptions.js +0 -12
  108. package/ui/testing/CliUiTest.js +0 -792
  109. package/ui/testing/fakeStreams.js +0 -104
  110. package/ui/testing/terminalModel.js +0 -67
  111. package/ui-testing-serializer.d.ts +0 -12
  112. package/ui-testing-serializer.js +0 -32
  113. package/ui-testing.d.ts +0 -558
  114. package/ui-testing.js +0 -3
  115. package/ui.d.ts +0 -1990
  116. package/ui.js +0 -17
package/CliEnv.js DELETED
@@ -1,101 +0,0 @@
1
- import { CliInteractive } from "./CliInteractive.js";
2
- import { ambientLinksLayer } from "./CliLinks.js";
3
- import { CliTheme } from "./CliTheme.js";
4
- import { displayPathLayer } from "./internal/displayPath.js";
5
- import { CliPrompt } from "./CliPrompt.js";
6
- import { capabilitiesLayer } from "./internal/terminalCapabilities.js";
7
- import { ConfigProvider, Layer, Option } from "effect";
8
- import { Audience, CurrentRuntimeEnv, TerminalEnv } from "@effected/env";
9
-
10
- // src/CliEnv.ts
11
- /**
12
- * The environment services a CLI reads, built once and in the right order.
13
- *
14
- * @remarks
15
- * Builds `CurrentRuntimeEnv`, `TerminalEnv`, `Audience`, `CliTheme` and `CliLinks`, and sets `CliInteractive` from them, then
16
- * installs the two gates for the program: `CliPrompt.gateTerminal`, which replaces `Terminal` with a quiet one when
17
- * the run is not interactive so no prompt runner ever attaches to stdin, and `CliPrompt.gateWizard`, which drops
18
- * `--wizard` then. `TerminalEnv` is built from the real terminal first. The layer therefore also outputs
19
- * `Terminal`, the gated one, and consumers never compose the gates themselves. A
20
- * `Context.Reference`'s key type is `never`, so the layer's output type does not list `CliInteractive`: it sets
21
- * the reference rather than providing a service. Every read of the environment goes through `Config` and
22
- * degrades to "unset" when it fails, so building the layer does not fail on a bad provider; it fails only when
23
- * `Stdio` or `Terminal` do.
24
- *
25
- * `CliLinks` reads `FileSystem` and `Path` from the surrounding context if it has them, and does not require them: a
26
- * `.vscode/` directory is looked for, and a relative path resolved, only when the platform is provided OUTSIDE this
27
- * layer, as `CliRuntime.main` does. Without them `auto` is `vscode` on the terminal signal alone.
28
- *
29
- * Not interactive, the gated `Terminal`'s `readLine` fails as a quit, its input is already ended and its `display`
30
- * writes nothing. A program that reads piped data must read `Stdio.stdin`, and one that writes output must use
31
- * `Console` or `Stdio`, never `Terminal`. It also installs `CliTheme.promptTheme`, so core's prompts follow the
32
- * theme.
33
- *
34
- * It sets core's `TerminalCapabilities` from `TerminalEnv` (through `TerminalEnv.capabilities` in `@effected/env`), so
35
- * the colour of core's own help, version and error output follows the kit's colour rule, and `canAnimate` is stdout
36
- * being a terminal that is not `dumb`. `canPrompt` alone is held at `true` whatever the terminal is: core skips a
37
- * fallback prompt entirely when it is `false`, which would lose `CliPrompt.fallback`'s and `CliUi.fallback`'s
38
- * `otherwise` in every piped run, and `CliInteractive` with `CliPrompt.gateTerminal` already owns that decision (the
39
- * gate quits any other prompt at once when the run is not interactive). A `TerminalCapabilities` with
40
- * `canPrompt: false` provided inside the program bypasses the kit's fallbacks: the parameter then fails as missing even
41
- * when an `otherwise` is given.
42
- *
43
- * @public
44
- */
45
- var CliEnv = class {
46
- constructor() {}
47
- /**
48
- * The environment services for the terminal `Stdio` and `Terminal` describe.
49
- *
50
- * @remarks
51
- * A layer-returning function mints a fresh layer per call: call it once and bind the result to a constant.
52
- *
53
- * @param options - the audience env var, the stderr check and the theme options
54
- */
55
- static layer = (options = {}) => {
56
- const base = Layer.mergeAll(CurrentRuntimeEnv.layer, TerminalEnv.layer(options.stderrIsTerminal === void 0 ? void 0 : { stderrIsTerminal: options.stderrIsTerminal }));
57
- const withAudience = Audience.layer(options.audienceEnvVar === void 0 ? void 0 : { envVar: options.audienceEnvVar }).pipe(Layer.provideMerge(base));
58
- const withTheme = CliTheme.layer(options.theme).pipe(Layer.provideMerge(withAudience));
59
- const withLinks = ambientLinksLayer({
60
- ...options.editorLinks === void 0 ? {} : { editorLinks: options.editorLinks },
61
- ...options.editorLinksEnvVar === void 0 ? {} : { envVar: options.editorLinksEnvVar }
62
- }).pipe(Layer.provideMerge(withTheme));
63
- const withCapabilities = capabilitiesLayer.pipe(Layer.provideMerge(withLinks));
64
- const withInteractive = CliInteractive.layer.pipe(Layer.provideMerge(withCapabilities));
65
- return Layer.mergeAll(CliPrompt.gateTerminal, CliPrompt.gateWizard, CliTheme.promptTheme, displayPathLayer(options.displayPath)).pipe(Layer.provideMerge(withInteractive));
66
- };
67
- /**
68
- * The environment services a test fixes, needing nothing and reading nothing of the host's: `TerminalEnv` and
69
- * `Audience` from the answers given, `CliTheme` built from them as {@link CliEnv.layer} builds it, and
70
- * `CliInteractive` set from them by the same rule (a human, every stream a terminal, and a `TERM` that is not
71
- * `dumb`), and core's `TerminalCapabilities` from them as `CliEnv.layer` sets it, `canPrompt` held at `true`.
72
- *
73
- * @remarks
74
- * `term` is handed to the theme and interactivity builds alone, through a `ConfigProvider` of their own: the
75
- * program under the layer keeps its own provider, and a host's `TERM` (a test runner in a dumb terminal) never
76
- * decides. A screen or a live view also needs `UiStreams` from `@effected/cli/ui`, which `CliUiTest` provides; this
77
- * layer provides no `Terminal` and installs neither of `CliEnv.layer`'s prompt gates.
78
- *
79
- * A layer-returning function mints a fresh layer per call: call it once and bind the result to a constant.
80
- *
81
- * @param options - whether the streams are terminals, the `TERM`, the audience, the width, the colour and the theme
82
- */
83
- static layerTest = (options = {}) => {
84
- const tty = options.tty ?? false;
85
- const stream = {
86
- isTerminal: tty,
87
- color: options.color ?? "none",
88
- columns: options.columns === void 0 ? Option.none() : Option.some(options.columns)
89
- };
90
- const facts = Layer.mergeAll(TerminalEnv.layerTest({
91
- stdinIsTerminal: tty,
92
- stdout: stream,
93
- stderr: stream
94
- }), Audience.layerTest(options.audience ?? "human"));
95
- const term = ConfigProvider.layer(ConfigProvider.fromUnknown(options.term === void 0 ? {} : { TERM: options.term }));
96
- const interactive = CliInteractive.layer.pipe(Layer.provideMerge(capabilitiesLayer));
97
- return Layer.mergeAll(CliTheme.layer(options.theme), interactive).pipe(Layer.provide(term), Layer.provideMerge(facts));
98
- };
99
- };
100
-
101
- export { CliEnv };
package/CliExit.js DELETED
@@ -1,64 +0,0 @@
1
- import { isExitCode } from "./internal/isExitCode.js";
2
- import { Context, Effect, Layer, MutableRef } from "effect";
3
-
4
- // src/CliExit.ts
5
- /**
6
- * The exit code a successful run wants, for commands whose findings are a
7
- * result rather than a failure (a linter that found problems, say).
8
- *
9
- * @remarks
10
- * Findings are not a failure — a JSON `tapError` must not fire, and the
11
- * handler must return normally — yet the process must exit non-zero. Writing
12
- * `process.exitCode` works only because Node's `runMain` skips
13
- * `process.exit(0)` on success; `process.exit(n)` in a handler skips every
14
- * finalizer. `CliRuntime.main` reads this cell after the program
15
- * succeeds and turns a non-zero code into a marked failure the runtime's
16
- * teardown honours, on any runtime, with finalizers intact.
17
- *
18
- * A `Service`, not a `Reference`: forgetting `CliRuntime.main` is a type error,
19
- * never a silently ignored global.
20
- *
21
- * @public
22
- */
23
- var CliExit = class CliExit extends Context.Service()("@effected/cli/CliExit") {
24
- /**
25
- * A fresh cell at `0`; `CliRuntime.main` provides it, and tests provide it
26
- * directly.
27
- *
28
- * @remarks
29
- * Every provide mints a new cell. Layers memoize by reference across
30
- * `Effect.provide` calls, so without `Layer.fresh` a second provide of this
31
- * layer anywhere in the program — a nested `CliRuntime.main`, a test
32
- * helper — would silently share the first run's cell and inherit its code.
33
- *
34
- * A program run under `CliRuntime.main` must NOT provide `CliExit.layer`
35
- * itself: `main` already provides one, and a second provide mints a second,
36
- * unrelated cell that `main` never reads back, so `CliExit.set` calls made
37
- * against it are silently discarded and the run exits `0`.
38
- */
39
- static layer = Layer.fresh(Layer.sync(this, () => ({ code: MutableRef.make(0) })));
40
- /**
41
- * Record an exit code; the highest code set during the run wins.
42
- *
43
- * @remarks
44
- * Highest-wins, not last-wins, so a later "clean" step cannot quietly
45
- * downgrade an earlier finding's code.
46
- *
47
- * The code must be an integer in `0..255` — the range a POSIX exit status
48
- * can carry. Anything else dies as a defect naming the value: `256` would
49
- * wrap to exit `0` and silently pass a run with findings, and a fraction
50
- * such as `1.5` makes `process.exit` throw `ERR_OUT_OF_RANGE` after the
51
- * program has finished.
52
- *
53
- * The code only applies to a run that **succeeds**. A program failure beats
54
- * findings: when the program fails, `CliRuntime.main` never reads this
55
- * cell, and the failure's own exit code (or the `exitCode` fallback) wins.
56
- */
57
- static set = (code) => Effect.gen(function* () {
58
- if (!isExitCode(code)) return yield* Effect.die(/* @__PURE__ */ new Error(`CliExit.set: exit code must be an integer 0..255, received ${code}`));
59
- const exit = yield* CliExit;
60
- if (code > MutableRef.get(exit.code)) MutableRef.set(exit.code, code);
61
- });
62
- };
63
-
64
- export { CliExit };
package/CliFailure.js DELETED
@@ -1,287 +0,0 @@
1
- import { Cancelled } from "./Cancelled.js";
2
- import { Fmt } from "./Fmt.js";
3
- import { identity } from "./internal/displayPath.js";
4
- import { Doc } from "./Doc.js";
5
- import { issueEntries, issueTreeChildren } from "./internal/format.js";
6
- import { failureText, readProperty, tagOf } from "./internal/inspect.js";
7
- import { splitFrame } from "./internal/splitFrame.js";
8
- import { NotInteractive } from "./NotInteractive.js";
9
- import { Status } from "./Status.js";
10
- import { Cause, Context, SchemaIssue } from "effect";
11
-
12
- // src/CliFailure.ts
13
- /**
14
- * The protocol an error class implements to say how a failure is shown: a method under this key that returns the
15
- * document.
16
- *
17
- * @remarks
18
- * `CliFailure.toDoc` calls it for a typed failure that has one, so an application error draws itself (a heading, a
19
- * table of what went wrong) and the default report uses it with no registration. A `Symbol.for` key, so two copies
20
- * of this package agree on it.
21
- *
22
- * @public
23
- */
24
- const CliDoc = Symbol.for("@effected/cli/CliDoc");
25
- /** The deepest an `Error.cause` chain, or a stack, is followed. */
26
- const MAX_DEPTH = 8;
27
- const MAX_SPANS = 32;
28
- const hasCliDoc = (value) => typeof value === "object" && value !== null && typeof value[CliDoc] === "function";
29
- const firstLine = (text) => text.split(/\r\n|\r|\n/, 1)[0] ?? "";
30
- /** The failure status and the text of a message: one block per line, the status on the first. */
31
- const failureBlocks = (message) => {
32
- const [first = "", ...rest] = message.split(/\r\n|\r|\n/);
33
- return [Doc.paragraph(Doc.status(Status.core, "failure"), " ", first), ...rest.map((line) => Doc.paragraph(line))];
34
- };
35
- /** The issue of a schema failure: the value itself, or the `issue` an error carries. */
36
- const issueOf = (error) => {
37
- if (SchemaIssue.isIssue(error)) return error;
38
- const issue = readProperty(error, "issue");
39
- return SchemaIssue.isIssue(issue) ? issue : void 0;
40
- };
41
- const schemaBlocks = (error) => {
42
- const entries = issueEntries(issueOf(error));
43
- if (entries.length === 0) return void 0;
44
- const header = typeof error === "object" && error !== null && !SchemaIssue.isIssue(error) ? firstLine(failureText(readProperty(error, "message") ?? "")) : "";
45
- const root = {
46
- label: [
47
- Doc.status(Status.core, "failure"),
48
- " ",
49
- header === "" ? `invalid value (${Fmt.plural(entries.length, "problem")})` : header
50
- ],
51
- children: issueTreeChildren(entries)
52
- };
53
- return [Doc.tree(root)];
54
- };
55
- /** The location of a frame as a path: a `file:` URL decoded, or an absolute POSIX or drive path; else `undefined`. */
56
- const asPath = (location) => {
57
- if (location.startsWith("file:")) try {
58
- const path = decodeURIComponent(new URL(location).pathname);
59
- return /^\/[A-Za-z]:\//.test(path) ? path.slice(1) : path;
60
- } catch {
61
- return;
62
- }
63
- return location.startsWith("/") || /^[A-Za-z]:[\\/]/.test(location) ? location : void 0;
64
- };
65
- /** A location without its `:line:col`, and the position when there is one. */
66
- const splitPosition = (location) => {
67
- const position = /^(.*):(\d+):(\d+)$/.exec(location);
68
- return position === null ? { where: location } : {
69
- where: position[1] ?? "",
70
- line: Number(position[2]),
71
- col: Number(position[3])
72
- };
73
- };
74
- const parseFrame = (raw) => {
75
- const { text, fn, location } = splitFrame(raw);
76
- const { where, line, col } = splitPosition(location);
77
- const file = asPath(where);
78
- return {
79
- raw: text,
80
- ...fn === void 0 ? {} : { fn },
81
- ...file === void 0 ? {} : { file },
82
- ...file === void 0 || line === void 0 || col === void 0 ? {} : {
83
- line,
84
- col
85
- }
86
- };
87
- };
88
- /** The file a frame ran in, as a path, or `undefined` for a frame with none: `node:`, `<anonymous>`, `native`, `eval`. */
89
- const frameFile = (raw) => asPath(splitPosition(splitFrame(raw).location).where);
90
- /** A frame under `node_modules`: a dependency's, or an installed program's own. Decided by its file alone. */
91
- const isDependency = (raw) => /[\\/]node_modules[\\/]/.test(frameFile(raw) ?? "");
92
- /**
93
- * A frame that is the runtime's or Effect's, never the program's, wherever the program is installed: one with no file
94
- * (every `node:` frame, `<anonymous>`, native), or one in Effect's own files. Decided by the file alone, never by the
95
- * function name: V8 names a program's own thunk by Effect's method alias (`boom [as ~effect/Effect/args]`).
96
- */
97
- const isRuntime = (raw) => {
98
- const file = frameFile(raw);
99
- return file === void 0 || /[\\/]node_modules[\\/]effect[\\/]/.test(file) || /[\\/]packages[\\/]effect[\\/]src[\\/]/.test(file);
100
- };
101
- /** The frames of a stack that belong to the program, and how many were left out. */
102
- const cleanStack = (stack, mode) => {
103
- if (typeof stack !== "string") return {
104
- frames: [],
105
- hidden: 0
106
- };
107
- const lines = stack.split(/\r\n|\r|\n/).filter((line) => /^\s*at\s/.test(line));
108
- const app = lines.filter((line) => !isRuntime(line) && !isDependency(line));
109
- const kept = mode === "all" ? lines : app.length > 0 ? app : lines.filter((line) => !isRuntime(line));
110
- return {
111
- frames: kept.map(parseFrame),
112
- hidden: lines.length - kept.length
113
- };
114
- };
115
- const frameBlock = (frame, displayPath) => {
116
- if (frame.file === void 0) return Doc.paragraph(Doc.text("at ", "muted"), frame.raw);
117
- const where = `${displayPath(frame.file)}${frame.line === void 0 ? "" : `:${frame.line}:${frame.col}`}`;
118
- const target = {
119
- file: frame.file,
120
- ...frame.line === void 0 ? {} : { line: frame.line },
121
- ...frame.col === void 0 ? {} : { col: frame.col }
122
- };
123
- return Doc.paragraph(Doc.text("at ", "muted"), ...frame.fn === void 0 ? [] : [`${frame.fn} `], Doc.link(target, where));
124
- };
125
- const stackBlock = (defect, displayPath, mode) => {
126
- const { frames, hidden } = cleanStack(readProperty(defect, "stack"), mode);
127
- if (frames.length > 0) {
128
- const shown = frames.map((frame) => frameBlock(frame, displayPath));
129
- const note = hidden === 0 ? [] : [Doc.paragraph(Doc.text(`(+${hidden} internal frames hidden)`, "muted"))];
130
- return Doc.collapsible("stack", [...shown, ...note], { open: true });
131
- }
132
- const note = hidden === 0 ? "no stack" : `no user frames (${hidden} internal frames hidden)`;
133
- return Doc.collapsible("stack", [Doc.paragraph(Doc.text(note, "muted"))], { open: true });
134
- };
135
- /** The `Error.cause` chain below a defect as a tree of one-line messages, or none. */
136
- const causeTree = (defect) => {
137
- const chain = [];
138
- const seen = /* @__PURE__ */ new Set([defect]);
139
- for (let current = readProperty(defect, "cause"); current !== void 0 && chain.length < MAX_DEPTH;) {
140
- if (seen.has(current)) break;
141
- seen.add(current);
142
- chain.push(firstLine(failureText(current)));
143
- current = current instanceof Error ? readProperty(current, "cause") : void 0;
144
- }
145
- if (chain.length === 0) return void 0;
146
- const nest = (index) => index === chain.length - 1 ? { label: chain[index] } : {
147
- label: chain[index],
148
- children: [nest(index + 1)]
149
- };
150
- return Doc.tree({
151
- label: firstLine(failureText(defect)),
152
- children: [nest(0)]
153
- });
154
- };
155
- const dieBlocks = (defect, spans, displayPath, mode) => {
156
- if (defect instanceof Cancelled || defect instanceof NotInteractive) return [Doc.paragraph(defect.message)];
157
- const header = failureBlocks(failureText(defect));
158
- if (!(defect instanceof Error)) return [...header, ...spans];
159
- const chain = causeTree(defect);
160
- return [
161
- ...header,
162
- ...spans,
163
- stackBlock(defect, displayPath, mode),
164
- ...chain === void 0 ? [] : [chain]
165
- ];
166
- };
167
- const failBlocks = (error, spans, options) => {
168
- if (hasCliDoc(error)) try {
169
- return [...error[CliDoc](), ...spans];
170
- } catch {}
171
- const tag = tagOf(error);
172
- const custom = tag === void 0 || options?.render === void 0 || !Object.hasOwn(options.render, tag) ? void 0 : options.render[tag];
173
- if (custom !== void 0) try {
174
- return [...custom(error), ...spans];
175
- } catch {}
176
- if (error instanceof Cancelled || error instanceof NotInteractive) return [Doc.paragraph(error.message)];
177
- const schema = schemaBlocks(error);
178
- if (schema !== void 0) return [...schema, ...spans];
179
- return [...failureBlocks(failureText(error)), ...spans];
180
- };
181
- /** A file of the kit's own packages or of Effect, as installed: a span defined there is not the program's. */
182
- const isKitFile = (file) => /[\\/]node_modules[\\/](?:@effected[\\/]|effect[\\/])/.test(file);
183
- /**
184
- * The installed package directory that holds `module`: the path through the last `node_modules/<name>` or
185
- * `node_modules/@scope/name` in it, or `undefined` when the module is not under `node_modules` (or not a path at all).
186
- * Read from the path alone, with no file system.
187
- */
188
- const packageDirOf = (module) => {
189
- if (module === void 0) return void 0;
190
- const path = asPath(module);
191
- if (path === void 0) return void 0;
192
- return /^(.*\/node_modules\/(?:@[^/]+\/)?[^/]+)\//.exec(comparable(path))?.[1];
193
- };
194
- /**
195
- * A path in the one spelling both sides of a comparison use: `/` separators, and a Windows drive letter lower-cased. A
196
- * CommonJS frame on Windows reads `C:\…` where an `import.meta.url` reads `file:///C:/…`, and the drive's case is not
197
- * fixed (`c:` and `C:` are one drive). Lexical only: no file system, no realpath.
198
- */
199
- const comparable = (path) => path.replace(/\\/g, "/").replace(/^([A-Za-z]):/, (_, drive) => `${drive.toLowerCase()}:`);
200
- /** Whether `file` is in the program's own package `appDir`, and not in a dependency nested in its `node_modules`. */
201
- const isAppFile = (file, appDir) => {
202
- if (appDir === void 0) return false;
203
- const path = comparable(file);
204
- if (!path.startsWith(appDir)) return false;
205
- const rest = path.slice(appDir.length);
206
- return rest.startsWith("/") && !rest.includes("/node_modules/");
207
- };
208
- /** The file a span frame's captured stack points at, or `undefined` when it captured none. */
209
- const spanFile = (frame) => {
210
- try {
211
- const stack = frame.stack();
212
- if (stack === void 0) return void 0;
213
- const line = stack.split(/\r\n|\r|\n/).find((candidate) => /^\s*at\s/.test(candidate)) ?? stack;
214
- return frameFile(line.trim().startsWith("at ") ? line : `at ${line.trim()}`);
215
- } catch {
216
- return;
217
- }
218
- };
219
- /**
220
- * `in: outer › inner`, from the span stack the runtime annotates a reason with, or none. With `app`, a span the kit or
221
- * Effect defined is left out. An `Effect.fn` call carries its definition as its parent (`name (definition)`): the two
222
- * are one entry, `name`, judged by the definition's site, so a kit function the program calls goes with its
223
- * definition.
224
- */
225
- const spanBlocks = (reason, mode, appDir) => {
226
- if (mode === "off") return [];
227
- const names = [];
228
- let frame = Context.getOrUndefined(Cause.reasonAnnotations(reason), Cause.StackTrace);
229
- for (let seen = 0; frame !== void 0 && seen < MAX_SPANS; seen++) {
230
- const parent = frame.parent;
231
- const definition = parent !== void 0 && parent.name === `${frame.name} (definition)` ? parent : void 0;
232
- const file = (definition === void 0 ? void 0 : spanFile(definition)) ?? spanFile(frame);
233
- if (mode === "all" || file === void 0 || isAppFile(file, appDir) || !isKitFile(file)) names.push(frame.name);
234
- frame = definition === void 0 ? parent : definition.parent;
235
- }
236
- if (names.length === 0) return [];
237
- return [Doc.paragraph(Doc.text("in: ", "muted"), Doc.path(...names.reverse()))];
238
- };
239
- /**
240
- * A failure as a document: what the default report prints, and a building block for a custom one.
241
- *
242
- * @remarks
243
- * One run of blocks per `Cause` reason. A typed failure is, in order of preference: the document of an error that
244
- * implements {@link CliDoc}; the document `options.render` holds for its `_tag`; for `Cancelled` and
245
- * `NotInteractive`, their one fixed line; a `Tree` of the rejected values, for a schema error or issue; else a failure
246
- * status line with its message. A defect is its message followed by a collapsible `stack` of the program's own frames,
247
- * each a file link (so a terminal can open it in an editor) shown through `displayPath`, with the runtime's frames (every
248
- * `node:` frame and every frame with no file) and every `node_modules` frame (Effect's and any other dependency's)
249
- * left out unless `stackFrames` is `all`. A frame is classified by its file alone, never by its function name, so a
250
- * program's own thunk that V8 names by Effect's method alias is still shown. When that would
251
- * leave no frame at all, as for a program run from its own install under `node_modules`, only the runtime's and
252
- * Effect's are left out. Then an
253
- * `Error.cause` chain as a tree. When cleaning leaves no frame the stack says
254
- * `no user frames (N internal frames hidden)`, never an empty block; when frames survive and some were left out, the
255
- * count follows them as `(+N internal frames hidden)`. A reason that ran under spans is followed by
256
- * `in: outer › inner`: by default only the program's own spans, the kit's and Effect's left out (`spans`). Interrupts are not rendered beside a real failure, and a cause with only interrupts is the
257
- * one line `interrupted`.
258
- *
259
- * All text goes through the document, so a control character in a message or a stack frame never reaches the terminal.
260
- * Building it never throws: an error whose `_tag`, `message`, `stack`, `cause` or `issue` getter throws is drawn without
261
- * that part (an unreadable `stack` as `no stack`, an unreadable `cause` ending the chain there). Render it with `Render.context` and `Render.plain`, `ansi`, `markdown` or `githubLog`, or `Doc.print` it.
262
- *
263
- * @public
264
- */
265
- var CliFailure = class {
266
- constructor() {}
267
- /**
268
- * Build the document of a cause.
269
- *
270
- * @param cause - the failure
271
- * @param options - per-tag documents and a path display function
272
- */
273
- static toDoc = (cause, options) => {
274
- const reasons = cause.reasons;
275
- if (reasons.length > 0 && reasons.every(Cause.isInterruptReason)) return [Doc.paragraph("interrupted")];
276
- const displayPath = options?.displayPath ?? identity;
277
- return reasons.flatMap((reason) => {
278
- const spans = options?.spans ?? "app";
279
- const appDir = packageDirOf(options?.appModule);
280
- if (Cause.isFailReason(reason)) return failBlocks(reason.error, spanBlocks(reason, spans, appDir), options);
281
- if (Cause.isDieReason(reason)) return dieBlocks(reason.defect, spanBlocks(reason, spans, appDir), displayPath, options?.stackFrames ?? "app");
282
- return [];
283
- });
284
- };
285
- };
286
-
287
- export { CliDoc, CliFailure };
package/CliFormat.js DELETED
@@ -1,124 +0,0 @@
1
- import { underGithubActions } from "./internal/autoFormat.js";
2
- import { Doc } from "./Doc.js";
3
- import { neutralizeJson } from "./internal/logSafety.js";
4
- import { Console, Effect } from "effect";
5
- import { Flag } from "effect/cli";
6
-
7
- // src/CliFormat.ts
8
- const BUILT_IN = /* @__PURE__ */ new Set([
9
- "auto",
10
- "plain",
11
- "ansi",
12
- "markdown",
13
- "githubLog",
14
- "json"
15
- ]);
16
- /**
17
- * A `--format` flag and the one print that honours it, so a command builds its report once as a `Doc` and its data
18
- * once as a value, and never writes per-format output by hand.
19
- *
20
- * @remarks
21
- * The flag is core's `Flag.ChoiceWithValue` with the kit's names, default and description; the print hands every
22
- * document format to `Doc.print` and writes `json` itself.
23
- *
24
- * ```ts
25
- * const format = CliFormat.flag(["auto", "markdown", "json"])
26
- * const doctor = Command.make("doctor", { format }, ({ format }) =>
27
- * CliFormat.print(format, { doc: () => report(results), json: () => results }),
28
- * )
29
- * ```
30
- *
31
- * @public
32
- */
33
- var CliFormat = class CliFormat {
34
- constructor() {}
35
- /**
36
- * A `--format` flag offering `formats`, and the caller's own `aliases` and `custom` formats.
37
- *
38
- * @remarks
39
- * Its default is `options.default`, else `auto` when it is offered, else the first format. Any other value is a
40
- * usage error from core's parser, naming the accepted ones. The value is the built-in format a name stands for
41
- * (`human` under `{ human: "auto" }` is `auto`), or a custom name as itself, so the value of a flag with no custom
42
- * formats goes straight to {@link CliFormat.print}.
43
- *
44
- * Two offered names that clash (an alias or custom name reusing a built-in name, or any name offered twice) and a
45
- * default that is not offered are programming mistakes: the call throws. A CLI that still accepts a `--json` of its
46
- * own declares and resolves that flag itself; {@link CliFormat.printJson} prints the JSON the same way.
47
- *
48
- * ```ts
49
- * // okfit's contract: --format human|json, and a graph command that also draws mermaid and dot
50
- * const format = CliFormat.flag(["json"], { aliases: { human: "auto" }, default: "human" })
51
- * const graph = CliFormat.flag(["json"], { custom: ["mermaid", "dot"], default: "mermaid" })
52
- * ```
53
- *
54
- * @param formats - the built-in formats the command offers under their own names, at least one
55
- * @param options - the default, the flag's name and description, aliases and custom formats
56
- */
57
- static flag = (formats, options) => {
58
- const name = options?.name ?? "format";
59
- const choices = [
60
- ...formats.map((format) => [format, format]),
61
- ...Object.entries(options?.aliases ?? {}).map(([alias, target]) => [alias, target]),
62
- ...(options?.custom ?? []).map((custom) => [custom, custom])
63
- ];
64
- const offered = choices.map(([spelling]) => spelling);
65
- for (const [index, spelling] of offered.entries()) {
66
- if (offered.indexOf(spelling) !== index) throw new Error(`CliFormat.flag: the format name "${spelling}" is offered twice`);
67
- if (index >= formats.length && BUILT_IN.has(spelling)) throw new Error(`CliFormat.flag: "${spelling}" is a built-in format, not an alias or a custom one`);
68
- }
69
- const fallback = options?.default ?? (offered.includes("auto") ? "auto" : formats[0]);
70
- const fallbackChoice = choices.find(([spelling]) => spelling === fallback);
71
- if (fallbackChoice === void 0) throw new Error(`CliFormat.flag: the default "${fallback}" is not one of ${offered.join(", ")}`);
72
- const flag = Flag.ChoiceWithValue(name, choices).pipe(Flag.withDescription(options?.description ?? `Output format: ${offered.join(", ")} (default ${fallback})`));
73
- return Flag.withDefault(flag, fallbackChoice[1]);
74
- };
75
- /**
76
- * Print a command's output in `format`.
77
- *
78
- * @remarks
79
- * `json` writes `output.json()` exactly as {@link CliFormat.printJson} writes a value, which it delegates to. Every
80
- * other format prints `output.doc()` with `Doc.print`, which renders it as `auto` or the named renderer would.
81
- *
82
- * @param format - the chosen format, usually the value of {@link CliFormat.flag}
83
- * @param output - the document and the data
84
- * @param options - the stream and the JSON indent
85
- */
86
- static print = (format, output, options) => Effect.gen(function* () {
87
- const stream = options?.stream ?? "stdout";
88
- if (format !== "json") return yield* Doc.print(output.doc(), {
89
- stream,
90
- format
91
- });
92
- const data = output.json;
93
- if (data === void 0) return yield* Effect.die(/* @__PURE__ */ new Error("CliFormat.print: format json needs output.json"));
94
- return yield* CliFormat.printJson(data(), options);
95
- });
96
- /**
97
- * Print `value` as JSON, the way {@link CliFormat.print} prints the `json` format, for a CLI that keeps a JSON flag
98
- * of its own.
99
- *
100
- * @remarks
101
- * One `Console.log` (or `Console.error` for stderr), indented by `indent`: one document, never one line per item.
102
- * Under GitHub Actions a `##[` inside it is written as the JSON escape `#\u0023[`, which decodes to the same
103
- * text, so the runner cannot read it as a command. A value JSON cannot hold (a cycle, a bigint, `undefined`) is a
104
- * defect. It needs no theme, terminal or audience: whether this is GitHub Actions is read from `CurrentRuntimeEnv`
105
- * when the environment has it, and is "no" otherwise.
106
- *
107
- * ```ts
108
- * const status = Command.make("status", { json: Flag.Boolean("json").pipe(Flag.withDefault(false)) }, ({ json }) =>
109
- * json ? CliFormat.printJson(results) : Doc.print(report(results)),
110
- * )
111
- * ```
112
- *
113
- * @param value - the data to print
114
- * @param options - the stream and the JSON indent
115
- */
116
- static printJson = (value, options) => Effect.gen(function* () {
117
- const text = yield* Effect.sync(() => JSON.stringify(value, null, options?.indent ?? 2));
118
- if (text === void 0) return yield* Effect.die(/* @__PURE__ */ new Error("CliFormat.printJson: the value is not JSON"));
119
- const safe = (yield* underGithubActions) ? neutralizeJson(text) : text;
120
- yield* (options?.stream ?? "stdout") === "stdout" ? Console.log(safe) : Console.error(safe);
121
- });
122
- };
123
-
124
- export { CliFormat };
package/CliInteractive.js DELETED
@@ -1,94 +0,0 @@
1
- import { canPrompt } from "./internal/canPrompt.js";
2
- import { Context, Effect, Layer } from "effect";
3
- import { Audience, TerminalEnv } from "@effected/env";
4
- import { TerminalCapabilities } from "effect/cli";
5
-
6
- // src/CliInteractive.ts
7
- /**
8
- * Whether this run may prompt a person: a human audience, with a terminal on
9
- * both standard input and standard output, and a `TERM` that is not `dumb`.
10
- *
11
- * @remarks
12
- * A `Context.Reference`, not a `Context.Service`, for three reasons. It is one
13
- * boolean with a safe default, which is what a reference is for. A scoped
14
- * override, {@link CliInteractive.unless}, is a plain
15
- * `Effect.provideService`, so a command can switch prompting off for one
16
- * subtree without a layer. And forgetting to provide it is not a type error
17
- * but a harmless answer, since it reads `false` when no layer is provided: a
18
- * program that never wired it refuses to prompt rather than hanging on a
19
- * terminal that is not there. Read it with `yield* CliInteractive`.
20
- *
21
- * Because a reference's key type is `never`, {@link CliInteractive.layer} and
22
- * {@link CliInteractive.layerTest} are typed `Layer<never>`: they set the
23
- * reference rather than provide a service.
24
- *
25
- * Both layers also hold core's `TerminalCapabilities.canPrompt` at `true`,
26
- * keeping its `canColor` and `canAnimate` as they find them. Core skips a
27
- * `Flag.withFallbackPrompt` or `Argument.withFallbackPrompt` fallback entirely
28
- * when `canPrompt` is `false`, and decides that from the process's own
29
- * terminal, so a kit fallback (`CliPrompt.fallback`, `CliUi.fallback`) would
30
- * never be called in a piped run and its `otherwise` would be lost. With the
31
- * gate held open, this reference makes the prompt decision instead: the kit's
32
- * fallbacks answer without a person when it is `false`, and
33
- * `CliPrompt.gateTerminal` quits any other prompt at once. A
34
- * `TerminalCapabilities` with `canPrompt: false` provided inside the program,
35
- * under these layers, bypasses the kit's fallbacks again: the parameter fails
36
- * as missing even when an `otherwise` is given.
37
- *
38
- * @public
39
- */
40
- var CliInteractive = class CliInteractive extends Context.Reference("@effected/cli/CliInteractive", { defaultValue: () => false }) {
41
- /**
42
- * Decide from the audience and the terminal: `true` only for a human audience with a terminal on both
43
- * standard input and standard output, and a `TERM` that is not `dumb`.
44
- *
45
- * @remarks
46
- * A dumb terminal is a terminal, but it cannot move the cursor or take synchronized output, which a prompt or a
47
- * screen redrawing in place needs: it gets what a pipe gets. `TERM` is read through the ambient `ConfigProvider`,
48
- * as `@effected/env` reads the environment, so it adds no requirement; a test fixes it with
49
- * `Effect.provideService(ConfigProvider.ConfigProvider, ConfigProvider.fromUnknown({ TERM: "dumb" }))`.
50
- *
51
- * It also holds core's `TerminalCapabilities.canPrompt` at `true`; see {@link CliInteractive}.
52
- *
53
- * Bind the layer to a constant and provide it once; `Audience` and `TerminalEnv` come from `@effected/env`.
54
- */
55
- static layer = Layer.effectContext(Effect.gen(function* () {
56
- const audience = yield* Audience;
57
- const terminal = yield* TerminalEnv;
58
- return yield* decided(audience.kind === "human" && (yield* canPrompt(terminal)));
59
- }));
60
- /**
61
- * A fixed answer that needs nothing. It holds core's `canPrompt` open too, as {@link CliInteractive.layer} does.
62
- *
63
- * @param value - whether the run is interactive
64
- */
65
- static layerTest = (value) => Layer.effectContext(decided(value));
66
- /**
67
- * Run `self` with interactivity switched off when `condition` holds.
68
- *
69
- * @remarks
70
- * It only narrows: `unless(false)` leaves the current value alone and never turns interactivity on, so a
71
- * non-interactive scope stays non-interactive. The outer value is restored when `self` ends, whether it
72
- * succeeds, fails or is interrupted.
73
- *
74
- * A flag that resolves the audience (`--human`, under `CliAudience.runWith` or `provide`) recomputes interactivity
75
- * from the terminal facts, so it can override an outer `unless` or a `layerTest(false)`: those narrow the
76
- * environment's answer, and the flag is a later, explicit one.
77
- *
78
- * @param condition - `true` to switch prompting off for `self`
79
- */
80
- static unless = (condition) => (self) => Effect.gen(function* () {
81
- const current = yield* CliInteractive;
82
- return yield* Effect.provideService(self, CliInteractive, current && !condition);
83
- });
84
- };
85
- /**
86
- * The context the two layers set: the decision, and core's capabilities as they are found with `canPrompt` held at
87
- * `true`, so core always hands a missing parameter to its fallback and the kit's decision governs.
88
- */
89
- const decided = (interactive) => Effect.map(TerminalCapabilities.TerminalCapabilities, (capabilities) => Context.make(CliInteractive, interactive).pipe(Context.add(TerminalCapabilities.TerminalCapabilities, {
90
- ...capabilities,
91
- canPrompt: true
92
- })));
93
-
94
- export { CliInteractive };