Compare commits

...
Sign in to create a new pull request.

1 commit

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 834 additions and 839 deletions

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"

View file

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

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"

View file

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

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({

View file

@ -1,7 +1,6 @@
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",
@ -30,9 +29,9 @@ export namespace FileIgnore {
"mypy_cache", "mypy_cache",
".history", ".history",
".gradle", ".gradle",
]) ])
const FILES = [ const FILES = [
"**/*.swp", "**/*.swp",
"**/*.swo", "**/*.swo",
@ -51,17 +50,17 @@ export namespace FileIgnore {
// 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
} }
@ -77,5 +76,4 @@ export namespace FileIgnore {
} }
return false return false
}
} }

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"

View file

@ -37,16 +37,15 @@ 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)),
@ -55,5 +54,4 @@ export namespace Protected {
] ]
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 []
}
} }

View file

@ -8,10 +8,9 @@ import { ripgrep } from "ripgrep"
import { Filesystem } from "@/util" import { Filesystem } from "@/util"
import { Log } from "@/util" import { Log } from "@/util"
export namespace Ripgrep { const log = Log.create({ service: "ripgrep" })
const log = Log.create({ service: "ripgrep" })
const Stats = z.object({ const Stats = z.object({
elapsed: z.object({ elapsed: z.object({
secs: z.number(), secs: z.number(),
nanos: z.number(), nanos: z.number(),
@ -23,18 +22,18 @@ export namespace Ripgrep {
bytes_printed: z.number(), bytes_printed: z.number(),
matched_lines: z.number(), matched_lines: z.number(),
matches: z.number(), matches: z.number(),
}) })
const Begin = z.object({ const Begin = z.object({
type: z.literal("begin"), type: z.literal("begin"),
data: z.object({ data: z.object({
path: z.object({ path: z.object({
text: z.string(), text: z.string(),
}), }),
}), }),
}) })
export const Match = z.object({ export const Match = z.object({
type: z.literal("match"), type: z.literal("match"),
data: z.object({ data: z.object({
path: z.object({ path: z.object({
@ -55,9 +54,9 @@ export namespace Ripgrep {
}), }),
), ),
}), }),
}) })
const End = z.object({ const End = z.object({
type: z.literal("end"), type: z.literal("end"),
data: z.object({ data: z.object({
path: z.object({ path: z.object({
@ -66,9 +65,9 @@ export namespace Ripgrep {
binary_offset: z.number().nullable(), binary_offset: z.number().nullable(),
stats: Stats, stats: Stats,
}), }),
}) })
const Summary = z.object({ const Summary = z.object({
type: z.literal("summary"), type: z.literal("summary"),
data: z.object({ data: z.object({
elapsed_total: z.object({ elapsed_total: z.object({
@ -78,33 +77,33 @@ export namespace Ripgrep {
}), }),
stats: Stats, stats: Stats,
}), }),
}) })
const Result = z.union([Begin, Match, End, Summary]) const Result = z.union([Begin, Match, End, Summary])
export type Result = z.infer<typeof Result> export type Result = z.infer<typeof Result>
export type Match = z.infer<typeof Match> export type Match = z.infer<typeof Match>
export type Item = Match["data"] export type Item = Match["data"]
export type Begin = z.infer<typeof Begin> export type Begin = z.infer<typeof Begin>
export type End = z.infer<typeof End> export type End = z.infer<typeof End>
export type Summary = z.infer<typeof Summary> export type Summary = z.infer<typeof Summary>
export type Row = Match["data"] export type Row = Match["data"]
export interface SearchResult { export interface SearchResult {
items: Item[] items: Item[]
partial: boolean partial: boolean
} }
export interface FilesInput { export interface FilesInput {
cwd: string cwd: string
glob?: string[] glob?: string[]
hidden?: boolean hidden?: boolean
follow?: boolean follow?: boolean
maxDepth?: number maxDepth?: number
signal?: AbortSignal signal?: AbortSignal
} }
export interface SearchInput { export interface SearchInput {
cwd: string cwd: string
pattern: string pattern: string
glob?: string[] glob?: string[]
@ -112,91 +111,91 @@ export namespace Ripgrep {
follow?: boolean follow?: boolean
file?: string[] file?: string[]
signal?: AbortSignal signal?: AbortSignal
} }
export interface TreeInput { export interface TreeInput {
cwd: string cwd: string
limit?: number limit?: number
signal?: AbortSignal signal?: AbortSignal
} }
export interface Interface { export interface Interface {
readonly files: (input: FilesInput) => Stream.Stream<string, Error> readonly files: (input: FilesInput) => Stream.Stream<string, Error>
readonly tree: (input: TreeInput) => Effect.Effect<string, Error> readonly tree: (input: TreeInput) => Effect.Effect<string, Error>
readonly search: (input: SearchInput) => Effect.Effect<SearchResult, Error> readonly search: (input: SearchInput) => Effect.Effect<SearchResult, Error>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/Ripgrep") {} export class Service extends Context.Service<Service, Interface>()("@opencode/Ripgrep") {}
type Run = { kind: "files" | "search"; cwd: string; args: string[] } type Run = { kind: "files" | "search"; cwd: string; args: string[] }
type WorkerResult = { type WorkerResult = {
type: "result" type: "result"
code: number code: number
stdout: string stdout: string
stderr: string stderr: string
} }
type WorkerLine = { type WorkerLine = {
type: "line" type: "line"
line: string line: string
} }
type WorkerDone = { type WorkerDone = {
type: "done" type: "done"
code: number code: number
stderr: string stderr: string
} }
type WorkerError = { type WorkerError = {
type: "error" type: "error"
error: { error: {
message: string message: string
name?: string name?: string
stack?: string stack?: string
} }
} }
function env() { function env() {
const env = Object.fromEntries( const env = Object.fromEntries(
Object.entries(process.env).filter((item): item is [string, string] => item[1] !== undefined), Object.entries(process.env).filter((item): item is [string, string] => item[1] !== undefined),
) )
delete env.RIPGREP_CONFIG_PATH delete env.RIPGREP_CONFIG_PATH
return env return env
} }
function text(input: unknown) { function text(input: unknown) {
if (typeof input === "string") return input if (typeof input === "string") return input
if (input instanceof ArrayBuffer) return Buffer.from(input).toString() if (input instanceof ArrayBuffer) return Buffer.from(input).toString()
if (ArrayBuffer.isView(input)) return Buffer.from(input.buffer, input.byteOffset, input.byteLength).toString() if (ArrayBuffer.isView(input)) return Buffer.from(input.buffer, input.byteOffset, input.byteLength).toString()
return String(input) return String(input)
} }
function toError(input: unknown) { function toError(input: unknown) {
if (input instanceof Error) return input if (input instanceof Error) return input
if (typeof input === "string") return new Error(input) if (typeof input === "string") return new Error(input)
return new Error(String(input)) return new Error(String(input))
} }
function abort(signal?: AbortSignal) { function abort(signal?: AbortSignal) {
const err = signal?.reason const err = signal?.reason
if (err instanceof Error) return err if (err instanceof Error) return err
const out = new Error("Aborted") const out = new Error("Aborted")
out.name = "AbortError" out.name = "AbortError"
return out return out
} }
function error(stderr: string, code: number) { function error(stderr: string, code: number) {
const err = new Error(stderr.trim() || `ripgrep failed with code ${code}`) const err = new Error(stderr.trim() || `ripgrep failed with code ${code}`)
err.name = "RipgrepError" err.name = "RipgrepError"
return err return err
} }
function clean(file: string) { function clean(file: string) {
return path.normalize(file.replace(/^\.[\\/]/, "")) return path.normalize(file.replace(/^\.[\\/]/, ""))
} }
function row(data: Row): Row { function row(data: Row): Row {
return { return {
...data, ...data,
path: { path: {
@ -204,16 +203,16 @@ export namespace Ripgrep {
text: clean(data.path.text), text: clean(data.path.text),
}, },
} }
} }
function opts(cwd: string) { function opts(cwd: string) {
return { return {
env: env(), env: env(),
preopens: { ".": cwd }, preopens: { ".": cwd },
} }
} }
function check(cwd: string) { function check(cwd: string) {
return Effect.tryPromise({ return Effect.tryPromise({
try: () => fs.stat(cwd).catch(() => undefined), try: () => fs.stat(cwd).catch(() => undefined),
catch: toError, catch: toError,
@ -230,9 +229,9 @@ export namespace Ripgrep {
), ),
), ),
) )
} }
function filesArgs(input: FilesInput) { function filesArgs(input: FilesInput) {
const args = ["--files", "--glob=!.git/*"] const args = ["--files", "--glob=!.git/*"]
if (input.follow) args.push("--follow") if (input.follow) args.push("--follow")
if (input.hidden !== false) args.push("--hidden") if (input.hidden !== false) args.push("--hidden")
@ -244,9 +243,9 @@ export namespace Ripgrep {
} }
args.push(".") args.push(".")
return args return args
} }
function searchArgs(input: SearchInput) { function searchArgs(input: SearchInput) {
const args = ["--json", "--hidden", "--glob=!.git/*", "--no-messages"] const args = ["--json", "--hidden", "--glob=!.git/*", "--no-messages"]
if (input.follow) args.push("--follow") if (input.follow) args.push("--follow")
if (input.glob) { if (input.glob) {
@ -257,20 +256,20 @@ export namespace Ripgrep {
if (input.limit) args.push(`--max-count=${input.limit}`) if (input.limit) args.push(`--max-count=${input.limit}`)
args.push("--", input.pattern, ...(input.file ?? ["."])) args.push("--", input.pattern, ...(input.file ?? ["."]))
return args return args
} }
function parse(stdout: string) { function parse(stdout: string) {
return stdout return stdout
.trim() .trim()
.split(/\r?\n/) .split(/\r?\n/)
.filter(Boolean) .filter(Boolean)
.map((line) => Result.parse(JSON.parse(line))) .map((line) => Result.parse(JSON.parse(line)))
.flatMap((item) => (item.type === "match" ? [row(item.data)] : [])) .flatMap((item) => (item.type === "match" ? [row(item.data)] : []))
} }
declare const OPENCODE_RIPGREP_WORKER_PATH: string declare const OPENCODE_RIPGREP_WORKER_PATH: string
function target(): Effect.Effect<string | URL, Error> { function target(): Effect.Effect<string | URL, Error> {
if (typeof OPENCODE_RIPGREP_WORKER_PATH !== "undefined") { if (typeof OPENCODE_RIPGREP_WORKER_PATH !== "undefined") {
return Effect.succeed(OPENCODE_RIPGREP_WORKER_PATH) return Effect.succeed(OPENCODE_RIPGREP_WORKER_PATH)
} }
@ -279,26 +278,26 @@ export namespace Ripgrep {
try: () => Filesystem.exists(fileURLToPath(js)), try: () => Filesystem.exists(fileURLToPath(js)),
catch: toError, catch: toError,
}).pipe(Effect.map((exists) => (exists ? js : new URL("./ripgrep.worker.ts", import.meta.url)))) }).pipe(Effect.map((exists) => (exists ? js : new URL("./ripgrep.worker.ts", import.meta.url))))
} }
function worker() { function worker() {
return target().pipe(Effect.flatMap((file) => Effect.sync(() => new Worker(file, { env: env() })))) return target().pipe(Effect.flatMap((file) => Effect.sync(() => new Worker(file, { env: env() }))))
} }
function drain(buf: string, chunk: unknown, push: (line: string) => void) { function drain(buf: string, chunk: unknown, push: (line: string) => void) {
const lines = (buf + text(chunk)).split(/\r?\n/) const lines = (buf + text(chunk)).split(/\r?\n/)
buf = lines.pop() || "" buf = lines.pop() || ""
for (const line of lines) { for (const line of lines) {
if (line) push(line) if (line) push(line)
} }
return buf return buf
} }
function fail(queue: Queue.Queue<string, Error | Cause.Done>, err: Error) { function fail(queue: Queue.Queue<string, Error | Cause.Done>, err: Error) {
Queue.failCauseUnsafe(queue, Cause.fail(err)) Queue.failCauseUnsafe(queue, Cause.fail(err))
} }
function searchDirect(input: SearchInput) { function searchDirect(input: SearchInput) {
return Effect.tryPromise({ return Effect.tryPromise({
try: () => try: () =>
ripgrep(searchArgs(input), { ripgrep(searchArgs(input), {
@ -318,9 +317,9 @@ export namespace Ripgrep {
})) }))
}), }),
) )
} }
function searchWorker(input: SearchInput) { function searchWorker(input: SearchInput) {
if (input.signal?.aborted) return Effect.fail(abort(input.signal)) if (input.signal?.aborted) return Effect.fail(abort(input.signal))
return Effect.acquireUseRelease( return Effect.acquireUseRelease(
@ -377,9 +376,9 @@ export namespace Ripgrep {
}), }),
(w) => Effect.sync(() => w.terminate()), (w) => Effect.sync(() => w.terminate()),
) )
} }
function filesDirect(input: FilesInput) { function filesDirect(input: FilesInput) {
return Stream.callback<string, Error>( return Stream.callback<string, Error>(
Effect.fnUntraced(function* (queue: Queue.Queue<string, Error | Cause.Done>) { Effect.fnUntraced(function* (queue: Queue.Queue<string, Error | Cause.Done>) {
let buf = "" let buf = ""
@ -427,9 +426,9 @@ export namespace Ripgrep {
) )
}), }),
) )
} }
function filesWorker(input: FilesInput) { function filesWorker(input: FilesInput) {
return Stream.callback<string, Error>( return Stream.callback<string, Error>(
Effect.fnUntraced(function* (queue: Queue.Queue<string, Error | Cause.Done>) { Effect.fnUntraced(function* (queue: Queue.Queue<string, Error | Cause.Done>) {
if (input.signal?.aborted) { if (input.signal?.aborted) {
@ -489,9 +488,9 @@ export namespace Ripgrep {
) )
}), }),
) )
} }
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const source = (input: FilesInput) => { const source = (input: FilesInput) => {
@ -569,7 +568,6 @@ export namespace Ripgrep {
return Service.of({ files, tree, search }) return Service.of({ files, tree, search })
}), }),
) )
export const defaultLayer = layer export const defaultLayer = layer
}

View file

@ -5,39 +5,38 @@ 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 session = (reads: Map<SessionID, Map<string, Stamp>>, sessionID: SessionID) => {
const value = reads.get(sessionID) const value = reads.get(sessionID)
if (value) return value if (value) return value
const next = new Map<string, Stamp>() const next = new Map<string, Stamp>()
reads.set(sessionID, next) reads.set(sessionID, next)
return next return next
} }
interface State { interface State {
reads: Map<SessionID, Map<string, Stamp>> reads: Map<SessionID, Map<string, Stamp>>
locks: Map<string, Semaphore.Semaphore> locks: Map<string, Semaphore.Semaphore>
} }
export interface Interface { export interface Interface {
readonly read: (sessionID: SessionID, file: string) => Effect.Effect<void> readonly read: (sessionID: SessionID, file: string) => Effect.Effect<void>
readonly get: (sessionID: SessionID, file: string) => Effect.Effect<Date | undefined> readonly get: (sessionID: SessionID, file: string) => Effect.Effect<Date | undefined>
readonly assert: (sessionID: SessionID, filepath: string) => Effect.Effect<void> readonly assert: (sessionID: SessionID, filepath: string) => Effect.Effect<void>
readonly withLock: <T>(filepath: string, fn: () => Effect.Effect<T>) => Effect.Effect<T> readonly withLock: <T>(filepath: string, fn: () => Effect.Effect<T>) => Effect.Effect<T>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/FileTime") {} export class Service extends Context.Service<Service, Interface>()("@opencode/FileTime") {}
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const fsys = yield* AppFileSystem.Service const fsys = yield* AppFileSystem.Service
@ -107,7 +106,6 @@ export namespace FileTime {
return Service.of({ read, get, assert, withLock }) return Service.of({ read, get, assert, withLock })
}), }),
).pipe(Layer.orDie) ).pipe(Layer.orDie)
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer)) export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
}

View file

@ -13,17 +13,16 @@ 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({
@ -31,9 +30,9 @@ export namespace FileWatcher {
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 => { const watcher = lazy((): typeof import("@parcel/watcher") | undefined => {
try { try {
const binding = require( const binding = require(
`@parcel/watcher-${process.platform}-${process.arch}${process.platform === "linux" ? `-${OPENCODE_LIBC || "glibc"}` : ""}`, `@parcel/watcher-${process.platform}-${process.arch}${process.platform === "linux" ? `-${OPENCODE_LIBC || "glibc"}` : ""}`,
@ -43,30 +42,30 @@ export namespace FileWatcher {
log.error("failed to load watcher binding", { error }) log.error("failed to load watcher binding", { error })
return return
} }
}) })
function getBackend() { function getBackend() {
if (process.platform === "win32") return "windows" if (process.platform === "win32") return "windows"
if (process.platform === "darwin") return "fs-events" if (process.platform === "darwin") return "fs-events"
if (process.platform === "linux") return "inotify" if (process.platform === "linux") return "inotify"
} }
function protecteds(dir: string) { function protecteds(dir: string) {
return Protected.paths().filter((item) => { return Protected.paths().filter((item) => {
const rel = path.relative(dir, item) const rel = path.relative(dir, item)
return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel) return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel)
}) })
} }
export const hasNativeBinding = () => !!watcher() export const hasNativeBinding = () => !!watcher()
export interface Interface { export interface Interface {
readonly init: () => Effect.Effect<void> readonly init: () => Effect.Effect<void>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/FileWatcher") {} export class Service extends Context.Service<Service, Interface>()("@opencode/FileWatcher") {}
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const config = yield* Config.Service const config = yield* Config.Service
@ -157,7 +156,6 @@ export namespace FileWatcher {
}), }),
}) })
}), }),
) )
export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer), Layer.provide(Git.defaultLayer)) export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer), Layer.provide(Git.defaultLayer))
}

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"

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"

View file

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

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"

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"

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"

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"

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"

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"

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"

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"

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"

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)

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)

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

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"

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"

View file

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

View file

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

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)

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"

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"

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"

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"