Compare commits
1 Commits
refactor/m
...
fix/issue-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ba1d675b9 |
47
src/cli/config-manager/bun-install.test.ts
Normal file
47
src/cli/config-manager/bun-install.test.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test"
|
||||
import { initConfigContext, resetConfigContext } from "./config-context"
|
||||
import { runBunInstallWithDetails } from "./bun-install"
|
||||
|
||||
describe("bun-install", () => {
|
||||
let originalPlatform: NodeJS.Platform
|
||||
|
||||
beforeEach(() => {
|
||||
originalPlatform = process.platform
|
||||
resetConfigContext()
|
||||
initConfigContext("opencode", null)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, "platform", { value: originalPlatform })
|
||||
resetConfigContext()
|
||||
})
|
||||
|
||||
test("#given Windows with bun.exe on PATH #when runBunInstallWithDetails is called #then uses bun.exe", async () => {
|
||||
Object.defineProperty(process, "platform", { value: "win32" })
|
||||
|
||||
const whichSpy = spyOn(Bun, "which")
|
||||
.mockImplementation((binary: string) => {
|
||||
if (binary === "bun.exe") {
|
||||
return "C:\\Tools\\bun.exe"
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const spawnSpy = spyOn(Bun, "spawn").mockReturnValue({
|
||||
exited: Promise.resolve(0),
|
||||
exitCode: 0,
|
||||
kill: () => {},
|
||||
} as unknown as ReturnType<typeof Bun.spawn>)
|
||||
|
||||
try {
|
||||
const result = await runBunInstallWithDetails()
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(spawnSpy).toHaveBeenCalledTimes(1)
|
||||
expect(spawnSpy.mock.calls[0]?.[0]).toEqual(["C:\\Tools\\bun.exe", "install"])
|
||||
} finally {
|
||||
spawnSpy.mockRestore()
|
||||
whichSpy.mockRestore()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -9,6 +9,14 @@ export interface BunInstallResult {
|
||||
error?: string
|
||||
}
|
||||
|
||||
function resolveBunCommand(): string {
|
||||
if (process.platform === "win32") {
|
||||
return Bun.which("bun.exe") ?? Bun.which("bun") ?? "bun.exe"
|
||||
}
|
||||
|
||||
return Bun.which("bun") ?? "bun"
|
||||
}
|
||||
|
||||
export async function runBunInstall(): Promise<boolean> {
|
||||
const result = await runBunInstallWithDetails()
|
||||
return result.success
|
||||
@@ -16,7 +24,8 @@ export async function runBunInstall(): Promise<boolean> {
|
||||
|
||||
export async function runBunInstallWithDetails(): Promise<BunInstallResult> {
|
||||
try {
|
||||
const proc = Bun.spawn(["bun", "install"], {
|
||||
const bunCommand = resolveBunCommand()
|
||||
const proc = Bun.spawn([bunCommand, "install"], {
|
||||
cwd: getConfigDir(),
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
@@ -39,7 +48,7 @@ export async function runBunInstallWithDetails(): Promise<BunInstallResult> {
|
||||
return {
|
||||
success: false,
|
||||
timedOut: true,
|
||||
error: `bun install timed out after ${BUN_INSTALL_TIMEOUT_SECONDS} seconds. Try running manually: cd ${getConfigDir()} && bun i`,
|
||||
error: `bun install timed out after ${BUN_INSTALL_TIMEOUT_SECONDS} seconds. Try running manually in ${getConfigDir()}: bun install`,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +64,7 @@ export async function runBunInstallWithDetails(): Promise<BunInstallResult> {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
return {
|
||||
success: false,
|
||||
error: `bun install failed: ${message}. Is bun installed? Try: curl -fsSL https://bun.sh/install | bash`,
|
||||
error: `bun install failed: ${message}. Ensure Bun is installed and available in PATH: https://bun.sh/docs/installation`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
39
src/shared/command-executor/shell-path.test.ts
Normal file
39
src/shared/command-executor/shell-path.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
||||
import { findBashPath } from "./shell-path"
|
||||
|
||||
describe("shell-path", () => {
|
||||
let originalPlatform: NodeJS.Platform
|
||||
let originalComspec: string | undefined
|
||||
|
||||
beforeEach(() => {
|
||||
originalPlatform = process.platform
|
||||
originalComspec = process.env.COMSPEC
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, "platform", { value: originalPlatform })
|
||||
if (originalComspec !== undefined) {
|
||||
process.env.COMSPEC = originalComspec
|
||||
return
|
||||
}
|
||||
delete process.env.COMSPEC
|
||||
})
|
||||
|
||||
test("#given Windows platform with COMSPEC #when findBashPath is called #then returns COMSPEC path", () => {
|
||||
Object.defineProperty(process, "platform", { value: "win32" })
|
||||
process.env.COMSPEC = "C:\\Windows\\System32\\cmd.exe"
|
||||
|
||||
const result = findBashPath()
|
||||
|
||||
expect(result).toBe("C:\\Windows\\System32\\cmd.exe")
|
||||
})
|
||||
|
||||
test("#given Windows platform without COMSPEC #when findBashPath is called #then returns default cmd path", () => {
|
||||
Object.defineProperty(process, "platform", { value: "win32" })
|
||||
delete process.env.COMSPEC
|
||||
|
||||
const result = findBashPath()
|
||||
|
||||
expect(result).toBe("C:\\Windows\\System32\\cmd.exe")
|
||||
})
|
||||
})
|
||||
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs"
|
||||
|
||||
const DEFAULT_ZSH_PATHS = ["/bin/zsh", "/usr/bin/zsh", "/usr/local/bin/zsh"]
|
||||
const DEFAULT_BASH_PATHS = ["/bin/bash", "/usr/bin/bash", "/usr/local/bin/bash"]
|
||||
const DEFAULT_WINDOWS_CMD_PATH = "C:\\Windows\\System32\\cmd.exe"
|
||||
|
||||
function findShellPath(
|
||||
defaultPaths: string[],
|
||||
@@ -19,9 +20,17 @@ function findShellPath(
|
||||
}
|
||||
|
||||
export function findZshPath(customZshPath?: string): string | null {
|
||||
if (process.platform === "win32") {
|
||||
return process.env.COMSPEC?.trim() || DEFAULT_WINDOWS_CMD_PATH
|
||||
}
|
||||
|
||||
return findShellPath(DEFAULT_ZSH_PATHS, customZshPath)
|
||||
}
|
||||
|
||||
export function findBashPath(): string | null {
|
||||
if (process.platform === "win32") {
|
||||
return process.env.COMSPEC?.trim() || DEFAULT_WINDOWS_CMD_PATH
|
||||
}
|
||||
|
||||
return findShellPath(DEFAULT_BASH_PATHS)
|
||||
}
|
||||
|
||||
57
src/shared/data-path.test.ts
Normal file
57
src/shared/data-path.test.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
||||
import { homedir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { getCacheDir, getDataDir } from "./data-path"
|
||||
|
||||
describe("data-path", () => {
|
||||
let originalPlatform: NodeJS.Platform
|
||||
let originalEnv: Record<string, string | undefined>
|
||||
|
||||
beforeEach(() => {
|
||||
originalPlatform = process.platform
|
||||
originalEnv = {
|
||||
LOCALAPPDATA: process.env.LOCALAPPDATA,
|
||||
APPDATA: process.env.APPDATA,
|
||||
XDG_DATA_HOME: process.env.XDG_DATA_HOME,
|
||||
XDG_CACHE_HOME: process.env.XDG_CACHE_HOME,
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, "platform", { value: originalPlatform })
|
||||
for (const [key, value] of Object.entries(originalEnv)) {
|
||||
if (value !== undefined) {
|
||||
process.env[key] = value
|
||||
} else {
|
||||
delete process.env[key]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test("#given Windows with LOCALAPPDATA #when getDataDir is called #then returns LOCALAPPDATA", () => {
|
||||
Object.defineProperty(process, "platform", { value: "win32" })
|
||||
process.env.LOCALAPPDATA = "C:\\Users\\TestUser\\AppData\\Local"
|
||||
|
||||
const result = getDataDir()
|
||||
|
||||
expect(result).toBe("C:\\Users\\TestUser\\AppData\\Local")
|
||||
})
|
||||
|
||||
test("#given Windows without LOCALAPPDATA #when getDataDir is called #then falls back to AppData Local", () => {
|
||||
Object.defineProperty(process, "platform", { value: "win32" })
|
||||
delete process.env.LOCALAPPDATA
|
||||
|
||||
const result = getDataDir()
|
||||
|
||||
expect(result).toBe(join(homedir(), "AppData", "Local"))
|
||||
})
|
||||
|
||||
test("#given Windows with LOCALAPPDATA #when getCacheDir is called #then returns Local cache path", () => {
|
||||
Object.defineProperty(process, "platform", { value: "win32" })
|
||||
process.env.LOCALAPPDATA = "C:\\Users\\TestUser\\AppData\\Local"
|
||||
|
||||
const result = getCacheDir()
|
||||
|
||||
expect(result).toBe(join("C:\\Users\\TestUser\\AppData\\Local", "cache"))
|
||||
})
|
||||
})
|
||||
@@ -10,6 +10,10 @@ import * as os from "node:os"
|
||||
* including Windows, so we match that behavior exactly.
|
||||
*/
|
||||
export function getDataDir(): string {
|
||||
if (process.platform === "win32") {
|
||||
return process.env.LOCALAPPDATA ?? path.join(os.homedir(), "AppData", "Local")
|
||||
}
|
||||
|
||||
return process.env.XDG_DATA_HOME ?? path.join(os.homedir(), ".local", "share")
|
||||
}
|
||||
|
||||
@@ -27,6 +31,11 @@ export function getOpenCodeStorageDir(): string {
|
||||
* - All platforms: XDG_CACHE_HOME or ~/.cache
|
||||
*/
|
||||
export function getCacheDir(): string {
|
||||
if (process.platform === "win32") {
|
||||
const localAppData = process.env.LOCALAPPDATA ?? path.join(os.homedir(), "AppData", "Local")
|
||||
return path.join(localAppData, "cache")
|
||||
}
|
||||
|
||||
return process.env.XDG_CACHE_HOME ?? path.join(os.homedir(), ".cache")
|
||||
}
|
||||
|
||||
|
||||
@@ -179,7 +179,7 @@ describe("opencode-config-dir", () => {
|
||||
expect(result).toBe(join(homedir(), ".config", "opencode"))
|
||||
})
|
||||
|
||||
test("returns ~/.config/opencode on Windows by default", () => {
|
||||
test("returns %APPDATA%/opencode on Windows by default", () => {
|
||||
// given opencode CLI binary detected, platform is Windows
|
||||
Object.defineProperty(process, "platform", { value: "win32" })
|
||||
delete process.env.APPDATA
|
||||
@@ -188,8 +188,8 @@ describe("opencode-config-dir", () => {
|
||||
// when getOpenCodeConfigDir is called with binary="opencode"
|
||||
const result = getOpenCodeConfigDir({ binary: "opencode", version: "1.0.200", checkExisting: false })
|
||||
|
||||
// then returns ~/.config/opencode (cross-platform default)
|
||||
expect(result).toBe(join(homedir(), ".config", "opencode"))
|
||||
// then returns %APPDATA%/opencode
|
||||
expect(result).toBe(join(homedir(), "AppData", "Roaming", "opencode"))
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -42,29 +42,32 @@ function getTauriConfigDir(identifier: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
function getCliConfigDir(): string {
|
||||
function getCliConfigDir(checkExisting = true): string {
|
||||
const envConfigDir = process.env.OPENCODE_CONFIG_DIR?.trim()
|
||||
if (envConfigDir) {
|
||||
return resolve(envConfigDir)
|
||||
}
|
||||
|
||||
if (process.platform === "win32") {
|
||||
const crossPlatformDir = join(homedir(), ".config", "opencode")
|
||||
const crossPlatformConfig = join(crossPlatformDir, "opencode.json")
|
||||
|
||||
if (existsSync(crossPlatformConfig)) {
|
||||
return crossPlatformDir
|
||||
}
|
||||
|
||||
const appData = process.env.APPDATA || join(homedir(), "AppData", "Roaming")
|
||||
const appdataDir = join(appData, "opencode")
|
||||
const crossPlatformDir = join(homedir(), ".config", "opencode")
|
||||
if (!checkExisting) {
|
||||
return appdataDir
|
||||
}
|
||||
|
||||
const appdataConfig = join(appdataDir, "opencode.json")
|
||||
const crossPlatformConfig = join(crossPlatformDir, "opencode.json")
|
||||
|
||||
if (existsSync(appdataConfig)) {
|
||||
return appdataDir
|
||||
}
|
||||
|
||||
return crossPlatformDir
|
||||
if (existsSync(crossPlatformConfig)) {
|
||||
return crossPlatformDir
|
||||
}
|
||||
|
||||
return appdataDir
|
||||
}
|
||||
|
||||
const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), ".config")
|
||||
@@ -75,14 +78,14 @@ export function getOpenCodeConfigDir(options: OpenCodeConfigDirOptions): string
|
||||
const { binary, version, checkExisting = true } = options
|
||||
|
||||
if (binary === "opencode") {
|
||||
return getCliConfigDir()
|
||||
return getCliConfigDir(checkExisting)
|
||||
}
|
||||
|
||||
const identifier = isDevBuild(version) ? TAURI_APP_IDENTIFIER_DEV : TAURI_APP_IDENTIFIER
|
||||
const tauriDir = getTauriConfigDir(identifier)
|
||||
|
||||
if (checkExisting) {
|
||||
const legacyDir = getCliConfigDir()
|
||||
const legacyDir = getCliConfigDir(true)
|
||||
const legacyConfig = join(legacyDir, "opencode.json")
|
||||
const legacyConfigC = join(legacyDir, "opencode.jsonc")
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ describe("shell-env", () => {
|
||||
originalEnv = {
|
||||
SHELL: process.env.SHELL,
|
||||
PSModulePath: process.env.PSModulePath,
|
||||
COMSPEC: process.env.COMSPEC,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -57,6 +58,7 @@ describe("shell-env", () => {
|
||||
test("#given Windows platform without PSModulePath #when detectShellType is called #then returns cmd", () => {
|
||||
delete process.env.PSModulePath
|
||||
delete process.env.SHELL
|
||||
delete process.env.COMSPEC
|
||||
Object.defineProperty(process, "platform", { value: "win32" })
|
||||
|
||||
const result = detectShellType()
|
||||
@@ -77,12 +79,35 @@ describe("shell-env", () => {
|
||||
test("#given PSModulePath takes priority over SHELL #when both are set #then returns powershell", () => {
|
||||
process.env.PSModulePath = "C:\\Program Files\\PowerShell\\Modules"
|
||||
process.env.SHELL = "/bin/bash"
|
||||
process.env.COMSPEC = "C:\\Windows\\System32\\cmd.exe"
|
||||
Object.defineProperty(process, "platform", { value: "win32" })
|
||||
|
||||
const result = detectShellType()
|
||||
|
||||
expect(result).toBe("powershell")
|
||||
})
|
||||
|
||||
test("#given Windows COMSPEC points to powershell #when detectShellType is called #then returns powershell", () => {
|
||||
delete process.env.PSModulePath
|
||||
delete process.env.SHELL
|
||||
process.env.COMSPEC = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"
|
||||
Object.defineProperty(process, "platform", { value: "win32" })
|
||||
|
||||
const result = detectShellType()
|
||||
|
||||
expect(result).toBe("powershell")
|
||||
})
|
||||
|
||||
test("#given Windows COMSPEC points to bash executable #when detectShellType is called #then returns unix", () => {
|
||||
delete process.env.PSModulePath
|
||||
delete process.env.SHELL
|
||||
process.env.COMSPEC = "C:\\Program Files\\Git\\bin\\bash.exe"
|
||||
Object.defineProperty(process, "platform", { value: "win32" })
|
||||
|
||||
const result = detectShellType()
|
||||
|
||||
expect(result).toBe("unix")
|
||||
})
|
||||
})
|
||||
|
||||
describe("shellEscape", () => {
|
||||
|
||||
@@ -13,11 +13,29 @@ export function detectShellType(): ShellType {
|
||||
return "powershell"
|
||||
}
|
||||
|
||||
if (process.platform === "win32") {
|
||||
const comspec = process.env.COMSPEC ?? process.env.ComSpec
|
||||
const normalizedComspec = comspec?.toLowerCase()
|
||||
if (normalizedComspec?.includes("powershell") || normalizedComspec?.includes("pwsh")) {
|
||||
return "powershell"
|
||||
}
|
||||
|
||||
if (normalizedComspec?.includes("bash") || normalizedComspec?.includes("zsh")) {
|
||||
return "unix"
|
||||
}
|
||||
|
||||
if (process.env.SHELL) {
|
||||
return "unix"
|
||||
}
|
||||
|
||||
return "cmd"
|
||||
}
|
||||
|
||||
if (process.env.SHELL) {
|
||||
return "unix"
|
||||
}
|
||||
|
||||
return process.platform === "win32" ? "cmd" : "unix"
|
||||
return "unix"
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user