@cuongtran001/kanna 1.3.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/assets/{arc-CAysfmX5.js → arc-DXtN3EWJ.js} +1 -1
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-BKqRYsm4.js → architectureDiagram-3BPJPVTR-CIGqW1z-.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-Ce7Eorec.js → blockDiagram-GPEHLZMM-BafNS_H6.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-CJ77zauH.js → c4Diagram-AAUBKEIU-YdAgRSis.js} +1 -1
- package/dist/client/assets/channel-BEYTrUAm.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-Rk_3zLxJ.js → chunk-2J33WTMH-eiP5y5G-.js} +1 -1
- package/dist/client/assets/{chunk-4BX2VUAB-DPIz6Nvf.js → chunk-4BX2VUAB-CvLGzPMF.js} +1 -1
- package/dist/client/assets/{chunk-55IACEB6-D-TkbGh2.js → chunk-55IACEB6-Y-S9Fb8A.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-B4NOSIZV.js → chunk-727SXJPM-6sJH1RI9.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-Cm_TGLYm.js → chunk-AQP2D5EJ-hV1r_IXO.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-Bu45BUVh.js → chunk-FMBD7UC4-CC3L28S5.js} +1 -1
- package/dist/client/assets/{chunk-ND2GUHAM-C1Bw8_gg.js → chunk-ND2GUHAM-BZ3czhdH.js} +1 -1
- package/dist/client/assets/{chunk-QZHKN3VN-DueG15o0.js → chunk-QZHKN3VN-Bjyd6Yr-.js} +1 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-CvnF5Ud6.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CvnF5Ud6.js +1 -0
- package/dist/client/assets/{cose-bilkent-S5V4N54A-YgpMurXf.js → cose-bilkent-S5V4N54A-CfxdDghE.js} +1 -1
- package/dist/client/assets/{dagre-BM42HDAG-Dop7Oq67.js → dagre-BM42HDAG-B8CddsbB.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-unMfBEJH.js → diagram-2AECGRRQ-F0yS0fVB.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-D9EJ9qMI.js → diagram-5GNKFQAL-cW0i0qlh.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-C5IykOIR.js → diagram-KO2AKTUF-BzAKMeJT.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-DJAuPngz.js → diagram-LMA3HP47-qdC4XqIl.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-B3wHFtS-.js → diagram-OG6HWLK6-CGcpi6Dh.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-Dm7kt9o5.js → erDiagram-TEJ5UH35-CxNTDicM.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-s9Lds3ny.js → flowDiagram-I6XJVG4X-qkGa5TqY.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-CRQNYdAf.js → ganttDiagram-6RSMTGT7-D9mVPoQi.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-DhYjK4Jb.js → gitGraphDiagram-PVQCEYII-DA0grAXe.js} +1 -1
- package/dist/client/assets/{index-CbSY9HfS.js → index-BqUY1Bm4.js} +1 -1
- package/dist/client/assets/{index-C0U2kwuB.js → index-TU1k6ov6.js} +5 -5
- package/dist/client/assets/{infoDiagram-5YYISTIA-CRKJ0PSH.js → infoDiagram-5YYISTIA-CGljtLs3.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CEAkB0IY.js → ishikawaDiagram-YF4QCWOH-CtC0xrmG.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-CyOK5_DG.js → journeyDiagram-JHISSGLW-URLnVZeN.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-BfKjiJSo.js → kanban-definition-UN3LZRKU-C2Q6aqQh.js} +1 -1
- package/dist/client/assets/{linear-DLR6miy3.js → linear-ByKHtsIx.js} +1 -1
- package/dist/client/assets/{mermaid.core-D7AdC_lE.js → mermaid.core-Dl_f2KLc.js} +4 -4
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-zYyJtDSa.js → mindmap-definition-RKZ34NQL-CaJ9IDLI.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-BgP8PFlo.js → pieDiagram-4H26LBE5-mmYaf2Oa.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-Di8h7J96.js → quadrantDiagram-W4KKPZXB-B_PO6OCC.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-DuTzFl3R.js → requirementDiagram-4Y6WPE33-M4huwJt1.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DBRoMm_T.js → sankeyDiagram-5OEKKPKP-B0hb1sWh.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-Bmb-1HUN.js → sequenceDiagram-3UESZ5HK-BAqSptnA.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-fnv5ayn-.js → stateDiagram-AJRCARHV-DTNkvPi5.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-CVN3oGTF.js +1 -0
- package/dist/client/assets/{timeline-definition-PNZ67QCA-ByWRdpQX.js → timeline-definition-PNZ67QCA-MgAjHz8Q.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN--2WGpVXW.js → vennDiagram-CIIHVFJN-DmKoO51W.js} +1 -1
- package/dist/client/assets/{wardley-L42UT6IY-24zlnzxK.js → wardley-L42UT6IY-CWcyHnYs.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-Cllvqb3K.js → wardleyDiagram-YWT4CUSO-BGIGmFP9.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-ZcdC6nPE.js → xychartDiagram-2RQKCTM6-D20YIvEl.js} +1 -1
- package/dist/client/index.html +1 -1
- package/package.json +1 -1
- package/src/server/agent-coordinator-types.ts +158 -0
- package/src/server/agent-coordinator.ts +919 -0
- package/src/server/agent-deps-builders.ts +425 -0
- package/src/server/agent.test.ts +22 -6
- package/src/server/agent.ts +35 -4987
- package/src/server/claude-autocontinue-commands.test.ts +494 -0
- package/src/server/claude-autocontinue-commands.ts +256 -0
- package/src/server/claude-cancel-handler.test.ts +472 -0
- package/src/server/claude-cancel-handler.ts +241 -0
- package/src/server/claude-chat-management.test.ts +395 -0
- package/src/server/claude-chat-management.ts +251 -0
- package/src/server/claude-harness-stream.test.ts +273 -0
- package/src/server/claude-harness-stream.ts +158 -0
- package/src/server/claude-loop-orch-commands.test.ts +579 -0
- package/src/server/claude-loop-orch-commands.ts +466 -0
- package/src/server/claude-message-normalizer.test.ts +213 -0
- package/src/server/claude-message-normalizer.ts +436 -0
- package/src/server/claude-prompt-helpers.test.ts +248 -0
- package/src/server/claude-prompt-helpers.ts +139 -0
- package/src/server/claude-sdk-queue.test.ts +169 -0
- package/src/server/claude-sdk-queue.ts +82 -0
- package/src/server/claude-send-command.test.ts +414 -0
- package/src/server/claude-send-command.ts +420 -0
- package/src/server/claude-session-config-helpers.test.ts +279 -0
- package/src/server/claude-session-config-helpers.ts +151 -0
- package/src/server/claude-session-config.test.ts +250 -0
- package/src/server/claude-session-config.ts +159 -0
- package/src/server/claude-session-error-handler.test.ts +498 -0
- package/src/server/claude-session-error-handler.ts +361 -0
- package/src/server/claude-session-lifecycle.test.ts +669 -0
- package/src/server/claude-session-lifecycle.ts +283 -0
- package/src/server/claude-session-rebuild.test.ts +224 -0
- package/src/server/claude-session-rebuild.ts +101 -0
- package/src/server/claude-session-runner.test.ts +523 -0
- package/src/server/claude-session-runner.ts +521 -0
- package/src/server/claude-session-spawner.test.ts +401 -0
- package/src/server/claude-session-spawner.ts +401 -0
- package/src/server/claude-session-start.test.ts +205 -0
- package/src/server/claude-session-start.ts +277 -0
- package/src/server/claude-session-state-queries.test.ts +344 -0
- package/src/server/claude-session-state-queries.ts +160 -0
- package/src/server/claude-session-state.ts +104 -0
- package/src/server/claude-slash-commands.test.ts +277 -0
- package/src/server/claude-slash-commands.ts +234 -0
- package/src/server/claude-spawn-helpers.test.ts +221 -0
- package/src/server/claude-spawn-helpers.ts +179 -0
- package/src/server/claude-steer-log.test.ts +243 -0
- package/src/server/claude-steer-log.ts +58 -0
- package/src/server/claude-subagent-tool-response.test.ts +213 -0
- package/src/server/claude-subagent-tool-response.ts +165 -0
- package/src/server/claude-subagent-wiring.test.ts +263 -0
- package/src/server/claude-subagent-wiring.ts +324 -0
- package/src/server/claude-tool-respond.test.ts +321 -0
- package/src/server/claude-tool-respond.ts +142 -0
- package/src/server/claude-turn-runner.test.ts +310 -0
- package/src/server/claude-turn-runner.ts +228 -0
- package/src/server/claude-turn-starter.test.ts +266 -0
- package/src/server/claude-turn-starter.ts +566 -0
- package/src/server/claude-usage-math.test.ts +169 -0
- package/src/server/claude-usage-math.ts +132 -0
- package/src/server/diff-store-branch-ops.adapter.test.ts +332 -0
- package/src/server/diff-store-branch-ops.adapter.ts +602 -0
- package/src/server/diff-store-commit-ops.adapter.test.ts +358 -0
- package/src/server/diff-store-commit-ops.adapter.ts +258 -0
- package/src/server/diff-store-errors.ts +85 -0
- package/src/server/diff-store-file-ops.adapter.ts +212 -0
- package/src/server/diff-store-git-branch.adapter.ts +686 -0
- package/src/server/diff-store-io.adapter.ts +27 -0
- package/src/server/diff-store-parse.test.ts +212 -0
- package/src/server/diff-store-parse.ts +122 -0
- package/src/server/diff-store-state.ts +82 -0
- package/src/server/diff-store.ts +76 -1854
- package/src/server/event-store-apply.test.ts +280 -0
- package/src/server/event-store-apply.ts +132 -0
- package/src/server/event-store-chat-lifecycle.test.ts +408 -0
- package/src/server/event-store-chat-lifecycle.ts +441 -0
- package/src/server/event-store-entity-write.ts +356 -0
- package/src/server/event-store-helpers.test.ts +230 -0
- package/src/server/event-store-helpers.ts +235 -0
- package/src/server/event-store-init.ts +276 -0
- package/src/server/event-store-messages.adapter.test.ts +365 -0
- package/src/server/event-store-messages.adapter.ts +213 -0
- package/src/server/event-store-orch-subscription.ts +124 -0
- package/src/server/event-store-orch.test.ts +462 -0
- package/src/server/event-store-orch.ts +312 -0
- package/src/server/event-store-peripheral-events.adapter.test.ts +254 -0
- package/src/server/event-store-peripheral-events.adapter.ts +105 -0
- package/src/server/event-store-snapshot.test.ts +597 -0
- package/src/server/event-store-snapshot.ts +658 -0
- package/src/server/event-store-subagent.test.ts +408 -0
- package/src/server/event-store-subagent.ts +198 -0
- package/src/server/event-store-tool-requests.test.ts +210 -0
- package/src/server/event-store-tool-requests.ts +131 -0
- package/src/server/event-store-transcript-write.adapter.ts +285 -0
- package/src/server/event-store-write-ops.test.ts +554 -0
- package/src/server/event-store-write-ops.ts +529 -0
- package/src/server/event-store.test.ts +35 -0
- package/src/server/event-store.ts +251 -2285
- package/src/server/harness-types.ts +15 -1
- package/src/server/oauth-errors.ts +19 -0
- package/src/server/ws-router-agent-ctrl.test.ts +187 -0
- package/src/server/ws-router-agent-ctrl.ts +137 -0
- package/src/server/ws-router-broadcast.ts +488 -0
- package/src/server/ws-router-chat.test.ts +461 -0
- package/src/server/ws-router-chat.ts +279 -0
- package/src/server/ws-router-defaults.test.ts +283 -0
- package/src/server/ws-router-defaults.ts +434 -0
- package/src/server/ws-router-diff.test.ts +167 -0
- package/src/server/ws-router-diff.ts +250 -0
- package/src/server/ws-router-envelope.ts +329 -0
- package/src/server/ws-router-misc.test.ts +422 -0
- package/src/server/ws-router-misc.ts +265 -0
- package/src/server/ws-router-orch.test.ts +190 -0
- package/src/server/ws-router-orch.ts +93 -0
- package/src/server/ws-router-project.test.ts +403 -0
- package/src/server/ws-router-project.ts +256 -0
- package/src/server/ws-router-push.test.ts +193 -0
- package/src/server/ws-router-push.ts +120 -0
- package/src/server/ws-router-settings.test.ts +173 -0
- package/src/server/ws-router-settings.ts +416 -0
- package/src/server/ws-router-skills.test.ts +194 -0
- package/src/server/ws-router-skills.ts +204 -0
- package/src/server/ws-router-utils.ts +224 -0
- package/src/server/ws-router.ts +339 -2254
- package/src/shared/app-settings-types.ts +365 -0
- package/src/shared/core-types.ts +27 -0
- package/src/shared/git-diff-types.ts +165 -0
- package/src/shared/mcp-types.ts +105 -0
- package/src/shared/provider-model-types.ts +437 -0
- package/src/shared/subagent-types.ts +198 -0
- package/src/shared/tool-call-types.ts +314 -0
- package/src/shared/transcript-types.ts +270 -0
- package/src/shared/types.ts +25 -1711
- package/dist/client/assets/channel-KQhRwDzJ.js +0 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-ChxT1GNq.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-ChxT1GNq.js +0 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-D4X-ZnLV.js +0 -1
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import {
|
|
3
|
+
runCommand,
|
|
4
|
+
runGit,
|
|
5
|
+
statOrNull,
|
|
6
|
+
summarizeGitFailure,
|
|
7
|
+
} from "./diff-store-io.adapter"
|
|
8
|
+
import type {
|
|
9
|
+
ChatBranchHistoryEntry,
|
|
10
|
+
ChatBranchHistorySnapshot,
|
|
11
|
+
GithubRelease,
|
|
12
|
+
} from "../shared/types"
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Types
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
export type SelectedBranch =
|
|
19
|
+
| { kind: "local"; name: string }
|
|
20
|
+
| { kind: "remote"; name: string; remoteRef: string }
|
|
21
|
+
| {
|
|
22
|
+
kind: "pull_request"
|
|
23
|
+
name: string
|
|
24
|
+
prNumber: number
|
|
25
|
+
headRefName: string
|
|
26
|
+
headRepoCloneUrl?: string
|
|
27
|
+
isCrossRepository?: boolean
|
|
28
|
+
remoteRef?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Repo / branch queries
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
export async function resolveRepo(
|
|
36
|
+
projectPath: string
|
|
37
|
+
): Promise<{ repoRoot: string; baseCommit: string | null } | null> {
|
|
38
|
+
const topLevel = await runGit(["rev-parse", "--show-toplevel"], projectPath)
|
|
39
|
+
if (topLevel.exitCode !== 0) {
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const repoRoot = topLevel.stdout.trim()
|
|
44
|
+
const head = await runGit(["rev-parse", "--verify", "HEAD"], repoRoot)
|
|
45
|
+
return {
|
|
46
|
+
repoRoot,
|
|
47
|
+
baseCommit: head.exitCode === 0 ? head.stdout.trim() : null,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function getBranchName(repoRoot: string) {
|
|
52
|
+
const symbolicRef = await runGit(["symbolic-ref", "--quiet", "--short", "HEAD"], repoRoot)
|
|
53
|
+
if (symbolicRef.exitCode === 0) {
|
|
54
|
+
return symbolicRef.stdout.trim()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const revParse = await runGit(["rev-parse", "--abbrev-ref", "HEAD"], repoRoot)
|
|
58
|
+
if (revParse.exitCode === 0) {
|
|
59
|
+
return revParse.stdout.trim()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return undefined
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function hasUpstreamBranch(repoRoot: string) {
|
|
66
|
+
const upstream = await runGit(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"], repoRoot)
|
|
67
|
+
return upstream.exitCode === 0 && upstream.stdout.trim().length > 0
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function getLastFetchedAt(repoRoot: string) {
|
|
71
|
+
const gitDirResult = await runGit(["rev-parse", "--git-dir"], repoRoot)
|
|
72
|
+
if (gitDirResult.exitCode !== 0) {
|
|
73
|
+
return undefined
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const gitDir = gitDirResult.stdout.trim()
|
|
77
|
+
const fetchHeadPath = path.resolve(repoRoot, gitDir, "FETCH_HEAD")
|
|
78
|
+
try {
|
|
79
|
+
const fetchHeadStat = (await statOrNull(fetchHeadPath))!
|
|
80
|
+
return fetchHeadStat.mtime.toISOString()
|
|
81
|
+
} catch {
|
|
82
|
+
return undefined
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function getUpstreamStatusCounts(repoRoot: string) {
|
|
87
|
+
const result = await runGit(["rev-list", "--left-right", "--count", "HEAD...@{upstream}"], repoRoot)
|
|
88
|
+
if (result.exitCode !== 0) {
|
|
89
|
+
return { aheadCount: undefined, behindCount: undefined }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const [aheadRaw, behindRaw] = result.stdout.trim().split(/\s+/u)
|
|
93
|
+
const aheadCount = Number.parseInt(aheadRaw ?? "", 10)
|
|
94
|
+
const behindCount = Number.parseInt(behindRaw ?? "", 10)
|
|
95
|
+
return {
|
|
96
|
+
aheadCount: Number.isFinite(aheadCount) ? aheadCount : undefined,
|
|
97
|
+
behindCount: Number.isFinite(behindCount) ? behindCount : undefined,
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function getOriginRemoteUrl(repoRoot: string) {
|
|
102
|
+
const result = await runGit(["remote", "get-url", "origin"], repoRoot)
|
|
103
|
+
if (result.exitCode !== 0) {
|
|
104
|
+
return null
|
|
105
|
+
}
|
|
106
|
+
const remoteUrl = result.stdout.trim()
|
|
107
|
+
return remoteUrl.length > 0 ? remoteUrl : null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function getGitHubRemoteSlugs(repoRoot: string) {
|
|
111
|
+
const remotesResult = await runGit(["remote"], repoRoot)
|
|
112
|
+
if (remotesResult.exitCode !== 0) {
|
|
113
|
+
return new Map<string, string>()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const remoteNames = remotesResult.stdout
|
|
117
|
+
.split(/\r?\n/u)
|
|
118
|
+
.map((line) => line.trim())
|
|
119
|
+
.filter(Boolean)
|
|
120
|
+
|
|
121
|
+
const remoteSlugEntries = await Promise.all(remoteNames.map(async (remoteName) => {
|
|
122
|
+
const remoteUrlResult = await runGit(["remote", "get-url", remoteName], repoRoot)
|
|
123
|
+
if (remoteUrlResult.exitCode !== 0) {
|
|
124
|
+
return null
|
|
125
|
+
}
|
|
126
|
+
const repoSlug = extractGitHubRepoSlug(remoteUrlResult.stdout.trim())
|
|
127
|
+
return repoSlug ? [remoteName, repoSlug.toLowerCase()] as const : null
|
|
128
|
+
}))
|
|
129
|
+
|
|
130
|
+
return new Map(remoteSlugEntries.filter((entry): entry is readonly [string, string] => Boolean(entry)))
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function getLocalBranchNames(repoRoot: string) {
|
|
134
|
+
const result = await runGit(["for-each-ref", "--format=%(refname:short)", "refs/heads"], repoRoot)
|
|
135
|
+
if (result.exitCode !== 0) {
|
|
136
|
+
throw new Error(result.stderr.trim() || "Failed to list local branches")
|
|
137
|
+
}
|
|
138
|
+
return result.stdout
|
|
139
|
+
.split(/\r?\n/u)
|
|
140
|
+
.map((line) => line.trim())
|
|
141
|
+
.filter(Boolean)
|
|
142
|
+
.sort((left, right) => left.localeCompare(right))
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function getRemoteBranchNames(repoRoot: string) {
|
|
146
|
+
const result = await runGit(["for-each-ref", "--format=%(refname:short)", "refs/remotes"], repoRoot)
|
|
147
|
+
if (result.exitCode !== 0) {
|
|
148
|
+
throw new Error(result.stderr.trim() || "Failed to list remote branches")
|
|
149
|
+
}
|
|
150
|
+
return result.stdout
|
|
151
|
+
.split(/\r?\n/u)
|
|
152
|
+
.map((line) => line.trim())
|
|
153
|
+
.filter((line) => line.length > 0 && !line.endsWith("/HEAD"))
|
|
154
|
+
.sort((left, right) => left.localeCompare(right))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function getBranchUpdatedAtMap(repoRoot: string, refPrefix: "refs/heads" | "refs/remotes") {
|
|
158
|
+
const result = await runGit(
|
|
159
|
+
["for-each-ref", "--format=%(refname:short)\t%(committerdate:iso-strict)", refPrefix],
|
|
160
|
+
repoRoot
|
|
161
|
+
)
|
|
162
|
+
if (result.exitCode !== 0) {
|
|
163
|
+
throw new Error(result.stderr.trim() || "Failed to read branch update times")
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const entries = new Map<string, string>()
|
|
167
|
+
for (const line of result.stdout.split(/\r?\n/u)) {
|
|
168
|
+
const trimmed = line.trim()
|
|
169
|
+
if (!trimmed) continue
|
|
170
|
+
const [name, updatedAt] = trimmed.split("\t")
|
|
171
|
+
if (!name || !updatedAt || (refPrefix === "refs/remotes" && name.endsWith("/HEAD"))) {
|
|
172
|
+
continue
|
|
173
|
+
}
|
|
174
|
+
entries.set(name, updatedAt)
|
|
175
|
+
}
|
|
176
|
+
return entries
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function resolveDefaultBranchName(repoRoot: string) {
|
|
180
|
+
const originHead = await runGit(["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"], repoRoot)
|
|
181
|
+
if (originHead.exitCode === 0) {
|
|
182
|
+
const ref = originHead.stdout.trim()
|
|
183
|
+
if (ref.startsWith("origin/")) {
|
|
184
|
+
return ref.slice("origin/".length)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const localBranches = await getLocalBranchNames(repoRoot)
|
|
189
|
+
if (localBranches.includes("main")) return "main"
|
|
190
|
+
if (localBranches.includes("master")) return "master"
|
|
191
|
+
return (await getBranchName(repoRoot)) ?? localBranches[0] ?? undefined
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function getRecentBranchNames(repoRoot: string) {
|
|
195
|
+
const result = await runGit(["reflog", "--format=%gs", "--max-count=100", "HEAD"], repoRoot)
|
|
196
|
+
if (result.exitCode !== 0) {
|
|
197
|
+
return []
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const recent: string[] = []
|
|
201
|
+
const seen = new Set<string>()
|
|
202
|
+
for (const line of result.stdout.split(/\r?\n/u)) {
|
|
203
|
+
const match = /checkout: moving from .* to (?<branch>.+)$/u.exec(line.trim())
|
|
204
|
+
const branch = match?.groups?.branch?.trim()
|
|
205
|
+
if (!branch || branch === "HEAD" || branch.startsWith("refs/")) {
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
if (seen.has(branch)) continue
|
|
209
|
+
seen.add(branch)
|
|
210
|
+
recent.push(branch)
|
|
211
|
+
}
|
|
212
|
+
return recent
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function resolveSelectedBranchRef(repoRoot: string, branch: SelectedBranch) {
|
|
216
|
+
if (branch.kind === "local") {
|
|
217
|
+
const localBranchNames = await getLocalBranchNames(repoRoot)
|
|
218
|
+
if (!localBranchNames.includes(branch.name)) {
|
|
219
|
+
throw new Error(`Local branch not found: ${branch.name}`)
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
ref: branch.name,
|
|
223
|
+
displayName: branch.name,
|
|
224
|
+
branchName: branch.name,
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (branch.kind === "remote") {
|
|
229
|
+
const remoteRef = branch.remoteRef.trim()
|
|
230
|
+
const remoteBranchNames = await getRemoteBranchNames(repoRoot)
|
|
231
|
+
if (!remoteBranchNames.includes(remoteRef)) {
|
|
232
|
+
throw new Error(`Remote branch not found: ${remoteRef}`)
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
ref: remoteRef,
|
|
236
|
+
displayName: remoteRef,
|
|
237
|
+
branchName: branch.name,
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const localBranchNames = await getLocalBranchNames(repoRoot)
|
|
242
|
+
if (localBranchNames.includes(branch.name)) {
|
|
243
|
+
return {
|
|
244
|
+
ref: branch.name,
|
|
245
|
+
displayName: `PR #${branch.prNumber}`,
|
|
246
|
+
branchName: branch.name,
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const remoteRef = branch.remoteRef?.trim()
|
|
251
|
+
if (remoteRef) {
|
|
252
|
+
const remoteBranchNames = await getRemoteBranchNames(repoRoot)
|
|
253
|
+
if (remoteBranchNames.includes(remoteRef)) {
|
|
254
|
+
return {
|
|
255
|
+
ref: remoteRef,
|
|
256
|
+
displayName: `PR #${branch.prNumber}`,
|
|
257
|
+
branchName: branch.headRefName || branch.name,
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (branch.isCrossRepository) {
|
|
263
|
+
throw new Error("This pull request branch is not available locally yet. Check it out first before merging.")
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
throw new Error(`Pull request branch not found: ${branch.headRefName || branch.name}`)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export async function getMergeCommitCount(repoRoot: string, sourceRef: string) {
|
|
270
|
+
const result = await runGit(["rev-list", "--count", `HEAD..${sourceRef}`], repoRoot)
|
|
271
|
+
if (result.exitCode !== 0) {
|
|
272
|
+
throw new Error(result.stderr.trim() || "Failed to calculate merge commit count")
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const commitCount = Number.parseInt(result.stdout.trim(), 10)
|
|
276
|
+
return Number.isFinite(commitCount) ? commitCount : 0
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export async function predictMergeConflicts(repoRoot: string, sourceRef: string) {
|
|
280
|
+
// Try the newer `git merge-tree --write-tree` form (requires Git 2.38+).
|
|
281
|
+
const newResult = await runGit(["merge-tree", "--write-tree", "--messages", "HEAD", sourceRef], repoRoot)
|
|
282
|
+
|
|
283
|
+
// Exit code 129 means the --write-tree flag is not supported (Git < 2.38).
|
|
284
|
+
// Fall back to the legacy three-argument form.
|
|
285
|
+
if (newResult.exitCode !== 129) {
|
|
286
|
+
const output = `${newResult.stdout}\n${newResult.stderr}`.trim()
|
|
287
|
+
|
|
288
|
+
if (newResult.exitCode === 0) {
|
|
289
|
+
return { hasConflicts: false }
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const normalizedOutput = output.toLowerCase()
|
|
293
|
+
if (newResult.exitCode === 1 || normalizedOutput.includes("conflict")) {
|
|
294
|
+
return {
|
|
295
|
+
hasConflicts: true,
|
|
296
|
+
detail: output || "Git reported merge conflicts for this branch pair.",
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
throw new Error(output || "Failed to analyze merge conflicts")
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Legacy fallback: `git merge-tree <base> HEAD <source>` (Git < 2.38).
|
|
304
|
+
const baseResult = await runGit(["merge-base", "HEAD", sourceRef], repoRoot)
|
|
305
|
+
if (baseResult.exitCode !== 0) {
|
|
306
|
+
throw new Error(baseResult.stderr.trim() || "Failed to find merge base")
|
|
307
|
+
}
|
|
308
|
+
const baseTree = baseResult.stdout.trim()
|
|
309
|
+
|
|
310
|
+
const legacyResult = await runGit(["merge-tree", baseTree, "HEAD", sourceRef], repoRoot)
|
|
311
|
+
const legacyOutput = `${legacyResult.stdout}\n${legacyResult.stderr}`.trim()
|
|
312
|
+
|
|
313
|
+
if (legacyResult.exitCode !== 0) {
|
|
314
|
+
throw new Error(legacyOutput || "Failed to analyze merge conflicts")
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// In the legacy form, conflict markers (<<<<<<) appear in the output when there are conflicts.
|
|
318
|
+
if (legacyOutput.includes("<<<<<<<") || legacyOutput.toLowerCase().includes("conflict")) {
|
|
319
|
+
return {
|
|
320
|
+
hasConflicts: true,
|
|
321
|
+
detail: legacyOutput || "Git reported merge conflicts for this branch pair.",
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return { hasConflicts: false }
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ---------------------------------------------------------------------------
|
|
329
|
+
// GitHub URL parsing (pure)
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
|
|
332
|
+
export function extractGitHubRepoSlug(remoteUrl: string | null | undefined) {
|
|
333
|
+
if (!remoteUrl) return null
|
|
334
|
+
|
|
335
|
+
const sshMatch = /^git@github\.com:(?<owner>[^/]+)\/(?<repo>[^/]+?)(?:\.git)?$/u.exec(remoteUrl)
|
|
336
|
+
if (sshMatch?.groups?.owner && sshMatch.groups.repo) {
|
|
337
|
+
return `${sshMatch.groups.owner}/${sshMatch.groups.repo}`
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const sshProtocolMatch = /^ssh:\/\/git@github\.com\/(?<owner>[^/]+)\/(?<repo>[^/]+?)(?:\.git)?$/u.exec(remoteUrl)
|
|
341
|
+
if (sshProtocolMatch?.groups?.owner && sshProtocolMatch.groups.repo) {
|
|
342
|
+
return `${sshProtocolMatch.groups.owner}/${sshProtocolMatch.groups.repo}`
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const httpsMatch = /^https?:\/\/github\.com\/(?<owner>[^/]+)\/(?<repo>[^/]+?)(?:\.git)?$/u.exec(remoteUrl)
|
|
346
|
+
if (httpsMatch?.groups?.owner && httpsMatch.groups.repo) {
|
|
347
|
+
return `${httpsMatch.groups.owner}/${httpsMatch.groups.repo}`
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return null
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
// GitHub API — pull requests
|
|
355
|
+
// ---------------------------------------------------------------------------
|
|
356
|
+
|
|
357
|
+
export interface GitHubPullRequestResponseItem {
|
|
358
|
+
number: number
|
|
359
|
+
title: string
|
|
360
|
+
head?: {
|
|
361
|
+
ref?: string
|
|
362
|
+
label?: string
|
|
363
|
+
repo?: {
|
|
364
|
+
clone_url?: string
|
|
365
|
+
full_name?: string
|
|
366
|
+
} | null
|
|
367
|
+
}
|
|
368
|
+
base?: {
|
|
369
|
+
ref?: string
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
type FetchLike = (input: string, init?: RequestInit) => Promise<Response>
|
|
374
|
+
|
|
375
|
+
type GitHubCliApiLike = (path: string) => Promise<GitHubPullRequestResponseItem[] | null>
|
|
376
|
+
|
|
377
|
+
interface FetchGitHubPullRequestsDeps {
|
|
378
|
+
fetchImpl?: FetchLike
|
|
379
|
+
ghApiImpl?: GitHubCliApiLike
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function fetchGitHubPullRequestsViaGh(path: string): Promise<GitHubPullRequestResponseItem[] | null> {
|
|
383
|
+
const result = await runCommand([
|
|
384
|
+
"gh",
|
|
385
|
+
"api",
|
|
386
|
+
"-H",
|
|
387
|
+
"Accept: application/vnd.github+json",
|
|
388
|
+
path,
|
|
389
|
+
])
|
|
390
|
+
if (result.exitCode !== 0) {
|
|
391
|
+
return null
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const json: GitHubPullRequestResponseItem[] = JSON.parse(result.stdout)
|
|
395
|
+
return Array.isArray(json) ? json : []
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export async function fetchGitHubPullRequests(
|
|
399
|
+
repoSlug: string,
|
|
400
|
+
deps: FetchLike | FetchGitHubPullRequestsDeps = fetch
|
|
401
|
+
): Promise<GitHubPullRequestResponseItem[]> {
|
|
402
|
+
const fetchImpl = typeof deps === "function" ? deps : (deps.fetchImpl ?? fetch)
|
|
403
|
+
const ghApiImpl = typeof deps === "function" ? fetchGitHubPullRequestsViaGh : (deps.ghApiImpl ?? fetchGitHubPullRequestsViaGh)
|
|
404
|
+
const ghPath = `repos/${repoSlug}/pulls?state=open&per_page=50`
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
const ghPulls = await ghApiImpl(ghPath)
|
|
408
|
+
if (ghPulls) {
|
|
409
|
+
return ghPulls
|
|
410
|
+
}
|
|
411
|
+
} catch {
|
|
412
|
+
// Fall back to an unauthenticated HTTP request when `gh` is unavailable.
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const response = await fetchImpl(`https://api.github.com/repos/${repoSlug}/pulls?state=open&per_page=50`, {
|
|
416
|
+
headers: {
|
|
417
|
+
Accept: "application/vnd.github+json",
|
|
418
|
+
},
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
if (!response.ok) {
|
|
422
|
+
throw new Error(`GitHub pull requests request failed with status ${response.status}`)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const json: GitHubPullRequestResponseItem[] = await response.json()
|
|
426
|
+
return Array.isArray(json) ? json : []
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ---------------------------------------------------------------------------
|
|
430
|
+
// GitHub API — releases
|
|
431
|
+
// ---------------------------------------------------------------------------
|
|
432
|
+
|
|
433
|
+
type GitHubReleasesCliApiLike = (path: string) => Promise<GithubRelease[] | null>
|
|
434
|
+
|
|
435
|
+
interface FetchGitHubReleasesDeps {
|
|
436
|
+
fetchImpl?: FetchLike
|
|
437
|
+
ghApiImpl?: GitHubReleasesCliApiLike
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async function fetchGitHubReleasesViaGh(path: string): Promise<GithubRelease[] | null> {
|
|
441
|
+
const result = await runCommand([
|
|
442
|
+
"gh",
|
|
443
|
+
"api",
|
|
444
|
+
"-H",
|
|
445
|
+
"Accept: application/vnd.github+json",
|
|
446
|
+
path,
|
|
447
|
+
])
|
|
448
|
+
if (result.exitCode !== 0) {
|
|
449
|
+
return null
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const json: GithubRelease[] = JSON.parse(result.stdout)
|
|
453
|
+
return Array.isArray(json) ? json : []
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Fetches published releases for the changelog panel. Prefers the authenticated
|
|
457
|
+
// `gh` CLI (5000 req/hr) and falls back to an unauthenticated HTTP request
|
|
458
|
+
// (60 req/hr per IP) so the browser never hits GitHub's low anonymous limit.
|
|
459
|
+
export async function fetchGitHubReleases(
|
|
460
|
+
repoSlug: string,
|
|
461
|
+
deps: FetchLike | FetchGitHubReleasesDeps = fetch
|
|
462
|
+
): Promise<GithubRelease[]> {
|
|
463
|
+
const fetchImpl = typeof deps === "function" ? deps : (deps.fetchImpl ?? fetch)
|
|
464
|
+
const ghApiImpl = typeof deps === "function" ? fetchGitHubReleasesViaGh : (deps.ghApiImpl ?? fetchGitHubReleasesViaGh)
|
|
465
|
+
const ghPath = `repos/${repoSlug}/releases`
|
|
466
|
+
|
|
467
|
+
let releases: GithubRelease[] | null = null
|
|
468
|
+
try {
|
|
469
|
+
releases = await ghApiImpl(ghPath)
|
|
470
|
+
} catch {
|
|
471
|
+
// Fall back to an unauthenticated HTTP request when `gh` is unavailable.
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (!releases) {
|
|
475
|
+
const response = await fetchImpl(`https://api.github.com/${ghPath}`, {
|
|
476
|
+
headers: {
|
|
477
|
+
Accept: "application/vnd.github+json",
|
|
478
|
+
},
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
if (!response.ok) {
|
|
482
|
+
throw new Error(`GitHub releases request failed with status ${response.status}`)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
releases = await response.json()
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return Array.isArray(releases) ? releases.filter((release) => !release.draft) : []
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// ---------------------------------------------------------------------------
|
|
492
|
+
// Commit history
|
|
493
|
+
// ---------------------------------------------------------------------------
|
|
494
|
+
|
|
495
|
+
function buildGitHubCommitUrl(remoteUrl: string | null, sha: string) {
|
|
496
|
+
const slug = extractGitHubRepoSlug(remoteUrl)
|
|
497
|
+
return slug ? `https://github.com/${slug}/commit/${sha}` : undefined
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async function getTagsByCommit(repoRoot: string, shas: string[]): Promise<Map<string, string[]>> {
|
|
501
|
+
const tagMap = new Map<string, string[]>()
|
|
502
|
+
if (shas.length === 0) return tagMap
|
|
503
|
+
|
|
504
|
+
for (const sha of shas) {
|
|
505
|
+
tagMap.set(sha, [])
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const result = await runGit(
|
|
509
|
+
["log", "--max-count", String(shas.length), "--decorate-refs=refs/tags", "--format=%H %D", shas[0]!],
|
|
510
|
+
repoRoot
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
if (result.exitCode !== 0) return tagMap
|
|
514
|
+
|
|
515
|
+
for (const line of result.stdout.split(/\r?\n/u)) {
|
|
516
|
+
const trimmed = line.trim()
|
|
517
|
+
if (!trimmed) continue
|
|
518
|
+
const spaceIndex = trimmed.indexOf(" ")
|
|
519
|
+
if (spaceIndex < 0) continue
|
|
520
|
+
const sha = trimmed.slice(0, spaceIndex)
|
|
521
|
+
const decorations = trimmed.slice(spaceIndex + 1)
|
|
522
|
+
if (!tagMap.has(sha) || !decorations) continue
|
|
523
|
+
const tags = decorations
|
|
524
|
+
.split(",")
|
|
525
|
+
.map((decoration) => decoration.trim())
|
|
526
|
+
.filter((decoration) => decoration.startsWith("tag: "))
|
|
527
|
+
.map((decoration) => decoration.slice(5))
|
|
528
|
+
.filter(Boolean)
|
|
529
|
+
.sort((left, right) => left.localeCompare(right))
|
|
530
|
+
tagMap.set(sha, tags)
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return tagMap
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export async function getBranchHistory(args: {
|
|
537
|
+
repoRoot: string
|
|
538
|
+
ref: string
|
|
539
|
+
limit: number
|
|
540
|
+
}): Promise<ChatBranchHistorySnapshot> {
|
|
541
|
+
const logResult = await runGit(
|
|
542
|
+
[
|
|
543
|
+
"log",
|
|
544
|
+
"--max-count",
|
|
545
|
+
String(args.limit),
|
|
546
|
+
"--pretty=format:%H%x1f%s%x1f%b%x1f%an%x1f%aI%x1e",
|
|
547
|
+
args.ref,
|
|
548
|
+
],
|
|
549
|
+
args.repoRoot
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
if (logResult.exitCode !== 0) {
|
|
553
|
+
throw new Error(logResult.stderr.trim() || "Failed to read git log")
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const remoteUrl = await getOriginRemoteUrl(args.repoRoot)
|
|
557
|
+
const parsedRecords: Array<{ sha: string; summary: string; description: string; authorName?: string; authoredAt: string }> = []
|
|
558
|
+
|
|
559
|
+
for (const record of logResult.stdout.split("")) {
|
|
560
|
+
const trimmed = record.trim()
|
|
561
|
+
if (!trimmed) continue
|
|
562
|
+
const [sha, summary, description, authorName, authoredAt] = trimmed.split("")
|
|
563
|
+
if (!sha || !summary || !authoredAt) continue
|
|
564
|
+
parsedRecords.push({
|
|
565
|
+
sha,
|
|
566
|
+
summary,
|
|
567
|
+
description: (description ?? "").trim(),
|
|
568
|
+
authorName: authorName?.trim() || undefined,
|
|
569
|
+
authoredAt,
|
|
570
|
+
})
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const tagMap = await getTagsByCommit(args.repoRoot, parsedRecords.map((record) => record.sha))
|
|
574
|
+
|
|
575
|
+
const entries: ChatBranchHistoryEntry[] = parsedRecords.map((record) => ({
|
|
576
|
+
...record,
|
|
577
|
+
tags: tagMap.get(record.sha) ?? [],
|
|
578
|
+
githubUrl: buildGitHubCommitUrl(remoteUrl, record.sha),
|
|
579
|
+
}))
|
|
580
|
+
|
|
581
|
+
return { entries }
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// ---------------------------------------------------------------------------
|
|
585
|
+
// Branch action failure factories
|
|
586
|
+
// ---------------------------------------------------------------------------
|
|
587
|
+
|
|
588
|
+
export function createBranchActionFailure(title: string, detail: string, fallback: string) {
|
|
589
|
+
return {
|
|
590
|
+
ok: false,
|
|
591
|
+
title,
|
|
592
|
+
message: summarizeGitFailure(detail, fallback),
|
|
593
|
+
detail,
|
|
594
|
+
} as const
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
export function createMergeActionFailure(args: {
|
|
598
|
+
title: string
|
|
599
|
+
detail: string
|
|
600
|
+
fallback: string
|
|
601
|
+
snapshotChanged: boolean
|
|
602
|
+
}) {
|
|
603
|
+
return {
|
|
604
|
+
ok: false,
|
|
605
|
+
title: args.title,
|
|
606
|
+
message: summarizeGitFailure(args.detail, args.fallback),
|
|
607
|
+
detail: args.detail,
|
|
608
|
+
snapshotChanged: args.snapshotChanged,
|
|
609
|
+
} as const
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// ---------------------------------------------------------------------------
|
|
613
|
+
// GitHub auth / publish helpers
|
|
614
|
+
// ---------------------------------------------------------------------------
|
|
615
|
+
|
|
616
|
+
export function sanitizeRepoName(name: string) {
|
|
617
|
+
return name
|
|
618
|
+
.trim()
|
|
619
|
+
.toLowerCase()
|
|
620
|
+
.replace(/[\s_]+/gu, "-")
|
|
621
|
+
.replace(/[^a-z0-9.-]+/gu, "-")
|
|
622
|
+
.replace(/-+/gu, "-")
|
|
623
|
+
.replace(/^-|-$/gu, "")
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
export async function getGhAuthInfo() {
|
|
627
|
+
const versionResult = await runCommand(["gh", "--version"])
|
|
628
|
+
if (versionResult.exitCode !== 0) {
|
|
629
|
+
return {
|
|
630
|
+
ghInstalled: false,
|
|
631
|
+
authenticated: false,
|
|
632
|
+
activeAccountLogin: undefined,
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const authStatusResult = await runCommand(["gh", "auth", "status", "--json", "hosts"])
|
|
637
|
+
if (authStatusResult.exitCode !== 0) {
|
|
638
|
+
return {
|
|
639
|
+
ghInstalled: true,
|
|
640
|
+
authenticated: false,
|
|
641
|
+
activeAccountLogin: undefined,
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
try {
|
|
646
|
+
const parsed: { hosts?: Record<string, Array<{ active?: boolean; login?: string; state?: string }>> } = JSON.parse(authStatusResult.stdout)
|
|
647
|
+
const accounts = parsed.hosts?.["github.com"] ?? []
|
|
648
|
+
const activeAccount = accounts.find((account) => account.active) ?? accounts[0]
|
|
649
|
+
return {
|
|
650
|
+
ghInstalled: true,
|
|
651
|
+
authenticated: activeAccount?.state === "success",
|
|
652
|
+
activeAccountLogin: activeAccount?.login,
|
|
653
|
+
}
|
|
654
|
+
} catch {
|
|
655
|
+
return {
|
|
656
|
+
ghInstalled: true,
|
|
657
|
+
authenticated: false,
|
|
658
|
+
activeAccountLogin: undefined,
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
export async function getGitHubOwners(): Promise<string[]> {
|
|
664
|
+
const userResult = await runCommand(["gh", "api", "user", "--jq", ".login"])
|
|
665
|
+
if (userResult.exitCode !== 0) {
|
|
666
|
+
return []
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const owners = new Set<string>()
|
|
670
|
+
const userLogin = userResult.stdout.trim()
|
|
671
|
+
if (userLogin) {
|
|
672
|
+
owners.add(userLogin)
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const orgsResult = await runCommand(["gh", "api", "user/orgs", "--paginate", "--jq", ".[].login"])
|
|
676
|
+
if (orgsResult.exitCode === 0) {
|
|
677
|
+
for (const line of orgsResult.stdout.split(/\r?\n/u)) {
|
|
678
|
+
const login = line.trim()
|
|
679
|
+
if (login) {
|
|
680
|
+
owners.add(login)
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
return [...owners]
|
|
686
|
+
}
|