chore: generate

This commit is contained in:
opencode-agent[bot]
2026-03-18 01:05:16 +00:00
parent 9e7c136de7
commit bc949af623
7 changed files with 816 additions and 982 deletions

View File

@@ -1,15 +1,14 @@
import { ServiceMap } from "effect"; import { ServiceMap } from "effect"
import type { Project } from "@/project/project"; import type { Project } from "@/project/project"
export declare namespace InstanceContext { export declare namespace InstanceContext {
export interface Shape { export interface Shape {
readonly directory: string; readonly directory: string
readonly worktree: string; readonly worktree: string
readonly project: Project.Info; readonly project: Project.Info
} }
} }
export class InstanceContext extends ServiceMap.Service< export class InstanceContext extends ServiceMap.Service<InstanceContext, InstanceContext.Shape>()(
InstanceContext, "opencode/InstanceContext",
InstanceContext.Shape ) {}
>()("opencode/InstanceContext") {}

View File

@@ -1,31 +1,31 @@
import { Effect, Layer, LayerMap, ServiceMap } from "effect"; import { Effect, Layer, LayerMap, ServiceMap } from "effect"
import { FileService } from "@/file"; import { FileService } from "@/file"
import { FileTimeService } from "@/file/time"; import { FileTimeService } from "@/file/time"
import { FileWatcherService } from "@/file/watcher"; import { FileWatcherService } from "@/file/watcher"
import { FormatService } from "@/format"; import { FormatService } from "@/format"
import { PermissionService } from "@/permission/service"; import { PermissionService } from "@/permission/service"
import { Instance } from "@/project/instance"; import { Instance } from "@/project/instance"
import { VcsService } from "@/project/vcs"; import { VcsService } from "@/project/vcs"
import { ProviderAuthService } from "@/provider/auth-service"; import { ProviderAuthService } from "@/provider/auth-service"
import { QuestionService } from "@/question/service"; import { QuestionService } from "@/question/service"
import { SkillService } from "@/skill/skill"; import { SkillService } from "@/skill/skill"
import { SnapshotService } from "@/snapshot"; import { SnapshotService } from "@/snapshot"
import { InstanceContext } from "./instance-context"; import { InstanceContext } from "./instance-context"
import { registerDisposer } from "./instance-registry"; import { registerDisposer } from "./instance-registry"
export { InstanceContext } from "./instance-context"; export { InstanceContext } from "./instance-context"
export type InstanceServices = export type InstanceServices =
| QuestionService | QuestionService
| PermissionService | PermissionService
| ProviderAuthService | ProviderAuthService
| FileWatcherService | FileWatcherService
| VcsService | VcsService
| FileTimeService | FileTimeService
| FormatService | FormatService
| FileService | FileService
| SkillService | SkillService
| SnapshotService; | SnapshotService
// NOTE: LayerMap only passes the key (directory string) to lookup, but we need // NOTE: LayerMap only passes the key (directory string) to lookup, but we need
// the full instance context (directory, worktree, project). We read from the // 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 // This should go away once the old Instance type is removed and lookup can load
// the full context directly. // the full context directly.
function lookup(_key: string) { function lookup(_key: string) {
const ctx = Layer.sync(InstanceContext, () => const ctx = Layer.sync(InstanceContext, () => InstanceContext.of(Instance.current))
InstanceContext.of(Instance.current), return Layer.mergeAll(
); Layer.fresh(QuestionService.layer),
return Layer.mergeAll( Layer.fresh(PermissionService.layer),
Layer.fresh(QuestionService.layer), Layer.fresh(ProviderAuthService.layer),
Layer.fresh(PermissionService.layer), Layer.fresh(FileWatcherService.layer).pipe(Layer.orDie),
Layer.fresh(ProviderAuthService.layer), Layer.fresh(VcsService.layer),
Layer.fresh(FileWatcherService.layer).pipe(Layer.orDie), Layer.fresh(FileTimeService.layer).pipe(Layer.orDie),
Layer.fresh(VcsService.layer), Layer.fresh(FormatService.layer),
Layer.fresh(FileTimeService.layer).pipe(Layer.orDie), Layer.fresh(FileService.layer),
Layer.fresh(FormatService.layer), Layer.fresh(SkillService.layer),
Layer.fresh(FileService.layer), Layer.fresh(SnapshotService.layer),
Layer.fresh(SkillService.layer), ).pipe(Layer.provide(ctx))
Layer.fresh(SnapshotService.layer),
).pipe(Layer.provide(ctx));
} }
export class Instances extends ServiceMap.Service< export class Instances extends ServiceMap.Service<Instances, LayerMap.LayerMap<string, InstanceServices>>()(
Instances, "opencode/Instances",
LayerMap.LayerMap<string, InstanceServices> ) {
>()("opencode/Instances") { static readonly layer = Layer.effect(
static readonly layer = Layer.effect( Instances,
Instances, Effect.gen(function* () {
Effect.gen(function* () { const layerMap = yield* LayerMap.make(lookup, {
const layerMap = yield* LayerMap.make(lookup, { idleTimeToLive: Infinity,
idleTimeToLive: Infinity, })
}); const unregister = registerDisposer((directory) => Effect.runPromise(layerMap.invalidate(directory)))
const unregister = registerDisposer((directory) => yield* Effect.addFinalizer(() => Effect.sync(unregister))
Effect.runPromise(layerMap.invalidate(directory)), return Instances.of(layerMap)
); }),
yield* Effect.addFinalizer(() => Effect.sync(unregister)); )
return Instances.of(layerMap);
}),
);
static get( static get(directory: string): Layer.Layer<InstanceServices, never, Instances> {
directory: string, return Layer.unwrap(Instances.use((map) => Effect.succeed(map.get(directory))))
): Layer.Layer<InstanceServices, never, Instances> { }
return Layer.unwrap(
Instances.use((map) => Effect.succeed(map.get(directory))),
);
}
static invalidate(directory: string): Effect.Effect<void, never, Instances> { static invalidate(directory: string): Effect.Effect<void, never, Instances> {
return Instances.use((map) => map.invalidate(directory)); return Instances.use((map) => map.invalidate(directory))
} }
} }

View File

@@ -1,185 +1,166 @@
import { GlobalBus } from "@/bus/global"; import { GlobalBus } from "@/bus/global"
import { disposeInstance } from "@/effect/instance-registry"; import { disposeInstance } from "@/effect/instance-registry"
import { Filesystem } from "@/util/filesystem"; import { Filesystem } from "@/util/filesystem"
import { iife } from "@/util/iife"; import { iife } from "@/util/iife"
import { Log } from "@/util/log"; import { Log } from "@/util/log"
import { Context } from "../util/context"; import { Context } from "../util/context"
import { Project } from "./project"; import { Project } from "./project"
import { State } from "./state"; import { State } from "./state"
interface Context { interface Context {
directory: string; directory: string
worktree: string; worktree: string
project: Project.Info; project: Project.Info
} }
const context = Context.create<Context>("instance"); const context = Context.create<Context>("instance")
const cache = new Map<string, Promise<Context>>(); const cache = new Map<string, Promise<Context>>()
const disposal = { const disposal = {
all: undefined as Promise<void> | undefined, all: undefined as Promise<void> | undefined,
};
function emit(directory: string) {
GlobalBus.emit("event", {
directory,
payload: {
type: "server.instance.disposed",
properties: {
directory,
},
},
});
} }
function boot(input: { function emit(directory: string) {
directory: string; GlobalBus.emit("event", {
init?: () => Promise<any>; directory,
project?: Project.Info; payload: {
worktree?: string; type: "server.instance.disposed",
}) { properties: {
return iife(async () => { directory,
const ctx = },
input.project && input.worktree },
? { })
directory: input.directory, }
worktree: input.worktree,
project: input.project, function boot(input: { directory: string; init?: () => Promise<any>; project?: Project.Info; worktree?: string }) {
} return iife(async () => {
: await Project.fromDirectory(input.directory).then( const ctx =
({ project, sandbox }) => ({ input.project && input.worktree
directory: input.directory, ? {
worktree: sandbox, directory: input.directory,
project, worktree: input.worktree,
}), project: input.project,
); }
await context.provide(ctx, async () => { : await Project.fromDirectory(input.directory).then(({ project, sandbox }) => ({
await input.init?.(); directory: input.directory,
}); worktree: sandbox,
return ctx; project,
}); }))
await context.provide(ctx, async () => {
await input.init?.()
})
return ctx
})
} }
function track(directory: string, next: Promise<Context>) { function track(directory: string, next: Promise<Context>) {
const task = next.catch((error) => { const task = next.catch((error) => {
if (cache.get(directory) === task) cache.delete(directory); if (cache.get(directory) === task) cache.delete(directory)
throw error; throw error
}); })
cache.set(directory, task); cache.set(directory, task)
return task; return task
} }
export const Instance = { export const Instance = {
async provide<R>(input: { async provide<R>(input: { directory: string; init?: () => Promise<any>; fn: () => R }): Promise<R> {
directory: string; const directory = Filesystem.resolve(input.directory)
init?: () => Promise<any>; let existing = cache.get(directory)
fn: () => R; if (!existing) {
}): Promise<R> { Log.Default.info("creating instance", { directory })
const directory = Filesystem.resolve(input.directory); existing = track(
let existing = cache.get(directory); directory,
if (!existing) { boot({
Log.Default.info("creating instance", { directory }); directory,
existing = track( init: input.init,
directory, }),
boot({ )
directory, }
init: input.init, const ctx = await existing
}), return context.provide(ctx, async () => {
); return input.fn()
} })
const ctx = await existing; },
return context.provide(ctx, async () => { get current() {
return input.fn(); return context.use()
}); },
}, get directory() {
get current() { return context.use().directory
return context.use(); },
}, get worktree() {
get directory() { return context.use().worktree
return context.use().directory; },
}, get project() {
get worktree() { return context.use().project
return context.use().worktree; },
}, /**
get project() { * Check if a path is within the project boundary.
return context.use().project; * 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.
/** */
* Check if a path is within the project boundary. containsPath(filepath: string) {
* Returns true if path is inside Instance.directory OR Instance.worktree. if (Filesystem.contains(Instance.directory, filepath)) return true
* Paths within the worktree but outside the working directory should not trigger external_directory permission. // Non-git projects set worktree to "/" which would match ANY absolute path.
*/ // Skip worktree check in this case to preserve external_directory permissions.
containsPath(filepath: string) { if (Instance.worktree === "/") return false
if (Filesystem.contains(Instance.directory, filepath)) return true; return Filesystem.contains(Instance.worktree, filepath)
// 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; * Captures the current instance ALS context and returns a wrapper that
return Filesystem.contains(Instance.worktree, filepath); * restores it when called. Use this for callbacks that fire outside the
}, * instance async context (native addons, event emitters, timers, etc.).
/** */
* Captures the current instance ALS context and returns a wrapper that bind<F extends (...args: any[]) => any>(fn: F): F {
* restores it when called. Use this for callbacks that fire outside the const ctx = context.use()
* instance async context (native addons, event emitters, timers, etc.). return ((...args: any[]) => context.provide(ctx, () => fn(...args))) as F
*/ },
bind<F extends (...args: any[]) => any>(fn: F): F { state<S>(init: () => S, dispose?: (state: Awaited<S>) => Promise<void>): () => S {
const ctx = context.use(); return State.create(() => Instance.directory, init, dispose)
return ((...args: any[]) => context.provide(ctx, () => fn(...args))) as F; },
}, async reload(input: { directory: string; init?: () => Promise<any>; project?: Project.Info; worktree?: string }) {
state<S>( const directory = Filesystem.resolve(input.directory)
init: () => S, Log.Default.info("reloading instance", { directory })
dispose?: (state: Awaited<S>) => Promise<void>, await Promise.all([State.dispose(directory), disposeInstance(directory)])
): () => S { cache.delete(directory)
return State.create(() => Instance.directory, init, dispose); const next = track(directory, boot({ ...input, directory }))
}, emit(directory)
async reload(input: { return await next
directory: string; },
init?: () => Promise<any>; async dispose() {
project?: Project.Info; const directory = Instance.directory
worktree?: string; Log.Default.info("disposing instance", { directory })
}) { await Promise.all([State.dispose(directory), disposeInstance(directory)])
const directory = Filesystem.resolve(input.directory); cache.delete(directory)
Log.Default.info("reloading instance", { directory }); emit(directory)
await Promise.all([State.dispose(directory), disposeInstance(directory)]); },
cache.delete(directory); async disposeAll() {
const next = track(directory, boot({ ...input, directory })); if (disposal.all) return disposal.all
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 () => { disposal.all = iife(async () => {
Log.Default.info("disposing all instances"); Log.Default.info("disposing all instances")
const entries = [...cache.entries()]; const entries = [...cache.entries()]
for (const [key, value] of entries) { for (const [key, value] of entries) {
if (cache.get(key) !== value) continue; if (cache.get(key) !== value) continue
const ctx = await value.catch((error) => { const ctx = await value.catch((error) => {
Log.Default.warn("instance dispose failed", { key, error }); Log.Default.warn("instance dispose failed", { key, error })
return undefined; return undefined
}); })
if (!ctx) { if (!ctx) {
if (cache.get(key) === value) cache.delete(key); if (cache.get(key) === value) cache.delete(key)
continue; continue
} }
if (cache.get(key) !== value) continue; if (cache.get(key) !== value) continue
await context.provide(ctx, async () => { await context.provide(ctx, async () => {
await Instance.dispose(); await Instance.dispose()
}); })
} }
}).finally(() => { }).finally(() => {
disposal.all = undefined; disposal.all = undefined
}); })
return disposal.all; return disposal.all
}, },
}; }

View File

@@ -1,516 +1,381 @@
import { import { NodeChildProcessSpawner, NodeFileSystem, NodePath } from "@effect/platform-node"
NodeChildProcessSpawner, import { Cause, Duration, Effect, FileSystem, Layer, Schedule, ServiceMap, Stream } from "effect"
NodeFileSystem, import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
NodePath, import path from "path"
} from "@effect/platform-node"; import z from "zod"
import { import { InstanceContext } from "@/effect/instance-context"
Cause, import { runPromiseInstance } from "@/effect/runtime"
Duration, import { Config } from "../config/config"
Effect, import { Global } from "../global"
FileSystem, import { Log } from "../util/log"
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 log = Log.create({ service: "snapshot" })
const PRUNE = "7.days"; const PRUNE = "7.days"
// Common git config flags shared across snapshot operations // Common git config flags shared across snapshot operations
const GIT_CORE = ["-c", "core.longpaths=true", "-c", "core.symlinks=true"]; const GIT_CORE = ["-c", "core.longpaths=true", "-c", "core.symlinks=true"]
const GIT_CFG = ["-c", "core.autocrlf=false", ...GIT_CORE]; const GIT_CFG = ["-c", "core.autocrlf=false", ...GIT_CORE]
const GIT_CFG_QUOTE = [...GIT_CFG, "-c", "core.quotepath=false"]; const GIT_CFG_QUOTE = [...GIT_CFG, "-c", "core.quotepath=false"]
interface GitResult { interface GitResult {
readonly code: ChildProcessSpawner.ExitCode; readonly code: ChildProcessSpawner.ExitCode
readonly text: string; readonly text: string
readonly stderr: string; readonly stderr: string
} }
export namespace Snapshot { export namespace Snapshot {
export const Patch = z.object({ export const Patch = z.object({
hash: z.string(), hash: z.string(),
files: z.string().array(), files: z.string().array(),
}); })
export type Patch = z.infer<typeof Patch>; export type Patch = z.infer<typeof Patch>
export const FileDiff = z export const FileDiff = z
.object({ .object({
file: z.string(), file: z.string(),
before: z.string(), before: z.string(),
after: z.string(), after: z.string(),
additions: z.number(), additions: z.number(),
deletions: z.number(), deletions: z.number(),
status: z.enum(["added", "deleted", "modified"]).optional(), status: z.enum(["added", "deleted", "modified"]).optional(),
}) })
.meta({ .meta({
ref: "FileDiff", ref: "FileDiff",
}); })
export type FileDiff = z.infer<typeof FileDiff>; export type FileDiff = z.infer<typeof FileDiff>
// Promise facade — existing callers use these // Promise facade — existing callers use these
export function init() { export function init() {
void runPromiseInstance(SnapshotService.use((s) => s.init())); void runPromiseInstance(SnapshotService.use((s) => s.init()))
} }
export async function cleanup() { export async function cleanup() {
return runPromiseInstance(SnapshotService.use((s) => s.cleanup())); return runPromiseInstance(SnapshotService.use((s) => s.cleanup()))
} }
export async function track() { export async function track() {
return runPromiseInstance(SnapshotService.use((s) => s.track())); return runPromiseInstance(SnapshotService.use((s) => s.track()))
} }
export async function patch(hash: string) { export async function patch(hash: string) {
return runPromiseInstance(SnapshotService.use((s) => s.patch(hash))); return runPromiseInstance(SnapshotService.use((s) => s.patch(hash)))
} }
export async function restore(snapshot: string) { export async function restore(snapshot: string) {
return runPromiseInstance(SnapshotService.use((s) => s.restore(snapshot))); return runPromiseInstance(SnapshotService.use((s) => s.restore(snapshot)))
} }
export async function revert(patches: Patch[]) { export async function revert(patches: Patch[]) {
return runPromiseInstance(SnapshotService.use((s) => s.revert(patches))); return runPromiseInstance(SnapshotService.use((s) => s.revert(patches)))
} }
export async function diff(hash: string) { export async function diff(hash: string) {
return runPromiseInstance(SnapshotService.use((s) => s.diff(hash))); return runPromiseInstance(SnapshotService.use((s) => s.diff(hash)))
} }
export async function diffFull(from: string, to: string) { export async function diffFull(from: string, to: string) {
return runPromiseInstance(SnapshotService.use((s) => s.diffFull(from, to))); return runPromiseInstance(SnapshotService.use((s) => s.diffFull(from, to)))
} }
} }
export namespace SnapshotService { export namespace SnapshotService {
export interface Service { export interface Service {
readonly init: () => Effect.Effect<void>; readonly init: () => Effect.Effect<void>
readonly cleanup: () => Effect.Effect<void>; readonly cleanup: () => Effect.Effect<void>
readonly track: () => Effect.Effect<string | undefined>; readonly track: () => Effect.Effect<string | undefined>
readonly patch: (hash: string) => Effect.Effect<Snapshot.Patch>; readonly patch: (hash: string) => Effect.Effect<Snapshot.Patch>
readonly restore: (snapshot: string) => Effect.Effect<void>; readonly restore: (snapshot: string) => Effect.Effect<void>
readonly revert: (patches: Snapshot.Patch[]) => Effect.Effect<void>; readonly revert: (patches: Snapshot.Patch[]) => Effect.Effect<void>
readonly diff: (hash: string) => Effect.Effect<string>; readonly diff: (hash: string) => Effect.Effect<string>
readonly diffFull: ( readonly diffFull: (from: string, to: string) => Effect.Effect<Snapshot.FileDiff[]>
from: string, }
to: string,
) => Effect.Effect<Snapshot.FileDiff[]>;
}
} }
export class SnapshotService extends ServiceMap.Service< export class SnapshotService extends ServiceMap.Service<SnapshotService, SnapshotService.Service>()(
SnapshotService, "@opencode/Snapshot",
SnapshotService.Service ) {
>()("@opencode/Snapshot") { static readonly layer = Layer.effect(
static readonly layer = Layer.effect( SnapshotService,
SnapshotService, Effect.gen(function* () {
Effect.gen(function* () { const ctx = yield* InstanceContext
const ctx = yield* InstanceContext; const fileSystem = yield* FileSystem.FileSystem
const fileSystem = yield* FileSystem.FileSystem; const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const { directory, worktree, project } = ctx
const { directory, worktree, project } = ctx; const isGit = project.vcs === "git"
const isGit = project.vcs === "git"; const snapshotGit = path.join(Global.Path.data, "snapshot", project.id)
const snapshotGit = path.join(Global.Path.data, "snapshot", project.id);
const gitArgs = (cmd: string[]) => [ const gitArgs = (cmd: string[]) => ["--git-dir", snapshotGit, "--work-tree", worktree, ...cmd]
"--git-dir",
snapshotGit,
"--work-tree",
worktree,
...cmd,
];
// Run git with nothrow semantics — always returns a result, never fails // Run git with nothrow semantics — always returns a result, never fails
const git = ( const git = (args: string[], opts?: { cwd?: string; env?: Record<string, string> }): Effect.Effect<GitResult> =>
args: string[], Effect.gen(function* () {
opts?: { cwd?: string; env?: Record<string, string> }, const command = ChildProcess.make("git", args, {
): Effect.Effect<GitResult> => cwd: opts?.cwd,
Effect.gen(function* () { env: opts?.env,
const command = ChildProcess.make("git", args, { extendEnv: true,
cwd: opts?.cwd, })
env: opts?.env, const handle = yield* spawner.spawn(command)
extendEnv: true, const [text, stderr] = yield* Effect.all(
}); [Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))],
const handle = yield* spawner.spawn(command); { concurrency: 2 },
const [text, stderr] = yield* Effect.all( )
[ const code = yield* handle.exitCode
Stream.mkString(Stream.decodeText(handle.stdout)), return { code, text, stderr }
Stream.mkString(Stream.decodeText(handle.stderr)), }).pipe(
], Effect.scoped,
{ concurrency: 2 }, Effect.catch((err) =>
); Effect.succeed({
const code = yield* handle.exitCode; code: ChildProcessSpawner.ExitCode(1),
return { code, text, stderr }; text: "",
}).pipe( stderr: String(err),
Effect.scoped, }),
Effect.catch((err) => ),
Effect.succeed({ )
code: ChildProcessSpawner.ExitCode(1),
text: "",
stderr: String(err),
}),
),
);
// FileSystem helpers — orDie converts PlatformError to defects // FileSystem helpers — orDie converts PlatformError to defects
const exists = (p: string) => fileSystem.exists(p).pipe(Effect.orDie); const exists = (p: string) => fileSystem.exists(p).pipe(Effect.orDie)
const mkdir = (p: string) => const mkdir = (p: string) => fileSystem.makeDirectory(p, { recursive: true }).pipe(Effect.orDie)
fileSystem.makeDirectory(p, { recursive: true }).pipe(Effect.orDie); const writeFile = (p: string, content: string) => fileSystem.writeFileString(p, content).pipe(Effect.orDie)
const writeFile = (p: string, content: string) => const readFile = (p: string) => fileSystem.readFileString(p).pipe(Effect.catch(() => Effect.succeed("")))
fileSystem.writeFileString(p, content).pipe(Effect.orDie); const removeFile = (p: string) => fileSystem.remove(p).pipe(Effect.catch(() => Effect.void))
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* () { const isEnabled = Effect.gen(function* () {
if (!isGit) return false; if (!isGit) return false
const cfg = yield* Effect.promise(() => Config.get()); const cfg = yield* Effect.promise(() => Config.get())
return cfg.snapshot !== false; return cfg.snapshot !== false
}); })
const excludesPath = Effect.gen(function* () { const excludesPath = Effect.gen(function* () {
const result = yield* git( const result = yield* git(["rev-parse", "--path-format=absolute", "--git-path", "info/exclude"], {
["rev-parse", "--path-format=absolute", "--git-path", "info/exclude"], cwd: worktree,
{ })
cwd: worktree, const file = result.text.trim()
}, if (!file) return undefined
); if (!(yield* exists(file))) return undefined
const file = result.text.trim(); return file
if (!file) return undefined; })
if (!(yield* exists(file))) return undefined;
return file;
});
const syncExclude = Effect.gen(function* () { const syncExclude = Effect.gen(function* () {
const file = yield* excludesPath; const file = yield* excludesPath
const target = path.join(snapshotGit, "info", "exclude"); const target = path.join(snapshotGit, "info", "exclude")
yield* mkdir(path.join(snapshotGit, "info")); yield* mkdir(path.join(snapshotGit, "info"))
if (!file) { if (!file) {
yield* writeFile(target, ""); yield* writeFile(target, "")
return; return
} }
const text = yield* readFile(file); const text = yield* readFile(file)
yield* writeFile(target, text); yield* writeFile(target, text)
}); })
const add = Effect.gen(function* () { const add = Effect.gen(function* () {
yield* syncExclude; yield* syncExclude
yield* git([...GIT_CFG, ...gitArgs(["add", "."])], { cwd: directory }); yield* git([...GIT_CFG, ...gitArgs(["add", "."])], { cwd: directory })
}); })
// --- service methods --- // --- service methods ---
const cleanup = Effect.fn("SnapshotService.cleanup")(function* () { const cleanup = Effect.fn("SnapshotService.cleanup")(function* () {
if (!(yield* isEnabled)) return; if (!(yield* isEnabled)) return
if (!(yield* exists(snapshotGit))) return; if (!(yield* exists(snapshotGit))) return
const result = yield* git(gitArgs(["gc", `--prune=${PRUNE}`]), { const result = yield* git(gitArgs(["gc", `--prune=${PRUNE}`]), {
cwd: directory, cwd: directory,
}); })
if (result.code !== 0) { if (result.code !== 0) {
log.warn("cleanup failed", { log.warn("cleanup failed", {
exitCode: result.code, exitCode: result.code,
stderr: result.stderr, stderr: result.stderr,
}); })
return; return
} }
log.info("cleanup", { prune: PRUNE }); log.info("cleanup", { prune: PRUNE })
}); })
const track = Effect.fn("SnapshotService.track")(function* () { const track = Effect.fn("SnapshotService.track")(function* () {
if (!(yield* isEnabled)) return undefined; if (!(yield* isEnabled)) return undefined
const existed = yield* exists(snapshotGit); const existed = yield* exists(snapshotGit)
yield* mkdir(snapshotGit); yield* mkdir(snapshotGit)
if (!existed) { if (!existed) {
yield* git(["init"], { yield* git(["init"], {
env: { GIT_DIR: snapshotGit, GIT_WORK_TREE: worktree }, env: { GIT_DIR: snapshotGit, GIT_WORK_TREE: worktree },
}); })
yield* git([ yield* git(["--git-dir", snapshotGit, "config", "core.autocrlf", "false"])
"--git-dir", yield* git(["--git-dir", snapshotGit, "config", "core.longpaths", "true"])
snapshotGit, yield* git(["--git-dir", snapshotGit, "config", "core.symlinks", "true"])
"config", yield* git(["--git-dir", snapshotGit, "config", "core.fsmonitor", "false"])
"core.autocrlf", log.info("initialized")
"false", }
]); yield* add
yield* git([ const result = yield* git(gitArgs(["write-tree"]), { cwd: directory })
"--git-dir", const hash = result.text.trim()
snapshotGit, log.info("tracking", { hash, cwd: directory, git: snapshotGit })
"config", return hash
"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* ( const patch = Effect.fn("SnapshotService.patch")(function* (hash: string) {
hash: string, yield* add
) { const result = yield* git(
yield* add; [...GIT_CFG_QUOTE, ...gitArgs(["diff", "--no-ext-diff", "--name-only", hash, "--", "."])],
const result = yield* git( { cwd: directory },
[ )
...GIT_CFG_QUOTE,
...gitArgs([
"diff",
"--no-ext-diff",
"--name-only",
hash,
"--",
".",
]),
],
{ cwd: directory },
);
if (result.code !== 0) { if (result.code !== 0) {
log.warn("failed to get diff", { hash, exitCode: result.code }); log.warn("failed to get diff", { hash, exitCode: result.code })
return { hash, files: [] } as Snapshot.Patch; return { hash, files: [] } as Snapshot.Patch
} }
return { return {
hash, hash,
files: result.text files: result.text
.trim() .trim()
.split("\n") .split("\n")
.map((x: string) => x.trim()) .map((x: string) => x.trim())
.filter(Boolean) .filter(Boolean)
.map((x: string) => path.join(worktree, x).replaceAll("\\", "/")), .map((x: string) => path.join(worktree, x).replaceAll("\\", "/")),
} as Snapshot.Patch; } as Snapshot.Patch
}); })
const restore = Effect.fn("SnapshotService.restore")(function* ( const restore = Effect.fn("SnapshotService.restore")(function* (snapshot: string) {
snapshot: string, log.info("restore", { commit: snapshot })
) { const result = yield* git([...GIT_CORE, ...gitArgs(["read-tree", snapshot])], { cwd: worktree })
log.info("restore", { commit: snapshot }); if (result.code === 0) {
const result = yield* git( const checkout = yield* git([...GIT_CORE, ...gitArgs(["checkout-index", "-a", "-f"])], { cwd: worktree })
[...GIT_CORE, ...gitArgs(["read-tree", snapshot])], if (checkout.code === 0) return
{ cwd: worktree }, log.error("failed to restore snapshot", {
); snapshot,
if (result.code === 0) { exitCode: checkout.code,
const checkout = yield* git( stderr: checkout.stderr,
[...GIT_CORE, ...gitArgs(["checkout-index", "-a", "-f"])], })
{ cwd: worktree }, return
); }
if (checkout.code === 0) return; log.error("failed to restore snapshot", {
log.error("failed to restore snapshot", { snapshot,
snapshot, exitCode: result.code,
exitCode: checkout.code, stderr: result.stderr,
stderr: checkout.stderr, })
}); })
return;
}
log.error("failed to restore snapshot", {
snapshot,
exitCode: result.code,
stderr: result.stderr,
});
});
const revert = Effect.fn("SnapshotService.revert")(function* ( const revert = Effect.fn("SnapshotService.revert")(function* (patches: Snapshot.Patch[]) {
patches: Snapshot.Patch[], const seen = new Set<string>()
) { for (const item of patches) {
const seen = new Set<string>(); for (const file of item.files) {
for (const item of patches) { if (seen.has(file)) continue
for (const file of item.files) { log.info("reverting", { file, hash: item.hash })
if (seen.has(file)) continue; const result = yield* git([...GIT_CORE, ...gitArgs(["checkout", item.hash, "--", file])], {
log.info("reverting", { file, hash: item.hash }); cwd: worktree,
const result = yield* git( })
[...GIT_CORE, ...gitArgs(["checkout", item.hash, "--", file])], if (result.code !== 0) {
{ const relativePath = path.relative(worktree, file)
cwd: worktree, const checkTree = yield* git([...GIT_CORE, ...gitArgs(["ls-tree", item.hash, "--", relativePath])], {
}, cwd: worktree,
); })
if (result.code !== 0) { if (checkTree.code === 0 && checkTree.text.trim()) {
const relativePath = path.relative(worktree, file); log.info("file existed in snapshot but checkout failed, keeping", { file })
const checkTree = yield* git( } else {
[ log.info("file did not exist in snapshot, deleting", { file })
...GIT_CORE, yield* removeFile(file)
...gitArgs(["ls-tree", item.hash, "--", relativePath]), }
], }
{ seen.add(file)
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) { const diff = Effect.fn("SnapshotService.diff")(function* (hash: string) {
yield* add; yield* add
const result = yield* git( const result = yield* git([...GIT_CFG_QUOTE, ...gitArgs(["diff", "--no-ext-diff", hash, "--", "."])], {
[ cwd: worktree,
...GIT_CFG_QUOTE, })
...gitArgs(["diff", "--no-ext-diff", hash, "--", "."]),
],
{
cwd: worktree,
},
);
if (result.code !== 0) { if (result.code !== 0) {
log.warn("failed to get diff", { log.warn("failed to get diff", {
hash, hash,
exitCode: result.code, exitCode: result.code,
stderr: result.stderr, stderr: result.stderr,
}); })
return ""; return ""
} }
return result.text.trim(); return result.text.trim()
}); })
const diffFull = Effect.fn("SnapshotService.diffFull")(function* ( const diffFull = Effect.fn("SnapshotService.diffFull")(function* (from: string, to: string) {
from: string, const result: Snapshot.FileDiff[] = []
to: string, const status = new Map<string, "added" | "deleted" | "modified">()
) {
const result: Snapshot.FileDiff[] = [];
const status = new Map<string, "added" | "deleted" | "modified">();
const statuses = yield* git( const statuses = yield* git(
[ [
...GIT_CFG_QUOTE, ...GIT_CFG_QUOTE,
...gitArgs([ ...gitArgs(["diff", "--no-ext-diff", "--name-status", "--no-renames", from, to, "--", "."]),
"diff", ],
"--no-ext-diff", { cwd: directory },
"--name-status", )
"--no-renames",
from,
to,
"--",
".",
]),
],
{ cwd: directory },
);
for (const line of statuses.text.trim().split("\n")) { for (const line of statuses.text.trim().split("\n")) {
if (!line) continue; if (!line) continue
const [code, file] = line.split("\t"); const [code, file] = line.split("\t")
if (!code || !file) continue; if (!code || !file) continue
const kind = code.startsWith("A") const kind = code.startsWith("A") ? "added" : code.startsWith("D") ? "deleted" : "modified"
? "added" status.set(file, kind)
: code.startsWith("D") }
? "deleted"
: "modified";
status.set(file, kind);
}
const numstat = yield* git( const numstat = yield* git(
[ [...GIT_CFG_QUOTE, ...gitArgs(["diff", "--no-ext-diff", "--no-renames", "--numstat", from, to, "--", "."])],
...GIT_CFG_QUOTE, { cwd: directory },
...gitArgs([ )
"diff",
"--no-ext-diff",
"--no-renames",
"--numstat",
from,
to,
"--",
".",
]),
],
{ cwd: directory },
);
for (const line of numstat.text.trim().split("\n")) { for (const line of numstat.text.trim().split("\n")) {
if (!line) continue; if (!line) continue
const [additions, deletions, file] = line.split("\t"); const [additions, deletions, file] = line.split("\t")
const isBinaryFile = additions === "-" && deletions === "-"; const isBinaryFile = additions === "-" && deletions === "-"
const [before, after] = isBinaryFile const [before, after] = isBinaryFile
? ["", ""] ? ["", ""]
: yield* Effect.all( : yield* Effect.all(
[ [
git([ git([...GIT_CFG, ...gitArgs(["show", `${from}:${file}`])]).pipe(Effect.map((r) => r.text)),
...GIT_CFG, git([...GIT_CFG, ...gitArgs(["show", `${to}:${file}`])]).pipe(Effect.map((r) => r.text)),
...gitArgs(["show", `${from}:${file}`]), ],
]).pipe(Effect.map((r) => r.text)), { concurrency: 2 },
git([...GIT_CFG, ...gitArgs(["show", `${to}:${file}`])]).pipe( )
Effect.map((r) => r.text), const added = isBinaryFile ? 0 : parseInt(additions!)
), const deleted = isBinaryFile ? 0 : parseInt(deletions!)
], result.push({
{ concurrency: 2 }, file: file!,
); before,
const added = isBinaryFile ? 0 : parseInt(additions!); after,
const deleted = isBinaryFile ? 0 : parseInt(deletions!); additions: Number.isFinite(added) ? added : 0,
result.push({ deletions: Number.isFinite(deleted) ? deleted : 0,
file: file!, status: status.get(file!) ?? "modified",
before, })
after, }
additions: Number.isFinite(added) ? added : 0, return result
deletions: Number.isFinite(deleted) ? deleted : 0, })
status: status.get(file!) ?? "modified",
});
}
return result;
});
// Start hourly cleanup fiber — scoped to instance lifetime // Start hourly cleanup fiber — scoped to instance lifetime
yield* cleanup().pipe( yield* cleanup().pipe(
Effect.catchCause((cause) => { Effect.catchCause((cause) => {
log.error("cleanup loop failed", { cause: Cause.pretty(cause) }); log.error("cleanup loop failed", { cause: Cause.pretty(cause) })
return Effect.void; return Effect.void
}), }),
Effect.repeat(Schedule.spaced(Duration.hours(1))), Effect.repeat(Schedule.spaced(Duration.hours(1))),
Effect.forkScoped, Effect.forkScoped,
); )
return SnapshotService.of({ return SnapshotService.of({
init: Effect.fn("SnapshotService.init")(function* () {}), init: Effect.fn("SnapshotService.init")(function* () {}),
cleanup, cleanup,
track, track,
patch, patch,
restore, restore,
revert, revert,
diff, diff,
diffFull, diffFull,
}); })
}), }),
).pipe( ).pipe(
Layer.provide(NodeChildProcessSpawner.layer), Layer.provide(NodeChildProcessSpawner.layer),
Layer.provide(NodeFileSystem.layer), Layer.provide(NodeFileSystem.layer),
Layer.provide(NodePath.layer), Layer.provide(NodePath.layer),
); )
} }

View File

@@ -1,14 +1,14 @@
import { ConfigProvider, Layer, ManagedRuntime } from "effect"; import { ConfigProvider, Layer, ManagedRuntime } from "effect"
import { InstanceContext } from "../../src/effect/instance-context"; import { InstanceContext } from "../../src/effect/instance-context"
import { Instance } from "../../src/project/instance"; import { Instance } from "../../src/project/instance"
/** ConfigProvider that enables the experimental file watcher. */ /** ConfigProvider that enables the experimental file watcher. */
export const watcherConfigLayer = ConfigProvider.layer( export const watcherConfigLayer = ConfigProvider.layer(
ConfigProvider.fromUnknown({ ConfigProvider.fromUnknown({
OPENCODE_EXPERIMENTAL_FILEWATCHER: "true", OPENCODE_EXPERIMENTAL_FILEWATCHER: "true",
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "false", OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "false",
}), }),
); )
/** /**
* Boot an Instance with the given service layers and run `body` with * 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). * Pass extra layers via `options.provide` (e.g. ConfigProvider.layer).
*/ */
export function withServices<S>( export function withServices<S>(
directory: string, directory: string,
layer: Layer.Layer<S, any, InstanceContext>, layer: Layer.Layer<S, any, InstanceContext>,
body: (rt: ManagedRuntime.ManagedRuntime<S, never>) => Promise<void>, body: (rt: ManagedRuntime.ManagedRuntime<S, never>) => Promise<void>,
options?: { provide?: Layer.Layer<never>[] }, options?: { provide?: Layer.Layer<never>[] },
) { ) {
return Instance.provide({ return Instance.provide({
directory, directory,
fn: async () => { fn: async () => {
const ctx = Layer.sync(InstanceContext, () => const ctx = Layer.sync(InstanceContext, () =>
InstanceContext.of({ InstanceContext.of({
directory: Instance.directory, directory: Instance.directory,
worktree: Instance.worktree, worktree: Instance.worktree,
project: Instance.project, project: Instance.project,
}), }),
); )
let resolved: Layer.Layer<S> = Layer.fresh(layer).pipe( let resolved: Layer.Layer<S> = Layer.fresh(layer).pipe(Layer.provide(ctx)) as any
Layer.provide(ctx), if (options?.provide) {
) as any; for (const l of options.provide) {
if (options?.provide) { resolved = resolved.pipe(Layer.provide(l)) as any
for (const l of options.provide) { }
resolved = resolved.pipe(Layer.provide(l)) as any; }
} const rt = ManagedRuntime.make(resolved)
} try {
const rt = ManagedRuntime.make(resolved); await body(rt)
try { } finally {
await body(rt); await rt.dispose()
} finally { }
await rt.dispose(); },
} })
},
});
} }

View File

@@ -47,6 +47,13 @@ export type EventProjectUpdated = {
properties: Project properties: Project
} }
export type EventFileEdited = {
type: "file.edited"
properties: {
file: string
}
}
export type EventServerInstanceDisposed = { export type EventServerInstanceDisposed = {
type: "server.instance.disposed" type: "server.instance.disposed"
properties: { 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<string>
metadata: {
[key: string]: unknown
}
always: Array<string>
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 = { export type QuestionOption = {
/** /**
* Display text (1-5 words, concise) * Display text (1-5 words, concise)
@@ -125,57 +176,6 @@ export type EventQuestionRejected = {
} }
} }
export type PermissionRequest = {
id: string
sessionID: string
permission: string
patterns: Array<string>
metadata: {
[key: string]: unknown
}
always: Array<string>
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 = { export type EventServerConnected = {
type: "server.connected" type: "server.connected"
properties: { properties: {
@@ -961,15 +961,15 @@ export type Event =
| EventInstallationUpdated | EventInstallationUpdated
| EventInstallationUpdateAvailable | EventInstallationUpdateAvailable
| EventProjectUpdated | EventProjectUpdated
| EventFileEdited
| EventServerInstanceDisposed | EventServerInstanceDisposed
| EventFileWatcherUpdated
| EventPermissionAsked
| EventPermissionReplied
| EventVcsBranchUpdated
| EventQuestionAsked | EventQuestionAsked
| EventQuestionReplied | EventQuestionReplied
| EventQuestionRejected | EventQuestionRejected
| EventPermissionAsked
| EventPermissionReplied
| EventFileWatcherUpdated
| EventVcsBranchUpdated
| EventFileEdited
| EventServerConnected | EventServerConnected
| EventGlobalDisposed | EventGlobalDisposed
| EventLspClientDiagnostics | EventLspClientDiagnostics

View File

@@ -7043,6 +7043,25 @@
}, },
"required": ["type", "properties"] "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": { "Event.server.instance.disposed": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -7062,6 +7081,149 @@
}, },
"required": ["type", "properties"] "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": { "QuestionOption": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -7212,168 +7374,6 @@
}, },
"required": ["type", "properties"] "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": { "Event.server.connected": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -9608,9 +9608,24 @@
{ {
"$ref": "#/components/schemas/Event.project.updated" "$ref": "#/components/schemas/Event.project.updated"
}, },
{
"$ref": "#/components/schemas/Event.file.edited"
},
{ {
"$ref": "#/components/schemas/Event.server.instance.disposed" "$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" "$ref": "#/components/schemas/Event.question.asked"
}, },
@@ -9620,21 +9635,6 @@
{ {
"$ref": "#/components/schemas/Event.question.rejected" "$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" "$ref": "#/components/schemas/Event.server.connected"
}, },