dev3000 0.0.173 ā 0.0.175
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 +0 -4
- package/dist/cdp-monitor.d.ts +21 -2
- package/dist/cdp-monitor.d.ts.map +1 -1
- package/dist/cdp-monitor.js +449 -107
- package/dist/cdp-monitor.js.map +1 -1
- package/dist/cli.js +193 -216
- package/dist/cli.js.map +1 -1
- package/dist/commands/crawl.d.ts.map +1 -1
- package/dist/commands/crawl.js +4 -43
- package/dist/commands/crawl.js.map +1 -1
- package/dist/commands/errors.d.ts.map +1 -1
- package/dist/commands/errors.js +4 -53
- package/dist/commands/errors.js.map +1 -1
- package/dist/commands/fix.d.ts.map +1 -1
- package/dist/commands/fix.js +5 -74
- package/dist/commands/fix.js.map +1 -1
- package/dist/commands/logs.d.ts.map +1 -1
- package/dist/commands/logs.js +4 -53
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/resume.d.ts +11 -0
- package/dist/commands/resume.d.ts.map +1 -0
- package/dist/commands/resume.js +75 -0
- package/dist/commands/resume.js.map +1 -0
- package/dist/commands/skill-runner.d.ts +14 -0
- package/dist/commands/skill-runner.d.ts.map +1 -0
- package/dist/commands/skill-runner.js +494 -0
- package/dist/commands/skill-runner.js.map +1 -0
- package/dist/dev-environment.d.ts +26 -3
- package/dist/dev-environment.d.ts.map +1 -1
- package/dist/dev-environment.js +285 -118
- package/dist/dev-environment.js.map +1 -1
- package/dist/skills/d3k/internal-skill.md +145 -0
- package/dist/skills/index.test.ts +28 -1
- package/dist/skills/index.ts +58 -7
- package/dist/tui-interface-opentui.d.ts +2 -0
- package/dist/tui-interface-opentui.d.ts.map +1 -1
- package/dist/tui-interface-opentui.js +17 -3
- package/dist/tui-interface-opentui.js.map +1 -1
- package/dist/utils/agent-browser.d.ts.map +1 -1
- package/dist/utils/agent-browser.js +6 -3
- package/dist/utils/agent-browser.js.map +1 -1
- package/dist/utils/agent-detection.d.ts +1 -0
- package/dist/utils/agent-detection.d.ts.map +1 -1
- package/dist/utils/agent-detection.js +11 -0
- package/dist/utils/agent-detection.js.map +1 -1
- package/dist/utils/agent-selection.js +4 -4
- package/dist/utils/agent-selection.js.map +1 -1
- package/dist/utils/browser-command-argv.d.ts +1 -1
- package/dist/utils/browser-command-argv.d.ts.map +1 -1
- package/dist/utils/browser-command-argv.js +1 -1
- package/dist/utils/browser-command-argv.js.map +1 -1
- package/dist/utils/project-metadata.d.ts +4 -0
- package/dist/utils/project-metadata.d.ts.map +1 -0
- package/dist/utils/project-metadata.js +48 -0
- package/dist/utils/project-metadata.js.map +1 -0
- package/dist/utils/project-name.d.ts +2 -0
- package/dist/utils/project-name.d.ts.map +1 -1
- package/dist/utils/project-name.js +6 -0
- package/dist/utils/project-name.js.map +1 -1
- package/dist/utils/session.d.ts +14 -0
- package/dist/utils/session.d.ts.map +1 -0
- package/dist/utils/session.js +65 -0
- package/dist/utils/session.js.map +1 -0
- package/dist/utils/version-check.js +2 -2
- package/dist/utils/version-check.js.map +1 -1
- package/package.json +9 -21
- package/dist/commands/cloud-check-pr.d.ts +0 -9
- package/dist/commands/cloud-check-pr.d.ts.map +0 -1
- package/dist/commands/cloud-check-pr.js +0 -243
- package/dist/commands/cloud-check-pr.js.map +0 -1
- package/dist/commands/cloud-fix.d.ts +0 -13
- package/dist/commands/cloud-fix.d.ts.map +0 -1
- package/dist/commands/cloud-fix.js +0 -79
- package/dist/commands/cloud-fix.js.map +0 -1
- package/dist/commands/find-component.d.ts +0 -8
- package/dist/commands/find-component.d.ts.map +0 -1
- package/dist/commands/find-component.js +0 -182
- package/dist/commands/find-component.js.map +0 -1
- package/dist/skills/d3k/SKILL.md +0 -126
- package/dist/skills/index.d.ts +0 -46
- package/dist/skills/index.d.ts.map +0 -1
- package/dist/skills/index.js +0 -174
- package/dist/skills/index.js.map +0 -1
package/dist/dev-environment.js
CHANGED
|
@@ -2,7 +2,7 @@ import chalk from "chalk";
|
|
|
2
2
|
import { execSync, spawn, spawnSync } from "child_process";
|
|
3
3
|
import { appendFileSync, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "fs";
|
|
4
4
|
import https from "https";
|
|
5
|
-
import { createServer } from "net";
|
|
5
|
+
import { createConnection, createServer } from "net";
|
|
6
6
|
import ora from "ora";
|
|
7
7
|
import { homedir, tmpdir } from "os";
|
|
8
8
|
import { dirname, join, resolve, sep } from "path";
|
|
@@ -12,8 +12,9 @@ import { CDPMonitor } from "./cdp-monitor.js";
|
|
|
12
12
|
import { ensurePortlessAlias, getPortlessUrl, isPortlessInstalled, removePortlessAlias } from "./portless.js";
|
|
13
13
|
import { ScreencastManager } from "./screencast-manager.js";
|
|
14
14
|
import { NextJsErrorDetector, OutputProcessor, StandardLogParser } from "./services/parsers/index.js";
|
|
15
|
-
import {
|
|
15
|
+
import { getBundledD3kSkillPath, listAvailableSkills } from "./skills/index.js";
|
|
16
16
|
import { DevTUI } from "./tui-interface.js";
|
|
17
|
+
import { readProjectAgentName, rememberProjectAgentName } from "./utils/project-metadata.js";
|
|
17
18
|
import { getProjectDir, getProjectDisplayName, getProjectName } from "./utils/project-name.js";
|
|
18
19
|
import { getApplicablePackages, getSkillsPathForLocation, installSkillPackage, isPackageInstalled } from "./utils/skill-installer.js";
|
|
19
20
|
import { formatTimestamp } from "./utils/timestamp.js";
|
|
@@ -102,8 +103,8 @@ class Logger {
|
|
|
102
103
|
if (!existsSync(logDir)) {
|
|
103
104
|
mkdirSync(logDir, { recursive: true });
|
|
104
105
|
}
|
|
105
|
-
//
|
|
106
|
-
|
|
106
|
+
// Touch the log file without truncating an explicit existing --log-file.
|
|
107
|
+
appendFileSync(this.logFile, "");
|
|
107
108
|
}
|
|
108
109
|
log(source, message) {
|
|
109
110
|
const timestamp = formatTimestamp(new Date(), this.dateTimeFormat);
|
|
@@ -125,6 +126,68 @@ function isInSandbox() {
|
|
|
125
126
|
existsSync("/.dockerenv") ||
|
|
126
127
|
existsSync("/run/.containerenv"));
|
|
127
128
|
}
|
|
129
|
+
function parsePortNumber(port) {
|
|
130
|
+
if (!/^\d+$/.test(port)) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
const parsed = Number.parseInt(port, 10);
|
|
134
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
return parsed;
|
|
138
|
+
}
|
|
139
|
+
function escapeRegexForPkill(value) {
|
|
140
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
141
|
+
}
|
|
142
|
+
function killPortProcessesSync(port, debugLog) {
|
|
143
|
+
if (isInSandbox()) {
|
|
144
|
+
debugLog(`Skipping synchronous port kill in sandbox environment`);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const parsedPort = parsePortNumber(port);
|
|
148
|
+
if (parsedPort === null) {
|
|
149
|
+
debugLog(`Skipping synchronous port kill for invalid port: ${port}`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const result = spawnSync("lsof", ["-ti", `:${parsedPort}`], {
|
|
154
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
155
|
+
encoding: "utf8",
|
|
156
|
+
timeout: 5000
|
|
157
|
+
});
|
|
158
|
+
const pidStrings = result.stdout
|
|
159
|
+
.split(/\s+/)
|
|
160
|
+
.map((value) => value.trim())
|
|
161
|
+
.filter((value) => /^\d+$/.test(value));
|
|
162
|
+
for (const pidString of pidStrings) {
|
|
163
|
+
try {
|
|
164
|
+
process.kill(Number.parseInt(pidString, 10), "SIGKILL");
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
debugLog(`Failed to synchronously kill PID ${pidString} on port ${parsedPort}: ${error}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
debugLog(`Synchronous kill for port ${parsedPort} found ${pidStrings.length} process(es)`);
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
debugLog(`Synchronous kill error for port ${parsedPort}: ${error}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function pkillByPattern(pattern, debugLog) {
|
|
177
|
+
if (isInSandbox()) {
|
|
178
|
+
debugLog(`Skipping pkill in sandbox environment`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
spawnSync("pkill", ["-f", pattern], {
|
|
183
|
+
stdio: "ignore",
|
|
184
|
+
timeout: 5000
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
debugLog(`pkill failed for pattern ${pattern}: ${error}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
128
191
|
/**
|
|
129
192
|
* Count active d3k instances by checking PID files in tmpdir.
|
|
130
193
|
* Returns the count of running d3k processes (excluding the current one if specified).
|
|
@@ -163,6 +226,115 @@ export function countActiveD3kInstances(excludeCurrentPid = false) {
|
|
|
163
226
|
return excludeCurrentPid ? 0 : 1;
|
|
164
227
|
}
|
|
165
228
|
}
|
|
229
|
+
export function parseD3kLockContent(content) {
|
|
230
|
+
const trimmed = content.trim();
|
|
231
|
+
if (!trimmed)
|
|
232
|
+
return null;
|
|
233
|
+
if (/^\d+$/.test(trimmed)) {
|
|
234
|
+
const pid = Number.parseInt(trimmed, 10);
|
|
235
|
+
return Number.isInteger(pid) && pid > 0 ? { pid } : null;
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
const parsed = JSON.parse(trimmed);
|
|
239
|
+
const pid = parsed?.pid;
|
|
240
|
+
if (!parsed || typeof parsed !== "object" || typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
pid,
|
|
245
|
+
cwd: typeof parsed.cwd === "string" ? parsed.cwd : null,
|
|
246
|
+
createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : null,
|
|
247
|
+
processStartTime: typeof parsed.processStartTime === "string" ? parsed.processStartTime : null
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function getProcessPsField(pid, field) {
|
|
255
|
+
try {
|
|
256
|
+
const result = spawnSync("ps", ["-p", String(pid), "-o", `${field}=`], {
|
|
257
|
+
encoding: "utf8",
|
|
258
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
259
|
+
});
|
|
260
|
+
if (result.status !== 0)
|
|
261
|
+
return null;
|
|
262
|
+
const output = result.stdout.trim();
|
|
263
|
+
return output || null;
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
export function getProcessStartTime(pid) {
|
|
270
|
+
return getProcessPsField(pid, "lstart");
|
|
271
|
+
}
|
|
272
|
+
export function getProcessCommand(pid) {
|
|
273
|
+
return getProcessPsField(pid, "command");
|
|
274
|
+
}
|
|
275
|
+
function isLikelyD3kCommand(command) {
|
|
276
|
+
const normalized = command.toLowerCase();
|
|
277
|
+
return normalized.includes("d3k") || normalized.includes("dev3000");
|
|
278
|
+
}
|
|
279
|
+
export function validateD3kLockContent(content, deps = {}) {
|
|
280
|
+
const lockInfo = parseD3kLockContent(content);
|
|
281
|
+
if (!lockInfo) {
|
|
282
|
+
return {
|
|
283
|
+
active: false,
|
|
284
|
+
reason: "lock file contents are invalid",
|
|
285
|
+
lockInfo: null
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
const processExists = deps.processExists ||
|
|
289
|
+
((pid) => {
|
|
290
|
+
try {
|
|
291
|
+
process.kill(pid, 0);
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
if (!processExists(lockInfo.pid)) {
|
|
299
|
+
return {
|
|
300
|
+
active: false,
|
|
301
|
+
reason: `process ${lockInfo.pid} is not running`,
|
|
302
|
+
lockInfo
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
const getStartTime = deps.getProcessStartTime || getProcessStartTime;
|
|
306
|
+
if (lockInfo.processStartTime) {
|
|
307
|
+
const liveStartTime = getStartTime(lockInfo.pid);
|
|
308
|
+
if (liveStartTime && liveStartTime !== lockInfo.processStartTime) {
|
|
309
|
+
return {
|
|
310
|
+
active: false,
|
|
311
|
+
reason: `pid ${lockInfo.pid} was reused by a different process`,
|
|
312
|
+
lockInfo
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
if (liveStartTime === lockInfo.processStartTime) {
|
|
316
|
+
return {
|
|
317
|
+
active: true,
|
|
318
|
+
reason: `process ${lockInfo.pid} matches recorded start time`,
|
|
319
|
+
lockInfo
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const getCommand = deps.getProcessCommand || getProcessCommand;
|
|
324
|
+
const liveCommand = getCommand(lockInfo.pid);
|
|
325
|
+
if (liveCommand && !isLikelyD3kCommand(liveCommand)) {
|
|
326
|
+
return {
|
|
327
|
+
active: false,
|
|
328
|
+
reason: `pid ${lockInfo.pid} belongs to a non-d3k process`,
|
|
329
|
+
lockInfo
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
active: true,
|
|
334
|
+
reason: `process ${lockInfo.pid} still owns the lock`,
|
|
335
|
+
lockInfo
|
|
336
|
+
};
|
|
337
|
+
}
|
|
166
338
|
/**
|
|
167
339
|
* Check if a port is available for binding (no process is listening on it).
|
|
168
340
|
* Used for finding available ports before starting servers.
|
|
@@ -172,6 +344,40 @@ async function isPortAvailable(port) {
|
|
|
172
344
|
if (!Number.isInteger(portNumber) || portNumber < 0 || portNumber > 65535) {
|
|
173
345
|
return false;
|
|
174
346
|
}
|
|
347
|
+
const hasActiveListener = await Promise.any(["127.0.0.1", "::1", "localhost"].map((host) => new Promise((resolve, reject) => {
|
|
348
|
+
const socket = createConnection({ host, port: portNumber });
|
|
349
|
+
let settled = false;
|
|
350
|
+
const finish = (result, shouldReject = false) => {
|
|
351
|
+
if (settled)
|
|
352
|
+
return;
|
|
353
|
+
settled = true;
|
|
354
|
+
socket.destroy();
|
|
355
|
+
if (shouldReject) {
|
|
356
|
+
reject(new Error("unreachable"));
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
resolve(result);
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
socket.once("connect", () => finish(true));
|
|
363
|
+
socket.once("timeout", () => finish(false, true));
|
|
364
|
+
socket.once("error", (error) => {
|
|
365
|
+
if (error.code === "ECONNREFUSED" ||
|
|
366
|
+
error.code === "EHOSTUNREACH" ||
|
|
367
|
+
error.code === "ENETUNREACH" ||
|
|
368
|
+
error.code === "EINVAL") {
|
|
369
|
+
finish(false, true);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
finish(false);
|
|
373
|
+
});
|
|
374
|
+
socket.setTimeout(150);
|
|
375
|
+
})))
|
|
376
|
+
.then((result) => result)
|
|
377
|
+
.catch(() => false);
|
|
378
|
+
if (hasActiveListener) {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
175
381
|
return new Promise((resolve) => {
|
|
176
382
|
const server = createServer();
|
|
177
383
|
server.once("error", (error) => {
|
|
@@ -271,11 +477,8 @@ export async function findAvailablePort(startPort) {
|
|
|
271
477
|
*/
|
|
272
478
|
async function ensureD3kSkill(skillsAgentId) {
|
|
273
479
|
try {
|
|
274
|
-
const
|
|
275
|
-
if (!
|
|
276
|
-
return;
|
|
277
|
-
const bundledSkillPath = join(bundledSkillsDir, "d3k", "SKILL.md");
|
|
278
|
-
if (!existsSync(bundledSkillPath))
|
|
480
|
+
const bundledSkillPath = getBundledD3kSkillPath();
|
|
481
|
+
if (!bundledSkillPath)
|
|
279
482
|
return;
|
|
280
483
|
const targetSkillsDir = skillsAgentId ? getSkillsPathForLocation(skillsAgentId, "project")?.path : null;
|
|
281
484
|
const skillRoots = new Set();
|
|
@@ -327,7 +530,21 @@ async function autoInstallSkills(agentId, debugLog) {
|
|
|
327
530
|
}
|
|
328
531
|
// REMOVED: cleanup functions are no longer needed
|
|
329
532
|
// CLI integration config files are now kept persistent across dev3000 restarts
|
|
330
|
-
export function createPersistentLogFile() {
|
|
533
|
+
export function createPersistentLogFile(override) {
|
|
534
|
+
if (override && override.length > 0) {
|
|
535
|
+
const resolved = resolve(override);
|
|
536
|
+
const logDir = dirname(resolved);
|
|
537
|
+
try {
|
|
538
|
+
if (!existsSync(logDir)) {
|
|
539
|
+
mkdirSync(logDir, { recursive: true });
|
|
540
|
+
}
|
|
541
|
+
appendFileSync(resolved, "");
|
|
542
|
+
return resolved;
|
|
543
|
+
}
|
|
544
|
+
catch (error) {
|
|
545
|
+
throw new Error(`Failed to initialize --log-file at ${resolved}: ${error instanceof Error ? error.message : String(error)}`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
331
548
|
// Get unique project name
|
|
332
549
|
const projectName = getProjectName();
|
|
333
550
|
// Use ~/.d3k/{projectName}/logs directory for persistent, accessible logs
|
|
@@ -358,13 +575,15 @@ export function createPersistentLogFile() {
|
|
|
358
575
|
* processes belong to THIS d3k instance so we don't accidentally kill
|
|
359
576
|
* Chrome instances from other d3k sessions.
|
|
360
577
|
*/
|
|
361
|
-
export function writeSessionInfo(projectName, logFilePath, appPort, publicUrl, cdpUrl, chromePids, serverCommand, framework, serverPid, skillsInstalled, skillsAgentId, preferredBrowserTool) {
|
|
578
|
+
export function writeSessionInfo(projectName, logFilePath, appPort, publicUrl, cdpUrl, chromePids, serverCommand, framework, serverPid, skillsInstalled, skillsAgentId, preferredBrowserTool, agentName) {
|
|
362
579
|
const projectDir = getProjectDir();
|
|
363
580
|
try {
|
|
364
581
|
// Create project directory if it doesn't exist
|
|
365
582
|
if (!existsSync(projectDir)) {
|
|
366
583
|
mkdirSync(projectDir, { recursive: true });
|
|
367
584
|
}
|
|
585
|
+
const sessionFile = join(projectDir, "session.json");
|
|
586
|
+
const persistedAgentName = readProjectAgentName();
|
|
368
587
|
// Session file contains project info
|
|
369
588
|
const sessionInfo = {
|
|
370
589
|
projectName,
|
|
@@ -381,11 +600,14 @@ export function writeSessionInfo(projectName, logFilePath, appPort, publicUrl, c
|
|
|
381
600
|
serverPid: serverPid || null,
|
|
382
601
|
skillsInstalled: skillsInstalled || [],
|
|
383
602
|
skillsAgentId: skillsAgentId || null,
|
|
384
|
-
preferredBrowserTool: preferredBrowserTool || "agent-browser"
|
|
603
|
+
preferredBrowserTool: preferredBrowserTool || "agent-browser",
|
|
604
|
+
agentName: agentName || persistedAgentName || null
|
|
385
605
|
};
|
|
386
606
|
// Write session file in project directory
|
|
387
|
-
const sessionFile = join(projectDir, "session.json");
|
|
388
607
|
writeFileSync(sessionFile, JSON.stringify(sessionInfo, null, 2));
|
|
608
|
+
if (sessionInfo.agentName) {
|
|
609
|
+
rememberProjectAgentName(sessionInfo.agentName);
|
|
610
|
+
}
|
|
389
611
|
}
|
|
390
612
|
catch (error) {
|
|
391
613
|
// Non-fatal - just log a warning
|
|
@@ -511,7 +733,7 @@ export class DevEnvironment {
|
|
|
511
733
|
}
|
|
512
734
|
// Store screenshots in project-specific directory for local access
|
|
513
735
|
const projectName = getProjectName();
|
|
514
|
-
this.screenshotDir = join(getProjectDir(), "screenshots");
|
|
736
|
+
this.screenshotDir = options.screenshotsDir ? resolve(options.screenshotsDir) : join(getProjectDir(), "screenshots");
|
|
515
737
|
// Use project-specific PID and lock files to allow multiple projects to run simultaneously
|
|
516
738
|
this.pidFile = join(tmpdir(), `dev3000-${projectName}.pid`);
|
|
517
739
|
this.lockFile = join(tmpdir(), `dev3000-${projectName}.lock`);
|
|
@@ -680,10 +902,7 @@ export class DevEnvironment {
|
|
|
680
902
|
// Best-effort synchronous cleanup (mirrors SIGHUP handler).
|
|
681
903
|
const port = this.options.port;
|
|
682
904
|
this.debugLog(`Synchronous kill for port ${port}`);
|
|
683
|
-
|
|
684
|
-
stdio: "pipe",
|
|
685
|
-
timeout: 5000
|
|
686
|
-
});
|
|
905
|
+
killPortProcessesSync(port, this.debugLog.bind(this));
|
|
687
906
|
const projectName = getProjectName();
|
|
688
907
|
const sessionInfo = getSessionInfo(projectName);
|
|
689
908
|
const chromePids = sessionInfo?.chromePids ?? [];
|
|
@@ -755,25 +974,7 @@ export class DevEnvironment {
|
|
|
755
974
|
// This ensures cleanup happens even if the event loop gets interrupted
|
|
756
975
|
const port = this.options.port;
|
|
757
976
|
this.debugLog(`Synchronous kill for port ${port}`);
|
|
758
|
-
|
|
759
|
-
stdio: "pipe",
|
|
760
|
-
timeout: 5000
|
|
761
|
-
});
|
|
762
|
-
const projectName = getProjectName();
|
|
763
|
-
const chromePids = getSessionChromePids(projectName);
|
|
764
|
-
if (chromePids.length > 0) {
|
|
765
|
-
this.debugLog(`Synchronous kill for Chrome PIDs: [${chromePids.join(", ")}]`);
|
|
766
|
-
for (const pid of chromePids) {
|
|
767
|
-
try {
|
|
768
|
-
process.kill(pid, "SIGTERM");
|
|
769
|
-
process.kill(pid, 0);
|
|
770
|
-
process.kill(pid, "SIGKILL");
|
|
771
|
-
}
|
|
772
|
-
catch {
|
|
773
|
-
// Ignore - process may already be dead
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
}
|
|
977
|
+
killPortProcessesSync(port, this.debugLog.bind(this));
|
|
777
978
|
// Now do the rest of cleanup async
|
|
778
979
|
this.tui?.updateStatus("Shutting down...");
|
|
779
980
|
this.handleShutdown()
|
|
@@ -942,7 +1143,7 @@ export class DevEnvironment {
|
|
|
942
1143
|
console.log(chalk.cyan(`Logs: ${this.options.logFile}`));
|
|
943
1144
|
console.log(chalk.cyan("āļø Give this to an AI to auto debug and fix your app\n"));
|
|
944
1145
|
console.log(chalk.cyan(`š Your App: ${this.preferredAppUrl}`));
|
|
945
|
-
console.log(chalk.cyan(`š§ CLI Tools: d3k fix, d3k crawl
|
|
1146
|
+
console.log(chalk.cyan(`š§ CLI Tools: d3k fix, d3k crawl`));
|
|
946
1147
|
if (this.options.serversOnly) {
|
|
947
1148
|
console.log(chalk.cyan("š„ļø Servers-only mode - browser monitoring disabled"));
|
|
948
1149
|
}
|
|
@@ -1189,21 +1390,21 @@ export class DevEnvironment {
|
|
|
1189
1390
|
// Check if lock file exists
|
|
1190
1391
|
if (existsSync(this.lockFile)) {
|
|
1191
1392
|
const lockContent = readFileSync(this.lockFile, "utf8");
|
|
1192
|
-
const
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
process.kill(oldPID, 0); // Signal 0 just checks if process exists
|
|
1196
|
-
// Process is running, lock is valid
|
|
1393
|
+
const validation = validateD3kLockContent(lockContent);
|
|
1394
|
+
if (validation.active) {
|
|
1395
|
+
this.debugLog(`Lock file is active: ${validation.reason}`);
|
|
1197
1396
|
return false;
|
|
1198
1397
|
}
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
this.debugLog(`Removing stale lock file for PID ${oldPID}`);
|
|
1202
|
-
unlinkSync(this.lockFile);
|
|
1203
|
-
}
|
|
1398
|
+
this.debugLog(`Removing stale lock file: ${validation.reason}`);
|
|
1399
|
+
unlinkSync(this.lockFile);
|
|
1204
1400
|
}
|
|
1205
|
-
//
|
|
1206
|
-
writeFileSync(this.lockFile,
|
|
1401
|
+
// Store enough metadata to detect PID reuse after crashes.
|
|
1402
|
+
writeFileSync(this.lockFile, JSON.stringify({
|
|
1403
|
+
pid: process.pid,
|
|
1404
|
+
cwd: process.cwd(),
|
|
1405
|
+
createdAt: new Date().toISOString(),
|
|
1406
|
+
processStartTime: getProcessStartTime(process.pid)
|
|
1407
|
+
}));
|
|
1207
1408
|
this.debugLog(`Acquired lock file: ${this.lockFile}`);
|
|
1208
1409
|
return true;
|
|
1209
1410
|
}
|
|
@@ -1247,7 +1448,7 @@ export class DevEnvironment {
|
|
|
1247
1448
|
const cdpUrl = this.cdpMonitor?.getCdpUrl() || null;
|
|
1248
1449
|
const chromePids = this.cdpMonitor?.getChromePids() || [];
|
|
1249
1450
|
const skillsInstalled = listAvailableSkills(process.cwd());
|
|
1250
|
-
writeSessionInfo(projectName, this.options.logFile, this.options.port, this.publicAppUrl, cdpUrl, chromePids, this.options.serverCommand, this.options.framework, this.serverProcess?.pid, skillsInstalled, this.options.skillsAgentId ?? null, this.options.browserTool);
|
|
1451
|
+
writeSessionInfo(projectName, this.options.logFile, this.options.port, 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);
|
|
1251
1452
|
}
|
|
1252
1453
|
detectPortChange(text) {
|
|
1253
1454
|
// Detect Next.js port switch: "ā Port 3000 is in use by process 39543, using available port 3001 instead."
|
|
@@ -1561,7 +1762,7 @@ export class DevEnvironment {
|
|
|
1561
1762
|
this.cdpMonitor = new CDPMonitor(this.options.profileDir, this.screenshotDir, (_source, message) => {
|
|
1562
1763
|
this.logger.log("browser", message);
|
|
1563
1764
|
}, this.options.debug, this.options.browser, this.options.pluginReactScan, this.options.port, // App server port to monitor
|
|
1564
|
-
this.preferredAppUrl, this.options.debugPort, // Chrome debug port
|
|
1765
|
+
this.preferredAppUrl, this.options.browserNavigationTimeoutSeconds * 1000, this.options.debugPort, // Chrome debug port
|
|
1565
1766
|
this.options.headless, // Headless mode for serverless/CI environments
|
|
1566
1767
|
this.options.framework // Framework hint for optional React DevTools launch args
|
|
1567
1768
|
);
|
|
@@ -1594,9 +1795,7 @@ export class DevEnvironment {
|
|
|
1594
1795
|
this.writeCurrentSessionInfo(projectName);
|
|
1595
1796
|
this.debugLog(`Updated session info with CDP URL: ${cdpUrl}, Chrome PIDs: [${chromePids.join(", ")}]`);
|
|
1596
1797
|
this.logger.log("browser", `[CDP] Session info written with cdpUrl: ${cdpUrl ? "available" : "null"}`);
|
|
1597
|
-
|
|
1598
|
-
await this.cdpMonitor.navigateToUrl(this.preferredAppUrl);
|
|
1599
|
-
this.logger.log("browser", `[CDP] Navigated to ${this.preferredAppUrl}`);
|
|
1798
|
+
this.logger.log("browser", `[CDP] Loading page will hand off to ${this.preferredAppUrl}`);
|
|
1600
1799
|
}
|
|
1601
1800
|
catch (error) {
|
|
1602
1801
|
// Log error and throw to trigger graceful shutdown
|
|
@@ -1664,7 +1863,7 @@ export class DevEnvironment {
|
|
|
1664
1863
|
console.log(chalk.gray(`ā¹ļø Skipping ${name} port kill in sandbox environment`));
|
|
1665
1864
|
return;
|
|
1666
1865
|
}
|
|
1667
|
-
if (
|
|
1866
|
+
if (parsePortNumber(port) === null) {
|
|
1668
1867
|
this.debugLog(`Skipping ${name} port kill for invalid port: ${port}`);
|
|
1669
1868
|
return;
|
|
1670
1869
|
}
|
|
@@ -1704,23 +1903,27 @@ export class DevEnvironment {
|
|
|
1704
1903
|
// Kill app server only
|
|
1705
1904
|
console.log(chalk.cyan("š Killing app server..."));
|
|
1706
1905
|
await killPortProcess(this.options.port, "your app server");
|
|
1707
|
-
// Kill server process and its children using the saved PID (from before session file was deleted)
|
|
1708
|
-
if (sessionInfo?.serverPid) {
|
|
1709
|
-
this.killServerPidIfOwned(sessionInfo.serverPid, sessionInfo.cwd, "graceful shutdown");
|
|
1710
|
-
}
|
|
1711
|
-
// Always kill tracked Chrome processes, even if cdpMonitor is unavailable.
|
|
1712
|
-
this.killTrackedChromePids(sessionInfo?.chromePids ?? [], "graceful shutdown");
|
|
1713
1906
|
// Shutdown CDP monitor if it was started
|
|
1907
|
+
let chromeShutdownHandled = false;
|
|
1714
1908
|
if (this.cdpMonitor) {
|
|
1715
1909
|
try {
|
|
1716
1910
|
console.log(chalk.cyan("š Closing CDP monitor..."));
|
|
1717
1911
|
await this.cdpMonitor.shutdown();
|
|
1912
|
+
chromeShutdownHandled = true;
|
|
1718
1913
|
console.log(chalk.green("ā
CDP monitor closed"));
|
|
1719
1914
|
}
|
|
1720
1915
|
catch (_error) {
|
|
1721
1916
|
console.log(chalk.gray("ā ļø CDP monitor shutdown failed"));
|
|
1722
1917
|
}
|
|
1723
1918
|
}
|
|
1919
|
+
if (!chromeShutdownHandled) {
|
|
1920
|
+
// Safety net when CDP was unavailable: terminate only Chrome processes from this d3k session.
|
|
1921
|
+
this.killTrackedChromePids(sessionInfo?.chromePids ?? [], "graceful shutdown");
|
|
1922
|
+
}
|
|
1923
|
+
// Kill server process and its children using the saved PID (from before session file was deleted)
|
|
1924
|
+
if (sessionInfo?.serverPid) {
|
|
1925
|
+
this.killServerPidIfOwned(sessionInfo.serverPid, sessionInfo.cwd, "graceful shutdown");
|
|
1926
|
+
}
|
|
1724
1927
|
console.log(chalk.red(`ā ${this.options.commandName} exited due to server failure`));
|
|
1725
1928
|
// Show recent log entries to help diagnose the issue
|
|
1726
1929
|
this.showRecentLogs();
|
|
@@ -1761,26 +1964,9 @@ export class DevEnvironment {
|
|
|
1761
1964
|
// This ensures cleanup happens even if the event loop gets interrupted
|
|
1762
1965
|
const port = this.options.port;
|
|
1763
1966
|
this.debugLog(`Synchronous kill for port ${port}`);
|
|
1764
|
-
|
|
1765
|
-
stdio: "pipe",
|
|
1766
|
-
timeout: 5000
|
|
1767
|
-
});
|
|
1967
|
+
killPortProcessesSync(port, this.debugLog.bind(this));
|
|
1768
1968
|
const projectName = getProjectName();
|
|
1769
1969
|
const sessionInfo = getSessionInfo(projectName);
|
|
1770
|
-
const chromePids = sessionInfo?.chromePids ?? [];
|
|
1771
|
-
if (chromePids.length > 0) {
|
|
1772
|
-
this.debugLog(`Synchronous kill for Chrome PIDs: [${chromePids.join(", ")}]`);
|
|
1773
|
-
for (const pid of chromePids) {
|
|
1774
|
-
try {
|
|
1775
|
-
process.kill(pid, "SIGTERM");
|
|
1776
|
-
process.kill(pid, 0);
|
|
1777
|
-
process.kill(pid, "SIGKILL");
|
|
1778
|
-
}
|
|
1779
|
-
catch {
|
|
1780
|
-
// Ignore - process may already be dead
|
|
1781
|
-
}
|
|
1782
|
-
}
|
|
1783
|
-
}
|
|
1784
1970
|
if (sessionInfo?.serverPid) {
|
|
1785
1971
|
this.killServerPidIfOwned(sessionInfo.serverPid, sessionInfo.cwd, "SIGINT");
|
|
1786
1972
|
}
|
|
@@ -1793,7 +1979,7 @@ export class DevEnvironment {
|
|
|
1793
1979
|
const forceExitTimeout = setTimeout(() => {
|
|
1794
1980
|
this.debugLog("Shutdown timeout reached, forcing exit");
|
|
1795
1981
|
process.exit(1);
|
|
1796
|
-
},
|
|
1982
|
+
}, 10000); // Give Chrome enough time to record a clean profile exit.
|
|
1797
1983
|
// Call async cleanup in a non-blocking way
|
|
1798
1984
|
this.handleShutdown()
|
|
1799
1985
|
.then(() => {
|
|
@@ -1820,26 +2006,9 @@ export class DevEnvironment {
|
|
|
1820
2006
|
// CRITICAL: Kill port processes SYNCHRONOUSLY first, before anything else
|
|
1821
2007
|
const port = this.options.port;
|
|
1822
2008
|
this.debugLog(`Synchronous kill for port ${port}`);
|
|
1823
|
-
|
|
1824
|
-
stdio: "pipe",
|
|
1825
|
-
timeout: 5000
|
|
1826
|
-
});
|
|
2009
|
+
killPortProcessesSync(port, this.debugLog.bind(this));
|
|
1827
2010
|
const projectName = getProjectName();
|
|
1828
2011
|
const sessionInfo = getSessionInfo(projectName);
|
|
1829
|
-
const chromePids = sessionInfo?.chromePids ?? [];
|
|
1830
|
-
if (chromePids.length > 0) {
|
|
1831
|
-
this.debugLog(`Synchronous kill for Chrome PIDs: [${chromePids.join(", ")}]`);
|
|
1832
|
-
for (const pid of chromePids) {
|
|
1833
|
-
try {
|
|
1834
|
-
process.kill(pid, "SIGTERM");
|
|
1835
|
-
process.kill(pid, 0);
|
|
1836
|
-
process.kill(pid, "SIGKILL");
|
|
1837
|
-
}
|
|
1838
|
-
catch {
|
|
1839
|
-
// Ignore - process may already be dead
|
|
1840
|
-
}
|
|
1841
|
-
}
|
|
1842
|
-
}
|
|
1843
2012
|
if (sessionInfo?.serverPid) {
|
|
1844
2013
|
this.killServerPidIfOwned(sessionInfo.serverPid, sessionInfo.cwd, "SIGTERM");
|
|
1845
2014
|
}
|
|
@@ -1932,12 +2101,14 @@ export class DevEnvironment {
|
|
|
1932
2101
|
console.log(chalk.yellow("\nš Received interrupt signal. Cleaning up processes..."));
|
|
1933
2102
|
}
|
|
1934
2103
|
// Shutdown CDP monitor FIRST - this should close Chrome
|
|
2104
|
+
let chromeShutdownHandled = false;
|
|
1935
2105
|
if (this.cdpMonitor) {
|
|
1936
2106
|
try {
|
|
1937
2107
|
if (!this.options.tui) {
|
|
1938
2108
|
console.log(chalk.cyan("š Closing Chrome browser..."));
|
|
1939
2109
|
}
|
|
1940
2110
|
await this.cdpMonitor.shutdown();
|
|
2111
|
+
chromeShutdownHandled = true;
|
|
1941
2112
|
if (!this.options.tui) {
|
|
1942
2113
|
console.log(chalk.green("ā
Chrome browser closed"));
|
|
1943
2114
|
}
|
|
@@ -1973,8 +2144,10 @@ export class DevEnvironment {
|
|
|
1973
2144
|
}
|
|
1974
2145
|
}
|
|
1975
2146
|
}
|
|
1976
|
-
|
|
1977
|
-
|
|
2147
|
+
if (!chromeShutdownHandled) {
|
|
2148
|
+
// Safety net: ensure tracked Chrome processes are always terminated on shutdown.
|
|
2149
|
+
this.killTrackedChromePids(sessionInfo?.chromePids ?? [], "handleShutdown");
|
|
2150
|
+
}
|
|
1978
2151
|
// REMOVED: No longer clean up CLI config files on shutdown
|
|
1979
2152
|
// This was causing Claude Code instances to crash when dev3000 was killed
|
|
1980
2153
|
// Config file cleanup removed; keep user config files untouched on shutdown
|
|
@@ -1985,7 +2158,7 @@ export class DevEnvironment {
|
|
|
1985
2158
|
this.debugLog(`Skipping ${name} port kill in sandbox environment`);
|
|
1986
2159
|
return;
|
|
1987
2160
|
}
|
|
1988
|
-
if (
|
|
2161
|
+
if (parsePortNumber(port) === null) {
|
|
1989
2162
|
this.debugLog(`Skipping ${name} port kill for invalid port: ${port}`);
|
|
1990
2163
|
return;
|
|
1991
2164
|
}
|
|
@@ -2046,12 +2219,7 @@ export class DevEnvironment {
|
|
|
2046
2219
|
// Primary: Synchronous kill - most reliable, ensures completion
|
|
2047
2220
|
// NOTE: We always try this, even if lsof might not exist - errors are caught
|
|
2048
2221
|
try {
|
|
2049
|
-
|
|
2050
|
-
const result = spawnSync("sh", ["-c", `lsof -ti:${this.options.port} | xargs kill -9 2>/dev/null`], {
|
|
2051
|
-
stdio: "pipe",
|
|
2052
|
-
timeout: 5000
|
|
2053
|
-
});
|
|
2054
|
-
this.debugLog(`Synchronous kill for port ${this.options.port} exit code: ${result.status}`);
|
|
2222
|
+
killPortProcessesSync(this.options.port, this.debugLog.bind(this));
|
|
2055
2223
|
}
|
|
2056
2224
|
catch (error) {
|
|
2057
2225
|
this.debugLog(`Synchronous kill error: ${error}`);
|
|
@@ -2080,25 +2248,24 @@ export class DevEnvironment {
|
|
|
2080
2248
|
await killPortProcess(this.options.port, "your app server");
|
|
2081
2249
|
// Double-check: try to kill any remaining processes on the app port
|
|
2082
2250
|
try {
|
|
2083
|
-
const
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2251
|
+
const parsedPort = parsePortNumber(this.options.port);
|
|
2252
|
+
if (parsedPort !== null) {
|
|
2253
|
+
pkillByPattern(`:${parsedPort}`, this.debugLog.bind(this));
|
|
2254
|
+
this.debugLog(`Sent pkill signal for port ${parsedPort}`);
|
|
2255
|
+
}
|
|
2087
2256
|
// Specifically kill any remaining next dev processes in the current directory
|
|
2088
2257
|
// This catches cases where the shell wrapper exited but next-server survived
|
|
2089
2258
|
const cwd = process.cwd();
|
|
2090
|
-
|
|
2091
|
-
|
|
2259
|
+
const escapedCwd = escapeRegexForPkill(cwd);
|
|
2260
|
+
pkillByPattern(`next dev.*${escapedCwd}`, this.debugLog.bind(this));
|
|
2261
|
+
pkillByPattern(`next-server.*${escapedCwd}`, this.debugLog.bind(this));
|
|
2092
2262
|
this.debugLog(`Sent pkill signal for next processes in ${cwd}`);
|
|
2093
2263
|
// Kill server process and its children using the saved PID (from before session file was deleted)
|
|
2094
2264
|
if (sessionInfo?.serverPid) {
|
|
2095
2265
|
this.killServerPidIfOwned(sessionInfo.serverPid, sessionInfo.cwd, "handleShutdown");
|
|
2096
2266
|
}
|
|
2097
2267
|
// Final synchronous lsof kill - most reliable method
|
|
2098
|
-
|
|
2099
|
-
stdio: "pipe"
|
|
2100
|
-
});
|
|
2101
|
-
this.debugLog(`Final lsof kill exit code: ${result.status}`);
|
|
2268
|
+
killPortProcessesSync(this.options.port, this.debugLog.bind(this));
|
|
2102
2269
|
}
|
|
2103
2270
|
catch {
|
|
2104
2271
|
// Ignore pkill errors
|