@finebi/finebi-cli 1.6.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/README.md +3 -1
  2. package/agent-installation-guide.md +70 -0
  3. package/package.json +2 -1
  4. package/skills/finebi-cli-agent/references/dashboard-data-result.md +191 -191
  5. package/skills/finebi-cli-agent/references/dashboard-dsl-api.md +44 -44
  6. package/skills/finebi-cli-agent/scripts/finebi-cli/package.json +1 -1
  7. package/skills/finebi-cli-agent/scripts/finebi-cli/src/app.js +160 -157
  8. package/skills/finebi-cli-agent/scripts/finebi-cli/src/config.js +27 -27
  9. package/skills/finebi-cli-agent/scripts/finebi-cli/src/help.js +55 -55
  10. package/skills/finebi-cli-agent/scripts/finebi-cli/src/install-skill.js +89 -89
  11. package/skills/finebi-cli-agent/scripts/finebi-cli/src/profiles.js +158 -158
  12. package/skills/finebi-cli-agent/scripts/finebi-cli/src/skill-files.js +95 -94
  13. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/acceptance.md +80 -80
  14. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/examples/cockpit.config.json +22 -22
  15. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/examples/retail.config.json +185 -185
  16. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/gitignore.template +6 -0
  17. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/index.html +2 -2
  18. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/public/app-config.json +11 -11
  19. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/App.tsx +63 -63
  20. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/cockpit.css +3 -3
  21. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Chart.tsx +40 -40
  22. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Insights.tsx +17 -17
  23. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/MonthPicker.tsx +32 -32
  24. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/OptionFilter.tsx +7 -7
  25. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/RangeFilter.tsx +9 -9
  26. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Widget.tsx +40 -40
  27. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/calendar-locales.ts +3 -3
  28. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/calendar.ts +14 -14
  29. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Bar.tsx +6 -6
  30. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Line.tsx +6 -6
  31. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Pie.tsx +6 -6
  32. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/month-picker.css +1 -1
  33. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/config.d.ts +3 -3
  34. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/config.js +58 -58
  35. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/date.d.ts +1 -1
  36. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/date.js +5 -5
  37. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/filters.d.ts +4 -4
  38. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/filters.js +106 -106
  39. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/normalize.ts +235 -235
  40. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/types.ts +20 -20
  41. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/main.tsx +5 -5
  42. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/retail.css +12 -12
  43. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/presentation.ts +36 -36
  44. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/table-data.ts +47 -47
  45. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/view-data.ts +18 -18
  46. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/widget-queries.ts +18 -18
  47. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/fixtures.mjs +28 -28
  48. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/index.html +16 -16
  49. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/main.tsx +149 -149
  50. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/test-page.css +4 -4
  51. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/vite.config.mjs +13 -13
  52. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/bundle.test.mjs +19 -19
  53. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/cockpit.test.mjs +73 -73
  54. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/field-selection.test.mjs +70 -70
  55. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/retail.test.mjs +31 -31
  56. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/table-normalize.test.mjs +189 -189
  57. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tsconfig.json +4 -4
  58. package/skills/finebi-cli-agent/scripts/finebi-cli-runner.mjs +4 -4
  59. package/src/app.js +160 -157
  60. package/src/config.js +27 -27
  61. package/src/help.js +55 -55
  62. package/src/install-skill.js +89 -89
  63. package/src/profiles.js +158 -158
  64. package/src/skill-files.js +95 -94
  65. package/templates/web-app/docs/acceptance.md +80 -80
  66. package/templates/web-app/examples/cockpit.config.json +22 -22
  67. package/templates/web-app/examples/retail.config.json +185 -185
  68. package/templates/web-app/gitignore.template +6 -0
  69. package/templates/web-app/index.html +2 -2
  70. package/templates/web-app/public/app-config.json +11 -11
  71. package/templates/web-app/src/App.tsx +63 -63
  72. package/templates/web-app/src/cockpit.css +3 -3
  73. package/templates/web-app/src/components/Chart.tsx +40 -40
  74. package/templates/web-app/src/components/Insights.tsx +17 -17
  75. package/templates/web-app/src/components/MonthPicker.tsx +32 -32
  76. package/templates/web-app/src/components/OptionFilter.tsx +7 -7
  77. package/templates/web-app/src/components/RangeFilter.tsx +9 -9
  78. package/templates/web-app/src/components/Widget.tsx +40 -40
  79. package/templates/web-app/src/components/calendar-locales.ts +3 -3
  80. package/templates/web-app/src/components/calendar.ts +14 -14
  81. package/templates/web-app/src/components/charts/Bar.tsx +6 -6
  82. package/templates/web-app/src/components/charts/Line.tsx +6 -6
  83. package/templates/web-app/src/components/charts/Pie.tsx +6 -6
  84. package/templates/web-app/src/components/month-picker.css +1 -1
  85. package/templates/web-app/src/finebi/config.d.ts +3 -3
  86. package/templates/web-app/src/finebi/config.js +58 -58
  87. package/templates/web-app/src/finebi/date.d.ts +1 -1
  88. package/templates/web-app/src/finebi/date.js +5 -5
  89. package/templates/web-app/src/finebi/filters.d.ts +4 -4
  90. package/templates/web-app/src/finebi/filters.js +106 -106
  91. package/templates/web-app/src/finebi/normalize.ts +235 -235
  92. package/templates/web-app/src/finebi/types.ts +20 -20
  93. package/templates/web-app/src/main.tsx +5 -5
  94. package/templates/web-app/src/retail.css +12 -12
  95. package/templates/web-app/src/services/presentation.ts +36 -36
  96. package/templates/web-app/src/services/table-data.ts +47 -47
  97. package/templates/web-app/src/services/view-data.ts +18 -18
  98. package/templates/web-app/src/services/widget-queries.ts +18 -18
  99. package/templates/web-app/tests/browser/fixtures.mjs +28 -28
  100. package/templates/web-app/tests/browser/index.html +16 -16
  101. package/templates/web-app/tests/browser/main.tsx +149 -149
  102. package/templates/web-app/tests/browser/test-page.css +4 -4
  103. package/templates/web-app/tests/browser/vite.config.mjs +13 -13
  104. package/templates/web-app/tests/bundle.test.mjs +19 -19
  105. package/templates/web-app/tests/cockpit.test.mjs +73 -73
  106. package/templates/web-app/tests/field-selection.test.mjs +70 -70
  107. package/templates/web-app/tests/retail.test.mjs +31 -31
  108. package/templates/web-app/tests/table-normalize.test.mjs +189 -189
  109. package/templates/web-app/tsconfig.json +4 -4
@@ -1,4 +1,4 @@
1
- {
2
- "compilerOptions": {"target":"ES2020","lib":["ES2020","DOM","DOM.Iterable"],"module":"ESNext","moduleResolution":"Bundler","jsx":"react-jsx","strict":true,"skipLibCheck":true,"esModuleInterop":true,"resolveJsonModule":true,"isolatedModules":true,"noEmit":true},
3
- "include":["src","vite.config.ts"]
4
- }
1
+ {
2
+ "compilerOptions": {"target":"ES2020","lib":["ES2020","DOM","DOM.Iterable"],"module":"ESNext","moduleResolution":"Bundler","jsx":"react-jsx","strict":true,"skipLibCheck":true,"esModuleInterop":true,"resolveJsonModule":true,"isolatedModules":true,"noEmit":true},
3
+ "include":["src","vite.config.ts"]
4
+ }
@@ -10,10 +10,10 @@ const scriptDirectory = dirname(fileURLToPath(import.meta.url));
10
10
  const bundledCliEntry = resolve(scriptDirectory, "finebi-cli", "src", "index.js");
11
11
 
12
12
  export async function run(argv = process.argv.slice(2), options = {}) {
13
- const env = {...(options.env ?? process.env)};
14
- if (!env.FINEBI_CLI_PATH?.trim() && existsSync(bundledCliEntry)) {
15
- env.FINEBI_CLI_PATH = bundledCliEntry;
16
- }
13
+ const env = {...(options.env ?? process.env)};
14
+ if (!env.FINEBI_CLI_PATH?.trim() && existsSync(bundledCliEntry)) {
15
+ env.FINEBI_CLI_PATH = bundledCliEntry;
16
+ }
17
17
  return runCore(argv, {...options, env});
18
18
  }
19
19
 
package/src/app.js CHANGED
@@ -1,157 +1,160 @@
1
- import {cp, mkdir, readdir, readFile, stat, writeFile} from "node:fs/promises";
2
- import {dirname, resolve} from "node:path";
3
- import {fileURLToPath} from "node:url";
4
-
5
- import {cliError} from "./errors.js";
6
- import {failure, success} from "./output.js";
7
-
8
- const TEMPLATE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "templates", "web-app");
9
- const NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
10
- const WINDOWS_DEVICE_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/iu;
11
-
12
- export async function handleApp(args, context = {}) {
13
- const action = args[0];
14
- if (action === "create") return createApp(args.slice(1), context);
15
- if (action === "inspect") return inspectApp(args.slice(1));
16
- return invalid("app", "App command is required.", ["finebi-cli app create --name <name> --path <parent>", "finebi-cli app inspect --path <project>"]);
17
- }
18
-
19
- export async function createApp(args, context = {}) {
20
- const options = parseOptions(args, "app.create");
21
- if (!options.ok) return options.error;
22
- const {name, path: parentPath} = options.value;
23
- if (!name || name.length > 214 || !NAME_PATTERN.test(name) || WINDOWS_DEVICE_NAME.test(name)) {
24
- return invalid("app.create", "name must be a simple lowercase npm name (letters, digits, and single hyphens).", ["finebi-cli app create --name sales-dashboard --path <parent>"]);
25
- }
26
- if (!parentPath) {
27
- return invalid("app.create", "path must be an existing parent directory.");
28
- }
29
- if (options.value.layout && !['overview','workbench','cockpit','retail'].includes(options.value.layout)) return invalid('app.create','layout must be overview, workbench, cockpit or retail.');
30
- const parent = resolve(parentPath);
31
- const destination = resolve(parent, name);
32
- if (dirname(destination) !== parent) {
33
- return invalid("app.create", "name must stay inside the selected parent directory.");
34
- }
35
- try {
36
- if (!(await stat(parent)).isDirectory()) return invalid("app.create", "path must point to a directory.");
37
- } catch (error) {
38
- return invalid("app.create", error.code === "ENOENT" ? `Parent directory does not exist: ${parent}` : `Cannot inspect parent directory: ${error.message}`);
39
- }
40
- try {
41
- const templateRoot = context.templateRoot ? resolve(context.templateRoot) : TEMPLATE_ROOT;
42
- const templatePackage = JSON.parse(await readFile(resolve(templateRoot, "package.json"), "utf8"));
43
- const 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
- }
1
+ import {cp, mkdir, readdir, readFile, stat, writeFile} from "node:fs/promises";
2
+ import {dirname, resolve} from "node:path";
3
+ import {fileURLToPath} from "node:url";
4
+
5
+ import {cliError} from "./errors.js";
6
+ import {failure, success} from "./output.js";
7
+
8
+ const TEMPLATE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "templates", "web-app");
9
+ const NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
10
+ const WINDOWS_DEVICE_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/iu;
11
+
12
+ export async function handleApp(args, context = {}) {
13
+ const action = args[0];
14
+ if (action === "create") return createApp(args.slice(1), context);
15
+ if (action === "inspect") return inspectApp(args.slice(1));
16
+ return invalid("app", "App command is required.", ["finebi-cli app create --name <name> --path <parent>", "finebi-cli app inspect --path <project>"]);
17
+ }
18
+
19
+ export async function createApp(args, context = {}) {
20
+ const options = parseOptions(args, "app.create");
21
+ if (!options.ok) return options.error;
22
+ const {name, path: parentPath} = options.value;
23
+ if (!name || name.length > 214 || !NAME_PATTERN.test(name) || WINDOWS_DEVICE_NAME.test(name)) {
24
+ return invalid("app.create", "name must be a simple lowercase npm name (letters, digits, and single hyphens).", ["finebi-cli app create --name sales-dashboard --path <parent>"]);
25
+ }
26
+ if (!parentPath) {
27
+ return invalid("app.create", "path must be an existing parent directory.");
28
+ }
29
+ if (options.value.layout && !['overview','workbench','cockpit','retail'].includes(options.value.layout)) return invalid('app.create','layout must be overview, workbench, cockpit or retail.');
30
+ const parent = resolve(parentPath);
31
+ const destination = resolve(parent, name);
32
+ if (dirname(destination) !== parent) {
33
+ return invalid("app.create", "name must stay inside the selected parent directory.");
34
+ }
35
+ try {
36
+ if (!(await stat(parent)).isDirectory()) return invalid("app.create", "path must point to a directory.");
37
+ } catch (error) {
38
+ return invalid("app.create", error.code === "ENOENT" ? `Parent directory does not exist: ${parent}` : `Cannot inspect parent directory: ${error.message}`);
39
+ }
40
+ try {
41
+ const templateRoot = context.templateRoot ? resolve(context.templateRoot) : TEMPLATE_ROOT;
42
+ const templatePackage = JSON.parse(await readFile(resolve(templateRoot, "package.json"), "utf8"));
43
+ const gitignore = await readFile(resolve(templateRoot, "gitignore.template"), "utf8");
44
+ const templateConfigPath = resolve(templateRoot, "public", "app-config.json");
45
+ let config = {schemaVersion: "1", title: name, basePath: "/webroot/decision", layout: "overview", dashboardId: "", maxRows: 1000, pageSize: 8, widgets: [], filters: []};
46
+ try {
47
+ config = JSON.parse(await readFile(templateConfigPath, "utf8"));
48
+ } catch (error) {
49
+ if (error.code !== "ENOENT") throw error;
50
+ }
51
+ try {
52
+ await mkdir(destination, {recursive: false});
53
+ } catch (error) {
54
+ return invalid("app.create", error.code === "EEXIST" ? `Destination already exists: ${destination}` : `Cannot create destination: ${error.message}`);
55
+ }
56
+ await copyTemplateContents(templateRoot, destination);
57
+ await writeFile(resolve(destination, ".gitignore"), gitignore, "utf8");
58
+ if(options.value.layout) config.layout=options.value.layout;
59
+ if(config.layout==='cockpit') config.insights=true;
60
+ await mkdir(resolve(destination, "public"), {recursive: true});
61
+ await writeFile(resolve(destination, "package.json"), `${JSON.stringify({...templatePackage, name}, null, 2)}\n`, "utf8");
62
+ await writeFile(resolve(destination, "public", "app-config.json"), `${JSON.stringify({...config, templateVersion: templatePackage.version}, null, 2)}\n`, "utf8");
63
+ const lockPath = resolve(destination, "package-lock.json");
64
+ try {
65
+ const lock = JSON.parse(await readFile(lockPath, "utf8"));
66
+ lock.name = name;
67
+ if (lock.packages?.[""]) lock.packages[""].name = name;
68
+ await writeFile(lockPath, `${JSON.stringify(lock, null, 2)}\n`, "utf8");
69
+ } catch (error) {
70
+ if (error.code !== "ENOENT") throw error;
71
+ }
72
+ return success("app.create", {name, path: destination, templateVersion: templatePackage.version});
73
+ } catch (error) {
74
+ return invalid("app.create", `Could not create project: ${error.message}`);
75
+ }
76
+ }
77
+
78
+ async function copyTemplateContents(sourceRoot, destinationRoot, relativeDirectory = "") {
79
+ const sourceDirectory = resolve(sourceRoot, relativeDirectory);
80
+ const destinationDirectory = resolve(destinationRoot, relativeDirectory);
81
+ for (const entry of (await readdir(sourceDirectory, {withFileTypes: true})).sort((left, right) => left.name.localeCompare(right.name, "en"))) {
82
+ const relativePath = `${relativeDirectory}/${entry.name}`.replace(/^\//u, "");
83
+ if (relativePath === ".gitignore" || relativePath === "gitignore.template") continue;
84
+ if (!templatePathAllowed(relativePath)) continue;
85
+ const sourcePath = resolve(sourceDirectory, entry.name);
86
+ const destinationPath = resolve(destinationDirectory, entry.name);
87
+ if (entry.isDirectory()) {
88
+ await mkdir(destinationPath, {recursive: false});
89
+ await copyTemplateContents(sourceRoot, destinationRoot, relativePath);
90
+ } else if (entry.isFile()) {
91
+ await cp(sourcePath, destinationPath, {force: false, errorOnExist: true});
92
+ } else {
93
+ throw new Error(`Unsupported template entry: ${relativePath}`);
94
+ }
95
+ }
96
+ }
97
+
98
+ function templatePathAllowed(relative) {
99
+ const relativePath = relative.replaceAll("\\", "/");
100
+ const parts = relativePath.split("/").filter(Boolean);
101
+ const basename = parts.at(-1) || "";
102
+ return !parts.some(part => ["node_modules", "dist", "temp", ".git"].includes(part))
103
+ && basename !== ".env"
104
+ && (!basename.startsWith(".env.") || basename === ".env.example");
105
+ }
106
+
107
+ async function inspectApp(args) {
108
+ const options = parseOptions(args, "app.inspect");
109
+ if (!options.ok) return options.error;
110
+ const projectPath = options.value.path;
111
+ if (!projectPath) return invalid("app.inspect", "path must be a project directory.");
112
+ const project = resolve(projectPath);
113
+ try {
114
+ if (!(await stat(project)).isDirectory()) return invalid("app.inspect", "path must point to a project directory.");
115
+ const configPath = resolve(project, "public", "app-config.json");
116
+ const config = JSON.parse(await readFile(configPath, "utf8"));
117
+ if (!config || typeof config !== "object" || Array.isArray(config)) return invalid("app.inspect", "app-config.json must contain a JSON object.");
118
+ if (options.value.strict) {
119
+ const {configErrors}=await import('../templates/web-app/src/finebi/config.js');
120
+ const errors = configErrors(config);
121
+ if (errors.length) return failure("app.inspect", cliError("INVALID_ARGUMENT", {path: "public/app-config.json", message: errors.join(" ")}));
122
+ }
123
+ return success("app.inspect", {
124
+ path: project,
125
+ dashboardId: config.dashboardId ?? null,
126
+ layout: config.layout ?? null,
127
+ directoryEntryId: config.directoryEntryId || null,
128
+ widgets: Array.isArray(config.widgets) ? config.widgets.length : null,
129
+ templateVersion: config.templateVersion || null,
130
+ configValidated: options.value.strict === true,
131
+ remoteValidated: false,
132
+ });
133
+ } catch (error) {
134
+ return invalid("app.inspect", `Cannot read project configuration: ${error.message}`);
135
+ }
136
+ }
137
+
138
+ function parseOptions(args, command) {
139
+ const value = {};
140
+ for (let index = 0; index < args.length; index++) {
141
+ const option = args[index];
142
+ if (option === "--json") continue;
143
+ if (option === "--strict" && command === "app.inspect") {
144
+ if (value.strict) return {ok: false, error: invalid(command, "--strict must only be supplied once.")};
145
+ value.strict = true;
146
+ continue;
147
+ }
148
+ if (!["--name","--path",...(command==='app.create'?['--layout']:[])].includes(option)) return {ok: false, error: invalid(command, `Unsupported option: ${option}`)};
149
+ const key = option.slice(2);
150
+ const next = args[++index];
151
+ if (!next || next.startsWith("--")) return {ok: false, error: invalid(command, `${option} requires a value.`)};
152
+ if (value[key]) return {ok: false, error: invalid(command, `${option} must only be supplied once.`)};
153
+ value[key] = next;
154
+ }
155
+ return {ok: true, value};
156
+ }
157
+
158
+ function invalid(command, message, nextActions = []) {
159
+ return failure(command, cliError("INVALID_ARGUMENT", {path: command, message, nextActions}));
160
+ }
package/src/config.js CHANGED
@@ -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();