tailii-host 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (348) hide show
  1. package/README.md +27 -10
  2. package/dist/{broker.d.ts → approval/broker.d.ts} +9 -5
  3. package/dist/{broker.js → approval/broker.js} +42 -6
  4. package/dist/approval/broker.js.map +1 -0
  5. package/dist/{hook.d.ts → approval/hook.d.ts} +48 -7
  6. package/dist/{hook.js → approval/hook.js} +359 -50
  7. package/dist/approval/hook.js.map +1 -0
  8. package/dist/backend/herdr.d.ts +226 -0
  9. package/dist/backend/herdr.js +749 -0
  10. package/dist/backend/herdr.js.map +1 -0
  11. package/dist/backend/sessionBackend.d.ts +81 -0
  12. package/dist/backend/sessionBackend.js +114 -0
  13. package/dist/backend/sessionBackend.js.map +1 -0
  14. package/dist/backend/tmux.d.ts +125 -0
  15. package/dist/backend/tmux.js +392 -0
  16. package/dist/backend/tmux.js.map +1 -0
  17. package/dist/{chatTailController.d.ts → chat/chatTailController.d.ts} +29 -6
  18. package/dist/{chatTailController.js → chat/chatTailController.js} +94 -18
  19. package/dist/chat/chatTailController.js.map +1 -0
  20. package/dist/{imageService.d.ts → chat/imageService.d.ts} +12 -1
  21. package/dist/{imageService.js → chat/imageService.js} +39 -11
  22. package/dist/chat/imageService.js.map +1 -0
  23. package/dist/chat/remoteQuestionMonitor.d.ts +27 -0
  24. package/dist/chat/remoteQuestionMonitor.js +128 -0
  25. package/dist/chat/remoteQuestionMonitor.js.map +1 -0
  26. package/dist/chat/subagentTailer.d.ts +30 -0
  27. package/dist/chat/subagentTailer.js +704 -0
  28. package/dist/chat/subagentTailer.js.map +1 -0
  29. package/dist/chat/subagentTranscript.d.ts +20 -0
  30. package/dist/chat/subagentTranscript.js +273 -0
  31. package/dist/chat/subagentTranscript.js.map +1 -0
  32. package/dist/{transcriptTailer.d.ts → chat/transcriptTailer.d.ts} +12 -1
  33. package/dist/{transcriptTailer.js → chat/transcriptTailer.js} +183 -10
  34. package/dist/chat/transcriptTailer.js.map +1 -0
  35. package/dist/cli.js +15 -10
  36. package/dist/cli.js.map +1 -1
  37. package/dist/codex/codexAccountUsage.d.ts +22 -0
  38. package/dist/codex/codexAccountUsage.js +128 -0
  39. package/dist/codex/codexAccountUsage.js.map +1 -0
  40. package/dist/codex/codexAppServer.d.ts +166 -0
  41. package/dist/codex/codexAppServer.js +1297 -0
  42. package/dist/codex/codexAppServer.js.map +1 -0
  43. package/dist/codex/codexNativeTurnController.d.ts +153 -0
  44. package/dist/codex/codexNativeTurnController.js +673 -0
  45. package/dist/codex/codexNativeTurnController.js.map +1 -0
  46. package/dist/codex/codexRolloutTailer.d.ts +100 -0
  47. package/dist/codex/codexRolloutTailer.js +568 -0
  48. package/dist/codex/codexRolloutTailer.js.map +1 -0
  49. package/dist/{codexSessionStore.d.ts → codex/codexSessionStore.d.ts} +10 -2
  50. package/dist/{codexSessionStore.js → codex/codexSessionStore.js} +184 -15
  51. package/dist/codex/codexSessionStore.js.map +1 -0
  52. package/dist/codex/codexSubagentTracker.d.ts +21 -0
  53. package/dist/codex/codexSubagentTracker.js +281 -0
  54. package/dist/codex/codexSubagentTracker.js.map +1 -0
  55. package/dist/codex/codexToolActivity.d.ts +69 -0
  56. package/dist/codex/codexToolActivity.js +425 -0
  57. package/dist/codex/codexToolActivity.js.map +1 -0
  58. package/dist/{codexUsage.d.ts → codex/codexUsage.d.ts} +1 -1
  59. package/dist/{codexUsage.js → codex/codexUsage.js} +19 -26
  60. package/dist/codex/codexUsage.js.map +1 -0
  61. package/dist/commands/doctor.d.ts +65 -0
  62. package/dist/commands/doctor.js +425 -0
  63. package/dist/commands/doctor.js.map +1 -0
  64. package/dist/{hookSettings.d.ts → commands/hookSettings.d.ts} +14 -1
  65. package/dist/{hookSettings.js → commands/hookSettings.js} +96 -12
  66. package/dist/commands/hookSettings.js.map +1 -0
  67. package/dist/{kick.js → commands/kick.js} +16 -1
  68. package/dist/commands/kick.js.map +1 -0
  69. package/dist/{launch.d.ts → commands/launch.d.ts} +67 -5
  70. package/dist/commands/launch.js +716 -0
  71. package/dist/commands/launch.js.map +1 -0
  72. package/dist/commands/legacyHomeMigration.js.map +1 -0
  73. package/dist/{pairingCode.d.ts → commands/pairingCode.d.ts} +29 -2
  74. package/dist/{pairingCode.js → commands/pairingCode.js} +120 -11
  75. package/dist/commands/pairingCode.js.map +1 -0
  76. package/dist/{pushTokenCommand.d.ts → commands/pushTokenCommand.d.ts} +1 -1
  77. package/dist/{pushTokenCommand.js → commands/pushTokenCommand.js} +2 -2
  78. package/dist/commands/pushTokenCommand.js.map +1 -0
  79. package/dist/{setup.d.ts → commands/setup.d.ts} +17 -6
  80. package/dist/{setup.js → commands/setup.js} +133 -75
  81. package/dist/commands/setup.js.map +1 -0
  82. package/dist/engine/context.d.ts +145 -0
  83. package/dist/engine/context.js +184 -0
  84. package/dist/engine/context.js.map +1 -0
  85. package/dist/engine/engine.d.ts +121 -0
  86. package/dist/engine/engine.js +799 -0
  87. package/dist/engine/engine.js.map +1 -0
  88. package/dist/engine/handlers/accountUsage.d.ts +53 -0
  89. package/dist/engine/handlers/accountUsage.js +203 -0
  90. package/dist/engine/handlers/accountUsage.js.map +1 -0
  91. package/dist/engine/handlers/claudeModels.d.ts +2 -0
  92. package/dist/engine/handlers/claudeModels.js +23 -0
  93. package/dist/engine/handlers/claudeModels.js.map +1 -0
  94. package/dist/engine/handlers/codex.d.ts +2 -0
  95. package/dist/engine/handlers/codex.js +97 -0
  96. package/dist/engine/handlers/codex.js.map +1 -0
  97. package/dist/engine/handlers/conversation.d.ts +2 -0
  98. package/dist/engine/handlers/conversation.js +259 -0
  99. package/dist/engine/handlers/conversation.js.map +1 -0
  100. package/dist/engine/handlers/core.d.ts +2 -0
  101. package/dist/engine/handlers/core.js +66 -0
  102. package/dist/engine/handlers/core.js.map +1 -0
  103. package/dist/engine/handlers/git.d.ts +2 -0
  104. package/dist/engine/handlers/git.js +127 -0
  105. package/dist/engine/handlers/git.js.map +1 -0
  106. package/dist/engine/handlers/index.d.ts +2 -0
  107. package/dist/engine/handlers/index.js +32 -0
  108. package/dist/engine/handlers/index.js.map +1 -0
  109. package/dist/engine/handlers/mode.d.ts +2 -0
  110. package/dist/engine/handlers/mode.js +194 -0
  111. package/dist/engine/handlers/mode.js.map +1 -0
  112. package/dist/engine/handlers/officialApps.d.ts +2 -0
  113. package/dist/engine/handlers/officialApps.js +79 -0
  114. package/dist/engine/handlers/officialApps.js.map +1 -0
  115. package/dist/engine/handlers/preview.d.ts +2 -0
  116. package/dist/engine/handlers/preview.js +59 -0
  117. package/dist/engine/handlers/preview.js.map +1 -0
  118. package/dist/engine/handlers/session.d.ts +2 -0
  119. package/dist/engine/handlers/session.js +385 -0
  120. package/dist/engine/handlers/session.js.map +1 -0
  121. package/dist/engine/handlers/usage.d.ts +2 -0
  122. package/dist/engine/handlers/usage.js +96 -0
  123. package/dist/engine/handlers/usage.js.map +1 -0
  124. package/dist/engine/handlers/workspace.d.ts +2 -0
  125. package/dist/engine/handlers/workspace.js +110 -0
  126. package/dist/engine/handlers/workspace.js.map +1 -0
  127. package/dist/engine/slashCommands.d.ts +3 -0
  128. package/dist/engine/slashCommands.js +211 -0
  129. package/dist/engine/slashCommands.js.map +1 -0
  130. package/dist/hub/engineRelaySocket.d.ts +58 -0
  131. package/dist/hub/engineRelaySocket.js +161 -0
  132. package/dist/hub/engineRelaySocket.js.map +1 -0
  133. package/dist/hub/hubClient.d.ts +28 -0
  134. package/dist/hub/hubClient.js +187 -0
  135. package/dist/hub/hubClient.js.map +1 -0
  136. package/dist/hub/hubDaemon.d.ts +69 -0
  137. package/dist/hub/hubDaemon.js +466 -0
  138. package/dist/hub/hubDaemon.js.map +1 -0
  139. package/dist/hub/hubProtocol.d.ts +161 -0
  140. package/dist/hub/hubProtocol.js +379 -0
  141. package/dist/hub/hubProtocol.js.map +1 -0
  142. package/dist/hub/panePreviewPump.d.ts +65 -0
  143. package/dist/hub/panePreviewPump.js +187 -0
  144. package/dist/hub/panePreviewPump.js.map +1 -0
  145. package/dist/hub/questionInjection.d.ts +19 -0
  146. package/dist/hub/questionInjection.js +126 -0
  147. package/dist/hub/questionInjection.js.map +1 -0
  148. package/dist/hub/reaper.d.ts +74 -0
  149. package/dist/hub/reaper.js +333 -0
  150. package/dist/hub/reaper.js.map +1 -0
  151. package/dist/hub/sessionHub.d.ts +203 -0
  152. package/dist/hub/sessionHub.js +1862 -0
  153. package/dist/hub/sessionHub.js.map +1 -0
  154. package/dist/protocol/common.d.ts +18 -0
  155. package/dist/protocol/common.js +78 -0
  156. package/dist/protocol/common.js.map +1 -0
  157. package/dist/protocol/decode.d.ts +3 -0
  158. package/dist/protocol/decode.js +1036 -0
  159. package/dist/protocol/decode.js.map +1 -0
  160. package/dist/protocol/encode.d.ts +3 -0
  161. package/dist/protocol/encode.js +84 -0
  162. package/dist/protocol/encode.js.map +1 -0
  163. package/dist/protocol/messages.d.ts +1002 -0
  164. package/dist/protocol/messages.js +8 -0
  165. package/dist/protocol/messages.js.map +1 -0
  166. package/dist/protocol.d.ts +4 -329
  167. package/dist/protocol.js +13 -531
  168. package/dist/protocol.js.map +1 -1
  169. package/dist/push/apnsConfigStore.js.map +1 -0
  170. package/dist/push/apnsJwtProvider.js.map +1 -0
  171. package/dist/{apnsSender.js.map → push/apnsSender.js.map} +1 -1
  172. package/dist/{approvalPushNotifier.js → push/approvalPushNotifier.js} +1 -1
  173. package/dist/push/approvalPushNotifier.js.map +1 -0
  174. package/dist/push/deviceTokenStore.js.map +1 -0
  175. package/dist/{pushTypes.js → push/pushTypes.js} +1 -1
  176. package/dist/push/pushTypes.js.map +1 -0
  177. package/dist/push/sendLog.js.map +1 -0
  178. package/dist/services/accountIdentity.d.ts +44 -0
  179. package/dist/services/accountIdentity.js +184 -0
  180. package/dist/services/accountIdentity.js.map +1 -0
  181. package/dist/services/claudeCredentialRefresh.d.ts +30 -0
  182. package/dist/services/claudeCredentialRefresh.js +345 -0
  183. package/dist/services/claudeCredentialRefresh.js.map +1 -0
  184. package/dist/services/claudeModelCatalog.d.ts +21 -0
  185. package/dist/services/claudeModelCatalog.js +181 -0
  186. package/dist/services/claudeModelCatalog.js.map +1 -0
  187. package/dist/{dirLister.d.ts → services/dirLister.d.ts} +9 -4
  188. package/dist/services/dirLister.js +169 -0
  189. package/dist/services/dirLister.js.map +1 -0
  190. package/dist/services/fileService.d.ts +10 -0
  191. package/dist/services/fileService.js +123 -0
  192. package/dist/services/fileService.js.map +1 -0
  193. package/dist/services/gitService.d.ts +79 -0
  194. package/dist/services/gitService.js +648 -0
  195. package/dist/services/gitService.js.map +1 -0
  196. package/dist/services/hostVersions.d.ts +38 -0
  197. package/dist/services/hostVersions.js +120 -0
  198. package/dist/services/hostVersions.js.map +1 -0
  199. package/dist/services/officialApps.d.ts +148 -0
  200. package/dist/services/officialApps.js +1079 -0
  201. package/dist/services/officialApps.js.map +1 -0
  202. package/dist/services/planUsageFetcher.d.ts +119 -0
  203. package/dist/services/planUsageFetcher.js +357 -0
  204. package/dist/services/planUsageFetcher.js.map +1 -0
  205. package/dist/services/previewServer.d.ts +31 -0
  206. package/dist/services/previewServer.js +233 -0
  207. package/dist/services/previewServer.js.map +1 -0
  208. package/dist/services/quicGateway.d.ts +143 -0
  209. package/dist/services/quicGateway.js +292 -0
  210. package/dist/services/quicGateway.js.map +1 -0
  211. package/dist/services/serveService.d.ts +50 -0
  212. package/dist/services/serveService.js +302 -0
  213. package/dist/services/serveService.js.map +1 -0
  214. package/dist/services/usageAggregator.js.map +1 -0
  215. package/dist/sessions/claudeSessionStore.d.ts +80 -0
  216. package/dist/sessions/claudeSessionStore.js +477 -0
  217. package/dist/sessions/claudeSessionStore.js.map +1 -0
  218. package/dist/sessions/heartbeat.d.ts +24 -0
  219. package/dist/sessions/heartbeat.js +86 -0
  220. package/dist/sessions/heartbeat.js.map +1 -0
  221. package/dist/sessions/liveSessionJoin.d.ts +20 -0
  222. package/dist/sessions/liveSessionJoin.js +80 -0
  223. package/dist/sessions/liveSessionJoin.js.map +1 -0
  224. package/dist/sessions/sessionActivityProvider.d.ts +14 -0
  225. package/dist/sessions/sessionActivityProvider.js +57 -0
  226. package/dist/sessions/sessionActivityProvider.js.map +1 -0
  227. package/dist/{sessionListService.d.ts → sessions/sessionListService.d.ts} +3 -3
  228. package/dist/{sessionListService.js → sessions/sessionListService.js} +23 -6
  229. package/dist/sessions/sessionListService.js.map +1 -0
  230. package/dist/{sessionMetadataStore.d.ts → sessions/sessionMetadataStore.d.ts} +18 -0
  231. package/dist/{sessionMetadataStore.js → sessions/sessionMetadataStore.js} +36 -1
  232. package/dist/sessions/sessionMetadataStore.js.map +1 -0
  233. package/dist/{sessionSearch.d.ts → sessions/sessionSearch.d.ts} +1 -1
  234. package/dist/sessions/sessionSearch.js.map +1 -0
  235. package/dist/shared/hostDisplayName.d.ts +11 -0
  236. package/dist/shared/hostDisplayName.js +50 -0
  237. package/dist/shared/hostDisplayName.js.map +1 -0
  238. package/dist/{lineWriter.d.ts → shared/lineWriter.d.ts} +1 -1
  239. package/dist/{lineWriter.js → shared/lineWriter.js} +1 -1
  240. package/dist/shared/lineWriter.js.map +1 -0
  241. package/dist/{observationLog.js.map → shared/observationLog.js.map} +1 -1
  242. package/dist/{paths.d.ts → shared/paths.d.ts} +7 -0
  243. package/dist/{paths.js → shared/paths.js} +9 -0
  244. package/dist/shared/paths.js.map +1 -0
  245. package/dist/shared/permissionMode.d.ts +10 -0
  246. package/dist/shared/permissionMode.js +51 -0
  247. package/dist/shared/permissionMode.js.map +1 -0
  248. package/dist/shared/skillInjection.d.ts +9 -0
  249. package/dist/shared/skillInjection.js +16 -0
  250. package/dist/shared/skillInjection.js.map +1 -0
  251. package/dist/shared/sleep.js.map +1 -0
  252. package/dist/{socketPath.d.ts → shared/socketPath.d.ts} +4 -0
  253. package/dist/{socketPath.js → shared/socketPath.js} +22 -0
  254. package/dist/shared/socketPath.js.map +1 -0
  255. package/dist/shared/version.js.map +1 -0
  256. package/package.json +13 -7
  257. package/dist/apnsConfigStore.js.map +0 -1
  258. package/dist/apnsJwtProvider.js.map +0 -1
  259. package/dist/approvalPushNotifier.js.map +0 -1
  260. package/dist/broker.js.map +0 -1
  261. package/dist/chatTailController.js.map +0 -1
  262. package/dist/claudeSessionStore.d.ts +0 -15
  263. package/dist/claudeSessionStore.js +0 -201
  264. package/dist/claudeSessionStore.js.map +0 -1
  265. package/dist/codexRolloutTailer.d.ts +0 -56
  266. package/dist/codexRolloutTailer.js +0 -303
  267. package/dist/codexRolloutTailer.js.map +0 -1
  268. package/dist/codexSessionStore.js.map +0 -1
  269. package/dist/codexUsage.js.map +0 -1
  270. package/dist/deviceTokenStore.js.map +0 -1
  271. package/dist/dirLister.js +0 -98
  272. package/dist/dirLister.js.map +0 -1
  273. package/dist/doctor.d.ts +0 -36
  274. package/dist/doctor.js +0 -162
  275. package/dist/doctor.js.map +0 -1
  276. package/dist/engine.d.ts +0 -85
  277. package/dist/engine.js +0 -962
  278. package/dist/engine.js.map +0 -1
  279. package/dist/hook.js.map +0 -1
  280. package/dist/hookSettings.js.map +0 -1
  281. package/dist/idleReaper.d.ts +0 -13
  282. package/dist/idleReaper.js +0 -30
  283. package/dist/idleReaper.js.map +0 -1
  284. package/dist/imageService.js.map +0 -1
  285. package/dist/kick.js.map +0 -1
  286. package/dist/launch.js +0 -396
  287. package/dist/launch.js.map +0 -1
  288. package/dist/legacyHomeMigration.js.map +0 -1
  289. package/dist/lineWriter.js.map +0 -1
  290. package/dist/mdnsAdvertiser.d.ts +0 -46
  291. package/dist/mdnsAdvertiser.js +0 -268
  292. package/dist/mdnsAdvertiser.js.map +0 -1
  293. package/dist/pairingCode.js.map +0 -1
  294. package/dist/paths.js.map +0 -1
  295. package/dist/permissionMode.d.ts +0 -8
  296. package/dist/permissionMode.js +0 -32
  297. package/dist/permissionMode.js.map +0 -1
  298. package/dist/planUsageFetcher.d.ts +0 -35
  299. package/dist/planUsageFetcher.js +0 -172
  300. package/dist/planUsageFetcher.js.map +0 -1
  301. package/dist/pushTokenCommand.js.map +0 -1
  302. package/dist/pushTypes.js.map +0 -1
  303. package/dist/sendLog.js.map +0 -1
  304. package/dist/sessionActivityProvider.d.ts +0 -7
  305. package/dist/sessionActivityProvider.js +0 -44
  306. package/dist/sessionActivityProvider.js.map +0 -1
  307. package/dist/sessionIdleTracker.d.ts +0 -19
  308. package/dist/sessionIdleTracker.js +0 -42
  309. package/dist/sessionIdleTracker.js.map +0 -1
  310. package/dist/sessionListService.js.map +0 -1
  311. package/dist/sessionMetadataStore.js.map +0 -1
  312. package/dist/sessionSearch.js.map +0 -1
  313. package/dist/setup.js.map +0 -1
  314. package/dist/sleep.js.map +0 -1
  315. package/dist/socketPath.js.map +0 -1
  316. package/dist/subagentTailer.d.ts +0 -15
  317. package/dist/subagentTailer.js +0 -399
  318. package/dist/subagentTailer.js.map +0 -1
  319. package/dist/tmux.d.ts +0 -62
  320. package/dist/tmux.js +0 -163
  321. package/dist/tmux.js.map +0 -1
  322. package/dist/transcriptTailer.js.map +0 -1
  323. package/dist/usageAggregator.js.map +0 -1
  324. package/dist/version.js.map +0 -1
  325. /package/dist/{kick.d.ts → commands/kick.d.ts} +0 -0
  326. /package/dist/{legacyHomeMigration.d.ts → commands/legacyHomeMigration.d.ts} +0 -0
  327. /package/dist/{legacyHomeMigration.js → commands/legacyHomeMigration.js} +0 -0
  328. /package/dist/{apnsConfigStore.d.ts → push/apnsConfigStore.d.ts} +0 -0
  329. /package/dist/{apnsConfigStore.js → push/apnsConfigStore.js} +0 -0
  330. /package/dist/{apnsJwtProvider.d.ts → push/apnsJwtProvider.d.ts} +0 -0
  331. /package/dist/{apnsJwtProvider.js → push/apnsJwtProvider.js} +0 -0
  332. /package/dist/{apnsSender.d.ts → push/apnsSender.d.ts} +0 -0
  333. /package/dist/{apnsSender.js → push/apnsSender.js} +0 -0
  334. /package/dist/{approvalPushNotifier.d.ts → push/approvalPushNotifier.d.ts} +0 -0
  335. /package/dist/{deviceTokenStore.d.ts → push/deviceTokenStore.d.ts} +0 -0
  336. /package/dist/{deviceTokenStore.js → push/deviceTokenStore.js} +0 -0
  337. /package/dist/{pushTypes.d.ts → push/pushTypes.d.ts} +0 -0
  338. /package/dist/{sendLog.d.ts → push/sendLog.d.ts} +0 -0
  339. /package/dist/{sendLog.js → push/sendLog.js} +0 -0
  340. /package/dist/{usageAggregator.d.ts → services/usageAggregator.d.ts} +0 -0
  341. /package/dist/{usageAggregator.js → services/usageAggregator.js} +0 -0
  342. /package/dist/{sessionSearch.js → sessions/sessionSearch.js} +0 -0
  343. /package/dist/{observationLog.d.ts → shared/observationLog.d.ts} +0 -0
  344. /package/dist/{observationLog.js → shared/observationLog.js} +0 -0
  345. /package/dist/{sleep.d.ts → shared/sleep.d.ts} +0 -0
  346. /package/dist/{sleep.js → shared/sleep.js} +0 -0
  347. /package/dist/{version.d.ts → shared/version.d.ts} +0 -0
  348. /package/dist/{version.js → shared/version.js} +0 -0
@@ -0,0 +1,648 @@
1
+ // gitService.ts
2
+ // tailii (TS host) — cwd Git 状態・差分・履歴・更新サービス
3
+ import { execFile } from "node:child_process";
4
+ import * as fs from "node:fs";
5
+ import * as os from "node:os";
6
+ import * as path from "node:path";
7
+ const GIT_TIMEOUT_MS = 10_000;
8
+ const GIT_MAX_BUFFER = 8 * 1024 * 1024;
9
+ const DIFF_CHARACTER_LIMIT = 200_000;
10
+ const DEFAULT_LOG_LIMIT = 50;
11
+ const MAX_LOG_LIMIT = 200;
12
+ const MAX_WORKTREE_INCLUDE_FILES = 500;
13
+ const WORKTREE_EXCLUDE_LINE = ".claude/worktrees/";
14
+ const WORKTREE_LOCK_REASON = "tailii-session";
15
+ function validRepositoryPath(candidate) {
16
+ return path.isAbsolute(candidate) && !candidate.startsWith("~");
17
+ }
18
+ function runGit(repositoryPath, args) {
19
+ if (!validRepositoryPath(repositoryPath)) {
20
+ return Promise.resolve({
21
+ ok: false, exitCode: null, stdout: "", stderr: "絶対パスを指定してください。",
22
+ });
23
+ }
24
+ return new Promise((resolve) => {
25
+ execFile("git", ["-C", repositoryPath, ...args], { timeout: GIT_TIMEOUT_MS, maxBuffer: GIT_MAX_BUFFER, encoding: "utf8" }, (error, stdout, stderr) => {
26
+ resolve({
27
+ ok: error === null,
28
+ exitCode: error !== null && typeof error.code === "number" ? error.code : null,
29
+ stdout: String(stdout),
30
+ stderr: String(stderr).trim() || (error === null ? "" : error.message),
31
+ });
32
+ });
33
+ });
34
+ }
35
+ function formatWorktreeTimestamp(now) {
36
+ const pad = (value) => String(value).padStart(2, "0");
37
+ return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-` +
38
+ `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
39
+ }
40
+ function resolveGitPath(repositoryPath, value) {
41
+ return path.isAbsolute(value) ? path.normalize(value) : path.resolve(repositoryPath, value);
42
+ }
43
+ async function appendWorktreeExclude(repositoryPath) {
44
+ const commonDir = await runGit(repositoryPath, ["rev-parse", "--git-common-dir"]);
45
+ if (!commonDir.ok)
46
+ return commonDir.stderr;
47
+ try {
48
+ const excludePath = path.join(resolveGitPath(repositoryPath, commonDir.stdout.trim()), "info", "exclude");
49
+ fs.mkdirSync(path.dirname(excludePath), { recursive: true });
50
+ const current = fs.existsSync(excludePath) ? fs.readFileSync(excludePath, "utf8") : "";
51
+ if (current.split(/\r?\n/).some((line) => line.trim() === WORKTREE_EXCLUDE_LINE))
52
+ return null;
53
+ const separator = current.length > 0 && !current.endsWith("\n") ? "\n" : "";
54
+ fs.appendFileSync(excludePath, `${separator}${WORKTREE_EXCLUDE_LINE}\n`);
55
+ return null;
56
+ }
57
+ catch (error) {
58
+ return String(error);
59
+ }
60
+ }
61
+ function isPathInside(root, candidate) {
62
+ const relative = path.relative(root, candidate);
63
+ return relative !== "" && relative !== ".." && !relative.startsWith(`..${path.sep}`) &&
64
+ !path.isAbsolute(relative);
65
+ }
66
+ function gitServiceDiag(message) {
67
+ if (process.env["TAILII_DEBUG"] === "1")
68
+ process.stderr.write(`[tailii-host git] ${message}\n`);
69
+ }
70
+ function parseNulSeparatedPaths(output) {
71
+ if (output.length === 0)
72
+ return [];
73
+ const records = output.split("\0");
74
+ if (records[records.length - 1] === "")
75
+ records.pop();
76
+ return records.filter((record) => record.length > 0);
77
+ }
78
+ function pathEntryExists(candidate) {
79
+ try {
80
+ fs.lstatSync(candidate);
81
+ return true;
82
+ }
83
+ catch (error) {
84
+ if (error.code === "ENOENT")
85
+ return false;
86
+ throw error;
87
+ }
88
+ }
89
+ function worktreeDestination(worktreePath, relativePath) {
90
+ const destination = path.resolve(worktreePath, relativePath);
91
+ if (!isPathInside(worktreePath, destination))
92
+ return null;
93
+ let current = worktreePath;
94
+ const parentRelative = path.relative(worktreePath, path.dirname(destination));
95
+ for (const component of parentRelative.split(path.sep).filter(Boolean)) {
96
+ current = path.join(current, component);
97
+ if (pathEntryExists(current)) {
98
+ const status = fs.lstatSync(current);
99
+ if (status.isSymbolicLink() || !status.isDirectory())
100
+ return null;
101
+ }
102
+ else {
103
+ fs.mkdirSync(current);
104
+ }
105
+ }
106
+ return pathEntryExists(destination) ? null : destination;
107
+ }
108
+ async function copyWorktreeIncludes(repoRoot, worktreePath) {
109
+ const includePath = path.join(repoRoot, ".worktreeinclude");
110
+ if (!fs.existsSync(includePath))
111
+ return null;
112
+ let temporaryDirectory = null;
113
+ let errorMessage = null;
114
+ try {
115
+ const actuallyIgnored = await runGit(repoRoot, [
116
+ "ls-files", "--others", "--ignored", "--exclude-standard", "-z",
117
+ ]);
118
+ if (!actuallyIgnored.ok) {
119
+ errorMessage = actuallyIgnored.stderr || "gitignore 対象ファイルを取得できませんでした。";
120
+ }
121
+ else {
122
+ temporaryDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "tailii-worktreeinclude-"));
123
+ const temporaryIncludePath = path.join(temporaryDirectory, "patterns");
124
+ fs.copyFileSync(includePath, temporaryIncludePath);
125
+ const matchingInclude = await runGit(repoRoot, [
126
+ "ls-files", "--others", "--ignored", `--exclude-from=${temporaryIncludePath}`, "-z",
127
+ ]);
128
+ if (!matchingInclude.ok) {
129
+ errorMessage = matchingInclude.stderr || ".worktreeinclude 対象ファイルを取得できませんでした。";
130
+ }
131
+ else {
132
+ const ignoredPaths = new Set(parseNulSeparatedPaths(actuallyIgnored.stdout));
133
+ const copyPaths = parseNulSeparatedPaths(matchingInclude.stdout)
134
+ .filter((relativePath) => ignoredPaths.has(relativePath));
135
+ let copiedFiles = 0;
136
+ for (const relativePath of copyPaths) {
137
+ if (copiedFiles >= MAX_WORKTREE_INCLUDE_FILES)
138
+ break;
139
+ const source = path.resolve(repoRoot, relativePath);
140
+ if (!isPathInside(repoRoot, source) || !fs.existsSync(source))
141
+ continue;
142
+ const sourceStatus = fs.lstatSync(source);
143
+ if (sourceStatus.isSymbolicLink() || !sourceStatus.isFile())
144
+ continue;
145
+ const destination = worktreeDestination(worktreePath, relativePath);
146
+ if (destination === null)
147
+ continue;
148
+ fs.copyFileSync(source, destination);
149
+ copiedFiles += 1;
150
+ }
151
+ }
152
+ }
153
+ }
154
+ catch (error) {
155
+ errorMessage = String(error);
156
+ }
157
+ finally {
158
+ if (temporaryDirectory !== null) {
159
+ try {
160
+ fs.rmSync(temporaryDirectory, { recursive: true, force: true });
161
+ }
162
+ catch (error) {
163
+ const cleanupError = String(error);
164
+ errorMessage = errorMessage === null ? cleanupError : `${errorMessage}\n${cleanupError}`;
165
+ }
166
+ }
167
+ }
168
+ return errorMessage;
169
+ }
170
+ function normalizedExistingPath(value) {
171
+ try {
172
+ return fs.realpathSync(value);
173
+ }
174
+ catch {
175
+ return path.resolve(value);
176
+ }
177
+ }
178
+ function worktreeManagementPath(listOutput, worktreePath) {
179
+ const target = normalizedExistingPath(worktreePath);
180
+ const listed = listOutput.split(/\r?\n/)
181
+ .filter((line) => line.startsWith("worktree "))
182
+ .map((line) => line.slice("worktree ".length));
183
+ return listed.find((candidate) => normalizedExistingPath(candidate) !== target) ?? listed[0] ?? null;
184
+ }
185
+ async function isGitRepository(repositoryPath) {
186
+ const result = await runGit(repositoryPath, ["rev-parse", "--is-inside-work-tree"]);
187
+ return result.ok && result.stdout.trim() === "true";
188
+ }
189
+ /** `git status --porcelain=v2 --branch` を wire 応答用に解析する。 */
190
+ export function parsePorcelainV2(output) {
191
+ let branch = "";
192
+ let oid = "";
193
+ let upstream = null;
194
+ let ahead = 0;
195
+ let behind = 0;
196
+ const files = [];
197
+ const records = output.includes("\0") ? output.split("\0") : output.split(/\r?\n/);
198
+ for (let index = 0; index < records.length; index += 1) {
199
+ const record = records[index];
200
+ if (!record)
201
+ continue;
202
+ if (record.startsWith("# branch.oid "))
203
+ oid = record.slice(13).trim();
204
+ else if (record.startsWith("# branch.head "))
205
+ branch = record.slice(14).trim();
206
+ else if (record.startsWith("# branch.upstream "))
207
+ upstream = record.slice(18).trim();
208
+ else if (record.startsWith("# branch.ab ")) {
209
+ const match = record.match(/^# branch\.ab \+(\d+) -(\d+)$/);
210
+ if (match) {
211
+ ahead = Number.parseInt(match[1], 10);
212
+ behind = Number.parseInt(match[2], 10);
213
+ }
214
+ }
215
+ else if (record.startsWith("1 ")) {
216
+ const match = record.match(/^1 (\S+) \S+ \S+ \S+ \S+ \S+ \S+ (.*)$/);
217
+ if (match) {
218
+ const xy = match[1];
219
+ files.push({
220
+ path: decodeGitPath(match[2]),
221
+ indexStatus: xy[0] ?? ".",
222
+ worktreeStatus: xy[1] ?? ".",
223
+ renamedFrom: null,
224
+ });
225
+ }
226
+ }
227
+ else if (record.startsWith("2 ")) {
228
+ const match = record.match(/^2 (\S+) \S+ \S+ \S+ \S+ \S+ \S+ \S+ (.*)$/);
229
+ if (match) {
230
+ const xy = match[1];
231
+ let currentPath = match[2];
232
+ let renamedFrom = null;
233
+ if (output.includes("\0")) {
234
+ renamedFrom = records[++index] ?? null;
235
+ }
236
+ else {
237
+ const tab = currentPath.indexOf("\t");
238
+ if (tab >= 0) {
239
+ renamedFrom = decodeGitPath(currentPath.slice(tab + 1));
240
+ currentPath = currentPath.slice(0, tab);
241
+ }
242
+ }
243
+ files.push({
244
+ path: decodeGitPath(currentPath),
245
+ indexStatus: xy[0] ?? ".",
246
+ worktreeStatus: xy[1] ?? ".",
247
+ renamedFrom,
248
+ });
249
+ }
250
+ }
251
+ else if (record.startsWith("? ")) {
252
+ files.push({
253
+ path: decodeGitPath(record.slice(2)),
254
+ indexStatus: "?",
255
+ worktreeStatus: "?",
256
+ renamedFrom: null,
257
+ });
258
+ }
259
+ }
260
+ if (branch === "(detached)") {
261
+ const shortOid = oid && oid !== "(initial)" ? oid.slice(0, 7) : "unknown";
262
+ branch = `(detached ${shortOid})`;
263
+ }
264
+ return { branch, upstream, ahead, behind, files };
265
+ }
266
+ function decodeGitPath(value) {
267
+ if (!value.startsWith('"'))
268
+ return value;
269
+ try {
270
+ return JSON.parse(value);
271
+ }
272
+ catch {
273
+ // Git の C-style quote は JSON にない \a / \v / 3桁8進 byte escape も使う。
274
+ const inner = value.endsWith('"') ? value.slice(1, -1) : value.slice(1);
275
+ const bytes = [];
276
+ for (let index = 0; index < inner.length;) {
277
+ if (inner[index] !== "\\") {
278
+ const codePoint = inner.codePointAt(index);
279
+ const literal = String.fromCodePoint(codePoint);
280
+ bytes.push(...Buffer.from(literal));
281
+ index += literal.length;
282
+ continue;
283
+ }
284
+ const escaped = inner[index + 1];
285
+ const simpleEscapes = {
286
+ a: 0x07, b: 0x08, t: 0x09, n: 0x0a, v: 0x0b, f: 0x0c, r: 0x0d,
287
+ "\\": 0x5c, '"': 0x22,
288
+ };
289
+ const simpleEscape = escaped === undefined ? undefined : simpleEscapes[escaped];
290
+ if (simpleEscape !== undefined) {
291
+ bytes.push(simpleEscape);
292
+ index += 2;
293
+ continue;
294
+ }
295
+ const octal = inner.slice(index + 1).match(/^[0-7]{1,3}/)?.[0];
296
+ if (octal !== undefined) {
297
+ bytes.push(Number.parseInt(octal, 8));
298
+ index += 1 + octal.length;
299
+ continue;
300
+ }
301
+ bytes.push(0x5c);
302
+ index += 1;
303
+ }
304
+ return Buffer.from(bytes).toString("utf8");
305
+ }
306
+ }
307
+ export async function gitStatus(repositoryPath) {
308
+ if (!(await isGitRepository(repositoryPath))) {
309
+ return { isRepo: false, branch: "", upstream: null, ahead: 0, behind: 0, files: [] };
310
+ }
311
+ const result = await runGit(repositoryPath, ["status", "--porcelain=v2", "--branch", "-z"]);
312
+ if (!result.ok)
313
+ return { isRepo: false, branch: "", upstream: null, ahead: 0, behind: 0, files: [] };
314
+ const repoRootResult = await runGit(repositoryPath, ["rev-parse", "--show-toplevel"]);
315
+ const headDiffstat = await runGit(repositoryPath, ["diff", "--shortstat", "HEAD"]);
316
+ const diffstat = headDiffstat.ok
317
+ ? headDiffstat
318
+ : await runGit(repositoryPath, ["diff", "--shortstat"]);
319
+ const { additions: diffAdditions, deletions: diffDeletions } = parseDiffShortstat(diffstat.stdout);
320
+ return {
321
+ isRepo: true,
322
+ ...parsePorcelainV2(result.stdout),
323
+ repoRoot: repoRootResult.stdout.trim(),
324
+ diffAdditions,
325
+ diffDeletions,
326
+ };
327
+ }
328
+ function parseDiffShortstat(output) {
329
+ const additions = output.match(/(\d+) insertion(?:s)?\(\+\)/);
330
+ const deletions = output.match(/(\d+) deletion(?:s)?\(-\)/);
331
+ return {
332
+ additions: additions ? Number.parseInt(additions[1], 10) : 0,
333
+ deletions: deletions ? Number.parseInt(deletions[1], 10) : 0,
334
+ };
335
+ }
336
+ async function untrackedPathKind(repositoryPath, file) {
337
+ const result = await runGit(repositoryPath, [
338
+ "ls-files", "--others", "--exclude-standard", "-z", "--", file,
339
+ ]);
340
+ if (!result.ok)
341
+ return null;
342
+ const requested = normalizeGitPath(file);
343
+ const candidates = parseNulSeparatedPaths(result.stdout).map(normalizeGitPath);
344
+ if (candidates.includes(requested))
345
+ return "file";
346
+ if (candidates.some((candidate) => candidate.startsWith(`${requested}/`)))
347
+ return "directory";
348
+ return null;
349
+ }
350
+ async function runUntrackedDiff(repositoryPath, file, kind) {
351
+ if (kind === "file") {
352
+ return runGit(repositoryPath, ["diff", "--no-index", "--", "/dev/null", file]);
353
+ }
354
+ const emptyDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "tailii-git-diff-empty-"));
355
+ try {
356
+ return await runGit(repositoryPath, [
357
+ "diff", "--no-index", "--", emptyDirectory, file,
358
+ ]);
359
+ }
360
+ finally {
361
+ fs.rmSync(emptyDirectory, { recursive: true, force: true });
362
+ }
363
+ }
364
+ export async function gitDiff(repositoryPath, options = {}) {
365
+ if (!(await isGitRepository(repositoryPath)))
366
+ return { isRepo: false, diff: "", truncated: false };
367
+ let showUntrackedPath = false;
368
+ let result;
369
+ if (options.commit) {
370
+ result = await runGit(repositoryPath, ["show", options.commit]);
371
+ }
372
+ else if (options.file !== undefined && options.staged !== true) {
373
+ const kind = await untrackedPathKind(repositoryPath, options.file);
374
+ if (kind !== null) {
375
+ showUntrackedPath = true;
376
+ result = await runUntrackedDiff(repositoryPath, options.file, kind);
377
+ }
378
+ else {
379
+ result = await runGit(repositoryPath, ["diff", "--", options.file]);
380
+ }
381
+ }
382
+ else {
383
+ result = await runGit(repositoryPath, [
384
+ "diff",
385
+ ...(options.staged ? ["--cached"] : []),
386
+ ...(options.file ? ["--", options.file] : []),
387
+ ]);
388
+ }
389
+ const expectedNoIndexDifference = showUntrackedPath && result.exitCode === 1;
390
+ if (!result.ok && !expectedNoIndexDifference && result.stdout.length === 0) {
391
+ return { isRepo: true, diff: "", truncated: false };
392
+ }
393
+ return {
394
+ isRepo: true,
395
+ diff: result.stdout.slice(0, DIFF_CHARACTER_LIMIT),
396
+ truncated: (!result.ok && !expectedNoIndexDifference)
397
+ || result.stdout.length > DIFF_CHARACTER_LIMIT,
398
+ };
399
+ }
400
+ export async function gitLog(repositoryPath, limit = DEFAULT_LOG_LIMIT) {
401
+ if (!(await isGitRepository(repositoryPath)))
402
+ return { isRepo: false, commits: [] };
403
+ const safeLimit = Number.isInteger(limit) ? Math.max(1, Math.min(limit, MAX_LOG_LIMIT)) : DEFAULT_LOG_LIMIT;
404
+ const result = await runGit(repositoryPath, [
405
+ "log",
406
+ "--pretty=format:%H%x1f%h%x1f%an%x1f%at%x1f%s%x1e",
407
+ "-n",
408
+ String(safeLimit),
409
+ ]);
410
+ if (!result.ok)
411
+ return { isRepo: true, commits: [] };
412
+ const commits = result.stdout
413
+ .split("\x1e")
414
+ .map((record) => record.replace(/^\r?\n/, ""))
415
+ .filter(Boolean)
416
+ .flatMap((record) => {
417
+ const fields = record.split("\x1f");
418
+ if (fields.length !== 5)
419
+ return [];
420
+ return [{
421
+ hash: fields[0],
422
+ shortHash: fields[1],
423
+ authorName: fields[2],
424
+ dateMs: Number.parseInt(fields[3], 10) * 1_000,
425
+ subject: fields[4],
426
+ }];
427
+ });
428
+ return { isRepo: true, commits };
429
+ }
430
+ export async function gitBranchList(repositoryPath) {
431
+ if (!(await isGitRepository(repositoryPath)))
432
+ return { isRepo: false, branches: [] };
433
+ const result = await runGit(repositoryPath, [
434
+ "for-each-ref",
435
+ "refs/heads",
436
+ "--sort=-committerdate",
437
+ "--format=%(HEAD)%1f%(refname:short)%1f%(subject)%1f%(committerdate:unix)%1f%(upstream:track,nobracket)%1e",
438
+ ]);
439
+ if (!result.ok)
440
+ return { isRepo: true, branches: [] };
441
+ const branches = result.stdout
442
+ .split("\x1e")
443
+ .map((record) => record.replace(/^\r?\n/, ""))
444
+ .filter(Boolean)
445
+ .flatMap((record) => {
446
+ const fields = record.split("\x1f");
447
+ if (fields.length !== 5)
448
+ return [];
449
+ const tracking = fields[4];
450
+ const ahead = tracking.match(/(?:^|, )ahead (\d+)(?:,|$)/);
451
+ const behind = tracking.match(/(?:^|, )behind (\d+)(?:,|$)/);
452
+ return [{
453
+ name: fields[1],
454
+ subject: fields[2],
455
+ dateMs: Number.parseInt(fields[3], 10) * 1_000,
456
+ isCurrent: fields[0].trim() === "*",
457
+ ahead: ahead ? Number.parseInt(ahead[1], 10) : 0,
458
+ behind: behind ? Number.parseInt(behind[1], 10) : 0,
459
+ }];
460
+ });
461
+ return { isRepo: true, branches };
462
+ }
463
+ export async function gitCheckout(repositoryPath, branch, create) {
464
+ const result = await runGit(repositoryPath, create ? ["checkout", "-b", branch] : ["checkout", branch]);
465
+ return { ok: result.ok, branch, error: result.ok ? null : result.stderr };
466
+ }
467
+ export async function gitDiscard(repositoryPath, files) {
468
+ if (files.length === 0)
469
+ return { ok: false, error: "files は1件以上必要です。" };
470
+ const status = await gitStatus(repositoryPath);
471
+ if (!status.isRepo)
472
+ return { ok: false, error: "Git リポジトリではありません。" };
473
+ const requested = files.map(normalizeGitPath);
474
+ const tracked = new Set();
475
+ const untracked = new Set();
476
+ for (const file of status.files) {
477
+ const currentPath = normalizeGitPath(file.path);
478
+ const previousPath = file.renamedFrom === null ? null : normalizeGitPath(file.renamedFrom);
479
+ const selected = requested.some((candidate) => currentPath === candidate || currentPath.startsWith(`${candidate}/`) ||
480
+ previousPath === candidate || previousPath?.startsWith(`${candidate}/`) === true);
481
+ if (!selected)
482
+ continue;
483
+ if (file.indexStatus === "?" && file.worktreeStatus === "?") {
484
+ untracked.add(file.path);
485
+ }
486
+ else {
487
+ tracked.add(file.path);
488
+ if (file.renamedFrom !== null)
489
+ tracked.add(file.renamedFrom);
490
+ }
491
+ }
492
+ if (tracked.size > 0) {
493
+ const restored = await runGit(repositoryPath, [
494
+ "restore", "--source=HEAD", "--staged", "--worktree", "--", ...tracked,
495
+ ]);
496
+ if (!restored.ok)
497
+ return { ok: false, error: restored.stderr };
498
+ }
499
+ if (untracked.size > 0) {
500
+ const cleaned = await runGit(repositoryPath, ["clean", "-f", "-d", "--", ...untracked]);
501
+ if (!cleaned.ok)
502
+ return { ok: false, error: cleaned.stderr };
503
+ }
504
+ return { ok: true, error: null };
505
+ }
506
+ function normalizeGitPath(value) {
507
+ return path.normalize(value).replace(/\/+$/, "");
508
+ }
509
+ export async function gitInit(directoryPath) {
510
+ if (await isGitRepository(directoryPath)) {
511
+ return { ok: false, error: "既に Git リポジトリです。" };
512
+ }
513
+ const result = await runGit(directoryPath, ["init", "-b", "main"]);
514
+ return { ok: result.ok, error: result.ok ? null : result.stderr };
515
+ }
516
+ /** Claude Code Desktop と同じ `.claude/worktrees` 配下へセッション用 worktree を作る。 */
517
+ export async function gitWorktreeCreate(repositoryWorkdir, baseBranch) {
518
+ const rootResult = await runGit(repositoryWorkdir, ["rev-parse", "--show-toplevel"]);
519
+ if (!rootResult.ok) {
520
+ return { ok: false, branch: "", worktreePath: "", error: rootResult.stderr };
521
+ }
522
+ const repoRoot = rootResult.stdout.trim();
523
+ if (!validRepositoryPath(repoRoot)) {
524
+ return { ok: false, branch: "", worktreePath: "", error: "Git ルートが絶対パスではありません。" };
525
+ }
526
+ const timestamp = formatWorktreeTimestamp(new Date());
527
+ let suffix = 1;
528
+ let slug = timestamp;
529
+ let branch = `worktree-${slug}`;
530
+ let worktreePath = path.join(repoRoot, ".claude", "worktrees", slug);
531
+ for (;;) {
532
+ const branchExists = await runGit(repoRoot, ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`]);
533
+ if (!fs.existsSync(worktreePath) && !branchExists.ok)
534
+ break;
535
+ suffix += 1;
536
+ slug = `${timestamp}-${suffix}`;
537
+ branch = `worktree-${slug}`;
538
+ worktreePath = path.join(repoRoot, ".claude", "worktrees", slug);
539
+ }
540
+ const added = await runGit(repoRoot, ["worktree", "add", worktreePath, "-b", branch, baseBranch]);
541
+ if (!added.ok)
542
+ return { ok: false, branch, worktreePath, error: added.stderr };
543
+ const errors = [];
544
+ const excludeError = await appendWorktreeExclude(repoRoot);
545
+ if (excludeError !== null)
546
+ errors.push(excludeError);
547
+ const includeError = await copyWorktreeIncludes(repoRoot, worktreePath);
548
+ if (includeError !== null) {
549
+ gitServiceDiag(`.worktreeinclude コピー失敗 path=${worktreePath}: ${includeError}`);
550
+ }
551
+ const locked = await runGit(repoRoot, ["worktree", "lock", "--reason", WORKTREE_LOCK_REASON, worktreePath]);
552
+ if (!locked.ok)
553
+ errors.push(locked.stderr);
554
+ return {
555
+ ok: errors.length === 0,
556
+ branch,
557
+ worktreePath,
558
+ error: errors.length === 0 ? null : errors.join("\n"),
559
+ };
560
+ }
561
+ /** worktree のロック解除。dirty worktree を残してセッションだけ終了する場合にも使う。 */
562
+ export async function gitWorktreeUnlock(worktreePath) {
563
+ const result = await runGit(worktreePath, ["worktree", "unlock", worktreePath]);
564
+ return { ok: result.ok, error: result.ok ? null : result.stderr };
565
+ }
566
+ /**
567
+ * worktree に未保存変更がなく、HEAD が自分以外のローカルブランチから到達可能なら clean。
568
+ * 後者により worktree 上だけの新規コミットを自動削除しない。
569
+ */
570
+ export async function gitWorktreeIsClean(worktreePath) {
571
+ const status = await runGit(worktreePath, ["status", "--porcelain"]);
572
+ if (!status.ok || status.stdout.length !== 0)
573
+ return false;
574
+ const head = await runGit(worktreePath, ["rev-parse", "--verify", "HEAD"]);
575
+ if (!head.ok)
576
+ return false;
577
+ const current = await runGit(worktreePath, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
578
+ const branches = await runGit(worktreePath, [
579
+ "for-each-ref", "--format=%(refname:short)", "--contains=HEAD", "refs/heads",
580
+ ]);
581
+ if (!branches.ok)
582
+ return false;
583
+ const currentBranch = current.ok ? current.stdout.trim() : null;
584
+ return branches.stdout.split(/\r?\n/).some((branchName) => branchName.length > 0 && branchName !== currentBranch);
585
+ }
586
+ /** `.claude/worktrees` 配下を文字列だけで判定する純関数。 */
587
+ export function isTailiiWorktreePath(candidate) {
588
+ return candidate.replaceAll("\\", "/").includes("/.claude/worktrees/");
589
+ }
590
+ /** worktree パスから repo ルート(`/.claude/worktrees/` の手前)を文字列だけで取り出す純関数。 */
591
+ export function tailiiWorktreeRepoRoot(candidate) {
592
+ const index = candidate.replaceAll("\\", "/").indexOf("/.claude/worktrees/");
593
+ if (index <= 0)
594
+ return null;
595
+ return candidate.slice(0, index);
596
+ }
597
+ /** worktree と、安全な自動生成ブランチだけを削除して管理情報を prune する。 */
598
+ export async function gitWorktreeRemove(worktreePath, force) {
599
+ const listed = await runGit(worktreePath, ["worktree", "list", "--porcelain"]);
600
+ if (!listed.ok)
601
+ return { ok: false, error: listed.stderr };
602
+ const managementPath = worktreeManagementPath(listed.stdout, worktreePath);
603
+ if (managementPath === null || !validRepositoryPath(managementPath)) {
604
+ return { ok: false, error: "worktree の管理リポジトリを特定できません。" };
605
+ }
606
+ const current = await runGit(worktreePath, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
607
+ const branch = current.ok ? current.stdout.trim() : null;
608
+ await runGit(managementPath, ["worktree", "unlock", worktreePath]);
609
+ const errors = [];
610
+ const removed = await runGit(managementPath, [
611
+ "worktree", "remove", ...(force ? ["--force"] : []), worktreePath,
612
+ ]);
613
+ if (!removed.ok)
614
+ errors.push(removed.stderr);
615
+ if (removed.ok && branch !== null &&
616
+ (branch.startsWith("worktree-") || branch.startsWith("tailii/"))) {
617
+ const deleted = await runGit(managementPath, ["branch", force ? "-D" : "-d", branch]);
618
+ if (!deleted.ok)
619
+ errors.push(deleted.stderr);
620
+ }
621
+ const pruned = await runGit(managementPath, ["worktree", "prune"]);
622
+ if (!pruned.ok)
623
+ errors.push(pruned.stderr);
624
+ return { ok: errors.length === 0, error: errors.length === 0 ? null : errors.join("\n") };
625
+ }
626
+ export async function gitEntryStatuses(directoryPath, names) {
627
+ const result = await runGit(directoryPath, [
628
+ "-c", "core.quotePath=false", "status", "--porcelain=v2", "--untracked-files=all", "--", ".",
629
+ ]);
630
+ if (!result.ok)
631
+ return new Map();
632
+ const requestedNames = new Set(names);
633
+ const statuses = new Map();
634
+ for (const file of parsePorcelainV2(result.stdout).files) {
635
+ const [entryName, ...descendants] = file.path.split("/");
636
+ if (entryName === undefined || !requestedNames.has(entryName))
637
+ continue;
638
+ if (descendants.length > 0) {
639
+ statuses.set(entryName, "M");
640
+ continue;
641
+ }
642
+ const status = file.worktreeStatus !== "." ? file.worktreeStatus : file.indexStatus;
643
+ if (status !== ".")
644
+ statuses.set(entryName, status);
645
+ }
646
+ return statuses;
647
+ }
648
+ //# sourceMappingURL=gitService.js.map