Просмотр исходного кода

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

Co-authored-by: GitHub Action <[email protected]>
Dax 5 месяцев назад
Родитель
Сommit
14cb2d2af6

+ 1 - 0
bun.lock

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

+ 1 - 7
opencode.json

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

+ 1 - 0
packages/opencode/package.json

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

+ 2 - 2
packages/opencode/src/cli/cmd/tui.ts

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

+ 5 - 0
packages/opencode/src/config/config.ts

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

+ 61 - 0
packages/opencode/src/file/ignore.ts

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

+ 0 - 46
packages/opencode/src/file/watch.ts

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

+ 61 - 0
packages/opencode/src/file/watcher.ts

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

+ 3 - 1
packages/opencode/src/flag/flag.ts

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

+ 1 - 1
packages/sdk/go/.release-please-manifest.json

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

+ 2 - 2
packages/sdk/go/.stats.yml

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

+ 8 - 0
packages/sdk/go/CHANGELOG.md

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

+ 1 - 1
packages/sdk/go/README.md

@@ -24,7 +24,7 @@ Or to pin the version:
 <!-- x-release-please-start-version -->
 
 ```sh
-go get -u 'github.com/sst/[email protected]3.0'
+go get -u 'github.com/sst/[email protected]4.0'
 ```
 
 <!-- x-release-please-end -->

+ 1 - 1
packages/sdk/go/internal/version.go

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

+ 28 - 1
packages/sdk/go/sessionpermission.go

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

+ 4 - 0
packages/sdk/go/shared/union.go

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