mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-05 00:33:27 +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:
@@ -10,6 +10,8 @@ const path = require('path');
|
||||
const {
|
||||
buildDoctorReport,
|
||||
discoverInstalledStates,
|
||||
repairInstalledStates,
|
||||
uninstallInstalledStates,
|
||||
} = require('../../scripts/lib/install-lifecycle');
|
||||
const {
|
||||
createInstallState,
|
||||
@@ -350,6 +352,385 @@ function runTests() {
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('repair restores render-template outputs from recorded rendered content', () => {
|
||||
const homeDir = createTempDir('install-lifecycle-home-');
|
||||
const projectRoot = createTempDir('install-lifecycle-project-');
|
||||
|
||||
try {
|
||||
const targetRoot = path.join(homeDir, '.claude');
|
||||
const statePath = path.join(targetRoot, 'ecc', 'install-state.json');
|
||||
const destinationPath = path.join(targetRoot, 'plugin.json');
|
||||
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
||||
fs.writeFileSync(destinationPath, '{"drifted":true}\n');
|
||||
|
||||
writeState(statePath, {
|
||||
adapter: { id: 'claude-home', target: 'claude', kind: 'home' },
|
||||
targetRoot,
|
||||
installStatePath: statePath,
|
||||
request: {
|
||||
profile: null,
|
||||
modules: [],
|
||||
legacyLanguages: ['typescript'],
|
||||
legacyMode: true,
|
||||
},
|
||||
resolution: {
|
||||
selectedModules: ['legacy-claude-rules'],
|
||||
skippedModules: [],
|
||||
},
|
||||
operations: [
|
||||
{
|
||||
kind: 'render-template',
|
||||
moduleId: 'platform-configs',
|
||||
sourceRelativePath: '.claude-plugin/plugin.json.template',
|
||||
destinationPath,
|
||||
strategy: 'render-template',
|
||||
ownership: 'managed',
|
||||
scaffoldOnly: false,
|
||||
renderedContent: '{"ok":true}\n',
|
||||
},
|
||||
],
|
||||
source: {
|
||||
repoVersion: CURRENT_PACKAGE_VERSION,
|
||||
repoCommit: 'abc123',
|
||||
manifestVersion: CURRENT_MANIFEST_VERSION,
|
||||
},
|
||||
});
|
||||
|
||||
const result = repairInstalledStates({
|
||||
repoRoot: REPO_ROOT,
|
||||
homeDir,
|
||||
projectRoot,
|
||||
targets: ['claude'],
|
||||
});
|
||||
|
||||
assert.strictEqual(result.results[0].status, 'repaired');
|
||||
assert.strictEqual(fs.readFileSync(destinationPath, 'utf8'), '{"ok":true}\n');
|
||||
} finally {
|
||||
cleanup(homeDir);
|
||||
cleanup(projectRoot);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('repair reapplies merge-json operations without clobbering unrelated keys', () => {
|
||||
const homeDir = createTempDir('install-lifecycle-home-');
|
||||
const projectRoot = createTempDir('install-lifecycle-project-');
|
||||
|
||||
try {
|
||||
const targetRoot = path.join(projectRoot, '.cursor');
|
||||
const statePath = path.join(targetRoot, 'ecc-install-state.json');
|
||||
const destinationPath = path.join(targetRoot, 'hooks.json');
|
||||
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
||||
fs.writeFileSync(destinationPath, JSON.stringify({
|
||||
existing: true,
|
||||
nested: {
|
||||
enabled: false,
|
||||
},
|
||||
}, null, 2));
|
||||
|
||||
writeState(statePath, {
|
||||
adapter: { id: 'cursor-project', target: 'cursor', kind: 'project' },
|
||||
targetRoot,
|
||||
installStatePath: statePath,
|
||||
request: {
|
||||
profile: null,
|
||||
modules: [],
|
||||
legacyLanguages: ['typescript'],
|
||||
legacyMode: true,
|
||||
},
|
||||
resolution: {
|
||||
selectedModules: ['legacy-cursor-install'],
|
||||
skippedModules: [],
|
||||
},
|
||||
operations: [
|
||||
{
|
||||
kind: 'merge-json',
|
||||
moduleId: 'platform-configs',
|
||||
sourceRelativePath: '.cursor/hooks.json',
|
||||
destinationPath,
|
||||
strategy: 'merge-json',
|
||||
ownership: 'managed',
|
||||
scaffoldOnly: false,
|
||||
mergePayload: {
|
||||
nested: {
|
||||
enabled: true,
|
||||
},
|
||||
managed: 'yes',
|
||||
},
|
||||
},
|
||||
],
|
||||
source: {
|
||||
repoVersion: CURRENT_PACKAGE_VERSION,
|
||||
repoCommit: 'abc123',
|
||||
manifestVersion: CURRENT_MANIFEST_VERSION,
|
||||
},
|
||||
});
|
||||
|
||||
const result = repairInstalledStates({
|
||||
repoRoot: REPO_ROOT,
|
||||
homeDir,
|
||||
projectRoot,
|
||||
targets: ['cursor'],
|
||||
});
|
||||
|
||||
assert.strictEqual(result.results[0].status, 'repaired');
|
||||
assert.deepStrictEqual(JSON.parse(fs.readFileSync(destinationPath, 'utf8')), {
|
||||
existing: true,
|
||||
nested: {
|
||||
enabled: true,
|
||||
},
|
||||
managed: 'yes',
|
||||
});
|
||||
} finally {
|
||||
cleanup(homeDir);
|
||||
cleanup(projectRoot);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('repair re-applies managed remove operations when files reappear', () => {
|
||||
const homeDir = createTempDir('install-lifecycle-home-');
|
||||
const projectRoot = createTempDir('install-lifecycle-project-');
|
||||
|
||||
try {
|
||||
const targetRoot = path.join(projectRoot, '.cursor');
|
||||
const statePath = path.join(targetRoot, 'ecc-install-state.json');
|
||||
const destinationPath = path.join(targetRoot, 'legacy-note.txt');
|
||||
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
||||
fs.writeFileSync(destinationPath, 'stale');
|
||||
|
||||
writeState(statePath, {
|
||||
adapter: { id: 'cursor-project', target: 'cursor', kind: 'project' },
|
||||
targetRoot,
|
||||
installStatePath: statePath,
|
||||
request: {
|
||||
profile: null,
|
||||
modules: [],
|
||||
legacyLanguages: ['typescript'],
|
||||
legacyMode: true,
|
||||
},
|
||||
resolution: {
|
||||
selectedModules: ['legacy-cursor-install'],
|
||||
skippedModules: [],
|
||||
},
|
||||
operations: [
|
||||
{
|
||||
kind: 'remove',
|
||||
moduleId: 'platform-configs',
|
||||
sourceRelativePath: '.cursor/legacy-note.txt',
|
||||
destinationPath,
|
||||
strategy: 'remove',
|
||||
ownership: 'managed',
|
||||
scaffoldOnly: false,
|
||||
},
|
||||
],
|
||||
source: {
|
||||
repoVersion: CURRENT_PACKAGE_VERSION,
|
||||
repoCommit: 'abc123',
|
||||
manifestVersion: CURRENT_MANIFEST_VERSION,
|
||||
},
|
||||
});
|
||||
|
||||
const result = repairInstalledStates({
|
||||
repoRoot: REPO_ROOT,
|
||||
homeDir,
|
||||
projectRoot,
|
||||
targets: ['cursor'],
|
||||
});
|
||||
|
||||
assert.strictEqual(result.results[0].status, 'repaired');
|
||||
assert.ok(!fs.existsSync(destinationPath));
|
||||
} finally {
|
||||
cleanup(homeDir);
|
||||
cleanup(projectRoot);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('uninstall restores JSON merged files from recorded previous content', () => {
|
||||
const homeDir = createTempDir('install-lifecycle-home-');
|
||||
const projectRoot = createTempDir('install-lifecycle-project-');
|
||||
|
||||
try {
|
||||
const targetRoot = path.join(projectRoot, '.cursor');
|
||||
const statePath = path.join(targetRoot, 'ecc-install-state.json');
|
||||
const destinationPath = path.join(targetRoot, 'hooks.json');
|
||||
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
||||
fs.writeFileSync(destinationPath, JSON.stringify({
|
||||
existing: true,
|
||||
managed: true,
|
||||
}, null, 2));
|
||||
|
||||
writeState(statePath, {
|
||||
adapter: { id: 'cursor-project', target: 'cursor', kind: 'project' },
|
||||
targetRoot,
|
||||
installStatePath: statePath,
|
||||
request: {
|
||||
profile: null,
|
||||
modules: [],
|
||||
legacyLanguages: ['typescript'],
|
||||
legacyMode: true,
|
||||
},
|
||||
resolution: {
|
||||
selectedModules: ['legacy-cursor-install'],
|
||||
skippedModules: [],
|
||||
},
|
||||
operations: [
|
||||
{
|
||||
kind: 'merge-json',
|
||||
moduleId: 'platform-configs',
|
||||
sourceRelativePath: '.cursor/hooks.json',
|
||||
destinationPath,
|
||||
strategy: 'merge-json',
|
||||
ownership: 'managed',
|
||||
scaffoldOnly: false,
|
||||
mergePayload: {
|
||||
managed: true,
|
||||
},
|
||||
previousContent: JSON.stringify({
|
||||
existing: true,
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
source: {
|
||||
repoVersion: CURRENT_PACKAGE_VERSION,
|
||||
repoCommit: 'abc123',
|
||||
manifestVersion: CURRENT_MANIFEST_VERSION,
|
||||
},
|
||||
});
|
||||
|
||||
const result = uninstallInstalledStates({
|
||||
homeDir,
|
||||
projectRoot,
|
||||
targets: ['cursor'],
|
||||
});
|
||||
|
||||
assert.strictEqual(result.results[0].status, 'uninstalled');
|
||||
assert.deepStrictEqual(JSON.parse(fs.readFileSync(destinationPath, 'utf8')), {
|
||||
existing: true,
|
||||
});
|
||||
assert.ok(!fs.existsSync(statePath));
|
||||
} finally {
|
||||
cleanup(homeDir);
|
||||
cleanup(projectRoot);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('uninstall restores rendered template files from recorded previous content', () => {
|
||||
const tempDir = createTempDir('install-lifecycle-');
|
||||
|
||||
try {
|
||||
const targetRoot = path.join(tempDir, '.claude');
|
||||
const statePath = path.join(targetRoot, 'ecc', 'install-state.json');
|
||||
const destinationPath = path.join(targetRoot, 'plugin.json');
|
||||
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
||||
fs.writeFileSync(destinationPath, '{"generated":true}\n');
|
||||
|
||||
writeInstallState(statePath, createInstallState({
|
||||
adapter: { id: 'claude-home', target: 'claude', kind: 'home' },
|
||||
targetRoot,
|
||||
installStatePath: statePath,
|
||||
request: {
|
||||
profile: 'core',
|
||||
modules: ['platform-configs'],
|
||||
includeComponents: [],
|
||||
excludeComponents: [],
|
||||
legacyLanguages: [],
|
||||
legacyMode: false,
|
||||
},
|
||||
resolution: {
|
||||
selectedModules: ['platform-configs'],
|
||||
skippedModules: [],
|
||||
},
|
||||
source: {
|
||||
repoVersion: '1.8.0',
|
||||
repoCommit: 'abc123',
|
||||
manifestVersion: 1,
|
||||
},
|
||||
operations: [
|
||||
{
|
||||
kind: 'render-template',
|
||||
moduleId: 'platform-configs',
|
||||
sourceRelativePath: '.claude/plugin.json.template',
|
||||
destinationPath,
|
||||
strategy: 'render-template',
|
||||
ownership: 'managed',
|
||||
scaffoldOnly: false,
|
||||
renderedContent: '{"generated":true}\n',
|
||||
previousContent: '{"existing":true}\n',
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
const result = uninstallInstalledStates({
|
||||
homeDir: tempDir,
|
||||
projectRoot: tempDir,
|
||||
targets: ['claude'],
|
||||
});
|
||||
|
||||
assert.strictEqual(result.summary.uninstalledCount, 1);
|
||||
assert.strictEqual(fs.readFileSync(destinationPath, 'utf8'), '{"existing":true}\n');
|
||||
assert.ok(!fs.existsSync(statePath));
|
||||
} finally {
|
||||
cleanup(tempDir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('uninstall restores files removed during install when previous content is recorded', () => {
|
||||
const homeDir = createTempDir('install-lifecycle-home-');
|
||||
const projectRoot = createTempDir('install-lifecycle-project-');
|
||||
|
||||
try {
|
||||
const targetRoot = path.join(projectRoot, '.cursor');
|
||||
const statePath = path.join(targetRoot, 'ecc-install-state.json');
|
||||
const destinationPath = path.join(targetRoot, 'legacy-note.txt');
|
||||
fs.mkdirSync(targetRoot, { recursive: true });
|
||||
|
||||
writeState(statePath, {
|
||||
adapter: { id: 'cursor-project', target: 'cursor', kind: 'project' },
|
||||
targetRoot,
|
||||
installStatePath: statePath,
|
||||
request: {
|
||||
profile: null,
|
||||
modules: [],
|
||||
legacyLanguages: ['typescript'],
|
||||
legacyMode: true,
|
||||
},
|
||||
resolution: {
|
||||
selectedModules: ['legacy-cursor-install'],
|
||||
skippedModules: [],
|
||||
},
|
||||
operations: [
|
||||
{
|
||||
kind: 'remove',
|
||||
moduleId: 'platform-configs',
|
||||
sourceRelativePath: '.cursor/legacy-note.txt',
|
||||
destinationPath,
|
||||
strategy: 'remove',
|
||||
ownership: 'managed',
|
||||
scaffoldOnly: false,
|
||||
previousContent: 'restore me\n',
|
||||
},
|
||||
],
|
||||
source: {
|
||||
repoVersion: CURRENT_PACKAGE_VERSION,
|
||||
repoCommit: 'abc123',
|
||||
manifestVersion: CURRENT_MANIFEST_VERSION,
|
||||
},
|
||||
});
|
||||
|
||||
const result = uninstallInstalledStates({
|
||||
homeDir,
|
||||
projectRoot,
|
||||
targets: ['cursor'],
|
||||
});
|
||||
|
||||
assert.strictEqual(result.results[0].status, 'uninstalled');
|
||||
assert.strictEqual(fs.readFileSync(destinationPath, 'utf8'), 'restore me\n');
|
||||
assert.ok(!fs.existsSync(statePath));
|
||||
} finally {
|
||||
cleanup(homeDir);
|
||||
cleanup(projectRoot);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user