@finebi/finebi-cli 1.4.0 → 1.5.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 +25 -1
- package/package.json +2 -2
- package/skills/finebi-cli-agent/references/command-workflows.md +54 -4
- package/skills/finebi-cli-agent/references/dashboard-data-result.md +219 -0
- package/skills/finebi-cli-agent/references/dashboard-dsl-api.md +24 -7
- package/skills/finebi-cli-agent/references/file-to-dashboard.md +1 -1
- package/skills/finebi-cli-agent/references/handoff-contract.md +2 -0
- package/skills/finebi-cli-agent/references/mapping-rules.md +1 -1
- package/skills/finebi-cli-agent/references/widget-dsl-api-examples.md +18 -0
- package/skills/finebi-cli-agent/references/widget-dsl-api.md +43 -5
- package/skills/finebi-cli-agent/scripts/finebi-cli/package.json +1 -1
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/config.js +27 -27
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/help.js +65 -57
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/install-skill.js +89 -89
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/profiles.js +158 -158
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/schema-options.js +44 -8
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/skill-files.js +94 -94
- package/skills/finebi-cli-agent/scripts/finebi-cli-runner.mjs +4 -4
- package/src/config.js +27 -27
- package/src/help.js +65 -57
- package/src/install-skill.js +89 -89
- package/src/profiles.js +158 -158
- package/src/schema-options.js +44 -8
- package/src/skill-files.js +94 -94
|
@@ -145,8 +145,12 @@ windowCalculation(
|
|
|
145
145
|
|
|
146
146
|
`windowCalculation(measure, configuration)` 是全局函数;`WindowCalculationModel` 是窗口配置命名空间。
|
|
147
147
|
仅标准分析主题可使用移动计算;自助分析主题会在保存前报错,此限制同样适用于记录数和隐藏通道。
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
组件形态需与设计器可达面一致,否则报错:组件至少要有一个维度;`calculateType` 组内要求
|
|
149
|
+
表格某一轴(行/列)或图表多于一个维度;图表方向只能是 `0`(向下,设计器仅提供「默认」);
|
|
150
|
+
表格方向按行/列区维度开放——`1` 向右需要列维度,`2`/`3` 组合方向要求行、列维度都有,
|
|
151
|
+
且至少要有一个维度。上述例子计算前一行与当前行的合计。计算作用于组件聚合后的结果行;
|
|
152
|
+
窗口顺序取决于组件排序,按日期移动时应使用日期维度并核对排序与出数。
|
|
153
|
+
原指标可另放一列,逐行核对窗口计算值。
|
|
150
154
|
|
|
151
155
|
| `WindowCalculationModel.of` 参数 | 可用值 |
|
|
152
156
|
|---|---|
|
|
@@ -179,6 +183,7 @@ windowCalculation(
|
|
|
179
183
|
| `from("来源表").dim("字段名", grouping, "别名")` | 同时指定分组和展示名。 |
|
|
180
184
|
| `measureID("配置 ID").dim()` | 已保存计算字段使用默认分组。 |
|
|
181
185
|
| `measureID("配置 ID").dim(grouping)` | 已保存计算字段使用与字段类型兼容的显式分组;不支持别名。 |
|
|
186
|
+
| `....hidden()` | 关闭展示字段:维度仍绑定进轴、保留槽位,查看态不展示(`used=false`);可叠加在上述任意维度形态之后,与分组/别名/经纬度组合。 |
|
|
182
187
|
|
|
183
188
|
分组使用以下稳定语法:
|
|
184
189
|
|
|
@@ -211,6 +216,14 @@ DimensionGrouping.textGroups([
|
|
|
211
216
|
孤立载荷、默认文本 ID_GROUP / 默认日期 YMD 维度旁的 stray customGroup)会作为 warning 返回;
|
|
212
217
|
`cal[*]` 嵌套 customGroup 始终 warning。
|
|
213
218
|
|
|
219
|
+
`.hidden()` 对应设计器"展示字段"开关,回读保留(输出为维度表达式末尾的 `.hidden()` 后缀)。
|
|
220
|
+
仅视图轴绑定的维度生效:filter 字段、钻取层级、迷你图依赖、KPI 通道等非展示面引用带
|
|
221
|
+
`.hidden()` 会报错或无效果(KPI 通道维度明确拒绝)。同一字段在同一组件内多处引用须保持
|
|
222
|
+
一致 hidden 状态,否则保存报错——明细表例外:同字段多列按列独立生效,可一列隐藏一列展示;
|
|
223
|
+
分类轴(`categoryAxis`)目标引用须重复携带一致的 `.hidden()` 后缀才能命中轴上维度。
|
|
224
|
+
指标名维度(`metricNameInRow/InCol` 与自动指标名)不支持隐藏,设计器侧关闭时回读会以
|
|
225
|
+
warning 留痕。
|
|
226
|
+
|
|
214
227
|
### 2.2 聚合指标
|
|
215
228
|
|
|
216
229
|
| DSL API | 业务含义 |
|
|
@@ -739,17 +752,29 @@ WidgetDsl.create("订单明细").chart().detailTable()
|
|
|
739
752
|
| pageColumns(n) | 交叉表每页 1–1000 列,默认 7 |
|
|
740
753
|
| sequence(boolean) | 显示序号,默认关闭 |
|
|
741
754
|
| rowInfo(boolean) | 明细表显示行数信息,默认开启 |
|
|
742
|
-
| totalRows(boolean) / totalPages(boolean) |
|
|
755
|
+
| totalRows(boolean) / totalPages(boolean) | 显示总行数/总页数,支持显式关闭;省略则继承平台查看态规则;不是合计行;仅标准分析主题可声明,显式 true 时对应行/列展开须为 DISABLED |
|
|
743
756
|
| rowExpansion(TableExpansion.DISABLED / COLLAPSED / EXPANDED) | 分组/交叉表禁止展开收起交互(全部展开)/允许交互且初始收起/允许交互且初始展开 |
|
|
744
757
|
| columnExpansion(TableExpansion.DISABLED / COLLAPSED / EXPANDED) | 交叉表列维度展开规则,同上 |
|
|
745
758
|
| freezeDimensions(boolean) | 分组/交叉表冻结维度,默认开启 |
|
|
746
759
|
| frozenColumns(n) | 明细表冻结前 1–10 列;0 关闭冻结,默认 1 |
|
|
747
760
|
|
|
748
761
|
未声明展开配置时使用 DSL 默认的可交互、初始收起;迷你图固定禁止行展开交互。
|
|
749
|
-
总行数/总页数配置入口属于 7.0.13+
|
|
762
|
+
总行数/总页数配置入口属于 7.0.13+ 标准分析主题:自助分析主题下显式声明这两个开关中任一
|
|
763
|
+
会报错(设计器不显示该开关组);标准分析主题下显式 true 要求对应行/列展开为
|
|
764
|
+
`TableExpansion.DISABLED`(交叉表行/列按钮任一开启即报错),显式 false 与省略等价始终放行。
|
|
750
765
|
这两个布尔值省略时保留继承,显式 false 会关闭显示。宽度适应时前端不执行冻结。
|
|
751
766
|
重复 tableFormat 以最后整组为准,空 Builder 恢复默认;不适用的表格专属选项直接报错。
|
|
752
767
|
|
|
768
|
+
**新建默认与设计器系统参数**:CLI 新建组件固定展开收起可交互、初始收起与总合计开启;标准分析
|
|
769
|
+
主题下设计器**新建**表格会按系统参数 `standardAnalysisSupportExpandCollapse` /
|
|
770
|
+
`standardAnalysisSelectGrandTotal`(默认均为 false)改写为「隐藏展开按钮、关闭总合计」。需要与
|
|
771
|
+
设计器新建同款默认形态时显式声明:`.tableFormat(...rowExpansion(TableExpansion.DISABLED)
|
|
772
|
+
.totalRows(false).totalPages(false)...)`(交叉表另加 `.columnExpansion(TableExpansion.DISABLED)`),
|
|
773
|
+
并加 `.totalRows(TableTotal.rows().grand(false).build())`(交叉表列合计用
|
|
774
|
+
`.totalColumns(TableTotal.columns().grand(false).build())`);这些值都不是省略默认,回读与重放稳定。
|
|
775
|
+
配方适用于分组表/交叉表;明细表不适用 `rowExpansion` 与合计配置,只需 `.tableFormat(...)`
|
|
776
|
+
声明 `totalRows(false)` 与 `totalPages(false)`。
|
|
777
|
+
|
|
753
778
|
```javascript
|
|
754
779
|
WidgetDsl.create("交叉表格式").chart().crossTable()
|
|
755
780
|
.row(from("订单表").dim("地区"))
|
|
@@ -1280,6 +1305,13 @@ currentTime 在查询时取当前时钟,time 为固定整秒时刻;`.time(""
|
|
|
1280
1305
|
| THIRD | 聚合计算过滤 | 聚合计算过滤 |
|
|
1281
1306
|
| FOURTH | 快速计算过滤 | 快速计算过滤 |
|
|
1282
1307
|
|
|
1308
|
+
未声明 `.level(...)` 时按设计器面板的默认落值写入(两主题不同):
|
|
1309
|
+
维度自身过滤与未聚合指标过滤在标准主题恒第二层、自助主题第一层(top/bottom N 时第二层);
|
|
1310
|
+
聚合指标过滤第三层、top/bottom N 时第四层;公式过滤引用指标时第二层、否则第一层。
|
|
1311
|
+
**明细表**过滤走独立编辑器,默认标准主题 22(固定明细档)、非标准主题第一层。已知差异
|
|
1312
|
+
(前端有、本实现暂未镜像,可用显式 `.level(...)` 覆盖):自助主题「数值维度/维度依赖聚合指标」
|
|
1313
|
+
项前端固定第二层、本实现落第一层。带快速计算的聚合指标过滤前端默认第四层、标准化路径
|
|
1314
|
+
不可声明(`.filter()` 的字段不接受携带快速计算)。
|
|
1283
1315
|
普通维度进入分析区且依赖分组时可调整到四层;不在分析区时仅支持 FIRST/SECOND。
|
|
1284
1316
|
自助主题的 DEF/WINDOW/FIXED 维度进入分析区后只允许 SECOND,维度依赖指标过滤允许 SECOND/THIRD/FOURTH。
|
|
1285
1317
|
标准主题的聚合输出维度和分析区内依赖指标过滤仅允许 SECOND;新增列维度仅允许 FIRST/SECOND。
|
|
@@ -1314,18 +1346,24 @@ DSL 回读保留分类绑定、色板和映射,自动主题色不输出显式
|
|
|
1314
1346
|
|
|
1315
1347
|
```js
|
|
1316
1348
|
NumberFormatModel.auto()
|
|
1349
|
+
NumberFormatModel.auto(2, NumberFormatModelScale.THOUSAND, null, true)
|
|
1317
1350
|
NumberFormatModel.number(2)
|
|
1318
|
-
NumberFormatModel.number(2, NumberFormatModelScale.TEN_THOUSAND, "
|
|
1351
|
+
NumberFormatModel.number(2, NumberFormatModelScale.TEN_THOUSAND, "元", true)
|
|
1319
1352
|
NumberFormatModel.percentage(1, true)
|
|
1320
1353
|
```
|
|
1321
1354
|
|
|
1322
1355
|
| DSL API | 业务含义 |
|
|
1323
1356
|
|---------------------------------------------------------------------|--------------------|
|
|
1324
1357
|
| `NumberFormatModel.auto()` | 使用平台自动格式。 |
|
|
1358
|
+
| `NumberFormatModel.auto(decimalPlaces, scale, unit, useGrouping)` | 自动精度并保留缩放、单位和千分位配置;decimalPlaces 可为 null,保存面板值但不强制固定小数位。 |
|
|
1325
1359
|
| `NumberFormatModel.number(decimalPlaces)` | 普通数值格式。 |
|
|
1326
1360
|
| `NumberFormatModel.number(decimalPlaces, scale, unit, useGrouping)` | 带缩放、单位和千分位控制的数值格式。 |
|
|
1327
1361
|
| `NumberFormatModel.percentage(decimalPlaces, useGrouping)` | 百分比格式。 |
|
|
1328
1362
|
|
|
1363
|
+
`scale` 同时决定缩放与数量级单位,`unit` 是追加的单位文本。例如 `TEN_THOUSAND` + `"元"`
|
|
1364
|
+
显示“万元”;只显示“万”时使用 `null`,不要重复填写 `"万"`。`number(2, ...)` 固定两位小数;
|
|
1365
|
+
带参数的 `auto(...)` 保留自动精度。只有完全默认的 `auto()` 在回读时归一为未设置。
|
|
1366
|
+
|
|
1329
1367
|
## 9. 可用枚举
|
|
1330
1368
|
|
|
1331
1369
|
| 枚举 | 可用值 |
|
|
@@ -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();
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {brandCommandValue, CLI_COMMAND_NAME, CLI_DISPLAY_NAME, commandLine} from "./branding.js";
|
|
2
2
|
import {cliError} from "./errors.js";
|
|
3
|
-
import {failure, success} from "./output.js";
|
|
4
|
-
|
|
5
|
-
const GLOBAL_OPTIONS = [
|
|
6
|
-
{name: "--profile", type: "string", description: "Environment name. Overrides FINEBI_PROFILE and the saved default; ordinary commands do not create environments or switch the default."},
|
|
7
|
-
{name: "--base-url", type: "string", description: "Connection URL override. A different server never inherits saved credentials or capabilities."},
|
|
8
|
-
{name: "--token", type: "string", description: "Temporary session token override; not persisted."},
|
|
9
|
-
];
|
|
3
|
+
import {failure, success} from "./output.js";
|
|
4
|
+
|
|
5
|
+
const GLOBAL_OPTIONS = [
|
|
6
|
+
{name: "--profile", type: "string", description: "Environment name. Overrides FINEBI_PROFILE and the saved default; ordinary commands do not create environments or switch the default."},
|
|
7
|
+
{name: "--base-url", type: "string", description: "Connection URL override. A different server never inherits saved credentials or capabilities."},
|
|
8
|
+
{name: "--token", type: "string", description: "Temporary session token override; not persisted."},
|
|
9
|
+
];
|
|
10
10
|
|
|
11
11
|
export function renderTopLevelHelp(manifest) {
|
|
12
12
|
return success("help", {
|
|
@@ -14,8 +14,8 @@ export function renderTopLevelHelp(manifest) {
|
|
|
14
14
|
name: resource.name,
|
|
15
15
|
summary: resource.summary,
|
|
16
16
|
})),
|
|
17
|
-
dynamicCapabilitiesLoaded: Boolean(manifest),
|
|
18
|
-
globalOptions: GLOBAL_OPTIONS,
|
|
17
|
+
dynamicCapabilitiesLoaded: Boolean(manifest),
|
|
18
|
+
globalOptions: GLOBAL_OPTIONS,
|
|
19
19
|
}, { schemaVersion: manifest?.schemaVersion });
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -24,7 +24,7 @@ export function renderBuiltInHelp(resourceName) {
|
|
|
24
24
|
if (!resource) {
|
|
25
25
|
return capabilityNotFound(resourceName);
|
|
26
26
|
}
|
|
27
|
-
return success("help", brandCommandValue({...resource, globalOptions: GLOBAL_OPTIONS}));
|
|
27
|
+
return success("help", brandCommandValue({...resource, globalOptions: GLOBAL_OPTIONS}));
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export function renderBuiltInCommandHelp(resourceName, commandName) {
|
|
@@ -38,8 +38,8 @@ export function renderBuiltInCommandHelp(resourceName, commandName) {
|
|
|
38
38
|
command: command.name,
|
|
39
39
|
summary: command.summary,
|
|
40
40
|
usage: command.usage,
|
|
41
|
-
options: command.options || [],
|
|
42
|
-
globalOptions: GLOBAL_OPTIONS,
|
|
41
|
+
options: command.options || [],
|
|
42
|
+
globalOptions: GLOBAL_OPTIONS,
|
|
43
43
|
details: command.details || [],
|
|
44
44
|
examples: command.examples || [],
|
|
45
45
|
}));
|
|
@@ -52,8 +52,8 @@ export function renderResourceHelp(manifest, resourceName) {
|
|
|
52
52
|
}
|
|
53
53
|
return success("help", {
|
|
54
54
|
resource: resource.name,
|
|
55
|
-
summary: resource.summary,
|
|
56
|
-
globalOptions: GLOBAL_OPTIONS,
|
|
55
|
+
summary: resource.summary,
|
|
56
|
+
globalOptions: GLOBAL_OPTIONS,
|
|
57
57
|
commands: (resource.commands || []).map((command) => ({
|
|
58
58
|
name: command.name,
|
|
59
59
|
summary: command.summary,
|
|
@@ -72,8 +72,8 @@ export function renderCommandHelp(manifest, resourceName, commandName) {
|
|
|
72
72
|
command: command.name,
|
|
73
73
|
summary: command.summary,
|
|
74
74
|
usage: usageFor(resource.name, command),
|
|
75
|
-
options: commandOptionsFor(command),
|
|
76
|
-
globalOptions: GLOBAL_OPTIONS,
|
|
75
|
+
options: commandOptionsFor(command),
|
|
76
|
+
globalOptions: GLOBAL_OPTIONS,
|
|
77
77
|
display: command.display,
|
|
78
78
|
requestSchema: command.requestSchema,
|
|
79
79
|
responseSchema: command.responseSchema,
|
|
@@ -152,7 +152,7 @@ const BUILT_IN_RESOURCES = {
|
|
|
152
152
|
},
|
|
153
153
|
{
|
|
154
154
|
name: "clear",
|
|
155
|
-
summary: "Remove the selected environment's local configuration, credentials and capability cache; does not revoke server keys.",
|
|
155
|
+
summary: "Remove the selected environment's local configuration, credentials and capability cache; does not revoke server keys.",
|
|
156
156
|
usage: "finebi-cli config clear",
|
|
157
157
|
},
|
|
158
158
|
],
|
|
@@ -164,18 +164,18 @@ const BUILT_IN_RESOURCES = {
|
|
|
164
164
|
{
|
|
165
165
|
name: "login",
|
|
166
166
|
summary: "Login with FineBI credentials or browser authorization and persist an Access Key.",
|
|
167
|
-
usage: "finebi-cli auth login [--profile <name>] [--base-url <url>] [--browser] [--username <username>] [--from-env] [--password-stdin] [--no-use]",
|
|
168
|
-
options: [
|
|
169
|
-
{ name: "--no-use", type: "boolean", description: "Save successful login without changing the default environment." },
|
|
167
|
+
usage: "finebi-cli auth login [--profile <name>] [--base-url <url>] [--browser] [--username <username>] [--from-env] [--password-stdin] [--no-use]",
|
|
168
|
+
options: [
|
|
169
|
+
{ name: "--no-use", type: "boolean", description: "Save successful login without changing the default environment." },
|
|
170
170
|
{ name: "--username", type: "string", description: "FineBI username." },
|
|
171
171
|
{ name: "--from-env", type: "boolean", description: "Read base URL, username, and password from environment variables." },
|
|
172
172
|
{ name: "--password-stdin", type: "boolean", description: "Read password from standard input." },
|
|
173
173
|
{ name: "--browser", type: "boolean", description: "Authorize this CLI from a signed-in FineBI browser without sending a password." },
|
|
174
174
|
],
|
|
175
|
-
details: [
|
|
176
|
-
"Login creates the named environment if missing and selects it after successful authentication, unless --no-use is supplied.",
|
|
177
|
-
"A new environment requires --base-url or FINEBI_BASE_URL; it never inherits another environment's address or identity.",
|
|
178
|
-
"Failed authentication preserves saved environments and the default selection.",
|
|
175
|
+
details: [
|
|
176
|
+
"Login creates the named environment if missing and selects it after successful authentication, unless --no-use is supplied.",
|
|
177
|
+
"A new environment requires --base-url or FINEBI_BASE_URL; it never inherits another environment's address or identity.",
|
|
178
|
+
"Failed authentication preserves saved environments and the default selection.",
|
|
179
179
|
"Environment variables for --from-env:",
|
|
180
180
|
" FINEBI_BASE_URL",
|
|
181
181
|
" FINEBI_USERNAME",
|
|
@@ -187,28 +187,28 @@ const BUILT_IN_RESOURCES = {
|
|
|
187
187
|
"finebi-cli auth login --username <username>",
|
|
188
188
|
"finebi-cli auth login --from-env",
|
|
189
189
|
"echo <password> | finebi-cli auth login --username <username> --password-stdin",
|
|
190
|
-
"finebi-cli auth login --browser",
|
|
191
|
-
"finebi-cli auth login --profile test --base-url https://test.example.com/webroot/decision --browser",
|
|
190
|
+
"finebi-cli auth login --browser",
|
|
191
|
+
"finebi-cli auth login --profile test --base-url https://test.example.com/webroot/decision --browser",
|
|
192
192
|
],
|
|
193
193
|
},
|
|
194
|
-
{
|
|
195
|
-
name: "list",
|
|
196
|
-
summary: "List saved environments and mark the default; local metadata only, no credential validation or secret output.",
|
|
197
|
-
usage: "finebi-cli auth list",
|
|
198
|
-
},
|
|
199
|
-
{
|
|
200
|
-
name: "use",
|
|
201
|
-
summary: "Select an existing environment as default without logging in.",
|
|
202
|
-
usage: "finebi-cli auth use <name>",
|
|
203
|
-
},
|
|
204
|
-
{
|
|
205
|
-
name: "remove",
|
|
206
|
-
summary: "Delete one local environment and its capability cache; does not revoke server keys.",
|
|
207
|
-
usage: "finebi-cli auth remove <name>",
|
|
208
|
-
details: ["Use auth logout --profile <name> first to revoke its Access Key.", "Removing the default environment does not select another environment."],
|
|
209
|
-
},
|
|
210
|
-
{
|
|
211
|
-
name: "info",
|
|
194
|
+
{
|
|
195
|
+
name: "list",
|
|
196
|
+
summary: "List saved environments and mark the default; local metadata only, no credential validation or secret output.",
|
|
197
|
+
usage: "finebi-cli auth list",
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: "use",
|
|
201
|
+
summary: "Select an existing environment as default without logging in.",
|
|
202
|
+
usage: "finebi-cli auth use <name>",
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: "remove",
|
|
206
|
+
summary: "Delete one local environment and its capability cache; does not revoke server keys.",
|
|
207
|
+
usage: "finebi-cli auth remove <name>",
|
|
208
|
+
details: ["Use auth logout --profile <name> first to revoke its Access Key.", "Removing the default environment does not select another environment."],
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
name: "info",
|
|
212
212
|
summary: "Show stored authentication metadata with secrets masked.",
|
|
213
213
|
usage: "finebi-cli auth info",
|
|
214
214
|
},
|
|
@@ -288,7 +288,7 @@ function commandOptionsFor(command) {
|
|
|
288
288
|
|
|
289
289
|
function optionsFor(schema) {
|
|
290
290
|
const required = schema.required || [];
|
|
291
|
-
return Object.keys(schema.properties || {}).filter((fieldName) => schema.properties[fieldName].input !== "argument").
|
|
291
|
+
return Object.keys(schema.properties || {}).filter((fieldName) => schema.properties[fieldName].input !== "argument").flatMap((fieldName) => {
|
|
292
292
|
const property = schema.properties[fieldName];
|
|
293
293
|
const optionName = optionNameFor(fieldName, property);
|
|
294
294
|
const option = {
|
|
@@ -305,7 +305,15 @@ function optionsFor(schema) {
|
|
|
305
305
|
if (defaultValue !== undefined && defaultValue !== "") {
|
|
306
306
|
option.default = defaultValue;
|
|
307
307
|
}
|
|
308
|
-
|
|
308
|
+
if (property.keyValueOption) {
|
|
309
|
+
return [option, {
|
|
310
|
+
name: `--${property.keyValueOption}`,
|
|
311
|
+
required: false,
|
|
312
|
+
type: "name=value",
|
|
313
|
+
description: `Repeatable; maps each name to a one-element string array; mutually exclusive with --${optionName}. Split at the first =; empty names/values and duplicate names are rejected.`,
|
|
314
|
+
}];
|
|
315
|
+
}
|
|
316
|
+
return [option];
|
|
309
317
|
});
|
|
310
318
|
}
|
|
311
319
|
|
|
@@ -368,10 +376,10 @@ function renderTopLevelHelpText(data) {
|
|
|
368
376
|
"",
|
|
369
377
|
"Use --help or -h for command details.",
|
|
370
378
|
"Use --json with --help for structured output.",
|
|
371
|
-
"Output options: --select <paths> / --max-chars <n> / --save <path> for large responses.",
|
|
372
|
-
"Environment: --profile <name> > FINEBI_PROFILE > saved default. Global options may precede or follow the command.",
|
|
373
|
-
);
|
|
374
|
-
lines.push("", "Global Options:", ...formatOptionRows(data.globalOptions || []));
|
|
379
|
+
"Output options: --select <paths> / --max-chars <n> / --save <path> for large responses.",
|
|
380
|
+
"Environment: --profile <name> > FINEBI_PROFILE > saved default. Global options may precede or follow the command.",
|
|
381
|
+
);
|
|
382
|
+
lines.push("", "Global Options:", ...formatOptionRows(data.globalOptions || []));
|
|
375
383
|
return `${lines.join("\n")}\n`;
|
|
376
384
|
}
|
|
377
385
|
|
|
@@ -387,8 +395,8 @@ function renderResourceHelpText(data) {
|
|
|
387
395
|
"",
|
|
388
396
|
"Commands:",
|
|
389
397
|
];
|
|
390
|
-
lines.push(...formatRows(data.commands || [], "name", "summary"));
|
|
391
|
-
lines.push("", "Global Options:", ...formatOptionRows(data.globalOptions || []));
|
|
398
|
+
lines.push(...formatRows(data.commands || [], "name", "summary"));
|
|
399
|
+
lines.push("", "Global Options:", ...formatOptionRows(data.globalOptions || []));
|
|
392
400
|
lines.push("", `Use ${commandLine(data.resource, "<command>", "--help")} for command details.`);
|
|
393
401
|
return `${lines.join("\n")}\n`;
|
|
394
402
|
}
|
|
@@ -403,13 +411,13 @@ function renderCommandHelpText(data) {
|
|
|
403
411
|
"Usage:",
|
|
404
412
|
` ${data.usage}`,
|
|
405
413
|
];
|
|
406
|
-
if ((data.options || []).length > 0) {
|
|
414
|
+
if ((data.options || []).length > 0) {
|
|
407
415
|
lines.push("", "Options:");
|
|
408
416
|
lines.push(...formatOptionRows(data.options));
|
|
409
|
-
}
|
|
410
|
-
if ((data.globalOptions || []).length > 0) {
|
|
411
|
-
lines.push("", "Global Options:", ...formatOptionRows(data.globalOptions));
|
|
412
|
-
}
|
|
417
|
+
}
|
|
418
|
+
if ((data.globalOptions || []).length > 0) {
|
|
419
|
+
lines.push("", "Global Options:", ...formatOptionRows(data.globalOptions));
|
|
420
|
+
}
|
|
413
421
|
if ((data.details || []).length > 0) {
|
|
414
422
|
lines.push("", ...data.details);
|
|
415
423
|
}
|
|
@@ -1,89 +1,89 @@
|
|
|
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 source = context.skillSource || [
|
|
59
|
-
"../skills/finebi-cli-agent/", "../../skills/finebi-cli-agent/", "../../../",
|
|
60
|
-
].map(path => fileURLToPath(new URL(path, import.meta.url)))
|
|
61
|
-
.find(path => existsSync(join(path, "SKILL.md")) && existsSync(join(path, "scripts/finebi-cli/src/index.js")));
|
|
62
|
-
if (!source) throw new Error("FineBI Skill resources were not found. Reinstall the CLI package or build the Skill in the source checkout.");
|
|
63
|
-
await readFile(join(source, "SKILL.md"));
|
|
64
|
-
await readFile(join(source, "scripts/finebi-cli/src/index.js"));
|
|
65
|
-
await mkdir(directory, {recursive: true});
|
|
66
|
-
try { lock = await open(lockPath, "wx"); }
|
|
67
|
-
catch (error) { if (error.code === "EEXIST") throw new Error(`Skill installation lock exists: ${lockPath}`); throw error; }
|
|
68
|
-
let state = {};
|
|
69
|
-
try { state = JSON.parse(await readFile(statePath, "utf8")); }
|
|
70
|
-
catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
71
|
-
const results = await installSkills({source, home, env, agents: options.agents, replace: options.replace, previous: state.skills || []});
|
|
72
|
-
const skills = new Map((state.skills || []).map(skill => [skill.agent, skill]));
|
|
73
|
-
for (const skill of results) skills.set(skill.agent, {...skills.get(skill.agent), ...skill});
|
|
74
|
-
const stagedState = join(directory, "installation.json.stage");
|
|
75
|
-
await writeFile(stagedState, `${JSON.stringify({...state, skills: [...skills.values()], updatedAt: new Date().toISOString()}, null, 2)}\n`);
|
|
76
|
-
await rename(stagedState, statePath);
|
|
77
|
-
const data = {skills: results.map(({hashes, ...skill}) => skill)};
|
|
78
|
-
if (results.some(skill => !["installed", "unchanged"].includes(skill.status))) {
|
|
79
|
-
return failure("install-skill", cliError("SKILL_INSTALL_FAILED", {
|
|
80
|
-
message: "Some Skills were kept or failed. Review the results; --replace backs up and replaces local changes.", partialResult: data,
|
|
81
|
-
}));
|
|
82
|
-
}
|
|
83
|
-
return success("install-skill", data);
|
|
84
|
-
} catch (error) {
|
|
85
|
-
return failure("install-skill", cliError("SKILL_INSTALL_FAILED", {message: error.message}));
|
|
86
|
-
} finally {
|
|
87
|
-
if (lock) { await lock.close(); await unlink(lockPath); }
|
|
88
|
-
}
|
|
89
|
-
}
|
|
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 source = context.skillSource || [
|
|
59
|
+
"../skills/finebi-cli-agent/", "../../skills/finebi-cli-agent/", "../../../",
|
|
60
|
+
].map(path => fileURLToPath(new URL(path, import.meta.url)))
|
|
61
|
+
.find(path => existsSync(join(path, "SKILL.md")) && existsSync(join(path, "scripts/finebi-cli/src/index.js")));
|
|
62
|
+
if (!source) throw new Error("FineBI Skill resources were not found. Reinstall the CLI package or build the Skill in the source checkout.");
|
|
63
|
+
await readFile(join(source, "SKILL.md"));
|
|
64
|
+
await readFile(join(source, "scripts/finebi-cli/src/index.js"));
|
|
65
|
+
await mkdir(directory, {recursive: true});
|
|
66
|
+
try { lock = await open(lockPath, "wx"); }
|
|
67
|
+
catch (error) { if (error.code === "EEXIST") throw new Error(`Skill installation lock exists: ${lockPath}`); throw error; }
|
|
68
|
+
let state = {};
|
|
69
|
+
try { state = JSON.parse(await readFile(statePath, "utf8")); }
|
|
70
|
+
catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
71
|
+
const results = await installSkills({source, home, env, agents: options.agents, replace: options.replace, previous: state.skills || []});
|
|
72
|
+
const skills = new Map((state.skills || []).map(skill => [skill.agent, skill]));
|
|
73
|
+
for (const skill of results) skills.set(skill.agent, {...skills.get(skill.agent), ...skill});
|
|
74
|
+
const stagedState = join(directory, "installation.json.stage");
|
|
75
|
+
await writeFile(stagedState, `${JSON.stringify({...state, skills: [...skills.values()], updatedAt: new Date().toISOString()}, null, 2)}\n`);
|
|
76
|
+
await rename(stagedState, statePath);
|
|
77
|
+
const data = {skills: results.map(({hashes, ...skill}) => skill)};
|
|
78
|
+
if (results.some(skill => !["installed", "unchanged"].includes(skill.status))) {
|
|
79
|
+
return failure("install-skill", cliError("SKILL_INSTALL_FAILED", {
|
|
80
|
+
message: "Some Skills were kept or failed. Review the results; --replace backs up and replaces local changes.", partialResult: data,
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
return success("install-skill", data);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
return failure("install-skill", cliError("SKILL_INSTALL_FAILED", {message: error.message}));
|
|
86
|
+
} finally {
|
|
87
|
+
if (lock) { await lock.close(); await unlink(lockPath); }
|
|
88
|
+
}
|
|
89
|
+
}
|