Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f93669f4c |
@@ -450,7 +450,6 @@
|
||||
"version": "1.4.3",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
"zod": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-gFbo3B6TFAmin2marXlwUyfchTX6ogsaUFEzBIl4zaI=",
|
||||
"aarch64-linux": "sha256-HUKL7zBVtb1KPaoAgfSfAzjDoAPRUe2WNFHDrsoqEF8=",
|
||||
"aarch64-darwin": "sha256-qWPRkuVA3nDEEaVZ0Ex4sYsFFarSRJSyOn+KJm1D3U0=",
|
||||
"x86_64-darwin": "sha256-FxhOYMXkxjn/9xQPeVX/gfQT/KjHT4wIBqzVDZuYlos="
|
||||
"x86_64-linux": "sha256-285KZ7rZLRoc6XqCZRHc25NE+mmpGh/BVeMpv8aPQtQ=",
|
||||
"aarch64-linux": "sha256-qIwmY4TP4CI7R7G6A5OMYRrorVNXjkg25tTtVpIHm2o=",
|
||||
"aarch64-darwin": "sha256-RwvnZQhdYZ0u7h7evyfxuPLHHX9eO/jXTAxIFc8B+IE=",
|
||||
"x86_64-darwin": "sha256-vVj40al+TEeMpbe5XG2GmJEpN+eQAvtr9W0T98l5PBE="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,7 +223,7 @@ Fully migrated (single namespace, InstanceState where needed, flattened facade):
|
||||
|
||||
Still open:
|
||||
|
||||
- [x] `SessionTodo` — `session/todo.ts`
|
||||
- [ ] `SessionTodo` — `session/todo.ts`
|
||||
- [ ] `SyncEvent` — `sync/index.ts`
|
||||
- [ ] `Workspace` — `control-plane/workspace.ts`
|
||||
|
||||
@@ -336,8 +336,6 @@ For each service, the migration is roughly:
|
||||
|
||||
### Migration log
|
||||
|
||||
- `Snapshot` — facade destroyed 2026-04-11. Removed the `makeRuntime(...)` wrapper and the seven async exports (`init`, `track`, `patch`, `restore`, `revert`, `diff`, `diffFull`); `project/bootstrap.ts` now runs `svc.init()` through `BootstrapRuntime`, the debug CLI commands use `AppRuntime.runPromise(Snapshot.Service.use(...))`, and the snapshot/revert-compact/project-init-git tests were rewritten to the same pattern. `Snapshot.defaultLayer` was added to `BootstrapLayer` so bootstrap can resolve the service without pulling in `AppRuntime`.
|
||||
- `SessionStatus` — migrated 2026-04-11. Replaced the last route and retry-policy callers with `AppRuntime.runPromise(SessionStatus.Service.use(...))` and removed the `makeRuntime(...)` facade.
|
||||
- `ShareNext` — migrated 2026-04-11. Swapped remaining async callers to `AppRuntime.runPromise(ShareNext.Service.use(...))`, removed the `makeRuntime(...)` facade, and kept instance bootstrap on the shared app runtime.
|
||||
- `SessionTodo` — migrated 2026-04-10. Already matched the target service shape in `session/todo.ts`: single namespace, traced Effect methods, and no `makeRuntime(...)` facade remained; checklist updated to reflect the completed migration.
|
||||
- `Storage` — migrated 2026-04-10. One production caller (`Session.diff`) and all storage.test.ts tests converted to effectful style. Facades and `makeRuntime` removed.
|
||||
|
||||
@@ -158,7 +158,7 @@ async function createToolContext(agent: Agent.Info) {
|
||||
agent: agent.name,
|
||||
abort: new AbortController().signal,
|
||||
messages: [],
|
||||
metadata: () => Effect.void,
|
||||
metadata: () => {},
|
||||
ask(req: Omit<Permission.Request, "id" | "sessionID" | "tool">) {
|
||||
return Effect.sync(() => {
|
||||
for (const pattern of req.patterns) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { Snapshot } from "../../../snapshot"
|
||||
import { bootstrap } from "../../bootstrap"
|
||||
import { cmd } from "../cmd"
|
||||
@@ -15,7 +14,7 @@ const TrackCommand = cmd({
|
||||
describe: "track current snapshot state",
|
||||
async handler() {
|
||||
await bootstrap(process.cwd(), async () => {
|
||||
console.log(await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track())))
|
||||
console.log(await Snapshot.track())
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -31,7 +30,7 @@ const PatchCommand = cmd({
|
||||
}),
|
||||
async handler(args) {
|
||||
await bootstrap(process.cwd(), async () => {
|
||||
console.log(await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(args.hash))))
|
||||
console.log(await Snapshot.patch(args.hash))
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -47,7 +46,7 @@ const DiffCommand = cmd({
|
||||
}),
|
||||
async handler(args) {
|
||||
await bootstrap(process.cwd(), async () => {
|
||||
console.log(await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.diff(args.hash))))
|
||||
console.log(await Snapshot.diff(args.hash))
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -29,7 +29,6 @@ import { Provider } from "../../provider/provider"
|
||||
import { Bus } from "../../bus"
|
||||
import { MessageV2 } from "../../session/message-v2"
|
||||
import { SessionPrompt } from "@/session/prompt"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { Git } from "@/git"
|
||||
import { setTimeout as sleep } from "node:timers/promises"
|
||||
import { Process } from "@/util/process"
|
||||
@@ -259,9 +258,7 @@ export const GithubInstallCommand = cmd({
|
||||
}
|
||||
|
||||
// Get repo info
|
||||
const info = await AppRuntime.runPromise(
|
||||
Git.Service.use((git) => git.run(["remote", "get-url", "origin"], { cwd: Instance.worktree })),
|
||||
).then((x) => x.text().trim())
|
||||
const info = (await Git.run(["remote", "get-url", "origin"], { cwd: Instance.worktree })).text().trim()
|
||||
const parsed = parseGitHubRemote(info)
|
||||
if (!parsed) {
|
||||
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
|
||||
@@ -500,21 +497,20 @@ export const GithubRunCommand = cmd({
|
||||
: "issue"
|
||||
: undefined
|
||||
const gitText = async (args: string[]) => {
|
||||
const result = await AppRuntime.runPromise(Git.Service.use((git) => git.run(args, { cwd: Instance.worktree })))
|
||||
const result = await Git.run(args, { cwd: Instance.worktree })
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr)
|
||||
}
|
||||
return result.text().trim()
|
||||
}
|
||||
const gitRun = async (args: string[]) => {
|
||||
const result = await AppRuntime.runPromise(Git.Service.use((git) => git.run(args, { cwd: Instance.worktree })))
|
||||
const result = await Git.run(args, { cwd: Instance.worktree })
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr)
|
||||
}
|
||||
return result
|
||||
}
|
||||
const gitStatus = (args: string[]) =>
|
||||
AppRuntime.runPromise(Git.Service.use((git) => git.run(args, { cwd: Instance.worktree })))
|
||||
const gitStatus = (args: string[]) => Git.run(args, { cwd: Instance.worktree })
|
||||
const commitChanges = async (summary: string, actor?: string) => {
|
||||
const args = ["commit", "-m", summary]
|
||||
if (actor) args.push("-m", `Co-authored-by: ${actor} <${actor}@users.noreply.github.com>`)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { UI } from "../ui"
|
||||
import { cmd } from "./cmd"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { Git } from "@/git"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { Process } from "@/util/process"
|
||||
@@ -68,29 +67,19 @@ export const PrCommand = cmd({
|
||||
const remoteName = forkOwner
|
||||
|
||||
// Check if remote already exists
|
||||
const remotes = await AppRuntime.runPromise(
|
||||
Git.Service.use((git) => git.run(["remote"], { cwd: Instance.worktree })),
|
||||
).then((x) => x.text().trim())
|
||||
const remotes = (await Git.run(["remote"], { cwd: Instance.worktree })).text().trim()
|
||||
if (!remotes.split("\n").includes(remoteName)) {
|
||||
await AppRuntime.runPromise(
|
||||
Git.Service.use((git) =>
|
||||
git.run(["remote", "add", remoteName, `https://github.com/${forkOwner}/${forkName}.git`], {
|
||||
cwd: Instance.worktree,
|
||||
}),
|
||||
),
|
||||
)
|
||||
await Git.run(["remote", "add", remoteName, `https://github.com/${forkOwner}/${forkName}.git`], {
|
||||
cwd: Instance.worktree,
|
||||
})
|
||||
UI.println(`Added fork remote: ${remoteName}`)
|
||||
}
|
||||
|
||||
// Set upstream to the fork so pushes go there
|
||||
const headRefName = prInfo.headRefName
|
||||
await AppRuntime.runPromise(
|
||||
Git.Service.use((git) =>
|
||||
git.run(["branch", `--set-upstream-to=${remoteName}/${headRefName}`, localBranchName], {
|
||||
cwd: Instance.worktree,
|
||||
}),
|
||||
),
|
||||
)
|
||||
await Git.run(["branch", `--set-upstream-to=${remoteName}/${headRefName}`, localBranchName], {
|
||||
cwd: Instance.worktree,
|
||||
})
|
||||
}
|
||||
|
||||
// Check for opencode session link in PR body
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Argv } from "yargs"
|
||||
import { UI } from "../ui"
|
||||
import * as prompts from "@clack/prompts"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { Installation } from "../../installation"
|
||||
import { Global } from "../../global"
|
||||
import fs from "fs/promises"
|
||||
@@ -58,7 +57,7 @@ export const UninstallCommand = {
|
||||
UI.empty()
|
||||
prompts.intro("Uninstall OpenCode")
|
||||
|
||||
const method = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.method()))
|
||||
const method = await Installation.method()
|
||||
prompts.log.info(`Installation method: ${method}`)
|
||||
|
||||
const targets = await collectRemovalTargets(args, method)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Argv } from "yargs"
|
||||
import { UI } from "../ui"
|
||||
import * as prompts from "@clack/prompts"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { Installation } from "../../installation"
|
||||
|
||||
export const UpgradeCommand = {
|
||||
@@ -25,7 +24,7 @@ export const UpgradeCommand = {
|
||||
UI.println(UI.logo(" "))
|
||||
UI.empty()
|
||||
prompts.intro("Upgrade")
|
||||
const detectedMethod = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.method()))
|
||||
const detectedMethod = await Installation.method()
|
||||
const method = (args.method as Installation.Method) ?? detectedMethod
|
||||
if (method === "unknown") {
|
||||
prompts.log.error(`opencode is installed to ${process.execPath} and may be managed by a package manager`)
|
||||
@@ -43,9 +42,7 @@ export const UpgradeCommand = {
|
||||
}
|
||||
}
|
||||
prompts.log.info("Using method: " + method)
|
||||
const target = args.target
|
||||
? args.target.replace(/^v/, "")
|
||||
: await AppRuntime.runPromise(Installation.Service.use((svc) => svc.latest()))
|
||||
const target = args.target ? args.target.replace(/^v/, "") : await Installation.latest()
|
||||
|
||||
if (Installation.VERSION === target) {
|
||||
prompts.log.warn(`opencode upgrade skipped: ${target} is already installed`)
|
||||
@@ -56,9 +53,7 @@ export const UpgradeCommand = {
|
||||
prompts.log.info(`From ${Installation.VERSION} → ${target}`)
|
||||
const spinner = prompts.spinner()
|
||||
spinner.start("Upgrading...")
|
||||
const err = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.upgrade(method, target))).catch(
|
||||
(err) => err,
|
||||
)
|
||||
const err = await Installation.upgrade(method, target).catch((err) => err)
|
||||
if (err) {
|
||||
spinner.stop("Upgrade failed", 1)
|
||||
if (err instanceof Installation.UpgradeFailedError) {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { Bus } from "@/bus"
|
||||
import { Config } from "@/config/config"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { Flag } from "@/flag/flag"
|
||||
import { Installation } from "@/installation"
|
||||
|
||||
export async function upgrade() {
|
||||
const config = await Config.getGlobal()
|
||||
const method = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.method()))
|
||||
const latest = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.latest(method))).catch(() => {})
|
||||
const method = await Installation.method()
|
||||
const latest = await Installation.latest(method).catch(() => {})
|
||||
if (!latest) return
|
||||
|
||||
if (Flag.OPENCODE_ALWAYS_NOTIFY_UPDATE) {
|
||||
@@ -26,7 +25,7 @@ export async function upgrade() {
|
||||
}
|
||||
|
||||
if (method === "unknown") return
|
||||
await AppRuntime.runPromise(Installation.Service.use((svc) => svc.upgrade(method, latest)))
|
||||
await Installation.upgrade(method, latest)
|
||||
.then(() => Bus.publish(Installation.Event.Updated, { version: latest }))
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import type { InstanceContext } from "@/project/instance"
|
||||
import { SessionID, MessageID } from "@/session/schema"
|
||||
import { Effect, Layer, Context } from "effect"
|
||||
@@ -188,4 +189,10 @@ export namespace Command {
|
||||
Layer.provide(MCP.defaultLayer),
|
||||
Layer.provide(Skill.defaultLayer),
|
||||
)
|
||||
|
||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||
|
||||
export async function list() {
|
||||
return runPromise((svc) => svc.list())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Layer, ManagedRuntime } from "effect"
|
||||
import { memoMap } from "./run-service"
|
||||
|
||||
import { Format } from "@/format"
|
||||
import { ShareNext } from "@/share/share-next"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
|
||||
export const BootstrapLayer = Layer.mergeAll(Format.defaultLayer, ShareNext.defaultLayer, Snapshot.defaultLayer)
|
||||
|
||||
export const BootstrapRuntime = ManagedRuntime.make(BootstrapLayer, { memoMap })
|
||||
@@ -2,6 +2,7 @@ import { Effect, Layer, Context } from "effect"
|
||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import path from "path"
|
||||
import { mergeDeep } from "remeda"
|
||||
import z from "zod"
|
||||
@@ -192,4 +193,18 @@ export namespace Format {
|
||||
Layer.provide(Config.defaultLayer),
|
||||
Layer.provide(CrossSpawnSpawner.defaultLayer),
|
||||
)
|
||||
|
||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||
|
||||
export async function init() {
|
||||
return runPromise((s) => s.init())
|
||||
}
|
||||
|
||||
export async function status() {
|
||||
return runPromise((s) => s.status())
|
||||
}
|
||||
|
||||
export async function file(filepath: string) {
|
||||
return runPromise((s) => s.file(filepath))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
|
||||
import { Effect, Layer, Context, Stream } from "effect"
|
||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
|
||||
export namespace Git {
|
||||
const cfg = [
|
||||
@@ -257,4 +258,14 @@ export namespace Git {
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(CrossSpawnSpawner.defaultLayer))
|
||||
|
||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||
|
||||
export async function run(args: string[], opts: Options) {
|
||||
return runPromise((git) => git.run(args, opts))
|
||||
}
|
||||
|
||||
export async function defaultBranch(cwd: string) {
|
||||
return runPromise((git) => git.defaultBranch(cwd))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Effect, Layer, Schema, Context, Stream } from "effect"
|
||||
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { withTransientReadRetry } from "@/util/effect-http-client"
|
||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||
import path from "path"
|
||||
@@ -337,4 +338,18 @@ export namespace Installation {
|
||||
Layer.provide(FetchHttpClient.layer),
|
||||
Layer.provide(CrossSpawnSpawner.defaultLayer),
|
||||
)
|
||||
|
||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||
|
||||
export async function method(): Promise<Method> {
|
||||
return runPromise((svc) => svc.method())
|
||||
}
|
||||
|
||||
export async function latest(installMethod?: Method): Promise<string> {
|
||||
return runPromise((svc) => svc.latest(installMethod))
|
||||
}
|
||||
|
||||
export async function upgrade(m: Method, target: string): Promise<void> {
|
||||
return runPromise((svc) => svc.upgrade(m, target))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,19 +10,19 @@ import { Bus } from "../bus"
|
||||
import { Command } from "../command"
|
||||
import { Instance } from "./instance"
|
||||
import { Log } from "@/util/log"
|
||||
import { BootstrapRuntime } from "@/effect/bootstrap-runtime"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { ShareNext } from "@/share/share-next"
|
||||
|
||||
export async function InstanceBootstrap() {
|
||||
Log.Default.info("bootstrapping", { directory: Instance.directory })
|
||||
await Plugin.init()
|
||||
void BootstrapRuntime.runPromise(ShareNext.Service.use((svc) => svc.init()))
|
||||
void BootstrapRuntime.runPromise(Format.Service.use((svc) => svc.init()))
|
||||
void AppRuntime.runPromise(ShareNext.Service.use((svc) => svc.init()))
|
||||
Format.init()
|
||||
await LSP.init()
|
||||
File.init()
|
||||
FileWatcher.init()
|
||||
Vcs.init()
|
||||
void BootstrapRuntime.runPromise(Snapshot.Service.use((svc) => svc.init()))
|
||||
Snapshot.init()
|
||||
|
||||
Bus.subscribe(Command.Event.Executed, async (payload) => {
|
||||
if (payload.properties.name === Command.Default.INIT) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { AuthOAuthResult, Hooks } from "@opencode-ai/plugin"
|
||||
import { NamedError } from "@opencode-ai/util/error"
|
||||
import { Auth } from "@/auth"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { Plugin } from "../plugin"
|
||||
import { ProviderID } from "./schema"
|
||||
import { Array as Arr, Effect, Layer, Record, Result, Context } from "effect"
|
||||
@@ -231,4 +232,22 @@ export namespace ProviderAuth {
|
||||
export const defaultLayer = Layer.suspend(() =>
|
||||
layer.pipe(Layer.provide(Auth.defaultLayer), Layer.provide(Plugin.defaultLayer)),
|
||||
)
|
||||
|
||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||
|
||||
export async function methods() {
|
||||
return runPromise((svc) => svc.methods())
|
||||
}
|
||||
|
||||
export async function authorize(input: {
|
||||
providerID: ProviderID
|
||||
method: number
|
||||
inputs?: Record<string, string>
|
||||
}): Promise<Authorization | undefined> {
|
||||
return runPromise((svc) => svc.authorize(input))
|
||||
}
|
||||
|
||||
export async function callback(input: { providerID: ProviderID; method: number; code?: string }) {
|
||||
return runPromise((svc) => svc.callback(input))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ import { ProviderRoutes } from "./routes/provider"
|
||||
import { EventRoutes } from "./routes/event"
|
||||
import { errorHandler } from "./middleware"
|
||||
import { getMimeType } from "hono/utils/mime"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
|
||||
const log = Log.create({ service: "server" })
|
||||
|
||||
@@ -191,7 +190,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket, app: Hono = new Hono()
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const commands = await AppRuntime.runPromise(Command.Service.use((svc) => svc.list()))
|
||||
const commands = await Command.list()
|
||||
return c.json(commands)
|
||||
},
|
||||
)
|
||||
@@ -278,7 +277,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket, app: Hono = new Hono()
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
return c.json(await AppRuntime.runPromise(Format.Service.use((svc) => svc.status())))
|
||||
return c.json(await Format.status())
|
||||
},
|
||||
)
|
||||
.all("/*", async (c) => {
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { Hono, type Context } from "hono"
|
||||
import { describeRoute, resolver, validator } from "hono-openapi"
|
||||
import { streamSSE } from "hono/streaming"
|
||||
import { Effect } from "effect"
|
||||
import z from "zod"
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { SyncEvent } from "@/sync"
|
||||
import { GlobalBus } from "@/bus/global"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { AsyncQueue } from "@/util/queue"
|
||||
import { Instance } from "../../project/instance"
|
||||
import { Installation } from "@/installation"
|
||||
@@ -292,41 +290,25 @@ export const GlobalRoutes = lazy(() =>
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const result = await AppRuntime.runPromise(
|
||||
Installation.Service.use((svc) =>
|
||||
Effect.gen(function* () {
|
||||
const method = yield* svc.method()
|
||||
if (method === "unknown") {
|
||||
return { success: false as const, status: 400 as const, error: "Unknown installation method" }
|
||||
}
|
||||
|
||||
const target = c.req.valid("json").target || (yield* svc.latest(method))
|
||||
const result = yield* Effect.catch(
|
||||
svc.upgrade(method, target).pipe(Effect.as({ success: true as const, version: target })),
|
||||
(err) =>
|
||||
Effect.succeed({
|
||||
success: false as const,
|
||||
status: 500 as const,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
}),
|
||||
)
|
||||
if (!result.success) return result
|
||||
return { ...result, status: 200 as const }
|
||||
}),
|
||||
),
|
||||
)
|
||||
if (!result.success) {
|
||||
return c.json({ success: false, error: result.error }, result.status)
|
||||
const method = await Installation.method()
|
||||
if (method === "unknown") {
|
||||
return c.json({ success: false, error: "Unknown installation method" }, 400)
|
||||
}
|
||||
const target = result.version
|
||||
GlobalBus.emit("event", {
|
||||
directory: "global",
|
||||
payload: {
|
||||
type: Installation.Event.Updated.type,
|
||||
properties: { version: target },
|
||||
},
|
||||
})
|
||||
return c.json({ success: true, version: target })
|
||||
const target = c.req.valid("json").target || (await Installation.latest(method))
|
||||
const result = await Installation.upgrade(method, target)
|
||||
.then(() => ({ success: true as const, version: target }))
|
||||
.catch((e) => ({ success: false as const, error: e instanceof Error ? e.message : String(e) }))
|
||||
if (result.success) {
|
||||
GlobalBus.emit("event", {
|
||||
directory: "global",
|
||||
payload: {
|
||||
type: Installation.Event.Updated.type,
|
||||
properties: { version: target },
|
||||
},
|
||||
})
|
||||
return c.json(result)
|
||||
}
|
||||
return c.json(result, 500)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@@ -6,7 +6,6 @@ import { Provider } from "../../provider/provider"
|
||||
import { ModelsDev } from "../../provider/models"
|
||||
import { ProviderAuth } from "../../provider/auth"
|
||||
import { ProviderID } from "../../provider/schema"
|
||||
import { AppRuntime } from "../../effect/app-runtime"
|
||||
import { mapValues } from "remeda"
|
||||
import { errors } from "../error"
|
||||
import { lazy } from "../../util/lazy"
|
||||
@@ -82,7 +81,7 @@ export const ProviderRoutes = lazy(() =>
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
return c.json(await AppRuntime.runPromise(ProviderAuth.Service.use((svc) => svc.methods())))
|
||||
return c.json(await ProviderAuth.methods())
|
||||
},
|
||||
)
|
||||
.post(
|
||||
@@ -119,15 +118,11 @@ export const ProviderRoutes = lazy(() =>
|
||||
async (c) => {
|
||||
const providerID = c.req.valid("param").providerID
|
||||
const { method, inputs } = c.req.valid("json")
|
||||
const result = await AppRuntime.runPromise(
|
||||
ProviderAuth.Service.use((svc) =>
|
||||
svc.authorize({
|
||||
providerID,
|
||||
method,
|
||||
inputs,
|
||||
}),
|
||||
),
|
||||
)
|
||||
const result = await ProviderAuth.authorize({
|
||||
providerID,
|
||||
method,
|
||||
inputs,
|
||||
})
|
||||
return c.json(result)
|
||||
},
|
||||
)
|
||||
@@ -165,15 +160,11 @@ export const ProviderRoutes = lazy(() =>
|
||||
async (c) => {
|
||||
const providerID = c.req.valid("param").providerID
|
||||
const { method, code } = c.req.valid("json")
|
||||
await AppRuntime.runPromise(
|
||||
ProviderAuth.Service.use((svc) =>
|
||||
svc.callback({
|
||||
providerID,
|
||||
method,
|
||||
code,
|
||||
}),
|
||||
),
|
||||
)
|
||||
await ProviderAuth.callback({
|
||||
providerID,
|
||||
method,
|
||||
code,
|
||||
})
|
||||
return c.json(true)
|
||||
},
|
||||
),
|
||||
|
||||
@@ -353,10 +353,16 @@ export namespace Session {
|
||||
delta: string
|
||||
}) => Effect.Effect<void>
|
||||
/** Finds the first message matching the predicate, searching newest-first. */
|
||||
readonly findMessage: (
|
||||
sessionID: SessionID,
|
||||
predicate: (msg: MessageV2.WithParts) => boolean,
|
||||
) => Effect.Effect<Option.Option<MessageV2.WithParts>>
|
||||
readonly findMessage: {
|
||||
<T extends MessageV2.WithParts>(
|
||||
sessionID: SessionID,
|
||||
predicate: (msg: MessageV2.WithParts) => msg is T,
|
||||
): Effect.Effect<Option.Option<T>>
|
||||
(
|
||||
sessionID: SessionID,
|
||||
predicate: (msg: MessageV2.WithParts) => boolean,
|
||||
): Effect.Effect<Option.Option<MessageV2.WithParts>>
|
||||
}
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Session") {}
|
||||
@@ -850,4 +856,15 @@ export namespace Session {
|
||||
MessageV2.Part.parse(part)
|
||||
return runPromise((svc) => svc.updatePart(part))
|
||||
}
|
||||
|
||||
export const updatePartDelta = fn(
|
||||
z.object({
|
||||
sessionID: SessionID.zod,
|
||||
messageID: MessageID.zod,
|
||||
partID: PartID.zod,
|
||||
field: z.string(),
|
||||
delta: z.string(),
|
||||
}),
|
||||
(input) => runPromise((svc) => svc.updatePartDelta(input)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -364,19 +364,21 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
agent: input.agent.name,
|
||||
messages: input.messages,
|
||||
metadata: (val) =>
|
||||
input.processor.updateToolCall(options.toolCallId, (match) => {
|
||||
if (!["running", "pending"].includes(match.state.status)) return match
|
||||
return {
|
||||
...match,
|
||||
state: {
|
||||
title: val.title,
|
||||
metadata: val.metadata,
|
||||
status: "running",
|
||||
input: args,
|
||||
time: { start: Date.now() },
|
||||
},
|
||||
}
|
||||
}),
|
||||
run.promise(
|
||||
input.processor.updateToolCall(options.toolCallId, (match) => {
|
||||
if (!["running", "pending"].includes(match.state.status)) return match
|
||||
return {
|
||||
...match,
|
||||
state: {
|
||||
title: val.title,
|
||||
metadata: val.metadata,
|
||||
status: "running",
|
||||
input: args,
|
||||
time: { start: Date.now() },
|
||||
},
|
||||
}
|
||||
}),
|
||||
),
|
||||
ask: (req) =>
|
||||
permission
|
||||
.ask({
|
||||
@@ -590,14 +592,17 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
callID: part.callID,
|
||||
extra: { bypassAgentCheck: true, promptOps },
|
||||
messages: msgs,
|
||||
metadata: (val: { title?: string; metadata?: Record<string, any> }) =>
|
||||
Effect.gen(function* () {
|
||||
part = yield* sessions.updatePart({
|
||||
...part,
|
||||
type: "tool",
|
||||
state: { ...part.state, ...val },
|
||||
} satisfies MessageV2.ToolPart)
|
||||
}),
|
||||
metadata(val: { title?: string; metadata?: Record<string, any> }) {
|
||||
return run.promise(
|
||||
Effect.gen(function* () {
|
||||
part = yield* sessions.updatePart({
|
||||
...part,
|
||||
type: "tool",
|
||||
state: { ...part.state, ...val },
|
||||
} satisfies MessageV2.ToolPart)
|
||||
}),
|
||||
)
|
||||
},
|
||||
ask: (req: any) =>
|
||||
permission
|
||||
.ask({
|
||||
@@ -902,8 +907,11 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
})
|
||||
|
||||
const lastModel = Effect.fnUntraced(function* (sessionID: SessionID) {
|
||||
const match = yield* sessions.findMessage(sessionID, (m) => m.info.role === "user" && !!m.info.model)
|
||||
if (Option.isSome(match) && match.value.info.role === "user") return match.value.info.model
|
||||
const match = yield* sessions.findMessage(
|
||||
sessionID,
|
||||
(m): m is MessageV2.WithParts & { info: MessageV2.User } => m.info.role === "user" && !!m.info.model,
|
||||
)
|
||||
if (Option.isSome(match)) return match.value.info.model
|
||||
return yield* provider.defaultModel()
|
||||
})
|
||||
|
||||
@@ -1045,7 +1053,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
messageID: info.id,
|
||||
extra: { bypassCwdCheck: true, ...extra },
|
||||
messages: [],
|
||||
metadata: () => Effect.void,
|
||||
metadata: () => {},
|
||||
ask: () => Effect.void,
|
||||
})
|
||||
.pipe(Effect.onInterrupt(() => Effect.sync(() => controller.abort())))
|
||||
|
||||
@@ -5,6 +5,7 @@ import path from "path"
|
||||
import z from "zod"
|
||||
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { AppFileSystem } from "@/filesystem"
|
||||
import { Hash } from "@/util/hash"
|
||||
import { Config } from "../config/config"
|
||||
@@ -689,4 +690,34 @@ export namespace Snapshot {
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(Config.defaultLayer),
|
||||
)
|
||||
|
||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||
|
||||
export async function init() {
|
||||
return runPromise((svc) => svc.init())
|
||||
}
|
||||
|
||||
export async function track() {
|
||||
return runPromise((svc) => svc.track())
|
||||
}
|
||||
|
||||
export async function patch(hash: string) {
|
||||
return runPromise((svc) => svc.patch(hash))
|
||||
}
|
||||
|
||||
export async function restore(snapshot: string) {
|
||||
return runPromise((svc) => svc.restore(snapshot))
|
||||
}
|
||||
|
||||
export async function revert(patches: Patch[]) {
|
||||
return runPromise((svc) => svc.revert(patches))
|
||||
}
|
||||
|
||||
export async function diff(hash: string) {
|
||||
return runPromise((svc) => svc.diff(hash))
|
||||
}
|
||||
|
||||
export async function diffFull(from: string, to: string) {
|
||||
return runPromise((svc) => svc.diffFull(from, to))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,7 +385,7 @@ export const BashTool = Tool.define(
|
||||
let expired = false
|
||||
let aborted = false
|
||||
|
||||
yield* ctx.metadata({
|
||||
ctx.metadata({
|
||||
metadata: {
|
||||
output: "",
|
||||
description: input.description,
|
||||
@@ -397,15 +397,17 @@ export const BashTool = Tool.define(
|
||||
const handle = yield* spawner.spawn(cmd(input.shell, input.name, input.command, input.cwd, input.env))
|
||||
|
||||
yield* Effect.forkScoped(
|
||||
Stream.runForEach(Stream.decodeText(handle.all), (chunk) => {
|
||||
output += chunk
|
||||
return ctx.metadata({
|
||||
metadata: {
|
||||
output: preview(output),
|
||||
description: input.description,
|
||||
},
|
||||
})
|
||||
}),
|
||||
Stream.runForEach(Stream.decodeText(handle.all), (chunk) =>
|
||||
Effect.sync(() => {
|
||||
output += chunk
|
||||
ctx.metadata({
|
||||
metadata: {
|
||||
output: preview(output),
|
||||
description: input.description,
|
||||
},
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
const abort = Effect.callback<void>((resume) => {
|
||||
|
||||
@@ -158,7 +158,7 @@ export const EditTool = Tool.define(
|
||||
if (change.removed) filediff.deletions += change.count || 0
|
||||
}
|
||||
|
||||
yield* ctx.metadata({
|
||||
ctx.metadata({
|
||||
metadata: {
|
||||
diff,
|
||||
filediff,
|
||||
|
||||
@@ -30,6 +30,7 @@ import { Glob } from "../util/glob"
|
||||
import path from "path"
|
||||
import { pathToFileURL } from "url"
|
||||
import { Effect, Layer, Context } from "effect"
|
||||
import { EffectLogger } from "@/effect/logger"
|
||||
import { FetchHttpClient, HttpClient } from "effect/unstable/http"
|
||||
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
|
||||
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
|
||||
@@ -136,7 +137,7 @@ export namespace ToolRegistry {
|
||||
Effect.gen(function* () {
|
||||
const pluginCtx: PluginToolContext = {
|
||||
...toolCtx,
|
||||
ask: (req) => toolCtx.ask(req),
|
||||
ask: (req) => Effect.runPromise(toolCtx.ask(req).pipe(Effect.provide(EffectLogger.layer))),
|
||||
directory: ctx.directory,
|
||||
worktree: ctx.worktree,
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@ export const TaskTool = Tool.define(
|
||||
Effect.gen(function* () {
|
||||
const agent = yield* Agent.Service
|
||||
const config = yield* Config.Service
|
||||
const sessions = yield* Session.Service
|
||||
|
||||
const run = Effect.fn("TaskTool.execute")(function* (params: z.infer<typeof parameters>, ctx: Tool.Context) {
|
||||
const cfg = yield* config.get()
|
||||
@@ -63,39 +62,44 @@ export const TaskTool = Tool.define(
|
||||
|
||||
const taskID = params.task_id
|
||||
const session = taskID
|
||||
? yield* sessions.get(SessionID.make(taskID)).pipe(Effect.catchCause(() => Effect.succeed(undefined)))
|
||||
? yield* Effect.promise(() => {
|
||||
const id = SessionID.make(taskID)
|
||||
return Session.get(id).catch(() => undefined)
|
||||
})
|
||||
: undefined
|
||||
const nextSession =
|
||||
session ??
|
||||
(yield* sessions.create({
|
||||
parentID: ctx.sessionID,
|
||||
title: params.description + ` (@${next.name} subagent)`,
|
||||
permission: [
|
||||
...(canTodo
|
||||
? []
|
||||
: [
|
||||
{
|
||||
permission: "todowrite" as const,
|
||||
pattern: "*" as const,
|
||||
action: "deny" as const,
|
||||
},
|
||||
]),
|
||||
...(canTask
|
||||
? []
|
||||
: [
|
||||
{
|
||||
permission: id,
|
||||
pattern: "*" as const,
|
||||
action: "deny" as const,
|
||||
},
|
||||
]),
|
||||
...(cfg.experimental?.primary_tools?.map((item) => ({
|
||||
pattern: "*",
|
||||
action: "allow" as const,
|
||||
permission: item,
|
||||
})) ?? []),
|
||||
],
|
||||
}))
|
||||
(yield* Effect.promise(() =>
|
||||
Session.create({
|
||||
parentID: ctx.sessionID,
|
||||
title: params.description + ` (@${next.name} subagent)`,
|
||||
permission: [
|
||||
...(canTodo
|
||||
? []
|
||||
: [
|
||||
{
|
||||
permission: "todowrite" as const,
|
||||
pattern: "*" as const,
|
||||
action: "deny" as const,
|
||||
},
|
||||
]),
|
||||
...(canTask
|
||||
? []
|
||||
: [
|
||||
{
|
||||
permission: id,
|
||||
pattern: "*" as const,
|
||||
action: "deny" as const,
|
||||
},
|
||||
]),
|
||||
...(cfg.experimental?.primary_tools?.map((item) => ({
|
||||
pattern: "*",
|
||||
action: "allow" as const,
|
||||
permission: item,
|
||||
})) ?? []),
|
||||
],
|
||||
}),
|
||||
))
|
||||
|
||||
const msg = yield* Effect.sync(() => MessageV2.get({ sessionID: ctx.sessionID, messageID: ctx.messageID }))
|
||||
if (msg.info.role !== "assistant") return yield* Effect.fail(new Error("Not an assistant message"))
|
||||
@@ -105,7 +109,7 @@ export const TaskTool = Tool.define(
|
||||
providerID: msg.info.providerID,
|
||||
}
|
||||
|
||||
yield* ctx.metadata({
|
||||
ctx.metadata({
|
||||
title: params.description,
|
||||
metadata: {
|
||||
sessionId: nextSession.id,
|
||||
|
||||
@@ -22,7 +22,7 @@ export namespace Tool {
|
||||
callID?: string
|
||||
extra?: { [key: string]: any }
|
||||
messages: MessageV2.WithParts[]
|
||||
metadata(input: { title?: string; metadata?: M }): Effect.Effect<void>
|
||||
metadata(input: { title?: string; metadata?: M }): void
|
||||
ask(input: Omit<Permission.Request, "id" | "sessionID" | "tool">): Effect.Effect<void>
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { Effect } from "effect"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { ProviderAuth } from "../../src/provider/auth"
|
||||
@@ -40,18 +39,14 @@ describe("plugin.auth-override", () => {
|
||||
const methods = await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
return Effect.runPromise(
|
||||
ProviderAuth.Service.use((svc) => svc.methods()).pipe(Effect.provide(ProviderAuth.defaultLayer)),
|
||||
)
|
||||
return ProviderAuth.methods()
|
||||
},
|
||||
})
|
||||
|
||||
const plainMethods = await Instance.provide({
|
||||
directory: plain.path,
|
||||
fn: async () => {
|
||||
return Effect.runPromise(
|
||||
ProviderAuth.Service.use((svc) => svc.methods()).pipe(Effect.provide(ProviderAuth.defaultLayer)),
|
||||
)
|
||||
return ProviderAuth.methods()
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import { afterEach, describe, expect, spyOn, test } from "bun:test"
|
||||
import path from "path"
|
||||
import { GlobalBus } from "../../src/bus/global"
|
||||
import { Snapshot } from "../../src/snapshot"
|
||||
import { AppRuntime } from "../../src/effect/app-runtime"
|
||||
import { InstanceBootstrap } from "../../src/project/bootstrap"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Server } from "../../src/server/server"
|
||||
@@ -65,7 +64,7 @@ describe("project.initGit endpoint", () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
expect(await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))).toBeTruthy()
|
||||
expect(await Snapshot.track()).toBeTruthy()
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
|
||||
@@ -7,7 +7,6 @@ import { SessionRevert } from "../../src/session/revert"
|
||||
import { SessionCompaction } from "../../src/session/compaction"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
import { Snapshot } from "../../src/snapshot"
|
||||
import { AppRuntime } from "../../src/effect/app-runtime"
|
||||
import { Log } from "../../src/util/log"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { MessageID, PartID } from "../../src/session/schema"
|
||||
@@ -461,12 +460,12 @@ describe("revert + compact workflow", () => {
|
||||
const u = await user(sid)
|
||||
await text(sid, u.id, `${file}:${next}`)
|
||||
const a = await assistant(sid, u.id, tmp.path)
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
if (!before) throw new Error("expected snapshot")
|
||||
await fs.writeFile(path.join(tmp.path, file), next)
|
||||
const after = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const after = await Snapshot.track()
|
||||
if (!after) throw new Error("expected snapshot")
|
||||
const patch = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(before)))
|
||||
const patch = await Snapshot.patch(before)
|
||||
await Session.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: a.id,
|
||||
@@ -551,12 +550,12 @@ describe("revert + compact workflow", () => {
|
||||
const u = await user(sid)
|
||||
await text(sid, u.id, `a.txt:${next}`)
|
||||
const a = await assistant(sid, u.id, tmp.path)
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
if (!before) throw new Error("expected snapshot")
|
||||
await fs.writeFile(path.join(tmp.path, "a.txt"), next)
|
||||
const after = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const after = await Snapshot.track()
|
||||
if (!after) throw new Error("expected snapshot")
|
||||
const patch = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(before)))
|
||||
const patch = await Snapshot.patch(before)
|
||||
await Session.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: a.id,
|
||||
|
||||
@@ -3,12 +3,11 @@ import { $ } from "bun"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { Snapshot } from "../../src/snapshot"
|
||||
import { AppRuntime } from "../../src/effect/app-runtime"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Filesystem } from "../../src/util/filesystem"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
// Git always outputs /-separated paths internally. AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch())) joins them
|
||||
// Git always outputs /-separated paths internally. Snapshot.patch() joins them
|
||||
// with path.join (which produces \ on Windows) then normalizes back to /.
|
||||
// This helper does the same for expected values so assertions match cross-platform.
|
||||
const fwd = (...parts: string[]) => path.join(...parts).replaceAll("\\", "/")
|
||||
@@ -41,12 +40,12 @@ test("tracks deleted files correctly", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await $`rm ${tmp.path}/a.txt`.quiet()
|
||||
|
||||
expect((await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(before!)))).files).toContain(fwd(tmp.path, "a.txt"))
|
||||
expect((await Snapshot.patch(before!)).files).toContain(fwd(tmp.path, "a.txt"))
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -56,13 +55,12 @@ test("revert should remove new files", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await Filesystem.write(`${tmp.path}/new.txt`, "NEW")
|
||||
|
||||
const patch = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(before!)))
|
||||
await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.revert([patch])))
|
||||
await Snapshot.revert([await Snapshot.patch(before!)])
|
||||
|
||||
expect(
|
||||
await fs
|
||||
@@ -79,14 +77,13 @@ test("revert in subdirectory", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await $`mkdir -p ${tmp.path}/sub`.quiet()
|
||||
await Filesystem.write(`${tmp.path}/sub/file.txt`, "SUB")
|
||||
|
||||
const patch = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(before!)))
|
||||
await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.revert([patch])))
|
||||
await Snapshot.revert([await Snapshot.patch(before!)])
|
||||
|
||||
expect(
|
||||
await fs
|
||||
@@ -105,7 +102,7 @@ test("multiple file operations", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await $`rm ${tmp.path}/a.txt`.quiet()
|
||||
@@ -114,8 +111,7 @@ test("multiple file operations", async () => {
|
||||
await Filesystem.write(`${tmp.path}/dir/d.txt`, "D")
|
||||
await Filesystem.write(`${tmp.path}/b.txt`, "MODIFIED")
|
||||
|
||||
const patch = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(before!)))
|
||||
await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.revert([patch])))
|
||||
await Snapshot.revert([await Snapshot.patch(before!)])
|
||||
|
||||
expect(await fs.readFile(`${tmp.path}/a.txt`, "utf-8")).toBe(tmp.extra.aContent)
|
||||
expect(
|
||||
@@ -136,12 +132,12 @@ test("empty directory handling", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await $`mkdir ${tmp.path}/empty`.quiet()
|
||||
|
||||
expect((await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(before!)))).files.length).toBe(0)
|
||||
expect((await Snapshot.patch(before!)).files.length).toBe(0)
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -151,15 +147,15 @@ test("binary file handling", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await Filesystem.write(`${tmp.path}/image.png`, new Uint8Array([0x89, 0x50, 0x4e, 0x47]))
|
||||
|
||||
const patch = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(before!)))
|
||||
const patch = await Snapshot.patch(before!)
|
||||
expect(patch.files).toContain(fwd(tmp.path, "image.png"))
|
||||
|
||||
await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.revert([patch])))
|
||||
await Snapshot.revert([patch])
|
||||
expect(
|
||||
await fs
|
||||
.access(`${tmp.path}/image.png`)
|
||||
@@ -175,12 +171,12 @@ test("symlink handling", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await fs.symlink(`${tmp.path}/a.txt`, `${tmp.path}/link.txt`, "file")
|
||||
|
||||
expect((await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(before!)))).files).toContain(fwd(tmp.path, "link.txt"))
|
||||
expect((await Snapshot.patch(before!)).files).toContain(fwd(tmp.path, "link.txt"))
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -190,12 +186,12 @@ test("file under size limit handling", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await Filesystem.write(`${tmp.path}/large.txt`, "x".repeat(1024 * 1024))
|
||||
|
||||
expect((await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(before!)))).files).toContain(fwd(tmp.path, "large.txt"))
|
||||
expect((await Snapshot.patch(before!)).files).toContain(fwd(tmp.path, "large.txt"))
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -205,14 +201,14 @@ test("large added files are skipped", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await Filesystem.write(`${tmp.path}/huge.txt`, new Uint8Array(2 * 1024 * 1024 + 1))
|
||||
|
||||
expect((await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(before!)))).files).toEqual([])
|
||||
expect(await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.diff(before!)))).toBe("")
|
||||
expect(await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))).toBe(before)
|
||||
expect((await Snapshot.patch(before!)).files).toEqual([])
|
||||
expect(await Snapshot.diff(before!)).toBe("")
|
||||
expect(await Snapshot.track()).toBe(before)
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -222,14 +218,13 @@ test("nested directory revert", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await $`mkdir -p ${tmp.path}/level1/level2/level3`.quiet()
|
||||
await Filesystem.write(`${tmp.path}/level1/level2/level3/deep.txt`, "DEEP")
|
||||
|
||||
const patch = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(before!)))
|
||||
await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.revert([patch])))
|
||||
await Snapshot.revert([await Snapshot.patch(before!)])
|
||||
|
||||
expect(
|
||||
await fs
|
||||
@@ -246,14 +241,14 @@ test("special characters in filenames", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await Filesystem.write(`${tmp.path}/file with spaces.txt`, "SPACES")
|
||||
await Filesystem.write(`${tmp.path}/file-with-dashes.txt`, "DASHES")
|
||||
await Filesystem.write(`${tmp.path}/file_with_underscores.txt`, "UNDERSCORES")
|
||||
|
||||
const files = (await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(before!)))).files
|
||||
const files = (await Snapshot.patch(before!)).files
|
||||
expect(files).toContain(fwd(tmp.path, "file with spaces.txt"))
|
||||
expect(files).toContain(fwd(tmp.path, "file-with-dashes.txt"))
|
||||
expect(files).toContain(fwd(tmp.path, "file_with_underscores.txt"))
|
||||
@@ -267,10 +262,10 @@ test("revert with empty patches", async () => {
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
// Should not crash with empty patches
|
||||
expect(AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.revert([])))).resolves.toBeUndefined()
|
||||
expect(Snapshot.revert([])).resolves.toBeUndefined()
|
||||
|
||||
// Should not crash with patches that have empty file lists
|
||||
expect(AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.revert([{ hash: "dummy", files: [] }])))).resolves.toBeUndefined()
|
||||
expect(Snapshot.revert([{ hash: "dummy", files: [] }])).resolves.toBeUndefined()
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -280,14 +275,14 @@ test("patch with invalid hash", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
// Create a change
|
||||
await Filesystem.write(`${tmp.path}/test.txt`, "TEST")
|
||||
|
||||
// Try to patch with invalid hash - should handle gracefully
|
||||
const patch = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch("invalid-hash-12345")))
|
||||
const patch = await Snapshot.patch("invalid-hash-12345")
|
||||
expect(patch.files).toEqual([])
|
||||
expect(patch.hash).toBe("invalid-hash-12345")
|
||||
},
|
||||
@@ -299,18 +294,18 @@ test("revert non-existent file", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
// Try to revert a file that doesn't exist in the snapshot
|
||||
// This should not crash
|
||||
expect(
|
||||
AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.revert([
|
||||
Snapshot.revert([
|
||||
{
|
||||
hash: before!,
|
||||
files: [`${tmp.path}/nonexistent.txt`],
|
||||
},
|
||||
]))),
|
||||
]),
|
||||
).resolves.toBeUndefined()
|
||||
},
|
||||
})
|
||||
@@ -321,7 +316,7 @@ test("unicode filenames", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
const unicodeFiles = [
|
||||
@@ -335,14 +330,14 @@ test("unicode filenames", async () => {
|
||||
await Filesystem.write(file.path, file.content)
|
||||
}
|
||||
|
||||
const patch = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(before!)))
|
||||
const patch = await Snapshot.patch(before!)
|
||||
expect(patch.files.length).toBe(4)
|
||||
|
||||
for (const file of unicodeFiles) {
|
||||
expect(patch.files).toContain(file.path)
|
||||
}
|
||||
|
||||
await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.revert([patch])))
|
||||
await Snapshot.revert([patch])
|
||||
|
||||
for (const file of unicodeFiles) {
|
||||
expect(
|
||||
@@ -367,17 +362,17 @@ test.skip("unicode filenames modification and restore", async () => {
|
||||
await Filesystem.write(chineseFile, "original chinese")
|
||||
await Filesystem.write(cyrillicFile, "original cyrillic")
|
||||
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await Filesystem.write(chineseFile, "modified chinese")
|
||||
await Filesystem.write(cyrillicFile, "modified cyrillic")
|
||||
|
||||
const patch = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(before!)))
|
||||
const patch = await Snapshot.patch(before!)
|
||||
expect(patch.files).toContain(chineseFile)
|
||||
expect(patch.files).toContain(cyrillicFile)
|
||||
|
||||
await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.revert([patch])))
|
||||
await Snapshot.revert([patch])
|
||||
|
||||
expect(await fs.readFile(chineseFile, "utf-8")).toBe("original chinese")
|
||||
expect(await fs.readFile(cyrillicFile, "utf-8")).toBe("original cyrillic")
|
||||
@@ -390,17 +385,17 @@ test("unicode filenames in subdirectories", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await $`mkdir -p "${tmp.path}/目录/подкаталог"`.quiet()
|
||||
const deepFile = fwd(tmp.path, "目录", "подкаталог", "文件.txt")
|
||||
await Filesystem.write(deepFile, "deep unicode content")
|
||||
|
||||
const patch = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(before!)))
|
||||
const patch = await Snapshot.patch(before!)
|
||||
expect(patch.files).toContain(deepFile)
|
||||
|
||||
await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.revert([patch])))
|
||||
await Snapshot.revert([patch])
|
||||
expect(
|
||||
await fs
|
||||
.access(deepFile)
|
||||
@@ -416,7 +411,7 @@ test("very long filenames", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
const longName = "a".repeat(200) + ".txt"
|
||||
@@ -424,10 +419,10 @@ test("very long filenames", async () => {
|
||||
|
||||
await Filesystem.write(longFile, "long filename content")
|
||||
|
||||
const patch = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(before!)))
|
||||
const patch = await Snapshot.patch(before!)
|
||||
expect(patch.files).toContain(longFile)
|
||||
|
||||
await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.revert([patch])))
|
||||
await Snapshot.revert([patch])
|
||||
expect(
|
||||
await fs
|
||||
.access(longFile)
|
||||
@@ -443,14 +438,14 @@ test("hidden files", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await Filesystem.write(`${tmp.path}/.hidden`, "hidden content")
|
||||
await Filesystem.write(`${tmp.path}/.gitignore`, "*.log")
|
||||
await Filesystem.write(`${tmp.path}/.config`, "config content")
|
||||
|
||||
const patch = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(before!)))
|
||||
const patch = await Snapshot.patch(before!)
|
||||
expect(patch.files).toContain(fwd(tmp.path, ".hidden"))
|
||||
expect(patch.files).toContain(fwd(tmp.path, ".gitignore"))
|
||||
expect(patch.files).toContain(fwd(tmp.path, ".config"))
|
||||
@@ -463,7 +458,7 @@ test("nested symlinks", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await $`mkdir -p ${tmp.path}/sub/dir`.quiet()
|
||||
@@ -471,7 +466,7 @@ test("nested symlinks", async () => {
|
||||
await fs.symlink(`${tmp.path}/sub/dir/target.txt`, `${tmp.path}/sub/dir/link.txt`, "file")
|
||||
await fs.symlink(`${tmp.path}/sub`, `${tmp.path}/sub-link`, "dir")
|
||||
|
||||
const patch = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(before!)))
|
||||
const patch = await Snapshot.patch(before!)
|
||||
expect(patch.files).toContain(fwd(tmp.path, "sub", "dir", "link.txt"))
|
||||
expect(patch.files).toContain(fwd(tmp.path, "sub-link"))
|
||||
},
|
||||
@@ -483,7 +478,7 @@ test("file permissions and ownership changes", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
// Change permissions multiple times
|
||||
@@ -491,7 +486,7 @@ test("file permissions and ownership changes", async () => {
|
||||
await $`chmod 755 ${tmp.path}/a.txt`.quiet()
|
||||
await $`chmod 644 ${tmp.path}/a.txt`.quiet()
|
||||
|
||||
const patch = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(before!)))
|
||||
const patch = await Snapshot.patch(before!)
|
||||
// Note: git doesn't track permission changes on existing files by default
|
||||
// Only tracks executable bit when files are first added
|
||||
expect(patch.files.length).toBe(0)
|
||||
@@ -504,13 +499,13 @@ test("circular symlinks", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
// Create circular symlink
|
||||
await fs.symlink(`${tmp.path}/circular`, `${tmp.path}/circular`, "dir").catch(() => {})
|
||||
|
||||
const patch = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(before!)))
|
||||
const patch = await Snapshot.patch(before!)
|
||||
expect(patch.files.length).toBeGreaterThanOrEqual(0) // Should not crash
|
||||
},
|
||||
})
|
||||
@@ -521,14 +516,14 @@ test("gitignore changes", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await Filesystem.write(`${tmp.path}/.gitignore`, "*.ignored")
|
||||
await Filesystem.write(`${tmp.path}/test.ignored`, "ignored content")
|
||||
await Filesystem.write(`${tmp.path}/normal.txt`, "normal content")
|
||||
|
||||
const patch = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(before!)))
|
||||
const patch = await Snapshot.patch(before!)
|
||||
|
||||
// Should track gitignore itself
|
||||
expect(patch.files).toContain(fwd(tmp.path, ".gitignore"))
|
||||
@@ -545,7 +540,7 @@ test("git info exclude changes", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
const file = `${tmp.path}/.git/info/exclude`
|
||||
@@ -554,12 +549,12 @@ test("git info exclude changes", async () => {
|
||||
await Bun.write(`${tmp.path}/ignored.txt`, "ignored content")
|
||||
await Bun.write(`${tmp.path}/normal.txt`, "normal content")
|
||||
|
||||
const patch = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(before!)))
|
||||
const patch = await Snapshot.patch(before!)
|
||||
expect(patch.files).toContain(fwd(tmp.path, "normal.txt"))
|
||||
expect(patch.files).not.toContain(fwd(tmp.path, "ignored.txt"))
|
||||
|
||||
const after = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const diffs = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.diffFull(before!, after!)))
|
||||
const after = await Snapshot.track()
|
||||
const diffs = await Snapshot.diffFull(before!, after!)
|
||||
expect(diffs.some((x) => x.file === "normal.txt")).toBe(true)
|
||||
expect(diffs.some((x) => x.file === "ignored.txt")).toBe(false)
|
||||
},
|
||||
@@ -579,7 +574,7 @@ test("git info exclude keeps global excludes", async () => {
|
||||
const prev = process.env.GIT_CONFIG_GLOBAL
|
||||
process.env.GIT_CONFIG_GLOBAL = config
|
||||
try {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
const file = `${tmp.path}/.git/info/exclude`
|
||||
@@ -590,7 +585,7 @@ test("git info exclude keeps global excludes", async () => {
|
||||
await Bun.write(`${tmp.path}/info.tmp`, "info content")
|
||||
await Bun.write(`${tmp.path}/normal.txt`, "normal content")
|
||||
|
||||
const patch = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(before!)))
|
||||
const patch = await Snapshot.patch(before!)
|
||||
expect(patch.files).toContain(fwd(tmp.path, "normal.txt"))
|
||||
expect(patch.files).not.toContain(fwd(tmp.path, "global.tmp"))
|
||||
expect(patch.files).not.toContain(fwd(tmp.path, "info.tmp"))
|
||||
@@ -607,7 +602,7 @@ test("concurrent file operations during patch", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
// Start creating files
|
||||
@@ -620,7 +615,7 @@ test("concurrent file operations during patch", async () => {
|
||||
})()
|
||||
|
||||
// Get patch while files are being created
|
||||
const patchPromise = AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(before!)))
|
||||
const patchPromise = Snapshot.patch(before!)
|
||||
|
||||
await createPromise
|
||||
const patch = await patchPromise
|
||||
@@ -639,9 +634,9 @@ test("snapshot state isolation between projects", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp1.path,
|
||||
fn: async () => {
|
||||
const before1 = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before1 = await Snapshot.track()
|
||||
await Filesystem.write(`${tmp1.path}/project1.txt`, "project1 content")
|
||||
const patch1 = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(before1!)))
|
||||
const patch1 = await Snapshot.patch(before1!)
|
||||
expect(patch1.files).toContain(fwd(tmp1.path, "project1.txt"))
|
||||
},
|
||||
})
|
||||
@@ -649,9 +644,9 @@ test("snapshot state isolation between projects", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp2.path,
|
||||
fn: async () => {
|
||||
const before2 = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before2 = await Snapshot.track()
|
||||
await Filesystem.write(`${tmp2.path}/project2.txt`, "project2 content")
|
||||
const patch2 = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(before2!)))
|
||||
const patch2 = await Snapshot.patch(before2!)
|
||||
expect(patch2.files).toContain(fwd(tmp2.path, "project2.txt"))
|
||||
|
||||
// Ensure project1 files don't appear in project2
|
||||
@@ -669,20 +664,20 @@ test("patch detects changes in secondary worktree", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
expect(await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))).toBeTruthy()
|
||||
expect(await Snapshot.track()).toBeTruthy()
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: worktreePath,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
const worktreeFile = fwd(worktreePath, "worktree.txt")
|
||||
await Filesystem.write(worktreeFile, "worktree content")
|
||||
|
||||
const patch = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(before!)))
|
||||
const patch = await Snapshot.patch(before!)
|
||||
expect(patch.files).toContain(worktreeFile)
|
||||
},
|
||||
})
|
||||
@@ -701,7 +696,7 @@ test("revert only removes files in invoking worktree", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
expect(await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))).toBeTruthy()
|
||||
expect(await Snapshot.track()).toBeTruthy()
|
||||
},
|
||||
})
|
||||
const primaryFile = `${tmp.path}/worktree.txt`
|
||||
@@ -710,14 +705,14 @@ test("revert only removes files in invoking worktree", async () => {
|
||||
await Instance.provide({
|
||||
directory: worktreePath,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
const worktreeFile = fwd(worktreePath, "worktree.txt")
|
||||
await Filesystem.write(worktreeFile, "worktree content")
|
||||
|
||||
const patch = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(before!)))
|
||||
await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.revert([patch])))
|
||||
const patch = await Snapshot.patch(before!)
|
||||
await Snapshot.revert([patch])
|
||||
|
||||
expect(
|
||||
await fs
|
||||
@@ -745,14 +740,14 @@ test("diff reports worktree-only/shared edits and ignores primary-only", async (
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
expect(await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))).toBeTruthy()
|
||||
expect(await Snapshot.track()).toBeTruthy()
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: worktreePath,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await Filesystem.write(`${worktreePath}/worktree-only.txt`, "worktree diff content")
|
||||
@@ -760,7 +755,7 @@ test("diff reports worktree-only/shared edits and ignores primary-only", async (
|
||||
await Filesystem.write(`${tmp.path}/shared.txt`, "primary edit")
|
||||
await Filesystem.write(`${tmp.path}/primary-only.txt`, "primary change")
|
||||
|
||||
const diff = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.diff(before!)))
|
||||
const diff = await Snapshot.diff(before!)
|
||||
expect(diff).toContain("worktree-only.txt")
|
||||
expect(diff).toContain("shared.txt")
|
||||
expect(diff).not.toContain("primary-only.txt")
|
||||
@@ -779,15 +774,15 @@ test("track with no changes returns same hash", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const hash1 = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const hash1 = await Snapshot.track()
|
||||
expect(hash1).toBeTruthy()
|
||||
|
||||
// Track again with no changes
|
||||
const hash2 = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const hash2 = await Snapshot.track()
|
||||
expect(hash2).toBe(hash1!)
|
||||
|
||||
// Track again
|
||||
const hash3 = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const hash3 = await Snapshot.track()
|
||||
expect(hash3).toBe(hash1!)
|
||||
},
|
||||
})
|
||||
@@ -798,7 +793,7 @@ test("diff function with various changes", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
// Make various changes
|
||||
@@ -806,7 +801,7 @@ test("diff function with various changes", async () => {
|
||||
await Filesystem.write(`${tmp.path}/new.txt`, "new content")
|
||||
await Filesystem.write(`${tmp.path}/b.txt`, "modified content")
|
||||
|
||||
const diff = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.diff(before!)))
|
||||
const diff = await Snapshot.diff(before!)
|
||||
expect(diff).toContain("a.txt")
|
||||
expect(diff).toContain("b.txt")
|
||||
expect(diff).toContain("new.txt")
|
||||
@@ -819,7 +814,7 @@ test("restore function", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
// Make changes
|
||||
@@ -828,7 +823,7 @@ test("restore function", async () => {
|
||||
await Filesystem.write(`${tmp.path}/b.txt`, "modified")
|
||||
|
||||
// Restore to original state
|
||||
await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.restore(before!)))
|
||||
await Snapshot.restore(before!)
|
||||
|
||||
expect(
|
||||
await fs
|
||||
@@ -853,20 +848,20 @@ test("revert should not delete files that existed but were deleted in snapshot",
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const snapshot1 = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const snapshot1 = await Snapshot.track()
|
||||
expect(snapshot1).toBeTruthy()
|
||||
|
||||
await $`rm ${tmp.path}/a.txt`.quiet()
|
||||
|
||||
const snapshot2 = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const snapshot2 = await Snapshot.track()
|
||||
expect(snapshot2).toBeTruthy()
|
||||
|
||||
await Filesystem.write(`${tmp.path}/a.txt`, "recreated content")
|
||||
|
||||
const patch = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(snapshot2!)))
|
||||
const patch = await Snapshot.patch(snapshot2!)
|
||||
expect(patch.files).toContain(fwd(tmp.path, "a.txt"))
|
||||
|
||||
await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.revert([patch])))
|
||||
await Snapshot.revert([patch])
|
||||
|
||||
expect(
|
||||
await fs
|
||||
@@ -885,18 +880,18 @@ test("revert preserves file that existed in snapshot when deleted then recreated
|
||||
fn: async () => {
|
||||
await Filesystem.write(`${tmp.path}/existing.txt`, "original content")
|
||||
|
||||
const snapshot = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const snapshot = await Snapshot.track()
|
||||
expect(snapshot).toBeTruthy()
|
||||
|
||||
await $`rm ${tmp.path}/existing.txt`.quiet()
|
||||
await Filesystem.write(`${tmp.path}/existing.txt`, "recreated")
|
||||
await Filesystem.write(`${tmp.path}/newfile.txt`, "new")
|
||||
|
||||
const patch = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(snapshot!)))
|
||||
const patch = await Snapshot.patch(snapshot!)
|
||||
expect(patch.files).toContain(fwd(tmp.path, "existing.txt"))
|
||||
expect(patch.files).toContain(fwd(tmp.path, "newfile.txt"))
|
||||
|
||||
await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.revert([patch])))
|
||||
await Snapshot.revert([patch])
|
||||
|
||||
expect(
|
||||
await fs
|
||||
@@ -924,7 +919,7 @@ test("diffFull sets status based on git change type", async () => {
|
||||
await Filesystem.write(`${tmp.path}/trim.txt`, "line1\nline2\n")
|
||||
await Filesystem.write(`${tmp.path}/delete.txt`, "gone")
|
||||
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await Filesystem.write(`${tmp.path}/grow.txt`, "one\ntwo\n")
|
||||
@@ -932,10 +927,10 @@ test("diffFull sets status based on git change type", async () => {
|
||||
await $`rm ${tmp.path}/delete.txt`.quiet()
|
||||
await Filesystem.write(`${tmp.path}/added.txt`, "new")
|
||||
|
||||
const after = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const after = await Snapshot.track()
|
||||
expect(after).toBeTruthy()
|
||||
|
||||
const diffs = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.diffFull(before!, after!)))
|
||||
const diffs = await Snapshot.diffFull(before!, after!)
|
||||
expect(diffs.length).toBe(4)
|
||||
|
||||
const added = diffs.find((d) => d.file === "added.txt")
|
||||
@@ -966,15 +961,15 @@ test("diffFull with new file additions", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await Filesystem.write(`${tmp.path}/new.txt`, "new content")
|
||||
|
||||
const after = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const after = await Snapshot.track()
|
||||
expect(after).toBeTruthy()
|
||||
|
||||
const diffs = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.diffFull(before!, after!)))
|
||||
const diffs = await Snapshot.diffFull(before!, after!)
|
||||
expect(diffs.length).toBe(1)
|
||||
|
||||
const newFileDiff = diffs[0]
|
||||
@@ -1004,7 +999,7 @@ test("diffFull with a large interleaved mixed diff", async () => {
|
||||
...bin.map((file, i) => Filesystem.write(file, new Uint8Array([0, i, 255, i % 251]))),
|
||||
])
|
||||
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await Promise.all([
|
||||
@@ -1014,10 +1009,10 @@ test("diffFull with a large interleaved mixed diff", async () => {
|
||||
...del.map((file) => fs.rm(file)),
|
||||
])
|
||||
|
||||
const after = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const after = await Snapshot.track()
|
||||
expect(after).toBeTruthy()
|
||||
|
||||
const diffs = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.diffFull(before!, after!)))
|
||||
const diffs = await Snapshot.diffFull(before!, after!)
|
||||
expect(diffs).toHaveLength(ids.length * 4)
|
||||
|
||||
const map = new Map(diffs.map((item) => [item.file, item]))
|
||||
@@ -1059,17 +1054,17 @@ test("diffFull preserves git diff order across batch boundaries", async () => {
|
||||
await $`mkdir -p ${tmp.path}/order`.quiet()
|
||||
await Promise.all(ids.map((id) => Filesystem.write(`${tmp.path}/order/${id}.txt`, `before-${id}`)))
|
||||
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await Promise.all(ids.map((id) => Filesystem.write(`${tmp.path}/order/${id}.txt`, `after-${id}`)))
|
||||
|
||||
const after = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const after = await Snapshot.track()
|
||||
expect(after).toBeTruthy()
|
||||
|
||||
const expected = ids.map((id) => `order/${id}.txt`)
|
||||
|
||||
const diffs = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.diffFull(before!, after!)))
|
||||
const diffs = await Snapshot.diffFull(before!, after!)
|
||||
expect(diffs.map((item) => item.file)).toEqual(expected)
|
||||
},
|
||||
})
|
||||
@@ -1080,15 +1075,15 @@ test("diffFull with file modifications", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await Filesystem.write(`${tmp.path}/b.txt`, "modified content")
|
||||
|
||||
const after = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const after = await Snapshot.track()
|
||||
expect(after).toBeTruthy()
|
||||
|
||||
const diffs = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.diffFull(before!, after!)))
|
||||
const diffs = await Snapshot.diffFull(before!, after!)
|
||||
expect(diffs.length).toBe(1)
|
||||
|
||||
const modifiedFileDiff = diffs[0]
|
||||
@@ -1106,15 +1101,15 @@ test("diffFull with file deletions", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await $`rm ${tmp.path}/a.txt`.quiet()
|
||||
|
||||
const after = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const after = await Snapshot.track()
|
||||
expect(after).toBeTruthy()
|
||||
|
||||
const diffs = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.diffFull(before!, after!)))
|
||||
const diffs = await Snapshot.diffFull(before!, after!)
|
||||
expect(diffs.length).toBe(1)
|
||||
|
||||
const removedFileDiff = diffs[0]
|
||||
@@ -1131,15 +1126,15 @@ test("diffFull with multiple line additions", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await Filesystem.write(`${tmp.path}/multi.txt`, "line1\nline2\nline3")
|
||||
|
||||
const after = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const after = await Snapshot.track()
|
||||
expect(after).toBeTruthy()
|
||||
|
||||
const diffs = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.diffFull(before!, after!)))
|
||||
const diffs = await Snapshot.diffFull(before!, after!)
|
||||
expect(diffs.length).toBe(1)
|
||||
|
||||
const multiDiff = diffs[0]
|
||||
@@ -1157,16 +1152,16 @@ test("diffFull with addition and deletion", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await Filesystem.write(`${tmp.path}/added.txt`, "added content")
|
||||
await $`rm ${tmp.path}/a.txt`.quiet()
|
||||
|
||||
const after = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const after = await Snapshot.track()
|
||||
expect(after).toBeTruthy()
|
||||
|
||||
const diffs = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.diffFull(before!, after!)))
|
||||
const diffs = await Snapshot.diffFull(before!, after!)
|
||||
expect(diffs.length).toBe(2)
|
||||
|
||||
const addedFileDiff = diffs.find((d) => d.file === "added.txt")
|
||||
@@ -1189,7 +1184,7 @@ test("diffFull with multiple additions and deletions", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await Filesystem.write(`${tmp.path}/multi1.txt`, "line1\nline2\nline3")
|
||||
@@ -1197,10 +1192,10 @@ test("diffFull with multiple additions and deletions", async () => {
|
||||
await $`rm ${tmp.path}/a.txt`.quiet()
|
||||
await $`rm ${tmp.path}/b.txt`.quiet()
|
||||
|
||||
const after = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const after = await Snapshot.track()
|
||||
expect(after).toBeTruthy()
|
||||
|
||||
const diffs = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.diffFull(before!, after!)))
|
||||
const diffs = await Snapshot.diffFull(before!, after!)
|
||||
expect(diffs.length).toBe(4)
|
||||
|
||||
const multi1Diff = diffs.find((d) => d.file === "multi1.txt")
|
||||
@@ -1231,13 +1226,13 @@ test("diffFull with no changes", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
const after = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const after = await Snapshot.track()
|
||||
expect(after).toBeTruthy()
|
||||
|
||||
const diffs = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.diffFull(before!, after!)))
|
||||
const diffs = await Snapshot.diffFull(before!, after!)
|
||||
expect(diffs.length).toBe(0)
|
||||
},
|
||||
})
|
||||
@@ -1248,15 +1243,15 @@ test("diffFull with binary file changes", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await Filesystem.write(`${tmp.path}/binary.bin`, new Uint8Array([0x00, 0x01, 0x02, 0x03]))
|
||||
|
||||
const after = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const after = await Snapshot.track()
|
||||
expect(after).toBeTruthy()
|
||||
|
||||
const diffs = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.diffFull(before!, after!)))
|
||||
const diffs = await Snapshot.diffFull(before!, after!)
|
||||
expect(diffs.length).toBe(1)
|
||||
|
||||
const binaryDiff = diffs[0]
|
||||
@@ -1272,15 +1267,15 @@ test("diffFull with whitespace changes", async () => {
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await Filesystem.write(`${tmp.path}/whitespace.txt`, "line1\nline2")
|
||||
const before = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
await Filesystem.write(`${tmp.path}/whitespace.txt`, "line1\n\nline2\n")
|
||||
|
||||
const after = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const after = await Snapshot.track()
|
||||
expect(after).toBeTruthy()
|
||||
|
||||
const diffs = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.diffFull(before!, after!)))
|
||||
const diffs = await Snapshot.diffFull(before!, after!)
|
||||
expect(diffs.length).toBe(1)
|
||||
|
||||
const whitespaceDiff = diffs[0]
|
||||
@@ -1297,26 +1292,26 @@ test("revert with overlapping files across patches uses first patch hash", async
|
||||
fn: async () => {
|
||||
// Write initial content and snapshot
|
||||
await Filesystem.write(`${tmp.path}/shared.txt`, "v1")
|
||||
const snap1 = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const snap1 = await Snapshot.track()
|
||||
expect(snap1).toBeTruthy()
|
||||
|
||||
// Modify and snapshot again
|
||||
await Filesystem.write(`${tmp.path}/shared.txt`, "v2")
|
||||
const snap2 = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const snap2 = await Snapshot.track()
|
||||
expect(snap2).toBeTruthy()
|
||||
|
||||
// Modify once more so both patches include shared.txt
|
||||
await Filesystem.write(`${tmp.path}/shared.txt`, "v3")
|
||||
|
||||
const patch1 = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(snap1!)))
|
||||
const patch2 = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(snap2!)))
|
||||
const patch1 = await Snapshot.patch(snap1!)
|
||||
const patch2 = await Snapshot.patch(snap2!)
|
||||
|
||||
// Both patches should include shared.txt
|
||||
expect(patch1.files).toContain(fwd(tmp.path, "shared.txt"))
|
||||
expect(patch2.files).toContain(fwd(tmp.path, "shared.txt"))
|
||||
|
||||
// Revert with patch1 first — should use snap1's hash (restoring "v1")
|
||||
await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.revert([patch1, patch2])))
|
||||
await Snapshot.revert([patch1, patch2])
|
||||
|
||||
const content = await fs.readFile(`${tmp.path}/shared.txt`, "utf-8")
|
||||
expect(content).toBe("v1")
|
||||
@@ -1333,24 +1328,24 @@ test("revert preserves patch order when the same hash appears again", async () =
|
||||
await Filesystem.write(`${tmp.path}/foo/bar`, "v1")
|
||||
await Filesystem.write(`${tmp.path}/a.txt`, "v1")
|
||||
|
||||
const snap1 = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const snap1 = await Snapshot.track()
|
||||
expect(snap1).toBeTruthy()
|
||||
|
||||
await $`rm -rf ${tmp.path}/foo`.quiet()
|
||||
await Filesystem.write(`${tmp.path}/foo`, "v2")
|
||||
await Filesystem.write(`${tmp.path}/a.txt`, "v2")
|
||||
|
||||
const snap2 = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const snap2 = await Snapshot.track()
|
||||
expect(snap2).toBeTruthy()
|
||||
|
||||
await $`rm -rf ${tmp.path}/foo`.quiet()
|
||||
await Filesystem.write(`${tmp.path}/a.txt`, "v3")
|
||||
|
||||
await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.revert([
|
||||
await Snapshot.revert([
|
||||
{ hash: snap1!, files: [fwd(tmp.path, "a.txt")] },
|
||||
{ hash: snap2!, files: [fwd(tmp.path, "foo")] },
|
||||
{ hash: snap1!, files: [fwd(tmp.path, "foo", "bar")] },
|
||||
])))
|
||||
])
|
||||
|
||||
expect(await fs.readFile(`${tmp.path}/a.txt`, "utf-8")).toBe("v1")
|
||||
expect((await fs.stat(`${tmp.path}/foo`)).isDirectory()).toBe(true)
|
||||
@@ -1370,16 +1365,16 @@ test("revert handles large mixed batches across chunk boundaries", async () => {
|
||||
await $`mkdir -p ${tmp.path}/batch ${tmp.path}/fresh`.quiet()
|
||||
await Promise.all(base.map((file, i) => Filesystem.write(file, `base-${i}`)))
|
||||
|
||||
const snap = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track()))
|
||||
const snap = await Snapshot.track()
|
||||
expect(snap).toBeTruthy()
|
||||
|
||||
await Promise.all(base.map((file, i) => Filesystem.write(file, `next-${i}`)))
|
||||
await Promise.all(fresh.map((file, i) => Filesystem.write(file, `fresh-${i}`)))
|
||||
|
||||
const patch = await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(snap!)))
|
||||
const patch = await Snapshot.patch(snap!)
|
||||
expect(patch.files.length).toBe(base.length + fresh.length)
|
||||
|
||||
await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.revert([patch])))
|
||||
await Snapshot.revert([patch])
|
||||
|
||||
await Promise.all(
|
||||
base.map(async (file, i) => {
|
||||
|
||||
@@ -22,7 +22,7 @@ const baseCtx = {
|
||||
agent: "build",
|
||||
abort: AbortSignal.any([]),
|
||||
messages: [],
|
||||
metadata: () => Effect.void,
|
||||
metadata: () => {},
|
||||
}
|
||||
|
||||
type AskInput = {
|
||||
|
||||
@@ -29,7 +29,7 @@ const ctx = {
|
||||
agent: "build",
|
||||
abort: AbortSignal.any([]),
|
||||
messages: [],
|
||||
metadata: () => Effect.void,
|
||||
metadata: () => {},
|
||||
ask: () => Effect.void,
|
||||
}
|
||||
|
||||
@@ -982,14 +982,13 @@ describe("tool.bash abort", () => {
|
||||
{
|
||||
...ctx,
|
||||
abort: controller.signal,
|
||||
metadata: (input) =>
|
||||
Effect.sync(() => {
|
||||
const output = (input.metadata as { output?: string })?.output
|
||||
if (output && output.includes("before") && !controller.signal.aborted) {
|
||||
collected.push(output)
|
||||
controller.abort()
|
||||
}
|
||||
}),
|
||||
metadata: (input) => {
|
||||
const output = (input.metadata as { output?: string })?.output
|
||||
if (output && output.includes("before") && !controller.signal.aborted) {
|
||||
collected.push(output)
|
||||
controller.abort()
|
||||
}
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -1075,11 +1074,10 @@ describe("tool.bash abort", () => {
|
||||
},
|
||||
{
|
||||
...ctx,
|
||||
metadata: (input) =>
|
||||
Effect.sync(() => {
|
||||
const output = (input.metadata as { output?: string })?.output
|
||||
if (output) updates.push(output)
|
||||
}),
|
||||
metadata: (input) => {
|
||||
const output = (input.metadata as { output?: string })?.output
|
||||
if (output) updates.push(output)
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@@ -20,7 +20,7 @@ const ctx = {
|
||||
agent: "build",
|
||||
abort: AbortSignal.any([]),
|
||||
messages: [],
|
||||
metadata: () => Effect.void,
|
||||
metadata: () => {},
|
||||
ask: () => Effect.void,
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ const baseCtx: Omit<Tool.Context, "ask"> = {
|
||||
agent: "build",
|
||||
abort: AbortSignal.any([]),
|
||||
messages: [],
|
||||
metadata: () => Effect.void,
|
||||
metadata: () => {},
|
||||
}
|
||||
|
||||
const glob = (p: string) =>
|
||||
|
||||
@@ -20,7 +20,7 @@ const ctx = {
|
||||
agent: "build",
|
||||
abort: AbortSignal.any([]),
|
||||
messages: [],
|
||||
metadata: () => Effect.void,
|
||||
metadata: () => {},
|
||||
ask: () => Effect.void,
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ const ctx = {
|
||||
agent: "test-agent",
|
||||
abort: AbortSignal.any([]),
|
||||
messages: [],
|
||||
metadata: () => Effect.void,
|
||||
metadata: () => {},
|
||||
ask: () => Effect.void,
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ const ctx = {
|
||||
agent: "build",
|
||||
abort: AbortSignal.any([]),
|
||||
messages: [],
|
||||
metadata: () => Effect.void,
|
||||
metadata: () => {},
|
||||
ask: () => Effect.void,
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ const baseCtx: Omit<Tool.Context, "ask"> = {
|
||||
agent: "build",
|
||||
abort: AbortSignal.any([]),
|
||||
messages: [],
|
||||
metadata: () => Effect.void,
|
||||
metadata: () => {},
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
@@ -209,7 +209,7 @@ describe("tool.task", () => {
|
||||
abort: new AbortController().signal,
|
||||
extra: { promptOps },
|
||||
messages: [],
|
||||
metadata: () => Effect.void,
|
||||
metadata() {},
|
||||
ask: () => Effect.void,
|
||||
},
|
||||
)
|
||||
@@ -247,7 +247,7 @@ describe("tool.task", () => {
|
||||
abort: new AbortController().signal,
|
||||
extra: { promptOps, ...extra },
|
||||
messages: [],
|
||||
metadata: () => Effect.void,
|
||||
metadata() {},
|
||||
ask: (input) =>
|
||||
Effect.sync(() => {
|
||||
calls.push(input)
|
||||
@@ -296,7 +296,7 @@ describe("tool.task", () => {
|
||||
abort: new AbortController().signal,
|
||||
extra: { promptOps },
|
||||
messages: [],
|
||||
metadata: () => Effect.void,
|
||||
metadata() {},
|
||||
ask: () => Effect.void,
|
||||
},
|
||||
)
|
||||
@@ -335,7 +335,7 @@ describe("tool.task", () => {
|
||||
abort: new AbortController().signal,
|
||||
extra: { promptOps },
|
||||
messages: [],
|
||||
metadata: () => Effect.void,
|
||||
metadata() {},
|
||||
ask: () => Effect.void,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ const ctx = {
|
||||
agent: "build",
|
||||
abort: AbortSignal.any([]),
|
||||
messages: [],
|
||||
metadata: () => Effect.void,
|
||||
metadata: () => {},
|
||||
ask: () => Effect.void,
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ const ctx = {
|
||||
agent: "build",
|
||||
abort: AbortSignal.any([]),
|
||||
messages: [],
|
||||
metadata: () => Effect.void,
|
||||
metadata: () => {},
|
||||
ask: () => Effect.void,
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { z } from "zod"
|
||||
import { Effect } from "effect"
|
||||
|
||||
export type ToolContext = {
|
||||
sessionID: string
|
||||
@@ -17,7 +16,7 @@ export type ToolContext = {
|
||||
worktree: string
|
||||
abort: AbortSignal
|
||||
metadata(input: { title?: string; metadata?: { [key: string]: any } }): void
|
||||
ask(input: AskInput): Effect.Effect<void>
|
||||
ask(input: AskInput): Promise<void>
|
||||
}
|
||||
|
||||
type AskInput = {
|
||||
|
||||
@@ -316,29 +316,6 @@ export type EventCommandExecuted = {
|
||||
}
|
||||
}
|
||||
|
||||
export type EventWorkspaceReady = {
|
||||
type: "workspace.ready"
|
||||
properties: {
|
||||
name: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventWorkspaceFailed = {
|
||||
type: "workspace.failed"
|
||||
properties: {
|
||||
message: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventWorkspaceStatus = {
|
||||
type: "workspace.status"
|
||||
properties: {
|
||||
workspaceID: string
|
||||
status: "connected" | "connecting" | "disconnected" | "error"
|
||||
error?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type QuestionOption = {
|
||||
/**
|
||||
* Display text (1-5 words, concise)
|
||||
@@ -410,6 +387,29 @@ export type EventQuestionRejected = {
|
||||
}
|
||||
}
|
||||
|
||||
export type Todo = {
|
||||
/**
|
||||
* Brief description of the task
|
||||
*/
|
||||
content: string
|
||||
/**
|
||||
* Current status of the task: pending, in_progress, completed, cancelled
|
||||
*/
|
||||
status: string
|
||||
/**
|
||||
* Priority level of the task: high, medium, low
|
||||
*/
|
||||
priority: string
|
||||
}
|
||||
|
||||
export type EventTodoUpdated = {
|
||||
type: "todo.updated"
|
||||
properties: {
|
||||
sessionID: string
|
||||
todos: Array<Todo>
|
||||
}
|
||||
}
|
||||
|
||||
export type SessionStatus =
|
||||
| {
|
||||
type: "idle"
|
||||
@@ -446,29 +446,6 @@ export type EventSessionCompacted = {
|
||||
}
|
||||
}
|
||||
|
||||
export type Todo = {
|
||||
/**
|
||||
* Brief description of the task
|
||||
*/
|
||||
content: string
|
||||
/**
|
||||
* Current status of the task: pending, in_progress, completed, cancelled
|
||||
*/
|
||||
status: string
|
||||
/**
|
||||
* Priority level of the task: high, medium, low
|
||||
*/
|
||||
priority: string
|
||||
}
|
||||
|
||||
export type EventTodoUpdated = {
|
||||
type: "todo.updated"
|
||||
properties: {
|
||||
sessionID: string
|
||||
todos: Array<Todo>
|
||||
}
|
||||
}
|
||||
|
||||
export type EventWorktreeReady = {
|
||||
type: "worktree.ready"
|
||||
properties: {
|
||||
@@ -523,6 +500,29 @@ export type EventPtyDeleted = {
|
||||
}
|
||||
}
|
||||
|
||||
export type EventWorkspaceReady = {
|
||||
type: "workspace.ready"
|
||||
properties: {
|
||||
name: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventWorkspaceFailed = {
|
||||
type: "workspace.failed"
|
||||
properties: {
|
||||
message: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventWorkspaceStatus = {
|
||||
type: "workspace.status"
|
||||
properties: {
|
||||
workspaceID: string
|
||||
status: "connected" | "connecting" | "disconnected" | "error"
|
||||
error?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type OutputFormatText = {
|
||||
type: "text"
|
||||
}
|
||||
@@ -995,22 +995,22 @@ export type Event =
|
||||
| EventMcpToolsChanged
|
||||
| EventMcpBrowserOpenFailed
|
||||
| EventCommandExecuted
|
||||
| EventWorkspaceReady
|
||||
| EventWorkspaceFailed
|
||||
| EventWorkspaceStatus
|
||||
| EventQuestionAsked
|
||||
| EventQuestionReplied
|
||||
| EventQuestionRejected
|
||||
| EventTodoUpdated
|
||||
| EventSessionStatus
|
||||
| EventSessionIdle
|
||||
| EventSessionCompacted
|
||||
| EventTodoUpdated
|
||||
| EventWorktreeReady
|
||||
| EventWorktreeFailed
|
||||
| EventPtyCreated
|
||||
| EventPtyUpdated
|
||||
| EventPtyExited
|
||||
| EventPtyDeleted
|
||||
| EventWorkspaceReady
|
||||
| EventWorkspaceFailed
|
||||
| EventWorkspaceStatus
|
||||
| EventMessageUpdated
|
||||
| EventMessageRemoved
|
||||
| EventMessagePartUpdated
|
||||
|
||||
+121
-121
@@ -7986,71 +7986,6 @@
|
||||
},
|
||||
"required": ["type", "properties"]
|
||||
},
|
||||
"Event.workspace.ready": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "workspace.ready"
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["name"]
|
||||
}
|
||||
},
|
||||
"required": ["type", "properties"]
|
||||
},
|
||||
"Event.workspace.failed": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "workspace.failed"
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["message"]
|
||||
}
|
||||
},
|
||||
"required": ["type", "properties"]
|
||||
},
|
||||
"Event.workspace.status": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "workspace.status"
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"workspaceID": {
|
||||
"type": "string",
|
||||
"pattern": "^wrk.*"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["connected", "connecting", "disconnected", "error"]
|
||||
},
|
||||
"error": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["workspaceID", "status"]
|
||||
}
|
||||
},
|
||||
"required": ["type", "properties"]
|
||||
},
|
||||
"QuestionOption": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -8201,6 +8136,50 @@
|
||||
},
|
||||
"required": ["type", "properties"]
|
||||
},
|
||||
"Todo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"description": "Brief description of the task",
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"description": "Current status of the task: pending, in_progress, completed, cancelled",
|
||||
"type": "string"
|
||||
},
|
||||
"priority": {
|
||||
"description": "Priority level of the task: high, medium, low",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["content", "status", "priority"]
|
||||
},
|
||||
"Event.todo.updated": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "todo.updated"
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sessionID": {
|
||||
"type": "string",
|
||||
"pattern": "^ses.*"
|
||||
},
|
||||
"todos": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Todo"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["sessionID", "todos"]
|
||||
}
|
||||
},
|
||||
"required": ["type", "properties"]
|
||||
},
|
||||
"SessionStatus": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -8307,50 +8286,6 @@
|
||||
},
|
||||
"required": ["type", "properties"]
|
||||
},
|
||||
"Todo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"description": "Brief description of the task",
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"description": "Current status of the task: pending, in_progress, completed, cancelled",
|
||||
"type": "string"
|
||||
},
|
||||
"priority": {
|
||||
"description": "Priority level of the task: high, medium, low",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["content", "status", "priority"]
|
||||
},
|
||||
"Event.todo.updated": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "todo.updated"
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sessionID": {
|
||||
"type": "string",
|
||||
"pattern": "^ses.*"
|
||||
},
|
||||
"todos": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Todo"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["sessionID", "todos"]
|
||||
}
|
||||
},
|
||||
"required": ["type", "properties"]
|
||||
},
|
||||
"Event.worktree.ready": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -8505,6 +8440,71 @@
|
||||
},
|
||||
"required": ["type", "properties"]
|
||||
},
|
||||
"Event.workspace.ready": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "workspace.ready"
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["name"]
|
||||
}
|
||||
},
|
||||
"required": ["type", "properties"]
|
||||
},
|
||||
"Event.workspace.failed": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "workspace.failed"
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["message"]
|
||||
}
|
||||
},
|
||||
"required": ["type", "properties"]
|
||||
},
|
||||
"Event.workspace.status": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "workspace.status"
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"workspaceID": {
|
||||
"type": "string",
|
||||
"pattern": "^wrk.*"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["connected", "connecting", "disconnected", "error"]
|
||||
},
|
||||
"error": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["workspaceID", "status"]
|
||||
}
|
||||
},
|
||||
"required": ["type", "properties"]
|
||||
},
|
||||
"OutputFormatText": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -9937,15 +9937,6 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.command.executed"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.workspace.ready"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.workspace.failed"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.workspace.status"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.question.asked"
|
||||
},
|
||||
@@ -9955,6 +9946,9 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.question.rejected"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.todo.updated"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.session.status"
|
||||
},
|
||||
@@ -9964,9 +9958,6 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.session.compacted"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.todo.updated"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.worktree.ready"
|
||||
},
|
||||
@@ -9985,6 +9976,15 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.pty.deleted"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.workspace.ready"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.workspace.failed"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.workspace.status"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.message.updated"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user