stashes 0.1.27 → 0.1.29
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 +465 -220
- package/dist/mcp.js +448 -203
- package/dist/web/assets/{index-leC0pOEQ.js → index-CBnXcjSs.js} +19 -19
- package/dist/web/assets/index-DUXYFG67.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-Yo4820v-.css +0 -1
package/dist/cli.js
CHANGED
|
@@ -20,7 +20,7 @@ var __require = import.meta.require;
|
|
|
20
20
|
|
|
21
21
|
// src/index.ts
|
|
22
22
|
import { readFileSync as readFileSync9 } from "fs";
|
|
23
|
-
import { join as
|
|
23
|
+
import { join as join13, dirname as dirname5 } from "path";
|
|
24
24
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
25
25
|
import { Command } from "commander";
|
|
26
26
|
|
|
@@ -31,9 +31,9 @@ import open from "open";
|
|
|
31
31
|
// ../server/dist/index.js
|
|
32
32
|
import { Hono as Hono2 } from "hono";
|
|
33
33
|
import { cors } from "hono/cors";
|
|
34
|
-
import { join as
|
|
34
|
+
import { join as join9, dirname as dirname2 } from "path";
|
|
35
35
|
import { fileURLToPath } from "url";
|
|
36
|
-
import { existsSync as
|
|
36
|
+
import { existsSync as existsSync9, 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;
|
|
@@ -159,10 +159,10 @@ function ensureProject(persistence) {
|
|
|
159
159
|
var apiRoutes = app;
|
|
160
160
|
|
|
161
161
|
// ../core/dist/generation.js
|
|
162
|
-
import { readFileSync as readFileSync3, existsSync as
|
|
163
|
-
import { join as
|
|
162
|
+
import { readFileSync as readFileSync3, existsSync as existsSync7 } from "fs";
|
|
163
|
+
import { join as join7 } from "path";
|
|
164
164
|
var {spawn: spawn3 } = globalThis.Bun;
|
|
165
|
-
import
|
|
165
|
+
import simpleGit3 from "simple-git";
|
|
166
166
|
|
|
167
167
|
// ../core/dist/worktree.js
|
|
168
168
|
import simpleGit from "simple-git";
|
|
@@ -542,6 +542,14 @@ class PersistenceService {
|
|
|
542
542
|
const filePath = join4(this.basePath, "projects", projectId, "stashes.json");
|
|
543
543
|
writeJson(filePath, stashes);
|
|
544
544
|
}
|
|
545
|
+
getProjectSettings(projectId) {
|
|
546
|
+
const filePath = join4(this.basePath, "projects", projectId, "settings.json");
|
|
547
|
+
return readJson(filePath, {});
|
|
548
|
+
}
|
|
549
|
+
saveProjectSettings(projectId, settings) {
|
|
550
|
+
const filePath = join4(this.basePath, "projects", projectId, "settings.json");
|
|
551
|
+
writeJson(filePath, settings);
|
|
552
|
+
}
|
|
545
553
|
listChats(projectId) {
|
|
546
554
|
const dir = join4(this.basePath, "projects", projectId, "chats");
|
|
547
555
|
if (!existsSync4(dir))
|
|
@@ -622,18 +630,22 @@ class PersistenceService {
|
|
|
622
630
|
var {spawn } = globalThis.Bun;
|
|
623
631
|
var CLAUDE_BIN = "/opt/homebrew/bin/claude";
|
|
624
632
|
var processes = new Map;
|
|
625
|
-
function startAiProcess(id, prompt, cwd, resumeSessionId) {
|
|
633
|
+
function startAiProcess(id, prompt, cwd, resumeSessionId, model) {
|
|
626
634
|
killAiProcess(id);
|
|
627
635
|
logger.info("claude", `spawning process: ${id}`, {
|
|
628
636
|
cwd,
|
|
629
637
|
promptLength: prompt.length,
|
|
630
638
|
promptPreview: prompt.substring(0, 100),
|
|
631
|
-
resumeSessionId
|
|
639
|
+
resumeSessionId,
|
|
640
|
+
model
|
|
632
641
|
});
|
|
633
642
|
const cmd = [CLAUDE_BIN, "-p", prompt, "--output-format=stream-json", "--verbose", "--dangerously-skip-permissions"];
|
|
634
643
|
if (resumeSessionId) {
|
|
635
644
|
cmd.push("--resume", resumeSessionId);
|
|
636
645
|
}
|
|
646
|
+
if (model) {
|
|
647
|
+
cmd.push("--model", model);
|
|
648
|
+
}
|
|
637
649
|
const proc = spawn({
|
|
638
650
|
cmd,
|
|
639
651
|
stdin: "ignore",
|
|
@@ -757,6 +769,11 @@ async function* parseClaudeStream(proc) {
|
|
|
757
769
|
}
|
|
758
770
|
}
|
|
759
771
|
|
|
772
|
+
// ../core/dist/smart-screenshot.js
|
|
773
|
+
import { join as join6 } from "path";
|
|
774
|
+
import { mkdirSync as mkdirSync4, existsSync as existsSync6 } from "fs";
|
|
775
|
+
import simpleGit2 from "simple-git";
|
|
776
|
+
|
|
760
777
|
// ../core/dist/screenshot.js
|
|
761
778
|
var {spawn: spawn2 } = globalThis.Bun;
|
|
762
779
|
import { join as join5 } from "path";
|
|
@@ -799,6 +816,171 @@ async function captureScreenshot(port, projectPath, stashId) {
|
|
|
799
816
|
return `/api/screenshots/${filename}`;
|
|
800
817
|
}
|
|
801
818
|
|
|
819
|
+
// ../core/dist/smart-screenshot.js
|
|
820
|
+
var SCREENSHOTS_DIR2 = ".stashes/screenshots";
|
|
821
|
+
var DEFAULT_TIMEOUT = 120000;
|
|
822
|
+
var DIFF_MAX_CHARS = 1e4;
|
|
823
|
+
async function getStashDiff(worktreePath, parentBranch) {
|
|
824
|
+
const git = simpleGit2(worktreePath);
|
|
825
|
+
try {
|
|
826
|
+
const diff = await git.diff([parentBranch, "HEAD"]);
|
|
827
|
+
if (diff.length <= DIFF_MAX_CHARS)
|
|
828
|
+
return diff;
|
|
829
|
+
const diffStat = await git.diff([parentBranch, "HEAD", "--stat"]);
|
|
830
|
+
const truncatedDiff = diff.substring(0, DIFF_MAX_CHARS);
|
|
831
|
+
return `## Changed files:
|
|
832
|
+
${diffStat}
|
|
833
|
+
|
|
834
|
+
## Partial diff (truncated):
|
|
835
|
+
${truncatedDiff}`;
|
|
836
|
+
} catch (err) {
|
|
837
|
+
logger.warn("smart-screenshot", `git diff failed, falling back`, {
|
|
838
|
+
error: err instanceof Error ? err.message : String(err)
|
|
839
|
+
});
|
|
840
|
+
return "";
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
function buildScreenshotPrompt(port, diff, screenshotDir, stashId) {
|
|
844
|
+
return [
|
|
845
|
+
"You are a screenshot assistant. A developer made UI changes to a web app.",
|
|
846
|
+
"Your job: navigate the running app, find the pages affected by the changes, and take screenshots.",
|
|
847
|
+
"",
|
|
848
|
+
`## The app is running at: http://localhost:${port}`,
|
|
849
|
+
"",
|
|
850
|
+
"## Git diff of changes:",
|
|
851
|
+
"```",
|
|
852
|
+
diff,
|
|
853
|
+
"```",
|
|
854
|
+
"",
|
|
855
|
+
"## Instructions:",
|
|
856
|
+
"1. Analyze the diff to identify which components/pages changed",
|
|
857
|
+
"2. Determine the URL routes where these changes are visible",
|
|
858
|
+
"3. For each route:",
|
|
859
|
+
" - Navigate to it using browser_navigate",
|
|
860
|
+
" - If the change is behind an interaction (tab, modal, accordion), perform that interaction",
|
|
861
|
+
" - Wait for the page to settle",
|
|
862
|
+
" - Take a screenshot using browser_take_screenshot",
|
|
863
|
+
"4. Decide which screenshot shows the MOST visually significant change \u2014 mark it as primary",
|
|
864
|
+
"",
|
|
865
|
+
`## Screenshot save paths:`,
|
|
866
|
+
`- Primary: ${join6(screenshotDir, `${stashId}.png`)}`,
|
|
867
|
+
`- Additional: ${join6(screenshotDir, `${stashId}-1.png`)}, ${join6(screenshotDir, `${stashId}-2.png`)}, etc.`,
|
|
868
|
+
"",
|
|
869
|
+
"## Output format (after all screenshots are taken):",
|
|
870
|
+
"Respond with ONLY this JSON (no markdown fences):",
|
|
871
|
+
"{",
|
|
872
|
+
' "screenshots": [',
|
|
873
|
+
" {",
|
|
874
|
+
` "path": "${join6(screenshotDir, `${stashId}.png`)}",`,
|
|
875
|
+
' "label": "Short description of what is shown",',
|
|
876
|
+
' "route": "/the-url-path",',
|
|
877
|
+
' "isPrimary": true',
|
|
878
|
+
" }",
|
|
879
|
+
" ]",
|
|
880
|
+
"}"
|
|
881
|
+
].join(`
|
|
882
|
+
`);
|
|
883
|
+
}
|
|
884
|
+
function parseAiResult(text) {
|
|
885
|
+
try {
|
|
886
|
+
return JSON.parse(text);
|
|
887
|
+
} catch {
|
|
888
|
+
const jsonMatch = text.match(/\{[\s\S]*"screenshots"[\s\S]*\}/);
|
|
889
|
+
if (jsonMatch) {
|
|
890
|
+
try {
|
|
891
|
+
return JSON.parse(jsonMatch[0]);
|
|
892
|
+
} catch {
|
|
893
|
+
return null;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
return null;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
async function captureSmartScreenshots(opts) {
|
|
900
|
+
const { projectPath, stashId, stashBranch, parentBranch, worktreePath, port, model = "haiku", timeout = DEFAULT_TIMEOUT } = opts;
|
|
901
|
+
const screenshotDir = join6(projectPath, SCREENSHOTS_DIR2);
|
|
902
|
+
if (!existsSync6(screenshotDir)) {
|
|
903
|
+
mkdirSync4(screenshotDir, { recursive: true });
|
|
904
|
+
}
|
|
905
|
+
const diff = await getStashDiff(worktreePath, parentBranch);
|
|
906
|
+
if (!diff) {
|
|
907
|
+
logger.info("smart-screenshot", `No diff found for ${stashId}, using simple screenshot`);
|
|
908
|
+
const url = await captureScreenshot(port, projectPath, stashId);
|
|
909
|
+
return {
|
|
910
|
+
primary: url,
|
|
911
|
+
screenshots: [{ url, label: "Homepage", route: "/", isPrimary: true }]
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
const processId = `screenshot-ai-${stashId}`;
|
|
915
|
+
const prompt = buildScreenshotPrompt(port, diff, screenshotDir, stashId);
|
|
916
|
+
const modelFlag = model === "sonnet" ? "sonnet" : "haiku";
|
|
917
|
+
const aiProcess = startAiProcess(processId, prompt, worktreePath, undefined, modelFlag);
|
|
918
|
+
let textOutput = "";
|
|
919
|
+
let timedOut = false;
|
|
920
|
+
const timeoutId = setTimeout(() => {
|
|
921
|
+
timedOut = true;
|
|
922
|
+
killAiProcess(processId);
|
|
923
|
+
}, timeout);
|
|
924
|
+
try {
|
|
925
|
+
for await (const chunk of parseClaudeStream(aiProcess.process)) {
|
|
926
|
+
if (chunk.type === "text") {
|
|
927
|
+
textOutput += chunk.content;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
await aiProcess.process.exited;
|
|
931
|
+
} catch (err) {
|
|
932
|
+
logger.warn("smart-screenshot", `AI process error for ${stashId}`, {
|
|
933
|
+
error: err instanceof Error ? err.message : String(err),
|
|
934
|
+
timedOut
|
|
935
|
+
});
|
|
936
|
+
} finally {
|
|
937
|
+
clearTimeout(timeoutId);
|
|
938
|
+
killAiProcess(processId);
|
|
939
|
+
}
|
|
940
|
+
const result = parseAiResult(textOutput);
|
|
941
|
+
if (!result || !result.screenshots || result.screenshots.length === 0) {
|
|
942
|
+
logger.info("smart-screenshot", `AI returned no screenshots for ${stashId}, falling back`);
|
|
943
|
+
const url = await captureScreenshot(port, projectPath, stashId);
|
|
944
|
+
return {
|
|
945
|
+
primary: url,
|
|
946
|
+
screenshots: [{ url, label: "Homepage", route: "/", isPrimary: true }]
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
const screenshots = [];
|
|
950
|
+
let primaryUrl = "";
|
|
951
|
+
for (const shot of result.screenshots) {
|
|
952
|
+
const filename = shot.path.split("/").pop() || "";
|
|
953
|
+
if (!existsSync6(shot.path)) {
|
|
954
|
+
logger.warn("smart-screenshot", `Screenshot file not found: ${shot.path}`);
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
const url = `/api/screenshots/${filename}`;
|
|
958
|
+
const screenshot = {
|
|
959
|
+
url,
|
|
960
|
+
label: shot.label,
|
|
961
|
+
route: shot.route,
|
|
962
|
+
isPrimary: shot.isPrimary
|
|
963
|
+
};
|
|
964
|
+
screenshots.push(screenshot);
|
|
965
|
+
if (shot.isPrimary)
|
|
966
|
+
primaryUrl = url;
|
|
967
|
+
}
|
|
968
|
+
if (screenshots.length === 0) {
|
|
969
|
+
logger.info("smart-screenshot", `No valid screenshots for ${stashId}, falling back`);
|
|
970
|
+
const url = await captureScreenshot(port, projectPath, stashId);
|
|
971
|
+
return {
|
|
972
|
+
primary: url,
|
|
973
|
+
screenshots: [{ url, label: "Homepage", route: "/", isPrimary: true }]
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
if (!primaryUrl) {
|
|
977
|
+
primaryUrl = screenshots[0].url;
|
|
978
|
+
screenshots[0] = { ...screenshots[0], isPrimary: true };
|
|
979
|
+
}
|
|
980
|
+
logger.info("smart-screenshot", `Captured ${screenshots.length} smart screenshots for ${stashId}`);
|
|
981
|
+
return { primary: primaryUrl, screenshots };
|
|
982
|
+
}
|
|
983
|
+
|
|
802
984
|
// ../core/dist/prompt.js
|
|
803
985
|
function buildStashPrompt(component, sourceCode, userPrompt, directive) {
|
|
804
986
|
const parts = [
|
|
@@ -849,23 +1031,6 @@ async function waitForPort(port, timeout) {
|
|
|
849
1031
|
}
|
|
850
1032
|
throw new Error(`Port ${port} not ready within ${timeout}ms`);
|
|
851
1033
|
}
|
|
852
|
-
async function captureEphemeralScreenshot(worktreePath, projectPath, stashId, port) {
|
|
853
|
-
const devServer = spawn3({
|
|
854
|
-
cmd: ["npm", "run", "dev"],
|
|
855
|
-
cwd: worktreePath,
|
|
856
|
-
stdin: "ignore",
|
|
857
|
-
stdout: "pipe",
|
|
858
|
-
stderr: "pipe",
|
|
859
|
-
env: { ...process.env, PORT: String(port), BROWSER: "none" }
|
|
860
|
-
});
|
|
861
|
-
try {
|
|
862
|
-
await waitForPort(port, 60000);
|
|
863
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
864
|
-
return await captureScreenshot(port, projectPath, stashId);
|
|
865
|
-
} finally {
|
|
866
|
-
devServer.kill();
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
1034
|
async function allocatePort() {
|
|
870
1035
|
for (let port = 4010;port <= 4030; port++) {
|
|
871
1036
|
try {
|
|
@@ -883,8 +1048,8 @@ async function generate(opts) {
|
|
|
883
1048
|
const selectedDirectives = directives.slice(0, count);
|
|
884
1049
|
let sourceCode = "";
|
|
885
1050
|
if (component?.filePath) {
|
|
886
|
-
const sourceFile =
|
|
887
|
-
if (
|
|
1051
|
+
const sourceFile = join7(projectPath, component.filePath);
|
|
1052
|
+
if (existsSync7(sourceFile)) {
|
|
888
1053
|
sourceCode = readFileSync3(sourceFile, "utf-8");
|
|
889
1054
|
}
|
|
890
1055
|
}
|
|
@@ -906,6 +1071,7 @@ async function generate(opts) {
|
|
|
906
1071
|
worktreePath: worktree.path,
|
|
907
1072
|
port: null,
|
|
908
1073
|
screenshotUrl: null,
|
|
1074
|
+
screenshots: [],
|
|
909
1075
|
status: "generating",
|
|
910
1076
|
error: null,
|
|
911
1077
|
relatedTo: [],
|
|
@@ -930,7 +1096,7 @@ async function generate(opts) {
|
|
|
930
1096
|
});
|
|
931
1097
|
}
|
|
932
1098
|
await aiProcess.process.exited;
|
|
933
|
-
const wtGit =
|
|
1099
|
+
const wtGit = simpleGit3(worktree.path);
|
|
934
1100
|
try {
|
|
935
1101
|
await wtGit.add("-A");
|
|
936
1102
|
await wtGit.commit(`stashes: stash ${stashId}`);
|
|
@@ -953,15 +1119,40 @@ async function generate(opts) {
|
|
|
953
1119
|
try {
|
|
954
1120
|
const port = await allocatePort();
|
|
955
1121
|
const worktree = await worktreeManager.createForGeneration(`screenshot-${stash.id}`);
|
|
956
|
-
const screenshotGit =
|
|
1122
|
+
const screenshotGit = simpleGit3(worktree.path);
|
|
957
1123
|
await screenshotGit.checkout(["-f", stash.branch]);
|
|
958
|
-
const
|
|
1124
|
+
const devServer = spawn3({
|
|
1125
|
+
cmd: ["npm", "run", "dev"],
|
|
1126
|
+
cwd: worktree.path,
|
|
1127
|
+
stdin: "ignore",
|
|
1128
|
+
stdout: "pipe",
|
|
1129
|
+
stderr: "pipe",
|
|
1130
|
+
env: { ...process.env, PORT: String(port), BROWSER: "none" }
|
|
1131
|
+
});
|
|
1132
|
+
try {
|
|
1133
|
+
await waitForPort(port, 60000);
|
|
1134
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
1135
|
+
const mainGit = simpleGit3(projectPath);
|
|
1136
|
+
const parentBranch = (await mainGit.revparse(["HEAD"])).trim();
|
|
1137
|
+
const { primary, screenshots } = await captureSmartScreenshots({
|
|
1138
|
+
projectPath,
|
|
1139
|
+
stashId: stash.id,
|
|
1140
|
+
stashBranch: stash.branch,
|
|
1141
|
+
parentBranch,
|
|
1142
|
+
worktreePath: worktree.path,
|
|
1143
|
+
port,
|
|
1144
|
+
model: opts.screenshotModel,
|
|
1145
|
+
timeout: opts.screenshotTimeout
|
|
1146
|
+
});
|
|
1147
|
+
const updatedStash = { ...stash, screenshotUrl: primary, screenshots };
|
|
1148
|
+
persistence.saveStash(updatedStash);
|
|
1149
|
+
const idx = completedStashes.indexOf(stash);
|
|
1150
|
+
completedStashes[idx] = updatedStash;
|
|
1151
|
+
emit(onProgress, { type: "ready", stashId: stash.id, screenshotPath: primary });
|
|
1152
|
+
} finally {
|
|
1153
|
+
devServer.kill();
|
|
1154
|
+
}
|
|
959
1155
|
await worktreeManager.removeGeneration(`screenshot-${stash.id}`);
|
|
960
|
-
const updatedStash = { ...stash, screenshotUrl: screenshotPath };
|
|
961
|
-
persistence.saveStash(updatedStash);
|
|
962
|
-
const idx = completedStashes.indexOf(stash);
|
|
963
|
-
completedStashes[idx] = updatedStash;
|
|
964
|
-
emit(onProgress, { type: "ready", stashId: stash.id, screenshotPath });
|
|
965
1156
|
} catch (err) {
|
|
966
1157
|
logger.error("generation", `screenshot failed for ${stash.id}`, {
|
|
967
1158
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -973,7 +1164,7 @@ async function generate(opts) {
|
|
|
973
1164
|
}
|
|
974
1165
|
// ../core/dist/vary.js
|
|
975
1166
|
var {spawn: spawn4 } = globalThis.Bun;
|
|
976
|
-
import
|
|
1167
|
+
import simpleGit4 from "simple-git";
|
|
977
1168
|
function emit2(onProgress, event) {
|
|
978
1169
|
if (onProgress)
|
|
979
1170
|
onProgress(event);
|
|
@@ -1031,6 +1222,7 @@ async function vary(opts) {
|
|
|
1031
1222
|
worktreePath: worktree.path,
|
|
1032
1223
|
port: null,
|
|
1033
1224
|
screenshotUrl: null,
|
|
1225
|
+
screenshots: [],
|
|
1034
1226
|
status: "generating",
|
|
1035
1227
|
error: null,
|
|
1036
1228
|
relatedTo: [sourceStashId],
|
|
@@ -1050,7 +1242,7 @@ async function vary(opts) {
|
|
|
1050
1242
|
});
|
|
1051
1243
|
}
|
|
1052
1244
|
await aiProcess.process.exited;
|
|
1053
|
-
const wtGit =
|
|
1245
|
+
const wtGit = simpleGit4(worktree.path);
|
|
1054
1246
|
try {
|
|
1055
1247
|
await wtGit.add("-A");
|
|
1056
1248
|
await wtGit.commit(`stashes: vary ${stashId} from ${sourceStashId}`);
|
|
@@ -1058,10 +1250,11 @@ async function vary(opts) {
|
|
|
1058
1250
|
await worktreeManager.removeGeneration(stashId);
|
|
1059
1251
|
emit2(onProgress, { type: "screenshotting", stashId });
|
|
1060
1252
|
let screenshotPath = "";
|
|
1253
|
+
let screenshots = [];
|
|
1061
1254
|
try {
|
|
1062
1255
|
const port = await allocatePort2();
|
|
1063
1256
|
const screenshotWorktree = await worktreeManager.createForGeneration(`screenshot-${stashId}`);
|
|
1064
|
-
const screenshotGit =
|
|
1257
|
+
const screenshotGit = simpleGit4(screenshotWorktree.path);
|
|
1065
1258
|
await screenshotGit.checkout(["-f", stash.branch]);
|
|
1066
1259
|
const devServer = spawn4({
|
|
1067
1260
|
cmd: ["npm", "run", "dev"],
|
|
@@ -1074,7 +1267,18 @@ async function vary(opts) {
|
|
|
1074
1267
|
try {
|
|
1075
1268
|
await waitForPort2(port, 60000);
|
|
1076
1269
|
await new Promise((r) => setTimeout(r, 2000));
|
|
1077
|
-
|
|
1270
|
+
const result = await captureSmartScreenshots({
|
|
1271
|
+
projectPath,
|
|
1272
|
+
stashId,
|
|
1273
|
+
stashBranch: stash.branch,
|
|
1274
|
+
parentBranch: sourceStash.branch,
|
|
1275
|
+
worktreePath: screenshotWorktree.path,
|
|
1276
|
+
port,
|
|
1277
|
+
model: opts.screenshotModel,
|
|
1278
|
+
timeout: opts.screenshotTimeout
|
|
1279
|
+
});
|
|
1280
|
+
screenshotPath = result.primary;
|
|
1281
|
+
screenshots = [...result.screenshots];
|
|
1078
1282
|
} finally {
|
|
1079
1283
|
devServer.kill();
|
|
1080
1284
|
}
|
|
@@ -1084,7 +1288,7 @@ async function vary(opts) {
|
|
|
1084
1288
|
error: err instanceof Error ? err.message : String(err)
|
|
1085
1289
|
});
|
|
1086
1290
|
}
|
|
1087
|
-
const readyStash = { ...stash, status: "ready", screenshotUrl: screenshotPath || null };
|
|
1291
|
+
const readyStash = { ...stash, status: "ready", screenshotUrl: screenshotPath || null, screenshots };
|
|
1088
1292
|
persistence.saveStash(readyStash);
|
|
1089
1293
|
emit2(onProgress, { type: "ready", stashId, screenshotPath });
|
|
1090
1294
|
return readyStash;
|
|
@@ -1108,7 +1312,7 @@ async function apply(opts) {
|
|
|
1108
1312
|
logger.info("apply", `stash ${stashId} applied and worktrees cleaned up`);
|
|
1109
1313
|
}
|
|
1110
1314
|
// ../core/dist/manage.js
|
|
1111
|
-
import
|
|
1315
|
+
import simpleGit5 from "simple-git";
|
|
1112
1316
|
async function list(projectPath) {
|
|
1113
1317
|
const persistence = new PersistenceService(projectPath);
|
|
1114
1318
|
const projects = persistence.listProjects();
|
|
@@ -1129,7 +1333,7 @@ async function remove(projectPath, stashId) {
|
|
|
1129
1333
|
}
|
|
1130
1334
|
}
|
|
1131
1335
|
try {
|
|
1132
|
-
const git =
|
|
1336
|
+
const git = simpleGit5(projectPath);
|
|
1133
1337
|
await git.raw(["branch", "-D", `stashes/${stashId}`]);
|
|
1134
1338
|
} catch {}
|
|
1135
1339
|
logger.info("manage", `removed stash: ${stashId}`);
|
|
@@ -1140,8 +1344,8 @@ async function cleanup(projectPath) {
|
|
|
1140
1344
|
logger.info("manage", "cleanup complete");
|
|
1141
1345
|
}
|
|
1142
1346
|
// ../server/dist/services/stash-service.js
|
|
1143
|
-
import { readFileSync as readFileSync4, existsSync as
|
|
1144
|
-
import { join as
|
|
1347
|
+
import { readFileSync as readFileSync4, existsSync as existsSync8 } from "fs";
|
|
1348
|
+
import { join as join8 } from "path";
|
|
1145
1349
|
|
|
1146
1350
|
// ../server/dist/services/preview-pool.js
|
|
1147
1351
|
class PreviewPool {
|
|
@@ -1333,7 +1537,10 @@ class StashService {
|
|
|
1333
1537
|
selectedComponent = null;
|
|
1334
1538
|
messageQueue = [];
|
|
1335
1539
|
isProcessingMessage = false;
|
|
1540
|
+
activeChatId = null;
|
|
1336
1541
|
chatSessions = new Map;
|
|
1542
|
+
stashPollTimer = null;
|
|
1543
|
+
knownStashIds = new Set;
|
|
1337
1544
|
constructor(projectPath, worktreeManager, persistence, broadcast) {
|
|
1338
1545
|
this.projectPath = projectPath;
|
|
1339
1546
|
this.worktreeManager = worktreeManager;
|
|
@@ -1341,6 +1548,9 @@ class StashService {
|
|
|
1341
1548
|
this.broadcast = broadcast;
|
|
1342
1549
|
this.previewPool = new PreviewPool(worktreeManager, broadcast);
|
|
1343
1550
|
}
|
|
1551
|
+
getActiveChatId() {
|
|
1552
|
+
return this.activeChatId;
|
|
1553
|
+
}
|
|
1344
1554
|
setSelectedComponent(component) {
|
|
1345
1555
|
this.selectedComponent = component;
|
|
1346
1556
|
if (component.filePath === "auto-detect") {
|
|
@@ -1395,8 +1605,10 @@ class StashService {
|
|
|
1395
1605
|
this.isProcessingMessage = true;
|
|
1396
1606
|
while (this.messageQueue.length > 0) {
|
|
1397
1607
|
const msg = this.messageQueue.shift();
|
|
1608
|
+
this.activeChatId = msg.chatId;
|
|
1398
1609
|
await this.processMessage(msg.projectId, msg.chatId, msg.message, msg.referenceStashIds, msg.componentContext);
|
|
1399
1610
|
}
|
|
1611
|
+
this.activeChatId = null;
|
|
1400
1612
|
this.isProcessingMessage = false;
|
|
1401
1613
|
}
|
|
1402
1614
|
async processMessage(projectId, chatId, message, referenceStashIds, componentContext) {
|
|
@@ -1404,8 +1616,8 @@ class StashService {
|
|
|
1404
1616
|
let sourceCode = "";
|
|
1405
1617
|
const filePath = component?.filePath || "";
|
|
1406
1618
|
if (filePath && filePath !== "auto-detect") {
|
|
1407
|
-
const sourceFile =
|
|
1408
|
-
if (
|
|
1619
|
+
const sourceFile = join8(this.projectPath, filePath);
|
|
1620
|
+
if (existsSync8(sourceFile)) {
|
|
1409
1621
|
sourceCode = readFileSync4(sourceFile, "utf-8");
|
|
1410
1622
|
}
|
|
1411
1623
|
}
|
|
@@ -1456,20 +1668,22 @@ ${sourceCode.substring(0, 3000)}
|
|
|
1456
1668
|
const aiProcess = startAiProcess("chat", chatPrompt, this.projectPath, existingSessionId);
|
|
1457
1669
|
let thinkingBuf = "";
|
|
1458
1670
|
let textBuf = "";
|
|
1459
|
-
const pendingMessages = [];
|
|
1460
1671
|
const now = new Date().toISOString();
|
|
1461
|
-
|
|
1672
|
+
const save = (msg) => {
|
|
1673
|
+
this.persistence.saveChatMessage(projectId, chatId, msg);
|
|
1674
|
+
};
|
|
1675
|
+
const flushThinking = () => {
|
|
1462
1676
|
if (!thinkingBuf)
|
|
1463
1677
|
return;
|
|
1464
|
-
|
|
1678
|
+
save({ id: crypto.randomUUID(), role: "assistant", content: thinkingBuf, type: "thinking", createdAt: now });
|
|
1465
1679
|
thinkingBuf = "";
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1680
|
+
};
|
|
1681
|
+
const flushText = () => {
|
|
1468
1682
|
if (!textBuf)
|
|
1469
1683
|
return;
|
|
1470
|
-
|
|
1684
|
+
save({ id: crypto.randomUUID(), role: "assistant", content: textBuf, type: "text", createdAt: now });
|
|
1471
1685
|
textBuf = "";
|
|
1472
|
-
}
|
|
1686
|
+
};
|
|
1473
1687
|
try {
|
|
1474
1688
|
for await (const chunk of parseClaudeStream(aiProcess.process)) {
|
|
1475
1689
|
if (chunk.type === "session_id" && chunk.sessionId) {
|
|
@@ -1503,7 +1717,7 @@ ${sourceCode.substring(0, 3000)}
|
|
|
1503
1717
|
toolName = parsed.tool ?? "unknown";
|
|
1504
1718
|
toolParams = parsed.input ?? {};
|
|
1505
1719
|
} catch {}
|
|
1506
|
-
|
|
1720
|
+
save({
|
|
1507
1721
|
id: crypto.randomUUID(),
|
|
1508
1722
|
role: "assistant",
|
|
1509
1723
|
content: chunk.content,
|
|
@@ -1522,7 +1736,11 @@ ${sourceCode.substring(0, 3000)}
|
|
|
1522
1736
|
toolParams,
|
|
1523
1737
|
toolStatus: "running"
|
|
1524
1738
|
});
|
|
1739
|
+
if (toolName.includes("stashes_generate") || toolName.includes("stashes_vary")) {
|
|
1740
|
+
this.startStashPoll(projectId, chatId);
|
|
1741
|
+
}
|
|
1525
1742
|
} else if (chunk.type === "tool_result") {
|
|
1743
|
+
this.stopStashPoll();
|
|
1526
1744
|
let toolResult = chunk.content;
|
|
1527
1745
|
let isError = false;
|
|
1528
1746
|
try {
|
|
@@ -1530,7 +1748,7 @@ ${sourceCode.substring(0, 3000)}
|
|
|
1530
1748
|
toolResult = parsed.result ?? chunk.content;
|
|
1531
1749
|
isError = !!parsed.is_error;
|
|
1532
1750
|
} catch {}
|
|
1533
|
-
|
|
1751
|
+
save({
|
|
1534
1752
|
id: crypto.randomUUID(),
|
|
1535
1753
|
role: "assistant",
|
|
1536
1754
|
content: chunk.content,
|
|
@@ -1552,10 +1770,7 @@ ${sourceCode.substring(0, 3000)}
|
|
|
1552
1770
|
await aiProcess.process.exited;
|
|
1553
1771
|
flushThinking();
|
|
1554
1772
|
flushText();
|
|
1555
|
-
|
|
1556
|
-
this.persistence.saveChatMessage(projectId, chatId, msg);
|
|
1557
|
-
}
|
|
1558
|
-
this.syncStashesFromDisk(projectId);
|
|
1773
|
+
this.syncStashesFromDisk(projectId, chatId);
|
|
1559
1774
|
} catch (err) {
|
|
1560
1775
|
this.broadcast({
|
|
1561
1776
|
type: "ai_stream",
|
|
@@ -1564,12 +1779,17 @@ ${sourceCode.substring(0, 3000)}
|
|
|
1564
1779
|
source: "chat"
|
|
1565
1780
|
});
|
|
1566
1781
|
} finally {
|
|
1782
|
+
this.stopStashPoll();
|
|
1567
1783
|
killAiProcess("chat");
|
|
1568
1784
|
}
|
|
1569
1785
|
}
|
|
1570
|
-
syncStashesFromDisk(projectId) {
|
|
1786
|
+
syncStashesFromDisk(projectId, chatId) {
|
|
1571
1787
|
const diskStashes = this.persistence.listStashes(projectId);
|
|
1572
1788
|
for (const stash of diskStashes) {
|
|
1789
|
+
if (chatId && !stash.originChatId && !this.knownStashIds.has(stash.id)) {
|
|
1790
|
+
const updated = { ...stash, originChatId: chatId };
|
|
1791
|
+
this.persistence.saveStash(updated);
|
|
1792
|
+
}
|
|
1573
1793
|
this.broadcast({
|
|
1574
1794
|
type: "stash:status",
|
|
1575
1795
|
stashId: stash.id,
|
|
@@ -1580,9 +1800,52 @@ ${sourceCode.substring(0, 3000)}
|
|
|
1580
1800
|
this.broadcast({
|
|
1581
1801
|
type: "stash:screenshot",
|
|
1582
1802
|
stashId: stash.id,
|
|
1583
|
-
url: stash.screenshotUrl
|
|
1803
|
+
url: stash.screenshotUrl,
|
|
1804
|
+
screenshots: stash.screenshots?.length ? [...stash.screenshots] : undefined
|
|
1584
1805
|
});
|
|
1585
1806
|
}
|
|
1807
|
+
this.knownStashIds.add(stash.id);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
startStashPoll(projectId, chatId) {
|
|
1811
|
+
this.stopStashPoll();
|
|
1812
|
+
const lastStatus = new Map;
|
|
1813
|
+
for (const s of this.persistence.listStashes(projectId)) {
|
|
1814
|
+
this.knownStashIds.add(s.id);
|
|
1815
|
+
lastStatus.set(s.id, s.status);
|
|
1816
|
+
}
|
|
1817
|
+
this.stashPollTimer = setInterval(() => {
|
|
1818
|
+
const stashes = this.persistence.listStashes(projectId);
|
|
1819
|
+
for (const stash of stashes) {
|
|
1820
|
+
const prev = lastStatus.get(stash.id);
|
|
1821
|
+
if (!prev || prev !== stash.status) {
|
|
1822
|
+
if (chatId && !stash.originChatId && !prev) {
|
|
1823
|
+
this.persistence.saveStash({ ...stash, originChatId: chatId });
|
|
1824
|
+
}
|
|
1825
|
+
this.broadcast({
|
|
1826
|
+
type: "stash:status",
|
|
1827
|
+
stashId: stash.id,
|
|
1828
|
+
status: stash.status,
|
|
1829
|
+
number: stash.number
|
|
1830
|
+
});
|
|
1831
|
+
if (stash.screenshotUrl && prev !== stash.status) {
|
|
1832
|
+
this.broadcast({
|
|
1833
|
+
type: "stash:screenshot",
|
|
1834
|
+
stashId: stash.id,
|
|
1835
|
+
url: stash.screenshotUrl,
|
|
1836
|
+
screenshots: stash.screenshots?.length ? [...stash.screenshots] : undefined
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
lastStatus.set(stash.id, stash.status);
|
|
1840
|
+
this.knownStashIds.add(stash.id);
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
}, 2000);
|
|
1844
|
+
}
|
|
1845
|
+
stopStashPoll() {
|
|
1846
|
+
if (this.stashPollTimer) {
|
|
1847
|
+
clearInterval(this.stashPollTimer);
|
|
1848
|
+
this.stashPollTimer = null;
|
|
1586
1849
|
}
|
|
1587
1850
|
}
|
|
1588
1851
|
progressToBroadcast(event) {
|
|
@@ -1597,7 +1860,12 @@ ${sourceCode.substring(0, 3000)}
|
|
|
1597
1860
|
..."number" in event ? { number: event.number } : {}
|
|
1598
1861
|
});
|
|
1599
1862
|
if (event.type === "ready" && "screenshotPath" in event && event.screenshotPath) {
|
|
1600
|
-
this.broadcast({
|
|
1863
|
+
this.broadcast({
|
|
1864
|
+
type: "stash:screenshot",
|
|
1865
|
+
stashId: event.stashId,
|
|
1866
|
+
url: event.screenshotPath,
|
|
1867
|
+
screenshots: "screenshots" in event ? event.screenshots : undefined
|
|
1868
|
+
});
|
|
1601
1869
|
}
|
|
1602
1870
|
break;
|
|
1603
1871
|
case "error":
|
|
@@ -1611,6 +1879,7 @@ ${sourceCode.substring(0, 3000)}
|
|
|
1611
1879
|
}
|
|
1612
1880
|
}
|
|
1613
1881
|
async generate(projectId, prompt, stashCount = DEFAULT_STASH_COUNT, referenceStashIds) {
|
|
1882
|
+
const settings = this.persistence.getProjectSettings(projectId);
|
|
1614
1883
|
let enrichedPrompt = prompt;
|
|
1615
1884
|
if (referenceStashIds?.length) {
|
|
1616
1885
|
const refDescriptions = referenceStashIds.map((id) => this.persistence.getStash(projectId, id)).filter(Boolean).map((s) => `- "${s.prompt}"${s.componentPath ? ` (${s.componentPath})` : ""}`);
|
|
@@ -1628,14 +1897,27 @@ ${refDescriptions.join(`
|
|
|
1628
1897
|
prompt: enrichedPrompt,
|
|
1629
1898
|
component: this.selectedComponent ? { filePath: this.selectedComponent.filePath, exportName: this.selectedComponent.name } : undefined,
|
|
1630
1899
|
count: stashCount,
|
|
1900
|
+
screenshotModel: settings.screenshotModel,
|
|
1901
|
+
screenshotTimeout: settings.screenshotTimeout,
|
|
1631
1902
|
onProgress: (event) => this.progressToBroadcast(event)
|
|
1632
1903
|
});
|
|
1633
1904
|
}
|
|
1634
1905
|
async vary(sourceStashId, prompt) {
|
|
1906
|
+
let projectId = "";
|
|
1907
|
+
for (const project of this.persistence.listProjects()) {
|
|
1908
|
+
const stashes = this.persistence.listStashes(project.id);
|
|
1909
|
+
if (stashes.find((s) => s.id === sourceStashId)) {
|
|
1910
|
+
projectId = project.id;
|
|
1911
|
+
break;
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
const settings = projectId ? this.persistence.getProjectSettings(projectId) : {};
|
|
1635
1915
|
await vary({
|
|
1636
1916
|
projectPath: this.projectPath,
|
|
1637
1917
|
sourceStashId,
|
|
1638
1918
|
prompt,
|
|
1919
|
+
screenshotModel: settings.screenshotModel,
|
|
1920
|
+
screenshotTimeout: settings.screenshotTimeout,
|
|
1639
1921
|
onProgress: (event) => this.progressToBroadcast(event)
|
|
1640
1922
|
});
|
|
1641
1923
|
}
|
|
@@ -1690,6 +1972,10 @@ function createWebSocketHandler(projectPath, userDevPort, appProxyPort) {
|
|
|
1690
1972
|
projectId: project.id,
|
|
1691
1973
|
projectName: project.name
|
|
1692
1974
|
}));
|
|
1975
|
+
const activeChatId = stashService.getActiveChatId();
|
|
1976
|
+
if (activeChatId) {
|
|
1977
|
+
ws.send(JSON.stringify({ type: "processing", chatId: activeChatId }));
|
|
1978
|
+
}
|
|
1693
1979
|
},
|
|
1694
1980
|
async message(ws, message) {
|
|
1695
1981
|
const raw = typeof message === "string" ? message : new TextDecoder().decode(message);
|
|
@@ -1749,105 +2035,93 @@ function createWebSocketHandler(projectPath, userDevPort, appProxyPort) {
|
|
|
1749
2035
|
}
|
|
1750
2036
|
|
|
1751
2037
|
// ../server/dist/services/app-proxy.js
|
|
2038
|
+
import { spawn as spawn5 } from "child_process";
|
|
1752
2039
|
function startAppProxy(userDevPort, proxyPort, injectOverlay) {
|
|
1753
|
-
const
|
|
1754
|
-
const
|
|
1755
|
-
const
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
const contentType = response.headers.get("content-type") || "";
|
|
1787
|
-
const respHeaders = new Headers(response.headers);
|
|
1788
|
-
if (response.status >= 300 && response.status < 400) {
|
|
1789
|
-
const location = respHeaders.get("location");
|
|
1790
|
-
if (location?.startsWith(upstreamOrigin)) {
|
|
1791
|
-
respHeaders.set("location", proxyOrigin + location.substring(upstreamOrigin.length));
|
|
1792
|
-
}
|
|
1793
|
-
}
|
|
1794
|
-
respHeaders.delete("content-encoding");
|
|
1795
|
-
return new Response(response.body, {
|
|
1796
|
-
status: response.status,
|
|
1797
|
-
headers: respHeaders
|
|
1798
|
-
});
|
|
1799
|
-
} catch (err) {
|
|
1800
|
-
return new Response(JSON.stringify({ error: "Proxy failed", detail: String(err) }), {
|
|
1801
|
-
status: 502,
|
|
1802
|
-
headers: { "content-type": "application/json" }
|
|
1803
|
-
});
|
|
1804
|
-
}
|
|
1805
|
-
},
|
|
1806
|
-
websocket: {
|
|
1807
|
-
open(ws) {
|
|
1808
|
-
const { data } = ws;
|
|
1809
|
-
const upstream = new WebSocket(`ws://localhost:${userDevPort}${data.path}`);
|
|
1810
|
-
upstream.addEventListener("open", () => {
|
|
1811
|
-
data.upstream = upstream;
|
|
1812
|
-
data.ready = true;
|
|
1813
|
-
for (const msg of data.buffer) {
|
|
1814
|
-
upstream.send(msg);
|
|
1815
|
-
}
|
|
1816
|
-
data.buffer = [];
|
|
1817
|
-
});
|
|
1818
|
-
upstream.addEventListener("message", (event) => {
|
|
1819
|
-
try {
|
|
1820
|
-
ws.sendText(typeof event.data === "string" ? event.data : String(event.data));
|
|
1821
|
-
} catch {}
|
|
1822
|
-
});
|
|
1823
|
-
upstream.addEventListener("close", () => {
|
|
1824
|
-
try {
|
|
1825
|
-
ws.close();
|
|
1826
|
-
} catch {}
|
|
1827
|
-
});
|
|
1828
|
-
upstream.addEventListener("error", () => {
|
|
1829
|
-
try {
|
|
1830
|
-
ws.close();
|
|
1831
|
-
} catch {}
|
|
1832
|
-
});
|
|
1833
|
-
},
|
|
1834
|
-
message(ws, msg) {
|
|
1835
|
-
const { data } = ws;
|
|
1836
|
-
if (data.ready && data.upstream) {
|
|
1837
|
-
data.upstream.send(typeof msg === "string" ? msg : new Uint8Array(msg));
|
|
1838
|
-
} else {
|
|
1839
|
-
data.buffer.push(typeof msg === "string" ? msg : new Uint8Array(msg).buffer);
|
|
2040
|
+
const overlayScript = injectOverlay("", userDevPort, proxyPort);
|
|
2041
|
+
const overlayEscaped = JSON.stringify(overlayScript);
|
|
2042
|
+
const proxyScript = `
|
|
2043
|
+
const http = require('http');
|
|
2044
|
+
const net = require('net');
|
|
2045
|
+
const zlib = require('zlib');
|
|
2046
|
+
const UPSTREAM = ${userDevPort};
|
|
2047
|
+
const OVERLAY = ${overlayEscaped};
|
|
2048
|
+
|
|
2049
|
+
const server = http.createServer((clientReq, clientRes) => {
|
|
2050
|
+
const opts = {
|
|
2051
|
+
hostname: 'localhost',
|
|
2052
|
+
port: UPSTREAM,
|
|
2053
|
+
path: clientReq.url,
|
|
2054
|
+
method: clientReq.method,
|
|
2055
|
+
headers: clientReq.headers,
|
|
2056
|
+
};
|
|
2057
|
+
const proxyReq = http.request(opts, (proxyRes) => {
|
|
2058
|
+
const ct = proxyRes.headers['content-type'] || '';
|
|
2059
|
+
if (ct.includes('text/html')) {
|
|
2060
|
+
// Buffer HTML to inject overlay
|
|
2061
|
+
const chunks = [];
|
|
2062
|
+
proxyRes.on('data', c => chunks.push(c));
|
|
2063
|
+
proxyRes.on('end', () => {
|
|
2064
|
+
let html = Buffer.concat(chunks);
|
|
2065
|
+
const enc = proxyRes.headers['content-encoding'];
|
|
2066
|
+
// Decompress if needed
|
|
2067
|
+
if (enc === 'gzip') {
|
|
2068
|
+
try { html = zlib.gunzipSync(html); } catch {}
|
|
2069
|
+
} else if (enc === 'br') {
|
|
2070
|
+
try { html = zlib.brotliDecompressSync(html); } catch {}
|
|
2071
|
+
} else if (enc === 'deflate') {
|
|
2072
|
+
try { html = zlib.inflateSync(html); } catch {}
|
|
1840
2073
|
}
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
2074
|
+
const hdrs = { ...proxyRes.headers };
|
|
2075
|
+
delete hdrs['content-length'];
|
|
2076
|
+
delete hdrs['content-encoding'];
|
|
2077
|
+
delete hdrs['transfer-encoding'];
|
|
2078
|
+
clientRes.writeHead(proxyRes.statusCode, hdrs);
|
|
2079
|
+
clientRes.write(html);
|
|
2080
|
+
clientRes.end(OVERLAY);
|
|
2081
|
+
});
|
|
2082
|
+
} else {
|
|
2083
|
+
// Non-HTML: stream through unchanged
|
|
2084
|
+
clientRes.writeHead(proxyRes.statusCode, proxyRes.headers);
|
|
2085
|
+
proxyRes.pipe(clientRes);
|
|
1847
2086
|
}
|
|
2087
|
+
proxyRes.on('error', () => clientRes.end());
|
|
1848
2088
|
});
|
|
1849
|
-
|
|
1850
|
-
|
|
2089
|
+
proxyReq.on('error', () => { try { clientRes.writeHead(502); clientRes.end(); } catch {} });
|
|
2090
|
+
clientReq.pipe(proxyReq);
|
|
2091
|
+
});
|
|
2092
|
+
|
|
2093
|
+
// WebSocket upgrades: raw TCP pipe
|
|
2094
|
+
server.on('upgrade', (req, socket, head) => {
|
|
2095
|
+
const upstream = net.createConnection(UPSTREAM, 'localhost', () => {
|
|
2096
|
+
const lines = [req.method + ' ' + req.url + ' HTTP/1.1'];
|
|
2097
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
2098
|
+
lines.push(k + ': ' + (Array.isArray(v) ? v.join(', ') : v));
|
|
2099
|
+
}
|
|
2100
|
+
upstream.write(lines.join('\\r\\n') + '\\r\\n\\r\\n');
|
|
2101
|
+
if (head.length) upstream.write(head);
|
|
2102
|
+
socket.pipe(upstream);
|
|
2103
|
+
upstream.pipe(socket);
|
|
2104
|
+
});
|
|
2105
|
+
upstream.on('error', () => socket.destroy());
|
|
2106
|
+
socket.on('error', () => upstream.destroy());
|
|
2107
|
+
});
|
|
2108
|
+
|
|
2109
|
+
server.listen(${proxyPort}, () => {
|
|
2110
|
+
if (process.send) process.send('ready');
|
|
2111
|
+
});
|
|
2112
|
+
`;
|
|
2113
|
+
const child = spawn5("node", ["-e", proxyScript], {
|
|
2114
|
+
stdio: ["ignore", "inherit", "inherit", "ipc"]
|
|
2115
|
+
});
|
|
2116
|
+
child.on("error", (err) => {
|
|
2117
|
+
logger.error("proxy", `Failed to start proxy: ${err.message}`);
|
|
2118
|
+
});
|
|
2119
|
+
child.on("message", (msg) => {
|
|
2120
|
+
if (msg === "ready") {
|
|
2121
|
+
logger.info("proxy", `App proxy at http://localhost:${proxyPort} \u2192 http://localhost:${userDevPort}`);
|
|
2122
|
+
}
|
|
2123
|
+
});
|
|
2124
|
+
return child;
|
|
1851
2125
|
}
|
|
1852
2126
|
|
|
1853
2127
|
// ../server/dist/index.js
|
|
@@ -1864,12 +2138,12 @@ app2.route("/api", apiRoutes);
|
|
|
1864
2138
|
app2.get("/*", async (c) => {
|
|
1865
2139
|
const path = c.req.path;
|
|
1866
2140
|
const selfDir = dirname2(fileURLToPath(import.meta.url));
|
|
1867
|
-
const bundledWebDir =
|
|
1868
|
-
const monorepoWebDir =
|
|
1869
|
-
const webDistDir =
|
|
2141
|
+
const bundledWebDir = join9(selfDir, "web");
|
|
2142
|
+
const monorepoWebDir = join9(selfDir, "../../web/dist");
|
|
2143
|
+
const webDistDir = existsSync9(join9(bundledWebDir, "index.html")) ? bundledWebDir : monorepoWebDir;
|
|
1870
2144
|
const requestPath = path === "/" ? "/index.html" : path;
|
|
1871
|
-
const filePath =
|
|
1872
|
-
if (
|
|
2145
|
+
const filePath = join9(webDistDir, requestPath);
|
|
2146
|
+
if (existsSync9(filePath) && !filePath.includes("..")) {
|
|
1873
2147
|
const content = readFileSync5(filePath);
|
|
1874
2148
|
const ext = filePath.split(".").pop() || "";
|
|
1875
2149
|
const contentTypes = {
|
|
@@ -1888,8 +2162,8 @@ app2.get("/*", async (c) => {
|
|
|
1888
2162
|
headers: { "content-type": contentTypes[ext] || "application/octet-stream" }
|
|
1889
2163
|
});
|
|
1890
2164
|
}
|
|
1891
|
-
const indexPath =
|
|
1892
|
-
if (
|
|
2165
|
+
const indexPath = join9(webDistDir, "index.html");
|
|
2166
|
+
if (existsSync9(indexPath)) {
|
|
1893
2167
|
const html = readFileSync5(indexPath, "utf-8");
|
|
1894
2168
|
return new Response(html, {
|
|
1895
2169
|
headers: { "content-type": "text/html; charset=utf-8" }
|
|
@@ -1925,7 +2199,7 @@ function startServer(projectPath, userDevPort, port = STASHES_PORT) {
|
|
|
1925
2199
|
logger.info("server", `Project: ${projectPath}`);
|
|
1926
2200
|
return server;
|
|
1927
2201
|
}
|
|
1928
|
-
function injectOverlayScript(html,
|
|
2202
|
+
function injectOverlayScript(html, _upstreamPort, _proxyPort) {
|
|
1929
2203
|
const overlayScript = `
|
|
1930
2204
|
<script data-stashes-overlay>
|
|
1931
2205
|
(function() {
|
|
@@ -1933,35 +2207,6 @@ function injectOverlayScript(html, upstreamPort, proxyPort) {
|
|
|
1933
2207
|
var pickerEnabled = false;
|
|
1934
2208
|
var precisionMode = false;
|
|
1935
2209
|
|
|
1936
|
-
// Rewrite cross-origin requests to the upstream dev server through the proxy
|
|
1937
|
-
var upstreamOrigin = 'http://localhost:${upstreamPort}';
|
|
1938
|
-
var proxyOrigin = window.location.origin;
|
|
1939
|
-
if (proxyOrigin !== upstreamOrigin) {
|
|
1940
|
-
function rewriteUrl(url) {
|
|
1941
|
-
if (typeof url === 'string' && (url.startsWith(upstreamOrigin + '/') || url === upstreamOrigin)) {
|
|
1942
|
-
return proxyOrigin + url.substring(upstreamOrigin.length);
|
|
1943
|
-
}
|
|
1944
|
-
return url;
|
|
1945
|
-
}
|
|
1946
|
-
var origFetch = window.fetch;
|
|
1947
|
-
window.fetch = function(input, init) {
|
|
1948
|
-
if (typeof input === 'string') {
|
|
1949
|
-
input = rewriteUrl(input);
|
|
1950
|
-
} else if (input instanceof Request) {
|
|
1951
|
-
var rewritten = rewriteUrl(input.url);
|
|
1952
|
-
if (rewritten !== input.url) { input = new Request(rewritten, input); }
|
|
1953
|
-
}
|
|
1954
|
-
return origFetch.call(window, input, init);
|
|
1955
|
-
};
|
|
1956
|
-
var origXhrOpen = XMLHttpRequest.prototype.open;
|
|
1957
|
-
XMLHttpRequest.prototype.open = function() {
|
|
1958
|
-
if (arguments.length >= 2 && typeof arguments[1] === 'string') {
|
|
1959
|
-
arguments[1] = rewriteUrl(arguments[1]);
|
|
1960
|
-
}
|
|
1961
|
-
return origXhrOpen.apply(this, arguments);
|
|
1962
|
-
};
|
|
1963
|
-
}
|
|
1964
|
-
|
|
1965
2210
|
function createOverlay() {
|
|
1966
2211
|
var overlay = document.createElement('div');
|
|
1967
2212
|
overlay.id = 'stashes-highlight';
|
|
@@ -2137,11 +2382,11 @@ function injectOverlayScript(html, upstreamPort, proxyPort) {
|
|
|
2137
2382
|
}
|
|
2138
2383
|
|
|
2139
2384
|
// ../server/dist/services/detector.js
|
|
2140
|
-
import { existsSync as
|
|
2141
|
-
import { join as
|
|
2385
|
+
import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
|
|
2386
|
+
import { join as join10 } from "path";
|
|
2142
2387
|
function detectFramework(projectPath) {
|
|
2143
|
-
const packageJsonPath =
|
|
2144
|
-
if (!
|
|
2388
|
+
const packageJsonPath = join10(projectPath, "package.json");
|
|
2389
|
+
if (!existsSync10(packageJsonPath)) {
|
|
2145
2390
|
return {
|
|
2146
2391
|
framework: "unknown",
|
|
2147
2392
|
devCommand: "npm run dev",
|
|
@@ -2203,7 +2448,7 @@ function getDevCommand(packageJson, fallback) {
|
|
|
2203
2448
|
}
|
|
2204
2449
|
function findConfig(projectPath, candidates) {
|
|
2205
2450
|
for (const candidate of candidates) {
|
|
2206
|
-
if (
|
|
2451
|
+
if (existsSync10(join10(projectPath, candidate))) {
|
|
2207
2452
|
return candidate;
|
|
2208
2453
|
}
|
|
2209
2454
|
}
|
|
@@ -2392,8 +2637,8 @@ Cleaning up all stashes and worktrees...`);
|
|
|
2392
2637
|
}
|
|
2393
2638
|
|
|
2394
2639
|
// src/commands/setup.ts
|
|
2395
|
-
import { existsSync as
|
|
2396
|
-
import { dirname as dirname3, join as
|
|
2640
|
+
import { existsSync as existsSync11, readFileSync as readFileSync7, writeFileSync as writeFileSync2, mkdirSync as mkdirSync5 } from "fs";
|
|
2641
|
+
import { dirname as dirname3, join as join11 } from "path";
|
|
2397
2642
|
import { homedir } from "os";
|
|
2398
2643
|
import * as p from "@clack/prompts";
|
|
2399
2644
|
import pc from "picocolors";
|
|
@@ -2412,60 +2657,60 @@ var MCP_ENTRY_ZED = {
|
|
|
2412
2657
|
};
|
|
2413
2658
|
function buildToolDefinitions() {
|
|
2414
2659
|
const home = homedir();
|
|
2415
|
-
const appSupport =
|
|
2660
|
+
const appSupport = join11(home, "Library", "Application Support");
|
|
2416
2661
|
return [
|
|
2417
2662
|
{
|
|
2418
2663
|
id: "claude-code",
|
|
2419
2664
|
name: "Claude Code",
|
|
2420
|
-
configPath:
|
|
2665
|
+
configPath: join11(home, ".claude.json"),
|
|
2421
2666
|
serversKey: "mcpServers",
|
|
2422
2667
|
format: "standard",
|
|
2423
|
-
detect: () =>
|
|
2668
|
+
detect: () => existsSync11(join11(home, ".claude.json")) || existsSync11(join11(home, ".claude"))
|
|
2424
2669
|
},
|
|
2425
2670
|
{
|
|
2426
2671
|
id: "claude-desktop",
|
|
2427
2672
|
name: "Claude Desktop",
|
|
2428
|
-
configPath:
|
|
2673
|
+
configPath: join11(appSupport, "Claude", "claude_desktop_config.json"),
|
|
2429
2674
|
serversKey: "mcpServers",
|
|
2430
2675
|
format: "standard",
|
|
2431
|
-
detect: () =>
|
|
2676
|
+
detect: () => existsSync11(join11(appSupport, "Claude")) || existsSync11("/Applications/Claude.app")
|
|
2432
2677
|
},
|
|
2433
2678
|
{
|
|
2434
2679
|
id: "vscode",
|
|
2435
2680
|
name: "VS Code",
|
|
2436
|
-
configPath:
|
|
2681
|
+
configPath: join11(appSupport, "Code", "User", "mcp.json"),
|
|
2437
2682
|
serversKey: "servers",
|
|
2438
2683
|
format: "standard",
|
|
2439
|
-
detect: () =>
|
|
2684
|
+
detect: () => existsSync11(join11(appSupport, "Code", "User"))
|
|
2440
2685
|
},
|
|
2441
2686
|
{
|
|
2442
2687
|
id: "cursor",
|
|
2443
2688
|
name: "Cursor",
|
|
2444
|
-
configPath:
|
|
2689
|
+
configPath: join11(home, ".cursor", "mcp.json"),
|
|
2445
2690
|
serversKey: "mcpServers",
|
|
2446
2691
|
format: "standard",
|
|
2447
|
-
detect: () =>
|
|
2692
|
+
detect: () => existsSync11(join11(home, ".cursor"))
|
|
2448
2693
|
},
|
|
2449
2694
|
{
|
|
2450
2695
|
id: "windsurf",
|
|
2451
2696
|
name: "Windsurf",
|
|
2452
|
-
configPath:
|
|
2697
|
+
configPath: join11(home, ".codeium", "windsurf", "mcp_config.json"),
|
|
2453
2698
|
serversKey: "mcpServers",
|
|
2454
2699
|
format: "standard",
|
|
2455
|
-
detect: () =>
|
|
2700
|
+
detect: () => existsSync11(join11(home, ".codeium", "windsurf"))
|
|
2456
2701
|
},
|
|
2457
2702
|
{
|
|
2458
2703
|
id: "zed",
|
|
2459
2704
|
name: "Zed",
|
|
2460
|
-
configPath:
|
|
2705
|
+
configPath: join11(appSupport, "Zed", "settings.json"),
|
|
2461
2706
|
serversKey: "context_servers",
|
|
2462
2707
|
format: "zed",
|
|
2463
|
-
detect: () =>
|
|
2708
|
+
detect: () => existsSync11(join11(appSupport, "Zed"))
|
|
2464
2709
|
}
|
|
2465
2710
|
];
|
|
2466
2711
|
}
|
|
2467
2712
|
function readJsonFile(path) {
|
|
2468
|
-
if (!
|
|
2713
|
+
if (!existsSync11(path))
|
|
2469
2714
|
return {};
|
|
2470
2715
|
try {
|
|
2471
2716
|
const raw = readFileSync7(path, "utf-8").trim();
|
|
@@ -2477,7 +2722,7 @@ function readJsonFile(path) {
|
|
|
2477
2722
|
}
|
|
2478
2723
|
}
|
|
2479
2724
|
function writeJsonFile(path, data) {
|
|
2480
|
-
|
|
2725
|
+
mkdirSync5(dirname3(path), { recursive: true });
|
|
2481
2726
|
writeFileSync2(path, JSON.stringify(data, null, 2) + `
|
|
2482
2727
|
`);
|
|
2483
2728
|
}
|
|
@@ -2618,13 +2863,13 @@ async function setupCommand(options) {
|
|
|
2618
2863
|
import { execFileSync, execSync } from "child_process";
|
|
2619
2864
|
import { writeFileSync as writeFileSync3, unlinkSync, chmodSync, readFileSync as readFileSync8 } from "fs";
|
|
2620
2865
|
import { tmpdir } from "os";
|
|
2621
|
-
import { join as
|
|
2866
|
+
import { join as join12, dirname as dirname4 } from "path";
|
|
2622
2867
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2623
2868
|
import * as p2 from "@clack/prompts";
|
|
2624
2869
|
import pc2 from "picocolors";
|
|
2625
2870
|
function getCurrentVersion() {
|
|
2626
2871
|
const selfDir = dirname4(fileURLToPath2(import.meta.url));
|
|
2627
|
-
const pkgPath =
|
|
2872
|
+
const pkgPath = join12(selfDir, "..", "package.json");
|
|
2628
2873
|
return JSON.parse(readFileSync8(pkgPath, "utf-8")).version;
|
|
2629
2874
|
}
|
|
2630
2875
|
function fetchLatestVersion() {
|
|
@@ -2701,7 +2946,7 @@ async function updateCommand() {
|
|
|
2701
2946
|
}
|
|
2702
2947
|
s.stop(`Removed from ${configuredTools.length} tool${configuredTools.length === 1 ? "" : "s"}`);
|
|
2703
2948
|
}
|
|
2704
|
-
const scriptPath =
|
|
2949
|
+
const scriptPath = join12(tmpdir(), `stashes-update-${Date.now()}.sh`);
|
|
2705
2950
|
writeFileSync3(scriptPath, buildUpdateScript(), "utf-8");
|
|
2706
2951
|
chmodSync(scriptPath, 493);
|
|
2707
2952
|
try {
|
|
@@ -2720,7 +2965,7 @@ Update failed. Try manually:`);
|
|
|
2720
2965
|
|
|
2721
2966
|
// src/index.ts
|
|
2722
2967
|
var selfDir = dirname5(fileURLToPath3(import.meta.url));
|
|
2723
|
-
var pkgPath =
|
|
2968
|
+
var pkgPath = join13(selfDir, "..", "package.json");
|
|
2724
2969
|
var version = JSON.parse(readFileSync9(pkgPath, "utf-8")).version;
|
|
2725
2970
|
var program = new Command;
|
|
2726
2971
|
program.name("stashes").description("Generate AI-powered UI design explorations in your project").version(version, "-v, --version");
|