stashes 0.1.6 → 0.1.8
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 +167 -87
- package/dist/mcp.js +167 -87
- package/dist/web/assets/index-8mMmnYX5.js +62 -0
- package/dist/web/assets/index-BEubPa-l.css +1 -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,6 +33,7 @@ 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;
|
|
37
38
|
var DEFAULT_DIRECTIVES = [
|
|
38
39
|
"Minimal and clean \u2014 reduce visual noise, emphasize whitespace, limit to 2-3 colors, typography-driven hierarchy",
|
|
@@ -346,6 +347,9 @@ class PersistenceService {
|
|
|
346
347
|
const filePath = join3(this.basePath, "projects", projectId, "stashes.json");
|
|
347
348
|
return readJson(filePath, []);
|
|
348
349
|
}
|
|
350
|
+
getStash(projectId, stashId) {
|
|
351
|
+
return this.listStashes(projectId).find((s) => s.id === stashId);
|
|
352
|
+
}
|
|
349
353
|
saveStash(stash) {
|
|
350
354
|
const stashes = [...this.listStashes(stash.projectId)];
|
|
351
355
|
const index = stashes.findIndex((s) => s.id === stash.id);
|
|
@@ -569,12 +573,12 @@ async function waitForPort(port, timeout) {
|
|
|
569
573
|
}
|
|
570
574
|
async function captureEphemeralScreenshot(worktreePath, projectPath, stashId, port) {
|
|
571
575
|
const devServer = spawn3({
|
|
572
|
-
cmd: ["npm", "run", "dev"
|
|
576
|
+
cmd: ["npm", "run", "dev"],
|
|
573
577
|
cwd: worktreePath,
|
|
574
578
|
stdin: "ignore",
|
|
575
579
|
stdout: "pipe",
|
|
576
580
|
stderr: "pipe",
|
|
577
|
-
env: { ...process.env, PORT: String(port) }
|
|
581
|
+
env: { ...process.env, PORT: String(port), BROWSER: "none" }
|
|
578
582
|
});
|
|
579
583
|
try {
|
|
580
584
|
await waitForPort(port, 60000);
|
|
@@ -773,12 +777,12 @@ async function vary(opts) {
|
|
|
773
777
|
const screenshotGit = simpleGit3(screenshotWorktree.path);
|
|
774
778
|
await screenshotGit.checkout(["-f", stash.branch]);
|
|
775
779
|
const devServer = spawn4({
|
|
776
|
-
cmd: ["npm", "run", "dev"
|
|
780
|
+
cmd: ["npm", "run", "dev"],
|
|
777
781
|
cwd: screenshotWorktree.path,
|
|
778
782
|
stdin: "ignore",
|
|
779
783
|
stdout: "pipe",
|
|
780
784
|
stderr: "pipe",
|
|
781
|
-
env: { ...process.env, PORT: String(port) }
|
|
785
|
+
env: { ...process.env, PORT: String(port), BROWSER: "none" }
|
|
782
786
|
});
|
|
783
787
|
try {
|
|
784
788
|
await waitForPort2(port, 60000);
|
|
@@ -1103,7 +1107,7 @@ class StashService {
|
|
|
1103
1107
|
});
|
|
1104
1108
|
}
|
|
1105
1109
|
}
|
|
1106
|
-
async chat(projectId, message) {
|
|
1110
|
+
async chat(projectId, message, referenceStashIds) {
|
|
1107
1111
|
const component = this.selectedComponent;
|
|
1108
1112
|
let sourceCode = "";
|
|
1109
1113
|
const filePath = component?.filePath || "";
|
|
@@ -1113,8 +1117,18 @@ class StashService {
|
|
|
1113
1117
|
sourceCode = readFileSync4(sourceFile, "utf-8");
|
|
1114
1118
|
}
|
|
1115
1119
|
}
|
|
1120
|
+
let stashContext = "";
|
|
1121
|
+
if (referenceStashIds?.length) {
|
|
1122
|
+
const refs = referenceStashIds.map((id) => this.persistence.getStash(projectId, id)).filter(Boolean).map((s) => `- "${s.prompt}"${s.componentPath ? ` (${s.componentPath})` : ""}`);
|
|
1123
|
+
if (refs.length) {
|
|
1124
|
+
stashContext = `
|
|
1125
|
+
Referenced stashes:
|
|
1126
|
+
${refs.join(`
|
|
1127
|
+
`)}`;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1116
1130
|
const chatPrompt = [
|
|
1117
|
-
"The user is asking about
|
|
1131
|
+
"The user is asking about their UI project. Answer concisely.",
|
|
1118
1132
|
"Do NOT modify any files.",
|
|
1119
1133
|
"",
|
|
1120
1134
|
component ? `Component: ${component.name}` : "",
|
|
@@ -1124,6 +1138,7 @@ Source:
|
|
|
1124
1138
|
\`\`\`
|
|
1125
1139
|
${sourceCode.substring(0, 3000)}
|
|
1126
1140
|
\`\`\`` : "",
|
|
1141
|
+
stashContext,
|
|
1127
1142
|
"",
|
|
1128
1143
|
`User question: ${message}`
|
|
1129
1144
|
].filter(Boolean).join(`
|
|
@@ -1176,18 +1191,23 @@ ${sourceCode.substring(0, 3000)}
|
|
|
1176
1191
|
break;
|
|
1177
1192
|
}
|
|
1178
1193
|
}
|
|
1179
|
-
async generate(projectId, prompt, stashCount = DEFAULT_STASH_COUNT) {
|
|
1180
|
-
|
|
1181
|
-
|
|
1194
|
+
async generate(projectId, prompt, stashCount = DEFAULT_STASH_COUNT, referenceStashIds) {
|
|
1195
|
+
let enrichedPrompt = prompt;
|
|
1196
|
+
if (referenceStashIds?.length) {
|
|
1197
|
+
const refDescriptions = referenceStashIds.map((id) => this.persistence.getStash(projectId, id)).filter(Boolean).map((s) => `- "${s.prompt}"${s.componentPath ? ` (${s.componentPath})` : ""}`);
|
|
1198
|
+
if (refDescriptions.length) {
|
|
1199
|
+
enrichedPrompt = `${prompt}
|
|
1200
|
+
|
|
1201
|
+
## Reference Stashes (use as inspiration)
|
|
1202
|
+
${refDescriptions.join(`
|
|
1203
|
+
`)}`;
|
|
1204
|
+
}
|
|
1182
1205
|
}
|
|
1183
1206
|
await generate({
|
|
1184
1207
|
projectPath: this.projectPath,
|
|
1185
1208
|
projectId,
|
|
1186
|
-
prompt,
|
|
1187
|
-
component: {
|
|
1188
|
-
filePath: this.selectedComponent.filePath,
|
|
1189
|
-
exportName: this.selectedComponent.name
|
|
1190
|
-
},
|
|
1209
|
+
prompt: enrichedPrompt,
|
|
1210
|
+
component: this.selectedComponent ? { filePath: this.selectedComponent.filePath, exportName: this.selectedComponent.name } : undefined,
|
|
1191
1211
|
count: stashCount,
|
|
1192
1212
|
onProgress: (event) => this.progressToBroadcast(event)
|
|
1193
1213
|
});
|
|
@@ -1244,12 +1264,12 @@ ${sourceCode.substring(0, 3000)}
|
|
|
1244
1264
|
async startPreviewServer(previewPath) {
|
|
1245
1265
|
const port = this.worktreeManager.getPreviewPort();
|
|
1246
1266
|
this.previewServer = Bun.spawn({
|
|
1247
|
-
cmd: ["npm", "run", "dev"
|
|
1267
|
+
cmd: ["npm", "run", "dev"],
|
|
1248
1268
|
cwd: previewPath,
|
|
1249
1269
|
stdin: "ignore",
|
|
1250
1270
|
stdout: "pipe",
|
|
1251
1271
|
stderr: "pipe",
|
|
1252
|
-
env: { ...process.env, PORT: String(port) }
|
|
1272
|
+
env: { ...process.env, PORT: String(port), BROWSER: "none" }
|
|
1253
1273
|
});
|
|
1254
1274
|
await this.waitForPort(port, 60000);
|
|
1255
1275
|
}
|
|
@@ -1304,7 +1324,7 @@ function broadcast(event) {
|
|
|
1304
1324
|
function getPersistenceFromWs() {
|
|
1305
1325
|
return persistence;
|
|
1306
1326
|
}
|
|
1307
|
-
function createWebSocketHandler(projectPath, userDevPort) {
|
|
1327
|
+
function createWebSocketHandler(projectPath, userDevPort, appProxyPort) {
|
|
1308
1328
|
worktreeManager = new WorktreeManager(projectPath);
|
|
1309
1329
|
persistence = new PersistenceService(projectPath);
|
|
1310
1330
|
stashService = new StashService(projectPath, worktreeManager, persistence, broadcast);
|
|
@@ -1312,7 +1332,7 @@ function createWebSocketHandler(projectPath, userDevPort) {
|
|
|
1312
1332
|
open(ws) {
|
|
1313
1333
|
clients.add(ws);
|
|
1314
1334
|
logger.info("ws", "client connected", { total: clients.size });
|
|
1315
|
-
ws.send(JSON.stringify({ type: "server_ready", port: userDevPort }));
|
|
1335
|
+
ws.send(JSON.stringify({ type: "server_ready", port: userDevPort, appProxyPort }));
|
|
1316
1336
|
},
|
|
1317
1337
|
async message(ws, message) {
|
|
1318
1338
|
const raw = typeof message === "string" ? message : new TextDecoder().decode(message);
|
|
@@ -1339,7 +1359,7 @@ function createWebSocketHandler(projectPath, userDevPort) {
|
|
|
1339
1359
|
type: "text",
|
|
1340
1360
|
createdAt: new Date().toISOString()
|
|
1341
1361
|
});
|
|
1342
|
-
await stashService.chat(event.projectId, event.message);
|
|
1362
|
+
await stashService.chat(event.projectId, event.message, event.referenceStashIds);
|
|
1343
1363
|
break;
|
|
1344
1364
|
case "generate":
|
|
1345
1365
|
persistence.saveChatMessage(event.projectId, {
|
|
@@ -1349,7 +1369,7 @@ function createWebSocketHandler(projectPath, userDevPort) {
|
|
|
1349
1369
|
type: "text",
|
|
1350
1370
|
createdAt: new Date().toISOString()
|
|
1351
1371
|
});
|
|
1352
|
-
await stashService.generate(event.projectId, event.prompt, event.stashCount);
|
|
1372
|
+
await stashService.generate(event.projectId, event.prompt, event.stashCount, event.referenceStashIds);
|
|
1353
1373
|
break;
|
|
1354
1374
|
case "vary":
|
|
1355
1375
|
await stashService.vary(event.sourceStashId, event.prompt);
|
|
@@ -1377,6 +1397,111 @@ function createWebSocketHandler(projectPath, userDevPort) {
|
|
|
1377
1397
|
};
|
|
1378
1398
|
}
|
|
1379
1399
|
|
|
1400
|
+
// ../server/dist/services/app-proxy.js
|
|
1401
|
+
function startAppProxy(userDevPort, proxyPort, injectOverlay) {
|
|
1402
|
+
const server = Bun.serve({
|
|
1403
|
+
port: proxyPort,
|
|
1404
|
+
async fetch(req, server2) {
|
|
1405
|
+
if (req.headers.get("upgrade")?.toLowerCase() === "websocket") {
|
|
1406
|
+
const url2 = new URL(req.url);
|
|
1407
|
+
const success = server2.upgrade(req, {
|
|
1408
|
+
data: {
|
|
1409
|
+
path: url2.pathname + url2.search,
|
|
1410
|
+
upstream: null,
|
|
1411
|
+
ready: false,
|
|
1412
|
+
buffer: []
|
|
1413
|
+
}
|
|
1414
|
+
});
|
|
1415
|
+
return success ? undefined : new Response("WebSocket upgrade failed", { status: 400 });
|
|
1416
|
+
}
|
|
1417
|
+
const url = new URL(req.url);
|
|
1418
|
+
const targetUrl = `http://localhost:${userDevPort}${url.pathname}${url.search}`;
|
|
1419
|
+
try {
|
|
1420
|
+
const headers = new Headers;
|
|
1421
|
+
for (const [key, value] of req.headers.entries()) {
|
|
1422
|
+
if (!["host", "accept-encoding"].includes(key.toLowerCase())) {
|
|
1423
|
+
headers.set(key, value);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
headers.set("host", `localhost:${userDevPort}`);
|
|
1427
|
+
const response = await fetch(targetUrl, {
|
|
1428
|
+
method: req.method,
|
|
1429
|
+
headers,
|
|
1430
|
+
body: req.method !== "GET" && req.method !== "HEAD" ? await req.arrayBuffer() : undefined,
|
|
1431
|
+
redirect: "manual"
|
|
1432
|
+
});
|
|
1433
|
+
const contentType = response.headers.get("content-type") || "";
|
|
1434
|
+
if (contentType.includes("text/html")) {
|
|
1435
|
+
const html = await response.text();
|
|
1436
|
+
const injectedHtml = injectOverlay(html);
|
|
1437
|
+
const respHeaders2 = new Headers(response.headers);
|
|
1438
|
+
respHeaders2.delete("content-encoding");
|
|
1439
|
+
respHeaders2.delete("content-length");
|
|
1440
|
+
return new Response(injectedHtml, {
|
|
1441
|
+
status: response.status,
|
|
1442
|
+
headers: respHeaders2
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
const respHeaders = new Headers(response.headers);
|
|
1446
|
+
respHeaders.delete("content-encoding");
|
|
1447
|
+
return new Response(response.body, {
|
|
1448
|
+
status: response.status,
|
|
1449
|
+
headers: respHeaders
|
|
1450
|
+
});
|
|
1451
|
+
} catch (err) {
|
|
1452
|
+
return new Response(JSON.stringify({ error: "Proxy failed", detail: String(err) }), {
|
|
1453
|
+
status: 502,
|
|
1454
|
+
headers: { "content-type": "application/json" }
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
},
|
|
1458
|
+
websocket: {
|
|
1459
|
+
open(ws) {
|
|
1460
|
+
const { data } = ws;
|
|
1461
|
+
const upstream = new WebSocket(`ws://localhost:${userDevPort}${data.path}`);
|
|
1462
|
+
upstream.addEventListener("open", () => {
|
|
1463
|
+
data.upstream = upstream;
|
|
1464
|
+
data.ready = true;
|
|
1465
|
+
for (const msg of data.buffer) {
|
|
1466
|
+
upstream.send(msg);
|
|
1467
|
+
}
|
|
1468
|
+
data.buffer = [];
|
|
1469
|
+
});
|
|
1470
|
+
upstream.addEventListener("message", (event) => {
|
|
1471
|
+
try {
|
|
1472
|
+
ws.sendText(typeof event.data === "string" ? event.data : String(event.data));
|
|
1473
|
+
} catch {}
|
|
1474
|
+
});
|
|
1475
|
+
upstream.addEventListener("close", () => {
|
|
1476
|
+
try {
|
|
1477
|
+
ws.close();
|
|
1478
|
+
} catch {}
|
|
1479
|
+
});
|
|
1480
|
+
upstream.addEventListener("error", () => {
|
|
1481
|
+
try {
|
|
1482
|
+
ws.close();
|
|
1483
|
+
} catch {}
|
|
1484
|
+
});
|
|
1485
|
+
},
|
|
1486
|
+
message(ws, msg) {
|
|
1487
|
+
const { data } = ws;
|
|
1488
|
+
if (data.ready && data.upstream) {
|
|
1489
|
+
data.upstream.send(typeof msg === "string" ? msg : new Uint8Array(msg));
|
|
1490
|
+
} else {
|
|
1491
|
+
data.buffer.push(typeof msg === "string" ? msg : new Uint8Array(msg).buffer);
|
|
1492
|
+
}
|
|
1493
|
+
},
|
|
1494
|
+
close(ws) {
|
|
1495
|
+
try {
|
|
1496
|
+
ws.data.upstream?.close();
|
|
1497
|
+
} catch {}
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
});
|
|
1501
|
+
logger.info("proxy", `App proxy at http://localhost:${proxyPort} \u2192 http://localhost:${userDevPort}`);
|
|
1502
|
+
return server;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1380
1505
|
// ../server/dist/index.js
|
|
1381
1506
|
var serverState = {
|
|
1382
1507
|
projectPath: "",
|
|
@@ -1388,56 +1513,6 @@ function getPersistence() {
|
|
|
1388
1513
|
var app2 = new Hono2;
|
|
1389
1514
|
app2.use("/*", cors());
|
|
1390
1515
|
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
1516
|
app2.get("/*", async (c) => {
|
|
1442
1517
|
const path = c.req.path;
|
|
1443
1518
|
const selfDir = dirname2(fileURLToPath(import.meta.url));
|
|
@@ -1480,7 +1555,9 @@ app2.get("/*", async (c) => {
|
|
|
1480
1555
|
function startServer(projectPath, userDevPort, port = STASHES_PORT) {
|
|
1481
1556
|
serverState = { projectPath, userDevPort };
|
|
1482
1557
|
initLogFile(projectPath);
|
|
1483
|
-
const
|
|
1558
|
+
const appProxyPort = port + 1;
|
|
1559
|
+
startAppProxy(userDevPort, appProxyPort, injectOverlayScript);
|
|
1560
|
+
const wsHandler = createWebSocketHandler(projectPath, userDevPort, appProxyPort);
|
|
1484
1561
|
const server = Bun.serve({
|
|
1485
1562
|
port,
|
|
1486
1563
|
fetch(req, server2) {
|
|
@@ -1645,22 +1722,25 @@ function injectOverlayScript(html) {
|
|
|
1645
1722
|
}
|
|
1646
1723
|
});
|
|
1647
1724
|
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1725
|
+
// Report current URL to parent for status bar display
|
|
1726
|
+
function reportUrl() {
|
|
1727
|
+
window.parent.postMessage({
|
|
1728
|
+
type: 'stashes:url_change',
|
|
1729
|
+
url: window.location.pathname + window.location.search + window.location.hash
|
|
1730
|
+
}, '*');
|
|
1731
|
+
}
|
|
1732
|
+
reportUrl();
|
|
1733
|
+
var origPush = history.pushState;
|
|
1734
|
+
history.pushState = function() {
|
|
1735
|
+
origPush.apply(this, arguments);
|
|
1736
|
+
setTimeout(reportUrl, 0);
|
|
1737
|
+
};
|
|
1738
|
+
var origReplace = history.replaceState;
|
|
1739
|
+
history.replaceState = function() {
|
|
1740
|
+
origReplace.apply(this, arguments);
|
|
1741
|
+
setTimeout(reportUrl, 0);
|
|
1742
|
+
};
|
|
1743
|
+
window.addEventListener('popstate', reportUrl);
|
|
1664
1744
|
|
|
1665
1745
|
document.addEventListener('mousemove', onMouseMove, { passive: true });
|
|
1666
1746
|
document.addEventListener('click', onClick, true);
|