mirror of
https://github.com/sst/opencode.git
synced 2025-07-08 00:25:00 +00:00
add auto formatting and experimental hooks feature
This commit is contained in:
parent
a4f32d602b
commit
f8b78f08b4
8 changed files with 269 additions and 3 deletions
|
@ -1,3 +1,15 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://opencode.ai/config.json"
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"experimental": {
|
||||||
|
"hook": {
|
||||||
|
"file_edited": {
|
||||||
|
".json": []
|
||||||
|
},
|
||||||
|
"session_completed": [
|
||||||
|
{
|
||||||
|
"command": ["touch", "./node_modules/foo"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -183,6 +183,9 @@
|
||||||
"temperature": {
|
"temperature": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"tool_call": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"cost": {
|
"cost": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -223,6 +226,10 @@
|
||||||
},
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
@ -295,6 +302,69 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"description": "MCP (Model Context Protocol) server configurations"
|
"description": "MCP (Model Context Protocol) server configurations"
|
||||||
|
},
|
||||||
|
"experimental": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"hook": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"file_edited": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"command": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"command"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"session_completed": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"command": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"command"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
|
|
|
@ -171,4 +171,3 @@ export const RunCommand = cmd({
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -167,6 +167,32 @@ export namespace Config {
|
||||||
.record(z.string(), Mcp)
|
.record(z.string(), Mcp)
|
||||||
.optional()
|
.optional()
|
||||||
.describe("MCP (Model Context Protocol) server configurations"),
|
.describe("MCP (Model Context Protocol) server configurations"),
|
||||||
|
experimental: z
|
||||||
|
.object({
|
||||||
|
hook: z
|
||||||
|
.object({
|
||||||
|
file_edited: z
|
||||||
|
.record(
|
||||||
|
z.string(),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
command: z.string().array(),
|
||||||
|
environment: z.record(z.string(), z.string()).optional(),
|
||||||
|
})
|
||||||
|
.array(),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
session_completed: z
|
||||||
|
.object({
|
||||||
|
command: z.string().array(),
|
||||||
|
environment: z.record(z.string(), z.string()).optional(),
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.openapi({
|
.openapi({
|
||||||
|
|
143
packages/opencode/src/format/index.ts
Normal file
143
packages/opencode/src/format/index.ts
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
import { App } from '../app/app'
|
||||||
|
import { BunProc } from '../bun'
|
||||||
|
import { Config } from '../config/config'
|
||||||
|
import { Log } from '../util/log'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
export namespace Format {
|
||||||
|
const log = Log.create({ service: 'format' })
|
||||||
|
|
||||||
|
const state = App.state('format', async () => {
|
||||||
|
const hooks: Record<string, Hook[]> = {}
|
||||||
|
for (const item of FORMATTERS) {
|
||||||
|
if (await item.enabled()) {
|
||||||
|
for (const ext of item.extensions) {
|
||||||
|
const list = hooks[ext] ?? []
|
||||||
|
list.push({
|
||||||
|
command: item.command,
|
||||||
|
environment: item.environment,
|
||||||
|
})
|
||||||
|
hooks[ext] = list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cfg = await Config.get()
|
||||||
|
for (const [file, items] of Object.entries(
|
||||||
|
cfg.experimental?.hook?.file_edited ?? {},
|
||||||
|
)) {
|
||||||
|
for (const item of items) {
|
||||||
|
const list = hooks[file] ?? []
|
||||||
|
list.push({
|
||||||
|
command: item.command,
|
||||||
|
environment: item.environment,
|
||||||
|
})
|
||||||
|
hooks[file] = list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hooks,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export async function run(file: string) {
|
||||||
|
log.info('formatting', { file })
|
||||||
|
const { hooks } = await state()
|
||||||
|
const ext = path.extname(file)
|
||||||
|
const match = hooks[ext]
|
||||||
|
if (!match) return
|
||||||
|
|
||||||
|
for (const item of match) {
|
||||||
|
log.info('running', { command: item.command })
|
||||||
|
const proc = Bun.spawn({
|
||||||
|
cmd: item.command.map((x) => x.replace('$FILE', file)),
|
||||||
|
cwd: App.info().path.cwd,
|
||||||
|
env: item.environment,
|
||||||
|
})
|
||||||
|
const exit = await proc.exited
|
||||||
|
if (exit !== 0)
|
||||||
|
log.error('failed', {
|
||||||
|
command: item.command,
|
||||||
|
...item.environment,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Hook {
|
||||||
|
command: string[]
|
||||||
|
environment?: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Native {
|
||||||
|
name: string
|
||||||
|
command: string[]
|
||||||
|
environment?: Record<string, string>
|
||||||
|
extensions: string[]
|
||||||
|
enabled(): Promise<boolean>
|
||||||
|
}
|
||||||
|
|
||||||
|
const FORMATTERS: Native[] = [
|
||||||
|
{
|
||||||
|
name: 'prettier',
|
||||||
|
extensions: [
|
||||||
|
'.js',
|
||||||
|
'.jsx',
|
||||||
|
'.mjs',
|
||||||
|
'.cjs',
|
||||||
|
'.ts',
|
||||||
|
'.tsx',
|
||||||
|
'.mts',
|
||||||
|
'.cts',
|
||||||
|
'.html',
|
||||||
|
'.htm',
|
||||||
|
'.css',
|
||||||
|
'.scss',
|
||||||
|
'.sass',
|
||||||
|
'.less',
|
||||||
|
'.vue',
|
||||||
|
'.svelte',
|
||||||
|
'.json',
|
||||||
|
'.jsonc',
|
||||||
|
'.yaml',
|
||||||
|
'.yml',
|
||||||
|
'.toml',
|
||||||
|
'.xml',
|
||||||
|
'.md',
|
||||||
|
'.mdx',
|
||||||
|
'.php',
|
||||||
|
'.rb',
|
||||||
|
'.java',
|
||||||
|
'.go',
|
||||||
|
'.rs',
|
||||||
|
'.swift',
|
||||||
|
'.kt',
|
||||||
|
'.kts',
|
||||||
|
'.sol',
|
||||||
|
'.graphql',
|
||||||
|
'.gql',
|
||||||
|
],
|
||||||
|
command: [BunProc.which(), 'run', 'prettier', '--write', '$FILE'],
|
||||||
|
environment: {
|
||||||
|
BUN_BE_BUN: '1',
|
||||||
|
},
|
||||||
|
async enabled() {
|
||||||
|
try {
|
||||||
|
const proc = Bun.spawn({
|
||||||
|
cmd: [BunProc.which(), 'run', 'prettier', '--version'],
|
||||||
|
cwd: App.info().path.cwd,
|
||||||
|
env: {
|
||||||
|
BUN_BE_BUN: '1',
|
||||||
|
},
|
||||||
|
stdout: 'ignore',
|
||||||
|
stderr: 'ignore',
|
||||||
|
})
|
||||||
|
const exit = await proc.exited
|
||||||
|
return exit === 0
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
|
@ -853,6 +853,17 @@ export namespace Session {
|
||||||
[Symbol.dispose]() {
|
[Symbol.dispose]() {
|
||||||
log.info("unlocking", { sessionID })
|
log.info("unlocking", { sessionID })
|
||||||
state().pending.delete(sessionID)
|
state().pending.delete(sessionID)
|
||||||
|
Config.get().then((cfg) => {
|
||||||
|
if (cfg.experimental?.hook?.session_completed) {
|
||||||
|
for (const item of cfg.experimental.hook.session_completed) {
|
||||||
|
Bun.spawn({
|
||||||
|
cmd: item.command,
|
||||||
|
cwd: App.info().path.cwd,
|
||||||
|
env: item.environment,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { createTwoFilesPatch } from "diff"
|
||||||
import { Permission } from "../permission"
|
import { Permission } from "../permission"
|
||||||
import DESCRIPTION from "./edit.txt"
|
import DESCRIPTION from "./edit.txt"
|
||||||
import { App } from "../app/app"
|
import { App } from "../app/app"
|
||||||
|
import { Format } from "../format"
|
||||||
|
|
||||||
export const EditTool = Tool.define({
|
export const EditTool = Tool.define({
|
||||||
id: "edit",
|
id: "edit",
|
||||||
|
@ -59,6 +60,7 @@ export const EditTool = Tool.define({
|
||||||
if (params.oldString === "") {
|
if (params.oldString === "") {
|
||||||
contentNew = params.newString
|
contentNew = params.newString
|
||||||
await Bun.write(filepath, params.newString)
|
await Bun.write(filepath, params.newString)
|
||||||
|
await Format.run(filepath)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,6 +79,7 @@ export const EditTool = Tool.define({
|
||||||
params.replaceAll,
|
params.replaceAll,
|
||||||
)
|
)
|
||||||
await file.write(contentNew)
|
await file.write(contentNew)
|
||||||
|
await Format.run(filepath)
|
||||||
})()
|
})()
|
||||||
|
|
||||||
const diff = trimDiff(
|
const diff = trimDiff(
|
||||||
|
@ -473,7 +476,7 @@ export function replace(
|
||||||
if (oldString === newString) {
|
if (oldString === newString) {
|
||||||
throw new Error("oldString and newString must be different")
|
throw new Error("oldString and newString must be different")
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const replacer of [
|
for (const replacer of [
|
||||||
SimpleReplacer,
|
SimpleReplacer,
|
||||||
LineTrimmedReplacer,
|
LineTrimmedReplacer,
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { LSP } from "../lsp"
|
||||||
import { Permission } from "../permission"
|
import { Permission } from "../permission"
|
||||||
import DESCRIPTION from "./write.txt"
|
import DESCRIPTION from "./write.txt"
|
||||||
import { App } from "../app/app"
|
import { App } from "../app/app"
|
||||||
|
import { Format } from "../format"
|
||||||
|
|
||||||
export const WriteTool = Tool.define({
|
export const WriteTool = Tool.define({
|
||||||
id: "write",
|
id: "write",
|
||||||
|
@ -42,6 +43,7 @@ export const WriteTool = Tool.define({
|
||||||
})
|
})
|
||||||
|
|
||||||
await Bun.write(filepath, params.content)
|
await Bun.write(filepath, params.content)
|
||||||
|
await Format.run(filepath)
|
||||||
FileTimes.read(ctx.sessionID, filepath)
|
FileTimes.read(ctx.sessionID, filepath)
|
||||||
|
|
||||||
let output = ""
|
let output = ""
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue