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.
- package/dist/cdp-monitor.d.ts +18 -2
- package/dist/cdp-monitor.d.ts.map +1 -1
- package/dist/cdp-monitor.js +234 -79
- package/dist/cdp-monitor.js.map +1 -1
- package/dist/cli.js +210 -95
- package/dist/cli.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/dev-environment.d.ts +24 -1
- package/dist/dev-environment.d.ts.map +1 -1
- package/dist/dev-environment.js +175 -23
- package/dist/dev-environment.js.map +1 -1
- package/dist/skills/d3k/SKILL.md +40 -0
- 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 +8 -10
- package/dist/utils/agent-browser.js.map +1 -1
- package/dist/utils/agent-selection.js +1 -1
- package/dist/utils/agent-selection.js.map +1 -1
- package/dist/utils/browser-command-argv.d.ts +8 -0
- package/dist/utils/browser-command-argv.d.ts.map +1 -0
- package/dist/utils/browser-command-argv.js +41 -0
- package/dist/utils/browser-command-argv.js.map +1 -0
- 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/package.json +9 -10
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";
|
|
@@ -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
|
|
1192
|
-
|
|
1193
|
-
|
|
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
|
-
|
|
1199
|
-
|
|
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
|
-
//
|
|
1205
|
-
writeFileSync(this.lockFile,
|
|
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
|
-
|
|
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
|
-
|
|
1976
|
-
|
|
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
|