Files
ECC/scripts/lib/install/copilot-agent.js
T
Brian SwigerandCopilot d58441072d feat(install): add a copilot project target for skills and agents
GitHub Copilot CLI discovers SKILL.md skills from .github/skills/ and custom
agents from .github/agents/, so ECC's catalog can be installed there without a
conversion step. This adds a managed-project install target that places both.

Scope is deliberately limited to agents and skills. ECC hooks are written
against Claude Code's event model, and commands rely on slash-command argument
substitution Copilot does not provide, so neither is installed.

Agent frontmatter goes through an allowlist transform rather than a passthrough:
Copilot resolves the session model from user configuration, so a Claude model id
makes it warn and fall back on every invocation; Claude tool names are not
Copilot tool names, and Copilot governs tool access per session via
--allow-tool/--deny-tool. Unknown keys added to ECC agents later therefore
cannot leak into the Copilot copy.

Target selection stays permissive in supportsModule so dependency-anchor modules
still resolve; planOperations is what narrows the install, matching the
convention used by the other adapters.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-09-04 20:58:17 +01:00

71 lines
2.7 KiB
JavaScript

'use strict';
// GitHub Copilot CLI custom agents are markdown files with `name` and
// `description` frontmatter. Claude Code's agent files additionally carry
// `model`, `tools`, and `color`, none of which transfer cleanly:
//
// model Copilot CLI resolves the session model from user configuration and
// plan entitlement. Carrying a Claude model id ("opus", "sonnet")
// makes Copilot emit "specifies model ... which is not available" on
// every invocation and silently fall back. Dropping the key selects
// the session default with no warning.
// tools Claude tool names (Read, Grep, Bash, ...) are not Copilot CLI tool
// names. Copilot governs tool access at the session level through
// --allow-tool/--deny-tool, so carrying the Claude list would assert
// a restriction Copilot does not actually apply.
// color Claude Code presentation only.
//
// The transform is therefore an allowlist rather than a denylist: unknown keys
// added to ECC agents later cannot silently leak into the Copilot copy.
const SUPPORTED_FRONTMATTER_KEYS = Object.freeze(['name', 'description']);
const REQUIRED_FRONTMATTER_KEYS = Object.freeze(['name', 'description']);
function splitFrontmatter(source, label) {
const match = String(source || '').match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
if (!match) {
throw new Error(`Cannot adapt Copilot agent ${label}: missing YAML frontmatter`);
}
// Keep YAML loading behind the transform boundary. Public help commands load
// the installer graph without executing a transform, including in hermetic
// packed-artifact checks where runtime dependencies are intentionally absent.
const frontmatter = require('js-yaml').load(match[1]);
if (!frontmatter || typeof frontmatter !== 'object' || Array.isArray(frontmatter)) {
throw new Error(`Cannot adapt Copilot agent ${label}: frontmatter must be an object`);
}
return {
frontmatter,
body: source.slice(match[0].length),
};
}
function adaptCopilotAgent(source, label = '<unknown>') {
const { frontmatter, body } = splitFrontmatter(source, label);
for (const key of REQUIRED_FRONTMATTER_KEYS) {
const value = frontmatter[key];
if (typeof value !== 'string' || value.trim() === '') {
throw new Error(`Cannot adapt Copilot agent ${label}: missing required frontmatter "${key}"`);
}
}
const adapted = {};
for (const key of SUPPORTED_FRONTMATTER_KEYS) {
if (Object.hasOwn(frontmatter, key)) {
adapted[key] = frontmatter[key];
}
}
const serialized = require('js-yaml')
.dump(adapted, { lineWidth: -1, noRefs: true })
.trimEnd();
return `---\n${serialized}\n---\n${body}`;
}
module.exports = {
adaptCopilotAgent,
SUPPORTED_FRONTMATTER_KEYS,
};