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/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", "--", "--port", String(port)],
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", "--", "--port", String(port)],
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 a UI component in their project. Answer concisely.",
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
- if (!this.selectedComponent) {
1181
- throw new Error("No component selected");
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", "--", "--port", String(port)],
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 wsHandler = createWebSocketHandler(projectPath, userDevPort);
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
- document.addEventListener('click', function(e) {
1649
- if (pickerEnabled) return;
1650
- var link = e.target;
1651
- while (link && link.tagName !== 'A') link = link.parentElement;
1652
- if (link && link.href) {
1653
- var url;
1654
- try { url = new URL(link.href); } catch(_) { return; }
1655
- if (url.origin === window.location.origin) {
1656
- var path = url.pathname;
1657
- if (!path.startsWith('/app/')) {
1658
- e.preventDefault();
1659
- window.location.href = '/app' + path + url.search + url.hash;
1660
- }
1661
- }
1662
- }
1663
- }, false);
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);