@finebi/finebi-cli 1.2.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/src/help.js ADDED
@@ -0,0 +1,452 @@
1
+ import {brandCommandValue, CLI_COMMAND_NAME, CLI_DISPLAY_NAME, commandLine} from "./branding.js";
2
+ import {cliError} from "./errors.js";
3
+ import {failure, success} from "./output.js";
4
+
5
+ const GLOBAL_OPTIONS = [
6
+ {name: "--profile", type: "string", description: "Environment name. Overrides FINEBI_PROFILE and the saved default; ordinary commands do not create environments or switch the default."},
7
+ {name: "--base-url", type: "string", description: "Connection URL override. A different server never inherits saved credentials or capabilities."},
8
+ {name: "--token", type: "string", description: "Temporary session token override; not persisted."},
9
+ ];
10
+
11
+ export function renderTopLevelHelp(manifest) {
12
+ return success("help", {
13
+ resources: (manifest?.resources || []).map((resource) => ({
14
+ name: resource.name,
15
+ summary: resource.summary,
16
+ })),
17
+ dynamicCapabilitiesLoaded: Boolean(manifest),
18
+ globalOptions: GLOBAL_OPTIONS,
19
+ }, { schemaVersion: manifest?.schemaVersion });
20
+ }
21
+
22
+ export function renderBuiltInHelp(resourceName) {
23
+ const resource = BUILT_IN_RESOURCES[resourceName];
24
+ if (!resource) {
25
+ return capabilityNotFound(resourceName);
26
+ }
27
+ return success("help", brandCommandValue({...resource, globalOptions: GLOBAL_OPTIONS}));
28
+ }
29
+
30
+ export function renderBuiltInCommandHelp(resourceName, commandName) {
31
+ const resource = BUILT_IN_RESOURCES[resourceName];
32
+ const command = resource?.commands?.find((item) => item.name === commandName);
33
+ if (!resource || !command) {
34
+ return capabilityNotFound(`${resourceName}.${commandName}`);
35
+ }
36
+ return success("help", brandCommandValue({
37
+ resource: resource.resource,
38
+ command: command.name,
39
+ summary: command.summary,
40
+ usage: command.usage,
41
+ options: command.options || [],
42
+ globalOptions: GLOBAL_OPTIONS,
43
+ details: command.details || [],
44
+ examples: command.examples || [],
45
+ }));
46
+ }
47
+
48
+ export function renderResourceHelp(manifest, resourceName) {
49
+ const resource = findResource(manifest, resourceName);
50
+ if (!resource) {
51
+ return capabilityNotFound(resourceName);
52
+ }
53
+ return success("help", {
54
+ resource: resource.name,
55
+ summary: resource.summary,
56
+ globalOptions: GLOBAL_OPTIONS,
57
+ commands: (resource.commands || []).map((command) => ({
58
+ name: command.name,
59
+ summary: command.summary,
60
+ })),
61
+ }, { schemaVersion: manifest.schemaVersion });
62
+ }
63
+
64
+ export function renderCommandHelp(manifest, resourceName, commandName) {
65
+ const resource = findResource(manifest, resourceName);
66
+ const command = resource?.commands?.find((item) => item.name === commandName);
67
+ if (!resource || !command) {
68
+ return capabilityNotFound(`${resourceName}.${commandName}`);
69
+ }
70
+ return success("help", {
71
+ resource: resource.name,
72
+ command: command.name,
73
+ summary: command.summary,
74
+ usage: usageFor(resource.name, command),
75
+ options: commandOptionsFor(command),
76
+ globalOptions: GLOBAL_OPTIONS,
77
+ display: command.display,
78
+ requestSchema: command.requestSchema,
79
+ responseSchema: command.responseSchema,
80
+ }, { schemaVersion: manifest.schemaVersion });
81
+ }
82
+
83
+ export function renderHelpText(envelope) {
84
+ if (!envelope.ok) {
85
+ return renderErrorText(envelope);
86
+ }
87
+ const data = envelope.data || {};
88
+ if (data.usage) {
89
+ return renderCommandHelpText(data);
90
+ }
91
+ if (data.commands) {
92
+ return renderResourceHelpText(data);
93
+ }
94
+ return renderTopLevelHelpText(data);
95
+ }
96
+
97
+ function findResource(manifest, resourceName) {
98
+ return (manifest.resources || []).find((resource) => resource.name === resourceName);
99
+ }
100
+
101
+ const BUILT_IN_RESOURCES = {
102
+ version: {
103
+ command: "version",
104
+ resource: "version",
105
+ summary: "Show FineBI CLI version.",
106
+ usage: "finebi-cli version",
107
+ },
108
+ capabilities: {
109
+ command: "capabilities",
110
+ resource: "capabilities",
111
+ summary: "Fetch FineBI dynamic capabilities and update the local cache.",
112
+ usage: "finebi-cli capabilities",
113
+ },
114
+ config: {
115
+ resource: "config",
116
+ summary: "Local FineBI CLI configuration.",
117
+ commands: [
118
+ {
119
+ name: "get",
120
+ summary: "Show local configuration with secrets masked.",
121
+ usage: "finebi-cli config get",
122
+ },
123
+ {
124
+ name: "set",
125
+ summary: "Set base-url, timeout-ms, or capability-ttl-ms.",
126
+ usage: "finebi-cli config set <key> <value>",
127
+ details: [
128
+ "Keys:",
129
+ " base-url",
130
+ " timeout-ms",
131
+ " capability-ttl-ms",
132
+ ],
133
+ examples: [
134
+ "finebi-cli config set base-url http://localhost:37799/webroot/decision",
135
+ "finebi-cli config set timeout-ms 30000",
136
+ "finebi-cli config set capability-ttl-ms 600000",
137
+ ],
138
+ },
139
+ {
140
+ name: "clear",
141
+ summary: "Remove the selected environment's local configuration, credentials and capability cache; does not revoke server keys.",
142
+ usage: "finebi-cli config clear",
143
+ },
144
+ ],
145
+ },
146
+ auth: {
147
+ resource: "auth",
148
+ summary: "FineBI authentication commands.",
149
+ commands: [
150
+ {
151
+ name: "login",
152
+ summary: "Login with FineBI credentials or browser authorization and persist an Access Key.",
153
+ usage: "finebi-cli auth login [--profile <name>] [--base-url <url>] [--browser] [--username <username>] [--from-env] [--password-stdin] [--no-use]",
154
+ options: [
155
+ { name: "--no-use", type: "boolean", description: "Save successful login without changing the default environment." },
156
+ { name: "--username", type: "string", description: "FineBI username." },
157
+ { name: "--from-env", type: "boolean", description: "Read base URL, username, and password from environment variables." },
158
+ { name: "--password-stdin", type: "boolean", description: "Read password from standard input." },
159
+ { name: "--browser", type: "boolean", description: "Authorize this CLI from a signed-in FineBI browser without sending a password." },
160
+ ],
161
+ details: [
162
+ "Login creates the named environment if missing and selects it after successful authentication, unless --no-use is supplied.",
163
+ "A new environment requires --base-url or FINEBI_BASE_URL; it never inherits another environment's address or identity.",
164
+ "Failed authentication preserves saved environments and the default selection.",
165
+ "Environment variables for --from-env:",
166
+ " FINEBI_BASE_URL",
167
+ " FINEBI_USERNAME",
168
+ " FINEBI_PASSWORD",
169
+ "Browser authorization opens a same-server confirmation URL and polls until it is approved, denied, or expires.",
170
+ "Successful login refreshes the dynamic capability cache automatically.",
171
+ ],
172
+ examples: [
173
+ "finebi-cli auth login --username <username>",
174
+ "finebi-cli auth login --from-env",
175
+ "echo <password> | finebi-cli auth login --username <username> --password-stdin",
176
+ "finebi-cli auth login --browser",
177
+ "finebi-cli auth login --profile test --base-url https://test.example.com/webroot/decision --browser",
178
+ ],
179
+ },
180
+ {
181
+ name: "list",
182
+ summary: "List saved environments and mark the default; local metadata only, no credential validation or secret output.",
183
+ usage: "finebi-cli auth list",
184
+ },
185
+ {
186
+ name: "use",
187
+ summary: "Select an existing environment as default without logging in.",
188
+ usage: "finebi-cli auth use <name>",
189
+ },
190
+ {
191
+ name: "remove",
192
+ summary: "Delete one local environment and its capability cache; does not revoke server keys.",
193
+ usage: "finebi-cli auth remove <name>",
194
+ details: ["Use auth logout --profile <name> first to revoke its Access Key.", "Removing the default environment does not select another environment."],
195
+ },
196
+ {
197
+ name: "info",
198
+ summary: "Show stored authentication metadata with secrets masked.",
199
+ usage: "finebi-cli auth info",
200
+ },
201
+ {
202
+ name: "logout",
203
+ summary: "Revoke the Access Key before removing stored authentication.",
204
+ usage: "finebi-cli auth logout [--force]",
205
+ options: [
206
+ { name: "--force", type: "boolean", description: "Remove local credentials even when server-side revocation fails." },
207
+ ],
208
+ },
209
+ ],
210
+ },
211
+ capability: {
212
+ resource: "capability",
213
+ summary: "FineBI dynamic capability cache commands.",
214
+ commands: [
215
+ {
216
+ name: "refresh",
217
+ summary: "Fetch dynamic command capabilities from FineBI.",
218
+ usage: "finebi-cli capability refresh",
219
+ details: [
220
+ "Requires:",
221
+ " finebi-cli config set base-url <baseUrl>",
222
+ " finebi-cli auth login --username <username>",
223
+ "Login refreshes capabilities automatically; use this command when backend commands changed or the cache expired.",
224
+ ],
225
+ },
226
+ {
227
+ name: "info",
228
+ summary: "Show capability cache status.",
229
+ usage: "finebi-cli capability info",
230
+ },
231
+ {
232
+ name: "clear-cache",
233
+ summary: "Remove the local capability cache.",
234
+ usage: "finebi-cli capability clear-cache",
235
+ },
236
+ ],
237
+ },
238
+ };
239
+
240
+ function capabilityNotFound(path) {
241
+ return failure("help", cliError("CAPABILITY_NOT_FOUND", {
242
+ path,
243
+ message: `Capability not found: ${path}.`,
244
+ hint: "Run finebi-cli capability refresh and check finebi-cli --help.",
245
+ retryable: true,
246
+ nextActions: ["finebi-cli capability refresh", "finebi-cli --help"],
247
+ }));
248
+ }
249
+
250
+ function usageFor(resourceName, command) {
251
+ const options = optionsFor(command.requestSchema || { properties: {}, required: [] });
252
+ const argumentsText = argumentsFor(command.requestSchema || { properties: {}, required: [] }).map((argument) => {
253
+ return argument.required ? `<${argument.name}>` : `[${argument.name}]`;
254
+ });
255
+ const usageOptions = options.map((option) => {
256
+ const placeholder = option.type === "file" ? "file" : option.name.slice(2);
257
+ const argument = `${option.name} <${placeholder}>`;
258
+ return option.required ? argument : `[${argument}]`;
259
+ });
260
+ return [CLI_COMMAND_NAME, resourceName, command.name].concat(argumentsText).concat(usageOptions).join(" ");
261
+ }
262
+
263
+ function commandOptionsFor(command) {
264
+ return optionsFor(command.requestSchema || { properties: {}, required: [] }).concat([
265
+ {
266
+ name: "-f, --format",
267
+ required: false,
268
+ type: "format",
269
+ description: "Output format for dynamic command display: auto, table, expanded, or csv.",
270
+ default: "auto",
271
+ },
272
+ ]);
273
+ }
274
+
275
+ function optionsFor(schema) {
276
+ const required = schema.required || [];
277
+ return Object.keys(schema.properties || {}).filter((fieldName) => schema.properties[fieldName].input !== "argument").map((fieldName) => {
278
+ const property = schema.properties[fieldName];
279
+ const optionName = optionNameFor(fieldName, property);
280
+ const option = {
281
+ name: `--${optionName}`,
282
+ required: required.includes(fieldName),
283
+ type: property.file || property.input === "file" ? "file" : property.type,
284
+ description: property.description || "",
285
+ };
286
+ const enumValues = property.enumValues || property.enum || [];
287
+ if (enumValues.length > 0) {
288
+ option.enumValues = enumValues;
289
+ }
290
+ const defaultValue = property.defaultValue ?? property.default;
291
+ if (defaultValue !== undefined && defaultValue !== "") {
292
+ option.default = defaultValue;
293
+ }
294
+ return option;
295
+ });
296
+ }
297
+
298
+ function argumentsFor(schema) {
299
+ const required = schema.required || [];
300
+ return Object.keys(schema.properties || {}).filter((fieldName) => schema.properties[fieldName].input === "argument").map((fieldName) => ({
301
+ name: fieldName,
302
+ required: required.includes(fieldName),
303
+ description: schema.properties[fieldName].description || "",
304
+ }));
305
+ }
306
+
307
+ function camelToKebab(value) {
308
+ return value.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
309
+ }
310
+
311
+ function optionNameFor(fieldName, property) {
312
+ if (property.input === "file" || property.file) {
313
+ return property.option || "file";
314
+ }
315
+ return camelToKebab(fieldName);
316
+ }
317
+
318
+ function renderTopLevelHelpText(data) {
319
+ const resources = data.resources || [];
320
+ const firstResourceName = resources[0]?.name;
321
+ const resourceHelpStep = firstResourceName ? commandLine(firstResourceName, "--help") : commandLine("--help");
322
+ const lines = [
323
+ CLI_DISPLAY_NAME,
324
+ "",
325
+ "Usage:",
326
+ ` ${commandLine("<resource>", "<command>", "[options]")}`,
327
+ ` ${commandLine("<resource>", "--help")}`,
328
+ ` ${commandLine("<resource>", "<command>", "--help")}`,
329
+ "",
330
+ "Getting Started:",
331
+ ` 1. ${commandLine("config", "set", "base-url", "<baseUrl>")}`,
332
+ ` 2. ${commandLine("auth", "login", "--username", "<username>")}`,
333
+ ` 3. ${resourceHelpStep}`,
334
+ " Login refreshes dynamic capabilities automatically.",
335
+ ` Run ${commandLine("capability", "refresh")} when backend commands changed or the cache expired.`,
336
+ "",
337
+ "Resources:",
338
+ ];
339
+ lines.push(...formatRows(resources, "name", "summary"));
340
+ if (!data.dynamicCapabilitiesLoaded) {
341
+ lines.push(` Dynamic resources are not loaded yet. Login or run ${commandLine("capability", "refresh")} after login.`);
342
+ }
343
+ lines.push(
344
+ "",
345
+ "Common Commands:",
346
+ ` ${commandLine("version")}`,
347
+ ` ${commandLine("capabilities")}`,
348
+ ` ${commandLine("config", "get")}`,
349
+ ` ${commandLine("config", "set", "base-url", "<baseUrl>")}`,
350
+ ` ${commandLine("auth", "login", "--username", "<username>")}`,
351
+ ` ${commandLine("auth", "login", "--from-env")}`,
352
+ ` ${commandLine("capability", "refresh")}`,
353
+ "",
354
+ "Use --help or -h for command details.",
355
+ "Use --json with --help for structured output.",
356
+ "Output options: --select <paths> / --max-chars <n> / --save <path> for large responses.",
357
+ "Environment: --profile <name> > FINEBI_PROFILE > saved default. Global options may precede or follow the command.",
358
+ );
359
+ lines.push("", "Global Options:", ...formatOptionRows(data.globalOptions || []));
360
+ return `${lines.join("\n")}\n`;
361
+ }
362
+
363
+ function renderResourceHelpText(data) {
364
+ const lines = [
365
+ data.resource,
366
+ "",
367
+ data.summary || "",
368
+ "",
369
+ "Usage:",
370
+ ` ${commandLine(data.resource, "<command>", "[options]")}`,
371
+ ` ${commandLine(data.resource, "<command>", "--help")}`,
372
+ "",
373
+ "Commands:",
374
+ ];
375
+ lines.push(...formatRows(data.commands || [], "name", "summary"));
376
+ lines.push("", "Global Options:", ...formatOptionRows(data.globalOptions || []));
377
+ lines.push("", `Use ${commandLine(data.resource, "<command>", "--help")} for command details.`);
378
+ return `${lines.join("\n")}\n`;
379
+ }
380
+
381
+ function renderCommandHelpText(data) {
382
+ const title = data.resource && data.resource !== data.command ? `${data.resource} ${data.command}` : data.command;
383
+ const lines = [
384
+ title,
385
+ "",
386
+ data.summary || "",
387
+ "",
388
+ "Usage:",
389
+ ` ${data.usage}`,
390
+ ];
391
+ if ((data.options || []).length > 0) {
392
+ lines.push("", "Options:");
393
+ lines.push(...formatOptionRows(data.options));
394
+ }
395
+ if ((data.globalOptions || []).length > 0) {
396
+ lines.push("", "Global Options:", ...formatOptionRows(data.globalOptions));
397
+ }
398
+ if ((data.details || []).length > 0) {
399
+ lines.push("", ...data.details);
400
+ }
401
+ if ((data.examples || []).length > 0) {
402
+ lines.push("", "Examples:");
403
+ lines.push(...data.examples.map((example) => ` ${example}`));
404
+ }
405
+ lines.push("", "Use --json for structured output.",
406
+ "Output options: --select <paths> / --max-chars <n> / --save <path> for large responses.");
407
+ return `${lines.join("\n")}\n`;
408
+ }
409
+
410
+ function renderErrorText(envelope) {
411
+ const error = envelope.errors?.[0] || {};
412
+ const lines = [
413
+ `Error: ${error.message || "Command failed."}`,
414
+ ];
415
+ if (error.hint) {
416
+ lines.push(`Hint: ${error.hint}`);
417
+ }
418
+ if ((error.nextActions || []).length > 0) {
419
+ lines.push("", "Next Actions:");
420
+ lines.push(...error.nextActions.map((action) => ` ${action}`));
421
+ }
422
+ return `${lines.join("\n")}\n`;
423
+ }
424
+
425
+ function formatRows(items, nameKey, summaryKey) {
426
+ if (items.length === 0) {
427
+ return [" (none)"];
428
+ }
429
+ const width = Math.max(...items.map((item) => String(item[nameKey] || "").length));
430
+ return items.map((item) => ` ${String(item[nameKey] || "").padEnd(width + 2)}${item[summaryKey] || ""}`);
431
+ }
432
+
433
+ function formatOptionRows(options) {
434
+ const optionTexts = options.map((option) => `${option.name} <${option.type || "value"}>`);
435
+ const width = Math.max(...optionTexts.map((item) => item.length));
436
+ return options.map((option, index) => {
437
+ const parts = [];
438
+ if (option.description) {
439
+ parts.push(option.description);
440
+ }
441
+ if (option.required) {
442
+ parts.push("Required.");
443
+ }
444
+ if (option.default !== undefined) {
445
+ parts.push(`Default: ${option.default}.`);
446
+ }
447
+ if ((option.enumValues || []).length > 0) {
448
+ parts.push(`Values: ${option.enumValues.join(", ")}.`);
449
+ }
450
+ return ` ${optionTexts[index].padEnd(width + 2)}${parts.join(" ")}`;
451
+ });
452
+ }