@qelos/plugins-cli 0.0.10 → 0.0.12

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/commands/pull.mjs CHANGED
@@ -2,13 +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, blueprints, configurations, and more.',
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, blueprints, configurations, or more.',
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'],
11
+ choices: ['components', 'blueprints', 'configs', 'plugins', 'blocks', 'all', '*'],
12
12
  required: true
13
13
  })
14
14
  .positional('path', {
package/commands/push.mjs CHANGED
@@ -2,13 +2,13 @@ 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, blueprints, configurations, and more.',
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 resource to push. Can be components, blueprints, configurations, or more.',
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'],
11
+ choices: ['components', 'blueprints', 'configs', 'plugins', 'blocks', 'all', '*'],
12
12
  required: true
13
13
  })
14
14
  .positional('path', {
@@ -2,6 +2,8 @@ import { initializeSdk } from '../services/sdk.mjs';
2
2
  import { pullComponents } from '../services/components.mjs';
3
3
  import { pullBlueprints } from '../services/blueprints.mjs';
4
4
  import { pullConfigurations } from '../services/configurations.mjs';
5
+ import { pullPlugins } from '../services/plugins.mjs';
6
+ import { pullBlocks } from '../services/blocks.mjs';
5
7
  import { logger } from '../services/logger.mjs';
6
8
  import fs from 'node:fs';
7
9
  import path from 'node:path';
@@ -25,17 +27,48 @@ export default async function pullController({ type, path: targetPath }) {
25
27
 
26
28
  const sdk = await initializeSdk();
27
29
 
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
+ ];
41
+
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;
55
+ }
56
+
28
57
  logger.section(`Pulling ${type} to ${targetPath}`);
29
58
 
30
59
  if (type === 'components') {
31
60
  await pullComponents(sdk, targetPath);
32
61
  } else if (type === 'blueprints') {
33
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);
34
67
  } else if (type === 'config' || type === 'configs' || type === 'configuration') {
35
68
  await pullConfigurations(sdk, targetPath);
36
69
  } else {
37
70
  logger.error(`Unknown type: ${type}`);
38
- logger.info('Supported types: components, blueprints, config, configs, configuration');
71
+ logger.info('Supported types: components, blueprints, plugins, blocks, config, configs, configuration, all');
39
72
  process.exit(1);
40
73
  }
41
74
 
@@ -2,38 +2,79 @@ import { initializeSdk } from '../services/sdk.mjs';
2
2
  import { pushComponents } from '../services/components.mjs';
3
3
  import { pushBlueprints } from '../services/blueprints.mjs';
4
4
  import { pushConfigurations } from '../services/configurations.mjs';
5
+ import { pushPlugins } from '../services/plugins.mjs';
6
+ import { pushBlocks } from '../services/blocks.mjs';
5
7
  import { logger } from '../services/logger.mjs';
6
8
  import fs from 'node:fs';
9
+ import path from 'node:path';
7
10
 
8
- export default async function pushController({ type, path }) {
11
+ export default async function pushController({ type, path: sourcePath }) {
9
12
  try {
10
13
  // Validate path exists
11
- if (!fs.existsSync(path)) {
12
- logger.error(`Path does not exist: ${path}`);
14
+ if (!fs.existsSync(sourcePath)) {
15
+ logger.error(`Path does not exist: ${sourcePath}`);
13
16
  logger.info('Please provide a valid directory path');
14
17
  process.exit(1);
15
18
  }
16
19
 
17
20
  // Validate path is a directory
18
- if (!fs.statSync(path).isDirectory()) {
19
- logger.error(`Path is not a directory: ${path}`);
21
+ if (!fs.statSync(sourcePath).isDirectory()) {
22
+ logger.error(`Path is not a directory: ${sourcePath}`);
20
23
  logger.info('Please provide a directory path, not a file');
21
24
  process.exit(1);
22
25
  }
23
26
 
24
27
  const sdk = await initializeSdk();
25
28
 
26
- logger.section(`Pushing ${type} from ${path}`);
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);
56
+ }
57
+ }
58
+
59
+ logger.success(`Successfully pushed all resources from ${sourcePath}`);
60
+ return;
61
+ }
62
+
63
+ logger.section(`Pushing ${type} from ${sourcePath}`);
27
64
 
28
65
  if (type === 'components') {
29
- await pushComponents(sdk, path);
66
+ await pushComponents(sdk, sourcePath);
30
67
  } else if (type === 'blueprints') {
31
- await pushBlueprints(sdk, path);
32
- } else if (type === 'config' || type === 'configs' || type === 'configuration') {
33
- await pushConfigurations(sdk, path);
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);
34
75
  } else {
35
76
  logger.error(`Unknown type: ${type}`);
36
- logger.info('Supported types: components, blueprints, config, configs, configuration');
77
+ logger.info('Supported types: components, blueprints, plugins, blocks, config, configs, configuration, all');
37
78
  process.exit(1);
38
79
  }
39
80
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qelos/plugins-cli",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "description": "CLI to manage QELOS plugins",
5
5
  "main": "cli.mjs",
6
6
  "bin": {
@@ -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,129 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { logger } from './logger.mjs';
4
+
5
+ /**
6
+ * Convert a string to kebab-case
7
+ * @param {string} str - String to convert
8
+ * @returns {string} Kebab-cased string
9
+ */
10
+ function toKebabCase(str) {
11
+ return str
12
+ .replace(/([a-z])([A-Z])/g, '$1-$2')
13
+ .replace(/[\s_]+/g, '-')
14
+ .toLowerCase();
15
+ }
16
+
17
+ /**
18
+ * Extract micro-frontend structures to separate HTML files
19
+ * @param {Array} microFrontends - Array of micro-frontend objects
20
+ * @param {string} pluginPath - Base path for the plugin directory
21
+ * @returns {Array} Updated micro-frontends with $ref instead of structure
22
+ */
23
+ export function extractMicroFrontendStructures(microFrontends, pluginPath) {
24
+ if (!microFrontends || microFrontends.length === 0) {
25
+ return microFrontends;
26
+ }
27
+
28
+ const microFrontendsDir = path.join(pluginPath, 'micro-frontends');
29
+
30
+ // Create micro-frontends directory if it doesn't exist
31
+ if (!fs.existsSync(microFrontendsDir)) {
32
+ fs.mkdirSync(microFrontendsDir, { recursive: true });
33
+ }
34
+
35
+ return microFrontends.map((mf) => {
36
+ // Skip if no structure or structure is already a reference
37
+ if (!mf.structure || typeof mf.structure === 'object') {
38
+ return mf;
39
+ }
40
+
41
+ // Generate filename from route name or micro-frontend name
42
+ const fileName = mf.route?.name
43
+ ? toKebabCase(mf.route.name)
44
+ : toKebabCase(mf.name);
45
+
46
+ const htmlFileName = `${fileName}.html`;
47
+ const htmlFilePath = path.join(microFrontendsDir, htmlFileName);
48
+ const relativeRef = `./micro-frontends/${htmlFileName}`;
49
+
50
+ // Write structure to HTML file
51
+ fs.writeFileSync(htmlFilePath, mf.structure, 'utf-8');
52
+ logger.debug(`Extracted structure to: ${relativeRef}`);
53
+
54
+ // Return micro-frontend with $ref instead of structure
55
+ return {
56
+ ...mf,
57
+ structure: { $ref: relativeRef }
58
+ };
59
+ });
60
+ }
61
+
62
+ /**
63
+ * Load content from a $ref reference
64
+ * @param {string} ref - Reference path (relative, absolute, or URL)
65
+ * @param {string} basePath - Base path for resolving relative references
66
+ * @returns {Promise<string>} Loaded content
67
+ */
68
+ async function loadReference(ref, basePath) {
69
+ // Handle HTTP/HTTPS URLs
70
+ if (ref.startsWith('http://') || ref.startsWith('https://')) {
71
+ logger.debug(`Loading structure from URL: ${ref}`);
72
+ try {
73
+ const response = await fetch(ref);
74
+ if (!response.ok) {
75
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
76
+ }
77
+ return await response.text();
78
+ } catch (error) {
79
+ throw new Error(`Failed to load from URL ${ref}: ${error.message}`);
80
+ }
81
+ }
82
+
83
+ // Handle absolute and relative paths
84
+ const filePath = path.isAbsolute(ref)
85
+ ? ref
86
+ : path.resolve(basePath, ref);
87
+
88
+ logger.debug(`Loading structure from file: ${filePath}`);
89
+
90
+ if (!fs.existsSync(filePath)) {
91
+ throw new Error(`Referenced file does not exist: ${filePath}`);
92
+ }
93
+
94
+ return fs.readFileSync(filePath, 'utf-8');
95
+ }
96
+
97
+ /**
98
+ * Resolve micro-frontend structures from $ref references
99
+ * @param {Array} microFrontends - Array of micro-frontend objects
100
+ * @param {string} pluginPath - Base path for resolving relative references
101
+ * @returns {Promise<Array>} Updated micro-frontends with resolved structures
102
+ */
103
+ export async function resolveMicroFrontendStructures(microFrontends, pluginPath) {
104
+ if (!microFrontends || microFrontends.length === 0) {
105
+ return microFrontends;
106
+ }
107
+
108
+ return await Promise.all(microFrontends.map(async (mf) => {
109
+ // Skip if no structure or structure is not a reference object
110
+ if (!mf.structure || typeof mf.structure !== 'object' || !mf.structure.$ref) {
111
+ return mf;
112
+ }
113
+
114
+ const ref = mf.structure.$ref;
115
+
116
+ try {
117
+ const content = await loadReference(ref, pluginPath);
118
+
119
+ // Return micro-frontend with resolved structure
120
+ return {
121
+ ...mf,
122
+ structure: content
123
+ };
124
+ } catch (error) {
125
+ logger.error(`Failed to resolve $ref for ${mf.name}: ${error.message}`);
126
+ throw error;
127
+ }
128
+ }));
129
+ }
@@ -0,0 +1,181 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { join } from 'node:path';
4
+ import { logger } from './logger.mjs';
5
+ import { extractMicroFrontendStructures, resolveMicroFrontendStructures } from './micro-frontends.mjs';
6
+
7
+ /**
8
+ * Push plugins from local directory to remote
9
+ * @param {Object} sdk - Initialized SDK instance
10
+ * @param {string} path - Path to plugins directory
11
+ */
12
+ export async function pushPlugins(sdk, path) {
13
+ const files = fs.readdirSync(path);
14
+ const pluginFiles = files.filter(f => f.endsWith('.plugin.json'));
15
+
16
+ if (pluginFiles.length === 0) {
17
+ logger.warning(`No plugin files (*.plugin.json) found in ${path}`);
18
+ return;
19
+ }
20
+
21
+ logger.info(`Found ${pluginFiles.length} plugin(s) to push`);
22
+ const existingPlugins = await sdk.managePlugins.getList();
23
+
24
+ const results = await Promise.allSettled(pluginFiles.map(async (file) => {
25
+ if (file.endsWith('.plugin.json')) {
26
+ let pluginData;
27
+ try {
28
+ pluginData = JSON.parse(fs.readFileSync(join(path, file), 'utf-8'));
29
+ } catch (error) {
30
+ logger.error(`Failed to parse ${file}`, error);
31
+ throw new Error(`Parse error in ${file}: ${error.message}`);
32
+ }
33
+
34
+ const apiPath = pluginData.apiPath;
35
+
36
+ if (!apiPath) {
37
+ logger.warning(`Skipping ${file}: missing apiPath field`);
38
+ return { skipped: true, file };
39
+ }
40
+
41
+ logger.step(`Pushing plugin: ${apiPath}`);
42
+
43
+ // Resolve micro-frontend structures from $ref references
44
+ if (pluginData.microFrontends && pluginData.microFrontends.length > 0) {
45
+ try {
46
+ pluginData.microFrontends = await resolveMicroFrontendStructures(
47
+ pluginData.microFrontends,
48
+ path
49
+ );
50
+ } catch (error) {
51
+ logger.error(`Failed to resolve micro-frontend structures for ${apiPath}`, error);
52
+ throw new Error(`Failed to resolve structures for ${apiPath}: ${error.message}`);
53
+ }
54
+ }
55
+
56
+ const existingPlugin = existingPlugins.find(
57
+ plugin => plugin.apiPath === apiPath
58
+ );
59
+
60
+ try {
61
+ if (existingPlugin) {
62
+ await sdk.managePlugins.update(existingPlugin._id, pluginData);
63
+ logger.success(`Updated: ${apiPath}`);
64
+ } else {
65
+ await sdk.managePlugins.create(pluginData);
66
+ logger.success(`Created: ${apiPath}`);
67
+ }
68
+ return { success: true, apiPath };
69
+ } catch (error) {
70
+ // Extract detailed error information
71
+ let errorMessage = error.message || 'Unknown error';
72
+ let errorDetails = null;
73
+
74
+ // The SDK throws the response body as the error
75
+ if (typeof error === 'object' && error !== null) {
76
+ if (error.message) {
77
+ errorMessage = error.message;
78
+ }
79
+ if (error.error) {
80
+ errorDetails = error.error;
81
+ }
82
+ if (error.errors) {
83
+ errorDetails = error.errors;
84
+ }
85
+ }
86
+
87
+ logger.error(`Failed to push ${apiPath}: ${errorMessage}`);
88
+
89
+ if (errorDetails) {
90
+ if (typeof errorDetails === 'string') {
91
+ logger.error(` Details: ${errorDetails}`);
92
+ } else {
93
+ logger.error(` Details: ${JSON.stringify(errorDetails, null, 2)}`);
94
+ }
95
+ }
96
+
97
+ if (process.env.VERBOSE && error.stack) {
98
+ logger.debug(`Stack: ${error.stack}`);
99
+ }
100
+
101
+ throw new Error(`Failed to push ${apiPath}: ${errorMessage}`);
102
+ }
103
+ }
104
+ }));
105
+
106
+ // Check for failures
107
+ const failures = results.filter(r => r.status === 'rejected');
108
+
109
+ if (failures.length > 0) {
110
+ logger.error(`\n${failures.length} plugin(s) failed to push:`);
111
+ failures.forEach(f => {
112
+ logger.error(` • ${f.reason.message}`);
113
+ });
114
+ throw new Error(`Failed to push ${failures.length} plugin(s)`);
115
+ }
116
+
117
+ logger.info(`Pushed ${pluginFiles.length} plugin(s)`);
118
+ }
119
+
120
+ /**
121
+ * Pull plugins from remote to local directory
122
+ * @param {Object} sdk - Initialized SDK instance
123
+ * @param {string} targetPath - Path to save plugins
124
+ */
125
+ export async function pullPlugins(sdk, targetPath) {
126
+ // Create directory if it doesn't exist
127
+ if (!fs.existsSync(targetPath)) {
128
+ fs.mkdirSync(targetPath, { recursive: true });
129
+ logger.info(`Created directory: ${targetPath}`);
130
+ }
131
+
132
+ const plugins = await sdk.managePlugins.getList();
133
+
134
+ if (plugins.length === 0) {
135
+ logger.warning('No plugins found to pull');
136
+ return;
137
+ }
138
+
139
+ logger.info(`Found ${plugins.length} plugin(s) to pull`);
140
+
141
+ await Promise.all(plugins.map(async (plugin) => {
142
+ const fileName = `${plugin.apiPath}.plugin.json`;
143
+ const filePath = join(targetPath, fileName);
144
+ const pluginDir = join(targetPath, plugin.apiPath);
145
+
146
+ // Fetch full plugin details
147
+ const fullPlugin = await sdk.managePlugins.getById(plugin._id);
148
+
149
+ function removeIdFromObject(obj) {
150
+ const { _id, ...rest } = obj;
151
+ return rest;
152
+ }
153
+
154
+ // Extract micro-frontend structures to separate files
155
+ const processedMicroFrontends = extractMicroFrontendStructures(
156
+ (fullPlugin.microFrontends || []).map(removeIdFromObject),
157
+ targetPath
158
+ );
159
+
160
+ const relevantFields = {
161
+ name: fullPlugin.name,
162
+ description: fullPlugin.description,
163
+ manifestUrl: fullPlugin.manifestUrl,
164
+ callbackUrl: fullPlugin.callbackUrl,
165
+ registerUrl: fullPlugin.registerUrl,
166
+ apiPath: fullPlugin.apiPath,
167
+ authAcquire: fullPlugin.authAcquire,
168
+ proxyUrl: fullPlugin.proxyUrl,
169
+ subscribedEvents: (fullPlugin.subscribedEvents || []).map(removeIdFromObject),
170
+ microFrontends: processedMicroFrontends,
171
+ injectables: (fullPlugin.injectables || []).map(removeIdFromObject),
172
+ navBarGroups: (fullPlugin.navBarGroups || []).map(removeIdFromObject),
173
+ cruds: (fullPlugin.cruds || []).map(removeIdFromObject),
174
+ }
175
+
176
+ fs.writeFileSync(filePath, JSON.stringify(relevantFields, null, 2), 'utf-8');
177
+ logger.step(`Pulled: ${plugin.apiPath}`);
178
+ }));
179
+
180
+ logger.info(`Pulled ${plugins.length} plugin(s)`);
181
+ }