icore 1.0.18 → 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.
- package/CHANGELOG.md +93 -54
- package/dist/argv/parser.js +1 -0
- package/dist/command/mechanics.js +3 -0
- package/dist/errors/icore-error.d.ts +133 -10
- package/dist/errors/icore-error.js +24 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/options/parser.js +7 -1
- package/dist/terminal/app.d.ts +42 -4
- package/dist/terminal/app.js +78 -25
- package/examples/terminal-app.md +67 -0
- package/examples/two-phase-primitives.md +37 -0
- package/package.json +2 -2
- package/readme.md +170 -632
package/dist/terminal/app.js
CHANGED
|
@@ -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,64 +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
|
+
}
|
|
42
|
+
async function writePreparedOutput(prepared, terminalOutput) {
|
|
43
|
+
const renderedOutput = renderTerminalOutput(terminalOutput, resolveFormat(prepared), presentation);
|
|
44
|
+
await writeTerminalOutput(renderedOutput, output);
|
|
45
|
+
}
|
|
30
46
|
async function runPrepared(prepared, context) {
|
|
47
|
+
return runPreparedFromArgs(prepared, context);
|
|
48
|
+
}
|
|
49
|
+
async function runPreparedFromArgs(prepared, context, args) {
|
|
50
|
+
let result;
|
|
31
51
|
try {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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);
|
|
35
66
|
return 0;
|
|
36
67
|
}
|
|
37
68
|
catch (error) {
|
|
38
|
-
|
|
39
|
-
return 1;
|
|
69
|
+
return reportError(error, createPreparedErrorContext('write', prepared, args));
|
|
40
70
|
}
|
|
41
71
|
}
|
|
42
72
|
return {
|
|
43
73
|
commands: options.commands,
|
|
44
74
|
presentation,
|
|
45
75
|
output,
|
|
76
|
+
reportError,
|
|
46
77
|
prepare(args, commandOptions) {
|
|
47
78
|
return options.commands.prepare(args, commandOptions);
|
|
48
79
|
},
|
|
49
80
|
runPrepared,
|
|
81
|
+
writePreparedOutput,
|
|
50
82
|
async run(args, context, commandOptions) {
|
|
83
|
+
let prepared;
|
|
51
84
|
try {
|
|
52
|
-
|
|
53
|
-
return runPrepared(prepared, context);
|
|
85
|
+
prepared = await options.commands.prepare(args, commandOptions);
|
|
54
86
|
}
|
|
55
87
|
catch (error) {
|
|
56
|
-
|
|
57
|
-
|
|
88
|
+
return reportError(error, {
|
|
89
|
+
phase: 'prepare',
|
|
90
|
+
args
|
|
91
|
+
});
|
|
58
92
|
}
|
|
93
|
+
return runPreparedFromArgs(prepared, context, args);
|
|
59
94
|
}
|
|
60
95
|
};
|
|
61
96
|
}
|
|
62
|
-
|
|
63
|
-
* Writes only terminal-supported command output shapes.
|
|
64
|
-
*
|
|
65
|
-
* This keeps the terminal boundary explicit: application objects must be
|
|
66
|
-
* mapped to text or presentation views before they reach stdout.
|
|
67
|
-
*/
|
|
68
|
-
async function writeTerminalOutput(result, format, presentation, output) {
|
|
97
|
+
function renderTerminalOutput(result, format, presentation) {
|
|
69
98
|
if (result === undefined) {
|
|
70
|
-
return;
|
|
99
|
+
return undefined;
|
|
71
100
|
}
|
|
72
101
|
if (typeof result === 'string') {
|
|
73
|
-
|
|
74
|
-
return;
|
|
102
|
+
return result;
|
|
75
103
|
}
|
|
76
104
|
if (isAsyncIterable(result)) {
|
|
77
|
-
|
|
78
|
-
await output.write(chunk);
|
|
79
|
-
}
|
|
80
|
-
return;
|
|
105
|
+
return result;
|
|
81
106
|
}
|
|
82
107
|
if ((0, result_renderer_1.isPresentationResult)(result)) {
|
|
83
|
-
|
|
84
|
-
return;
|
|
108
|
+
return presentation.render(result, format);
|
|
85
109
|
}
|
|
86
110
|
throw new Error('Expected terminal command output');
|
|
87
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
|
+
}
|
|
88
138
|
/**
|
|
89
139
|
* Keeps the default format convention local to terminal composition.
|
|
90
140
|
*/
|
|
@@ -103,6 +153,9 @@ function renderTerminalError(error) {
|
|
|
103
153
|
}
|
|
104
154
|
return `${String(error)}\n`;
|
|
105
155
|
}
|
|
156
|
+
function resolveTerminalExitCode() {
|
|
157
|
+
return 1;
|
|
158
|
+
}
|
|
106
159
|
function isAsyncIterable(value) {
|
|
107
160
|
return (typeof value === 'object'
|
|
108
161
|
&& value !== null
|
package/examples/terminal-app.md
CHANGED
|
@@ -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.
|
|
@@ -104,6 +104,43 @@ finally {
|
|
|
104
104
|
Resource creation and cleanup stay application-owned. The terminal app only
|
|
105
105
|
runs the prepared command and applies the same output behavior as `app.run(...)`.
|
|
106
106
|
|
|
107
|
+
## Write Prepared Output
|
|
108
|
+
|
|
109
|
+
Use `app.writePreparedOutput(...)` when the application needs to inspect the raw
|
|
110
|
+
command result before terminal output is written.
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const prepared = await app.prepare([
|
|
114
|
+
'jobs',
|
|
115
|
+
'run',
|
|
116
|
+
'--job-id',
|
|
117
|
+
'job-42'
|
|
118
|
+
], {
|
|
119
|
+
strict: true
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const context = await createContext(prepared);
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const result = await app.commands.run(prepared, context);
|
|
126
|
+
|
|
127
|
+
if (isShutdownHandle(result)) {
|
|
128
|
+
installShutdownHooks(result);
|
|
129
|
+
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
await app.writePreparedOutput(prepared, result);
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
await cleanup(context);
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Only terminal output belongs here: ready text, streaming text, presentation
|
|
141
|
+
results, or no output. Strings are written exactly as provided; include `\n`
|
|
142
|
+
when line output is desired.
|
|
143
|
+
|
|
107
144
|
## Prepare From A Registry
|
|
108
145
|
|
|
109
146
|
`prepareCommandFromArgs(...)` is the standalone primitive behind
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "icore",
|
|
3
|
-
"version": "
|
|
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
|
|
37
|
+
"lint": "eslint"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@eslint/js": "^9.39.4",
|