opencode-dux 1.0.0
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/LICENSE +21 -0
- package/README.md +452 -0
- package/dist/agents/descriptions.d.ts +6 -0
- package/dist/agents/designer.d.ts +2 -0
- package/dist/agents/explorer.d.ts +2 -0
- package/dist/agents/fixer.d.ts +2 -0
- package/dist/agents/index.d.ts +22 -0
- package/dist/agents/interpreter.d.ts +2 -0
- package/dist/agents/librarian.d.ts +2 -0
- package/dist/agents/oracle.d.ts +2 -0
- package/dist/agents/orchestrator.d.ts +27 -0
- package/dist/agents/overrides.d.ts +18 -0
- package/dist/agents/prompt-blocks.d.ts +97 -0
- package/dist/agents/steward.d.ts +3 -0
- package/dist/cli/config-io.d.ts +24 -0
- package/dist/cli/config-manager.d.ts +4 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +1006 -0
- package/dist/cli/install.d.ts +2 -0
- package/dist/cli/mcps.d.ts +13 -0
- package/dist/cli/model-key-normalization.d.ts +1 -0
- package/dist/cli/paths.d.ts +35 -0
- package/dist/cli/providers.d.ts +137 -0
- package/dist/cli/skills.d.ts +22 -0
- package/dist/cli/system.d.ts +5 -0
- package/dist/cli/types.d.ts +38 -0
- package/dist/config/constants.d.ts +12 -0
- package/dist/config/index.d.ts +4 -0
- package/dist/config/loader.d.ts +40 -0
- package/dist/config/runtime-preset.d.ts +12 -0
- package/dist/config/schema.d.ts +281 -0
- package/dist/config/utils.d.ts +10 -0
- package/dist/discovery/local/types.d.ts +79 -0
- package/dist/discovery/local.d.ts +73 -0
- package/dist/discovery/mcp-servers.d.ts +88 -0
- package/dist/discovery/skills.d.ts +94 -0
- package/dist/hooks/apply-patch/codec.d.ts +7 -0
- package/dist/hooks/apply-patch/errors.d.ts +25 -0
- package/dist/hooks/apply-patch/execution-context.d.ts +27 -0
- package/dist/hooks/apply-patch/index.d.ts +15 -0
- package/dist/hooks/apply-patch/matching.d.ts +26 -0
- package/dist/hooks/apply-patch/operations.d.ts +3 -0
- package/dist/hooks/apply-patch/patch.d.ts +2 -0
- package/dist/hooks/apply-patch/prepared-changes.d.ts +17 -0
- package/dist/hooks/apply-patch/resolution.d.ts +19 -0
- package/dist/hooks/apply-patch/rewrite.d.ts +7 -0
- package/dist/hooks/apply-patch/test-helpers.d.ts +6 -0
- package/dist/hooks/apply-patch/types.d.ts +80 -0
- package/dist/hooks/auto-update-checker/cache.d.ts +11 -0
- package/dist/hooks/auto-update-checker/checker.d.ts +32 -0
- package/dist/hooks/auto-update-checker/constants.d.ts +11 -0
- package/dist/hooks/auto-update-checker/index.d.ts +18 -0
- package/dist/hooks/auto-update-checker/types.d.ts +22 -0
- package/dist/hooks/chat-headers.d.ts +16 -0
- package/dist/hooks/context-pressure-reminder/index.d.ts +33 -0
- package/dist/hooks/delegate-task-retry/guidance.d.ts +2 -0
- package/dist/hooks/delegate-task-retry/hook.d.ts +8 -0
- package/dist/hooks/delegate-task-retry/index.d.ts +4 -0
- package/dist/hooks/delegate-task-retry/patterns.d.ts +11 -0
- package/dist/hooks/filter-available-skills/index.d.ts +32 -0
- package/dist/hooks/foreground-fallback/index.d.ts +72 -0
- package/dist/hooks/image-hook.d.ts +5 -0
- package/dist/hooks/index.d.ts +14 -0
- package/dist/hooks/json-error-recovery/hook.d.ts +18 -0
- package/dist/hooks/json-error-recovery/index.d.ts +1 -0
- package/dist/hooks/phase-reminder/index.d.ts +26 -0
- package/dist/hooks/post-file-tool-nudge/index.d.ts +19 -0
- package/dist/hooks/task-session-manager/index.d.ts +52 -0
- package/dist/hooks/todo-continuation/index.d.ts +53 -0
- package/dist/hooks/todo-continuation/todo-hygiene.d.ts +35 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +31782 -0
- package/dist/mcp/context7.d.ts +6 -0
- package/dist/mcp/grep-app.d.ts +6 -0
- package/dist/mcp/index.d.ts +13 -0
- package/dist/mcp/types.d.ts +12 -0
- package/dist/mcp/websearch.d.ts +9 -0
- package/dist/skills/registry.d.ts +29 -0
- package/dist/subscriptions/accounts-store.d.ts +57 -0
- package/dist/subscriptions/index.d.ts +13 -0
- package/dist/subscriptions/neuralwatt-scraper.d.ts +14 -0
- package/dist/subscriptions/opencode-go-scraper.d.ts +27 -0
- package/dist/subscriptions/types.d.ts +115 -0
- package/dist/subscriptions/usage-service.d.ts +74 -0
- package/dist/tools/ast-grep/cli.d.ts +15 -0
- package/dist/tools/ast-grep/constants.d.ts +25 -0
- package/dist/tools/ast-grep/downloader.d.ts +5 -0
- package/dist/tools/ast-grep/index.d.ts +10 -0
- package/dist/tools/ast-grep/tools.d.ts +3 -0
- package/dist/tools/ast-grep/types.d.ts +30 -0
- package/dist/tools/ast-grep/utils.d.ts +4 -0
- package/dist/tools/delegate.d.ts +14 -0
- package/dist/tools/index.d.ts +5 -0
- package/dist/tools/preset-manager.d.ts +27 -0
- package/dist/tools/smartfetch/binary.d.ts +3 -0
- package/dist/tools/smartfetch/cache.d.ts +6 -0
- package/dist/tools/smartfetch/constants.d.ts +12 -0
- package/dist/tools/smartfetch/index.d.ts +3 -0
- package/dist/tools/smartfetch/network.d.ts +38 -0
- package/dist/tools/smartfetch/secondary-model.d.ts +28 -0
- package/dist/tools/smartfetch/tool.d.ts +3 -0
- package/dist/tools/smartfetch/types.d.ts +122 -0
- package/dist/tools/smartfetch/utils.d.ts +18 -0
- package/dist/tui-state.d.ts +168 -0
- package/dist/tui.d.ts +37 -0
- package/dist/tui.js +1896 -0
- package/dist/utils/agent-variant.d.ts +63 -0
- package/dist/utils/compat.d.ts +30 -0
- package/dist/utils/env.d.ts +1 -0
- package/dist/utils/index.d.ts +9 -0
- package/dist/utils/internal-initiator.d.ts +6 -0
- package/dist/utils/logger.d.ts +8 -0
- package/dist/utils/polling.d.ts +21 -0
- package/dist/utils/session-manager.d.ts +55 -0
- package/dist/utils/session.d.ts +90 -0
- package/dist/utils/subagent-depth.d.ts +35 -0
- package/dist/utils/system-collapse.d.ts +6 -0
- package/dist/utils/task.d.ts +4 -0
- package/dist/utils/zip-extractor.d.ts +1 -0
- package/index.ts +1 -0
- package/opencode-dux.schema.json +634 -0
- package/package.json +103 -0
- package/src/agents/descriptions.ts +55 -0
- package/src/agents/designer.test.ts +86 -0
- package/src/agents/designer.ts +154 -0
- package/src/agents/display-name.test.ts +186 -0
- package/src/agents/explorer.test.ts +79 -0
- package/src/agents/explorer.ts +144 -0
- package/src/agents/fixer.test.ts +79 -0
- package/src/agents/fixer.ts +145 -0
- package/src/agents/index.test.ts +472 -0
- package/src/agents/index.ts +248 -0
- package/src/agents/interpreter.ts +136 -0
- package/src/agents/librarian.test.ts +80 -0
- package/src/agents/librarian.ts +145 -0
- package/src/agents/oracle.test.ts +89 -0
- package/src/agents/oracle.ts +184 -0
- package/src/agents/orchestrator.test.ts +116 -0
- package/src/agents/orchestrator.ts +574 -0
- package/src/agents/overrides.ts +95 -0
- package/src/agents/prompt-blocks.test.ts +114 -0
- package/src/agents/prompt-blocks.ts +640 -0
- package/src/agents/steward.ts +146 -0
- package/src/cli/config-io.test.ts +536 -0
- package/src/cli/config-io.ts +473 -0
- package/src/cli/config-manager.test.ts +141 -0
- package/src/cli/config-manager.ts +4 -0
- package/src/cli/index.ts +88 -0
- package/src/cli/install.ts +282 -0
- package/src/cli/mcps.test.ts +62 -0
- package/src/cli/mcps.ts +39 -0
- package/src/cli/model-key-normalization.test.ts +21 -0
- package/src/cli/model-key-normalization.ts +60 -0
- package/src/cli/paths.test.ts +167 -0
- package/src/cli/paths.ts +144 -0
- package/src/cli/providers.test.ts +118 -0
- package/src/cli/providers.ts +141 -0
- package/src/cli/skills.test.ts +111 -0
- package/src/cli/skills.ts +103 -0
- package/src/cli/system.test.ts +91 -0
- package/src/cli/system.ts +180 -0
- package/src/cli/types.ts +43 -0
- package/src/config/constants.ts +58 -0
- package/src/config/index.ts +4 -0
- package/src/config/loader.test.ts +1194 -0
- package/src/config/loader.ts +269 -0
- package/src/config/model-resolution.test.ts +176 -0
- package/src/config/runtime-preset.test.ts +61 -0
- package/src/config/runtime-preset.ts +37 -0
- package/src/config/schema.ts +248 -0
- package/src/config/utils.test.ts +41 -0
- package/src/config/utils.ts +23 -0
- package/src/discovery/local/types.ts +85 -0
- package/src/discovery/local.ts +322 -0
- package/src/discovery/mcp-servers.ts +804 -0
- package/src/discovery/skills.ts +959 -0
- package/src/hooks/apply-patch/codec.test.ts +184 -0
- package/src/hooks/apply-patch/codec.ts +352 -0
- package/src/hooks/apply-patch/errors.ts +117 -0
- package/src/hooks/apply-patch/execution-context.ts +432 -0
- package/src/hooks/apply-patch/hook.test.ts +768 -0
- package/src/hooks/apply-patch/index.ts +126 -0
- package/src/hooks/apply-patch/matching.test.ts +215 -0
- package/src/hooks/apply-patch/matching.ts +586 -0
- package/src/hooks/apply-patch/operations.test.ts +1535 -0
- package/src/hooks/apply-patch/operations.ts +3 -0
- package/src/hooks/apply-patch/patch.ts +9 -0
- package/src/hooks/apply-patch/prepared-changes.ts +400 -0
- package/src/hooks/apply-patch/resolution.test.ts +420 -0
- package/src/hooks/apply-patch/resolution.ts +437 -0
- package/src/hooks/apply-patch/rewrite.ts +496 -0
- package/src/hooks/apply-patch/test-helpers.ts +52 -0
- package/src/hooks/apply-patch/types.ts +111 -0
- package/src/hooks/auto-update-checker/cache.test.ts +179 -0
- package/src/hooks/auto-update-checker/cache.ts +188 -0
- package/src/hooks/auto-update-checker/checker.test.ts +159 -0
- package/src/hooks/auto-update-checker/checker.ts +308 -0
- package/src/hooks/auto-update-checker/constants.ts +33 -0
- package/src/hooks/auto-update-checker/index.test.ts +282 -0
- package/src/hooks/auto-update-checker/index.ts +225 -0
- package/src/hooks/auto-update-checker/types.ts +26 -0
- package/src/hooks/chat-headers.test.ts +236 -0
- package/src/hooks/chat-headers.ts +97 -0
- package/src/hooks/context-pressure-reminder/index.test.ts +179 -0
- package/src/hooks/context-pressure-reminder/index.ts +137 -0
- package/src/hooks/delegate-task-retry/guidance.ts +41 -0
- package/src/hooks/delegate-task-retry/hook.ts +23 -0
- package/src/hooks/delegate-task-retry/index.test.ts +38 -0
- package/src/hooks/delegate-task-retry/index.ts +7 -0
- package/src/hooks/delegate-task-retry/patterns.ts +79 -0
- package/src/hooks/filter-available-skills/index.test.ts +297 -0
- package/src/hooks/filter-available-skills/index.ts +160 -0
- package/src/hooks/foreground-fallback/index.test.ts +624 -0
- package/src/hooks/foreground-fallback/index.ts +374 -0
- package/src/hooks/image-hook.ts +6 -0
- package/src/hooks/index.ts +17 -0
- package/src/hooks/json-error-recovery/hook.ts +73 -0
- package/src/hooks/json-error-recovery/index.test.ts +111 -0
- package/src/hooks/json-error-recovery/index.ts +6 -0
- package/src/hooks/phase-reminder/index.test.ts +74 -0
- package/src/hooks/phase-reminder/index.ts +85 -0
- package/src/hooks/post-file-tool-nudge/index.test.ts +94 -0
- package/src/hooks/post-file-tool-nudge/index.ts +63 -0
- package/src/hooks/task-session-manager/index.test.ts +833 -0
- package/src/hooks/task-session-manager/index.ts +434 -0
- package/src/hooks/todo-continuation/index.test.ts +3026 -0
- package/src/hooks/todo-continuation/index.ts +878 -0
- package/src/hooks/todo-continuation/todo-hygiene.test.ts +204 -0
- package/src/hooks/todo-continuation/todo-hygiene.ts +207 -0
- package/src/index.ts +1672 -0
- package/src/mcp/context7.ts +14 -0
- package/src/mcp/grep-app.ts +11 -0
- package/src/mcp/index.test.ts +96 -0
- package/src/mcp/index.ts +66 -0
- package/src/mcp/types.ts +16 -0
- package/src/mcp/websearch.ts +47 -0
- package/src/skills/codemap/README.md +60 -0
- package/src/skills/codemap/SKILL.md +174 -0
- package/src/skills/codemap/scripts/codemap.mjs +483 -0
- package/src/skills/codemap/scripts/codemap.test.ts +129 -0
- package/src/skills/registry.ts +218 -0
- package/src/skills/simplify/README.md +19 -0
- package/src/skills/simplify/SKILL.md +138 -0
- package/src/subscriptions/accounts-store.test.ts +236 -0
- package/src/subscriptions/accounts-store.ts +184 -0
- package/src/subscriptions/index.ts +30 -0
- package/src/subscriptions/neuralwatt-scraper.ts +108 -0
- package/src/subscriptions/opencode-go-scraper.ts +301 -0
- package/src/subscriptions/types.ts +145 -0
- package/src/subscriptions/usage-service.test.ts +202 -0
- package/src/subscriptions/usage-service.ts +651 -0
- package/src/tools/ast-grep/cli.ts +257 -0
- package/src/tools/ast-grep/constants.ts +214 -0
- package/src/tools/ast-grep/downloader.ts +131 -0
- package/src/tools/ast-grep/index.ts +24 -0
- package/src/tools/ast-grep/tools.ts +117 -0
- package/src/tools/ast-grep/types.ts +51 -0
- package/src/tools/ast-grep/utils.ts +126 -0
- package/src/tools/delegate-handoff.test.ts +18 -0
- package/src/tools/delegate.ts +508 -0
- package/src/tools/index.ts +8 -0
- package/src/tools/preset-manager.test.ts +795 -0
- package/src/tools/preset-manager.ts +332 -0
- package/src/tools/smartfetch/binary.ts +58 -0
- package/src/tools/smartfetch/cache.test.ts +34 -0
- package/src/tools/smartfetch/cache.ts +112 -0
- package/src/tools/smartfetch/constants.ts +29 -0
- package/src/tools/smartfetch/index.ts +8 -0
- package/src/tools/smartfetch/network.test.ts +178 -0
- package/src/tools/smartfetch/network.ts +614 -0
- package/src/tools/smartfetch/secondary-model.test.ts +85 -0
- package/src/tools/smartfetch/secondary-model.ts +276 -0
- package/src/tools/smartfetch/tool.test.ts +60 -0
- package/src/tools/smartfetch/tool.ts +832 -0
- package/src/tools/smartfetch/types.ts +135 -0
- package/src/tools/smartfetch/utils.test.ts +24 -0
- package/src/tools/smartfetch/utils.ts +456 -0
- package/src/tui-state.test.ts +867 -0
- package/src/tui-state.ts +1255 -0
- package/src/tui.test.ts +336 -0
- package/src/tui.ts +1539 -0
- package/src/utils/agent-variant.test.ts +244 -0
- package/src/utils/agent-variant.ts +187 -0
- package/src/utils/compat.ts +91 -0
- package/src/utils/env.ts +12 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/internal-initiator.ts +28 -0
- package/src/utils/logger.test.ts +220 -0
- package/src/utils/logger.ts +136 -0
- package/src/utils/polling.test.ts +191 -0
- package/src/utils/polling.ts +67 -0
- package/src/utils/session-manager.test.ts +173 -0
- package/src/utils/session-manager.ts +356 -0
- package/src/utils/session.test.ts +110 -0
- package/src/utils/session.ts +389 -0
- package/src/utils/subagent-depth.test.ts +170 -0
- package/src/utils/subagent-depth.ts +75 -0
- package/src/utils/system-collapse.test.ts +86 -0
- package/src/utils/system-collapse.ts +24 -0
- package/src/utils/task.test.ts +24 -0
- package/src/utils/task.ts +20 -0
- package/src/utils/zip-extractor.ts +102 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { AgentDefinition } from './orchestrator';
|
|
2
|
+
import { resolvePrompt } from './orchestrator';
|
|
3
|
+
import {
|
|
4
|
+
formatBlockedOutputBlock,
|
|
5
|
+
formatStewardAgentStewardPathsBody,
|
|
6
|
+
NEEDS_USER_OUTPUT_FORMAT_BLOCK,
|
|
7
|
+
SELF_REVIEW_BLOCK,
|
|
8
|
+
STEWARD_VARIANT_MAX_NOTE,
|
|
9
|
+
STEWARD_VARIANT_SCOPE_LINES,
|
|
10
|
+
SUBAGENT_NEEDS_USER_FORMAT,
|
|
11
|
+
USER_CHOICE_POLICY_BLOCK,
|
|
12
|
+
} from './prompt-blocks';
|
|
13
|
+
|
|
14
|
+
export { STEWARD_PATH_GLOBS } from './prompt-blocks';
|
|
15
|
+
|
|
16
|
+
const STEWARD_CRITICAL_INVARIANTS = `<critical_invariants>
|
|
17
|
+
Violating any = failure mode.
|
|
18
|
+
1) NEVER invent rules — cite exact path + verbatim quote only.
|
|
19
|
+
2) NEVER diagnose code — steward_paths citations only.
|
|
20
|
+
3) NEVER analyze rules content for correctness, consistency, contradictions, logic gaps, or applicability to code changes. You cite verbatim text from steward_paths files — you do not evaluate, compare, or interpret. Cross-file rule comparison, gap analysis, and contradiction hunting are @oracle tasks.
|
|
21
|
+
4) NEVER search for patterns beyond globbing which steward_paths exist on disk. If asked to "find all files that mention X" or "search for pattern Y," redirect to @explorer via <blocked>.
|
|
22
|
+
5) NEVER modify files or delegate to subagents.
|
|
23
|
+
</critical_invariants>`;
|
|
24
|
+
|
|
25
|
+
const STEWARD_PROMPT = `<role>
|
|
26
|
+
You are Steward: a rules citation agent. You locate, read, and cite verbatim excerpts from agent-facing convention and IDE config files listed in steward_paths. You do NOT analyze, evaluate, compare, or interpret these files — you only cite what they literally say. Code analysis, contradiction hunting, gap detection, and cross-file consistency checks are NOT your job; those are @oracle tasks. If the orchestrator delegates any of those to you, respond with <blocked> stating: "This is an @oracle analysis task. I only cite verbatim text from steward_paths files with path attribution."
|
|
27
|
+
</role>
|
|
28
|
+
|
|
29
|
+
${STEWARD_CRITICAL_INVARIANTS}
|
|
30
|
+
|
|
31
|
+
<capabilities>
|
|
32
|
+
- Locate and read agent convention files (AGENTS.md, AGENT.md, CLAUDE.md, etc.)
|
|
33
|
+
- Discover IDE-specific rule configs (.cursor/rules, .opencode, .github/copilot-instructions.md)
|
|
34
|
+
- Cite verbatim excerpts from those files with file path (and heading) attribution
|
|
35
|
+
- Report which steward_paths exist vs which do not
|
|
36
|
+
</capabilities>
|
|
37
|
+
|
|
38
|
+
<steward_paths>
|
|
39
|
+
${formatStewardAgentStewardPathsBody()}
|
|
40
|
+
</steward_paths>
|
|
41
|
+
|
|
42
|
+
<workflow>
|
|
43
|
+
1) Read root AGENTS.md then AGENT.md (both when present). If neither exists,
|
|
44
|
+
note it in <summary> and proceed.
|
|
45
|
+
2) Glob which other steward_paths exist; do not assume every path is present.
|
|
46
|
+
3) Read remaining steward_paths in descending priority order (\`CLAUDE.md\`, \`.cursor/rules/\`, \`.opencode/\`, \`.docs/\`, etc.) when the orchestrator's convention-domain request suggests those files are relevant (e.g., "commit conventions" → check CONTRIBUTING.md; "code style" → check .cursorrules). When the request is broad ("all conventions"), read all available steward_paths up to the read budget. Never rank files by applicability to a code task — read in the fixed priority order defined by steward_paths.
|
|
47
|
+
4) Read budget: prefer ≤12 whole-file reads per delegation (caps tokens/latency; many repos legitimately have more rule shards than that).
|
|
48
|
+
When globs return many matches: read from highest to lowest priority, and stop when the budget is reached. List unread paths under \`<not_found>\` so the orchestrator can delegate a narrower follow-up; note capped coverage in \`<summary>\`.
|
|
49
|
+
5) Return cited bullets only — every cited rule must include \`path\` (and heading when helpful); quote short excerpts verbatim, not whole files unless the orchestrator named that file explicitly. Prefer leading with \`AGENTS.md\` / \`AGENT.md\` citations when those files were read.
|
|
50
|
+
6) IMPORTANT: You are a citation agent, not an analysis agent. Extract and quote what steward_paths files literally state. Do NOT evaluate whether rules are correct, consistent, complete, or applicable to any code change. Do NOT compare rules across files for contradictions. Do NOT search for patterns, gaps, or logical issues in the rules content. If the orchestrator asks you to "analyze", "find contradictions", "identify gaps", "check consistency", or perform any evaluation of rules content beyond verbatim citation, respond with \`<blocked>\` stating: "This is an @oracle analysis task. I only cite verbatim text from steward_paths files with path attribution."
|
|
51
|
+
</workflow>
|
|
52
|
+
|
|
53
|
+
${USER_CHOICE_POLICY_BLOCK}
|
|
54
|
+
|
|
55
|
+
<constraints>
|
|
56
|
+
- NEVER invent project rules; if nothing applies, say so and list paths searched.
|
|
57
|
+
- NEVER diagnose product/runtime code, stack traces, or missing symbols/APIs unless a steward_paths file states it verbatim — then cite \`path\` and quote the excerpt only.
|
|
58
|
+
- NEVER analyze steward_paths content for correctness, consistency, gaps, or contradictions. You cite verbatim, you do not evaluate. Cross-file rule comparison and contradiction hunting are @oracle's job.
|
|
59
|
+
- NEVER search for patterns beyond globbing which steward_paths exist on disk. If the orchestrator asks you to "find all files that mention X" or "search for pattern Y", redirect to @explorer via <blocked>.
|
|
60
|
+
- NEVER delegate to subagents.
|
|
61
|
+
- NEVER modify files.
|
|
62
|
+
- NEVER treat plain \`docs/\` as authoritative unless explicitly scoped by
|
|
63
|
+
the orchestrator prompt. Plain \`docs/\` is user-facing documentation (not
|
|
64
|
+
agent rules). Only \`.docs/\` (dot-prefixed) is authoritative.
|
|
65
|
+
|
|
66
|
+
</constraints>
|
|
67
|
+
|
|
68
|
+
<variant_policy>
|
|
69
|
+
${STEWARD_VARIANT_SCOPE_LINES.map((l) => `- ${l}`).join('\n')}
|
|
70
|
+
- max: ${STEWARD_VARIANT_MAX_NOTE}
|
|
71
|
+
</variant_policy>
|
|
72
|
+
|
|
73
|
+
${SUBAGENT_NEEDS_USER_FORMAT}
|
|
74
|
+
|
|
75
|
+
${SELF_REVIEW_BLOCK}
|
|
76
|
+
|
|
77
|
+
<output_format>
|
|
78
|
+
<summary>
|
|
79
|
+
One line: which steward_paths were read and what convention topics were found.
|
|
80
|
+
</summary>
|
|
81
|
+
<rules_applicable>
|
|
82
|
+
- \`path\` - bullet citing only what the files actually say
|
|
83
|
+
</rules_applicable>
|
|
84
|
+
<not_found>
|
|
85
|
+
Optional - paths/globs tried with no relevant hits.
|
|
86
|
+
</not_found>
|
|
87
|
+
${formatBlockedOutputBlock('steward_paths cannot be read or the requested convention domain is outside steward_paths scope')}
|
|
88
|
+
${NEEDS_USER_OUTPUT_FORMAT_BLOCK}
|
|
89
|
+
|
|
90
|
+
<good_example>
|
|
91
|
+
User: "What are the testing conventions?"
|
|
92
|
+
Steward reads AGENTS.md test section, cites verbatim.
|
|
93
|
+
If multiple convention domains are relevant:
|
|
94
|
+
<needs_user>
|
|
95
|
+
<reason>Multiple convention domains found; need scope.</reason>
|
|
96
|
+
<questions>[{"question": "Which convention domain should I cite?", "header": "Convention scope", "options": [{"label": "Testing", "description": "Test file naming, structure, and runner conventions"}, {"label": "Commits", "description": "Commit message format and branch naming"}]}]</questions>
|
|
97
|
+
</needs_user>
|
|
98
|
+
</good_example>
|
|
99
|
+
|
|
100
|
+
<good_example>
|
|
101
|
+
User: "What are the project's testing conventions?"
|
|
102
|
+
Steward: Reads AGENTS.md lines 30-35, cites "bun test" + test file pattern.
|
|
103
|
+
Returns: <rules_applicable> with path + quote excerpts.
|
|
104
|
+
</good_example>
|
|
105
|
+
|
|
106
|
+
<bad_example>
|
|
107
|
+
User: "What are the project's testing conventions?"
|
|
108
|
+
Steward: Invents rules without citing paths, diagnoses product code.
|
|
109
|
+
Missing: path citations, "cite only" discipline.
|
|
110
|
+
</bad_example>
|
|
111
|
+
|
|
112
|
+
<bad_example>
|
|
113
|
+
User: "Find contradictions between AGENTS.md and CONTRIBUTING.md"
|
|
114
|
+
Steward: Reads both files and reports differences.
|
|
115
|
+
Missing: This is an @oracle analysis task. Steward should respond with <blocked>.
|
|
116
|
+
</bad_example>
|
|
117
|
+
|
|
118
|
+
<bad_example>
|
|
119
|
+
User: "Comprehensive deep analysis of all agent prompt files for logic gaps"
|
|
120
|
+
Steward: Reads all files and evaluates for gaps.
|
|
121
|
+
Missing: Steward cannot evaluate logic or identify gaps. This is an @oracle task. Steward should respond with <blocked>.
|
|
122
|
+
</bad_example>
|
|
123
|
+
</output_format>`;
|
|
124
|
+
|
|
125
|
+
export function createStewardAgent(
|
|
126
|
+
model: string,
|
|
127
|
+
customPrompt?: string,
|
|
128
|
+
customAppendPrompt?: string,
|
|
129
|
+
): AgentDefinition {
|
|
130
|
+
const prompt = resolvePrompt(
|
|
131
|
+
STEWARD_PROMPT,
|
|
132
|
+
customPrompt,
|
|
133
|
+
customAppendPrompt,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
name: 'steward',
|
|
138
|
+
description:
|
|
139
|
+
'Rules handoff: cited agent/IDE conventions (AGENTS.md / AGENT.md, .docs, .opencode, .cursor/rules). No application code diagnosis.',
|
|
140
|
+
config: {
|
|
141
|
+
model,
|
|
142
|
+
temperature: 0.1,
|
|
143
|
+
prompt,
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
/// <reference types="bun-types" />
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
|
|
4
|
+
import {
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
mkdtempSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from 'node:fs';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import {
|
|
15
|
+
addPluginToOpenCodeConfig,
|
|
16
|
+
addPluginToOpenCodeTuiConfig,
|
|
17
|
+
detectCurrentConfig,
|
|
18
|
+
disableDefaultAgents,
|
|
19
|
+
enableLspByDefault,
|
|
20
|
+
parseConfig,
|
|
21
|
+
parseConfigFile,
|
|
22
|
+
stripJsonComments,
|
|
23
|
+
writeConfig,
|
|
24
|
+
writeLiteConfig,
|
|
25
|
+
} from './config-io';
|
|
26
|
+
import * as paths from './paths';
|
|
27
|
+
|
|
28
|
+
describe('config-io', () => {
|
|
29
|
+
let tmpDir: string;
|
|
30
|
+
const originalEnv = { ...process.env };
|
|
31
|
+
const originalArgv = [...process.argv];
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'opencode-io-test-'));
|
|
35
|
+
delete process.env.OPENCODE_CONFIG_DIR;
|
|
36
|
+
delete process.env.OPENCODE_TUI_CONFIG;
|
|
37
|
+
process.env.XDG_CONFIG_HOME = tmpDir;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
process.env = { ...originalEnv };
|
|
42
|
+
process.argv = [...originalArgv];
|
|
43
|
+
if (tmpDir && existsSync(tmpDir)) {
|
|
44
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
mock.restore();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
function writePackageJson(dir: string): void {
|
|
50
|
+
mkdirSync(dir, { recursive: true });
|
|
51
|
+
writeFileSync(
|
|
52
|
+
join(dir, 'package.json'),
|
|
53
|
+
JSON.stringify({ name: 'opencode-dux' }),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
test('stripJsonComments strips comments and trailing commas', () => {
|
|
58
|
+
const jsonc = `{
|
|
59
|
+
// comment
|
|
60
|
+
"a": 1, /* multi
|
|
61
|
+
line */
|
|
62
|
+
"b": [2,],
|
|
63
|
+
}`;
|
|
64
|
+
const stripped = stripJsonComments(jsonc);
|
|
65
|
+
expect(JSON.parse(stripped)).toEqual({ a: 1, b: [2] });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('parseConfigFile parses valid JSON', () => {
|
|
69
|
+
const path = join(tmpDir, 'test.json');
|
|
70
|
+
writeFileSync(path, '{"a": 1}');
|
|
71
|
+
const result = parseConfigFile(path);
|
|
72
|
+
expect(result.config).toEqual({ a: 1 } as any);
|
|
73
|
+
expect(result.error).toBeUndefined();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('parseConfigFile returns null for non-existent file', () => {
|
|
77
|
+
const result = parseConfigFile(join(tmpDir, 'nonexistent.json'));
|
|
78
|
+
expect(result.config).toBeNull();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('parseConfigFile returns null for empty or whitespace-only file', () => {
|
|
82
|
+
const emptyPath = join(tmpDir, 'empty.json');
|
|
83
|
+
writeFileSync(emptyPath, '');
|
|
84
|
+
expect(parseConfigFile(emptyPath).config).toBeNull();
|
|
85
|
+
|
|
86
|
+
const whitespacePath = join(tmpDir, 'whitespace.json');
|
|
87
|
+
writeFileSync(whitespacePath, ' \n ');
|
|
88
|
+
expect(parseConfigFile(whitespacePath).config).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('parseConfigFile returns error for invalid JSON', () => {
|
|
92
|
+
const path = join(tmpDir, 'invalid.json');
|
|
93
|
+
writeFileSync(path, '{"a": 1');
|
|
94
|
+
const result = parseConfigFile(path);
|
|
95
|
+
expect(result.config).toBeNull();
|
|
96
|
+
expect(result.error).toBeDefined();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('parseConfig tries .jsonc if .json is missing', () => {
|
|
100
|
+
const jsoncPath = join(tmpDir, 'test.jsonc');
|
|
101
|
+
writeFileSync(jsoncPath, '{"a": 1}');
|
|
102
|
+
|
|
103
|
+
// We pass .json path, it should try .jsonc
|
|
104
|
+
const result = parseConfig(join(tmpDir, 'test.json'));
|
|
105
|
+
expect(result.config).toEqual({ a: 1 } as any);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('writeConfig writes JSON and creates backup', () => {
|
|
109
|
+
const path = join(tmpDir, 'test.json');
|
|
110
|
+
writeFileSync(path, '{"old": true}');
|
|
111
|
+
|
|
112
|
+
writeConfig(path, { new: true } as any);
|
|
113
|
+
|
|
114
|
+
expect(JSON.parse(readFileSync(path, 'utf-8'))).toEqual({ new: true });
|
|
115
|
+
expect(JSON.parse(readFileSync(`${path}.bak`, 'utf-8'))).toEqual({
|
|
116
|
+
old: true,
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('addPluginToOpenCodeConfig adds plugin and removes duplicates', async () => {
|
|
121
|
+
const configPath = join(tmpDir, 'opencode', 'opencode.json');
|
|
122
|
+
paths.ensureConfigDir();
|
|
123
|
+
writeFileSync(
|
|
124
|
+
configPath,
|
|
125
|
+
JSON.stringify({ plugin: ['other', 'opencode-dux@1.0.0'] }),
|
|
126
|
+
);
|
|
127
|
+
process.argv[1] = '';
|
|
128
|
+
|
|
129
|
+
const result = await addPluginToOpenCodeConfig();
|
|
130
|
+
expect(result.success).toBe(true);
|
|
131
|
+
|
|
132
|
+
const saved = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
133
|
+
expect(saved.plugin).toContain('opencode-dux');
|
|
134
|
+
expect(saved.plugin).not.toContain('opencode-dux@1.0.0');
|
|
135
|
+
expect(saved.plugin.length).toBe(2);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('addPluginToOpenCodeConfig stores package name for bunx temp paths', async () => {
|
|
139
|
+
const configPath = join(tmpDir, 'opencode', 'opencode.json');
|
|
140
|
+
const packageRoot = join(
|
|
141
|
+
tmpDir,
|
|
142
|
+
'bunx-1000-opencode-dux@latest',
|
|
143
|
+
'node_modules',
|
|
144
|
+
'opencode-dux',
|
|
145
|
+
);
|
|
146
|
+
paths.ensureConfigDir();
|
|
147
|
+
writeFileSync(configPath, JSON.stringify({ plugin: [] }));
|
|
148
|
+
writePackageJson(packageRoot);
|
|
149
|
+
process.argv[1] = join(packageRoot, 'dist', 'cli', 'index.js');
|
|
150
|
+
|
|
151
|
+
const result = await addPluginToOpenCodeConfig();
|
|
152
|
+
|
|
153
|
+
expect(result.success).toBe(true);
|
|
154
|
+
const saved = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
155
|
+
expect(saved.plugin).toEqual(['opencode-dux']);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('addPluginToOpenCodeConfig stores local repo path for local dev paths', async () => {
|
|
159
|
+
const configPath = join(tmpDir, 'opencode', 'opencode.json');
|
|
160
|
+
const packageRoot = join(tmpDir, 'repo');
|
|
161
|
+
const localCliPath = join(packageRoot, 'dist', 'cli', 'index.js');
|
|
162
|
+
paths.ensureConfigDir();
|
|
163
|
+
writeFileSync(configPath, JSON.stringify({ plugin: [] }));
|
|
164
|
+
writePackageJson(packageRoot);
|
|
165
|
+
process.argv[1] = localCliPath;
|
|
166
|
+
|
|
167
|
+
const result = await addPluginToOpenCodeConfig();
|
|
168
|
+
|
|
169
|
+
expect(result.success).toBe(true);
|
|
170
|
+
const saved = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
171
|
+
expect(saved.plugin).toEqual([packageRoot]);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('addPluginToOpenCodeConfig stores local repo path for local paths containing bunx-', async () => {
|
|
175
|
+
const configPath = join(tmpDir, 'opencode', 'opencode.json');
|
|
176
|
+
const packageRoot = join(tmpDir, 'repo', 'bunx-tools');
|
|
177
|
+
const localCliPath = join(packageRoot, 'dist', 'cli', 'index.js');
|
|
178
|
+
paths.ensureConfigDir();
|
|
179
|
+
writeFileSync(configPath, JSON.stringify({ plugin: [] }));
|
|
180
|
+
writePackageJson(packageRoot);
|
|
181
|
+
process.argv[1] = localCliPath;
|
|
182
|
+
|
|
183
|
+
const result = await addPluginToOpenCodeConfig();
|
|
184
|
+
|
|
185
|
+
expect(result.success).toBe(true);
|
|
186
|
+
const saved = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
187
|
+
expect(saved.plugin).toEqual([packageRoot]);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('addPluginToOpenCodeConfig deduplicates existing local repo path entries', async () => {
|
|
191
|
+
const configPath = join(tmpDir, 'opencode', 'opencode.json');
|
|
192
|
+
const packageRoot = join(tmpDir, 'repo');
|
|
193
|
+
const localCliPath = join(packageRoot, 'dist', 'cli', 'index.js');
|
|
194
|
+
paths.ensureConfigDir();
|
|
195
|
+
writePackageJson(packageRoot);
|
|
196
|
+
writeFileSync(
|
|
197
|
+
configPath,
|
|
198
|
+
JSON.stringify({ plugin: ['other', packageRoot] }),
|
|
199
|
+
);
|
|
200
|
+
process.argv[1] = localCliPath;
|
|
201
|
+
|
|
202
|
+
const result = await addPluginToOpenCodeConfig();
|
|
203
|
+
|
|
204
|
+
expect(result.success).toBe(true);
|
|
205
|
+
const saved = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
206
|
+
expect(saved.plugin).toEqual(['other', packageRoot]);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('addPluginToOpenCodeConfig preserves non-string plugin entries when refreshing', async () => {
|
|
210
|
+
const configPath = join(tmpDir, 'opencode', 'opencode.json');
|
|
211
|
+
paths.ensureConfigDir();
|
|
212
|
+
process.argv[1] = '';
|
|
213
|
+
|
|
214
|
+
const objectPlugin = { name: 'some-config-plugin', enabled: true };
|
|
215
|
+
writeFileSync(
|
|
216
|
+
configPath,
|
|
217
|
+
JSON.stringify({
|
|
218
|
+
plugin: ['other-plugin', objectPlugin, 'opencode-dux@1.0.0'],
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const result = await addPluginToOpenCodeConfig();
|
|
223
|
+
expect(result.success).toBe(true);
|
|
224
|
+
|
|
225
|
+
const saved = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
226
|
+
expect(saved.plugin).toContain('opencode-dux');
|
|
227
|
+
expect(saved.plugin).toContain('other-plugin');
|
|
228
|
+
expect(saved.plugin).not.toContain('opencode-dux@1.0.0');
|
|
229
|
+
// Non-string entries (objects) must survive the plugin refresh
|
|
230
|
+
expect(saved.plugin).toContainEqual(objectPlugin);
|
|
231
|
+
expect(saved.plugin.length).toBe(3);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('addPluginToOpenCodeConfig removes tuple plugin entries', async () => {
|
|
235
|
+
const configPath = join(tmpDir, 'opencode', 'opencode.json');
|
|
236
|
+
paths.ensureConfigDir();
|
|
237
|
+
writeFileSync(
|
|
238
|
+
configPath,
|
|
239
|
+
JSON.stringify({
|
|
240
|
+
plugin: ['other', ['opencode-dux', { enabled: true }]],
|
|
241
|
+
}),
|
|
242
|
+
);
|
|
243
|
+
process.argv[1] = '';
|
|
244
|
+
|
|
245
|
+
const result = await addPluginToOpenCodeConfig();
|
|
246
|
+
expect(result.success).toBe(true);
|
|
247
|
+
|
|
248
|
+
const saved = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
249
|
+
expect(saved.plugin).toEqual(['other', 'opencode-dux']);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test('addPluginToOpenCodeTuiConfig adds plugin to tui.json and removes duplicates', async () => {
|
|
253
|
+
const tuiPath = join(tmpDir, 'opencode', 'tui.json');
|
|
254
|
+
paths.ensureConfigDir();
|
|
255
|
+
writeFileSync(
|
|
256
|
+
tuiPath,
|
|
257
|
+
JSON.stringify({ plugin: ['other', 'opencode-dux@1.0.0'] }),
|
|
258
|
+
);
|
|
259
|
+
process.argv[1] = '';
|
|
260
|
+
|
|
261
|
+
const result = await addPluginToOpenCodeTuiConfig();
|
|
262
|
+
expect(result.success).toBe(true);
|
|
263
|
+
|
|
264
|
+
const saved = JSON.parse(readFileSync(tuiPath, 'utf-8'));
|
|
265
|
+
expect(saved.plugin).toContain('opencode-dux');
|
|
266
|
+
expect(saved.plugin).not.toContain('opencode-dux@1.0.0');
|
|
267
|
+
expect(saved.plugin.length).toBe(2);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test('addPluginToOpenCodeTuiConfig stores package name for bunx temp paths', async () => {
|
|
271
|
+
const tuiPath = join(tmpDir, 'opencode', 'tui.json');
|
|
272
|
+
const packageRoot = join(
|
|
273
|
+
tmpDir,
|
|
274
|
+
'bunx-1000-opencode-dux@latest',
|
|
275
|
+
'node_modules',
|
|
276
|
+
'opencode-dux',
|
|
277
|
+
);
|
|
278
|
+
paths.ensureConfigDir();
|
|
279
|
+
writeFileSync(tuiPath, JSON.stringify({ plugin: [] }));
|
|
280
|
+
writePackageJson(packageRoot);
|
|
281
|
+
process.argv[1] = join(packageRoot, 'dist', 'cli', 'index.js');
|
|
282
|
+
|
|
283
|
+
const result = await addPluginToOpenCodeTuiConfig();
|
|
284
|
+
|
|
285
|
+
expect(result.success).toBe(true);
|
|
286
|
+
const saved = JSON.parse(readFileSync(tuiPath, 'utf-8'));
|
|
287
|
+
expect(saved.plugin).toEqual(['opencode-dux']);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test('addPluginToOpenCodeTuiConfig removes tuple plugin entries', async () => {
|
|
291
|
+
const tuiPath = join(tmpDir, 'opencode', 'tui.json');
|
|
292
|
+
paths.ensureConfigDir();
|
|
293
|
+
writeFileSync(
|
|
294
|
+
tuiPath,
|
|
295
|
+
JSON.stringify({
|
|
296
|
+
plugin: ['other', ['opencode-dux', { enabled: true }]],
|
|
297
|
+
}),
|
|
298
|
+
);
|
|
299
|
+
process.argv[1] = '';
|
|
300
|
+
|
|
301
|
+
const result = await addPluginToOpenCodeTuiConfig();
|
|
302
|
+
expect(result.success).toBe(true);
|
|
303
|
+
|
|
304
|
+
const saved = JSON.parse(readFileSync(tuiPath, 'utf-8'));
|
|
305
|
+
expect(saved.plugin).toEqual(['other', 'opencode-dux']);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test('addPluginToOpenCodeTuiConfig honors OPENCODE_TUI_CONFIG', async () => {
|
|
309
|
+
const tuiPath = join(tmpDir, 'custom', 'tui.custom.json');
|
|
310
|
+
process.env.OPENCODE_TUI_CONFIG = tuiPath;
|
|
311
|
+
process.argv[1] = '';
|
|
312
|
+
|
|
313
|
+
const result = await addPluginToOpenCodeTuiConfig();
|
|
314
|
+
expect(result.success).toBe(true);
|
|
315
|
+
expect(result.configPath).toBe(tuiPath);
|
|
316
|
+
|
|
317
|
+
const saved = JSON.parse(readFileSync(tuiPath, 'utf-8'));
|
|
318
|
+
expect(saved.plugin).toEqual(['opencode-dux']);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test('addPluginToOpenCodeTuiConfig does not bypass OPENCODE_TUI_CONFIG for existing default config', async () => {
|
|
322
|
+
const defaultTuiPath = join(tmpDir, 'opencode', 'tui.jsonc');
|
|
323
|
+
const customTuiPath = join(tmpDir, 'custom', 'tui.json');
|
|
324
|
+
paths.ensureConfigDir();
|
|
325
|
+
writeFileSync(defaultTuiPath, JSON.stringify({ plugin: ['default'] }));
|
|
326
|
+
process.env.OPENCODE_TUI_CONFIG = customTuiPath;
|
|
327
|
+
process.argv[1] = '';
|
|
328
|
+
|
|
329
|
+
const result = await addPluginToOpenCodeTuiConfig();
|
|
330
|
+
expect(result.success).toBe(true);
|
|
331
|
+
expect(result.configPath).toBe(customTuiPath);
|
|
332
|
+
|
|
333
|
+
const custom = JSON.parse(readFileSync(customTuiPath, 'utf-8'));
|
|
334
|
+
const original = JSON.parse(readFileSync(defaultTuiPath, 'utf-8'));
|
|
335
|
+
expect(custom.plugin).toEqual(['opencode-dux']);
|
|
336
|
+
expect(original.plugin).toEqual(['default']);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test('addPluginToOpenCodeTuiConfig stores local repo path for local dev paths', async () => {
|
|
340
|
+
const tuiPath = join(tmpDir, 'opencode', 'tui.json');
|
|
341
|
+
const packageRoot = join(tmpDir, 'repo');
|
|
342
|
+
const localCliPath = join(packageRoot, 'dist', 'cli', 'index.js');
|
|
343
|
+
paths.ensureConfigDir();
|
|
344
|
+
writeFileSync(tuiPath, JSON.stringify({ plugin: [] }));
|
|
345
|
+
writePackageJson(packageRoot);
|
|
346
|
+
process.argv[1] = localCliPath;
|
|
347
|
+
|
|
348
|
+
const result = await addPluginToOpenCodeTuiConfig();
|
|
349
|
+
|
|
350
|
+
expect(result.success).toBe(true);
|
|
351
|
+
const saved = JSON.parse(readFileSync(tuiPath, 'utf-8'));
|
|
352
|
+
expect(saved.plugin).toEqual([packageRoot]);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test('addPluginToOpenCodeTuiConfig deduplicates existing local repo path entries', async () => {
|
|
356
|
+
const tuiPath = join(tmpDir, 'opencode', 'tui.json');
|
|
357
|
+
const packageRoot = join(tmpDir, 'repo');
|
|
358
|
+
const localCliPath = join(packageRoot, 'dist', 'cli', 'index.js');
|
|
359
|
+
paths.ensureConfigDir();
|
|
360
|
+
writePackageJson(packageRoot);
|
|
361
|
+
writeFileSync(tuiPath, JSON.stringify({ plugin: ['other', packageRoot] }));
|
|
362
|
+
process.argv[1] = localCliPath;
|
|
363
|
+
|
|
364
|
+
const result = await addPluginToOpenCodeTuiConfig();
|
|
365
|
+
|
|
366
|
+
expect(result.success).toBe(true);
|
|
367
|
+
const saved = JSON.parse(readFileSync(tuiPath, 'utf-8'));
|
|
368
|
+
expect(saved.plugin).toEqual(['other', packageRoot]);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test('addPluginToOpenCodeTuiConfig preserves non-string plugin entries when refreshing', async () => {
|
|
372
|
+
const tuiPath = join(tmpDir, 'opencode', 'tui.json');
|
|
373
|
+
paths.ensureConfigDir();
|
|
374
|
+
process.argv[1] = '';
|
|
375
|
+
|
|
376
|
+
const objectPlugin = { name: 'some-tui-plugin', enabled: true };
|
|
377
|
+
writeFileSync(
|
|
378
|
+
tuiPath,
|
|
379
|
+
JSON.stringify({
|
|
380
|
+
plugin: ['other-plugin', objectPlugin, 'opencode-dux@1.0.0'],
|
|
381
|
+
}),
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
const result = await addPluginToOpenCodeTuiConfig();
|
|
385
|
+
expect(result.success).toBe(true);
|
|
386
|
+
|
|
387
|
+
const saved = JSON.parse(readFileSync(tuiPath, 'utf-8'));
|
|
388
|
+
expect(saved.plugin).toContain('opencode-dux');
|
|
389
|
+
expect(saved.plugin).toContain('other-plugin');
|
|
390
|
+
expect(saved.plugin).not.toContain('opencode-dux@1.0.0');
|
|
391
|
+
// Non-string entries (objects) must survive the plugin refresh
|
|
392
|
+
expect(saved.plugin).toContainEqual(objectPlugin);
|
|
393
|
+
expect(saved.plugin.length).toBe(3);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test('writeLiteConfig writes lite config with OpenAI preset', () => {
|
|
397
|
+
const litePath = join(tmpDir, 'opencode', 'opencode-dux.json');
|
|
398
|
+
paths.ensureConfigDir();
|
|
399
|
+
|
|
400
|
+
const result = writeLiteConfig({
|
|
401
|
+
installSkills: false,
|
|
402
|
+
installCustomSkills: false,
|
|
403
|
+
reset: false,
|
|
404
|
+
});
|
|
405
|
+
expect(result.success).toBe(true);
|
|
406
|
+
|
|
407
|
+
const saved = JSON.parse(readFileSync(litePath, 'utf-8'));
|
|
408
|
+
expect(saved.$schema).toBe(
|
|
409
|
+
'https://unpkg.com/opencode-dux@latest/opencode-dux.schema.json',
|
|
410
|
+
);
|
|
411
|
+
expect(saved.preset).toBe('openai');
|
|
412
|
+
expect(saved.presets.openai).toBeDefined();
|
|
413
|
+
expect(saved.presets['opencode-go']).toBeDefined();
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test('writeLiteConfig writes selected preset', () => {
|
|
417
|
+
const litePath = join(tmpDir, 'opencode', 'opencode-dux.json');
|
|
418
|
+
paths.ensureConfigDir();
|
|
419
|
+
|
|
420
|
+
const result = writeLiteConfig({
|
|
421
|
+
installSkills: false,
|
|
422
|
+
installCustomSkills: false,
|
|
423
|
+
preset: 'opencode-go',
|
|
424
|
+
reset: false,
|
|
425
|
+
});
|
|
426
|
+
expect(result.success).toBe(true);
|
|
427
|
+
|
|
428
|
+
const saved = JSON.parse(readFileSync(litePath, 'utf-8'));
|
|
429
|
+
expect(saved.preset).toBe('opencode-go');
|
|
430
|
+
expect(saved.presets.openai).toBeDefined();
|
|
431
|
+
expect(saved.presets['opencode-go'].orchestrator.model).toBe(
|
|
432
|
+
'neuralwatt/zai-org/GLM-5.1-FP8',
|
|
433
|
+
);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test('disableDefaultAgents disables explore and general agents', () => {
|
|
437
|
+
const configPath = join(tmpDir, 'opencode', 'opencode.json');
|
|
438
|
+
paths.ensureConfigDir();
|
|
439
|
+
writeFileSync(configPath, JSON.stringify({}));
|
|
440
|
+
|
|
441
|
+
const result = disableDefaultAgents();
|
|
442
|
+
expect(result.success).toBe(true);
|
|
443
|
+
|
|
444
|
+
const saved = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
445
|
+
expect(saved.agent.explore.disable).toBe(true);
|
|
446
|
+
expect(saved.agent.general.disable).toBe(true);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test('enableLspByDefault sets lsp true when missing', () => {
|
|
450
|
+
const configPath = join(tmpDir, 'opencode', 'opencode.json');
|
|
451
|
+
paths.ensureConfigDir();
|
|
452
|
+
writeFileSync(configPath, JSON.stringify({ plugin: ['other'] }));
|
|
453
|
+
|
|
454
|
+
const result = enableLspByDefault();
|
|
455
|
+
expect(result.success).toBe(true);
|
|
456
|
+
|
|
457
|
+
const saved = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
458
|
+
expect(saved.lsp).toBe(true);
|
|
459
|
+
expect(saved.plugin).toEqual(['other']);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test('enableLspByDefault preserves explicit lsp config', () => {
|
|
463
|
+
const configPath = join(tmpDir, 'opencode', 'opencode.json');
|
|
464
|
+
paths.ensureConfigDir();
|
|
465
|
+
writeFileSync(configPath, JSON.stringify({ lsp: false }));
|
|
466
|
+
|
|
467
|
+
const result = enableLspByDefault();
|
|
468
|
+
expect(result.success).toBe(true);
|
|
469
|
+
|
|
470
|
+
const saved = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
471
|
+
expect(saved.lsp).toBe(false);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
test('enableLspByDefault does not write when lsp exists', () => {
|
|
475
|
+
const configPath = join(tmpDir, 'opencode', 'opencode.json');
|
|
476
|
+
paths.ensureConfigDir();
|
|
477
|
+
writeFileSync(configPath, JSON.stringify({ lsp: false }));
|
|
478
|
+
|
|
479
|
+
const result = enableLspByDefault();
|
|
480
|
+
expect(result.success).toBe(true);
|
|
481
|
+
|
|
482
|
+
expect(existsSync(`${configPath}.bak`)).toBe(false);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
test('detectCurrentConfig detects installed status', () => {
|
|
486
|
+
const configPath = join(tmpDir, 'opencode', 'opencode.json');
|
|
487
|
+
const litePath = join(tmpDir, 'opencode', 'opencode-dux.json');
|
|
488
|
+
paths.ensureConfigDir();
|
|
489
|
+
|
|
490
|
+
writeFileSync(
|
|
491
|
+
configPath,
|
|
492
|
+
JSON.stringify({
|
|
493
|
+
plugin: ['opencode-dux'],
|
|
494
|
+
provider: {
|
|
495
|
+
kimi: {
|
|
496
|
+
npm: '@ai-sdk/openai-compatible',
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
}),
|
|
500
|
+
);
|
|
501
|
+
writeFileSync(
|
|
502
|
+
litePath,
|
|
503
|
+
JSON.stringify({
|
|
504
|
+
preset: 'openai',
|
|
505
|
+
presets: {
|
|
506
|
+
openai: {
|
|
507
|
+
orchestrator: { model: 'openai/gpt-4' },
|
|
508
|
+
oracle: { model: 'anthropic/claude-opus-4-6' },
|
|
509
|
+
explorer: { model: 'github-copilot/grok-code-fast-1' },
|
|
510
|
+
librarian: { model: 'zai-coding-plan/glm-4.7' },
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
}),
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
const detected = detectCurrentConfig();
|
|
517
|
+
expect(detected.isInstalled).toBe(true);
|
|
518
|
+
expect(detected.hasKimi).toBe(true);
|
|
519
|
+
expect(detected.hasOpenAI).toBe(true);
|
|
520
|
+
expect(detected.hasAnthropic).toBe(true);
|
|
521
|
+
expect(detected.hasCopilot).toBe(true);
|
|
522
|
+
expect(detected.hasZaiPlan).toBe(true);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
test('detectCurrentConfig treats local repo path entries as installed', () => {
|
|
526
|
+
const configPath = join(tmpDir, 'opencode', 'opencode.json');
|
|
527
|
+
const packageRoot = join(tmpDir, 'repo');
|
|
528
|
+
paths.ensureConfigDir();
|
|
529
|
+
writePackageJson(packageRoot);
|
|
530
|
+
writeFileSync(configPath, JSON.stringify({ plugin: [packageRoot] }));
|
|
531
|
+
|
|
532
|
+
const detected = detectCurrentConfig();
|
|
533
|
+
|
|
534
|
+
expect(detected.isInstalled).toBe(true);
|
|
535
|
+
});
|
|
536
|
+
});
|