Skip to content

Commit 9db2bd0

Browse files
authored
feat(plugins): add --from flag to load plugin command (#5)
* feat(plugins): add --from flag to load plugin command - Add --from flag to load plugins from any URL (npm, file://, https://, git://) - Enforce exclusive usage of package name vs --from flag - Update help text and documentation with new functionality - Add examples for loading plugins from various sources - Adhere to npm standards by wrapping npm install directly Commands are now mutually exclusive: - c8 load plugin <name> (from npm registry) - c8 load plugin --from <url> (from any valid URL) Refs: Plugin system enhancement * refactor(plugins): remove metadata export requirement - Plugins now only need to export commands object - Remove metadata validation from listPlugins - Simplify plugin detection to check for c8ctl-plugin.js/ts file - Update all tests to remove metadata checks - Update documentation and examples This simplifies plugin development by removing unnecessary boilerplate.
1 parent c96ebd9 commit 9db2bd0

File tree

9 files changed

+66
-67
lines changed

9 files changed

+66
-67
lines changed

EXAMPLES.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -357,9 +357,14 @@ c8 output text
357357
### Load Plugin
358358

359359
```bash
360-
# Install a c8ctl plugin from npm
360+
# Install a c8ctl plugin from npm registry
361361
c8 load plugin my-custom-plugin
362362

363+
# Install a plugin from a URL (file, https, git, etc.)
364+
c8 load plugin --from https://github.com/user/my-plugin
365+
c8 load plugin --from file:///path/to/local/plugin
366+
c8 load plugin --from git://github.com/user/plugin.git
367+
363368
# The plugin is now available
364369
# (assuming the plugin exports an 'analyze' command)
365370
c8 analyze
@@ -381,7 +386,7 @@ c8 list plugins
381386

382387
**Plugin Development:**
383388

384-
Plugins should include a `c8ctl-plugin.js` or `c8ctl-plugin.ts` file that exports custom commands. The `c8ctl` runtime object provides environment information:
389+
Plugins must be regular Node.js modules with a `c8ctl-plugin.js` or `c8ctl-plugin.ts` file in the root directory. The plugin file must export a `commands` object. The `c8ctl` runtime object provides environment information:
385390

386391
```typescript
387392
// c8ctl-plugin.ts

README.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,15 @@ c8 output text
124124
c8ctl supports a plugin system that allows extending the CLI with custom commands via npm packages.
125125

126126
```bash
127-
# Load a plugin (wraps npm install)
127+
# Load a plugin from npm registry
128128
c8 load plugin <package-name>
129129

130+
# Load a plugin from a URL (including file URLs)
131+
c8 load plugin --from <url>
132+
c8 load plugin --from file:///path/to/plugin
133+
c8 load plugin --from https://github.com/user/repo
134+
c8 load plugin --from git://github.com/user/repo.git
135+
130136
# Unload a plugin (wraps npm uninstall)
131137
c8 unload plugin <package-name>
132138

@@ -135,8 +141,9 @@ c8 list plugins
135141
```
136142

137143
**Plugin Requirements:**
138-
- Plugin packages must include a `c8ctl-plugin.js` or `c8ctl-plugin.ts` file
139-
- The plugin file should export custom commands
144+
- Plugin packages must be regular Node.js modules
145+
- They must include a `c8ctl-plugin.js` or `c8ctl-plugin.ts` file in the root directory
146+
- The plugin file must export a `commands` object
140147
- Plugins are installed in `node_modules` like regular npm packages
141148
- The runtime object `c8ctl` provides environment information to plugins
142149

src/commands/help.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,16 @@ Commands:
5252
run <path> Deploy and start process
5353
add profile <name> Add a profile
5454
remove profile <name> Remove a profile (alias: rm)
55-
load plugin <name> Load a c8ctl plugin (npm install wrapper)
55+
load plugin <name> Load a c8ctl plugin from npm registry
56+
load plugin --from Load a c8ctl plugin from URL (file://, https://, git://)
5657
unload plugin <name> Unload a c8ctl plugin (npm uninstall wrapper)
5758
use profile|tenant Set active profile or tenant
5859
output json|text Set output format
5960
help Show this help
6061
6162
Flags:
6263
--profile <name> Use specific profile for this command
64+
--from <url> Load plugin from URL (use with 'load plugin')
6365
--version, -v Show version
6466
--help, -h Show help
6567
@@ -77,6 +79,8 @@ Examples:
7779
c8 run ./my-process.bpmn Deploy and start process
7880
c8 use profile prod Set active profile
7981
c8 output json Switch to JSON output
82+
c8 load plugin my-plugin Load plugin from npm registry
83+
c8 load plugin --from file:///path/to/plugin Load plugin from file URL
8084
`.trim());
8185
}
8286

src/commands/plugins.ts

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,39 @@
44

55
import { getLogger } from '../logger.ts';
66
import { execSync } from 'node:child_process';
7-
import { readFileSync } from 'node:fs';
7+
import { readFileSync, existsSync } from 'node:fs';
88
import { join } from 'node:path';
99

1010
/**
1111
* Load a plugin (npm install wrapper)
12+
* Supports either package name or --from flag with URL
1213
*/
13-
export function loadPlugin(packageName: string): void {
14+
export function loadPlugin(packageNameOrFrom?: string, fromUrl?: string): void {
1415
const logger = getLogger();
1516

16-
if (!packageName) {
17-
logger.error('Package name required. Usage: c8 load plugin <package-name>');
17+
// Validate exclusive usage
18+
if (packageNameOrFrom && fromUrl) {
19+
logger.error('Cannot specify both package name and --from flag. Use either "c8 load plugin <name>" or "c8 load plugin --from <url>"');
20+
process.exit(1);
21+
}
22+
23+
if (!packageNameOrFrom && !fromUrl) {
24+
logger.error('Package name or --from URL required. Usage: c8 load plugin <package-name> OR c8 load plugin --from <url>');
1825
process.exit(1);
1926
}
2027

2128
try {
22-
logger.info(`Loading plugin: ${packageName}...`);
23-
execSync(`npm install ${packageName}`, { stdio: 'inherit' });
24-
logger.success('Plugin loaded successfully', packageName);
29+
if (fromUrl) {
30+
// Install from URL (file://, https://, git://, etc.)
31+
logger.info(`Loading plugin from: ${fromUrl}...`);
32+
execSync(`npm install ${fromUrl}`, { stdio: 'inherit' });
33+
logger.success('Plugin loaded successfully from URL', fromUrl);
34+
} else {
35+
// Install from npm registry by package name
36+
logger.info(`Loading plugin: ${packageNameOrFrom}...`);
37+
execSync(`npm install ${packageNameOrFrom}`, { stdio: 'inherit' });
38+
logger.success('Plugin loaded successfully', packageNameOrFrom);
39+
}
2540
} catch (error) {
2641
logger.error('Failed to load plugin', error as Error);
2742
process.exit(1);
@@ -70,16 +85,13 @@ export function listPlugins(): void {
7085
for (const [name, version] of Object.entries(allDeps)) {
7186
try {
7287
// Try to resolve the package
73-
const packagePath = join(process.cwd(), 'node_modules', name, 'package.json');
74-
const pkgJson = JSON.parse(readFileSync(packagePath, 'utf-8'));
88+
const packageDir = join(process.cwd(), 'node_modules', name);
7589

76-
// Check if package exports c8ctl-plugin.js or c8ctl-plugin.ts
77-
const hasPlugin = pkgJson.main === 'c8ctl-plugin.js' ||
78-
pkgJson.main === 'c8ctl-plugin.ts' ||
79-
pkgJson.exports?.['./c8ctl-plugin.js'] ||
80-
pkgJson.exports?.['./c8ctl-plugin.ts'];
90+
// Check if package has c8ctl-plugin.js or c8ctl-plugin.ts file in root
91+
const hasPluginFile = existsSync(join(packageDir, 'c8ctl-plugin.js')) ||
92+
existsSync(join(packageDir, 'c8ctl-plugin.ts'));
8193

82-
if (hasPlugin) {
94+
if (hasPluginFile) {
8395
plugins.push({
8496
Name: name,
8597
Version: version as string,

src/index.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ function parseCliArgs() {
7373
oAuthUrl: { type: 'string' },
7474
defaultTenantId: { type: 'string' },
7575
version_num: { type: 'string' },
76+
from: { type: 'string' },
7677
},
7778
allowPositionals: true,
7879
strict: false,
@@ -192,11 +193,21 @@ async function main() {
192193
}
193194

194195
if (verb === 'load' && normalizedResource === 'plugin') {
195-
if (!args[0]) {
196-
logger.error('Package name required. Usage: c8 load plugin <package-name>');
196+
const fromUrl = values.from as string | undefined;
197+
const packageName = args[0];
198+
199+
// Ensure exclusive usage
200+
if (packageName && fromUrl) {
201+
logger.error('Cannot specify both package name and --from flag. Use either "c8 load plugin <name>" or "c8 load plugin --from <url>"');
202+
process.exit(1);
203+
}
204+
205+
if (!packageName && !fromUrl) {
206+
logger.error('Package name or --from URL required. Usage: c8 load plugin <package-name> OR c8 load plugin --from <url>');
197207
process.exit(1);
198208
}
199-
loadPlugin(args[0]);
209+
210+
loadPlugin(packageName, fromUrl);
200211
return;
201212
}
202213

tests/fixtures/plugins/README.md

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ TypeScript implementation demonstrating:
1010
- TypeScript type annotations
1111
- Access to c8ctl runtime object
1212
- Multiple command exports (analyze, validate)
13-
- Metadata export
1413

1514
**Usage in plugins:**
1615
```typescript
@@ -29,7 +28,6 @@ JavaScript implementation demonstrating:
2928
- Command with hyphens ('deploy-all')
3029
- Argument handling
3130
- Multiple commands (deploy-all, status, report)
32-
- Metadata with command list
3331

3432
**Usage in plugins:**
3533
```javascript
@@ -44,16 +42,15 @@ export const commands = {
4442

4543
For a package to be recognized as a c8ctl plugin:
4644

47-
1. Must have either `c8ctl-plugin.js` or `c8ctl-plugin.ts` as main entry
48-
2. Must export a `commands` object with async functions
49-
3. Should export `metadata` with name, version, description
45+
1. Must be a regular Node.js module with proper package.json
46+
2. Must have either `c8ctl-plugin.js` or `c8ctl-plugin.ts` file in root directory
47+
3. Must export a `commands` object with async functions
5048
4. Can access c8ctl runtime via `import { c8ctl } from 'c8ctl/runtime'`
5149

5250
## Testing
5351

5452
These fixtures are used by `tests/unit/plugins.test.ts` to verify:
5553
- Plugin structure validation
5654
- Command export format
57-
- Metadata format
5855
- Dynamic import capability
5956
- Runtime object access

tests/fixtures/plugins/c8ctl-plugin.js

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,3 @@ export const commands = {
3737
console.log(`Output format: ${format}`);
3838
},
3939
};
40-
41-
export const metadata = {
42-
name: 'sample-js-plugin',
43-
version: '2.0.0',
44-
description: 'A sample JavaScript plugin for c8ctl',
45-
commands: ['deploy-all', 'status', 'report'],
46-
};

tests/fixtures/plugins/c8ctl-plugin.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,3 @@ export const commands = {
2929
console.log(`Validating files: ${args.join(', ')}`);
3030
},
3131
};
32-
33-
export const metadata = {
34-
name: 'sample-ts-plugin',
35-
version: '1.0.0',
36-
description: 'A sample TypeScript plugin for c8ctl',
37-
};

tests/unit/plugins.test.ts

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -55,17 +55,6 @@ describe('Plugin Structure', () => {
5555
assert.ok(pluginContent.includes('c8ctl.env'), 'Plugin uses c8ctl.env');
5656
});
5757

58-
test('should have metadata export', async () => {
59-
const pluginPath = join(process.cwd(), 'tests/fixtures/plugins/c8ctl-plugin.ts');
60-
const pluginContent = readFileSync(pluginPath, 'utf-8');
61-
62-
// Verify plugin exports metadata
63-
assert.ok(pluginContent.includes('export const metadata'), 'Plugin exports metadata');
64-
assert.ok(pluginContent.includes('name:'), 'Metadata has name');
65-
assert.ok(pluginContent.includes('version:'), 'Metadata has version');
66-
assert.ok(pluginContent.includes('description:'), 'Metadata has description');
67-
});
68-
6958
test('should be valid TypeScript syntax', async () => {
7059
const pluginPath = join(process.cwd(), 'tests/fixtures/plugins/c8ctl-plugin.ts');
7160
const pluginContent = readFileSync(pluginPath, 'utf-8');
@@ -90,17 +79,6 @@ describe('Plugin Structure', () => {
9079
assert.ok(pluginContent.includes('report:'), 'Plugin has report command');
9180
});
9281

93-
test('should have metadata export', async () => {
94-
const pluginPath = join(process.cwd(), 'tests/fixtures/plugins/c8ctl-plugin.js');
95-
const pluginContent = readFileSync(pluginPath, 'utf-8');
96-
97-
// Verify plugin exports metadata
98-
assert.ok(pluginContent.includes('export const metadata'), 'Plugin exports metadata');
99-
assert.ok(pluginContent.includes('name:'), 'Metadata has name');
100-
assert.ok(pluginContent.includes('version:'), 'Metadata has version');
101-
assert.ok(pluginContent.includes('commands:'), 'Metadata lists commands');
102-
});
103-
10482
test('should use ES6 module syntax', async () => {
10583
const pluginPath = join(process.cwd(), 'tests/fixtures/plugins/c8ctl-plugin.js');
10684
const pluginContent = readFileSync(pluginPath, 'utf-8');
@@ -130,7 +108,6 @@ describe('Plugin Structure', () => {
130108
assert.ok(plugin.commands, 'Plugin has commands export');
131109
assert.ok(typeof plugin.commands.analyze === 'function', 'analyze is a function');
132110
assert.ok(typeof plugin.commands.validate === 'function', 'validate is a function');
133-
assert.ok(plugin.metadata, 'Plugin has metadata export');
134111
} catch (error) {
135112
// If import fails, just verify the file exists
136113
const pluginPath = join(process.cwd(), 'tests/fixtures/plugins/c8ctl-plugin.ts');
@@ -149,7 +126,6 @@ describe('Plugin Structure', () => {
149126
assert.ok(typeof plugin.commands['deploy-all'] === 'function', 'deploy-all is a function');
150127
assert.ok(typeof plugin.commands.status === 'function', 'status is a function');
151128
assert.ok(typeof plugin.commands.report === 'function', 'report is a function');
152-
assert.ok(plugin.metadata, 'Plugin has metadata export');
153129
} catch (error) {
154130
// If import fails, just verify the file exists
155131
const pluginPath = join(process.cwd(), 'tests/fixtures/plugins/c8ctl-plugin.js');

0 commit comments

Comments
 (0)