kanna-code 0.26.3 → 0.26.5
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/index-BaEB4jie.js +2583 -0
- package/dist/client/index.html +1 -1
- package/package.json +1 -1
- package/src/server/diff-store.test.ts +68 -1
- package/src/server/diff-store.ts +39 -9
- package/src/server/server.ts +2 -2
- package/src/server/uploads.test.ts +2 -2
- package/src/server/ws-router.ts +1 -1
- package/src/shared/types.ts +1 -0
- package/dist/client/assets/index-QXLrswKX.js +0 -2583
package/dist/client/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
7
7
|
<title>Kanna</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-BaEB4jie.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-C5RBxQW4.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test"
|
|
2
|
-
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"
|
|
3
3
|
import { tmpdir } from "node:os"
|
|
4
4
|
import path from "node:path"
|
|
5
5
|
import { appendGitIgnoreEntry, DiffStore, extractGitHubRepoSlug, fetchGitHubPullRequests } from "./diff-store"
|
|
@@ -148,6 +148,52 @@ describe("DiffStore", () => {
|
|
|
148
148
|
expect((await run(["git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], repoRoot)).trim()).toBe("origin/feature/publish-me")
|
|
149
149
|
})
|
|
150
150
|
|
|
151
|
+
test("commit_and_push degrades to a local commit when origin is missing", async () => {
|
|
152
|
+
const repoRoot = await createRepo()
|
|
153
|
+
tempDirs.push(repoRoot)
|
|
154
|
+
await writeFile(path.join(repoRoot, "app.txt"), "base\n", "utf8")
|
|
155
|
+
await run(["git", "add", "."], repoRoot)
|
|
156
|
+
await run(["git", "commit", "-m", "init"], repoRoot)
|
|
157
|
+
await writeFile(path.join(repoRoot, "app.txt"), "changed\n", "utf8")
|
|
158
|
+
|
|
159
|
+
const store = new DiffStore(repoRoot)
|
|
160
|
+
await store.initialize()
|
|
161
|
+
await store.refreshSnapshot("project-1", repoRoot)
|
|
162
|
+
|
|
163
|
+
const result = await store.commitFiles({
|
|
164
|
+
projectId: "project-1",
|
|
165
|
+
projectPath: repoRoot,
|
|
166
|
+
paths: ["app.txt"],
|
|
167
|
+
summary: "Local only",
|
|
168
|
+
mode: "commit_and_push",
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
expect(result).toMatchObject({
|
|
172
|
+
ok: true,
|
|
173
|
+
mode: "commit_and_push",
|
|
174
|
+
pushed: false,
|
|
175
|
+
})
|
|
176
|
+
expect((await run(["git", "log", "-1", "--pretty=%s"], repoRoot)).trim()).toBe("Local only")
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test("refreshSnapshot reports origin presence before the first commit", async () => {
|
|
180
|
+
const repoRoot = await createRepo()
|
|
181
|
+
tempDirs.push(repoRoot)
|
|
182
|
+
await run(["git", "remote", "add", "origin", "https://github.com/jakemor/test224.git"], repoRoot)
|
|
183
|
+
await writeFile(path.join(repoRoot, "poem.md"), "rose\n", "utf8")
|
|
184
|
+
|
|
185
|
+
const store = new DiffStore(repoRoot)
|
|
186
|
+
await store.initialize()
|
|
187
|
+
await store.refreshSnapshot("project-1", repoRoot)
|
|
188
|
+
|
|
189
|
+
expect(store.getProjectSnapshot("project-1")).toMatchObject({
|
|
190
|
+
status: "ready",
|
|
191
|
+
branchName: "main",
|
|
192
|
+
hasOriginRemote: true,
|
|
193
|
+
originRepoSlug: "jakemor/test224",
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
|
|
151
197
|
test("detects renamed files", async () => {
|
|
152
198
|
const repoRoot = await createRepo()
|
|
153
199
|
tempDirs.push(repoRoot)
|
|
@@ -273,6 +319,27 @@ describe("DiffStore", () => {
|
|
|
273
319
|
expect(await readFile(path.join(repoRoot, ".gitignore"), "utf8")).toBe("scratch.log\n")
|
|
274
320
|
})
|
|
275
321
|
|
|
322
|
+
test("ignoreFile accepts a folder entry for an untracked diff", async () => {
|
|
323
|
+
const repoRoot = await createRepo()
|
|
324
|
+
tempDirs.push(repoRoot)
|
|
325
|
+
await writeFile(path.join(repoRoot, "tracked.txt"), "base\n", "utf8")
|
|
326
|
+
await run(["git", "add", "."], repoRoot)
|
|
327
|
+
await run(["git", "commit", "-m", "init"], repoRoot)
|
|
328
|
+
await mkdir(path.join(repoRoot, "tmp/cache"), { recursive: true })
|
|
329
|
+
await writeFile(path.join(repoRoot, "tmp/cache/output.log"), "tmp\n", "utf8")
|
|
330
|
+
|
|
331
|
+
const store = new DiffStore(repoRoot)
|
|
332
|
+
await store.initialize()
|
|
333
|
+
await store.refreshSnapshot("project-1", repoRoot)
|
|
334
|
+
await store.ignoreFile({
|
|
335
|
+
projectId: "project-1",
|
|
336
|
+
projectPath: repoRoot,
|
|
337
|
+
path: "tmp/cache/",
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
expect(await readFile(path.join(repoRoot, ".gitignore"), "utf8")).toBe("tmp/cache/\n")
|
|
341
|
+
})
|
|
342
|
+
|
|
276
343
|
test("appendGitIgnoreEntry does not duplicate an existing identical entry", () => {
|
|
277
344
|
expect(appendGitIgnoreEntry("scratch.log\n", "scratch.log")).toBe("scratch.log\n")
|
|
278
345
|
expect(appendGitIgnoreEntry("scratch.log", "scratch.log")).toBe("scratch.log\n")
|
package/src/server/diff-store.ts
CHANGED
|
@@ -37,6 +37,7 @@ function createEmptyState(): StoredChatDiffState {
|
|
|
37
37
|
status: "unknown",
|
|
38
38
|
branchName: undefined,
|
|
39
39
|
defaultBranchName: undefined,
|
|
40
|
+
hasOriginRemote: undefined,
|
|
40
41
|
originRepoSlug: undefined,
|
|
41
42
|
hasUpstream: undefined,
|
|
42
43
|
aheadCount: undefined,
|
|
@@ -50,6 +51,7 @@ function createEmptyState(): StoredChatDiffState {
|
|
|
50
51
|
function branchMetadataEqual(left: BranchMetadata, right: BranchMetadata) {
|
|
51
52
|
return left.branchName === right.branchName
|
|
52
53
|
&& left.defaultBranchName === right.defaultBranchName
|
|
54
|
+
&& left.hasOriginRemote === right.hasOriginRemote
|
|
53
55
|
&& left.originRepoSlug === right.originRepoSlug
|
|
54
56
|
&& left.hasUpstream === right.hasUpstream
|
|
55
57
|
}
|
|
@@ -196,6 +198,9 @@ function createPushFailure(mode: DiffCommitMode, detail: string, snapshotChanged
|
|
|
196
198
|
if (normalized.includes("non-fast-forward") || normalized.includes("fetch first")) {
|
|
197
199
|
title = "Branch is not up to date"
|
|
198
200
|
message = "Your branch is behind its remote. Pull or rebase, then try pushing again."
|
|
201
|
+
} else if (normalized.includes("does not appear to be a git repository")) {
|
|
202
|
+
title = "No origin remote configured"
|
|
203
|
+
message = "This repository does not have an origin remote configured."
|
|
199
204
|
} else if (normalized.includes("has no upstream branch") || normalized.includes("set-upstream")) {
|
|
200
205
|
title = "No upstream branch configured"
|
|
201
206
|
message = "This branch does not have an upstream remote branch configured yet."
|
|
@@ -1317,6 +1322,7 @@ export class DiffStore {
|
|
|
1317
1322
|
status: state.status,
|
|
1318
1323
|
branchName: state.branchName,
|
|
1319
1324
|
defaultBranchName: state.defaultBranchName,
|
|
1325
|
+
hasOriginRemote: state.hasOriginRemote,
|
|
1320
1326
|
originRepoSlug: state.originRepoSlug,
|
|
1321
1327
|
hasUpstream: state.hasUpstream,
|
|
1322
1328
|
aheadCount: state.aheadCount,
|
|
@@ -1339,6 +1345,7 @@ export class DiffStore {
|
|
|
1339
1345
|
status: "no_repo",
|
|
1340
1346
|
branchName: undefined,
|
|
1341
1347
|
defaultBranchName: undefined,
|
|
1348
|
+
hasOriginRemote: undefined,
|
|
1342
1349
|
originRepoSlug: undefined,
|
|
1343
1350
|
hasUpstream: undefined,
|
|
1344
1351
|
aheadCount: undefined,
|
|
@@ -1356,6 +1363,7 @@ export class DiffStore {
|
|
|
1356
1363
|
const branchName = await getBranchName(repo.repoRoot)
|
|
1357
1364
|
const defaultBranchName = await resolveDefaultBranchName(repo.repoRoot)
|
|
1358
1365
|
const originRemoteUrl = await getOriginRemoteUrl(repo.repoRoot)
|
|
1366
|
+
const hasOriginRemote = originRemoteUrl !== null
|
|
1359
1367
|
const originRepoSlug = extractGitHubRepoSlug(originRemoteUrl) ?? undefined
|
|
1360
1368
|
const hasUpstream = await hasUpstreamBranch(repo.repoRoot)
|
|
1361
1369
|
const { aheadCount, behindCount } = hasUpstream
|
|
@@ -1373,6 +1381,7 @@ export class DiffStore {
|
|
|
1373
1381
|
status: "ready",
|
|
1374
1382
|
branchName,
|
|
1375
1383
|
defaultBranchName,
|
|
1384
|
+
hasOriginRemote,
|
|
1376
1385
|
originRepoSlug,
|
|
1377
1386
|
hasUpstream,
|
|
1378
1387
|
aheadCount,
|
|
@@ -1808,7 +1817,11 @@ export class DiffStore {
|
|
|
1808
1817
|
throw new Error("Project is not in a git repository")
|
|
1809
1818
|
}
|
|
1810
1819
|
|
|
1811
|
-
const hasUpstream = await
|
|
1820
|
+
const [hasUpstream, originRemoteUrl] = await Promise.all([
|
|
1821
|
+
hasUpstreamBranch(repo.repoRoot),
|
|
1822
|
+
getOriginRemoteUrl(repo.repoRoot),
|
|
1823
|
+
])
|
|
1824
|
+
const hasOriginRemote = originRemoteUrl !== null
|
|
1812
1825
|
if (args.action === "publish") {
|
|
1813
1826
|
const publishResult = await runGit(["push", "-u", "origin", "HEAD"], repo.repoRoot)
|
|
1814
1827
|
if (publishResult.exitCode !== 0) {
|
|
@@ -2002,7 +2015,11 @@ export class DiffStore {
|
|
|
2002
2015
|
if (!repo) {
|
|
2003
2016
|
throw new Error("Project is not in a git repository")
|
|
2004
2017
|
}
|
|
2005
|
-
const hasUpstream = await
|
|
2018
|
+
const [hasUpstream, originRemoteUrl] = await Promise.all([
|
|
2019
|
+
hasUpstreamBranch(repo.repoRoot),
|
|
2020
|
+
getOriginRemoteUrl(repo.repoRoot),
|
|
2021
|
+
])
|
|
2022
|
+
const hasOriginRemote = originRemoteUrl !== null
|
|
2006
2023
|
|
|
2007
2024
|
const currentDirtyPaths = new Set((await listDirtyPaths(repo.repoRoot)).map((entry) => entry.path))
|
|
2008
2025
|
const missingPaths = normalizedPaths.filter((relativePath) => !currentDirtyPaths.has(relativePath))
|
|
@@ -2039,6 +2056,16 @@ export class DiffStore {
|
|
|
2039
2056
|
} satisfies DiffCommitResult
|
|
2040
2057
|
}
|
|
2041
2058
|
|
|
2059
|
+
if (!hasUpstream && !hasOriginRemote) {
|
|
2060
|
+
return {
|
|
2061
|
+
ok: true,
|
|
2062
|
+
mode: args.mode,
|
|
2063
|
+
branchName,
|
|
2064
|
+
pushed: false,
|
|
2065
|
+
snapshotChanged,
|
|
2066
|
+
} satisfies DiffCommitResult
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2042
2069
|
const pushResult = hasUpstream
|
|
2043
2070
|
? await runGit(["push"], repo.repoRoot)
|
|
2044
2071
|
: await runGit(["push", "-u", "origin", "HEAD"], repo.repoRoot)
|
|
@@ -2103,23 +2130,26 @@ export class DiffStore {
|
|
|
2103
2130
|
projectPath: string
|
|
2104
2131
|
path: string
|
|
2105
2132
|
}) {
|
|
2106
|
-
const
|
|
2133
|
+
const ignoreEntry = normalizeRepoRelativePath(args.path)
|
|
2107
2134
|
const repo = await resolveRepo(args.projectPath)
|
|
2108
2135
|
if (!repo) {
|
|
2109
2136
|
throw new Error("Project is not in a git repository")
|
|
2110
2137
|
}
|
|
2111
2138
|
|
|
2112
|
-
const
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
}
|
|
2116
|
-
if (!entry.isUntracked) {
|
|
2139
|
+
const dirtyPaths = await listDirtyPaths(repo.repoRoot)
|
|
2140
|
+
const exactEntry = dirtyPaths.find((candidate) => candidate.path === ignoreEntry)
|
|
2141
|
+
if (exactEntry && !exactEntry.isUntracked) {
|
|
2117
2142
|
throw new Error("Only untracked files can be ignored from the diff viewer")
|
|
2118
2143
|
}
|
|
2119
2144
|
|
|
2145
|
+
const entry = dirtyPaths.find((candidate) => candidate.isUntracked && (candidate.path === ignoreEntry || candidate.path.startsWith(ignoreEntry)))
|
|
2146
|
+
if (!entry) {
|
|
2147
|
+
throw new Error(`File is no longer changed: ${ignoreEntry}`)
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2120
2150
|
const gitignorePath = path.join(repo.repoRoot, ".gitignore")
|
|
2121
2151
|
const currentContents = await readFile(gitignorePath, "utf8").catch(() => null)
|
|
2122
|
-
const nextContents = appendGitIgnoreEntry(currentContents,
|
|
2152
|
+
const nextContents = appendGitIgnoreEntry(currentContents, ignoreEntry)
|
|
2123
2153
|
if (nextContents !== currentContents) {
|
|
2124
2154
|
await writeFile(gitignorePath, nextContents, "utf8")
|
|
2125
2155
|
}
|
package/src/server/server.ts
CHANGED
|
@@ -15,8 +15,8 @@ import { createWsRouter, type ClientState } from "./ws-router"
|
|
|
15
15
|
import { deleteProjectUpload, inferAttachmentContentType, inferProjectFileContentType, persistProjectUpload } from "./uploads"
|
|
16
16
|
import { getProjectUploadDir } from "./paths"
|
|
17
17
|
|
|
18
|
-
const MAX_UPLOAD_FILES =
|
|
19
|
-
const MAX_UPLOAD_SIZE_BYTES =
|
|
18
|
+
const MAX_UPLOAD_FILES = 50
|
|
19
|
+
const MAX_UPLOAD_SIZE_BYTES = 100 * 1024 * 1024
|
|
20
20
|
const STALE_EMPTY_CHAT_PRUNE_INTERVAL_MS = 60 * 1000
|
|
21
21
|
|
|
22
22
|
export async function persistUploadedFiles(args: {
|
|
@@ -182,7 +182,7 @@ describe("uploads", () => {
|
|
|
182
182
|
try {
|
|
183
183
|
const project = await server.store.openProject(projectDir, "Project")
|
|
184
184
|
const formData = new FormData()
|
|
185
|
-
formData.append("files", new File([new Uint8Array(
|
|
185
|
+
formData.append("files", new File([new Uint8Array(100 * 1024 * 1024 + 1)], "big.bin", { type: "application/octet-stream" }))
|
|
186
186
|
|
|
187
187
|
const response = await fetch(`http://localhost:${server.port}/api/projects/${project.id}/uploads`, {
|
|
188
188
|
method: "POST",
|
|
@@ -191,7 +191,7 @@ describe("uploads", () => {
|
|
|
191
191
|
|
|
192
192
|
expect(response.status).toBe(413)
|
|
193
193
|
expect(await response.json()).toEqual({
|
|
194
|
-
error: "File \"big.bin\" exceeds the
|
|
194
|
+
error: "File \"big.bin\" exceeds the 100 MB limit.",
|
|
195
195
|
})
|
|
196
196
|
} finally {
|
|
197
197
|
await server.stop()
|
package/src/server/ws-router.ts
CHANGED
|
@@ -57,7 +57,7 @@ export function createWsRouter({
|
|
|
57
57
|
}: CreateWsRouterArgs) {
|
|
58
58
|
const sockets = new Set<ServerWebSocket<ClientState>>()
|
|
59
59
|
const resolvedDiffStore = diffStore ?? {
|
|
60
|
-
getProjectSnapshot: () => ({ status: "unknown", branchName: undefined, defaultBranchName: undefined, originRepoSlug: undefined, hasUpstream: undefined, aheadCount: undefined, behindCount: undefined, lastFetchedAt: undefined, files: [] as const, branchHistory: { entries: [] as const } }),
|
|
60
|
+
getProjectSnapshot: () => ({ status: "unknown", branchName: undefined, defaultBranchName: undefined, hasOriginRemote: undefined, originRepoSlug: undefined, hasUpstream: undefined, aheadCount: undefined, behindCount: undefined, lastFetchedAt: undefined, files: [] as const, branchHistory: { entries: [] as const } }),
|
|
61
61
|
refreshSnapshot: async () => false,
|
|
62
62
|
initializeGit: async () => ({ ok: true, branchName: undefined, snapshotChanged: false }),
|
|
63
63
|
getGitHubPublishInfo: async () => ({ ghInstalled: false, authenticated: false, activeAccountLogin: undefined, owners: [], suggestedRepoName: "my-repo" }),
|
package/src/shared/types.ts
CHANGED