open-ready 0.1.1 → 0.1.3
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 +90 -37
- package/package.json +2 -2
package/open-when-ready.mjs
CHANGED
|
@@ -1,57 +1,90 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview CLI tool that spawns a dev server command, monitors its log
|
|
5
|
+
* output for a "ready" signal, and automatically opens the local URL in the
|
|
6
|
+
* browser. If an error is detected first, it opens an AI assistant with the
|
|
7
|
+
* error context for troubleshooting.
|
|
8
|
+
*/
|
|
9
|
+
|
|
2
10
|
import fsPromises from "fs/promises";
|
|
3
11
|
import fs from "fs";
|
|
4
12
|
import path from "path";
|
|
5
13
|
import { spawn } from "child_process";
|
|
6
|
-
import
|
|
14
|
+
import opener from "opener";
|
|
7
15
|
import minimist from "minimist";
|
|
16
|
+
import waitOn from "wait-on";
|
|
8
17
|
|
|
9
18
|
const argv = minimist(process.argv.slice(2));
|
|
10
19
|
const cmdArgs = argv._;
|
|
11
20
|
const aiBase = argv["ai-base"] || "https://perplexity.ai?q=";
|
|
12
21
|
const noAi = argv.noAi || argv.noai || argv.ai === false;
|
|
22
|
+
const noOpen = argv.noOpen || argv.noopen || false;
|
|
13
23
|
const maxErrorContextChars = 1000;
|
|
24
|
+
const pollDelay = argv.pollDelay || 1200;
|
|
14
25
|
|
|
15
26
|
const nextDir = path.join(".", ".next");
|
|
16
27
|
const logPath = fs.existsSync(nextDir)
|
|
17
28
|
? path.join(".next", "port.log")
|
|
18
29
|
: "open-when-ready.log";
|
|
19
30
|
|
|
20
|
-
console.log(`📁 Log: ${logPath}`);
|
|
21
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Scans log output for error lines and extracts surrounding context
|
|
34
|
+
* as a URL-encoded string suitable for an AI search query.
|
|
35
|
+
* @param {string} log - Raw log output from the dev server
|
|
36
|
+
* @returns {string} URL-encoded error context, or empty string if no error found
|
|
37
|
+
*/
|
|
22
38
|
function getErrorContext(log) {
|
|
23
|
-
console.log("🔍 Log preview:", log.slice(0, 200));
|
|
24
39
|
const lines = log.split(/\n/);
|
|
25
|
-
const errorRegex = /⨯|[Ss]yntax[Ee]rror|error/i;
|
|
26
|
-
console.log("Regex:", errorRegex);
|
|
40
|
+
const errorRegex = /⨯|[Ss]yntax[Ee]rror|error|failed|exception/i;
|
|
27
41
|
for (let i = 0; i < lines.length; i++) {
|
|
28
42
|
if (errorRegex.test(lines[i])) {
|
|
29
|
-
console.log("✅ MATCHED on line", i, ":", lines[i].slice(0, 100));
|
|
30
43
|
const context = lines
|
|
31
44
|
.slice(Math.max(0, i - 5), i + 16)
|
|
32
45
|
.slice(0, 25)
|
|
33
46
|
.join(" ")
|
|
34
|
-
.replace(/[^\w\s
|
|
47
|
+
.replace(/[^\w\s:./\\-]/g, "")
|
|
35
48
|
.trim()
|
|
36
49
|
.replace(/\s{2,}/g, " ")
|
|
37
50
|
.slice(0, maxErrorContextChars)
|
|
38
51
|
.replace(/ /g, "%20");
|
|
39
|
-
console.log("Context:", context.slice(0, 100));
|
|
40
52
|
return context;
|
|
41
53
|
}
|
|
42
54
|
}
|
|
43
55
|
return "";
|
|
44
56
|
}
|
|
45
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Parses log output to find the local dev server URL (e.g. http://localhost:3000).
|
|
60
|
+
* @param {string} log - Raw log output from the dev server
|
|
61
|
+
* @returns {string|null} The localhost URL, or null if not found
|
|
62
|
+
*/
|
|
63
|
+
function extractUrl(log) {
|
|
64
|
+
// Better regex for Local: line
|
|
65
|
+
const localMatch = log.match(/Local:\s+(http:\/\/localhost:\d+)/i);
|
|
66
|
+
if (localMatch) return localMatch[1];
|
|
67
|
+
|
|
68
|
+
// Fallback port
|
|
69
|
+
const portMatch = log.match(/localhost:(\d+)/);
|
|
70
|
+
if (portMatch) return `http://localhost:${portMatch[1]}`;
|
|
71
|
+
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Main entry point. Spawns the dev server command, pipes its output to a log
|
|
77
|
+
* file, and polls the log for ready/error signals to open the browser or AI helper.
|
|
78
|
+
*/
|
|
46
79
|
async function run() {
|
|
47
80
|
try {
|
|
48
81
|
await fsPromises.rm(logPath, { force: true });
|
|
49
82
|
} catch {}
|
|
50
|
-
console.log("🚀 Starting", cmdArgs.join(" "));
|
|
51
83
|
|
|
52
84
|
const proc = spawn(cmdArgs.join(" "), [], {
|
|
53
85
|
stdio: ["ignore", "pipe", "pipe", "ipc"],
|
|
54
86
|
shell: true,
|
|
87
|
+
detached: true,
|
|
55
88
|
});
|
|
56
89
|
|
|
57
90
|
const logStream = fs.createWriteStream(logPath);
|
|
@@ -61,44 +94,64 @@ async function run() {
|
|
|
61
94
|
proc.stderr?.pipe(logStream);
|
|
62
95
|
|
|
63
96
|
let opened = false;
|
|
64
|
-
let
|
|
97
|
+
let errorOpened = false;
|
|
98
|
+
let lastReadySeen = false;
|
|
99
|
+
let lastLogSize = 0;
|
|
100
|
+
let stableCount = 0;
|
|
65
101
|
|
|
66
|
-
// Aggressive 500ms poll + debug
|
|
67
102
|
const poll = setInterval(async () => {
|
|
68
|
-
pollCount++;
|
|
69
|
-
console.log(`⏱️ Poll #${pollCount}`);
|
|
70
|
-
if (opened) return;
|
|
71
103
|
try {
|
|
72
|
-
await sleep(100); // Brief settle
|
|
73
104
|
const stats = fs.statSync(logPath);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
105
|
+
const currentLog = await fsPromises.readFile(logPath, "utf8");
|
|
106
|
+
|
|
107
|
+
// Stability check LAST - check ready first!
|
|
108
|
+
const logChanged = stats.size !== lastLogSize;
|
|
109
|
+
if (logChanged && stats.size > 100) {
|
|
110
|
+
stableCount = 0;
|
|
111
|
+
lastLogSize = stats.size;
|
|
112
|
+
} else if (stats.size > 100) {
|
|
113
|
+
stableCount++;
|
|
114
|
+
if (stableCount >= 5) {
|
|
84
115
|
clearInterval(poll);
|
|
85
|
-
proc.kill();
|
|
86
116
|
return;
|
|
87
117
|
}
|
|
88
118
|
}
|
|
89
|
-
} catch (e) {
|
|
90
|
-
console.log("Poll error:", e.message);
|
|
91
|
-
}
|
|
92
|
-
}, 500);
|
|
93
119
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
120
|
+
// Error first
|
|
121
|
+
const errorRegex = /⨯|[Ss]yntax[Ee]rror|error|failed|exception/i;
|
|
122
|
+
if (errorRegex.test(currentLog) && !noAi && !errorOpened) {
|
|
123
|
+
const err = getErrorContext(currentLog);
|
|
124
|
+
const customPrompt = "Explain what the error is, how to fix it, then give a shell script with the best recommended solution.";
|
|
125
|
+
const prompt = encodeURIComponent(customPrompt + " ");
|
|
126
|
+
opener(`${aiBase}${prompt}next.js+${err}`);
|
|
127
|
+
errorOpened = true;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Ready check BEFORE stability stops us - with transition guard
|
|
132
|
+
const readyRegex = /(?:ready[\s-]*started server|Ready in \d+ms)/i;
|
|
133
|
+
const isReadyNow = readyRegex.test(currentLog);
|
|
134
|
+
if (isReadyNow && !lastReadySeen && !opened) {
|
|
135
|
+
opened = true;
|
|
136
|
+
lastReadySeen = true;
|
|
137
|
+
const url = extractUrl(currentLog);
|
|
138
|
+
if (url && !noOpen) {
|
|
139
|
+
try {
|
|
140
|
+
await waitOn(url, { timeout: 10000, http: true });
|
|
141
|
+
} catch {}
|
|
142
|
+
opener(url);
|
|
143
|
+
}
|
|
144
|
+
clearInterval(poll);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
lastReadySeen = isReadyNow;
|
|
148
|
+
} catch {}
|
|
149
|
+
}, pollDelay);
|
|
99
150
|
|
|
100
|
-
|
|
101
|
-
|
|
151
|
+
process.on("SIGINT", () => {
|
|
152
|
+
clearInterval(poll);
|
|
153
|
+
process.exit(0);
|
|
154
|
+
});
|
|
102
155
|
}
|
|
103
156
|
|
|
104
157
|
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.3",
|
|
4
4
|
"description": "Smart dev server launcher: error→AI search, success→auto-open browser. Any CLI tool.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"license": "MIT",
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"minimist": "^1.2.8",
|
|
23
|
-
"
|
|
23
|
+
"opener": "^1.5.2",
|
|
24
24
|
"wait-on": "^8.0.1"
|
|
25
25
|
},
|
|
26
26
|
"engines": {
|