mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-20 09:32:19 +00:00
refactor: extract shell tool factory to eliminate duplication
This commit is contained in:
parent
67dfbcbcfd
commit
3e26c3ae83
4 changed files with 88 additions and 225 deletions
|
|
@ -1,76 +1,7 @@
|
|||
import z from "zod"
|
||||
import { Tool } from "../tool"
|
||||
import DESCRIPTION from "./shell.txt"
|
||||
import { Log } from "@/util/log"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { Flag } from "@/flag/flag"
|
||||
import { Shell } from "@/shell/shell"
|
||||
import { resolvePath, formatShellDescription, askPermission } from "./util"
|
||||
import { ShellParser } from "./parser"
|
||||
import { ShellRunner } from "./runner"
|
||||
import { createShellTool } from "./util"
|
||||
|
||||
export const log = Log.create({ service: "bash-tool" })
|
||||
|
||||
const DEFAULT_TIMEOUT = Flag.OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS || 2 * 60 * 1000
|
||||
|
||||
const NAME = "bash"
|
||||
|
||||
export const BashTool = Tool.define(NAME, async () => {
|
||||
const shell = Shell.acceptable()
|
||||
const name = Shell.name(shell)
|
||||
log.info("bash tool using shell", { shell, name })
|
||||
|
||||
return {
|
||||
description: formatShellDescription(DESCRIPTION, {
|
||||
name,
|
||||
shellName: "Bash",
|
||||
chaining:
|
||||
"use a single Bash call with '&&' to chain them together (e.g., `git add . && git commit -m \"message\" && git push`).",
|
||||
}),
|
||||
parameters: z.object({
|
||||
command: z.string().describe("The command to execute"),
|
||||
timeout: z.number().describe("Optional timeout in milliseconds").optional(),
|
||||
workdir: z
|
||||
.string()
|
||||
.describe(
|
||||
`The working directory to run the command in. Defaults to ${Instance.directory}. Use this instead of 'cd' commands.`,
|
||||
)
|
||||
.optional(),
|
||||
description: z
|
||||
.string()
|
||||
.describe(
|
||||
"Clear, concise description of what this command does in 5-10 words. Examples:\nInput: ls\nOutput: Lists files in current directory\n\nInput: git status\nOutput: Shows working tree status\n\nInput: npm install\nOutput: Installs package dependencies\n\nInput: mkdir foo\nOutput: Creates directory 'foo'",
|
||||
),
|
||||
}),
|
||||
async execute(params, ctx) {
|
||||
const cwd = params.workdir ? await resolvePath(params.workdir, Instance.directory, shell) : Instance.directory
|
||||
if (params.timeout !== undefined && params.timeout < 0) {
|
||||
throw new Error(`Invalid timeout value: ${params.timeout}. Timeout must be a positive number.`)
|
||||
}
|
||||
const timeout = params.timeout ?? DEFAULT_TIMEOUT
|
||||
|
||||
const scan = await ShellParser.collect({
|
||||
command: params.command,
|
||||
cwd,
|
||||
shell,
|
||||
shellType: NAME,
|
||||
})
|
||||
if (!Instance.containsPath(cwd)) scan.dirs.add(cwd)
|
||||
|
||||
await askPermission(ctx, scan, NAME)
|
||||
|
||||
return ShellRunner.run(
|
||||
{
|
||||
shell,
|
||||
name,
|
||||
command: params.command,
|
||||
cwd,
|
||||
env: await ShellRunner.shellEnv(ctx, cwd),
|
||||
timeout,
|
||||
description: params.description,
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
},
|
||||
}
|
||||
})
|
||||
export const BashTool = createShellTool(
|
||||
"bash",
|
||||
"Bash",
|
||||
"use a single Bash call with '&&' to chain them together (e.g., `git add . && git commit -m \"message\" && git push`).",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,76 +1,7 @@
|
|||
import z from "zod"
|
||||
import { Tool } from "../tool"
|
||||
import DESCRIPTION from "./shell.txt"
|
||||
import { Log } from "@/util/log"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { Flag } from "@/flag/flag"
|
||||
import { Shell } from "@/shell/shell"
|
||||
import { resolvePath, formatShellDescription, askPermission } from "./util"
|
||||
import { ShellParser } from "./parser"
|
||||
import { ShellRunner } from "./runner"
|
||||
import { createShellTool } from "./util"
|
||||
|
||||
export const log = Log.create({ service: "powershell-tool" })
|
||||
|
||||
const DEFAULT_TIMEOUT = Flag.OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS || 2 * 60 * 1000
|
||||
|
||||
const NAME = "powershell"
|
||||
|
||||
export const PowershellTool = Tool.define(NAME, async () => {
|
||||
const shell = Shell.acceptable()
|
||||
const name = Shell.name(shell)
|
||||
log.info("powershell tool using shell", { shell, name })
|
||||
|
||||
return {
|
||||
description: formatShellDescription(DESCRIPTION, {
|
||||
name,
|
||||
shellName: "Windows PowerShell",
|
||||
chaining:
|
||||
"avoid '&&' in this shell because Windows PowerShell 5.1 does not support it. Use PowerShell conditionals such as `cmd1; if ($?) { cmd2 }`",
|
||||
}),
|
||||
parameters: z.object({
|
||||
command: z.string().describe("The command to execute"),
|
||||
timeout: z.number().describe("Optional timeout in milliseconds").optional(),
|
||||
workdir: z
|
||||
.string()
|
||||
.describe(
|
||||
`The working directory to run the command in. Defaults to ${Instance.directory}. Use this instead of 'cd' commands.`,
|
||||
)
|
||||
.optional(),
|
||||
description: z
|
||||
.string()
|
||||
.describe(
|
||||
"Clear, concise description of what this command does in 5-10 words. Examples:\nInput: ls\nOutput: Lists files in current directory\n\nInput: git status\nOutput: Shows working tree status\n\nInput: npm install\nOutput: Installs package dependencies\n\nInput: mkdir foo\nOutput: Creates directory 'foo'",
|
||||
),
|
||||
}),
|
||||
async execute(params, ctx) {
|
||||
const cwd = params.workdir ? await resolvePath(params.workdir, Instance.directory, shell) : Instance.directory
|
||||
if (params.timeout !== undefined && params.timeout < 0) {
|
||||
throw new Error(`Invalid timeout value: ${params.timeout}. Timeout must be a positive number.`)
|
||||
}
|
||||
const timeout = params.timeout ?? DEFAULT_TIMEOUT
|
||||
|
||||
const scan = await ShellParser.collect({
|
||||
command: params.command,
|
||||
cwd,
|
||||
shell,
|
||||
shellType: NAME,
|
||||
})
|
||||
if (!Instance.containsPath(cwd)) scan.dirs.add(cwd)
|
||||
|
||||
await askPermission(ctx, scan, NAME)
|
||||
|
||||
return ShellRunner.run(
|
||||
{
|
||||
shell,
|
||||
name,
|
||||
command: params.command,
|
||||
cwd,
|
||||
env: await ShellRunner.shellEnv(ctx, cwd),
|
||||
timeout,
|
||||
description: params.description,
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
},
|
||||
}
|
||||
})
|
||||
export const PowershellTool = createShellTool(
|
||||
"powershell",
|
||||
"Windows PowerShell",
|
||||
"avoid '&&' in this shell because Windows PowerShell 5.1 does not support it. Use PowerShell conditionals such as `cmd1; if ($?) { cmd2 }`",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,76 +1,7 @@
|
|||
import z from "zod"
|
||||
import { Tool } from "../tool"
|
||||
import DESCRIPTION from "./shell.txt"
|
||||
import { Log } from "@/util/log"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { Flag } from "@/flag/flag"
|
||||
import { Shell } from "@/shell/shell"
|
||||
import { resolvePath, formatShellDescription, askPermission } from "./util"
|
||||
import { ShellParser } from "./parser"
|
||||
import { ShellRunner } from "./runner"
|
||||
import { createShellTool } from "./util"
|
||||
|
||||
export const log = Log.create({ service: "pwsh-tool" })
|
||||
|
||||
const DEFAULT_TIMEOUT = Flag.OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS || 2 * 60 * 1000
|
||||
|
||||
const NAME = "pwsh"
|
||||
|
||||
export const PwshTool = Tool.define(NAME, async () => {
|
||||
const shell = Shell.acceptable()
|
||||
const name = Shell.name(shell)
|
||||
log.info("pwsh tool using shell", { shell, name })
|
||||
|
||||
return {
|
||||
description: formatShellDescription(DESCRIPTION, {
|
||||
name,
|
||||
shellName: "PowerShell Core",
|
||||
chaining:
|
||||
"use a single PowerShell call with '&&' to chain them together (e.g., `git add . && git commit -m \"message\" && git push`).",
|
||||
}),
|
||||
parameters: z.object({
|
||||
command: z.string().describe("The command to execute"),
|
||||
timeout: z.number().describe("Optional timeout in milliseconds").optional(),
|
||||
workdir: z
|
||||
.string()
|
||||
.describe(
|
||||
`The working directory to run the command in. Defaults to ${Instance.directory}. Use this instead of 'cd' commands.`,
|
||||
)
|
||||
.optional(),
|
||||
description: z
|
||||
.string()
|
||||
.describe(
|
||||
"Clear, concise description of what this command does in 5-10 words. Examples:\nInput: ls\nOutput: Lists files in current directory\n\nInput: git status\nOutput: Shows working tree status\n\nInput: npm install\nOutput: Installs package dependencies\n\nInput: mkdir foo\nOutput: Creates directory 'foo'",
|
||||
),
|
||||
}),
|
||||
async execute(params, ctx) {
|
||||
const cwd = params.workdir ? await resolvePath(params.workdir, Instance.directory, shell) : Instance.directory
|
||||
if (params.timeout !== undefined && params.timeout < 0) {
|
||||
throw new Error(`Invalid timeout value: ${params.timeout}. Timeout must be a positive number.`)
|
||||
}
|
||||
const timeout = params.timeout ?? DEFAULT_TIMEOUT
|
||||
|
||||
const scan = await ShellParser.collect({
|
||||
command: params.command,
|
||||
cwd,
|
||||
shell,
|
||||
shellType: NAME,
|
||||
})
|
||||
if (!Instance.containsPath(cwd)) scan.dirs.add(cwd)
|
||||
|
||||
await askPermission(ctx, scan, NAME)
|
||||
|
||||
return ShellRunner.run(
|
||||
{
|
||||
shell,
|
||||
name,
|
||||
command: params.command,
|
||||
cwd,
|
||||
env: await ShellRunner.shellEnv(ctx, cwd),
|
||||
timeout,
|
||||
description: params.description,
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
},
|
||||
}
|
||||
})
|
||||
export const PwshTool = createShellTool(
|
||||
"pwsh",
|
||||
"PowerShell Core",
|
||||
"use a single PowerShell call with '&&' to chain them together (e.g., `git add . && git commit -m \"message\" && git push`).",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -91,6 +91,76 @@ export function formatShellDescription(template: string, opts: { name: string; s
|
|||
.replaceAll("${maxBytes}", String(Truncate.MAX_BYTES))
|
||||
}
|
||||
|
||||
import z from "zod"
|
||||
import DESCRIPTION from "./shell.txt"
|
||||
import { Log } from "@/util/log"
|
||||
import { Flag } from "@/flag/flag"
|
||||
import { ShellParser } from "./parser"
|
||||
import { ShellRunner } from "./runner"
|
||||
|
||||
export type ShellType = "bash" | "pwsh" | "powershell"
|
||||
|
||||
const DEFAULT_TIMEOUT = Flag.OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS || 2 * 60 * 1000
|
||||
|
||||
export function createShellTool(id: ShellType, shellName: string, chaining: string) {
|
||||
const log = Log.create({ service: `${id}-tool` })
|
||||
|
||||
return Tool.define(id, async () => {
|
||||
const shell = Shell.acceptable()
|
||||
const name = Shell.name(shell)
|
||||
log.info(`${id} tool using shell`, { shell, name })
|
||||
|
||||
return {
|
||||
description: formatShellDescription(DESCRIPTION, { name, shellName, chaining }),
|
||||
parameters: z.object({
|
||||
command: z.string().describe("The command to execute"),
|
||||
timeout: z.number().describe("Optional timeout in milliseconds").optional(),
|
||||
workdir: z
|
||||
.string()
|
||||
.describe(
|
||||
`The working directory to run the command in. Defaults to ${Instance.directory}. Use this instead of 'cd' commands.`,
|
||||
)
|
||||
.optional(),
|
||||
description: z
|
||||
.string()
|
||||
.describe(
|
||||
"Clear, concise description of what this command does in 5-10 words. Examples:\nInput: ls\nOutput: Lists files in current directory\n\nInput: git status\nOutput: Shows working tree status\n\nInput: npm install\nOutput: Installs package dependencies\n\nInput: mkdir foo\nOutput: Creates directory 'foo'",
|
||||
),
|
||||
}),
|
||||
async execute(params, ctx) {
|
||||
const cwd = params.workdir ? await resolvePath(params.workdir, Instance.directory, shell) : Instance.directory
|
||||
if (params.timeout !== undefined && params.timeout < 0) {
|
||||
throw new Error(`Invalid timeout value: ${params.timeout}. Timeout must be a positive number.`)
|
||||
}
|
||||
const timeout = params.timeout ?? DEFAULT_TIMEOUT
|
||||
|
||||
const scan = await ShellParser.collect({
|
||||
command: params.command,
|
||||
cwd,
|
||||
shell,
|
||||
shellType: id,
|
||||
})
|
||||
if (!Instance.containsPath(cwd)) scan.dirs.add(cwd)
|
||||
|
||||
await askPermission(ctx, scan, id)
|
||||
|
||||
return ShellRunner.run(
|
||||
{
|
||||
shell,
|
||||
name,
|
||||
command: params.command,
|
||||
cwd,
|
||||
env: await ShellRunner.shellEnv(ctx, cwd),
|
||||
timeout,
|
||||
description: params.description,
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function askPermission(ctx: Tool.Context, scan: Scan, permissionName: string = "bash") {
|
||||
if (scan.dirs.size > 0) {
|
||||
const globs = Array.from(scan.dirs).map((dir) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue