@qelos/plugins-cli 0.0.14 → 0.0.16

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 CHANGED
@@ -10,6 +10,7 @@ import createCommand from './commands/create.mjs';
10
10
  import pushCommand from './commands/push.mjs';
11
11
  import pullCommand from './commands/pull.mjs';
12
12
  import generateCommand from './commands/generate.mjs';
13
+ import blueprintsCommand from './commands/blueprints.mjs';
13
14
  const __dirname = dirname(fileURLToPath(import.meta.url));
14
15
 
15
16
  const program = yargs(hideBin(process.argv));
@@ -26,5 +27,6 @@ createCommand(program)
26
27
  pushCommand(program)
27
28
  pullCommand(program)
28
29
  generateCommand(program)
30
+ blueprintsCommand(program)
29
31
 
30
32
  program.help().argv;
@@ -0,0 +1,28 @@
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
+ .option('guides', {
21
+ describe: 'Generate SDK guides for each blueprint',
22
+ type: 'boolean',
23
+ required: false,
24
+ default: true
25
+ })
26
+ },
27
+ blueprintsController)
28
+ }
@@ -0,0 +1,28 @@
1
+ import path from "node:path";
2
+ import { logger } from "../services/logger.mjs";
3
+ import {
4
+ SUPPORTED_PROTOCOL,
5
+ generateBlueprintsFromMongo,
6
+ } from "../services/blueprint-generator.mjs";
7
+
8
+ export default async function blueprintsController({ uri, path: targetPath, guides = true }) {
9
+ const connectionUri = uri || "mongodb://localhost:27017/db";
10
+
11
+ if (!SUPPORTED_PROTOCOL.test(connectionUri)) {
12
+ logger.error("Only mongodb:// URIs are supported at the moment.");
13
+ process.exit(1);
14
+ }
15
+
16
+ const targetDir = path.join(process.cwd(), targetPath);
17
+ const shouldGenerateGuides = guides !== false;
18
+
19
+ try {
20
+ await generateBlueprintsFromMongo({
21
+ uri: connectionUri,
22
+ targetDir,
23
+ createGuides: shouldGenerateGuides,
24
+ });
25
+ } catch {
26
+ process.exit(1);
27
+ }
28
+ }
@@ -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";
@@ -1,7 +1,5 @@
1
1
  import { generateRules } from '../services/generate-rules.mjs';
2
2
  import { logger } from '../services/logger.mjs';
3
- import path from 'node:path';
4
- import fs from 'node:fs';
5
3
 
6
4
  export default async function generateController({ type }) {
7
5
  try {
@@ -17,10 +17,16 @@ export default async function pushController({ type, path: sourcePath }) {
17
17
  process.exit(1);
18
18
  }
19
19
 
20
- // Validate path is a directory
21
- if (!fs.statSync(sourcePath).isDirectory()) {
22
- logger.error(`Path is not a directory: ${sourcePath}`);
23
- logger.info('Please provide a directory path, not a file');
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(sourcePath, name);
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 ${sourcePath}`);
73
+ logger.section(`Pushing ${type} from ${targetFile ? `${basePath} (${targetFile})` : basePath}`);
64
74
 
65
75
  if (type === 'components') {
66
- await pushComponents(sdk, sourcePath);
76
+ await pushComponents(sdk, basePath, { targetFile });
67
77
  } else if (type === 'blueprints') {
68
- await pushBlueprints(sdk, sourcePath);
78
+ await pushBlueprints(sdk, basePath, { targetFile });
69
79
  } else if (type === 'plugins') {
70
- await pushPlugins(sdk, sourcePath);
80
+ await pushPlugins(sdk, basePath, { targetFile });
71
81
  } else if (type === 'blocks') {
72
- await pushBlocks(sdk, sourcePath);
82
+ await pushBlocks(sdk, basePath, { targetFile });
73
83
  } else if (type === 'config' || type === 'configs' || type === 'configuration') {
74
- await pushConfigurations(sdk, sourcePath);
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.14",
3
+ "version": "0.0.16",
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"
@@ -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 files = fs.readdirSync(path);
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
- logger.warning(`No .html files found in ${path}`);
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
 
@@ -0,0 +1,247 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { MongoClient } from 'mongodb';
4
+ import { logger } from './logger.mjs';
5
+ import { generateSdkGuide } from './blueprint-sdk-guides.mjs';
6
+ import {
7
+ formatTitle,
8
+ toIdentifier,
9
+ ensureSingular,
10
+ detectBlueprintType,
11
+ mapBlueprintTypeToJsonSchema
12
+ } from './blueprint-shared.mjs';
13
+
14
+ export const SUPPORTED_PROTOCOL = /^mongodb:\/\//i;
15
+ const SAMPLE_SIZE = 50;
16
+
17
+ export async function generateBlueprintsFromMongo({ uri, targetDir, createGuides = true }) {
18
+ ensureDirectory(targetDir);
19
+ const client = new MongoClient(uri, { serverSelectionTimeoutMS: 10_000 });
20
+
21
+ try {
22
+ await client.connect();
23
+ const dbName = getDatabaseName(uri);
24
+ const db = client.db(dbName);
25
+
26
+ logger.section(`Connected to MongoDB database: ${db.databaseName}`);
27
+
28
+ const collections = await db.listCollections().toArray();
29
+ const filteredCollections = collections.filter(({ name }) => !name.startsWith('system.'));
30
+
31
+ if (filteredCollections.length === 0) {
32
+ logger.warning('No collections found to generate blueprints from.');
33
+ return;
34
+ }
35
+
36
+ for (const collection of filteredCollections) {
37
+ await generateBlueprintForCollection({
38
+ db,
39
+ collectionName: collection.name,
40
+ targetDir,
41
+ createGuides,
42
+ });
43
+ }
44
+
45
+ logger.success(`Generated ${filteredCollections.length} blueprint file(s) in ${targetDir}`);
46
+ } catch (error) {
47
+ logger.error('Failed to generate blueprints', error);
48
+ throw error;
49
+ } finally {
50
+ await client.close().catch(() => {});
51
+ }
52
+ }
53
+
54
+ async function generateBlueprintForCollection({ db, collectionName, targetDir, createGuides }) {
55
+ logger.step(`Analyzing collection: ${collectionName}`);
56
+ try {
57
+ const collection = db.collection(collectionName);
58
+ const documents = await sampleCollectionDocuments(collection);
59
+ const properties = buildProperties(collectionName, documents);
60
+
61
+ const blueprint = createBlueprintPayload(collectionName, properties);
62
+ const blueprintPath = path.join(targetDir, `${blueprint.identifier}.blueprint.json`);
63
+
64
+ fs.writeFileSync(blueprintPath, JSON.stringify(blueprint, null, 2));
65
+ logger.success(`Blueprint generated: ${blueprintPath}`);
66
+
67
+ if (createGuides) {
68
+ generateSdkGuide({ blueprint, documents, targetDir });
69
+ }
70
+ } catch (error) {
71
+ logger.error(`Failed to process collection ${collectionName}`, error);
72
+ }
73
+ }
74
+
75
+ function buildProperties(collectionName, documents) {
76
+ const properties = {};
77
+ let processedDocuments = 0;
78
+
79
+ for (const doc of documents) {
80
+ if (processedDocuments >= SAMPLE_SIZE) {
81
+ break;
82
+ }
83
+ processedDocuments += 1;
84
+ if (!doc || typeof doc !== 'object') continue;
85
+
86
+ for (const [key, value] of Object.entries(doc)) {
87
+ if (shouldSkipField(key) || properties[key]) continue;
88
+ properties[key] = createPropertyDescriptor(key, value, collectionName);
89
+ }
90
+ }
91
+
92
+ if (Object.keys(properties).length === 0) {
93
+ logger.warning(
94
+ `No properties detected for collection ${collectionName}. Generated blueprint will contain empty properties.`
95
+ );
96
+ }
97
+
98
+ return properties;
99
+ }
100
+
101
+ function createPropertyDescriptor(key, sampleValue, collectionName) {
102
+ const { normalizedValue, multi } = normalizeSampleValue(sampleValue);
103
+ const type = detectBlueprintType(normalizedValue);
104
+
105
+ const descriptor = {
106
+ title: formatTitle(key),
107
+ type,
108
+ description: '',
109
+ required: false
110
+ };
111
+
112
+ if (multi) {
113
+ descriptor.multi = true;
114
+ }
115
+
116
+ if (type === 'object') {
117
+ descriptor.schema = buildObjectSchema(normalizedValue);
118
+ }
119
+
120
+ return descriptor;
121
+ }
122
+
123
+ function normalizeSampleValue(value) {
124
+ if (Array.isArray(value)) {
125
+ const firstValue = value.find((item) => item !== null && item !== undefined);
126
+ return { normalizedValue: firstValue ?? null, multi: true };
127
+ }
128
+
129
+ return { normalizedValue: value, multi: false };
130
+ }
131
+
132
+ function createBlueprintPayload(collectionName, properties) {
133
+ const singularName = ensureSingular(collectionName);
134
+ return {
135
+ identifier: toIdentifier(singularName),
136
+ name: formatTitle(singularName),
137
+ description: `Auto-generated blueprint for MongoDB collection "${collectionName}"`,
138
+ entityIdentifierMechanism: 'objectid',
139
+ permissions: createDefaultPermissions(),
140
+ permissionScope: 'workspace',
141
+ properties,
142
+ relations: [],
143
+ dispatchers: {
144
+ create: false,
145
+ update: false,
146
+ delete: false
147
+ },
148
+ limitations: []
149
+ };
150
+ }
151
+
152
+ function createDefaultPermissions() {
153
+ const operations = ['create', 'read', 'update', 'delete'];
154
+ return operations.map((operation) => ({
155
+ scope: 'workspace',
156
+ operation,
157
+ guest: false,
158
+ roleBased: ['*'],
159
+ workspaceRoleBased: ['*'],
160
+ workspaceLabelsBased: ['*']
161
+ }));
162
+ }
163
+
164
+ async function sampleCollectionDocuments(collection) {
165
+ try {
166
+ return await collection.aggregate([{ $sample: { size: SAMPLE_SIZE } }]).toArray();
167
+ } catch (error) {
168
+ logger.debug(`Falling back to sequential sampling for ${collection.collectionName}: ${error.message}`);
169
+ return collection.find({}).limit(SAMPLE_SIZE).toArray();
170
+ }
171
+ }
172
+
173
+ function buildObjectSchema(sample, depth = 0) {
174
+ const MAX_SCHEMA_DEPTH = 3;
175
+ if (!sample || typeof sample !== 'object' || depth >= MAX_SCHEMA_DEPTH) {
176
+ return { type: 'object' };
177
+ }
178
+
179
+ const properties = {};
180
+
181
+ for (const [key, value] of Object.entries(sample)) {
182
+ if (key === '_id' || value === undefined) continue;
183
+ properties[key] = buildSchemaFromValue(value, depth + 1);
184
+ }
185
+
186
+ if (Object.keys(properties).length === 0) {
187
+ return { type: 'object' };
188
+ }
189
+
190
+ return {
191
+ type: 'object',
192
+ properties
193
+ };
194
+ }
195
+
196
+ function buildSchemaFromValue(value, depth) {
197
+ if (Array.isArray(value)) {
198
+ const arraySample = value.find((item) => item !== null && item !== undefined);
199
+ const itemsSchema = arraySample ? buildSchemaFromValue(arraySample, depth + 1) : { type: 'string' };
200
+ return {
201
+ type: 'array',
202
+ items: itemsSchema
203
+ };
204
+ }
205
+
206
+ const valueType = detectBlueprintType(value);
207
+
208
+ if (valueType === 'object' && value && typeof value === 'object') {
209
+ return buildObjectSchema(value, depth + 1);
210
+ }
211
+
212
+ return {
213
+ type: mapBlueprintTypeToJsonSchema(valueType)
214
+ };
215
+ }
216
+
217
+ function shouldSkipField(key) {
218
+ if (!key) return true;
219
+ const normalized = key.toLowerCase();
220
+ if (
221
+ normalized === '_id' ||
222
+ normalized === 'id' ||
223
+ normalized === 'user' ||
224
+ normalized === 'userid' ||
225
+ normalized === 'workspace' ||
226
+ normalized === 'workspaceid'
227
+ ) {
228
+ return true;
229
+ }
230
+ return key.startsWith('__');
231
+ }
232
+
233
+ function ensureDirectory(targetDir) {
234
+ if (!fs.existsSync(targetDir)) {
235
+ fs.mkdirSync(targetDir, { recursive: true });
236
+ logger.info(`Created output directory at ${targetDir}`);
237
+ }
238
+ }
239
+
240
+ function getDatabaseName(connectionUri) {
241
+ try {
242
+ const parsed = new URL(connectionUri);
243
+ return parsed.pathname.replace(/^\//, '') || undefined;
244
+ } catch {
245
+ return undefined;
246
+ }
247
+ }
@@ -0,0 +1,226 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { formatTitle, detectBlueprintType } from './blueprint-shared.mjs';
4
+
5
+ export function generateSdkGuide({ blueprint, documents, targetDir }) {
6
+ const guidePath = path.join(targetDir, `${blueprint.identifier}.sdk.md`);
7
+ const interfaceName = `${formatTitle(blueprint.identifier).replace(/\s+/g, '')}Entity`;
8
+ const entityVarName = `${toCamelCase(blueprint.identifier)}Entities`;
9
+ const sampleDoc = documents.find((doc) => doc && typeof doc === 'object');
10
+ const interfaceDefinition = buildInterfaceDefinition(interfaceName, blueprint);
11
+ const exampleEntity = buildExampleEntity(blueprint, sampleDoc);
12
+ const exampleLiteral = stringifyObjectLiteral(exampleEntity);
13
+
14
+ const markdown = buildMarkdown({
15
+ blueprint,
16
+ interfaceName,
17
+ entityVarName,
18
+ interfaceDefinition,
19
+ exampleLiteral,
20
+ });
21
+
22
+ fs.writeFileSync(guidePath, markdown);
23
+ return guidePath;
24
+ }
25
+
26
+ function buildMarkdown({ blueprint, interfaceName, entityVarName, interfaceDefinition, exampleLiteral }) {
27
+ return [
28
+ `# ${blueprint.name} Blueprint SDK Guide`,
29
+ '',
30
+ '## Install the SDK',
31
+ '```bash',
32
+ 'npm install @qelos/sdk',
33
+ '```',
34
+ '',
35
+ '## Initialize the Administrator SDK',
36
+ '```ts',
37
+ "import QelosAdministratorSDK from '@qelos/sdk/administrator';",
38
+ '',
39
+ 'const sdk = new QelosAdministratorSDK({',
40
+ " appUrl: process.env.QELOS_URL || 'http://localhost:3000',",
41
+ ' fetch,',
42
+ '});',
43
+ '',
44
+ `const ${entityVarName} = sdk.blueprints.entitiesOf<${interfaceName}>('${blueprint.identifier}');`,
45
+ '```',
46
+ '',
47
+ '## TypeScript Interface',
48
+ '```ts',
49
+ interfaceDefinition,
50
+ '```',
51
+ '',
52
+ '## Example Entity Payload',
53
+ '```ts',
54
+ `const sample${interfaceName} = ${exampleLiteral};`,
55
+ '```',
56
+ '',
57
+ '## CRUD Examples',
58
+ '',
59
+ '### List Entities',
60
+ '```ts',
61
+ `const entities = await ${entityVarName}.getList({ $limit: 20, $sort: '-created' });`,
62
+ '```',
63
+ '',
64
+ '### Fetch a Single Entity',
65
+ '```ts',
66
+ `const entity = await ${entityVarName}.getEntity('replace-with-entity-id');`,
67
+ '```',
68
+ '',
69
+ '### Create an Entity',
70
+ '```ts',
71
+ `const created = await ${entityVarName}.create(${exampleLiteral});`,
72
+ '```',
73
+ '',
74
+ '### Update an Entity',
75
+ '```ts',
76
+ `const updated = await ${entityVarName}.update('replace-with-entity-id', {\n ...${exampleLiteral.replace(/\n/g, '\n ')},\n});`,
77
+ '```',
78
+ '',
79
+ '### Delete an Entity',
80
+ '```ts',
81
+ `await ${entityVarName}.remove('replace-with-entity-id');`,
82
+ '```',
83
+ ].join('\n');
84
+ }
85
+
86
+ function buildInterfaceDefinition(interfaceName, blueprint) {
87
+ const lines = [`export interface ${interfaceName} {`];
88
+ for (const [key, descriptor] of Object.entries(blueprint.properties)) {
89
+ const tsType = mapBlueprintPropertyToTs(descriptor);
90
+ const optionalFlag = descriptor.required ? '' : '?';
91
+ const description = descriptor.title || key;
92
+ lines.push(` ${key}${optionalFlag}: ${tsType}; // ${description}`);
93
+ }
94
+ lines.push('}');
95
+ return lines.join('\n');
96
+ }
97
+
98
+ function mapBlueprintPropertyToTs(descriptor) {
99
+ const baseType = descriptor.type;
100
+ let tsType;
101
+ switch (baseType) {
102
+ case 'number':
103
+ tsType = 'number';
104
+ break;
105
+ case 'boolean':
106
+ tsType = 'boolean';
107
+ break;
108
+ case 'date':
109
+ case 'datetime':
110
+ case 'time':
111
+ case 'file':
112
+ case 'string':
113
+ tsType = 'string';
114
+ break;
115
+ case 'object':
116
+ tsType = 'Record<string, any>';
117
+ break;
118
+ default:
119
+ tsType = 'any';
120
+ }
121
+
122
+ if (descriptor.multi) {
123
+ return `${tsType}[]`;
124
+ }
125
+
126
+ return tsType;
127
+ }
128
+
129
+ function buildExampleEntity(blueprint, sampleDoc = {}) {
130
+ const entity = {};
131
+ for (const [key, descriptor] of Object.entries(blueprint.properties)) {
132
+ if (sampleDoc && sampleDoc[key] !== undefined) {
133
+ entity[key] = sanitizeExampleValue(sampleDoc[key], descriptor);
134
+ } else {
135
+ entity[key] = getDefaultValueForDescriptor(descriptor, key);
136
+ }
137
+ }
138
+ return entity;
139
+ }
140
+
141
+ function sanitizeExampleValue(value, descriptor) {
142
+ if (Array.isArray(value)) {
143
+ const normalizedItems = value
144
+ .filter((item) => item !== null && item !== undefined)
145
+ .map((item) => sanitizeExampleValue(item, { ...descriptor, multi: false }));
146
+ return descriptor.multi ? normalizedItems : normalizedItems[0];
147
+ }
148
+
149
+ if (value && typeof value === 'object') {
150
+ if (value._bsontype) {
151
+ return getDefaultValueForDescriptor(descriptor);
152
+ }
153
+ const result = {};
154
+ for (const [innerKey, innerValue] of Object.entries(value)) {
155
+ result[innerKey] = sanitizeExampleValue(innerValue, { type: detectBlueprintType(innerValue) });
156
+ }
157
+ return result;
158
+ }
159
+
160
+ return value;
161
+ }
162
+
163
+ function getDefaultValueForDescriptor(descriptor, key = '') {
164
+ const baseValue = (() => {
165
+ switch (descriptor.type) {
166
+ case 'number':
167
+ return 0;
168
+ case 'boolean':
169
+ return false;
170
+ case 'datetime':
171
+ case 'date':
172
+ case 'time':
173
+ return new Date().toISOString();
174
+ case 'object':
175
+ return {};
176
+ case 'file':
177
+ return 'https://example.com/file';
178
+ default:
179
+ return `Sample ${formatTitle(key || 'value')}`;
180
+ }
181
+ })();
182
+
183
+ if (descriptor.multi) {
184
+ return [baseValue];
185
+ }
186
+
187
+ return baseValue;
188
+ }
189
+
190
+ function stringifyObjectLiteral(value, level = 0) {
191
+ const indent = ' '.repeat(level + 2);
192
+ const baseIndent = ' '.repeat(level);
193
+
194
+ if (Array.isArray(value)) {
195
+ if (value.length === 0) {
196
+ return '[]';
197
+ }
198
+ const items = value
199
+ .map((item) => `${indent}${stringifyObjectLiteral(item, level + 2)}`)
200
+ .join(',\n');
201
+ return `[\n${items}\n${baseIndent}]`;
202
+ }
203
+
204
+ if (value && typeof value === 'object') {
205
+ const entries = Object.entries(value);
206
+ if (entries.length === 0) {
207
+ return '{}';
208
+ }
209
+ const lines = entries.map(
210
+ ([key, val]) => `${indent}${key}: ${stringifyObjectLiteral(val, level + 2)},`
211
+ );
212
+ return `\n${indent ? baseIndent : ''}{\n${lines.join('\n')}\n${baseIndent}}`;
213
+ }
214
+
215
+ if (typeof value === 'string') {
216
+ return JSON.stringify(value);
217
+ }
218
+
219
+ return String(value);
220
+ }
221
+
222
+ function toCamelCase(value) {
223
+ return (value || '')
224
+ .replace(/[-_\s]+(.)?/g, (_, chr) => (chr ? chr.toUpperCase() : ''))
225
+ .replace(/^(.)/, (match) => match.toLowerCase());
226
+ }
@@ -0,0 +1,77 @@
1
+ export function formatTitle(value) {
2
+ return (value || '')
3
+ .replace(/[_-]+/g, ' ')
4
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
5
+ .replace(/\s+/g, ' ')
6
+ .trim()
7
+ .replace(/\b\w/g, (match) => match.toUpperCase());
8
+ }
9
+
10
+ export function toIdentifier(collectionName) {
11
+ const sanitized = (collectionName || '')
12
+ .trim()
13
+ .toLowerCase()
14
+ .replace(/[^a-z0-9]+/g, '_')
15
+ .replace(/^_+|_+$/g, '');
16
+
17
+ return sanitized || `collection_${Date.now()}`;
18
+ }
19
+
20
+ export function ensureSingular(value = '') {
21
+ const normalized = value.trim();
22
+ const lower = normalized.toLowerCase();
23
+
24
+ if (lower.endsWith('ies')) {
25
+ return normalized.slice(0, -3) + normalized.slice(-3).replace(/ies$/i, 'y');
26
+ }
27
+
28
+ if (/(sses|xes|zes|ches|shes)$/i.test(lower)) {
29
+ return normalized.slice(0, -2);
30
+ }
31
+
32
+ if (lower.endsWith('s') && !lower.endsWith('ss')) {
33
+ return normalized.slice(0, -1);
34
+ }
35
+
36
+ return normalized;
37
+ }
38
+
39
+ export function detectBlueprintType(value) {
40
+ if (value === null || value === undefined) {
41
+ return 'string';
42
+ }
43
+
44
+ if (value instanceof Date) {
45
+ return 'datetime';
46
+ }
47
+
48
+ if (typeof value === 'number') {
49
+ return 'number';
50
+ }
51
+
52
+ if (typeof value === 'boolean') {
53
+ return 'boolean';
54
+ }
55
+
56
+ if (typeof value === 'object') {
57
+ if (value?._bsontype === 'ObjectId') {
58
+ return 'string';
59
+ }
60
+ return 'object';
61
+ }
62
+
63
+ return 'string';
64
+ }
65
+
66
+ export function mapBlueprintTypeToJsonSchema(type) {
67
+ switch (type) {
68
+ case 'number':
69
+ return 'number';
70
+ case 'boolean':
71
+ return 'boolean';
72
+ case 'object':
73
+ return 'object';
74
+ default:
75
+ return 'string';
76
+ }
77
+ }
@@ -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 files = fs.readdirSync(path);
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
- logger.warning(`No blueprint files (*.blueprint.json) found in ${path}`);
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
 
@@ -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 files = fs.readdirSync(path);
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
- logger.warning(`No .vue files found in ${path}`);
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(files.map(async (file) => {
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 files = fs.readdirSync(path);
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
- logger.warning(`No configuration files (*.config.json) found in ${path}`);
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
 
@@ -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 files = fs.readdirSync(path);
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
- logger.warning(`No plugin files (*.plugin.json) found in ${path}`);
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