From 6dee9d144ce761863d65398c2bc3929610b0773b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Mon, 26 Oct 2026 18:50:41 +0000 Subject: [PATCH] fix(cli): init creates its files where hard links are refused, with no Developer Mode needed (#6463) --- .../cli/src/utils/projectConfig.create.test.ts | 18 +++++++++++++++++- packages/cli/src/utils/writeNewFile.ts | 16 ++++------------ 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/packages/cli/src/utils/projectConfig.create.test.ts b/packages/cli/src/utils/projectConfig.create.test.ts index 221aeb9d4c..5a4a446b67 100644 --- a/packages/cli/src/utils/projectConfig.create.test.ts +++ b/packages/cli/src/utils/projectConfig.create.test.ts @@ -12,7 +12,11 @@ import { vi.mock("node:fs", async (importOriginal) => { const original = await importOriginal(); - return { ...original, readFileSync: vi.fn(original.readFileSync) }; + return { + ...original, + readFileSync: vi.fn(original.readFileSync), + linkSync: vi.fn(original.linkSync), + }; }); const dirs: string[] = []; @@ -43,6 +47,18 @@ describe("project config creation", () => { expect(JSON.parse(fs.readFileSync(path, "utf-8"))).toEqual(updated); }); + it("creates the config on a volume or sandbox that refuses hard links (EPERM)", () => { + const dir = project(); + vi.mocked(fs.linkSync).mockImplementationOnce(() => { + throw Object.assign(new Error("operation not permitted"), { code: "EPERM" }); + }); + createProjectConfig(dir); + expect(JSON.parse(fs.readFileSync(projectConfigPath(dir), "utf-8"))).toEqual( + DEFAULT_PROJECT_CONFIG, + ); + expect(fs.readdirSync(dir)).toEqual(["hyperframes.json"]); + }); + it("preserves a config created after the skill seed reads ENOENT", () => { const dir = project(); const path = projectConfigPath(dir); diff --git a/packages/cli/src/utils/writeNewFile.ts b/packages/cli/src/utils/writeNewFile.ts index 137af84a33..02214ab407 100644 --- a/packages/cli/src/utils/writeNewFile.ts +++ b/packages/cli/src/utils/writeNewFile.ts @@ -1,18 +1,10 @@ -import { linkSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { dirname, join } from "node:path"; +import { createFileAtomically } from "@hyperframes/core/atomic-file"; /** Publish complete content without replacing or following an existing destination entry. */ export function writeNewFileSync(filePath: string, content: string): void { - const stagingDir = mkdtempSync(join(dirname(filePath), ".hf-create-")); try { - const stagedPath = join(stagingDir, "content"); - writeFileSync(stagedPath, content, { encoding: "utf-8", flag: "wx" }); - try { - linkSync(stagedPath, filePath); - } catch (err) { - if (!(err instanceof Error && "code" in err && err.code === "EEXIST")) throw err; - } - } finally { - rmSync(stagingDir, { recursive: true, force: true }); + createFileAtomically(filePath, content); + } catch (err) { + if (!(err instanceof Error && "code" in err && err.code === "EEXIST")) throw err; } }