Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions plugins/headlamp-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,39 @@ headlamp-plugin --help
headlamp-plugin.js uninstall [pluginName] Uninstall the plugin.
```

## Scaffolding the Claude Code agent harness

`create` accepts an opt-in `--with-claude-skills` flag:

```
headlamp-plugin create my-plugin --with-claude-skills
```

In addition to the default scaffold, this adds a Claude Code agent harness for
building the plugin with an AI agent:

- `CLAUDE.md` — always-on agent policy for a Headlamp plugin (replaces the
default `AGENTS.md`).
- `.claude/skills/` — step-by-step skills for the CNCF/CRD plugin workflow
(`create-cncf-plugin`, `plan-plugin`, `define-resource`, `add-list-view`,
`add-detail-view`, `add-settings`, `ensure-dependency`, `seed-test-data`,
`run-and-verify`, `document-plugin`).
- `.claude/settings.json` — a permission allowlist for the common dev commands.
- `.mcp.json` — the `kubernetes`, `helm` and `chrome-devtools` MCP servers the
skills use.

Without the flag, `create` behaves exactly as before (default `AGENTS.md`, no
`.claude/` or `.mcp.json`).

To add the harness to an **existing** plugin, pass the same flag to `upgrade`:

```
headlamp-plugin upgrade --with-claude-skills
```

Existing harness files are left untouched, so it is safe to re-run; it only adds
what is missing and drops `AGENTS.md` once `CLAUDE.md` is present.

## Template for installing plugins from a configuration file

plugins.yaml:
Expand Down
81 changes: 77 additions & 4 deletions plugins/headlamp-plugin/bin/headlamp-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,40 @@ const vitePromise = import('vite');
* Copies the files within template, and modifies a couple.
* Then runs "npm ci" inside of the folder.
*
/**
* Adds the opt-in Claude Code agent harness to a plugin folder.
*
* Copies CLAUDE.md, .claude/ (skills + settings) and .mcp.json from the
* "template-claude" folder into dstFolder, and drops AGENTS.md since CLAUDE.md
* supersedes it as the single agent guide. Existing files are left untouched
* (overwrite: false), so it is safe to run against an already-scaffolded plugin.
*
* @param {string} dstFolder - plugin folder to add the harness to.
*/
function addClaudeHarness(dstFolder) {
const claudeTemplateFolder = path.resolve(__dirname, '..', 'template-claude');
console.log('Adding Claude Code agent skills (CLAUDE.md, .claude/, .mcp.json)');
fs.copySync(claudeTemplateFolder, dstFolder, {
overwrite: false,
errorOnExist: false,
});
const agentsPath = path.join(dstFolder, 'AGENTS.md');
if (fs.existsSync(agentsPath)) {
fs.removeSync(agentsPath);
}
}

/**
* @param {string} name - name of package and output folder.
* @param {boolean} link - if we link @kinvolk/headlamp-plugin for testing
* @param {boolean} noInstall - if we skip installing with "npm ci"
* @param {boolean} withClaudeSkills - if we also scaffold the Claude Code agent
* harness (CLAUDE.md, .claude/skills, .claude/settings.json, .mcp.json) from
* the "template-claude" folder. When set, the default template's AGENTS.md is
* dropped in favour of CLAUDE.md.
* @returns {0 | 1 | 2 | 3} Exit code, where 0 is success, 1, 2, and 3 are failures.
*/
function create(name, link, noInstall) {
function create(name, link, noInstall, withClaudeSkills) {
const dstFolder = name;
const templateFolder = path.resolve(__dirname, '..', 'template');
const indexPath = path.join(dstFolder, 'src', 'index.tsx');
Expand Down Expand Up @@ -98,6 +126,13 @@ function create(name, link, noInstall) {
replaceFileVariables(indexPath);
replaceFileVariables(readmePath);

// Opt-in Claude Code agent harness. Copied from a separate "template-claude"
// folder so the default scaffold stays untouched unless --with-claude-skills
// is passed.
if (withClaudeSkills) {
addClaudeHarness(dstFolder);
}

// This can be used to make testing locally easier.
if (link) {
console.log('Linking @kinvolk/headlamp-plugin');
Expand Down Expand Up @@ -945,9 +980,12 @@ function getNpmOutdated() {
* @param packageFolder {string} - folder where the package, or folder of packages is.
* @parm skipPackageUpdates {boolean} - do not upgrade packages if true.
* @param headlampPluginVersion {string} - tag or version of headlamp-plugin to upgrade to.
* @param withClaudeSkills {boolean} - if true, add the Claude Code agent harness
* (CLAUDE.md, .claude/, .mcp.json) to the package(s) being upgraded. Existing
* harness files are left untouched; AGENTS.md is dropped in favour of CLAUDE.md.
* @returns {0 | 1} Exit code, where 0 is success, 1 is failure.
*/
function upgrade(packageFolder, skipPackageUpdates, headlampPluginVersion) {
function upgrade(packageFolder, skipPackageUpdates, headlampPluginVersion, withClaudeSkills) {
/**
* Files from the template might not be there.
*
Expand All @@ -965,6 +1003,17 @@ function upgrade(packageFolder, skipPackageUpdates, headlampPluginVersion) {
'tsconfig.json',
'AGENTS.md',
];

// Plugins scaffolded with `create --with-claude-skills` use CLAUDE.md as the
// single agent guide instead of AGENTS.md, so don't reintroduce AGENTS.md on
// upgrade for them.
if (fs.existsSync('CLAUDE.md')) {
const agentsIndex = missingFiles.indexOf('AGENTS.md');
if (agentsIndex !== -1) {
missingFiles.splice(agentsIndex, 1);
}
}

const templateFolder = path.resolve(__dirname, '..', 'template');

missingFiles.forEach(pathToCheck => {
Expand Down Expand Up @@ -1194,6 +1243,12 @@ function upgrade(packageFolder, skipPackageUpdates, headlampPluginVersion) {
process.chdir(folder);
console.log(`Upgrading "${folder}"...`);

// Opt-in: add the Claude Code harness. Run before addMissingTemplateFiles so
// that CLAUDE.md exists and AGENTS.md is not (re)added for harness plugins.
if (withClaudeSkills) {
addClaudeHarness('.');
}

addMissingTemplateFiles();
addMissingConfiguration();
removeFiles();
Expand Down Expand Up @@ -1756,11 +1811,17 @@ yargs(process.argv.slice(2))
.option('noinstall', {
describe: 'Skip installing dependencies with npm ci',
type: 'boolean',
})
.option('with-claude-skills', {
describe:
'Also scaffold the Claude Code agent harness (CLAUDE.md, .claude/skills, ' +
'.claude/settings.json, .mcp.json). Replaces the default AGENTS.md with CLAUDE.md.',
type: 'boolean',
});
},
argv => {
// @ts-ignore
process.exitCode = create(argv.name, argv.link, argv.noinstall);
process.exitCode = create(argv.name, argv.link, argv.noinstall, argv['with-claude-skills']);
}
)
.command(
Expand Down Expand Up @@ -1978,11 +2039,23 @@ yargs(process.argv.slice(2))
describe:
'Use a specific headlamp-plugin-version when upgrading packages. Defaults to "latest".',
type: 'string',
})
.option('with-claude-skills', {
describe:
'Add the Claude Code agent harness (CLAUDE.md, .claude/skills, ' +
'.claude/settings.json, .mcp.json) to the upgraded plugin(s). Replaces AGENTS.md ' +
'with CLAUDE.md. Existing harness files are left untouched.',
type: 'boolean',
});
},
argv => {
// @ts-ignore
process.exitCode = upgrade(argv.package, argv.skipPackageUpdates, argv.headlampPluginVersion);
process.exitCode = upgrade(
argv.package,
argv.skipPackageUpdates,
argv.headlampPluginVersion,
argv['with-claude-skills']
);
}
)
.command(
Expand Down
1 change: 1 addition & 0 deletions plugins/headlamp-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@
"bin",
"config",
"template",
"template-claude",
"lib",
"types",
".storybook",
Expand Down
42 changes: 42 additions & 0 deletions plugins/headlamp-plugin/template-claude/.claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"permissions": {
"allow": [
"Skill",
"Bash(npm install:*)",
"Bash(npm start:*)",
"Bash(npm run tsc:*)",
"Bash(npm run lint:*)",
"Bash(npm run lint-fix:*)",
"Bash(npm run build:*)",
"Bash(npm run format:*)",
"Bash(npm run test:*)",
"Bash(npm run i18n:*)",
"Bash(npm run package:*)",
"Bash(npx @kinvolk/headlamp-plugin:*)",
"Bash(npx tsc:*)",
"Read(.claude/**)",
"Read(node_modules/@kinvolk/headlamp-plugin/**)",
"Read(/tmp/**)",
"Bash(echo:*)",
"Bash(ls:*)",
"Bash(cat:*)",
"Bash(grep:*)",
"Bash(find:*)",
"Bash(head:*)",
"Bash(tail:*)",
"Bash(wc:*)",
"Bash(which:*)",
"Bash(sleep:*)",
"Bash(mkdir:*)",
"Bash(cp:*)",
"Bash(kubectl get:*)",
"Bash(kubectl describe:*)",
"Bash(kubectl explain:*)",
"Bash(kubectl wait:*)",
"mcp__kubernetes__*",
"mcp__chrome-devtools__*"
],
"ask": [],
"deny": []
}
}
Loading
Loading