@finebi/finebi-cli 1.3.0 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finebi/finebi-cli",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "FineBI dynamic capability command line client.",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {
@@ -12,15 +12,15 @@
12
12
  "finebi-cli": "./src/index.js"
13
13
  },
14
14
  "files": [
15
- "src",
15
+ "src",
16
16
  "skills",
17
17
  "README.md"
18
18
  ],
19
19
  "engines": {
20
20
  "node": ">=18.17"
21
21
  },
22
- "scripts": {
23
- "prepack": "node ../scripts/build-finebi-cli-agent.mjs",
22
+ "scripts": {
23
+ "prepack": "node ../scripts/build-finebi-cli-agent.mjs",
24
24
  "test:pack": "node ../scripts/test-cli-package.mjs",
25
25
  "test": "npm run test:unit && npm run test:process && npm run test:contract && npm run test:integration:stub",
26
26
  "test:unit": "node --test tests/unit/access-key.test.js tests/unit/auth.test.js tests/unit/capability.test.js tests/unit/config-permissions.test.js tests/unit/config.test.js tests/unit/dynamic-command-forwarding.test.js tests/unit/dynamic-command-multipart.test.js tests/unit/dynamic-command-preflight.test.js tests/unit/error-catalog-loader.test.js tests/unit/error-catalog.test.js tests/unit/error-construction-guard.test.js tests/unit/help.test.js tests/unit/http-stub.test.js tests/unit/index-auth-login.test.js tests/unit/index-auth-session.test.js tests/unit/index-browser-auth.test.js tests/unit/index-config.test.js tests/unit/index-dynamic-errors.test.js tests/unit/index-dynamic-render.test.js tests/unit/index-help.test.js tests/unit/index-profiles.test.js tests/unit/install-skill.test.js tests/unit/output-limits.test.js tests/unit/output.test.js tests/unit/package.test.js tests/unit/schema-options-capability.test.js tests/unit/schema-options-file-input.test.js tests/unit/schema-options-scalar.test.js",
@@ -28,7 +28,8 @@ AnalysisTableDsl.create("表名")
28
28
 
29
29
  ```js
30
30
  .from("来源表原始名")
31
- .fromExcel("附件id", "Sheet1", ...) // Excel 来源(create 专用;sheetNames 可省略=全部有效 sheet)
31
+ .fromExcel("附件id", "Sheet1", ...) // Excel 来源(create 专用;省略 sheetNames 默认仅取首个有效 Sheet)
32
+ .mergeSheets() // 可选:合并所选 Sheet;省略 sheetNames 时合并全部有效 Sheet
32
33
  .select("字段名", ...) // 可选:引用白名单(只允许后续步骤引用这些字段)
33
34
  ```
34
35
 
@@ -37,6 +38,9 @@ AnalysisTableDsl.create("表名")
37
38
  - `from` 必选第一步,来源可以是公共数据集、主题内表或 Excel 表的**原始名**(全局唯一,即
38
39
  `analysis-ds search` / `public-ds search` 返回的 `id`)。转义名/展示名允许重名,不作来源标识。
39
40
  - `fromExcel` 的附件 id 来自 `analysis-ds upload-excel`;引擎默认 Spider。
41
+ - `fromExcel` 一脚本一表:未声明 `.mergeSheets()` 时,省略 `sheetNames` 仅取首个有效 Sheet;
42
+ 显式指定多个 Sheet 会被拒绝。声明 `.mergeSheets()` 时合并指定的 Sheet;省略 `sheetNames`
43
+ 时合并全部有效 Sheet。合并要求至少两个 Sheet,且字段名、类型及顺序一致,否则报错。
40
44
  - `from` 原始名直查失败时,错误会列出两棵树的近似候选(转义名 + id + 位置),可据此修正。
41
45
  - `select` 可选:声明后续步骤可引用的字段范围(白名单);**不产生字段收窄**——7.0 前端
42
46
  第一步恒保存来源全字段,字段收窄由字段设置步骤的 `hide` 表达。
@@ -22,7 +22,7 @@
22
22
  |---|---|---|
23
23
  | `analysis-ds` | `dsl`†, `info`, `operator-schema`†, `preview`, `search` | `add`, `update`†, `data-update`, `delete`, `publish` |
24
24
  | `calculated-field` | `context`, `dsl`, `info`, `validate` | `add`, `delete`, `update` |
25
- | `dashboard` | `info`, `preview` | `add`, `delete`, `update` |
25
+ | `dashboard` | `search`, `info`, `preview` | `add`, `delete`, `update` |
26
26
  | `data-connection` | `search`, `tables` | — |
27
27
  | `model` | `info`, `search` | `add`, `delete` |
28
28
  | `metric-condition` | `dsl` | `update` |
@@ -33,7 +33,7 @@
33
33
  | `public-ds-folder` | `search` | `add` |
34
34
  | `relationship` | `info` | `add`, `delete` |
35
35
  | `subject` | `info`, `search` | `add`, `data-update`, `delete` |
36
- | `widget` | `data`, `info`, `preview` | `add`, `delete`, `update` |
36
+ | `widget` | `data`, `info`, `preview`, `search` | `add`, `delete`, `update` |
37
37
 
38
38
  `geographic-role match` 的 `--values` 使用单个 JSON 数组参数,如 `'["上海","江苏"]'`;最多 200 个值,不代表整列扫描。`catalog` 按区域路径去重后分页。
39
39
 
@@ -101,7 +101,7 @@
101
101
  (内部试用开关开启的服务器);正式版默认态下 `add` 仅支持原样引用创建(create/from/fromExcel),
102
102
  `template` 仅 `basic` 场景,不得规划改表工作流(见第 2 节 † 注)。
103
103
 
104
- 创建与步骤覆盖使用 AnalysisTableDsl JS 文件:`add --subject-id <id> --file <x.js>` 创建新表(DSL `from(表原始名)` 声明来源,原始名= search 返回的 `id`,全局唯一;Excel 源用 `fromExcel(attachId, [sheetNames])`,attachId 来自 `upload-excel`),`update --id <id> --file <x.js>` 覆盖步骤链(来源不变,DSL `from` 必须匹配现有来源)。创建后使用响应中的实际 `id`/`ids` 逐一调用 `info`,失败清理必须覆盖 `ids` 中的全部实际 ID。`dsl --id <id>` 反读已保存表的可编辑 DSL 文本(Bean→DSL),适用于「读取 → 修改 → 写回」;写回用 `update`。**影响面透出(2026-08-31)**:`update` 响应带 `changedFields`(保存前后字段差分,rename 以旧名消失 + 新名出现表达);`dsl` 反读响应带 `resultToIdentifyNameMap`(结果字段名 ≠ identify name 的别名差异条目,组件引用按 identify name 解析)。**别名保留(2026-08-31)**:DSL 可选 `resultName(resultFieldName, identifyName)` 声明(key=最后一步结果字段名=字段转义名,value=对外 identify name;未声明完全交给平台);`dsl` 反读把别名差异输出为 `.resultName(...)` 声明,写回即幂等——rename 涉及被组件引用的字段时,声明 `resultName("新名", "旧名")` 保留 identify name,组件不失效。改表工作流:`dsl` 看别名层(评估 rename 风险)→ `update` → 看 `changedFields` → 需要保留别名时在 DSL 声明 `resultName(...)` → `verify-data` 验证。AnalysisTableDsl 语句面见 handoff-dsl-writer 的 `analysis-dsl-api.md`(八类步骤 + 可选 resultName 声明:来源/过滤/排序/字段设置/公式列/分组汇总/左右合并/上下合并;`template --scenario <basic|formula|group|full>` 给可替换模板)。**合并步骤(2026-08-31)**:`join("右表原始名", joinType.X).on(左字段, 右字段).renameResult(旧名, 新名)` 左右合并、`union("表A").union("表B").alignField(左, 右).withFields(...).withAllFields().renameResult(...)` 上下合并——右表/合并表**只支持同主题内数据集**(用 `analysis-ds.search` 确认表在当前主题);结果字段由引擎推导(抽取表合并依据去重成一个字段、直连表全保留);改名语句是 `renameResult`(避让字段设置的 `rename`)。先按第 4 节区分引擎类型;DIRECT 数据集不得因更新状态或摘要 `usable` 规划、执行 `data-update`。SPIDER 的 `data-update` 可能触发数据重算,不能视为可逆。`delete` 必须独立确认。
104
+ 创建与步骤覆盖使用 AnalysisTableDsl JS 文件:`add --subject-id <id> --file <x.js>` 创建新表(DSL `from(表原始名)` 声明来源,原始名= search 返回的 `id`,全局唯一;Excel 源用 `fromExcel(attachId, "Sheet1", ...)`(Sheet 名逐个传字符串),attachId 来自 `upload-excel`),`update --id <id> --file <x.js>` 覆盖步骤链(来源不变,DSL `from` 必须匹配现有来源)。创建后使用响应中的实际 `id`/`ids` 逐一调用 `info`,失败清理必须覆盖 `ids` 中的全部实际 ID。`dsl --id <id>` 反读已保存表的可编辑 DSL 文本(Bean→DSL),适用于「读取 → 修改 → 写回」;写回用 `update`。**影响面透出(2026-08-31)**:`update` 响应带 `changedFields`(保存前后字段差分,rename 以旧名消失 + 新名出现表达);`dsl` 反读响应带 `resultToIdentifyNameMap`(结果字段名 ≠ identify name 的别名差异条目,组件引用按 identify name 解析)。**别名保留(2026-08-31)**:DSL 可选 `resultName(resultFieldName, identifyName)` 声明(key=最后一步结果字段名=字段转义名,value=对外 identify name;未声明完全交给平台);`dsl` 反读把别名差异输出为 `.resultName(...)` 声明,写回即幂等——rename 涉及被组件引用的字段时,声明 `resultName("新名", "旧名")` 保留 identify name,组件不失效。改表工作流:`dsl` 看别名层(评估 rename 风险)→ `update` → 看 `changedFields` → 需要保留别名时在 DSL 声明 `resultName(...)` → `verify-data` 验证。AnalysisTableDsl 语句面见 handoff-dsl-writer 的 `analysis-dsl-api.md`(八类步骤 + 可选 resultName 声明:来源/过滤/排序/字段设置/公式列/分组汇总/左右合并/上下合并;`template --scenario <basic|formula|group|full>` 给可替换模板)。**合并步骤(2026-08-31)**:`join("右表原始名", joinType.X).on(左字段, 右字段).renameResult(旧名, 新名)` 左右合并、`union("表A").union("表B").alignField(左, 右).withFields(...).withAllFields().renameResult(...)` 上下合并——右表/合并表**只支持同主题内数据集**(用 `analysis-ds.search` 确认表在当前主题);结果字段由引擎推导(抽取表合并依据去重成一个字段、直连表全保留);改名语句是 `renameResult`(避让字段设置的 `rename`)。先按第 4 节区分引擎类型;DIRECT 数据集不得因更新状态或摘要 `usable` 规划、执行 `data-update`。SPIDER 的 `data-update` 可能触发数据重算,不能视为可逆。`delete` 必须独立确认。
105
105
 
106
106
  主题共享钻取目录:`drill-directory.update --subject-id <id> --file <directory.js>` 使用 `DrillDirectoryModel.builder("目录名").levels([from("表").dim("省"), from("表").dim("市")]).build()` 创建,用 `.dissolve()` 解散。`drill-directory.dsl --subject-id <id> --name <目录名>` 回读;目录按主题内精确名称定位。解散保留原字段,操作影响主题共享字段面板;目录重名且层级不同、字段已属于其他目录时拒绝写入,不能隐式覆盖。
107
107
 
@@ -115,17 +115,22 @@
115
115
 
116
116
  ### 7.3 Dashboard 与 Widget
117
117
 
118
+ 仅知道仪表板名称时,用 `dashboard search "关键词"` 搜索「我的分析」和发布目录中有预览权限的仪表板;结果按 ID 合并并通过 `locations` 保留命中的来源。目录挂载的是主题,只有发布清单中的仪表板参与搜索,匹配仪表板名而非挂载主题名。用名称、主题、目录位置消歧;仅目录来源须保留 `id/entryId/publishId`,不能据此假定可使用原主题的 `info/dsl/preview` 或写命令,也不能自行拼接预览 URL。
119
+
120
+ 组件名称使用 `widget search "关键词"`,只搜索「我的分析」。两类搜索均支持省略关键词分页枚举,`--limit` 默认 100(1–500),`--offset` 默认 0 且必须为 limit 的整数倍,遍历 `pagination.hasNext/nextOffset`。仪表板的 total 和分页以合并后的结果为准。已知主题时用 `subject info` 枚举主题内资源,搜索命令不接受 `--subject-id`。搜索失败不能解释为没有资源。
121
+
118
122
  `add`/`update` 仅消费已经验证的 JS 文件。涉及自然语言组件或仪表板设计时遵循 `scenario-workflows.md` 和 `handoff-contract.md`。创建后用响应实际 ID 调用 `info`,必要时调用 `preview`;需要核对取数(逐行、分页或展开档位)时用 `widget data`(见第 5 节)。Dashboard 引用新 Widget 时,先创建并回查 Widget,再进入 Dashboard 阶段。
119
123
 
120
124
  `widget template` 的 `basic` 支持 `seriesLine`、`ROSE`、`MULTI_PIE`。`seriesLine` 使用 `dimensionField` 分类轴、指标求和折线和 `seriesField` 颜色;`ROSE` 使用 `dimensionField` 分类,数值字段 `measureField` 求和后同时绑定角度与半径;`MULTI_PIE` 使用 `dimensionField` 颜色、指标求和大小和必填 `granularityField` 粒度,不配置 x/y 或 tooltip。公共变量为 `widgetName`、`tableName`、数值字段 `measureField`,变量按 JSON 字面量替换。
121
125
 
122
126
  `LINE basic` 使用 `dateField`(DATE/DATE_TIME,默认年月日)和两个不同的数值字段 `measureField1`、`measureField2`,分别以 `sum` 绘制上下分区折线。多指标自动按指标名称着色并生成图例;颜色由转换器绑定到各指标图形,不在“全部”卡片显式绑定。`widgetName`、`tableName` 为公共变量;仍编译为 `sliceLine`,既有输入别名保持兼容。
123
127
 
124
- **读取已有资源的 DSL 形态**:`widget dsl --id <widgetId>` / `dashboard dsl --id <dashboardId>` 把平台 Bean 反向翻译为可编辑的 DSL 文本。改板先 `dashboard info --id <dashboardId>` 清点条目与板级留痕(轻量),定位到目标后再 dsl 回读。`dashboard dsl` 的响应附带所引用数据组件的 Widget DSL 回显,便于一次读取;仪表板 DSL 本身只引用已有组件,附带回显不代表 `dashboard update` 会保存组件定义。
128
+ **读取已有资源的 DSL 形态**:`widget dsl --id <widgetId>` / `dashboard dsl --id <dashboardId>` 把平台 Bean 反向翻译为可编辑的 DSL 文本。改板先 `dashboard info --id <dashboardId>` 清点条目与板级留痕(轻量),定位到目标后再 dsl 回读。只看一个条目时用单条目切片 `dashboard dsl --id <dashboardId> --entry <名>`(重名板用 `--entry-id <entries[].entryId>` 消歧),`warnings`/组件回显同步过滤到目标条目。`dashboard dsl` 的响应附带所引用数据组件的 Widget DSL 回显,便于一次读取;仪表板 DSL 本身只引用已有组件,附带回显不代表 `dashboard update` 会保存组件定义。
125
129
 
126
130
  **按修改对象选择写入命令**:
127
131
 
128
132
  - 仅修改数据组件的字段、指标、聚合或图表配置时,从上述回显或 `widget dsl` 取得目标组件的完整 DSL,保留无关配置,修改后用 `widget update --id <widgetId> --file <widget.js>` 写回原组件。例如把“销售额”改成“利润”,只更新对应 Widget;仪表板布局和引用不变时无需执行 `dashboard update`。写后回查组件配置与数据,并核对仪表板中的目标组件。
133
+ - 仅修改仪表板上一个条目(控件 / 文本 / 图片 / web / 复合过滤 / 组件引用条目的板侧面)时,用单条目写:`dashboard update-entry --id <dashboardId> --file <fragment.js>`(fragment 与 `dsl --entry` 切片同文法,是条目的完整新定义——切片读全条 → 改 → 写回;重名板带 `--entry-id` 消歧)。未点名条目原样保留;省略 `position` 沿用原槽位。新增 / 删除条目用 `dashboard add-entry` / `remove-entry`(被控件圈定或联动的条目删除会被拒,按错误信息先改引用);`add-entry` 的 fragment 无切片可抄、需手写——单条目语句 + 可选 `position` 行,不包整板脚本,缺省 position 落首个空位。并发编辑带 `--expected-update-time`(上次写响应的 `lastUpdateTime`),不匹配报 `VERSION_CONFLICT`,重读后重试。
129
134
  - 修改仪表板布局、组件引用或板级配置时,保留完整仪表板 DSL 中的其他条目和配置,用 `dashboard update --id <dashboardId> --file <dashboard.js>` 写回;不能用只含目标条目的局部片段覆盖整板。若同时修改数据组件,先完成 `widget update` 并回查,再执行必要的仪表板更新。`dashboard update` 不接受内联数据组件定义,不能代替 `widget update` 修改组件字段。
130
135
 
131
136
  DSL 语句面与 `widget template` / `dashboard template` 一致(见 handoff-dsl-writer 的 api 文档);主题模型字段会按作用域保持根级指标、数据集字段或模型计算字段路径,该回显可作为模型名/字段名证据。回读含 warnings 字段(Bean 上文本 DSL 无法表达、被丢弃的配置会带出登记),改动前应审视 warnings,避免把不可达配置覆盖丢失——条目级 `*_DROPPED` 留痕的条目整板 `dashboard update` 会被写侧防丢守卫以 `ENTRY_LOSS_BLOCKED` 拒绝(处置口径见 handoff-dsl-writer 的 dashboard-dsl-api)。
@@ -13,6 +13,18 @@
13
13
  **改板前先 `dashboard info --id <dashboardId>`**:板级构成摘要(条目清单 + 悬浮层 +
14
14
  板级留痕)与本文语句面的条目一一对应,先清点再按需 `dashboard dsl` 取局部 DSL,
15
15
  不必为定位条目拉全量回读;`warnings` 出现条目级 `*_DROPPED` 留痕时按上一段处置。
16
+ 只看一个条目时用单条目切片 `dashboard dsl --id <dashboardId> --entry <名>`(重名板
17
+ 改用 `--entry-id <entries[].entryId>` 消歧):`dsl` 只含该条目语句 + 可选 `position`
18
+ (容器成员 position 是容器局部格位),`warnings`/`widgets` 同步过滤到目标条目。
19
+ **只改一个条目时用单条目写**:`dashboard update-entry --id <dashboardId> --file
20
+ <fragment.js>`(fragment 与切片同文法,是条目的完整新定义——读全条 → 改 → 写回;
21
+ 重名板带 `--entry-id` 消歧)。未点名条目原样保留,不会丢;widget 引用条目只改板侧面
22
+ (position/title),组件本体走 `widget update`。新增 / 删除条目用 `add-entry` /
23
+ `remove-entry`(被控件圈定或联动的条目删除会被拒,按错误信息先改引用)。`add-entry`
24
+ 的 fragment 无切片可抄、需手写:单条目语句(上文各条目语句形态之一)+ 可选
25
+ `position` 行,不包 `DashboardDsl.create` 整板脚本;缺省 position 落首个空位。
26
+ 并发编辑带 `--expected-update-time`(上次写响应的 `lastUpdateTime`),不匹配会报
27
+ `VERSION_CONFLICT`,重读后重试。
16
28
 
17
29
  **写新看板前先取场景样例**:`dashboard template`(默认场景)返回场景目录与通用设计要点,
18
30
  `dashboard template --scenario <id>` 取某一个场景的完整可抄脚本 + `guidance` 设计要点
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finebi/finebi-cli",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "private": true,
5
5
  "description": "FineBI dynamic capability command line client.",
6
6
  "license": "UNLICENSED",
@@ -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
  },
@@ -368,10 +368,10 @@ function renderTopLevelHelpText(data) {
368
368
  "",
369
369
  "Use --help or -h for command details.",
370
370
  "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 || []));
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 || []));
375
375
  return `${lines.join("\n")}\n`;
376
376
  }
377
377
 
@@ -387,8 +387,8 @@ function renderResourceHelpText(data) {
387
387
  "",
388
388
  "Commands:",
389
389
  ];
390
- lines.push(...formatRows(data.commands || [], "name", "summary"));
391
- lines.push("", "Global Options:", ...formatOptionRows(data.globalOptions || []));
390
+ lines.push(...formatRows(data.commands || [], "name", "summary"));
391
+ lines.push("", "Global Options:", ...formatOptionRows(data.globalOptions || []));
392
392
  lines.push("", `Use ${commandLine(data.resource, "<command>", "--help")} for command details.`);
393
393
  return `${lines.join("\n")}\n`;
394
394
  }
@@ -403,13 +403,13 @@ function renderCommandHelpText(data) {
403
403
  "Usage:",
404
404
  ` ${data.usage}`,
405
405
  ];
406
- if ((data.options || []).length > 0) {
406
+ if ((data.options || []).length > 0) {
407
407
  lines.push("", "Options:");
408
408
  lines.push(...formatOptionRows(data.options));
409
- }
410
- if ((data.globalOptions || []).length > 0) {
411
- lines.push("", "Global Options:", ...formatOptionRows(data.globalOptions));
412
- }
409
+ }
410
+ if ((data.globalOptions || []).length > 0) {
411
+ lines.push("", "Global Options:", ...formatOptionRows(data.globalOptions));
412
+ }
413
413
  if ((data.details || []).length > 0) {
414
414
  lines.push("", ...data.details);
415
415
  }
@@ -51,8 +51,11 @@ export async function runCli(argv, context = {}) {
51
51
  const management = args[0] === "auth" && ["list", "use", "remove"].includes(args[1]);
52
52
  const builtInHelp = isHelpFlagInvocation(args) && (args.length === 1 || isBuiltInResource(args[0]));
53
53
  const optional = management || builtInHelp || args.length === 0 || args[0] === "version";
54
- const profileName = management ? null : selectProfile(configPath(context), firstNonEmpty(flags.profile, env.FINEBI_PROFILE), {
55
- allowCreate: args[0] === "auth" && args[1] === "login", optional,
54
+ const requestedProfile = firstNonEmpty(flags.profile, env.FINEBI_PROFILE);
55
+ const allowCreate = args[0] === "auth" && args[1] === "login";
56
+ const initialDefault = !requestedProfile && !existsSync(configPath(context));
57
+ const profileName = management ? null : selectProfile(configPath(context), requestedProfile, {
58
+ allowCreate, optional,
56
59
  });
57
60
  if (management && (flags.profile || env.FINEBI_PROFILE) && args[1] !== "list") {
58
61
  if (flags.profile && flags.profile !== args[2]) throw new ProfileError("Use the positional environment name for auth use/remove; --profile must not select a different environment.");
@@ -64,7 +67,12 @@ export async function runCli(argv, context = {}) {
64
67
  );
65
68
  const envelope = mutatesProfile
66
69
  ? await withProfileLoginLock(configPath(context), args[0] === "auth" && args[1] === "remove" ? args[2] : profileName,
67
- () => dispatchCli(args, context))
70
+ () => {
71
+ if (!management) selectProfile(configPath(context), profileName, {
72
+ allowCreate: allowCreate || (initialDefault && profileName === "default"),
73
+ });
74
+ return dispatchCli(args, context);
75
+ })
68
76
  : await dispatchCli(args, context);
69
77
  if (profileName) {
70
78
  envelope.meta = {...envelope.meta, profile: profileName};