@effected/cli 0.22.2 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CliEnv.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { CliInteractive } from "./CliInteractive.js";
2
2
  import { ambientLinksLayer } from "./CliLinks.js";
3
3
  import { CliTheme } from "./CliTheme.js";
4
+ import { displayPathLayer } from "./internal/displayPath.js";
4
5
  import { CliPrompt } from "./CliPrompt.js";
5
6
  import { ConfigProvider, Layer, Option } from "effect";
6
7
  import { Audience, CurrentRuntimeEnv, TerminalEnv } from "@effected/env";
@@ -50,7 +51,7 @@ var CliEnv = class {
50
51
  ...options.editorLinksEnvVar === void 0 ? {} : { envVar: options.editorLinksEnvVar }
51
52
  }).pipe(Layer.provideMerge(withTheme));
52
53
  const withInteractive = CliInteractive.layer.pipe(Layer.provideMerge(withLinks));
53
- return Layer.mergeAll(CliPrompt.gateTerminal, CliPrompt.gateWizard, CliTheme.promptTheme).pipe(Layer.provideMerge(withInteractive));
54
+ return Layer.mergeAll(CliPrompt.gateTerminal, CliPrompt.gateWizard, CliTheme.promptTheme, displayPathLayer(options.displayPath)).pipe(Layer.provideMerge(withInteractive));
54
55
  };
55
56
  /**
56
57
  * The environment services a test fixes, needing nothing and reading nothing of the host's: `TerminalEnv` and
package/CliFailure.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { Cancelled } from "./Cancelled.js";
2
2
  import { Fmt } from "./Fmt.js";
3
+ import { identity } from "./internal/displayPath.js";
3
4
  import { Doc } from "./Doc.js";
4
5
  import { issueEntries, issueTreeChildren } from "./internal/format.js";
6
+ import { failureText, readProperty, tagOf } from "./internal/inspect.js";
5
7
  import { splitFrame } from "./internal/splitFrame.js";
6
8
  import { NotInteractive } from "./NotInteractive.js";
7
9
  import { Status } from "./Status.js";
@@ -24,22 +26,7 @@ const CliDoc = Symbol.for("@effected/cli/CliDoc");
24
26
  const MAX_DEPTH = 8;
25
27
  const MAX_SPANS = 32;
26
28
  const hasCliDoc = (value) => typeof value === "object" && value !== null && typeof value[CliDoc] === "function";
27
- const describe = (value) => {
28
- try {
29
- const text = String(value);
30
- if (text !== "[object Object]") return text;
31
- const message = value?.message;
32
- return typeof message === "string" ? message : text;
33
- } catch {
34
- return "[unprintable value]";
35
- }
36
- };
37
29
  const firstLine = (text) => text.split(/\r\n|\r|\n/, 1)[0] ?? "";
38
- const tagOf = (value) => {
39
- if (typeof value !== "object" || value === null) return void 0;
40
- const tag = value._tag;
41
- return typeof tag === "string" ? tag : void 0;
42
- };
43
30
  /** The failure status and the text of a message: one block per line, the status on the first. */
44
31
  const failureBlocks = (message) => {
45
32
  const [first = "", ...rest] = message.split(/\r\n|\r|\n/);
@@ -48,15 +35,13 @@ const failureBlocks = (message) => {
48
35
  /** The issue of a schema failure: the value itself, or the `issue` an error carries. */
49
36
  const issueOf = (error) => {
50
37
  if (SchemaIssue.isIssue(error)) return error;
51
- if (typeof error === "object" && error !== null) {
52
- const issue = error.issue;
53
- if (SchemaIssue.isIssue(issue)) return issue;
54
- }
38
+ const issue = readProperty(error, "issue");
39
+ return SchemaIssue.isIssue(issue) ? issue : void 0;
55
40
  };
56
41
  const schemaBlocks = (error) => {
57
42
  const entries = issueEntries(issueOf(error));
58
43
  if (entries.length === 0) return void 0;
59
- const header = typeof error === "object" && error !== null && !SchemaIssue.isIssue(error) ? firstLine(describe(error.message ?? "")) : "";
44
+ const header = typeof error === "object" && error !== null && !SchemaIssue.isIssue(error) ? firstLine(failureText(readProperty(error, "message") ?? "")) : "";
60
45
  const root = {
61
46
  label: [
62
47
  Doc.status(Status.core, "failure"),
@@ -138,7 +123,7 @@ const frameBlock = (frame, displayPath) => {
138
123
  return Doc.paragraph(Doc.text("at ", "muted"), ...frame.fn === void 0 ? [] : [`${frame.fn} `], Doc.link(target, where));
139
124
  };
140
125
  const stackBlock = (defect, displayPath, mode) => {
141
- const { frames, hidden } = cleanStack(defect.stack, mode);
126
+ const { frames, hidden } = cleanStack(readProperty(defect, "stack"), mode);
142
127
  if (frames.length > 0) {
143
128
  const shown = frames.map((frame) => frameBlock(frame, displayPath));
144
129
  const note = hidden === 0 ? [] : [Doc.paragraph(Doc.text(`(+${hidden} internal frames hidden)`, "muted"))];
@@ -151,11 +136,11 @@ const stackBlock = (defect, displayPath, mode) => {
151
136
  const causeTree = (defect) => {
152
137
  const chain = [];
153
138
  const seen = /* @__PURE__ */ new Set([defect]);
154
- for (let current = defect.cause; current !== void 0 && chain.length < MAX_DEPTH;) {
139
+ for (let current = readProperty(defect, "cause"); current !== void 0 && chain.length < MAX_DEPTH;) {
155
140
  if (seen.has(current)) break;
156
141
  seen.add(current);
157
- chain.push(firstLine(describe(current)));
158
- current = current instanceof Error ? current.cause : void 0;
142
+ chain.push(firstLine(failureText(current)));
143
+ current = current instanceof Error ? readProperty(current, "cause") : void 0;
159
144
  }
160
145
  if (chain.length === 0) return void 0;
161
146
  const nest = (index) => index === chain.length - 1 ? { label: chain[index] } : {
@@ -163,13 +148,13 @@ const causeTree = (defect) => {
163
148
  children: [nest(index + 1)]
164
149
  };
165
150
  return Doc.tree({
166
- label: firstLine(describe(defect)),
151
+ label: firstLine(failureText(defect)),
167
152
  children: [nest(0)]
168
153
  });
169
154
  };
170
155
  const dieBlocks = (defect, spans, displayPath, mode) => {
171
156
  if (defect instanceof Cancelled || defect instanceof NotInteractive) return [Doc.paragraph(defect.message)];
172
- const header = failureBlocks(describe(defect));
157
+ const header = failureBlocks(failureText(defect));
173
158
  if (!(defect instanceof Error)) return [...header, ...spans];
174
159
  const chain = causeTree(defect);
175
160
  return [
@@ -191,7 +176,7 @@ const failBlocks = (error, spans, options) => {
191
176
  if (error instanceof Cancelled || error instanceof NotInteractive) return [Doc.paragraph(error.message)];
192
177
  const schema = schemaBlocks(error);
193
178
  if (schema !== void 0) return [...schema, ...spans];
194
- return [...failureBlocks(describe(error)), ...spans];
179
+ return [...failureBlocks(failureText(error)), ...spans];
195
180
  };
196
181
  /** A file of the kit's own packages or of Effect, as installed: a span defined there is not the program's. */
197
182
  const isKitFile = (file) => /[\\/]node_modules[\\/](?:@effected[\\/]|effect[\\/])/.test(file);
@@ -272,7 +257,8 @@ const spanBlocks = (reason, mode, appDir) => {
272
257
  * one line `interrupted`.
273
258
  *
274
259
  * All text goes through the document, so a control character in a message or a stack frame never reaches the terminal.
275
- * Render it with `Render.context` and `Render.plain`, `ansi`, `markdown` or `githubLog`, or `Doc.print` it.
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.
276
262
  *
277
263
  * @public
278
264
  */
@@ -287,7 +273,7 @@ var CliFailure = class {
287
273
  static toDoc = (cause, options) => {
288
274
  const reasons = cause.reasons;
289
275
  if (reasons.length > 0 && reasons.every(Cause.isInterruptReason)) return [Doc.paragraph("interrupted")];
290
- const displayPath = options?.displayPath ?? ((absolute) => absolute);
276
+ const displayPath = options?.displayPath ?? identity;
291
277
  return reasons.flatMap((reason) => {
292
278
  const spans = options?.spans ?? "app";
293
279
  const appDir = packageDirOf(options?.appModule);
package/CliFormat.js ADDED
@@ -0,0 +1,69 @@
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
+ /**
9
+ * A `--format` flag and the one print that honours it, so a command builds its report once as a `Doc` and its data
10
+ * once as a value, and never writes per-format output by hand.
11
+ *
12
+ * @remarks
13
+ * The flag is core's `Flag.Literals` with the kit's names, default and description; the print hands every document
14
+ * format to `Doc.print` and writes `json` itself.
15
+ *
16
+ * ```ts
17
+ * const format = CliFormat.flag(["auto", "markdown", "json"])
18
+ * const doctor = Command.make("doctor", { format }, ({ format }) =>
19
+ * CliFormat.print(format, { doc: () => report(results), json: () => results }),
20
+ * )
21
+ * ```
22
+ *
23
+ * @public
24
+ */
25
+ var CliFormat = class {
26
+ constructor() {}
27
+ /**
28
+ * A `--format` flag offering `formats`.
29
+ *
30
+ * @remarks
31
+ * Its default is `options.default`, else `auto` when it is offered, else the first format. Any other value is a
32
+ * usage error from core's parser, naming the accepted ones.
33
+ *
34
+ * @param formats - the formats the command offers, at least one
35
+ * @param options - the default, the flag's name and its description
36
+ */
37
+ static flag = (formats, options) => {
38
+ const fallback = options?.default ?? (formats.includes("auto") ? "auto" : formats[0]);
39
+ return Flag.Literals(options?.name ?? "format", formats).pipe(Flag.withDefault(fallback), Flag.withDescription(options?.description ?? `Output format: ${formats.join(", ")} (default ${fallback})`));
40
+ };
41
+ /**
42
+ * Print a command's output in `format`.
43
+ *
44
+ * @remarks
45
+ * `json` writes `output.json()` as JSON, one `Console.log` (or `Console.error` for stderr), indented by `indent`.
46
+ * Under GitHub Actions a `##[` inside it is written as the JSON escape `#\u0023[`, which decodes to the same text,
47
+ * so the runner cannot read it as a command. A value JSON cannot hold (a cycle, a bigint) is a defect. Every other
48
+ * format prints `output.doc()` with `Doc.print`, which renders it as `auto` or the named renderer would.
49
+ *
50
+ * @param format - the chosen format, usually the value of {@link CliFormat.flag}
51
+ * @param output - the document and the data
52
+ * @param options - the stream and the JSON indent
53
+ */
54
+ static print = (format, output, options) => Effect.gen(function* () {
55
+ const stream = options?.stream ?? "stdout";
56
+ if (format !== "json") return yield* Doc.print(output.doc(), {
57
+ stream,
58
+ format
59
+ });
60
+ const data = output.json;
61
+ if (data === void 0) return yield* Effect.die(/* @__PURE__ */ new Error("CliFormat.print: format json needs output.json"));
62
+ const text = yield* Effect.sync(() => JSON.stringify(data(), null, options?.indent ?? 2));
63
+ if (text === void 0) return yield* Effect.die(/* @__PURE__ */ new Error("CliFormat.print: output.json() is not JSON"));
64
+ const safe = (yield* underGithubActions) ? neutralizeJson(text) : text;
65
+ yield* stream === "stdout" ? Console.log(safe) : Console.error(safe);
66
+ });
67
+ };
68
+
69
+ export { CliFormat };
package/CliLog.js CHANGED
@@ -19,18 +19,20 @@ const indentOf = (indent) => {
19
19
  if (typeof indent === "number") return Number.isFinite(indent) && indent > 0 ? " ".repeat(Math.min(MAX_INDENT, Math.floor(indent))) : "";
20
20
  return sanitize(indent).replace(/\r\n|\r|\n/g, "");
21
21
  };
22
- /** The accepted spellings of a level, lower-cased, to the level they mean. */
23
- const LEVELS = {
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
- };
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()));
34
36
  const LEVEL_STYLES = {
35
37
  FATAL: {
36
38
  fg: "red",
@@ -57,14 +59,14 @@ const readLevel = (explicit, envVar) => Effect.gen(function* () {
57
59
  level: "None",
58
60
  invalid: void 0
59
61
  };
60
- const level = LEVELS[raw.value.toLowerCase()];
61
- if (level !== void 0) return {
62
- level,
62
+ const level = parseLevel(raw.value);
63
+ if (Option.isSome(level)) return {
64
+ level: level.value,
63
65
  invalid: void 0
64
66
  };
65
67
  return {
66
68
  level: "None",
67
- invalid: `${envVar}=${raw.value} is not a log level (${Object.keys(LEVELS).join("|")}); ignoring it`
69
+ invalid: `${envVar}=${raw.value} is not a log level (${[...LEVELS.keys()].join("|")}); ignoring it`
68
70
  };
69
71
  });
70
72
  /**
@@ -149,6 +151,26 @@ var CliLog = class CliLog {
149
151
  * cannot lower it below the level the layer installed, because Effect has already dropped those records.
150
152
  */
151
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
+ }
152
174
  static layer(options = {}) {
153
175
  const file = "file" in options ? options.file : void 0;
154
176
  const format = options.format ?? "auto";
package/CliRuntime.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Cancelled } from "./Cancelled.js";
2
2
  import { sanitize } from "./Fmt.js";
3
+ import { failureText, tagOf } from "./internal/inspect.js";
3
4
  import { NotInteractive } from "./NotInteractive.js";
4
5
  import { FailureTargetCell, currentTarget, fallbackTarget, guardConsumerLines, linesOf, plainFailureLines, readSpans, refreshFailureTarget } from "./internal/failureTarget.js";
5
6
  import { CliColor } from "./CliColor.js";
@@ -11,24 +12,29 @@ import { CliLogger, makeCliLogger } from "./CliLogger.js";
11
12
  import { CliLog, envBuildLogLayer, platformLogLayer } from "./CliLog.js";
12
13
  import { ExitRequested } from "./internal/ExitRequested.js";
13
14
  import { routeHelpOnUsageError } from "./internal/HelpRouting.js";
14
- import { Cause, Effect, Layer, Logger, MutableRef, Runtime } from "effect";
15
- import { CliError } from "effect/cli";
15
+ import { Cause, Effect, Layer, Logger, MutableRef, Option, Runtime } from "effect";
16
+ import { CliError, CliOutput } from "effect/cli";
16
17
  import { CommandNeutralizer } from "@effected/github-commands";
18
+ import { withDistribution, withDistributionVersion } from "@effected/engine";
17
19
 
18
20
  // src/CliRuntime.ts
19
21
  const isShowHelp = (u) => CliError.isCliError(u) && u._tag === "ShowHelp";
20
22
  /** A `UserError` `Command.runWith` already printed: it sets the mark to `false` after rendering. */
21
23
  const isRenderedUserError = (u) => CliError.isCliError(u) && u._tag === "UserError" && Runtime.getErrorReported(u) === false;
22
- /** The last line of defence of a failure report: the error's text, sanitised and neutralized, whatever else broke. */
23
- const lastResort = (error) => {
24
- let text;
25
- try {
26
- text = String(error);
27
- } catch {
28
- text = "[unprintable failure]";
29
- }
30
- return CommandNeutralizer.lines(sanitize(text));
24
+ /**
25
+ * The program with `CurrentDistribution` set (engine's own `withDistribution`), and `--version` decorated with the
26
+ * carrier's suffix (`withDistributionVersion`), wrapping whatever `formatVersion` is in effect rather than replacing a
27
+ * coloured or custom one.
28
+ */
29
+ const withCarrier = (program, distribution) => {
30
+ const provided = program.pipe(withDistribution(distribution));
31
+ return distribution === null ? provided : Effect.updateService(provided, CliOutput.Formatter, (formatter) => ({
32
+ ...formatter,
33
+ formatVersion: withDistributionVersion(Option.some(distribution))(formatter.formatVersion)
34
+ }));
31
35
  };
36
+ /** The last line of defence of a failure report: the error's text, sanitised and neutralized, whatever else broke. */
37
+ const lastResort = (error) => CommandNeutralizer.lines(sanitize(failureText(error, "[unprintable failure]")));
32
38
  const toLines = (rendered) => typeof rendered === "string" ? [rendered] : rendered;
33
39
  /**
34
40
  * The error's own exit code when it carries one, otherwise the fallback.
@@ -39,7 +45,17 @@ const toLines = (rendered) => typeof rendered === "string" ? [rendered] : render
39
45
  * silently override a deliberate `1`. Testing for the marker keeps "the error
40
46
  * chose its code" distinct from "nothing chose".
41
47
  */
42
- const chooseExitCode = (error, fallback) => typeof error === "object" && error !== null && Runtime.errorExitCode in error ? Runtime.getErrorExitCode(error) : fallback ?? 1;
48
+ const chooseExitCode = (error, fallback, byTag) => {
49
+ if (typeof error === "object" && error !== null && Runtime.errorExitCode in error) return Runtime.getErrorExitCode(error);
50
+ if (byTag === void 0) return fallback ?? 1;
51
+ const tag = tagOf(error);
52
+ return tag !== void 0 && Object.hasOwn(byTag, tag) ? byTag[tag] : fallback ?? 1;
53
+ };
54
+ /** Every `exitCodes` entry an integer in `1..255`, or a `RangeError` naming the first that is not. */
55
+ const checkExitCodes = (exitCodes) => {
56
+ if (exitCodes === void 0) return;
57
+ for (const [tag, code] of Object.entries(exitCodes)) if (!isExitCode(code) || code === 0) throw new RangeError(`CliRuntime: exitCodes.${tag} must be an integer 1..255, received ${String(code)}`);
58
+ };
43
59
  /**
44
60
  * Report a CLI program's failures through the program's own logger.
45
61
  *
@@ -148,43 +164,49 @@ var CliRuntime = class CliRuntime {
148
164
  *
149
165
  * @param options - how to render the failure and which exit codes to use
150
166
  */
151
- static reportFailures = (options = {}) => (effect) => effect.pipe(Effect.catchCause((cause) => {
152
- if (Cause.hasInterruptsOnly(cause)) return Effect.failCause(cause);
153
- const error = Cause.squash(cause);
154
- if (error instanceof ExitRequested) return Effect.fail(error);
155
- if (isShowHelp(error)) {
156
- const code = error.errors.length > 0 ? options.usageExitCode ?? 64 : 0;
157
- return Effect.fail(CliRuntime.reported(error, code));
158
- }
159
- if (isRenderedUserError(error)) return Effect.fail(CliRuntime.reported(error, chooseExitCode(error, options.usageExitCode ?? 64)));
160
- const render = options.render;
161
- return Effect.gen(function* () {
162
- const target = yield* currentTarget.pipe(Effect.catchCause(() => Effect.succeed(fallbackTarget)));
163
- const reportLines = (status, spans) => {
164
- try {
165
- return linesOf(cause, target, status, spans ?? target.spans);
166
- } catch {
167
+ static reportFailures = (options = {}) => {
168
+ checkExitCodes(options.exitCodes);
169
+ return (effect) => effect.pipe(Effect.catchCause((cause) => {
170
+ if (Cause.hasInterruptsOnly(cause)) return Effect.failCause(cause);
171
+ const error = Cause.squash(cause);
172
+ if (error instanceof ExitRequested) return Effect.fail(error);
173
+ if (isShowHelp(error)) {
174
+ const code = error.errors.length > 0 ? options.usageExitCode ?? 64 : 0;
175
+ return Effect.fail(CliRuntime.reported(error, code));
176
+ }
177
+ if (isRenderedUserError(error)) return Effect.fail(CliRuntime.reported(error, chooseExitCode(error, options.usageExitCode ?? 64)));
178
+ const render = options.render;
179
+ return Effect.gen(function* () {
180
+ const target = yield* currentTarget.pipe(Effect.catchCause(() => Effect.succeed(fallbackTarget)));
181
+ const reportLines = (status, spans) => {
167
182
  try {
168
- return plainFailureLines(cause, status, spans ?? target.spans);
183
+ return linesOf(cause, target, status, spans ?? target.spans);
169
184
  } catch {
170
- return lastResort(error);
185
+ try {
186
+ return plainFailureLines(cause, status, spans ?? target.spans);
187
+ } catch {
188
+ return lastResort(error);
189
+ }
171
190
  }
172
- }
173
- };
174
- const defaultLines = reportLines(true);
175
- const details = {
176
- cause,
177
- isDefect: !Cause.hasFails(cause),
178
- isCancelled: error instanceof Cancelled,
179
- isNotInteractive: error instanceof NotInteractive,
180
- defaultLines,
181
- lines: (options) => options?.status === false || options?.spans !== void 0 ? reportLines(options?.status !== false, options?.spans) : defaultLines
182
- };
183
- const lines = render === void 0 ? defaultLines : yield* guardConsumerLines(toLines(render(error, details)));
184
- for (const line of lines) yield* Effect.logError(line).pipe(Effect.provideService(TrustedLine, true));
185
- return yield* Effect.fail(CliRuntime.reported(error, chooseExitCode(error, options.exitCode)));
186
- });
187
- }));
191
+ };
192
+ const defaultLines = reportLines(true);
193
+ const isDefect = !Cause.hasFails(cause);
194
+ const exitCode = chooseExitCode(error, options.exitCode, isDefect ? void 0 : options.exitCodes);
195
+ const details = {
196
+ cause,
197
+ isDefect,
198
+ exitCode,
199
+ isCancelled: error instanceof Cancelled,
200
+ isNotInteractive: error instanceof NotInteractive,
201
+ defaultLines,
202
+ lines: (options) => options?.status === false || options?.spans !== void 0 ? reportLines(options?.status !== false, options?.spans) : defaultLines
203
+ };
204
+ const lines = render === void 0 ? defaultLines : yield* guardConsumerLines(toLines(render(error, details)));
205
+ for (const line of lines) yield* Effect.logError(line).pipe(Effect.provideService(TrustedLine, true));
206
+ return yield* Effect.fail(CliRuntime.reported(error, exitCode));
207
+ });
208
+ }));
209
+ };
188
210
  static main(program, options) {
189
211
  const env = options.env === void 0 ? void 0 : CliEnv.layer(options.env);
190
212
  const envLog = options.env?.log;
@@ -193,14 +215,14 @@ var CliRuntime = class CliRuntime {
193
215
  const { spans, invalid } = yield* readSpans(options.env?.spans, options.env?.spansEnvVar);
194
216
  if (invalid !== void 0) yield* Effect.logWarning(invalid).pipe(Effect.provideService(Logger.CurrentLoggers, /* @__PURE__ */ new Set([makeCliLogger(envLog?.logger)])));
195
217
  yield* refreshFailureTarget(void 0, {
196
- displayPath: options.env?.displayPath,
197
218
  stackFrames: options.env?.stackFrames,
198
219
  spans,
199
220
  appModule: options.env?.appModule
200
221
  });
201
222
  }))).pipe(Layer.provideMerge(env));
223
+ const carried = options.distribution === void 0 ? program : withCarrier(program, options.distribution);
202
224
  const run = Effect.gen(function* () {
203
- yield* options.helpOnUsageError === "stderr" ? routeHelpOnUsageError(program) : program;
225
+ yield* options.helpOnUsageError === "stderr" ? routeHelpOnUsageError(carried) : carried;
204
226
  const exit = yield* CliExit;
205
227
  const code = MutableRef.get(exit.code);
206
228
  if (!isExitCode(code)) return yield* Effect.die(/* @__PURE__ */ new Error(`CliRuntime.main: CliExit code must be an integer 0..255, received ${code}`));
package/Fmt.js CHANGED
@@ -1,5 +1,6 @@
1
- import { displayWidth, graphemes, stripAnsi } from "./internal/displayWidth.js";
2
- import { Duration, Option } from "effect";
1
+ import { causeMessage } from "./internal/causeMessage.js";
2
+ import { displayWidth, graphemes, isEscapeOnly, stripAnsi } from "./internal/displayWidth.js";
3
+ import { Duration, Option, Predicate } from "effect";
3
4
 
4
5
  // src/Fmt.ts
5
6
  const CONTROL = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g;
@@ -137,6 +138,126 @@ var Fmt = class {
137
138
  * @param plural - the noun otherwise; `singular` with an `s` appended by default
138
139
  */
139
140
  static plural = (n, singular, plural) => `${n} ${n === 1 ? singular : plural ?? `${singular}s`}`;
141
+ /**
142
+ * Pad `text` to `width` terminal columns, measured as {@link Fmt.width} measures: a wide character counts two and
143
+ * an escape sequence none, so coloured text pads to the same column as plain text.
144
+ *
145
+ * @remarks
146
+ * Text already `width` columns or wider is returned unchanged: compose with {@link Fmt.truncate} to cut it. A
147
+ * `width` that is not a finite number pads nothing. Never throws.
148
+ *
149
+ * @param text - the text to pad
150
+ * @param width - the columns the result should take
151
+ * @param options - the alignment and the fill
152
+ */
153
+ static pad = (text, width, options) => {
154
+ const missing = Number.isFinite(width) ? Math.floor(width) - displayWidth(text) : 0;
155
+ if (missing <= 0) return text;
156
+ const fill = options?.fill !== void 0 && displayWidth(options.fill) === 1 && stripAnsi(options.fill) === options.fill ? options.fill : " ";
157
+ switch (options?.align) {
158
+ case "right": return `${fill.repeat(missing)}${text}`;
159
+ case "center": {
160
+ const before = Math.floor(missing / 2);
161
+ return `${fill.repeat(before)}${text}${fill.repeat(missing - before)}`;
162
+ }
163
+ default: return `${text}${fill.repeat(missing)}`;
164
+ }
165
+ };
166
+ /**
167
+ * `text` without its escape sequences: CSI (colour, cursor moves, erases), OSC (titles, OSC 8 hyperlinks) ending in
168
+ * BEL or ST, the DCS, SOS, PM and APC strings, and every other escape, the nF sequences (`ESC ( B`) and the
169
+ * two-byte ones (`ESC 7`, `ESC 8`, `ESC =`, `ESC c`).
170
+ *
171
+ * @remarks
172
+ * The same strip {@link Fmt.width} and {@link Fmt.sanitize} start from. Only complete sequences go: a lone ESC, or
173
+ * any other control character, is kept, and so is an ESC in front of a lowercase letter other than `c`, which a
174
+ * stray ESC before a word would otherwise cost the word its first letter. An unterminated CSI or OSC keeps its `[`
175
+ * or `]`. Use {@link Fmt.sanitize} for text that will be laid out and printed.
176
+ *
177
+ * @param text - the text
178
+ */
179
+ static stripAnsi = (text) => stripAnsi(text);
180
+ /**
181
+ * Whether `text` is nothing but escape sequences, as {@link Fmt.stripAnsi} reads them: a cursor hide or show, a
182
+ * clear-screen, an erase-scrollback. Empty text is not escape-only, and neither is a bare control character
183
+ * (`\r`, BEL), which is not an escape sequence.
184
+ *
185
+ * @param text - the text
186
+ */
187
+ static isEscapeOnly = (text) => isEscapeOnly(text);
188
+ /**
189
+ * Integers as compact ranges: `[1, 2, 3, 5, 10, 11]` is `"1-3,5,10-11"`, a run of consecutive numbers written
190
+ * `start-end` and the rest one by one, joined with commas.
191
+ *
192
+ * @remarks
193
+ * The input is sorted and its duplicates dropped first, so order does not matter. Anything that is not a finite
194
+ * integer is left out, and no input is `""`. Never throws.
195
+ *
196
+ * @param numbers - the integers, such as line numbers
197
+ */
198
+ static ranges = (numbers) => {
199
+ const sorted = [...new Set(Array.from(numbers).filter((n) => Number.isInteger(n)))].sort((a, b) => a - b);
200
+ const parts = [];
201
+ let index = 0;
202
+ while (index < sorted.length) {
203
+ const start = sorted[index];
204
+ let end = start;
205
+ while (index + 1 < sorted.length && sorted[index + 1] === end + 1) end = sorted[++index];
206
+ parts.push(start === end ? `${start}` : `${start}-${end}`);
207
+ index++;
208
+ }
209
+ return parts.join(",");
210
+ };
211
+ /**
212
+ * Any value as text, without ever throwing.
213
+ *
214
+ * @remarks
215
+ * A string is itself; a number, boolean, bigint or symbol is `String(value)`; `undefined` and `null` are
216
+ * `"undefined"` and `"null"`. A value with a string `message` (an `Error`, one from another realm, a plain
217
+ * `{ message }`) is that message. Anything else is its JSON, or `String(value)` when it has none (a cycle, a
218
+ * function), or `"[unprintable value]"` when that throws too. A getter that throws is a value with no message.
219
+ *
220
+ * The text is not sanitised: pass it through {@link Fmt.sanitize} before printing data you do not control. For
221
+ * the messages along an error's `cause` chain, use {@link Fmt.causeMessage}.
222
+ *
223
+ * @param value - the value
224
+ */
225
+ static describe = (value) => {
226
+ if (Predicate.isString(value)) return value;
227
+ if (value === void 0 || value === null) return String(value);
228
+ if (typeof value !== "object" && typeof value !== "function") return String(value);
229
+ try {
230
+ if (Predicate.hasProperty(value, "message") && Predicate.isString(value.message)) return value.message;
231
+ } catch {}
232
+ try {
233
+ const json = JSON.stringify(value);
234
+ if (json !== void 0) return json;
235
+ } catch {}
236
+ try {
237
+ return String(value);
238
+ } catch {
239
+ return "[unprintable value]";
240
+ }
241
+ };
242
+ /**
243
+ * Every distinct non-empty message along a value's `cause` chain, nearest first and joined with `: `, or
244
+ * `undefined` when nothing along it has one.
245
+ *
246
+ * @remarks
247
+ * The kit's one guarded cause-message read, the one its own errors render a preserved cause with. A node's message
248
+ * is a thrown string or a non-empty string `message` property, read without `instanceof Error`, so an error from
249
+ * another realm or a plain `{ message }` object counts. A deeper message the joined text already contains is not
250
+ * repeated, and one that contains the joined text replaces it, so a `SqlError` wrapping SQLite's own text reads
251
+ * `Failed to execute statement: SQLITE_BUSY: database is locked`. It is the whole chain, not the innermost
252
+ * message alone. The walk stops after eight nodes, so a cyclic chain terminates.
253
+ *
254
+ * Never throws: a `message` getter that throws leaves that node without text, and a `cause` getter that throws ends
255
+ * the walk with the text found so far, `undefined` when there is none, so a caller's own fallback
256
+ * (`?? Fmt.describe(value)`) applies.
257
+ *
258
+ * @param value - the error, or whatever was thrown
259
+ */
260
+ static causeMessage = (value) => causeMessage(value);
140
261
  };
141
262
 
142
263
  export { Fmt, sanitize };
package/Render.js CHANGED
@@ -4,6 +4,7 @@ import { Glyphs } from "./Glyphs.js";
4
4
  import { paintStyle } from "./internal/ansi.js";
5
5
  import { Token } from "./Token.js";
6
6
  import { CliTheme } from "./CliTheme.js";
7
+ import { CurrentDisplayPath, identity } from "./internal/displayPath.js";
7
8
  import { renderAnsi } from "./internal/renderAnsi.js";
8
9
  import { renderPlain } from "./internal/renderPlain.js";
9
10
  import { renderGithubLog } from "./internal/renderGithubLog.js";
@@ -62,6 +63,8 @@ var Render = class {
62
63
  * - `groupDepth` is the fiber's count of open Actions log groups (the fiber-local keyed by
63
64
  * `WorkflowCommand.groupDepthKey`, which `ActionLogger.group` provides), set when it is above `0`, so a
64
65
  * document printed inside a group opens no second one; see {@link RenderContext.groupDepth}.
66
+ * - `displayPath` is the option, else the run's own path display (`CliEnvOptions.displayPath`, which
67
+ * `CliEnv.layer` sets), else the identity.
65
68
  *
66
69
  * @param stream - the stream the output is for
67
70
  * @param options - an explicit width and a path display function
@@ -87,7 +90,7 @@ var Render = class {
87
90
  hyperlinks: terminal[stream].hyperlinks,
88
91
  audience: kind
89
92
  }),
90
- displayPath: options?.displayPath ?? ((absolute) => absolute)
93
+ displayPath: options?.displayPath ?? (yield* CurrentDisplayPath)
91
94
  };
92
95
  });
93
96
  /**
@@ -117,7 +120,7 @@ var Render = class {
117
120
  hyperlinks: true,
118
121
  audience: options.audience
119
122
  }),
120
- displayPath: options.displayPath ?? ((absolute) => absolute),
123
+ displayPath: options.displayPath ?? identity,
121
124
  ...options.neutralizeWorkflowCommands ?? options.audience === "ci" ? { neutralizeWorkflowCommands: true } : options.neutralizeWorkflowCommands === false ? { neutralizeWorkflowCommands: false } : {},
122
125
  ...options.linkBase === void 0 ? {} : { linkBase: options.linkBase },
123
126
  ...options.groupDepth === void 0 ? {} : { groupDepth: options.groupDepth }