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.
Files changed (302) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +452 -0
  3. package/dist/agents/descriptions.d.ts +6 -0
  4. package/dist/agents/designer.d.ts +2 -0
  5. package/dist/agents/explorer.d.ts +2 -0
  6. package/dist/agents/fixer.d.ts +2 -0
  7. package/dist/agents/index.d.ts +22 -0
  8. package/dist/agents/interpreter.d.ts +2 -0
  9. package/dist/agents/librarian.d.ts +2 -0
  10. package/dist/agents/oracle.d.ts +2 -0
  11. package/dist/agents/orchestrator.d.ts +27 -0
  12. package/dist/agents/overrides.d.ts +18 -0
  13. package/dist/agents/prompt-blocks.d.ts +97 -0
  14. package/dist/agents/steward.d.ts +3 -0
  15. package/dist/cli/config-io.d.ts +24 -0
  16. package/dist/cli/config-manager.d.ts +4 -0
  17. package/dist/cli/index.d.ts +2 -0
  18. package/dist/cli/index.js +1006 -0
  19. package/dist/cli/install.d.ts +2 -0
  20. package/dist/cli/mcps.d.ts +13 -0
  21. package/dist/cli/model-key-normalization.d.ts +1 -0
  22. package/dist/cli/paths.d.ts +35 -0
  23. package/dist/cli/providers.d.ts +137 -0
  24. package/dist/cli/skills.d.ts +22 -0
  25. package/dist/cli/system.d.ts +5 -0
  26. package/dist/cli/types.d.ts +38 -0
  27. package/dist/config/constants.d.ts +12 -0
  28. package/dist/config/index.d.ts +4 -0
  29. package/dist/config/loader.d.ts +40 -0
  30. package/dist/config/runtime-preset.d.ts +12 -0
  31. package/dist/config/schema.d.ts +281 -0
  32. package/dist/config/utils.d.ts +10 -0
  33. package/dist/discovery/local/types.d.ts +79 -0
  34. package/dist/discovery/local.d.ts +73 -0
  35. package/dist/discovery/mcp-servers.d.ts +88 -0
  36. package/dist/discovery/skills.d.ts +94 -0
  37. package/dist/hooks/apply-patch/codec.d.ts +7 -0
  38. package/dist/hooks/apply-patch/errors.d.ts +25 -0
  39. package/dist/hooks/apply-patch/execution-context.d.ts +27 -0
  40. package/dist/hooks/apply-patch/index.d.ts +15 -0
  41. package/dist/hooks/apply-patch/matching.d.ts +26 -0
  42. package/dist/hooks/apply-patch/operations.d.ts +3 -0
  43. package/dist/hooks/apply-patch/patch.d.ts +2 -0
  44. package/dist/hooks/apply-patch/prepared-changes.d.ts +17 -0
  45. package/dist/hooks/apply-patch/resolution.d.ts +19 -0
  46. package/dist/hooks/apply-patch/rewrite.d.ts +7 -0
  47. package/dist/hooks/apply-patch/test-helpers.d.ts +6 -0
  48. package/dist/hooks/apply-patch/types.d.ts +80 -0
  49. package/dist/hooks/auto-update-checker/cache.d.ts +11 -0
  50. package/dist/hooks/auto-update-checker/checker.d.ts +32 -0
  51. package/dist/hooks/auto-update-checker/constants.d.ts +11 -0
  52. package/dist/hooks/auto-update-checker/index.d.ts +18 -0
  53. package/dist/hooks/auto-update-checker/types.d.ts +22 -0
  54. package/dist/hooks/chat-headers.d.ts +16 -0
  55. package/dist/hooks/context-pressure-reminder/index.d.ts +33 -0
  56. package/dist/hooks/delegate-task-retry/guidance.d.ts +2 -0
  57. package/dist/hooks/delegate-task-retry/hook.d.ts +8 -0
  58. package/dist/hooks/delegate-task-retry/index.d.ts +4 -0
  59. package/dist/hooks/delegate-task-retry/patterns.d.ts +11 -0
  60. package/dist/hooks/filter-available-skills/index.d.ts +32 -0
  61. package/dist/hooks/foreground-fallback/index.d.ts +72 -0
  62. package/dist/hooks/image-hook.d.ts +5 -0
  63. package/dist/hooks/index.d.ts +14 -0
  64. package/dist/hooks/json-error-recovery/hook.d.ts +18 -0
  65. package/dist/hooks/json-error-recovery/index.d.ts +1 -0
  66. package/dist/hooks/phase-reminder/index.d.ts +26 -0
  67. package/dist/hooks/post-file-tool-nudge/index.d.ts +19 -0
  68. package/dist/hooks/task-session-manager/index.d.ts +52 -0
  69. package/dist/hooks/todo-continuation/index.d.ts +53 -0
  70. package/dist/hooks/todo-continuation/todo-hygiene.d.ts +35 -0
  71. package/dist/index.d.ts +5 -0
  72. package/dist/index.js +31782 -0
  73. package/dist/mcp/context7.d.ts +6 -0
  74. package/dist/mcp/grep-app.d.ts +6 -0
  75. package/dist/mcp/index.d.ts +13 -0
  76. package/dist/mcp/types.d.ts +12 -0
  77. package/dist/mcp/websearch.d.ts +9 -0
  78. package/dist/skills/registry.d.ts +29 -0
  79. package/dist/subscriptions/accounts-store.d.ts +57 -0
  80. package/dist/subscriptions/index.d.ts +13 -0
  81. package/dist/subscriptions/neuralwatt-scraper.d.ts +14 -0
  82. package/dist/subscriptions/opencode-go-scraper.d.ts +27 -0
  83. package/dist/subscriptions/types.d.ts +115 -0
  84. package/dist/subscriptions/usage-service.d.ts +74 -0
  85. package/dist/tools/ast-grep/cli.d.ts +15 -0
  86. package/dist/tools/ast-grep/constants.d.ts +25 -0
  87. package/dist/tools/ast-grep/downloader.d.ts +5 -0
  88. package/dist/tools/ast-grep/index.d.ts +10 -0
  89. package/dist/tools/ast-grep/tools.d.ts +3 -0
  90. package/dist/tools/ast-grep/types.d.ts +30 -0
  91. package/dist/tools/ast-grep/utils.d.ts +4 -0
  92. package/dist/tools/delegate.d.ts +14 -0
  93. package/dist/tools/index.d.ts +5 -0
  94. package/dist/tools/preset-manager.d.ts +27 -0
  95. package/dist/tools/smartfetch/binary.d.ts +3 -0
  96. package/dist/tools/smartfetch/cache.d.ts +6 -0
  97. package/dist/tools/smartfetch/constants.d.ts +12 -0
  98. package/dist/tools/smartfetch/index.d.ts +3 -0
  99. package/dist/tools/smartfetch/network.d.ts +38 -0
  100. package/dist/tools/smartfetch/secondary-model.d.ts +28 -0
  101. package/dist/tools/smartfetch/tool.d.ts +3 -0
  102. package/dist/tools/smartfetch/types.d.ts +122 -0
  103. package/dist/tools/smartfetch/utils.d.ts +18 -0
  104. package/dist/tui-state.d.ts +168 -0
  105. package/dist/tui.d.ts +37 -0
  106. package/dist/tui.js +1896 -0
  107. package/dist/utils/agent-variant.d.ts +63 -0
  108. package/dist/utils/compat.d.ts +30 -0
  109. package/dist/utils/env.d.ts +1 -0
  110. package/dist/utils/index.d.ts +9 -0
  111. package/dist/utils/internal-initiator.d.ts +6 -0
  112. package/dist/utils/logger.d.ts +8 -0
  113. package/dist/utils/polling.d.ts +21 -0
  114. package/dist/utils/session-manager.d.ts +55 -0
  115. package/dist/utils/session.d.ts +90 -0
  116. package/dist/utils/subagent-depth.d.ts +35 -0
  117. package/dist/utils/system-collapse.d.ts +6 -0
  118. package/dist/utils/task.d.ts +4 -0
  119. package/dist/utils/zip-extractor.d.ts +1 -0
  120. package/index.ts +1 -0
  121. package/opencode-dux.schema.json +634 -0
  122. package/package.json +103 -0
  123. package/src/agents/descriptions.ts +55 -0
  124. package/src/agents/designer.test.ts +86 -0
  125. package/src/agents/designer.ts +154 -0
  126. package/src/agents/display-name.test.ts +186 -0
  127. package/src/agents/explorer.test.ts +79 -0
  128. package/src/agents/explorer.ts +144 -0
  129. package/src/agents/fixer.test.ts +79 -0
  130. package/src/agents/fixer.ts +145 -0
  131. package/src/agents/index.test.ts +472 -0
  132. package/src/agents/index.ts +248 -0
  133. package/src/agents/interpreter.ts +136 -0
  134. package/src/agents/librarian.test.ts +80 -0
  135. package/src/agents/librarian.ts +145 -0
  136. package/src/agents/oracle.test.ts +89 -0
  137. package/src/agents/oracle.ts +184 -0
  138. package/src/agents/orchestrator.test.ts +116 -0
  139. package/src/agents/orchestrator.ts +574 -0
  140. package/src/agents/overrides.ts +95 -0
  141. package/src/agents/prompt-blocks.test.ts +114 -0
  142. package/src/agents/prompt-blocks.ts +640 -0
  143. package/src/agents/steward.ts +146 -0
  144. package/src/cli/config-io.test.ts +536 -0
  145. package/src/cli/config-io.ts +473 -0
  146. package/src/cli/config-manager.test.ts +141 -0
  147. package/src/cli/config-manager.ts +4 -0
  148. package/src/cli/index.ts +88 -0
  149. package/src/cli/install.ts +282 -0
  150. package/src/cli/mcps.test.ts +62 -0
  151. package/src/cli/mcps.ts +39 -0
  152. package/src/cli/model-key-normalization.test.ts +21 -0
  153. package/src/cli/model-key-normalization.ts +60 -0
  154. package/src/cli/paths.test.ts +167 -0
  155. package/src/cli/paths.ts +144 -0
  156. package/src/cli/providers.test.ts +118 -0
  157. package/src/cli/providers.ts +141 -0
  158. package/src/cli/skills.test.ts +111 -0
  159. package/src/cli/skills.ts +103 -0
  160. package/src/cli/system.test.ts +91 -0
  161. package/src/cli/system.ts +180 -0
  162. package/src/cli/types.ts +43 -0
  163. package/src/config/constants.ts +58 -0
  164. package/src/config/index.ts +4 -0
  165. package/src/config/loader.test.ts +1194 -0
  166. package/src/config/loader.ts +269 -0
  167. package/src/config/model-resolution.test.ts +176 -0
  168. package/src/config/runtime-preset.test.ts +61 -0
  169. package/src/config/runtime-preset.ts +37 -0
  170. package/src/config/schema.ts +248 -0
  171. package/src/config/utils.test.ts +41 -0
  172. package/src/config/utils.ts +23 -0
  173. package/src/discovery/local/types.ts +85 -0
  174. package/src/discovery/local.ts +322 -0
  175. package/src/discovery/mcp-servers.ts +804 -0
  176. package/src/discovery/skills.ts +959 -0
  177. package/src/hooks/apply-patch/codec.test.ts +184 -0
  178. package/src/hooks/apply-patch/codec.ts +352 -0
  179. package/src/hooks/apply-patch/errors.ts +117 -0
  180. package/src/hooks/apply-patch/execution-context.ts +432 -0
  181. package/src/hooks/apply-patch/hook.test.ts +768 -0
  182. package/src/hooks/apply-patch/index.ts +126 -0
  183. package/src/hooks/apply-patch/matching.test.ts +215 -0
  184. package/src/hooks/apply-patch/matching.ts +586 -0
  185. package/src/hooks/apply-patch/operations.test.ts +1535 -0
  186. package/src/hooks/apply-patch/operations.ts +3 -0
  187. package/src/hooks/apply-patch/patch.ts +9 -0
  188. package/src/hooks/apply-patch/prepared-changes.ts +400 -0
  189. package/src/hooks/apply-patch/resolution.test.ts +420 -0
  190. package/src/hooks/apply-patch/resolution.ts +437 -0
  191. package/src/hooks/apply-patch/rewrite.ts +496 -0
  192. package/src/hooks/apply-patch/test-helpers.ts +52 -0
  193. package/src/hooks/apply-patch/types.ts +111 -0
  194. package/src/hooks/auto-update-checker/cache.test.ts +179 -0
  195. package/src/hooks/auto-update-checker/cache.ts +188 -0
  196. package/src/hooks/auto-update-checker/checker.test.ts +159 -0
  197. package/src/hooks/auto-update-checker/checker.ts +308 -0
  198. package/src/hooks/auto-update-checker/constants.ts +33 -0
  199. package/src/hooks/auto-update-checker/index.test.ts +282 -0
  200. package/src/hooks/auto-update-checker/index.ts +225 -0
  201. package/src/hooks/auto-update-checker/types.ts +26 -0
  202. package/src/hooks/chat-headers.test.ts +236 -0
  203. package/src/hooks/chat-headers.ts +97 -0
  204. package/src/hooks/context-pressure-reminder/index.test.ts +179 -0
  205. package/src/hooks/context-pressure-reminder/index.ts +137 -0
  206. package/src/hooks/delegate-task-retry/guidance.ts +41 -0
  207. package/src/hooks/delegate-task-retry/hook.ts +23 -0
  208. package/src/hooks/delegate-task-retry/index.test.ts +38 -0
  209. package/src/hooks/delegate-task-retry/index.ts +7 -0
  210. package/src/hooks/delegate-task-retry/patterns.ts +79 -0
  211. package/src/hooks/filter-available-skills/index.test.ts +297 -0
  212. package/src/hooks/filter-available-skills/index.ts +160 -0
  213. package/src/hooks/foreground-fallback/index.test.ts +624 -0
  214. package/src/hooks/foreground-fallback/index.ts +374 -0
  215. package/src/hooks/image-hook.ts +6 -0
  216. package/src/hooks/index.ts +17 -0
  217. package/src/hooks/json-error-recovery/hook.ts +73 -0
  218. package/src/hooks/json-error-recovery/index.test.ts +111 -0
  219. package/src/hooks/json-error-recovery/index.ts +6 -0
  220. package/src/hooks/phase-reminder/index.test.ts +74 -0
  221. package/src/hooks/phase-reminder/index.ts +85 -0
  222. package/src/hooks/post-file-tool-nudge/index.test.ts +94 -0
  223. package/src/hooks/post-file-tool-nudge/index.ts +63 -0
  224. package/src/hooks/task-session-manager/index.test.ts +833 -0
  225. package/src/hooks/task-session-manager/index.ts +434 -0
  226. package/src/hooks/todo-continuation/index.test.ts +3026 -0
  227. package/src/hooks/todo-continuation/index.ts +878 -0
  228. package/src/hooks/todo-continuation/todo-hygiene.test.ts +204 -0
  229. package/src/hooks/todo-continuation/todo-hygiene.ts +207 -0
  230. package/src/index.ts +1672 -0
  231. package/src/mcp/context7.ts +14 -0
  232. package/src/mcp/grep-app.ts +11 -0
  233. package/src/mcp/index.test.ts +96 -0
  234. package/src/mcp/index.ts +66 -0
  235. package/src/mcp/types.ts +16 -0
  236. package/src/mcp/websearch.ts +47 -0
  237. package/src/skills/codemap/README.md +60 -0
  238. package/src/skills/codemap/SKILL.md +174 -0
  239. package/src/skills/codemap/scripts/codemap.mjs +483 -0
  240. package/src/skills/codemap/scripts/codemap.test.ts +129 -0
  241. package/src/skills/registry.ts +218 -0
  242. package/src/skills/simplify/README.md +19 -0
  243. package/src/skills/simplify/SKILL.md +138 -0
  244. package/src/subscriptions/accounts-store.test.ts +236 -0
  245. package/src/subscriptions/accounts-store.ts +184 -0
  246. package/src/subscriptions/index.ts +30 -0
  247. package/src/subscriptions/neuralwatt-scraper.ts +108 -0
  248. package/src/subscriptions/opencode-go-scraper.ts +301 -0
  249. package/src/subscriptions/types.ts +145 -0
  250. package/src/subscriptions/usage-service.test.ts +202 -0
  251. package/src/subscriptions/usage-service.ts +651 -0
  252. package/src/tools/ast-grep/cli.ts +257 -0
  253. package/src/tools/ast-grep/constants.ts +214 -0
  254. package/src/tools/ast-grep/downloader.ts +131 -0
  255. package/src/tools/ast-grep/index.ts +24 -0
  256. package/src/tools/ast-grep/tools.ts +117 -0
  257. package/src/tools/ast-grep/types.ts +51 -0
  258. package/src/tools/ast-grep/utils.ts +126 -0
  259. package/src/tools/delegate-handoff.test.ts +18 -0
  260. package/src/tools/delegate.ts +508 -0
  261. package/src/tools/index.ts +8 -0
  262. package/src/tools/preset-manager.test.ts +795 -0
  263. package/src/tools/preset-manager.ts +332 -0
  264. package/src/tools/smartfetch/binary.ts +58 -0
  265. package/src/tools/smartfetch/cache.test.ts +34 -0
  266. package/src/tools/smartfetch/cache.ts +112 -0
  267. package/src/tools/smartfetch/constants.ts +29 -0
  268. package/src/tools/smartfetch/index.ts +8 -0
  269. package/src/tools/smartfetch/network.test.ts +178 -0
  270. package/src/tools/smartfetch/network.ts +614 -0
  271. package/src/tools/smartfetch/secondary-model.test.ts +85 -0
  272. package/src/tools/smartfetch/secondary-model.ts +276 -0
  273. package/src/tools/smartfetch/tool.test.ts +60 -0
  274. package/src/tools/smartfetch/tool.ts +832 -0
  275. package/src/tools/smartfetch/types.ts +135 -0
  276. package/src/tools/smartfetch/utils.test.ts +24 -0
  277. package/src/tools/smartfetch/utils.ts +456 -0
  278. package/src/tui-state.test.ts +867 -0
  279. package/src/tui-state.ts +1255 -0
  280. package/src/tui.test.ts +336 -0
  281. package/src/tui.ts +1539 -0
  282. package/src/utils/agent-variant.test.ts +244 -0
  283. package/src/utils/agent-variant.ts +187 -0
  284. package/src/utils/compat.ts +91 -0
  285. package/src/utils/env.ts +12 -0
  286. package/src/utils/index.ts +9 -0
  287. package/src/utils/internal-initiator.ts +28 -0
  288. package/src/utils/logger.test.ts +220 -0
  289. package/src/utils/logger.ts +136 -0
  290. package/src/utils/polling.test.ts +191 -0
  291. package/src/utils/polling.ts +67 -0
  292. package/src/utils/session-manager.test.ts +173 -0
  293. package/src/utils/session-manager.ts +356 -0
  294. package/src/utils/session.test.ts +110 -0
  295. package/src/utils/session.ts +389 -0
  296. package/src/utils/subagent-depth.test.ts +170 -0
  297. package/src/utils/subagent-depth.ts +75 -0
  298. package/src/utils/system-collapse.test.ts +86 -0
  299. package/src/utils/system-collapse.ts +24 -0
  300. package/src/utils/task.test.ts +24 -0
  301. package/src/utils/task.ts +20 -0
  302. package/src/utils/zip-extractor.ts +102 -0
@@ -0,0 +1,473 @@
1
+ import {
2
+ copyFileSync,
3
+ existsSync,
4
+ readFileSync,
5
+ renameSync,
6
+ statSync,
7
+ writeFileSync,
8
+ } from 'node:fs';
9
+ import { dirname, join } from 'node:path';
10
+ import {
11
+ ensureConfigDir,
12
+ ensureOpenCodeConfigDir,
13
+ ensureTuiConfigDir,
14
+ getExistingConfigPath,
15
+ getExistingTuiConfigPath,
16
+ getLiteConfig,
17
+ } from './paths';
18
+ import { generateLiteConfig } from './providers';
19
+ import type {
20
+ ConfigMergeResult,
21
+ DetectedConfig,
22
+ InstallConfig,
23
+ OpenCodeConfig,
24
+ } from './types';
25
+
26
+ const PACKAGE_NAME = 'opencode-dux';
27
+
28
+ function isString(value: unknown): value is string {
29
+ return typeof value === 'string';
30
+ }
31
+
32
+ function getPlugins(config: OpenCodeConfig): unknown[] {
33
+ return Array.isArray(config.plugin) ? config.plugin : [];
34
+ }
35
+
36
+ function getPluginEntries(config: OpenCodeConfig): string[] {
37
+ return getPlugins(config).filter(isString);
38
+ }
39
+
40
+ function getPluginSpec(entry: unknown): string | undefined {
41
+ if (isString(entry)) return entry;
42
+ if (!Array.isArray(entry)) return undefined;
43
+
44
+ const spec = entry[0];
45
+ return isString(spec) ? spec : undefined;
46
+ }
47
+
48
+ function normalizePathForMatch(path: string): string {
49
+ return path.replaceAll('\\', '/');
50
+ }
51
+
52
+ function findPackageRoot(startPath: string): string | null {
53
+ let currentPath = dirname(startPath);
54
+
55
+ while (true) {
56
+ const packageJsonPath = join(currentPath, 'package.json');
57
+
58
+ if (existsSync(packageJsonPath)) {
59
+ try {
60
+ const packageJson = JSON.parse(
61
+ readFileSync(packageJsonPath, 'utf-8'),
62
+ ) as {
63
+ name?: string;
64
+ };
65
+
66
+ if (packageJson.name === PACKAGE_NAME) {
67
+ return currentPath;
68
+ }
69
+ } catch {
70
+ // Ignore invalid package.json while walking upward.
71
+ }
72
+ }
73
+
74
+ const parentPath = dirname(currentPath);
75
+ if (parentPath === currentPath) {
76
+ return null;
77
+ }
78
+ currentPath = parentPath;
79
+ }
80
+ }
81
+
82
+ function isPackageManagerInstall(path: string): boolean {
83
+ const normalizedPath = normalizePathForMatch(path);
84
+ return normalizedPath.includes(`/node_modules/${PACKAGE_NAME}`);
85
+ }
86
+
87
+ function isLocalPackageRootEntry(entry: string): boolean {
88
+ if (!entry || entry.startsWith('file://')) {
89
+ return false;
90
+ }
91
+
92
+ const packageJsonPath = join(entry, 'package.json');
93
+ if (!existsSync(packageJsonPath)) {
94
+ return false;
95
+ }
96
+
97
+ try {
98
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as {
99
+ name?: string;
100
+ };
101
+ return packageJson.name === PACKAGE_NAME;
102
+ } catch {
103
+ return false;
104
+ }
105
+ }
106
+
107
+ function isPluginEntry(entry: string): boolean {
108
+ return (
109
+ entry === PACKAGE_NAME ||
110
+ entry.startsWith(`${PACKAGE_NAME}@`) ||
111
+ (entry.startsWith('file://') && entry.includes(PACKAGE_NAME)) ||
112
+ isLocalPackageRootEntry(entry)
113
+ );
114
+ }
115
+
116
+ function isMatchingPluginEntry(entry: unknown): boolean {
117
+ const spec = getPluginSpec(entry);
118
+ return spec ? isPluginEntry(spec) : false;
119
+ }
120
+
121
+ function getPluginEntry(): string {
122
+ const cliEntryPath = process.argv[1];
123
+
124
+ if (!cliEntryPath) {
125
+ return PACKAGE_NAME;
126
+ }
127
+
128
+ try {
129
+ const packageRoot = findPackageRoot(cliEntryPath);
130
+
131
+ if (!packageRoot || isPackageManagerInstall(packageRoot)) {
132
+ return PACKAGE_NAME;
133
+ }
134
+
135
+ return packageRoot;
136
+ } catch {
137
+ return PACKAGE_NAME;
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Strip JSON comments (single-line // and multi-line) and trailing commas for JSONC support.
143
+ */
144
+ export function stripJsonComments(json: string): string {
145
+ const commentPattern = /\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g;
146
+ const trailingCommaPattern = /\\"|"(?:\\"|[^"])*"|(,)(\s*[}\]])/g;
147
+
148
+ return json
149
+ .replace(commentPattern, (match, commentGroup) =>
150
+ commentGroup ? '' : match,
151
+ )
152
+ .replace(trailingCommaPattern, (match, comma, closing) =>
153
+ comma ? closing : match,
154
+ );
155
+ }
156
+
157
+ export function parseConfigFile(path: string): {
158
+ config: OpenCodeConfig | null;
159
+ error?: string;
160
+ } {
161
+ try {
162
+ if (!existsSync(path)) return { config: null };
163
+ const stat = statSync(path);
164
+ if (stat.size === 0) return { config: null };
165
+ const content = readFileSync(path, 'utf-8');
166
+ if (content.trim().length === 0) return { config: null };
167
+ return { config: JSON.parse(stripJsonComments(content)) as OpenCodeConfig };
168
+ } catch (err) {
169
+ return { config: null, error: String(err) };
170
+ }
171
+ }
172
+
173
+ export function parseConfig(path: string): {
174
+ config: OpenCodeConfig | null;
175
+ error?: string;
176
+ } {
177
+ const result = parseConfigFile(path);
178
+ if (result.config || result.error) return result;
179
+
180
+ if (path.endsWith('.json')) {
181
+ const jsoncPath = path.replace(/\.json$/, '.jsonc');
182
+ return parseConfigFile(jsoncPath);
183
+ }
184
+ return { config: null };
185
+ }
186
+
187
+ /**
188
+ * Write config to file atomically.
189
+ */
190
+ export function writeConfig(configPath: string, config: OpenCodeConfig): void {
191
+ if (configPath.endsWith('.jsonc')) {
192
+ console.warn(
193
+ '[config-manager] Writing to .jsonc file - comments will not be preserved',
194
+ );
195
+ }
196
+
197
+ const tmpPath = `${configPath}.tmp`;
198
+ const bakPath = `${configPath}.bak`;
199
+ const content = `${JSON.stringify(config, null, 2)}\n`;
200
+
201
+ // Backup existing config if it exists
202
+ if (existsSync(configPath)) {
203
+ copyFileSync(configPath, bakPath);
204
+ }
205
+
206
+ // Atomic write pattern: write to tmp, then rename
207
+ writeFileSync(tmpPath, content);
208
+ renameSync(tmpPath, configPath);
209
+ }
210
+
211
+ export async function addPluginToOpenCodeConfig(): Promise<ConfigMergeResult> {
212
+ const configPath = getExistingConfigPath();
213
+
214
+ try {
215
+ ensureOpenCodeConfigDir();
216
+ } catch (err) {
217
+ return {
218
+ success: false,
219
+ configPath,
220
+ error: `Failed to create config directory: ${err}`,
221
+ };
222
+ }
223
+
224
+ try {
225
+ const { config: parsedConfig, error } = parseConfig(configPath);
226
+ if (error) {
227
+ return {
228
+ success: false,
229
+ configPath,
230
+ error: `Failed to parse config: ${error}`,
231
+ };
232
+ }
233
+ const config = parsedConfig ?? {};
234
+ const plugins = getPlugins(config);
235
+
236
+ const pluginEntry = getPluginEntry();
237
+
238
+ // Remove existing opencode-dux entries
239
+ const filteredPlugins = plugins.filter(
240
+ (plugin) => !isMatchingPluginEntry(plugin),
241
+ );
242
+
243
+ // Add fresh entry
244
+ filteredPlugins.push(pluginEntry);
245
+ config.plugin = filteredPlugins;
246
+
247
+ writeConfig(configPath, config);
248
+ return { success: true, configPath };
249
+ } catch (err) {
250
+ return {
251
+ success: false,
252
+ configPath,
253
+ error: `Failed to update opencode config: ${err}`,
254
+ };
255
+ }
256
+ }
257
+
258
+ export async function addPluginToOpenCodeTuiConfig(): Promise<ConfigMergeResult> {
259
+ const configPath = getExistingTuiConfigPath();
260
+
261
+ try {
262
+ ensureTuiConfigDir();
263
+ } catch (err) {
264
+ return {
265
+ success: false,
266
+ configPath,
267
+ error: `Failed to create config directory: ${err}`,
268
+ };
269
+ }
270
+
271
+ try {
272
+ const { config: parsedConfig, error } = parseConfig(configPath);
273
+ if (error) {
274
+ return {
275
+ success: false,
276
+ configPath,
277
+ error: `Failed to parse TUI config: ${error}`,
278
+ };
279
+ }
280
+ const config = parsedConfig ?? {};
281
+ const plugins = getPlugins(config);
282
+ const pluginEntry = getPluginEntry();
283
+ const filteredPlugins = plugins.filter(
284
+ (plugin) => !isMatchingPluginEntry(plugin),
285
+ );
286
+
287
+ filteredPlugins.push(pluginEntry);
288
+ config.plugin = filteredPlugins;
289
+
290
+ writeConfig(configPath, config);
291
+ return { success: true, configPath };
292
+ } catch (err) {
293
+ return {
294
+ success: false,
295
+ configPath,
296
+ error: `Failed to update opencode TUI config: ${err}`,
297
+ };
298
+ }
299
+ }
300
+
301
+ // Removed: addAuthPlugins - no longer needed with cliproxy
302
+ // Removed: addProviderConfig - default opencode now has kimi provider config
303
+
304
+ export function writeLiteConfig(
305
+ installConfig: InstallConfig,
306
+ targetPath?: string,
307
+ ): ConfigMergeResult {
308
+ const configPath = targetPath ?? getLiteConfig();
309
+
310
+ try {
311
+ ensureConfigDir();
312
+ const config = generateLiteConfig(installConfig);
313
+
314
+ // Atomic write for lite config too
315
+ const tmpPath = `${configPath}.tmp`;
316
+ const bakPath = `${configPath}.bak`;
317
+ const content = `${JSON.stringify(config, null, 2)}\n`;
318
+
319
+ // Backup existing config if it exists
320
+ if (existsSync(configPath)) {
321
+ copyFileSync(configPath, bakPath);
322
+ }
323
+
324
+ writeFileSync(tmpPath, content);
325
+ renameSync(tmpPath, configPath);
326
+
327
+ return { success: true, configPath };
328
+ } catch (err) {
329
+ return {
330
+ success: false,
331
+ configPath,
332
+ error: `Failed to write lite config: ${err}`,
333
+ };
334
+ }
335
+ }
336
+
337
+ export function disableDefaultAgents(): ConfigMergeResult {
338
+ const configPath = getExistingConfigPath();
339
+
340
+ try {
341
+ ensureOpenCodeConfigDir();
342
+ const { config: parsedConfig, error } = parseConfig(configPath);
343
+ if (error) {
344
+ return {
345
+ success: false,
346
+ configPath,
347
+ error: `Failed to parse config: ${error}`,
348
+ };
349
+ }
350
+ const config = parsedConfig ?? {};
351
+
352
+ const agent = (config.agent ?? {}) as Record<string, unknown>;
353
+ agent.explore = { disable: true };
354
+ agent.general = { disable: true };
355
+ config.agent = agent;
356
+
357
+ writeConfig(configPath, config);
358
+ return { success: true, configPath };
359
+ } catch (err) {
360
+ return {
361
+ success: false,
362
+ configPath,
363
+ error: `Failed to disable default agents: ${err}`,
364
+ };
365
+ }
366
+ }
367
+
368
+ export function enableLspByDefault(): ConfigMergeResult {
369
+ const configPath = getExistingConfigPath();
370
+
371
+ try {
372
+ ensureOpenCodeConfigDir();
373
+ const { config: parsedConfig, error } = parseConfig(configPath);
374
+ if (error) {
375
+ return {
376
+ success: false,
377
+ configPath,
378
+ error: `Failed to parse config: ${error}`,
379
+ };
380
+ }
381
+ const config = parsedConfig ?? {};
382
+
383
+ if (config.lsp === undefined) {
384
+ config.lsp = true;
385
+ writeConfig(configPath, config);
386
+ }
387
+
388
+ return { success: true, configPath };
389
+ } catch (err) {
390
+ return {
391
+ success: false,
392
+ configPath,
393
+ error: `Failed to enable LSP: ${err}`,
394
+ };
395
+ }
396
+ }
397
+
398
+ export function canModifyOpenCodeConfig(): boolean {
399
+ try {
400
+ const configPath = getExistingConfigPath();
401
+ if (!existsSync(configPath)) return true; // Will be created
402
+ const stat = statSync(configPath);
403
+ // Check if writable - simple check for now
404
+ return !!(stat.mode & 0o200);
405
+ } catch {
406
+ return false;
407
+ }
408
+ }
409
+
410
+ // Antigravity, Google provider, and Chutes provider functions removed in simplification refactor.
411
+
412
+ export function detectCurrentConfig(): DetectedConfig {
413
+ const result: DetectedConfig = {
414
+ isInstalled: false,
415
+ hasKimi: false,
416
+ hasOpenAI: false,
417
+ hasAnthropic: false,
418
+ hasCopilot: false,
419
+ hasZaiPlan: false,
420
+ hasAntigravity: false,
421
+ hasChutes: false,
422
+ hasOpencodeZen: false,
423
+ };
424
+
425
+ const { config } = parseConfig(getExistingConfigPath());
426
+ if (!config) return result;
427
+
428
+ const plugins = getPluginEntries(config);
429
+ result.isInstalled = plugins.some((p) => isPluginEntry(p));
430
+ result.hasAntigravity = plugins.some((p) =>
431
+ p.startsWith('opencode-antigravity-auth'),
432
+ );
433
+
434
+ // Check for providers
435
+ const providers = config.provider as Record<string, unknown> | undefined;
436
+ result.hasKimi = !!providers?.kimi;
437
+ result.hasAnthropic = !!providers?.anthropic;
438
+ result.hasCopilot = !!providers?.['github-copilot'];
439
+ result.hasZaiPlan = !!providers?.['zai-coding-plan'];
440
+ result.hasChutes = !!providers?.chutes;
441
+ if (providers?.google) result.hasAntigravity = true;
442
+
443
+ // Try to detect from lite config
444
+ const { config: liteConfig } = parseConfig(getLiteConfig());
445
+ if (liteConfig && typeof liteConfig === 'object') {
446
+ const configObj = liteConfig as Record<string, unknown>;
447
+ const presetName = configObj.preset as string;
448
+ const presets = configObj.presets as Record<string, unknown>;
449
+ const agents = presets?.[presetName] as
450
+ | Record<string, { model?: string }>
451
+ | undefined;
452
+
453
+ if (agents) {
454
+ const models = Object.values(agents)
455
+ .map((a) => a?.model)
456
+ .filter(Boolean);
457
+ result.hasOpenAI = models.some((m) => m?.startsWith('openai/'));
458
+ result.hasAnthropic = models.some((m) => m?.startsWith('anthropic/'));
459
+ result.hasCopilot = models.some((m) => m?.startsWith('github-copilot/'));
460
+ result.hasZaiPlan = models.some((m) => m?.startsWith('zai-coding-plan/'));
461
+ result.hasOpencodeZen = models.some((m) => m?.startsWith('opencode/'));
462
+ if (models.some((m) => m?.startsWith('google/'))) {
463
+ result.hasAntigravity = true;
464
+ }
465
+ if (models.some((m) => m?.startsWith('chutes/'))) {
466
+ result.hasChutes = true;
467
+ }
468
+ }
469
+
470
+ }
471
+
472
+ return result;
473
+ }
@@ -0,0 +1,141 @@
1
+ /// <reference types="bun-types" />
2
+
3
+ import { describe, expect, test } from 'bun:test';
4
+ import { stripJsonComments } from './config-manager';
5
+
6
+ describe('config-manager (barrel)', () => {
7
+ describe('stripJsonComments', () => {
8
+ test('returns unchanged JSON without comments', () => {
9
+ const json = '{"key": "value"}';
10
+ expect(stripJsonComments(json)).toBe(json);
11
+ });
12
+
13
+ test('strips single-line comments', () => {
14
+ const json = `{
15
+ "key": "value" // this is a comment
16
+ }`;
17
+ expect(JSON.parse(stripJsonComments(json))).toEqual({ key: 'value' });
18
+ });
19
+
20
+ test('strips multi-line comments', () => {
21
+ const json = `{
22
+ /* this is a
23
+ multi-line comment */
24
+ "key": "value"
25
+ }`;
26
+ expect(JSON.parse(stripJsonComments(json))).toEqual({ key: 'value' });
27
+ });
28
+
29
+ test('strips trailing commas', () => {
30
+ const json = `{
31
+ "key": "value",
32
+ }`;
33
+ expect(JSON.parse(stripJsonComments(json))).toEqual({ key: 'value' });
34
+ });
35
+
36
+ test('strips trailing commas in arrays', () => {
37
+ const json = `{
38
+ "arr": [1, 2, 3,]
39
+ }`;
40
+ expect(JSON.parse(stripJsonComments(json))).toEqual({ arr: [1, 2, 3] });
41
+ });
42
+
43
+ test('preserves URLs with double slashes', () => {
44
+ const json = '{"url": "https://example.com"}';
45
+ expect(JSON.parse(stripJsonComments(json))).toEqual({
46
+ url: 'https://example.com',
47
+ });
48
+ });
49
+
50
+ test('preserves strings containing comment-like patterns', () => {
51
+ const json = '{"code": "// not a comment", "block": "/* also not */"}';
52
+ expect(JSON.parse(stripJsonComments(json))).toEqual({
53
+ code: '// not a comment',
54
+ block: '/* also not */',
55
+ });
56
+ });
57
+
58
+ test('handles complex JSONC with mixed comments and trailing commas', () => {
59
+ const json = `{
60
+ // Configuration for the plugin
61
+ "plugin": ["opencode-dux"],
62
+ /* Provider settings
63
+ with multiple lines */
64
+ "provider": {
65
+ "google": {
66
+ "name": "Google", // inline comment
67
+ },
68
+ },
69
+ }`;
70
+ const result = JSON.parse(stripJsonComments(json));
71
+ expect(result).toEqual({
72
+ plugin: ['opencode-dux'],
73
+ provider: {
74
+ google: {
75
+ name: 'Google',
76
+ },
77
+ },
78
+ });
79
+ });
80
+
81
+ test('handles escaped quotes in strings', () => {
82
+ const json = '{"message": "He said \\"hello\\""}';
83
+ expect(JSON.parse(stripJsonComments(json))).toEqual({
84
+ message: 'He said "hello"',
85
+ });
86
+ });
87
+
88
+ test('handles empty input', () => {
89
+ expect(stripJsonComments('')).toBe('');
90
+ });
91
+
92
+ test('handles whitespace-only input', () => {
93
+ expect(stripJsonComments(' ')).toBe(' ');
94
+ });
95
+
96
+ test('handles single-line comment at start of file', () => {
97
+ const json = `// comment at start
98
+ {"key": "value"}`;
99
+ expect(JSON.parse(stripJsonComments(json))).toEqual({ key: 'value' });
100
+ });
101
+
102
+ test('handles comment-only lines between properties', () => {
103
+ const json = `{
104
+ "a": 1,
105
+ // comment line
106
+ "b": 2
107
+ }`;
108
+ expect(JSON.parse(stripJsonComments(json))).toEqual({ a: 1, b: 2 });
109
+ });
110
+
111
+ test('handles multiple trailing commas in nested structures', () => {
112
+ const json = `{"nested": {"a": 1,},}`;
113
+ expect(JSON.parse(stripJsonComments(json))).toEqual({ nested: { a: 1 } });
114
+ });
115
+
116
+ test('handles unclosed string gracefully without throwing', () => {
117
+ const json = '{"key": "unclosed';
118
+ expect(() => stripJsonComments(json)).not.toThrow();
119
+ });
120
+
121
+ test('preserves comma-bracket patterns inside strings', () => {
122
+ const json = '{"script": "test [,]", "json": "{,}"}';
123
+ const result = JSON.parse(stripJsonComments(json));
124
+ expect(result.script).toBe('test [,]');
125
+ expect(result.json).toBe('{,}');
126
+ });
127
+
128
+ test('preserves comma-brace patterns inside strings', () => {
129
+ const json = '{"glob": "*.{js,ts}", "arr": "[a,]"}';
130
+ const result = JSON.parse(stripJsonComments(json));
131
+ expect(result.glob).toBe('*.{js,ts}');
132
+ expect(result.arr).toBe('[a,]');
133
+ });
134
+
135
+ test('handles Windows CRLF line endings', () => {
136
+ const json = '{\r\n "key": "value", // comment\r\n}';
137
+ const result = JSON.parse(stripJsonComments(json));
138
+ expect(result).toEqual({ key: 'value' });
139
+ });
140
+ });
141
+ });
@@ -0,0 +1,4 @@
1
+ export * from './config-io';
2
+ export * from './paths';
3
+ export * from './providers';
4
+ export * from './system';
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env bun
2
+ import { install } from './install';
3
+ import { getGeneratedPresetNames, isGeneratedPresetName } from './providers';
4
+ import type { BooleanArg, InstallArgs } from './types';
5
+
6
+ function parseArgs(args: string[]): InstallArgs {
7
+ const result: InstallArgs = {
8
+ tui: true,
9
+ skills: 'yes',
10
+ };
11
+
12
+ for (const arg of args) {
13
+ if (arg === '--no-tui') {
14
+ result.tui = false;
15
+ } else if (arg.startsWith('--skills=')) {
16
+ result.skills = arg.split('=')[1] as BooleanArg;
17
+ } else if (arg.startsWith('--preset=')) {
18
+ const preset = arg.split('=')[1];
19
+ if (!isGeneratedPresetName(preset)) {
20
+ console.error(
21
+ `Unsupported preset: ${preset}. Available presets: ${getGeneratedPresetNames().join(', ')}`,
22
+ );
23
+ process.exit(1);
24
+ }
25
+ result.preset = preset;
26
+ } else if (arg === '--dry-run') {
27
+ result.dryRun = true;
28
+ } else if (arg === '--reset') {
29
+ result.reset = true;
30
+ } else if (arg === '-h' || arg === '--help') {
31
+ printHelp();
32
+ process.exit(0);
33
+ }
34
+ }
35
+
36
+ return result;
37
+ }
38
+
39
+ function printHelp(): void {
40
+ console.log(`
41
+ opencode-dux installer
42
+
43
+ Usage: bunx opencode-dux install [OPTIONS]
44
+
45
+ Options:
46
+ --skills=yes|no Install recommended and bundled skills (default: yes)
47
+ --preset=<name> Active generated config preset (default: openai)
48
+ --no-tui Non-interactive mode
49
+ --dry-run Simulate install without writing files
50
+ --reset Force overwrite of existing configuration
51
+ -h, --help Show this help message
52
+
53
+ Available presets: ${getGeneratedPresetNames().join(', ')}
54
+
55
+ The installer generates OpenAI and OpenCode Go presets by default.
56
+ OpenAI is active unless --preset selects another generated preset.
57
+ For the full config reference, see docs/configuration.md.
58
+
59
+ Examples:
60
+ bunx opencode-dux install
61
+ bunx opencode-dux install --no-tui --skills=yes
62
+ bunx opencode-dux install --preset=opencode-go
63
+ bunx opencode-dux install --reset
64
+ `);
65
+ }
66
+
67
+ async function main(): Promise<void> {
68
+ const args = process.argv.slice(2);
69
+
70
+ if (args.length === 0 || args[0] === 'install') {
71
+ const hasSubcommand = args[0] === 'install';
72
+ const installArgs = parseArgs(args.slice(hasSubcommand ? 1 : 0));
73
+ const exitCode = await install(installArgs);
74
+ process.exit(exitCode);
75
+ } else if (args[0] === '-h' || args[0] === '--help') {
76
+ printHelp();
77
+ process.exit(0);
78
+ } else {
79
+ console.error(`Unknown command: ${args[0]}`);
80
+ console.error('Run with --help for usage information');
81
+ process.exit(1);
82
+ }
83
+ }
84
+
85
+ main().catch((err) => {
86
+ console.error('Fatal error:', err);
87
+ process.exit(1);
88
+ });