stashes 0.1.50 → 0.1.52
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 +537 -299
- package/dist/mcp.js +478 -212
- package/dist/web/assets/{index-B2HUtHbu.js → index-DjPE9klT.js} +40 -40
- package/dist/web/assets/index-mdSV-b1c.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-DXopYbWS.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,68 @@ 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
|
+
var DEFAULT_TOOLS = ["Read", "Write", "Edit", "Glob", "Grep", "Bash"];
|
|
552
|
+
function getPlaywrightMcpConfigPath() {
|
|
553
|
+
const configDir = join4(tmpdir(), "stashes-mcp");
|
|
554
|
+
const configPath = join4(configDir, "playwright.json");
|
|
555
|
+
if (!existsSync4(configPath)) {
|
|
556
|
+
mkdirSync3(configDir, { recursive: true });
|
|
557
|
+
writeFileSync2(configPath, JSON.stringify({
|
|
558
|
+
mcpServers: {
|
|
559
|
+
playwright: { command: "npx", args: ["@playwright/mcp@latest"] }
|
|
560
|
+
}
|
|
561
|
+
}), "utf-8");
|
|
562
|
+
}
|
|
563
|
+
return configPath;
|
|
564
|
+
}
|
|
681
565
|
var processes = new Map;
|
|
682
|
-
function startAiProcess(
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
566
|
+
function startAiProcess(idOrOpts, prompt, cwd, resumeSessionId, model) {
|
|
567
|
+
const opts = typeof idOrOpts === "string" ? { id: idOrOpts, prompt, cwd, resumeSessionId, model } : idOrOpts;
|
|
568
|
+
const bare = opts.bare ?? true;
|
|
569
|
+
const tools = opts.tools ?? DEFAULT_TOOLS;
|
|
570
|
+
killAiProcess(opts.id);
|
|
571
|
+
logger.info("claude", `spawning process: ${opts.id}`, {
|
|
572
|
+
cwd: opts.cwd,
|
|
573
|
+
promptLength: opts.prompt.length,
|
|
574
|
+
promptPreview: opts.prompt.substring(0, 100),
|
|
575
|
+
resumeSessionId: opts.resumeSessionId,
|
|
576
|
+
model: opts.model,
|
|
577
|
+
bare,
|
|
578
|
+
tools: bare ? tools.join(",") : "all"
|
|
690
579
|
});
|
|
691
|
-
const cmd = [CLAUDE_BIN, "-p", prompt, "--output-format=stream-json", "--verbose", "--dangerously-skip-permissions"];
|
|
692
|
-
if (
|
|
693
|
-
cmd.push("--
|
|
580
|
+
const cmd = [CLAUDE_BIN, "-p", opts.prompt, "--output-format=stream-json", "--verbose", "--dangerously-skip-permissions"];
|
|
581
|
+
if (bare) {
|
|
582
|
+
cmd.push("--bare");
|
|
583
|
+
if (tools.length > 0) {
|
|
584
|
+
cmd.push("--tools", tools.join(","));
|
|
585
|
+
}
|
|
586
|
+
if (opts.mcpConfigPath) {
|
|
587
|
+
cmd.push("--mcp-config", opts.mcpConfigPath);
|
|
588
|
+
}
|
|
694
589
|
}
|
|
695
|
-
if (
|
|
696
|
-
cmd.push("--
|
|
590
|
+
if (opts.resumeSessionId) {
|
|
591
|
+
cmd.push("--resume", opts.resumeSessionId);
|
|
592
|
+
}
|
|
593
|
+
if (opts.model) {
|
|
594
|
+
cmd.push("--model", opts.model);
|
|
697
595
|
}
|
|
698
596
|
const proc = spawn({
|
|
699
597
|
cmd,
|
|
700
598
|
stdin: "ignore",
|
|
701
599
|
stdout: "pipe",
|
|
702
600
|
stderr: "pipe",
|
|
703
|
-
cwd,
|
|
601
|
+
cwd: opts.cwd,
|
|
704
602
|
env: { ...process.env }
|
|
705
603
|
});
|
|
706
604
|
proc.exited.then((code) => {
|
|
707
|
-
logger.info("claude", `process exited: ${id}`, { exitCode: code });
|
|
605
|
+
logger.info("claude", `process exited: ${opts.id}`, { exitCode: code });
|
|
708
606
|
});
|
|
709
|
-
const aiProcess = { process: proc, id };
|
|
710
|
-
processes.set(id, aiProcess);
|
|
607
|
+
const aiProcess = { process: proc, id: opts.id };
|
|
608
|
+
processes.set(opts.id, aiProcess);
|
|
711
609
|
return aiProcess;
|
|
712
610
|
}
|
|
713
611
|
function killAiProcess(id) {
|
|
@@ -722,8 +620,8 @@ function killAiProcess(id) {
|
|
|
722
620
|
}
|
|
723
621
|
return false;
|
|
724
622
|
}
|
|
725
|
-
var toolNameMap = new Map;
|
|
726
623
|
async function* parseClaudeStream(proc) {
|
|
624
|
+
const toolNameMap = new Map;
|
|
727
625
|
const stdout = proc.stdout;
|
|
728
626
|
if (!stdout || typeof stdout === "number") {
|
|
729
627
|
throw new Error("Process stdout is not a readable stream");
|
|
@@ -820,18 +718,18 @@ async function* parseClaudeStream(proc) {
|
|
|
820
718
|
|
|
821
719
|
// ../core/dist/smart-screenshot.js
|
|
822
720
|
import { join as join6 } from "path";
|
|
823
|
-
import { mkdirSync as
|
|
721
|
+
import { mkdirSync as mkdirSync5, existsSync as existsSync6 } from "fs";
|
|
824
722
|
import simpleGit2 from "simple-git";
|
|
825
723
|
|
|
826
724
|
// ../core/dist/screenshot.js
|
|
827
725
|
var {spawn: spawn2 } = globalThis.Bun;
|
|
828
726
|
import { join as join5 } from "path";
|
|
829
|
-
import { mkdirSync as
|
|
727
|
+
import { mkdirSync as mkdirSync4, existsSync as existsSync5 } from "fs";
|
|
830
728
|
var SCREENSHOTS_DIR = ".stashes/screenshots";
|
|
831
729
|
async function captureScreenshot(port, projectPath, stashId) {
|
|
832
730
|
const screenshotsDir = join5(projectPath, SCREENSHOTS_DIR);
|
|
833
731
|
if (!existsSync5(screenshotsDir)) {
|
|
834
|
-
|
|
732
|
+
mkdirSync4(screenshotsDir, { recursive: true });
|
|
835
733
|
}
|
|
836
734
|
const filename = `${stashId}.png`;
|
|
837
735
|
const outputPath = join5(screenshotsDir, filename);
|
|
@@ -891,14 +789,8 @@ ${truncatedDiff}`;
|
|
|
891
789
|
}
|
|
892
790
|
function buildScreenshotPrompt(port, diff, screenshotDir, stashId) {
|
|
893
791
|
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",
|
|
792
|
+
"You are a screenshot assistant. Take screenshots of a running web app using Playwright MCP tools.",
|
|
793
|
+
"Be fast \u2014 you have a strict time limit.",
|
|
902
794
|
"",
|
|
903
795
|
`## The app is running at: http://localhost:${port}`,
|
|
904
796
|
"",
|
|
@@ -965,7 +857,7 @@ async function captureSmartScreenshots(opts) {
|
|
|
965
857
|
const { projectPath, stashId, stashBranch, parentBranch, worktreePath, port, model = "sonnet", timeout = DEFAULT_TIMEOUT } = opts;
|
|
966
858
|
const screenshotDir = join6(projectPath, SCREENSHOTS_DIR2);
|
|
967
859
|
if (!existsSync6(screenshotDir)) {
|
|
968
|
-
|
|
860
|
+
mkdirSync5(screenshotDir, { recursive: true });
|
|
969
861
|
}
|
|
970
862
|
const diff = await getStashDiff(worktreePath, parentBranch);
|
|
971
863
|
if (!diff) {
|
|
@@ -975,7 +867,15 @@ async function captureSmartScreenshots(opts) {
|
|
|
975
867
|
const processId = `screenshot-ai-${stashId}`;
|
|
976
868
|
const prompt = buildScreenshotPrompt(port, diff, screenshotDir, stashId);
|
|
977
869
|
const modelFlag = model === "sonnet" ? "sonnet" : "haiku";
|
|
978
|
-
const aiProcess = startAiProcess(
|
|
870
|
+
const aiProcess = startAiProcess({
|
|
871
|
+
id: processId,
|
|
872
|
+
prompt,
|
|
873
|
+
cwd: worktreePath,
|
|
874
|
+
model: modelFlag,
|
|
875
|
+
bare: true,
|
|
876
|
+
tools: [],
|
|
877
|
+
mcpConfigPath: getPlaywrightMcpConfigPath()
|
|
878
|
+
});
|
|
979
879
|
let textOutput = "";
|
|
980
880
|
let timedOut = false;
|
|
981
881
|
const timeoutId = setTimeout(() => {
|
|
@@ -1103,7 +1003,7 @@ async function generate(opts) {
|
|
|
1103
1003
|
if (component?.filePath) {
|
|
1104
1004
|
const sourceFile = join7(projectPath, component.filePath);
|
|
1105
1005
|
if (existsSync7(sourceFile)) {
|
|
1106
|
-
sourceCode =
|
|
1006
|
+
sourceCode = readFileSync2(sourceFile, "utf-8");
|
|
1107
1007
|
}
|
|
1108
1008
|
}
|
|
1109
1009
|
const completedStashes = [];
|
|
@@ -1138,7 +1038,12 @@ async function generate(opts) {
|
|
|
1138
1038
|
} else {
|
|
1139
1039
|
stashPrompt = buildFreeformStashPrompt(prompt, directive);
|
|
1140
1040
|
}
|
|
1141
|
-
const aiProcess = startAiProcess(
|
|
1041
|
+
const aiProcess = startAiProcess({
|
|
1042
|
+
id: stashId,
|
|
1043
|
+
prompt: stashPrompt,
|
|
1044
|
+
cwd: worktree.path,
|
|
1045
|
+
bare: false
|
|
1046
|
+
});
|
|
1142
1047
|
try {
|
|
1143
1048
|
for await (const chunk of parseClaudeStream(aiProcess.process)) {
|
|
1144
1049
|
emit(onProgress, {
|
|
@@ -1147,14 +1052,63 @@ async function generate(opts) {
|
|
|
1147
1052
|
content: chunk.content,
|
|
1148
1053
|
streamType: chunk.type
|
|
1149
1054
|
});
|
|
1055
|
+
if (chunk.type === "tool_use" && chunk.toolName) {
|
|
1056
|
+
const knownTools = ["Read", "Write", "Edit", "Glob", "Grep", "Bash"];
|
|
1057
|
+
if (knownTools.includes(chunk.toolName)) {
|
|
1058
|
+
const filePath = chunk.toolInput?.file_path ?? chunk.toolInput?.path ?? chunk.toolInput?.command ?? undefined;
|
|
1059
|
+
const lines = chunk.toolInput?.content ? chunk.toolInput.content.split(`
|
|
1060
|
+
`).length : undefined;
|
|
1061
|
+
emit(onProgress, {
|
|
1062
|
+
type: "activity",
|
|
1063
|
+
stashId,
|
|
1064
|
+
action: chunk.toolName,
|
|
1065
|
+
file: filePath,
|
|
1066
|
+
lines,
|
|
1067
|
+
timestamp: Date.now()
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
} else if (chunk.type === "thinking") {
|
|
1071
|
+
emit(onProgress, {
|
|
1072
|
+
type: "activity",
|
|
1073
|
+
stashId,
|
|
1074
|
+
action: "thinking",
|
|
1075
|
+
content: chunk.content.substring(0, 200),
|
|
1076
|
+
timestamp: Date.now()
|
|
1077
|
+
});
|
|
1078
|
+
} else if (chunk.type === "text") {
|
|
1079
|
+
emit(onProgress, {
|
|
1080
|
+
type: "activity",
|
|
1081
|
+
stashId,
|
|
1082
|
+
action: "text",
|
|
1083
|
+
content: chunk.content.substring(0, 200),
|
|
1084
|
+
timestamp: Date.now()
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1150
1087
|
}
|
|
1151
1088
|
await aiProcess.process.exited;
|
|
1152
1089
|
const wtGit = simpleGit3(worktree.path);
|
|
1090
|
+
let hasChanges = false;
|
|
1153
1091
|
try {
|
|
1154
1092
|
await wtGit.add("-A");
|
|
1155
|
-
await wtGit.
|
|
1156
|
-
|
|
1093
|
+
const status = await wtGit.status();
|
|
1094
|
+
if (status.staged.length > 0) {
|
|
1095
|
+
await wtGit.commit(`stashes: stash ${stashId}`);
|
|
1096
|
+
hasChanges = true;
|
|
1097
|
+
logger.info("generation", `committed changes for ${stashId}`, { files: status.staged.length });
|
|
1098
|
+
} else {
|
|
1099
|
+
logger.warn("generation", `AI produced no file changes for ${stashId} \u2014 skipping`);
|
|
1100
|
+
}
|
|
1101
|
+
} catch (commitErr) {
|
|
1102
|
+
logger.warn("generation", `commit failed for ${stashId}`, {
|
|
1103
|
+
error: commitErr instanceof Error ? commitErr.message : String(commitErr)
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1157
1106
|
await worktreeManager.removeGeneration(stashId);
|
|
1107
|
+
if (!hasChanges) {
|
|
1108
|
+
persistence.saveStash({ ...stash, status: "error", error: "AI generation produced no file changes" });
|
|
1109
|
+
emit(onProgress, { type: "error", stashId, error: "AI generation produced no file changes" });
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1158
1112
|
const generatedStash = { ...stash, status: "screenshotting" };
|
|
1159
1113
|
completedStashes.push(generatedStash);
|
|
1160
1114
|
persistence.saveStash(generatedStash);
|
|
@@ -1285,7 +1239,12 @@ async function vary(opts) {
|
|
|
1285
1239
|
persistence.saveStash(stash);
|
|
1286
1240
|
emit2(onProgress, { type: "generating", stashId, number: stashNumber });
|
|
1287
1241
|
const varyPrompt = `The user wants to vary the current UI. Apply this change: ${prompt}`;
|
|
1288
|
-
const aiProcess = startAiProcess(
|
|
1242
|
+
const aiProcess = startAiProcess({
|
|
1243
|
+
id: stashId,
|
|
1244
|
+
prompt: varyPrompt,
|
|
1245
|
+
cwd: worktree.path,
|
|
1246
|
+
bare: false
|
|
1247
|
+
});
|
|
1289
1248
|
try {
|
|
1290
1249
|
for await (const chunk of parseClaudeStream(aiProcess.process)) {
|
|
1291
1250
|
emit2(onProgress, {
|
|
@@ -1297,11 +1256,29 @@ async function vary(opts) {
|
|
|
1297
1256
|
}
|
|
1298
1257
|
await aiProcess.process.exited;
|
|
1299
1258
|
const wtGit = simpleGit4(worktree.path);
|
|
1259
|
+
let hasChanges = false;
|
|
1300
1260
|
try {
|
|
1301
1261
|
await wtGit.add("-A");
|
|
1302
|
-
await wtGit.
|
|
1303
|
-
|
|
1262
|
+
const status = await wtGit.status();
|
|
1263
|
+
if (status.staged.length > 0) {
|
|
1264
|
+
await wtGit.commit(`stashes: vary ${stashId} from ${sourceStashId}`);
|
|
1265
|
+
hasChanges = true;
|
|
1266
|
+
logger.info("vary", `committed changes for ${stashId}`, { files: status.staged.length });
|
|
1267
|
+
} else {
|
|
1268
|
+
logger.warn("vary", `AI produced no file changes for ${stashId}`);
|
|
1269
|
+
}
|
|
1270
|
+
} catch (commitErr) {
|
|
1271
|
+
logger.warn("vary", `commit failed for ${stashId}`, {
|
|
1272
|
+
error: commitErr instanceof Error ? commitErr.message : String(commitErr)
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1304
1275
|
await worktreeManager.removeGeneration(stashId);
|
|
1276
|
+
if (!hasChanges) {
|
|
1277
|
+
const errorStash = { ...stash, status: "error", error: "AI generation produced no file changes" };
|
|
1278
|
+
persistence.saveStash(errorStash);
|
|
1279
|
+
emit2(onProgress, { type: "error", stashId, error: "AI generation produced no file changes" });
|
|
1280
|
+
return errorStash;
|
|
1281
|
+
}
|
|
1305
1282
|
persistence.saveStash({ ...stash, status: "screenshotting" });
|
|
1306
1283
|
emit2(onProgress, { type: "screenshotting", stashId });
|
|
1307
1284
|
let screenshotPath = "";
|
|
@@ -1399,7 +1376,7 @@ async function cleanup(projectPath) {
|
|
|
1399
1376
|
logger.info("manage", "cleanup complete");
|
|
1400
1377
|
}
|
|
1401
1378
|
// ../server/dist/services/stash-service.js
|
|
1402
|
-
import { readFileSync as
|
|
1379
|
+
import { readFileSync as readFileSync3, existsSync as existsSync8 } from "fs";
|
|
1403
1380
|
import { join as join8 } from "path";
|
|
1404
1381
|
|
|
1405
1382
|
// ../server/dist/services/app-proxy.js
|
|
@@ -1883,6 +1860,7 @@ class StashService {
|
|
|
1883
1860
|
worktreeManager;
|
|
1884
1861
|
persistence;
|
|
1885
1862
|
broadcast;
|
|
1863
|
+
activityStore;
|
|
1886
1864
|
previewPool;
|
|
1887
1865
|
selectedComponent = null;
|
|
1888
1866
|
messageQueue = [];
|
|
@@ -1892,11 +1870,12 @@ class StashService {
|
|
|
1892
1870
|
stashPollTimer = null;
|
|
1893
1871
|
knownStashIds = new Set;
|
|
1894
1872
|
pendingComponentResolve = null;
|
|
1895
|
-
constructor(projectPath, worktreeManager, persistence, broadcast, stashPortStart, stashPortEnd) {
|
|
1873
|
+
constructor(projectPath, worktreeManager, persistence, broadcast, activityStore, stashPortStart, stashPortEnd) {
|
|
1896
1874
|
this.projectPath = projectPath;
|
|
1897
1875
|
this.worktreeManager = worktreeManager;
|
|
1898
1876
|
this.persistence = persistence;
|
|
1899
1877
|
this.broadcast = broadcast;
|
|
1878
|
+
this.activityStore = activityStore;
|
|
1900
1879
|
this.previewPool = new PreviewPool(worktreeManager, broadcast, undefined, undefined, stashPortStart, stashPortEnd);
|
|
1901
1880
|
}
|
|
1902
1881
|
getActiveChatId() {
|
|
@@ -1930,7 +1909,14 @@ class StashService {
|
|
|
1930
1909
|
"Reply with ONLY the file path relative to the project root."
|
|
1931
1910
|
].join(`
|
|
1932
1911
|
`);
|
|
1933
|
-
const aiProcess = startAiProcess(
|
|
1912
|
+
const aiProcess = startAiProcess({
|
|
1913
|
+
id: "resolve-component",
|
|
1914
|
+
prompt,
|
|
1915
|
+
cwd: this.projectPath,
|
|
1916
|
+
model: "claude-haiku-4-5-20251001",
|
|
1917
|
+
bare: true,
|
|
1918
|
+
tools: ["Read", "Grep", "Glob", "Bash"]
|
|
1919
|
+
});
|
|
1934
1920
|
let resolvedPath = "";
|
|
1935
1921
|
try {
|
|
1936
1922
|
for await (const chunk of parseClaudeStream(aiProcess.process)) {
|
|
@@ -1993,7 +1979,7 @@ class StashService {
|
|
|
1993
1979
|
if (filePath && filePath !== "auto-detect") {
|
|
1994
1980
|
const sourceFile = join8(this.projectPath, filePath);
|
|
1995
1981
|
if (existsSync8(sourceFile)) {
|
|
1996
|
-
sourceCode =
|
|
1982
|
+
sourceCode = readFileSync3(sourceFile, "utf-8");
|
|
1997
1983
|
}
|
|
1998
1984
|
}
|
|
1999
1985
|
let stashContext = "";
|
|
@@ -2040,7 +2026,13 @@ ${sourceCode.substring(0, 3000)}
|
|
|
2040
2026
|
].filter(Boolean).join(`
|
|
2041
2027
|
`);
|
|
2042
2028
|
}
|
|
2043
|
-
const aiProcess = startAiProcess(
|
|
2029
|
+
const aiProcess = startAiProcess({
|
|
2030
|
+
id: "chat",
|
|
2031
|
+
prompt: chatPrompt,
|
|
2032
|
+
cwd: this.projectPath,
|
|
2033
|
+
resumeSessionId: existingSessionId,
|
|
2034
|
+
bare: false
|
|
2035
|
+
});
|
|
2044
2036
|
let thinkingBuf = "";
|
|
2045
2037
|
let textBuf = "";
|
|
2046
2038
|
const now = new Date().toISOString();
|
|
@@ -2116,6 +2108,21 @@ ${sourceCode.substring(0, 3000)}
|
|
|
2116
2108
|
}
|
|
2117
2109
|
} else if (chunk.type === "tool_result") {
|
|
2118
2110
|
this.stopStashPoll();
|
|
2111
|
+
let stashActivity;
|
|
2112
|
+
const toolNameForSnapshot = chunk.toolName ?? "";
|
|
2113
|
+
if (toolNameForSnapshot.includes("stashes_generate") || toolNameForSnapshot.includes("stashes_vary")) {
|
|
2114
|
+
const projectId2 = this.persistence.listProjects()[0]?.id ?? "";
|
|
2115
|
+
const allStashes = this.persistence.listStashes(projectId2);
|
|
2116
|
+
stashActivity = {};
|
|
2117
|
+
for (const s of allStashes) {
|
|
2118
|
+
if (this.activityStore.has(s.id)) {
|
|
2119
|
+
stashActivity[s.id] = this.activityStore.getSnapshot(s.id);
|
|
2120
|
+
this.activityStore.clear(s.id);
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
if (Object.keys(stashActivity).length === 0)
|
|
2124
|
+
stashActivity = undefined;
|
|
2125
|
+
}
|
|
2119
2126
|
let toolResult = chunk.content;
|
|
2120
2127
|
let isError = false;
|
|
2121
2128
|
try {
|
|
@@ -2130,6 +2137,7 @@ ${sourceCode.substring(0, 3000)}
|
|
|
2130
2137
|
type: "tool_end",
|
|
2131
2138
|
toolStatus: isError ? "error" : "completed",
|
|
2132
2139
|
toolResult: toolResult.substring(0, 300),
|
|
2140
|
+
stashActivity,
|
|
2133
2141
|
createdAt: now
|
|
2134
2142
|
});
|
|
2135
2143
|
this.broadcast({
|
|
@@ -2319,10 +2327,77 @@ ${refDescriptions.join(`
|
|
|
2319
2327
|
}
|
|
2320
2328
|
}
|
|
2321
2329
|
|
|
2330
|
+
// ../server/dist/services/activity-store.js
|
|
2331
|
+
import { appendFileSync as appendFileSync2, readFileSync as readFileSync4, existsSync as existsSync9, mkdirSync as mkdirSync6, rmSync as rmSync3 } from "fs";
|
|
2332
|
+
import { join as join9, dirname as dirname2 } from "path";
|
|
2333
|
+
|
|
2334
|
+
class ActivityStore {
|
|
2335
|
+
cache = new Map;
|
|
2336
|
+
projectPath;
|
|
2337
|
+
constructor(projectPath) {
|
|
2338
|
+
this.projectPath = projectPath;
|
|
2339
|
+
}
|
|
2340
|
+
jsonlPath(stashId) {
|
|
2341
|
+
return join9(this.projectPath, ".stashes", "activity", `${stashId}.jsonl`);
|
|
2342
|
+
}
|
|
2343
|
+
append(event) {
|
|
2344
|
+
const existing = this.cache.get(event.stashId) ?? [];
|
|
2345
|
+
existing.push(event);
|
|
2346
|
+
this.cache.set(event.stashId, existing);
|
|
2347
|
+
const filePath = this.jsonlPath(event.stashId);
|
|
2348
|
+
const dir = dirname2(filePath);
|
|
2349
|
+
if (!existsSync9(dir))
|
|
2350
|
+
mkdirSync6(dir, { recursive: true });
|
|
2351
|
+
appendFileSync2(filePath, JSON.stringify(event) + `
|
|
2352
|
+
`, "utf-8");
|
|
2353
|
+
}
|
|
2354
|
+
getEvents(stashId) {
|
|
2355
|
+
const cached = this.cache.get(stashId);
|
|
2356
|
+
if (cached && cached.length > 0)
|
|
2357
|
+
return cached;
|
|
2358
|
+
const filePath = this.jsonlPath(stashId);
|
|
2359
|
+
if (!existsSync9(filePath))
|
|
2360
|
+
return [];
|
|
2361
|
+
const lines = readFileSync4(filePath, "utf-8").trim().split(`
|
|
2362
|
+
`).filter(Boolean);
|
|
2363
|
+
const events = lines.map((line) => JSON.parse(line));
|
|
2364
|
+
this.cache.set(stashId, events);
|
|
2365
|
+
return events;
|
|
2366
|
+
}
|
|
2367
|
+
getSnapshot(stashId) {
|
|
2368
|
+
const actions = this.getEvents(stashId);
|
|
2369
|
+
const fileActions = actions.filter((a) => ["Read", "Write", "Edit", "Glob", "Grep", "Bash"].includes(a.action));
|
|
2370
|
+
const uniqueFiles = new Set(fileActions.filter((a) => a.file).map((a) => a.file));
|
|
2371
|
+
const timestamps = actions.map((a) => a.timestamp);
|
|
2372
|
+
const duration = timestamps.length > 1 ? Math.round((Math.max(...timestamps) - Math.min(...timestamps)) / 1000) : 0;
|
|
2373
|
+
return {
|
|
2374
|
+
actions: [...actions],
|
|
2375
|
+
stats: {
|
|
2376
|
+
filesChanged: uniqueFiles.size,
|
|
2377
|
+
duration,
|
|
2378
|
+
totalActions: actions.length
|
|
2379
|
+
}
|
|
2380
|
+
};
|
|
2381
|
+
}
|
|
2382
|
+
clear(stashId) {
|
|
2383
|
+
this.cache.delete(stashId);
|
|
2384
|
+
const filePath = this.jsonlPath(stashId);
|
|
2385
|
+
if (existsSync9(filePath)) {
|
|
2386
|
+
rmSync3(filePath);
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
has(stashId) {
|
|
2390
|
+
if (this.cache.has(stashId) && (this.cache.get(stashId)?.length ?? 0) > 0)
|
|
2391
|
+
return true;
|
|
2392
|
+
return existsSync9(this.jsonlPath(stashId));
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2322
2396
|
// ../server/dist/services/websocket.js
|
|
2323
2397
|
var worktreeManager;
|
|
2324
2398
|
var stashService;
|
|
2325
2399
|
var persistence;
|
|
2400
|
+
var activityStore;
|
|
2326
2401
|
var clients = new Set;
|
|
2327
2402
|
function broadcast(event) {
|
|
2328
2403
|
const data = JSON.stringify(event);
|
|
@@ -2333,10 +2408,14 @@ function broadcast(event) {
|
|
|
2333
2408
|
function getPersistenceFromWs() {
|
|
2334
2409
|
return persistence;
|
|
2335
2410
|
}
|
|
2411
|
+
function getActivityStoreFromWs() {
|
|
2412
|
+
return activityStore;
|
|
2413
|
+
}
|
|
2336
2414
|
function createWebSocketHandler(projectPath, userDevPort, appProxyPort, stashPortStart, stashPortEnd) {
|
|
2337
2415
|
worktreeManager = new WorktreeManager(projectPath);
|
|
2338
2416
|
persistence = new PersistenceService(projectPath);
|
|
2339
|
-
|
|
2417
|
+
activityStore = new ActivityStore(projectPath);
|
|
2418
|
+
stashService = new StashService(projectPath, worktreeManager, persistence, broadcast, activityStore, stashPortStart, stashPortEnd);
|
|
2340
2419
|
return {
|
|
2341
2420
|
open(ws) {
|
|
2342
2421
|
clients.add(ws);
|
|
@@ -2353,6 +2432,15 @@ function createWebSocketHandler(projectPath, userDevPort, appProxyPort, stashPor
|
|
|
2353
2432
|
if (activeChatId) {
|
|
2354
2433
|
ws.send(JSON.stringify({ type: "processing", chatId: activeChatId }));
|
|
2355
2434
|
}
|
|
2435
|
+
const allStashes = persistence.listStashes(project.id);
|
|
2436
|
+
for (const stash of allStashes) {
|
|
2437
|
+
if (stash.status === "generating" && activityStore.has(stash.id)) {
|
|
2438
|
+
const events = activityStore.getEvents(stash.id);
|
|
2439
|
+
for (const event of events) {
|
|
2440
|
+
ws.send(JSON.stringify({ type: "stash:activity", stashId: stash.id, event }));
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2356
2444
|
},
|
|
2357
2445
|
async message(ws, message) {
|
|
2358
2446
|
const raw = typeof message === "string" ? message : new TextDecoder().decode(message);
|
|
@@ -2432,6 +2520,156 @@ function createWebSocketHandler(projectPath, userDevPort, appProxyPort, stashPor
|
|
|
2432
2520
|
};
|
|
2433
2521
|
}
|
|
2434
2522
|
|
|
2523
|
+
// ../server/dist/routes/api.js
|
|
2524
|
+
var app = new Hono;
|
|
2525
|
+
app.get("/health", (c) => c.json({ status: "ok", service: "stashes" }));
|
|
2526
|
+
app.get("/projects", (c) => {
|
|
2527
|
+
const persistence2 = getPersistence();
|
|
2528
|
+
const projects = persistence2.listProjects();
|
|
2529
|
+
const projectsWithCounts = projects.map((p) => ({
|
|
2530
|
+
...p,
|
|
2531
|
+
stashCount: persistence2.listStashes(p.id).length,
|
|
2532
|
+
recentScreenshots: persistence2.listStashes(p.id).filter((s) => s.screenshotUrl).slice(-4).map((s) => s.screenshotUrl)
|
|
2533
|
+
}));
|
|
2534
|
+
return c.json({ data: projectsWithCounts });
|
|
2535
|
+
});
|
|
2536
|
+
app.post("/projects", async (c) => {
|
|
2537
|
+
const { name, description } = await c.req.json();
|
|
2538
|
+
const project = {
|
|
2539
|
+
id: `proj_${crypto.randomUUID().substring(0, 8)}`,
|
|
2540
|
+
name,
|
|
2541
|
+
description,
|
|
2542
|
+
createdAt: new Date().toISOString(),
|
|
2543
|
+
updatedAt: new Date().toISOString()
|
|
2544
|
+
};
|
|
2545
|
+
getPersistence().saveProject(project);
|
|
2546
|
+
return c.json({ data: project }, 201);
|
|
2547
|
+
});
|
|
2548
|
+
app.get("/projects/:id", (c) => {
|
|
2549
|
+
const persistence2 = getPersistence();
|
|
2550
|
+
const project = persistence2.getProject(c.req.param("id"));
|
|
2551
|
+
if (!project)
|
|
2552
|
+
return c.json({ error: "Project not found" }, 404);
|
|
2553
|
+
const stashes = persistence2.listStashes(project.id);
|
|
2554
|
+
const chats = persistence2.listChats(project.id);
|
|
2555
|
+
return c.json({ data: { ...project, stashes, chats } });
|
|
2556
|
+
});
|
|
2557
|
+
app.delete("/projects/:id", (c) => {
|
|
2558
|
+
const id = c.req.param("id");
|
|
2559
|
+
getPersistence().deleteProject(id);
|
|
2560
|
+
return c.json({ data: { deleted: id } });
|
|
2561
|
+
});
|
|
2562
|
+
app.get("/chats", (c) => {
|
|
2563
|
+
const persistence2 = getPersistence();
|
|
2564
|
+
const project = ensureProject(persistence2);
|
|
2565
|
+
const chats = persistence2.listChats(project.id);
|
|
2566
|
+
const stashes = persistence2.listStashes(project.id);
|
|
2567
|
+
return c.json({ data: { project, chats, stashes } });
|
|
2568
|
+
});
|
|
2569
|
+
app.post("/chats", async (c) => {
|
|
2570
|
+
const persistence2 = getPersistence();
|
|
2571
|
+
const project = ensureProject(persistence2);
|
|
2572
|
+
const { title, referencedStashIds } = await c.req.json();
|
|
2573
|
+
const chatCount = persistence2.listChats(project.id).length;
|
|
2574
|
+
const chat = {
|
|
2575
|
+
id: `chat_${crypto.randomUUID().substring(0, 8)}`,
|
|
2576
|
+
projectId: project.id,
|
|
2577
|
+
title: title?.trim() || `Chat ${chatCount + 1}`,
|
|
2578
|
+
referencedStashIds: referencedStashIds ?? [],
|
|
2579
|
+
createdAt: new Date().toISOString(),
|
|
2580
|
+
updatedAt: new Date().toISOString()
|
|
2581
|
+
};
|
|
2582
|
+
persistence2.saveChat(chat);
|
|
2583
|
+
return c.json({ data: chat }, 201);
|
|
2584
|
+
});
|
|
2585
|
+
app.patch("/chats/:chatId", async (c) => {
|
|
2586
|
+
const persistence2 = getPersistence();
|
|
2587
|
+
const project = ensureProject(persistence2);
|
|
2588
|
+
const chatId = c.req.param("chatId");
|
|
2589
|
+
const chat = persistence2.getChat(project.id, chatId);
|
|
2590
|
+
if (!chat)
|
|
2591
|
+
return c.json({ error: "Chat not found" }, 404);
|
|
2592
|
+
const body = await c.req.json();
|
|
2593
|
+
const updated = {
|
|
2594
|
+
...chat,
|
|
2595
|
+
...body.referencedStashIds !== undefined ? { referencedStashIds: body.referencedStashIds } : {},
|
|
2596
|
+
updatedAt: new Date().toISOString()
|
|
2597
|
+
};
|
|
2598
|
+
persistence2.saveChat(updated);
|
|
2599
|
+
return c.json({ data: updated });
|
|
2600
|
+
});
|
|
2601
|
+
app.get("/chats/:chatId", (c) => {
|
|
2602
|
+
const persistence2 = getPersistence();
|
|
2603
|
+
const project = ensureProject(persistence2);
|
|
2604
|
+
const chatId = c.req.param("chatId");
|
|
2605
|
+
const chat = persistence2.getChat(project.id, chatId);
|
|
2606
|
+
if (!chat)
|
|
2607
|
+
return c.json({ error: "Chat not found" }, 404);
|
|
2608
|
+
const messages = persistence2.getChatMessages(project.id, chatId);
|
|
2609
|
+
const refIds = new Set(chat.referencedStashIds ?? []);
|
|
2610
|
+
const stashes = persistence2.listStashes(project.id).filter((s) => s.originChatId === chatId || refIds.has(s.id));
|
|
2611
|
+
return c.json({ data: { ...chat, messages, stashes } });
|
|
2612
|
+
});
|
|
2613
|
+
app.delete("/chats/:chatId", (c) => {
|
|
2614
|
+
const persistence2 = getPersistence();
|
|
2615
|
+
const project = ensureProject(persistence2);
|
|
2616
|
+
const chatId = c.req.param("chatId");
|
|
2617
|
+
persistence2.deleteChat(project.id, chatId);
|
|
2618
|
+
return c.json({ data: { deleted: chatId } });
|
|
2619
|
+
});
|
|
2620
|
+
app.get("/dev-server-status", async (c) => {
|
|
2621
|
+
const port = serverState.userDevPort;
|
|
2622
|
+
try {
|
|
2623
|
+
const res = await fetch(`http://localhost:${port}`, {
|
|
2624
|
+
method: "HEAD",
|
|
2625
|
+
signal: AbortSignal.timeout(2000)
|
|
2626
|
+
});
|
|
2627
|
+
return c.json({ up: res.status < 500, port });
|
|
2628
|
+
} catch {
|
|
2629
|
+
return c.json({ up: false, port });
|
|
2630
|
+
}
|
|
2631
|
+
});
|
|
2632
|
+
app.get("/screenshots/:filename", (c) => {
|
|
2633
|
+
const filename = c.req.param("filename");
|
|
2634
|
+
const filePath = join10(serverState.projectPath, ".stashes", "screenshots", filename);
|
|
2635
|
+
if (!existsSync10(filePath))
|
|
2636
|
+
return c.json({ error: "Not found" }, 404);
|
|
2637
|
+
const content = readFileSync5(filePath);
|
|
2638
|
+
return new Response(content, {
|
|
2639
|
+
headers: { "content-type": "image/png", "cache-control": "no-cache" }
|
|
2640
|
+
});
|
|
2641
|
+
});
|
|
2642
|
+
app.post("/stash-activity", async (c) => {
|
|
2643
|
+
const events = await c.req.json();
|
|
2644
|
+
const store = getActivityStoreFromWs();
|
|
2645
|
+
for (const event of events) {
|
|
2646
|
+
store.append(event);
|
|
2647
|
+
broadcast({ type: "stash:activity", stashId: event.stashId, event });
|
|
2648
|
+
}
|
|
2649
|
+
return c.json({ ok: true });
|
|
2650
|
+
});
|
|
2651
|
+
app.get("/stash-activity/:stashId", (c) => {
|
|
2652
|
+
const stashId = c.req.param("stashId");
|
|
2653
|
+
const store = getActivityStoreFromWs();
|
|
2654
|
+
const events = store.getEvents(stashId);
|
|
2655
|
+
return c.json({ data: events });
|
|
2656
|
+
});
|
|
2657
|
+
function ensureProject(persistence2) {
|
|
2658
|
+
const projects = persistence2.listProjects();
|
|
2659
|
+
if (projects.length > 0)
|
|
2660
|
+
return projects[0];
|
|
2661
|
+
const project = {
|
|
2662
|
+
id: `proj_${crypto.randomUUID().substring(0, 8)}`,
|
|
2663
|
+
name: basename(serverState.projectPath),
|
|
2664
|
+
createdAt: new Date().toISOString(),
|
|
2665
|
+
updatedAt: new Date().toISOString()
|
|
2666
|
+
};
|
|
2667
|
+
persistence2.saveProject(project);
|
|
2668
|
+
persistence2.migrateOldChat(project.id);
|
|
2669
|
+
return project;
|
|
2670
|
+
}
|
|
2671
|
+
var apiRoutes = app;
|
|
2672
|
+
|
|
2435
2673
|
// ../server/dist/index.js
|
|
2436
2674
|
var serverState = {
|
|
2437
2675
|
projectPath: "",
|
|
@@ -2445,14 +2683,14 @@ app2.use("/*", cors());
|
|
|
2445
2683
|
app2.route("/api", apiRoutes);
|
|
2446
2684
|
app2.get("/*", async (c) => {
|
|
2447
2685
|
const path = c.req.path;
|
|
2448
|
-
const selfDir =
|
|
2449
|
-
const bundledWebDir =
|
|
2450
|
-
const monorepoWebDir =
|
|
2451
|
-
const webDistDir =
|
|
2686
|
+
const selfDir = dirname3(fileURLToPath(import.meta.url));
|
|
2687
|
+
const bundledWebDir = join11(selfDir, "web");
|
|
2688
|
+
const monorepoWebDir = join11(selfDir, "../../web/dist");
|
|
2689
|
+
const webDistDir = existsSync11(join11(bundledWebDir, "index.html")) ? bundledWebDir : monorepoWebDir;
|
|
2452
2690
|
const requestPath = path === "/" ? "/index.html" : path;
|
|
2453
|
-
const filePath =
|
|
2454
|
-
if (
|
|
2455
|
-
const content =
|
|
2691
|
+
const filePath = join11(webDistDir, requestPath);
|
|
2692
|
+
if (existsSync11(filePath) && !filePath.includes("..")) {
|
|
2693
|
+
const content = readFileSync6(filePath);
|
|
2456
2694
|
const ext = filePath.split(".").pop() || "";
|
|
2457
2695
|
const contentTypes = {
|
|
2458
2696
|
html: "text/html; charset=utf-8",
|
|
@@ -2470,9 +2708,9 @@ app2.get("/*", async (c) => {
|
|
|
2470
2708
|
headers: { "content-type": contentTypes[ext] || "application/octet-stream" }
|
|
2471
2709
|
});
|
|
2472
2710
|
}
|
|
2473
|
-
const indexPath =
|
|
2474
|
-
if (
|
|
2475
|
-
const html =
|
|
2711
|
+
const indexPath = join11(webDistDir, "index.html");
|
|
2712
|
+
if (existsSync11(indexPath)) {
|
|
2713
|
+
const html = readFileSync6(indexPath, "utf-8");
|
|
2476
2714
|
return new Response(html, {
|
|
2477
2715
|
headers: { "content-type": "text/html; charset=utf-8" }
|
|
2478
2716
|
});
|
|
@@ -2532,11 +2770,11 @@ async function startServer(projectPath, userDevPort, requestedPort = STASHES_POR
|
|
|
2532
2770
|
}
|
|
2533
2771
|
|
|
2534
2772
|
// ../server/dist/services/detector.js
|
|
2535
|
-
import { existsSync as
|
|
2536
|
-
import { join as
|
|
2773
|
+
import { existsSync as existsSync12, readFileSync as readFileSync7 } from "fs";
|
|
2774
|
+
import { join as join12 } from "path";
|
|
2537
2775
|
function detectFramework(projectPath) {
|
|
2538
|
-
const packageJsonPath =
|
|
2539
|
-
if (!
|
|
2776
|
+
const packageJsonPath = join12(projectPath, "package.json");
|
|
2777
|
+
if (!existsSync12(packageJsonPath)) {
|
|
2540
2778
|
return {
|
|
2541
2779
|
framework: "unknown",
|
|
2542
2780
|
devCommand: "npm run dev",
|
|
@@ -2544,7 +2782,7 @@ function detectFramework(projectPath) {
|
|
|
2544
2782
|
configFile: null
|
|
2545
2783
|
};
|
|
2546
2784
|
}
|
|
2547
|
-
const packageJson = JSON.parse(
|
|
2785
|
+
const packageJson = JSON.parse(readFileSync7(packageJsonPath, "utf-8"));
|
|
2548
2786
|
const deps = {
|
|
2549
2787
|
...packageJson.dependencies,
|
|
2550
2788
|
...packageJson.devDependencies
|
|
@@ -2598,7 +2836,7 @@ function getDevCommand(packageJson, fallback) {
|
|
|
2598
2836
|
}
|
|
2599
2837
|
function findConfig(projectPath, candidates) {
|
|
2600
2838
|
for (const candidate of candidates) {
|
|
2601
|
-
if (
|
|
2839
|
+
if (existsSync12(join12(projectPath, candidate))) {
|
|
2602
2840
|
return candidate;
|
|
2603
2841
|
}
|
|
2604
2842
|
}
|
|
@@ -2790,8 +3028,8 @@ Cleaning up all stashes and worktrees...`);
|
|
|
2790
3028
|
}
|
|
2791
3029
|
|
|
2792
3030
|
// src/commands/setup.ts
|
|
2793
|
-
import { existsSync as
|
|
2794
|
-
import { dirname as
|
|
3031
|
+
import { existsSync as existsSync13, readFileSync as readFileSync8, writeFileSync as writeFileSync3, mkdirSync as mkdirSync7 } from "fs";
|
|
3032
|
+
import { dirname as dirname4, join as join13 } from "path";
|
|
2795
3033
|
import { homedir } from "os";
|
|
2796
3034
|
import * as p from "@clack/prompts";
|
|
2797
3035
|
import pc from "picocolors";
|
|
@@ -2810,63 +3048,63 @@ var MCP_ENTRY_ZED = {
|
|
|
2810
3048
|
};
|
|
2811
3049
|
function buildToolDefinitions() {
|
|
2812
3050
|
const home = homedir();
|
|
2813
|
-
const appSupport =
|
|
3051
|
+
const appSupport = join13(home, "Library", "Application Support");
|
|
2814
3052
|
return [
|
|
2815
3053
|
{
|
|
2816
3054
|
id: "claude-code",
|
|
2817
3055
|
name: "Claude Code",
|
|
2818
|
-
configPath:
|
|
3056
|
+
configPath: join13(home, ".claude.json"),
|
|
2819
3057
|
serversKey: "mcpServers",
|
|
2820
3058
|
format: "standard",
|
|
2821
|
-
detect: () =>
|
|
3059
|
+
detect: () => existsSync13(join13(home, ".claude.json")) || existsSync13(join13(home, ".claude"))
|
|
2822
3060
|
},
|
|
2823
3061
|
{
|
|
2824
3062
|
id: "claude-desktop",
|
|
2825
3063
|
name: "Claude Desktop",
|
|
2826
|
-
configPath:
|
|
3064
|
+
configPath: join13(appSupport, "Claude", "claude_desktop_config.json"),
|
|
2827
3065
|
serversKey: "mcpServers",
|
|
2828
3066
|
format: "standard",
|
|
2829
|
-
detect: () =>
|
|
3067
|
+
detect: () => existsSync13(join13(appSupport, "Claude")) || existsSync13("/Applications/Claude.app")
|
|
2830
3068
|
},
|
|
2831
3069
|
{
|
|
2832
3070
|
id: "vscode",
|
|
2833
3071
|
name: "VS Code",
|
|
2834
|
-
configPath:
|
|
3072
|
+
configPath: join13(appSupport, "Code", "User", "mcp.json"),
|
|
2835
3073
|
serversKey: "servers",
|
|
2836
3074
|
format: "standard",
|
|
2837
|
-
detect: () =>
|
|
3075
|
+
detect: () => existsSync13(join13(appSupport, "Code", "User"))
|
|
2838
3076
|
},
|
|
2839
3077
|
{
|
|
2840
3078
|
id: "cursor",
|
|
2841
3079
|
name: "Cursor",
|
|
2842
|
-
configPath:
|
|
3080
|
+
configPath: join13(home, ".cursor", "mcp.json"),
|
|
2843
3081
|
serversKey: "mcpServers",
|
|
2844
3082
|
format: "standard",
|
|
2845
|
-
detect: () =>
|
|
3083
|
+
detect: () => existsSync13(join13(home, ".cursor"))
|
|
2846
3084
|
},
|
|
2847
3085
|
{
|
|
2848
3086
|
id: "windsurf",
|
|
2849
3087
|
name: "Windsurf",
|
|
2850
|
-
configPath:
|
|
3088
|
+
configPath: join13(home, ".codeium", "windsurf", "mcp_config.json"),
|
|
2851
3089
|
serversKey: "mcpServers",
|
|
2852
3090
|
format: "standard",
|
|
2853
|
-
detect: () =>
|
|
3091
|
+
detect: () => existsSync13(join13(home, ".codeium", "windsurf"))
|
|
2854
3092
|
},
|
|
2855
3093
|
{
|
|
2856
3094
|
id: "zed",
|
|
2857
3095
|
name: "Zed",
|
|
2858
|
-
configPath:
|
|
3096
|
+
configPath: join13(appSupport, "Zed", "settings.json"),
|
|
2859
3097
|
serversKey: "context_servers",
|
|
2860
3098
|
format: "zed",
|
|
2861
|
-
detect: () =>
|
|
3099
|
+
detect: () => existsSync13(join13(appSupport, "Zed"))
|
|
2862
3100
|
}
|
|
2863
3101
|
];
|
|
2864
3102
|
}
|
|
2865
3103
|
function readJsonFile(path) {
|
|
2866
|
-
if (!
|
|
3104
|
+
if (!existsSync13(path))
|
|
2867
3105
|
return {};
|
|
2868
3106
|
try {
|
|
2869
|
-
const raw =
|
|
3107
|
+
const raw = readFileSync8(path, "utf-8").trim();
|
|
2870
3108
|
if (!raw)
|
|
2871
3109
|
return {};
|
|
2872
3110
|
return JSON.parse(raw);
|
|
@@ -2875,8 +3113,8 @@ function readJsonFile(path) {
|
|
|
2875
3113
|
}
|
|
2876
3114
|
}
|
|
2877
3115
|
function writeJsonFile(path, data) {
|
|
2878
|
-
|
|
2879
|
-
|
|
3116
|
+
mkdirSync7(dirname4(path), { recursive: true });
|
|
3117
|
+
writeFileSync3(path, JSON.stringify(data, null, 2) + `
|
|
2880
3118
|
`);
|
|
2881
3119
|
}
|
|
2882
3120
|
function isConfigured(tool) {
|
|
@@ -3014,16 +3252,16 @@ async function setupCommand(options) {
|
|
|
3014
3252
|
|
|
3015
3253
|
// src/commands/update.ts
|
|
3016
3254
|
import { execFileSync, execSync } from "child_process";
|
|
3017
|
-
import { writeFileSync as
|
|
3018
|
-
import { tmpdir } from "os";
|
|
3019
|
-
import { join as
|
|
3255
|
+
import { writeFileSync as writeFileSync4, unlinkSync, chmodSync, readFileSync as readFileSync9 } from "fs";
|
|
3256
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
3257
|
+
import { join as join14, dirname as dirname5 } from "path";
|
|
3020
3258
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3021
3259
|
import * as p2 from "@clack/prompts";
|
|
3022
3260
|
import pc2 from "picocolors";
|
|
3023
3261
|
function getCurrentVersion() {
|
|
3024
|
-
const selfDir =
|
|
3025
|
-
const pkgPath =
|
|
3026
|
-
return JSON.parse(
|
|
3262
|
+
const selfDir = dirname5(fileURLToPath2(import.meta.url));
|
|
3263
|
+
const pkgPath = join14(selfDir, "..", "package.json");
|
|
3264
|
+
return JSON.parse(readFileSync9(pkgPath, "utf-8")).version;
|
|
3027
3265
|
}
|
|
3028
3266
|
function fetchLatestVersion() {
|
|
3029
3267
|
try {
|
|
@@ -3099,8 +3337,8 @@ async function updateCommand() {
|
|
|
3099
3337
|
}
|
|
3100
3338
|
s.stop(`Removed from ${configuredTools.length} tool${configuredTools.length === 1 ? "" : "s"}`);
|
|
3101
3339
|
}
|
|
3102
|
-
const scriptPath =
|
|
3103
|
-
|
|
3340
|
+
const scriptPath = join14(tmpdir2(), `stashes-update-${Date.now()}.sh`);
|
|
3341
|
+
writeFileSync4(scriptPath, buildUpdateScript(), "utf-8");
|
|
3104
3342
|
chmodSync(scriptPath, 493);
|
|
3105
3343
|
try {
|
|
3106
3344
|
execFileSync("bash", [scriptPath], { stdio: "inherit" });
|
|
@@ -3117,9 +3355,9 @@ Update failed. Try manually:`);
|
|
|
3117
3355
|
}
|
|
3118
3356
|
|
|
3119
3357
|
// src/index.ts
|
|
3120
|
-
var selfDir =
|
|
3121
|
-
var pkgPath =
|
|
3122
|
-
var version = JSON.parse(
|
|
3358
|
+
var selfDir = dirname6(fileURLToPath3(import.meta.url));
|
|
3359
|
+
var pkgPath = join15(selfDir, "..", "package.json");
|
|
3360
|
+
var version = JSON.parse(readFileSync10(pkgPath, "utf-8")).version;
|
|
3123
3361
|
var program = new Command;
|
|
3124
3362
|
program.name("stashes").description("Generate AI-powered UI design explorations in your project").version(version, "-v, --version");
|
|
3125
3363
|
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);
|