dev3000 0.0.185 → 0.0.187

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 (237) hide show
  1. package/README.md +102 -13
  2. package/bin/dev3000 +1 -2
  3. package/dist/cdp-monitor.d.ts +7 -12
  4. package/dist/cdp-monitor.d.ts.map +1 -1
  5. package/dist/cdp-monitor.js +86 -170
  6. package/dist/cdp-monitor.js.map +1 -1
  7. package/dist/cli.d.ts +1 -2
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cli.js +1 -1369
  10. package/dist/cli.js.map +1 -1
  11. package/dist/index.d.ts +1 -2
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +0 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/portless.d.ts +10 -0
  16. package/dist/portless.d.ts.map +1 -1
  17. package/dist/portless.js +44 -1
  18. package/dist/portless.js.map +1 -1
  19. package/dist/runtime/bridge.d.ts +26 -0
  20. package/dist/runtime/bridge.d.ts.map +1 -0
  21. package/dist/runtime/bridge.js +340 -0
  22. package/dist/runtime/bridge.js.map +1 -0
  23. package/dist/runtime/chrome-isolation.d.ts +16 -0
  24. package/dist/runtime/chrome-isolation.d.ts.map +1 -0
  25. package/dist/runtime/chrome-isolation.js +97 -0
  26. package/dist/runtime/chrome-isolation.js.map +1 -0
  27. package/dist/runtime/cli.js +116 -26
  28. package/dist/runtime/cli.js.map +1 -1
  29. package/dist/runtime/event-views.d.ts +21 -0
  30. package/dist/runtime/event-views.d.ts.map +1 -0
  31. package/dist/runtime/event-views.js +53 -0
  32. package/dist/runtime/event-views.js.map +1 -0
  33. package/dist/runtime/harness.d.ts +16 -0
  34. package/dist/runtime/harness.d.ts.map +1 -0
  35. package/dist/runtime/harness.js +68 -0
  36. package/dist/runtime/harness.js.map +1 -0
  37. package/dist/runtime/local-proxy.d.ts +12 -0
  38. package/dist/runtime/local-proxy.d.ts.map +1 -0
  39. package/dist/runtime/local-proxy.js +114 -0
  40. package/dist/runtime/local-proxy.js.map +1 -0
  41. package/dist/runtime/runtime.d.ts +14 -0
  42. package/dist/runtime/runtime.d.ts.map +1 -1
  43. package/dist/runtime/runtime.js +82 -21
  44. package/dist/runtime/runtime.js.map +1 -1
  45. package/dist/runtime/sandbox-agent.d.ts +6 -0
  46. package/dist/runtime/sandbox-agent.d.ts.map +1 -0
  47. package/dist/runtime/sandbox-agent.js +35 -0
  48. package/dist/runtime/sandbox-agent.js.map +1 -0
  49. package/dist/runtime/sandbox-command.d.ts +9 -0
  50. package/dist/runtime/sandbox-command.d.ts.map +1 -0
  51. package/dist/runtime/sandbox-command.js +101 -0
  52. package/dist/runtime/sandbox-command.js.map +1 -0
  53. package/dist/runtime/sandbox-proxy.d.ts +11 -0
  54. package/dist/runtime/sandbox-proxy.d.ts.map +1 -0
  55. package/dist/runtime/sandbox-proxy.js +123 -0
  56. package/dist/runtime/sandbox-proxy.js.map +1 -0
  57. package/dist/runtime/sandbox-sessions.d.ts +31 -0
  58. package/dist/runtime/sandbox-sessions.d.ts.map +1 -0
  59. package/dist/runtime/sandbox-sessions.js +102 -0
  60. package/dist/runtime/sandbox-sessions.js.map +1 -0
  61. package/dist/runtime/sandbox.d.ts +21 -0
  62. package/dist/runtime/sandbox.d.ts.map +1 -0
  63. package/dist/runtime/sandbox.js +481 -0
  64. package/dist/runtime/sandbox.js.map +1 -0
  65. package/dist/utils/agent-browser.d.ts +2 -120
  66. package/dist/utils/agent-browser.d.ts.map +1 -1
  67. package/dist/utils/agent-browser.js +13 -376
  68. package/dist/utils/agent-browser.js.map +1 -1
  69. package/dist/utils/project-name.d.ts +11 -1
  70. package/dist/utils/project-name.d.ts.map +1 -1
  71. package/dist/utils/project-name.js +14 -1
  72. package/dist/utils/project-name.js.map +1 -1
  73. package/package.json +13 -22
  74. package/skills/d3k/SKILL.md +23 -1
  75. package/dist/commands/crawl.d.ts +0 -12
  76. package/dist/commands/crawl.d.ts.map +0 -1
  77. package/dist/commands/crawl.js +0 -101
  78. package/dist/commands/crawl.js.map +0 -1
  79. package/dist/commands/errors.d.ts +0 -14
  80. package/dist/commands/errors.d.ts.map +0 -1
  81. package/dist/commands/errors.js +0 -246
  82. package/dist/commands/errors.js.map +0 -1
  83. package/dist/commands/fix.d.ts +0 -13
  84. package/dist/commands/fix.d.ts.map +0 -1
  85. package/dist/commands/fix.js +0 -219
  86. package/dist/commands/fix.js.map +0 -1
  87. package/dist/commands/logs.d.ts +0 -13
  88. package/dist/commands/logs.d.ts.map +0 -1
  89. package/dist/commands/logs.js +0 -146
  90. package/dist/commands/logs.js.map +0 -1
  91. package/dist/commands/resume.d.ts +0 -11
  92. package/dist/commands/resume.d.ts.map +0 -1
  93. package/dist/commands/resume.js +0 -75
  94. package/dist/commands/resume.js.map +0 -1
  95. package/dist/commands/status.d.ts +0 -21
  96. package/dist/commands/status.d.ts.map +0 -1
  97. package/dist/commands/status.js +0 -44
  98. package/dist/commands/status.js.map +0 -1
  99. package/dist/components/AgentSelector.d.ts +0 -12
  100. package/dist/components/AgentSelector.d.ts.map +0 -1
  101. package/dist/components/AgentSelector.js +0 -99
  102. package/dist/components/AgentSelector.js.map +0 -1
  103. package/dist/components/PackageSelector.d.ts +0 -13
  104. package/dist/components/PackageSelector.d.ts.map +0 -1
  105. package/dist/components/PackageSelector.js +0 -79
  106. package/dist/components/PackageSelector.js.map +0 -1
  107. package/dist/constants/log-colors.d.ts +0 -43
  108. package/dist/constants/log-colors.d.ts.map +0 -1
  109. package/dist/constants/log-colors.js +0 -59
  110. package/dist/constants/log-colors.js.map +0 -1
  111. package/dist/dev-environment.d.ts +0 -187
  112. package/dist/dev-environment.d.ts.map +0 -1
  113. package/dist/dev-environment.js +0 -2248
  114. package/dist/dev-environment.js.map +0 -1
  115. package/dist/screencast-manager.d.ts +0 -78
  116. package/dist/screencast-manager.d.ts.map +0 -1
  117. package/dist/screencast-manager.js +0 -529
  118. package/dist/screencast-manager.js.map +0 -1
  119. package/dist/services/parsers/error-detectors/base.d.ts +0 -24
  120. package/dist/services/parsers/error-detectors/base.d.ts.map +0 -1
  121. package/dist/services/parsers/error-detectors/base.js +0 -44
  122. package/dist/services/parsers/error-detectors/base.js.map +0 -1
  123. package/dist/services/parsers/error-detectors/index.d.ts +0 -6
  124. package/dist/services/parsers/error-detectors/index.d.ts.map +0 -1
  125. package/dist/services/parsers/error-detectors/index.js +0 -6
  126. package/dist/services/parsers/error-detectors/index.js.map +0 -1
  127. package/dist/services/parsers/error-detectors/nextjs.d.ts +0 -9
  128. package/dist/services/parsers/error-detectors/nextjs.d.ts.map +0 -1
  129. package/dist/services/parsers/error-detectors/nextjs.js +0 -52
  130. package/dist/services/parsers/error-detectors/nextjs.js.map +0 -1
  131. package/dist/services/parsers/index.d.ts +0 -9
  132. package/dist/services/parsers/index.d.ts.map +0 -1
  133. package/dist/services/parsers/index.js +0 -10
  134. package/dist/services/parsers/index.js.map +0 -1
  135. package/dist/services/parsers/log-parsers/base.d.ts +0 -26
  136. package/dist/services/parsers/log-parsers/base.d.ts.map +0 -1
  137. package/dist/services/parsers/log-parsers/base.js +0 -6
  138. package/dist/services/parsers/log-parsers/base.js.map +0 -1
  139. package/dist/services/parsers/log-parsers/index.d.ts +0 -6
  140. package/dist/services/parsers/log-parsers/index.d.ts.map +0 -1
  141. package/dist/services/parsers/log-parsers/index.js +0 -5
  142. package/dist/services/parsers/log-parsers/index.js.map +0 -1
  143. package/dist/services/parsers/log-parsers/standard.d.ts +0 -9
  144. package/dist/services/parsers/log-parsers/standard.d.ts.map +0 -1
  145. package/dist/services/parsers/log-parsers/standard.js +0 -17
  146. package/dist/services/parsers/log-parsers/standard.js.map +0 -1
  147. package/dist/services/parsers/output-processor.d.ts +0 -39
  148. package/dist/services/parsers/output-processor.d.ts.map +0 -1
  149. package/dist/services/parsers/output-processor.js +0 -54
  150. package/dist/services/parsers/output-processor.js.map +0 -1
  151. package/dist/skills/d3k/internal-skill.md +0 -32
  152. package/dist/skills/index.d.ts +0 -54
  153. package/dist/skills/index.d.ts.map +0 -1
  154. package/dist/skills/index.js +0 -214
  155. package/dist/skills/index.js.map +0 -1
  156. package/dist/src/tui-interface-impl.tsx +0 -644
  157. package/dist/tui-interface-impl.d.ts +0 -30
  158. package/dist/tui-interface-impl.d.ts.map +0 -1
  159. package/dist/tui-interface-impl.js +0 -403
  160. package/dist/tui-interface-impl.js.map +0 -1
  161. package/dist/tui-interface-opentui.d.ts +0 -33
  162. package/dist/tui-interface-opentui.d.ts.map +0 -1
  163. package/dist/tui-interface-opentui.js +0 -1128
  164. package/dist/tui-interface-opentui.js.map +0 -1
  165. package/dist/tui-interface.d.ts +0 -38
  166. package/dist/tui-interface.d.ts.map +0 -1
  167. package/dist/tui-interface.js +0 -84
  168. package/dist/tui-interface.js.map +0 -1
  169. package/dist/utils/agent-detection.d.ts +0 -17
  170. package/dist/utils/agent-detection.d.ts.map +0 -1
  171. package/dist/utils/agent-detection.js +0 -81
  172. package/dist/utils/agent-detection.js.map +0 -1
  173. package/dist/utils/agent-selection.d.ts +0 -25
  174. package/dist/utils/agent-selection.d.ts.map +0 -1
  175. package/dist/utils/agent-selection.js +0 -73
  176. package/dist/utils/agent-selection.js.map +0 -1
  177. package/dist/utils/browser-command-argv.d.ts +0 -19
  178. package/dist/utils/browser-command-argv.d.ts.map +0 -1
  179. package/dist/utils/browser-command-argv.js +0 -149
  180. package/dist/utils/browser-command-argv.js.map +0 -1
  181. package/dist/utils/build-version.d.ts +0 -2
  182. package/dist/utils/build-version.d.ts.map +0 -1
  183. package/dist/utils/build-version.js +0 -8
  184. package/dist/utils/build-version.js.map +0 -1
  185. package/dist/utils/cli-options.d.ts +0 -5
  186. package/dist/utils/cli-options.d.ts.map +0 -1
  187. package/dist/utils/cli-options.js +0 -36
  188. package/dist/utils/cli-options.js.map +0 -1
  189. package/dist/utils/d3k-dir.d.ts +0 -3
  190. package/dist/utils/d3k-dir.d.ts.map +0 -1
  191. package/dist/utils/d3k-dir.js +0 -12
  192. package/dist/utils/d3k-dir.js.map +0 -1
  193. package/dist/utils/d3k-skill-installer.d.ts +0 -4
  194. package/dist/utils/d3k-skill-installer.d.ts.map +0 -1
  195. package/dist/utils/d3k-skill-installer.js +0 -30
  196. package/dist/utils/d3k-skill-installer.js.map +0 -1
  197. package/dist/utils/log-filename.d.ts +0 -30
  198. package/dist/utils/log-filename.d.ts.map +0 -1
  199. package/dist/utils/log-filename.js +0 -60
  200. package/dist/utils/log-filename.js.map +0 -1
  201. package/dist/utils/project-detector.d.ts +0 -25
  202. package/dist/utils/project-detector.d.ts.map +0 -1
  203. package/dist/utils/project-detector.js +0 -153
  204. package/dist/utils/project-detector.js.map +0 -1
  205. package/dist/utils/project-metadata.d.ts +0 -4
  206. package/dist/utils/project-metadata.d.ts.map +0 -1
  207. package/dist/utils/project-metadata.js +0 -48
  208. package/dist/utils/project-metadata.js.map +0 -1
  209. package/dist/utils/repo-validate.d.ts +0 -2
  210. package/dist/utils/repo-validate.d.ts.map +0 -1
  211. package/dist/utils/repo-validate.js +0 -10
  212. package/dist/utils/repo-validate.js.map +0 -1
  213. package/dist/utils/session.d.ts +0 -19
  214. package/dist/utils/session.d.ts.map +0 -1
  215. package/dist/utils/session.js +0 -65
  216. package/dist/utils/session.js.map +0 -1
  217. package/dist/utils/skill-installer.d.ts +0 -65
  218. package/dist/utils/skill-installer.d.ts.map +0 -1
  219. package/dist/utils/skill-installer.js +0 -265
  220. package/dist/utils/skill-installer.js.map +0 -1
  221. package/dist/utils/timestamp.d.ts +0 -20
  222. package/dist/utils/timestamp.d.ts.map +0 -1
  223. package/dist/utils/timestamp.js +0 -57
  224. package/dist/utils/timestamp.js.map +0 -1
  225. package/dist/utils/tmux-helpers.d.ts +0 -36
  226. package/dist/utils/tmux-helpers.d.ts.map +0 -1
  227. package/dist/utils/tmux-helpers.js +0 -118
  228. package/dist/utils/tmux-helpers.js.map +0 -1
  229. package/dist/utils/user-config.d.ts +0 -12
  230. package/dist/utils/user-config.d.ts.map +0 -1
  231. package/dist/utils/user-config.js +0 -46
  232. package/dist/utils/user-config.js.map +0 -1
  233. package/dist/utils/version-check.d.ts +0 -52
  234. package/dist/utils/version-check.d.ts.map +0 -1
  235. package/dist/utils/version-check.js +0 -304
  236. package/dist/utils/version-check.js.map +0 -1
  237. package/src/tui-interface-impl.tsx +0 -644
@@ -1,2248 +0,0 @@
1
- import chalk from "chalk";
2
- import { execSync, spawn, spawnSync } from "child_process";
3
- import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "fs";
4
- import https from "https";
5
- import { createConnection, createServer } from "net";
6
- import ora from "ora";
7
- import { homedir, tmpdir } from "os";
8
- import { dirname, join, resolve, sep } from "path";
9
- import { fileURLToPath } from "url";
10
- import { stripVTControlCharacters } from "util";
11
- import { CDPMonitor } from "./cdp-monitor.js";
12
- import { buildPortlessServerCommand, preparePortlessRuntime } from "./portless.js";
13
- import { readRuntimeSession } from "./runtime/session.js";
14
- import { ScreencastManager } from "./screencast-manager.js";
15
- import { NextJsErrorDetector, OutputProcessor, StandardLogParser } from "./services/parsers/index.js";
16
- import { listAvailableSkills } from "./skills/index.js";
17
- import { DevTUI } from "./tui-interface.js";
18
- import { ensureD3kSkill } from "./utils/d3k-skill-installer.js";
19
- import { readProjectAgentName, rememberProjectAgentName } from "./utils/project-metadata.js";
20
- import { getProjectDir, getProjectDisplayName, getProjectName } from "./utils/project-name.js";
21
- import { getApplicablePackages, installSkillPackage, isPackageInstalled } from "./utils/skill-installer.js";
22
- import { formatTimestamp } from "./utils/timestamp.js";
23
- import { checkForUpdates, performUpgradeAsync } from "./utils/version-check.js";
24
- // Vercel tools URL (legacy, kept for potential future use)
25
- // @ts-expect-error Unused but kept for reference
26
- const _VERCEL_TOOLS_URL = "https://mcp.vercel.com";
27
- /**
28
- * Check if the current project has a .vercel directory (indicating a Vercel project)
29
- * Kept for potential future use
30
- */
31
- // @ts-expect-error Unused but kept for potential future use
32
- function _hasVercelProject() {
33
- return existsSync(join(process.cwd(), ".vercel"));
34
- }
35
- /**
36
- * Gracefully terminate a process by first trying SIGTERM, waiting for graceful
37
- * shutdown, then falling back to SIGKILL if needed.
38
- *
39
- * This is important for processes like Next.js dev server that need to clean up
40
- * resources (like .next/dev/lock) before exiting.
41
- *
42
- * @param options - Kill options including PID and optional overrides for testing
43
- * @returns Result indicating how the process was terminated
44
- */
45
- export async function gracefulKillProcess(options) {
46
- const { pid, gracePeriodMs = 500, killFn = (p, s) => process.kill(p, s), delayFn = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), debugLog = () => { } } = options;
47
- const result = {
48
- terminated: false,
49
- graceful: false,
50
- forcedKill: false
51
- };
52
- // Try process group first (negative PID), fall back to direct PID
53
- const pgid = -pid;
54
- // Step 1: Send SIGTERM for graceful shutdown
55
- debugLog(`Sending SIGTERM to process group ${pgid} (PID: ${pid})`);
56
- try {
57
- killFn(pgid, "SIGTERM");
58
- }
59
- catch {
60
- // Process group may not exist, try direct kill
61
- try {
62
- killFn(pid, "SIGTERM");
63
- }
64
- catch {
65
- // Process may already be dead
66
- debugLog(`Process ${pid} not found for SIGTERM`);
67
- return result;
68
- }
69
- }
70
- // Step 2: Wait for graceful shutdown
71
- await delayFn(gracePeriodMs);
72
- // Step 3: Check if process is still running
73
- try {
74
- // Signal 0 checks if process exists without killing it
75
- killFn(pid, 0);
76
- // Process still running, need to force kill
77
- debugLog(`Process still running after SIGTERM, sending SIGKILL`);
78
- try {
79
- killFn(pgid, "SIGKILL");
80
- }
81
- catch {
82
- killFn(pid, "SIGKILL");
83
- }
84
- result.terminated = true;
85
- result.forcedKill = true;
86
- }
87
- catch {
88
- // Process already dead - graceful shutdown succeeded
89
- debugLog(`Process terminated gracefully after SIGTERM`);
90
- result.terminated = true;
91
- result.graceful = true;
92
- }
93
- return result;
94
- }
95
- class Logger {
96
- logFile;
97
- tail;
98
- dateTimeFormat;
99
- constructor(logFile, tail = false, dateTimeFormat = "local") {
100
- this.logFile = logFile;
101
- this.tail = tail;
102
- this.dateTimeFormat = dateTimeFormat;
103
- // Ensure directory exists
104
- const logDir = dirname(logFile);
105
- if (!existsSync(logDir)) {
106
- mkdirSync(logDir, { recursive: true });
107
- }
108
- // Touch the log file without truncating an explicit existing --log-file.
109
- appendFileSync(this.logFile, "");
110
- }
111
- log(source, message) {
112
- const timestamp = formatTimestamp(new Date(), this.dateTimeFormat);
113
- const logEntry = `[${timestamp}] [${source.toUpperCase()}] ${message}\n`;
114
- appendFileSync(this.logFile, logEntry);
115
- // If tail is enabled, also output to console
116
- if (this.tail) {
117
- process.stdout.write(logEntry);
118
- }
119
- }
120
- }
121
- /**
122
- * Detect if we're in a sandbox environment (Vercel Sandbox, Docker, etc.)
123
- * where lsof and other system utilities may not be available.
124
- */
125
- function isInSandbox() {
126
- return (process.env.VERCEL_SANDBOX === "1" ||
127
- process.env.VERCEL === "1" ||
128
- existsSync("/.dockerenv") ||
129
- existsSync("/run/.containerenv"));
130
- }
131
- function parsePortNumber(port) {
132
- if (!/^\d+$/.test(port)) {
133
- return null;
134
- }
135
- const parsed = Number.parseInt(port, 10);
136
- if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
137
- return null;
138
- }
139
- return parsed;
140
- }
141
- export function detectServerPortFromOutput(text) {
142
- return (text.match(/Running:\s+PORT=(\d+)/i)?.[1] ||
143
- text.match(/using available port (\d+) instead/i)?.[1] ||
144
- text.match(/Local:.*localhost:(\d+)/i)?.[1] ||
145
- null);
146
- }
147
- function escapeRegexForPkill(value) {
148
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
149
- }
150
- function killPortProcessesSync(port, debugLog) {
151
- if (isInSandbox()) {
152
- debugLog(`Skipping synchronous port kill in sandbox environment`);
153
- return;
154
- }
155
- const parsedPort = parsePortNumber(port);
156
- if (parsedPort === null) {
157
- debugLog(`Skipping synchronous port kill for invalid port: ${port}`);
158
- return;
159
- }
160
- try {
161
- const result = spawnSync("lsof", ["-ti", `:${parsedPort}`], {
162
- stdio: ["ignore", "pipe", "pipe"],
163
- encoding: "utf8",
164
- timeout: 5000
165
- });
166
- const pidStrings = result.stdout
167
- .split(/\s+/)
168
- .map((value) => value.trim())
169
- .filter((value) => /^\d+$/.test(value));
170
- for (const pidString of pidStrings) {
171
- try {
172
- process.kill(Number.parseInt(pidString, 10), "SIGKILL");
173
- }
174
- catch (error) {
175
- debugLog(`Failed to synchronously kill PID ${pidString} on port ${parsedPort}: ${error}`);
176
- }
177
- }
178
- debugLog(`Synchronous kill for port ${parsedPort} found ${pidStrings.length} process(es)`);
179
- }
180
- catch (error) {
181
- debugLog(`Synchronous kill error for port ${parsedPort}: ${error}`);
182
- }
183
- }
184
- function pkillByPattern(pattern, debugLog) {
185
- if (isInSandbox()) {
186
- debugLog(`Skipping pkill in sandbox environment`);
187
- return;
188
- }
189
- try {
190
- spawnSync("pkill", ["-f", pattern], {
191
- stdio: "ignore",
192
- timeout: 5000
193
- });
194
- }
195
- catch (error) {
196
- debugLog(`pkill failed for pattern ${pattern}: ${error}`);
197
- }
198
- }
199
- /**
200
- * Count active d3k instances by checking PID files in tmpdir.
201
- * Returns the count of running d3k processes (excluding the current one if specified).
202
- *
203
- * CRITICAL FOR PROCESS CLEANUP:
204
- * This function is used to reason about multi-instance shutdown behavior.
205
- */
206
- export function countActiveD3kInstances(excludeCurrentPid = false) {
207
- try {
208
- const tmpDir = tmpdir();
209
- const files = readdirSync(tmpDir);
210
- const pidFiles = files.filter((f) => f.startsWith("dev3000-") && f.endsWith(".pid"));
211
- let activeCount = 0;
212
- for (const pidFile of pidFiles) {
213
- try {
214
- const pidPath = join(tmpDir, pidFile);
215
- const pidStr = readFileSync(pidPath, "utf-8").trim();
216
- const pid = parseInt(pidStr, 10);
217
- if (Number.isNaN(pid))
218
- continue;
219
- // Skip current process if requested
220
- if (excludeCurrentPid && pid === process.pid)
221
- continue;
222
- // Check if process is still running (signal 0 just checks existence)
223
- process.kill(pid, 0);
224
- activeCount++;
225
- }
226
- catch {
227
- // Process doesn't exist or can't be signaled - not active
228
- }
229
- }
230
- return activeCount;
231
- }
232
- catch {
233
- // Can't read tmpdir - assume we're the only one
234
- return excludeCurrentPid ? 0 : 1;
235
- }
236
- }
237
- export function parseD3kLockContent(content) {
238
- const trimmed = content.trim();
239
- if (!trimmed)
240
- return null;
241
- if (/^\d+$/.test(trimmed)) {
242
- const pid = Number.parseInt(trimmed, 10);
243
- return Number.isInteger(pid) && pid > 0 ? { pid } : null;
244
- }
245
- try {
246
- const parsed = JSON.parse(trimmed);
247
- const pid = parsed?.pid;
248
- if (!parsed || typeof parsed !== "object" || typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) {
249
- return null;
250
- }
251
- return {
252
- pid,
253
- cwd: typeof parsed.cwd === "string" ? parsed.cwd : null,
254
- createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : null,
255
- processStartTime: typeof parsed.processStartTime === "string" ? parsed.processStartTime : null
256
- };
257
- }
258
- catch {
259
- return null;
260
- }
261
- }
262
- function getProcessPsField(pid, field) {
263
- try {
264
- const result = spawnSync("ps", ["-p", String(pid), "-o", `${field}=`], {
265
- encoding: "utf8",
266
- stdio: ["ignore", "pipe", "ignore"]
267
- });
268
- if (result.status !== 0)
269
- return null;
270
- const output = result.stdout.trim();
271
- return output || null;
272
- }
273
- catch {
274
- return null;
275
- }
276
- }
277
- export function getProcessStartTime(pid) {
278
- return getProcessPsField(pid, "lstart");
279
- }
280
- export function getProcessCommand(pid) {
281
- return getProcessPsField(pid, "command");
282
- }
283
- function isLikelyD3kCommand(command) {
284
- const normalized = command.toLowerCase();
285
- return normalized.includes("d3k") || normalized.includes("dev3000");
286
- }
287
- export function validateD3kLockContent(content, deps = {}) {
288
- const lockInfo = parseD3kLockContent(content);
289
- if (!lockInfo) {
290
- return {
291
- active: false,
292
- reason: "lock file contents are invalid",
293
- lockInfo: null
294
- };
295
- }
296
- const processExists = deps.processExists ||
297
- ((pid) => {
298
- try {
299
- process.kill(pid, 0);
300
- return true;
301
- }
302
- catch {
303
- return false;
304
- }
305
- });
306
- if (!processExists(lockInfo.pid)) {
307
- return {
308
- active: false,
309
- reason: `process ${lockInfo.pid} is not running`,
310
- lockInfo
311
- };
312
- }
313
- const getStartTime = deps.getProcessStartTime || getProcessStartTime;
314
- if (lockInfo.processStartTime) {
315
- const liveStartTime = getStartTime(lockInfo.pid);
316
- if (liveStartTime && liveStartTime !== lockInfo.processStartTime) {
317
- return {
318
- active: false,
319
- reason: `pid ${lockInfo.pid} was reused by a different process`,
320
- lockInfo
321
- };
322
- }
323
- if (liveStartTime === lockInfo.processStartTime) {
324
- return {
325
- active: true,
326
- reason: `process ${lockInfo.pid} matches recorded start time`,
327
- lockInfo
328
- };
329
- }
330
- }
331
- const getCommand = deps.getProcessCommand || getProcessCommand;
332
- const liveCommand = getCommand(lockInfo.pid);
333
- if (liveCommand && !isLikelyD3kCommand(liveCommand)) {
334
- return {
335
- active: false,
336
- reason: `pid ${lockInfo.pid} belongs to a non-d3k process`,
337
- lockInfo
338
- };
339
- }
340
- return {
341
- active: true,
342
- reason: `process ${lockInfo.pid} still owns the lock`,
343
- lockInfo
344
- };
345
- }
346
- /**
347
- * Check if a port is available for binding (no process is listening on it).
348
- * Used for finding available ports before starting servers.
349
- */
350
- async function isPortAvailable(port) {
351
- const portNumber = Number.parseInt(port, 10);
352
- if (!Number.isInteger(portNumber) || portNumber < 0 || portNumber > 65535) {
353
- return false;
354
- }
355
- const hasActiveListener = await Promise.any(["127.0.0.1", "::1", "localhost"].map((host) => new Promise((resolve, reject) => {
356
- const socket = createConnection({ host, port: portNumber });
357
- let settled = false;
358
- const finish = (result, shouldReject = false) => {
359
- if (settled)
360
- return;
361
- settled = true;
362
- socket.destroy();
363
- if (shouldReject) {
364
- reject(new Error("unreachable"));
365
- }
366
- else {
367
- resolve(result);
368
- }
369
- };
370
- socket.once("connect", () => finish(true));
371
- socket.once("timeout", () => finish(false, true));
372
- socket.once("error", (error) => {
373
- if (error.code === "ECONNREFUSED" ||
374
- error.code === "EHOSTUNREACH" ||
375
- error.code === "ENETUNREACH" ||
376
- error.code === "EINVAL") {
377
- finish(false, true);
378
- return;
379
- }
380
- finish(false);
381
- });
382
- socket.setTimeout(150);
383
- })))
384
- .then((result) => result)
385
- .catch(() => false);
386
- if (hasActiveListener) {
387
- return false;
388
- }
389
- return new Promise((resolve) => {
390
- const server = createServer();
391
- server.once("error", (error) => {
392
- // EADDRINUSE/EACCES both mean we cannot bind this port.
393
- if (error.code === "EADDRINUSE" || error.code === "EACCES") {
394
- resolve(false);
395
- return;
396
- }
397
- resolve(true);
398
- });
399
- server.once("listening", () => {
400
- server.close(() => resolve(true));
401
- });
402
- server.listen(portNumber);
403
- });
404
- }
405
- export async function isServerListening(port) {
406
- // Try HTTP first
407
- if (await tryHttpConnection(port)) {
408
- return { listening: true, https: false };
409
- }
410
- // Fall back to HTTPS (for servers using --experimental-https or similar)
411
- if (await tryHttpsConnection(port)) {
412
- return { listening: true, https: true };
413
- }
414
- return { listening: false, https: false };
415
- }
416
- export async function tryHttpConnection(port) {
417
- try {
418
- const controller = new AbortController();
419
- const timeout = setTimeout(() => controller.abort(), 2000);
420
- try {
421
- await fetch(`http://localhost:${port}/`, {
422
- method: "HEAD",
423
- signal: controller.signal
424
- });
425
- clearTimeout(timeout);
426
- // Any response (even 4xx/5xx) means server is listening
427
- return true;
428
- }
429
- catch (error) {
430
- clearTimeout(timeout);
431
- const errorMsg = error instanceof Error ? error.message : String(error);
432
- // ECONNREFUSED means no server is listening
433
- // AbortError means timeout (server might be starting)
434
- if (errorMsg.includes("ECONNREFUSED") || errorMsg.includes("fetch failed") || errorMsg.includes("aborted")) {
435
- return false;
436
- }
437
- // Other errors (like network issues) - assume not listening
438
- return false;
439
- }
440
- }
441
- catch {
442
- return false;
443
- }
444
- }
445
- export async function tryHttpsConnection(port) {
446
- return new Promise((resolve) => {
447
- const req = https.request({
448
- hostname: "localhost",
449
- port: Number(port),
450
- path: "/",
451
- method: "HEAD",
452
- rejectUnauthorized: false, // Accept self-signed certificates
453
- timeout: 2000
454
- }, () => {
455
- // Any response means server is listening
456
- req.destroy();
457
- resolve(true);
458
- });
459
- req.on("error", () => {
460
- req.destroy();
461
- resolve(false);
462
- });
463
- req.on("timeout", () => {
464
- req.destroy();
465
- resolve(false);
466
- });
467
- req.end();
468
- });
469
- }
470
- export async function findAvailablePort(startPort) {
471
- let port = startPort;
472
- while (port < 65535) {
473
- if (await isPortAvailable(port.toString())) {
474
- return port.toString();
475
- }
476
- port++;
477
- }
478
- throw new Error(`No available ports found starting from ${startPort}`);
479
- }
480
- // REMOVED: isNextjsMcpEnabled check - now using framework detection from cli.ts
481
- // Framework detection happens in cli.ts and is stored in session files for CLI integration
482
- async function autoInstallSkills(agentId, debugLog) {
483
- if (!agentId) {
484
- return;
485
- }
486
- const packages = getApplicablePackages();
487
- if (packages.length === 0) {
488
- return;
489
- }
490
- for (const pkg of packages) {
491
- if (isPackageInstalled(pkg, agentId)) {
492
- continue;
493
- }
494
- debugLog(`Auto-installing skill package: ${pkg.repo}`);
495
- const result = await installSkillPackage(pkg, "project", agentId);
496
- if (!result.success) {
497
- debugLog(`Skill install failed for ${pkg.repo}: ${result.error || "unknown error"}`);
498
- }
499
- }
500
- }
501
- // REMOVED: cleanup functions are no longer needed
502
- // CLI integration config files are now kept persistent across dev3000 restarts
503
- export function createPersistentLogFile(override) {
504
- if (override && override.length > 0) {
505
- const resolved = resolve(override);
506
- const logDir = dirname(resolved);
507
- try {
508
- if (!existsSync(logDir)) {
509
- mkdirSync(logDir, { recursive: true });
510
- }
511
- appendFileSync(resolved, "");
512
- return resolved;
513
- }
514
- catch (error) {
515
- throw new Error(`Failed to initialize --log-file at ${resolved}: ${error instanceof Error ? error.message : String(error)}`);
516
- }
517
- }
518
- // Get unique project name
519
- const projectName = getProjectName();
520
- // Use ~/.d3k/{projectName}/logs directory for persistent, accessible logs
521
- const logBaseDir = join(getProjectDir(), "logs");
522
- try {
523
- if (!existsSync(logBaseDir)) {
524
- mkdirSync(logBaseDir, { recursive: true });
525
- }
526
- return createLogFileInDir(logBaseDir, projectName);
527
- }
528
- catch (_error) {
529
- // Fallback to user's temp directory if ~/.d3k is not writable
530
- const fallbackDir = join(tmpdir(), "dev3000-logs");
531
- if (!existsSync(fallbackDir)) {
532
- mkdirSync(fallbackDir, { recursive: true });
533
- }
534
- return createLogFileInDir(fallbackDir, projectName);
535
- }
536
- }
537
- /**
538
- * Write session info for external tooling to discover.
539
- *
540
- * CRITICAL FOR PROCESS CLEANUP:
541
- * This writes the session.json file that contains chromePids and serverPid.
542
- * These PIDs are read during shutdown to kill the correct processes.
543
- *
544
- * The chromePids array is particularly important - it tracks which Chrome
545
- * processes belong to THIS d3k instance so we don't accidentally kill
546
- * Chrome instances from other d3k sessions.
547
- */
548
- export function writeSessionInfo(projectName, logFilePath, appPort, publicUrl, cdpUrl, chromePids, serverCommand, framework, serverPid, skillsInstalled, skillsAgentId, preferredBrowserTool, agentName, portless, ready) {
549
- const projectDir = getProjectDir();
550
- try {
551
- // Create project directory if it doesn't exist
552
- if (!existsSync(projectDir)) {
553
- mkdirSync(projectDir, { recursive: true });
554
- }
555
- const sessionFile = join(projectDir, "session.json");
556
- const persistedAgentName = readProjectAgentName();
557
- // Session file contains project info
558
- const sessionInfo = {
559
- projectName,
560
- logFilePath,
561
- appPort,
562
- publicUrl: publicUrl || null,
563
- cdpUrl: cdpUrl || null,
564
- startTime: new Date().toISOString(),
565
- pid: process.pid,
566
- cwd: process.cwd(),
567
- chromePids: chromePids || [],
568
- serverCommand: serverCommand || null,
569
- framework: framework || null,
570
- serverPid: serverPid || null,
571
- skillsInstalled: skillsInstalled || [],
572
- skillsAgentId: skillsAgentId || null,
573
- preferredBrowserTool: preferredBrowserTool || "agent-browser",
574
- agentName: agentName || persistedAgentName || null,
575
- portless: portless === true,
576
- ready: ready === true
577
- };
578
- // Write session file in project directory
579
- writeFileSync(sessionFile, JSON.stringify(sessionInfo, null, 2));
580
- if (sessionInfo.agentName) {
581
- rememberProjectAgentName(sessionInfo.agentName);
582
- }
583
- }
584
- catch (error) {
585
- // Non-fatal - just log a warning
586
- console.warn(chalk.yellow(`⚠️ Could not write session info: ${error}`));
587
- }
588
- }
589
- function getSessionInfo(projectName) {
590
- const sessionFile = join(homedir(), ".d3k", projectName, "session.json");
591
- try {
592
- if (existsSync(sessionFile)) {
593
- const sessionInfo = JSON.parse(readFileSync(sessionFile, "utf8"));
594
- return {
595
- serverPid: sessionInfo.serverPid ?? null,
596
- chromePids: sessionInfo.chromePids ?? [],
597
- cwd: sessionInfo.cwd ?? null
598
- };
599
- }
600
- }
601
- catch (_error) {
602
- // Non-fatal - return null
603
- }
604
- return null;
605
- }
606
- export function getSessionChromePids(projectName) {
607
- return getSessionInfo(projectName)?.chromePids ?? [];
608
- }
609
- function createLogFileInDir(baseDir, _projectName) {
610
- // Create short timestamp: MMDD-HHmmss (e.g., 0106-171301)
611
- const now = new Date();
612
- const timestamp = [
613
- String(now.getMonth() + 1).padStart(2, "0"),
614
- String(now.getDate()).padStart(2, "0"),
615
- "-",
616
- String(now.getHours()).padStart(2, "0"),
617
- String(now.getMinutes()).padStart(2, "0"),
618
- String(now.getSeconds()).padStart(2, "0")
619
- ].join("");
620
- // Create log file path (project name already in directory path)
621
- const logFileName = `${timestamp}.log`;
622
- const logFilePath = join(baseDir, logFileName);
623
- // Prune old logs (keep only 10 most recent)
624
- pruneOldLogs(baseDir);
625
- // Create the log file
626
- writeFileSync(logFilePath, "");
627
- return logFilePath;
628
- }
629
- function pruneOldLogs(baseDir) {
630
- try {
631
- // Find all log files in directory
632
- const files = readdirSync(baseDir)
633
- .filter((file) => file.endsWith(".log"))
634
- .map((file) => ({
635
- name: file,
636
- path: join(baseDir, file),
637
- mtime: statSync(join(baseDir, file)).mtime
638
- }))
639
- .sort((a, b) => b.mtime.getTime() - a.mtime.getTime()); // Most recent first
640
- // Keep only the 10 most recent, delete the rest
641
- if (files.length >= 10) {
642
- const filesToDelete = files.slice(9); // Keep first 9, delete the rest
643
- for (const file of filesToDelete) {
644
- try {
645
- unlinkSync(file.path);
646
- }
647
- catch (_error) {
648
- // Silently ignore deletion errors
649
- }
650
- }
651
- }
652
- }
653
- catch (_error) {
654
- // Silently ignore prune errors
655
- }
656
- }
657
- export class DevEnvironment {
658
- serverProcess = null;
659
- cdpMonitor = null;
660
- screencastManager = null;
661
- logger;
662
- outputProcessor;
663
- options;
664
- screenshotDir;
665
- pidFile;
666
- lockFile;
667
- spinner;
668
- version;
669
- isShuttingDown = false;
670
- serverStartTime = null;
671
- healthCheckTimer = null;
672
- tui = null;
673
- portChangeMessage = null;
674
- portDetected = false;
675
- serverUsesHttps = false;
676
- publicAppUrl = null;
677
- portlessRuntime = null;
678
- runtimeReady = false;
679
- /** Returns "https" or "http" based on detected server protocol */
680
- get serverProtocol() {
681
- return this.serverUsesHttps ? "https" : "http";
682
- }
683
- get localhostAppUrl() {
684
- return `${this.serverProtocol}://localhost:${this.options.port}`;
685
- }
686
- get preferredAppUrl() {
687
- return this.options.appUrl || this.publicAppUrl || this.localhostAppUrl;
688
- }
689
- constructor(options) {
690
- this.options = { ...options };
691
- this.logger = new Logger(options.logFile, options.tail || false, options.dateTimeFormat || "local");
692
- this.outputProcessor = new OutputProcessor(new StandardLogParser(), new NextJsErrorDetector());
693
- // Detect if running from compiled binary
694
- const execPath = process.execPath;
695
- const isCompiledBinary = execPath.includes("@d3k/darwin-") || execPath.includes("d3k-darwin-") || execPath.endsWith("/dev3000");
696
- let packageRoot;
697
- if (isCompiledBinary) {
698
- // For compiled binaries: bin/dev3000 -> package root
699
- const binDir = dirname(execPath);
700
- packageRoot = dirname(binDir);
701
- }
702
- else {
703
- // Normal install: dist/dev-environment.js -> package root
704
- const currentFile = fileURLToPath(import.meta.url);
705
- packageRoot = dirname(dirname(currentFile));
706
- }
707
- // Store screenshots in project-specific directory for local access
708
- const projectName = getProjectName();
709
- this.screenshotDir = options.screenshotsDir ? resolve(options.screenshotsDir) : join(getProjectDir(), "screenshots");
710
- // Use project-specific PID and lock files to allow multiple projects to run simultaneously
711
- this.pidFile = join(tmpdir(), `dev3000-${projectName}.pid`);
712
- this.lockFile = join(tmpdir(), `dev3000-${projectName}.lock`);
713
- // Allow CLI-level crash handlers to trigger emergency cleanup.
714
- globalThis.__d3kEmergencyShutdown = (reason, error) => {
715
- this.emergencyShutdown(1, reason, error);
716
- };
717
- // Read version - for compiled binaries, use injected version; otherwise read from package.json
718
- this.version = "0.0.0";
719
- // Check for compile-time injected version first
720
- if (typeof __D3K_VERSION__ !== "undefined") {
721
- this.version = __D3K_VERSION__;
722
- }
723
- else {
724
- try {
725
- const packageJsonPath = join(packageRoot, "package.json");
726
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
727
- this.version = packageJson.version;
728
- // Use git to detect if we're in the dev3000 source repository
729
- try {
730
- const gitRemote = execSync("git remote get-url origin 2>/dev/null", {
731
- cwd: packageRoot,
732
- encoding: "utf8"
733
- }).trim();
734
- if (gitRemote.includes("vercel-labs/dev3000") && !this.version.includes("canary")) {
735
- this.version += "-local";
736
- }
737
- }
738
- catch {
739
- // Not in git repo or no git - use version as-is
740
- }
741
- }
742
- catch (_error) {
743
- // Use fallback version
744
- }
745
- }
746
- // Initialize spinner for clean output management (only if not in TUI mode)
747
- this.spinner = ora({
748
- text: "Initializing...",
749
- spinner: "dots",
750
- isEnabled: !options.tui && !options.tail // Disable spinner in TUI mode
751
- });
752
- if (options.portless !== false) {
753
- const portless = preparePortlessRuntime(getProjectDisplayName());
754
- if (portless.success && portless.runtime) {
755
- this.portlessRuntime = portless.runtime;
756
- this.publicAppUrl = portless.runtime.url;
757
- }
758
- else {
759
- const reason = portless.error || "Portless initialization failed";
760
- throw new Error(`Canonical Portless routing is required by default. ${reason} To intentionally use direct localhost, pass \`--no-portless\`.`);
761
- }
762
- }
763
- // Ensure screenshot directory exists
764
- try {
765
- if (!existsSync(this.screenshotDir)) {
766
- mkdirSync(this.screenshotDir, { recursive: true });
767
- }
768
- }
769
- catch {
770
- // Fall back to temp directory if project dir isn't writable
771
- this.screenshotDir = join(tmpdir(), "d3k-screenshots");
772
- if (!existsSync(this.screenshotDir)) {
773
- mkdirSync(this.screenshotDir, { recursive: true });
774
- }
775
- }
776
- // Initialize project-specific D3K log file (clear for new session)
777
- this.initializeD3KLog();
778
- }
779
- async checkPortsAvailable(silent = false) {
780
- // No legacy server to clean up; continue with port checks.
781
- // Check if user explicitly set ports via CLI flags
782
- const userSetAppPort = this.options.userSetPort || false;
783
- // If user didn't set ports, find available ones first (before checking)
784
- if (!userSetAppPort && !this.portlessRuntime) {
785
- const startPort = parseInt(this.options.port, 10);
786
- const availablePort = await findAvailablePort(startPort);
787
- if (availablePort !== this.options.port) {
788
- if (!silent) {
789
- console.log(chalk.yellow(`Port ${this.options.port} is in use, using port ${availablePort} for app server`));
790
- }
791
- // Store message for TUI display
792
- this.portChangeMessage = `Port ${this.options.port} is in use, using port ${availablePort} for app server`;
793
- this.options.port = availablePort;
794
- }
795
- }
796
- // If user set explicit app port, fail if it's not available
797
- if (userSetAppPort) {
798
- const available = await isPortAvailable(this.options.port);
799
- if (!available) {
800
- if (this.spinner?.isSpinning) {
801
- this.spinner.fail(`Port ${this.options.port} is already in use`);
802
- }
803
- if (!silent) {
804
- console.log(chalk.yellow(`💡 To free up port ${this.options.port}, run: lsof -ti:${this.options.port} | xargs kill -9`));
805
- }
806
- if (this.tui) {
807
- await this.tui.shutdown();
808
- }
809
- throw new Error(`Port ${this.options.port} is already in use. Please free the port and try again.`);
810
- }
811
- }
812
- // Legacy server removed - only check app port availability
813
- }
814
- async checkProcessHealth() {
815
- if (this.isShuttingDown)
816
- return true; // Skip health check if already shutting down
817
- // In sandbox environments, skip lsof-based health checks since lsof doesn't exist
818
- // Trust that the sandbox manages process lifecycle
819
- if (isInSandbox()) {
820
- this.debugLog("Health check skipped: Running in sandbox environment");
821
- return true;
822
- }
823
- try {
824
- // Only check app port - legacy server has been removed
825
- const ports = [this.options.port];
826
- for (const port of ports) {
827
- const result = await new Promise((resolve, reject) => {
828
- const proc = spawn("lsof", ["-ti", `:${port}`], { stdio: "pipe" });
829
- let output = "";
830
- proc.stdout?.on("data", (data) => {
831
- output += data.toString();
832
- });
833
- proc.on("error", (err) => reject(err));
834
- proc.on("exit", () => resolve(output.trim()));
835
- });
836
- if (!result) {
837
- this.debugLog(`Health check failed: Port ${port} is no longer in use`);
838
- this.logger.log("server", `Health check failed: Critical process on port ${port} is no longer running`);
839
- return false;
840
- }
841
- }
842
- this.debugLog("Health check passed: All critical processes are running");
843
- return true;
844
- }
845
- catch (error) {
846
- this.debugLog(`Health check error: ${error}`);
847
- // Treat errors as non-fatal - network issues shouldn't kill the process
848
- return true;
849
- }
850
- }
851
- startHealthCheck() {
852
- // Start health checks every 10 seconds
853
- this.healthCheckTimer = setInterval(async () => {
854
- const isHealthy = await this.checkProcessHealth();
855
- if (!isHealthy) {
856
- console.log(chalk.yellow("⚠️ Critical processes no longer detected. Shutting down gracefully..."));
857
- this.gracefulShutdown();
858
- }
859
- }, 10000); // 10 seconds
860
- this.debugLog("Health check timer started (10 second intervals)");
861
- }
862
- stopHealthCheck() {
863
- if (this.healthCheckTimer) {
864
- clearInterval(this.healthCheckTimer);
865
- this.healthCheckTimer = null;
866
- this.debugLog("Health check timer stopped");
867
- }
868
- }
869
- emergencyShutdown(exitCode, reason, error) {
870
- if (this.isShuttingDown)
871
- return;
872
- this.isShuttingDown = true;
873
- this.debugLog(`Emergency shutdown requested (${reason})`);
874
- if (error) {
875
- const errorText = error instanceof Error ? error.stack || error.message : String(error);
876
- this.debugLog(`Emergency shutdown error: ${errorText}`);
877
- }
878
- // Stop CDP reconnection attempts before killing the app server.
879
- if (this.cdpMonitor) {
880
- this.cdpMonitor.prepareShutdown();
881
- }
882
- // Best-effort synchronous cleanup (mirrors SIGHUP handler).
883
- const port = this.options.port;
884
- this.debugLog(`Synchronous kill for port ${port}`);
885
- killPortProcessesSync(port, this.debugLog.bind(this));
886
- const projectName = getProjectName();
887
- const sessionInfo = getSessionInfo(projectName);
888
- const chromePids = sessionInfo?.chromePids ?? [];
889
- if (chromePids.length > 0) {
890
- this.debugLog(`Synchronously killing Chrome PIDs: [${chromePids.join(", ")}]`);
891
- for (const pid of chromePids) {
892
- try {
893
- process.kill(pid, "SIGTERM");
894
- }
895
- catch {
896
- // Ignore - process may already be dead
897
- }
898
- }
899
- }
900
- if (sessionInfo?.serverPid) {
901
- this.killServerPidIfOwned(sessionInfo.serverPid, sessionInfo.cwd, `emergency:${reason}`);
902
- }
903
- this.handleShutdown()
904
- .then(() => {
905
- process.exit(exitCode);
906
- })
907
- .catch(() => {
908
- process.exit(exitCode);
909
- });
910
- }
911
- async start() {
912
- if (readRuntimeSession())
913
- throw new Error("The skill runtime already owns this project; use d3k-runtime commands");
914
- // Check if another instance is already running for this project
915
- if (!this.acquireLock()) {
916
- console.error(chalk.red(`\n❌ Another dev3000 instance is already running for this project.`));
917
- console.error(chalk.yellow(` If you're sure no other instance is running, remove: ${this.lockFile}`));
918
- process.exit(1);
919
- }
920
- // Kill any orphaned server process from a previous run of this project
921
- this.cleanupOrphanedServer();
922
- // Check if TUI mode is enabled (default) and stdin supports it
923
- const canUseTUI = this.options.tui && process.stdin.isTTY;
924
- if (!canUseTUI && this.options.tui) {
925
- this.debugLog("TTY not available, falling back to non-TUI mode");
926
- }
927
- if (canUseTUI) {
928
- // Get unique project name
929
- const projectName = getProjectName();
930
- const projectDisplayName = getProjectDisplayName();
931
- // Check for updates in parallel with TUI startup (non-blocking)
932
- const updateCheckPromise = checkForUpdates().catch(() => null);
933
- // Start TUI interface with initial status and updated port
934
- this.tui = new DevTUI({
935
- appPort: this.options.port, // This may have been updated by checkPortsAvailable
936
- appUrl: this.options.appUrl || this.publicAppUrl,
937
- logFile: this.options.logFile,
938
- commandName: this.options.commandName,
939
- serversOnly: this.options.serversOnly,
940
- version: this.version,
941
- projectName: projectDisplayName,
942
- updateInfo: null, // Will be updated async after auto-upgrade
943
- onRequestShutdown: () => {
944
- // Direct shutdown callback from TUI - bypasses signal handling
945
- if (this.isShuttingDown)
946
- return;
947
- this.isShuttingDown = true;
948
- this.debugLog("TUI requested shutdown via callback");
949
- // Signal CDP monitor to stop reconnection attempts BEFORE killing the app server
950
- if (this.cdpMonitor) {
951
- this.cdpMonitor.prepareShutdown();
952
- }
953
- // CRITICAL: Kill port processes SYNCHRONOUSLY first, before anything else
954
- // This ensures cleanup happens even if the event loop gets interrupted
955
- const port = this.options.port;
956
- this.debugLog(`Synchronous kill for port ${port}`);
957
- killPortProcessesSync(port, this.debugLog.bind(this));
958
- // Now do the rest of cleanup async
959
- this.tui?.updateStatus("Shutting down...");
960
- this.handleShutdown()
961
- .then(() => {
962
- this.debugLog("Shutdown complete");
963
- process.exit(0);
964
- })
965
- .catch((error) => {
966
- this.debugLog(`Shutdown error: ${error}`);
967
- process.exit(1);
968
- });
969
- }
970
- });
971
- await this.tui.start();
972
- // Auto-upgrade if update available (non-blocking)
973
- updateCheckPromise.then(async (versionInfo) => {
974
- if (versionInfo?.updateAvailable && versionInfo.latestVersion && this.tui) {
975
- this.debugLog(`Update available: ${versionInfo.currentVersion} -> ${versionInfo.latestVersion}, auto-upgrading...`);
976
- // Perform upgrade in background
977
- const result = await performUpgradeAsync();
978
- if (result.success) {
979
- const newVersion = result.newVersion || versionInfo.latestVersion;
980
- this.debugLog(`Auto-upgrade successful: ${newVersion}`);
981
- // Show "Updated to vX.X.X" message (auto-hides after 10s)
982
- this.tui.updateUpdateInfo({ type: "updated", newVersion });
983
- }
984
- else {
985
- // Upgrade failed - show update available instead
986
- this.debugLog(`Auto-upgrade failed: ${result.error}, showing update available`);
987
- this.tui.updateUpdateInfo({ type: "available", latestVersion: versionInfo.latestVersion });
988
- }
989
- }
990
- });
991
- const installSkills = this.options.installSkills !== false;
992
- // Show initial status message
993
- if (installSkills) {
994
- await this.tui.updateStatus("d3k is checking for skill updates...");
995
- }
996
- // Install d3k skill early so it's available when Claude Code starts
997
- // This is important for --with-agent where both start simultaneously
998
- if (installSkills) {
999
- await ensureD3kSkill(this.options.skillsAgentId);
1000
- if (this.options.autoSkills) {
1001
- await autoInstallSkills(this.options.skillsAgentId, this.debugLog);
1002
- }
1003
- }
1004
- // Check ports in background after TUI is visible
1005
- await this.checkPortsAvailable(true); // silent mode for TUI
1006
- // Update the app port in TUI (may have changed during port check)
1007
- this.tui.updateAppPort(this.options.port);
1008
- this.tui.updateAppUrl(this.options.appUrl || this.publicAppUrl);
1009
- // Show port change message if needed
1010
- if (this.portChangeMessage) {
1011
- await this.tui.updateStatus(this.portChangeMessage);
1012
- // Clear the message after a moment
1013
- setTimeout(async () => {
1014
- if (this.tui) {
1015
- await this.tui.updateStatus("Setting up environment...");
1016
- }
1017
- }, 2000);
1018
- }
1019
- else {
1020
- await this.tui.updateStatus("Setting up environment...");
1021
- }
1022
- // Write our process group ID to PID file for cleanup
1023
- writeFileSync(this.pidFile, process.pid.toString());
1024
- // Setup cleanup handlers BEFORE starting TUI to ensure they work
1025
- this.setupCleanupHandlers();
1026
- // Start user's dev server
1027
- await this.tui.updateStatus("Starting your dev server...");
1028
- await this.startServer();
1029
- // Legacy tools server removed - using CLI commands instead
1030
- // await this.startMcpServer()
1031
- // Wait for servers to be ready
1032
- await this.tui.updateStatus("Waiting for app...");
1033
- const serverStarted = await this.waitForServer();
1034
- if (!serverStarted) {
1035
- this.writeStartupFailureContext(`App did not become reachable within ${this.options.startupTimeoutSeconds} seconds.`, 30);
1036
- await this.tui.updateStatus("❌ Server failed to start");
1037
- console.error(chalk.red(`\n❌ Your app server failed to start after ${this.options.startupTimeoutSeconds} seconds.`));
1038
- console.error(chalk.yellow(`Check the logs at ~/.d3k/${getProjectName()}/logs/ for errors.`));
1039
- console.error(chalk.yellow("Exiting without launching browser."));
1040
- await this.gracefulShutdown();
1041
- return;
1042
- }
1043
- // Update TUI with confirmed port (may have changed during server startup)
1044
- this.tui.updateAppPort(this.options.port);
1045
- this.tui.updateAppUrl(this.options.appUrl || this.publicAppUrl);
1046
- // Legacy tools server removed - using CLI commands instead
1047
- // await this.waitForMcpServer()
1048
- // Start CDP monitoring only if server started successfully and not in servers-only mode
1049
- if (!this.options.serversOnly && serverStarted) {
1050
- await this.tui.updateStatus(`Starting ${this.options.commandName} browser...`);
1051
- await this.startCDPMonitoringSync();
1052
- }
1053
- else if (!this.options.serversOnly) {
1054
- this.debugLog("Browser monitoring skipped - server failed to start");
1055
- }
1056
- else {
1057
- this.debugLog("Browser monitoring disabled via --servers-only flag");
1058
- }
1059
- // Write session info for tooling discovery (include CDP URL if browser monitoring was started)
1060
- this.runtimeReady = true;
1061
- this.writeCurrentSessionInfo(projectName);
1062
- // Clear status - ready!
1063
- await this.tui.updateStatus(null);
1064
- }
1065
- else {
1066
- // Non-TUI mode - original flow
1067
- console.log(chalk.hex("#A18CE5")(`Starting ${this.options.commandName} (v${this.version})`));
1068
- const installSkills = this.options.installSkills !== false;
1069
- // Install d3k skill early so it's available when Claude Code starts
1070
- // This is important for --with-agent where both start simultaneously
1071
- if (installSkills) {
1072
- await ensureD3kSkill(this.options.skillsAgentId);
1073
- if (this.options.autoSkills) {
1074
- await autoInstallSkills(this.options.skillsAgentId, this.debugLog);
1075
- }
1076
- }
1077
- // Start spinner
1078
- this.spinner.start("Checking ports...");
1079
- // Check if ports are available first
1080
- await this.checkPortsAvailable(false); // normal mode with console output
1081
- this.spinner.text = "Setting up environment...";
1082
- // Write our process group ID to PID file for cleanup
1083
- writeFileSync(this.pidFile, process.pid.toString());
1084
- // Setup cleanup handlers
1085
- this.setupCleanupHandlers();
1086
- // Start user's dev server
1087
- this.spinner.text = "Starting your dev server...";
1088
- await this.startServer();
1089
- // Legacy server removed - using CLI commands instead
1090
- // await this.startMcpServer()
1091
- // Wait for servers to be ready
1092
- this.spinner.text = "Waiting for app...";
1093
- const serverStarted = await this.waitForServer();
1094
- if (!serverStarted) {
1095
- this.writeStartupFailureContext(`App did not become reachable within ${this.options.startupTimeoutSeconds} seconds.`, 30);
1096
- this.spinner.fail("Server failed to start");
1097
- console.error(chalk.red(`\n❌ Your app server failed to start after ${this.options.startupTimeoutSeconds} seconds.`));
1098
- console.error(chalk.yellow(`Check the logs at ~/.d3k/${getProjectName()}/logs/ for errors.`));
1099
- console.error(chalk.yellow("Exiting without launching browser."));
1100
- await this.gracefulShutdown();
1101
- return;
1102
- }
1103
- // Legacy server removed - using CLI commands instead
1104
- // await this.waitForMcpServer()
1105
- // Start CDP monitoring only if server started successfully and not in servers-only mode
1106
- if (!this.options.serversOnly && serverStarted) {
1107
- this.spinner.text = `Starting ${this.options.commandName} browser...`;
1108
- await this.startCDPMonitoringSync();
1109
- }
1110
- else if (!this.options.serversOnly) {
1111
- this.debugLog("Browser monitoring skipped - server failed to start");
1112
- }
1113
- else {
1114
- this.debugLog("Browser monitoring disabled via --servers-only flag");
1115
- }
1116
- // Get project name for session info and Visual Timeline URL
1117
- const projectName = getProjectName();
1118
- this.runtimeReady = true;
1119
- this.writeCurrentSessionInfo(projectName);
1120
- // Complete startup with success message only in non-TUI mode
1121
- this.spinner.succeed("Development environment ready!");
1122
- // Regular console output (non-TUI default)
1123
- console.log(chalk.cyan(`Logs: ${this.options.logFile}`));
1124
- console.log(chalk.cyan("☝️ Give this to an AI to auto debug and fix your app\n"));
1125
- console.log(chalk.cyan(`🌐 Your App: ${this.preferredAppUrl}`));
1126
- console.log(chalk.cyan(`🔧 CLI Tools: d3k fix, d3k crawl`));
1127
- if (this.options.serversOnly) {
1128
- console.log(chalk.cyan("🖥️ Servers-only mode - browser monitoring disabled"));
1129
- }
1130
- console.log(chalk.cyan("\nUse Ctrl-C to stop.\n"));
1131
- // Auto-upgrade in non-TUI mode (non-blocking)
1132
- checkForUpdates()
1133
- .then(async (versionInfo) => {
1134
- if (versionInfo?.updateAvailable && versionInfo.latestVersion) {
1135
- this.debugLog(`Update available: ${versionInfo.currentVersion} -> ${versionInfo.latestVersion}, auto-upgrading...`);
1136
- const result = await performUpgradeAsync();
1137
- if (result.success) {
1138
- const newVersion = result.newVersion || versionInfo.latestVersion;
1139
- console.log(chalk.green(`\n✓ Updated to v${newVersion}`));
1140
- }
1141
- else {
1142
- // Upgrade failed - show update available
1143
- console.log(chalk.yellow(`\n↑ Update available: v${versionInfo.currentVersion} → v${versionInfo.latestVersion}`));
1144
- console.log(chalk.gray(` Run 'd3k upgrade' to update\n`));
1145
- }
1146
- }
1147
- })
1148
- .catch(() => {
1149
- // Silently ignore update check failures
1150
- });
1151
- }
1152
- // Start health monitoring after everything is ready
1153
- this.startHealthCheck();
1154
- }
1155
- async startServer() {
1156
- const launchCommand = this.portlessRuntime
1157
- ? buildPortlessServerCommand(this.portlessRuntime, this.options.serverCommand, {
1158
- appPort: this.options.userSetPort ? this.options.port : undefined,
1159
- customCommand: this.options.customServerCommand
1160
- })
1161
- : this.options.serverCommand;
1162
- this.debugLog(`Starting server process: ${launchCommand}`);
1163
- this.serverStartTime = Date.now();
1164
- const serverEnv = { ...process.env };
1165
- if (this.publicAppUrl) {
1166
- serverEnv.PORTLESS_URL = this.publicAppUrl;
1167
- serverEnv.__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS = ".localhost";
1168
- }
1169
- // Use the full command string with shell: true to properly handle complex commands
1170
- this.serverProcess = spawn(launchCommand, {
1171
- stdio: ["ignore", "pipe", "pipe"],
1172
- shell: true,
1173
- detached: true, // Run independently
1174
- env: serverEnv
1175
- });
1176
- this.debugLog(`Server process spawned with PID: ${this.serverProcess.pid}`);
1177
- // Log server output (to file only, reduce stdout noise)
1178
- this.serverProcess.stdout?.on("data", (data) => {
1179
- const text = data.toString();
1180
- const entries = this.outputProcessor.process(text, false);
1181
- entries.forEach((entry) => {
1182
- this.logger.log("server", entry.formatted);
1183
- // Detect when server switches to a different port
1184
- this.detectPortChange(text);
1185
- });
1186
- });
1187
- this.serverProcess.stderr?.on("data", (data) => {
1188
- const text = data.toString();
1189
- const entries = this.outputProcessor.process(text, true);
1190
- entries.forEach((entry) => {
1191
- this.logger.log("server", entry.formatted);
1192
- // Detect when server switches to a different port
1193
- this.detectPortChange(text);
1194
- // Show critical errors to console (parser determines what's critical)
1195
- if (entry.isCritical && entry.rawMessage) {
1196
- console.error(chalk.red("[ERROR]"), entry.rawMessage);
1197
- }
1198
- });
1199
- // Check for Next.js lock file error - show in TUI status since it's a critical startup error
1200
- if (text.includes("Unable to acquire lock")) {
1201
- const errorMsg = "❌ Another Next.js dev server is running. Kill it or remove .next/dev/lock";
1202
- if (this.tui) {
1203
- this.tui.updateStatus(errorMsg);
1204
- }
1205
- else {
1206
- console.error(chalk.red(errorMsg));
1207
- }
1208
- }
1209
- });
1210
- this.serverProcess.on("exit", (code) => {
1211
- if (this.isShuttingDown)
1212
- return; // Don't handle exits during shutdown
1213
- if (code !== 0 && code !== null) {
1214
- this.debugLog(`Server process exited with code ${code}`);
1215
- this.logger.log("server", `Server process exited with code ${code}`);
1216
- const timeSinceStart = this.serverStartTime ? Date.now() - this.serverStartTime : 0;
1217
- const isEarlyExit = timeSinceStart < 5000; // Less than 5 seconds
1218
- // Check if node_modules exists
1219
- const nodeModulesExists = existsSync(join(process.cwd(), "node_modules"));
1220
- // If it's an early exit and node_modules doesn't exist, show helpful message
1221
- if (isEarlyExit && !nodeModulesExists) {
1222
- this.writeStartupFailureContext(`Server exited early with code ${code}.`, 30);
1223
- if (this.spinner?.isSpinning) {
1224
- this.spinner.fail("Server script failed to start - missing dependencies");
1225
- }
1226
- else {
1227
- console.log(chalk.red("\n❌ Server script failed to start"));
1228
- }
1229
- console.log(chalk.yellow("💡 It looks like dependencies are not installed."));
1230
- console.log(chalk.yellow(" Run 'pnpm install' (or npm/yarn install) and try again."));
1231
- this.showRecentLogs();
1232
- this.gracefulShutdown();
1233
- return;
1234
- }
1235
- // If it's an early exit but node_modules exists, it's still likely a configuration issue
1236
- if (isEarlyExit) {
1237
- this.writeStartupFailureContext(`Server exited early with code ${code}.`, 30);
1238
- if (this.spinner?.isSpinning) {
1239
- this.spinner.fail(`Server script failed to start (exited with code ${code})`);
1240
- }
1241
- else {
1242
- console.log(chalk.red(`\n❌ Server script failed to start (exited with code ${code})`));
1243
- }
1244
- console.log(chalk.yellow("💡 Check your server command configuration and project setup"));
1245
- console.log(chalk.yellow(` Command: ${this.options.serverCommand}`));
1246
- this.showRecentLogs();
1247
- this.gracefulShutdown();
1248
- return;
1249
- }
1250
- // For later exits, any non-zero exit code should be treated as fatal
1251
- // Only ignore successful exit and specific signal-based exit codes:
1252
- // - Code 0: Success (not fatal)
1253
- // - Code 130: Ctrl+C (SIGINT)
1254
- // - Code 143: SIGTERM
1255
- const isFatalExit = code !== 0 && code !== 130 && code !== 143;
1256
- if (isFatalExit) {
1257
- this.writeStartupFailureContext(`Server process exited with code ${code}.`, 30);
1258
- // Stop spinner and show error for fatal exits
1259
- if (this.spinner?.isSpinning) {
1260
- this.spinner.fail(`Server process exited with code ${code}`);
1261
- }
1262
- else {
1263
- console.log(chalk.red(`\n❌ Server process exited with code ${code}`));
1264
- }
1265
- // Show recent log entries to help with debugging
1266
- this.showRecentLogs();
1267
- this.gracefulShutdown();
1268
- }
1269
- else {
1270
- // For non-fatal exits (like build failures), just log and continue
1271
- if (this.spinner?.isSpinning) {
1272
- this.spinner.text = "Server process restarted, waiting...";
1273
- }
1274
- }
1275
- }
1276
- });
1277
- }
1278
- isSessionCwdOwned(sessionCwd) {
1279
- if (!sessionCwd)
1280
- return false;
1281
- try {
1282
- const current = resolve(process.cwd());
1283
- const session = resolve(sessionCwd);
1284
- return session === current || current.startsWith(session + sep) || session.startsWith(current + sep);
1285
- }
1286
- catch {
1287
- return false;
1288
- }
1289
- }
1290
- isPidRunning(pid) {
1291
- try {
1292
- process.kill(pid, 0);
1293
- return true;
1294
- }
1295
- catch {
1296
- return false;
1297
- }
1298
- }
1299
- killServerPidIfOwned(pid, sessionCwd, reason) {
1300
- if (!this.isSessionCwdOwned(sessionCwd)) {
1301
- this.debugLog(`Skipping server PID ${pid} kill (${reason}): session cwd mismatch`);
1302
- return;
1303
- }
1304
- if (!this.isPidRunning(pid)) {
1305
- this.debugLog(`Skipping server PID ${pid} kill (${reason}): not running`);
1306
- return;
1307
- }
1308
- this.debugLog(`Killing server PID ${pid} (${reason})`);
1309
- try {
1310
- process.kill(pid, "SIGTERM");
1311
- }
1312
- catch {
1313
- // Ignore - process may already be dead
1314
- }
1315
- if (process.platform !== "win32") {
1316
- try {
1317
- process.kill(-pid, "SIGTERM");
1318
- }
1319
- catch {
1320
- // Ignore - process group may not exist
1321
- }
1322
- }
1323
- try {
1324
- process.kill(pid, 0);
1325
- process.kill(pid, "SIGKILL");
1326
- }
1327
- catch {
1328
- // Ignore - process may already be dead
1329
- }
1330
- if (process.platform !== "win32") {
1331
- try {
1332
- process.kill(-pid, "SIGKILL");
1333
- }
1334
- catch {
1335
- // Ignore - process group may not exist
1336
- }
1337
- }
1338
- if (!isInSandbox()) {
1339
- try {
1340
- spawnSync("pkill", ["-P", pid.toString()], { stdio: "ignore" });
1341
- }
1342
- catch {
1343
- // Ignore pkill errors
1344
- }
1345
- }
1346
- }
1347
- killTrackedChromePids(chromePids, reason) {
1348
- if (chromePids.length === 0)
1349
- return;
1350
- this.debugLog(`Killing tracked Chrome PIDs (${reason}): [${chromePids.join(", ")}]`);
1351
- for (const pid of chromePids) {
1352
- try {
1353
- process.kill(pid, "SIGTERM");
1354
- }
1355
- catch {
1356
- // Ignore - process may already be dead
1357
- }
1358
- try {
1359
- process.kill(pid, 0);
1360
- process.kill(pid, "SIGKILL");
1361
- }
1362
- catch {
1363
- // Ignore - process may already be dead
1364
- }
1365
- }
1366
- }
1367
- cleanupOrphanedServer() {
1368
- const projectName = getProjectName();
1369
- const sessionInfo = getSessionInfo(projectName);
1370
- if (!sessionInfo?.serverPid)
1371
- return;
1372
- this.killServerPidIfOwned(sessionInfo.serverPid, sessionInfo.cwd, "startup orphan cleanup");
1373
- }
1374
- acquireLock() {
1375
- try {
1376
- // Check if lock file exists
1377
- if (existsSync(this.lockFile)) {
1378
- const lockContent = readFileSync(this.lockFile, "utf8");
1379
- const validation = validateD3kLockContent(lockContent);
1380
- if (validation.active) {
1381
- this.debugLog(`Lock file is active: ${validation.reason}`);
1382
- return false;
1383
- }
1384
- this.debugLog(`Removing stale lock file: ${validation.reason}`);
1385
- unlinkSync(this.lockFile);
1386
- }
1387
- // Store enough metadata to detect PID reuse after crashes.
1388
- writeFileSync(this.lockFile, JSON.stringify({
1389
- pid: process.pid,
1390
- cwd: process.cwd(),
1391
- createdAt: new Date().toISOString(),
1392
- processStartTime: getProcessStartTime(process.pid)
1393
- }));
1394
- this.debugLog(`Acquired lock file: ${this.lockFile}`);
1395
- return true;
1396
- }
1397
- catch (error) {
1398
- this.debugLog(`Failed to acquire lock: ${error}`);
1399
- return false;
1400
- }
1401
- }
1402
- releaseLock() {
1403
- try {
1404
- if (existsSync(this.lockFile)) {
1405
- unlinkSync(this.lockFile);
1406
- this.debugLog(`Released lock file: ${this.lockFile}`);
1407
- }
1408
- }
1409
- catch (error) {
1410
- this.debugLog(`Failed to release lock: ${error}`);
1411
- }
1412
- }
1413
- writeCurrentSessionInfo(projectName = getProjectName()) {
1414
- const cdpUrl = this.cdpMonitor?.getCdpUrl() || null;
1415
- const chromePids = this.cdpMonitor?.getChromePids() || [];
1416
- const skillsInstalled = listAvailableSkills(process.cwd());
1417
- writeSessionInfo(projectName, this.options.logFile, this.options.port, this.options.appUrl || this.publicAppUrl, cdpUrl, chromePids, this.options.serverCommand, this.options.framework, this.serverProcess?.pid, skillsInstalled, this.options.skillsAgentId ?? null, this.options.browserTool, this.options.agentName ?? null, Boolean(this.portlessRuntime), this.runtimeReady);
1418
- }
1419
- detectPortChange(text) {
1420
- // Detect Next.js port switch: "⚠ Port 3000 is in use by process 39543, using available port 3001 instead."
1421
- // Also detect: "Local: http://localhost:3001"
1422
- const detectedPort = detectServerPortFromOutput(text);
1423
- if (detectedPort && detectedPort !== this.options.port) {
1424
- const oldPort = this.options.port;
1425
- this.debugLog(`Detected server port change from ${oldPort} to ${detectedPort}`);
1426
- this.logger.log("server", `[PORT] Server switched from port ${oldPort} to ${detectedPort}`);
1427
- this.options.port = detectedPort;
1428
- this.portDetected = true;
1429
- // Update session info with new port
1430
- const projectName = getProjectName();
1431
- if (this.cdpMonitor || this.serverProcess?.pid) {
1432
- this.writeCurrentSessionInfo(projectName);
1433
- this.debugLog(`Updated session info with new port: ${this.options.port}`);
1434
- }
1435
- // Update TUI header with new port
1436
- if (this.tui) {
1437
- this.tui.updateAppPort(detectedPort);
1438
- this.tui.updateAppUrl(this.options.appUrl || this.publicAppUrl);
1439
- }
1440
- // Navigate browser to new port if CDP monitor is active
1441
- if (this.cdpMonitor) {
1442
- this.debugLog(`Re-navigating browser from port ${oldPort} to ${detectedPort}`);
1443
- this.logger.log("browser", `[CDP] Port changed - navigating to ${this.preferredAppUrl}`);
1444
- this.cdpMonitor.navigateToUrl(this.preferredAppUrl).catch((error) => {
1445
- this.debugLog(`Failed to navigate browser to new port: ${error}`);
1446
- });
1447
- }
1448
- }
1449
- else if (!this.portDetected) {
1450
- // Fallback: detect generic server startup messages when no explicit port is found
1451
- // This handles test apps and servers that don't output port information
1452
- const serverStartPatterns = [
1453
- /server\s+(is\s+)?running/i,
1454
- /ready\s+(in|on)/i,
1455
- /listening\s+on/i,
1456
- /started\s+server/i,
1457
- /http:\/\/localhost/i
1458
- ];
1459
- if (serverStartPatterns.some((pattern) => pattern.test(text))) {
1460
- this.debugLog(`Detected server startup via generic message, using configured port ${this.options.port}`);
1461
- this.portDetected = true;
1462
- // Update TUI header with configured port
1463
- if (this.tui) {
1464
- this.tui.updateAppPort(this.options.port);
1465
- }
1466
- }
1467
- }
1468
- }
1469
- debugLog = (message) => {
1470
- const timestamp = formatTimestamp(new Date(), this.options.dateTimeFormat || "local");
1471
- if (this.options.debug) {
1472
- if (this.spinner?.isSpinning) {
1473
- // Temporarily stop the spinner, show debug message, then restart
1474
- const currentText = this.spinner.text;
1475
- this.spinner.stop();
1476
- console.log(chalk.gray(`[DEBUG] ${message}`));
1477
- this.spinner.start(currentText);
1478
- }
1479
- else {
1480
- console.log(chalk.gray(`[DEBUG] ${message}`));
1481
- }
1482
- }
1483
- // Always write to d3k debug log file (even when not in debug mode)
1484
- try {
1485
- const projectDir = getProjectDir();
1486
- if (!existsSync(projectDir)) {
1487
- mkdirSync(projectDir, { recursive: true });
1488
- }
1489
- const debugLogFile = join(projectDir, "debug.log");
1490
- const logEntry = `[${timestamp}] [DEBUG] ${message}\n`;
1491
- appendFileSync(debugLogFile, logEntry);
1492
- }
1493
- catch {
1494
- // Ignore debug log write errors
1495
- }
1496
- };
1497
- showRecentLogs() {
1498
- try {
1499
- if (existsSync(this.options.logFile)) {
1500
- const logContent = readFileSync(this.options.logFile, "utf8");
1501
- const lines = logContent
1502
- .trim()
1503
- .split("\n")
1504
- .filter((line) => line.trim());
1505
- if (lines.length > 0) {
1506
- // Show last 20 lines, or fewer if log is shorter
1507
- const recentLines = lines.slice(-20);
1508
- console.log(chalk.yellow("\n📋 Recent log entries:"));
1509
- for (const line of recentLines) {
1510
- console.log(chalk.gray(` ${line}`));
1511
- }
1512
- }
1513
- }
1514
- console.log(chalk.cyan(`\n📄 Full logs: ${this.options.logFile}`));
1515
- console.log(chalk.cyan(` Quick access: tail -f ${this.options.logFile}`));
1516
- }
1517
- catch (_error) {
1518
- // Fallback if we can't read the log file
1519
- console.log(chalk.yellow(`💡 Check logs for details: ${this.options.logFile}`));
1520
- }
1521
- }
1522
- getRecentLogLines(limit = 30) {
1523
- try {
1524
- if (!existsSync(this.options.logFile))
1525
- return [];
1526
- const logContent = readFileSync(this.options.logFile, "utf8");
1527
- return logContent
1528
- .split("\n")
1529
- .map((line) => line.trimEnd())
1530
- .filter((line) => line.trim() && !line.includes("[STARTUP_FAILURE]") && !line.includes("[STARTUP_CONTEXT]"))
1531
- .slice(-limit);
1532
- }
1533
- catch {
1534
- return [];
1535
- }
1536
- }
1537
- simplifyLogLine(line) {
1538
- // Strip ANSI escapes + non-printables, then remove "[time] [source]" prefix.
1539
- const noAnsi = stripVTControlCharacters(line).replace(/[^\t\r\n -~]/g, "");
1540
- return noAnsi.replace(/^\[[^\]]+\]\s+\[[^\]]+\]\s+/, "");
1541
- }
1542
- inferStartupFailureHint(logLines) {
1543
- const joined = logLines.join("\n");
1544
- if (/ERR_PNPM_NO_SCRIPT/i.test(joined) || /Missing script:\s*dev/i.test(joined)) {
1545
- return "Missing package script. Pass --script or --command for this project.";
1546
- }
1547
- if (/EADDRINUSE|port .* in use/i.test(joined)) {
1548
- return "Port conflict detected. Try another --port or stop the process using that port.";
1549
- }
1550
- if (/Cannot find module|MODULE_NOT_FOUND|ENOENT/i.test(joined)) {
1551
- return "Dependency or path issue detected. Verify installs and command paths.";
1552
- }
1553
- if (/permission denied|EACCES/i.test(joined)) {
1554
- return "Permission issue detected. Check executable permissions and environment access.";
1555
- }
1556
- if (/\/\/:dev:ts:/i.test(joined) && !/vercel-site:dev:|Local:\s+http|Ready in/i.test(joined)) {
1557
- return "Background typegen/typecheck is running, but app server has not started yet. Use a longer timeout or run the app-specific dev command.";
1558
- }
1559
- if (/Starting compilation in watch mode/i.test(joined) && !/Local:\s+http|Ready in|listening on/i.test(joined)) {
1560
- return "Compiler watch mode started, but no app port was detected yet. This may need a longer startup timeout.";
1561
- }
1562
- return null;
1563
- }
1564
- getStartupContextLines(limit = 30) {
1565
- const recentLines = this.getRecentLogLines(200);
1566
- if (recentLines.length === 0)
1567
- return [];
1568
- const errorPattern = /(^|[\s:])(error|failed|fatal|exception|unhandled rejection|err_|eaddrinuse|module_not_found|missing script|not found|timeout)([\s:.,]|$)/i;
1569
- const ignorePattern = /Found 0 errors/i;
1570
- const errorLines = recentLines.filter((line) => errorPattern.test(line) && !ignorePattern.test(line));
1571
- // Prefer error-focused context when available; otherwise fall back to the latest lines.
1572
- if (errorLines.length > 0) {
1573
- const focused = errorLines.slice(-20);
1574
- const tail = recentLines.slice(-10);
1575
- return [...focused, ...tail].slice(-limit);
1576
- }
1577
- return recentLines.slice(-limit);
1578
- }
1579
- writeStartupFailureContext(reason, contextLines = 30) {
1580
- const context = this.getStartupContextLines(contextLines);
1581
- const hint = this.inferStartupFailureHint(context);
1582
- this.logger.log("server", `[STARTUP_FAILURE] ${reason}`);
1583
- this.logger.log("server", `[STARTUP_FAILURE] Command: ${this.options.serverCommand}`);
1584
- this.logger.log("server", `[STARTUP_FAILURE] Full logs: ${this.options.logFile}`);
1585
- if (hint) {
1586
- this.logger.log("server", `[STARTUP_HINT] ${hint}`);
1587
- }
1588
- if (context.length > 0) {
1589
- this.logger.log("server", `[STARTUP_CONTEXT] Selected ${context.length} relevant log lines:`);
1590
- for (const line of context) {
1591
- this.logger.log("server", `[STARTUP_CONTEXT] ${this.simplifyLogLine(line)}`);
1592
- }
1593
- }
1594
- }
1595
- checkForCommonIssues() {
1596
- try {
1597
- if (!existsSync(this.options.logFile))
1598
- return;
1599
- const logContent = readFileSync(this.options.logFile, "utf8");
1600
- // Check for Next.js lock file issue (this fix also kills the process holding the port)
1601
- if (logContent.includes("Unable to acquire lock") && logContent.includes(".next/dev/lock")) {
1602
- console.log(chalk.yellow("\n💡 Detected Next.js lock file issue!"));
1603
- console.log(chalk.white(" Another Next.js dev server may be running or crashed without cleanup."));
1604
- console.log(chalk.white(" To fix, run:"));
1605
- console.log(chalk.cyan(" rm -f .next/dev/lock && pkill -f 'next dev'"));
1606
- return; // pkill also fixes the port-in-use issue, so skip that check
1607
- }
1608
- // Check for port in use (only if not a Next.js lock issue)
1609
- const portInUseMatch = logContent.match(/Port (\d+) is in use by process (\d+)/);
1610
- if (portInUseMatch) {
1611
- const [, port, pid] = portInUseMatch;
1612
- console.log(chalk.yellow(`\n💡 Port ${port} was already in use by process ${pid}`));
1613
- console.log(chalk.white(" To kill that process, run:"));
1614
- console.log(chalk.cyan(` kill -9 ${pid}`));
1615
- }
1616
- }
1617
- catch {
1618
- // Ignore errors reading log file
1619
- }
1620
- }
1621
- async waitForServer() {
1622
- const maxAttempts = this.options.startupTimeoutSeconds;
1623
- let attempts = 0;
1624
- const startTime = Date.now();
1625
- this.debugLog(`Waiting for server to report its port...`);
1626
- while (attempts < maxAttempts) {
1627
- const attemptStartTime = Date.now();
1628
- // Wait for port to be detected from server logs before checking
1629
- if (!this.portDetected) {
1630
- this.debugLog(`Port not yet detected from server logs, waiting... (attempt ${attempts + 1}/${maxAttempts})`);
1631
- await new Promise((resolve) => setTimeout(resolve, 1000));
1632
- attempts++;
1633
- continue;
1634
- }
1635
- // Now check if the detected port is actually listening
1636
- const currentPort = this.options.port;
1637
- try {
1638
- this.debugLog(`Server check attempt ${attempts + 1}/${maxAttempts}: checking port ${currentPort}`);
1639
- // Use HTTP-based check which works in sandboxes where lsof doesn't exist
1640
- const serverStatus = await isServerListening(currentPort);
1641
- const attemptTime = Date.now() - attemptStartTime;
1642
- if (serverStatus.listening) {
1643
- const totalTime = Date.now() - startTime;
1644
- this.serverUsesHttps = serverStatus.https;
1645
- // Update TUI to show correct protocol
1646
- if (this.tui) {
1647
- this.tui.updateUseHttps(serverStatus.https);
1648
- }
1649
- const protocol = serverStatus.https ? "HTTPS" : "HTTP";
1650
- this.debugLog(`Server is ready! Port ${currentPort} is listening (${protocol}). Total wait time: ${totalTime}ms (${attempts + 1} attempts)`);
1651
- return true;
1652
- }
1653
- else {
1654
- this.debugLog(`Port ${currentPort} not yet responding after ${attemptTime}ms`);
1655
- }
1656
- }
1657
- catch (error) {
1658
- const attemptTime = Date.now() - attemptStartTime;
1659
- this.debugLog(`Server check failed in ${attemptTime}ms: ${error instanceof Error ? error.message : "Unknown error"}`);
1660
- }
1661
- attempts++;
1662
- if (attempts < maxAttempts) {
1663
- this.debugLog(`Waiting 1 second before next attempt...`);
1664
- await new Promise((resolve) => setTimeout(resolve, 1000));
1665
- }
1666
- }
1667
- const totalTime = Date.now() - startTime;
1668
- this.debugLog(`Server readiness check timed out after ${totalTime}ms (${maxAttempts} attempts)`);
1669
- return false;
1670
- }
1671
- initializeD3KLog() {
1672
- try {
1673
- const projectDir = getProjectDir();
1674
- if (!existsSync(projectDir)) {
1675
- mkdirSync(projectDir, { recursive: true });
1676
- }
1677
- // Create D3K log file and clear it for new session
1678
- const d3kLogFile = join(projectDir, "d3k.log");
1679
- writeFileSync(d3kLogFile, "");
1680
- }
1681
- catch {
1682
- // Ignore D3K log initialization errors - non-critical
1683
- }
1684
- }
1685
- async startCDPMonitoringSync() {
1686
- // Skip if in servers-only mode
1687
- if (this.options.serversOnly) {
1688
- return;
1689
- }
1690
- try {
1691
- await this.startCDPMonitoring();
1692
- }
1693
- catch (error) {
1694
- console.error(chalk.red("⚠️ CDP monitoring setup failed:"), error);
1695
- this.logger.log("browser", `[CDP] Monitoring startup failed: ${error}`);
1696
- this.logger.log("browser", "[CDP] Browser monitoring disabled; app server will continue running.");
1697
- if (this.tui) {
1698
- this.tui.updateStatus("⚠ Browser monitoring failed; continuing with server logs only");
1699
- }
1700
- // Best-effort cleanup of partially initialized monitor, but keep d3k running.
1701
- if (this.cdpMonitor) {
1702
- try {
1703
- this.cdpMonitor.prepareShutdown();
1704
- await this.cdpMonitor.shutdown();
1705
- }
1706
- catch {
1707
- // Ignore cleanup errors; monitoring is already disabled
1708
- }
1709
- this.cdpMonitor = null;
1710
- }
1711
- this.debugLog(`CDP monitoring disabled due to startup failure: ${error instanceof Error ? error.message : error}`);
1712
- }
1713
- }
1714
- async startCDPMonitoring() {
1715
- // Skip if in servers-only mode
1716
- if (this.options.serversOnly) {
1717
- this.debugLog("Browser monitoring disabled via --servers-only flag");
1718
- return;
1719
- }
1720
- // Ensure profile directory exists
1721
- if (!existsSync(this.options.profileDir)) {
1722
- mkdirSync(this.options.profileDir, { recursive: true });
1723
- }
1724
- // Initialize CDP monitor with enhanced logging
1725
- this.cdpMonitor = new CDPMonitor(this.options.profileDir, this.screenshotDir, (_source, message) => {
1726
- this.logger.log("browser", message);
1727
- }, this.options.debug, this.options.browser, this.options.pluginReactScan, this.options.port, // App server port to monitor
1728
- this.preferredAppUrl, this.options.browserNavigationTimeoutSeconds * 1000, this.options.debugPort, // Chrome debug port
1729
- this.options.headless, // Headless mode for serverless/CI environments
1730
- this.options.framework, // Framework hint for optional React DevTools launch args
1731
- this.options.externalCdpBase);
1732
- try {
1733
- // Set up callback for when Chrome window is manually closed
1734
- this.cdpMonitor.setOnWindowClosedCallback(() => {
1735
- this.debugLog("Chrome window closed callback triggered, initiating graceful shutdown");
1736
- this.logger.log("browser", "[CDP] Chrome window was manually closed, shutting down d3k");
1737
- // Trigger graceful shutdown
1738
- this.gracefulShutdown();
1739
- });
1740
- // Start CDP monitoring
1741
- await this.cdpMonitor.start();
1742
- this.logger.log("browser", this.options.externalCdpBase
1743
- ? "[CDP] Connected to external DevTools Protocol endpoint"
1744
- : "[CDP] Chrome launched with DevTools Protocol monitoring");
1745
- // Update session info with CDP URL and Chrome PIDs now that we have them
1746
- const projectName = getProjectName();
1747
- const cdpUrl = this.cdpMonitor.getCdpUrl();
1748
- const chromePids = this.cdpMonitor.getChromePids();
1749
- // Start screencast manager for automatic jank detection
1750
- if (cdpUrl) {
1751
- this.screencastManager = new ScreencastManager(cdpUrl, (msg) => {
1752
- // Pass through CDP messages directly - they already have their own category tags
1753
- this.logger.log("browser", msg);
1754
- }, this.options.port.toString(), this.options.debug, this.options.appUrl || this.publicAppUrl);
1755
- await this.screencastManager.start();
1756
- // this.logger.log("browser", "[Screencast] Auto-capture enabled for navigation events")
1757
- }
1758
- // Always write session info after CDP monitoring starts - this is critical for
1759
- // sandbox environments where external tools poll for the cdpUrl in the session file
1760
- this.writeCurrentSessionInfo(projectName);
1761
- this.debugLog(`Updated session info with CDP URL: ${cdpUrl}, Chrome PIDs: [${chromePids.join(", ")}]`);
1762
- this.logger.log("browser", `[CDP] Session info written with cdpUrl: ${cdpUrl ? "available" : "null"}`);
1763
- this.logger.log("browser", `[CDP] Loading page will hand off to ${this.preferredAppUrl}`);
1764
- }
1765
- catch (error) {
1766
- // Log error and throw to trigger graceful shutdown
1767
- this.logger.log("browser", `[CDP] Failed to start CDP monitoring: ${error}`);
1768
- throw error;
1769
- }
1770
- }
1771
- async gracefulShutdown() {
1772
- if (this.isShuttingDown)
1773
- return; // Prevent multiple shutdown attempts
1774
- this.isShuttingDown = true;
1775
- // Stop health monitoring
1776
- this.stopHealthCheck();
1777
- // Stop screencast manager
1778
- if (this.screencastManager) {
1779
- await this.screencastManager.stop();
1780
- this.screencastManager = null;
1781
- }
1782
- // Read session info BEFORE deleting it (needed for cleanup)
1783
- const projectName = getProjectName();
1784
- const sessionInfo = getSessionInfo(projectName);
1785
- // Clean up session file
1786
- try {
1787
- const sessionFile = join(homedir(), ".d3k", projectName, "session.json");
1788
- if (existsSync(sessionFile)) {
1789
- unlinkSync(sessionFile);
1790
- }
1791
- }
1792
- catch (_error) {
1793
- // Non-fatal - ignore cleanup errors
1794
- }
1795
- // Check PID file ownership BEFORE deleting (needed for cleanup decisions)
1796
- let weOwnPidFile = false;
1797
- try {
1798
- if (existsSync(this.pidFile)) {
1799
- const pidInFile = parseInt(readFileSync(this.pidFile, "utf-8").trim(), 10);
1800
- weOwnPidFile = pidInFile === process.pid;
1801
- this.debugLog(`PID file check: file has ${pidInFile}, we are ${process.pid}, we own it: ${weOwnPidFile}`);
1802
- if (weOwnPidFile) {
1803
- unlinkSync(this.pidFile);
1804
- }
1805
- }
1806
- }
1807
- catch (_error) {
1808
- // Non-fatal - ignore cleanup errors
1809
- }
1810
- // Stop TUI if it's running
1811
- if (this.tui) {
1812
- await this.tui.shutdown();
1813
- this.tui = null;
1814
- }
1815
- // Stop spinner if it's running
1816
- if (this.spinner?.isSpinning) {
1817
- this.spinner.fail("Critical failure detected");
1818
- }
1819
- // Only show console messages if not in TUI mode
1820
- if (!this.options.tui) {
1821
- console.log(chalk.yellow(`🛑 Shutting down ${this.options.commandName} due to critical failure...`));
1822
- }
1823
- // Kill processes on both ports (skip in sandbox - lsof doesn't exist)
1824
- const killPortProcess = async (port, name) => {
1825
- if (isInSandbox()) {
1826
- console.log(chalk.gray(`ℹ️ Skipping ${name} port kill in sandbox environment`));
1827
- return;
1828
- }
1829
- if (parsePortNumber(port) === null) {
1830
- this.debugLog(`Skipping ${name} port kill for invalid port: ${port}`);
1831
- return;
1832
- }
1833
- try {
1834
- const { spawn } = await import("child_process");
1835
- const findPids = spawn("lsof", ["-ti", `:${port}`], { stdio: "pipe" });
1836
- let pidsOutput = "";
1837
- findPids.stdout?.on("data", (data) => {
1838
- pidsOutput += data.toString();
1839
- });
1840
- return new Promise((resolve) => {
1841
- findPids.on("exit", (code) => {
1842
- if (code === 0 && pidsOutput.trim()) {
1843
- const pids = pidsOutput.trim().split("\n").filter(Boolean);
1844
- let killedCount = 0;
1845
- for (const pid of pids) {
1846
- try {
1847
- process.kill(parseInt(pid.trim(), 10), "SIGKILL");
1848
- killedCount++;
1849
- }
1850
- catch {
1851
- // Ignore individual kill errors
1852
- }
1853
- }
1854
- if (killedCount > 0) {
1855
- console.log(chalk.green(`✅ Killed ${killedCount} ${name} process(es) on port ${port}`));
1856
- }
1857
- }
1858
- resolve();
1859
- });
1860
- });
1861
- }
1862
- catch (_error) {
1863
- console.log(chalk.gray(`⚠️ Could not kill ${name} on port ${port}`));
1864
- }
1865
- };
1866
- // Kill app server only
1867
- console.log(chalk.cyan("🔄 Killing app server..."));
1868
- await killPortProcess(this.options.port, "your app server");
1869
- // Shutdown CDP monitor if it was started
1870
- let chromeShutdownHandled = false;
1871
- if (this.cdpMonitor) {
1872
- try {
1873
- console.log(chalk.cyan("🔄 Closing CDP monitor..."));
1874
- await this.cdpMonitor.shutdown();
1875
- chromeShutdownHandled = true;
1876
- console.log(chalk.green("✅ CDP monitor closed"));
1877
- }
1878
- catch (_error) {
1879
- console.log(chalk.gray("⚠️ CDP monitor shutdown failed"));
1880
- }
1881
- }
1882
- if (!chromeShutdownHandled) {
1883
- // Safety net when CDP was unavailable: terminate only Chrome processes from this d3k session.
1884
- this.killTrackedChromePids(sessionInfo?.chromePids ?? [], "graceful shutdown");
1885
- }
1886
- // Kill server process and its children using the saved PID (from before session file was deleted)
1887
- if (sessionInfo?.serverPid) {
1888
- this.killServerPidIfOwned(sessionInfo.serverPid, sessionInfo.cwd, "graceful shutdown");
1889
- }
1890
- console.log(chalk.red(`❌ ${this.options.commandName} exited due to server failure`));
1891
- // Show recent log entries to help diagnose the issue
1892
- this.showRecentLogs();
1893
- // Check for common issues and provide specific guidance
1894
- this.checkForCommonIssues();
1895
- process.exit(1);
1896
- }
1897
- setupCleanupHandlers() {
1898
- this.debugLog(`Setting up cleanup handlers for ${this.options.tui ? "TUI" : "debug"} mode`);
1899
- // Debug: log when process is about to exit
1900
- process.on("exit", (code) => {
1901
- console.log(`[DEBUG] Process exiting with code ${code}`);
1902
- this.debugLog(`Process exiting with code ${code}`);
1903
- });
1904
- // Handle Ctrl+C to kill all processes
1905
- process.on("SIGINT", () => {
1906
- this.debugLog("SIGINT received");
1907
- // In TUI mode, the TUI already handles double-tap protection
1908
- if (this.options.tui && this.tui) {
1909
- this.debugLog("TUI mode - proceeding directly to shutdown");
1910
- // Fall through to shutdown code below
1911
- }
1912
- else {
1913
- // Non-TUI mode: proceed directly to shutdown
1914
- // (double Ctrl+C doesn't work reliably in bun compiled binaries)
1915
- this.debugLog("Non-TUI mode - proceeding to shutdown");
1916
- }
1917
- // Proceed with shutdown
1918
- if (this.isShuttingDown)
1919
- return; // Prevent multiple shutdown attempts
1920
- this.isShuttingDown = true;
1921
- this.debugLog("Starting shutdown");
1922
- // Signal CDP monitor to stop reconnection attempts
1923
- if (this.cdpMonitor) {
1924
- this.cdpMonitor.prepareShutdown();
1925
- }
1926
- // CRITICAL: Kill port processes SYNCHRONOUSLY first, before anything else
1927
- // This ensures cleanup happens even if the event loop gets interrupted
1928
- const port = this.options.port;
1929
- this.debugLog(`Synchronous kill for port ${port}`);
1930
- killPortProcessesSync(port, this.debugLog.bind(this));
1931
- const projectName = getProjectName();
1932
- const sessionInfo = getSessionInfo(projectName);
1933
- if (sessionInfo?.serverPid) {
1934
- this.killServerPidIfOwned(sessionInfo.serverPid, sessionInfo.cwd, "SIGINT");
1935
- }
1936
- if (this.options.tui && this.tui) {
1937
- // In TUI mode, show shutting down message
1938
- this.debugLog("Updating TUI status with shutdown message");
1939
- this.tui.updateStatus("Shutting down...");
1940
- }
1941
- // Set a timeout to force exit if shutdown takes too long
1942
- const forceExitTimeout = setTimeout(() => {
1943
- this.debugLog("Shutdown timeout reached, forcing exit");
1944
- process.exit(1);
1945
- }, 10000); // Give Chrome enough time to record a clean profile exit.
1946
- // Call async cleanup in a non-blocking way
1947
- this.handleShutdown()
1948
- .then(() => {
1949
- clearTimeout(forceExitTimeout);
1950
- this.debugLog("Graceful shutdown completed");
1951
- process.exit(0);
1952
- })
1953
- .catch((error) => {
1954
- clearTimeout(forceExitTimeout);
1955
- this.debugLog(`Shutdown error: ${error}`);
1956
- process.exit(1);
1957
- });
1958
- });
1959
- // Also handle SIGTERM
1960
- process.on("SIGTERM", () => {
1961
- if (this.isShuttingDown)
1962
- return;
1963
- this.isShuttingDown = true;
1964
- this.debugLog("SIGTERM received");
1965
- // Signal CDP monitor to stop reconnection attempts BEFORE killing the app server
1966
- if (this.cdpMonitor) {
1967
- this.cdpMonitor.prepareShutdown();
1968
- }
1969
- // CRITICAL: Kill port processes SYNCHRONOUSLY first, before anything else
1970
- const port = this.options.port;
1971
- this.debugLog(`Synchronous kill for port ${port}`);
1972
- killPortProcessesSync(port, this.debugLog.bind(this));
1973
- const projectName = getProjectName();
1974
- const sessionInfo = getSessionInfo(projectName);
1975
- if (sessionInfo?.serverPid) {
1976
- this.killServerPidIfOwned(sessionInfo.serverPid, sessionInfo.cwd, "SIGTERM");
1977
- }
1978
- this.handleShutdown()
1979
- .then(() => {
1980
- process.exit(0);
1981
- })
1982
- .catch(() => {
1983
- process.exit(1);
1984
- });
1985
- });
1986
- /**
1987
- * SIGHUP Handler - CRITICAL for tmux/TUI mode cleanup
1988
- *
1989
- * When tmux kills a pane (Ctrl+C in agent pane, or closing terminal), it sends
1990
- * SIGHUP to the process. We MUST clean up SYNCHRONOUSLY because tmux may
1991
- * terminate us very quickly after sending the signal.
1992
- *
1993
- * CLEANUP ORDER (tested and verified - DO NOT CHANGE without updating tests):
1994
- * 1. Signal CDP monitor to stop reconnection attempts
1995
- * 2. Kill dev server processes on app port (synchronous via lsof)
1996
- * 3. Kill Chrome PIDs from session.json (synchronous)
1997
- * 4. Call handleShutdown() for async cleanup
1998
- *
1999
- * INVARIANTS (enforced by tests in dev-environment.test.ts):
2000
- * - Chrome PIDs are stored per-session in session.json
2001
- * - We only kill Chrome instances WE spawned (via chromePids array)
2002
- * - All cleanup happens BEFORE process.exit()
2003
- *
2004
- * @see dev-environment.test.ts - "SIGHUP handler cleanup" test suite
2005
- */
2006
- process.on("SIGHUP", () => {
2007
- this.debugLog("SIGHUP received (tmux session closing)");
2008
- this.emergencyShutdown(0, "SIGHUP");
2009
- });
2010
- }
2011
- async handleShutdown() {
2012
- // Stop health monitoring
2013
- this.stopHealthCheck();
2014
- // Release the lock file
2015
- this.releaseLock();
2016
- // Read session info BEFORE deleting it (needed for cleanup)
2017
- const projectName = getProjectName();
2018
- const sessionInfo = getSessionInfo(projectName);
2019
- // Clean up session file
2020
- try {
2021
- const sessionFile = join(homedir(), ".d3k", projectName, "session.json");
2022
- if (existsSync(sessionFile)) {
2023
- unlinkSync(sessionFile);
2024
- }
2025
- }
2026
- catch (_error) {
2027
- // Non-fatal - ignore cleanup errors
2028
- }
2029
- // Check PID file ownership BEFORE deleting (needed for cleanup decisions)
2030
- let weOwnPidFile = false;
2031
- try {
2032
- if (existsSync(this.pidFile)) {
2033
- const pidInFile = parseInt(readFileSync(this.pidFile, "utf-8").trim(), 10);
2034
- weOwnPidFile = pidInFile === process.pid;
2035
- this.debugLog(`PID file check: file has ${pidInFile}, we are ${process.pid}, we own it: ${weOwnPidFile}`);
2036
- // Only delete the PID file if we own it
2037
- if (weOwnPidFile) {
2038
- unlinkSync(this.pidFile);
2039
- this.debugLog("Deleted our PID file");
2040
- }
2041
- }
2042
- else {
2043
- // PID file doesn't exist - might have been cleaned by another process
2044
- this.debugLog("PID file doesn't exist");
2045
- }
2046
- }
2047
- catch (_error) {
2048
- // Non-fatal - ignore cleanup errors
2049
- this.debugLog(`PID file check error: ${_error}`);
2050
- }
2051
- // Stop TUI if it's running
2052
- if (this.tui) {
2053
- await this.tui.shutdown();
2054
- this.tui = null;
2055
- }
2056
- // Stop spinner if it's running
2057
- if (this.spinner?.isSpinning) {
2058
- this.spinner.fail("Interrupted");
2059
- }
2060
- // Only show console messages if not in TUI mode
2061
- if (!this.options.tui) {
2062
- console.log(chalk.yellow("\n🛑 Received interrupt signal. Cleaning up processes..."));
2063
- }
2064
- // Shutdown CDP monitor FIRST - this should close Chrome
2065
- let chromeShutdownHandled = false;
2066
- if (this.cdpMonitor) {
2067
- try {
2068
- if (!this.options.tui) {
2069
- console.log(chalk.cyan("🔄 Closing Chrome browser..."));
2070
- }
2071
- await this.cdpMonitor.shutdown();
2072
- chromeShutdownHandled = true;
2073
- if (!this.options.tui) {
2074
- console.log(chalk.green("✅ Chrome browser closed"));
2075
- }
2076
- }
2077
- catch (_error) {
2078
- if (!this.options.tui) {
2079
- console.log(chalk.gray("⚠️ Chrome shutdown failed"));
2080
- }
2081
- // Fallback: force kill any remaining Chrome processes for THIS instance only
2082
- try {
2083
- const chromePids = sessionInfo?.chromePids ?? [];
2084
- if (chromePids.length > 0) {
2085
- this.debugLog(`Fallback cleanup: killing Chrome PIDs for this instance: [${chromePids.join(", ")}]`);
2086
- const { spawn } = await import("child_process");
2087
- for (const pid of chromePids) {
2088
- try {
2089
- spawn("kill", ["-9", pid.toString()], { stdio: "ignore" });
2090
- }
2091
- catch {
2092
- // Ignore individual kill errors
2093
- }
2094
- }
2095
- }
2096
- else {
2097
- this.debugLog("Fallback cleanup: no Chrome PIDs found for this instance");
2098
- }
2099
- if (sessionInfo?.serverPid) {
2100
- this.killServerPidIfOwned(sessionInfo.serverPid, sessionInfo.cwd, "TUI shutdown");
2101
- }
2102
- }
2103
- catch {
2104
- // Ignore errors in fallback cleanup
2105
- }
2106
- }
2107
- }
2108
- if (!chromeShutdownHandled) {
2109
- // Safety net: ensure tracked Chrome processes are always terminated on shutdown.
2110
- this.killTrackedChromePids(sessionInfo?.chromePids ?? [], "handleShutdown");
2111
- }
2112
- // REMOVED: No longer clean up CLI config files on shutdown
2113
- // This was causing Claude Code instances to crash when dev3000 was killed
2114
- // Config file cleanup removed; keep user config files untouched on shutdown
2115
- // Kill processes on both ports (skip in sandbox - lsof doesn't exist)
2116
- const killPortProcess = async (port, name) => {
2117
- // Skip lsof-based kill in sandbox environments
2118
- if (isInSandbox()) {
2119
- this.debugLog(`Skipping ${name} port kill in sandbox environment`);
2120
- return;
2121
- }
2122
- if (parsePortNumber(port) === null) {
2123
- this.debugLog(`Skipping ${name} port kill for invalid port: ${port}`);
2124
- return;
2125
- }
2126
- try {
2127
- const { spawn } = await import("child_process");
2128
- // First, find PIDs on the port
2129
- const findPids = spawn("lsof", ["-ti", `:${port}`], { stdio: "pipe" });
2130
- let pidsOutput = "";
2131
- findPids.stdout?.on("data", (data) => {
2132
- pidsOutput += data.toString();
2133
- });
2134
- return new Promise((resolve) => {
2135
- findPids.on("exit", (code) => {
2136
- if (code === 0 && pidsOutput.trim()) {
2137
- // Found PIDs, now kill them
2138
- const pids = pidsOutput.trim().split("\n").filter(Boolean);
2139
- this.debugLog(`Found PIDs on port ${port}: [${pids.join(", ")}]`);
2140
- // Kill each PID individually
2141
- let killedCount = 0;
2142
- for (const pid of pids) {
2143
- try {
2144
- process.kill(parseInt(pid.trim(), 10), "SIGKILL");
2145
- killedCount++;
2146
- this.debugLog(`Killed PID ${pid} on port ${port}`);
2147
- }
2148
- catch (error) {
2149
- this.debugLog(`Failed to kill PID ${pid}: ${error}`);
2150
- }
2151
- }
2152
- if (killedCount > 0 && !this.options.tui) {
2153
- console.log(chalk.green(`✅ Killed ${killedCount} ${name} process(es) on port ${port}`));
2154
- }
2155
- }
2156
- else {
2157
- this.debugLog(`No processes found on port ${port} (exit code: ${code})`);
2158
- if (!this.options.tui) {
2159
- console.log(chalk.gray(`ℹ️ No ${name} running on port ${port}`));
2160
- }
2161
- }
2162
- resolve();
2163
- });
2164
- });
2165
- }
2166
- catch (error) {
2167
- this.debugLog(`Error killing processes on port ${port}: ${error}`);
2168
- if (!this.options.tui) {
2169
- console.log(chalk.gray(`⚠️ Could not kill ${name} on port ${port}`));
2170
- }
2171
- }
2172
- };
2173
- // Kill app server
2174
- if (!this.options.tui) {
2175
- console.log(chalk.yellow("🔄 Killing app server..."));
2176
- }
2177
- // IMPORTANT: With shell: true, the shell process exits quickly after spawning
2178
- // the actual command, leaving Next.js orphaned (reparented to PID 1).
2179
- // We use SYNCHRONOUS lsof kill first to ensure it completes before process exits.
2180
- // Primary: Synchronous kill - most reliable, ensures completion
2181
- // NOTE: We always try this, even if lsof might not exist - errors are caught
2182
- try {
2183
- killPortProcessesSync(this.options.port, this.debugLog.bind(this));
2184
- }
2185
- catch (error) {
2186
- this.debugLog(`Synchronous kill error: ${error}`);
2187
- }
2188
- // Also try async kill as backup
2189
- await killPortProcess(this.options.port, "your app server");
2190
- // Also try to kill the process group if we have the reference (belt and suspenders)
2191
- if (this.serverProcess?.pid) {
2192
- try {
2193
- const result = await gracefulKillProcess({
2194
- pid: this.serverProcess.pid,
2195
- gracePeriodMs: 500,
2196
- debugLog: (msg) => this.debugLog(msg)
2197
- });
2198
- if (result.terminated) {
2199
- this.debugLog(`Killed server process group (PID: ${this.serverProcess.pid})`);
2200
- }
2201
- }
2202
- catch (error) {
2203
- this.debugLog(`Could not kill process group: ${error}`);
2204
- }
2205
- }
2206
- // Wait for processes to fully terminate
2207
- await new Promise((resolve) => setTimeout(resolve, 300));
2208
- // Second pass: kill any remaining processes on the port
2209
- await killPortProcess(this.options.port, "your app server");
2210
- // Double-check: try to kill any remaining processes on the app port
2211
- try {
2212
- const parsedPort = parsePortNumber(this.options.port);
2213
- if (parsedPort !== null) {
2214
- pkillByPattern(`:${parsedPort}`, this.debugLog.bind(this));
2215
- this.debugLog(`Sent pkill signal for port ${parsedPort}`);
2216
- }
2217
- // Specifically kill any remaining next dev processes in the current directory
2218
- // This catches cases where the shell wrapper exited but next-server survived
2219
- const cwd = process.cwd();
2220
- const escapedCwd = escapeRegexForPkill(cwd);
2221
- pkillByPattern(`next dev.*${escapedCwd}`, this.debugLog.bind(this));
2222
- pkillByPattern(`next-server.*${escapedCwd}`, this.debugLog.bind(this));
2223
- this.debugLog(`Sent pkill signal for next processes in ${cwd}`);
2224
- // Kill server process and its children using the saved PID (from before session file was deleted)
2225
- if (sessionInfo?.serverPid) {
2226
- this.killServerPidIfOwned(sessionInfo.serverPid, sessionInfo.cwd, "handleShutdown");
2227
- }
2228
- // Final synchronous lsof kill - most reliable method
2229
- killPortProcessesSync(this.options.port, this.debugLog.bind(this));
2230
- }
2231
- catch {
2232
- // Ignore pkill errors
2233
- }
2234
- if (!this.options.tui) {
2235
- console.log(chalk.green("✅ Cleanup complete"));
2236
- }
2237
- }
2238
- }
2239
- export async function startDevEnvironment(options) {
2240
- // Clear terminal before starting TUI (unless in servers-only or debug mode)
2241
- if (options.tui && !options.serversOnly && !options.debug) {
2242
- // ANSI escape codes: clear screen + move cursor to top-left
2243
- process.stdout.write("\x1b[2J\x1b[0f");
2244
- }
2245
- const devEnv = new DevEnvironment(options);
2246
- await devEnv.start();
2247
- }
2248
- //# sourceMappingURL=dev-environment.js.map