@lewishowles/cli-style 0.1.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/LICENSE +21 -0
- package/README.md +153 -0
- package/adapters/bash/cli-style.sh +50 -0
- package/adapters/python/cli_style.py +104 -0
- package/bin/cli-style.js +82 -0
- package/examples/diagnostic.mjs +30 -0
- package/examples/diagnostic.py +38 -0
- package/examples/diagnostic.sh +29 -0
- package/examples/gallery.mjs +18 -0
- package/package.json +42 -0
- package/src/capability/resolve-profile.js +69 -0
- package/src/capability/terminal.js +105 -0
- package/src/cli/gallery-command.js +238 -0
- package/src/cli/help.js +35 -0
- package/src/cli/render-command.js +283 -0
- package/src/create-cli-style.js +53 -0
- package/src/formatters/ansi.js +154 -0
- package/src/gallery/render-gallery.js +758 -0
- package/src/index.js +52 -0
- package/src/patterns/agent-transcript.js +180 -0
- package/src/patterns/audit-finding.js +107 -0
- package/src/patterns/command-result.js +91 -0
- package/src/patterns/compact-data-table.js +67 -0
- package/src/patterns/confirmation-result.js +88 -0
- package/src/patterns/diagnostic-report.js +144 -0
- package/src/patterns/helpers.js +106 -0
- package/src/patterns/next-step-block.js +118 -0
- package/src/patterns/task-summary.js +59 -0
- package/src/primitives/bar-chart.js +131 -0
- package/src/primitives/chip.js +33 -0
- package/src/primitives/divider.js +64 -0
- package/src/primitives/feedback.js +83 -0
- package/src/primitives/panel.js +100 -0
- package/src/primitives/progress-bar.js +121 -0
- package/src/primitives/row.js +80 -0
- package/src/primitives/status.js +39 -0
- package/src/primitives/step-progress.js +184 -0
- package/src/primitives/table.js +239 -0
- package/src/profiles/profiles.js +24 -0
- package/src/theme/colours.js +141 -0
- package/src/theme/results.js +98 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Default terminal width when no stream width is available.
|
|
2
|
+
const defaultWidth = 80;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolve terminal output capabilities from injected args, env, and stream data.
|
|
6
|
+
*
|
|
7
|
+
* @param {object} options
|
|
8
|
+
* Terminal capability inputs.
|
|
9
|
+
* @param {string[]} options.argv
|
|
10
|
+
* CLI arguments to inspect.
|
|
11
|
+
* @param {object} options.env
|
|
12
|
+
* Environment values to inspect.
|
|
13
|
+
* @param {object} options.stdout
|
|
14
|
+
* Stream-like object with `isTTY` and `columns`.
|
|
15
|
+
* @returns {object}
|
|
16
|
+
* Resolved terminal capabilities.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveTerminalCapabilities(options = {}) {
|
|
19
|
+
const argv = options.argv ?? [];
|
|
20
|
+
const env = options.env ?? {};
|
|
21
|
+
const stdout = options.stdout ?? {};
|
|
22
|
+
|
|
23
|
+
const isTty = stdout.isTTY === true;
|
|
24
|
+
const isCi = env.CI === "true" || env.CI === "1";
|
|
25
|
+
const isDumb = env.TERM === "dumb";
|
|
26
|
+
const width = resolveWidth(stdout.columns);
|
|
27
|
+
const usesPlain = argv.includes("--plain");
|
|
28
|
+
|
|
29
|
+
const colour = resolveColour({
|
|
30
|
+
argv,
|
|
31
|
+
env,
|
|
32
|
+
isDumb,
|
|
33
|
+
isTty,
|
|
34
|
+
usesPlain,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const unicode = resolveUnicode({
|
|
38
|
+
argv,
|
|
39
|
+
isDumb,
|
|
40
|
+
usesPlain,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
colour,
|
|
45
|
+
isCi,
|
|
46
|
+
isDumb,
|
|
47
|
+
isTty,
|
|
48
|
+
unicode,
|
|
49
|
+
width,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolve colour support from explicit flags and terminal state.
|
|
55
|
+
*
|
|
56
|
+
* @param {object} options
|
|
57
|
+
* Colour resolution inputs.
|
|
58
|
+
* @returns {boolean}
|
|
59
|
+
* Whether coloured output should be used.
|
|
60
|
+
*/
|
|
61
|
+
function resolveColour(options) {
|
|
62
|
+
if (
|
|
63
|
+
options.usesPlain ||
|
|
64
|
+
options.argv.includes("--no-colour") ||
|
|
65
|
+
options.argv.includes("--no-color") ||
|
|
66
|
+
options.env.NO_COLOR !== undefined ||
|
|
67
|
+
options.isDumb
|
|
68
|
+
) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return options.isTty;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Resolve Unicode support from explicit flags and terminal state.
|
|
77
|
+
*
|
|
78
|
+
* @param {object} options
|
|
79
|
+
* Unicode resolution inputs.
|
|
80
|
+
* @returns {boolean}
|
|
81
|
+
* Whether Unicode symbols should be used.
|
|
82
|
+
*/
|
|
83
|
+
function resolveUnicode(options) {
|
|
84
|
+
if (options.usesPlain || options.argv.includes("--no-unicode") || options.isDumb) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Resolve terminal width with a stable fallback.
|
|
93
|
+
*
|
|
94
|
+
* @param {number} width
|
|
95
|
+
* Stream-reported column count.
|
|
96
|
+
* @returns {number}
|
|
97
|
+
* Usable terminal width.
|
|
98
|
+
*/
|
|
99
|
+
function resolveWidth(width) {
|
|
100
|
+
if (Number.isInteger(width) && width > 0) {
|
|
101
|
+
return width;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return defaultWidth;
|
|
105
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
galleryFixtures,
|
|
5
|
+
gallerySections,
|
|
6
|
+
galleryVariants,
|
|
7
|
+
} from "../gallery/render-gallery.js";
|
|
8
|
+
import { isProfile, profiles } from "../profiles/profiles.js";
|
|
9
|
+
|
|
10
|
+
// Global rendering flags accepted after `gallery` for focused review commands.
|
|
11
|
+
const globalRenderingFlags = new Set([
|
|
12
|
+
"--no-color",
|
|
13
|
+
"--no-colour",
|
|
14
|
+
"--no-unicode",
|
|
15
|
+
"--plain",
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Parse gallery command arguments.
|
|
20
|
+
*
|
|
21
|
+
* @param {string[]} args
|
|
22
|
+
* Arguments after the gallery command.
|
|
23
|
+
* @returns {object}
|
|
24
|
+
* Gallery rendering request.
|
|
25
|
+
*/
|
|
26
|
+
export function parseGalleryRequest(args = []) {
|
|
27
|
+
let hasVariant = false;
|
|
28
|
+
const request = {
|
|
29
|
+
fixture: undefined,
|
|
30
|
+
interactive: false,
|
|
31
|
+
section: undefined,
|
|
32
|
+
variant: "current",
|
|
33
|
+
variants: false,
|
|
34
|
+
width: undefined,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
38
|
+
const argument = args[index];
|
|
39
|
+
|
|
40
|
+
if (argument === "--variants" || argument === "--matrix") {
|
|
41
|
+
request.variants = true;
|
|
42
|
+
} else if (argument === "--interactive") {
|
|
43
|
+
request.interactive = true;
|
|
44
|
+
} else if (argument === "--section") {
|
|
45
|
+
request.section = readOptionValue(args, index, argument);
|
|
46
|
+
index += 1;
|
|
47
|
+
} else if (argument === "--fixture") {
|
|
48
|
+
request.fixture = readOptionValue(args, index, argument);
|
|
49
|
+
index += 1;
|
|
50
|
+
} else if (argument === "--profile") {
|
|
51
|
+
validateGalleryProfile(readOptionValue(args, index, argument));
|
|
52
|
+
index += 1;
|
|
53
|
+
} else if (argument.startsWith("--profile=")) {
|
|
54
|
+
validateGalleryProfile(readInlineOptionValue(argument, "--profile"));
|
|
55
|
+
} else if (argument === "--width") {
|
|
56
|
+
request.width = readWidthValue(readOptionValue(args, index, argument));
|
|
57
|
+
index += 1;
|
|
58
|
+
} else if (argument.startsWith("--width=")) {
|
|
59
|
+
request.width = readWidthValue(readInlineOptionValue(argument, "--width"));
|
|
60
|
+
} else if (argument === "--json") {
|
|
61
|
+
throw new Error("Gallery does not support json output");
|
|
62
|
+
} else if (globalRenderingFlags.has(argument)) {
|
|
63
|
+
// Accepted here because createCliStyle() has already resolved these flags.
|
|
64
|
+
} else if (argument.startsWith("-")) {
|
|
65
|
+
throw new Error(`Unknown gallery option: ${argument}`);
|
|
66
|
+
} else if (hasVariant) {
|
|
67
|
+
throw new Error(`Unexpected gallery argument: ${argument}`);
|
|
68
|
+
} else {
|
|
69
|
+
request.variant = argument;
|
|
70
|
+
hasVariant = true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
validateRequest(request);
|
|
75
|
+
|
|
76
|
+
return request;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Select a gallery section or fixture through fzf when available.
|
|
81
|
+
*
|
|
82
|
+
* @param {object} request
|
|
83
|
+
* Parsed gallery request.
|
|
84
|
+
* @returns {object}
|
|
85
|
+
* Selected or unchanged request.
|
|
86
|
+
*/
|
|
87
|
+
export function selectInteractiveGalleryRequest(request) {
|
|
88
|
+
if (request.interactive !== true || !hasFzf()) {
|
|
89
|
+
return {
|
|
90
|
+
...request,
|
|
91
|
+
interactive: false,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const choices = [
|
|
96
|
+
...gallerySections.map((section) => `section:${section}`),
|
|
97
|
+
...galleryFixtures.map((fixture) => `fixture:${fixture}`),
|
|
98
|
+
];
|
|
99
|
+
const selection = spawnSync("fzf", [
|
|
100
|
+
"--height=60%",
|
|
101
|
+
"--layout=reverse",
|
|
102
|
+
"--prompt=Gallery > ",
|
|
103
|
+
], {
|
|
104
|
+
encoding: "utf8",
|
|
105
|
+
input: choices.join("\n"),
|
|
106
|
+
stdio: ["pipe", "pipe", "inherit"],
|
|
107
|
+
});
|
|
108
|
+
const value = selection.status === 0 ? selection.stdout.trim() : "";
|
|
109
|
+
|
|
110
|
+
if (value === "") {
|
|
111
|
+
return {
|
|
112
|
+
...request,
|
|
113
|
+
interactive: false,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const [type, name] = value.split(":");
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
...request,
|
|
121
|
+
fixture: type === "fixture" ? name : undefined,
|
|
122
|
+
interactive: false,
|
|
123
|
+
section: type === "section" ? name : undefined,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Read a required inline option value.
|
|
129
|
+
*
|
|
130
|
+
* @param {string} argument
|
|
131
|
+
* Full `--option=value` argument.
|
|
132
|
+
* @param {string} option
|
|
133
|
+
* Option name.
|
|
134
|
+
* @returns {string}
|
|
135
|
+
* Option value.
|
|
136
|
+
*/
|
|
137
|
+
function readInlineOptionValue(argument, option) {
|
|
138
|
+
const value = argument.slice(`${option}=`.length);
|
|
139
|
+
|
|
140
|
+
if (value === "") {
|
|
141
|
+
throw new Error(`Missing value for ${option}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Read a required option value.
|
|
149
|
+
*
|
|
150
|
+
* @param {string[]} args
|
|
151
|
+
* Command arguments.
|
|
152
|
+
* @param {number} index
|
|
153
|
+
* Option index.
|
|
154
|
+
* @param {string} option
|
|
155
|
+
* Option name.
|
|
156
|
+
* @returns {string}
|
|
157
|
+
* Option value.
|
|
158
|
+
*/
|
|
159
|
+
function readOptionValue(args, index, option) {
|
|
160
|
+
const value = args[index + 1];
|
|
161
|
+
|
|
162
|
+
if (value === undefined || value.startsWith("-")) {
|
|
163
|
+
throw new Error(`Missing value for ${option}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return value;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Validate a gallery text profile.
|
|
171
|
+
*
|
|
172
|
+
* @param {string} profile
|
|
173
|
+
* Requested output profile.
|
|
174
|
+
* @returns {void}
|
|
175
|
+
*/
|
|
176
|
+
function validateGalleryProfile(profile) {
|
|
177
|
+
if (!isProfile(profile)) {
|
|
178
|
+
throw new Error(`Unknown profile: ${profile}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (profile === profiles.JSON) {
|
|
182
|
+
throw new Error("Gallery does not support json output");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Parse a gallery width option.
|
|
188
|
+
*
|
|
189
|
+
* @param {string} value
|
|
190
|
+
* Width argument value.
|
|
191
|
+
* @returns {number}
|
|
192
|
+
* Positive integer width.
|
|
193
|
+
*/
|
|
194
|
+
function readWidthValue(value) {
|
|
195
|
+
const width = Number(value);
|
|
196
|
+
|
|
197
|
+
if (!Number.isInteger(width) || width <= 0) {
|
|
198
|
+
throw new Error("Gallery width must be a positive integer");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return width;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Validate gallery request values.
|
|
206
|
+
*
|
|
207
|
+
* @param {object} request
|
|
208
|
+
* Parsed gallery request.
|
|
209
|
+
* @returns {void}
|
|
210
|
+
*/
|
|
211
|
+
function validateRequest(request) {
|
|
212
|
+
if (!galleryVariants.includes(request.variant)) {
|
|
213
|
+
throw new Error(`Unknown gallery variant: ${request.variant}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (request.section !== undefined && !gallerySections.includes(request.section)) {
|
|
217
|
+
throw new Error(`Unknown gallery section: ${request.section}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (request.fixture !== undefined && !galleryFixtures.includes(request.fixture)) {
|
|
221
|
+
throw new Error(`Unknown gallery fixture: ${request.fixture}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Check whether the optional fzf binary is available.
|
|
227
|
+
*
|
|
228
|
+
* @returns {boolean}
|
|
229
|
+
* Whether fzf can be invoked.
|
|
230
|
+
*/
|
|
231
|
+
function hasFzf() {
|
|
232
|
+
const result = spawnSync("fzf", ["--version"], {
|
|
233
|
+
encoding: "utf8",
|
|
234
|
+
stdio: "ignore",
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
return result.status === 0;
|
|
238
|
+
}
|
package/src/cli/help.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Help text for the package binary.
|
|
2
|
+
const helpLines = [
|
|
3
|
+
"cli-style",
|
|
4
|
+
"",
|
|
5
|
+
"Usage:",
|
|
6
|
+
" cli-style --help",
|
|
7
|
+
" cli-style gallery [variant] [options]",
|
|
8
|
+
" cli-style render <renderer> [options] < input.json",
|
|
9
|
+
"",
|
|
10
|
+
"Commands:",
|
|
11
|
+
" gallery Print the read-only style gallery.",
|
|
12
|
+
" render Render caller-provided JSON from stdin.",
|
|
13
|
+
"",
|
|
14
|
+
"Gallery variants:",
|
|
15
|
+
" current, no-colour, no-unicode, plain",
|
|
16
|
+
"",
|
|
17
|
+
"Options:",
|
|
18
|
+
" --section <name> Show primitives or patterns.",
|
|
19
|
+
" --fixture <name> Show one named pattern fixture.",
|
|
20
|
+
" --profile <name> Render with a text profile.",
|
|
21
|
+
" --width <columns> Render with a fixed terminal width.",
|
|
22
|
+
" --interactive Select a section or fixture with fzf when available.",
|
|
23
|
+
" --variants Show every capability variant.",
|
|
24
|
+
" -h, --help Show this help.",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Render command help text.
|
|
29
|
+
*
|
|
30
|
+
* @returns {string}
|
|
31
|
+
* CLI help output.
|
|
32
|
+
*/
|
|
33
|
+
export function renderHelp() {
|
|
34
|
+
return helpLines.join("\n");
|
|
35
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { agentTranscript } from "../patterns/agent-transcript.js";
|
|
2
|
+
import { auditFinding } from "../patterns/audit-finding.js";
|
|
3
|
+
import { commandResult } from "../patterns/command-result.js";
|
|
4
|
+
import { compactDataTable } from "../patterns/compact-data-table.js";
|
|
5
|
+
import { confirmationResult } from "../patterns/confirmation-result.js";
|
|
6
|
+
import { diagnosticReport } from "../patterns/diagnostic-report.js";
|
|
7
|
+
import { nextStepBlock } from "../patterns/next-step-block.js";
|
|
8
|
+
import { taskSummary } from "../patterns/task-summary.js";
|
|
9
|
+
import { barChart } from "../primitives/bar-chart.js";
|
|
10
|
+
import { chip } from "../primitives/chip.js";
|
|
11
|
+
import { divider } from "../primitives/divider.js";
|
|
12
|
+
import { emptyState, errorBlock, hint } from "../primitives/feedback.js";
|
|
13
|
+
import { panel } from "../primitives/panel.js";
|
|
14
|
+
import { progressBar } from "../primitives/progress-bar.js";
|
|
15
|
+
import { row } from "../primitives/row.js";
|
|
16
|
+
import { status } from "../primitives/status.js";
|
|
17
|
+
import { step, stepProgress } from "../primitives/step-progress.js";
|
|
18
|
+
import { table } from "../primitives/table.js";
|
|
19
|
+
import { isProfile, profiles } from "../profiles/profiles.js";
|
|
20
|
+
|
|
21
|
+
// Renderer names accepted by the custom-input render command.
|
|
22
|
+
const renderers = {
|
|
23
|
+
"agent-transcript": (input, options) => agentTranscript(input, options),
|
|
24
|
+
"audit-finding": (input, options) => auditFinding(input, options),
|
|
25
|
+
"bar-chart": (input, options) => barChart({
|
|
26
|
+
...input,
|
|
27
|
+
...options,
|
|
28
|
+
}),
|
|
29
|
+
chip: (input, options) => chip(input.label, input.tone, {
|
|
30
|
+
...input,
|
|
31
|
+
...options,
|
|
32
|
+
}),
|
|
33
|
+
"command-result": (input, options) => commandResult(input, options),
|
|
34
|
+
"compact-data-table": (input, options) => compactDataTable(input, options),
|
|
35
|
+
"confirmation-result": (input, options) => confirmationResult(input, options),
|
|
36
|
+
"diagnostic-report": (input, options) => diagnosticReport(input, options),
|
|
37
|
+
divider: (input, options) => divider({
|
|
38
|
+
...input,
|
|
39
|
+
...options,
|
|
40
|
+
}),
|
|
41
|
+
"empty-state": (input, options) => emptyState(input.title, input.detail, {
|
|
42
|
+
...input,
|
|
43
|
+
...options,
|
|
44
|
+
}),
|
|
45
|
+
"error-block": (input, options) => errorBlock(input.title, input.lines, {
|
|
46
|
+
...input,
|
|
47
|
+
...options,
|
|
48
|
+
}),
|
|
49
|
+
hint: (input, options) => hint(input.message, {
|
|
50
|
+
...input,
|
|
51
|
+
...options,
|
|
52
|
+
}),
|
|
53
|
+
"next-step-block": (input, options) => nextStepBlock(input, options),
|
|
54
|
+
panel: (input, options) => panel({
|
|
55
|
+
...input,
|
|
56
|
+
...options,
|
|
57
|
+
}),
|
|
58
|
+
"progress-bar": (input, options) => progressBar({
|
|
59
|
+
...input,
|
|
60
|
+
...options,
|
|
61
|
+
}),
|
|
62
|
+
row: (input, options) => row(input.label, input.value, {
|
|
63
|
+
...input,
|
|
64
|
+
...options,
|
|
65
|
+
}),
|
|
66
|
+
status: (input, options) => status(input.type, input.detail, {
|
|
67
|
+
...input,
|
|
68
|
+
...options,
|
|
69
|
+
}),
|
|
70
|
+
step: (input, options) => step(input.label, input.state, {
|
|
71
|
+
...input,
|
|
72
|
+
...options,
|
|
73
|
+
}),
|
|
74
|
+
"step-progress": (input, options) => stepProgress({
|
|
75
|
+
...input,
|
|
76
|
+
...options,
|
|
77
|
+
}),
|
|
78
|
+
table: (input, options) => table({
|
|
79
|
+
...input,
|
|
80
|
+
...options,
|
|
81
|
+
}),
|
|
82
|
+
"task-summary": (input, options) => taskSummary(input, options),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Global rendering flags accepted after `render <renderer>`.
|
|
86
|
+
const globalRenderingFlags = new Set([
|
|
87
|
+
"--no-color",
|
|
88
|
+
"--no-colour",
|
|
89
|
+
"--no-unicode",
|
|
90
|
+
"--plain",
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
// Renderer names exposed for help text and tests.
|
|
94
|
+
export const rendererNames = Object.keys(renderers);
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Parse render command arguments.
|
|
98
|
+
*
|
|
99
|
+
* @param {string[]} args
|
|
100
|
+
* Arguments after the render command.
|
|
101
|
+
* @returns {object}
|
|
102
|
+
* Render request.
|
|
103
|
+
*/
|
|
104
|
+
export function parseRenderRequest(args = []) {
|
|
105
|
+
const request = {
|
|
106
|
+
renderer: undefined,
|
|
107
|
+
width: undefined,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
111
|
+
const argument = args[index];
|
|
112
|
+
|
|
113
|
+
if (argument === "--profile") {
|
|
114
|
+
validateRenderProfile(readOptionValue(args, index, argument));
|
|
115
|
+
index += 1;
|
|
116
|
+
} else if (argument.startsWith("--profile=")) {
|
|
117
|
+
validateRenderProfile(readInlineOptionValue(argument, "--profile"));
|
|
118
|
+
} else if (argument === "--width") {
|
|
119
|
+
request.width = readWidthValue(readOptionValue(args, index, argument));
|
|
120
|
+
index += 1;
|
|
121
|
+
} else if (argument.startsWith("--width=")) {
|
|
122
|
+
request.width = readWidthValue(readInlineOptionValue(argument, "--width"));
|
|
123
|
+
} else if (argument === "--json") {
|
|
124
|
+
throw new Error("Render does not support json output");
|
|
125
|
+
} else if (globalRenderingFlags.has(argument)) {
|
|
126
|
+
// Accepted here because createCliStyle() has already resolved these flags.
|
|
127
|
+
} else if (argument.startsWith("-")) {
|
|
128
|
+
throw new Error(`Unknown render option: ${argument}`);
|
|
129
|
+
} else if (request.renderer !== undefined) {
|
|
130
|
+
throw new Error(`Unexpected render argument: ${argument}`);
|
|
131
|
+
} else {
|
|
132
|
+
request.renderer = argument;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
validateRequest(request);
|
|
137
|
+
|
|
138
|
+
return request;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Render caller-provided JSON through a named renderer.
|
|
143
|
+
*
|
|
144
|
+
* @param {string} json
|
|
145
|
+
* JSON input from stdin.
|
|
146
|
+
* @param {object} request
|
|
147
|
+
* Parsed render request.
|
|
148
|
+
* @param {object} options
|
|
149
|
+
* Resolved render options.
|
|
150
|
+
* @returns {string}
|
|
151
|
+
* Rendered text.
|
|
152
|
+
*/
|
|
153
|
+
export function renderJsonInput(json, request, options = {}) {
|
|
154
|
+
if (options.profile === profiles.JSON) {
|
|
155
|
+
throw new Error("Render does not support json output");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const input = parseJsonObject(json);
|
|
159
|
+
const renderer = renderers[request.renderer];
|
|
160
|
+
|
|
161
|
+
return renderer(input, {
|
|
162
|
+
...options,
|
|
163
|
+
width: request.width ?? options.width,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Read a required inline option value.
|
|
169
|
+
*
|
|
170
|
+
* @param {string} argument
|
|
171
|
+
* Full `--option=value` argument.
|
|
172
|
+
* @param {string} option
|
|
173
|
+
* Option name.
|
|
174
|
+
* @returns {string}
|
|
175
|
+
* Option value.
|
|
176
|
+
*/
|
|
177
|
+
function readInlineOptionValue(argument, option) {
|
|
178
|
+
const value = argument.slice(`${option}=`.length);
|
|
179
|
+
|
|
180
|
+
if (value === "") {
|
|
181
|
+
throw new Error(`Missing value for ${option}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return value;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Read a required option value.
|
|
189
|
+
*
|
|
190
|
+
* @param {string[]} args
|
|
191
|
+
* Command arguments.
|
|
192
|
+
* @param {number} index
|
|
193
|
+
* Option index.
|
|
194
|
+
* @param {string} option
|
|
195
|
+
* Option name.
|
|
196
|
+
* @returns {string}
|
|
197
|
+
* Option value.
|
|
198
|
+
*/
|
|
199
|
+
function readOptionValue(args, index, option) {
|
|
200
|
+
const value = args[index + 1];
|
|
201
|
+
|
|
202
|
+
if (value === undefined || value.startsWith("-")) {
|
|
203
|
+
throw new Error(`Missing value for ${option}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return value;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Parse a render width option.
|
|
211
|
+
*
|
|
212
|
+
* @param {string} value
|
|
213
|
+
* Width argument value.
|
|
214
|
+
* @returns {number}
|
|
215
|
+
* Positive integer width.
|
|
216
|
+
*/
|
|
217
|
+
function readWidthValue(value) {
|
|
218
|
+
const width = Number(value);
|
|
219
|
+
|
|
220
|
+
if (!Number.isInteger(width) || width <= 0) {
|
|
221
|
+
throw new Error("Render width must be a positive integer");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return width;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Validate a render text profile.
|
|
229
|
+
*
|
|
230
|
+
* @param {string} profile
|
|
231
|
+
* Requested output profile.
|
|
232
|
+
* @returns {void}
|
|
233
|
+
*/
|
|
234
|
+
function validateRenderProfile(profile) {
|
|
235
|
+
if (!isProfile(profile)) {
|
|
236
|
+
throw new Error(`Unknown profile: ${profile}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (profile === profiles.JSON) {
|
|
240
|
+
throw new Error("Render does not support json output");
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Validate render request values.
|
|
246
|
+
*
|
|
247
|
+
* @param {object} request
|
|
248
|
+
* Parsed render request.
|
|
249
|
+
* @returns {void}
|
|
250
|
+
*/
|
|
251
|
+
function validateRequest(request) {
|
|
252
|
+
if (request.renderer === undefined) {
|
|
253
|
+
throw new Error("Missing renderer");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (!Object.hasOwn(renderers, request.renderer)) {
|
|
257
|
+
throw new Error(`Unknown renderer: ${request.renderer}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Parse JSON stdin into an object accepted by renderers.
|
|
263
|
+
*
|
|
264
|
+
* @param {string} json
|
|
265
|
+
* JSON input.
|
|
266
|
+
* @returns {object}
|
|
267
|
+
* Parsed input object.
|
|
268
|
+
*/
|
|
269
|
+
function parseJsonObject(json) {
|
|
270
|
+
let input;
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
input = JSON.parse(json);
|
|
274
|
+
} catch {
|
|
275
|
+
throw new Error("Render input must be valid JSON");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (input === null || Array.isArray(input) || typeof input !== "object") {
|
|
279
|
+
throw new Error("Render input must be a JSON object");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return input;
|
|
283
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { resolveProfile } from "./capability/resolve-profile.js";
|
|
2
|
+
import { resolveTerminalCapabilities } from "./capability/terminal.js";
|
|
3
|
+
import { profiles } from "./profiles/profiles.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create a CLI style renderer with stable, testable defaults.
|
|
7
|
+
*
|
|
8
|
+
* @param {object} options
|
|
9
|
+
* Output profile, terminal capability, stream, environment, and width options.
|
|
10
|
+
* @returns {object}
|
|
11
|
+
* Renderer helpers and resolved options.
|
|
12
|
+
*/
|
|
13
|
+
export function createCliStyle(options = {}) {
|
|
14
|
+
const capabilities = resolveTerminalCapabilities(options);
|
|
15
|
+
|
|
16
|
+
const resolvedOptions = {
|
|
17
|
+
...capabilities,
|
|
18
|
+
profile: resolveProfile(options),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
applyExplicitOption(resolvedOptions, options, "colour");
|
|
22
|
+
applyExplicitOption(resolvedOptions, options, "unicode");
|
|
23
|
+
applyExplicitOption(resolvedOptions, options, "width");
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
options: resolvedOptions,
|
|
27
|
+
print: (value) => {
|
|
28
|
+
console.log(value);
|
|
29
|
+
},
|
|
30
|
+
write: (value) => {
|
|
31
|
+
process.stdout.write(value);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Apply an explicit option override when provided.
|
|
38
|
+
*
|
|
39
|
+
* @param {object} target
|
|
40
|
+
* Resolved options to update.
|
|
41
|
+
* @param {object} source
|
|
42
|
+
* Caller-provided options.
|
|
43
|
+
* @param {string} key
|
|
44
|
+
* Option key to copy.
|
|
45
|
+
* @returns {void}
|
|
46
|
+
*/
|
|
47
|
+
function applyExplicitOption(target, source, key) {
|
|
48
|
+
if (Object.hasOwn(source, key)) {
|
|
49
|
+
target[key] = source[key];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { profiles };
|