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);