@camunda8/cli 2.0.1-alpha.2 → 2.1.0-alpha.2

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.
@@ -3,46 +3,43 @@
3
3
  */
4
4
  import { getLogger } from "../logger.js";
5
5
  import { execSync } from 'node:child_process';
6
- import { readFileSync, existsSync } from 'node:fs';
6
+ import { readFileSync, existsSync, readdirSync } from 'node:fs';
7
7
  import { join } from 'node:path';
8
8
  import { clearLoadedPlugins } from "../plugin-loader.js";
9
- import { addPluginToRegistry, removePluginFromRegistry, getRegisteredPlugins, } from "../plugin-registry.js";
9
+ import { ensurePluginsDir } from "../config.js";
10
+ import { addPluginToRegistry, removePluginFromRegistry, getRegisteredPlugins, isPluginRegistered, getPluginEntry, } from "../plugin-registry.js";
10
11
  /**
11
12
  * Load a plugin (npm install wrapper)
12
13
  * Supports either package name or --from flag with URL
14
+ * Installs to global plugins directory
13
15
  */
14
16
  export async function loadPlugin(packageNameOrFrom, fromUrl) {
15
17
  const logger = getLogger();
16
18
  // Validate exclusive usage
17
19
  if (packageNameOrFrom && fromUrl) {
18
- logger.error('Cannot specify both package name and --from flag. Use either "c8 load plugin <name>" or "c8 load plugin --from <url>"');
20
+ logger.error('Cannot specify both package name and --from flag. Use either "c8ctl load plugin <name>" or "c8ctl load plugin --from <url>"');
19
21
  process.exit(1);
20
22
  }
21
23
  if (!packageNameOrFrom && !fromUrl) {
22
- logger.error('Package name or --from URL required. Usage: c8 load plugin <package-name> OR c8 load plugin --from <url>');
23
- process.exit(1);
24
- }
25
- // Check if we have package.json in current directory
26
- const packageJsonPath = join(process.cwd(), 'package.json');
27
- if (!existsSync(packageJsonPath)) {
28
- logger.error('No package.json found in current directory.');
29
- logger.info('💡 Actionable hint: Run "npm init -y" to create a package.json, or navigate to a directory with an existing package.json');
24
+ logger.error('Package name or --from URL required. Usage: c8ctl load plugin <package-name> OR c8ctl load plugin --from <url>');
30
25
  process.exit(1);
31
26
  }
27
+ // Get global plugins directory
28
+ const pluginsDir = ensurePluginsDir();
32
29
  try {
33
30
  let pluginName;
34
31
  let pluginSource;
35
32
  if (fromUrl) {
36
33
  // Install from URL (file://, https://, git://, etc.)
37
34
  logger.info(`Loading plugin from: ${fromUrl}...`);
38
- execSync(`npm install ${fromUrl}`, { stdio: 'inherit' });
39
- // Extract package name from URL using pattern matching
40
- pluginName = extractPackageNameFromUrl(fromUrl);
35
+ execSync(`npm install ${fromUrl} --prefix "${pluginsDir}"`, { stdio: 'inherit' });
36
+ // Extract package name from installed package
37
+ pluginName = extractPackageNameFromUrl(fromUrl, pluginsDir);
41
38
  pluginSource = fromUrl;
42
39
  // Validate plugin name
43
40
  if (!pluginName || pluginName.trim() === '') {
44
41
  logger.error('Failed to extract plugin name from URL');
45
- logger.info('💡 Actionable hint: Ensure the URL points to a valid npm package with a package.json file');
42
+ logger.info('Ensure the URL points to a valid npm package with a package.json file');
46
43
  process.exit(1);
47
44
  }
48
45
  logger.success('Plugin loaded successfully from URL', fromUrl);
@@ -50,7 +47,7 @@ export async function loadPlugin(packageNameOrFrom, fromUrl) {
50
47
  else {
51
48
  // Install from npm registry by package name
52
49
  logger.info(`Loading plugin: ${packageNameOrFrom}...`);
53
- execSync(`npm install ${packageNameOrFrom}`, { stdio: 'inherit' });
50
+ execSync(`npm install ${packageNameOrFrom} --prefix "${pluginsDir}"`, { stdio: 'inherit' });
54
51
  pluginName = packageNameOrFrom;
55
52
  pluginSource = packageNameOrFrom;
56
53
  logger.success('Plugin loaded successfully', packageNameOrFrom);
@@ -64,39 +61,112 @@ export async function loadPlugin(packageNameOrFrom, fromUrl) {
64
61
  }
65
62
  catch (error) {
66
63
  logger.error('Failed to load plugin', error);
67
- logger.info('💡 Actionable hint: Check that the plugin name/URL is correct and you have network access if loading from a remote source');
64
+ logger.info('Check that the plugin name/URL is correct and you have network access if loading from a remote source');
68
65
  process.exit(1);
69
66
  }
70
67
  }
71
68
  /**
72
- * Extract package name from URL or path
73
- * This is a best-effort extraction - for complex cases, the user may need to specify manually
74
- * Note: This doesn't handle all edge cases like scoped packages in git URLs
69
+ * Check if a package has a c8ctl plugin file
75
70
  */
76
- function extractPackageNameFromUrl(url) {
77
- // For npm packages: git+https://github.com/user/repo.git -> repo
78
- // For file paths: file:///path/to/plugin -> plugin
79
- // For git URLs: git://github.com/user/repo.git -> repo
80
- // Note: Scoped packages like @scope/package in URLs are not fully supported
81
- const match = url.match(/\/([^\/]+?)(\.git)?$/);
82
- if (match) {
83
- return match[1];
71
+ function hasPluginFile(packagePath) {
72
+ return existsSync(join(packagePath, 'c8ctl-plugin.js')) ||
73
+ existsSync(join(packagePath, 'c8ctl-plugin.ts'));
74
+ }
75
+ /**
76
+ * Check if a package is a valid c8ctl plugin
77
+ */
78
+ function isValidPlugin(pkgPath) {
79
+ const pkgJsonPath = join(pkgPath, 'package.json');
80
+ if (!existsSync(pkgJsonPath))
81
+ return false;
82
+ try {
83
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
84
+ return hasPluginFile(pkgPath) && pkgJson.keywords?.includes('c8ctl');
85
+ }
86
+ catch {
87
+ return false;
88
+ }
89
+ }
90
+ /**
91
+ * Get package name from a valid plugin directory
92
+ */
93
+ function getPackageName(pkgPath) {
94
+ const pkgJsonPath = join(pkgPath, 'package.json');
95
+ try {
96
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
97
+ return pkgJson.name;
98
+ }
99
+ catch {
100
+ return null;
84
101
  }
85
- // Fallback: use a cleaned version of the URL as the name
86
- return url.replace(/[^a-zA-Z0-9-_@\/]/g, '-');
102
+ }
103
+ /**
104
+ * Scan directory entries for c8ctl plugins
105
+ */
106
+ function scanForPlugin(nodeModulesPath, entries) {
107
+ for (const entry of entries.filter(e => !e.startsWith('.'))) {
108
+ const pkgPath = entry.startsWith('@')
109
+ ? null // Scoped packages handled separately
110
+ : join(nodeModulesPath, entry);
111
+ if (pkgPath && isValidPlugin(pkgPath)) {
112
+ return getPackageName(pkgPath);
113
+ }
114
+ // Handle scoped packages
115
+ if (entry.startsWith('@')) {
116
+ const scopePath = join(nodeModulesPath, entry);
117
+ try {
118
+ const scopedPackages = readdirSync(scopePath);
119
+ for (const scopedPkg of scopedPackages) {
120
+ const pkgPath = join(scopePath, scopedPkg);
121
+ if (isValidPlugin(pkgPath)) {
122
+ return getPackageName(pkgPath);
123
+ }
124
+ }
125
+ }
126
+ catch {
127
+ // Skip scoped packages that can't be read
128
+ }
129
+ }
130
+ }
131
+ return null;
132
+ }
133
+ /**
134
+ * Extract package name from URL or installed package
135
+ * Tries to read package.json from installed package, falls back to URL parsing
136
+ */
137
+ function extractPackageNameFromUrl(url, pluginsDir) {
138
+ // Try to scan node_modules to find the package by reading package.json
139
+ try {
140
+ const nodeModulesPath = join(pluginsDir, 'node_modules');
141
+ if (existsSync(nodeModulesPath)) {
142
+ const entries = readdirSync(nodeModulesPath);
143
+ const foundName = scanForPlugin(nodeModulesPath, entries);
144
+ if (foundName)
145
+ return foundName;
146
+ }
147
+ }
148
+ catch (error) {
149
+ // Fall through to URL-based name extraction
150
+ }
151
+ // Fallback: extract from URL pattern
152
+ const match = url.match(/\/([^\/]+?)(\.git)?$/);
153
+ return match ? match[1] : url.replace(/[^a-zA-Z0-9-_@\/]/g, '-');
87
154
  }
88
155
  /**
89
156
  * Unload a plugin (npm uninstall wrapper)
157
+ * Uninstalls from global plugins directory
90
158
  */
91
159
  export async function unloadPlugin(packageName) {
92
160
  const logger = getLogger();
93
161
  if (!packageName) {
94
- logger.error('Package name required. Usage: c8 unload plugin <package-name>');
162
+ logger.error('Package name required. Usage: c8ctl unload plugin <package-name>');
95
163
  process.exit(1);
96
164
  }
165
+ // Get global plugins directory
166
+ const pluginsDir = ensurePluginsDir();
97
167
  try {
98
168
  logger.info(`Unloading plugin: ${packageName}...`);
99
- execSync(`npm uninstall ${packageName}`, { stdio: 'inherit' });
169
+ execSync(`npm uninstall ${packageName} --prefix "${pluginsDir}"`, { stdio: 'inherit' });
100
170
  // Only remove from registry after successful uninstall
101
171
  removePluginFromRegistry(packageName);
102
172
  logger.debug(`Removed ${packageName} from plugin registry`);
@@ -108,10 +178,59 @@ export async function unloadPlugin(packageName) {
108
178
  }
109
179
  catch (error) {
110
180
  logger.error('Failed to unload plugin', error);
111
- logger.info('💡 Actionable hint: Verify the plugin name is correct by running "c8 list plugins"');
181
+ logger.info('Verify the plugin name is correct by running "c8ctl list plugins"');
112
182
  process.exit(1);
113
183
  }
114
184
  }
185
+ /**
186
+ * Scan a directory entry for c8ctl plugins and add to the set
187
+ */
188
+ function addPluginIfFound(entry, nodeModulesPath, installedPlugins) {
189
+ if (entry.startsWith('.'))
190
+ return;
191
+ if (entry.startsWith('@')) {
192
+ // Scoped package - scan subdirectories
193
+ const scopePath = join(nodeModulesPath, entry);
194
+ try {
195
+ readdirSync(scopePath)
196
+ .filter(pkg => !pkg.startsWith('.'))
197
+ .forEach(scopedPkg => {
198
+ const packageNameWithScope = `${entry}/${scopedPkg}`;
199
+ const packagePath = join(nodeModulesPath, entry, scopedPkg);
200
+ if (hasPluginFile(packagePath)) {
201
+ installedPlugins.add(packageNameWithScope);
202
+ }
203
+ });
204
+ }
205
+ catch {
206
+ // Skip packages that can't be read
207
+ }
208
+ }
209
+ else {
210
+ // Regular package
211
+ const packagePath = join(nodeModulesPath, entry);
212
+ if (hasPluginFile(packagePath)) {
213
+ installedPlugins.add(entry);
214
+ }
215
+ }
216
+ }
217
+ /**
218
+ * Scan node_modules for installed plugins
219
+ */
220
+ function scanInstalledPlugins(nodeModulesPath) {
221
+ const installedPlugins = new Set();
222
+ if (!existsSync(nodeModulesPath)) {
223
+ return installedPlugins;
224
+ }
225
+ try {
226
+ const entries = readdirSync(nodeModulesPath);
227
+ entries.forEach(entry => addPluginIfFound(entry, nodeModulesPath, installedPlugins));
228
+ }
229
+ catch (error) {
230
+ getLogger().debug('Error scanning global plugins directory:', error);
231
+ }
232
+ return installedPlugins;
233
+ }
115
234
  /**
116
235
  * List installed plugins
117
236
  */
@@ -120,34 +239,15 @@ export function listPlugins() {
120
239
  try {
121
240
  // Get plugins from registry (local source of truth)
122
241
  const registeredPlugins = getRegisteredPlugins();
123
- // Check package.json if it exists
124
- const packageJsonPath = join(process.cwd(), 'package.json');
125
- let packageJsonPlugins = new Set();
126
- if (existsSync(packageJsonPath)) {
127
- const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
128
- const dependencies = packageJson.dependencies || {};
129
- const devDependencies = packageJson.devDependencies || {};
130
- const allDeps = { ...dependencies, ...devDependencies };
131
- // Find c8ctl plugins in package.json
132
- for (const [name] of Object.entries(allDeps)) {
133
- try {
134
- const packageDir = join(process.cwd(), 'node_modules', name);
135
- const hasPluginFile = existsSync(join(packageDir, 'c8ctl-plugin.js')) ||
136
- existsSync(join(packageDir, 'c8ctl-plugin.ts'));
137
- if (hasPluginFile) {
138
- packageJsonPlugins.add(name);
139
- }
140
- }
141
- catch {
142
- // Skip packages that can't be read
143
- }
144
- }
145
- }
242
+ // Check global plugins directory
243
+ const pluginsDir = ensurePluginsDir();
244
+ const nodeModulesPath = join(pluginsDir, 'node_modules');
245
+ const installedPlugins = scanInstalledPlugins(nodeModulesPath);
146
246
  // Build unified list with status
147
247
  const plugins = [];
148
248
  // Add registered plugins
149
249
  for (const plugin of registeredPlugins) {
150
- const isInstalled = packageJsonPlugins.has(plugin.name);
250
+ const isInstalled = installedPlugins.has(plugin.name);
151
251
  const installStatus = isInstalled ? '✓ Installed' : '⚠ Not installed';
152
252
  plugins.push({
153
253
  Name: plugin.name,
@@ -155,14 +255,14 @@ export function listPlugins() {
155
255
  Source: plugin.source,
156
256
  'Installed At': new Date(plugin.installedAt).toLocaleString(),
157
257
  });
158
- packageJsonPlugins.delete(plugin.name);
258
+ installedPlugins.delete(plugin.name);
159
259
  }
160
- // Add any plugins in package.json but not in registry
161
- for (const name of packageJsonPlugins) {
260
+ // Add any plugins installed but not in registry
261
+ for (const name of installedPlugins) {
162
262
  plugins.push({
163
263
  Name: name,
164
264
  Status: '⚠ Not in registry',
165
- Source: 'package.json',
265
+ Source: 'Unknown',
166
266
  'Installed At': 'Unknown',
167
267
  });
168
268
  }
@@ -175,28 +275,23 @@ export function listPlugins() {
175
275
  logger.table(plugins);
176
276
  if (needsSync) {
177
277
  logger.info('');
178
- logger.info('💡 Actionable hint: Some plugins are out of sync. Run "c8 sync plugins" to synchronize your plugins');
278
+ logger.info('Some plugins are out of sync. Run "c8ctl sync plugins" to synchronize your plugins');
179
279
  }
180
280
  }
181
281
  catch (error) {
182
282
  logger.error('Failed to list plugins', error);
183
- logger.info('💡 Actionable hint: Ensure you are in a directory with a package.json file');
184
283
  process.exit(1);
185
284
  }
186
285
  }
187
286
  /**
188
287
  * Sync plugins - synchronize registry with actual installations
189
- * Local (registry) has precedence over package.json
288
+ * Registry has precedence - plugins are installed to global directory
190
289
  */
191
290
  export async function syncPlugins() {
192
291
  const logger = getLogger();
193
- // Check if package.json exists
194
- const packageJsonPath = join(process.cwd(), 'package.json');
195
- if (!existsSync(packageJsonPath)) {
196
- logger.error('No package.json found in current directory.');
197
- logger.info('💡 Actionable hint: Run "npm init -y" to create a package.json, or navigate to a directory with an existing package.json');
198
- process.exit(1);
199
- }
292
+ // Get global plugins directory
293
+ const pluginsDir = ensurePluginsDir();
294
+ const nodeModulesPath = join(pluginsDir, 'node_modules');
200
295
  logger.info('Starting plugin synchronization...');
201
296
  logger.info('');
202
297
  // Get registered plugins (local source of truth)
@@ -217,14 +312,14 @@ export async function syncPlugins() {
217
312
  for (const plugin of registeredPlugins) {
218
313
  logger.info(`Syncing ${plugin.name}...`);
219
314
  try {
220
- // Check if plugin is installed
221
- const packageDir = join(process.cwd(), 'node_modules', plugin.name);
315
+ // Check if plugin is installed in global directory
316
+ const packageDir = join(nodeModulesPath, plugin.name);
222
317
  const isInstalled = existsSync(packageDir);
223
318
  if (isInstalled) {
224
319
  logger.info(` ✓ ${plugin.name} is already installed, attempting rebuild...`);
225
320
  // Try npm rebuild first
226
321
  try {
227
- execSync(`npm rebuild ${plugin.name}`, { stdio: 'pipe' });
322
+ execSync(`npm rebuild ${plugin.name} --prefix "${pluginsDir}"`, { stdio: 'pipe' });
228
323
  logger.success(` ✓ ${plugin.name} rebuilt successfully`);
229
324
  syncedCount++;
230
325
  continue;
@@ -238,7 +333,7 @@ export async function syncPlugins() {
238
333
  }
239
334
  // Fresh install
240
335
  try {
241
- execSync(`npm install ${plugin.source}`, { stdio: 'inherit' });
336
+ execSync(`npm install ${plugin.source} --prefix "${pluginsDir}"`, { stdio: 'inherit' });
242
337
  logger.success(` ✓ ${plugin.name} installed successfully`);
243
338
  syncedCount++;
244
339
  }
@@ -272,9 +367,267 @@ export async function syncPlugins() {
272
367
  logger.error(` - ${failure.plugin}: ${failure.error}`);
273
368
  }
274
369
  logger.info('');
275
- logger.info('💡 Actionable hint: Check network connectivity and verify plugin sources are accessible. You may need to remove failed plugins from the registry with "c8 unload plugin <name>"');
370
+ logger.info('Check network connectivity and verify plugin sources are accessible. You may need to remove failed plugins from the registry with "c8ctl unload plugin <name>"');
276
371
  process.exit(1);
277
372
  }
278
373
  logger.success('All plugins synced successfully!');
279
374
  }
375
+ /**
376
+ * Upgrade a plugin to the latest version or a specific version
377
+ */
378
+ export async function upgradePlugin(packageName, version) {
379
+ const logger = getLogger();
380
+ if (!packageName) {
381
+ logger.error('Package name required. Usage: c8ctl upgrade plugin <package-name> [version]');
382
+ process.exit(1);
383
+ }
384
+ // Check if plugin is registered
385
+ if (!isPluginRegistered(packageName)) {
386
+ logger.error(`Plugin "${packageName}" is not registered.`);
387
+ logger.info('Run "c8ctl list plugins" to see installed plugins');
388
+ process.exit(1);
389
+ }
390
+ const pluginEntry = getPluginEntry(packageName);
391
+ const pluginsDir = ensurePluginsDir();
392
+ try {
393
+ const versionSpec = version ? `@${version}` : '@latest';
394
+ logger.info(`Upgrading plugin: ${packageName} to ${version || 'latest'}...`);
395
+ // Uninstall current version
396
+ execSync(`npm uninstall ${packageName} --prefix "${pluginsDir}"`, { stdio: 'pipe' });
397
+ // Install new version
398
+ const installTarget = version ? `${packageName}${versionSpec}` : pluginEntry.source;
399
+ execSync(`npm install ${installTarget} --prefix "${pluginsDir}"`, { stdio: 'inherit' });
400
+ // Update registry with new source if version was specified
401
+ if (version) {
402
+ addPluginToRegistry(packageName, `${packageName}${versionSpec}`);
403
+ }
404
+ // Clear plugin cache
405
+ clearLoadedPlugins();
406
+ logger.success('Plugin upgraded successfully', packageName);
407
+ logger.info('Plugin will be available on next command execution');
408
+ }
409
+ catch (error) {
410
+ logger.error('Failed to upgrade plugin', error);
411
+ logger.info('Check network connectivity and verify the package/version exists');
412
+ process.exit(1);
413
+ }
414
+ }
415
+ /**
416
+ * Downgrade a plugin to a specific version
417
+ */
418
+ export async function downgradePlugin(packageName, version) {
419
+ const logger = getLogger();
420
+ if (!packageName || !version) {
421
+ logger.error('Package name and version required. Usage: c8ctl downgrade plugin <package-name> <version>');
422
+ process.exit(1);
423
+ }
424
+ // Check if plugin is registered
425
+ if (!isPluginRegistered(packageName)) {
426
+ logger.error(`Plugin "${packageName}" is not registered.`);
427
+ logger.info('Run "c8ctl list plugins" to see installed plugins');
428
+ process.exit(1);
429
+ }
430
+ const pluginsDir = ensurePluginsDir();
431
+ try {
432
+ logger.info(`Downgrading plugin: ${packageName} to version ${version}...`);
433
+ // Uninstall current version
434
+ execSync(`npm uninstall ${packageName} --prefix "${pluginsDir}"`, { stdio: 'pipe' });
435
+ // Install specific version
436
+ const installTarget = `${packageName}@${version}`;
437
+ execSync(`npm install ${installTarget} --prefix "${pluginsDir}"`, { stdio: 'inherit' });
438
+ // Update registry with new source
439
+ addPluginToRegistry(packageName, installTarget);
440
+ // Clear plugin cache
441
+ clearLoadedPlugins();
442
+ logger.success('Plugin downgraded successfully', packageName);
443
+ logger.info('Plugin will be available on next command execution');
444
+ }
445
+ catch (error) {
446
+ logger.error('Failed to downgrade plugin', error);
447
+ logger.info('Check network connectivity and verify the version exists');
448
+ process.exit(1);
449
+ }
450
+ }
451
+ /**
452
+ * Initialize a new plugin project with TypeScript template
453
+ */
454
+ export async function initPlugin(pluginName) {
455
+ const logger = getLogger();
456
+ const { mkdirSync, writeFileSync } = await import('node:fs');
457
+ const { resolve } = await import('node:path');
458
+ // Use provided name or default
459
+ const name = pluginName || 'my-c8ctl-plugin';
460
+ const dirName = name.startsWith('c8ctl-') ? name : `c8ctl-${name}`;
461
+ const pluginDir = resolve(process.cwd(), dirName);
462
+ // Check if directory already exists
463
+ if (existsSync(pluginDir)) {
464
+ logger.error(`Directory "${dirName}" already exists.`);
465
+ logger.info('Choose a different name or remove the existing directory');
466
+ process.exit(1);
467
+ }
468
+ try {
469
+ logger.info(`Creating plugin: ${dirName}...`);
470
+ // Create plugin directory
471
+ mkdirSync(pluginDir, { recursive: true });
472
+ // Create package.json
473
+ const packageJson = {
474
+ name: dirName,
475
+ version: '1.0.0',
476
+ type: 'module',
477
+ description: `A c8ctl plugin`,
478
+ keywords: ['c8ctl', 'c8ctl-plugin'],
479
+ main: 'c8ctl-plugin.js',
480
+ scripts: {
481
+ build: 'tsc',
482
+ watch: 'tsc --watch',
483
+ },
484
+ devDependencies: {
485
+ typescript: '^5.0.0',
486
+ '@types/node': '^22.0.0',
487
+ },
488
+ };
489
+ writeFileSync(join(pluginDir, 'package.json'), JSON.stringify(packageJson, null, 2));
490
+ // Create tsconfig.json
491
+ const tsConfig = {
492
+ compilerOptions: {
493
+ target: 'ES2022',
494
+ module: 'ES2022',
495
+ moduleResolution: 'node',
496
+ outDir: '.',
497
+ rootDir: './src',
498
+ strict: true,
499
+ esModuleInterop: true,
500
+ skipLibCheck: true,
501
+ forceConsistentCasingInFileNames: true,
502
+ },
503
+ include: ['src/**/*'],
504
+ exclude: ['node_modules'],
505
+ };
506
+ writeFileSync(join(pluginDir, 'tsconfig.json'), JSON.stringify(tsConfig, null, 2));
507
+ // Create src directory
508
+ mkdirSync(join(pluginDir, 'src'), { recursive: true });
509
+ // Create c8ctl-plugin.ts
510
+ const pluginTemplate = `/**
511
+ * ${dirName} - A c8ctl plugin
512
+ */
513
+
514
+ // The c8ctl runtime is available globally
515
+ declare const c8ctl: {
516
+ version: string;
517
+ nodeVersion: string;
518
+ platform: string;
519
+ arch: string;
520
+ cwd: string;
521
+ outputMode: 'text' | 'json';
522
+ activeProfile?: string;
523
+ activeTenant?: string;
524
+ };
525
+
526
+ // Optional metadata for help text
527
+ export const metadata = {
528
+ name: '${dirName}',
529
+ description: 'A c8ctl plugin',
530
+ commands: {
531
+ hello: {
532
+ description: 'Say hello from the plugin',
533
+ },
534
+ },
535
+ };
536
+
537
+ // Required commands export
538
+ export const commands = {
539
+ hello: async (args: string[]) => {
540
+ console.log('Hello from ${dirName}!');
541
+ console.log('c8ctl version:', c8ctl.version);
542
+ console.log('Node version:', c8ctl.nodeVersion);
543
+
544
+ if (args.length > 0) {
545
+ console.log('Arguments:', args.join(', '));
546
+ }
547
+
548
+ // Example: Access c8ctl runtime
549
+ console.log('Current directory:', c8ctl.cwd);
550
+ console.log('Output mode:', c8ctl.outputMode);
551
+
552
+ if (c8ctl.activeProfile) {
553
+ console.log('Active profile:', c8ctl.activeProfile);
554
+ }
555
+ },
556
+ };
557
+ `;
558
+ writeFileSync(join(pluginDir, 'src', 'c8ctl-plugin.ts'), pluginTemplate);
559
+ // Create README.md
560
+ const readme = `# ${dirName}
561
+
562
+ A c8ctl plugin.
563
+
564
+ ## Development
565
+
566
+ 1. Install dependencies:
567
+ \`\`\`bash
568
+ npm install
569
+ \`\`\`
570
+
571
+ 2. Build the plugin:
572
+ \`\`\`bash
573
+ npm run build
574
+ \`\`\`
575
+
576
+ 3. Load the plugin for testing:
577
+ \`\`\`bash
578
+ c8ctl load plugin --from file://\${PWD}
579
+ \`\`\`
580
+
581
+ 4. Test the plugin command:
582
+ \`\`\`bash
583
+ c8ctl hello
584
+ \`\`\`
585
+
586
+ ## Plugin Structure
587
+
588
+ - \`src/c8ctl-plugin.ts\` - Plugin source code (TypeScript)
589
+ - \`c8ctl-plugin.js\` - Compiled plugin file (JavaScript)
590
+ - \`package.json\` - Package metadata with c8ctl keywords
591
+
592
+ ## Publishing
593
+
594
+ Before publishing, ensure:
595
+ - The plugin is built (\`npm run build\`)
596
+ - The package.json has correct metadata
597
+ - Keywords include 'c8ctl' or 'c8ctl-plugin'
598
+
599
+ Then publish to npm:
600
+ \`\`\`bash
601
+ npm publish
602
+ \`\`\`
603
+
604
+ Users can install your plugin with:
605
+ \`\`\`bash
606
+ c8ctl load plugin ${dirName}
607
+ \`\`\`
608
+ `;
609
+ writeFileSync(join(pluginDir, 'README.md'), readme);
610
+ // Create .gitignore
611
+ const gitignore = `node_modules/
612
+ *.js
613
+ *.js.map
614
+ !c8ctl-plugin.js
615
+ `;
616
+ writeFileSync(join(pluginDir, '.gitignore'), gitignore);
617
+ logger.success('Plugin scaffolding created successfully!');
618
+ logger.info('');
619
+ logger.info(`Next steps:`);
620
+ logger.info(` 1. cd ${dirName}`);
621
+ logger.info(` 2. npm install`);
622
+ logger.info(` 3. npm run build`);
623
+ logger.info(` 4. c8ctl load plugin --from file://\${PWD}`);
624
+ logger.info(` 5. c8ctl hello`);
625
+ logger.info('');
626
+ logger.info(`Edit src/c8ctl-plugin.ts to add your plugin logic.`);
627
+ }
628
+ catch (error) {
629
+ logger.error('Failed to create plugin', error);
630
+ process.exit(1);
631
+ }
632
+ }
280
633
  //# sourceMappingURL=plugins.js.map