mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-11-03 03:11:57 +00:00
242 lines
12 KiB
JavaScript
242 lines
12 KiB
JavaScript
import { spawnSync } from 'node:child_process';
|
|
import { randomUUID } from 'node:crypto';
|
|
import { renameSync, writeFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { TorchError } from '../kernel/errors.mjs';
|
|
import { fileHash } from '../kernel/files.mjs';
|
|
import { loadProjectConfig } from '../kernel/config.mjs';
|
|
import { readInstallManifest, writeInstallManifest } from '../kernel/install.mjs';
|
|
|
|
function text(value, name) {
|
|
if (typeof value !== 'string' || !value.trim()) {
|
|
throw new TorchError(`${name} must be a non-empty string`, {
|
|
code: 'FORGE_INPUT_INVALID', details: { field: name },
|
|
});
|
|
}
|
|
return value.trim();
|
|
}
|
|
|
|
function git(root, args, { timeout = 5_000 } = {}) {
|
|
const result = spawnSync('git', ['-C', root, ...args], {
|
|
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout,
|
|
});
|
|
return {
|
|
status: result.status ?? 1, stdout: result.stdout?.trim() ?? '', stderr: result.stderr?.trim() ?? '',
|
|
error: result.error?.message ?? null,
|
|
};
|
|
}
|
|
|
|
function configPaths(repositoryRoot) {
|
|
return {
|
|
config: join(repositoryRoot, '.torch', 'torch.yaml'),
|
|
manifest: join(repositoryRoot, '.torch', 'install-manifest.json'),
|
|
};
|
|
}
|
|
|
|
function configurationOwned(repositoryRoot, manifest) {
|
|
const record = manifest.created.find((entry) => entry.path === '.torch/torch.yaml');
|
|
return Boolean(record && fileHash(configPaths(repositoryRoot).config) === record.sha256);
|
|
}
|
|
|
|
function remoteHead(repositoryRoot, remote, branch) {
|
|
const result = git(repositoryRoot, ['ls-remote', '--heads', remote, `refs/heads/${branch}`]);
|
|
if (result.status !== 0) return { available: false, commit: null, error: result.error ?? result.stderr };
|
|
const commit = result.stdout.split(/\s+/)[0] || null;
|
|
return { available: true, commit, error: null };
|
|
}
|
|
|
|
function writeConfiguration(repositoryRoot, config, manifest) {
|
|
const paths = configPaths(repositoryRoot);
|
|
const temporary = `${paths.config}.tmp-${process.pid}-${randomUUID()}`;
|
|
writeFileSync(temporary, `${JSON.stringify(config, null, 2)}\n`, {
|
|
encoding: 'utf8', mode: 0o600, flag: 'wx',
|
|
});
|
|
renameSync(temporary, paths.config);
|
|
const record = manifest.created.find((entry) => entry.path === '.torch/torch.yaml');
|
|
if (!record) throw new TorchError('Installation manifest does not own project configuration', { code: 'MANIFEST_INVALID' });
|
|
record.sha256 = fileHash(paths.config);
|
|
writeInstallManifest(repositoryRoot, manifest);
|
|
}
|
|
|
|
export function forgeStatus({ repositoryRoot } = {}) {
|
|
const config = loadProjectConfig(repositoryRoot);
|
|
const provider = config.forge?.provider ?? 'none';
|
|
if (provider === 'none') {
|
|
return {
|
|
mode: 'local-only', provider: 'none', remote: null, available: null,
|
|
synchronized: null, pendingSynchronization: false, localOperational: true,
|
|
mutationPerformed: false,
|
|
};
|
|
}
|
|
const remote = config.forge.remote;
|
|
const localCommit = git(repositoryRoot, ['rev-parse', config.project.main_branch]).stdout;
|
|
const observed = remoteHead(repositoryRoot, remote, config.project.main_branch);
|
|
return {
|
|
mode: observed.available ? 'forge-backed' : 'forge-degraded', provider, remote,
|
|
available: observed.available, localCommit, remoteCommit: observed.commit,
|
|
synchronized: observed.available && observed.commit === localCommit,
|
|
pendingSynchronization: !observed.available || observed.commit !== localCommit,
|
|
error: observed.error, localOperational: true, mutationPerformed: false,
|
|
};
|
|
}
|
|
|
|
export function planForgeSync({ repositoryRoot } = {}) {
|
|
const config = loadProjectConfig(repositoryRoot);
|
|
const remote = config.forge?.remote ?? null;
|
|
const branch = config.project.main_branch;
|
|
const localCommit = git(repositoryRoot, ['rev-parse', `refs/heads/${branch}`]).stdout;
|
|
const remotes = git(repositoryRoot, ['remote']).stdout.split('\n').filter(Boolean);
|
|
const blockers = [];
|
|
if (config.forge?.provider === 'none' || !remote) {
|
|
blockers.push({ code: 'FORGE_NOT_ATTACHED' });
|
|
}
|
|
if (config.repository.canonical.type !== 'remote'
|
|
|| config.repository.canonical.remote !== remote) {
|
|
blockers.push({ code: 'FORGE_CANONICAL_CHANGED', canonical: config.repository.canonical });
|
|
}
|
|
if (remote && !remotes.includes(remote)) blockers.push({ code: 'FORGE_REMOTE_MISSING', remote });
|
|
const branchCheck = git(repositoryRoot, ['check-ref-format', '--branch', branch]);
|
|
if (branchCheck.status !== 0) blockers.push({ code: 'FORGE_TARGET_BRANCH_INVALID', branch });
|
|
const observed = remote && remotes.includes(remote)
|
|
? remoteHead(repositoryRoot, remote, branch)
|
|
: { available: false, commit: null, error: 'configured remote is unavailable' };
|
|
if (!observed.available) blockers.push({ code: 'FORGE_UNAVAILABLE', remote, error: observed.error });
|
|
|
|
let disposition = 'blocked';
|
|
if (blockers.length === 0) {
|
|
if (observed.commit === localCommit) disposition = 'already-synchronized';
|
|
else if (!observed.commit) disposition = 'publish-new-branch';
|
|
else if (git(repositoryRoot, ['cat-file', '-e', `${observed.commit}^{commit}`], { timeout: 5_000 }).status !== 0) {
|
|
blockers.push({ code: 'FORGE_SYNC_FETCH_REQUIRED', remoteCommit: observed.commit,
|
|
message: `Fetch ${remote}/${branch} before syncing so TORCH can verify ancestry.` });
|
|
} else if (git(repositoryRoot, [
|
|
'merge-base', '--is-ancestor', localCommit, observed.commit,
|
|
], { timeout: 5_000 }).status === 0) {
|
|
blockers.push({ code: 'FORGE_REMOTE_AHEAD', localCommit, remoteCommit: observed.commit });
|
|
} else if (git(repositoryRoot, [
|
|
'merge-base', '--is-ancestor', observed.commit, localCommit,
|
|
], { timeout: 5_000 }).status === 0) disposition = 'fast-forward-remote';
|
|
else blockers.push({ code: 'FORGE_REMOTE_DIVERGED', localCommit, remoteCommit: observed.commit });
|
|
}
|
|
return {
|
|
action: 'sync-forge-canonical-branch', remote, branch, localCommit,
|
|
remoteCommit: observed.commit, disposition, requiresConfirmation: disposition !== 'already-synchronized',
|
|
blockers, canProceed: blockers.length === 0, mutationPerformed: false,
|
|
};
|
|
}
|
|
|
|
export function syncForgeRemote({ repositoryRoot, controlPlane, actorId = 'owner' } = {}) {
|
|
if (!controlPlane) throw new TorchError('Forge synchronization requires the project control plane for owner audit', {
|
|
code: 'FORGE_AUDIT_REQUIRED',
|
|
});
|
|
controlPlane.assertOwnerActor(actorId);
|
|
const plan = planForgeSync({ repositoryRoot });
|
|
if (!plan.canProceed) throw new TorchError('Forge synchronization is blocked', {
|
|
code: 'FORGE_SYNC_BLOCKED', details: plan.blockers,
|
|
});
|
|
if (plan.disposition === 'already-synchronized') {
|
|
return { ...plan, synchronized: true, publishedCommit: null, mutationPerformed: false };
|
|
}
|
|
const result = git(repositoryRoot, [
|
|
'push', '--porcelain', '--', plan.remote, `${plan.localCommit}:refs/heads/${plan.branch}`,
|
|
], { timeout: 60_000 });
|
|
if (result.status !== 0) throw new TorchError('Forge rejected canonical synchronization; no force push was attempted', {
|
|
code: 'FORGE_SYNC_PUSH_REJECTED', details: { remote: plan.remote, branch: plan.branch, stderr: result.stderr },
|
|
});
|
|
const observed = remoteHead(repositoryRoot, plan.remote, plan.branch);
|
|
const synchronized = observed.available && observed.commit === plan.localCommit;
|
|
controlPlane.auditOwnerAction({
|
|
actorId, operation: 'forge.sync', entityType: 'forge-publication', entityId: plan.localCommit,
|
|
details: {
|
|
remote: plan.remote, branch: plan.branch, previousRemoteCommit: plan.remoteCommit,
|
|
publishedCommit: plan.localCommit, observedRemoteCommit: observed.commit,
|
|
synchronized,
|
|
},
|
|
});
|
|
return {
|
|
...plan, synchronized, publishedCommit: plan.localCommit,
|
|
observedRemoteCommit: observed.commit, mutationPerformed: true,
|
|
output: result.stdout, pendingSynchronization: !synchronized,
|
|
};
|
|
}
|
|
|
|
export function planForgeAttach({ repositoryRoot, remote, provider = 'generic-git' } = {}) {
|
|
const selectedRemote = text(remote, 'remote');
|
|
const selectedProvider = text(provider, 'provider');
|
|
if (selectedProvider === 'none') throw new TorchError('Forge provider cannot be none during attach', { code: 'FORGE_INPUT_INVALID' });
|
|
const config = loadProjectConfig(repositoryRoot);
|
|
const manifest = readInstallManifest(repositoryRoot);
|
|
const blockers = [];
|
|
const remotes = git(repositoryRoot, ['remote']).stdout.split('\n').filter(Boolean);
|
|
if (!remotes.includes(selectedRemote)) blockers.push({ code: 'FORGE_REMOTE_MISSING', remote: selectedRemote });
|
|
if (config.forge.provider !== 'none') blockers.push({ code: 'FORGE_ALREADY_ATTACHED', provider: config.forge.provider });
|
|
if (!configurationOwned(repositoryRoot, manifest)) blockers.push({ code: 'FORGE_CONFIGURATION_MODIFIED' });
|
|
const localCommit = git(repositoryRoot, ['rev-parse', config.project.main_branch]).stdout;
|
|
const observed = remotes.includes(selectedRemote)
|
|
? remoteHead(repositoryRoot, selectedRemote, config.project.main_branch)
|
|
: { available: false, commit: null, error: 'remote missing' };
|
|
if (!observed.available) blockers.push({ code: 'FORGE_UNAVAILABLE', remote: selectedRemote, error: observed.error });
|
|
else if (!observed.commit) blockers.push({ code: 'FORGE_BRANCH_MISSING', remote: selectedRemote, branch: config.project.main_branch });
|
|
else if (observed.commit !== localCommit) blockers.push({
|
|
code: 'FORGE_REF_MISMATCH', remote: selectedRemote, branch: config.project.main_branch,
|
|
localCommit, remoteCommit: observed.commit,
|
|
});
|
|
return {
|
|
action: 'forge-attach', provider: selectedProvider, remote: selectedRemote,
|
|
branch: config.project.main_branch, localCommit, remoteCommit: observed.commit,
|
|
previousCanonical: config.repository.canonical,
|
|
blockers, canProceed: blockers.length === 0, mutationPerformed: false,
|
|
};
|
|
}
|
|
|
|
export function attachForge(input = {}) {
|
|
const plan = planForgeAttach(input);
|
|
if (!plan.canProceed) throw new TorchError('Forge attach is blocked', { code: 'FORGE_ATTACH_BLOCKED', details: plan.blockers });
|
|
const config = loadProjectConfig(input.repositoryRoot);
|
|
const manifest = readInstallManifest(input.repositoryRoot);
|
|
const attachedAt = new Date().toISOString();
|
|
config.forge = {
|
|
provider: plan.provider, remote: plan.remote, attached_at: attachedAt,
|
|
previous_canonical: plan.previousCanonical,
|
|
};
|
|
config.repository.canonical = { type: 'remote', remote: plan.remote };
|
|
writeConfiguration(input.repositoryRoot, config, manifest);
|
|
return { ...plan, attachedAt, mutationPerformed: true };
|
|
}
|
|
|
|
export function planForgeDetach({ repositoryRoot } = {}) {
|
|
const config = loadProjectConfig(repositoryRoot);
|
|
const manifest = readInstallManifest(repositoryRoot);
|
|
const blockers = [];
|
|
if (config.forge.provider === 'none') blockers.push({ code: 'FORGE_NOT_ATTACHED' });
|
|
if (!configurationOwned(repositoryRoot, manifest)) blockers.push({ code: 'FORGE_CONFIGURATION_MODIFIED' });
|
|
if (config.repository.canonical.type !== 'remote'
|
|
|| config.repository.canonical.remote !== config.forge.remote) {
|
|
blockers.push({ code: 'FORGE_CANONICAL_CHANGED', canonical: config.repository.canonical });
|
|
}
|
|
const restoreCanonical = config.forge.previous_canonical ?? { type: 'unconfigured' };
|
|
return {
|
|
action: 'forge-detach', provider: config.forge.provider, remote: config.forge.remote ?? null,
|
|
restoreCanonical, blockers, canProceed: blockers.length === 0, mutationPerformed: false,
|
|
};
|
|
}
|
|
|
|
export function detachForge({ repositoryRoot } = {}) {
|
|
const plan = planForgeDetach({ repositoryRoot });
|
|
if (!plan.canProceed) throw new TorchError('Forge detach is blocked', { code: 'FORGE_DETACH_BLOCKED', details: plan.blockers });
|
|
const config = loadProjectConfig(repositoryRoot);
|
|
const manifest = readInstallManifest(repositoryRoot);
|
|
const detachedAt = new Date().toISOString();
|
|
config.repository.canonical = plan.restoreCanonical;
|
|
config.forge = {
|
|
provider: 'none', last_provider: plan.provider, last_remote: plan.remote,
|
|
detached_at: detachedAt,
|
|
};
|
|
writeConfiguration(repositoryRoot, config, manifest);
|
|
return {
|
|
...plan, detachedAt, localOperational: true,
|
|
preserved: ['identities', 'worktrees', 'backlog', 'decisions', 'messages', 'local state'],
|
|
mutationPerformed: true,
|
|
};
|
|
}
|