Files
ECC/scripts/lib/install/inventory.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

149 lines
4.5 KiB
JavaScript

'use strict';
const fs = require('fs');
const os = require('os');
const path = require('path');
const { isWithinRoot, realpathNearestExisting } = require('../path-safety');
const CURRENT_PLUGIN_ID = 'ecc@ecc';
const LEGACY_PLUGIN_IDS = new Set([
'everything-claude-code@everything-claude-code',
'everything-claude-code@ecc',
]);
function resolveClaudePaths(options = {}) {
const homeDir = options.homeDir
|| process.env.HOME
|| process.env.USERPROFILE
|| os.homedir();
const configDir = options.configDir
|| process.env.CLAUDE_CONFIG_DIR
|| path.join(homeDir, '.claude');
const projectRoot = options.projectRoot || process.cwd();
return {
homeDir: path.resolve(homeDir),
configDir: path.resolve(configDir),
projectRoot: path.resolve(projectRoot),
};
}
function readJsonObject(filePath, label) {
let value;
try {
value = JSON.parse(fs.readFileSync(filePath, 'utf8'));
} catch (error) {
throw new Error(`${label} is invalid at ${filePath}: ${error.message}`);
}
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new Error(`${label} is invalid at ${filePath}: expected a JSON object`);
}
return value;
}
function findManualClaudePlugin(options = {}) {
const { configDir } = resolveClaudePaths(options);
const pluginsDir = path.join(configDir, 'plugins');
const candidates = [
['ecc', '.claude-plugin', 'plugin.json'],
['ecc', 'plugin.json'],
['ecc@ecc', '.claude-plugin', 'plugin.json'],
['ecc@ecc', 'plugin.json'],
['everything-claude-code', '.claude-plugin', 'plugin.json'],
['everything-claude-code', 'plugin.json'],
];
for (const segments of candidates) {
const manifestPath = path.join(pluginsDir, ...segments);
if (fs.existsSync(manifestPath)) {
return {
manifestPath,
installPath: path.dirname(path.dirname(manifestPath)),
};
}
}
return null;
}
function validateManagedState(state, statePath, expectedRoot) {
const selectedModules = state?.resolution?.selectedModules;
const operations = state?.operations;
if (
state?.schemaVersion !== 'ecc.install.v1'
|| !state.target
|| typeof state.target !== 'object'
|| Array.isArray(state.target)
|| !Array.isArray(selectedModules)
|| !selectedModules.every(moduleId => typeof moduleId === 'string' && moduleId.length > 0)
|| !Array.isArray(operations)
) {
throw new Error(`Managed Claude install-state is invalid at ${statePath}`);
}
for (const operation of operations) {
if (
!operation
|| typeof operation !== 'object'
|| typeof operation.destinationPath !== 'string'
|| !path.isAbsolute(operation.destinationPath)
|| !isWithinRoot(operation.destinationPath, expectedRoot)
) {
throw new Error(`Managed Claude install-state is invalid at ${statePath}`);
}
}
return { selectedModules, operations };
}
function operationOverlapsPlugin(operation, expectedRoot) {
const canonicalRoot = realpathNearestExisting(expectedRoot);
const canonicalDestination = realpathNearestExisting(operation.destinationPath);
const relativePath = path.relative(canonicalRoot, canonicalDestination);
const firstSegment = relativePath.split(path.sep)[0];
return ['agents', 'commands', 'hooks', 'skills'].includes(firstSegment);
}
function findManagedClaudeInstalls(options = {}) {
const { configDir, projectRoot } = resolveClaudePaths(options);
const candidates = [
{
statePath: path.join(configDir, 'ecc', 'install-state.json'),
expectedRoot: configDir,
},
{
statePath: path.join(projectRoot, '.claude', 'ecc', 'install-state.json'),
expectedRoot: path.join(projectRoot, '.claude'),
},
];
const findings = [];
for (const candidate of candidates) {
if (!fs.existsSync(candidate.statePath)) continue;
const state = readJsonObject(candidate.statePath, 'Managed Claude install-state');
const { selectedModules, operations } = validateManagedState(
state,
candidate.statePath,
candidate.expectedRoot
);
const modulesOverlap = selectedModules.some(moduleId => moduleId !== 'rules-core');
const operationsOverlap = operations.some(operation => (
operationOverlapsPlugin(operation, candidate.expectedRoot)
));
findings.push({
statePath: candidate.statePath,
selectedModules: [...selectedModules],
overlapsPlugin: modulesOverlap || operationsOverlap,
});
}
return findings;
}
module.exports = {
CURRENT_PLUGIN_ID,
LEGACY_PLUGIN_IDS,
findManagedClaudeInstalls,
findManualClaudePlugin,
resolveClaudePaths,
};