kimaki 0.26.0 → 0.28.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.
- package/dist/agent-model.e2e.test.js +250 -164
- package/dist/anthropic-account-identity.js +9 -1
- package/dist/anthropic-auth-plugin.js +10 -13
- package/dist/anthropic-auth-plugin.test.js +17 -0
- package/dist/anthropic-auth-state.js +1 -0
- package/dist/bash-tool-schema-plugin.js +32 -0
- package/dist/bash-tool-schema-plugin.test.js +49 -0
- package/dist/bin.js +73 -19
- package/dist/cli-commands/maintenance.js +46 -7
- package/dist/cli-commands/project.js +11 -27
- package/dist/cli-commands/send.js +30 -1
- package/dist/cli-commands/session.js +322 -90
- package/dist/cli-commands/task.js +34 -15
- package/dist/cli-parsing.test.js +83 -4
- package/dist/cli-runner.js +44 -8
- package/dist/cli-runner.test.js +27 -0
- package/dist/cli-send-thread.e2e.test.js +9 -9
- package/dist/cli.js +32 -0
- package/dist/commands/abort.js +5 -1
- package/dist/commands/action-buttons.js +2 -1
- package/dist/commands/add-project.js +4 -7
- package/dist/commands/agent.js +164 -32
- package/dist/commands/ask-question.js +23 -33
- package/dist/commands/ask-question.test.js +43 -1
- package/dist/commands/btw.js +28 -6
- package/dist/commands/file-upload.js +2 -1
- package/dist/commands/fork-subagent.js +2 -4
- package/dist/commands/fork.js +4 -4
- package/dist/commands/last-sessions.js +17 -10
- package/dist/commands/login.js +6 -0
- package/dist/commands/merge-worktree.js +47 -14
- package/dist/commands/model-variant.js +8 -1
- package/dist/commands/model.js +16 -9
- package/dist/commands/multioauth.js +1 -0
- package/dist/commands/new-worktree.js +170 -102
- package/dist/commands/queue.js +3 -2
- package/dist/commands/resume.js +4 -4
- package/dist/commands/session.js +78 -63
- package/dist/commands/user-command.js +6 -8
- package/dist/commands/worktrees.js +5 -13
- package/dist/config.js +16 -1
- package/dist/cpu-profiler.js +211 -0
- package/dist/cpu-profiler.test.js +160 -0
- package/dist/database.js +152 -3
- package/dist/db.js +33 -0
- package/dist/db.test.js +267 -1
- package/dist/default-channel-provisioning.test.js +148 -0
- package/dist/discord-bot.js +517 -488
- package/dist/discord-command-registration.js +25 -10
- package/dist/discord-command-registration.test.js +17 -0
- package/dist/discord-utils.js +97 -5
- package/dist/discord-utils.test.js +151 -1
- package/dist/errors.js +10 -0
- package/dist/exec-async.js +17 -5
- package/dist/external-directory-permissions.e2e.test.js +11 -7
- package/dist/external-directory-project-deny.e2e.test.js +5 -3
- package/dist/external-opencode-sync.js +94 -17
- package/dist/external-opencode-sync.test.js +206 -0
- package/dist/extract-sdk-error-message.test.js +13 -0
- package/dist/file-edit-log.js +197 -0
- package/dist/file-edit-log.test.js +258 -0
- package/dist/format-tables.js +52 -0
- package/dist/format-tables.test.js +93 -0
- package/dist/gateway-proxy-reconnect.e2e.test.js +28 -8
- package/dist/gateway-proxy.e2e.test.js +18 -18
- package/dist/git-worktree-core.js +182 -17
- package/dist/interaction-handler.js +416 -398
- package/dist/ipc-tools-plugin.js +68 -3
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +122 -58
- package/dist/kimaki-opencode-plugin.js +9 -3
- package/dist/kimaki-workspace-adaptor.js +33 -10
- package/dist/logger.js +11 -1
- package/dist/markdown.js +3 -1
- package/dist/markdown.test.js +11 -3
- package/dist/message-formatting.js +181 -51
- package/dist/message-formatting.test.js +529 -22
- package/dist/message-preprocessing.js +105 -1
- package/dist/oauth-rotation-shared.js +10 -0
- package/dist/openai-auth-plugin.js +1 -0
- package/dist/openai-auth-state.js +1 -0
- package/dist/opencode-command.js +28 -0
- package/dist/opencode-command.test.js +109 -1
- package/dist/opencode-interrupt-plugin.js +4 -0
- package/dist/opencode-interrupt-plugin.test.js +24 -0
- package/dist/opencode.js +82 -15
- package/dist/project-add.test.js +62 -0
- package/dist/question-abort-clears-dropdown.e2e.test.js +89 -0
- package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +150 -0
- package/dist/question-text-before-dropdown.e2e.test.js +52 -0
- package/dist/queue-advanced-abort.e2e.test.js +8 -10
- package/dist/queue-advanced-action-buttons.e2e.test.js +8 -8
- package/dist/queue-advanced-e2e-setup.js +78 -5
- package/dist/queue-advanced-footer.e2e.test.js +60 -269
- package/dist/queue-advanced-model-switch.e2e.test.js +9 -11
- package/dist/queue-advanced-permissions-typing.e2e.test.js +15 -15
- package/dist/queue-advanced-question.e2e.test.js +32 -27
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +10 -12
- package/dist/queue-advanced-typing.e2e.test.js +7 -9
- package/dist/queue-delete-message.e2e.test.js +3 -3
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +5 -5
- package/dist/queue-interrupt-drain.e2e.test.js +122 -12
- package/dist/queue-question-select-drain.e2e.test.js +34 -132
- package/dist/runtime-lifecycle.e2e.test.js +21 -51
- package/dist/schema.js +35 -1
- package/dist/session-handler/event-stream-state.js +391 -50
- package/dist/session-handler/event-stream-state.test.js +736 -8
- package/dist/session-handler/model-utils.js +172 -5
- package/dist/session-handler/model-utils.test.js +445 -0
- package/dist/session-handler/opencode-session-event-log.js +30 -1
- package/dist/session-handler/thread-ingress.test.js +41 -0
- package/dist/session-handler/thread-runtime-state.js +2 -0
- package/dist/session-handler/thread-session-runtime-summary.test.js +121 -0
- package/dist/session-handler/thread-session-runtime.js +574 -299
- package/dist/session-search.js +123 -4
- package/dist/session-search.test.js +248 -1
- package/dist/session-sleep.test.js +224 -0
- package/dist/session-title-rename.test.js +37 -39
- package/dist/sleep.e2e.test.js +494 -0
- package/dist/startup-time.e2e.test.js +2 -1
- package/dist/store.js +3 -0
- package/dist/system-message.js +102 -16
- package/dist/system-message.test.js +192 -15
- package/dist/task-runner.js +96 -5
- package/dist/task-schedule.js +87 -1
- package/dist/task-schedule.test.js +149 -1
- package/dist/test-utils.js +17 -9
- package/dist/thinking-utils.js +10 -0
- package/dist/thinking-utils.test.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +65 -213
- package/dist/undo-redo.e2e.test.js +17 -9
- package/dist/voice-attachment.js +23 -1
- package/dist/voice-handler.js +21 -4
- package/dist/voice-message.e2e.test.js +144 -30
- package/dist/voice-routing-input.e2e.test.js +195 -0
- package/dist/voice-routing-question.e2e.test.js +125 -0
- package/dist/voice.js +28 -8
- package/dist/voice.test.js +96 -30
- package/dist/worktree-clone-isolation.e2e.test.js +188 -0
- package/dist/worktree-lifecycle.e2e.test.js +55 -36
- package/dist/worktrees.js +226 -443
- package/dist/worktrees.test.js +435 -58
- package/dist/xai-auth-plugin.js +1 -0
- package/dist/xai-auth-state.js +1 -0
- package/package.json +8 -6
- package/skills/bump-holocron-consumers/SKILL.md +137 -0
- package/skills/critique/SKILL.md +4 -4
- package/skills/errore/SKILL.md +60 -218
- package/skills/goke/SKILL.md +150 -10
- package/skills/holocron/SKILL.md +154 -17
- package/skills/npm-package/SKILL.md +55 -35
- package/skills/profano/SKILL.md +17 -2
- package/skills/spiceflow/SKILL.md +39 -3
- package/skills/strada/SKILL.md +39 -0
- package/skills/zele/SKILL.md +19 -0
- package/src/agent-model.e2e.test.ts +287 -186
- package/src/anthropic-account-identity.ts +20 -1
- package/src/anthropic-auth-plugin.test.ts +19 -0
- package/src/anthropic-auth-plugin.ts +12 -16
- package/src/anthropic-auth-state.ts +1 -0
- package/src/bash-tool-schema-plugin.test.ts +55 -0
- package/src/bash-tool-schema-plugin.ts +46 -0
- package/src/bin.ts +78 -26
- package/src/cli-commands/maintenance.ts +59 -13
- package/src/cli-commands/project.ts +14 -35
- package/src/cli-commands/send.ts +31 -1
- package/src/cli-commands/session.ts +374 -114
- package/src/cli-commands/task.ts +34 -15
- package/src/cli-parsing.test.ts +103 -4
- package/src/cli-runner.test.ts +49 -0
- package/src/cli-runner.ts +44 -7
- package/src/cli-send-thread.e2e.test.ts +9 -11
- package/src/cli.ts +53 -0
- package/src/commands/abort.ts +6 -1
- package/src/commands/action-buttons.ts +2 -1
- package/src/commands/add-project.ts +4 -8
- package/src/commands/agent.ts +205 -29
- package/src/commands/ask-question.test.ts +48 -1
- package/src/commands/ask-question.ts +23 -33
- package/src/commands/btw.ts +40 -5
- package/src/commands/file-upload.ts +2 -1
- package/src/commands/fork-subagent.ts +2 -3
- package/src/commands/fork.ts +4 -3
- package/src/commands/last-sessions.ts +10 -4
- package/src/commands/login.ts +6 -0
- package/src/commands/merge-worktree.ts +54 -16
- package/src/commands/model-variant.ts +9 -1
- package/src/commands/model.ts +20 -9
- package/src/commands/multioauth.ts +1 -0
- package/src/commands/new-worktree.ts +138 -55
- package/src/commands/queue.ts +3 -2
- package/src/commands/resume.ts +4 -3
- package/src/commands/session.ts +94 -70
- package/src/commands/user-command.ts +6 -6
- package/src/commands/worktrees.ts +5 -12
- package/src/config.ts +18 -1
- package/src/cpu-profiler.test.ts +183 -0
- package/src/cpu-profiler.ts +293 -0
- package/src/database.ts +235 -3
- package/src/db.test.ts +293 -0
- package/src/db.ts +38 -0
- package/src/default-channel-provisioning.test.ts +155 -0
- package/src/discord-bot.ts +78 -36
- package/src/discord-command-registration.test.ts +20 -0
- package/src/discord-command-registration.ts +42 -12
- package/src/discord-utils.test.ts +176 -0
- package/src/discord-utils.ts +152 -7
- package/src/errors.ts +12 -0
- package/src/exec-async.ts +20 -11
- package/src/external-directory-permissions.e2e.test.ts +11 -7
- package/src/external-directory-project-deny.e2e.test.ts +5 -3
- package/src/external-opencode-sync.test.ts +248 -0
- package/src/external-opencode-sync.ts +122 -19
- package/src/extract-sdk-error-message.test.ts +18 -0
- package/src/file-edit-log.test.ts +287 -0
- package/src/file-edit-log.ts +283 -0
- package/src/format-tables.test.ts +101 -0
- package/src/format-tables.ts +75 -0
- package/src/gateway-proxy-reconnect.e2e.test.ts +38 -8
- package/src/gateway-proxy.e2e.test.ts +18 -17
- package/src/git-worktree-core.ts +247 -20
- package/src/interaction-handler.ts +36 -13
- package/src/ipc-tools-plugin.ts +82 -5
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +188 -97
- package/src/kimaki-opencode-plugin.ts +9 -3
- package/src/kimaki-workspace-adaptor.ts +40 -11
- package/src/logger.ts +14 -4
- package/src/markdown.test.ts +13 -3
- package/src/markdown.ts +4 -1
- package/src/message-formatting.test.ts +672 -25
- package/src/message-formatting.ts +242 -53
- package/src/message-preprocessing.ts +116 -1
- package/src/oauth-rotation-shared.ts +10 -0
- package/src/openai-auth-plugin.ts +1 -0
- package/src/openai-auth-state.ts +1 -0
- package/src/opencode-command.test.ts +140 -0
- package/src/opencode-command.ts +38 -0
- package/src/opencode-interrupt-plugin.test.ts +32 -0
- package/src/opencode-interrupt-plugin.ts +3 -0
- package/src/opencode.ts +115 -15
- package/src/project-add.test.ts +80 -0
- package/src/question-abort-clears-dropdown.e2e.test.ts +109 -0
- package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +172 -0
- package/src/question-text-before-dropdown.e2e.test.ts +67 -0
- package/src/queue-advanced-abort.e2e.test.ts +8 -9
- package/src/queue-advanced-action-buttons.e2e.test.ts +8 -8
- package/src/queue-advanced-e2e-setup.ts +83 -4
- package/src/queue-advanced-footer.e2e.test.ts +67 -315
- package/src/queue-advanced-model-switch.e2e.test.ts +9 -10
- package/src/queue-advanced-permissions-typing.e2e.test.ts +15 -15
- package/src/queue-advanced-question.e2e.test.ts +36 -30
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +11 -11
- package/src/queue-advanced-typing.e2e.test.ts +8 -8
- package/src/queue-delete-message.e2e.test.ts +3 -3
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +5 -5
- package/src/queue-interrupt-drain.e2e.test.ts +133 -12
- package/src/queue-question-select-drain.e2e.test.ts +36 -161
- package/src/runtime-lifecycle.e2e.test.ts +21 -54
- package/src/schema.sql +15 -2
- package/src/schema.ts +37 -1
- package/src/session-handler/event-stream-state.test.ts +790 -7
- package/src/session-handler/event-stream-state.ts +568 -53
- package/src/session-handler/model-utils.test.ts +537 -0
- package/src/session-handler/model-utils.ts +281 -5
- package/src/session-handler/opencode-session-event-log.ts +31 -1
- package/src/session-handler/thread-ingress.test.ts +54 -0
- package/src/session-handler/thread-runtime-state.ts +3 -0
- package/src/session-handler/thread-session-runtime-summary.test.ts +169 -0
- package/src/session-handler/thread-session-runtime.ts +712 -358
- package/src/session-search.test.ts +281 -0
- package/src/session-search.ts +241 -7
- package/src/session-sleep.test.ts +294 -0
- package/src/session-title-rename.test.ts +54 -48
- package/src/sleep.e2e.test.ts +565 -0
- package/src/startup-time.e2e.test.ts +5 -1
- package/src/store.ts +22 -0
- package/src/system-message.test.ts +226 -15
- package/src/system-message.ts +130 -16
- package/src/task-runner.ts +138 -3
- package/src/task-schedule.test.ts +170 -0
- package/src/task-schedule.ts +141 -1
- package/src/test-utils.ts +21 -10
- package/src/thinking-utils.test.ts +46 -0
- package/src/thinking-utils.ts +21 -0
- package/src/thread-message-queue.e2e.test.ts +65 -245
- package/src/undo-redo.e2e.test.ts +17 -9
- package/src/voice-attachment.ts +29 -1
- package/src/voice-handler.ts +23 -4
- package/src/voice-message.e2e.test.ts +149 -29
- package/src/voice-routing-input.e2e.test.ts +203 -0
- package/src/voice-routing-question.e2e.test.ts +131 -0
- package/src/voice.test.ts +107 -29
- package/src/voice.ts +34 -6
- package/src/worktree-clone-isolation.e2e.test.ts +213 -0
- package/src/worktree-lifecycle.e2e.test.ts +55 -35
- package/src/worktrees.test.ts +505 -64
- package/src/worktrees.ts +257 -603
- package/src/xai-auth-plugin.ts +1 -0
- package/src/xai-auth-state.ts +1 -0
- package/dist/message-finish-field.e2e.test.js +0 -169
- package/dist/subagent-rate-limit-plugin.js +0 -184
- package/src/message-finish-field.e2e.test.ts +0 -196
- package/src/subagent-rate-limit-plugin.ts +0 -228
package/src/format-tables.ts
CHANGED
|
@@ -266,6 +266,11 @@ type MarkdownBlock =
|
|
|
266
266
|
| { type: 'text'; text: string }
|
|
267
267
|
| { type: 'callout'; content: string; callout: CalloutDescriptor }
|
|
268
268
|
|
|
269
|
+
type MarkdownFence = {
|
|
270
|
+
marker: '`' | '~'
|
|
271
|
+
length: number
|
|
272
|
+
}
|
|
273
|
+
|
|
269
274
|
function splitMarkdownByCallouts({
|
|
270
275
|
markdown,
|
|
271
276
|
}: {
|
|
@@ -276,9 +281,33 @@ function splitMarkdownByCallouts({
|
|
|
276
281
|
}) ?? [markdown]
|
|
277
282
|
const blocks: MarkdownBlock[] = []
|
|
278
283
|
let textBuffer = ''
|
|
284
|
+
let fence: MarkdownFence | null = null
|
|
279
285
|
|
|
280
286
|
for (let index = 0; index < lines.length; index++) {
|
|
281
287
|
const line = lines[index]!
|
|
288
|
+
if (fence) {
|
|
289
|
+
textBuffer += line
|
|
290
|
+
if (isMarkdownFenceClose({ line, fence })) fence = null
|
|
291
|
+
continue
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const openingFence = parseMarkdownFenceOpen({ line })
|
|
295
|
+
if (openingFence) {
|
|
296
|
+
fence = openingFence
|
|
297
|
+
textBuffer += line
|
|
298
|
+
continue
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const singleLineCallout = parseSingleLineCallout({ line })
|
|
302
|
+
if (singleLineCallout) {
|
|
303
|
+
if (textBuffer.length > 0) {
|
|
304
|
+
blocks.push({ type: 'text', text: textBuffer })
|
|
305
|
+
textBuffer = ''
|
|
306
|
+
}
|
|
307
|
+
blocks.push(singleLineCallout)
|
|
308
|
+
continue
|
|
309
|
+
}
|
|
310
|
+
|
|
282
311
|
const callout = parseCalloutOpenLine({ line })
|
|
283
312
|
if (!callout) {
|
|
284
313
|
textBuffer += line
|
|
@@ -503,6 +532,52 @@ function collectCalloutBodyFromLines({
|
|
|
503
532
|
return new Error('Unclosed <callout> block')
|
|
504
533
|
}
|
|
505
534
|
|
|
535
|
+
function parseMarkdownFenceOpen({
|
|
536
|
+
line,
|
|
537
|
+
}: {
|
|
538
|
+
line: string
|
|
539
|
+
}): MarkdownFence | null {
|
|
540
|
+
const marker = line.match(/^ {0,3}(`{3,}|~{3,})/)?.[1]
|
|
541
|
+
if (!marker) return null
|
|
542
|
+
return {
|
|
543
|
+
marker: marker.startsWith('`') ? '`' : '~',
|
|
544
|
+
length: marker.length,
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function isMarkdownFenceClose({
|
|
549
|
+
line,
|
|
550
|
+
fence,
|
|
551
|
+
}: {
|
|
552
|
+
line: string
|
|
553
|
+
fence: MarkdownFence
|
|
554
|
+
}): boolean {
|
|
555
|
+
const marker = line.trim()
|
|
556
|
+
return marker.length >= fence.length &&
|
|
557
|
+
[...marker].every((character) => character === fence.marker)
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function parseSingleLineCallout({
|
|
561
|
+
line,
|
|
562
|
+
}: {
|
|
563
|
+
line: string
|
|
564
|
+
}): Extract<MarkdownBlock, { type: 'callout' }> | null {
|
|
565
|
+
const match = line.trim().match(/^(<callout(?:\s+[^>]*)?>)(.*?)<\/callout>$/i)
|
|
566
|
+
if (!match) return null
|
|
567
|
+
|
|
568
|
+
const content = match[2]!
|
|
569
|
+
if (/<\/?callout(?:\s|>)/i.test(content)) return null
|
|
570
|
+
|
|
571
|
+
const callout = parseCalloutOpenLine({ line: match[1]! })
|
|
572
|
+
if (!callout) return null
|
|
573
|
+
|
|
574
|
+
return {
|
|
575
|
+
type: 'callout',
|
|
576
|
+
content,
|
|
577
|
+
callout,
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
506
581
|
function parseCalloutOpenLine({
|
|
507
582
|
line,
|
|
508
583
|
}: {
|
|
@@ -318,6 +318,34 @@ function waitForClientReady({
|
|
|
318
318
|
})
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
+
function waitForGatewayUnavailable({
|
|
322
|
+
client,
|
|
323
|
+
timeoutMs = 10_000,
|
|
324
|
+
}: {
|
|
325
|
+
client: Client
|
|
326
|
+
timeoutMs?: number
|
|
327
|
+
}): Promise<void> {
|
|
328
|
+
return new Promise<void>((resolve, reject) => {
|
|
329
|
+
let reconnectAttempts = 0
|
|
330
|
+
const timeout = setTimeout(() => {
|
|
331
|
+
client.off(Events.ShardReconnecting, onReconnecting)
|
|
332
|
+
reject(
|
|
333
|
+
new Error(
|
|
334
|
+
`Client did not observe gateway unavailability within ${timeoutMs}ms`,
|
|
335
|
+
),
|
|
336
|
+
)
|
|
337
|
+
}, timeoutMs)
|
|
338
|
+
const onReconnecting = () => {
|
|
339
|
+
reconnectAttempts += 1
|
|
340
|
+
if (reconnectAttempts < 2) return
|
|
341
|
+
clearTimeout(timeout)
|
|
342
|
+
client.off(Events.ShardReconnecting, onReconnecting)
|
|
343
|
+
resolve()
|
|
344
|
+
}
|
|
345
|
+
client.on(Events.ShardReconnecting, onReconnecting)
|
|
346
|
+
})
|
|
347
|
+
}
|
|
348
|
+
|
|
321
349
|
function waitForReconnection({
|
|
322
350
|
client,
|
|
323
351
|
events,
|
|
@@ -336,14 +364,14 @@ function waitForReconnection({
|
|
|
336
364
|
resolve(false)
|
|
337
365
|
}, timeoutMs)
|
|
338
366
|
|
|
339
|
-
|
|
367
|
+
const finish = () => {
|
|
340
368
|
clearTimeout(timeout)
|
|
369
|
+
client.off(Events.ShardReady, finish)
|
|
370
|
+
client.off(Events.ShardResume, finish)
|
|
341
371
|
resolve(true)
|
|
342
|
-
}
|
|
343
|
-
client.on(Events.
|
|
344
|
-
|
|
345
|
-
resolve(true)
|
|
346
|
-
})
|
|
372
|
+
}
|
|
373
|
+
client.on(Events.ShardReady, finish)
|
|
374
|
+
client.on(Events.ShardResume, finish)
|
|
347
375
|
})
|
|
348
376
|
}
|
|
349
377
|
|
|
@@ -419,15 +447,17 @@ describeLocal('gateway-proxy reconnection (local binary)', () => {
|
|
|
419
447
|
|
|
420
448
|
// Kill proxy
|
|
421
449
|
const firstProxy = proxyProcess
|
|
450
|
+
const gatewayUnavailable = waitForGatewayUnavailable({ client })
|
|
422
451
|
await killProxy(proxyProcess)
|
|
423
|
-
await
|
|
452
|
+
await gatewayUnavailable
|
|
424
453
|
|
|
425
454
|
// Restart proxy on same port
|
|
455
|
+
const reconnection = waitForReconnection({ client, events, label: 'local' })
|
|
426
456
|
proxyProcess = startProxy(proxyOpts)
|
|
427
457
|
await waitForProxyReady({ port: proxyPort })
|
|
428
458
|
console.log('[local] Proxy restarted')
|
|
429
459
|
|
|
430
|
-
const reconnected = await
|
|
460
|
+
const reconnected = await reconnection
|
|
431
461
|
console.log('[local] All events:', events)
|
|
432
462
|
if (!reconnected) {
|
|
433
463
|
dumpProxyLogs(firstProxy)
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
chooseLockPort,
|
|
40
40
|
cleanupTestSessions,
|
|
41
41
|
initTestGitRepo,
|
|
42
|
+
getMessageVisibleText,
|
|
42
43
|
waitForFooterMessage,
|
|
43
44
|
} from './test-utils.js'
|
|
44
45
|
import { stopOpencodeServer } from './opencode.js'
|
|
@@ -419,12 +420,12 @@ describeIf('gateway-proxy e2e', () => {
|
|
|
419
420
|
"--- from: user (proxy-tester)
|
|
420
421
|
hello from gateway proxy test
|
|
421
422
|
--- from: assistant (TestBot)
|
|
422
|
-
*using deterministic-provider/deterministic-v2*
|
|
423
|
-
|
|
424
|
-
*project ⋅ main ⋅
|
|
423
|
+
> *using deterministic-provider/deterministic-v2*
|
|
424
|
+
> gateway-proxy-reply
|
|
425
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@900000000000000001>"
|
|
425
426
|
`)
|
|
426
427
|
expect(reply).toBeDefined()
|
|
427
|
-
expect(reply.
|
|
428
|
+
expect(getMessageVisibleText(reply).trim().length).toBeGreaterThan(0)
|
|
428
429
|
},
|
|
429
430
|
15_000,
|
|
430
431
|
)
|
|
@@ -455,17 +456,17 @@ describeIf('gateway-proxy e2e', () => {
|
|
|
455
456
|
"--- from: user (proxy-tester)
|
|
456
457
|
hello from gateway proxy test
|
|
457
458
|
--- from: assistant (TestBot)
|
|
458
|
-
*using deterministic-provider/deterministic-v2*
|
|
459
|
-
|
|
460
|
-
*project ⋅ main ⋅
|
|
459
|
+
> *using deterministic-provider/deterministic-v2*
|
|
460
|
+
> gateway-proxy-reply
|
|
461
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@900000000000000001>
|
|
461
462
|
--- from: user (proxy-tester)
|
|
462
463
|
follow up through proxy
|
|
463
464
|
--- from: assistant (TestBot)
|
|
464
|
-
|
|
465
|
-
*project ⋅ main ⋅
|
|
465
|
+
> gateway-proxy-reply
|
|
466
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@900000000000000001>"
|
|
466
467
|
`)
|
|
467
468
|
expect(reply).toBeDefined()
|
|
468
|
-
expect(reply.
|
|
469
|
+
expect(getMessageVisibleText(reply).trim().length).toBeGreaterThan(0)
|
|
469
470
|
},
|
|
470
471
|
15_000,
|
|
471
472
|
)
|
|
@@ -496,14 +497,14 @@ describeIf('gateway-proxy e2e', () => {
|
|
|
496
497
|
"--- from: user (proxy-tester)
|
|
497
498
|
hello from gateway proxy test
|
|
498
499
|
--- from: assistant (TestBot)
|
|
499
|
-
*using deterministic-provider/deterministic-v2*
|
|
500
|
-
|
|
501
|
-
*project ⋅ main ⋅
|
|
500
|
+
> *using deterministic-provider/deterministic-v2*
|
|
501
|
+
> gateway-proxy-reply
|
|
502
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@900000000000000001>
|
|
502
503
|
--- from: user (proxy-tester)
|
|
503
504
|
follow up through proxy
|
|
504
505
|
--- from: assistant (TestBot)
|
|
505
|
-
|
|
506
|
-
*project ⋅ main ⋅
|
|
506
|
+
> gateway-proxy-reply
|
|
507
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@900000000000000001>
|
|
507
508
|
--- from: user (proxy-tester)
|
|
508
509
|
!echo proxy-shell-test
|
|
509
510
|
--- from: assistant (TestBot)
|
|
@@ -541,10 +542,10 @@ describeIf('gateway-proxy e2e', () => {
|
|
|
541
542
|
"--- from: user (proxy-tester)
|
|
542
543
|
second message through proxy
|
|
543
544
|
--- from: assistant (TestBot)
|
|
544
|
-
*using deterministic-provider/deterministic-v2*"
|
|
545
|
+
> *using deterministic-provider/deterministic-v2*"
|
|
545
546
|
`)
|
|
546
547
|
expect(reply).toBeDefined()
|
|
547
|
-
expect(reply.
|
|
548
|
+
expect(getMessageVisibleText(reply).trim().length).toBeGreaterThan(0)
|
|
548
549
|
},
|
|
549
550
|
15_000,
|
|
550
551
|
)
|
package/src/git-worktree-core.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Plugin-safe
|
|
1
|
+
// Plugin-safe Git worktree creation, identity validation, and removal primitives.
|
|
2
2
|
// This module must NOT import config.ts, logger.ts, or any module that
|
|
3
3
|
// transitively pulls them in. It is used by both:
|
|
4
4
|
// - worktrees.ts (bot process — wraps with kimaki logger + config)
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import fs from 'node:fs'
|
|
10
10
|
import path from 'node:path'
|
|
11
|
+
import * as errore from 'errore'
|
|
11
12
|
import { execAsync } from './exec-async.js'
|
|
12
13
|
|
|
13
14
|
const SUBMODULE_INIT_TIMEOUT_MS = 20 * 60_000
|
|
@@ -27,6 +28,13 @@ export type WorktreeLog = {
|
|
|
27
28
|
error(message: string): void
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
export const KIMAKI_WORKTREE_ADAPTER_TYPE = 'kimaki-worktree'
|
|
32
|
+
|
|
33
|
+
export class WorktreeIdentityError extends errore.createTaggedError({
|
|
34
|
+
name: 'WorktreeIdentityError',
|
|
35
|
+
message: 'Created worktree does not match the requested checkout: $reason',
|
|
36
|
+
}) {}
|
|
37
|
+
|
|
30
38
|
const silentLog: WorktreeLog = {
|
|
31
39
|
info() {},
|
|
32
40
|
warn() {},
|
|
@@ -136,7 +144,15 @@ async function hasSubmoduleGitMetadata(directory: string): Promise<boolean> {
|
|
|
136
144
|
* Remove broken submodule stubs created by `git worktree add`.
|
|
137
145
|
* git worktree creates .git files pointing to incomplete gitdirs (missing HEAD).
|
|
138
146
|
*/
|
|
139
|
-
async function removeBrokenSubmoduleStubs(
|
|
147
|
+
async function removeBrokenSubmoduleStubs({
|
|
148
|
+
directory,
|
|
149
|
+
expectedCommonGitDirectory,
|
|
150
|
+
log,
|
|
151
|
+
}: {
|
|
152
|
+
directory: string
|
|
153
|
+
expectedCommonGitDirectory: string
|
|
154
|
+
log: WorktreeLog
|
|
155
|
+
}): Promise<void> {
|
|
140
156
|
const submodulePaths = await getSubmodulePaths(directory, log)
|
|
141
157
|
for (const subPath of submodulePaths) {
|
|
142
158
|
const fullPath = path.join(directory, subPath)
|
|
@@ -150,7 +166,10 @@ async function removeBrokenSubmoduleStubs(directory: string, log: WorktreeLog):
|
|
|
150
166
|
const gitdir = path.resolve(fullPath, match[1].trim())
|
|
151
167
|
const headFile = path.join(gitdir, 'HEAD')
|
|
152
168
|
const headExists = await fs.promises.access(headFile).then(() => true).catch(() => false)
|
|
153
|
-
|
|
169
|
+
const relativeGitdir = path.relative(expectedCommonGitDirectory, gitdir)
|
|
170
|
+
const pointsOutsideExpectedRepository =
|
|
171
|
+
relativeGitdir === '..' || relativeGitdir.startsWith(`..${path.sep}`)
|
|
172
|
+
if (!headExists || pointsOutsideExpectedRepository) {
|
|
154
173
|
log.info(`Removing broken submodule stub: ${subPath}`)
|
|
155
174
|
await fs.promises.rm(fullPath, { recursive: true, force: true })
|
|
156
175
|
}
|
|
@@ -235,17 +254,36 @@ async function initializeSubmodulesWithLocalReferences({
|
|
|
235
254
|
}
|
|
236
255
|
}
|
|
237
256
|
|
|
238
|
-
async function validateSubmodulePointers(
|
|
239
|
-
|
|
257
|
+
async function validateSubmodulePointers({
|
|
258
|
+
directory,
|
|
259
|
+
expectedCommonGitDirectory,
|
|
260
|
+
log,
|
|
261
|
+
}: {
|
|
262
|
+
directory: string
|
|
263
|
+
expectedCommonGitDirectory: string
|
|
264
|
+
log: WorktreeLog
|
|
265
|
+
}): Promise<void | Error> {
|
|
266
|
+
const submoduleConfigs = await readSubmoduleConfigs(directory)
|
|
267
|
+
if (submoduleConfigs instanceof Error) {
|
|
268
|
+
log.warn(submoduleConfigs.message)
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
const submodulePaths = submoduleConfigs.map(({ path: submodulePath }) => {
|
|
272
|
+
return submodulePath
|
|
273
|
+
})
|
|
240
274
|
if (submodulePaths.length === 0) return
|
|
241
275
|
|
|
242
276
|
const issues: string[] = []
|
|
277
|
+
const foreignGitdirs: string[] = []
|
|
243
278
|
await Promise.all(
|
|
244
279
|
submodulePaths.map(async (subPath) => {
|
|
245
280
|
const gitFile = path.join(directory, subPath, '.git')
|
|
246
281
|
try {
|
|
247
282
|
const stat = await fs.promises.stat(gitFile)
|
|
248
|
-
if (!stat.isFile())
|
|
283
|
+
if (!stat.isFile()) {
|
|
284
|
+
issues.push(`${subPath}: .git is not a file`)
|
|
285
|
+
return
|
|
286
|
+
}
|
|
249
287
|
const content = await fs.promises.readFile(gitFile, 'utf-8')
|
|
250
288
|
const match = content.match(/^gitdir:\s*(.+)$/m)
|
|
251
289
|
const gitdir = match?.[1]?.trim()
|
|
@@ -254,19 +292,30 @@ async function validateSubmodulePointers(directory: string, log: WorktreeLog): P
|
|
|
254
292
|
return
|
|
255
293
|
}
|
|
256
294
|
const resolvedGitdir = path.resolve(path.join(directory, subPath), gitdir)
|
|
295
|
+
const relativeGitdir = path.relative(expectedCommonGitDirectory, resolvedGitdir)
|
|
296
|
+
if (relativeGitdir === '..' || relativeGitdir.startsWith(`..${path.sep}`)) {
|
|
297
|
+
foreignGitdirs.push(`${subPath}: ${resolvedGitdir}`)
|
|
298
|
+
return
|
|
299
|
+
}
|
|
257
300
|
const headExists = await fs.promises.access(path.join(resolvedGitdir, 'HEAD')).then(() => true).catch(() => false)
|
|
258
301
|
if (!headExists) {
|
|
259
302
|
issues.push(`${subPath}: gitdir missing HEAD (${resolvedGitdir})`)
|
|
303
|
+
return
|
|
260
304
|
}
|
|
261
|
-
} catch {
|
|
262
|
-
|
|
305
|
+
} catch (e) {
|
|
306
|
+
issues.push(
|
|
307
|
+
`${subPath}: failed to validate .git (${e instanceof Error ? e.message : String(e)})`,
|
|
308
|
+
)
|
|
263
309
|
}
|
|
264
310
|
}),
|
|
265
311
|
)
|
|
266
312
|
|
|
267
|
-
if (
|
|
268
|
-
|
|
313
|
+
if (foreignGitdirs.length > 0) {
|
|
314
|
+
return new Error(
|
|
315
|
+
`Submodule gitdir belongs to another repository: ${foreignGitdirs.join('; ')}`,
|
|
316
|
+
)
|
|
269
317
|
}
|
|
318
|
+
if (issues.length > 0) log.warn(`Submodule validation issues: ${issues.join('; ')}`)
|
|
270
319
|
}
|
|
271
320
|
|
|
272
321
|
function detectInstallCommand(directory: string): string | null {
|
|
@@ -297,6 +346,120 @@ export type WorktreeResult = {
|
|
|
297
346
|
branch: string
|
|
298
347
|
}
|
|
299
348
|
|
|
349
|
+
async function canonicalizePath(value: string) {
|
|
350
|
+
return await fs.promises.realpath(value).catch(() => path.resolve(value))
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export async function resolveGitCommit({
|
|
354
|
+
directory,
|
|
355
|
+
ref,
|
|
356
|
+
}: {
|
|
357
|
+
directory: string
|
|
358
|
+
ref: string
|
|
359
|
+
}): Promise<string | Error> {
|
|
360
|
+
const result = await execAsync(`git rev-parse --verify ${JSON.stringify(`${ref}^{commit}`)}`, {
|
|
361
|
+
cwd: directory,
|
|
362
|
+
timeout: 10_000,
|
|
363
|
+
}).catch((e) => new Error(`Failed to resolve Git commit ${ref}`, { cause: e }))
|
|
364
|
+
if (result instanceof Error) return result
|
|
365
|
+
const commit = result.stdout.trim()
|
|
366
|
+
if (!/^[0-9a-f]{40}$/i.test(commit)) {
|
|
367
|
+
return new Error(`Git returned an invalid commit for ${ref}: ${commit}`)
|
|
368
|
+
}
|
|
369
|
+
return commit
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export async function resolveGitCommonDirectory({
|
|
373
|
+
directory,
|
|
374
|
+
}: {
|
|
375
|
+
directory: string
|
|
376
|
+
}): Promise<string | Error> {
|
|
377
|
+
const result = await execAsync('git rev-parse --git-common-dir', {
|
|
378
|
+
cwd: directory,
|
|
379
|
+
timeout: 10_000,
|
|
380
|
+
}).catch(
|
|
381
|
+
(e) => new Error(`Failed to resolve Git common directory for ${directory}`, { cause: e }),
|
|
382
|
+
)
|
|
383
|
+
if (result instanceof Error) return result
|
|
384
|
+
const commonDirectory = path.isAbsolute(result.stdout.trim())
|
|
385
|
+
? result.stdout.trim()
|
|
386
|
+
: path.resolve(directory, result.stdout.trim())
|
|
387
|
+
return canonicalizePath(commonDirectory)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function resolveGitDirectory({
|
|
391
|
+
directory,
|
|
392
|
+
}: {
|
|
393
|
+
directory: string
|
|
394
|
+
}): Promise<string | Error> {
|
|
395
|
+
const result = await execAsync('git rev-parse --absolute-git-dir', {
|
|
396
|
+
cwd: directory,
|
|
397
|
+
timeout: 10_000,
|
|
398
|
+
}).catch(
|
|
399
|
+
(e) => new Error(`Failed to resolve Git directory for ${directory}`, { cause: e }),
|
|
400
|
+
)
|
|
401
|
+
if (result instanceof Error) return result
|
|
402
|
+
return canonicalizePath(result.stdout.trim())
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export async function validateWorktreeIdentity({
|
|
406
|
+
projectDirectory,
|
|
407
|
+
worktreeDirectory,
|
|
408
|
+
baseCommit,
|
|
409
|
+
}: {
|
|
410
|
+
projectDirectory: string
|
|
411
|
+
baseCommit: string
|
|
412
|
+
worktreeDirectory: string
|
|
413
|
+
}): Promise<{ commonGitDirectory: string; headCommit: string } | WorktreeIdentityError> {
|
|
414
|
+
const [sourceCommonGitDirectory, commonGitDirectory, gitDirectory, headCommit] =
|
|
415
|
+
await Promise.all([
|
|
416
|
+
resolveGitCommonDirectory({ directory: projectDirectory }),
|
|
417
|
+
resolveGitCommonDirectory({ directory: worktreeDirectory }),
|
|
418
|
+
resolveGitDirectory({ directory: worktreeDirectory }),
|
|
419
|
+
resolveGitCommit({ directory: worktreeDirectory, ref: 'HEAD' }),
|
|
420
|
+
])
|
|
421
|
+
if (sourceCommonGitDirectory instanceof Error) {
|
|
422
|
+
return new WorktreeIdentityError({
|
|
423
|
+
reason: sourceCommonGitDirectory.message,
|
|
424
|
+
cause: sourceCommonGitDirectory,
|
|
425
|
+
})
|
|
426
|
+
}
|
|
427
|
+
if (commonGitDirectory instanceof Error) {
|
|
428
|
+
return new WorktreeIdentityError({
|
|
429
|
+
reason: commonGitDirectory.message,
|
|
430
|
+
cause: commonGitDirectory,
|
|
431
|
+
})
|
|
432
|
+
}
|
|
433
|
+
if (headCommit instanceof Error) {
|
|
434
|
+
return new WorktreeIdentityError({
|
|
435
|
+
reason: headCommit.message,
|
|
436
|
+
cause: headCommit,
|
|
437
|
+
})
|
|
438
|
+
}
|
|
439
|
+
if (gitDirectory instanceof Error) {
|
|
440
|
+
return new WorktreeIdentityError({
|
|
441
|
+
reason: gitDirectory.message,
|
|
442
|
+
cause: gitDirectory,
|
|
443
|
+
})
|
|
444
|
+
}
|
|
445
|
+
if (commonGitDirectory !== sourceCommonGitDirectory) {
|
|
446
|
+
return new WorktreeIdentityError({
|
|
447
|
+
reason: `expected common directory ${sourceCommonGitDirectory}, received ${commonGitDirectory}`,
|
|
448
|
+
})
|
|
449
|
+
}
|
|
450
|
+
if (gitDirectory === commonGitDirectory) {
|
|
451
|
+
return new WorktreeIdentityError({
|
|
452
|
+
reason: `${worktreeDirectory} is not a linked worktree`,
|
|
453
|
+
})
|
|
454
|
+
}
|
|
455
|
+
if (headCommit !== baseCommit) {
|
|
456
|
+
return new WorktreeIdentityError({
|
|
457
|
+
reason: `expected HEAD ${baseCommit}, received ${headCommit}`,
|
|
458
|
+
})
|
|
459
|
+
}
|
|
460
|
+
return { commonGitDirectory, headCommit }
|
|
461
|
+
}
|
|
462
|
+
|
|
300
463
|
/**
|
|
301
464
|
* Create a git worktree with full submodule initialization and dependency install.
|
|
302
465
|
* Plugin-safe: no config.ts or logger.ts imports. Logging is done via the `log`
|
|
@@ -306,24 +469,27 @@ export async function createWorktreeCore({
|
|
|
306
469
|
projectDirectory,
|
|
307
470
|
targetDirectory,
|
|
308
471
|
branchName,
|
|
309
|
-
|
|
472
|
+
baseCommit,
|
|
310
473
|
onProgress,
|
|
311
474
|
log = silentLog,
|
|
312
475
|
}: {
|
|
313
476
|
projectDirectory: string
|
|
314
477
|
targetDirectory: string
|
|
315
478
|
branchName: string
|
|
316
|
-
|
|
479
|
+
baseCommit: string
|
|
317
480
|
onProgress?: (phase: string) => void
|
|
318
481
|
log?: WorktreeLog
|
|
319
482
|
}): Promise<WorktreeResult | Error> {
|
|
320
483
|
if (fs.existsSync(targetDirectory)) {
|
|
321
484
|
return new Error(`Worktree directory already exists: ${targetDirectory}`)
|
|
322
485
|
}
|
|
486
|
+
const commonGitDirectory = await resolveGitCommonDirectory({
|
|
487
|
+
directory: projectDirectory,
|
|
488
|
+
})
|
|
489
|
+
if (commonGitDirectory instanceof Error) return commonGitDirectory
|
|
323
490
|
await fs.promises.mkdir(path.dirname(targetDirectory), { recursive: true })
|
|
324
491
|
|
|
325
|
-
const
|
|
326
|
-
const createCmd = `git worktree add ${JSON.stringify(targetDirectory)} -B ${JSON.stringify(branchName)} ${JSON.stringify(targetRef)}`
|
|
492
|
+
const createCmd = `git worktree add ${JSON.stringify(targetDirectory)} -B ${JSON.stringify(branchName)} ${JSON.stringify(baseCommit)}`
|
|
327
493
|
const createResult = await execAsync(createCmd, {
|
|
328
494
|
cwd: projectDirectory,
|
|
329
495
|
timeout: SUBMODULE_INIT_TIMEOUT_MS,
|
|
@@ -333,7 +499,11 @@ export async function createWorktreeCore({
|
|
|
333
499
|
if (createResult instanceof Error) return createResult
|
|
334
500
|
|
|
335
501
|
// Remove broken submodule stubs before init
|
|
336
|
-
await removeBrokenSubmoduleStubs(
|
|
502
|
+
await removeBrokenSubmoduleStubs({
|
|
503
|
+
directory: targetDirectory,
|
|
504
|
+
expectedCommonGitDirectory: commonGitDirectory,
|
|
505
|
+
log,
|
|
506
|
+
})
|
|
337
507
|
|
|
338
508
|
// Init submodules with local --reference directories
|
|
339
509
|
log.info(`Initializing submodules in ${targetDirectory}`)
|
|
@@ -348,8 +518,25 @@ export async function createWorktreeCore({
|
|
|
348
518
|
log.info(`Submodules initialized in ${targetDirectory}`)
|
|
349
519
|
}
|
|
350
520
|
|
|
351
|
-
//
|
|
352
|
-
await validateSubmodulePointers(
|
|
521
|
+
// Invalid submodule pointers can route later Git commands into another clone.
|
|
522
|
+
const submoduleValidation = await validateSubmodulePointers({
|
|
523
|
+
directory: targetDirectory,
|
|
524
|
+
expectedCommonGitDirectory: commonGitDirectory,
|
|
525
|
+
log,
|
|
526
|
+
})
|
|
527
|
+
if (submoduleValidation instanceof Error) {
|
|
528
|
+
const cleanupResult = await removeWorktreeCore({
|
|
529
|
+
projectDirectory,
|
|
530
|
+
worktreeDirectory: targetDirectory,
|
|
531
|
+
branchName,
|
|
532
|
+
})
|
|
533
|
+
if (cleanupResult instanceof Error) {
|
|
534
|
+
return new Error(`${submoduleValidation.message}; cleanup failed: ${cleanupResult.message}`, {
|
|
535
|
+
cause: submoduleValidation,
|
|
536
|
+
})
|
|
537
|
+
}
|
|
538
|
+
return submoduleValidation
|
|
539
|
+
}
|
|
353
540
|
|
|
354
541
|
// Dependency install (non-fatal)
|
|
355
542
|
onProgress?.('Installing dependencies...')
|
|
@@ -381,9 +568,49 @@ export async function removeWorktreeCore({
|
|
|
381
568
|
if (removeResult instanceof Error) return removeResult
|
|
382
569
|
|
|
383
570
|
if (branchName) {
|
|
384
|
-
await execAsync(
|
|
385
|
-
`git branch
|
|
571
|
+
const listResult = await execAsync(
|
|
572
|
+
`git branch --list --format=${JSON.stringify('%(refname:short)')} ${JSON.stringify(branchName)}`,
|
|
386
573
|
{ cwd: projectDirectory, timeout: 10_000 },
|
|
387
|
-
).catch(() =>
|
|
574
|
+
).catch((e) =>
|
|
575
|
+
new Error(`git branch list failed: ${formatCommandError(e)}`, { cause: e }),
|
|
576
|
+
)
|
|
577
|
+
if (listResult instanceof Error) return listResult
|
|
578
|
+
if (listResult.stdout.trim() === branchName) {
|
|
579
|
+
const deleteResult = await execAsync(
|
|
580
|
+
`git branch -D ${JSON.stringify(branchName)}`,
|
|
581
|
+
{ cwd: projectDirectory, timeout: 10_000 },
|
|
582
|
+
).catch((e) =>
|
|
583
|
+
new Error(`git branch delete failed: ${formatCommandError(e)}`, { cause: e }),
|
|
584
|
+
)
|
|
585
|
+
if (deleteResult instanceof Error) return deleteResult
|
|
586
|
+
}
|
|
388
587
|
}
|
|
389
588
|
}
|
|
589
|
+
|
|
590
|
+
export async function removeWorktreeFromOwnRepository({
|
|
591
|
+
worktreeDirectory,
|
|
592
|
+
branchName,
|
|
593
|
+
}: {
|
|
594
|
+
worktreeDirectory: string
|
|
595
|
+
branchName: string
|
|
596
|
+
}): Promise<void | Error> {
|
|
597
|
+
if (!fs.existsSync(worktreeDirectory)) return
|
|
598
|
+
const listResult = await execAsync('git worktree list --porcelain', {
|
|
599
|
+
cwd: worktreeDirectory,
|
|
600
|
+
timeout: 10_000,
|
|
601
|
+
}).catch(
|
|
602
|
+
(e) => new Error('Failed to locate the worktree repository', { cause: e }),
|
|
603
|
+
)
|
|
604
|
+
if (listResult instanceof Error) return listResult
|
|
605
|
+
const mainWorktreeLine = listResult.stdout
|
|
606
|
+
.split('\n')
|
|
607
|
+
.find((line) => line.startsWith('worktree '))
|
|
608
|
+
if (!mainWorktreeLine) {
|
|
609
|
+
return new Error('Git returned no main worktree during cleanup')
|
|
610
|
+
}
|
|
611
|
+
return removeWorktreeCore({
|
|
612
|
+
projectDirectory: mainWorktreeLine.slice('worktree '.length),
|
|
613
|
+
worktreeDirectory,
|
|
614
|
+
branchName,
|
|
615
|
+
})
|
|
616
|
+
}
|