@fireberry/cli 0.0.5-beta.13 → 0.0.5-beta.16
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 +119 -0
- package/dist/api/requests.d.ts +1 -0
- package/dist/api/requests.js +9 -0
- package/dist/api/types.d.ts +25 -1
- package/dist/bin/fireberry.js +7 -0
- package/dist/commands/delete.d.ts +1 -0
- package/dist/commands/delete.js +31 -0
- package/dist/constants/component-types.d.ts +5 -0
- package/dist/constants/component-types.js +4 -0
- package/dist/utils/components.utils.d.ts +2 -2
- package/dist/utils/components.utils.js +51 -0
- package/package.json +1 -1
- package/src/api/requests.ts +9 -0
- package/src/api/types.ts +35 -1
- package/src/bin/fireberry.ts +9 -1
- package/src/commands/delete.ts +40 -0
- package/src/constants/component-types.ts +6 -0
- package/src/utils/components.utils.ts +103 -4
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
```bash
|
|
13
|
+
npm run build # Compile TypeScript to dist/
|
|
14
|
+
npm run build:dev # Clean, build, link globally, and make executable
|
|
15
|
+
npm run dev # Watch mode compilation
|
|
16
|
+
npm run clean # Remove dist/ directory
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Testing
|
|
20
|
+
```bash
|
|
21
|
+
npm test # Run smoke test (verifies CLI --help works)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Local Development
|
|
25
|
+
```bash
|
|
26
|
+
npm run build:dev # Build and link CLI locally
|
|
27
|
+
fireberry --help # Test the linked CLI
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Publishing
|
|
31
|
+
```bash
|
|
32
|
+
npm run publish:beta # Version bump (beta) and publish to npm with beta tag
|
|
33
|
+
npm run publish:prod # Publish to npm with latest tag
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Architecture
|
|
37
|
+
|
|
38
|
+
### Module System
|
|
39
|
+
- **Type**: ESM (ES Modules) with `"type": "module"` in package.json
|
|
40
|
+
- **Module Resolution**: NodeNext
|
|
41
|
+
- **Import Extensions**: All local imports must use `.js` extension (e.g., `import { foo } from "./bar.js"`)
|
|
42
|
+
- **JSON Imports**: Use `with { type: "json" }` syntax (e.g., `import packageJson from "../../package.json" with { type: "json" }`)
|
|
43
|
+
|
|
44
|
+
### CLI Entry Point
|
|
45
|
+
- **Binary**: [dist/bin/fireberry.js](dist/bin/fireberry.js) (generated from [src/bin/fireberry.ts](src/bin/fireberry.ts))
|
|
46
|
+
- **Framework**: Commander.js for command parsing and routing
|
|
47
|
+
- **Error Handling**: Global error handler in [src/bin/fireberry.ts](src/bin/fireberry.ts) catches and formats errors
|
|
48
|
+
|
|
49
|
+
### Command Structure
|
|
50
|
+
Each CLI command is in [src/commands/](src/commands/):
|
|
51
|
+
- **init**: Stores Fireberry API token in local config using `env-paths`
|
|
52
|
+
- **create**: Creates new Fireberry app from templates with generated UUIDs
|
|
53
|
+
- **push**: Validates manifest, zips components, uploads to Fireberry API
|
|
54
|
+
- **install**: Installs app on user's Fireberry account
|
|
55
|
+
|
|
56
|
+
### Configuration System
|
|
57
|
+
- **User Config**: Stored via `env-paths` ("Fireberry CLI") in OS-specific config directory
|
|
58
|
+
- **Config File**: `config.json` with `{ apiToken, createdAt }`
|
|
59
|
+
- **Environment**: [src/config/env.ts](src/config/env.ts) loads `.env` file from project root using dotenv
|
|
60
|
+
|
|
61
|
+
### API Layer ([src/api/](src/api/))
|
|
62
|
+
- **axios.ts**: Axios instance with automatic token injection via interceptors
|
|
63
|
+
- **API URL Logic**:
|
|
64
|
+
- Beta versions (version contains "beta") → Uses `FIREBERRY_STAGING_URL` environment variable
|
|
65
|
+
- Production versions → `FIREBERRY_API_URL` or `https://api.fireberry.com/api/v3`
|
|
66
|
+
- **Authentication**: Token passed via `tokenId` header (read from config in interceptor)
|
|
67
|
+
- **requests.ts**: API endpoints (`createApp`, `pushComponents`, `installApp`)
|
|
68
|
+
- **types.ts**: TypeScript interfaces for API requests/responses
|
|
69
|
+
|
|
70
|
+
### Component System
|
|
71
|
+
Component utilities in [src/utils/components.utils.ts](src/utils/components.utils.ts):
|
|
72
|
+
- **Manifest Parsing**: YAML manifest loaded from `manifest.yml` in current directory
|
|
73
|
+
- **Component Validation**: Checks paths exist, components are unique
|
|
74
|
+
- **Component Zipping**: Creates tar.gz archives of component builds (directories or single files)
|
|
75
|
+
- **Manifest Structure**:
|
|
76
|
+
```yaml
|
|
77
|
+
app:
|
|
78
|
+
id: "uuid"
|
|
79
|
+
name: "App Name"
|
|
80
|
+
components:
|
|
81
|
+
- type: record
|
|
82
|
+
title: component-name
|
|
83
|
+
id: "uuid"
|
|
84
|
+
path: relative/path/to/build
|
|
85
|
+
settings: {...}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Templates ([src/templates/](src/templates/))
|
|
89
|
+
Templates use mustache-style placeholders (`{{appName}}`, `{{appId}}`, `{{componentId}}`):
|
|
90
|
+
- **manifest.yml**: Default manifest with single component
|
|
91
|
+
- **index.html**: Basic HTML template
|
|
92
|
+
|
|
93
|
+
## Key Patterns
|
|
94
|
+
|
|
95
|
+
### Error Handling
|
|
96
|
+
- Commands throw errors which are caught by the global handler in [src/bin/fireberry.ts:47](src/bin/fireberry.ts#L47)
|
|
97
|
+
- Errors are formatted with chalk.red and prefixed with "Error:" if not already present
|
|
98
|
+
- API errors map HTTP status codes to user-friendly messages
|
|
99
|
+
|
|
100
|
+
### User Feedback
|
|
101
|
+
- Use `ora` spinners for long-running operations (start → succeed/fail)
|
|
102
|
+
- Use `chalk` for colored output (cyan for highlights, gray for details, yellow for warnings)
|
|
103
|
+
- Use `inquirer` for interactive prompts when arguments are missing
|
|
104
|
+
|
|
105
|
+
### File Operations
|
|
106
|
+
- Use `fs-extra` for all file operations (promisified, ensures directories exist)
|
|
107
|
+
- Check `fs.pathExists()` before operations
|
|
108
|
+
- Use `process.cwd()` as base for relative paths in user projects
|
|
109
|
+
|
|
110
|
+
### Component Path Resolution
|
|
111
|
+
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"`.
|
|
112
|
+
|
|
113
|
+
## Important Notes
|
|
114
|
+
|
|
115
|
+
- **Manifest Required**: `push` and `install` commands must be run from a directory containing `manifest.yml`
|
|
116
|
+
- **Token Required**: Most commands require prior `init` to store API token
|
|
117
|
+
- **Component IDs**: Must be unique UUIDs within a manifest
|
|
118
|
+
- **Build Zipping**: Single files are wrapped in a directory before tar.gz creation
|
|
119
|
+
- **Template Location**: Templates are resolved from `src/templates/` at compile time, copied to `dist/templates/`
|
package/dist/api/requests.d.ts
CHANGED
|
@@ -3,3 +3,4 @@ import type { CreateAppRequest, Manifest, ZippedComponent } from "./types.js";
|
|
|
3
3
|
export declare const createApp: (data: CreateAppRequest) => Promise<void>;
|
|
4
4
|
export declare const pushComponents: (appId: string, components: ZippedComponent[]) => Promise<void>;
|
|
5
5
|
export declare const installApp: (manifest: Manifest) => Promise<void>;
|
|
6
|
+
export declare const deleteApp: (manifest: Manifest) => Promise<void>;
|
package/dist/api/requests.js
CHANGED
|
@@ -27,3 +27,12 @@ export const installApp = async (manifest) => {
|
|
|
27
27
|
throw new Error(error instanceof Error ? error.message : "Unknown error");
|
|
28
28
|
}
|
|
29
29
|
};
|
|
30
|
+
export const deleteApp = async (manifest) => {
|
|
31
|
+
const url = `/services/developer/delete`;
|
|
32
|
+
try {
|
|
33
|
+
await api.delete(url, manifest);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
throw new Error(error instanceof Error ? error.message : "Unknown error");
|
|
37
|
+
}
|
|
38
|
+
};
|
package/dist/api/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { COMPONENT_TYPE } from "../constants/component-types.js";
|
|
1
2
|
export interface CreateAppRequest {
|
|
2
3
|
appId: string;
|
|
3
4
|
}
|
|
@@ -16,7 +17,30 @@ interface ManifestApp {
|
|
|
16
17
|
name: string;
|
|
17
18
|
description?: string;
|
|
18
19
|
}
|
|
19
|
-
export interface
|
|
20
|
+
export interface RecordComponentSettings {
|
|
21
|
+
iconName: string;
|
|
22
|
+
iconColor: string;
|
|
23
|
+
objectType: number;
|
|
24
|
+
}
|
|
25
|
+
export interface GlobalMenuComponentSettings {
|
|
26
|
+
displayName: string;
|
|
27
|
+
}
|
|
28
|
+
export type ComponentSettings = RecordComponentSettings | GlobalMenuComponentSettings;
|
|
29
|
+
export interface BaseManifestComponent {
|
|
30
|
+
title: string;
|
|
31
|
+
id: string;
|
|
32
|
+
path: string;
|
|
33
|
+
}
|
|
34
|
+
export interface RecordComponent extends BaseManifestComponent {
|
|
35
|
+
type: typeof COMPONENT_TYPE.RECORD;
|
|
36
|
+
settings: RecordComponentSettings;
|
|
37
|
+
}
|
|
38
|
+
export interface GlobalMenuComponent extends BaseManifestComponent {
|
|
39
|
+
type: typeof COMPONENT_TYPE.GLOBAL_MENU;
|
|
40
|
+
settings: GlobalMenuComponentSettings;
|
|
41
|
+
}
|
|
42
|
+
export type ManifestComponent = RecordComponent | GlobalMenuComponent;
|
|
43
|
+
export interface UntypedManifestComponent {
|
|
20
44
|
type: string;
|
|
21
45
|
title: string;
|
|
22
46
|
id: string;
|
package/dist/bin/fireberry.js
CHANGED
|
@@ -6,6 +6,7 @@ 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
8
|
import { runInstall } from "../commands/install.js";
|
|
9
|
+
import { runDelete } from "../commands/delete.js";
|
|
9
10
|
const program = new Command();
|
|
10
11
|
program
|
|
11
12
|
.name("fireberry")
|
|
@@ -37,6 +38,12 @@ program.command("install")
|
|
|
37
38
|
.action(async () => {
|
|
38
39
|
await runInstall();
|
|
39
40
|
});
|
|
41
|
+
program
|
|
42
|
+
.command("delete")
|
|
43
|
+
.description("Delete a Fireberry app")
|
|
44
|
+
.action(async () => {
|
|
45
|
+
await runDelete();
|
|
46
|
+
});
|
|
40
47
|
program.parseAsync(process.argv).catch((err) => {
|
|
41
48
|
const errorMessage = err instanceof Error
|
|
42
49
|
? err.message
|
|
@@ -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
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Manifest,
|
|
1
|
+
import { Manifest, ZippedComponent, UntypedManifestComponent } from "../api/types.js";
|
|
2
2
|
export declare const getManifest: () => Promise<Manifest>;
|
|
3
|
-
export declare const validateComponentBuild: (componentPath: string, comp:
|
|
3
|
+
export declare const validateComponentBuild: (componentPath: string, comp: UntypedManifestComponent) => Promise<void>;
|
|
4
4
|
export declare const zipComponentBuild: (componentPath: string, title: string) => Promise<Buffer>;
|
|
5
5
|
export declare const validateManifestComponents: (manifest: Manifest) => Promise<void>;
|
|
6
6
|
export declare const handleComponents: (manifest: Manifest) => Promise<ZippedComponent[]>;
|
|
@@ -4,6 +4,7 @@ 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 { COMPONENT_TYPE } from "../constants/component-types.js";
|
|
7
8
|
export const getManifest = async () => {
|
|
8
9
|
const manifestPath = path.join(process.cwd(), "manifest.yml");
|
|
9
10
|
if (!(await fs.pathExists(manifestPath))) {
|
|
@@ -20,6 +21,55 @@ export const getManifest = async () => {
|
|
|
20
21
|
}
|
|
21
22
|
return manifest;
|
|
22
23
|
};
|
|
24
|
+
const validateRecordComponentSettings = (comp) => {
|
|
25
|
+
const settings = comp.settings;
|
|
26
|
+
if (!settings) {
|
|
27
|
+
throw new Error(`Component "${comp.title}" (type: ${COMPONENT_TYPE.RECORD}) is missing required settings`);
|
|
28
|
+
}
|
|
29
|
+
const requiredFields = [
|
|
30
|
+
"iconName",
|
|
31
|
+
"iconColor",
|
|
32
|
+
"objectType",
|
|
33
|
+
];
|
|
34
|
+
for (const fieldName of requiredFields) {
|
|
35
|
+
if (settings[fieldName] === undefined || settings[fieldName] === null) {
|
|
36
|
+
throw new Error(`Component "${comp.title}" (type: ${COMPONENT_TYPE.RECORD}) is missing required setting: ${fieldName}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (typeof settings.iconName !== "string") {
|
|
40
|
+
throw new Error(`Component "${comp.title}" (type: ${COMPONENT_TYPE.RECORD}) setting "iconName" must be a string`);
|
|
41
|
+
}
|
|
42
|
+
if (typeof settings.iconColor !== "string") {
|
|
43
|
+
throw new Error(`Component "${comp.title}" (type: ${COMPONENT_TYPE.RECORD}) setting "iconColor" must be a string`);
|
|
44
|
+
}
|
|
45
|
+
if (typeof settings.objectType !== "number") {
|
|
46
|
+
throw new Error(`Component "${comp.title}" (type: ${COMPONENT_TYPE.RECORD}) setting "objectType" must be a number`);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const validateGlobalMenuComponentSettings = (comp) => {
|
|
50
|
+
const settings = comp.settings;
|
|
51
|
+
if (!settings) {
|
|
52
|
+
throw new Error(`Component "${comp.title}" (type: ${COMPONENT_TYPE.GLOBAL_MENU}) is missing required settings`);
|
|
53
|
+
}
|
|
54
|
+
if (!settings.displayName) {
|
|
55
|
+
throw new Error(`Component "${comp.title}" (type: ${COMPONENT_TYPE.GLOBAL_MENU}) is missing required setting: displayName`);
|
|
56
|
+
}
|
|
57
|
+
if (typeof settings.displayName !== "string") {
|
|
58
|
+
throw new Error(`Component "${comp.title}" (type: ${COMPONENT_TYPE.GLOBAL_MENU}) setting "displayName" must be a string`);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const validateComponentSettings = (comp) => {
|
|
62
|
+
switch (comp.type) {
|
|
63
|
+
case COMPONENT_TYPE.RECORD:
|
|
64
|
+
validateRecordComponentSettings(comp);
|
|
65
|
+
break;
|
|
66
|
+
case COMPONENT_TYPE.GLOBAL_MENU:
|
|
67
|
+
validateGlobalMenuComponentSettings(comp);
|
|
68
|
+
break;
|
|
69
|
+
default:
|
|
70
|
+
throw new Error(`Component "${comp.title}" has unsupported type: ${comp.type}. Supported types: ${COMPONENT_TYPE.RECORD}, ${COMPONENT_TYPE.GLOBAL_MENU}`);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
23
73
|
export const validateComponentBuild = async (componentPath, comp) => {
|
|
24
74
|
if (!(await fs.pathExists(componentPath))) {
|
|
25
75
|
throw new Error(`Component "${comp.title}" path does not exist: ${chalk.yellow(componentPath)}\n` + `Make sure the path in manifest.yml is correct.`);
|
|
@@ -31,6 +81,7 @@ export const validateComponentBuild = async (componentPath, comp) => {
|
|
|
31
81
|
throw new Error(`Component <${comp.id}> at: /${comp.path} not found`);
|
|
32
82
|
}
|
|
33
83
|
}
|
|
84
|
+
validateComponentSettings(comp);
|
|
34
85
|
};
|
|
35
86
|
export const zipComponentBuild = async (componentPath, title) => {
|
|
36
87
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "fireberry-"));
|
package/package.json
CHANGED
package/src/api/requests.ts
CHANGED
|
@@ -31,3 +31,12 @@ export const installApp = async (manifest: Manifest): Promise<void> => {
|
|
|
31
31
|
throw new Error(error instanceof Error ? error.message : "Unknown error");
|
|
32
32
|
}
|
|
33
33
|
};
|
|
34
|
+
|
|
35
|
+
export const deleteApp = async (manifest: Manifest): Promise<void> => {
|
|
36
|
+
const url = `/services/developer/delete`;
|
|
37
|
+
try {
|
|
38
|
+
await api.delete<void>(url, manifest);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
throw new Error(error instanceof Error ? error.message : "Unknown error");
|
|
41
|
+
}
|
|
42
|
+
};
|
package/src/api/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { COMPONENT_TYPE } from "../constants/component-types.js";
|
|
2
|
+
|
|
1
3
|
export interface CreateAppRequest {
|
|
2
4
|
appId: string;
|
|
3
5
|
}
|
|
@@ -20,7 +22,39 @@ interface ManifestApp {
|
|
|
20
22
|
description?: string;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
export interface
|
|
25
|
+
export interface RecordComponentSettings {
|
|
26
|
+
iconName: string;
|
|
27
|
+
iconColor: string;
|
|
28
|
+
objectType: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface GlobalMenuComponentSettings {
|
|
32
|
+
displayName: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type ComponentSettings =
|
|
36
|
+
| RecordComponentSettings
|
|
37
|
+
| GlobalMenuComponentSettings;
|
|
38
|
+
|
|
39
|
+
export interface BaseManifestComponent {
|
|
40
|
+
title: string;
|
|
41
|
+
id: string;
|
|
42
|
+
path: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface RecordComponent extends BaseManifestComponent {
|
|
46
|
+
type: typeof COMPONENT_TYPE.RECORD;
|
|
47
|
+
settings: RecordComponentSettings;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface GlobalMenuComponent extends BaseManifestComponent {
|
|
51
|
+
type: typeof COMPONENT_TYPE.GLOBAL_MENU;
|
|
52
|
+
settings: GlobalMenuComponentSettings;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type ManifestComponent = RecordComponent | GlobalMenuComponent;
|
|
56
|
+
|
|
57
|
+
export interface UntypedManifestComponent {
|
|
24
58
|
type: string;
|
|
25
59
|
title: string;
|
|
26
60
|
id: string;
|
package/src/bin/fireberry.ts
CHANGED
|
@@ -6,6 +6,7 @@ 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
8
|
import { runInstall } from "../commands/install.js";
|
|
9
|
+
import { runDelete } from "../commands/delete.js";
|
|
9
10
|
|
|
10
11
|
const program = new Command();
|
|
11
12
|
|
|
@@ -38,12 +39,19 @@ program
|
|
|
38
39
|
await runPush();
|
|
39
40
|
});
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
program.command("install")
|
|
42
43
|
.description("Install app on your Fireberry account")
|
|
43
44
|
.action(async () => {
|
|
44
45
|
await runInstall();
|
|
45
46
|
});
|
|
46
47
|
|
|
48
|
+
program
|
|
49
|
+
.command("delete")
|
|
50
|
+
.description("Delete a Fireberry app")
|
|
51
|
+
.action(async () => {
|
|
52
|
+
await runDelete();
|
|
53
|
+
});
|
|
54
|
+
|
|
47
55
|
program.parseAsync(process.argv).catch((err: unknown) => {
|
|
48
56
|
const errorMessage = err instanceof Error
|
|
49
57
|
? err.message
|
|
@@ -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
|
+
}
|
|
@@ -4,7 +4,14 @@ 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 {
|
|
7
|
+
import {
|
|
8
|
+
Manifest,
|
|
9
|
+
ZippedComponent,
|
|
10
|
+
UntypedManifestComponent,
|
|
11
|
+
RecordComponentSettings,
|
|
12
|
+
GlobalMenuComponentSettings,
|
|
13
|
+
} from "../api/types.js";
|
|
14
|
+
import { COMPONENT_TYPE } from "../constants/component-types.js";
|
|
8
15
|
|
|
9
16
|
export const getManifest = async (): Promise<Manifest> => {
|
|
10
17
|
const manifestPath = path.join(process.cwd(), "manifest.yml");
|
|
@@ -33,9 +40,96 @@ export const getManifest = async (): Promise<Manifest> => {
|
|
|
33
40
|
return manifest;
|
|
34
41
|
};
|
|
35
42
|
|
|
43
|
+
const validateRecordComponentSettings = (
|
|
44
|
+
comp: UntypedManifestComponent
|
|
45
|
+
): void => {
|
|
46
|
+
const settings = comp.settings as
|
|
47
|
+
| Partial<RecordComponentSettings>
|
|
48
|
+
| undefined;
|
|
49
|
+
|
|
50
|
+
if (!settings) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`Component "${comp.title}" (type: ${COMPONENT_TYPE.RECORD}) is missing required settings`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const requiredFields: (keyof RecordComponentSettings)[] = [
|
|
57
|
+
"iconName",
|
|
58
|
+
"iconColor",
|
|
59
|
+
"objectType",
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
for (const fieldName of requiredFields) {
|
|
63
|
+
if (settings[fieldName] === undefined || settings[fieldName] === null) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Component "${comp.title}" (type: ${COMPONENT_TYPE.RECORD}) is missing required setting: ${fieldName}`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (typeof settings.iconName !== "string") {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Component "${comp.title}" (type: ${COMPONENT_TYPE.RECORD}) setting "iconName" must be a string`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (typeof settings.iconColor !== "string") {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Component "${comp.title}" (type: ${COMPONENT_TYPE.RECORD}) setting "iconColor" must be a string`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (typeof settings.objectType !== "number") {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Component "${comp.title}" (type: ${COMPONENT_TYPE.RECORD}) setting "objectType" must be a number`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const validateGlobalMenuComponentSettings = (
|
|
90
|
+
comp: UntypedManifestComponent
|
|
91
|
+
): void => {
|
|
92
|
+
const settings = comp.settings as
|
|
93
|
+
| Partial<GlobalMenuComponentSettings>
|
|
94
|
+
| undefined;
|
|
95
|
+
|
|
96
|
+
if (!settings) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`Component "${comp.title}" (type: ${COMPONENT_TYPE.GLOBAL_MENU}) is missing required settings`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!settings.displayName) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Component "${comp.title}" (type: ${COMPONENT_TYPE.GLOBAL_MENU}) is missing required setting: displayName`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (typeof settings.displayName !== "string") {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`Component "${comp.title}" (type: ${COMPONENT_TYPE.GLOBAL_MENU}) setting "displayName" must be a string`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const validateComponentSettings = (comp: UntypedManifestComponent): void => {
|
|
116
|
+
switch (comp.type) {
|
|
117
|
+
case COMPONENT_TYPE.RECORD:
|
|
118
|
+
validateRecordComponentSettings(comp);
|
|
119
|
+
break;
|
|
120
|
+
case COMPONENT_TYPE.GLOBAL_MENU:
|
|
121
|
+
validateGlobalMenuComponentSettings(comp);
|
|
122
|
+
break;
|
|
123
|
+
default:
|
|
124
|
+
throw new Error(
|
|
125
|
+
`Component "${comp.title}" has unsupported type: ${comp.type}. Supported types: ${COMPONENT_TYPE.RECORD}, ${COMPONENT_TYPE.GLOBAL_MENU}`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
36
130
|
export const validateComponentBuild = async (
|
|
37
131
|
componentPath: string,
|
|
38
|
-
comp:
|
|
132
|
+
comp: UntypedManifestComponent
|
|
39
133
|
) => {
|
|
40
134
|
if (!(await fs.pathExists(componentPath))) {
|
|
41
135
|
throw new Error(
|
|
@@ -54,6 +148,8 @@ export const validateComponentBuild = async (
|
|
|
54
148
|
throw new Error(`Component <${comp.id}> at: /${comp.path} not found`);
|
|
55
149
|
}
|
|
56
150
|
}
|
|
151
|
+
|
|
152
|
+
validateComponentSettings(comp);
|
|
57
153
|
};
|
|
58
154
|
|
|
59
155
|
export const zipComponentBuild = async (
|
|
@@ -104,7 +200,9 @@ export const zipComponentBuild = async (
|
|
|
104
200
|
};
|
|
105
201
|
|
|
106
202
|
export const validateManifestComponents = async (manifest: Manifest) => {
|
|
107
|
-
const components = manifest.components
|
|
203
|
+
const components = manifest.components as unknown as
|
|
204
|
+
| UntypedManifestComponent[]
|
|
205
|
+
| undefined;
|
|
108
206
|
if (!components || components.length === 0) {
|
|
109
207
|
throw new Error("No components found in manifest");
|
|
110
208
|
}
|
|
@@ -124,7 +222,8 @@ export const handleComponents = async (
|
|
|
124
222
|
manifest: Manifest
|
|
125
223
|
): Promise<ZippedComponent[]> => {
|
|
126
224
|
await validateManifestComponents(manifest);
|
|
127
|
-
const components =
|
|
225
|
+
const components =
|
|
226
|
+
manifest.components as unknown as UntypedManifestComponent[];
|
|
128
227
|
|
|
129
228
|
const zippedComponents: ZippedComponent[] = [];
|
|
130
229
|
|