fix(dashboard): use fs.mkdir for cloudflared local-bin fallback on Windows
Replace execFileAsync("mkdir", ["-p", ...]) with fs.mkdir({ recursive: true })
in the cloudflared install fallback path. The -p flag is Unix-only and breaks
on Windows cmd.exe ("A subdirectory or file -p already exists"). Test mocks
updated to verify the fs.mkdir call instead of the shelled-out mkdir.
The original report covered both this site and packages/engine/src/worktree-hooks.ts;
the latter was already converted to fs.mkdir independently, so only the
dashboard route change is needed.
Co-Authored-By: kenlin8827 <kenlin8827@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
5
.changeset/windows-mkdir-compatibility.md
Normal file
5
.changeset/windows-mkdir-compatibility.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Fix Windows compatibility in cloudflared install fallback by replacing `execFileAsync("mkdir", ["-p", ...])` with `fs.mkdir({ recursive: true })`. The shell-level `-p` flag is Unix-only and breaks installation on Windows cmd.exe with "A subdirectory or file -p already exists". The worktree-hooks fix from the original report was already landed independently.
|
||||
@@ -4,8 +4,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import express from "express";
|
||||
import type { TaskStore } from "@fusion/core";
|
||||
|
||||
const { mockExecFile } = vi.hoisted(() => ({
|
||||
const { mockExecFile, mockMkdir } = vi.hoisted(() => ({
|
||||
mockExecFile: vi.fn(),
|
||||
mockMkdir: vi.fn(),
|
||||
}));
|
||||
vi.mock("node:child_process", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("node:child_process")>();
|
||||
@@ -14,6 +15,13 @@ vi.mock("node:child_process", async (importOriginal) => {
|
||||
execFile: mockExecFile,
|
||||
};
|
||||
});
|
||||
vi.mock("node:fs/promises", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("node:fs/promises")>();
|
||||
return {
|
||||
...actual,
|
||||
mkdir: mockMkdir.mockImplementation(async () => undefined),
|
||||
};
|
||||
});
|
||||
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { createHash } from "node:crypto";
|
||||
@@ -123,6 +131,8 @@ afterEach(() => {
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecFile.mockReset();
|
||||
mockMkdir.mockReset();
|
||||
mockMkdir.mockImplementation(async () => undefined);
|
||||
mockExecFile.mockImplementation((command: string, _args: string[], optionsOrCallback: unknown, maybeCallback?: (error: Error | null, stdout?: string, stderr?: string) => void) => {
|
||||
const callback = typeof optionsOrCallback === "function"
|
||||
? optionsOrCallback as (error: Error | null, stdout?: string, stderr?: string) => void
|
||||
@@ -381,7 +391,7 @@ describe("remote access provider/lifecycle contracts", () => {
|
||||
|
||||
expect(result.status).toBe(200);
|
||||
expect(result.body).toEqual(expect.objectContaining({ success: true }));
|
||||
expect(mockExecFile.mock.calls.some(([command, args]) => command === "mkdir" && Array.isArray(args) && args[0] === "-p")).toBe(true);
|
||||
expect(mockMkdir).toHaveBeenCalledWith(expect.stringContaining('/.local/bin'), { recursive: true });
|
||||
expect(mockExecFile.mock.calls.some(([command, args]) => command === "mv" && Array.isArray(args) && String(args[1]).includes("/.local/bin/cloudflared"))).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ import crypto from "node:crypto";
|
||||
import { createReadStream } from "node:fs";
|
||||
import { execFile } from "node:child_process";
|
||||
import { homedir } from "node:os";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { promisify } from "node:util";
|
||||
import { ApiError, badRequest } from "../api-error.js";
|
||||
import { resolveGithubTrackingAuth } from "../github-auth.js";
|
||||
@@ -345,9 +346,9 @@ export function registerSettingsMemoryRoutes(ctx: ApiRoutesContext, deps: Settin
|
||||
} catch (error) {
|
||||
const localBinDir = `${homedir()}/.local/bin`;
|
||||
const localInstallPath = `${localBinDir}/cloudflared`;
|
||||
attemptedCommands.push(`mkdir -p ${localBinDir}`);
|
||||
attemptedCommands.push(`mkdir(${localBinDir}, { recursive: true })`);
|
||||
attemptedCommands.push(`mv ${tempPath} ${localInstallPath}`);
|
||||
await execFileAsync("mkdir", ["-p", localBinDir], { timeout: 30_000 });
|
||||
await mkdir(localBinDir, { recursive: true });
|
||||
try {
|
||||
await execFileAsync("mv", [tempPath, localInstallPath], { timeout: 30_000 });
|
||||
} catch (fallbackError) {
|
||||
|
||||
Reference in New Issue
Block a user