@qelos/plugins-cli 0.0.20 → 0.0.22
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 +1 -1
- package/commands/push.mjs +1 -1
- package/package.json +1 -1
- package/services/components.mjs +23 -16
- package/services/configurations.mjs +8 -1
- package/services/file-refs.mjs +263 -0
- package/services/integrations.mjs +36 -4
- package/services/micro-frontends.mjs +1 -35
- package/services/plugins.mjs +46 -1
package/commands/pull.mjs
CHANGED
|
@@ -8,7 +8,7 @@ export default function pullCommand(program) {
|
|
|
8
8
|
.positional('type', {
|
|
9
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', 'integrations', 'all', '*'],
|
|
11
|
+
choices: ['components', 'blueprints', 'configs', 'plugins', 'blocks', 'integrations', 'connections', 'all', '*'],
|
|
12
12
|
required: true
|
|
13
13
|
})
|
|
14
14
|
.positional('path', {
|
package/commands/push.mjs
CHANGED
|
@@ -8,7 +8,7 @@ export default function createCommand(program) {
|
|
|
8
8
|
.positional('type', {
|
|
9
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', 'integrations', 'all', '*'],
|
|
11
|
+
choices: ['components', 'blueprints', 'configs', 'plugins', 'blocks', 'integrations', 'connections', 'all', '*'],
|
|
12
12
|
required: true
|
|
13
13
|
})
|
|
14
14
|
.positional('path', {
|
package/package.json
CHANGED
package/services/components.mjs
CHANGED
|
@@ -50,22 +50,29 @@ export async function pushComponents(sdk, path, options = {}) {
|
|
|
50
50
|
component => component.identifier === targetIdentifier || component.componentName === componentName
|
|
51
51
|
);
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
53
|
+
try {
|
|
54
|
+
if (existingComponent) {
|
|
55
|
+
await sdk.components.update(existingComponent._id, {
|
|
56
|
+
identifier: targetIdentifier,
|
|
57
|
+
componentName: componentName,
|
|
58
|
+
content,
|
|
59
|
+
description: info.description || existingComponent.description || 'Component description'
|
|
60
|
+
});
|
|
61
|
+
logger.success(`Updated: ${componentName}`);
|
|
62
|
+
} else {
|
|
63
|
+
await sdk.components.create({
|
|
64
|
+
identifier: targetIdentifier,
|
|
65
|
+
componentName: componentName,
|
|
66
|
+
content,
|
|
67
|
+
description: targetDescription
|
|
68
|
+
});
|
|
69
|
+
logger.success(`Created: ${componentName}`);
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
// Extract reason from error details if available
|
|
73
|
+
const reason = error.details?.reason || error.message;
|
|
74
|
+
logger.error(`Failed to push component: ${componentName} - ${reason}`);
|
|
75
|
+
throw error;
|
|
69
76
|
}
|
|
70
77
|
}
|
|
71
78
|
}));
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { logger } from './logger.mjs';
|
|
4
4
|
import { appUrl } from './sdk.mjs';
|
|
5
|
+
import { extractConfigContent, resolveReferences } from './file-refs.mjs';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Push configurations from local directory to remote
|
|
@@ -45,6 +46,9 @@ export async function pushConfigurations(sdk, path, options = {}) {
|
|
|
45
46
|
|
|
46
47
|
logger.step(`Pushing configuration: ${key}`);
|
|
47
48
|
|
|
49
|
+
// Resolve any $ref references in the configuration
|
|
50
|
+
configData = await resolveReferences(configData, path);
|
|
51
|
+
|
|
48
52
|
// Special handling for app-configuration: ensure QELOS_URL hostname is in websiteUrls
|
|
49
53
|
if (key === 'app-configuration') {
|
|
50
54
|
try {
|
|
@@ -160,7 +164,10 @@ export async function pullConfigurations(sdk, targetPath) {
|
|
|
160
164
|
// Remove fields that shouldn't be in the file
|
|
161
165
|
const { _id, tenant, created, updated, ...relevantFields } = fullConfig;
|
|
162
166
|
|
|
163
|
-
|
|
167
|
+
// Extract content to files for supported config types
|
|
168
|
+
const processedConfig = extractConfigContent(relevantFields, targetPath);
|
|
169
|
+
|
|
170
|
+
fs.writeFileSync(filePath, JSON.stringify(processedConfig, null, 2), 'utf-8');
|
|
164
171
|
logger.step(`Pulled: ${config.key}`);
|
|
165
172
|
}));
|
|
166
173
|
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { logger } from './logger.mjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Configuration mapping for which fields should be extracted to files for each config type
|
|
7
|
+
*/
|
|
8
|
+
const CONFIG_EXTRACTION_MAP = {
|
|
9
|
+
'ssr-scripts': {
|
|
10
|
+
fields: ['head', 'body'],
|
|
11
|
+
fileExtension: '.html',
|
|
12
|
+
subdirectory: 'html'
|
|
13
|
+
},
|
|
14
|
+
'users-header': {
|
|
15
|
+
fields: ['html'],
|
|
16
|
+
fileExtension: '.html',
|
|
17
|
+
subdirectory: 'html'
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if an integration is an AI agent that should have its pre_messages extracted
|
|
23
|
+
* @param {Object} integration - Integration object
|
|
24
|
+
* @returns {boolean} True if this is an AI agent with chatCompletion
|
|
25
|
+
*/
|
|
26
|
+
function isAiAgent(integration) {
|
|
27
|
+
return (
|
|
28
|
+
integration &&
|
|
29
|
+
Array.isArray(integration.kind) &&
|
|
30
|
+
integration.kind.includes('qelos') &&
|
|
31
|
+
integration.trigger?.operation === 'chatCompletion' &&
|
|
32
|
+
integration.target?.operation === 'chatCompletion' &&
|
|
33
|
+
integration.target?.details?.pre_messages?.length > 0
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Convert a string to kebab-case for filenames
|
|
39
|
+
* @param {string} str - String to convert
|
|
40
|
+
* @returns {string} Kebab-cased string
|
|
41
|
+
*/
|
|
42
|
+
function toKebabCase(str) {
|
|
43
|
+
return str
|
|
44
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
45
|
+
.replace(/[\s_]+/g, '-')
|
|
46
|
+
.toLowerCase();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Load content from a $ref reference
|
|
51
|
+
* @param {string} ref - Reference path (relative, absolute, or URL)
|
|
52
|
+
* @param {string} basePath - Base path for resolving relative references
|
|
53
|
+
* @returns {Promise<string>} Loaded content
|
|
54
|
+
*/
|
|
55
|
+
export async function loadReference(ref, basePath) {
|
|
56
|
+
// Handle HTTP/HTTPS URLs
|
|
57
|
+
if (ref.startsWith('http://') || ref.startsWith('https://')) {
|
|
58
|
+
logger.debug(`Loading reference from URL: ${ref}`);
|
|
59
|
+
try {
|
|
60
|
+
const response = await fetch(ref);
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
63
|
+
}
|
|
64
|
+
return await response.text();
|
|
65
|
+
} catch (error) {
|
|
66
|
+
throw new Error(`Failed to load from URL ${ref}: ${error.message}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Handle absolute and relative paths
|
|
71
|
+
const filePath = path.isAbsolute(ref)
|
|
72
|
+
? ref
|
|
73
|
+
: path.resolve(basePath, ref);
|
|
74
|
+
|
|
75
|
+
logger.debug(`Loading reference from file: ${filePath}`);
|
|
76
|
+
|
|
77
|
+
if (!fs.existsSync(filePath)) {
|
|
78
|
+
throw new Error(`Referenced file does not exist: ${filePath}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Recursively resolve all $ref references in an object
|
|
86
|
+
* @param {any} obj - Object to resolve references in
|
|
87
|
+
* @param {string} basePath - Base path for resolving relative references
|
|
88
|
+
* @returns {Promise<any>} Object with all references resolved
|
|
89
|
+
*/
|
|
90
|
+
export async function resolveReferences(obj, basePath) {
|
|
91
|
+
if (Array.isArray(obj)) {
|
|
92
|
+
return await Promise.all(obj.map(item => resolveReferences(item, basePath)));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (obj && typeof obj === 'object') {
|
|
96
|
+
// Check if this is a $ref object
|
|
97
|
+
if (obj.$ref && typeof obj.$ref === 'string') {
|
|
98
|
+
return await loadReference(obj.$ref, basePath);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Otherwise, recursively resolve all properties
|
|
102
|
+
const resolved = {};
|
|
103
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
104
|
+
resolved[key] = await resolveReferences(value, basePath);
|
|
105
|
+
}
|
|
106
|
+
return resolved;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Primitive value, return as-is
|
|
110
|
+
return obj;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Extract content from integration pre_messages to separate files
|
|
115
|
+
* @param {Object} integration - Integration object
|
|
116
|
+
* @param {string} integrationPath - Path where integration files are stored
|
|
117
|
+
* @param {string} fileName - Name of the integration file (without extension)
|
|
118
|
+
* @returns {Object} Updated integration with $ref objects
|
|
119
|
+
*/
|
|
120
|
+
export function extractIntegrationContent(integration, integrationPath, fileName) {
|
|
121
|
+
// Check if this is an AI agent
|
|
122
|
+
if (!isAiAgent(integration)) {
|
|
123
|
+
return integration;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const updatedIntegration = JSON.parse(JSON.stringify(integration)); // Deep clone
|
|
127
|
+
|
|
128
|
+
// Create prompts subdirectory if needed
|
|
129
|
+
const extractDir = path.join(integrationPath, 'prompts');
|
|
130
|
+
if (!fs.existsSync(extractDir)) {
|
|
131
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
132
|
+
logger.debug(`Created directory: ${extractDir}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Extract the first pre_message content
|
|
136
|
+
const preMessage = updatedIntegration.target.details.pre_messages[0];
|
|
137
|
+
if (preMessage && preMessage.content) {
|
|
138
|
+
// Check if content is already a $ref
|
|
139
|
+
if (typeof preMessage.content === 'object' && preMessage.content.$ref) {
|
|
140
|
+
return updatedIntegration;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Only extract if content is a string
|
|
144
|
+
if (typeof preMessage.content === 'string') {
|
|
145
|
+
const content = preMessage.content;
|
|
146
|
+
|
|
147
|
+
// Skip if content is empty or just whitespace
|
|
148
|
+
if (!content.trim()) {
|
|
149
|
+
return updatedIntegration;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Generate filename
|
|
153
|
+
const baseName = path.basename(fileName, '.integration.json');
|
|
154
|
+
const mdFileName = `${baseName}.md`;
|
|
155
|
+
const mdFilePath = path.join(extractDir, mdFileName);
|
|
156
|
+
const relativeRef = `./prompts/${mdFileName}`;
|
|
157
|
+
|
|
158
|
+
// Write content to file
|
|
159
|
+
fs.writeFileSync(mdFilePath, content, 'utf-8');
|
|
160
|
+
|
|
161
|
+
// Replace with $ref
|
|
162
|
+
updatedIntegration.target.details.pre_messages[0].content = { $ref: relativeRef };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return updatedIntegration;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Extract all integrations that have content to be externalized
|
|
171
|
+
* @param {Array} integrations - Array of integration objects
|
|
172
|
+
* @param {string} integrationPath - Path where integration files are stored
|
|
173
|
+
* @returns {Array} Updated integrations with $ref objects
|
|
174
|
+
*/
|
|
175
|
+
export function extractAllIntegrationContent(integrations, integrationPath) {
|
|
176
|
+
return integrations.map((integration, index) => {
|
|
177
|
+
// Generate a filename for this integration
|
|
178
|
+
const displayName = integration?.trigger?.details?.name ||
|
|
179
|
+
integration?.target?.details?.name ||
|
|
180
|
+
integration?._id ||
|
|
181
|
+
`integration-${index + 1}`;
|
|
182
|
+
const fileName = `${toKebabCase(displayName)}.integration.json`;
|
|
183
|
+
|
|
184
|
+
return extractIntegrationContent(integration, integrationPath, fileName);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
export function extractConfigContent(config, configPath) {
|
|
188
|
+
const extractionConfig = CONFIG_EXTRACTION_MAP[config.key];
|
|
189
|
+
|
|
190
|
+
if (!extractionConfig || !config.metadata) {
|
|
191
|
+
return config;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const updatedConfig = JSON.parse(JSON.stringify(config)); // Deep clone
|
|
195
|
+
const { fields, fileExtension, subdirectory } = extractionConfig;
|
|
196
|
+
|
|
197
|
+
// Create subdirectory if needed
|
|
198
|
+
const extractDir = path.join(configPath, subdirectory);
|
|
199
|
+
if (!fs.existsSync(extractDir)) {
|
|
200
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
201
|
+
logger.debug(`Created directory: ${extractDir}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Extract each field
|
|
205
|
+
for (const field of fields) {
|
|
206
|
+
if (updatedConfig.metadata[field] && typeof updatedConfig.metadata[field] === 'string') {
|
|
207
|
+
const content = updatedConfig.metadata[field];
|
|
208
|
+
|
|
209
|
+
// Skip if content is empty or just whitespace
|
|
210
|
+
if (!content.trim()) {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Generate filename - use config key if only one field, otherwise include field name
|
|
215
|
+
let fileName;
|
|
216
|
+
if (fields.length === 1) {
|
|
217
|
+
fileName = `${config.key}${fileExtension}`;
|
|
218
|
+
} else {
|
|
219
|
+
fileName = `${config.key}-${field}${fileExtension}`;
|
|
220
|
+
}
|
|
221
|
+
const filePath = path.join(extractDir, fileName);
|
|
222
|
+
const relativeRef = `./${subdirectory}/${fileName}`;
|
|
223
|
+
|
|
224
|
+
// Write content to file
|
|
225
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
226
|
+
logger.debug(`Extracted ${field} to: ${relativeRef}`);
|
|
227
|
+
|
|
228
|
+
// Replace with $ref
|
|
229
|
+
updatedConfig.metadata[field] = { $ref: relativeRef };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return updatedConfig;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Extract all configurations that have content to be externalized
|
|
238
|
+
* @param {Array} configs - Array of configuration objects
|
|
239
|
+
* @param {string} configPath - Path where config files are stored
|
|
240
|
+
* @returns {Array} Updated configurations with $ref objects
|
|
241
|
+
*/
|
|
242
|
+
export function extractAllConfigContent(configs, configPath) {
|
|
243
|
+
return configs.map(config => extractConfigContent(config, configPath));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Recursively find all $ref objects in an object
|
|
248
|
+
* @param {any} obj - Object to search
|
|
249
|
+
* @param {Array} refs - Array to collect found references (used internally)
|
|
250
|
+
* @returns {Array} Array of found $ref paths
|
|
251
|
+
*/
|
|
252
|
+
export function findAllRefs(obj, refs = []) {
|
|
253
|
+
if (Array.isArray(obj)) {
|
|
254
|
+
obj.forEach(item => findAllRefs(item, refs));
|
|
255
|
+
} else if (obj && typeof obj === 'object') {
|
|
256
|
+
if (obj.$ref && typeof obj.$ref === 'string') {
|
|
257
|
+
refs.push(obj.$ref);
|
|
258
|
+
} else {
|
|
259
|
+
Object.values(obj).forEach(value => findAllRefs(value, refs));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return refs;
|
|
263
|
+
}
|
|
@@ -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 { extractIntegrationContent, resolveReferences } from './file-refs.mjs';
|
|
4
5
|
|
|
5
6
|
const INTEGRATION_FILE_EXTENSION = '.integration.json';
|
|
6
7
|
const INTEGRATIONS_API_PATH = '/api/integrations';
|
|
@@ -75,6 +76,24 @@ function sanitizeIntegrationForFile(integration) {
|
|
|
75
76
|
delete sanitized[field];
|
|
76
77
|
}
|
|
77
78
|
});
|
|
79
|
+
|
|
80
|
+
// Remove _id from internal objects
|
|
81
|
+
if (sanitized.trigger && sanitized.trigger._id) {
|
|
82
|
+
delete sanitized.trigger._id;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (sanitized.target && sanitized.target._id) {
|
|
86
|
+
delete sanitized.target._id;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (Array.isArray(sanitized.dataManipulation)) {
|
|
90
|
+
sanitized.dataManipulation.forEach(item => {
|
|
91
|
+
if (item._id) {
|
|
92
|
+
delete item._id;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
78
97
|
return sanitized;
|
|
79
98
|
}
|
|
80
99
|
|
|
@@ -123,7 +142,11 @@ export async function pullIntegrations(sdk, targetPath) {
|
|
|
123
142
|
integrations.forEach((integration, index) => {
|
|
124
143
|
const fileName = buildFileName(integration, index, usedNames);
|
|
125
144
|
const filePath = join(targetPath, fileName);
|
|
126
|
-
|
|
145
|
+
|
|
146
|
+
// Extract content to files for AI agents
|
|
147
|
+
const processedIntegration = extractIntegrationContent(integration, targetPath, fileName);
|
|
148
|
+
|
|
149
|
+
writeIntegrationFile(filePath, sanitizeIntegrationForFile(processedIntegration));
|
|
127
150
|
logger.step(`Pulled: ${getIntegrationDisplayName(integration) || integration._id || fileName}`);
|
|
128
151
|
});
|
|
129
152
|
|
|
@@ -154,8 +177,12 @@ export async function pushIntegrations(sdk, path, options = {}) {
|
|
|
154
177
|
try {
|
|
155
178
|
const integrationData = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
156
179
|
validateIntegrationPayload(integrationData, file);
|
|
157
|
-
|
|
158
|
-
|
|
180
|
+
|
|
181
|
+
// Resolve any $ref references in the integration
|
|
182
|
+
const resolvedIntegration = await resolveReferences(integrationData, path);
|
|
183
|
+
|
|
184
|
+
const payload = toRequestPayload(resolvedIntegration);
|
|
185
|
+
const displayName = getIntegrationDisplayName(resolvedIntegration) || file.replace(INTEGRATION_FILE_EXTENSION, '');
|
|
159
186
|
|
|
160
187
|
logger.step(`Pushing integration: ${displayName}`);
|
|
161
188
|
|
|
@@ -168,8 +195,13 @@ export async function pushIntegrations(sdk, path, options = {}) {
|
|
|
168
195
|
logger.success(`Created: ${displayName}`);
|
|
169
196
|
}
|
|
170
197
|
|
|
198
|
+
// Re-extract content to files to maintain $ref structure
|
|
199
|
+
// This ensures pre_messages are stored as prompt md files after pushing
|
|
200
|
+
const processedResponse = extractIntegrationContent(response, path, file);
|
|
201
|
+
|
|
171
202
|
// Persist returned integration (with _id) back to disk
|
|
172
|
-
writeIntegrationFile(filePath, sanitizeIntegrationForFile(
|
|
203
|
+
writeIntegrationFile(filePath, sanitizeIntegrationForFile(processedResponse));
|
|
204
|
+
|
|
173
205
|
results.push({ status: 'fulfilled' });
|
|
174
206
|
} catch (error) {
|
|
175
207
|
logger.error(`Failed to push integration file ${file}`, error);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { logger } from './logger.mjs';
|
|
4
|
+
import { loadReference } from './file-refs.mjs';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Convert a string to kebab-case
|
|
@@ -59,41 +60,6 @@ export function extractMicroFrontendStructures(microFrontends, pluginPath) {
|
|
|
59
60
|
});
|
|
60
61
|
}
|
|
61
62
|
|
|
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
63
|
/**
|
|
98
64
|
* Resolve micro-frontend structures from $ref references
|
|
99
65
|
* @param {Array} microFrontends - Array of micro-frontend objects
|
package/services/plugins.mjs
CHANGED
|
@@ -4,6 +4,48 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { logger } from './logger.mjs';
|
|
5
5
|
import { extractMicroFrontendStructures, resolveMicroFrontendStructures } from './micro-frontends.mjs';
|
|
6
6
|
|
|
7
|
+
function sanitizePluginForFile(plugin) {
|
|
8
|
+
const sanitized = JSON.parse(JSON.stringify(plugin));
|
|
9
|
+
|
|
10
|
+
// Remove _id from internal objects in arrays
|
|
11
|
+
if (Array.isArray(sanitized.subscribedEvents)) {
|
|
12
|
+
sanitized.subscribedEvents.forEach(item => {
|
|
13
|
+
if (item._id) delete item._id;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (Array.isArray(sanitized.microFrontends)) {
|
|
18
|
+
sanitized.microFrontends.forEach(mfe => {
|
|
19
|
+
if (mfe._id) delete mfe._id;
|
|
20
|
+
if (Array.isArray(mfe.requires)) {
|
|
21
|
+
mfe.requires.forEach(item => {
|
|
22
|
+
if (item._id) delete item._id;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (Array.isArray(sanitized.injectables)) {
|
|
29
|
+
sanitized.injectables.forEach(item => {
|
|
30
|
+
if (item._id) delete item._id;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (Array.isArray(sanitized.navBarGroups)) {
|
|
35
|
+
sanitized.navBarGroups.forEach(item => {
|
|
36
|
+
if (item._id) delete item._id;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (Array.isArray(sanitized.cruds)) {
|
|
41
|
+
sanitized.cruds.forEach(item => {
|
|
42
|
+
if (item._id) delete item._id;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return sanitized;
|
|
47
|
+
}
|
|
48
|
+
|
|
7
49
|
/**
|
|
8
50
|
* Push plugins from local directory to remote
|
|
9
51
|
* @param {Object} sdk - Initialized SDK instance
|
|
@@ -179,7 +221,10 @@ export async function pullPlugins(sdk, targetPath) {
|
|
|
179
221
|
cruds: (fullPlugin.cruds || []).map(removeIdFromObject),
|
|
180
222
|
}
|
|
181
223
|
|
|
182
|
-
|
|
224
|
+
// Sanitize the plugin to remove any remaining _id fields from internal objects
|
|
225
|
+
const sanitizedPlugin = sanitizePluginForFile(relevantFields);
|
|
226
|
+
|
|
227
|
+
fs.writeFileSync(filePath, JSON.stringify(sanitizedPlugin, null, 2), 'utf-8');
|
|
183
228
|
logger.step(`Pulled: ${plugin.apiPath}`);
|
|
184
229
|
}));
|
|
185
230
|
|