diff --git a/packages/opencode/src/effect/instance-context.ts b/packages/opencode/src/effect/instance-context.ts index af5f9236f..fd4590190 100644 --- a/packages/opencode/src/effect/instance-context.ts +++ b/packages/opencode/src/effect/instance-context.ts @@ -1,15 +1,14 @@ -import { ServiceMap } from "effect"; -import type { Project } from "@/project/project"; +import { ServiceMap } from "effect" +import type { Project } from "@/project/project" export declare namespace InstanceContext { - export interface Shape { - readonly directory: string; - readonly worktree: string; - readonly project: Project.Info; - } + export interface Shape { + readonly directory: string + readonly worktree: string + readonly project: Project.Info + } } -export class InstanceContext extends ServiceMap.Service< - InstanceContext, - InstanceContext.Shape ->()("opencode/InstanceContext") {} +export class InstanceContext extends ServiceMap.Service()( + "opencode/InstanceContext", +) {} diff --git a/packages/opencode/src/effect/instances.ts b/packages/opencode/src/effect/instances.ts index 075663f08..16186f729 100644 --- a/packages/opencode/src/effect/instances.ts +++ b/packages/opencode/src/effect/instances.ts @@ -1,31 +1,31 @@ -import { Effect, Layer, LayerMap, ServiceMap } from "effect"; -import { FileService } from "@/file"; -import { FileTimeService } from "@/file/time"; -import { FileWatcherService } from "@/file/watcher"; -import { FormatService } from "@/format"; -import { PermissionService } from "@/permission/service"; -import { Instance } from "@/project/instance"; -import { VcsService } from "@/project/vcs"; -import { ProviderAuthService } from "@/provider/auth-service"; -import { QuestionService } from "@/question/service"; -import { SkillService } from "@/skill/skill"; -import { SnapshotService } from "@/snapshot"; -import { InstanceContext } from "./instance-context"; -import { registerDisposer } from "./instance-registry"; +import { Effect, Layer, LayerMap, ServiceMap } from "effect" +import { FileService } from "@/file" +import { FileTimeService } from "@/file/time" +import { FileWatcherService } from "@/file/watcher" +import { FormatService } from "@/format" +import { PermissionService } from "@/permission/service" +import { Instance } from "@/project/instance" +import { VcsService } from "@/project/vcs" +import { ProviderAuthService } from "@/provider/auth-service" +import { QuestionService } from "@/question/service" +import { SkillService } from "@/skill/skill" +import { SnapshotService } from "@/snapshot" +import { InstanceContext } from "./instance-context" +import { registerDisposer } from "./instance-registry" -export { InstanceContext } from "./instance-context"; +export { InstanceContext } from "./instance-context" export type InstanceServices = - | QuestionService - | PermissionService - | ProviderAuthService - | FileWatcherService - | VcsService - | FileTimeService - | FormatService - | FileService - | SkillService - | SnapshotService; + | QuestionService + | PermissionService + | ProviderAuthService + | FileWatcherService + | VcsService + | FileTimeService + | FormatService + | FileService + | SkillService + | SnapshotService // NOTE: LayerMap only passes the key (directory string) to lookup, but we need // the full instance context (directory, worktree, project). We read from the @@ -34,50 +34,41 @@ export type InstanceServices = // This should go away once the old Instance type is removed and lookup can load // the full context directly. function lookup(_key: string) { - const ctx = Layer.sync(InstanceContext, () => - InstanceContext.of(Instance.current), - ); - return Layer.mergeAll( - Layer.fresh(QuestionService.layer), - Layer.fresh(PermissionService.layer), - Layer.fresh(ProviderAuthService.layer), - Layer.fresh(FileWatcherService.layer).pipe(Layer.orDie), - Layer.fresh(VcsService.layer), - Layer.fresh(FileTimeService.layer).pipe(Layer.orDie), - Layer.fresh(FormatService.layer), - Layer.fresh(FileService.layer), - Layer.fresh(SkillService.layer), - Layer.fresh(SnapshotService.layer), - ).pipe(Layer.provide(ctx)); + const ctx = Layer.sync(InstanceContext, () => InstanceContext.of(Instance.current)) + return Layer.mergeAll( + Layer.fresh(QuestionService.layer), + Layer.fresh(PermissionService.layer), + Layer.fresh(ProviderAuthService.layer), + Layer.fresh(FileWatcherService.layer).pipe(Layer.orDie), + Layer.fresh(VcsService.layer), + Layer.fresh(FileTimeService.layer).pipe(Layer.orDie), + Layer.fresh(FormatService.layer), + Layer.fresh(FileService.layer), + Layer.fresh(SkillService.layer), + Layer.fresh(SnapshotService.layer), + ).pipe(Layer.provide(ctx)) } -export class Instances extends ServiceMap.Service< - Instances, - LayerMap.LayerMap ->()("opencode/Instances") { - static readonly layer = Layer.effect( - Instances, - Effect.gen(function* () { - const layerMap = yield* LayerMap.make(lookup, { - idleTimeToLive: Infinity, - }); - const unregister = registerDisposer((directory) => - Effect.runPromise(layerMap.invalidate(directory)), - ); - yield* Effect.addFinalizer(() => Effect.sync(unregister)); - return Instances.of(layerMap); - }), - ); +export class Instances extends ServiceMap.Service>()( + "opencode/Instances", +) { + static readonly layer = Layer.effect( + Instances, + Effect.gen(function* () { + const layerMap = yield* LayerMap.make(lookup, { + idleTimeToLive: Infinity, + }) + const unregister = registerDisposer((directory) => Effect.runPromise(layerMap.invalidate(directory))) + yield* Effect.addFinalizer(() => Effect.sync(unregister)) + return Instances.of(layerMap) + }), + ) - static get( - directory: string, - ): Layer.Layer { - return Layer.unwrap( - Instances.use((map) => Effect.succeed(map.get(directory))), - ); - } + static get(directory: string): Layer.Layer { + return Layer.unwrap(Instances.use((map) => Effect.succeed(map.get(directory)))) + } - static invalidate(directory: string): Effect.Effect { - return Instances.use((map) => map.invalidate(directory)); - } + static invalidate(directory: string): Effect.Effect { + return Instances.use((map) => map.invalidate(directory)) + } } diff --git a/packages/opencode/src/project/instance.ts b/packages/opencode/src/project/instance.ts index 61f6dd793..607554016 100644 --- a/packages/opencode/src/project/instance.ts +++ b/packages/opencode/src/project/instance.ts @@ -1,185 +1,166 @@ -import { GlobalBus } from "@/bus/global"; -import { disposeInstance } from "@/effect/instance-registry"; -import { Filesystem } from "@/util/filesystem"; -import { iife } from "@/util/iife"; -import { Log } from "@/util/log"; -import { Context } from "../util/context"; -import { Project } from "./project"; -import { State } from "./state"; +import { GlobalBus } from "@/bus/global" +import { disposeInstance } from "@/effect/instance-registry" +import { Filesystem } from "@/util/filesystem" +import { iife } from "@/util/iife" +import { Log } from "@/util/log" +import { Context } from "../util/context" +import { Project } from "./project" +import { State } from "./state" interface Context { - directory: string; - worktree: string; - project: Project.Info; + directory: string + worktree: string + project: Project.Info } -const context = Context.create("instance"); -const cache = new Map>(); +const context = Context.create("instance") +const cache = new Map>() const disposal = { - all: undefined as Promise | undefined, -}; - -function emit(directory: string) { - GlobalBus.emit("event", { - directory, - payload: { - type: "server.instance.disposed", - properties: { - directory, - }, - }, - }); + all: undefined as Promise | undefined, } -function boot(input: { - directory: string; - init?: () => Promise; - project?: Project.Info; - worktree?: string; -}) { - return iife(async () => { - const ctx = - input.project && input.worktree - ? { - directory: input.directory, - worktree: input.worktree, - project: input.project, - } - : await Project.fromDirectory(input.directory).then( - ({ project, sandbox }) => ({ - directory: input.directory, - worktree: sandbox, - project, - }), - ); - await context.provide(ctx, async () => { - await input.init?.(); - }); - return ctx; - }); +function emit(directory: string) { + GlobalBus.emit("event", { + directory, + payload: { + type: "server.instance.disposed", + properties: { + directory, + }, + }, + }) +} + +function boot(input: { directory: string; init?: () => Promise; project?: Project.Info; worktree?: string }) { + return iife(async () => { + const ctx = + input.project && input.worktree + ? { + directory: input.directory, + worktree: input.worktree, + project: input.project, + } + : await Project.fromDirectory(input.directory).then(({ project, sandbox }) => ({ + directory: input.directory, + worktree: sandbox, + project, + })) + await context.provide(ctx, async () => { + await input.init?.() + }) + return ctx + }) } function track(directory: string, next: Promise) { - const task = next.catch((error) => { - if (cache.get(directory) === task) cache.delete(directory); - throw error; - }); - cache.set(directory, task); - return task; + const task = next.catch((error) => { + if (cache.get(directory) === task) cache.delete(directory) + throw error + }) + cache.set(directory, task) + return task } export const Instance = { - async provide(input: { - directory: string; - init?: () => Promise; - fn: () => R; - }): Promise { - const directory = Filesystem.resolve(input.directory); - let existing = cache.get(directory); - if (!existing) { - Log.Default.info("creating instance", { directory }); - existing = track( - directory, - boot({ - directory, - init: input.init, - }), - ); - } - const ctx = await existing; - return context.provide(ctx, async () => { - return input.fn(); - }); - }, - get current() { - return context.use(); - }, - get directory() { - return context.use().directory; - }, - get worktree() { - return context.use().worktree; - }, - get project() { - return context.use().project; - }, - /** - * Check if a path is within the project boundary. - * Returns true if path is inside Instance.directory OR Instance.worktree. - * Paths within the worktree but outside the working directory should not trigger external_directory permission. - */ - containsPath(filepath: string) { - if (Filesystem.contains(Instance.directory, filepath)) return true; - // Non-git projects set worktree to "/" which would match ANY absolute path. - // Skip worktree check in this case to preserve external_directory permissions. - if (Instance.worktree === "/") return false; - return Filesystem.contains(Instance.worktree, filepath); - }, - /** - * Captures the current instance ALS context and returns a wrapper that - * restores it when called. Use this for callbacks that fire outside the - * instance async context (native addons, event emitters, timers, etc.). - */ - bind any>(fn: F): F { - const ctx = context.use(); - return ((...args: any[]) => context.provide(ctx, () => fn(...args))) as F; - }, - state( - init: () => S, - dispose?: (state: Awaited) => Promise, - ): () => S { - return State.create(() => Instance.directory, init, dispose); - }, - async reload(input: { - directory: string; - init?: () => Promise; - project?: Project.Info; - worktree?: string; - }) { - const directory = Filesystem.resolve(input.directory); - Log.Default.info("reloading instance", { directory }); - await Promise.all([State.dispose(directory), disposeInstance(directory)]); - cache.delete(directory); - const next = track(directory, boot({ ...input, directory })); - emit(directory); - return await next; - }, - async dispose() { - const directory = Instance.directory; - Log.Default.info("disposing instance", { directory }); - await Promise.all([State.dispose(directory), disposeInstance(directory)]); - cache.delete(directory); - emit(directory); - }, - async disposeAll() { - if (disposal.all) return disposal.all; + async provide(input: { directory: string; init?: () => Promise; fn: () => R }): Promise { + const directory = Filesystem.resolve(input.directory) + let existing = cache.get(directory) + if (!existing) { + Log.Default.info("creating instance", { directory }) + existing = track( + directory, + boot({ + directory, + init: input.init, + }), + ) + } + const ctx = await existing + return context.provide(ctx, async () => { + return input.fn() + }) + }, + get current() { + return context.use() + }, + get directory() { + return context.use().directory + }, + get worktree() { + return context.use().worktree + }, + get project() { + return context.use().project + }, + /** + * Check if a path is within the project boundary. + * Returns true if path is inside Instance.directory OR Instance.worktree. + * Paths within the worktree but outside the working directory should not trigger external_directory permission. + */ + containsPath(filepath: string) { + if (Filesystem.contains(Instance.directory, filepath)) return true + // Non-git projects set worktree to "/" which would match ANY absolute path. + // Skip worktree check in this case to preserve external_directory permissions. + if (Instance.worktree === "/") return false + return Filesystem.contains(Instance.worktree, filepath) + }, + /** + * Captures the current instance ALS context and returns a wrapper that + * restores it when called. Use this for callbacks that fire outside the + * instance async context (native addons, event emitters, timers, etc.). + */ + bind any>(fn: F): F { + const ctx = context.use() + return ((...args: any[]) => context.provide(ctx, () => fn(...args))) as F + }, + state(init: () => S, dispose?: (state: Awaited) => Promise): () => S { + return State.create(() => Instance.directory, init, dispose) + }, + async reload(input: { directory: string; init?: () => Promise; project?: Project.Info; worktree?: string }) { + const directory = Filesystem.resolve(input.directory) + Log.Default.info("reloading instance", { directory }) + await Promise.all([State.dispose(directory), disposeInstance(directory)]) + cache.delete(directory) + const next = track(directory, boot({ ...input, directory })) + emit(directory) + return await next + }, + async dispose() { + const directory = Instance.directory + Log.Default.info("disposing instance", { directory }) + await Promise.all([State.dispose(directory), disposeInstance(directory)]) + cache.delete(directory) + emit(directory) + }, + async disposeAll() { + if (disposal.all) return disposal.all - disposal.all = iife(async () => { - Log.Default.info("disposing all instances"); - const entries = [...cache.entries()]; - for (const [key, value] of entries) { - if (cache.get(key) !== value) continue; + disposal.all = iife(async () => { + Log.Default.info("disposing all instances") + const entries = [...cache.entries()] + for (const [key, value] of entries) { + if (cache.get(key) !== value) continue - const ctx = await value.catch((error) => { - Log.Default.warn("instance dispose failed", { key, error }); - return undefined; - }); + const ctx = await value.catch((error) => { + Log.Default.warn("instance dispose failed", { key, error }) + return undefined + }) - if (!ctx) { - if (cache.get(key) === value) cache.delete(key); - continue; - } + if (!ctx) { + if (cache.get(key) === value) cache.delete(key) + continue + } - if (cache.get(key) !== value) continue; + if (cache.get(key) !== value) continue - await context.provide(ctx, async () => { - await Instance.dispose(); - }); - } - }).finally(() => { - disposal.all = undefined; - }); + await context.provide(ctx, async () => { + await Instance.dispose() + }) + } + }).finally(() => { + disposal.all = undefined + }) - return disposal.all; - }, -}; + return disposal.all + }, +} diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index ccba830b8..a9489451c 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -1,516 +1,381 @@ -import { - NodeChildProcessSpawner, - NodeFileSystem, - NodePath, -} from "@effect/platform-node"; -import { - Cause, - Duration, - Effect, - FileSystem, - Layer, - Schedule, - ServiceMap, - Stream, -} from "effect"; -import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; -import path from "path"; -import z from "zod"; -import { InstanceContext } from "@/effect/instance-context"; -import { runPromiseInstance } from "@/effect/runtime"; -import { Config } from "../config/config"; -import { Global } from "../global"; -import { Log } from "../util/log"; +import { NodeChildProcessSpawner, NodeFileSystem, NodePath } from "@effect/platform-node" +import { Cause, Duration, Effect, FileSystem, Layer, Schedule, ServiceMap, Stream } from "effect" +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" +import path from "path" +import z from "zod" +import { InstanceContext } from "@/effect/instance-context" +import { runPromiseInstance } from "@/effect/runtime" +import { Config } from "../config/config" +import { Global } from "../global" +import { Log } from "../util/log" -const log = Log.create({ service: "snapshot" }); -const PRUNE = "7.days"; +const log = Log.create({ service: "snapshot" }) +const PRUNE = "7.days" // Common git config flags shared across snapshot operations -const GIT_CORE = ["-c", "core.longpaths=true", "-c", "core.symlinks=true"]; -const GIT_CFG = ["-c", "core.autocrlf=false", ...GIT_CORE]; -const GIT_CFG_QUOTE = [...GIT_CFG, "-c", "core.quotepath=false"]; +const GIT_CORE = ["-c", "core.longpaths=true", "-c", "core.symlinks=true"] +const GIT_CFG = ["-c", "core.autocrlf=false", ...GIT_CORE] +const GIT_CFG_QUOTE = [...GIT_CFG, "-c", "core.quotepath=false"] interface GitResult { - readonly code: ChildProcessSpawner.ExitCode; - readonly text: string; - readonly stderr: string; + readonly code: ChildProcessSpawner.ExitCode + readonly text: string + readonly stderr: string } export namespace Snapshot { - export const Patch = z.object({ - hash: z.string(), - files: z.string().array(), - }); - export type Patch = z.infer; + export const Patch = z.object({ + hash: z.string(), + files: z.string().array(), + }) + export type Patch = z.infer - export const FileDiff = z - .object({ - file: z.string(), - before: z.string(), - after: z.string(), - additions: z.number(), - deletions: z.number(), - status: z.enum(["added", "deleted", "modified"]).optional(), - }) - .meta({ - ref: "FileDiff", - }); - export type FileDiff = z.infer; + export const FileDiff = z + .object({ + file: z.string(), + before: z.string(), + after: z.string(), + additions: z.number(), + deletions: z.number(), + status: z.enum(["added", "deleted", "modified"]).optional(), + }) + .meta({ + ref: "FileDiff", + }) + export type FileDiff = z.infer - // Promise facade — existing callers use these - export function init() { - void runPromiseInstance(SnapshotService.use((s) => s.init())); - } + // Promise facade — existing callers use these + export function init() { + void runPromiseInstance(SnapshotService.use((s) => s.init())) + } - export async function cleanup() { - return runPromiseInstance(SnapshotService.use((s) => s.cleanup())); - } + export async function cleanup() { + return runPromiseInstance(SnapshotService.use((s) => s.cleanup())) + } - export async function track() { - return runPromiseInstance(SnapshotService.use((s) => s.track())); - } + export async function track() { + return runPromiseInstance(SnapshotService.use((s) => s.track())) + } - export async function patch(hash: string) { - return runPromiseInstance(SnapshotService.use((s) => s.patch(hash))); - } + export async function patch(hash: string) { + return runPromiseInstance(SnapshotService.use((s) => s.patch(hash))) + } - export async function restore(snapshot: string) { - return runPromiseInstance(SnapshotService.use((s) => s.restore(snapshot))); - } + export async function restore(snapshot: string) { + return runPromiseInstance(SnapshotService.use((s) => s.restore(snapshot))) + } - export async function revert(patches: Patch[]) { - return runPromiseInstance(SnapshotService.use((s) => s.revert(patches))); - } + export async function revert(patches: Patch[]) { + return runPromiseInstance(SnapshotService.use((s) => s.revert(patches))) + } - export async function diff(hash: string) { - return runPromiseInstance(SnapshotService.use((s) => s.diff(hash))); - } + export async function diff(hash: string) { + return runPromiseInstance(SnapshotService.use((s) => s.diff(hash))) + } - export async function diffFull(from: string, to: string) { - return runPromiseInstance(SnapshotService.use((s) => s.diffFull(from, to))); - } + export async function diffFull(from: string, to: string) { + return runPromiseInstance(SnapshotService.use((s) => s.diffFull(from, to))) + } } export namespace SnapshotService { - export interface Service { - readonly init: () => Effect.Effect; - readonly cleanup: () => Effect.Effect; - readonly track: () => Effect.Effect; - readonly patch: (hash: string) => Effect.Effect; - readonly restore: (snapshot: string) => Effect.Effect; - readonly revert: (patches: Snapshot.Patch[]) => Effect.Effect; - readonly diff: (hash: string) => Effect.Effect; - readonly diffFull: ( - from: string, - to: string, - ) => Effect.Effect; - } + export interface Service { + readonly init: () => Effect.Effect + readonly cleanup: () => Effect.Effect + readonly track: () => Effect.Effect + readonly patch: (hash: string) => Effect.Effect + readonly restore: (snapshot: string) => Effect.Effect + readonly revert: (patches: Snapshot.Patch[]) => Effect.Effect + readonly diff: (hash: string) => Effect.Effect + readonly diffFull: (from: string, to: string) => Effect.Effect + } } -export class SnapshotService extends ServiceMap.Service< - SnapshotService, - SnapshotService.Service ->()("@opencode/Snapshot") { - static readonly layer = Layer.effect( - SnapshotService, - Effect.gen(function* () { - const ctx = yield* InstanceContext; - const fileSystem = yield* FileSystem.FileSystem; - const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; - const { directory, worktree, project } = ctx; - const isGit = project.vcs === "git"; - const snapshotGit = path.join(Global.Path.data, "snapshot", project.id); +export class SnapshotService extends ServiceMap.Service()( + "@opencode/Snapshot", +) { + static readonly layer = Layer.effect( + SnapshotService, + Effect.gen(function* () { + const ctx = yield* InstanceContext + const fileSystem = yield* FileSystem.FileSystem + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner + const { directory, worktree, project } = ctx + const isGit = project.vcs === "git" + const snapshotGit = path.join(Global.Path.data, "snapshot", project.id) - const gitArgs = (cmd: string[]) => [ - "--git-dir", - snapshotGit, - "--work-tree", - worktree, - ...cmd, - ]; + const gitArgs = (cmd: string[]) => ["--git-dir", snapshotGit, "--work-tree", worktree, ...cmd] - // Run git with nothrow semantics — always returns a result, never fails - const git = ( - args: string[], - opts?: { cwd?: string; env?: Record }, - ): Effect.Effect => - Effect.gen(function* () { - const command = ChildProcess.make("git", args, { - cwd: opts?.cwd, - env: opts?.env, - extendEnv: true, - }); - const handle = yield* spawner.spawn(command); - const [text, stderr] = yield* Effect.all( - [ - Stream.mkString(Stream.decodeText(handle.stdout)), - Stream.mkString(Stream.decodeText(handle.stderr)), - ], - { concurrency: 2 }, - ); - const code = yield* handle.exitCode; - return { code, text, stderr }; - }).pipe( - Effect.scoped, - Effect.catch((err) => - Effect.succeed({ - code: ChildProcessSpawner.ExitCode(1), - text: "", - stderr: String(err), - }), - ), - ); + // Run git with nothrow semantics — always returns a result, never fails + const git = (args: string[], opts?: { cwd?: string; env?: Record }): Effect.Effect => + Effect.gen(function* () { + const command = ChildProcess.make("git", args, { + cwd: opts?.cwd, + env: opts?.env, + extendEnv: true, + }) + const handle = yield* spawner.spawn(command) + const [text, stderr] = yield* Effect.all( + [Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))], + { concurrency: 2 }, + ) + const code = yield* handle.exitCode + return { code, text, stderr } + }).pipe( + Effect.scoped, + Effect.catch((err) => + Effect.succeed({ + code: ChildProcessSpawner.ExitCode(1), + text: "", + stderr: String(err), + }), + ), + ) - // FileSystem helpers — orDie converts PlatformError to defects - const exists = (p: string) => fileSystem.exists(p).pipe(Effect.orDie); - const mkdir = (p: string) => - fileSystem.makeDirectory(p, { recursive: true }).pipe(Effect.orDie); - const writeFile = (p: string, content: string) => - fileSystem.writeFileString(p, content).pipe(Effect.orDie); - const readFile = (p: string) => - fileSystem - .readFileString(p) - .pipe(Effect.catch(() => Effect.succeed(""))); - const removeFile = (p: string) => - fileSystem.remove(p).pipe(Effect.catch(() => Effect.void)); + // FileSystem helpers — orDie converts PlatformError to defects + const exists = (p: string) => fileSystem.exists(p).pipe(Effect.orDie) + const mkdir = (p: string) => fileSystem.makeDirectory(p, { recursive: true }).pipe(Effect.orDie) + const writeFile = (p: string, content: string) => fileSystem.writeFileString(p, content).pipe(Effect.orDie) + const readFile = (p: string) => fileSystem.readFileString(p).pipe(Effect.catch(() => Effect.succeed(""))) + const removeFile = (p: string) => fileSystem.remove(p).pipe(Effect.catch(() => Effect.void)) - // --- internal Effect helpers --- + // --- internal Effect helpers --- - const isEnabled = Effect.gen(function* () { - if (!isGit) return false; - const cfg = yield* Effect.promise(() => Config.get()); - return cfg.snapshot !== false; - }); + const isEnabled = Effect.gen(function* () { + if (!isGit) return false + const cfg = yield* Effect.promise(() => Config.get()) + return cfg.snapshot !== false + }) - const excludesPath = Effect.gen(function* () { - const result = yield* git( - ["rev-parse", "--path-format=absolute", "--git-path", "info/exclude"], - { - cwd: worktree, - }, - ); - const file = result.text.trim(); - if (!file) return undefined; - if (!(yield* exists(file))) return undefined; - return file; - }); + const excludesPath = Effect.gen(function* () { + const result = yield* git(["rev-parse", "--path-format=absolute", "--git-path", "info/exclude"], { + cwd: worktree, + }) + const file = result.text.trim() + if (!file) return undefined + if (!(yield* exists(file))) return undefined + return file + }) - const syncExclude = Effect.gen(function* () { - const file = yield* excludesPath; - const target = path.join(snapshotGit, "info", "exclude"); - yield* mkdir(path.join(snapshotGit, "info")); - if (!file) { - yield* writeFile(target, ""); - return; - } - const text = yield* readFile(file); - yield* writeFile(target, text); - }); + const syncExclude = Effect.gen(function* () { + const file = yield* excludesPath + const target = path.join(snapshotGit, "info", "exclude") + yield* mkdir(path.join(snapshotGit, "info")) + if (!file) { + yield* writeFile(target, "") + return + } + const text = yield* readFile(file) + yield* writeFile(target, text) + }) - const add = Effect.gen(function* () { - yield* syncExclude; - yield* git([...GIT_CFG, ...gitArgs(["add", "."])], { cwd: directory }); - }); + const add = Effect.gen(function* () { + yield* syncExclude + yield* git([...GIT_CFG, ...gitArgs(["add", "."])], { cwd: directory }) + }) - // --- service methods --- + // --- service methods --- - const cleanup = Effect.fn("SnapshotService.cleanup")(function* () { - if (!(yield* isEnabled)) return; - if (!(yield* exists(snapshotGit))) return; - const result = yield* git(gitArgs(["gc", `--prune=${PRUNE}`]), { - cwd: directory, - }); - if (result.code !== 0) { - log.warn("cleanup failed", { - exitCode: result.code, - stderr: result.stderr, - }); - return; - } - log.info("cleanup", { prune: PRUNE }); - }); + const cleanup = Effect.fn("SnapshotService.cleanup")(function* () { + if (!(yield* isEnabled)) return + if (!(yield* exists(snapshotGit))) return + const result = yield* git(gitArgs(["gc", `--prune=${PRUNE}`]), { + cwd: directory, + }) + if (result.code !== 0) { + log.warn("cleanup failed", { + exitCode: result.code, + stderr: result.stderr, + }) + return + } + log.info("cleanup", { prune: PRUNE }) + }) - const track = Effect.fn("SnapshotService.track")(function* () { - if (!(yield* isEnabled)) return undefined; - const existed = yield* exists(snapshotGit); - yield* mkdir(snapshotGit); - if (!existed) { - yield* git(["init"], { - env: { GIT_DIR: snapshotGit, GIT_WORK_TREE: worktree }, - }); - yield* git([ - "--git-dir", - snapshotGit, - "config", - "core.autocrlf", - "false", - ]); - yield* git([ - "--git-dir", - snapshotGit, - "config", - "core.longpaths", - "true", - ]); - yield* git([ - "--git-dir", - snapshotGit, - "config", - "core.symlinks", - "true", - ]); - yield* git([ - "--git-dir", - snapshotGit, - "config", - "core.fsmonitor", - "false", - ]); - log.info("initialized"); - } - yield* add; - const result = yield* git(gitArgs(["write-tree"]), { cwd: directory }); - const hash = result.text.trim(); - log.info("tracking", { hash, cwd: directory, git: snapshotGit }); - return hash; - }); + const track = Effect.fn("SnapshotService.track")(function* () { + if (!(yield* isEnabled)) return undefined + const existed = yield* exists(snapshotGit) + yield* mkdir(snapshotGit) + if (!existed) { + yield* git(["init"], { + env: { GIT_DIR: snapshotGit, GIT_WORK_TREE: worktree }, + }) + yield* git(["--git-dir", snapshotGit, "config", "core.autocrlf", "false"]) + yield* git(["--git-dir", snapshotGit, "config", "core.longpaths", "true"]) + yield* git(["--git-dir", snapshotGit, "config", "core.symlinks", "true"]) + yield* git(["--git-dir", snapshotGit, "config", "core.fsmonitor", "false"]) + log.info("initialized") + } + yield* add + const result = yield* git(gitArgs(["write-tree"]), { cwd: directory }) + const hash = result.text.trim() + log.info("tracking", { hash, cwd: directory, git: snapshotGit }) + return hash + }) - const patch = Effect.fn("SnapshotService.patch")(function* ( - hash: string, - ) { - yield* add; - const result = yield* git( - [ - ...GIT_CFG_QUOTE, - ...gitArgs([ - "diff", - "--no-ext-diff", - "--name-only", - hash, - "--", - ".", - ]), - ], - { cwd: directory }, - ); + const patch = Effect.fn("SnapshotService.patch")(function* (hash: string) { + yield* add + const result = yield* git( + [...GIT_CFG_QUOTE, ...gitArgs(["diff", "--no-ext-diff", "--name-only", hash, "--", "."])], + { cwd: directory }, + ) - if (result.code !== 0) { - log.warn("failed to get diff", { hash, exitCode: result.code }); - return { hash, files: [] } as Snapshot.Patch; - } + if (result.code !== 0) { + log.warn("failed to get diff", { hash, exitCode: result.code }) + return { hash, files: [] } as Snapshot.Patch + } - return { - hash, - files: result.text - .trim() - .split("\n") - .map((x: string) => x.trim()) - .filter(Boolean) - .map((x: string) => path.join(worktree, x).replaceAll("\\", "/")), - } as Snapshot.Patch; - }); + return { + hash, + files: result.text + .trim() + .split("\n") + .map((x: string) => x.trim()) + .filter(Boolean) + .map((x: string) => path.join(worktree, x).replaceAll("\\", "/")), + } as Snapshot.Patch + }) - const restore = Effect.fn("SnapshotService.restore")(function* ( - snapshot: string, - ) { - log.info("restore", { commit: snapshot }); - const result = yield* git( - [...GIT_CORE, ...gitArgs(["read-tree", snapshot])], - { cwd: worktree }, - ); - if (result.code === 0) { - const checkout = yield* git( - [...GIT_CORE, ...gitArgs(["checkout-index", "-a", "-f"])], - { cwd: worktree }, - ); - if (checkout.code === 0) return; - log.error("failed to restore snapshot", { - snapshot, - exitCode: checkout.code, - stderr: checkout.stderr, - }); - return; - } - log.error("failed to restore snapshot", { - snapshot, - exitCode: result.code, - stderr: result.stderr, - }); - }); + const restore = Effect.fn("SnapshotService.restore")(function* (snapshot: string) { + log.info("restore", { commit: snapshot }) + const result = yield* git([...GIT_CORE, ...gitArgs(["read-tree", snapshot])], { cwd: worktree }) + if (result.code === 0) { + const checkout = yield* git([...GIT_CORE, ...gitArgs(["checkout-index", "-a", "-f"])], { cwd: worktree }) + if (checkout.code === 0) return + log.error("failed to restore snapshot", { + snapshot, + exitCode: checkout.code, + stderr: checkout.stderr, + }) + return + } + log.error("failed to restore snapshot", { + snapshot, + exitCode: result.code, + stderr: result.stderr, + }) + }) - const revert = Effect.fn("SnapshotService.revert")(function* ( - patches: Snapshot.Patch[], - ) { - const seen = new Set(); - for (const item of patches) { - for (const file of item.files) { - if (seen.has(file)) continue; - log.info("reverting", { file, hash: item.hash }); - const result = yield* git( - [...GIT_CORE, ...gitArgs(["checkout", item.hash, "--", file])], - { - cwd: worktree, - }, - ); - if (result.code !== 0) { - const relativePath = path.relative(worktree, file); - const checkTree = yield* git( - [ - ...GIT_CORE, - ...gitArgs(["ls-tree", item.hash, "--", relativePath]), - ], - { - cwd: worktree, - }, - ); - if (checkTree.code === 0 && checkTree.text.trim()) { - log.info( - "file existed in snapshot but checkout failed, keeping", - { file }, - ); - } else { - log.info("file did not exist in snapshot, deleting", { file }); - yield* removeFile(file); - } - } - seen.add(file); - } - } - }); + const revert = Effect.fn("SnapshotService.revert")(function* (patches: Snapshot.Patch[]) { + const seen = new Set() + for (const item of patches) { + for (const file of item.files) { + if (seen.has(file)) continue + log.info("reverting", { file, hash: item.hash }) + const result = yield* git([...GIT_CORE, ...gitArgs(["checkout", item.hash, "--", file])], { + cwd: worktree, + }) + if (result.code !== 0) { + const relativePath = path.relative(worktree, file) + const checkTree = yield* git([...GIT_CORE, ...gitArgs(["ls-tree", item.hash, "--", relativePath])], { + cwd: worktree, + }) + if (checkTree.code === 0 && checkTree.text.trim()) { + log.info("file existed in snapshot but checkout failed, keeping", { file }) + } else { + log.info("file did not exist in snapshot, deleting", { file }) + yield* removeFile(file) + } + } + seen.add(file) + } + } + }) - const diff = Effect.fn("SnapshotService.diff")(function* (hash: string) { - yield* add; - const result = yield* git( - [ - ...GIT_CFG_QUOTE, - ...gitArgs(["diff", "--no-ext-diff", hash, "--", "."]), - ], - { - cwd: worktree, - }, - ); + const diff = Effect.fn("SnapshotService.diff")(function* (hash: string) { + yield* add + const result = yield* git([...GIT_CFG_QUOTE, ...gitArgs(["diff", "--no-ext-diff", hash, "--", "."])], { + cwd: worktree, + }) - if (result.code !== 0) { - log.warn("failed to get diff", { - hash, - exitCode: result.code, - stderr: result.stderr, - }); - return ""; - } + if (result.code !== 0) { + log.warn("failed to get diff", { + hash, + exitCode: result.code, + stderr: result.stderr, + }) + return "" + } - return result.text.trim(); - }); + return result.text.trim() + }) - const diffFull = Effect.fn("SnapshotService.diffFull")(function* ( - from: string, - to: string, - ) { - const result: Snapshot.FileDiff[] = []; - const status = new Map(); + const diffFull = Effect.fn("SnapshotService.diffFull")(function* (from: string, to: string) { + const result: Snapshot.FileDiff[] = [] + const status = new Map() - const statuses = yield* git( - [ - ...GIT_CFG_QUOTE, - ...gitArgs([ - "diff", - "--no-ext-diff", - "--name-status", - "--no-renames", - from, - to, - "--", - ".", - ]), - ], - { cwd: directory }, - ); + const statuses = yield* git( + [ + ...GIT_CFG_QUOTE, + ...gitArgs(["diff", "--no-ext-diff", "--name-status", "--no-renames", from, to, "--", "."]), + ], + { cwd: directory }, + ) - for (const line of statuses.text.trim().split("\n")) { - if (!line) continue; - const [code, file] = line.split("\t"); - if (!code || !file) continue; - const kind = code.startsWith("A") - ? "added" - : code.startsWith("D") - ? "deleted" - : "modified"; - status.set(file, kind); - } + for (const line of statuses.text.trim().split("\n")) { + if (!line) continue + const [code, file] = line.split("\t") + if (!code || !file) continue + const kind = code.startsWith("A") ? "added" : code.startsWith("D") ? "deleted" : "modified" + status.set(file, kind) + } - const numstat = yield* git( - [ - ...GIT_CFG_QUOTE, - ...gitArgs([ - "diff", - "--no-ext-diff", - "--no-renames", - "--numstat", - from, - to, - "--", - ".", - ]), - ], - { cwd: directory }, - ); + const numstat = yield* git( + [...GIT_CFG_QUOTE, ...gitArgs(["diff", "--no-ext-diff", "--no-renames", "--numstat", from, to, "--", "."])], + { cwd: directory }, + ) - for (const line of numstat.text.trim().split("\n")) { - if (!line) continue; - const [additions, deletions, file] = line.split("\t"); - const isBinaryFile = additions === "-" && deletions === "-"; - const [before, after] = isBinaryFile - ? ["", ""] - : yield* Effect.all( - [ - git([ - ...GIT_CFG, - ...gitArgs(["show", `${from}:${file}`]), - ]).pipe(Effect.map((r) => r.text)), - git([...GIT_CFG, ...gitArgs(["show", `${to}:${file}`])]).pipe( - Effect.map((r) => r.text), - ), - ], - { concurrency: 2 }, - ); - const added = isBinaryFile ? 0 : parseInt(additions!); - const deleted = isBinaryFile ? 0 : parseInt(deletions!); - result.push({ - file: file!, - before, - after, - additions: Number.isFinite(added) ? added : 0, - deletions: Number.isFinite(deleted) ? deleted : 0, - status: status.get(file!) ?? "modified", - }); - } - return result; - }); + for (const line of numstat.text.trim().split("\n")) { + if (!line) continue + const [additions, deletions, file] = line.split("\t") + const isBinaryFile = additions === "-" && deletions === "-" + const [before, after] = isBinaryFile + ? ["", ""] + : yield* Effect.all( + [ + git([...GIT_CFG, ...gitArgs(["show", `${from}:${file}`])]).pipe(Effect.map((r) => r.text)), + git([...GIT_CFG, ...gitArgs(["show", `${to}:${file}`])]).pipe(Effect.map((r) => r.text)), + ], + { concurrency: 2 }, + ) + const added = isBinaryFile ? 0 : parseInt(additions!) + const deleted = isBinaryFile ? 0 : parseInt(deletions!) + result.push({ + file: file!, + before, + after, + additions: Number.isFinite(added) ? added : 0, + deletions: Number.isFinite(deleted) ? deleted : 0, + status: status.get(file!) ?? "modified", + }) + } + return result + }) - // Start hourly cleanup fiber — scoped to instance lifetime - yield* cleanup().pipe( - Effect.catchCause((cause) => { - log.error("cleanup loop failed", { cause: Cause.pretty(cause) }); - return Effect.void; - }), - Effect.repeat(Schedule.spaced(Duration.hours(1))), - Effect.forkScoped, - ); + // Start hourly cleanup fiber — scoped to instance lifetime + yield* cleanup().pipe( + Effect.catchCause((cause) => { + log.error("cleanup loop failed", { cause: Cause.pretty(cause) }) + return Effect.void + }), + Effect.repeat(Schedule.spaced(Duration.hours(1))), + Effect.forkScoped, + ) - return SnapshotService.of({ - init: Effect.fn("SnapshotService.init")(function* () {}), - cleanup, - track, - patch, - restore, - revert, - diff, - diffFull, - }); - }), - ).pipe( - Layer.provide(NodeChildProcessSpawner.layer), - Layer.provide(NodeFileSystem.layer), - Layer.provide(NodePath.layer), - ); + return SnapshotService.of({ + init: Effect.fn("SnapshotService.init")(function* () {}), + cleanup, + track, + patch, + restore, + revert, + diff, + diffFull, + }) + }), + ).pipe( + Layer.provide(NodeChildProcessSpawner.layer), + Layer.provide(NodeFileSystem.layer), + Layer.provide(NodePath.layer), + ) } diff --git a/packages/opencode/test/fixture/instance.ts b/packages/opencode/test/fixture/instance.ts index 1a7096b63..ce880d70d 100644 --- a/packages/opencode/test/fixture/instance.ts +++ b/packages/opencode/test/fixture/instance.ts @@ -1,14 +1,14 @@ -import { ConfigProvider, Layer, ManagedRuntime } from "effect"; -import { InstanceContext } from "../../src/effect/instance-context"; -import { Instance } from "../../src/project/instance"; +import { ConfigProvider, Layer, ManagedRuntime } from "effect" +import { InstanceContext } from "../../src/effect/instance-context" +import { Instance } from "../../src/project/instance" /** ConfigProvider that enables the experimental file watcher. */ export const watcherConfigLayer = ConfigProvider.layer( - ConfigProvider.fromUnknown({ - OPENCODE_EXPERIMENTAL_FILEWATCHER: "true", - OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "false", - }), -); + ConfigProvider.fromUnknown({ + OPENCODE_EXPERIMENTAL_FILEWATCHER: "true", + OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "false", + }), +) /** * Boot an Instance with the given service layers and run `body` with @@ -19,35 +19,33 @@ export const watcherConfigLayer = ConfigProvider.layer( * Pass extra layers via `options.provide` (e.g. ConfigProvider.layer). */ export function withServices( - directory: string, - layer: Layer.Layer, - body: (rt: ManagedRuntime.ManagedRuntime) => Promise, - options?: { provide?: Layer.Layer[] }, + directory: string, + layer: Layer.Layer, + body: (rt: ManagedRuntime.ManagedRuntime) => Promise, + options?: { provide?: Layer.Layer[] }, ) { - return Instance.provide({ - directory, - fn: async () => { - const ctx = Layer.sync(InstanceContext, () => - InstanceContext.of({ - directory: Instance.directory, - worktree: Instance.worktree, - project: Instance.project, - }), - ); - let resolved: Layer.Layer = Layer.fresh(layer).pipe( - Layer.provide(ctx), - ) as any; - if (options?.provide) { - for (const l of options.provide) { - resolved = resolved.pipe(Layer.provide(l)) as any; - } - } - const rt = ManagedRuntime.make(resolved); - try { - await body(rt); - } finally { - await rt.dispose(); - } - }, - }); + return Instance.provide({ + directory, + fn: async () => { + const ctx = Layer.sync(InstanceContext, () => + InstanceContext.of({ + directory: Instance.directory, + worktree: Instance.worktree, + project: Instance.project, + }), + ) + let resolved: Layer.Layer = Layer.fresh(layer).pipe(Layer.provide(ctx)) as any + if (options?.provide) { + for (const l of options.provide) { + resolved = resolved.pipe(Layer.provide(l)) as any + } + } + const rt = ManagedRuntime.make(resolved) + try { + await body(rt) + } finally { + await rt.dispose() + } + }, + }) } diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index fd80a51a2..9c5ca274e 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -47,6 +47,13 @@ export type EventProjectUpdated = { properties: Project } +export type EventFileEdited = { + type: "file.edited" + properties: { + file: string + } +} + export type EventServerInstanceDisposed = { type: "server.instance.disposed" properties: { @@ -54,6 +61,50 @@ export type EventServerInstanceDisposed = { } } +export type EventFileWatcherUpdated = { + type: "file.watcher.updated" + properties: { + file: string + event: "add" | "change" | "unlink" + } +} + +export type PermissionRequest = { + id: string + sessionID: string + permission: string + patterns: Array + metadata: { + [key: string]: unknown + } + always: Array + tool?: { + messageID: string + callID: string + } +} + +export type EventPermissionAsked = { + type: "permission.asked" + properties: PermissionRequest +} + +export type EventPermissionReplied = { + type: "permission.replied" + properties: { + sessionID: string + requestID: string + reply: "once" | "always" | "reject" + } +} + +export type EventVcsBranchUpdated = { + type: "vcs.branch.updated" + properties: { + branch?: string + } +} + export type QuestionOption = { /** * Display text (1-5 words, concise) @@ -125,57 +176,6 @@ export type EventQuestionRejected = { } } -export type PermissionRequest = { - id: string - sessionID: string - permission: string - patterns: Array - metadata: { - [key: string]: unknown - } - always: Array - tool?: { - messageID: string - callID: string - } -} - -export type EventPermissionAsked = { - type: "permission.asked" - properties: PermissionRequest -} - -export type EventPermissionReplied = { - type: "permission.replied" - properties: { - sessionID: string - requestID: string - reply: "once" | "always" | "reject" - } -} - -export type EventFileWatcherUpdated = { - type: "file.watcher.updated" - properties: { - file: string - event: "add" | "change" | "unlink" - } -} - -export type EventVcsBranchUpdated = { - type: "vcs.branch.updated" - properties: { - branch?: string - } -} - -export type EventFileEdited = { - type: "file.edited" - properties: { - file: string - } -} - export type EventServerConnected = { type: "server.connected" properties: { @@ -961,15 +961,15 @@ export type Event = | EventInstallationUpdated | EventInstallationUpdateAvailable | EventProjectUpdated + | EventFileEdited | EventServerInstanceDisposed + | EventFileWatcherUpdated + | EventPermissionAsked + | EventPermissionReplied + | EventVcsBranchUpdated | EventQuestionAsked | EventQuestionReplied | EventQuestionRejected - | EventPermissionAsked - | EventPermissionReplied - | EventFileWatcherUpdated - | EventVcsBranchUpdated - | EventFileEdited | EventServerConnected | EventGlobalDisposed | EventLspClientDiagnostics diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 2f7e9952e..c6d79b11e 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -7043,6 +7043,25 @@ }, "required": ["type", "properties"] }, + "Event.file.edited": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "file.edited" + }, + "properties": { + "type": "object", + "properties": { + "file": { + "type": "string" + } + }, + "required": ["file"] + } + }, + "required": ["type", "properties"] + }, "Event.server.instance.disposed": { "type": "object", "properties": { @@ -7062,6 +7081,149 @@ }, "required": ["type", "properties"] }, + "Event.file.watcher.updated": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "file.watcher.updated" + }, + "properties": { + "type": "object", + "properties": { + "file": { + "type": "string" + }, + "event": { + "anyOf": [ + { + "type": "string", + "const": "add" + }, + { + "type": "string", + "const": "change" + }, + { + "type": "string", + "const": "unlink" + } + ] + } + }, + "required": ["file", "event"] + } + }, + "required": ["type", "properties"] + }, + "PermissionRequest": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^per.*" + }, + "sessionID": { + "type": "string", + "pattern": "^ses.*" + }, + "permission": { + "type": "string" + }, + "patterns": { + "type": "array", + "items": { + "type": "string" + } + }, + "metadata": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "always": { + "type": "array", + "items": { + "type": "string" + } + }, + "tool": { + "type": "object", + "properties": { + "messageID": { + "type": "string", + "pattern": "^msg.*" + }, + "callID": { + "type": "string" + } + }, + "required": ["messageID", "callID"] + } + }, + "required": ["id", "sessionID", "permission", "patterns", "metadata", "always"] + }, + "Event.permission.asked": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "permission.asked" + }, + "properties": { + "$ref": "#/components/schemas/PermissionRequest" + } + }, + "required": ["type", "properties"] + }, + "Event.permission.replied": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "permission.replied" + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "pattern": "^ses.*" + }, + "requestID": { + "type": "string", + "pattern": "^per.*" + }, + "reply": { + "type": "string", + "enum": ["once", "always", "reject"] + } + }, + "required": ["sessionID", "requestID", "reply"] + } + }, + "required": ["type", "properties"] + }, + "Event.vcs.branch.updated": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "vcs.branch.updated" + }, + "properties": { + "type": "object", + "properties": { + "branch": { + "type": "string" + } + } + } + }, + "required": ["type", "properties"] + }, "QuestionOption": { "type": "object", "properties": { @@ -7212,168 +7374,6 @@ }, "required": ["type", "properties"] }, - "PermissionRequest": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^per.*" - }, - "sessionID": { - "type": "string", - "pattern": "^ses.*" - }, - "permission": { - "type": "string" - }, - "patterns": { - "type": "array", - "items": { - "type": "string" - } - }, - "metadata": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "always": { - "type": "array", - "items": { - "type": "string" - } - }, - "tool": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "pattern": "^msg.*" - }, - "callID": { - "type": "string" - } - }, - "required": ["messageID", "callID"] - } - }, - "required": ["id", "sessionID", "permission", "patterns", "metadata", "always"] - }, - "Event.permission.asked": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "permission.asked" - }, - "properties": { - "$ref": "#/components/schemas/PermissionRequest" - } - }, - "required": ["type", "properties"] - }, - "Event.permission.replied": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "permission.replied" - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses.*" - }, - "requestID": { - "type": "string", - "pattern": "^per.*" - }, - "reply": { - "type": "string", - "enum": ["once", "always", "reject"] - } - }, - "required": ["sessionID", "requestID", "reply"] - } - }, - "required": ["type", "properties"] - }, - "Event.file.watcher.updated": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "file.watcher.updated" - }, - "properties": { - "type": "object", - "properties": { - "file": { - "type": "string" - }, - "event": { - "anyOf": [ - { - "type": "string", - "const": "add" - }, - { - "type": "string", - "const": "change" - }, - { - "type": "string", - "const": "unlink" - } - ] - } - }, - "required": ["file", "event"] - } - }, - "required": ["type", "properties"] - }, - "Event.vcs.branch.updated": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "vcs.branch.updated" - }, - "properties": { - "type": "object", - "properties": { - "branch": { - "type": "string" - } - } - } - }, - "required": ["type", "properties"] - }, - "Event.file.edited": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "file.edited" - }, - "properties": { - "type": "object", - "properties": { - "file": { - "type": "string" - } - }, - "required": ["file"] - } - }, - "required": ["type", "properties"] - }, "Event.server.connected": { "type": "object", "properties": { @@ -9608,9 +9608,24 @@ { "$ref": "#/components/schemas/Event.project.updated" }, + { + "$ref": "#/components/schemas/Event.file.edited" + }, { "$ref": "#/components/schemas/Event.server.instance.disposed" }, + { + "$ref": "#/components/schemas/Event.file.watcher.updated" + }, + { + "$ref": "#/components/schemas/Event.permission.asked" + }, + { + "$ref": "#/components/schemas/Event.permission.replied" + }, + { + "$ref": "#/components/schemas/Event.vcs.branch.updated" + }, { "$ref": "#/components/schemas/Event.question.asked" }, @@ -9620,21 +9635,6 @@ { "$ref": "#/components/schemas/Event.question.rejected" }, - { - "$ref": "#/components/schemas/Event.permission.asked" - }, - { - "$ref": "#/components/schemas/Event.permission.replied" - }, - { - "$ref": "#/components/schemas/Event.file.watcher.updated" - }, - { - "$ref": "#/components/schemas/Event.vcs.branch.updated" - }, - { - "$ref": "#/components/schemas/Event.file.edited" - }, { "$ref": "#/components/schemas/Event.server.connected" },