@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,727 @@
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
+ import crypto from 'node:crypto';
5
+ import { exec, spawn } from 'node:child_process';
6
+ import fs from 'node:fs';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+ import { promisify } from 'node:util';
10
+ import { createLogger, LogPrefix } from './logger.js';
11
+ const DEFAULT_EXEC_TIMEOUT_MS = 10_000;
12
+ const SUBMODULE_INIT_TIMEOUT_MS = 20 * 60_000;
13
+ const _execAsync = promisify(exec);
14
+ // Wraps child_process.exec with a default 10s timeout via Promise.race.
15
+ // Callers can override with a longer timeout in the options.
16
+ export function execAsync(command, options) {
17
+ const timeoutMs = options?.timeout || DEFAULT_EXEC_TIMEOUT_MS;
18
+ const execPromise = _execAsync(command, options);
19
+ const timeoutPromise = new Promise((_, reject) => {
20
+ setTimeout(() => {
21
+ execPromise.child?.kill();
22
+ reject(new Error(`Command timed out after ${timeoutMs}ms: ${command}`));
23
+ }, timeoutMs);
24
+ });
25
+ return Promise.race([execPromise, timeoutPromise]);
26
+ }
27
+ const logger = createLogger(LogPrefix.WORKTREE);
28
+ const LOCKFILE_TO_INSTALL_COMMAND = [
29
+ ['pnpm-lock.yaml', 'pnpm install'],
30
+ ['bun.lock', 'bun install'],
31
+ ['bun.lockb', 'bun install'],
32
+ ['yarn.lock', 'yarn install'],
33
+ ['package-lock.json', 'npm install'],
34
+ ];
35
+ function detectInstallCommand(directory) {
36
+ for (const [lockfile, command] of LOCKFILE_TO_INSTALL_COMMAND) {
37
+ if (fs.existsSync(path.join(directory, lockfile))) {
38
+ return command;
39
+ }
40
+ }
41
+ return null;
42
+ }
43
+ function formatCommandError(error) {
44
+ if (!(error instanceof Error)) {
45
+ return String(error);
46
+ }
47
+ const commandError = error;
48
+ const details = [commandError.message];
49
+ if (commandError.cmd) {
50
+ details.push(`cmd=${commandError.cmd}`);
51
+ }
52
+ if (commandError.signal) {
53
+ details.push(`signal=${commandError.signal}`);
54
+ }
55
+ if (commandError.killed) {
56
+ details.push('process=killed');
57
+ }
58
+ if (commandError.stderr?.trim()) {
59
+ details.push(`stderr=${commandError.stderr.trim()}`);
60
+ }
61
+ if (commandError.stdout?.trim()) {
62
+ details.push(`stdout=${commandError.stdout.trim()}`);
63
+ }
64
+ return details.join(' | ');
65
+ }
66
+ /**
67
+ * Get submodule paths from .gitmodules file.
68
+ * Returns empty array if no submodules or on error.
69
+ */
70
+ async function getSubmodulePaths(directory) {
71
+ try {
72
+ const result = await execAsync('git config --file .gitmodules --get-regexp path', {
73
+ cwd: directory,
74
+ });
75
+ // Output format: "submodule.name.path value"
76
+ return result.stdout
77
+ .trim()
78
+ .split('\n')
79
+ .filter(Boolean)
80
+ .map((line) => {
81
+ return line.split(' ')[1];
82
+ })
83
+ .filter((p) => {
84
+ return Boolean(p);
85
+ });
86
+ }
87
+ catch {
88
+ return []; // No .gitmodules or no submodules
89
+ }
90
+ }
91
+ /**
92
+ * Remove broken submodule stubs created by git worktree.
93
+ * When git worktree add runs on a repo with submodules, it creates submodule
94
+ * directories with .git files pointing to ../.git/worktrees/<name>/modules/<submodule>
95
+ * but that path only has a config file, missing HEAD/objects/refs.
96
+ * This causes git commands to fail with "fatal: not a git repository".
97
+ */
98
+ async function removeBrokenSubmoduleStubs(directory) {
99
+ const submodulePaths = await getSubmodulePaths(directory);
100
+ for (const subPath of submodulePaths) {
101
+ const fullPath = path.join(directory, subPath);
102
+ const gitFile = path.join(fullPath, '.git');
103
+ try {
104
+ const stat = await fs.promises.stat(gitFile);
105
+ if (!stat.isFile()) {
106
+ continue;
107
+ }
108
+ // Read .git file to get gitdir path
109
+ const content = await fs.promises.readFile(gitFile, 'utf-8');
110
+ const match = content.match(/^gitdir:\s*(.+)$/m);
111
+ if (!match || !match[1]) {
112
+ continue;
113
+ }
114
+ const gitdir = path.resolve(fullPath, match[1].trim());
115
+ const headFile = path.join(gitdir, 'HEAD');
116
+ // If HEAD doesn't exist, this is a broken stub
117
+ const headExists = await fs.promises
118
+ .access(headFile)
119
+ .then(() => {
120
+ return true;
121
+ })
122
+ .catch(() => {
123
+ return false;
124
+ });
125
+ if (!headExists) {
126
+ logger.log(`Removing broken submodule stub: ${subPath}`);
127
+ await fs.promises.rm(fullPath, { recursive: true, force: true });
128
+ }
129
+ }
130
+ catch {
131
+ // Directory doesn't exist or other error, skip
132
+ }
133
+ }
134
+ }
135
+ function parseSubmoduleGitdir(gitFileContent) {
136
+ const match = gitFileContent.match(/^gitdir:\s*(.+)$/m);
137
+ const gitdir = match?.[1]?.trim();
138
+ if (!gitdir) {
139
+ return new Error('Missing gitdir pointer');
140
+ }
141
+ return gitdir;
142
+ }
143
+ async function validateSubmodulePointers(directory) {
144
+ const submodulePaths = await getSubmodulePaths(directory);
145
+ if (submodulePaths.length === 0) {
146
+ return;
147
+ }
148
+ const validationIssues = [];
149
+ await Promise.all(submodulePaths.map(async (submodulePath) => {
150
+ const submoduleDir = path.join(directory, submodulePath);
151
+ const submoduleGitFile = path.join(submoduleDir, '.git');
152
+ const gitFileExists = await fs.promises
153
+ .access(submoduleGitFile)
154
+ .then(() => {
155
+ return true;
156
+ })
157
+ .catch(() => {
158
+ return false;
159
+ });
160
+ if (!gitFileExists) {
161
+ validationIssues.push(`${submodulePath}: missing .git file`);
162
+ return;
163
+ }
164
+ const gitFileContentResult = await errore.tryAsync({
165
+ try: () => fs.promises.readFile(submoduleGitFile, 'utf-8'),
166
+ catch: (e) => new Error(`Failed to read .git for ${submodulePath}`, { cause: e }),
167
+ });
168
+ if (gitFileContentResult instanceof Error) {
169
+ validationIssues.push(`${submodulePath}: ${gitFileContentResult.message}`);
170
+ return;
171
+ }
172
+ const parsedGitdir = parseSubmoduleGitdir(gitFileContentResult);
173
+ if (parsedGitdir instanceof Error) {
174
+ validationIssues.push(`${submodulePath}: ${parsedGitdir.message}`);
175
+ return;
176
+ }
177
+ const resolvedGitdir = path.resolve(submoduleDir, parsedGitdir);
178
+ const headPath = path.join(resolvedGitdir, 'HEAD');
179
+ const headExists = await fs.promises
180
+ .access(headPath)
181
+ .then(() => {
182
+ return true;
183
+ })
184
+ .catch(() => {
185
+ return false;
186
+ });
187
+ if (!headExists) {
188
+ validationIssues.push(`${submodulePath}: gitdir missing HEAD (${resolvedGitdir})`);
189
+ }
190
+ }));
191
+ const submoduleStatusResult = await errore.tryAsync({
192
+ try: () => execAsync('git submodule status --recursive', {
193
+ cwd: directory,
194
+ timeout: SUBMODULE_INIT_TIMEOUT_MS,
195
+ }),
196
+ catch: (e) => new Error('git submodule status --recursive failed', { cause: e }),
197
+ });
198
+ if (submoduleStatusResult instanceof Error) {
199
+ validationIssues.push(submoduleStatusResult.message);
200
+ }
201
+ if (validationIssues.length === 0) {
202
+ return;
203
+ }
204
+ return new Error(`Submodule validation failed: ${validationIssues.join('; ')}`);
205
+ }
206
+ async function resolveDefaultWorktreeTarget(directory) {
207
+ const remoteHead = await execAsync('git symbolic-ref refs/remotes/origin/HEAD', {
208
+ cwd: directory,
209
+ }).catch(() => {
210
+ return null;
211
+ });
212
+ const remoteRef = remoteHead?.stdout.trim();
213
+ if (remoteRef?.startsWith('refs/remotes/')) {
214
+ return remoteRef.replace('refs/remotes/', '');
215
+ }
216
+ const hasMain = await execAsync('git show-ref --verify --quiet refs/heads/main', {
217
+ cwd: directory,
218
+ })
219
+ .then(() => {
220
+ return true;
221
+ })
222
+ .catch(() => {
223
+ return false;
224
+ });
225
+ if (hasMain) {
226
+ return 'main';
227
+ }
228
+ const hasMaster = await execAsync('git show-ref --verify --quiet refs/heads/master', {
229
+ cwd: directory,
230
+ })
231
+ .then(() => {
232
+ return true;
233
+ })
234
+ .catch(() => {
235
+ return false;
236
+ });
237
+ if (hasMaster) {
238
+ return 'master';
239
+ }
240
+ return 'HEAD';
241
+ }
242
+ function getManagedWorktreeDirectory({ directory, name, }) {
243
+ const projectHash = crypto.createHash('sha1').update(directory).digest('hex');
244
+ const safeName = name.replaceAll('/', '-');
245
+ return path.join(os.homedir(), '.local', 'share', 'opencode', 'worktree', projectHash, safeName);
246
+ }
247
+ /**
248
+ * Create a worktree using git and initialize git submodules.
249
+ * This wrapper ensures submodules are properly set up in new worktrees.
250
+ *
251
+ * If diff is provided, it's applied BEFORE submodule update to ensure
252
+ * any submodule pointer changes in the diff are respected.
253
+ */
254
+ export async function createWorktreeWithSubmodules({ directory, name, diff, }) {
255
+ // 1. Create worktree via git (checked out immediately).
256
+ const worktreeDir = getManagedWorktreeDirectory({ directory, name });
257
+ const targetRef = await resolveDefaultWorktreeTarget(directory);
258
+ if (fs.existsSync(worktreeDir)) {
259
+ return new Error(`Worktree directory already exists: ${worktreeDir}`);
260
+ }
261
+ await fs.promises.mkdir(path.dirname(worktreeDir), { recursive: true });
262
+ const createCommand = `git worktree add ${JSON.stringify(worktreeDir)} -B ${JSON.stringify(name)} ${JSON.stringify(targetRef)}`;
263
+ const createResult = await errore.tryAsync({
264
+ try: () => execAsync(createCommand, {
265
+ cwd: directory,
266
+ timeout: SUBMODULE_INIT_TIMEOUT_MS,
267
+ }),
268
+ catch: (e) => new Error(`git worktree add failed: ${formatCommandError(e)}`, {
269
+ cause: e,
270
+ }),
271
+ });
272
+ if (createResult instanceof Error) {
273
+ return createResult;
274
+ }
275
+ let diffApplied = false;
276
+ // 2. Apply diff BEFORE submodule update (if provided)
277
+ // This ensures any submodule pointer changes in the diff are applied first,
278
+ // so submodule update checks out the correct commits.
279
+ if (diff) {
280
+ logger.log(`Applying diff to ${worktreeDir} before submodule init`);
281
+ diffApplied = await applyGitDiff(worktreeDir, diff);
282
+ }
283
+ // 3. Remove broken submodule stubs before init
284
+ // git worktree creates stub directories with .git files pointing to incomplete gitdirs
285
+ await removeBrokenSubmoduleStubs(worktreeDir);
286
+ // 4. Init submodules in new worktree
287
+ // Uses --init to initialize, --recursive for nested submodules.
288
+ // Submodules will be checked out at the commit specified by the (possibly updated) index.
289
+ try {
290
+ logger.log(`Initializing submodules in ${worktreeDir} (timeout=${SUBMODULE_INIT_TIMEOUT_MS}ms)`);
291
+ await execAsync('git submodule update --init --recursive', {
292
+ cwd: worktreeDir,
293
+ timeout: SUBMODULE_INIT_TIMEOUT_MS,
294
+ });
295
+ logger.log(`Submodules initialized in ${worktreeDir}`);
296
+ }
297
+ catch (e) {
298
+ const errorMessage = formatCommandError(e);
299
+ logger.error('Submodule initialization failed', {
300
+ worktreeDir,
301
+ timeoutMs: SUBMODULE_INIT_TIMEOUT_MS,
302
+ command: 'git submodule update --init --recursive',
303
+ error: errorMessage,
304
+ });
305
+ return new Error(`Submodule initialization failed: ${errorMessage}`);
306
+ }
307
+ // 4.5 Validate submodule pointers and git metadata before marking ready.
308
+ const submoduleValidationError = await validateSubmodulePointers(worktreeDir);
309
+ if (submoduleValidationError instanceof Error) {
310
+ logger.error('Submodule validation failed after init', {
311
+ worktreeDir,
312
+ error: submoduleValidationError.message,
313
+ });
314
+ return submoduleValidationError;
315
+ }
316
+ // 5. Dependency install disabled.
317
+ // `npx -y ni` resolved to the wrong npm package `ni` (browser-launcher), not `@antfu/ni`.
318
+ // detectInstallCommand() was built as a replacement but install is skipped for now.
319
+ // Opencode sessions can run install themselves if needed.
320
+ return { directory: worktreeDir, branch: name, diffApplied };
321
+ }
322
+ /**
323
+ * Capture git diff from a directory (both staged and unstaged changes).
324
+ * Returns null if no changes or on error.
325
+ */
326
+ export async function captureGitDiff(directory) {
327
+ try {
328
+ // Capture unstaged changes
329
+ const unstagedResult = await execAsync('git diff', { cwd: directory });
330
+ const unstaged = unstagedResult.stdout.trim();
331
+ // Capture staged changes
332
+ const stagedResult = await execAsync('git diff --staged', {
333
+ cwd: directory,
334
+ });
335
+ const staged = stagedResult.stdout.trim();
336
+ if (!unstaged && !staged) {
337
+ return null;
338
+ }
339
+ return { unstaged, staged };
340
+ }
341
+ catch (e) {
342
+ logger.warn(`Failed to capture git diff from ${directory}: ${e instanceof Error ? e.message : String(e)}`);
343
+ return null;
344
+ }
345
+ }
346
+ /**
347
+ * Run a git command with stdin input.
348
+ * Uses spawn to pipe the diff content to git apply.
349
+ */
350
+ function runGitWithStdin(args, cwd, input) {
351
+ return new Promise((resolve, reject) => {
352
+ const child = spawn('git', args, { cwd, stdio: ['pipe', 'pipe', 'pipe'] });
353
+ let stderr = '';
354
+ child.stderr?.on('data', (data) => {
355
+ stderr += data.toString();
356
+ });
357
+ child.on('close', (code) => {
358
+ if (code === 0) {
359
+ resolve();
360
+ }
361
+ else {
362
+ reject(new Error(stderr || `git ${args.join(' ')} failed with code ${code}`));
363
+ }
364
+ });
365
+ child.on('error', reject);
366
+ child.stdin?.write(input);
367
+ child.stdin?.end();
368
+ });
369
+ }
370
+ /**
371
+ * Apply a captured git diff to a directory.
372
+ * Applies staged changes first, then unstaged.
373
+ */
374
+ export async function applyGitDiff(directory, diff) {
375
+ try {
376
+ // Apply staged changes first (and stage them)
377
+ if (diff.staged) {
378
+ logger.log(`Applying staged diff to ${directory}`);
379
+ await runGitWithStdin(['apply', '--index'], directory, diff.staged);
380
+ }
381
+ // Apply unstaged changes (don't stage them)
382
+ if (diff.unstaged) {
383
+ logger.log(`Applying unstaged diff to ${directory}`);
384
+ await runGitWithStdin(['apply'], directory, diff.unstaged);
385
+ }
386
+ logger.log(`Successfully applied diff to ${directory}`);
387
+ return true;
388
+ }
389
+ catch (e) {
390
+ logger.warn(`Failed to apply git diff to ${directory}: ${e instanceof Error ? e.message : String(e)}`);
391
+ return false;
392
+ }
393
+ }
394
+ // ─── Worktree merge ──────────────────────────────────────────────────────────
395
+ // Implements a worktrunk-style merge pipeline:
396
+ // 1. Reject if uncommitted changes exist
397
+ // 2. Squash all commits since merge-base into one
398
+ // 3. Rebase onto target (default branch)
399
+ // 4. Fast-forward push to target via local git push
400
+ // 5. Switch to detached HEAD, delete branch
401
+ //
402
+ // Uses `git push <git-common-dir> HEAD:<target>` with
403
+ // `receive.denyCurrentBranch=updateInstead` to fast-forward the target
404
+ // WITHOUT checking it out in the main repo.
405
+ //
406
+ // Returns MergeWorktreeErrors | MergeSuccess. All errors are tagged via errore.
407
+ // - DirtyWorktreeError → git untouched
408
+ // - NothingToMergeError → git untouched
409
+ // - SquashError → HEAD may be at merge-base with staged changes
410
+ // - RebaseConflictError → git left mid-rebase for AI/user resolution
411
+ // - RebaseError → rebase not in progress; temp branch cleaned
412
+ // - NotFastForwardError → source intact; no push
413
+ // - ConflictingFilesError → no push; lists overlapping files
414
+ // - PushError → source rebased but target unchanged
415
+ // - GitCommandError → catch-all for unexpected git failures
416
+ import * as errore from 'errore';
417
+ import { DirtyWorktreeError, NothingToMergeError, SquashError, RebaseConflictError, RebaseError, NotFastForwardError, ConflictingFilesError, PushError, GitCommandError, } from './errors.js';
418
+ async function git(dir, args, opts) {
419
+ const result = await errore.tryAsync({
420
+ try: () => execAsync(`git -C "${dir}" ${args}`, opts ? { timeout: opts.timeout } : undefined),
421
+ catch: (e) => new GitCommandError({ command: args, cause: e }),
422
+ });
423
+ if (result instanceof Error) {
424
+ return result;
425
+ }
426
+ return result.stdout.trim();
427
+ }
428
+ async function getDefaultBranch(repoDir) {
429
+ const ref = await git(repoDir, 'symbolic-ref refs/remotes/origin/HEAD');
430
+ if (ref instanceof Error) {
431
+ return 'main';
432
+ }
433
+ return ref.replace(/^refs\/remotes\/origin\//, '') || 'main';
434
+ }
435
+ async function isDirty(dir) {
436
+ const status = await git(dir, 'status --porcelain');
437
+ if (status instanceof Error) {
438
+ return false;
439
+ }
440
+ return status.length > 0;
441
+ }
442
+ async function getGitCommonDir(dir) {
443
+ const commonDir = await git(dir, 'rev-parse --git-common-dir');
444
+ if (commonDir instanceof Error) {
445
+ return commonDir;
446
+ }
447
+ if (path.isAbsolute(commonDir)) {
448
+ return commonDir;
449
+ }
450
+ return path.resolve(dir, commonDir);
451
+ }
452
+ async function isAncestor(dir, ref1, ref2) {
453
+ const result = await git(dir, `merge-base --is-ancestor "${ref1}" "${ref2}"`);
454
+ return !(result instanceof Error);
455
+ }
456
+ async function isRebasedOnto(dir, target) {
457
+ const mergeBase = await git(dir, `merge-base HEAD "${target}"`);
458
+ if (mergeBase instanceof Error) {
459
+ return false;
460
+ }
461
+ const targetSha = await git(dir, `rev-parse "${target}"`);
462
+ if (targetSha instanceof Error) {
463
+ return false;
464
+ }
465
+ return mergeBase === targetSha;
466
+ }
467
+ async function getChangedFiles(dir, ref1, ref2) {
468
+ const result = await git(dir, `diff --name-only "${ref1}" "${ref2}"`);
469
+ if (result instanceof Error) {
470
+ return [];
471
+ }
472
+ return result.split('\n').filter(Boolean);
473
+ }
474
+ /**
475
+ * Get dirty files using porcelain -z format.
476
+ * Handles rename/copy entries which emit two NUL-separated paths.
477
+ */
478
+ async function getDirtyFiles(dir) {
479
+ const result = await git(dir, 'status --porcelain -z');
480
+ if (result instanceof Error) {
481
+ return [];
482
+ }
483
+ const files = [];
484
+ const parts = result.split('\0');
485
+ let i = 0;
486
+ while (i < parts.length) {
487
+ const entry = parts[i];
488
+ if (!entry || entry.length < 3) {
489
+ i++;
490
+ continue;
491
+ }
492
+ const status = entry.slice(0, 2);
493
+ const filePath = entry.slice(3);
494
+ if (filePath) {
495
+ files.push(filePath);
496
+ }
497
+ if (status[0] === 'R' ||
498
+ status[0] === 'C' ||
499
+ status[1] === 'R' ||
500
+ status[1] === 'C') {
501
+ i++;
502
+ const oldPath = parts[i];
503
+ if (oldPath) {
504
+ files.push(oldPath);
505
+ }
506
+ }
507
+ i++;
508
+ }
509
+ return files;
510
+ }
511
+ /**
512
+ * Check if target worktree has dirty files overlapping with the push range.
513
+ * updateInstead only rejects overlapping files; non-overlapping dirty files
514
+ * are left untouched.
515
+ */
516
+ async function checkTargetWorktreeConflicts({ targetDir, sourceDir, targetBranch, }) {
517
+ if (!(await isDirty(targetDir))) {
518
+ return null;
519
+ }
520
+ const pushFiles = await getChangedFiles(sourceDir, targetBranch, 'HEAD');
521
+ const dirtyFiles = await getDirtyFiles(targetDir);
522
+ const overlapping = pushFiles.filter((f) => {
523
+ return dirtyFiles.includes(f);
524
+ });
525
+ return overlapping.length > 0 ? overlapping : null;
526
+ }
527
+ /**
528
+ * Check if git is mid-rebase by looking for rebase-merge or rebase-apply dirs.
529
+ */
530
+ async function isRebaseInProgress(dir) {
531
+ for (const rebaseDir of ['rebase-merge', 'rebase-apply']) {
532
+ const gitPath = await git(dir, `rev-parse --git-path ${rebaseDir}`);
533
+ if (gitPath instanceof Error) {
534
+ continue;
535
+ }
536
+ const resolvedPath = path.isAbsolute(gitPath)
537
+ ? gitPath
538
+ : path.resolve(dir, gitPath);
539
+ const exists = await fs.promises
540
+ .access(resolvedPath)
541
+ .then(() => {
542
+ return true;
543
+ })
544
+ .catch(() => {
545
+ return false;
546
+ });
547
+ if (exists) {
548
+ return true;
549
+ }
550
+ }
551
+ return false;
552
+ }
553
+ export function buildSquashMessage({ branchName, commitMessages, }) {
554
+ const lines = [`worktree merge: ${branchName}`];
555
+ if (commitMessages.length > 0) {
556
+ lines.push('');
557
+ for (const message of commitMessages) {
558
+ const msgLines = message.split('\n');
559
+ lines.push(`- ${msgLines[0]}`);
560
+ for (const extra of msgLines.slice(1)) {
561
+ lines.push(` ${extra}`);
562
+ }
563
+ }
564
+ }
565
+ return lines.join('\n');
566
+ }
567
+ /**
568
+ * Merge a worktree branch into the default branch using worktrunk-style pipeline.
569
+ * Returns MergeWorktreeErrors | MergeSuccess.
570
+ */
571
+ export async function mergeWorktree({ worktreeDir, mainRepoDir, worktreeName, onProgress, }) {
572
+ const log = (msg) => {
573
+ logger.log(msg);
574
+ onProgress?.(msg);
575
+ };
576
+ // Resolve current branch. If detached, create a temp branch.
577
+ let branchName;
578
+ let tempBranch = null;
579
+ const branchResult = await git(worktreeDir, 'symbolic-ref --short HEAD');
580
+ if (branchResult instanceof Error) {
581
+ tempBranch = `kimaki-merge-${Date.now()}`;
582
+ const createResult = await git(worktreeDir, `checkout -b "${tempBranch}"`);
583
+ if (createResult instanceof Error) {
584
+ return createResult;
585
+ }
586
+ branchName = tempBranch;
587
+ }
588
+ else {
589
+ branchName = branchResult || worktreeName;
590
+ }
591
+ const defaultBranch = await getDefaultBranch(mainRepoDir);
592
+ log(`Merging ${branchName} into ${defaultBranch}`);
593
+ // Best-effort cleanup of temp branch on error paths
594
+ const cleanupTempBranch = async () => {
595
+ if (!tempBranch) {
596
+ return;
597
+ }
598
+ await git(worktreeDir, 'checkout --detach');
599
+ await git(worktreeDir, `branch -D "${tempBranch}"`);
600
+ };
601
+ // ── Step 1: Reject uncommitted changes ──
602
+ if (await isDirty(worktreeDir)) {
603
+ await cleanupTempBranch();
604
+ return new DirtyWorktreeError();
605
+ }
606
+ // ── Step 2: Squash + Step 3: Rebase ──
607
+ // If already rebased onto target, skip squash+rebase entirely.
608
+ // This happens on retry after the model resolved a rebase conflict --
609
+ // the previous run already squashed, and the model completed the rebase.
610
+ const alreadyRebased = await isRebasedOnto(worktreeDir, defaultBranch);
611
+ const mergeBaseResult = await git(worktreeDir, `merge-base HEAD "${defaultBranch}"`);
612
+ const mergeBase = mergeBaseResult instanceof Error ? defaultBranch : mergeBaseResult;
613
+ const commitCountResult = await git(worktreeDir, `rev-list --count "${mergeBase}..HEAD"`);
614
+ if (commitCountResult instanceof Error) {
615
+ await cleanupTempBranch();
616
+ return commitCountResult;
617
+ }
618
+ const commitCount = parseInt(commitCountResult, 10);
619
+ if (commitCount === 0) {
620
+ await cleanupTempBranch();
621
+ return new NothingToMergeError({ target: defaultBranch });
622
+ }
623
+ if (!alreadyRebased) {
624
+ // Squash into single commit with full commit messages
625
+ log(commitCount > 1
626
+ ? `Squashing ${commitCount} commits...`
627
+ : 'Preparing merge commit...');
628
+ const SEP = '---KIMAKI-COMMIT-SEP---';
629
+ const logRange = `${mergeBase}..HEAD`;
630
+ const messagesResult = await git(worktreeDir, `log --format="%B${SEP}" --reverse "${logRange}"`);
631
+ if (messagesResult instanceof Error) {
632
+ await cleanupTempBranch();
633
+ return new SquashError({
634
+ reason: 'Failed to read commit messages',
635
+ cause: messagesResult,
636
+ });
637
+ }
638
+ const commitMessages = messagesResult
639
+ .split(SEP)
640
+ .map((m) => {
641
+ return m.trim();
642
+ })
643
+ .filter(Boolean);
644
+ const squashMessage = buildSquashMessage({
645
+ branchName: worktreeName || branchName,
646
+ commitMessages,
647
+ });
648
+ const resetResult = await git(worktreeDir, `reset --soft "${mergeBase}"`);
649
+ if (resetResult instanceof Error) {
650
+ await cleanupTempBranch();
651
+ return new SquashError({
652
+ reason: 'git reset --soft failed',
653
+ cause: resetResult,
654
+ });
655
+ }
656
+ const commitResult = await errore.tryAsync({
657
+ try: () => runGitWithStdin(['commit', '-m', squashMessage, '--'], worktreeDir, ''),
658
+ catch: (e) => new SquashError({ reason: 'git commit failed after reset', cause: e }),
659
+ });
660
+ if (commitResult instanceof Error) {
661
+ await cleanupTempBranch();
662
+ return commitResult;
663
+ }
664
+ // Rebase onto target
665
+ log(`Rebasing onto ${defaultBranch}...`);
666
+ const rebaseResult = await git(worktreeDir, `rebase "${defaultBranch}"`, {
667
+ timeout: 60_000,
668
+ });
669
+ if (rebaseResult instanceof Error) {
670
+ if (await isRebaseInProgress(worktreeDir)) {
671
+ return new RebaseConflictError({
672
+ target: defaultBranch,
673
+ cause: rebaseResult,
674
+ });
675
+ }
676
+ await cleanupTempBranch();
677
+ return new RebaseError({ target: defaultBranch, cause: rebaseResult });
678
+ }
679
+ }
680
+ else {
681
+ log('Already rebased onto target');
682
+ }
683
+ // ── Step 4: Fast-forward push via local git push ──
684
+ if (!(await isAncestor(worktreeDir, defaultBranch, 'HEAD'))) {
685
+ await cleanupTempBranch();
686
+ return new NotFastForwardError({ target: defaultBranch });
687
+ }
688
+ const overlappingFiles = await checkTargetWorktreeConflicts({
689
+ targetDir: mainRepoDir,
690
+ sourceDir: worktreeDir,
691
+ targetBranch: defaultBranch,
692
+ });
693
+ if (overlappingFiles) {
694
+ await cleanupTempBranch();
695
+ return new ConflictingFilesError({ target: defaultBranch });
696
+ }
697
+ const gitCommonDir = await getGitCommonDir(worktreeDir);
698
+ if (gitCommonDir instanceof Error) {
699
+ await cleanupTempBranch();
700
+ return gitCommonDir;
701
+ }
702
+ log(`Pushing to ${defaultBranch}...`);
703
+ const pushResult = await git(worktreeDir, `push --receive-pack="git -c receive.denyCurrentBranch=updateInstead receive-pack" "${gitCommonDir}" "HEAD:${defaultBranch}"`, { timeout: 30_000 });
704
+ if (pushResult instanceof Error) {
705
+ await cleanupTempBranch();
706
+ return new PushError({ target: defaultBranch, cause: pushResult });
707
+ }
708
+ // Get short SHA for display
709
+ const shortSha = await git(worktreeDir, 'rev-parse --short HEAD');
710
+ if (shortSha instanceof Error) {
711
+ // Push succeeded but can't get SHA -- non-fatal, use placeholder
712
+ logger.warn('Failed to get short SHA after push');
713
+ }
714
+ // ── Step 5: Clean up -- detach HEAD and delete branch ──
715
+ log('Cleaning up worktree...');
716
+ await git(worktreeDir, `checkout --detach "${defaultBranch}"`);
717
+ await git(worktreeDir, `branch -D "${branchName}"`);
718
+ if (branchName !== worktreeName && worktreeName) {
719
+ await git(worktreeDir, `branch -D "${worktreeName}"`);
720
+ }
721
+ return {
722
+ defaultBranch,
723
+ branchName: worktreeName || branchName,
724
+ commitCount,
725
+ shortSha: shortSha instanceof Error ? 'unknown' : shortSha,
726
+ };
727
+ }