@qelos/plugins-cli 0.0.8 → 0.0.10

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 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
- > npm install -g @qelos/plugins-cli
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
- Basic usage to create new plugin
13
- > qplay create my-app
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, plugins, integrations, blueprints, and more.',
5
+ .command('pull [type] [path]', 'pull from qelos app. Ability to pull components, blueprints, configurations, and more.',
6
6
  (yargs) => {
7
7
  return yargs
8
8
  .positional('type', {
9
- describe: 'Type of the resource to pull. Can be components, plugins, integrations, blueprints, or more.',
9
+ describe: 'Type of the resource to pull. Can be components, blueprints, configurations, or more.',
10
10
  type: 'string',
11
+ choices: ['components', 'blueprints', 'configs'],
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, plugins, integrations, blueprints, and more.',
5
+ .command('push [type] [path]', 'push to qelos app. Ability to push components, blueprints, configurations, and more.',
6
6
  (yargs) => {
7
7
  return yargs
8
8
  .positional('type', {
9
- describe: 'Type of the plugin to push. Can be components, plugins, integrations, blueprints, or more.',
9
+ describe: 'Type of the resource to push. Can be components, blueprints, configurations, or more.',
10
10
  type: 'string',
11
+ choices: ['components', 'blueprints', 'configs'],
11
12
  required: true
12
13
  })
13
14
  .positional('path', {
14
- describe: 'Path to the context file to push.',
15
+ describe: 'Path to the resource to push.',
15
16
  type: 'string',
16
17
  required: true
17
18
  })
@@ -1,37 +1,48 @@
1
- import QelosAdministratorSDK from "@qelos/sdk/src/administrator";
2
- import fs from 'node:fs'
3
- import path from 'node:path'
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 { logger } from '../services/logger.mjs';
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
4
8
 
5
9
  export default async function pullController({ type, path: targetPath }) {
10
+ try {
11
+ // Validate parent directory exists
12
+ const parentDir = path.dirname(targetPath);
13
+ if (!fs.existsSync(parentDir)) {
14
+ logger.error(`Parent directory does not exist: ${parentDir}`);
15
+ logger.info('Please ensure the parent directory exists');
16
+ process.exit(1);
17
+ }
18
+
19
+ // Warn if target path exists and is not a directory
20
+ if (fs.existsSync(targetPath) && !fs.statSync(targetPath).isDirectory()) {
21
+ logger.error(`Path exists but is not a directory: ${targetPath}`);
22
+ logger.info('Please provide a directory path, not a file');
23
+ process.exit(1);
24
+ }
6
25
 
7
- const sdk = new QelosAdministratorSDK({
8
- appUrl: process.env.QELOS_URL || "http://localhost:3000",
9
- })
26
+ const sdk = await initializeSdk();
10
27
 
11
- await sdk.authentication.oAuthSignin({
12
- username: process.env.QELOS_USERNAME || 'test@test.com',
13
- password: process.env.QELOS_PASSWORD || 'admin',
14
- })
28
+ logger.section(`Pulling ${type} to ${targetPath}`);
15
29
 
16
- // if type === 'components' - fetch all components and save them as vue files
17
- if (type === 'components') {
18
- // Create directory if it doesn't exist
19
- if (!fs.existsSync(targetPath)) {
20
- fs.mkdirSync(targetPath, { recursive: true })
21
- console.log('Created directory:', targetPath)
30
+ if (type === 'components') {
31
+ await pullComponents(sdk, targetPath);
32
+ } else if (type === 'blueprints') {
33
+ await pullBlueprints(sdk, targetPath);
34
+ } else if (type === 'config' || type === 'configs' || type === 'configuration') {
35
+ await pullConfigurations(sdk, targetPath);
36
+ } else {
37
+ logger.error(`Unknown type: ${type}`);
38
+ logger.info('Supported types: components, blueprints, config, configs, configuration');
39
+ process.exit(1);
22
40
  }
23
41
 
24
- const components = await sdk.components.getList()
25
- console.log(`Found ${components.length} components to pull`)
26
-
27
- await Promise.all(components.map(async (component) => {
28
- const fileName = `${component.identifier}.vue`
29
- const filePath = path.join(targetPath, fileName)
30
-
31
- fs.writeFileSync(filePath, component.content, 'utf-8')
32
- console.log('Pulled component:', component.identifier)
33
- }))
42
+ logger.success(`Successfully pulled ${type} to ${targetPath}`);
34
43
 
35
- console.log(`All ${components.length} components pulled to ${targetPath}`)
44
+ } catch (error) {
45
+ logger.error(`Failed to pull ${type}`, error);
46
+ process.exit(1);
36
47
  }
37
48
  }
@@ -1,43 +1,57 @@
1
- import QelosAdministratorSDK from "@qelos/sdk/src/administrator";
2
- import fs from 'node:fs'
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 { logger } from '../services/logger.mjs';
6
+ import fs from 'node:fs';
3
7
 
4
- export default async function pushController({ type, path }) {
8
+ export default async function pushController({ type, path }) {
9
+ try {
10
+ // Validate path exists
11
+ if (!fs.existsSync(path)) {
12
+ logger.error(`Path does not exist: ${path}`);
13
+ logger.info('Please provide a valid directory path');
14
+ process.exit(1);
15
+ }
5
16
 
6
- const sdk = new QelosAdministratorSDK({
7
- appUrl: process.env.QELOS_URL || "http://localhost:3000",
8
- })
17
+ // Validate path is a directory
18
+ if (!fs.statSync(path).isDirectory()) {
19
+ logger.error(`Path is not a directory: ${path}`);
20
+ logger.info('Please provide a directory path, not a file');
21
+ process.exit(1);
22
+ }
9
23
 
10
- await sdk.authentication.oAuthSignin({
11
- username: process.env.QELOS_USERNAME || 'test@test.com',
12
- password: process.env.QELOS_PASSWORD || 'admin',
13
- })
24
+ const sdk = await initializeSdk();
14
25
 
15
- // if type === 'components' - load all vue files from path and push them using sdk
16
- if (type === 'components') {
17
- const files = fs.readdirSync(path)
18
- const existingComponents = await sdk.components.getList()
19
- await Promise.all(files.map(async (file) => {
20
- if (file.endsWith('.vue')) {
21
- const content = fs.readFileSync(path + '/' + file, 'utf-8')
22
- console.log('Pushing component:', file.replace('.vue', ''))
23
- const existingComponent = existingComponents.find(component => component.identifier === file.replace('.vue', ''))
24
- if (existingComponent) {
25
- await sdk.components.update(existingComponent._id, {
26
- content,
27
- description: 'Component description'
28
- })
29
- console.log('Component updated:', file.replace('.vue', ''))
30
- } else {
31
- await sdk.components.create({
32
- identifier: file.replace('.vue', ''),
33
- componentName: file.replace('.vue', ''),
34
- content,
35
- description: 'Component description'
36
- })
37
- console.log('Component pushed:', file.replace('.vue', ''))
38
- }
39
- }
40
- }))
41
- console.log('All components pushed')
26
+ logger.section(`Pushing ${type} from ${path}`);
27
+
28
+ if (type === 'components') {
29
+ await pushComponents(sdk, path);
30
+ } else if (type === 'blueprints') {
31
+ await pushBlueprints(sdk, path);
32
+ } else if (type === 'config' || type === 'configs' || type === 'configuration') {
33
+ await pushConfigurations(sdk, path);
34
+ } else {
35
+ logger.error(`Unknown type: ${type}`);
36
+ logger.info('Supported types: components, blueprints, config, configs, configuration');
37
+ process.exit(1);
38
+ }
39
+
40
+ logger.success(`Successfully pushed ${type}`);
41
+
42
+ } catch (error) {
43
+ // Don't log the error again if it's already been logged by the service
44
+ if (!error.message?.includes('Failed to push')) {
45
+ logger.error(`Failed to push ${type}`, error);
46
+ }
47
+
48
+ if (process.env.VERBOSE) {
49
+ console.error('\nStack trace:');
50
+ console.error(error.stack);
51
+ } else {
52
+ console.error('\nRun with VERBOSE=true for full stack trace');
53
+ }
54
+
55
+ process.exit(1);
42
56
  }
43
57
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qelos/plugins-cli",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "description": "CLI to manage QELOS plugins",
5
5
  "main": "cli.mjs",
6
6
  "bin": {
@@ -19,6 +19,7 @@
19
19
  "cli-select": "^1.1.2",
20
20
  "decompress-zip": "^0.3.3",
21
21
  "follow-redirects": "^1.15.11",
22
+ "jiti": "^2.6.1",
22
23
  "rimraf": "^6.0.1",
23
24
  "yargs": "^18.0.0",
24
25
  "zx": "^8.8.5"
@@ -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,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
+ }