@finebi/finebi-cli 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +3 -1
  2. package/package.json +1 -1
  3. package/skills/finebi-cli-agent/SKILL.md +1 -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/references/web-app.md +4 -1
  7. package/skills/finebi-cli-agent/scripts/finebi-cli/package.json +1 -1
  8. package/skills/finebi-cli-agent/scripts/finebi-cli/src/app.js +160 -160
  9. package/skills/finebi-cli-agent/scripts/finebi-cli/src/config.js +27 -27
  10. package/skills/finebi-cli-agent/scripts/finebi-cli/src/help.js +55 -55
  11. package/skills/finebi-cli-agent/scripts/finebi-cli/src/install-skill.js +89 -89
  12. package/skills/finebi-cli-agent/scripts/finebi-cli/src/profiles.js +158 -158
  13. package/skills/finebi-cli-agent/scripts/finebi-cli/src/skill-files.js +96 -95
  14. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/AGENTS.md +1 -1
  15. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/acceptance.md +80 -80
  16. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/examples/cockpit.config.json +22 -22
  17. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/examples/retail.config.json +185 -185
  18. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/gitignore.template +6 -6
  19. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/index.html +2 -2
  20. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/public/app-config.json +11 -11
  21. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/App.tsx +63 -63
  22. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/cockpit.css +3 -3
  23. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Chart.tsx +40 -40
  24. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Insights.tsx +17 -17
  25. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/MonthPicker.tsx +32 -32
  26. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/OptionFilter.tsx +7 -7
  27. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/RangeFilter.tsx +9 -9
  28. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Widget.tsx +40 -40
  29. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/calendar-locales.ts +3 -3
  30. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/calendar.ts +14 -14
  31. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Bar.tsx +6 -6
  32. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Line.tsx +6 -6
  33. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Pie.tsx +6 -6
  34. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/month-picker.css +1 -1
  35. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/config.d.ts +3 -3
  36. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/config.js +58 -58
  37. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/date.d.ts +1 -1
  38. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/date.js +5 -5
  39. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/filters.d.ts +4 -4
  40. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/filters.js +106 -106
  41. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/normalize.ts +235 -235
  42. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/types.ts +20 -20
  43. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/main.tsx +5 -5
  44. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/retail.css +12 -12
  45. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/presentation.ts +36 -36
  46. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/table-data.ts +47 -47
  47. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/view-data.ts +18 -18
  48. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/widget-queries.ts +18 -18
  49. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/fixtures.mjs +28 -28
  50. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/index.html +16 -16
  51. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/main.tsx +149 -149
  52. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/test-page.css +4 -4
  53. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/vite.config.mjs +13 -13
  54. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/bundle.test.mjs +19 -19
  55. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/cockpit.test.mjs +73 -73
  56. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/field-selection.test.mjs +70 -70
  57. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/retail.test.mjs +31 -31
  58. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/table-normalize.test.mjs +189 -189
  59. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tsconfig.json +4 -4
  60. package/skills/finebi-cli-agent/scripts/finebi-cli-runner.mjs +4 -4
  61. package/src/app.js +160 -160
  62. package/src/config.js +27 -27
  63. package/src/help.js +55 -55
  64. package/src/install-skill.js +89 -89
  65. package/src/profiles.js +158 -158
  66. package/src/skill-files.js +96 -95
  67. package/templates/web-app/AGENTS.md +1 -1
  68. package/templates/web-app/docs/acceptance.md +80 -80
  69. package/templates/web-app/examples/cockpit.config.json +22 -22
  70. package/templates/web-app/examples/retail.config.json +185 -185
  71. package/templates/web-app/gitignore.template +6 -6
  72. package/templates/web-app/index.html +2 -2
  73. package/templates/web-app/public/app-config.json +11 -11
  74. package/templates/web-app/src/App.tsx +63 -63
  75. package/templates/web-app/src/cockpit.css +3 -3
  76. package/templates/web-app/src/components/Chart.tsx +40 -40
  77. package/templates/web-app/src/components/Insights.tsx +17 -17
  78. package/templates/web-app/src/components/MonthPicker.tsx +32 -32
  79. package/templates/web-app/src/components/OptionFilter.tsx +7 -7
  80. package/templates/web-app/src/components/RangeFilter.tsx +9 -9
  81. package/templates/web-app/src/components/Widget.tsx +40 -40
  82. package/templates/web-app/src/components/calendar-locales.ts +3 -3
  83. package/templates/web-app/src/components/calendar.ts +14 -14
  84. package/templates/web-app/src/components/charts/Bar.tsx +6 -6
  85. package/templates/web-app/src/components/charts/Line.tsx +6 -6
  86. package/templates/web-app/src/components/charts/Pie.tsx +6 -6
  87. package/templates/web-app/src/components/month-picker.css +1 -1
  88. package/templates/web-app/src/finebi/config.d.ts +3 -3
  89. package/templates/web-app/src/finebi/config.js +58 -58
  90. package/templates/web-app/src/finebi/date.d.ts +1 -1
  91. package/templates/web-app/src/finebi/date.js +5 -5
  92. package/templates/web-app/src/finebi/filters.d.ts +4 -4
  93. package/templates/web-app/src/finebi/filters.js +106 -106
  94. package/templates/web-app/src/finebi/normalize.ts +235 -235
  95. package/templates/web-app/src/finebi/types.ts +20 -20
  96. package/templates/web-app/src/main.tsx +5 -5
  97. package/templates/web-app/src/retail.css +12 -12
  98. package/templates/web-app/src/services/presentation.ts +36 -36
  99. package/templates/web-app/src/services/table-data.ts +47 -47
  100. package/templates/web-app/src/services/view-data.ts +18 -18
  101. package/templates/web-app/src/services/widget-queries.ts +18 -18
  102. package/templates/web-app/tests/browser/fixtures.mjs +28 -28
  103. package/templates/web-app/tests/browser/index.html +16 -16
  104. package/templates/web-app/tests/browser/main.tsx +149 -149
  105. package/templates/web-app/tests/browser/test-page.css +4 -4
  106. package/templates/web-app/tests/browser/vite.config.mjs +13 -13
  107. package/templates/web-app/tests/bundle.test.mjs +19 -19
  108. package/templates/web-app/tests/cockpit.test.mjs +73 -73
  109. package/templates/web-app/tests/field-selection.test.mjs +70 -70
  110. package/templates/web-app/tests/retail.test.mjs +31 -31
  111. package/templates/web-app/tests/table-normalize.test.mjs +189 -189
  112. package/templates/web-app/tsconfig.json +4 -4
@@ -1,160 +1,160 @@
1
- import {cp, mkdir, readdir, readFile, stat, writeFile} from "node:fs/promises";
2
- import {dirname, resolve} from "node:path";
3
- import {fileURLToPath} from "node:url";
4
-
5
- import {cliError} from "./errors.js";
6
- import {failure, success} from "./output.js";
7
-
8
- const TEMPLATE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "templates", "web-app");
9
- const NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
10
- const WINDOWS_DEVICE_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/iu;
11
-
12
- export async function handleApp(args, context = {}) {
13
- const action = args[0];
14
- if (action === "create") return createApp(args.slice(1), context);
15
- if (action === "inspect") return inspectApp(args.slice(1));
16
- return invalid("app", "App command is required.", ["finebi-cli app create --name <name> --path <parent>", "finebi-cli app inspect --path <project>"]);
17
- }
18
-
19
- export async function createApp(args, context = {}) {
20
- const options = parseOptions(args, "app.create");
21
- if (!options.ok) return options.error;
22
- const {name, path: parentPath} = options.value;
23
- if (!name || name.length > 214 || !NAME_PATTERN.test(name) || WINDOWS_DEVICE_NAME.test(name)) {
24
- return invalid("app.create", "name must be a simple lowercase npm name (letters, digits, and single hyphens).", ["finebi-cli app create --name sales-dashboard --path <parent>"]);
25
- }
26
- if (!parentPath) {
27
- return invalid("app.create", "path must be an existing parent directory.");
28
- }
29
- if (options.value.layout && !['overview','workbench','cockpit','retail'].includes(options.value.layout)) return invalid('app.create','layout must be overview, workbench, cockpit or retail.');
30
- const parent = resolve(parentPath);
31
- const destination = resolve(parent, name);
32
- if (dirname(destination) !== parent) {
33
- return invalid("app.create", "name must stay inside the selected parent directory.");
34
- }
35
- try {
36
- if (!(await stat(parent)).isDirectory()) return invalid("app.create", "path must point to a directory.");
37
- } catch (error) {
38
- return invalid("app.create", error.code === "ENOENT" ? `Parent directory does not exist: ${parent}` : `Cannot inspect parent directory: ${error.message}`);
39
- }
40
- try {
41
- const templateRoot = context.templateRoot ? resolve(context.templateRoot) : TEMPLATE_ROOT;
42
- const templatePackage = JSON.parse(await readFile(resolve(templateRoot, "package.json"), "utf8"));
43
- const gitignore = await readFile(resolve(templateRoot, "gitignore.template"), "utf8");
44
- const templateConfigPath = resolve(templateRoot, "public", "app-config.json");
45
- let config = {schemaVersion: "1", title: name, basePath: "/webroot/decision", layout: "overview", dashboardId: "", maxRows: 1000, pageSize: 8, widgets: [], filters: []};
46
- try {
47
- config = JSON.parse(await readFile(templateConfigPath, "utf8"));
48
- } catch (error) {
49
- if (error.code !== "ENOENT") throw error;
50
- }
51
- try {
52
- await mkdir(destination, {recursive: false});
53
- } catch (error) {
54
- return invalid("app.create", error.code === "EEXIST" ? `Destination already exists: ${destination}` : `Cannot create destination: ${error.message}`);
55
- }
56
- await copyTemplateContents(templateRoot, destination);
57
- await writeFile(resolve(destination, ".gitignore"), gitignore, "utf8");
58
- if(options.value.layout) config.layout=options.value.layout;
59
- if(config.layout==='cockpit') config.insights=true;
60
- await mkdir(resolve(destination, "public"), {recursive: true});
61
- await writeFile(resolve(destination, "package.json"), `${JSON.stringify({...templatePackage, name}, null, 2)}\n`, "utf8");
62
- await writeFile(resolve(destination, "public", "app-config.json"), `${JSON.stringify({...config, templateVersion: templatePackage.version}, null, 2)}\n`, "utf8");
63
- const lockPath = resolve(destination, "package-lock.json");
64
- try {
65
- const lock = JSON.parse(await readFile(lockPath, "utf8"));
66
- lock.name = name;
67
- if (lock.packages?.[""]) lock.packages[""].name = name;
68
- await writeFile(lockPath, `${JSON.stringify(lock, null, 2)}\n`, "utf8");
69
- } catch (error) {
70
- if (error.code !== "ENOENT") throw error;
71
- }
72
- return success("app.create", {name, path: destination, templateVersion: templatePackage.version});
73
- } catch (error) {
74
- return invalid("app.create", `Could not create project: ${error.message}`);
75
- }
76
- }
77
-
78
- async function copyTemplateContents(sourceRoot, destinationRoot, relativeDirectory = "") {
79
- const sourceDirectory = resolve(sourceRoot, relativeDirectory);
80
- const destinationDirectory = resolve(destinationRoot, relativeDirectory);
81
- for (const entry of (await readdir(sourceDirectory, {withFileTypes: true})).sort((left, right) => left.name.localeCompare(right.name, "en"))) {
82
- const relativePath = `${relativeDirectory}/${entry.name}`.replace(/^\//u, "");
83
- if (relativePath === ".gitignore" || relativePath === "gitignore.template") continue;
84
- if (!templatePathAllowed(relativePath)) continue;
85
- const sourcePath = resolve(sourceDirectory, entry.name);
86
- const destinationPath = resolve(destinationDirectory, entry.name);
87
- if (entry.isDirectory()) {
88
- await mkdir(destinationPath, {recursive: false});
89
- await copyTemplateContents(sourceRoot, destinationRoot, relativePath);
90
- } else if (entry.isFile()) {
91
- await cp(sourcePath, destinationPath, {force: false, errorOnExist: true});
92
- } else {
93
- throw new Error(`Unsupported template entry: ${relativePath}`);
94
- }
95
- }
96
- }
97
-
98
- function templatePathAllowed(relative) {
99
- const relativePath = relative.replaceAll("\\", "/");
100
- const parts = relativePath.split("/").filter(Boolean);
101
- const basename = parts.at(-1) || "";
102
- return !parts.some(part => ["node_modules", "dist", "temp", ".git"].includes(part))
103
- && basename !== ".env"
104
- && (!basename.startsWith(".env.") || basename === ".env.example");
105
- }
106
-
107
- async function inspectApp(args) {
108
- const options = parseOptions(args, "app.inspect");
109
- if (!options.ok) return options.error;
110
- const projectPath = options.value.path;
111
- if (!projectPath) return invalid("app.inspect", "path must be a project directory.");
112
- const project = resolve(projectPath);
113
- try {
114
- if (!(await stat(project)).isDirectory()) return invalid("app.inspect", "path must point to a project directory.");
115
- const configPath = resolve(project, "public", "app-config.json");
116
- const config = JSON.parse(await readFile(configPath, "utf8"));
117
- if (!config || typeof config !== "object" || Array.isArray(config)) return invalid("app.inspect", "app-config.json must contain a JSON object.");
118
- if (options.value.strict) {
119
- const {configErrors}=await import('../templates/web-app/src/finebi/config.js');
120
- const errors = configErrors(config);
121
- if (errors.length) return failure("app.inspect", cliError("INVALID_ARGUMENT", {path: "public/app-config.json", message: errors.join(" ")}));
122
- }
123
- return success("app.inspect", {
124
- path: project,
125
- dashboardId: config.dashboardId ?? null,
126
- layout: config.layout ?? null,
127
- directoryEntryId: config.directoryEntryId || null,
128
- widgets: Array.isArray(config.widgets) ? config.widgets.length : null,
129
- templateVersion: config.templateVersion || null,
130
- configValidated: options.value.strict === true,
131
- remoteValidated: false,
132
- });
133
- } catch (error) {
134
- return invalid("app.inspect", `Cannot read project configuration: ${error.message}`);
135
- }
136
- }
137
-
138
- function parseOptions(args, command) {
139
- const value = {};
140
- for (let index = 0; index < args.length; index++) {
141
- const option = args[index];
142
- if (option === "--json") continue;
143
- if (option === "--strict" && command === "app.inspect") {
144
- if (value.strict) return {ok: false, error: invalid(command, "--strict must only be supplied once.")};
145
- value.strict = true;
146
- continue;
147
- }
148
- if (!["--name","--path",...(command==='app.create'?['--layout']:[])].includes(option)) return {ok: false, error: invalid(command, `Unsupported option: ${option}`)};
149
- const key = option.slice(2);
150
- const next = args[++index];
151
- if (!next || next.startsWith("--")) return {ok: false, error: invalid(command, `${option} requires a value.`)};
152
- if (value[key]) return {ok: false, error: invalid(command, `${option} must only be supplied once.`)};
153
- value[key] = next;
154
- }
155
- return {ok: true, value};
156
- }
157
-
158
- function invalid(command, message, nextActions = []) {
159
- return failure(command, cliError("INVALID_ARGUMENT", {path: command, message, nextActions}));
160
- }
1
+ import {cp, mkdir, readdir, readFile, stat, writeFile} from "node:fs/promises";
2
+ import {dirname, resolve} from "node:path";
3
+ import {fileURLToPath} from "node:url";
4
+
5
+ import {cliError} from "./errors.js";
6
+ import {failure, success} from "./output.js";
7
+
8
+ const TEMPLATE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "templates", "web-app");
9
+ const NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
10
+ const WINDOWS_DEVICE_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/iu;
11
+
12
+ export async function handleApp(args, context = {}) {
13
+ const action = args[0];
14
+ if (action === "create") return createApp(args.slice(1), context);
15
+ if (action === "inspect") return inspectApp(args.slice(1));
16
+ return invalid("app", "App command is required.", ["finebi-cli app create --name <name> --path <parent>", "finebi-cli app inspect --path <project>"]);
17
+ }
18
+
19
+ export async function createApp(args, context = {}) {
20
+ const options = parseOptions(args, "app.create");
21
+ if (!options.ok) return options.error;
22
+ const {name, path: parentPath} = options.value;
23
+ if (!name || name.length > 214 || !NAME_PATTERN.test(name) || WINDOWS_DEVICE_NAME.test(name)) {
24
+ return invalid("app.create", "name must be a simple lowercase npm name (letters, digits, and single hyphens).", ["finebi-cli app create --name sales-dashboard --path <parent>"]);
25
+ }
26
+ if (!parentPath) {
27
+ return invalid("app.create", "path must be an existing parent directory.");
28
+ }
29
+ if (options.value.layout && !['overview','workbench','cockpit','retail'].includes(options.value.layout)) return invalid('app.create','layout must be overview, workbench, cockpit or retail.');
30
+ const parent = resolve(parentPath);
31
+ const destination = resolve(parent, name);
32
+ if (dirname(destination) !== parent) {
33
+ return invalid("app.create", "name must stay inside the selected parent directory.");
34
+ }
35
+ try {
36
+ if (!(await stat(parent)).isDirectory()) return invalid("app.create", "path must point to a directory.");
37
+ } catch (error) {
38
+ return invalid("app.create", error.code === "ENOENT" ? `Parent directory does not exist: ${parent}` : `Cannot inspect parent directory: ${error.message}`);
39
+ }
40
+ try {
41
+ const templateRoot = context.templateRoot ? resolve(context.templateRoot) : TEMPLATE_ROOT;
42
+ const templatePackage = JSON.parse(await readFile(resolve(templateRoot, "package.json"), "utf8"));
43
+ const gitignore = await readFile(resolve(templateRoot, "gitignore.template"), "utf8");
44
+ const templateConfigPath = resolve(templateRoot, "public", "app-config.json");
45
+ let config = {schemaVersion: "1", title: name, basePath: "/webroot/decision", layout: "overview", dashboardId: "", maxRows: 1000, pageSize: 8, widgets: [], filters: []};
46
+ try {
47
+ config = JSON.parse(await readFile(templateConfigPath, "utf8"));
48
+ } catch (error) {
49
+ if (error.code !== "ENOENT") throw error;
50
+ }
51
+ try {
52
+ await mkdir(destination, {recursive: false});
53
+ } catch (error) {
54
+ return invalid("app.create", error.code === "EEXIST" ? `Destination already exists: ${destination}` : `Cannot create destination: ${error.message}`);
55
+ }
56
+ await copyTemplateContents(templateRoot, destination);
57
+ await writeFile(resolve(destination, ".gitignore"), gitignore, "utf8");
58
+ if(options.value.layout) config.layout=options.value.layout;
59
+ if(config.layout==='cockpit') config.insights=true;
60
+ await mkdir(resolve(destination, "public"), {recursive: true});
61
+ await writeFile(resolve(destination, "package.json"), `${JSON.stringify({...templatePackage, name}, null, 2)}\n`, "utf8");
62
+ await writeFile(resolve(destination, "public", "app-config.json"), `${JSON.stringify({...config, templateVersion: templatePackage.version}, null, 2)}\n`, "utf8");
63
+ const lockPath = resolve(destination, "package-lock.json");
64
+ try {
65
+ const lock = JSON.parse(await readFile(lockPath, "utf8"));
66
+ lock.name = name;
67
+ if (lock.packages?.[""]) lock.packages[""].name = name;
68
+ await writeFile(lockPath, `${JSON.stringify(lock, null, 2)}\n`, "utf8");
69
+ } catch (error) {
70
+ if (error.code !== "ENOENT") throw error;
71
+ }
72
+ return success("app.create", {name, path: destination, templateVersion: templatePackage.version});
73
+ } catch (error) {
74
+ return invalid("app.create", `Could not create project: ${error.message}`);
75
+ }
76
+ }
77
+
78
+ async function copyTemplateContents(sourceRoot, destinationRoot, relativeDirectory = "") {
79
+ const sourceDirectory = resolve(sourceRoot, relativeDirectory);
80
+ const destinationDirectory = resolve(destinationRoot, relativeDirectory);
81
+ for (const entry of (await readdir(sourceDirectory, {withFileTypes: true})).sort((left, right) => left.name.localeCompare(right.name, "en"))) {
82
+ const relativePath = `${relativeDirectory}/${entry.name}`.replace(/^\//u, "");
83
+ if (relativePath === ".gitignore" || relativePath === "gitignore.template") continue;
84
+ if (!templatePathAllowed(relativePath)) continue;
85
+ const sourcePath = resolve(sourceDirectory, entry.name);
86
+ const destinationPath = resolve(destinationDirectory, entry.name);
87
+ if (entry.isDirectory()) {
88
+ await mkdir(destinationPath, {recursive: false});
89
+ await copyTemplateContents(sourceRoot, destinationRoot, relativePath);
90
+ } else if (entry.isFile()) {
91
+ await cp(sourcePath, destinationPath, {force: false, errorOnExist: true});
92
+ } else {
93
+ throw new Error(`Unsupported template entry: ${relativePath}`);
94
+ }
95
+ }
96
+ }
97
+
98
+ function templatePathAllowed(relative) {
99
+ const relativePath = relative.replaceAll("\\", "/");
100
+ const parts = relativePath.split("/").filter(Boolean);
101
+ const basename = parts.at(-1) || "";
102
+ return !parts.some(part => ["node_modules", "dist", "temp", ".git"].includes(part))
103
+ && basename !== ".env"
104
+ && (!basename.startsWith(".env.") || basename === ".env.example");
105
+ }
106
+
107
+ async function inspectApp(args) {
108
+ const options = parseOptions(args, "app.inspect");
109
+ if (!options.ok) return options.error;
110
+ const projectPath = options.value.path;
111
+ if (!projectPath) return invalid("app.inspect", "path must be a project directory.");
112
+ const project = resolve(projectPath);
113
+ try {
114
+ if (!(await stat(project)).isDirectory()) return invalid("app.inspect", "path must point to a project directory.");
115
+ const configPath = resolve(project, "public", "app-config.json");
116
+ const config = JSON.parse(await readFile(configPath, "utf8"));
117
+ if (!config || typeof config !== "object" || Array.isArray(config)) return invalid("app.inspect", "app-config.json must contain a JSON object.");
118
+ if (options.value.strict) {
119
+ const {configErrors}=await import('../templates/web-app/src/finebi/config.js');
120
+ const errors = configErrors(config);
121
+ if (errors.length) return failure("app.inspect", cliError("INVALID_ARGUMENT", {path: "public/app-config.json", message: errors.join(" ")}));
122
+ }
123
+ return success("app.inspect", {
124
+ path: project,
125
+ dashboardId: config.dashboardId ?? null,
126
+ layout: config.layout ?? null,
127
+ directoryEntryId: config.directoryEntryId || null,
128
+ widgets: Array.isArray(config.widgets) ? config.widgets.length : null,
129
+ templateVersion: config.templateVersion || null,
130
+ configValidated: options.value.strict === true,
131
+ remoteValidated: false,
132
+ });
133
+ } catch (error) {
134
+ return invalid("app.inspect", `Cannot read project configuration: ${error.message}`);
135
+ }
136
+ }
137
+
138
+ function parseOptions(args, command) {
139
+ const value = {};
140
+ for (let index = 0; index < args.length; index++) {
141
+ const option = args[index];
142
+ if (option === "--json") continue;
143
+ if (option === "--strict" && command === "app.inspect") {
144
+ if (value.strict) return {ok: false, error: invalid(command, "--strict must only be supplied once.")};
145
+ value.strict = true;
146
+ continue;
147
+ }
148
+ if (!["--name","--path",...(command==='app.create'?['--layout']:[])].includes(option)) return {ok: false, error: invalid(command, `Unsupported option: ${option}`)};
149
+ const key = option.slice(2);
150
+ const next = args[++index];
151
+ if (!next || next.startsWith("--")) return {ok: false, error: invalid(command, `${option} requires a value.`)};
152
+ if (value[key]) return {ok: false, error: invalid(command, `${option} must only be supplied once.`)};
153
+ value[key] = next;
154
+ }
155
+ return {ok: true, value};
156
+ }
157
+
158
+ function invalid(command, message, nextActions = []) {
159
+ return failure(command, cliError("INVALID_ARGUMENT", {path: command, message, nextActions}));
160
+ }
@@ -56,23 +56,23 @@ export function writeConfigAtomically(path, config, options = {}) {
56
56
  }
57
57
  }
58
58
 
59
- export function resolveConfig(flags = {}, env = process.env, fileConfig = readConfig()) {
60
- const baseUrl = firstNonEmpty(flags.baseUrl, env.FINEBI_BASE_URL, fileConfig.baseUrl);
61
- const sameServer = sameBaseUrl(baseUrl, fileConfig.baseUrl);
62
- const explicitToken = firstNonEmpty(flags.token, env.FINEBI_AUTH_TOKEN);
63
- const savedAuth = sameServer ? fileConfig.auth : undefined;
64
- const token = firstNonEmpty(explicitToken, savedAuth?.token, sameServer ? fileConfig.token : undefined);
59
+ export function resolveConfig(flags = {}, env = process.env, fileConfig = readConfig()) {
60
+ const baseUrl = firstNonEmpty(flags.baseUrl, env.FINEBI_BASE_URL, fileConfig.baseUrl);
61
+ const sameServer = sameBaseUrl(baseUrl, fileConfig.baseUrl);
62
+ const explicitToken = firstNonEmpty(flags.token, env.FINEBI_AUTH_TOKEN);
63
+ const savedAuth = sameServer ? fileConfig.auth : undefined;
64
+ const token = firstNonEmpty(explicitToken, savedAuth?.token, sameServer ? fileConfig.token : undefined);
65
65
  return normalizeConfig({
66
66
  ...fileConfig,
67
- baseUrl,
67
+ baseUrl,
68
68
  timeoutMs: parsePositiveInt(firstNonEmpty(flags.timeoutMs, env.FINEBI_TIMEOUT_MS), fileConfig.timeoutMs),
69
69
  capabilityTtlMs: parsePositiveInt(
70
70
  firstNonEmpty(flags.capabilityTtlMs, env.FINEBI_CAPABILITY_TTL_MS),
71
71
  fileConfig.capabilityTtlMs,
72
72
  ),
73
73
  auth: {
74
- ...(explicitToken ? {} : savedAuth),
75
- token,
74
+ ...(explicitToken ? {} : savedAuth),
75
+ token,
76
76
  },
77
77
  });
78
78
  }
@@ -81,10 +81,10 @@ export function updateConfigValue(config, key, value, now = () => new Date().toI
81
81
  const next = normalizeConfig(config);
82
82
  switch (key) {
83
83
  case "base-url":
84
- if (next.baseUrl !== value) {
85
- next.capabilityCacheInvalidatedAt = now();
86
- delete next.auth;
87
- delete next.token;
84
+ if (next.baseUrl !== value) {
85
+ next.capabilityCacheInvalidatedAt = now();
86
+ delete next.auth;
87
+ delete next.token;
88
88
  }
89
89
  next.baseUrl = value;
90
90
  return next;
@@ -128,7 +128,7 @@ function defaultConfig() {
128
128
  };
129
129
  }
130
130
 
131
- export function normalizeConfig(config) {
131
+ export function normalizeConfig(config) {
132
132
  return {
133
133
  ...config,
134
134
  timeoutMs: parsePositiveInt(config.timeoutMs, DEFAULT_TIMEOUT_MS),
@@ -136,14 +136,14 @@ export function normalizeConfig(config) {
136
136
  };
137
137
  }
138
138
 
139
- function sanitizeConfig(config) {
140
- if (config.schemaVersion === 2) {
141
- return {
142
- schemaVersion: 2,
143
- currentProfile: config.currentProfile,
144
- profiles: Object.fromEntries(Object.entries(config.profiles).map(([name, profile]) => [name, sanitizeConfig(normalizeConfig(profile))])),
145
- };
146
- }
139
+ function sanitizeConfig(config) {
140
+ if (config.schemaVersion === 2) {
141
+ return {
142
+ schemaVersion: 2,
143
+ currentProfile: config.currentProfile,
144
+ profiles: Object.fromEntries(Object.entries(config.profiles).map(([name, profile]) => [name, sanitizeConfig(normalizeConfig(profile))])),
145
+ };
146
+ }
147
147
  const { token: _legacyToken, ...rest } = config;
148
148
  const auth = config.auth
149
149
  ? {
@@ -166,11 +166,11 @@ function sanitizeConfig(config) {
166
166
  ...rest,
167
167
  auth,
168
168
  };
169
- }
170
-
171
- export function sameBaseUrl(left, right) {
172
- return (left || "").replace(/\/+$/, "") === (right || "").replace(/\/+$/, "");
173
- }
169
+ }
170
+
171
+ export function sameBaseUrl(left, right) {
172
+ return (left || "").replace(/\/+$/, "") === (right || "").replace(/\/+$/, "");
173
+ }
174
174
 
175
175
  function firstNonEmpty(...values) {
176
176
  return values.find((value) => typeof value === "string" && value.trim().length > 0)?.trim();
@@ -1,12 +1,12 @@
1
1
  import {brandCommandValue, CLI_COMMAND_NAME, CLI_DISPLAY_NAME, commandLine} from "./branding.js";
2
2
  import {cliError} from "./errors.js";
3
- import {failure, success} from "./output.js";
4
-
5
- const GLOBAL_OPTIONS = [
6
- {name: "--profile", type: "string", description: "Environment name. Overrides FINEBI_PROFILE and the saved default; ordinary commands do not create environments or switch the default."},
7
- {name: "--base-url", type: "string", description: "Connection URL override. A different server never inherits saved credentials or capabilities."},
8
- {name: "--token", type: "string", description: "Temporary session token override; not persisted."},
9
- ];
3
+ import {failure, success} from "./output.js";
4
+
5
+ const GLOBAL_OPTIONS = [
6
+ {name: "--profile", type: "string", description: "Environment name. Overrides FINEBI_PROFILE and the saved default; ordinary commands do not create environments or switch the default."},
7
+ {name: "--base-url", type: "string", description: "Connection URL override. A different server never inherits saved credentials or capabilities."},
8
+ {name: "--token", type: "string", description: "Temporary session token override; not persisted."},
9
+ ];
10
10
 
11
11
  export function renderTopLevelHelp(manifest) {
12
12
  return success("help", {
@@ -14,8 +14,8 @@ export function renderTopLevelHelp(manifest) {
14
14
  name: resource.name,
15
15
  summary: resource.summary,
16
16
  })),
17
- dynamicCapabilitiesLoaded: Boolean(manifest),
18
- globalOptions: GLOBAL_OPTIONS,
17
+ dynamicCapabilitiesLoaded: Boolean(manifest),
18
+ globalOptions: GLOBAL_OPTIONS,
19
19
  }, { schemaVersion: manifest?.schemaVersion });
20
20
  }
21
21
 
@@ -24,7 +24,7 @@ export function renderBuiltInHelp(resourceName) {
24
24
  if (!resource) {
25
25
  return capabilityNotFound(resourceName);
26
26
  }
27
- return success("help", brandCommandValue({...resource, globalOptions: GLOBAL_OPTIONS}));
27
+ return success("help", brandCommandValue({...resource, globalOptions: GLOBAL_OPTIONS}));
28
28
  }
29
29
 
30
30
  export function renderBuiltInCommandHelp(resourceName, commandName) {
@@ -38,8 +38,8 @@ export function renderBuiltInCommandHelp(resourceName, commandName) {
38
38
  command: command.name,
39
39
  summary: command.summary,
40
40
  usage: command.usage,
41
- options: command.options || [],
42
- globalOptions: GLOBAL_OPTIONS,
41
+ options: command.options || [],
42
+ globalOptions: GLOBAL_OPTIONS,
43
43
  details: command.details || [],
44
44
  examples: command.examples || [],
45
45
  }));
@@ -52,8 +52,8 @@ export function renderResourceHelp(manifest, resourceName) {
52
52
  }
53
53
  return success("help", {
54
54
  resource: resource.name,
55
- summary: resource.summary,
56
- globalOptions: GLOBAL_OPTIONS,
55
+ summary: resource.summary,
56
+ globalOptions: GLOBAL_OPTIONS,
57
57
  commands: (resource.commands || []).map((command) => ({
58
58
  name: command.name,
59
59
  summary: command.summary,
@@ -72,8 +72,8 @@ export function renderCommandHelp(manifest, resourceName, commandName) {
72
72
  command: command.name,
73
73
  summary: command.summary,
74
74
  usage: usageFor(resource.name, command),
75
- options: commandOptionsFor(command),
76
- globalOptions: GLOBAL_OPTIONS,
75
+ options: commandOptionsFor(command),
76
+ globalOptions: GLOBAL_OPTIONS,
77
77
  display: command.display,
78
78
  requestSchema: command.requestSchema,
79
79
  responseSchema: command.responseSchema,
@@ -179,7 +179,7 @@ const BUILT_IN_RESOURCES = {
179
179
  },
180
180
  {
181
181
  name: "clear",
182
- summary: "Remove the selected environment's local configuration, credentials and capability cache; does not revoke server keys.",
182
+ summary: "Remove the selected environment's local configuration, credentials and capability cache; does not revoke server keys.",
183
183
  usage: "finebi-cli config clear",
184
184
  },
185
185
  ],
@@ -191,18 +191,18 @@ const BUILT_IN_RESOURCES = {
191
191
  {
192
192
  name: "login",
193
193
  summary: "Login with FineBI credentials or browser authorization and persist an Access Key.",
194
- usage: "finebi-cli auth login [--profile <name>] [--base-url <url>] [--browser] [--username <username>] [--from-env] [--password-stdin] [--no-use]",
195
- options: [
196
- { name: "--no-use", type: "boolean", description: "Save successful login without changing the default environment." },
194
+ usage: "finebi-cli auth login [--profile <name>] [--base-url <url>] [--browser] [--username <username>] [--from-env] [--password-stdin] [--no-use]",
195
+ options: [
196
+ { name: "--no-use", type: "boolean", description: "Save successful login without changing the default environment." },
197
197
  { name: "--username", type: "string", description: "FineBI username." },
198
198
  { name: "--from-env", type: "boolean", description: "Read base URL, username, and password from environment variables." },
199
199
  { name: "--password-stdin", type: "boolean", description: "Read password from standard input." },
200
200
  { name: "--browser", type: "boolean", description: "Authorize this CLI from a signed-in FineBI browser without sending a password." },
201
201
  ],
202
- details: [
203
- "Login creates the named environment if missing and selects it after successful authentication, unless --no-use is supplied.",
204
- "A new environment requires --base-url or FINEBI_BASE_URL; it never inherits another environment's address or identity.",
205
- "Failed authentication preserves saved environments and the default selection.",
202
+ details: [
203
+ "Login creates the named environment if missing and selects it after successful authentication, unless --no-use is supplied.",
204
+ "A new environment requires --base-url or FINEBI_BASE_URL; it never inherits another environment's address or identity.",
205
+ "Failed authentication preserves saved environments and the default selection.",
206
206
  "Environment variables for --from-env:",
207
207
  " FINEBI_BASE_URL",
208
208
  " FINEBI_USERNAME",
@@ -214,28 +214,28 @@ const BUILT_IN_RESOURCES = {
214
214
  "finebi-cli auth login --username <username>",
215
215
  "finebi-cli auth login --from-env",
216
216
  "echo <password> | finebi-cli auth login --username <username> --password-stdin",
217
- "finebi-cli auth login --browser",
218
- "finebi-cli auth login --profile test --base-url https://test.example.com/webroot/decision --browser",
217
+ "finebi-cli auth login --browser",
218
+ "finebi-cli auth login --profile test --base-url https://test.example.com/webroot/decision --browser",
219
219
  ],
220
220
  },
221
- {
222
- name: "list",
223
- summary: "List saved environments and mark the default; local metadata only, no credential validation or secret output.",
224
- usage: "finebi-cli auth list",
225
- },
226
- {
227
- name: "use",
228
- summary: "Select an existing environment as default without logging in.",
229
- usage: "finebi-cli auth use <name>",
230
- },
231
- {
232
- name: "remove",
233
- summary: "Delete one local environment and its capability cache; does not revoke server keys.",
234
- usage: "finebi-cli auth remove <name>",
235
- details: ["Use auth logout --profile <name> first to revoke its Access Key.", "Removing the default environment does not select another environment."],
236
- },
237
- {
238
- name: "info",
221
+ {
222
+ name: "list",
223
+ summary: "List saved environments and mark the default; local metadata only, no credential validation or secret output.",
224
+ usage: "finebi-cli auth list",
225
+ },
226
+ {
227
+ name: "use",
228
+ summary: "Select an existing environment as default without logging in.",
229
+ usage: "finebi-cli auth use <name>",
230
+ },
231
+ {
232
+ name: "remove",
233
+ summary: "Delete one local environment and its capability cache; does not revoke server keys.",
234
+ usage: "finebi-cli auth remove <name>",
235
+ details: ["Use auth logout --profile <name> first to revoke its Access Key.", "Removing the default environment does not select another environment."],
236
+ },
237
+ {
238
+ name: "info",
239
239
  summary: "Show stored authentication metadata with secrets masked.",
240
240
  usage: "finebi-cli auth info",
241
241
  },
@@ -403,10 +403,10 @@ function renderTopLevelHelpText(data) {
403
403
  "",
404
404
  "Use --help or -h for command details.",
405
405
  "Use --json with --help for structured output.",
406
- "Output options: --select <paths> / --max-chars <n> / --save <path> for large responses.",
407
- "Environment: --profile <name> > FINEBI_PROFILE > saved default. Global options may precede or follow the command.",
408
- );
409
- lines.push("", "Global Options:", ...formatOptionRows(data.globalOptions || []));
406
+ "Output options: --select <paths> / --max-chars <n> / --save <path> for large responses.",
407
+ "Environment: --profile <name> > FINEBI_PROFILE > saved default. Global options may precede or follow the command.",
408
+ );
409
+ lines.push("", "Global Options:", ...formatOptionRows(data.globalOptions || []));
410
410
  return `${lines.join("\n")}\n`;
411
411
  }
412
412
 
@@ -422,8 +422,8 @@ function renderResourceHelpText(data) {
422
422
  "",
423
423
  "Commands:",
424
424
  ];
425
- lines.push(...formatRows(data.commands || [], "name", "summary"));
426
- lines.push("", "Global Options:", ...formatOptionRows(data.globalOptions || []));
425
+ lines.push(...formatRows(data.commands || [], "name", "summary"));
426
+ lines.push("", "Global Options:", ...formatOptionRows(data.globalOptions || []));
427
427
  lines.push("", `Use ${commandLine(data.resource, "<command>", "--help")} for command details.`);
428
428
  return `${lines.join("\n")}\n`;
429
429
  }
@@ -438,13 +438,13 @@ function renderCommandHelpText(data) {
438
438
  "Usage:",
439
439
  ` ${data.usage}`,
440
440
  ];
441
- if ((data.options || []).length > 0) {
441
+ if ((data.options || []).length > 0) {
442
442
  lines.push("", "Options:");
443
443
  lines.push(...formatOptionRows(data.options));
444
- }
445
- if ((data.globalOptions || []).length > 0) {
446
- lines.push("", "Global Options:", ...formatOptionRows(data.globalOptions));
447
- }
444
+ }
445
+ if ((data.globalOptions || []).length > 0) {
446
+ lines.push("", "Global Options:", ...formatOptionRows(data.globalOptions));
447
+ }
448
448
  if ((data.details || []).length > 0) {
449
449
  lines.push("", ...data.details);
450
450
  }