@effected/cli 0.22.1 → 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/ui/CliUi.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { Cancelled } from "../Cancelled.js";
2
2
  import { CliInteractive } from "../CliInteractive.js";
3
- import { underGithubActions } from "../internal/autoFormat.js";
4
3
  import { CliTheme } from "../CliTheme.js";
5
4
  import { NotInteractive } from "../NotInteractive.js";
6
5
  import { answerWithoutPerson } from "../internal/fallbackAnswer.js";
@@ -11,8 +10,10 @@ import { lazyView } from "./internal/lazyView.js";
11
10
  import { mountPermit } from "./internal/mountPermit.js";
12
11
  import { UiRenderOptions } from "./internal/renderOptions.js";
13
12
  import { useScreenGuard } from "./internal/ScreenContext.js";
14
- import { uiProviders } from "./internal/UiProviders.js";
13
+ import { treeBase, uiProviders } from "./internal/UiProviders.js";
15
14
  import { live } from "./CliUiLive.js";
15
+ import { liveDetached } from "./CliUiDetached.js";
16
+ import { routeStream } from "./CliUiRouteStream.js";
16
17
  import { KeyTable, useKeys } from "./KeyTable.js";
17
18
  import { Cause, Deferred, Effect, Exit, Option, Semaphore } from "effect";
18
19
  import { Audience } from "@effected/env";
@@ -36,8 +37,9 @@ const RootKeys = (props) => {
36
37
  return props.children;
37
38
  };
38
39
  const SCREEN_EXITED = "@effected/cli/ui: the screen exited without resolving or cancelling";
39
- const mount = (screen, theme, clear, crash, neutralize) => Effect.gen(function* () {
40
+ const mount = (screen, theme, clear, crash) => Effect.gen(function* () {
40
41
  const overrides = yield* UiRenderOptions;
42
+ const base = yield* treeBase(theme);
41
43
  yield* Effect.acquireRelease(Effect.sync(() => overrides.onMount?.("screen")), (_, exit) => Effect.sync(() => {
42
44
  const died = Exit.isFailure(exit) ? exit.cause.reasons.find(Cause.isDieReason) : void 0;
43
45
  const interrupted = Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause);
@@ -63,11 +65,9 @@ const mount = (screen, theme, clear, crash, neutralize) => Effect.gen(function*
63
65
  const tree = react.createElement(errorBoundary(), {
64
66
  onError: die,
65
67
  children: uiProviders({
68
+ ...base,
66
69
  cancel: control.cancel,
67
- die,
68
- theme,
69
- glyphs: theme.glyphs,
70
- ...neutralize ? { neutralizeWorkflowCommands: true } : {}
70
+ die
71
71
  }, react.createElement(RootKeys, {
72
72
  cancel: control.cancel,
73
73
  children: element
@@ -153,9 +153,8 @@ var CliUi = class CliUi {
153
153
  static run = (screen, options) => Effect.gen(function* () {
154
154
  if (!(yield* CliInteractive)) return yield* Effect.fail(new NotInteractive());
155
155
  const theme = yield* audienceTheme;
156
- const neutralize = yield* underGithubActions;
157
156
  const crash = { current: void 0 };
158
- const exit = yield* Effect.exit(Semaphore.withPermit(mountPermit, Effect.scoped(mount(screen, theme, options?.clear === true, crash, neutralize))));
157
+ const exit = yield* Effect.exit(Semaphore.withPermit(mountPermit, Effect.scoped(mount(screen, theme, options?.clear === true, crash))));
159
158
  const crashed = crash.current;
160
159
  const interrupted = Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause);
161
160
  if (crashed === void 0 || interrupted) return yield* exit;
@@ -184,14 +183,11 @@ var CliUi = class CliUi {
184
183
  * ```
185
184
  */
186
185
  static context = Effect.gen(function* () {
187
- const theme = yield* audienceTheme;
188
- const neutralize = yield* underGithubActions;
186
+ const base = yield* treeBase(yield* audienceTheme);
189
187
  yield* loadInk;
190
188
  return {
191
189
  "~@effected/cli/ui/UiContextValue": true,
192
- theme,
193
- glyphs: theme.glyphs,
194
- ...neutralize ? { neutralizeWorkflowCommands: true } : {}
190
+ ...base
195
191
  };
196
192
  });
197
193
  /**
@@ -272,6 +268,82 @@ var CliUi = class CliUi {
272
268
  */
273
269
  static live = live;
274
270
  /**
271
+ * A live view hosted by code that is not built on Effect: a test runner's reporter, a bundler plugin, a build hook.
272
+ *
273
+ * @remarks
274
+ * It runs `CliUi.live` in a fiber of its own, under `env` and in a scope of its own, and returns at once. A `PubSub`
275
+ * given as `events` is subscribed before it returns, so the first event published after the call is not lost.
276
+ * `env` is the environment the view reads, built by the host, which owns the platform: under Node,
277
+ * `CliEnv.layer().pipe(Layer.provide(NodeServices.layer))`; a test gives `CliEnv.layerTest` with a session's layer.
278
+ *
279
+ * End it with `close`, and only then shut the `PubSub` down: `close` folds the subscription's queued tail, commits
280
+ * the last run and releases the scope, and a shutdown before it drops that tail. `close` never rejects; `exit`
281
+ * says how the view ended. Forward output the host did not write through `printAbove`, or hand
282
+ * `CliUi.routeStream` the detached view's `printAbove` as its printer.
283
+ *
284
+ * A render that needs an environment service is given as an effect that makes the render function: it runs once
285
+ * under `env`, before the view is set up, so `CliLinks` or `TerminalEnv` come from the same build the view reads and
286
+ * the layer is not built a second time to resolve them. `env` must provide what that effect reads.
287
+ *
288
+ * Inside an Effect program, use `CliUi.live` in a scope instead.
289
+ *
290
+ * @example
291
+ * ```ts
292
+ * const view = CliUi.liveDetached(
293
+ * {
294
+ * ...options,
295
+ * render: Effect.gen(function* () {
296
+ * const links = yield* CliLinks
297
+ * return (state: State, frame: number) => drawWith(links, state, frame)
298
+ * }),
299
+ * },
300
+ * CliEnv.layer().pipe(Layer.provide(NodeServices.layer)),
301
+ * )
302
+ * ```
303
+ *
304
+ * @param options - the live view's options, with the events as a `PubSub` or a stream and the render given directly
305
+ * or as an effect that makes it
306
+ * @param env - the environment the view runs under, providing what a render effect reads
307
+ */
308
+ static liveDetached = liveDetached;
309
+ /**
310
+ * A stream to hand to a writer in place of a terminal stream, which routes what it writes line by line through
311
+ * `printer`, usually a live view's `printAbove`, and writes whatever the printer declines to `original`.
312
+ *
313
+ * @remarks
314
+ * For output the program does not write itself: a child process's stderr, or the streams of a test runner or a
315
+ * bundler running in-process. Each complete line is offered to `printer`; a `false` (no frame mounted) writes it,
316
+ * with its line break, to `original`. Text without a line break yet is held until one comes, until `flush`, or
317
+ * until it passes `maxHeld` characters, when it is offered as a line of its own. Bytes are decoded as UTF-8 across
318
+ * writes, so a character split between two writes arrives whole.
319
+ *
320
+ * Everything written to `original` is the decoded text, so a character split across two writes, by whichever path,
321
+ * arrives once and whole. A write that is nothing but escape sequences (a cursor hide, a clear-screen) goes
322
+ * straight to `original` when no partial line is held, since there is nothing to print above a frame. Ending
323
+ * `writable` flushes the held text, so a writer's last line with no line break is printed, not lost. A line's own cursor
324
+ * movements still tear the frame; `carriageReturn: "last"` keeps only what a progress bar redrawn with `\r` would
325
+ * have shown.
326
+ *
327
+ * The returned `writable` reports the original's `isTTY`, `columns`, `rows`, `getColorDepth`, `hasColors` and
328
+ * `getWindowSize` where the original has them. A printer that throws fails the write. Node only, as `./ui` is.
329
+ *
330
+ * @example
331
+ * ```ts
332
+ * const view = CliUi.liveDetached(options, env)
333
+ * const routed = CliUi.routeStream(process.stderr, view.printAbove, { stream: "stderr" })
334
+ * runner.errorStream = routed.writable
335
+ * // ...at the end
336
+ * routed.flush()
337
+ * await view.close()
338
+ * ```
339
+ *
340
+ * @param original - the stream the writer would have written to
341
+ * @param printer - where each line is offered first: `true` when it printed it, `false` with nothing written
342
+ * @param options - the printer's stream, the hold limit, carriage returns and a text tap
343
+ * @throws RangeError when `maxHeld` is not a non-negative integer
344
+ */
345
+ static routeStream = routeStream;
346
+ /**
275
347
  * Run `screen` from a handler when the run is interactive; otherwise answer with `otherwise`, or fail with
276
348
  * `NotInteractive` when there is none.
277
349
  *
@@ -0,0 +1,36 @@
1
+ import { live } from "./CliUiLive.js";
2
+ import { Deferred, Effect, Exit, Fiber, PubSub, Scope } from "effect";
3
+
4
+ // src/ui/CliUiDetached.ts
5
+ /** @internal */
6
+ const liveDetached = (options, env) => {
7
+ const scope = Effect.runSync(Scope.make());
8
+ const { events: source, render: drawing, ...rest } = options;
9
+ const render = Effect.isEffect(drawing) ? drawing : Effect.succeed(drawing);
10
+ const events = PubSub.isPubSub(source) ? Effect.runSync(PubSub.subscribe(source).pipe(Scope.provide(scope))) : source;
11
+ const mounted = Effect.runSync(Deferred.make());
12
+ const program = Effect.flatMap(render, (draw) => live({
13
+ ...rest,
14
+ render: draw,
15
+ events
16
+ })).pipe(Effect.tap((handle) => Deferred.succeed(mounted, handle)), Effect.flatMap((handle) => handle.done), Scope.provide(scope), Effect.provide(env), Effect.onExit((exit) => Deferred.done(mounted, Exit.isFailure(exit) ? Exit.failCause(exit.cause) : Exit.die(/* @__PURE__ */ new Error("CliUi.liveDetached: the view ended")))));
17
+ const fiber = Effect.runFork(program);
18
+ const exit = Effect.runPromise(Fiber.await(fiber));
19
+ const handle = Effect.runPromise(Deferred.await(mounted));
20
+ let closing;
21
+ let print;
22
+ handle.then((mountedHandle) => {
23
+ print = mountedHandle.printAbove;
24
+ }, () => void 0);
25
+ return {
26
+ handle,
27
+ exit,
28
+ printAbove: (stream, line) => closing === void 0 && print !== void 0 ? print(stream, line) : false,
29
+ close: () => {
30
+ closing ??= Effect.runPromise(Deferred.await(mounted).pipe(Effect.flatMap((mountedHandle) => mountedHandle.close), Effect.exit, Effect.ensuring(Scope.close(scope, Exit.void)), Effect.asVoid));
31
+ return closing;
32
+ }
33
+ };
34
+ };
35
+
36
+ export { liveDetached };
package/ui/CliUiLive.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { CliInteractive } from "../CliInteractive.js";
2
- import { autoFormat, underGithubActions } from "../internal/autoFormat.js";
2
+ import { autoFormat } from "../internal/autoFormat.js";
3
3
  import { CliLinks } from "../CliLinks.js";
4
4
  import { CliTheme } from "../CliTheme.js";
5
5
  import { Render } from "../Render.js";
@@ -11,7 +11,7 @@ import { makeInkConsole } from "./internal/inkConsole.js";
11
11
  import { LazyViewShapeError, loadView } from "./internal/lazyView.js";
12
12
  import { mountPermit } from "./internal/mountPermit.js";
13
13
  import { UiRenderOptions } from "./internal/renderOptions.js";
14
- import { uiProviders } from "./internal/UiProviders.js";
14
+ import { treeBase, uiProviders } from "./internal/UiProviders.js";
15
15
  import { useTerminalSize } from "./UiTheme.js";
16
16
  import { Cause, Clock, Duration, Effect, Exit, Fiber, Option, PubSub, Pull, Queue, Schedule, Scheduler, Scope, Stream } from "effect";
17
17
  import { Audience, TerminalEnv } from "@effected/env";
@@ -53,12 +53,8 @@ const live = (options) => Effect.gen(function* () {
53
53
  const cliTheme = yield* CliTheme;
54
54
  const theme = CliTheme.forAudience(cliTheme.forStream("stdout"), Option.isSome(audience) ? audience.value.kind : void 0);
55
55
  const colour = theme.color;
56
- const neutralize = yield* underGithubActions;
57
- const provided = {
58
- theme,
59
- glyphs: theme.glyphs,
60
- ...neutralize ? { neutralizeWorkflowCommands: true } : {}
61
- };
56
+ const provided = yield* treeBase(theme);
57
+ const neutralize = provided.neutralizeWorkflowCommands === true;
62
58
  const interactive = yield* CliInteractive;
63
59
  const streams = yield* UiStreams;
64
60
  const overrides = yield* UiRenderOptions;
@@ -158,7 +154,8 @@ const live = (options) => Effect.gen(function* () {
158
154
  audience: Option.isSome(audience) ? audience.value.kind : "human",
159
155
  color: theme.color,
160
156
  glyphs: theme.glyphs,
161
- ...neutralize ? { neutralizeWorkflowCommands: true } : {}
157
+ ...neutralize ? { neutralizeWorkflowCommands: true } : {},
158
+ displayPath: provided.displayPath
162
159
  });
163
160
  });
164
161
  /** A run's `final` document, printed as `Doc.print` would, to the view's stdout; never Ink. */
@@ -0,0 +1,72 @@
1
+ import { makeLineRouter } from "./internal/lineRouter.js";
2
+ import { Writable } from "node:stream";
3
+
4
+ // src/ui/CliUiRouteStream.ts
5
+ /** The terminal facts a writer reads off its stream, delegated only where the original carries them. */
6
+ const DELEGATED = [
7
+ "isTTY",
8
+ "columns",
9
+ "rows",
10
+ "getColorDepth",
11
+ "hasColors",
12
+ "getWindowSize"
13
+ ];
14
+ const DEFAULT_MAX_HELD = 8192;
15
+ /** Run one `Writable` step and settle its callback: with nothing, or with what the step threw, as an `Error`. */
16
+ const guarded = (step, callback) => {
17
+ try {
18
+ step();
19
+ callback();
20
+ } catch (error) {
21
+ callback(error instanceof Error ? error : new Error(String(error)));
22
+ }
23
+ };
24
+ /** @internal */
25
+ const routeStream = (original, printer, options) => {
26
+ const maxHeld = options.maxHeld ?? DEFAULT_MAX_HELD;
27
+ if (!Number.isInteger(maxHeld) || maxHeld < 0) throw new RangeError(`CliUi.routeStream: maxHeld must be a non-negative integer, got ${String(maxHeld)}`);
28
+ const router = makeLineRouter({
29
+ stream: options.stream,
30
+ printer,
31
+ write: (text) => {
32
+ original.write(text);
33
+ },
34
+ maxHeld,
35
+ carriageReturn: options.carriageReturn ?? "keep",
36
+ onText: options.onText
37
+ });
38
+ const writable = new Writable({
39
+ decodeStrings: false,
40
+ write(chunk, _encoding, callback) {
41
+ guarded(() => router.take(chunk), callback);
42
+ },
43
+ final(callback) {
44
+ guarded(router.flush, callback);
45
+ }
46
+ });
47
+ for (const key of DELEGATED) {
48
+ if (!(key in original)) continue;
49
+ let source;
50
+ let bound;
51
+ Object.defineProperty(writable, key, {
52
+ configurable: true,
53
+ enumerable: true,
54
+ get: () => {
55
+ const value = original[key];
56
+ if (typeof value !== "function") return value;
57
+ if (value !== source) {
58
+ source = value;
59
+ bound = value.bind(original);
60
+ }
61
+ return bound;
62
+ }
63
+ });
64
+ }
65
+ return {
66
+ writable,
67
+ route: router.route,
68
+ flush: router.flush
69
+ };
70
+ };
71
+
72
+ export { routeStream };
package/ui/DocView.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { identity } from "../internal/displayPath.js";
1
2
  import { Render } from "../Render.js";
2
3
  import { fromReact, inkModules } from "./internal/ink.js";
3
4
  import { screenContext } from "./internal/ScreenContext.js";
@@ -6,7 +7,7 @@ import { useTerminalSize } from "./UiTheme.js";
6
7
  // src/ui/DocView.ts
7
8
  const OUTSIDE = "@effected/cli/ui: DocView was drawn with no ctx outside a screen, a live view or a UiProvider, so it has no theme";
8
9
  /** The context a `DocView` builds from its tree's theme: the theme's own paint, so token overrides hold. */
9
- const contextOf = (theme, width, neutralize) => ({
10
+ const contextOf = (theme, width, neutralize, displayPath) => ({
10
11
  ...neutralize ? { neutralizeWorkflowCommands: true } : {},
11
12
  width,
12
13
  audience: "human",
@@ -14,7 +15,7 @@ const contextOf = (theme, width, neutralize) => ({
14
15
  paint: theme.paint,
15
16
  glyphs: theme.glyphs,
16
17
  link: (_target, label) => label,
17
- displayPath: (absolute) => absolute
18
+ displayPath
18
19
  });
19
20
  /** The document's lines as the kit's own renderer lays them out: plain at colour `none`, painted otherwise. */
20
21
  const linesOf = (doc, ctx) => {
@@ -31,12 +32,14 @@ const docView = fromReact((react) => {
31
32
  const given = props.ctx;
32
33
  const theme = screen?.theme;
33
34
  const neutralize = screen?.neutralizeWorkflowCommands === true;
35
+ const displayPath = screen?.displayPath ?? identity;
34
36
  if (given === void 0 && theme === void 0) throw new Error(OUTSIDE);
35
- const ctx = react.useMemo(() => given ?? contextOf(theme, columns, neutralize), [
37
+ const ctx = react.useMemo(() => given ?? contextOf(theme, columns, neutralize, displayPath), [
36
38
  given,
37
39
  theme,
38
40
  columns,
39
- neutralize
41
+ neutralize,
42
+ displayPath
40
43
  ]);
41
44
  const lines = react.useMemo(() => linesOf(props.doc, ctx), [props.doc, ctx]);
42
45
  return react.createElement(ink.Box, {
package/ui/UiProvider.js CHANGED
@@ -34,9 +34,11 @@ const UiProvider = (props) => {
34
34
  const cancel = parent?.cancel;
35
35
  const die = parent?.die;
36
36
  const neutralize = props.value.neutralizeWorkflowCommands === true || parent?.neutralizeWorkflowCommands === true;
37
+ const displayPath = props.value.displayPath ?? parent?.displayPath;
37
38
  const value = react.useMemo(() => ({
38
39
  theme,
39
40
  glyphs,
41
+ ...displayPath === void 0 ? {} : { displayPath },
40
42
  ...columns === void 0 || rows === void 0 ? {} : { size: {
41
43
  columns,
42
44
  rows
@@ -51,7 +53,8 @@ const UiProvider = (props) => {
51
53
  rows,
52
54
  cancel,
53
55
  die,
54
- neutralize
56
+ neutralize,
57
+ displayPath
55
58
  ]);
56
59
  return uiProviders(value, props.children);
57
60
  };
@@ -1,5 +1,8 @@
1
+ import { underGithubActions } from "../../internal/autoFormat.js";
2
+ import { CurrentDisplayPath } from "../../internal/displayPath.js";
1
3
  import { inkModules } from "./ink.js";
2
4
  import { screenContext } from "./ScreenContext.js";
5
+ import { Effect } from "effect";
3
6
 
4
7
  // src/ui/internal/UiProviders.ts
5
8
  /**
@@ -16,5 +19,20 @@ const uiProviders = (value, children) => inkModules().react.createElement(screen
16
19
  value,
17
20
  children
18
21
  });
22
+ /**
23
+ * The base of a kit tree drawn with `theme`: its glyphs, `neutralizeWorkflowCommands` under the GitHub Actions
24
+ * runner, and the run's path display. Read once per mount, so a screen, a live view and `CliUi.context` agree.
25
+ *
26
+ * @internal
27
+ */
28
+ const treeBase = (theme) => Effect.gen(function* () {
29
+ const neutralize = yield* underGithubActions;
30
+ return {
31
+ theme,
32
+ glyphs: theme.glyphs,
33
+ ...neutralize ? { neutralizeWorkflowCommands: true } : {},
34
+ displayPath: yield* CurrentDisplayPath
35
+ };
36
+ });
19
37
 
20
- export { uiProviders };
38
+ export { treeBase, uiProviders };
@@ -1,57 +1,47 @@
1
- import { createRequire } from "node:module";
2
1
  import { Option } from "effect";
3
- import { realpathSync } from "node:fs";
4
- import { fileURLToPath, pathToFileURL } from "node:url";
5
2
 
6
3
  // src/ui/internal/inkChalk.ts
7
4
  const isInkChalk = (value) => (typeof value === "function" || typeof value === "object" && value !== null) && typeof value.level === "number";
8
- /** The runtime's `import.meta.resolve`, when it has one. */
9
- const esmResolve = typeof import.meta.resolve === "function" ? (specifier) => import.meta.resolve(specifier) : void 0;
10
5
  /**
11
- * The path of Ink's entry: through `resolve` (the runtime's `import.meta.resolve` by default), or, where that is
12
- * missing or throws, through Node's CommonJS resolution from this module.
13
- *
14
- * @remarks
15
- * Vite's module runner, which evaluates a Vitest reporter loaded by path, has an `import.meta.resolve` that throws
16
- * ("not supported"); the CommonJS resolution finds the same file, since Ink's `exports` has a `default` condition.
17
- *
18
- * @param resolve - an ESM resolver, to stand in for the runtime's in a test
6
+ * The runtime's `import.meta.resolve` for THIS module, when it has one. It resolves relative to the module that owns
7
+ * it, so it has to be the kit's own: `ModuleResolution` cannot supply it.
19
8
  *
20
9
  * @internal
21
10
  */
22
- const resolveInkEntry = (resolve = esmResolve) => {
23
- if (resolve !== void 0) try {
24
- return fileURLToPath(resolve("ink"));
25
- } catch {}
26
- return createRequire(import.meta.url).resolve("ink");
27
- };
11
+ const esmResolve = typeof import.meta.resolve === "function" ? (specifier) => import.meta.resolve(specifier) : void 0;
28
12
  /**
29
13
  * The chalk instance Ink itself imports, resolved from Ink's own location; `None` when it cannot be resolved.
30
14
  *
31
15
  * @remarks
32
- * One of the three files licensed to touch Node. Ink's `exports` has
33
- * only `"."` and chalk is its own dependency, so the kit cannot import Ink's chalk by name: a `chalk` of the kit's
34
- * own could be a different copy, and setting its level would silently change nothing. Resolving `chalk` from Ink's
35
- * entry ({@link resolveInkEntry}) and importing its realpath yields the very module Ink imports, because Node keys ES
36
- * modules by realpath. A consumer that bundles Ink leaves nothing
37
- * to resolve, and the answer is `None`; it never rejects.
16
+ * Ink's `exports` has only `"."` and chalk is its own dependency, so the kit cannot import Ink's chalk by name: a
17
+ * `chalk` of the kit's own could be a different copy, and setting its level would silently change nothing. So Ink's
18
+ * entry is resolved from here (`esm` first, Node's CommonJS resolution where it is missing or throws, as under Vite's
19
+ * module runner, which evaluates a Vitest reporter loaded by path), `chalk` is resolved from that entry to its
20
+ * realpath, and that file is imported: Node keys ES modules by realpath, so it is the very module Ink imports. The
21
+ * resolution is `@effected/commands/node-sync`'s `ModuleResolution`, loaded only when this runs, so importing `./ui`
22
+ * loads none of it. A consumer that bundles Ink leaves nothing to resolve, and the answer is `None`; it never rejects.
38
23
  *
39
- * @param inkEntryOf - how Ink's entry is found; {@link resolveInkEntry} by default
24
+ * @param esm - an ESM resolver relative to this module; the runtime's `import.meta.resolve` by default, and a stand-in
25
+ * in a test
40
26
  *
41
27
  * @internal
42
28
  */
43
- const inkChalk = async (inkEntryOf = () => resolveInkEntry()) => {
29
+ const inkChalk = async (esm = esmResolve) => {
44
30
  try {
45
- const inkEntry = inkEntryOf();
46
- const chalkPath = realpathSync(createRequire(inkEntry).resolve("chalk"));
47
- const chalk = await import(
48
- /* @vite-ignore */
49
- pathToFileURL(chalkPath).href
50
- );
31
+ const { ModuleResolution } = await import("@effected/commands/node-sync");
32
+ const chalkPath = Option.flatMap(ModuleResolution.resolve("ink", {
33
+ from: import.meta.url,
34
+ esm
35
+ }), (entry) => ModuleResolution.resolve("chalk", {
36
+ from: entry,
37
+ realPath: true
38
+ }));
39
+ if (Option.isNone(chalkPath)) return Option.none();
40
+ const chalk = await ModuleResolution.load(chalkPath.value);
51
41
  return isInkChalk(chalk.default) ? Option.some(chalk.default) : Option.none();
52
42
  } catch {
53
43
  return Option.none();
54
44
  }
55
45
  };
56
46
 
57
- export { inkChalk, resolveInkEntry };
47
+ export { esmResolve, inkChalk };
@@ -0,0 +1,70 @@
1
+ import { isEscapeOnly } from "../../internal/displayWidth.js";
2
+
3
+ // src/ui/internal/lineRouter.ts
4
+ /** The text a printer is handed for one line: a CRLF's `\r` dropped, and under `last` only what the line ends on. */
5
+ const printable = (line, carriageReturn) => {
6
+ const text = line.endsWith("\r") ? line.slice(0, -1) : line;
7
+ if (carriageReturn === "keep") return text;
8
+ const cut = text.lastIndexOf("\r");
9
+ return cut === -1 ? text : text.slice(cut + 1);
10
+ };
11
+ /** @internal */
12
+ const makeLineRouter = (options) => {
13
+ const decoder = new TextDecoder("utf-8");
14
+ let held = "";
15
+ let routing = true;
16
+ const offer = (line) => {
17
+ if (!options.printer(options.stream, printable(line, options.carriageReturn))) options.write(`${line}\n`);
18
+ };
19
+ const route = (text) => {
20
+ if (text === "") return;
21
+ if (!routing) {
22
+ options.write(text);
23
+ return;
24
+ }
25
+ if (held === "" && isEscapeOnly(text)) {
26
+ options.write(text);
27
+ return;
28
+ }
29
+ if (!text.includes("\n")) {
30
+ held += text;
31
+ if (held.length > options.maxHeld) {
32
+ const line = held;
33
+ held = "";
34
+ offer(line);
35
+ }
36
+ return;
37
+ }
38
+ const lines = (held + text).split("\n");
39
+ const rest = lines.pop() ?? "";
40
+ if (rest.length > options.maxHeld) {
41
+ lines.push(rest);
42
+ held = "";
43
+ } else held = rest;
44
+ for (const line of lines) offer(line);
45
+ };
46
+ return {
47
+ take: (chunk) => {
48
+ const text = typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true });
49
+ if (text !== "") options.onText?.(text);
50
+ route(text);
51
+ },
52
+ route: (on) => {
53
+ if (!on && held !== "") {
54
+ options.write(held);
55
+ held = "";
56
+ }
57
+ routing = on;
58
+ },
59
+ flush: () => {
60
+ const pending = decoder.decode();
61
+ if (pending !== "") options.onText?.(pending);
62
+ const text = held + pending;
63
+ held = "";
64
+ if (text === "") return;
65
+ if (!routing || !options.printer(options.stream, printable(text, options.carriageReturn))) options.write(text);
66
+ }
67
+ };
68
+ };
69
+
70
+ export { makeLineRouter };
@@ -236,7 +236,10 @@ const makeTerminal = (options, settings = { screens: "debug" }) => {
236
236
  if (mode === "debug") return chunk;
237
237
  const text = chunk.replace(FRAME_BRACKETS, "");
238
238
  if (text === "" || CONTROLS_ONLY.test(text)) return void 0;
239
- return text.replace(LEADING_MOVES, "").replace(/\n+$/, "");
239
+ const frame = text.replace(LEADING_MOVES, "");
240
+ let end = frame.length;
241
+ while (end > 0 && frame[end - 1] === "\n") end -= 1;
242
+ return frame.slice(0, end);
240
243
  };
241
244
  const fake = makeFakeStreams({
242
245
  columns,