dev3000 0.0.171 → 0.0.174

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.
@@ -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";
@@ -14,6 +14,7 @@ import { ScreencastManager } from "./screencast-manager.js";
14
14
  import { NextJsErrorDetector, OutputProcessor, StandardLogParser } from "./services/parsers/index.js";
15
15
  import { getBundledSkillsPath, 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";
@@ -163,6 +164,115 @@ export function countActiveD3kInstances(excludeCurrentPid = false) {
163
164
  return excludeCurrentPid ? 0 : 1;
164
165
  }
165
166
  }
167
+ export function parseD3kLockContent(content) {
168
+ const trimmed = content.trim();
169
+ if (!trimmed)
170
+ return null;
171
+ if (/^\d+$/.test(trimmed)) {
172
+ const pid = Number.parseInt(trimmed, 10);
173
+ return Number.isInteger(pid) && pid > 0 ? { pid } : null;
174
+ }
175
+ try {
176
+ const parsed = JSON.parse(trimmed);
177
+ const pid = parsed?.pid;
178
+ if (!parsed || typeof parsed !== "object" || typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) {
179
+ return null;
180
+ }
181
+ return {
182
+ pid,
183
+ cwd: typeof parsed.cwd === "string" ? parsed.cwd : null,
184
+ createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : null,
185
+ processStartTime: typeof parsed.processStartTime === "string" ? parsed.processStartTime : null
186
+ };
187
+ }
188
+ catch {
189
+ return null;
190
+ }
191
+ }
192
+ function getProcessPsField(pid, field) {
193
+ try {
194
+ const result = spawnSync("ps", ["-p", String(pid), "-o", `${field}=`], {
195
+ encoding: "utf8",
196
+ stdio: ["ignore", "pipe", "ignore"]
197
+ });
198
+ if (result.status !== 0)
199
+ return null;
200
+ const output = result.stdout.trim();
201
+ return output || null;
202
+ }
203
+ catch {
204
+ return null;
205
+ }
206
+ }
207
+ export function getProcessStartTime(pid) {
208
+ return getProcessPsField(pid, "lstart");
209
+ }
210
+ export function getProcessCommand(pid) {
211
+ return getProcessPsField(pid, "command");
212
+ }
213
+ function isLikelyD3kCommand(command) {
214
+ const normalized = command.toLowerCase();
215
+ return normalized.includes("d3k") || normalized.includes("dev3000");
216
+ }
217
+ export function validateD3kLockContent(content, deps = {}) {
218
+ const lockInfo = parseD3kLockContent(content);
219
+ if (!lockInfo) {
220
+ return {
221
+ active: false,
222
+ reason: "lock file contents are invalid",
223
+ lockInfo: null
224
+ };
225
+ }
226
+ const processExists = deps.processExists ||
227
+ ((pid) => {
228
+ try {
229
+ process.kill(pid, 0);
230
+ return true;
231
+ }
232
+ catch {
233
+ return false;
234
+ }
235
+ });
236
+ if (!processExists(lockInfo.pid)) {
237
+ return {
238
+ active: false,
239
+ reason: `process ${lockInfo.pid} is not running`,
240
+ lockInfo
241
+ };
242
+ }
243
+ const getStartTime = deps.getProcessStartTime || getProcessStartTime;
244
+ if (lockInfo.processStartTime) {
245
+ const liveStartTime = getStartTime(lockInfo.pid);
246
+ if (liveStartTime && liveStartTime !== lockInfo.processStartTime) {
247
+ return {
248
+ active: false,
249
+ reason: `pid ${lockInfo.pid} was reused by a different process`,
250
+ lockInfo
251
+ };
252
+ }
253
+ if (liveStartTime === lockInfo.processStartTime) {
254
+ return {
255
+ active: true,
256
+ reason: `process ${lockInfo.pid} matches recorded start time`,
257
+ lockInfo
258
+ };
259
+ }
260
+ }
261
+ const getCommand = deps.getProcessCommand || getProcessCommand;
262
+ const liveCommand = getCommand(lockInfo.pid);
263
+ if (liveCommand && !isLikelyD3kCommand(liveCommand)) {
264
+ return {
265
+ active: false,
266
+ reason: `pid ${lockInfo.pid} belongs to a non-d3k process`,
267
+ lockInfo
268
+ };
269
+ }
270
+ return {
271
+ active: true,
272
+ reason: `process ${lockInfo.pid} still owns the lock`,
273
+ lockInfo
274
+ };
275
+ }
166
276
  /**
167
277
  * Check if a port is available for binding (no process is listening on it).
168
278
  * Used for finding available ports before starting servers.
@@ -172,6 +282,40 @@ async function isPortAvailable(port) {
172
282
  if (!Number.isInteger(portNumber) || portNumber < 0 || portNumber > 65535) {
173
283
  return false;
174
284
  }
285
+ const hasActiveListener = await Promise.any(["127.0.0.1", "::1", "localhost"].map((host) => new Promise((resolve, reject) => {
286
+ const socket = createConnection({ host, port: portNumber });
287
+ let settled = false;
288
+ const finish = (result, shouldReject = false) => {
289
+ if (settled)
290
+ return;
291
+ settled = true;
292
+ socket.destroy();
293
+ if (shouldReject) {
294
+ reject(new Error("unreachable"));
295
+ }
296
+ else {
297
+ resolve(result);
298
+ }
299
+ };
300
+ socket.once("connect", () => finish(true));
301
+ socket.once("timeout", () => finish(false, true));
302
+ socket.once("error", (error) => {
303
+ if (error.code === "ECONNREFUSED" ||
304
+ error.code === "EHOSTUNREACH" ||
305
+ error.code === "ENETUNREACH" ||
306
+ error.code === "EINVAL") {
307
+ finish(false, true);
308
+ return;
309
+ }
310
+ finish(false);
311
+ });
312
+ socket.setTimeout(150);
313
+ })))
314
+ .then((result) => result)
315
+ .catch(() => false);
316
+ if (hasActiveListener) {
317
+ return false;
318
+ }
175
319
  return new Promise((resolve) => {
176
320
  const server = createServer();
177
321
  server.once("error", (error) => {
@@ -358,13 +502,15 @@ export function createPersistentLogFile() {
358
502
  * processes belong to THIS d3k instance so we don't accidentally kill
359
503
  * Chrome instances from other d3k sessions.
360
504
  */
361
- export function writeSessionInfo(projectName, logFilePath, appPort, publicUrl, cdpUrl, chromePids, serverCommand, framework, serverPid, skillsInstalled, skillsAgentId) {
505
+ export function writeSessionInfo(projectName, logFilePath, appPort, publicUrl, cdpUrl, chromePids, serverCommand, framework, serverPid, skillsInstalled, skillsAgentId, preferredBrowserTool, agentName) {
362
506
  const projectDir = getProjectDir();
363
507
  try {
364
508
  // Create project directory if it doesn't exist
365
509
  if (!existsSync(projectDir)) {
366
510
  mkdirSync(projectDir, { recursive: true });
367
511
  }
512
+ const sessionFile = join(projectDir, "session.json");
513
+ const persistedAgentName = readProjectAgentName();
368
514
  // Session file contains project info
369
515
  const sessionInfo = {
370
516
  projectName,
@@ -380,11 +526,15 @@ export function writeSessionInfo(projectName, logFilePath, appPort, publicUrl, c
380
526
  framework: framework || null,
381
527
  serverPid: serverPid || null,
382
528
  skillsInstalled: skillsInstalled || [],
383
- skillsAgentId: skillsAgentId || null
529
+ skillsAgentId: skillsAgentId || null,
530
+ preferredBrowserTool: preferredBrowserTool || "agent-browser",
531
+ agentName: agentName || persistedAgentName || null
384
532
  };
385
533
  // Write session file in project directory
386
- const sessionFile = join(projectDir, "session.json");
387
534
  writeFileSync(sessionFile, JSON.stringify(sessionInfo, null, 2));
535
+ if (sessionInfo.agentName) {
536
+ rememberProjectAgentName(sessionInfo.agentName);
537
+ }
388
538
  }
389
539
  catch (error) {
390
540
  // Non-fatal - just log a warning
@@ -1188,21 +1338,21 @@ export class DevEnvironment {
1188
1338
  // Check if lock file exists
1189
1339
  if (existsSync(this.lockFile)) {
1190
1340
  const lockContent = readFileSync(this.lockFile, "utf8");
1191
- const oldPID = parseInt(lockContent, 10);
1192
- // Check if the process is still running
1193
- try {
1194
- process.kill(oldPID, 0); // Signal 0 just checks if process exists
1195
- // Process is running, lock is valid
1341
+ const validation = validateD3kLockContent(lockContent);
1342
+ if (validation.active) {
1343
+ this.debugLog(`Lock file is active: ${validation.reason}`);
1196
1344
  return false;
1197
1345
  }
1198
- catch {
1199
- // Process doesn't exist, remove stale lock
1200
- this.debugLog(`Removing stale lock file for PID ${oldPID}`);
1201
- unlinkSync(this.lockFile);
1202
- }
1346
+ this.debugLog(`Removing stale lock file: ${validation.reason}`);
1347
+ unlinkSync(this.lockFile);
1203
1348
  }
1204
- // Create lock file with our PID
1205
- writeFileSync(this.lockFile, process.pid.toString());
1349
+ // Store enough metadata to detect PID reuse after crashes.
1350
+ writeFileSync(this.lockFile, JSON.stringify({
1351
+ pid: process.pid,
1352
+ cwd: process.cwd(),
1353
+ createdAt: new Date().toISOString(),
1354
+ processStartTime: getProcessStartTime(process.pid)
1355
+ }));
1206
1356
  this.debugLog(`Acquired lock file: ${this.lockFile}`);
1207
1357
  return true;
1208
1358
  }
@@ -1246,7 +1396,7 @@ export class DevEnvironment {
1246
1396
  const cdpUrl = this.cdpMonitor?.getCdpUrl() || null;
1247
1397
  const chromePids = this.cdpMonitor?.getChromePids() || [];
1248
1398
  const skillsInstalled = listAvailableSkills(process.cwd());
1249
- 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);
1399
+ 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);
1250
1400
  }
1251
1401
  detectPortChange(text) {
1252
1402
  // Detect Next.js port switch: "⚠ Port 3000 is in use by process 39543, using available port 3001 instead."
@@ -1560,7 +1710,7 @@ export class DevEnvironment {
1560
1710
  this.cdpMonitor = new CDPMonitor(this.options.profileDir, this.screenshotDir, (_source, message) => {
1561
1711
  this.logger.log("browser", message);
1562
1712
  }, this.options.debug, this.options.browser, this.options.pluginReactScan, this.options.port, // App server port to monitor
1563
- this.preferredAppUrl, this.options.debugPort, // Chrome debug port
1713
+ this.preferredAppUrl, this.options.browserNavigationTimeoutSeconds * 1000, this.options.debugPort, // Chrome debug port
1564
1714
  this.options.headless, // Headless mode for serverless/CI environments
1565
1715
  this.options.framework // Framework hint for optional React DevTools launch args
1566
1716
  );
@@ -1593,9 +1743,7 @@ export class DevEnvironment {
1593
1743
  this.writeCurrentSessionInfo(projectName);
1594
1744
  this.debugLog(`Updated session info with CDP URL: ${cdpUrl}, Chrome PIDs: [${chromePids.join(", ")}]`);
1595
1745
  this.logger.log("browser", `[CDP] Session info written with cdpUrl: ${cdpUrl ? "available" : "null"}`);
1596
- // Navigate to the app
1597
- await this.cdpMonitor.navigateToUrl(this.preferredAppUrl);
1598
- this.logger.log("browser", `[CDP] Navigated to ${this.preferredAppUrl}`);
1746
+ this.logger.log("browser", `[CDP] Loading page will hand off to ${this.preferredAppUrl}`);
1599
1747
  }
1600
1748
  catch (error) {
1601
1749
  // Log error and throw to trigger graceful shutdown
@@ -1931,12 +2079,14 @@ export class DevEnvironment {
1931
2079
  console.log(chalk.yellow("\nšŸ›‘ Received interrupt signal. Cleaning up processes..."));
1932
2080
  }
1933
2081
  // Shutdown CDP monitor FIRST - this should close Chrome
2082
+ let chromeShutdownHandled = false;
1934
2083
  if (this.cdpMonitor) {
1935
2084
  try {
1936
2085
  if (!this.options.tui) {
1937
2086
  console.log(chalk.cyan("šŸ”„ Closing Chrome browser..."));
1938
2087
  }
1939
2088
  await this.cdpMonitor.shutdown();
2089
+ chromeShutdownHandled = true;
1940
2090
  if (!this.options.tui) {
1941
2091
  console.log(chalk.green("āœ… Chrome browser closed"));
1942
2092
  }
@@ -1972,8 +2122,10 @@ export class DevEnvironment {
1972
2122
  }
1973
2123
  }
1974
2124
  }
1975
- // Safety net: ensure tracked Chrome processes are always terminated on shutdown.
1976
- this.killTrackedChromePids(sessionInfo?.chromePids ?? [], "handleShutdown");
2125
+ if (!chromeShutdownHandled) {
2126
+ // Safety net: ensure tracked Chrome processes are always terminated on shutdown.
2127
+ this.killTrackedChromePids(sessionInfo?.chromePids ?? [], "handleShutdown");
2128
+ }
1977
2129
  // REMOVED: No longer clean up CLI config files on shutdown
1978
2130
  // This was causing Claude Code instances to crash when dev3000 was killed
1979
2131
  // Config file cleanup removed; keep user config files untouched on shutdown