@utdk/cli 0.1.0-dev.646adf4
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/.turbo/turbo-build.log +4 -0
- package/LICENSE +373 -0
- package/dist/auth.d.ts +24 -0
- package/dist/auth.js +111 -0
- package/dist/auth.js.map +1 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +11 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli.d.ts +30 -0
- package/dist/cli.js +368 -0
- package/dist/cli.js.map +1 -0
- package/dist/format.d.ts +16 -0
- package/dist/format.js +111 -0
- package/dist/format.js.map +1 -0
- package/dist/providers.d.ts +70 -0
- package/dist/providers.js +317 -0
- package/dist/providers.js.map +1 -0
- package/package.json +32 -0
- package/src/auth.ts +123 -0
- package/src/bin.ts +11 -0
- package/src/cli.ts +451 -0
- package/src/format.ts +127 -0
- package/src/providers.ts +478 -0
- package/tsconfig.json +11 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core CLI logic for @utdk/cli.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* utdk [--output json|table] [--agent] <provider> <operation> [--flag value…]
|
|
6
|
+
* utdk --help
|
|
7
|
+
* utdk --version
|
|
8
|
+
* utdk <provider> --help
|
|
9
|
+
*
|
|
10
|
+
* Exit codes (matching cli-printing-press):
|
|
11
|
+
* 0 success
|
|
12
|
+
* 2 usage error (unknown provider / operation, bad args)
|
|
13
|
+
* 3 not found (HTTP 404)
|
|
14
|
+
* 4 auth failure (HTTP 401/403)
|
|
15
|
+
* 5 API error (HTTP 5xx)
|
|
16
|
+
* 7 rate limited (HTTP 429)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { readFileSync } from "fs";
|
|
20
|
+
import { join } from "path";
|
|
21
|
+
import { createClient } from "utdk/client";
|
|
22
|
+
|
|
23
|
+
import { resolveAuth, authEnvVars } from "./auth.js";
|
|
24
|
+
import {
|
|
25
|
+
defaultOutputMode,
|
|
26
|
+
renderHelpList,
|
|
27
|
+
writeOutput,
|
|
28
|
+
type OutputMode,
|
|
29
|
+
} from "./format.js";
|
|
30
|
+
import {
|
|
31
|
+
buildToolMetadata,
|
|
32
|
+
findOperation,
|
|
33
|
+
getProviderInfo,
|
|
34
|
+
listOperations,
|
|
35
|
+
listProviders,
|
|
36
|
+
UTDK_ROOT,
|
|
37
|
+
type OperationInfo,
|
|
38
|
+
} from "./providers.js";
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// CLI version
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
const VERSION = "0.1.0";
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Arg parsing
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
type ParsedArgs = {
|
|
51
|
+
output: OutputMode | undefined;
|
|
52
|
+
agent: boolean;
|
|
53
|
+
help: boolean;
|
|
54
|
+
version: boolean;
|
|
55
|
+
provider: string | undefined;
|
|
56
|
+
operation: string | undefined;
|
|
57
|
+
params: Record<string, unknown>;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function coerceValue(raw: string): unknown {
|
|
61
|
+
if (raw === "true") return true;
|
|
62
|
+
if (raw === "false") return false;
|
|
63
|
+
if (/^-?\d+(\.\d+)?$/.test(raw)) return Number(raw);
|
|
64
|
+
return raw;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function parseArgs(argv: string[]): ParsedArgs {
|
|
68
|
+
const result: ParsedArgs = {
|
|
69
|
+
output: undefined,
|
|
70
|
+
agent: false,
|
|
71
|
+
help: false,
|
|
72
|
+
version: false,
|
|
73
|
+
provider: undefined,
|
|
74
|
+
operation: undefined,
|
|
75
|
+
params: {},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const positionals: string[] = [];
|
|
79
|
+
|
|
80
|
+
let i = 0;
|
|
81
|
+
while (i < argv.length) {
|
|
82
|
+
const arg = argv[i] as string;
|
|
83
|
+
|
|
84
|
+
if (arg === "--help" || arg === "-h") {
|
|
85
|
+
result.help = true;
|
|
86
|
+
i++;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (arg === "--version" || arg === "-v") {
|
|
91
|
+
result.version = true;
|
|
92
|
+
i++;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (arg === "--agent") {
|
|
97
|
+
result.agent = true;
|
|
98
|
+
i++;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (arg === "--output" || arg === "-o") {
|
|
103
|
+
const val = argv[i + 1];
|
|
104
|
+
if (val === "json" || val === "table" || val === "agent") {
|
|
105
|
+
result.output = val;
|
|
106
|
+
i += 2;
|
|
107
|
+
} else {
|
|
108
|
+
i++;
|
|
109
|
+
}
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (arg.startsWith("--output=")) {
|
|
114
|
+
const val = arg.slice(9);
|
|
115
|
+
if (val === "json" || val === "table" || val === "agent") result.output = val;
|
|
116
|
+
i++;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (arg.startsWith("--") && arg.length > 2) {
|
|
121
|
+
const key = arg.slice(2);
|
|
122
|
+
const nextArg = argv[i + 1];
|
|
123
|
+
if (nextArg !== undefined && !nextArg.startsWith("--")) {
|
|
124
|
+
const existing = result.params[key];
|
|
125
|
+
const coerced = coerceValue(nextArg);
|
|
126
|
+
if (existing !== undefined) {
|
|
127
|
+
result.params[key] = Array.isArray(existing)
|
|
128
|
+
? [...existing, coerced]
|
|
129
|
+
: [existing, coerced];
|
|
130
|
+
} else {
|
|
131
|
+
result.params[key] = coerced;
|
|
132
|
+
}
|
|
133
|
+
i += 2;
|
|
134
|
+
} else {
|
|
135
|
+
result.params[key] = true;
|
|
136
|
+
i++;
|
|
137
|
+
}
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
positionals.push(arg);
|
|
142
|
+
i++;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
result.provider = positionals[0];
|
|
146
|
+
result.operation = positionals[1];
|
|
147
|
+
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// Error → exit code mapping
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
function exitCodeForError(error: unknown): number {
|
|
156
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
157
|
+
const m = /Request failed: (\d+)/.exec(msg);
|
|
158
|
+
if (!m) return 1;
|
|
159
|
+
const status = parseInt(m[1] ?? "0", 10);
|
|
160
|
+
if (status === 404) return 3;
|
|
161
|
+
if (status === 401 || status === 403) return 4;
|
|
162
|
+
if (status === 429) return 7;
|
|
163
|
+
if (status >= 500) return 5;
|
|
164
|
+
return 2;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Help text builders
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
function printGlobalHelp(): void {
|
|
172
|
+
const providers = listProviders();
|
|
173
|
+
process.stdout.write(
|
|
174
|
+
[
|
|
175
|
+
`utdk v${VERSION} — agent-native CLI for @utdk providers`,
|
|
176
|
+
"",
|
|
177
|
+
"Usage:",
|
|
178
|
+
" utdk [--output json|table] [--agent] <provider> <operation> [--flag value…]",
|
|
179
|
+
" utdk <provider> --help",
|
|
180
|
+
" utdk --help",
|
|
181
|
+
"",
|
|
182
|
+
"Global flags:",
|
|
183
|
+
" --output json|table Output format (default: table in TTY, json when piped)",
|
|
184
|
+
" --agent Compact JSON output optimised for agent parsing",
|
|
185
|
+
" --help, -h Show this help or provider/operation help",
|
|
186
|
+
" --version, -v Print version",
|
|
187
|
+
"",
|
|
188
|
+
"Providers:",
|
|
189
|
+
].join("\n"),
|
|
190
|
+
);
|
|
191
|
+
process.stdout.write("\n");
|
|
192
|
+
if (providers.length === 0) {
|
|
193
|
+
process.stdout.write(" (none found — is the utdk package built?)\n");
|
|
194
|
+
} else {
|
|
195
|
+
const items = providers.map((name): [string, string] => {
|
|
196
|
+
const info = getProviderInfo(name);
|
|
197
|
+
return [name, info?.description?.slice(0, 70) ?? ""];
|
|
198
|
+
});
|
|
199
|
+
process.stdout.write(renderHelpList(items, 20));
|
|
200
|
+
process.stdout.write("\n");
|
|
201
|
+
}
|
|
202
|
+
process.stdout.write("\n");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function printProviderHelp(providerName: string): void {
|
|
206
|
+
const info = getProviderInfo(providerName);
|
|
207
|
+
if (!info) {
|
|
208
|
+
process.stderr.write(`Provider "${providerName}" not found.\n`);
|
|
209
|
+
process.exitCode = 2;
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const ops = listOperations(providerName);
|
|
214
|
+
const envVars = authEnvVars(providerName, info.auth);
|
|
215
|
+
|
|
216
|
+
process.stdout.write(
|
|
217
|
+
[
|
|
218
|
+
`utdk ${providerName} — ${info.description ?? ""}`,
|
|
219
|
+
"",
|
|
220
|
+
"Usage:",
|
|
221
|
+
` utdk ${providerName} <operation> [--flag value…]`,
|
|
222
|
+
"",
|
|
223
|
+
"Auth environment variables:",
|
|
224
|
+
envVars.map((v) => ` ${v}`).join("\n"),
|
|
225
|
+
"",
|
|
226
|
+
`Operations (${ops.length} total):`,
|
|
227
|
+
].join("\n"),
|
|
228
|
+
);
|
|
229
|
+
process.stdout.write("\n");
|
|
230
|
+
|
|
231
|
+
const items: Array<[string, string]> = ops.slice(0, 100).map((op) => [
|
|
232
|
+
op.accessPath.join("."),
|
|
233
|
+
op.summary?.slice(0, 60) ?? `${op.method} ${op.path}`,
|
|
234
|
+
]);
|
|
235
|
+
process.stdout.write(renderHelpList(items, 40));
|
|
236
|
+
process.stdout.write("\n");
|
|
237
|
+
|
|
238
|
+
if (ops.length > 100) {
|
|
239
|
+
process.stdout.write(
|
|
240
|
+
`\n … and ${ops.length - 100} more. Filter with: utdk ${providerName} --help | grep <term>\n`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
process.stdout.write("\n");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function printOperationHelp(providerName: string, op: OperationInfo): void {
|
|
247
|
+
const info = getProviderInfo(providerName);
|
|
248
|
+
const envVars = authEnvVars(providerName, info?.auth ?? []);
|
|
249
|
+
|
|
250
|
+
process.stdout.write(
|
|
251
|
+
[
|
|
252
|
+
`utdk ${providerName} ${op.accessPath.join(".")}`,
|
|
253
|
+
"",
|
|
254
|
+
`${op.method} ${op.path}`,
|
|
255
|
+
op.summary ? `Summary: ${op.summary}` : "",
|
|
256
|
+
op.description ? `\n${op.description.slice(0, 300)}` : "",
|
|
257
|
+
"",
|
|
258
|
+
"Parameters:",
|
|
259
|
+
]
|
|
260
|
+
.filter((l) => l !== "")
|
|
261
|
+
.join("\n"),
|
|
262
|
+
);
|
|
263
|
+
process.stdout.write("\n");
|
|
264
|
+
|
|
265
|
+
if (op.parameters.length === 0) {
|
|
266
|
+
process.stdout.write(" (none)\n");
|
|
267
|
+
} else {
|
|
268
|
+
const items: Array<[string, string]> = op.parameters.map((p) => [
|
|
269
|
+
`--${p.name}${p.required ? " *" : ""}`,
|
|
270
|
+
[p.in, p.type, p.description?.slice(0, 50)].filter(Boolean).join(" · "),
|
|
271
|
+
]);
|
|
272
|
+
process.stdout.write(renderHelpList(items, 30));
|
|
273
|
+
process.stdout.write("\n");
|
|
274
|
+
process.stdout.write("\n * = required\n");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
process.stdout.write("\nAuth:\n");
|
|
278
|
+
process.stdout.write(envVars.map((v) => ` ${v}`).join("\n"));
|
|
279
|
+
process.stdout.write("\n\n");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
// Operation executor
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
/** Navigate a nested object using a dot-path. */
|
|
287
|
+
function getNestedValue(obj: unknown, path: string[]): unknown {
|
|
288
|
+
return path.reduce<unknown>((current, key) => {
|
|
289
|
+
if (current !== null && typeof current === "object") {
|
|
290
|
+
return (current as Record<string, unknown>)[key];
|
|
291
|
+
}
|
|
292
|
+
return undefined;
|
|
293
|
+
}, obj);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function executeOperation(
|
|
297
|
+
providerName: string,
|
|
298
|
+
op: OperationInfo,
|
|
299
|
+
params: Record<string, unknown>,
|
|
300
|
+
outputMode: OutputMode,
|
|
301
|
+
): Promise<number> {
|
|
302
|
+
// Load the OpenAPI document for this provider
|
|
303
|
+
const openApiPath = join(UTDK_ROOT, providerName, "openapi.json");
|
|
304
|
+
let openApiDocument: object;
|
|
305
|
+
try {
|
|
306
|
+
openApiDocument = JSON.parse(readFileSync(openApiPath, "utf-8")) as object;
|
|
307
|
+
} catch {
|
|
308
|
+
process.stderr.write(`Failed to load OpenAPI spec for "${providerName}".\n`);
|
|
309
|
+
return 2;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Resolve auth
|
|
313
|
+
const info = getProviderInfo(providerName);
|
|
314
|
+
const auth = resolveAuth(providerName, info?.auth ?? []);
|
|
315
|
+
|
|
316
|
+
// Build tool metadata from the OpenAPI spec so path/query/header/body
|
|
317
|
+
// params are routed correctly (provider metadata.ts files are often empty).
|
|
318
|
+
const allOps = listOperations(providerName);
|
|
319
|
+
const toolMetadata = buildToolMetadata(allOps);
|
|
320
|
+
|
|
321
|
+
// Create the client
|
|
322
|
+
let client: unknown;
|
|
323
|
+
try {
|
|
324
|
+
client = await createClient({
|
|
325
|
+
name: providerName,
|
|
326
|
+
openApiDocument,
|
|
327
|
+
toolMetadata,
|
|
328
|
+
...(auth ? { auth } : {}),
|
|
329
|
+
});
|
|
330
|
+
} catch (err) {
|
|
331
|
+
process.stderr.write(
|
|
332
|
+
`Failed to create client for "${providerName}": ${err instanceof Error ? err.message : String(err)}\n`,
|
|
333
|
+
);
|
|
334
|
+
return 2;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Navigate to the operation function
|
|
338
|
+
const fn = getNestedValue(client, op.accessPath);
|
|
339
|
+
if (typeof fn !== "function") {
|
|
340
|
+
process.stderr.write(
|
|
341
|
+
`Operation "${op.accessPath.join(".")}" not found on the ${providerName} client.\n`,
|
|
342
|
+
);
|
|
343
|
+
return 2;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Call it
|
|
347
|
+
let result: unknown;
|
|
348
|
+
try {
|
|
349
|
+
result = await (fn as (params: Record<string, unknown>) => Promise<unknown>)(params);
|
|
350
|
+
} catch (err) {
|
|
351
|
+
const code = exitCodeForError(err);
|
|
352
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
353
|
+
if (outputMode === "json" || outputMode === "agent") {
|
|
354
|
+
writeOutput({ ok: false, error: msg, exitCode: code }, outputMode);
|
|
355
|
+
} else {
|
|
356
|
+
process.stderr.write(`Error: ${msg}\n`);
|
|
357
|
+
}
|
|
358
|
+
return code;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
writeOutput(result, outputMode);
|
|
362
|
+
return 0;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ---------------------------------------------------------------------------
|
|
366
|
+
// Main entry
|
|
367
|
+
// ---------------------------------------------------------------------------
|
|
368
|
+
|
|
369
|
+
export async function runCli(argv: string[]): Promise<number> {
|
|
370
|
+
const args = parseArgs(argv);
|
|
371
|
+
|
|
372
|
+
// Determine output mode: --agent overrides --output
|
|
373
|
+
const outputMode: OutputMode = args.agent
|
|
374
|
+
? "agent"
|
|
375
|
+
: (args.output ?? defaultOutputMode());
|
|
376
|
+
|
|
377
|
+
// --version
|
|
378
|
+
if (args.version) {
|
|
379
|
+
process.stdout.write(`@utdk/cli v${VERSION}\n`);
|
|
380
|
+
return 0;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// utdk --help
|
|
384
|
+
if (args.help && !args.provider) {
|
|
385
|
+
printGlobalHelp();
|
|
386
|
+
return 0;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// No provider — show global help
|
|
390
|
+
if (!args.provider) {
|
|
391
|
+
printGlobalHelp();
|
|
392
|
+
return 0;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Validate provider
|
|
396
|
+
const providers = listProviders();
|
|
397
|
+
if (!providers.includes(args.provider)) {
|
|
398
|
+
if (outputMode === "json" || outputMode === "agent") {
|
|
399
|
+
writeOutput(
|
|
400
|
+
{
|
|
401
|
+
ok: false,
|
|
402
|
+
error: `Unknown provider: ${args.provider}`,
|
|
403
|
+
available: providers,
|
|
404
|
+
},
|
|
405
|
+
outputMode,
|
|
406
|
+
);
|
|
407
|
+
} else {
|
|
408
|
+
process.stderr.write(
|
|
409
|
+
`Unknown provider: "${args.provider}"\nAvailable: ${providers.join(", ")}\n`,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
return 2;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// utdk <provider> --help or utdk <provider> (no operation)
|
|
416
|
+
if (args.help || !args.operation) {
|
|
417
|
+
printProviderHelp(args.provider);
|
|
418
|
+
return 0;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Find the operation
|
|
422
|
+
const op = findOperation(args.provider, args.operation);
|
|
423
|
+
if (!op) {
|
|
424
|
+
// Maybe the user wants help on what they typed
|
|
425
|
+
if (outputMode === "json" || outputMode === "agent") {
|
|
426
|
+
writeOutput(
|
|
427
|
+
{
|
|
428
|
+
ok: false,
|
|
429
|
+
error: `Unknown operation: ${args.operation}`,
|
|
430
|
+
provider: args.provider,
|
|
431
|
+
},
|
|
432
|
+
outputMode,
|
|
433
|
+
);
|
|
434
|
+
} else {
|
|
435
|
+
process.stderr.write(
|
|
436
|
+
`Unknown operation "${args.operation}" for provider "${args.provider}".\n` +
|
|
437
|
+
`Run "utdk ${args.provider} --help" to list available operations.\n`,
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
return 2;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// utdk <provider> <operation> --help
|
|
444
|
+
if (args.help) {
|
|
445
|
+
printOperationHelp(args.provider, op);
|
|
446
|
+
return 0;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Execute
|
|
450
|
+
return executeOperation(args.provider, op, args.params, outputMode);
|
|
451
|
+
}
|
package/src/format.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output formatting for @utdk/cli.
|
|
3
|
+
*
|
|
4
|
+
* Supports three modes:
|
|
5
|
+
* - json : pretty-printed JSON (default when stdout is not a TTY)
|
|
6
|
+
* - table : padded text table (default when stdout is a TTY)
|
|
7
|
+
* - agent : compact single-line JSON with no ANSI escapes
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type OutputMode = "json" | "table" | "agent";
|
|
11
|
+
|
|
12
|
+
/** Detect the default output mode based on whether stdout is a TTY. */
|
|
13
|
+
export function defaultOutputMode(): OutputMode {
|
|
14
|
+
return process.stdout.isTTY ? "table" : "json";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// JSON output
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
export function formatJson(data: unknown, compact = false): string {
|
|
22
|
+
return compact ? JSON.stringify(data) : JSON.stringify(data, null, 2);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Table output
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
/** Render a two-column key/value table for a plain object. */
|
|
30
|
+
function renderObjectTable(obj: Record<string, unknown>): string {
|
|
31
|
+
const entries = Object.entries(obj);
|
|
32
|
+
const keyWidth = Math.min(
|
|
33
|
+
40,
|
|
34
|
+
Math.max(4, ...entries.map(([k]) => k.length)),
|
|
35
|
+
);
|
|
36
|
+
const lines: string[] = [];
|
|
37
|
+
for (const [key, value] of entries) {
|
|
38
|
+
const k = key.padEnd(keyWidth);
|
|
39
|
+
const v = valueToCell(value);
|
|
40
|
+
lines.push(`${k} ${v}`);
|
|
41
|
+
}
|
|
42
|
+
return lines.join("\n");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Render an array of objects as a columnar table. */
|
|
46
|
+
function renderArrayTable(items: Record<string, unknown>[]): string {
|
|
47
|
+
if (items.length === 0) return "(empty)";
|
|
48
|
+
// Collect all column names from the first item (limit to 8 columns for readability)
|
|
49
|
+
const allKeys = Object.keys(items[0] ?? {});
|
|
50
|
+
const keys = allKeys.slice(0, 8);
|
|
51
|
+
|
|
52
|
+
const widths: number[] = keys.map((k) => {
|
|
53
|
+
const maxValue = Math.max(...items.map((r) => valueToCell(r[k]).length));
|
|
54
|
+
return Math.min(60, Math.max(k.length, maxValue));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const header = keys.map((k, i) => k.padEnd(widths[i] ?? k.length)).join(" ");
|
|
58
|
+
const divider = widths.map((w) => "─".repeat(w)).join(" ");
|
|
59
|
+
const rows = items.map((item) =>
|
|
60
|
+
keys
|
|
61
|
+
.map((k, i) => valueToCell(item[k]).slice(0, widths[i] ?? 60).padEnd(widths[i] ?? 0))
|
|
62
|
+
.join(" "),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
return [header, divider, ...rows].join("\n");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function valueToCell(v: unknown): string {
|
|
69
|
+
if (v === null || v === undefined) return "";
|
|
70
|
+
if (typeof v === "string") return v;
|
|
71
|
+
if (typeof v === "number" || typeof v === "boolean") return String(v);
|
|
72
|
+
if (Array.isArray(v)) return `[${v.length} items]`;
|
|
73
|
+
if (typeof v === "object") return "{…}";
|
|
74
|
+
return String(v);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isObjectArray(data: unknown): data is Record<string, unknown>[] {
|
|
78
|
+
return (
|
|
79
|
+
Array.isArray(data) &&
|
|
80
|
+
data.length > 0 &&
|
|
81
|
+
typeof data[0] === "object" &&
|
|
82
|
+
data[0] !== null &&
|
|
83
|
+
!Array.isArray(data[0])
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isPlainObject(data: unknown): data is Record<string, unknown> {
|
|
88
|
+
return typeof data === "object" && data !== null && !Array.isArray(data);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function formatTable(data: unknown): string {
|
|
92
|
+
if (isObjectArray(data)) return renderArrayTable(data);
|
|
93
|
+
if (isPlainObject(data)) return renderObjectTable(data);
|
|
94
|
+
if (Array.isArray(data)) return data.map((item) => valueToCell(item)).join("\n");
|
|
95
|
+
return valueToCell(data);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Unified output writer
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
export function writeOutput(data: unknown, mode: OutputMode): void {
|
|
103
|
+
let out: string;
|
|
104
|
+
switch (mode) {
|
|
105
|
+
case "json":
|
|
106
|
+
out = formatJson(data, false);
|
|
107
|
+
break;
|
|
108
|
+
case "agent":
|
|
109
|
+
out = formatJson(data, true);
|
|
110
|
+
break;
|
|
111
|
+
case "table":
|
|
112
|
+
out = formatTable(data);
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
process.stdout.write(`${out}\n`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// Help table helpers
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
/** Render a two-column list with fixed-width left column. */
|
|
123
|
+
export function renderHelpList(items: Array<[string, string]>, leftWidth = 30): string {
|
|
124
|
+
return items
|
|
125
|
+
.map(([left, right]) => ` ${left.padEnd(leftWidth)} ${right}`)
|
|
126
|
+
.join("\n");
|
|
127
|
+
}
|