@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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +153 -0
  3. package/adapters/bash/cli-style.sh +50 -0
  4. package/adapters/python/cli_style.py +104 -0
  5. package/bin/cli-style.js +82 -0
  6. package/examples/diagnostic.mjs +30 -0
  7. package/examples/diagnostic.py +38 -0
  8. package/examples/diagnostic.sh +29 -0
  9. package/examples/gallery.mjs +18 -0
  10. package/package.json +42 -0
  11. package/src/capability/resolve-profile.js +69 -0
  12. package/src/capability/terminal.js +105 -0
  13. package/src/cli/gallery-command.js +238 -0
  14. package/src/cli/help.js +35 -0
  15. package/src/cli/render-command.js +283 -0
  16. package/src/create-cli-style.js +53 -0
  17. package/src/formatters/ansi.js +154 -0
  18. package/src/gallery/render-gallery.js +758 -0
  19. package/src/index.js +52 -0
  20. package/src/patterns/agent-transcript.js +180 -0
  21. package/src/patterns/audit-finding.js +107 -0
  22. package/src/patterns/command-result.js +91 -0
  23. package/src/patterns/compact-data-table.js +67 -0
  24. package/src/patterns/confirmation-result.js +88 -0
  25. package/src/patterns/diagnostic-report.js +144 -0
  26. package/src/patterns/helpers.js +106 -0
  27. package/src/patterns/next-step-block.js +118 -0
  28. package/src/patterns/task-summary.js +59 -0
  29. package/src/primitives/bar-chart.js +131 -0
  30. package/src/primitives/chip.js +33 -0
  31. package/src/primitives/divider.js +64 -0
  32. package/src/primitives/feedback.js +83 -0
  33. package/src/primitives/panel.js +100 -0
  34. package/src/primitives/progress-bar.js +121 -0
  35. package/src/primitives/row.js +80 -0
  36. package/src/primitives/status.js +39 -0
  37. package/src/primitives/step-progress.js +184 -0
  38. package/src/primitives/table.js +239 -0
  39. package/src/profiles/profiles.js +24 -0
  40. package/src/theme/colours.js +141 -0
  41. 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
+ }
@@ -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 };