mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-21 17:54:23 +01:00
ignore: rework bootstrap so server lazy starts it
This commit is contained in:
@@ -1,19 +1,14 @@
|
|||||||
import { Format } from "../format"
|
import { InstanceBootstrap } from "../project/bootstrap"
|
||||||
import { LSP } from "../lsp"
|
|
||||||
import { Plugin } from "../plugin"
|
|
||||||
import { Instance } from "../project/instance"
|
import { Instance } from "../project/instance"
|
||||||
import { Share } from "../share/share"
|
|
||||||
import { Snapshot } from "../snapshot"
|
|
||||||
|
|
||||||
export async function bootstrap<T>(directory: string, cb: () => Promise<T>) {
|
export async function bootstrap<T>(directory: string, cb: () => Promise<T>) {
|
||||||
return Instance.provide(directory, async () => {
|
return Instance.provide({
|
||||||
await Plugin.init()
|
directory,
|
||||||
Share.init()
|
init: InstanceBootstrap,
|
||||||
Format.init()
|
fn: async () => {
|
||||||
LSP.init()
|
|
||||||
Snapshot.init()
|
|
||||||
const result = await cb()
|
const result = await cb()
|
||||||
await Instance.dispose()
|
await Instance.dispose()
|
||||||
return result
|
return result
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ const AgentCreateCommand = cmd({
|
|||||||
command: "create",
|
command: "create",
|
||||||
describe: "create a new agent",
|
describe: "create a new agent",
|
||||||
async handler() {
|
async handler() {
|
||||||
await Instance.provide(process.cwd(), async () => {
|
await Instance.provide({
|
||||||
|
directory: process.cwd(),
|
||||||
|
async fn() {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("Create agent")
|
prompts.intro("Create agent")
|
||||||
const project = Instance.project
|
const project = Instance.project
|
||||||
@@ -126,6 +128,7 @@ const AgentCreateCommand = cmd({
|
|||||||
|
|
||||||
prompts.log.success(`Agent created: ${filePath}`)
|
prompts.log.success(`Agent created: ${filePath}`)
|
||||||
prompts.outro("Done")
|
prompts.outro("Done")
|
||||||
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -74,7 +74,9 @@ export const AuthLoginCommand = cmd({
|
|||||||
type: "string",
|
type: "string",
|
||||||
}),
|
}),
|
||||||
async handler(args) {
|
async handler(args) {
|
||||||
await Instance.provide(process.cwd(), async () => {
|
await Instance.provide({
|
||||||
|
directory: process.cwd(),
|
||||||
|
async fn() {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("Add credential")
|
prompts.intro("Add credential")
|
||||||
if (args.url) {
|
if (args.url) {
|
||||||
@@ -264,6 +266,7 @@ export const AuthLoginCommand = cmd({
|
|||||||
})
|
})
|
||||||
|
|
||||||
prompts.outro("Done")
|
prompts.outro("Done")
|
||||||
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ export const GithubInstallCommand = cmd({
|
|||||||
command: "install",
|
command: "install",
|
||||||
describe: "install the GitHub agent",
|
describe: "install the GitHub agent",
|
||||||
async handler() {
|
async handler() {
|
||||||
await Instance.provide(process.cwd(), async () => {
|
await Instance.provide({
|
||||||
|
directory: process.cwd(),
|
||||||
|
async fn() {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("Install GitHub agent")
|
prompts.intro("Install GitHub agent")
|
||||||
const app = await getAppInfo()
|
const app = await getAppInfo()
|
||||||
@@ -190,7 +192,9 @@ export const GithubInstallCommand = cmd({
|
|||||||
s.stop("Installed GitHub app")
|
s.stop("Installed GitHub app")
|
||||||
|
|
||||||
async function getInstallation() {
|
async function getInstallation() {
|
||||||
return await fetch(`https://api.opencode.ai/get_github_app_installation?owner=${app.owner}&repo=${app.repo}`)
|
return await fetch(
|
||||||
|
`https://api.opencode.ai/get_github_app_installation?owner=${app.owner}&repo=${app.repo}`,
|
||||||
|
)
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((data) => data.installation)
|
.then((data) => data.installation)
|
||||||
}
|
}
|
||||||
@@ -235,6 +239,7 @@ jobs:
|
|||||||
|
|
||||||
prompts.log.success(`Added workflow file: "${WORKFLOW_FILE}"`)
|
prompts.log.success(`Added workflow file: "${WORKFLOW_FILE}"`)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ export const ModelsCommand = cmd({
|
|||||||
command: "models",
|
command: "models",
|
||||||
describe: "list all available models",
|
describe: "list all available models",
|
||||||
handler: async () => {
|
handler: async () => {
|
||||||
await Instance.provide(process.cwd(), async () => {
|
await Instance.provide({
|
||||||
|
directory: process.cwd(),
|
||||||
|
async fn() {
|
||||||
const providers = await Provider.list()
|
const providers = await Provider.list()
|
||||||
|
|
||||||
for (const [providerID, provider] of Object.entries(providers)) {
|
for (const [providerID, provider] of Object.entries(providers)) {
|
||||||
@@ -14,6 +16,7 @@ export const ModelsCommand = cmd({
|
|||||||
console.log(`${providerID}/${modelID}`)
|
console.log(`${providerID}/${modelID}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Global } from "../../global"
|
import { Global } from "../../global"
|
||||||
import { Provider } from "../../provider/provider"
|
import { Provider } from "../../provider/provider"
|
||||||
import { Server } from "../../server/server"
|
import { Server } from "../../server/server"
|
||||||
import { bootstrap } from "../bootstrap"
|
|
||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
@@ -16,6 +15,7 @@ import { Ide } from "../../ide"
|
|||||||
import { Flag } from "../../flag/flag"
|
import { Flag } from "../../flag/flag"
|
||||||
import { Session } from "../../session"
|
import { Session } from "../../session"
|
||||||
import { $ } from "bun"
|
import { $ } from "bun"
|
||||||
|
import { bootstrap } from "../bootstrap"
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
const OPENCODE_TUI_PATH: string
|
const OPENCODE_TUI_PATH: string
|
||||||
|
|||||||
13
packages/opencode/src/project/bootstrap.ts
Normal file
13
packages/opencode/src/project/bootstrap.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Plugin } from "../plugin"
|
||||||
|
import { Share } from "../share/share"
|
||||||
|
import { Format } from "../format"
|
||||||
|
import { LSP } from "../lsp"
|
||||||
|
import { Snapshot } from "../snapshot"
|
||||||
|
|
||||||
|
export async function InstanceBootstrap() {
|
||||||
|
await Plugin.init()
|
||||||
|
Share.init()
|
||||||
|
Format.init()
|
||||||
|
LSP.init()
|
||||||
|
Snapshot.init()
|
||||||
|
}
|
||||||
@@ -2,12 +2,32 @@ import { Context } from "../util/context"
|
|||||||
import { Project } from "./project"
|
import { Project } from "./project"
|
||||||
import { State } from "./state"
|
import { State } from "./state"
|
||||||
|
|
||||||
const context = Context.create<{ directory: string; worktree: string; project: Project.Info }>("path")
|
interface Context {
|
||||||
|
directory: string
|
||||||
|
worktree: string
|
||||||
|
project: Project.Info
|
||||||
|
}
|
||||||
|
const context = Context.create<Context>("instance")
|
||||||
|
const cache = new Map<string, Context>()
|
||||||
|
|
||||||
export const Instance = {
|
export const Instance = {
|
||||||
async provide<R>(directory: string, cb: () => R): Promise<R> {
|
async provide<R>(input: { directory: string; init?: () => Promise<any>; fn: () => R }): Promise<R> {
|
||||||
const project = await Project.fromDirectory(directory)
|
let existing = cache.get(input.directory)
|
||||||
return context.provide({ directory, worktree: project.worktree, project }, cb)
|
if (!existing) {
|
||||||
|
const project = await Project.fromDirectory(input.directory)
|
||||||
|
existing = {
|
||||||
|
directory: input.directory,
|
||||||
|
worktree: project.worktree,
|
||||||
|
project,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return context.provide(existing, async () => {
|
||||||
|
if (!cache.has(input.directory)) {
|
||||||
|
await input.init?.()
|
||||||
|
cache.set(input.directory, existing)
|
||||||
|
}
|
||||||
|
return input.fn()
|
||||||
|
})
|
||||||
},
|
},
|
||||||
get directory() {
|
get directory() {
|
||||||
return context.use().directory
|
return context.use().directory
|
||||||
|
|||||||
@@ -22,10 +22,8 @@ export namespace Project {
|
|||||||
})
|
})
|
||||||
export type Info = z.infer<typeof Info>
|
export type Info = z.infer<typeof Info>
|
||||||
|
|
||||||
const cache = new Map<string, Info>()
|
|
||||||
export async function fromDirectory(directory: string) {
|
export async function fromDirectory(directory: string) {
|
||||||
log.info("fromDirectory", { directory })
|
log.info("fromDirectory", { directory })
|
||||||
const fn = async () => {
|
|
||||||
const matches = Filesystem.up({ targets: [".git"], start: directory })
|
const matches = Filesystem.up({ targets: [".git"], start: directory })
|
||||||
const git = await matches.next().then((x) => x.value)
|
const git = await matches.next().then((x) => x.value)
|
||||||
await matches.return()
|
await matches.return()
|
||||||
@@ -83,13 +81,6 @@ export namespace Project {
|
|||||||
await Storage.write<Info>(["project", id], project)
|
await Storage.write<Info>(["project", id], project)
|
||||||
return project
|
return project
|
||||||
}
|
}
|
||||||
if (cache.has(directory)) {
|
|
||||||
return cache.get(directory)!
|
|
||||||
}
|
|
||||||
const result = await fn()
|
|
||||||
cache.set(directory, result)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function setInitialized(projectID: string) {
|
export async function setInitialized(projectID: string) {
|
||||||
await Storage.update<Info>(["project", projectID], (draft) => {
|
await Storage.update<Info>(["project", projectID], (draft) => {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { SessionPrompt } from "../session/prompt"
|
|||||||
import { SessionCompaction } from "../session/compaction"
|
import { SessionCompaction } from "../session/compaction"
|
||||||
import { SessionRevert } from "../session/revert"
|
import { SessionRevert } from "../session/revert"
|
||||||
import { lazy } from "../util/lazy"
|
import { lazy } from "../util/lazy"
|
||||||
|
import { InstanceBootstrap } from "../project/bootstrap"
|
||||||
|
|
||||||
const ERRORS = {
|
const ERRORS = {
|
||||||
400: {
|
400: {
|
||||||
@@ -90,8 +91,12 @@ export namespace Server {
|
|||||||
})
|
})
|
||||||
.use(async (c, next) => {
|
.use(async (c, next) => {
|
||||||
const directory = c.req.query("directory") ?? process.cwd()
|
const directory = c.req.query("directory") ?? process.cwd()
|
||||||
return Instance.provide(directory, async () => {
|
return Instance.provide({
|
||||||
|
directory,
|
||||||
|
init: InstanceBootstrap,
|
||||||
|
async fn() {
|
||||||
return next()
|
return next()
|
||||||
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.use(cors())
|
.use(cors())
|
||||||
|
|||||||
@@ -27,19 +27,19 @@ async function bootstrap() {
|
|||||||
|
|
||||||
test("tracks deleted files correctly", async () => {
|
test("tracks deleted files correctly", async () => {
|
||||||
await using tmp = await bootstrap()
|
await using tmp = await bootstrap()
|
||||||
await Instance.provide(tmp.dir, async () => {
|
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||||
const before = await Snapshot.track()
|
const before = await Snapshot.track()
|
||||||
expect(before).toBeTruthy()
|
expect(before).toBeTruthy()
|
||||||
|
|
||||||
await $`rm ${tmp.dir}/a.txt`.quiet()
|
await $`rm ${tmp.dir}/a.txt`.quiet()
|
||||||
|
|
||||||
expect((await Snapshot.patch(before!)).files).toContain(`${tmp.dir}/a.txt`)
|
expect((await Snapshot.patch(before!)).files).toContain(`${tmp.dir}/a.txt`)
|
||||||
})
|
}})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("revert should remove new files", async () => {
|
test("revert should remove new files", async () => {
|
||||||
await using tmp = await bootstrap()
|
await using tmp = await bootstrap()
|
||||||
await Instance.provide(tmp.dir, async () => {
|
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||||
const before = await Snapshot.track()
|
const before = await Snapshot.track()
|
||||||
expect(before).toBeTruthy()
|
expect(before).toBeTruthy()
|
||||||
|
|
||||||
@@ -48,12 +48,12 @@ test("revert should remove new files", async () => {
|
|||||||
await Snapshot.revert([await Snapshot.patch(before!)])
|
await Snapshot.revert([await Snapshot.patch(before!)])
|
||||||
|
|
||||||
expect(await Bun.file(`${tmp.dir}/new.txt`).exists()).toBe(false)
|
expect(await Bun.file(`${tmp.dir}/new.txt`).exists()).toBe(false)
|
||||||
})
|
}})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("revert in subdirectory", async () => {
|
test("revert in subdirectory", async () => {
|
||||||
await using tmp = await bootstrap()
|
await using tmp = await bootstrap()
|
||||||
await Instance.provide(tmp.dir, async () => {
|
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||||
const before = await Snapshot.track()
|
const before = await Snapshot.track()
|
||||||
expect(before).toBeTruthy()
|
expect(before).toBeTruthy()
|
||||||
|
|
||||||
@@ -65,12 +65,12 @@ test("revert in subdirectory", async () => {
|
|||||||
expect(await Bun.file(`${tmp.dir}/sub/file.txt`).exists()).toBe(false)
|
expect(await Bun.file(`${tmp.dir}/sub/file.txt`).exists()).toBe(false)
|
||||||
// Note: revert currently only removes files, not directories
|
// Note: revert currently only removes files, not directories
|
||||||
// The empty subdirectory will remain
|
// The empty subdirectory will remain
|
||||||
})
|
}})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("multiple file operations", async () => {
|
test("multiple file operations", async () => {
|
||||||
await using tmp = await bootstrap()
|
await using tmp = await bootstrap()
|
||||||
await Instance.provide(tmp.dir, async () => {
|
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||||
const before = await Snapshot.track()
|
const before = await Snapshot.track()
|
||||||
expect(before).toBeTruthy()
|
expect(before).toBeTruthy()
|
||||||
|
|
||||||
@@ -87,24 +87,24 @@ test("multiple file operations", async () => {
|
|||||||
// Note: revert currently only removes files, not directories
|
// Note: revert currently only removes files, not directories
|
||||||
// The empty directory will remain
|
// The empty directory will remain
|
||||||
expect(await Bun.file(`${tmp.dir}/b.txt`).text()).toBe(tmp.bContent)
|
expect(await Bun.file(`${tmp.dir}/b.txt`).text()).toBe(tmp.bContent)
|
||||||
})
|
}})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("empty directory handling", async () => {
|
test("empty directory handling", async () => {
|
||||||
await using tmp = await bootstrap()
|
await using tmp = await bootstrap()
|
||||||
await Instance.provide(tmp.dir, async () => {
|
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||||
const before = await Snapshot.track()
|
const before = await Snapshot.track()
|
||||||
expect(before).toBeTruthy()
|
expect(before).toBeTruthy()
|
||||||
|
|
||||||
await $`mkdir ${tmp.dir}/empty`.quiet()
|
await $`mkdir ${tmp.dir}/empty`.quiet()
|
||||||
|
|
||||||
expect((await Snapshot.patch(before!)).files.length).toBe(0)
|
expect((await Snapshot.patch(before!)).files.length).toBe(0)
|
||||||
})
|
}})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("binary file handling", async () => {
|
test("binary file handling", async () => {
|
||||||
await using tmp = await bootstrap()
|
await using tmp = await bootstrap()
|
||||||
await Instance.provide(tmp.dir, async () => {
|
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||||
const before = await Snapshot.track()
|
const before = await Snapshot.track()
|
||||||
expect(before).toBeTruthy()
|
expect(before).toBeTruthy()
|
||||||
|
|
||||||
@@ -115,36 +115,36 @@ test("binary file handling", async () => {
|
|||||||
|
|
||||||
await Snapshot.revert([patch])
|
await Snapshot.revert([patch])
|
||||||
expect(await Bun.file(`${tmp.dir}/image.png`).exists()).toBe(false)
|
expect(await Bun.file(`${tmp.dir}/image.png`).exists()).toBe(false)
|
||||||
})
|
}})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("symlink handling", async () => {
|
test("symlink handling", async () => {
|
||||||
await using tmp = await bootstrap()
|
await using tmp = await bootstrap()
|
||||||
await Instance.provide(tmp.dir, async () => {
|
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||||
const before = await Snapshot.track()
|
const before = await Snapshot.track()
|
||||||
expect(before).toBeTruthy()
|
expect(before).toBeTruthy()
|
||||||
|
|
||||||
await $`ln -s ${tmp.dir}/a.txt ${tmp.dir}/link.txt`.quiet()
|
await $`ln -s ${tmp.dir}/a.txt ${tmp.dir}/link.txt`.quiet()
|
||||||
|
|
||||||
expect((await Snapshot.patch(before!)).files).toContain(`${tmp.dir}/link.txt`)
|
expect((await Snapshot.patch(before!)).files).toContain(`${tmp.dir}/link.txt`)
|
||||||
})
|
}})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("large file handling", async () => {
|
test("large file handling", async () => {
|
||||||
await using tmp = await bootstrap()
|
await using tmp = await bootstrap()
|
||||||
await Instance.provide(tmp.dir, async () => {
|
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||||
const before = await Snapshot.track()
|
const before = await Snapshot.track()
|
||||||
expect(before).toBeTruthy()
|
expect(before).toBeTruthy()
|
||||||
|
|
||||||
await Bun.write(`${tmp.dir}/large.txt`, "x".repeat(1024 * 1024))
|
await Bun.write(`${tmp.dir}/large.txt`, "x".repeat(1024 * 1024))
|
||||||
|
|
||||||
expect((await Snapshot.patch(before!)).files).toContain(`${tmp.dir}/large.txt`)
|
expect((await Snapshot.patch(before!)).files).toContain(`${tmp.dir}/large.txt`)
|
||||||
})
|
}})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("nested directory revert", async () => {
|
test("nested directory revert", async () => {
|
||||||
await using tmp = await bootstrap()
|
await using tmp = await bootstrap()
|
||||||
await Instance.provide(tmp.dir, async () => {
|
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||||
const before = await Snapshot.track()
|
const before = await Snapshot.track()
|
||||||
expect(before).toBeTruthy()
|
expect(before).toBeTruthy()
|
||||||
|
|
||||||
@@ -154,12 +154,12 @@ test("nested directory revert", async () => {
|
|||||||
await Snapshot.revert([await Snapshot.patch(before!)])
|
await Snapshot.revert([await Snapshot.patch(before!)])
|
||||||
|
|
||||||
expect(await Bun.file(`${tmp.dir}/level1/level2/level3/deep.txt`).exists()).toBe(false)
|
expect(await Bun.file(`${tmp.dir}/level1/level2/level3/deep.txt`).exists()).toBe(false)
|
||||||
})
|
}})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("special characters in filenames", async () => {
|
test("special characters in filenames", async () => {
|
||||||
await using tmp = await bootstrap()
|
await using tmp = await bootstrap()
|
||||||
await Instance.provide(tmp.dir, async () => {
|
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||||
const before = await Snapshot.track()
|
const before = await Snapshot.track()
|
||||||
expect(before).toBeTruthy()
|
expect(before).toBeTruthy()
|
||||||
|
|
||||||
@@ -171,23 +171,23 @@ test("special characters in filenames", async () => {
|
|||||||
expect(files).toContain(`${tmp.dir}/file with spaces.txt`)
|
expect(files).toContain(`${tmp.dir}/file with spaces.txt`)
|
||||||
expect(files).toContain(`${tmp.dir}/file-with-dashes.txt`)
|
expect(files).toContain(`${tmp.dir}/file-with-dashes.txt`)
|
||||||
expect(files).toContain(`${tmp.dir}/file_with_underscores.txt`)
|
expect(files).toContain(`${tmp.dir}/file_with_underscores.txt`)
|
||||||
})
|
}})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("revert with empty patches", async () => {
|
test("revert with empty patches", async () => {
|
||||||
await using tmp = await bootstrap()
|
await using tmp = await bootstrap()
|
||||||
await Instance.provide(tmp.dir, async () => {
|
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||||
// Should not crash with empty patches
|
// Should not crash with empty patches
|
||||||
expect(Snapshot.revert([])).resolves.toBeUndefined()
|
expect(Snapshot.revert([])).resolves.toBeUndefined()
|
||||||
|
|
||||||
// Should not crash with patches that have empty file lists
|
// Should not crash with patches that have empty file lists
|
||||||
expect(Snapshot.revert([{ hash: "dummy", files: [] }])).resolves.toBeUndefined()
|
expect(Snapshot.revert([{ hash: "dummy", files: [] }])).resolves.toBeUndefined()
|
||||||
})
|
}})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("patch with invalid hash", async () => {
|
test("patch with invalid hash", async () => {
|
||||||
await using tmp = await bootstrap()
|
await using tmp = await bootstrap()
|
||||||
await Instance.provide(tmp.dir, async () => {
|
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||||
const before = await Snapshot.track()
|
const before = await Snapshot.track()
|
||||||
expect(before).toBeTruthy()
|
expect(before).toBeTruthy()
|
||||||
|
|
||||||
@@ -198,12 +198,12 @@ test("patch with invalid hash", async () => {
|
|||||||
const patch = await Snapshot.patch("invalid-hash-12345")
|
const patch = await Snapshot.patch("invalid-hash-12345")
|
||||||
expect(patch.files).toEqual([])
|
expect(patch.files).toEqual([])
|
||||||
expect(patch.hash).toBe("invalid-hash-12345")
|
expect(patch.hash).toBe("invalid-hash-12345")
|
||||||
})
|
}})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("revert non-existent file", async () => {
|
test("revert non-existent file", async () => {
|
||||||
await using tmp = await bootstrap()
|
await using tmp = await bootstrap()
|
||||||
await Instance.provide(tmp.dir, async () => {
|
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||||
const before = await Snapshot.track()
|
const before = await Snapshot.track()
|
||||||
expect(before).toBeTruthy()
|
expect(before).toBeTruthy()
|
||||||
|
|
||||||
@@ -217,12 +217,12 @@ test("revert non-existent file", async () => {
|
|||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
).resolves.toBeUndefined()
|
).resolves.toBeUndefined()
|
||||||
})
|
}})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("unicode filenames", async () => {
|
test("unicode filenames", async () => {
|
||||||
await using tmp = await bootstrap()
|
await using tmp = await bootstrap()
|
||||||
await Instance.provide(tmp.dir, async () => {
|
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||||
const before = await Snapshot.track()
|
const before = await Snapshot.track()
|
||||||
expect(before).toBeTruthy()
|
expect(before).toBeTruthy()
|
||||||
|
|
||||||
@@ -244,12 +244,12 @@ test("unicode filenames", async () => {
|
|||||||
|
|
||||||
// Skip revert test due to git filename escaping issues
|
// Skip revert test due to git filename escaping issues
|
||||||
// The functionality works but git uses escaped filenames internally
|
// The functionality works but git uses escaped filenames internally
|
||||||
})
|
}})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("very long filenames", async () => {
|
test("very long filenames", async () => {
|
||||||
await using tmp = await bootstrap()
|
await using tmp = await bootstrap()
|
||||||
await Instance.provide(tmp.dir, async () => {
|
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||||
const before = await Snapshot.track()
|
const before = await Snapshot.track()
|
||||||
expect(before).toBeTruthy()
|
expect(before).toBeTruthy()
|
||||||
|
|
||||||
@@ -263,12 +263,12 @@ test("very long filenames", async () => {
|
|||||||
|
|
||||||
await Snapshot.revert([patch])
|
await Snapshot.revert([patch])
|
||||||
expect(await Bun.file(longFile).exists()).toBe(false)
|
expect(await Bun.file(longFile).exists()).toBe(false)
|
||||||
})
|
}})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("hidden files", async () => {
|
test("hidden files", async () => {
|
||||||
await using tmp = await bootstrap()
|
await using tmp = await bootstrap()
|
||||||
await Instance.provide(tmp.dir, async () => {
|
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||||
const before = await Snapshot.track()
|
const before = await Snapshot.track()
|
||||||
expect(before).toBeTruthy()
|
expect(before).toBeTruthy()
|
||||||
|
|
||||||
@@ -280,12 +280,12 @@ test("hidden files", async () => {
|
|||||||
expect(patch.files).toContain(`${tmp.dir}/.hidden`)
|
expect(patch.files).toContain(`${tmp.dir}/.hidden`)
|
||||||
expect(patch.files).toContain(`${tmp.dir}/.gitignore`)
|
expect(patch.files).toContain(`${tmp.dir}/.gitignore`)
|
||||||
expect(patch.files).toContain(`${tmp.dir}/.config`)
|
expect(patch.files).toContain(`${tmp.dir}/.config`)
|
||||||
})
|
}})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("nested symlinks", async () => {
|
test("nested symlinks", async () => {
|
||||||
await using tmp = await bootstrap()
|
await using tmp = await bootstrap()
|
||||||
await Instance.provide(tmp.dir, async () => {
|
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||||
const before = await Snapshot.track()
|
const before = await Snapshot.track()
|
||||||
expect(before).toBeTruthy()
|
expect(before).toBeTruthy()
|
||||||
|
|
||||||
@@ -297,12 +297,12 @@ test("nested symlinks", async () => {
|
|||||||
const patch = await Snapshot.patch(before!)
|
const patch = await Snapshot.patch(before!)
|
||||||
expect(patch.files).toContain(`${tmp.dir}/sub/dir/link.txt`)
|
expect(patch.files).toContain(`${tmp.dir}/sub/dir/link.txt`)
|
||||||
expect(patch.files).toContain(`${tmp.dir}/sub-link`)
|
expect(patch.files).toContain(`${tmp.dir}/sub-link`)
|
||||||
})
|
}})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("file permissions and ownership changes", async () => {
|
test("file permissions and ownership changes", async () => {
|
||||||
await using tmp = await bootstrap()
|
await using tmp = await bootstrap()
|
||||||
await Instance.provide(tmp.dir, async () => {
|
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||||
const before = await Snapshot.track()
|
const before = await Snapshot.track()
|
||||||
expect(before).toBeTruthy()
|
expect(before).toBeTruthy()
|
||||||
|
|
||||||
@@ -315,12 +315,12 @@ test("file permissions and ownership changes", async () => {
|
|||||||
// Note: git doesn't track permission changes on existing files by default
|
// Note: git doesn't track permission changes on existing files by default
|
||||||
// Only tracks executable bit when files are first added
|
// Only tracks executable bit when files are first added
|
||||||
expect(patch.files.length).toBe(0)
|
expect(patch.files.length).toBe(0)
|
||||||
})
|
}})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("circular symlinks", async () => {
|
test("circular symlinks", async () => {
|
||||||
await using tmp = await bootstrap()
|
await using tmp = await bootstrap()
|
||||||
await Instance.provide(tmp.dir, async () => {
|
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||||
const before = await Snapshot.track()
|
const before = await Snapshot.track()
|
||||||
expect(before).toBeTruthy()
|
expect(before).toBeTruthy()
|
||||||
|
|
||||||
@@ -329,12 +329,12 @@ test("circular symlinks", async () => {
|
|||||||
|
|
||||||
const patch = await Snapshot.patch(before!)
|
const patch = await Snapshot.patch(before!)
|
||||||
expect(patch.files.length).toBeGreaterThanOrEqual(0) // Should not crash
|
expect(patch.files.length).toBeGreaterThanOrEqual(0) // Should not crash
|
||||||
})
|
}})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("gitignore changes", async () => {
|
test("gitignore changes", async () => {
|
||||||
await using tmp = await bootstrap()
|
await using tmp = await bootstrap()
|
||||||
await Instance.provide(tmp.dir, async () => {
|
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||||
const before = await Snapshot.track()
|
const before = await Snapshot.track()
|
||||||
expect(before).toBeTruthy()
|
expect(before).toBeTruthy()
|
||||||
|
|
||||||
@@ -350,12 +350,12 @@ test("gitignore changes", async () => {
|
|||||||
expect(patch.files).toContain(`${tmp.dir}/normal.txt`)
|
expect(patch.files).toContain(`${tmp.dir}/normal.txt`)
|
||||||
// Should not track ignored files (git won't see them)
|
// Should not track ignored files (git won't see them)
|
||||||
expect(patch.files).not.toContain(`${tmp.dir}/test.ignored`)
|
expect(patch.files).not.toContain(`${tmp.dir}/test.ignored`)
|
||||||
})
|
}})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("concurrent file operations during patch", async () => {
|
test("concurrent file operations during patch", async () => {
|
||||||
await using tmp = await bootstrap()
|
await using tmp = await bootstrap()
|
||||||
await Instance.provide(tmp.dir, async () => {
|
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||||
const before = await Snapshot.track()
|
const before = await Snapshot.track()
|
||||||
expect(before).toBeTruthy()
|
expect(before).toBeTruthy()
|
||||||
|
|
||||||
@@ -376,7 +376,7 @@ test("concurrent file operations during patch", async () => {
|
|||||||
|
|
||||||
// Should capture some or all of the concurrent files
|
// Should capture some or all of the concurrent files
|
||||||
expect(patch.files.length).toBeGreaterThanOrEqual(0)
|
expect(patch.files.length).toBeGreaterThanOrEqual(0)
|
||||||
})
|
}})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("snapshot state isolation between projects", async () => {
|
test("snapshot state isolation between projects", async () => {
|
||||||
@@ -384,14 +384,14 @@ test("snapshot state isolation between projects", async () => {
|
|||||||
await using tmp1 = await bootstrap()
|
await using tmp1 = await bootstrap()
|
||||||
await using tmp2 = await bootstrap()
|
await using tmp2 = await bootstrap()
|
||||||
|
|
||||||
await Instance.provide(tmp1.dir, async () => {
|
await Instance.provide({ directory: tmp1.dir, fn: async () => {
|
||||||
const before1 = await Snapshot.track()
|
const before1 = await Snapshot.track()
|
||||||
await Bun.write(`${tmp1.dir}/project1.txt`, "project1 content")
|
await Bun.write(`${tmp1.dir}/project1.txt`, "project1 content")
|
||||||
const patch1 = await Snapshot.patch(before1!)
|
const patch1 = await Snapshot.patch(before1!)
|
||||||
expect(patch1.files).toContain(`${tmp1.dir}/project1.txt`)
|
expect(patch1.files).toContain(`${tmp1.dir}/project1.txt`)
|
||||||
})
|
}})
|
||||||
|
|
||||||
await Instance.provide(tmp2.dir, async () => {
|
await Instance.provide({ directory: tmp2.dir, fn: async () => {
|
||||||
const before2 = await Snapshot.track()
|
const before2 = await Snapshot.track()
|
||||||
await Bun.write(`${tmp2.dir}/project2.txt`, "project2 content")
|
await Bun.write(`${tmp2.dir}/project2.txt`, "project2 content")
|
||||||
const patch2 = await Snapshot.patch(before2!)
|
const patch2 = await Snapshot.patch(before2!)
|
||||||
@@ -399,12 +399,12 @@ test("snapshot state isolation between projects", async () => {
|
|||||||
|
|
||||||
// Ensure project1 files don't appear in project2
|
// Ensure project1 files don't appear in project2
|
||||||
expect(patch2.files).not.toContain(`${tmp1?.dir}/project1.txt`)
|
expect(patch2.files).not.toContain(`${tmp1?.dir}/project1.txt`)
|
||||||
})
|
}})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("track with no changes returns same hash", async () => {
|
test("track with no changes returns same hash", async () => {
|
||||||
await using tmp = await bootstrap()
|
await using tmp = await bootstrap()
|
||||||
await Instance.provide(tmp.dir, async () => {
|
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||||
const hash1 = await Snapshot.track()
|
const hash1 = await Snapshot.track()
|
||||||
expect(hash1).toBeTruthy()
|
expect(hash1).toBeTruthy()
|
||||||
|
|
||||||
@@ -415,12 +415,12 @@ test("track with no changes returns same hash", async () => {
|
|||||||
// Track again
|
// Track again
|
||||||
const hash3 = await Snapshot.track()
|
const hash3 = await Snapshot.track()
|
||||||
expect(hash3).toBe(hash1!)
|
expect(hash3).toBe(hash1!)
|
||||||
})
|
}})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("diff function with various changes", async () => {
|
test("diff function with various changes", async () => {
|
||||||
await using tmp = await bootstrap()
|
await using tmp = await bootstrap()
|
||||||
await Instance.provide(tmp.dir, async () => {
|
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||||
const before = await Snapshot.track()
|
const before = await Snapshot.track()
|
||||||
expect(before).toBeTruthy()
|
expect(before).toBeTruthy()
|
||||||
|
|
||||||
@@ -433,12 +433,12 @@ test("diff function with various changes", async () => {
|
|||||||
expect(diff).toContain("deleted")
|
expect(diff).toContain("deleted")
|
||||||
expect(diff).toContain("modified")
|
expect(diff).toContain("modified")
|
||||||
// Note: git diff only shows changes to tracked files, not untracked files like new.txt
|
// Note: git diff only shows changes to tracked files, not untracked files like new.txt
|
||||||
})
|
}})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("restore function", async () => {
|
test("restore function", async () => {
|
||||||
await using tmp = await bootstrap()
|
await using tmp = await bootstrap()
|
||||||
await Instance.provide(tmp.dir, async () => {
|
await Instance.provide({ directory: tmp.dir, fn: async () => {
|
||||||
const before = await Snapshot.track()
|
const before = await Snapshot.track()
|
||||||
expect(before).toBeTruthy()
|
expect(before).toBeTruthy()
|
||||||
|
|
||||||
@@ -454,5 +454,5 @@ test("restore function", async () => {
|
|||||||
expect(await Bun.file(`${tmp.dir}/a.txt`).text()).toBe(tmp.aContent)
|
expect(await Bun.file(`${tmp.dir}/a.txt`).text()).toBe(tmp.aContent)
|
||||||
expect(await Bun.file(`${tmp.dir}/new.txt`).exists()).toBe(true) // New files should remain
|
expect(await Bun.file(`${tmp.dir}/new.txt`).exists()).toBe(true) // New files should remain
|
||||||
expect(await Bun.file(`${tmp.dir}/b.txt`).text()).toBe(tmp.bContent)
|
expect(await Bun.file(`${tmp.dir}/b.txt`).text()).toBe(tmp.bContent)
|
||||||
})
|
}})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ Log.init({ print: false })
|
|||||||
|
|
||||||
describe("tool.bash", () => {
|
describe("tool.bash", () => {
|
||||||
test("basic", async () => {
|
test("basic", async () => {
|
||||||
await Instance.provide(projectRoot, async () => {
|
await Instance.provide({
|
||||||
|
directory: projectRoot,
|
||||||
|
fn: async () => {
|
||||||
const result = await bash.execute(
|
const result = await bash.execute(
|
||||||
{
|
{
|
||||||
command: "echo 'test'",
|
command: "echo 'test'",
|
||||||
@@ -29,11 +31,14 @@ describe("tool.bash", () => {
|
|||||||
)
|
)
|
||||||
expect(result.metadata.exit).toBe(0)
|
expect(result.metadata.exit).toBe(0)
|
||||||
expect(result.metadata.output).toContain("test")
|
expect(result.metadata.output).toContain("test")
|
||||||
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("cd ../ should fail outside of project root", async () => {
|
test("cd ../ should fail outside of project root", async () => {
|
||||||
await Instance.provide(projectRoot, async () => {
|
await Instance.provide({
|
||||||
|
directory: projectRoot,
|
||||||
|
fn: async () => {
|
||||||
expect(
|
expect(
|
||||||
bash.execute(
|
bash.execute(
|
||||||
{
|
{
|
||||||
@@ -43,6 +48,7 @@ describe("tool.bash", () => {
|
|||||||
ctx,
|
ctx,
|
||||||
),
|
),
|
||||||
).rejects.toThrow("This command references paths outside of")
|
).rejects.toThrow("This command references paths outside of")
|
||||||
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ const fixturePath = path.join(__dirname, "../fixtures/example")
|
|||||||
|
|
||||||
describe("tool.glob", () => {
|
describe("tool.glob", () => {
|
||||||
test("truncate", async () => {
|
test("truncate", async () => {
|
||||||
await Instance.provide(projectRoot, async () => {
|
await Instance.provide({
|
||||||
|
directory: projectRoot,
|
||||||
|
fn: async () => {
|
||||||
let result = await glob.execute(
|
let result = await glob.execute(
|
||||||
{
|
{
|
||||||
pattern: "**/*",
|
pattern: "**/*",
|
||||||
@@ -29,10 +31,13 @@ describe("tool.glob", () => {
|
|||||||
ctx,
|
ctx,
|
||||||
)
|
)
|
||||||
expect(result.metadata.truncated).toBe(true)
|
expect(result.metadata.truncated).toBe(true)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
test("basic", async () => {
|
test("basic", async () => {
|
||||||
await Instance.provide(projectRoot, async () => {
|
await Instance.provide({
|
||||||
|
directory: projectRoot,
|
||||||
|
fn: async () => {
|
||||||
let result = await glob.execute(
|
let result = await glob.execute(
|
||||||
{
|
{
|
||||||
pattern: "*.json",
|
pattern: "*.json",
|
||||||
@@ -44,14 +49,18 @@ describe("tool.glob", () => {
|
|||||||
truncated: false,
|
truncated: false,
|
||||||
count: 2,
|
count: 2,
|
||||||
})
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("tool.ls", () => {
|
describe("tool.ls", () => {
|
||||||
test("basic", async () => {
|
test("basic", async () => {
|
||||||
const result = await Instance.provide(projectRoot, async () => {
|
const result = await Instance.provide({
|
||||||
|
directory: projectRoot,
|
||||||
|
fn: async () => {
|
||||||
return await list.execute({ path: fixturePath, ignore: [".git"] }, ctx)
|
return await list.execute({ path: fixturePath, ignore: [".git"] }, ctx)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Normalize absolute path to relative for consistent snapshots
|
// Normalize absolute path to relative for consistent snapshots
|
||||||
|
|||||||
Reference in New Issue
Block a user