icore 1.0.19 → 2.0.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.
@@ -6,6 +6,7 @@
6
6
  * - preparing and running command facades;
7
7
  * - rendering presentation results;
8
8
  * - writing command output through the output facade;
9
+ * - reporting terminal errors through caller-configured policy;
9
10
  *
10
11
  * This file must not contain argv tokenization, option validation internals,
11
12
  * domain behavior, or application-specific report mapping.
@@ -27,68 +28,113 @@ function createTerminalApp(options) {
27
28
  const presentation = options.presentation ?? (0, facade_1.createPresentation)();
28
29
  const output = options.output ?? (0, facade_2.createOutput)();
29
30
  const resolveFormat = options.resolveFormat ?? resolvePreparedFormat;
31
+ async function reportError(error, context = {
32
+ phase: 'external'
33
+ }) {
34
+ const renderedError = options.errorPolicy?.renderError === undefined
35
+ ? renderTerminalError(error)
36
+ : options.errorPolicy.renderError(error, context);
37
+ await output.error(renderedError);
38
+ return options.errorPolicy?.resolveExitCode === undefined
39
+ ? resolveTerminalExitCode()
40
+ : options.errorPolicy.resolveExitCode(error, context);
41
+ }
30
42
  async function writePreparedOutput(prepared, terminalOutput) {
31
- const format = resolveFormat(prepared);
32
- await writeTerminalOutput(terminalOutput, format, presentation, output);
43
+ const renderedOutput = renderTerminalOutput(terminalOutput, resolveFormat(prepared), presentation);
44
+ await writeTerminalOutput(renderedOutput, output);
33
45
  }
34
46
  async function runPrepared(prepared, context) {
47
+ return runPreparedFromArgs(prepared, context);
48
+ }
49
+ async function runPreparedFromArgs(prepared, context, args) {
50
+ let result;
35
51
  try {
36
- const result = await options.commands.run(prepared, context);
37
- await writeTerminalOutput(result, resolveFormat(prepared), presentation, output);
52
+ result = await options.commands.run(prepared, context);
53
+ }
54
+ catch (error) {
55
+ return reportError(error, createPreparedErrorContext('execute', prepared, args));
56
+ }
57
+ let renderedOutput;
58
+ try {
59
+ renderedOutput = renderTerminalOutput(result, resolveFormat(prepared), presentation);
60
+ }
61
+ catch (error) {
62
+ return reportError(error, createPreparedErrorContext('render', prepared, args));
63
+ }
64
+ try {
65
+ await writeTerminalOutput(renderedOutput, output);
38
66
  return 0;
39
67
  }
40
68
  catch (error) {
41
- await output.error(renderTerminalError(error));
42
- return 1;
69
+ return reportError(error, createPreparedErrorContext('write', prepared, args));
43
70
  }
44
71
  }
45
72
  return {
46
73
  commands: options.commands,
47
74
  presentation,
48
75
  output,
76
+ reportError,
49
77
  prepare(args, commandOptions) {
50
78
  return options.commands.prepare(args, commandOptions);
51
79
  },
52
80
  runPrepared,
53
81
  writePreparedOutput,
54
82
  async run(args, context, commandOptions) {
83
+ let prepared;
55
84
  try {
56
- const prepared = await options.commands.prepare(args, commandOptions);
57
- return runPrepared(prepared, context);
85
+ prepared = await options.commands.prepare(args, commandOptions);
58
86
  }
59
87
  catch (error) {
60
- await output.error(renderTerminalError(error));
61
- return 1;
88
+ return reportError(error, {
89
+ phase: 'prepare',
90
+ args
91
+ });
62
92
  }
93
+ return runPreparedFromArgs(prepared, context, args);
63
94
  }
64
95
  };
65
96
  }
66
- /**
67
- * Writes only terminal-supported command output shapes.
68
- *
69
- * This keeps the terminal boundary explicit: application objects must be
70
- * mapped to text or presentation views before they reach stdout.
71
- */
72
- async function writeTerminalOutput(result, format, presentation, output) {
97
+ function renderTerminalOutput(result, format, presentation) {
73
98
  if (result === undefined) {
74
- return;
99
+ return undefined;
75
100
  }
76
101
  if (typeof result === 'string') {
77
- await output.write(result);
78
- return;
102
+ return result;
79
103
  }
80
104
  if (isAsyncIterable(result)) {
81
- for await (const chunk of result) {
82
- await output.write(chunk);
83
- }
84
- return;
105
+ return result;
85
106
  }
86
107
  if ((0, result_renderer_1.isPresentationResult)(result)) {
87
- await output.write(presentation.render(result, format));
88
- return;
108
+ return presentation.render(result, format);
89
109
  }
90
110
  throw new Error('Expected terminal command output');
91
111
  }
112
+ /** Writes already rendered terminal text while preserving stream backpressure. */
113
+ async function writeTerminalOutput(result, output) {
114
+ if (result === undefined) {
115
+ return;
116
+ }
117
+ if (typeof result === 'string') {
118
+ await output.write(result);
119
+ return;
120
+ }
121
+ for await (const chunk of result) {
122
+ await output.write(chunk);
123
+ }
124
+ }
125
+ function createPreparedErrorContext(phase, prepared, args) {
126
+ if (args === undefined) {
127
+ return {
128
+ phase,
129
+ prepared
130
+ };
131
+ }
132
+ return {
133
+ phase,
134
+ prepared,
135
+ args
136
+ };
137
+ }
92
138
  /**
93
139
  * Keeps the default format convention local to terminal composition.
94
140
  */
@@ -107,6 +153,9 @@ function renderTerminalError(error) {
107
153
  }
108
154
  return `${String(error)}\n`;
109
155
  }
156
+ function resolveTerminalExitCode() {
157
+ return 1;
158
+ }
110
159
  function isAsyncIterable(value) {
111
160
  return (typeof value === 'object'
112
161
  && value !== null
@@ -116,3 +116,70 @@ const appWithFormatPolicy = createTerminalApp({
116
116
  This is useful when one command has a different default output contract. It is
117
117
  also a point where the application can make a bad abstraction: avoid hiding
118
118
  format decisions here when a normal `--format` option would be clearer.
119
+
120
+ ## Reuse Error Reporting In A Custom Lifecycle
121
+
122
+ Configure one error policy when the application sometimes uses `app.run(...)`
123
+ and sometimes owns command execution itself. The built-in flow and explicit
124
+ `app.reportError(...)` calls use the same policy.
125
+
126
+ ```ts
127
+ import { isIcoreError } from 'icore';
128
+
129
+ const appWithErrorPolicy = createTerminalApp({
130
+ commands,
131
+ presentation,
132
+ output: createOutput(),
133
+ errorPolicy: {
134
+ renderError(error) {
135
+ const message = error instanceof Error
136
+ ? error.message
137
+ : String(error);
138
+
139
+ return `${message}\n`;
140
+ },
141
+ resolveExitCode(error) {
142
+ return isIcoreError(error) && error.category === 'usage'
143
+ ? 2
144
+ : 1;
145
+ }
146
+ }
147
+ });
148
+ ```
149
+
150
+ When the application owns context creation and cleanup, it can prepare and run
151
+ the command explicitly while keeping terminal diagnostics consistent:
152
+
153
+ ```ts
154
+ const prepared = await appWithErrorPolicy.prepare(args, {
155
+ strict: true
156
+ });
157
+ let result;
158
+
159
+ try {
160
+ result = await appWithErrorPolicy.commands.run(prepared, context);
161
+ }
162
+ catch (error) {
163
+ process.exitCode = await appWithErrorPolicy.reportError(error, {
164
+ phase: 'execute',
165
+ args,
166
+ prepared
167
+ });
168
+
169
+ return;
170
+ }
171
+
172
+ try {
173
+ await appWithErrorPolicy.writePreparedOutput(prepared, result);
174
+ }
175
+ catch (error) {
176
+ process.exitCode = await appWithErrorPolicy.reportError(error, {
177
+ phase: 'write',
178
+ args,
179
+ prepared
180
+ });
181
+ }
182
+ ```
183
+
184
+ The application still owns help text and lifecycle behavior. The policy only
185
+ provides a shared terminal reporting boundary.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icore",
3
- "version": "1.0.19",
3
+ "version": "2.0.0",
4
4
  "description": "Declarative command line interface and terminal presentation mechanics for Node.js",
5
5
  "keywords": [
6
6
  "command-line",
@@ -34,7 +34,7 @@
34
34
  "scripts": {
35
35
  "build": "tsc",
36
36
  "test": "fwa --prune",
37
- "lint": "eslint src"
37
+ "lint": "eslint"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@eslint/js": "^9.39.4",