@brianli/kimaki 0.4.72-brianli.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (328) hide show
  1. package/bin.js +2 -0
  2. package/dist/ai-tool-to-genai.js +233 -0
  3. package/dist/ai-tool-to-genai.test.js +267 -0
  4. package/dist/ai-tool.js +6 -0
  5. package/dist/bin.js +87 -0
  6. package/dist/bot-token.js +121 -0
  7. package/dist/bot-token.test.js +134 -0
  8. package/dist/channel-management.js +101 -0
  9. package/dist/cli-parsing.test.js +89 -0
  10. package/dist/cli.js +2529 -0
  11. package/dist/commands/abort.js +82 -0
  12. package/dist/commands/action-buttons.js +257 -0
  13. package/dist/commands/add-project.js +114 -0
  14. package/dist/commands/agent.js +291 -0
  15. package/dist/commands/ask-question.js +223 -0
  16. package/dist/commands/compact.js +120 -0
  17. package/dist/commands/context-usage.js +140 -0
  18. package/dist/commands/create-new-project.js +118 -0
  19. package/dist/commands/diff.js +128 -0
  20. package/dist/commands/file-upload.js +275 -0
  21. package/dist/commands/fork.js +217 -0
  22. package/dist/commands/gemini-apikey.js +70 -0
  23. package/dist/commands/login.js +490 -0
  24. package/dist/commands/mention-mode.js +51 -0
  25. package/dist/commands/merge-worktree.js +124 -0
  26. package/dist/commands/model.js +694 -0
  27. package/dist/commands/permissions.js +163 -0
  28. package/dist/commands/queue.js +217 -0
  29. package/dist/commands/remove-project.js +115 -0
  30. package/dist/commands/restart-opencode-server.js +116 -0
  31. package/dist/commands/resume.js +159 -0
  32. package/dist/commands/run-command.js +79 -0
  33. package/dist/commands/session-id.js +78 -0
  34. package/dist/commands/session.js +192 -0
  35. package/dist/commands/share.js +80 -0
  36. package/dist/commands/types.js +2 -0
  37. package/dist/commands/undo-redo.js +159 -0
  38. package/dist/commands/unset-model.js +152 -0
  39. package/dist/commands/upgrade.js +42 -0
  40. package/dist/commands/user-command.js +148 -0
  41. package/dist/commands/verbosity.js +60 -0
  42. package/dist/commands/worktree-settings.js +50 -0
  43. package/dist/commands/worktree.js +299 -0
  44. package/dist/condense-memory.js +33 -0
  45. package/dist/config.js +110 -0
  46. package/dist/database.js +1050 -0
  47. package/dist/db.js +159 -0
  48. package/dist/db.test.js +49 -0
  49. package/dist/discord-api.js +28 -0
  50. package/dist/discord-auth.js +231 -0
  51. package/dist/discord-auth.test.js +80 -0
  52. package/dist/discord-bot.js +997 -0
  53. package/dist/discord-utils.js +560 -0
  54. package/dist/discord-utils.test.js +115 -0
  55. package/dist/errors.js +167 -0
  56. package/dist/escape-backticks.test.js +429 -0
  57. package/dist/format-tables.js +122 -0
  58. package/dist/format-tables.test.js +199 -0
  59. package/dist/forum-sync/config.js +79 -0
  60. package/dist/forum-sync/discord-operations.js +154 -0
  61. package/dist/forum-sync/index.js +5 -0
  62. package/dist/forum-sync/markdown.js +117 -0
  63. package/dist/forum-sync/sync-to-discord.js +417 -0
  64. package/dist/forum-sync/sync-to-files.js +190 -0
  65. package/dist/forum-sync/types.js +53 -0
  66. package/dist/forum-sync/watchers.js +307 -0
  67. package/dist/gateway-consumer.js +232 -0
  68. package/dist/gateway-consumer.test.js +18 -0
  69. package/dist/genai-worker-wrapper.js +111 -0
  70. package/dist/genai-worker.js +311 -0
  71. package/dist/genai.js +232 -0
  72. package/dist/generated/browser.js +17 -0
  73. package/dist/generated/client.js +35 -0
  74. package/dist/generated/commonInputTypes.js +10 -0
  75. package/dist/generated/enums.js +30 -0
  76. package/dist/generated/internal/class.js +41 -0
  77. package/dist/generated/internal/prismaNamespace.js +239 -0
  78. package/dist/generated/internal/prismaNamespaceBrowser.js +209 -0
  79. package/dist/generated/models/bot_api_keys.js +1 -0
  80. package/dist/generated/models/bot_tokens.js +1 -0
  81. package/dist/generated/models/channel_agents.js +1 -0
  82. package/dist/generated/models/channel_directories.js +1 -0
  83. package/dist/generated/models/channel_mention_mode.js +1 -0
  84. package/dist/generated/models/channel_models.js +1 -0
  85. package/dist/generated/models/channel_verbosity.js +1 -0
  86. package/dist/generated/models/channel_worktrees.js +1 -0
  87. package/dist/generated/models/forum_sync_configs.js +1 -0
  88. package/dist/generated/models/global_models.js +1 -0
  89. package/dist/generated/models/ipc_requests.js +1 -0
  90. package/dist/generated/models/part_messages.js +1 -0
  91. package/dist/generated/models/scheduled_tasks.js +1 -0
  92. package/dist/generated/models/session_agents.js +1 -0
  93. package/dist/generated/models/session_models.js +1 -0
  94. package/dist/generated/models/session_start_sources.js +1 -0
  95. package/dist/generated/models/thread_sessions.js +1 -0
  96. package/dist/generated/models/thread_worktrees.js +1 -0
  97. package/dist/generated/models.js +1 -0
  98. package/dist/heap-monitor.js +95 -0
  99. package/dist/hrana-server.js +416 -0
  100. package/dist/hrana-server.test.js +368 -0
  101. package/dist/image-utils.js +112 -0
  102. package/dist/interaction-handler.js +327 -0
  103. package/dist/ipc-polling.js +251 -0
  104. package/dist/kimaki-digital-twin.e2e.test.js +165 -0
  105. package/dist/limit-heading-depth.js +25 -0
  106. package/dist/limit-heading-depth.test.js +105 -0
  107. package/dist/logger.js +160 -0
  108. package/dist/markdown.js +342 -0
  109. package/dist/markdown.test.js +253 -0
  110. package/dist/message-formatting.js +433 -0
  111. package/dist/message-formatting.test.js +73 -0
  112. package/dist/openai-realtime.js +228 -0
  113. package/dist/opencode-plugin-loading.e2e.test.js +91 -0
  114. package/dist/opencode-plugin.js +536 -0
  115. package/dist/opencode-plugin.test.js +98 -0
  116. package/dist/opencode.js +409 -0
  117. package/dist/privacy-sanitizer.js +105 -0
  118. package/dist/runtime-mode.js +51 -0
  119. package/dist/runtime-mode.test.js +115 -0
  120. package/dist/sentry.js +127 -0
  121. package/dist/session-handler/state.js +151 -0
  122. package/dist/session-handler.js +1874 -0
  123. package/dist/session-search.js +100 -0
  124. package/dist/session-search.test.js +40 -0
  125. package/dist/startup-service.js +153 -0
  126. package/dist/system-message.js +499 -0
  127. package/dist/task-runner.js +282 -0
  128. package/dist/task-schedule.js +191 -0
  129. package/dist/task-schedule.test.js +71 -0
  130. package/dist/thinking-utils.js +35 -0
  131. package/dist/thread-message-queue.e2e.test.js +781 -0
  132. package/dist/tools.js +359 -0
  133. package/dist/unnest-code-blocks.js +136 -0
  134. package/dist/unnest-code-blocks.test.js +641 -0
  135. package/dist/upgrade.js +114 -0
  136. package/dist/utils.js +109 -0
  137. package/dist/voice-handler.js +606 -0
  138. package/dist/voice.js +304 -0
  139. package/dist/voice.test.js +187 -0
  140. package/dist/wait-session.js +94 -0
  141. package/dist/worker-types.js +4 -0
  142. package/dist/worktree-utils.js +727 -0
  143. package/dist/xml.js +92 -0
  144. package/dist/xml.test.js +32 -0
  145. package/package.json +82 -0
  146. package/schema.prisma +246 -0
  147. package/skills/batch/SKILL.md +87 -0
  148. package/skills/critique/SKILL.md +129 -0
  149. package/skills/errore/SKILL.md +589 -0
  150. package/skills/goke/.prettierrc +5 -0
  151. package/skills/goke/CHANGELOG.md +40 -0
  152. package/skills/goke/LICENSE +21 -0
  153. package/skills/goke/README.md +666 -0
  154. package/skills/goke/SKILL.md +458 -0
  155. package/skills/goke/package.json +43 -0
  156. package/skills/goke/src/__test__/coerce.test.ts +411 -0
  157. package/skills/goke/src/__test__/index.test.ts +1798 -0
  158. package/skills/goke/src/__test__/types.test-d.ts +111 -0
  159. package/skills/goke/src/coerce.ts +547 -0
  160. package/skills/goke/src/goke.ts +1362 -0
  161. package/skills/goke/src/index.ts +16 -0
  162. package/skills/goke/src/mri.ts +164 -0
  163. package/skills/goke/tsconfig.json +15 -0
  164. package/skills/jitter/EDITOR.md +219 -0
  165. package/skills/jitter/EXPORT-INTERNALS.md +309 -0
  166. package/skills/jitter/SKILL.md +158 -0
  167. package/skills/jitter/jitter-clipboard.json +1042 -0
  168. package/skills/jitter/package.json +14 -0
  169. package/skills/jitter/tsconfig.json +15 -0
  170. package/skills/jitter/utils/actions.ts +212 -0
  171. package/skills/jitter/utils/export.ts +114 -0
  172. package/skills/jitter/utils/index.ts +141 -0
  173. package/skills/jitter/utils/snapshot.ts +154 -0
  174. package/skills/jitter/utils/traverse.ts +246 -0
  175. package/skills/jitter/utils/types.ts +279 -0
  176. package/skills/jitter/utils/wait.ts +133 -0
  177. package/skills/playwriter/SKILL.md +31 -0
  178. package/skills/security-review/SKILL.md +208 -0
  179. package/skills/simplify/SKILL.md +58 -0
  180. package/skills/termcast/SKILL.md +945 -0
  181. package/skills/tuistory/SKILL.md +250 -0
  182. package/skills/zustand-centralized-state/SKILL.md +582 -0
  183. package/src/__snapshots__/compact-session-context-no-system.md +35 -0
  184. package/src/__snapshots__/compact-session-context.md +41 -0
  185. package/src/__snapshots__/first-session-no-info.md +17 -0
  186. package/src/__snapshots__/first-session-with-info.md +23 -0
  187. package/src/__snapshots__/session-1.md +17 -0
  188. package/src/__snapshots__/session-2.md +5871 -0
  189. package/src/__snapshots__/session-3.md +17 -0
  190. package/src/__snapshots__/session-with-tools.md +5871 -0
  191. package/src/ai-tool-to-genai.test.ts +296 -0
  192. package/src/ai-tool-to-genai.ts +282 -0
  193. package/src/ai-tool.ts +39 -0
  194. package/src/bin.ts +108 -0
  195. package/src/bot-token.test.ts +171 -0
  196. package/src/bot-token.ts +159 -0
  197. package/src/channel-management.ts +172 -0
  198. package/src/cli-parsing.test.ts +132 -0
  199. package/src/cli.ts +3605 -0
  200. package/src/commands/abort.ts +112 -0
  201. package/src/commands/action-buttons.ts +376 -0
  202. package/src/commands/add-project.ts +152 -0
  203. package/src/commands/agent.ts +404 -0
  204. package/src/commands/ask-question.ts +330 -0
  205. package/src/commands/compact.ts +157 -0
  206. package/src/commands/context-usage.ts +199 -0
  207. package/src/commands/create-new-project.ts +179 -0
  208. package/src/commands/diff.ts +165 -0
  209. package/src/commands/file-upload.ts +389 -0
  210. package/src/commands/fork.ts +320 -0
  211. package/src/commands/gemini-apikey.ts +104 -0
  212. package/src/commands/login.ts +634 -0
  213. package/src/commands/mention-mode.ts +77 -0
  214. package/src/commands/merge-worktree.ts +177 -0
  215. package/src/commands/model.ts +961 -0
  216. package/src/commands/permissions.ts +261 -0
  217. package/src/commands/queue.ts +296 -0
  218. package/src/commands/remove-project.ts +155 -0
  219. package/src/commands/restart-opencode-server.ts +162 -0
  220. package/src/commands/resume.ts +242 -0
  221. package/src/commands/run-command.ts +123 -0
  222. package/src/commands/session-id.ts +109 -0
  223. package/src/commands/session.ts +250 -0
  224. package/src/commands/share.ts +106 -0
  225. package/src/commands/types.ts +25 -0
  226. package/src/commands/undo-redo.ts +221 -0
  227. package/src/commands/unset-model.ts +189 -0
  228. package/src/commands/upgrade.ts +52 -0
  229. package/src/commands/user-command.ts +193 -0
  230. package/src/commands/verbosity.ts +88 -0
  231. package/src/commands/worktree-settings.ts +79 -0
  232. package/src/commands/worktree.ts +431 -0
  233. package/src/condense-memory.ts +36 -0
  234. package/src/config.ts +148 -0
  235. package/src/database.ts +1530 -0
  236. package/src/db.test.ts +60 -0
  237. package/src/db.ts +190 -0
  238. package/src/discord-api.ts +35 -0
  239. package/src/discord-bot.ts +1316 -0
  240. package/src/discord-utils.test.ts +132 -0
  241. package/src/discord-utils.ts +767 -0
  242. package/src/errors.ts +213 -0
  243. package/src/escape-backticks.test.ts +469 -0
  244. package/src/format-tables.test.ts +223 -0
  245. package/src/format-tables.ts +145 -0
  246. package/src/forum-sync/config.ts +92 -0
  247. package/src/forum-sync/discord-operations.ts +241 -0
  248. package/src/forum-sync/index.ts +9 -0
  249. package/src/forum-sync/markdown.ts +176 -0
  250. package/src/forum-sync/sync-to-discord.ts +595 -0
  251. package/src/forum-sync/sync-to-files.ts +294 -0
  252. package/src/forum-sync/types.ts +175 -0
  253. package/src/forum-sync/watchers.ts +454 -0
  254. package/src/genai-worker-wrapper.ts +164 -0
  255. package/src/genai-worker.ts +386 -0
  256. package/src/genai.ts +321 -0
  257. package/src/generated/browser.ts +109 -0
  258. package/src/generated/client.ts +131 -0
  259. package/src/generated/commonInputTypes.ts +512 -0
  260. package/src/generated/enums.ts +46 -0
  261. package/src/generated/internal/class.ts +362 -0
  262. package/src/generated/internal/prismaNamespace.ts +2251 -0
  263. package/src/generated/internal/prismaNamespaceBrowser.ts +308 -0
  264. package/src/generated/models/bot_api_keys.ts +1288 -0
  265. package/src/generated/models/bot_tokens.ts +1577 -0
  266. package/src/generated/models/channel_agents.ts +1256 -0
  267. package/src/generated/models/channel_directories.ts +2104 -0
  268. package/src/generated/models/channel_mention_mode.ts +1300 -0
  269. package/src/generated/models/channel_models.ts +1288 -0
  270. package/src/generated/models/channel_verbosity.ts +1224 -0
  271. package/src/generated/models/channel_worktrees.ts +1308 -0
  272. package/src/generated/models/forum_sync_configs.ts +1452 -0
  273. package/src/generated/models/global_models.ts +1288 -0
  274. package/src/generated/models/ipc_requests.ts +1485 -0
  275. package/src/generated/models/part_messages.ts +1302 -0
  276. package/src/generated/models/scheduled_tasks.ts +2320 -0
  277. package/src/generated/models/session_agents.ts +1086 -0
  278. package/src/generated/models/session_models.ts +1114 -0
  279. package/src/generated/models/session_start_sources.ts +1408 -0
  280. package/src/generated/models/thread_sessions.ts +1599 -0
  281. package/src/generated/models/thread_worktrees.ts +1352 -0
  282. package/src/generated/models.ts +29 -0
  283. package/src/heap-monitor.ts +121 -0
  284. package/src/hrana-server.test.ts +428 -0
  285. package/src/hrana-server.ts +547 -0
  286. package/src/image-utils.ts +149 -0
  287. package/src/interaction-handler.ts +461 -0
  288. package/src/ipc-polling.ts +325 -0
  289. package/src/kimaki-digital-twin.e2e.test.ts +201 -0
  290. package/src/limit-heading-depth.test.ts +116 -0
  291. package/src/limit-heading-depth.ts +26 -0
  292. package/src/logger.ts +203 -0
  293. package/src/markdown.test.ts +360 -0
  294. package/src/markdown.ts +410 -0
  295. package/src/message-formatting.test.ts +81 -0
  296. package/src/message-formatting.ts +549 -0
  297. package/src/openai-realtime.ts +362 -0
  298. package/src/opencode-plugin-loading.e2e.test.ts +112 -0
  299. package/src/opencode-plugin.test.ts +108 -0
  300. package/src/opencode-plugin.ts +652 -0
  301. package/src/opencode.ts +554 -0
  302. package/src/privacy-sanitizer.ts +142 -0
  303. package/src/schema.sql +158 -0
  304. package/src/sentry.ts +137 -0
  305. package/src/session-handler/state.ts +232 -0
  306. package/src/session-handler.ts +2668 -0
  307. package/src/session-search.test.ts +50 -0
  308. package/src/session-search.ts +148 -0
  309. package/src/startup-service.ts +200 -0
  310. package/src/system-message.ts +568 -0
  311. package/src/task-runner.ts +425 -0
  312. package/src/task-schedule.test.ts +84 -0
  313. package/src/task-schedule.ts +287 -0
  314. package/src/thinking-utils.ts +61 -0
  315. package/src/thread-message-queue.e2e.test.ts +997 -0
  316. package/src/tools.ts +432 -0
  317. package/src/unnest-code-blocks.test.ts +679 -0
  318. package/src/unnest-code-blocks.ts +168 -0
  319. package/src/upgrade.ts +127 -0
  320. package/src/utils.ts +145 -0
  321. package/src/voice-handler.ts +852 -0
  322. package/src/voice.test.ts +219 -0
  323. package/src/voice.ts +444 -0
  324. package/src/wait-session.ts +147 -0
  325. package/src/worker-types.ts +64 -0
  326. package/src/worktree-utils.ts +988 -0
  327. package/src/xml.test.ts +38 -0
  328. package/src/xml.ts +121 -0
@@ -0,0 +1,988 @@
1
+ // Worktree utility functions.
2
+ // Wrapper for git worktree creation that initializes and validates submodules.
3
+ // Also handles capturing and applying git diffs when creating worktrees from threads.
4
+
5
+ import crypto from 'node:crypto'
6
+ import { exec, spawn } from 'node:child_process'
7
+ import fs from 'node:fs'
8
+ import os from 'node:os'
9
+ import path from 'node:path'
10
+ import { promisify } from 'node:util'
11
+ import { createLogger, LogPrefix } from './logger.js'
12
+
13
+ const DEFAULT_EXEC_TIMEOUT_MS = 10_000
14
+ const SUBMODULE_INIT_TIMEOUT_MS = 20 * 60_000
15
+
16
+ const _execAsync = promisify(exec)
17
+
18
+ // Wraps child_process.exec with a default 10s timeout via Promise.race.
19
+ // Callers can override with a longer timeout in the options.
20
+ export function execAsync(
21
+ command: string,
22
+ options?: Parameters<typeof _execAsync>[1],
23
+ ): Promise<{ stdout: string; stderr: string }> {
24
+ const timeoutMs =
25
+ (options as { timeout?: number })?.timeout || DEFAULT_EXEC_TIMEOUT_MS
26
+ const execPromise = _execAsync(command, options) as Promise<{
27
+ stdout: string
28
+ stderr: string
29
+ }> & { child?: import('node:child_process').ChildProcess }
30
+ const timeoutPromise = new Promise<never>((_, reject) => {
31
+ setTimeout(() => {
32
+ execPromise.child?.kill()
33
+ reject(new Error(`Command timed out after ${timeoutMs}ms: ${command}`))
34
+ }, timeoutMs)
35
+ })
36
+ return Promise.race([execPromise, timeoutPromise])
37
+ }
38
+
39
+ const logger = createLogger(LogPrefix.WORKTREE)
40
+
41
+ const LOCKFILE_TO_INSTALL_COMMAND: Array<[string, string]> = [
42
+ ['pnpm-lock.yaml', 'pnpm install'],
43
+ ['bun.lock', 'bun install'],
44
+ ['bun.lockb', 'bun install'],
45
+ ['yarn.lock', 'yarn install'],
46
+ ['package-lock.json', 'npm install'],
47
+ ]
48
+
49
+ function detectInstallCommand(directory: string): string | null {
50
+ for (const [lockfile, command] of LOCKFILE_TO_INSTALL_COMMAND) {
51
+ if (fs.existsSync(path.join(directory, lockfile))) {
52
+ return command
53
+ }
54
+ }
55
+ return null
56
+ }
57
+
58
+ type CommandError = Error & {
59
+ cmd?: string
60
+ stderr?: string
61
+ stdout?: string
62
+ signal?: NodeJS.Signals
63
+ killed?: boolean
64
+ }
65
+
66
+ function formatCommandError(error: unknown): string {
67
+ if (!(error instanceof Error)) {
68
+ return String(error)
69
+ }
70
+
71
+ const commandError = error as CommandError
72
+ const details: string[] = [commandError.message]
73
+
74
+ if (commandError.cmd) {
75
+ details.push(`cmd=${commandError.cmd}`)
76
+ }
77
+ if (commandError.signal) {
78
+ details.push(`signal=${commandError.signal}`)
79
+ }
80
+ if (commandError.killed) {
81
+ details.push('process=killed')
82
+ }
83
+ if (commandError.stderr?.trim()) {
84
+ details.push(`stderr=${commandError.stderr.trim()}`)
85
+ }
86
+ if (commandError.stdout?.trim()) {
87
+ details.push(`stdout=${commandError.stdout.trim()}`)
88
+ }
89
+
90
+ return details.join(' | ')
91
+ }
92
+
93
+ /**
94
+ * Get submodule paths from .gitmodules file.
95
+ * Returns empty array if no submodules or on error.
96
+ */
97
+ async function getSubmodulePaths(directory: string): Promise<string[]> {
98
+ try {
99
+ const result = await execAsync(
100
+ 'git config --file .gitmodules --get-regexp path',
101
+ {
102
+ cwd: directory,
103
+ },
104
+ )
105
+ // Output format: "submodule.name.path value"
106
+ return result.stdout
107
+ .trim()
108
+ .split('\n')
109
+ .filter(Boolean)
110
+ .map((line) => {
111
+ return line.split(' ')[1]
112
+ })
113
+ .filter((p): p is string => {
114
+ return Boolean(p)
115
+ })
116
+ } catch {
117
+ return [] // No .gitmodules or no submodules
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Remove broken submodule stubs created by git worktree.
123
+ * When git worktree add runs on a repo with submodules, it creates submodule
124
+ * directories with .git files pointing to ../.git/worktrees/<name>/modules/<submodule>
125
+ * but that path only has a config file, missing HEAD/objects/refs.
126
+ * This causes git commands to fail with "fatal: not a git repository".
127
+ */
128
+ async function removeBrokenSubmoduleStubs(directory: string): Promise<void> {
129
+ const submodulePaths = await getSubmodulePaths(directory)
130
+
131
+ for (const subPath of submodulePaths) {
132
+ const fullPath = path.join(directory, subPath)
133
+ const gitFile = path.join(fullPath, '.git')
134
+
135
+ try {
136
+ const stat = await fs.promises.stat(gitFile)
137
+ if (!stat.isFile()) {
138
+ continue
139
+ }
140
+
141
+ // Read .git file to get gitdir path
142
+ const content = await fs.promises.readFile(gitFile, 'utf-8')
143
+ const match = content.match(/^gitdir:\s*(.+)$/m)
144
+ if (!match || !match[1]) {
145
+ continue
146
+ }
147
+
148
+ const gitdir = path.resolve(fullPath, match[1].trim())
149
+ const headFile = path.join(gitdir, 'HEAD')
150
+
151
+ // If HEAD doesn't exist, this is a broken stub
152
+ const headExists = await fs.promises
153
+ .access(headFile)
154
+ .then(() => {
155
+ return true
156
+ })
157
+ .catch(() => {
158
+ return false
159
+ })
160
+
161
+ if (!headExists) {
162
+ logger.log(`Removing broken submodule stub: ${subPath}`)
163
+ await fs.promises.rm(fullPath, { recursive: true, force: true })
164
+ }
165
+ } catch {
166
+ // Directory doesn't exist or other error, skip
167
+ }
168
+ }
169
+ }
170
+
171
+ function parseSubmoduleGitdir(gitFileContent: string): string | Error {
172
+ const match = gitFileContent.match(/^gitdir:\s*(.+)$/m)
173
+ const gitdir = match?.[1]?.trim()
174
+ if (!gitdir) {
175
+ return new Error('Missing gitdir pointer')
176
+ }
177
+ return gitdir
178
+ }
179
+
180
+ async function validateSubmodulePointers(
181
+ directory: string,
182
+ ): Promise<void | Error> {
183
+ const submodulePaths = await getSubmodulePaths(directory)
184
+ if (submodulePaths.length === 0) {
185
+ return
186
+ }
187
+
188
+ const validationIssues: string[] = []
189
+
190
+ await Promise.all(
191
+ submodulePaths.map(async (submodulePath) => {
192
+ const submoduleDir = path.join(directory, submodulePath)
193
+ const submoduleGitFile = path.join(submoduleDir, '.git')
194
+
195
+ const gitFileExists = await fs.promises
196
+ .access(submoduleGitFile)
197
+ .then(() => {
198
+ return true
199
+ })
200
+ .catch(() => {
201
+ return false
202
+ })
203
+ if (!gitFileExists) {
204
+ validationIssues.push(`${submodulePath}: missing .git file`)
205
+ return
206
+ }
207
+
208
+ const gitFileContentResult = await errore.tryAsync({
209
+ try: () => fs.promises.readFile(submoduleGitFile, 'utf-8'),
210
+ catch: (e) =>
211
+ new Error(`Failed to read .git for ${submodulePath}`, { cause: e }),
212
+ })
213
+ if (gitFileContentResult instanceof Error) {
214
+ validationIssues.push(
215
+ `${submodulePath}: ${gitFileContentResult.message}`,
216
+ )
217
+ return
218
+ }
219
+
220
+ const parsedGitdir = parseSubmoduleGitdir(gitFileContentResult)
221
+ if (parsedGitdir instanceof Error) {
222
+ validationIssues.push(`${submodulePath}: ${parsedGitdir.message}`)
223
+ return
224
+ }
225
+
226
+ const resolvedGitdir = path.resolve(submoduleDir, parsedGitdir)
227
+ const headPath = path.join(resolvedGitdir, 'HEAD')
228
+ const headExists = await fs.promises
229
+ .access(headPath)
230
+ .then(() => {
231
+ return true
232
+ })
233
+ .catch(() => {
234
+ return false
235
+ })
236
+ if (!headExists) {
237
+ validationIssues.push(
238
+ `${submodulePath}: gitdir missing HEAD (${resolvedGitdir})`,
239
+ )
240
+ }
241
+ }),
242
+ )
243
+
244
+ const submoduleStatusResult = await errore.tryAsync({
245
+ try: () =>
246
+ execAsync('git submodule status --recursive', {
247
+ cwd: directory,
248
+ timeout: SUBMODULE_INIT_TIMEOUT_MS,
249
+ }),
250
+ catch: (e) =>
251
+ new Error('git submodule status --recursive failed', { cause: e }),
252
+ })
253
+ if (submoduleStatusResult instanceof Error) {
254
+ validationIssues.push(submoduleStatusResult.message)
255
+ }
256
+
257
+ if (validationIssues.length === 0) {
258
+ return
259
+ }
260
+
261
+ return new Error(
262
+ `Submodule validation failed: ${validationIssues.join('; ')}`,
263
+ )
264
+ }
265
+
266
+ type WorktreeResult = {
267
+ directory: string
268
+ branch: string
269
+ }
270
+
271
+ async function resolveDefaultWorktreeTarget(
272
+ directory: string,
273
+ ): Promise<string> {
274
+ const remoteHead = await execAsync(
275
+ 'git symbolic-ref refs/remotes/origin/HEAD',
276
+ {
277
+ cwd: directory,
278
+ },
279
+ ).catch(() => {
280
+ return null
281
+ })
282
+
283
+ const remoteRef = remoteHead?.stdout.trim()
284
+ if (remoteRef?.startsWith('refs/remotes/')) {
285
+ return remoteRef.replace('refs/remotes/', '')
286
+ }
287
+
288
+ const hasMain = await execAsync(
289
+ 'git show-ref --verify --quiet refs/heads/main',
290
+ {
291
+ cwd: directory,
292
+ },
293
+ )
294
+ .then(() => {
295
+ return true
296
+ })
297
+ .catch(() => {
298
+ return false
299
+ })
300
+ if (hasMain) {
301
+ return 'main'
302
+ }
303
+
304
+ const hasMaster = await execAsync(
305
+ 'git show-ref --verify --quiet refs/heads/master',
306
+ {
307
+ cwd: directory,
308
+ },
309
+ )
310
+ .then(() => {
311
+ return true
312
+ })
313
+ .catch(() => {
314
+ return false
315
+ })
316
+ if (hasMaster) {
317
+ return 'master'
318
+ }
319
+
320
+ return 'HEAD'
321
+ }
322
+
323
+ function getManagedWorktreeDirectory({
324
+ directory,
325
+ name,
326
+ }: {
327
+ directory: string
328
+ name: string
329
+ }): string {
330
+ const projectHash = crypto.createHash('sha1').update(directory).digest('hex')
331
+ const safeName = name.replaceAll('/', '-')
332
+ return path.join(
333
+ os.homedir(),
334
+ '.local',
335
+ 'share',
336
+ 'opencode',
337
+ 'worktree',
338
+ projectHash,
339
+ safeName,
340
+ )
341
+ }
342
+
343
+ /**
344
+ * Create a worktree using git and initialize git submodules.
345
+ * This wrapper ensures submodules are properly set up in new worktrees.
346
+ *
347
+ * If diff is provided, it's applied BEFORE submodule update to ensure
348
+ * any submodule pointer changes in the diff are respected.
349
+ */
350
+ export async function createWorktreeWithSubmodules({
351
+ directory,
352
+ name,
353
+ diff,
354
+ }: {
355
+ directory: string
356
+ name: string
357
+ diff?: CapturedDiff | null
358
+ }): Promise<(WorktreeResult & { diffApplied: boolean }) | Error> {
359
+ // 1. Create worktree via git (checked out immediately).
360
+ const worktreeDir = getManagedWorktreeDirectory({ directory, name })
361
+ const targetRef = await resolveDefaultWorktreeTarget(directory)
362
+
363
+ if (fs.existsSync(worktreeDir)) {
364
+ return new Error(`Worktree directory already exists: ${worktreeDir}`)
365
+ }
366
+
367
+ await fs.promises.mkdir(path.dirname(worktreeDir), { recursive: true })
368
+
369
+ const createCommand = `git worktree add ${JSON.stringify(worktreeDir)} -B ${JSON.stringify(name)} ${JSON.stringify(targetRef)}`
370
+ const createResult = await errore.tryAsync({
371
+ try: () =>
372
+ execAsync(createCommand, {
373
+ cwd: directory,
374
+ timeout: SUBMODULE_INIT_TIMEOUT_MS,
375
+ }),
376
+ catch: (e) =>
377
+ new Error(`git worktree add failed: ${formatCommandError(e)}`, {
378
+ cause: e,
379
+ }),
380
+ })
381
+ if (createResult instanceof Error) {
382
+ return createResult
383
+ }
384
+
385
+ let diffApplied = false
386
+
387
+ // 2. Apply diff BEFORE submodule update (if provided)
388
+ // This ensures any submodule pointer changes in the diff are applied first,
389
+ // so submodule update checks out the correct commits.
390
+ if (diff) {
391
+ logger.log(`Applying diff to ${worktreeDir} before submodule init`)
392
+ diffApplied = await applyGitDiff(worktreeDir, diff)
393
+ }
394
+
395
+ // 3. Remove broken submodule stubs before init
396
+ // git worktree creates stub directories with .git files pointing to incomplete gitdirs
397
+ await removeBrokenSubmoduleStubs(worktreeDir)
398
+
399
+ // 4. Init submodules in new worktree
400
+ // Uses --init to initialize, --recursive for nested submodules.
401
+ // Submodules will be checked out at the commit specified by the (possibly updated) index.
402
+ try {
403
+ logger.log(
404
+ `Initializing submodules in ${worktreeDir} (timeout=${SUBMODULE_INIT_TIMEOUT_MS}ms)`,
405
+ )
406
+ await execAsync('git submodule update --init --recursive', {
407
+ cwd: worktreeDir,
408
+ timeout: SUBMODULE_INIT_TIMEOUT_MS,
409
+ })
410
+ logger.log(`Submodules initialized in ${worktreeDir}`)
411
+ } catch (e) {
412
+ const errorMessage = formatCommandError(e)
413
+ logger.error('Submodule initialization failed', {
414
+ worktreeDir,
415
+ timeoutMs: SUBMODULE_INIT_TIMEOUT_MS,
416
+ command: 'git submodule update --init --recursive',
417
+ error: errorMessage,
418
+ })
419
+ return new Error(`Submodule initialization failed: ${errorMessage}`)
420
+ }
421
+
422
+ // 4.5 Validate submodule pointers and git metadata before marking ready.
423
+ const submoduleValidationError = await validateSubmodulePointers(worktreeDir)
424
+ if (submoduleValidationError instanceof Error) {
425
+ logger.error('Submodule validation failed after init', {
426
+ worktreeDir,
427
+ error: submoduleValidationError.message,
428
+ })
429
+ return submoduleValidationError
430
+ }
431
+
432
+ // 5. Dependency install disabled.
433
+ // `npx -y ni` resolved to the wrong npm package `ni` (browser-launcher), not `@antfu/ni`.
434
+ // detectInstallCommand() was built as a replacement but install is skipped for now.
435
+ // Opencode sessions can run install themselves if needed.
436
+
437
+ return { directory: worktreeDir, branch: name, diffApplied }
438
+ }
439
+
440
+ /**
441
+ * Captured git diff (both staged and unstaged changes).
442
+ */
443
+ export type CapturedDiff = {
444
+ unstaged: string
445
+ staged: string
446
+ }
447
+
448
+ /**
449
+ * Capture git diff from a directory (both staged and unstaged changes).
450
+ * Returns null if no changes or on error.
451
+ */
452
+ export async function captureGitDiff(
453
+ directory: string,
454
+ ): Promise<CapturedDiff | null> {
455
+ try {
456
+ // Capture unstaged changes
457
+ const unstagedResult = await execAsync('git diff', { cwd: directory })
458
+ const unstaged = unstagedResult.stdout.trim()
459
+
460
+ // Capture staged changes
461
+ const stagedResult = await execAsync('git diff --staged', {
462
+ cwd: directory,
463
+ })
464
+ const staged = stagedResult.stdout.trim()
465
+
466
+ if (!unstaged && !staged) {
467
+ return null
468
+ }
469
+
470
+ return { unstaged, staged }
471
+ } catch (e) {
472
+ logger.warn(
473
+ `Failed to capture git diff from ${directory}: ${e instanceof Error ? e.message : String(e)}`,
474
+ )
475
+ return null
476
+ }
477
+ }
478
+
479
+ /**
480
+ * Run a git command with stdin input.
481
+ * Uses spawn to pipe the diff content to git apply.
482
+ */
483
+ function runGitWithStdin(
484
+ args: string[],
485
+ cwd: string,
486
+ input: string,
487
+ ): Promise<void> {
488
+ return new Promise((resolve, reject) => {
489
+ const child = spawn('git', args, { cwd, stdio: ['pipe', 'pipe', 'pipe'] })
490
+
491
+ let stderr = ''
492
+ child.stderr?.on('data', (data) => {
493
+ stderr += data.toString()
494
+ })
495
+
496
+ child.on('close', (code) => {
497
+ if (code === 0) {
498
+ resolve()
499
+ } else {
500
+ reject(
501
+ new Error(stderr || `git ${args.join(' ')} failed with code ${code}`),
502
+ )
503
+ }
504
+ })
505
+
506
+ child.on('error', reject)
507
+
508
+ child.stdin?.write(input)
509
+ child.stdin?.end()
510
+ })
511
+ }
512
+
513
+ /**
514
+ * Apply a captured git diff to a directory.
515
+ * Applies staged changes first, then unstaged.
516
+ */
517
+ export async function applyGitDiff(
518
+ directory: string,
519
+ diff: CapturedDiff,
520
+ ): Promise<boolean> {
521
+ try {
522
+ // Apply staged changes first (and stage them)
523
+ if (diff.staged) {
524
+ logger.log(`Applying staged diff to ${directory}`)
525
+ await runGitWithStdin(['apply', '--index'], directory, diff.staged)
526
+ }
527
+
528
+ // Apply unstaged changes (don't stage them)
529
+ if (diff.unstaged) {
530
+ logger.log(`Applying unstaged diff to ${directory}`)
531
+ await runGitWithStdin(['apply'], directory, diff.unstaged)
532
+ }
533
+
534
+ logger.log(`Successfully applied diff to ${directory}`)
535
+ return true
536
+ } catch (e) {
537
+ logger.warn(
538
+ `Failed to apply git diff to ${directory}: ${e instanceof Error ? e.message : String(e)}`,
539
+ )
540
+ return false
541
+ }
542
+ }
543
+
544
+ // ─── Worktree merge ──────────────────────────────────────────────────────────
545
+ // Implements a worktrunk-style merge pipeline:
546
+ // 1. Reject if uncommitted changes exist
547
+ // 2. Squash all commits since merge-base into one
548
+ // 3. Rebase onto target (default branch)
549
+ // 4. Fast-forward push to target via local git push
550
+ // 5. Switch to detached HEAD, delete branch
551
+ //
552
+ // Uses `git push <git-common-dir> HEAD:<target>` with
553
+ // `receive.denyCurrentBranch=updateInstead` to fast-forward the target
554
+ // WITHOUT checking it out in the main repo.
555
+ //
556
+ // Returns MergeWorktreeErrors | MergeSuccess. All errors are tagged via errore.
557
+ // - DirtyWorktreeError → git untouched
558
+ // - NothingToMergeError → git untouched
559
+ // - SquashError → HEAD may be at merge-base with staged changes
560
+ // - RebaseConflictError → git left mid-rebase for AI/user resolution
561
+ // - RebaseError → rebase not in progress; temp branch cleaned
562
+ // - NotFastForwardError → source intact; no push
563
+ // - ConflictingFilesError → no push; lists overlapping files
564
+ // - PushError → source rebased but target unchanged
565
+ // - GitCommandError → catch-all for unexpected git failures
566
+
567
+ import * as errore from 'errore'
568
+ import {
569
+ DirtyWorktreeError,
570
+ NothingToMergeError,
571
+ SquashError,
572
+ RebaseConflictError,
573
+ RebaseError,
574
+ NotFastForwardError,
575
+ ConflictingFilesError,
576
+ PushError,
577
+ GitCommandError,
578
+ type MergeWorktreeErrors,
579
+ } from './errors.js'
580
+
581
+ export type MergeSuccess = {
582
+ defaultBranch: string
583
+ branchName: string
584
+ commitCount: number
585
+ shortSha: string
586
+ }
587
+
588
+ async function git(
589
+ dir: string,
590
+ args: string,
591
+ opts?: { timeout?: number },
592
+ ): Promise<GitCommandError | string> {
593
+ const result = await errore.tryAsync({
594
+ try: () =>
595
+ execAsync(
596
+ `git -C "${dir}" ${args}`,
597
+ opts ? { timeout: opts.timeout } : undefined,
598
+ ),
599
+ catch: (e) => new GitCommandError({ command: args, cause: e }),
600
+ })
601
+ if (result instanceof Error) {
602
+ return result
603
+ }
604
+ return result.stdout.trim()
605
+ }
606
+
607
+ async function getDefaultBranch(repoDir: string): Promise<string> {
608
+ const ref = await git(repoDir, 'symbolic-ref refs/remotes/origin/HEAD')
609
+ if (ref instanceof Error) {
610
+ return 'main'
611
+ }
612
+ return ref.replace(/^refs\/remotes\/origin\//, '') || 'main'
613
+ }
614
+
615
+ async function isDirty(dir: string): Promise<boolean> {
616
+ const status = await git(dir, 'status --porcelain')
617
+ if (status instanceof Error) {
618
+ return false
619
+ }
620
+ return status.length > 0
621
+ }
622
+
623
+ async function getGitCommonDir(dir: string): Promise<GitCommandError | string> {
624
+ const commonDir = await git(dir, 'rev-parse --git-common-dir')
625
+ if (commonDir instanceof Error) {
626
+ return commonDir
627
+ }
628
+ if (path.isAbsolute(commonDir)) {
629
+ return commonDir
630
+ }
631
+ return path.resolve(dir, commonDir)
632
+ }
633
+
634
+ async function isAncestor(
635
+ dir: string,
636
+ ref1: string,
637
+ ref2: string,
638
+ ): Promise<boolean> {
639
+ const result = await git(dir, `merge-base --is-ancestor "${ref1}" "${ref2}"`)
640
+ return !(result instanceof Error)
641
+ }
642
+
643
+ async function isRebasedOnto(dir: string, target: string): Promise<boolean> {
644
+ const mergeBase = await git(dir, `merge-base HEAD "${target}"`)
645
+ if (mergeBase instanceof Error) {
646
+ return false
647
+ }
648
+ const targetSha = await git(dir, `rev-parse "${target}"`)
649
+ if (targetSha instanceof Error) {
650
+ return false
651
+ }
652
+ return mergeBase === targetSha
653
+ }
654
+
655
+ async function getChangedFiles(
656
+ dir: string,
657
+ ref1: string,
658
+ ref2: string,
659
+ ): Promise<string[]> {
660
+ const result = await git(dir, `diff --name-only "${ref1}" "${ref2}"`)
661
+ if (result instanceof Error) {
662
+ return []
663
+ }
664
+ return result.split('\n').filter(Boolean)
665
+ }
666
+
667
+ /**
668
+ * Get dirty files using porcelain -z format.
669
+ * Handles rename/copy entries which emit two NUL-separated paths.
670
+ */
671
+ async function getDirtyFiles(dir: string): Promise<string[]> {
672
+ const result = await git(dir, 'status --porcelain -z')
673
+ if (result instanceof Error) {
674
+ return []
675
+ }
676
+ const files: string[] = []
677
+ const parts = result.split('\0')
678
+ let i = 0
679
+ while (i < parts.length) {
680
+ const entry = parts[i]
681
+ if (!entry || entry.length < 3) {
682
+ i++
683
+ continue
684
+ }
685
+ const status = entry.slice(0, 2)
686
+ const filePath = entry.slice(3)
687
+ if (filePath) {
688
+ files.push(filePath)
689
+ }
690
+ if (
691
+ status[0] === 'R' ||
692
+ status[0] === 'C' ||
693
+ status[1] === 'R' ||
694
+ status[1] === 'C'
695
+ ) {
696
+ i++
697
+ const oldPath = parts[i]
698
+ if (oldPath) {
699
+ files.push(oldPath)
700
+ }
701
+ }
702
+ i++
703
+ }
704
+ return files
705
+ }
706
+
707
+ /**
708
+ * Check if target worktree has dirty files overlapping with the push range.
709
+ * updateInstead only rejects overlapping files; non-overlapping dirty files
710
+ * are left untouched.
711
+ */
712
+ async function checkTargetWorktreeConflicts({
713
+ targetDir,
714
+ sourceDir,
715
+ targetBranch,
716
+ }: {
717
+ targetDir: string
718
+ sourceDir: string
719
+ targetBranch: string
720
+ }): Promise<string[] | null> {
721
+ if (!(await isDirty(targetDir))) {
722
+ return null
723
+ }
724
+ const pushFiles = await getChangedFiles(sourceDir, targetBranch, 'HEAD')
725
+ const dirtyFiles = await getDirtyFiles(targetDir)
726
+ const overlapping = pushFiles.filter((f) => {
727
+ return dirtyFiles.includes(f)
728
+ })
729
+ return overlapping.length > 0 ? overlapping : null
730
+ }
731
+
732
+ /**
733
+ * Check if git is mid-rebase by looking for rebase-merge or rebase-apply dirs.
734
+ */
735
+ async function isRebaseInProgress(dir: string): Promise<boolean> {
736
+ for (const rebaseDir of ['rebase-merge', 'rebase-apply']) {
737
+ const gitPath = await git(dir, `rev-parse --git-path ${rebaseDir}`)
738
+ if (gitPath instanceof Error) {
739
+ continue
740
+ }
741
+ const resolvedPath = path.isAbsolute(gitPath)
742
+ ? gitPath
743
+ : path.resolve(dir, gitPath)
744
+ const exists = await fs.promises
745
+ .access(resolvedPath)
746
+ .then(() => {
747
+ return true
748
+ })
749
+ .catch(() => {
750
+ return false
751
+ })
752
+ if (exists) {
753
+ return true
754
+ }
755
+ }
756
+ return false
757
+ }
758
+
759
+ export function buildSquashMessage({
760
+ branchName,
761
+ commitMessages,
762
+ }: {
763
+ branchName: string
764
+ commitMessages: string[]
765
+ }): string {
766
+ const lines: string[] = [`worktree merge: ${branchName}`]
767
+ if (commitMessages.length > 0) {
768
+ lines.push('')
769
+ for (const message of commitMessages) {
770
+ const msgLines = message.split('\n')
771
+ lines.push(`- ${msgLines[0]}`)
772
+ for (const extra of msgLines.slice(1)) {
773
+ lines.push(` ${extra}`)
774
+ }
775
+ }
776
+ }
777
+ return lines.join('\n')
778
+ }
779
+
780
+ /**
781
+ * Merge a worktree branch into the default branch using worktrunk-style pipeline.
782
+ * Returns MergeWorktreeErrors | MergeSuccess.
783
+ */
784
+ export async function mergeWorktree({
785
+ worktreeDir,
786
+ mainRepoDir,
787
+ worktreeName,
788
+ onProgress,
789
+ }: {
790
+ worktreeDir: string
791
+ mainRepoDir: string
792
+ worktreeName: string
793
+ onProgress?: (message: string) => void
794
+ }): Promise<MergeWorktreeErrors | MergeSuccess> {
795
+ const log = (msg: string) => {
796
+ logger.log(msg)
797
+ onProgress?.(msg)
798
+ }
799
+
800
+ // Resolve current branch. If detached, create a temp branch.
801
+ let branchName: string
802
+ let tempBranch: string | null = null
803
+ const branchResult = await git(worktreeDir, 'symbolic-ref --short HEAD')
804
+ if (branchResult instanceof Error) {
805
+ tempBranch = `kimaki-merge-${Date.now()}`
806
+ const createResult = await git(worktreeDir, `checkout -b "${tempBranch}"`)
807
+ if (createResult instanceof Error) {
808
+ return createResult
809
+ }
810
+ branchName = tempBranch
811
+ } else {
812
+ branchName = branchResult || worktreeName
813
+ }
814
+
815
+ const defaultBranch = await getDefaultBranch(mainRepoDir)
816
+ log(`Merging ${branchName} into ${defaultBranch}`)
817
+
818
+ // Best-effort cleanup of temp branch on error paths
819
+ const cleanupTempBranch = async () => {
820
+ if (!tempBranch) {
821
+ return
822
+ }
823
+ await git(worktreeDir, 'checkout --detach')
824
+ await git(worktreeDir, `branch -D "${tempBranch}"`)
825
+ }
826
+
827
+ // ── Step 1: Reject uncommitted changes ──
828
+ if (await isDirty(worktreeDir)) {
829
+ await cleanupTempBranch()
830
+ return new DirtyWorktreeError()
831
+ }
832
+
833
+ // ── Step 2: Squash + Step 3: Rebase ──
834
+ // If already rebased onto target, skip squash+rebase entirely.
835
+ // This happens on retry after the model resolved a rebase conflict --
836
+ // the previous run already squashed, and the model completed the rebase.
837
+ const alreadyRebased = await isRebasedOnto(worktreeDir, defaultBranch)
838
+
839
+ const mergeBaseResult = await git(
840
+ worktreeDir,
841
+ `merge-base HEAD "${defaultBranch}"`,
842
+ )
843
+ const mergeBase =
844
+ mergeBaseResult instanceof Error ? defaultBranch : mergeBaseResult
845
+
846
+ const commitCountResult = await git(
847
+ worktreeDir,
848
+ `rev-list --count "${mergeBase}..HEAD"`,
849
+ )
850
+ if (commitCountResult instanceof Error) {
851
+ await cleanupTempBranch()
852
+ return commitCountResult
853
+ }
854
+ const commitCount = parseInt(commitCountResult, 10)
855
+
856
+ if (commitCount === 0) {
857
+ await cleanupTempBranch()
858
+ return new NothingToMergeError({ target: defaultBranch })
859
+ }
860
+
861
+ if (!alreadyRebased) {
862
+ // Squash into single commit with full commit messages
863
+ log(
864
+ commitCount > 1
865
+ ? `Squashing ${commitCount} commits...`
866
+ : 'Preparing merge commit...',
867
+ )
868
+
869
+ const SEP = '---KIMAKI-COMMIT-SEP---'
870
+ const logRange = `${mergeBase}..HEAD`
871
+ const messagesResult = await git(
872
+ worktreeDir,
873
+ `log --format="%B${SEP}" --reverse "${logRange}"`,
874
+ )
875
+ if (messagesResult instanceof Error) {
876
+ await cleanupTempBranch()
877
+ return new SquashError({
878
+ reason: 'Failed to read commit messages',
879
+ cause: messagesResult,
880
+ })
881
+ }
882
+
883
+ const commitMessages = messagesResult
884
+ .split(SEP)
885
+ .map((m) => {
886
+ return m.trim()
887
+ })
888
+ .filter(Boolean)
889
+
890
+ const squashMessage = buildSquashMessage({
891
+ branchName: worktreeName || branchName,
892
+ commitMessages,
893
+ })
894
+
895
+ const resetResult = await git(worktreeDir, `reset --soft "${mergeBase}"`)
896
+ if (resetResult instanceof Error) {
897
+ await cleanupTempBranch()
898
+ return new SquashError({
899
+ reason: 'git reset --soft failed',
900
+ cause: resetResult,
901
+ })
902
+ }
903
+
904
+ const commitResult = await errore.tryAsync({
905
+ try: () =>
906
+ runGitWithStdin(['commit', '-m', squashMessage, '--'], worktreeDir, ''),
907
+ catch: (e) =>
908
+ new SquashError({ reason: 'git commit failed after reset', cause: e }),
909
+ })
910
+ if (commitResult instanceof Error) {
911
+ await cleanupTempBranch()
912
+ return commitResult
913
+ }
914
+
915
+ // Rebase onto target
916
+ log(`Rebasing onto ${defaultBranch}...`)
917
+ const rebaseResult = await git(worktreeDir, `rebase "${defaultBranch}"`, {
918
+ timeout: 60_000,
919
+ })
920
+ if (rebaseResult instanceof Error) {
921
+ if (await isRebaseInProgress(worktreeDir)) {
922
+ return new RebaseConflictError({
923
+ target: defaultBranch,
924
+ cause: rebaseResult,
925
+ })
926
+ }
927
+ await cleanupTempBranch()
928
+ return new RebaseError({ target: defaultBranch, cause: rebaseResult })
929
+ }
930
+ } else {
931
+ log('Already rebased onto target')
932
+ }
933
+
934
+ // ── Step 4: Fast-forward push via local git push ──
935
+ if (!(await isAncestor(worktreeDir, defaultBranch, 'HEAD'))) {
936
+ await cleanupTempBranch()
937
+ return new NotFastForwardError({ target: defaultBranch })
938
+ }
939
+
940
+ const overlappingFiles = await checkTargetWorktreeConflicts({
941
+ targetDir: mainRepoDir,
942
+ sourceDir: worktreeDir,
943
+ targetBranch: defaultBranch,
944
+ })
945
+ if (overlappingFiles) {
946
+ await cleanupTempBranch()
947
+ return new ConflictingFilesError({ target: defaultBranch })
948
+ }
949
+
950
+ const gitCommonDir = await getGitCommonDir(worktreeDir)
951
+ if (gitCommonDir instanceof Error) {
952
+ await cleanupTempBranch()
953
+ return gitCommonDir
954
+ }
955
+
956
+ log(`Pushing to ${defaultBranch}...`)
957
+ const pushResult = await git(
958
+ worktreeDir,
959
+ `push --receive-pack="git -c receive.denyCurrentBranch=updateInstead receive-pack" "${gitCommonDir}" "HEAD:${defaultBranch}"`,
960
+ { timeout: 30_000 },
961
+ )
962
+ if (pushResult instanceof Error) {
963
+ await cleanupTempBranch()
964
+ return new PushError({ target: defaultBranch, cause: pushResult })
965
+ }
966
+
967
+ // Get short SHA for display
968
+ const shortSha = await git(worktreeDir, 'rev-parse --short HEAD')
969
+ if (shortSha instanceof Error) {
970
+ // Push succeeded but can't get SHA -- non-fatal, use placeholder
971
+ logger.warn('Failed to get short SHA after push')
972
+ }
973
+
974
+ // ── Step 5: Clean up -- detach HEAD and delete branch ──
975
+ log('Cleaning up worktree...')
976
+ await git(worktreeDir, `checkout --detach "${defaultBranch}"`)
977
+ await git(worktreeDir, `branch -D "${branchName}"`)
978
+ if (branchName !== worktreeName && worktreeName) {
979
+ await git(worktreeDir, `branch -D "${worktreeName}"`)
980
+ }
981
+
982
+ return {
983
+ defaultBranch,
984
+ branchName: worktreeName || branchName,
985
+ commitCount,
986
+ shortSha: shortSha instanceof Error ? 'unknown' : shortSha,
987
+ }
988
+ }