mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-18 22:13:45 +02:00
merge: preserve tested Copilot install sources 2960 and 2963
This commit is contained in:
@@ -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/
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
));
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user