feat: improve file watcher with chokidar and better ignore patterns (#2621)

Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
Dax
2025-09-16 00:17:10 -04:00
committed by GitHub
parent 52fb571739
commit 14cb2d2af6
16 changed files with 180 additions and 62 deletions

View File

@@ -143,6 +143,7 @@
"@standard-schema/spec": "1.0.0",
"@zip.js/zip.js": "2.7.62",
"ai": "catalog:",
"chokidar": "4.0.3",
"decimal.js": "10.5.0",
"diff": "8.0.2",
"gray-matter": "4.0.3",

View File

@@ -1,9 +1,3 @@
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"weather": {
"type": "local",
"command": ["opencode", "x", "@h1deya/mcp-server-weather"]
}
}
"$schema": "https://opencode.ai/config.json"
}

View File

@@ -37,6 +37,7 @@
"@standard-schema/spec": "1.0.0",
"@zip.js/zip.js": "2.7.62",
"ai": "catalog:",
"chokidar": "4.0.3",
"decimal.js": "10.5.0",
"diff": "8.0.2",
"gray-matter": "4.0.3",

View File

@@ -10,7 +10,7 @@ import { Installation } from "../../installation"
import { Config } from "../../config/config"
import { Bus } from "../../bus"
import { Log } from "../../util/log"
import { FileWatcher } from "../../file/watch"
import { FileWatcher } from "../../file/watcher"
import { Ide } from "../../ide"
import { Flag } from "../../flag/flag"
@@ -101,7 +101,6 @@ export const TuiCommand = cmd({
}
return undefined
})()
FileWatcher.init()
const providers = await Provider.list()
if (Object.keys(providers).length === 0) {
return "needs_provider"
@@ -181,6 +180,7 @@ export const TuiCommand = cmd({
.then(() => Bus.publish(Ide.Event.Installed, { ide }))
.catch(() => {})
})()
FileWatcher.init()
await proc.exited
server.stop()

View File

@@ -365,6 +365,11 @@ export namespace Config {
.record(z.string(), Command)
.optional()
.describe("Command configuration, see https://opencode.ai/docs/commands"),
watcher: z
.object({
ignore: z.array(z.string()).optional(),
})
.optional(),
plugin: z.string().array().optional(),
snapshot: z.boolean().optional(),
share: z

View File

@@ -0,0 +1,61 @@
export namespace FileIgnore {
const DEFAULT_PATTERNS = [
// Dependencies
"**/node_modules/**",
"**/bower_components/**",
"**/.pnpm-store/**",
"**/vendor/**",
// vcs
"**/.git/**",
// Build outputs
"**/dist/**",
"**/build/**",
"**/out/**",
"**/.next/**",
"**/target/**", // Rust
"**/bin/**",
"**/obj/**", // .NET
// Version control
"**/.git/**",
"**/.svn/**",
"**/.hg/**",
// IDE/Editor
"**/.vscode/**",
"**/.idea/**",
"**/*.swp",
"**/*.swo",
// OS
"**/.DS_Store",
"**/Thumbs.db",
// Logs & temp
"**/logs/**",
"**/tmp/**",
"**/temp/**",
"**/*.log",
// Coverage/test outputs
"**/coverage/**",
"**/.nyc_output/**",
]
const GLOBS = DEFAULT_PATTERNS.map((p) => new Bun.Glob(p))
export function match(
filepath: string,
opts: {
extra?: Bun.Glob[]
},
) {
const extra = opts.extra || []
for (const glob of [...GLOBS, ...extra]) {
if (glob.match(filepath)) return true
}
return false
}
}

View File

@@ -1,46 +0,0 @@
import z from "zod/v4"
import { Bus } from "../bus"
import fs from "fs"
import { Log } from "../util/log"
import { Flag } from "../flag/flag"
import { Instance } from "../project/instance"
export namespace FileWatcher {
const log = Log.create({ service: "file.watcher" })
export const Event = {
Updated: Bus.event(
"file.watcher.updated",
z.object({
file: z.string(),
event: z.union([z.literal("rename"), z.literal("change")]),
}),
),
}
const state = Instance.state(
() => {
if (Instance.project.vcs !== "git") return {}
try {
const watcher = fs.watch(Instance.directory, { recursive: true }, (event, file) => {
log.info("change", { file, event })
if (!file) return
Bus.publish(Event.Updated, {
file,
event,
})
})
return { watcher }
} catch {
return {}
}
},
async (state) => {
state.watcher?.close()
},
)
export function init() {
if (Flag.OPENCODE_DISABLE_WATCHER || true) return
state()
}
}

View File

@@ -0,0 +1,61 @@
import z from "zod/v4"
import { Bus } from "../bus"
import chokidar from "chokidar"
import { Flag } from "../flag/flag"
import { Instance } from "../project/instance"
import { Log } from "../util/log"
import { FileIgnore } from "./ignore"
import { Config } from "../config/config"
export namespace FileWatcher {
const log = Log.create({ service: "file.watcher" })
export const Event = {
Updated: Bus.event(
"file.watcher.updated",
z.object({
file: z.string(),
event: z.union([z.literal("add"), z.literal("change"), z.literal("unlink")]),
}),
),
}
const state = Instance.state(
async () => {
if (Instance.project.vcs !== "git") return {}
log.info("init")
const cfg = await Config.get()
const ignore = (cfg.watcher?.ignore ?? []).map((v) => new Bun.Glob(v))
const watcher = chokidar.watch(Instance.directory, {
ignoreInitial: true,
awaitWriteFinish: true,
ignored: (filepath) => {
return FileIgnore.match(filepath, {
extra: ignore,
})
},
})
watcher.on("change", (file) => {
Bus.publish(Event.Updated, { file, event: "change" })
})
watcher.on("add", (file) => {
Bus.publish(Event.Updated, { file, event: "add" })
})
watcher.on("unlink", (file) => {
Bus.publish(Event.Updated, { file, event: "unlink" })
})
watcher.on("ready", () => {
log.info("ready")
})
return { watcher }
},
async (state) => {
state.watcher?.close()
},
)
export function init() {
if (!Flag.OPENCODE_EXPERIMENTAL_WATCHER) return
state()
}
}

View File

@@ -1,6 +1,5 @@
export namespace Flag {
export const OPENCODE_AUTO_SHARE = truthy("OPENCODE_AUTO_SHARE")
export const OPENCODE_DISABLE_WATCHER = truthy("OPENCODE_DISABLE_WATCHER")
export const OPENCODE_CONFIG = process.env["OPENCODE_CONFIG"]
export const OPENCODE_CONFIG_CONTENT = process.env["OPENCODE_CONFIG_CONTENT"]
export const OPENCODE_DISABLE_AUTOUPDATE = truthy("OPENCODE_DISABLE_AUTOUPDATE")
@@ -10,6 +9,9 @@ export namespace Flag {
export const OPENCODE_ENABLE_EXPERIMENTAL_MODELS = truthy("OPENCODE_ENABLE_EXPERIMENTAL_MODELS")
export const OPENCODE_DISABLE_AUTOCOMPACT = truthy("OPENCODE_DISABLE_AUTOCOMPACT")
// Experimental
export const OPENCODE_EXPERIMENTAL_WATCHER = truthy("OPENCODE_EXPERIMENTAL_WATCHER")
function truthy(key: string) {
const value = process.env[key]?.toLowerCase()
return value === "true" || value === "1"

View File

@@ -1,3 +1,3 @@
{
".": "0.13.0"
".": "0.14.0"
}

View File

@@ -1,4 +1,4 @@
configured_endpoints: 43
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-2e754dafcad0636137256cef499b2bcd72cf17de08f44ec03c3589b2a05341a2.yml
openapi_spec_hash: 2d3cf84d3033068ce6c07386411527ef
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-0a4165f1eabf826d3092ea6b789aa527048278dcd4bd891f9e5ac890b9bcbb35.yml
openapi_spec_hash: da60e4fc813eb0f9ac3ab5f112e26bf6
config_hash: 026ef000d34bf2f930e7b41e77d2d3ff

View File

@@ -1,5 +1,13 @@
# Changelog
## 0.14.0 (2025-09-14)
Full Changelog: [v0.13.0...v0.14.0](https://github.com/sst/opencode-sdk-go/compare/v0.13.0...v0.14.0)
### Features
- **api:** api update ([dad0bc3](https://github.com/sst/opencode-sdk-go/commit/dad0bc3da99f20a0d002a6b94e049fb70f8e6a77))
## 0.13.0 (2025-09-14)
Full Changelog: [v0.12.0...v0.13.0](https://github.com/sst/opencode-sdk-go/compare/v0.12.0...v0.13.0)

View File

@@ -24,7 +24,7 @@ Or to pin the version:
<!-- x-release-please-start-version -->
```sh
go get -u 'github.com/sst/opencode-sdk-go@v0.13.0'
go get -u 'github.com/sst/opencode-sdk-go@v0.14.0'
```
<!-- x-release-please-end -->

View File

@@ -2,4 +2,4 @@
package internal
const PackageVersion = "0.13.0" // x-release-please-version
const PackageVersion = "0.14.0" // x-release-please-version

View File

@@ -8,12 +8,15 @@ import (
"fmt"
"net/http"
"net/url"
"reflect"
"github.com/sst/opencode-sdk-go/internal/apijson"
"github.com/sst/opencode-sdk-go/internal/apiquery"
"github.com/sst/opencode-sdk-go/internal/param"
"github.com/sst/opencode-sdk-go/internal/requestconfig"
"github.com/sst/opencode-sdk-go/option"
"github.com/sst/opencode-sdk-go/shared"
"github.com/tidwall/gjson"
)
// SessionPermissionService contains methods and other services that help with
@@ -60,7 +63,7 @@ type Permission struct {
Title string `json:"title,required"`
Type string `json:"type,required"`
CallID string `json:"callID"`
Pattern string `json:"pattern"`
Pattern PermissionPatternUnion `json:"pattern"`
JSON permissionJSON `json:"-"`
}
@@ -107,6 +110,30 @@ func (r permissionTimeJSON) RawJSON() string {
return r.raw
}
// Union satisfied by [shared.UnionString] or [PermissionPatternArray].
type PermissionPatternUnion interface {
ImplementsPermissionPatternUnion()
}
func init() {
apijson.RegisterUnion(
reflect.TypeOf((*PermissionPatternUnion)(nil)).Elem(),
"",
apijson.UnionVariant{
TypeFilter: gjson.String,
Type: reflect.TypeOf(shared.UnionString("")),
},
apijson.UnionVariant{
TypeFilter: gjson.JSON,
Type: reflect.TypeOf(PermissionPatternArray{}),
},
)
}
type PermissionPatternArray []string
func (r PermissionPatternArray) ImplementsPermissionPatternUnion() {}
type SessionPermissionRespondParams struct {
Response param.Field[SessionPermissionRespondParamsResponse] `json:"response,required"`
Directory param.Field[string] `query:"directory"`

View File

@@ -2,6 +2,10 @@
package shared
type UnionString string
func (UnionString) ImplementsPermissionPatternUnion() {}
type UnionBool bool
func (UnionBool) ImplementsConfigProviderOptionsTimeoutUnion() {}