@lovrabet/cli-framework 0.1.1-beta.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/README.md +246 -0
- package/lib/errors.d.ts +121 -0
- package/lib/errors.js +73 -0
- package/lib/framework/build-all-flags.d.ts +39 -0
- package/lib/framework/build-all-flags.js +54 -0
- package/lib/framework/flags.d.ts +37 -0
- package/lib/framework/flags.js +152 -0
- package/lib/framework/help.d.ts +136 -0
- package/lib/framework/help.js +244 -0
- package/lib/framework/output.d.ts +45 -0
- package/lib/framework/output.js +354 -0
- package/lib/framework/response.d.ts +60 -0
- package/lib/framework/response.js +47 -0
- package/lib/framework/runner-shared.d.ts +244 -0
- package/lib/framework/runner-shared.js +240 -0
- package/lib/framework/runner.d.ts +246 -0
- package/lib/framework/runner.js +184 -0
- package/lib/framework/schema-export.d.ts +176 -0
- package/lib/framework/schema-export.js +148 -0
- package/lib/framework/types.d.ts +338 -0
- package/lib/framework/types.js +53 -0
- package/lib/index.d.ts +209 -0
- package/lib/index.js +52 -0
- package/lib/utils/apply-jq-filter.d.ts +33 -0
- package/lib/utils/apply-jq-filter.js +99 -0
- package/package.json +35 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Output formatting pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Renders a {@link CommandResult} to stdout in one of three modes:
|
|
5
|
+
* - `json` — Full indented JSON envelope.
|
|
6
|
+
* - `compress` — Single-line JSON envelope (default).
|
|
7
|
+
* - `pretty` — Human-readable plain-text layout.
|
|
8
|
+
*
|
|
9
|
+
* Supports `--jq` post-filtering in `json` / `compress` modes via the
|
|
10
|
+
* bundled `node-jq` executable (or a `jq` binary on PATH).
|
|
11
|
+
*/
|
|
12
|
+
import chalk from "chalk";
|
|
13
|
+
import { createJqFilter } from "../utils/apply-jq-filter.js";
|
|
14
|
+
import { createCliErrors } from "../errors.js";
|
|
15
|
+
const applyJqFilter = createJqFilter(createCliErrors({
|
|
16
|
+
cliBinName: "cli",
|
|
17
|
+
authRequiredHint: "",
|
|
18
|
+
configMissingHint: "",
|
|
19
|
+
notInProjectHint: "",
|
|
20
|
+
}));
|
|
21
|
+
/**
|
|
22
|
+
* Central formatting entry point used by the runner adapter.
|
|
23
|
+
*
|
|
24
|
+
* Dispatches to the appropriate internal printer based on `options.format`:
|
|
25
|
+
* - `"json"` → {@link printJson}
|
|
26
|
+
* - `"compress"` → {@link printCompress}
|
|
27
|
+
* - `"pretty"` → {@link printPretty}
|
|
28
|
+
*
|
|
29
|
+
* All output is written to stdout. Errors are written to stderr.
|
|
30
|
+
*
|
|
31
|
+
* @param result - The command result to render.
|
|
32
|
+
* @param options - Formatting options including format, command label, risk, and jq filter.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* formatOutput({ ok: true, data: { id: 1 } }, { command: "app list", risk: "read", format: "compress" });
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function formatOutput(result, options) {
|
|
40
|
+
switch (options.format) {
|
|
41
|
+
case "json":
|
|
42
|
+
printJson(result, options);
|
|
43
|
+
break;
|
|
44
|
+
case "compress":
|
|
45
|
+
printCompress(result, options);
|
|
46
|
+
break;
|
|
47
|
+
case "pretty":
|
|
48
|
+
default:
|
|
49
|
+
printPretty(result, options);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Wraps the command result in an {@link OutputEnvelope} by merging the
|
|
55
|
+
* command metadata from `options` and the result data / error.
|
|
56
|
+
*
|
|
57
|
+
* @template T - Type of the result data payload.
|
|
58
|
+
* @param result - The command result to wrap.
|
|
59
|
+
* @param options - Output options providing command, risk, and dry-run flag.
|
|
60
|
+
* @returns A fully populated output envelope ready for JSON serialization.
|
|
61
|
+
*/
|
|
62
|
+
function buildEnvelope(result, options) {
|
|
63
|
+
const envelope = {
|
|
64
|
+
ok: result.ok,
|
|
65
|
+
command: options.command,
|
|
66
|
+
risk: options.risk,
|
|
67
|
+
};
|
|
68
|
+
if (options.dryRun)
|
|
69
|
+
envelope.dryRun = true;
|
|
70
|
+
if (result.data !== undefined)
|
|
71
|
+
envelope.data = result.data;
|
|
72
|
+
if (!result.ok && result.message) {
|
|
73
|
+
envelope.error = { code: "command_error", message: result.message };
|
|
74
|
+
}
|
|
75
|
+
return envelope;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Formats and prints the result as pretty-printed (2-space indent) JSON.
|
|
79
|
+
* Applies the `--jq` filter if `options.jqFilter` is set.
|
|
80
|
+
*
|
|
81
|
+
* @param result - The command result to serialize.
|
|
82
|
+
* @param options - Output options including jq filter.
|
|
83
|
+
*/
|
|
84
|
+
function printJson(result, options) {
|
|
85
|
+
writeJsonWithOptionalJq(`${JSON.stringify(buildEnvelope(result, options), null, 2)}\n`, options);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Formats and prints the result as single-line (compact) JSON.
|
|
89
|
+
* Applies the `--jq` filter if `options.jqFilter` is set.
|
|
90
|
+
*
|
|
91
|
+
* @param result - The command result to serialize.
|
|
92
|
+
* @param options - Output options including jq filter.
|
|
93
|
+
*/
|
|
94
|
+
function printCompress(result, options) {
|
|
95
|
+
writeJsonWithOptionalJq(`${JSON.stringify(buildEnvelope(result, options))}\n`, options);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Writes a JSON string to stdout, optionally piping it through `jq`.
|
|
99
|
+
*
|
|
100
|
+
* @param jsonLine - Complete JSON line (must end with `\n`).
|
|
101
|
+
* @param options - Output options carrying the jq expression.
|
|
102
|
+
*/
|
|
103
|
+
function writeJsonWithOptionalJq(jsonLine, options) {
|
|
104
|
+
const expr = options.jqFilter?.trim();
|
|
105
|
+
if (!expr) {
|
|
106
|
+
process.stdout.write(jsonLine);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const out = applyJqFilter(jsonLine, expr);
|
|
110
|
+
process.stdout.write(out.endsWith("\n") ? out : `${out}\n`);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Formats and prints the result as human-readable plain text.
|
|
114
|
+
* Handles special data shapes (`ListWithMetaData`, `ListEnvelope`, arrays,
|
|
115
|
+
* objects, scalars) with appropriate visual treatments.
|
|
116
|
+
*
|
|
117
|
+
* @param result - The command result to render.
|
|
118
|
+
* @param options - Output options.
|
|
119
|
+
*/
|
|
120
|
+
function printPretty(result, options) {
|
|
121
|
+
if (options.dryRun) {
|
|
122
|
+
console.log("[dry-run] Would execute:");
|
|
123
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (!result.ok) {
|
|
127
|
+
console.error(`Error: ${result.message ?? "Unknown error"}`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const data = result.data;
|
|
131
|
+
if (data === undefined || data === null) {
|
|
132
|
+
if (result.message)
|
|
133
|
+
console.log(result.message);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (isListWithMetaData(data)) {
|
|
137
|
+
printAppListMeta(data.meta);
|
|
138
|
+
printArrayPretty(data.items, { appListHighlight: true });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (isListEnvelope(data)) {
|
|
142
|
+
printListEnvelopePretty(data);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (Array.isArray(data)) {
|
|
146
|
+
printArrayPretty(data);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (typeof data === "object") {
|
|
150
|
+
printObjectPretty(data);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
console.log(String(data));
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Type guard for the `{ items: unknown[]; meta: Record<string, unknown> }` shape
|
|
157
|
+
* used by app-list commands.
|
|
158
|
+
*
|
|
159
|
+
* @param data - Value to test.
|
|
160
|
+
*/
|
|
161
|
+
function isListWithMetaData(data) {
|
|
162
|
+
return (typeof data === "object" &&
|
|
163
|
+
data !== null &&
|
|
164
|
+
"items" in data &&
|
|
165
|
+
Array.isArray(data.items) &&
|
|
166
|
+
"meta" in data &&
|
|
167
|
+
typeof data.meta === "object" &&
|
|
168
|
+
data.meta !== null);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Type guard for the `{ items: unknown[]; [key: string]: unknown }` shape
|
|
172
|
+
* used by list commands that include extra summary fields.
|
|
173
|
+
*
|
|
174
|
+
* @param data - Value to test.
|
|
175
|
+
*/
|
|
176
|
+
function isListEnvelope(data) {
|
|
177
|
+
return (typeof data === "object" &&
|
|
178
|
+
data !== null &&
|
|
179
|
+
"items" in data &&
|
|
180
|
+
Array.isArray(data.items));
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Prints the metadata block for app-list output in pretty mode.
|
|
184
|
+
* Switches between "scope" mode (single config path) and "dual" mode
|
|
185
|
+
* (separate global/project paths with default-app annotation).
|
|
186
|
+
*
|
|
187
|
+
* @param meta - Metadata object with config paths and default-app info.
|
|
188
|
+
*/
|
|
189
|
+
function printAppListMeta(meta) {
|
|
190
|
+
if (meta.scope) {
|
|
191
|
+
const cp = meta.configPath != null ? String(meta.configPath) : "(none)";
|
|
192
|
+
console.log(` Scope: ${String(meta.scope)}`);
|
|
193
|
+
console.log(` Config: ${cp}`);
|
|
194
|
+
console.log();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const gp = meta.globalPath != null ? String(meta.globalPath) : "(none)";
|
|
198
|
+
const pp = meta.projectPath != null ? String(meta.projectPath) : "(none)";
|
|
199
|
+
const def = meta.defaultApp != null ? String(meta.defaultApp) : "(none)";
|
|
200
|
+
const src = meta.defaultAppSource;
|
|
201
|
+
const defSuffix = src === "project"
|
|
202
|
+
? " (default from project file)"
|
|
203
|
+
: src === "global"
|
|
204
|
+
? " (default from global file)"
|
|
205
|
+
: "";
|
|
206
|
+
console.log(` Global config: ${gp}`);
|
|
207
|
+
console.log(` Project config: ${pp}`);
|
|
208
|
+
console.log(` Default app: ${def}${defSuffix}`);
|
|
209
|
+
console.log();
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Prints a list envelope (items + summary fields) in pretty mode.
|
|
213
|
+
* Summary fields are rendered as `key value` rows first, then the item list.
|
|
214
|
+
*
|
|
215
|
+
* @param data - List envelope object.
|
|
216
|
+
*/
|
|
217
|
+
function printListEnvelopePretty(data) {
|
|
218
|
+
const { items, ...rest } = data;
|
|
219
|
+
const summaryEntries = Object.entries(rest).filter(([, val]) => val !== undefined && val !== null && val !== "");
|
|
220
|
+
if (summaryEntries.length > 0) {
|
|
221
|
+
const maxKeyLen = Math.max(...summaryEntries.map(([key]) => key.length), 0);
|
|
222
|
+
for (const [key, val] of summaryEntries) {
|
|
223
|
+
const label = key.padEnd(maxKeyLen);
|
|
224
|
+
console.log(`${label} ${formatPrettyScalarValue(val)}`);
|
|
225
|
+
}
|
|
226
|
+
console.log("");
|
|
227
|
+
}
|
|
228
|
+
printArrayPretty(items);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Prints a plain object as key-value rows with default indentation.
|
|
232
|
+
* Delegates to {@link printObjectPrettyWithIndent}.
|
|
233
|
+
*
|
|
234
|
+
* @param obj - Object to print.
|
|
235
|
+
*/
|
|
236
|
+
function printObjectPretty(obj) {
|
|
237
|
+
printObjectPrettyWithIndent(obj);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Converts any non-object scalar to a display string.
|
|
241
|
+
* Objects and arrays are JSON-stringified; all other values are cast to string.
|
|
242
|
+
*
|
|
243
|
+
* @param raw - Raw value to format.
|
|
244
|
+
* @returns Display string.
|
|
245
|
+
*/
|
|
246
|
+
function formatPrettyScalarValue(raw) {
|
|
247
|
+
return typeof raw === "object" && raw !== null ? JSON.stringify(raw) : String(raw);
|
|
248
|
+
}
|
|
249
|
+
/** Keys excluded from app-list item display in pretty mode. */
|
|
250
|
+
const APP_LIST_PRETTY_HIDE_KEYS = new Set(["isDefault", "isCurrent"]);
|
|
251
|
+
/**
|
|
252
|
+
* Formats a single cell value for an app-list item in pretty mode.
|
|
253
|
+
* Applies special annotations for `isDefault` (gray "(default)") and
|
|
254
|
+
* `isCurrent` (green "← current").
|
|
255
|
+
*
|
|
256
|
+
* @param key - Field name.
|
|
257
|
+
* @param raw - Raw field value.
|
|
258
|
+
* @param item - The full item object (used for cross-field context).
|
|
259
|
+
* @returns Formatted display string for the cell.
|
|
260
|
+
*/
|
|
261
|
+
function formatAppListPrettyCell(key, raw, item) {
|
|
262
|
+
const val = formatPrettyScalarValue(raw);
|
|
263
|
+
const hasName = item.name != null && item.name !== "";
|
|
264
|
+
if (key === "name" && hasName) {
|
|
265
|
+
let out = formatPrettyScalarValue(raw);
|
|
266
|
+
if (item.isDefault === true)
|
|
267
|
+
out = `${out}${chalk.gray(" (default)")}`;
|
|
268
|
+
if (item.isCurrent === true)
|
|
269
|
+
out = `${out}${chalk.green(" ← current")}`;
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
if (key === "appcode" && !hasName) {
|
|
273
|
+
if (item.isDefault !== true && item.isCurrent !== true)
|
|
274
|
+
return val;
|
|
275
|
+
let out = formatPrettyScalarValue(raw);
|
|
276
|
+
if (item.isDefault === true)
|
|
277
|
+
out = `${out}${chalk.gray(" (default)")}`;
|
|
278
|
+
if (item.isCurrent === true)
|
|
279
|
+
out = `${out}${chalk.green(" ← current")}`;
|
|
280
|
+
return out;
|
|
281
|
+
}
|
|
282
|
+
return val;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Prints an array of items in pretty mode.
|
|
286
|
+
* Each item is either a primitive (printed as `- value`) or an object
|
|
287
|
+
* (printed as key-value rows under the item). Nested structures are handled
|
|
288
|
+
* recursively.
|
|
289
|
+
*
|
|
290
|
+
* @param arr - Array to print.
|
|
291
|
+
* @param opts - Rendering options.
|
|
292
|
+
*/
|
|
293
|
+
function printArrayPretty(arr, opts) {
|
|
294
|
+
const indent = opts?.indent ?? "";
|
|
295
|
+
if (arr.length === 0) {
|
|
296
|
+
console.log(`${indent}(empty)`);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (opts?.showCount !== false) {
|
|
300
|
+
console.log(`${indent}Found ${arr.length} items:\n`);
|
|
301
|
+
}
|
|
302
|
+
const highlight = opts?.appListHighlight === true;
|
|
303
|
+
for (const item of arr) {
|
|
304
|
+
if (typeof item === "object" && item !== null) {
|
|
305
|
+
const entries = Object.entries(item)
|
|
306
|
+
.filter(([, v]) => v != null && v !== "")
|
|
307
|
+
.filter(([k]) => !highlight || !APP_LIST_PRETTY_HIDE_KEYS.has(k));
|
|
308
|
+
for (const [key, val] of entries) {
|
|
309
|
+
if (!highlight && Array.isArray(val)) {
|
|
310
|
+
console.log(`${indent}- ${key}:`);
|
|
311
|
+
printArrayPretty(val, { indent: `${indent} `, showCount: false });
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (!highlight && typeof val === "object" && val !== null) {
|
|
315
|
+
console.log(`${indent}- ${key}:`);
|
|
316
|
+
printObjectPrettyWithIndent(val, `${indent} `);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
const rendered = highlight
|
|
320
|
+
? formatAppListPrettyCell(key, val, item)
|
|
321
|
+
: formatPrettyScalarValue(val);
|
|
322
|
+
console.log(`${indent}- ${key}: ${rendered}`);
|
|
323
|
+
}
|
|
324
|
+
console.log(`${indent}`);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
console.log(`${indent}- ${formatPrettyScalarValue(item)}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Recursively prints key-value rows for a plain object with configurable indent.
|
|
332
|
+
* Arrays are delegated to {@link printArrayPretty}; nested objects recurse.
|
|
333
|
+
*
|
|
334
|
+
* @param obj - Object to print.
|
|
335
|
+
* @param indent - Current indentation prefix.
|
|
336
|
+
*/
|
|
337
|
+
function printObjectPrettyWithIndent(obj, indent = "") {
|
|
338
|
+
const entries = Object.entries(obj).filter(([, val]) => val !== undefined && val !== null);
|
|
339
|
+
const maxKeyLen = Math.max(...entries.map(([key]) => key.length), 0);
|
|
340
|
+
for (const [key, val] of entries) {
|
|
341
|
+
const label = `${indent}${key.padEnd(maxKeyLen)}`;
|
|
342
|
+
if (Array.isArray(val)) {
|
|
343
|
+
console.log(`${label}`);
|
|
344
|
+
printArrayPretty(val, { indent: `${indent} `, showCount: false });
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (typeof val === "object" && val !== null) {
|
|
348
|
+
console.log(`${label}`);
|
|
349
|
+
printObjectPrettyWithIndent(val, `${indent} `);
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
console.log(`${label} ${formatPrettyScalarValue(val)}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Utilities for extracting structured data from API responses.
|
|
3
|
+
*
|
|
4
|
+
* Normalizes responses from heterogeneous API shapes into a consistent
|
|
5
|
+
* list + paging model used by the output formatter and list commands.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Pagination metadata embedded in list responses.
|
|
9
|
+
*/
|
|
10
|
+
export interface Paging {
|
|
11
|
+
/** 1-based current page number. */
|
|
12
|
+
currentPage: number;
|
|
13
|
+
/** Total number of records across all pages. */
|
|
14
|
+
totalCount: number;
|
|
15
|
+
/** Number of records per page. */
|
|
16
|
+
pageSize: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A list response wrapper returned by many API endpoints.
|
|
20
|
+
* `tableData` holds the records; `paging` and `tableColumns` are optional.
|
|
21
|
+
*
|
|
22
|
+
* @template T - Type of each record in `tableData`.
|
|
23
|
+
*/
|
|
24
|
+
export interface ListResponse<T = any> {
|
|
25
|
+
/** Array of record objects. */
|
|
26
|
+
tableData: T[];
|
|
27
|
+
/** Present when the response includes pagination metadata. */
|
|
28
|
+
paging?: Paging;
|
|
29
|
+
/** Optional column schema for tabular rendering. */
|
|
30
|
+
tableColumns?: unknown[];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Extracts the array of records from a response that may be either:
|
|
34
|
+
* - A plain array `T[]`.
|
|
35
|
+
* - An object with a `tableData` property.
|
|
36
|
+
*
|
|
37
|
+
* @template T - Inferred record type.
|
|
38
|
+
* @param data - Raw API response body.
|
|
39
|
+
* @returns The extracted array, or `[]` if the shape is unrecognized.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* const items = extractList<User>(apiResponse);
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export declare function extractList<T = any>(data: unknown): T[];
|
|
47
|
+
/**
|
|
48
|
+
* Extracts the {@link Paging} metadata from a response object.
|
|
49
|
+
* Returns `undefined` for plain arrays or empty responses.
|
|
50
|
+
*
|
|
51
|
+
* @param data - Raw API response body.
|
|
52
|
+
* @returns The paging object if present, otherwise `undefined`.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* const paging = extractPaging(response);
|
|
57
|
+
* if (paging) console.log(`Page ${paging.currentPage} of ${Math.ceil(paging.totalCount / paging.pageSize)}`);
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export declare function extractPaging(data: unknown): Paging | undefined;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Utilities for extracting structured data from API responses.
|
|
3
|
+
*
|
|
4
|
+
* Normalizes responses from heterogeneous API shapes into a consistent
|
|
5
|
+
* list + paging model used by the output formatter and list commands.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Extracts the array of records from a response that may be either:
|
|
9
|
+
* - A plain array `T[]`.
|
|
10
|
+
* - An object with a `tableData` property.
|
|
11
|
+
*
|
|
12
|
+
* @template T - Inferred record type.
|
|
13
|
+
* @param data - Raw API response body.
|
|
14
|
+
* @returns The extracted array, or `[]` if the shape is unrecognized.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* const items = extractList<User>(apiResponse);
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function extractList(data) {
|
|
22
|
+
if (Array.isArray(data))
|
|
23
|
+
return data;
|
|
24
|
+
if (data && typeof data === "object") {
|
|
25
|
+
return data.tableData ?? [];
|
|
26
|
+
}
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Extracts the {@link Paging} metadata from a response object.
|
|
31
|
+
* Returns `undefined` for plain arrays or empty responses.
|
|
32
|
+
*
|
|
33
|
+
* @param data - Raw API response body.
|
|
34
|
+
* @returns The paging object if present, otherwise `undefined`.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* const paging = extractPaging(response);
|
|
39
|
+
* if (paging) console.log(`Page ${paging.currentPage} of ${Math.ceil(paging.totalCount / paging.pageSize)}`);
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export function extractPaging(data) {
|
|
43
|
+
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
44
|
+
return data.paging;
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Shared utilities used by the framework runner.
|
|
3
|
+
*
|
|
4
|
+
* Provides pure functions for:
|
|
5
|
+
* - Resolving the effective output format and jq filter from flags/config.
|
|
6
|
+
* - Building the runtime context object passed to command implementations.
|
|
7
|
+
* - Enforcing risk-level policy.
|
|
8
|
+
* - Running dry-run previews and confirmation prompts.
|
|
9
|
+
*
|
|
10
|
+
* These functions are called from both the runner adapter and help/schema
|
|
11
|
+
* generators, so they carry no side-effects beyond argument validation.
|
|
12
|
+
*/
|
|
13
|
+
import type { CommandDefinition, CommandResult, OutputFormat, Risk, RuntimeContext } from "./types.js";
|
|
14
|
+
import type { ParsedFlags } from "./flags.js";
|
|
15
|
+
/** Options for {@link resolveOutputConfig}. */
|
|
16
|
+
export interface ResolveOutputOptions {
|
|
17
|
+
/** Parsed flags from the CLI. */
|
|
18
|
+
flags: ParsedFlags;
|
|
19
|
+
/** Command definition supplying format defaults. */
|
|
20
|
+
def: CommandDefinition;
|
|
21
|
+
/**
|
|
22
|
+
* Global config default format. Falls through to:
|
|
23
|
+
* `flags.format` → `configDefault` → `def.defaultOutputFormat` → `"compress"`.
|
|
24
|
+
*/
|
|
25
|
+
configDefault?: OutputFormat;
|
|
26
|
+
}
|
|
27
|
+
/** Output configuration resolved by {@link resolveOutputConfig}. */
|
|
28
|
+
export interface ResolvedOutputConfig {
|
|
29
|
+
/** Effective output format. */
|
|
30
|
+
format: OutputFormat;
|
|
31
|
+
/**
|
|
32
|
+
* jq expression from `--jq`, or `undefined` if not provided.
|
|
33
|
+
*/
|
|
34
|
+
jqFilter?: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Extracts the `--jq` flag value from parsed flags.
|
|
38
|
+
*
|
|
39
|
+
* @param flags - Parsed flag map.
|
|
40
|
+
* @returns Trimmed jq expression, or `undefined` if absent / blank.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* const jq = resolveJqFilter(flags); // ".[].name" | undefined
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare function resolveJqFilter(flags: ParsedFlags): string | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* Resolves the effective output format from the cascade:
|
|
50
|
+
* CLI flag → global config default → command default → `"compress"`.
|
|
51
|
+
*
|
|
52
|
+
* @param options - Resolution options.
|
|
53
|
+
* @returns The effective output format.
|
|
54
|
+
*/
|
|
55
|
+
export declare function resolveFormat({ flags, def, configDefault, }: ResolveOutputOptions): OutputFormat;
|
|
56
|
+
/**
|
|
57
|
+
* Resolves both the output format and jq filter, and validates that `--jq`
|
|
58
|
+
* is only used with `json` or `compress` formats.
|
|
59
|
+
*
|
|
60
|
+
* @param options - Resolution options.
|
|
61
|
+
* @returns Resolved format and jq filter.
|
|
62
|
+
* @throws Error with code `"validation_error"` if `--jq` is used with an incompatible format.
|
|
63
|
+
*/
|
|
64
|
+
export declare function resolveOutputConfig(options: ResolveOutputOptions): ResolvedOutputConfig;
|
|
65
|
+
/** Options for {@link assertRiskWithinLevel}. */
|
|
66
|
+
export interface AssertRiskWithinLevelOptions {
|
|
67
|
+
/** Human-readable command label for error messages. */
|
|
68
|
+
commandLabel: string;
|
|
69
|
+
/** Risk level declared by the command. */
|
|
70
|
+
commandRisk: Risk;
|
|
71
|
+
/**
|
|
72
|
+
* User-configured risk ceiling from the config file. Commands with a
|
|
73
|
+
* higher risk than this will be blocked.
|
|
74
|
+
*/
|
|
75
|
+
configuredRiskLevel?: Risk;
|
|
76
|
+
/** Whether the current session is non-interactive. */
|
|
77
|
+
nonInteractive: boolean;
|
|
78
|
+
/** `true` when `--dry-run` was supplied. */
|
|
79
|
+
dryRun?: boolean;
|
|
80
|
+
/** Skip the assertion entirely when `dryRun` is active. */
|
|
81
|
+
skipWhenDryRun?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Optional side-effect callback invoked with the violation message
|
|
84
|
+
* before the error is thrown (e.g. for logging).
|
|
85
|
+
*/
|
|
86
|
+
onViolation?: (message: string) => void;
|
|
87
|
+
/**
|
|
88
|
+
* Factory for creating the error to throw. Allows the caller to use
|
|
89
|
+
* its own error type (e.g. a framework {@link CliError}).
|
|
90
|
+
*/
|
|
91
|
+
createError: (message: string) => unknown;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Asserts that the command's risk level does not exceed the configured ceiling.
|
|
95
|
+
*
|
|
96
|
+
* The comparison uses {@link riskLevelOrder}: `high-risk-write (2) > write (1) > read (0)`.
|
|
97
|
+
*
|
|
98
|
+
* @param options - Assertion options.
|
|
99
|
+
* @throws The error returned by `options.createError` when the ceiling is exceeded.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```ts
|
|
103
|
+
* assertRiskWithinLevel({
|
|
104
|
+
* commandLabel: "app delete",
|
|
105
|
+
* commandRisk: "high-risk-write",
|
|
106
|
+
* configuredRiskLevel: "write",
|
|
107
|
+
* nonInteractive: true,
|
|
108
|
+
* createError: (msg) => cliErrors.validation(msg),
|
|
109
|
+
* });
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
export declare function assertRiskWithinLevel(options: AssertRiskWithinLevelOptions): void;
|
|
113
|
+
/** Options for {@link buildRuntimeContext}. */
|
|
114
|
+
export interface BuildRuntimeContextOptions<Extras extends object = {}> {
|
|
115
|
+
/** Full command label (e.g. `"dataset list"`). */
|
|
116
|
+
commandLabel: string;
|
|
117
|
+
/** Resolved output format. */
|
|
118
|
+
format: OutputFormat;
|
|
119
|
+
/** Resolved jq expression. */
|
|
120
|
+
jqFilter?: string;
|
|
121
|
+
/** Parsed flags map. */
|
|
122
|
+
flags: ParsedFlags;
|
|
123
|
+
/** Command definition for metadata (args, risk, etc.). */
|
|
124
|
+
def: CommandDefinition;
|
|
125
|
+
/** `true` when running non-interactively. */
|
|
126
|
+
nonInteractive: boolean;
|
|
127
|
+
/** Positional arguments. */
|
|
128
|
+
args: string[];
|
|
129
|
+
/**
|
|
130
|
+
* Static default values for boolean and number flags that take effect
|
|
131
|
+
* when the flag is absent. Allows commands to distinguish "not set"
|
|
132
|
+
* from "set to false/0".
|
|
133
|
+
*/
|
|
134
|
+
defaults?: {
|
|
135
|
+
booleans?: Record<string, boolean | undefined>;
|
|
136
|
+
numbers?: Record<string, number | undefined>;
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* Additional context properties injected by the adapter's `prepare` hook.
|
|
140
|
+
* Examples: `appCode`, `cookie`, `session`, `config`.
|
|
141
|
+
*/
|
|
142
|
+
extras?: Extras;
|
|
143
|
+
/**
|
|
144
|
+
* The `formatOutput` function from the adapter, bound to the correct
|
|
145
|
+
* command label and risk so the context can render results correctly.
|
|
146
|
+
*/
|
|
147
|
+
formatOutput: <T>(result: CommandResult<T>, options: {
|
|
148
|
+
command: string;
|
|
149
|
+
risk: CommandDefinition["risk"];
|
|
150
|
+
format: OutputFormat;
|
|
151
|
+
jqFilter?: string;
|
|
152
|
+
}) => void;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Constructs the {@link RuntimeContext} object passed to every command's
|
|
156
|
+
* `validate`, `dryRun`, and `execute` function.
|
|
157
|
+
*
|
|
158
|
+
* The context provides:
|
|
159
|
+
* - Typed accessors for flags (`str`, `bool`, `num`, `flag`).
|
|
160
|
+
* - Positional argument accessors (`arg`).
|
|
161
|
+
* - The `output()` method for rendering results through the formatter.
|
|
162
|
+
*
|
|
163
|
+
* @template Extras - Shape of the adapter-supplied extras object.
|
|
164
|
+
* @param opts - Construction options.
|
|
165
|
+
* @returns A fully populated runtime context object.
|
|
166
|
+
*/
|
|
167
|
+
export declare function buildRuntimeContext<Extras extends object = {}>(opts: BuildRuntimeContextOptions<Extras>): RuntimeContext<Extras>;
|
|
168
|
+
/** Options for {@link runDryRunIfNeeded}. */
|
|
169
|
+
export interface RunDryRunOptions {
|
|
170
|
+
/** Parsed flags map (checked for `--dry-run`). */
|
|
171
|
+
flags: ParsedFlags;
|
|
172
|
+
/** Command definition whose `dryRun` hook will be called. */
|
|
173
|
+
def: CommandDefinition;
|
|
174
|
+
/** Fully constructed runtime context. */
|
|
175
|
+
ctx: RuntimeContext;
|
|
176
|
+
/** Human-readable command label for error messages. */
|
|
177
|
+
commandLabel: string;
|
|
178
|
+
/** Resolved output format. */
|
|
179
|
+
format: OutputFormat;
|
|
180
|
+
/** Resolved jq expression. */
|
|
181
|
+
jqFilter?: string;
|
|
182
|
+
/** Adapter's `formatOutput` function. */
|
|
183
|
+
formatOutput: <T>(result: CommandResult<T>, options: {
|
|
184
|
+
command: string;
|
|
185
|
+
risk: CommandDefinition["risk"];
|
|
186
|
+
format: OutputFormat;
|
|
187
|
+
dryRun?: boolean;
|
|
188
|
+
jqFilter?: string;
|
|
189
|
+
}) => void;
|
|
190
|
+
/**
|
|
191
|
+
* Optional error mapper applied to errors thrown by `def.dryRun`.
|
|
192
|
+
* Allows the adapter to normalize framework-internal errors to its own type.
|
|
193
|
+
*/
|
|
194
|
+
mapError?: (error: unknown) => unknown;
|
|
195
|
+
/**
|
|
196
|
+
* Factory for the error thrown when `--dry-run` is used on a command
|
|
197
|
+
* that does not define a `dryRun` hook.
|
|
198
|
+
*/
|
|
199
|
+
createUnsupportedError: (message: string) => unknown;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Checks whether `--dry-run` was supplied and, if so, calls the command's
|
|
203
|
+
* `dryRun` hook and outputs the preview.
|
|
204
|
+
*
|
|
205
|
+
* @param options - Dry-run options.
|
|
206
|
+
* @returns `true` if dry-run was handled (caller should return early);
|
|
207
|
+
* `false` if `--dry-run` was not present.
|
|
208
|
+
* @throws The error from `createUnsupportedError` when `--dry-run` is used
|
|
209
|
+
* on a command without a `dryRun` hook.
|
|
210
|
+
*/
|
|
211
|
+
export declare function runDryRunIfNeeded(options: RunDryRunOptions): Promise<boolean>;
|
|
212
|
+
/** Options for {@link requireConfirmationPrompt}. */
|
|
213
|
+
export interface ConfirmationPromptOptions {
|
|
214
|
+
/**
|
|
215
|
+
* Lines displayed as the prompt (written to stderr).
|
|
216
|
+
* The last line typically ends with `(y/N)`.
|
|
217
|
+
*/
|
|
218
|
+
lines: string[];
|
|
219
|
+
/**
|
|
220
|
+
* Factory for the error thrown when the user declines or presses Ctrl+C.
|
|
221
|
+
* The error code should be `"cancelled"` so the top-level handler exits 0.
|
|
222
|
+
*/
|
|
223
|
+
createCancelledError: (message: string) => unknown;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Displays an interactive confirmation prompt on stderr and resolves
|
|
227
|
+
* when the user types `y` or `yes` (case-insensitive).
|
|
228
|
+
*
|
|
229
|
+
* - Any other answer throws a `"cancelled"` error.
|
|
230
|
+
* - Ctrl+C (SIGINT) also throws a `"cancelled"` error without a stack trace.
|
|
231
|
+
*
|
|
232
|
+
* @param options - Prompt options.
|
|
233
|
+
* @returns Resolves normally on confirmation.
|
|
234
|
+
* @throws The error from `createCancelledError` on decline or Ctrl+C.
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```ts
|
|
238
|
+
* await requireConfirmationPrompt({
|
|
239
|
+
* lines: ["This will permanently delete the dataset.", "Are you sure? (y/N)"],
|
|
240
|
+
* createCancelledError: (msg) => cliErrors.cancelled(msg),
|
|
241
|
+
* });
|
|
242
|
+
* ```
|
|
243
|
+
*/
|
|
244
|
+
export declare function requireConfirmationPrompt(options: ConfirmationPromptOptions): Promise<void>;
|