Fix canonical worktree briefs and add guided setup and safe reinstall

This commit is contained in:
thePR0M3TH3AN committed 2026-09-30 15:14:10 -04:00
1 parent f72daadcba
commit c11754ab18
13 files changed
+311 -13

No files matched your search

+38
View File
@@ -0,0 +1,38 @@
import { execFileSync } from 'node:child_process';
import { existsSync, readFileSync, realpathSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { projectStatePath } from './paths.mjs';
import { TorchError } from './errors.mjs';
const read = path => JSON.parse(readFileSync(path, 'utf8'));
const git = (root, args) => execFileSync('git', ['-C', root, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
// A worktree's copied configuration is not authoritative. Only registered,
// same-installation worktrees may resolve to the current canonical definition.
export function resolveInstalledProjectRoot(root, env = process.env) {
root = realpathSync(root);
const path = join(root, '.torch', 'install-manifest.json');
if (!existsSync(path)) return root;
const manifest = read(path);
const stateRoot = projectStatePath(manifest.projectId, env);
const local = manifest.external?.find(entry => entry.type === 'local-state');
if (!local || resolve(local.path) !== resolve(stateRoot)) {
throw new TorchError('Installation state path does not match this project', { code: 'LOCAL_STATE_PATH_MISMATCH' });
}
const metadata = read(join(stateRoot, 'project.json'));
if (metadata.projectId !== manifest.projectId) {
throw new TorchError('Installation metadata belongs to another project', { code: 'LOCAL_STATE_METADATA_MISMATCH' });
}
const canonical = realpathSync(metadata.root);
if (canonical === root) return root;
const current = read(join(canonical, '.torch', 'install-manifest.json'));
const registered = current.external?.find(entry => entry.type === 'worktree' && existsSync(entry.path) && realpathSync(entry.path) === root);
const common = directory => realpathSync(resolve(directory, git(directory, ['rev-parse', '--git-common-dir'])));
const members = git(canonical, ['worktree', 'list', '--porcelain']).split('\n').filter(line => line.startsWith('worktree ')).map(line => realpathSync(line.slice(9)));
if (current.projectId !== manifest.projectId || current.installationId !== manifest.installationId
|| !registered || common(root) !== common(canonical) || !members.includes(root)
|| git(root, ['branch', '--show-current']) !== registered.branch) {
throw new TorchError('This checkout is not a registered worktree of the installation', { code: 'UNREGISTERED_PROJECT_WORKTREE' });
}
return canonical;
}
+55
View File
@@ -0,0 +1,55 @@
import { execFileSync } from 'node:child_process';
import { readFileSync, renameSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { inspectRepository } from './git.mjs';
import { readInstallManifest } from './install.mjs';
import { projectStatePath } from './paths.mjs';
import { resolveInstalledProjectRoot } from './project-root.mjs';
import { createWorktrees, planWorktrees } from './worktrees.mjs';
import { TorchError } from './errors.mjs';
const git = (root, args) => execFileSync('git', ['-C', root, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
export function setupProject({ repository, env = process.env, authorized = false, dryRun = false, parentOverride }) {
const root = resolveInstalledProjectRoot(repository.root, env);
repository = inspectRepository(root);
const manifest = readInstallManifest(root);
const owned = [...new Set([...manifest.created.map(record => record.path), '.torch/install-manifest.json'])];
const changed = owned.filter(path => git(root, ['status', '--porcelain=v1', '--', path]));
const staged = git(root, ['diff', '--cached', '--name-only']);
if (staged) throw new TorchError('Setup requires an empty staging area; preserve and finish your existing staged work first', { code: 'SETUP_STAGED_WORK', details: staged });
const unrelated = repository.dirtyEntries.filter(entry => !owned.includes(entry.slice(3)));
const worktrees = planWorktrees({ repository: { ...repository, dirtyEntries: unrelated }, parentOverride });
const plan = { action: 'setup', projectId: manifest.projectId, checkpointPaths: changed, worktrees, canProceed: worktrees.canProceed, sessionsStarted: false, mutationPerformed: false };
if (dryRun) return plan;
if (!authorized) throw new TorchError('Setup commits TORCH-owned configuration and provisions worktrees; review --dry-run then use --yes', { code: 'APPROVAL_REQUIRED' });
if (!plan.canProceed) throw new TorchError('Setup has blockers', { code: 'SETUP_BLOCKED', details: worktrees.conflicts });
const checkpoint = (paths, message) => {
const pending = paths.filter(path => git(root, ['status', '--porcelain=v1', '--', path]));
if (!pending.length) return null;
git(root, ['add', '--', ...pending]);
git(root, ['commit', '--only', '-m', message, '--', ...pending]);
return git(root, ['rev-parse', 'HEAD']);
};
const configurationCommit = checkpoint(changed, 'Checkpoint approved TORCH configuration');
const provisioned = createWorktrees({ repository: inspectRepository(root), parentOverride });
const registrationCommit = checkpoint(['.torch/install-manifest.json'], 'Checkpoint TORCH worktree registration');
return { ...plan, configurationCommit, registrationCommit, provisioned, mutationPerformed: Boolean(configurationCommit || registrationCommit || provisioned.mutationPerformed) };
}
export function restoreProject({ repository, env = process.env, authorized = false, dryRun = false }) {
const root = resolveInstalledProjectRoot(repository.root, env);
const manifest = readInstallManifest(root);
const path = join(projectStatePath(manifest.projectId, env), 'project.json');
const metadata = JSON.parse(readFileSync(path, 'utf8'));
const result = { action: 'restore-installation', projectId: manifest.projectId, installationId: manifest.installationId, wasDetached: Boolean(metadata.detachedAt), preserved: ['configuration', 'worktrees', 'branches', 'session history', 'provider profiles'], sessionsStarted: false, mutationPerformed: false };
if (dryRun) return result;
if (!authorized) throw new TorchError('Restoring a retained installation requires --yes', { code: 'APPROVAL_REQUIRED' });
if (metadata.detachedAt) {
const temporary = `${path}.restore-${process.pid}`;
writeFileSync(temporary, `${JSON.stringify({ ...metadata, detachedAt: null, attachedAt: new Date().toISOString() }, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
renameSync(temporary, path);
result.mutationPerformed = true;
}
return result;
}