@finebi/finebi-cli 1.6.3 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +5 -1
  2. package/agent-installation-guide.md +70 -0
  3. package/package.json +2 -1
  4. package/skills/finebi-cli-agent/SKILL.md +1 -1
  5. package/skills/finebi-cli-agent/references/dashboard-data-result.md +191 -191
  6. package/skills/finebi-cli-agent/references/dashboard-dsl-api.md +44 -44
  7. package/skills/finebi-cli-agent/references/web-app.md +4 -1
  8. package/skills/finebi-cli-agent/scripts/finebi-cli/package.json +1 -1
  9. package/skills/finebi-cli-agent/scripts/finebi-cli/src/app.js +160 -160
  10. package/skills/finebi-cli-agent/scripts/finebi-cli/src/config.js +27 -27
  11. package/skills/finebi-cli-agent/scripts/finebi-cli/src/help.js +55 -55
  12. package/skills/finebi-cli-agent/scripts/finebi-cli/src/install-skill.js +89 -89
  13. package/skills/finebi-cli-agent/scripts/finebi-cli/src/profiles.js +158 -158
  14. package/skills/finebi-cli-agent/scripts/finebi-cli/src/skill-files.js +96 -94
  15. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/AGENTS.md +1 -1
  16. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/acceptance.md +80 -80
  17. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/examples/cockpit.config.json +22 -22
  18. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/examples/retail.config.json +185 -185
  19. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/gitignore.template +6 -6
  20. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/index.html +2 -2
  21. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/public/app-config.json +11 -11
  22. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/App.tsx +63 -63
  23. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/cockpit.css +3 -3
  24. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Chart.tsx +40 -40
  25. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Insights.tsx +17 -17
  26. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/MonthPicker.tsx +32 -32
  27. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/OptionFilter.tsx +7 -7
  28. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/RangeFilter.tsx +9 -9
  29. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Widget.tsx +40 -40
  30. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/calendar-locales.ts +3 -3
  31. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/calendar.ts +14 -14
  32. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Bar.tsx +6 -6
  33. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Line.tsx +6 -6
  34. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Pie.tsx +6 -6
  35. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/month-picker.css +1 -1
  36. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/config.d.ts +3 -3
  37. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/config.js +58 -58
  38. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/date.d.ts +1 -1
  39. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/date.js +5 -5
  40. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/filters.d.ts +4 -4
  41. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/filters.js +106 -106
  42. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/normalize.ts +235 -235
  43. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/types.ts +20 -20
  44. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/main.tsx +5 -5
  45. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/retail.css +12 -12
  46. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/presentation.ts +36 -36
  47. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/table-data.ts +47 -47
  48. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/view-data.ts +18 -18
  49. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/widget-queries.ts +18 -18
  50. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/fixtures.mjs +28 -28
  51. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/index.html +16 -16
  52. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/main.tsx +149 -149
  53. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/test-page.css +4 -4
  54. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/vite.config.mjs +13 -13
  55. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/bundle.test.mjs +19 -19
  56. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/cockpit.test.mjs +73 -73
  57. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/field-selection.test.mjs +70 -70
  58. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/retail.test.mjs +31 -31
  59. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/table-normalize.test.mjs +189 -189
  60. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tsconfig.json +4 -4
  61. package/skills/finebi-cli-agent/scripts/finebi-cli-runner.mjs +4 -4
  62. package/src/app.js +160 -160
  63. package/src/config.js +27 -27
  64. package/src/help.js +55 -55
  65. package/src/install-skill.js +89 -89
  66. package/src/profiles.js +158 -158
  67. package/src/skill-files.js +96 -94
  68. package/templates/web-app/AGENTS.md +1 -1
  69. package/templates/web-app/docs/acceptance.md +80 -80
  70. package/templates/web-app/examples/cockpit.config.json +22 -22
  71. package/templates/web-app/examples/retail.config.json +185 -185
  72. package/templates/web-app/gitignore.template +6 -6
  73. package/templates/web-app/index.html +2 -2
  74. package/templates/web-app/public/app-config.json +11 -11
  75. package/templates/web-app/src/App.tsx +63 -63
  76. package/templates/web-app/src/cockpit.css +3 -3
  77. package/templates/web-app/src/components/Chart.tsx +40 -40
  78. package/templates/web-app/src/components/Insights.tsx +17 -17
  79. package/templates/web-app/src/components/MonthPicker.tsx +32 -32
  80. package/templates/web-app/src/components/OptionFilter.tsx +7 -7
  81. package/templates/web-app/src/components/RangeFilter.tsx +9 -9
  82. package/templates/web-app/src/components/Widget.tsx +40 -40
  83. package/templates/web-app/src/components/calendar-locales.ts +3 -3
  84. package/templates/web-app/src/components/calendar.ts +14 -14
  85. package/templates/web-app/src/components/charts/Bar.tsx +6 -6
  86. package/templates/web-app/src/components/charts/Line.tsx +6 -6
  87. package/templates/web-app/src/components/charts/Pie.tsx +6 -6
  88. package/templates/web-app/src/components/month-picker.css +1 -1
  89. package/templates/web-app/src/finebi/config.d.ts +3 -3
  90. package/templates/web-app/src/finebi/config.js +58 -58
  91. package/templates/web-app/src/finebi/date.d.ts +1 -1
  92. package/templates/web-app/src/finebi/date.js +5 -5
  93. package/templates/web-app/src/finebi/filters.d.ts +4 -4
  94. package/templates/web-app/src/finebi/filters.js +106 -106
  95. package/templates/web-app/src/finebi/normalize.ts +235 -235
  96. package/templates/web-app/src/finebi/types.ts +20 -20
  97. package/templates/web-app/src/main.tsx +5 -5
  98. package/templates/web-app/src/retail.css +12 -12
  99. package/templates/web-app/src/services/presentation.ts +36 -36
  100. package/templates/web-app/src/services/table-data.ts +47 -47
  101. package/templates/web-app/src/services/view-data.ts +18 -18
  102. package/templates/web-app/src/services/widget-queries.ts +18 -18
  103. package/templates/web-app/tests/browser/fixtures.mjs +28 -28
  104. package/templates/web-app/tests/browser/index.html +16 -16
  105. package/templates/web-app/tests/browser/main.tsx +149 -149
  106. package/templates/web-app/tests/browser/test-page.css +4 -4
  107. package/templates/web-app/tests/browser/vite.config.mjs +13 -13
  108. package/templates/web-app/tests/bundle.test.mjs +19 -19
  109. package/templates/web-app/tests/cockpit.test.mjs +73 -73
  110. package/templates/web-app/tests/field-selection.test.mjs +70 -70
  111. package/templates/web-app/tests/retail.test.mjs +31 -31
  112. package/templates/web-app/tests/table-normalize.test.mjs +189 -189
  113. package/templates/web-app/tsconfig.json +4 -4
@@ -1,44 +1,44 @@
1
- # Dashboard DSL API
2
-
3
- 这份文档只列当前文本 DSL 可以直接使用的 Dashboard API,以及各 API 的业务含义。本语句面同时是
4
- `dashboard dsl --id <dashboardId>` 回读命令的输出面(响应内嵌数据组件 DSL 回显,见 docs/commands/)。
5
-
6
- **回读 warnings 里出现条目级 `*_DROPPED` 留痕(`UNSUPPORTED_WIDGET_DROPPED` /
7
- `UNREACHABLE_WIDGET_DROPPED` / 整条目跳过的 `UNREACHABLE_IMAGE` / `UNREACHABLE_WEB` /
8
- `AI_SUBJECT_DROPPED` / `COMBINE_DISSOLVED` 等)时不要整板 `dashboard update`**:这些条目
9
- 不在回读 DSL 里,整板重写会使其从板上消失,写侧防丢守卫会以 `ENTRY_LOSS_BLOCKED`
10
- 拒绝保存。先完成与该条目无关的操作;需要处理时询问用户在 FineBI 设计器中处置
11
- (删除或修复该条目),不要试图绕过。
12
-
13
- **改板前先 `dashboard info --id <dashboardId>`**:板级构成摘要(条目清单 + 悬浮层 +
14
- 板级留痕)与本文语句面的条目一一对应,先清点再按需 `dashboard dsl` 取局部 DSL,
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
- 并发编辑(含用户在设计器手改后再让 Agent 续改)带 `--expected-update-time`——基准取
27
- 读板响应(`info` / `dsl` 的 `lastUpdateTime`)或上次写响应的新值,不匹配会报
28
- `VERSION_CONFLICT`(附当前值),重读后重试。entryId 按条目名稳定:设计器保存与整板
29
- `update` 下同名条目沿用原值,新增条目的新值从 update 响应 `entries[]` 直接可得。
30
-
31
- **写新看板前先取场景样例**:`dashboard template`(默认场景)返回场景目录与通用设计要点,
32
- `dashboard template --scenario <id>` 取某一个场景的完整可抄脚本 + `guidance` 设计要点
33
- (场景:`basic` 经营看板 / `kpi` KPI 概览 / `filter` 筛选控件 / `style` 样式面 /
34
- `screen` 大屏驾驶舱 / `rich-text` 富文本与动态字段 / `linkage-jump` 联动与跳转 /
35
- `tree-time` 层级与时间筛选 / `blocks` 容器区块)。
36
- 动态合法值(样式预设 id 与全局样式名)
37
- 用 `dashboard style-presets` 查,语句面速查用 `dashboard schema`。API 面拆分为下表文件,
38
- 按需读取当前操作涉及的文件,不一次加载全部;章节编号沿用拆分前原编号。
39
-
40
- | 文件 | 章节 | 何时读取 |
41
- | --- | --- | --- |
42
- | [dashboard-dsl-core.md](dashboard-dsl-core.md) | §1-§5 根入口、顶层 Builder、内容域(条目挂载、联动与跳转)、布局与样式域、移动端域;§7 字段写法 | 写任何仪表板前必读 |
43
- | [dashboard-dsl-items.md](dashboard-dsl-items.md) | §6 可构造值对象(6.1-6.14:控件、文本、图片、联动/跳转模型、自适应、移动端配置、富文本、Web、组合、Tab、冻结栏、复合过滤、AI 诊断) | 挂载或配置具体条目类型时 |
44
- | [dashboard-dsl-examples.md](dashboard-dsl-examples.md) | §8 推荐写法示例、§9 场景模板与设计要点 | 需要可抄写法或场景模板时 |
1
+ # Dashboard DSL API
2
+
3
+ 这份文档只列当前文本 DSL 可以直接使用的 Dashboard API,以及各 API 的业务含义。本语句面同时是
4
+ `dashboard dsl --id <dashboardId>` 回读命令的输出面(响应内嵌数据组件 DSL 回显,见 docs/commands/)。
5
+
6
+ **回读 warnings 里出现条目级 `*_DROPPED` 留痕(`UNSUPPORTED_WIDGET_DROPPED` /
7
+ `UNREACHABLE_WIDGET_DROPPED` / 整条目跳过的 `UNREACHABLE_IMAGE` / `UNREACHABLE_WEB` /
8
+ `AI_SUBJECT_DROPPED` / `COMBINE_DISSOLVED` 等)时不要整板 `dashboard update`**:这些条目
9
+ 不在回读 DSL 里,整板重写会使其从板上消失,写侧防丢守卫会以 `ENTRY_LOSS_BLOCKED`
10
+ 拒绝保存。先完成与该条目无关的操作;需要处理时询问用户在 FineBI 设计器中处置
11
+ (删除或修复该条目),不要试图绕过。
12
+
13
+ **改板前先 `dashboard info --id <dashboardId>`**:板级构成摘要(条目清单 + 悬浮层 +
14
+ 板级留痕)与本文语句面的条目一一对应,先清点再按需 `dashboard dsl` 取局部 DSL,
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
+ 并发编辑(含用户在设计器手改后再让 Agent 续改)带 `--expected-update-time`——基准取
27
+ 读板响应(`info` / `dsl` 的 `lastUpdateTime`)或上次写响应的新值,不匹配会报
28
+ `VERSION_CONFLICT`(附当前值),重读后重试。entryId 按条目名稳定:设计器保存与整板
29
+ `update` 下同名条目沿用原值,新增条目的新值从 update 响应 `entries[]` 直接可得。
30
+
31
+ **写新看板前先取场景样例**:`dashboard template`(默认场景)返回场景目录与通用设计要点,
32
+ `dashboard template --scenario <id>` 取某一个场景的完整可抄脚本 + `guidance` 设计要点
33
+ (场景:`basic` 经营看板 / `kpi` KPI 概览 / `filter` 筛选控件 / `style` 样式面 /
34
+ `screen` 大屏驾驶舱 / `rich-text` 富文本与动态字段 / `linkage-jump` 联动与跳转 /
35
+ `tree-time` 层级与时间筛选 / `blocks` 容器区块)。
36
+ 动态合法值(样式预设 id 与全局样式名)
37
+ 用 `dashboard style-presets` 查,语句面速查用 `dashboard schema`。API 面拆分为下表文件,
38
+ 按需读取当前操作涉及的文件,不一次加载全部;章节编号沿用拆分前原编号。
39
+
40
+ | 文件 | 章节 | 何时读取 |
41
+ | --- | --- | --- |
42
+ | [dashboard-dsl-core.md](dashboard-dsl-core.md) | §1-§5 根入口、顶层 Builder、内容域(条目挂载、联动与跳转)、布局与样式域、移动端域;§7 字段写法 | 写任何仪表板前必读 |
43
+ | [dashboard-dsl-items.md](dashboard-dsl-items.md) | §6 可构造值对象(6.1-6.14:控件、文本、图片、联动/跳转模型、自适应、移动端配置、富文本、Web、组合、Tab、冻结栏、复合过滤、AI 诊断) | 挂载或配置具体条目类型时 |
44
+ | [dashboard-dsl-examples.md](dashboard-dsl-examples.md) | §8 推荐写法示例、§9 场景模板与设计要点 | 需要可抄写法或场景模板时 |
@@ -4,10 +4,13 @@
4
4
 
5
5
  1. `finebi-cli app create --name <name> --path <parent> [--layout retail]` 创建工程,不安装/运行/发布。米白横向经营简报可选 retail,绿色驾驶舱可选 cockpit;默认 overview,另有 workbench。
6
6
  2. 用dashboard search/info/data确认唯一来源、板内entryId、结果字段dId、真实控件filterName;按生成项目 docs/acceptance.md 记录指标口径、单位/时区、筛选范围和数据完整性。样本仅用于理解和测试。
7
- 3. 按生成项目 AGENTS.md 的“阅读路径”分阶段读取文档,不要求开场通读全部说明。先按 docs/acceptance.md 的“最小真实查询”只配置一个真实组件,按 README 安装依赖、配置开发身份并启动实际页面,验证开发代理查询与页面值。CLI 成功不能替代这一步;优先复用已有会话,不重复索要可用身份的密码。
7
+ 3. 按生成项目 AGENTS.md 的“阅读路径”分阶段读取文档,不要求开场通读全部说明。先检查开发身份:优先复用目标环境的有效会话或已有登录配置;只有 CLI Access Key 不算开发身份。缺少可用身份且需要用户补充凭据或完成授权时,立即说明阻断点并请求最小补充,按项目 docs/auth-and-deploy.md 配置;不要等整页做完才询问,也不重复索要可用身份的密码。
8
+ 身份具备后,按 docs/acceptance.md 的“最小真实查询”只配置一个真实组件,按 README 安装依赖并启动实际页面。留下目标环境、组件请求参数、真实业务响应和页面值的脱敏证据,核对一致后才进入下一阶段;CLI 成功、HTTP 200 或合成测试通过均不能替代这一步。
8
9
  4. 最小真实查询及适用的代表性图表、筛选链路通过后,再扩展整页绑定。按 docs/design.md 对应章节确定首屏、导航、图表分组和风格差异;请求、认证、解析复用模板src/finebi,图表库和UI组件库按页面需求选择。需要查看绑定摘要时可运行 `finebi-cli app inspect --path <project>`,默认不校验模板字段或展示格式,也不证明应用可用。只有需要检查与 CLI 内置模板的兼容性时才显式使用 `--strict`;定制应用无需通过该检查,不要为消除 strict 报错而删除自定义能力或反复修改应用。验收以项目自身测试、构建和真实浏览器验证为准。
9
10
  5. 按生成项目 docs/acceptance.md 分别完成自动测试、构建、test:browser 合成数据验收和真实页面验收,检查整页及窄屏,记录 CLI、开发身份、生产 Cookie 各自的已验证与未验证项。生产部署必须用访问者Cookie,不携带开发身份。
10
11
 
12
+ 阶段阻断时只继续不依赖真实数据的设计或测试,暂停扩展整页绑定。不得自行将开发代理切到 mock、历史响应回放或静态数据来交付或启动预览;注明“回放”或“真实查询未验证”也不等于完成动态应用。用户明确要求离线演示时才单独提供,并说明其不通过真实查询阶段;恢复动态应用开发仍须先完成上述验证。
13
+
11
14
  内置CLI同样通过 `node "<skill-dir>/scripts/finebi-cli/src/index.js" app ...` 调用,不走动态业务runner。生产页面不调用CLI。禁止将开发时查到的业务数据或token写入HTML/JS/config;无筛选控件不能伪造服务端过滤。pageLimit不是分页,hasNext和warnings必须呈现。不提供发布上传或服务端续页。目录来源显式配置 directoryEntryId,由原生 reportEntryId 透传;目标平台需支持 HTTP 目录权限预检与 MOUNT 查询,不能失败后回退“我的分析”。
12
15
 
13
16
  cockpit 的 examples/cockpit.config.json 仅包含占位绑定,必须替换成真实资源和结果 fieldId;多模型显式 datasetId。布局含可配置 KPI、多系列趋势、分类对比、通栏表格及可选观察/口径。年月为自定义十二月/年份网格面板,选中只改 YYYY-MM 草稿。表格搜索、精确排序、筛选、分页、CSV 只处理已加载结果;观察不内置销售公式。widgets[].filterNames 来自真实映射,[] 表示不传覆盖,省略由源看板决定,不宣称未知映射为全局联动。请求/认证仍复用 src/finebi,不能复制个人 CLI 子进程后端。
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finebi/finebi-cli",
3
- "version": "1.6.3",
3
+ "version": "2.0.1",
4
4
  "private": true,
5
5
  "description": "FineBI dynamic capability command line client.",
6
6
  "license": "UNLICENSED",
@@ -1,160 +1,160 @@
1
- import {cp, mkdir, readdir, readFile, stat, writeFile} from "node:fs/promises";
2
- import {dirname, resolve} from "node:path";
3
- import {fileURLToPath} from "node:url";
4
-
5
- import {cliError} from "./errors.js";
6
- import {failure, success} from "./output.js";
7
-
8
- const TEMPLATE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "templates", "web-app");
9
- const NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
10
- const WINDOWS_DEVICE_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/iu;
11
-
12
- export async function handleApp(args, context = {}) {
13
- const action = args[0];
14
- if (action === "create") return createApp(args.slice(1), context);
15
- if (action === "inspect") return inspectApp(args.slice(1));
16
- return invalid("app", "App command is required.", ["finebi-cli app create --name <name> --path <parent>", "finebi-cli app inspect --path <project>"]);
17
- }
18
-
19
- export async function createApp(args, context = {}) {
20
- const options = parseOptions(args, "app.create");
21
- if (!options.ok) return options.error;
22
- const {name, path: parentPath} = options.value;
23
- if (!name || name.length > 214 || !NAME_PATTERN.test(name) || WINDOWS_DEVICE_NAME.test(name)) {
24
- return invalid("app.create", "name must be a simple lowercase npm name (letters, digits, and single hyphens).", ["finebi-cli app create --name sales-dashboard --path <parent>"]);
25
- }
26
- if (!parentPath) {
27
- return invalid("app.create", "path must be an existing parent directory.");
28
- }
29
- if (options.value.layout && !['overview','workbench','cockpit','retail'].includes(options.value.layout)) return invalid('app.create','layout must be overview, workbench, cockpit or retail.');
30
- const parent = resolve(parentPath);
31
- const destination = resolve(parent, name);
32
- if (dirname(destination) !== parent) {
33
- return invalid("app.create", "name must stay inside the selected parent directory.");
34
- }
35
- try {
36
- if (!(await stat(parent)).isDirectory()) return invalid("app.create", "path must point to a directory.");
37
- } catch (error) {
38
- return invalid("app.create", error.code === "ENOENT" ? `Parent directory does not exist: ${parent}` : `Cannot inspect parent directory: ${error.message}`);
39
- }
40
- try {
41
- const templateRoot = context.templateRoot ? resolve(context.templateRoot) : TEMPLATE_ROOT;
42
- const templatePackage = JSON.parse(await readFile(resolve(templateRoot, "package.json"), "utf8"));
43
- const gitignore = await readFile(resolve(templateRoot, "gitignore.template"), "utf8");
44
- const templateConfigPath = resolve(templateRoot, "public", "app-config.json");
45
- let config = {schemaVersion: "1", title: name, basePath: "/webroot/decision", layout: "overview", dashboardId: "", maxRows: 1000, pageSize: 8, widgets: [], filters: []};
46
- try {
47
- config = JSON.parse(await readFile(templateConfigPath, "utf8"));
48
- } catch (error) {
49
- if (error.code !== "ENOENT") throw error;
50
- }
51
- try {
52
- await mkdir(destination, {recursive: false});
53
- } catch (error) {
54
- return invalid("app.create", error.code === "EEXIST" ? `Destination already exists: ${destination}` : `Cannot create destination: ${error.message}`);
55
- }
56
- await copyTemplateContents(templateRoot, destination);
57
- await writeFile(resolve(destination, ".gitignore"), gitignore, "utf8");
58
- if(options.value.layout) config.layout=options.value.layout;
59
- if(config.layout==='cockpit') config.insights=true;
60
- await mkdir(resolve(destination, "public"), {recursive: true});
61
- await writeFile(resolve(destination, "package.json"), `${JSON.stringify({...templatePackage, name}, null, 2)}\n`, "utf8");
62
- await writeFile(resolve(destination, "public", "app-config.json"), `${JSON.stringify({...config, templateVersion: templatePackage.version}, null, 2)}\n`, "utf8");
63
- const lockPath = resolve(destination, "package-lock.json");
64
- try {
65
- const lock = JSON.parse(await readFile(lockPath, "utf8"));
66
- lock.name = name;
67
- if (lock.packages?.[""]) lock.packages[""].name = name;
68
- await writeFile(lockPath, `${JSON.stringify(lock, null, 2)}\n`, "utf8");
69
- } catch (error) {
70
- if (error.code !== "ENOENT") throw error;
71
- }
72
- return success("app.create", {name, path: destination, templateVersion: templatePackage.version});
73
- } catch (error) {
74
- return invalid("app.create", `Could not create project: ${error.message}`);
75
- }
76
- }
77
-
78
- async function copyTemplateContents(sourceRoot, destinationRoot, relativeDirectory = "") {
79
- const sourceDirectory = resolve(sourceRoot, relativeDirectory);
80
- const destinationDirectory = resolve(destinationRoot, relativeDirectory);
81
- for (const entry of (await readdir(sourceDirectory, {withFileTypes: true})).sort((left, right) => left.name.localeCompare(right.name, "en"))) {
82
- const relativePath = `${relativeDirectory}/${entry.name}`.replace(/^\//u, "");
83
- if (relativePath === ".gitignore" || relativePath === "gitignore.template") continue;
84
- if (!templatePathAllowed(relativePath)) continue;
85
- const sourcePath = resolve(sourceDirectory, entry.name);
86
- const destinationPath = resolve(destinationDirectory, entry.name);
87
- if (entry.isDirectory()) {
88
- await mkdir(destinationPath, {recursive: false});
89
- await copyTemplateContents(sourceRoot, destinationRoot, relativePath);
90
- } else if (entry.isFile()) {
91
- await cp(sourcePath, destinationPath, {force: false, errorOnExist: true});
92
- } else {
93
- throw new Error(`Unsupported template entry: ${relativePath}`);
94
- }
95
- }
96
- }
97
-
98
- function templatePathAllowed(relative) {
99
- const relativePath = relative.replaceAll("\\", "/");
100
- const parts = relativePath.split("/").filter(Boolean);
101
- const basename = parts.at(-1) || "";
102
- return !parts.some(part => ["node_modules", "dist", "temp", ".git"].includes(part))
103
- && basename !== ".env"
104
- && (!basename.startsWith(".env.") || basename === ".env.example");
105
- }
106
-
107
- async function inspectApp(args) {
108
- const options = parseOptions(args, "app.inspect");
109
- if (!options.ok) return options.error;
110
- const projectPath = options.value.path;
111
- if (!projectPath) return invalid("app.inspect", "path must be a project directory.");
112
- const project = resolve(projectPath);
113
- try {
114
- if (!(await stat(project)).isDirectory()) return invalid("app.inspect", "path must point to a project directory.");
115
- const configPath = resolve(project, "public", "app-config.json");
116
- const config = JSON.parse(await readFile(configPath, "utf8"));
117
- if (!config || typeof config !== "object" || Array.isArray(config)) return invalid("app.inspect", "app-config.json must contain a JSON object.");
118
- if (options.value.strict) {
119
- const {configErrors}=await import('../templates/web-app/src/finebi/config.js');
120
- const errors = configErrors(config);
121
- if (errors.length) return failure("app.inspect", cliError("INVALID_ARGUMENT", {path: "public/app-config.json", message: errors.join(" ")}));
122
- }
123
- return success("app.inspect", {
124
- path: project,
125
- dashboardId: config.dashboardId ?? null,
126
- layout: config.layout ?? null,
127
- directoryEntryId: config.directoryEntryId || null,
128
- widgets: Array.isArray(config.widgets) ? config.widgets.length : null,
129
- templateVersion: config.templateVersion || null,
130
- configValidated: options.value.strict === true,
131
- remoteValidated: false,
132
- });
133
- } catch (error) {
134
- return invalid("app.inspect", `Cannot read project configuration: ${error.message}`);
135
- }
136
- }
137
-
138
- function parseOptions(args, command) {
139
- const value = {};
140
- for (let index = 0; index < args.length; index++) {
141
- const option = args[index];
142
- if (option === "--json") continue;
143
- if (option === "--strict" && command === "app.inspect") {
144
- if (value.strict) return {ok: false, error: invalid(command, "--strict must only be supplied once.")};
145
- value.strict = true;
146
- continue;
147
- }
148
- if (!["--name","--path",...(command==='app.create'?['--layout']:[])].includes(option)) return {ok: false, error: invalid(command, `Unsupported option: ${option}`)};
149
- const key = option.slice(2);
150
- const next = args[++index];
151
- if (!next || next.startsWith("--")) return {ok: false, error: invalid(command, `${option} requires a value.`)};
152
- if (value[key]) return {ok: false, error: invalid(command, `${option} must only be supplied once.`)};
153
- value[key] = next;
154
- }
155
- return {ok: true, value};
156
- }
157
-
158
- function invalid(command, message, nextActions = []) {
159
- return failure(command, cliError("INVALID_ARGUMENT", {path: command, message, nextActions}));
160
- }
1
+ import {cp, mkdir, readdir, readFile, stat, writeFile} from "node:fs/promises";
2
+ import {dirname, resolve} from "node:path";
3
+ import {fileURLToPath} from "node:url";
4
+
5
+ import {cliError} from "./errors.js";
6
+ import {failure, success} from "./output.js";
7
+
8
+ const TEMPLATE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "templates", "web-app");
9
+ const NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
10
+ const WINDOWS_DEVICE_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/iu;
11
+
12
+ export async function handleApp(args, context = {}) {
13
+ const action = args[0];
14
+ if (action === "create") return createApp(args.slice(1), context);
15
+ if (action === "inspect") return inspectApp(args.slice(1));
16
+ return invalid("app", "App command is required.", ["finebi-cli app create --name <name> --path <parent>", "finebi-cli app inspect --path <project>"]);
17
+ }
18
+
19
+ export async function createApp(args, context = {}) {
20
+ const options = parseOptions(args, "app.create");
21
+ if (!options.ok) return options.error;
22
+ const {name, path: parentPath} = options.value;
23
+ if (!name || name.length > 214 || !NAME_PATTERN.test(name) || WINDOWS_DEVICE_NAME.test(name)) {
24
+ return invalid("app.create", "name must be a simple lowercase npm name (letters, digits, and single hyphens).", ["finebi-cli app create --name sales-dashboard --path <parent>"]);
25
+ }
26
+ if (!parentPath) {
27
+ return invalid("app.create", "path must be an existing parent directory.");
28
+ }
29
+ if (options.value.layout && !['overview','workbench','cockpit','retail'].includes(options.value.layout)) return invalid('app.create','layout must be overview, workbench, cockpit or retail.');
30
+ const parent = resolve(parentPath);
31
+ const destination = resolve(parent, name);
32
+ if (dirname(destination) !== parent) {
33
+ return invalid("app.create", "name must stay inside the selected parent directory.");
34
+ }
35
+ try {
36
+ if (!(await stat(parent)).isDirectory()) return invalid("app.create", "path must point to a directory.");
37
+ } catch (error) {
38
+ return invalid("app.create", error.code === "ENOENT" ? `Parent directory does not exist: ${parent}` : `Cannot inspect parent directory: ${error.message}`);
39
+ }
40
+ try {
41
+ const templateRoot = context.templateRoot ? resolve(context.templateRoot) : TEMPLATE_ROOT;
42
+ const templatePackage = JSON.parse(await readFile(resolve(templateRoot, "package.json"), "utf8"));
43
+ const gitignore = await readFile(resolve(templateRoot, "gitignore.template"), "utf8");
44
+ const templateConfigPath = resolve(templateRoot, "public", "app-config.json");
45
+ let config = {schemaVersion: "1", title: name, basePath: "/webroot/decision", layout: "overview", dashboardId: "", maxRows: 1000, pageSize: 8, widgets: [], filters: []};
46
+ try {
47
+ config = JSON.parse(await readFile(templateConfigPath, "utf8"));
48
+ } catch (error) {
49
+ if (error.code !== "ENOENT") throw error;
50
+ }
51
+ try {
52
+ await mkdir(destination, {recursive: false});
53
+ } catch (error) {
54
+ return invalid("app.create", error.code === "EEXIST" ? `Destination already exists: ${destination}` : `Cannot create destination: ${error.message}`);
55
+ }
56
+ await copyTemplateContents(templateRoot, destination);
57
+ await writeFile(resolve(destination, ".gitignore"), gitignore, "utf8");
58
+ if(options.value.layout) config.layout=options.value.layout;
59
+ if(config.layout==='cockpit') config.insights=true;
60
+ await mkdir(resolve(destination, "public"), {recursive: true});
61
+ await writeFile(resolve(destination, "package.json"), `${JSON.stringify({...templatePackage, name}, null, 2)}\n`, "utf8");
62
+ await writeFile(resolve(destination, "public", "app-config.json"), `${JSON.stringify({...config, templateVersion: templatePackage.version}, null, 2)}\n`, "utf8");
63
+ const lockPath = resolve(destination, "package-lock.json");
64
+ try {
65
+ const lock = JSON.parse(await readFile(lockPath, "utf8"));
66
+ lock.name = name;
67
+ if (lock.packages?.[""]) lock.packages[""].name = name;
68
+ await writeFile(lockPath, `${JSON.stringify(lock, null, 2)}\n`, "utf8");
69
+ } catch (error) {
70
+ if (error.code !== "ENOENT") throw error;
71
+ }
72
+ return success("app.create", {name, path: destination, templateVersion: templatePackage.version});
73
+ } catch (error) {
74
+ return invalid("app.create", `Could not create project: ${error.message}`);
75
+ }
76
+ }
77
+
78
+ async function copyTemplateContents(sourceRoot, destinationRoot, relativeDirectory = "") {
79
+ const sourceDirectory = resolve(sourceRoot, relativeDirectory);
80
+ const destinationDirectory = resolve(destinationRoot, relativeDirectory);
81
+ for (const entry of (await readdir(sourceDirectory, {withFileTypes: true})).sort((left, right) => left.name.localeCompare(right.name, "en"))) {
82
+ const relativePath = `${relativeDirectory}/${entry.name}`.replace(/^\//u, "");
83
+ if (relativePath === ".gitignore" || relativePath === "gitignore.template") continue;
84
+ if (!templatePathAllowed(relativePath)) continue;
85
+ const sourcePath = resolve(sourceDirectory, entry.name);
86
+ const destinationPath = resolve(destinationDirectory, entry.name);
87
+ if (entry.isDirectory()) {
88
+ await mkdir(destinationPath, {recursive: false});
89
+ await copyTemplateContents(sourceRoot, destinationRoot, relativePath);
90
+ } else if (entry.isFile()) {
91
+ await cp(sourcePath, destinationPath, {force: false, errorOnExist: true});
92
+ } else {
93
+ throw new Error(`Unsupported template entry: ${relativePath}`);
94
+ }
95
+ }
96
+ }
97
+
98
+ function templatePathAllowed(relative) {
99
+ const relativePath = relative.replaceAll("\\", "/");
100
+ const parts = relativePath.split("/").filter(Boolean);
101
+ const basename = parts.at(-1) || "";
102
+ return !parts.some(part => ["node_modules", "dist", "temp", ".git"].includes(part))
103
+ && basename !== ".env"
104
+ && (!basename.startsWith(".env.") || basename === ".env.example");
105
+ }
106
+
107
+ async function inspectApp(args) {
108
+ const options = parseOptions(args, "app.inspect");
109
+ if (!options.ok) return options.error;
110
+ const projectPath = options.value.path;
111
+ if (!projectPath) return invalid("app.inspect", "path must be a project directory.");
112
+ const project = resolve(projectPath);
113
+ try {
114
+ if (!(await stat(project)).isDirectory()) return invalid("app.inspect", "path must point to a project directory.");
115
+ const configPath = resolve(project, "public", "app-config.json");
116
+ const config = JSON.parse(await readFile(configPath, "utf8"));
117
+ if (!config || typeof config !== "object" || Array.isArray(config)) return invalid("app.inspect", "app-config.json must contain a JSON object.");
118
+ if (options.value.strict) {
119
+ const {configErrors}=await import('../templates/web-app/src/finebi/config.js');
120
+ const errors = configErrors(config);
121
+ if (errors.length) return failure("app.inspect", cliError("INVALID_ARGUMENT", {path: "public/app-config.json", message: errors.join(" ")}));
122
+ }
123
+ return success("app.inspect", {
124
+ path: project,
125
+ dashboardId: config.dashboardId ?? null,
126
+ layout: config.layout ?? null,
127
+ directoryEntryId: config.directoryEntryId || null,
128
+ widgets: Array.isArray(config.widgets) ? config.widgets.length : null,
129
+ templateVersion: config.templateVersion || null,
130
+ configValidated: options.value.strict === true,
131
+ remoteValidated: false,
132
+ });
133
+ } catch (error) {
134
+ return invalid("app.inspect", `Cannot read project configuration: ${error.message}`);
135
+ }
136
+ }
137
+
138
+ function parseOptions(args, command) {
139
+ const value = {};
140
+ for (let index = 0; index < args.length; index++) {
141
+ const option = args[index];
142
+ if (option === "--json") continue;
143
+ if (option === "--strict" && command === "app.inspect") {
144
+ if (value.strict) return {ok: false, error: invalid(command, "--strict must only be supplied once.")};
145
+ value.strict = true;
146
+ continue;
147
+ }
148
+ if (!["--name","--path",...(command==='app.create'?['--layout']:[])].includes(option)) return {ok: false, error: invalid(command, `Unsupported option: ${option}`)};
149
+ const key = option.slice(2);
150
+ const next = args[++index];
151
+ if (!next || next.startsWith("--")) return {ok: false, error: invalid(command, `${option} requires a value.`)};
152
+ if (value[key]) return {ok: false, error: invalid(command, `${option} must only be supplied once.`)};
153
+ value[key] = next;
154
+ }
155
+ return {ok: true, value};
156
+ }
157
+
158
+ function invalid(command, message, nextActions = []) {
159
+ return failure(command, cliError("INVALID_ARGUMENT", {path: command, message, nextActions}));
160
+ }
@@ -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();