@cuongtran001/kanna 1.2.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-BLk1CZKB.js → arc-DXtN3EWJ.js} +1 -1
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-B01ZQYte.js → architectureDiagram-3BPJPVTR-CIGqW1z-.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-diL6a3cq.js → blockDiagram-GPEHLZMM-BafNS_H6.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-BY1verL8.js → c4Diagram-AAUBKEIU-YdAgRSis.js} +1 -1
- package/dist/client/assets/channel-BEYTrUAm.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-psdZuajw.js → chunk-2J33WTMH-eiP5y5G-.js} +1 -1
- package/dist/client/assets/{chunk-4BX2VUAB-CrwQVBc5.js → chunk-4BX2VUAB-CvLGzPMF.js} +1 -1
- package/dist/client/assets/{chunk-55IACEB6-9-aj4jKO.js → chunk-55IACEB6-Y-S9Fb8A.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-1FNK9_nB.js → chunk-727SXJPM-6sJH1RI9.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-DFhvd_Ek.js → chunk-AQP2D5EJ-hV1r_IXO.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-B7MLrDa7.js → chunk-FMBD7UC4-CC3L28S5.js} +1 -1
- package/dist/client/assets/{chunk-ND2GUHAM-D4Df05QE.js → chunk-ND2GUHAM-BZ3czhdH.js} +1 -1
- package/dist/client/assets/{chunk-QZHKN3VN-Iq17Dznm.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-BmaZhtXy.js → cose-bilkent-S5V4N54A-CfxdDghE.js} +1 -1
- package/dist/client/assets/{dagre-BM42HDAG-BCmpfnfP.js → dagre-BM42HDAG-B8CddsbB.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-BIEcKowS.js → diagram-2AECGRRQ-F0yS0fVB.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-CRBABBfv.js → diagram-5GNKFQAL-cW0i0qlh.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-BRxUk16N.js → diagram-KO2AKTUF-BzAKMeJT.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-DPBs4xl1.js → diagram-LMA3HP47-qdC4XqIl.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-BE9rgVv8.js → diagram-OG6HWLK6-CGcpi6Dh.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-D3vgA1xJ.js → erDiagram-TEJ5UH35-CxNTDicM.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-Cgu-8XXC.js → flowDiagram-I6XJVG4X-qkGa5TqY.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-CgjXVyTR.js → ganttDiagram-6RSMTGT7-D9mVPoQi.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-zD5D8B-i.js → gitGraphDiagram-PVQCEYII-DA0grAXe.js} +1 -1
- package/dist/client/assets/{index-DBTdhMbJ.js → index-BqUY1Bm4.js} +1 -1
- package/dist/client/assets/index-CLsOWXff.css +32 -0
- package/dist/client/assets/{index-zQLLeAfl.js → index-TU1k6ov6.js} +253 -243
- package/dist/client/assets/{infoDiagram-5YYISTIA-BaFHqT4u.js → infoDiagram-5YYISTIA-CGljtLs3.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-2yAC1br2.js → ishikawaDiagram-YF4QCWOH-CtC0xrmG.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-DacQ-ND5.js → journeyDiagram-JHISSGLW-URLnVZeN.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-Qlbwm3z4.js → kanban-definition-UN3LZRKU-C2Q6aqQh.js} +1 -1
- package/dist/client/assets/{linear-DeafqpgO.js → linear-ByKHtsIx.js} +1 -1
- package/dist/client/assets/{mermaid.core-KYiCPJDj.js → mermaid.core-Dl_f2KLc.js} +4 -4
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-B1V-T6d3.js → mindmap-definition-RKZ34NQL-CaJ9IDLI.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-DOF9OFRu.js → pieDiagram-4H26LBE5-mmYaf2Oa.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-lH1mB7ko.js → quadrantDiagram-W4KKPZXB-B_PO6OCC.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-BaTsJ_sS.js → requirementDiagram-4Y6WPE33-M4huwJt1.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-Pfxib5iI.js → sankeyDiagram-5OEKKPKP-B0hb1sWh.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-BQMEKkBC.js → sequenceDiagram-3UESZ5HK-BAqSptnA.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-CuldNnzp.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-DP7u0YAg.js → timeline-definition-PNZ67QCA-MgAjHz8Q.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN-QlrjpNeQ.js → vennDiagram-CIIHVFJN-DmKoO51W.js} +1 -1
- package/dist/client/assets/{wardley-L42UT6IY-BqdJ_sAM.js → wardley-L42UT6IY-CWcyHnYs.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-Wi3kaQ5X.js → wardleyDiagram-YWT4CUSO-BGIGmFP9.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-D-KXMyCG.js → xychartDiagram-2RQKCTM6-D20YIvEl.js} +1 -1
- package/dist/client/index.html +2 -2
- 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.oauth-rotation.test.ts +101 -0
- package/src/server/agent.test.ts +24 -8
- package/src/server/agent.ts +35 -4963
- package/src/server/auto-continue/read-model.test.ts +2 -2
- package/src/server/auto-continue/read-model.ts +4 -1
- 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 -2284
- package/src/server/events.ts +3 -0
- package/src/server/harness-types.ts +15 -1
- package/src/server/oauth-errors.ts +19 -0
- package/src/server/read-models.ts +26 -1
- package/src/server/subagent-orchestrator.ts +3 -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/loop-progress.test.ts +106 -0
- package/src/shared/loop-progress.ts +88 -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 +27 -1668
- package/dist/client/assets/channel-BMkhjiLI.js +0 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-C5Kg1XOD.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-C5Kg1XOD.js +0 -1
- package/dist/client/assets/index-H6C-f2Vp.css +0 -32
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Ca2U11af.js +0 -1
|
@@ -70,3 +70,30 @@ export async function spawnCommandCapture(args: string[]): Promise<SpawnResult>
|
|
|
70
70
|
export function getBunEnv(): Record<string, string | undefined> {
|
|
71
71
|
return Bun.env
|
|
72
72
|
}
|
|
73
|
+
|
|
74
|
+
export const NON_INTERACTIVE_GIT_ENV = {
|
|
75
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
76
|
+
GIT_ASKPASS: "echo",
|
|
77
|
+
SSH_ASKPASS: "echo",
|
|
78
|
+
GCM_INTERACTIVE: "Never",
|
|
79
|
+
} as const
|
|
80
|
+
|
|
81
|
+
export function runGit(args: string[], cwd: string) {
|
|
82
|
+
return spawnGitCapture(args, cwd, { ...getBunEnv(), ...NON_INTERACTIVE_GIT_ENV })
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function runCommand(args: string[]) {
|
|
86
|
+
return spawnCommandCapture(args)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function formatGitFailure(result: SpawnResult) {
|
|
90
|
+
return [result.stderr.trim(), result.stdout.trim()].filter(Boolean).join("\n")
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function summarizeGitFailure(detail: string, fallback: string) {
|
|
94
|
+
return detail
|
|
95
|
+
.split(/\r?\n/u)
|
|
96
|
+
.map((line) => line.trim())
|
|
97
|
+
.find((line) => line.length > 0)
|
|
98
|
+
?? fallback
|
|
99
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import {
|
|
3
|
+
appendGitIgnoreEntry,
|
|
4
|
+
countTextLines,
|
|
5
|
+
getContentDigest,
|
|
6
|
+
normalizeRepoRelativePath,
|
|
7
|
+
parseNumstatValue,
|
|
8
|
+
parseStatusPaths,
|
|
9
|
+
} from "./diff-store-parse"
|
|
10
|
+
|
|
11
|
+
describe("parseStatusPaths", () => {
|
|
12
|
+
test("parses modified file", () => {
|
|
13
|
+
const result = parseStatusPaths(" M src/foo.ts\n")
|
|
14
|
+
expect(result).toHaveLength(1)
|
|
15
|
+
expect(result[0]).toMatchObject({ path: "src/foo.ts", changeType: "modified", isUntracked: false })
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test("parses untracked file", () => {
|
|
19
|
+
const result = parseStatusPaths("?? new-file.ts\n")
|
|
20
|
+
expect(result).toHaveLength(1)
|
|
21
|
+
expect(result[0]).toMatchObject({ path: "new-file.ts", changeType: "added", isUntracked: true })
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test("parses added file", () => {
|
|
25
|
+
const result = parseStatusPaths("A staged.ts\n")
|
|
26
|
+
expect(result).toHaveLength(1)
|
|
27
|
+
expect(result[0]).toMatchObject({ path: "staged.ts", changeType: "added", isUntracked: false })
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test("parses deleted file", () => {
|
|
31
|
+
const result = parseStatusPaths(" D gone.ts\n")
|
|
32
|
+
expect(result).toHaveLength(1)
|
|
33
|
+
expect(result[0]).toMatchObject({ path: "gone.ts", changeType: "deleted", isUntracked: false })
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test("parses renamed file", () => {
|
|
37
|
+
const result = parseStatusPaths("R old.ts -> new.ts\n")
|
|
38
|
+
expect(result).toHaveLength(1)
|
|
39
|
+
expect(result[0]).toMatchObject({
|
|
40
|
+
path: "new.ts",
|
|
41
|
+
previousPath: "old.ts",
|
|
42
|
+
changeType: "renamed",
|
|
43
|
+
isUntracked: false,
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test("sorts results by path", () => {
|
|
48
|
+
const output = "?? b.ts\n?? a.ts\n?? c.ts\n"
|
|
49
|
+
const result = parseStatusPaths(output)
|
|
50
|
+
expect(result.map((e) => e.path)).toEqual(["a.ts", "b.ts", "c.ts"])
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test("ignores lines shorter than 4 chars", () => {
|
|
54
|
+
const result = parseStatusPaths("AB\n M valid.ts\n")
|
|
55
|
+
expect(result).toHaveLength(1)
|
|
56
|
+
expect(result[0]?.path).toBe("valid.ts")
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test("skips lines with empty value after status code", () => {
|
|
60
|
+
const result = parseStatusPaths(" M \n M real.ts\n")
|
|
61
|
+
expect(result).toHaveLength(1)
|
|
62
|
+
expect(result[0]?.path).toBe("real.ts")
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test("returns empty array for empty input", () => {
|
|
66
|
+
expect(parseStatusPaths("")).toEqual([])
|
|
67
|
+
expect(parseStatusPaths("\n\n")).toEqual([])
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe("getContentDigest", () => {
|
|
72
|
+
test("returns a hex string", () => {
|
|
73
|
+
const digest = getContentDigest({
|
|
74
|
+
changeType: "modified",
|
|
75
|
+
beforePath: "a.ts",
|
|
76
|
+
afterPath: "a.ts",
|
|
77
|
+
beforeText: "old",
|
|
78
|
+
afterText: "new",
|
|
79
|
+
})
|
|
80
|
+
expect(digest).toMatch(/^[\da-f]{40}$/)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test("same inputs produce same digest", () => {
|
|
84
|
+
const args = {
|
|
85
|
+
changeType: "modified" as const,
|
|
86
|
+
beforePath: "a.ts",
|
|
87
|
+
afterPath: "a.ts",
|
|
88
|
+
beforeText: "old",
|
|
89
|
+
afterText: "new",
|
|
90
|
+
}
|
|
91
|
+
expect(getContentDigest(args)).toBe(getContentDigest(args))
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test("different inputs produce different digests", () => {
|
|
95
|
+
const base = {
|
|
96
|
+
changeType: "modified" as const,
|
|
97
|
+
beforePath: "a.ts",
|
|
98
|
+
afterPath: "a.ts",
|
|
99
|
+
beforeText: "old",
|
|
100
|
+
afterText: "new",
|
|
101
|
+
}
|
|
102
|
+
const changed = { ...base, afterText: "different" }
|
|
103
|
+
expect(getContentDigest(base)).not.toBe(getContentDigest(changed))
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test("handles null text", () => {
|
|
107
|
+
const digest = getContentDigest({
|
|
108
|
+
changeType: "added",
|
|
109
|
+
beforePath: "a.ts",
|
|
110
|
+
afterPath: "a.ts",
|
|
111
|
+
beforeText: null,
|
|
112
|
+
afterText: "content",
|
|
113
|
+
})
|
|
114
|
+
expect(typeof digest).toBe("string")
|
|
115
|
+
expect(digest).toHaveLength(40)
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
describe("parseNumstatValue", () => {
|
|
120
|
+
test("parses integer", () => {
|
|
121
|
+
expect(parseNumstatValue("42")).toBe(42)
|
|
122
|
+
expect(parseNumstatValue("0")).toBe(0)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test("returns 0 for binary marker", () => {
|
|
126
|
+
expect(parseNumstatValue("-")).toBe(0)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test("returns 0 for empty string", () => {
|
|
130
|
+
expect(parseNumstatValue("")).toBe(0)
|
|
131
|
+
expect(parseNumstatValue(" ")).toBe(0)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test("returns 0 for non-numeric string", () => {
|
|
135
|
+
expect(parseNumstatValue("abc")).toBe(0)
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
describe("countTextLines", () => {
|
|
140
|
+
test("counts lines", () => {
|
|
141
|
+
expect(countTextLines("a\nb\nc\n")).toBe(3)
|
|
142
|
+
expect(countTextLines("a\nb")).toBe(2)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test("returns 0 for null", () => {
|
|
146
|
+
expect(countTextLines(null)).toBe(0)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
test("returns 0 for empty string", () => {
|
|
150
|
+
expect(countTextLines("")).toBe(0)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test("handles single line with trailing newline", () => {
|
|
154
|
+
expect(countTextLines("hello\n")).toBe(1)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
test("handles single line without trailing newline", () => {
|
|
158
|
+
expect(countTextLines("hello")).toBe(1)
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
describe("normalizeRepoRelativePath", () => {
|
|
163
|
+
test("passes through simple paths", () => {
|
|
164
|
+
expect(normalizeRepoRelativePath("src/foo.ts")).toBe("src/foo.ts")
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
test("strips leading ./", () => {
|
|
168
|
+
expect(normalizeRepoRelativePath("./src/foo.ts")).toBe("src/foo.ts")
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test("rejects absolute paths", () => {
|
|
172
|
+
expect(() => normalizeRepoRelativePath("/etc/passwd")).toThrow("Invalid diff path")
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test("rejects parent traversal", () => {
|
|
176
|
+
expect(() => normalizeRepoRelativePath("../outside")).toThrow("Invalid diff path")
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test("rejects embedded traversal", () => {
|
|
180
|
+
expect(() => normalizeRepoRelativePath("src/../../../etc/passwd")).toThrow("Invalid diff path")
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
test("normalizes backslashes on Windows-style paths", () => {
|
|
184
|
+
expect(normalizeRepoRelativePath("src\\foo.ts")).toBe("src/foo.ts")
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
describe("appendGitIgnoreEntry", () => {
|
|
189
|
+
test("adds entry to empty file", () => {
|
|
190
|
+
expect(appendGitIgnoreEntry(null, "dist/")).toBe("dist/\n")
|
|
191
|
+
expect(appendGitIgnoreEntry("", "dist/")).toBe("dist/\n")
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
test("appends entry to existing content", () => {
|
|
195
|
+
expect(appendGitIgnoreEntry("node_modules/\n", "dist/")).toBe("node_modules/\ndist/\n")
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
test("does not duplicate existing entry", () => {
|
|
199
|
+
const result = appendGitIgnoreEntry("dist/\n", "dist/")
|
|
200
|
+
expect(result).toBe("dist/\n")
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
test("adds newline to file missing trailing newline before appending", () => {
|
|
204
|
+
const result = appendGitIgnoreEntry("node_modules/", "dist/")
|
|
205
|
+
expect(result).toBe("node_modules/\ndist/\n")
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
test("does not duplicate entry that exists without trailing newline", () => {
|
|
209
|
+
const result = appendGitIgnoreEntry("dist/", "dist/")
|
|
210
|
+
expect(result).toBe("dist/\n")
|
|
211
|
+
})
|
|
212
|
+
})
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { createHash } from "node:crypto"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import type { ChatDiffFile } from "../shared/types"
|
|
4
|
+
|
|
5
|
+
export interface DirtyPathEntry {
|
|
6
|
+
path: string
|
|
7
|
+
previousPath?: string
|
|
8
|
+
changeType: ChatDiffFile["changeType"]
|
|
9
|
+
isUntracked: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function parseStatusPaths(output: string): DirtyPathEntry[] {
|
|
13
|
+
const entries: DirtyPathEntry[] = []
|
|
14
|
+
for (const rawLine of output.split(/\r?\n/u)) {
|
|
15
|
+
const line = rawLine.trimEnd()
|
|
16
|
+
if (line.length < 4) continue
|
|
17
|
+
const statusCode = line.slice(0, 2)
|
|
18
|
+
const value = line.slice(3)
|
|
19
|
+
if (!value) continue
|
|
20
|
+
const isUntracked = statusCode === "??"
|
|
21
|
+
const isRename = statusCode.includes("R")
|
|
22
|
+
const isDelete = statusCode.includes("D")
|
|
23
|
+
const isAdd = statusCode.includes("A") || isUntracked
|
|
24
|
+
let changeType: ChatDiffFile["changeType"]
|
|
25
|
+
if (isRename) {
|
|
26
|
+
changeType = "renamed"
|
|
27
|
+
} else if (isDelete) {
|
|
28
|
+
changeType = "deleted"
|
|
29
|
+
} else if (isAdd) {
|
|
30
|
+
changeType = "added"
|
|
31
|
+
} else {
|
|
32
|
+
changeType = "modified"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (isRename && value.includes(" -> ")) {
|
|
36
|
+
const [previousPath, nextPath] = value.split(" -> ")
|
|
37
|
+
if (nextPath) {
|
|
38
|
+
entries.push({
|
|
39
|
+
path: nextPath,
|
|
40
|
+
previousPath: previousPath || undefined,
|
|
41
|
+
changeType,
|
|
42
|
+
isUntracked,
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
entries.push({
|
|
49
|
+
path: value,
|
|
50
|
+
changeType,
|
|
51
|
+
isUntracked,
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
return entries.sort((left, right) => left.path.localeCompare(right.path))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getContentDigest(args: {
|
|
58
|
+
changeType: ChatDiffFile["changeType"]
|
|
59
|
+
beforePath: string
|
|
60
|
+
afterPath: string
|
|
61
|
+
beforeText: string | null
|
|
62
|
+
afterText: string | null
|
|
63
|
+
}): string {
|
|
64
|
+
return createHash("sha1")
|
|
65
|
+
.update(args.changeType)
|
|
66
|
+
.update("\u0000")
|
|
67
|
+
.update(args.beforePath)
|
|
68
|
+
.update("\u0000")
|
|
69
|
+
.update(args.afterPath)
|
|
70
|
+
.update("\u0000")
|
|
71
|
+
.update(args.beforeText ?? "")
|
|
72
|
+
.update("\u0000")
|
|
73
|
+
.update(args.afterText ?? "")
|
|
74
|
+
.digest("hex")
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function parseNumstatValue(value: string): number {
|
|
78
|
+
if (value === "-" || value.trim() === "") return 0
|
|
79
|
+
const parsed = Number.parseInt(value, 10)
|
|
80
|
+
return Number.isFinite(parsed) ? parsed : 0
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function countTextLines(text: string | null): number {
|
|
84
|
+
if (!text) return 0
|
|
85
|
+
const lines = text.split(/\r?\n/u)
|
|
86
|
+
if (lines.at(-1) === "") {
|
|
87
|
+
lines.pop()
|
|
88
|
+
}
|
|
89
|
+
return lines.length
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function normalizeRepoRelativePath(inputPath: string): string {
|
|
93
|
+
const normalized = path.posix.normalize(inputPath.replaceAll("\\", "/")).replace(/^\.\/+/u, "")
|
|
94
|
+
if (!normalized || normalized === "." || normalized.startsWith("../") || normalized.includes("/../") || path.posix.isAbsolute(normalized)) {
|
|
95
|
+
throw new Error(`Invalid diff path: ${inputPath}`)
|
|
96
|
+
}
|
|
97
|
+
return normalized
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function appendGitIgnoreEntry(currentContents: string | null, entry: string): string {
|
|
101
|
+
const normalizedContents = currentContents ?? ""
|
|
102
|
+
const existingEntries = normalizedContents
|
|
103
|
+
.split(/\r?\n/u)
|
|
104
|
+
.map((line) => line.trim())
|
|
105
|
+
.filter(Boolean)
|
|
106
|
+
|
|
107
|
+
if (existingEntries.includes(entry)) {
|
|
108
|
+
return normalizedContents.length > 0 && !normalizedContents.endsWith("\n")
|
|
109
|
+
? `${normalizedContents}\n`
|
|
110
|
+
: normalizedContents
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let prefix: string
|
|
114
|
+
if (normalizedContents.length === 0) {
|
|
115
|
+
prefix = ""
|
|
116
|
+
} else if (normalizedContents.endsWith("\n")) {
|
|
117
|
+
prefix = normalizedContents
|
|
118
|
+
} else {
|
|
119
|
+
prefix = `${normalizedContents}\n`
|
|
120
|
+
}
|
|
121
|
+
return `${prefix}${entry}\n`
|
|
122
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BranchMetadata,
|
|
3
|
+
ChatBranchHistorySnapshot,
|
|
4
|
+
ChatDiffFile,
|
|
5
|
+
ChatDiffSnapshot,
|
|
6
|
+
UpstreamStatus,
|
|
7
|
+
} from "../shared/types"
|
|
8
|
+
|
|
9
|
+
export interface StoredChatDiffState extends BranchMetadata, UpstreamStatus {
|
|
10
|
+
status: ChatDiffSnapshot["status"]
|
|
11
|
+
files: ChatDiffFile[]
|
|
12
|
+
branchHistory: ChatBranchHistorySnapshot
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createEmptyState(): StoredChatDiffState {
|
|
16
|
+
return {
|
|
17
|
+
status: "unknown",
|
|
18
|
+
branchName: undefined,
|
|
19
|
+
defaultBranchName: undefined,
|
|
20
|
+
hasOriginRemote: undefined,
|
|
21
|
+
originRepoSlug: undefined,
|
|
22
|
+
hasUpstream: undefined,
|
|
23
|
+
aheadCount: undefined,
|
|
24
|
+
behindCount: undefined,
|
|
25
|
+
lastFetchedAt: undefined,
|
|
26
|
+
files: [],
|
|
27
|
+
branchHistory: { entries: [] },
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function branchMetadataEqual(left: BranchMetadata, right: BranchMetadata): boolean {
|
|
32
|
+
return left.branchName === right.branchName
|
|
33
|
+
&& left.defaultBranchName === right.defaultBranchName
|
|
34
|
+
&& left.hasOriginRemote === right.hasOriginRemote
|
|
35
|
+
&& left.originRepoSlug === right.originRepoSlug
|
|
36
|
+
&& left.hasUpstream === right.hasUpstream
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function upstreamStatusEqual(left: UpstreamStatus, right: UpstreamStatus): boolean {
|
|
40
|
+
return left.aheadCount === right.aheadCount
|
|
41
|
+
&& left.behindCount === right.behindCount
|
|
42
|
+
&& left.lastFetchedAt === right.lastFetchedAt
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function branchHistoryEqual(left: ChatBranchHistorySnapshot, right: ChatBranchHistorySnapshot): boolean {
|
|
46
|
+
if (left.entries.length !== right.entries.length) return false
|
|
47
|
+
return left.entries.every((entry, index) => {
|
|
48
|
+
const other = right.entries[index]
|
|
49
|
+
return Boolean(other)
|
|
50
|
+
&& entry.sha === other.sha
|
|
51
|
+
&& entry.summary === other.summary
|
|
52
|
+
&& entry.description === other.description
|
|
53
|
+
&& entry.authorName === other.authorName
|
|
54
|
+
&& entry.authoredAt === other.authoredAt
|
|
55
|
+
&& entry.githubUrl === other.githubUrl
|
|
56
|
+
&& entry.tags.length === other.tags.length
|
|
57
|
+
&& entry.tags.every((tag, tagIndex) => tag === other.tags[tagIndex])
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function snapshotsEqual(left: StoredChatDiffState | undefined, right: StoredChatDiffState): boolean {
|
|
62
|
+
if (!left) {
|
|
63
|
+
return right.status === "unknown" && right.files.length === 0
|
|
64
|
+
}
|
|
65
|
+
if (left.status !== right.status) return false
|
|
66
|
+
if (!branchMetadataEqual(left, right)) return false
|
|
67
|
+
if (!upstreamStatusEqual(left, right)) return false
|
|
68
|
+
if (left.files.length !== right.files.length) return false
|
|
69
|
+
if (!branchHistoryEqual(left.branchHistory, right.branchHistory)) return false
|
|
70
|
+
return left.files.every((file, index) => {
|
|
71
|
+
const other = right.files[index]
|
|
72
|
+
return Boolean(other)
|
|
73
|
+
&& file.path === other.path
|
|
74
|
+
&& file.changeType === other.changeType
|
|
75
|
+
&& file.isUntracked === other.isUntracked
|
|
76
|
+
&& file.additions === other.additions
|
|
77
|
+
&& file.deletions === other.deletions
|
|
78
|
+
&& file.patchDigest === other.patchDigest
|
|
79
|
+
&& file.mimeType === other.mimeType
|
|
80
|
+
&& file.size === other.size
|
|
81
|
+
})
|
|
82
|
+
}
|