Skip to content

Commit 6758764

Browse files
Copilotvobugithub-advanced-security[bot]
authored
feat: move plugin system to global scope and add lifecycle management (#55)
* Initial plan * refactor: Move plugin system to global scope - Plugins now install to global user data directory instead of local node_modules - Removed dependency on local package.json for plugin loading - Updated all plugin commands (load/unload/sync/list) to use global directory - Updated plugin loader to scan global plugins directory - Updated integration tests to work with global plugin system Co-authored-by: vobu <6573426+vobu@users.noreply.github.com> * fix: Improve plugin name extraction from URLs - Extract actual package name from package.json in node_modules - Scan installed plugins to find the correct package name - Fall back to URL-based name extraction if needed Co-authored-by: vobu <6573426+vobu@users.noreply.github.com> * refactor: Address code review feedback - Simplify extractPackageNameFromUrl to scan node_modules first - Remove duplicate path logic for scoped packages - Rename packageName to scopedPackageName for clarity in listPlugins Co-authored-by: vobu <6573426+vobu@users.noreply.github.com> * refactor: Extract hasPluginFile helper and improve naming - Add hasPluginFile() helper to eliminate duplicate logic - Rename scopedPackageName to packageNameWithScope for clarity - Use consistent helper function across all plugin checks Co-authored-by: vobu <6573426+vobu@users.noreply.github.com> * wip: Potential fix for code scanning alert no. 3: Shell command built from environment values Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * feat: Add plugin lifecycle commands and scaffolding - Add upgrade/downgrade commands for plugin version management - Add init plugin command to scaffold TypeScript plugin templates - Create default-plugins directory with hello-world example plugin - Load default plugins automatically on startup - Expose c8ctl runtime properties directly for plugin compatibility - Update help documentation with new plugin commands Co-authored-by: vobu <6573426+vobu@users.noreply.github.com> * docs: Update README with new plugin lifecycle commands - Document init plugin command for scaffolding - Document upgrade/downgrade commands - Add section on plugin development - Mention default plugins Co-authored-by: vobu <6573426+vobu@users.noreply.github.com> * refactor: Simplify plugin scanning with functional approach - Extract helper functions for plugin validation and scanning - Replace nested if-else with functional forEach and filter - Improve code readability and maintainability - No functional changes, all tests pass Co-authored-by: vobu <6573426+vobu@users.noreply.github.com> * test: Add comprehensive plugin lifecycle integration test - Test full lifecycle: init, build, load, execute, help - Dogfoods c8ctl init plugin command - Verifies TypeScript compilation works - Tests plugin command execution and help integration - Fix tsconfig exclude to allow build to work Co-authored-by: vobu <6573426+vobu@users.noreply.github.com> * chore: Add test plugin directories to gitignore - Ignore c8ctl-test-* directories - Ignore test-plugin-temp directory - Remove accidentally committed test scaffold Co-authored-by: vobu <6573426+vobu@users.noreply.github.com> * wip: potential fix for code scanning alert no. 4: Shell command built from environment values Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * fix: Update bullet points to dashes in plugin API features output * docs: Improve README formatting and clarify plugin creation instructions * fix: Add upgrade, downgrade, and init commands to plugin management and shell completions * docs: Update plugin help and README to include macOS installation paths and clarify plugin registry locations * test: Add test for plugin command precedence - Verify that plugins cannot override built-in commands - Built-in commands always execute first - Plugin with conflicting command name is ignored - Test passes validating correct behavior Co-authored-by: vobu <6573426+vobu@users.noreply.github.com> * wip: potential fix for code scanning alert no. 5: Shell command built from environment values Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * build: Include default-plugins in distribution - Add copy-plugins script to copy default-plugins to dist/ during build - Update plugin-loader to check both dev and prod paths for default-plugins - Ensure default-plugins are bundled with transpiled JS in dist/ - Verified with npm pack --dry-run that plugins are included - Both dev mode (node src/index.ts) and built mode (node dist/index.js) work correctly Co-authored-by: vobu <6573426+vobu@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: vobu <6573426+vobu@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: Volker Buzek <dev@metalodge.com>
1 parent d03dd29 commit 6758764

File tree

18 files changed

+1368
-115
lines changed

18 files changed

+1368
-115
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,7 @@ dist
140140
# Vite logs files
141141
vite.config.js.timestamp-*
142142
vite.config.ts.timestamp-*
143+
144+
# Test plugin directories
145+
c8ctl-test-*
146+
test-plugin-temp

PLUGIN-HELP.md

Lines changed: 81 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,46 @@ This document describes how c8ctl plugins can provide help text that gets integr
66

77
When users load plugins, their commands automatically appear in the help text. Plugins can optionally provide descriptions for their commands to make the help more informative.
88

9+
## Global Plugin System
10+
11+
c8ctl uses a global plugin system where plugins are installed to a user-specific directory. This means:
12+
13+
- **No local package.json required**: Plugins work from any directory
14+
- **Global installation**: Plugins are installed to OS-specific directories:
15+
- **Linux**: `~/.config/c8ctl/plugins/node_modules`
16+
- **macOS**: `~/Library/Application Support/c8ctl/plugins/node_modules`
17+
- **Windows**: `%APPDATA%\c8ctl\plugins\node_modules`
18+
- **Plugin registry**: Tracked in `plugins.json` in the same parent directory
19+
- **Persistent across projects**: Once loaded, plugins are available everywhere
20+
- **Centralized management**: All plugins are managed through the c8ctl plugin registry
21+
- **Cannot override built-in commands**: Plugin commands are executed only if no built-in command matches
22+
23+
> **Note:** You can override the default data directory by setting the `C8CTL_DATA_DIR` environment variable.
24+
25+
## Plugin Registry
26+
27+
The plugin registry (`plugins.json`) maintains a list of all installed plugins with metadata:
28+
29+
```json
30+
{
31+
"plugins": [
32+
{
33+
"name": "my-plugin",
34+
"source": "my-plugin@1.0.0",
35+
"installedAt": "2024-01-15T10:30:00.000Z"
36+
}
37+
]
38+
}
39+
```
40+
41+
Registry locations by OS:
42+
- **Linux**: `~/.config/c8ctl/plugins.json`
43+
- **macOS**: `~/Library/Application Support/c8ctl/plugins.json`
44+
- **Windows**: `%APPDATA%\c8ctl\plugins.json`
45+
946
## How It Works
1047

11-
1. **Automatic Discovery**: When `c8ctl help` is invoked, it scans all loaded plugins
48+
1. **Automatic Discovery**: When `c8ctl help` is invoked, it scans all loaded plugins from the global plugins directory
1249
2. **Plugin Section**: If plugins are loaded, a "Plugin Commands" section appears at the bottom of the help text
1350
3. **Command Listing**: Each plugin command is listed with its optional description
1451

@@ -83,6 +120,7 @@ export const commands = {
83120
## Help Output Example
84121

85122
Without plugins loaded:
123+
86124
```
87125
c8ctl - Camunda 8 CLI v2.0.0
88126
@@ -95,6 +133,7 @@ Commands:
95133
```
96134

97135
With plugins loaded:
136+
98137
```
99138
c8ctl - Camunda 8 CLI v2.0.0
100139
@@ -121,6 +160,7 @@ The plugin loader ([src/plugin-loader.ts](src/plugin-loader.ts)) provides:
121160
- `getPluginCommandNames()`: Returns array of command names
122161
- `getPluginCommandsInfo()`: Returns detailed info including descriptions
123162
- Automatic metadata extraction during plugin loading
163+
- Scans the [global plugins directory](#global-plugin-system) for installed plugins
124164

125165
### Help Command
126166

@@ -150,25 +190,59 @@ interface PluginMetadata {
150190
2. **Keep descriptions concise**: Aim for one line (< 60 characters)
151191
3. **Use imperative verbs**: Start with action words (Analyze, Deploy, Check, etc.)
152192
4. **Match command names**: Ensure metadata command names match exported functions
153-
5. **TypeScript plugins**: The `c8ctl-plugin.js` entry point must be JavaScript. Node.js doesn't support type stripping in `node_modules`. Transpile TypeScript to JavaScript before publishing your plugin.
193+
5. **Use unique command names**: Plugin commands cannot override built-in commands (see [Command Precedence](#command-precedence))
194+
6. **TypeScript plugins**: The `c8ctl-plugin.js` entry point must be JavaScript. Node.js doesn't support type stripping in `node_modules`. Transpile TypeScript to JavaScript before publishing your plugin.
195+
196+
## Command Precedence
197+
198+
**Important:** Plugin commands cannot override built-in c8ctl commands. Built-in commands always take precedence.
199+
200+
When c8ctl processes a command, it follows this order:
201+
202+
1. Check for built-in commands (list, get, create, deploy, etc.)
203+
2. If no built-in command matches, check plugin commands
204+
3. Execute the matched command
205+
206+
### Example
207+
208+
If a plugin exports a command named `list`:
209+
210+
```javascript
211+
export const commands = {
212+
'list': async (args) => {
213+
console.log('This will NEVER execute');
214+
}
215+
};
216+
```
217+
218+
When users run `c8ctl list profiles`, the built-in `list` command will execute, not the plugin version.
219+
220+
### Recommendation
221+
222+
Choose descriptive, unique names for your plugin commands that don't conflict with built-in commands. For example:
223+
-`analyze-process`, `export-data`, `sync-resources`
224+
-`list`, `get`, `create`, `deploy`
154225

155226
## Testing
156227

157228
See [tests/unit/plugin-loader.test.ts](tests/unit/plugin-loader.test.ts) for unit tests that verify:
229+
158230
- `getPluginCommandsInfo()` returns correct structure
159231
- Help text includes plugin commands
160232
- Metadata is properly parsed
161233

162234
## Example Plugin Development Flow
163235

164236
1. Create plugin with commands:
237+
165238
```typescript
166239
export const commands = {
167240
myCommand: async () => { /* ... */ }
168241
};
169242
```
170243

171-
2. Add metadata for help:
244+
1. Add metadata for help:
245+
172246
```typescript
173247
export const metadata = {
174248
commands: {
@@ -179,12 +253,14 @@ export const metadata = {
179253
};
180254
```
181255

182-
3. Load plugin:
256+
1. Load plugin:
257+
183258
```bash
184259
c8ctl load plugin my-plugin
185260
```
186261

187-
4. Verify help includes your command:
262+
1. Verify help includes your command:
263+
188264
```bash
189265
c8ctl help
190266
```

README.md

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -306,9 +306,24 @@ Debug output is written to stderr with timestamps and won't interfere with norma
306306

307307
### Plugin Management
308308

309-
c8ctl supports a plugin system that allows extending the CLI with custom commands via npm packages. Plugins are tracked in a registry file (`~/.config/c8ctl/plugins.json` on Linux, similar locations on other platforms) for persistence across npm operations.
309+
c8ctl supports a global plugin system that allows extending the CLI with custom commands via npm packages. Plugins are installed globally to a user-specific directory and tracked in a registry file.
310+
311+
**Plugin Storage Locations:**
312+
313+
The plugin system uses OS-specific directories:
314+
315+
| OS | Plugins Directory | Registry File |
316+
|----|-------------------|---------------|
317+
| **Linux** | `~/.config/c8ctl/plugins/node_modules` | `~/.config/c8ctl/plugins.json` |
318+
| **macOS** | `~/Library/Application Support/c8ctl/plugins/node_modules` | `~/Library/Application Support/c8ctl/plugins.json` |
319+
| **Windows** | `%APPDATA%\c8ctl\plugins\node_modules` | `%APPDATA%\c8ctl\plugins.json` |
320+
321+
> **Note:** You can override the data directory with the `C8CTL_DATA_DIR` environment variable.
310322
311323
```bash
324+
# Create a new plugin from template
325+
c8ctl init plugin my-plugin
326+
312327
# Load a plugin from npm registry
313328
c8ctl load plugin <package-name>
314329

@@ -318,7 +333,14 @@ c8ctl load plugin --from file:///path/to/plugin
318333
c8ctl load plugin --from https://github.com/user/repo
319334
c8ctl load plugin --from git://github.com/user/repo.git
320335

321-
# Unload a plugin (wraps npm uninstall)
336+
# Upgrade a plugin to latest or specific version
337+
c8ctl upgrade plugin <package-name>
338+
c8ctl upgrade plugin <package-name> 1.2.3
339+
340+
# Downgrade a plugin to a specific version
341+
c8ctl downgrade plugin <package-name> 1.0.0
342+
343+
# Unload a plugin
322344
c8ctl unload plugin <package-name>
323345

324346
# List installed plugins (shows sync status)
@@ -333,21 +355,32 @@ c8ctl sync plugins
333355
c8ctl help
334356
```
335357

336-
**Plugin Registry:**
337-
- Plugins are tracked independently of `package.json` in a registry file
338-
- The registry serves as the source of truth (local precedence)
358+
**Global Plugin System:**
359+
- Plugins are installed to a global directory (OS-specific, see table above)
360+
- Plugin registry file (`plugins.json`) tracks all installed plugins
361+
- No local `package.json` is required in your working directory
362+
- Plugins are available globally from any directory
363+
- The registry serves as the source of truth for installed plugins
364+
- Default plugins are bundled with c8ctl and loaded automatically
365+
- **Plugin commands cannot override built-in commands** - built-in commands always take precedence
339366
- `c8ctl list plugins` shows sync status:
340367
- `✓ Installed` - Plugin is in registry and installed
341-
- `⚠ Not installed` - Plugin is in registry but not in node_modules (run `sync`)
342-
- `⚠ Not in registry` - Plugin is in package.json but not tracked in registry
368+
- `⚠ Not installed` - Plugin is in registry but not in global directory (run `sync`)
369+
- `⚠ Not in registry` - Plugin is installed but not tracked in registry
343370
- `c8ctl sync plugins` synchronizes plugins from the registry, rebuilding or reinstalling as needed
344371

372+
**Plugin Development:**
373+
- Use `c8ctl init plugin <name>` to scaffold a new plugin with TypeScript template
374+
- Generated scaffold includes all necessary files and build configuration
375+
- Plugins have access to the c8ctl runtime via `globalThis.c8ctl`
376+
- See the bundled `hello-world` plugin in `default-plugins/` for a complete example
377+
345378
**Plugin Requirements:**
346379
- Plugin packages must be regular Node.js modules
347380
- They must include a `c8ctl-plugin.js` or `c8ctl-plugin.ts` file in the root directory
348381
- The plugin file must export a `commands` object
349382
- Optionally export a `metadata` object to provide help text
350-
- Plugins are installed in `node_modules` like regular npm packages
383+
- Plugins are installed globally and work from any directory
351384
- The runtime object `c8ctl` provides environment information to plugins
352385
- **Important**: `c8ctl-plugin.js` must be JavaScript. Node.js doesn't support type stripping in `node_modules`. If writing in TypeScript, transpile to JS before publishing.
353386

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# c8ctl-plugin-hello-world
2+
3+
A default "hello world" plugin for c8ctl that demonstrates the complete plugin API and lifecycle.
4+
5+
## Purpose
6+
7+
This plugin serves multiple purposes:
8+
9+
1. **Example**: Shows how to create a fully compliant c8ctl plugin
10+
2. **Documentation**: Demonstrates all plugin API features
11+
3. **Testing**: Validates the plugin system is working correctly
12+
4. **Template**: Provides a reference implementation for new plugins
13+
14+
## Features
15+
16+
- ✅ Complete metadata for help integration
17+
- ✅ Command that displays runtime information
18+
- ✅ Argument handling demonstration
19+
- ✅ Compliant with plugin API requirements
20+
- ✅ Loaded by default as an essential plugin
21+
22+
## Usage
23+
24+
This plugin is loaded automatically with c8ctl. Try it:
25+
26+
```bash
27+
c8ctl hello-world
28+
c8ctl hello-world arg1 arg2 arg3
29+
```
30+
31+
## Plugin Structure
32+
33+
```
34+
hello-world/
35+
├── package.json # Package metadata with c8ctl keywords
36+
├── c8ctl-plugin.js # Plugin implementation (ES6 module)
37+
└── README.md # This file
38+
```
39+
40+
## Requirements
41+
42+
- Package must have `c8ctl` or `c8ctl-plugin` in keywords
43+
- Must export `commands` object with command functions
44+
- Optionally export `metadata` object for help text
45+
- Entry point must be `c8ctl-plugin.js` (or .ts)
46+
47+
## Plugin API
48+
49+
### Storage Locations
50+
51+
Plugins are stored in OS-specific directories:
52+
53+
| OS | Plugin Directory | Registry File |
54+
|----|-----------------|---------------|
55+
| **Linux** | `~/.config/c8ctl/plugins/node_modules` | `~/.config/c8ctl/plugins.json` |
56+
| **macOS** | `~/Library/Application Support/c8ctl/plugins/node_modules` | `~/Library/Application Support/c8ctl/plugins.json` |
57+
| **Windows** | `%APPDATA%\c8ctl\plugins\node_modules` | `%APPDATA%\c8ctl\plugins.json` |
58+
59+
> **Note:** Override with `C8CTL_DATA_DIR` environment variable if needed.
60+
61+
### Runtime Access
62+
63+
The plugin can access c8ctl runtime via `globalThis.c8ctl`:
64+
65+
```javascript
66+
globalThis.c8ctl = {
67+
version: string; // c8ctl version
68+
nodeVersion: string; // Node.js version
69+
platform: string; // OS platform
70+
arch: string; // CPU architecture
71+
cwd: string; // Current working directory
72+
outputMode: 'text' | 'json'; // Output format
73+
activeProfile?: string; // Active connection profile
74+
activeTenant?: string; // Active tenant
75+
};
76+
```
77+
78+
### Command Implementation
79+
80+
Commands receive arguments and return promises:
81+
82+
```javascript
83+
export const commands = {
84+
'command-name': async (args) => {
85+
// args is an array of strings
86+
// Use console.log for output
87+
// Return a promise or async function
88+
},
89+
};
90+
```
91+
92+
### Metadata
93+
94+
Provide help text via metadata export:
95+
96+
```javascript
97+
export const metadata = {
98+
name: 'plugin-name',
99+
description: 'Plugin description',
100+
commands: {
101+
'command-name': {
102+
description: 'Command description shown in help',
103+
},
104+
},
105+
};
106+
```
107+
108+
## Important Limitations
109+
110+
### Command Precedence
111+
112+
**Plugin commands cannot override built-in commands.** The c8ctl CLI always checks for built-in commands first. If a plugin exports a command with the same name as a built-in command (e.g., `list`, `get`, `create`), the built-in command will always execute, and the plugin command will be ignored.
113+
114+
For example, if a plugin tries to export a `list` command:
115+
116+
```javascript
117+
export const commands = {
118+
'list': async (args) => {
119+
console.log('This will NEVER execute');
120+
}
121+
};
122+
```
123+
124+
When users run `c8ctl list`, the built-in `list` command will execute instead. Choose unique command names for your plugins to avoid conflicts.
125+
126+
## Development
127+
128+
To create a similar plugin:
129+
130+
1. Use the scaffolding command:
131+
132+
```bash
133+
c8ctl init plugin my-plugin
134+
```
135+
136+
2. Or manually create the structure following this example
137+
138+
3. Test locally:
139+
140+
```bash
141+
c8ctl load plugin --from file:///path/to/plugin
142+
```
143+
144+
4. Publish to npm:
145+
146+
```bash
147+
npm publish
148+
```
149+
150+
5. Users can install:
151+
152+
```bash
153+
c8ctl load plugin your-plugin-name
154+
```
155+
156+
## License
157+
158+
You decide, but our [own Drink-ware](/LICENSE.md) is of course the most fitting :)

0 commit comments

Comments
 (0)