1import * as Effect from "effect/Effect";
2import type * as FileSystem from "effect/FileSystem";
3import type { PlatformError } from "effect/PlatformError";
4
5/**
6 * Write `contents` to `filePath` via a same-directory temp file + rename.
7 *
8 * A plain `writeFileString` truncates the target before writing, so a crash
9 * mid-write (or a concurrent reader — another CLI process, the RPC sidecar)
10 * can observe empty/partial JSON. rename(2) is atomic on the same
11 * filesystem, so readers see either the old contents or the new, never a
12 * torn write. When `mode` is given it is applied to the temp file *before*
13 * the rename, so a secret is never visible at its final path with default
14 * permissions. The temp file is removed on failure.
15 *
16 * Used for `~/.alchemy/profiles.json`, the per-provider credential files,
17 * and local state files.
18 */
19export const writeFileAtomic = (
20 fs: FileSystem.FileSystem,
21 filePath: string,
22 contents: string,
23 mode?: number,
24): Effect.Effect<void, PlatformError> =>
25 Effect.suspend(() => {
26 const tmp = `${filePath}.${process.pid}.${Math.random()
27 .toString(36)
28 .slice(2)}.tmp`;
29 return fs.writeFileString(tmp, contents).pipe(
30 Effect.flatMap(() =>
31 mode === undefined ? Effect.void : fs.chmod(tmp, mode),
32 ),
33 Effect.flatMap(() => fs.rename(tmp, filePath)),
34 Effect.tapError(() => fs.remove(tmp).pipe(Effect.ignore)),
35 );
36 });