@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,265 @@
|
|
|
1
|
+
import {readFileSync, statSync} from "node:fs";
|
|
2
|
+
|
|
3
|
+
import {cliError} from "./errors.js";
|
|
4
|
+
import {validateSchemaValue} from "./schema-validator.js";
|
|
5
|
+
|
|
6
|
+
export function buildRequestBody(schema, args) {
|
|
7
|
+
const parsed = parseArgs(args);
|
|
8
|
+
if (!parsed.ok) {
|
|
9
|
+
return parsed;
|
|
10
|
+
}
|
|
11
|
+
const properties = schema.properties || {};
|
|
12
|
+
const argumentFields = Object.keys(properties).filter((fieldName) => properties[fieldName].input === "argument");
|
|
13
|
+
if (parsed.positionals.length > argumentFields.length) {
|
|
14
|
+
return invalidArgument(parsed.positionals[argumentFields.length], `Unexpected argument: ${parsed.positionals[argumentFields.length]}.`);
|
|
15
|
+
}
|
|
16
|
+
const body = {};
|
|
17
|
+
for (let index = 0; index < argumentFields.length; index += 1) {
|
|
18
|
+
const fieldName = argumentFields[index];
|
|
19
|
+
if (parsed.positionals[index] !== undefined) {
|
|
20
|
+
const coerced = coerceValue(fieldName, parsed.positionals[index], properties[fieldName]);
|
|
21
|
+
if (!coerced.ok) {
|
|
22
|
+
return coerced;
|
|
23
|
+
}
|
|
24
|
+
body[fieldName] = coerced.value;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
for (const fieldName of Object.keys(body)) {
|
|
28
|
+
const validation = validateValue(fieldName, body[fieldName], properties[fieldName]);
|
|
29
|
+
if (!validation.ok) {
|
|
30
|
+
return validation;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
for (const fieldName of Object.keys(properties)) {
|
|
34
|
+
const property = properties[fieldName];
|
|
35
|
+
const optionName = optionNameFor(fieldName, property);
|
|
36
|
+
if (property.input === "argument" && body[fieldName] !== undefined) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (Object.prototype.hasOwnProperty.call(parsed.options, optionName)) {
|
|
40
|
+
const coerced = coerceValue(fieldName, parsed.options[optionName], property);
|
|
41
|
+
if (!coerced.ok) {
|
|
42
|
+
return coerced;
|
|
43
|
+
}
|
|
44
|
+
body[fieldName] = coerced.value;
|
|
45
|
+
} else {
|
|
46
|
+
const defaultValue = property.defaultValue ?? property.default;
|
|
47
|
+
if (defaultValue !== undefined && (defaultValue !== "" || property.hasDefault === true)) {
|
|
48
|
+
const coerced = coerceValue(fieldName, defaultValue, property);
|
|
49
|
+
if (!coerced.ok) {
|
|
50
|
+
return coerced;
|
|
51
|
+
}
|
|
52
|
+
body[fieldName] = coerced.value;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (body[fieldName] !== undefined) {
|
|
56
|
+
const validation = validateValue(fieldName, body[fieldName], property);
|
|
57
|
+
if (!validation.ok) {
|
|
58
|
+
return validation;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
for (const optionName of Object.keys(parsed.options)) {
|
|
63
|
+
if (!Object.keys(properties).some((fieldName) => optionNameFor(fieldName, properties[fieldName]) === optionName)) {
|
|
64
|
+
return invalidArgument(`--${optionName}`, `Unknown option: --${optionName}.`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
for (const requiredField of schema.required || []) {
|
|
68
|
+
if (body[requiredField] === undefined) {
|
|
69
|
+
return invalidArgument(requiredField, `Missing required option: --${camelToKebab(requiredField)}.`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
ok: true,
|
|
74
|
+
body,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function parseArgs(args) {
|
|
79
|
+
const options = {};
|
|
80
|
+
const positionals = [];
|
|
81
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
82
|
+
const current = args[index];
|
|
83
|
+
if (!current.startsWith("--")) {
|
|
84
|
+
positionals.push(current);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const optionName = current.slice(2);
|
|
88
|
+
const next = args[index + 1];
|
|
89
|
+
if (next === undefined || next.startsWith("--")) {
|
|
90
|
+
options[optionName] = true;
|
|
91
|
+
} else {
|
|
92
|
+
options[optionName] = next;
|
|
93
|
+
index += 1;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
ok: true,
|
|
98
|
+
options,
|
|
99
|
+
positionals,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function coerceValue(fieldName, value, property) {
|
|
104
|
+
if (property.input === "file" || property.file) {
|
|
105
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
106
|
+
return invalidArgument(fieldName, `Expected a file path for ${fieldName}.`);
|
|
107
|
+
}
|
|
108
|
+
const contentType = fileContentType(property);
|
|
109
|
+
if (contentType === "BINARY") {
|
|
110
|
+
try {
|
|
111
|
+
if (!statSync(value).isFile()) {
|
|
112
|
+
return invalidArgument(fieldName, `Expected a regular file for ${fieldName}.`);
|
|
113
|
+
}
|
|
114
|
+
} catch (error) {
|
|
115
|
+
return invalidArgument(fieldName, `Failed to read BINARY file for ${fieldName}: ${error.message}`);
|
|
116
|
+
}
|
|
117
|
+
return {ok: true, value};
|
|
118
|
+
}
|
|
119
|
+
let contents;
|
|
120
|
+
try {
|
|
121
|
+
contents = readFileSync(value, "utf8");
|
|
122
|
+
} catch (error) {
|
|
123
|
+
return invalidArgument(fieldName, `Failed to read ${contentType} file for ${fieldName}: ${error.message}`);
|
|
124
|
+
}
|
|
125
|
+
// TEXT 是不可信源码的透明传输协议;CLI 不执行,也不做 JSON/schema 解释。
|
|
126
|
+
if (fileContentType(property) === "TEXT") {
|
|
127
|
+
return { ok: true, value: contents };
|
|
128
|
+
}
|
|
129
|
+
let parsed;
|
|
130
|
+
try {
|
|
131
|
+
parsed = JSON.parse(contents);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
return invalidJson(fieldName, `Invalid JSON syntax in file for ${fieldName}: ${error.message}`);
|
|
134
|
+
}
|
|
135
|
+
if (property.schema) {
|
|
136
|
+
const validation = validateSchemaValue(parsed, property.schema, fieldName);
|
|
137
|
+
if (!validation.ok) {
|
|
138
|
+
return validation;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return { ok: true, value: parsed };
|
|
142
|
+
}
|
|
143
|
+
const type = property.type;
|
|
144
|
+
if (type === "array") {
|
|
145
|
+
let parsed = value;
|
|
146
|
+
if (typeof value === "string") {
|
|
147
|
+
try {
|
|
148
|
+
parsed = JSON.parse(value);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
return invalidArgument(fieldName, `Expected a JSON array for ${fieldName}.`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const validation = validateSchemaValue(parsed, property, fieldName);
|
|
154
|
+
return validation.ok ? {ok: true, value: parsed} : validation;
|
|
155
|
+
}
|
|
156
|
+
if (type === "integer") {
|
|
157
|
+
if (typeof value === "number" && Number.isInteger(value)) {
|
|
158
|
+
return { ok: true, value };
|
|
159
|
+
}
|
|
160
|
+
if (typeof value !== "string" || !/^-?\d+$/.test(value.trim())) {
|
|
161
|
+
return invalidArgument(fieldName, `Expected an integer value for ${fieldName}.`);
|
|
162
|
+
}
|
|
163
|
+
return { ok: true, value: Number.parseInt(value, 10) };
|
|
164
|
+
}
|
|
165
|
+
if (type === "number") {
|
|
166
|
+
const numberValue = Number(value);
|
|
167
|
+
if (!Number.isFinite(numberValue)) {
|
|
168
|
+
return invalidArgument(fieldName, `Expected a numeric value for ${fieldName}.`);
|
|
169
|
+
}
|
|
170
|
+
return { ok: true, value: numberValue };
|
|
171
|
+
}
|
|
172
|
+
if (type === "boolean") {
|
|
173
|
+
if (typeof value === "boolean") {
|
|
174
|
+
return { ok: true, value };
|
|
175
|
+
}
|
|
176
|
+
return { ok: true, value: value === "true" };
|
|
177
|
+
}
|
|
178
|
+
if (type === "string" && typeof value !== "string") {
|
|
179
|
+
return invalidArgument(fieldName, `Expected a string value for ${fieldName}.`);
|
|
180
|
+
}
|
|
181
|
+
const enumValues = property.enumValues || property.enum || [];
|
|
182
|
+
if (type === "string" && enumValues.length > 0 && typeof value === "string") {
|
|
183
|
+
const matched = enumValues.find((enumValue) => enumValue.toLowerCase() === value.toLowerCase());
|
|
184
|
+
if (matched !== undefined) {
|
|
185
|
+
return { ok: true, value: matched };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return { ok: true, value };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function fileContentType(property) {
|
|
192
|
+
return String(property.fileContentType || "JSON").toUpperCase();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function validateValue(fieldName, value, property) {
|
|
196
|
+
if ((property.type === "integer" || property.type === "number") && !Number.isFinite(value)) {
|
|
197
|
+
return invalidArgument(fieldName, `Invalid numeric value for ${fieldName}.`);
|
|
198
|
+
}
|
|
199
|
+
if (property.type === "integer" && !Number.isInteger(value)) {
|
|
200
|
+
return invalidArgument(fieldName, `Invalid integer value for ${fieldName}.`);
|
|
201
|
+
}
|
|
202
|
+
if (property.minimum !== undefined && property.minimum !== null && value < Number(property.minimum)) {
|
|
203
|
+
return invalidArgument(fieldName, `${fieldName} must be greater than or equal to ${property.minimum}.`);
|
|
204
|
+
}
|
|
205
|
+
if (property.maximum !== undefined && property.maximum !== null && value > Number(property.maximum)) {
|
|
206
|
+
return invalidArgument(fieldName, `${fieldName} must be less than or equal to ${property.maximum}.`);
|
|
207
|
+
}
|
|
208
|
+
if (property.type === "string" && property.minLength !== undefined && property.minLength !== null
|
|
209
|
+
&& value.length < Number(property.minLength)) {
|
|
210
|
+
return invalidArgument(fieldName, `${fieldName} must contain at least ${property.minLength} character(s).`);
|
|
211
|
+
}
|
|
212
|
+
if (property.type === "string" && property.maxLength !== undefined && property.maxLength !== null
|
|
213
|
+
&& value.length > Number(property.maxLength)) {
|
|
214
|
+
return invalidArgument(fieldName, `${fieldName} must contain at most ${property.maxLength} character(s).`);
|
|
215
|
+
}
|
|
216
|
+
if (property.type === "string" && property.pattern && !new RegExp(property.pattern).test(value)) {
|
|
217
|
+
return invalidArgument(fieldName, `${fieldName} must match ${property.pattern}.`);
|
|
218
|
+
}
|
|
219
|
+
const constValue = property.constValue ?? property.const;
|
|
220
|
+
if (constValue !== undefined && constValue !== null && value !== constValue) {
|
|
221
|
+
return invalidArgument(fieldName, `${fieldName} must equal ${JSON.stringify(constValue)}.`);
|
|
222
|
+
}
|
|
223
|
+
const enumValues = property.enumValues || property.enum || [];
|
|
224
|
+
if (enumValues.length > 0 && !enumValues.includes(value)) {
|
|
225
|
+
return invalidArgument(fieldName, `${fieldName} must be one of: ${enumValues.join(", ")}.`);
|
|
226
|
+
}
|
|
227
|
+
return { ok: true };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function invalidArgument(path, message) {
|
|
231
|
+
return {
|
|
232
|
+
ok: false,
|
|
233
|
+
error: cliError("INVALID_ARGUMENT", {
|
|
234
|
+
path,
|
|
235
|
+
message,
|
|
236
|
+
hint: "Run the command with --help to check supported options.",
|
|
237
|
+
retryable: false,
|
|
238
|
+
nextActions: ["finebi-cli --help"],
|
|
239
|
+
}),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function invalidJson(path, message) {
|
|
244
|
+
return {
|
|
245
|
+
ok: false,
|
|
246
|
+
error: cliError("INVALID_JSON", {
|
|
247
|
+
path,
|
|
248
|
+
message,
|
|
249
|
+
hint: "Fix the JSON syntax and rerun the command.",
|
|
250
|
+
retryable: false,
|
|
251
|
+
nextActions: ["finebi-cli --help"],
|
|
252
|
+
}),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function camelToKebab(value) {
|
|
257
|
+
return value.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function optionNameFor(fieldName, property) {
|
|
261
|
+
if (property.input === "file" || property.file) {
|
|
262
|
+
return property.option || "file";
|
|
263
|
+
}
|
|
264
|
+
return camelToKebab(fieldName);
|
|
265
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import {cliError} from "./errors.js";
|
|
2
|
+
|
|
3
|
+
export function validateSchemaValue(value, schema, path) {
|
|
4
|
+
const effectiveSchema = schema || {};
|
|
5
|
+
// nullable 是 capability 显式声明的字段级约束;required 只控制字段是否必须出现。
|
|
6
|
+
if (value === null && effectiveSchema.nullable === true) {
|
|
7
|
+
return { ok: true };
|
|
8
|
+
}
|
|
9
|
+
const typeError = validateType(value, effectiveSchema, path);
|
|
10
|
+
if (typeError) {
|
|
11
|
+
return typeError;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (effectiveSchema.discriminator && Array.isArray(effectiveSchema.variants)) {
|
|
15
|
+
return validateDiscriminator(value, effectiveSchema, path);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (effectiveSchema.type === "object") {
|
|
19
|
+
return validateObject(value, effectiveSchema, path);
|
|
20
|
+
}
|
|
21
|
+
if (effectiveSchema.type === "array") {
|
|
22
|
+
return validateArray(value, effectiveSchema, path);
|
|
23
|
+
}
|
|
24
|
+
return validateScalar(value, effectiveSchema, path);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function validateType(value, schema, path) {
|
|
28
|
+
switch (schema.type) {
|
|
29
|
+
case "object":
|
|
30
|
+
if (!isPlainObject(value)) {
|
|
31
|
+
return invalidRequestJson(path, `Expected object at "${path}".`);
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
case "array":
|
|
35
|
+
if (!Array.isArray(value)) {
|
|
36
|
+
return invalidRequestJson(path, `Expected array at "${path}".`);
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
case "string":
|
|
40
|
+
if (typeof value !== "string") {
|
|
41
|
+
return invalidRequestJson(path, `Expected string at "${path}".`);
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
case "integer":
|
|
45
|
+
if (!Number.isInteger(value)) {
|
|
46
|
+
return invalidRequestJson(path, `Expected integer at "${path}".`);
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
case "number":
|
|
50
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
51
|
+
return invalidRequestJson(path, `Expected number at "${path}".`);
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
case "boolean":
|
|
55
|
+
if (typeof value !== "boolean") {
|
|
56
|
+
return invalidRequestJson(path, `Expected boolean at "${path}".`);
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
default:
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function validateDiscriminator(value, schema, path) {
|
|
65
|
+
const discriminator = schema.discriminator;
|
|
66
|
+
const discriminatorPath = joinPath(path, discriminator);
|
|
67
|
+
if (!Object.prototype.hasOwnProperty.call(value, discriminator)) {
|
|
68
|
+
return invalidRequestJson(discriminatorPath, `Missing discriminator field "${discriminator}".`);
|
|
69
|
+
}
|
|
70
|
+
const discriminatorValue = value[discriminator];
|
|
71
|
+
const variant = schema.variants.find((item) => item.value === discriminatorValue);
|
|
72
|
+
if (!variant) {
|
|
73
|
+
return invalidRequestJson(
|
|
74
|
+
discriminatorPath,
|
|
75
|
+
`Unsupported discriminator value "${discriminatorValue}" for "${discriminator}". Expected one of: ${schema.variants.map((item) => item.value).join(", ")}.`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const validation = validateSchemaValue(value, variant.schema, path);
|
|
79
|
+
if (!validation.ok && validation.error.message.startsWith("Unsupported field ")) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
error: {
|
|
83
|
+
...validation.error,
|
|
84
|
+
message: `${validation.error.message.slice(0, -1)} for discriminator "${discriminator}" value "${discriminatorValue}".`,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return validation;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function validateObject(value, schema, path) {
|
|
92
|
+
const properties = schema.properties || {};
|
|
93
|
+
for (const requiredField of schema.required || []) {
|
|
94
|
+
if (!Object.prototype.hasOwnProperty.call(value, requiredField)) {
|
|
95
|
+
return invalidRequestJson(joinPath(path, requiredField), `Missing required field "${requiredField}".`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (Object.keys(properties).length > 0) {
|
|
99
|
+
for (const fieldName of Object.keys(value)) {
|
|
100
|
+
if (!Object.prototype.hasOwnProperty.call(properties, fieldName)) {
|
|
101
|
+
return invalidRequestJson(joinPath(path, fieldName), `Unsupported field "${fieldName}".`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
for (const fieldName of Object.keys(properties)) {
|
|
106
|
+
if (!Object.prototype.hasOwnProperty.call(value, fieldName)) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const validation = validateSchemaValue(value[fieldName], nestedSchema(properties[fieldName]), joinPath(path, fieldName));
|
|
110
|
+
if (!validation.ok) {
|
|
111
|
+
return validation;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return { ok: true };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function validateArray(value, schema, path) {
|
|
118
|
+
if (schema.minItems !== undefined && schema.minItems !== null && value.length < Number(schema.minItems)) {
|
|
119
|
+
return invalidRequestJson(path, `"${path}" must contain at least ${schema.minItems} item(s).`);
|
|
120
|
+
}
|
|
121
|
+
const itemSchema = schema.items;
|
|
122
|
+
if (!itemSchema) {
|
|
123
|
+
return { ok: true };
|
|
124
|
+
}
|
|
125
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
126
|
+
const validation = validateSchemaValue(value[index], nestedSchema(itemSchema), `${path}[${index}]`);
|
|
127
|
+
if (!validation.ok) {
|
|
128
|
+
return validation;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return { ok: true };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function validateScalar(value, schema, path) {
|
|
135
|
+
const constValue = schema.constValue ?? schema.const;
|
|
136
|
+
if (constValue !== undefined && constValue !== null && value !== constValue) {
|
|
137
|
+
return invalidRequestJson(path, `"${path}" must equal ${JSON.stringify(constValue)}.`);
|
|
138
|
+
}
|
|
139
|
+
const enumValues = schema.enumValues || schema.enum || [];
|
|
140
|
+
if (enumValues.length > 0 && !enumValues.includes(value)) {
|
|
141
|
+
return invalidRequestJson(path, `"${path}" must be one of: ${enumValues.join(", ")}.`);
|
|
142
|
+
}
|
|
143
|
+
if ((schema.type === "integer" || schema.type === "number") && schema.minimum !== undefined && schema.minimum !== null && value < Number(schema.minimum)) {
|
|
144
|
+
return invalidRequestJson(path, `"${path}" must be greater than or equal to ${schema.minimum}.`);
|
|
145
|
+
}
|
|
146
|
+
if ((schema.type === "integer" || schema.type === "number") && schema.maximum !== undefined && schema.maximum !== null && value > Number(schema.maximum)) {
|
|
147
|
+
return invalidRequestJson(path, `"${path}" must be less than or equal to ${schema.maximum}.`);
|
|
148
|
+
}
|
|
149
|
+
if (schema.type === "string" && schema.pattern && !new RegExp(schema.pattern).test(value)) {
|
|
150
|
+
return invalidRequestJson(path, `"${path}" must match pattern ${schema.pattern}.`);
|
|
151
|
+
}
|
|
152
|
+
if (schema.type === "string" && schema.minLength !== undefined && schema.minLength !== null
|
|
153
|
+
&& value.length < Number(schema.minLength)) {
|
|
154
|
+
return invalidRequestJson(path, `"${path}" must contain at least ${schema.minLength} character(s).`);
|
|
155
|
+
}
|
|
156
|
+
if (schema.type === "string" && schema.maxLength !== undefined && schema.maxLength !== null
|
|
157
|
+
&& value.length > Number(schema.maxLength)) {
|
|
158
|
+
return invalidRequestJson(path, `"${path}" must contain at most ${schema.maxLength} character(s).`);
|
|
159
|
+
}
|
|
160
|
+
return { ok: true };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function nestedSchema(property) {
|
|
164
|
+
if (!property.schema) {
|
|
165
|
+
return property;
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
...property.schema,
|
|
169
|
+
nullable: property.nullable ?? property.schema.nullable,
|
|
170
|
+
discriminator: property.discriminator ?? property.schema.discriminator,
|
|
171
|
+
variants: property.variants ?? property.schema.variants,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function joinPath(base, fieldName) {
|
|
176
|
+
return base ? `${base}.${fieldName}` : fieldName;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function isPlainObject(value) {
|
|
180
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function invalidRequestJson(path, message) {
|
|
184
|
+
return {
|
|
185
|
+
ok: false,
|
|
186
|
+
error: cliError("INVALID_REQUEST_JSON", {
|
|
187
|
+
path,
|
|
188
|
+
message,
|
|
189
|
+
hint: "Remove unsupported fields or check the command schema.",
|
|
190
|
+
retryable: false,
|
|
191
|
+
nextActions: ["finebi-cli --help"],
|
|
192
|
+
}),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {createHash, randomUUID} from "node:crypto";
|
|
2
|
+
import {existsSync} from "node:fs";
|
|
3
|
+
import {cp, mkdir, lstat, mkdtemp, readFile, readdir, rename, rm} from "node:fs/promises";
|
|
4
|
+
import {homedir} from "node:os";
|
|
5
|
+
import {join, dirname, resolve} from "node:path";
|
|
6
|
+
|
|
7
|
+
export function agentCatalog({home = homedir(), env = process.env} = {}) {
|
|
8
|
+
const config = env.XDG_CONFIG_HOME || join(home, ".config");
|
|
9
|
+
return [
|
|
10
|
+
["claude-code", "Claude Code", env.CLAUDE_CONFIG_DIR || join(home, ".claude")],
|
|
11
|
+
["codex", "Codex", env.CODEX_HOME || join(home, ".codex")],
|
|
12
|
+
["cursor", "Cursor", join(home, ".cursor")],
|
|
13
|
+
["gemini-cli", "Gemini CLI", join(home, ".gemini")],
|
|
14
|
+
["github-copilot", "GitHub Copilot", join(home, ".copilot")],
|
|
15
|
+
["opencode", "OpenCode", join(config, "opencode")],
|
|
16
|
+
["cline", "Cline", join(home, ".agents")],
|
|
17
|
+
["windsurf", "Windsurf", join(home, ".codeium", "windsurf")],
|
|
18
|
+
["trae", "Trae", join(home, ".trae")],
|
|
19
|
+
["trae-cn", "Trae CN", join(home, ".trae-cn")],
|
|
20
|
+
["openclaw", "OpenClaw", [".openclaw", ".clawdbot", ".moltbot"].map(p => join(home, p)).find(existsSync) || join(home, ".openclaw")],
|
|
21
|
+
["roo", "Roo Code", join(home, ".roo")],
|
|
22
|
+
["qwen-code", "Qwen Code", join(home, ".qwen")],
|
|
23
|
+
].map(([id, name, root]) => ({id, name, directory: join(root, "skills"), detected: existsSync(root)}));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function hashTree(root, prefix = "") {
|
|
27
|
+
const files = {};
|
|
28
|
+
for (const entry of (await readdir(join(root, prefix), {withFileTypes: true})).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
29
|
+
const name = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
30
|
+
if (entry.isSymbolicLink()) throw new Error(`Skill contains a symbolic link: ${name}`);
|
|
31
|
+
if (entry.isDirectory()) Object.assign(files, await hashTree(root, name));
|
|
32
|
+
else files[name] = createHash("sha256").update(await readFile(join(root, name))).digest("hex");
|
|
33
|
+
}
|
|
34
|
+
return files;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function sameHashes(left, right) {
|
|
38
|
+
return left && right && Object.keys(left).length === Object.keys(right).length
|
|
39
|
+
&& Object.keys(left).every(key => left[key] === right[key]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function installSkills({source, home, env, agents, replace = false, previous = []}) {
|
|
43
|
+
const catalog = agentCatalog({home, env});
|
|
44
|
+
const selected = agents.map(id => {
|
|
45
|
+
const agent = catalog.find(item => item.id === id);
|
|
46
|
+
if (!agent) throw new Error(`Unknown Agent: ${id}`);
|
|
47
|
+
return agent;
|
|
48
|
+
});
|
|
49
|
+
const sourceHashes = await hashTree(source);
|
|
50
|
+
const results = [];
|
|
51
|
+
for (const agent of selected) {
|
|
52
|
+
const target = join(agent.directory, "finebi-cli-agent");
|
|
53
|
+
let backup;
|
|
54
|
+
let candidate;
|
|
55
|
+
try {
|
|
56
|
+
const stat = await lstat(target).catch(error => { if (error.code !== "ENOENT") throw error; });
|
|
57
|
+
if (stat) {
|
|
58
|
+
if (stat.isSymbolicLink()) throw new Error("Skill target is a symbolic link; refusing to replace it.");
|
|
59
|
+
const actual = await hashTree(target);
|
|
60
|
+
if (sameHashes(sourceHashes, actual)) {
|
|
61
|
+
results.push({agent: agent.id, path: target, status: "unchanged", hashes: sourceHashes});
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const recorded = previous.find(item => item.agent === agent.id && item.path === target)?.hashes;
|
|
65
|
+
if (!replace && !sameHashes(recorded, actual)) {
|
|
66
|
+
results.push({agent: agent.id, path: target, status: "kept"});
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
backup = join(home, ".finebi-cli", "skill-backups", `${agent.id}-${randomUUID()}`);
|
|
70
|
+
await mkdir(dirname(backup), {recursive: true});
|
|
71
|
+
await cp(target, backup, {recursive: true, errorOnExist: true, force: false});
|
|
72
|
+
if (!sameHashes(actual, await hashTree(backup))) throw new Error("Skill backup integrity check failed.");
|
|
73
|
+
}
|
|
74
|
+
await mkdir(agent.directory, {recursive: true});
|
|
75
|
+
candidate = await mkdtemp(join(agent.directory, ".finebi-cli-stage-"));
|
|
76
|
+
await cp(source, candidate, {recursive: true});
|
|
77
|
+
if (!sameHashes(sourceHashes, await hashTree(candidate))) throw new Error("Skill copy integrity check failed.");
|
|
78
|
+
const old = `${candidate}-old`;
|
|
79
|
+
if (stat) await rename(target, old);
|
|
80
|
+
try { await rename(candidate, target); }
|
|
81
|
+
catch (error) { if (stat) await rename(old, target); throw error; }
|
|
82
|
+
candidate = undefined;
|
|
83
|
+
// Delete only the relocated, backed-up directory inside this Agent's skills directory.
|
|
84
|
+
if (stat && backup && dirname(resolve(old)) === resolve(agent.directory)) await rm(old, {recursive: true});
|
|
85
|
+
if (!sameHashes(sourceHashes, await hashTree(target))) throw new Error("Installed Skill integrity check failed.");
|
|
86
|
+
results.push({agent: agent.id, path: target, status: "installed", hashes: sourceHashes, ...(backup ? {backup} : {})});
|
|
87
|
+
} catch (error) {
|
|
88
|
+
results.push({agent: agent.id, path: target, status: "failed", error: error.message, ...(backup ? {backup} : {})});
|
|
89
|
+
} finally {
|
|
90
|
+
if (candidate && dirname(resolve(candidate)) === resolve(agent.directory)) await rm(candidate, {recursive: true, force: true});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return results;
|
|
94
|
+
}
|