@finebi/finebi-cli 1.6.1 → 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.
- package/README.md +8 -8
- package/package.json +1 -1
- package/skills/finebi-cli-agent/SKILL.md +43 -123
- package/skills/finebi-cli-agent/agents/openai.yaml +1 -1
- package/skills/finebi-cli-agent/references/artifact-ordering.md +3 -3
- package/skills/finebi-cli-agent/references/command-workflows.md +30 -1
- package/skills/finebi-cli-agent/references/dashboard-data-result.md +191 -191
- package/skills/finebi-cli-agent/references/dashboard-dsl-api.md +44 -44
- package/skills/finebi-cli-agent/references/dsl-handoff-contract.md +1 -1
- package/skills/finebi-cli-agent/references/handoff-contract.md +6 -4
- package/skills/finebi-cli-agent/references/mutation-confirmation-contract.md +2 -0
- package/skills/finebi-cli-agent/references/scenario-workflows.md +10 -8
- package/skills/finebi-cli-agent/references/web-app.md +4 -2
- package/skills/finebi-cli-agent/references/widget-dsl-core.md +6 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/package.json +1 -1
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/app.js +157 -148
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/browser-auth.js +3 -4
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/config.js +28 -28
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/error-catalog.js +2 -2
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/help.js +63 -60
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/install-skill.js +89 -89
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/profiles.js +158 -158
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/skill-files.js +94 -94
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/AGENTS.md +18 -5
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/README.md +12 -5
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/acceptance.md +80 -67
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/auth-and-deploy.md +17 -2
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/data-contract.md +66 -38
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/filters.md +2 -2
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/examples/cockpit.config.json +22 -22
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/examples/retail.config.json +185 -185
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/index.html +2 -2
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/public/app-config.json +11 -11
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/scripts/browser-session.mjs +59 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/scripts/dev-proxy.mjs +25 -24
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/scripts/dev-session.mjs +57 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/scripts/session-file.mjs +45 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/App.tsx +63 -56
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/cockpit.css +3 -3
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Chart.tsx +40 -40
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/DataTable.tsx +1 -1
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Insights.tsx +17 -17
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/MonthPicker.tsx +32 -32
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/OptionFilter.tsx +7 -7
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/RangeFilter.tsx +9 -9
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Widget.tsx +40 -39
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/calendar-locales.ts +3 -3
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/calendar.ts +14 -14
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Bar.tsx +6 -6
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Line.tsx +6 -6
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Pie.tsx +6 -6
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/month-picker.css +1 -1
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/client.ts +20 -4
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/config.d.ts +3 -1
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/config.js +58 -44
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/date.d.ts +1 -1
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/date.js +5 -5
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/filters.d.ts +4 -4
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/filters.js +106 -106
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/normalize.ts +235 -77
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/types.ts +20 -18
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/main.tsx +5 -5
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/retail.css +12 -12
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/presentation.ts +36 -36
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/table-data.ts +47 -47
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/view-data.ts +18 -18
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/widget-queries.ts +18 -18
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/fixtures.mjs +28 -28
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/index.html +16 -16
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/main.tsx +149 -149
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/test-page.css +4 -4
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/vite.config.mjs +13 -13
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/bundle.test.mjs +19 -19
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/cockpit.test.mjs +73 -73
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/field-selection.test.mjs +70 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/proxy.test.mjs +20 -5
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/retail.test.mjs +31 -31
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/runtime.test.mjs +7 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/session.test.mjs +91 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/table-normalize.test.mjs +189 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/vite-auth.test.mjs +41 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tsconfig.json +4 -4
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/vite.config.ts +9 -4
- package/skills/finebi-cli-agent/scripts/finebi-cli-runner.mjs +4 -4
- package/src/app.js +157 -148
- package/src/browser-auth.js +3 -4
- package/src/config.js +28 -28
- package/src/error-catalog.js +2 -2
- package/src/help.js +63 -60
- package/src/install-skill.js +89 -89
- package/src/profiles.js +158 -158
- package/src/skill-files.js +94 -94
- package/templates/web-app/AGENTS.md +18 -5
- package/templates/web-app/README.md +12 -5
- package/templates/web-app/docs/acceptance.md +80 -67
- package/templates/web-app/docs/auth-and-deploy.md +17 -2
- package/templates/web-app/docs/data-contract.md +66 -38
- package/templates/web-app/docs/filters.md +2 -2
- package/templates/web-app/examples/cockpit.config.json +22 -22
- package/templates/web-app/examples/retail.config.json +185 -185
- package/templates/web-app/index.html +2 -2
- package/templates/web-app/public/app-config.json +11 -11
- package/templates/web-app/scripts/browser-session.mjs +59 -0
- package/templates/web-app/scripts/dev-proxy.mjs +25 -24
- package/templates/web-app/scripts/dev-session.mjs +57 -0
- package/templates/web-app/scripts/session-file.mjs +45 -0
- package/templates/web-app/src/App.tsx +63 -56
- package/templates/web-app/src/cockpit.css +3 -3
- package/templates/web-app/src/components/Chart.tsx +40 -40
- package/templates/web-app/src/components/DataTable.tsx +1 -1
- package/templates/web-app/src/components/Insights.tsx +17 -17
- package/templates/web-app/src/components/MonthPicker.tsx +32 -32
- package/templates/web-app/src/components/OptionFilter.tsx +7 -7
- package/templates/web-app/src/components/RangeFilter.tsx +9 -9
- package/templates/web-app/src/components/Widget.tsx +40 -39
- package/templates/web-app/src/components/calendar-locales.ts +3 -3
- package/templates/web-app/src/components/calendar.ts +14 -14
- package/templates/web-app/src/components/charts/Bar.tsx +6 -6
- package/templates/web-app/src/components/charts/Line.tsx +6 -6
- package/templates/web-app/src/components/charts/Pie.tsx +6 -6
- package/templates/web-app/src/components/month-picker.css +1 -1
- package/templates/web-app/src/finebi/client.ts +20 -4
- package/templates/web-app/src/finebi/config.d.ts +3 -1
- package/templates/web-app/src/finebi/config.js +58 -44
- package/templates/web-app/src/finebi/date.d.ts +1 -1
- package/templates/web-app/src/finebi/date.js +5 -5
- package/templates/web-app/src/finebi/filters.d.ts +4 -4
- package/templates/web-app/src/finebi/filters.js +106 -106
- package/templates/web-app/src/finebi/normalize.ts +235 -77
- package/templates/web-app/src/finebi/types.ts +20 -18
- package/templates/web-app/src/main.tsx +5 -5
- package/templates/web-app/src/retail.css +12 -12
- package/templates/web-app/src/services/presentation.ts +36 -36
- package/templates/web-app/src/services/table-data.ts +47 -47
- package/templates/web-app/src/services/view-data.ts +18 -18
- package/templates/web-app/src/services/widget-queries.ts +18 -18
- package/templates/web-app/tests/browser/fixtures.mjs +28 -28
- package/templates/web-app/tests/browser/index.html +16 -16
- package/templates/web-app/tests/browser/main.tsx +149 -149
- package/templates/web-app/tests/browser/test-page.css +4 -4
- package/templates/web-app/tests/browser/vite.config.mjs +13 -13
- package/templates/web-app/tests/bundle.test.mjs +19 -19
- package/templates/web-app/tests/cockpit.test.mjs +73 -73
- package/templates/web-app/tests/field-selection.test.mjs +70 -0
- package/templates/web-app/tests/proxy.test.mjs +20 -5
- package/templates/web-app/tests/retail.test.mjs +31 -31
- package/templates/web-app/tests/runtime.test.mjs +7 -0
- package/templates/web-app/tests/session.test.mjs +91 -0
- package/templates/web-app/tests/table-normalize.test.mjs +189 -0
- package/templates/web-app/tests/vite-auth.test.mjs +41 -0
- package/templates/web-app/tsconfig.json +4 -4
- package/templates/web-app/vite.config.ts +9 -4
|
@@ -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
|
+
}
|
|
@@ -3,17 +3,30 @@
|
|
|
3
3
|
## 开发顺序
|
|
4
4
|
|
|
5
5
|
1. 用 FineBI CLI 搜索并读取 dashboard info/data,确认唯一来源、看板实例 entryId、queryFields.dId 和真实控件 filterName。按[验收文档](docs/acceptance.md#1-绑定与设计准备)记录指标公式/聚合、单位、时间粒度与时区、筛选作用范围及数据完整性;必要时读取 bean,未知项明确标记,不按组件标题猜语义。
|
|
6
|
-
2.
|
|
6
|
+
2. 按下方[阅读路径](#阅读路径)读取当前步骤需要的章节,再修改 public/app-config.json;不要求开场通读全部文档。
|
|
7
7
|
3. 按[设计选择](docs/design.md#设计选择与信息组织)写出页面面向谁、首屏重点、导航、图表分组和信息密度,遵循用户已确认的方向继续开发。新风格说明与已有布局在结构、层级和视觉上的区别。从 overview、workbench、cockpit 或 retail 起步,示例配置仅含占位绑定。
|
|
8
|
-
4.
|
|
8
|
+
4. 整页绑定前,按[最小真实查询](docs/acceptance.md#11-最小真实查询)先配置一个组件,启动开发页面,通过开发代理确认真实身份、查询、解析和页面值;再补齐本页需要的指标、图表与真实筛选链路,通过后扩展整页。使用真实接口开发;合成样本仅用于 tests,禁止将开发查询数据写入正式页面、配置或回退数据。
|
|
9
9
|
5. 业务转换放 src/services,页面放 src/pages 或 App.tsx,通用请求与原生解析复用 src/finebi。改动共享组件时检查其他布局,不为换风格复制查询、认证或筛选编码。
|
|
10
10
|
6. 按[分层验收](docs/acceptance.md)执行 npm test、npm run build、浏览器冒烟及真实绑定验收。检查整页和关键状态,分别记录 CLI、开发身份、生产 Cookie 的验证结论;构建成功不等于认证、取数或视觉验收通过。
|
|
11
11
|
|
|
12
|
+
## 阅读路径
|
|
13
|
+
|
|
14
|
+
首次进入项目先读本文件、[数据契约](docs/data-contract.md)和[绑定与设计准备](docs/acceptance.md#1-绑定与设计准备)。其余文档在对应步骤读取,同一任务中已读且未变化的章节直接复用。
|
|
15
|
+
|
|
16
|
+
| 当前步骤 | 阅读内容 |
|
|
17
|
+
|---|---|
|
|
18
|
+
| 配置并启动最小真实查询 | [README 开始](README.md#开始)、[本地开发认证](docs/auth-and-deploy.md#本地开发)、[最小真实查询](docs/acceptance.md#11-最小真实查询) |
|
|
19
|
+
| 绑定真实筛选控件 | [筛选与联动](docs/filters.md)中对应控件类型、作用范围、应用与恢复默认的规则;无控件时无需预读 |
|
|
20
|
+
| 确定页面结构、配置展示或扩展 UI | [设计选择与信息组织](docs/design.md#设计选择与信息组织)及本次布局、展示能力对应章节 |
|
|
21
|
+
| 执行整页验收与交付 | [开发与验收](docs/acceptance.md)第 2–5 节 |
|
|
22
|
+
| 部署或验证生产身份 | [生产认证与部署](docs/auth-and-deploy.md#生产) |
|
|
23
|
+
|
|
12
24
|
## 数据规则
|
|
13
25
|
|
|
14
|
-
-
|
|
26
|
+
- `widgets[].dimensionIds` 为可选字段,常规绑定省略以沿用保存配置;需要指定时列出所需维度和指标的完整白名单,只传维度会关闭未选指标。与 CLI `--dimension-ids` 使用同一来源的可选 dId,完整语义见[数据契约](docs/data-contract.md#字段选择)。
|
|
15
27
|
- 先检查 normalization、warnings、range,再选择唯一 dataset;多模型需显式 datasetId。
|
|
16
28
|
- display 用于显示;计算仅使用 AVAILABLE 的 value,保留 DECIMAL_STRING 精度。禁止从百分比/千分位/单位文本恢复数值。受限值、未知值不能补零。
|
|
29
|
+
- 分组表 BIGROUP 使用 table 展示,按原生汇总标记区分数据、小计和总计;不能混加各类行。交叉表 BICROSS 按 top 槽位对应 left 数值,仅用 table 展示当前矩阵,不能按重复的指标 dId 合并列或固定绑定跨查询槽位。详见 docs/data-contract.md。
|
|
17
30
|
- 不按名称猜字段、不累加 UNKNOWN 行、不从首页估算全量 KPI。KPI 示例仅消费单行结果。
|
|
18
31
|
- 控件筛选通过真实名称生成 refer;无控件时不伪造服务端过滤。搜索和排序的范围必须在页面说明。
|
|
19
32
|
- 筛选绑定先核对源控件类型,配置/状态/请求统一通过 src/finebi/filters.js 校验。完整支持矩阵、逗号和 null 保留字、恢复默认与筛选空值语义见 docs/filters.md;不以更换 type 绕过服务端能力限制。
|
|
@@ -21,9 +34,9 @@
|
|
|
21
34
|
|
|
22
35
|
## 认证与交付
|
|
23
36
|
|
|
24
|
-
-
|
|
37
|
+
- 开发凭证由 Node 从 FINEBI_DEV_* 环境或项目 .env.local 读取;新会话自动保存到 .env.local。优先复用 token,获取时密码优先、浏览器授权兜底,具体流程见 docs/auth-and-deploy.md。禁止 VITE_* 凭证、前端 localStorage token、提交凭据文件和认证秘密日志。
|
|
25
38
|
- 生产应用使用同源当前用户 Cookie。开发代理不属于构建产物;不要把它部署为共享多用户服务。
|
|
26
|
-
- 目录来源配置 directoryEntryId,原生请求保留 reportEntryId;需目标平台 HTTP 接口支持目录权限预检与 MOUNT
|
|
39
|
+
- 目录来源配置 directoryEntryId,原生请求保留 reportEntryId;需目标平台 HTTP 接口支持目录权限预检与 MOUNT 查询,不得失败后回退到分析主题。不支持服务端续页、全局排序或托管发布命令。
|
|
27
40
|
- 图表按生命周期销毁;异步请求取消且旧响应不得覆盖新条件。失败状态不得展示过期数据充当最新结果。
|
|
28
41
|
- 验收证据、截图和临时脚本放项目 temp/,不随包发布。交付记录已验证/失败/未验证项、运行方式、模板版本和业务绑定范围;模板维护流程由 CLI 源仓库负责,本项目无需其脚本才能开发和验收。
|
|
29
42
|
|
|
@@ -4,14 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
## 开始
|
|
6
6
|
|
|
7
|
+
按 [Agent 阅读路径](AGENTS.md#阅读路径)分阶段读取文档,再执行以下步骤。
|
|
8
|
+
|
|
7
9
|
1. 安装依赖:`npm ci`。
|
|
8
|
-
2.
|
|
9
|
-
3.
|
|
10
|
-
4. `npm run dev`,访问 `http://localhost:4173
|
|
11
|
-
5. 按[
|
|
10
|
+
2. 先在 `public/app-config.json` 中填写一个真实组件的最小绑定。`dashboardId` 来自目标看板;`widgets[].entryId` 使用板内实例 ID;图表 `categoryFieldId/valueFieldId` 使用本次 `dashboard info` 的结果字段 dId。相同实例可以有不同展示,但 KPI 必须对应单行查询结果。
|
|
11
|
+
3. 在项目 `.env.local` 设置 `FINEBI_DEV_BASE_URL`(完整 decision 地址);有账密时填写 `FINEBI_DEV_USERNAME/FINEBI_DEV_PASSWORD`,只有 SSO 时省略。也可提供已有的 FineBI 会话 `FINEBI_DEV_TOKEN`,不能使用 CLI Access Key。不要把值写入 VITE_* 或 app-config。
|
|
12
|
+
4. `npm run dev`,访问 `http://localhost:4173`。代理优先复用 token,再尝试密码,最后按终端提示进行浏览器授权。新 token 自动保存到 `.env.local`;过期后恢复并重试一次。浏览器授权的服务端插件和 HTTPS 要求见 [认证与部署](docs/auth-and-deploy.md)。
|
|
13
|
+
5. 按[最小真实查询](docs/acceptance.md#11-最小真实查询)核对实际请求与页面值,补齐适用的代表性图表和筛选链路,通过后再扩展整页。
|
|
14
|
+
6. 按[开发与验收](docs/acceptance.md)执行 `npm test`、`npm run build`、`npm run test:browser`,并完成整页真实绑定验收。浏览器验收页使用合成数据,不连接 FineBI。`npm run preview` 只展示 dist,不启用开发身份代理;真实生产取数需要同源 FineBI 路径。
|
|
12
15
|
|
|
13
16
|
## 配置
|
|
14
17
|
|
|
18
|
+
`widgets[].dimensionIds` 可选,常规绑定省略以沿用组件保存配置。需要指定维度和指标时填写完整白名单,`[]` 会关闭全部可选字段;示例、代理校验与同实例多绑定规则见[字段选择](docs/data-contract.md#字段选择)。
|
|
19
|
+
|
|
15
20
|
`layout` 为 `overview`(顶部概览)、`workbench`(侧栏工作台)、`cockpit`(绿色经营驾驶舱)或 `retail`(米白经营简报)。可用 `finebi-cli app create --name my-retail --path <parent> --layout retail` 创建;默认配置仍为空绑定,不会请求示例资源。`maxRows` 默认 1000,上限 10000;`pageSize` 是本地展示页大小,1–100。
|
|
16
21
|
|
|
17
22
|
```json
|
|
@@ -31,6 +36,8 @@ cockpit 的配置骨架在 [examples/cockpit.config.json](examples/cockpit.confi
|
|
|
31
36
|
|
|
32
37
|
retail 的 [examples/retail.config.json](examples/retail.config.json) 提供零售分析绑定骨架:四项 KPI、趋势、门店 TOP10、区域/品类、门店类型和明细。采用横向导航、米白背景和蓝橙图表,不依赖侧栏。示例不含业务数据,`REPLACE_*`、模型选择、时区及筛选作用范围必须核实替换;普通分析来源需删除目录入口占位字段。金额与百分比、毫秒月份的显式显示配置见[页面骨架](docs/design.md)。
|
|
33
38
|
|
|
39
|
+
分组表 BIGROUP 可使用 `type: "table"` 展示,保留分组键及小计、总计行;字段和值状态、首页范围见[数据契约](docs/data-contract.md#分组表)。交叉表 BICROSS 同样绑定为 `type: "table"`,按行列分组路径显示当前矩阵;通常省略 tableFieldIds,以保留随查询变化的全部交叉列。它不提供服务端翻页、节点展开或原生合并单元格,字段和双轴范围见[交叉表契约](docs/data-contract.md#交叉表)。
|
|
40
|
+
|
|
34
41
|
目录发布看板在配置中增加 `directoryEntryId`,使用与该入口匹配的 dashboardId/实例。模板将其原样作为 `reportEntryId` 传给 Cookie 查询接口及开发代理;目标平台需同时支持目录权限预检和 MOUNT 查询,不能只支持 CLI 目录查询。错误独立显示,不回退“我的分析”。
|
|
35
42
|
|
|
36
|
-
筛选先编辑、再点击“应用筛选”;“恢复全部默认”重新查询看板保存条件。模板支持文本、单选/多选、数值选择和日期类控件,完整类型、空值语义与配置示例以[筛选契约](docs/filters.md)为准;`app inspect`
|
|
43
|
+
筛选先编辑、再点击“应用筛选”;“恢复全部默认”重新查询看板保存条件。模板支持文本、单选/多选、数值选择和日期类控件,完整类型、空值语义与配置示例以[筛选契约](docs/filters.md)为准;`app inspect` 默认只查看配置摘要;可选 `--strict` 检查与 CLI 内置模板规则的兼容性,不作为定制应用验收门禁。绑定后仍需通过项目测试、构建与真实浏览器验证条件生效。
|