Compare commits

...

3 Commits

Author SHA1 Message Date
YeonGyu-Kim
deef9d864b fix(slashcommand): use "commands" (plural) for OpenCode command directories
Closes #1918
2026-02-24 21:37:02 +09:00
github-actions[bot]
55b9ad60d8 release: v3.8.5 2026-02-24 09:45:36 +00:00
YeonGyu-Kim
e997e0071c Merge pull request #2088 from minpeter/feat/hashline-edit-error-hints
fix(hashline-edit): improve error messages for invalid LINE#ID references
2026-02-24 18:36:04 +09:00
14 changed files with 193 additions and 22 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode",
"version": "3.8.4",
"version": "3.8.5",
"description": "The Best AI Agent Harness - Batteries-Included OpenCode Plugin with Multi-Model Orchestration, Parallel Background Agents, and Crafted LSP/AST Tools",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@@ -74,13 +74,13 @@
"typescript": "^5.7.3"
},
"optionalDependencies": {
"oh-my-opencode-darwin-arm64": "3.8.4",
"oh-my-opencode-darwin-x64": "3.8.4",
"oh-my-opencode-linux-arm64": "3.8.4",
"oh-my-opencode-linux-arm64-musl": "3.8.4",
"oh-my-opencode-linux-x64": "3.8.4",
"oh-my-opencode-linux-x64-musl": "3.8.4",
"oh-my-opencode-windows-x64": "3.8.4"
"oh-my-opencode-darwin-arm64": "3.8.5",
"oh-my-opencode-darwin-x64": "3.8.5",
"oh-my-opencode-linux-arm64": "3.8.5",
"oh-my-opencode-linux-arm64-musl": "3.8.5",
"oh-my-opencode-linux-x64": "3.8.5",
"oh-my-opencode-linux-x64-musl": "3.8.5",
"oh-my-opencode-windows-x64": "3.8.5"
},
"trustedDependencies": [
"@ast-grep/cli",

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode-darwin-arm64",
"version": "3.8.4",
"version": "3.8.5",
"description": "Platform-specific binary for oh-my-opencode (darwin-arm64)",
"license": "MIT",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode-darwin-x64",
"version": "3.8.4",
"version": "3.8.5",
"description": "Platform-specific binary for oh-my-opencode (darwin-x64)",
"license": "MIT",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode-linux-arm64-musl",
"version": "3.8.4",
"version": "3.8.5",
"description": "Platform-specific binary for oh-my-opencode (linux-arm64-musl)",
"license": "MIT",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode-linux-arm64",
"version": "3.8.4",
"version": "3.8.5",
"description": "Platform-specific binary for oh-my-opencode (linux-arm64)",
"license": "MIT",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode-linux-x64-musl",
"version": "3.8.4",
"version": "3.8.5",
"description": "Platform-specific binary for oh-my-opencode (linux-x64-musl)",
"license": "MIT",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode-linux-x64",
"version": "3.8.4",
"version": "3.8.5",
"description": "Platform-specific binary for oh-my-opencode (linux-x64)",
"license": "MIT",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode-windows-x64",
"version": "3.8.4",
"version": "3.8.5",
"description": "Platform-specific binary for oh-my-opencode (windows-x64)",
"license": "MIT",
"repository": {

View 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()
})
})

View File

@@ -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)
}

View 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)
}
})
})

View File

@@ -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")

View File

@@ -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")

View File

@@ -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)
})
})