mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-02 23:23:31 +08:00
feat: strengthen install lifecycle and target adapters (#512)
* fix: strengthen install lifecycle adapters * fix: restore template content on uninstall
This commit is contained in:
@@ -1,11 +1,28 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const Ajv = require('ajv');
|
||||
|
||||
let Ajv = null;
|
||||
try {
|
||||
// Prefer schema-backed validation when dependencies are installed.
|
||||
// The fallback validator below keeps source checkouts usable in bare environments.
|
||||
const ajvModule = require('ajv');
|
||||
Ajv = ajvModule.default || ajvModule;
|
||||
} catch (_error) {
|
||||
Ajv = null;
|
||||
}
|
||||
|
||||
const SCHEMA_PATH = path.join(__dirname, '..', '..', 'schemas', 'install-state.schema.json');
|
||||
|
||||
let cachedValidator = null;
|
||||
|
||||
function cloneJsonValue(value) {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
|
||||
function readJson(filePath, label) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
@@ -19,12 +36,188 @@ function getValidator() {
|
||||
return cachedValidator;
|
||||
}
|
||||
|
||||
const schema = readJson(SCHEMA_PATH, 'install-state schema');
|
||||
const ajv = new Ajv({ allErrors: true });
|
||||
cachedValidator = ajv.compile(schema);
|
||||
if (Ajv) {
|
||||
const schema = readJson(SCHEMA_PATH, 'install-state schema');
|
||||
const ajv = new Ajv({ allErrors: true });
|
||||
cachedValidator = ajv.compile(schema);
|
||||
return cachedValidator;
|
||||
}
|
||||
|
||||
cachedValidator = createFallbackValidator();
|
||||
return cachedValidator;
|
||||
}
|
||||
|
||||
function createFallbackValidator() {
|
||||
const validate = state => {
|
||||
const errors = [];
|
||||
validate.errors = errors;
|
||||
|
||||
function pushError(instancePath, message) {
|
||||
errors.push({
|
||||
instancePath,
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
function isNonEmptyString(value) {
|
||||
return typeof value === 'string' && value.length > 0;
|
||||
}
|
||||
|
||||
function validateNoAdditionalProperties(value, instancePath, allowedKeys) {
|
||||
for (const key of Object.keys(value)) {
|
||||
if (!allowedKeys.includes(key)) {
|
||||
pushError(`${instancePath}/${key}`, 'must NOT have additional properties');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateStringArray(value, instancePath) {
|
||||
if (!Array.isArray(value)) {
|
||||
pushError(instancePath, 'must be array');
|
||||
return;
|
||||
}
|
||||
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
if (!isNonEmptyString(value[index])) {
|
||||
pushError(`${instancePath}/${index}`, 'must be non-empty string');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateOptionalString(value, instancePath) {
|
||||
if (value !== undefined && value !== null && !isNonEmptyString(value)) {
|
||||
pushError(instancePath, 'must be string or null');
|
||||
}
|
||||
}
|
||||
|
||||
if (!state || typeof state !== 'object' || Array.isArray(state)) {
|
||||
pushError('/', 'must be object');
|
||||
return false;
|
||||
}
|
||||
|
||||
validateNoAdditionalProperties(
|
||||
state,
|
||||
'',
|
||||
['schemaVersion', 'installedAt', 'lastValidatedAt', 'target', 'request', 'resolution', 'source', 'operations']
|
||||
);
|
||||
|
||||
if (state.schemaVersion !== 'ecc.install.v1') {
|
||||
pushError('/schemaVersion', 'must equal ecc.install.v1');
|
||||
}
|
||||
|
||||
if (!isNonEmptyString(state.installedAt)) {
|
||||
pushError('/installedAt', 'must be non-empty string');
|
||||
}
|
||||
|
||||
if (state.lastValidatedAt !== undefined && !isNonEmptyString(state.lastValidatedAt)) {
|
||||
pushError('/lastValidatedAt', 'must be non-empty string');
|
||||
}
|
||||
|
||||
const target = state.target;
|
||||
if (!target || typeof target !== 'object' || Array.isArray(target)) {
|
||||
pushError('/target', 'must be object');
|
||||
} else {
|
||||
validateNoAdditionalProperties(target, '/target', ['id', 'target', 'kind', 'root', 'installStatePath']);
|
||||
if (!isNonEmptyString(target.id)) {
|
||||
pushError('/target/id', 'must be non-empty string');
|
||||
}
|
||||
validateOptionalString(target.target, '/target/target');
|
||||
if (target.kind !== undefined && !['home', 'project'].includes(target.kind)) {
|
||||
pushError('/target/kind', 'must be equal to one of the allowed values');
|
||||
}
|
||||
if (!isNonEmptyString(target.root)) {
|
||||
pushError('/target/root', 'must be non-empty string');
|
||||
}
|
||||
if (!isNonEmptyString(target.installStatePath)) {
|
||||
pushError('/target/installStatePath', 'must be non-empty string');
|
||||
}
|
||||
}
|
||||
|
||||
const request = state.request;
|
||||
if (!request || typeof request !== 'object' || Array.isArray(request)) {
|
||||
pushError('/request', 'must be object');
|
||||
} else {
|
||||
validateNoAdditionalProperties(
|
||||
request,
|
||||
'/request',
|
||||
['profile', 'modules', 'includeComponents', 'excludeComponents', 'legacyLanguages', 'legacyMode']
|
||||
);
|
||||
if (!(Object.prototype.hasOwnProperty.call(request, 'profile') && (request.profile === null || typeof request.profile === 'string'))) {
|
||||
pushError('/request/profile', 'must be string or null');
|
||||
}
|
||||
validateStringArray(request.modules, '/request/modules');
|
||||
validateStringArray(request.includeComponents, '/request/includeComponents');
|
||||
validateStringArray(request.excludeComponents, '/request/excludeComponents');
|
||||
validateStringArray(request.legacyLanguages, '/request/legacyLanguages');
|
||||
if (typeof request.legacyMode !== 'boolean') {
|
||||
pushError('/request/legacyMode', 'must be boolean');
|
||||
}
|
||||
}
|
||||
|
||||
const resolution = state.resolution;
|
||||
if (!resolution || typeof resolution !== 'object' || Array.isArray(resolution)) {
|
||||
pushError('/resolution', 'must be object');
|
||||
} else {
|
||||
validateNoAdditionalProperties(resolution, '/resolution', ['selectedModules', 'skippedModules']);
|
||||
validateStringArray(resolution.selectedModules, '/resolution/selectedModules');
|
||||
validateStringArray(resolution.skippedModules, '/resolution/skippedModules');
|
||||
}
|
||||
|
||||
const source = state.source;
|
||||
if (!source || typeof source !== 'object' || Array.isArray(source)) {
|
||||
pushError('/source', 'must be object');
|
||||
} else {
|
||||
validateNoAdditionalProperties(source, '/source', ['repoVersion', 'repoCommit', 'manifestVersion']);
|
||||
validateOptionalString(source.repoVersion, '/source/repoVersion');
|
||||
validateOptionalString(source.repoCommit, '/source/repoCommit');
|
||||
if (!Number.isInteger(source.manifestVersion) || source.manifestVersion < 1) {
|
||||
pushError('/source/manifestVersion', 'must be integer >= 1');
|
||||
}
|
||||
}
|
||||
|
||||
if (!Array.isArray(state.operations)) {
|
||||
pushError('/operations', 'must be array');
|
||||
} else {
|
||||
for (let index = 0; index < state.operations.length; index += 1) {
|
||||
const operation = state.operations[index];
|
||||
const instancePath = `/operations/${index}`;
|
||||
|
||||
if (!operation || typeof operation !== 'object' || Array.isArray(operation)) {
|
||||
pushError(instancePath, 'must be object');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isNonEmptyString(operation.kind)) {
|
||||
pushError(`${instancePath}/kind`, 'must be non-empty string');
|
||||
}
|
||||
if (!isNonEmptyString(operation.moduleId)) {
|
||||
pushError(`${instancePath}/moduleId`, 'must be non-empty string');
|
||||
}
|
||||
if (!isNonEmptyString(operation.sourceRelativePath)) {
|
||||
pushError(`${instancePath}/sourceRelativePath`, 'must be non-empty string');
|
||||
}
|
||||
if (!isNonEmptyString(operation.destinationPath)) {
|
||||
pushError(`${instancePath}/destinationPath`, 'must be non-empty string');
|
||||
}
|
||||
if (!isNonEmptyString(operation.strategy)) {
|
||||
pushError(`${instancePath}/strategy`, 'must be non-empty string');
|
||||
}
|
||||
if (!isNonEmptyString(operation.ownership)) {
|
||||
pushError(`${instancePath}/ownership`, 'must be non-empty string');
|
||||
}
|
||||
if (typeof operation.scaffoldOnly !== 'boolean') {
|
||||
pushError(`${instancePath}/scaffoldOnly`, 'must be boolean');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors.length === 0;
|
||||
};
|
||||
|
||||
validate.errors = [];
|
||||
return validate;
|
||||
}
|
||||
|
||||
function formatValidationErrors(errors = []) {
|
||||
return errors
|
||||
.map(error => `${error.instancePath || '/'} ${error.message}`)
|
||||
@@ -87,7 +280,7 @@ function createInstallState(options) {
|
||||
manifestVersion: options.source.manifestVersion,
|
||||
},
|
||||
operations: Array.isArray(options.operations)
|
||||
? options.operations.map(operation => ({ ...operation }))
|
||||
? options.operations.map(operation => cloneJsonValue(operation))
|
||||
: [],
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user