Files
ECC/scripts/lib/codex-plugin-setup.js
T
28e53a0bc1 feat(install): add guided multi-harness installer (#2649)
* feat(install): add guided Claude plugin setup

* fix: support Claude command shims on Windows

* feat: support safe Claude plugin scope migration

* fix(install): preserve interactive setup terminal

* fix(install): auto-migrate setup scope changes

* feat(install): add guided multi-harness installer

* fix(install): sync Yarn binary metadata

* fix(install): handle wizard EOF on Node 18

* ci: allow installer matrix tests to finish

* test(install): allow slower PowerShell delegation

* fix(install): harden guided provider reconciliation

* test(install): harden packaged and local compatibility

* chore: prepare guided installer release 2.2.0

* fix(install): report refreshed Codex marketplace state

* fix(install): verify managed content provenance

* test(install): allow empty Yarn smoke fixture

* test(install): invoke Windows package shims safely

* fix(install): close cross-platform release gaps

* fix(install): require trusted GitHub origins

* fix(install): preserve hook profile precedence

* refactor(install): centralize trusted GitHub origins

* ci: retrigger workflow run after merge of main

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 15:39:49 -04:00

479 lines
14 KiB
JavaScript

'use strict';
const { execFile: nodeExecFile } = require('child_process');
const path = require('path');
const { normalizeGitHubGitOrigin } = require('./github-origin');
const CODEX_PLUGIN_ID = 'ecc@ecc';
const OFFICIAL_MARKETPLACE_NAME = 'ecc';
const OFFICIAL_MARKETPLACE_REPO = 'affaan-m/ECC';
const NORMALIZED_OFFICIAL_MARKETPLACE_REPO = OFFICIAL_MARKETPLACE_REPO.toLowerCase();
const MAX_OUTPUT_BYTES = 10 * 1024 * 1024;
const PROVIDER_COMMAND_TIMEOUT_MS = 120 * 1000;
class CodexPluginSetupError extends Error {
constructor(code, message, details = {}) {
super(message);
this.name = 'CodexPluginSetupError';
this.code = code;
this.phase = details.phase || 'inventory';
this.argv = [...(details.argv || [])];
}
}
function fail(code, message, details) {
throw new CodexPluginSetupError(code, message, details);
}
function parseJsonObject(stdout, inventoryName, phase = 'inventory') {
let parsed;
try {
parsed = JSON.parse(String(stdout || ''));
} catch (error) {
fail(
`INVALID_${inventoryName.toUpperCase()}_INVENTORY`,
`Codex ${inventoryName} inventory returned invalid JSON: ${error.message}`,
{ phase }
);
}
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
fail(
`INVALID_${inventoryName.toUpperCase()}_INVENTORY`,
`Codex ${inventoryName} inventory is invalid: expected a JSON object`,
{ phase }
);
}
return parsed;
}
function parseMarketplaceInventory(stdout, phase) {
const inventory = parseJsonObject(stdout, 'marketplace', phase);
if (!Array.isArray(inventory.marketplaces)) {
fail(
'INVALID_MARKETPLACE_INVENTORY',
'Codex marketplace inventory is invalid: expected `marketplaces` to be an array',
{ phase }
);
}
for (const marketplace of inventory.marketplaces) {
if (
!marketplace
|| typeof marketplace.name !== 'string'
|| marketplace.name.length === 0
|| typeof marketplace.root !== 'string'
|| marketplace.root.length === 0
) {
fail(
'INVALID_MARKETPLACE_INVENTORY',
'Codex marketplace inventory contains an invalid marketplace entry',
{ phase }
);
}
}
const eccEntries = inventory.marketplaces.filter(
marketplace => marketplace.name === OFFICIAL_MARKETPLACE_NAME
);
if (eccEntries.length > 1) {
fail(
'INVALID_MARKETPLACE_INVENTORY',
'Codex marketplace inventory contains duplicate `ecc` entries',
{ phase }
);
}
return inventory.marketplaces;
}
function assertPluginEntries(entries, field, phase) {
if (!Array.isArray(entries)) {
fail(
'INVALID_PLUGIN_INVENTORY',
`Codex plugin inventory is invalid: expected \`${field}\` to be an array`,
{ phase }
);
}
for (const plugin of entries) {
if (
!plugin
|| typeof plugin.pluginId !== 'string'
|| plugin.pluginId.length === 0
) {
fail(
'INVALID_PLUGIN_INVENTORY',
`Codex plugin inventory contains an invalid \`${field}\` entry`,
{ phase }
);
}
if (plugin.installed !== undefined && typeof plugin.installed !== 'boolean') {
fail(
'INVALID_PLUGIN_INVENTORY',
`Codex plugin inventory contains an invalid \`${field}\` install state`,
{ phase }
);
}
if (plugin.enabled !== undefined && typeof plugin.enabled !== 'boolean') {
fail(
'INVALID_PLUGIN_INVENTORY',
`Codex plugin inventory contains an invalid \`${field}\` enabled state`,
{ phase }
);
}
}
}
function parsePluginInventory(stdout, phase) {
const inventory = parseJsonObject(stdout, 'plugin', phase);
assertPluginEntries(inventory.installed, 'installed', phase);
assertPluginEntries(inventory.available, 'available', phase);
const eccEntries = inventory.installed.filter(
plugin => plugin.pluginId === CODEX_PLUGIN_ID
);
if (eccEntries.length > 1) {
fail(
'INVALID_PLUGIN_INVENTORY',
`Codex plugin inventory contains duplicate ${CODEX_PLUGIN_ID} entries`,
{ phase }
);
}
return {
installed: [...inventory.installed],
available: [...inventory.available],
};
}
function executeFile(execFile, command, args, options) {
return new Promise((resolve, reject) => {
execFile(command, args, options, (error, stdout, stderr) => {
if (error) {
if (error.stderr === undefined) error.stderr = stderr;
if (error.stdout === undefined) error.stdout = stdout;
reject(error);
return;
}
resolve({ stdout: String(stdout || ''), stderr: String(stderr || '') });
});
});
}
function isCommandTimeout(error, killSignal = 'SIGKILL') {
return error?.code === 'ETIMEDOUT'
|| (error?.killed === true && error?.signal === killSignal);
}
async function runCodexCommand(args, options = {}, dependencies = {}) {
const command = dependencies.command || options.command || 'codex';
const execFile = dependencies.execFile || nodeExecFile;
const argv = [...args];
const timeoutMs = options.timeoutMs ?? PROVIDER_COMMAND_TIMEOUT_MS;
const killSignal = 'SIGKILL';
try {
return await executeFile(execFile, command, argv, {
cwd: options.cwd || process.cwd(),
encoding: 'utf8',
env: options.env || process.env,
maxBuffer: MAX_OUTPUT_BYTES,
killSignal,
shell: false,
timeout: timeoutMs,
windowsHide: true,
});
} catch (error) {
if (isCommandTimeout(error, killSignal)) {
fail(
'CODEX_COMMAND_TIMEOUT',
`Codex command timed out after ${timeoutMs} ms`,
{ argv, phase: options.phase }
);
}
if (error?.code === 'ENOENT') {
fail(
'CODEX_NOT_FOUND',
'Codex CLI is not installed or `codex` is not on PATH. Install Codex, then rerun ECC setup.',
{ argv, phase: options.phase }
);
}
const detail = String(error?.stderr || error?.stdout || error?.message || '').trim();
fail(
'CODEX_COMMAND_FAILED',
`Codex command failed${detail ? `: ${detail}` : ''}`,
{ argv, phase: options.phase }
);
}
}
async function resolveMarketplaceRepository(marketplace, options = {}, dependencies = {}) {
const execFile = dependencies.execFile || nodeExecFile;
const timeoutMs = options.timeoutMs ?? PROVIDER_COMMAND_TIMEOUT_MS;
const killSignal = 'SIGKILL';
let result;
try {
result = await executeFile(
execFile,
dependencies.gitCommand || 'git',
['-C', marketplace.root, 'remote', 'get-url', 'origin'],
{
cwd: options.cwd || process.cwd(),
encoding: 'utf8',
env: options.env || process.env,
maxBuffer: MAX_OUTPUT_BYTES,
killSignal,
shell: false,
timeout: timeoutMs,
windowsHide: true,
}
);
} catch (error) {
if (isCommandTimeout(error, killSignal)) {
fail(
'MARKETPLACE_PROVENANCE_TIMEOUT',
`Git provenance verification timed out after ${timeoutMs} ms`,
{ phase: options.phase || 'marketplace-provenance' }
);
}
const detail = String(error?.stderr || error?.message || '').trim();
fail(
'MARKETPLACE_COLLISION',
`Refusing the existing \`ecc\` marketplace because its Git provenance could not be verified${detail ? `: ${detail}` : ''}.`,
{ phase: options.phase || 'marketplace-provenance' }
);
}
return String(result.stdout || '').trim();
}
async function assertOfficialMarketplace(
marketplace,
options,
dependencies,
phase = 'marketplace-provenance'
) {
if (!marketplace) return;
const resolveRepository = dependencies.resolveMarketplaceRepository
|| (entry => resolveMarketplaceRepository(
entry,
{ ...options, phase },
dependencies
));
let repository;
try {
repository = normalizeGitHubGitOrigin(await resolveRepository(marketplace));
} catch (error) {
if (error instanceof CodexPluginSetupError) throw error;
const detail = String(error?.message || error || '').trim();
fail(
'MARKETPLACE_COLLISION',
`Refusing the existing \`ecc\` marketplace because its provenance could not be verified${detail ? `: ${detail}` : ''}.`,
{ phase }
);
}
if (repository !== NORMALIZED_OFFICIAL_MARKETPLACE_REPO) {
fail(
'MARKETPLACE_COLLISION',
'Refusing the existing `ecc` marketplace because it is not the official affaan-m/ECC source.',
{ phase }
);
}
}
function normalizeMarketplaceRoot(value) {
if (typeof value !== 'string' || value.length === 0) return null;
const isWindowsPath = /^[a-z]:[\\/]/i.test(value) || /^\\\\/.test(value);
const normalized = isWindowsPath
? path.win32.normalize(value)
: path.posix.normalize(value);
return isWindowsPath ? normalized.toLowerCase() : normalized;
}
function parseMarketplaceUpgradeResult(stdout, marketplace) {
const phase = 'marketplace-upgrade';
const argv = [
'plugin', 'marketplace', 'upgrade', OFFICIAL_MARKETPLACE_NAME, '--json',
];
let result;
try {
result = JSON.parse(String(stdout || ''));
} catch (error) {
fail(
'INVALID_MARKETPLACE_UPGRADE_RESULT',
`Codex marketplace refresh returned invalid JSON: ${error.message}`,
{ phase, argv }
);
}
const validShape = (
result
&& typeof result === 'object'
&& !Array.isArray(result)
&& Array.isArray(result.selectedMarketplaces)
&& result.selectedMarketplaces.every(name => typeof name === 'string')
&& Array.isArray(result.upgradedRoots)
&& result.upgradedRoots.every(root => typeof root === 'string' && root.length > 0)
&& Array.isArray(result.errors)
);
if (!validShape) {
fail(
'INVALID_MARKETPLACE_UPGRADE_RESULT',
'Codex marketplace refresh returned an invalid result.',
{ phase, argv }
);
}
const expectedRoot = normalizeMarketplaceRoot(marketplace.root);
const upgradedRoot = result.upgradedRoots.length === 1
? normalizeMarketplaceRoot(result.upgradedRoots[0])
: null;
if (
result.errors.length > 0
|| result.selectedMarketplaces.length !== 1
|| result.selectedMarketplaces[0] !== OFFICIAL_MARKETPLACE_NAME
|| upgradedRoot !== expectedRoot
) {
fail(
'MARKETPLACE_REFRESH_FAILED',
'Codex did not confirm that the official ECC marketplace was refreshed.',
{ phase, argv }
);
}
return result;
}
function findEccMarketplace(marketplaces) {
return marketplaces.find(
marketplace => marketplace.name === OFFICIAL_MARKETPLACE_NAME
) || null;
}
function findInstalledEccPlugin(inventory) {
return inventory.installed.find(
plugin => plugin.pluginId === CODEX_PLUGIN_ID
) || null;
}
async function readMarketplaceInventory(run, phase) {
const result = await run(
['plugin', 'marketplace', 'list', '--json'],
{ phase }
);
return parseMarketplaceInventory(result.stdout, phase);
}
async function readPluginInventory(run, phase) {
const result = await run(['plugin', 'list', '--json'], { phase });
return parsePluginInventory(result.stdout, phase);
}
async function reconcileCodexPlugin(options = {}, dependencies = {}) {
const run = (args, details = {}) => runCodexCommand(
args,
{
command: options.command,
cwd: options.cwd,
env: options.env,
phase: details.phase,
},
dependencies
);
const marketplaces = await readMarketplaceInventory(run, 'marketplace-inventory');
const plugins = await readPluginInventory(run, 'plugin-inventory');
const marketplace = findEccMarketplace(marketplaces);
const installedPlugin = findInstalledEccPlugin(plugins);
await assertOfficialMarketplace(marketplace, options, dependencies);
const pluginReady = (
installedPlugin?.installed === true
&& installedPlugin.enabled === true
);
const isReconciled = Boolean(marketplace && pluginReady);
if (options.dryRun) {
return {
action: isReconciled
? 'unchanged'
: (installedPlugin ? 'would-update' : 'would-install'),
dryRun: true,
marketplaceAction: marketplace
? 'would-upgrade'
: 'would-add',
pluginId: CODEX_PLUGIN_ID,
restartRequired: !isReconciled,
};
}
const marketplaceArgs = marketplace
? ['plugin', 'marketplace', 'upgrade', OFFICIAL_MARKETPLACE_NAME, '--json']
: ['plugin', 'marketplace', 'add', OFFICIAL_MARKETPLACE_REPO, '--json'];
const marketplaceAction = marketplace ? 'upgraded' : 'added';
const marketplaceResult = await run(marketplaceArgs, {
phase: marketplace ? 'marketplace-upgrade' : 'marketplace-add',
});
if (marketplace) {
parseMarketplaceUpgradeResult(marketplaceResult.stdout, marketplace);
}
const verifiedMarketplaces = await readMarketplaceInventory(
run,
'marketplace-verification'
);
if (!findEccMarketplace(verifiedMarketplaces)) {
fail(
'MARKETPLACE_VERIFICATION_FAILED',
'Could not verify the ECC marketplace after reconciliation.',
{ phase: 'marketplace-verification' }
);
}
await assertOfficialMarketplace(
findEccMarketplace(verifiedMarketplaces),
options,
dependencies,
'marketplace-verification'
);
const pluginsAfterMarketplace = marketplace
? await readPluginInventory(run, 'plugin-verification')
: plugins;
const pluginAfterMarketplace = findInstalledEccPlugin(pluginsAfterMarketplace);
const pluginReadyAfterMarketplace = (
pluginAfterMarketplace?.installed === true
&& pluginAfterMarketplace.enabled === true
);
if (!pluginReadyAfterMarketplace) {
await run(
['plugin', 'add', CODEX_PLUGIN_ID, '--json'],
{ phase: 'plugin-add' }
);
}
const verifiedPlugins = pluginReadyAfterMarketplace
? pluginsAfterMarketplace
: await readPluginInventory(run, 'plugin-verification');
const verifiedPlugin = findInstalledEccPlugin(verifiedPlugins);
if (!(verifiedPlugin?.installed === true && verifiedPlugin.enabled === true)) {
fail(
'PLUGIN_VERIFICATION_FAILED',
`Could not verify ${CODEX_PLUGIN_ID} as installed and enabled after reconciliation.`,
{ phase: 'plugin-verification' }
);
}
return {
action: installedPlugin ? 'updated' : 'installed',
marketplaceAction,
pluginId: CODEX_PLUGIN_ID,
restartRequired: marketplaceAction === 'upgraded' || !pluginReadyAfterMarketplace,
};
}
module.exports = {
CODEX_PLUGIN_ID,
CodexPluginSetupError,
OFFICIAL_MARKETPLACE_NAME,
OFFICIAL_MARKETPLACE_REPO,
PROVIDER_COMMAND_TIMEOUT_MS,
executeFile,
findEccMarketplace,
findInstalledEccPlugin,
normalizeGitHubGitOrigin,
parseMarketplaceInventory,
parseMarketplaceUpgradeResult,
parsePluginInventory,
reconcileCodexPlugin,
resolveMarketplaceRepository,
runCodexCommand,
};