@qelos/plugins-cli 0.0.13 → 0.0.15
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/cli.mjs +4 -0
- package/commands/blueprints.mjs +22 -0
- package/commands/generate.mjs +15 -0
- package/controllers/blueprints.mjs +336 -0
- package/controllers/create.mjs +1 -1
- package/controllers/generate.mjs +32 -0
- package/controllers/pull.mjs +1 -1
- package/controllers/push.mjs +21 -11
- package/package.json +2 -1
- package/services/blocks.mjs +9 -3
- package/services/blueprints.mjs +9 -3
- package/services/components.mjs +10 -4
- package/services/configurations.mjs +9 -3
- package/services/generate-rules.mjs +667 -0
- package/services/plugins.mjs +9 -3
package/cli.mjs
CHANGED
|
@@ -9,6 +9,8 @@ import process from 'node:process';
|
|
|
9
9
|
import createCommand from './commands/create.mjs';
|
|
10
10
|
import pushCommand from './commands/push.mjs';
|
|
11
11
|
import pullCommand from './commands/pull.mjs';
|
|
12
|
+
import generateCommand from './commands/generate.mjs';
|
|
13
|
+
import blueprintsCommand from './commands/blueprints.mjs';
|
|
12
14
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
15
|
|
|
14
16
|
const program = yargs(hideBin(process.argv));
|
|
@@ -24,5 +26,7 @@ program.option('verbose', {
|
|
|
24
26
|
createCommand(program)
|
|
25
27
|
pushCommand(program)
|
|
26
28
|
pullCommand(program)
|
|
29
|
+
generateCommand(program)
|
|
30
|
+
blueprintsCommand(program)
|
|
27
31
|
|
|
28
32
|
program.help().argv;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import blueprintsController from "../controllers/blueprints.mjs";
|
|
2
|
+
|
|
3
|
+
export default function blueprintsCommand(program) {
|
|
4
|
+
program
|
|
5
|
+
.command('blueprints generate [path]', 'generate new blueprints from actual database tables/collections',
|
|
6
|
+
(yargs) => {
|
|
7
|
+
return yargs
|
|
8
|
+
.positional('path', {
|
|
9
|
+
describe: 'Path to store the pulled resources.',
|
|
10
|
+
type: 'string',
|
|
11
|
+
default: './blueprints',
|
|
12
|
+
required: false
|
|
13
|
+
})
|
|
14
|
+
.option('uri', {
|
|
15
|
+
describe: 'The URI of the database. Defaults to mongodb://localhost:27017/db',
|
|
16
|
+
type: 'string',
|
|
17
|
+
required: false,
|
|
18
|
+
default: 'mongodb://localhost:27017/db'
|
|
19
|
+
})
|
|
20
|
+
},
|
|
21
|
+
blueprintsController)
|
|
22
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import generateController from "../controllers/generate.mjs";
|
|
2
|
+
|
|
3
|
+
export default function generateCommand(program) {
|
|
4
|
+
program
|
|
5
|
+
.command('generate rules <type>', 'Generate IDE-specific rules files for working with pulled Qelos resources.',
|
|
6
|
+
(yargs) => {
|
|
7
|
+
return yargs
|
|
8
|
+
.positional('type', {
|
|
9
|
+
describe: 'Type of IDE rules to generate. Can be windsurf, cursor, claude, or all.',
|
|
10
|
+
type: 'string',
|
|
11
|
+
choices: ['windsurf', 'cursor', 'claude', 'all']
|
|
12
|
+
})
|
|
13
|
+
},
|
|
14
|
+
generateController)
|
|
15
|
+
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { MongoClient } from "mongodb";
|
|
4
|
+
import { logger } from "../services/logger.mjs";
|
|
5
|
+
|
|
6
|
+
const SUPPORTED_PROTOCOL = /^mongodb:\/\//i;
|
|
7
|
+
const SAMPLE_SIZE = 50;
|
|
8
|
+
|
|
9
|
+
export default async function blueprintsController({ uri, path: targetPath }) {
|
|
10
|
+
const connectionUri = uri || "mongodb://localhost:27017/db";
|
|
11
|
+
|
|
12
|
+
if (!SUPPORTED_PROTOCOL.test(connectionUri)) {
|
|
13
|
+
logger.error("Only mongodb:// URIs are supported at the moment.");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const targetDir = path.join(process.cwd(), targetPath);
|
|
18
|
+
ensureDirectory(targetDir);
|
|
19
|
+
|
|
20
|
+
const client = new MongoClient(connectionUri, {
|
|
21
|
+
serverSelectionTimeoutMS: 10_000,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
await client.connect();
|
|
26
|
+
const dbName = getDatabaseName(connectionUri);
|
|
27
|
+
const db = client.db(dbName);
|
|
28
|
+
|
|
29
|
+
logger.section(`Connected to MongoDB database: ${db.databaseName}`);
|
|
30
|
+
|
|
31
|
+
const collections = await db.listCollections().toArray();
|
|
32
|
+
const filteredCollections = collections.filter(
|
|
33
|
+
(collection) => !collection.name.startsWith("system.")
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
if (filteredCollections.length === 0) {
|
|
37
|
+
logger.warning("No collections found to generate blueprints from.");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const collection of filteredCollections) {
|
|
42
|
+
await generateBlueprintForCollection(db, collection.name, targetDir);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
logger.success(
|
|
46
|
+
`Generated ${filteredCollections.length} blueprint file(s) in ${targetDir}`
|
|
47
|
+
);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
logger.error("Failed to generate blueprints", error);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
} finally {
|
|
52
|
+
await client.close().catch(() => {});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function ensureDirectory(targetDir) {
|
|
57
|
+
if (!fs.existsSync(targetDir)) {
|
|
58
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
59
|
+
logger.info(`Created output directory at ${targetDir}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function generateBlueprintForCollection(db, collectionName, targetDir) {
|
|
64
|
+
logger.step(`Analyzing collection: ${collectionName}`);
|
|
65
|
+
try {
|
|
66
|
+
const collection = db.collection(collectionName);
|
|
67
|
+
const documents = await sampleCollectionDocuments(collection);
|
|
68
|
+
const properties = buildProperties(collectionName, documents);
|
|
69
|
+
|
|
70
|
+
const blueprint = createBlueprintPayload(collectionName, properties);
|
|
71
|
+
const filePath = path.join(
|
|
72
|
+
targetDir,
|
|
73
|
+
`${blueprint.identifier}.blueprint.json`
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
fs.writeFileSync(filePath, JSON.stringify(blueprint, null, 2));
|
|
77
|
+
logger.success(`Blueprint generated: ${filePath}`);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
logger.error(`Failed to process collection ${collectionName}`, error);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function buildProperties(collectionName, documents) {
|
|
84
|
+
const properties = {};
|
|
85
|
+
let processedDocuments = 0;
|
|
86
|
+
|
|
87
|
+
for (const doc of documents) {
|
|
88
|
+
if (processedDocuments >= SAMPLE_SIZE) {
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
processedDocuments += 1;
|
|
92
|
+
if (!doc || typeof doc !== "object") continue;
|
|
93
|
+
|
|
94
|
+
for (const [key, value] of Object.entries(doc)) {
|
|
95
|
+
if (shouldSkipField(key) || properties[key]) continue;
|
|
96
|
+
properties[key] = createPropertyDescriptor(key, value, collectionName);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (Object.keys(properties).length === 0) {
|
|
101
|
+
logger.warning(
|
|
102
|
+
`No properties detected for collection ${collectionName}. Generated blueprint will contain empty properties.`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return properties;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function createPropertyDescriptor(key, sampleValue, collectionName) {
|
|
110
|
+
const { normalizedValue, multi } = normalizeSampleValue(sampleValue);
|
|
111
|
+
const type = detectBlueprintType(normalizedValue);
|
|
112
|
+
|
|
113
|
+
const descriptor = {
|
|
114
|
+
title: formatTitle(key),
|
|
115
|
+
type,
|
|
116
|
+
description: "",
|
|
117
|
+
required: false,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
if (multi) {
|
|
121
|
+
descriptor.multi = true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (type === "object") {
|
|
125
|
+
descriptor.schema = buildObjectSchema(normalizedValue);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return descriptor;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function normalizeSampleValue(value) {
|
|
132
|
+
if (Array.isArray(value)) {
|
|
133
|
+
const firstValue = value.find(
|
|
134
|
+
(item) => item !== null && item !== undefined
|
|
135
|
+
);
|
|
136
|
+
return { normalizedValue: firstValue ?? null, multi: true };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { normalizedValue: value, multi: false };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function detectBlueprintType(value) {
|
|
143
|
+
if (value === null || value === undefined) {
|
|
144
|
+
return "string";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (value instanceof Date) {
|
|
148
|
+
return "datetime";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (typeof value === "number") {
|
|
152
|
+
return "number";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (typeof value === "boolean") {
|
|
156
|
+
return "boolean";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (typeof value === "object") {
|
|
160
|
+
if (value?._bsontype === "ObjectId") {
|
|
161
|
+
return "string";
|
|
162
|
+
}
|
|
163
|
+
return "object";
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return "string";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function createBlueprintPayload(collectionName, properties) {
|
|
170
|
+
const singularName = ensureSingular(collectionName);
|
|
171
|
+
return {
|
|
172
|
+
identifier: toIdentifier(singularName),
|
|
173
|
+
name: formatTitle(singularName),
|
|
174
|
+
description: `Auto-generated blueprint for MongoDB collection "${collectionName}"`,
|
|
175
|
+
entityIdentifierMechanism: "objectid",
|
|
176
|
+
permissions: createDefaultPermissions(),
|
|
177
|
+
permissionScope: "workspace",
|
|
178
|
+
properties,
|
|
179
|
+
relations: [],
|
|
180
|
+
dispatchers: {
|
|
181
|
+
create: false,
|
|
182
|
+
update: false,
|
|
183
|
+
delete: false,
|
|
184
|
+
},
|
|
185
|
+
limitations: [],
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function createDefaultPermissions() {
|
|
190
|
+
const operations = ["create", "read", "update", "delete"];
|
|
191
|
+
return operations.map((operation) => ({
|
|
192
|
+
scope: "workspace",
|
|
193
|
+
operation,
|
|
194
|
+
guest: false,
|
|
195
|
+
roleBased: ["*"],
|
|
196
|
+
workspaceRoleBased: ["*"],
|
|
197
|
+
workspaceLabelsBased: ["*"],
|
|
198
|
+
}));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function toIdentifier(collectionName) {
|
|
202
|
+
const sanitized = collectionName
|
|
203
|
+
.trim()
|
|
204
|
+
.toLowerCase()
|
|
205
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
206
|
+
.replace(/^_+|_+$/g, "");
|
|
207
|
+
|
|
208
|
+
return sanitized || `collection_${Date.now()}`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function formatTitle(value) {
|
|
212
|
+
return value
|
|
213
|
+
.replace(/[_-]+/g, " ")
|
|
214
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
215
|
+
.replace(/\s+/g, " ")
|
|
216
|
+
.trim()
|
|
217
|
+
.replace(/\b\w/g, (match) => match.toUpperCase());
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function sampleCollectionDocuments(collection) {
|
|
221
|
+
try {
|
|
222
|
+
return await collection
|
|
223
|
+
.aggregate([{ $sample: { size: SAMPLE_SIZE } }])
|
|
224
|
+
.toArray();
|
|
225
|
+
} catch (error) {
|
|
226
|
+
logger.debug(
|
|
227
|
+
`Falling back to sequential sampling for ${collection.collectionName}: ${error.message}`
|
|
228
|
+
);
|
|
229
|
+
return collection.find({}).limit(SAMPLE_SIZE).toArray();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function buildObjectSchema(sample, depth = 0) {
|
|
234
|
+
const MAX_SCHEMA_DEPTH = 3;
|
|
235
|
+
if (!sample || typeof sample !== "object" || depth >= MAX_SCHEMA_DEPTH) {
|
|
236
|
+
return { type: "object" };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const properties = {};
|
|
240
|
+
|
|
241
|
+
for (const [key, value] of Object.entries(sample)) {
|
|
242
|
+
if (key === "_id" || value === undefined) continue;
|
|
243
|
+
properties[key] = buildSchemaFromValue(value, depth + 1);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (Object.keys(properties).length === 0) {
|
|
247
|
+
return { type: "object" };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
type: "object",
|
|
252
|
+
properties,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function buildSchemaFromValue(value, depth) {
|
|
257
|
+
if (Array.isArray(value)) {
|
|
258
|
+
const arraySample = value.find(
|
|
259
|
+
(item) => item !== null && item !== undefined
|
|
260
|
+
);
|
|
261
|
+
const itemsSchema = arraySample
|
|
262
|
+
? buildSchemaFromValue(arraySample, depth + 1)
|
|
263
|
+
: { type: "string" };
|
|
264
|
+
return {
|
|
265
|
+
type: "array",
|
|
266
|
+
items: itemsSchema,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const valueType = detectBlueprintType(value);
|
|
271
|
+
|
|
272
|
+
if (valueType === "object" && value && typeof value === "object") {
|
|
273
|
+
return buildObjectSchema(value, depth + 1);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
type: mapBlueprintTypeToJsonSchema(valueType),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function mapBlueprintTypeToJsonSchema(type) {
|
|
282
|
+
switch (type) {
|
|
283
|
+
case "number":
|
|
284
|
+
return "number";
|
|
285
|
+
case "boolean":
|
|
286
|
+
return "boolean";
|
|
287
|
+
case "object":
|
|
288
|
+
return "object";
|
|
289
|
+
default:
|
|
290
|
+
return "string";
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function shouldSkipField(key) {
|
|
295
|
+
if (!key) return true;
|
|
296
|
+
const normalized = key.toLowerCase();
|
|
297
|
+
if (
|
|
298
|
+
normalized === "_id" ||
|
|
299
|
+
normalized === "id" ||
|
|
300
|
+
normalized === "user" ||
|
|
301
|
+
normalized === "userid" ||
|
|
302
|
+
normalized === "workspace" ||
|
|
303
|
+
normalized === "workspaceid"
|
|
304
|
+
) {
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
return key.startsWith("__");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function ensureSingular(value = "") {
|
|
311
|
+
const normalized = value.trim();
|
|
312
|
+
const lower = normalized.toLowerCase();
|
|
313
|
+
|
|
314
|
+
if (lower.endsWith("ies")) {
|
|
315
|
+
return normalized.slice(0, -3) + normalized.slice(-3).replace(/ies$/i, "y");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (/(sses|xes|zes|ches|shes)$/i.test(lower)) {
|
|
319
|
+
return normalized.slice(0, -2);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (lower.endsWith("s") && !lower.endsWith("ss")) {
|
|
323
|
+
return normalized.slice(0, -1);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return normalized;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function getDatabaseName(connectionUri) {
|
|
330
|
+
try {
|
|
331
|
+
const parsed = new URL(connectionUri);
|
|
332
|
+
return parsed.pathname.replace(/^\//, "") || undefined;
|
|
333
|
+
} catch {
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
}
|
package/controllers/create.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import follow from "follow-redirects";
|
|
|
2
2
|
import cliSelect from "cli-select";
|
|
3
3
|
import { blue } from "../utils/colors.mjs";
|
|
4
4
|
import DecompressZip from "decompress-zip";
|
|
5
|
-
import { join } from "path";
|
|
5
|
+
import { join } from "node:path";
|
|
6
6
|
import { rimraf } from "rimraf";
|
|
7
7
|
import ProgressBar from "../utils/progress-bar.mjs";
|
|
8
8
|
import * as readline from "node:readline";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { generateRules } from '../services/generate-rules.mjs';
|
|
2
|
+
import { logger } from '../services/logger.mjs';
|
|
3
|
+
|
|
4
|
+
export default async function generateController({ type }) {
|
|
5
|
+
try {
|
|
6
|
+
const cwd = process.cwd();
|
|
7
|
+
|
|
8
|
+
logger.section(`Generating ${type} rules for ${cwd}`);
|
|
9
|
+
|
|
10
|
+
// Determine which IDE types to generate
|
|
11
|
+
const ideTypes = type === 'all'
|
|
12
|
+
? ['windsurf', 'cursor', 'claude']
|
|
13
|
+
: [type];
|
|
14
|
+
|
|
15
|
+
for (const ideType of ideTypes) {
|
|
16
|
+
logger.step(`Generating ${ideType} rules...`);
|
|
17
|
+
const result = await generateRules(ideType, cwd);
|
|
18
|
+
|
|
19
|
+
if (result.success) {
|
|
20
|
+
logger.success(`Generated ${ideType} rules at: ${result.filePath}`);
|
|
21
|
+
} else {
|
|
22
|
+
logger.warning(`Skipped ${ideType}: ${result.message}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
logger.success(`Rules generation completed`);
|
|
27
|
+
|
|
28
|
+
} catch (error) {
|
|
29
|
+
logger.error(`Failed to generate rules`, error);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
}
|
package/controllers/pull.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { logger } from '../services/logger.mjs';
|
|
|
8
8
|
import fs from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
|
|
11
|
-
export default async function pullController({ type, path: targetPath }) {
|
|
11
|
+
export default async function pullController({ type, path: targetPath = './' }) {
|
|
12
12
|
try {
|
|
13
13
|
// Validate parent directory exists
|
|
14
14
|
const parentDir = path.dirname(targetPath);
|
package/controllers/push.mjs
CHANGED
|
@@ -17,10 +17,16 @@ export default async function pushController({ type, path: sourcePath }) {
|
|
|
17
17
|
process.exit(1);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
const stat = fs.statSync(sourcePath);
|
|
21
|
+
let basePath = sourcePath;
|
|
22
|
+
let targetFile = null;
|
|
23
|
+
|
|
24
|
+
if (stat.isFile()) {
|
|
25
|
+
basePath = path.dirname(sourcePath);
|
|
26
|
+
targetFile = path.basename(sourcePath);
|
|
27
|
+
logger.info(`Detected file path. Only pushing ${targetFile}`);
|
|
28
|
+
} else if (!stat.isDirectory()) {
|
|
29
|
+
logger.error(`Path must be a file or directory: ${sourcePath}`);
|
|
24
30
|
process.exit(1);
|
|
25
31
|
}
|
|
26
32
|
|
|
@@ -28,6 +34,10 @@ export default async function pushController({ type, path: sourcePath }) {
|
|
|
28
34
|
|
|
29
35
|
// Handle "all" or "*" type
|
|
30
36
|
if (type === 'all' || type === '*') {
|
|
37
|
+
if (targetFile) {
|
|
38
|
+
logger.error('Cannot push "all" using a single file. Please provide a directory path.');
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
31
41
|
logger.section(`Pushing all resources from ${sourcePath}`);
|
|
32
42
|
|
|
33
43
|
const types = [
|
|
@@ -39,7 +49,7 @@ export default async function pushController({ type, path: sourcePath }) {
|
|
|
39
49
|
];
|
|
40
50
|
|
|
41
51
|
for (const { name, fn } of types) {
|
|
42
|
-
const typePath = path.join(
|
|
52
|
+
const typePath = path.join(basePath, name);
|
|
43
53
|
|
|
44
54
|
// Skip if directory doesn't exist
|
|
45
55
|
if (!fs.existsSync(typePath)) {
|
|
@@ -60,18 +70,18 @@ export default async function pushController({ type, path: sourcePath }) {
|
|
|
60
70
|
return;
|
|
61
71
|
}
|
|
62
72
|
|
|
63
|
-
logger.section(`Pushing ${type} from ${
|
|
73
|
+
logger.section(`Pushing ${type} from ${targetFile ? `${basePath} (${targetFile})` : basePath}`);
|
|
64
74
|
|
|
65
75
|
if (type === 'components') {
|
|
66
|
-
await pushComponents(sdk,
|
|
76
|
+
await pushComponents(sdk, basePath, { targetFile });
|
|
67
77
|
} else if (type === 'blueprints') {
|
|
68
|
-
await pushBlueprints(sdk,
|
|
78
|
+
await pushBlueprints(sdk, basePath, { targetFile });
|
|
69
79
|
} else if (type === 'plugins') {
|
|
70
|
-
await pushPlugins(sdk,
|
|
80
|
+
await pushPlugins(sdk, basePath, { targetFile });
|
|
71
81
|
} else if (type === 'blocks') {
|
|
72
|
-
await pushBlocks(sdk,
|
|
82
|
+
await pushBlocks(sdk, basePath, { targetFile });
|
|
73
83
|
} else if (type === 'config' || type === 'configs' || type === 'configuration') {
|
|
74
|
-
await pushConfigurations(sdk,
|
|
84
|
+
await pushConfigurations(sdk, basePath, { targetFile });
|
|
75
85
|
} else {
|
|
76
86
|
logger.error(`Unknown type: ${type}`);
|
|
77
87
|
logger.info('Supported types: components, blueprints, plugins, blocks, config, configs, configuration, all');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qelos/plugins-cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
4
4
|
"description": "CLI to manage QELOS plugins",
|
|
5
5
|
"main": "cli.mjs",
|
|
6
6
|
"bin": {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"decompress-zip": "^0.3.3",
|
|
21
21
|
"follow-redirects": "^1.15.11",
|
|
22
22
|
"jiti": "^2.6.1",
|
|
23
|
+
"mongodb": "^7.0.0",
|
|
23
24
|
"rimraf": "^6.0.1",
|
|
24
25
|
"yargs": "^18.0.0",
|
|
25
26
|
"zx": "^8.8.5"
|
package/services/blocks.mjs
CHANGED
|
@@ -17,12 +17,18 @@ function toKebabCase(str) {
|
|
|
17
17
|
* @param {Object} sdk - Initialized SDK instance
|
|
18
18
|
* @param {string} path - Path to blocks directory
|
|
19
19
|
*/
|
|
20
|
-
export async function pushBlocks(sdk, path) {
|
|
21
|
-
const
|
|
20
|
+
export async function pushBlocks(sdk, path, options = {}) {
|
|
21
|
+
const { targetFile } = options;
|
|
22
|
+
const directoryFiles = fs.readdirSync(path);
|
|
23
|
+
const files = targetFile ? [targetFile] : directoryFiles;
|
|
22
24
|
const blockFiles = files.filter(f => f.endsWith('.html'));
|
|
23
25
|
|
|
24
26
|
if (blockFiles.length === 0) {
|
|
25
|
-
|
|
27
|
+
if (targetFile) {
|
|
28
|
+
logger.warning(`File ${targetFile} is not an .html block. Skipping.`);
|
|
29
|
+
} else {
|
|
30
|
+
logger.warning(`No .html files found in ${path}`);
|
|
31
|
+
}
|
|
26
32
|
return;
|
|
27
33
|
}
|
|
28
34
|
|
package/services/blueprints.mjs
CHANGED
|
@@ -7,12 +7,18 @@ import { logger } from './logger.mjs';
|
|
|
7
7
|
* @param {Object} sdk - Initialized SDK instance
|
|
8
8
|
* @param {string} path - Path to blueprints directory
|
|
9
9
|
*/
|
|
10
|
-
export async function pushBlueprints(sdk, path) {
|
|
11
|
-
const
|
|
10
|
+
export async function pushBlueprints(sdk, path, options = {}) {
|
|
11
|
+
const { targetFile } = options;
|
|
12
|
+
const directoryFiles = fs.readdirSync(path);
|
|
13
|
+
const files = targetFile ? [targetFile] : directoryFiles;
|
|
12
14
|
const blueprintFiles = files.filter(f => f.endsWith('.blueprint.json'));
|
|
13
15
|
|
|
14
16
|
if (blueprintFiles.length === 0) {
|
|
15
|
-
|
|
17
|
+
if (targetFile) {
|
|
18
|
+
logger.warning(`File ${targetFile} is not a .blueprint.json file. Skipping.`);
|
|
19
|
+
} else {
|
|
20
|
+
logger.warning(`No blueprint files (*.blueprint.json) found in ${path}`);
|
|
21
|
+
}
|
|
16
22
|
return;
|
|
17
23
|
}
|
|
18
24
|
|
package/services/components.mjs
CHANGED
|
@@ -7,12 +7,18 @@ import { logger } from './logger.mjs';
|
|
|
7
7
|
* @param {Object} sdk - Initialized SDK instance
|
|
8
8
|
* @param {string} path - Path to components directory
|
|
9
9
|
*/
|
|
10
|
-
export async function pushComponents(sdk, path) {
|
|
11
|
-
const
|
|
10
|
+
export async function pushComponents(sdk, path, options = {}) {
|
|
11
|
+
const { targetFile } = options;
|
|
12
|
+
const directoryFiles = fs.readdirSync(path);
|
|
13
|
+
const files = targetFile ? [targetFile] : directoryFiles;
|
|
12
14
|
const vueFiles = files.filter(f => f.endsWith('.vue'));
|
|
13
15
|
|
|
14
16
|
if (vueFiles.length === 0) {
|
|
15
|
-
|
|
17
|
+
if (targetFile) {
|
|
18
|
+
logger.warning(`File ${targetFile} is not a .vue component. Skipping.`);
|
|
19
|
+
} else {
|
|
20
|
+
logger.warning(`No .vue files found in ${path}`);
|
|
21
|
+
}
|
|
16
22
|
return;
|
|
17
23
|
}
|
|
18
24
|
|
|
@@ -30,7 +36,7 @@ export async function pushComponents(sdk, path) {
|
|
|
30
36
|
|
|
31
37
|
const existingComponents = await sdk.components.getList();
|
|
32
38
|
|
|
33
|
-
await Promise.all(
|
|
39
|
+
await Promise.all(vueFiles.map(async (file) => {
|
|
34
40
|
if (file.endsWith('.vue')) {
|
|
35
41
|
const componentName = file.replace('.vue', '');
|
|
36
42
|
const info = componentsJson[componentName] || {};
|
|
@@ -8,12 +8,18 @@ import { appUrl } from './sdk.mjs';
|
|
|
8
8
|
* @param {Object} sdk - Initialized SDK instance
|
|
9
9
|
* @param {string} path - Path to configurations directory
|
|
10
10
|
*/
|
|
11
|
-
export async function pushConfigurations(sdk, path) {
|
|
12
|
-
const
|
|
11
|
+
export async function pushConfigurations(sdk, path, options = {}) {
|
|
12
|
+
const { targetFile } = options;
|
|
13
|
+
const directoryFiles = fs.readdirSync(path);
|
|
14
|
+
const files = targetFile ? [targetFile] : directoryFiles;
|
|
13
15
|
const configFiles = files.filter(f => f.endsWith('.config.json'));
|
|
14
16
|
|
|
15
17
|
if (configFiles.length === 0) {
|
|
16
|
-
|
|
18
|
+
if (targetFile) {
|
|
19
|
+
logger.warning(`File ${targetFile} is not a .config.json file. Skipping.`);
|
|
20
|
+
} else {
|
|
21
|
+
logger.warning(`No configuration files (*.config.json) found in ${path}`);
|
|
22
|
+
}
|
|
17
23
|
return;
|
|
18
24
|
}
|
|
19
25
|
|
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { logger } from './logger.mjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Fetch Qelos global components from documentation
|
|
7
|
+
* @returns {Promise<Object>} Object with components and directives
|
|
8
|
+
*/
|
|
9
|
+
async function fetchQelosGlobalComponents() {
|
|
10
|
+
try {
|
|
11
|
+
const response = await fetch('https://docs.qelos.io/pre-designed-frontends/components/');
|
|
12
|
+
if (!response.ok) {
|
|
13
|
+
logger.debug('Failed to fetch Qelos components documentation');
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const html = await response.text();
|
|
18
|
+
|
|
19
|
+
// Extract component names and descriptions from the HTML
|
|
20
|
+
const components = [];
|
|
21
|
+
const directives = [];
|
|
22
|
+
|
|
23
|
+
// Documented components with links and descriptions
|
|
24
|
+
const documentedComponents = [
|
|
25
|
+
{ name: 'ai-chat', description: 'Complete AI chat interface with streaming, file attachments, and customizable UI' },
|
|
26
|
+
{ name: 'form-input', description: 'Input component for forms' },
|
|
27
|
+
{ name: 'form-row-group', description: 'Group form inputs in rows' },
|
|
28
|
+
{ name: 'save-button', description: 'Button for saving forms' },
|
|
29
|
+
{ name: 'monaco', description: 'Code editor component' },
|
|
30
|
+
{ name: 'quick-table', description: 'Simplified table component' },
|
|
31
|
+
{ name: 'v-chart', description: 'Chart visualization component' },
|
|
32
|
+
{ name: 'content-box', description: 'Component that loads HTML content blocks from the database' },
|
|
33
|
+
{ name: 'copy-to-clipboard', description: 'Button to copy content to clipboard' },
|
|
34
|
+
{ name: 'empty-state', description: 'Component for empty state display' },
|
|
35
|
+
{ name: 'life-cycle', description: 'Component for displaying lifecycle stages' },
|
|
36
|
+
{ name: 'q-pre', description: 'Pre-formatted text component with HTML escaping and line break handling' }
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
// Other available components
|
|
40
|
+
const otherComponents = [
|
|
41
|
+
{ name: 'edit-header', description: 'Header for edit pages' },
|
|
42
|
+
{ name: 'info-icon', description: 'Icon with tooltip information' },
|
|
43
|
+
{ name: 'block-item', description: 'Block container for content' },
|
|
44
|
+
{ name: 'list-page-title', description: 'Title component for list pages' },
|
|
45
|
+
{ name: 'general-form', description: 'Generic form component' },
|
|
46
|
+
{ name: 'blueprint-entity-form', description: 'Form for blueprint entities' },
|
|
47
|
+
{ name: 'confirm-message', description: 'Confirmation dialog component' },
|
|
48
|
+
{ name: 'remove-button', description: 'Button for deletion actions' },
|
|
49
|
+
{ name: 'editable-content', description: 'Content that can be edited inline' },
|
|
50
|
+
{ name: 'remove-confirmation', description: 'Confirmation dialog for delete actions' },
|
|
51
|
+
{ name: 'stats-card', description: 'Card for displaying statistics' },
|
|
52
|
+
{ name: 'q-rating', description: 'Rating component' }
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
components.push(...documentedComponents, ...otherComponents);
|
|
56
|
+
|
|
57
|
+
// Directives
|
|
58
|
+
directives.push({ name: 'v-loading', description: 'Adds loading state to an element' });
|
|
59
|
+
|
|
60
|
+
return { components, directives };
|
|
61
|
+
} catch (error) {
|
|
62
|
+
logger.debug(`Failed to fetch Qelos components: ${error.message}`);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Scan directory for pulled resources
|
|
69
|
+
* @param {string} basePath - Base path to scan
|
|
70
|
+
* @returns {Object} Object containing found resources
|
|
71
|
+
*/
|
|
72
|
+
function scanPulledResources(basePath) {
|
|
73
|
+
const resources = {
|
|
74
|
+
components: null,
|
|
75
|
+
blocks: null,
|
|
76
|
+
blueprints: [],
|
|
77
|
+
plugins: [],
|
|
78
|
+
microFrontends: []
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Check for components directory and components.json
|
|
82
|
+
const componentsPath = path.join(basePath, 'components');
|
|
83
|
+
const componentsJsonPath = path.join(componentsPath, 'components.json');
|
|
84
|
+
if (fs.existsSync(componentsJsonPath)) {
|
|
85
|
+
try {
|
|
86
|
+
resources.components = {
|
|
87
|
+
path: componentsPath,
|
|
88
|
+
metadata: JSON.parse(fs.readFileSync(componentsJsonPath, 'utf-8'))
|
|
89
|
+
};
|
|
90
|
+
} catch (error) {
|
|
91
|
+
logger.debug(`Failed to parse components.json: ${error.message}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check for blocks directory and blocks.json
|
|
96
|
+
const blocksPath = path.join(basePath, 'blocks');
|
|
97
|
+
const blocksJsonPath = path.join(blocksPath, 'blocks.json');
|
|
98
|
+
if (fs.existsSync(blocksJsonPath)) {
|
|
99
|
+
try {
|
|
100
|
+
resources.blocks = {
|
|
101
|
+
path: blocksPath,
|
|
102
|
+
metadata: JSON.parse(fs.readFileSync(blocksJsonPath, 'utf-8'))
|
|
103
|
+
};
|
|
104
|
+
} catch (error) {
|
|
105
|
+
logger.debug(`Failed to parse blocks.json: ${error.message}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check for blueprints directory
|
|
110
|
+
const blueprintsPath = path.join(basePath, 'blueprints');
|
|
111
|
+
if (fs.existsSync(blueprintsPath)) {
|
|
112
|
+
const blueprintFiles = fs.readdirSync(blueprintsPath)
|
|
113
|
+
.filter(f => f.endsWith('.blueprint.json'));
|
|
114
|
+
|
|
115
|
+
for (const file of blueprintFiles) {
|
|
116
|
+
try {
|
|
117
|
+
const blueprint = JSON.parse(
|
|
118
|
+
fs.readFileSync(path.join(blueprintsPath, file), 'utf-8')
|
|
119
|
+
);
|
|
120
|
+
resources.blueprints.push({
|
|
121
|
+
file,
|
|
122
|
+
identifier: blueprint.identifier,
|
|
123
|
+
name: blueprint.name,
|
|
124
|
+
properties: blueprint.properties,
|
|
125
|
+
relations: blueprint.relations,
|
|
126
|
+
dispatchers: blueprint.dispatchers
|
|
127
|
+
});
|
|
128
|
+
} catch (error) {
|
|
129
|
+
logger.debug(`Failed to parse ${file}: ${error.message}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Check for plugins directory
|
|
135
|
+
const pluginsPath = path.join(basePath, 'plugins');
|
|
136
|
+
if (fs.existsSync(pluginsPath)) {
|
|
137
|
+
const pluginFiles = fs.readdirSync(pluginsPath)
|
|
138
|
+
.filter(f => f.endsWith('.plugin.json'));
|
|
139
|
+
|
|
140
|
+
for (const file of pluginFiles) {
|
|
141
|
+
try {
|
|
142
|
+
const plugin = JSON.parse(
|
|
143
|
+
fs.readFileSync(path.join(pluginsPath, file), 'utf-8')
|
|
144
|
+
);
|
|
145
|
+
resources.plugins.push({
|
|
146
|
+
file,
|
|
147
|
+
apiPath: plugin.apiPath,
|
|
148
|
+
name: plugin.name,
|
|
149
|
+
microFrontends: plugin.microFrontends || [],
|
|
150
|
+
injectables: plugin.injectables || [],
|
|
151
|
+
navBarGroups: plugin.navBarGroups || []
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Check for micro-frontends directory
|
|
155
|
+
const microFrontendsDir = path.join(pluginsPath, 'micro-frontends');
|
|
156
|
+
if (fs.existsSync(microFrontendsDir)) {
|
|
157
|
+
const htmlFiles = fs.readdirSync(microFrontendsDir)
|
|
158
|
+
.filter(f => f.endsWith('.html'));
|
|
159
|
+
|
|
160
|
+
for (const htmlFile of htmlFiles) {
|
|
161
|
+
resources.microFrontends.push({
|
|
162
|
+
file: htmlFile,
|
|
163
|
+
pluginApiPath: plugin.apiPath,
|
|
164
|
+
path: path.join(microFrontendsDir, htmlFile)
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
} catch (error) {
|
|
169
|
+
logger.debug(`Failed to parse ${file}: ${error.message}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return resources;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Generate rules content based on scanned resources
|
|
179
|
+
* @param {Object} resources - Scanned resources
|
|
180
|
+
* @param {string} ideType - Type of IDE (windsurf, cursor, claude)
|
|
181
|
+
* @param {Object} qelosComponents - Qelos global components from docs
|
|
182
|
+
* @returns {string} Generated rules content
|
|
183
|
+
*/
|
|
184
|
+
function generateRulesContent(resources, ideType, qelosComponents) {
|
|
185
|
+
const sections = [];
|
|
186
|
+
|
|
187
|
+
// Header
|
|
188
|
+
sections.push(`# Qelos Project Rules for ${ideType.charAt(0).toUpperCase() + ideType.slice(1)}`);
|
|
189
|
+
sections.push('');
|
|
190
|
+
sections.push('This file contains rules to help you work with pulled Qelos resources.');
|
|
191
|
+
sections.push('Generated automatically by the Qelos CLI.');
|
|
192
|
+
sections.push('');
|
|
193
|
+
|
|
194
|
+
// Components section
|
|
195
|
+
if (resources.components) {
|
|
196
|
+
sections.push('## Components');
|
|
197
|
+
sections.push('');
|
|
198
|
+
sections.push('### Component Structure');
|
|
199
|
+
sections.push('- Components are Vue 3.5 Single File Components (.vue files)');
|
|
200
|
+
sections.push('- Each component has metadata stored in `components.json`');
|
|
201
|
+
sections.push('- Use Composition API with `<script setup>` syntax');
|
|
202
|
+
sections.push('- Components can use Vue Router, Element Plus, and Vue I18n');
|
|
203
|
+
sections.push('');
|
|
204
|
+
sections.push('### Available Libraries & Documentation');
|
|
205
|
+
sections.push('- **Vue 3**: https://vuejs.org/api/');
|
|
206
|
+
sections.push('- **Vue Router**: https://router.vuejs.org/api/');
|
|
207
|
+
sections.push('- **Element Plus**: https://element-plus.org/en-US/component/overview.html');
|
|
208
|
+
sections.push('- **Vue I18n**: https://vue-i18n.intlify.dev/api/general.html');
|
|
209
|
+
sections.push('- **Pinia**: https://pinia.vuejs.org/api/');
|
|
210
|
+
sections.push('');
|
|
211
|
+
sections.push('### Qelos SDK Access');
|
|
212
|
+
sections.push('Components have access to the Qelos SDK instance via the `@sdk` alias:');
|
|
213
|
+
sections.push('```javascript');
|
|
214
|
+
sections.push('import sdk from "@sdk";');
|
|
215
|
+
sections.push('');
|
|
216
|
+
sections.push('// sdk is an instance of QelosAdministratorSDK');
|
|
217
|
+
sections.push('// Available methods include:');
|
|
218
|
+
sections.push('// - sdk.manageComponents');
|
|
219
|
+
sections.push('// - sdk.manageBlueprints');
|
|
220
|
+
sections.push('// - sdk.managePlugins');
|
|
221
|
+
sections.push('// - sdk.manageConfigurations');
|
|
222
|
+
sections.push('// - And more...');
|
|
223
|
+
sections.push('```');
|
|
224
|
+
sections.push('');
|
|
225
|
+
sections.push('**SDK Documentation**: https://docs.qelos.io/sdk/sdk');
|
|
226
|
+
sections.push('');
|
|
227
|
+
sections.push('### Qelos Global Components');
|
|
228
|
+
sections.push('All components and HTML templates can use Qelos pre-designed components **without importing them**.');
|
|
229
|
+
sections.push('These components are globally registered and available everywhere in kebab-case format.');
|
|
230
|
+
sections.push('');
|
|
231
|
+
sections.push('**Documentation**: https://docs.qelos.io/pre-designed-frontends/components/');
|
|
232
|
+
sections.push('');
|
|
233
|
+
|
|
234
|
+
if (qelosComponents && qelosComponents.components && qelosComponents.components.length > 0) {
|
|
235
|
+
sections.push('**Available Components:**');
|
|
236
|
+
qelosComponents.components.forEach(comp => {
|
|
237
|
+
sections.push(`- \`<${comp.name}>\` - ${comp.description}`);
|
|
238
|
+
});
|
|
239
|
+
sections.push('');
|
|
240
|
+
|
|
241
|
+
if (qelosComponents.directives && qelosComponents.directives.length > 0) {
|
|
242
|
+
sections.push('**Available Directives:**');
|
|
243
|
+
qelosComponents.directives.forEach(dir => {
|
|
244
|
+
sections.push(`- \`${dir.name}\` - ${dir.description}`);
|
|
245
|
+
});
|
|
246
|
+
sections.push('');
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
// Fallback if fetch failed
|
|
250
|
+
sections.push('**Note**: Visit the documentation link above for the complete list of available components.');
|
|
251
|
+
sections.push('');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
sections.push('**Usage Example:**');
|
|
255
|
+
sections.push('```html');
|
|
256
|
+
sections.push('<form-input label="Name" v-model="name"></form-input>');
|
|
257
|
+
sections.push('<save-button @click="saveData"></save-button>');
|
|
258
|
+
sections.push('<content-box title="User Information">Content goes here</content-box>');
|
|
259
|
+
sections.push('<div v-loading="isLoading">Loading content...</div>');
|
|
260
|
+
sections.push('```');
|
|
261
|
+
sections.push('');
|
|
262
|
+
sections.push('**Important**: All components must use kebab-case and have closing tags.');
|
|
263
|
+
sections.push('');
|
|
264
|
+
sections.push('### Component Metadata Mapping');
|
|
265
|
+
sections.push('The `components.json` file maps component names to their metadata:');
|
|
266
|
+
sections.push('```json');
|
|
267
|
+
sections.push('{');
|
|
268
|
+
sections.push(' "VideoPlayer": {');
|
|
269
|
+
sections.push(' "_id": "507f1f77bcf86cd799439011",');
|
|
270
|
+
sections.push(' "componentName": "VideoPlayer",');
|
|
271
|
+
sections.push(' "identifier": "video-player-component",');
|
|
272
|
+
sections.push(' "description": "A reusable video player component"');
|
|
273
|
+
sections.push(' },');
|
|
274
|
+
sections.push(' "UserProfile": {');
|
|
275
|
+
sections.push(' "_id": "507f191e810c19729de860ea",');
|
|
276
|
+
sections.push(' "componentName": "UserProfile",');
|
|
277
|
+
sections.push(' "identifier": "user-profile-component",');
|
|
278
|
+
sections.push(' "description": "Displays user profile information"');
|
|
279
|
+
sections.push(' }');
|
|
280
|
+
sections.push('}');
|
|
281
|
+
sections.push('```');
|
|
282
|
+
sections.push('');
|
|
283
|
+
sections.push('### Component Naming Convention');
|
|
284
|
+
sections.push('Components follow Vue naming conventions:');
|
|
285
|
+
sections.push('- **File names**: PascalCase (e.g., `ProductCard.vue`, `VideoPlayer.vue`)');
|
|
286
|
+
sections.push('- **Template usage**: kebab-case (e.g., `<product-card>`, `<video-player>`)');
|
|
287
|
+
sections.push('- **Conversion**: PascalCase file names are automatically converted to kebab-case in templates');
|
|
288
|
+
sections.push('');
|
|
289
|
+
sections.push('**Example mapping:**');
|
|
290
|
+
sections.push('```');
|
|
291
|
+
sections.push('ProductCard.vue → <product-card>');
|
|
292
|
+
sections.push('VideoPlayer.vue → <video-player>');
|
|
293
|
+
sections.push('UserProfile.vue → <user-profile>');
|
|
294
|
+
sections.push('DataTable.vue → <data-table>');
|
|
295
|
+
sections.push('```');
|
|
296
|
+
sections.push('');
|
|
297
|
+
sections.push('When you see a component used in HTML/templates like `<product-card>`,');
|
|
298
|
+
sections.push('the actual component file is `ProductCard.vue` in the components directory.');
|
|
299
|
+
sections.push('');
|
|
300
|
+
sections.push('### Working with Components');
|
|
301
|
+
sections.push('- When modifying a component, update both the `.vue` file and its entry in `components.json`');
|
|
302
|
+
sections.push('- The `_id` field in `components.json` is used to sync with the remote Qelos instance');
|
|
303
|
+
sections.push('- Component files are named exactly as their `componentName` property (e.g., `VideoPlayer.vue`)');
|
|
304
|
+
sections.push('- Use `qelos-cli push components <path>` to push changes back to Qelos');
|
|
305
|
+
sections.push('');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Blocks section
|
|
309
|
+
if (resources.blocks) {
|
|
310
|
+
sections.push('## Blocks');
|
|
311
|
+
sections.push('');
|
|
312
|
+
sections.push('### Block Structure');
|
|
313
|
+
sections.push('- Blocks are HTML template files (.html files)');
|
|
314
|
+
sections.push('- Each block has metadata stored in `blocks.json`');
|
|
315
|
+
sections.push('- Blocks are used as reusable HTML templates in the Qelos platform');
|
|
316
|
+
sections.push('');
|
|
317
|
+
sections.push('### IMPORTANT: Block Limitations');
|
|
318
|
+
sections.push('**Blocks CANNOT contain `<script>` tags or JavaScript code.**');
|
|
319
|
+
sections.push('');
|
|
320
|
+
sections.push('To add JavaScript functionality to a block:');
|
|
321
|
+
sections.push('1. Create a new Vue component in the `components/` folder');
|
|
322
|
+
sections.push('2. Implement your JavaScript logic in the component');
|
|
323
|
+
sections.push('3. Use the component in your block HTML using kebab-case with closing tags');
|
|
324
|
+
sections.push('4. No import statement is needed - components are globally available');
|
|
325
|
+
sections.push('');
|
|
326
|
+
sections.push('**Example:**');
|
|
327
|
+
sections.push('```html');
|
|
328
|
+
sections.push('<!-- ❌ WRONG: Do not add <script> tags in blocks -->');
|
|
329
|
+
sections.push('<div id="my-element"></div>');
|
|
330
|
+
sections.push('<script>');
|
|
331
|
+
sections.push(' document.getElementById("my-element").addEventListener("click", ...);');
|
|
332
|
+
sections.push('</script>');
|
|
333
|
+
sections.push('');
|
|
334
|
+
sections.push('<!-- ✅ CORRECT: Create a component and use it -->');
|
|
335
|
+
sections.push('<!-- First create components/InteractiveButton.vue with your logic -->');
|
|
336
|
+
sections.push('<interactive-button></interactive-button>');
|
|
337
|
+
sections.push('```');
|
|
338
|
+
sections.push('');
|
|
339
|
+
sections.push('Blocks can contain:');
|
|
340
|
+
sections.push('- HTML markup');
|
|
341
|
+
sections.push('- CSS in `<style>` tags');
|
|
342
|
+
sections.push('- Vue components (kebab-case, with closing tags)');
|
|
343
|
+
sections.push('- Qelos global components (ai-chat, form-input, etc.)');
|
|
344
|
+
sections.push('');
|
|
345
|
+
sections.push('### Block Metadata Mapping');
|
|
346
|
+
sections.push('The `blocks.json` file maps block filenames (kebab-case) to their metadata:');
|
|
347
|
+
sections.push('```json');
|
|
348
|
+
sections.push('{');
|
|
349
|
+
sections.push(' "login-header": {');
|
|
350
|
+
sections.push(' "_id": "507f1f77bcf86cd799439011",');
|
|
351
|
+
sections.push(' "name": "Login Header",');
|
|
352
|
+
sections.push(' "description": "Header component for login page",');
|
|
353
|
+
sections.push(' "contentType": "html"');
|
|
354
|
+
sections.push(' },');
|
|
355
|
+
sections.push(' "footer-template": {');
|
|
356
|
+
sections.push(' "_id": "507f191e810c19729de860ea",');
|
|
357
|
+
sections.push(' "name": "Footer Template",');
|
|
358
|
+
sections.push(' "description": "Reusable footer template",');
|
|
359
|
+
sections.push(' "contentType": "html"');
|
|
360
|
+
sections.push(' }');
|
|
361
|
+
sections.push('}');
|
|
362
|
+
sections.push('```');
|
|
363
|
+
sections.push('');
|
|
364
|
+
sections.push('### Working with Blocks');
|
|
365
|
+
sections.push('- When modifying a block, update both the `.html` file and its entry in `blocks.json`');
|
|
366
|
+
sections.push('- The filename in kebab-case must match the key in `blocks.json`');
|
|
367
|
+
sections.push('- Block names are converted to kebab-case for filenames (e.g., "Login Header" → `login-header.html`)');
|
|
368
|
+
sections.push('- The `_id` field is used to sync with the remote Qelos instance');
|
|
369
|
+
sections.push('- Use `qelos-cli push blocks <path>` to push changes back to Qelos');
|
|
370
|
+
sections.push('');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Blueprints section
|
|
374
|
+
if (resources.blueprints.length > 0) {
|
|
375
|
+
sections.push('## Blueprints');
|
|
376
|
+
sections.push('');
|
|
377
|
+
sections.push('### Blueprint Structure');
|
|
378
|
+
sections.push('Blueprints define data models and entity structures in Qelos. Each blueprint file contains:');
|
|
379
|
+
sections.push('');
|
|
380
|
+
sections.push('```typescript');
|
|
381
|
+
sections.push('interface IBlueprint {');
|
|
382
|
+
sections.push(' identifier: string; // Unique identifier');
|
|
383
|
+
sections.push(' name: string; // Display name');
|
|
384
|
+
sections.push(' description?: string; // Description');
|
|
385
|
+
sections.push(' entityIdentifierMechanism: string; // "objectid" or "guid"');
|
|
386
|
+
sections.push(' properties: Record<string, PropertyDescriptor>; // Entity properties/fields');
|
|
387
|
+
sections.push(' relations: { key: string, target: string }[]; // Relations to other blueprints');
|
|
388
|
+
sections.push(' dispatchers: { // Event dispatchers');
|
|
389
|
+
sections.push(' create: boolean,');
|
|
390
|
+
sections.push(' update: boolean,');
|
|
391
|
+
sections.push(' delete: boolean');
|
|
392
|
+
sections.push(' };');
|
|
393
|
+
sections.push(' permissions: Array<PermissionsDescriptor>; // Access permissions');
|
|
394
|
+
sections.push(' permissionScope: string; // "user", "workspace", or "tenant"');
|
|
395
|
+
sections.push(' limitations?: Array<Limitation>; // Usage limitations');
|
|
396
|
+
sections.push('}');
|
|
397
|
+
sections.push('```');
|
|
398
|
+
sections.push('');
|
|
399
|
+
sections.push('### Blueprint Example');
|
|
400
|
+
sections.push('```json');
|
|
401
|
+
sections.push('{');
|
|
402
|
+
sections.push(' "identifier": "product",');
|
|
403
|
+
sections.push(' "name": "Product",');
|
|
404
|
+
sections.push(' "description": "Product catalog item",');
|
|
405
|
+
sections.push(' "entityIdentifierMechanism": "objectid",');
|
|
406
|
+
sections.push(' "permissionScope": "workspace",');
|
|
407
|
+
sections.push(' "properties": {');
|
|
408
|
+
sections.push(' "name": {');
|
|
409
|
+
sections.push(' "title": "Product Name",');
|
|
410
|
+
sections.push(' "type": "string",');
|
|
411
|
+
sections.push(' "description": "Name of the product",');
|
|
412
|
+
sections.push(' "required": true');
|
|
413
|
+
sections.push(' },');
|
|
414
|
+
sections.push(' "price": {');
|
|
415
|
+
sections.push(' "title": "Price",');
|
|
416
|
+
sections.push(' "type": "number",');
|
|
417
|
+
sections.push(' "description": "Product price",');
|
|
418
|
+
sections.push(' "required": true,');
|
|
419
|
+
sections.push(' "min": 0');
|
|
420
|
+
sections.push(' },');
|
|
421
|
+
sections.push(' "inStock": {');
|
|
422
|
+
sections.push(' "title": "In Stock",');
|
|
423
|
+
sections.push(' "type": "boolean",');
|
|
424
|
+
sections.push(' "description": "Whether product is in stock",');
|
|
425
|
+
sections.push(' "required": false');
|
|
426
|
+
sections.push(' }');
|
|
427
|
+
sections.push(' },');
|
|
428
|
+
sections.push(' "relations": [');
|
|
429
|
+
sections.push(' {');
|
|
430
|
+
sections.push(' "key": "category",');
|
|
431
|
+
sections.push(' "target": "product_category"');
|
|
432
|
+
sections.push(' }');
|
|
433
|
+
sections.push(' ],');
|
|
434
|
+
sections.push(' "dispatchers": {');
|
|
435
|
+
sections.push(' "create": true,');
|
|
436
|
+
sections.push(' "update": true,');
|
|
437
|
+
sections.push(' "delete": false');
|
|
438
|
+
sections.push(' },');
|
|
439
|
+
sections.push(' "permissions": [');
|
|
440
|
+
sections.push(' {');
|
|
441
|
+
sections.push(' "scope": "workspace",');
|
|
442
|
+
sections.push(' "operation": "create",');
|
|
443
|
+
sections.push(' "roleBased": ["admin", "editor"]');
|
|
444
|
+
sections.push(' }');
|
|
445
|
+
sections.push(' ]');
|
|
446
|
+
sections.push('}');
|
|
447
|
+
sections.push('```');
|
|
448
|
+
sections.push('');
|
|
449
|
+
sections.push('### Blueprint to Entity Mapping');
|
|
450
|
+
sections.push('When working with blueprint entities:');
|
|
451
|
+
sections.push('- **Properties** define the fields available on each entity instance');
|
|
452
|
+
sections.push(' - Properties is a Record (object) where keys are field names');
|
|
453
|
+
sections.push(' - Example: A `product` entity will have `name`, `price`, and `inStock` fields');
|
|
454
|
+
sections.push('- **Relations** define how entities connect to other blueprint entities');
|
|
455
|
+
sections.push(' - Each relation has a `key` (field name) and `target` (blueprint identifier)');
|
|
456
|
+
sections.push(' - Example: `category` relation to `product_category` blueprint');
|
|
457
|
+
sections.push('- **Dispatchers** define which CRUD operations trigger events');
|
|
458
|
+
sections.push(' - Boolean flags for `create`, `update`, and `delete` operations');
|
|
459
|
+
sections.push(' - Example: Product creation and updates trigger events, but deletion does not');
|
|
460
|
+
sections.push('- **Permissions** control who can perform operations on entities');
|
|
461
|
+
sections.push(' - Scoped at user, workspace, or tenant level');
|
|
462
|
+
sections.push(' - Can be role-based or label-based');
|
|
463
|
+
sections.push('- Use the blueprint structure to understand what data is available when building components');
|
|
464
|
+
sections.push('');
|
|
465
|
+
sections.push('### Working with Blueprints');
|
|
466
|
+
sections.push('- Blueprint files are named as `{identifier}.blueprint.json`');
|
|
467
|
+
sections.push('- When building components that display blueprint entities, reference the properties structure');
|
|
468
|
+
sections.push('- Use relations to understand how to fetch related entity data');
|
|
469
|
+
sections.push('- Consider dispatchers when implementing entity lifecycle hooks');
|
|
470
|
+
sections.push('- Use `qelos-cli push blueprints <path>` to push changes back to Qelos');
|
|
471
|
+
sections.push('');
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Plugins section
|
|
475
|
+
if (resources.plugins.length > 0) {
|
|
476
|
+
sections.push('## Plugins');
|
|
477
|
+
sections.push('');
|
|
478
|
+
sections.push('### Plugin Structure');
|
|
479
|
+
sections.push('Plugins extend Qelos functionality and can include:');
|
|
480
|
+
sections.push('- **Micro-frontends**: UI components loaded dynamically');
|
|
481
|
+
sections.push('- **Injectables**: Services or utilities injected into the platform');
|
|
482
|
+
sections.push('- **Navigation groups**: Menu items and navigation structure');
|
|
483
|
+
sections.push('');
|
|
484
|
+
sections.push('### Plugin Files and References');
|
|
485
|
+
sections.push('Each plugin has:');
|
|
486
|
+
sections.push('- A main `.plugin.json` file with plugin configuration');
|
|
487
|
+
sections.push('- A `micro-frontends/` directory containing HTML structure files');
|
|
488
|
+
sections.push('- Micro-frontend structures are referenced using `{ "$ref": "./micro-frontends/filename.html" }`');
|
|
489
|
+
sections.push('');
|
|
490
|
+
sections.push('### Plugin Example');
|
|
491
|
+
sections.push('```json');
|
|
492
|
+
sections.push('{');
|
|
493
|
+
sections.push(' "name": "Agent Editor Plugin",');
|
|
494
|
+
sections.push(' "apiPath": "agent-editor",');
|
|
495
|
+
sections.push(' "description": "Plugin for agent editing functionality",');
|
|
496
|
+
sections.push(' "microFrontends": [');
|
|
497
|
+
sections.push(' {');
|
|
498
|
+
sections.push(' "name": "Agent Editor",');
|
|
499
|
+
sections.push(' "route": {');
|
|
500
|
+
sections.push(' "name": "agent-editor",');
|
|
501
|
+
sections.push(' "path": "/editor/:agentId",');
|
|
502
|
+
sections.push(' "requirements": {');
|
|
503
|
+
sections.push(' "permissions": ["agent.edit"],');
|
|
504
|
+
sections.push(' "blueprints": ["agent"]');
|
|
505
|
+
sections.push(' }');
|
|
506
|
+
sections.push(' },');
|
|
507
|
+
sections.push(' "structure": {');
|
|
508
|
+
sections.push(' "$ref": "./micro-frontends/agent-editor.html"');
|
|
509
|
+
sections.push(' }');
|
|
510
|
+
sections.push(' }');
|
|
511
|
+
sections.push(' ],');
|
|
512
|
+
sections.push(' "navBarGroups": [');
|
|
513
|
+
sections.push(' {');
|
|
514
|
+
sections.push(' "label": "Agent Tools",');
|
|
515
|
+
sections.push(' "items": [...]');
|
|
516
|
+
sections.push(' }');
|
|
517
|
+
sections.push(' ],');
|
|
518
|
+
sections.push(' "injectables": [...]');
|
|
519
|
+
sections.push('}');
|
|
520
|
+
sections.push('```');
|
|
521
|
+
sections.push('');
|
|
522
|
+
sections.push('### Micro-frontend Structure Reference');
|
|
523
|
+
sections.push('The `$ref` field points to an HTML file in the `micro-frontends/` directory:');
|
|
524
|
+
sections.push('- **Route name**: Used to identify the micro-frontend (converted to kebab-case for filename)');
|
|
525
|
+
sections.push('- **Route path**: The URL path where the micro-frontend is accessible');
|
|
526
|
+
sections.push('- **Requirements**: Conditions that must be met for the micro-frontend to load');
|
|
527
|
+
sections.push(' - `permissions`: Required user permissions');
|
|
528
|
+
sections.push(' - `blueprints`: Required blueprint entities');
|
|
529
|
+
sections.push('- **Structure file**: HTML template referenced via `$ref`');
|
|
530
|
+
sections.push('');
|
|
531
|
+
sections.push('### IMPORTANT: Micro-frontend HTML Limitations');
|
|
532
|
+
sections.push('**Micro-frontend HTML files CANNOT contain `<script>` tags or JavaScript code.**');
|
|
533
|
+
sections.push('');
|
|
534
|
+
sections.push('Just like blocks, to add JavaScript functionality:');
|
|
535
|
+
sections.push('1. Create a Vue component in `components/` folder with your logic');
|
|
536
|
+
sections.push('2. Use the component in your micro-frontend HTML (kebab-case, with closing tags)');
|
|
537
|
+
sections.push('3. No import needed - all components are globally available');
|
|
538
|
+
sections.push('');
|
|
539
|
+
sections.push('### Using Components in Micro-frontends');
|
|
540
|
+
sections.push('Micro-frontend HTML files can use components from the `components/` directory:');
|
|
541
|
+
sections.push('```html');
|
|
542
|
+
sections.push('<!-- In micro-frontends/agent-editor.html -->');
|
|
543
|
+
sections.push('<agent-editor');
|
|
544
|
+
sections.push(' :agent-id="currentAgent.id"');
|
|
545
|
+
sections.push(' :autoplay="true"');
|
|
546
|
+
sections.push(' @ended="handleAgentEnd"');
|
|
547
|
+
sections.push('/>');
|
|
548
|
+
sections.push('```');
|
|
549
|
+
sections.push('');
|
|
550
|
+
sections.push('This `<agent-editor>` component maps to:');
|
|
551
|
+
sections.push('- **Component file**: `components/AgentEditor.vue`');
|
|
552
|
+
sections.push('- **Metadata entry**: `components.json["AgentEditor"]`');
|
|
553
|
+
sections.push('');
|
|
554
|
+
sections.push('Remember: kebab-case in templates = PascalCase component file name.');
|
|
555
|
+
sections.push('');
|
|
556
|
+
sections.push('### Working with Plugins');
|
|
557
|
+
sections.push('- Plugin files are named as `{apiPath}.plugin.json`');
|
|
558
|
+
sections.push('- When modifying micro-frontend structures, edit the HTML files in `micro-frontends/`');
|
|
559
|
+
sections.push('- The plugin JSON file references these HTML files using `$ref`');
|
|
560
|
+
sections.push('- Route names and paths are defined in the plugin configuration');
|
|
561
|
+
sections.push('- Requirements specify what conditions must be met for the micro-frontend to load');
|
|
562
|
+
sections.push('- Use `qelos-cli push plugins <path>` to push changes back to Qelos');
|
|
563
|
+
sections.push('');
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// General guidelines
|
|
567
|
+
sections.push('## General Guidelines');
|
|
568
|
+
sections.push('');
|
|
569
|
+
sections.push('### File Naming Conventions');
|
|
570
|
+
sections.push('- Components: `ComponentName.vue`');
|
|
571
|
+
sections.push('- Blocks: `block-name.html` (kebab-case)');
|
|
572
|
+
sections.push('- Blueprints: `identifier.blueprint.json`');
|
|
573
|
+
sections.push('- Plugins: `api-path.plugin.json`');
|
|
574
|
+
sections.push('- Micro-frontends: `route-name.html` (kebab-case)');
|
|
575
|
+
sections.push('');
|
|
576
|
+
sections.push('### Metadata Files');
|
|
577
|
+
sections.push('- `components.json`: Maps component filenames to metadata');
|
|
578
|
+
sections.push('- `blocks.json`: Maps block filenames to metadata');
|
|
579
|
+
sections.push('- Always keep metadata files in sync with their corresponding files');
|
|
580
|
+
sections.push('- The `_id` field is crucial for syncing with the remote Qelos instance');
|
|
581
|
+
sections.push('');
|
|
582
|
+
sections.push('### Best Practices');
|
|
583
|
+
sections.push('- Use the Qelos CLI to pull and push resources');
|
|
584
|
+
sections.push('- Maintain the directory structure created by the pull command');
|
|
585
|
+
sections.push('- Reference blueprint structures when building components that display entities');
|
|
586
|
+
sections.push('- Use micro-frontend route names and requirements to understand loading conditions');
|
|
587
|
+
sections.push('- Test changes locally before pushing to the remote Qelos instance');
|
|
588
|
+
sections.push('');
|
|
589
|
+
|
|
590
|
+
return sections.join('\n');
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Get the appropriate file path for the IDE type
|
|
595
|
+
* @param {string} ideType - Type of IDE
|
|
596
|
+
* @param {string} basePath - Base path
|
|
597
|
+
* @returns {string} File path for the rules file
|
|
598
|
+
*/
|
|
599
|
+
function getRulesFilePath(ideType, basePath) {
|
|
600
|
+
switch (ideType) {
|
|
601
|
+
case 'windsurf':
|
|
602
|
+
return path.join(basePath, '.windsurf', 'rules', 'qelos-resources.md');
|
|
603
|
+
case 'cursor':
|
|
604
|
+
return path.join(basePath, '.cursorrules');
|
|
605
|
+
case 'claude':
|
|
606
|
+
return path.join(basePath, '.clinerules');
|
|
607
|
+
default:
|
|
608
|
+
throw new Error(`Unknown IDE type: ${ideType}`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Generate rules file for a specific IDE
|
|
614
|
+
* @param {string} ideType - Type of IDE (windsurf, cursor, claude)
|
|
615
|
+
* @param {string} basePath - Base path to scan for resources
|
|
616
|
+
* @returns {Object} Result object with success status and file path
|
|
617
|
+
*/
|
|
618
|
+
export async function generateRules(ideType, basePath) {
|
|
619
|
+
try {
|
|
620
|
+
// Scan for pulled resources
|
|
621
|
+
const resources = scanPulledResources(basePath);
|
|
622
|
+
|
|
623
|
+
// Check if any resources were found
|
|
624
|
+
const hasResources =
|
|
625
|
+
resources.components !== null ||
|
|
626
|
+
resources.blocks !== null ||
|
|
627
|
+
resources.blueprints.length > 0 ||
|
|
628
|
+
resources.plugins.length > 0;
|
|
629
|
+
|
|
630
|
+
if (!hasResources) {
|
|
631
|
+
return {
|
|
632
|
+
success: false,
|
|
633
|
+
message: 'No pulled resources found. Run pull command first.'
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Fetch Qelos global components from documentation
|
|
638
|
+
logger.debug('Fetching Qelos global components from documentation...');
|
|
639
|
+
const qelosComponents = await fetchQelosGlobalComponents();
|
|
640
|
+
if (qelosComponents) {
|
|
641
|
+
logger.debug(`Fetched ${qelosComponents.components.length} components and ${qelosComponents.directives.length} directives`);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Generate rules content
|
|
645
|
+
const content = generateRulesContent(resources, ideType, qelosComponents);
|
|
646
|
+
|
|
647
|
+
// Get file path and ensure directory exists
|
|
648
|
+
const filePath = getRulesFilePath(ideType, basePath);
|
|
649
|
+
const dir = path.dirname(filePath);
|
|
650
|
+
|
|
651
|
+
if (!fs.existsSync(dir)) {
|
|
652
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Write rules file
|
|
656
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
657
|
+
|
|
658
|
+
return {
|
|
659
|
+
success: true,
|
|
660
|
+
filePath
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
} catch (error) {
|
|
664
|
+
logger.error(`Failed to generate ${ideType} rules: ${error.message}`);
|
|
665
|
+
throw error;
|
|
666
|
+
}
|
|
667
|
+
}
|
package/services/plugins.mjs
CHANGED
|
@@ -9,12 +9,18 @@ import { extractMicroFrontendStructures, resolveMicroFrontendStructures } from '
|
|
|
9
9
|
* @param {Object} sdk - Initialized SDK instance
|
|
10
10
|
* @param {string} path - Path to plugins directory
|
|
11
11
|
*/
|
|
12
|
-
export async function pushPlugins(sdk, path) {
|
|
13
|
-
const
|
|
12
|
+
export async function pushPlugins(sdk, path, options = {}) {
|
|
13
|
+
const { targetFile } = options;
|
|
14
|
+
const directoryFiles = fs.readdirSync(path);
|
|
15
|
+
const files = targetFile ? [targetFile] : directoryFiles;
|
|
14
16
|
const pluginFiles = files.filter(f => f.endsWith('.plugin.json'));
|
|
15
17
|
|
|
16
18
|
if (pluginFiles.length === 0) {
|
|
17
|
-
|
|
19
|
+
if (targetFile) {
|
|
20
|
+
logger.warning(`File ${targetFile} is not a .plugin.json file. Skipping.`);
|
|
21
|
+
} else {
|
|
22
|
+
logger.warning(`No plugin files (*.plugin.json) found in ${path}`);
|
|
23
|
+
}
|
|
18
24
|
return;
|
|
19
25
|
}
|
|
20
26
|
|