@stonerzju/opencode 1.2.17 → 1.2.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/opencode +29 -157
- package/package.json +29 -29
- package/src/acp/agent.ts +4 -4
- package/src/acp/session.ts +1 -1
- package/src/agent/agent.ts +3 -3
- package/src/bun/index.ts +2 -2
- package/src/cli/cmd/acp.ts +3 -3
- package/src/cli/cmd/debug/file.ts +1 -1
- package/src/cli/cmd/github.ts +2 -2
- package/src/cli/cmd/pr.ts +1 -1
- package/src/cli/cmd/tui/app.tsx +24 -24
- package/src/cli/cmd/tui/attach.ts +3 -3
- package/src/cli/cmd/tui/component/dialog-agent.tsx +3 -3
- package/src/cli/cmd/tui/component/dialog-command.tsx +3 -3
- package/src/cli/cmd/tui/component/dialog-mcp.tsx +5 -5
- package/src/cli/cmd/tui/component/dialog-model.tsx +4 -4
- package/src/cli/cmd/tui/component/dialog-provider.tsx +4 -4
- package/src/cli/cmd/tui/component/dialog-session-list.tsx +5 -5
- package/src/cli/cmd/tui/component/dialog-session-rename.tsx +3 -3
- package/src/cli/cmd/tui/component/dialog-skill.tsx +3 -3
- package/src/cli/cmd/tui/component/dialog-stash.tsx +3 -3
- package/src/cli/cmd/tui/component/dialog-status.tsx +2 -2
- package/src/cli/cmd/tui/component/dialog-tag.tsx +3 -3
- package/src/cli/cmd/tui/component/logo.tsx +2 -2
- package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +6 -6
- package/src/cli/cmd/tui/component/prompt/frecency.tsx +2 -2
- package/src/cli/cmd/tui/component/prompt/history.tsx +2 -2
- package/src/cli/cmd/tui/component/prompt/index.tsx +14 -14
- package/src/cli/cmd/tui/component/prompt/stash.tsx +2 -2
- package/src/cli/cmd/tui/component/textarea-keybindings.ts +1 -1
- package/src/cli/cmd/tui/component/tips.tsx +1 -1
- package/src/cli/cmd/tui/context/directory.ts +1 -1
- package/src/cli/cmd/tui/context/exit.tsx +1 -1
- package/src/cli/cmd/tui/context/keybind.tsx +2 -2
- package/src/cli/cmd/tui/context/kv.tsx +2 -2
- package/src/cli/cmd/tui/context/local.tsx +6 -6
- package/src/cli/cmd/tui/context/sync.tsx +4 -4
- package/src/cli/cmd/tui/context/theme/opencode.json +245 -0
- package/src/cli/cmd/tui/context/theme.tsx +2 -2
- package/src/cli/cmd/tui/context/tui-config.tsx +1 -1
- package/src/cli/cmd/tui/event.ts +2 -2
- package/src/cli/cmd/tui/routes/home.tsx +6 -6
- package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +6 -6
- package/src/cli/cmd/tui/routes/session/dialog-message.tsx +6 -6
- package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +2 -2
- package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +3 -3
- package/src/cli/cmd/tui/routes/session/header.tsx +5 -5
- package/src/cli/cmd/tui/routes/session/index.tsx +32 -32
- package/src/cli/cmd/tui/routes/session/permission.tsx +4 -4
- package/src/cli/cmd/tui/routes/session/sidebar.tsx +4 -4
- package/src/cli/cmd/tui/thread.ts +9 -9
- package/src/cli/cmd/tui/ui/dialog-confirm.tsx +1 -1
- package/src/cli/cmd/tui/ui/dialog-help.tsx +2 -2
- package/src/cli/cmd/tui/ui/dialog-select.tsx +5 -5
- package/src/cli/cmd/tui/ui/dialog.tsx +3 -3
- package/src/cli/cmd/tui/ui/toast.tsx +1 -1
- package/src/cli/cmd/tui/util/editor.ts +3 -3
- package/src/cli/cmd/tui/util/transcript.ts +1 -1
- package/src/cli/cmd/tui/worker.ts +10 -10
- package/src/cli/error.ts +1 -1
- package/src/cli/ui.ts +1 -1
- package/src/cli/upgrade.ts +4 -4
- package/src/command/index.ts +1 -1
- package/src/config/config.ts +10 -10
- package/src/config/markdown.ts +1 -1
- package/src/config/migrate-tui-config.ts +5 -5
- package/src/config/paths.ts +4 -4
- package/src/config/tui.ts +4 -4
- package/src/control/control.sql.ts +1 -1
- package/src/control/index.ts +1 -1
- package/src/control-plane/adaptors/worktree.ts +1 -1
- package/src/control-plane/session-proxy-middleware.ts +1 -1
- package/src/control-plane/workspace.sql.ts +1 -1
- package/src/control-plane/workspace.ts +7 -7
- package/src/file/index.ts +1 -1
- package/src/file/ripgrep.ts +2 -2
- package/src/file/watcher.ts +5 -5
- package/src/format/formatter.ts +1 -1
- package/src/ide/index.ts +3 -3
- package/src/index.ts +1 -1
- package/src/installation/index.ts +3 -3
- package/src/lsp/client.ts +3 -3
- package/src/lsp/index.ts +3 -3
- package/src/mcp/index.ts +4 -4
- package/src/permission/index.ts +2 -2
- package/src/permission/next.ts +10 -10
- package/src/plugin/codex.ts +1 -1
- package/src/plugin/copilot.ts +2 -2
- package/src/plugin/index.ts +1 -1
- package/src/project/bootstrap.ts +2 -2
- package/src/project/instance.ts +4 -4
- package/src/project/project.sql.ts +1 -1
- package/src/project/project.ts +5 -5
- package/src/project/state.ts +1 -1
- package/src/project/vcs.ts +4 -4
- package/src/provider/auth.ts +4 -4
- package/src/provider/error.ts +1 -1
- package/src/provider/models-snapshot.ts +2 -0
- package/src/provider/models.ts +1 -1
- package/src/provider/provider.ts +2 -2
- package/src/provider/transform.ts +2 -2
- package/src/pty/index.ts +5 -5
- package/src/question/index.ts +5 -5
- package/src/server/event.ts +1 -1
- package/src/server/mdns.ts +1 -1
- package/src/server/routes/global.ts +3 -3
- package/src/server/routes/permission.ts +1 -1
- package/src/server/routes/pty.ts +1 -1
- package/src/server/routes/session.ts +4 -4
- package/src/server/routes/tui.ts +1 -1
- package/src/server/server.ts +3 -3
- package/src/session/compaction.ts +7 -7
- package/src/session/index.ts +10 -10
- package/src/session/instruction.ts +1 -1
- package/src/session/llm.ts +11 -11
- package/src/session/message-v2.ts +10 -10
- package/src/session/message.ts +1 -1
- package/src/session/processor.ts +10 -10
- package/src/session/prompt.ts +8 -8
- package/src/session/retry.ts +2 -2
- package/src/session/revert.ts +1 -1
- package/src/session/session.sql.ts +3 -3
- package/src/session/status.ts +3 -3
- package/src/session/summary.ts +5 -5
- package/src/session/system.ts +1 -1
- package/src/session/todo.ts +2 -2
- package/src/share/share-next.ts +7 -7
- package/src/share/share.sql.ts +1 -1
- package/src/shell/shell.ts +3 -3
- package/src/skill/skill.ts +6 -6
- package/src/storage/db.ts +1 -1
- package/src/storage/storage.ts +1 -1
- package/src/tool/bash.ts +6 -6
- package/src/tool/edit.ts +1 -1
- package/src/tool/registry.ts +2 -2
- package/src/tool/skill.ts +1 -1
- package/src/tool/task.ts +3 -3
- package/src/util/array.ts +10 -0
- package/src/util/binary.ts +41 -0
- package/src/util/encode.ts +51 -0
- package/src/util/error.ts +54 -0
- package/src/util/identifier.ts +48 -0
- package/src/util/lazy.ts +4 -16
- package/src/util/path.ts +37 -0
- package/src/util/retry.ts +41 -0
- package/src/util/slug.ts +74 -0
- package/src/worktree/index.ts +3 -3
- package/AGENTS.md +0 -10
- package/BUN_SHELL_MIGRATION_PLAN.md +0 -136
- package/Dockerfile +0 -18
- package/README.md +0 -15
- package/bunfig.toml +0 -7
- package/drizzle.config.ts +0 -10
- package/script/build.ts +0 -224
- package/script/check-migrations.ts +0 -16
- package/script/postinstall.mjs +0 -131
- package/script/publish.ts +0 -181
- package/script/schema.ts +0 -63
- package/script/seed-e2e.ts +0 -50
- package/sst-env.d.ts +0 -10
- package/test/AGENTS.md +0 -81
- package/test/acp/agent-interface.test.ts +0 -51
- package/test/acp/event-subscription.test.ts +0 -683
- package/test/agent/agent.test.ts +0 -689
- package/test/bun.test.ts +0 -53
- package/test/cli/github-action.test.ts +0 -197
- package/test/cli/github-remote.test.ts +0 -80
- package/test/cli/import.test.ts +0 -38
- package/test/cli/plugin-auth-picker.test.ts +0 -120
- package/test/cli/tui/transcript.test.ts +0 -322
- package/test/config/agent-color.test.ts +0 -71
- package/test/config/config.test.ts +0 -1886
- package/test/config/fixtures/empty-frontmatter.md +0 -4
- package/test/config/fixtures/frontmatter.md +0 -28
- package/test/config/fixtures/markdown-header.md +0 -11
- package/test/config/fixtures/no-frontmatter.md +0 -1
- package/test/config/fixtures/weird-model-id.md +0 -13
- package/test/config/markdown.test.ts +0 -228
- package/test/config/tui.test.ts +0 -510
- package/test/control-plane/session-proxy-middleware.test.ts +0 -147
- package/test/control-plane/sse.test.ts +0 -56
- package/test/control-plane/workspace-server-sse.test.ts +0 -65
- package/test/control-plane/workspace-sync.test.ts +0 -97
- package/test/file/ignore.test.ts +0 -10
- package/test/file/index.test.ts +0 -394
- package/test/file/path-traversal.test.ts +0 -198
- package/test/file/ripgrep.test.ts +0 -39
- package/test/file/time.test.ts +0 -361
- package/test/fixture/db.ts +0 -11
- package/test/fixture/fixture.ts +0 -45
- package/test/fixture/lsp/fake-lsp-server.js +0 -77
- package/test/fixture/skills/agents-sdk/SKILL.md +0 -152
- package/test/fixture/skills/agents-sdk/references/callable.md +0 -92
- package/test/fixture/skills/cloudflare/SKILL.md +0 -211
- package/test/fixture/skills/index.json +0 -6
- package/test/ide/ide.test.ts +0 -82
- package/test/keybind.test.ts +0 -421
- package/test/lsp/client.test.ts +0 -95
- package/test/mcp/headers.test.ts +0 -153
- package/test/mcp/oauth-browser.test.ts +0 -249
- package/test/memory/abort-leak.test.ts +0 -136
- package/test/patch/patch.test.ts +0 -348
- package/test/permission/arity.test.ts +0 -33
- package/test/permission/next.test.ts +0 -689
- package/test/permission-task.test.ts +0 -319
- package/test/plugin/auth-override.test.ts +0 -44
- package/test/plugin/codex.test.ts +0 -123
- package/test/preload.ts +0 -80
- package/test/project/project.test.ts +0 -348
- package/test/project/worktree-remove.test.ts +0 -65
- package/test/provider/amazon-bedrock.test.ts +0 -446
- package/test/provider/copilot/convert-to-copilot-messages.test.ts +0 -523
- package/test/provider/copilot/copilot-chat-model.test.ts +0 -592
- package/test/provider/gitlab-duo.test.ts +0 -262
- package/test/provider/provider.test.ts +0 -2220
- package/test/provider/transform.test.ts +0 -2353
- package/test/pty/pty-output-isolation.test.ts +0 -140
- package/test/question/question.test.ts +0 -300
- package/test/scheduler.test.ts +0 -73
- package/test/server/global-session-list.test.ts +0 -89
- package/test/server/session-list.test.ts +0 -90
- package/test/server/session-select.test.ts +0 -78
- package/test/session/compaction.test.ts +0 -423
- package/test/session/instruction.test.ts +0 -170
- package/test/session/llm.test.ts +0 -667
- package/test/session/message-v2.test.ts +0 -924
- package/test/session/prompt.test.ts +0 -211
- package/test/session/retry.test.ts +0 -188
- package/test/session/revert-compact.test.ts +0 -285
- package/test/session/session.test.ts +0 -71
- package/test/session/structured-output-integration.test.ts +0 -233
- package/test/session/structured-output.test.ts +0 -385
- package/test/skill/discovery.test.ts +0 -110
- package/test/skill/skill.test.ts +0 -388
- package/test/snapshot/snapshot.test.ts +0 -1180
- package/test/storage/json-migration.test.ts +0 -846
- package/test/tool/__snapshots__/tool.test.ts.snap +0 -9
- package/test/tool/apply_patch.test.ts +0 -566
- package/test/tool/bash.test.ts +0 -402
- package/test/tool/edit.test.ts +0 -496
- package/test/tool/external-directory.test.ts +0 -127
- package/test/tool/fixtures/large-image.png +0 -0
- package/test/tool/fixtures/models-api.json +0 -38413
- package/test/tool/grep.test.ts +0 -110
- package/test/tool/question.test.ts +0 -107
- package/test/tool/read.test.ts +0 -504
- package/test/tool/registry.test.ts +0 -122
- package/test/tool/skill.test.ts +0 -112
- package/test/tool/truncation.test.ts +0 -160
- package/test/tool/webfetch.test.ts +0 -100
- package/test/tool/write.test.ts +0 -348
- package/test/util/filesystem.test.ts +0 -443
- package/test/util/format.test.ts +0 -59
- package/test/util/glob.test.ts +0 -164
- package/test/util/iife.test.ts +0 -36
- package/test/util/lazy.test.ts +0 -50
- package/test/util/lock.test.ts +0 -72
- package/test/util/process.test.ts +0 -59
- package/test/util/timeout.test.ts +0 -21
- package/test/util/wildcard.test.ts +0 -90
- package/tsconfig.json +0 -16
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, test } from "bun:test"
|
|
2
|
-
import { Log } from "../../src/util/log"
|
|
3
|
-
import { WorkspaceServer } from "../../src/control-plane/workspace-server/server"
|
|
4
|
-
import { parseSSE } from "../../src/control-plane/sse"
|
|
5
|
-
import { GlobalBus } from "../../src/bus/global"
|
|
6
|
-
import { resetDatabase } from "../fixture/db"
|
|
7
|
-
|
|
8
|
-
afterEach(async () => {
|
|
9
|
-
await resetDatabase()
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
Log.init({ print: false })
|
|
13
|
-
|
|
14
|
-
describe("control-plane/workspace-server SSE", () => {
|
|
15
|
-
test("streams GlobalBus events and parseSSE reads them", async () => {
|
|
16
|
-
const app = WorkspaceServer.App()
|
|
17
|
-
const stop = new AbortController()
|
|
18
|
-
const seen: unknown[] = []
|
|
19
|
-
|
|
20
|
-
try {
|
|
21
|
-
const response = await app.request("/event", {
|
|
22
|
-
signal: stop.signal,
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
expect(response.status).toBe(200)
|
|
26
|
-
expect(response.body).toBeDefined()
|
|
27
|
-
|
|
28
|
-
const done = new Promise<void>((resolve, reject) => {
|
|
29
|
-
const timeout = setTimeout(() => {
|
|
30
|
-
reject(new Error("timed out waiting for workspace.test event"))
|
|
31
|
-
}, 3000)
|
|
32
|
-
|
|
33
|
-
void parseSSE(response.body!, stop.signal, (event) => {
|
|
34
|
-
seen.push(event)
|
|
35
|
-
const next = event as { type?: string }
|
|
36
|
-
if (next.type === "server.connected") {
|
|
37
|
-
GlobalBus.emit("event", {
|
|
38
|
-
payload: {
|
|
39
|
-
type: "workspace.test",
|
|
40
|
-
properties: { ok: true },
|
|
41
|
-
},
|
|
42
|
-
})
|
|
43
|
-
return
|
|
44
|
-
}
|
|
45
|
-
if (next.type !== "workspace.test") return
|
|
46
|
-
clearTimeout(timeout)
|
|
47
|
-
resolve()
|
|
48
|
-
}).catch((error) => {
|
|
49
|
-
clearTimeout(timeout)
|
|
50
|
-
reject(error)
|
|
51
|
-
})
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
await done
|
|
55
|
-
|
|
56
|
-
expect(seen.some((event) => (event as { type?: string }).type === "server.connected")).toBe(true)
|
|
57
|
-
expect(seen).toContainEqual({
|
|
58
|
-
type: "workspace.test",
|
|
59
|
-
properties: { ok: true },
|
|
60
|
-
})
|
|
61
|
-
} finally {
|
|
62
|
-
stop.abort()
|
|
63
|
-
}
|
|
64
|
-
})
|
|
65
|
-
})
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, mock, test } from "bun:test"
|
|
2
|
-
import { Identifier } from "../../src/id/id"
|
|
3
|
-
import { Log } from "../../src/util/log"
|
|
4
|
-
import { tmpdir } from "../fixture/fixture"
|
|
5
|
-
import { Project } from "../../src/project/project"
|
|
6
|
-
import { Database } from "../../src/storage/db"
|
|
7
|
-
import { WorkspaceTable } from "../../src/control-plane/workspace.sql"
|
|
8
|
-
import { GlobalBus } from "../../src/bus/global"
|
|
9
|
-
import { resetDatabase } from "../fixture/db"
|
|
10
|
-
|
|
11
|
-
afterEach(async () => {
|
|
12
|
-
mock.restore()
|
|
13
|
-
await resetDatabase()
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
Log.init({ print: false })
|
|
17
|
-
|
|
18
|
-
const seen: string[] = []
|
|
19
|
-
const remote = { type: "testing", name: "remote-a" } as unknown as typeof WorkspaceTable.$inferInsert.config
|
|
20
|
-
|
|
21
|
-
mock.module("../../src/control-plane/adaptors", () => ({
|
|
22
|
-
getAdaptor: (config: { type: string }) => {
|
|
23
|
-
seen.push(config.type)
|
|
24
|
-
return {
|
|
25
|
-
async create() {
|
|
26
|
-
throw new Error("not used")
|
|
27
|
-
},
|
|
28
|
-
async remove() {},
|
|
29
|
-
async request() {
|
|
30
|
-
const body = new ReadableStream<Uint8Array>({
|
|
31
|
-
start(controller) {
|
|
32
|
-
const encoder = new TextEncoder()
|
|
33
|
-
controller.enqueue(encoder.encode('data: {"type":"remote.ready","properties":{}}\n\n'))
|
|
34
|
-
controller.close()
|
|
35
|
-
},
|
|
36
|
-
})
|
|
37
|
-
return new Response(body, {
|
|
38
|
-
status: 200,
|
|
39
|
-
headers: {
|
|
40
|
-
"content-type": "text/event-stream",
|
|
41
|
-
},
|
|
42
|
-
})
|
|
43
|
-
},
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
}))
|
|
47
|
-
|
|
48
|
-
describe("control-plane/workspace.startSyncing", () => {
|
|
49
|
-
test("syncs only remote workspaces and emits remote SSE events", async () => {
|
|
50
|
-
const { Workspace } = await import("../../src/control-plane/workspace")
|
|
51
|
-
await using tmp = await tmpdir({ git: true })
|
|
52
|
-
const { project } = await Project.fromDirectory(tmp.path)
|
|
53
|
-
|
|
54
|
-
const id1 = Identifier.descending("workspace")
|
|
55
|
-
const id2 = Identifier.descending("workspace")
|
|
56
|
-
|
|
57
|
-
Database.use((db) =>
|
|
58
|
-
db
|
|
59
|
-
.insert(WorkspaceTable)
|
|
60
|
-
.values([
|
|
61
|
-
{
|
|
62
|
-
id: id1,
|
|
63
|
-
branch: "main",
|
|
64
|
-
project_id: project.id,
|
|
65
|
-
config: remote,
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
id: id2,
|
|
69
|
-
branch: "main",
|
|
70
|
-
project_id: project.id,
|
|
71
|
-
config: { type: "worktree", directory: tmp.path },
|
|
72
|
-
},
|
|
73
|
-
])
|
|
74
|
-
.run(),
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
const done = new Promise<void>((resolve) => {
|
|
78
|
-
const listener = (event: { directory?: string; payload: { type: string } }) => {
|
|
79
|
-
if (event.directory !== id1) return
|
|
80
|
-
if (event.payload.type !== "remote.ready") return
|
|
81
|
-
GlobalBus.off("event", listener)
|
|
82
|
-
resolve()
|
|
83
|
-
}
|
|
84
|
-
GlobalBus.on("event", listener)
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
const sync = Workspace.startSyncing(project)
|
|
88
|
-
await Promise.race([
|
|
89
|
-
done,
|
|
90
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error("timed out waiting for sync event")), 2000)),
|
|
91
|
-
])
|
|
92
|
-
|
|
93
|
-
await sync.stop()
|
|
94
|
-
expect(seen).toContain("testing")
|
|
95
|
-
expect(seen).not.toContain("worktree")
|
|
96
|
-
})
|
|
97
|
-
})
|
package/test/file/ignore.test.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { test, expect } from "bun:test"
|
|
2
|
-
import { FileIgnore } from "../../src/file/ignore"
|
|
3
|
-
|
|
4
|
-
test("match nested and non-nested", () => {
|
|
5
|
-
expect(FileIgnore.match("node_modules/index.js")).toBe(true)
|
|
6
|
-
expect(FileIgnore.match("node_modules")).toBe(true)
|
|
7
|
-
expect(FileIgnore.match("node_modules/")).toBe(true)
|
|
8
|
-
expect(FileIgnore.match("node_modules/bar")).toBe(true)
|
|
9
|
-
expect(FileIgnore.match("node_modules/bar/")).toBe(true)
|
|
10
|
-
})
|
package/test/file/index.test.ts
DELETED
|
@@ -1,394 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "bun:test"
|
|
2
|
-
import path from "path"
|
|
3
|
-
import fs from "fs/promises"
|
|
4
|
-
import { File } from "../../src/file"
|
|
5
|
-
import { Instance } from "../../src/project/instance"
|
|
6
|
-
import { Filesystem } from "../../src/util/filesystem"
|
|
7
|
-
import { tmpdir } from "../fixture/fixture"
|
|
8
|
-
|
|
9
|
-
describe("file/index Filesystem patterns", () => {
|
|
10
|
-
describe("File.read() - text content", () => {
|
|
11
|
-
test("reads text file via Filesystem.readText()", async () => {
|
|
12
|
-
await using tmp = await tmpdir()
|
|
13
|
-
const filepath = path.join(tmp.path, "test.txt")
|
|
14
|
-
await fs.writeFile(filepath, "Hello World", "utf-8")
|
|
15
|
-
|
|
16
|
-
await Instance.provide({
|
|
17
|
-
directory: tmp.path,
|
|
18
|
-
fn: async () => {
|
|
19
|
-
const result = await File.read("test.txt")
|
|
20
|
-
expect(result.type).toBe("text")
|
|
21
|
-
expect(result.content).toBe("Hello World")
|
|
22
|
-
},
|
|
23
|
-
})
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
test("reads with Filesystem.exists() check", async () => {
|
|
27
|
-
await using tmp = await tmpdir()
|
|
28
|
-
|
|
29
|
-
await Instance.provide({
|
|
30
|
-
directory: tmp.path,
|
|
31
|
-
fn: async () => {
|
|
32
|
-
// Non-existent file should return empty content
|
|
33
|
-
const result = await File.read("nonexistent.txt")
|
|
34
|
-
expect(result.type).toBe("text")
|
|
35
|
-
expect(result.content).toBe("")
|
|
36
|
-
},
|
|
37
|
-
})
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
test("trims whitespace from text content", async () => {
|
|
41
|
-
await using tmp = await tmpdir()
|
|
42
|
-
const filepath = path.join(tmp.path, "test.txt")
|
|
43
|
-
await fs.writeFile(filepath, " content with spaces \n\n", "utf-8")
|
|
44
|
-
|
|
45
|
-
await Instance.provide({
|
|
46
|
-
directory: tmp.path,
|
|
47
|
-
fn: async () => {
|
|
48
|
-
const result = await File.read("test.txt")
|
|
49
|
-
expect(result.content).toBe("content with spaces")
|
|
50
|
-
},
|
|
51
|
-
})
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
test("handles empty text file", async () => {
|
|
55
|
-
await using tmp = await tmpdir()
|
|
56
|
-
const filepath = path.join(tmp.path, "empty.txt")
|
|
57
|
-
await fs.writeFile(filepath, "", "utf-8")
|
|
58
|
-
|
|
59
|
-
await Instance.provide({
|
|
60
|
-
directory: tmp.path,
|
|
61
|
-
fn: async () => {
|
|
62
|
-
const result = await File.read("empty.txt")
|
|
63
|
-
expect(result.type).toBe("text")
|
|
64
|
-
expect(result.content).toBe("")
|
|
65
|
-
},
|
|
66
|
-
})
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
test("handles multi-line text files", async () => {
|
|
70
|
-
await using tmp = await tmpdir()
|
|
71
|
-
const filepath = path.join(tmp.path, "multiline.txt")
|
|
72
|
-
await fs.writeFile(filepath, "line1\nline2\nline3", "utf-8")
|
|
73
|
-
|
|
74
|
-
await Instance.provide({
|
|
75
|
-
directory: tmp.path,
|
|
76
|
-
fn: async () => {
|
|
77
|
-
const result = await File.read("multiline.txt")
|
|
78
|
-
expect(result.content).toBe("line1\nline2\nline3")
|
|
79
|
-
},
|
|
80
|
-
})
|
|
81
|
-
})
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
describe("File.read() - binary content", () => {
|
|
85
|
-
test("reads binary file via Filesystem.readArrayBuffer()", async () => {
|
|
86
|
-
await using tmp = await tmpdir()
|
|
87
|
-
const filepath = path.join(tmp.path, "image.png")
|
|
88
|
-
const binaryContent = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
|
|
89
|
-
await fs.writeFile(filepath, binaryContent)
|
|
90
|
-
|
|
91
|
-
await Instance.provide({
|
|
92
|
-
directory: tmp.path,
|
|
93
|
-
fn: async () => {
|
|
94
|
-
const result = await File.read("image.png")
|
|
95
|
-
expect(result.type).toBe("text") // Images return as text with base64 encoding
|
|
96
|
-
expect(result.encoding).toBe("base64")
|
|
97
|
-
expect(result.mimeType).toBe("image/png")
|
|
98
|
-
expect(result.content).toBe(binaryContent.toString("base64"))
|
|
99
|
-
},
|
|
100
|
-
})
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
test("returns empty for binary non-image files", async () => {
|
|
104
|
-
await using tmp = await tmpdir()
|
|
105
|
-
const filepath = path.join(tmp.path, "binary.so")
|
|
106
|
-
await fs.writeFile(filepath, Buffer.from([0x7f, 0x45, 0x4c, 0x46]), "binary")
|
|
107
|
-
|
|
108
|
-
await Instance.provide({
|
|
109
|
-
directory: tmp.path,
|
|
110
|
-
fn: async () => {
|
|
111
|
-
const result = await File.read("binary.so")
|
|
112
|
-
expect(result.type).toBe("binary")
|
|
113
|
-
expect(result.content).toBe("")
|
|
114
|
-
},
|
|
115
|
-
})
|
|
116
|
-
})
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
describe("File.read() - Filesystem.mimeType()", () => {
|
|
120
|
-
test("detects MIME type via Filesystem.mimeType()", async () => {
|
|
121
|
-
await using tmp = await tmpdir()
|
|
122
|
-
const filepath = path.join(tmp.path, "test.json")
|
|
123
|
-
await fs.writeFile(filepath, '{"key": "value"}', "utf-8")
|
|
124
|
-
|
|
125
|
-
await Instance.provide({
|
|
126
|
-
directory: tmp.path,
|
|
127
|
-
fn: async () => {
|
|
128
|
-
expect(Filesystem.mimeType(filepath)).toContain("application/json")
|
|
129
|
-
|
|
130
|
-
const result = await File.read("test.json")
|
|
131
|
-
expect(result.type).toBe("text")
|
|
132
|
-
},
|
|
133
|
-
})
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
test("handles various image MIME types", async () => {
|
|
137
|
-
await using tmp = await tmpdir()
|
|
138
|
-
const testCases = [
|
|
139
|
-
{ ext: "jpg", mime: "image/jpeg" },
|
|
140
|
-
{ ext: "png", mime: "image/png" },
|
|
141
|
-
{ ext: "gif", mime: "image/gif" },
|
|
142
|
-
{ ext: "webp", mime: "image/webp" },
|
|
143
|
-
]
|
|
144
|
-
|
|
145
|
-
for (const { ext, mime } of testCases) {
|
|
146
|
-
const filepath = path.join(tmp.path, `test.${ext}`)
|
|
147
|
-
await fs.writeFile(filepath, Buffer.from([0x00, 0x00, 0x00, 0x00]), "binary")
|
|
148
|
-
|
|
149
|
-
await Instance.provide({
|
|
150
|
-
directory: tmp.path,
|
|
151
|
-
fn: async () => {
|
|
152
|
-
expect(Filesystem.mimeType(filepath)).toContain(mime)
|
|
153
|
-
},
|
|
154
|
-
})
|
|
155
|
-
}
|
|
156
|
-
})
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
describe("File.list() - Filesystem.exists() and readText()", () => {
|
|
160
|
-
test("reads .gitignore via Filesystem.exists() and readText()", async () => {
|
|
161
|
-
await using tmp = await tmpdir({ git: true })
|
|
162
|
-
|
|
163
|
-
await Instance.provide({
|
|
164
|
-
directory: tmp.path,
|
|
165
|
-
fn: async () => {
|
|
166
|
-
const gitignorePath = path.join(tmp.path, ".gitignore")
|
|
167
|
-
await fs.writeFile(gitignorePath, "node_modules\ndist\n", "utf-8")
|
|
168
|
-
|
|
169
|
-
// This is used internally in File.list()
|
|
170
|
-
expect(await Filesystem.exists(gitignorePath)).toBe(true)
|
|
171
|
-
|
|
172
|
-
const content = await Filesystem.readText(gitignorePath)
|
|
173
|
-
expect(content).toContain("node_modules")
|
|
174
|
-
},
|
|
175
|
-
})
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
test("reads .ignore file similarly", async () => {
|
|
179
|
-
await using tmp = await tmpdir({ git: true })
|
|
180
|
-
|
|
181
|
-
await Instance.provide({
|
|
182
|
-
directory: tmp.path,
|
|
183
|
-
fn: async () => {
|
|
184
|
-
const ignorePath = path.join(tmp.path, ".ignore")
|
|
185
|
-
await fs.writeFile(ignorePath, "*.log\n.env\n", "utf-8")
|
|
186
|
-
|
|
187
|
-
expect(await Filesystem.exists(ignorePath)).toBe(true)
|
|
188
|
-
expect(await Filesystem.readText(ignorePath)).toContain("*.log")
|
|
189
|
-
},
|
|
190
|
-
})
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
test("handles missing .gitignore gracefully", async () => {
|
|
194
|
-
await using tmp = await tmpdir({ git: true })
|
|
195
|
-
|
|
196
|
-
await Instance.provide({
|
|
197
|
-
directory: tmp.path,
|
|
198
|
-
fn: async () => {
|
|
199
|
-
const gitignorePath = path.join(tmp.path, ".gitignore")
|
|
200
|
-
expect(await Filesystem.exists(gitignorePath)).toBe(false)
|
|
201
|
-
|
|
202
|
-
// File.list() should still work
|
|
203
|
-
const nodes = await File.list()
|
|
204
|
-
expect(Array.isArray(nodes)).toBe(true)
|
|
205
|
-
},
|
|
206
|
-
})
|
|
207
|
-
})
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
describe("File.changed() - Filesystem.readText() for untracked files", () => {
|
|
211
|
-
test("reads untracked files via Filesystem.readText()", async () => {
|
|
212
|
-
await using tmp = await tmpdir({ git: true })
|
|
213
|
-
|
|
214
|
-
await Instance.provide({
|
|
215
|
-
directory: tmp.path,
|
|
216
|
-
fn: async () => {
|
|
217
|
-
const untrackedPath = path.join(tmp.path, "untracked.txt")
|
|
218
|
-
await fs.writeFile(untrackedPath, "new content\nwith multiple lines", "utf-8")
|
|
219
|
-
|
|
220
|
-
// This is how File.changed() reads untracked files
|
|
221
|
-
const content = await Filesystem.readText(untrackedPath)
|
|
222
|
-
const lines = content.split("\n").length
|
|
223
|
-
expect(lines).toBe(2)
|
|
224
|
-
},
|
|
225
|
-
})
|
|
226
|
-
})
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
describe("Error handling", () => {
|
|
230
|
-
test("handles errors gracefully in Filesystem.readText()", async () => {
|
|
231
|
-
await using tmp = await tmpdir()
|
|
232
|
-
const filepath = path.join(tmp.path, "readonly.txt")
|
|
233
|
-
await fs.writeFile(filepath, "content", "utf-8")
|
|
234
|
-
|
|
235
|
-
await Instance.provide({
|
|
236
|
-
directory: tmp.path,
|
|
237
|
-
fn: async () => {
|
|
238
|
-
const nonExistentPath = path.join(tmp.path, "does-not-exist.txt")
|
|
239
|
-
// Filesystem.readText() on non-existent file throws
|
|
240
|
-
await expect(Filesystem.readText(nonExistentPath)).rejects.toThrow()
|
|
241
|
-
|
|
242
|
-
// But File.read() handles this gracefully
|
|
243
|
-
const result = await File.read("does-not-exist.txt")
|
|
244
|
-
expect(result.content).toBe("")
|
|
245
|
-
},
|
|
246
|
-
})
|
|
247
|
-
})
|
|
248
|
-
|
|
249
|
-
test("handles errors in Filesystem.readArrayBuffer()", async () => {
|
|
250
|
-
await using tmp = await tmpdir()
|
|
251
|
-
|
|
252
|
-
await Instance.provide({
|
|
253
|
-
directory: tmp.path,
|
|
254
|
-
fn: async () => {
|
|
255
|
-
const nonExistentPath = path.join(tmp.path, "does-not-exist.bin")
|
|
256
|
-
const buffer = await Filesystem.readArrayBuffer(nonExistentPath).catch(() => new ArrayBuffer(0))
|
|
257
|
-
expect(buffer.byteLength).toBe(0)
|
|
258
|
-
},
|
|
259
|
-
})
|
|
260
|
-
})
|
|
261
|
-
|
|
262
|
-
test("returns empty array buffer on error for images", async () => {
|
|
263
|
-
await using tmp = await tmpdir()
|
|
264
|
-
const filepath = path.join(tmp.path, "broken.png")
|
|
265
|
-
// Don't create the file
|
|
266
|
-
|
|
267
|
-
await Instance.provide({
|
|
268
|
-
directory: tmp.path,
|
|
269
|
-
fn: async () => {
|
|
270
|
-
// File.read() handles missing images gracefully
|
|
271
|
-
const result = await File.read("broken.png")
|
|
272
|
-
expect(result.type).toBe("text")
|
|
273
|
-
expect(result.content).toBe("")
|
|
274
|
-
},
|
|
275
|
-
})
|
|
276
|
-
})
|
|
277
|
-
})
|
|
278
|
-
|
|
279
|
-
describe("shouldEncode() logic", () => {
|
|
280
|
-
test("treats .ts files as text", async () => {
|
|
281
|
-
await using tmp = await tmpdir()
|
|
282
|
-
const filepath = path.join(tmp.path, "test.ts")
|
|
283
|
-
await fs.writeFile(filepath, "export const value = 1", "utf-8")
|
|
284
|
-
|
|
285
|
-
await Instance.provide({
|
|
286
|
-
directory: tmp.path,
|
|
287
|
-
fn: async () => {
|
|
288
|
-
const result = await File.read("test.ts")
|
|
289
|
-
expect(result.type).toBe("text")
|
|
290
|
-
expect(result.content).toBe("export const value = 1")
|
|
291
|
-
},
|
|
292
|
-
})
|
|
293
|
-
})
|
|
294
|
-
|
|
295
|
-
test("treats .mts files as text", async () => {
|
|
296
|
-
await using tmp = await tmpdir()
|
|
297
|
-
const filepath = path.join(tmp.path, "test.mts")
|
|
298
|
-
await fs.writeFile(filepath, "export const value = 1", "utf-8")
|
|
299
|
-
|
|
300
|
-
await Instance.provide({
|
|
301
|
-
directory: tmp.path,
|
|
302
|
-
fn: async () => {
|
|
303
|
-
const result = await File.read("test.mts")
|
|
304
|
-
expect(result.type).toBe("text")
|
|
305
|
-
expect(result.content).toBe("export const value = 1")
|
|
306
|
-
},
|
|
307
|
-
})
|
|
308
|
-
})
|
|
309
|
-
|
|
310
|
-
test("treats .sh files as text", async () => {
|
|
311
|
-
await using tmp = await tmpdir()
|
|
312
|
-
const filepath = path.join(tmp.path, "test.sh")
|
|
313
|
-
await fs.writeFile(filepath, "#!/usr/bin/env bash\necho hello", "utf-8")
|
|
314
|
-
|
|
315
|
-
await Instance.provide({
|
|
316
|
-
directory: tmp.path,
|
|
317
|
-
fn: async () => {
|
|
318
|
-
const result = await File.read("test.sh")
|
|
319
|
-
expect(result.type).toBe("text")
|
|
320
|
-
expect(result.content).toBe("#!/usr/bin/env bash\necho hello")
|
|
321
|
-
},
|
|
322
|
-
})
|
|
323
|
-
})
|
|
324
|
-
|
|
325
|
-
test("treats Dockerfile as text", async () => {
|
|
326
|
-
await using tmp = await tmpdir()
|
|
327
|
-
const filepath = path.join(tmp.path, "Dockerfile")
|
|
328
|
-
await fs.writeFile(filepath, "FROM alpine:3.20", "utf-8")
|
|
329
|
-
|
|
330
|
-
await Instance.provide({
|
|
331
|
-
directory: tmp.path,
|
|
332
|
-
fn: async () => {
|
|
333
|
-
const result = await File.read("Dockerfile")
|
|
334
|
-
expect(result.type).toBe("text")
|
|
335
|
-
expect(result.content).toBe("FROM alpine:3.20")
|
|
336
|
-
},
|
|
337
|
-
})
|
|
338
|
-
})
|
|
339
|
-
|
|
340
|
-
test("returns encoding info for text files", async () => {
|
|
341
|
-
await using tmp = await tmpdir()
|
|
342
|
-
const filepath = path.join(tmp.path, "test.txt")
|
|
343
|
-
await fs.writeFile(filepath, "simple text", "utf-8")
|
|
344
|
-
|
|
345
|
-
await Instance.provide({
|
|
346
|
-
directory: tmp.path,
|
|
347
|
-
fn: async () => {
|
|
348
|
-
const result = await File.read("test.txt")
|
|
349
|
-
expect(result.encoding).toBeUndefined()
|
|
350
|
-
expect(result.type).toBe("text")
|
|
351
|
-
},
|
|
352
|
-
})
|
|
353
|
-
})
|
|
354
|
-
|
|
355
|
-
test("returns base64 encoding for images", async () => {
|
|
356
|
-
await using tmp = await tmpdir()
|
|
357
|
-
const filepath = path.join(tmp.path, "test.jpg")
|
|
358
|
-
await fs.writeFile(filepath, Buffer.from([0xff, 0xd8, 0xff, 0xe0]), "binary")
|
|
359
|
-
|
|
360
|
-
await Instance.provide({
|
|
361
|
-
directory: tmp.path,
|
|
362
|
-
fn: async () => {
|
|
363
|
-
const result = await File.read("test.jpg")
|
|
364
|
-
expect(result.encoding).toBe("base64")
|
|
365
|
-
expect(result.mimeType).toBe("image/jpeg")
|
|
366
|
-
},
|
|
367
|
-
})
|
|
368
|
-
})
|
|
369
|
-
})
|
|
370
|
-
|
|
371
|
-
describe("Path security", () => {
|
|
372
|
-
test("throws for paths outside project directory", async () => {
|
|
373
|
-
await using tmp = await tmpdir()
|
|
374
|
-
|
|
375
|
-
await Instance.provide({
|
|
376
|
-
directory: tmp.path,
|
|
377
|
-
fn: async () => {
|
|
378
|
-
await expect(File.read("../outside.txt")).rejects.toThrow("Access denied")
|
|
379
|
-
},
|
|
380
|
-
})
|
|
381
|
-
})
|
|
382
|
-
|
|
383
|
-
test("throws for paths outside project directory", async () => {
|
|
384
|
-
await using tmp = await tmpdir()
|
|
385
|
-
|
|
386
|
-
await Instance.provide({
|
|
387
|
-
directory: tmp.path,
|
|
388
|
-
fn: async () => {
|
|
389
|
-
await expect(File.read("../outside.txt")).rejects.toThrow("Access denied")
|
|
390
|
-
},
|
|
391
|
-
})
|
|
392
|
-
})
|
|
393
|
-
})
|
|
394
|
-
})
|