@finebi/finebi-cli 1.2.1 → 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 +63 -39
  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,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
+ }
@@ -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",
package/src/help.js CHANGED
@@ -99,6 +99,20 @@ function findResource(manifest, resourceName) {
99
99
  }
100
100
 
101
101
  const BUILT_IN_RESOURCES = {
102
+ "install-skill": {
103
+ command: "install-skill",
104
+ resource: "install-skill",
105
+ summary: "Install or update the bundled FineBI Skill in Agent directories without a server connection.",
106
+ usage: "finebi-cli install-skill [--agents <ids>] [--replace]",
107
+ options: [
108
+ {name: "--agents", type: "string", description: "Comma-separated Agent IDs; required outside an interactive terminal."},
109
+ {name: "--list", type: "boolean", description: "List supported Agents and their installation directories without writing files."},
110
+ {name: "--replace", type: "boolean", description: "Back up and replace locally modified or untracked Skill installations."},
111
+ ],
112
+ details: ["Unmodified managed installations update automatically with a backup. Local changes are kept by default.",
113
+ "No server credentials are needed. Restart your Agent or refresh its Skills after installation."],
114
+ examples: ["finebi-cli install-skill --agents codex,cursor", "finebi-cli install-skill --list"],
115
+ },
102
116
  version: {
103
117
  command: "version",
104
118
  resource: "version",
@@ -344,6 +358,7 @@ function renderTopLevelHelpText(data) {
344
358
  "",
345
359
  "Common Commands:",
346
360
  ` ${commandLine("version")}`,
361
+ ` ${commandLine("install-skill", "--agents", "codex")}`,
347
362
  ` ${commandLine("capabilities")}`,
348
363
  ` ${commandLine("config", "get")}`,
349
364
  ` ${commandLine("config", "set", "base-url", "<baseUrl>")}`,
package/src/index.js CHANGED
@@ -18,6 +18,7 @@ import {defaultConfigPath, maskConfig, resolveConfig, sameBaseUrl, updateConfigV
18
18
  import {parseConnectionArgs, ProfileError, profileCapabilityPath, readProfile, readProfiles, removeProfile, saveProfile, selectProfile, useProfile, withProfileLoginLock} from "./profiles.js";
19
19
  import {loginWithPassword, resolveLoginInputFromEnv} from "./auth.js";
20
20
  import {executeDynamicCommand} from "./dynamic-command.js";
21
+ import {handleInstallSkill} from "./install-skill.js";
21
22
  import {cliError} from "./errors.js";
22
23
  import {
23
24
  renderBuiltInCommandHelp,
@@ -44,6 +45,9 @@ export async function runCli(argv, context = {}) {
44
45
  try {
45
46
  const {args, flags} = parseConnectionArgs(argv);
46
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
+ }
47
51
  const management = args[0] === "auth" && ["list", "use", "remove"].includes(args[1]);
48
52
  const builtInHelp = isHelpFlagInvocation(args) && (args.length === 1 || isBuiltInResource(args[0]));
49
53
  const optional = management || builtInHelp || args.length === 0 || args[0] === "version";
@@ -671,7 +675,8 @@ async function handleHelp(args, context) {
671
675
  }
672
676
 
673
677
  function isBuiltInResource(resourceName) {
674
- return resourceName === "config"
678
+ return resourceName === "install-skill"
679
+ || resourceName === "config"
675
680
  || resourceName === "auth"
676
681
  || resourceName === "capability"
677
682
  || resourceName === "version"
@@ -0,0 +1,87 @@
1
+ import {mkdir, readFile, writeFile, rename, open, unlink} from "node:fs/promises";
2
+ import {existsSync} from "node:fs";
3
+ import {homedir} from "node:os";
4
+ import {join, resolve} from "node:path";
5
+ import {fileURLToPath} from "node:url";
6
+ import {createInterface} from "node:readline/promises";
7
+ import {agentCatalog, installSkills} from "./skill-files.js";
8
+ import {cliError} from "./errors.js";
9
+ import {failure, success} from "./output.js";
10
+
11
+ function parseOptions(args) {
12
+ const options = {};
13
+ for (let i = 0; i < args.length; i++) {
14
+ const arg = args[i];
15
+ if (arg === "--json") continue;
16
+ if (arg === "--list") options.list = true;
17
+ else if (arg === "--replace") options.replace = true;
18
+ else if (arg === "--agents") {
19
+ if (!args[i + 1] || args[i + 1].startsWith("--")) throw new Error("--agents requires comma-separated Agent IDs.");
20
+ options.agents = [...new Set(args[++i].split(",").map(id => id.trim()).filter(Boolean))];
21
+ if (!options.agents.length) throw new Error("--agents requires at least one Agent ID.");
22
+ } else throw new Error(`Unknown install-skill option: ${arg}`);
23
+ }
24
+ if (options.list && (options.agents || options.replace)) throw new Error("--list cannot be combined with installation options.");
25
+ return options;
26
+ }
27
+
28
+ export async function handleInstallSkill(args, context = {}) {
29
+ const env = context.env || process.env;
30
+ const home = resolve(env.USERPROFILE || env.HOME || homedir());
31
+ const catalog = agentCatalog({home, env});
32
+ let options;
33
+ try {
34
+ options = parseOptions(args);
35
+ if (options.list) return success("install-skill", {agents: catalog});
36
+ if (!options.agents) {
37
+ const interactive = context.isTTY ?? (process.stdin.isTTY && process.stderr.isTTY);
38
+ if (!interactive) throw new Error("Specify --agents <ids>; use --list to see supported Agents.");
39
+ const prompt = createInterface({input: process.stdin, output: process.stderr});
40
+ try {
41
+ process.stderr.write(catalog.map(a => `${a.id}: ${a.name}${a.detected ? " (detected)" : ""}`).join("\n") + "\n");
42
+ const answer = await prompt.question("Select Agent IDs (comma-separated): ");
43
+ options.agents = parseOptions(["--agents", answer]).agents;
44
+ } finally { prompt.close(); }
45
+ }
46
+ for (const id of options.agents) if (!catalog.some(agent => agent.id === id)) throw new Error(`Unknown Agent: ${id}`);
47
+ } catch (error) {
48
+ return failure("install-skill", cliError("INVALID_ARGUMENT", {
49
+ message: error.message, nextActions: ["finebi-cli install-skill --help"],
50
+ }));
51
+ }
52
+
53
+ const directory = join(home, ".finebi-cli");
54
+ const statePath = join(directory, "installation.json");
55
+ const lockPath = join(directory, "skill-install.lock");
56
+ let lock;
57
+ try {
58
+ const npmSource = fileURLToPath(new URL("../skills/finebi-cli-agent/", import.meta.url));
59
+ const source = context.skillSource || (existsSync(join(npmSource, "SKILL.md"))
60
+ ? npmSource : fileURLToPath(new URL("../../../", import.meta.url)));
61
+ await readFile(join(source, "SKILL.md"));
62
+ await readFile(join(source, "scripts/finebi-cli/src/index.js"));
63
+ await mkdir(directory, {recursive: true});
64
+ try { lock = await open(lockPath, "wx"); }
65
+ catch (error) { if (error.code === "EEXIST") throw new Error(`Skill installation lock exists: ${lockPath}`); throw error; }
66
+ let state = {};
67
+ try { state = JSON.parse(await readFile(statePath, "utf8")); }
68
+ catch (error) { if (error.code !== "ENOENT") throw error; }
69
+ const results = await installSkills({source, home, env, agents: options.agents, replace: options.replace, previous: state.skills || []});
70
+ const skills = new Map((state.skills || []).map(skill => [skill.agent, skill]));
71
+ for (const skill of results) skills.set(skill.agent, {...skills.get(skill.agent), ...skill});
72
+ const stagedState = join(directory, "installation.json.stage");
73
+ await writeFile(stagedState, `${JSON.stringify({...state, skills: [...skills.values()], updatedAt: new Date().toISOString()}, null, 2)}\n`);
74
+ await rename(stagedState, statePath);
75
+ const data = {skills: results.map(({hashes, ...skill}) => skill)};
76
+ if (results.some(skill => !["installed", "unchanged"].includes(skill.status))) {
77
+ return failure("install-skill", cliError("SKILL_INSTALL_FAILED", {
78
+ message: "Some Skills were kept or failed. Review the results; --replace backs up and replaces local changes.", partialResult: data,
79
+ }));
80
+ }
81
+ return success("install-skill", data);
82
+ } catch (error) {
83
+ return failure("install-skill", cliError("SKILL_INSTALL_FAILED", {message: error.message}));
84
+ } finally {
85
+ if (lock) { await lock.close(); await unlink(lockPath); }
86
+ }
87
+ }