From dad6500fe0ff17a003349d18c29fb2d89f66fedd Mon Sep 17 00:00:00 2001 From: ophiocus <1540596+ophiocus@users.noreply.github.com> Date: Mon, 31 Aug 2026 11:35:41 -0500 Subject: [PATCH] fix(init): create a junction for .agents/skills on Windows and report link failures honestly On Windows without Developer Mode the skills symlink fails with EPERM and init blamed "filesystem without symlink support" - the wrong cause and no remedy (#230). Directory junctions need no privileges, so init now falls back to one (absolute target, said out loud in the ok line) and, when even that fails, names the two real fixes. The init end-to-end test also spawned Biome through its POSIX .bin shim, which spawnSync cannot execute on Windows (status null) - masked until now because the symlink death came first; it now runs Biome's JS entry through node. init.test.mjs: 8/8 on Windows for the first time, 8/8 on Linux unchanged. Closes #230 Co-Authored-By: Claude Opus 5 --- packages/cli/src/init.mjs | 22 ++++++++++++++++++---- packages/cli/test/init.test.mjs | 10 +++++++--- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/init.mjs b/packages/cli/src/init.mjs index cc6f1a78..662f5560 100644 --- a/packages/cli/src/init.mjs +++ b/packages/cli/src/init.mjs @@ -6,7 +6,7 @@ import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs"; import { createHash } from "node:crypto"; import { pathToFileURL } from "node:url"; -import { dirname, join } from "node:path"; +import { dirname, join, resolve } from "node:path"; import { detect } from "./detect.mjs"; import { render, hasManagedBlock, appendManagedBlock } from "./render.mjs"; import { ask, confirm, closePrompts } from "./prompts.mjs"; @@ -518,12 +518,26 @@ export async function init(flags, pkgRoot, version) { lstatSync(agentsSkillsLink); skip(".agents/skills exists — left untouched"); } catch { + mkdirSync(join(dir, ".agents"), { recursive: true }); try { - mkdirSync(join(dir, ".agents"), { recursive: true }); symlinkSync("../.claude/skills", agentsSkillsLink, "dir"); ok(".agents/skills → .claude/skills"); - } catch { - warn(".agents/skills symlink could not be created (filesystem without symlink support) — skipped."); + } catch (error) { + // On Windows, plain symlinks need Developer Mode or elevation (EPERM). + // Directory junctions need neither — they just require an absolute + // target — so fall back to one instead of misreporting the cause. + if (process.platform === "win32" && (error.code === "EPERM" || error.code === "EINVAL")) { + try { + symlinkSync(resolve(dir, ".claude/skills"), agentsSkillsLink, "junction"); + ok(".agents/skills → .claude/skills (junction — absolute; recreate after moving the repo)"); + } catch { + warn( + ".agents/skills link could not be created: Windows symlinks need Developer Mode or an elevated shell, and the junction fallback also failed — skipped.", + ); + } + } else { + warn(".agents/skills symlink could not be created (filesystem without symlink support) — skipped."); + } } } diff --git a/packages/cli/test/init.test.mjs b/packages/cli/test/init.test.mjs index d5f5c4ed..a45505c9 100644 --- a/packages/cli/test/init.test.mjs +++ b/packages/cli/test/init.test.mjs @@ -11,7 +11,11 @@ import { parse as parseYaml } from "yaml"; const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const cli = join(pkgRoot, "bin", "facility.mjs"); -const biome = resolve(pkgRoot, "..", "..", "node_modules", ".bin", "biome"); +// Biome's .bin shim is a POSIX script that spawnSync cannot execute on +// Windows (status null); its JS entry runs through node on every platform. +const biomeJs = resolve(pkgRoot, "..", "..", "node_modules", "@biomejs", "biome", "bin", "biome"); +const biome = process.execPath; +const biomeArgs = (args) => [biomeJs, ...args]; function makeTargetRepo() { const dir = mkdtempSync(join(tmpdir(), "facility-test-")); @@ -445,12 +449,12 @@ test("init installs the method end to end", async (t) => { 2, )}\n`, ); - const formatConfig = spawnSync(biome, ["format", "--write", "biome.json"], { + const formatConfig = spawnSync(biome, biomeArgs(["format", "--write", "biome.json"]), { cwd: dir, encoding: "utf8", }); assert.equal(formatConfig.status, 0, formatConfig.stdout + formatConfig.stderr); - const strictBiome = spawnSync(biome, ["check", "."], { cwd: dir, encoding: "utf8" }); + const strictBiome = spawnSync(biome, biomeArgs(["check", "."]), { cwd: dir, encoding: "utf8" }); assert.equal(strictBiome.status, 0, strictBiome.stdout + strictBiome.stderr); const doctor = runCli(["doctor", `--dir=${dir}`], dir);