@fireberry/cli 0.0.5-beta.9 → 0.2.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.
package/CLAUDE.md ADDED
@@ -0,0 +1,156 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Overview
6
+
7
+ `@fireberry/cli` is a command-line tool for managing Fireberry applications. It allows developers to initialize credentials, create new apps from templates, push components to the Fireberry platform, and install apps on Fireberry accounts.
8
+
9
+ ## Development Commands
10
+
11
+ ### Build and Development
12
+
13
+ ```bash
14
+ npm run build # Compile TypeScript to dist/
15
+ npm run build:dev # Clean, build, link globally, and make executable
16
+ npm run dev # Watch mode compilation
17
+ npm run clean # Remove dist/ directory
18
+ ```
19
+
20
+ ### Testing
21
+
22
+ ```bash
23
+ npm test # Run smoke test (verifies CLI --help works)
24
+ ```
25
+
26
+ ### Local Development
27
+
28
+ ```bash
29
+ npm run build:dev # Build and link CLI locally
30
+ fireberry --help # Test the linked CLI
31
+ ```
32
+
33
+ ### Publishing
34
+
35
+ ```bash
36
+ npm run publish:beta # Version bump (beta) and publish to npm with beta tag
37
+ npm run publish:prod # Publish to npm with latest tag
38
+ ```
39
+
40
+ ## Architecture
41
+
42
+ ### Module System
43
+
44
+ - **Type**: ESM (ES Modules) with `"type": "module"` in package.json
45
+ - **Module Resolution**: NodeNext
46
+ - **Import Extensions**: All local imports must use `.js` extension (e.g., `import { foo } from "./bar.js"`)
47
+ - **JSON Imports**: Use `with { type: "json" }` syntax (e.g., `import packageJson from "../../package.json" with { type: "json" }`)
48
+
49
+ ### CLI Entry Point
50
+
51
+ - **Binary**: [dist/bin/fireberry.js](dist/bin/fireberry.js) (generated from [src/bin/fireberry.ts](src/bin/fireberry.ts))
52
+ - **Framework**: Commander.js for command parsing and routing
53
+ - **Error Handling**: Global error handler in [src/bin/fireberry.ts](src/bin/fireberry.ts) catches and formats errors
54
+
55
+ ### Command Structure
56
+
57
+ Each CLI command is in [src/commands/](src/commands/):
58
+
59
+ - **init**: Stores Fireberry API token in local config using `env-paths`
60
+ - **create**: Creates new Fireberry app from templates with generated UUIDs
61
+ - **push**: Validates manifest, zips components, uploads to Fireberry API
62
+ - **install**: Installs app on user's Fireberry account
63
+ - **delete**: Deletes app from Fireberry platform (requires confirmation)
64
+
65
+ ### Configuration System
66
+
67
+ - **User Config**: Stored via `env-paths` ("Fireberry CLI") in OS-specific config directory
68
+ - **Config File**: `config.json` with `{ apiToken, createdAt }`
69
+ - **Environment**: [src/config/env.ts](src/config/env.ts) loads `.env` file from project root using dotenv
70
+
71
+ ### API Layer ([src/api/](src/api/))
72
+
73
+ - **axios.ts**: Axios instance with automatic token injection via interceptors
74
+ - **API URL Logic**:
75
+ - Beta versions (version contains "beta") → Uses `FIREBERRY_STAGING_URL` environment variable
76
+ - Production versions → `FIREBERRY_API_URL` or `https://api.fireberry.com/api/v3`
77
+ - **Authentication**: Token passed via `tokenId` header (read from config in interceptor)
78
+ - **requests.ts**: API endpoints (`createApp`, `pushComponents`, `installApp`, `deleteApp`)
79
+ - **types.ts**: TypeScript interfaces for API requests/responses
80
+
81
+ ### Component System
82
+
83
+ Component utilities in [src/utils/components.utils.ts](src/utils/components.utils.ts):
84
+
85
+ - **Manifest Parsing**: YAML manifest loaded from `manifest.yml` in current directory
86
+ - **Component Validation**: Checks paths exist, components are unique, validates required settings per component type
87
+ - **Component Zipping**: Creates tar.gz archives of component builds (directories or single files)
88
+ - **Component Types** ([src/constants/component-types.ts](src/constants/component-types.ts)):
89
+ - **record**: Record page component with required settings: `iconName` (string), `iconColor` (string), `objectType` (number)
90
+ - **global-menu**: Global menu component with required setting: `displayName` (string)
91
+ - **side-menu**: Side menu component with required settings: `icon` (string), `width` (string: "S" | "M" | "L")
92
+ - **Manifest Structure**:
93
+ ```yaml
94
+ app:
95
+ id: "uuid"
96
+ name: "App Name"
97
+ components:
98
+ - type: record | global-menu | side-menu
99
+ title: component-name
100
+ id: "uuid"
101
+ path: relative/path/to/build
102
+ settings:
103
+ # For record type:
104
+ iconName: "icon-name"
105
+ iconColor: "#hexcolor"
106
+ objectType: 1
107
+ # For global-menu type:
108
+ displayName: "Menu Name"
109
+ # For side-menu type:
110
+ icon: "icon-name"
111
+ width: "S" | "M" | "L"
112
+ ```
113
+
114
+ ### Templates ([src/templates/](src/templates/))
115
+
116
+ Templates use mustache-style placeholders (`{{appName}}`, `{{appId}}`, `{{componentId}}`):
117
+
118
+ - **manifest.yml**: Default manifest with single component
119
+ - **index.html**: Basic HTML template
120
+
121
+ ## Key Patterns
122
+
123
+ ### Error Handling
124
+
125
+ - Commands throw errors which are caught by the global handler in [src/bin/fireberry.ts:47](src/bin/fireberry.ts#L47)
126
+ - Errors are formatted with chalk.red and prefixed with "Error:" if not already present
127
+ - API errors map HTTP status codes to user-friendly messages
128
+
129
+ ### User Feedback
130
+
131
+ - Use `ora` spinners for long-running operations (start → succeed/fail)
132
+ - Use `chalk` for colored output (cyan for highlights, gray for details, yellow for warnings)
133
+ - Use `inquirer` for interactive prompts when arguments are missing
134
+
135
+ ### File Operations
136
+
137
+ - Use `fs-extra` for all file operations (promisified, ensures directories exist)
138
+ - Check `fs.pathExists()` before operations
139
+ - Use `process.cwd()` as base for relative paths in user projects
140
+
141
+ ### Component Path Resolution
142
+
143
+ Component paths in manifest.yml are relative to the current working directory, not the CLI installation directory. Example: `path: static/comp/build` resolves to `process.cwd() + "/static/comp/build"`.
144
+
145
+ ## Important Notes
146
+
147
+ - **Manifest Required**: `push`, `install`, and `delete` commands must be run from a directory containing `manifest.yml`
148
+ - **Token Required**: Most commands require prior `init` to store API token
149
+ - **Component IDs**: Must be unique UUIDs within a manifest
150
+ - **Component Settings Validation**: Each component type has required settings that are validated during `push`:
151
+ - `record`: Must have `iconName`, `iconColor`, and `objectType`
152
+ - `global-menu`: Must have `displayName`
153
+ - `side-menu`: Must have `icon` and `width` (S/M/L)
154
+ - **Build Zipping**: Single files are wrapped in a directory before tar.gz creation
155
+ - **Template Location**: Templates are resolved from `src/templates/` at compile time, copied to `dist/templates/`
156
+ - **Delete Safety**: Delete command requires user confirmation before executing (cannot be undone)
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Fireberry LTD
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/api/axios.js CHANGED
@@ -46,7 +46,7 @@ export async function sendApiRequest(config) {
46
46
  let errorMessage;
47
47
  switch (status) {
48
48
  case 401:
49
- console.log(error.response?.data?.message || error.message);
49
+ console.log(error.response?.data?.message || error.message); // TODO: remove this
50
50
  errorMessage = "Unauthorized user.";
51
51
  break;
52
52
  case 500:
@@ -1,4 +1,6 @@
1
1
  import "../config/env.js";
2
- import type { CreateAppRequest, ZippedComponent } from "./types.js";
3
- export declare const createApp: (data: CreateAppRequest) => Promise<void>;
4
- export declare const pushComponents: (appId: string, components: ZippedComponent[]) => Promise<void>;
2
+ import type { Manifest, ZippedComponent } from "./types.js";
3
+ export declare const createApp: (manifest: Manifest) => Promise<void>;
4
+ export declare const pushComponents: (appId: string, components: ZippedComponent[], manifest: Manifest) => Promise<void>;
5
+ export declare const installApp: (manifest: Manifest) => Promise<void>;
6
+ export declare const deleteApp: (manifest: Manifest) => Promise<void>;
@@ -1,18 +1,37 @@
1
1
  import "../config/env.js";
2
+ import { BASE_SERVICE_URL } from "../constants/component-types.js";
2
3
  import { api } from "./axios.js";
3
- export const createApp = async (data) => {
4
- const url = "/services/developer/create";
4
+ export const createApp = async (manifest) => {
5
+ const url = `${BASE_SERVICE_URL}/create`;
5
6
  try {
6
- await api.post(url, data);
7
+ await api.post(url, { manifest });
7
8
  }
8
9
  catch (error) {
9
10
  throw new Error(error instanceof Error ? error.message : "Unknown error");
10
11
  }
11
12
  };
12
- export const pushComponents = async (appId, components) => {
13
- const url = `/services/developer/push`;
13
+ export const pushComponents = async (appId, components, manifest) => {
14
+ const url = `${BASE_SERVICE_URL}/push`;
14
15
  try {
15
- await api.post(url, { appId, components });
16
+ await api.post(url, { appId, components, manifest });
17
+ }
18
+ catch (error) {
19
+ throw new Error(error instanceof Error ? error.message : "Unknown error");
20
+ }
21
+ };
22
+ export const installApp = async (manifest) => {
23
+ const url = `${BASE_SERVICE_URL}/install`;
24
+ try {
25
+ await api.post(url, { manifest }, { timeout: 300000 }); // 5 minutes
26
+ }
27
+ catch (error) {
28
+ throw new Error(error instanceof Error ? error.message : "Unknown error");
29
+ }
30
+ };
31
+ export const deleteApp = async (manifest) => {
32
+ const url = `${BASE_SERVICE_URL}/delete`;
33
+ try {
34
+ await api.delete(url, { manifest });
16
35
  }
17
36
  catch (error) {
18
37
  throw new Error(error instanceof Error ? error.message : "Unknown error");
@@ -1,5 +1,8 @@
1
+ import { COMPONENT_TYPE } from "../constants/component-types.js";
2
+ import { HeightOption } from "../constants/height-options.js";
1
3
  export interface CreateAppRequest {
2
4
  appId: string;
5
+ componentId: string;
3
6
  }
4
7
  export interface ApiError {
5
8
  message: string;
@@ -16,10 +19,38 @@ interface ManifestApp {
16
19
  name: string;
17
20
  description?: string;
18
21
  }
19
- export interface ManifestComponent {
22
+ export interface RecordComponentSettings {
23
+ iconName: string;
24
+ iconColor: string;
25
+ objectType: number;
26
+ height: HeightOption;
27
+ }
28
+ export interface GlobalMenuComponentSettings {
29
+ displayName: string;
30
+ }
31
+ export interface SideMenuComponentSettings {
32
+ icon: string;
33
+ width: "S" | "M" | "L";
34
+ }
35
+ export type ComponentSettings = RecordComponentSettings | GlobalMenuComponentSettings | SideMenuComponentSettings;
36
+ export interface BaseManifestComponent {
37
+ title: string;
38
+ id: string;
39
+ path: string;
40
+ }
41
+ export interface RecordComponent extends BaseManifestComponent {
42
+ type: typeof COMPONENT_TYPE.RECORD;
43
+ settings: RecordComponentSettings;
44
+ }
45
+ export interface GlobalMenuComponent extends BaseManifestComponent {
46
+ type: typeof COMPONENT_TYPE.GLOBAL_MENU;
47
+ settings: GlobalMenuComponentSettings;
48
+ }
49
+ export type ManifestComponent = RecordComponent | GlobalMenuComponent;
50
+ export interface UntypedManifestComponent {
20
51
  type: string;
21
52
  title: string;
22
- key: string;
53
+ id: string;
23
54
  path: string;
24
55
  settings?: Record<string, unknown>;
25
56
  }
@@ -29,7 +60,7 @@ export interface Manifest {
29
60
  }
30
61
  export interface ZippedComponent {
31
62
  title: string;
32
- key: string;
63
+ id: string;
33
64
  build: Buffer;
34
65
  }
35
66
  export {};
@@ -5,6 +5,8 @@ import { runInit } from "../commands/init.js";
5
5
  import { runCreate } from "../commands/create.js";
6
6
  import packageJson from "../../package.json" with { type: "json" };
7
7
  import { runPush } from "../commands/push.js";
8
+ import { runInstall } from "../commands/install.js";
9
+ import { runDelete } from "../commands/delete.js";
8
10
  const program = new Command();
9
11
  program
10
12
  .name("fireberry")
@@ -31,6 +33,17 @@ program
31
33
  .action(async () => {
32
34
  await runPush();
33
35
  });
36
+ program.command("install")
37
+ .description("Install app on your Fireberry account")
38
+ .action(async () => {
39
+ await runInstall();
40
+ });
41
+ program
42
+ .command("delete")
43
+ .description("Delete a Fireberry app")
44
+ .action(async () => {
45
+ await runDelete();
46
+ });
34
47
  program.parseAsync(process.argv).catch((err) => {
35
48
  const errorMessage = err instanceof Error
36
49
  ? err.message
@@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url";
6
6
  import ora from "ora";
7
7
  import chalk from "chalk";
8
8
  import { createApp } from "../api/requests.js";
9
+ import { getManifest } from "../utils/components.utils.js";
9
10
  const __filename = fileURLToPath(import.meta.url);
10
11
  const __dirname = path.dirname(__filename);
11
12
  function slugifyName(name) {
@@ -28,23 +29,26 @@ export async function runCreate({ name }) {
28
29
  }
29
30
  const slug = slugifyName(appName);
30
31
  const appId = uuidv4();
32
+ const componentId = uuidv4();
31
33
  const appDir = path.resolve(process.cwd(), slug);
32
34
  if (await fs.pathExists(appDir)) {
33
35
  throw new Error(`Already exists. ${chalk.yellow(slug)}`);
34
36
  }
35
37
  const spinner = ora(`Creating app "${chalk.cyan(appName)}"...`).start();
36
38
  try {
37
- await createApp({ appId });
38
39
  await fs.ensureDir(appDir);
39
40
  const templatesDir = path.join(__dirname, "..", "..", "src", "templates");
40
41
  const manifestTemplate = await fs.readFile(path.join(templatesDir, "manifest.yml"), "utf-8");
41
42
  const htmlTemplate = await fs.readFile(path.join(templatesDir, "index.html"), "utf-8");
42
43
  const manifestContent = manifestTemplate
43
44
  .replace(/{{appName}}/g, appName)
44
- .replace(/{{appId}}/g, appId);
45
+ .replace(/{{appId}}/g, appId)
46
+ .replace(/{{componentId}}/g, componentId);
45
47
  const htmlContent = htmlTemplate.replace(/{{appName}}/g, appName);
46
48
  await fs.writeFile(path.join(appDir, "manifest.yml"), manifestContent);
47
49
  await fs.writeFile(path.join(appDir, "index.html"), htmlContent);
50
+ const manifest = await getManifest(appDir);
51
+ await createApp(manifest);
48
52
  spinner.succeed(`Successfully created "${chalk.cyan(appName)}" app!`);
49
53
  console.log(chalk.gray(`📁 Location: ${appDir}`));
50
54
  console.log(chalk.gray(`App ID: ${appId}`));
@@ -0,0 +1 @@
1
+ export declare function runDelete(): Promise<void>;
@@ -0,0 +1,31 @@
1
+ import ora from "ora";
2
+ import chalk from "chalk";
3
+ import inquirer from "inquirer";
4
+ import { deleteApp } from "../api/requests.js";
5
+ import { getManifest } from "../utils/components.utils.js";
6
+ export async function runDelete() {
7
+ const spinner = ora("Loading manifest...").start();
8
+ try {
9
+ const manifest = await getManifest();
10
+ spinner.stop();
11
+ const confirmAnswer = await inquirer.prompt([
12
+ {
13
+ type: "confirm",
14
+ name: "confirm",
15
+ message: `Are you sure you want to delete app ${chalk.yellow(manifest.app.name)} (${chalk.gray(manifest.app.id)})? This action cannot be undone.`,
16
+ default: false,
17
+ },
18
+ ]);
19
+ if (!confirmAnswer.confirm) {
20
+ console.log(chalk.gray("Delete operation cancelled."));
21
+ return;
22
+ }
23
+ spinner.start(`Deleting app "${chalk.cyan(manifest.app.name)}"...`);
24
+ await deleteApp(manifest);
25
+ spinner.succeed(`Successfully deleted app "${chalk.cyan(manifest.app.name)}"`);
26
+ }
27
+ catch (error) {
28
+ spinner.fail("Failed to delete app");
29
+ throw error;
30
+ }
31
+ }
@@ -0,0 +1 @@
1
+ export declare function runInstall(): Promise<void>;
@@ -0,0 +1,17 @@
1
+ import ora from "ora";
2
+ import { installApp } from "../api/requests.js";
3
+ import { getManifest, validateManifestComponents, } from "../utils/components.utils.js";
4
+ export async function runInstall() {
5
+ const spinner = ora("Loading manifest...").start();
6
+ try {
7
+ const manifest = await getManifest();
8
+ await validateManifestComponents(manifest);
9
+ spinner.start("Installing app on Fireberry...");
10
+ await installApp(manifest);
11
+ spinner.succeed("App installed successfully");
12
+ }
13
+ catch (error) {
14
+ spinner.fail("Installation failed");
15
+ throw error;
16
+ }
17
+ }
@@ -15,10 +15,10 @@ export async function runPush() {
15
15
  console.log(chalk.cyan("\nComponents ready to push:"));
16
16
  zippedComponents.forEach((comp, idx) => {
17
17
  const sizeKB = (comp.build.length / 1024).toFixed(2);
18
- console.log(chalk.gray(` ${idx + 1}. ${comp.title} (${comp.key}) - ${sizeKB} KB`));
18
+ console.log(chalk.gray(` ${idx + 1}. ${comp.title} (${comp.id}) - ${sizeKB} KB`));
19
19
  });
20
20
  spinner.start("Uploading to Fireberry...");
21
- await pushComponents(manifest.app.id, zippedComponents);
21
+ await pushComponents(manifest.app.id, zippedComponents, manifest);
22
22
  spinner.succeed("Components pushed successfully");
23
23
  }
24
24
  else {
@@ -0,0 +1,7 @@
1
+ export declare const COMPONENT_TYPE: {
2
+ readonly RECORD: "record";
3
+ readonly GLOBAL_MENU: "global-menu";
4
+ readonly SIDE_MENU: "side-menu";
5
+ };
6
+ export type ComponentType = (typeof COMPONENT_TYPE)[keyof typeof COMPONENT_TYPE];
7
+ export declare const BASE_SERVICE_URL = "/services/developer/app";
@@ -0,0 +1,6 @@
1
+ export const COMPONENT_TYPE = {
2
+ RECORD: "record",
3
+ GLOBAL_MENU: "global-menu",
4
+ SIDE_MENU: "side-menu",
5
+ };
6
+ export const BASE_SERVICE_URL = "/services/developer/app";
@@ -0,0 +1,2 @@
1
+ export declare const HEIGHT_OPTIONS: readonly ["S", "M", "L", "XL"];
2
+ export type HeightOption = (typeof HEIGHT_OPTIONS)[number];
@@ -0,0 +1 @@
1
+ export const HEIGHT_OPTIONS = ["S", "M", "L", "XL"];
@@ -1,5 +1,6 @@
1
- import { Manifest, ManifestComponent, ZippedComponent } from "../api/types.js";
2
- export declare const getManifest: () => Promise<Manifest>;
3
- export declare const validateComponentBuild: (componentPath: string, comp: ManifestComponent) => Promise<void>;
1
+ import { Manifest, ZippedComponent, UntypedManifestComponent } from "../api/types.js";
2
+ export declare const getManifest: (basePath?: string) => Promise<Manifest>;
3
+ export declare const validateComponentBuild: (componentPath: string, comp: UntypedManifestComponent) => Promise<void>;
4
4
  export declare const zipComponentBuild: (componentPath: string, title: string) => Promise<Buffer>;
5
+ export declare const validateManifestComponents: (manifest: Manifest) => Promise<void>;
5
6
  export declare const handleComponents: (manifest: Manifest) => Promise<ZippedComponent[]>;
@@ -4,10 +4,13 @@ import yaml from "js-yaml";
4
4
  import chalk from "chalk";
5
5
  import * as tar from "tar";
6
6
  import os from "node:os";
7
- export const getManifest = async () => {
8
- const manifestPath = path.join(process.cwd(), "manifest.yml");
7
+ import { COMPONENT_TYPE } from "../constants/component-types.js";
8
+ import { HEIGHT_OPTIONS } from "../constants/height-options.js";
9
+ export const getManifest = async (basePath) => {
10
+ const manifestPath = path.join(basePath || process.cwd(), "manifest.yml");
11
+ const searchDir = basePath || process.cwd();
9
12
  if (!(await fs.pathExists(manifestPath))) {
10
- throw new Error(`No manifest.yml found at ${chalk.yellow(process.cwd())}.\n` +
13
+ throw new Error(`No manifest.yml found at ${chalk.yellow(searchDir)}.\n` +
11
14
  `Please run this command from your Fireberry app directory.`);
12
15
  }
13
16
  const manifestContent = await fs.readFile(manifestPath, "utf-8");
@@ -20,6 +23,82 @@ export const getManifest = async () => {
20
23
  }
21
24
  return manifest;
22
25
  };
26
+ const validateRecordComponentSettings = (comp) => {
27
+ const settings = comp.settings;
28
+ if (!settings) {
29
+ throw new Error(`Component "${comp.title}" (type: ${COMPONENT_TYPE.RECORD}) is missing required settings`);
30
+ }
31
+ const requiredFields = [
32
+ "iconName",
33
+ "iconColor",
34
+ "objectType",
35
+ "height",
36
+ ];
37
+ for (const fieldName of requiredFields) {
38
+ if (settings[fieldName] === undefined || settings[fieldName] === null) {
39
+ throw new Error(`Component "${comp.title}" (type: ${COMPONENT_TYPE.RECORD}) is missing required setting: ${fieldName}`);
40
+ }
41
+ }
42
+ if (typeof settings.iconName !== "string") {
43
+ throw new Error(`Component "${comp.title}" (type: ${COMPONENT_TYPE.RECORD}) setting "iconName" must be a string`);
44
+ }
45
+ if (typeof settings.iconColor !== "string") {
46
+ throw new Error(`Component "${comp.title}" (type: ${COMPONENT_TYPE.RECORD}) setting "iconColor" must be a string`);
47
+ }
48
+ if (typeof settings.objectType !== "number") {
49
+ throw new Error(`Component "${comp.title}" (type: ${COMPONENT_TYPE.RECORD}) setting "objectType" must be a number`);
50
+ }
51
+ if (!settings.height) {
52
+ throw new Error(`Component "${comp.title}" (type: ${COMPONENT_TYPE.RECORD}) setting "height" must be one of: ${HEIGHT_OPTIONS.join(" | ")}`);
53
+ }
54
+ if (!HEIGHT_OPTIONS.includes(settings.height)) {
55
+ throw new Error(`Component "${comp.title}" (type: ${COMPONENT_TYPE.RECORD}) setting "height" must be one of: ${HEIGHT_OPTIONS.join(" | ")}`);
56
+ }
57
+ };
58
+ const validateGlobalMenuComponentSettings = (comp) => {
59
+ const settings = comp.settings;
60
+ if (!settings) {
61
+ throw new Error(`Component "${comp.title}" (type: ${COMPONENT_TYPE.GLOBAL_MENU}) is missing required settings`);
62
+ }
63
+ if (!settings.displayName) {
64
+ throw new Error(`Component "${comp.title}" (type: ${COMPONENT_TYPE.GLOBAL_MENU}) is missing required setting: displayName`);
65
+ }
66
+ if (typeof settings.displayName !== "string") {
67
+ throw new Error(`Component "${comp.title}" (type: ${COMPONENT_TYPE.GLOBAL_MENU}) setting "displayName" must be a string`);
68
+ }
69
+ };
70
+ const validateSideMenuComponentSettings = (comp) => {
71
+ const settings = comp.settings;
72
+ if (!settings) {
73
+ throw new Error(`Component "${comp.title}" (type: ${COMPONENT_TYPE.SIDE_MENU}) is missing required settings`);
74
+ }
75
+ if (!settings.icon) {
76
+ throw new Error(`Component "${comp.title}" (type: ${COMPONENT_TYPE.SIDE_MENU}) setting "icon" must be a string`);
77
+ }
78
+ if (!settings.width) {
79
+ throw new Error(`Component "${comp.title}" (type: ${COMPONENT_TYPE.SIDE_MENU}) setting "width" must be a S | M | L`);
80
+ }
81
+ if (settings.width !== "S" &&
82
+ settings.width !== "M" &&
83
+ settings.width !== "L") {
84
+ throw new Error(`Component "${comp.title}" (type: ${COMPONENT_TYPE.SIDE_MENU}) setting "width" must be a S | M | L`);
85
+ }
86
+ };
87
+ const validateComponentSettings = (comp) => {
88
+ switch (comp.type) {
89
+ case COMPONENT_TYPE.RECORD:
90
+ validateRecordComponentSettings(comp);
91
+ break;
92
+ case COMPONENT_TYPE.GLOBAL_MENU:
93
+ validateGlobalMenuComponentSettings(comp);
94
+ break;
95
+ case COMPONENT_TYPE.SIDE_MENU:
96
+ validateSideMenuComponentSettings(comp);
97
+ break;
98
+ default:
99
+ throw new Error(`Component "${comp.title}" has unsupported type: ${comp.type}. Supported types: ${COMPONENT_TYPE.RECORD}, ${COMPONENT_TYPE.GLOBAL_MENU}`);
100
+ }
101
+ };
23
102
  export const validateComponentBuild = async (componentPath, comp) => {
24
103
  if (!(await fs.pathExists(componentPath))) {
25
104
  throw new Error(`Component "${comp.title}" path does not exist: ${chalk.yellow(componentPath)}\n` + `Make sure the path in manifest.yml is correct.`);
@@ -28,9 +107,10 @@ export const validateComponentBuild = async (componentPath, comp) => {
28
107
  if (stats.isDirectory()) {
29
108
  const files = await fs.readdir(componentPath);
30
109
  if (files.length === 0) {
31
- throw new Error(`Component <${comp.key}> at: /${comp.path} not found`);
110
+ throw new Error(`Component <${comp.id}> at: /${comp.path} not found`);
32
111
  }
33
112
  }
113
+ validateComponentSettings(comp);
34
114
  };
35
115
  export const zipComponentBuild = async (componentPath, title) => {
36
116
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "fireberry-"));
@@ -64,23 +144,30 @@ export const zipComponentBuild = async (componentPath, title) => {
64
144
  throw error;
65
145
  }
66
146
  };
67
- export const handleComponents = async (manifest) => {
147
+ export const validateManifestComponents = async (manifest) => {
68
148
  const components = manifest.components;
69
149
  if (!components || components.length === 0) {
70
- return [];
150
+ throw new Error("No components found in manifest");
71
151
  }
72
- const keys = components.map((comp) => comp.key);
73
- if (new Set(keys).size !== keys.length) {
74
- throw new Error("All component keys must be unique");
152
+ const ids = components.map((comp) => comp.id);
153
+ if (new Set(ids).size !== ids.length) {
154
+ throw new Error("All component ids must be unique");
75
155
  }
76
- const zippedComponents = [];
77
156
  for (const comp of components) {
78
157
  const componentPath = path.join(process.cwd(), comp.path);
79
158
  await validateComponentBuild(componentPath, comp);
159
+ }
160
+ };
161
+ export const handleComponents = async (manifest) => {
162
+ await validateManifestComponents(manifest);
163
+ const components = manifest.components;
164
+ const zippedComponents = [];
165
+ for (const comp of components) {
166
+ const componentPath = path.join(process.cwd(), comp.path);
80
167
  const buildBuffer = await zipComponentBuild(componentPath, comp.title);
81
168
  zippedComponents.push({
82
169
  title: comp.title,
83
- key: comp.key,
170
+ id: comp.id,
84
171
  build: buildBuffer,
85
172
  });
86
173
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fireberry/cli",
3
- "version": "0.0.5-beta.9",
3
+ "version": "0.2.0",
4
4
  "description": "Fireberry CLI tool",
5
5
  "type": "module",
6
6
  "author": "",
package/src/api/axios.ts CHANGED
@@ -59,7 +59,7 @@ export async function sendApiRequest<T = any>(
59
59
 
60
60
  switch (status) {
61
61
  case 401:
62
- console.log(error.response?.data?.message || error.message);
62
+ console.log(error.response?.data?.message || error.message); // TODO: remove this
63
63
  errorMessage = "Unauthorized user.";
64
64
  break;
65
65
  case 500:
@@ -1,11 +1,12 @@
1
1
  import "../config/env.js";
2
+ import { BASE_SERVICE_URL } from "../constants/component-types.js";
2
3
  import { api } from "./axios.js";
3
- import type { CreateAppRequest, ZippedComponent } from "./types.js";
4
+ import type { Manifest, ZippedComponent } from "./types.js";
4
5
 
5
- export const createApp = async (data: CreateAppRequest): Promise<void> => {
6
- const url = "/services/developer/create";
6
+ export const createApp = async (manifest: Manifest): Promise<void> => {
7
+ const url = `${BASE_SERVICE_URL}/create`;
7
8
  try {
8
- await api.post<void>(url, data);
9
+ await api.post<void>(url, { manifest });
9
10
  } catch (error) {
10
11
  throw new Error(error instanceof Error ? error.message : "Unknown error");
11
12
  }
@@ -13,11 +14,30 @@ export const createApp = async (data: CreateAppRequest): Promise<void> => {
13
14
 
14
15
  export const pushComponents = async (
15
16
  appId: string,
16
- components: ZippedComponent[]
17
+ components: ZippedComponent[],
18
+ manifest: Manifest
17
19
  ): Promise<void> => {
18
- const url = `/services/developer/push`;
20
+ const url = `${BASE_SERVICE_URL}/push`;
19
21
  try {
20
- await api.post<void>(url, { appId, components });
22
+ await api.post<void>(url, { appId, components, manifest });
23
+ } catch (error) {
24
+ throw new Error(error instanceof Error ? error.message : "Unknown error");
25
+ }
26
+ };
27
+
28
+ export const installApp = async (manifest: Manifest): Promise<void> => {
29
+ const url = `${BASE_SERVICE_URL}/install`;
30
+ try {
31
+ await api.post<void>(url, { manifest }, { timeout: 300000 }); // 5 minutes
32
+ } catch (error) {
33
+ throw new Error(error instanceof Error ? error.message : "Unknown error");
34
+ }
35
+ };
36
+
37
+ export const deleteApp = async (manifest: Manifest): Promise<void> => {
38
+ const url = `${BASE_SERVICE_URL}/delete`;
39
+ try {
40
+ await api.delete<void>(url, { manifest });
21
41
  } catch (error) {
22
42
  throw new Error(error instanceof Error ? error.message : "Unknown error");
23
43
  }
package/src/api/types.ts CHANGED
@@ -1,5 +1,9 @@
1
+ import { COMPONENT_TYPE } from "../constants/component-types.js";
2
+ import { HeightOption } from "../constants/height-options.js";
3
+
1
4
  export interface CreateAppRequest {
2
5
  appId: string;
6
+ componentId: string;
3
7
  }
4
8
 
5
9
  export interface ApiError {
@@ -20,10 +24,49 @@ interface ManifestApp {
20
24
  description?: string;
21
25
  }
22
26
 
23
- export interface ManifestComponent {
27
+ export interface RecordComponentSettings {
28
+ iconName: string;
29
+ iconColor: string;
30
+ objectType: number;
31
+ height: HeightOption;
32
+ }
33
+
34
+ export interface GlobalMenuComponentSettings {
35
+ displayName: string;
36
+ }
37
+
38
+ export interface SideMenuComponentSettings {
39
+ icon: string;
40
+ width: "S" | "M" | "L";
41
+ }
42
+
43
+ export type ComponentSettings =
44
+ | RecordComponentSettings
45
+ | GlobalMenuComponentSettings
46
+ | SideMenuComponentSettings;
47
+
48
+ export interface BaseManifestComponent {
49
+ title: string;
50
+ id: string;
51
+ path: string;
52
+ }
53
+
54
+ export interface RecordComponent extends BaseManifestComponent {
55
+ type: typeof COMPONENT_TYPE.RECORD;
56
+ settings: RecordComponentSettings;
57
+ }
58
+
59
+ export interface GlobalMenuComponent extends BaseManifestComponent {
60
+ type: typeof COMPONENT_TYPE.GLOBAL_MENU;
61
+ settings: GlobalMenuComponentSettings;
62
+ }
63
+
64
+ export type ManifestComponent = RecordComponent | GlobalMenuComponent;
65
+
66
+ export interface UntypedManifestComponent {
24
67
  type: string;
25
68
  title: string;
26
- key: string;
69
+ id: string;
27
70
  path: string;
28
71
  settings?: Record<string, unknown>;
29
72
  }
@@ -35,6 +78,6 @@ export interface Manifest {
35
78
 
36
79
  export interface ZippedComponent {
37
80
  title: string;
38
- key: string;
81
+ id: string;
39
82
  build: Buffer;
40
83
  }
@@ -5,6 +5,8 @@ import { runInit } from "../commands/init.js";
5
5
  import { runCreate } from "../commands/create.js";
6
6
  import packageJson from "../../package.json" with { type: "json" };
7
7
  import { runPush } from "../commands/push.js";
8
+ import { runInstall } from "../commands/install.js";
9
+ import { runDelete } from "../commands/delete.js";
8
10
 
9
11
  const program = new Command();
10
12
 
@@ -37,6 +39,19 @@ program
37
39
  await runPush();
38
40
  });
39
41
 
42
+ program.command("install")
43
+ .description("Install app on your Fireberry account")
44
+ .action(async () => {
45
+ await runInstall();
46
+ });
47
+
48
+ program
49
+ .command("delete")
50
+ .description("Delete a Fireberry app")
51
+ .action(async () => {
52
+ await runDelete();
53
+ });
54
+
40
55
  program.parseAsync(process.argv).catch((err: unknown) => {
41
56
  const errorMessage = err instanceof Error
42
57
  ? err.message
@@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url";
6
6
  import ora from "ora";
7
7
  import chalk from "chalk";
8
8
  import { createApp } from "../api/requests.js";
9
+ import { getManifest } from "../utils/components.utils.js";
9
10
 
10
11
  const __filename = fileURLToPath(import.meta.url);
11
12
  const __dirname = path.dirname(__filename);
@@ -40,6 +41,7 @@ export async function runCreate({ name }: CreateOptions): Promise<void> {
40
41
 
41
42
  const slug = slugifyName(appName);
42
43
  const appId = uuidv4();
44
+ const componentId = uuidv4();
43
45
  const appDir = path.resolve(process.cwd(), slug);
44
46
 
45
47
  if (await fs.pathExists(appDir)) {
@@ -49,8 +51,6 @@ export async function runCreate({ name }: CreateOptions): Promise<void> {
49
51
  const spinner = ora(`Creating app "${chalk.cyan(appName)}"...`).start();
50
52
 
51
53
  try {
52
- await createApp({ appId });
53
-
54
54
  await fs.ensureDir(appDir);
55
55
 
56
56
  const templatesDir = path.join(__dirname, "..", "..", "src", "templates");
@@ -65,12 +65,16 @@ export async function runCreate({ name }: CreateOptions): Promise<void> {
65
65
 
66
66
  const manifestContent = manifestTemplate
67
67
  .replace(/{{appName}}/g, appName)
68
- .replace(/{{appId}}/g, appId);
68
+ .replace(/{{appId}}/g, appId)
69
+ .replace(/{{componentId}}/g, componentId);
69
70
 
70
71
  const htmlContent = htmlTemplate.replace(/{{appName}}/g, appName);
71
72
 
72
73
  await fs.writeFile(path.join(appDir, "manifest.yml"), manifestContent);
73
74
  await fs.writeFile(path.join(appDir, "index.html"), htmlContent);
75
+ const manifest = await getManifest(appDir);
76
+
77
+ await createApp(manifest);
74
78
 
75
79
  spinner.succeed(`Successfully created "${chalk.cyan(appName)}" app!`);
76
80
  console.log(chalk.gray(`📁 Location: ${appDir}`));
@@ -0,0 +1,40 @@
1
+ import ora from "ora";
2
+ import chalk from "chalk";
3
+ import inquirer from "inquirer";
4
+ import { deleteApp } from "../api/requests.js";
5
+ import { getManifest } from "../utils/components.utils.js";
6
+
7
+ export async function runDelete(): Promise<void> {
8
+ const spinner = ora("Loading manifest...").start();
9
+
10
+ try {
11
+ const manifest = await getManifest();
12
+ spinner.stop();
13
+
14
+ const confirmAnswer = await inquirer.prompt([
15
+ {
16
+ type: "confirm",
17
+ name: "confirm",
18
+ message: `Are you sure you want to delete app ${chalk.yellow(
19
+ manifest.app.name
20
+ )} (${chalk.gray(manifest.app.id)})? This action cannot be undone.`,
21
+ default: false,
22
+ },
23
+ ]);
24
+
25
+ if (!confirmAnswer.confirm) {
26
+ console.log(chalk.gray("Delete operation cancelled."));
27
+ return;
28
+ }
29
+
30
+ spinner.start(`Deleting app "${chalk.cyan(manifest.app.name)}"...`);
31
+
32
+ await deleteApp(manifest);
33
+ spinner.succeed(
34
+ `Successfully deleted app "${chalk.cyan(manifest.app.name)}"`
35
+ );
36
+ } catch (error) {
37
+ spinner.fail("Failed to delete app");
38
+ throw error;
39
+ }
40
+ }
@@ -0,0 +1,23 @@
1
+ import ora from "ora";
2
+ import { installApp } from "../api/requests.js";
3
+ import {
4
+ getManifest,
5
+ validateManifestComponents,
6
+ } from "../utils/components.utils.js";
7
+
8
+ export async function runInstall(): Promise<void> {
9
+ const spinner = ora("Loading manifest...").start();
10
+
11
+ try {
12
+ const manifest = await getManifest();
13
+
14
+ await validateManifestComponents(manifest);
15
+
16
+ spinner.start("Installing app on Fireberry...");
17
+ await installApp(manifest);
18
+ spinner.succeed("App installed successfully");
19
+ } catch (error) {
20
+ spinner.fail("Installation failed");
21
+ throw error;
22
+ }
23
+ }
@@ -24,13 +24,13 @@ export async function runPush(): Promise<void> {
24
24
  zippedComponents.forEach((comp, idx) => {
25
25
  const sizeKB = (comp.build.length / 1024).toFixed(2);
26
26
  console.log(
27
- chalk.gray(` ${idx + 1}. ${comp.title} (${comp.key}) - ${sizeKB} KB`)
27
+ chalk.gray(` ${idx + 1}. ${comp.title} (${comp.id}) - ${sizeKB} KB`)
28
28
  );
29
29
  });
30
30
 
31
31
  spinner.start("Uploading to Fireberry...");
32
32
 
33
- await pushComponents(manifest.app.id, zippedComponents);
33
+ await pushComponents(manifest.app.id, zippedComponents, manifest);
34
34
  spinner.succeed("Components pushed successfully");
35
35
  } else {
36
36
  spinner.succeed("No components to push");
@@ -0,0 +1,10 @@
1
+ export const COMPONENT_TYPE = {
2
+ RECORD: "record",
3
+ GLOBAL_MENU: "global-menu",
4
+ SIDE_MENU: "side-menu",
5
+ } as const;
6
+
7
+ export type ComponentType =
8
+ (typeof COMPONENT_TYPE)[keyof typeof COMPONENT_TYPE];
9
+
10
+ export const BASE_SERVICE_URL = "/services/developer/app";
@@ -0,0 +1,3 @@
1
+ export const HEIGHT_OPTIONS = ["S", "M", "L", "XL"] as const;
2
+
3
+ export type HeightOption = (typeof HEIGHT_OPTIONS)[number];
@@ -5,6 +5,10 @@ app:
5
5
  components:
6
6
  - type: record
7
7
  title: my-first-component
8
- key: comp
8
+ id: "{{componentId}}"
9
9
  path: static/comp/build
10
- settings: {}
10
+ settings:
11
+ iconName: "related-single"
12
+ iconColor: "#7aae7f"
13
+ objectType: 0
14
+ height: "M"
@@ -4,14 +4,24 @@ import yaml from "js-yaml";
4
4
  import chalk from "chalk";
5
5
  import * as tar from "tar";
6
6
  import os from "node:os";
7
- import { Manifest, ManifestComponent, ZippedComponent } from "../api/types.js";
7
+ import {
8
+ Manifest,
9
+ ZippedComponent,
10
+ UntypedManifestComponent,
11
+ RecordComponentSettings,
12
+ GlobalMenuComponentSettings,
13
+ SideMenuComponentSettings,
14
+ } from "../api/types.js";
15
+ import { COMPONENT_TYPE } from "../constants/component-types.js";
16
+ import { HEIGHT_OPTIONS } from "../constants/height-options.js";
8
17
 
9
- export const getManifest = async (): Promise<Manifest> => {
10
- const manifestPath = path.join(process.cwd(), "manifest.yml");
18
+ export const getManifest = async (basePath?: string): Promise<Manifest> => {
19
+ const manifestPath = path.join(basePath || process.cwd(), "manifest.yml");
20
+ const searchDir = basePath || process.cwd();
11
21
 
12
22
  if (!(await fs.pathExists(manifestPath))) {
13
23
  throw new Error(
14
- `No manifest.yml found at ${chalk.yellow(process.cwd())}.\n` +
24
+ `No manifest.yml found at ${chalk.yellow(searchDir)}.\n` +
15
25
  `Please run this command from your Fireberry app directory.`
16
26
  );
17
27
  }
@@ -33,9 +43,147 @@ export const getManifest = async (): Promise<Manifest> => {
33
43
  return manifest;
34
44
  };
35
45
 
46
+ const validateRecordComponentSettings = (
47
+ comp: UntypedManifestComponent
48
+ ): void => {
49
+ const settings = comp.settings as
50
+ | Partial<RecordComponentSettings>
51
+ | undefined;
52
+
53
+ if (!settings) {
54
+ throw new Error(
55
+ `Component "${comp.title}" (type: ${COMPONENT_TYPE.RECORD}) is missing required settings`
56
+ );
57
+ }
58
+
59
+ const requiredFields: (keyof RecordComponentSettings)[] = [
60
+ "iconName",
61
+ "iconColor",
62
+ "objectType",
63
+ "height",
64
+ ];
65
+
66
+ for (const fieldName of requiredFields) {
67
+ if (settings[fieldName] === undefined || settings[fieldName] === null) {
68
+ throw new Error(
69
+ `Component "${comp.title}" (type: ${COMPONENT_TYPE.RECORD}) is missing required setting: ${fieldName}`
70
+ );
71
+ }
72
+ }
73
+
74
+ if (typeof settings.iconName !== "string") {
75
+ throw new Error(
76
+ `Component "${comp.title}" (type: ${COMPONENT_TYPE.RECORD}) setting "iconName" must be a string`
77
+ );
78
+ }
79
+
80
+ if (typeof settings.iconColor !== "string") {
81
+ throw new Error(
82
+ `Component "${comp.title}" (type: ${COMPONENT_TYPE.RECORD}) setting "iconColor" must be a string`
83
+ );
84
+ }
85
+
86
+ if (typeof settings.objectType !== "number") {
87
+ throw new Error(
88
+ `Component "${comp.title}" (type: ${COMPONENT_TYPE.RECORD}) setting "objectType" must be a number`
89
+ );
90
+ }
91
+ if (!settings.height) {
92
+ throw new Error(
93
+ `Component "${comp.title}" (type: ${COMPONENT_TYPE.RECORD}) setting "height" must be one of: ${HEIGHT_OPTIONS.join(" | ")}`
94
+ );
95
+ }
96
+
97
+ if (!HEIGHT_OPTIONS.includes(settings.height as any)) {
98
+ throw new Error(
99
+ `Component "${comp.title}" (type: ${COMPONENT_TYPE.RECORD}) setting "height" must be one of: ${HEIGHT_OPTIONS.join(" | ")}`
100
+ );
101
+ }
102
+ };
103
+
104
+ const validateGlobalMenuComponentSettings = (
105
+ comp: UntypedManifestComponent
106
+ ): void => {
107
+ const settings = comp.settings as
108
+ | Partial<GlobalMenuComponentSettings>
109
+ | undefined;
110
+
111
+ if (!settings) {
112
+ throw new Error(
113
+ `Component "${comp.title}" (type: ${COMPONENT_TYPE.GLOBAL_MENU}) is missing required settings`
114
+ );
115
+ }
116
+
117
+ if (!settings.displayName) {
118
+ throw new Error(
119
+ `Component "${comp.title}" (type: ${COMPONENT_TYPE.GLOBAL_MENU}) is missing required setting: displayName`
120
+ );
121
+ }
122
+
123
+ if (typeof settings.displayName !== "string") {
124
+ throw new Error(
125
+ `Component "${comp.title}" (type: ${COMPONENT_TYPE.GLOBAL_MENU}) setting "displayName" must be a string`
126
+ );
127
+ }
128
+ };
129
+
130
+ const validateSideMenuComponentSettings = (
131
+ comp: UntypedManifestComponent
132
+ ): void => {
133
+ const settings = comp.settings as
134
+ | Partial<SideMenuComponentSettings>
135
+ | undefined;
136
+
137
+ if (!settings) {
138
+ throw new Error(
139
+ `Component "${comp.title}" (type: ${COMPONENT_TYPE.SIDE_MENU}) is missing required settings`
140
+ );
141
+ }
142
+
143
+ if (!settings.icon) {
144
+ throw new Error(
145
+ `Component "${comp.title}" (type: ${COMPONENT_TYPE.SIDE_MENU}) setting "icon" must be a string`
146
+ );
147
+ }
148
+
149
+ if (!settings.width) {
150
+ throw new Error(
151
+ `Component "${comp.title}" (type: ${COMPONENT_TYPE.SIDE_MENU}) setting "width" must be a S | M | L`
152
+ );
153
+ }
154
+
155
+ if (
156
+ settings.width !== "S" &&
157
+ settings.width !== "M" &&
158
+ settings.width !== "L"
159
+ ) {
160
+ throw new Error(
161
+ `Component "${comp.title}" (type: ${COMPONENT_TYPE.SIDE_MENU}) setting "width" must be a S | M | L`
162
+ );
163
+ }
164
+ };
165
+
166
+ const validateComponentSettings = (comp: UntypedManifestComponent): void => {
167
+ switch (comp.type) {
168
+ case COMPONENT_TYPE.RECORD:
169
+ validateRecordComponentSettings(comp);
170
+ break;
171
+ case COMPONENT_TYPE.GLOBAL_MENU:
172
+ validateGlobalMenuComponentSettings(comp);
173
+ break;
174
+ case COMPONENT_TYPE.SIDE_MENU:
175
+ validateSideMenuComponentSettings(comp);
176
+ break;
177
+ default:
178
+ throw new Error(
179
+ `Component "${comp.title}" has unsupported type: ${comp.type}. Supported types: ${COMPONENT_TYPE.RECORD}, ${COMPONENT_TYPE.GLOBAL_MENU}`
180
+ );
181
+ }
182
+ };
183
+
36
184
  export const validateComponentBuild = async (
37
185
  componentPath: string,
38
- comp: ManifestComponent
186
+ comp: UntypedManifestComponent
39
187
  ) => {
40
188
  if (!(await fs.pathExists(componentPath))) {
41
189
  throw new Error(
@@ -49,10 +197,13 @@ export const validateComponentBuild = async (
49
197
 
50
198
  if (stats.isDirectory()) {
51
199
  const files = await fs.readdir(componentPath);
200
+
52
201
  if (files.length === 0) {
53
- throw new Error(`Component <${comp.key}> at: /${comp.path} not found`);
202
+ throw new Error(`Component <${comp.id}> at: /${comp.path} not found`);
54
203
  }
55
204
  }
205
+
206
+ validateComponentSettings(comp);
56
207
  };
57
208
 
58
209
  export const zipComponentBuild = async (
@@ -102,31 +253,42 @@ export const zipComponentBuild = async (
102
253
  }
103
254
  };
104
255
 
105
- export const handleComponents = async (
106
- manifest: Manifest
107
- ): Promise<ZippedComponent[]> => {
108
- const components = manifest.components;
256
+ export const validateManifestComponents = async (manifest: Manifest) => {
257
+ const components = manifest.components as unknown as
258
+ | UntypedManifestComponent[]
259
+ | undefined;
109
260
  if (!components || components.length === 0) {
110
- return [];
261
+ throw new Error("No components found in manifest");
262
+ }
263
+
264
+ const ids = components.map((comp) => comp.id);
265
+ if (new Set(ids).size !== ids.length) {
266
+ throw new Error("All component ids must be unique");
111
267
  }
112
268
 
113
- const keys = components.map((comp) => comp.key);
114
- if (new Set(keys).size !== keys.length) {
115
- throw new Error("All component keys must be unique");
269
+ for (const comp of components) {
270
+ const componentPath = path.join(process.cwd(), comp.path);
271
+ await validateComponentBuild(componentPath, comp);
116
272
  }
273
+ };
274
+
275
+ export const handleComponents = async (
276
+ manifest: Manifest
277
+ ): Promise<ZippedComponent[]> => {
278
+ await validateManifestComponents(manifest);
279
+ const components =
280
+ manifest.components as unknown as UntypedManifestComponent[];
117
281
 
118
282
  const zippedComponents: ZippedComponent[] = [];
119
283
 
120
284
  for (const comp of components) {
121
285
  const componentPath = path.join(process.cwd(), comp.path);
122
286
 
123
- await validateComponentBuild(componentPath, comp);
124
-
125
287
  const buildBuffer = await zipComponentBuild(componentPath, comp.title);
126
288
 
127
289
  zippedComponents.push({
128
290
  title: comp.title,
129
- key: comp.key,
291
+ id: comp.id,
130
292
  build: buildBuffer,
131
293
  });
132
294
  }