Compare commits
1 Commits
fix/issue-
...
fix/issue-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
deef9d864b |
@@ -1,47 +0,0 @@
|
||||
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,14 +9,6 @@ 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
|
||||
@@ -24,8 +16,7 @@ export async function runBunInstall(): Promise<boolean> {
|
||||
|
||||
export async function runBunInstallWithDetails(): Promise<BunInstallResult> {
|
||||
try {
|
||||
const bunCommand = resolveBunCommand()
|
||||
const proc = Bun.spawn([bunCommand, "install"], {
|
||||
const proc = Bun.spawn(["bun", "install"], {
|
||||
cwd: getConfigDir(),
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
@@ -48,7 +39,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 in ${getConfigDir()}: bun install`,
|
||||
error: `bun install timed out after ${BUN_INSTALL_TIMEOUT_SECONDS} seconds. Try running manually: cd ${getConfigDir()} && bun i`,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +55,7 @@ export async function runBunInstallWithDetails(): Promise<BunInstallResult> {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
return {
|
||||
success: false,
|
||||
error: `bun install failed: ${message}. Ensure Bun is installed and available in PATH: https://bun.sh/docs/installation`,
|
||||
error: `bun install failed: ${message}. Is bun installed? Try: curl -fsSL https://bun.sh/install | bash`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
59
src/features/claude-code-command-loader/loader.test.ts
Normal file
59
src/features/claude-code-command-loader/loader.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import { afterEach, describe, expect, it } from "bun:test"
|
||||
import { mkdirSync, rmSync, writeFileSync } from "node:fs"
|
||||
import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { loadOpencodeGlobalCommands, loadOpencodeProjectCommands } from "./loader"
|
||||
|
||||
const testRoots: string[] = []
|
||||
|
||||
function createTempRoot(): string {
|
||||
const root = join(tmpdir(), `command-loader-${Date.now()}-${Math.random().toString(16).slice(2)}`)
|
||||
mkdirSync(root, { recursive: true })
|
||||
testRoots.push(root)
|
||||
return root
|
||||
}
|
||||
|
||||
function writeCommand(dir: string, name: string): void {
|
||||
mkdirSync(dir, { recursive: true })
|
||||
writeFileSync(
|
||||
join(dir, `${name}.md`),
|
||||
"---\ndescription: command from test\n---\nUse this command"
|
||||
)
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of testRoots.splice(0)) {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
delete process.env.OPENCODE_CONFIG_DIR
|
||||
})
|
||||
|
||||
describe("claude-code-command-loader OpenCode paths", () => {
|
||||
it("loads commands from global OpenCode commands directory", async () => {
|
||||
// given
|
||||
const root = createTempRoot()
|
||||
const opencodeConfigDir = join(root, "config")
|
||||
writeCommand(join(opencodeConfigDir, "commands"), "global-opencode")
|
||||
process.env.OPENCODE_CONFIG_DIR = opencodeConfigDir
|
||||
|
||||
// when
|
||||
const commands = await loadOpencodeGlobalCommands()
|
||||
|
||||
// then
|
||||
expect(commands["global-opencode"]).toBeDefined()
|
||||
})
|
||||
|
||||
it("loads commands from project OpenCode commands directory", async () => {
|
||||
// given
|
||||
const root = createTempRoot()
|
||||
writeCommand(join(root, ".opencode", "commands"), "project-opencode")
|
||||
|
||||
// when
|
||||
const commands = await loadOpencodeProjectCommands(root)
|
||||
|
||||
// then
|
||||
expect(commands["project-opencode"]).toBeDefined()
|
||||
})
|
||||
})
|
||||
@@ -122,13 +122,13 @@ export async function loadProjectCommands(directory?: string): Promise<Record<st
|
||||
|
||||
export async function loadOpencodeGlobalCommands(): Promise<Record<string, CommandDefinition>> {
|
||||
const configDir = getOpenCodeConfigDir({ binary: "opencode" })
|
||||
const opencodeCommandsDir = join(configDir, "command")
|
||||
const opencodeCommandsDir = join(configDir, "commands")
|
||||
const commands = await loadCommandsFromDir(opencodeCommandsDir, "opencode")
|
||||
return commandsToRecord(commands)
|
||||
}
|
||||
|
||||
export async function loadOpencodeProjectCommands(directory?: string): Promise<Record<string, CommandDefinition>> {
|
||||
const opencodeProjectDir = join(directory ?? process.cwd(), ".opencode", "command")
|
||||
const opencodeProjectDir = join(directory ?? process.cwd(), ".opencode", "commands")
|
||||
const commands = await loadCommandsFromDir(opencodeProjectDir, "opencode-project")
|
||||
return commandsToRecord(commands)
|
||||
}
|
||||
|
||||
63
src/hooks/auto-slash-command/executor.test.ts
Normal file
63
src/hooks/auto-slash-command/executor.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import { afterEach, describe, expect, it } from "bun:test"
|
||||
import { mkdirSync, rmSync, writeFileSync } from "node:fs"
|
||||
import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { executeSlashCommand } from "./executor"
|
||||
|
||||
const testRoots: string[] = []
|
||||
|
||||
function createTempRoot(): string {
|
||||
const root = join(tmpdir(), `auto-slash-executor-${Date.now()}-${Math.random().toString(16).slice(2)}`)
|
||||
mkdirSync(root, { recursive: true })
|
||||
testRoots.push(root)
|
||||
return root
|
||||
}
|
||||
|
||||
function writeCommand(dir: string, name: string): void {
|
||||
mkdirSync(dir, { recursive: true })
|
||||
writeFileSync(
|
||||
join(dir, `${name}.md`),
|
||||
"---\ndescription: command from test\n---\nRun from OpenCode command directory"
|
||||
)
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of testRoots.splice(0)) {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
delete process.env.OPENCODE_CONFIG_DIR
|
||||
})
|
||||
|
||||
describe("auto-slash-command executor OpenCode paths", () => {
|
||||
it("resolves commands from OpenCode global and project plural directories", async () => {
|
||||
// given
|
||||
const root = createTempRoot()
|
||||
const opencodeConfigDir = join(root, "config")
|
||||
writeCommand(join(opencodeConfigDir, "commands"), "global-cmd")
|
||||
writeCommand(join(root, ".opencode", "commands"), "project-cmd")
|
||||
process.env.OPENCODE_CONFIG_DIR = opencodeConfigDir
|
||||
|
||||
const originalCwd = process.cwd()
|
||||
process.chdir(root)
|
||||
|
||||
try {
|
||||
// when
|
||||
const globalResult = await executeSlashCommand(
|
||||
{ command: "global-cmd", args: "", raw: "/global-cmd" },
|
||||
{ skills: [] }
|
||||
)
|
||||
const projectResult = await executeSlashCommand(
|
||||
{ command: "project-cmd", args: "", raw: "/project-cmd" },
|
||||
{ skills: [] }
|
||||
)
|
||||
|
||||
// then
|
||||
expect(globalResult.success).toBe(true)
|
||||
expect(projectResult.success).toBe(true)
|
||||
} finally {
|
||||
process.chdir(originalCwd)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -105,8 +105,8 @@ async function discoverAllCommands(options?: ExecutorOptions): Promise<CommandIn
|
||||
const configDir = getOpenCodeConfigDir({ binary: "opencode" })
|
||||
const userCommandsDir = join(getClaudeConfigDir(), "commands")
|
||||
const projectCommandsDir = join(process.cwd(), ".claude", "commands")
|
||||
const opencodeGlobalDir = join(configDir, "command")
|
||||
const opencodeProjectDir = join(process.cwd(), ".opencode", "command")
|
||||
const opencodeGlobalDir = join(configDir, "commands")
|
||||
const opencodeProjectDir = join(process.cwd(), ".opencode", "commands")
|
||||
|
||||
const userCommands = discoverCommandsFromDir(userCommandsDir, "user")
|
||||
const opencodeGlobalCommands = discoverCommandsFromDir(opencodeGlobalDir, "opencode")
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
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,7 +2,6 @@ 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[],
|
||||
@@ -20,17 +19,9 @@ 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)
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
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,10 +10,6 @@ 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")
|
||||
}
|
||||
|
||||
@@ -31,11 +27,6 @@ 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 %APPDATA%/opencode on Windows by default", () => {
|
||||
test("returns ~/.config/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 %APPDATA%/opencode
|
||||
expect(result).toBe(join(homedir(), "AppData", "Roaming", "opencode"))
|
||||
// then returns ~/.config/opencode (cross-platform default)
|
||||
expect(result).toBe(join(homedir(), ".config", "opencode"))
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -42,32 +42,29 @@ function getTauriConfigDir(identifier: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
function getCliConfigDir(checkExisting = true): string {
|
||||
function getCliConfigDir(): string {
|
||||
const envConfigDir = process.env.OPENCODE_CONFIG_DIR?.trim()
|
||||
if (envConfigDir) {
|
||||
return resolve(envConfigDir)
|
||||
}
|
||||
|
||||
if (process.platform === "win32") {
|
||||
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
|
||||
}
|
||||
|
||||
if (existsSync(crossPlatformConfig)) {
|
||||
return crossPlatformDir
|
||||
}
|
||||
|
||||
return appdataDir
|
||||
const appData = process.env.APPDATA || join(homedir(), "AppData", "Roaming")
|
||||
const appdataDir = join(appData, "opencode")
|
||||
const appdataConfig = join(appdataDir, "opencode.json")
|
||||
|
||||
if (existsSync(appdataConfig)) {
|
||||
return appdataDir
|
||||
}
|
||||
|
||||
return crossPlatformDir
|
||||
}
|
||||
|
||||
const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), ".config")
|
||||
@@ -78,14 +75,14 @@ export function getOpenCodeConfigDir(options: OpenCodeConfigDirOptions): string
|
||||
const { binary, version, checkExisting = true } = options
|
||||
|
||||
if (binary === "opencode") {
|
||||
return getCliConfigDir(checkExisting)
|
||||
return getCliConfigDir()
|
||||
}
|
||||
|
||||
const identifier = isDevBuild(version) ? TAURI_APP_IDENTIFIER_DEV : TAURI_APP_IDENTIFIER
|
||||
const tauriDir = getTauriConfigDir(identifier)
|
||||
|
||||
if (checkExisting) {
|
||||
const legacyDir = getCliConfigDir(true)
|
||||
const legacyDir = getCliConfigDir()
|
||||
const legacyConfig = join(legacyDir, "opencode.json")
|
||||
const legacyConfigC = join(legacyDir, "opencode.jsonc")
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ describe("shell-env", () => {
|
||||
originalEnv = {
|
||||
SHELL: process.env.SHELL,
|
||||
PSModulePath: process.env.PSModulePath,
|
||||
COMSPEC: process.env.COMSPEC,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -58,7 +57,6 @@ 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()
|
||||
@@ -79,35 +77,12 @@ 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,29 +13,11 @@ 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 "unix"
|
||||
return process.platform === "win32" ? "cmd" : "unix"
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -52,8 +52,8 @@ export function discoverCommandsSync(directory?: string): CommandInfo[] {
|
||||
const configDir = getOpenCodeConfigDir({ binary: "opencode" })
|
||||
const userCommandsDir = join(getClaudeConfigDir(), "commands")
|
||||
const projectCommandsDir = join(directory ?? process.cwd(), ".claude", "commands")
|
||||
const opencodeGlobalDir = join(configDir, "command")
|
||||
const opencodeProjectDir = join(directory ?? process.cwd(), ".opencode", "command")
|
||||
const opencodeGlobalDir = join(configDir, "commands")
|
||||
const opencodeProjectDir = join(directory ?? process.cwd(), ".opencode", "commands")
|
||||
|
||||
const userCommands = discoverCommandsFromDir(userCommandsDir, "user")
|
||||
const opencodeGlobalCommands = discoverCommandsFromDir(opencodeGlobalDir, "opencode")
|
||||
|
||||
@@ -1,6 +1,27 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import { afterEach, describe, expect, it } from "bun:test"
|
||||
import { mkdirSync, rmSync, writeFileSync } from "node:fs"
|
||||
import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import * as slashcommand from "./index"
|
||||
|
||||
const testRoots: string[] = []
|
||||
|
||||
function createTempRoot(): string {
|
||||
const root = join(tmpdir(), `slashcommand-discovery-${Date.now()}-${Math.random().toString(16).slice(2)}`)
|
||||
mkdirSync(root, { recursive: true })
|
||||
testRoots.push(root)
|
||||
return root
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of testRoots.splice(0)) {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
delete process.env.OPENCODE_CONFIG_DIR
|
||||
})
|
||||
|
||||
describe("slashcommand module exports", () => {
|
||||
it("exports discovery API only", () => {
|
||||
// given
|
||||
@@ -14,4 +35,32 @@ describe("slashcommand module exports", () => {
|
||||
expect(exportNames).not.toContain("createSlashcommandTool")
|
||||
expect(exportNames).not.toContain("slashcommand")
|
||||
})
|
||||
|
||||
it("discovers commands from OpenCode plural command directories", () => {
|
||||
// given
|
||||
const root = createTempRoot()
|
||||
const opencodeConfigDir = join(root, "config")
|
||||
const globalCommandsDir = join(opencodeConfigDir, "commands")
|
||||
const projectCommandsDir = join(root, ".opencode", "commands")
|
||||
|
||||
mkdirSync(globalCommandsDir, { recursive: true })
|
||||
mkdirSync(projectCommandsDir, { recursive: true })
|
||||
|
||||
writeFileSync(
|
||||
join(globalCommandsDir, "global-cmd.md"),
|
||||
"---\ndescription: global command\n---\nGlobal command body"
|
||||
)
|
||||
writeFileSync(
|
||||
join(projectCommandsDir, "project-cmd.md"),
|
||||
"---\ndescription: project command\n---\nProject command body"
|
||||
)
|
||||
process.env.OPENCODE_CONFIG_DIR = opencodeConfigDir
|
||||
|
||||
// when
|
||||
const commands = slashcommand.discoverCommandsSync(root)
|
||||
|
||||
// then
|
||||
expect(commands.some((cmd) => cmd.name === "global-cmd" && cmd.scope === "opencode")).toBe(true)
|
||||
expect(commands.some((cmd) => cmd.name === "project-cmd" && cmd.scope === "opencode-project")).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user