mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-09 10:59:49 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e61fe8e5f8 |
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
@@ -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))
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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,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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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) =>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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 })
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user