@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,399 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import {spawn} from "node:child_process";
|
|
4
|
+
import {createHash} from "node:crypto";
|
|
5
|
+
import {existsSync, readFileSync} from "node:fs";
|
|
6
|
+
import {delimiter, dirname, extname, resolve} from "node:path";
|
|
7
|
+
import {fileURLToPath} from "node:url";
|
|
8
|
+
|
|
9
|
+
const SENSITIVE_OPTION_PATTERN = /PASSWORD|TOKEN|COOKIE|SECRET|ACCESS[_-]?KEY|AUTH/i;
|
|
10
|
+
const SENSITIVE_ENV_PATTERN = /(?:^|[_-])(?:PASSWORD|TOKEN|COOKIE|SECRET|ACCESS[_-]?KEY|AUTHORIZATION)(?:$|[_-])/i;
|
|
11
|
+
const VALUELESS_SENSITIVE_OPTIONS = new Set(["--password-stdin", "--no-access-key"]);
|
|
12
|
+
|
|
13
|
+
export async function run(argv = process.argv.slice(2), options = {}) {
|
|
14
|
+
const selected = extractProfile(argv);
|
|
15
|
+
argv = selected.argv;
|
|
16
|
+
const {mode, resource, command, commandArgv} = parseArguments(argv);
|
|
17
|
+
const env = {...(options.env ?? process.env), ...(selected.profile ? {FINEBI_PROFILE: selected.profile} : {})};
|
|
18
|
+
const cwd = options.cwd ?? process.cwd();
|
|
19
|
+
const invocation = resolveCliInvocation(env, cwd);
|
|
20
|
+
|
|
21
|
+
if (mode === "inspect") {
|
|
22
|
+
return inspectCommand({invocation, resource, command, cwd, env});
|
|
23
|
+
}
|
|
24
|
+
if (command === "template") {
|
|
25
|
+
return executeReadOnlyTemplate({invocation, resource, command, commandArgv, cwd, env});
|
|
26
|
+
}
|
|
27
|
+
return executeCommand({invocation, commandArgv, cwd, env});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function extractProfile(argv) {
|
|
31
|
+
const remaining = [];
|
|
32
|
+
let profile;
|
|
33
|
+
for (let index = 0; index < argv.length; index++) {
|
|
34
|
+
const arg = argv[index];
|
|
35
|
+
if (arg !== "--profile" && !arg.startsWith("--profile=")) {
|
|
36
|
+
remaining.push(arg);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (profile !== undefined) throw new Error("--profile must only be supplied once.");
|
|
40
|
+
profile = arg === "--profile" ? argv[++index] : arg.slice("--profile=".length);
|
|
41
|
+
if (!profile || !/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(profile)) throw new Error("--profile requires a valid environment name.");
|
|
42
|
+
}
|
|
43
|
+
return {argv: remaining, profile};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function parseArguments(argv) {
|
|
47
|
+
const [mode, ...rest] = argv;
|
|
48
|
+
if (mode === "inspect" && rest.length === 2) {
|
|
49
|
+
return {mode, resource: rest[0], command: rest[1], commandArgv: null};
|
|
50
|
+
}
|
|
51
|
+
if (mode === "run" && rest.length >= 3 && rest.includes("--json")) {
|
|
52
|
+
return {mode, resource: rest[0], command: rest[1], commandArgv: rest};
|
|
53
|
+
}
|
|
54
|
+
throw new Error("Usage: finebi-cli-runner.mjs inspect <resource> <command> | run <resource> <command> [...args] --json");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function resolveCliInvocation(env = process.env, cwd = process.cwd()) {
|
|
58
|
+
const configuredPath = env.FINEBI_CLI_PATH?.trim();
|
|
59
|
+
if (configuredPath) {
|
|
60
|
+
const cliPath = resolve(cwd, configuredPath);
|
|
61
|
+
if (!existsSync(cliPath)) {
|
|
62
|
+
throw new Error(`FINEBI_CLI_PATH does not exist: ${cliPath}`);
|
|
63
|
+
}
|
|
64
|
+
return invocationForPath(cliPath);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const cliPath = findExecutableOnPath(env.PATH || "");
|
|
68
|
+
if (!cliPath) {
|
|
69
|
+
throw new Error("FineBI CLI is not installed on PATH. Install @finebi/finebi-cli or set FINEBI_CLI_PATH to its executable or JavaScript entry file.");
|
|
70
|
+
}
|
|
71
|
+
return invocationForPath(cliPath);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function findExecutableOnPath(pathValue) {
|
|
75
|
+
const names = process.platform === "win32"
|
|
76
|
+
? ["finebi-cli.exe", "finebi-cli.cmd", "finebi-cli.bat", "finebi-cli"]
|
|
77
|
+
: ["finebi-cli"];
|
|
78
|
+
for (const directory of pathValue.split(delimiter).filter(Boolean)) {
|
|
79
|
+
const normalizedDirectory = directory.replace(/^"|"$/g, "");
|
|
80
|
+
for (const name of names) {
|
|
81
|
+
const candidate = resolve(normalizedDirectory, name);
|
|
82
|
+
if (existsSync(candidate)) return candidate;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function invocationForPath(cliPath) {
|
|
89
|
+
const extension = extname(cliPath).toLowerCase();
|
|
90
|
+
if ([".js", ".mjs", ".cjs"].includes(extension)) {
|
|
91
|
+
return {executable: process.execPath, prefixArgs: [cliPath], displayCommand: "finebi-cli"};
|
|
92
|
+
}
|
|
93
|
+
if (extension === ".cmd" || extension === ".bat") {
|
|
94
|
+
return {
|
|
95
|
+
executable: process.execPath,
|
|
96
|
+
prefixArgs: [resolveNodeEntryFromWindowsShim(cliPath)],
|
|
97
|
+
displayCommand: "finebi-cli",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return {executable: cliPath, prefixArgs: [], displayCommand: "finebi-cli"};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function resolveNodeEntryFromWindowsShim(shimPath) {
|
|
104
|
+
const content = readFileSync(shimPath, "utf8");
|
|
105
|
+
const matches = [...content.matchAll(/%(?:~dp0|dp0%)([^"\r\n]*?\.(?:c|m)?js)/giu)];
|
|
106
|
+
const relativeEntry = matches.at(-1)?.[1];
|
|
107
|
+
if (!relativeEntry) {
|
|
108
|
+
throw new Error(`Cannot resolve the JavaScript entry from ${shimPath}. Set FINEBI_CLI_PATH to the installed FineBI CLI JavaScript entry file.`);
|
|
109
|
+
}
|
|
110
|
+
const entryPath = resolve(dirname(shimPath), relativeEntry.replace(/^[\\/]+/u, ""));
|
|
111
|
+
if (!existsSync(entryPath)) {
|
|
112
|
+
throw new Error(`FineBI CLI JavaScript entry does not exist: ${entryPath}`);
|
|
113
|
+
}
|
|
114
|
+
return entryPath;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function projectCapability(cache, resourceName, commandName) {
|
|
118
|
+
const manifest = cache?.manifest;
|
|
119
|
+
const resource = manifest?.resources?.find((item) => item?.name === resourceName);
|
|
120
|
+
const command = resource?.commands?.find((item) => item?.name === commandName);
|
|
121
|
+
if (!command) {
|
|
122
|
+
throw new Error(`Capability does not contain ${resourceName}.${commandName}.`);
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
fetchedAt: cache.fetchedAt ?? null,
|
|
126
|
+
expiresAt: cache.expiresAt ?? null,
|
|
127
|
+
ttlMs: cache.ttlMs ?? null,
|
|
128
|
+
schemaVersion: manifest.schemaVersion ?? null,
|
|
129
|
+
capabilityFingerprint: createHash("sha256").update(JSON.stringify(manifest)).digest("hex"),
|
|
130
|
+
resource: resourceName,
|
|
131
|
+
command: commandName,
|
|
132
|
+
summary: command.summary ?? null,
|
|
133
|
+
method: command.method ?? null,
|
|
134
|
+
path: command.path ?? null,
|
|
135
|
+
mutation: command.mutation === true,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function inspectCommand({invocation, resource, command, cwd, env}) {
|
|
140
|
+
const capabilityResult = await runCli(invocation, ["capability", "info", "--json"], cwd, env);
|
|
141
|
+
if (!capabilityResult.envelope?.ok) {
|
|
142
|
+
return reportFailure("inspect", capabilityResult, invocation);
|
|
143
|
+
}
|
|
144
|
+
const capability = projectCapability(capabilityResult.envelope.data?.cache, resource, command);
|
|
145
|
+
const helpArgv = [resource, command, "--help", "--json"];
|
|
146
|
+
const helpResult = await runCli(invocation, helpArgv, cwd, env);
|
|
147
|
+
if (!helpResult.envelope?.ok) {
|
|
148
|
+
return reportFailure("inspect", helpResult, invocation, capability);
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
ok: true,
|
|
152
|
+
mode: "inspect",
|
|
153
|
+
capability,
|
|
154
|
+
help: projectHelp(helpResult.envelope.data),
|
|
155
|
+
executions: [
|
|
156
|
+
cliExecution(invocation, capabilityResult),
|
|
157
|
+
cliExecution(invocation, helpResult),
|
|
158
|
+
],
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function projectHelp(help) {
|
|
163
|
+
return {
|
|
164
|
+
resource: help?.resource ?? null,
|
|
165
|
+
command: help?.command ?? null,
|
|
166
|
+
summary: help?.summary ?? null,
|
|
167
|
+
usage: help?.usage ?? null,
|
|
168
|
+
options: Array.isArray(help?.options) ? help.options.map((option) => ({
|
|
169
|
+
name: option.name ?? null,
|
|
170
|
+
required: option.required === true,
|
|
171
|
+
type: option.type ?? null,
|
|
172
|
+
default: option.default ?? null,
|
|
173
|
+
})) : [],
|
|
174
|
+
display: help?.display ?? null,
|
|
175
|
+
requestSchema: compactSchema(help?.requestSchema, 0),
|
|
176
|
+
responseSchema: compactSchema(help?.responseSchema, 0),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function compactSchema(schema, depth) {
|
|
181
|
+
if (!schema || typeof schema !== "object") return null;
|
|
182
|
+
const projected = {
|
|
183
|
+
type: schema.type ?? null,
|
|
184
|
+
required: Array.isArray(schema.required) ? schema.required : [],
|
|
185
|
+
};
|
|
186
|
+
if (Array.isArray(schema.enumValues) && schema.enumValues.length > 0) {
|
|
187
|
+
projected.enumValues = schema.enumValues;
|
|
188
|
+
}
|
|
189
|
+
if (schema.file === true) projected.file = true;
|
|
190
|
+
if (typeof schema.input === "string" && schema.input) projected.input = schema.input;
|
|
191
|
+
if (schema.nullable === true) projected.nullable = true;
|
|
192
|
+
if (schema.items && typeof schema.items === "object") {
|
|
193
|
+
projected.items = compactSchema(schema.items, depth + 1);
|
|
194
|
+
}
|
|
195
|
+
if (schema.schema && typeof schema.schema === "object") {
|
|
196
|
+
projected.schema = compactSchema(schema.schema, depth + 1);
|
|
197
|
+
}
|
|
198
|
+
const properties = schema.properties && typeof schema.properties === "object" ? schema.properties : null;
|
|
199
|
+
if (properties) {
|
|
200
|
+
projected.properties = Object.fromEntries(Object.entries(properties).map(([name, property]) => {
|
|
201
|
+
if (depth >= 2) return [name, {type: property?.type ?? null}];
|
|
202
|
+
return [name, compactSchema(property, depth + 1)];
|
|
203
|
+
}));
|
|
204
|
+
}
|
|
205
|
+
return projected;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function executeReadOnlyTemplate({invocation, resource, command, commandArgv, cwd, env}) {
|
|
209
|
+
const capabilityResult = await runCli(invocation, ["capability", "info", "--json"], cwd, env);
|
|
210
|
+
if (!capabilityResult.envelope?.ok) {
|
|
211
|
+
return reportFailure("run", capabilityResult, invocation);
|
|
212
|
+
}
|
|
213
|
+
const capability = projectCapability(capabilityResult.envelope.data?.cache, resource, command);
|
|
214
|
+
if (capability.mutation) {
|
|
215
|
+
return {
|
|
216
|
+
ok: false,
|
|
217
|
+
mode: "run",
|
|
218
|
+
capability,
|
|
219
|
+
execution: cliExecution(invocation, capabilityResult),
|
|
220
|
+
error: {
|
|
221
|
+
code: "TEMPLATE_MUTATION_FORBIDDEN",
|
|
222
|
+
message: resource + "." + command + " is not read-only in the current capability.",
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
const result = await executeCommand({invocation, commandArgv, cwd, env});
|
|
227
|
+
return {...result, capability, preflight: cliExecution(invocation, capabilityResult)};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function executeCommand({invocation, commandArgv, cwd, env}) {
|
|
231
|
+
const result = await runCli(invocation, commandArgv, cwd, env);
|
|
232
|
+
return {
|
|
233
|
+
ok: result.envelope?.ok === true && result.exitCode === 0,
|
|
234
|
+
mode: "run",
|
|
235
|
+
execution: cliExecution(invocation, result),
|
|
236
|
+
envelope: result.envelope,
|
|
237
|
+
stderr: result.stderr || "",
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function runCli(invocation, argv, cwd, env) {
|
|
242
|
+
return new Promise((resolvePromise) => {
|
|
243
|
+
const startMs = Date.now();
|
|
244
|
+
const sensitiveValues = collectSensitiveValues(argv, env);
|
|
245
|
+
const child = spawn(invocation.executable, [...invocation.prefixArgs, ...argv], {
|
|
246
|
+
cwd,
|
|
247
|
+
env,
|
|
248
|
+
shell: false,
|
|
249
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
250
|
+
windowsHide: true,
|
|
251
|
+
});
|
|
252
|
+
let stdout = "";
|
|
253
|
+
let stderr = "";
|
|
254
|
+
let settled = false;
|
|
255
|
+
let timedOut = false;
|
|
256
|
+
let spawnError = null;
|
|
257
|
+
let timeoutHandle;
|
|
258
|
+
|
|
259
|
+
const finish = (exitCode) => {
|
|
260
|
+
if (settled) return;
|
|
261
|
+
settled = true;
|
|
262
|
+
clearTimeout(timeoutHandle);
|
|
263
|
+
const safeStdout = redactText(stdout, sensitiveValues);
|
|
264
|
+
const safeStderr = redactText(stderr, sensitiveValues);
|
|
265
|
+
let envelope = null;
|
|
266
|
+
try {
|
|
267
|
+
envelope = JSON.parse(safeStdout.trim());
|
|
268
|
+
} catch (_error) {
|
|
269
|
+
envelope = null;
|
|
270
|
+
}
|
|
271
|
+
resolvePromise({
|
|
272
|
+
argv: redactArgv(argv),
|
|
273
|
+
exitCode: exitCode ?? 1,
|
|
274
|
+
durationMs: Date.now() - startMs,
|
|
275
|
+
timedOut,
|
|
276
|
+
spawnError: spawnError ? redactText(spawnError.message, sensitiveValues) : null,
|
|
277
|
+
stdout: safeStdout,
|
|
278
|
+
stderr: safeStderr,
|
|
279
|
+
envelope,
|
|
280
|
+
});
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
child.stdout.setEncoding("utf8");
|
|
284
|
+
child.stderr.setEncoding("utf8");
|
|
285
|
+
child.stdout.on("data", (chunk) => {
|
|
286
|
+
stdout += chunk;
|
|
287
|
+
});
|
|
288
|
+
child.stderr.on("data", (chunk) => {
|
|
289
|
+
stderr += chunk;
|
|
290
|
+
});
|
|
291
|
+
child.once("error", (error) => {
|
|
292
|
+
spawnError = error;
|
|
293
|
+
finish(1);
|
|
294
|
+
});
|
|
295
|
+
child.once("close", finish);
|
|
296
|
+
timeoutHandle = setTimeout(() => {
|
|
297
|
+
timedOut = true;
|
|
298
|
+
stderr += "FineBI CLI timed out after 30000 ms.\n";
|
|
299
|
+
child.kill();
|
|
300
|
+
}, 30000);
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function collectSensitiveValues(argv, env) {
|
|
305
|
+
const values = [];
|
|
306
|
+
for (const [name, value] of Object.entries(env)) {
|
|
307
|
+
if (SENSITIVE_ENV_PATTERN.test(name) && value) values.push(String(value));
|
|
308
|
+
}
|
|
309
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
310
|
+
const argument = String(argv[index]);
|
|
311
|
+
const equalsIndex = argument.indexOf("=");
|
|
312
|
+
if (equalsIndex > 0 && isSensitiveOption(argument.slice(0, equalsIndex))) {
|
|
313
|
+
values.push(argument.slice(equalsIndex + 1));
|
|
314
|
+
} else if (isSensitiveOption(argument) && !VALUELESS_SENSITIVE_OPTIONS.has(argument.toLowerCase()) && index + 1 < argv.length) {
|
|
315
|
+
values.push(String(argv[index + 1]));
|
|
316
|
+
index += 1;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return [...new Set(values.filter(Boolean))].sort((left, right) => right.length - left.length);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function redactArgv(argv) {
|
|
323
|
+
const safeArgv = argv.map(String);
|
|
324
|
+
for (let index = 0; index < safeArgv.length; index += 1) {
|
|
325
|
+
const argument = safeArgv[index];
|
|
326
|
+
const equalsIndex = argument.indexOf("=");
|
|
327
|
+
if (equalsIndex > 0 && isSensitiveOption(argument.slice(0, equalsIndex))) {
|
|
328
|
+
safeArgv[index] = `${argument.slice(0, equalsIndex + 1)}[REDACTED]`;
|
|
329
|
+
} else if (isSensitiveOption(argument) && !VALUELESS_SENSITIVE_OPTIONS.has(argument.toLowerCase()) && index + 1 < safeArgv.length) {
|
|
330
|
+
safeArgv[index + 1] = "[REDACTED]";
|
|
331
|
+
index += 1;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return safeArgv;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function isSensitiveOption(argument) {
|
|
338
|
+
return /^-{1,2}[^-]/.test(argument) && SENSITIVE_OPTION_PATTERN.test(argument);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function redactText(text, sensitiveValues) {
|
|
342
|
+
let safeText = text || "";
|
|
343
|
+
for (const value of sensitiveValues) safeText = redactBoundedSecret(safeText, value);
|
|
344
|
+
return safeText;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function redactBoundedSecret(text, secret) {
|
|
348
|
+
let safeText = "";
|
|
349
|
+
let cursor = 0;
|
|
350
|
+
let matchIndex = text.indexOf(secret);
|
|
351
|
+
while (matchIndex !== -1) {
|
|
352
|
+
const matchEnd = matchIndex + secret.length;
|
|
353
|
+
const left = matchIndex === 0 ? "" : text[matchIndex - 1];
|
|
354
|
+
const right = matchEnd === text.length ? "" : text[matchEnd];
|
|
355
|
+
const hasTokenBoundaries = !isProtocolIdentifierChar(left) && !isProtocolIdentifierChar(right);
|
|
356
|
+
safeText += text.slice(cursor, hasTokenBoundaries ? matchIndex : matchEnd);
|
|
357
|
+
if (hasTokenBoundaries) safeText += "[REDACTED]";
|
|
358
|
+
cursor = matchEnd;
|
|
359
|
+
matchIndex = text.indexOf(secret, cursor);
|
|
360
|
+
}
|
|
361
|
+
return safeText + text.slice(cursor);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function isProtocolIdentifierChar(character) {
|
|
365
|
+
return character !== "" && /[\p{L}\p{N}_-]/u.test(character);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function cliExecution(invocation, result) {
|
|
369
|
+
return {
|
|
370
|
+
argv: [invocation.displayCommand, ...result.argv],
|
|
371
|
+
exitCode: result.exitCode,
|
|
372
|
+
durationMs: result.durationMs,
|
|
373
|
+
timedOut: result.timedOut,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function reportFailure(mode, result, invocation, capability = null) {
|
|
378
|
+
return {
|
|
379
|
+
ok: false,
|
|
380
|
+
mode,
|
|
381
|
+
capability,
|
|
382
|
+
execution: cliExecution(invocation, result),
|
|
383
|
+
error: {
|
|
384
|
+
code: result.timedOut ? "CLI_TIMEOUT" : "CLI_EXECUTION_FAILED",
|
|
385
|
+
message: result.stderr || result.spawnError || "FineBI CLI execution failed.",
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
391
|
+
try {
|
|
392
|
+
const result = await run();
|
|
393
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
394
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
395
|
+
} catch (error) {
|
|
396
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
397
|
+
process.exitCode = 1;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import {existsSync} from "node:fs";
|
|
4
|
+
import {dirname, resolve} from "node:path";
|
|
5
|
+
import {fileURLToPath} from "node:url";
|
|
6
|
+
|
|
7
|
+
import {run as runCore} from "./finebi-cli-runner-core.mjs";
|
|
8
|
+
|
|
9
|
+
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const bundledCliEntry = resolve(scriptDirectory, "finebi-cli", "src", "index.js");
|
|
11
|
+
|
|
12
|
+
export async function run(argv = process.argv.slice(2), options = {}) {
|
|
13
|
+
const env = {...(options.env ?? process.env)};
|
|
14
|
+
if (!env.FINEBI_CLI_PATH?.trim() && existsSync(bundledCliEntry)) {
|
|
15
|
+
env.FINEBI_CLI_PATH = bundledCliEntry;
|
|
16
|
+
}
|
|
17
|
+
return runCore(argv, {...options, env});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
21
|
+
try {
|
|
22
|
+
const result = await run();
|
|
23
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
24
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/config.js
CHANGED
|
@@ -56,23 +56,23 @@ export function writeConfigAtomically(path, config, options = {}) {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
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);
|
|
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
65
|
return normalizeConfig({
|
|
66
66
|
...fileConfig,
|
|
67
|
-
baseUrl,
|
|
67
|
+
baseUrl,
|
|
68
68
|
timeoutMs: parsePositiveInt(firstNonEmpty(flags.timeoutMs, env.FINEBI_TIMEOUT_MS), fileConfig.timeoutMs),
|
|
69
69
|
capabilityTtlMs: parsePositiveInt(
|
|
70
70
|
firstNonEmpty(flags.capabilityTtlMs, env.FINEBI_CAPABILITY_TTL_MS),
|
|
71
71
|
fileConfig.capabilityTtlMs,
|
|
72
72
|
),
|
|
73
73
|
auth: {
|
|
74
|
-
...(explicitToken ? {} : savedAuth),
|
|
75
|
-
token,
|
|
74
|
+
...(explicitToken ? {} : savedAuth),
|
|
75
|
+
token,
|
|
76
76
|
},
|
|
77
77
|
});
|
|
78
78
|
}
|
|
@@ -81,10 +81,10 @@ export function updateConfigValue(config, key, value, now = () => new Date().toI
|
|
|
81
81
|
const next = normalizeConfig(config);
|
|
82
82
|
switch (key) {
|
|
83
83
|
case "base-url":
|
|
84
|
-
if (next.baseUrl !== value) {
|
|
85
|
-
next.capabilityCacheInvalidatedAt = now();
|
|
86
|
-
delete next.auth;
|
|
87
|
-
delete next.token;
|
|
84
|
+
if (next.baseUrl !== value) {
|
|
85
|
+
next.capabilityCacheInvalidatedAt = now();
|
|
86
|
+
delete next.auth;
|
|
87
|
+
delete next.token;
|
|
88
88
|
}
|
|
89
89
|
next.baseUrl = value;
|
|
90
90
|
return next;
|
|
@@ -128,7 +128,7 @@ function defaultConfig() {
|
|
|
128
128
|
};
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
export function normalizeConfig(config) {
|
|
131
|
+
export function normalizeConfig(config) {
|
|
132
132
|
return {
|
|
133
133
|
...config,
|
|
134
134
|
timeoutMs: parsePositiveInt(config.timeoutMs, DEFAULT_TIMEOUT_MS),
|
|
@@ -136,14 +136,14 @@ export function normalizeConfig(config) {
|
|
|
136
136
|
};
|
|
137
137
|
}
|
|
138
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
|
-
}
|
|
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
147
|
const { token: _legacyToken, ...rest } = config;
|
|
148
148
|
const auth = config.auth
|
|
149
149
|
? {
|
|
@@ -166,11 +166,11 @@ function sanitizeConfig(config) {
|
|
|
166
166
|
...rest,
|
|
167
167
|
auth,
|
|
168
168
|
};
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export function sameBaseUrl(left, right) {
|
|
172
|
-
return (left || "").replace(/\/+$/, "") === (right || "").replace(/\/+$/, "");
|
|
173
|
-
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function sameBaseUrl(left, right) {
|
|
172
|
+
return (left || "").replace(/\/+$/, "") === (right || "").replace(/\/+$/, "");
|
|
173
|
+
}
|
|
174
174
|
|
|
175
175
|
function firstNonEmpty(...values) {
|
|
176
176
|
return values.find((value) => typeof value === "string" && value.trim().length > 0)?.trim();
|
package/src/error-catalog.js
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
export const ERROR_CATALOG = [
|
|
2
|
+
{
|
|
3
|
+
code: "SKILL_INSTALL_FAILED",
|
|
4
|
+
layer: "node",
|
|
5
|
+
origin: "install-skill",
|
|
6
|
+
category: "configuration",
|
|
7
|
+
recoverableByAgent: false,
|
|
8
|
+
defaultRetryable: false,
|
|
9
|
+
pathSemantics: "Local Skill directory or installation record",
|
|
10
|
+
canonicalNextActions: ["finebi-cli install-skill --help"],
|
|
11
|
+
expectedAgentBehaviors: ["inspect the installation result and preserve local edits", "request explicit replacement before overwriting local changes"],
|
|
12
|
+
benchmarkTags: ["skills", "installation"],
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
code: "BROWSER_AUTH_URL_INVALID",
|
|
4
16
|
layer: "node",
|