open-ready 0.1.1 β 0.1.2
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/open-when-ready.mjs +102 -33
- package/package.json +2 -1
package/open-when-ready.mjs
CHANGED
|
@@ -3,46 +3,76 @@ import fsPromises from "fs/promises";
|
|
|
3
3
|
import fs from "fs";
|
|
4
4
|
import path from "path";
|
|
5
5
|
import { spawn } from "child_process";
|
|
6
|
-
import
|
|
6
|
+
import opener from "opener";
|
|
7
7
|
import minimist from "minimist";
|
|
8
|
+
import waitOn from "wait-on";
|
|
8
9
|
|
|
9
10
|
const argv = minimist(process.argv.slice(2));
|
|
10
11
|
const cmdArgs = argv._;
|
|
11
12
|
const aiBase = argv["ai-base"] || "https://perplexity.ai?q=";
|
|
12
13
|
const noAi = argv.noAi || argv.noai || argv.ai === false;
|
|
14
|
+
const noOpen = argv.noOpen || argv.noopen || false;
|
|
13
15
|
const maxErrorContextChars = 1000;
|
|
16
|
+
const pollDelay = argv.pollDelay || 1200;
|
|
14
17
|
|
|
15
18
|
const nextDir = path.join(".", ".next");
|
|
16
19
|
const logPath = fs.existsSync(nextDir)
|
|
17
20
|
? path.join(".next", "port.log")
|
|
18
21
|
: "open-when-ready.log";
|
|
19
22
|
|
|
20
|
-
console.log(`π Log: ${logPath}`);
|
|
23
|
+
console.log(`π Log: ${logPath} | Poll: ${pollDelay}ms`);
|
|
21
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Opens browser when dev server is ready.
|
|
27
|
+
*
|
|
28
|
+
* ### Features
|
|
29
|
+
* - **Automatic**: Detects server start
|
|
30
|
+
* - **Versatile**: Supports multiple browsers
|
|
31
|
+
*
|
|
32
|
+
* ```js
|
|
33
|
+
* console.log("Ready!");
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
22
36
|
function getErrorContext(log) {
|
|
23
|
-
console.log("π Log preview:", log.slice(0, 200));
|
|
24
37
|
const lines = log.split(/\n/);
|
|
25
|
-
const errorRegex = /β¨―|[Ss]yntax[Ee]rror|error/i;
|
|
26
|
-
console.log("Regex:", errorRegex);
|
|
38
|
+
const errorRegex = /β¨―|[Ss]yntax[Ee]rror|error|failed|exception/i;
|
|
27
39
|
for (let i = 0; i < lines.length; i++) {
|
|
28
40
|
if (errorRegex.test(lines[i])) {
|
|
29
|
-
console.log("β
MATCHED on line", i, ":", lines[i].slice(0, 100));
|
|
30
41
|
const context = lines
|
|
31
42
|
.slice(Math.max(0, i - 5), i + 16)
|
|
32
43
|
.slice(0, 25)
|
|
33
44
|
.join(" ")
|
|
34
|
-
.replace(/[^\w\s
|
|
45
|
+
.replace(/[^\w\s:./\\-]/g, "")
|
|
35
46
|
.trim()
|
|
36
47
|
.replace(/\s{2,}/g, " ")
|
|
37
48
|
.slice(0, maxErrorContextChars)
|
|
38
49
|
.replace(/ /g, "%20");
|
|
39
|
-
console.log("Context:", context.slice(0, 100));
|
|
40
50
|
return context;
|
|
41
51
|
}
|
|
42
52
|
}
|
|
43
53
|
return "";
|
|
44
54
|
}
|
|
45
55
|
|
|
56
|
+
function extractUrl(log) {
|
|
57
|
+
// Better regex for Local: line
|
|
58
|
+
const localMatch = log.match(/Local:\s+(http:\/\/localhost:\d+)/i);
|
|
59
|
+
if (localMatch) {
|
|
60
|
+
console.log("β
EXTRACTED URL:", localMatch[1]);
|
|
61
|
+
return localMatch[1];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Fallback port
|
|
65
|
+
const portMatch = log.match(/localhost:(\d+)/);
|
|
66
|
+
if (portMatch) {
|
|
67
|
+
const url = `http://localhost:${portMatch[1]}`;
|
|
68
|
+
console.log("β
FALLBACK URL:", url);
|
|
69
|
+
return url;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log("β NO URL FOUND");
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
46
76
|
async function run() {
|
|
47
77
|
try {
|
|
48
78
|
await fsPromises.rm(logPath, { force: true });
|
|
@@ -52,6 +82,7 @@ async function run() {
|
|
|
52
82
|
const proc = spawn(cmdArgs.join(" "), [], {
|
|
53
83
|
stdio: ["ignore", "pipe", "pipe", "ipc"],
|
|
54
84
|
shell: true,
|
|
85
|
+
detached: true,
|
|
55
86
|
});
|
|
56
87
|
|
|
57
88
|
const logStream = fs.createWriteStream(logPath);
|
|
@@ -61,44 +92,82 @@ async function run() {
|
|
|
61
92
|
proc.stderr?.pipe(logStream);
|
|
62
93
|
|
|
63
94
|
let opened = false;
|
|
64
|
-
let
|
|
95
|
+
let errorOpened = false;
|
|
96
|
+
let lastReadySeen = false;
|
|
97
|
+
let lastLogSize = 0;
|
|
98
|
+
let stableCount = 0;
|
|
65
99
|
|
|
66
|
-
// Aggressive 500ms poll + debug
|
|
67
100
|
const poll = setInterval(async () => {
|
|
68
|
-
pollCount++;
|
|
69
|
-
console.log(`β±οΈ Poll #${pollCount}`);
|
|
70
|
-
if (opened) return;
|
|
71
101
|
try {
|
|
72
|
-
await sleep(100); // Brief settle
|
|
73
102
|
const stats = fs.statSync(logPath);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
103
|
+
const currentLog = await fsPromises.readFile(logPath, "utf8");
|
|
104
|
+
|
|
105
|
+
// Stability check LAST - check ready first!
|
|
106
|
+
const logChanged = stats.size !== lastLogSize;
|
|
107
|
+
if (logChanged && stats.size > 100) {
|
|
108
|
+
stableCount = 0;
|
|
109
|
+
lastLogSize = stats.size;
|
|
110
|
+
console.log(
|
|
111
|
+
`π ${stats.size}B CHANGED | LOG:\n${currentLog.slice(0, 500)}...`,
|
|
112
|
+
);
|
|
113
|
+
} else if (stats.size > 100) {
|
|
114
|
+
stableCount++;
|
|
115
|
+
console.log(`π ${stats.size}B STABLE (${stableCount}/5)`);
|
|
116
|
+
if (stableCount >= 5) {
|
|
117
|
+
console.log("π Log stable 5 polls - stopping");
|
|
84
118
|
clearInterval(poll);
|
|
85
|
-
proc.kill();
|
|
86
119
|
return;
|
|
87
120
|
}
|
|
88
121
|
}
|
|
122
|
+
|
|
123
|
+
// Error first
|
|
124
|
+
const errorRegex = /β¨―|[Ss]yntax[Ee]rror|error|failed|exception/i;
|
|
125
|
+
if (errorRegex.test(currentLog) && !noAi && !errorOpened) {
|
|
126
|
+
console.log("π― ERROR!");
|
|
127
|
+
const err = getErrorContext(currentLog);
|
|
128
|
+
const customPrompt = "Explain what the error is, how to fix it, then give a shell script with the best recommended solution.";
|
|
129
|
+
const prompt = encodeURIComponent(customPrompt + " ");
|
|
130
|
+
opener(`${aiBase}${prompt}next.js+${err}`);
|
|
131
|
+
errorOpened = true;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Ready check BEFORE stability stops us - with transition guard
|
|
136
|
+
const readyRegex = /(?:ready[\s-]*started server|Ready in \d+ms)/i;
|
|
137
|
+
const isReadyNow = readyRegex.test(currentLog);
|
|
138
|
+
if (isReadyNow && !lastReadySeen && !opened) {
|
|
139
|
+
opened = true;
|
|
140
|
+
lastReadySeen = true;
|
|
141
|
+
console.log("π READY DETECTED!");
|
|
142
|
+
const url = extractUrl(currentLog);
|
|
143
|
+
if (url && !noOpen) {
|
|
144
|
+
console.log("π Testing", url);
|
|
145
|
+
try {
|
|
146
|
+
await waitOn(url, { timeout: 10000, http: true });
|
|
147
|
+
console.log("β
HTTP READY!");
|
|
148
|
+
} catch (e) {
|
|
149
|
+
console.log(
|
|
150
|
+
"β HTTP not ready, but opening:",
|
|
151
|
+
e.message.slice(0, 50),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
console.log("π OPENING BROWSER:", url);
|
|
155
|
+
opener(url);
|
|
156
|
+
}
|
|
157
|
+
clearInterval(poll);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
lastReadySeen = isReadyNow;
|
|
89
161
|
} catch (e) {
|
|
90
162
|
console.log("Poll error:", e.message);
|
|
91
163
|
}
|
|
92
|
-
},
|
|
164
|
+
}, pollDelay);
|
|
93
165
|
|
|
94
|
-
|
|
166
|
+
process.on("SIGINT", () => {
|
|
167
|
+
console.log("\nπ Exiting (dev server alive)");
|
|
95
168
|
clearInterval(poll);
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function sleep(ms) {
|
|
101
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
169
|
+
process.exit(0);
|
|
170
|
+
});
|
|
102
171
|
}
|
|
103
172
|
|
|
104
173
|
run().catch(console.error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "open-ready",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Smart dev server launcher: errorβAI search, successβauto-open browser. Any CLI tool.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"minimist": "^1.2.8",
|
|
23
23
|
"open": "^10.1.0",
|
|
24
|
+
"opener": "^1.5.2",
|
|
24
25
|
"wait-on": "^8.0.1"
|
|
25
26
|
},
|
|
26
27
|
"engines": {
|