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/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"
|
|
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"
|
|
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
|
|
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
|
-
|
|
1051
|
-
|
|
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"
|
|
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
|
|
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
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
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);
|