@finebi/finebi-cli 1.2.0 → 1.3.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 (65) hide show
  1. package/README.md +62 -142
  2. package/package.json +38 -35
  3. package/skills/finebi-cli-agent/SKILL.md +133 -0
  4. package/skills/finebi-cli-agent/agents/openai.yaml +7 -0
  5. package/skills/finebi-cli-agent/references/analysis-dsl-api.md +318 -0
  6. package/skills/finebi-cli-agent/references/artifact-ordering.md +29 -0
  7. package/skills/finebi-cli-agent/references/command-workflows.md +194 -0
  8. package/skills/finebi-cli-agent/references/dashboard-dsl-api.md +1025 -0
  9. package/skills/finebi-cli-agent/references/dsl-handoff-contract.md +195 -0
  10. package/skills/finebi-cli-agent/references/file-to-dashboard.md +65 -0
  11. package/skills/finebi-cli-agent/references/formula-basics.md +102 -0
  12. package/skills/finebi-cli-agent/references/formula-contexts.md +66 -0
  13. package/skills/finebi-cli-agent/references/formula-functions.md +247 -0
  14. package/skills/finebi-cli-agent/references/formula-generation.md +121 -0
  15. package/skills/finebi-cli-agent/references/formula-glossary.md +17 -0
  16. package/skills/finebi-cli-agent/references/formula-help-boundaries.md +87 -0
  17. package/skills/finebi-cli-agent/references/formula-help-index.md +264 -0
  18. package/skills/finebi-cli-agent/references/formula-parameters-aggregate.md +143 -0
  19. package/skills/finebi-cli-agent/references/formula-parameters-analysis.md +223 -0
  20. package/skills/finebi-cli-agent/references/formula-parameters-date.md +236 -0
  21. package/skills/finebi-cli-agent/references/formula-parameters-math-logic.md +271 -0
  22. package/skills/finebi-cli-agent/references/formula-parameters-text.md +217 -0
  23. package/skills/finebi-cli-agent/references/formula-recipes-analysis.md +124 -0
  24. package/skills/finebi-cli-agent/references/formula-recipes-text-date.md +86 -0
  25. package/skills/finebi-cli-agent/references/formula-semantics.md +39 -0
  26. package/skills/finebi-cli-agent/references/formula-syntax.md +70 -0
  27. package/skills/finebi-cli-agent/references/formula-troubleshooting.md +63 -0
  28. package/skills/finebi-cli-agent/references/formula-validation.md +36 -0
  29. package/skills/finebi-cli-agent/references/formula.md +37 -0
  30. package/skills/finebi-cli-agent/references/handoff-contract.md +210 -0
  31. package/skills/finebi-cli-agent/references/mapping-rules.md +423 -0
  32. package/skills/finebi-cli-agent/references/mutation-confirmation-contract.md +150 -0
  33. package/skills/finebi-cli-agent/references/scenario-workflows.md +75 -0
  34. package/skills/finebi-cli-agent/references/widget-dsl-api-examples.md +457 -0
  35. package/skills/finebi-cli-agent/references/widget-dsl-api.md +1340 -0
  36. package/skills/finebi-cli-agent/scripts/finebi-cli/package.json +11 -0
  37. package/skills/finebi-cli-agent/scripts/finebi-cli/src/access-key-contract.js +2 -0
  38. package/skills/finebi-cli-agent/scripts/finebi-cli/src/access-key.js +174 -0
  39. package/skills/finebi-cli-agent/scripts/finebi-cli/src/auth-failure.js +18 -0
  40. package/skills/finebi-cli-agent/scripts/finebi-cli/src/auth-headers.js +21 -0
  41. package/skills/finebi-cli-agent/scripts/finebi-cli/src/auth.js +116 -0
  42. package/skills/finebi-cli-agent/scripts/finebi-cli/src/branding.js +31 -0
  43. package/skills/finebi-cli-agent/scripts/finebi-cli/src/browser-auth.js +331 -0
  44. package/skills/finebi-cli-agent/scripts/finebi-cli/src/capability.js +122 -0
  45. package/skills/finebi-cli-agent/scripts/finebi-cli/src/config-permissions.js +87 -0
  46. package/skills/finebi-cli-agent/scripts/finebi-cli/src/config.js +189 -0
  47. package/skills/finebi-cli-agent/scripts/finebi-cli/src/dynamic-command.js +324 -0
  48. package/skills/finebi-cli-agent/scripts/finebi-cli/src/error-catalog.js +482 -0
  49. package/skills/finebi-cli-agent/scripts/finebi-cli/src/errors.js +22 -0
  50. package/skills/finebi-cli-agent/scripts/finebi-cli/src/help.js +467 -0
  51. package/skills/finebi-cli-agent/scripts/finebi-cli/src/index.js +1343 -0
  52. package/skills/finebi-cli-agent/scripts/finebi-cli/src/install-skill.js +87 -0
  53. package/skills/finebi-cli-agent/scripts/finebi-cli/src/output-limits.js +188 -0
  54. package/skills/finebi-cli-agent/scripts/finebi-cli/src/output.js +33 -0
  55. package/skills/finebi-cli-agent/scripts/finebi-cli/src/profiles.js +158 -0
  56. package/skills/finebi-cli-agent/scripts/finebi-cli/src/schema-options.js +265 -0
  57. package/skills/finebi-cli-agent/scripts/finebi-cli/src/schema-validator.js +194 -0
  58. package/skills/finebi-cli-agent/scripts/finebi-cli/src/skill-files.js +94 -0
  59. package/skills/finebi-cli-agent/scripts/finebi-cli-runner-core.mjs +399 -0
  60. package/skills/finebi-cli-agent/scripts/finebi-cli-runner.mjs +29 -0
  61. package/src/error-catalog.js +12 -0
  62. package/src/help.js +15 -0
  63. package/src/index.js +6 -1
  64. package/src/install-skill.js +87 -0
  65. package/src/skill-files.js +94 -0
@@ -0,0 +1,1343 @@
1
+ #!/usr/bin/env node
2
+ import {existsSync, readFileSync, realpathSync, rmSync} from "node:fs";
3
+ import {hostname} from "node:os";
4
+ import {dirname, join} from "node:path";
5
+ import {fileURLToPath, pathToFileURL} from "node:url";
6
+
7
+ import {provisionAccessKey, selfRevokeAccessKey} from "./access-key.js";
8
+ import {authorizeWithBrowser} from "./browser-auth.js";
9
+ import {brandCommandText, CLI_COMMAND_NAME, commandLine} from "./branding.js";
10
+ import {
11
+ cacheCapability,
12
+ capabilityCachePath,
13
+ fetchCapabilities,
14
+ readCapabilityCache,
15
+ resolveCapability,
16
+ } from "./capability.js";
17
+ import {defaultConfigPath, maskConfig, resolveConfig, sameBaseUrl, updateConfigValue} from "./config.js";
18
+ import {parseConnectionArgs, ProfileError, profileCapabilityPath, readProfile, readProfiles, removeProfile, saveProfile, selectProfile, useProfile, withProfileLoginLock} from "./profiles.js";
19
+ import {loginWithPassword, resolveLoginInputFromEnv} from "./auth.js";
20
+ import {executeDynamicCommand} from "./dynamic-command.js";
21
+ import {handleInstallSkill} from "./install-skill.js";
22
+ import {cliError} from "./errors.js";
23
+ import {
24
+ renderBuiltInCommandHelp,
25
+ renderBuiltInHelp,
26
+ renderCommandHelp,
27
+ renderHelpText,
28
+ renderResourceHelp,
29
+ renderTopLevelHelp,
30
+ } from "./help.js";
31
+ import {failure, success} from "./output.js";
32
+ import {
33
+ SELECT_FLAG,
34
+ parseOutputArgs,
35
+ saveFailure,
36
+ saveOutput,
37
+ savedSummary,
38
+ selectPaths,
39
+ truncateMiddle,
40
+ } from "./output-limits.js";
41
+
42
+ const AUTH_EXPIRE_SKEW_MS = 300000;
43
+
44
+ export async function runCli(argv, context = {}) {
45
+ try {
46
+ const {args, flags} = parseConnectionArgs(argv);
47
+ const env = context.env || process.env;
48
+ if (args[0] === "install-skill") {
49
+ return isHelpFlagInvocation(args) ? renderBuiltInHelp("install-skill") : handleInstallSkill(args.slice(1), context);
50
+ }
51
+ const management = args[0] === "auth" && ["list", "use", "remove"].includes(args[1]);
52
+ const builtInHelp = isHelpFlagInvocation(args) && (args.length === 1 || isBuiltInResource(args[0]));
53
+ const optional = management || builtInHelp || args.length === 0 || args[0] === "version";
54
+ const profileName = management ? null : selectProfile(configPath(context), firstNonEmpty(flags.profile, env.FINEBI_PROFILE), {
55
+ allowCreate: args[0] === "auth" && args[1] === "login", optional,
56
+ });
57
+ if (management && (flags.profile || env.FINEBI_PROFILE) && args[1] !== "list") {
58
+ if (flags.profile && flags.profile !== args[2]) throw new ProfileError("Use the positional environment name for auth use/remove; --profile must not select a different environment.");
59
+ }
60
+ context = {...context, profileName, connectionFlags: flags};
61
+ const mutatesProfile = !isHelpFlagInvocation(args) && (
62
+ (args[0] === "auth" && ["login", "logout", "remove"].includes(args[1])) ||
63
+ (args[0] === "config" && ["set", "clear"].includes(args[1]))
64
+ );
65
+ const envelope = mutatesProfile
66
+ ? await withProfileLoginLock(configPath(context), args[0] === "auth" && args[1] === "remove" ? args[2] : profileName,
67
+ () => dispatchCli(args, context))
68
+ : await dispatchCli(args, context);
69
+ if (profileName) {
70
+ envelope.meta = {...envelope.meta, profile: profileName};
71
+ if (profileName !== "default" || readProfiles(configPath(context)).multi) {
72
+ for (const error of [...(envelope.errors || []), ...(envelope.warnings || [])]) {
73
+ error.nextActions = (error.nextActions || []).map((action) => typeof action === "string" && !action.includes("--profile")
74
+ ? action.replaceAll(`${CLI_COMMAND_NAME} `, `${CLI_COMMAND_NAME} --profile ${profileName} `) : action);
75
+ }
76
+ }
77
+ }
78
+ return envelope;
79
+ } catch (error) {
80
+ if (!(error instanceof ProfileError)) throw error;
81
+ return failure("profile", cliError("INVALID_ARGUMENT", {
82
+ path: "profile", message: error.message,
83
+ nextActions: ["finebi-cli auth list", "finebi-cli auth login --help"],
84
+ }));
85
+ }
86
+ }
87
+
88
+ async function dispatchCli(args, context) {
89
+ if (args.length === 0) {
90
+ return handleHelp([], context);
91
+ }
92
+ if (isHelpFlagInvocation(args)) {
93
+ return handleHelp(args.filter((arg) => !isHelpFlag(arg)), context);
94
+ }
95
+ const command = args.shift();
96
+ switch (command) {
97
+ case "config":
98
+ if (args.length === 0) {
99
+ return handleHelp(["config"], context);
100
+ }
101
+ return handleConfig(args, context);
102
+ case "auth":
103
+ if (args.length === 0) {
104
+ return handleHelp(["auth"], context);
105
+ }
106
+ return handleAuth(args, context);
107
+ case "capability":
108
+ if (args.length === 0) {
109
+ return handleHelp(["capability"], context);
110
+ }
111
+ return handleCapability(args, context);
112
+ case "capabilities":
113
+ return fetchAndCacheCapabilities("capabilities", context);
114
+ case "version":
115
+ return handleVersion(context);
116
+ case "--help":
117
+ case "-h":
118
+ return handleHelp(args, context);
119
+ case "help":
120
+ return usageError("help", "The help command is not supported. Use --help or -h instead.", [
121
+ "finebi-cli --help",
122
+ "finebi-cli <resource> --help",
123
+ "finebi-cli <resource> <command> --help",
124
+ ]);
125
+ default:
126
+ return handleDynamic(command, args, context);
127
+ }
128
+ }
129
+
130
+ export async function main(argv = process.argv.slice(2), context = {}) {
131
+ const parsed = parseOutputArgs(argv);
132
+ if (!parsed.ok) {
133
+ process.stdout.write(formatCliOutput(failure("output.args", parsed.error), jsonArgs(argv)));
134
+ process.exitCode = 1;
135
+ return;
136
+ }
137
+ const envelope = await runCli(parsed.args, context);
138
+ const rendered = renderFinalOutput(envelope, parsed.options, parsed.args);
139
+ process.stdout.write(rendered.text);
140
+ process.exitCode = rendered.ok ? 0 : 1;
141
+ }
142
+
143
+ /** 输出参数错误与投影的 JSON 口径:显式 --json 或用了 --select 时输出结构化信封。 */
144
+ function jsonArgs(argv) {
145
+ return argv.includes("--json") || argv.includes(SELECT_FLAG) ? ["--json"] : [];
146
+ }
147
+
148
+ /**
149
+ * 渲染最终 stdout:投影(--select,恒 JSON)→ 渲染 → 落盘(--save)/截断(--max-chars)。
150
+ * --save 恒落 JSON 信封(用了 --select 则落投影结果)——人读渲染是有损的,落盘要的是完整数据;
151
+ * --max-chars 只作用于 stdout 文本,且 --save 时被忽略(摘要本身极短)。失败信封不投影——
152
+ * 失败响应 data 恒为 null,投影只会把真实错误盖成 "Unknown path(s)";但照常走落盘/截断链,
153
+ * 与无 --select 的失败路径一致(脚本消费方不会拿到上一轮的陈旧落盘文件)。
154
+ */
155
+ export function renderFinalOutput(envelope, options, argv = []) {
156
+ const jsonArgv = options.select ? ["--json"] : jsonArgs(argv);
157
+ if (options.select && !envelope.ok) {
158
+ return finishOutput(formatCliOutput(envelope, jsonArgv), `${JSON.stringify(envelope, null, 2)}\n`,
159
+ envelope, options, jsonArgv);
160
+ }
161
+ if (options.select) {
162
+ const projected = selectPaths(envelope, options.select);
163
+ if (!projected.ok) {
164
+ const rejected = failure(envelope.command, projected.error);
165
+ const rejectedJson = `${JSON.stringify(rejected, null, 2)}\n`;
166
+ return finishOutput(rejectedJson, rejectedJson, rejected, options, jsonArgv);
167
+ }
168
+ const projectedJson = `${JSON.stringify(projected.value, null, 2)}\n`;
169
+ return finishOutput(projectedJson, projectedJson, envelope, options, jsonArgv);
170
+ }
171
+ return finishOutput(formatCliOutput(envelope, argv), `${JSON.stringify(envelope, null, 2)}\n`, envelope,
172
+ options, jsonArgv);
173
+ }
174
+
175
+ function finishOutput(displayText, fullJson, envelope, options, jsonArgv) {
176
+ if (options.save) {
177
+ let chars;
178
+ try {
179
+ chars = saveOutput(fullJson, options.save);
180
+ } catch (error) {
181
+ return {
182
+ ok: false,
183
+ text: formatCliOutput(failure(envelope.command, saveFailure(options.save, error)), jsonArgv),
184
+ };
185
+ }
186
+ return {ok: envelope.ok, text: savedSummary(envelope, options.save, chars)};
187
+ }
188
+ if (options.maxChars) {
189
+ return {ok: envelope.ok, text: truncateMiddle(displayText, options.maxChars)};
190
+ }
191
+ return {ok: envelope.ok, text: displayText};
192
+ }
193
+
194
+ export function formatCliOutput(envelope, argv = []) {
195
+ if ((isHelpInvocation(argv) || envelope.command === "help") && !argv.includes("--json")) {
196
+ return renderHelpText(envelope);
197
+ }
198
+ if (!argv.includes("--json")) {
199
+ const text = renderHumanOutput(envelope);
200
+ if (text !== undefined) {
201
+ return text;
202
+ }
203
+ }
204
+ return `${JSON.stringify(envelope, null, 2)}\n`;
205
+ }
206
+
207
+ export function isDirectInvocation(modulePath, invokedPath) {
208
+ if (!invokedPath) {
209
+ return false;
210
+ }
211
+ return normalizePath(modulePath) === normalizePath(invokedPath);
212
+ }
213
+
214
+ async function handleConfig(args, context) {
215
+ const action = args[0];
216
+ if (action === "get") {
217
+ return success("config.get", maskConfig(readContextConfig(context)));
218
+ }
219
+ if (action === "set") {
220
+ return setConfigValue(args.slice(1), context);
221
+ }
222
+ if (action === "clear") {
223
+ removeProfile(configPath(context), context.profileName);
224
+ removeFile(capabilityPath(context));
225
+ return success("config.clear", { cleared: true });
226
+ }
227
+ return usageError("config", "Unsupported config command.", ["finebi-cli config get", "finebi-cli config set base-url <baseUrl>"]);
228
+ }
229
+
230
+ function setConfigValue(args, context) {
231
+ const key = args[0];
232
+ const value = args[1];
233
+ if (!key || value === undefined) {
234
+ return usageError("config.set", "Config key and value are required.", ["finebi-cli config set base-url <baseUrl>"]);
235
+ }
236
+ try {
237
+ const current = readContextConfig(context);
238
+ const next = updateConfigValue(current, key, value, nowFn(context));
239
+ writeContextConfig(context, next);
240
+ if (key === "base-url" && current.baseUrl !== next.baseUrl) removeFile(capabilityPath(context));
241
+ return success("config.set", { key, value });
242
+ } catch (error) {
243
+ return failure("config.set", cliError("INVALID_ARGUMENT", {
244
+ path: key,
245
+ message: error instanceof Error ? error.message : String(error),
246
+ hint: "Use finebi-cli config get to inspect supported local settings.",
247
+ retryable: false,
248
+ nextActions: ["finebi-cli config get"],
249
+ }));
250
+ }
251
+ }
252
+
253
+ async function handleAuth(args, context) {
254
+ const action = args[0];
255
+ if (action === "list") {
256
+ const store = readProfiles(configPath(context));
257
+ return success("auth.list", {
258
+ currentProfile: store.currentProfile,
259
+ profiles: Object.entries(store.profiles).map(([name, config]) => ({
260
+ name, isDefault: name === store.currentProfile, baseUrl: config.baseUrl || null,
261
+ username: config.auth?.username || null, authMode: buildAuthInfo(config).authMode,
262
+ expiresAt: config.auth?.expiresAt || null,
263
+ })),
264
+ });
265
+ }
266
+ if (action === "use" || action === "remove") {
267
+ const name = args[1];
268
+ if (!name || args.slice(2).some((arg) => arg !== "--json")) throw new ProfileError(`Usage: finebi-cli auth ${action} <name>`);
269
+ if (action === "use") useProfile(configPath(context), name);
270
+ else {
271
+ selectProfile(configPath(context), name);
272
+ removeProfile(configPath(context), name);
273
+ removeFile(capabilityPath({...context, profileName: name}));
274
+ }
275
+ return success(`auth.${action}`, {profile: name, [action === "use" ? "selected" : "removed"]: true});
276
+ }
277
+ if (action === "login") {
278
+ return login(args.slice(1), context);
279
+ }
280
+ if (action === "info") {
281
+ return success("auth.info", buildAuthInfo(readContextConfig(context)));
282
+ }
283
+ if (action === "logout") {
284
+ return logout(args.slice(1), context);
285
+ }
286
+ return usageError("auth", "Unsupported auth command.", ["finebi-cli auth login --username <username>", "finebi-cli auth info"]);
287
+ }
288
+
289
+ function buildAuthInfo(config) {
290
+ const auth = maskConfig(config).auth;
291
+ const authMode = auth?.accessKey?.value ? "access-key" : auth?.token ? "session-token" : "none";
292
+ return { authMode, auth };
293
+ }
294
+
295
+ async function logout(args, context) {
296
+ const flags = parseFlags(args);
297
+ const config = resolveConfig({}, {}, readContextConfig(context));
298
+ const warnings = [];
299
+ const accessKey = config.auth?.accessKey;
300
+ if (accessKey?.value && config.baseUrl) {
301
+ const revoked = await selfRevokeAccessKey({
302
+ baseUrl: config.baseUrl,
303
+ accessKey: accessKey.value,
304
+ timeoutMs: config.timeoutMs,
305
+ fetchImpl: context.fetchImpl,
306
+ });
307
+ if (!revoked.ok) {
308
+ if (!flags.force) {
309
+ return failure("auth.logout", cliError("ACCESS_KEY_REVOKE_FAILED", {
310
+ path: "auth.accessKey",
311
+ message: "Could not revoke the server-side Access Key; local credentials were preserved for retry.",
312
+ hint: "Retry logout, or use --force to remove local credentials without confirming server revocation.",
313
+ retryable: true,
314
+ nextActions: ["finebi-cli auth logout", "finebi-cli auth logout --force"],
315
+ id: accessKey.id,
316
+ cause: revoked.errors?.[0],
317
+ }));
318
+ }
319
+ warnings.push(cliError("ACCESS_KEY_REVOKE_FAILED", {
320
+ message: "Server revocation failed; local credentials were removed because --force was specified.",
321
+ hint: "Revoke the Access Key manually in FineBI if it is still present.",
322
+ retryable: true,
323
+ nextActions: ["finebi-cli auth login"],
324
+ id: accessKey.id,
325
+ cause: revoked.errors?.[0],
326
+ }));
327
+ }
328
+ }
329
+ const stored = readContextConfig(context);
330
+ delete stored.auth;
331
+ delete stored.token;
332
+ writeContextConfig(context, stored);
333
+ removeFile(capabilityPath(context));
334
+ return success("auth.logout", { loggedOut: true, forced: Boolean(flags.force) }, {}, warnings);
335
+ }
336
+
337
+ async function login(args, context) {
338
+ const flags = {...parseFlags(args), ...context.connectionFlags};
339
+ const stored = readContextConfig(context);
340
+ const target = firstNonEmpty(flags.baseUrl, (context.env || process.env).FINEBI_BASE_URL, stored.baseUrl);
341
+ const config = sameBaseUrl(target, stored.baseUrl) ? stored : {...stored, auth: undefined, token: undefined};
342
+ if (flags.browser) {
343
+ const incompatible = [flags.username && "--username", flags.password && "--password", flags.passwordStdin && "--password-stdin",
344
+ flags.fromEnv && "--from-env", flags.noAccessKey && "--no-access-key", flags.token && "--token"].find(Boolean);
345
+ if (incompatible) {
346
+ return usageError("auth.login", `${incompatible} cannot be used with --browser.`, [
347
+ "finebi-cli auth login --browser [--base-url <baseUrl>]",
348
+ ]);
349
+ }
350
+ return loginWithBrowser(flags, config, context);
351
+ }
352
+ const input = flags.fromEnv ? resolveLoginInputFromEnv(context.env || process.env) : {};
353
+ const baseUrl = target;
354
+ const username = firstNonEmpty(flags.username, input.username, config.auth?.username);
355
+ if (flags.passwordStdin && isStdinTty(context)) {
356
+ return usageError("auth.login", "The --password-stdin option requires pipe input.", [
357
+ "echo <password> | finebi-cli auth login --username <username> --password-stdin",
358
+ "finebi-cli auth login --username <username>",
359
+ ]);
360
+ }
361
+ let password;
362
+ try {
363
+ password = await resolvePassword(flags, input, context);
364
+ } catch (error) {
365
+ return failure("auth.login", cliError("PASSWORD_INPUT_FAILED", {
366
+ path: "password",
367
+ message: error instanceof Error ? error.message : String(error),
368
+ hint: "Use --from-env or --password-stdin when interactive input is unavailable.",
369
+ retryable: false,
370
+ nextActions: ["finebi-cli auth login --from-env", "echo <password> | finebi-cli auth login --username <username> --password-stdin"],
371
+ }));
372
+ }
373
+ if (!baseUrl || !username || !password) {
374
+ return usageError("auth.login", "base-url, username, and password are required.", [
375
+ "finebi-cli auth login --from-env",
376
+ "finebi-cli auth login --username <username>",
377
+ "finebi-cli auth login --username <username> --password-stdin",
378
+ ]);
379
+ }
380
+ const envelope = await loginWithPassword({
381
+ baseUrl,
382
+ username,
383
+ password,
384
+ timeoutMs: resolveConfig({}, context.env || process.env, config).timeoutMs,
385
+ now: nowFn(context),
386
+ fetchImpl: context.fetchImpl,
387
+ });
388
+ if (envelope.ok) {
389
+ const nextConfig = {
390
+ ...config,
391
+ baseUrl,
392
+ auth: envelope.data.auth,
393
+ };
394
+ writeContextConfig(context, nextConfig, {makeCurrent: !flags.noUse, forceMulti: flags.noUse});
395
+ removeFile(capabilityPath(context));
396
+ const capabilityRefresh = await refreshCapabilityAfterLogin(nextConfig, context);
397
+ envelope.data.capabilityRefresh = capabilityRefresh.data;
398
+ if (!capabilityRefresh.ok) {
399
+ envelope.warnings.push(cliError("CAPABILITY_AUTO_REFRESH_FAILED", {
400
+ message: "Capability cache was not refreshed after login.",
401
+ nextActions: ["finebi-cli capability refresh"],
402
+ cause: capabilityRefresh.error,
403
+ }));
404
+ }
405
+ if (!flags.noAccessKey) {
406
+ const provision = await provisionAccessKeyAfterLogin(context);
407
+ envelope.data.accessKey = provision.data;
408
+ if (provision.warnings.length) {
409
+ envelope.warnings.push(...provision.warnings);
410
+ }
411
+ }
412
+ }
413
+ return envelope;
414
+ }
415
+
416
+ async function loginWithBrowser(flags, config, context) {
417
+ const baseUrl = firstNonEmpty(flags.baseUrl, (context.env || process.env).FINEBI_BASE_URL, config.baseUrl);
418
+ if (!baseUrl) {
419
+ return failure("auth.login", cliError("CONFIG_MISSING", {
420
+ path: "baseUrl",
421
+ message: "FineBI base-url is not configured.",
422
+ nextActions: ["finebi-cli config set base-url <baseUrl>"],
423
+ }));
424
+ }
425
+ const authorization = await authorizeWithBrowser({
426
+ baseUrl,
427
+ description: accessKeyDescription(context),
428
+ timeoutMs: resolveConfig({}, context.env || process.env, config).timeoutMs,
429
+ fetchImpl: context.fetchImpl,
430
+ interactive: isStdinTty(context),
431
+ openBrowser: context.openBrowser,
432
+ sleep: context.sleep,
433
+ nowMs: context.nowMs,
434
+ signal: context.signal,
435
+ writeStderr: context.writeStderr,
436
+ onApproved: async (key) => {
437
+ const nextConfig = {
438
+ ...config,
439
+ baseUrl,
440
+ auth: {
441
+ username: key.username,
442
+ accessKey: {
443
+ id: key.id,
444
+ value: key.token,
445
+ description: key.description ?? accessKeyDescription(context),
446
+ createdAt: now(context),
447
+ },
448
+ },
449
+ };
450
+ writeContextConfig(context, nextConfig, {
451
+ makeCurrent: !flags.noUse, forceMulti: flags.noUse, writer: context.writeConfigAtomically,
452
+ });
453
+ removeFile(capabilityPath(context));
454
+ },
455
+ });
456
+ if (!authorization.ok) {
457
+ return authorization;
458
+ }
459
+ const stored = readContextConfig(context);
460
+ const capabilityRefresh = await refreshCapabilityAfterLogin(stored, context);
461
+ const envelope = success("auth.login", {
462
+ auth: {username: stored.auth.username},
463
+ browserAuthorization: {id: authorization.data.id, description: authorization.data.description},
464
+ capabilityRefresh: capabilityRefresh.data,
465
+ });
466
+ if (!capabilityRefresh.ok) {
467
+ envelope.warnings.push(cliError("CAPABILITY_AUTO_REFRESH_FAILED", {
468
+ message: "Capability cache was not refreshed after browser authorization.",
469
+ nextActions: ["finebi-cli capability refresh"],
470
+ cause: capabilityRefresh.error,
471
+ }));
472
+ }
473
+ return envelope;
474
+ }
475
+
476
+ async function provisionAccessKeyAfterLogin(context) {
477
+ const config = resolveConfig({}, {}, readContextConfig(context));
478
+ const description = accessKeyDescription(context);
479
+ const { created, warnings } = await provisionAccessKey({
480
+ baseUrl: config.baseUrl,
481
+ token: config.auth.token,
482
+ username: config.auth.username,
483
+ description,
484
+ timeoutMs: config.timeoutMs,
485
+ fetchImpl: context.fetchImpl,
486
+ });
487
+ if (!created.ok) {
488
+ return {
489
+ data: { issued: false, description },
490
+ warnings: [
491
+ ...warnings,
492
+ cliError("ACCESS_KEY_ISSUE_FAILED", {
493
+ message: "Access Key was not issued; commands will use the session token until it expires.",
494
+ nextActions: ["finebi-cli auth login"],
495
+ cause: created.errors?.[0],
496
+ }),
497
+ ],
498
+ };
499
+ }
500
+ const key = created.data;
501
+ const stored = readContextConfig(context);
502
+ stored.auth = {
503
+ ...stored.auth,
504
+ accessKey: {
505
+ id: key.id,
506
+ value: key.token,
507
+ description: key.description ?? description,
508
+ createdAt: now(context),
509
+ },
510
+ };
511
+ writeContextConfig(context, stored);
512
+ return {
513
+ data: { issued: true, id: key.id, description: key.description ?? description },
514
+ warnings,
515
+ };
516
+ }
517
+
518
+ function accessKeyDescription(context) {
519
+ const host = context.hostname || safeHostname();
520
+ const suffix = context.profileName && context.profileName !== "default" ? `/${context.profileName}` : "";
521
+ return `finebi-cli@${host}${suffix}`;
522
+ }
523
+
524
+ function safeHostname() {
525
+ try {
526
+ return hostname() || "unknown-host";
527
+ } catch (_error) {
528
+ return "unknown-host";
529
+ }
530
+ }
531
+
532
+ async function refreshCapabilityAfterLogin(config, context) {
533
+ const envelope = await fetchCapabilities({
534
+ baseUrl: config.baseUrl,
535
+ token: config.auth.token,
536
+ accessKey: config.auth.accessKey?.value,
537
+ timeoutMs: resolveConfig({}, context.env || process.env, config).timeoutMs,
538
+ fetchImpl: context.fetchImpl,
539
+ });
540
+ if (!envelope.ok) {
541
+ return {
542
+ ok: false,
543
+ data: {
544
+ ok: false,
545
+ refreshed: false,
546
+ },
547
+ error: envelope.errors?.[0],
548
+ };
549
+ }
550
+ if (!Array.isArray(envelope.data?.resources)) {
551
+ return {
552
+ ok: false,
553
+ data: {
554
+ ok: false,
555
+ refreshed: false,
556
+ },
557
+ error: cliError("CAPABILITY_MANIFEST_INVALID", {
558
+ message: "Capability response does not contain resources.",
559
+ }),
560
+ };
561
+ }
562
+ const record = cacheCapability(capabilityPath(context), envelope.data, {
563
+ baseUrl: config.baseUrl,
564
+ username: config.auth.username,
565
+ fetchedAt: now(context),
566
+ ttlMs: config.capabilityTtlMs,
567
+ });
568
+ return {
569
+ ok: true,
570
+ data: {
571
+ ok: true,
572
+ refreshed: true,
573
+ resources: envelope.data.resources.length,
574
+ commands: envelope.data.resources.reduce((sum, resource) => sum + (resource.commands || []).length, 0),
575
+ fetchedAt: record.fetchedAt,
576
+ expiresAt: record.expiresAt,
577
+ },
578
+ };
579
+ }
580
+
581
+ async function resolvePassword(flags, input, context) {
582
+ if (flags.fromEnv) {
583
+ return input.password;
584
+ }
585
+ if (flags.passwordStdin) {
586
+ return (await (context.readStdin ? context.readStdin() : readStdin())).trim();
587
+ }
588
+ if (context.promptPassword) {
589
+ return context.promptPassword();
590
+ }
591
+ if (isStdinTty(context)) {
592
+ return promptHiddenPassword("Password: ");
593
+ }
594
+ return undefined;
595
+ }
596
+
597
+ async function handleCapability(args, context) {
598
+ const action = args[0];
599
+ if (action === "refresh") {
600
+ return refreshCapability(context);
601
+ }
602
+ if (action === "info") {
603
+ const cache = readContextCache(context);
604
+ return success("capability.info", { cache: cache.record || null, hit: cache.hit, stale: cache.stale });
605
+ }
606
+ if (action === "clear-cache") {
607
+ removeFile(capabilityPath(context));
608
+ return success("capability.clear-cache", { cleared: true });
609
+ }
610
+ return usageError("capability", "Unsupported capability command.", ["finebi-cli capability refresh", "finebi-cli capability info"]);
611
+ }
612
+
613
+ async function refreshCapability(context) {
614
+ return fetchAndCacheCapabilities("capability.refresh", context);
615
+ }
616
+
617
+ async function fetchAndCacheCapabilities(command, context) {
618
+ const config = resolveContextConfig(context);
619
+ const authError = requireConfiguredAuth(config, command, context);
620
+ if (authError) {
621
+ return authError;
622
+ }
623
+ const envelope = await fetchCapabilities({
624
+ baseUrl: config.baseUrl,
625
+ token: config.auth.token,
626
+ accessKey: config.auth.accessKey?.value,
627
+ timeoutMs: config.timeoutMs,
628
+ fetchImpl: context.fetchImpl,
629
+ });
630
+ if (envelope.ok && !hasTransientConnection(context)) {
631
+ cacheCapability(capabilityPath(context), envelope.data, {
632
+ baseUrl: config.baseUrl,
633
+ username: config.auth.username,
634
+ fetchedAt: now(context),
635
+ ttlMs: config.capabilityTtlMs,
636
+ });
637
+ }
638
+ return command === envelope.command ? envelope : { ...envelope, command };
639
+ }
640
+
641
+ function handleVersion(context) {
642
+ const metadata = readPackageMetadata(context);
643
+ return success("version", {
644
+ cli: {
645
+ name: metadata.name,
646
+ version: metadata.version,
647
+ },
648
+ });
649
+ }
650
+
651
+ async function handleHelp(args, context) {
652
+ args = args.filter((arg) => arg !== "--json");
653
+ if (args.length === 0) {
654
+ const cache = readContextCache(context);
655
+ return renderTopLevelHelp(cache.manifest);
656
+ }
657
+ if (args.length === 1 && isBuiltInResource(args[0])) {
658
+ return renderBuiltInHelp(args[0]);
659
+ }
660
+ if (args.length >= 2 && isBuiltInResource(args[0])) {
661
+ return renderBuiltInCommandHelp(args[0], args[1]);
662
+ }
663
+ const cache = readContextCache(context);
664
+ const resolved = resolveCapability(cache, "help");
665
+ if (resolved.error) {
666
+ return failure("help", resolved.error);
667
+ }
668
+ if (args.length >= 2) {
669
+ return renderCommandHelp(resolved.manifest, args[0], args[1]);
670
+ }
671
+ if (args.length === 1) {
672
+ return renderResourceHelp(resolved.manifest, args[0]);
673
+ }
674
+ return renderTopLevelHelp(resolved.manifest);
675
+ }
676
+
677
+ function isBuiltInResource(resourceName) {
678
+ return resourceName === "install-skill"
679
+ || resourceName === "config"
680
+ || resourceName === "auth"
681
+ || resourceName === "capability"
682
+ || resourceName === "version"
683
+ || resourceName === "capabilities";
684
+ }
685
+
686
+ function isHelpInvocation(argv) {
687
+ return argv.length === 0 || argv.some((arg) => isHelpFlag(arg));
688
+ }
689
+
690
+ function isHelpFlagInvocation(args) {
691
+ return args.some((arg) => isHelpFlag(arg));
692
+ }
693
+
694
+ function isHelpFlag(arg) {
695
+ return arg === "--help" || arg === "-h";
696
+ }
697
+
698
+ async function handleDynamic(resourceName, args, context) {
699
+ const commandName = args[0];
700
+ if (!commandName) {
701
+ return handleHelp([resourceName], context);
702
+ }
703
+ const config = resolveContextConfig(context);
704
+ const authError = requireConfiguredAuth(config, `${resourceName}.${commandName}`, context);
705
+ if (authError) {
706
+ return authError;
707
+ }
708
+ const cache = hasTransientConnection(context)
709
+ ? await fetchTransientCapability(config, context)
710
+ : readContextCache(context);
711
+ if (cache.error) return failure(`${resourceName}.${commandName}`, cache.error);
712
+ const resolved = resolveCapability(cache, "command");
713
+ if (resolved.error) {
714
+ return failure(`${resourceName}.${commandName}`, resolved.error);
715
+ }
716
+ return executeDynamicCommand(resolved.manifest, resourceName, commandName, args.slice(1), {
717
+ baseUrl: config.baseUrl,
718
+ token: config.auth.token,
719
+ accessKey: config.auth.accessKey?.value,
720
+ timeoutMs: config.timeoutMs,
721
+ fetchImpl: context.fetchImpl,
722
+ });
723
+ }
724
+
725
+ function requireConfiguredAuth(config, command, context) {
726
+ if (!config.baseUrl) {
727
+ return failure(command, cliError("CONFIG_MISSING", {
728
+ path: "baseUrl",
729
+ message: "FineBI base-url is not configured.",
730
+ hint: "Set the FineBI decision root URL.",
731
+ retryable: false,
732
+ nextActions: ["finebi-cli config set base-url <baseUrl>"],
733
+ }));
734
+ }
735
+ if (config.auth?.accessKey?.value) {
736
+ return null;
737
+ }
738
+ if (!config.auth?.token) {
739
+ return failure(command, cliError("AUTH_REQUIRED", {
740
+ path: "auth.token",
741
+ message: "Authentication is required.",
742
+ hint: "Login with a FineBI username and password.",
743
+ retryable: false,
744
+ nextActions: ["finebi-cli auth login --username <username>"],
745
+ }));
746
+ }
747
+ if (isAuthExpired(config.auth.expiresAt, now(context))) {
748
+ return failure(command, cliError("AUTH_EXPIRED", {
749
+ path: "auth.expiresAt",
750
+ message: "Authentication has expired or is about to expire.",
751
+ hint: "Login again before running this command.",
752
+ retryable: false,
753
+ nextActions: ["finebi-cli auth login --username <username>"],
754
+ }));
755
+ }
756
+ return null;
757
+ }
758
+
759
+ function parseFlags(args) {
760
+ const flags = {};
761
+ for (let index = 0; index < args.length; index += 1) {
762
+ const item = args[index];
763
+ if (item === "--from-env") {
764
+ flags.fromEnv = true;
765
+ } else if (item === "--no-use") {
766
+ flags.noUse = true;
767
+ } else if (item === "--password-stdin") {
768
+ flags.passwordStdin = true;
769
+ } else if (item === "--no-access-key") {
770
+ flags.noAccessKey = true;
771
+ } else if (item === "--browser") {
772
+ flags.browser = true;
773
+ } else if (item === "--password") {
774
+ flags.password = args[index + 1];
775
+ index += 1;
776
+ } else if (item === "--force") {
777
+ flags.force = true;
778
+ } else if (item === "--username") {
779
+ flags.username = args[index + 1];
780
+ index += 1;
781
+ } else if (item === "--base-url") {
782
+ flags.baseUrl = args[index + 1];
783
+ index += 1;
784
+ } else if (item === "--token") {
785
+ flags.token = args[index + 1];
786
+ index += 1;
787
+ }
788
+ }
789
+ return flags;
790
+ }
791
+
792
+ function usageError(command, message, nextActions) {
793
+ return failure(command, cliError("INVALID_ARGUMENT", {
794
+ path: "",
795
+ message,
796
+ hint: "Run finebi-cli --help for supported commands.",
797
+ retryable: false,
798
+ nextActions,
799
+ }));
800
+ }
801
+
802
+ function isAuthExpired(expiresAt, currentTime) {
803
+ if (!expiresAt) {
804
+ return false;
805
+ }
806
+ return Date.parse(expiresAt) - Date.parse(currentTime) <= AUTH_EXPIRE_SKEW_MS;
807
+ }
808
+
809
+ function now(context) {
810
+ return nowFn(context)();
811
+ }
812
+
813
+ function nowFn(context) {
814
+ return context.now || (() => new Date().toISOString());
815
+ }
816
+
817
+ function configPath(context) {
818
+ return context.configPath || defaultConfigPath(context.env || process.env);
819
+ }
820
+
821
+ function capabilityPath(context) {
822
+ return profileCapabilityPath(context.capabilityPath || capabilityCachePath(context.env || process.env), context.profileName);
823
+ }
824
+
825
+ function readContextConfig(context) {
826
+ return readProfile(configPath(context), context.profileName);
827
+ }
828
+
829
+ function writeContextConfig(context, config, options) {
830
+ saveProfile(configPath(context), context.profileName, config, options);
831
+ }
832
+
833
+ function resolveContextConfig(context) {
834
+ return resolveConfig(context.connectionFlags, context.env || process.env, readContextConfig(context));
835
+ }
836
+
837
+ function hasTransientConnection(context) {
838
+ const stored = readContextConfig(context);
839
+ const env = context.env || process.env;
840
+ return !sameBaseUrl(resolveContextConfig(context).baseUrl, stored.baseUrl)
841
+ || Boolean(context.connectionFlags?.token || env.FINEBI_AUTH_TOKEN);
842
+ }
843
+
844
+ function readContextCache(context) {
845
+ if (hasTransientConnection(context)) return {hit: false, stale: false};
846
+ const cache = readCapabilityCache(capabilityPath(context), now(context));
847
+ const stored = readContextConfig(context);
848
+ if (cache.record && (!sameBaseUrl(cache.record.baseUrl, stored.baseUrl) || cache.record.username !== stored.auth?.username)) {
849
+ return {hit: false, stale: false};
850
+ }
851
+ return cache;
852
+ }
853
+
854
+ async function fetchTransientCapability(config, context) {
855
+ const result = await fetchCapabilities({
856
+ baseUrl: config.baseUrl, token: config.auth?.token, accessKey: config.auth?.accessKey?.value,
857
+ timeoutMs: config.timeoutMs, fetchImpl: context.fetchImpl,
858
+ });
859
+ return result.ok ? {hit: true, manifest: result.data} : {error: result.errors[0]};
860
+ }
861
+
862
+ function removeFile(path) {
863
+ if (path && existsSync(path)) {
864
+ rmSync(path, { force: true });
865
+ }
866
+ }
867
+
868
+ function renderHumanOutput(envelope) {
869
+ if (!envelope.ok) {
870
+ return renderFailure(envelope);
871
+ }
872
+ switch (envelope.command) {
873
+ case "version":
874
+ return `${envelope.data?.cli?.version || ""}\n`;
875
+ case "config.get":
876
+ return `profile: ${envelope.meta?.profile || "(none)"}\n${renderConfigGet(envelope.data || {})}`;
877
+ case "auth.info":
878
+ return `profile: ${envelope.meta?.profile || "(none)"}\n${renderAuthInfo(envelope.data || {})}`;
879
+ case "auth.list":
880
+ return `${(envelope.data.profiles || []).map((profile) =>
881
+ `${profile.isDefault ? "*" : " "} ${profile.name} ${profile.baseUrl || "(not set)"} ${profile.username || "(unknown)"} ${profile.authMode}`,
882
+ ).join("\n") || "No environments configured."}\n`;
883
+ case "auth.use":
884
+ return `Default environment: ${envelope.data.profile}\n`;
885
+ case "auth.remove":
886
+ return `Removed local environment: ${envelope.data.profile}\n`;
887
+ case "capability.info":
888
+ return renderCapabilityInfo(envelope.data || {});
889
+ case "auth.logout":
890
+ return renderLocalAction("Logged out.", envelope);
891
+ case "auth.login":
892
+ return renderAuthLogin(envelope);
893
+ case "config.clear":
894
+ return "Local config and capability cache cleared.\n";
895
+ case "capability.clear-cache":
896
+ return "Capability cache cleared.\n";
897
+ default:
898
+ if (isDynamicBusinessCommand(envelope.command)) {
899
+ return renderDynamicSuccess(envelope);
900
+ }
901
+ return undefined;
902
+ }
903
+ }
904
+
905
+ function renderAuthLogin(envelope) {
906
+ const auth = envelope.data?.auth || {};
907
+ const refresh = envelope.data?.capabilityRefresh || {};
908
+ const lines = [`Logged in${auth.username ? ` as ${auth.username}` : ""}.`];
909
+ if (refresh.refreshed) {
910
+ lines.push(`Capability cache refreshed: ${refresh.resources} resources, ${refresh.commands} commands.`);
911
+ } else if ((envelope.warnings || []).length > 0) {
912
+ lines.push(`Capability cache was not refreshed. Run ${commandLine("capability", "refresh")} after checking the server.`);
913
+ }
914
+ lines.push(...renderWarningLines(envelope.warnings));
915
+ return `${lines.join("\n")}\n`;
916
+ }
917
+
918
+ function renderLocalAction(message, envelope) {
919
+ return `${[message, ...renderWarningLines(envelope.warnings)].join("\n")}\n`;
920
+ }
921
+
922
+ function renderWarningLines(warnings = []) {
923
+ return warnings.map((warning) => {
924
+ const code = warning.code ? ` [${warning.code}]` : "";
925
+ return `Warning${code}: ${warning.message || "Command completed with a warning."}`;
926
+ });
927
+ }
928
+
929
+ function hasWarning(envelope, code) {
930
+ return (envelope.warnings || []).some((warning) => warning.code === code);
931
+ }
932
+
933
+ function renderFailure(envelope) {
934
+ const [error] = envelope.errors || [];
935
+ if (!error) {
936
+ return `Error: ${envelope.command || "Command failed"}\n`;
937
+ }
938
+ const lines = [`Error: ${error.message || "Command failed."}`];
939
+ if (error.code) {
940
+ lines.push(`Code: ${error.code}`);
941
+ }
942
+ if (error.path) {
943
+ lines.push(`Path: ${error.path}`);
944
+ }
945
+ if (error.hint) {
946
+ lines.push(`Hint: ${error.hint}`);
947
+ }
948
+ if (typeof error.retryable === "boolean") {
949
+ lines.push(`Retryable: ${error.retryable}`);
950
+ }
951
+ if (Array.isArray(error.nextActions) && error.nextActions.length > 0) {
952
+ lines.push("Next actions:");
953
+ for (const action of error.nextActions) {
954
+ lines.push(` - ${action}`);
955
+ }
956
+ }
957
+ if (error.partialResult && typeof error.partialResult === "object") {
958
+ lines.push("Partial result:");
959
+ lines.push(...renderValue(error.partialResult, 1));
960
+ }
961
+ return `${lines.join("\n")}\n`;
962
+ }
963
+
964
+ function renderDynamicSuccess(envelope) {
965
+ const businessEnvelope = withoutBusinessLinks(envelope);
966
+ const displayOutput = renderDynamicDisplay(businessEnvelope);
967
+ if (displayOutput !== undefined) {
968
+ return appendResolvedLinks(displayOutput, envelope);
969
+ }
970
+ const lines = [envelope.command];
971
+ if (businessEnvelope.data !== undefined && businessEnvelope.data !== null) {
972
+ lines.push(...renderValue(businessEnvelope.data, 0));
973
+ }
974
+ if (Array.isArray(envelope.warnings) && envelope.warnings.length > 0) {
975
+ lines.push("warnings:");
976
+ for (const warning of envelope.warnings) {
977
+ lines.push(` - ${stringifyScalar(warning)}`);
978
+ }
979
+ }
980
+ return appendResolvedLinks(`${lines.join("\n")}\n`, envelope);
981
+ }
982
+
983
+ function withoutBusinessLinks(envelope) {
984
+ if (!envelope.data || typeof envelope.data !== "object" || Array.isArray(envelope.data)) {
985
+ return envelope;
986
+ }
987
+ return {...envelope, data: withoutKeys(envelope.data, ["links"])};
988
+ }
989
+
990
+ function appendResolvedLinks(output, envelope) {
991
+ const links = envelope.resolved?.links;
992
+ if (!links || typeof links !== "object" || Array.isArray(links)) {
993
+ return output;
994
+ }
995
+ const lines = Object.entries(links).flatMap(([relation, link]) => {
996
+ return typeof link?.url === "string" && link.url ? [`${relation}-url: ${link.url}`] : [];
997
+ });
998
+ return lines.length > 0 ? `${output.trimEnd()}\n${lines.join("\n")}\n` : output;
999
+ }
1000
+
1001
+ function renderDynamicDisplay(envelope) {
1002
+ const display = envelope.resolved?.display || {};
1003
+ const shape = display.shape || "generic";
1004
+ const format = display.format === "auto" || !display.format
1005
+ ? display.defaultFormat || "expanded"
1006
+ : display.format;
1007
+ if (format === "csv") {
1008
+ if (shape === "tabular-preview") {
1009
+ return renderTabularPreviewCsv(envelope.data || {});
1010
+ }
1011
+ return undefined;
1012
+ }
1013
+ if (format === "table") {
1014
+ if (shape === "record-list") {
1015
+ return renderRecordListTable(envelope.data || {}, display);
1016
+ }
1017
+ if (shape === "tabular-preview") {
1018
+ return renderTabularPreviewTable(envelope.data || {}, display);
1019
+ }
1020
+ }
1021
+ if (format === "expanded" && shape === "detail") {
1022
+ return `${renderValue(envelope.data || {}, 0, { omitNullFields: display.omitNullFields === true }).join("\n")}\n`;
1023
+ }
1024
+ return undefined;
1025
+ }
1026
+
1027
+ function renderRecordListTable(data, display) {
1028
+ const items = Array.isArray(data.items) ? data.items : null;
1029
+ if (!items) {
1030
+ return undefined;
1031
+ }
1032
+ const columns = recordListColumns(items);
1033
+ const rows = items.map((item) => columns.map((column) => item?.[column]));
1034
+ const lines = columns.length > 0 ? renderAlignedTable(columns, rows) : ["(no items)"];
1035
+ const rest = withoutKeys(data, ["items"]);
1036
+ if (Object.keys(rest).length > 0) {
1037
+ lines.push("---");
1038
+ lines.push(...renderValue(rest, 0));
1039
+ }
1040
+ return `${lines.join("\n")}\n`;
1041
+ }
1042
+
1043
+ function recordListColumns(items) {
1044
+ const seen = new Set();
1045
+ for (const item of items) {
1046
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
1047
+ continue;
1048
+ }
1049
+ for (const key of Object.keys(item)) {
1050
+ seen.add(key);
1051
+ }
1052
+ }
1053
+ return [...seen];
1054
+ }
1055
+
1056
+ function renderTabularPreviewTable(data) {
1057
+ const fields = Array.isArray(data.fields) ? data.fields : null;
1058
+ const rows = Array.isArray(data.rows) ? data.rows : null;
1059
+ if (!fields || !rows) {
1060
+ return undefined;
1061
+ }
1062
+ const headers = previewHeaders(fields);
1063
+ const lines = renderAlignedTable(headers, rows);
1064
+ const rest = withoutKeys(data, ["fields", "rows"]);
1065
+ if (Object.keys(rest).length > 0) {
1066
+ lines.push("---");
1067
+ lines.push(...renderValue(rest, 0));
1068
+ }
1069
+ return `${lines.join("\n")}\n`;
1070
+ }
1071
+
1072
+ function renderTabularPreviewCsv(data) {
1073
+ const fields = Array.isArray(data.fields) ? data.fields : null;
1074
+ const rows = Array.isArray(data.rows) ? data.rows : null;
1075
+ if (!fields || !rows) {
1076
+ return undefined;
1077
+ }
1078
+ const lines = [previewHeaders(fields), ...rows].map((row) => row.map(csvCell).join(","));
1079
+ return `${lines.join("\n")}\n`;
1080
+ }
1081
+
1082
+ function previewHeaders(fields) {
1083
+ return fields.map((field) => field.displayName || field.transferName || field.name || field.id || "");
1084
+ }
1085
+
1086
+ function csvCell(value) {
1087
+ const text = stringifyScalar(value);
1088
+ if (/[",\r\n]/.test(text)) {
1089
+ return `"${text.replace(/"/g, "\"\"")}"`;
1090
+ }
1091
+ return text;
1092
+ }
1093
+
1094
+ function renderAlignedTable(headers, rows) {
1095
+ const values = [headers, ...rows].map((row) => row.map((value) => stringifyScalar(value)));
1096
+ const widths = headers.map((_, columnIndex) => Math.max(...values.map((row) => displayWidth(row[columnIndex] || ""))));
1097
+ return values.map((row) => row.map((value, columnIndex) => padEndDisplay(value, widths[columnIndex])).join(" ").trimEnd());
1098
+ }
1099
+
1100
+ function padEndDisplay(value, width) {
1101
+ return value + " ".repeat(Math.max(0, width - displayWidth(value)));
1102
+ }
1103
+
1104
+ function displayWidth(value) {
1105
+ return [...value].reduce((sum, char) => sum + (char.charCodeAt(0) > 127 ? 2 : 1), 0);
1106
+ }
1107
+
1108
+ function withoutKeys(value, keys) {
1109
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1110
+ return {};
1111
+ }
1112
+ const ignored = new Set(keys);
1113
+ return Object.fromEntries(Object.entries(value).filter(([key]) => !ignored.has(key)));
1114
+ }
1115
+
1116
+ function renderValue(value, indent, options = {}) {
1117
+ if (Array.isArray(value)) {
1118
+ if (value.length === 0) {
1119
+ return [`${spaces(indent)}[]`];
1120
+ }
1121
+ return value.flatMap((item) => renderArrayItem(item, indent, options));
1122
+ }
1123
+ if (value && typeof value === "object") {
1124
+ const entries = Object.entries(value).filter(([, item]) => {
1125
+ return !(options.omitNullFields === true && (item === null || item === undefined));
1126
+ });
1127
+ if (entries.length === 0) {
1128
+ return [`${spaces(indent)}{}`];
1129
+ }
1130
+ return entries.flatMap(([key, item]) => {
1131
+ if (isScalar(item)) {
1132
+ return [`${spaces(indent)}${key}: ${stringifyScalar(item)}`];
1133
+ }
1134
+ if (Array.isArray(item) && item.length === 0) {
1135
+ return [`${spaces(indent)}${key}: []`];
1136
+ }
1137
+ if (item && typeof item === "object" && Object.keys(item).length === 0) {
1138
+ return [`${spaces(indent)}${key}: {}`];
1139
+ }
1140
+ return [`${spaces(indent)}${key}:`, ...renderValue(item, indent + 2, options)];
1141
+ });
1142
+ }
1143
+ return [`${spaces(indent)}${stringifyScalar(value)}`];
1144
+ }
1145
+
1146
+ function renderArrayItem(item, indent, options = {}) {
1147
+ if (isScalar(item)) {
1148
+ return [`${spaces(indent)}- ${stringifyScalar(item)}`];
1149
+ }
1150
+ const rendered = renderValue(item, indent + 2, options);
1151
+ if (rendered.length === 0) {
1152
+ return [`${spaces(indent)}-`];
1153
+ }
1154
+ return [`${spaces(indent)}- ${rendered[0].trimStart()}`, ...rendered.slice(1)];
1155
+ }
1156
+
1157
+ function isDynamicBusinessCommand(command) {
1158
+ return typeof command === "string" && command.includes(".");
1159
+ }
1160
+
1161
+ function isScalar(value) {
1162
+ return value === null || value === undefined || typeof value !== "object";
1163
+ }
1164
+
1165
+ function stringifyScalar(value) {
1166
+ if (value === null) {
1167
+ return "null";
1168
+ }
1169
+ if (value === undefined) {
1170
+ return "";
1171
+ }
1172
+ if (typeof value === "string") {
1173
+ return value;
1174
+ }
1175
+ if (typeof value === "object") {
1176
+ return JSON.stringify(value);
1177
+ }
1178
+ return String(value);
1179
+ }
1180
+
1181
+ function spaces(count) {
1182
+ return " ".repeat(count);
1183
+ }
1184
+
1185
+ function renderConfigGet(config) {
1186
+ const lines = [
1187
+ `base-url: ${config.baseUrl || "(not set)"}`,
1188
+ `timeout-ms: ${config.timeoutMs}`,
1189
+ `capability-ttl-ms: ${config.capabilityTtlMs}`,
1190
+ ];
1191
+ if (config.auth) {
1192
+ lines.push("auth:");
1193
+ lines.push(` username: ${config.auth.username || "(unknown)"}`);
1194
+ lines.push(` token: ${config.auth.token || "(not set)"}`);
1195
+ if (config.auth.loginAt) {
1196
+ lines.push(` login-at: ${config.auth.loginAt}`);
1197
+ }
1198
+ if (config.auth.expiresAt) {
1199
+ lines.push(` expires-at: ${config.auth.expiresAt}`);
1200
+ }
1201
+ if (config.auth.source) {
1202
+ lines.push(` source: ${config.auth.source}`);
1203
+ }
1204
+ } else {
1205
+ lines.push("auth: (not logged in)");
1206
+ }
1207
+ return `${lines.join("\n")}\n`;
1208
+ }
1209
+
1210
+ function renderAuthInfo(data) {
1211
+ const auth = data.auth;
1212
+ if (!auth) {
1213
+ return "Not logged in.\n";
1214
+ }
1215
+ const lines = [
1216
+ `auth-mode: ${data.authMode || "none"}`,
1217
+ `username: ${auth.username || "(unknown)"}`,
1218
+ `token: ${auth.token || "(not set)"}`,
1219
+ ];
1220
+ if (auth.accessKey?.value) {
1221
+ lines.push(`access-key: ${auth.accessKey.value}`);
1222
+ }
1223
+ if (auth.accessKey?.createdAt) {
1224
+ lines.push(`access-key-created-at: ${auth.accessKey.createdAt}`);
1225
+ }
1226
+ if (auth.loginAt) {
1227
+ lines.push(`login-at: ${auth.loginAt}`);
1228
+ }
1229
+ if (auth.expiresAt) {
1230
+ lines.push(`expires-at: ${auth.expiresAt}`);
1231
+ }
1232
+ if (auth.source) {
1233
+ lines.push(`source: ${auth.source}`);
1234
+ }
1235
+ return `${lines.join("\n")}\n`;
1236
+ }
1237
+
1238
+ function renderCapabilityInfo(data) {
1239
+ const lines = [
1240
+ `cache: ${data.hit ? "hit" : "missing"}`,
1241
+ `stale: ${Boolean(data.stale)}`,
1242
+ ];
1243
+ const record = data.cache;
1244
+ if (record) {
1245
+ lines.push(`base-url: ${record.baseUrl || "(unknown)"}`);
1246
+ lines.push(`username: ${record.username || "(unknown)"}`);
1247
+ lines.push(`fetched-at: ${record.fetchedAt || "(unknown)"}`);
1248
+ lines.push(`expires-at: ${record.expiresAt || "(unknown)"}`);
1249
+ lines.push(`ttl-ms: ${record.ttlMs}`);
1250
+ lines.push(`schema-version: ${record.manifest?.schemaVersion || "(unknown)"}`);
1251
+ const resources = record.manifest?.resources || [];
1252
+ lines.push(`resources: ${resources.length}`);
1253
+ lines.push(`commands: ${resources.reduce((sum, resource) => sum + (resource.commands || []).length, 0)}`);
1254
+ } else {
1255
+ lines.push(`hint: ${brandCommandText("Run finebi-cli capability refresh.")}`);
1256
+ }
1257
+ return `${lines.join("\n")}\n`;
1258
+ }
1259
+
1260
+ function readPackageMetadata(context) {
1261
+ if (context.packageMetadata) {
1262
+ return context.packageMetadata;
1263
+ }
1264
+ const packageJsonPath = join(dirname(currentFile), "..", "package.json");
1265
+ return JSON.parse(readFileSync(packageJsonPath, "utf8"));
1266
+ }
1267
+
1268
+ function firstNonEmpty(...values) {
1269
+ return values.find((value) => typeof value === "string" && value.trim().length > 0)?.trim();
1270
+ }
1271
+
1272
+ function readStdin() {
1273
+ return new Promise((resolve, reject) => {
1274
+ let text = "";
1275
+ process.stdin.setEncoding("utf8");
1276
+ process.stdin.on("data", (chunk) => {
1277
+ text += chunk;
1278
+ });
1279
+ process.stdin.on("end", () => resolve(text));
1280
+ process.stdin.on("error", reject);
1281
+ });
1282
+ }
1283
+
1284
+ function isStdinTty(context) {
1285
+ if (typeof context.stdinIsTTY === "boolean") {
1286
+ return context.stdinIsTTY;
1287
+ }
1288
+ return Boolean(process.stdin.isTTY);
1289
+ }
1290
+
1291
+ function promptHiddenPassword(promptText) {
1292
+ return new Promise((resolve, reject) => {
1293
+ const input = process.stdin;
1294
+ const output = process.stdout;
1295
+ if (!input.setRawMode) {
1296
+ reject(new Error("Interactive password input is not supported by this terminal."));
1297
+ return;
1298
+ }
1299
+ let value = "";
1300
+ output.write(promptText);
1301
+ input.setEncoding("utf8");
1302
+ input.setRawMode(true);
1303
+ input.resume();
1304
+ const cleanup = () => {
1305
+ input.setRawMode(false);
1306
+ input.pause();
1307
+ input.removeListener("data", onData);
1308
+ output.write("\n");
1309
+ };
1310
+ const onData = (chunk) => {
1311
+ if (chunk === "\u0003") {
1312
+ cleanup();
1313
+ reject(new Error("Password input cancelled."));
1314
+ return;
1315
+ }
1316
+ if (chunk === "\r" || chunk === "\n" || chunk === "\r\n") {
1317
+ cleanup();
1318
+ resolve(value);
1319
+ return;
1320
+ }
1321
+ if (chunk === "\b" || chunk === "\u007f") {
1322
+ value = value.slice(0, -1);
1323
+ return;
1324
+ }
1325
+ value += chunk;
1326
+ };
1327
+ input.on("data", onData);
1328
+ });
1329
+ }
1330
+
1331
+ const currentFile = fileURLToPath(import.meta.url);
1332
+ const invokedFile = process.argv[1] ? fileURLToPath(pathToFileURL(process.argv[1])) : "";
1333
+ if (isDirectInvocation(currentFile, invokedFile)) {
1334
+ main();
1335
+ }
1336
+
1337
+ function normalizePath(path) {
1338
+ try {
1339
+ return realpathSync.native(path).replace(/\\/g, "/").toLowerCase();
1340
+ } catch (_error) {
1341
+ return path.replace(/\\/g, "/").toLowerCase();
1342
+ }
1343
+ }