fix(install): isolate Copilot helpers from ESM projects

This commit is contained in:
Yann Roberto committed 2026-10-30 18:31:48 +08:00
1 parent b38be6a7f6
commit c68afd7c35
2 files changed
+42 -3

No files matched your search

@@ -137,7 +137,7 @@ module.exports = createInstallTargetAdapter({
const { repoRoot, projectRoot, homeDir } = input;
const targetRoot = adapter.resolveRoot({ repoRoot, projectRoot, homeDir });
return modules.flatMap(module => {
const operations = modules.flatMap(module => {
const paths = Array.isArray(module.paths) ? module.paths : [];
return paths
.filter(supportsCopilotSourcePath)
@@ -148,5 +148,13 @@ module.exports = createInstallTargetAdapter({
repoRoot
));
});
const helper = operations.find(operation => COPILOT_HELPER_PATHS.has(operation.sourceRelativePath));
if (!helper) return operations;
// Keep managed helpers executable when the host project uses ES modules.
return [...operations, createManagedOperation({
moduleId: helper.moduleId,
sourceRelativePath: 'manifests/install-assets/commonjs-scripts-package.json',
destinationPath: path.join(targetRoot, 'ecc', 'scripts', 'package.json'),
})];
},
});
+33 -2
View File
@@ -137,6 +137,18 @@ test('skills are planned into .github/skills with installed helper references',
);
});
test('declared helpers share one managed CommonJS boundary owned by their module', () => {
const operations = planFor([{ id: 'commands-core', paths: [
'scripts/setup-package-manager.js', 'scripts/lib/package-manager.js'
] }]);
const boundaries = operations.filter(operation => operation.destinationPath.endsWith('package.json'));
assert.strictEqual(boundaries.length, 1);
assert.strictEqual(boundaries[0].sourceRelativePath, 'manifests/install-assets/commonjs-scripts-package.json');
assert.strictEqual(boundaries[0].destinationPath, path.join('/project', '.github', 'ecc', 'scripts', 'package.json'));
assert.strictEqual(boundaries[0].moduleId, 'commands-core');
assert.strictEqual(boundaries[0].ownership, 'managed');
});
test('rule transform converts paths frontmatter to applyTo', () => {
const source = [
'---',
@@ -350,7 +362,7 @@ test('repair and uninstall preserve user-owned agents across project adapters',
}
});
test('normal installed Copilot TDD command runs from the project with a complete managed helper closure', () => {
test('installed Copilot TDD command runs in an ESM project with a complete managed helper closure', () => {
const fs = require('fs');
const os = require('os');
const { spawnSync } = require('child_process');
@@ -363,6 +375,8 @@ test('normal installed Copilot TDD command runs from the project with a complete
fs.mkdirSync(path.join(project, 'scripts'));
fs.writeFileSync(path.join(project, 'scripts', 'setup-package-manager.js'), 'USER PROJECT SENTINEL\n');
fs.writeFileSync(path.join(project, 'pnpm-lock.yaml'), 'lockfileVersion: 9.0\n');
const projectPackage = '{"type":"module"}\n';
fs.writeFileSync(path.join(project, 'package.json'), projectPackage);
const plan = createManifestInstallPlan({ target: 'copilot', moduleIds: ['workflow-quality'], projectRoot: project, homeDir, env });
applyInstallPlan(plan);
const skill = fs.readFileSync(path.join(project, '.github', 'skills', 'tdd-workflow', 'SKILL.md'), 'utf8');
@@ -396,29 +410,46 @@ test('normal installed Copilot TDD command runs from the project with a complete
assert.ok(!plan.operations.some(operation => operation.kind === 'update-claude-settings'
|| /[\\/]\.github[\\/]hooks[\\/]|[\\/]ecc[\\/]scripts[\\/]hooks[\\/]|settings\.json$|hooks\.json$/.test(operation.destinationPath)), 'Copilot must not register hooks');
assert.strictEqual(fs.readFileSync(path.join(project, 'scripts', 'setup-package-manager.js'), 'utf8'), 'USER PROJECT SENTINEL\n');
const boundary = path.join(project, '.github', 'ecc', 'scripts', 'package.json');
const installedState = JSON.parse(fs.readFileSync(plan.installStatePath, 'utf8'));
assert.ok(installedState.operations.some(operation => operation.destinationPath === boundary));
assert.deepStrictEqual(JSON.parse(fs.readFileSync(boundary, 'utf8')), { type: 'commonjs' });
fs.unlinkSync(boundary);
const options = { repoRoot: REPO_ROOT, projectRoot: project, homeDir, targets: ['copilot'], env };
const repair = repairInstalledStates(options);
assert.strictEqual(repair.summary.errorCount, 0, JSON.stringify(repair));
const repaired = spawnSync(process.execPath, [command[1], '--detect'], { cwd: project, env, encoding: 'utf8', timeout: 15000 });
assert.strictEqual(repaired.status, 0, repaired.stderr);
assert.match(repaired.stdout, /pnpm/);
const uninstall = uninstallInstalledStates(options);
assert.strictEqual(uninstall.summary.errorCount, 0, JSON.stringify(uninstall));
assert.ok(!fs.existsSync(path.join(project, '.github', 'ecc', 'scripts', 'lib', 'package-manager.js')));
assert.ok(!fs.existsSync(boundary));
assert.strictEqual(fs.readFileSync(path.join(project, 'package.json'), 'utf8'), projectPackage);
const userProject = path.join(project, 'user-project');
const userHelper = path.join(userProject, '.github', 'ecc', 'scripts', 'setup-package-manager.js');
fs.mkdirSync(path.dirname(userHelper), { recursive: true });
fs.writeFileSync(userHelper, 'USER OWNED HELPER SENTINEL\n');
const userBoundary = path.join(path.dirname(userHelper), 'package.json');
const userPackage = '{"type":"module","private":true}\n';
fs.writeFileSync(userBoundary, userPackage);
const userPlan = createManifestInstallPlan({ target: 'copilot', moduleIds: ['workflow-quality'], projectRoot: userProject, homeDir, env });
applyInstallPlan(userPlan);
assert.strictEqual(fs.readFileSync(userHelper, 'utf8'), 'USER OWNED HELPER SENTINEL\n');
assert.strictEqual(fs.readFileSync(userBoundary, 'utf8'), userPackage);
const userOptions = { ...options, projectRoot: userProject };
const userRepair = repairInstalledStates(userOptions);
assert.strictEqual(userRepair.summary.errorCount, 0);
assert.strictEqual(fs.readFileSync(userHelper, 'utf8'), 'USER OWNED HELPER SENTINEL\n');
assert.strictEqual(fs.readFileSync(userBoundary, 'utf8'), userPackage);
const state = JSON.parse(fs.readFileSync(userPlan.installStatePath, 'utf8'));
assert.ok(!state.operations.some(operation => operation.destinationPath === userHelper));
assert.ok(!state.operations.some(operation => operation.destinationPath === userHelper
|| operation.destinationPath === userBoundary));
const userUninstall = uninstallInstalledStates(userOptions);
assert.strictEqual(userUninstall.summary.errorCount, 0);
assert.strictEqual(fs.readFileSync(userHelper, 'utf8'), 'USER OWNED HELPER SENTINEL\n');
assert.strictEqual(fs.readFileSync(userBoundary, 'utf8'), userPackage);
} finally {
fs.rmSync(project, { recursive: true, force: true });
}