@qelos/plugins-cli 0.0.20 → 0.0.21
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 +256 -0
- package/services/integrations.mjs +12 -3
- package/services/micro-frontends.mjs +1 -35
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,256 @@
|
|
|
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 && typeof preMessage.content === 'string') {
|
|
138
|
+
const content = preMessage.content;
|
|
139
|
+
|
|
140
|
+
// Skip if content is empty or just whitespace
|
|
141
|
+
if (!content.trim()) {
|
|
142
|
+
return updatedIntegration;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Generate filename
|
|
146
|
+
const baseName = path.basename(fileName, '.integration.json');
|
|
147
|
+
const mdFileName = `${baseName}.md`;
|
|
148
|
+
const mdFilePath = path.join(extractDir, mdFileName);
|
|
149
|
+
const relativeRef = `./prompts/${mdFileName}`;
|
|
150
|
+
|
|
151
|
+
// Write content to file
|
|
152
|
+
fs.writeFileSync(mdFilePath, content, 'utf-8');
|
|
153
|
+
logger.debug(`Extracted pre_message content to: ${relativeRef}`);
|
|
154
|
+
|
|
155
|
+
// Replace with $ref
|
|
156
|
+
updatedIntegration.target.details.pre_messages[0].content = { $ref: relativeRef };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return updatedIntegration;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Extract all integrations that have content to be externalized
|
|
164
|
+
* @param {Array} integrations - Array of integration objects
|
|
165
|
+
* @param {string} integrationPath - Path where integration files are stored
|
|
166
|
+
* @returns {Array} Updated integrations with $ref objects
|
|
167
|
+
*/
|
|
168
|
+
export function extractAllIntegrationContent(integrations, integrationPath) {
|
|
169
|
+
return integrations.map((integration, index) => {
|
|
170
|
+
// Generate a filename for this integration
|
|
171
|
+
const displayName = integration?.trigger?.details?.name ||
|
|
172
|
+
integration?.target?.details?.name ||
|
|
173
|
+
integration?._id ||
|
|
174
|
+
`integration-${index + 1}`;
|
|
175
|
+
const fileName = `${toKebabCase(displayName)}.integration.json`;
|
|
176
|
+
|
|
177
|
+
return extractIntegrationContent(integration, integrationPath, fileName);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
export function extractConfigContent(config, configPath) {
|
|
181
|
+
const extractionConfig = CONFIG_EXTRACTION_MAP[config.key];
|
|
182
|
+
|
|
183
|
+
if (!extractionConfig || !config.metadata) {
|
|
184
|
+
return config;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const updatedConfig = JSON.parse(JSON.stringify(config)); // Deep clone
|
|
188
|
+
const { fields, fileExtension, subdirectory } = extractionConfig;
|
|
189
|
+
|
|
190
|
+
// Create subdirectory if needed
|
|
191
|
+
const extractDir = path.join(configPath, subdirectory);
|
|
192
|
+
if (!fs.existsSync(extractDir)) {
|
|
193
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
194
|
+
logger.debug(`Created directory: ${extractDir}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Extract each field
|
|
198
|
+
for (const field of fields) {
|
|
199
|
+
if (updatedConfig.metadata[field] && typeof updatedConfig.metadata[field] === 'string') {
|
|
200
|
+
const content = updatedConfig.metadata[field];
|
|
201
|
+
|
|
202
|
+
// Skip if content is empty or just whitespace
|
|
203
|
+
if (!content.trim()) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Generate filename - use config key if only one field, otherwise include field name
|
|
208
|
+
let fileName;
|
|
209
|
+
if (fields.length === 1) {
|
|
210
|
+
fileName = `${config.key}${fileExtension}`;
|
|
211
|
+
} else {
|
|
212
|
+
fileName = `${config.key}-${field}${fileExtension}`;
|
|
213
|
+
}
|
|
214
|
+
const filePath = path.join(extractDir, fileName);
|
|
215
|
+
const relativeRef = `./${subdirectory}/${fileName}`;
|
|
216
|
+
|
|
217
|
+
// Write content to file
|
|
218
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
219
|
+
logger.debug(`Extracted ${field} to: ${relativeRef}`);
|
|
220
|
+
|
|
221
|
+
// Replace with $ref
|
|
222
|
+
updatedConfig.metadata[field] = { $ref: relativeRef };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return updatedConfig;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Extract all configurations that have content to be externalized
|
|
231
|
+
* @param {Array} configs - Array of configuration objects
|
|
232
|
+
* @param {string} configPath - Path where config files are stored
|
|
233
|
+
* @returns {Array} Updated configurations with $ref objects
|
|
234
|
+
*/
|
|
235
|
+
export function extractAllConfigContent(configs, configPath) {
|
|
236
|
+
return configs.map(config => extractConfigContent(config, configPath));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Recursively find all $ref objects in an object
|
|
241
|
+
* @param {any} obj - Object to search
|
|
242
|
+
* @param {Array} refs - Array to collect found references (used internally)
|
|
243
|
+
* @returns {Array} Array of found $ref paths
|
|
244
|
+
*/
|
|
245
|
+
export function findAllRefs(obj, refs = []) {
|
|
246
|
+
if (Array.isArray(obj)) {
|
|
247
|
+
obj.forEach(item => findAllRefs(item, refs));
|
|
248
|
+
} else if (obj && typeof obj === 'object') {
|
|
249
|
+
if (obj.$ref && typeof obj.$ref === 'string') {
|
|
250
|
+
refs.push(obj.$ref);
|
|
251
|
+
} else {
|
|
252
|
+
Object.values(obj).forEach(value => findAllRefs(value, refs));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return refs;
|
|
256
|
+
}
|
|
@@ -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';
|
|
@@ -123,7 +124,11 @@ export async function pullIntegrations(sdk, targetPath) {
|
|
|
123
124
|
integrations.forEach((integration, index) => {
|
|
124
125
|
const fileName = buildFileName(integration, index, usedNames);
|
|
125
126
|
const filePath = join(targetPath, fileName);
|
|
126
|
-
|
|
127
|
+
|
|
128
|
+
// Extract content to files for AI agents
|
|
129
|
+
const processedIntegration = extractIntegrationContent(integration, targetPath, fileName);
|
|
130
|
+
|
|
131
|
+
writeIntegrationFile(filePath, sanitizeIntegrationForFile(processedIntegration));
|
|
127
132
|
logger.step(`Pulled: ${getIntegrationDisplayName(integration) || integration._id || fileName}`);
|
|
128
133
|
});
|
|
129
134
|
|
|
@@ -154,8 +159,12 @@ export async function pushIntegrations(sdk, path, options = {}) {
|
|
|
154
159
|
try {
|
|
155
160
|
const integrationData = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
156
161
|
validateIntegrationPayload(integrationData, file);
|
|
157
|
-
|
|
158
|
-
|
|
162
|
+
|
|
163
|
+
// Resolve any $ref references in the integration
|
|
164
|
+
const resolvedIntegration = await resolveReferences(integrationData, path);
|
|
165
|
+
|
|
166
|
+
const payload = toRequestPayload(resolvedIntegration);
|
|
167
|
+
const displayName = getIntegrationDisplayName(resolvedIntegration) || file.replace(INTEGRATION_FILE_EXTENSION, '');
|
|
159
168
|
|
|
160
169
|
logger.step(`Pushing integration: ${displayName}`);
|
|
161
170
|
|
|
@@ -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
|