mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-11-08 13:52:16 +00:00
Begin portable TORCH fleet rewrite
This commit is contained in:
1 parent
a46832314f
commit
cce059fc3a
16 files changed
+2258
-613
No files matched your search
@@ -0,0 +1,203 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import {
|
||||
existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync,
|
||||
} from 'node:fs';
|
||||
import { dirname, join, relative, resolve, sep } from 'node:path';
|
||||
import { TorchError } from './errors.mjs';
|
||||
import { fileHash, writeNewFile } from './files.mjs';
|
||||
import { projectStatePath } from './paths.mjs';
|
||||
|
||||
const TRACKED_DIR = '.torch';
|
||||
|
||||
function jsonYaml(value) {
|
||||
return `${JSON.stringify(value, null, 2)}\n`;
|
||||
}
|
||||
|
||||
function initialFiles({ repository, projectId, createdAt }) {
|
||||
const config = {
|
||||
schema: 'torch.dev/v1alpha1',
|
||||
project: {
|
||||
id: projectId,
|
||||
name: repository.name,
|
||||
main_branch: repository.branch || 'main',
|
||||
},
|
||||
paths: { tracked_state: TRACKED_DIR, worktree_parent: '~/TORCHWorktrees' },
|
||||
repository: repository.canonicalRemote
|
||||
? { canonical: { type: 'remote', remote: repository.canonicalRemote.name } }
|
||||
: { canonical: { type: 'unconfigured' } },
|
||||
forge: { provider: 'none' },
|
||||
git: {
|
||||
branch_prefix: 'torch/', convergence: 'merge', allow_rebase: false,
|
||||
allow_force_push: false, allow_bare_stash: false,
|
||||
},
|
||||
synchronization: { strategy: 'dispatcher-managed', auto_merge_worktrees: false },
|
||||
session_manager: { id: 'session-manager', start_last: true },
|
||||
domains: [],
|
||||
};
|
||||
const roster = {
|
||||
schema: 'torch.dev/roster/v1alpha1',
|
||||
areas: [{
|
||||
id: 'session-manager', title: 'TORCH Session Manager',
|
||||
scope: ['routing', 'priority', 'ownership rulings', 'fleet health'],
|
||||
not_scope: ['project feature implementation by default'],
|
||||
neighbours: ['all'],
|
||||
}],
|
||||
};
|
||||
|
||||
return new Map([
|
||||
['torch.yaml', jsonYaml(config)],
|
||||
['roster.yaml', jsonYaml(roster)],
|
||||
['decisions.md', '# TORCH decisions\n\nNo project decisions recorded yet.\n'],
|
||||
['RESUME-BRIEF.md', '# TORCH resume brief\n\nFleet not started yet.\n'],
|
||||
['prompts/COMMON.md', '# Common fleet rules\n\nRepository state outranks conversation memory. Query ownership before crossing a domain boundary.\n'],
|
||||
['prompts/session-manager.md', '# TORCH Session Manager\n\nRoute owner requests, establish ownership and priority, and keep routine coordination inside the fleet.\n'],
|
||||
['backlog/.gitkeep', ''],
|
||||
['INSTALLATION.md', `# TORCH installation\n\nInstalled ${createdAt}. Run \`torch doctor\` before starting the fleet.\n`],
|
||||
]);
|
||||
}
|
||||
|
||||
function manifestPath(root) {
|
||||
return join(root, TRACKED_DIR, 'install-manifest.json');
|
||||
}
|
||||
|
||||
function readManifest(root) {
|
||||
const path = manifestPath(root);
|
||||
if (!existsSync(path)) {
|
||||
throw new TorchError(`No TORCH ownership manifest at ${path}`, { code: 'NOT_INSTALLED' });
|
||||
}
|
||||
try {
|
||||
return JSON.parse(readFileSync(path, 'utf8'));
|
||||
} catch (error) {
|
||||
throw new TorchError(`Invalid TORCH ownership manifest: ${path}`, {
|
||||
code: 'INVALID_INSTALL_MANIFEST', details: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function safeProjectPath(root, relativePath) {
|
||||
const target = resolve(root, relativePath);
|
||||
const prefix = `${resolve(root)}${sep}`;
|
||||
if (!target.startsWith(prefix)) {
|
||||
throw new TorchError(`Manifest path escapes project root: ${relativePath}`, {
|
||||
code: 'UNSAFE_MANIFEST_PATH', details: { relativePath },
|
||||
});
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
export function planInstall({ repository, env = process.env }) {
|
||||
const trackedRoot = join(repository.root, TRACKED_DIR);
|
||||
return {
|
||||
action: 'install',
|
||||
projectRoot: repository.root,
|
||||
trackedRoot,
|
||||
stateParent: join(projectStatePath('<new-project-id>', env), '..'),
|
||||
wouldCreateTrackedState: !existsSync(trackedRoot),
|
||||
mutationPerformed: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function installProject({
|
||||
repository,
|
||||
env = process.env,
|
||||
projectId = randomUUID(),
|
||||
now = () => new Date(),
|
||||
} = {}) {
|
||||
const trackedRoot = join(repository.root, TRACKED_DIR);
|
||||
if (existsSync(trackedRoot)) {
|
||||
throw new TorchError(`Refusing to replace existing ${trackedRoot}`, {
|
||||
code: 'ALREADY_INSTALLED', details: { trackedRoot },
|
||||
});
|
||||
}
|
||||
|
||||
const createdAt = now().toISOString();
|
||||
const stateRoot = projectStatePath(projectId, env);
|
||||
if (existsSync(stateRoot)) {
|
||||
throw new TorchError(`Refusing to replace existing TORCH state: ${stateRoot}`, {
|
||||
code: 'STATE_ALREADY_EXISTS', details: { stateRoot },
|
||||
});
|
||||
}
|
||||
|
||||
const created = [];
|
||||
try {
|
||||
for (const [path, content] of initialFiles({ repository, projectId, createdAt })) {
|
||||
const absolute = join(trackedRoot, path);
|
||||
const record = writeNewFile(absolute, content);
|
||||
created.push({ path: relative(repository.root, record.path), sha256: record.sha256 });
|
||||
}
|
||||
|
||||
mkdirSync(dirname(stateRoot), { recursive: true });
|
||||
mkdirSync(stateRoot, { recursive: false });
|
||||
for (const dir of ['sessions', 'messages', 'locks', 'leases', 'checks', 'integration', 'logs']) {
|
||||
mkdirSync(join(stateRoot, dir));
|
||||
}
|
||||
writeFileSync(join(stateRoot, 'project.json'), jsonYaml({
|
||||
schema: 'torch.dev/local-project/v1alpha1', projectId,
|
||||
root: repository.root, createdAt, detachedAt: null,
|
||||
}), { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
||||
|
||||
const manifest = {
|
||||
schema: 'torch.dev/install-manifest/v1alpha1',
|
||||
installationId: randomUUID(),
|
||||
projectId,
|
||||
createdAt,
|
||||
created,
|
||||
patched: [],
|
||||
external: [{ type: 'local-state', path: stateRoot }],
|
||||
};
|
||||
writeNewFile(manifestPath(repository.root), jsonYaml(manifest));
|
||||
return { projectId, trackedRoot, stateRoot, manifest, mutationPerformed: true };
|
||||
} catch (error) {
|
||||
if (existsSync(trackedRoot)) rmSync(trackedRoot, { recursive: true, force: true });
|
||||
if (existsSync(stateRoot)) rmSync(stateRoot, { recursive: true, force: true });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function nonEmptyEntries(path) {
|
||||
if (!existsSync(path)) return [];
|
||||
return readdirSync(path, { recursive: true, withFileTypes: true })
|
||||
.filter((entry) => entry.isFile() && entry.name !== 'project.json')
|
||||
.map((entry) => entry.name);
|
||||
}
|
||||
|
||||
export function planUninstall({ repository, purge = false }) {
|
||||
const manifest = readManifest(repository.root);
|
||||
const problems = [];
|
||||
for (const record of manifest.created) {
|
||||
const path = safeProjectPath(repository.root, record.path);
|
||||
if (!existsSync(path)) problems.push({ type: 'missing', path: record.path });
|
||||
else if (fileHash(path) !== record.sha256) problems.push({ type: 'modified', path: record.path });
|
||||
}
|
||||
for (const entry of manifest.external ?? []) {
|
||||
if (entry.type === 'local-state') {
|
||||
const extras = nonEmptyEntries(entry.path);
|
||||
if (extras.length) problems.push({ type: 'local-state-not-empty', path: entry.path, entries: extras });
|
||||
}
|
||||
}
|
||||
return {
|
||||
action: purge ? 'purge' : 'detach', manifest, problems,
|
||||
canProceed: !purge || problems.length === 0,
|
||||
mutationPerformed: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function uninstallProject({ repository, purge = false, dryRun = false }) {
|
||||
const plan = planUninstall({ repository, purge });
|
||||
if (dryRun || !purge) return plan;
|
||||
if (!plan.canProceed) {
|
||||
throw new TorchError('TORCH purge stopped because managed state changed', {
|
||||
code: 'UNSAFE_TO_PURGE', details: plan.problems,
|
||||
});
|
||||
}
|
||||
|
||||
for (const record of [...plan.manifest.created].reverse()) {
|
||||
rmSync(safeProjectPath(repository.root, record.path));
|
||||
}
|
||||
rmSync(manifestPath(repository.root));
|
||||
rmSync(join(repository.root, TRACKED_DIR), { recursive: true });
|
||||
for (const entry of plan.manifest.external ?? []) {
|
||||
if (entry.type === 'local-state' && existsSync(entry.path)) rmSync(entry.path, { recursive: true });
|
||||
}
|
||||
return { ...plan, mutationPerformed: true };
|
||||
}
|
||||
Reference in new issue
Block a user