@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectstack/cli",
3
- "version": "2.0.6",
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": "2.0.6",
26
- "@objectstack/driver-memory": "^2.0.6",
27
- "@objectstack/objectql": "^2.0.6",
28
- "@objectstack/plugin-hono-server": "2.0.6",
29
- "@objectstack/rest": "2.0.6",
30
- "@objectstack/runtime": "^2.0.6",
31
- "@objectstack/spec": "2.0.6"
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": "2.0.6"
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
- program.parse(process.argv);
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
+ });
@@ -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
- export const generateCommand = new Command('generate')
210
- .alias('g')
211
- .description('Generate metadata files (object, view, action, flow, agent, dashboard, app)')
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';