Spaces:
Running
Running
File size: 3,631 Bytes
31b3c47 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | #!/usr/bin/env node
/**
* Hand a staged file over to a path another agent reads, in one step.
*
* node scripts/write-atomic.mjs /workspace/.grok/og.jpg.tmp public/og.jpg
*
* The brand-asset task writes public/og.jpg and src/lib/og/site.json while the
* parent may be mid-`npm run build`, so an in-place write can be read
* half-finished. rename(2) is atomic within one filesystem: a reader sees the
* old bytes or the new ones. /workspace is one filesystem, so a staged file
* from anywhere else (/tmp is a separate mount) is refused rather than copied:
* copying would have to land its temp in the target's directory, which is the
* one thing a staged path is not allowed to do.
*/
import { existsSync, mkdirSync, renameSync } from "node:fs";
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
export function parseWriteAtomicArgs(argv) {
const [staged, target, ...rest] = argv;
if (!staged || !target) {
return { error: "usage: node scripts/write-atomic.mjs <staged-file> <target>" };
}
if (rest.length > 0) return { error: `unexpected argument: ${rest[0]}` };
return { staged, target };
}
function isInside(dir, file) {
const rel = relative(dir, file);
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
}
/**
* Why a staged path can be refused: `vite build` copies public/ verbatim into
* the deployed app, so a temp that lands there ships (and an interrupted run
* leaves it behind).
*/
export function stagingError({ staged, target, publicDir }) {
if (staged === target) return `staged file and target are the same path: ${target}`;
if (isInside(publicDir, staged)) {
return `stage outside ${publicDir} (vite build ships that directory verbatim): ${staged}`;
}
return null;
}
/**
* Moves `staged` onto `target`, leaving `target` untouched if anything fails.
* `rename` is injectable because EXDEV — the refusal the "stage under
* /workspace/.grok/" contract rests on — cannot be provoked portably.
*/
export function handOver(staged, target, { rename = renameSync } = {}) {
if (!existsSync(staged)) {
throw Object.assign(new Error(`staged file is missing: ${staged}`), { code: "ENOENT" });
}
mkdirSync(dirname(target), { recursive: true });
try {
rename(staged, target);
} catch (err) {
if (err?.code === "EXDEV") {
throw new Error(
`${staged} is on another filesystem than ${target}, so the hand-over cannot be a `
+ "rename — stage under /workspace/.grok/ instead",
{ cause: err },
);
}
throw err;
}
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const args = parseWriteAtomicArgs(process.argv.slice(2));
if (args.error) {
console.error(`[write-atomic] ${args.error}`);
process.exit(1);
}
// Relative paths follow this script's root, not the caller's cwd: the brand
// pass runs from wherever its sub-shell left it, and the public/ refusal
// below is defined against that same root.
const staged = resolve(ROOT, args.staged);
const target = resolve(ROOT, args.target);
const problem = stagingError({ staged, target, publicDir: join(ROOT, "public") });
if (problem) {
console.error(`[write-atomic] ${problem}`);
process.exit(1);
}
try {
handOver(staged, target);
} catch (err) {
console.error(`[write-atomic] ${staged} → ${target} failed: ${err?.message || err}`);
process.exit(1);
}
console.log(`[write-atomic] wrote ${target}`);
}
|