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,496 @@
1
+ import path from 'node:path';
2
+
3
+ import { formatPatch, normalizePatchText } from './codec';
4
+ import {
5
+ createApplyPatchVerificationError,
6
+ ensureApplyPatchError,
7
+ } from './errors';
8
+ import {
9
+ createPatchExecutionContext,
10
+ resolvePreparedUpdate,
11
+ stageAddedText,
12
+ } from './execution-context';
13
+ import { deriveNewContentFromText } from './resolution';
14
+ import type {
15
+ ApplyPatchRuntimeOptions,
16
+ PatchHunk,
17
+ UpdatePatchHunk,
18
+ } from './types';
19
+
20
+ export type RewritePatchResult = {
21
+ patchText: string;
22
+ changed: boolean;
23
+ };
24
+
25
+ type RewriteUpdateGroup = {
26
+ index: number;
27
+ sourcePath: string;
28
+ outputPath: string;
29
+ sourceFilePath: string;
30
+ outputFilePath: string;
31
+ baseText: string;
32
+ finalText: string;
33
+ chunks?: UpdatePatchHunk['chunks'];
34
+ };
35
+
36
+ type RewriteAddGroup = {
37
+ index: number;
38
+ outputPath: string;
39
+ outputFilePath: string;
40
+ finalText: string;
41
+ };
42
+
43
+ type RewriteDependencyGroup =
44
+ | { kind: 'add'; group: RewriteAddGroup }
45
+ | { kind: 'update'; group: RewriteUpdateGroup };
46
+
47
+ function normalizeTextLineEndings(text: string): string {
48
+ return text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
49
+ }
50
+
51
+ function splitPatchTextLines(text: string): string[] {
52
+ const normalized = normalizeTextLineEndings(text);
53
+ const lines = normalized.split('\n');
54
+ if (normalized.endsWith('\n')) {
55
+ lines.pop();
56
+ }
57
+ return lines;
58
+ }
59
+
60
+ function createCollapsedUpdateHunk(
61
+ pathValue: string,
62
+ filePath: string,
63
+ baseText: string,
64
+ finalText: string,
65
+ cfg: ApplyPatchRuntimeOptions,
66
+ movePath?: string,
67
+ ): UpdatePatchHunk {
68
+ const collapsedChunk = {
69
+ old_lines: splitPatchTextLines(baseText),
70
+ new_lines: splitPatchTextLines(finalText),
71
+ change_context: undefined,
72
+ is_end_of_file: true,
73
+ } satisfies UpdatePatchHunk['chunks'][number];
74
+
75
+ const minimizedChunk = minimizeMergedChunk(collapsedChunk);
76
+ const chunk =
77
+ minimizedChunk.old_lines.length === collapsedChunk.old_lines.length &&
78
+ minimizedChunk.new_lines.length === collapsedChunk.new_lines.length &&
79
+ minimizedChunk.change_context === collapsedChunk.change_context &&
80
+ minimizedChunk.is_end_of_file === collapsedChunk.is_end_of_file
81
+ ? collapsedChunk
82
+ : (() => {
83
+ try {
84
+ return deriveNewContentFromText(
85
+ filePath,
86
+ baseText,
87
+ [minimizedChunk],
88
+ cfg,
89
+ ) === finalText
90
+ ? minimizedChunk
91
+ : collapsedChunk;
92
+ } catch {
93
+ // Keep the whole-file chunk when trimming shared context would make
94
+ // the fallback ambiguous or no longer reproduce the same result.
95
+ return collapsedChunk;
96
+ }
97
+ })();
98
+
99
+ return {
100
+ type: 'update',
101
+ path: pathValue,
102
+ move_path: movePath,
103
+ chunks: [chunk],
104
+ };
105
+ }
106
+
107
+ function clonePatchChunks(
108
+ chunks: UpdatePatchHunk['chunks'],
109
+ ): UpdatePatchHunk['chunks'] {
110
+ return chunks.map((chunk) => ({
111
+ old_lines: [...chunk.old_lines],
112
+ new_lines: [...chunk.new_lines],
113
+ change_context: chunk.change_context,
114
+ is_end_of_file: chunk.is_end_of_file,
115
+ }));
116
+ }
117
+
118
+ function minimizeMergedChunk(chunk: UpdatePatchHunk['chunks'][number]) {
119
+ if (chunk.old_lines.length === 0 && chunk.new_lines.length === 0) {
120
+ return {
121
+ old_lines: [],
122
+ new_lines: [],
123
+ change_context: chunk.change_context,
124
+ is_end_of_file: chunk.is_end_of_file,
125
+ };
126
+ }
127
+
128
+ let prefixLength = 0;
129
+ while (
130
+ prefixLength < chunk.old_lines.length &&
131
+ prefixLength < chunk.new_lines.length &&
132
+ chunk.old_lines[prefixLength] === chunk.new_lines[prefixLength]
133
+ ) {
134
+ prefixLength += 1;
135
+ }
136
+
137
+ let suffixLength = 0;
138
+ while (
139
+ chunk.old_lines.length - suffixLength - 1 >= prefixLength &&
140
+ chunk.new_lines.length - suffixLength - 1 >= prefixLength &&
141
+ chunk.old_lines[chunk.old_lines.length - suffixLength - 1] ===
142
+ chunk.new_lines[chunk.new_lines.length - suffixLength - 1]
143
+ ) {
144
+ suffixLength += 1;
145
+ }
146
+
147
+ if (prefixLength === 0 && suffixLength === 0) {
148
+ return {
149
+ old_lines: [...chunk.old_lines],
150
+ new_lines: [...chunk.new_lines],
151
+ change_context: chunk.change_context,
152
+ is_end_of_file: chunk.is_end_of_file,
153
+ };
154
+ }
155
+
156
+ return {
157
+ old_lines: chunk.old_lines.slice(
158
+ prefixLength,
159
+ chunk.old_lines.length - suffixLength,
160
+ ),
161
+ new_lines: chunk.new_lines.slice(
162
+ prefixLength,
163
+ chunk.new_lines.length - suffixLength,
164
+ ),
165
+ change_context:
166
+ prefixLength > 0
167
+ ? chunk.old_lines[prefixLength - 1]
168
+ : chunk.change_context,
169
+ is_end_of_file:
170
+ chunk.is_end_of_file && suffixLength === 0 ? true : undefined,
171
+ };
172
+ }
173
+
174
+ function createUpdateHunk(
175
+ pathValue: string,
176
+ chunks: UpdatePatchHunk['chunks'],
177
+ movePath?: string,
178
+ ): UpdatePatchHunk {
179
+ return {
180
+ type: 'update',
181
+ path: pathValue,
182
+ move_path: movePath,
183
+ chunks: clonePatchChunks(chunks),
184
+ };
185
+ }
186
+
187
+ function mergeSameFileUpdateGroupChunks(
188
+ filePath: string,
189
+ group: RewriteUpdateGroup,
190
+ nextChunks: UpdatePatchHunk['chunks'],
191
+ finalText: string,
192
+ cfg: ApplyPatchRuntimeOptions,
193
+ ): UpdatePatchHunk['chunks'] | undefined {
194
+ if (!group.chunks) {
195
+ return undefined;
196
+ }
197
+
198
+ const mergedChunks = [
199
+ ...clonePatchChunks(group.chunks).map(minimizeMergedChunk),
200
+ ...clonePatchChunks(nextChunks).map(minimizeMergedChunk),
201
+ ];
202
+
203
+ try {
204
+ const mergedText = deriveNewContentFromText(
205
+ filePath,
206
+ group.baseText,
207
+ mergedChunks,
208
+ cfg,
209
+ );
210
+
211
+ return mergedText === finalText ? mergedChunks : undefined;
212
+ } catch {
213
+ return undefined;
214
+ }
215
+ }
216
+
217
+ function addContentsFromFinalText(text: string): string {
218
+ return text.endsWith('\n') ? text.slice(0, -1) : text;
219
+ }
220
+
221
+ function renderRewriteDependencyGroup(
222
+ group: RewriteDependencyGroup,
223
+ cfg: ApplyPatchRuntimeOptions,
224
+ ): PatchHunk {
225
+ if (group.kind === 'add') {
226
+ return {
227
+ type: 'add',
228
+ path: group.group.outputPath,
229
+ contents: addContentsFromFinalText(group.group.finalText),
230
+ };
231
+ }
232
+
233
+ return group.group.chunks
234
+ ? createUpdateHunk(
235
+ group.group.sourcePath,
236
+ group.group.chunks,
237
+ group.group.outputPath !== group.group.sourcePath
238
+ ? group.group.outputPath
239
+ : undefined,
240
+ )
241
+ : createCollapsedUpdateHunk(
242
+ group.group.sourcePath,
243
+ group.group.sourceFilePath,
244
+ group.group.baseText,
245
+ group.group.finalText,
246
+ cfg,
247
+ group.group.outputPath !== group.group.sourcePath
248
+ ? group.group.outputPath
249
+ : undefined,
250
+ );
251
+ }
252
+
253
+ function combineDependentUpdateGroup(
254
+ filePath: string,
255
+ group: RewriteDependencyGroup,
256
+ nextChunks: UpdatePatchHunk['chunks'],
257
+ finalText: string,
258
+ nextOutputPath: string,
259
+ nextOutputFilePath: string,
260
+ cfg: ApplyPatchRuntimeOptions,
261
+ ): RewriteDependencyGroup {
262
+ if (group.kind === 'add') {
263
+ return {
264
+ kind: 'add',
265
+ group: {
266
+ ...group.group,
267
+ outputPath: nextOutputPath,
268
+ outputFilePath: nextOutputFilePath,
269
+ finalText,
270
+ },
271
+ };
272
+ }
273
+
274
+ const mergedChunks =
275
+ group.group.outputFilePath === filePath &&
276
+ group.group.sourceFilePath === filePath &&
277
+ nextOutputFilePath === filePath
278
+ ? mergeSameFileUpdateGroupChunks(
279
+ filePath,
280
+ group.group,
281
+ nextChunks,
282
+ finalText,
283
+ cfg,
284
+ )
285
+ : undefined;
286
+
287
+ return {
288
+ kind: 'update',
289
+ group: {
290
+ ...group.group,
291
+ outputPath: nextOutputPath,
292
+ outputFilePath: nextOutputFilePath,
293
+ finalText,
294
+ chunks: mergedChunks,
295
+ },
296
+ };
297
+ }
298
+
299
+ export async function rewritePatch(
300
+ root: string,
301
+ patchText: string,
302
+ cfg: ApplyPatchRuntimeOptions,
303
+ worktree?: string,
304
+ ): Promise<RewritePatchResult> {
305
+ try {
306
+ const {
307
+ hunks,
308
+ pathsNormalized,
309
+ staged,
310
+ getPreparedFileState,
311
+ assertPreparedPathMissing,
312
+ } = await createPatchExecutionContext(root, patchText, worktree);
313
+ const normalizedPatchText = normalizePatchText(patchText);
314
+ const rewritten: PatchHunk[] = [];
315
+ let changed = false;
316
+
317
+ const dependencyGroups = new Map<string, RewriteDependencyGroup>();
318
+
319
+ function clearDependencyGroup(filePath: string) {
320
+ dependencyGroups.delete(filePath);
321
+ }
322
+
323
+ for (const hunk of hunks) {
324
+ if (hunk.type === 'add') {
325
+ const filePath = path.resolve(root, hunk.path);
326
+ await assertPreparedPathMissing(filePath, 'add');
327
+ rewritten.push(hunk);
328
+ clearDependencyGroup(filePath);
329
+ const finalText = stageAddedText(hunk.contents);
330
+ staged.set(filePath, {
331
+ exists: true,
332
+ text: finalText,
333
+ derived: true,
334
+ });
335
+ dependencyGroups.set(filePath, {
336
+ kind: 'add',
337
+ group: {
338
+ index: rewritten.length - 1,
339
+ outputPath: hunk.path,
340
+ outputFilePath: filePath,
341
+ finalText,
342
+ },
343
+ });
344
+ continue;
345
+ }
346
+
347
+ if (hunk.type === 'delete') {
348
+ const filePath = path.resolve(root, hunk.path);
349
+ await getPreparedFileState(filePath, 'delete');
350
+ clearDependencyGroup(filePath);
351
+ rewritten.push(hunk);
352
+ staged.set(filePath, { exists: false, derived: true });
353
+ continue;
354
+ }
355
+
356
+ const filePath = path.resolve(root, hunk.path);
357
+ const currentDependency = dependencyGroups.get(filePath);
358
+ const current = await getPreparedFileState(filePath, 'update');
359
+ if (!current.exists) {
360
+ throw createApplyPatchVerificationError(
361
+ `Failed to read file to update: ${filePath}`,
362
+ );
363
+ }
364
+
365
+ const movePath = hunk.move_path
366
+ ? path.resolve(root, hunk.move_path)
367
+ : undefined;
368
+ if (movePath && movePath !== filePath) {
369
+ await assertPreparedPathMissing(movePath, 'move');
370
+ }
371
+
372
+ const { resolved, nextText } = resolvePreparedUpdate(
373
+ filePath,
374
+ current.text,
375
+ hunk,
376
+ cfg,
377
+ );
378
+
379
+ const next = resolved.map((chunk, index) => ({
380
+ old_lines: [...chunk.canonical_old_lines],
381
+ new_lines: [...chunk.canonical_new_lines],
382
+ change_context:
383
+ chunk.canonical_change_context ?? hunk.chunks[index].change_context,
384
+ is_end_of_file:
385
+ hunk.chunks[index].is_end_of_file && chunk.resolved_is_end_of_file
386
+ ? true
387
+ : undefined,
388
+ }));
389
+
390
+ for (const chunk of resolved) {
391
+ if (!chunk.rewritten) {
392
+ continue;
393
+ }
394
+ changed = true;
395
+ }
396
+
397
+ const nextOutputPath = hunk.move_path ?? hunk.path;
398
+ const nextOutputFilePath = movePath ?? filePath;
399
+
400
+ if (current.derived && currentDependency) {
401
+ const nextGroup = combineDependentUpdateGroup(
402
+ filePath,
403
+ currentDependency,
404
+ next,
405
+ nextText,
406
+ nextOutputPath,
407
+ nextOutputFilePath,
408
+ cfg,
409
+ );
410
+ rewritten[currentDependency.group.index] = renderRewriteDependencyGroup(
411
+ nextGroup,
412
+ cfg,
413
+ );
414
+ changed = true;
415
+ clearDependencyGroup(filePath);
416
+ if (movePath && movePath !== filePath) {
417
+ clearDependencyGroup(movePath);
418
+ }
419
+ dependencyGroups.set(nextOutputFilePath, nextGroup);
420
+ } else {
421
+ rewritten.push(createUpdateHunk(hunk.path, next, hunk.move_path));
422
+ clearDependencyGroup(filePath);
423
+ if (movePath && movePath !== filePath) {
424
+ clearDependencyGroup(movePath);
425
+ }
426
+ dependencyGroups.set(nextOutputFilePath, {
427
+ kind: 'update',
428
+ group: {
429
+ index: rewritten.length - 1,
430
+ sourcePath: hunk.path,
431
+ outputPath: nextOutputPath,
432
+ sourceFilePath: filePath,
433
+ outputFilePath: nextOutputFilePath,
434
+ baseText: current.text,
435
+ finalText: nextText,
436
+ chunks: clonePatchChunks(next),
437
+ },
438
+ });
439
+ }
440
+
441
+ if (movePath && movePath !== filePath) {
442
+ staged.set(filePath, { exists: false, derived: true });
443
+ staged.set(movePath, {
444
+ exists: true,
445
+ text: nextText,
446
+ mode: current.mode,
447
+ derived: true,
448
+ });
449
+ } else {
450
+ staged.set(filePath, {
451
+ exists: true,
452
+ text: nextText,
453
+ mode: current.mode,
454
+ derived: true,
455
+ });
456
+ }
457
+ }
458
+
459
+ if (!changed) {
460
+ if (pathsNormalized) {
461
+ return {
462
+ patchText: formatPatch({ hunks }),
463
+ changed: true,
464
+ };
465
+ }
466
+
467
+ if (normalizedPatchText !== patchText) {
468
+ return {
469
+ patchText: normalizedPatchText,
470
+ changed: true,
471
+ };
472
+ }
473
+
474
+ return {
475
+ patchText,
476
+ changed: false,
477
+ };
478
+ }
479
+
480
+ return {
481
+ patchText: formatPatch({ hunks: rewritten }),
482
+ changed: true,
483
+ };
484
+ } catch (error) {
485
+ throw ensureApplyPatchError(error, 'Unexpected rewrite failure');
486
+ }
487
+ }
488
+
489
+ export async function rewritePatchText(
490
+ root: string,
491
+ patchText: string,
492
+ cfg: ApplyPatchRuntimeOptions,
493
+ worktree?: string,
494
+ ): Promise<string> {
495
+ return (await rewritePatch(root, patchText, cfg, worktree)).patchText;
496
+ }
@@ -0,0 +1,52 @@
1
+ import { afterEach } from 'bun:test';
2
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ import { applyPreparedChanges, preparePatchChanges } from './operations';
7
+ import type { ApplyPatchRuntimeOptions } from './types';
8
+
9
+ const tempDirs: string[] = [];
10
+
11
+ afterEach(async () => {
12
+ await Promise.all(
13
+ tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })),
14
+ );
15
+ });
16
+
17
+ export const DEFAULT_OPTIONS: ApplyPatchRuntimeOptions = {
18
+ prefixSuffix: true,
19
+ lcsRescue: true,
20
+ };
21
+
22
+ export async function createTempDir(prefix = 'apply-patch-'): Promise<string> {
23
+ const dir = await mkdtemp(path.join(os.tmpdir(), prefix));
24
+ tempDirs.push(dir);
25
+ return dir;
26
+ }
27
+
28
+ export async function writeFixture(
29
+ root: string,
30
+ relativePath: string,
31
+ contents: string,
32
+ ): Promise<void> {
33
+ const target = path.join(root, relativePath);
34
+ await mkdir(path.dirname(target), { recursive: true });
35
+ await writeFile(target, contents, 'utf-8');
36
+ }
37
+
38
+ export async function readText(
39
+ root: string,
40
+ relativePath: string,
41
+ ): Promise<string> {
42
+ return await readFile(path.join(root, relativePath), 'utf-8');
43
+ }
44
+
45
+ export async function applyPatch(
46
+ root: string,
47
+ patchText: string,
48
+ cfg: ApplyPatchRuntimeOptions = DEFAULT_OPTIONS,
49
+ ): Promise<void> {
50
+ const changes = await preparePatchChanges(root, patchText, cfg);
51
+ await applyPreparedChanges(changes);
52
+ }
@@ -0,0 +1,111 @@
1
+ export type ApplyPatchRuntimeOptions = {
2
+ prefixSuffix: boolean;
3
+ lcsRescue: boolean;
4
+ };
5
+
6
+ export type ApplyPatchErrorKind =
7
+ | 'blocked'
8
+ | 'validation'
9
+ | 'verification'
10
+ | 'internal';
11
+
12
+ export type ApplyPatchErrorCode =
13
+ | 'malformed_patch'
14
+ | 'outside_workspace'
15
+ | 'verification_failed'
16
+ | 'internal_unexpected';
17
+
18
+ export type ApplyPatchRescueStrategy = 'prefix/suffix' | 'lcs' | 'anchor';
19
+
20
+ export type MatchComparatorName =
21
+ | 'exact'
22
+ | 'unicode'
23
+ | 'trim-end'
24
+ | 'unicode-trim-end'
25
+ | 'trim'
26
+ | 'unicode-trim';
27
+
28
+ export type PatchChunk = {
29
+ old_lines: string[];
30
+ new_lines: string[];
31
+ change_context?: string;
32
+ is_end_of_file?: boolean;
33
+ };
34
+
35
+ export type AddPatchHunk = {
36
+ type: 'add';
37
+ path: string;
38
+ contents: string;
39
+ };
40
+
41
+ export type DeletePatchHunk = {
42
+ type: 'delete';
43
+ path: string;
44
+ };
45
+
46
+ export type UpdatePatchHunk = {
47
+ type: 'update';
48
+ path: string;
49
+ move_path?: string;
50
+ chunks: PatchChunk[];
51
+ };
52
+
53
+ export type PatchHunk = AddPatchHunk | DeletePatchHunk | UpdatePatchHunk;
54
+
55
+ export type ParsedPatch = {
56
+ hunks: PatchHunk[];
57
+ };
58
+
59
+ export type AddPreparedChange = {
60
+ type: 'add';
61
+ file: string;
62
+ text: string;
63
+ };
64
+
65
+ export type DeletePreparedChange = {
66
+ type: 'delete';
67
+ file: string;
68
+ };
69
+
70
+ export type UpdatePreparedChange = {
71
+ type: 'update';
72
+ file: string;
73
+ move?: string;
74
+ text: string;
75
+ };
76
+
77
+ export type PreparedChange =
78
+ | AddPreparedChange
79
+ | DeletePreparedChange
80
+ | UpdatePreparedChange;
81
+
82
+ export type MatchHit = {
83
+ start: number;
84
+ del: number;
85
+ add: string[];
86
+ };
87
+
88
+ export type SeekHit = {
89
+ index: number;
90
+ comparator: MatchComparatorName;
91
+ exact: boolean;
92
+ };
93
+
94
+ export type ResolvedChunk = {
95
+ hit: MatchHit;
96
+ old_lines: string[];
97
+ canonical_old_lines: string[];
98
+ canonical_new_lines: string[];
99
+ canonical_change_context?: string;
100
+ resolved_is_end_of_file: boolean;
101
+ rewritten: boolean;
102
+ strategy?: ApplyPatchRescueStrategy;
103
+ matchComparator?: MatchComparatorName;
104
+ };
105
+
106
+ export type RescueResult =
107
+ | { kind: 'miss' }
108
+ | { kind: 'ambiguous'; phase: 'prefix_suffix' | 'lcs' }
109
+ | { kind: 'match'; hit: MatchHit };
110
+
111
+ export type LineComparator = (a: string, b: string) => boolean;