@objectstack/cli 3.0.6 → 3.0.8

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.
Files changed (140) hide show
  1. package/.turbo/turbo-build.log +2 -26
  2. package/CHANGELOG.md +27 -0
  3. package/README.md +98 -54
  4. package/bin/run-dev.js +5 -0
  5. package/bin/run.js +5 -0
  6. package/dist/bin.d.ts +11 -0
  7. package/dist/bin.d.ts.map +1 -0
  8. package/dist/bin.js +12 -3767
  9. package/dist/bin.js.map +1 -0
  10. package/dist/commands/codemod/v2-to-v3.d.ts +10 -0
  11. package/dist/commands/codemod/v2-to-v3.d.ts.map +1 -0
  12. package/dist/commands/codemod/v2-to-v3.js +145 -0
  13. package/dist/commands/codemod/v2-to-v3.js.map +1 -0
  14. package/dist/commands/compile.d.ts +13 -0
  15. package/dist/commands/compile.d.ts.map +1 -0
  16. package/dist/commands/compile.js +91 -0
  17. package/dist/commands/compile.js.map +1 -0
  18. package/dist/commands/create.d.ts +91 -0
  19. package/dist/commands/create.d.ts.map +1 -0
  20. package/dist/commands/create.js +259 -0
  21. package/dist/commands/create.js.map +1 -0
  22. package/dist/commands/dev.d.ts +14 -0
  23. package/dist/commands/dev.d.ts.map +1 -0
  24. package/dist/commands/dev.js +67 -0
  25. package/dist/commands/dev.js.map +1 -0
  26. package/dist/commands/diff.d.ts +16 -0
  27. package/dist/commands/diff.d.ts.map +1 -0
  28. package/dist/commands/diff.js +239 -0
  29. package/dist/commands/diff.js.map +1 -0
  30. package/dist/commands/doctor.d.ts +10 -0
  31. package/dist/commands/doctor.d.ts.map +1 -0
  32. package/dist/commands/doctor.js +532 -0
  33. package/dist/commands/doctor.js.map +1 -0
  34. package/dist/commands/explain.d.ts +12 -0
  35. package/dist/commands/explain.d.ts.map +1 -0
  36. package/dist/commands/explain.js +368 -0
  37. package/dist/commands/explain.js.map +1 -0
  38. package/dist/commands/generate.d.ts +17 -0
  39. package/dist/commands/generate.d.ts.map +1 -0
  40. package/dist/commands/generate.js +833 -0
  41. package/dist/commands/generate.js.map +1 -0
  42. package/dist/commands/info.d.ts +12 -0
  43. package/dist/commands/info.d.ts.map +1 -0
  44. package/dist/commands/info.js +100 -0
  45. package/dist/commands/info.js.map +1 -0
  46. package/dist/commands/init.d.ts +22 -0
  47. package/dist/commands/init.d.ts.map +1 -0
  48. package/dist/commands/init.js +295 -0
  49. package/dist/commands/init.js.map +1 -0
  50. package/dist/commands/lint.d.ts +13 -0
  51. package/dist/commands/lint.d.ts.map +1 -0
  52. package/dist/commands/lint.js +255 -0
  53. package/dist/commands/lint.js.map +1 -0
  54. package/dist/commands/plugin/add.d.ts +22 -0
  55. package/dist/commands/plugin/add.d.ts.map +1 -0
  56. package/dist/commands/plugin/add.js +93 -0
  57. package/dist/commands/plugin/add.js.map +1 -0
  58. package/dist/commands/plugin/info.d.ts +10 -0
  59. package/dist/commands/plugin/info.d.ts.map +1 -0
  60. package/dist/commands/plugin/info.js +65 -0
  61. package/dist/commands/plugin/info.js.map +1 -0
  62. package/dist/commands/plugin/list.d.ts +13 -0
  63. package/dist/commands/plugin/list.d.ts.map +1 -0
  64. package/dist/commands/plugin/list.js +78 -0
  65. package/dist/commands/plugin/list.js.map +1 -0
  66. package/dist/commands/plugin/remove.d.ts +20 -0
  67. package/dist/commands/plugin/remove.d.ts.map +1 -0
  68. package/dist/commands/plugin/remove.js +79 -0
  69. package/dist/commands/plugin/remove.js.map +1 -0
  70. package/dist/commands/serve.d.ts +15 -0
  71. package/dist/commands/serve.d.ts.map +1 -0
  72. package/dist/commands/serve.js +286 -0
  73. package/dist/commands/serve.js.map +1 -0
  74. package/dist/commands/studio.d.ts +19 -0
  75. package/dist/commands/studio.d.ts.map +1 -0
  76. package/dist/commands/studio.js +43 -0
  77. package/dist/commands/studio.js.map +1 -0
  78. package/dist/commands/test.d.ts +13 -0
  79. package/dist/commands/test.d.ts.map +1 -0
  80. package/dist/commands/test.js +120 -0
  81. package/dist/commands/test.js.map +1 -0
  82. package/dist/commands/validate.d.ts +13 -0
  83. package/dist/commands/validate.d.ts.map +1 -0
  84. package/dist/commands/validate.js +115 -0
  85. package/dist/commands/validate.js.map +1 -0
  86. package/dist/index.d.ts +15 -114
  87. package/dist/index.d.ts.map +1 -0
  88. package/dist/index.js +20 -2805
  89. package/dist/index.js.map +1 -0
  90. package/dist/utils/config.d.ts +21 -0
  91. package/dist/utils/config.d.ts.map +1 -0
  92. package/dist/utils/config.js +66 -0
  93. package/dist/utils/config.js.map +1 -0
  94. package/dist/utils/format.d.ts +52 -0
  95. package/dist/utils/format.d.ts.map +1 -0
  96. package/dist/utils/format.js +202 -0
  97. package/dist/utils/format.js.map +1 -0
  98. package/dist/utils/plugin-helpers.d.ts +14 -0
  99. package/dist/utils/plugin-helpers.d.ts.map +1 -0
  100. package/dist/utils/plugin-helpers.js +40 -0
  101. package/dist/utils/plugin-helpers.js.map +1 -0
  102. package/dist/utils/studio.d.ts +58 -0
  103. package/dist/utils/studio.d.ts.map +1 -0
  104. package/dist/utils/studio.js +288 -0
  105. package/dist/utils/studio.js.map +1 -0
  106. package/package.json +33 -15
  107. package/src/bin.ts +11 -104
  108. package/src/commands/{codemod.ts → codemod/v2-to-v3.ts} +21 -28
  109. package/src/commands/compile.ts +31 -22
  110. package/src/commands/create.ts +29 -19
  111. package/src/commands/dev.ts +21 -10
  112. package/src/commands/diff.ts +28 -19
  113. package/src/commands/doctor.ts +17 -10
  114. package/src/commands/explain.ts +20 -10
  115. package/src/commands/generate.ts +81 -90
  116. package/src/commands/info.ts +20 -11
  117. package/src/commands/init.ts +32 -20
  118. package/src/commands/lint.ts +24 -14
  119. package/src/commands/plugin/add.ts +112 -0
  120. package/src/commands/plugin/info.ts +79 -0
  121. package/src/commands/plugin/list.ts +93 -0
  122. package/src/commands/plugin/remove.ts +97 -0
  123. package/src/commands/serve.ts +30 -20
  124. package/src/commands/studio.ts +21 -11
  125. package/src/commands/test.ts +21 -10
  126. package/src/commands/validate.ts +32 -22
  127. package/src/index.ts +20 -12
  128. package/src/utils/plugin-helpers.ts +37 -0
  129. package/src/utils/studio.ts +0 -1
  130. package/test/commands.test.ts +76 -37
  131. package/test/plugin-commands.test.ts +42 -160
  132. package/test/plugin.test.ts +19 -23
  133. package/tsconfig.build.json +18 -0
  134. package/bin/objectstack.js +0 -2
  135. package/dist/chunk-T2YN4AB7.js +0 -249
  136. package/dist/chunk-XNACYTC5.js +0 -251
  137. package/dist/config-FOXDQ5F7.js +0 -10
  138. package/dist/config-GBR54FKL.js +0 -11
  139. package/src/commands/plugin.ts +0 -372
  140. package/src/utils/plugin-commands.ts +0 -163
@@ -1,372 +0,0 @@
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);
@@ -1,163 +0,0 @@
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 { loadConfig } from './config.js';
6
-
7
- /**
8
- * CLI Command Contribution resolved from a plugin manifest.
9
- */
10
- interface ResolvedCommandContribution {
11
- /** CLI command name */
12
- name: string;
13
- /** Brief description */
14
- description?: string;
15
- /** Module path to import */
16
- module?: string;
17
- /** Source plugin package name */
18
- pluginName: string;
19
- }
20
-
21
- /**
22
- * Discover CLI command contributions from installed plugins.
23
- *
24
- * Scans the project's `objectstack.config.ts` for plugins that declare
25
- * `contributes.commands` in their manifest, then dynamically imports
26
- * those plugin modules to register Commander.js commands.
27
- *
28
- * @param program - The root Commander.js program to register commands on
29
- */
30
- export async function loadPluginCommands(program: Command): Promise<void> {
31
- let config: Record<string, unknown>;
32
-
33
- try {
34
- const loaded = await loadConfig();
35
- config = loaded.config;
36
- } catch {
37
- // No config file found — nothing to load
38
- return;
39
- }
40
-
41
- const plugins: unknown[] = [
42
- ...((config.plugins as unknown[] | undefined) || []),
43
- ...((config.devPlugins as unknown[] | undefined) || []),
44
- ];
45
-
46
- // Collect command contributions from plugin manifests
47
- const contributions: ResolvedCommandContribution[] = [];
48
-
49
- for (const plugin of plugins) {
50
- if (!plugin || typeof plugin !== 'object') continue;
51
- const p = plugin as Record<string, unknown>;
52
-
53
- const manifest = p.manifest as Record<string, unknown> | undefined;
54
- const contributes = (manifest?.contributes ?? p.contributes) as Record<string, unknown> | undefined;
55
- if (!contributes) continue;
56
-
57
- const commands = contributes.commands as Array<Record<string, unknown>> | undefined;
58
- if (!Array.isArray(commands)) continue;
59
-
60
- const pluginName = resolvePluginName(p);
61
-
62
- for (const cmd of commands) {
63
- if (!cmd || typeof cmd.name !== 'string') continue;
64
- contributions.push({
65
- name: cmd.name,
66
- description: typeof cmd.description === 'string' ? cmd.description : undefined,
67
- module: typeof cmd.module === 'string' ? cmd.module : undefined,
68
- pluginName,
69
- });
70
- }
71
- }
72
-
73
- if (contributions.length === 0) return;
74
-
75
- // Load and register each contributed command
76
- for (const contribution of contributions) {
77
- try {
78
- const commands = await importPluginCommands(contribution);
79
- for (const cmd of commands) {
80
- program.addCommand(cmd);
81
- }
82
- } catch (error: unknown) {
83
- // Log warning but don't crash — plugin commands are optional
84
- if (process.env.DEBUG) {
85
- const message = error instanceof Error ? error.message : String(error);
86
- console.error(
87
- chalk.yellow(` ⚠ Failed to load CLI command '${contribution.name}' from plugin '${contribution.pluginName}': ${message}`)
88
- );
89
- }
90
- }
91
- }
92
- }
93
-
94
- /**
95
- * Import Commander.js commands from a plugin module.
96
- *
97
- * The module must export commands in one of these forms:
98
- * - `export const commands: Command[]`
99
- * - `export default Command`
100
- * - `export default Command[]`
101
- */
102
- async function importPluginCommands(
103
- contribution: ResolvedCommandContribution
104
- ): Promise<Command[]> {
105
- // Resolve the module specifier
106
- const moduleId = contribution.module
107
- ? `${contribution.pluginName}/${contribution.module.replace(/^\.\//, '')}`
108
- : contribution.pluginName;
109
-
110
- const mod = await import(moduleId);
111
-
112
- // Form 1: Named export `commands`
113
- if (Array.isArray(mod.commands)) {
114
- return mod.commands.filter(isCommandInstance);
115
- }
116
-
117
- // Form 2: Default export (single or array)
118
- const defaultExport = mod.default;
119
- if (defaultExport) {
120
- if (Array.isArray(defaultExport)) {
121
- return defaultExport.filter(isCommandInstance);
122
- }
123
- if (isCommandInstance(defaultExport)) {
124
- return [defaultExport];
125
- }
126
- }
127
-
128
- // Fallback: search for any Command instances in module exports
129
- const commands: Command[] = [];
130
- for (const key of Object.keys(mod)) {
131
- if (isCommandInstance(mod[key])) {
132
- commands.push(mod[key]);
133
- }
134
- }
135
-
136
- return commands;
137
- }
138
-
139
- /**
140
- * Check if a value is a Commander.js Command instance.
141
- * Uses duck-typing to avoid import dependency issues.
142
- */
143
- function isCommandInstance(value: unknown): value is Command {
144
- if (value === null || typeof value !== 'object') return false;
145
- const obj = value as Record<string, unknown>;
146
- return (
147
- typeof obj.name === 'function' &&
148
- typeof obj.description === 'function' &&
149
- typeof obj.action === 'function' &&
150
- typeof obj.parse === 'function'
151
- );
152
- }
153
-
154
- /**
155
- * Resolve a human-readable name from a plugin object.
156
- */
157
- function resolvePluginName(plugin: Record<string, unknown>): string {
158
- if (typeof plugin.name === 'string') return plugin.name;
159
- const manifest = plugin.manifest as Record<string, unknown> | undefined;
160
- if (manifest && typeof manifest.name === 'string') return manifest.name;
161
- if (plugin.constructor && plugin.constructor.name !== 'Object') return plugin.constructor.name;
162
- return 'unknown';
163
- }