@stonerzju/opencode 1.2.16-offline.1
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/AGENTS.md +10 -0
- package/BUN_SHELL_MIGRATION_PLAN.md +136 -0
- package/Dockerfile +18 -0
- package/README.md +15 -0
- package/bin/opencode +179 -0
- package/bunfig.toml +7 -0
- package/drizzle.config.ts +10 -0
- package/migration/20260127222353_familiar_lady_ursula/migration.sql +90 -0
- package/migration/20260127222353_familiar_lady_ursula/snapshot.json +796 -0
- package/migration/20260211171708_add_project_commands/migration.sql +1 -0
- package/migration/20260211171708_add_project_commands/snapshot.json +806 -0
- package/migration/20260213144116_wakeful_the_professor/migration.sql +11 -0
- package/migration/20260213144116_wakeful_the_professor/snapshot.json +897 -0
- package/migration/20260225215848_workspace/migration.sql +7 -0
- package/migration/20260225215848_workspace/snapshot.json +959 -0
- package/package.json +140 -0
- package/package.json.bak +140 -0
- package/parsers-config.ts +254 -0
- package/script/build.ts +224 -0
- package/script/check-migrations.ts +16 -0
- package/script/postinstall.mjs +131 -0
- package/script/publish.ts +181 -0
- package/script/schema.ts +63 -0
- package/script/seed-e2e.ts +50 -0
- package/src/acp/README.md +174 -0
- package/src/acp/agent.ts +1741 -0
- package/src/acp/session.ts +116 -0
- package/src/acp/types.ts +23 -0
- package/src/agent/agent.ts +339 -0
- package/src/agent/generate.txt +75 -0
- package/src/agent/prompt/compaction.txt +14 -0
- package/src/agent/prompt/explore.txt +18 -0
- package/src/agent/prompt/summary.txt +11 -0
- package/src/agent/prompt/title.txt +44 -0
- package/src/auth/index.ts +68 -0
- package/src/bun/index.ts +131 -0
- package/src/bun/registry.ts +50 -0
- package/src/bus/bus-event.ts +43 -0
- package/src/bus/global.ts +10 -0
- package/src/bus/index.ts +105 -0
- package/src/cli/bootstrap.ts +17 -0
- package/src/cli/cmd/acp.ts +70 -0
- package/src/cli/cmd/agent.ts +257 -0
- package/src/cli/cmd/auth.ts +449 -0
- package/src/cli/cmd/cmd.ts +7 -0
- package/src/cli/cmd/db.ts +118 -0
- package/src/cli/cmd/debug/agent.ts +167 -0
- package/src/cli/cmd/debug/config.ts +16 -0
- package/src/cli/cmd/debug/file.ts +97 -0
- package/src/cli/cmd/debug/index.ts +48 -0
- package/src/cli/cmd/debug/lsp.ts +52 -0
- package/src/cli/cmd/debug/ripgrep.ts +87 -0
- package/src/cli/cmd/debug/scrap.ts +16 -0
- package/src/cli/cmd/debug/skill.ts +16 -0
- package/src/cli/cmd/debug/snapshot.ts +52 -0
- package/src/cli/cmd/export.ts +88 -0
- package/src/cli/cmd/generate.ts +38 -0
- package/src/cli/cmd/github.ts +1631 -0
- package/src/cli/cmd/import.ts +170 -0
- package/src/cli/cmd/mcp.ts +754 -0
- package/src/cli/cmd/models.ts +77 -0
- package/src/cli/cmd/pr.ts +112 -0
- package/src/cli/cmd/run.ts +625 -0
- package/src/cli/cmd/serve.ts +31 -0
- package/src/cli/cmd/session.ts +156 -0
- package/src/cli/cmd/stats.ts +410 -0
- package/src/cli/cmd/tui/app.tsx +845 -0
- package/src/cli/cmd/tui/attach.ts +88 -0
- package/src/cli/cmd/tui/component/border.tsx +21 -0
- package/src/cli/cmd/tui/component/dialog-agent.tsx +31 -0
- package/src/cli/cmd/tui/component/dialog-command.tsx +147 -0
- package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
- package/src/cli/cmd/tui/component/dialog-model.tsx +165 -0
- package/src/cli/cmd/tui/component/dialog-provider.tsx +259 -0
- package/src/cli/cmd/tui/component/dialog-session-list.tsx +108 -0
- package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
- package/src/cli/cmd/tui/component/dialog-skill.tsx +36 -0
- package/src/cli/cmd/tui/component/dialog-stash.tsx +87 -0
- package/src/cli/cmd/tui/component/dialog-status.tsx +167 -0
- package/src/cli/cmd/tui/component/dialog-tag.tsx +44 -0
- package/src/cli/cmd/tui/component/dialog-theme-list.tsx +50 -0
- package/src/cli/cmd/tui/component/logo.tsx +85 -0
- package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +667 -0
- package/src/cli/cmd/tui/component/prompt/frecency.tsx +90 -0
- package/src/cli/cmd/tui/component/prompt/history.tsx +108 -0
- package/src/cli/cmd/tui/component/prompt/index.tsx +1155 -0
- package/src/cli/cmd/tui/component/prompt/stash.tsx +101 -0
- package/src/cli/cmd/tui/component/spinner.tsx +24 -0
- package/src/cli/cmd/tui/component/textarea-keybindings.ts +73 -0
- package/src/cli/cmd/tui/component/tips.tsx +152 -0
- package/src/cli/cmd/tui/component/todo-item.tsx +32 -0
- package/src/cli/cmd/tui/context/args.tsx +15 -0
- package/src/cli/cmd/tui/context/directory.ts +13 -0
- package/src/cli/cmd/tui/context/exit.tsx +53 -0
- package/src/cli/cmd/tui/context/helper.tsx +25 -0
- package/src/cli/cmd/tui/context/keybind.tsx +102 -0
- package/src/cli/cmd/tui/context/kv.tsx +52 -0
- package/src/cli/cmd/tui/context/local.tsx +406 -0
- package/src/cli/cmd/tui/context/prompt.tsx +18 -0
- package/src/cli/cmd/tui/context/route.tsx +46 -0
- package/src/cli/cmd/tui/context/sdk.tsx +101 -0
- package/src/cli/cmd/tui/context/sync.tsx +488 -0
- package/src/cli/cmd/tui/context/theme/aura.json +69 -0
- package/src/cli/cmd/tui/context/theme/ayu.json +80 -0
- package/src/cli/cmd/tui/context/theme/carbonfox.json +248 -0
- package/src/cli/cmd/tui/context/theme/catppuccin-frappe.json +233 -0
- package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +233 -0
- package/src/cli/cmd/tui/context/theme/catppuccin.json +112 -0
- package/src/cli/cmd/tui/context/theme/cobalt2.json +228 -0
- package/src/cli/cmd/tui/context/theme/cursor.json +249 -0
- package/src/cli/cmd/tui/context/theme/dracula.json +219 -0
- package/src/cli/cmd/tui/context/theme/everforest.json +241 -0
- package/src/cli/cmd/tui/context/theme/flexoki.json +237 -0
- package/src/cli/cmd/tui/context/theme/github.json +233 -0
- package/src/cli/cmd/tui/context/theme/gruvbox.json +242 -0
- package/src/cli/cmd/tui/context/theme/kanagawa.json +77 -0
- package/src/cli/cmd/tui/context/theme/lucent-orng.json +237 -0
- package/src/cli/cmd/tui/context/theme/material.json +235 -0
- package/src/cli/cmd/tui/context/theme/matrix.json +77 -0
- package/src/cli/cmd/tui/context/theme/mercury.json +252 -0
- package/src/cli/cmd/tui/context/theme/monokai.json +221 -0
- package/src/cli/cmd/tui/context/theme/nightowl.json +221 -0
- package/src/cli/cmd/tui/context/theme/nord.json +223 -0
- package/src/cli/cmd/tui/context/theme/one-dark.json +84 -0
- package/src/cli/cmd/tui/context/theme/orng.json +249 -0
- package/src/cli/cmd/tui/context/theme/osaka-jade.json +93 -0
- package/src/cli/cmd/tui/context/theme/palenight.json +222 -0
- package/src/cli/cmd/tui/context/theme/rosepine.json +234 -0
- package/src/cli/cmd/tui/context/theme/solarized.json +223 -0
- package/src/cli/cmd/tui/context/theme/synthwave84.json +226 -0
- package/src/cli/cmd/tui/context/theme/tokyonight.json +243 -0
- package/src/cli/cmd/tui/context/theme/vercel.json +245 -0
- package/src/cli/cmd/tui/context/theme/vesper.json +218 -0
- package/src/cli/cmd/tui/context/theme/zenburn.json +223 -0
- package/src/cli/cmd/tui/context/theme.tsx +1152 -0
- package/src/cli/cmd/tui/context/tui-config.tsx +9 -0
- package/src/cli/cmd/tui/event.ts +48 -0
- package/src/cli/cmd/tui/routes/home.tsx +145 -0
- package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +64 -0
- package/src/cli/cmd/tui/routes/session/dialog-message.tsx +109 -0
- package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +26 -0
- package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +47 -0
- package/src/cli/cmd/tui/routes/session/footer.tsx +91 -0
- package/src/cli/cmd/tui/routes/session/header.tsx +135 -0
- package/src/cli/cmd/tui/routes/session/index.tsx +2219 -0
- package/src/cli/cmd/tui/routes/session/permission.tsx +685 -0
- package/src/cli/cmd/tui/routes/session/question.tsx +466 -0
- package/src/cli/cmd/tui/routes/session/sidebar.tsx +321 -0
- package/src/cli/cmd/tui/thread.ts +199 -0
- package/src/cli/cmd/tui/ui/dialog-alert.tsx +59 -0
- package/src/cli/cmd/tui/ui/dialog-confirm.tsx +85 -0
- package/src/cli/cmd/tui/ui/dialog-export-options.tsx +207 -0
- package/src/cli/cmd/tui/ui/dialog-help.tsx +40 -0
- package/src/cli/cmd/tui/ui/dialog-prompt.tsx +80 -0
- package/src/cli/cmd/tui/ui/dialog-select.tsx +401 -0
- package/src/cli/cmd/tui/ui/dialog.tsx +182 -0
- package/src/cli/cmd/tui/ui/link.tsx +28 -0
- package/src/cli/cmd/tui/ui/spinner.ts +368 -0
- package/src/cli/cmd/tui/ui/toast.tsx +100 -0
- package/src/cli/cmd/tui/util/clipboard.ts +164 -0
- package/src/cli/cmd/tui/util/editor.ts +33 -0
- package/src/cli/cmd/tui/util/selection.ts +25 -0
- package/src/cli/cmd/tui/util/signal.ts +7 -0
- package/src/cli/cmd/tui/util/terminal.ts +114 -0
- package/src/cli/cmd/tui/util/transcript.ts +98 -0
- package/src/cli/cmd/tui/win32.ts +129 -0
- package/src/cli/cmd/tui/worker.ts +157 -0
- package/src/cli/cmd/uninstall.ts +356 -0
- package/src/cli/cmd/upgrade.ts +73 -0
- package/src/cli/cmd/web.ts +81 -0
- package/src/cli/cmd/workspace-serve.ts +16 -0
- package/src/cli/error.ts +57 -0
- package/src/cli/logo.ts +6 -0
- package/src/cli/network.ts +60 -0
- package/src/cli/ui.ts +116 -0
- package/src/cli/upgrade.ts +25 -0
- package/src/command/index.ts +150 -0
- package/src/command/template/initialize.txt +10 -0
- package/src/command/template/review.txt +101 -0
- package/src/config/config.ts +1408 -0
- package/src/config/markdown.ts +99 -0
- package/src/config/migrate-tui-config.ts +155 -0
- package/src/config/paths.ts +174 -0
- package/src/config/tui-schema.ts +34 -0
- package/src/config/tui.ts +118 -0
- package/src/control/control.sql.ts +22 -0
- package/src/control/index.ts +67 -0
- package/src/control-plane/adaptors/index.ts +10 -0
- package/src/control-plane/adaptors/types.ts +7 -0
- package/src/control-plane/adaptors/worktree.ts +26 -0
- package/src/control-plane/config.ts +10 -0
- package/src/control-plane/session-proxy-middleware.ts +46 -0
- package/src/control-plane/sse.ts +66 -0
- package/src/control-plane/workspace-server/routes.ts +33 -0
- package/src/control-plane/workspace-server/server.ts +24 -0
- package/src/control-plane/workspace.sql.ts +12 -0
- package/src/control-plane/workspace.ts +160 -0
- package/src/env/index.ts +28 -0
- package/src/file/ignore.ts +82 -0
- package/src/file/index.ts +646 -0
- package/src/file/ripgrep.ts +372 -0
- package/src/file/time.ts +71 -0
- package/src/file/watcher.ts +128 -0
- package/src/flag/flag.ts +109 -0
- package/src/format/formatter.ts +395 -0
- package/src/format/index.ts +140 -0
- package/src/global/index.ts +54 -0
- package/src/id/id.ts +84 -0
- package/src/ide/index.ts +76 -0
- package/src/index.ts +210 -0
- package/src/installation/index.ts +266 -0
- package/src/lsp/client.ts +251 -0
- package/src/lsp/index.ts +485 -0
- package/src/lsp/language.ts +120 -0
- package/src/lsp/server.ts +2142 -0
- package/src/mcp/auth.ts +130 -0
- package/src/mcp/index.ts +937 -0
- package/src/mcp/oauth-callback.ts +200 -0
- package/src/mcp/oauth-provider.ts +176 -0
- package/src/patch/index.ts +680 -0
- package/src/permission/arity.ts +163 -0
- package/src/permission/index.ts +210 -0
- package/src/permission/next.ts +286 -0
- package/src/plugin/codex.ts +624 -0
- package/src/plugin/copilot.ts +327 -0
- package/src/plugin/index.ts +143 -0
- package/src/project/bootstrap.ts +33 -0
- package/src/project/instance.ts +114 -0
- package/src/project/project.sql.ts +15 -0
- package/src/project/project.ts +441 -0
- package/src/project/state.ts +70 -0
- package/src/project/vcs.ts +76 -0
- package/src/provider/auth.ts +147 -0
- package/src/provider/error.ts +189 -0
- package/src/provider/models.ts +146 -0
- package/src/provider/provider.ts +1338 -0
- package/src/provider/sdk/copilot/README.md +5 -0
- package/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts +164 -0
- package/src/provider/sdk/copilot/chat/get-response-metadata.ts +15 -0
- package/src/provider/sdk/copilot/chat/map-openai-compatible-finish-reason.ts +17 -0
- package/src/provider/sdk/copilot/chat/openai-compatible-api-types.ts +64 -0
- package/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts +780 -0
- package/src/provider/sdk/copilot/chat/openai-compatible-chat-options.ts +28 -0
- package/src/provider/sdk/copilot/chat/openai-compatible-metadata-extractor.ts +44 -0
- package/src/provider/sdk/copilot/chat/openai-compatible-prepare-tools.ts +87 -0
- package/src/provider/sdk/copilot/copilot-provider.ts +100 -0
- package/src/provider/sdk/copilot/index.ts +2 -0
- package/src/provider/sdk/copilot/openai-compatible-error.ts +27 -0
- package/src/provider/sdk/copilot/responses/convert-to-openai-responses-input.ts +303 -0
- package/src/provider/sdk/copilot/responses/map-openai-responses-finish-reason.ts +22 -0
- package/src/provider/sdk/copilot/responses/openai-config.ts +18 -0
- package/src/provider/sdk/copilot/responses/openai-error.ts +22 -0
- package/src/provider/sdk/copilot/responses/openai-responses-api-types.ts +207 -0
- package/src/provider/sdk/copilot/responses/openai-responses-language-model.ts +1732 -0
- package/src/provider/sdk/copilot/responses/openai-responses-prepare-tools.ts +177 -0
- package/src/provider/sdk/copilot/responses/openai-responses-settings.ts +1 -0
- package/src/provider/sdk/copilot/responses/tool/code-interpreter.ts +88 -0
- package/src/provider/sdk/copilot/responses/tool/file-search.ts +128 -0
- package/src/provider/sdk/copilot/responses/tool/image-generation.ts +115 -0
- package/src/provider/sdk/copilot/responses/tool/local-shell.ts +65 -0
- package/src/provider/sdk/copilot/responses/tool/web-search-preview.ts +104 -0
- package/src/provider/sdk/copilot/responses/tool/web-search.ts +103 -0
- package/src/provider/transform.ts +955 -0
- package/src/pty/index.ts +324 -0
- package/src/question/index.ts +171 -0
- package/src/scheduler/index.ts +61 -0
- package/src/server/error.ts +36 -0
- package/src/server/event.ts +7 -0
- package/src/server/mdns.ts +60 -0
- package/src/server/routes/config.ts +92 -0
- package/src/server/routes/experimental.ts +270 -0
- package/src/server/routes/file.ts +197 -0
- package/src/server/routes/global.ts +185 -0
- package/src/server/routes/mcp.ts +225 -0
- package/src/server/routes/permission.ts +68 -0
- package/src/server/routes/project.ts +82 -0
- package/src/server/routes/provider.ts +165 -0
- package/src/server/routes/pty.ts +200 -0
- package/src/server/routes/question.ts +98 -0
- package/src/server/routes/session.ts +974 -0
- package/src/server/routes/tui.ts +379 -0
- package/src/server/routes/workspace.ts +104 -0
- package/src/server/server.ts +623 -0
- package/src/session/compaction.ts +261 -0
- package/src/session/index.ts +877 -0
- package/src/session/instruction.ts +192 -0
- package/src/session/llm.ts +279 -0
- package/src/session/message-v2.ts +899 -0
- package/src/session/message.ts +189 -0
- package/src/session/processor.ts +421 -0
- package/src/session/prompt/anthropic-20250930.txt +166 -0
- package/src/session/prompt/anthropic.txt +105 -0
- package/src/session/prompt/beast.txt +147 -0
- package/src/session/prompt/build-switch.txt +5 -0
- package/src/session/prompt/codex_header.txt +79 -0
- package/src/session/prompt/copilot-gpt-5.txt +143 -0
- package/src/session/prompt/gemini.txt +155 -0
- package/src/session/prompt/max-steps.txt +16 -0
- package/src/session/prompt/plan-reminder-anthropic.txt +67 -0
- package/src/session/prompt/plan.txt +26 -0
- package/src/session/prompt/qwen.txt +109 -0
- package/src/session/prompt/trinity.txt +97 -0
- package/src/session/prompt.ts +1959 -0
- package/src/session/retry.ts +101 -0
- package/src/session/revert.ts +138 -0
- package/src/session/session.sql.ts +88 -0
- package/src/session/status.ts +76 -0
- package/src/session/summary.ts +161 -0
- package/src/session/system.ts +54 -0
- package/src/session/todo.ts +56 -0
- package/src/share/share-next.ts +210 -0
- package/src/share/share.sql.ts +13 -0
- package/src/shell/shell.ts +68 -0
- package/src/skill/discovery.ts +98 -0
- package/src/skill/index.ts +1 -0
- package/src/skill/skill.ts +189 -0
- package/src/snapshot/index.ts +297 -0
- package/src/sql.d.ts +4 -0
- package/src/storage/db.ts +155 -0
- package/src/storage/json-migration.ts +425 -0
- package/src/storage/schema.sql.ts +10 -0
- package/src/storage/schema.ts +5 -0
- package/src/storage/storage.ts +220 -0
- package/src/tool/apply_patch.ts +281 -0
- package/src/tool/apply_patch.txt +33 -0
- package/src/tool/bash.ts +274 -0
- package/src/tool/bash.txt +115 -0
- package/src/tool/batch.ts +181 -0
- package/src/tool/batch.txt +24 -0
- package/src/tool/codesearch.ts +132 -0
- package/src/tool/codesearch.txt +12 -0
- package/src/tool/edit.ts +654 -0
- package/src/tool/edit.txt +10 -0
- package/src/tool/external-directory.ts +32 -0
- package/src/tool/glob.ts +78 -0
- package/src/tool/glob.txt +6 -0
- package/src/tool/grep.ts +156 -0
- package/src/tool/grep.txt +8 -0
- package/src/tool/invalid.ts +17 -0
- package/src/tool/ls.ts +121 -0
- package/src/tool/ls.txt +1 -0
- package/src/tool/lsp.ts +97 -0
- package/src/tool/lsp.txt +19 -0
- package/src/tool/multiedit.ts +46 -0
- package/src/tool/multiedit.txt +41 -0
- package/src/tool/plan-enter.txt +14 -0
- package/src/tool/plan-exit.txt +13 -0
- package/src/tool/plan.ts +131 -0
- package/src/tool/question.ts +33 -0
- package/src/tool/question.txt +10 -0
- package/src/tool/read.ts +293 -0
- package/src/tool/read.txt +14 -0
- package/src/tool/registry.ts +173 -0
- package/src/tool/skill.ts +123 -0
- package/src/tool/task.ts +165 -0
- package/src/tool/task.txt +60 -0
- package/src/tool/todo.ts +53 -0
- package/src/tool/todoread.txt +14 -0
- package/src/tool/todowrite.txt +167 -0
- package/src/tool/tool.ts +89 -0
- package/src/tool/truncation.ts +107 -0
- package/src/tool/webfetch.ts +206 -0
- package/src/tool/webfetch.txt +13 -0
- package/src/tool/websearch.ts +150 -0
- package/src/tool/websearch.txt +14 -0
- package/src/tool/write.ts +84 -0
- package/src/tool/write.txt +8 -0
- package/src/util/abort.ts +35 -0
- package/src/util/archive.ts +16 -0
- package/src/util/color.ts +19 -0
- package/src/util/context.ts +25 -0
- package/src/util/defer.ts +12 -0
- package/src/util/eventloop.ts +20 -0
- package/src/util/filesystem.ts +189 -0
- package/src/util/fn.ts +11 -0
- package/src/util/format.ts +20 -0
- package/src/util/git.ts +35 -0
- package/src/util/glob.ts +34 -0
- package/src/util/iife.ts +3 -0
- package/src/util/keybind.ts +103 -0
- package/src/util/lazy.ts +23 -0
- package/src/util/locale.ts +81 -0
- package/src/util/lock.ts +98 -0
- package/src/util/log.ts +182 -0
- package/src/util/process.ts +126 -0
- package/src/util/proxied.ts +3 -0
- package/src/util/queue.ts +32 -0
- package/src/util/rpc.ts +66 -0
- package/src/util/scrap.ts +10 -0
- package/src/util/signal.ts +12 -0
- package/src/util/timeout.ts +14 -0
- package/src/util/token.ts +7 -0
- package/src/util/wildcard.ts +59 -0
- package/src/worktree/index.ts +643 -0
- package/sst-env.d.ts +10 -0
- package/test/AGENTS.md +81 -0
- package/test/acp/agent-interface.test.ts +51 -0
- package/test/acp/event-subscription.test.ts +683 -0
- package/test/agent/agent.test.ts +689 -0
- package/test/bun.test.ts +53 -0
- package/test/cli/github-action.test.ts +197 -0
- package/test/cli/github-remote.test.ts +80 -0
- package/test/cli/import.test.ts +38 -0
- package/test/cli/plugin-auth-picker.test.ts +120 -0
- package/test/cli/tui/transcript.test.ts +322 -0
- package/test/config/agent-color.test.ts +71 -0
- package/test/config/config.test.ts +1886 -0
- package/test/config/fixtures/empty-frontmatter.md +4 -0
- package/test/config/fixtures/frontmatter.md +28 -0
- package/test/config/fixtures/markdown-header.md +11 -0
- package/test/config/fixtures/no-frontmatter.md +1 -0
- package/test/config/fixtures/weird-model-id.md +13 -0
- package/test/config/markdown.test.ts +228 -0
- package/test/config/tui.test.ts +510 -0
- package/test/control-plane/session-proxy-middleware.test.ts +147 -0
- package/test/control-plane/sse.test.ts +56 -0
- package/test/control-plane/workspace-server-sse.test.ts +65 -0
- package/test/control-plane/workspace-sync.test.ts +97 -0
- package/test/file/ignore.test.ts +10 -0
- package/test/file/index.test.ts +394 -0
- package/test/file/path-traversal.test.ts +198 -0
- package/test/file/ripgrep.test.ts +39 -0
- package/test/file/time.test.ts +361 -0
- package/test/fixture/db.ts +11 -0
- package/test/fixture/fixture.ts +45 -0
- package/test/fixture/lsp/fake-lsp-server.js +77 -0
- package/test/fixture/skills/agents-sdk/SKILL.md +152 -0
- package/test/fixture/skills/agents-sdk/references/callable.md +92 -0
- package/test/fixture/skills/cloudflare/SKILL.md +211 -0
- package/test/fixture/skills/index.json +6 -0
- package/test/ide/ide.test.ts +82 -0
- package/test/keybind.test.ts +421 -0
- package/test/lsp/client.test.ts +95 -0
- package/test/mcp/headers.test.ts +153 -0
- package/test/mcp/oauth-browser.test.ts +249 -0
- package/test/memory/abort-leak.test.ts +136 -0
- package/test/patch/patch.test.ts +348 -0
- package/test/permission/arity.test.ts +33 -0
- package/test/permission/next.test.ts +689 -0
- package/test/permission-task.test.ts +319 -0
- package/test/plugin/auth-override.test.ts +44 -0
- package/test/plugin/codex.test.ts +123 -0
- package/test/preload.ts +80 -0
- package/test/project/project.test.ts +348 -0
- package/test/project/worktree-remove.test.ts +65 -0
- package/test/provider/amazon-bedrock.test.ts +446 -0
- package/test/provider/copilot/convert-to-copilot-messages.test.ts +523 -0
- package/test/provider/copilot/copilot-chat-model.test.ts +592 -0
- package/test/provider/gitlab-duo.test.ts +262 -0
- package/test/provider/provider.test.ts +2220 -0
- package/test/provider/transform.test.ts +2353 -0
- package/test/pty/pty-output-isolation.test.ts +140 -0
- package/test/question/question.test.ts +300 -0
- package/test/scheduler.test.ts +73 -0
- package/test/server/global-session-list.test.ts +89 -0
- package/test/server/session-list.test.ts +90 -0
- package/test/server/session-select.test.ts +78 -0
- package/test/session/compaction.test.ts +423 -0
- package/test/session/instruction.test.ts +170 -0
- package/test/session/llm.test.ts +667 -0
- package/test/session/message-v2.test.ts +924 -0
- package/test/session/prompt.test.ts +211 -0
- package/test/session/retry.test.ts +188 -0
- package/test/session/revert-compact.test.ts +285 -0
- package/test/session/session.test.ts +71 -0
- package/test/session/structured-output-integration.test.ts +233 -0
- package/test/session/structured-output.test.ts +385 -0
- package/test/skill/discovery.test.ts +110 -0
- package/test/skill/skill.test.ts +388 -0
- package/test/snapshot/snapshot.test.ts +1180 -0
- package/test/storage/json-migration.test.ts +846 -0
- package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
- package/test/tool/apply_patch.test.ts +566 -0
- package/test/tool/bash.test.ts +402 -0
- package/test/tool/edit.test.ts +496 -0
- package/test/tool/external-directory.test.ts +127 -0
- package/test/tool/fixtures/large-image.png +0 -0
- package/test/tool/fixtures/models-api.json +38413 -0
- package/test/tool/grep.test.ts +110 -0
- package/test/tool/question.test.ts +107 -0
- package/test/tool/read.test.ts +504 -0
- package/test/tool/registry.test.ts +122 -0
- package/test/tool/skill.test.ts +112 -0
- package/test/tool/truncation.test.ts +160 -0
- package/test/tool/webfetch.test.ts +100 -0
- package/test/tool/write.test.ts +348 -0
- package/test/util/filesystem.test.ts +443 -0
- package/test/util/format.test.ts +59 -0
- package/test/util/glob.test.ts +164 -0
- package/test/util/iife.test.ts +36 -0
- package/test/util/lazy.test.ts +50 -0
- package/test/util/lock.test.ts +72 -0
- package/test/util/process.test.ts +59 -0
- package/test/util/timeout.test.ts +21 -0
- package/test/util/wildcard.test.ts +90 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
import { afterEach, expect, test } from "bun:test"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import fs from "fs/promises"
|
|
4
|
+
import { tmpdir } from "../fixture/fixture"
|
|
5
|
+
import { Instance } from "../../src/project/instance"
|
|
6
|
+
import { TuiConfig } from "../../src/config/tui"
|
|
7
|
+
import { Global } from "../../src/global"
|
|
8
|
+
import { Filesystem } from "../../src/util/filesystem"
|
|
9
|
+
|
|
10
|
+
const managedConfigDir = process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR!
|
|
11
|
+
|
|
12
|
+
afterEach(async () => {
|
|
13
|
+
delete process.env.OPENCODE_CONFIG
|
|
14
|
+
delete process.env.OPENCODE_TUI_CONFIG
|
|
15
|
+
await fs.rm(path.join(Global.Path.config, "tui.json"), { force: true }).catch(() => {})
|
|
16
|
+
await fs.rm(path.join(Global.Path.config, "tui.jsonc"), { force: true }).catch(() => {})
|
|
17
|
+
await fs.rm(managedConfigDir, { force: true, recursive: true }).catch(() => {})
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test("loads tui config with the same precedence order as server config paths", async () => {
|
|
21
|
+
await using tmp = await tmpdir({
|
|
22
|
+
init: async (dir) => {
|
|
23
|
+
await Bun.write(path.join(Global.Path.config, "tui.json"), JSON.stringify({ theme: "global" }, null, 2))
|
|
24
|
+
await Bun.write(path.join(dir, "tui.json"), JSON.stringify({ theme: "project" }, null, 2))
|
|
25
|
+
await fs.mkdir(path.join(dir, ".opencode"), { recursive: true })
|
|
26
|
+
await Bun.write(
|
|
27
|
+
path.join(dir, ".opencode", "tui.json"),
|
|
28
|
+
JSON.stringify({ theme: "local", diff_style: "stacked" }, null, 2),
|
|
29
|
+
)
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
await Instance.provide({
|
|
34
|
+
directory: tmp.path,
|
|
35
|
+
fn: async () => {
|
|
36
|
+
const config = await TuiConfig.get()
|
|
37
|
+
expect(config.theme).toBe("local")
|
|
38
|
+
expect(config.diff_style).toBe("stacked")
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test("migrates tui-specific keys from opencode.json when tui.json does not exist", async () => {
|
|
44
|
+
await using tmp = await tmpdir({
|
|
45
|
+
init: async (dir) => {
|
|
46
|
+
await Bun.write(
|
|
47
|
+
path.join(dir, "opencode.json"),
|
|
48
|
+
JSON.stringify(
|
|
49
|
+
{
|
|
50
|
+
theme: "migrated-theme",
|
|
51
|
+
tui: { scroll_speed: 5 },
|
|
52
|
+
keybinds: { app_exit: "ctrl+q" },
|
|
53
|
+
},
|
|
54
|
+
null,
|
|
55
|
+
2,
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
await Instance.provide({
|
|
62
|
+
directory: tmp.path,
|
|
63
|
+
fn: async () => {
|
|
64
|
+
const config = await TuiConfig.get()
|
|
65
|
+
expect(config.theme).toBe("migrated-theme")
|
|
66
|
+
expect(config.scroll_speed).toBe(5)
|
|
67
|
+
expect(config.keybinds?.app_exit).toBe("ctrl+q")
|
|
68
|
+
const text = await Filesystem.readText(path.join(tmp.path, "tui.json"))
|
|
69
|
+
expect(JSON.parse(text)).toMatchObject({
|
|
70
|
+
theme: "migrated-theme",
|
|
71
|
+
scroll_speed: 5,
|
|
72
|
+
})
|
|
73
|
+
const server = JSON.parse(await Filesystem.readText(path.join(tmp.path, "opencode.json")))
|
|
74
|
+
expect(server.theme).toBeUndefined()
|
|
75
|
+
expect(server.keybinds).toBeUndefined()
|
|
76
|
+
expect(server.tui).toBeUndefined()
|
|
77
|
+
expect(await Filesystem.exists(path.join(tmp.path, "opencode.json.tui-migration.bak"))).toBe(true)
|
|
78
|
+
expect(await Filesystem.exists(path.join(tmp.path, "tui.json"))).toBe(true)
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test("migrates project legacy tui keys even when global tui.json already exists", async () => {
|
|
84
|
+
await using tmp = await tmpdir({
|
|
85
|
+
init: async (dir) => {
|
|
86
|
+
await Bun.write(path.join(Global.Path.config, "tui.json"), JSON.stringify({ theme: "global" }, null, 2))
|
|
87
|
+
await Bun.write(
|
|
88
|
+
path.join(dir, "opencode.json"),
|
|
89
|
+
JSON.stringify(
|
|
90
|
+
{
|
|
91
|
+
theme: "project-migrated",
|
|
92
|
+
tui: { scroll_speed: 2 },
|
|
93
|
+
},
|
|
94
|
+
null,
|
|
95
|
+
2,
|
|
96
|
+
),
|
|
97
|
+
)
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
await Instance.provide({
|
|
102
|
+
directory: tmp.path,
|
|
103
|
+
fn: async () => {
|
|
104
|
+
const config = await TuiConfig.get()
|
|
105
|
+
expect(config.theme).toBe("project-migrated")
|
|
106
|
+
expect(config.scroll_speed).toBe(2)
|
|
107
|
+
expect(await Filesystem.exists(path.join(tmp.path, "tui.json"))).toBe(true)
|
|
108
|
+
|
|
109
|
+
const server = JSON.parse(await Filesystem.readText(path.join(tmp.path, "opencode.json")))
|
|
110
|
+
expect(server.theme).toBeUndefined()
|
|
111
|
+
expect(server.tui).toBeUndefined()
|
|
112
|
+
},
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test("drops unknown legacy tui keys during migration", async () => {
|
|
117
|
+
await using tmp = await tmpdir({
|
|
118
|
+
init: async (dir) => {
|
|
119
|
+
await Bun.write(
|
|
120
|
+
path.join(dir, "opencode.json"),
|
|
121
|
+
JSON.stringify(
|
|
122
|
+
{
|
|
123
|
+
theme: "migrated-theme",
|
|
124
|
+
tui: { scroll_speed: 2, foo: 1 },
|
|
125
|
+
},
|
|
126
|
+
null,
|
|
127
|
+
2,
|
|
128
|
+
),
|
|
129
|
+
)
|
|
130
|
+
},
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
await Instance.provide({
|
|
134
|
+
directory: tmp.path,
|
|
135
|
+
fn: async () => {
|
|
136
|
+
const config = await TuiConfig.get()
|
|
137
|
+
expect(config.theme).toBe("migrated-theme")
|
|
138
|
+
expect(config.scroll_speed).toBe(2)
|
|
139
|
+
|
|
140
|
+
const text = await Filesystem.readText(path.join(tmp.path, "tui.json"))
|
|
141
|
+
const migrated = JSON.parse(text)
|
|
142
|
+
expect(migrated.scroll_speed).toBe(2)
|
|
143
|
+
expect(migrated.foo).toBeUndefined()
|
|
144
|
+
},
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test("skips migration when opencode.jsonc is syntactically invalid", async () => {
|
|
149
|
+
await using tmp = await tmpdir({
|
|
150
|
+
init: async (dir) => {
|
|
151
|
+
await Bun.write(
|
|
152
|
+
path.join(dir, "opencode.jsonc"),
|
|
153
|
+
`{
|
|
154
|
+
"theme": "broken-theme",
|
|
155
|
+
"tui": { "scroll_speed": 2 }
|
|
156
|
+
"username": "still-broken"
|
|
157
|
+
}`,
|
|
158
|
+
)
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
await Instance.provide({
|
|
163
|
+
directory: tmp.path,
|
|
164
|
+
fn: async () => {
|
|
165
|
+
const config = await TuiConfig.get()
|
|
166
|
+
expect(config.theme).toBeUndefined()
|
|
167
|
+
expect(config.scroll_speed).toBeUndefined()
|
|
168
|
+
expect(await Filesystem.exists(path.join(tmp.path, "tui.json"))).toBe(false)
|
|
169
|
+
expect(await Filesystem.exists(path.join(tmp.path, "opencode.jsonc.tui-migration.bak"))).toBe(false)
|
|
170
|
+
const source = await Filesystem.readText(path.join(tmp.path, "opencode.jsonc"))
|
|
171
|
+
expect(source).toContain('"theme": "broken-theme"')
|
|
172
|
+
expect(source).toContain('"tui": { "scroll_speed": 2 }')
|
|
173
|
+
},
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test("skips migration when tui.json already exists", async () => {
|
|
178
|
+
await using tmp = await tmpdir({
|
|
179
|
+
init: async (dir) => {
|
|
180
|
+
await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ theme: "legacy" }, null, 2))
|
|
181
|
+
await Bun.write(path.join(dir, "tui.json"), JSON.stringify({ diff_style: "stacked" }, null, 2))
|
|
182
|
+
},
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
await Instance.provide({
|
|
186
|
+
directory: tmp.path,
|
|
187
|
+
fn: async () => {
|
|
188
|
+
const config = await TuiConfig.get()
|
|
189
|
+
expect(config.diff_style).toBe("stacked")
|
|
190
|
+
expect(config.theme).toBeUndefined()
|
|
191
|
+
|
|
192
|
+
const server = JSON.parse(await Filesystem.readText(path.join(tmp.path, "opencode.json")))
|
|
193
|
+
expect(server.theme).toBe("legacy")
|
|
194
|
+
expect(await Filesystem.exists(path.join(tmp.path, "opencode.json.tui-migration.bak"))).toBe(false)
|
|
195
|
+
},
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
test("continues loading tui config when legacy source cannot be stripped", async () => {
|
|
200
|
+
await using tmp = await tmpdir({
|
|
201
|
+
init: async (dir) => {
|
|
202
|
+
await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ theme: "readonly-theme" }, null, 2))
|
|
203
|
+
},
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
const source = path.join(tmp.path, "opencode.json")
|
|
207
|
+
await fs.chmod(source, 0o444)
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
await Instance.provide({
|
|
211
|
+
directory: tmp.path,
|
|
212
|
+
fn: async () => {
|
|
213
|
+
const config = await TuiConfig.get()
|
|
214
|
+
expect(config.theme).toBe("readonly-theme")
|
|
215
|
+
expect(await Filesystem.exists(path.join(tmp.path, "tui.json"))).toBe(true)
|
|
216
|
+
|
|
217
|
+
const server = JSON.parse(await Filesystem.readText(source))
|
|
218
|
+
expect(server.theme).toBe("readonly-theme")
|
|
219
|
+
},
|
|
220
|
+
})
|
|
221
|
+
} finally {
|
|
222
|
+
await fs.chmod(source, 0o644)
|
|
223
|
+
}
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
test("migration backup preserves JSONC comments", async () => {
|
|
227
|
+
await using tmp = await tmpdir({
|
|
228
|
+
init: async (dir) => {
|
|
229
|
+
await Bun.write(
|
|
230
|
+
path.join(dir, "opencode.jsonc"),
|
|
231
|
+
`{
|
|
232
|
+
// top-level comment
|
|
233
|
+
"theme": "jsonc-theme",
|
|
234
|
+
"tui": {
|
|
235
|
+
// nested comment
|
|
236
|
+
"scroll_speed": 1.5
|
|
237
|
+
}
|
|
238
|
+
}`,
|
|
239
|
+
)
|
|
240
|
+
},
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
await Instance.provide({
|
|
244
|
+
directory: tmp.path,
|
|
245
|
+
fn: async () => {
|
|
246
|
+
await TuiConfig.get()
|
|
247
|
+
const backup = await Filesystem.readText(path.join(tmp.path, "opencode.jsonc.tui-migration.bak"))
|
|
248
|
+
expect(backup).toContain("// top-level comment")
|
|
249
|
+
expect(backup).toContain("// nested comment")
|
|
250
|
+
expect(backup).toContain('"theme": "jsonc-theme"')
|
|
251
|
+
expect(backup).toContain('"scroll_speed": 1.5')
|
|
252
|
+
},
|
|
253
|
+
})
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
test("migrates legacy tui keys across multiple opencode.json levels", async () => {
|
|
257
|
+
await using tmp = await tmpdir({
|
|
258
|
+
init: async (dir) => {
|
|
259
|
+
const nested = path.join(dir, "apps", "client")
|
|
260
|
+
await fs.mkdir(nested, { recursive: true })
|
|
261
|
+
await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ theme: "root-theme" }, null, 2))
|
|
262
|
+
await Bun.write(path.join(nested, "opencode.json"), JSON.stringify({ theme: "nested-theme" }, null, 2))
|
|
263
|
+
},
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
await Instance.provide({
|
|
267
|
+
directory: path.join(tmp.path, "apps", "client"),
|
|
268
|
+
fn: async () => {
|
|
269
|
+
const config = await TuiConfig.get()
|
|
270
|
+
expect(config.theme).toBe("nested-theme")
|
|
271
|
+
expect(await Filesystem.exists(path.join(tmp.path, "tui.json"))).toBe(true)
|
|
272
|
+
expect(await Filesystem.exists(path.join(tmp.path, "apps", "client", "tui.json"))).toBe(true)
|
|
273
|
+
},
|
|
274
|
+
})
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
test("flattens nested tui key inside tui.json", async () => {
|
|
278
|
+
await using tmp = await tmpdir({
|
|
279
|
+
init: async (dir) => {
|
|
280
|
+
await Bun.write(
|
|
281
|
+
path.join(dir, "tui.json"),
|
|
282
|
+
JSON.stringify({
|
|
283
|
+
theme: "outer",
|
|
284
|
+
tui: { scroll_speed: 3, diff_style: "stacked" },
|
|
285
|
+
}),
|
|
286
|
+
)
|
|
287
|
+
},
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
await Instance.provide({
|
|
291
|
+
directory: tmp.path,
|
|
292
|
+
fn: async () => {
|
|
293
|
+
const config = await TuiConfig.get()
|
|
294
|
+
expect(config.scroll_speed).toBe(3)
|
|
295
|
+
expect(config.diff_style).toBe("stacked")
|
|
296
|
+
// top-level keys take precedence over nested tui keys
|
|
297
|
+
expect(config.theme).toBe("outer")
|
|
298
|
+
},
|
|
299
|
+
})
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
test("top-level keys in tui.json take precedence over nested tui key", async () => {
|
|
303
|
+
await using tmp = await tmpdir({
|
|
304
|
+
init: async (dir) => {
|
|
305
|
+
await Bun.write(
|
|
306
|
+
path.join(dir, "tui.json"),
|
|
307
|
+
JSON.stringify({
|
|
308
|
+
diff_style: "auto",
|
|
309
|
+
tui: { diff_style: "stacked", scroll_speed: 2 },
|
|
310
|
+
}),
|
|
311
|
+
)
|
|
312
|
+
},
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
await Instance.provide({
|
|
316
|
+
directory: tmp.path,
|
|
317
|
+
fn: async () => {
|
|
318
|
+
const config = await TuiConfig.get()
|
|
319
|
+
expect(config.diff_style).toBe("auto")
|
|
320
|
+
expect(config.scroll_speed).toBe(2)
|
|
321
|
+
},
|
|
322
|
+
})
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
test("project config takes precedence over OPENCODE_TUI_CONFIG (matches OPENCODE_CONFIG)", async () => {
|
|
326
|
+
await using tmp = await tmpdir({
|
|
327
|
+
init: async (dir) => {
|
|
328
|
+
await Bun.write(path.join(dir, "tui.json"), JSON.stringify({ theme: "project", diff_style: "auto" }))
|
|
329
|
+
const custom = path.join(dir, "custom-tui.json")
|
|
330
|
+
await Bun.write(custom, JSON.stringify({ theme: "custom", diff_style: "stacked" }))
|
|
331
|
+
process.env.OPENCODE_TUI_CONFIG = custom
|
|
332
|
+
},
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
await Instance.provide({
|
|
336
|
+
directory: tmp.path,
|
|
337
|
+
fn: async () => {
|
|
338
|
+
const config = await TuiConfig.get()
|
|
339
|
+
// project tui.json overrides the custom path, same as server config precedence
|
|
340
|
+
expect(config.theme).toBe("project")
|
|
341
|
+
// project also set diff_style, so that wins
|
|
342
|
+
expect(config.diff_style).toBe("auto")
|
|
343
|
+
},
|
|
344
|
+
})
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
test("merges keybind overrides across precedence layers", async () => {
|
|
348
|
+
await using tmp = await tmpdir({
|
|
349
|
+
init: async (dir) => {
|
|
350
|
+
await Bun.write(path.join(Global.Path.config, "tui.json"), JSON.stringify({ keybinds: { app_exit: "ctrl+q" } }))
|
|
351
|
+
await Bun.write(path.join(dir, "tui.json"), JSON.stringify({ keybinds: { theme_list: "ctrl+k" } }))
|
|
352
|
+
},
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
await Instance.provide({
|
|
356
|
+
directory: tmp.path,
|
|
357
|
+
fn: async () => {
|
|
358
|
+
const config = await TuiConfig.get()
|
|
359
|
+
expect(config.keybinds?.app_exit).toBe("ctrl+q")
|
|
360
|
+
expect(config.keybinds?.theme_list).toBe("ctrl+k")
|
|
361
|
+
},
|
|
362
|
+
})
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
test("OPENCODE_TUI_CONFIG provides settings when no project config exists", async () => {
|
|
366
|
+
await using tmp = await tmpdir({
|
|
367
|
+
init: async (dir) => {
|
|
368
|
+
const custom = path.join(dir, "custom-tui.json")
|
|
369
|
+
await Bun.write(custom, JSON.stringify({ theme: "from-env", diff_style: "stacked" }))
|
|
370
|
+
process.env.OPENCODE_TUI_CONFIG = custom
|
|
371
|
+
},
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
await Instance.provide({
|
|
375
|
+
directory: tmp.path,
|
|
376
|
+
fn: async () => {
|
|
377
|
+
const config = await TuiConfig.get()
|
|
378
|
+
expect(config.theme).toBe("from-env")
|
|
379
|
+
expect(config.diff_style).toBe("stacked")
|
|
380
|
+
},
|
|
381
|
+
})
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
test("does not derive tui path from OPENCODE_CONFIG", async () => {
|
|
385
|
+
await using tmp = await tmpdir({
|
|
386
|
+
init: async (dir) => {
|
|
387
|
+
const customDir = path.join(dir, "custom")
|
|
388
|
+
await fs.mkdir(customDir, { recursive: true })
|
|
389
|
+
await Bun.write(path.join(customDir, "opencode.json"), JSON.stringify({ model: "test/model" }))
|
|
390
|
+
await Bun.write(path.join(customDir, "tui.json"), JSON.stringify({ theme: "should-not-load" }))
|
|
391
|
+
process.env.OPENCODE_CONFIG = path.join(customDir, "opencode.json")
|
|
392
|
+
},
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
await Instance.provide({
|
|
396
|
+
directory: tmp.path,
|
|
397
|
+
fn: async () => {
|
|
398
|
+
const config = await TuiConfig.get()
|
|
399
|
+
expect(config.theme).toBeUndefined()
|
|
400
|
+
},
|
|
401
|
+
})
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
test("applies env and file substitutions in tui.json", async () => {
|
|
405
|
+
const original = process.env.TUI_THEME_TEST
|
|
406
|
+
process.env.TUI_THEME_TEST = "env-theme"
|
|
407
|
+
try {
|
|
408
|
+
await using tmp = await tmpdir({
|
|
409
|
+
init: async (dir) => {
|
|
410
|
+
await Bun.write(path.join(dir, "keybind.txt"), "ctrl+q")
|
|
411
|
+
await Bun.write(
|
|
412
|
+
path.join(dir, "tui.json"),
|
|
413
|
+
JSON.stringify({
|
|
414
|
+
theme: "{env:TUI_THEME_TEST}",
|
|
415
|
+
keybinds: { app_exit: "{file:keybind.txt}" },
|
|
416
|
+
}),
|
|
417
|
+
)
|
|
418
|
+
},
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
await Instance.provide({
|
|
422
|
+
directory: tmp.path,
|
|
423
|
+
fn: async () => {
|
|
424
|
+
const config = await TuiConfig.get()
|
|
425
|
+
expect(config.theme).toBe("env-theme")
|
|
426
|
+
expect(config.keybinds?.app_exit).toBe("ctrl+q")
|
|
427
|
+
},
|
|
428
|
+
})
|
|
429
|
+
} finally {
|
|
430
|
+
if (original === undefined) delete process.env.TUI_THEME_TEST
|
|
431
|
+
else process.env.TUI_THEME_TEST = original
|
|
432
|
+
}
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
test("applies file substitutions when first identical token is in a commented line", async () => {
|
|
436
|
+
await using tmp = await tmpdir({
|
|
437
|
+
init: async (dir) => {
|
|
438
|
+
await Bun.write(path.join(dir, "theme.txt"), "resolved-theme")
|
|
439
|
+
await Bun.write(
|
|
440
|
+
path.join(dir, "tui.jsonc"),
|
|
441
|
+
`{
|
|
442
|
+
// "theme": "{file:theme.txt}",
|
|
443
|
+
"theme": "{file:theme.txt}"
|
|
444
|
+
}`,
|
|
445
|
+
)
|
|
446
|
+
},
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
await Instance.provide({
|
|
450
|
+
directory: tmp.path,
|
|
451
|
+
fn: async () => {
|
|
452
|
+
const config = await TuiConfig.get()
|
|
453
|
+
expect(config.theme).toBe("resolved-theme")
|
|
454
|
+
},
|
|
455
|
+
})
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
test("loads managed tui config and gives it highest precedence", async () => {
|
|
459
|
+
await using tmp = await tmpdir({
|
|
460
|
+
init: async (dir) => {
|
|
461
|
+
await Bun.write(path.join(dir, "tui.json"), JSON.stringify({ theme: "project-theme" }, null, 2))
|
|
462
|
+
await fs.mkdir(managedConfigDir, { recursive: true })
|
|
463
|
+
await Bun.write(path.join(managedConfigDir, "tui.json"), JSON.stringify({ theme: "managed-theme" }, null, 2))
|
|
464
|
+
},
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
await Instance.provide({
|
|
468
|
+
directory: tmp.path,
|
|
469
|
+
fn: async () => {
|
|
470
|
+
const config = await TuiConfig.get()
|
|
471
|
+
expect(config.theme).toBe("managed-theme")
|
|
472
|
+
},
|
|
473
|
+
})
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
test("loads .opencode/tui.json", async () => {
|
|
477
|
+
await using tmp = await tmpdir({
|
|
478
|
+
init: async (dir) => {
|
|
479
|
+
await fs.mkdir(path.join(dir, ".opencode"), { recursive: true })
|
|
480
|
+
await Bun.write(path.join(dir, ".opencode", "tui.json"), JSON.stringify({ diff_style: "stacked" }, null, 2))
|
|
481
|
+
},
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
await Instance.provide({
|
|
485
|
+
directory: tmp.path,
|
|
486
|
+
fn: async () => {
|
|
487
|
+
const config = await TuiConfig.get()
|
|
488
|
+
expect(config.diff_style).toBe("stacked")
|
|
489
|
+
},
|
|
490
|
+
})
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
test("gracefully falls back when tui.json has invalid JSON", async () => {
|
|
494
|
+
await using tmp = await tmpdir({
|
|
495
|
+
init: async (dir) => {
|
|
496
|
+
await Bun.write(path.join(dir, "tui.json"), "{ invalid json }")
|
|
497
|
+
await fs.mkdir(managedConfigDir, { recursive: true })
|
|
498
|
+
await Bun.write(path.join(managedConfigDir, "tui.json"), JSON.stringify({ theme: "managed-fallback" }, null, 2))
|
|
499
|
+
},
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
await Instance.provide({
|
|
503
|
+
directory: tmp.path,
|
|
504
|
+
fn: async () => {
|
|
505
|
+
const config = await TuiConfig.get()
|
|
506
|
+
expect(config.theme).toBe("managed-fallback")
|
|
507
|
+
expect(config.keybinds).toBeDefined()
|
|
508
|
+
},
|
|
509
|
+
})
|
|
510
|
+
})
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { afterEach, describe, expect, mock, test } from "bun:test"
|
|
2
|
+
import { Identifier } from "../../src/id/id"
|
|
3
|
+
import { Hono } from "hono"
|
|
4
|
+
import { tmpdir } from "../fixture/fixture"
|
|
5
|
+
import { Project } from "../../src/project/project"
|
|
6
|
+
import { WorkspaceTable } from "../../src/control-plane/workspace.sql"
|
|
7
|
+
import { Instance } from "../../src/project/instance"
|
|
8
|
+
import { Database } from "../../src/storage/db"
|
|
9
|
+
import { resetDatabase } from "../fixture/db"
|
|
10
|
+
|
|
11
|
+
afterEach(async () => {
|
|
12
|
+
mock.restore()
|
|
13
|
+
await resetDatabase()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
type State = {
|
|
17
|
+
workspace?: "first" | "second"
|
|
18
|
+
calls: Array<{ method: string; url: string; body?: string }>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const remote = { type: "testing", name: "remote-a" } as unknown as typeof WorkspaceTable.$inferInsert.config
|
|
22
|
+
|
|
23
|
+
async function setup(state: State) {
|
|
24
|
+
mock.module("../../src/control-plane/adaptors", () => ({
|
|
25
|
+
getAdaptor: () => ({
|
|
26
|
+
request: async (_config: unknown, method: string, url: string, data?: BodyInit) => {
|
|
27
|
+
const body = data ? await new Response(data).text() : undefined
|
|
28
|
+
state.calls.push({ method, url, body })
|
|
29
|
+
return new Response("proxied", { status: 202 })
|
|
30
|
+
},
|
|
31
|
+
}),
|
|
32
|
+
}))
|
|
33
|
+
|
|
34
|
+
await using tmp = await tmpdir({ git: true })
|
|
35
|
+
const { project } = await Project.fromDirectory(tmp.path)
|
|
36
|
+
|
|
37
|
+
const id1 = Identifier.descending("workspace")
|
|
38
|
+
const id2 = Identifier.descending("workspace")
|
|
39
|
+
|
|
40
|
+
Database.use((db) =>
|
|
41
|
+
db
|
|
42
|
+
.insert(WorkspaceTable)
|
|
43
|
+
.values([
|
|
44
|
+
{
|
|
45
|
+
id: id1,
|
|
46
|
+
branch: "main",
|
|
47
|
+
project_id: project.id,
|
|
48
|
+
config: remote,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: id2,
|
|
52
|
+
branch: "main",
|
|
53
|
+
project_id: project.id,
|
|
54
|
+
config: { type: "worktree", directory: tmp.path },
|
|
55
|
+
},
|
|
56
|
+
])
|
|
57
|
+
.run(),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
const { SessionProxyMiddleware } = await import("../../src/control-plane/session-proxy-middleware")
|
|
61
|
+
const app = new Hono().use(SessionProxyMiddleware)
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
id1,
|
|
65
|
+
id2,
|
|
66
|
+
app,
|
|
67
|
+
async request(input: RequestInfo | URL, init?: RequestInit) {
|
|
68
|
+
return Instance.provide({
|
|
69
|
+
directory: state.workspace === "first" ? id1 : id2,
|
|
70
|
+
fn: async () => app.request(input, init),
|
|
71
|
+
})
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
describe("control-plane/session-proxy-middleware", () => {
|
|
77
|
+
test("forwards non-GET session requests for remote workspaces", async () => {
|
|
78
|
+
const state: State = {
|
|
79
|
+
workspace: "first",
|
|
80
|
+
calls: [],
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const ctx = await setup(state)
|
|
84
|
+
|
|
85
|
+
ctx.app.post("/session/foo", (c) => c.text("local", 200))
|
|
86
|
+
const response = await ctx.request("http://workspace.test/session/foo?x=1", {
|
|
87
|
+
method: "POST",
|
|
88
|
+
body: JSON.stringify({ hello: "world" }),
|
|
89
|
+
headers: {
|
|
90
|
+
"content-type": "application/json",
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
expect(response.status).toBe(202)
|
|
95
|
+
expect(await response.text()).toBe("proxied")
|
|
96
|
+
expect(state.calls).toEqual([
|
|
97
|
+
{
|
|
98
|
+
method: "POST",
|
|
99
|
+
url: "/session/foo?x=1",
|
|
100
|
+
body: '{"hello":"world"}',
|
|
101
|
+
},
|
|
102
|
+
])
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test("does not forward GET requests", async () => {
|
|
106
|
+
const state: State = {
|
|
107
|
+
workspace: "first",
|
|
108
|
+
calls: [],
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const ctx = await setup(state)
|
|
112
|
+
|
|
113
|
+
ctx.app.get("/session/foo", (c) => c.text("local", 200))
|
|
114
|
+
const response = await ctx.request("http://workspace.test/session/foo?x=1")
|
|
115
|
+
|
|
116
|
+
expect(response.status).toBe(200)
|
|
117
|
+
expect(await response.text()).toBe("local")
|
|
118
|
+
expect(state.calls).toEqual([])
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test("does not forward GET or POST requests for worktree workspaces", async () => {
|
|
122
|
+
const state: State = {
|
|
123
|
+
workspace: "second",
|
|
124
|
+
calls: [],
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const ctx = await setup(state)
|
|
128
|
+
|
|
129
|
+
ctx.app.get("/session/foo", (c) => c.text("local-get", 200))
|
|
130
|
+
ctx.app.post("/session/foo", (c) => c.text("local-post", 200))
|
|
131
|
+
|
|
132
|
+
const getResponse = await ctx.request("http://workspace.test/session/foo?x=1")
|
|
133
|
+
const postResponse = await ctx.request("http://workspace.test/session/foo?x=1", {
|
|
134
|
+
method: "POST",
|
|
135
|
+
body: JSON.stringify({ hello: "world" }),
|
|
136
|
+
headers: {
|
|
137
|
+
"content-type": "application/json",
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
expect(getResponse.status).toBe(200)
|
|
142
|
+
expect(await getResponse.text()).toBe("local-get")
|
|
143
|
+
expect(postResponse.status).toBe(200)
|
|
144
|
+
expect(await postResponse.text()).toBe("local-post")
|
|
145
|
+
expect(state.calls).toEqual([])
|
|
146
|
+
})
|
|
147
|
+
})
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test"
|
|
2
|
+
import { parseSSE } from "../../src/control-plane/sse"
|
|
3
|
+
import { resetDatabase } from "../fixture/db"
|
|
4
|
+
|
|
5
|
+
afterEach(async () => {
|
|
6
|
+
await resetDatabase()
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
function stream(chunks: string[]) {
|
|
10
|
+
return new ReadableStream<Uint8Array>({
|
|
11
|
+
start(controller) {
|
|
12
|
+
const encoder = new TextEncoder()
|
|
13
|
+
chunks.forEach((chunk) => controller.enqueue(encoder.encode(chunk)))
|
|
14
|
+
controller.close()
|
|
15
|
+
},
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("control-plane/sse", () => {
|
|
20
|
+
test("parses JSON events with CRLF and multiline data blocks", async () => {
|
|
21
|
+
const events: unknown[] = []
|
|
22
|
+
const stop = new AbortController()
|
|
23
|
+
|
|
24
|
+
await parseSSE(
|
|
25
|
+
stream([
|
|
26
|
+
'data: {"type":"one","properties":{"ok":true}}\r\n\r\n',
|
|
27
|
+
'data: {"type":"two",\r\ndata: "properties":{"n":2}}\r\n\r\n',
|
|
28
|
+
]),
|
|
29
|
+
stop.signal,
|
|
30
|
+
(event) => events.push(event),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
expect(events).toEqual([
|
|
34
|
+
{ type: "one", properties: { ok: true } },
|
|
35
|
+
{ type: "two", properties: { n: 2 } },
|
|
36
|
+
])
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test("falls back to sse.message for non-json payload", async () => {
|
|
40
|
+
const events: unknown[] = []
|
|
41
|
+
const stop = new AbortController()
|
|
42
|
+
|
|
43
|
+
await parseSSE(stream(["id: abc\nretry: 1500\ndata: hello world\n\n"]), stop.signal, (event) => events.push(event))
|
|
44
|
+
|
|
45
|
+
expect(events).toEqual([
|
|
46
|
+
{
|
|
47
|
+
type: "sse.message",
|
|
48
|
+
properties: {
|
|
49
|
+
data: "hello world",
|
|
50
|
+
id: "abc",
|
|
51
|
+
retry: 1500,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
])
|
|
55
|
+
})
|
|
56
|
+
})
|