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 CHANGED
@@ -37,6 +37,7 @@ import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
37
37
  // ../shared/dist/constants/index.js
38
38
  var STASHES_PORT = 4000;
39
39
  var DEFAULT_STASH_COUNT = 3;
40
+ var APP_PROXY_PORT = STASHES_PORT + 1;
40
41
  var STASH_PORT_START = 4010;
41
42
  var DEFAULT_DIRECTIVES = [
42
43
  "Minimal and clean \u2014 reduce visual noise, emphasize whitespace, limit to 2-3 colors, typography-driven hierarchy",
@@ -410,6 +411,9 @@ class PersistenceService {
410
411
  const filePath = join4(this.basePath, "projects", projectId, "stashes.json");
411
412
  return readJson(filePath, []);
412
413
  }
414
+ getStash(projectId, stashId) {
415
+ return this.listStashes(projectId).find((s) => s.id === stashId);
416
+ }
413
417
  saveStash(stash) {
414
418
  const stashes = [...this.listStashes(stash.projectId)];
415
419
  const index = stashes.findIndex((s) => s.id === stash.id);
@@ -633,12 +637,12 @@ async function waitForPort(port, timeout) {
633
637
  }
634
638
  async function captureEphemeralScreenshot(worktreePath, projectPath, stashId, port) {
635
639
  const devServer = spawn3({
636
- cmd: ["npm", "run", "dev", "--", "--port", String(port)],
640
+ cmd: ["npm", "run", "dev"],
637
641
  cwd: worktreePath,
638
642
  stdin: "ignore",
639
643
  stdout: "pipe",
640
644
  stderr: "pipe",
641
- env: { ...process.env, PORT: String(port) }
645
+ env: { ...process.env, PORT: String(port), BROWSER: "none" }
642
646
  });
643
647
  try {
644
648
  await waitForPort(port, 60000);
@@ -837,12 +841,12 @@ async function vary(opts) {
837
841
  const screenshotGit = simpleGit3(screenshotWorktree.path);
838
842
  await screenshotGit.checkout(["-f", stash.branch]);
839
843
  const devServer = spawn4({
840
- cmd: ["npm", "run", "dev", "--", "--port", String(port)],
844
+ cmd: ["npm", "run", "dev"],
841
845
  cwd: screenshotWorktree.path,
842
846
  stdin: "ignore",
843
847
  stdout: "pipe",
844
848
  stderr: "pipe",
845
- env: { ...process.env, PORT: String(port) }
849
+ env: { ...process.env, PORT: String(port), BROWSER: "none" }
846
850
  });
847
851
  try {
848
852
  await waitForPort2(port, 60000);
@@ -973,7 +977,7 @@ class StashService {
973
977
  });
974
978
  }
975
979
  }
976
- async chat(projectId, message) {
980
+ async chat(projectId, message, referenceStashIds) {
977
981
  const component = this.selectedComponent;
978
982
  let sourceCode = "";
979
983
  const filePath = component?.filePath || "";
@@ -983,8 +987,18 @@ class StashService {
983
987
  sourceCode = readFileSync4(sourceFile, "utf-8");
984
988
  }
985
989
  }
990
+ let stashContext = "";
991
+ if (referenceStashIds?.length) {
992
+ const refs = referenceStashIds.map((id) => this.persistence.getStash(projectId, id)).filter(Boolean).map((s) => `- "${s.prompt}"${s.componentPath ? ` (${s.componentPath})` : ""}`);
993
+ if (refs.length) {
994
+ stashContext = `
995
+ Referenced stashes:
996
+ ${refs.join(`
997
+ `)}`;
998
+ }
999
+ }
986
1000
  const chatPrompt = [
987
- "The user is asking about a UI component in their project. Answer concisely.",
1001
+ "The user is asking about their UI project. Answer concisely.",
988
1002
  "Do NOT modify any files.",
989
1003
  "",
990
1004
  component ? `Component: ${component.name}` : "",
@@ -994,6 +1008,7 @@ Source:
994
1008
  \`\`\`
995
1009
  ${sourceCode.substring(0, 3000)}
996
1010
  \`\`\`` : "",
1011
+ stashContext,
997
1012
  "",
998
1013
  `User question: ${message}`
999
1014
  ].filter(Boolean).join(`
@@ -1046,18 +1061,23 @@ ${sourceCode.substring(0, 3000)}
1046
1061
  break;
1047
1062
  }
1048
1063
  }
1049
- async generate(projectId, prompt, stashCount = DEFAULT_STASH_COUNT) {
1050
- if (!this.selectedComponent) {
1051
- throw new Error("No component selected");
1064
+ async generate(projectId, prompt, stashCount = DEFAULT_STASH_COUNT, referenceStashIds) {
1065
+ let enrichedPrompt = prompt;
1066
+ if (referenceStashIds?.length) {
1067
+ const refDescriptions = referenceStashIds.map((id) => this.persistence.getStash(projectId, id)).filter(Boolean).map((s) => `- "${s.prompt}"${s.componentPath ? ` (${s.componentPath})` : ""}`);
1068
+ if (refDescriptions.length) {
1069
+ enrichedPrompt = `${prompt}
1070
+
1071
+ ## Reference Stashes (use as inspiration)
1072
+ ${refDescriptions.join(`
1073
+ `)}`;
1074
+ }
1052
1075
  }
1053
1076
  await generate({
1054
1077
  projectPath: this.projectPath,
1055
1078
  projectId,
1056
- prompt,
1057
- component: {
1058
- filePath: this.selectedComponent.filePath,
1059
- exportName: this.selectedComponent.name
1060
- },
1079
+ prompt: enrichedPrompt,
1080
+ component: this.selectedComponent ? { filePath: this.selectedComponent.filePath, exportName: this.selectedComponent.name } : undefined,
1061
1081
  count: stashCount,
1062
1082
  onProgress: (event) => this.progressToBroadcast(event)
1063
1083
  });
@@ -1114,12 +1134,12 @@ ${sourceCode.substring(0, 3000)}
1114
1134
  async startPreviewServer(previewPath) {
1115
1135
  const port = this.worktreeManager.getPreviewPort();
1116
1136
  this.previewServer = Bun.spawn({
1117
- cmd: ["npm", "run", "dev", "--", "--port", String(port)],
1137
+ cmd: ["npm", "run", "dev"],
1118
1138
  cwd: previewPath,
1119
1139
  stdin: "ignore",
1120
1140
  stdout: "pipe",
1121
1141
  stderr: "pipe",
1122
- env: { ...process.env, PORT: String(port) }
1142
+ env: { ...process.env, PORT: String(port), BROWSER: "none" }
1123
1143
  });
1124
1144
  await this.waitForPort(port, 60000);
1125
1145
  }
@@ -1174,7 +1194,7 @@ function broadcast(event) {
1174
1194
  function getPersistenceFromWs() {
1175
1195
  return persistence;
1176
1196
  }
1177
- function createWebSocketHandler(projectPath, userDevPort) {
1197
+ function createWebSocketHandler(projectPath, userDevPort, appProxyPort) {
1178
1198
  worktreeManager = new WorktreeManager(projectPath);
1179
1199
  persistence = new PersistenceService(projectPath);
1180
1200
  stashService = new StashService(projectPath, worktreeManager, persistence, broadcast);
@@ -1182,7 +1202,7 @@ function createWebSocketHandler(projectPath, userDevPort) {
1182
1202
  open(ws) {
1183
1203
  clients.add(ws);
1184
1204
  logger.info("ws", "client connected", { total: clients.size });
1185
- ws.send(JSON.stringify({ type: "server_ready", port: userDevPort }));
1205
+ ws.send(JSON.stringify({ type: "server_ready", port: userDevPort, appProxyPort }));
1186
1206
  },
1187
1207
  async message(ws, message) {
1188
1208
  const raw = typeof message === "string" ? message : new TextDecoder().decode(message);
@@ -1209,7 +1229,7 @@ function createWebSocketHandler(projectPath, userDevPort) {
1209
1229
  type: "text",
1210
1230
  createdAt: new Date().toISOString()
1211
1231
  });
1212
- await stashService.chat(event.projectId, event.message);
1232
+ await stashService.chat(event.projectId, event.message, event.referenceStashIds);
1213
1233
  break;
1214
1234
  case "generate":
1215
1235
  persistence.saveChatMessage(event.projectId, {
@@ -1219,7 +1239,7 @@ function createWebSocketHandler(projectPath, userDevPort) {
1219
1239
  type: "text",
1220
1240
  createdAt: new Date().toISOString()
1221
1241
  });
1222
- await stashService.generate(event.projectId, event.prompt, event.stashCount);
1242
+ await stashService.generate(event.projectId, event.prompt, event.stashCount, event.referenceStashIds);
1223
1243
  break;
1224
1244
  case "vary":
1225
1245
  await stashService.vary(event.sourceStashId, event.prompt);
@@ -1247,6 +1267,111 @@ function createWebSocketHandler(projectPath, userDevPort) {
1247
1267
  };
1248
1268
  }
1249
1269
 
1270
+ // ../server/dist/services/app-proxy.js
1271
+ function startAppProxy(userDevPort, proxyPort, injectOverlay) {
1272
+ const server = Bun.serve({
1273
+ port: proxyPort,
1274
+ async fetch(req, server2) {
1275
+ if (req.headers.get("upgrade")?.toLowerCase() === "websocket") {
1276
+ const url2 = new URL(req.url);
1277
+ const success = server2.upgrade(req, {
1278
+ data: {
1279
+ path: url2.pathname + url2.search,
1280
+ upstream: null,
1281
+ ready: false,
1282
+ buffer: []
1283
+ }
1284
+ });
1285
+ return success ? undefined : new Response("WebSocket upgrade failed", { status: 400 });
1286
+ }
1287
+ const url = new URL(req.url);
1288
+ const targetUrl = `http://localhost:${userDevPort}${url.pathname}${url.search}`;
1289
+ try {
1290
+ const headers = new Headers;
1291
+ for (const [key, value] of req.headers.entries()) {
1292
+ if (!["host", "accept-encoding"].includes(key.toLowerCase())) {
1293
+ headers.set(key, value);
1294
+ }
1295
+ }
1296
+ headers.set("host", `localhost:${userDevPort}`);
1297
+ const response = await fetch(targetUrl, {
1298
+ method: req.method,
1299
+ headers,
1300
+ body: req.method !== "GET" && req.method !== "HEAD" ? await req.arrayBuffer() : undefined,
1301
+ redirect: "manual"
1302
+ });
1303
+ const contentType = response.headers.get("content-type") || "";
1304
+ if (contentType.includes("text/html")) {
1305
+ const html = await response.text();
1306
+ const injectedHtml = injectOverlay(html);
1307
+ const respHeaders2 = new Headers(response.headers);
1308
+ respHeaders2.delete("content-encoding");
1309
+ respHeaders2.delete("content-length");
1310
+ return new Response(injectedHtml, {
1311
+ status: response.status,
1312
+ headers: respHeaders2
1313
+ });
1314
+ }
1315
+ const respHeaders = new Headers(response.headers);
1316
+ respHeaders.delete("content-encoding");
1317
+ return new Response(response.body, {
1318
+ status: response.status,
1319
+ headers: respHeaders
1320
+ });
1321
+ } catch (err) {
1322
+ return new Response(JSON.stringify({ error: "Proxy failed", detail: String(err) }), {
1323
+ status: 502,
1324
+ headers: { "content-type": "application/json" }
1325
+ });
1326
+ }
1327
+ },
1328
+ websocket: {
1329
+ open(ws) {
1330
+ const { data } = ws;
1331
+ const upstream = new WebSocket(`ws://localhost:${userDevPort}${data.path}`);
1332
+ upstream.addEventListener("open", () => {
1333
+ data.upstream = upstream;
1334
+ data.ready = true;
1335
+ for (const msg of data.buffer) {
1336
+ upstream.send(msg);
1337
+ }
1338
+ data.buffer = [];
1339
+ });
1340
+ upstream.addEventListener("message", (event) => {
1341
+ try {
1342
+ ws.sendText(typeof event.data === "string" ? event.data : String(event.data));
1343
+ } catch {}
1344
+ });
1345
+ upstream.addEventListener("close", () => {
1346
+ try {
1347
+ ws.close();
1348
+ } catch {}
1349
+ });
1350
+ upstream.addEventListener("error", () => {
1351
+ try {
1352
+ ws.close();
1353
+ } catch {}
1354
+ });
1355
+ },
1356
+ message(ws, msg) {
1357
+ const { data } = ws;
1358
+ if (data.ready && data.upstream) {
1359
+ data.upstream.send(typeof msg === "string" ? msg : new Uint8Array(msg));
1360
+ } else {
1361
+ data.buffer.push(typeof msg === "string" ? msg : new Uint8Array(msg).buffer);
1362
+ }
1363
+ },
1364
+ close(ws) {
1365
+ try {
1366
+ ws.data.upstream?.close();
1367
+ } catch {}
1368
+ }
1369
+ }
1370
+ });
1371
+ logger.info("proxy", `App proxy at http://localhost:${proxyPort} \u2192 http://localhost:${userDevPort}`);
1372
+ return server;
1373
+ }
1374
+
1250
1375
  // ../server/dist/index.js
1251
1376
  var serverState = {
1252
1377
  projectPath: "",
@@ -1258,56 +1383,6 @@ function getPersistence() {
1258
1383
  var app2 = new Hono2;
1259
1384
  app2.use("/*", cors());
1260
1385
  app2.route("/api", apiRoutes);
1261
- async function proxyToUserApp(c, targetPath, injectOverlay = false) {
1262
- const userDevPort = serverState.userDevPort;
1263
- const url = new URL(c.req.url);
1264
- const targetUrl = `http://localhost:${userDevPort}${targetPath}${url.search}`;
1265
- try {
1266
- const headers = new Headers;
1267
- for (const [key, value] of Object.entries(c.req.header())) {
1268
- if (!["host", "accept-encoding"].includes(key.toLowerCase()) && value) {
1269
- headers.set(key, value);
1270
- }
1271
- }
1272
- headers.set("host", `localhost:${userDevPort}`);
1273
- const response = await fetch(targetUrl, {
1274
- method: c.req.method,
1275
- headers,
1276
- body: c.req.method !== "GET" && c.req.method !== "HEAD" ? await c.req.arrayBuffer() : undefined,
1277
- redirect: "manual"
1278
- });
1279
- const contentType = response.headers.get("content-type") || "";
1280
- if (injectOverlay && contentType.includes("text/html")) {
1281
- const html = await response.text();
1282
- const injectedHtml = injectOverlayScript(html);
1283
- const respHeaders2 = new Headers(response.headers);
1284
- respHeaders2.delete("content-encoding");
1285
- respHeaders2.delete("content-length");
1286
- return new Response(injectedHtml, {
1287
- status: response.status,
1288
- headers: respHeaders2
1289
- });
1290
- }
1291
- const respHeaders = new Headers(response.headers);
1292
- respHeaders.delete("content-encoding");
1293
- return new Response(response.body, {
1294
- status: response.status,
1295
- headers: respHeaders
1296
- });
1297
- } catch (err) {
1298
- return new Response(JSON.stringify({ error: "Proxy failed", detail: String(err) }), {
1299
- status: 502,
1300
- headers: { "content-type": "application/json" }
1301
- });
1302
- }
1303
- }
1304
- app2.all("/app/*", (c) => {
1305
- const path = c.req.path.replace(/^\/app/, "") || "/";
1306
- return proxyToUserApp(c, path, true);
1307
- });
1308
- app2.all("/_next/*", (c) => proxyToUserApp(c, c.req.path));
1309
- app2.all("/__nextjs*", (c) => proxyToUserApp(c, c.req.path));
1310
- app2.all("/__next*", (c) => proxyToUserApp(c, c.req.path));
1311
1386
  app2.get("/*", async (c) => {
1312
1387
  const path = c.req.path;
1313
1388
  const selfDir = dirname2(fileURLToPath(import.meta.url));
@@ -1350,7 +1425,9 @@ app2.get("/*", async (c) => {
1350
1425
  function startServer(projectPath, userDevPort, port = STASHES_PORT) {
1351
1426
  serverState = { projectPath, userDevPort };
1352
1427
  initLogFile(projectPath);
1353
- const wsHandler = createWebSocketHandler(projectPath, userDevPort);
1428
+ const appProxyPort = port + 1;
1429
+ startAppProxy(userDevPort, appProxyPort, injectOverlayScript);
1430
+ const wsHandler = createWebSocketHandler(projectPath, userDevPort, appProxyPort);
1354
1431
  const server = Bun.serve({
1355
1432
  port,
1356
1433
  fetch(req, server2) {
@@ -1515,22 +1592,25 @@ function injectOverlayScript(html) {
1515
1592
  }
1516
1593
  });
1517
1594
 
1518
- document.addEventListener('click', function(e) {
1519
- if (pickerEnabled) return;
1520
- var link = e.target;
1521
- while (link && link.tagName !== 'A') link = link.parentElement;
1522
- if (link && link.href) {
1523
- var url;
1524
- try { url = new URL(link.href); } catch(_) { return; }
1525
- if (url.origin === window.location.origin) {
1526
- var path = url.pathname;
1527
- if (!path.startsWith('/app/')) {
1528
- e.preventDefault();
1529
- window.location.href = '/app' + path + url.search + url.hash;
1530
- }
1531
- }
1532
- }
1533
- }, false);
1595
+ // Report current URL to parent for status bar display
1596
+ function reportUrl() {
1597
+ window.parent.postMessage({
1598
+ type: 'stashes:url_change',
1599
+ url: window.location.pathname + window.location.search + window.location.hash
1600
+ }, '*');
1601
+ }
1602
+ reportUrl();
1603
+ var origPush = history.pushState;
1604
+ history.pushState = function() {
1605
+ origPush.apply(this, arguments);
1606
+ setTimeout(reportUrl, 0);
1607
+ };
1608
+ var origReplace = history.replaceState;
1609
+ history.replaceState = function() {
1610
+ origReplace.apply(this, arguments);
1611
+ setTimeout(reportUrl, 0);
1612
+ };
1613
+ window.addEventListener('popstate', reportUrl);
1534
1614
 
1535
1615
  document.addEventListener('mousemove', onMouseMove, { passive: true });
1536
1616
  document.addEventListener('click', onClick, true);