@finebi/finebi-cli 1.5.0 → 1.6.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 (152) hide show
  1. package/README.md +17 -3
  2. package/package.json +4 -3
  3. package/skills/finebi-cli-agent/SKILL.md +8 -4
  4. package/skills/finebi-cli-agent/references/command-workflows.md +8 -4
  5. package/skills/finebi-cli-agent/references/dashboard-data-result.md +191 -191
  6. package/skills/finebi-cli-agent/references/dashboard-dsl-api.md +8 -1018
  7. package/skills/finebi-cli-agent/references/dashboard-dsl-core.md +194 -0
  8. package/skills/finebi-cli-agent/references/dashboard-dsl-examples.md +378 -0
  9. package/skills/finebi-cli-agent/references/dashboard-dsl-items.md +456 -0
  10. package/skills/finebi-cli-agent/references/mapping-rules.md +14 -14
  11. package/skills/finebi-cli-agent/references/scenario-workflows.md +1 -1
  12. package/skills/finebi-cli-agent/references/web-app.md +17 -0
  13. package/skills/finebi-cli-agent/references/widget-dsl-api-examples.md +3 -4
  14. package/skills/finebi-cli-agent/references/widget-dsl-api.md +10 -1374
  15. package/skills/finebi-cli-agent/references/widget-dsl-contexts.md +142 -0
  16. package/skills/finebi-cli-agent/references/widget-dsl-core.md +374 -0
  17. package/skills/finebi-cli-agent/references/widget-dsl-encodings.md +457 -0
  18. package/skills/finebi-cli-agent/references/widget-dsl-styles.md +413 -0
  19. package/skills/finebi-cli-agent/scripts/finebi-cli/package.json +1 -1
  20. package/skills/finebi-cli-agent/scripts/finebi-cli/src/app.js +148 -0
  21. package/skills/finebi-cli-agent/scripts/finebi-cli/src/config.js +27 -27
  22. package/skills/finebi-cli-agent/scripts/finebi-cli/src/help.js +79 -55
  23. package/skills/finebi-cli-agent/scripts/finebi-cli/src/index.js +5 -0
  24. package/skills/finebi-cli-agent/scripts/finebi-cli/src/install-skill.js +89 -89
  25. package/skills/finebi-cli-agent/scripts/finebi-cli/src/profiles.js +158 -158
  26. package/skills/finebi-cli-agent/scripts/finebi-cli/src/skill-files.js +94 -94
  27. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/AGENTS.md +30 -0
  28. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/README.md +36 -0
  29. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/acceptance.md +67 -0
  30. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/auth-and-deploy.md +17 -0
  31. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/data-contract.md +38 -0
  32. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/design.md +54 -0
  33. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/filters.md +93 -0
  34. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/examples/cockpit.config.json +22 -0
  35. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/examples/retail.config.json +185 -0
  36. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/index.html +2 -0
  37. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/package-lock.json +2191 -0
  38. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/package.json +16 -0
  39. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/public/app-config.json +11 -0
  40. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/scripts/dev-proxy.mjs +57 -0
  41. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/App.tsx +56 -0
  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 -0
  44. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/DataTable.tsx +21 -0
  45. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/FilterBar.tsx +48 -0
  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 +39 -0
  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 -0
  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 +42 -0
  58. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/config.d.ts +1 -0
  59. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/config.js +44 -0
  60. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/date.d.ts +1 -0
  61. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/date.js +5 -0
  62. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/filters.d.ts +4 -0
  63. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/filters.js +106 -0
  64. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/normalize.ts +77 -0
  65. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/types.ts +18 -0
  66. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/main.tsx +5 -0
  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 -0
  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/src/styles.css +8 -0
  73. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/fixtures.mjs +28 -0
  74. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/index.html +16 -0
  75. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/main.tsx +149 -0
  76. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/test-page.css +4 -0
  77. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/vite.config.mjs +13 -0
  78. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/bundle.test.mjs +19 -0
  79. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/calendar.test.mjs +25 -0
  80. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/cockpit.test.mjs +73 -0
  81. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/proxy.test.mjs +96 -0
  82. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/retail.test.mjs +31 -0
  83. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/runtime.test.mjs +136 -0
  84. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tsconfig.json +4 -0
  85. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/vite.config.ts +15 -0
  86. package/skills/finebi-cli-agent/scripts/finebi-cli-runner.mjs +4 -4
  87. package/src/app.js +148 -0
  88. package/src/config.js +27 -27
  89. package/src/help.js +79 -55
  90. package/src/index.js +5 -0
  91. package/src/install-skill.js +89 -89
  92. package/src/profiles.js +158 -158
  93. package/src/skill-files.js +94 -94
  94. package/templates/web-app/AGENTS.md +30 -0
  95. package/templates/web-app/README.md +36 -0
  96. package/templates/web-app/docs/acceptance.md +67 -0
  97. package/templates/web-app/docs/auth-and-deploy.md +17 -0
  98. package/templates/web-app/docs/data-contract.md +38 -0
  99. package/templates/web-app/docs/design.md +54 -0
  100. package/templates/web-app/docs/filters.md +93 -0
  101. package/templates/web-app/examples/cockpit.config.json +22 -0
  102. package/templates/web-app/examples/retail.config.json +185 -0
  103. package/templates/web-app/index.html +2 -0
  104. package/templates/web-app/package-lock.json +2191 -0
  105. package/templates/web-app/package.json +16 -0
  106. package/templates/web-app/public/app-config.json +11 -0
  107. package/templates/web-app/scripts/dev-proxy.mjs +57 -0
  108. package/templates/web-app/src/App.tsx +56 -0
  109. package/templates/web-app/src/cockpit.css +3 -0
  110. package/templates/web-app/src/components/Chart.tsx +40 -0
  111. package/templates/web-app/src/components/DataTable.tsx +21 -0
  112. package/templates/web-app/src/components/FilterBar.tsx +48 -0
  113. package/templates/web-app/src/components/Insights.tsx +17 -0
  114. package/templates/web-app/src/components/MonthPicker.tsx +32 -0
  115. package/templates/web-app/src/components/OptionFilter.tsx +7 -0
  116. package/templates/web-app/src/components/RangeFilter.tsx +9 -0
  117. package/templates/web-app/src/components/Widget.tsx +39 -0
  118. package/templates/web-app/src/components/calendar-locales.ts +3 -0
  119. package/templates/web-app/src/components/calendar.ts +14 -0
  120. package/templates/web-app/src/components/charts/Bar.tsx +6 -0
  121. package/templates/web-app/src/components/charts/Line.tsx +6 -0
  122. package/templates/web-app/src/components/charts/Pie.tsx +6 -0
  123. package/templates/web-app/src/components/month-picker.css +1 -0
  124. package/templates/web-app/src/finebi/client.ts +42 -0
  125. package/templates/web-app/src/finebi/config.d.ts +1 -0
  126. package/templates/web-app/src/finebi/config.js +44 -0
  127. package/templates/web-app/src/finebi/date.d.ts +1 -0
  128. package/templates/web-app/src/finebi/date.js +5 -0
  129. package/templates/web-app/src/finebi/filters.d.ts +4 -0
  130. package/templates/web-app/src/finebi/filters.js +106 -0
  131. package/templates/web-app/src/finebi/normalize.ts +77 -0
  132. package/templates/web-app/src/finebi/types.ts +18 -0
  133. package/templates/web-app/src/main.tsx +5 -0
  134. package/templates/web-app/src/retail.css +13 -0
  135. package/templates/web-app/src/services/presentation.ts +36 -0
  136. package/templates/web-app/src/services/table-data.ts +47 -0
  137. package/templates/web-app/src/services/view-data.ts +18 -0
  138. package/templates/web-app/src/services/widget-queries.ts +18 -0
  139. package/templates/web-app/src/styles.css +8 -0
  140. package/templates/web-app/tests/browser/fixtures.mjs +28 -0
  141. package/templates/web-app/tests/browser/index.html +16 -0
  142. package/templates/web-app/tests/browser/main.tsx +149 -0
  143. package/templates/web-app/tests/browser/test-page.css +4 -0
  144. package/templates/web-app/tests/browser/vite.config.mjs +13 -0
  145. package/templates/web-app/tests/bundle.test.mjs +19 -0
  146. package/templates/web-app/tests/calendar.test.mjs +25 -0
  147. package/templates/web-app/tests/cockpit.test.mjs +73 -0
  148. package/templates/web-app/tests/proxy.test.mjs +96 -0
  149. package/templates/web-app/tests/retail.test.mjs +31 -0
  150. package/templates/web-app/tests/runtime.test.mjs +136 -0
  151. package/templates/web-app/tsconfig.json +4 -0
  152. package/templates/web-app/vite.config.ts +15 -0
package/src/profiles.js CHANGED
@@ -1,158 +1,158 @@
1
- import {closeSync, existsSync, mkdirSync, openSync, rmSync} from "node:fs";
2
- import {dirname, join} from "node:path";
3
-
4
- import {normalizeConfig, readConfig, writeConfigAtomically} from "./config.js";
5
-
6
- export class ProfileError extends Error {}
7
-
8
- export function validateProfileName(name) {
9
- if (typeof name !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(name)) {
10
- throw new ProfileError("Profile name must contain 1-64 letters, digits, underscores or hyphens and start with a letter or digit.");
11
- }
12
- return name;
13
- }
14
-
15
- export function readProfiles(path) {
16
- const config = readConfig(path);
17
- if (config.schemaVersion === 2) {
18
- if (!config.profiles || typeof config.profiles !== "object" || Array.isArray(config.profiles)) {
19
- throw new ProfileError("Invalid profiles configuration.");
20
- }
21
- return {multi: true, currentProfile: config.currentProfile ?? null, profiles: config.profiles};
22
- }
23
- // Preserve legacy session tokens when a single-environment file is migrated.
24
- if (config.token && !config.auth?.token) {
25
- config.auth = {...config.auth, token: config.token};
26
- }
27
- const present = existsSync(path);
28
- return {multi: false, currentProfile: present ? "default" : null, profiles: present ? {default: config} : {}};
29
- }
30
-
31
- export function selectProfile(path, requested, {allowCreate = false, optional = false} = {}) {
32
- const store = readProfiles(path);
33
- const name = requested ?? store.currentProfile ?? (!store.multi ? "default" : null);
34
- if (!name) {
35
- if (optional) return null;
36
- throw new ProfileError("No default environment is selected. Run finebi-cli auth use <name> or pass --profile <name>.");
37
- }
38
- validateProfileName(name);
39
- const implicitInitial = !requested && !store.multi && !existsSync(path);
40
- if (!Object.hasOwn(store.profiles, name) && !allowCreate && !implicitInitial && !optional) {
41
- throw new ProfileError(`Environment '${name}' does not exist. Create it with finebi-cli auth login --profile ${name} --base-url <url>.`);
42
- }
43
- if (requested && !Object.hasOwn(store.profiles, name) && !allowCreate) {
44
- throw new ProfileError(`Environment '${name}' does not exist.`);
45
- }
46
- return name;
47
- }
48
-
49
- export function readProfile(path, name) {
50
- const store = readProfiles(path);
51
- return normalizeConfig(name && Object.hasOwn(store.profiles, name) ? store.profiles[name] : {});
52
- }
53
-
54
- export function profileCapabilityPath(path, name) {
55
- if (!name || name === "default") return path;
56
- // Hex encoding preserves case-sensitive names on case-insensitive filesystems.
57
- return join(dirname(path), `capabilities-profile-${Buffer.from(validateProfileName(name)).toString("hex")}.json`);
58
- }
59
-
60
- export function saveProfile(path, name, config, {makeCurrent = false, forceMulti = false, writer = writeConfigAtomically} = {}) {
61
- validateProfileName(name);
62
- return withProfileLock(path, () => {
63
- const store = readProfiles(path);
64
- if (!store.multi && name === "default" && !forceMulti) {
65
- writer(path, config);
66
- return;
67
- }
68
- const profiles = {...store.profiles, [name]: config};
69
- writer(path, {schemaVersion: 2, currentProfile: makeCurrent ? name : store.currentProfile, profiles});
70
- });
71
- }
72
-
73
- export function useProfile(path, name) {
74
- validateProfileName(name);
75
- return withProfileLock(path, () => {
76
- const store = readProfiles(path);
77
- if (!Object.hasOwn(store.profiles, name)) throw new ProfileError(`Environment '${name}' does not exist.`);
78
- writeConfigAtomically(path, {schemaVersion: 2, currentProfile: name, profiles: store.profiles});
79
- });
80
- }
81
-
82
- export function removeProfile(path, name) {
83
- validateProfileName(name);
84
- return withProfileLock(path, () => {
85
- const store = readProfiles(path);
86
- if (!Object.hasOwn(store.profiles, name)) {
87
- if (!store.multi && name === "default") return;
88
- throw new ProfileError(`Environment '${name}' does not exist.`);
89
- }
90
- if (!store.multi && name === "default") {
91
- rmSync(path);
92
- return;
93
- }
94
- const profiles = {...store.profiles};
95
- delete profiles[name];
96
- writeConfigAtomically(path, {schemaVersion: 2, currentProfile: store.currentProfile === name ? null : store.currentProfile, profiles});
97
- });
98
- }
99
-
100
- export async function withProfileLoginLock(path, name, operation) {
101
- validateProfileName(name);
102
- mkdirSync(dirname(path), {recursive: true, mode: 0o700});
103
- const lock = `${path}.login-${Buffer.from(name).toString("hex")}.lock`;
104
- let fd;
105
- try {
106
- fd = openSync(lock, "wx", 0o600);
107
- } catch (error) {
108
- if (error.code === "EEXIST") throw new ProfileError(`An operation for environment '${name}' is already in progress. Retry after it finishes.`);
109
- throw error;
110
- }
111
- try {
112
- return await operation();
113
- } finally {
114
- closeSync(fd);
115
- rmSync(lock, {force: true});
116
- }
117
- }
118
-
119
- function withProfileLock(path, operation) {
120
- mkdirSync(dirname(path), {recursive: true, mode: 0o700});
121
- const lock = `${path}.lock`;
122
- const deadline = Date.now() + 3000;
123
- let fd;
124
- while (fd === undefined) {
125
- try {
126
- fd = openSync(lock, "wx", 0o600);
127
- } catch (error) {
128
- if (error.code !== "EEXIST") throw error;
129
- if (Date.now() >= deadline) throw new ProfileError("Configuration is locked by another process. Retry after it finishes.");
130
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
131
- }
132
- }
133
- try {
134
- return operation();
135
- } finally {
136
- closeSync(fd);
137
- rmSync(lock, {force: true});
138
- }
139
- }
140
-
141
- export function parseConnectionArgs(argv) {
142
- const args = [];
143
- const flags = {};
144
- const names = {"--profile": "profile", "--base-url": "baseUrl", "--token": "token"};
145
- for (let index = 0; index < argv.length; index++) {
146
- const [option, ...inline] = argv[index].split("=");
147
- if (!Object.hasOwn(names, option)) {
148
- args.push(argv[index]);
149
- continue;
150
- }
151
- const value = inline.length ? inline.join("=") : argv[++index];
152
- if (!value?.trim() || value.startsWith("--")) throw new ProfileError(`${option} requires a value.`);
153
- const key = names[option];
154
- if (Object.hasOwn(flags, key)) throw new ProfileError(`${option} must only be supplied once.`);
155
- flags[key] = value.trim();
156
- }
157
- return {args, flags};
158
- }
1
+ import {closeSync, existsSync, mkdirSync, openSync, rmSync} from "node:fs";
2
+ import {dirname, join} from "node:path";
3
+
4
+ import {normalizeConfig, readConfig, writeConfigAtomically} from "./config.js";
5
+
6
+ export class ProfileError extends Error {}
7
+
8
+ export function validateProfileName(name) {
9
+ if (typeof name !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(name)) {
10
+ throw new ProfileError("Profile name must contain 1-64 letters, digits, underscores or hyphens and start with a letter or digit.");
11
+ }
12
+ return name;
13
+ }
14
+
15
+ export function readProfiles(path) {
16
+ const config = readConfig(path);
17
+ if (config.schemaVersion === 2) {
18
+ if (!config.profiles || typeof config.profiles !== "object" || Array.isArray(config.profiles)) {
19
+ throw new ProfileError("Invalid profiles configuration.");
20
+ }
21
+ return {multi: true, currentProfile: config.currentProfile ?? null, profiles: config.profiles};
22
+ }
23
+ // Preserve legacy session tokens when a single-environment file is migrated.
24
+ if (config.token && !config.auth?.token) {
25
+ config.auth = {...config.auth, token: config.token};
26
+ }
27
+ const present = existsSync(path);
28
+ return {multi: false, currentProfile: present ? "default" : null, profiles: present ? {default: config} : {}};
29
+ }
30
+
31
+ export function selectProfile(path, requested, {allowCreate = false, optional = false} = {}) {
32
+ const store = readProfiles(path);
33
+ const name = requested ?? store.currentProfile ?? (!store.multi ? "default" : null);
34
+ if (!name) {
35
+ if (optional) return null;
36
+ throw new ProfileError("No default environment is selected. Run finebi-cli auth use <name> or pass --profile <name>.");
37
+ }
38
+ validateProfileName(name);
39
+ const implicitInitial = !requested && !store.multi && !existsSync(path);
40
+ if (!Object.hasOwn(store.profiles, name) && !allowCreate && !implicitInitial && !optional) {
41
+ throw new ProfileError(`Environment '${name}' does not exist. Create it with finebi-cli auth login --profile ${name} --base-url <url>.`);
42
+ }
43
+ if (requested && !Object.hasOwn(store.profiles, name) && !allowCreate) {
44
+ throw new ProfileError(`Environment '${name}' does not exist.`);
45
+ }
46
+ return name;
47
+ }
48
+
49
+ export function readProfile(path, name) {
50
+ const store = readProfiles(path);
51
+ return normalizeConfig(name && Object.hasOwn(store.profiles, name) ? store.profiles[name] : {});
52
+ }
53
+
54
+ export function profileCapabilityPath(path, name) {
55
+ if (!name || name === "default") return path;
56
+ // Hex encoding preserves case-sensitive names on case-insensitive filesystems.
57
+ return join(dirname(path), `capabilities-profile-${Buffer.from(validateProfileName(name)).toString("hex")}.json`);
58
+ }
59
+
60
+ export function saveProfile(path, name, config, {makeCurrent = false, forceMulti = false, writer = writeConfigAtomically} = {}) {
61
+ validateProfileName(name);
62
+ return withProfileLock(path, () => {
63
+ const store = readProfiles(path);
64
+ if (!store.multi && name === "default" && !forceMulti) {
65
+ writer(path, config);
66
+ return;
67
+ }
68
+ const profiles = {...store.profiles, [name]: config};
69
+ writer(path, {schemaVersion: 2, currentProfile: makeCurrent ? name : store.currentProfile, profiles});
70
+ });
71
+ }
72
+
73
+ export function useProfile(path, name) {
74
+ validateProfileName(name);
75
+ return withProfileLock(path, () => {
76
+ const store = readProfiles(path);
77
+ if (!Object.hasOwn(store.profiles, name)) throw new ProfileError(`Environment '${name}' does not exist.`);
78
+ writeConfigAtomically(path, {schemaVersion: 2, currentProfile: name, profiles: store.profiles});
79
+ });
80
+ }
81
+
82
+ export function removeProfile(path, name) {
83
+ validateProfileName(name);
84
+ return withProfileLock(path, () => {
85
+ const store = readProfiles(path);
86
+ if (!Object.hasOwn(store.profiles, name)) {
87
+ if (!store.multi && name === "default") return;
88
+ throw new ProfileError(`Environment '${name}' does not exist.`);
89
+ }
90
+ if (!store.multi && name === "default") {
91
+ rmSync(path);
92
+ return;
93
+ }
94
+ const profiles = {...store.profiles};
95
+ delete profiles[name];
96
+ writeConfigAtomically(path, {schemaVersion: 2, currentProfile: store.currentProfile === name ? null : store.currentProfile, profiles});
97
+ });
98
+ }
99
+
100
+ export async function withProfileLoginLock(path, name, operation) {
101
+ validateProfileName(name);
102
+ mkdirSync(dirname(path), {recursive: true, mode: 0o700});
103
+ const lock = `${path}.login-${Buffer.from(name).toString("hex")}.lock`;
104
+ let fd;
105
+ try {
106
+ fd = openSync(lock, "wx", 0o600);
107
+ } catch (error) {
108
+ if (error.code === "EEXIST") throw new ProfileError(`An operation for environment '${name}' is already in progress. Retry after it finishes.`);
109
+ throw error;
110
+ }
111
+ try {
112
+ return await operation();
113
+ } finally {
114
+ closeSync(fd);
115
+ rmSync(lock, {force: true});
116
+ }
117
+ }
118
+
119
+ function withProfileLock(path, operation) {
120
+ mkdirSync(dirname(path), {recursive: true, mode: 0o700});
121
+ const lock = `${path}.lock`;
122
+ const deadline = Date.now() + 3000;
123
+ let fd;
124
+ while (fd === undefined) {
125
+ try {
126
+ fd = openSync(lock, "wx", 0o600);
127
+ } catch (error) {
128
+ if (error.code !== "EEXIST") throw error;
129
+ if (Date.now() >= deadline) throw new ProfileError("Configuration is locked by another process. Retry after it finishes.");
130
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
131
+ }
132
+ }
133
+ try {
134
+ return operation();
135
+ } finally {
136
+ closeSync(fd);
137
+ rmSync(lock, {force: true});
138
+ }
139
+ }
140
+
141
+ export function parseConnectionArgs(argv) {
142
+ const args = [];
143
+ const flags = {};
144
+ const names = {"--profile": "profile", "--base-url": "baseUrl", "--token": "token"};
145
+ for (let index = 0; index < argv.length; index++) {
146
+ const [option, ...inline] = argv[index].split("=");
147
+ if (!Object.hasOwn(names, option)) {
148
+ args.push(argv[index]);
149
+ continue;
150
+ }
151
+ const value = inline.length ? inline.join("=") : argv[++index];
152
+ if (!value?.trim() || value.startsWith("--")) throw new ProfileError(`${option} requires a value.`);
153
+ const key = names[option];
154
+ if (Object.hasOwn(flags, key)) throw new ProfileError(`${option} must only be supplied once.`);
155
+ flags[key] = value.trim();
156
+ }
157
+ return {args, flags};
158
+ }
@@ -1,94 +1,94 @@
1
- import {createHash, randomUUID} from "node:crypto";
2
- import {existsSync} from "node:fs";
3
- import {cp, mkdir, lstat, mkdtemp, readFile, readdir, rename, rm} from "node:fs/promises";
4
- import {homedir} from "node:os";
5
- import {join, dirname, resolve} from "node:path";
6
-
7
- export function agentCatalog({home = homedir(), env = process.env} = {}) {
8
- const config = env.XDG_CONFIG_HOME || join(home, ".config");
9
- return [
10
- ["claude-code", "Claude Code", env.CLAUDE_CONFIG_DIR || join(home, ".claude")],
11
- ["codex", "Codex", env.CODEX_HOME || join(home, ".codex")],
12
- ["cursor", "Cursor", join(home, ".cursor")],
13
- ["gemini-cli", "Gemini CLI", join(home, ".gemini")],
14
- ["github-copilot", "GitHub Copilot", join(home, ".copilot")],
15
- ["opencode", "OpenCode", join(config, "opencode")],
16
- ["cline", "Cline", join(home, ".agents")],
17
- ["windsurf", "Windsurf", join(home, ".codeium", "windsurf")],
18
- ["trae", "Trae", join(home, ".trae")],
19
- ["trae-cn", "Trae CN", join(home, ".trae-cn")],
20
- ["openclaw", "OpenClaw", [".openclaw", ".clawdbot", ".moltbot"].map(p => join(home, p)).find(existsSync) || join(home, ".openclaw")],
21
- ["roo", "Roo Code", join(home, ".roo")],
22
- ["qwen-code", "Qwen Code", join(home, ".qwen")],
23
- ].map(([id, name, root]) => ({id, name, directory: join(root, "skills"), detected: existsSync(root)}));
24
- }
25
-
26
- export async function hashTree(root, prefix = "") {
27
- const files = {};
28
- for (const entry of (await readdir(join(root, prefix), {withFileTypes: true})).sort((a, b) => a.name.localeCompare(b.name))) {
29
- const name = prefix ? `${prefix}/${entry.name}` : entry.name;
30
- if (entry.isSymbolicLink()) throw new Error(`Skill contains a symbolic link: ${name}`);
31
- if (entry.isDirectory()) Object.assign(files, await hashTree(root, name));
32
- else files[name] = createHash("sha256").update(await readFile(join(root, name))).digest("hex");
33
- }
34
- return files;
35
- }
36
-
37
- function sameHashes(left, right) {
38
- return left && right && Object.keys(left).length === Object.keys(right).length
39
- && Object.keys(left).every(key => left[key] === right[key]);
40
- }
41
-
42
- export async function installSkills({source, home, env, agents, replace = false, previous = []}) {
43
- const catalog = agentCatalog({home, env});
44
- const selected = agents.map(id => {
45
- const agent = catalog.find(item => item.id === id);
46
- if (!agent) throw new Error(`Unknown Agent: ${id}`);
47
- return agent;
48
- });
49
- const sourceHashes = await hashTree(source);
50
- const results = [];
51
- for (const agent of selected) {
52
- const target = join(agent.directory, "finebi-cli-agent");
53
- let backup;
54
- let candidate;
55
- try {
56
- const stat = await lstat(target).catch(error => { if (error.code !== "ENOENT") throw error; });
57
- if (stat) {
58
- if (stat.isSymbolicLink()) throw new Error("Skill target is a symbolic link; refusing to replace it.");
59
- const actual = await hashTree(target);
60
- if (sameHashes(sourceHashes, actual)) {
61
- results.push({agent: agent.id, path: target, status: "unchanged", hashes: sourceHashes});
62
- continue;
63
- }
64
- const recorded = previous.find(item => item.agent === agent.id && item.path === target)?.hashes;
65
- if (!replace && !sameHashes(recorded, actual)) {
66
- results.push({agent: agent.id, path: target, status: "kept"});
67
- continue;
68
- }
69
- backup = join(home, ".finebi-cli", "skill-backups", `${agent.id}-${randomUUID()}`);
70
- await mkdir(dirname(backup), {recursive: true});
71
- await cp(target, backup, {recursive: true, errorOnExist: true, force: false});
72
- if (!sameHashes(actual, await hashTree(backup))) throw new Error("Skill backup integrity check failed.");
73
- }
74
- await mkdir(agent.directory, {recursive: true});
75
- candidate = await mkdtemp(join(agent.directory, ".finebi-cli-stage-"));
76
- await cp(source, candidate, {recursive: true});
77
- if (!sameHashes(sourceHashes, await hashTree(candidate))) throw new Error("Skill copy integrity check failed.");
78
- const old = `${candidate}-old`;
79
- if (stat) await rename(target, old);
80
- try { await rename(candidate, target); }
81
- catch (error) { if (stat) await rename(old, target); throw error; }
82
- candidate = undefined;
83
- // Delete only the relocated, backed-up directory inside this Agent's skills directory.
84
- if (stat && backup && dirname(resolve(old)) === resolve(agent.directory)) await rm(old, {recursive: true});
85
- if (!sameHashes(sourceHashes, await hashTree(target))) throw new Error("Installed Skill integrity check failed.");
86
- results.push({agent: agent.id, path: target, status: "installed", hashes: sourceHashes, ...(backup ? {backup} : {})});
87
- } catch (error) {
88
- results.push({agent: agent.id, path: target, status: "failed", error: error.message, ...(backup ? {backup} : {})});
89
- } finally {
90
- if (candidate && dirname(resolve(candidate)) === resolve(agent.directory)) await rm(candidate, {recursive: true, force: true});
91
- }
92
- }
93
- return results;
94
- }
1
+ import {createHash, randomUUID} from "node:crypto";
2
+ import {existsSync} from "node:fs";
3
+ import {cp, mkdir, lstat, mkdtemp, readFile, readdir, rename, rm} from "node:fs/promises";
4
+ import {homedir} from "node:os";
5
+ import {join, dirname, resolve} from "node:path";
6
+
7
+ export function agentCatalog({home = homedir(), env = process.env} = {}) {
8
+ const config = env.XDG_CONFIG_HOME || join(home, ".config");
9
+ return [
10
+ ["claude-code", "Claude Code", env.CLAUDE_CONFIG_DIR || join(home, ".claude")],
11
+ ["codex", "Codex", env.CODEX_HOME || join(home, ".codex")],
12
+ ["cursor", "Cursor", join(home, ".cursor")],
13
+ ["gemini-cli", "Gemini CLI", join(home, ".gemini")],
14
+ ["github-copilot", "GitHub Copilot", join(home, ".copilot")],
15
+ ["opencode", "OpenCode", join(config, "opencode")],
16
+ ["cline", "Cline", join(home, ".agents")],
17
+ ["windsurf", "Windsurf", join(home, ".codeium", "windsurf")],
18
+ ["trae", "Trae", join(home, ".trae")],
19
+ ["trae-cn", "Trae CN", join(home, ".trae-cn")],
20
+ ["openclaw", "OpenClaw", [".openclaw", ".clawdbot", ".moltbot"].map(p => join(home, p)).find(existsSync) || join(home, ".openclaw")],
21
+ ["roo", "Roo Code", join(home, ".roo")],
22
+ ["qwen-code", "Qwen Code", join(home, ".qwen")],
23
+ ].map(([id, name, root]) => ({id, name, directory: join(root, "skills"), detected: existsSync(root)}));
24
+ }
25
+
26
+ export async function hashTree(root, prefix = "") {
27
+ const files = {};
28
+ for (const entry of (await readdir(join(root, prefix), {withFileTypes: true})).sort((a, b) => a.name.localeCompare(b.name))) {
29
+ const name = prefix ? `${prefix}/${entry.name}` : entry.name;
30
+ if (entry.isSymbolicLink()) throw new Error(`Skill contains a symbolic link: ${name}`);
31
+ if (entry.isDirectory()) Object.assign(files, await hashTree(root, name));
32
+ else files[name] = createHash("sha256").update(await readFile(join(root, name))).digest("hex");
33
+ }
34
+ return files;
35
+ }
36
+
37
+ function sameHashes(left, right) {
38
+ return left && right && Object.keys(left).length === Object.keys(right).length
39
+ && Object.keys(left).every(key => left[key] === right[key]);
40
+ }
41
+
42
+ export async function installSkills({source, home, env, agents, replace = false, previous = []}) {
43
+ const catalog = agentCatalog({home, env});
44
+ const selected = agents.map(id => {
45
+ const agent = catalog.find(item => item.id === id);
46
+ if (!agent) throw new Error(`Unknown Agent: ${id}`);
47
+ return agent;
48
+ });
49
+ const sourceHashes = await hashTree(source);
50
+ const results = [];
51
+ for (const agent of selected) {
52
+ const target = join(agent.directory, "finebi-cli-agent");
53
+ let backup;
54
+ let candidate;
55
+ try {
56
+ const stat = await lstat(target).catch(error => { if (error.code !== "ENOENT") throw error; });
57
+ if (stat) {
58
+ if (stat.isSymbolicLink()) throw new Error("Skill target is a symbolic link; refusing to replace it.");
59
+ const actual = await hashTree(target);
60
+ if (sameHashes(sourceHashes, actual)) {
61
+ results.push({agent: agent.id, path: target, status: "unchanged", hashes: sourceHashes});
62
+ continue;
63
+ }
64
+ const recorded = previous.find(item => item.agent === agent.id && item.path === target)?.hashes;
65
+ if (!replace && !sameHashes(recorded, actual)) {
66
+ results.push({agent: agent.id, path: target, status: "kept"});
67
+ continue;
68
+ }
69
+ backup = join(home, ".finebi-cli", "skill-backups", `${agent.id}-${randomUUID()}`);
70
+ await mkdir(dirname(backup), {recursive: true});
71
+ await cp(target, backup, {recursive: true, errorOnExist: true, force: false});
72
+ if (!sameHashes(actual, await hashTree(backup))) throw new Error("Skill backup integrity check failed.");
73
+ }
74
+ await mkdir(agent.directory, {recursive: true});
75
+ candidate = await mkdtemp(join(agent.directory, ".finebi-cli-stage-"));
76
+ await cp(source, candidate, {recursive: true});
77
+ if (!sameHashes(sourceHashes, await hashTree(candidate))) throw new Error("Skill copy integrity check failed.");
78
+ const old = `${candidate}-old`;
79
+ if (stat) await rename(target, old);
80
+ try { await rename(candidate, target); }
81
+ catch (error) { if (stat) await rename(old, target); throw error; }
82
+ candidate = undefined;
83
+ // Delete only the relocated, backed-up directory inside this Agent's skills directory.
84
+ if (stat && backup && dirname(resolve(old)) === resolve(agent.directory)) await rm(old, {recursive: true});
85
+ if (!sameHashes(sourceHashes, await hashTree(target))) throw new Error("Installed Skill integrity check failed.");
86
+ results.push({agent: agent.id, path: target, status: "installed", hashes: sourceHashes, ...(backup ? {backup} : {})});
87
+ } catch (error) {
88
+ results.push({agent: agent.id, path: target, status: "failed", error: error.message, ...(backup ? {backup} : {})});
89
+ } finally {
90
+ if (candidate && dirname(resolve(candidate)) === resolve(agent.directory)) await rm(candidate, {recursive: true, force: true});
91
+ }
92
+ }
93
+ return results;
94
+ }
@@ -0,0 +1,30 @@
1
+ # FineBI 动态页面开发
2
+
3
+ ## 开发顺序
4
+
5
+ 1. 用 FineBI CLI 搜索并读取 dashboard info/data,确认唯一来源、看板实例 entryId、queryFields.dId 和真实控件 filterName。按[验收文档](docs/acceptance.md#1-绑定与设计准备)记录指标公式/聚合、单位、时间粒度与时区、筛选作用范围及数据完整性;必要时读取 bean,未知项明确标记,不按组件标题猜语义。
6
+ 2. 阅读 [数据契约](docs/data-contract.md)、[筛选](docs/filters.md)、[认证部署](docs/auth-and-deploy.md),再修改 public/app-config.json。
7
+ 3. 按[设计选择](docs/design.md#设计选择与信息组织)写出页面面向谁、首屏重点、导航、图表分组和信息密度,遵循用户已确认的方向继续开发。新风格说明与已有布局在结构、层级和视觉上的区别。从 overview、workbench、cockpit 或 retail 起步,示例配置仅含占位绑定。
8
+ 4. 先跑通一个指标、一张图和一个真实控件(源看板有控件时)的查询、展示、应用与恢复默认,再扩展整页。使用真实接口开发;合成样本仅用于 tests,禁止将开发查询数据写入正式页面、配置或回退数据。
9
+ 5. 业务转换放 src/services,页面放 src/pages 或 App.tsx,通用请求与原生解析复用 src/finebi。改动共享组件时检查其他布局,不为换风格复制查询、认证或筛选编码。
10
+ 6. 按[分层验收](docs/acceptance.md)执行 npm test、npm run build、浏览器冒烟及真实绑定验收。检查整页和关键状态,分别记录 CLI、开发身份、生产 Cookie 的验证结论;构建成功不等于认证、取数或视觉验收通过。
11
+
12
+ ## 数据规则
13
+
14
+ - 用 CLI 指定 `--dimension-ids` 时,必须同时列出需要的维度和指标;只传维度会关闭未选指标。模板目前不提供字段选择配置,浏览器查询沿用保存配置;完整语义见[数据契约](docs/data-contract.md#字段选择)。
15
+ - 先检查 normalization、warnings、range,再选择唯一 dataset;多模型需显式 datasetId。
16
+ - display 用于显示;计算仅使用 AVAILABLE 的 value,保留 DECIMAL_STRING 精度。禁止从百分比/千分位/单位文本恢复数值。受限值、未知值不能补零。
17
+ - 不按名称猜字段、不累加 UNKNOWN 行、不从首页估算全量 KPI。KPI 示例仅消费单行结果。
18
+ - 控件筛选通过真实名称生成 refer;无控件时不伪造服务端过滤。搜索和排序的范围必须在页面说明。
19
+ - 筛选绑定先核对源控件类型,配置/状态/请求统一通过 src/finebi/filters.js 校验。完整支持矩阵、逗号和 null 保留字、恢复默认与筛选空值语义见 docs/filters.md;不以更换 type 绕过服务端能力限制。
20
+ - pageLimit 是首页大小,不是分页。不得循环放大请求以绕过 maxRows;hasNext=true 必须保留警告,不能宣称全量。
21
+
22
+ ## 认证与交付
23
+
24
+ - 开发凭证只存在服务端环境 FINEBI_DEV_* 或内存;禁止 VITE_* 凭证、前端 localStorage token、提交 .env 和认证日志。
25
+ - 生产应用使用同源当前用户 Cookie。开发代理不属于构建产物;不要把它部署为共享多用户服务。
26
+ - 目录来源配置 directoryEntryId,原生请求保留 reportEntryId;需目标平台 HTTP 接口支持目录权限预检与 MOUNT 查询,不得失败后回退到分析主题。不支持交叉表、服务端续页、全局排序或托管发布命令。
27
+ - 图表按生命周期销毁;异步请求取消且旧响应不得覆盖新条件。失败状态不得展示过期数据充当最新结果。
28
+ - 验收证据、截图和临时脚本放项目 temp/,不随包发布。交付记录已验证/失败/未验证项、运行方式、模板版本和业务绑定范围;模板维护流程由 CLI 源仓库负责,本项目无需其脚本才能开发和验收。
29
+
30
+ 项目创建不会安装依赖、启动服务、初始化 Git 或发布,按用户授权范围执行后续步骤。
@@ -0,0 +1,36 @@
1
+ # FineBI 动态数据应用
2
+
3
+ 这是运行时调用 FineBI 的静态应用。开发者通过 CLI 发现资源后配置绑定;生产查询使用访问者自己的登录态。
4
+
5
+ ## 开始
6
+
7
+ 1. 安装依赖:`npm ci`。
8
+ 2. 填写 `public/app-config.json`。`dashboardId` 来自目标看板;`widgets[].entryId` 使用板内实例 ID;图表 `categoryFieldId/valueFieldId` 使用本次 `dashboard info` 的结果字段 dId。相同实例可以有不同展示,但 KPI 必须对应单行查询结果。
9
+ 3. 在当前进程设置 `FINEBI_DEV_BASE_URL`(完整 decision 地址)和 `FINEBI_DEV_USERNAME/FINEBI_DEV_PASSWORD`,或 `FINEBI_DEV_TOKEN`。不要把值写入 VITE_* 或 app-config。
10
+ 4. `npm run dev`,访问 `http://localhost:4173`。开发代理仅监听本机,自动以开发身份登录,凭证不返回浏览器。
11
+ 5. 按[开发与验收](docs/acceptance.md)执行 `npm test`、`npm run build`、`npm run test:browser`,再验证真实绑定。浏览器验收页使用合成数据,不连接 FineBI。`npm run preview` 只展示 dist,不启用开发身份代理;真实生产取数需要同源 FineBI 路径。
12
+
13
+ ## 配置
14
+
15
+ `layout` 为 `overview`(顶部概览)、`workbench`(侧栏工作台)、`cockpit`(绿色经营驾驶舱)或 `retail`(米白经营简报)。可用 `finebi-cli app create --name my-retail --path <parent> --layout retail` 创建;默认配置仍为空绑定,不会请求示例资源。`maxRows` 默认 1000,上限 10000;`pageSize` 是本地展示页大小,1–100。
16
+
17
+ ```json
18
+ {
19
+ "schemaVersion":"1", "title":"销售分析", "basePath":"/webroot/decision",
20
+ "layout":"overview", "dashboardId":"真实看板ID", "maxRows":1000, "pageSize":8,
21
+ "widgets":[
22
+ {"entryId":"真实实例ID", "name":"销售趋势", "type":"line", "categoryFieldId":"日期结果字段ID", "valueFieldId":"销售结果字段ID"}
23
+ ],
24
+ "filters":[]
25
+ }
26
+ ```
27
+
28
+ 默认组件 type 支持 table、kpi、bar、line、pie;多模型时指定 datasetId。配置不包含数据或任何凭证。协议和限制见 [数据契约](docs/data-contract.md)、[筛选与联动](docs/filters.md)、[认证与部署](docs/auth-and-deploy.md)。布局、图表库和UI组件库的选型与替换约定见 [页面骨架](docs/design.md)。
29
+
30
+ cockpit 的配置骨架在 [examples/cockpit.config.json](examples/cockpit.config.json),仅包含占位绑定。替换所有 `REPLACE_*`,再复制到 `public/app-config.json`;可删除不需要的组件,KPI 数量也可调整。`insights:true` 开启当前查询观察,`dataNotes` 补充口径。多系列 `series`、表格字段/本地筛选和控件作用映射见 [页面骨架](docs/design.md)。示例中的 `filterNames:[]` 表示不覆盖控件,必须按真实映射配置,不能直接当成业务默认。
31
+
32
+ retail 的 [examples/retail.config.json](examples/retail.config.json) 提供零售分析绑定骨架:四项 KPI、趋势、门店 TOP10、区域/品类、门店类型和明细。采用横向导航、米白背景和蓝橙图表,不依赖侧栏。示例不含业务数据,`REPLACE_*`、模型选择、时区及筛选作用范围必须核实替换;普通分析来源需删除目录入口占位字段。金额与百分比、毫秒月份的显式显示配置见[页面骨架](docs/design.md)。
33
+
34
+ 目录发布看板在配置中增加 `directoryEntryId`,使用与该入口匹配的 dashboardId/实例。模板将其原样作为 `reportEntryId` 传给 Cookie 查询接口及开发代理;目标平台需同时支持目录权限预检和 MOUNT 查询,不能只支持 CLI 目录查询。错误独立显示,不回退“我的分析”。
35
+
36
+ 筛选先编辑、再点击“应用筛选”;“恢复全部默认”重新查询看板保存条件。模板支持文本、单选/多选、数值选择和日期类控件,完整类型、空值语义与配置示例以[筛选契约](docs/filters.md)为准;`app inspect` 仅检查本地配置,绑定后仍需验证真实条件生效。