stashes 0.1.51 → 0.1.53
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/cli.js +551 -299
- package/dist/mcp.js +492 -212
- package/dist/web/assets/{index-BtKpaSzl.js → index-C12Cs3ba.js} +42 -41
- package/dist/web/assets/index-C4hi1WQy.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-BT4XvC8L.css +0 -1
package/dist/cli.js
CHANGED
|
@@ -19,8 +19,8 @@ var __toESM = (mod, isNodeMode, target) => {
|
|
|
19
19
|
var __require = import.meta.require;
|
|
20
20
|
|
|
21
21
|
// src/index.ts
|
|
22
|
-
import { readFileSync as
|
|
23
|
-
import { join as
|
|
22
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
23
|
+
import { join as join15, dirname as dirname6 } from "path";
|
|
24
24
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
25
25
|
import { Command } from "commander";
|
|
26
26
|
|
|
@@ -31,9 +31,9 @@ import open from "open";
|
|
|
31
31
|
// ../server/dist/index.js
|
|
32
32
|
import { Hono as Hono2 } from "hono";
|
|
33
33
|
import { cors } from "hono/cors";
|
|
34
|
-
import { join as
|
|
34
|
+
import { join as join11, dirname as dirname3 } from "path";
|
|
35
35
|
import { fileURLToPath } from "url";
|
|
36
|
-
import { existsSync as
|
|
36
|
+
import { existsSync as existsSync11, readFileSync as readFileSync6 } from "fs";
|
|
37
37
|
// ../shared/dist/constants/index.js
|
|
38
38
|
var STASHES_PORT = 4000;
|
|
39
39
|
var DEFAULT_STASH_COUNT = 3;
|
|
@@ -56,156 +56,23 @@ var DEFAULT_DIRECTIVES = [
|
|
|
56
56
|
];
|
|
57
57
|
// ../server/dist/routes/api.js
|
|
58
58
|
import { Hono } from "hono";
|
|
59
|
-
import { join, basename } from "path";
|
|
60
|
-
import { existsSync, readFileSync } from "fs";
|
|
61
|
-
var app = new Hono;
|
|
62
|
-
app.get("/health", (c) => c.json({ status: "ok", service: "stashes" }));
|
|
63
|
-
app.get("/projects", (c) => {
|
|
64
|
-
const persistence = getPersistence();
|
|
65
|
-
const projects = persistence.listProjects();
|
|
66
|
-
const projectsWithCounts = projects.map((p) => ({
|
|
67
|
-
...p,
|
|
68
|
-
stashCount: persistence.listStashes(p.id).length,
|
|
69
|
-
recentScreenshots: persistence.listStashes(p.id).filter((s) => s.screenshotUrl).slice(-4).map((s) => s.screenshotUrl)
|
|
70
|
-
}));
|
|
71
|
-
return c.json({ data: projectsWithCounts });
|
|
72
|
-
});
|
|
73
|
-
app.post("/projects", async (c) => {
|
|
74
|
-
const { name, description } = await c.req.json();
|
|
75
|
-
const project = {
|
|
76
|
-
id: `proj_${crypto.randomUUID().substring(0, 8)}`,
|
|
77
|
-
name,
|
|
78
|
-
description,
|
|
79
|
-
createdAt: new Date().toISOString(),
|
|
80
|
-
updatedAt: new Date().toISOString()
|
|
81
|
-
};
|
|
82
|
-
getPersistence().saveProject(project);
|
|
83
|
-
return c.json({ data: project }, 201);
|
|
84
|
-
});
|
|
85
|
-
app.get("/projects/:id", (c) => {
|
|
86
|
-
const persistence = getPersistence();
|
|
87
|
-
const project = persistence.getProject(c.req.param("id"));
|
|
88
|
-
if (!project)
|
|
89
|
-
return c.json({ error: "Project not found" }, 404);
|
|
90
|
-
const stashes = persistence.listStashes(project.id);
|
|
91
|
-
const chats = persistence.listChats(project.id);
|
|
92
|
-
return c.json({ data: { ...project, stashes, chats } });
|
|
93
|
-
});
|
|
94
|
-
app.delete("/projects/:id", (c) => {
|
|
95
|
-
const id = c.req.param("id");
|
|
96
|
-
getPersistence().deleteProject(id);
|
|
97
|
-
return c.json({ data: { deleted: id } });
|
|
98
|
-
});
|
|
99
|
-
app.get("/chats", (c) => {
|
|
100
|
-
const persistence = getPersistence();
|
|
101
|
-
const project = ensureProject(persistence);
|
|
102
|
-
const chats = persistence.listChats(project.id);
|
|
103
|
-
const stashes = persistence.listStashes(project.id);
|
|
104
|
-
return c.json({ data: { project, chats, stashes } });
|
|
105
|
-
});
|
|
106
|
-
app.post("/chats", async (c) => {
|
|
107
|
-
const persistence = getPersistence();
|
|
108
|
-
const project = ensureProject(persistence);
|
|
109
|
-
const { title, referencedStashIds } = await c.req.json();
|
|
110
|
-
const chatCount = persistence.listChats(project.id).length;
|
|
111
|
-
const chat = {
|
|
112
|
-
id: `chat_${crypto.randomUUID().substring(0, 8)}`,
|
|
113
|
-
projectId: project.id,
|
|
114
|
-
title: title?.trim() || `Chat ${chatCount + 1}`,
|
|
115
|
-
referencedStashIds: referencedStashIds ?? [],
|
|
116
|
-
createdAt: new Date().toISOString(),
|
|
117
|
-
updatedAt: new Date().toISOString()
|
|
118
|
-
};
|
|
119
|
-
persistence.saveChat(chat);
|
|
120
|
-
return c.json({ data: chat }, 201);
|
|
121
|
-
});
|
|
122
|
-
app.patch("/chats/:chatId", async (c) => {
|
|
123
|
-
const persistence = getPersistence();
|
|
124
|
-
const project = ensureProject(persistence);
|
|
125
|
-
const chatId = c.req.param("chatId");
|
|
126
|
-
const chat = persistence.getChat(project.id, chatId);
|
|
127
|
-
if (!chat)
|
|
128
|
-
return c.json({ error: "Chat not found" }, 404);
|
|
129
|
-
const body = await c.req.json();
|
|
130
|
-
const updated = {
|
|
131
|
-
...chat,
|
|
132
|
-
...body.referencedStashIds !== undefined ? { referencedStashIds: body.referencedStashIds } : {},
|
|
133
|
-
updatedAt: new Date().toISOString()
|
|
134
|
-
};
|
|
135
|
-
persistence.saveChat(updated);
|
|
136
|
-
return c.json({ data: updated });
|
|
137
|
-
});
|
|
138
|
-
app.get("/chats/:chatId", (c) => {
|
|
139
|
-
const persistence = getPersistence();
|
|
140
|
-
const project = ensureProject(persistence);
|
|
141
|
-
const chatId = c.req.param("chatId");
|
|
142
|
-
const chat = persistence.getChat(project.id, chatId);
|
|
143
|
-
if (!chat)
|
|
144
|
-
return c.json({ error: "Chat not found" }, 404);
|
|
145
|
-
const messages = persistence.getChatMessages(project.id, chatId);
|
|
146
|
-
const refIds = new Set(chat.referencedStashIds ?? []);
|
|
147
|
-
const stashes = persistence.listStashes(project.id).filter((s) => s.originChatId === chatId || refIds.has(s.id));
|
|
148
|
-
return c.json({ data: { ...chat, messages, stashes } });
|
|
149
|
-
});
|
|
150
|
-
app.delete("/chats/:chatId", (c) => {
|
|
151
|
-
const persistence = getPersistence();
|
|
152
|
-
const project = ensureProject(persistence);
|
|
153
|
-
const chatId = c.req.param("chatId");
|
|
154
|
-
persistence.deleteChat(project.id, chatId);
|
|
155
|
-
return c.json({ data: { deleted: chatId } });
|
|
156
|
-
});
|
|
157
|
-
app.get("/dev-server-status", async (c) => {
|
|
158
|
-
const port = serverState.userDevPort;
|
|
159
|
-
try {
|
|
160
|
-
const res = await fetch(`http://localhost:${port}`, {
|
|
161
|
-
method: "HEAD",
|
|
162
|
-
signal: AbortSignal.timeout(2000)
|
|
163
|
-
});
|
|
164
|
-
return c.json({ up: res.status < 500, port });
|
|
165
|
-
} catch {
|
|
166
|
-
return c.json({ up: false, port });
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
app.get("/screenshots/:filename", (c) => {
|
|
170
|
-
const filename = c.req.param("filename");
|
|
171
|
-
const filePath = join(serverState.projectPath, ".stashes", "screenshots", filename);
|
|
172
|
-
if (!existsSync(filePath))
|
|
173
|
-
return c.json({ error: "Not found" }, 404);
|
|
174
|
-
const content = readFileSync(filePath);
|
|
175
|
-
return new Response(content, {
|
|
176
|
-
headers: { "content-type": "image/png", "cache-control": "no-cache" }
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
function ensureProject(persistence) {
|
|
180
|
-
const projects = persistence.listProjects();
|
|
181
|
-
if (projects.length > 0)
|
|
182
|
-
return projects[0];
|
|
183
|
-
const project = {
|
|
184
|
-
id: `proj_${crypto.randomUUID().substring(0, 8)}`,
|
|
185
|
-
name: basename(serverState.projectPath),
|
|
186
|
-
createdAt: new Date().toISOString(),
|
|
187
|
-
updatedAt: new Date().toISOString()
|
|
188
|
-
};
|
|
189
|
-
persistence.saveProject(project);
|
|
190
|
-
persistence.migrateOldChat(project.id);
|
|
191
|
-
return project;
|
|
192
|
-
}
|
|
193
|
-
var apiRoutes = app;
|
|
59
|
+
import { join as join10, basename } from "path";
|
|
60
|
+
import { existsSync as existsSync10, readFileSync as readFileSync5 } from "fs";
|
|
194
61
|
|
|
195
62
|
// ../core/dist/generation.js
|
|
196
|
-
import { readFileSync as
|
|
63
|
+
import { readFileSync as readFileSync2, existsSync as existsSync7 } from "fs";
|
|
197
64
|
import { join as join7 } from "path";
|
|
198
65
|
var {spawn: spawn3 } = globalThis.Bun;
|
|
199
66
|
import simpleGit3 from "simple-git";
|
|
200
67
|
|
|
201
68
|
// ../core/dist/worktree.js
|
|
202
69
|
import simpleGit from "simple-git";
|
|
203
|
-
import { join as
|
|
204
|
-
import { existsSync as
|
|
70
|
+
import { join as join2 } from "path";
|
|
71
|
+
import { existsSync as existsSync2, rmSync, symlinkSync } from "fs";
|
|
205
72
|
|
|
206
73
|
// ../core/dist/logger.js
|
|
207
|
-
import { appendFileSync, mkdirSync, existsSync
|
|
208
|
-
import { join
|
|
74
|
+
import { appendFileSync, mkdirSync, existsSync } from "fs";
|
|
75
|
+
import { join } from "path";
|
|
209
76
|
var COLORS = {
|
|
210
77
|
reset: "\x1B[0m",
|
|
211
78
|
dim: "\x1B[2m",
|
|
@@ -229,12 +96,12 @@ var LEVEL_LABELS = {
|
|
|
229
96
|
};
|
|
230
97
|
var logFilePath = null;
|
|
231
98
|
function initLogFile(projectPath) {
|
|
232
|
-
const logDir =
|
|
233
|
-
if (!
|
|
99
|
+
const logDir = join(projectPath, ".stashes", "logs");
|
|
100
|
+
if (!existsSync(logDir)) {
|
|
234
101
|
mkdirSync(logDir, { recursive: true });
|
|
235
102
|
}
|
|
236
103
|
const date = new Date().toISOString().substring(0, 10);
|
|
237
|
-
logFilePath =
|
|
104
|
+
logFilePath = join(logDir, `stashes-${date}.log`);
|
|
238
105
|
}
|
|
239
106
|
function ts() {
|
|
240
107
|
return new Date().toISOString().substring(11, 23);
|
|
@@ -274,8 +141,8 @@ class WorktreeManager {
|
|
|
274
141
|
this.cleanupStaleWorktrees();
|
|
275
142
|
}
|
|
276
143
|
async cleanupStaleWorktrees() {
|
|
277
|
-
const worktreesDir =
|
|
278
|
-
if (!
|
|
144
|
+
const worktreesDir = join2(this.projectPath, ".stashes", "worktrees");
|
|
145
|
+
if (!existsSync2(worktreesDir))
|
|
279
146
|
return;
|
|
280
147
|
try {
|
|
281
148
|
const { readdirSync } = await import("fs");
|
|
@@ -285,7 +152,7 @@ class WorktreeManager {
|
|
|
285
152
|
logger.info("worktree", `skipping active screenshot worktree: ${entry}`);
|
|
286
153
|
continue;
|
|
287
154
|
}
|
|
288
|
-
const worktreePath =
|
|
155
|
+
const worktreePath = join2(worktreesDir, entry);
|
|
289
156
|
logger.info("worktree", `cleaning up stale worktree: ${entry}`);
|
|
290
157
|
try {
|
|
291
158
|
await this.git.raw(["worktree", "remove", "--force", worktreePath]);
|
|
@@ -303,8 +170,8 @@ class WorktreeManager {
|
|
|
303
170
|
}
|
|
304
171
|
async createForGeneration(stashId) {
|
|
305
172
|
const branch = `stashes/${stashId}`;
|
|
306
|
-
const worktreePath =
|
|
307
|
-
if (
|
|
173
|
+
const worktreePath = join2(this.projectPath, ".stashes", "worktrees", stashId);
|
|
174
|
+
if (existsSync2(worktreePath)) {
|
|
308
175
|
await this.removeGeneration(stashId);
|
|
309
176
|
}
|
|
310
177
|
logger.info("worktree", `creating generation: ${stashId}`, { branch, path: worktreePath });
|
|
@@ -317,8 +184,8 @@ class WorktreeManager {
|
|
|
317
184
|
}
|
|
318
185
|
async createForVary(stashId, sourceBranch) {
|
|
319
186
|
const branch = `stashes/${stashId}`;
|
|
320
|
-
const worktreePath =
|
|
321
|
-
if (
|
|
187
|
+
const worktreePath = join2(this.projectPath, ".stashes", "worktrees", stashId);
|
|
188
|
+
if (existsSync2(worktreePath)) {
|
|
322
189
|
await this.removeGeneration(stashId);
|
|
323
190
|
}
|
|
324
191
|
logger.info("worktree", `creating vary: ${stashId}`, { sourceBranch, branch });
|
|
@@ -331,20 +198,20 @@ class WorktreeManager {
|
|
|
331
198
|
}
|
|
332
199
|
async removeGeneration(stashId) {
|
|
333
200
|
const info = this.worktrees.get(stashId);
|
|
334
|
-
const worktreePath = info?.path ??
|
|
201
|
+
const worktreePath = info?.path ?? join2(this.projectPath, ".stashes", "worktrees", stashId);
|
|
335
202
|
logger.info("worktree", `removing generation worktree: ${stashId}`);
|
|
336
203
|
try {
|
|
337
204
|
await this.git.raw(["worktree", "remove", "--force", worktreePath]);
|
|
338
205
|
} catch {
|
|
339
|
-
if (
|
|
206
|
+
if (existsSync2(worktreePath)) {
|
|
340
207
|
rmSync(worktreePath, { recursive: true, force: true });
|
|
341
208
|
}
|
|
342
209
|
}
|
|
343
210
|
this.worktrees.delete(stashId);
|
|
344
211
|
}
|
|
345
212
|
async createPreview() {
|
|
346
|
-
const previewPath =
|
|
347
|
-
if (
|
|
213
|
+
const previewPath = join2(this.projectPath, ".stashes", "preview");
|
|
214
|
+
if (existsSync2(previewPath)) {
|
|
348
215
|
try {
|
|
349
216
|
await this.git.raw(["worktree", "remove", "--force", previewPath]);
|
|
350
217
|
} catch {
|
|
@@ -370,11 +237,11 @@ class WorktreeManager {
|
|
|
370
237
|
const msg = err instanceof Error ? err.message : String(err);
|
|
371
238
|
if (msg.includes("is already used by worktree")) {
|
|
372
239
|
logger.warn("worktree", `stale worktree blocking ${branch}, cleaning up`);
|
|
373
|
-
const staleDir =
|
|
240
|
+
const staleDir = join2(this.projectPath, ".stashes", "worktrees", stashId);
|
|
374
241
|
try {
|
|
375
242
|
await this.git.raw(["worktree", "remove", "--force", staleDir]);
|
|
376
243
|
} catch {
|
|
377
|
-
if (
|
|
244
|
+
if (existsSync2(staleDir)) {
|
|
378
245
|
rmSync(staleDir, { recursive: true, force: true });
|
|
379
246
|
}
|
|
380
247
|
}
|
|
@@ -393,9 +260,9 @@ class WorktreeManager {
|
|
|
393
260
|
return PREVIEW_PORT;
|
|
394
261
|
}
|
|
395
262
|
async createPreviewForPool(stashId) {
|
|
396
|
-
const previewPath =
|
|
263
|
+
const previewPath = join2(this.projectPath, ".stashes", "previews", stashId);
|
|
397
264
|
const branch = `stashes/${stashId}`;
|
|
398
|
-
if (
|
|
265
|
+
if (existsSync2(previewPath)) {
|
|
399
266
|
try {
|
|
400
267
|
await this.git.raw(["worktree", "remove", "--force", previewPath]);
|
|
401
268
|
} catch {
|
|
@@ -410,10 +277,10 @@ class WorktreeManager {
|
|
|
410
277
|
const msg = err instanceof Error ? err.message : String(err);
|
|
411
278
|
if (msg.includes("is already used by worktree")) {
|
|
412
279
|
logger.warn("worktree", `branch ${branch} locked by stale worktree, cleaning up`);
|
|
413
|
-
const staleDir =
|
|
414
|
-
const legacyPreview =
|
|
280
|
+
const staleDir = join2(this.projectPath, ".stashes", "worktrees", stashId);
|
|
281
|
+
const legacyPreview = join2(this.projectPath, ".stashes", "preview");
|
|
415
282
|
for (const dir of [staleDir, legacyPreview]) {
|
|
416
|
-
if (
|
|
283
|
+
if (existsSync2(dir)) {
|
|
417
284
|
try {
|
|
418
285
|
await this.git.raw(["worktree", "remove", "--force", dir]);
|
|
419
286
|
} catch {
|
|
@@ -432,12 +299,12 @@ class WorktreeManager {
|
|
|
432
299
|
return previewPath;
|
|
433
300
|
}
|
|
434
301
|
async removePreviewForPool(stashId) {
|
|
435
|
-
const previewPath =
|
|
302
|
+
const previewPath = join2(this.projectPath, ".stashes", "previews", stashId);
|
|
436
303
|
logger.info("worktree", `removing pool preview: ${stashId}`);
|
|
437
304
|
try {
|
|
438
305
|
await this.git.raw(["worktree", "remove", "--force", previewPath]);
|
|
439
306
|
} catch {
|
|
440
|
-
if (
|
|
307
|
+
if (existsSync2(previewPath)) {
|
|
441
308
|
rmSync(previewPath, { recursive: true, force: true });
|
|
442
309
|
}
|
|
443
310
|
}
|
|
@@ -454,7 +321,7 @@ class WorktreeManager {
|
|
|
454
321
|
try {
|
|
455
322
|
await this.git.raw(["worktree", "remove", "--force", this.previewPath]);
|
|
456
323
|
} catch {
|
|
457
|
-
if (
|
|
324
|
+
if (existsSync2(this.previewPath)) {
|
|
458
325
|
rmSync(this.previewPath, { recursive: true, force: true });
|
|
459
326
|
}
|
|
460
327
|
}
|
|
@@ -474,16 +341,16 @@ class WorktreeManager {
|
|
|
474
341
|
}
|
|
475
342
|
}
|
|
476
343
|
} catch {}
|
|
477
|
-
const worktreesDir =
|
|
478
|
-
if (
|
|
344
|
+
const worktreesDir = join2(this.projectPath, ".stashes", "worktrees");
|
|
345
|
+
if (existsSync2(worktreesDir)) {
|
|
479
346
|
rmSync(worktreesDir, { recursive: true, force: true });
|
|
480
347
|
}
|
|
481
|
-
const previewDir =
|
|
482
|
-
if (
|
|
348
|
+
const previewDir = join2(this.projectPath, ".stashes", "preview");
|
|
349
|
+
if (existsSync2(previewDir)) {
|
|
483
350
|
rmSync(previewDir, { recursive: true, force: true });
|
|
484
351
|
}
|
|
485
|
-
const previewsDir =
|
|
486
|
-
if (
|
|
352
|
+
const previewsDir = join2(this.projectPath, ".stashes", "previews");
|
|
353
|
+
if (existsSync2(previewsDir)) {
|
|
487
354
|
rmSync(previewsDir, { recursive: true, force: true });
|
|
488
355
|
}
|
|
489
356
|
logger.info("worktree", `cleanup complete`);
|
|
@@ -491,9 +358,9 @@ class WorktreeManager {
|
|
|
491
358
|
symlinkDeps(worktreePath) {
|
|
492
359
|
const symlinks = ["node_modules", ".env", ".env.local"];
|
|
493
360
|
for (const name of symlinks) {
|
|
494
|
-
const source =
|
|
495
|
-
const target =
|
|
496
|
-
if (
|
|
361
|
+
const source = join2(this.projectPath, name);
|
|
362
|
+
const target = join2(worktreePath, name);
|
|
363
|
+
if (existsSync2(source) && !existsSync2(target)) {
|
|
497
364
|
try {
|
|
498
365
|
symlinkSync(source, target);
|
|
499
366
|
} catch {}
|
|
@@ -503,19 +370,19 @@ class WorktreeManager {
|
|
|
503
370
|
}
|
|
504
371
|
|
|
505
372
|
// ../core/dist/persistence.js
|
|
506
|
-
import { readFileSync
|
|
507
|
-
import { join as
|
|
373
|
+
import { readFileSync, writeFileSync, mkdirSync as mkdirSync2, existsSync as existsSync3, rmSync as rmSync2, readdirSync } from "fs";
|
|
374
|
+
import { join as join3, dirname } from "path";
|
|
508
375
|
var STASHES_DIR = ".stashes";
|
|
509
376
|
function ensureDir(dirPath) {
|
|
510
|
-
if (!
|
|
377
|
+
if (!existsSync3(dirPath)) {
|
|
511
378
|
mkdirSync2(dirPath, { recursive: true });
|
|
512
379
|
}
|
|
513
380
|
}
|
|
514
381
|
function readJson(filePath, fallback) {
|
|
515
|
-
if (!
|
|
382
|
+
if (!existsSync3(filePath))
|
|
516
383
|
return fallback;
|
|
517
384
|
try {
|
|
518
|
-
return JSON.parse(
|
|
385
|
+
return JSON.parse(readFileSync(filePath, "utf-8"));
|
|
519
386
|
} catch {
|
|
520
387
|
logger.warn("persistence", `failed to read ${filePath}, using fallback`);
|
|
521
388
|
return fallback;
|
|
@@ -529,12 +396,12 @@ function writeJson(filePath, data) {
|
|
|
529
396
|
class PersistenceService {
|
|
530
397
|
basePath;
|
|
531
398
|
constructor(projectPath) {
|
|
532
|
-
this.basePath =
|
|
399
|
+
this.basePath = join3(projectPath, STASHES_DIR);
|
|
533
400
|
ensureDir(this.basePath);
|
|
534
401
|
this.ensureGitignore(projectPath);
|
|
535
402
|
}
|
|
536
403
|
listProjects() {
|
|
537
|
-
return readJson(
|
|
404
|
+
return readJson(join3(this.basePath, "projects.json"), []);
|
|
538
405
|
}
|
|
539
406
|
getProject(projectId) {
|
|
540
407
|
return this.listProjects().find((p) => p.id === projectId);
|
|
@@ -547,18 +414,18 @@ class PersistenceService {
|
|
|
547
414
|
} else {
|
|
548
415
|
projects.push(project);
|
|
549
416
|
}
|
|
550
|
-
writeJson(
|
|
417
|
+
writeJson(join3(this.basePath, "projects.json"), projects);
|
|
551
418
|
}
|
|
552
419
|
deleteProject(projectId) {
|
|
553
420
|
const projects = this.listProjects().filter((p) => p.id !== projectId);
|
|
554
|
-
writeJson(
|
|
555
|
-
const projectDir =
|
|
556
|
-
if (
|
|
421
|
+
writeJson(join3(this.basePath, "projects.json"), projects);
|
|
422
|
+
const projectDir = join3(this.basePath, "projects", projectId);
|
|
423
|
+
if (existsSync3(projectDir)) {
|
|
557
424
|
rmSync2(projectDir, { recursive: true, force: true });
|
|
558
425
|
}
|
|
559
426
|
}
|
|
560
427
|
listStashes(projectId) {
|
|
561
|
-
const filePath =
|
|
428
|
+
const filePath = join3(this.basePath, "projects", projectId, "stashes.json");
|
|
562
429
|
return readJson(filePath, []);
|
|
563
430
|
}
|
|
564
431
|
getStash(projectId, stashId) {
|
|
@@ -577,67 +444,67 @@ class PersistenceService {
|
|
|
577
444
|
} else {
|
|
578
445
|
stashes.push(stash);
|
|
579
446
|
}
|
|
580
|
-
const filePath =
|
|
447
|
+
const filePath = join3(this.basePath, "projects", stash.projectId, "stashes.json");
|
|
581
448
|
writeJson(filePath, stashes);
|
|
582
449
|
}
|
|
583
450
|
deleteStash(projectId, stashId) {
|
|
584
451
|
const stashes = this.listStashes(projectId).filter((s) => s.id !== stashId);
|
|
585
|
-
const filePath =
|
|
452
|
+
const filePath = join3(this.basePath, "projects", projectId, "stashes.json");
|
|
586
453
|
writeJson(filePath, stashes);
|
|
587
454
|
}
|
|
588
455
|
getProjectSettings(projectId) {
|
|
589
|
-
const filePath =
|
|
456
|
+
const filePath = join3(this.basePath, "projects", projectId, "settings.json");
|
|
590
457
|
return readJson(filePath, {});
|
|
591
458
|
}
|
|
592
459
|
saveProjectSettings(projectId, settings) {
|
|
593
|
-
const filePath =
|
|
460
|
+
const filePath = join3(this.basePath, "projects", projectId, "settings.json");
|
|
594
461
|
writeJson(filePath, settings);
|
|
595
462
|
}
|
|
596
463
|
listChats(projectId) {
|
|
597
|
-
const dir =
|
|
598
|
-
if (!
|
|
464
|
+
const dir = join3(this.basePath, "projects", projectId, "chats");
|
|
465
|
+
if (!existsSync3(dir))
|
|
599
466
|
return [];
|
|
600
467
|
const files = readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
601
468
|
return files.map((f) => {
|
|
602
|
-
const data = readJson(
|
|
469
|
+
const data = readJson(join3(dir, f), null);
|
|
603
470
|
return data?.chat;
|
|
604
471
|
}).filter(Boolean).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
605
472
|
}
|
|
606
473
|
getChat(projectId, chatId) {
|
|
607
|
-
const filePath =
|
|
474
|
+
const filePath = join3(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
|
|
608
475
|
const data = readJson(filePath, null);
|
|
609
476
|
return data?.chat;
|
|
610
477
|
}
|
|
611
478
|
saveChat(chat) {
|
|
612
|
-
const filePath =
|
|
479
|
+
const filePath = join3(this.basePath, "projects", chat.projectId, "chats", `${chat.id}.json`);
|
|
613
480
|
const existing = readJson(filePath, { chat, messages: [] });
|
|
614
481
|
writeJson(filePath, { ...existing, chat });
|
|
615
482
|
}
|
|
616
483
|
deleteChat(projectId, chatId) {
|
|
617
|
-
const filePath =
|
|
618
|
-
if (
|
|
484
|
+
const filePath = join3(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
|
|
485
|
+
if (existsSync3(filePath)) {
|
|
619
486
|
rmSync2(filePath);
|
|
620
487
|
}
|
|
621
488
|
}
|
|
622
489
|
getChatMessages(projectId, chatId) {
|
|
623
|
-
const filePath =
|
|
490
|
+
const filePath = join3(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
|
|
624
491
|
const data = readJson(filePath, { messages: [] });
|
|
625
492
|
return data.messages;
|
|
626
493
|
}
|
|
627
494
|
saveChatMessage(projectId, chatId, message) {
|
|
628
|
-
const filePath =
|
|
495
|
+
const filePath = join3(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
|
|
629
496
|
const data = readJson(filePath, { chat: this.getChat(projectId, chatId), messages: [] });
|
|
630
497
|
writeJson(filePath, { ...data, messages: [...data.messages, message] });
|
|
631
498
|
}
|
|
632
499
|
updateChatMessage(projectId, chatId, messageId, patch) {
|
|
633
|
-
const filePath =
|
|
500
|
+
const filePath = join3(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
|
|
634
501
|
const data = readJson(filePath, { chat: this.getChat(projectId, chatId), messages: [] });
|
|
635
502
|
const messages = data.messages.map((m) => m.id === messageId ? { ...m, ...patch } : m);
|
|
636
503
|
writeJson(filePath, { ...data, messages });
|
|
637
504
|
}
|
|
638
505
|
migrateOldChat(projectId) {
|
|
639
|
-
const oldPath =
|
|
640
|
-
if (!
|
|
506
|
+
const oldPath = join3(this.basePath, "projects", projectId, "chat.json");
|
|
507
|
+
if (!existsSync3(oldPath))
|
|
641
508
|
return null;
|
|
642
509
|
const messages = readJson(oldPath, []);
|
|
643
510
|
if (messages.length === 0) {
|
|
@@ -652,16 +519,16 @@ class PersistenceService {
|
|
|
652
519
|
createdAt: messages[0].createdAt,
|
|
653
520
|
updatedAt: messages[messages.length - 1].createdAt
|
|
654
521
|
};
|
|
655
|
-
const filePath =
|
|
522
|
+
const filePath = join3(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
|
|
656
523
|
writeJson(filePath, { chat, messages });
|
|
657
524
|
rmSync2(oldPath);
|
|
658
525
|
logger.info("persistence", `migrated old chat.json \u2192 ${chatId}`);
|
|
659
526
|
return chatId;
|
|
660
527
|
}
|
|
661
528
|
ensureGitignore(projectPath) {
|
|
662
|
-
const gitignorePath =
|
|
663
|
-
if (
|
|
664
|
-
const content =
|
|
529
|
+
const gitignorePath = join3(projectPath, ".gitignore");
|
|
530
|
+
if (existsSync3(gitignorePath)) {
|
|
531
|
+
const content = readFileSync(gitignorePath, "utf-8");
|
|
665
532
|
if (!content.includes(".stashes/")) {
|
|
666
533
|
writeFileSync(gitignorePath, content.trimEnd() + `
|
|
667
534
|
.stashes/
|
|
@@ -677,37 +544,84 @@ class PersistenceService {
|
|
|
677
544
|
|
|
678
545
|
// ../core/dist/ai-process.js
|
|
679
546
|
var {spawn } = globalThis.Bun;
|
|
547
|
+
import { writeFileSync as writeFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
|
|
548
|
+
import { join as join4 } from "path";
|
|
549
|
+
import { tmpdir } from "os";
|
|
680
550
|
var CLAUDE_BIN = "/opt/homebrew/bin/claude";
|
|
551
|
+
function getPlaywrightMcpConfigPath() {
|
|
552
|
+
const configDir = join4(tmpdir(), "stashes-mcp");
|
|
553
|
+
const configPath = join4(configDir, "playwright.json");
|
|
554
|
+
if (!existsSync4(configPath)) {
|
|
555
|
+
mkdirSync3(configDir, { recursive: true });
|
|
556
|
+
writeFileSync2(configPath, JSON.stringify({
|
|
557
|
+
mcpServers: {
|
|
558
|
+
playwright: { command: "npx", args: ["@playwright/mcp@latest"] }
|
|
559
|
+
}
|
|
560
|
+
}), "utf-8");
|
|
561
|
+
}
|
|
562
|
+
return configPath;
|
|
563
|
+
}
|
|
564
|
+
var OVERHEAD_TOOLS = [
|
|
565
|
+
"Agent",
|
|
566
|
+
"TodoWrite",
|
|
567
|
+
"TaskCreate",
|
|
568
|
+
"TaskUpdate",
|
|
569
|
+
"TaskList",
|
|
570
|
+
"TaskGet",
|
|
571
|
+
"Skill",
|
|
572
|
+
"ToolSearch",
|
|
573
|
+
"EnterPlanMode",
|
|
574
|
+
"ExitPlanMode",
|
|
575
|
+
"WebSearch",
|
|
576
|
+
"WebFetch",
|
|
577
|
+
"NotebookEdit",
|
|
578
|
+
"mcp__UseAI__*",
|
|
579
|
+
"mcp__stashes__*",
|
|
580
|
+
"mcp__plugin_drills*",
|
|
581
|
+
"mcp__plugin_coverit*"
|
|
582
|
+
];
|
|
681
583
|
var processes = new Map;
|
|
682
|
-
function startAiProcess(
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
584
|
+
function startAiProcess(idOrOpts, prompt, cwd, resumeSessionId, model) {
|
|
585
|
+
const opts = typeof idOrOpts === "string" ? { id: idOrOpts, prompt, cwd, resumeSessionId, model } : idOrOpts;
|
|
586
|
+
const restricted = opts.tools !== undefined;
|
|
587
|
+
killAiProcess(opts.id);
|
|
588
|
+
logger.info("claude", `spawning process: ${opts.id}`, {
|
|
589
|
+
cwd: opts.cwd,
|
|
590
|
+
promptLength: opts.prompt.length,
|
|
591
|
+
promptPreview: opts.prompt.substring(0, 100),
|
|
592
|
+
resumeSessionId: opts.resumeSessionId,
|
|
593
|
+
model: opts.model,
|
|
594
|
+
restricted,
|
|
595
|
+
tools: restricted ? opts.tools.join(",") || "none" : "all"
|
|
690
596
|
});
|
|
691
|
-
const cmd = [CLAUDE_BIN, "-p", prompt, "--output-format=stream-json", "--verbose", "--dangerously-skip-permissions"];
|
|
692
|
-
if (
|
|
693
|
-
cmd.push("--
|
|
597
|
+
const cmd = [CLAUDE_BIN, "-p", opts.prompt, "--output-format=stream-json", "--verbose", "--dangerously-skip-permissions"];
|
|
598
|
+
if (restricted) {
|
|
599
|
+
cmd.push("--tools", opts.tools.length > 0 ? opts.tools.join(",") : '""');
|
|
600
|
+
cmd.push("--disallowedTools", OVERHEAD_TOOLS.join(","));
|
|
601
|
+
cmd.push("--strict-mcp-config");
|
|
602
|
+
if (opts.mcpConfigPath) {
|
|
603
|
+
cmd.push("--mcp-config", opts.mcpConfigPath);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if (opts.resumeSessionId) {
|
|
607
|
+
cmd.push("--resume", opts.resumeSessionId);
|
|
694
608
|
}
|
|
695
|
-
if (model) {
|
|
696
|
-
cmd.push("--model", model);
|
|
609
|
+
if (opts.model) {
|
|
610
|
+
cmd.push("--model", opts.model);
|
|
697
611
|
}
|
|
698
612
|
const proc = spawn({
|
|
699
613
|
cmd,
|
|
700
614
|
stdin: "ignore",
|
|
701
615
|
stdout: "pipe",
|
|
702
616
|
stderr: "pipe",
|
|
703
|
-
cwd,
|
|
617
|
+
cwd: opts.cwd,
|
|
704
618
|
env: { ...process.env }
|
|
705
619
|
});
|
|
706
620
|
proc.exited.then((code) => {
|
|
707
|
-
logger.info("claude", `process exited: ${id}`, { exitCode: code });
|
|
621
|
+
logger.info("claude", `process exited: ${opts.id}`, { exitCode: code });
|
|
708
622
|
});
|
|
709
|
-
const aiProcess = { process: proc, id };
|
|
710
|
-
processes.set(id, aiProcess);
|
|
623
|
+
const aiProcess = { process: proc, id: opts.id };
|
|
624
|
+
processes.set(opts.id, aiProcess);
|
|
711
625
|
return aiProcess;
|
|
712
626
|
}
|
|
713
627
|
function killAiProcess(id) {
|
|
@@ -722,8 +636,8 @@ function killAiProcess(id) {
|
|
|
722
636
|
}
|
|
723
637
|
return false;
|
|
724
638
|
}
|
|
725
|
-
var toolNameMap = new Map;
|
|
726
639
|
async function* parseClaudeStream(proc) {
|
|
640
|
+
const toolNameMap = new Map;
|
|
727
641
|
const stdout = proc.stdout;
|
|
728
642
|
if (!stdout || typeof stdout === "number") {
|
|
729
643
|
throw new Error("Process stdout is not a readable stream");
|
|
@@ -820,18 +734,18 @@ async function* parseClaudeStream(proc) {
|
|
|
820
734
|
|
|
821
735
|
// ../core/dist/smart-screenshot.js
|
|
822
736
|
import { join as join6 } from "path";
|
|
823
|
-
import { mkdirSync as
|
|
737
|
+
import { mkdirSync as mkdirSync5, existsSync as existsSync6 } from "fs";
|
|
824
738
|
import simpleGit2 from "simple-git";
|
|
825
739
|
|
|
826
740
|
// ../core/dist/screenshot.js
|
|
827
741
|
var {spawn: spawn2 } = globalThis.Bun;
|
|
828
742
|
import { join as join5 } from "path";
|
|
829
|
-
import { mkdirSync as
|
|
743
|
+
import { mkdirSync as mkdirSync4, existsSync as existsSync5 } from "fs";
|
|
830
744
|
var SCREENSHOTS_DIR = ".stashes/screenshots";
|
|
831
745
|
async function captureScreenshot(port, projectPath, stashId) {
|
|
832
746
|
const screenshotsDir = join5(projectPath, SCREENSHOTS_DIR);
|
|
833
747
|
if (!existsSync5(screenshotsDir)) {
|
|
834
|
-
|
|
748
|
+
mkdirSync4(screenshotsDir, { recursive: true });
|
|
835
749
|
}
|
|
836
750
|
const filename = `${stashId}.png`;
|
|
837
751
|
const outputPath = join5(screenshotsDir, filename);
|
|
@@ -891,14 +805,8 @@ ${truncatedDiff}`;
|
|
|
891
805
|
}
|
|
892
806
|
function buildScreenshotPrompt(port, diff, screenshotDir, stashId) {
|
|
893
807
|
return [
|
|
894
|
-
"You are a screenshot assistant.
|
|
895
|
-
"",
|
|
896
|
-
"IMPORTANT RULES:",
|
|
897
|
-
"- ONLY use these Playwright MCP tools: mcp__playwright__browser_navigate, mcp__playwright__browser_take_screenshot, mcp__playwright__browser_click, mcp__playwright__browser_snapshot",
|
|
898
|
-
"- Do NOT use any other tools (no Bash, Read, Grep, Agent, ToolSearch, etc.)",
|
|
899
|
-
"- Do NOT start any sessions or call useai tools",
|
|
900
|
-
"- Do NOT read or analyze code files \u2014 the diff below tells you everything",
|
|
901
|
-
"- Be fast \u2014 you have a strict time limit",
|
|
808
|
+
"You are a screenshot assistant. Take screenshots of a running web app using Playwright MCP tools.",
|
|
809
|
+
"Be fast \u2014 you have a strict time limit.",
|
|
902
810
|
"",
|
|
903
811
|
`## The app is running at: http://localhost:${port}`,
|
|
904
812
|
"",
|
|
@@ -965,7 +873,7 @@ async function captureSmartScreenshots(opts) {
|
|
|
965
873
|
const { projectPath, stashId, stashBranch, parentBranch, worktreePath, port, model = "sonnet", timeout = DEFAULT_TIMEOUT } = opts;
|
|
966
874
|
const screenshotDir = join6(projectPath, SCREENSHOTS_DIR2);
|
|
967
875
|
if (!existsSync6(screenshotDir)) {
|
|
968
|
-
|
|
876
|
+
mkdirSync5(screenshotDir, { recursive: true });
|
|
969
877
|
}
|
|
970
878
|
const diff = await getStashDiff(worktreePath, parentBranch);
|
|
971
879
|
if (!diff) {
|
|
@@ -975,7 +883,14 @@ async function captureSmartScreenshots(opts) {
|
|
|
975
883
|
const processId = `screenshot-ai-${stashId}`;
|
|
976
884
|
const prompt = buildScreenshotPrompt(port, diff, screenshotDir, stashId);
|
|
977
885
|
const modelFlag = model === "sonnet" ? "sonnet" : "haiku";
|
|
978
|
-
const aiProcess = startAiProcess(
|
|
886
|
+
const aiProcess = startAiProcess({
|
|
887
|
+
id: processId,
|
|
888
|
+
prompt,
|
|
889
|
+
cwd: worktreePath,
|
|
890
|
+
model: modelFlag,
|
|
891
|
+
tools: [],
|
|
892
|
+
mcpConfigPath: getPlaywrightMcpConfigPath()
|
|
893
|
+
});
|
|
979
894
|
let textOutput = "";
|
|
980
895
|
let timedOut = false;
|
|
981
896
|
const timeoutId = setTimeout(() => {
|
|
@@ -1103,7 +1018,7 @@ async function generate(opts) {
|
|
|
1103
1018
|
if (component?.filePath) {
|
|
1104
1019
|
const sourceFile = join7(projectPath, component.filePath);
|
|
1105
1020
|
if (existsSync7(sourceFile)) {
|
|
1106
|
-
sourceCode =
|
|
1021
|
+
sourceCode = readFileSync2(sourceFile, "utf-8");
|
|
1107
1022
|
}
|
|
1108
1023
|
}
|
|
1109
1024
|
const completedStashes = [];
|
|
@@ -1138,7 +1053,11 @@ async function generate(opts) {
|
|
|
1138
1053
|
} else {
|
|
1139
1054
|
stashPrompt = buildFreeformStashPrompt(prompt, directive);
|
|
1140
1055
|
}
|
|
1141
|
-
const aiProcess = startAiProcess(
|
|
1056
|
+
const aiProcess = startAiProcess({
|
|
1057
|
+
id: stashId,
|
|
1058
|
+
prompt: stashPrompt,
|
|
1059
|
+
cwd: worktree.path
|
|
1060
|
+
});
|
|
1142
1061
|
try {
|
|
1143
1062
|
for await (const chunk of parseClaudeStream(aiProcess.process)) {
|
|
1144
1063
|
emit(onProgress, {
|
|
@@ -1147,14 +1066,63 @@ async function generate(opts) {
|
|
|
1147
1066
|
content: chunk.content,
|
|
1148
1067
|
streamType: chunk.type
|
|
1149
1068
|
});
|
|
1069
|
+
if (chunk.type === "tool_use" && chunk.toolName) {
|
|
1070
|
+
const knownTools = ["Read", "Write", "Edit", "Glob", "Grep", "Bash"];
|
|
1071
|
+
if (knownTools.includes(chunk.toolName)) {
|
|
1072
|
+
const filePath = chunk.toolInput?.file_path ?? chunk.toolInput?.path ?? chunk.toolInput?.command ?? undefined;
|
|
1073
|
+
const lines = chunk.toolInput?.content ? chunk.toolInput.content.split(`
|
|
1074
|
+
`).length : undefined;
|
|
1075
|
+
emit(onProgress, {
|
|
1076
|
+
type: "activity",
|
|
1077
|
+
stashId,
|
|
1078
|
+
action: chunk.toolName,
|
|
1079
|
+
file: filePath,
|
|
1080
|
+
lines,
|
|
1081
|
+
timestamp: Date.now()
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
} else if (chunk.type === "thinking") {
|
|
1085
|
+
emit(onProgress, {
|
|
1086
|
+
type: "activity",
|
|
1087
|
+
stashId,
|
|
1088
|
+
action: "thinking",
|
|
1089
|
+
content: chunk.content.substring(0, 200),
|
|
1090
|
+
timestamp: Date.now()
|
|
1091
|
+
});
|
|
1092
|
+
} else if (chunk.type === "text") {
|
|
1093
|
+
emit(onProgress, {
|
|
1094
|
+
type: "activity",
|
|
1095
|
+
stashId,
|
|
1096
|
+
action: "text",
|
|
1097
|
+
content: chunk.content.substring(0, 200),
|
|
1098
|
+
timestamp: Date.now()
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1150
1101
|
}
|
|
1151
1102
|
await aiProcess.process.exited;
|
|
1152
1103
|
const wtGit = simpleGit3(worktree.path);
|
|
1104
|
+
let hasChanges = false;
|
|
1153
1105
|
try {
|
|
1154
1106
|
await wtGit.add("-A");
|
|
1155
|
-
await wtGit.
|
|
1156
|
-
|
|
1107
|
+
const status = await wtGit.status();
|
|
1108
|
+
if (status.staged.length > 0) {
|
|
1109
|
+
await wtGit.commit(`stashes: stash ${stashId}`);
|
|
1110
|
+
hasChanges = true;
|
|
1111
|
+
logger.info("generation", `committed changes for ${stashId}`, { files: status.staged.length });
|
|
1112
|
+
} else {
|
|
1113
|
+
logger.warn("generation", `AI produced no file changes for ${stashId} \u2014 skipping`);
|
|
1114
|
+
}
|
|
1115
|
+
} catch (commitErr) {
|
|
1116
|
+
logger.warn("generation", `commit failed for ${stashId}`, {
|
|
1117
|
+
error: commitErr instanceof Error ? commitErr.message : String(commitErr)
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1157
1120
|
await worktreeManager.removeGeneration(stashId);
|
|
1121
|
+
if (!hasChanges) {
|
|
1122
|
+
persistence.saveStash({ ...stash, status: "error", error: "AI generation produced no file changes" });
|
|
1123
|
+
emit(onProgress, { type: "error", stashId, error: "AI generation produced no file changes" });
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1158
1126
|
const generatedStash = { ...stash, status: "screenshotting" };
|
|
1159
1127
|
completedStashes.push(generatedStash);
|
|
1160
1128
|
persistence.saveStash(generatedStash);
|
|
@@ -1285,7 +1253,11 @@ async function vary(opts) {
|
|
|
1285
1253
|
persistence.saveStash(stash);
|
|
1286
1254
|
emit2(onProgress, { type: "generating", stashId, number: stashNumber });
|
|
1287
1255
|
const varyPrompt = `The user wants to vary the current UI. Apply this change: ${prompt}`;
|
|
1288
|
-
const aiProcess = startAiProcess(
|
|
1256
|
+
const aiProcess = startAiProcess({
|
|
1257
|
+
id: stashId,
|
|
1258
|
+
prompt: varyPrompt,
|
|
1259
|
+
cwd: worktree.path
|
|
1260
|
+
});
|
|
1289
1261
|
try {
|
|
1290
1262
|
for await (const chunk of parseClaudeStream(aiProcess.process)) {
|
|
1291
1263
|
emit2(onProgress, {
|
|
@@ -1297,11 +1269,29 @@ async function vary(opts) {
|
|
|
1297
1269
|
}
|
|
1298
1270
|
await aiProcess.process.exited;
|
|
1299
1271
|
const wtGit = simpleGit4(worktree.path);
|
|
1272
|
+
let hasChanges = false;
|
|
1300
1273
|
try {
|
|
1301
1274
|
await wtGit.add("-A");
|
|
1302
|
-
await wtGit.
|
|
1303
|
-
|
|
1275
|
+
const status = await wtGit.status();
|
|
1276
|
+
if (status.staged.length > 0) {
|
|
1277
|
+
await wtGit.commit(`stashes: vary ${stashId} from ${sourceStashId}`);
|
|
1278
|
+
hasChanges = true;
|
|
1279
|
+
logger.info("vary", `committed changes for ${stashId}`, { files: status.staged.length });
|
|
1280
|
+
} else {
|
|
1281
|
+
logger.warn("vary", `AI produced no file changes for ${stashId}`);
|
|
1282
|
+
}
|
|
1283
|
+
} catch (commitErr) {
|
|
1284
|
+
logger.warn("vary", `commit failed for ${stashId}`, {
|
|
1285
|
+
error: commitErr instanceof Error ? commitErr.message : String(commitErr)
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1304
1288
|
await worktreeManager.removeGeneration(stashId);
|
|
1289
|
+
if (!hasChanges) {
|
|
1290
|
+
const errorStash = { ...stash, status: "error", error: "AI generation produced no file changes" };
|
|
1291
|
+
persistence.saveStash(errorStash);
|
|
1292
|
+
emit2(onProgress, { type: "error", stashId, error: "AI generation produced no file changes" });
|
|
1293
|
+
return errorStash;
|
|
1294
|
+
}
|
|
1305
1295
|
persistence.saveStash({ ...stash, status: "screenshotting" });
|
|
1306
1296
|
emit2(onProgress, { type: "screenshotting", stashId });
|
|
1307
1297
|
let screenshotPath = "";
|
|
@@ -1399,7 +1389,7 @@ async function cleanup(projectPath) {
|
|
|
1399
1389
|
logger.info("manage", "cleanup complete");
|
|
1400
1390
|
}
|
|
1401
1391
|
// ../server/dist/services/stash-service.js
|
|
1402
|
-
import { readFileSync as
|
|
1392
|
+
import { readFileSync as readFileSync3, existsSync as existsSync8 } from "fs";
|
|
1403
1393
|
import { join as join8 } from "path";
|
|
1404
1394
|
|
|
1405
1395
|
// ../server/dist/services/app-proxy.js
|
|
@@ -1883,6 +1873,7 @@ class StashService {
|
|
|
1883
1873
|
worktreeManager;
|
|
1884
1874
|
persistence;
|
|
1885
1875
|
broadcast;
|
|
1876
|
+
activityStore;
|
|
1886
1877
|
previewPool;
|
|
1887
1878
|
selectedComponent = null;
|
|
1888
1879
|
messageQueue = [];
|
|
@@ -1892,11 +1883,12 @@ class StashService {
|
|
|
1892
1883
|
stashPollTimer = null;
|
|
1893
1884
|
knownStashIds = new Set;
|
|
1894
1885
|
pendingComponentResolve = null;
|
|
1895
|
-
constructor(projectPath, worktreeManager, persistence, broadcast, stashPortStart, stashPortEnd) {
|
|
1886
|
+
constructor(projectPath, worktreeManager, persistence, broadcast, activityStore, stashPortStart, stashPortEnd) {
|
|
1896
1887
|
this.projectPath = projectPath;
|
|
1897
1888
|
this.worktreeManager = worktreeManager;
|
|
1898
1889
|
this.persistence = persistence;
|
|
1899
1890
|
this.broadcast = broadcast;
|
|
1891
|
+
this.activityStore = activityStore;
|
|
1900
1892
|
this.previewPool = new PreviewPool(worktreeManager, broadcast, undefined, undefined, stashPortStart, stashPortEnd);
|
|
1901
1893
|
}
|
|
1902
1894
|
getActiveChatId() {
|
|
@@ -1930,7 +1922,13 @@ class StashService {
|
|
|
1930
1922
|
"Reply with ONLY the file path relative to the project root."
|
|
1931
1923
|
].join(`
|
|
1932
1924
|
`);
|
|
1933
|
-
const aiProcess = startAiProcess(
|
|
1925
|
+
const aiProcess = startAiProcess({
|
|
1926
|
+
id: "resolve-component",
|
|
1927
|
+
prompt,
|
|
1928
|
+
cwd: this.projectPath,
|
|
1929
|
+
model: "claude-haiku-4-5-20251001",
|
|
1930
|
+
tools: ["Read", "Grep", "Glob", "Bash"]
|
|
1931
|
+
});
|
|
1934
1932
|
let resolvedPath = "";
|
|
1935
1933
|
try {
|
|
1936
1934
|
for await (const chunk of parseClaudeStream(aiProcess.process)) {
|
|
@@ -1993,7 +1991,7 @@ class StashService {
|
|
|
1993
1991
|
if (filePath && filePath !== "auto-detect") {
|
|
1994
1992
|
const sourceFile = join8(this.projectPath, filePath);
|
|
1995
1993
|
if (existsSync8(sourceFile)) {
|
|
1996
|
-
sourceCode =
|
|
1994
|
+
sourceCode = readFileSync3(sourceFile, "utf-8");
|
|
1997
1995
|
}
|
|
1998
1996
|
}
|
|
1999
1997
|
let stashContext = "";
|
|
@@ -2040,7 +2038,12 @@ ${sourceCode.substring(0, 3000)}
|
|
|
2040
2038
|
].filter(Boolean).join(`
|
|
2041
2039
|
`);
|
|
2042
2040
|
}
|
|
2043
|
-
const aiProcess = startAiProcess(
|
|
2041
|
+
const aiProcess = startAiProcess({
|
|
2042
|
+
id: "chat",
|
|
2043
|
+
prompt: chatPrompt,
|
|
2044
|
+
cwd: this.projectPath,
|
|
2045
|
+
resumeSessionId: existingSessionId
|
|
2046
|
+
});
|
|
2044
2047
|
let thinkingBuf = "";
|
|
2045
2048
|
let textBuf = "";
|
|
2046
2049
|
const now = new Date().toISOString();
|
|
@@ -2116,6 +2119,21 @@ ${sourceCode.substring(0, 3000)}
|
|
|
2116
2119
|
}
|
|
2117
2120
|
} else if (chunk.type === "tool_result") {
|
|
2118
2121
|
this.stopStashPoll();
|
|
2122
|
+
let stashActivity;
|
|
2123
|
+
const toolNameForSnapshot = chunk.toolName ?? "";
|
|
2124
|
+
if (toolNameForSnapshot.includes("stashes_generate") || toolNameForSnapshot.includes("stashes_vary")) {
|
|
2125
|
+
const projectId2 = this.persistence.listProjects()[0]?.id ?? "";
|
|
2126
|
+
const allStashes = this.persistence.listStashes(projectId2);
|
|
2127
|
+
stashActivity = {};
|
|
2128
|
+
for (const s of allStashes) {
|
|
2129
|
+
if (this.activityStore.has(s.id)) {
|
|
2130
|
+
stashActivity[s.id] = this.activityStore.getSnapshot(s.id);
|
|
2131
|
+
this.activityStore.clear(s.id);
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
if (Object.keys(stashActivity).length === 0)
|
|
2135
|
+
stashActivity = undefined;
|
|
2136
|
+
}
|
|
2119
2137
|
let toolResult = chunk.content;
|
|
2120
2138
|
let isError = false;
|
|
2121
2139
|
try {
|
|
@@ -2123,13 +2141,16 @@ ${sourceCode.substring(0, 3000)}
|
|
|
2123
2141
|
toolResult = parsed.result ?? chunk.content;
|
|
2124
2142
|
isError = !!parsed.is_error;
|
|
2125
2143
|
} catch {}
|
|
2144
|
+
const endToolName = chunk.toolName ?? "unknown";
|
|
2126
2145
|
save({
|
|
2127
2146
|
id: crypto.randomUUID(),
|
|
2128
2147
|
role: "assistant",
|
|
2129
2148
|
content: chunk.content,
|
|
2130
2149
|
type: "tool_end",
|
|
2150
|
+
toolName: endToolName,
|
|
2131
2151
|
toolStatus: isError ? "error" : "completed",
|
|
2132
2152
|
toolResult: toolResult.substring(0, 300),
|
|
2153
|
+
stashActivity,
|
|
2133
2154
|
createdAt: now
|
|
2134
2155
|
});
|
|
2135
2156
|
this.broadcast({
|
|
@@ -2137,6 +2158,7 @@ ${sourceCode.substring(0, 3000)}
|
|
|
2137
2158
|
content: chunk.content,
|
|
2138
2159
|
streamType: "tool_end",
|
|
2139
2160
|
source: "chat",
|
|
2161
|
+
toolName: endToolName,
|
|
2140
2162
|
toolStatus: isError ? "error" : "completed",
|
|
2141
2163
|
toolResult: toolResult.substring(0, 300)
|
|
2142
2164
|
});
|
|
@@ -2319,10 +2341,77 @@ ${refDescriptions.join(`
|
|
|
2319
2341
|
}
|
|
2320
2342
|
}
|
|
2321
2343
|
|
|
2344
|
+
// ../server/dist/services/activity-store.js
|
|
2345
|
+
import { appendFileSync as appendFileSync2, readFileSync as readFileSync4, existsSync as existsSync9, mkdirSync as mkdirSync6, rmSync as rmSync3 } from "fs";
|
|
2346
|
+
import { join as join9, dirname as dirname2 } from "path";
|
|
2347
|
+
|
|
2348
|
+
class ActivityStore {
|
|
2349
|
+
cache = new Map;
|
|
2350
|
+
projectPath;
|
|
2351
|
+
constructor(projectPath) {
|
|
2352
|
+
this.projectPath = projectPath;
|
|
2353
|
+
}
|
|
2354
|
+
jsonlPath(stashId) {
|
|
2355
|
+
return join9(this.projectPath, ".stashes", "activity", `${stashId}.jsonl`);
|
|
2356
|
+
}
|
|
2357
|
+
append(event) {
|
|
2358
|
+
const existing = this.cache.get(event.stashId) ?? [];
|
|
2359
|
+
existing.push(event);
|
|
2360
|
+
this.cache.set(event.stashId, existing);
|
|
2361
|
+
const filePath = this.jsonlPath(event.stashId);
|
|
2362
|
+
const dir = dirname2(filePath);
|
|
2363
|
+
if (!existsSync9(dir))
|
|
2364
|
+
mkdirSync6(dir, { recursive: true });
|
|
2365
|
+
appendFileSync2(filePath, JSON.stringify(event) + `
|
|
2366
|
+
`, "utf-8");
|
|
2367
|
+
}
|
|
2368
|
+
getEvents(stashId) {
|
|
2369
|
+
const cached = this.cache.get(stashId);
|
|
2370
|
+
if (cached && cached.length > 0)
|
|
2371
|
+
return cached;
|
|
2372
|
+
const filePath = this.jsonlPath(stashId);
|
|
2373
|
+
if (!existsSync9(filePath))
|
|
2374
|
+
return [];
|
|
2375
|
+
const lines = readFileSync4(filePath, "utf-8").trim().split(`
|
|
2376
|
+
`).filter(Boolean);
|
|
2377
|
+
const events = lines.map((line) => JSON.parse(line));
|
|
2378
|
+
this.cache.set(stashId, events);
|
|
2379
|
+
return events;
|
|
2380
|
+
}
|
|
2381
|
+
getSnapshot(stashId) {
|
|
2382
|
+
const actions = this.getEvents(stashId);
|
|
2383
|
+
const fileActions = actions.filter((a) => ["Read", "Write", "Edit", "Glob", "Grep", "Bash"].includes(a.action));
|
|
2384
|
+
const uniqueFiles = new Set(fileActions.filter((a) => a.file).map((a) => a.file));
|
|
2385
|
+
const timestamps = actions.map((a) => a.timestamp);
|
|
2386
|
+
const duration = timestamps.length > 1 ? Math.round((Math.max(...timestamps) - Math.min(...timestamps)) / 1000) : 0;
|
|
2387
|
+
return {
|
|
2388
|
+
actions: [...actions],
|
|
2389
|
+
stats: {
|
|
2390
|
+
filesChanged: uniqueFiles.size,
|
|
2391
|
+
duration,
|
|
2392
|
+
totalActions: actions.length
|
|
2393
|
+
}
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
clear(stashId) {
|
|
2397
|
+
this.cache.delete(stashId);
|
|
2398
|
+
const filePath = this.jsonlPath(stashId);
|
|
2399
|
+
if (existsSync9(filePath)) {
|
|
2400
|
+
rmSync3(filePath);
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
has(stashId) {
|
|
2404
|
+
if (this.cache.has(stashId) && (this.cache.get(stashId)?.length ?? 0) > 0)
|
|
2405
|
+
return true;
|
|
2406
|
+
return existsSync9(this.jsonlPath(stashId));
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2322
2410
|
// ../server/dist/services/websocket.js
|
|
2323
2411
|
var worktreeManager;
|
|
2324
2412
|
var stashService;
|
|
2325
2413
|
var persistence;
|
|
2414
|
+
var activityStore;
|
|
2326
2415
|
var clients = new Set;
|
|
2327
2416
|
function broadcast(event) {
|
|
2328
2417
|
const data = JSON.stringify(event);
|
|
@@ -2333,10 +2422,14 @@ function broadcast(event) {
|
|
|
2333
2422
|
function getPersistenceFromWs() {
|
|
2334
2423
|
return persistence;
|
|
2335
2424
|
}
|
|
2425
|
+
function getActivityStoreFromWs() {
|
|
2426
|
+
return activityStore;
|
|
2427
|
+
}
|
|
2336
2428
|
function createWebSocketHandler(projectPath, userDevPort, appProxyPort, stashPortStart, stashPortEnd) {
|
|
2337
2429
|
worktreeManager = new WorktreeManager(projectPath);
|
|
2338
2430
|
persistence = new PersistenceService(projectPath);
|
|
2339
|
-
|
|
2431
|
+
activityStore = new ActivityStore(projectPath);
|
|
2432
|
+
stashService = new StashService(projectPath, worktreeManager, persistence, broadcast, activityStore, stashPortStart, stashPortEnd);
|
|
2340
2433
|
return {
|
|
2341
2434
|
open(ws) {
|
|
2342
2435
|
clients.add(ws);
|
|
@@ -2353,6 +2446,15 @@ function createWebSocketHandler(projectPath, userDevPort, appProxyPort, stashPor
|
|
|
2353
2446
|
if (activeChatId) {
|
|
2354
2447
|
ws.send(JSON.stringify({ type: "processing", chatId: activeChatId }));
|
|
2355
2448
|
}
|
|
2449
|
+
const allStashes = persistence.listStashes(project.id);
|
|
2450
|
+
for (const stash of allStashes) {
|
|
2451
|
+
if (stash.status === "generating" && activityStore.has(stash.id)) {
|
|
2452
|
+
const events = activityStore.getEvents(stash.id);
|
|
2453
|
+
for (const event of events) {
|
|
2454
|
+
ws.send(JSON.stringify({ type: "stash:activity", stashId: stash.id, event }));
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2356
2458
|
},
|
|
2357
2459
|
async message(ws, message) {
|
|
2358
2460
|
const raw = typeof message === "string" ? message : new TextDecoder().decode(message);
|
|
@@ -2432,6 +2534,156 @@ function createWebSocketHandler(projectPath, userDevPort, appProxyPort, stashPor
|
|
|
2432
2534
|
};
|
|
2433
2535
|
}
|
|
2434
2536
|
|
|
2537
|
+
// ../server/dist/routes/api.js
|
|
2538
|
+
var app = new Hono;
|
|
2539
|
+
app.get("/health", (c) => c.json({ status: "ok", service: "stashes" }));
|
|
2540
|
+
app.get("/projects", (c) => {
|
|
2541
|
+
const persistence2 = getPersistence();
|
|
2542
|
+
const projects = persistence2.listProjects();
|
|
2543
|
+
const projectsWithCounts = projects.map((p) => ({
|
|
2544
|
+
...p,
|
|
2545
|
+
stashCount: persistence2.listStashes(p.id).length,
|
|
2546
|
+
recentScreenshots: persistence2.listStashes(p.id).filter((s) => s.screenshotUrl).slice(-4).map((s) => s.screenshotUrl)
|
|
2547
|
+
}));
|
|
2548
|
+
return c.json({ data: projectsWithCounts });
|
|
2549
|
+
});
|
|
2550
|
+
app.post("/projects", async (c) => {
|
|
2551
|
+
const { name, description } = await c.req.json();
|
|
2552
|
+
const project = {
|
|
2553
|
+
id: `proj_${crypto.randomUUID().substring(0, 8)}`,
|
|
2554
|
+
name,
|
|
2555
|
+
description,
|
|
2556
|
+
createdAt: new Date().toISOString(),
|
|
2557
|
+
updatedAt: new Date().toISOString()
|
|
2558
|
+
};
|
|
2559
|
+
getPersistence().saveProject(project);
|
|
2560
|
+
return c.json({ data: project }, 201);
|
|
2561
|
+
});
|
|
2562
|
+
app.get("/projects/:id", (c) => {
|
|
2563
|
+
const persistence2 = getPersistence();
|
|
2564
|
+
const project = persistence2.getProject(c.req.param("id"));
|
|
2565
|
+
if (!project)
|
|
2566
|
+
return c.json({ error: "Project not found" }, 404);
|
|
2567
|
+
const stashes = persistence2.listStashes(project.id);
|
|
2568
|
+
const chats = persistence2.listChats(project.id);
|
|
2569
|
+
return c.json({ data: { ...project, stashes, chats } });
|
|
2570
|
+
});
|
|
2571
|
+
app.delete("/projects/:id", (c) => {
|
|
2572
|
+
const id = c.req.param("id");
|
|
2573
|
+
getPersistence().deleteProject(id);
|
|
2574
|
+
return c.json({ data: { deleted: id } });
|
|
2575
|
+
});
|
|
2576
|
+
app.get("/chats", (c) => {
|
|
2577
|
+
const persistence2 = getPersistence();
|
|
2578
|
+
const project = ensureProject(persistence2);
|
|
2579
|
+
const chats = persistence2.listChats(project.id);
|
|
2580
|
+
const stashes = persistence2.listStashes(project.id);
|
|
2581
|
+
return c.json({ data: { project, chats, stashes } });
|
|
2582
|
+
});
|
|
2583
|
+
app.post("/chats", async (c) => {
|
|
2584
|
+
const persistence2 = getPersistence();
|
|
2585
|
+
const project = ensureProject(persistence2);
|
|
2586
|
+
const { title, referencedStashIds } = await c.req.json();
|
|
2587
|
+
const chatCount = persistence2.listChats(project.id).length;
|
|
2588
|
+
const chat = {
|
|
2589
|
+
id: `chat_${crypto.randomUUID().substring(0, 8)}`,
|
|
2590
|
+
projectId: project.id,
|
|
2591
|
+
title: title?.trim() || `Chat ${chatCount + 1}`,
|
|
2592
|
+
referencedStashIds: referencedStashIds ?? [],
|
|
2593
|
+
createdAt: new Date().toISOString(),
|
|
2594
|
+
updatedAt: new Date().toISOString()
|
|
2595
|
+
};
|
|
2596
|
+
persistence2.saveChat(chat);
|
|
2597
|
+
return c.json({ data: chat }, 201);
|
|
2598
|
+
});
|
|
2599
|
+
app.patch("/chats/:chatId", async (c) => {
|
|
2600
|
+
const persistence2 = getPersistence();
|
|
2601
|
+
const project = ensureProject(persistence2);
|
|
2602
|
+
const chatId = c.req.param("chatId");
|
|
2603
|
+
const chat = persistence2.getChat(project.id, chatId);
|
|
2604
|
+
if (!chat)
|
|
2605
|
+
return c.json({ error: "Chat not found" }, 404);
|
|
2606
|
+
const body = await c.req.json();
|
|
2607
|
+
const updated = {
|
|
2608
|
+
...chat,
|
|
2609
|
+
...body.referencedStashIds !== undefined ? { referencedStashIds: body.referencedStashIds } : {},
|
|
2610
|
+
updatedAt: new Date().toISOString()
|
|
2611
|
+
};
|
|
2612
|
+
persistence2.saveChat(updated);
|
|
2613
|
+
return c.json({ data: updated });
|
|
2614
|
+
});
|
|
2615
|
+
app.get("/chats/:chatId", (c) => {
|
|
2616
|
+
const persistence2 = getPersistence();
|
|
2617
|
+
const project = ensureProject(persistence2);
|
|
2618
|
+
const chatId = c.req.param("chatId");
|
|
2619
|
+
const chat = persistence2.getChat(project.id, chatId);
|
|
2620
|
+
if (!chat)
|
|
2621
|
+
return c.json({ error: "Chat not found" }, 404);
|
|
2622
|
+
const messages = persistence2.getChatMessages(project.id, chatId);
|
|
2623
|
+
const refIds = new Set(chat.referencedStashIds ?? []);
|
|
2624
|
+
const stashes = persistence2.listStashes(project.id).filter((s) => s.originChatId === chatId || refIds.has(s.id));
|
|
2625
|
+
return c.json({ data: { ...chat, messages, stashes } });
|
|
2626
|
+
});
|
|
2627
|
+
app.delete("/chats/:chatId", (c) => {
|
|
2628
|
+
const persistence2 = getPersistence();
|
|
2629
|
+
const project = ensureProject(persistence2);
|
|
2630
|
+
const chatId = c.req.param("chatId");
|
|
2631
|
+
persistence2.deleteChat(project.id, chatId);
|
|
2632
|
+
return c.json({ data: { deleted: chatId } });
|
|
2633
|
+
});
|
|
2634
|
+
app.get("/dev-server-status", async (c) => {
|
|
2635
|
+
const port = serverState.userDevPort;
|
|
2636
|
+
try {
|
|
2637
|
+
const res = await fetch(`http://localhost:${port}`, {
|
|
2638
|
+
method: "HEAD",
|
|
2639
|
+
signal: AbortSignal.timeout(2000)
|
|
2640
|
+
});
|
|
2641
|
+
return c.json({ up: res.status < 500, port });
|
|
2642
|
+
} catch {
|
|
2643
|
+
return c.json({ up: false, port });
|
|
2644
|
+
}
|
|
2645
|
+
});
|
|
2646
|
+
app.get("/screenshots/:filename", (c) => {
|
|
2647
|
+
const filename = c.req.param("filename");
|
|
2648
|
+
const filePath = join10(serverState.projectPath, ".stashes", "screenshots", filename);
|
|
2649
|
+
if (!existsSync10(filePath))
|
|
2650
|
+
return c.json({ error: "Not found" }, 404);
|
|
2651
|
+
const content = readFileSync5(filePath);
|
|
2652
|
+
return new Response(content, {
|
|
2653
|
+
headers: { "content-type": "image/png", "cache-control": "no-cache" }
|
|
2654
|
+
});
|
|
2655
|
+
});
|
|
2656
|
+
app.post("/stash-activity", async (c) => {
|
|
2657
|
+
const events = await c.req.json();
|
|
2658
|
+
const store = getActivityStoreFromWs();
|
|
2659
|
+
for (const event of events) {
|
|
2660
|
+
store.append(event);
|
|
2661
|
+
broadcast({ type: "stash:activity", stashId: event.stashId, event });
|
|
2662
|
+
}
|
|
2663
|
+
return c.json({ ok: true });
|
|
2664
|
+
});
|
|
2665
|
+
app.get("/stash-activity/:stashId", (c) => {
|
|
2666
|
+
const stashId = c.req.param("stashId");
|
|
2667
|
+
const store = getActivityStoreFromWs();
|
|
2668
|
+
const events = store.getEvents(stashId);
|
|
2669
|
+
return c.json({ data: events });
|
|
2670
|
+
});
|
|
2671
|
+
function ensureProject(persistence2) {
|
|
2672
|
+
const projects = persistence2.listProjects();
|
|
2673
|
+
if (projects.length > 0)
|
|
2674
|
+
return projects[0];
|
|
2675
|
+
const project = {
|
|
2676
|
+
id: `proj_${crypto.randomUUID().substring(0, 8)}`,
|
|
2677
|
+
name: basename(serverState.projectPath),
|
|
2678
|
+
createdAt: new Date().toISOString(),
|
|
2679
|
+
updatedAt: new Date().toISOString()
|
|
2680
|
+
};
|
|
2681
|
+
persistence2.saveProject(project);
|
|
2682
|
+
persistence2.migrateOldChat(project.id);
|
|
2683
|
+
return project;
|
|
2684
|
+
}
|
|
2685
|
+
var apiRoutes = app;
|
|
2686
|
+
|
|
2435
2687
|
// ../server/dist/index.js
|
|
2436
2688
|
var serverState = {
|
|
2437
2689
|
projectPath: "",
|
|
@@ -2445,14 +2697,14 @@ app2.use("/*", cors());
|
|
|
2445
2697
|
app2.route("/api", apiRoutes);
|
|
2446
2698
|
app2.get("/*", async (c) => {
|
|
2447
2699
|
const path = c.req.path;
|
|
2448
|
-
const selfDir =
|
|
2449
|
-
const bundledWebDir =
|
|
2450
|
-
const monorepoWebDir =
|
|
2451
|
-
const webDistDir =
|
|
2700
|
+
const selfDir = dirname3(fileURLToPath(import.meta.url));
|
|
2701
|
+
const bundledWebDir = join11(selfDir, "web");
|
|
2702
|
+
const monorepoWebDir = join11(selfDir, "../../web/dist");
|
|
2703
|
+
const webDistDir = existsSync11(join11(bundledWebDir, "index.html")) ? bundledWebDir : monorepoWebDir;
|
|
2452
2704
|
const requestPath = path === "/" ? "/index.html" : path;
|
|
2453
|
-
const filePath =
|
|
2454
|
-
if (
|
|
2455
|
-
const content =
|
|
2705
|
+
const filePath = join11(webDistDir, requestPath);
|
|
2706
|
+
if (existsSync11(filePath) && !filePath.includes("..")) {
|
|
2707
|
+
const content = readFileSync6(filePath);
|
|
2456
2708
|
const ext = filePath.split(".").pop() || "";
|
|
2457
2709
|
const contentTypes = {
|
|
2458
2710
|
html: "text/html; charset=utf-8",
|
|
@@ -2470,9 +2722,9 @@ app2.get("/*", async (c) => {
|
|
|
2470
2722
|
headers: { "content-type": contentTypes[ext] || "application/octet-stream" }
|
|
2471
2723
|
});
|
|
2472
2724
|
}
|
|
2473
|
-
const indexPath =
|
|
2474
|
-
if (
|
|
2475
|
-
const html =
|
|
2725
|
+
const indexPath = join11(webDistDir, "index.html");
|
|
2726
|
+
if (existsSync11(indexPath)) {
|
|
2727
|
+
const html = readFileSync6(indexPath, "utf-8");
|
|
2476
2728
|
return new Response(html, {
|
|
2477
2729
|
headers: { "content-type": "text/html; charset=utf-8" }
|
|
2478
2730
|
});
|
|
@@ -2532,11 +2784,11 @@ async function startServer(projectPath, userDevPort, requestedPort = STASHES_POR
|
|
|
2532
2784
|
}
|
|
2533
2785
|
|
|
2534
2786
|
// ../server/dist/services/detector.js
|
|
2535
|
-
import { existsSync as
|
|
2536
|
-
import { join as
|
|
2787
|
+
import { existsSync as existsSync12, readFileSync as readFileSync7 } from "fs";
|
|
2788
|
+
import { join as join12 } from "path";
|
|
2537
2789
|
function detectFramework(projectPath) {
|
|
2538
|
-
const packageJsonPath =
|
|
2539
|
-
if (!
|
|
2790
|
+
const packageJsonPath = join12(projectPath, "package.json");
|
|
2791
|
+
if (!existsSync12(packageJsonPath)) {
|
|
2540
2792
|
return {
|
|
2541
2793
|
framework: "unknown",
|
|
2542
2794
|
devCommand: "npm run dev",
|
|
@@ -2544,7 +2796,7 @@ function detectFramework(projectPath) {
|
|
|
2544
2796
|
configFile: null
|
|
2545
2797
|
};
|
|
2546
2798
|
}
|
|
2547
|
-
const packageJson = JSON.parse(
|
|
2799
|
+
const packageJson = JSON.parse(readFileSync7(packageJsonPath, "utf-8"));
|
|
2548
2800
|
const deps = {
|
|
2549
2801
|
...packageJson.dependencies,
|
|
2550
2802
|
...packageJson.devDependencies
|
|
@@ -2598,7 +2850,7 @@ function getDevCommand(packageJson, fallback) {
|
|
|
2598
2850
|
}
|
|
2599
2851
|
function findConfig(projectPath, candidates) {
|
|
2600
2852
|
for (const candidate of candidates) {
|
|
2601
|
-
if (
|
|
2853
|
+
if (existsSync12(join12(projectPath, candidate))) {
|
|
2602
2854
|
return candidate;
|
|
2603
2855
|
}
|
|
2604
2856
|
}
|
|
@@ -2790,8 +3042,8 @@ Cleaning up all stashes and worktrees...`);
|
|
|
2790
3042
|
}
|
|
2791
3043
|
|
|
2792
3044
|
// src/commands/setup.ts
|
|
2793
|
-
import { existsSync as
|
|
2794
|
-
import { dirname as
|
|
3045
|
+
import { existsSync as existsSync13, readFileSync as readFileSync8, writeFileSync as writeFileSync3, mkdirSync as mkdirSync7 } from "fs";
|
|
3046
|
+
import { dirname as dirname4, join as join13 } from "path";
|
|
2795
3047
|
import { homedir } from "os";
|
|
2796
3048
|
import * as p from "@clack/prompts";
|
|
2797
3049
|
import pc from "picocolors";
|
|
@@ -2810,63 +3062,63 @@ var MCP_ENTRY_ZED = {
|
|
|
2810
3062
|
};
|
|
2811
3063
|
function buildToolDefinitions() {
|
|
2812
3064
|
const home = homedir();
|
|
2813
|
-
const appSupport =
|
|
3065
|
+
const appSupport = join13(home, "Library", "Application Support");
|
|
2814
3066
|
return [
|
|
2815
3067
|
{
|
|
2816
3068
|
id: "claude-code",
|
|
2817
3069
|
name: "Claude Code",
|
|
2818
|
-
configPath:
|
|
3070
|
+
configPath: join13(home, ".claude.json"),
|
|
2819
3071
|
serversKey: "mcpServers",
|
|
2820
3072
|
format: "standard",
|
|
2821
|
-
detect: () =>
|
|
3073
|
+
detect: () => existsSync13(join13(home, ".claude.json")) || existsSync13(join13(home, ".claude"))
|
|
2822
3074
|
},
|
|
2823
3075
|
{
|
|
2824
3076
|
id: "claude-desktop",
|
|
2825
3077
|
name: "Claude Desktop",
|
|
2826
|
-
configPath:
|
|
3078
|
+
configPath: join13(appSupport, "Claude", "claude_desktop_config.json"),
|
|
2827
3079
|
serversKey: "mcpServers",
|
|
2828
3080
|
format: "standard",
|
|
2829
|
-
detect: () =>
|
|
3081
|
+
detect: () => existsSync13(join13(appSupport, "Claude")) || existsSync13("/Applications/Claude.app")
|
|
2830
3082
|
},
|
|
2831
3083
|
{
|
|
2832
3084
|
id: "vscode",
|
|
2833
3085
|
name: "VS Code",
|
|
2834
|
-
configPath:
|
|
3086
|
+
configPath: join13(appSupport, "Code", "User", "mcp.json"),
|
|
2835
3087
|
serversKey: "servers",
|
|
2836
3088
|
format: "standard",
|
|
2837
|
-
detect: () =>
|
|
3089
|
+
detect: () => existsSync13(join13(appSupport, "Code", "User"))
|
|
2838
3090
|
},
|
|
2839
3091
|
{
|
|
2840
3092
|
id: "cursor",
|
|
2841
3093
|
name: "Cursor",
|
|
2842
|
-
configPath:
|
|
3094
|
+
configPath: join13(home, ".cursor", "mcp.json"),
|
|
2843
3095
|
serversKey: "mcpServers",
|
|
2844
3096
|
format: "standard",
|
|
2845
|
-
detect: () =>
|
|
3097
|
+
detect: () => existsSync13(join13(home, ".cursor"))
|
|
2846
3098
|
},
|
|
2847
3099
|
{
|
|
2848
3100
|
id: "windsurf",
|
|
2849
3101
|
name: "Windsurf",
|
|
2850
|
-
configPath:
|
|
3102
|
+
configPath: join13(home, ".codeium", "windsurf", "mcp_config.json"),
|
|
2851
3103
|
serversKey: "mcpServers",
|
|
2852
3104
|
format: "standard",
|
|
2853
|
-
detect: () =>
|
|
3105
|
+
detect: () => existsSync13(join13(home, ".codeium", "windsurf"))
|
|
2854
3106
|
},
|
|
2855
3107
|
{
|
|
2856
3108
|
id: "zed",
|
|
2857
3109
|
name: "Zed",
|
|
2858
|
-
configPath:
|
|
3110
|
+
configPath: join13(appSupport, "Zed", "settings.json"),
|
|
2859
3111
|
serversKey: "context_servers",
|
|
2860
3112
|
format: "zed",
|
|
2861
|
-
detect: () =>
|
|
3113
|
+
detect: () => existsSync13(join13(appSupport, "Zed"))
|
|
2862
3114
|
}
|
|
2863
3115
|
];
|
|
2864
3116
|
}
|
|
2865
3117
|
function readJsonFile(path) {
|
|
2866
|
-
if (!
|
|
3118
|
+
if (!existsSync13(path))
|
|
2867
3119
|
return {};
|
|
2868
3120
|
try {
|
|
2869
|
-
const raw =
|
|
3121
|
+
const raw = readFileSync8(path, "utf-8").trim();
|
|
2870
3122
|
if (!raw)
|
|
2871
3123
|
return {};
|
|
2872
3124
|
return JSON.parse(raw);
|
|
@@ -2875,8 +3127,8 @@ function readJsonFile(path) {
|
|
|
2875
3127
|
}
|
|
2876
3128
|
}
|
|
2877
3129
|
function writeJsonFile(path, data) {
|
|
2878
|
-
|
|
2879
|
-
|
|
3130
|
+
mkdirSync7(dirname4(path), { recursive: true });
|
|
3131
|
+
writeFileSync3(path, JSON.stringify(data, null, 2) + `
|
|
2880
3132
|
`);
|
|
2881
3133
|
}
|
|
2882
3134
|
function isConfigured(tool) {
|
|
@@ -3014,16 +3266,16 @@ async function setupCommand(options) {
|
|
|
3014
3266
|
|
|
3015
3267
|
// src/commands/update.ts
|
|
3016
3268
|
import { execFileSync, execSync } from "child_process";
|
|
3017
|
-
import { writeFileSync as
|
|
3018
|
-
import { tmpdir } from "os";
|
|
3019
|
-
import { join as
|
|
3269
|
+
import { writeFileSync as writeFileSync4, unlinkSync, chmodSync, readFileSync as readFileSync9 } from "fs";
|
|
3270
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
3271
|
+
import { join as join14, dirname as dirname5 } from "path";
|
|
3020
3272
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3021
3273
|
import * as p2 from "@clack/prompts";
|
|
3022
3274
|
import pc2 from "picocolors";
|
|
3023
3275
|
function getCurrentVersion() {
|
|
3024
|
-
const selfDir =
|
|
3025
|
-
const pkgPath =
|
|
3026
|
-
return JSON.parse(
|
|
3276
|
+
const selfDir = dirname5(fileURLToPath2(import.meta.url));
|
|
3277
|
+
const pkgPath = join14(selfDir, "..", "package.json");
|
|
3278
|
+
return JSON.parse(readFileSync9(pkgPath, "utf-8")).version;
|
|
3027
3279
|
}
|
|
3028
3280
|
function fetchLatestVersion() {
|
|
3029
3281
|
try {
|
|
@@ -3099,8 +3351,8 @@ async function updateCommand() {
|
|
|
3099
3351
|
}
|
|
3100
3352
|
s.stop(`Removed from ${configuredTools.length} tool${configuredTools.length === 1 ? "" : "s"}`);
|
|
3101
3353
|
}
|
|
3102
|
-
const scriptPath =
|
|
3103
|
-
|
|
3354
|
+
const scriptPath = join14(tmpdir2(), `stashes-update-${Date.now()}.sh`);
|
|
3355
|
+
writeFileSync4(scriptPath, buildUpdateScript(), "utf-8");
|
|
3104
3356
|
chmodSync(scriptPath, 493);
|
|
3105
3357
|
try {
|
|
3106
3358
|
execFileSync("bash", [scriptPath], { stdio: "inherit" });
|
|
@@ -3117,9 +3369,9 @@ Update failed. Try manually:`);
|
|
|
3117
3369
|
}
|
|
3118
3370
|
|
|
3119
3371
|
// src/index.ts
|
|
3120
|
-
var selfDir =
|
|
3121
|
-
var pkgPath =
|
|
3122
|
-
var version = JSON.parse(
|
|
3372
|
+
var selfDir = dirname6(fileURLToPath3(import.meta.url));
|
|
3373
|
+
var pkgPath = join15(selfDir, "..", "package.json");
|
|
3374
|
+
var version = JSON.parse(readFileSync10(pkgPath, "utf-8")).version;
|
|
3123
3375
|
var program = new Command;
|
|
3124
3376
|
program.name("stashes").description("Generate AI-powered UI design explorations in your project").version(version, "-v, --version");
|
|
3125
3377
|
program.command("browse", { isDefault: true }).description("Start the Stashes server and open the web UI").argument("[path]", "Project directory", ".").option("-p, --port <port>", "Stashes server port", "4000").option("-d, --dev-port <port>", "Your app dev server port (auto-detected)").option("-n, --stashes <count>", "Default stash count", "3").option("--no-open", "Do not open the browser").action(startCommand);
|