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,184 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import {
4
+ formatPatch,
5
+ normalizeUnicode,
6
+ parsePatch,
7
+ parsePatchStrict,
8
+ stripHeredoc,
9
+ } from './codec';
10
+ import type { ParsedPatch } from './types';
11
+
12
+ describe('apply-patch/codec', () => {
13
+ test('stripHeredoc extracts the real patch content', () => {
14
+ expect(
15
+ stripHeredoc(`cat <<'PATCH'
16
+ *** Begin Patch
17
+ *** End Patch
18
+ PATCH`),
19
+ ).toBe('*** Begin Patch\n*** End Patch');
20
+ });
21
+
22
+ test('parsePatch recognizes add delete update and move', () => {
23
+ const parsed = parsePatch(`*** Begin Patch
24
+ *** Add File: added.txt
25
+ +alpha
26
+ *** Delete File: removed.txt
27
+ *** Update File: before.txt
28
+ *** Move to: after.txt
29
+ @@ ctx
30
+ line-a
31
+ -line-b
32
+ +line-c
33
+ *** End of File
34
+ *** End Patch`);
35
+
36
+ expect(parsed.hunks).toHaveLength(3);
37
+ expect(parsed.hunks[0]).toEqual({
38
+ type: 'add',
39
+ path: 'added.txt',
40
+ contents: 'alpha',
41
+ });
42
+ expect(parsed.hunks[1]).toEqual({ type: 'delete', path: 'removed.txt' });
43
+ expect(parsed.hunks[2]).toEqual({
44
+ type: 'update',
45
+ path: 'before.txt',
46
+ move_path: 'after.txt',
47
+ chunks: [
48
+ {
49
+ old_lines: ['line-a', 'line-b'],
50
+ new_lines: ['line-a', 'line-c'],
51
+ change_context: 'ctx',
52
+ is_end_of_file: true,
53
+ },
54
+ ],
55
+ });
56
+ });
57
+
58
+ test('parsePatch tolerates heredocs with aggressive CRLF and preserves EOF', () => {
59
+ const parsed = parsePatch(`cat <<'PATCH'\r
60
+ *** Begin Patch\r
61
+ *** Update File: sample.txt\r
62
+ @@\r
63
+ -alpha\r
64
+ +beta\r
65
+ *** End of File\r
66
+ *** End Patch\r
67
+ PATCH`);
68
+
69
+ expect(parsed.hunks).toEqual([
70
+ {
71
+ type: 'update',
72
+ path: 'sample.txt',
73
+ chunks: [
74
+ {
75
+ old_lines: ['alpha'],
76
+ new_lines: ['beta'],
77
+ change_context: undefined,
78
+ is_end_of_file: true,
79
+ },
80
+ ],
81
+ },
82
+ ]);
83
+ });
84
+
85
+ test('parsePatchStrict preserves End Patch text when it is hunk context', () => {
86
+ const markerPadding = ' ';
87
+ const parsed = parsePatchStrict(`*** Begin Patch${markerPadding}
88
+ *** Update File: sample.txt
89
+ @@ marker
90
+ *** End Patch
91
+ keep
92
+ *** End Patch${markerPadding}`);
93
+
94
+ expect(parsed.hunks).toEqual([
95
+ {
96
+ type: 'update',
97
+ path: 'sample.txt',
98
+ chunks: [
99
+ {
100
+ old_lines: ['*** End Patch', 'keep'],
101
+ new_lines: ['*** End Patch', 'keep'],
102
+ change_context: 'marker',
103
+ is_end_of_file: undefined,
104
+ },
105
+ ],
106
+ },
107
+ ]);
108
+ });
109
+
110
+ test('parsePatchStrict fails on garbage inside @@', () => {
111
+ expect(() =>
112
+ parsePatchStrict(`*** Begin Patch
113
+ *** Update File: sample.txt
114
+ @@
115
+ -alpha
116
+ garbage
117
+ +beta
118
+ *** End Patch`),
119
+ ).toThrow('unexpected line in patch chunk');
120
+ });
121
+
122
+ test('parsePatchStrict fails on garbage inside Add File', () => {
123
+ expect(() =>
124
+ parsePatchStrict(`*** Begin Patch
125
+ *** Add File: sample.txt
126
+ +alpha
127
+ garbage
128
+ *** End Patch`),
129
+ ).toThrow('unexpected line in Add File body');
130
+ });
131
+
132
+ test('parsePatchStrict fails on malformed Delete File', () => {
133
+ expect(() =>
134
+ parsePatchStrict(`*** Begin Patch
135
+ *** Delete File: sample.txt
136
+ +ghost
137
+ *** End Patch`),
138
+ ).toThrow('unexpected line between hunks');
139
+ });
140
+
141
+ test('parsePatchStrict fails on garbage after End Patch', () => {
142
+ expect(() =>
143
+ parsePatchStrict(`*** Begin Patch
144
+ *** Delete File: sample.txt
145
+ *** End Patch
146
+ garbage`),
147
+ ).toThrow('unexpected line after End Patch');
148
+ });
149
+
150
+ test('parsePatchStrict fails when Update File has no @@ chunks', () => {
151
+ expect(() =>
152
+ parsePatchStrict(`*** Begin Patch
153
+ *** Update File: sample.txt
154
+ *** End Patch`),
155
+ ).toThrow('missing @@ chunk body');
156
+ });
157
+
158
+ test('formatPatch allows stable parse -> format -> parse roundtrips', () => {
159
+ const parsed: ParsedPatch = {
160
+ hunks: [
161
+ {
162
+ type: 'update',
163
+ path: 'sample.txt',
164
+ chunks: [
165
+ {
166
+ old_lines: ['alpha', 'beta'],
167
+ new_lines: ['alpha', 'BETA'],
168
+ },
169
+ ],
170
+ },
171
+ ],
172
+ };
173
+
174
+ expect(parsePatch(formatPatch(parsed))).toEqual(parsed);
175
+ });
176
+
177
+ test('normalizeUnicode unifies expected typographic variants', () => {
178
+ expect(normalizeUnicode('“uno”…\u00A0dos-tres')).toBe('"uno"... dos-tres');
179
+ });
180
+
181
+ test('normalizeUnicode covers less common typographic variants', () => {
182
+ expect(normalizeUnicode('‛uno‟―dos')).toBe(`'uno"-dos`);
183
+ });
184
+ });
@@ -0,0 +1,352 @@
1
+ import type { ParsedPatch, PatchChunk, PatchHunk } from './types';
2
+
3
+ type ParseMode = 'permissive' | 'strict';
4
+
5
+ function normalizeLineEndings(text: string): string {
6
+ return text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
7
+ }
8
+
9
+ export function normalizeUnicode(text: string): string {
10
+ return text
11
+ .replace(/[\u2018\u2019\u201A\u201B]/g, "'")
12
+ .replace(/[\u201C\u201D\u201E\u201F]/g, '"')
13
+ .replace(/[\u2010\u2011\u2012\u2013\u2014\u2015]/g, '-')
14
+ .replace(/\u2026/g, '...')
15
+ .replace(/\u00A0/g, ' ');
16
+ }
17
+
18
+ export function stripHeredoc(input: string): string {
19
+ const normalized = normalizeLineEndings(input);
20
+ const match = normalized.match(
21
+ /^(?:cat\s+)?<<['"]?(\w+)['"]?\s*\n([\s\S]*?)\n\1\s*$/,
22
+ );
23
+ return match ? match[2] : normalized;
24
+ }
25
+
26
+ export function normalizePatchText(patchText: string): string {
27
+ return stripHeredoc(normalizeLineEndings(patchText).trim());
28
+ }
29
+
30
+ function parseHeader(lines: string[], index: number) {
31
+ const line = lines[index];
32
+
33
+ if (line.startsWith('*** Add File:')) {
34
+ const file = line.slice('*** Add File:'.length).trim();
35
+ return file ? { file, next: index + 1 } : null;
36
+ }
37
+
38
+ if (line.startsWith('*** Delete File:')) {
39
+ const file = line.slice('*** Delete File:'.length).trim();
40
+ return file ? { file, next: index + 1 } : null;
41
+ }
42
+
43
+ if (line.startsWith('*** Update File:')) {
44
+ const file = line.slice('*** Update File:'.length).trim();
45
+ let move: string | undefined;
46
+ let next = index + 1;
47
+
48
+ if (next < lines.length && lines[next].startsWith('*** Move to:')) {
49
+ const moveTarget = lines[next].slice('*** Move to:'.length).trim();
50
+ if (!moveTarget) {
51
+ return null;
52
+ }
53
+
54
+ move = moveTarget;
55
+ next += 1;
56
+ }
57
+
58
+ return file ? { file, move, next } : null;
59
+ }
60
+
61
+ return null;
62
+ }
63
+
64
+ function unexpectedPatchLine(context: string, line: string): never {
65
+ const rendered = line.length === 0 ? '<empty>' : line;
66
+ throw new Error(
67
+ `Invalid patch format: unexpected line ${context}: ${rendered}`,
68
+ );
69
+ }
70
+
71
+ function parseChangeContext(line: string): string | undefined {
72
+ const context = line.slice(2);
73
+ if (context.length === 0) {
74
+ return undefined;
75
+ }
76
+
77
+ return context.startsWith(' ') ? context.slice(1) || undefined : context;
78
+ }
79
+
80
+ function isPatchBoundary(line: string, marker: string): boolean {
81
+ return line.trimEnd() === marker;
82
+ }
83
+
84
+ function parseChunks(lines: string[], index: number, mode: ParseMode) {
85
+ const chunks: PatchChunk[] = [];
86
+ let at = index;
87
+
88
+ while (at < lines.length && !lines[at].startsWith('***')) {
89
+ if (!lines[at].startsWith('@@')) {
90
+ if (mode === 'strict') {
91
+ unexpectedPatchLine('in update body', lines[at]);
92
+ }
93
+ at += 1;
94
+ continue;
95
+ }
96
+
97
+ const context = parseChangeContext(lines[at]);
98
+ at += 1;
99
+
100
+ const old_lines: string[] = [];
101
+ const new_lines: string[] = [];
102
+ let eof = false;
103
+
104
+ while (
105
+ at < lines.length &&
106
+ !lines[at].startsWith('@@') &&
107
+ (!lines[at].startsWith('***') || lines[at] === '*** End of File')
108
+ ) {
109
+ const line = lines[at];
110
+
111
+ if (line === '*** End of File') {
112
+ eof = true;
113
+ at += 1;
114
+ break;
115
+ }
116
+
117
+ if (line.startsWith(' ')) {
118
+ old_lines.push(line.slice(1));
119
+ new_lines.push(line.slice(1));
120
+ at += 1;
121
+ continue;
122
+ }
123
+
124
+ if (line.startsWith('-')) {
125
+ old_lines.push(line.slice(1));
126
+ at += 1;
127
+ continue;
128
+ }
129
+
130
+ if (line.startsWith('+')) {
131
+ new_lines.push(line.slice(1));
132
+ at += 1;
133
+ continue;
134
+ }
135
+
136
+ if (mode === 'strict') {
137
+ unexpectedPatchLine('in patch chunk', line);
138
+ }
139
+
140
+ at += 1;
141
+ }
142
+
143
+ chunks.push({
144
+ old_lines,
145
+ new_lines,
146
+ change_context: context,
147
+ is_end_of_file: eof || undefined,
148
+ });
149
+ }
150
+
151
+ return { chunks, next: at };
152
+ }
153
+
154
+ function parseAdd(lines: string[], index: number, mode: ParseMode) {
155
+ const contents: string[] = [];
156
+ let at = index;
157
+
158
+ while (at < lines.length && !lines[at].startsWith('***')) {
159
+ if (lines[at].startsWith('+')) {
160
+ contents.push(lines[at].slice(1));
161
+ at += 1;
162
+ continue;
163
+ }
164
+
165
+ if (mode === 'strict') {
166
+ unexpectedPatchLine('in Add File body', lines[at]);
167
+ }
168
+
169
+ at += 1;
170
+ }
171
+
172
+ return { content: contents.join('\n'), next: at };
173
+ }
174
+
175
+ function parsePatchInternal(patchText: string, mode: ParseMode): ParsedPatch {
176
+ const clean = normalizePatchText(patchText);
177
+ const lines = clean.split('\n');
178
+ const begin = lines.findIndex((line) =>
179
+ isPatchBoundary(line, '*** Begin Patch'),
180
+ );
181
+ const end = lines.findIndex(
182
+ (line, index) => index > begin && isPatchBoundary(line, '*** End Patch'),
183
+ );
184
+
185
+ if (begin === -1 || end === -1 || begin >= end) {
186
+ throw new Error('Invalid patch format: missing Begin/End markers');
187
+ }
188
+
189
+ if (mode === 'strict') {
190
+ for (const line of lines.slice(0, begin)) {
191
+ unexpectedPatchLine('before Begin Patch', line);
192
+ }
193
+
194
+ for (const line of lines.slice(end + 1)) {
195
+ unexpectedPatchLine('after End Patch', line);
196
+ }
197
+ }
198
+
199
+ const hunks: PatchHunk[] = [];
200
+ let index = begin + 1;
201
+
202
+ while (index < end) {
203
+ const header = parseHeader(lines, index);
204
+
205
+ if (!header) {
206
+ if (mode === 'strict') {
207
+ unexpectedPatchLine('between hunks', lines[index]);
208
+ }
209
+ index += 1;
210
+ continue;
211
+ }
212
+
213
+ if (lines[index].startsWith('*** Add File:')) {
214
+ const next = parseAdd(lines, header.next, mode);
215
+ hunks.push({
216
+ type: 'add',
217
+ path: header.file,
218
+ contents: next.content,
219
+ });
220
+ index = next.next;
221
+ continue;
222
+ }
223
+
224
+ if (lines[index].startsWith('*** Delete File:')) {
225
+ hunks.push({ type: 'delete', path: header.file });
226
+ index = header.next;
227
+ continue;
228
+ }
229
+
230
+ const next = parseChunks(lines, header.next, mode);
231
+ if (mode === 'strict' && next.chunks.length === 0) {
232
+ throw new Error(
233
+ `Invalid patch format: Update File is missing @@ chunk body: ${header.file}`,
234
+ );
235
+ }
236
+
237
+ hunks.push({
238
+ type: 'update',
239
+ path: header.file,
240
+ move_path: header.move,
241
+ chunks: next.chunks,
242
+ });
243
+ index = next.next;
244
+ }
245
+
246
+ return { hunks };
247
+ }
248
+
249
+ export function parsePatch(patchText: string): ParsedPatch {
250
+ return parsePatchInternal(patchText, 'permissive');
251
+ }
252
+
253
+ export function parsePatchStrict(patchText: string): ParsedPatch {
254
+ return parsePatchInternal(patchText, 'strict');
255
+ }
256
+
257
+ function diffMatrix(old_lines: string[], new_lines: string[]): number[][] {
258
+ const dp = Array.from({ length: old_lines.length + 1 }, () =>
259
+ Array<number>(new_lines.length + 1).fill(0),
260
+ );
261
+
262
+ for (let oldIndex = 1; oldIndex <= old_lines.length; oldIndex += 1) {
263
+ for (let newIndex = 1; newIndex <= new_lines.length; newIndex += 1) {
264
+ dp[oldIndex][newIndex] =
265
+ old_lines[oldIndex - 1] === new_lines[newIndex - 1]
266
+ ? dp[oldIndex - 1][newIndex - 1] + 1
267
+ : Math.max(dp[oldIndex - 1][newIndex], dp[oldIndex][newIndex - 1]);
268
+ }
269
+ }
270
+
271
+ return dp;
272
+ }
273
+
274
+ function renderChunk(chunk: PatchChunk): string[] {
275
+ const lines = [chunk.change_context ? `@@ ${chunk.change_context}` : '@@'];
276
+ const dp = diffMatrix(chunk.old_lines, chunk.new_lines);
277
+ const body: string[] = [];
278
+ let oldIndex = chunk.old_lines.length;
279
+ let newIndex = chunk.new_lines.length;
280
+
281
+ while (oldIndex > 0 && newIndex > 0) {
282
+ if (chunk.old_lines[oldIndex - 1] === chunk.new_lines[newIndex - 1]) {
283
+ body.push(` ${chunk.old_lines[oldIndex - 1]}`);
284
+ oldIndex -= 1;
285
+ newIndex -= 1;
286
+ continue;
287
+ }
288
+
289
+ if (dp[oldIndex - 1][newIndex] >= dp[oldIndex][newIndex - 1]) {
290
+ body.push(`-${chunk.old_lines[oldIndex - 1]}`);
291
+ oldIndex -= 1;
292
+ continue;
293
+ }
294
+
295
+ body.push(`+${chunk.new_lines[newIndex - 1]}`);
296
+ newIndex -= 1;
297
+ }
298
+
299
+ while (oldIndex > 0) {
300
+ body.push(`-${chunk.old_lines[oldIndex - 1]}`);
301
+ oldIndex -= 1;
302
+ }
303
+
304
+ while (newIndex > 0) {
305
+ body.push(`+${chunk.new_lines[newIndex - 1]}`);
306
+ newIndex -= 1;
307
+ }
308
+
309
+ lines.push(...body.reverse());
310
+
311
+ if (chunk.is_end_of_file) {
312
+ lines.push('*** End of File');
313
+ }
314
+
315
+ return lines;
316
+ }
317
+
318
+ function renderAddContents(contents: string): string[] {
319
+ if (contents.length === 0) {
320
+ return [];
321
+ }
322
+
323
+ return contents.split('\n').map((line) => `+${line}`);
324
+ }
325
+
326
+ export function formatPatch(patch: ParsedPatch): string {
327
+ const lines = ['*** Begin Patch'];
328
+
329
+ for (const hunk of patch.hunks) {
330
+ if (hunk.type === 'add') {
331
+ lines.push(`*** Add File: ${hunk.path}`);
332
+ lines.push(...renderAddContents(hunk.contents));
333
+ continue;
334
+ }
335
+
336
+ if (hunk.type === 'delete') {
337
+ lines.push(`*** Delete File: ${hunk.path}`);
338
+ continue;
339
+ }
340
+
341
+ lines.push(`*** Update File: ${hunk.path}`);
342
+ if (hunk.move_path) {
343
+ lines.push(`*** Move to: ${hunk.move_path}`);
344
+ }
345
+ for (const chunk of hunk.chunks) {
346
+ lines.push(...renderChunk(chunk));
347
+ }
348
+ }
349
+
350
+ lines.push('*** End Patch');
351
+ return lines.join('\n');
352
+ }
@@ -0,0 +1,117 @@
1
+ import type { ApplyPatchErrorCode, ApplyPatchErrorKind } from './types';
2
+
3
+ const APPLY_PATCH_ERROR_PREFIX: Record<ApplyPatchErrorKind, string> = {
4
+ blocked: 'apply_patch blocked',
5
+ validation: 'apply_patch validation failed',
6
+ verification: 'apply_patch verification failed',
7
+ internal: 'apply_patch internal error',
8
+ };
9
+
10
+ export class ApplyPatchError extends Error {
11
+ override readonly cause?: unknown;
12
+
13
+ constructor(
14
+ readonly kind: ApplyPatchErrorKind,
15
+ readonly code: ApplyPatchErrorCode,
16
+ message: string,
17
+ options?: {
18
+ cause?: unknown;
19
+ },
20
+ ) {
21
+ super(`${APPLY_PATCH_ERROR_PREFIX[kind]}: ${message}`);
22
+ this.name = 'ApplyPatchError';
23
+ this.cause = options?.cause;
24
+ }
25
+ }
26
+
27
+ export function getErrorMessage(error: unknown): string {
28
+ return error instanceof Error ? error.message : String(error);
29
+ }
30
+
31
+ export function createApplyPatchBlockedError(
32
+ message: string,
33
+ cause?: unknown,
34
+ ): ApplyPatchError {
35
+ return new ApplyPatchError('blocked', 'outside_workspace', message, {
36
+ cause,
37
+ });
38
+ }
39
+
40
+ export function createApplyPatchValidationError(
41
+ message: string,
42
+ cause?: unknown,
43
+ ): ApplyPatchError {
44
+ return new ApplyPatchError('validation', 'malformed_patch', message, {
45
+ cause,
46
+ });
47
+ }
48
+
49
+ export function createApplyPatchVerificationError(
50
+ message: string,
51
+ cause?: unknown,
52
+ ): ApplyPatchError {
53
+ return new ApplyPatchError('verification', 'verification_failed', message, {
54
+ cause,
55
+ });
56
+ }
57
+
58
+ export function createApplyPatchInternalError(
59
+ message: string,
60
+ cause?: unknown,
61
+ ): ApplyPatchError {
62
+ return new ApplyPatchError('internal', 'internal_unexpected', message, {
63
+ cause,
64
+ });
65
+ }
66
+
67
+ export function isApplyPatchError(error: unknown): error is ApplyPatchError {
68
+ return error instanceof ApplyPatchError;
69
+ }
70
+
71
+ export function isApplyPatchBlockedError(error: unknown): boolean {
72
+ return isApplyPatchError(error) && error.kind === 'blocked';
73
+ }
74
+
75
+ export function isApplyPatchValidationError(error: unknown): boolean {
76
+ return isApplyPatchError(error) && error.kind === 'validation';
77
+ }
78
+
79
+ export function isApplyPatchVerificationError(error: unknown): boolean {
80
+ return isApplyPatchError(error) && error.kind === 'verification';
81
+ }
82
+
83
+ export function isApplyPatchInternalError(error: unknown): boolean {
84
+ return isApplyPatchError(error) && error.kind === 'internal';
85
+ }
86
+
87
+ export function getApplyPatchErrorDetails(error: unknown):
88
+ | {
89
+ kind: ApplyPatchErrorKind;
90
+ code: ApplyPatchErrorCode;
91
+ message: string;
92
+ }
93
+ | undefined {
94
+ if (!isApplyPatchError(error)) {
95
+ return undefined;
96
+ }
97
+
98
+ return {
99
+ kind: error.kind,
100
+ code: error.code,
101
+ message: error.message,
102
+ };
103
+ }
104
+
105
+ export function ensureApplyPatchError(
106
+ error: unknown,
107
+ context: string,
108
+ ): ApplyPatchError {
109
+ if (isApplyPatchError(error)) {
110
+ return error;
111
+ }
112
+
113
+ return createApplyPatchInternalError(
114
+ `${context}: ${getErrorMessage(error)}`,
115
+ error,
116
+ );
117
+ }