@finebi/finebi-cli 1.2.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -39
- package/package.json +40 -37
- package/skills/finebi-cli-agent/SKILL.md +133 -0
- package/skills/finebi-cli-agent/agents/openai.yaml +7 -0
- package/skills/finebi-cli-agent/references/analysis-dsl-api.md +322 -0
- package/skills/finebi-cli-agent/references/artifact-ordering.md +29 -0
- package/skills/finebi-cli-agent/references/command-workflows.md +199 -0
- package/skills/finebi-cli-agent/references/dashboard-dsl-api.md +1037 -0
- package/skills/finebi-cli-agent/references/dsl-handoff-contract.md +195 -0
- package/skills/finebi-cli-agent/references/file-to-dashboard.md +65 -0
- package/skills/finebi-cli-agent/references/formula-basics.md +102 -0
- package/skills/finebi-cli-agent/references/formula-contexts.md +66 -0
- package/skills/finebi-cli-agent/references/formula-functions.md +247 -0
- package/skills/finebi-cli-agent/references/formula-generation.md +121 -0
- package/skills/finebi-cli-agent/references/formula-glossary.md +17 -0
- package/skills/finebi-cli-agent/references/formula-help-boundaries.md +87 -0
- package/skills/finebi-cli-agent/references/formula-help-index.md +264 -0
- package/skills/finebi-cli-agent/references/formula-parameters-aggregate.md +143 -0
- package/skills/finebi-cli-agent/references/formula-parameters-analysis.md +223 -0
- package/skills/finebi-cli-agent/references/formula-parameters-date.md +236 -0
- package/skills/finebi-cli-agent/references/formula-parameters-math-logic.md +271 -0
- package/skills/finebi-cli-agent/references/formula-parameters-text.md +217 -0
- package/skills/finebi-cli-agent/references/formula-recipes-analysis.md +124 -0
- package/skills/finebi-cli-agent/references/formula-recipes-text-date.md +86 -0
- package/skills/finebi-cli-agent/references/formula-semantics.md +39 -0
- package/skills/finebi-cli-agent/references/formula-syntax.md +70 -0
- package/skills/finebi-cli-agent/references/formula-troubleshooting.md +63 -0
- package/skills/finebi-cli-agent/references/formula-validation.md +36 -0
- package/skills/finebi-cli-agent/references/formula.md +37 -0
- package/skills/finebi-cli-agent/references/handoff-contract.md +210 -0
- package/skills/finebi-cli-agent/references/mapping-rules.md +423 -0
- package/skills/finebi-cli-agent/references/mutation-confirmation-contract.md +150 -0
- package/skills/finebi-cli-agent/references/scenario-workflows.md +75 -0
- package/skills/finebi-cli-agent/references/widget-dsl-api-examples.md +457 -0
- package/skills/finebi-cli-agent/references/widget-dsl-api.md +1340 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/package.json +11 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/access-key-contract.js +2 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/access-key.js +174 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/auth-failure.js +18 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/auth-headers.js +21 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/auth.js +116 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/branding.js +31 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/browser-auth.js +331 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/capability.js +122 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/config-permissions.js +87 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/config.js +189 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/dynamic-command.js +324 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/error-catalog.js +482 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/errors.js +22 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/help.js +467 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/index.js +1351 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/install-skill.js +89 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/output-limits.js +188 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/output.js +33 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/profiles.js +158 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/schema-options.js +265 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/schema-validator.js +194 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/skill-files.js +94 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli-runner-core.mjs +399 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli-runner.mjs +29 -0
- package/src/config.js +27 -27
- package/src/error-catalog.js +12 -0
- package/src/help.js +70 -55
- package/src/index.js +17 -4
- package/src/install-skill.js +89 -0
- package/src/profiles.js +158 -158
- package/src/skill-files.js +94 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import {existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync} from "node:fs";
|
|
2
|
+
import {randomUUID} from "node:crypto";
|
|
3
|
+
import {homedir} from "node:os";
|
|
4
|
+
import {basename, dirname, join, win32} from "node:path";
|
|
5
|
+
|
|
6
|
+
import {createConfigPermissionController} from "./config-permissions.js";
|
|
7
|
+
import {CLI_CONFIG_DIRECTORY} from "./branding.js";
|
|
8
|
+
import {maskSecret} from "./output.js";
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_TIMEOUT_MS = 30000;
|
|
11
|
+
export const DEFAULT_CAPABILITY_TTL_MS = 600000;
|
|
12
|
+
|
|
13
|
+
export function defaultConfigPath(env = process.env) {
|
|
14
|
+
const home = env.USERPROFILE || env.HOME || homedir();
|
|
15
|
+
if (isWindowsPath(home)) {
|
|
16
|
+
return win32.join(home, CLI_CONFIG_DIRECTORY, "config.json");
|
|
17
|
+
}
|
|
18
|
+
return join(home, CLI_CONFIG_DIRECTORY, "config.json");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function readConfig(path = defaultConfigPath()) {
|
|
22
|
+
if (!existsSync(path)) {
|
|
23
|
+
return defaultConfig();
|
|
24
|
+
}
|
|
25
|
+
return normalizeConfig(JSON.parse(readFileSync(path, "utf8")));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function writeConfig(path, config, permissionOptions = {}) {
|
|
29
|
+
const directory = dirname(path);
|
|
30
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
31
|
+
const permissions = createConfigPermissionController(permissionOptions);
|
|
32
|
+
permissions.restrictDirectory(directory);
|
|
33
|
+
// 先创建不含敏感内容的空文件并收紧权限,避免 JWT/Access Key 在 chmod/icacls 前短暂暴露。
|
|
34
|
+
writeFileSync(path, "", { encoding: "utf8", mode: 0o600, flag: "a" });
|
|
35
|
+
permissions.restrictFile(path);
|
|
36
|
+
writeFileSync(path, `${JSON.stringify(sanitizeConfig(normalizeConfig(config)), null, 2)}\n`, {
|
|
37
|
+
encoding: "utf8",
|
|
38
|
+
mode: 0o600,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Write a complete protected staging file beside config.json, then atomically replace it. */
|
|
43
|
+
export function writeConfigAtomically(path, config, options = {}) {
|
|
44
|
+
const {rename = renameSync, remove = rmSync, ...permissionOptions} = options;
|
|
45
|
+
const staging = join(dirname(path), `.${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
|
|
46
|
+
try {
|
|
47
|
+
writeConfig(staging, config, permissionOptions);
|
|
48
|
+
rename(staging, path);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
try {
|
|
51
|
+
remove(staging, {force: true});
|
|
52
|
+
} catch (_cleanupError) {
|
|
53
|
+
// Preserve the original write error; the staging file only contains the new local configuration.
|
|
54
|
+
}
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function resolveConfig(flags = {}, env = process.env, fileConfig = readConfig()) {
|
|
60
|
+
const baseUrl = firstNonEmpty(flags.baseUrl, env.FINEBI_BASE_URL, fileConfig.baseUrl);
|
|
61
|
+
const sameServer = sameBaseUrl(baseUrl, fileConfig.baseUrl);
|
|
62
|
+
const explicitToken = firstNonEmpty(flags.token, env.FINEBI_AUTH_TOKEN);
|
|
63
|
+
const savedAuth = sameServer ? fileConfig.auth : undefined;
|
|
64
|
+
const token = firstNonEmpty(explicitToken, savedAuth?.token, sameServer ? fileConfig.token : undefined);
|
|
65
|
+
return normalizeConfig({
|
|
66
|
+
...fileConfig,
|
|
67
|
+
baseUrl,
|
|
68
|
+
timeoutMs: parsePositiveInt(firstNonEmpty(flags.timeoutMs, env.FINEBI_TIMEOUT_MS), fileConfig.timeoutMs),
|
|
69
|
+
capabilityTtlMs: parsePositiveInt(
|
|
70
|
+
firstNonEmpty(flags.capabilityTtlMs, env.FINEBI_CAPABILITY_TTL_MS),
|
|
71
|
+
fileConfig.capabilityTtlMs,
|
|
72
|
+
),
|
|
73
|
+
auth: {
|
|
74
|
+
...(explicitToken ? {} : savedAuth),
|
|
75
|
+
token,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function updateConfigValue(config, key, value, now = () => new Date().toISOString()) {
|
|
81
|
+
const next = normalizeConfig(config);
|
|
82
|
+
switch (key) {
|
|
83
|
+
case "base-url":
|
|
84
|
+
if (next.baseUrl !== value) {
|
|
85
|
+
next.capabilityCacheInvalidatedAt = now();
|
|
86
|
+
delete next.auth;
|
|
87
|
+
delete next.token;
|
|
88
|
+
}
|
|
89
|
+
next.baseUrl = value;
|
|
90
|
+
return next;
|
|
91
|
+
case "timeout-ms":
|
|
92
|
+
next.timeoutMs = parsePositiveInt(value, DEFAULT_TIMEOUT_MS);
|
|
93
|
+
return next;
|
|
94
|
+
case "capability-ttl-ms":
|
|
95
|
+
next.capabilityTtlMs = parsePositiveInt(value, DEFAULT_CAPABILITY_TTL_MS);
|
|
96
|
+
return next;
|
|
97
|
+
default:
|
|
98
|
+
throw new Error(`Unsupported config key: ${key}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function maskConfig(config) {
|
|
103
|
+
const normalized = normalizeConfig(config);
|
|
104
|
+
return {
|
|
105
|
+
...normalized,
|
|
106
|
+
token: maskSecret(normalized.token),
|
|
107
|
+
auth: normalized.auth
|
|
108
|
+
? {
|
|
109
|
+
...normalized.auth,
|
|
110
|
+
token: maskSecret(normalized.auth.token),
|
|
111
|
+
...(normalized.auth.accessKey
|
|
112
|
+
? {
|
|
113
|
+
accessKey: {
|
|
114
|
+
...normalized.auth.accessKey,
|
|
115
|
+
value: maskSecret(normalized.auth.accessKey.value),
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
: {}),
|
|
119
|
+
}
|
|
120
|
+
: undefined,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function defaultConfig() {
|
|
125
|
+
return {
|
|
126
|
+
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
127
|
+
capabilityTtlMs: DEFAULT_CAPABILITY_TTL_MS,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function normalizeConfig(config) {
|
|
132
|
+
return {
|
|
133
|
+
...config,
|
|
134
|
+
timeoutMs: parsePositiveInt(config.timeoutMs, DEFAULT_TIMEOUT_MS),
|
|
135
|
+
capabilityTtlMs: parsePositiveInt(config.capabilityTtlMs, DEFAULT_CAPABILITY_TTL_MS),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function sanitizeConfig(config) {
|
|
140
|
+
if (config.schemaVersion === 2) {
|
|
141
|
+
return {
|
|
142
|
+
schemaVersion: 2,
|
|
143
|
+
currentProfile: config.currentProfile,
|
|
144
|
+
profiles: Object.fromEntries(Object.entries(config.profiles).map(([name, profile]) => [name, sanitizeConfig(normalizeConfig(profile))])),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const { token: _legacyToken, ...rest } = config;
|
|
148
|
+
const auth = config.auth
|
|
149
|
+
? {
|
|
150
|
+
username: config.auth.username,
|
|
151
|
+
token: config.auth.token,
|
|
152
|
+
expiresAt: config.auth.expiresAt,
|
|
153
|
+
loginAt: config.auth.loginAt,
|
|
154
|
+
source: config.auth.source,
|
|
155
|
+
accessKey: config.auth.accessKey
|
|
156
|
+
? {
|
|
157
|
+
id: config.auth.accessKey.id,
|
|
158
|
+
value: config.auth.accessKey.value,
|
|
159
|
+
description: config.auth.accessKey.description,
|
|
160
|
+
createdAt: config.auth.accessKey.createdAt,
|
|
161
|
+
}
|
|
162
|
+
: undefined,
|
|
163
|
+
}
|
|
164
|
+
: undefined;
|
|
165
|
+
return {
|
|
166
|
+
...rest,
|
|
167
|
+
auth,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function sameBaseUrl(left, right) {
|
|
172
|
+
return (left || "").replace(/\/+$/, "") === (right || "").replace(/\/+$/, "");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function firstNonEmpty(...values) {
|
|
176
|
+
return values.find((value) => typeof value === "string" && value.trim().length > 0)?.trim();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function parsePositiveInt(value, fallback) {
|
|
180
|
+
const parsed = Number(value);
|
|
181
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
182
|
+
return fallback;
|
|
183
|
+
}
|
|
184
|
+
return parsed;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function isWindowsPath(value) {
|
|
188
|
+
return /^[a-zA-Z]:[\\/]/.test(value) || value.includes("\\");
|
|
189
|
+
}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import {randomBytes} from "node:crypto";
|
|
2
|
+
import {createReadStream} from "node:fs";
|
|
3
|
+
import {basename, extname} from "node:path";
|
|
4
|
+
import {Readable} from "node:stream";
|
|
5
|
+
|
|
6
|
+
import {authenticationFailure} from "./auth-failure.js";
|
|
7
|
+
import {buildAuthHeaders} from "./auth-headers.js";
|
|
8
|
+
import {commandLine} from "./branding.js";
|
|
9
|
+
import {cliError} from "./errors.js";
|
|
10
|
+
import {failure} from "./output.js";
|
|
11
|
+
import {buildRequestBody} from "./schema-options.js";
|
|
12
|
+
|
|
13
|
+
export async function executeDynamicCommand(manifest, resourceName, commandName, args, options) {
|
|
14
|
+
const command = findCommand(manifest, resourceName, commandName);
|
|
15
|
+
if (!command) {
|
|
16
|
+
return failure(`${resourceName}.${commandName}`, cliError("CAPABILITY_NOT_FOUND", {
|
|
17
|
+
path: `${resourceName}.${commandName}`,
|
|
18
|
+
message: `Capability not found: ${resourceName}.${commandName}.`,
|
|
19
|
+
hint: "Run finebi-cli capability refresh and check finebi-cli --help.",
|
|
20
|
+
retryable: true,
|
|
21
|
+
nextActions: ["finebi-cli capability refresh", "finebi-cli --help"],
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
const displayArgs = parseDisplayArgs(args, command);
|
|
25
|
+
if (!displayArgs.ok) {
|
|
26
|
+
return failure(command.command, withCommandHelp(displayArgs.error, resourceName, commandName));
|
|
27
|
+
}
|
|
28
|
+
const requestBody = buildRequestBody(command.requestSchema || { properties: {}, required: [] }, displayArgs.args);
|
|
29
|
+
if (!requestBody.ok) {
|
|
30
|
+
return failure(command.command, withCommandHelp(requestBody.error, resourceName, commandName));
|
|
31
|
+
}
|
|
32
|
+
const envelope = await sendDynamicRequest(command, requestBody.body, options);
|
|
33
|
+
return withDisplayResolved(withResolvedLinks(envelope, options.baseUrl), displayArgs.display);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function findCommand(manifest, resourceName, commandName) {
|
|
37
|
+
const resource = (manifest.resources || []).find((item) => item.name === resourceName);
|
|
38
|
+
return resource?.commands?.find((item) => item.name === commandName);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function sendDynamicRequest(command, body, options) {
|
|
42
|
+
const baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const timer = setTimeout(() => controller.abort(), options.timeoutMs || 30000);
|
|
45
|
+
try {
|
|
46
|
+
const request = multipartRequest(command, body, options)
|
|
47
|
+
|| jsonRequest(command, body, options);
|
|
48
|
+
const response = await (options.fetchImpl || fetch)(`${baseUrl}${command.path}${request.query}`, {
|
|
49
|
+
method: command.method || "POST",
|
|
50
|
+
headers: request.headers,
|
|
51
|
+
body: request.body,
|
|
52
|
+
signal: controller.signal,
|
|
53
|
+
...(request.duplex ? {duplex: request.duplex} : {}),
|
|
54
|
+
});
|
|
55
|
+
return parseResponse(response, command.command);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
return failure(command.command, cliError("NETWORK_ERROR", {
|
|
58
|
+
path: command.path,
|
|
59
|
+
message: error instanceof Error ? error.message : String(error),
|
|
60
|
+
hint: "Check FineBI base-url and network connection.",
|
|
61
|
+
retryable: true,
|
|
62
|
+
nextActions: ["finebi-cli auth info", "finebi-cli capability refresh"],
|
|
63
|
+
}));
|
|
64
|
+
} finally {
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function jsonRequest(command, body, options) {
|
|
70
|
+
return {
|
|
71
|
+
query: "",
|
|
72
|
+
headers: buildAuthHeaders({
|
|
73
|
+
token: options.token,
|
|
74
|
+
accessKey: options.accessKey,
|
|
75
|
+
contentType: "application/json",
|
|
76
|
+
}),
|
|
77
|
+
body: JSON.stringify(body),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function multipartRequest(command, body, options) {
|
|
82
|
+
if (String(command.requestEncoding || "").toLowerCase() !== "multipart/form-data") {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const properties = command.requestSchema?.properties || {};
|
|
86
|
+
const entry = Object.entries(properties).find(([, property]) =>
|
|
87
|
+
property.file && String(property.fileContentType || "").toUpperCase() === "BINARY");
|
|
88
|
+
if (!entry) {
|
|
89
|
+
throw new Error(`Capability ${command.command} does not declare a BINARY file field.`);
|
|
90
|
+
}
|
|
91
|
+
const [fieldName] = entry;
|
|
92
|
+
const filePath = body[fieldName];
|
|
93
|
+
const fileName = basename(filePath);
|
|
94
|
+
const boundary = `finebi-cli-${randomBytes(16).toString("hex")}`;
|
|
95
|
+
return {
|
|
96
|
+
query: `?${encodeURIComponent(fieldName)}=${encodeURIComponent(fileName)}`,
|
|
97
|
+
headers: buildAuthHeaders({
|
|
98
|
+
token: options.token,
|
|
99
|
+
accessKey: options.accessKey,
|
|
100
|
+
contentType: `multipart/form-data; boundary=${boundary}`,
|
|
101
|
+
}),
|
|
102
|
+
body: createMultipartBody(boundary, fieldName, fileName, filePath),
|
|
103
|
+
duplex: "half",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function createMultipartBody(boundary, fieldName, fileName, filePath) {
|
|
108
|
+
const dispositionName = multipartHeaderValue(fieldName);
|
|
109
|
+
const dispositionFileName = multipartHeaderValue(fileName);
|
|
110
|
+
const opening = Buffer.from(
|
|
111
|
+
`--${boundary}\r\n`
|
|
112
|
+
+ `Content-Disposition: form-data; name="${dispositionName}"; filename="${dispositionFileName}"\r\n`
|
|
113
|
+
+ `Content-Type: ${uploadContentType(fileName)}\r\n\r\n`,
|
|
114
|
+
);
|
|
115
|
+
const closing = Buffer.from(`\r\n--${boundary}--\r\n`);
|
|
116
|
+
return Readable.from((async function* () {
|
|
117
|
+
yield opening;
|
|
118
|
+
for await (const chunk of createReadStream(filePath)) {
|
|
119
|
+
yield chunk;
|
|
120
|
+
}
|
|
121
|
+
yield closing;
|
|
122
|
+
})());
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function uploadContentType(fileName) {
|
|
126
|
+
switch (extname(fileName).toLowerCase()) {
|
|
127
|
+
case ".xlsx":
|
|
128
|
+
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
|
129
|
+
case ".xls":
|
|
130
|
+
return "application/vnd.ms-excel";
|
|
131
|
+
case ".csv":
|
|
132
|
+
return "text/csv";
|
|
133
|
+
case ".png":
|
|
134
|
+
return "image/png";
|
|
135
|
+
case ".jpg":
|
|
136
|
+
case ".jpeg":
|
|
137
|
+
return "image/jpeg";
|
|
138
|
+
case ".gif":
|
|
139
|
+
return "image/gif";
|
|
140
|
+
case ".bmp":
|
|
141
|
+
return "image/bmp";
|
|
142
|
+
default:
|
|
143
|
+
return "application/octet-stream";
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function multipartHeaderValue(value) {
|
|
148
|
+
return String(value).replace(/[\r\n"]/g, "_");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function parseResponse(response, commandName) {
|
|
152
|
+
const text = await response.text();
|
|
153
|
+
const contentType = response.headers.get("content-type") || "";
|
|
154
|
+
if (!contentType.includes("json") || text.trim().startsWith("<")) {
|
|
155
|
+
if (isAuthStatusResponse(response)) {
|
|
156
|
+
return failure(commandName, cliError("AUTH_REDIRECT_OR_HTML_RESPONSE", {
|
|
157
|
+
path: "",
|
|
158
|
+
message: "FineBI returned an HTML login or redirect page instead of JSON.",
|
|
159
|
+
hint: "Run finebi-cli auth login and retry the command.",
|
|
160
|
+
retryable: true,
|
|
161
|
+
nextActions: ["finebi-cli auth login"],
|
|
162
|
+
}), {status: response.status});
|
|
163
|
+
}
|
|
164
|
+
if (isAuthHtmlResponse(text)) {
|
|
165
|
+
return failure(commandName, cliError("AUTH_REDIRECT_OR_HTML_RESPONSE", {
|
|
166
|
+
path: "",
|
|
167
|
+
message: "FineBI returned an HTML login or redirect page instead of JSON.",
|
|
168
|
+
hint: "Run finebi-cli auth login and retry the command.",
|
|
169
|
+
retryable: true,
|
|
170
|
+
nextActions: ["finebi-cli auth login"],
|
|
171
|
+
}), {status: response.status});
|
|
172
|
+
}
|
|
173
|
+
return failure(commandName, cliError("HTTP_HTML_RESPONSE", {
|
|
174
|
+
path: "",
|
|
175
|
+
message: `FineBI returned HTTP ${response.status} with an HTML page instead of JSON: ${text.replace(/\s+/g, " ").slice(0, 200)}`,
|
|
176
|
+
hint: "Check FineBI server logs and retry after the backend error is fixed.",
|
|
177
|
+
retryable: false,
|
|
178
|
+
nextActions: ["Check FineBI server logs"],
|
|
179
|
+
}), {status: response.status});
|
|
180
|
+
}
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
if (response.status === 401 || response.status === 403) {
|
|
183
|
+
return authenticationFailure(commandName, "", response.status);
|
|
184
|
+
}
|
|
185
|
+
return failure(commandName, cliError("HTTP_ERROR", {
|
|
186
|
+
path: "",
|
|
187
|
+
message: `FineBI returned HTTP ${response.status}: ${text.slice(0, 300)}`,
|
|
188
|
+
hint: "Check FineBI server logs and command arguments.",
|
|
189
|
+
retryable: false,
|
|
190
|
+
nextActions: ["finebi-cli capability refresh"],
|
|
191
|
+
}), {status: response.status});
|
|
192
|
+
}
|
|
193
|
+
return normalizeDynamicEnvelope(JSON.parse(text), commandName);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function normalizeDynamicEnvelope(envelope, commandName) {
|
|
197
|
+
return {
|
|
198
|
+
ok: Boolean(envelope.ok),
|
|
199
|
+
command: envelope.command || commandName,
|
|
200
|
+
data: envelope.data ?? null,
|
|
201
|
+
errors: Array.isArray(envelope.errors) ? envelope.errors : [],
|
|
202
|
+
warnings: Array.isArray(envelope.warnings) ? envelope.warnings : [],
|
|
203
|
+
resolved: envelope.resolved && typeof envelope.resolved === "object" ? envelope.resolved : {},
|
|
204
|
+
meta: envelope.meta && typeof envelope.meta === "object" ? envelope.meta : {},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function parseDisplayArgs(args, command) {
|
|
209
|
+
const nextArgs = [];
|
|
210
|
+
let format = "auto";
|
|
211
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
212
|
+
const current = args[index];
|
|
213
|
+
if (current === "--json") {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (current === "-f" || current === "--format") {
|
|
217
|
+
const value = args[index + 1];
|
|
218
|
+
if (!value || value.startsWith("-")) {
|
|
219
|
+
return invalidDisplayArgument(current, `Missing value for ${current}.`);
|
|
220
|
+
}
|
|
221
|
+
format = value;
|
|
222
|
+
index += 1;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
nextArgs.push(current);
|
|
226
|
+
}
|
|
227
|
+
if (!["auto", "table", "expanded", "csv"].includes(format)) {
|
|
228
|
+
return invalidDisplayArgument(format, `Unsupported output format: ${format}.`);
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
ok: true,
|
|
232
|
+
args: nextArgs,
|
|
233
|
+
display: {
|
|
234
|
+
...(command.display || {}),
|
|
235
|
+
format,
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function invalidDisplayArgument(path, message) {
|
|
241
|
+
return {
|
|
242
|
+
ok: false,
|
|
243
|
+
error: cliError("INVALID_ARGUMENT", {
|
|
244
|
+
path,
|
|
245
|
+
message,
|
|
246
|
+
hint: "Use -f auto, -f table, -f expanded, or -f csv.",
|
|
247
|
+
retryable: false,
|
|
248
|
+
nextActions: ["finebi-cli <resource> <command> --help"],
|
|
249
|
+
}),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function withCommandHelp(error, resourceName, commandName) {
|
|
254
|
+
return {
|
|
255
|
+
...error,
|
|
256
|
+
nextActions: [commandLine(resourceName, commandName, "--help")],
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function withDisplayResolved(envelope, display) {
|
|
261
|
+
const resolved = envelope.resolved || {};
|
|
262
|
+
return {
|
|
263
|
+
...envelope,
|
|
264
|
+
resolved: {
|
|
265
|
+
...resolved,
|
|
266
|
+
display: {
|
|
267
|
+
...(resolved.display || {}),
|
|
268
|
+
...display,
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function withResolvedLinks(envelope, baseUrl) {
|
|
275
|
+
const links = envelope.data?.links;
|
|
276
|
+
if (!links || typeof links !== "object" || Array.isArray(links)) {
|
|
277
|
+
return envelope;
|
|
278
|
+
}
|
|
279
|
+
const resolvedLinks = Object.fromEntries(Object.entries(links).flatMap(([relation, link]) => {
|
|
280
|
+
const url = resolveLinkUrl(baseUrl, link?.path);
|
|
281
|
+
return url ? [[relation, {url}]] : [];
|
|
282
|
+
}));
|
|
283
|
+
if (Object.keys(resolvedLinks).length === 0) {
|
|
284
|
+
return envelope;
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
...envelope,
|
|
288
|
+
resolved: {
|
|
289
|
+
...(envelope.resolved || {}),
|
|
290
|
+
links: {
|
|
291
|
+
...(envelope.resolved?.links || {}),
|
|
292
|
+
...resolvedLinks,
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function resolveLinkUrl(baseUrl, path) {
|
|
299
|
+
if (typeof baseUrl !== "string" || typeof path !== "string" || !path.trim()) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
const cleanBaseUrl = baseUrl.replace(/[?#].*$/, "").replace(/\/+$/, "");
|
|
303
|
+
return `${cleanBaseUrl}${path.trim()}`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function isAuthStatusResponse(response) {
|
|
307
|
+
if (response.status === 401 || response.status === 403 || response.status >= 300 && response.status < 400) {
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function isAuthHtmlResponse(text) {
|
|
314
|
+
const lower = text.toLowerCase();
|
|
315
|
+
return lower.includes("login") && lower.includes("password");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function commandHelpAction(commandName) {
|
|
319
|
+
const parts = commandName.split(".");
|
|
320
|
+
if (parts.length >= 2) {
|
|
321
|
+
return commandLine(parts[0], parts[1], "--help");
|
|
322
|
+
}
|
|
323
|
+
return commandLine("<resource>", "<command>", "--help");
|
|
324
|
+
}
|