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.
- package/README.md +102 -13
- package/bin/dev3000 +1 -2
- package/dist/cdp-monitor.d.ts +7 -12
- package/dist/cdp-monitor.d.ts.map +1 -1
- package/dist/cdp-monitor.js +86 -170
- package/dist/cdp-monitor.js.map +1 -1
- package/dist/cli.d.ts +1 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -1369
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/portless.d.ts +10 -0
- package/dist/portless.d.ts.map +1 -1
- package/dist/portless.js +44 -1
- package/dist/portless.js.map +1 -1
- package/dist/runtime/bridge.d.ts +26 -0
- package/dist/runtime/bridge.d.ts.map +1 -0
- package/dist/runtime/bridge.js +340 -0
- package/dist/runtime/bridge.js.map +1 -0
- package/dist/runtime/chrome-isolation.d.ts +16 -0
- package/dist/runtime/chrome-isolation.d.ts.map +1 -0
- package/dist/runtime/chrome-isolation.js +97 -0
- package/dist/runtime/chrome-isolation.js.map +1 -0
- package/dist/runtime/cli.js +116 -26
- package/dist/runtime/cli.js.map +1 -1
- package/dist/runtime/event-views.d.ts +21 -0
- package/dist/runtime/event-views.d.ts.map +1 -0
- package/dist/runtime/event-views.js +53 -0
- package/dist/runtime/event-views.js.map +1 -0
- package/dist/runtime/harness.d.ts +16 -0
- package/dist/runtime/harness.d.ts.map +1 -0
- package/dist/runtime/harness.js +68 -0
- package/dist/runtime/harness.js.map +1 -0
- package/dist/runtime/local-proxy.d.ts +12 -0
- package/dist/runtime/local-proxy.d.ts.map +1 -0
- package/dist/runtime/local-proxy.js +114 -0
- package/dist/runtime/local-proxy.js.map +1 -0
- package/dist/runtime/runtime.d.ts +14 -0
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +82 -21
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/sandbox-agent.d.ts +6 -0
- package/dist/runtime/sandbox-agent.d.ts.map +1 -0
- package/dist/runtime/sandbox-agent.js +35 -0
- package/dist/runtime/sandbox-agent.js.map +1 -0
- package/dist/runtime/sandbox-command.d.ts +9 -0
- package/dist/runtime/sandbox-command.d.ts.map +1 -0
- package/dist/runtime/sandbox-command.js +101 -0
- package/dist/runtime/sandbox-command.js.map +1 -0
- package/dist/runtime/sandbox-proxy.d.ts +11 -0
- package/dist/runtime/sandbox-proxy.d.ts.map +1 -0
- package/dist/runtime/sandbox-proxy.js +123 -0
- package/dist/runtime/sandbox-proxy.js.map +1 -0
- package/dist/runtime/sandbox-sessions.d.ts +31 -0
- package/dist/runtime/sandbox-sessions.d.ts.map +1 -0
- package/dist/runtime/sandbox-sessions.js +102 -0
- package/dist/runtime/sandbox-sessions.js.map +1 -0
- package/dist/runtime/sandbox.d.ts +21 -0
- package/dist/runtime/sandbox.d.ts.map +1 -0
- package/dist/runtime/sandbox.js +481 -0
- package/dist/runtime/sandbox.js.map +1 -0
- package/dist/utils/agent-browser.d.ts +2 -120
- package/dist/utils/agent-browser.d.ts.map +1 -1
- package/dist/utils/agent-browser.js +13 -376
- package/dist/utils/agent-browser.js.map +1 -1
- package/dist/utils/project-name.d.ts +11 -1
- package/dist/utils/project-name.d.ts.map +1 -1
- package/dist/utils/project-name.js +14 -1
- package/dist/utils/project-name.js.map +1 -1
- package/package.json +13 -22
- package/skills/d3k/SKILL.md +23 -1
- package/dist/commands/crawl.d.ts +0 -12
- package/dist/commands/crawl.d.ts.map +0 -1
- package/dist/commands/crawl.js +0 -101
- package/dist/commands/crawl.js.map +0 -1
- package/dist/commands/errors.d.ts +0 -14
- package/dist/commands/errors.d.ts.map +0 -1
- package/dist/commands/errors.js +0 -246
- package/dist/commands/errors.js.map +0 -1
- package/dist/commands/fix.d.ts +0 -13
- package/dist/commands/fix.d.ts.map +0 -1
- package/dist/commands/fix.js +0 -219
- package/dist/commands/fix.js.map +0 -1
- package/dist/commands/logs.d.ts +0 -13
- package/dist/commands/logs.d.ts.map +0 -1
- package/dist/commands/logs.js +0 -146
- package/dist/commands/logs.js.map +0 -1
- package/dist/commands/resume.d.ts +0 -11
- package/dist/commands/resume.d.ts.map +0 -1
- package/dist/commands/resume.js +0 -75
- package/dist/commands/resume.js.map +0 -1
- package/dist/commands/status.d.ts +0 -21
- package/dist/commands/status.d.ts.map +0 -1
- package/dist/commands/status.js +0 -44
- package/dist/commands/status.js.map +0 -1
- package/dist/components/AgentSelector.d.ts +0 -12
- package/dist/components/AgentSelector.d.ts.map +0 -1
- package/dist/components/AgentSelector.js +0 -99
- package/dist/components/AgentSelector.js.map +0 -1
- package/dist/components/PackageSelector.d.ts +0 -13
- package/dist/components/PackageSelector.d.ts.map +0 -1
- package/dist/components/PackageSelector.js +0 -79
- package/dist/components/PackageSelector.js.map +0 -1
- package/dist/constants/log-colors.d.ts +0 -43
- package/dist/constants/log-colors.d.ts.map +0 -1
- package/dist/constants/log-colors.js +0 -59
- package/dist/constants/log-colors.js.map +0 -1
- package/dist/dev-environment.d.ts +0 -187
- package/dist/dev-environment.d.ts.map +0 -1
- package/dist/dev-environment.js +0 -2248
- package/dist/dev-environment.js.map +0 -1
- package/dist/screencast-manager.d.ts +0 -78
- package/dist/screencast-manager.d.ts.map +0 -1
- package/dist/screencast-manager.js +0 -529
- package/dist/screencast-manager.js.map +0 -1
- package/dist/services/parsers/error-detectors/base.d.ts +0 -24
- package/dist/services/parsers/error-detectors/base.d.ts.map +0 -1
- package/dist/services/parsers/error-detectors/base.js +0 -44
- package/dist/services/parsers/error-detectors/base.js.map +0 -1
- package/dist/services/parsers/error-detectors/index.d.ts +0 -6
- package/dist/services/parsers/error-detectors/index.d.ts.map +0 -1
- package/dist/services/parsers/error-detectors/index.js +0 -6
- package/dist/services/parsers/error-detectors/index.js.map +0 -1
- package/dist/services/parsers/error-detectors/nextjs.d.ts +0 -9
- package/dist/services/parsers/error-detectors/nextjs.d.ts.map +0 -1
- package/dist/services/parsers/error-detectors/nextjs.js +0 -52
- package/dist/services/parsers/error-detectors/nextjs.js.map +0 -1
- package/dist/services/parsers/index.d.ts +0 -9
- package/dist/services/parsers/index.d.ts.map +0 -1
- package/dist/services/parsers/index.js +0 -10
- package/dist/services/parsers/index.js.map +0 -1
- package/dist/services/parsers/log-parsers/base.d.ts +0 -26
- package/dist/services/parsers/log-parsers/base.d.ts.map +0 -1
- package/dist/services/parsers/log-parsers/base.js +0 -6
- package/dist/services/parsers/log-parsers/base.js.map +0 -1
- package/dist/services/parsers/log-parsers/index.d.ts +0 -6
- package/dist/services/parsers/log-parsers/index.d.ts.map +0 -1
- package/dist/services/parsers/log-parsers/index.js +0 -5
- package/dist/services/parsers/log-parsers/index.js.map +0 -1
- package/dist/services/parsers/log-parsers/standard.d.ts +0 -9
- package/dist/services/parsers/log-parsers/standard.d.ts.map +0 -1
- package/dist/services/parsers/log-parsers/standard.js +0 -17
- package/dist/services/parsers/log-parsers/standard.js.map +0 -1
- package/dist/services/parsers/output-processor.d.ts +0 -39
- package/dist/services/parsers/output-processor.d.ts.map +0 -1
- package/dist/services/parsers/output-processor.js +0 -54
- package/dist/services/parsers/output-processor.js.map +0 -1
- package/dist/skills/d3k/internal-skill.md +0 -32
- package/dist/skills/index.d.ts +0 -54
- package/dist/skills/index.d.ts.map +0 -1
- package/dist/skills/index.js +0 -214
- package/dist/skills/index.js.map +0 -1
- package/dist/src/tui-interface-impl.tsx +0 -644
- package/dist/tui-interface-impl.d.ts +0 -30
- package/dist/tui-interface-impl.d.ts.map +0 -1
- package/dist/tui-interface-impl.js +0 -403
- package/dist/tui-interface-impl.js.map +0 -1
- package/dist/tui-interface-opentui.d.ts +0 -33
- package/dist/tui-interface-opentui.d.ts.map +0 -1
- package/dist/tui-interface-opentui.js +0 -1128
- package/dist/tui-interface-opentui.js.map +0 -1
- package/dist/tui-interface.d.ts +0 -38
- package/dist/tui-interface.d.ts.map +0 -1
- package/dist/tui-interface.js +0 -84
- package/dist/tui-interface.js.map +0 -1
- package/dist/utils/agent-detection.d.ts +0 -17
- package/dist/utils/agent-detection.d.ts.map +0 -1
- package/dist/utils/agent-detection.js +0 -81
- package/dist/utils/agent-detection.js.map +0 -1
- package/dist/utils/agent-selection.d.ts +0 -25
- package/dist/utils/agent-selection.d.ts.map +0 -1
- package/dist/utils/agent-selection.js +0 -73
- package/dist/utils/agent-selection.js.map +0 -1
- package/dist/utils/browser-command-argv.d.ts +0 -19
- package/dist/utils/browser-command-argv.d.ts.map +0 -1
- package/dist/utils/browser-command-argv.js +0 -149
- package/dist/utils/browser-command-argv.js.map +0 -1
- package/dist/utils/build-version.d.ts +0 -2
- package/dist/utils/build-version.d.ts.map +0 -1
- package/dist/utils/build-version.js +0 -8
- package/dist/utils/build-version.js.map +0 -1
- package/dist/utils/cli-options.d.ts +0 -5
- package/dist/utils/cli-options.d.ts.map +0 -1
- package/dist/utils/cli-options.js +0 -36
- package/dist/utils/cli-options.js.map +0 -1
- package/dist/utils/d3k-dir.d.ts +0 -3
- package/dist/utils/d3k-dir.d.ts.map +0 -1
- package/dist/utils/d3k-dir.js +0 -12
- package/dist/utils/d3k-dir.js.map +0 -1
- package/dist/utils/d3k-skill-installer.d.ts +0 -4
- package/dist/utils/d3k-skill-installer.d.ts.map +0 -1
- package/dist/utils/d3k-skill-installer.js +0 -30
- package/dist/utils/d3k-skill-installer.js.map +0 -1
- package/dist/utils/log-filename.d.ts +0 -30
- package/dist/utils/log-filename.d.ts.map +0 -1
- package/dist/utils/log-filename.js +0 -60
- package/dist/utils/log-filename.js.map +0 -1
- package/dist/utils/project-detector.d.ts +0 -25
- package/dist/utils/project-detector.d.ts.map +0 -1
- package/dist/utils/project-detector.js +0 -153
- package/dist/utils/project-detector.js.map +0 -1
- package/dist/utils/project-metadata.d.ts +0 -4
- package/dist/utils/project-metadata.d.ts.map +0 -1
- package/dist/utils/project-metadata.js +0 -48
- package/dist/utils/project-metadata.js.map +0 -1
- package/dist/utils/repo-validate.d.ts +0 -2
- package/dist/utils/repo-validate.d.ts.map +0 -1
- package/dist/utils/repo-validate.js +0 -10
- package/dist/utils/repo-validate.js.map +0 -1
- package/dist/utils/session.d.ts +0 -19
- package/dist/utils/session.d.ts.map +0 -1
- package/dist/utils/session.js +0 -65
- package/dist/utils/session.js.map +0 -1
- package/dist/utils/skill-installer.d.ts +0 -65
- package/dist/utils/skill-installer.d.ts.map +0 -1
- package/dist/utils/skill-installer.js +0 -265
- package/dist/utils/skill-installer.js.map +0 -1
- package/dist/utils/timestamp.d.ts +0 -20
- package/dist/utils/timestamp.d.ts.map +0 -1
- package/dist/utils/timestamp.js +0 -57
- package/dist/utils/timestamp.js.map +0 -1
- package/dist/utils/tmux-helpers.d.ts +0 -36
- package/dist/utils/tmux-helpers.d.ts.map +0 -1
- package/dist/utils/tmux-helpers.js +0 -118
- package/dist/utils/tmux-helpers.js.map +0 -1
- package/dist/utils/user-config.d.ts +0 -12
- package/dist/utils/user-config.d.ts.map +0 -1
- package/dist/utils/user-config.js +0 -46
- package/dist/utils/user-config.js.map +0 -1
- package/dist/utils/version-check.d.ts +0 -52
- package/dist/utils/version-check.d.ts.map +0 -1
- package/dist/utils/version-check.js +0 -304
- package/dist/utils/version-check.js.map +0 -1
- package/src/tui-interface-impl.tsx +0 -644
package/dist/dev-environment.js
DELETED
|
@@ -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
|