merge: preserve tested Copilot install sources 2960 and 2963

This commit is contained in:
haelyra
2026-10-09 16:09:01 -04:00
15 changed files with 580 additions and 28 deletions
+1
View File
@@ -39,6 +39,7 @@ Targets:
cursor - Install rules, hooks, and bundled Cursor configs to ./.cursor/
antigravity - Install rules, workflows, skills, and agents to ./.agents/
codex - Install shared agents/config into ~/.codex/
copilot - Install skills and agents into ./.github/ for GitHub Copilot CLI (ECC hooks not configured)
gemini - Install project-local Gemini config into ./.gemini/
opencode - Install into OPENCODE_CONFIG_DIR, XDG_CONFIG_HOME/opencode, or ~/.config/opencode/
codebuddy - Install commands, agents, skills, and flattened rules into ./.codebuddy/
+17
View File
@@ -114,6 +114,23 @@ const HARNESS_CAPABILITIES = deepFreeze([
hooks: hooks('not-configured', false, 'ECC hooks are not configured by this adapter.'),
aliases: ['google-antigravity'],
},
{
id: 'copilot',
label: 'GitHub Copilot',
targetIds: ['copilot'],
channel: 'managed-project',
installMode: 'managed-project',
guidedReady: false,
availability: 'advanced',
destination: './.github',
scopes: [scope('project', 'copilot', './.github')],
hooks: hooks(
'not-configured',
false,
'ECC hooks target Claude Code events and are not installed for Copilot.'
),
aliases: ['github-copilot', 'copilot-cli'],
},
{
id: 'gemini',
label: 'Gemini CLI',
+10 -6
View File
@@ -42,6 +42,7 @@ const {
validateManagedHooks,
} = require('./install/claude-settings');
const { adaptAntigravityAgent } = require('./install/antigravity-agent');
const { adaptCopilotAgent } = require('./install/copilot-agent');
const { buildInstallIndex, rewriteRelativeLinks } = require('./install/link-rewrite');
const { getInstallTargetAdapter, listInstallTargetAdapters } = require('./install-targets/registry');
const { resolveInvocationEnvironment } = require('./invocation-environment');
@@ -245,6 +246,9 @@ function transformCopyFileContent(operation, content) {
if (operation.contentTransform === 'opencode-disable-plugin-entrypoint') {
return getDisabledOpenCodePluginContent();
}
if (operation.contentTransform === 'copilot-agent-frontmatter') {
return adaptCopilotAgent(content, operation.sourceRelativePath);
}
throw new Error(`Unknown install content transform: ${operation.contentTransform}`);
}
@@ -1919,9 +1923,11 @@ function prepareRepairMigration(plan, record) {
statePreview: buildAdapterDerivedStatePreview(plan.statePreview, record),
};
const initialMigration = prepareClaudeSkillMigration(trustedPlan);
const guardedMigration = record.adapter.id === 'codex-home'
? prepareUserOwnedFileGuard(trustedPlan, initialMigration)
: initialMigration;
// Repair normalizes recorded target metadata to the adapter-derived roots.
// Preserve its recorded operations as the ownership evidence.
const guardedMigration = prepareUserOwnedFileGuard(
trustedPlan, initialMigration, buildAdapterDerivedStatePreview(record.state, record)
);
const migration = trustedPlan.target === 'opencode'
? require('./install/apply').prepareHookConsentMigration(trustedPlan, guardedMigration)
: guardedMigration;
@@ -2240,9 +2246,7 @@ function repairInstalledStates(options = {}) {
const { assertOpenCodeActivationUnchanged } = require('./install/apply');
assertOpenCodeActivationUnchanged(desiredPlan, operation, activationSnapshot);
}
if (record.adapter.id === 'codex-home') {
assertNoNewUserOwnedFile(migration, operation, desiredPlan);
}
assertNoNewUserOwnedFile(migration, operation, desiredPlan);
const repairedPath = executeRepairOperation(
context.repoRoot,
operation,
+1 -1
View File
@@ -5,7 +5,7 @@ const { getInstallTargetAdapter, planInstallTargetScaffold } = require('./instal
const { resolveInvocationEnvironment } = require('./invocation-environment');
const DEFAULT_REPO_ROOT = path.join(__dirname, '../..');
const SUPPORTED_INSTALL_TARGETS = ['claude', 'claude-project', 'cursor', 'antigravity', 'codex', 'gemini', 'opencode', 'codebuddy', 'joycode', 'qwen', 'zed', 'hermes', 'openclaw', 'kimi', 'adal'];
const SUPPORTED_INSTALL_TARGETS = ['claude', 'claude-project', 'cursor', 'antigravity', 'codex', 'copilot', 'gemini', 'opencode', 'codebuddy', 'joycode', 'qwen', 'zed', 'hermes', 'openclaw', 'kimi', 'adal'];
const COMPONENT_FAMILY_PREFIXES = {
baseline: 'baseline:',
language: 'lang:',
@@ -0,0 +1,97 @@
const path = require('path');
const {
createInstallTargetAdapter,
createManagedOperation,
createManagedScaffoldOperation,
normalizeRelativePath,
} = require('./helpers');
// GitHub Copilot CLI discovers skills from `.github/skills/` and custom agents
// from `.github/agents/`. Those two directories are the entire supported
// surface for this target: ECC's hooks target Claude Code's event model, and
// commands rely on slash-command argument substitution Copilot does not
// provide, so neither is installed here.
const SUPPORTED_SOURCE_PREFIXES = ['agents', 'skills'];
function hasPrefix(normalizedPath, prefix) {
return normalizedPath === prefix || normalizedPath.startsWith(`${prefix}/`);
}
function supportsCopilotSourcePath(sourceRelativePath) {
const normalizedPath = normalizeRelativePath(sourceRelativePath);
if (normalizedPath.split('/').includes('..')) return false;
return SUPPORTED_SOURCE_PREFIXES.some(prefix => hasPrefix(normalizedPath, prefix));
}
function stripPrefix(normalizedPath, prefix) {
return normalizedPath === prefix ? '' : normalizedPath.slice(`${prefix}/`.length);
}
function planSourcePathOperations(module, sourceRelativePath, targetRoot) {
const normalizedSourcePath = normalizeRelativePath(sourceRelativePath);
if (hasPrefix(normalizedSourcePath, 'agents')) {
return [
createManagedOperation({
moduleId: module.id,
sourceRelativePath: normalizedSourcePath,
destinationPath: path.join(
targetRoot,
'agents',
stripPrefix(normalizedSourcePath, 'agents')
),
strategy: 'preserve-relative-path',
contentTransform: 'copilot-agent-frontmatter',
}),
];
}
if (hasPrefix(normalizedSourcePath, 'skills')) {
return [
createManagedScaffoldOperation(
module.id,
normalizedSourcePath,
path.join(targetRoot, 'skills', stripPrefix(normalizedSourcePath, 'skills')),
'preserve-relative-path'
),
];
}
return [];
}
module.exports = createInstallTargetAdapter({
id: 'copilot-project',
target: 'copilot',
kind: 'project',
rootSegments: ['.github'],
installStatePathSegments: ['ecc-install-state.json'],
supportsModule(module) {
// Selection gating stays permissive so modules that only act as dependency
// anchors (rules-core, commands-core, platform-configs) still resolve.
// planOperations() is what narrows the install to agents and skills, so an
// unsupported path contributes zero operations rather than skipping the
// module and every module that depends on it.
const paths = Array.isArray(module && module.paths) ? module.paths : [];
return paths.length > 0;
},
planOperations(input, adapter) {
const modules = Array.isArray(input.modules)
? input.modules
: (input.module ? [input.module] : []);
const { repoRoot, projectRoot, homeDir } = input;
const targetRoot = adapter.resolveRoot({ repoRoot, projectRoot, homeDir });
return modules.flatMap(module => {
const paths = Array.isArray(module.paths) ? module.paths : [];
return paths
.filter(supportsCopilotSourcePath)
.flatMap(sourceRelativePath => planSourcePathOperations(
module,
sourceRelativePath,
targetRoot
));
});
},
});
+2
View File
@@ -4,6 +4,7 @@ const claudeHome = require('./claude-home');
const claudeProject = require('./claude-project');
const codebuddyProject = require('./codebuddy-project');
const codexHome = require('./codex-home');
const copilotProject = require('./copilot-project');
const cursorProject = require('./cursor-project');
const geminiProject = require('./gemini-project');
const hermesHome = require('./hermes-home');
@@ -21,6 +22,7 @@ const ADAPTERS = Object.freeze([
cursorProject,
antigravityProject,
codexHome,
copilotProject,
geminiProject,
hermesHome,
opencodeHome,
+4
View File
@@ -52,6 +52,7 @@ const {
} = require('./excluded-paths-reconciliation');
const { buildInstallIndex, rewriteRelativeLinks } = require('./link-rewrite');
const { adaptAntigravityAgent } = require('./antigravity-agent');
const { adaptCopilotAgent } = require('./copilot-agent');
function isMarkdownPath(filePath) {
return /\.(md|mdx|markdown)$/i.test(String(filePath || ''));
@@ -70,6 +71,9 @@ function transformInstallContent(operation, content) {
if (operation.contentTransform === 'opencode-disable-plugin-entrypoint') {
return getDisabledOpenCodePluginContent();
}
if (operation.contentTransform === 'copilot-agent-frontmatter') {
return adaptCopilotAgent(content, operation.sourceRelativePath);
}
throw new Error(`Unknown install content transform: ${operation.contentTransform}`);
}
+70
View File
@@ -0,0 +1,70 @@
'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,
};
+4 -4
View File
@@ -37,10 +37,10 @@ function comparablePath(filePath) {
*
* All managed targets share this ownership boundary (#2964).
*/
function prepareUserOwnedFileGuard(plan, migration) {
const previousState = pathExists(plan.installStatePath)
? readInstallState(plan.installStatePath)
: null;
function prepareUserOwnedFileGuard(plan, migration, recordedState) {
const previousState = recordedState === undefined
? (pathExists(plan.installStatePath) ? readInstallState(plan.installStatePath) : null)
: recordedState;
if (previousState && (
previousState.target.id !== plan.adapter.id
|| comparablePath(previousState.target.root) !== comparablePath(plan.targetRoot)