@oscharko-dev/keiko-server 0.2.8 → 0.2.9

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 (281) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/chat-handlers.d.ts +18 -2
  3. package/dist/chat-handlers.d.ts.map +1 -1
  4. package/dist/chat-handlers.js +185 -3
  5. package/dist/command-runner-errors.d.ts +17 -0
  6. package/dist/command-runner-errors.d.ts.map +1 -0
  7. package/dist/command-runner-errors.js +37 -0
  8. package/dist/command-runner-evidence.d.ts +23 -0
  9. package/dist/command-runner-evidence.d.ts.map +1 -0
  10. package/dist/command-runner-evidence.js +69 -0
  11. package/dist/command-runner-routes.d.ts +7 -0
  12. package/dist/command-runner-routes.d.ts.map +1 -0
  13. package/dist/command-runner-routes.js +175 -0
  14. package/dist/command-runner.d.ts +29 -0
  15. package/dist/command-runner.d.ts.map +1 -0
  16. package/dist/command-runner.js +348 -0
  17. package/dist/conversation-prompt.d.ts +2 -2
  18. package/dist/conversation-prompt.d.ts.map +1 -1
  19. package/dist/conversation-prompt.js +17 -1
  20. package/dist/csp.d.ts.map +1 -1
  21. package/dist/csp.js +3 -0
  22. package/dist/deps.d.ts +27 -1
  23. package/dist/deps.d.ts.map +1 -1
  24. package/dist/deps.js +288 -13
  25. package/dist/discussion-prompt.d.ts +4 -0
  26. package/dist/discussion-prompt.d.ts.map +1 -0
  27. package/dist/discussion-prompt.js +19 -0
  28. package/dist/editor/agentActionAudit.d.ts +18 -0
  29. package/dist/editor/agentActionAudit.d.ts.map +1 -0
  30. package/dist/editor/agentActionAudit.js +80 -0
  31. package/dist/editor/agentRoutes.d.ts +1 -0
  32. package/dist/editor/agentRoutes.d.ts.map +1 -1
  33. package/dist/editor/agentRoutes.js +292 -55
  34. package/dist/editor/agentSessionRegistry.d.ts +35 -0
  35. package/dist/editor/agentSessionRegistry.d.ts.map +1 -0
  36. package/dist/editor/agentSessionRegistry.js +243 -0
  37. package/dist/editor/completionRoutes.d.ts.map +1 -1
  38. package/dist/editor/completionRoutes.js +5 -10
  39. package/dist/editor/languageRoutes.d.ts +12 -1
  40. package/dist/editor/languageRoutes.d.ts.map +1 -1
  41. package/dist/editor/languageRoutes.js +71 -8
  42. package/dist/editor/languageService.d.ts +3 -2
  43. package/dist/editor/languageService.d.ts.map +1 -1
  44. package/dist/editor/languageService.js +41 -3
  45. package/dist/editor/languageServiceHost.d.ts.map +1 -1
  46. package/dist/editor/languageServiceHost.js +2 -2
  47. package/dist/editor/lsp/hostLanguageOperation.d.ts +17 -0
  48. package/dist/editor/lsp/hostLanguageOperation.d.ts.map +1 -0
  49. package/dist/editor/lsp/hostLanguageOperation.js +436 -0
  50. package/dist/editor/lsp/hostLanguageProviders.d.ts +26 -0
  51. package/dist/editor/lsp/hostLanguageProviders.d.ts.map +1 -0
  52. package/dist/editor/lsp/hostLanguageProviders.js +161 -0
  53. package/dist/editor/lsp/lspFrameCodec.d.ts +13 -0
  54. package/dist/editor/lsp/lspFrameCodec.d.ts.map +1 -0
  55. package/dist/editor/lsp/lspFrameCodec.js +164 -0
  56. package/dist/editor/lsp/lspJsonRpcClient.d.ts +34 -0
  57. package/dist/editor/lsp/lspJsonRpcClient.d.ts.map +1 -0
  58. package/dist/editor/lsp/lspJsonRpcClient.js +173 -0
  59. package/dist/editor/lsp/lspLanguageProvider.d.ts +7 -0
  60. package/dist/editor/lsp/lspLanguageProvider.d.ts.map +1 -0
  61. package/dist/editor/lsp/lspLanguageProvider.js +29 -0
  62. package/dist/editor/lsp/lspLifecycleLedger.d.ts +5 -0
  63. package/dist/editor/lsp/lspLifecycleLedger.d.ts.map +1 -0
  64. package/dist/editor/lsp/lspLifecycleLedger.js +37 -0
  65. package/dist/editor/lsp/lspNodeAdapter.d.ts +31 -0
  66. package/dist/editor/lsp/lspNodeAdapter.d.ts.map +1 -0
  67. package/dist/editor/lsp/lspNodeAdapter.js +230 -0
  68. package/dist/editor/lsp/lspProcessManager.d.ts +24 -0
  69. package/dist/editor/lsp/lspProcessManager.d.ts.map +1 -0
  70. package/dist/editor/lsp/lspProcessManager.js +255 -0
  71. package/dist/editor/lsp/lspRestartThrottle.d.ts +6 -0
  72. package/dist/editor/lsp/lspRestartThrottle.d.ts.map +1 -0
  73. package/dist/editor/lsp/lspRestartThrottle.js +24 -0
  74. package/dist/editor/lsp/lspStatusRoute.d.ts +8 -0
  75. package/dist/editor/lsp/lspStatusRoute.d.ts.map +1 -0
  76. package/dist/editor/lsp/lspStatusRoute.js +22 -0
  77. package/dist/editor/lsp/lspTransport.d.ts +19 -0
  78. package/dist/editor/lsp/lspTransport.d.ts.map +1 -0
  79. package/dist/editor/lsp/lspTransport.js +55 -0
  80. package/dist/editor/lsp/testing/fakeLspProcess.d.ts +23 -0
  81. package/dist/editor/lsp/testing/fakeLspProcess.d.ts.map +1 -0
  82. package/dist/editor/lsp/testing/fakeLspProcess.js +132 -0
  83. package/dist/files.d.ts +45 -0
  84. package/dist/files.d.ts.map +1 -1
  85. package/dist/files.js +631 -7
  86. package/dist/gateway-readiness.js +3 -3
  87. package/dist/gateway-setup.d.ts +2 -0
  88. package/dist/gateway-setup.d.ts.map +1 -1
  89. package/dist/gateway-setup.js +275 -11
  90. package/dist/gitDelivery/actionSheetProjection.d.ts +30 -0
  91. package/dist/gitDelivery/actionSheetProjection.d.ts.map +1 -0
  92. package/dist/gitDelivery/actionSheetProjection.js +206 -0
  93. package/dist/gitDelivery/actionSheetRoutes.d.ts +29 -0
  94. package/dist/gitDelivery/actionSheetRoutes.d.ts.map +1 -0
  95. package/dist/gitDelivery/actionSheetRoutes.js +293 -0
  96. package/dist/gitDelivery/agentOperationsRoutes.d.ts +33 -0
  97. package/dist/gitDelivery/agentOperationsRoutes.d.ts.map +1 -0
  98. package/dist/gitDelivery/agentOperationsRoutes.js +405 -0
  99. package/dist/gitDelivery/commitRoutes.d.ts +23 -0
  100. package/dist/gitDelivery/commitRoutes.d.ts.map +1 -0
  101. package/dist/gitDelivery/commitRoutes.js +204 -0
  102. package/dist/gitDelivery/evidenceRoutes.d.ts +9 -0
  103. package/dist/gitDelivery/evidenceRoutes.d.ts.map +1 -0
  104. package/dist/gitDelivery/evidenceRoutes.js +101 -0
  105. package/dist/gitDelivery/execution.d.ts +38 -0
  106. package/dist/gitDelivery/execution.d.ts.map +1 -0
  107. package/dist/gitDelivery/execution.js +117 -0
  108. package/dist/gitDelivery/localMutationRoutes.d.ts +30 -0
  109. package/dist/gitDelivery/localMutationRoutes.d.ts.map +1 -0
  110. package/dist/gitDelivery/localMutationRoutes.js +165 -0
  111. package/dist/gitDelivery/mergeExecution.d.ts +63 -0
  112. package/dist/gitDelivery/mergeExecution.d.ts.map +1 -0
  113. package/dist/gitDelivery/mergeExecution.js +168 -0
  114. package/dist/gitDelivery/mergeRoutes.d.ts +12 -0
  115. package/dist/gitDelivery/mergeRoutes.d.ts.map +1 -0
  116. package/dist/gitDelivery/mergeRoutes.js +218 -0
  117. package/dist/gitDelivery/mutationEvidenceLedger.d.ts +23 -0
  118. package/dist/gitDelivery/mutationEvidenceLedger.d.ts.map +1 -0
  119. package/dist/gitDelivery/mutationEvidenceLedger.js +87 -0
  120. package/dist/gitDelivery/prExecution.d.ts +54 -0
  121. package/dist/gitDelivery/prExecution.d.ts.map +1 -0
  122. package/dist/gitDelivery/prExecution.js +192 -0
  123. package/dist/gitDelivery/prRoutes.d.ts +12 -0
  124. package/dist/gitDelivery/prRoutes.d.ts.map +1 -0
  125. package/dist/gitDelivery/prRoutes.js +256 -0
  126. package/dist/gitDelivery/pushExecution.d.ts +43 -0
  127. package/dist/gitDelivery/pushExecution.d.ts.map +1 -0
  128. package/dist/gitDelivery/pushExecution.js +124 -0
  129. package/dist/gitDelivery/pushRoutes.d.ts +12 -0
  130. package/dist/gitDelivery/pushRoutes.d.ts.map +1 -0
  131. package/dist/gitDelivery/pushRoutes.js +200 -0
  132. package/dist/gitDelivery/requestGuards.d.ts +15 -0
  133. package/dist/gitDelivery/requestGuards.d.ts.map +1 -0
  134. package/dist/gitDelivery/requestGuards.js +97 -0
  135. package/dist/gitDelivery/syncEvidence.d.ts +37 -0
  136. package/dist/gitDelivery/syncEvidence.d.ts.map +1 -0
  137. package/dist/gitDelivery/syncEvidence.js +85 -0
  138. package/dist/gitDelivery/syncExecution.d.ts +30 -0
  139. package/dist/gitDelivery/syncExecution.d.ts.map +1 -0
  140. package/dist/gitDelivery/syncExecution.js +266 -0
  141. package/dist/gitDelivery/syncRoutes.d.ts +13 -0
  142. package/dist/gitDelivery/syncRoutes.d.ts.map +1 -0
  143. package/dist/gitDelivery/syncRoutes.js +200 -0
  144. package/dist/gitPorcelainStatus.d.ts +15 -0
  145. package/dist/gitPorcelainStatus.d.ts.map +1 -0
  146. package/dist/gitPorcelainStatus.js +104 -0
  147. package/dist/gitRepositoryReads.d.ts +10 -0
  148. package/dist/gitRepositoryReads.d.ts.map +1 -0
  149. package/dist/gitRepositoryReads.js +314 -0
  150. package/dist/gitRepositoryRoutes.d.ts +7 -0
  151. package/dist/gitRepositoryRoutes.d.ts.map +1 -0
  152. package/dist/gitRepositoryRoutes.js +221 -0
  153. package/dist/gitRoutes.d.ts +66 -0
  154. package/dist/gitRoutes.d.ts.map +1 -0
  155. package/dist/gitRoutes.js +543 -0
  156. package/dist/governed-workflow.d.ts +2 -0
  157. package/dist/governed-workflow.d.ts.map +1 -1
  158. package/dist/governed-workflow.js +4 -0
  159. package/dist/grounded-qa.d.ts +11 -0
  160. package/dist/grounded-qa.d.ts.map +1 -1
  161. package/dist/grounded-qa.js +13 -4
  162. package/dist/headers.d.ts +4 -1
  163. package/dist/headers.d.ts.map +1 -1
  164. package/dist/headers.js +11 -4
  165. package/dist/index.d.ts +8 -1
  166. package/dist/index.d.ts.map +1 -1
  167. package/dist/index.js +9 -1
  168. package/dist/qualityIntelligence/figmaSnapshotRoutes.d.ts +1 -1
  169. package/dist/qualityIntelligence/figmaSnapshotRoutes.d.ts.map +1 -1
  170. package/dist/qualityIntelligence/figmaSnapshotRoutes.js +1 -1
  171. package/dist/read-handlers.d.ts +5 -0
  172. package/dist/read-handlers.d.ts.map +1 -1
  173. package/dist/read-handlers.js +57 -1
  174. package/dist/routes.d.ts.map +1 -1
  175. package/dist/routes.js +259 -6
  176. package/dist/run-engine.d.ts.map +1 -1
  177. package/dist/run-engine.js +3 -0
  178. package/dist/run-handlers.d.ts.map +1 -1
  179. package/dist/run-handlers.js +74 -4
  180. package/dist/run-request.d.ts +11 -0
  181. package/dist/run-request.d.ts.map +1 -1
  182. package/dist/run-request.js +158 -10
  183. package/dist/runtime/capabilityDetector.d.ts +38 -0
  184. package/dist/runtime/capabilityDetector.d.ts.map +1 -0
  185. package/dist/runtime/capabilityDetector.js +443 -0
  186. package/dist/runtime/capabilityRoutes.d.ts +9 -0
  187. package/dist/runtime/capabilityRoutes.d.ts.map +1 -0
  188. package/dist/runtime/capabilityRoutes.js +45 -0
  189. package/dist/runtime/containerEngineDetector.d.ts +17 -0
  190. package/dist/runtime/containerEngineDetector.d.ts.map +1 -0
  191. package/dist/runtime/containerEngineDetector.js +222 -0
  192. package/dist/runtime/containerRoutes.d.ts +8 -0
  193. package/dist/runtime/containerRoutes.d.ts.map +1 -0
  194. package/dist/runtime/containerRoutes.js +207 -0
  195. package/dist/runtime/containerRunner-errors.d.ts +18 -0
  196. package/dist/runtime/containerRunner-errors.d.ts.map +1 -0
  197. package/dist/runtime/containerRunner-errors.js +42 -0
  198. package/dist/runtime/containerRunner-evidence.d.ts +24 -0
  199. package/dist/runtime/containerRunner-evidence.d.ts.map +1 -0
  200. package/dist/runtime/containerRunner-evidence.js +74 -0
  201. package/dist/runtime/containerRunner.d.ts +37 -0
  202. package/dist/runtime/containerRunner.d.ts.map +1 -0
  203. package/dist/runtime/containerRunner.js +443 -0
  204. package/dist/server.d.ts.map +1 -1
  205. package/dist/server.js +24 -4
  206. package/dist/store/schema.d.ts +1 -1
  207. package/dist/store/schema.d.ts.map +1 -1
  208. package/dist/store/schema.js +62 -1
  209. package/dist/task-workspace/active-store.d.ts +21 -0
  210. package/dist/task-workspace/active-store.d.ts.map +1 -0
  211. package/dist/task-workspace/active-store.js +55 -0
  212. package/dist/task-workspace/authorization.d.ts +7 -0
  213. package/dist/task-workspace/authorization.d.ts.map +1 -0
  214. package/dist/task-workspace/authorization.js +54 -0
  215. package/dist/task-workspace/binding.d.ts +3 -0
  216. package/dist/task-workspace/binding.d.ts.map +1 -0
  217. package/dist/task-workspace/binding.js +22 -0
  218. package/dist/task-workspace/cleanup.d.ts +4 -0
  219. package/dist/task-workspace/cleanup.d.ts.map +1 -0
  220. package/dist/task-workspace/cleanup.js +428 -0
  221. package/dist/task-workspace/errors.d.ts +14 -0
  222. package/dist/task-workspace/errors.d.ts.map +1 -0
  223. package/dist/task-workspace/errors.js +81 -0
  224. package/dist/task-workspace/evidence.d.ts +32 -0
  225. package/dist/task-workspace/evidence.d.ts.map +1 -0
  226. package/dist/task-workspace/evidence.js +52 -0
  227. package/dist/task-workspace/field-safety.d.ts +3 -0
  228. package/dist/task-workspace/field-safety.d.ts.map +1 -0
  229. package/dist/task-workspace/field-safety.js +42 -0
  230. package/dist/task-workspace/health.d.ts +4 -0
  231. package/dist/task-workspace/health.d.ts.map +1 -0
  232. package/dist/task-workspace/health.js +163 -0
  233. package/dist/task-workspace/lifecycle.d.ts +3 -0
  234. package/dist/task-workspace/lifecycle.d.ts.map +1 -0
  235. package/dist/task-workspace/lifecycle.js +248 -0
  236. package/dist/task-workspace/locks.d.ts +13 -0
  237. package/dist/task-workspace/locks.d.ts.map +1 -0
  238. package/dist/task-workspace/locks.js +44 -0
  239. package/dist/task-workspace/managed-root.d.ts +7 -0
  240. package/dist/task-workspace/managed-root.d.ts.map +1 -0
  241. package/dist/task-workspace/managed-root.js +98 -0
  242. package/dist/task-workspace/mutex.d.ts +8 -0
  243. package/dist/task-workspace/mutex.d.ts.map +1 -0
  244. package/dist/task-workspace/mutex.js +82 -0
  245. package/dist/task-workspace/naming.d.ts +15 -0
  246. package/dist/task-workspace/naming.d.ts.map +1 -0
  247. package/dist/task-workspace/naming.js +0 -0
  248. package/dist/task-workspace/provisioning.d.ts +3 -0
  249. package/dist/task-workspace/provisioning.d.ts.map +1 -0
  250. package/dist/task-workspace/provisioning.js +528 -0
  251. package/dist/task-workspace/reconciliation.d.ts +15 -0
  252. package/dist/task-workspace/reconciliation.d.ts.map +1 -0
  253. package/dist/task-workspace/reconciliation.js +274 -0
  254. package/dist/task-workspace/repair.d.ts +3 -0
  255. package/dist/task-workspace/repair.d.ts.map +1 -0
  256. package/dist/task-workspace/repair.js +286 -0
  257. package/dist/task-workspace/routes.d.ts +19 -0
  258. package/dist/task-workspace/routes.d.ts.map +1 -0
  259. package/dist/task-workspace/routes.js +481 -0
  260. package/dist/task-workspace/store.d.ts +12 -0
  261. package/dist/task-workspace/store.d.ts.map +1 -0
  262. package/dist/task-workspace/store.js +128 -0
  263. package/dist/task-workspace/types.d.ts +170 -0
  264. package/dist/task-workspace/types.d.ts.map +1 -0
  265. package/dist/task-workspace/types.js +5 -0
  266. package/dist/voice-action-governance.d.ts +23 -0
  267. package/dist/voice-action-governance.d.ts.map +1 -0
  268. package/dist/voice-action-governance.js +126 -0
  269. package/dist/voice-handlers.d.ts +6 -0
  270. package/dist/voice-handlers.d.ts.map +1 -0
  271. package/dist/voice-handlers.js +570 -0
  272. package/dist/voice-realtime-grounded-tool.d.ts +31 -0
  273. package/dist/voice-realtime-grounded-tool.d.ts.map +1 -0
  274. package/dist/voice-realtime-grounded-tool.js +322 -0
  275. package/dist/voice-realtime.d.ts +69 -0
  276. package/dist/voice-realtime.d.ts.map +1 -0
  277. package/dist/voice-realtime.js +787 -0
  278. package/dist/workspace-state-handlers.d.ts +5 -0
  279. package/dist/workspace-state-handlers.d.ts.map +1 -0
  280. package/dist/workspace-state-handlers.js +106 -0
  281. package/package.json +20 -19
@@ -0,0 +1,266 @@
1
+ // Bounded fetch/pull sync execution core (Issue #1573, Epic #1572).
2
+ //
3
+ // fetch/pull are NOT governed mutations: they have no `GitDeliveryActionKind` and do NOT enter the
4
+ // #472 `runGitMutation` kernel. That taxonomy is frozen (#1572 reuse contract §3); widening it to add
5
+ // fetch/pull would weaken the governed control plane. Instead this module mirrors the push route
6
+ // STRUCTURE while reusing the hardened, fixed-arg process runner (`defaultGitProcessRunner`) and the
7
+ // shared porcelain-v2 parser. Two operations:
8
+ //
9
+ // * buildSyncPreview — READ-ONLY readiness: parse `status --porcelain=v2 --branch` + remote names to
10
+ // compute branch/detached/upstream/ahead/behind/hasRemote/hasUpstream/dirty + an executable gate
11
+ // and a typed blockReason.
12
+ // * runSyncExecute — requires an executable preview before running the bounded fetch/pull command,
13
+ // classifies exitCode/stderr/stdout/truncated, and re-reads ahead/behind after success.
14
+ //
15
+ // Pure parsing lives in gitPorcelainStatus.ts; this module owns only the bounded process effect and
16
+ // the deterministic outcome classifier. Both are seam-injectable for tests.
17
+ import { GIT_SYNC_SCHEMA_VERSION } from "@oscharko-dev/keiko-contracts";
18
+ import { defaultGitNetworkProcessRunner, defaultGitProcessRunner, } from "../gitRoutes.js";
19
+ import { parsePorcelainV2Branch } from "../gitPorcelainStatus.js";
20
+ const DEFAULT_SYNC_MAX_BYTES = 128 * 1024;
21
+ const DEFAULT_SYNC_TIMEOUT_MS = 30_000;
22
+ // An injected `seams.runner` overrides BOTH runners so tests stay deterministic; in production the
23
+ // local reads use the hardened `defaultGitProcessRunner` while the fetch/pull command uses the
24
+ // credential-capable `defaultGitNetworkProcessRunner` (see networkGitEnv in gitRoutes.ts).
25
+ function normalizeSeams(seams) {
26
+ return {
27
+ readRunner: seams.runner ?? defaultGitProcessRunner,
28
+ networkRunner: seams.runner ?? defaultGitNetworkProcessRunner,
29
+ maxBytes: seams.maxBytes ?? DEFAULT_SYNC_MAX_BYTES,
30
+ timeoutMs: seams.timeoutMs ?? DEFAULT_SYNC_TIMEOUT_MS,
31
+ };
32
+ }
33
+ function runWith(runner, repoRoot, seams, args) {
34
+ return runner(["--no-pager", "--no-optional-locks", "-C", repoRoot, ...args], {
35
+ cwd: repoRoot,
36
+ maxBytes: seams.maxBytes,
37
+ timeoutMs: seams.timeoutMs,
38
+ });
39
+ }
40
+ // Local read (status / remote / post-op re-read): config-isolated, never authenticates.
41
+ function runGit(repoRoot, seams, args) {
42
+ return runWith(seams.readRunner, repoRoot, seams, args);
43
+ }
44
+ // The actual fetch/pull network command: credential-capable env, still GIT_TERMINAL_PROMPT=0.
45
+ function runNetworkGit(repoRoot, seams, args) {
46
+ return runWith(seams.networkRunner, repoRoot, seams, args);
47
+ }
48
+ // `git remote` (names only — never URLs) tells us whether a fetch target exists at all.
49
+ function parseRemoteNames(stdout) {
50
+ return stdout
51
+ .split(/\r?\n/u)
52
+ .map((line) => line.trim())
53
+ .filter((line) => line.length > 0);
54
+ }
55
+ // --- preview ---------------------------------------------------------------
56
+ function hasRequestedRemote(remoteNames, remote) {
57
+ return remote === undefined ? remoteNames.length > 0 : remoteNames.includes(remote);
58
+ }
59
+ function previewBlockReason(operation, status, hasRemote) {
60
+ if (!hasRemote)
61
+ return "no-remote";
62
+ if (operation === "fetch")
63
+ return undefined;
64
+ if (status.detached)
65
+ return "detached-head";
66
+ if (status.upstream === undefined)
67
+ return "no-upstream";
68
+ return undefined;
69
+ }
70
+ function previewFor(operation, status, remote, hasRemote) {
71
+ const blockReason = previewBlockReason(operation, status, hasRemote);
72
+ return {
73
+ schemaVersion: GIT_SYNC_SCHEMA_VERSION,
74
+ operation,
75
+ available: true,
76
+ state: "available",
77
+ branch: status.branch,
78
+ detached: status.detached,
79
+ upstream: status.upstream,
80
+ remote,
81
+ ahead: status.ahead,
82
+ behind: status.behind,
83
+ hasRemote,
84
+ hasUpstream: status.upstream !== undefined,
85
+ dirty: status.dirty,
86
+ executable: blockReason === undefined,
87
+ blockReason,
88
+ };
89
+ }
90
+ /**
91
+ * Read-only fetch/pull readiness. Runs `status --porcelain=v2 --branch -z` + `git remote` (names only)
92
+ * and projects a content-free preview with an executable gate. Throws only when the status read fails
93
+ * (not a repository / unsafe owner); the caller maps that to a 409.
94
+ */
95
+ export async function buildSyncPreview(operation, repoRoot, remote, seams = {}) {
96
+ const normalized = normalizeSeams(seams);
97
+ const status = await runGit(repoRoot, normalized, [
98
+ "status",
99
+ "--porcelain=v2",
100
+ "--branch",
101
+ "-z",
102
+ "--untracked-files=all",
103
+ ]);
104
+ if (status.exitCode !== 0) {
105
+ throw new Error("git status failed for the sync preview");
106
+ }
107
+ const parsed = parsePorcelainV2Branch(status.stdout);
108
+ const remotesResult = await runGit(repoRoot, normalized, ["remote"]);
109
+ const remoteNames = remotesResult.exitCode === 0 ? parseRemoteNames(remotesResult.stdout) : [];
110
+ return previewFor(operation, parsed, remote, hasRequestedRemote(remoteNames, remote));
111
+ }
112
+ function syncArgs(operation, remote) {
113
+ const remoteArgs = remote === undefined ? [] : [remote];
114
+ return operation === "fetch"
115
+ ? ["fetch", "--no-tags", ...remoteArgs]
116
+ : ["pull", "--ff-only", "--no-edit", ...remoteArgs];
117
+ }
118
+ // Classifies the most-specific failure the stderr surfaces. Order matters: ownership, host trust, and
119
+ // auth checks precede generic remote/repository checks so trust and credential failures stay precise.
120
+ function isUnsafeRepositoryStderr(text) {
121
+ return text.includes("dubious ownership") || text.includes("safe.directory");
122
+ }
123
+ function isUntrustedHostKeyStderr(text) {
124
+ return (text.includes("host key verification failed") ||
125
+ text.includes("remote host identification has changed") ||
126
+ text.includes("strict host key checking") ||
127
+ text.includes("offending key"));
128
+ }
129
+ function isAuthFailedStderr(text) {
130
+ return (text.includes("could not read username") ||
131
+ text.includes("authentication failed") ||
132
+ text.includes("permission denied") ||
133
+ text.includes("could not read from remote") ||
134
+ text.includes("terminal prompts disabled"));
135
+ }
136
+ function isNoRemoteStderr(text) {
137
+ return text.includes("no such remote") || text.includes("does not appear to be a git repository");
138
+ }
139
+ function classifyStderr(stderr) {
140
+ const text = stderr.toLowerCase();
141
+ if (isUnsafeRepositoryStderr(text))
142
+ return "unsafe-repository";
143
+ if (isUntrustedHostKeyStderr(text))
144
+ return "untrusted-host-key";
145
+ if (isAuthFailedStderr(text))
146
+ return "auth-failed";
147
+ if (isNoRemoteStderr(text))
148
+ return "no-remote";
149
+ return undefined;
150
+ }
151
+ // Pull-only refusal reasons layered on top of the shared classifier.
152
+ function classifyPullStderr(stderr) {
153
+ const text = stderr.toLowerCase();
154
+ // A pull on a detached HEAD aborts with "You are not currently on a branch." — the execute-side
155
+ // mirror of the preview block reason, keeping the sync taxonomy fully live from execute.
156
+ if (text.includes("not currently on a branch"))
157
+ return "detached-head";
158
+ if (text.includes("there is no tracking information") ||
159
+ text.includes("no tracking information for the current branch")) {
160
+ return "no-upstream";
161
+ }
162
+ if (text.includes("not possible to fast-forward"))
163
+ return "not-fast-forward";
164
+ if (text.includes("local changes") || text.includes("would be overwritten")) {
165
+ return "dirty-worktree";
166
+ }
167
+ return undefined;
168
+ }
169
+ function isAlreadyUpToDate(stdout) {
170
+ return stdout.toLowerCase().includes("already up to date");
171
+ }
172
+ function classifyOutcome(operation, result) {
173
+ if (result.truncated)
174
+ return "timeout";
175
+ if (result.exitCode === 127)
176
+ return "git-missing";
177
+ const shared = classifyStderr(result.stderr);
178
+ if (shared !== undefined)
179
+ return shared;
180
+ if (operation === "pull") {
181
+ const pullReason = classifyPullStderr(result.stderr);
182
+ if (pullReason !== undefined)
183
+ return pullReason;
184
+ }
185
+ if (result.exitCode === 0) {
186
+ if (operation === "pull" && isAlreadyUpToDate(result.stdout))
187
+ return "up-to-date";
188
+ return "succeeded";
189
+ }
190
+ return "git-error";
191
+ }
192
+ function isSettledOk(outcome) {
193
+ return outcome === "succeeded" || outcome === "up-to-date";
194
+ }
195
+ function blockedOutcomeFor(preview) {
196
+ switch (preview.blockReason) {
197
+ case "no-remote":
198
+ return "no-remote";
199
+ case "no-upstream":
200
+ return "no-upstream";
201
+ case "detached-head":
202
+ return "detached-head";
203
+ case "git-missing":
204
+ return "git-missing";
205
+ case "unsafe-repository":
206
+ return "unsafe-repository";
207
+ case "unavailable":
208
+ case undefined:
209
+ return "git-error";
210
+ }
211
+ }
212
+ function blockedResultFor(preview) {
213
+ return {
214
+ outcome: blockedOutcomeFor(preview),
215
+ branch: preview.branch,
216
+ upstream: preview.upstream,
217
+ ahead: preview.ahead,
218
+ behind: preview.behind,
219
+ truncated: false,
220
+ };
221
+ }
222
+ // Re-reads branch/upstream/ahead/behind after a settled op so the response reflects the post-sync
223
+ // position. Best-effort: any failure tolerates and omits the counts.
224
+ async function readPostState(repoRoot, seams) {
225
+ try {
226
+ const status = await runGit(repoRoot, seams, [
227
+ "status",
228
+ "--porcelain=v2",
229
+ "--branch",
230
+ "-z",
231
+ "--untracked-files=all",
232
+ ]);
233
+ if (status.exitCode !== 0)
234
+ return {};
235
+ const parsed = parsePorcelainV2Branch(status.stdout);
236
+ return {
237
+ branch: parsed.branch,
238
+ upstream: parsed.upstream,
239
+ ahead: parsed.ahead,
240
+ behind: parsed.behind,
241
+ };
242
+ }
243
+ catch {
244
+ return {};
245
+ }
246
+ }
247
+ /**
248
+ * Runs ONE bounded fetch/pull and classifies the outcome. Never governs (no kernel, no policy) — the
249
+ * control surface is the fixed argv + hardened env of the reused runner. After a settled op the
250
+ * branch/upstream/ahead/behind are re-read best-effort for the response.
251
+ */
252
+ export async function runSyncExecute(operation, repoRoot, remote, seams = {}, preflight) {
253
+ const normalized = normalizeSeams(seams);
254
+ const preview = preflight ?? (await buildSyncPreview(operation, repoRoot, remote, seams));
255
+ if (!preview.executable)
256
+ return blockedResultFor(preview);
257
+ // ONLY the network fetch/pull uses the credential-capable runner; the post-state re-read below
258
+ // stays on the hardened local read runner.
259
+ const result = await runNetworkGit(repoRoot, normalized, syncArgs(operation, remote));
260
+ const outcome = classifyOutcome(operation, result);
261
+ if (!isSettledOk(outcome)) {
262
+ return { outcome, truncated: result.truncated };
263
+ }
264
+ const post = await readPostState(repoRoot, normalized);
265
+ return { outcome, ...post, truncated: result.truncated };
266
+ }
@@ -0,0 +1,13 @@
1
+ import type { GitSyncOperation } from "@oscharko-dev/keiko-contracts";
2
+ import type { RouteContext, RouteDefinition, RouteResult } from "../routes.js";
3
+ import type { UiHandlerDeps } from "../deps.js";
4
+ import { type GitDeliverySyncSeams } from "./syncExecution.js";
5
+ export type GitDeliverySyncErrorCode = "GIT_DELIVERY_SYNC_BAD_REQUEST" | "GIT_DELIVERY_SYNC_PAYLOAD_TOO_LARGE" | "GIT_DELIVERY_SYNC_FORBIDDEN_PAYLOAD" | "GIT_DELIVERY_SYNC_UNKNOWN_PROJECT" | "GIT_DELIVERY_SYNC_WORKTREE_UNAVAILABLE";
6
+ export interface GitDeliverySyncRouteOptions {
7
+ readonly execution?: GitDeliverySyncSeams;
8
+ }
9
+ export declare const createHandleSyncPreview: (operation: GitSyncOperation, options?: GitDeliverySyncRouteOptions) => ((ctx: RouteContext, deps: UiHandlerDeps) => Promise<RouteResult>);
10
+ export declare const createHandleSyncExecute: (operation: GitSyncOperation, options?: GitDeliverySyncRouteOptions) => ((ctx: RouteContext, deps: UiHandlerDeps) => Promise<RouteResult>);
11
+ export declare const createGitDeliverySyncRouteGroup: (options?: GitDeliverySyncRouteOptions) => readonly RouteDefinition[];
12
+ export declare const GIT_DELIVERY_SYNC_ROUTE_GROUP: readonly RouteDefinition[];
13
+ //# sourceMappingURL=syncRoutes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"syncRoutes.d.ts","sourceRoot":"","sources":["../../src/gitDelivery/syncRoutes.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAEV,gBAAgB,EAEjB,MAAM,+BAA+B,CAAC;AAEvC,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC/E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAWhD,OAAO,EAGL,KAAK,oBAAoB,EAE1B,MAAM,oBAAoB,CAAC;AAU5B,MAAM,MAAM,wBAAwB,GAChC,+BAA+B,GAC/B,qCAAqC,GACrC,qCAAqC,GACrC,mCAAmC,GACnC,wCAAwC,CAAC;AAmB7C,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,SAAS,CAAC,EAAE,oBAAoB,CAAC;CAC3C;AA4ED,eAAO,MAAM,uBAAuB,GAClC,WAAW,gBAAgB,EAC3B,UAAS,2BAAgC,KACxC,CAAC,CAAC,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,aAAa,KAAK,OAAO,CAAC,WAAW,CAAC,CAiBnE,CAAC;AAkDF,eAAO,MAAM,uBAAuB,GAClC,WAAW,gBAAgB,EAC3B,UAAS,2BAAgC,KACxC,CAAC,CAAC,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,aAAa,KAAK,OAAO,CAAC,WAAW,CAAC,CAgCnE,CAAC;AAIF,eAAO,MAAM,+BAA+B,GAC1C,UAAS,2BAAgC,KACxC,SAAS,eAAe,EAqB1B,CAAC;AAEF,eAAO,MAAM,6BAA6B,EAAE,SAAS,eAAe,EACjC,CAAC"}
@@ -0,0 +1,200 @@
1
+ // Governed fetch/pull sync routes: read-only preview + bounded execute (Issue #1573, Epic #1572).
2
+ //
3
+ // * POST /api/git-delivery/{fetch,pull}/preview — READ-ONLY. Projects the sync readiness envelope
4
+ // (branch / upstream / ahead / behind / hasRemote / hasUpstream / dirty + an executable gate and
5
+ // typed blockReason). Never mutates, never records evidence.
6
+ // * POST /api/git-delivery/{fetch,pull}/execute — Requires an executable preview before running
7
+ // ONE bounded fetch/pull through the credential-capable runner (NOT the #472 kernel —
8
+ // fetch/pull have no GitDeliveryActionKind) and appends a content-free sync evidence record for
9
+ // the terminal outcome.
10
+ //
11
+ // Mirrors pushRoutes.ts: the same bounded body read, allowed-key whitelist, credential-shape +
12
+ // unsafe-format-char scans, isSafeGitRef operand guard plus configured-remote preflight,
13
+ // content-free typed error envelope, and a `createGitDeliverySyncRouteGroup(options)` factory with
14
+ // an injectable execution seam. CSRF + JSON
15
+ // content type are enforced CENTRALLY by server.ts for POST, so they are NOT re-checked here.
16
+ import { GIT_SYNC_SCHEMA_VERSION } from "@oscharko-dev/keiko-contracts";
17
+ import { resolveProjectWorkspace } from "./execution.js";
18
+ import { GitDeliveryBodyTooLargeError, hasOnlyAllowedKeys, isNonEmptyString, isPlainObject, readGitDeliveryBody, scanForbiddenStrings, scanUnsafeFormatChars, } from "./requestGuards.js";
19
+ import { buildSyncPreview, runSyncExecute, } from "./syncExecution.js";
20
+ import { gitSyncRepoIdHash, recordGitSyncEvidence, GIT_SYNC_EVIDENCE_SCHEMA_VERSION, } from "./syncEvidence.js";
21
+ const SAFE_MESSAGES = {
22
+ GIT_DELIVERY_SYNC_BAD_REQUEST: "The request body is not a valid git sync request.",
23
+ GIT_DELIVERY_SYNC_PAYLOAD_TOO_LARGE: "The git sync request exceeds the maximum size.",
24
+ GIT_DELIVERY_SYNC_FORBIDDEN_PAYLOAD: "The request contained a forbidden field. Requests may not carry credentials, headers, or URLs.",
25
+ GIT_DELIVERY_SYNC_UNKNOWN_PROJECT: "The requested project is not a known workspace.",
26
+ GIT_DELIVERY_SYNC_WORKTREE_UNAVAILABLE: "The repository worktree could not be inspected. Confirm the project is a Git repository.",
27
+ };
28
+ const errResult = (status, code) => ({
29
+ status,
30
+ body: { error: { code, message: SAFE_MESSAGES[code] } },
31
+ });
32
+ async function readParsed(req) {
33
+ let raw;
34
+ try {
35
+ raw = await readGitDeliveryBody(req);
36
+ }
37
+ catch (error) {
38
+ const result = error instanceof GitDeliveryBodyTooLargeError
39
+ ? errResult(413, "GIT_DELIVERY_SYNC_PAYLOAD_TOO_LARGE")
40
+ : errResult(400, "GIT_DELIVERY_SYNC_BAD_REQUEST");
41
+ return { ok: false, result };
42
+ }
43
+ try {
44
+ return { ok: true, value: JSON.parse(raw) };
45
+ }
46
+ catch {
47
+ return { ok: false, result: errResult(400, "GIT_DELIVERY_SYNC_BAD_REQUEST") };
48
+ }
49
+ }
50
+ // A remote alias operand: non-empty, no whitespace, no leading "-" (flag-injection guard), no ":"
51
+ // (refspec-injection guard), no NUL. Defence-in-depth so a malformed remote is a clean 400 rather than
52
+ // an internal execution error.
53
+ // eslint-disable-next-line no-control-regex -- intentionally matches control chars to REJECT them
54
+ const REF_CONTROL_CHAR = new RegExp("[\u0000-\u001f\u007f]");
55
+ function isSafeGitRef(value) {
56
+ if (typeof value !== "string" || value.length === 0)
57
+ return false;
58
+ if (/\s/.test(value))
59
+ return false;
60
+ if (value.startsWith("-"))
61
+ return false;
62
+ if (REF_CONTROL_CHAR.test(value))
63
+ return false;
64
+ if (value.includes(":"))
65
+ return false;
66
+ return true;
67
+ }
68
+ const ALLOWED_KEYS = new Set(["schemaVersion", "projectId", "remote"]);
69
+ // The credential-shape + unsafe-format-char boundary scans. Returns the typed error RouteResult or
70
+ // undefined when the payload is clean.
71
+ function scanError(parsed) {
72
+ if (scanForbiddenStrings(parsed)) {
73
+ return errResult(400, "GIT_DELIVERY_SYNC_FORBIDDEN_PAYLOAD");
74
+ }
75
+ if (scanUnsafeFormatChars(parsed)) {
76
+ return errResult(400, "GIT_DELIVERY_SYNC_BAD_REQUEST");
77
+ }
78
+ return undefined;
79
+ }
80
+ function validate(parsed) {
81
+ const bad = { kind: "err", result: errResult(400, "GIT_DELIVERY_SYNC_BAD_REQUEST") };
82
+ if (!isPlainObject(parsed) || !hasOnlyAllowedKeys(parsed, ALLOWED_KEYS))
83
+ return bad;
84
+ if (parsed.schemaVersion !== GIT_SYNC_SCHEMA_VERSION || !isNonEmptyString(parsed.projectId)) {
85
+ return bad;
86
+ }
87
+ const scanErr = scanError(parsed);
88
+ if (scanErr !== undefined)
89
+ return { kind: "err", result: scanErr };
90
+ if (parsed.remote !== undefined && !isSafeGitRef(parsed.remote))
91
+ return bad;
92
+ return { kind: "ok", value: { projectId: parsed.projectId, remote: parsed.remote } };
93
+ }
94
+ // ─── Preview handler (read-only) ────────────────────────────────────────────────────────────────
95
+ export const createHandleSyncPreview = (operation, options = {}) => {
96
+ const seams = options.execution ?? {};
97
+ return async (ctx, deps) => {
98
+ const read = await readParsed(ctx.req);
99
+ if (!read.ok)
100
+ return read.result;
101
+ const validation = validate(read.value);
102
+ if (validation.kind === "err")
103
+ return validation.result;
104
+ const { projectId, remote } = validation.value;
105
+ const workspace = resolveProjectWorkspace(deps, projectId);
106
+ if (workspace === undefined)
107
+ return errResult(404, "GIT_DELIVERY_SYNC_UNKNOWN_PROJECT");
108
+ try {
109
+ const preview = await buildSyncPreview(operation, workspace.root, remote, seams);
110
+ return { status: 200, body: deps.redactor(preview) };
111
+ }
112
+ catch {
113
+ return errResult(409, "GIT_DELIVERY_SYNC_WORKTREE_UNAVAILABLE");
114
+ }
115
+ };
116
+ };
117
+ // ─── Execute handler (bounded fetch/pull) ───────────────────────────────────────────────────────
118
+ function redactStringFor(deps) {
119
+ return (input) => deps.redactor(input);
120
+ }
121
+ function executeResponse(operation, remote, result) {
122
+ return {
123
+ schemaVersion: GIT_SYNC_SCHEMA_VERSION,
124
+ operation,
125
+ status: result.outcome,
126
+ available: true,
127
+ branch: result.branch,
128
+ upstream: result.upstream,
129
+ remote,
130
+ ahead: result.ahead,
131
+ behind: result.behind,
132
+ truncated: result.truncated,
133
+ };
134
+ }
135
+ function evidenceRecord(operation, remote, repoIdHash, before, result, recordedAtMs) {
136
+ return {
137
+ schemaVersion: GIT_SYNC_EVIDENCE_SCHEMA_VERSION,
138
+ operation,
139
+ outcome: result.outcome,
140
+ repoIdHash,
141
+ branch: result.branch ?? before?.branch,
142
+ remote,
143
+ aheadBefore: before?.ahead,
144
+ behindBefore: before?.behind,
145
+ aheadAfter: result.ahead,
146
+ behindAfter: result.behind,
147
+ recordedAtMs,
148
+ };
149
+ }
150
+ export const createHandleSyncExecute = (operation, options = {}) => {
151
+ const seams = options.execution ?? {};
152
+ const now = () => (seams.now ?? Date.now)();
153
+ return async (ctx, deps) => {
154
+ const read = await readParsed(ctx.req);
155
+ if (!read.ok)
156
+ return read.result;
157
+ const validation = validate(read.value);
158
+ if (validation.kind === "err")
159
+ return validation.result;
160
+ const { projectId, remote } = validation.value;
161
+ const workspace = resolveProjectWorkspace(deps, projectId);
162
+ if (workspace === undefined)
163
+ return errResult(404, "GIT_DELIVERY_SYNC_UNKNOWN_PROJECT");
164
+ let before;
165
+ try {
166
+ before = await buildSyncPreview(operation, workspace.root, remote, seams);
167
+ }
168
+ catch {
169
+ return errResult(409, "GIT_DELIVERY_SYNC_WORKTREE_UNAVAILABLE");
170
+ }
171
+ const result = await runSyncExecute(operation, workspace.root, remote, seams, before);
172
+ const record = evidenceRecord(operation, remote, gitSyncRepoIdHash(workspace.root), before, result, now());
173
+ recordGitSyncEvidence({ evidenceStore: deps.evidenceStore, redactString: redactStringFor(deps) }, record);
174
+ return { status: 200, body: deps.redactor(executeResponse(operation, remote, result)) };
175
+ };
176
+ };
177
+ // ─── Route group ───────────────────────────────────────────────────────────────────────────────
178
+ export const createGitDeliverySyncRouteGroup = (options = {}) => [
179
+ {
180
+ method: "POST",
181
+ pattern: "/api/git-delivery/fetch/preview",
182
+ handler: createHandleSyncPreview("fetch", options),
183
+ },
184
+ {
185
+ method: "POST",
186
+ pattern: "/api/git-delivery/fetch/execute",
187
+ handler: createHandleSyncExecute("fetch", options),
188
+ },
189
+ {
190
+ method: "POST",
191
+ pattern: "/api/git-delivery/pull/preview",
192
+ handler: createHandleSyncPreview("pull", options),
193
+ },
194
+ {
195
+ method: "POST",
196
+ pattern: "/api/git-delivery/pull/execute",
197
+ handler: createHandleSyncExecute("pull", options),
198
+ },
199
+ ];
200
+ export const GIT_DELIVERY_SYNC_ROUTE_GROUP = createGitDeliverySyncRouteGroup();
@@ -0,0 +1,15 @@
1
+ import type { GitUpstreamSummary } from "@oscharko-dev/keiko-contracts";
2
+ export interface PorcelainV2Status {
3
+ readonly branch?: string | undefined;
4
+ readonly detached: boolean;
5
+ readonly upstream?: GitUpstreamSummary | undefined;
6
+ readonly ahead: number;
7
+ readonly behind: number;
8
+ readonly stagedCount: number;
9
+ readonly unstagedCount: number;
10
+ readonly untrackedCount: number;
11
+ readonly conflictedCount: number;
12
+ readonly dirty: boolean;
13
+ }
14
+ export declare function parsePorcelainV2Branch(stdout: string): PorcelainV2Status;
15
+ //# sourceMappingURL=gitPorcelainStatus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gitPorcelainStatus.d.ts","sourceRoot":"","sources":["../src/gitPorcelainStatus.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AAExE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,kBAAkB,GAAG,SAAS,CAAC;IACnD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAoFD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,iBAAiB,CA2BxE"}
@@ -0,0 +1,104 @@
1
+ // Shared `git status --porcelain=v2 --branch -z` parser (Issue #1573, Epic #1572).
2
+ // Pure parsing only: no process, no filesystem, no clock. Centralizing the porcelain-v2 XY
3
+ // semantics keeps branch/ahead/behind and change-record counting audited in one place so the
4
+ // summary read route and the fetch/pull sync preview consume identical logic.
5
+ // Upstream ref splits on the first `/`: prefix is the remote, remainder the tracked branch.
6
+ function parseUpstreamRef(ref) {
7
+ const slash = ref.indexOf("/");
8
+ if (slash <= 0 || slash >= ref.length - 1) {
9
+ return { ref };
10
+ }
11
+ return { ref, remote: ref.slice(0, slash), branch: ref.slice(slash + 1) };
12
+ }
13
+ function parseAheadBehind(value) {
14
+ let ahead = 0;
15
+ let behind = 0;
16
+ for (const token of value.split(/\s+/u).filter((entry) => entry.length > 0)) {
17
+ const magnitude = Number.parseInt(token.slice(1), 10);
18
+ if (Number.isNaN(magnitude))
19
+ continue;
20
+ if (token.startsWith("+"))
21
+ ahead = magnitude;
22
+ else if (token.startsWith("-"))
23
+ behind = magnitude;
24
+ }
25
+ return { ahead, behind };
26
+ }
27
+ function applyHeader(record, headers) {
28
+ const body = record.slice(2).trim();
29
+ if (body.startsWith("branch.head ")) {
30
+ const name = body.slice("branch.head ".length).trim();
31
+ if (name === "(detached)")
32
+ headers.detached = true;
33
+ else if (name.length > 0)
34
+ headers.branch = name;
35
+ return;
36
+ }
37
+ if (body.startsWith("branch.upstream ")) {
38
+ const ref = body.slice("branch.upstream ".length).trim();
39
+ if (ref.length > 0)
40
+ headers.upstream = parseUpstreamRef(ref);
41
+ return;
42
+ }
43
+ if (body.startsWith("branch.ab ")) {
44
+ const parsed = parseAheadBehind(body.slice("branch.ab ".length));
45
+ headers.ahead = parsed.ahead;
46
+ headers.behind = parsed.behind;
47
+ }
48
+ }
49
+ // Ordinary (`1`) and rename/copy (`2`) records carry XY at field offset 2..4 of the
50
+ // space-split tokens: X = index (staged) status, Y = worktree (unstaged) status.
51
+ function applyOrdinaryChange(record, counts) {
52
+ const xy = record.split(" ")[1] ?? "..";
53
+ const index = xy[0] ?? ".";
54
+ const worktree = xy[1] ?? ".";
55
+ if (index !== ".")
56
+ counts.staged += 1;
57
+ if (worktree !== ".")
58
+ counts.unstaged += 1;
59
+ }
60
+ function applyChangeRecord(record, counts) {
61
+ if (record.startsWith("1 ") || record.startsWith("2 ")) {
62
+ applyOrdinaryChange(record, counts);
63
+ }
64
+ else if (record.startsWith("u ")) {
65
+ counts.conflicted += 1;
66
+ }
67
+ else if (record.startsWith("? ")) {
68
+ counts.untracked += 1;
69
+ }
70
+ }
71
+ // Rename/copy (`2`) records are followed by an extra NUL-separated original-path field that must
72
+ // be skipped so it is never mistaken for a change record.
73
+ function isRenameRecord(record) {
74
+ return record.startsWith("2 ");
75
+ }
76
+ export function parsePorcelainV2Branch(stdout) {
77
+ const records = stdout.split("\0").filter((record) => record.length > 0);
78
+ const headers = { detached: false, ahead: 0, behind: 0 };
79
+ const counts = { staged: 0, unstaged: 0, untracked: 0, conflicted: 0 };
80
+ let changeRecords = 0;
81
+ for (let index = 0; index < records.length; index += 1) {
82
+ const record = records[index] ?? "";
83
+ if (record.startsWith("# ")) {
84
+ applyHeader(record, headers);
85
+ continue;
86
+ }
87
+ applyChangeRecord(record, counts);
88
+ changeRecords += 1;
89
+ if (isRenameRecord(record))
90
+ index += 1;
91
+ }
92
+ return {
93
+ branch: headers.branch,
94
+ detached: headers.detached,
95
+ upstream: headers.upstream,
96
+ ahead: headers.ahead,
97
+ behind: headers.behind,
98
+ stagedCount: counts.staged,
99
+ unstagedCount: counts.unstaged,
100
+ untrackedCount: counts.untracked,
101
+ conflictedCount: counts.conflicted,
102
+ dirty: changeRecords > 0,
103
+ };
104
+ }
@@ -0,0 +1,10 @@
1
+ import type { GitHistoryEntry, GitRemoteSummary } from "@oscharko-dev/keiko-contracts";
2
+ import { type GitRouteOptions } from "./gitRoutes.js";
3
+ import type { RouteContext, RouteResult } from "./routes.js";
4
+ import type { UiHandlerDeps } from "./deps.js";
5
+ export declare function parseRemotes(stdout: string): readonly GitRemoteSummary[];
6
+ export declare function handleGitSummary(ctx: RouteContext, deps: UiHandlerDeps, rawOptions?: GitRouteOptions): Promise<RouteResult>;
7
+ export declare function handleGitRemotes(ctx: RouteContext, deps: UiHandlerDeps, rawOptions?: GitRouteOptions): Promise<RouteResult>;
8
+ export declare function parseHistory(stdout: string): readonly GitHistoryEntry[];
9
+ export declare function handleGitHistory(ctx: RouteContext, deps: UiHandlerDeps, rawOptions?: GitRouteOptions): Promise<RouteResult>;
10
+ //# sourceMappingURL=gitRepositoryReads.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gitRepositoryReads.d.ts","sourceRoot":"","sources":["../src/gitRepositoryReads.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,eAAe,EAGf,gBAAgB,EAMjB,MAAM,+BAA+B,CAAC;AAKvC,OAAO,EAML,KAAK,eAAe,EAGrB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AA4B/C,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,gBAAgB,EAAE,CAqBxE;AA0GD,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,aAAa,EACnB,UAAU,CAAC,EAAE,eAAe,GAC3B,OAAO,CAAC,WAAW,CAAC,CA2BtB;AAID,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,aAAa,EACnB,UAAU,CAAC,EAAE,eAAe,GAC3B,OAAO,CAAC,WAAW,CAAC,CA2BtB;AA4CD,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,eAAe,EAAE,CAMvE;AAgED,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,aAAa,EACnB,UAAU,CAAC,EAAE,eAAe,GAC3B,OAAO,CAAC,WAAW,CAAC,CAgCtB"}