@finebi/finebi-cli 1.6.0 → 1.6.2

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 (160) hide show
  1. package/README.md +16 -12
  2. package/package.json +1 -1
  3. package/skills/finebi-cli-agent/SKILL.md +43 -123
  4. package/skills/finebi-cli-agent/agents/openai.yaml +1 -1
  5. package/skills/finebi-cli-agent/references/artifact-ordering.md +3 -3
  6. package/skills/finebi-cli-agent/references/command-workflows.md +31 -2
  7. package/skills/finebi-cli-agent/references/dashboard-data-result.md +191 -191
  8. package/skills/finebi-cli-agent/references/dashboard-dsl-api.md +44 -44
  9. package/skills/finebi-cli-agent/references/dsl-handoff-contract.md +1 -1
  10. package/skills/finebi-cli-agent/references/handoff-contract.md +6 -4
  11. package/skills/finebi-cli-agent/references/mutation-confirmation-contract.md +2 -0
  12. package/skills/finebi-cli-agent/references/scenario-workflows.md +10 -8
  13. package/skills/finebi-cli-agent/references/web-app.md +12 -6
  14. package/skills/finebi-cli-agent/references/widget-dsl-core.md +6 -0
  15. package/skills/finebi-cli-agent/scripts/finebi-cli/package.json +1 -1
  16. package/skills/finebi-cli-agent/scripts/finebi-cli/src/app.js +157 -189
  17. package/skills/finebi-cli-agent/scripts/finebi-cli/src/browser-auth.js +3 -4
  18. package/skills/finebi-cli-agent/scripts/finebi-cli/src/config.js +28 -28
  19. package/skills/finebi-cli-agent/scripts/finebi-cli/src/error-catalog.js +2 -2
  20. package/skills/finebi-cli-agent/scripts/finebi-cli/src/help.js +65 -61
  21. package/skills/finebi-cli-agent/scripts/finebi-cli/src/install-skill.js +89 -89
  22. package/skills/finebi-cli-agent/scripts/finebi-cli/src/profiles.js +158 -158
  23. package/skills/finebi-cli-agent/scripts/finebi-cli/src/skill-files.js +94 -94
  24. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/AGENTS.md +23 -7
  25. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/README.md +19 -6
  26. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/acceptance.md +80 -0
  27. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/auth-and-deploy.md +19 -4
  28. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/data-contract.md +48 -3
  29. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/design.md +43 -2
  30. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/filters.md +6 -2
  31. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/examples/cockpit.config.json +22 -0
  32. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/examples/retail.config.json +185 -0
  33. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/index.html +2 -2
  34. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/package-lock.json +2 -2
  35. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/package.json +3 -2
  36. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/public/app-config.json +11 -11
  37. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/scripts/browser-session.mjs +59 -0
  38. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/scripts/dev-proxy.mjs +26 -24
  39. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/scripts/dev-session.mjs +57 -0
  40. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/scripts/session-file.mjs +45 -0
  41. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/App.tsx +63 -31
  42. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/cockpit.css +3 -0
  43. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Chart.tsx +40 -31
  44. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/DataTable.tsx +12 -5
  45. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/FilterBar.tsx +16 -10
  46. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Insights.tsx +17 -0
  47. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/MonthPicker.tsx +32 -0
  48. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/OptionFilter.tsx +7 -0
  49. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/RangeFilter.tsx +9 -0
  50. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Widget.tsx +40 -28
  51. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/calendar-locales.ts +3 -0
  52. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/calendar.ts +14 -14
  53. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Bar.tsx +6 -0
  54. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Line.tsx +6 -0
  55. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Pie.tsx +6 -0
  56. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/month-picker.css +1 -0
  57. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/client.ts +32 -11
  58. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/config.d.ts +3 -0
  59. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/config.js +58 -0
  60. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/date.d.ts +1 -1
  61. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/date.js +5 -5
  62. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/filters.d.ts +4 -4
  63. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/filters.js +106 -106
  64. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/normalize.ts +235 -77
  65. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/types.ts +20 -18
  66. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/main.tsx +5 -5
  67. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/retail.css +13 -0
  68. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/presentation.ts +36 -0
  69. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/table-data.ts +47 -0
  70. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/view-data.ts +18 -18
  71. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/widget-queries.ts +18 -0
  72. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/fixtures.mjs +28 -0
  73. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/index.html +16 -0
  74. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/main.tsx +149 -0
  75. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/test-page.css +4 -0
  76. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/vite.config.mjs +13 -0
  77. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/bundle.test.mjs +19 -0
  78. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/cockpit.test.mjs +73 -0
  79. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/field-selection.test.mjs +70 -0
  80. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/proxy.test.mjs +31 -4
  81. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/retail.test.mjs +31 -0
  82. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/runtime.test.mjs +8 -1
  83. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/session.test.mjs +91 -0
  84. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/table-normalize.test.mjs +189 -0
  85. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/vite-auth.test.mjs +41 -0
  86. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tsconfig.json +4 -4
  87. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/vite.config.ts +11 -4
  88. package/skills/finebi-cli-agent/scripts/finebi-cli-runner.mjs +4 -4
  89. package/src/app.js +157 -189
  90. package/src/browser-auth.js +3 -4
  91. package/src/config.js +28 -28
  92. package/src/error-catalog.js +2 -2
  93. package/src/help.js +65 -61
  94. package/src/install-skill.js +89 -89
  95. package/src/profiles.js +158 -158
  96. package/src/skill-files.js +94 -94
  97. package/templates/web-app/AGENTS.md +23 -7
  98. package/templates/web-app/README.md +19 -6
  99. package/templates/web-app/docs/acceptance.md +80 -0
  100. package/templates/web-app/docs/auth-and-deploy.md +19 -4
  101. package/templates/web-app/docs/data-contract.md +48 -3
  102. package/templates/web-app/docs/design.md +43 -2
  103. package/templates/web-app/docs/filters.md +6 -2
  104. package/templates/web-app/examples/cockpit.config.json +22 -0
  105. package/templates/web-app/examples/retail.config.json +185 -0
  106. package/templates/web-app/index.html +2 -2
  107. package/templates/web-app/package-lock.json +2 -2
  108. package/templates/web-app/package.json +3 -2
  109. package/templates/web-app/public/app-config.json +11 -11
  110. package/templates/web-app/scripts/browser-session.mjs +59 -0
  111. package/templates/web-app/scripts/dev-proxy.mjs +26 -24
  112. package/templates/web-app/scripts/dev-session.mjs +57 -0
  113. package/templates/web-app/scripts/session-file.mjs +45 -0
  114. package/templates/web-app/src/App.tsx +63 -31
  115. package/templates/web-app/src/cockpit.css +3 -0
  116. package/templates/web-app/src/components/Chart.tsx +40 -31
  117. package/templates/web-app/src/components/DataTable.tsx +12 -5
  118. package/templates/web-app/src/components/FilterBar.tsx +16 -10
  119. package/templates/web-app/src/components/Insights.tsx +17 -0
  120. package/templates/web-app/src/components/MonthPicker.tsx +32 -0
  121. package/templates/web-app/src/components/OptionFilter.tsx +7 -0
  122. package/templates/web-app/src/components/RangeFilter.tsx +9 -0
  123. package/templates/web-app/src/components/Widget.tsx +40 -28
  124. package/templates/web-app/src/components/calendar-locales.ts +3 -0
  125. package/templates/web-app/src/components/calendar.ts +14 -14
  126. package/templates/web-app/src/components/charts/Bar.tsx +6 -0
  127. package/templates/web-app/src/components/charts/Line.tsx +6 -0
  128. package/templates/web-app/src/components/charts/Pie.tsx +6 -0
  129. package/templates/web-app/src/components/month-picker.css +1 -0
  130. package/templates/web-app/src/finebi/client.ts +32 -11
  131. package/templates/web-app/src/finebi/config.d.ts +3 -0
  132. package/templates/web-app/src/finebi/config.js +58 -0
  133. package/templates/web-app/src/finebi/date.d.ts +1 -1
  134. package/templates/web-app/src/finebi/date.js +5 -5
  135. package/templates/web-app/src/finebi/filters.d.ts +4 -4
  136. package/templates/web-app/src/finebi/filters.js +106 -106
  137. package/templates/web-app/src/finebi/normalize.ts +235 -77
  138. package/templates/web-app/src/finebi/types.ts +20 -18
  139. package/templates/web-app/src/main.tsx +5 -5
  140. package/templates/web-app/src/retail.css +13 -0
  141. package/templates/web-app/src/services/presentation.ts +36 -0
  142. package/templates/web-app/src/services/table-data.ts +47 -0
  143. package/templates/web-app/src/services/view-data.ts +18 -18
  144. package/templates/web-app/src/services/widget-queries.ts +18 -0
  145. package/templates/web-app/tests/browser/fixtures.mjs +28 -0
  146. package/templates/web-app/tests/browser/index.html +16 -0
  147. package/templates/web-app/tests/browser/main.tsx +149 -0
  148. package/templates/web-app/tests/browser/test-page.css +4 -0
  149. package/templates/web-app/tests/browser/vite.config.mjs +13 -0
  150. package/templates/web-app/tests/bundle.test.mjs +19 -0
  151. package/templates/web-app/tests/cockpit.test.mjs +73 -0
  152. package/templates/web-app/tests/field-selection.test.mjs +70 -0
  153. package/templates/web-app/tests/proxy.test.mjs +31 -4
  154. package/templates/web-app/tests/retail.test.mjs +31 -0
  155. package/templates/web-app/tests/runtime.test.mjs +8 -1
  156. package/templates/web-app/tests/session.test.mjs +91 -0
  157. package/templates/web-app/tests/table-normalize.test.mjs +189 -0
  158. package/templates/web-app/tests/vite-auth.test.mjs +41 -0
  159. package/templates/web-app/tsconfig.json +4 -4
  160. package/templates/web-app/vite.config.ts +11 -4
@@ -4,7 +4,7 @@
4
4
 
5
5
  仅当用户要根据业务描述创建或更新 Widget/Dashboard,且命令需要 JS DSL 文件时使用本文件。资源查询和执行边界仍遵循 `SKILL.md`、`command-workflows.md` 与 `mutation-confirmation-contract.md`;仅 `delete` 命令需要人工确认。
6
6
 
7
- 本 Skill 负责解析语义和消费已验证 JS 制品,不生成、修改、解析或修复 DSL。
7
+ 按入口声明的职责处理 DSL:完整 Agent 在同一任务内生成、回读并修复本次 JS 制品;独立场景规划器只解析语义、消费外部已验证制品。共享流程不扩大独立规划器的文件写入权限。生成本地文件与执行 FineBI 写命令分别遵循用户请求范围。
8
8
 
9
9
  ## 2. 解析业务语义
10
10
 
@@ -12,8 +12,8 @@
12
12
 
13
13
  依赖只读命令解析:
14
14
 
15
- 1. 遍历 `subject.search` 全部分页并用 `subject.info` 确认主题。
16
- 2. 需要按表名解析数据集时,先用用户提供的完整名称搜索 `analysis-ds` 和 `public-ds`,遍历全部分页,并按返回的真实名称全等判断唯一精确匹配。
15
+ 1. 已知主题 ID 时直接用 `subject.info` 核验;只有名称或归属不清时才搜索主题,并在证明名称唯一前遍历全部分页。同一连续任务中前提未变化的已核实主题证据可以复用。
16
+ 2. 已知数据集 ID 时直接用 `analysis-ds.info` 核验主题、名称和所需字段;需要按表名解析数据集时,先用用户提供的完整名称搜索 `analysis-ds` 和 `public-ds`,遍历全部分页,并按返回的真实名称全等判断唯一精确匹配。
17
17
  3. 没有精确匹配时,对分析表和公共数据集执行关键词搜索,结合字段、位置、主题归属、资源类型、引擎类型和用户意图选择候选;多个候选合理时要求用户选择。
18
18
  4. 选定资源为 `public-ds` 时,将其作为来源按前置资源流程创建目标主题内的 `analysis-ds`,使用写响应中的全部实际 ID 逐一调用 `analysis-ds.info` 和必要的 `preview`;转换完成前不得构建语义交接对象,也不得继续查询数据连接。
19
19
  5. 只有数据集层没有合理候选时,才查询数据连接和物理表,并按前置资源流程创建所需数据集;不得把物理表标识当作数据集 ID。
@@ -34,13 +34,15 @@
34
34
 
35
35
  搜索命中公共数据集时,`analysis-ds.add` 属于交接所需的前置资源创建:DSL 的 `from(表原始名)` 必须引用已验证的公共数据集原始名(`public-ds search` 返回的 `id`,全局唯一),目标主题通过 `--subject-id` 传入;执行后逐一回查服务端返回的分析表实际 ID。不得在前置资源尚未创建时向交接对象填入公共数据集 ID、预期 ID 或占位 ID。
36
36
 
37
- ## 4. 语义交接确认
37
+ ## 4. 语义记录与校验
38
38
 
39
- 按照 `handoff-contract.md` 构建完整 JSON 交接对象并展示。语义交接确认只确认制品生成输入,不授权执行任何 CLI 写命令。用户修改任何影响制品的语义后,原交接确认及已有制品映射均失效。
39
+ 同一 Agent 连续完成生成和执行时,记录本次实际用到的主题、来源与字段、关系、图表/布局意图、组件引用和产物路径;按 `handoff-contract.md` 和 DSL 输入契约核验这些信息,不必构造或展示完整交接 JSON。跨执行体交接时使用完整 `contractVersion: "1.0"` JSON 与文件映射。
40
+
41
+ 业务目标明确时直接继续,不为内部语义记录或产物路径增加审批;只有业务歧义未消除或用户明确要求先审阅时才等待。用户改变影响制品的语义后,更新记录、重新生成受影响文件并废止对应旧写计划。语义审阅不代替删除计划确认;仅要求本地生成不授权 FineBI 写命令。
40
42
 
41
43
  ## 5. JS 制品映射
42
44
 
43
- 仅接收调用方、用户或实际可用生成工作流提供的完整 `artifact key -> absolute JS file path` 映射。每个路径必须:
45
+ 有 DSL 生成职责的执行体自行安排当前任务隔离目录中的唯一绝对路径,或使用用户指定路径;按实际涉及的 API 生成并回读自检后,记录完整 `artifact key -> absolute JS file path` 映射。独立场景规划器仅接收调用方、用户或可用生成工作流提供的现有制品映射,不自行生成或修复 DSL。冻结远端写计划前,每个路径必须:
44
46
 
45
47
  - 是现有 `.js` 文件的绝对路径;
46
48
  - 由用户明确提供,或位于为当前任务创建的隔离临时目录;不得根据当前工作目录猜测 `cli/` 或其他项目路径;
@@ -48,7 +50,7 @@
48
50
  - 与其他 artifact key 的规范路径不同;
49
51
  - 在冻结写计划时记录 SHA-256。
50
52
 
51
- 缺少 key、路径不存在、路径重复或文件被修改时进入 `BLOCKED` 或废止当前写计划,不在本 Skill 中修复文件。
53
+ 缺少 key、路径不存在、路径重复或文件被修改时,不执行旧写计划。具有生成职责时先补齐或修复本次文件并重新校验;独立规划器报告缺失制品并等待提供。无法满足边界时进入 `BLOCKED`。
52
54
 
53
55
  ## 6. 创建 Widget
54
56
 
@@ -72,4 +74,4 @@
72
74
 
73
75
  以下情况进入 `BLOCKED`:运行时缺少所需命令;资源/字段/引用存在歧义;主题或数据集归属不匹配;JS 映射不完整或文件无效;无法证明 Dashboard 引用组件唯一;运行时 schema 与冻结计划不一致。
74
76
 
75
- 只是在等待语义确认、删除计划确认或包含删除的清理计划确认时,状态应为 `AWAITING_CONFIRMATION`,不是 `BLOCKED`。
77
+ 等待用户明确要求的语义审阅、删除计划确认或包含删除的清理计划确认时,状态为 `AWAITING_CONFIRMATION`。明确任务中的内部语义记录不经过该状态。
@@ -2,12 +2,18 @@
2
2
 
3
3
  用户要求生成动态页面、HTML看板或FineBI Web应用时,先选择同一CLI安装的本地 `app create`,再按生成项目AGENTS.md开发。创建模板不需要连接;读取/绑定FineBI资源才检查认证和实时命令help。
4
4
 
5
- 1. `finebi-cli app create --name <name> --path <parent>` 创建工程,不安装/运行/发布。
6
- 2. 用dashboard search/info/data确认唯一来源、板内entryId、结果字段dId、真实控件filterName;样本仅用于理解和测试。
7
- 3. 在public/app-config.json填写非敏感绑定。图表库和UI组件库按页面需求选择,默认实现及替换约定见生成项目的docs/design.md。请求、认证、解析复用模板src/finebi;业务代码按项目指南开发。
8
- 4. `finebi-cli app inspect --path <project>`只校验本地配置,不能证明字段/权限在远端存在。按项目README配置开发身份,再安装、启动、测试与构建。
9
- 5. 浏览器验收实际请求、空/错误/权限状态、图表与刷新;生产部署必须用访问者Cookie,不携带开发身份。
5
+ 1. `finebi-cli app create --name <name> --path <parent> [--layout retail]` 创建工程,不安装/运行/发布。米白横向经营简报可选 retail,绿色驾驶舱可选 cockpit;默认 overview,另有 workbench。
6
+ 2. 用dashboard search/info/data确认唯一来源、板内entryId、结果字段dId、真实控件filterName;按生成项目 docs/acceptance.md 记录指标口径、单位/时区、筛选范围和数据完整性。样本仅用于理解和测试。
7
+ 3. 按生成项目 AGENTS.md 的“阅读路径”分阶段读取文档,不要求开场通读全部说明。先按 docs/acceptance.md 的“最小真实查询”只配置一个真实组件,按 README 安装依赖、配置开发身份并启动实际页面,验证开发代理查询与页面值。CLI 成功不能替代这一步;优先复用已有会话,不重复索要可用身份的密码。
8
+ 4. 最小真实查询及适用的代表性图表、筛选链路通过后,再扩展整页绑定。按 docs/design.md 对应章节确定首屏、导航、图表分组和风格差异;请求、认证、解析复用模板src/finebi,图表库和UI组件库按页面需求选择。需要查看绑定摘要时可运行 `finebi-cli app inspect --path <project>`,默认不校验模板字段或展示格式,也不证明应用可用。只有需要检查与 CLI 内置模板的兼容性时才显式使用 `--strict`;定制应用无需通过该检查,不要为消除 strict 报错而删除自定义能力或反复修改应用。验收以项目自身测试、构建和真实浏览器验证为准。
9
+ 5. 按生成项目 docs/acceptance.md 分别完成自动测试、构建、test:browser 合成数据验收和真实页面验收,检查整页及窄屏,记录 CLI、开发身份、生产 Cookie 各自的已验证与未验证项。生产部署必须用访问者Cookie,不携带开发身份。
10
10
 
11
- 内置CLI同样通过 `node "<skill-dir>/scripts/finebi-cli/src/index.js" app ...` 调用,不走动态业务runner。生产页面不调用CLI。禁止将开发时查到的业务数据或token写入HTML/JS/config;无筛选控件不能伪造服务端过滤。pageLimit不是分页,hasNext和warnings必须呈现。v1不提供发布上传/目录来源/服务端续页,先说明缺口,不发明命令。
11
+ 内置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
+
13
+ cockpit 的 examples/cockpit.config.json 仅包含占位绑定,必须替换成真实资源和结果 fieldId;多模型显式 datasetId。布局含可配置 KPI、多系列趋势、分类对比、通栏表格及可选观察/口径。年月为自定义十二月/年份网格面板,选中只改 YYYY-MM 草稿。表格搜索、精确排序、筛选、分页、CSV 只处理已加载结果;观察不内置销售公式。widgets[].filterNames 来自真实映射,[] 表示不传覆盖,省略由源看板决定,不宣称未知映射为全局联动。请求/认证仍复用 src/finebi,不能复制个人 CLI 子进程后端。
14
+
15
+ retail 的 examples/retail.config.json 提供零售场景占位绑定,字段与口径须按同一来源核实;展示格式见生成项目 docs/design.md。不要根据原组件标题猜指标含义,也不要把记录数自动称为去重门店数。真实业务响应和分析结论不进入分发模板。
16
+
17
+ 分组表 BIGROUP 可绑定为 type: "table",使用 main 数据集与结果字段 dId;保留父级分组、小计、总计及 UNKNOWN 行,不将汇总和明细混加。日期/数值分组键只保证展示语义,不能按显示文本恢复原值。交叉表 BICROSS 可用 table 展示当前矩阵,通常省略 tableFieldIds/localFilterFieldIds;cross:row、cross:0 等为本次查询槽位,不是 FineBI dId,不能用于 dimensionIds 或固定跨查询图形/KPI 绑定。保留原生行列路径、汇总标记与双轴分页警告,不把当前矩阵宣称为全量。复用模板的行、列指标标题去重(保留真实同名分组层级与原始 path)、DATA 行内部“合计”节点隐藏和 NULL 展示规则,不把真实空值当作不可用或补零;详细判定与范围见生成项目 docs/data-contract.md。
12
18
 
13
19
  模板内置文本、单选/多选、数值选择、日期及日期区间、年月季度、年月区间和已配置时间标签;草稿统一应用,恢复默认与筛选空值分开。配置、状态、请求编码和代理校验共用 src/finebi/filters.js。数值范围、滑块、树/级联和动态候选项暂不支持,不得换成其他类型绕过检查。绑定前核实源控件类型和作用范围,候选项不得冒充完整数据。通用约定、完整支持矩阵、值格式及扩展步骤见生成项目 docs/filters.md。
@@ -273,6 +273,12 @@ warning 留痕。
273
273
  - `QuickCalculationType.YEAR_ON_YEAR_*`:同比
274
274
  - `QuickCalculationType.MONTH_ON_MONTH_*`:环比
275
275
 
276
+ 快速计算作用于指标引用,可用于 `metric`、`value`、`angle`、`text` 指标槽位,不能用于维度。
277
+ `secondaryCalculation` 只允许用户可切换二次计算的类型显式传入;这类类型省略时默认开启,固定策略类型必须省略该参数。
278
+ 内置同比、环比要求组件分析区域存在兼容的显示日期维度。
279
+ 完整的组件写法见[快速计算二次开关示例](widget-dsl-api-examples.md#1022-快速计算二次开关)。
280
+ `widget info` 的结构化回读与文本 DSL 是不同输出面,不能用回读字段的枚举范围替代文本 DSL 的能力范围。
281
+
276
282
  ### 2.5 主题共享钻取目录
277
283
 
278
284
  主题共享钻取目录使用独立命令 `drill-directory update --subject-id <id> --file <directory.js>`:
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finebi/finebi-cli",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "private": true,
5
5
  "description": "FineBI dynamic capability command line client.",
6
6
  "license": "UNLICENSED",
@@ -1,189 +1,157 @@
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
- const parent = resolve(parentPath);
30
- const destination = resolve(parent, name);
31
- if (dirname(destination) !== parent) {
32
- return invalid("app.create", "name must stay inside the selected parent directory.");
33
- }
34
- try {
35
- if (!(await stat(parent)).isDirectory()) return invalid("app.create", "path must point to a directory.");
36
- } catch (error) {
37
- return invalid("app.create", error.code === "ENOENT" ? `Parent directory does not exist: ${parent}` : `Cannot inspect parent directory: ${error.message}`);
38
- }
39
- try {
40
- const templateRoot = context.templateRoot ? resolve(context.templateRoot) : TEMPLATE_ROOT;
41
- const templatePackage = JSON.parse(await readFile(resolve(templateRoot, "package.json"), "utf8"));
42
- const templateConfigPath = resolve(templateRoot, "public", "app-config.json");
43
- let config = {schemaVersion: "1", title: name, basePath: "/webroot/decision", layout: "overview", dashboardId: "", maxRows: 1000, pageSize: 8, widgets: [], filters: []};
44
- try {
45
- config = JSON.parse(await readFile(templateConfigPath, "utf8"));
46
- } catch (error) {
47
- if (error.code !== "ENOENT") throw error;
48
- }
49
- try {
50
- await mkdir(destination, {recursive: false});
51
- } catch (error) {
52
- return invalid("app.create", error.code === "EEXIST" ? `Destination already exists: ${destination}` : `Cannot create destination: ${error.message}`);
53
- }
54
- await copyTemplateContents(templateRoot, destination);
55
- await mkdir(resolve(destination, "public"), {recursive: true});
56
- await writeFile(resolve(destination, "package.json"), `${JSON.stringify({...templatePackage, name}, null, 2)}\n`, "utf8");
57
- await writeFile(resolve(destination, "public", "app-config.json"), `${JSON.stringify({...config, templateVersion: templatePackage.version}, null, 2)}\n`, "utf8");
58
- const lockPath = resolve(destination, "package-lock.json");
59
- try {
60
- const lock = JSON.parse(await readFile(lockPath, "utf8"));
61
- lock.name = name;
62
- if (lock.packages?.[""]) lock.packages[""].name = name;
63
- await writeFile(lockPath, `${JSON.stringify(lock, null, 2)}\n`, "utf8");
64
- } catch (error) {
65
- if (error.code !== "ENOENT") throw error;
66
- }
67
- return success("app.create", {name, path: destination, templateVersion: templatePackage.version});
68
- } catch (error) {
69
- return invalid("app.create", `Could not create project: ${error.message}`);
70
- }
71
- }
72
-
73
- async function copyTemplateContents(sourceRoot, destinationRoot, relativeDirectory = "") {
74
- const sourceDirectory = resolve(sourceRoot, relativeDirectory);
75
- const destinationDirectory = resolve(destinationRoot, relativeDirectory);
76
- for (const entry of (await readdir(sourceDirectory, {withFileTypes: true})).sort((left, right) => left.name.localeCompare(right.name, "en"))) {
77
- const relativePath = `${relativeDirectory}/${entry.name}`.replace(/^\//u, "");
78
- if (!templatePathAllowed(relativePath)) continue;
79
- const sourcePath = resolve(sourceDirectory, entry.name);
80
- const destinationPath = resolve(destinationDirectory, entry.name);
81
- if (entry.isDirectory()) {
82
- await mkdir(destinationPath, {recursive: false});
83
- await copyTemplateContents(sourceRoot, destinationRoot, relativePath);
84
- } else if (entry.isFile()) {
85
- await cp(sourcePath, destinationPath, {force: false, errorOnExist: true});
86
- } else {
87
- throw new Error(`Unsupported template entry: ${relativePath}`);
88
- }
89
- }
90
- }
91
-
92
- function templatePathAllowed(relative) {
93
- const relativePath = relative.replaceAll("\\", "/");
94
- const parts = relativePath.split("/").filter(Boolean);
95
- const basename = parts.at(-1) || "";
96
- return !parts.some(part => ["node_modules", "dist", "temp", ".git"].includes(part))
97
- && basename !== ".env"
98
- && (!basename.startsWith(".env.") || basename === ".env.example");
99
- }
100
-
101
- async function inspectApp(args) {
102
- const options = parseOptions(args, "app.inspect");
103
- if (!options.ok) return options.error;
104
- const projectPath = options.value.path;
105
- if (!projectPath) return invalid("app.inspect", "path must be a project directory.");
106
- const project = resolve(projectPath);
107
- try {
108
- if (!(await stat(project)).isDirectory()) return invalid("app.inspect", "path must point to a project directory.");
109
- const configPath = resolve(project, "public", "app-config.json");
110
- const config = JSON.parse(await readFile(configPath, "utf8"));
111
- const errors = await validateConfig(config);
112
- if (errors.length) return failure("app.inspect", cliError("INVALID_ARGUMENT", {path: "public/app-config.json", message: errors.join(" ")}));
113
- return success("app.inspect", {
114
- path: project,
115
- dashboardId: config.dashboardId,
116
- widgets: config.widgets.length,
117
- templateVersion: config.templateVersion || null,
118
- remoteValidated: false,
119
- });
120
- } catch (error) {
121
- return invalid("app.inspect", `Cannot read project configuration: ${error.message}`);
122
- }
123
- }
124
-
125
- async function validateConfig(config) {
126
- const errors = [];
127
- if (!config || typeof config !== "object" || Array.isArray(config)) return ["configuration must be a JSON object."];
128
- const allowedConfigFields = new Set(["schemaVersion", "title", "basePath", "layout", "dashboardId", "maxRows", "pageSize", "widgets", "filters", "templateVersion"]);
129
- for (const field of Object.keys(config)) if (!allowedConfigFields.has(field)) errors.push(`unsupported config field: ${field}.`);
130
- if (config.schemaVersion !== "1") errors.push("schemaVersion must be \"1\".");
131
- if (!nonEmptyString(config.title)) errors.push("title is required.");
132
- if (!nonEmptyString(config.basePath) || !/^\/(?!\/)[\w/-]+$/.test(config.basePath)) errors.push("basePath must be a same-origin path.");
133
- if (!["overview", "workbench"].includes(config.layout)) errors.push("layout must be overview or workbench.");
134
- if (!Number.isInteger(config.maxRows) || config.maxRows < 1 || config.maxRows > 10000) errors.push("maxRows must be an integer from 1 to 10000.");
135
- if (!Number.isInteger(config.pageSize) || config.pageSize < 1 || config.pageSize > 100) errors.push("pageSize must be an integer from 1 to 100.");
136
- if (typeof config.dashboardId !== "string" || !config.dashboardId.trim()) {
137
- if (Array.isArray(config.widgets) && config.widgets.length === 0) return ["project is not bound: dashboardId and widgets must be configured."];
138
- errors.push("dashboardId is required.");
139
- }
140
- if (!Array.isArray(config.widgets) || config.widgets.length === 0) {
141
- errors.push("widgets must be a non-empty array.");
142
- return errors;
143
- }
144
- for (const [index, widget] of config.widgets.entries()) {
145
- if (!widget || typeof widget !== "object") {
146
- errors.push(`widgets[${index}] must be an object.`);
147
- continue;
148
- }
149
- const allowedWidgetFields = new Set(["entryId", "name", "type", "datasetId", "categoryFieldId", "valueFieldId"]);
150
- for (const field of Object.keys(widget)) if (!allowedWidgetFields.has(field)) errors.push(`widgets[${index}].${field} is unsupported.`);
151
- for (const field of ["entryId", "name", "type"]) {
152
- if (typeof widget[field] !== "string" || !widget[field].trim()) errors.push(`widgets[${index}].${field} is required.`);
153
- }
154
- if (!["table", "bar", "line", "pie", "kpi"].includes(widget.type)) errors.push(`widgets[${index}].type is unsupported.`);
155
- if (widget.type && widget.type !== "table" && !nonEmptyString(widget.valueFieldId)) errors.push(`widgets[${index}].valueFieldId is required for ${widget.type}.`);
156
- if (widget.type && !["table", "kpi"].includes(widget.type) && !nonEmptyString(widget.categoryFieldId)) errors.push(`widgets[${index}].categoryFieldId is required for ${widget.type}.`);
157
- for (const field of ["datasetId", "categoryFieldId", "valueFieldId"]) {
158
- if (widget[field] !== undefined && !nonEmptyString(widget[field])) errors.push(`widgets[${index}].${field} must be a non-empty string.`);
159
- }
160
- }
161
- try {
162
- const {validateFilterBindings} = await import("../templates/web-app/src/finebi/filters.js");
163
- validateFilterBindings(config.filters);
164
- } catch (error) {errors.push(error.message);}
165
- return errors;
166
- }
167
-
168
- function nonEmptyString(value) {
169
- return typeof value === "string" && value.trim().length > 0;
170
- }
171
-
172
- function parseOptions(args, command) {
173
- const value = {};
174
- for (let index = 0; index < args.length; index++) {
175
- const option = args[index];
176
- if (option === "--json") continue;
177
- if (option !== "--name" && option !== "--path") return {ok: false, error: invalid(command, `Unsupported option: ${option}`)};
178
- const key = option.slice(2);
179
- const next = args[++index];
180
- if (!next || next.startsWith("--")) return {ok: false, error: invalid(command, `${option} requires a value.`)};
181
- if (value[key]) return {ok: false, error: invalid(command, `${option} must only be supplied once.`)};
182
- value[key] = next;
183
- }
184
- return {ok: true, value};
185
- }
186
-
187
- function invalid(command, message, nextActions = []) {
188
- return failure(command, cliError("INVALID_ARGUMENT", {path: command, message, nextActions}));
189
- }
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 templateConfigPath = resolve(templateRoot, "public", "app-config.json");
44
+ let config = {schemaVersion: "1", title: name, basePath: "/webroot/decision", layout: "overview", dashboardId: "", maxRows: 1000, pageSize: 8, widgets: [], filters: []};
45
+ try {
46
+ config = JSON.parse(await readFile(templateConfigPath, "utf8"));
47
+ } catch (error) {
48
+ if (error.code !== "ENOENT") throw error;
49
+ }
50
+ try {
51
+ await mkdir(destination, {recursive: false});
52
+ } catch (error) {
53
+ return invalid("app.create", error.code === "EEXIST" ? `Destination already exists: ${destination}` : `Cannot create destination: ${error.message}`);
54
+ }
55
+ await copyTemplateContents(templateRoot, destination);
56
+ if(options.value.layout) config.layout=options.value.layout;
57
+ if(config.layout==='cockpit') config.insights=true;
58
+ await mkdir(resolve(destination, "public"), {recursive: true});
59
+ await writeFile(resolve(destination, "package.json"), `${JSON.stringify({...templatePackage, name}, null, 2)}\n`, "utf8");
60
+ await writeFile(resolve(destination, "public", "app-config.json"), `${JSON.stringify({...config, templateVersion: templatePackage.version}, null, 2)}\n`, "utf8");
61
+ const lockPath = resolve(destination, "package-lock.json");
62
+ try {
63
+ const lock = JSON.parse(await readFile(lockPath, "utf8"));
64
+ lock.name = name;
65
+ if (lock.packages?.[""]) lock.packages[""].name = name;
66
+ await writeFile(lockPath, `${JSON.stringify(lock, null, 2)}\n`, "utf8");
67
+ } catch (error) {
68
+ if (error.code !== "ENOENT") throw error;
69
+ }
70
+ return success("app.create", {name, path: destination, templateVersion: templatePackage.version});
71
+ } catch (error) {
72
+ return invalid("app.create", `Could not create project: ${error.message}`);
73
+ }
74
+ }
75
+
76
+ async function copyTemplateContents(sourceRoot, destinationRoot, relativeDirectory = "") {
77
+ const sourceDirectory = resolve(sourceRoot, relativeDirectory);
78
+ const destinationDirectory = resolve(destinationRoot, relativeDirectory);
79
+ for (const entry of (await readdir(sourceDirectory, {withFileTypes: true})).sort((left, right) => left.name.localeCompare(right.name, "en"))) {
80
+ const relativePath = `${relativeDirectory}/${entry.name}`.replace(/^\//u, "");
81
+ if (!templatePathAllowed(relativePath)) continue;
82
+ const sourcePath = resolve(sourceDirectory, entry.name);
83
+ const destinationPath = resolve(destinationDirectory, entry.name);
84
+ if (entry.isDirectory()) {
85
+ await mkdir(destinationPath, {recursive: false});
86
+ await copyTemplateContents(sourceRoot, destinationRoot, relativePath);
87
+ } else if (entry.isFile()) {
88
+ await cp(sourcePath, destinationPath, {force: false, errorOnExist: true});
89
+ } else {
90
+ throw new Error(`Unsupported template entry: ${relativePath}`);
91
+ }
92
+ }
93
+ }
94
+
95
+ function templatePathAllowed(relative) {
96
+ const relativePath = relative.replaceAll("\\", "/");
97
+ const parts = relativePath.split("/").filter(Boolean);
98
+ const basename = parts.at(-1) || "";
99
+ return !parts.some(part => ["node_modules", "dist", "temp", ".git"].includes(part))
100
+ && basename !== ".env"
101
+ && (!basename.startsWith(".env.") || basename === ".env.example");
102
+ }
103
+
104
+ async function inspectApp(args) {
105
+ const options = parseOptions(args, "app.inspect");
106
+ if (!options.ok) return options.error;
107
+ const projectPath = options.value.path;
108
+ if (!projectPath) return invalid("app.inspect", "path must be a project directory.");
109
+ const project = resolve(projectPath);
110
+ try {
111
+ if (!(await stat(project)).isDirectory()) return invalid("app.inspect", "path must point to a project directory.");
112
+ const configPath = resolve(project, "public", "app-config.json");
113
+ const config = JSON.parse(await readFile(configPath, "utf8"));
114
+ if (!config || typeof config !== "object" || Array.isArray(config)) return invalid("app.inspect", "app-config.json must contain a JSON object.");
115
+ if (options.value.strict) {
116
+ const {configErrors}=await import('../templates/web-app/src/finebi/config.js');
117
+ const errors = configErrors(config);
118
+ if (errors.length) return failure("app.inspect", cliError("INVALID_ARGUMENT", {path: "public/app-config.json", message: errors.join(" ")}));
119
+ }
120
+ return success("app.inspect", {
121
+ path: project,
122
+ dashboardId: config.dashboardId ?? null,
123
+ layout: config.layout ?? null,
124
+ directoryEntryId: config.directoryEntryId || null,
125
+ widgets: Array.isArray(config.widgets) ? config.widgets.length : null,
126
+ templateVersion: config.templateVersion || null,
127
+ configValidated: options.value.strict === true,
128
+ remoteValidated: false,
129
+ });
130
+ } catch (error) {
131
+ return invalid("app.inspect", `Cannot read project configuration: ${error.message}`);
132
+ }
133
+ }
134
+
135
+ function parseOptions(args, command) {
136
+ const value = {};
137
+ for (let index = 0; index < args.length; index++) {
138
+ const option = args[index];
139
+ if (option === "--json") continue;
140
+ if (option === "--strict" && command === "app.inspect") {
141
+ if (value.strict) return {ok: false, error: invalid(command, "--strict must only be supplied once.")};
142
+ value.strict = true;
143
+ continue;
144
+ }
145
+ if (!["--name","--path",...(command==='app.create'?['--layout']:[])].includes(option)) return {ok: false, error: invalid(command, `Unsupported option: ${option}`)};
146
+ const key = option.slice(2);
147
+ const next = args[++index];
148
+ if (!next || next.startsWith("--")) return {ok: false, error: invalid(command, `${option} requires a value.`)};
149
+ if (value[key]) return {ok: false, error: invalid(command, `${option} must only be supplied once.`)};
150
+ value[key] = next;
151
+ }
152
+ return {ok: true, value};
153
+ }
154
+
155
+ function invalid(command, message, nextActions = []) {
156
+ return failure(command, cliError("INVALID_ARGUMENT", {path: command, message, nextActions}));
157
+ }
@@ -160,9 +160,8 @@ function abortedAuthorization() {
160
160
  function validateBaseUrl(value) {
161
161
  try {
162
162
  const url = new URL(value);
163
- const localHttp = url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]", "::1"].includes(url.hostname.toLowerCase());
164
- if (url.protocol !== "https:" && !localHttp) {
165
- throw new Error("Browser authorization requires HTTPS, except for localhost development.");
163
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
164
+ throw new Error("Browser authorization requires an HTTP or HTTPS URL.");
166
165
  }
167
166
  if (url.username || url.password || url.search || url.hash) {
168
167
  throw new Error("Browser authorization base URL must not contain credentials, a query, or a fragment.");
@@ -172,7 +171,7 @@ function validateBaseUrl(value) {
172
171
  return failure("auth.login", cliError("BROWSER_AUTH_URL_INVALID", {
173
172
  path: "baseUrl",
174
173
  message: error instanceof Error ? error.message : String(error),
175
- nextActions: ["finebi-cli config set base-url <https-url>"],
174
+ nextActions: ["finebi-cli config set base-url <http-or-https-url>"],
176
175
  }));
177
176
  }
178
177
  }
@@ -8,7 +8,7 @@ import {CLI_CONFIG_DIRECTORY} from "./branding.js";
8
8
  import {maskSecret} from "./output.js";
9
9
 
10
10
  export const DEFAULT_TIMEOUT_MS = 30000;
11
- export const DEFAULT_CAPABILITY_TTL_MS = 600000;
11
+ export const DEFAULT_CAPABILITY_TTL_MS = 20 * 60 * 60 * 1000;
12
12
 
13
13
  export function defaultConfigPath(env = process.env) {
14
14
  const home = env.USERPROFILE || env.HOME || homedir();
@@ -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();
@@ -19,8 +19,8 @@ export const ERROR_CATALOG = [
19
19
  recoverableByAgent: true,
20
20
  defaultRetryable: false,
21
21
  pathSemantics: "Browser authorization base URL",
22
- canonicalNextActions: ["finebi-cli config set base-url <https-url>"],
23
- expectedAgentBehaviors: ["configure an HTTPS URL or localhost HTTP URL", "rerun browser login"],
22
+ canonicalNextActions: ["finebi-cli config set base-url <http-or-https-url>"],
23
+ expectedAgentBehaviors: ["configure an HTTP or HTTPS URL", "rerun browser login"],
24
24
  benchmarkTags: ["auth", "browser", "url-validation"],
25
25
  },
26
26
  {