mirror of
https://github.com/affaan-m/ECC.git
synced 2026-08-17 21:15:40 +02:00
Use no-follow file descriptors for legacy Codex snapshots, verification, restoration, and marker cleanup. Quarantine candidate removals and verify inode identity before deletion. Carry the lifecycle runner as a verified artifact so privileged release workflows never dynamically check out and execute an output-selected revision.
573 lines
20 KiB
JavaScript
573 lines
20 KiB
JavaScript
'use strict';
|
|
|
|
const crypto = require('crypto');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const { execFileSync } = require('child_process');
|
|
|
|
const SCHEMA = 'ecc.codex-legacy-sync.v1';
|
|
const BEGIN_MARKER = '<!-- BEGIN ECC -->';
|
|
const END_MARKER = '<!-- END ECC -->';
|
|
|
|
function getStatePath(codexHome) {
|
|
return path.join(codexHome, 'ecc', 'legacy-sync-state.json');
|
|
}
|
|
|
|
function openRegularFileNoFollow(filePath, writable = false) {
|
|
const noFollow = fs.constants.O_NOFOLLOW || 0;
|
|
const flags = (writable ? fs.constants.O_RDWR : fs.constants.O_RDONLY) | noFollow;
|
|
let descriptor;
|
|
try {
|
|
descriptor = fs.openSync(filePath, flags);
|
|
} catch (error) {
|
|
if (error.code === 'ENOENT') return null;
|
|
if (error.code === 'ELOOP') {
|
|
throw new Error(`Refusing to manage non-regular legacy sync path: ${filePath}`);
|
|
}
|
|
throw error;
|
|
}
|
|
const stat = fs.fstatSync(descriptor);
|
|
if (!stat.isFile()) {
|
|
fs.closeSync(descriptor);
|
|
throw new Error(`Refusing to manage non-regular legacy sync path: ${filePath}`);
|
|
}
|
|
return { descriptor, stat };
|
|
}
|
|
|
|
function readRegularFileNoFollow(filePath, encoding = null) {
|
|
const opened = openRegularFileNoFollow(filePath);
|
|
if (!opened) return null;
|
|
try {
|
|
return {
|
|
content: fs.readFileSync(opened.descriptor, encoding || undefined),
|
|
mode: opened.stat.mode & 0o777,
|
|
};
|
|
} finally {
|
|
fs.closeSync(opened.descriptor);
|
|
}
|
|
}
|
|
|
|
function replaceOpenedRegularFile(opened, content, mode = null) {
|
|
const buffer = Buffer.isBuffer(content) ? content : Buffer.from(content);
|
|
fs.ftruncateSync(opened.descriptor, 0);
|
|
fs.writeSync(opened.descriptor, buffer, 0, buffer.length, 0);
|
|
if (mode) fs.fchmodSync(opened.descriptor, mode);
|
|
fs.fsyncSync(opened.descriptor);
|
|
}
|
|
|
|
function createRegularFileNoFollow(filePath, content, mode = 0o600) {
|
|
const noFollow = fs.constants.O_NOFOLLOW || 0;
|
|
const flags = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | noFollow;
|
|
const descriptor = fs.openSync(filePath, flags, mode);
|
|
try {
|
|
const stat = fs.fstatSync(descriptor);
|
|
if (!stat.isFile()) {
|
|
throw new Error(`Refusing to create non-regular legacy sync path: ${filePath}`);
|
|
}
|
|
fs.writeFileSync(descriptor, content);
|
|
fs.fchmodSync(descriptor, mode);
|
|
fs.fsyncSync(descriptor);
|
|
} finally {
|
|
fs.closeSync(descriptor);
|
|
}
|
|
}
|
|
|
|
function removeOpenedRegularFile(filePath, opened) {
|
|
const quarantineDir = fs.mkdtempSync(path.join(path.dirname(filePath), '.ecc-remove-'));
|
|
const quarantinePath = path.join(quarantineDir, path.basename(filePath));
|
|
fs.renameSync(filePath, quarantinePath);
|
|
const quarantined = openRegularFileNoFollow(quarantinePath);
|
|
const openedStat = fs.fstatSync(opened.descriptor, { bigint: true });
|
|
const quarantinedStat = fs.fstatSync(quarantined.descriptor, { bigint: true });
|
|
fs.closeSync(quarantined.descriptor);
|
|
if (quarantinedStat.dev !== openedStat.dev || quarantinedStat.ino !== openedStat.ino) {
|
|
try {
|
|
fs.linkSync(quarantinePath, filePath);
|
|
fs.unlinkSync(quarantinePath);
|
|
fs.rmdirSync(quarantineDir);
|
|
} catch (_restoreError) {
|
|
throw new Error(
|
|
`Legacy sync path changed before removal; preserved replacement at ${quarantinePath}`
|
|
);
|
|
}
|
|
throw new Error(`Legacy sync path changed before removal: ${filePath}`);
|
|
}
|
|
fs.unlinkSync(quarantinePath);
|
|
fs.rmdirSync(quarantineDir);
|
|
}
|
|
|
|
function atomicWriteJson(filePath, value) {
|
|
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
fs.writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
fs.renameSync(tempPath, filePath);
|
|
}
|
|
|
|
function readState(statePath) {
|
|
const snapshot = readRegularFileNoFollow(statePath, 'utf8');
|
|
if (!snapshot) throw new Error(`Legacy Codex sync state not found at ${statePath}`);
|
|
return parseState(snapshot.content, statePath);
|
|
}
|
|
|
|
function readStateIfPresent(statePath) {
|
|
const snapshot = readRegularFileNoFollow(statePath, 'utf8');
|
|
return snapshot ? parseState(snapshot.content, statePath) : null;
|
|
}
|
|
|
|
function parseState(content, statePath) {
|
|
const state = JSON.parse(content);
|
|
if (state.schema !== SCHEMA || !Array.isArray(state.paths)) {
|
|
throw new Error(`Invalid legacy Codex sync state at ${statePath}`);
|
|
}
|
|
return state;
|
|
}
|
|
|
|
function hasUnsafeManagedAncestor(filePath, codexHome) {
|
|
const relativePath = path.relative(codexHome, filePath);
|
|
if (relativePath === '' || relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
|
|
return relativePath !== '';
|
|
}
|
|
const segments = relativePath.split(path.sep).slice(0, -1);
|
|
let currentPath = codexHome;
|
|
for (const segment of [null, ...segments]) {
|
|
if (segment !== null) currentPath = path.join(currentPath, segment);
|
|
try {
|
|
const stat = fs.lstatSync(currentPath);
|
|
if (stat.isSymbolicLink() || !stat.isDirectory()) return true;
|
|
} catch (error) {
|
|
if (error.code === 'ENOENT') break;
|
|
throw error;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function isWithinRoot(filePath, rootPath) {
|
|
const relativePath = path.relative(rootPath, filePath);
|
|
return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
|
|
}
|
|
|
|
function getTrustedRoot(state, filePath) {
|
|
const roots = Array.isArray(state.trustedRoots) && state.trustedRoots.length > 0
|
|
? state.trustedRoots
|
|
: [state.codexHome];
|
|
return roots
|
|
.map(rootPath => path.resolve(rootPath))
|
|
.find(rootPath => isWithinRoot(filePath, rootPath)) || null;
|
|
}
|
|
|
|
function snapshotLegacyPath(filePath) {
|
|
const snapshot = readRegularFileNoFollow(filePath);
|
|
const previousType = snapshot ? 'file' : 'missing';
|
|
return {
|
|
path: filePath,
|
|
installedSha256: null,
|
|
previousType,
|
|
previousContentBase64: snapshot ? snapshot.content.toString('base64') : null,
|
|
previousMode: snapshot ? snapshot.mode : null,
|
|
};
|
|
}
|
|
|
|
function assertInstalledStateUnmodified(state) {
|
|
for (const entry of state.paths) {
|
|
const filePath = path.resolve(entry.path);
|
|
const trustedRoot = getTrustedRoot(state, filePath);
|
|
if (!trustedRoot || hasUnsafeManagedAncestor(filePath, trustedRoot)) {
|
|
throw new Error(`Refusing to reuse unsafe legacy Codex ownership path: ${filePath}`);
|
|
}
|
|
const snapshot = readRegularFileNoFollow(filePath);
|
|
if (!entry.installedSha256) {
|
|
if (snapshot) throw new Error(`Refusing to replace modified legacy Codex artifact: ${filePath}`);
|
|
continue;
|
|
}
|
|
const digest = snapshot
|
|
? crypto.createHash('sha256').update(snapshot.content).digest('hex')
|
|
: null;
|
|
if (digest !== entry.installedSha256) {
|
|
throw new Error(`Refusing to replace modified legacy Codex artifact: ${filePath}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function beginLegacySyncState(options) {
|
|
const codexHome = path.resolve(options.codexHome);
|
|
const statePath = getStatePath(codexHome);
|
|
const configPath = path.join(codexHome, 'config.toml');
|
|
const agentsPath = path.join(codexHome, 'AGENTS.md');
|
|
const installedHooksPath = options.installedHooksPath ? path.resolve(options.installedHooksPath) : null;
|
|
const priorState = readStateIfPresent(statePath);
|
|
if (priorState && priorState.status !== 'installed') {
|
|
throw new Error(`Legacy Codex sync state requires recovery before reinstall: ${statePath}`);
|
|
}
|
|
if (priorState) assertInstalledStateUnmodified(priorState);
|
|
const trustedRoots = [...new Set([
|
|
codexHome,
|
|
...(Array.isArray(priorState?.trustedRoots) ? priorState.trustedRoots : []),
|
|
...(priorState?.installedHooksPath ? [priorState.installedHooksPath] : []),
|
|
...(installedHooksPath ? [installedHooksPath] : []),
|
|
].map(rootPath => path.resolve(rootPath)))];
|
|
const state = priorState ? {
|
|
...priorState,
|
|
status: 'applying',
|
|
updatedAt: new Date().toISOString(),
|
|
backupDir: options.backupDir ? path.resolve(options.backupDir) : priorState.backupDir,
|
|
installedHooksPath,
|
|
trustedRoots,
|
|
rollbackPreviousHooksPath: options.previousHooksPath || null,
|
|
rollbackPaths: priorState.paths.map(entry => snapshotLegacyPath(path.resolve(entry.path))),
|
|
previousInstalledState: priorState,
|
|
} : {
|
|
schema: SCHEMA,
|
|
status: 'applying',
|
|
createdAt: new Date().toISOString(),
|
|
codexHome,
|
|
backupDir: options.backupDir ? path.resolve(options.backupDir) : null,
|
|
previousHooksPath: options.previousHooksPath || null,
|
|
installedHooksPath,
|
|
trustedRoots,
|
|
before: {},
|
|
paths: [],
|
|
rollbackPaths: [],
|
|
};
|
|
|
|
for (const [key, filePath] of [['config', configPath], ['agents', agentsPath]]) {
|
|
if (priorState) break;
|
|
const snapshot = readRegularFileNoFollow(filePath, 'utf8');
|
|
state.before[key] = snapshot ? snapshot.content : null;
|
|
}
|
|
atomicWriteJson(statePath, state);
|
|
return statePath;
|
|
}
|
|
|
|
function recordLegacySyncPath(options) {
|
|
const state = readState(options.statePath);
|
|
const filePath = path.resolve(options.filePath);
|
|
const trustedRoot = getTrustedRoot(state, filePath);
|
|
if (!trustedRoot) {
|
|
throw new Error(`Refusing to record a legacy sync path outside trusted roots: ${filePath}`);
|
|
}
|
|
if (hasUnsafeManagedAncestor(filePath, trustedRoot)) {
|
|
throw new Error(`Refusing to manage legacy sync path through symlinked ancestor: ${filePath}`);
|
|
}
|
|
if (!state.paths.some(entry => entry.path === filePath)) {
|
|
const snapshot = snapshotLegacyPath(filePath);
|
|
state.paths.push(snapshot);
|
|
state.rollbackPaths = [...(state.rollbackPaths || []), { ...snapshot }];
|
|
atomicWriteJson(options.statePath, state);
|
|
}
|
|
}
|
|
|
|
function rollbackLegacyCodexSync(options) {
|
|
const state = readState(options.statePath);
|
|
const restoredPaths = [];
|
|
const retainedPaths = [];
|
|
|
|
const rollbackPaths = Array.isArray(state.rollbackPaths) ? state.rollbackPaths : state.paths;
|
|
for (const entry of [...rollbackPaths].reverse()) {
|
|
const filePath = path.resolve(entry.path);
|
|
const trustedRoot = getTrustedRoot(state, filePath);
|
|
if (!trustedRoot) {
|
|
retainedPaths.push(filePath);
|
|
continue;
|
|
}
|
|
if (hasUnsafeManagedAncestor(filePath, trustedRoot)) {
|
|
retainedPaths.push(filePath);
|
|
continue;
|
|
}
|
|
let opened = null;
|
|
try {
|
|
opened = openRegularFileNoFollow(filePath, true);
|
|
} catch (_error) {
|
|
retainedPaths.push(filePath);
|
|
continue;
|
|
}
|
|
if (entry.previousType === 'file' && typeof entry.previousContentBase64 === 'string') {
|
|
const previousContent = Buffer.from(entry.previousContentBase64, 'base64');
|
|
const previousMode = entry.previousMode || 0o600;
|
|
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
if (opened) {
|
|
try {
|
|
replaceOpenedRegularFile(opened, previousContent, previousMode);
|
|
} finally {
|
|
fs.closeSync(opened.descriptor);
|
|
}
|
|
} else {
|
|
createRegularFileNoFollow(filePath, previousContent, previousMode);
|
|
}
|
|
restoredPaths.push(filePath);
|
|
} else if (entry.previousType === 'missing' || entry.previousType === undefined) {
|
|
if (opened) {
|
|
try {
|
|
removeOpenedRegularFile(filePath, opened);
|
|
} finally {
|
|
fs.closeSync(opened.descriptor);
|
|
}
|
|
}
|
|
restoredPaths.push(filePath);
|
|
} else {
|
|
if (opened) fs.closeSync(opened.descriptor);
|
|
retainedPaths.push(filePath);
|
|
}
|
|
}
|
|
|
|
if (state.installedHooksPath) {
|
|
const getHooks = options.getGlobalHooksPath || defaultGetHooksPath;
|
|
const setHooks = options.setGlobalHooksPath || defaultSetHooksPath;
|
|
const currentHooks = getHooks();
|
|
if (currentHooks && path.resolve(currentHooks) === path.resolve(state.installedHooksPath)) {
|
|
setHooks(state.rollbackPreviousHooksPath ?? state.previousHooksPath ?? '');
|
|
} else if (currentHooks && currentHooks !== (state.rollbackPreviousHooksPath ?? state.previousHooksPath)) {
|
|
retainedPaths.push(`git:core.hooksPath=${currentHooks}`);
|
|
}
|
|
}
|
|
|
|
if (retainedPaths.length === 0) {
|
|
if (state.previousInstalledState) {
|
|
atomicWriteJson(options.statePath, state.previousInstalledState);
|
|
} else {
|
|
fs.rmSync(options.statePath, { force: true });
|
|
}
|
|
}
|
|
return {
|
|
status: retainedPaths.length === 0 ? 'rolled-back' : 'partial',
|
|
statePath: options.statePath,
|
|
restoredPaths,
|
|
retainedPaths: [...new Set(retainedPaths)].sort(),
|
|
};
|
|
}
|
|
|
|
function finalizeLegacySyncState(options) {
|
|
const state = readState(options.statePath);
|
|
state.status = 'installed';
|
|
state.installedAt = new Date().toISOString();
|
|
delete state.rollbackPaths;
|
|
delete state.rollbackPreviousHooksPath;
|
|
delete state.previousInstalledState;
|
|
state.paths = state.paths.map(entry => {
|
|
const trustedRoot = getTrustedRoot(state, path.resolve(entry.path));
|
|
let installedSha256 = null;
|
|
if (trustedRoot && !hasUnsafeManagedAncestor(entry.path, trustedRoot)) {
|
|
try {
|
|
const snapshot = readRegularFileNoFollow(entry.path);
|
|
installedSha256 = snapshot
|
|
? crypto.createHash('sha256').update(snapshot.content).digest('hex')
|
|
: null;
|
|
} catch (_error) {
|
|
installedSha256 = null;
|
|
}
|
|
}
|
|
return { ...entry, installedSha256 };
|
|
});
|
|
atomicWriteJson(options.statePath, state);
|
|
return state;
|
|
}
|
|
|
|
function stripMarkerBlock(content) {
|
|
const markers = [];
|
|
let fence = null;
|
|
let offset = 0;
|
|
for (const lineWithEnding of content.match(/.*(?:\r?\n|$)/g) || []) {
|
|
if (lineWithEnding === '') continue;
|
|
const line = lineWithEnding.replace(/\r?\n$/, '');
|
|
const fenceMatch = line.match(/^\s*(`{3,}|~{3,})(.*)$/);
|
|
if (fenceMatch) {
|
|
const run = fenceMatch[1];
|
|
const marker = run[0];
|
|
if (!fence) {
|
|
fence = { marker, length: run.length };
|
|
} else if (
|
|
marker === fence.marker
|
|
&& run.length >= fence.length
|
|
&& fenceMatch[2].trim() === ''
|
|
) {
|
|
fence = null;
|
|
}
|
|
} else if (!fence && (line === BEGIN_MARKER || line === END_MARKER)) {
|
|
markers.push({ marker: line, index: offset });
|
|
}
|
|
offset += lineWithEnding.length;
|
|
}
|
|
const begins = markers.filter(match => match.marker === BEGIN_MARKER);
|
|
const ends = markers.filter(match => match.marker === END_MARKER);
|
|
if (begins.length !== 1 || ends.length !== 1 || ends[0].index < begins[0].index) {
|
|
return content;
|
|
}
|
|
const suffixStart = ends[0].index + END_MARKER.length;
|
|
const suffixWithLineEnding = content.slice(suffixStart).replace(/^\r?\n/, '');
|
|
return `${content.slice(0, begins[0].index)}${suffixWithLineEnding}`;
|
|
}
|
|
|
|
function defaultGetHooksPath() {
|
|
try {
|
|
return execFileSync('git', ['config', '--global', '--get', 'core.hooksPath'], {
|
|
encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000,
|
|
}).trim();
|
|
} catch (_error) {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
function defaultSetHooksPath(value) {
|
|
const args = value
|
|
? ['config', '--global', 'core.hooksPath', value]
|
|
: ['config', '--global', '--unset-all', 'core.hooksPath'];
|
|
try {
|
|
execFileSync('git', args, { stdio: 'ignore', timeout: 5000 });
|
|
} catch (error) {
|
|
if (value || error.status !== 5) throw error;
|
|
}
|
|
}
|
|
|
|
function listLegacyCandidates(codexHome) {
|
|
const candidates = [];
|
|
const promptsDir = path.join(codexHome, 'prompts');
|
|
if (fs.existsSync(promptsDir)) {
|
|
for (const entry of fs.readdirSync(promptsDir)) {
|
|
if (entry.startsWith('ecc-') || entry.startsWith('ecc_') || entry.includes('ecc-rules-pack')) {
|
|
candidates.push(path.join(promptsDir, entry));
|
|
}
|
|
}
|
|
}
|
|
for (const relativePath of [
|
|
'docs/CODEX-NAVIGATION-GUIDE.md',
|
|
'docs/COMMAND-AGENT-MAP.md',
|
|
'COMMANDS-QUICK-REF.md',
|
|
'CONTRIBUTING.md',
|
|
'.github/PULL_REQUEST_TEMPLATE.md',
|
|
'ecc-prompts-manifest.txt',
|
|
'ecc-extension-prompts-manifest.txt',
|
|
]) {
|
|
const candidate = path.join(codexHome, relativePath);
|
|
if (fs.existsSync(candidate)) candidates.push(candidate);
|
|
}
|
|
return candidates;
|
|
}
|
|
|
|
function uninstallLegacyCodexSync(options = {}) {
|
|
const codexHome = path.resolve(options.codexHome || process.env.CODEX_HOME || path.join(process.env.HOME || os.homedir(), '.codex'));
|
|
const statePath = getStatePath(codexHome);
|
|
const dryRun = options.dryRun === true;
|
|
const retainedPaths = [];
|
|
const plannedRemovals = [];
|
|
const removedPaths = [];
|
|
const agentsPath = path.join(codexHome, 'AGENTS.md');
|
|
const state = readStateIfPresent(statePath);
|
|
|
|
if (!state) {
|
|
let openedAgents = null;
|
|
try {
|
|
openedAgents = openRegularFileNoFollow(agentsPath, !dryRun);
|
|
if (openedAgents) {
|
|
const content = fs.readFileSync(openedAgents.descriptor, 'utf8');
|
|
const stripped = stripMarkerBlock(content);
|
|
if (stripped !== content) {
|
|
plannedRemovals.push(`${agentsPath}#ecc-marker-block`);
|
|
if (!dryRun) replaceOpenedRegularFile(openedAgents, stripped, openedAgents.stat.mode & 0o777);
|
|
}
|
|
}
|
|
} catch (_error) {
|
|
retainedPaths.push(agentsPath);
|
|
} finally {
|
|
if (openedAgents) fs.closeSync(openedAgents.descriptor);
|
|
}
|
|
retainedPaths.push(...listLegacyCandidates(codexHome));
|
|
return {
|
|
status: dryRun ? 'planned' : retainedPaths.length > 0 ? 'partial' : plannedRemovals.length > 0 ? 'uninstalled' : 'not-found',
|
|
statePath: null,
|
|
plannedRemovals,
|
|
removedPaths,
|
|
retainedPaths: [...new Set(retainedPaths)].sort(),
|
|
warnings: retainedPaths.length > 0
|
|
? ['Legacy Codex artifacts without an ownership manifest were preserved for manual review.']
|
|
: [],
|
|
};
|
|
}
|
|
|
|
for (const entry of state.paths) {
|
|
const filePath = path.resolve(entry.path);
|
|
const trustedRoot = getTrustedRoot(state, filePath);
|
|
if (!trustedRoot) {
|
|
retainedPaths.push(filePath);
|
|
continue;
|
|
}
|
|
if (hasUnsafeManagedAncestor(filePath, trustedRoot)) {
|
|
retainedPaths.push(filePath);
|
|
continue;
|
|
}
|
|
let opened = null;
|
|
try {
|
|
opened = openRegularFileNoFollow(filePath, !dryRun);
|
|
} catch (_error) {
|
|
retainedPaths.push(filePath);
|
|
continue;
|
|
}
|
|
if (!opened) continue;
|
|
const currentContent = fs.readFileSync(opened.descriptor);
|
|
const matches = entry.installedSha256
|
|
? crypto.createHash('sha256').update(currentContent).digest('hex') === entry.installedSha256
|
|
: false;
|
|
if (!matches) {
|
|
fs.closeSync(opened.descriptor);
|
|
retainedPaths.push(filePath);
|
|
continue;
|
|
}
|
|
plannedRemovals.push(filePath);
|
|
if (!dryRun) {
|
|
if (entry.previousType === 'file' && typeof entry.previousContentBase64 === 'string') {
|
|
replaceOpenedRegularFile(
|
|
opened,
|
|
Buffer.from(entry.previousContentBase64, 'base64'),
|
|
entry.previousMode || 0o600
|
|
);
|
|
} else if (entry.previousType === 'missing' || entry.previousType === undefined) {
|
|
removeOpenedRegularFile(filePath, opened);
|
|
} else {
|
|
fs.closeSync(opened.descriptor);
|
|
retainedPaths.push(filePath);
|
|
continue;
|
|
}
|
|
removedPaths.push(filePath);
|
|
}
|
|
fs.closeSync(opened.descriptor);
|
|
}
|
|
|
|
if (state.installedHooksPath) {
|
|
const getHooks = options.getGlobalHooksPath || defaultGetHooksPath;
|
|
const setHooks = options.setGlobalHooksPath || defaultSetHooksPath;
|
|
const currentHooks = getHooks();
|
|
if (path.resolve(currentHooks || '.') === path.resolve(state.installedHooksPath)) {
|
|
if (!dryRun) setHooks(state.previousHooksPath || '');
|
|
} else if (currentHooks) {
|
|
retainedPaths.push(`git:core.hooksPath=${currentHooks}`);
|
|
}
|
|
}
|
|
|
|
if (!dryRun && retainedPaths.length === 0) {
|
|
fs.rmSync(statePath, { force: true });
|
|
}
|
|
return {
|
|
status: dryRun ? 'planned' : retainedPaths.length > 0 ? 'partial' : 'uninstalled',
|
|
statePath,
|
|
plannedRemovals: [...new Set(plannedRemovals)],
|
|
removedPaths,
|
|
retainedPaths: [...new Set(retainedPaths)].sort(),
|
|
warnings: retainedPaths.length > 0
|
|
? ['Modified or unverifiable legacy Codex artifacts were preserved.']
|
|
: [],
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
BEGIN_MARKER,
|
|
END_MARKER,
|
|
SCHEMA,
|
|
beginLegacySyncState,
|
|
finalizeLegacySyncState,
|
|
getStatePath,
|
|
recordLegacySyncPath,
|
|
rollbackLegacyCodexSync,
|
|
stripMarkerBlock,
|
|
uninstallLegacyCodexSync,
|
|
};
|