@loomcli/core 0.2.0 → 0.4.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 (51) hide show
  1. package/dist/application.d.ts +59 -34
  2. package/dist/application.js +162 -59
  3. package/dist/chain.d.ts +25 -6
  4. package/dist/chain.js +99 -15
  5. package/dist/command.d.ts +146 -42
  6. package/dist/command.js +642 -78
  7. package/dist/environment.d.ts +22 -0
  8. package/dist/environment.js +1 -0
  9. package/dist/errors.d.ts +31 -59
  10. package/dist/errors.js +49 -106
  11. package/dist/extension.d.ts +80 -20
  12. package/dist/extension.js +115 -30
  13. package/dist/globals.d.ts +14 -25
  14. package/dist/globals.js +10 -61
  15. package/dist/glyphs.generated.d.ts +464 -0
  16. package/dist/glyphs.generated.js +491 -0
  17. package/dist/host.js +2 -1
  18. package/dist/index.d.ts +15 -6
  19. package/dist/index.js +5 -2
  20. package/dist/inspect.d.ts +38 -4
  21. package/dist/inspect.js +69 -6
  22. package/dist/lanes.d.ts +26 -0
  23. package/dist/lanes.js +45 -0
  24. package/dist/output.d.ts +93 -15
  25. package/dist/output.js +307 -34
  26. package/dist/plugin.d.ts +32 -16
  27. package/dist/plugin.js +46 -18
  28. package/dist/rendering.d.ts +21 -0
  29. package/dist/rendering.js +72 -0
  30. package/dist/sequence.d.ts +41 -0
  31. package/dist/sequence.js +225 -0
  32. package/dist/style-ansi.d.ts +13 -0
  33. package/dist/style-ansi.js +306 -0
  34. package/dist/style-layout.d.ts +29 -0
  35. package/dist/style-layout.js +228 -0
  36. package/dist/style-resolve.d.ts +6 -0
  37. package/dist/style-resolve.js +26 -0
  38. package/dist/style-state.d.ts +14 -0
  39. package/dist/style-state.js +179 -0
  40. package/dist/style-wire.d.ts +31 -0
  41. package/dist/style-wire.js +201 -0
  42. package/dist/style.d.ts +86 -0
  43. package/dist/style.js +201 -0
  44. package/dist/theme.d.ts +3 -0
  45. package/dist/theme.js +22 -0
  46. package/dist/types.d.ts +174 -21
  47. package/dist/validation.d.ts +8 -1
  48. package/dist/validation.js +17 -2
  49. package/dist/view.d.ts +180 -0
  50. package/dist/view.js +307 -0
  51. package/package.json +2 -1
package/dist/inspect.d.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  import type { BuiltGraph } from './command.js';
2
- /** One declared argument. `default` wraps the declared value, so an explicit `undefined` shows. */
2
+ import type { DeclaredResult } from './types.js';
3
+ /** The plain JSON Schema a validated input publishes, or `null` where the graph holds no shape. */
4
+ type InputSchema = Readonly<Record<string, unknown>> | null;
5
+ /**
6
+ * One declared argument. `default` wraps the declared value, so an explicit `undefined` shows, and
7
+ * `schema` is the input schema its validator publishes through the Standard JSON Schema converter.
8
+ */
3
9
  interface ArgumentNode {
4
10
  readonly name: string;
5
11
  readonly description: string | undefined;
@@ -7,6 +13,7 @@ interface ArgumentNode {
7
13
  readonly variadic: boolean;
8
14
  readonly validated: boolean;
9
15
  readonly validateOmitted: boolean;
16
+ readonly schema: InputSchema;
10
17
  readonly default: {
11
18
  readonly value: unknown;
12
19
  } | undefined;
@@ -14,10 +21,15 @@ interface ArgumentNode {
14
21
  }
15
22
  /**
16
23
  * One declared option, in the shape its type gives it. Spellings are the accepted CLI forms, and
17
- * `scope` tells an application's own option from a plugin option, which reaches no action.
24
+ * `scope` tells an application's own option from a plugin option, which reaches no action. An
25
+ * option a plugin's lifecycle hook declared on a Command is that Command's own in every respect, so
26
+ * it reads `application` and names no plugin.
18
27
  * `hidden` is `false` unless the declaration says `true`, and `deprecated` is the declared
19
28
  * migration message or `undefined`. A listing projection omits a hidden node and marks a
20
29
  * deprecated one; parsing binds without reading either.
30
+ * `schema` is the input schema the validator publishes. A Boolean option validates nothing, so its
31
+ * variant carries the field at `null`, and every projection built on the node holds if a later
32
+ * contract lets it validate.
21
33
  */
22
34
  type OptionNode = {
23
35
  readonly type: 'string';
@@ -32,6 +44,7 @@ type OptionNode = {
32
44
  readonly multiple: boolean;
33
45
  readonly validated: boolean;
34
46
  readonly validateOmitted: boolean;
47
+ readonly schema: InputSchema;
35
48
  readonly default: {
36
49
  readonly value: unknown;
37
50
  } | undefined;
@@ -47,6 +60,7 @@ type OptionNode = {
47
60
  readonly short: string | null;
48
61
  readonly negative: string | null;
49
62
  readonly polarity: 'positive' | 'negative' | 'both';
63
+ readonly schema: InputSchema;
50
64
  readonly extensions: Readonly<Record<string, unknown>>;
51
65
  };
52
66
  /**
@@ -64,11 +78,21 @@ interface CommandNode {
64
78
  readonly hidden: boolean;
65
79
  readonly deprecated: string | undefined;
66
80
  readonly hasAction: boolean;
81
+ readonly result: ResultNode | null;
67
82
  readonly arguments: readonly ArgumentNode[];
68
83
  readonly options: readonly OptionNode[];
69
84
  readonly children: readonly CommandNode[];
70
85
  readonly extensions: Readonly<Record<string, unknown>>;
71
86
  }
87
+ /**
88
+ * The result one Command declares: the unit its action emits, the view names in record order, and
89
+ * the name of the view core renders when nothing selects another.
90
+ */
91
+ interface ResultNode {
92
+ readonly kind: 'value' | 'rows';
93
+ readonly views: readonly string[];
94
+ readonly default: string;
95
+ }
72
96
  /**
73
97
  * One built graph as plain data. The globals appear once here and in no `CommandNode`. `version`
74
98
  * and `description` are the Application's own core facts. `version` is the declared string, or
@@ -82,6 +106,16 @@ interface CommandGraph {
82
106
  readonly globals: readonly OptionNode[];
83
107
  readonly root: CommandNode;
84
108
  }
109
+ /**
110
+ * A snapshot of one value. Arrays and plain objects are copied and frozen to any depth, so a
111
+ * consumer cannot reach the source through the copy, and a later call reports the value again.
112
+ * Primitives and library objects, such as a class instance or a `Date` a schema produced, are
113
+ * reported as they are, because core cannot copy them meaningfully. The graph reads it for a
114
+ * declared value and the chain reads it for the request one middleware holds.
115
+ */
116
+ export declare function snapshot(value: unknown): unknown;
117
+ /** The declared result as plain data, or `null` on a Command that declares none. */
118
+ declare function resultNode(result: DeclaredResult | undefined): ResultNode | null;
85
119
  /**
86
120
  * Renders one built graph as frozen plain data. Nothing here reads a host fact or a schema. The
87
121
  * globals list holds the application's own options, then each installed plugin's in installation
@@ -91,5 +125,5 @@ declare function inspectGraph(name: string, graph: BuiltGraph, facts: {
91
125
  description: string | undefined;
92
126
  version: string;
93
127
  }): CommandGraph;
94
- export type { ArgumentNode, CommandGraph, CommandNode, OptionNode };
95
- export { inspectGraph };
128
+ export type { ArgumentNode, CommandGraph, CommandNode, OptionNode, ResultNode };
129
+ export { inspectGraph, resultNode };
package/dist/inspect.js CHANGED
@@ -17,23 +17,71 @@ function spellingsOf(table, name) {
17
17
  return spellings;
18
18
  }
19
19
  /**
20
- * A snapshot of one declared value. Arrays and plain objects are copied and frozen to any depth, so
21
- * a consumer cannot reach the declaration through the graph, and a later call reports the declared
22
- * value again. Primitives and library objects are reported as they are.
20
+ * A snapshot of one value. Arrays and plain objects are copied and frozen to any depth, so a
21
+ * consumer cannot reach the source through the copy, and a later call reports the value again.
22
+ * Primitives and library objects, such as a class instance or a `Date` a schema produced, are
23
+ * reported as they are, because core cannot copy them meaningfully. The graph reads it for a
24
+ * declared value and the chain reads it for the request one middleware holds.
23
25
  */
24
- function snapshot(value) {
26
+ export function snapshot(value) {
25
27
  if (Array.isArray(value)) {
26
28
  return Object.freeze(value.map((entry) => snapshot(entry)));
27
29
  }
28
30
  if (isPlainObject(value)) {
29
- return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, snapshot(entry)])));
31
+ return snapshotRecord(value);
30
32
  }
31
33
  return value;
32
34
  }
35
+ /** The snapshot of one plain object, under the record type the caller already established. */
36
+ function snapshotRecord(value) {
37
+ return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, snapshot(entry)])));
38
+ }
33
39
  /** A declared default is wrapped, so `default: undefined` reads apart from no default at all. */
34
40
  function declaredDefault(config) {
35
41
  return 'default' in config ? Object.freeze({ value: snapshot(config.default) }) : undefined;
36
42
  }
43
+ /**
44
+ * The JSON Schema draft build asks every converter for, with no library options. Every converter
45
+ * receives this one object, so it is frozen against a library that writes to its argument.
46
+ */
47
+ const schemaTarget = Object.freeze({ target: 'draft-2020-12' });
48
+ /** Whether a validator declares the Standard JSON Schema converter, both sides, beside `validate`. */
49
+ function publishesSchema(schema) {
50
+ const props = schema['~standard'];
51
+ return ('jsonSchema' in props &&
52
+ typeof props.jsonSchema === 'object' &&
53
+ props.jsonSchema !== null &&
54
+ 'input' in props.jsonSchema &&
55
+ typeof props.jsonSchema.input === 'function' &&
56
+ 'output' in props.jsonSchema &&
57
+ typeof props.jsonSchema.output === 'function');
58
+ }
59
+ /**
60
+ * The input-side schema a declaration's validator publishes, snapshotted the way a declared
61
+ * default is, or `null` where the graph holds no published shape: no validator, a validator with
62
+ * no converter, or a converter that throws or returns anything but a plain object. The contract of
63
+ * 2026-09-19 made that last case a declaration error `inspect()` alone reports; that diagnostic is
64
+ * held while the question of how a run tells development from a distributed application is
65
+ * decided, so it reads `null` on both paths.
66
+ */
67
+ function inputSchema(config) {
68
+ const schema = 'validate' in config ? config.validate : undefined;
69
+ if (schema === undefined) {
70
+ return null;
71
+ }
72
+ // The converter is the library's code from the first property read.
73
+ // A throw on reaching it and a throw on calling it are one failure.
74
+ try {
75
+ if (!publishesSchema(schema)) {
76
+ return null;
77
+ }
78
+ const published = schema['~standard'].jsonSchema.input(schemaTarget);
79
+ return isPlainObject(published) ? snapshotRecord(published) : null;
80
+ }
81
+ catch {
82
+ return null;
83
+ }
84
+ }
37
85
  /** One declaration's extension record, which is the shared empty one when it carries no value. */
38
86
  function extensionsOf(records, declaration) {
39
87
  return records.get(declaration) ?? noExtensions;
@@ -52,6 +100,7 @@ function optionNode(input, { records, scope, table }) {
52
100
  name,
53
101
  negative,
54
102
  polarity: config.polarity ?? 'positive',
103
+ schema: null,
55
104
  scope,
56
105
  short,
57
106
  type: 'boolean',
@@ -67,6 +116,7 @@ function optionNode(input, { records, scope, table }) {
67
116
  multiple: config.multiple === true,
68
117
  name,
69
118
  required: config.required === true,
119
+ schema: inputSchema(config),
70
120
  scope,
71
121
  short,
72
122
  type: 'string',
@@ -84,6 +134,7 @@ function argumentNode(slot, records) {
84
134
  extensions: extensionsOf(records, slot.input),
85
135
  name,
86
136
  required: slot.required,
137
+ schema: inputSchema(config),
87
138
  validateOmitted: validatesOmission(slot.input),
88
139
  validated: config.validate !== undefined,
89
140
  variadic: slot.variadic,
@@ -111,9 +162,21 @@ function commandNode(command, place) {
111
162
  name: command.name,
112
163
  options: Object.freeze(optionNodes(command.inputs, { records, scope: 'application', table: command.options })),
113
164
  path,
165
+ result: resultNode(command.result),
114
166
  };
115
167
  return Object.freeze(node);
116
168
  }
169
+ /** The declared result as plain data, or `null` on a Command that declares none. */
170
+ function resultNode(result) {
171
+ if (!result) {
172
+ return null;
173
+ }
174
+ return Object.freeze({
175
+ default: result.default,
176
+ kind: result.kind,
177
+ views: Object.freeze([...result.views.keys()]),
178
+ });
179
+ }
117
180
  /**
118
181
  * Renders one built graph as frozen plain data. Nothing here reads a host fact or a schema. The
119
182
  * globals list holds the application's own options, then each installed plugin's in installation
@@ -138,4 +201,4 @@ function inspectGraph(name, graph, facts) {
138
201
  };
139
202
  return Object.freeze(inspected);
140
203
  }
141
- export { inspectGraph };
204
+ export { inspectGraph, resultNode };
@@ -0,0 +1,26 @@
1
+ import type { AnyDeclaredView, DeclaredView } from './view.js';
2
+ /** The five semantic methods, each with the glyph and the style token its lane carries. */
3
+ type Lane = 'error' | 'info' | 'print' | 'success' | 'warn';
4
+ /**
5
+ * The five lane views core declares, one per semantic method. An application replaces the function
6
+ * of any of them through `views`, and the replacement owns the gutter the default supplies.
7
+ */
8
+ declare const lanes: Readonly<Record<Lane, DeclaredView<string>>>;
9
+ /**
10
+ * What one sequence that stopped early reports: the Command it belongs to, the rows its source
11
+ * produced before the stop, and the rows core wrote. `head` is no row, so it is not counted.
12
+ */
13
+ interface IncompleteResult {
14
+ path: readonly string[];
15
+ yielded: number;
16
+ written: number;
17
+ }
18
+ /**
19
+ * The line a sequence writes on stderr when it stopped before its end, so an empty result and a
20
+ * truncated one never read alike. An override that returns the empty string silences it.
21
+ */
22
+ declare const incompleteResult: DeclaredView<IncompleteResult>;
23
+ /** Every view core declares, so one build register holds their identities from the start. */
24
+ declare const coreViews: readonly AnyDeclaredView[];
25
+ export type { IncompleteResult, Lane };
26
+ export { coreViews, incompleteResult, lanes };
package/dist/lanes.js ADDED
@@ -0,0 +1,45 @@
1
+ import { routedSubject } from './errors.js';
2
+ import { glyph } from './glyphs.generated.js';
3
+ import { view } from './view.js';
4
+ /**
5
+ * The identity prefix core's own views take, the package name by the plugin-identity convention.
6
+ * Core compiles from `src` alone, so the name is spelled here rather than read from the manifest.
7
+ */
8
+ const core = '@loomcli/core';
9
+ /**
10
+ * The default function of a marked lane: the matching glyph and one space before the first line,
11
+ * and continuation lines indented by the measured gutter without repeating the glyph. The
12
+ * semantic method appends the newline, so the view returns none.
13
+ */
14
+ function marked(mark) {
15
+ return (message, context) => {
16
+ const glyphText = glyph[mark];
17
+ const gutter = ' '.repeat(context.width(glyphText) + 1);
18
+ return `${context.style[mark](glyphText)} ${message.replace(/(?<newline>\r?\n)(?!$)/gu, `$<newline>${gutter}`)}`;
19
+ };
20
+ }
21
+ /** One lane view over the original message string, line breaks and authored styles included. */
22
+ function lane(name, render) {
23
+ return view(`${core}/lanes/${name}`, { render });
24
+ }
25
+ /**
26
+ * The five lane views core declares, one per semantic method. An application replaces the function
27
+ * of any of them through `views`, and the replacement owns the gutter the default supplies.
28
+ */
29
+ const lanes = {
30
+ error: lane('error', marked('error')),
31
+ info: lane('info', marked('info')),
32
+ print: lane('print', (message) => message),
33
+ success: lane('success', marked('success')),
34
+ warn: lane('warn', marked('warning')),
35
+ };
36
+ /**
37
+ * The line a sequence writes on stderr when it stopped before its end, so an empty result and a
38
+ * truncated one never read alike. An override that returns the empty string silences it.
39
+ */
40
+ const incompleteResult = view(`${core}/results/incomplete`, {
41
+ render: ({ path, written, yielded }) => `Output is incomplete: ${routedSubject(path)} stopped after ${yielded} rows, ${written} written.\n`,
42
+ });
43
+ /** Every view core declares, so one build register holds their identities from the start. */
44
+ const coreViews = [...Object.values(lanes), incompleteResult];
45
+ export { coreViews, incompleteResult, lanes };
package/dist/output.d.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  import type { Writable } from 'node:stream';
2
- import type { Host, Out } from './types.js';
2
+ import type { Lane } from './lanes.js';
3
+ import type { RenderingPolicy } from './rendering.js';
4
+ import type { Palette } from './style-state.js';
5
+ import type { ActionChannel, Host, OpenResult, Out, ResultBinding, ViewContext } from './types.js';
6
+ import type { ViewRegistry } from './view.js';
3
7
  type WriteState = {
4
8
  kind: 'ok';
5
9
  } | {
@@ -7,44 +11,118 @@ type WriteState = {
7
11
  error: unknown;
8
12
  };
9
13
  /** The semantic calls, which choose a destination. A rendered value has no purpose of its own. */
10
- type Purpose = 'print' | 'info' | 'success' | 'warn' | 'error';
14
+ type Purpose = Lane;
15
+ /** The two streams every write site names. */
16
+ type Stream = 'stdout' | 'stderr';
11
17
  export declare class Output {
12
- readonly host: Pick<Host, 'stdout' | 'stderr'>;
18
+ readonly host: Host;
19
+ private readonly signal;
13
20
  private readonly destinations;
14
21
  private renderFault;
15
- readonly out: Out;
16
- constructor(host: Pick<Host, 'stdout' | 'stderr'>);
17
- /** A semantic message is one line on its destination; only `print` writes to stdout. */
18
- emit(kind: Purpose, message: string): Promise<void>;
22
+ private readonly stops;
23
+ private route;
24
+ /**
25
+ * The channel every caller writes through. It is typed with the result left open, because the
26
+ * declaration a call answers to is checked where the action was authored.
27
+ */
28
+ readonly out: Out<OpenResult>;
29
+ private palette;
30
+ private policy;
31
+ private registry;
32
+ style: import("./style.js").ContextualStyle;
33
+ constructor(host: Host, signal: AbortSignal);
34
+ /**
35
+ * The channel one action receives. On a Command that declares a result nothing the action writes
36
+ * but the result reaches stdout: `print` and `render` move to stderr, and they move the view
37
+ * context with the destination, so capability detection follows the stream the bytes reach. The
38
+ * destination is decided here, from the declaration, and never from the view a run selected.
39
+ */
40
+ channel(binding: ResultBinding): ActionChannel;
41
+ /**
42
+ * One `out.results` call on the action's channel. The declaration decides the unit, the selected
43
+ * view decides how it renders, and stdout carries the result under either one. The selected
44
+ * view is the one a middleware named before the action dispatched, or the declaration's default
45
+ * when none did. A call the declaration does not answer for is a fault of the lane and writes
46
+ * nothing.
47
+ */
48
+ private results;
49
+ /**
50
+ * One fault of the results lane: the call rejects, and the same failure is reported after this
51
+ * invocation's primary outcome, so a call the action never awaited still turns a would-be 0 into
52
+ * 1 and one the action let propagate is reported once.
53
+ */
54
+ private resultFault;
55
+ /** The registry one invocation resolves through, republished as each contributor is read. */
56
+ useViews(registry: ViewRegistry): void;
57
+ /** The routed path, published once routing resolved it, which an incomplete sequence names. */
58
+ useRoute(path: readonly string[]): void;
59
+ /** What this invocation's output raised beside its calls, in the order it was raised. */
60
+ get stopped(): readonly unknown[];
61
+ configure(policy: RenderingPolicy, palette: Palette): void;
62
+ context(destination: Stream): ViewContext;
63
+ /**
64
+ * A semantic message is one line on its destination; only `print` writes to stdout. The message
65
+ * is checked before the lane view runs, and this call appends the one newline after it, so a
66
+ * lane view returns none and an override that returns the empty string still writes one.
67
+ */
68
+ emit(kind: Purpose, message: string, destination: Stream): Promise<void>;
19
69
  /**
20
70
  * The failure report of one invocation. Like `render`, the text is queued on its destination
21
- * exactly as given: the caller already carries its own trailing newline, whether that text came
22
- * from a registered renderer or from core's own default text.
71
+ * after style resolution: the caller already carries its own trailing newline, whether that text
72
+ * came from a resolved view or from core's own default text.
23
73
  */
24
74
  report(text: string): Promise<void>;
25
75
  /**
26
- * The renderer owns every byte, so its text is queued on stdout exactly as returned. A throw or
27
- * a non-string return rejects this call alone: nothing is written for it, later output still
28
- * writes, and the recorded cause ends the invocation once the action has completed.
76
+ * One `out.render` call, dispatched on the shape of the view it was handed. The two shapes are
77
+ * exclusive, so a JavaScript author's value that carries both, or neither, is the output-view
78
+ * fault of this call and nothing is written for it.
79
+ */
80
+ private renderValue;
81
+ /**
82
+ * The view one sequence writes through, in the shape its own view carries. The caller supplies
83
+ * the contributors the view resolves through: `out.render`'s call-site view resolves through
84
+ * this invocation's registry, as ADR-0021 requires, and a result's view resolves through none,
85
+ * because a result's selected view is replaced by view name alone.
86
+ */
87
+ private sequenceView;
88
+ /**
89
+ * One sequence, which holds its place on the destination from here until its last piece is
90
+ * written. The returned rejection is observed here as well, because an action that never awaits
91
+ * the call must not end the process with an unhandled rejection.
92
+ */
93
+ private sequence;
94
+ /**
95
+ * The line one incomplete sequence writes on stderr, ahead of the fault's own report. It resolves
96
+ * through the registry like any other rendered output, so an override that returns the empty
97
+ * string silences it and one that throws is a view fault. A stderr that has failed already takes
98
+ * the plain fallback path and no further.
99
+ */
100
+ private incomplete;
101
+ /**
102
+ * The write site owns its newline; core resolves the view's marked text before queuing it. A
103
+ * throw or a non-string return rejects this call alone: nothing is written for it, later output
104
+ * still writes, and the recorded cause ends the invocation once the action has completed.
29
105
  */
30
106
  private rendered;
31
107
  /**
32
- * The rejected call. The first renderer failure is the reported one, so a later one adds no
108
+ * The rejected call. The first view failure is the reported one, so a later one adds no
33
109
  * second diagnostic, and the rejection is observed here as well, because an action that never
34
110
  * awaits the call must not end the process with an unhandled rejection.
35
111
  */
36
112
  private renderFailed;
37
- /** What a renderer failed with during this invocation, if one did. */
113
+ /** What a view failed with during this invocation, if one did. */
38
114
  get fault(): {
39
115
  cause: unknown;
40
116
  } | undefined;
117
+ /** The queue one stream writes through, opened the first time this invocation reaches it. */
118
+ private destination;
41
119
  private write;
42
120
  settle(): Promise<WriteState>;
43
121
  dispose(): void;
44
122
  }
45
123
  /**
46
124
  * The plain fallback path: a fresh destination on stderr, outside the invocation's queues and
47
- * outside every registration, so no application code runs on it. The caller composes the newlines
125
+ * outside every override, so no application code runs on it. The caller composes the newlines
48
126
  * between whatever it is reporting, then passes the one string this writes verbatim. A failed
49
127
  * write ends reporting.
50
128
  */