Skip to content

Commit 2ec1287

Browse files
committed
feat: enhance plugin scaffolding
1 parent 7bf6d51 commit 2ec1287

File tree

14 files changed

+313
-168
lines changed

14 files changed

+313
-168
lines changed

EXAMPLES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -908,6 +908,8 @@ c8 upgrade plugin my-custom-plugin 1.2.3
908908

909909
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:
910910

911+
When bootstrapping with `c8 init plugin <name>`, the generated project also includes an `AGENTS.md` file with an implementation workflow and runtime API reference for coding agents.
912+
911913
```typescript
912914
// c8ctl-plugin.ts
913915
import { c8ctl } from 'c8ctl/runtime';

PLUGIN-HELP.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,19 @@ See [tests/unit/plugin-loader.test.ts](tests/unit/plugin-loader.test.ts) for uni
273273
- Help text includes plugin commands
274274
- Metadata is properly parsed
275275

276+
## AGENTS.md in Scaffolded Plugins
277+
278+
When you bootstrap a plugin with `c8ctl init plugin <name>`, the generated project includes an `AGENTS.md` file.
279+
280+
Treat this file as the default implementation contract for coding agents and contributors. It captures:
281+
282+
- plugin contract expectations (`commands`, optional `metadata`, keywords)
283+
- available runtime APIs on global `c8ctl`
284+
- a fast local development loop (`install``build``load``help``run`)
285+
- minimal quality checks before considering work complete
286+
287+
Keeping `AGENTS.md` aligned with your plugin design helps autonomous contributors make correct, minimal, and testable changes.
288+
276289
## Example Plugin Development Flow
277290

278291
1. Create plugin with commands:

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ c8ctl help
394394

395395
**Plugin Development:**
396396
- Use `c8ctl init plugin <name>` to scaffold a new plugin with TypeScript template
397-
- Generated scaffold includes all necessary files and build configuration
397+
- Generated scaffold includes all necessary files, build configuration, and an `AGENTS.md` guide for autonomous plugin implementation
398398
- Plugins have access to the c8ctl runtime via `globalThis.c8ctl`
399399
- Plugins can create SDK clients via `globalThis.c8ctl.createClient(profile?, sdkConfig?)`
400400
- Plugins can resolve tenant IDs via `globalThis.c8ctl.resolveTenantId(profile?)`

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@
1919
"LICENSE"
2020
],
2121
"scripts": {
22-
"build": "npm run clean && tsc && npm run copy-plugins",
22+
"build": "npm run clean && tsc && npm run copy-plugins && npm run copy-templates",
2323
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
2424
"copy-plugins": "node -e \"const fs=require('fs');const path=require('path');const src='default-plugins';const dest='dist/default-plugins';if(fs.existsSync(src)){fs.cpSync(src,dest,{recursive:true})}\"",
25+
"copy-templates": "node -e \"const fs=require('fs');const src='src/templates';const dest='dist/templates';if(fs.existsSync(src)){fs.cpSync(src,dest,{recursive:true})}\"",
2526
"prepublishOnly": "npm run build",
2627
"test": "node --test tests/unit/setup.test.ts tests/unit/*.test.ts tests/integration/*.test.ts",
2728
"test:unit": "node --test tests/unit/setup.test.ts tests/unit/*.test.ts",

src/commands/plugins.ts

Lines changed: 41 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,34 @@
55
import { getLogger } from '../logger.ts';
66
import { execSync } from 'node:child_process';
77
import { readFileSync, existsSync, readdirSync } from 'node:fs';
8-
import { join } from 'node:path';
8+
import { join, dirname } from 'node:path';
9+
import { fileURLToPath } from 'node:url';
910
import { clearLoadedPlugins } from '../plugin-loader.ts';
1011
import { ensurePluginsDir } from '../config.ts';
1112
import {
1213
addPluginToRegistry,
1314
removePluginFromRegistry,
1415
getRegisteredPlugins,
1516
isPluginRegistered,
16-
getPluginEntry,
17-
type PluginEntry,
17+
getPluginEntry
1818
} from '../plugin-registry.ts';
1919

20+
const __filename = fileURLToPath(import.meta.url);
21+
const __dirname = dirname(__filename);
22+
23+
function getTemplate(templateFileName: string): string {
24+
const templatePath = join(__dirname, '..', 'templates', templateFileName);
25+
return readFileSync(templatePath, 'utf-8');
26+
}
27+
28+
function renderTemplate(templateFileName: string, replacements: Record<string, string> = {}): string {
29+
let content = getTemplate(templateFileName);
30+
for (const [key, value] of Object.entries(replacements)) {
31+
content = content.replaceAll(`{{${key}}}`, value);
32+
}
33+
return content;
34+
}
35+
2036
/**
2137
* Load a plugin (npm install wrapper)
2238
* Supports either package name or --from flag with URL
@@ -625,191 +641,51 @@ export async function initPlugin(pluginName?: string): Promise<void> {
625641

626642
try {
627643
logger.info(`Creating plugin: ${dirName}...`);
644+
645+
const templateVars = { PLUGIN_NAME: dirName };
628646

629647
// Create plugin directory
630648
mkdirSync(pluginDir, { recursive: true });
631649

632650
// Create package.json
633-
const packageJson = {
634-
name: dirName,
635-
version: '1.0.0',
636-
type: 'module',
637-
description: `A c8ctl plugin`,
638-
keywords: ['c8ctl', 'c8ctl-plugin'],
639-
main: 'c8ctl-plugin.js',
640-
scripts: {
641-
build: 'tsc',
642-
watch: 'tsc --watch',
643-
},
644-
devDependencies: {
645-
typescript: '^5.0.0',
646-
'@types/node': '^22.0.0',
647-
},
648-
};
649-
650-
writeFileSync(
651-
join(pluginDir, 'package.json'),
652-
JSON.stringify(packageJson, null, 2)
653-
);
654-
651+
writeFileSync(join(pluginDir, 'package.json'), renderTemplate('package.json', templateVars));
652+
655653
// Create tsconfig.json
656-
const tsConfig = {
657-
compilerOptions: {
658-
target: 'ES2022',
659-
module: 'ES2022',
660-
moduleResolution: 'node',
661-
outDir: '.',
662-
rootDir: './src',
663-
strict: true,
664-
esModuleInterop: true,
665-
skipLibCheck: true,
666-
forceConsistentCasingInFileNames: true,
667-
},
668-
include: ['src/**/*'],
669-
exclude: ['node_modules'],
670-
};
671-
672-
writeFileSync(
673-
join(pluginDir, 'tsconfig.json'),
674-
JSON.stringify(tsConfig, null, 2)
675-
);
654+
writeFileSync(join(pluginDir, 'tsconfig.json'), getTemplate('tsconfig.json'));
676655

677656
// Create src directory
678657
mkdirSync(join(pluginDir, 'src'), { recursive: true });
679-
680-
// Create c8ctl-plugin.ts
681-
const pluginTemplate = `/**
682-
* ${dirName} - A c8ctl plugin
683-
*/
684658

685-
// The c8ctl runtime is available globally
686-
declare const c8ctl: {
687-
version: string;
688-
nodeVersion: string;
689-
platform: string;
690-
arch: string;
691-
cwd: string;
692-
outputMode: 'text' | 'json';
693-
activeProfile?: string;
694-
activeTenant?: string;
695-
};
696-
697-
// Optional metadata for help text
698-
export const metadata = {
699-
name: '${dirName}',
700-
description: 'A c8ctl plugin',
701-
commands: {
702-
hello: {
703-
description: 'Say hello from the plugin',
704-
},
705-
},
706-
};
707-
708-
// Required commands export
709-
export const commands = {
710-
hello: async (args: string[]) => {
711-
console.log('Hello from ${dirName}!');
712-
console.log('c8ctl version:', c8ctl.version);
713-
console.log('Node version:', c8ctl.nodeVersion);
714-
715-
if (args.length > 0) {
716-
console.log('Arguments:', args.join(', '));
717-
}
659+
// Create root c8ctl-plugin.js entry point
660+
writeFileSync(join(pluginDir, 'c8ctl-plugin.js'), getTemplate('c8ctl-plugin.js'));
718661

719-
// Example: Access c8ctl runtime
720-
console.log('Current directory:', c8ctl.cwd);
721-
console.log('Output mode:', c8ctl.outputMode);
662+
// Create c8ctl-plugin.ts
663+
writeFileSync(join(pluginDir, 'src', 'c8ctl-plugin.ts'), renderTemplate('c8ctl-plugin.ts', templateVars));
722664

723-
if (c8ctl.activeProfile) {
724-
console.log('Active profile:', c8ctl.activeProfile);
725-
}
726-
},
727-
};
728-
`;
665+
// Create README.md
666+
const readme = renderTemplate('README.md', templateVars);
729667

730668
writeFileSync(
731-
join(pluginDir, 'src', 'c8ctl-plugin.ts'),
732-
pluginTemplate
669+
join(pluginDir, 'README.md'),
670+
readme
733671
);
734-
735-
// Create README.md
736-
const readme = `# ${dirName}
737-
738-
A c8ctl plugin.
739-
740-
## Development
741-
742-
1. Install dependencies:
743-
\`\`\`bash
744-
npm install
745-
\`\`\`
746-
747-
2. Build the plugin:
748-
\`\`\`bash
749-
npm run build
750-
\`\`\`
751-
752-
3. Load the plugin for testing:
753-
\`\`\`bash
754-
c8ctl load plugin --from file://\${PWD}
755-
\`\`\`
756-
757-
4. Test the plugin command:
758-
\`\`\`bash
759-
c8ctl hello
760-
\`\`\`
761-
762-
## Plugin Structure
763-
764-
- \`src/c8ctl-plugin.ts\` - Plugin source code (TypeScript)
765-
- \`c8ctl-plugin.js\` - Compiled plugin file (JavaScript)
766-
- \`package.json\` - Package metadata with c8ctl keywords
767672

768-
## Publishing
673+
// Create AGENTS.md
674+
const agents = getTemplate('AGENTS.md');
769675

770-
Before publishing, ensure:
771-
- The plugin is built (\`npm run build\`)
772-
- The package.json has correct metadata
773-
- Keywords include 'c8ctl' or 'c8ctl-plugin'
774-
775-
Then publish to npm:
776-
\`\`\`bash
777-
npm publish
778-
\`\`\`
779-
780-
Users can install your plugin with:
781-
\`\`\`bash
782-
c8ctl load plugin ${dirName}
783-
\`\`\`
784-
`;
785-
786676
writeFileSync(
787-
join(pluginDir, 'README.md'),
788-
readme
677+
join(pluginDir, 'AGENTS.md'),
678+
agents
789679
);
790680

791681
// Create .gitignore
792-
const gitignore = `node_modules/
793-
*.js
794-
*.js.map
795-
!c8ctl-plugin.js
796-
`;
797-
798-
writeFileSync(
799-
join(pluginDir, '.gitignore'),
800-
gitignore
801-
);
682+
writeFileSync(join(pluginDir, '.gitignore'), getTemplate('.gitignore'));
802683

803684
logger.success('Plugin scaffolding created successfully!');
804-
logger.info('');
805-
logger.info(`Next steps:`);
806-
logger.info(` 1. cd ${dirName}`);
807-
logger.info(` 2. npm install`);
808-
logger.info(` 3. npm run build`);
809-
logger.info(` 4. c8ctl load plugin --from file://\${PWD}`);
810-
logger.info(` 5. c8ctl hello`);
811-
logger.info('');
812-
logger.info(`Edit src/c8ctl-plugin.ts to add your plugin logic.`);
685+
const nextSteps = renderTemplate('init-plugin-next-steps.txt', templateVars);
686+
for (const line of nextSteps.split('\n')) {
687+
logger.info(line);
688+
}
813689
} catch (error) {
814690
logger.error('Failed to create plugin', error as Error);
815691
process.exit(1);

src/templates/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules/
2+
*.js
3+
*.js.map
4+
!c8ctl-plugin.js

src/templates/AGENTS.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# AGENTS.md
2+
3+
This file describes how to efficiently implement and iterate on this c8ctl plugin as an autonomous coding agent.
4+
5+
## Goal
6+
7+
Build and maintain a plugin that exposes useful commands through `export const commands` in `src/c8ctl-plugin.ts`.
8+
9+
## Plugin Contract
10+
11+
- Plugin entry point: `c8ctl-plugin.js` (root re-export to `dist/c8ctl-plugin.js`)
12+
- Source file: `src/c8ctl-plugin.ts`
13+
- Build output: `dist/c8ctl-plugin.js`
14+
- Required export: `commands` object mapping command name -> async handler
15+
- Optional export: `metadata` object for help descriptions
16+
- Package keywords must include `c8ctl` or `c8ctl-plugin`
17+
18+
## Runtime API (available as global `c8ctl`)
19+
20+
Runtime environment object:
21+
22+
- `c8ctl.env.version`
23+
- `c8ctl.env.nodeVersion`
24+
- `c8ctl.env.platform`
25+
- `c8ctl.env.arch`
26+
- `c8ctl.env.cwd`
27+
- `c8ctl.env.rootDir`
28+
29+
Direct compatibility fields:
30+
31+
- `c8ctl.version`
32+
- `c8ctl.nodeVersion`
33+
- `c8ctl.platform`
34+
- `c8ctl.arch`
35+
- `c8ctl.cwd`
36+
- `c8ctl.outputMode`
37+
- `c8ctl.activeProfile`
38+
- `c8ctl.activeTenant`
39+
40+
Methods:
41+
42+
- `c8ctl.createClient(profileFlag?, sdkConfig?)`
43+
- `c8ctl.resolveTenantId(profileFlag?)`
44+
- `c8ctl.getLogger(mode?)`
45+
46+
## Development Loop
47+
48+
1. Install dependencies: `npm install`
49+
2. Build plugin: `npm run build`
50+
3. Load from local folder: `c8ctl load plugin --from file://${PWD}`
51+
4. Verify command is available: `c8ctl <plugin>`
52+
5. Execute plugin command: `c8ctl <plugin> <commands>`
53+
54+
## Implementation Guidance
55+
56+
- Keep command handlers focused and composable.
57+
- Use `c8ctl.getLogger()` for output-mode-aware logs.
58+
- Use `c8ctl.createClient()` for Camunda API interactions.
59+
- Use `c8ctl.resolveTenantId()` instead of duplicating tenant fallback logic.
60+
- Prefer clear, actionable error messages.
61+
- Avoid command names that conflict with built-in c8ctl commands.
62+
- For any non-trivial implementation or behavior change, always cross-check against upstream `c8ctl` repository: <https://github.com/camunda/c8ctl> before finalizing
63+
64+
## Quality Checks
65+
66+
Before considering a change complete:
67+
68+
1. Build succeeds: `npm run build`
69+
2. Plugin loads: `c8ctl load plugin --from file://${PWD}`
70+
3. Command appears in help: `c8ctl help`
71+
4. Command executes with expected output
72+
73+
## Minimal Change Policy
74+
75+
- Make the smallest change required for each task.
76+
- Do not add unrelated commands or refactors.
77+
- Keep `metadata.commands` descriptions concise and user-facing.

0 commit comments

Comments
 (0)