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

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