import { randomUUID } from "node:crypto"; import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs"; import { hostname } from "node:os"; import { join, resolve } from "node:path"; import process from "node:process"; import { parseStateContents, serializeStateFile, validateEnvironmentName } from "@bedrock-rbx/core"; import { type } from "arktype"; const ok = (data) => ({ success: true, data }); const fail = (reason) => ({ success: false, err: { reason } }); function environmentFile(directory, environment) { const valid = validateEnvironmentName(environment); if (!valid.success) throw new Error(valid.err.reason); if (/^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(environment)) { throw new Error("Environment name is reserved on Windows."); } return join(directory, `${environment}.json`); } function stateFailure(file, error) { return { success: false, err: { kind: error.code === "EACCES" || error.code === "EPERM" ? "stateAccessDenied" : "stateError", file, reason: error.message, }, }; } /** Create a filesystem adapter without creating files during read-only commands. */ export function createLocalStatePorts(directory) { directory = resolve(directory); const lockPath = (environment) => `${environmentFile(directory, environment)}.lock`; const statePort = { async read(environment) { let file = directory; try { file = environmentFile(directory, environment); let raw; try { raw = readFileSync(file, "utf8"); } catch (error) { if (error.code === "ENOENT") return ok({}); throw error; } const parsed = parseStateContents(raw, file); if (!parsed.success) return parsed; if (parsed.data.environment !== environment) { throw new Error(`State belongs to "${parsed.data.environment}", not "${environment}".`); } return ok({ state: parsed.data }); } catch (error) { return stateFailure(file, error); } }, async write(state) { let file = directory; let temporary; try { file = environmentFile(directory, state.environment); const raw = serializeStateFile(state); const parsed = parseStateContents(raw, file); if (!parsed.success) return parsed; mkdirSync(directory, { recursive: true }); temporary = `${file}.${randomUUID()}.tmp`; const handle = openSync(temporary, "wx", 0o600); try { writeFileSync(handle, `${raw}\n`, "utf8"); fsyncSync(handle); } finally { closeSync(handle); } // Rename on the same filesystem keeps readers from seeing partial JSON. renameSync(temporary, file); return ok(undefined); } catch (error) { return stateFailure(file, error); } finally { if (temporary) { try { rmSync(temporary, { force: true }); } catch { // Keep the original failure if the filesystem also refuses cleanup. } } } }, }; const stateLockPort = { async acquire(environment, options = {}) { let lock; let created = false; try { lock = lockPath(environment); mkdirSync(directory, { recursive: true }); // Directory creation is exclusive across independent CLI processes. mkdirSync(lock); created = true; const token = randomUUID(); const holding = { token, owner: `${hostname()}:${process.pid}`, since: new Date().toISOString(), operation: options.operation, }; writeFileSync(join(lock, "owner.json"), JSON.stringify(holding), { mode: 0o600 }); return ok({ async release() { try { const current = JSON.parse(readFileSync(join(lock, "owner.json"), "utf8")); if (current.token !== token) return fail("The local state lock is held by another run."); rmSync(lock, { recursive: true }); return ok(undefined); } catch (error) { return fail(`Cannot release local state lock: ${error.message}`); } }, }); } catch (error) { if (created) { try { rmSync(lock, { recursive: true, force: true }); } catch { return fail( `Cannot initialize or remove local state lock at ${lock}: ${error.message}`, ); } } return fail( error.code === "EEXIST" ? `Local state is locked for "${environment}". Stop the other deploy before using bedrock state unlock --env ${environment}.` : `Cannot acquire local state lock: ${error.message}`, ); } }, async inspect(environment) { try { const lock = lockPath(environment); try { return ok(JSON.parse(readFileSync(join(lock, "owner.json"), "utf8"))); } catch (error) { if (error.code === "ENOENT") { return ok(existsSync(lock) ? { owner: "Unknown local run" } : undefined); } throw error; } } catch (error) { return fail(`Cannot inspect local state lock: ${error.message}`); } }, async forceRelease(environment) { try { const lock = lockPath(environment); if (!existsSync(lock)) return ok(undefined); const holding = await stateLockPort.inspect(environment); rmSync(lock, { recursive: true }); return ok(holding.success ? holding.data : { owner: "Unknown local run" }); } catch (error) { return fail(`Cannot unlock local state: ${error.message}`); } }, }; return { statePort, stateLockPort }; } export default { name: "crownstudio-local-state", stateBackends: [ { name: "local", schema: type({ directory: "string > 0" }), createPort: ({ stateConfig }) => ok(createLocalStatePorts(stateConfig.directory).statePort), createLockPort: ({ stateConfig }) => ok(createLocalStatePorts(stateConfig.directory).stateLockPort), migratePrompts: [ { key: "directory", label: "Local state directory?", placeholder: ".bedrock/state", validationMessage: "A local state directory is required.", }, ], }, ], };