Files
TORCH/src/cmd-backup.mjs
T
google-labs-jules[bot]andPR0M3TH3AN c0c5ac5473 fix(torch-lock): re-apply fixes for circular deps and syntax errors
- Moved `withTimeout` implementation to `src/lock-utils.mjs` and exported it correctly.
- Removed duplicate local definitions of `withTimeout`, `mergeRelayList`, and `secureRandom` in `src/lock-ops.mjs` and `src/lock-publisher.mjs`.
- Fixed imports in `src/lock-ops.mjs`, `src/lock-publisher.mjs`, and `src/relay-health.mjs` to use `src/lock-utils.mjs` consistently.
- Updated `test/utils.test.mjs` and `test/utils-async.test.mjs` to fix broken imports and test definitions.
- Verified `npm run lock:complete` and `npm run scheduler:daily` execute correctly.
- Updated `src/todo/TODO_2026-02-20T03-00-00Z.md` to mark verification task as completed.
- Verified `src/relay-health.mjs` does not use `MINUTE_MS` locally, despite mysterious CI error. Confirmed imports are correct.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-21 19:23:48 +00:00

110 lines
3.2 KiB
JavaScript

import fs from 'node:fs/promises';
import path from 'node:path';
import { execSync } from 'node:child_process';
const BACKUP_ROOT = path.resolve(process.cwd(), '.torch/backups');
// Paths to snapshot on each backup run
const STATE_SOURCES = [
'.scheduler-memory/memory-store.json',
'task-logs/daily/.scheduler-run-state.json',
];
/**
* Creates a timestamped snapshot of TORCH runtime state under .torch/backups/<timestamp>/.
*
* Files captured (if they exist):
* - .scheduler-memory/memory-store.json (agent long-term memory)
* - task-logs/daily/.scheduler-run-state.json (scheduler deferral state)
*
* A backup-manifest.json is always written recording what was captured,
* the git commit at backup time, and an ISO timestamp.
*
* @param {Object} [opts]
* @param {string|null} [opts.output] - Override destination directory (default: .torch/backups/<ts>)
* @returns {Promise<{backupDir: string, manifest: Object}>}
*/
export async function cmdBackup({ output = null } = {}) {
const ts = new Date().toISOString().replace(/[:.]/g, '-');
const backupDir = output
? path.resolve(process.cwd(), output)
: path.join(BACKUP_ROOT, ts);
await fs.mkdir(backupDir, { recursive: true });
let gitCommit = null;
try {
gitCommit = execSync('git rev-parse HEAD', { stdio: 'pipe' }).toString().trim();
} catch {
// Not in a git repo or git unavailable
}
const captured = [];
const skipped = [];
for (const relPath of STATE_SOURCES) {
const src = path.resolve(process.cwd(), relPath);
const dest = path.join(backupDir, relPath.replace(/\//g, '__'));
try {
await fs.copyFile(src, dest);
captured.push(relPath);
} catch (e) {
if (e.code === 'ENOENT') {
skipped.push({ path: relPath, reason: 'not found' });
} else {
skipped.push({ path: relPath, reason: e.message });
}
}
}
const manifest = {
createdAt: new Date().toISOString(),
gitCommit,
backupDir,
captured,
skipped,
};
await fs.writeFile(
path.join(backupDir, 'backup-manifest.json'),
JSON.stringify(manifest, null, 2)
);
console.log(JSON.stringify({ success: true, ...manifest }, null, 2));
return { backupDir, manifest };
}
/**
* Lists all available backups under .torch/backups/, newest first.
*
* @returns {Promise<Array<{id: string, createdAt: string|null, backupDir: string}>>}
*/
export async function listBackups() {
try {
const entries = await fs.readdir(BACKUP_ROOT, { withFileTypes: true });
const backups = [];
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const backupDir = path.join(BACKUP_ROOT, entry.name);
let manifest = null;
try {
manifest = JSON.parse(await fs.readFile(path.join(backupDir, 'backup-manifest.json'), 'utf8'));
} catch {
// No manifest
}
backups.push({
id: entry.name,
createdAt: manifest?.createdAt ?? null,
gitCommit: manifest?.gitCommit ?? null,
captured: manifest?.captured ?? [],
backupDir,
});
}
// Newest first (ISO timestamp sort)
backups.sort((a, b) => (b.createdAt ?? '').localeCompare(a.createdAt ?? ''));
return backups;
} catch {
return [];
}
}