@mandipadk7/kavi 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/git.js ADDED
@@ -0,0 +1,289 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { ensureDir } from "./fs.js";
4
+ import { buildSessionId } from "./paths.js";
5
+ import { runCommand } from "./process.js";
6
+ export async function detectRepoRoot(cwd) {
7
+ const result = await runCommand("git", [
8
+ "rev-parse",
9
+ "--show-toplevel"
10
+ ], {
11
+ cwd
12
+ });
13
+ if (result.code !== 0) {
14
+ throw new Error(result.stderr.trim() || "Not inside a git repository.");
15
+ }
16
+ return result.stdout.trim();
17
+ }
18
+ export async function getHeadCommit(repoRoot) {
19
+ const result = await runCommand("git", [
20
+ "rev-parse",
21
+ "HEAD"
22
+ ], {
23
+ cwd: repoRoot
24
+ });
25
+ if (result.code !== 0) {
26
+ throw new Error("Unable to resolve HEAD. Create an initial commit before opening a Kavi session.");
27
+ }
28
+ return result.stdout.trim();
29
+ }
30
+ export async function getCurrentBranch(repoRoot) {
31
+ const result = await runCommand("git", [
32
+ "branch",
33
+ "--show-current"
34
+ ], {
35
+ cwd: repoRoot
36
+ });
37
+ if (result.code !== 0) {
38
+ throw new Error(result.stderr.trim() || "Unable to resolve current branch.");
39
+ }
40
+ return result.stdout.trim();
41
+ }
42
+ export async function getBranchCommit(repoRoot, ref) {
43
+ const result = await runCommand("git", [
44
+ "rev-parse",
45
+ ref
46
+ ], {
47
+ cwd: repoRoot
48
+ });
49
+ if (result.code !== 0) {
50
+ throw new Error(result.stderr.trim() || `Unable to resolve ${ref}.`);
51
+ }
52
+ return result.stdout.trim();
53
+ }
54
+ export async function resolveTargetBranch(repoRoot, configuredBranch) {
55
+ const exists = await runCommand("git", [
56
+ "show-ref",
57
+ "--verify",
58
+ `refs/heads/${configuredBranch}`
59
+ ], {
60
+ cwd: repoRoot
61
+ });
62
+ if (exists.code === 0) {
63
+ return configuredBranch;
64
+ }
65
+ return getCurrentBranch(repoRoot);
66
+ }
67
+ async function ensureDetachedWorktree(repoRoot, worktreePath, baseCommit) {
68
+ const result = await runCommand("git", [
69
+ "worktree",
70
+ "add",
71
+ "--detach",
72
+ worktreePath,
73
+ baseCommit
74
+ ], {
75
+ cwd: repoRoot
76
+ });
77
+ if (result.code !== 0 && !result.stderr.includes("already exists")) {
78
+ throw new Error(result.stderr.trim() || `Unable to create worktree ${worktreePath}.`);
79
+ }
80
+ }
81
+ async function ensureBranch(worktreePath, branchName, baseCommit) {
82
+ const exists = await runCommand("git", [
83
+ "rev-parse",
84
+ "--verify",
85
+ branchName
86
+ ], {
87
+ cwd: worktreePath
88
+ });
89
+ if (exists.code !== 0) {
90
+ const createResult = await runCommand("git", [
91
+ "checkout",
92
+ "-b",
93
+ branchName,
94
+ baseCommit
95
+ ], {
96
+ cwd: worktreePath
97
+ });
98
+ if (createResult.code !== 0) {
99
+ throw new Error(createResult.stderr.trim() || `Unable to create branch ${branchName}.`);
100
+ }
101
+ return;
102
+ }
103
+ const checkoutResult = await runCommand("git", [
104
+ "checkout",
105
+ branchName
106
+ ], {
107
+ cwd: worktreePath
108
+ });
109
+ if (checkoutResult.code !== 0) {
110
+ throw new Error(checkoutResult.stderr.trim() || `Unable to checkout ${branchName}.`);
111
+ }
112
+ }
113
+ export async function ensureWorktrees(repoRoot, paths, sessionId, _config, baseCommit) {
114
+ await ensureDir(paths.worktreeRoot);
115
+ const agents = [
116
+ "codex",
117
+ "claude"
118
+ ];
119
+ const worktrees = [];
120
+ for (const agent of agents){
121
+ const worktreePath = path.join(paths.worktreeRoot, `${agent}-${sessionId}`);
122
+ const branchName = `kavi/${sessionId}/${agent}`;
123
+ await ensureDetachedWorktree(repoRoot, worktreePath, baseCommit);
124
+ await ensureBranch(worktreePath, branchName, baseCommit);
125
+ worktrees.push({
126
+ agent,
127
+ path: worktreePath,
128
+ branch: branchName
129
+ });
130
+ }
131
+ return worktrees;
132
+ }
133
+ export async function createGitignoreEntries(repoRoot) {
134
+ const gitignorePath = path.join(repoRoot, ".gitignore");
135
+ let content = "";
136
+ try {
137
+ content = await fs.readFile(gitignorePath, "utf8");
138
+ } catch {
139
+ content = "";
140
+ }
141
+ const entries = [
142
+ ".kavi/state",
143
+ ".kavi/runtime"
144
+ ];
145
+ const missing = entries.filter((entry)=>!content.split(/\r?\n/).includes(entry));
146
+ if (missing.length === 0) {
147
+ return;
148
+ }
149
+ const prefix = content.trimEnd() ? "\n" : "";
150
+ await fs.writeFile(gitignorePath, `${content.trimEnd()}${prefix}${missing.join("\n")}\n`, "utf8");
151
+ }
152
+ export async function landBranches(repoRoot, targetBranch, worktrees, validationCommand, sessionId, integrationRoot) {
153
+ const commandsRun = [];
154
+ const snapshotCommits = [];
155
+ const targetHead = await getBranchCommit(repoRoot, targetBranch);
156
+ const integrationId = buildSessionId().slice(0, 8);
157
+ const integrationBranch = `kavi/land/${sessionId}/${integrationId}`;
158
+ const integrationPath = path.join(integrationRoot, `${sessionId}-${integrationId}`);
159
+ await ensureDir(integrationRoot);
160
+ const addIntegration = await runCommand("git", [
161
+ "worktree",
162
+ "add",
163
+ "-b",
164
+ integrationBranch,
165
+ integrationPath,
166
+ targetHead
167
+ ], {
168
+ cwd: repoRoot
169
+ });
170
+ commandsRun.push(`git worktree add -b ${integrationBranch} ${integrationPath} ${targetHead}`);
171
+ if (addIntegration.code !== 0) {
172
+ throw new Error(addIntegration.stderr.trim() || `Unable to create integration worktree at ${integrationPath}.`);
173
+ }
174
+ for (const worktree of worktrees){
175
+ const snapshot = await snapshotWorktree(worktree, sessionId);
176
+ snapshotCommits.push(snapshot);
177
+ if (snapshot.createdCommit) {
178
+ commandsRun.push(`git -C ${worktree.path} add -A`);
179
+ commandsRun.push(`git -C ${worktree.path} -c user.name=Kavi -c user.email=kavi@local.invalid commit -m "kavi: snapshot ${worktree.agent} ${sessionId}"`);
180
+ }
181
+ const merge = await runCommand("git", [
182
+ "merge",
183
+ "--no-ff",
184
+ "--no-edit",
185
+ worktree.branch
186
+ ], {
187
+ cwd: integrationPath
188
+ });
189
+ commandsRun.push(`git -C ${integrationPath} merge --no-ff --no-edit ${worktree.branch}`);
190
+ if (merge.code !== 0) {
191
+ throw new Error(merge.stderr.trim() || `Unable to merge branch ${worktree.branch} into integration branch ${integrationBranch}.`);
192
+ }
193
+ }
194
+ if (validationCommand.trim()) {
195
+ const validation = await runCommand("zsh", [
196
+ "-lc",
197
+ validationCommand
198
+ ], {
199
+ cwd: integrationPath
200
+ });
201
+ commandsRun.push(validationCommand);
202
+ if (validation.code !== 0) {
203
+ throw new Error(`Validation command failed.\n${validation.stdout}\n${validation.stderr}`.trim());
204
+ }
205
+ }
206
+ const currentBranch = await getCurrentBranch(repoRoot).catch(()=>"");
207
+ if (currentBranch === targetBranch) {
208
+ const mergeIntegration = await runCommand("git", [
209
+ "merge",
210
+ "--ff-only",
211
+ integrationBranch
212
+ ], {
213
+ cwd: repoRoot
214
+ });
215
+ commandsRun.push(`git merge --ff-only ${integrationBranch}`);
216
+ if (mergeIntegration.code !== 0) {
217
+ throw new Error(mergeIntegration.stderr.trim() || `Unable to fast-forward ${targetBranch} to ${integrationBranch}.`);
218
+ }
219
+ } else {
220
+ const updateRef = await runCommand("git", [
221
+ "update-ref",
222
+ `refs/heads/${targetBranch}`,
223
+ `refs/heads/${integrationBranch}`,
224
+ targetHead
225
+ ], {
226
+ cwd: repoRoot
227
+ });
228
+ commandsRun.push(`git update-ref refs/heads/${targetBranch} refs/heads/${integrationBranch} ${targetHead}`);
229
+ if (updateRef.code !== 0) {
230
+ throw new Error(updateRef.stderr.trim() || `Unable to advance ${targetBranch}; it changed while landing was in progress.`);
231
+ }
232
+ }
233
+ return {
234
+ commandsRun,
235
+ integrationBranch,
236
+ integrationPath,
237
+ snapshotCommits
238
+ };
239
+ }
240
+ async function snapshotWorktree(worktree, sessionId) {
241
+ const status = await runCommand("git", [
242
+ "status",
243
+ "--short"
244
+ ], {
245
+ cwd: worktree.path
246
+ });
247
+ if (status.code !== 0) {
248
+ throw new Error(status.stderr.trim() || `Unable to inspect worktree ${worktree.path}.`);
249
+ }
250
+ if (!status.stdout.trim()) {
251
+ return {
252
+ agent: worktree.agent,
253
+ createdCommit: false,
254
+ commit: await getBranchCommit(worktree.path, "HEAD")
255
+ };
256
+ }
257
+ const add = await runCommand("git", [
258
+ "add",
259
+ "-A"
260
+ ], {
261
+ cwd: worktree.path
262
+ });
263
+ if (add.code !== 0) {
264
+ throw new Error(add.stderr.trim() || `Unable to stage worktree ${worktree.path}.`);
265
+ }
266
+ const commitMessage = `kavi: snapshot ${worktree.agent} ${sessionId}`;
267
+ const commit = await runCommand("git", [
268
+ "-c",
269
+ "user.name=Kavi",
270
+ "-c",
271
+ "user.email=kavi@local.invalid",
272
+ "commit",
273
+ "-m",
274
+ commitMessage
275
+ ], {
276
+ cwd: worktree.path
277
+ });
278
+ if (commit.code !== 0) {
279
+ throw new Error(commit.stderr.trim() || `Unable to snapshot worktree ${worktree.path}.`);
280
+ }
281
+ return {
282
+ agent: worktree.agent,
283
+ createdCommit: true,
284
+ commit: await getBranchCommit(worktree.path, "HEAD")
285
+ };
286
+ }
287
+
288
+
289
+ //# sourceURL=git.ts
@@ -0,0 +1,39 @@
1
+ import path from "node:path";
2
+ import { ensureDir } from "./fs.js";
3
+ export class EventHistory {
4
+ db;
5
+ constructor(db){
6
+ this.db = db;
7
+ this.db.exec(`
8
+ CREATE TABLE IF NOT EXISTS events (
9
+ id TEXT PRIMARY KEY,
10
+ session_id TEXT NOT NULL,
11
+ timestamp TEXT NOT NULL,
12
+ type TEXT NOT NULL,
13
+ payload_json TEXT NOT NULL
14
+ );
15
+ `);
16
+ }
17
+ static async open(paths, sessionId) {
18
+ if (process.env.KAVI_ENABLE_SQLITE_HISTORY !== "1") {
19
+ return null;
20
+ }
21
+ await ensureDir(paths.homeStateDir);
22
+ const dbPath = path.join(paths.homeStateDir, `${sessionId}.sqlite`);
23
+ const sqlite = await import("node:sqlite");
24
+ return new EventHistory(new sqlite.DatabaseSync(dbPath));
25
+ }
26
+ insert(sessionId, event) {
27
+ const statement = this.db.prepare(`
28
+ INSERT OR REPLACE INTO events (id, session_id, timestamp, type, payload_json)
29
+ VALUES (?, ?, ?, ?, ?)
30
+ `);
31
+ statement.run(event.id, sessionId, event.timestamp, event.type, JSON.stringify(event.payload));
32
+ }
33
+ close() {
34
+ this.db.close();
35
+ }
36
+ }
37
+
38
+
39
+ //# sourceURL=history.ts