@qelos/plugins-cli 0.0.23 → 0.0.25

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qelos/plugins-cli",
3
- "version": "0.0.23",
3
+ "version": "0.0.25",
4
4
  "description": "CLI to manage QELOS plugins",
5
5
  "main": "cli.mjs",
6
6
  "bin": {
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { logger } from './logger.mjs';
4
+ import { removeIdFromObject } from '../utils/object-utils.mjs';
4
5
 
5
6
  /**
6
7
  * Push blueprints from local directory to remote
@@ -137,11 +138,6 @@ export async function pullBlueprints(sdk, targetPath) {
137
138
  // Fetch full blueprint details
138
139
  const fullBlueprint = await sdk.manageBlueprints.getBlueprint(blueprint.identifier);
139
140
 
140
- function removeIdFromObject(obj) {
141
- const { _id, ...rest } = obj;
142
- return rest;
143
- }
144
-
145
141
  const relevantFields = {
146
142
  identifier: fullBlueprint.identifier,
147
143
  name: fullBlueprint.name,
@@ -1,23 +1,57 @@
1
1
  import fs from 'node:fs';
2
- import { join } from 'node:path';
2
+ import path, { join } from 'node:path';
3
3
  import { logger } from './logger.mjs';
4
4
 
5
+ function normalizeRelativeIdentifier(value) {
6
+ return String(value || '')
7
+ .replaceAll('\\', '/')
8
+ .replace(/^\//, '')
9
+ .trim();
10
+ }
11
+
12
+ function isSafeRelativeIdentifier(value) {
13
+ if (!value) return false;
14
+ if (value.includes('..')) return false;
15
+ if (value.startsWith('/')) return false;
16
+ return true;
17
+ }
18
+
19
+ function listVueFilesRecursively(rootDir, currentDir = rootDir, out = []) {
20
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
21
+ for (const entry of entries) {
22
+ const fullPath = join(currentDir, entry.name);
23
+ if (entry.isDirectory()) {
24
+ if (entry.name === 'node_modules' || entry.name === '.git') continue;
25
+ listVueFilesRecursively(rootDir, fullPath, out);
26
+ continue;
27
+ }
28
+ if (!entry.isFile()) continue;
29
+ if (!entry.name.endsWith('.vue')) continue;
30
+ if (entry.name === 'components.json') continue;
31
+ out.push(fullPath);
32
+ }
33
+ return out;
34
+ }
35
+
5
36
  /**
6
37
  * Push components from local directory to remote
7
38
  * @param {Object} sdk - Initialized SDK instance
8
39
  * @param {string} path - Path to components directory
9
40
  */
10
- export async function pushComponents(sdk, path, options = {}) {
41
+ export async function pushComponents(sdk, componentsPath, options = {}) {
11
42
  const { targetFile } = options;
12
- const directoryFiles = fs.readdirSync(path);
13
- const files = targetFile ? [targetFile] : directoryFiles;
14
- const vueFiles = files.filter(f => f.endsWith('.vue'));
43
+ const vueFilePaths = targetFile
44
+ ? [join(componentsPath, targetFile)]
45
+ : listVueFilesRecursively(componentsPath);
46
+ const vueFiles = vueFilePaths
47
+ .filter(filePath => fs.existsSync(filePath))
48
+ .filter(filePath => filePath.endsWith('.vue'));
15
49
 
16
50
  if (vueFiles.length === 0) {
17
51
  if (targetFile) {
18
52
  logger.warning(`File ${targetFile} is not a .vue component. Skipping.`);
19
53
  } else {
20
- logger.warning(`No .vue files found in ${path}`);
54
+ logger.warning(`No .vue files found in ${componentsPath}`);
21
55
  }
22
56
  return;
23
57
  }
@@ -26,7 +60,7 @@ export async function pushComponents(sdk, path, options = {}) {
26
60
  let componentsJson = {};
27
61
 
28
62
  try {
29
- const jsonPath = join(path, 'components.json');
63
+ const jsonPath = join(componentsPath, 'components.json');
30
64
  if (fs.existsSync(jsonPath)) {
31
65
  componentsJson = JSON.parse(fs.readFileSync(jsonPath));
32
66
  }
@@ -38,11 +72,17 @@ export async function pushComponents(sdk, path, options = {}) {
38
72
 
39
73
  await Promise.all(vueFiles.map(async (file) => {
40
74
  if (file.endsWith('.vue')) {
41
- const componentName = file.replace('.vue', '');
75
+ const relativePath = normalizeRelativeIdentifier(path.relative(componentsPath, file));
76
+ const componentName = relativePath.split('/').pop().replace('.vue', '');
42
77
  const info = componentsJson[componentName] || {};
43
- const content = fs.readFileSync(join(path, file), 'utf-8');
44
- const targetIdentifier = info.identifier || componentName;
78
+ const content = fs.readFileSync(file, 'utf-8');
79
+ const targetIdentifier = normalizeRelativeIdentifier(info.identifier || relativePath.replace('.vue', ''));
45
80
  const targetDescription = info.description || 'Component description';
81
+
82
+ if (!isSafeRelativeIdentifier(targetIdentifier)) {
83
+ logger.error(`Skipping component with unsafe identifier: ${targetIdentifier}`);
84
+ return;
85
+ }
46
86
 
47
87
  logger.step(`Pushing component: ${componentName}`);
48
88
 
@@ -102,9 +142,20 @@ export async function pullComponents(sdk, targetPath) {
102
142
  logger.info(`Found ${components.length} component(s) to pull`);
103
143
 
104
144
  const componentsInformation = await Promise.all(components.map(async (component) => {
105
- const fileName = `${component.componentName}.vue`;
145
+ const normalizedIdentifier = normalizeRelativeIdentifier(component.identifier);
146
+ const fileName = `${normalizedIdentifier}.vue`;
106
147
  const filePath = join(targetPath, fileName);
107
148
 
149
+ if (!isSafeRelativeIdentifier(normalizedIdentifier)) {
150
+ logger.error(`Skipping pull for unsafe identifier: ${component.identifier}`);
151
+ return null;
152
+ }
153
+
154
+ const targetDir = join(targetPath, fileName.split('/').slice(0, -1).join('/'));
155
+ if (targetDir && !fs.existsSync(targetDir)) {
156
+ fs.mkdirSync(targetDir, { recursive: true });
157
+ }
158
+
108
159
  const { content, description } = await sdk.components.getComponent(component._id);
109
160
 
110
161
  fs.writeFileSync(filePath, content, 'utf-8');
@@ -118,10 +169,12 @@ export async function pullComponents(sdk, targetPath) {
118
169
  };
119
170
  }));
120
171
 
172
+ const filteredComponentsInformation = componentsInformation.filter(Boolean);
173
+
121
174
  fs.writeFileSync(
122
175
  join(targetPath, 'components.json'),
123
176
  JSON.stringify(
124
- componentsInformation.reduce((obj, current) => {
177
+ filteredComponentsInformation.reduce((obj, current) => {
125
178
  obj[current.componentName] = current;
126
179
  return obj;
127
180
  }, {}),
@@ -131,5 +184,5 @@ export async function pullComponents(sdk, targetPath) {
131
184
  );
132
185
 
133
186
  logger.info(`Saved components.json with metadata`);
134
- logger.info(`Pulled ${components.length} component(s)`);
187
+ logger.info(`Pulled ${filteredComponentsInformation.length} component(s)`);
135
188
  }
@@ -3,6 +3,61 @@ import { logger } from './logger.mjs';
3
3
  import path from 'node:path';
4
4
  import fs from 'node:fs';
5
5
 
6
+ /**
7
+ * Resolve the actual file path from Git output
8
+ * Git might show temp paths or different formats, so we need to find the real file
9
+ * @param {string} gitPath - Path as reported by Git
10
+ * @param {string} basePath - Base path to search for the actual file
11
+ * @returns {string|null} The actual file path or null if not found
12
+ */
13
+ function resolveActualFilePath(gitPath, basePath) {
14
+ // First try the direct path
15
+ const directPath = path.resolve(basePath, gitPath);
16
+ if (fs.existsSync(directPath)) {
17
+ return directPath;
18
+ }
19
+
20
+ // If it's an HTML file, it might be in a micro-frontends directory
21
+ if (gitPath.endsWith('.html')) {
22
+ // Try to find it in any plugin's micro-frontends directory
23
+ const pluginsDir = path.join(basePath, 'plugins');
24
+ if (fs.existsSync(pluginsDir)) {
25
+ const plugins = fs.readdirSync(pluginsDir);
26
+ const filename = path.basename(gitPath);
27
+
28
+ for (const plugin of plugins) {
29
+ const mfPath = path.join(pluginsDir, plugin, 'micro-frontends', filename);
30
+ if (fs.existsSync(mfPath)) {
31
+ return mfPath;
32
+ }
33
+ }
34
+ }
35
+ }
36
+
37
+ // For other files, try searching in common directories
38
+ const filename = path.basename(gitPath);
39
+ const searchDirs = [
40
+ 'components',
41
+ 'blueprints',
42
+ 'configs',
43
+ 'plugins',
44
+ 'blocks',
45
+ 'integrations',
46
+ 'connections',
47
+ 'prompts'
48
+ ];
49
+
50
+ for (const dir of searchDirs) {
51
+ const searchPath = path.join(basePath, dir, filename);
52
+ if (fs.existsSync(searchPath)) {
53
+ return searchPath;
54
+ }
55
+ }
56
+
57
+ // If still not found, return null
58
+ return null;
59
+ }
60
+
6
61
  /**
7
62
  * Get the list of files committed in the last commit
8
63
  * @returns {string[]} Array of file paths
@@ -114,8 +169,22 @@ function classifyFiles(files, basePath) {
114
169
  };
115
170
 
116
171
  for (const file of files) {
172
+ // Try to resolve the actual file path (handles Git temp paths)
173
+ let fullPath = path.resolve(basePath, file);
174
+
175
+ // If the direct path doesn't exist, try to find the actual file
176
+ if (!fs.existsSync(fullPath)) {
177
+ const resolvedPath = resolveActualFilePath(file, basePath);
178
+ if (resolvedPath) {
179
+ logger.debug(`Resolved Git path ${file} to actual path ${resolvedPath}`);
180
+ fullPath = resolvedPath;
181
+ } else {
182
+ logger.warning(`File not found, skipping: ${file}`);
183
+ continue;
184
+ }
185
+ }
186
+
117
187
  // Make sure the file exists
118
- const fullPath = path.resolve(basePath, file);
119
188
  if (!fs.existsSync(fullPath)) {
120
189
  logger.warning(`File not found, skipping: ${file}`);
121
190
  continue;
@@ -164,8 +233,33 @@ function classifyFiles(files, basePath) {
164
233
 
165
234
  // For HTML files, we need to find which plugin contains them
166
235
  // HTML files in plugins are typically part of the plugin structure
167
- const pluginDir = path.dirname(fullPath);
168
- const pluginJson = path.join(pluginDir, 'plugin.json');
236
+ let pluginDir = path.dirname(fullPath);
237
+ let pluginJson = path.join(pluginDir, 'plugin.json');
238
+
239
+ // If the file is in a temp path or unusual location, try to find the actual plugin
240
+ if (!fs.existsSync(pluginJson)) {
241
+ // Check if we're in a micro-frontends subdirectory
242
+ if (path.basename(pluginDir) === 'micro-frontends' ||
243
+ relativePath.includes('micro-frontends/') ||
244
+ relativePath.includes('micro-frontends\\')) {
245
+ // Go up one more level to find the plugin directory
246
+ pluginDir = path.dirname(pluginDir);
247
+ pluginJson = path.join(pluginDir, 'plugin.json');
248
+ }
249
+
250
+ // If still not found, try searching for plugin.json in parent directories
251
+ if (!fs.existsSync(pluginJson)) {
252
+ let searchDir = pluginDir;
253
+ for (let i = 0; i < 3; i++) { // Search up to 3 levels up
254
+ searchDir = path.dirname(searchDir);
255
+ const testPluginJson = path.join(searchDir, 'plugin.json');
256
+ if (fs.existsSync(testPluginJson)) {
257
+ pluginJson = testPluginJson;
258
+ break;
259
+ }
260
+ }
261
+ }
262
+ }
169
263
 
170
264
  if (fs.existsSync(pluginJson)) {
171
265
  // This HTML file is part of a plugin
@@ -173,6 +267,8 @@ function classifyFiles(files, basePath) {
173
267
  classified.plugins.push(pluginJson);
174
268
  logger.debug(`Found plugin containing HTML ${relativePath}: ${path.basename(pluginJson)}`);
175
269
  }
270
+ } else {
271
+ logger.warning(`Could not find plugin.json for HTML file: ${relativePath}`);
176
272
  }
177
273
  } else {
178
274
  logger.debug(`Unclassified file: ${relativePath}`);
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { logger } from './logger.mjs';
4
4
  import { loadReference } from './file-refs.mjs';
5
+ import { removeIdFromObject } from '../utils/object-utils.mjs';
5
6
 
6
7
  /**
7
8
  * Convert a string to kebab-case
@@ -55,6 +56,7 @@ export function extractMicroFrontendStructures(microFrontends, pluginPath) {
55
56
  // Return micro-frontend with $ref instead of structure
56
57
  return {
57
58
  ...mf,
59
+ requirements: (mf.requirements || []).map(removeIdFromObject),
58
60
  structure: { $ref: relativeRef }
59
61
  };
60
62
  });
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { join } from 'node:path';
4
4
  import { logger } from './logger.mjs';
5
5
  import { extractMicroFrontendStructures, resolveMicroFrontendStructures } from './micro-frontends.mjs';
6
+ import { removeIdFromObject } from '../utils/object-utils.mjs';
6
7
 
7
8
  function sanitizePluginForFile(plugin) {
8
9
  const sanitized = JSON.parse(JSON.stringify(plugin));
@@ -194,11 +195,6 @@ export async function pullPlugins(sdk, targetPath) {
194
195
  // Fetch full plugin details
195
196
  const fullPlugin = await sdk.managePlugins.getById(plugin._id);
196
197
 
197
- function removeIdFromObject(obj) {
198
- const { _id, ...rest } = obj;
199
- return rest;
200
- }
201
-
202
198
  // Extract micro-frontend structures to separate files
203
199
  const processedMicroFrontends = extractMicroFrontendStructures(
204
200
  (fullPlugin.microFrontends || []).map(removeIdFromObject),
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Removes the _id property from an object
3
+ * @param {Object} obj - The object to remove _id from
4
+ * @returns {Object} - The object without _id
5
+ */
6
+ export function removeIdFromObject(obj) {
7
+ const { _id, ...rest } = obj;
8
+ return rest;
9
+ }