softpeach-cli 1.0.2 → 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.
- package/bin/softpeach.mjs +146 -3
- package/package.json +1 -1
package/bin/softpeach.mjs
CHANGED
|
@@ -3,8 +3,57 @@
|
|
|
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
|
-
const SOFTPEACH_URL = process.env.SOFTPEACH_URL || "https://softpeach.onrender.com";
|
|
8
|
+
const SOFTPEACH_URL = process.env.SOFTPEACH_URL || "https://softpeach-w2zu.onrender.com";
|
|
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>`;
|
|
8
57
|
|
|
9
58
|
function printBanner() {
|
|
10
59
|
console.log("");
|
|
@@ -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
|
-
|
|
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
|
|
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");
|