softpeach-cli 1.0.3 → 1.1.0

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.
Files changed (2) hide show
  1. package/bin/softpeach.mjs +145 -2
  2. package/package.json +1 -1
package/bin/softpeach.mjs CHANGED
@@ -3,9 +3,58 @@
3
3
  import { tunnel } from "cloudflared";
4
4
  import { execSync } from "child_process";
5
5
  import { randomBytes } from "crypto";
6
+ import http from "http";
6
7
 
7
8
  const SOFTPEACH_URL = process.env.SOFTPEACH_URL || "https://softpeach-w2zu.onrender.com";
8
9
 
10
+ // Script injected into HTML responses to enable SoftPeach features:
11
+ // - Scroll position reporting via postMessage
12
+ // - Scroll-to command handling (click comment in sidebar → scroll to it)
13
+ // - Element context queries (for AI prompt generation)
14
+ const HELPER_SCRIPT = `<script data-softpeach-helper>
15
+ (function(){
16
+ var lx=-1,ly=-1;
17
+ function report(){
18
+ var sx=window.scrollX||window.pageXOffset||0;
19
+ var sy=window.scrollY||window.pageYOffset||0;
20
+ var cw=document.documentElement.scrollWidth;
21
+ var ch=document.documentElement.scrollHeight;
22
+ if(sx!==lx||sy!==ly){
23
+ lx=sx;ly=sy;
24
+ parent.postMessage({type:"softpeach-scroll",scrollX:sx,scrollY:sy,contentWidth:cw,contentHeight:ch},"*");
25
+ }
26
+ requestAnimationFrame(report);
27
+ }
28
+ requestAnimationFrame(report);
29
+ window.addEventListener("load",function(){
30
+ setTimeout(function(){
31
+ var sx=window.scrollX||0,sy=window.scrollY||0;
32
+ parent.postMessage({type:"softpeach-scroll",scrollX:sx,scrollY:sy,
33
+ contentWidth:document.documentElement.scrollWidth,
34
+ contentHeight:document.documentElement.scrollHeight},"*");
35
+ },100);
36
+ });
37
+ window.addEventListener("message",function(e){
38
+ if(!e.data)return;
39
+ if(e.data.type==="softpeach-scroll-to"){
40
+ window.scrollTo({left:e.data.scrollX||0,top:e.data.scrollY||0,behavior:"smooth"});
41
+ }
42
+ if(e.data.type==="softpeach-element-query"){
43
+ var el=document.elementFromPoint(e.data.x,e.data.y);
44
+ var ctx="";
45
+ if(el){
46
+ var tag=el.tagName.toLowerCase();
47
+ var txt=(el.textContent||"").trim().substring(0,50);
48
+ var sec=el.closest("section,main,header,footer,nav,aside,article");
49
+ var sid=sec?(sec.tagName.toLowerCase()+(sec.id?"#"+sec.id:sec.className?" ."+sec.className.split(" ")[0]:"")):"";
50
+ ctx="<"+tag+">"+(txt?' "'+txt+'"':"")+(sid?" in <"+sid+">":"");
51
+ }
52
+ parent.postMessage({type:"softpeach-element-result",id:e.data.id,context:ctx},"*");
53
+ }
54
+ });
55
+ })();
56
+ </script>`;
57
+
9
58
  function printBanner() {
10
59
  console.log("");
11
60
  console.log(" \x1b[38;5;209m\x1b[1m🍑 SoftPeach\x1b[0m — Share your localhost for design review");
@@ -45,6 +94,82 @@ function generateRoomId() {
45
94
  return randomBytes(4).toString("hex");
46
95
  }
47
96
 
97
+ /**
98
+ * Creates a local HTTP proxy that forwards requests to the target port
99
+ * and injects the SoftPeach helper script into HTML responses.
100
+ * This enables scroll tracking, click-to-scroll, and element context
101
+ * detection when the page is loaded in SoftPeach's iframe.
102
+ */
103
+ function createInjectingProxy(targetPort) {
104
+ return new Promise((resolve, reject) => {
105
+ const server = http.createServer(async (req, res) => {
106
+ const targetUrl = `http://localhost:${targetPort}${req.url}`;
107
+
108
+ try {
109
+ // Collect request body for non-GET requests
110
+ const chunks = [];
111
+ for await (const chunk of req) chunks.push(chunk);
112
+ const body = chunks.length > 0 ? Buffer.concat(chunks) : undefined;
113
+
114
+ // Forward headers, fixing the host
115
+ const headers = { ...req.headers, host: `localhost:${targetPort}` };
116
+ delete headers["accept-encoding"]; // Don't accept compressed responses — we need to read HTML
117
+
118
+ const resp = await fetch(targetUrl, {
119
+ method: req.method,
120
+ headers,
121
+ body,
122
+ redirect: "follow",
123
+ });
124
+
125
+ const contentType = resp.headers.get("content-type") || "";
126
+
127
+ if (contentType.includes("text/html")) {
128
+ let html = await resp.text();
129
+
130
+ // Inject helper script — before </head> if possible, otherwise before </body>
131
+ if (html.includes("</head>")) {
132
+ html = html.replace("</head>", HELPER_SCRIPT + "</head>");
133
+ } else if (html.includes("</body>")) {
134
+ html = html.replace("</body>", HELPER_SCRIPT + "</body>");
135
+ } else if (html.includes("</HEAD>")) {
136
+ html = html.replace("</HEAD>", HELPER_SCRIPT + "</HEAD>");
137
+ } else {
138
+ html += HELPER_SCRIPT;
139
+ }
140
+
141
+ // Forward relevant response headers
142
+ const respHeaders = { "content-type": contentType };
143
+ const cacheControl = resp.headers.get("cache-control");
144
+ if (cacheControl) respHeaders["cache-control"] = cacheControl;
145
+
146
+ res.writeHead(resp.status, respHeaders);
147
+ res.end(html);
148
+ } else {
149
+ // Non-HTML: proxy as-is
150
+ const buffer = Buffer.from(await resp.arrayBuffer());
151
+ const respHeaders = { "content-type": contentType };
152
+ const cacheControl = resp.headers.get("cache-control");
153
+ if (cacheControl) respHeaders["cache-control"] = cacheControl;
154
+
155
+ res.writeHead(resp.status, respHeaders);
156
+ res.end(buffer);
157
+ }
158
+ } catch (err) {
159
+ res.writeHead(502, { "content-type": "text/plain" });
160
+ res.end(`SoftPeach proxy error: ${err.message}`);
161
+ }
162
+ });
163
+
164
+ server.listen(0, "127.0.0.1", () => {
165
+ const proxyPort = server.address().port;
166
+ resolve({ proxyPort, server });
167
+ });
168
+
169
+ server.on("error", reject);
170
+ });
171
+ }
172
+
48
173
  async function main() {
49
174
  const args = process.argv.slice(2);
50
175
 
@@ -95,10 +220,25 @@ async function main() {
95
220
  console.log("");
96
221
  }
97
222
 
223
+ // Start local proxy that injects SoftPeach helper script
224
+ console.log(" \x1b[36m⟳\x1b[0m Starting helper proxy...");
225
+ let proxyPort;
226
+ let proxyServer;
227
+ try {
228
+ const proxy = await createInjectingProxy(port);
229
+ proxyPort = proxy.proxyPort;
230
+ proxyServer = proxy.server;
231
+ console.log(` \x1b[32m✓\x1b[0m Helper proxy running on port ${proxyPort}`);
232
+ } catch (err) {
233
+ console.error(` \x1b[31m✗ Failed to start helper proxy:\x1b[0m ${err.message}`);
234
+ process.exit(1);
235
+ }
236
+
98
237
  console.log(" \x1b[36m⟳\x1b[0m Starting tunnel...");
99
238
 
100
239
  try {
101
- const { url, stop, connections } = tunnel({ "--url": `http://localhost:${port}` });
240
+ // Tunnel to the proxy, not directly to the dev server
241
+ const { url, stop, connections } = tunnel({ "--url": `http://localhost:${proxyPort}` });
102
242
 
103
243
  const tunnelUrl = await url;
104
244
 
@@ -143,14 +283,16 @@ async function main() {
143
283
 
144
284
  // Keep the process alive until interrupted
145
285
  process.on("SIGINT", () => {
146
- console.log("\n \x1b[33m■\x1b[0m Shutting down tunnel...");
286
+ console.log("\n \x1b[33m■\x1b[0m Shutting down...");
147
287
  stop();
288
+ proxyServer.close();
148
289
  console.log(" \x1b[32m✓\x1b[0m Done. Thanks for using SoftPeach!\n");
149
290
  process.exit(0);
150
291
  });
151
292
 
152
293
  process.on("SIGTERM", () => {
153
294
  stop();
295
+ proxyServer.close();
154
296
  process.exit(0);
155
297
  });
156
298
 
@@ -158,6 +300,7 @@ async function main() {
158
300
  await new Promise(() => {});
159
301
 
160
302
  } catch (err) {
303
+ proxyServer.close();
161
304
  console.error(`\n \x1b[31m✗ Failed to start tunnel:\x1b[0m ${err.message}`);
162
305
  console.log("");
163
306
  console.log(" \x1b[2mMake sure cloudflared is accessible. You can install it:\x1b[0m");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "softpeach-cli",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "Share your localhost with your team for design review on SoftPeach",
5
5
  "type": "module",
6
6
  "bin": {