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.
Files changed (2) hide show
  1. package/open-when-ready.mjs +90 -37
  2. package/package.json +2 -2
@@ -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 open from "open";
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:./\-]/g, "")
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 pollCount = 0;
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
- console.log("📊 Log size:", stats.size);
75
- if (stats.size > 100) {
76
- const log = await fsPromises.readFile(logPath, "utf8");
77
- const errorRegex = /⨯|[Ss]yntax[Ee]rror|error/i;
78
- if (errorRegex.test(log) && !noAi) {
79
- console.log("🎯 ERROR TRIGGERED!");
80
- const err = getErrorContext(log);
81
- console.log("🔗 Opening:", `${aiBase}next.js+${err.slice(0, 50)}...`);
82
- await open(`${aiBase}next.js+${err}`);
83
- opened = true;
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
- setTimeout(() => {
95
- clearInterval(poll);
96
- proc.kill();
97
- }, 30000);
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
- function sleep(ms) {
101
- return new Promise((r) => setTimeout(r, ms));
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.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
- "open": "^10.1.0",
23
+ "opener": "^1.5.2",
24
24
  "wait-on": "^8.0.1"
25
25
  },
26
26
  "engines": {