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.
Files changed (83) hide show
  1. package/README.md +0 -4
  2. package/dist/cdp-monitor.d.ts +21 -2
  3. package/dist/cdp-monitor.d.ts.map +1 -1
  4. package/dist/cdp-monitor.js +449 -107
  5. package/dist/cdp-monitor.js.map +1 -1
  6. package/dist/cli.js +193 -216
  7. package/dist/cli.js.map +1 -1
  8. package/dist/commands/crawl.d.ts.map +1 -1
  9. package/dist/commands/crawl.js +4 -43
  10. package/dist/commands/crawl.js.map +1 -1
  11. package/dist/commands/errors.d.ts.map +1 -1
  12. package/dist/commands/errors.js +4 -53
  13. package/dist/commands/errors.js.map +1 -1
  14. package/dist/commands/fix.d.ts.map +1 -1
  15. package/dist/commands/fix.js +5 -74
  16. package/dist/commands/fix.js.map +1 -1
  17. package/dist/commands/logs.d.ts.map +1 -1
  18. package/dist/commands/logs.js +4 -53
  19. package/dist/commands/logs.js.map +1 -1
  20. package/dist/commands/resume.d.ts +11 -0
  21. package/dist/commands/resume.d.ts.map +1 -0
  22. package/dist/commands/resume.js +75 -0
  23. package/dist/commands/resume.js.map +1 -0
  24. package/dist/commands/skill-runner.d.ts +14 -0
  25. package/dist/commands/skill-runner.d.ts.map +1 -0
  26. package/dist/commands/skill-runner.js +494 -0
  27. package/dist/commands/skill-runner.js.map +1 -0
  28. package/dist/dev-environment.d.ts +26 -3
  29. package/dist/dev-environment.d.ts.map +1 -1
  30. package/dist/dev-environment.js +285 -118
  31. package/dist/dev-environment.js.map +1 -1
  32. package/dist/skills/d3k/internal-skill.md +145 -0
  33. package/dist/skills/index.test.ts +28 -1
  34. package/dist/skills/index.ts +58 -7
  35. package/dist/tui-interface-opentui.d.ts +2 -0
  36. package/dist/tui-interface-opentui.d.ts.map +1 -1
  37. package/dist/tui-interface-opentui.js +17 -3
  38. package/dist/tui-interface-opentui.js.map +1 -1
  39. package/dist/utils/agent-browser.d.ts.map +1 -1
  40. package/dist/utils/agent-browser.js +6 -3
  41. package/dist/utils/agent-browser.js.map +1 -1
  42. package/dist/utils/agent-detection.d.ts +1 -0
  43. package/dist/utils/agent-detection.d.ts.map +1 -1
  44. package/dist/utils/agent-detection.js +11 -0
  45. package/dist/utils/agent-detection.js.map +1 -1
  46. package/dist/utils/agent-selection.js +4 -4
  47. package/dist/utils/agent-selection.js.map +1 -1
  48. package/dist/utils/browser-command-argv.d.ts +1 -1
  49. package/dist/utils/browser-command-argv.d.ts.map +1 -1
  50. package/dist/utils/browser-command-argv.js +1 -1
  51. package/dist/utils/browser-command-argv.js.map +1 -1
  52. package/dist/utils/project-metadata.d.ts +4 -0
  53. package/dist/utils/project-metadata.d.ts.map +1 -0
  54. package/dist/utils/project-metadata.js +48 -0
  55. package/dist/utils/project-metadata.js.map +1 -0
  56. package/dist/utils/project-name.d.ts +2 -0
  57. package/dist/utils/project-name.d.ts.map +1 -1
  58. package/dist/utils/project-name.js +6 -0
  59. package/dist/utils/project-name.js.map +1 -1
  60. package/dist/utils/session.d.ts +14 -0
  61. package/dist/utils/session.d.ts.map +1 -0
  62. package/dist/utils/session.js +65 -0
  63. package/dist/utils/session.js.map +1 -0
  64. package/dist/utils/version-check.js +2 -2
  65. package/dist/utils/version-check.js.map +1 -1
  66. package/package.json +9 -21
  67. package/dist/commands/cloud-check-pr.d.ts +0 -9
  68. package/dist/commands/cloud-check-pr.d.ts.map +0 -1
  69. package/dist/commands/cloud-check-pr.js +0 -243
  70. package/dist/commands/cloud-check-pr.js.map +0 -1
  71. package/dist/commands/cloud-fix.d.ts +0 -13
  72. package/dist/commands/cloud-fix.d.ts.map +0 -1
  73. package/dist/commands/cloud-fix.js +0 -79
  74. package/dist/commands/cloud-fix.js.map +0 -1
  75. package/dist/commands/find-component.d.ts +0 -8
  76. package/dist/commands/find-component.d.ts.map +0 -1
  77. package/dist/commands/find-component.js +0 -182
  78. package/dist/commands/find-component.js.map +0 -1
  79. package/dist/skills/d3k/SKILL.md +0 -126
  80. package/dist/skills/index.d.ts +0 -46
  81. package/dist/skills/index.d.ts.map +0 -1
  82. package/dist/skills/index.js +0 -174
  83. package/dist/skills/index.js.map +0 -1
@@ -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 { getBundledSkillsPath, listAvailableSkills } from "./skills/index.js";
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
- // Clear log file
106
- writeFileSync(this.logFile, "");
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 bundledSkillsDir = getBundledSkillsPath();
275
- if (!bundledSkillsDir)
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
- spawnSync("sh", ["-c", `lsof -ti:${port} | xargs kill -9 2>/dev/null`], {
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
- spawnSync("sh", ["-c", `lsof -ti:${port} | xargs kill -9 2>/dev/null`], {
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, d3k find-component`));
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 oldPID = parseInt(lockContent, 10);
1193
- // Check if the process is still running
1194
- try {
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
- catch {
1200
- // Process doesn't exist, remove stale lock
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
- // Create lock file with our PID
1206
- writeFileSync(this.lockFile, process.pid.toString());
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
- // Navigate to the app
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 (!/^\d+$/.test(port)) {
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
- spawnSync("sh", ["-c", `lsof -ti:${port} | xargs kill -9 2>/dev/null`], {
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
- }, 5000); // 5 second timeout
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
- spawnSync("sh", ["-c", `lsof -ti:${port} | xargs kill -9 2>/dev/null`], {
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
- // Safety net: ensure tracked Chrome processes are always terminated on shutdown.
1977
- this.killTrackedChromePids(sessionInfo?.chromePids ?? [], "handleShutdown");
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 (!/^\d+$/.test(port)) {
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
- const { spawnSync } = await import("child_process");
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 { spawnSync } = await import("child_process");
2084
- // Kill by port pattern
2085
- spawnSync("sh", ["-c", `pkill -f ":${this.options.port}"`], { stdio: "ignore" });
2086
- this.debugLog(`Sent pkill signal for port ${this.options.port}`);
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
- spawnSync("sh", ["-c", `pkill -f "next dev.*${cwd}"`], { stdio: "ignore" });
2091
- spawnSync("sh", ["-c", `pkill -f "next-server.*${cwd}"`], { stdio: "ignore" });
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
- const result = spawnSync("sh", ["-c", `lsof -ti:${this.options.port} | xargs kill -9 2>/dev/null`], {
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