Files
openrig-studio/create-studio/lib/emit.mjs
T
mvschwarz 2c1e40e6b1 Initial release — OpenRig Studio SDK, contract v0.1
An app shell and a contract, not a framework you link against. A rail of
surfaces, a stage, an agent sidebar, and a protocol for how a surface talks to
them. An app conforms to the contract; it never imports this package.

- app/            the reference runtime and shell
- contract/       the contract: shell protocol, manifest, row schema, runtime API
- create-studio/  a scaffolder for a new studio
- skills/         how to build an app on this SDK, written from experience
- fixtures/       example rig state for the FLOOR surface
- test/           73 tests, including the consumer-surface seam and redeploy survival

Consumers register surfaces from OUTSIDE the package via --surfaces, so nothing
is written into node_modules and a copied install cannot carry another
instance's rows.

Licensed Apache-2.0.
2026-08-02 03:03:14 -07:00

47 lines
1.9 KiB
JavaScript

// Emit — stage every file into a sibling temp directory, then ONE atomic
// rename into place.
//
// Why a sibling and not the system temp dir: rename() is only atomic within a
// filesystem. A sibling of the destination is guaranteed to be on the same
// filesystem; /tmp is not, and would silently degrade to a copy that can be
// interrupted halfway.
//
// The guarantee this buys: the destination either does not exist, or is the
// complete project. A crash mid-generation leaves a hidden staging directory
// that is removed on the way out — never a half-written project a developer
// might mistake for a working one.
import fs from "node:fs";
import path from "node:path";
export function emitProject(dest, files) {
const parent = path.dirname(dest);
const staging = fs.mkdtempSync(path.join(parent, ".create-studio-staging-"));
// Test affordance ONLY: throw after N files have been staged, to demonstrate
// that a mid-generation failure leaves no partial project. Inert when unset.
const failAfter = Number(process.env.OPENRIG_STUDIO_CREATE_FAIL_AFTER || 0);
let staged = 0;
try {
for (const [rel, content] of Object.entries(files)) {
const target = path.join(staging, rel);
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.writeFileSync(target, content, "utf8");
staged += 1;
if (failAfter > 0 && staged >= failAfter) {
throw new Error(
`OPENRIG_STUDIO_CREATE_FAIL_AFTER=${failAfter} — simulated mid-generation failure after ${staged} file(s)`
);
}
}
// POSIX rename() onto an existing EMPTY directory succeeds; admission has
// already refused a non-empty destination.
fs.renameSync(staging, dest);
return { dest, files: Object.keys(files) };
} catch (error) {
try { fs.rmSync(staging, { recursive: true, force: true }); } catch { /* best effort */ }
throw error;
}
}