@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/CliLinks.js DELETED
@@ -1,153 +0,0 @@
1
- import { sanitize } from "./Fmt.js";
2
- import { isAllowedLinkUrl } from "./internal/linkScheme.js";
3
- import { DRIVE, UNC, encodeForOsc8, fileUrlPath } from "./internal/linkTarget.js";
4
- import { Config, Context, Effect, FileSystem, Layer, Option, Path } from "effect";
5
- import { CurrentRuntimeEnv } from "@effected/env";
6
- import { Walker } from "@effected/walker";
7
-
8
- // src/CliLinks.ts
9
- const MODES = [
10
- "auto",
11
- "vscode",
12
- "file",
13
- "off"
14
- ];
15
- const parseSetting = (raw) => MODES.find((mode) => mode === raw.trim().toLowerCase());
16
- /** A URL with its control characters and line breaks removed: none is legal in one, and each could end an OSC 8 early. */
17
- const cleanUrl = (url) => sanitize(url).replace(/[\r\n]/g, "");
18
- const makeTarget = (mode, absolute) => (target) => {
19
- if ("url" in target) {
20
- const url = cleanUrl(target.url);
21
- return url === "" || !isAllowedLinkUrl(url) ? Option.none() : Option.some(url);
22
- }
23
- if (mode === "off") return Option.none();
24
- const resolved = absolute(target.file);
25
- if (resolved === void 0) return Option.none();
26
- const path = fileUrlPath(resolved);
27
- if (path === void 0) return Option.none();
28
- if (mode === "file") return Option.some(`file://${path}`);
29
- const position = target.line === void 0 ? "" : target.col === void 0 ? `:${target.line}` : `:${target.line}:${target.col}`;
30
- return Option.some(`vscode://file${path}${position}`);
31
- };
32
- const isDirectory = (fs, path) => fs.stat(path).pipe(Effect.map((info) => info.type === "Directory"), Effect.orElseSucceed(() => false));
33
- const exists = (fs, path) => fs.exists(path).pipe(Effect.orElseSucceed(() => false));
34
- /**
35
- * The nearest directory, from `cwd` up, that holds `.git` or `pnpm-workspace.yaml`.
36
- *
37
- * It looks at `cwd` and then climbs at most {@link MAX_ASCENT} directories, through `Walker.ascend`, which also stops
38
- * where `dirname` reaches a fixpoint (the filesystem root). `Walker.findRoot` absorbs a failed probe as "not a root",
39
- * so one unreadable directory never hides a root above it. `None` when there is none.
40
- */
41
- const findRoot = (fs, path, cwd) => Walker.ascend(cwd, { maxDepth: 65 }).pipe(Effect.provideService(Path.Path, path), Effect.flatMap((directories) => Walker.findRoot(directories, (directory) => Effect.gen(function* () {
42
- return (yield* exists(fs, path.join(directory, ".git"))) || (yield* exists(fs, path.join(directory, "pnpm-workspace.yaml")));
43
- }))));
44
- const readOption = (name) => Config.option(Config.String(name)).pipe(Effect.orElseSucceed(() => Option.none()));
45
- const build = (options, ambient) => Effect.gen(function* () {
46
- const runtime = yield* CurrentRuntimeEnv;
47
- const raw = options.envVar === void 0 ? "" : Option.getOrElse(yield* readOption(options.envVar), () => "").trim();
48
- const fromEnv = parseSetting(raw);
49
- if (options.envVar !== void 0 && raw !== "" && fromEnv === void 0) yield* Effect.logWarning(`${options.envVar}=${raw} is not one of ${MODES.join("|")}; ignoring it`);
50
- const setting = fromEnv ?? options.editorLinks ?? "auto";
51
- const pwd = options.cwd === void 0 ? yield* readOption("PWD") : Option.none();
52
- const cwd = options.cwd ?? Option.getOrUndefined(pwd) ?? (Option.isSome(ambient.path) ? ambient.path.value.resolve(".") : void 0);
53
- const path = Option.getOrUndefined(ambient.path);
54
- const absolute = (file) => {
55
- if (UNC.test(file)) return void 0;
56
- if (DRIVE.test(file)) return file;
57
- if (path === void 0) return file.startsWith("/") ? file : void 0;
58
- if (path.isAbsolute(file)) return file;
59
- return cwd === void 0 ? void 0 : path.resolve(cwd, file);
60
- };
61
- const mode = setting !== "auto" ? setting : Option.exists(runtime.terminal, (terminal) => terminal.name === "vscode") ? "vscode" : yield* Effect.gen(function* () {
62
- if (Option.isNone(ambient.fs) || path === void 0 || cwd === void 0) return "file";
63
- const root = yield* findRoot(ambient.fs.value, path, cwd);
64
- const base = Option.getOrElse(root, () => cwd);
65
- return (yield* isDirectory(ambient.fs.value, path.join(base, ".vscode"))) ? "vscode" : "file";
66
- });
67
- return {
68
- mode,
69
- target: makeTarget(mode, absolute)
70
- };
71
- });
72
- /**
73
- * Editor-aware links for file targets: where a link to a file opens.
74
- *
75
- * @remarks
76
- * The mode is decided once, when the layer is built. `auto` is `vscode` when `CurrentRuntimeEnv.terminal` is
77
- * `vscode` (`TERM_PROGRAM=vscode`) or a `.vscode/` directory sits at the project root, and `file` otherwise. The
78
- * root is the nearest directory, from the working directory up, that holds `.git` or `pnpm-workspace.yaml`; the
79
- * climb is bounded, at most 64 directories above the working directory, and stops where `Path.dirname` reaches the
80
- * filesystem root. With no root, the working directory itself is checked.
81
- *
82
- * Whether a link is written at all is a separate question, answered by {@link CliLinks.linker}.
83
- *
84
- * @public
85
- */
86
- var CliLinks = class CliLinks extends Context.Service()("@effected/cli/CliLinks") {
87
- /**
88
- * The links for the working directory, reading the filesystem for a `.vscode/` directory.
89
- *
90
- * @remarks
91
- * A layer-returning function mints a fresh layer per call: call it once and bind the result to a constant.
92
- *
93
- * @param options - the setting, the environment variable that overrides it, and the working directory
94
- */
95
- static layer = (options = {}) => Layer.effect(CliLinks, Effect.gen(function* () {
96
- const fs = yield* FileSystem.FileSystem;
97
- const path = yield* Path.Path;
98
- return yield* build(options, {
99
- fs: Option.some(fs),
100
- path: Option.some(path)
101
- });
102
- }));
103
- /**
104
- * Links fixed to a mode, with no filesystem: a relative path has no link, since there is no working directory.
105
- *
106
- * @param mode - `vscode`, `file` or `off`
107
- */
108
- static layerTest = (mode) => Layer.succeed(CliLinks, {
109
- mode,
110
- target: makeTarget(mode, (file) => file.startsWith("/") || DRIVE.test(file) ? file : void 0)
111
- });
112
- /**
113
- * The function that writes a link: a target and a label in, the label out, wrapped in OSC 8 when it should be.
114
- *
115
- * @remarks
116
- * It writes the hyperlink `ESC ] 8 ; ; URL ESC \ label ESC ] 8 ; ; ESC \` only when the stream's terminal can
117
- * render it (`hyperlinks`) and the audience is not an agent, which never gets an escape of any kind; in every other
118
- * case, and whenever the target has no URL, it returns the label unchanged. The URL has its control characters
119
- * removed again here, so a hostile target cannot end the sequence early or start another, and a URL whose scheme is
120
- * not one a link may have (`javascript:`, `data:`, and the like; the same list markdown uses) is the label alone. It is pure and cheap,
121
- * which {@link RenderContext}'s `link` requires.
122
- *
123
- * @param options - the links, whether hyperlinks are available, and the audience
124
- */
125
- static linker = (options) => (target, label) => {
126
- if (!options.hyperlinks || options.audience === "agent") return label;
127
- const url = options.links.target(target);
128
- if (Option.isNone(url)) return label;
129
- const written = encodeForOsc8(cleanUrl(url.value));
130
- if (!isAllowedLinkUrl(written)) return label;
131
- return `\u001B]8;;${written}\u001B\\${label}\u001B]8;;\u001B\\`;
132
- };
133
- };
134
- /**
135
- * The links for {@link CliEnv.layer}: the same as {@link CliLinks.layer}, except that `FileSystem` and `Path` are
136
- * taken from the environment if it has them, not required.
137
- *
138
- * Without them there is no `.vscode/` to look for and no working directory to resolve a relative path against, so
139
- * `auto` is `vscode` only on the terminal signal. This keeps the requirements of `CliEnv.layer` and of every
140
- * `CliRuntime.main` overload unchanged.
141
- *
142
- * @internal
143
- */
144
- const ambientLinksLayer = (options = {}) => Layer.effect(CliLinks, Effect.gen(function* () {
145
- const fs = yield* Effect.serviceOption(FileSystem.FileSystem);
146
- const path = yield* Effect.serviceOption(Path.Path);
147
- return yield* build(options, {
148
- fs,
149
- path
150
- });
151
- }));
152
-
153
- export { CliLinks, ambientLinksLayer };
package/CliLog.js DELETED
@@ -1,367 +0,0 @@
1
- import { sanitize } from "./Fmt.js";
2
- import { paintStyle } from "./internal/ansi.js";
3
- import { CliTheme } from "./CliTheme.js";
4
- import { scanAudience } from "./internal/scanAudience.js";
5
- import { TrustedLine, neutralizeJson } from "./internal/logSafety.js";
6
- import { makeCliLogger } from "./CliLogger.js";
7
- import { Level, passes } from "./internal/diagnostics.js";
8
- import { makeFileSink } from "./internal/fileSink.js";
9
- import { Cause, Config, Console, Context, Effect, Layer, LogLevel, Logger, Option, Path, References } from "effect";
10
- import { Audience, CurrentRuntimeEnv, TerminalEnv } from "@effected/env";
11
- import { CommandNeutralizer } from "@effected/github-commands";
12
-
13
- // src/CliLog.ts
14
- /** The most spaces a numeric `indent` writes. */
15
- const MAX_INDENT = 64;
16
- /** An indent as written before the glyph: spaces for a number, a string with nothing in it that is not text. */
17
- const indentOf = (indent) => {
18
- if (indent === void 0) return "";
19
- if (typeof indent === "number") return Number.isFinite(indent) && indent > 0 ? " ".repeat(Math.min(MAX_INDENT, Math.floor(indent))) : "";
20
- return sanitize(indent).replace(/\r\n|\r|\n/g, "");
21
- };
22
- /** The accepted spellings of a level, lower-cased, to the level they mean. A `Map`, so no inherited key matches. */
23
- const LEVELS = /* @__PURE__ */ new Map([
24
- ["all", "All"],
25
- ["trace", "Trace"],
26
- ["debug", "Debug"],
27
- ["info", "Info"],
28
- ["warn", "Warn"],
29
- ["warning", "Warn"],
30
- ["error", "Error"],
31
- ["fatal", "Fatal"],
32
- ["none", "None"]
33
- ]);
34
- /** A level's spelling, in any case, to the level it means. */
35
- const parseLevel = (text) => Option.fromUndefinedOr(LEVELS.get(text.toLowerCase()));
36
- const LEVEL_STYLES = {
37
- FATAL: {
38
- fg: "red",
39
- bold: true
40
- },
41
- ERROR: { fg: "red" },
42
- WARN: { fg: "yellow" },
43
- INFO: { fg: "cyan" },
44
- DEBUG: { fg: "blue" },
45
- TRACE: { dim: true }
46
- };
47
- /** The diagnostics level: the option when given, else the env var, and the raw text when that is not a level. */
48
- const readLevel = (explicit, envVar) => Effect.gen(function* () {
49
- if (explicit !== void 0) return {
50
- level: explicit,
51
- invalid: void 0
52
- };
53
- if (envVar === void 0) return {
54
- level: "None",
55
- invalid: void 0
56
- };
57
- const raw = yield* Config.option(Config.String(envVar)).pipe(Effect.orElseSucceed(() => Option.none()));
58
- if (Option.isNone(raw) || raw.value === "") return {
59
- level: "None",
60
- invalid: void 0
61
- };
62
- const level = parseLevel(raw.value);
63
- if (Option.isSome(level)) return {
64
- level: level.value,
65
- invalid: void 0
66
- };
67
- return {
68
- level: "None",
69
- invalid: `${envVar}=${raw.value} is not a log level (${[...LEVELS.keys()].join("|")}); ignoring it`
70
- };
71
- });
72
- /**
73
- * Whether a record's line is neutralized: the `neutralize` option when it is a boolean, else whether the logging
74
- * fiber's `CurrentRuntimeEnv`, or `fallback` where the fiber has none, says GitHub Actions.
75
- */
76
- const actionsDecision = (neutralize, fallback) => (fiber) => {
77
- if (neutralize !== "auto") return neutralize;
78
- const inFiber = Context.getOption(fiber.context, CurrentRuntimeEnv);
79
- const runtime = Option.isSome(inFiber) ? inFiber : fallback;
80
- return Option.contains(Option.flatMap(runtime, (env) => env.ci), "github-actions");
81
- };
82
- /**
83
- * Diagnostics kept apart from a program's output: a level, a format and a place to write.
84
- *
85
- * @remarks
86
- * Two kinds of line must never be silenced by a diagnostics default: the failure report
87
- * `CliRuntime.reportFailures` writes through `CliLogger`, and the `CliMessage` lines. This package therefore
88
- * never makes the diagnostics level a global switch. The diagnostics logger filters on its **own** threshold,
89
- * {@link CliLog.Level}, and writes to stderr only.
90
- *
91
- * `CliLog.layer` **owns the whole logger set**. It builds a `CliLogger` for ordinary log lines and the
92
- * diagnostics sink itself and replaces whatever was installed, without reading it, so there is no order to get
93
- * wrong. Use it instead of `CliLogger.layer`, not with it: `CliLogger.layer` alone is the no-diagnostics path,
94
- * and a `CliLogger.layer` layered on top would replace this layer's set.
95
- *
96
- * Effect drops a record below `MinimumLogLevel` before any logger runs, so to let a debug record reach the
97
- * diagnostics logger `MinimumLogLevel` has to be lowered. `CliLog.layer` does that only when the
98
- * diagnostics level is below the ambient minimum, and in the same step floors the `CliLogger` it built at the
99
- * minimum it had, so it never prints a record the diagnostics level alone let through. A failure report is
100
- * written outside the scope core's `--log-level` flag sets, so `--log-level none` does not silence it either.
101
- *
102
- * `format: "auto"`, the default, decides by the audience alone, at build time and at runtime alike: NDJSON for an
103
- * agent or a CI, the pretty line for a human, whatever stderr's terminal state. A human piping stderr therefore gets
104
- * plain lines (without escapes unless stderr is a colour terminal); pass `format: "json"` for machine-readable logs.
105
- *
106
- * The text a program logs is sanitised before anything is painted in the pretty line (the message, the component and an
107
- * error's cause lose their escape sequences and control characters), and under GitHub Actions, where
108
- * `CurrentRuntimeEnv` in the logging fiber's context says so, a line the runner would read as a workflow command is
109
- * neutralized. An NDJSON record is not safe merely because `JSON.stringify` escapes control characters: the runner's
110
- * legacy parser reads `##[` anywhere in a line, so under Actions it is written as the JSON escape `#\u0023[`, which
111
- * decodes to the identical text. The file sink's lines are not read by the runner and are written as they are.
112
- *
113
- * `CurrentRuntimeEnv` is read from the logging fiber's context, and where that has none, from the `runtimeEnv` option,
114
- * else from the layer's own build context (captured if present, never required), so a host that builds the layer over
115
- * its environment, or names it with `runtimeEnv`, neutralizes every record. A record with none of the three (a program
116
- * with no `CurrentRuntimeEnv` anywhere) is sanitised but not neutralized, unless the `neutralize` option says otherwise.
117
- *
118
- * Core's `--log-level` flag sets `MinimumLogLevel` inside the command. While it is set to something other than
119
- * the value this layer installed, the diagnostics logger follows the flag instead of its own level: it writes
120
- * every record that reaches it. The `CliLogger` prints the same record too, so a record at or above the ambient
121
- * minimum is written twice, once plain and once to the diagnostics sink. Stderr is therefore not pure NDJSON while
122
- * diagnostics are on: a parser reads the lines that start with `{`.
123
- *
124
- * One edge: a `--log-level` value that EQUALS the level this layer installed cannot be told from no flag, so the
125
- * sink keeps filtering on its own level rather than following the flag. The plain `CliLogger` prints those records
126
- * regardless.
127
- *
128
- * @example
129
- * ```ts
130
- * import { CliRuntime } from "@effected/cli"
131
- * import { NodeRuntime, NodeServices } from "@effect/platform-node"
132
- *
133
- * // `env.log` makes `main` install `CliLog.layer`: set MYTOOL_LOG_LEVEL=debug to get diagnostics on stderr.
134
- * NodeRuntime.runMain(
135
- * CliRuntime.main(program, {
136
- * platform: NodeServices.layer,
137
- * env: { audienceEnvVar: "MYTOOL_AUDIENCE", log: { envVar: "MYTOOL_LOG_LEVEL" } },
138
- * }),
139
- * )
140
- * ```
141
- *
142
- * @public
143
- */
144
- var CliLog = class CliLog {
145
- constructor() {}
146
- /**
147
- * The diagnostics threshold. Defaults to `None`, silent.
148
- *
149
- * @remarks
150
- * `CliLog.layer` sets it from the environment variable. A scope may raise it to narrow the output; it
151
- * cannot lower it below the level the layer installed, because Effect has already dropped those records.
152
- */
153
- static Level = Level;
154
- /**
155
- * Read a log level the way `CliLog.layer` reads its environment variable: `Option.some` of the level, or
156
- * `Option.none()` when the text is not one.
157
- *
158
- * @remarks
159
- * Case-insensitive, and not trimmed. The spellings are `all`, `trace`, `debug`, `info`, `warn` (or `warning`),
160
- * `error`, `fatal` and `none`. For a host that reads a level from its own flag or config and wants the same answer
161
- * the layer would give.
162
- *
163
- * @example
164
- * ```ts
165
- * CliLog.parseLevel("WARNING") // Option.some("Warn")
166
- * CliLog.parseLevel("loud") // Option.none()
167
- * ```
168
- *
169
- * @param text - the level's spelling
170
- */
171
- static parseLevel(text) {
172
- return parseLevel(text);
173
- }
174
- static layer(options = {}) {
175
- const file = "file" in options ? options.file : void 0;
176
- const format = options.format ?? "auto";
177
- return Layer.unwrap(Effect.gen(function* () {
178
- const audience = format === "auto" ? yield* Audience : void 0;
179
- const terminal = format === "json" ? void 0 : yield* TerminalEnv;
180
- const { level, invalid } = yield* readLevel(options.level, options.envVar);
181
- const ambient = yield* References.MinimumLogLevel;
182
- const captured = yield* Effect.serviceOption(CurrentRuntimeEnv);
183
- const fallback = options.runtimeEnv === void 0 ? captured : Option.some(options.runtimeEnv);
184
- const underActionsIn = actionsDecision(options.neutralize ?? "auto", fallback);
185
- const color = terminal?.stderr.color ?? "none";
186
- const isPretty = (record) => {
187
- if (format !== "auto") return format === "pretty";
188
- const inForce = Context.getOption(record.fiber.context, Audience);
189
- return (Option.isSome(inForce) ? inForce.value.kind : audience?.kind) === "human";
190
- };
191
- const lowered = LogLevel.isLessThan(level, ambient) ? level : ambient;
192
- const isLowered = lowered !== ambient;
193
- const render = (record) => {
194
- const underActions = underActionsIn(record.fiber);
195
- if (!isPretty(record)) {
196
- const json = Logger.formatJson.log(record);
197
- return underActions ? neutralizeJson(json) : json;
198
- }
199
- const annotations = record.fiber.getRef(References.CurrentLogAnnotations);
200
- const component = annotations.component === void 0 ? "" : ` [${sanitize(String(annotations.component))}]`;
201
- const message = sanitize(Array.isArray(record.message) ? record.message.map(String).join(" ") : String(record.message));
202
- const name = record.logLevel.toUpperCase();
203
- const levelText = paintStyle(LEVEL_STYLES[name] ?? {}, color, name);
204
- const cause = record.cause.reasons.length > 0 ? `\n${sanitize(Cause.pretty(record.cause))}` : "";
205
- const line = `${record.date.toISOString().slice(11, 23)} ${levelText}${component} ${message}${cause}`;
206
- return underActions ? CommandNeutralizer.text(line) : line;
207
- };
208
- const sink = Logger.make((record) => {
209
- if (!passes(record, lowered)) return;
210
- record.fiber.getRef(Console.Console).error(render(record));
211
- });
212
- const floor = (inner) => isLowered ? Logger.make((record) => {
213
- const current = record.fiber.getRef(References.MinimumLogLevel);
214
- const threshold = current === lowered ? ambient : current;
215
- if (LogLevel.isGreaterThanOrEqualTo(record.logLevel, threshold)) inner.log(record);
216
- }) : inner;
217
- const cliLogger = floor(makeCliLogger(options.logger, underActionsIn));
218
- const extras = (options.extraLoggers ?? []).map(floor);
219
- if (invalid !== void 0) yield* Effect.logWarning(invalid).pipe(Effect.provideService(Logger.CurrentLoggers, /* @__PURE__ */ new Set([cliLogger])));
220
- return Layer.mergeAll(Layer.succeed(CliLog.Level, level), isLowered ? Layer.succeed(References.MinimumLogLevel, lowered) : Layer.empty, Layer.effect(Logger.CurrentLoggers, Effect.gen(function* () {
221
- const loggers = [
222
- ...options.plainLogger === false ? [] : [cliLogger],
223
- sink,
224
- ...extras
225
- ];
226
- if (file !== void 0) {
227
- const target = "path" in file ? Option.some(file.path) : yield* Config.option(Config.String(file.envVar)).pipe(Effect.orElseSucceed(() => Option.none()));
228
- if (Option.isSome(target) && target.value !== "") {
229
- const location = yield* Path.Path;
230
- loggers.push(yield* makeFileSink(location.resolve(target.value), lowered, underActionsIn));
231
- }
232
- }
233
- return new Set(loggers);
234
- })));
235
- }));
236
- }
237
- /**
238
- * Log a status line: its glyph painted through the theme, then `text`, at a level that follows the status.
239
- *
240
- * @remarks
241
- * The line goes through the logger, so it is a diagnostic like any `Effect.log*` call: filtered by the level in
242
- * force (`--log-level`, `CliLog.Level`), routed by `CliLogger`'s `stderrFrom` (stderr by default) and neutralized
243
- * under GitHub Actions. What differs is the glyph: the logger sanitises every line a program logs, which strips a
244
- * colour a program painted itself, so a glyph on the log channel was always drawn bare. Here the kit paints it and
245
- * marks the line as its own, so the plain `CliLogger` line keeps the colour, while `text` is still sanitised:
246
- * escape sequences and control characters in it are removed, as in every line the kit writes.
247
- *
248
- * The glyph is painted with stderr's theme (where diagnostics go) through {@link CliTheme.forAudience}, so an agent
249
- * gets it unpainted, and an `Audience` is read only when provided, so it stays out of the requirements. ASCII glyphs
250
- * give the status's ASCII form. A diagnostics record carries the line as its message as any record does: the `CliLog`
251
- * sink's pretty line sanitises it (the glyph is drawn bare there), and NDJSON keeps it, JSON-escaped.
252
- *
253
- * The level defaults to the status's rank in `vocab`: `Error` at or above `failure`'s, `Warn` at or above
254
- * `warning`'s, `Info` below, so a custom status follows its own rank. Pass `level` to choose it, and `indent` (a
255
- * number of spaces, or a string, sanitised) to start the line inside an indented block: ` ✗ error x: red`.
256
- *
257
- * @example
258
- * ```ts
259
- * import { CliLog, Status } from "@effected/cli"
260
- *
261
- * // ✗ in the failure colour, then the message: on stderr, filtered by the log level.
262
- * const reportError = (resource: string, message: string) =>
263
- * CliLog.status(Status.core, "failure", `${resource}: ${message}`)
264
- * ```
265
- *
266
- * @param vocab - the vocabulary the status belongs to
267
- * @param name - the status
268
- * @param text - the text after the glyph, sanitised
269
- * @param options - the level to log at, and the indent before the glyph
270
- */
271
- static status = (vocab, name, text, options) => Effect.gen(function* () {
272
- const audience = yield* Effect.serviceOption(Audience);
273
- const theme = CliTheme.forAudience((yield* CliTheme).forStream("stderr"), Option.isSome(audience) ? audience.value.kind : void 0);
274
- const line = `${indentOf(options?.indent)}${theme.status(vocab, name, sanitize(text))}`;
275
- const core = vocab;
276
- const rank = vocab.def(name).rank;
277
- const level = options?.level ?? (rank >= core.def("failure").rank ? "Error" : rank >= core.def("warning").rank ? "Warn" : "Info");
278
- yield* Effect.logWithLevel(level)(line).pipe(Effect.provideService(TrustedLine, true));
279
- });
280
- /**
281
- * Mark the log records an effect emits as coming from `name`.
282
- *
283
- * @remarks
284
- * Shown as `[name]` in pretty output and as `annotations.component` in NDJSON.
285
- *
286
- * @param name - the component
287
- */
288
- static component = (name) => (self) => Effect.annotateLogs(self, "component", name);
289
- };
290
- /**
291
- * The audience while the platform builds, from what needs no platform: a flag in `argv` (the rule `CliAudience` reads
292
- * argv with), else the override variable, else detection from the environment. No terminal is consulted. Silent: the
293
- * environment layer warns about an invalid override value itself, once.
294
- */
295
- const buildTimeAudience = (argv, audienceEnvVar, detected) => {
296
- const { given, conflict } = scanAudience(argv ?? []);
297
- const [flagged] = given;
298
- if (!conflict && flagged !== void 0) return Effect.succeed(flagged);
299
- return Effect.map(Audience, (audience) => audience.kind).pipe(Effect.provide(Audience.layer(audienceEnvVar === void 0 ? void 0 : { envVar: audienceEnvVar }).pipe(Layer.provide(Layer.succeed(CurrentRuntimeEnv, detected)))), Effect.provideService(Logger.CurrentLoggers, /* @__PURE__ */ new Set()));
300
- };
301
- /** What the build-time loggers decide from, read once: the format and the runtime environment to neutralize by. */
302
- const buildTimeDecision = (options, audienceEnvVar) => Effect.gen(function* () {
303
- const detected = yield* Effect.provide(CurrentRuntimeEnv, CurrentRuntimeEnv.layer);
304
- const format = options.format ?? "auto";
305
- return {
306
- ndjson: format === "json" || format === "auto" && (yield* buildTimeAudience(options.argv, audienceEnvVar, detected)) !== "human",
307
- runtimeEnv: options.runtimeEnv ?? detected
308
- };
309
- });
310
- /**
311
- * The logger the platform is built under by `CliRuntime.main` with `env.log`: the log level and env var apply to
312
- * what the platform logs while it builds, too.
313
- *
314
- * @remarks
315
- * With `format: "json"`, or `auto` when the build-time audience (`argv`, the override variable, detection) is an
316
- * agent or a CI, it is the full `CliLog.layer` in NDJSON (that format needs neither the audience nor the terminal,
317
- * which the platform has not built yet), without the file sink, whose `FileSystem` the platform provides. Otherwise it
318
- * is the plain `CliLogger`, with `MinimumLogLevel` lowered to the resolved level. Either way it neutralizes under
319
- * GitHub Actions from the detected environment. The level is resolved silently: the program's own `CliLog.layer` warns
320
- * about an invalid value, once.
321
- *
322
- * @internal
323
- */
324
- const platformLogLayer = (options, audienceEnvVar) => Layer.unwrap(Effect.gen(function* () {
325
- const { level } = yield* readLevel(options.level, options.envVar);
326
- const ambient = yield* References.MinimumLogLevel;
327
- const { ndjson, runtimeEnv } = yield* buildTimeDecision(options, audienceEnvVar);
328
- if (ndjson) return CliLog.layer({
329
- level,
330
- format: "json",
331
- ...options.plainLogger === void 0 ? {} : { plainLogger: options.plainLogger },
332
- ...options.logger === void 0 ? {} : { logger: options.logger },
333
- ...options.extraLoggers === void 0 ? {} : { extraLoggers: options.extraLoggers },
334
- ...options.neutralize === void 0 ? {} : { neutralize: options.neutralize },
335
- runtimeEnv
336
- });
337
- return Layer.merge(Logger.layer([makeCliLogger(options.logger, actionsDecision(options.neutralize ?? "auto", Option.some(runtimeEnv)))]), LogLevel.isLessThan(level, ambient) ? Layer.succeed(References.MinimumLogLevel, level) : Layer.empty);
338
- }));
339
- /**
340
- * The logger `CliRuntime.main` builds the environment layer under: one line per record, in the build-time format.
341
- *
342
- * @remarks
343
- * What the environment layer logs is a configuration error (an invalid audience override, which interpolates the
344
- * variable's value), so it is never silenced and never written twice: NDJSON alone for an agent or a CI (`json`, or
345
- * `auto` for that build-time audience), a plain `CliLogger` line otherwise, whatever `plainLogger` and the diagnostics
346
- * level say, and neutralized under GitHub Actions like the platform's lines. It goes to stderr alone, whatever
347
- * `stderrFrom` the program's `CliLogger` options raise, and never to `extraLoggers` or the file sink. Only this
348
- * logger is pinned to stderr: the platform's build-time logger keeps the host's `stderrFrom`. It floors at `Warning` and installs no
349
- * `MinimumLogLevel`, so `CliLog.layer`'s own build, which shares this context, reads the ambient minimum.
350
- *
351
- * @internal
352
- */
353
- const envBuildLogLayer = (options, audienceEnvVar) => Layer.unwrap(Effect.gen(function* () {
354
- const { ndjson, runtimeEnv } = yield* buildTimeDecision(options, audienceEnvVar);
355
- const underActions = actionsDecision(options.neutralize ?? "auto", Option.some(runtimeEnv));
356
- if (!ndjson) return Logger.layer([makeCliLogger({
357
- ...options.logger,
358
- stderrFrom: "All"
359
- }, underActions)]);
360
- return Logger.layer([Logger.make((record) => {
361
- if (!LogLevel.isGreaterThanOrEqualTo(record.logLevel, "Warn")) return;
362
- const json = Logger.formatJson.log(record);
363
- record.fiber.getRef(Console.Console).error(underActions(record.fiber) ? neutralizeJson(json) : json);
364
- })]);
365
- }));
366
-
367
- export { CliLog, envBuildLogLayer, platformLogLayer };
package/CliLogger.js DELETED
@@ -1,91 +0,0 @@
1
- import { sanitize } from "./Fmt.js";
2
- import { TrustedLine, sanitizeParts, underActionsIn } from "./internal/logSafety.js";
3
- import { Console, LogLevel, Logger, References } from "effect";
4
- import { CommandNeutralizer } from "@effected/github-commands";
5
-
6
- // src/CliLogger.ts
7
- const defaultRender = (message) => Array.isArray(message) ? message.map(String).join(" ") : String(message);
8
- /**
9
- * A `Logger` that renders CLI output rather than service logs: no timestamp, level or fiber id, with every level
10
- * going to stderr by default so stdout carries only what the program writes.
11
- *
12
- * @remarks
13
- * Effect's default logger emits `[00:33:56.619] INFO (#2): message`. That is
14
- * the right shape for a long-running service being scraped and the wrong one
15
- * for a tool a person is watching: the timestamp, level and fiber id are noise
16
- * in front of output a human is reading, and they make a formatted block — a
17
- * permissions table, a summary — unreadable.
18
- *
19
- * **A program that never installs a CLI logger looks correct in review and
20
- * ships timestamps to its users**, so install this one at the program's
21
- * boundary.
22
- *
23
- * It writes through the `Console` reference read off the logging fiber,
24
- * synchronously, as core's own default logger does: `Logger.make` takes a
25
- * synchronous callback, and a `Stdio` sink write is an `Effect` a logger cannot
26
- * `yield*`. `Console.Console` is a `Context.Reference`, so it never appears in
27
- * `R`, and a test swaps the reference rather than stubbing a global.
28
- *
29
- * @example
30
- * ```ts
31
- * import { CliLogger } from "@effected/cli"
32
- * import { Console, Effect } from "effect"
33
- *
34
- * const program = Effect.gen(function* () {
35
- * yield* Effect.log("synced 3 repos") // stderr, no timestamp — a diagnostic
36
- * yield* Console.log("3 repos synced") // stdout — the program's actual output
37
- * yield* Effect.logError("one failed") // stderr
38
- * })
39
- *
40
- * program.pipe(Effect.provide(CliLogger.layer()))
41
- * ```
42
- *
43
- * `stderrFrom` defaults to `"All"`: a CLI's stdout is its product, so every
44
- * log level is a diagnostic unless a consumer narrows the threshold. Write
45
- * program output with `Console.log`, never `Effect.log`. Pass
46
- * `stderrFrom: "Error"` for a tool whose output *is* its log lines.
47
- *
48
- * @public
49
- */
50
- var CliLogger = class CliLogger {
51
- constructor() {}
52
- /**
53
- * The logger itself, for composing into an existing `Logger.layer` set.
54
- *
55
- * @remarks
56
- * Prefer {@link CliLogger.layer}. Reach for this only when you are building
57
- * the logger set yourself and want this one among several.
58
- */
59
- static make = (options = {}) => makeCliLogger(options);
60
- /**
61
- * Replace the default logger with this one.
62
- *
63
- * @remarks
64
- * `Logger.layer` **replaces** rather than merges, so nothing is emitted twice.
65
- *
66
- * Merge this into the layer you provide to the whole program rather than
67
- * providing it beneath: merged, it also covers lines emitted during layer
68
- * construction, which is exactly where a startup failure prints.
69
- */
70
- static layer = (options = {}) => Logger.layer([CliLogger.make(options)]);
71
- };
72
- /**
73
- * `CliLogger.make`, with the neutralizing decision as a parameter: `CliLog.layer` passes its own (the fiber's
74
- * `CurrentRuntimeEnv`, else the one captured at build, or its `neutralize` option), so its plain line and its
75
- * diagnostics line are neutralized alike.
76
- *
77
- * @internal
78
- */
79
- const makeCliLogger = (options = {}, underActions = underActionsIn) => {
80
- const custom = options.render;
81
- const render = custom ?? defaultRender;
82
- const stderrFrom = options.stderrFrom ?? "All";
83
- return Logger.make(({ fiber, logLevel, message }) => {
84
- const console = fiber.getRef(Console.Console);
85
- const write = fiber.getRef(References.LogToStderr) || LogLevel.isGreaterThanOrEqualTo(logLevel, stderrFrom) ? console.error : console.log;
86
- const rendered = fiber.getRef(TrustedLine) ? render(message) : custom === void 0 ? sanitize(render(message)) : render(sanitizeParts(message));
87
- write(underActions(fiber) ? CommandNeutralizer.text(rendered) : rendered);
88
- });
89
- };
90
-
91
- export { CliLogger, makeCliLogger };