@qelos/plugins-cli 0.0.9 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -3
- package/commands/pull.mjs +3 -2
- package/commands/push.mjs +4 -3
- package/controllers/pull.mjs +68 -30
- package/controllers/push.mjs +94 -43
- package/package.json +1 -1
- package/services/blocks.mjs +166 -0
- package/services/blueprints.mjs +157 -0
- package/services/components.mjs +120 -0
- package/services/configurations.mjs +142 -0
- package/services/logger.mjs +112 -0
- package/services/plugins.mjs +159 -0
- package/services/sdk.mjs +57 -0
package/README.md
CHANGED
|
@@ -3,11 +3,144 @@
|
|
|
3
3
|
A command-line interface to help you create and manage your Qelos plugins.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
### Global Installation
|
|
8
|
+
|
|
9
|
+
Install the CLI globally using npm:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g @qelos/plugins-cli
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
After installation, the CLI will be available as both `qelos` and `qplay` commands:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
qelos --version
|
|
19
|
+
qplay --version
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Environment Variables
|
|
23
|
+
|
|
24
|
+
The CLI requires the following environment variables to connect to your Qelos instance:
|
|
25
|
+
|
|
26
|
+
- `QELOS_URL` - Your Qelos instance URL (default: `http://localhost:3000`)
|
|
27
|
+
- `QELOS_USERNAME` - Your Qelos username (default: `test@test.com`)
|
|
28
|
+
- `QELOS_PASSWORD` - Your Qelos password (default: `admin`)
|
|
29
|
+
|
|
30
|
+
You can set these in your shell profile or use a `.env` file:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
export QELOS_URL=https://your-qelos-instance.com
|
|
34
|
+
export QELOS_USERNAME=your-username
|
|
35
|
+
export QELOS_PASSWORD=your-password
|
|
36
|
+
```
|
|
7
37
|
|
|
8
38
|
## Commands
|
|
9
39
|
|
|
10
40
|
### Create a new plugin
|
|
11
41
|
|
|
12
|
-
|
|
13
|
-
|
|
42
|
+
Create a new plugin project:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
qplay create my-app
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Pull
|
|
49
|
+
|
|
50
|
+
Pull resources from your Qelos instance to your local filesystem. This allows you to work on components, plugins, integrations, and blueprints locally.
|
|
51
|
+
|
|
52
|
+
**Syntax:**
|
|
53
|
+
```bash
|
|
54
|
+
qelos pull <type> <path>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Arguments:**
|
|
58
|
+
- `type` - Type of resource to pull (e.g., `components`, `plugins`, `integrations`, `blueprints`)
|
|
59
|
+
- `path` - Local directory path where resources will be saved
|
|
60
|
+
|
|
61
|
+
**Example - Pull Components:**
|
|
62
|
+
```bash
|
|
63
|
+
qelos pull components ./my-components
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
This command will:
|
|
67
|
+
1. Connect to your Qelos instance using the configured credentials
|
|
68
|
+
2. Fetch all components from the instance
|
|
69
|
+
3. Create the target directory if it doesn't exist
|
|
70
|
+
4. Save each component as a `.vue` file using its identifier as the filename
|
|
71
|
+
5. Display progress for each component pulled
|
|
72
|
+
|
|
73
|
+
**Output:**
|
|
74
|
+
```
|
|
75
|
+
Created directory: ./my-components
|
|
76
|
+
Found 5 components to pull
|
|
77
|
+
Pulled component: header-component
|
|
78
|
+
Pulled component: footer-component
|
|
79
|
+
Pulled component: sidebar-component
|
|
80
|
+
All 5 components pulled to ./my-components
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Push
|
|
84
|
+
|
|
85
|
+
Push local resources to your Qelos instance. This allows you to update or create components, plugins, integrations, and blueprints from your local filesystem.
|
|
86
|
+
|
|
87
|
+
**Syntax:**
|
|
88
|
+
```bash
|
|
89
|
+
qelos push <type> <path>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Arguments:**
|
|
93
|
+
- `type` - Type of resource to push (e.g., `components`, `plugins`, `integrations`, `blueprints`)
|
|
94
|
+
- `path` - Local directory path containing the resources to push
|
|
95
|
+
|
|
96
|
+
**Example - Push Components:**
|
|
97
|
+
```bash
|
|
98
|
+
qelos push components ./my-components
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
This command will:
|
|
102
|
+
1. Connect to your Qelos instance using the configured credentials
|
|
103
|
+
2. Read all `.vue` files from the specified directory
|
|
104
|
+
3. For each file:
|
|
105
|
+
- Check if a component with the same identifier exists
|
|
106
|
+
- Update the existing component or create a new one
|
|
107
|
+
- Display progress for each component
|
|
108
|
+
|
|
109
|
+
**Output:**
|
|
110
|
+
```
|
|
111
|
+
Pushing component: header-component
|
|
112
|
+
Component updated: header-component
|
|
113
|
+
Pushing component: new-component
|
|
114
|
+
Component pushed: new-component
|
|
115
|
+
All components pushed
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Workflow Example
|
|
119
|
+
|
|
120
|
+
A typical workflow for working with components:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
# Pull components from Qelos to work on them locally
|
|
124
|
+
qelos pull components ./local-components
|
|
125
|
+
|
|
126
|
+
# Make changes to the .vue files in ./local-components
|
|
127
|
+
|
|
128
|
+
# Push the updated components back to Qelos
|
|
129
|
+
qelos push components ./local-components
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Help
|
|
133
|
+
|
|
134
|
+
View all available commands and options:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
qelos --help
|
|
138
|
+
qplay --help
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
View help for a specific command:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
qelos pull --help
|
|
145
|
+
qelos push --help
|
|
146
|
+
```
|
package/commands/pull.mjs
CHANGED
|
@@ -2,12 +2,13 @@ import pullController from "../controllers/pull.mjs";
|
|
|
2
2
|
|
|
3
3
|
export default function pullCommand(program) {
|
|
4
4
|
program
|
|
5
|
-
.command('pull [type] [path]', 'pull from qelos app. Ability to pull components,
|
|
5
|
+
.command('pull [type] [path]', 'pull from qelos app. Ability to pull components, blueprints, configurations, plugins, blocks, and more.',
|
|
6
6
|
(yargs) => {
|
|
7
7
|
return yargs
|
|
8
8
|
.positional('type', {
|
|
9
|
-
describe: 'Type of the resource to pull. Can be components,
|
|
9
|
+
describe: 'Type of the resource to pull. Can be components, blueprints, configurations, plugins, blocks, or all.',
|
|
10
10
|
type: 'string',
|
|
11
|
+
choices: ['components', 'blueprints', 'configs', 'plugins', 'blocks', 'all', '*'],
|
|
11
12
|
required: true
|
|
12
13
|
})
|
|
13
14
|
.positional('path', {
|
package/commands/push.mjs
CHANGED
|
@@ -2,16 +2,17 @@ import pushController from "../controllers/push.mjs";
|
|
|
2
2
|
|
|
3
3
|
export default function createCommand(program) {
|
|
4
4
|
program
|
|
5
|
-
.command('push [type] [path]', 'push to qelos app. Ability to push components,
|
|
5
|
+
.command('push [type] [path]', 'push to qelos app. Ability to push components, blueprints, configurations, plugins, blocks, and more.',
|
|
6
6
|
(yargs) => {
|
|
7
7
|
return yargs
|
|
8
8
|
.positional('type', {
|
|
9
|
-
describe: 'Type of the
|
|
9
|
+
describe: 'Type of the resource to push. Can be components, blueprints, configurations, plugins, blocks, or all.',
|
|
10
10
|
type: 'string',
|
|
11
|
+
choices: ['components', 'blueprints', 'configs', 'plugins', 'blocks', 'all', '*'],
|
|
11
12
|
required: true
|
|
12
13
|
})
|
|
13
14
|
.positional('path', {
|
|
14
|
-
describe: 'Path to the
|
|
15
|
+
describe: 'Path to the resource to push.',
|
|
15
16
|
type: 'string',
|
|
16
17
|
required: true
|
|
17
18
|
})
|
package/controllers/pull.mjs
CHANGED
|
@@ -1,43 +1,81 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { initializeSdk } from '../services/sdk.mjs';
|
|
2
|
+
import { pullComponents } from '../services/components.mjs';
|
|
3
|
+
import { pullBlueprints } from '../services/blueprints.mjs';
|
|
4
|
+
import { pullConfigurations } from '../services/configurations.mjs';
|
|
5
|
+
import { pullPlugins } from '../services/plugins.mjs';
|
|
6
|
+
import { pullBlocks } from '../services/blocks.mjs';
|
|
7
|
+
import { logger } from '../services/logger.mjs';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
6
10
|
|
|
7
11
|
export default async function pullController({ type, path: targetPath }) {
|
|
12
|
+
try {
|
|
13
|
+
// Validate parent directory exists
|
|
14
|
+
const parentDir = path.dirname(targetPath);
|
|
15
|
+
if (!fs.existsSync(parentDir)) {
|
|
16
|
+
logger.error(`Parent directory does not exist: ${parentDir}`);
|
|
17
|
+
logger.info('Please ensure the parent directory exists');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
8
20
|
|
|
9
|
-
|
|
21
|
+
// Warn if target path exists and is not a directory
|
|
22
|
+
if (fs.existsSync(targetPath) && !fs.statSync(targetPath).isDirectory()) {
|
|
23
|
+
logger.error(`Path exists but is not a directory: ${targetPath}`);
|
|
24
|
+
logger.info('Please provide a directory path, not a file');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
10
27
|
|
|
11
|
-
|
|
12
|
-
appUrl: process.env.QELOS_URL || "http://localhost:3000",
|
|
13
|
-
})
|
|
28
|
+
const sdk = await initializeSdk();
|
|
14
29
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
30
|
+
// Handle "all" or "*" type
|
|
31
|
+
if (type === 'all' || type === '*') {
|
|
32
|
+
logger.section(`Pulling all resources to ${targetPath}`);
|
|
33
|
+
|
|
34
|
+
const types = [
|
|
35
|
+
{ name: 'components', fn: pullComponents },
|
|
36
|
+
{ name: 'blueprints', fn: pullBlueprints },
|
|
37
|
+
{ name: 'configs', fn: pullConfigurations },
|
|
38
|
+
{ name: 'plugins', fn: pullPlugins },
|
|
39
|
+
{ name: 'blocks', fn: pullBlocks }
|
|
40
|
+
];
|
|
19
41
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
42
|
+
for (const { name, fn } of types) {
|
|
43
|
+
const typePath = path.join(targetPath, name);
|
|
44
|
+
logger.section(`Pulling ${name} to ${typePath}`);
|
|
45
|
+
try {
|
|
46
|
+
await fn(sdk, typePath);
|
|
47
|
+
logger.success(`Successfully pulled ${name}`);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
logger.error(`Failed to pull ${name}`, error);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
logger.success(`Successfully pulled all resources to ${targetPath}`);
|
|
54
|
+
return;
|
|
26
55
|
}
|
|
27
56
|
|
|
28
|
-
|
|
29
|
-
console.log(`Found ${components.length} components to pull`)
|
|
57
|
+
logger.section(`Pulling ${type} to ${targetPath}`);
|
|
30
58
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
59
|
+
if (type === 'components') {
|
|
60
|
+
await pullComponents(sdk, targetPath);
|
|
61
|
+
} else if (type === 'blueprints') {
|
|
62
|
+
await pullBlueprints(sdk, targetPath);
|
|
63
|
+
} else if (type === 'plugins') {
|
|
64
|
+
await pullPlugins(sdk, targetPath);
|
|
65
|
+
} else if (type === 'blocks') {
|
|
66
|
+
await pullBlocks(sdk, targetPath);
|
|
67
|
+
} else if (type === 'config' || type === 'configs' || type === 'configuration') {
|
|
68
|
+
await pullConfigurations(sdk, targetPath);
|
|
69
|
+
} else {
|
|
70
|
+
logger.error(`Unknown type: ${type}`);
|
|
71
|
+
logger.info('Supported types: components, blueprints, plugins, blocks, config, configs, configuration, all');
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
34
74
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
fs.writeFileSync(filePath, content, 'utf-8')
|
|
38
|
-
console.log('Pulled component:', component.identifier)
|
|
39
|
-
}))
|
|
75
|
+
logger.success(`Successfully pulled ${type} to ${targetPath}`);
|
|
40
76
|
|
|
41
|
-
|
|
77
|
+
} catch (error) {
|
|
78
|
+
logger.error(`Failed to pull ${type}`, error);
|
|
79
|
+
process.exit(1);
|
|
42
80
|
}
|
|
43
81
|
}
|
package/controllers/push.mjs
CHANGED
|
@@ -1,47 +1,98 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
1
|
+
import { initializeSdk } from '../services/sdk.mjs';
|
|
2
|
+
import { pushComponents } from '../services/components.mjs';
|
|
3
|
+
import { pushBlueprints } from '../services/blueprints.mjs';
|
|
4
|
+
import { pushConfigurations } from '../services/configurations.mjs';
|
|
5
|
+
import { pushPlugins } from '../services/plugins.mjs';
|
|
6
|
+
import { pushBlocks } from '../services/blocks.mjs';
|
|
7
|
+
import { logger } from '../services/logger.mjs';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
export default async function pushController({ type, path: sourcePath }) {
|
|
12
|
+
try {
|
|
13
|
+
// Validate path exists
|
|
14
|
+
if (!fs.existsSync(sourcePath)) {
|
|
15
|
+
logger.error(`Path does not exist: ${sourcePath}`);
|
|
16
|
+
logger.info('Please provide a valid directory path');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Validate path is a directory
|
|
21
|
+
if (!fs.statSync(sourcePath).isDirectory()) {
|
|
22
|
+
logger.error(`Path is not a directory: ${sourcePath}`);
|
|
23
|
+
logger.info('Please provide a directory path, not a file');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const sdk = await initializeSdk();
|
|
28
|
+
|
|
29
|
+
// Handle "all" or "*" type
|
|
30
|
+
if (type === 'all' || type === '*') {
|
|
31
|
+
logger.section(`Pushing all resources from ${sourcePath}`);
|
|
32
|
+
|
|
33
|
+
const types = [
|
|
34
|
+
{ name: 'components', fn: pushComponents },
|
|
35
|
+
{ name: 'blueprints', fn: pushBlueprints },
|
|
36
|
+
{ name: 'configs', fn: pushConfigurations },
|
|
37
|
+
{ name: 'plugins', fn: pushPlugins },
|
|
38
|
+
{ name: 'blocks', fn: pushBlocks }
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
for (const { name, fn } of types) {
|
|
42
|
+
const typePath = path.join(sourcePath, name);
|
|
43
|
+
|
|
44
|
+
// Skip if directory doesn't exist
|
|
45
|
+
if (!fs.existsSync(typePath)) {
|
|
46
|
+
logger.info(`Skipping ${name} (directory not found: ${typePath})`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
logger.section(`Pushing ${name} from ${typePath}`);
|
|
51
|
+
try {
|
|
52
|
+
await fn(sdk, typePath);
|
|
53
|
+
logger.success(`Successfully pushed ${name}`);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
logger.error(`Failed to push ${name}`, error);
|
|
42
56
|
}
|
|
43
57
|
}
|
|
44
|
-
|
|
45
|
-
|
|
58
|
+
|
|
59
|
+
logger.success(`Successfully pushed all resources from ${sourcePath}`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
logger.section(`Pushing ${type} from ${sourcePath}`);
|
|
64
|
+
|
|
65
|
+
if (type === 'components') {
|
|
66
|
+
await pushComponents(sdk, sourcePath);
|
|
67
|
+
} else if (type === 'blueprints') {
|
|
68
|
+
await pushBlueprints(sdk, sourcePath);
|
|
69
|
+
} else if (type === 'plugins') {
|
|
70
|
+
await pushPlugins(sdk, sourcePath);
|
|
71
|
+
} else if (type === 'blocks') {
|
|
72
|
+
await pushBlocks(sdk, sourcePath);
|
|
73
|
+
} else if (type === 'config' || type === 'configs' || type === 'configuration') {
|
|
74
|
+
await pushConfigurations(sdk, sourcePath);
|
|
75
|
+
} else {
|
|
76
|
+
logger.error(`Unknown type: ${type}`);
|
|
77
|
+
logger.info('Supported types: components, blueprints, plugins, blocks, config, configs, configuration, all');
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
logger.success(`Successfully pushed ${type}`);
|
|
82
|
+
|
|
83
|
+
} catch (error) {
|
|
84
|
+
// Don't log the error again if it's already been logged by the service
|
|
85
|
+
if (!error.message?.includes('Failed to push')) {
|
|
86
|
+
logger.error(`Failed to push ${type}`, error);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (process.env.VERBOSE) {
|
|
90
|
+
console.error('\nStack trace:');
|
|
91
|
+
console.error(error.stack);
|
|
92
|
+
} else {
|
|
93
|
+
console.error('\nRun with VERBOSE=true for full stack trace');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
process.exit(1);
|
|
46
97
|
}
|
|
47
98
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { logger } from './logger.mjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Convert string to kebab-case
|
|
7
|
+
*/
|
|
8
|
+
function toKebabCase(str) {
|
|
9
|
+
return str
|
|
10
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
11
|
+
.replace(/[\s_]+/g, '-')
|
|
12
|
+
.toLowerCase();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Push blocks from local directory to remote
|
|
17
|
+
* @param {Object} sdk - Initialized SDK instance
|
|
18
|
+
* @param {string} path - Path to blocks directory
|
|
19
|
+
*/
|
|
20
|
+
export async function pushBlocks(sdk, path) {
|
|
21
|
+
const files = fs.readdirSync(path);
|
|
22
|
+
const blockFiles = files.filter(f => f.endsWith('.html'));
|
|
23
|
+
|
|
24
|
+
if (blockFiles.length === 0) {
|
|
25
|
+
logger.warning(`No .html files found in ${path}`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
logger.info(`Found ${blockFiles.length} block(s) to push`);
|
|
30
|
+
let blocksJson = {};
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const jsonPath = join(path, 'blocks.json');
|
|
34
|
+
if (fs.existsSync(jsonPath)) {
|
|
35
|
+
blocksJson = JSON.parse(fs.readFileSync(jsonPath, 'utf-8'));
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
logger.debug('No blocks.json found or invalid format');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const existingBlocks = await sdk.blocks.getList();
|
|
42
|
+
const updatedBlocksJson = { ...blocksJson };
|
|
43
|
+
|
|
44
|
+
await Promise.all(blockFiles.map(async (file) => {
|
|
45
|
+
const fileName = file.replace('.html', '');
|
|
46
|
+
const info = blocksJson[fileName] || {};
|
|
47
|
+
const content = fs.readFileSync(join(path, file), 'utf-8');
|
|
48
|
+
|
|
49
|
+
logger.step(`Pushing block: ${fileName}`);
|
|
50
|
+
|
|
51
|
+
// First check if we have an _id in blocks.json
|
|
52
|
+
let blockId = info._id;
|
|
53
|
+
|
|
54
|
+
// If we have an _id, try to update directly
|
|
55
|
+
if (blockId) {
|
|
56
|
+
try {
|
|
57
|
+
await sdk.blocks.update(blockId, {
|
|
58
|
+
name: info.name || fileName,
|
|
59
|
+
content,
|
|
60
|
+
contentType: info.contentType || 'html',
|
|
61
|
+
description: info.description || 'Block description'
|
|
62
|
+
});
|
|
63
|
+
logger.success(`Updated: ${fileName}`);
|
|
64
|
+
return;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
// If update fails, the block might have been deleted, so we'll create a new one
|
|
67
|
+
logger.debug(`Block ${fileName} not found by _id, will search or create`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Try to find existing block by name
|
|
72
|
+
const existingBlock = existingBlocks.find(
|
|
73
|
+
block => toKebabCase(block.name) === fileName
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
if (existingBlock) {
|
|
77
|
+
await sdk.blocks.update(existingBlock._id, {
|
|
78
|
+
name: info.name || existingBlock.name,
|
|
79
|
+
content,
|
|
80
|
+
contentType: info.contentType || 'html',
|
|
81
|
+
description: info.description || existingBlock.description || 'Block description'
|
|
82
|
+
});
|
|
83
|
+
updatedBlocksJson[fileName] = {
|
|
84
|
+
...info,
|
|
85
|
+
_id: existingBlock._id
|
|
86
|
+
};
|
|
87
|
+
logger.success(`Updated: ${fileName}`);
|
|
88
|
+
} else {
|
|
89
|
+
const newBlock = await sdk.blocks.create({
|
|
90
|
+
name: info.name || fileName,
|
|
91
|
+
content,
|
|
92
|
+
contentType: info.contentType || 'html',
|
|
93
|
+
description: info.description || 'Block description'
|
|
94
|
+
});
|
|
95
|
+
updatedBlocksJson[fileName] = {
|
|
96
|
+
...info,
|
|
97
|
+
_id: newBlock._id,
|
|
98
|
+
name: newBlock.name
|
|
99
|
+
};
|
|
100
|
+
logger.success(`Created: ${fileName}`);
|
|
101
|
+
}
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
// Update blocks.json with new _ids
|
|
105
|
+
fs.writeFileSync(
|
|
106
|
+
join(path, 'blocks.json'),
|
|
107
|
+
JSON.stringify(updatedBlocksJson, null, 2)
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
logger.info(`Pushed ${blockFiles.length} block(s)`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Pull blocks from remote to local directory
|
|
115
|
+
* @param {Object} sdk - Initialized SDK instance
|
|
116
|
+
* @param {string} targetPath - Path to save blocks
|
|
117
|
+
*/
|
|
118
|
+
export async function pullBlocks(sdk, targetPath) {
|
|
119
|
+
// Create directory if it doesn't exist
|
|
120
|
+
if (!fs.existsSync(targetPath)) {
|
|
121
|
+
fs.mkdirSync(targetPath, { recursive: true });
|
|
122
|
+
logger.info(`Created directory: ${targetPath}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const blocks = await sdk.blocks.getList();
|
|
126
|
+
|
|
127
|
+
if (blocks.length === 0) {
|
|
128
|
+
logger.warning('No blocks found to pull');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
logger.info(`Found ${blocks.length} block(s) to pull`);
|
|
133
|
+
|
|
134
|
+
const blocksInformation = await Promise.all(blocks.map(async (block) => {
|
|
135
|
+
const fileName = toKebabCase(block.name);
|
|
136
|
+
const filePath = join(targetPath, `${fileName}.html`);
|
|
137
|
+
|
|
138
|
+
const blockDetails = await sdk.blocks.getBlock(block._id);
|
|
139
|
+
|
|
140
|
+
fs.writeFileSync(filePath, blockDetails.content, 'utf-8');
|
|
141
|
+
logger.step(`Pulled: ${block.name}`);
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
_id: block._id,
|
|
145
|
+
name: block.name,
|
|
146
|
+
description: blockDetails.description,
|
|
147
|
+
contentType: blockDetails.contentType,
|
|
148
|
+
};
|
|
149
|
+
}));
|
|
150
|
+
|
|
151
|
+
fs.writeFileSync(
|
|
152
|
+
join(targetPath, 'blocks.json'),
|
|
153
|
+
JSON.stringify(
|
|
154
|
+
blocksInformation.reduce((obj, current) => {
|
|
155
|
+
const fileName = toKebabCase(current.name);
|
|
156
|
+
obj[fileName] = current;
|
|
157
|
+
return obj;
|
|
158
|
+
}, {}),
|
|
159
|
+
null,
|
|
160
|
+
2
|
|
161
|
+
)
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
logger.info(`Saved blocks.json with metadata`);
|
|
165
|
+
logger.info(`Pulled ${blocks.length} block(s)`);
|
|
166
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { logger } from './logger.mjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Push blueprints from local directory to remote
|
|
7
|
+
* @param {Object} sdk - Initialized SDK instance
|
|
8
|
+
* @param {string} path - Path to blueprints directory
|
|
9
|
+
*/
|
|
10
|
+
export async function pushBlueprints(sdk, path) {
|
|
11
|
+
const files = fs.readdirSync(path);
|
|
12
|
+
const blueprintFiles = files.filter(f => f.endsWith('.blueprint.json'));
|
|
13
|
+
|
|
14
|
+
if (blueprintFiles.length === 0) {
|
|
15
|
+
logger.warning(`No blueprint files (*.blueprint.json) found in ${path}`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
logger.info(`Found ${blueprintFiles.length} blueprint(s) to push`);
|
|
20
|
+
const existingBlueprints = await sdk.manageBlueprints.getList();
|
|
21
|
+
|
|
22
|
+
const results = await Promise.allSettled(blueprintFiles.map(async (file) => {
|
|
23
|
+
if (file.endsWith('.blueprint.json')) {
|
|
24
|
+
let blueprintData;
|
|
25
|
+
try {
|
|
26
|
+
blueprintData = JSON.parse(fs.readFileSync(join(path, file), 'utf-8'));
|
|
27
|
+
} catch (error) {
|
|
28
|
+
logger.error(`Failed to parse ${file}`, error);
|
|
29
|
+
throw new Error(`Parse error in ${file}: ${error.message}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const identifier = blueprintData.identifier;
|
|
33
|
+
|
|
34
|
+
if (!identifier) {
|
|
35
|
+
logger.warning(`Skipping ${file}: missing identifier field`);
|
|
36
|
+
return { skipped: true, file };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
logger.step(`Pushing blueprint: ${identifier}`);
|
|
40
|
+
|
|
41
|
+
const existingBlueprint = existingBlueprints.find(
|
|
42
|
+
blueprint => blueprint.identifier === identifier
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
if (existingBlueprint) {
|
|
47
|
+
await sdk.manageBlueprints.update(identifier, blueprintData);
|
|
48
|
+
logger.success(`Updated: ${identifier}`);
|
|
49
|
+
} else {
|
|
50
|
+
await sdk.manageBlueprints.create(blueprintData);
|
|
51
|
+
logger.success(`Created: ${identifier}`);
|
|
52
|
+
}
|
|
53
|
+
return { success: true, identifier };
|
|
54
|
+
} catch (error) {
|
|
55
|
+
// Extract detailed error information
|
|
56
|
+
let errorMessage = error.message || 'Unknown error';
|
|
57
|
+
let errorDetails = null;
|
|
58
|
+
|
|
59
|
+
// The SDK throws the response body as the error
|
|
60
|
+
if (typeof error === 'object' && error !== null) {
|
|
61
|
+
if (error.message) {
|
|
62
|
+
errorMessage = error.message;
|
|
63
|
+
}
|
|
64
|
+
if (error.error) {
|
|
65
|
+
errorDetails = error.error;
|
|
66
|
+
}
|
|
67
|
+
if (error.errors) {
|
|
68
|
+
errorDetails = error.errors;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
logger.error(`Failed to push ${identifier}: ${errorMessage}`);
|
|
73
|
+
|
|
74
|
+
if (errorDetails) {
|
|
75
|
+
if (typeof errorDetails === 'string') {
|
|
76
|
+
logger.error(` Details: ${errorDetails}`);
|
|
77
|
+
} else {
|
|
78
|
+
logger.error(` Details: ${JSON.stringify(errorDetails, null, 2)}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (process.env.VERBOSE && error.stack) {
|
|
83
|
+
logger.debug(`Stack: ${error.stack}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
throw new Error(`Failed to push ${identifier}: ${errorMessage}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
// Check for failures
|
|
92
|
+
const failures = results.filter(r => r.status === 'rejected');
|
|
93
|
+
const successes = results.filter(r => r.status === 'fulfilled' && r.value?.success);
|
|
94
|
+
|
|
95
|
+
if (failures.length > 0) {
|
|
96
|
+
logger.error(`\n${failures.length} blueprint(s) failed to push:`);
|
|
97
|
+
failures.forEach(f => {
|
|
98
|
+
logger.error(` • ${f.reason.message}`);
|
|
99
|
+
});
|
|
100
|
+
throw new Error(`Failed to push ${failures.length} blueprint(s)`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
logger.info(`Pushed ${blueprintFiles.length} blueprint(s)`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Pull blueprints from remote to local directory
|
|
108
|
+
* @param {Object} sdk - Initialized SDK instance
|
|
109
|
+
* @param {string} targetPath - Path to save blueprints
|
|
110
|
+
*/
|
|
111
|
+
export async function pullBlueprints(sdk, targetPath) {
|
|
112
|
+
// Create directory if it doesn't exist
|
|
113
|
+
if (!fs.existsSync(targetPath)) {
|
|
114
|
+
fs.mkdirSync(targetPath, { recursive: true });
|
|
115
|
+
logger.info(`Created directory: ${targetPath}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const blueprints = await sdk.manageBlueprints.getList();
|
|
119
|
+
|
|
120
|
+
if (blueprints.length === 0) {
|
|
121
|
+
logger.warning('No blueprints found to pull');
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
logger.info(`Found ${blueprints.length} blueprint(s) to pull`);
|
|
126
|
+
|
|
127
|
+
await Promise.all(blueprints.map(async (blueprint) => {
|
|
128
|
+
const fileName = `${blueprint.identifier}.blueprint.json`;
|
|
129
|
+
const filePath = join(targetPath, fileName);
|
|
130
|
+
|
|
131
|
+
// Fetch full blueprint details
|
|
132
|
+
const fullBlueprint = await sdk.manageBlueprints.getBlueprint(blueprint.identifier);
|
|
133
|
+
|
|
134
|
+
function removeIdFromObject(obj) {
|
|
135
|
+
const { _id, ...rest } = obj;
|
|
136
|
+
return rest;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const relevantFields = {
|
|
140
|
+
identifier: fullBlueprint.identifier,
|
|
141
|
+
name: fullBlueprint.name,
|
|
142
|
+
description: fullBlueprint.description,
|
|
143
|
+
properties: fullBlueprint.properties,
|
|
144
|
+
relations: fullBlueprint.relations,
|
|
145
|
+
dispatchers: fullBlueprint.dispatchers,
|
|
146
|
+
permissions: (fullBlueprint.permissions || []).map(removeIdFromObject),
|
|
147
|
+
permissionScope: fullBlueprint.permissionScope,
|
|
148
|
+
entityIdentifierMechanism: fullBlueprint.entityIdentifierMechanism,
|
|
149
|
+
limitations: (fullBlueprint.limitations || []).map(removeIdFromObject),
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
fs.writeFileSync(filePath, JSON.stringify(relevantFields, null, 2), 'utf-8');
|
|
153
|
+
logger.step(`Pulled: ${blueprint.identifier}`);
|
|
154
|
+
}));
|
|
155
|
+
|
|
156
|
+
logger.info(`Pulled ${blueprints.length} blueprint(s)`);
|
|
157
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { logger } from './logger.mjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Push components from local directory to remote
|
|
7
|
+
* @param {Object} sdk - Initialized SDK instance
|
|
8
|
+
* @param {string} path - Path to components directory
|
|
9
|
+
*/
|
|
10
|
+
export async function pushComponents(sdk, path) {
|
|
11
|
+
const files = fs.readdirSync(path);
|
|
12
|
+
const vueFiles = files.filter(f => f.endsWith('.vue'));
|
|
13
|
+
|
|
14
|
+
if (vueFiles.length === 0) {
|
|
15
|
+
logger.warning(`No .vue files found in ${path}`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
logger.info(`Found ${vueFiles.length} component(s) to push`);
|
|
20
|
+
let componentsJson = {};
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const jsonPath = join(path, 'components.json');
|
|
24
|
+
if (fs.existsSync(jsonPath)) {
|
|
25
|
+
componentsJson = JSON.parse(fs.readFileSync(jsonPath));
|
|
26
|
+
}
|
|
27
|
+
} catch (error) {
|
|
28
|
+
logger.debug('No components.json found or invalid format');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const existingComponents = await sdk.components.getList();
|
|
32
|
+
|
|
33
|
+
await Promise.all(files.map(async (file) => {
|
|
34
|
+
if (file.endsWith('.vue')) {
|
|
35
|
+
const componentName = file.replace('.vue', '');
|
|
36
|
+
const info = componentsJson[componentName] || {};
|
|
37
|
+
const content = fs.readFileSync(join(path, file), 'utf-8');
|
|
38
|
+
|
|
39
|
+
logger.step(`Pushing component: ${componentName}`);
|
|
40
|
+
|
|
41
|
+
const existingComponent = existingComponents.find(
|
|
42
|
+
component => component.identifier === componentName
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
if (existingComponent) {
|
|
46
|
+
await sdk.components.update(existingComponent._id, {
|
|
47
|
+
identifier: info.identifier || existingComponent.identifier || componentName,
|
|
48
|
+
componentName: componentName,
|
|
49
|
+
content,
|
|
50
|
+
description: info.description || existingComponent.description || 'Component description'
|
|
51
|
+
});
|
|
52
|
+
logger.success(`Updated: ${componentName}`);
|
|
53
|
+
} else {
|
|
54
|
+
await sdk.components.create({
|
|
55
|
+
identifier: info.identifier || componentName,
|
|
56
|
+
componentName: componentName,
|
|
57
|
+
content,
|
|
58
|
+
description: info.description || 'Component description'
|
|
59
|
+
});
|
|
60
|
+
logger.success(`Created: ${componentName}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
logger.info(`Pushed ${vueFiles.length} component(s)`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Pull components from remote to local directory
|
|
70
|
+
* @param {Object} sdk - Initialized SDK instance
|
|
71
|
+
* @param {string} targetPath - Path to save components
|
|
72
|
+
*/
|
|
73
|
+
export async function pullComponents(sdk, targetPath) {
|
|
74
|
+
// Create directory if it doesn't exist
|
|
75
|
+
if (!fs.existsSync(targetPath)) {
|
|
76
|
+
fs.mkdirSync(targetPath, { recursive: true });
|
|
77
|
+
logger.info(`Created directory: ${targetPath}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const components = await sdk.components.getList();
|
|
81
|
+
|
|
82
|
+
if (components.length === 0) {
|
|
83
|
+
logger.warning('No components found to pull');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
logger.info(`Found ${components.length} component(s) to pull`);
|
|
88
|
+
|
|
89
|
+
const componentsInformation = await Promise.all(components.map(async (component) => {
|
|
90
|
+
const fileName = `${component.componentName}.vue`;
|
|
91
|
+
const filePath = join(targetPath, fileName);
|
|
92
|
+
|
|
93
|
+
const { content, description } = await sdk.components.getComponent(component._id);
|
|
94
|
+
|
|
95
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
96
|
+
logger.step(`Pulled: ${component.identifier}`);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
_id: component._id,
|
|
100
|
+
componentName: component.componentName,
|
|
101
|
+
identifier: component.identifier,
|
|
102
|
+
description,
|
|
103
|
+
};
|
|
104
|
+
}));
|
|
105
|
+
|
|
106
|
+
fs.writeFileSync(
|
|
107
|
+
join(targetPath, 'components.json'),
|
|
108
|
+
JSON.stringify(
|
|
109
|
+
componentsInformation.reduce((obj, current) => {
|
|
110
|
+
obj[current.componentName] = current;
|
|
111
|
+
return obj;
|
|
112
|
+
}, {}),
|
|
113
|
+
null,
|
|
114
|
+
2
|
|
115
|
+
)
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
logger.info(`Saved components.json with metadata`);
|
|
119
|
+
logger.info(`Pulled ${components.length} component(s)`);
|
|
120
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { logger } from './logger.mjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Push configurations from local directory to remote
|
|
7
|
+
* @param {Object} sdk - Initialized SDK instance
|
|
8
|
+
* @param {string} path - Path to configurations directory
|
|
9
|
+
*/
|
|
10
|
+
export async function pushConfigurations(sdk, path) {
|
|
11
|
+
const files = fs.readdirSync(path);
|
|
12
|
+
const configFiles = files.filter(f => f.endsWith('.config.json'));
|
|
13
|
+
|
|
14
|
+
if (configFiles.length === 0) {
|
|
15
|
+
logger.warning(`No configuration files (*.config.json) found in ${path}`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
logger.info(`Found ${configFiles.length} configuration(s) to push`);
|
|
20
|
+
const existingConfigurations = await sdk.manageConfigurations.getList();
|
|
21
|
+
|
|
22
|
+
const results = await Promise.allSettled(configFiles.map(async (file) => {
|
|
23
|
+
if (file.endsWith('.config.json')) {
|
|
24
|
+
let configData;
|
|
25
|
+
try {
|
|
26
|
+
configData = JSON.parse(fs.readFileSync(join(path, file), 'utf-8'));
|
|
27
|
+
} catch (error) {
|
|
28
|
+
logger.error(`Failed to parse ${file}`, error);
|
|
29
|
+
throw new Error(`Parse error in ${file}: ${error.message}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const key = configData.key;
|
|
33
|
+
|
|
34
|
+
if (!key) {
|
|
35
|
+
logger.warning(`Skipping ${file}: missing key field`);
|
|
36
|
+
return { skipped: true, file };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
logger.step(`Pushing configuration: ${key}`);
|
|
40
|
+
|
|
41
|
+
const existingConfig = existingConfigurations.find(
|
|
42
|
+
config => config.key === key
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
if (existingConfig) {
|
|
47
|
+
await sdk.manageConfigurations.update(key, configData);
|
|
48
|
+
logger.success(`Updated: ${key}`);
|
|
49
|
+
} else {
|
|
50
|
+
await sdk.manageConfigurations.create(configData);
|
|
51
|
+
logger.success(`Created: ${key}`);
|
|
52
|
+
}
|
|
53
|
+
return { success: true, key };
|
|
54
|
+
} catch (error) {
|
|
55
|
+
// Extract detailed error information
|
|
56
|
+
let errorMessage = error.message || 'Unknown error';
|
|
57
|
+
let errorDetails = null;
|
|
58
|
+
|
|
59
|
+
// The SDK throws the response body as the error
|
|
60
|
+
if (typeof error === 'object' && error !== null) {
|
|
61
|
+
if (error.message) {
|
|
62
|
+
errorMessage = error.message;
|
|
63
|
+
}
|
|
64
|
+
if (error.error) {
|
|
65
|
+
errorDetails = error.error;
|
|
66
|
+
}
|
|
67
|
+
if (error.errors) {
|
|
68
|
+
errorDetails = error.errors;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
logger.error(`Failed to push ${key}: ${errorMessage}`);
|
|
73
|
+
|
|
74
|
+
if (errorDetails) {
|
|
75
|
+
if (typeof errorDetails === 'string') {
|
|
76
|
+
logger.error(` Details: ${errorDetails}`);
|
|
77
|
+
} else {
|
|
78
|
+
logger.error(` Details: ${JSON.stringify(errorDetails, null, 2)}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (process.env.VERBOSE && error.stack) {
|
|
83
|
+
logger.debug(`Stack: ${error.stack}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
throw new Error(`Failed to push ${key}: ${errorMessage}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
// Check for failures
|
|
92
|
+
const failures = results.filter(r => r.status === 'rejected');
|
|
93
|
+
const successes = results.filter(r => r.status === 'fulfilled' && r.value?.success);
|
|
94
|
+
|
|
95
|
+
if (failures.length > 0) {
|
|
96
|
+
logger.error(`\n${failures.length} configuration(s) failed to push:`);
|
|
97
|
+
failures.forEach(f => {
|
|
98
|
+
logger.error(` • ${f.reason.message}`);
|
|
99
|
+
});
|
|
100
|
+
throw new Error(`Failed to push ${failures.length} configuration(s)`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
logger.info(`Pushed ${configFiles.length} configuration(s)`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Pull configurations from remote to local directory
|
|
108
|
+
* @param {Object} sdk - Initialized SDK instance
|
|
109
|
+
* @param {string} targetPath - Path to save configurations
|
|
110
|
+
*/
|
|
111
|
+
export async function pullConfigurations(sdk, targetPath) {
|
|
112
|
+
// Create directory if it doesn't exist
|
|
113
|
+
if (!fs.existsSync(targetPath)) {
|
|
114
|
+
fs.mkdirSync(targetPath, { recursive: true });
|
|
115
|
+
logger.info(`Created directory: ${targetPath}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const configurations = await sdk.manageConfigurations.getList();
|
|
119
|
+
|
|
120
|
+
if (configurations.length === 0) {
|
|
121
|
+
logger.warning('No configurations found to pull');
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
logger.info(`Found ${configurations.length} configuration(s) to pull`);
|
|
126
|
+
|
|
127
|
+
await Promise.all(configurations.map(async (config) => {
|
|
128
|
+
const fileName = `${config.key}.config.json`;
|
|
129
|
+
const filePath = join(targetPath, fileName);
|
|
130
|
+
|
|
131
|
+
// Fetch full configuration details
|
|
132
|
+
const fullConfig = await sdk.manageConfigurations.getConfiguration(config.key);
|
|
133
|
+
|
|
134
|
+
// Remove fields that shouldn't be in the file
|
|
135
|
+
const { _id, tenant, created, updated, ...relevantFields } = fullConfig;
|
|
136
|
+
|
|
137
|
+
fs.writeFileSync(filePath, JSON.stringify(relevantFields, null, 2), 'utf-8');
|
|
138
|
+
logger.step(`Pulled: ${config.key}`);
|
|
139
|
+
}));
|
|
140
|
+
|
|
141
|
+
logger.info(`Pulled ${configurations.length} configuration(s)`);
|
|
142
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger utility for CLI with colored output and consistent formatting
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const colors = {
|
|
6
|
+
reset: '\x1b[0m',
|
|
7
|
+
bright: '\x1b[1m',
|
|
8
|
+
dim: '\x1b[2m',
|
|
9
|
+
red: '\x1b[31m',
|
|
10
|
+
green: '\x1b[32m',
|
|
11
|
+
yellow: '\x1b[33m',
|
|
12
|
+
blue: '\x1b[34m',
|
|
13
|
+
cyan: '\x1b[36m',
|
|
14
|
+
gray: '\x1b[90m',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const logger = {
|
|
18
|
+
success(message) {
|
|
19
|
+
console.log(`${colors.green}✓${colors.reset} ${message}`);
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
error(message, error = null) {
|
|
23
|
+
console.error(`${colors.red}✗ Error:${colors.reset} ${message}`);
|
|
24
|
+
if (error && process.env.VERBOSE) {
|
|
25
|
+
console.error(`${colors.gray}${error.stack || error}${colors.reset}`);
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
warning(message) {
|
|
30
|
+
console.warn(`${colors.yellow}⚠ Warning:${colors.reset} ${message}`);
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
info(message) {
|
|
34
|
+
console.log(`${colors.blue}ℹ${colors.reset} ${message}`);
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
debug(message) {
|
|
38
|
+
if (process.env.VERBOSE) {
|
|
39
|
+
console.log(`${colors.gray}[DEBUG]${colors.reset} ${message}`);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
step(message) {
|
|
44
|
+
console.log(`${colors.cyan}→${colors.reset} ${message}`);
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
section(title) {
|
|
48
|
+
console.log(`\n${colors.bright}${title}${colors.reset}`);
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Format and display a connection error with helpful context
|
|
53
|
+
*/
|
|
54
|
+
connectionError(url, error) {
|
|
55
|
+
console.error(`\n${colors.red}✗ Connection Failed${colors.reset}`);
|
|
56
|
+
console.error(`${colors.dim}Unable to connect to: ${colors.reset}${url}`);
|
|
57
|
+
|
|
58
|
+
if (error.code === 'UND_ERR_CONNECT_TIMEOUT') {
|
|
59
|
+
console.error(`${colors.yellow}Reason:${colors.reset} Connection timeout (10s)`);
|
|
60
|
+
console.error(`\n${colors.cyan}Suggestions:${colors.reset}`);
|
|
61
|
+
console.error(` • Check if the server is running and accessible`);
|
|
62
|
+
console.error(` • Verify the QELOS_URL is correct: ${url}`);
|
|
63
|
+
console.error(` • Check your network connection`);
|
|
64
|
+
console.error(` • Ensure there are no firewall rules blocking the connection`);
|
|
65
|
+
} else if (error.code === 'ENOTFOUND') {
|
|
66
|
+
console.error(`${colors.yellow}Reason:${colors.reset} Domain not found`);
|
|
67
|
+
console.error(`\n${colors.cyan}Suggestions:${colors.reset}`);
|
|
68
|
+
console.error(` • Verify the QELOS_URL is correct: ${url}`);
|
|
69
|
+
console.error(` • Check if the domain exists and is accessible`);
|
|
70
|
+
} else if (error.code === 'ECONNREFUSED') {
|
|
71
|
+
console.error(`${colors.yellow}Reason:${colors.reset} Connection refused`);
|
|
72
|
+
console.error(`\n${colors.cyan}Suggestions:${colors.reset}`);
|
|
73
|
+
console.error(` • Check if the server is running on ${url}`);
|
|
74
|
+
console.error(` • Verify the port number is correct`);
|
|
75
|
+
} else {
|
|
76
|
+
console.error(`${colors.yellow}Reason:${colors.reset} ${error.message}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (process.env.VERBOSE) {
|
|
80
|
+
console.error(`\n${colors.gray}Full error:${colors.reset}`);
|
|
81
|
+
console.error(`${colors.gray}${error.stack}${colors.reset}`);
|
|
82
|
+
} else {
|
|
83
|
+
console.error(`\n${colors.dim}Run with VERBOSE=true for more details${colors.reset}`);
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Format and display an authentication error
|
|
89
|
+
*/
|
|
90
|
+
authError(username, url) {
|
|
91
|
+
console.error(`\n${colors.red}✗ Authentication Failed${colors.reset}`);
|
|
92
|
+
console.error(`${colors.dim}Unable to authenticate user: ${colors.reset}${username}`);
|
|
93
|
+
console.error(`${colors.dim}Server: ${colors.reset}${url}`);
|
|
94
|
+
console.error(`\n${colors.cyan}Suggestions:${colors.reset}`);
|
|
95
|
+
console.error(` • Verify QELOS_USERNAME is correct: ${username}`);
|
|
96
|
+
console.error(` • Check if QELOS_PASSWORD is correct`);
|
|
97
|
+
console.error(` • Ensure the user account exists and is active`);
|
|
98
|
+
console.error(` • Verify you have the necessary permissions`);
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Display environment configuration
|
|
103
|
+
*/
|
|
104
|
+
showConfig(config) {
|
|
105
|
+
console.log(`\n${colors.bright}Configuration:${colors.reset}`);
|
|
106
|
+
Object.entries(config).forEach(([key, value]) => {
|
|
107
|
+
const displayValue = key.toLowerCase().includes('password') ? '***' : value;
|
|
108
|
+
console.log(` ${colors.cyan}${key}:${colors.reset} ${displayValue}`);
|
|
109
|
+
});
|
|
110
|
+
console.log('');
|
|
111
|
+
}
|
|
112
|
+
};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { logger } from './logger.mjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Push plugins from local directory to remote
|
|
7
|
+
* @param {Object} sdk - Initialized SDK instance
|
|
8
|
+
* @param {string} path - Path to plugins directory
|
|
9
|
+
*/
|
|
10
|
+
export async function pushPlugins(sdk, path) {
|
|
11
|
+
const files = fs.readdirSync(path);
|
|
12
|
+
const pluginFiles = files.filter(f => f.endsWith('.plugin.json'));
|
|
13
|
+
|
|
14
|
+
if (pluginFiles.length === 0) {
|
|
15
|
+
logger.warning(`No plugin files (*.plugin.json) found in ${path}`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
logger.info(`Found ${pluginFiles.length} plugin(s) to push`);
|
|
20
|
+
const existingPlugins = await sdk.managePlugins.getList();
|
|
21
|
+
|
|
22
|
+
const results = await Promise.allSettled(pluginFiles.map(async (file) => {
|
|
23
|
+
if (file.endsWith('.plugin.json')) {
|
|
24
|
+
let pluginData;
|
|
25
|
+
try {
|
|
26
|
+
pluginData = JSON.parse(fs.readFileSync(join(path, file), 'utf-8'));
|
|
27
|
+
} catch (error) {
|
|
28
|
+
logger.error(`Failed to parse ${file}`, error);
|
|
29
|
+
throw new Error(`Parse error in ${file}: ${error.message}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const apiPath = pluginData.apiPath;
|
|
33
|
+
|
|
34
|
+
if (!apiPath) {
|
|
35
|
+
logger.warning(`Skipping ${file}: missing apiPath field`);
|
|
36
|
+
return { skipped: true, file };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
logger.step(`Pushing plugin: ${apiPath}`);
|
|
40
|
+
|
|
41
|
+
const existingPlugin = existingPlugins.find(
|
|
42
|
+
plugin => plugin.apiPath === apiPath
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
if (existingPlugin) {
|
|
47
|
+
await sdk.managePlugins.update(existingPlugin._id, pluginData);
|
|
48
|
+
logger.success(`Updated: ${apiPath}`);
|
|
49
|
+
} else {
|
|
50
|
+
await sdk.managePlugins.create(pluginData);
|
|
51
|
+
logger.success(`Created: ${apiPath}`);
|
|
52
|
+
}
|
|
53
|
+
return { success: true, apiPath };
|
|
54
|
+
} catch (error) {
|
|
55
|
+
// Extract detailed error information
|
|
56
|
+
let errorMessage = error.message || 'Unknown error';
|
|
57
|
+
let errorDetails = null;
|
|
58
|
+
|
|
59
|
+
// The SDK throws the response body as the error
|
|
60
|
+
if (typeof error === 'object' && error !== null) {
|
|
61
|
+
if (error.message) {
|
|
62
|
+
errorMessage = error.message;
|
|
63
|
+
}
|
|
64
|
+
if (error.error) {
|
|
65
|
+
errorDetails = error.error;
|
|
66
|
+
}
|
|
67
|
+
if (error.errors) {
|
|
68
|
+
errorDetails = error.errors;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
logger.error(`Failed to push ${apiPath}: ${errorMessage}`);
|
|
73
|
+
|
|
74
|
+
if (errorDetails) {
|
|
75
|
+
if (typeof errorDetails === 'string') {
|
|
76
|
+
logger.error(` Details: ${errorDetails}`);
|
|
77
|
+
} else {
|
|
78
|
+
logger.error(` Details: ${JSON.stringify(errorDetails, null, 2)}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (process.env.VERBOSE && error.stack) {
|
|
83
|
+
logger.debug(`Stack: ${error.stack}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
throw new Error(`Failed to push ${apiPath}: ${errorMessage}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
// Check for failures
|
|
92
|
+
const failures = results.filter(r => r.status === 'rejected');
|
|
93
|
+
|
|
94
|
+
if (failures.length > 0) {
|
|
95
|
+
logger.error(`\n${failures.length} plugin(s) failed to push:`);
|
|
96
|
+
failures.forEach(f => {
|
|
97
|
+
logger.error(` • ${f.reason.message}`);
|
|
98
|
+
});
|
|
99
|
+
throw new Error(`Failed to push ${failures.length} plugin(s)`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
logger.info(`Pushed ${pluginFiles.length} plugin(s)`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Pull plugins from remote to local directory
|
|
107
|
+
* @param {Object} sdk - Initialized SDK instance
|
|
108
|
+
* @param {string} targetPath - Path to save plugins
|
|
109
|
+
*/
|
|
110
|
+
export async function pullPlugins(sdk, targetPath) {
|
|
111
|
+
// Create directory if it doesn't exist
|
|
112
|
+
if (!fs.existsSync(targetPath)) {
|
|
113
|
+
fs.mkdirSync(targetPath, { recursive: true });
|
|
114
|
+
logger.info(`Created directory: ${targetPath}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const plugins = await sdk.managePlugins.getList();
|
|
118
|
+
|
|
119
|
+
if (plugins.length === 0) {
|
|
120
|
+
logger.warning('No plugins found to pull');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
logger.info(`Found ${plugins.length} plugin(s) to pull`);
|
|
125
|
+
|
|
126
|
+
await Promise.all(plugins.map(async (plugin) => {
|
|
127
|
+
const fileName = `${plugin.apiPath}.plugin.json`;
|
|
128
|
+
const filePath = join(targetPath, fileName);
|
|
129
|
+
|
|
130
|
+
// Fetch full plugin details
|
|
131
|
+
const fullPlugin = await sdk.managePlugins.getById(plugin._id);
|
|
132
|
+
|
|
133
|
+
function removeIdFromObject(obj) {
|
|
134
|
+
const { _id, ...rest } = obj;
|
|
135
|
+
return rest;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const relevantFields = {
|
|
139
|
+
name: fullPlugin.name,
|
|
140
|
+
description: fullPlugin.description,
|
|
141
|
+
manifestUrl: fullPlugin.manifestUrl,
|
|
142
|
+
callbackUrl: fullPlugin.callbackUrl,
|
|
143
|
+
registerUrl: fullPlugin.registerUrl,
|
|
144
|
+
apiPath: fullPlugin.apiPath,
|
|
145
|
+
authAcquire: fullPlugin.authAcquire,
|
|
146
|
+
proxyUrl: fullPlugin.proxyUrl,
|
|
147
|
+
subscribedEvents: (fullPlugin.subscribedEvents || []).map(removeIdFromObject),
|
|
148
|
+
microFrontends: (fullPlugin.microFrontends || []).map(removeIdFromObject),
|
|
149
|
+
injectables: (fullPlugin.injectables || []).map(removeIdFromObject),
|
|
150
|
+
navBarGroups: (fullPlugin.navBarGroups || []).map(removeIdFromObject),
|
|
151
|
+
cruds: (fullPlugin.cruds || []).map(removeIdFromObject),
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
fs.writeFileSync(filePath, JSON.stringify(relevantFields, null, 2), 'utf-8');
|
|
155
|
+
logger.step(`Pulled: ${plugin.apiPath}`);
|
|
156
|
+
}));
|
|
157
|
+
|
|
158
|
+
logger.info(`Pulled ${plugins.length} plugin(s)`);
|
|
159
|
+
}
|
package/services/sdk.mjs
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createJiti } from 'jiti';
|
|
2
|
+
import { logger } from './logger.mjs';
|
|
3
|
+
|
|
4
|
+
const jiti = createJiti(import.meta.url);
|
|
5
|
+
|
|
6
|
+
export async function initializeSdk() {
|
|
7
|
+
const appUrl = process.env.QELOS_URL || "http://localhost:3000";
|
|
8
|
+
const username = process.env.QELOS_USERNAME || 'test@test.com';
|
|
9
|
+
const password = process.env.QELOS_PASSWORD || 'admin';
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
logger.debug('Initializing Qelos SDK...');
|
|
13
|
+
|
|
14
|
+
if (process.env.VERBOSE) {
|
|
15
|
+
logger.showConfig({
|
|
16
|
+
'QELOS_URL': appUrl,
|
|
17
|
+
'QELOS_USERNAME': username,
|
|
18
|
+
'QELOS_PASSWORD': password
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const QelosAdministratorSDK = await jiti('@qelos/sdk/src/administrator/index.ts');
|
|
23
|
+
|
|
24
|
+
const sdk = new QelosAdministratorSDK.default({
|
|
25
|
+
appUrl,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
logger.debug(`Authenticating as ${username}...`);
|
|
29
|
+
|
|
30
|
+
await sdk.authentication.oAuthSignin({
|
|
31
|
+
username,
|
|
32
|
+
password,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
logger.debug('Authentication successful');
|
|
36
|
+
return sdk;
|
|
37
|
+
|
|
38
|
+
} catch (error) {
|
|
39
|
+
// Handle connection errors
|
|
40
|
+
if (error.cause?.code === 'UND_ERR_CONNECT_TIMEOUT' ||
|
|
41
|
+
error.cause?.code === 'ENOTFOUND' ||
|
|
42
|
+
error.cause?.code === 'ECONNREFUSED' ||
|
|
43
|
+
error.message?.includes('fetch failed')) {
|
|
44
|
+
logger.connectionError(appUrl, error.cause || error);
|
|
45
|
+
}
|
|
46
|
+
// Handle authentication errors
|
|
47
|
+
else if (error.response?.status === 401 || error.message?.includes('authentication')) {
|
|
48
|
+
logger.authError(username, appUrl);
|
|
49
|
+
}
|
|
50
|
+
// Handle other errors
|
|
51
|
+
else {
|
|
52
|
+
logger.error('Failed to initialize SDK', error);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|