claudeck 1.1.1 → 1.2.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/README.md +30 -4
- package/config/skillsmp-config.json +5 -0
- package/db.js +248 -0
- package/package.json +11 -2
- package/public/css/panels/git-panel.css +220 -0
- package/public/css/panels/skills-manager.css +975 -0
- package/public/css/ui/input-history.css +109 -0
- package/public/css/ui/messages.css +51 -0
- package/public/css/ui/notification-bell.css +421 -0
- package/public/css/ui/sessions.css +41 -0
- package/public/css/ui/worktree.css +442 -0
- package/public/index.html +43 -10
- package/public/js/core/api.js +83 -0
- package/public/js/core/dom.js +15 -0
- package/public/js/features/background-sessions.js +11 -0
- package/public/js/features/chat.js +501 -3
- package/public/js/features/input-history.js +122 -0
- package/public/js/features/projects.js +16 -1
- package/public/js/features/sessions.js +77 -30
- package/public/js/main.js +3 -0
- package/public/js/panels/git-panel.js +385 -6
- package/public/js/panels/skills-manager.js +1005 -0
- package/public/js/ui/messages.js +58 -0
- package/public/js/ui/notification-bell.js +240 -0
- package/public/js/ui/notification-history.js +210 -0
- package/public/js/ui/parallel.js +11 -0
- package/public/js/ui/tab-sdk.js +1 -1
- package/public/style.css +4 -0
- package/server/agent-loop.js +13 -0
- package/server/notification-logger.js +27 -0
- package/server/routes/notifications.js +57 -1
- package/server/routes/sessions.js +41 -0
- package/server/routes/skills.js +454 -0
- package/server/routes/worktrees.js +93 -0
- package/server/utils/git-worktree.js +297 -0
- package/server/ws-handler.js +708 -629
- package/server.js +17 -1
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Worktree Utilities
|
|
3
|
+
*
|
|
4
|
+
* Manages git worktree lifecycle: create, commit, diff, merge, remove, cleanup.
|
|
5
|
+
* All functions receive the project path (original repo root) and use child_process
|
|
6
|
+
* to run git commands — no SDK dependency.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { exec } from "child_process";
|
|
10
|
+
import { promisify } from "util";
|
|
11
|
+
import { join, basename } from "path";
|
|
12
|
+
import { existsSync, mkdirSync } from "fs";
|
|
13
|
+
|
|
14
|
+
const execAsync = promisify(exec);
|
|
15
|
+
const EXEC_OPTS = { timeout: 30000, maxBuffer: 512 * 1024 };
|
|
16
|
+
|
|
17
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
function gitExec(command, cwd) {
|
|
20
|
+
return execAsync(command, { ...EXEC_OPTS, cwd });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Generate a git-safe branch name from a user prompt.
|
|
25
|
+
* Format: claudeck/<timestamp>-<slug>
|
|
26
|
+
*/
|
|
27
|
+
export function generateBranchName(prompt) {
|
|
28
|
+
const ts = Date.now().toString(36); // compact timestamp
|
|
29
|
+
const slug = (prompt || "worktree")
|
|
30
|
+
.toLowerCase()
|
|
31
|
+
.replace(/[^a-z0-9\s-]/g, "")
|
|
32
|
+
.trim()
|
|
33
|
+
.replace(/\s+/g, "-")
|
|
34
|
+
.slice(0, 30)
|
|
35
|
+
.replace(/-+$/, "");
|
|
36
|
+
return `claudeck/${ts}-${slug || "worktree"}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Branch & Status ────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
export async function getCurrentBranch(cwd) {
|
|
42
|
+
const { stdout } = await gitExec("git rev-parse --abbrev-ref HEAD", cwd);
|
|
43
|
+
return stdout.trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function isWorkingTreeClean(cwd) {
|
|
47
|
+
const { stdout } = await gitExec("git status --porcelain", cwd);
|
|
48
|
+
return stdout.trim() === "";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Auto-stash uncommitted changes if the working tree is dirty.
|
|
53
|
+
* Returns { stashed: boolean }.
|
|
54
|
+
*/
|
|
55
|
+
export async function ensureCleanWorkingTree(cwd) {
|
|
56
|
+
const clean = await isWorkingTreeClean(cwd);
|
|
57
|
+
if (clean) return { stashed: false };
|
|
58
|
+
await gitExec('git stash push -m "claudeck-worktree-auto"', cwd);
|
|
59
|
+
return { stashed: true };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Pop the auto-stash created by ensureCleanWorkingTree, if present.
|
|
64
|
+
*/
|
|
65
|
+
export async function unstash(cwd) {
|
|
66
|
+
try {
|
|
67
|
+
const { stdout } = await gitExec("git stash list", cwd);
|
|
68
|
+
const lines = stdout.split("\n");
|
|
69
|
+
const idx = lines.findIndex((l) => l.includes("claudeck-worktree-auto"));
|
|
70
|
+
if (idx >= 0) {
|
|
71
|
+
await gitExec(`git stash pop stash@{${idx}}`, cwd);
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
// No stash or pop conflict — silently continue
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Worktree Lifecycle ─────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Create a new git worktree with a dedicated branch.
|
|
82
|
+
* Worktrees are stored under <projectPath>/.claudeck-worktrees/<branchSlug>
|
|
83
|
+
*/
|
|
84
|
+
export async function createWorktree(projectPath, branchName) {
|
|
85
|
+
const baseBranch = await getCurrentBranch(projectPath);
|
|
86
|
+
const branchSlug = branchName.replace(/\//g, "-");
|
|
87
|
+
const wtDir = join(projectPath, ".claudeck-worktrees");
|
|
88
|
+
const worktreePath = join(wtDir, branchSlug);
|
|
89
|
+
|
|
90
|
+
if (!existsSync(wtDir)) {
|
|
91
|
+
mkdirSync(wtDir, { recursive: true });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Auto-add .claudeck-worktrees to .gitignore if not already present
|
|
95
|
+
await ensureGitignore(projectPath);
|
|
96
|
+
|
|
97
|
+
await gitExec(
|
|
98
|
+
`git worktree add "${worktreePath}" -b "${branchName}"`,
|
|
99
|
+
projectPath,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
return { worktreePath, branchName, baseBranch };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Remove a worktree and delete its branch. Idempotent.
|
|
107
|
+
*/
|
|
108
|
+
export async function removeWorktree(projectPath, worktreePath, branchName) {
|
|
109
|
+
try {
|
|
110
|
+
await gitExec(`git worktree remove --force "${worktreePath}"`, projectPath);
|
|
111
|
+
} catch {
|
|
112
|
+
// Already removed or path doesn't exist
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
await gitExec(`git branch -D "${branchName}"`, projectPath);
|
|
116
|
+
} catch {
|
|
117
|
+
// Branch already deleted
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Commit & Diff ──────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Stage all changes and commit inside a worktree.
|
|
125
|
+
* Returns { committed: boolean, hash?: string }.
|
|
126
|
+
*/
|
|
127
|
+
export async function autoCommitWorktree(worktreePath, message) {
|
|
128
|
+
const clean = await isWorkingTreeClean(worktreePath);
|
|
129
|
+
if (clean) return { committed: false };
|
|
130
|
+
|
|
131
|
+
await gitExec("git add -A", worktreePath);
|
|
132
|
+
const { stdout } = await gitExec(
|
|
133
|
+
`git commit -m "${message.replace(/"/g, '\\"')}"`,
|
|
134
|
+
worktreePath,
|
|
135
|
+
);
|
|
136
|
+
const hashMatch = stdout.match(/\[.*\s([a-f0-9]+)\]/);
|
|
137
|
+
return { committed: true, hash: hashMatch?.[1] || null };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get the unified diff between the worktree branch and the base branch.
|
|
142
|
+
* Auto-commits any uncommitted changes first.
|
|
143
|
+
*/
|
|
144
|
+
export async function getWorktreeDiff(worktreePath, baseBranch) {
|
|
145
|
+
// Ensure everything is committed before diffing
|
|
146
|
+
await autoCommitWorktree(worktreePath, "claudeck: auto-commit for diff");
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const { stdout } = await gitExec(
|
|
150
|
+
`git diff "${baseBranch}"...HEAD`,
|
|
151
|
+
worktreePath,
|
|
152
|
+
);
|
|
153
|
+
return stdout;
|
|
154
|
+
} catch {
|
|
155
|
+
return "";
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get diff stats (files changed, insertions, deletions).
|
|
161
|
+
*/
|
|
162
|
+
export async function getWorktreeDiffStats(worktreePath, baseBranch) {
|
|
163
|
+
try {
|
|
164
|
+
const { stdout } = await gitExec(
|
|
165
|
+
`git diff --stat "${baseBranch}"...HEAD`,
|
|
166
|
+
worktreePath,
|
|
167
|
+
);
|
|
168
|
+
const lines = stdout.trim().split("\n");
|
|
169
|
+
const summaryLine = lines[lines.length - 1] || "";
|
|
170
|
+
const filesMatch = summaryLine.match(/(\d+)\s+file/);
|
|
171
|
+
const insertMatch = summaryLine.match(/(\d+)\s+insertion/);
|
|
172
|
+
const deleteMatch = summaryLine.match(/(\d+)\s+deletion/);
|
|
173
|
+
return {
|
|
174
|
+
files: parseInt(filesMatch?.[1] || "0", 10),
|
|
175
|
+
insertions: parseInt(insertMatch?.[1] || "0", 10),
|
|
176
|
+
deletions: parseInt(deleteMatch?.[1] || "0", 10),
|
|
177
|
+
};
|
|
178
|
+
} catch {
|
|
179
|
+
return { files: 0, insertions: 0, deletions: 0 };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── Merge ──────────────────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Squash-merge the worktree branch into the main working tree.
|
|
187
|
+
* Stashes any uncommitted changes, merges, then restores the stash.
|
|
188
|
+
*/
|
|
189
|
+
export async function squashMerge(
|
|
190
|
+
projectPath,
|
|
191
|
+
worktreePath,
|
|
192
|
+
branchName,
|
|
193
|
+
commitMessage,
|
|
194
|
+
) {
|
|
195
|
+
// Ensure worktree changes are committed
|
|
196
|
+
await autoCommitWorktree(worktreePath, "claudeck: auto-commit before merge");
|
|
197
|
+
|
|
198
|
+
// Stash main tree if dirty
|
|
199
|
+
const { stashed } = await ensureCleanWorkingTree(projectPath);
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
await gitExec(`git merge --squash "${branchName}"`, projectPath);
|
|
203
|
+
const { stdout } = await gitExec(
|
|
204
|
+
`git commit -m "${commitMessage.replace(/"/g, '\\"')}"`,
|
|
205
|
+
projectPath,
|
|
206
|
+
);
|
|
207
|
+
const hashMatch = stdout.match(/\[.*\s([a-f0-9]+)\]/);
|
|
208
|
+
|
|
209
|
+
// Clean up worktree and branch
|
|
210
|
+
await removeWorktree(projectPath, worktreePath, branchName);
|
|
211
|
+
|
|
212
|
+
return { merged: true, hash: hashMatch?.[1] || null };
|
|
213
|
+
} finally {
|
|
214
|
+
if (stashed) await unstash(projectPath);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── Listing & Cleanup ──────────────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* List all git worktrees for a project (from git's perspective).
|
|
222
|
+
*/
|
|
223
|
+
export async function listGitWorktrees(projectPath) {
|
|
224
|
+
try {
|
|
225
|
+
const { stdout } = await gitExec(
|
|
226
|
+
"git worktree list --porcelain",
|
|
227
|
+
projectPath,
|
|
228
|
+
);
|
|
229
|
+
const worktrees = [];
|
|
230
|
+
let current = {};
|
|
231
|
+
for (const line of stdout.split("\n")) {
|
|
232
|
+
if (line.startsWith("worktree ")) {
|
|
233
|
+
if (current.path) worktrees.push(current);
|
|
234
|
+
current = { path: line.slice(9) };
|
|
235
|
+
} else if (line.startsWith("HEAD ")) {
|
|
236
|
+
current.head = line.slice(5);
|
|
237
|
+
} else if (line.startsWith("branch ")) {
|
|
238
|
+
current.branch = line.slice(7);
|
|
239
|
+
} else if (line === "") {
|
|
240
|
+
if (current.path) worktrees.push(current);
|
|
241
|
+
current = {};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (current.path) worktrees.push(current);
|
|
245
|
+
return worktrees;
|
|
246
|
+
} catch {
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Reconcile DB records against actual git worktrees on startup.
|
|
253
|
+
* Marks orphaned DB records and removes stale filesystem worktrees.
|
|
254
|
+
*/
|
|
255
|
+
export async function reconcileOrphanedWorktrees(
|
|
256
|
+
listActiveFn,
|
|
257
|
+
updateStatusFn,
|
|
258
|
+
) {
|
|
259
|
+
const activeRecords = listActiveFn();
|
|
260
|
+
if (!activeRecords.length) return;
|
|
261
|
+
|
|
262
|
+
// Group by project
|
|
263
|
+
const byProject = new Map();
|
|
264
|
+
for (const wt of activeRecords) {
|
|
265
|
+
if (!byProject.has(wt.project_path)) byProject.set(wt.project_path, []);
|
|
266
|
+
byProject.get(wt.project_path).push(wt);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
for (const [projectPath, records] of byProject) {
|
|
270
|
+
const gitWts = await listGitWorktrees(projectPath);
|
|
271
|
+
const gitPaths = new Set(gitWts.map((g) => g.path));
|
|
272
|
+
|
|
273
|
+
for (const record of records) {
|
|
274
|
+
if (!gitPaths.has(record.worktree_path)) {
|
|
275
|
+
updateStatusFn(record.id, "orphaned");
|
|
276
|
+
console.log(`Marked orphaned worktree: ${record.branch_name}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ── Internal ───────────────────────────────────────────────────────────────
|
|
283
|
+
|
|
284
|
+
async function ensureGitignore(projectPath) {
|
|
285
|
+
const gitignorePath = join(projectPath, ".gitignore");
|
|
286
|
+
const entry = ".claudeck-worktrees/";
|
|
287
|
+
try {
|
|
288
|
+
const { readFileSync, appendFileSync } = await import("fs");
|
|
289
|
+
if (existsSync(gitignorePath)) {
|
|
290
|
+
const content = readFileSync(gitignorePath, "utf8");
|
|
291
|
+
if (content.includes(entry)) return;
|
|
292
|
+
}
|
|
293
|
+
appendFileSync(gitignorePath, `\n# Claudeck worktrees\n${entry}\n`);
|
|
294
|
+
} catch {
|
|
295
|
+
// Non-critical — user can add manually
|
|
296
|
+
}
|
|
297
|
+
}
|