stashes 0.1.26 → 0.1.28
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 +412 -215
- package/dist/mcp.js +395 -198
- package/dist/web/assets/{index-Db9gbHCa.js → index-DFTLJjq2.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/mcp.js
CHANGED
|
@@ -26,10 +26,10 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
26
26
|
import { z } from "zod";
|
|
27
27
|
|
|
28
28
|
// ../core/dist/generation.js
|
|
29
|
-
import { readFileSync as readFileSync2, existsSync as
|
|
30
|
-
import { join as
|
|
29
|
+
import { readFileSync as readFileSync2, existsSync as existsSync6 } from "fs";
|
|
30
|
+
import { join as join6 } from "path";
|
|
31
31
|
var {spawn: spawn3 } = globalThis.Bun;
|
|
32
|
-
import
|
|
32
|
+
import simpleGit3 from "simple-git";
|
|
33
33
|
// ../shared/dist/constants/index.js
|
|
34
34
|
var STASHES_PORT = 4000;
|
|
35
35
|
var DEFAULT_STASH_COUNT = 3;
|
|
@@ -424,6 +424,14 @@ class PersistenceService {
|
|
|
424
424
|
const filePath = join3(this.basePath, "projects", projectId, "stashes.json");
|
|
425
425
|
writeJson(filePath, stashes);
|
|
426
426
|
}
|
|
427
|
+
getProjectSettings(projectId) {
|
|
428
|
+
const filePath = join3(this.basePath, "projects", projectId, "settings.json");
|
|
429
|
+
return readJson(filePath, {});
|
|
430
|
+
}
|
|
431
|
+
saveProjectSettings(projectId, settings) {
|
|
432
|
+
const filePath = join3(this.basePath, "projects", projectId, "settings.json");
|
|
433
|
+
writeJson(filePath, settings);
|
|
434
|
+
}
|
|
427
435
|
listChats(projectId) {
|
|
428
436
|
const dir = join3(this.basePath, "projects", projectId, "chats");
|
|
429
437
|
if (!existsSync3(dir))
|
|
@@ -504,18 +512,22 @@ class PersistenceService {
|
|
|
504
512
|
var {spawn } = globalThis.Bun;
|
|
505
513
|
var CLAUDE_BIN = "/opt/homebrew/bin/claude";
|
|
506
514
|
var processes = new Map;
|
|
507
|
-
function startAiProcess(id, prompt, cwd, resumeSessionId) {
|
|
515
|
+
function startAiProcess(id, prompt, cwd, resumeSessionId, model) {
|
|
508
516
|
killAiProcess(id);
|
|
509
517
|
logger.info("claude", `spawning process: ${id}`, {
|
|
510
518
|
cwd,
|
|
511
519
|
promptLength: prompt.length,
|
|
512
520
|
promptPreview: prompt.substring(0, 100),
|
|
513
|
-
resumeSessionId
|
|
521
|
+
resumeSessionId,
|
|
522
|
+
model
|
|
514
523
|
});
|
|
515
524
|
const cmd = [CLAUDE_BIN, "-p", prompt, "--output-format=stream-json", "--verbose", "--dangerously-skip-permissions"];
|
|
516
525
|
if (resumeSessionId) {
|
|
517
526
|
cmd.push("--resume", resumeSessionId);
|
|
518
527
|
}
|
|
528
|
+
if (model) {
|
|
529
|
+
cmd.push("--model", model);
|
|
530
|
+
}
|
|
519
531
|
const proc = spawn({
|
|
520
532
|
cmd,
|
|
521
533
|
stdin: "ignore",
|
|
@@ -639,6 +651,11 @@ async function* parseClaudeStream(proc) {
|
|
|
639
651
|
}
|
|
640
652
|
}
|
|
641
653
|
|
|
654
|
+
// ../core/dist/smart-screenshot.js
|
|
655
|
+
import { join as join5 } from "path";
|
|
656
|
+
import { mkdirSync as mkdirSync4, existsSync as existsSync5 } from "fs";
|
|
657
|
+
import simpleGit2 from "simple-git";
|
|
658
|
+
|
|
642
659
|
// ../core/dist/screenshot.js
|
|
643
660
|
var {spawn: spawn2 } = globalThis.Bun;
|
|
644
661
|
import { join as join4 } from "path";
|
|
@@ -681,6 +698,171 @@ async function captureScreenshot(port, projectPath, stashId) {
|
|
|
681
698
|
return `/api/screenshots/${filename}`;
|
|
682
699
|
}
|
|
683
700
|
|
|
701
|
+
// ../core/dist/smart-screenshot.js
|
|
702
|
+
var SCREENSHOTS_DIR2 = ".stashes/screenshots";
|
|
703
|
+
var DEFAULT_TIMEOUT = 120000;
|
|
704
|
+
var DIFF_MAX_CHARS = 1e4;
|
|
705
|
+
async function getStashDiff(worktreePath, parentBranch) {
|
|
706
|
+
const git = simpleGit2(worktreePath);
|
|
707
|
+
try {
|
|
708
|
+
const diff = await git.diff([parentBranch, "HEAD"]);
|
|
709
|
+
if (diff.length <= DIFF_MAX_CHARS)
|
|
710
|
+
return diff;
|
|
711
|
+
const diffStat = await git.diff([parentBranch, "HEAD", "--stat"]);
|
|
712
|
+
const truncatedDiff = diff.substring(0, DIFF_MAX_CHARS);
|
|
713
|
+
return `## Changed files:
|
|
714
|
+
${diffStat}
|
|
715
|
+
|
|
716
|
+
## Partial diff (truncated):
|
|
717
|
+
${truncatedDiff}`;
|
|
718
|
+
} catch (err) {
|
|
719
|
+
logger.warn("smart-screenshot", `git diff failed, falling back`, {
|
|
720
|
+
error: err instanceof Error ? err.message : String(err)
|
|
721
|
+
});
|
|
722
|
+
return "";
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
function buildScreenshotPrompt(port, diff, screenshotDir, stashId) {
|
|
726
|
+
return [
|
|
727
|
+
"You are a screenshot assistant. A developer made UI changes to a web app.",
|
|
728
|
+
"Your job: navigate the running app, find the pages affected by the changes, and take screenshots.",
|
|
729
|
+
"",
|
|
730
|
+
`## The app is running at: http://localhost:${port}`,
|
|
731
|
+
"",
|
|
732
|
+
"## Git diff of changes:",
|
|
733
|
+
"```",
|
|
734
|
+
diff,
|
|
735
|
+
"```",
|
|
736
|
+
"",
|
|
737
|
+
"## Instructions:",
|
|
738
|
+
"1. Analyze the diff to identify which components/pages changed",
|
|
739
|
+
"2. Determine the URL routes where these changes are visible",
|
|
740
|
+
"3. For each route:",
|
|
741
|
+
" - Navigate to it using browser_navigate",
|
|
742
|
+
" - If the change is behind an interaction (tab, modal, accordion), perform that interaction",
|
|
743
|
+
" - Wait for the page to settle",
|
|
744
|
+
" - Take a screenshot using browser_take_screenshot",
|
|
745
|
+
"4. Decide which screenshot shows the MOST visually significant change \u2014 mark it as primary",
|
|
746
|
+
"",
|
|
747
|
+
`## Screenshot save paths:`,
|
|
748
|
+
`- Primary: ${join5(screenshotDir, `${stashId}.png`)}`,
|
|
749
|
+
`- Additional: ${join5(screenshotDir, `${stashId}-1.png`)}, ${join5(screenshotDir, `${stashId}-2.png`)}, etc.`,
|
|
750
|
+
"",
|
|
751
|
+
"## Output format (after all screenshots are taken):",
|
|
752
|
+
"Respond with ONLY this JSON (no markdown fences):",
|
|
753
|
+
"{",
|
|
754
|
+
' "screenshots": [',
|
|
755
|
+
" {",
|
|
756
|
+
` "path": "${join5(screenshotDir, `${stashId}.png`)}",`,
|
|
757
|
+
' "label": "Short description of what is shown",',
|
|
758
|
+
' "route": "/the-url-path",',
|
|
759
|
+
' "isPrimary": true',
|
|
760
|
+
" }",
|
|
761
|
+
" ]",
|
|
762
|
+
"}"
|
|
763
|
+
].join(`
|
|
764
|
+
`);
|
|
765
|
+
}
|
|
766
|
+
function parseAiResult(text) {
|
|
767
|
+
try {
|
|
768
|
+
return JSON.parse(text);
|
|
769
|
+
} catch {
|
|
770
|
+
const jsonMatch = text.match(/\{[\s\S]*"screenshots"[\s\S]*\}/);
|
|
771
|
+
if (jsonMatch) {
|
|
772
|
+
try {
|
|
773
|
+
return JSON.parse(jsonMatch[0]);
|
|
774
|
+
} catch {
|
|
775
|
+
return null;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return null;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
async function captureSmartScreenshots(opts) {
|
|
782
|
+
const { projectPath, stashId, stashBranch, parentBranch, worktreePath, port, model = "haiku", timeout = DEFAULT_TIMEOUT } = opts;
|
|
783
|
+
const screenshotDir = join5(projectPath, SCREENSHOTS_DIR2);
|
|
784
|
+
if (!existsSync5(screenshotDir)) {
|
|
785
|
+
mkdirSync4(screenshotDir, { recursive: true });
|
|
786
|
+
}
|
|
787
|
+
const diff = await getStashDiff(worktreePath, parentBranch);
|
|
788
|
+
if (!diff) {
|
|
789
|
+
logger.info("smart-screenshot", `No diff found for ${stashId}, using simple screenshot`);
|
|
790
|
+
const url = await captureScreenshot(port, projectPath, stashId);
|
|
791
|
+
return {
|
|
792
|
+
primary: url,
|
|
793
|
+
screenshots: [{ url, label: "Homepage", route: "/", isPrimary: true }]
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
const processId = `screenshot-ai-${stashId}`;
|
|
797
|
+
const prompt = buildScreenshotPrompt(port, diff, screenshotDir, stashId);
|
|
798
|
+
const modelFlag = model === "sonnet" ? "sonnet" : "haiku";
|
|
799
|
+
const aiProcess = startAiProcess(processId, prompt, worktreePath, undefined, modelFlag);
|
|
800
|
+
let textOutput = "";
|
|
801
|
+
let timedOut = false;
|
|
802
|
+
const timeoutId = setTimeout(() => {
|
|
803
|
+
timedOut = true;
|
|
804
|
+
killAiProcess(processId);
|
|
805
|
+
}, timeout);
|
|
806
|
+
try {
|
|
807
|
+
for await (const chunk of parseClaudeStream(aiProcess.process)) {
|
|
808
|
+
if (chunk.type === "text") {
|
|
809
|
+
textOutput += chunk.content;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
await aiProcess.process.exited;
|
|
813
|
+
} catch (err) {
|
|
814
|
+
logger.warn("smart-screenshot", `AI process error for ${stashId}`, {
|
|
815
|
+
error: err instanceof Error ? err.message : String(err),
|
|
816
|
+
timedOut
|
|
817
|
+
});
|
|
818
|
+
} finally {
|
|
819
|
+
clearTimeout(timeoutId);
|
|
820
|
+
killAiProcess(processId);
|
|
821
|
+
}
|
|
822
|
+
const result = parseAiResult(textOutput);
|
|
823
|
+
if (!result || !result.screenshots || result.screenshots.length === 0) {
|
|
824
|
+
logger.info("smart-screenshot", `AI returned no screenshots for ${stashId}, falling back`);
|
|
825
|
+
const url = await captureScreenshot(port, projectPath, stashId);
|
|
826
|
+
return {
|
|
827
|
+
primary: url,
|
|
828
|
+
screenshots: [{ url, label: "Homepage", route: "/", isPrimary: true }]
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
const screenshots = [];
|
|
832
|
+
let primaryUrl = "";
|
|
833
|
+
for (const shot of result.screenshots) {
|
|
834
|
+
const filename = shot.path.split("/").pop() || "";
|
|
835
|
+
if (!existsSync5(shot.path)) {
|
|
836
|
+
logger.warn("smart-screenshot", `Screenshot file not found: ${shot.path}`);
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
const url = `/api/screenshots/${filename}`;
|
|
840
|
+
const screenshot = {
|
|
841
|
+
url,
|
|
842
|
+
label: shot.label,
|
|
843
|
+
route: shot.route,
|
|
844
|
+
isPrimary: shot.isPrimary
|
|
845
|
+
};
|
|
846
|
+
screenshots.push(screenshot);
|
|
847
|
+
if (shot.isPrimary)
|
|
848
|
+
primaryUrl = url;
|
|
849
|
+
}
|
|
850
|
+
if (screenshots.length === 0) {
|
|
851
|
+
logger.info("smart-screenshot", `No valid screenshots for ${stashId}, falling back`);
|
|
852
|
+
const url = await captureScreenshot(port, projectPath, stashId);
|
|
853
|
+
return {
|
|
854
|
+
primary: url,
|
|
855
|
+
screenshots: [{ url, label: "Homepage", route: "/", isPrimary: true }]
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
if (!primaryUrl) {
|
|
859
|
+
primaryUrl = screenshots[0].url;
|
|
860
|
+
screenshots[0] = { ...screenshots[0], isPrimary: true };
|
|
861
|
+
}
|
|
862
|
+
logger.info("smart-screenshot", `Captured ${screenshots.length} smart screenshots for ${stashId}`);
|
|
863
|
+
return { primary: primaryUrl, screenshots };
|
|
864
|
+
}
|
|
865
|
+
|
|
684
866
|
// ../core/dist/prompt.js
|
|
685
867
|
function buildStashPrompt(component, sourceCode, userPrompt, directive) {
|
|
686
868
|
const parts = [
|
|
@@ -731,23 +913,6 @@ async function waitForPort(port, timeout) {
|
|
|
731
913
|
}
|
|
732
914
|
throw new Error(`Port ${port} not ready within ${timeout}ms`);
|
|
733
915
|
}
|
|
734
|
-
async function captureEphemeralScreenshot(worktreePath, projectPath, stashId, port) {
|
|
735
|
-
const devServer = spawn3({
|
|
736
|
-
cmd: ["npm", "run", "dev"],
|
|
737
|
-
cwd: worktreePath,
|
|
738
|
-
stdin: "ignore",
|
|
739
|
-
stdout: "pipe",
|
|
740
|
-
stderr: "pipe",
|
|
741
|
-
env: { ...process.env, PORT: String(port), BROWSER: "none" }
|
|
742
|
-
});
|
|
743
|
-
try {
|
|
744
|
-
await waitForPort(port, 60000);
|
|
745
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
746
|
-
return await captureScreenshot(port, projectPath, stashId);
|
|
747
|
-
} finally {
|
|
748
|
-
devServer.kill();
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
916
|
async function allocatePort() {
|
|
752
917
|
for (let port = 4010;port <= 4030; port++) {
|
|
753
918
|
try {
|
|
@@ -765,8 +930,8 @@ async function generate(opts) {
|
|
|
765
930
|
const selectedDirectives = directives.slice(0, count);
|
|
766
931
|
let sourceCode = "";
|
|
767
932
|
if (component?.filePath) {
|
|
768
|
-
const sourceFile =
|
|
769
|
-
if (
|
|
933
|
+
const sourceFile = join6(projectPath, component.filePath);
|
|
934
|
+
if (existsSync6(sourceFile)) {
|
|
770
935
|
sourceCode = readFileSync2(sourceFile, "utf-8");
|
|
771
936
|
}
|
|
772
937
|
}
|
|
@@ -788,6 +953,7 @@ async function generate(opts) {
|
|
|
788
953
|
worktreePath: worktree.path,
|
|
789
954
|
port: null,
|
|
790
955
|
screenshotUrl: null,
|
|
956
|
+
screenshots: [],
|
|
791
957
|
status: "generating",
|
|
792
958
|
error: null,
|
|
793
959
|
relatedTo: [],
|
|
@@ -812,7 +978,7 @@ async function generate(opts) {
|
|
|
812
978
|
});
|
|
813
979
|
}
|
|
814
980
|
await aiProcess.process.exited;
|
|
815
|
-
const wtGit =
|
|
981
|
+
const wtGit = simpleGit3(worktree.path);
|
|
816
982
|
try {
|
|
817
983
|
await wtGit.add("-A");
|
|
818
984
|
await wtGit.commit(`stashes: stash ${stashId}`);
|
|
@@ -835,15 +1001,40 @@ async function generate(opts) {
|
|
|
835
1001
|
try {
|
|
836
1002
|
const port = await allocatePort();
|
|
837
1003
|
const worktree = await worktreeManager.createForGeneration(`screenshot-${stash.id}`);
|
|
838
|
-
const screenshotGit =
|
|
1004
|
+
const screenshotGit = simpleGit3(worktree.path);
|
|
839
1005
|
await screenshotGit.checkout(["-f", stash.branch]);
|
|
840
|
-
const
|
|
1006
|
+
const devServer = spawn3({
|
|
1007
|
+
cmd: ["npm", "run", "dev"],
|
|
1008
|
+
cwd: worktree.path,
|
|
1009
|
+
stdin: "ignore",
|
|
1010
|
+
stdout: "pipe",
|
|
1011
|
+
stderr: "pipe",
|
|
1012
|
+
env: { ...process.env, PORT: String(port), BROWSER: "none" }
|
|
1013
|
+
});
|
|
1014
|
+
try {
|
|
1015
|
+
await waitForPort(port, 60000);
|
|
1016
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
1017
|
+
const mainGit = simpleGit3(projectPath);
|
|
1018
|
+
const parentBranch = (await mainGit.revparse(["HEAD"])).trim();
|
|
1019
|
+
const { primary, screenshots } = await captureSmartScreenshots({
|
|
1020
|
+
projectPath,
|
|
1021
|
+
stashId: stash.id,
|
|
1022
|
+
stashBranch: stash.branch,
|
|
1023
|
+
parentBranch,
|
|
1024
|
+
worktreePath: worktree.path,
|
|
1025
|
+
port,
|
|
1026
|
+
model: opts.screenshotModel,
|
|
1027
|
+
timeout: opts.screenshotTimeout
|
|
1028
|
+
});
|
|
1029
|
+
const updatedStash = { ...stash, screenshotUrl: primary, screenshots };
|
|
1030
|
+
persistence.saveStash(updatedStash);
|
|
1031
|
+
const idx = completedStashes.indexOf(stash);
|
|
1032
|
+
completedStashes[idx] = updatedStash;
|
|
1033
|
+
emit(onProgress, { type: "ready", stashId: stash.id, screenshotPath: primary });
|
|
1034
|
+
} finally {
|
|
1035
|
+
devServer.kill();
|
|
1036
|
+
}
|
|
841
1037
|
await worktreeManager.removeGeneration(`screenshot-${stash.id}`);
|
|
842
|
-
const updatedStash = { ...stash, screenshotUrl: screenshotPath };
|
|
843
|
-
persistence.saveStash(updatedStash);
|
|
844
|
-
const idx = completedStashes.indexOf(stash);
|
|
845
|
-
completedStashes[idx] = updatedStash;
|
|
846
|
-
emit(onProgress, { type: "ready", stashId: stash.id, screenshotPath });
|
|
847
1038
|
} catch (err) {
|
|
848
1039
|
logger.error("generation", `screenshot failed for ${stash.id}`, {
|
|
849
1040
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -855,7 +1046,7 @@ async function generate(opts) {
|
|
|
855
1046
|
}
|
|
856
1047
|
// ../core/dist/vary.js
|
|
857
1048
|
var {spawn: spawn4 } = globalThis.Bun;
|
|
858
|
-
import
|
|
1049
|
+
import simpleGit4 from "simple-git";
|
|
859
1050
|
function emit2(onProgress, event) {
|
|
860
1051
|
if (onProgress)
|
|
861
1052
|
onProgress(event);
|
|
@@ -913,6 +1104,7 @@ async function vary(opts) {
|
|
|
913
1104
|
worktreePath: worktree.path,
|
|
914
1105
|
port: null,
|
|
915
1106
|
screenshotUrl: null,
|
|
1107
|
+
screenshots: [],
|
|
916
1108
|
status: "generating",
|
|
917
1109
|
error: null,
|
|
918
1110
|
relatedTo: [sourceStashId],
|
|
@@ -932,7 +1124,7 @@ async function vary(opts) {
|
|
|
932
1124
|
});
|
|
933
1125
|
}
|
|
934
1126
|
await aiProcess.process.exited;
|
|
935
|
-
const wtGit =
|
|
1127
|
+
const wtGit = simpleGit4(worktree.path);
|
|
936
1128
|
try {
|
|
937
1129
|
await wtGit.add("-A");
|
|
938
1130
|
await wtGit.commit(`stashes: vary ${stashId} from ${sourceStashId}`);
|
|
@@ -940,10 +1132,11 @@ async function vary(opts) {
|
|
|
940
1132
|
await worktreeManager.removeGeneration(stashId);
|
|
941
1133
|
emit2(onProgress, { type: "screenshotting", stashId });
|
|
942
1134
|
let screenshotPath = "";
|
|
1135
|
+
let screenshots = [];
|
|
943
1136
|
try {
|
|
944
1137
|
const port = await allocatePort2();
|
|
945
1138
|
const screenshotWorktree = await worktreeManager.createForGeneration(`screenshot-${stashId}`);
|
|
946
|
-
const screenshotGit =
|
|
1139
|
+
const screenshotGit = simpleGit4(screenshotWorktree.path);
|
|
947
1140
|
await screenshotGit.checkout(["-f", stash.branch]);
|
|
948
1141
|
const devServer = spawn4({
|
|
949
1142
|
cmd: ["npm", "run", "dev"],
|
|
@@ -956,7 +1149,18 @@ async function vary(opts) {
|
|
|
956
1149
|
try {
|
|
957
1150
|
await waitForPort2(port, 60000);
|
|
958
1151
|
await new Promise((r) => setTimeout(r, 2000));
|
|
959
|
-
|
|
1152
|
+
const result = await captureSmartScreenshots({
|
|
1153
|
+
projectPath,
|
|
1154
|
+
stashId,
|
|
1155
|
+
stashBranch: stash.branch,
|
|
1156
|
+
parentBranch: sourceStash.branch,
|
|
1157
|
+
worktreePath: screenshotWorktree.path,
|
|
1158
|
+
port,
|
|
1159
|
+
model: opts.screenshotModel,
|
|
1160
|
+
timeout: opts.screenshotTimeout
|
|
1161
|
+
});
|
|
1162
|
+
screenshotPath = result.primary;
|
|
1163
|
+
screenshots = [...result.screenshots];
|
|
960
1164
|
} finally {
|
|
961
1165
|
devServer.kill();
|
|
962
1166
|
}
|
|
@@ -966,7 +1170,7 @@ async function vary(opts) {
|
|
|
966
1170
|
error: err instanceof Error ? err.message : String(err)
|
|
967
1171
|
});
|
|
968
1172
|
}
|
|
969
|
-
const readyStash = { ...stash, status: "ready", screenshotUrl: screenshotPath || null };
|
|
1173
|
+
const readyStash = { ...stash, status: "ready", screenshotUrl: screenshotPath || null, screenshots };
|
|
970
1174
|
persistence.saveStash(readyStash);
|
|
971
1175
|
emit2(onProgress, { type: "ready", stashId, screenshotPath });
|
|
972
1176
|
return readyStash;
|
|
@@ -990,7 +1194,7 @@ async function apply(opts) {
|
|
|
990
1194
|
logger.info("apply", `stash ${stashId} applied and worktrees cleaned up`);
|
|
991
1195
|
}
|
|
992
1196
|
// ../core/dist/manage.js
|
|
993
|
-
import
|
|
1197
|
+
import simpleGit5 from "simple-git";
|
|
994
1198
|
async function list(projectPath) {
|
|
995
1199
|
const persistence = new PersistenceService(projectPath);
|
|
996
1200
|
const projects = persistence.listProjects();
|
|
@@ -1011,7 +1215,7 @@ async function remove(projectPath, stashId) {
|
|
|
1011
1215
|
}
|
|
1012
1216
|
}
|
|
1013
1217
|
try {
|
|
1014
|
-
const git =
|
|
1218
|
+
const git = simpleGit5(projectPath);
|
|
1015
1219
|
await git.raw(["branch", "-D", `stashes/${stashId}`]);
|
|
1016
1220
|
} catch {}
|
|
1017
1221
|
logger.info("manage", `removed stash: ${stashId}`);
|
|
@@ -1026,7 +1230,7 @@ async function show(projectPath, stashId) {
|
|
|
1026
1230
|
}
|
|
1027
1231
|
if (!found)
|
|
1028
1232
|
return null;
|
|
1029
|
-
const git =
|
|
1233
|
+
const git = simpleGit5(projectPath);
|
|
1030
1234
|
const branch = found.branch || `stashes/${stashId}`;
|
|
1031
1235
|
let diff = "";
|
|
1032
1236
|
let files = [];
|
|
@@ -1223,14 +1427,14 @@ async function handleRemove(args, projectPath) {
|
|
|
1223
1427
|
// ../server/dist/index.js
|
|
1224
1428
|
import { Hono as Hono2 } from "hono";
|
|
1225
1429
|
import { cors } from "hono/cors";
|
|
1226
|
-
import { join as
|
|
1430
|
+
import { join as join9, dirname as dirname2 } from "path";
|
|
1227
1431
|
import { fileURLToPath } from "url";
|
|
1228
|
-
import { existsSync as
|
|
1432
|
+
import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
|
|
1229
1433
|
|
|
1230
1434
|
// ../server/dist/routes/api.js
|
|
1231
1435
|
import { Hono } from "hono";
|
|
1232
|
-
import { join as
|
|
1233
|
-
import { existsSync as
|
|
1436
|
+
import { join as join7, basename } from "path";
|
|
1437
|
+
import { existsSync as existsSync7, readFileSync as readFileSync3 } from "fs";
|
|
1234
1438
|
var app = new Hono;
|
|
1235
1439
|
app.get("/health", (c) => c.json({ status: "ok", service: "stashes" }));
|
|
1236
1440
|
app.get("/projects", (c) => {
|
|
@@ -1311,8 +1515,8 @@ app.delete("/chats/:chatId", (c) => {
|
|
|
1311
1515
|
});
|
|
1312
1516
|
app.get("/screenshots/:filename", (c) => {
|
|
1313
1517
|
const filename = c.req.param("filename");
|
|
1314
|
-
const filePath =
|
|
1315
|
-
if (!
|
|
1518
|
+
const filePath = join7(serverState.projectPath, ".stashes", "screenshots", filename);
|
|
1519
|
+
if (!existsSync7(filePath))
|
|
1316
1520
|
return c.json({ error: "Not found" }, 404);
|
|
1317
1521
|
const content = readFileSync3(filePath);
|
|
1318
1522
|
return new Response(content, {
|
|
@@ -1336,8 +1540,8 @@ function ensureProject(persistence) {
|
|
|
1336
1540
|
var apiRoutes = app;
|
|
1337
1541
|
|
|
1338
1542
|
// ../server/dist/services/stash-service.js
|
|
1339
|
-
import { readFileSync as readFileSync4, existsSync as
|
|
1340
|
-
import { join as
|
|
1543
|
+
import { readFileSync as readFileSync4, existsSync as existsSync8 } from "fs";
|
|
1544
|
+
import { join as join8 } from "path";
|
|
1341
1545
|
|
|
1342
1546
|
// ../server/dist/services/preview-pool.js
|
|
1343
1547
|
class PreviewPool {
|
|
@@ -1600,8 +1804,8 @@ class StashService {
|
|
|
1600
1804
|
let sourceCode = "";
|
|
1601
1805
|
const filePath = component?.filePath || "";
|
|
1602
1806
|
if (filePath && filePath !== "auto-detect") {
|
|
1603
|
-
const sourceFile =
|
|
1604
|
-
if (
|
|
1807
|
+
const sourceFile = join8(this.projectPath, filePath);
|
|
1808
|
+
if (existsSync8(sourceFile)) {
|
|
1605
1809
|
sourceCode = readFileSync4(sourceFile, "utf-8");
|
|
1606
1810
|
}
|
|
1607
1811
|
}
|
|
@@ -1751,6 +1955,7 @@ ${sourceCode.substring(0, 3000)}
|
|
|
1751
1955
|
for (const msg of pendingMessages) {
|
|
1752
1956
|
this.persistence.saveChatMessage(projectId, chatId, msg);
|
|
1753
1957
|
}
|
|
1958
|
+
this.syncStashesFromDisk(projectId);
|
|
1754
1959
|
} catch (err) {
|
|
1755
1960
|
this.broadcast({
|
|
1756
1961
|
type: "ai_stream",
|
|
@@ -1762,6 +1967,25 @@ ${sourceCode.substring(0, 3000)}
|
|
|
1762
1967
|
killAiProcess("chat");
|
|
1763
1968
|
}
|
|
1764
1969
|
}
|
|
1970
|
+
syncStashesFromDisk(projectId) {
|
|
1971
|
+
const diskStashes = this.persistence.listStashes(projectId);
|
|
1972
|
+
for (const stash of diskStashes) {
|
|
1973
|
+
this.broadcast({
|
|
1974
|
+
type: "stash:status",
|
|
1975
|
+
stashId: stash.id,
|
|
1976
|
+
status: stash.status,
|
|
1977
|
+
number: stash.number
|
|
1978
|
+
});
|
|
1979
|
+
if (stash.screenshotUrl) {
|
|
1980
|
+
this.broadcast({
|
|
1981
|
+
type: "stash:screenshot",
|
|
1982
|
+
stashId: stash.id,
|
|
1983
|
+
url: stash.screenshotUrl,
|
|
1984
|
+
screenshots: stash.screenshots?.length ? [...stash.screenshots] : undefined
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1765
1989
|
progressToBroadcast(event) {
|
|
1766
1990
|
switch (event.type) {
|
|
1767
1991
|
case "generating":
|
|
@@ -1774,7 +1998,12 @@ ${sourceCode.substring(0, 3000)}
|
|
|
1774
1998
|
..."number" in event ? { number: event.number } : {}
|
|
1775
1999
|
});
|
|
1776
2000
|
if (event.type === "ready" && "screenshotPath" in event && event.screenshotPath) {
|
|
1777
|
-
this.broadcast({
|
|
2001
|
+
this.broadcast({
|
|
2002
|
+
type: "stash:screenshot",
|
|
2003
|
+
stashId: event.stashId,
|
|
2004
|
+
url: event.screenshotPath,
|
|
2005
|
+
screenshots: "screenshots" in event ? event.screenshots : undefined
|
|
2006
|
+
});
|
|
1778
2007
|
}
|
|
1779
2008
|
break;
|
|
1780
2009
|
case "error":
|
|
@@ -1788,6 +2017,7 @@ ${sourceCode.substring(0, 3000)}
|
|
|
1788
2017
|
}
|
|
1789
2018
|
}
|
|
1790
2019
|
async generate(projectId, prompt, stashCount = DEFAULT_STASH_COUNT, referenceStashIds) {
|
|
2020
|
+
const settings = this.persistence.getProjectSettings(projectId);
|
|
1791
2021
|
let enrichedPrompt = prompt;
|
|
1792
2022
|
if (referenceStashIds?.length) {
|
|
1793
2023
|
const refDescriptions = referenceStashIds.map((id) => this.persistence.getStash(projectId, id)).filter(Boolean).map((s) => `- "${s.prompt}"${s.componentPath ? ` (${s.componentPath})` : ""}`);
|
|
@@ -1805,14 +2035,27 @@ ${refDescriptions.join(`
|
|
|
1805
2035
|
prompt: enrichedPrompt,
|
|
1806
2036
|
component: this.selectedComponent ? { filePath: this.selectedComponent.filePath, exportName: this.selectedComponent.name } : undefined,
|
|
1807
2037
|
count: stashCount,
|
|
2038
|
+
screenshotModel: settings.screenshotModel,
|
|
2039
|
+
screenshotTimeout: settings.screenshotTimeout,
|
|
1808
2040
|
onProgress: (event) => this.progressToBroadcast(event)
|
|
1809
2041
|
});
|
|
1810
2042
|
}
|
|
1811
2043
|
async vary(sourceStashId, prompt) {
|
|
2044
|
+
let projectId = "";
|
|
2045
|
+
for (const project of this.persistence.listProjects()) {
|
|
2046
|
+
const stashes = this.persistence.listStashes(project.id);
|
|
2047
|
+
if (stashes.find((s) => s.id === sourceStashId)) {
|
|
2048
|
+
projectId = project.id;
|
|
2049
|
+
break;
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
const settings = projectId ? this.persistence.getProjectSettings(projectId) : {};
|
|
1812
2053
|
await vary({
|
|
1813
2054
|
projectPath: this.projectPath,
|
|
1814
2055
|
sourceStashId,
|
|
1815
2056
|
prompt,
|
|
2057
|
+
screenshotModel: settings.screenshotModel,
|
|
2058
|
+
screenshotTimeout: settings.screenshotTimeout,
|
|
1816
2059
|
onProgress: (event) => this.progressToBroadcast(event)
|
|
1817
2060
|
});
|
|
1818
2061
|
}
|
|
@@ -1926,115 +2169,93 @@ function createWebSocketHandler(projectPath, userDevPort, appProxyPort) {
|
|
|
1926
2169
|
}
|
|
1927
2170
|
|
|
1928
2171
|
// ../server/dist/services/app-proxy.js
|
|
2172
|
+
import { spawn as spawn5 } from "child_process";
|
|
1929
2173
|
function startAppProxy(userDevPort, proxyPort, injectOverlay) {
|
|
1930
|
-
const
|
|
1931
|
-
const
|
|
1932
|
-
const
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
const contentType = response.headers.get("content-type") || "";
|
|
1964
|
-
const respHeaders = new Headers(response.headers);
|
|
1965
|
-
if (response.status >= 300 && response.status < 400) {
|
|
1966
|
-
const location = respHeaders.get("location");
|
|
1967
|
-
if (location?.startsWith(upstreamOrigin)) {
|
|
1968
|
-
respHeaders.set("location", proxyOrigin + location.substring(upstreamOrigin.length));
|
|
1969
|
-
}
|
|
1970
|
-
}
|
|
1971
|
-
if (contentType.includes("text/html")) {
|
|
1972
|
-
const html = await response.text();
|
|
1973
|
-
const injectedHtml = injectOverlay(html, userDevPort, proxyPort);
|
|
1974
|
-
respHeaders.delete("content-encoding");
|
|
1975
|
-
respHeaders.delete("content-length");
|
|
1976
|
-
return new Response(injectedHtml, {
|
|
1977
|
-
status: response.status,
|
|
1978
|
-
headers: respHeaders
|
|
1979
|
-
});
|
|
1980
|
-
}
|
|
1981
|
-
respHeaders.delete("content-encoding");
|
|
1982
|
-
return new Response(response.body, {
|
|
1983
|
-
status: response.status,
|
|
1984
|
-
headers: respHeaders
|
|
1985
|
-
});
|
|
1986
|
-
} catch (err) {
|
|
1987
|
-
return new Response(JSON.stringify({ error: "Proxy failed", detail: String(err) }), {
|
|
1988
|
-
status: 502,
|
|
1989
|
-
headers: { "content-type": "application/json" }
|
|
1990
|
-
});
|
|
1991
|
-
}
|
|
1992
|
-
},
|
|
1993
|
-
websocket: {
|
|
1994
|
-
open(ws) {
|
|
1995
|
-
const { data } = ws;
|
|
1996
|
-
const upstream = new WebSocket(`ws://localhost:${userDevPort}${data.path}`);
|
|
1997
|
-
upstream.addEventListener("open", () => {
|
|
1998
|
-
data.upstream = upstream;
|
|
1999
|
-
data.ready = true;
|
|
2000
|
-
for (const msg of data.buffer) {
|
|
2001
|
-
upstream.send(msg);
|
|
2002
|
-
}
|
|
2003
|
-
data.buffer = [];
|
|
2004
|
-
});
|
|
2005
|
-
upstream.addEventListener("message", (event) => {
|
|
2006
|
-
try {
|
|
2007
|
-
ws.sendText(typeof event.data === "string" ? event.data : String(event.data));
|
|
2008
|
-
} catch {}
|
|
2009
|
-
});
|
|
2010
|
-
upstream.addEventListener("close", () => {
|
|
2011
|
-
try {
|
|
2012
|
-
ws.close();
|
|
2013
|
-
} catch {}
|
|
2014
|
-
});
|
|
2015
|
-
upstream.addEventListener("error", () => {
|
|
2016
|
-
try {
|
|
2017
|
-
ws.close();
|
|
2018
|
-
} catch {}
|
|
2019
|
-
});
|
|
2020
|
-
},
|
|
2021
|
-
message(ws, msg) {
|
|
2022
|
-
const { data } = ws;
|
|
2023
|
-
if (data.ready && data.upstream) {
|
|
2024
|
-
data.upstream.send(typeof msg === "string" ? msg : new Uint8Array(msg));
|
|
2025
|
-
} else {
|
|
2026
|
-
data.buffer.push(typeof msg === "string" ? msg : new Uint8Array(msg).buffer);
|
|
2174
|
+
const overlayScript = injectOverlay("", userDevPort, proxyPort);
|
|
2175
|
+
const overlayEscaped = JSON.stringify(overlayScript);
|
|
2176
|
+
const proxyScript = `
|
|
2177
|
+
const http = require('http');
|
|
2178
|
+
const net = require('net');
|
|
2179
|
+
const zlib = require('zlib');
|
|
2180
|
+
const UPSTREAM = ${userDevPort};
|
|
2181
|
+
const OVERLAY = ${overlayEscaped};
|
|
2182
|
+
|
|
2183
|
+
const server = http.createServer((clientReq, clientRes) => {
|
|
2184
|
+
const opts = {
|
|
2185
|
+
hostname: 'localhost',
|
|
2186
|
+
port: UPSTREAM,
|
|
2187
|
+
path: clientReq.url,
|
|
2188
|
+
method: clientReq.method,
|
|
2189
|
+
headers: clientReq.headers,
|
|
2190
|
+
};
|
|
2191
|
+
const proxyReq = http.request(opts, (proxyRes) => {
|
|
2192
|
+
const ct = proxyRes.headers['content-type'] || '';
|
|
2193
|
+
if (ct.includes('text/html')) {
|
|
2194
|
+
// Buffer HTML to inject overlay
|
|
2195
|
+
const chunks = [];
|
|
2196
|
+
proxyRes.on('data', c => chunks.push(c));
|
|
2197
|
+
proxyRes.on('end', () => {
|
|
2198
|
+
let html = Buffer.concat(chunks);
|
|
2199
|
+
const enc = proxyRes.headers['content-encoding'];
|
|
2200
|
+
// Decompress if needed
|
|
2201
|
+
if (enc === 'gzip') {
|
|
2202
|
+
try { html = zlib.gunzipSync(html); } catch {}
|
|
2203
|
+
} else if (enc === 'br') {
|
|
2204
|
+
try { html = zlib.brotliDecompressSync(html); } catch {}
|
|
2205
|
+
} else if (enc === 'deflate') {
|
|
2206
|
+
try { html = zlib.inflateSync(html); } catch {}
|
|
2027
2207
|
}
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2208
|
+
const hdrs = { ...proxyRes.headers };
|
|
2209
|
+
delete hdrs['content-length'];
|
|
2210
|
+
delete hdrs['content-encoding'];
|
|
2211
|
+
delete hdrs['transfer-encoding'];
|
|
2212
|
+
clientRes.writeHead(proxyRes.statusCode, hdrs);
|
|
2213
|
+
clientRes.write(html);
|
|
2214
|
+
clientRes.end(OVERLAY);
|
|
2215
|
+
});
|
|
2216
|
+
} else {
|
|
2217
|
+
// Non-HTML: stream through unchanged
|
|
2218
|
+
clientRes.writeHead(proxyRes.statusCode, proxyRes.headers);
|
|
2219
|
+
proxyRes.pipe(clientRes);
|
|
2034
2220
|
}
|
|
2221
|
+
proxyRes.on('error', () => clientRes.end());
|
|
2035
2222
|
});
|
|
2036
|
-
|
|
2037
|
-
|
|
2223
|
+
proxyReq.on('error', () => { try { clientRes.writeHead(502); clientRes.end(); } catch {} });
|
|
2224
|
+
clientReq.pipe(proxyReq);
|
|
2225
|
+
});
|
|
2226
|
+
|
|
2227
|
+
// WebSocket upgrades: raw TCP pipe
|
|
2228
|
+
server.on('upgrade', (req, socket, head) => {
|
|
2229
|
+
const upstream = net.createConnection(UPSTREAM, 'localhost', () => {
|
|
2230
|
+
const lines = [req.method + ' ' + req.url + ' HTTP/1.1'];
|
|
2231
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
2232
|
+
lines.push(k + ': ' + (Array.isArray(v) ? v.join(', ') : v));
|
|
2233
|
+
}
|
|
2234
|
+
upstream.write(lines.join('\\r\\n') + '\\r\\n\\r\\n');
|
|
2235
|
+
if (head.length) upstream.write(head);
|
|
2236
|
+
socket.pipe(upstream);
|
|
2237
|
+
upstream.pipe(socket);
|
|
2238
|
+
});
|
|
2239
|
+
upstream.on('error', () => socket.destroy());
|
|
2240
|
+
socket.on('error', () => upstream.destroy());
|
|
2241
|
+
});
|
|
2242
|
+
|
|
2243
|
+
server.listen(${proxyPort}, () => {
|
|
2244
|
+
if (process.send) process.send('ready');
|
|
2245
|
+
});
|
|
2246
|
+
`;
|
|
2247
|
+
const child = spawn5("node", ["-e", proxyScript], {
|
|
2248
|
+
stdio: ["ignore", "inherit", "inherit", "ipc"]
|
|
2249
|
+
});
|
|
2250
|
+
child.on("error", (err) => {
|
|
2251
|
+
logger.error("proxy", `Failed to start proxy: ${err.message}`);
|
|
2252
|
+
});
|
|
2253
|
+
child.on("message", (msg) => {
|
|
2254
|
+
if (msg === "ready") {
|
|
2255
|
+
logger.info("proxy", `App proxy at http://localhost:${proxyPort} \u2192 http://localhost:${userDevPort}`);
|
|
2256
|
+
}
|
|
2257
|
+
});
|
|
2258
|
+
return child;
|
|
2038
2259
|
}
|
|
2039
2260
|
|
|
2040
2261
|
// ../server/dist/index.js
|
|
@@ -2051,12 +2272,12 @@ app2.route("/api", apiRoutes);
|
|
|
2051
2272
|
app2.get("/*", async (c) => {
|
|
2052
2273
|
const path = c.req.path;
|
|
2053
2274
|
const selfDir = dirname2(fileURLToPath(import.meta.url));
|
|
2054
|
-
const bundledWebDir =
|
|
2055
|
-
const monorepoWebDir =
|
|
2056
|
-
const webDistDir =
|
|
2275
|
+
const bundledWebDir = join9(selfDir, "web");
|
|
2276
|
+
const monorepoWebDir = join9(selfDir, "../../web/dist");
|
|
2277
|
+
const webDistDir = existsSync9(join9(bundledWebDir, "index.html")) ? bundledWebDir : monorepoWebDir;
|
|
2057
2278
|
const requestPath = path === "/" ? "/index.html" : path;
|
|
2058
|
-
const filePath =
|
|
2059
|
-
if (
|
|
2279
|
+
const filePath = join9(webDistDir, requestPath);
|
|
2280
|
+
if (existsSync9(filePath) && !filePath.includes("..")) {
|
|
2060
2281
|
const content = readFileSync5(filePath);
|
|
2061
2282
|
const ext = filePath.split(".").pop() || "";
|
|
2062
2283
|
const contentTypes = {
|
|
@@ -2075,8 +2296,8 @@ app2.get("/*", async (c) => {
|
|
|
2075
2296
|
headers: { "content-type": contentTypes[ext] || "application/octet-stream" }
|
|
2076
2297
|
});
|
|
2077
2298
|
}
|
|
2078
|
-
const indexPath =
|
|
2079
|
-
if (
|
|
2299
|
+
const indexPath = join9(webDistDir, "index.html");
|
|
2300
|
+
if (existsSync9(indexPath)) {
|
|
2080
2301
|
const html = readFileSync5(indexPath, "utf-8");
|
|
2081
2302
|
return new Response(html, {
|
|
2082
2303
|
headers: { "content-type": "text/html; charset=utf-8" }
|
|
@@ -2112,7 +2333,7 @@ function startServer(projectPath, userDevPort, port = STASHES_PORT) {
|
|
|
2112
2333
|
logger.info("server", `Project: ${projectPath}`);
|
|
2113
2334
|
return server;
|
|
2114
2335
|
}
|
|
2115
|
-
function injectOverlayScript(html,
|
|
2336
|
+
function injectOverlayScript(html, _upstreamPort, _proxyPort) {
|
|
2116
2337
|
const overlayScript = `
|
|
2117
2338
|
<script data-stashes-overlay>
|
|
2118
2339
|
(function() {
|
|
@@ -2120,35 +2341,6 @@ function injectOverlayScript(html, upstreamPort, proxyPort) {
|
|
|
2120
2341
|
var pickerEnabled = false;
|
|
2121
2342
|
var precisionMode = false;
|
|
2122
2343
|
|
|
2123
|
-
// Rewrite cross-origin requests to the upstream dev server through the proxy
|
|
2124
|
-
var upstreamOrigin = 'http://localhost:${upstreamPort}';
|
|
2125
|
-
var proxyOrigin = window.location.origin;
|
|
2126
|
-
if (proxyOrigin !== upstreamOrigin) {
|
|
2127
|
-
function rewriteUrl(url) {
|
|
2128
|
-
if (typeof url === 'string' && (url.startsWith(upstreamOrigin + '/') || url === upstreamOrigin)) {
|
|
2129
|
-
return proxyOrigin + url.substring(upstreamOrigin.length);
|
|
2130
|
-
}
|
|
2131
|
-
return url;
|
|
2132
|
-
}
|
|
2133
|
-
var origFetch = window.fetch;
|
|
2134
|
-
window.fetch = function(input, init) {
|
|
2135
|
-
if (typeof input === 'string') {
|
|
2136
|
-
input = rewriteUrl(input);
|
|
2137
|
-
} else if (input instanceof Request) {
|
|
2138
|
-
var rewritten = rewriteUrl(input.url);
|
|
2139
|
-
if (rewritten !== input.url) { input = new Request(rewritten, input); }
|
|
2140
|
-
}
|
|
2141
|
-
return origFetch.call(window, input, init);
|
|
2142
|
-
};
|
|
2143
|
-
var origXhrOpen = XMLHttpRequest.prototype.open;
|
|
2144
|
-
XMLHttpRequest.prototype.open = function() {
|
|
2145
|
-
if (arguments.length >= 2 && typeof arguments[1] === 'string') {
|
|
2146
|
-
arguments[1] = rewriteUrl(arguments[1]);
|
|
2147
|
-
}
|
|
2148
|
-
return origXhrOpen.apply(this, arguments);
|
|
2149
|
-
};
|
|
2150
|
-
}
|
|
2151
|
-
|
|
2152
2344
|
function createOverlay() {
|
|
2153
2345
|
var overlay = document.createElement('div');
|
|
2154
2346
|
overlay.id = 'stashes-highlight';
|
|
@@ -2162,13 +2354,18 @@ function injectOverlayScript(html, upstreamPort, proxyPort) {
|
|
|
2162
2354
|
}
|
|
2163
2355
|
|
|
2164
2356
|
var SEMANTIC_TAGS = ['header','nav','main','section','article','aside','footer','form','dialog'];
|
|
2357
|
+
var LEAF_TAGS = ['h1','h2','h3','h4','h5','h6','p','button','a','input','textarea','select','img','svg','video','label','li','td','th','figcaption','blockquote','pre','code','span'];
|
|
2165
2358
|
|
|
2166
2359
|
function findTarget(el, precise) {
|
|
2167
2360
|
if (precise) return el;
|
|
2361
|
+
// If the element itself is a meaningful leaf, select it directly
|
|
2362
|
+
var elTag = el.tagName ? el.tagName.toLowerCase() : '';
|
|
2363
|
+
if (LEAF_TAGS.indexOf(elTag) !== -1) return el;
|
|
2168
2364
|
var current = el;
|
|
2169
2365
|
var best = el;
|
|
2170
2366
|
while (current && current !== document.body) {
|
|
2171
2367
|
var tag = current.tagName.toLowerCase();
|
|
2368
|
+
if (LEAF_TAGS.indexOf(tag) !== -1) { best = current; break; }
|
|
2172
2369
|
if (SEMANTIC_TAGS.indexOf(tag) !== -1) { best = current; break; }
|
|
2173
2370
|
if (current.id) { best = current; break; }
|
|
2174
2371
|
if (current.getAttribute('role')) { best = current; break; }
|
|
@@ -2322,11 +2519,11 @@ function injectOverlayScript(html, upstreamPort, proxyPort) {
|
|
|
2322
2519
|
import open from "open";
|
|
2323
2520
|
|
|
2324
2521
|
// ../server/dist/services/detector.js
|
|
2325
|
-
import { existsSync as
|
|
2326
|
-
import { join as
|
|
2522
|
+
import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
|
|
2523
|
+
import { join as join10 } from "path";
|
|
2327
2524
|
function detectFramework(projectPath) {
|
|
2328
|
-
const packageJsonPath =
|
|
2329
|
-
if (!
|
|
2525
|
+
const packageJsonPath = join10(projectPath, "package.json");
|
|
2526
|
+
if (!existsSync10(packageJsonPath)) {
|
|
2330
2527
|
return {
|
|
2331
2528
|
framework: "unknown",
|
|
2332
2529
|
devCommand: "npm run dev",
|
|
@@ -2388,7 +2585,7 @@ function getDevCommand(packageJson, fallback) {
|
|
|
2388
2585
|
}
|
|
2389
2586
|
function findConfig(projectPath, candidates) {
|
|
2390
2587
|
for (const candidate of candidates) {
|
|
2391
|
-
if (
|
|
2588
|
+
if (existsSync10(join10(projectPath, candidate))) {
|
|
2392
2589
|
return candidate;
|
|
2393
2590
|
}
|
|
2394
2591
|
}
|