From 26dcb85de1ad44c23700da0e59a89e9e2f627c28 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 1 Jul 2025 13:44:52 -0400 Subject: [PATCH] add file watcher --- packages/opencode/src/app/app.ts | 3 ++ packages/opencode/src/cli/bootstrap.ts | 2 + packages/opencode/src/cli/cmd/debug/index.ts | 11 +++++ packages/opencode/src/cli/cmd/serve.ts | 4 +- packages/opencode/src/file/watch.ts | 50 ++++++++++++++++++++ packages/opencode/src/session/message.ts | 5 ++ 6 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 packages/opencode/src/file/watch.ts diff --git a/packages/opencode/src/app/app.ts b/packages/opencode/src/app/app.ts index 95b17abb..6467d252 100644 --- a/packages/opencode/src/app/app.ts +++ b/packages/opencode/src/app/app.ts @@ -36,12 +36,15 @@ export namespace App { services: Map Promise }> }>("app") + export const use = ctx.use + const APP_JSON = "app.json" export type Input = { cwd: string } + export const provideExisting = ctx.provide export async function provide( input: Input, cb: (app: App.Info) => Promise, diff --git a/packages/opencode/src/cli/bootstrap.ts b/packages/opencode/src/cli/bootstrap.ts index e2b96f23..9ae274ed 100644 --- a/packages/opencode/src/cli/bootstrap.ts +++ b/packages/opencode/src/cli/bootstrap.ts @@ -1,5 +1,6 @@ import { App } from "../app/app" import { ConfigHooks } from "../config/hooks" +import { FileWatcher } from "../file/watch" import { Format } from "../format" import { LSP } from "../lsp" import { Share } from "../share/share" @@ -13,6 +14,7 @@ export async function bootstrap( Format.init() ConfigHooks.init() LSP.init() + FileWatcher.init() return cb(app) }) diff --git a/packages/opencode/src/cli/cmd/debug/index.ts b/packages/opencode/src/cli/cmd/debug/index.ts index 5a765997..e748183e 100644 --- a/packages/opencode/src/cli/cmd/debug/index.ts +++ b/packages/opencode/src/cli/cmd/debug/index.ts @@ -1,3 +1,4 @@ +import { bootstrap } from "../../bootstrap" import { cmd } from "../cmd" import { FileCommand } from "./file" import { LSPCommand } from "./lsp" @@ -12,6 +13,16 @@ export const DebugCommand = cmd({ .command(RipgrepCommand) .command(FileCommand) .command(SnapshotCommand) + .command({ + command: "wait", + async handler() { + await bootstrap({ cwd: process.cwd() }, async () => { + await new Promise((resolve) => + setTimeout(resolve, 1_000 * 60 * 60 * 24), + ) + }) + }, + }) .demandCommand(), async handler() {}, }) diff --git a/packages/opencode/src/cli/cmd/serve.ts b/packages/opencode/src/cli/cmd/serve.ts index ef09e794..f3686f30 100644 --- a/packages/opencode/src/cli/cmd/serve.ts +++ b/packages/opencode/src/cli/cmd/serve.ts @@ -1,7 +1,7 @@ -import { App } from "../../app/app" import { Provider } from "../../provider/provider" import { Server } from "../../server/server" import { Share } from "../../share/share" +import { bootstrap } from "../bootstrap" import { cmd } from "./cmd" export const ServeCommand = cmd({ @@ -23,7 +23,7 @@ export const ServeCommand = cmd({ describe: "starts a headless opencode server", handler: async (args) => { const cwd = process.cwd() - await App.provide({ cwd }, async () => { + await bootstrap({ cwd }, async () => { const providers = await Provider.list() if (Object.keys(providers).length === 0) { return "needs_provider" diff --git a/packages/opencode/src/file/watch.ts b/packages/opencode/src/file/watch.ts new file mode 100644 index 00000000..2a702984 --- /dev/null +++ b/packages/opencode/src/file/watch.ts @@ -0,0 +1,50 @@ +import { z } from "zod" +import { Bus } from "../bus" +import fs from "fs" +import { App } from "../app/app" +import { Log } from "../util/log" + +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")]), + }), + ), + } + + export function init() { + App.state( + "file.watcher", + () => { + const app = App.use() + const watcher = fs.watch( + app.info.path.cwd, + { recursive: true }, + (event, file) => { + log.info("change", { file, event }) + if (!file) return + // for some reason async local storage is lost here + // https://github.com/oven-sh/bun/issues/20754 + App.provideExisting(app, async () => { + Bus.publish(Event.Updated, { + file, + event, + }) + }) + }, + ) + return { + watcher, + } + }, + async (state) => { + state.watcher.close() + }, + )() + } +} diff --git a/packages/opencode/src/session/message.ts b/packages/opencode/src/session/message.ts index 6d16497e..b2171fa4 100644 --- a/packages/opencode/src/session/message.ts +++ b/packages/opencode/src/session/message.ts @@ -188,6 +188,11 @@ export namespace Message { }), }) .optional(), + user: z + .object({ + snapshot: z.string().optional(), + }) + .optional(), }) .openapi({ ref: "MessageMetadata" }), })