SPB Git

spb/forge-studio Public

The Instruments of LLM training — a native macOS cockpit for Forge. Train language models from scratch on Apple Silicon without a terminal.

Swift 95.7% Shell 4.3%
1.8 KB · 61 lines swift
Raw Blame History
1// Author: Simon-Pierre Boucher — contact@spboucher.ai2//3// Run registry with atomic persistence: every mutation writes4// runs.json.tmp then renames — the app survives kill -9 at any moment with5// either the old or the new registry, never a torn one.6import Foundation78@Observable9final class RunStore {10    private(set) var runs: [Run] = []11    let workspaceURL: URL12    private var registryURL: URL { workspaceURL.appendingPathComponent("runs.json") }1314    init(workspace: URL) {15        workspaceURL = workspace16        load()17    }1819    func load() {20        guard let data = try? Data(contentsOf: registryURL),21              let decoded = try? JSONDecoder().decode([Run].self, from: data) else {22            return23        }24        runs = decoded25    }2627    func upsert(_ run: Run) {28        if let i = runs.firstIndex(where: { $0.id == run.id }) {29            runs[i] = run30        } else {31            runs.append(run)32        }33        persist()34    }3536    func remove(_ run: Run) {37        runs.removeAll { $0.id == run.id }38        persist()39    }4041    private func persist() {42        do {43            let enc = JSONEncoder()44            enc.outputFormatting = [.prettyPrinted, .sortedKeys]45            enc.dateEncodingStrategy = .iso860146            let data = try enc.encode(runs)47            let tmp = registryURL.appendingPathExtension("tmp")48            try FileManager.default49                .createDirectory(at: workspaceURL, withIntermediateDirectories: true)50            try data.write(to: tmp, options: .atomic)51            _ = try FileManager.default.replaceItemAt(registryURL, withItemAt: tmp)52        } catch {53            // Persistence failure must be visible, never silent: kept on the54            // store for the UI to surface.55            lastPersistError = error.localizedDescription56        }57    }5859    private(set) var lastPersistError: String?60}61