stashes 0.1.7 → 0.1.9
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 +395 -163
- package/dist/mcp.js +395 -163
- package/dist/web/assets/index-8mMmnYX5.js +62 -0
- package/dist/web/assets/index-BEubPa-l.css +1 -0
- package/dist/web/assets/index-BMBumaom.js +62 -0
- package/dist/web/assets/index-BzwYXF3-.js +62 -0
- package/dist/web/assets/index-DBsH8rVY.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
package/dist/mcp.js
CHANGED
|
@@ -33,7 +33,12 @@ import simpleGit2 from "simple-git";
|
|
|
33
33
|
// ../shared/dist/constants/index.js
|
|
34
34
|
var STASHES_PORT = 4000;
|
|
35
35
|
var DEFAULT_STASH_COUNT = 3;
|
|
36
|
+
var APP_PROXY_PORT = STASHES_PORT + 1;
|
|
36
37
|
var STASH_PORT_START = 4010;
|
|
38
|
+
var STASH_PORT_END = 4030;
|
|
39
|
+
var MAX_PREVIEW_SERVERS = 5;
|
|
40
|
+
var PREVIEW_TTL_MS = 300000;
|
|
41
|
+
var PREVIEW_REAPER_INTERVAL = 30000;
|
|
37
42
|
var DEFAULT_DIRECTIVES = [
|
|
38
43
|
"Minimal and clean \u2014 reduce visual noise, emphasize whitespace, limit to 2-3 colors, typography-driven hierarchy",
|
|
39
44
|
"Bold and expressive \u2014 strong visual identity, use motion/animation, rich interactions, distinctive layout",
|
|
@@ -231,6 +236,34 @@ class WorktreeManager {
|
|
|
231
236
|
getPreviewPort() {
|
|
232
237
|
return PREVIEW_PORT;
|
|
233
238
|
}
|
|
239
|
+
async createPreviewForPool(stashId) {
|
|
240
|
+
const previewPath = join2(this.projectPath, ".stashes", "previews", stashId);
|
|
241
|
+
const branch = `stashes/${stashId}`;
|
|
242
|
+
if (existsSync2(previewPath)) {
|
|
243
|
+
try {
|
|
244
|
+
await this.git.raw(["worktree", "remove", "--force", previewPath]);
|
|
245
|
+
} catch {
|
|
246
|
+
rmSync(previewPath, { recursive: true, force: true });
|
|
247
|
+
}
|
|
248
|
+
await this.git.raw(["worktree", "prune"]);
|
|
249
|
+
}
|
|
250
|
+
logger.info("worktree", `creating pool preview: ${stashId}`, { branch, path: previewPath });
|
|
251
|
+
await this.git.raw(["worktree", "add", previewPath, branch]);
|
|
252
|
+
this.symlinkDeps(previewPath);
|
|
253
|
+
logger.info("worktree", `pool preview created: ${stashId}`);
|
|
254
|
+
return previewPath;
|
|
255
|
+
}
|
|
256
|
+
async removePreviewForPool(stashId) {
|
|
257
|
+
const previewPath = join2(this.projectPath, ".stashes", "previews", stashId);
|
|
258
|
+
logger.info("worktree", `removing pool preview: ${stashId}`);
|
|
259
|
+
try {
|
|
260
|
+
await this.git.raw(["worktree", "remove", "--force", previewPath]);
|
|
261
|
+
} catch {
|
|
262
|
+
if (existsSync2(previewPath)) {
|
|
263
|
+
rmSync(previewPath, { recursive: true, force: true });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
234
267
|
async apply(stashId) {
|
|
235
268
|
const branch = `stashes/${stashId}`;
|
|
236
269
|
logger.info("worktree", `merging: ${stashId}`, { branch });
|
|
@@ -271,6 +304,10 @@ class WorktreeManager {
|
|
|
271
304
|
if (existsSync2(previewDir)) {
|
|
272
305
|
rmSync(previewDir, { recursive: true, force: true });
|
|
273
306
|
}
|
|
307
|
+
const previewsDir = join2(this.projectPath, ".stashes", "previews");
|
|
308
|
+
if (existsSync2(previewsDir)) {
|
|
309
|
+
rmSync(previewsDir, { recursive: true, force: true });
|
|
310
|
+
}
|
|
274
311
|
logger.info("worktree", `cleanup complete`);
|
|
275
312
|
}
|
|
276
313
|
symlinkDeps(worktreePath) {
|
|
@@ -346,6 +383,9 @@ class PersistenceService {
|
|
|
346
383
|
const filePath = join3(this.basePath, "projects", projectId, "stashes.json");
|
|
347
384
|
return readJson(filePath, []);
|
|
348
385
|
}
|
|
386
|
+
getStash(projectId, stashId) {
|
|
387
|
+
return this.listStashes(projectId).find((s) => s.id === stashId);
|
|
388
|
+
}
|
|
349
389
|
saveStash(stash) {
|
|
350
390
|
const stashes = [...this.listStashes(stash.projectId)];
|
|
351
391
|
const index = stashes.findIndex((s) => s.id === stash.id);
|
|
@@ -1045,19 +1085,201 @@ var apiRoutes = app;
|
|
|
1045
1085
|
// ../server/dist/services/stash-service.js
|
|
1046
1086
|
import { readFileSync as readFileSync4, existsSync as existsSync7 } from "fs";
|
|
1047
1087
|
import { join as join7 } from "path";
|
|
1088
|
+
|
|
1089
|
+
// ../server/dist/services/preview-pool.js
|
|
1090
|
+
class PreviewPool {
|
|
1091
|
+
entries = new Map;
|
|
1092
|
+
usedPorts = new Set;
|
|
1093
|
+
maxSize;
|
|
1094
|
+
ttlMs;
|
|
1095
|
+
worktreeManager;
|
|
1096
|
+
broadcast;
|
|
1097
|
+
reaperInterval;
|
|
1098
|
+
constructor(worktreeManager, broadcast, maxSize = MAX_PREVIEW_SERVERS, ttlMs = PREVIEW_TTL_MS) {
|
|
1099
|
+
this.worktreeManager = worktreeManager;
|
|
1100
|
+
this.broadcast = broadcast;
|
|
1101
|
+
this.maxSize = maxSize;
|
|
1102
|
+
this.ttlMs = ttlMs;
|
|
1103
|
+
this.reaperInterval = setInterval(() => this.reap(), PREVIEW_REAPER_INTERVAL);
|
|
1104
|
+
}
|
|
1105
|
+
async getOrStart(stashId) {
|
|
1106
|
+
const existing = this.entries.get(stashId);
|
|
1107
|
+
if (existing) {
|
|
1108
|
+
existing.lastHeartbeat = Date.now();
|
|
1109
|
+
logger.info("pool", `warm hit: ${stashId} on port ${existing.port}`);
|
|
1110
|
+
return existing.port;
|
|
1111
|
+
}
|
|
1112
|
+
if (this.entries.size >= this.maxSize) {
|
|
1113
|
+
this.evictOldest();
|
|
1114
|
+
}
|
|
1115
|
+
const port = this.allocatePort();
|
|
1116
|
+
const worktreePath = await this.worktreeManager.createPreviewForPool(stashId);
|
|
1117
|
+
const process2 = Bun.spawn({
|
|
1118
|
+
cmd: ["npm", "run", "dev"],
|
|
1119
|
+
cwd: worktreePath,
|
|
1120
|
+
stdin: "ignore",
|
|
1121
|
+
stdout: "pipe",
|
|
1122
|
+
stderr: "pipe",
|
|
1123
|
+
env: { ...Bun.env, PORT: String(port), BROWSER: "none" }
|
|
1124
|
+
});
|
|
1125
|
+
const entry = {
|
|
1126
|
+
stashId,
|
|
1127
|
+
port,
|
|
1128
|
+
process: process2,
|
|
1129
|
+
worktreePath,
|
|
1130
|
+
lastHeartbeat: Date.now()
|
|
1131
|
+
};
|
|
1132
|
+
this.entries.set(stashId, entry);
|
|
1133
|
+
this.usedPorts.add(port);
|
|
1134
|
+
logger.info("pool", `cold start: ${stashId} on port ${port}`, { poolSize: this.entries.size });
|
|
1135
|
+
await this.waitForPort(port, 60000);
|
|
1136
|
+
return port;
|
|
1137
|
+
}
|
|
1138
|
+
heartbeat(stashId) {
|
|
1139
|
+
const entry = this.entries.get(stashId);
|
|
1140
|
+
if (entry) {
|
|
1141
|
+
entry.lastHeartbeat = Date.now();
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
isWarm(stashId) {
|
|
1145
|
+
return this.entries.has(stashId);
|
|
1146
|
+
}
|
|
1147
|
+
getPort(stashId) {
|
|
1148
|
+
return this.entries.get(stashId)?.port ?? null;
|
|
1149
|
+
}
|
|
1150
|
+
async stop(stashId) {
|
|
1151
|
+
const entry = this.entries.get(stashId);
|
|
1152
|
+
if (!entry)
|
|
1153
|
+
return;
|
|
1154
|
+
logger.info("pool", `stopping: ${stashId} on port ${entry.port}`);
|
|
1155
|
+
this.killEntry(entry);
|
|
1156
|
+
this.entries.delete(stashId);
|
|
1157
|
+
this.usedPorts.delete(entry.port);
|
|
1158
|
+
try {
|
|
1159
|
+
await this.worktreeManager.removePreviewForPool(stashId);
|
|
1160
|
+
} catch (err) {
|
|
1161
|
+
logger.warn("pool", `worktree removal failed for ${stashId}`, {
|
|
1162
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
prefetchNeighbors(currentStashId, sortedStashIds) {
|
|
1167
|
+
const currentIndex = sortedStashIds.indexOf(currentStashId);
|
|
1168
|
+
if (currentIndex === -1 || sortedStashIds.length < 2)
|
|
1169
|
+
return;
|
|
1170
|
+
const neighbors = [];
|
|
1171
|
+
const prevIndex = (currentIndex - 1 + sortedStashIds.length) % sortedStashIds.length;
|
|
1172
|
+
const nextIndex = (currentIndex + 1) % sortedStashIds.length;
|
|
1173
|
+
if (!this.entries.has(sortedStashIds[prevIndex])) {
|
|
1174
|
+
neighbors.push(sortedStashIds[prevIndex]);
|
|
1175
|
+
}
|
|
1176
|
+
if (!this.entries.has(sortedStashIds[nextIndex])) {
|
|
1177
|
+
neighbors.push(sortedStashIds[nextIndex]);
|
|
1178
|
+
}
|
|
1179
|
+
for (const stashId of neighbors) {
|
|
1180
|
+
if (this.entries.size >= this.maxSize)
|
|
1181
|
+
break;
|
|
1182
|
+
logger.info("pool", `prefetching neighbor: ${stashId}`);
|
|
1183
|
+
this.getOrStart(stashId).then((port) => {
|
|
1184
|
+
this.broadcast({ type: "stash:port", stashId, port });
|
|
1185
|
+
}).catch((err) => {
|
|
1186
|
+
logger.warn("pool", `prefetch failed for ${stashId}`, {
|
|
1187
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1188
|
+
});
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
async shutdown() {
|
|
1193
|
+
logger.info("pool", `shutting down all`, { poolSize: this.entries.size });
|
|
1194
|
+
clearInterval(this.reaperInterval);
|
|
1195
|
+
const stashIds = [...this.entries.keys()];
|
|
1196
|
+
for (const stashId of stashIds) {
|
|
1197
|
+
await this.stop(stashId);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
reap() {
|
|
1201
|
+
const now = Date.now();
|
|
1202
|
+
const expired = [];
|
|
1203
|
+
for (const [stashId, entry] of this.entries) {
|
|
1204
|
+
if (now - entry.lastHeartbeat > this.ttlMs) {
|
|
1205
|
+
expired.push(stashId);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
for (const stashId of expired) {
|
|
1209
|
+
logger.info("pool", `reaping inactive: ${stashId}`);
|
|
1210
|
+
const entry = this.entries.get(stashId);
|
|
1211
|
+
this.killEntry(entry);
|
|
1212
|
+
this.entries.delete(stashId);
|
|
1213
|
+
this.usedPorts.delete(entry.port);
|
|
1214
|
+
this.worktreeManager.removePreviewForPool(stashId).catch((err) => {
|
|
1215
|
+
logger.warn("pool", `reap worktree cleanup failed for ${stashId}`, {
|
|
1216
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1217
|
+
});
|
|
1218
|
+
});
|
|
1219
|
+
this.broadcast({ type: "stash:preview_stopped", stashId });
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
evictOldest() {
|
|
1223
|
+
let oldest = null;
|
|
1224
|
+
for (const entry of this.entries.values()) {
|
|
1225
|
+
if (!oldest || entry.lastHeartbeat < oldest.lastHeartbeat) {
|
|
1226
|
+
oldest = entry;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
if (oldest) {
|
|
1230
|
+
logger.info("pool", `evicting: ${oldest.stashId} (oldest heartbeat)`);
|
|
1231
|
+
this.killEntry(oldest);
|
|
1232
|
+
this.entries.delete(oldest.stashId);
|
|
1233
|
+
this.usedPorts.delete(oldest.port);
|
|
1234
|
+
this.worktreeManager.removePreviewForPool(oldest.stashId).catch((err) => {
|
|
1235
|
+
logger.warn("pool", `evict worktree cleanup failed for ${oldest.stashId}`, {
|
|
1236
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1237
|
+
});
|
|
1238
|
+
});
|
|
1239
|
+
this.broadcast({ type: "stash:preview_stopped", stashId: oldest.stashId });
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
allocatePort() {
|
|
1243
|
+
for (let port = STASH_PORT_START;port <= STASH_PORT_END; port++) {
|
|
1244
|
+
if (!this.usedPorts.has(port)) {
|
|
1245
|
+
return port;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
throw new Error(`No available ports in range ${STASH_PORT_START}-${STASH_PORT_END}`);
|
|
1249
|
+
}
|
|
1250
|
+
killEntry(entry) {
|
|
1251
|
+
try {
|
|
1252
|
+
entry.process.kill();
|
|
1253
|
+
} catch {}
|
|
1254
|
+
}
|
|
1255
|
+
async waitForPort(port, timeout) {
|
|
1256
|
+
const start = Date.now();
|
|
1257
|
+
while (Date.now() - start < timeout) {
|
|
1258
|
+
try {
|
|
1259
|
+
const res = await fetch(`http://localhost:${port}`, { signal: AbortSignal.timeout(2000) });
|
|
1260
|
+
if (res.ok || res.status < 500)
|
|
1261
|
+
return;
|
|
1262
|
+
} catch {}
|
|
1263
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
1264
|
+
}
|
|
1265
|
+
throw new Error(`Port ${port} not ready within ${timeout}ms`);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// ../server/dist/services/stash-service.js
|
|
1048
1270
|
class StashService {
|
|
1049
1271
|
projectPath;
|
|
1050
1272
|
worktreeManager;
|
|
1051
1273
|
persistence;
|
|
1052
1274
|
broadcast;
|
|
1275
|
+
previewPool;
|
|
1053
1276
|
selectedComponent = null;
|
|
1054
|
-
previewServer = null;
|
|
1055
|
-
activePreviewStashId = null;
|
|
1056
1277
|
constructor(projectPath, worktreeManager, persistence, broadcast) {
|
|
1057
1278
|
this.projectPath = projectPath;
|
|
1058
1279
|
this.worktreeManager = worktreeManager;
|
|
1059
1280
|
this.persistence = persistence;
|
|
1060
1281
|
this.broadcast = broadcast;
|
|
1282
|
+
this.previewPool = new PreviewPool(worktreeManager, broadcast);
|
|
1061
1283
|
}
|
|
1062
1284
|
setSelectedComponent(component) {
|
|
1063
1285
|
this.selectedComponent = component;
|
|
@@ -1103,7 +1325,7 @@ class StashService {
|
|
|
1103
1325
|
});
|
|
1104
1326
|
}
|
|
1105
1327
|
}
|
|
1106
|
-
async chat(projectId, message) {
|
|
1328
|
+
async chat(projectId, message, referenceStashIds) {
|
|
1107
1329
|
const component = this.selectedComponent;
|
|
1108
1330
|
let sourceCode = "";
|
|
1109
1331
|
const filePath = component?.filePath || "";
|
|
@@ -1113,8 +1335,18 @@ class StashService {
|
|
|
1113
1335
|
sourceCode = readFileSync4(sourceFile, "utf-8");
|
|
1114
1336
|
}
|
|
1115
1337
|
}
|
|
1338
|
+
let stashContext = "";
|
|
1339
|
+
if (referenceStashIds?.length) {
|
|
1340
|
+
const refs = referenceStashIds.map((id) => this.persistence.getStash(projectId, id)).filter(Boolean).map((s) => `- "${s.prompt}"${s.componentPath ? ` (${s.componentPath})` : ""}`);
|
|
1341
|
+
if (refs.length) {
|
|
1342
|
+
stashContext = `
|
|
1343
|
+
Referenced stashes:
|
|
1344
|
+
${refs.join(`
|
|
1345
|
+
`)}`;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1116
1348
|
const chatPrompt = [
|
|
1117
|
-
"The user is asking about
|
|
1349
|
+
"The user is asking about their UI project. Answer concisely.",
|
|
1118
1350
|
"Do NOT modify any files.",
|
|
1119
1351
|
"",
|
|
1120
1352
|
component ? `Component: ${component.name}` : "",
|
|
@@ -1124,6 +1356,7 @@ Source:
|
|
|
1124
1356
|
\`\`\`
|
|
1125
1357
|
${sourceCode.substring(0, 3000)}
|
|
1126
1358
|
\`\`\`` : "",
|
|
1359
|
+
stashContext,
|
|
1127
1360
|
"",
|
|
1128
1361
|
`User question: ${message}`
|
|
1129
1362
|
].filter(Boolean).join(`
|
|
@@ -1176,18 +1409,23 @@ ${sourceCode.substring(0, 3000)}
|
|
|
1176
1409
|
break;
|
|
1177
1410
|
}
|
|
1178
1411
|
}
|
|
1179
|
-
async generate(projectId, prompt, stashCount = DEFAULT_STASH_COUNT) {
|
|
1180
|
-
|
|
1181
|
-
|
|
1412
|
+
async generate(projectId, prompt, stashCount = DEFAULT_STASH_COUNT, referenceStashIds) {
|
|
1413
|
+
let enrichedPrompt = prompt;
|
|
1414
|
+
if (referenceStashIds?.length) {
|
|
1415
|
+
const refDescriptions = referenceStashIds.map((id) => this.persistence.getStash(projectId, id)).filter(Boolean).map((s) => `- "${s.prompt}"${s.componentPath ? ` (${s.componentPath})` : ""}`);
|
|
1416
|
+
if (refDescriptions.length) {
|
|
1417
|
+
enrichedPrompt = `${prompt}
|
|
1418
|
+
|
|
1419
|
+
## Reference Stashes (use as inspiration)
|
|
1420
|
+
${refDescriptions.join(`
|
|
1421
|
+
`)}`;
|
|
1422
|
+
}
|
|
1182
1423
|
}
|
|
1183
1424
|
await generate({
|
|
1184
1425
|
projectPath: this.projectPath,
|
|
1185
1426
|
projectId,
|
|
1186
|
-
prompt,
|
|
1187
|
-
component: {
|
|
1188
|
-
filePath: this.selectedComponent.filePath,
|
|
1189
|
-
exportName: this.selectedComponent.name
|
|
1190
|
-
},
|
|
1427
|
+
prompt: enrichedPrompt,
|
|
1428
|
+
component: this.selectedComponent ? { filePath: this.selectedComponent.filePath, exportName: this.selectedComponent.name } : undefined,
|
|
1191
1429
|
count: stashCount,
|
|
1192
1430
|
onProgress: (event) => this.progressToBroadcast(event)
|
|
1193
1431
|
});
|
|
@@ -1200,94 +1438,25 @@ ${sourceCode.substring(0, 3000)}
|
|
|
1200
1438
|
onProgress: (event) => this.progressToBroadcast(event)
|
|
1201
1439
|
});
|
|
1202
1440
|
}
|
|
1203
|
-
async switchPreview(stashId) {
|
|
1204
|
-
const
|
|
1205
|
-
|
|
1206
|
-
if (
|
|
1207
|
-
|
|
1208
|
-
}
|
|
1209
|
-
await this.ensurePreviewServer(previewPath);
|
|
1210
|
-
if (this.activePreviewStashId === stashId) {
|
|
1211
|
-
this.broadcast({ type: "stash:port", stashId, port: previewPort });
|
|
1212
|
-
return;
|
|
1441
|
+
async switchPreview(stashId, sortedStashIds) {
|
|
1442
|
+
const port = await this.previewPool.getOrStart(stashId);
|
|
1443
|
+
this.broadcast({ type: "stash:port", stashId, port });
|
|
1444
|
+
if (sortedStashIds && sortedStashIds.length > 1) {
|
|
1445
|
+
this.previewPool.prefetchNeighbors(stashId, sortedStashIds);
|
|
1213
1446
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
this.broadcast({ type: "stash:port", stashId, port: previewPort });
|
|
1447
|
+
}
|
|
1448
|
+
previewHeartbeat(stashId) {
|
|
1449
|
+
this.previewPool.heartbeat(stashId);
|
|
1218
1450
|
}
|
|
1219
1451
|
async applyStash(stashId) {
|
|
1220
|
-
this.
|
|
1452
|
+
await this.previewPool.shutdown();
|
|
1221
1453
|
await apply({ projectPath: this.projectPath, stashId });
|
|
1222
|
-
this.activePreviewStashId = null;
|
|
1223
1454
|
this.broadcast({ type: "stash:applied", stashId });
|
|
1224
1455
|
}
|
|
1225
1456
|
async deleteStash(stashId) {
|
|
1226
|
-
|
|
1227
|
-
this.activePreviewStashId = null;
|
|
1228
|
-
}
|
|
1457
|
+
await this.previewPool.stop(stashId);
|
|
1229
1458
|
await remove(this.projectPath, stashId);
|
|
1230
1459
|
}
|
|
1231
|
-
async ensurePreviewServer(previewPath) {
|
|
1232
|
-
if (this.previewServer) {
|
|
1233
|
-
const port = this.worktreeManager.getPreviewPort();
|
|
1234
|
-
try {
|
|
1235
|
-
const res = await fetch(`http://localhost:${port}`, { signal: AbortSignal.timeout(2000) });
|
|
1236
|
-
if (res.ok || res.status < 500)
|
|
1237
|
-
return;
|
|
1238
|
-
} catch {
|
|
1239
|
-
this.previewServer = null;
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
await this.startPreviewServer(previewPath);
|
|
1243
|
-
}
|
|
1244
|
-
async startPreviewServer(previewPath) {
|
|
1245
|
-
const port = this.worktreeManager.getPreviewPort();
|
|
1246
|
-
this.previewServer = Bun.spawn({
|
|
1247
|
-
cmd: ["npm", "run", "dev"],
|
|
1248
|
-
cwd: previewPath,
|
|
1249
|
-
stdin: "ignore",
|
|
1250
|
-
stdout: "pipe",
|
|
1251
|
-
stderr: "pipe",
|
|
1252
|
-
env: { ...process.env, PORT: String(port), BROWSER: "none" }
|
|
1253
|
-
});
|
|
1254
|
-
await this.waitForPort(port, 60000);
|
|
1255
|
-
}
|
|
1256
|
-
stopPreviewServer() {
|
|
1257
|
-
if (this.previewServer) {
|
|
1258
|
-
this.previewServer.kill();
|
|
1259
|
-
this.previewServer = null;
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
async waitForRecompile(port, timeout) {
|
|
1263
|
-
const start = Date.now();
|
|
1264
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
1265
|
-
while (Date.now() - start < timeout) {
|
|
1266
|
-
try {
|
|
1267
|
-
const res = await fetch(`http://localhost:${port}`, {
|
|
1268
|
-
signal: AbortSignal.timeout(3000),
|
|
1269
|
-
headers: { "cache-control": "no-cache" }
|
|
1270
|
-
});
|
|
1271
|
-
if (res.ok) {
|
|
1272
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
1273
|
-
return;
|
|
1274
|
-
}
|
|
1275
|
-
} catch {}
|
|
1276
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
async waitForPort(port, timeout) {
|
|
1280
|
-
const start = Date.now();
|
|
1281
|
-
while (Date.now() - start < timeout) {
|
|
1282
|
-
try {
|
|
1283
|
-
const res = await fetch(`http://localhost:${port}`, { signal: AbortSignal.timeout(2000) });
|
|
1284
|
-
if (res.ok || res.status < 500)
|
|
1285
|
-
return;
|
|
1286
|
-
} catch {}
|
|
1287
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
1288
|
-
}
|
|
1289
|
-
throw new Error(`Port ${port} not ready within ${timeout}ms`);
|
|
1290
|
-
}
|
|
1291
1460
|
}
|
|
1292
1461
|
|
|
1293
1462
|
// ../server/dist/services/websocket.js
|
|
@@ -1304,7 +1473,7 @@ function broadcast(event) {
|
|
|
1304
1473
|
function getPersistenceFromWs() {
|
|
1305
1474
|
return persistence;
|
|
1306
1475
|
}
|
|
1307
|
-
function createWebSocketHandler(projectPath, userDevPort) {
|
|
1476
|
+
function createWebSocketHandler(projectPath, userDevPort, appProxyPort) {
|
|
1308
1477
|
worktreeManager = new WorktreeManager(projectPath);
|
|
1309
1478
|
persistence = new PersistenceService(projectPath);
|
|
1310
1479
|
stashService = new StashService(projectPath, worktreeManager, persistence, broadcast);
|
|
@@ -1312,7 +1481,7 @@ function createWebSocketHandler(projectPath, userDevPort) {
|
|
|
1312
1481
|
open(ws) {
|
|
1313
1482
|
clients.add(ws);
|
|
1314
1483
|
logger.info("ws", "client connected", { total: clients.size });
|
|
1315
|
-
ws.send(JSON.stringify({ type: "server_ready", port: userDevPort }));
|
|
1484
|
+
ws.send(JSON.stringify({ type: "server_ready", port: userDevPort, appProxyPort }));
|
|
1316
1485
|
},
|
|
1317
1486
|
async message(ws, message) {
|
|
1318
1487
|
const raw = typeof message === "string" ? message : new TextDecoder().decode(message);
|
|
@@ -1339,7 +1508,7 @@ function createWebSocketHandler(projectPath, userDevPort) {
|
|
|
1339
1508
|
type: "text",
|
|
1340
1509
|
createdAt: new Date().toISOString()
|
|
1341
1510
|
});
|
|
1342
|
-
await stashService.chat(event.projectId, event.message);
|
|
1511
|
+
await stashService.chat(event.projectId, event.message, event.referenceStashIds);
|
|
1343
1512
|
break;
|
|
1344
1513
|
case "generate":
|
|
1345
1514
|
persistence.saveChatMessage(event.projectId, {
|
|
@@ -1349,13 +1518,16 @@ function createWebSocketHandler(projectPath, userDevPort) {
|
|
|
1349
1518
|
type: "text",
|
|
1350
1519
|
createdAt: new Date().toISOString()
|
|
1351
1520
|
});
|
|
1352
|
-
await stashService.generate(event.projectId, event.prompt, event.stashCount);
|
|
1521
|
+
await stashService.generate(event.projectId, event.prompt, event.stashCount, event.referenceStashIds);
|
|
1353
1522
|
break;
|
|
1354
1523
|
case "vary":
|
|
1355
1524
|
await stashService.vary(event.sourceStashId, event.prompt);
|
|
1356
1525
|
break;
|
|
1357
1526
|
case "interact":
|
|
1358
|
-
await stashService.switchPreview(event.stashId);
|
|
1527
|
+
await stashService.switchPreview(event.stashId, event.sortedStashIds);
|
|
1528
|
+
break;
|
|
1529
|
+
case "preview_heartbeat":
|
|
1530
|
+
stashService.previewHeartbeat(event.stashId);
|
|
1359
1531
|
break;
|
|
1360
1532
|
case "apply_stash":
|
|
1361
1533
|
await stashService.applyStash(event.stashId);
|
|
@@ -1377,6 +1549,111 @@ function createWebSocketHandler(projectPath, userDevPort) {
|
|
|
1377
1549
|
};
|
|
1378
1550
|
}
|
|
1379
1551
|
|
|
1552
|
+
// ../server/dist/services/app-proxy.js
|
|
1553
|
+
function startAppProxy(userDevPort, proxyPort, injectOverlay) {
|
|
1554
|
+
const server = Bun.serve({
|
|
1555
|
+
port: proxyPort,
|
|
1556
|
+
async fetch(req, server2) {
|
|
1557
|
+
if (req.headers.get("upgrade")?.toLowerCase() === "websocket") {
|
|
1558
|
+
const url2 = new URL(req.url);
|
|
1559
|
+
const success = server2.upgrade(req, {
|
|
1560
|
+
data: {
|
|
1561
|
+
path: url2.pathname + url2.search,
|
|
1562
|
+
upstream: null,
|
|
1563
|
+
ready: false,
|
|
1564
|
+
buffer: []
|
|
1565
|
+
}
|
|
1566
|
+
});
|
|
1567
|
+
return success ? undefined : new Response("WebSocket upgrade failed", { status: 400 });
|
|
1568
|
+
}
|
|
1569
|
+
const url = new URL(req.url);
|
|
1570
|
+
const targetUrl = `http://localhost:${userDevPort}${url.pathname}${url.search}`;
|
|
1571
|
+
try {
|
|
1572
|
+
const headers = new Headers;
|
|
1573
|
+
for (const [key, value] of req.headers.entries()) {
|
|
1574
|
+
if (!["host", "accept-encoding"].includes(key.toLowerCase())) {
|
|
1575
|
+
headers.set(key, value);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
headers.set("host", `localhost:${userDevPort}`);
|
|
1579
|
+
const response = await fetch(targetUrl, {
|
|
1580
|
+
method: req.method,
|
|
1581
|
+
headers,
|
|
1582
|
+
body: req.method !== "GET" && req.method !== "HEAD" ? await req.arrayBuffer() : undefined,
|
|
1583
|
+
redirect: "manual"
|
|
1584
|
+
});
|
|
1585
|
+
const contentType = response.headers.get("content-type") || "";
|
|
1586
|
+
if (contentType.includes("text/html")) {
|
|
1587
|
+
const html = await response.text();
|
|
1588
|
+
const injectedHtml = injectOverlay(html);
|
|
1589
|
+
const respHeaders2 = new Headers(response.headers);
|
|
1590
|
+
respHeaders2.delete("content-encoding");
|
|
1591
|
+
respHeaders2.delete("content-length");
|
|
1592
|
+
return new Response(injectedHtml, {
|
|
1593
|
+
status: response.status,
|
|
1594
|
+
headers: respHeaders2
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
const respHeaders = new Headers(response.headers);
|
|
1598
|
+
respHeaders.delete("content-encoding");
|
|
1599
|
+
return new Response(response.body, {
|
|
1600
|
+
status: response.status,
|
|
1601
|
+
headers: respHeaders
|
|
1602
|
+
});
|
|
1603
|
+
} catch (err) {
|
|
1604
|
+
return new Response(JSON.stringify({ error: "Proxy failed", detail: String(err) }), {
|
|
1605
|
+
status: 502,
|
|
1606
|
+
headers: { "content-type": "application/json" }
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
},
|
|
1610
|
+
websocket: {
|
|
1611
|
+
open(ws) {
|
|
1612
|
+
const { data } = ws;
|
|
1613
|
+
const upstream = new WebSocket(`ws://localhost:${userDevPort}${data.path}`);
|
|
1614
|
+
upstream.addEventListener("open", () => {
|
|
1615
|
+
data.upstream = upstream;
|
|
1616
|
+
data.ready = true;
|
|
1617
|
+
for (const msg of data.buffer) {
|
|
1618
|
+
upstream.send(msg);
|
|
1619
|
+
}
|
|
1620
|
+
data.buffer = [];
|
|
1621
|
+
});
|
|
1622
|
+
upstream.addEventListener("message", (event) => {
|
|
1623
|
+
try {
|
|
1624
|
+
ws.sendText(typeof event.data === "string" ? event.data : String(event.data));
|
|
1625
|
+
} catch {}
|
|
1626
|
+
});
|
|
1627
|
+
upstream.addEventListener("close", () => {
|
|
1628
|
+
try {
|
|
1629
|
+
ws.close();
|
|
1630
|
+
} catch {}
|
|
1631
|
+
});
|
|
1632
|
+
upstream.addEventListener("error", () => {
|
|
1633
|
+
try {
|
|
1634
|
+
ws.close();
|
|
1635
|
+
} catch {}
|
|
1636
|
+
});
|
|
1637
|
+
},
|
|
1638
|
+
message(ws, msg) {
|
|
1639
|
+
const { data } = ws;
|
|
1640
|
+
if (data.ready && data.upstream) {
|
|
1641
|
+
data.upstream.send(typeof msg === "string" ? msg : new Uint8Array(msg));
|
|
1642
|
+
} else {
|
|
1643
|
+
data.buffer.push(typeof msg === "string" ? msg : new Uint8Array(msg).buffer);
|
|
1644
|
+
}
|
|
1645
|
+
},
|
|
1646
|
+
close(ws) {
|
|
1647
|
+
try {
|
|
1648
|
+
ws.data.upstream?.close();
|
|
1649
|
+
} catch {}
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
});
|
|
1653
|
+
logger.info("proxy", `App proxy at http://localhost:${proxyPort} \u2192 http://localhost:${userDevPort}`);
|
|
1654
|
+
return server;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1380
1657
|
// ../server/dist/index.js
|
|
1381
1658
|
var serverState = {
|
|
1382
1659
|
projectPath: "",
|
|
@@ -1388,56 +1665,6 @@ function getPersistence() {
|
|
|
1388
1665
|
var app2 = new Hono2;
|
|
1389
1666
|
app2.use("/*", cors());
|
|
1390
1667
|
app2.route("/api", apiRoutes);
|
|
1391
|
-
async function proxyToUserApp(c, targetPath, injectOverlay = false) {
|
|
1392
|
-
const userDevPort = serverState.userDevPort;
|
|
1393
|
-
const url = new URL(c.req.url);
|
|
1394
|
-
const targetUrl = `http://localhost:${userDevPort}${targetPath}${url.search}`;
|
|
1395
|
-
try {
|
|
1396
|
-
const headers = new Headers;
|
|
1397
|
-
for (const [key, value] of Object.entries(c.req.header())) {
|
|
1398
|
-
if (!["host", "accept-encoding"].includes(key.toLowerCase()) && value) {
|
|
1399
|
-
headers.set(key, value);
|
|
1400
|
-
}
|
|
1401
|
-
}
|
|
1402
|
-
headers.set("host", `localhost:${userDevPort}`);
|
|
1403
|
-
const response = await fetch(targetUrl, {
|
|
1404
|
-
method: c.req.method,
|
|
1405
|
-
headers,
|
|
1406
|
-
body: c.req.method !== "GET" && c.req.method !== "HEAD" ? await c.req.arrayBuffer() : undefined,
|
|
1407
|
-
redirect: "manual"
|
|
1408
|
-
});
|
|
1409
|
-
const contentType = response.headers.get("content-type") || "";
|
|
1410
|
-
if (injectOverlay && contentType.includes("text/html")) {
|
|
1411
|
-
const html = await response.text();
|
|
1412
|
-
const injectedHtml = injectOverlayScript(html);
|
|
1413
|
-
const respHeaders2 = new Headers(response.headers);
|
|
1414
|
-
respHeaders2.delete("content-encoding");
|
|
1415
|
-
respHeaders2.delete("content-length");
|
|
1416
|
-
return new Response(injectedHtml, {
|
|
1417
|
-
status: response.status,
|
|
1418
|
-
headers: respHeaders2
|
|
1419
|
-
});
|
|
1420
|
-
}
|
|
1421
|
-
const respHeaders = new Headers(response.headers);
|
|
1422
|
-
respHeaders.delete("content-encoding");
|
|
1423
|
-
return new Response(response.body, {
|
|
1424
|
-
status: response.status,
|
|
1425
|
-
headers: respHeaders
|
|
1426
|
-
});
|
|
1427
|
-
} catch (err) {
|
|
1428
|
-
return new Response(JSON.stringify({ error: "Proxy failed", detail: String(err) }), {
|
|
1429
|
-
status: 502,
|
|
1430
|
-
headers: { "content-type": "application/json" }
|
|
1431
|
-
});
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
app2.all("/app/*", (c) => {
|
|
1435
|
-
const path = c.req.path.replace(/^\/app/, "") || "/";
|
|
1436
|
-
return proxyToUserApp(c, path, true);
|
|
1437
|
-
});
|
|
1438
|
-
app2.all("/_next/*", (c) => proxyToUserApp(c, c.req.path));
|
|
1439
|
-
app2.all("/__nextjs*", (c) => proxyToUserApp(c, c.req.path));
|
|
1440
|
-
app2.all("/__next*", (c) => proxyToUserApp(c, c.req.path));
|
|
1441
1668
|
app2.get("/*", async (c) => {
|
|
1442
1669
|
const path = c.req.path;
|
|
1443
1670
|
const selfDir = dirname2(fileURLToPath(import.meta.url));
|
|
@@ -1480,7 +1707,9 @@ app2.get("/*", async (c) => {
|
|
|
1480
1707
|
function startServer(projectPath, userDevPort, port = STASHES_PORT) {
|
|
1481
1708
|
serverState = { projectPath, userDevPort };
|
|
1482
1709
|
initLogFile(projectPath);
|
|
1483
|
-
const
|
|
1710
|
+
const appProxyPort = port + 1;
|
|
1711
|
+
startAppProxy(userDevPort, appProxyPort, injectOverlayScript);
|
|
1712
|
+
const wsHandler = createWebSocketHandler(projectPath, userDevPort, appProxyPort);
|
|
1484
1713
|
const server = Bun.serve({
|
|
1485
1714
|
port,
|
|
1486
1715
|
fetch(req, server2) {
|
|
@@ -1645,22 +1874,25 @@ function injectOverlayScript(html) {
|
|
|
1645
1874
|
}
|
|
1646
1875
|
});
|
|
1647
1876
|
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1877
|
+
// Report current URL to parent for status bar display
|
|
1878
|
+
function reportUrl() {
|
|
1879
|
+
window.parent.postMessage({
|
|
1880
|
+
type: 'stashes:url_change',
|
|
1881
|
+
url: window.location.pathname + window.location.search + window.location.hash
|
|
1882
|
+
}, '*');
|
|
1883
|
+
}
|
|
1884
|
+
reportUrl();
|
|
1885
|
+
var origPush = history.pushState;
|
|
1886
|
+
history.pushState = function() {
|
|
1887
|
+
origPush.apply(this, arguments);
|
|
1888
|
+
setTimeout(reportUrl, 0);
|
|
1889
|
+
};
|
|
1890
|
+
var origReplace = history.replaceState;
|
|
1891
|
+
history.replaceState = function() {
|
|
1892
|
+
origReplace.apply(this, arguments);
|
|
1893
|
+
setTimeout(reportUrl, 0);
|
|
1894
|
+
};
|
|
1895
|
+
window.addEventListener('popstate', reportUrl);
|
|
1664
1896
|
|
|
1665
1897
|
document.addEventListener('mousemove', onMouseMove, { passive: true });
|
|
1666
1898
|
document.addEventListener('click', onClick, true);
|