mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-18 07:37:59 +02:00
fix(install): preserve user files and honor uninstall previews
Generalize PR #2981 ownership protection to every managed target. Reject mismatched target state and preserve files that appear during writes or failed-install checkpoints. Keep prior hashes for managed files a failed attempt never writes. Integrate PR #2980 preview wording and global dry-run propagation, with PR #2956 fail-closed environment validation and CLI/legacy regression coverage. Fixes #2964. Fixes #2952. Co-authored-by: ilkmajans-cpu <ilkmajans-cpu@users.noreply.github.com> Co-authored-by: wellkilo <wellkilo@foxmail.com>
This commit is contained in:
co-authored by
ilkmajans-cpu
wellkilo
parent
967a5fa4ba
commit
59b74901c4
+85
-12
@@ -3,10 +3,12 @@
|
||||
*/
|
||||
|
||||
const assert = require('assert');
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
const { createInstallState, writeInstallState } = require('../../scripts/lib/install-state');
|
||||
|
||||
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'ecc.js');
|
||||
|
||||
@@ -14,23 +16,21 @@ function runCli(args, options = {}) {
|
||||
const envOverrides = {
|
||||
...(options.env || {}),
|
||||
};
|
||||
|
||||
if (typeof envOverrides.HOME === 'string' && !('USERPROFILE' in envOverrides)) {
|
||||
envOverrides.USERPROFILE = envOverrides.HOME;
|
||||
}
|
||||
|
||||
if (typeof envOverrides.USERPROFILE === 'string' && !('HOME' in envOverrides)) {
|
||||
envOverrides.HOME = envOverrides.USERPROFILE;
|
||||
}
|
||||
const inheritedEnv = Object.fromEntries(
|
||||
Object.entries(process.env).filter(([key]) => key !== 'ECC_DRY_RUN')
|
||||
);
|
||||
const homeAlias = typeof envOverrides.HOME === 'string' && !('USERPROFILE' in envOverrides)
|
||||
? { USERPROFILE: envOverrides.HOME }
|
||||
: typeof envOverrides.USERPROFILE === 'string' && !('HOME' in envOverrides)
|
||||
? { HOME: envOverrides.USERPROFILE }
|
||||
: {};
|
||||
const env = { ...inheritedEnv, ...envOverrides, ...homeAlias };
|
||||
|
||||
return spawnSync('node', [SCRIPT, ...args], {
|
||||
encoding: 'utf8',
|
||||
cwd: options.cwd || process.cwd(),
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
env: {
|
||||
...process.env,
|
||||
...envOverrides,
|
||||
},
|
||||
env,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -153,6 +153,79 @@ function main() {
|
||||
const payload = parseJson(result.stdout);
|
||||
assert.deepStrictEqual(payload.records, []);
|
||||
}],
|
||||
['keeps uninstall read-only when global --dry-run precedes the command', () => {
|
||||
const homeDir = createTempDir('ecc-cli-uninstall-home-');
|
||||
const projectRoot = createTempDir('ecc-cli-uninstall-project-');
|
||||
|
||||
try {
|
||||
const targetRoot = path.join(projectRoot, '.cursor');
|
||||
const statePath = path.join(targetRoot, 'ecc-install-state.json');
|
||||
const managedPath = path.join(targetRoot, 'managed-rule.md');
|
||||
const managedContent = 'managed\n';
|
||||
fs.mkdirSync(targetRoot, { recursive: true });
|
||||
fs.writeFileSync(managedPath, managedContent);
|
||||
writeInstallState(statePath, createInstallState({
|
||||
adapter: { id: 'cursor-project', target: 'cursor', kind: 'project' },
|
||||
targetRoot,
|
||||
installStatePath: statePath,
|
||||
request: {
|
||||
profile: null,
|
||||
modules: [],
|
||||
includeComponents: [],
|
||||
excludeComponents: [],
|
||||
legacyLanguages: ['typescript'],
|
||||
legacyMode: true,
|
||||
},
|
||||
resolution: {
|
||||
selectedModules: ['legacy-cursor-install'],
|
||||
skippedModules: [],
|
||||
},
|
||||
source: {
|
||||
repoVersion: null,
|
||||
repoCommit: null,
|
||||
manifestVersion: 1,
|
||||
},
|
||||
operations: [{
|
||||
kind: 'copy-file',
|
||||
moduleId: 'rules-core',
|
||||
sourceRelativePath: 'rules/common/coding-style.md',
|
||||
destinationPath: managedPath,
|
||||
strategy: 'preserve-relative-path',
|
||||
ownership: 'managed',
|
||||
scaffoldOnly: false,
|
||||
contentSha256: crypto.createHash('sha256').update(managedContent).digest('hex'),
|
||||
}],
|
||||
}));
|
||||
|
||||
const jsonResult = runCli(['--dry-run', 'uninstall', '--target', 'cursor', '--json'], {
|
||||
cwd: projectRoot,
|
||||
env: { HOME: homeDir },
|
||||
});
|
||||
|
||||
assert.strictEqual(jsonResult.status, 0, jsonResult.stderr);
|
||||
const preview = parseJson(jsonResult.stdout);
|
||||
assert.strictEqual(preview.dryRun, true);
|
||||
assert.strictEqual(preview.results[0].status, 'planned');
|
||||
assert.deepStrictEqual(
|
||||
preview.results[0].plannedRemovals.map(candidate => fs.realpathSync(candidate)).sort(),
|
||||
[managedPath, statePath].map(candidate => fs.realpathSync(candidate)).sort()
|
||||
);
|
||||
|
||||
const humanResult = runCli(['--dry-run', 'uninstall', '--target', 'cursor'], {
|
||||
cwd: projectRoot,
|
||||
env: { HOME: homeDir },
|
||||
});
|
||||
assert.strictEqual(humanResult.status, 0, humanResult.stderr);
|
||||
assert.match(humanResult.stdout, /Status: WOULD UNINSTALL/);
|
||||
assert.match(humanResult.stdout, /Would remove: 2/);
|
||||
assert.doesNotMatch(humanResult.stdout, /Status: UNINSTALLED|Removed paths:/);
|
||||
assert.ok(fs.existsSync(managedPath), 'global dry-run must preserve managed files');
|
||||
assert.ok(fs.existsSync(statePath), 'global dry-run must preserve install-state');
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { force: true, recursive: true });
|
||||
fs.rmSync(projectRoot, { force: true, recursive: true });
|
||||
}
|
||||
}],
|
||||
['delegates auto-update command', () => {
|
||||
const homeDir = createTempDir('ecc-cli-home-');
|
||||
const projectRoot = createTempDir('ecc-cli-project-');
|
||||
|
||||
Reference in New Issue
Block a user