@objectstack/cli 2.0.6 → 3.0.0
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/.turbo/turbo-build.log +10 -6
- package/CHANGELOG.md +30 -0
- package/README.md +88 -3
- package/dist/bin.js +691 -425
- package/dist/chunk-CSHQEILI.js +246 -0
- package/dist/chunk-Q74JNWKD.js +248 -0
- package/dist/config-A7BN6UIT.js +11 -0
- package/dist/config-UN34WBHT.js +10 -0
- package/dist/index.d.ts +14 -1
- package/dist/index.js +641 -382
- package/package.json +9 -9
- package/src/bin.ts +18 -1
- package/src/commands/generate.ts +181 -3
- package/src/commands/plugin.ts +372 -0
- package/src/index.ts +2 -0
- package/src/utils/plugin-commands.ts +163 -0
- package/test/commands.test.ts +6 -0
- package/test/plugin-commands.test.ts +162 -0
- package/test/plugin.test.ts +173 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Command Line Interface for ObjectStack Protocol",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -22,16 +22,16 @@
|
|
|
22
22
|
"commander": "^14.0.3",
|
|
23
23
|
"tsx": "^4.7.1",
|
|
24
24
|
"zod": "^4.3.6",
|
|
25
|
-
"@objectstack/core": "
|
|
26
|
-
"@objectstack/driver-memory": "^
|
|
27
|
-
"@objectstack/objectql": "^
|
|
28
|
-
"@objectstack/plugin-hono-server": "
|
|
29
|
-
"@objectstack/rest": "
|
|
30
|
-
"@objectstack/runtime": "^
|
|
31
|
-
"@objectstack/spec": "
|
|
25
|
+
"@objectstack/core": "3.0.0",
|
|
26
|
+
"@objectstack/driver-memory": "^3.0.0",
|
|
27
|
+
"@objectstack/objectql": "^3.0.0",
|
|
28
|
+
"@objectstack/plugin-hono-server": "3.0.0",
|
|
29
|
+
"@objectstack/rest": "3.0.0",
|
|
30
|
+
"@objectstack/runtime": "^3.0.0",
|
|
31
|
+
"@objectstack/spec": "3.0.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@objectstack/core": "
|
|
34
|
+
"@objectstack/core": "3.0.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^25.2.2",
|
package/src/bin.ts
CHANGED
|
@@ -16,6 +16,8 @@ import { validateCommand } from './commands/validate.js';
|
|
|
16
16
|
import { initCommand } from './commands/init.js';
|
|
17
17
|
import { infoCommand } from './commands/info.js';
|
|
18
18
|
import { generateCommand } from './commands/generate.js';
|
|
19
|
+
import { pluginCommand } from './commands/plugin.js';
|
|
20
|
+
import { loadPluginCommands } from './utils/plugin-commands.js';
|
|
19
21
|
|
|
20
22
|
const require = createRequire(import.meta.url);
|
|
21
23
|
const pkg = require('../package.json');
|
|
@@ -46,6 +48,7 @@ ${chalk.bold.cyan('◆ ObjectStack CLI')} ${chalk.dim(`v${pkg.version}`)}
|
|
|
46
48
|
${chalk.bold('Workflow:')}
|
|
47
49
|
${chalk.dim('$')} os init ${chalk.dim('# Create a new project')}
|
|
48
50
|
${chalk.dim('$')} os generate object task ${chalk.dim('# Add metadata')}
|
|
51
|
+
${chalk.dim('$')} os plugin add <package> ${chalk.dim('# Add a plugin')}
|
|
49
52
|
${chalk.dim('$')} os validate ${chalk.dim('# Check configuration')}
|
|
50
53
|
${chalk.dim('$')} os dev ${chalk.dim('# Start dev server')}
|
|
51
54
|
${chalk.dim('$')} os studio ${chalk.dim('# Dev server + Studio UI')}
|
|
@@ -70,8 +73,22 @@ program.addCommand(infoCommand);
|
|
|
70
73
|
program.addCommand(generateCommand);
|
|
71
74
|
program.addCommand(createCommand);
|
|
72
75
|
|
|
76
|
+
// ── Plugin Management ──
|
|
77
|
+
program.addCommand(pluginCommand);
|
|
78
|
+
|
|
73
79
|
// ── Quality ──
|
|
74
80
|
program.addCommand(testCommand);
|
|
75
81
|
program.addCommand(doctorCommand);
|
|
76
82
|
|
|
77
|
-
|
|
83
|
+
// ── Plugin-Contributed Commands ──
|
|
84
|
+
// Load commands from installed plugins that declare `contributes.commands` in their manifest.
|
|
85
|
+
// This must complete before `program.parse()` so that plugin commands are available.
|
|
86
|
+
loadPluginCommands(program).then(() => {
|
|
87
|
+
program.parse(process.argv);
|
|
88
|
+
}).catch((err) => {
|
|
89
|
+
// If plugin command loading fails, still parse with built-in commands
|
|
90
|
+
if (process.env.DEBUG) {
|
|
91
|
+
console.error(chalk.yellow(`\n ⚠ Plugin command loading failed: ${err?.message || err}`));
|
|
92
|
+
}
|
|
93
|
+
program.parse(process.argv);
|
|
94
|
+
});
|
package/src/commands/generate.ts
CHANGED
|
@@ -204,11 +204,110 @@ function toSnakeCase(str: string): string {
|
|
|
204
204
|
return str.replace(/[-]/g, '_').replace(/[A-Z]/g, c => `_${c.toLowerCase()}`).replace(/^_/, '');
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
// ─── Field Type Mapping ─────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
const FIELD_TYPE_MAP: Record<string, string> = {
|
|
210
|
+
text: 'string',
|
|
211
|
+
textarea: 'string',
|
|
212
|
+
richtext: 'string',
|
|
213
|
+
html: 'string',
|
|
214
|
+
markdown: 'string',
|
|
215
|
+
number: 'number',
|
|
216
|
+
integer: 'number',
|
|
217
|
+
currency: 'number',
|
|
218
|
+
percent: 'number',
|
|
219
|
+
boolean: 'boolean',
|
|
220
|
+
date: 'string',
|
|
221
|
+
datetime: 'string',
|
|
222
|
+
time: 'string',
|
|
223
|
+
email: 'string',
|
|
224
|
+
phone: 'string',
|
|
225
|
+
url: 'string',
|
|
226
|
+
select: 'string',
|
|
227
|
+
multiselect: 'string[]',
|
|
228
|
+
lookup: 'string',
|
|
229
|
+
master_detail: 'string',
|
|
230
|
+
formula: 'unknown',
|
|
231
|
+
autonumber: 'string',
|
|
232
|
+
json: 'Record<string, unknown>',
|
|
233
|
+
file: 'string',
|
|
234
|
+
image: 'string',
|
|
235
|
+
password: 'string',
|
|
236
|
+
slug: 'string',
|
|
237
|
+
uuid: 'string',
|
|
238
|
+
ip_address: 'string',
|
|
239
|
+
color: 'string',
|
|
240
|
+
rating: 'number',
|
|
241
|
+
geo_point: '{ lat: number; lng: number }',
|
|
242
|
+
vector: 'number[]',
|
|
243
|
+
encrypted: 'string',
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
function fieldTypeToTs(fieldType: string, multiple?: boolean): string {
|
|
247
|
+
const base = FIELD_TYPE_MAP[fieldType] || 'unknown';
|
|
248
|
+
return multiple ? `${base}[]` : base;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function generateTypesFromConfig(config: Record<string, unknown>): string {
|
|
252
|
+
const lines: string[] = [
|
|
253
|
+
'// Auto-generated by ObjectStack CLI — do not edit manually',
|
|
254
|
+
`// Generated at ${new Date().toISOString()}`,
|
|
255
|
+
'',
|
|
256
|
+
"import type { Data } from '@objectstack/spec';",
|
|
257
|
+
'',
|
|
258
|
+
];
|
|
259
|
+
|
|
260
|
+
// Extract objects from config (supports both top-level and nested)
|
|
261
|
+
const objects: Record<string, unknown>[] = [];
|
|
262
|
+
const rawObjects = (config as any).objects ?? (config as any).data?.objects ?? {};
|
|
263
|
+
|
|
264
|
+
if (Array.isArray(rawObjects)) {
|
|
265
|
+
objects.push(...rawObjects);
|
|
266
|
+
} else if (typeof rawObjects === 'object') {
|
|
267
|
+
for (const val of Object.values(rawObjects)) {
|
|
268
|
+
if (val && typeof val === 'object') objects.push(val as Record<string, unknown>);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (objects.length === 0) {
|
|
273
|
+
lines.push('// No objects found in configuration');
|
|
274
|
+
return lines.join('\n') + '\n';
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
for (const obj of objects) {
|
|
278
|
+
const name = String(obj.name || 'unknown');
|
|
279
|
+
const typeName = name
|
|
280
|
+
.split('_')
|
|
281
|
+
.map((w: string) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
282
|
+
.join('');
|
|
283
|
+
const fields = (obj.fields ?? {}) as Record<string, Record<string, unknown>>;
|
|
284
|
+
|
|
285
|
+
lines.push(`/** ${String(obj.label || typeName)} record type */`);
|
|
286
|
+
lines.push(`export interface ${typeName}Record {`);
|
|
287
|
+
lines.push(' id: string;');
|
|
288
|
+
|
|
289
|
+
for (const [fieldName, fieldDef] of Object.entries(fields)) {
|
|
290
|
+
const fType = String(fieldDef.type || 'text');
|
|
291
|
+
const tsType = fieldTypeToTs(fType, !!fieldDef.multiple);
|
|
292
|
+
const required = fieldDef.required ? '' : '?';
|
|
293
|
+
if (fieldDef.label) {
|
|
294
|
+
lines.push(` /** ${fieldDef.label} */`);
|
|
295
|
+
}
|
|
296
|
+
lines.push(` ${fieldName}${required}: ${tsType};`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
lines.push('}');
|
|
300
|
+
lines.push('');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return lines.join('\n') + '\n';
|
|
304
|
+
}
|
|
305
|
+
|
|
207
306
|
// ─── Command ────────────────────────────────────────────────────────
|
|
208
307
|
|
|
209
|
-
|
|
210
|
-
.alias('
|
|
211
|
-
.description('Generate metadata
|
|
308
|
+
const generateMetadataCommand = new Command('metadata')
|
|
309
|
+
.alias('m')
|
|
310
|
+
.description('Generate metadata scaffold (object, view, action, flow, agent, dashboard, app)')
|
|
212
311
|
.argument('<type>', 'Metadata type to generate')
|
|
213
312
|
.argument('<name>', 'Name for the metadata (use kebab-case)')
|
|
214
313
|
.option('-d, --dir <directory>', 'Target directory (overrides default)')
|
|
@@ -297,3 +396,82 @@ export const generateCommand = new Command('generate')
|
|
|
297
396
|
process.exit(1);
|
|
298
397
|
}
|
|
299
398
|
});
|
|
399
|
+
|
|
400
|
+
const generateTypesCommand = new Command('types')
|
|
401
|
+
.description('Generate TypeScript type definitions from ObjectStack configuration')
|
|
402
|
+
.argument('[config]', 'Configuration file path')
|
|
403
|
+
.option('-o, --output <file>', 'Output file path', 'src/types/objectstack.d.ts')
|
|
404
|
+
.option('--dry-run', 'Show what would be generated without writing files')
|
|
405
|
+
.action(async (configPath, options) => {
|
|
406
|
+
printHeader('Generate Types');
|
|
407
|
+
|
|
408
|
+
try {
|
|
409
|
+
const { loadConfig } = await import('../utils/config.js');
|
|
410
|
+
printInfo('Loading configuration...');
|
|
411
|
+
const { config, absolutePath } = await loadConfig(configPath);
|
|
412
|
+
|
|
413
|
+
console.log(` ${chalk.dim('Config:')} ${chalk.white(absolutePath)}`);
|
|
414
|
+
console.log(` ${chalk.dim('Output:')} ${chalk.white(options.output)}`);
|
|
415
|
+
console.log('');
|
|
416
|
+
|
|
417
|
+
const content = generateTypesFromConfig(config as Record<string, unknown>);
|
|
418
|
+
|
|
419
|
+
if (options.dryRun) {
|
|
420
|
+
printInfo('Dry run — no files written');
|
|
421
|
+
console.log('');
|
|
422
|
+
for (const line of content.split('\n')) {
|
|
423
|
+
console.log(chalk.dim(` ${line}`));
|
|
424
|
+
}
|
|
425
|
+
console.log('');
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const outPath = path.resolve(process.cwd(), options.output);
|
|
430
|
+
const outDir = path.dirname(outPath);
|
|
431
|
+
if (!fs.existsSync(outDir)) {
|
|
432
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
433
|
+
}
|
|
434
|
+
fs.writeFileSync(outPath, content);
|
|
435
|
+
printSuccess(`Generated types at ${options.output}`);
|
|
436
|
+
console.log('');
|
|
437
|
+
|
|
438
|
+
} catch (error: any) {
|
|
439
|
+
printError(error.message || String(error));
|
|
440
|
+
process.exit(1);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
export const generateCommand = new Command('generate')
|
|
445
|
+
.alias('g')
|
|
446
|
+
.description('Generate metadata files or TypeScript types')
|
|
447
|
+
.argument('[type]', 'Metadata type to generate (object, view, action, flow, agent, dashboard, app)')
|
|
448
|
+
.argument('[name]', 'Name for the metadata (use kebab-case)')
|
|
449
|
+
.option('-d, --dir <directory>', 'Target directory (overrides default)')
|
|
450
|
+
.option('--dry-run', 'Show what would be created without writing files')
|
|
451
|
+
.addCommand(generateTypesCommand)
|
|
452
|
+
.action(async (type: string | undefined, name: string | undefined, options) => {
|
|
453
|
+
if (!type) {
|
|
454
|
+
printHeader('Generate');
|
|
455
|
+
console.log(chalk.bold(' Sub-commands:'));
|
|
456
|
+
console.log(` ${chalk.cyan('types'.padEnd(12))} Generate TypeScript type definitions from config`);
|
|
457
|
+
console.log('');
|
|
458
|
+
console.log(chalk.bold(' Metadata types:'));
|
|
459
|
+
for (const [key, gen] of Object.entries(GENERATORS)) {
|
|
460
|
+
console.log(` ${chalk.cyan(key.padEnd(12))} ${chalk.dim(gen.description)}`);
|
|
461
|
+
}
|
|
462
|
+
console.log('');
|
|
463
|
+
console.log(chalk.dim(' Usage: objectstack generate <type> <name>'));
|
|
464
|
+
console.log(chalk.dim(' Usage: objectstack generate types [config]'));
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Delegate to metadata command action
|
|
469
|
+
if (!name) {
|
|
470
|
+
printError('Missing required argument: <name>');
|
|
471
|
+
console.log(chalk.dim(' Usage: objectstack generate <type> <name>'));
|
|
472
|
+
process.exit(1);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Execute metadata generation inline
|
|
476
|
+
await generateMetadataCommand.parseAsync([type, name, ...process.argv.slice(4)], { from: 'user' });
|
|
477
|
+
});
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { loadConfig, resolveConfigPath } from '../utils/config.js';
|
|
8
|
+
import { printHeader, printSuccess, printError, printInfo, printWarning, printKV } from '../utils/format.js';
|
|
9
|
+
|
|
10
|
+
// ─── Helpers ────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Resolve plugin display name from a plugin entry.
|
|
14
|
+
* Plugins can be string package names, objects with `.name`, or class instances.
|
|
15
|
+
*/
|
|
16
|
+
function resolvePluginName(plugin: unknown): string {
|
|
17
|
+
if (typeof plugin === 'string') return plugin;
|
|
18
|
+
if (plugin && typeof plugin === 'object') {
|
|
19
|
+
const p = plugin as Record<string, unknown>;
|
|
20
|
+
if (typeof p.name === 'string') return p.name;
|
|
21
|
+
if (p.constructor && p.constructor.name !== 'Object') return p.constructor.name;
|
|
22
|
+
}
|
|
23
|
+
return 'unknown';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve plugin version from a plugin entry.
|
|
28
|
+
*/
|
|
29
|
+
function resolvePluginVersion(plugin: unknown): string {
|
|
30
|
+
if (plugin && typeof plugin === 'object') {
|
|
31
|
+
const p = plugin as Record<string, unknown>;
|
|
32
|
+
if (typeof p.version === 'string') return p.version;
|
|
33
|
+
}
|
|
34
|
+
return '-';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve plugin type from a plugin entry.
|
|
39
|
+
*/
|
|
40
|
+
function resolvePluginType(plugin: unknown): string {
|
|
41
|
+
if (plugin && typeof plugin === 'object') {
|
|
42
|
+
const p = plugin as Record<string, unknown>;
|
|
43
|
+
if (typeof p.type === 'string') return p.type;
|
|
44
|
+
}
|
|
45
|
+
return 'standard';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Read the raw text of the config file.
|
|
50
|
+
*/
|
|
51
|
+
function readConfigText(configPath: string): string {
|
|
52
|
+
return fs.readFileSync(configPath, 'utf-8');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Add a plugin import and entry to objectstack.config.ts.
|
|
57
|
+
*
|
|
58
|
+
* This performs a simple text-based transformation:
|
|
59
|
+
* 1. Adds an import statement for the package at the top of the file.
|
|
60
|
+
* 2. Inserts the imported identifier into the `plugins` array, creating one if absent.
|
|
61
|
+
*/
|
|
62
|
+
function addPluginToConfig(configPath: string, packageName: string): void {
|
|
63
|
+
let content = readConfigText(configPath);
|
|
64
|
+
|
|
65
|
+
// Derive a variable name from the package name
|
|
66
|
+
// e.g. "@objectstack/plugin-auth" → "authPlugin"
|
|
67
|
+
const shortName = packageName
|
|
68
|
+
.replace(/^@[^/]+\//, '') // strip scope
|
|
69
|
+
.replace(/^plugin-/, '') // strip "plugin-" prefix
|
|
70
|
+
.replace(/-+/g, '-') // collapse consecutive hyphens
|
|
71
|
+
.replace(/^-|-$/g, ''); // trim leading/trailing hyphens
|
|
72
|
+
const varName = shortName.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase()) + 'Plugin';
|
|
73
|
+
|
|
74
|
+
// 1. Add import
|
|
75
|
+
const importLine = `import ${varName} from '${packageName}';\n`;
|
|
76
|
+
|
|
77
|
+
if (content.includes(packageName)) {
|
|
78
|
+
throw new Error(`Plugin '${packageName}' is already referenced in the config`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Insert import after the last existing import
|
|
82
|
+
const importRegex = /^import .+$/gm;
|
|
83
|
+
let lastImportEnd = 0;
|
|
84
|
+
let match: RegExpExecArray | null;
|
|
85
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
86
|
+
lastImportEnd = match.index + match[0].length;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (lastImportEnd > 0) {
|
|
90
|
+
content = content.slice(0, lastImportEnd) + '\n' + importLine + content.slice(lastImportEnd);
|
|
91
|
+
} else {
|
|
92
|
+
content = importLine + '\n' + content;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 2. Add to plugins array (target only the first plugins: [ within defineStack)
|
|
96
|
+
if (/plugins\s*:\s*\[/.test(content)) {
|
|
97
|
+
// plugins array exists — append to it (first occurrence only)
|
|
98
|
+
let replaced = false;
|
|
99
|
+
content = content.replace(
|
|
100
|
+
/(plugins\s*:\s*\[)/,
|
|
101
|
+
(match) => {
|
|
102
|
+
if (replaced) return match;
|
|
103
|
+
replaced = true;
|
|
104
|
+
return `${match}\n ${varName},`;
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
} else {
|
|
108
|
+
// No plugins array — add one before the closing of defineStack({...})
|
|
109
|
+
// Look for the last property before the closing `})` or `})`
|
|
110
|
+
content = content.replace(
|
|
111
|
+
/(defineStack\(\{[\s\S]*?)(}\s*\))/,
|
|
112
|
+
`$1 plugins: [\n ${varName},\n ],\n$2`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
fs.writeFileSync(configPath, content);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Remove a plugin reference from objectstack.config.ts.
|
|
121
|
+
*
|
|
122
|
+
* Removes matching import line and the entry from the plugins array.
|
|
123
|
+
*/
|
|
124
|
+
function removePluginFromConfig(configPath: string, pluginName: string): void {
|
|
125
|
+
let content = readConfigText(configPath);
|
|
126
|
+
|
|
127
|
+
// Remove the import line that references this plugin (exact package name match)
|
|
128
|
+
const importRegex = new RegExp(`^import .+['"]${escapeRegex(pluginName)}['"]\\s*;?\\s*$\\n?`, 'gm');
|
|
129
|
+
const hadImport = importRegex.test(content);
|
|
130
|
+
// Reset regex lastIndex after test()
|
|
131
|
+
importRegex.lastIndex = 0;
|
|
132
|
+
content = content.replace(importRegex, '');
|
|
133
|
+
|
|
134
|
+
// Also try to remove by a derived variable name
|
|
135
|
+
const shortName = pluginName
|
|
136
|
+
.replace(/^@[^/]+\//, '')
|
|
137
|
+
.replace(/^plugin-/, '')
|
|
138
|
+
.replace(/-+/g, '-')
|
|
139
|
+
.replace(/^-|-$/g, '');
|
|
140
|
+
const varName = shortName.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase()) + 'Plugin';
|
|
141
|
+
|
|
142
|
+
// Remove import by variable name if it wasn't caught above
|
|
143
|
+
if (!hadImport) {
|
|
144
|
+
const varImportRegex = new RegExp(`^import .* ${escapeRegex(varName)} .+$\\n?`, 'gm');
|
|
145
|
+
content = content.replace(varImportRegex, '');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Remove the entry from the plugins array
|
|
149
|
+
// Match: varName, or 'package-name', or "package-name"
|
|
150
|
+
const entryPatterns = [
|
|
151
|
+
new RegExp(`\\s*${escapeRegex(varName)},?\\n?`, 'g'),
|
|
152
|
+
new RegExp(`\\s*['"]${escapeRegex(pluginName)}['"],?\\n?`, 'g'),
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
for (const pattern of entryPatterns) {
|
|
156
|
+
content = content.replace(pattern, '\n');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Clean up empty plugins array: plugins: [\n ],
|
|
160
|
+
content = content.replace(/plugins\s*:\s*\[\s*\],?\n?/g, '');
|
|
161
|
+
|
|
162
|
+
fs.writeFileSync(configPath, content);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function escapeRegex(str: string): string {
|
|
166
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ─── Subcommands ────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
const listCommand = new Command('list')
|
|
172
|
+
.alias('ls')
|
|
173
|
+
.description('List plugins defined in the configuration')
|
|
174
|
+
.argument('[config]', 'Configuration file path')
|
|
175
|
+
.option('--json', 'Output as JSON')
|
|
176
|
+
.action(async (configSource?: string, options?: { json?: boolean }) => {
|
|
177
|
+
try {
|
|
178
|
+
const { config } = await loadConfig(configSource);
|
|
179
|
+
const plugins: unknown[] = config.plugins || [];
|
|
180
|
+
const devPlugins: unknown[] = config.devPlugins || [];
|
|
181
|
+
|
|
182
|
+
if (options?.json) {
|
|
183
|
+
const data = {
|
|
184
|
+
plugins: plugins.map(p => ({
|
|
185
|
+
name: resolvePluginName(p),
|
|
186
|
+
version: resolvePluginVersion(p),
|
|
187
|
+
type: resolvePluginType(p),
|
|
188
|
+
dev: false,
|
|
189
|
+
})),
|
|
190
|
+
devPlugins: devPlugins.map(p => ({
|
|
191
|
+
name: resolvePluginName(p),
|
|
192
|
+
version: resolvePluginVersion(p),
|
|
193
|
+
type: resolvePluginType(p),
|
|
194
|
+
dev: true,
|
|
195
|
+
})),
|
|
196
|
+
};
|
|
197
|
+
console.log(JSON.stringify(data, null, 2));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
printHeader('Plugins');
|
|
202
|
+
|
|
203
|
+
if (plugins.length === 0 && devPlugins.length === 0) {
|
|
204
|
+
printInfo('No plugins configured');
|
|
205
|
+
console.log('');
|
|
206
|
+
console.log(chalk.dim(' Hint: Add plugins to your objectstack.config.ts'));
|
|
207
|
+
console.log(chalk.dim(' Or run: os plugin add <package-name>'));
|
|
208
|
+
console.log('');
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (plugins.length > 0) {
|
|
213
|
+
console.log(chalk.bold(`\n Plugins (${plugins.length}):`));
|
|
214
|
+
for (const plugin of plugins) {
|
|
215
|
+
const name = resolvePluginName(plugin);
|
|
216
|
+
const version = resolvePluginVersion(plugin);
|
|
217
|
+
const type = resolvePluginType(plugin);
|
|
218
|
+
console.log(
|
|
219
|
+
` ${chalk.cyan('●')} ${chalk.white(name)}` +
|
|
220
|
+
(version !== '-' ? chalk.dim(` v${version}`) : '') +
|
|
221
|
+
(type !== 'standard' ? chalk.dim(` [${type}]`) : '')
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (devPlugins.length > 0) {
|
|
227
|
+
console.log(chalk.bold(`\n Dev Plugins (${devPlugins.length}):`));
|
|
228
|
+
for (const plugin of devPlugins) {
|
|
229
|
+
const name = resolvePluginName(plugin);
|
|
230
|
+
const version = resolvePluginVersion(plugin);
|
|
231
|
+
console.log(
|
|
232
|
+
` ${chalk.yellow('●')} ${chalk.white(name)}` +
|
|
233
|
+
(version !== '-' ? chalk.dim(` v${version}`) : '') +
|
|
234
|
+
chalk.dim(' [dev]')
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
console.log('');
|
|
240
|
+
} catch (error: any) {
|
|
241
|
+
printError(error.message || String(error));
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const infoSubCommand = new Command('info')
|
|
247
|
+
.description('Show detailed information about a plugin')
|
|
248
|
+
.argument('<name>', 'Plugin name or package name')
|
|
249
|
+
.argument('[config]', 'Configuration file path')
|
|
250
|
+
.action(async (name: string, configSource?: string) => {
|
|
251
|
+
try {
|
|
252
|
+
const { config } = await loadConfig(configSource);
|
|
253
|
+
const allPlugins: unknown[] = [
|
|
254
|
+
...(config.plugins || []),
|
|
255
|
+
...(config.devPlugins || []),
|
|
256
|
+
];
|
|
257
|
+
|
|
258
|
+
const found = allPlugins.find((p) => {
|
|
259
|
+
const pName = resolvePluginName(p);
|
|
260
|
+
return pName === name || pName.includes(name);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
if (!found) {
|
|
264
|
+
printError(`Plugin '${name}' not found in configuration`);
|
|
265
|
+
console.log('');
|
|
266
|
+
console.log(chalk.dim(' Available plugins:'));
|
|
267
|
+
for (const p of allPlugins) {
|
|
268
|
+
console.log(chalk.dim(` - ${resolvePluginName(p)}`));
|
|
269
|
+
}
|
|
270
|
+
console.log('');
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
printHeader(`Plugin: ${resolvePluginName(found)}`);
|
|
275
|
+
|
|
276
|
+
printKV('Name', resolvePluginName(found));
|
|
277
|
+
printKV('Version', resolvePluginVersion(found));
|
|
278
|
+
printKV('Type', resolvePluginType(found));
|
|
279
|
+
|
|
280
|
+
const isDev = (config.devPlugins || []).includes(found);
|
|
281
|
+
printKV('Environment', isDev ? 'development' : 'production');
|
|
282
|
+
|
|
283
|
+
if (found && typeof found === 'object') {
|
|
284
|
+
const p = found as Record<string, unknown>;
|
|
285
|
+
|
|
286
|
+
if (typeof p.description === 'string') {
|
|
287
|
+
printKV('Description', p.description);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (Array.isArray(p.dependencies) && p.dependencies.length > 0) {
|
|
291
|
+
printKV('Dependencies', p.dependencies.join(', '));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Show services if it's a loaded plugin instance
|
|
295
|
+
if (typeof p.init === 'function') {
|
|
296
|
+
printInfo('This is a runtime plugin instance (has init function)');
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (typeof found === 'string') {
|
|
301
|
+
printInfo('This is a string reference (will be imported at runtime)');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
console.log('');
|
|
305
|
+
} catch (error: any) {
|
|
306
|
+
printError(error.message || String(error));
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const addCommand = new Command('add')
|
|
312
|
+
.description('Add a plugin to objectstack.config.ts')
|
|
313
|
+
.argument('<package>', 'Plugin package name (e.g. @objectstack/plugin-auth)')
|
|
314
|
+
.option('-d, --dev', 'Add as a dev-only plugin')
|
|
315
|
+
.option('-c, --config <path>', 'Configuration file path')
|
|
316
|
+
.action(async (packageName: string, options?: { dev?: boolean; config?: string }) => {
|
|
317
|
+
try {
|
|
318
|
+
const configPath = resolveConfigPath(options?.config);
|
|
319
|
+
|
|
320
|
+
printHeader('Add Plugin');
|
|
321
|
+
console.log(` ${chalk.dim('Package:')} ${chalk.white(packageName)}`);
|
|
322
|
+
console.log(` ${chalk.dim('Config:')} ${chalk.white(path.relative(process.cwd(), configPath))}`);
|
|
323
|
+
console.log('');
|
|
324
|
+
|
|
325
|
+
addPluginToConfig(configPath, packageName);
|
|
326
|
+
printSuccess(`Added ${chalk.cyan(packageName)} to config`);
|
|
327
|
+
|
|
328
|
+
console.log('');
|
|
329
|
+
console.log(chalk.dim(' Next steps:'));
|
|
330
|
+
console.log(chalk.dim(` 1. Install the package: pnpm add ${packageName}`));
|
|
331
|
+
console.log(chalk.dim(' 2. Run: os validate'));
|
|
332
|
+
console.log('');
|
|
333
|
+
} catch (error: any) {
|
|
334
|
+
printError(error.message || String(error));
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const removeCommand = new Command('remove')
|
|
340
|
+
.alias('rm')
|
|
341
|
+
.description('Remove a plugin from objectstack.config.ts')
|
|
342
|
+
.argument('<name>', 'Plugin name or package name to remove')
|
|
343
|
+
.option('-c, --config <path>', 'Configuration file path')
|
|
344
|
+
.action(async (pluginName: string, options?: { config?: string }) => {
|
|
345
|
+
try {
|
|
346
|
+
const configPath = resolveConfigPath(options?.config);
|
|
347
|
+
|
|
348
|
+
printHeader('Remove Plugin');
|
|
349
|
+
console.log(` ${chalk.dim('Plugin:')} ${chalk.white(pluginName)}`);
|
|
350
|
+
console.log(` ${chalk.dim('Config:')} ${chalk.white(path.relative(process.cwd(), configPath))}`);
|
|
351
|
+
console.log('');
|
|
352
|
+
|
|
353
|
+
removePluginFromConfig(configPath, pluginName);
|
|
354
|
+
printSuccess(`Removed ${chalk.cyan(pluginName)} from config`);
|
|
355
|
+
|
|
356
|
+
console.log('');
|
|
357
|
+
console.log(chalk.dim(' Tip: Run `pnpm remove ' + pluginName + '` to uninstall the package'));
|
|
358
|
+
console.log('');
|
|
359
|
+
} catch (error: any) {
|
|
360
|
+
printError(error.message || String(error));
|
|
361
|
+
process.exit(1);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// ─── Main Plugin Command ────────────────────────────────────────────
|
|
366
|
+
|
|
367
|
+
export const pluginCommand = new Command('plugin')
|
|
368
|
+
.description('Manage plugins (list, info, add, remove)')
|
|
369
|
+
.addCommand(listCommand)
|
|
370
|
+
.addCommand(infoSubCommand)
|
|
371
|
+
.addCommand(addCommand)
|
|
372
|
+
.addCommand(removeCommand);
|
package/src/index.ts
CHANGED
|
@@ -6,7 +6,9 @@ export * from './commands/info.js';
|
|
|
6
6
|
export * from './commands/init.js';
|
|
7
7
|
export * from './commands/generate.js';
|
|
8
8
|
export * from './commands/create.js';
|
|
9
|
+
export * from './commands/plugin.js';
|
|
9
10
|
export * from './commands/dev.js';
|
|
10
11
|
export * from './commands/serve.js';
|
|
11
12
|
export * from './commands/test.js';
|
|
12
13
|
export * from './commands/doctor.js';
|
|
14
|
+
export * from './utils/plugin-commands.js';
|