Compare commits

...

1 Commits

Author SHA1 Message Date
Kit Langton e61fe8e5f8 feat: unwrap file namespaces to flat exports + barrel 2026-04-15 23:49:51 -04:00
35 changed files with 893 additions and 898 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
import { EOL } from "os" import { EOL } from "os"
import { AppRuntime } from "@/effect/app-runtime" import { AppRuntime } from "@/effect/app-runtime"
import { File } from "../../../file" import { File } from "../../../file"
import { Ripgrep } from "@/file/ripgrep" import { Ripgrep } from "@/file"
import { bootstrap } from "../../bootstrap" import { bootstrap } from "../../bootstrap"
import { cmd } from "../cmd" import { cmd } from "../cmd"
@@ -1,7 +1,7 @@
import { EOL } from "os" import { EOL } from "os"
import { Effect, Stream } from "effect" import { Effect, Stream } from "effect"
import { AppRuntime } from "../../../effect/app-runtime" import { AppRuntime } from "../../../effect/app-runtime"
import { Ripgrep } from "../../../file/ripgrep" import { Ripgrep } from "../../../file"
import { Instance } from "../../../project/instance" import { Instance } from "../../../project/instance"
import { bootstrap } from "../../bootstrap" import { bootstrap } from "../../bootstrap"
import { cmd } from "../cmd" import { cmd } from "../cmd"
+3 -3
View File
@@ -8,10 +8,10 @@ import { Auth } from "@/auth"
import { Account } from "@/account" import { Account } from "@/account"
import { Config } from "@/config" import { Config } from "@/config"
import { Git } from "@/git" import { Git } from "@/git"
import { Ripgrep } from "@/file/ripgrep" import { Ripgrep } from "@/file"
import { FileTime } from "@/file/time" import { FileTime } from "@/file"
import { File } from "@/file" import { File } from "@/file"
import { FileWatcher } from "@/file/watcher" import { FileWatcher } from "@/file"
import { Storage } from "@/storage" import { Storage } from "@/storage"
import { Snapshot } from "@/snapshot" import { Snapshot } from "@/snapshot"
import { Plugin } from "@/plugin" import { Plugin } from "@/plugin"
@@ -3,7 +3,7 @@ import { memoMap } from "./run-service"
import { Plugin } from "@/plugin" import { Plugin } from "@/plugin"
import { LSP } from "@/lsp" import { LSP } from "@/lsp"
import { FileWatcher } from "@/file/watcher" import { FileWatcher } from "@/file"
import { Format } from "@/format" import { Format } from "@/format"
import { ShareNext } from "@/share" import { ShareNext } from "@/share"
import { File } from "@/file" import { File } from "@/file"
+2 -2
View File
@@ -13,8 +13,8 @@ import z from "zod"
import { Global } from "../global" import { Global } from "../global"
import { Instance } from "../project/instance" import { Instance } from "../project/instance"
import { Log } from "../util" import { Log } from "../util"
import { Protected } from "./protected" import * as Protected from "./protected"
import { Ripgrep } from "./ripgrep" import * as Ripgrep from "./ripgrep"
export const Info = z export const Info = z
.object({ .object({
+68 -70
View File
@@ -1,81 +1,79 @@
import { Glob } from "@opencode-ai/shared/util/glob" import { Glob } from "@opencode-ai/shared/util/glob"
export namespace FileIgnore { const FOLDERS = new Set([
const FOLDERS = new Set([ "node_modules",
"node_modules", "bower_components",
"bower_components", ".pnpm-store",
".pnpm-store", "vendor",
"vendor", ".npm",
".npm", "dist",
"dist", "build",
"build", "out",
"out", ".next",
".next", "target",
"target", "bin",
"bin", "obj",
"obj", ".git",
".git", ".svn",
".svn", ".hg",
".hg", ".vscode",
".vscode", ".idea",
".idea", ".turbo",
".turbo", ".output",
".output", "desktop",
"desktop", ".sst",
".sst", ".cache",
".cache", ".webkit-cache",
".webkit-cache", "__pycache__",
"__pycache__", ".pytest_cache",
".pytest_cache", "mypy_cache",
"mypy_cache", ".history",
".history", ".gradle",
".gradle", ])
])
const FILES = [ const FILES = [
"**/*.swp", "**/*.swp",
"**/*.swo", "**/*.swo",
"**/*.pyc", "**/*.pyc",
// OS // OS
"**/.DS_Store", "**/.DS_Store",
"**/Thumbs.db", "**/Thumbs.db",
// Logs & temp // Logs & temp
"**/logs/**", "**/logs/**",
"**/tmp/**", "**/tmp/**",
"**/temp/**", "**/temp/**",
"**/*.log", "**/*.log",
// Coverage/test outputs // Coverage/test outputs
"**/coverage/**", "**/coverage/**",
"**/.nyc_output/**", "**/.nyc_output/**",
] ]
export const PATTERNS = [...FILES, ...FOLDERS] export const PATTERNS = [...FILES, ...FOLDERS]
export function match( export function match(
filepath: string, filepath: string,
opts?: { opts?: {
extra?: string[] extra?: string[]
whitelist?: string[] whitelist?: string[]
}, },
) { ) {
for (const pattern of opts?.whitelist || []) { for (const pattern of opts?.whitelist || []) {
if (Glob.match(pattern, filepath)) return false if (Glob.match(pattern, filepath)) return false
}
const parts = filepath.split(/[/\\]/)
for (let i = 0; i < parts.length; i++) {
if (FOLDERS.has(parts[i])) return true
}
const extra = opts?.extra || []
for (const pattern of [...FILES, ...extra]) {
if (Glob.match(pattern, filepath)) return true
}
return false
} }
const parts = filepath.split(/[/\\]/)
for (let i = 0; i < parts.length; i++) {
if (FOLDERS.has(parts[i])) return true
}
const extra = opts?.extra || []
for (const pattern of [...FILES, ...extra]) {
if (Glob.match(pattern, filepath)) return true
}
return false
} }
+5
View File
@@ -1 +1,6 @@
export * as File from "./file" export * as File from "./file"
export * as Protected from "./protected"
export * as FileIgnore from "./ignore"
export * as FileWatcher from "./watcher"
export * as FileTime from "./time"
export * as Ripgrep from "./ripgrep"
+17 -19
View File
@@ -37,23 +37,21 @@ const DARWIN_ROOT = ["/.DocumentRevisions-V100", "/.Spotlight-V100", "/.Trashes"
const WIN32_HOME = ["AppData", "Downloads", "Desktop", "Documents", "Pictures", "Music", "Videos", "OneDrive"] const WIN32_HOME = ["AppData", "Downloads", "Desktop", "Documents", "Pictures", "Music", "Videos", "OneDrive"]
export namespace Protected { /** Directory basenames to skip when scanning the home directory. */
/** Directory basenames to skip when scanning the home directory. */ export function names(): ReadonlySet<string> {
export function names(): ReadonlySet<string> { if (process.platform === "darwin") return new Set(DARWIN_HOME)
if (process.platform === "darwin") return new Set(DARWIN_HOME) if (process.platform === "win32") return new Set(WIN32_HOME)
if (process.platform === "win32") return new Set(WIN32_HOME) return new Set()
return new Set() }
}
/** Absolute paths that should never be watched, stated, or scanned. */
/** Absolute paths that should never be watched, stated, or scanned. */ export function paths(): string[] {
export function paths(): string[] { if (process.platform === "darwin")
if (process.platform === "darwin") return [
return [ ...DARWIN_HOME.map((n) => path.join(home, n)),
...DARWIN_HOME.map((n) => path.join(home, n)), ...DARWIN_LIBRARY.map((n) => path.join(home, "Library", n)),
...DARWIN_LIBRARY.map((n) => path.join(home, "Library", n)), ...DARWIN_ROOT,
...DARWIN_ROOT, ]
] if (process.platform === "win32") return WIN32_HOME.map((n) => path.join(home, n))
if (process.platform === "win32") return WIN32_HOME.map((n) => path.join(home, n)) return []
return []
}
} }
File diff suppressed because it is too large Load Diff
+102 -104
View File
@@ -5,109 +5,107 @@ import { Flag } from "@/flag/flag"
import type { SessionID } from "@/session/schema" import type { SessionID } from "@/session/schema"
import { Log } from "../util" import { Log } from "../util"
export namespace FileTime { const log = Log.create({ service: "file.time" })
const log = Log.create({ service: "file.time" })
export type Stamp = { export type Stamp = {
readonly read: Date readonly read: Date
readonly mtime: number | undefined readonly mtime: number | undefined
readonly size: number | undefined readonly size: number | undefined
}
const session = (reads: Map<SessionID, Map<string, Stamp>>, sessionID: SessionID) => {
const value = reads.get(sessionID)
if (value) return value
const next = new Map<string, Stamp>()
reads.set(sessionID, next)
return next
}
interface State {
reads: Map<SessionID, Map<string, Stamp>>
locks: Map<string, Semaphore.Semaphore>
}
export interface Interface {
readonly read: (sessionID: SessionID, file: string) => Effect.Effect<void>
readonly get: (sessionID: SessionID, file: string) => Effect.Effect<Date | undefined>
readonly assert: (sessionID: SessionID, filepath: string) => Effect.Effect<void>
readonly withLock: <T>(filepath: string, fn: () => Effect.Effect<T>) => Effect.Effect<T>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/FileTime") {}
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const fsys = yield* AppFileSystem.Service
const disableCheck = yield* Flag.OPENCODE_DISABLE_FILETIME_CHECK
const stamp = Effect.fnUntraced(function* (file: string) {
const info = yield* fsys.stat(file).pipe(Effect.catch(() => Effect.void))
return {
read: yield* DateTime.nowAsDate,
mtime: info ? Option.getOrUndefined(info.mtime)?.getTime() : undefined,
size: info ? Number(info.size) : undefined,
}
})
const state = yield* InstanceState.make<State>(
Effect.fn("FileTime.state")(() =>
Effect.succeed({
reads: new Map<SessionID, Map<string, Stamp>>(),
locks: new Map<string, Semaphore.Semaphore>(),
}),
),
)
const getLock = Effect.fn("FileTime.lock")(function* (filepath: string) {
filepath = AppFileSystem.normalizePath(filepath)
const locks = (yield* InstanceState.get(state)).locks
const lock = locks.get(filepath)
if (lock) return lock
const next = Semaphore.makeUnsafe(1)
locks.set(filepath, next)
return next
})
const read = Effect.fn("FileTime.read")(function* (sessionID: SessionID, file: string) {
file = AppFileSystem.normalizePath(file)
const reads = (yield* InstanceState.get(state)).reads
log.info("read", { sessionID, file })
session(reads, sessionID).set(file, yield* stamp(file))
})
const get = Effect.fn("FileTime.get")(function* (sessionID: SessionID, file: string) {
file = AppFileSystem.normalizePath(file)
const reads = (yield* InstanceState.get(state)).reads
return reads.get(sessionID)?.get(file)?.read
})
const assert = Effect.fn("FileTime.assert")(function* (sessionID: SessionID, filepath: string) {
if (disableCheck) return
filepath = AppFileSystem.normalizePath(filepath)
const reads = (yield* InstanceState.get(state)).reads
const time = reads.get(sessionID)?.get(filepath)
if (!time) throw new Error(`You must read file ${filepath} before overwriting it. Use the Read tool first`)
const next = yield* stamp(filepath)
const changed = next.mtime !== time.mtime || next.size !== time.size
if (!changed) return
throw new Error(
`File ${filepath} has been modified since it was last read.\nLast modification: ${new Date(next.mtime ?? next.read.getTime()).toISOString()}\nLast read: ${time.read.toISOString()}\n\nPlease read the file again before modifying it.`,
)
})
const withLock = Effect.fn("FileTime.withLock")(function* <T>(filepath: string, fn: () => Effect.Effect<T>) {
return yield* fn().pipe((yield* getLock(filepath)).withPermits(1))
})
return Service.of({ read, get, assert, withLock })
}),
).pipe(Layer.orDie)
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
} }
const session = (reads: Map<SessionID, Map<string, Stamp>>, sessionID: SessionID) => {
const value = reads.get(sessionID)
if (value) return value
const next = new Map<string, Stamp>()
reads.set(sessionID, next)
return next
}
interface State {
reads: Map<SessionID, Map<string, Stamp>>
locks: Map<string, Semaphore.Semaphore>
}
export interface Interface {
readonly read: (sessionID: SessionID, file: string) => Effect.Effect<void>
readonly get: (sessionID: SessionID, file: string) => Effect.Effect<Date | undefined>
readonly assert: (sessionID: SessionID, filepath: string) => Effect.Effect<void>
readonly withLock: <T>(filepath: string, fn: () => Effect.Effect<T>) => Effect.Effect<T>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/FileTime") {}
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const fsys = yield* AppFileSystem.Service
const disableCheck = yield* Flag.OPENCODE_DISABLE_FILETIME_CHECK
const stamp = Effect.fnUntraced(function* (file: string) {
const info = yield* fsys.stat(file).pipe(Effect.catch(() => Effect.void))
return {
read: yield* DateTime.nowAsDate,
mtime: info ? Option.getOrUndefined(info.mtime)?.getTime() : undefined,
size: info ? Number(info.size) : undefined,
}
})
const state = yield* InstanceState.make<State>(
Effect.fn("FileTime.state")(() =>
Effect.succeed({
reads: new Map<SessionID, Map<string, Stamp>>(),
locks: new Map<string, Semaphore.Semaphore>(),
}),
),
)
const getLock = Effect.fn("FileTime.lock")(function* (filepath: string) {
filepath = AppFileSystem.normalizePath(filepath)
const locks = (yield* InstanceState.get(state)).locks
const lock = locks.get(filepath)
if (lock) return lock
const next = Semaphore.makeUnsafe(1)
locks.set(filepath, next)
return next
})
const read = Effect.fn("FileTime.read")(function* (sessionID: SessionID, file: string) {
file = AppFileSystem.normalizePath(file)
const reads = (yield* InstanceState.get(state)).reads
log.info("read", { sessionID, file })
session(reads, sessionID).set(file, yield* stamp(file))
})
const get = Effect.fn("FileTime.get")(function* (sessionID: SessionID, file: string) {
file = AppFileSystem.normalizePath(file)
const reads = (yield* InstanceState.get(state)).reads
return reads.get(sessionID)?.get(file)?.read
})
const assert = Effect.fn("FileTime.assert")(function* (sessionID: SessionID, filepath: string) {
if (disableCheck) return
filepath = AppFileSystem.normalizePath(filepath)
const reads = (yield* InstanceState.get(state)).reads
const time = reads.get(sessionID)?.get(filepath)
if (!time) throw new Error(`You must read file ${filepath} before overwriting it. Use the Read tool first`)
const next = yield* stamp(filepath)
const changed = next.mtime !== time.mtime || next.size !== time.size
if (!changed) return
throw new Error(
`File ${filepath} has been modified since it was last read.\nLast modification: ${new Date(next.mtime ?? next.read.getTime()).toISOString()}\nLast read: ${time.read.toISOString()}\n\nPlease read the file again before modifying it.`,
)
})
const withLock = Effect.fn("FileTime.withLock")(function* <T>(filepath: string, fn: () => Effect.Effect<T>) {
return yield* fn().pipe((yield* getLock(filepath)).withPermits(1))
})
return Service.of({ read, get, assert, withLock })
}),
).pipe(Layer.orDie)
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
+139 -141
View File
@@ -13,151 +13,149 @@ import { Git } from "@/git"
import { Instance } from "@/project/instance" import { Instance } from "@/project/instance"
import { lazy } from "@/util/lazy" import { lazy } from "@/util/lazy"
import { Config } from "../config" import { Config } from "../config"
import { FileIgnore } from "./ignore" import * as FileIgnore from "./ignore"
import { Protected } from "./protected" import * as Protected from "./protected"
import { Log } from "../util" import { Log } from "../util"
declare const OPENCODE_LIBC: string | undefined declare const OPENCODE_LIBC: string | undefined
export namespace FileWatcher { const log = Log.create({ service: "file.watcher" })
const log = Log.create({ service: "file.watcher" }) const SUBSCRIBE_TIMEOUT_MS = 10_000
const SUBSCRIBE_TIMEOUT_MS = 10_000
export const Event = { export const Event = {
Updated: BusEvent.define( Updated: BusEvent.define(
"file.watcher.updated", "file.watcher.updated",
z.object({ z.object({
file: z.string(), file: z.string(),
event: z.union([z.literal("add"), z.literal("change"), z.literal("unlink")]), event: z.union([z.literal("add"), z.literal("change"), z.literal("unlink")]),
}),
),
}
const watcher = lazy((): typeof import("@parcel/watcher") | undefined => {
try {
const binding = require(
`@parcel/watcher-${process.platform}-${process.arch}${process.platform === "linux" ? `-${OPENCODE_LIBC || "glibc"}` : ""}`,
)
return createWrapper(binding) as typeof import("@parcel/watcher")
} catch (error) {
log.error("failed to load watcher binding", { error })
return
}
})
function getBackend() {
if (process.platform === "win32") return "windows"
if (process.platform === "darwin") return "fs-events"
if (process.platform === "linux") return "inotify"
}
function protecteds(dir: string) {
return Protected.paths().filter((item) => {
const rel = path.relative(dir, item)
return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel)
})
}
export const hasNativeBinding = () => !!watcher()
export interface Interface {
readonly init: () => Effect.Effect<void>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/FileWatcher") {}
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const config = yield* Config.Service
const git = yield* Git.Service
const state = yield* InstanceState.make(
Effect.fn("FileWatcher.state")(
function* () {
if (yield* Flag.OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER) return
log.info("init", { directory: Instance.directory })
const backend = getBackend()
if (!backend) {
log.error("watcher backend not supported", { directory: Instance.directory, platform: process.platform })
return
}
const w = watcher()
if (!w) return
log.info("watcher backend", { directory: Instance.directory, platform: process.platform, backend })
const subs: ParcelWatcher.AsyncSubscription[] = []
yield* Effect.addFinalizer(() =>
Effect.promise(() => Promise.allSettled(subs.map((sub) => sub.unsubscribe()))),
)
const cb: ParcelWatcher.SubscribeCallback = Instance.bind((err, evts) => {
if (err) return
for (const evt of evts) {
if (evt.type === "create") void Bus.publish(Event.Updated, { file: evt.path, event: "add" })
if (evt.type === "update") void Bus.publish(Event.Updated, { file: evt.path, event: "change" })
if (evt.type === "delete") void Bus.publish(Event.Updated, { file: evt.path, event: "unlink" })
}
})
const subscribe = (dir: string, ignore: string[]) => {
const pending = w.subscribe(dir, cb, { ignore, backend })
return Effect.gen(function* () {
const sub = yield* Effect.promise(() => pending)
subs.push(sub)
}).pipe(
Effect.timeout(SUBSCRIBE_TIMEOUT_MS),
Effect.catchCause((cause) => {
log.error("failed to subscribe", { dir, cause: Cause.pretty(cause) })
pending.then((s) => s.unsubscribe()).catch(() => {})
return Effect.void
}),
)
}
const cfg = yield* config.get()
const cfgIgnores = cfg.watcher?.ignore ?? []
if (yield* Flag.OPENCODE_EXPERIMENTAL_FILEWATCHER) {
yield* subscribe(Instance.directory, [
...FileIgnore.PATTERNS,
...cfgIgnores,
...protecteds(Instance.directory),
])
}
if (Instance.project.vcs === "git") {
const result = yield* git.run(["rev-parse", "--git-dir"], {
cwd: Instance.project.worktree,
})
const vcsDir =
result.exitCode === 0 ? path.resolve(Instance.project.worktree, result.text().trim()) : undefined
if (vcsDir && !cfgIgnores.includes(".git") && !cfgIgnores.includes(vcsDir)) {
const ignore = (yield* Effect.promise(() => readdir(vcsDir).catch(() => []))).filter(
(entry) => entry !== "HEAD",
)
yield* subscribe(vcsDir, ignore)
}
}
},
Effect.catchCause((cause) => {
log.error("failed to init watcher service", { cause: Cause.pretty(cause) })
return Effect.void
}),
),
)
return Service.of({
init: Effect.fn("FileWatcher.init")(function* () {
yield* InstanceState.get(state)
}),
})
}), }),
) ),
export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer), Layer.provide(Git.defaultLayer))
} }
const watcher = lazy((): typeof import("@parcel/watcher") | undefined => {
try {
const binding = require(
`@parcel/watcher-${process.platform}-${process.arch}${process.platform === "linux" ? `-${OPENCODE_LIBC || "glibc"}` : ""}`,
)
return createWrapper(binding) as typeof import("@parcel/watcher")
} catch (error) {
log.error("failed to load watcher binding", { error })
return
}
})
function getBackend() {
if (process.platform === "win32") return "windows"
if (process.platform === "darwin") return "fs-events"
if (process.platform === "linux") return "inotify"
}
function protecteds(dir: string) {
return Protected.paths().filter((item) => {
const rel = path.relative(dir, item)
return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel)
})
}
export const hasNativeBinding = () => !!watcher()
export interface Interface {
readonly init: () => Effect.Effect<void>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/FileWatcher") {}
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const config = yield* Config.Service
const git = yield* Git.Service
const state = yield* InstanceState.make(
Effect.fn("FileWatcher.state")(
function* () {
if (yield* Flag.OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER) return
log.info("init", { directory: Instance.directory })
const backend = getBackend()
if (!backend) {
log.error("watcher backend not supported", { directory: Instance.directory, platform: process.platform })
return
}
const w = watcher()
if (!w) return
log.info("watcher backend", { directory: Instance.directory, platform: process.platform, backend })
const subs: ParcelWatcher.AsyncSubscription[] = []
yield* Effect.addFinalizer(() =>
Effect.promise(() => Promise.allSettled(subs.map((sub) => sub.unsubscribe()))),
)
const cb: ParcelWatcher.SubscribeCallback = Instance.bind((err, evts) => {
if (err) return
for (const evt of evts) {
if (evt.type === "create") void Bus.publish(Event.Updated, { file: evt.path, event: "add" })
if (evt.type === "update") void Bus.publish(Event.Updated, { file: evt.path, event: "change" })
if (evt.type === "delete") void Bus.publish(Event.Updated, { file: evt.path, event: "unlink" })
}
})
const subscribe = (dir: string, ignore: string[]) => {
const pending = w.subscribe(dir, cb, { ignore, backend })
return Effect.gen(function* () {
const sub = yield* Effect.promise(() => pending)
subs.push(sub)
}).pipe(
Effect.timeout(SUBSCRIBE_TIMEOUT_MS),
Effect.catchCause((cause) => {
log.error("failed to subscribe", { dir, cause: Cause.pretty(cause) })
pending.then((s) => s.unsubscribe()).catch(() => {})
return Effect.void
}),
)
}
const cfg = yield* config.get()
const cfgIgnores = cfg.watcher?.ignore ?? []
if (yield* Flag.OPENCODE_EXPERIMENTAL_FILEWATCHER) {
yield* subscribe(Instance.directory, [
...FileIgnore.PATTERNS,
...cfgIgnores,
...protecteds(Instance.directory),
])
}
if (Instance.project.vcs === "git") {
const result = yield* git.run(["rev-parse", "--git-dir"], {
cwd: Instance.project.worktree,
})
const vcsDir =
result.exitCode === 0 ? path.resolve(Instance.project.worktree, result.text().trim()) : undefined
if (vcsDir && !cfgIgnores.includes(".git") && !cfgIgnores.includes(vcsDir)) {
const ignore = (yield* Effect.promise(() => readdir(vcsDir).catch(() => []))).filter(
(entry) => entry !== "HEAD",
)
yield* subscribe(vcsDir, ignore)
}
}
},
Effect.catchCause((cause) => {
log.error("failed to init watcher service", { cause: Cause.pretty(cause) })
return Effect.void
}),
),
)
return Service.of({
init: Effect.fn("FileWatcher.init")(function* () {
yield* InstanceState.get(state)
}),
})
}),
)
export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer), Layer.provide(Git.defaultLayer))
+1 -1
View File
@@ -9,7 +9,7 @@ import { Bus } from "../bus"
import { Command } from "../command" import { Command } from "../command"
import { Instance } from "./instance" import { Instance } from "./instance"
import { Log } from "@/util" import { Log } from "@/util"
import { FileWatcher } from "@/file/watcher" import { FileWatcher } from "@/file"
import { ShareNext } from "@/share" import { ShareNext } from "@/share"
import * as Effect from "effect/Effect" import * as Effect from "effect/Effect"
+1 -1
View File
@@ -5,7 +5,7 @@ import { Bus } from "@/bus"
import { BusEvent } from "@/bus/bus-event" import { BusEvent } from "@/bus/bus-event"
import { InstanceState } from "@/effect" import { InstanceState } from "@/effect"
import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { FileWatcher } from "@/file/watcher" import { FileWatcher } from "@/file"
import { Git } from "@/git" import { Git } from "@/git"
import { Log } from "@/util" import { Log } from "@/util"
import { Instance } from "./instance" import { Instance } from "./instance"
@@ -4,7 +4,7 @@ import { Effect } from "effect"
import z from "zod" import z from "zod"
import { AppRuntime } from "../../effect/app-runtime" import { AppRuntime } from "../../effect/app-runtime"
import { File } from "../../file" import { File } from "../../file"
import { Ripgrep } from "../../file/ripgrep" import { Ripgrep } from "../../file"
import { LSP } from "../../lsp" import { LSP } from "../../lsp"
import { Instance } from "../../project/instance" import { Instance } from "../../project/instance"
import { lazy } from "../../util/lazy" import { lazy } from "../../util/lazy"
+1 -1
View File
@@ -22,7 +22,7 @@ import MAX_STEPS from "../session/prompt/max-steps.txt"
import { ToolRegistry } from "../tool/registry" import { ToolRegistry } from "../tool/registry"
import { MCP } from "../mcp" import { MCP } from "../mcp"
import { LSP } from "../lsp" import { LSP } from "../lsp"
import { FileTime } from "../file/time" import { FileTime } from "../file"
import { Flag } from "../flag/flag" import { Flag } from "../flag/flag"
import { ulid } from "ulid" import { ulid } from "ulid"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
+1 -1
View File
@@ -3,7 +3,7 @@ import * as path from "path"
import { Effect } from "effect" import { Effect } from "effect"
import { Tool } from "./tool" import { Tool } from "./tool"
import { Bus } from "../bus" import { Bus } from "../bus"
import { FileWatcher } from "../file/watcher" import { FileWatcher } from "../file"
import { Instance } from "../project/instance" import { Instance } from "../project/instance"
import { Patch } from "../patch" import { Patch } from "../patch"
import { createTwoFilesPatch, diffLines } from "diff" import { createTwoFilesPatch, diffLines } from "diff"
+2 -2
View File
@@ -11,10 +11,10 @@ import { LSP } from "../lsp"
import { createTwoFilesPatch, diffLines } from "diff" import { createTwoFilesPatch, diffLines } from "diff"
import DESCRIPTION from "./edit.txt" import DESCRIPTION from "./edit.txt"
import { File } from "../file" import { File } from "../file"
import { FileWatcher } from "../file/watcher" import { FileWatcher } from "../file"
import { Bus } from "../bus" import { Bus } from "../bus"
import { Format } from "../format" import { Format } from "../format"
import { FileTime } from "../file/time" import { FileTime } from "../file"
import { Instance } from "../project/instance" import { Instance } from "../project/instance"
import { Snapshot } from "@/snapshot" import { Snapshot } from "@/snapshot"
import { assertExternalDirectoryEffect } from "./external-directory" import { assertExternalDirectoryEffect } from "./external-directory"
+1 -1
View File
@@ -4,7 +4,7 @@ import { Effect, Option } from "effect"
import * as Stream from "effect/Stream" import * as Stream from "effect/Stream"
import { InstanceState } from "@/effect" import { InstanceState } from "@/effect"
import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { Ripgrep } from "../file/ripgrep" import { Ripgrep } from "../file"
import { assertExternalDirectoryEffect } from "./external-directory" import { assertExternalDirectoryEffect } from "./external-directory"
import DESCRIPTION from "./glob.txt" import DESCRIPTION from "./glob.txt"
import { Tool } from "./tool" import { Tool } from "./tool"
+1 -1
View File
@@ -3,7 +3,7 @@ import z from "zod"
import { Effect, Option } from "effect" import { Effect, Option } from "effect"
import { InstanceState } from "@/effect" import { InstanceState } from "@/effect"
import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { Ripgrep } from "../file/ripgrep" import { Ripgrep } from "../file"
import { assertExternalDirectoryEffect } from "./external-directory" import { assertExternalDirectoryEffect } from "./external-directory"
import DESCRIPTION from "./grep.txt" import DESCRIPTION from "./grep.txt"
import { Tool } from "./tool" import { Tool } from "./tool"
+1 -1
View File
@@ -7,7 +7,7 @@ import { createInterface } from "readline"
import { Tool } from "./tool" import { Tool } from "./tool"
import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { LSP } from "../lsp" import { LSP } from "../lsp"
import { FileTime } from "../file/time" import { FileTime } from "../file"
import DESCRIPTION from "./read.txt" import DESCRIPTION from "./read.txt"
import { Instance } from "../project/instance" import { Instance } from "../project/instance"
import { assertExternalDirectoryEffect } from "./external-directory" import { assertExternalDirectoryEffect } from "./external-directory"
+2 -2
View File
@@ -33,13 +33,13 @@ import { Effect, Layer, Context } from "effect"
import { FetchHttpClient, HttpClient } from "effect/unstable/http" import { FetchHttpClient, HttpClient } from "effect/unstable/http"
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
import { Ripgrep } from "../file/ripgrep" import { Ripgrep } from "../file"
import { Format } from "../format" import { Format } from "../format"
import { InstanceState } from "@/effect" import { InstanceState } from "@/effect"
import { Question } from "../question" import { Question } from "../question"
import { Todo } from "../session/todo" import { Todo } from "../session/todo"
import { LSP } from "../lsp" import { LSP } from "../lsp"
import { FileTime } from "../file/time" import { FileTime } from "../file"
import { Instruction } from "../session/instruction" import { Instruction } from "../session/instruction"
import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { Bus } from "../bus" import { Bus } from "../bus"
+1 -1
View File
@@ -4,7 +4,7 @@ import z from "zod"
import { Effect } from "effect" import { Effect } from "effect"
import * as Stream from "effect/Stream" import * as Stream from "effect/Stream"
import { EffectLogger } from "@/effect" import { EffectLogger } from "@/effect"
import { Ripgrep } from "../file/ripgrep" import { Ripgrep } from "../file"
import { Skill } from "../skill" import { Skill } from "../skill"
import { Tool } from "./tool" import { Tool } from "./tool"
+2 -2
View File
@@ -7,9 +7,9 @@ import { createTwoFilesPatch } from "diff"
import DESCRIPTION from "./write.txt" import DESCRIPTION from "./write.txt"
import { Bus } from "../bus" import { Bus } from "../bus"
import { File } from "../file" import { File } from "../file"
import { FileWatcher } from "../file/watcher" import { FileWatcher } from "../file"
import { Format } from "../format" import { Format } from "../format"
import { FileTime } from "../file/time" import { FileTime } from "../file"
import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { Instance } from "../project/instance" import { Instance } from "../project/instance"
import { trimDiff } from "./edit" import { trimDiff } from "./edit"
+1 -1
View File
@@ -1,5 +1,5 @@
import { test, expect } from "bun:test" import { test, expect } from "bun:test"
import { FileIgnore } from "../../src/file/ignore" import { FileIgnore } from "../../src/file"
test("match nested and non-nested", () => { test("match nested and non-nested", () => {
expect(FileIgnore.match("node_modules/index.js")).toBe(true) expect(FileIgnore.match("node_modules/index.js")).toBe(true)
+1 -1
View File
@@ -4,7 +4,7 @@ import * as Stream from "effect/Stream"
import fs from "fs/promises" import fs from "fs/promises"
import path from "path" import path from "path"
import { tmpdir } from "../fixture/fixture" import { tmpdir } from "../fixture/fixture"
import { Ripgrep } from "../../src/file/ripgrep" import { Ripgrep } from "../../src/file"
const run = <A>(effect: Effect.Effect<A, unknown, Ripgrep.Service>) => const run = <A>(effect: Effect.Effect<A, unknown, Ripgrep.Service>) =>
effect.pipe(Effect.provide(Ripgrep.defaultLayer), Effect.runPromise) effect.pipe(Effect.provide(Ripgrep.defaultLayer), Effect.runPromise)
+2 -2
View File
@@ -3,7 +3,7 @@ import fs from "fs/promises"
import path from "path" import path from "path"
import { Cause, Deferred, Effect, Exit, Fiber, Layer } from "effect" import { Cause, Deferred, Effect, Exit, Fiber, Layer } from "effect"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { FileTime } from "../../src/file/time" import { FileTime } from "../../src/file"
import { Instance } from "../../src/project/instance" import { Instance } from "../../src/project/instance"
import { SessionID } from "../../src/session/schema" import { SessionID } from "../../src/session/schema"
import { Filesystem } from "../../src/util" import { Filesystem } from "../../src/util"
@@ -43,7 +43,7 @@ const fail = Effect.fn("FileTimeTest.fail")(function* <A, E, R>(self: Effect.Eff
throw new Error("expected file time effect to fail") throw new Error("expected file time effect to fail")
}) })
describe("file/time", () => { describe("file", () => {
describe("read() and get()", () => { describe("read() and get()", () => {
it.live("stores read timestamp", () => it.live("stores read timestamp", () =>
provideTmpdirInstance((dir) => provideTmpdirInstance((dir) =>
+1 -1
View File
@@ -6,7 +6,7 @@ import { ConfigProvider, Deferred, Effect, Layer, ManagedRuntime, Option } from
import { tmpdir } from "../fixture/fixture" import { tmpdir } from "../fixture/fixture"
import { Bus } from "../../src/bus" import { Bus } from "../../src/bus"
import { Config } from "../../src/config" import { Config } from "../../src/config"
import { FileWatcher } from "../../src/file/watcher" import { FileWatcher } from "../../src/file"
import { Git } from "../../src/git" import { Git } from "../../src/git"
import { Instance } from "../../src/project/instance" import { Instance } from "../../src/project/instance"
+1 -1
View File
@@ -5,7 +5,7 @@ import fs from "fs/promises"
import path from "path" import path from "path"
import { tmpdir } from "../fixture/fixture" import { tmpdir } from "../fixture/fixture"
import { AppRuntime } from "../../src/effect/app-runtime" import { AppRuntime } from "../../src/effect/app-runtime"
import { FileWatcher } from "../../src/file/watcher" import { FileWatcher } from "../../src/file"
import { Instance } from "../../src/project/instance" import { Instance } from "../../src/project/instance"
import { GlobalBus } from "../../src/bus/global" import { GlobalBus } from "../../src/bus/global"
import { Vcs } from "../../src/project" import { Vcs } from "../../src/project"
@@ -7,7 +7,7 @@ import { Agent as AgentSvc } from "../../src/agent/agent"
import { Bus } from "../../src/bus" import { Bus } from "../../src/bus"
import { Command } from "../../src/command" import { Command } from "../../src/command"
import { Config } from "../../src/config" import { Config } from "../../src/config"
import { FileTime } from "../../src/file/time" import { FileTime } from "../../src/file"
import { LSP } from "../../src/lsp" import { LSP } from "../../src/lsp"
import { MCP } from "../../src/mcp" import { MCP } from "../../src/mcp"
import { Permission } from "../../src/permission" import { Permission } from "../../src/permission"
@@ -38,7 +38,7 @@ import { ToolRegistry } from "../../src/tool/registry"
import { Truncate } from "../../src/tool/truncate" import { Truncate } from "../../src/tool/truncate"
import { Log } from "../../src/util" import { Log } from "../../src/util"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { Ripgrep } from "../../src/file/ripgrep" import { Ripgrep } from "../../src/file"
import { Format } from "../../src/format" import { Format } from "../../src/format"
import { provideTmpdirInstance, provideTmpdirServer } from "../fixture/fixture" import { provideTmpdirInstance, provideTmpdirServer } from "../fixture/fixture"
import { testEffect } from "../lib/effect" import { testEffect } from "../lib/effect"
@@ -33,7 +33,7 @@ import { Agent as AgentSvc } from "../../src/agent/agent"
import { Bus } from "../../src/bus" import { Bus } from "../../src/bus"
import { Command } from "../../src/command" import { Command } from "../../src/command"
import { Config } from "../../src/config" import { Config } from "../../src/config"
import { FileTime } from "../../src/file/time" import { FileTime } from "../../src/file"
import { LSP } from "../../src/lsp" import { LSP } from "../../src/lsp"
import { MCP } from "../../src/mcp" import { MCP } from "../../src/mcp"
import { Permission } from "../../src/permission" import { Permission } from "../../src/permission"
@@ -54,7 +54,7 @@ import { ToolRegistry } from "../../src/tool/registry"
import { Truncate } from "../../src/tool/truncate" import { Truncate } from "../../src/tool/truncate"
import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { Ripgrep } from "../../src/file/ripgrep" import { Ripgrep } from "../../src/file"
import { Format } from "../../src/format" import { Format } from "../../src/format"
void Log.init({ print: false }) void Log.init({ print: false })
+3 -3
View File
@@ -5,7 +5,7 @@ import { Effect, Layer, ManagedRuntime } from "effect"
import { EditTool } from "../../src/tool/edit" import { EditTool } from "../../src/tool/edit"
import { Instance } from "../../src/project/instance" import { Instance } from "../../src/project/instance"
import { tmpdir } from "../fixture/fixture" import { tmpdir } from "../fixture/fixture"
import { FileTime } from "../../src/file/time" import { FileTime } from "../../src/file"
import { LSP } from "../../src/lsp" import { LSP } from "../../src/lsp"
import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { Format } from "../../src/format" import { Format } from "../../src/format"
@@ -138,7 +138,7 @@ describe("tool.edit", () => {
await Instance.provide({ await Instance.provide({
directory: tmp.path, directory: tmp.path,
fn: async () => { fn: async () => {
const { FileWatcher } = await import("../../src/file/watcher") const { FileWatcher } = await import("../../src/file")
const updated = await onceBus(FileWatcher.Event.Updated) const updated = await onceBus(FileWatcher.Event.Updated)
@@ -371,7 +371,7 @@ describe("tool.edit", () => {
fn: async () => { fn: async () => {
await readFileTime(ctx.sessionID, filepath) await readFileTime(ctx.sessionID, filepath)
const { FileWatcher } = await import("../../src/file/watcher") const { FileWatcher } = await import("../../src/file")
const updated = await onceBus(FileWatcher.Event.Updated) const updated = await onceBus(FileWatcher.Event.Updated)
+1 -1
View File
@@ -4,7 +4,7 @@ import { Cause, Effect, Exit, Layer } from "effect"
import { GlobTool } from "../../src/tool/glob" import { GlobTool } from "../../src/tool/glob"
import { SessionID, MessageID } from "../../src/session/schema" import { SessionID, MessageID } from "../../src/session/schema"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { Ripgrep } from "../../src/file/ripgrep" import { Ripgrep } from "../../src/file"
import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { Truncate } from "../../src/tool/truncate" import { Truncate } from "../../src/tool/truncate"
import { Agent } from "../../src/agent/agent" import { Agent } from "../../src/agent/agent"
+1 -1
View File
@@ -7,7 +7,7 @@ import { SessionID, MessageID } from "../../src/session/schema"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { Truncate } from "../../src/tool/truncate" import { Truncate } from "../../src/tool/truncate"
import { Agent } from "../../src/agent/agent" import { Agent } from "../../src/agent/agent"
import { Ripgrep } from "../../src/file/ripgrep" import { Ripgrep } from "../../src/file"
import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { testEffect } from "../lib/effect" import { testEffect } from "../lib/effect"
+1 -1
View File
@@ -4,7 +4,7 @@ import path from "path"
import { Agent } from "../../src/agent/agent" import { Agent } from "../../src/agent/agent"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { FileTime } from "../../src/file/time" import { FileTime } from "../../src/file"
import { LSP } from "../../src/lsp" import { LSP } from "../../src/lsp"
import { Permission } from "../../src/permission" import { Permission } from "../../src/permission"
import { Instance } from "../../src/project/instance" import { Instance } from "../../src/project/instance"
+1 -1
View File
@@ -6,7 +6,7 @@ import { WriteTool } from "../../src/tool/write"
import { Instance } from "../../src/project/instance" import { Instance } from "../../src/project/instance"
import { LSP } from "../../src/lsp" import { LSP } from "../../src/lsp"
import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { FileTime } from "../../src/file/time" import { FileTime } from "../../src/file"
import { Bus } from "../../src/bus" import { Bus } from "../../src/bus"
import { Format } from "../../src/format" import { Format } from "../../src/format"
import { Truncate } from "../../src/tool/truncate" import { Truncate } from "../../src/tool/truncate"