dev3000 0.0.130 → 0.0.134
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/bin/dev3000 +90 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +216 -51
- package/dist/cli.js.map +1 -1
- package/dist/commands/cloud-fix.js +8 -8
- package/dist/commands/cloud-fix.js.map +1 -1
- package/dist/components/AgentSelector.d.ts +12 -0
- package/dist/components/AgentSelector.d.ts.map +1 -0
- package/dist/components/AgentSelector.js +99 -0
- package/dist/components/AgentSelector.js.map +1 -0
- package/dist/components/SkillSelector.d.ts +9 -0
- package/dist/components/SkillSelector.d.ts.map +1 -0
- package/dist/components/SkillSelector.js +70 -0
- package/dist/components/SkillSelector.js.map +1 -0
- package/dist/dev-environment.d.ts +38 -0
- package/dist/dev-environment.d.ts.map +1 -1
- package/dist/dev-environment.js +350 -116
- package/dist/dev-environment.js.map +1 -1
- package/dist/src/tui-interface-impl.tsx +36 -22
- package/dist/tui-interface-impl.d.ts +10 -6
- package/dist/tui-interface-impl.d.ts.map +1 -1
- package/dist/tui-interface-impl.js +24 -16
- package/dist/tui-interface-impl.js.map +1 -1
- package/dist/tui-interface.d.ts +11 -7
- package/dist/tui-interface.d.ts.map +1 -1
- package/dist/tui-interface.js +6 -6
- package/dist/tui-interface.js.map +1 -1
- package/dist/utils/agent-selection.d.ts +24 -0
- package/dist/utils/agent-selection.d.ts.map +1 -0
- package/dist/utils/agent-selection.js +34 -0
- package/dist/utils/agent-selection.js.map +1 -0
- package/dist/utils/project-name.d.ts +6 -0
- package/dist/utils/project-name.d.ts.map +1 -1
- package/dist/utils/project-name.js +10 -0
- package/dist/utils/project-name.js.map +1 -1
- package/dist/utils/skill-installer.d.ts +29 -0
- package/dist/utils/skill-installer.d.ts.map +1 -0
- package/dist/utils/skill-installer.js +185 -0
- package/dist/utils/skill-installer.js.map +1 -0
- package/dist/utils/tmux-helpers.d.ts +35 -0
- package/dist/utils/tmux-helpers.d.ts.map +1 -0
- package/dist/utils/tmux-helpers.js +62 -0
- package/dist/utils/tmux-helpers.js.map +1 -0
- package/dist/utils/user-config.d.ts +6 -0
- package/dist/utils/user-config.d.ts.map +1 -1
- package/dist/utils/user-config.js +30 -5
- package/dist/utils/user-config.js.map +1 -1
- package/dist/utils/version-check.d.ts +10 -1
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +60 -2
- package/dist/utils/version-check.js.map +1 -1
- package/mcp-server/.next/BUILD_ID +1 -1
- package/mcp-server/.next/build-manifest.json +2 -2
- package/mcp-server/.next/fallback-build-manifest.json +2 -2
- package/mcp-server/.next/next-minimal-server.js.nft.json +1 -1
- package/mcp-server/.next/next-server.js.nft.json +1 -1
- package/mcp-server/.next/prerender-manifest.json +3 -3
- package/mcp-server/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/mcp-server/.next/server/app/_global-error.html +2 -2
- package/mcp-server/.next/server/app/_global-error.rsc +1 -1
- package/mcp-server/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/mcp-server/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/mcp-server/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/mcp-server/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/mcp-server/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/mcp-server/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/mcp-server/.next/server/app/_not-found.html +1 -1
- package/mcp-server/.next/server/app/_not-found.rsc +1 -1
- package/mcp-server/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/mcp-server/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/mcp-server/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/mcp-server/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/mcp-server/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/mcp-server/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/mcp-server/.next/server/app/api/jank/[session]/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/api/logs/append/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/api/logs/head/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/api/logs/list/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/api/logs/rotate/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/api/logs/stream/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/api/logs/tail/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/api/orchestrator/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/api/screenshots/[filename]/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/api/screenshots/capture/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/api/screenshots/clear/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/api/screenshots/list/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/api/teams/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/api/tools/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/index.html +1 -1
- package/mcp-server/.next/server/app/index.rsc +1 -1
- package/mcp-server/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/mcp-server/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/mcp-server/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/mcp-server/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/mcp-server/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/mcp-server/.next/server/app/logs/page.js.nft.json +1 -1
- package/mcp-server/.next/server/app/mcp/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/page.js.nft.json +1 -1
- package/mcp-server/.next/server/app/video/[session]/page.js.nft.json +1 -1
- package/mcp-server/.next/server/chunks/[root-of-the-server]__141a5bc7._.js.map +1 -1
- package/mcp-server/.next/server/pages/404.html +1 -1
- package/mcp-server/.next/server/pages/500.html +2 -2
- package/mcp-server/.next/server/server-reference-manifest.js +1 -1
- package/mcp-server/.next/server/server-reference-manifest.json +1 -1
- package/mcp-server/app/mcp/route.ts +2 -0
- package/package.json +11 -4
- package/src/tui-interface-impl.tsx +36 -22
- /package/mcp-server/.next/static/{YTIno5QuIO6mgeAWJ8zng → Fekb6bDxTd8hH8dFQNlIK}/_buildManifest.js +0 -0
- /package/mcp-server/.next/static/{YTIno5QuIO6mgeAWJ8zng → Fekb6bDxTd8hH8dFQNlIK}/_clientMiddlewareManifest.json +0 -0
- /package/mcp-server/.next/static/{YTIno5QuIO6mgeAWJ8zng → Fekb6bDxTd8hH8dFQNlIK}/_ssgManifest.js +0 -0
package/dist/dev-environment.js
CHANGED
|
@@ -10,9 +10,9 @@ import { ScreencastManager } from "./screencast-manager.js";
|
|
|
10
10
|
import { NextJsErrorDetector, OutputProcessor, StandardLogParser } from "./services/parsers/index.js";
|
|
11
11
|
import { DevTUI } from "./tui-interface.js";
|
|
12
12
|
import { formatMcpConfigTargets, MCP_CONFIG_TARGETS } from "./utils/mcp-configs.js";
|
|
13
|
-
import { getProjectDisplayName, getProjectName } from "./utils/project-name.js";
|
|
13
|
+
import { getProjectDir, getProjectDisplayName, getProjectName } from "./utils/project-name.js";
|
|
14
14
|
import { formatTimestamp } from "./utils/timestamp.js";
|
|
15
|
-
import { checkForUpdates } from "./utils/version-check.js";
|
|
15
|
+
import { checkForUpdates, performUpgradeAsync } from "./utils/version-check.js";
|
|
16
16
|
// MCP names
|
|
17
17
|
const MCP_NAMES = {
|
|
18
18
|
DEV3000: "dev3000",
|
|
@@ -41,6 +41,66 @@ export const ORPHANED_PROCESS_CLEANUP_PATTERNS = [
|
|
|
41
41
|
function hasVercelProject() {
|
|
42
42
|
return existsSync(join(process.cwd(), ".vercel"));
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Gracefully terminate a process by first trying SIGTERM, waiting for graceful
|
|
46
|
+
* shutdown, then falling back to SIGKILL if needed.
|
|
47
|
+
*
|
|
48
|
+
* This is important for processes like Next.js dev server that need to clean up
|
|
49
|
+
* resources (like .next/dev/lock) before exiting.
|
|
50
|
+
*
|
|
51
|
+
* @param options - Kill options including PID and optional overrides for testing
|
|
52
|
+
* @returns Result indicating how the process was terminated
|
|
53
|
+
*/
|
|
54
|
+
export async function gracefulKillProcess(options) {
|
|
55
|
+
const { pid, gracePeriodMs = 500, killFn = (p, s) => process.kill(p, s), delayFn = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), debugLog = () => { } } = options;
|
|
56
|
+
const result = {
|
|
57
|
+
terminated: false,
|
|
58
|
+
graceful: false,
|
|
59
|
+
forcedKill: false
|
|
60
|
+
};
|
|
61
|
+
// Try process group first (negative PID), fall back to direct PID
|
|
62
|
+
const pgid = -pid;
|
|
63
|
+
// Step 1: Send SIGTERM for graceful shutdown
|
|
64
|
+
debugLog(`Sending SIGTERM to process group ${pgid} (PID: ${pid})`);
|
|
65
|
+
try {
|
|
66
|
+
killFn(pgid, "SIGTERM");
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Process group may not exist, try direct kill
|
|
70
|
+
try {
|
|
71
|
+
killFn(pid, "SIGTERM");
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Process may already be dead
|
|
75
|
+
debugLog(`Process ${pid} not found for SIGTERM`);
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Step 2: Wait for graceful shutdown
|
|
80
|
+
await delayFn(gracePeriodMs);
|
|
81
|
+
// Step 3: Check if process is still running
|
|
82
|
+
try {
|
|
83
|
+
// Signal 0 checks if process exists without killing it
|
|
84
|
+
killFn(pid, 0);
|
|
85
|
+
// Process still running, need to force kill
|
|
86
|
+
debugLog(`Process still running after SIGTERM, sending SIGKILL`);
|
|
87
|
+
try {
|
|
88
|
+
killFn(pgid, "SIGKILL");
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
killFn(pid, "SIGKILL");
|
|
92
|
+
}
|
|
93
|
+
result.terminated = true;
|
|
94
|
+
result.forcedKill = true;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Process already dead - graceful shutdown succeeded
|
|
98
|
+
debugLog(`Process terminated gracefully after SIGTERM`);
|
|
99
|
+
result.terminated = true;
|
|
100
|
+
result.graceful = true;
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
44
104
|
class Logger {
|
|
45
105
|
logFile;
|
|
46
106
|
tail;
|
|
@@ -436,7 +496,11 @@ async function ensureCursorMcpServers(mcpPort, _appPort, _enableChromeDevtools)
|
|
|
436
496
|
}
|
|
437
497
|
/**
|
|
438
498
|
* Ensure MCP server configurations are added to project's opencode.json
|
|
439
|
-
* OpenCode uses a different structure: "mcp" instead of "mcpServers"
|
|
499
|
+
* OpenCode uses a different structure: "mcp" instead of "mcpServers"
|
|
500
|
+
*
|
|
501
|
+
* IMPORTANT: OpenCode has issues with "type": "remote" for HTTP MCP servers.
|
|
502
|
+
* The workaround is to use "type": "local" with mcp-remote package to proxy requests.
|
|
503
|
+
* See: https://github.com/sst/opencode/issues/1595
|
|
440
504
|
*/
|
|
441
505
|
async function ensureOpenCodeMcpServers(mcpPort, _appPort, _enableChromeDevtools) {
|
|
442
506
|
try {
|
|
@@ -454,30 +518,43 @@ async function ensureOpenCodeMcpServers(mcpPort, _appPort, _enableChromeDevtools
|
|
|
454
518
|
if (!settings.mcp) {
|
|
455
519
|
settings.mcp = {};
|
|
456
520
|
}
|
|
457
|
-
let
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
521
|
+
let changed = false;
|
|
522
|
+
// Always update dev3000 MCP server config to ensure correct format
|
|
523
|
+
// Try simple remote type first - no OAuth needed for local dev3000
|
|
524
|
+
const expectedDev3000Config = {
|
|
525
|
+
type: "remote",
|
|
526
|
+
url: `http://localhost:${mcpPort}/mcp`,
|
|
527
|
+
enabled: true
|
|
528
|
+
};
|
|
529
|
+
const currentDev3000 = settings.mcp[MCP_NAMES.DEV3000];
|
|
530
|
+
if (!currentDev3000 ||
|
|
531
|
+
currentDev3000.type !== expectedDev3000Config.type ||
|
|
532
|
+
currentDev3000.url !== expectedDev3000Config.url) {
|
|
533
|
+
settings.mcp[MCP_NAMES.DEV3000] = expectedDev3000Config;
|
|
534
|
+
changed = true;
|
|
535
|
+
}
|
|
536
|
+
// Always update Vercel MCP if this is a Vercel project (.vercel directory exists)
|
|
537
|
+
// Vercel MCP requires OAuth, so use OpenCode's native remote type with oauth: {}
|
|
538
|
+
// This triggers OpenCode's built-in OAuth flow instead of mcp-remote
|
|
539
|
+
// See: https://github.com/sst/opencode/issues/5444
|
|
540
|
+
if (hasVercelProject()) {
|
|
541
|
+
const expectedVercelConfig = {
|
|
473
542
|
type: "remote",
|
|
474
543
|
url: VERCEL_MCP_URL,
|
|
544
|
+
oauth: {},
|
|
475
545
|
enabled: true
|
|
476
546
|
};
|
|
477
|
-
|
|
547
|
+
const currentVercel = settings.mcp[MCP_NAMES.VERCEL];
|
|
548
|
+
if (!currentVercel ||
|
|
549
|
+
currentVercel.type !== expectedVercelConfig.type ||
|
|
550
|
+
currentVercel.url !== expectedVercelConfig.url ||
|
|
551
|
+
!currentVercel.oauth) {
|
|
552
|
+
settings.mcp[MCP_NAMES.VERCEL] = expectedVercelConfig;
|
|
553
|
+
changed = true;
|
|
554
|
+
}
|
|
478
555
|
}
|
|
479
|
-
// Write if we
|
|
480
|
-
if (
|
|
556
|
+
// Write if we changed anything
|
|
557
|
+
if (changed) {
|
|
481
558
|
writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf-8");
|
|
482
559
|
}
|
|
483
560
|
}
|
|
@@ -521,8 +598,8 @@ async function ensureD3kSkill() {
|
|
|
521
598
|
export function createPersistentLogFile() {
|
|
522
599
|
// Get unique project name
|
|
523
600
|
const projectName = getProjectName();
|
|
524
|
-
// Use ~/.d3k/logs directory for persistent, accessible logs
|
|
525
|
-
const logBaseDir = join(
|
|
601
|
+
// Use ~/.d3k/{projectName}/logs directory for persistent, accessible logs
|
|
602
|
+
const logBaseDir = join(getProjectDir(), "logs");
|
|
526
603
|
try {
|
|
527
604
|
if (!existsSync(logBaseDir)) {
|
|
528
605
|
mkdirSync(logBaseDir, { recursive: true });
|
|
@@ -539,12 +616,12 @@ export function createPersistentLogFile() {
|
|
|
539
616
|
}
|
|
540
617
|
}
|
|
541
618
|
// Write session info for MCP server to discover
|
|
542
|
-
function writeSessionInfo(projectName, logFilePath, appPort, mcpPort, cdpUrl, chromePids, serverCommand, framework) {
|
|
543
|
-
const
|
|
619
|
+
function writeSessionInfo(projectName, logFilePath, appPort, mcpPort, cdpUrl, chromePids, serverCommand, framework, serverPid) {
|
|
620
|
+
const projectDir = getProjectDir();
|
|
544
621
|
try {
|
|
545
|
-
// Create
|
|
546
|
-
if (!existsSync(
|
|
547
|
-
mkdirSync(
|
|
622
|
+
// Create project directory if it doesn't exist
|
|
623
|
+
if (!existsSync(projectDir)) {
|
|
624
|
+
mkdirSync(projectDir, { recursive: true });
|
|
548
625
|
}
|
|
549
626
|
// Session file contains project info
|
|
550
627
|
const sessionInfo = {
|
|
@@ -558,10 +635,11 @@ function writeSessionInfo(projectName, logFilePath, appPort, mcpPort, cdpUrl, ch
|
|
|
558
635
|
cwd: process.cwd(),
|
|
559
636
|
chromePids: chromePids || [],
|
|
560
637
|
serverCommand: serverCommand || null,
|
|
561
|
-
framework: framework || null
|
|
638
|
+
framework: framework || null,
|
|
639
|
+
serverPid: serverPid || null
|
|
562
640
|
};
|
|
563
|
-
// Write session file
|
|
564
|
-
const sessionFile = join(
|
|
641
|
+
// Write session file in project directory
|
|
642
|
+
const sessionFile = join(projectDir, "session.json");
|
|
565
643
|
writeFileSync(sessionFile, JSON.stringify(sessionInfo, null, 2));
|
|
566
644
|
}
|
|
567
645
|
catch (error) {
|
|
@@ -571,8 +649,7 @@ function writeSessionInfo(projectName, logFilePath, appPort, mcpPort, cdpUrl, ch
|
|
|
571
649
|
}
|
|
572
650
|
// Get Chrome PIDs for this instance
|
|
573
651
|
function getSessionChromePids(projectName) {
|
|
574
|
-
const
|
|
575
|
-
const sessionFile = join(sessionDir, `${projectName}.json`);
|
|
652
|
+
const sessionFile = join(homedir(), ".d3k", projectName, "session.json");
|
|
576
653
|
try {
|
|
577
654
|
if (existsSync(sessionFile)) {
|
|
578
655
|
const sessionInfo = JSON.parse(readFileSync(sessionFile, "utf8"));
|
|
@@ -584,23 +661,45 @@ function getSessionChromePids(projectName) {
|
|
|
584
661
|
}
|
|
585
662
|
return [];
|
|
586
663
|
}
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
|
|
664
|
+
// Get server PID for this instance
|
|
665
|
+
function getSessionServerPid(projectName) {
|
|
666
|
+
const sessionFile = join(homedir(), ".d3k", projectName, "session.json");
|
|
667
|
+
try {
|
|
668
|
+
if (existsSync(sessionFile)) {
|
|
669
|
+
const sessionInfo = JSON.parse(readFileSync(sessionFile, "utf8"));
|
|
670
|
+
return sessionInfo.serverPid || null;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
catch (_error) {
|
|
674
|
+
// Non-fatal - return null
|
|
675
|
+
}
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
function createLogFileInDir(baseDir, _projectName) {
|
|
679
|
+
// Create short timestamp: MMDD-HHmmss (e.g., 0106-171301)
|
|
680
|
+
const now = new Date();
|
|
681
|
+
const timestamp = [
|
|
682
|
+
String(now.getMonth() + 1).padStart(2, "0"),
|
|
683
|
+
String(now.getDate()).padStart(2, "0"),
|
|
684
|
+
"-",
|
|
685
|
+
String(now.getHours()).padStart(2, "0"),
|
|
686
|
+
String(now.getMinutes()).padStart(2, "0"),
|
|
687
|
+
String(now.getSeconds()).padStart(2, "0")
|
|
688
|
+
].join("");
|
|
689
|
+
// Create log file path (project name already in directory path)
|
|
690
|
+
const logFileName = `${timestamp}.log`;
|
|
592
691
|
const logFilePath = join(baseDir, logFileName);
|
|
593
|
-
// Prune old logs
|
|
594
|
-
pruneOldLogs(baseDir
|
|
692
|
+
// Prune old logs (keep only 10 most recent)
|
|
693
|
+
pruneOldLogs(baseDir);
|
|
595
694
|
// Create the log file
|
|
596
695
|
writeFileSync(logFilePath, "");
|
|
597
696
|
return logFilePath;
|
|
598
697
|
}
|
|
599
|
-
function pruneOldLogs(baseDir
|
|
698
|
+
function pruneOldLogs(baseDir) {
|
|
600
699
|
try {
|
|
601
|
-
// Find all log files
|
|
700
|
+
// Find all log files in directory
|
|
602
701
|
const files = readdirSync(baseDir)
|
|
603
|
-
.filter((file) => file.
|
|
702
|
+
.filter((file) => file.endsWith(".log"))
|
|
604
703
|
.map((file) => ({
|
|
605
704
|
name: file,
|
|
606
705
|
path: join(baseDir, file),
|
|
@@ -620,8 +719,8 @@ function pruneOldLogs(baseDir, projectName) {
|
|
|
620
719
|
}
|
|
621
720
|
}
|
|
622
721
|
}
|
|
623
|
-
catch (
|
|
624
|
-
|
|
722
|
+
catch (_error) {
|
|
723
|
+
// Silently ignore prune errors
|
|
625
724
|
}
|
|
626
725
|
}
|
|
627
726
|
export class DevEnvironment {
|
|
@@ -657,9 +756,20 @@ export class DevEnvironment {
|
|
|
657
756
|
this.disabledMcpConfigSet = new Set(this.options.disabledMcpConfigs);
|
|
658
757
|
this.logger = new Logger(options.logFile, options.tail || false, options.dateTimeFormat || "local");
|
|
659
758
|
this.outputProcessor = new OutputProcessor(new StandardLogParser(), new NextJsErrorDetector());
|
|
660
|
-
//
|
|
661
|
-
const
|
|
662
|
-
const
|
|
759
|
+
// Detect if running from compiled binary
|
|
760
|
+
const execPath = process.execPath;
|
|
761
|
+
const isCompiledBinary = execPath.includes("@d3k/darwin-") || execPath.includes("d3k-darwin-") || execPath.endsWith("/dev3000");
|
|
762
|
+
let packageRoot;
|
|
763
|
+
if (isCompiledBinary) {
|
|
764
|
+
// For compiled binaries: bin/dev3000 -> package root
|
|
765
|
+
const binDir = dirname(execPath);
|
|
766
|
+
packageRoot = dirname(binDir);
|
|
767
|
+
}
|
|
768
|
+
else {
|
|
769
|
+
// Normal install: dist/dev-environment.js -> package root
|
|
770
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
771
|
+
packageRoot = dirname(dirname(currentFile));
|
|
772
|
+
}
|
|
663
773
|
// Always use MCP server's public directory for screenshots to ensure they're web-accessible
|
|
664
774
|
// and avoid permission issues with /var/log paths
|
|
665
775
|
this.screenshotDir = join(packageRoot, "mcp-server", "public", "screenshots");
|
|
@@ -668,30 +778,36 @@ export class DevEnvironment {
|
|
|
668
778
|
this.pidFile = join(tmpdir(), `dev3000-${projectName}.pid`);
|
|
669
779
|
this.lockFile = join(tmpdir(), `dev3000-${projectName}.lock`);
|
|
670
780
|
this.mcpPublicDir = join(packageRoot, "mcp-server", "public", "screenshots");
|
|
671
|
-
// Read version from package.json
|
|
781
|
+
// Read version - for compiled binaries, use injected version; otherwise read from package.json
|
|
672
782
|
this.version = "0.0.0";
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
783
|
+
// Check for compile-time injected version first
|
|
784
|
+
if (typeof __D3K_VERSION__ !== "undefined") {
|
|
785
|
+
this.version = __D3K_VERSION__;
|
|
786
|
+
}
|
|
787
|
+
else {
|
|
678
788
|
try {
|
|
679
|
-
const
|
|
680
|
-
const
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
789
|
+
const packageJsonPath = join(packageRoot, "package.json");
|
|
790
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
791
|
+
this.version = packageJson.version;
|
|
792
|
+
// Use git to detect if we're in the dev3000 source repository
|
|
793
|
+
try {
|
|
794
|
+
const { execSync } = require("child_process");
|
|
795
|
+
const gitRemote = execSync("git remote get-url origin 2>/dev/null", {
|
|
796
|
+
cwd: packageRoot,
|
|
797
|
+
encoding: "utf8"
|
|
798
|
+
}).trim();
|
|
799
|
+
if (gitRemote.includes("vercel-labs/dev3000") && !this.version.includes("canary")) {
|
|
800
|
+
this.version += "-local";
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
catch {
|
|
804
|
+
// Not in git repo or no git - use version as-is
|
|
686
805
|
}
|
|
687
806
|
}
|
|
688
|
-
catch {
|
|
689
|
-
//
|
|
807
|
+
catch (_error) {
|
|
808
|
+
// Use fallback version
|
|
690
809
|
}
|
|
691
810
|
}
|
|
692
|
-
catch (_error) {
|
|
693
|
-
// Use fallback version
|
|
694
|
-
}
|
|
695
811
|
// Initialize spinner for clean output management (only if not in TUI mode)
|
|
696
812
|
this.spinner = ora({
|
|
697
813
|
text: "Initializing...",
|
|
@@ -809,7 +925,7 @@ export class DevEnvironment {
|
|
|
809
925
|
this.debugLog(`Waiting ${waitTime}ms for port ${this.options.mcpPort} to be released...`);
|
|
810
926
|
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
|
811
927
|
// Check if port is now free
|
|
812
|
-
const available = await isPortAvailable(this.options.mcpPort
|
|
928
|
+
const available = await isPortAvailable(this.options.mcpPort?.toString() ?? "");
|
|
813
929
|
if (available) {
|
|
814
930
|
this.debugLog(`Port ${this.options.mcpPort} released successfully`);
|
|
815
931
|
return;
|
|
@@ -929,14 +1045,26 @@ export class DevEnvironment {
|
|
|
929
1045
|
serversOnly: this.options.serversOnly,
|
|
930
1046
|
version: this.version,
|
|
931
1047
|
projectName: projectDisplayName,
|
|
932
|
-
|
|
1048
|
+
updateInfo: null // Will be updated async after auto-upgrade
|
|
933
1049
|
});
|
|
934
1050
|
await this.tui.start();
|
|
935
|
-
//
|
|
936
|
-
updateCheckPromise.then((versionInfo) => {
|
|
1051
|
+
// Auto-upgrade if update available (non-blocking)
|
|
1052
|
+
updateCheckPromise.then(async (versionInfo) => {
|
|
937
1053
|
if (versionInfo?.updateAvailable && versionInfo.latestVersion && this.tui) {
|
|
938
|
-
this.debugLog(`Update available: ${versionInfo.currentVersion} -> ${versionInfo.latestVersion}
|
|
939
|
-
|
|
1054
|
+
this.debugLog(`Update available: ${versionInfo.currentVersion} -> ${versionInfo.latestVersion}, auto-upgrading...`);
|
|
1055
|
+
// Perform upgrade in background
|
|
1056
|
+
const result = await performUpgradeAsync();
|
|
1057
|
+
if (result.success) {
|
|
1058
|
+
const newVersion = result.newVersion || versionInfo.latestVersion;
|
|
1059
|
+
this.debugLog(`Auto-upgrade successful: ${newVersion}`);
|
|
1060
|
+
// Show "Updated to vX.X.X" message (auto-hides after 10s)
|
|
1061
|
+
this.tui.updateUpdateInfo({ type: "updated", newVersion });
|
|
1062
|
+
}
|
|
1063
|
+
else {
|
|
1064
|
+
// Upgrade failed - show update available instead
|
|
1065
|
+
this.debugLog(`Auto-upgrade failed: ${result.error}, showing update available`);
|
|
1066
|
+
this.tui.updateUpdateInfo({ type: "available", latestVersion: versionInfo.latestVersion });
|
|
1067
|
+
}
|
|
940
1068
|
}
|
|
941
1069
|
});
|
|
942
1070
|
// Check ports in background after TUI is visible
|
|
@@ -973,7 +1101,7 @@ export class DevEnvironment {
|
|
|
973
1101
|
if (!serverStarted) {
|
|
974
1102
|
await this.tui.updateStatus("❌ Server failed to start");
|
|
975
1103
|
console.error(chalk.red("\n❌ Your app server failed to start after 30 seconds."));
|
|
976
|
-
console.error(chalk.yellow(`Check the logs at ~/.d3k
|
|
1104
|
+
console.error(chalk.yellow(`Check the logs at ~/.d3k/${getProjectName()}/logs/ for errors.`));
|
|
977
1105
|
console.error(chalk.yellow("Exiting without launching browser."));
|
|
978
1106
|
process.exit(1);
|
|
979
1107
|
}
|
|
@@ -1008,7 +1136,7 @@ export class DevEnvironment {
|
|
|
1008
1136
|
// Write session info for MCP server discovery (include CDP URL if browser monitoring was started)
|
|
1009
1137
|
const cdpUrl = this.cdpMonitor?.getCdpUrl() || null;
|
|
1010
1138
|
const chromePids = this.cdpMonitor?.getChromePids() || [];
|
|
1011
|
-
writeSessionInfo(projectName, this.options.logFile, this.options.port, this.options.mcpPort, cdpUrl, chromePids, this.options.serverCommand, this.options.framework);
|
|
1139
|
+
writeSessionInfo(projectName, this.options.logFile, this.options.port, this.options.mcpPort, cdpUrl, chromePids, this.options.serverCommand, this.options.framework, this.serverProcess?.pid);
|
|
1012
1140
|
// Clear status - ready!
|
|
1013
1141
|
await this.tui.updateStatus(null);
|
|
1014
1142
|
}
|
|
@@ -1036,7 +1164,7 @@ export class DevEnvironment {
|
|
|
1036
1164
|
if (!serverStarted) {
|
|
1037
1165
|
this.spinner.fail("Server failed to start");
|
|
1038
1166
|
console.error(chalk.red("\n❌ Your app server failed to start after 30 seconds."));
|
|
1039
|
-
console.error(chalk.yellow(`Check the logs at ~/.d3k
|
|
1167
|
+
console.error(chalk.yellow(`Check the logs at ~/.d3k/${getProjectName()}/logs/ for errors.`));
|
|
1040
1168
|
console.error(chalk.yellow("Exiting without launching browser."));
|
|
1041
1169
|
process.exit(1);
|
|
1042
1170
|
}
|
|
@@ -1071,7 +1199,7 @@ export class DevEnvironment {
|
|
|
1071
1199
|
// Include CDP URL if browser monitoring was started
|
|
1072
1200
|
const cdpUrl = this.cdpMonitor?.getCdpUrl() || null;
|
|
1073
1201
|
const chromePids = this.cdpMonitor?.getChromePids() || [];
|
|
1074
|
-
writeSessionInfo(projectName, this.options.logFile, this.options.port, this.options.mcpPort, cdpUrl, chromePids, this.options.serverCommand, this.options.framework);
|
|
1202
|
+
writeSessionInfo(projectName, this.options.logFile, this.options.port, this.options.mcpPort, cdpUrl, chromePids, this.options.serverCommand, this.options.framework, this.serverProcess?.pid);
|
|
1075
1203
|
// Complete startup with success message only in non-TUI mode
|
|
1076
1204
|
this.spinner.succeed("Development environment ready!");
|
|
1077
1205
|
// Regular console output (when TUI is disabled with --no-tui)
|
|
@@ -1084,12 +1212,21 @@ export class DevEnvironment {
|
|
|
1084
1212
|
console.log(chalk.cyan("🖥️ Servers-only mode - use Chrome extension for browser monitoring"));
|
|
1085
1213
|
}
|
|
1086
1214
|
console.log(chalk.cyan("\nUse Ctrl-C to stop.\n"));
|
|
1087
|
-
//
|
|
1215
|
+
// Auto-upgrade in non-TUI mode (non-blocking)
|
|
1088
1216
|
checkForUpdates()
|
|
1089
|
-
.then((versionInfo) => {
|
|
1217
|
+
.then(async (versionInfo) => {
|
|
1090
1218
|
if (versionInfo?.updateAvailable && versionInfo.latestVersion) {
|
|
1091
|
-
|
|
1092
|
-
|
|
1219
|
+
this.debugLog(`Update available: ${versionInfo.currentVersion} -> ${versionInfo.latestVersion}, auto-upgrading...`);
|
|
1220
|
+
const result = await performUpgradeAsync();
|
|
1221
|
+
if (result.success) {
|
|
1222
|
+
const newVersion = result.newVersion || versionInfo.latestVersion;
|
|
1223
|
+
console.log(chalk.green(`\n✓ Updated to v${newVersion}`));
|
|
1224
|
+
}
|
|
1225
|
+
else {
|
|
1226
|
+
// Upgrade failed - show update available
|
|
1227
|
+
console.log(chalk.yellow(`\n↑ Update available: v${versionInfo.currentVersion} → v${versionInfo.latestVersion}`));
|
|
1228
|
+
console.log(chalk.gray(` Run 'd3k upgrade' to update\n`));
|
|
1229
|
+
}
|
|
1093
1230
|
}
|
|
1094
1231
|
})
|
|
1095
1232
|
.catch(() => {
|
|
@@ -1263,7 +1400,7 @@ export class DevEnvironment {
|
|
|
1263
1400
|
const cdpUrl = this.cdpMonitor?.getCdpUrl();
|
|
1264
1401
|
const chromePids = this.cdpMonitor?.getChromePids() || [];
|
|
1265
1402
|
if (cdpUrl || chromePids.length > 0) {
|
|
1266
|
-
writeSessionInfo(projectName, this.options.logFile, this.options.port, this.options.mcpPort, cdpUrl || undefined, chromePids, this.options.serverCommand);
|
|
1403
|
+
writeSessionInfo(projectName, this.options.logFile, this.options.port, this.options.mcpPort, cdpUrl || undefined, chromePids, this.options.serverCommand, this.options.framework, this.serverProcess?.pid);
|
|
1267
1404
|
this.debugLog(`Updated session info with new port: ${this.options.port}`);
|
|
1268
1405
|
}
|
|
1269
1406
|
// Update TUI header with new port
|
|
@@ -1315,11 +1452,11 @@ export class DevEnvironment {
|
|
|
1315
1452
|
}
|
|
1316
1453
|
// Always write to d3k debug log file (even when not in debug mode)
|
|
1317
1454
|
try {
|
|
1318
|
-
const
|
|
1319
|
-
if (!existsSync(
|
|
1320
|
-
mkdirSync(
|
|
1455
|
+
const projectDir = getProjectDir();
|
|
1456
|
+
if (!existsSync(projectDir)) {
|
|
1457
|
+
mkdirSync(projectDir, { recursive: true });
|
|
1321
1458
|
}
|
|
1322
|
-
const debugLogFile = join(
|
|
1459
|
+
const debugLogFile = join(projectDir, "debug.log");
|
|
1323
1460
|
const logEntry = `[${timestamp}] [DEBUG] ${message}\n`;
|
|
1324
1461
|
appendFileSync(debugLogFile, logEntry);
|
|
1325
1462
|
}
|
|
@@ -1352,14 +1489,58 @@ export class DevEnvironment {
|
|
|
1352
1489
|
console.log(chalk.yellow(`💡 Check logs for details: ${this.options.logFile}`));
|
|
1353
1490
|
}
|
|
1354
1491
|
}
|
|
1492
|
+
checkForCommonIssues() {
|
|
1493
|
+
try {
|
|
1494
|
+
if (!existsSync(this.options.logFile))
|
|
1495
|
+
return;
|
|
1496
|
+
const logContent = readFileSync(this.options.logFile, "utf8");
|
|
1497
|
+
// Check for Next.js lock file issue (this fix also kills the process holding the port)
|
|
1498
|
+
if (logContent.includes("Unable to acquire lock") && logContent.includes(".next/dev/lock")) {
|
|
1499
|
+
console.log(chalk.yellow("\n💡 Detected Next.js lock file issue!"));
|
|
1500
|
+
console.log(chalk.white(" Another Next.js dev server may be running or crashed without cleanup."));
|
|
1501
|
+
console.log(chalk.white(" To fix, run:"));
|
|
1502
|
+
console.log(chalk.cyan(" rm -f .next/dev/lock && pkill -f 'next dev'"));
|
|
1503
|
+
return; // pkill also fixes the port-in-use issue, so skip that check
|
|
1504
|
+
}
|
|
1505
|
+
// Check for port in use (only if not a Next.js lock issue)
|
|
1506
|
+
const portInUseMatch = logContent.match(/Port (\d+) is in use by process (\d+)/);
|
|
1507
|
+
if (portInUseMatch) {
|
|
1508
|
+
const [, port, pid] = portInUseMatch;
|
|
1509
|
+
console.log(chalk.yellow(`\n💡 Port ${port} was already in use by process ${pid}`));
|
|
1510
|
+
console.log(chalk.white(" To kill that process, run:"));
|
|
1511
|
+
console.log(chalk.cyan(` kill -9 ${pid}`));
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
catch {
|
|
1515
|
+
// Ignore errors reading log file
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1355
1518
|
async startMcpServer() {
|
|
1356
1519
|
this.debugLog("Starting MCP server setup");
|
|
1357
1520
|
// Note: MCP server cleanup now happens earlier in checkPortsAvailable()
|
|
1358
1521
|
// to ensure the port is free before we check availability
|
|
1359
1522
|
// Get the path to our bundled MCP server
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1523
|
+
// Handle both normal npm install and compiled binary cases
|
|
1524
|
+
let mcpServerPath;
|
|
1525
|
+
// Check if we're running from a compiled binary
|
|
1526
|
+
// Compiled binaries have process.execPath pointing to the binary itself
|
|
1527
|
+
const execPath = process.execPath;
|
|
1528
|
+
const isCompiledBinary = execPath.includes("@d3k/darwin-") || execPath.includes("d3k-darwin-") || execPath.endsWith("/dev3000");
|
|
1529
|
+
if (isCompiledBinary) {
|
|
1530
|
+
// For compiled binaries, mcp-server is a sibling to the bin directory
|
|
1531
|
+
// Structure: packages/d3k-darwin-arm64/bin/dev3000 -> packages/d3k-darwin-arm64/mcp-server
|
|
1532
|
+
const binDir = dirname(execPath);
|
|
1533
|
+
const packageDir = dirname(binDir);
|
|
1534
|
+
mcpServerPath = join(packageDir, "mcp-server");
|
|
1535
|
+
this.debugLog(`Compiled binary detected, MCP server path: ${mcpServerPath}`);
|
|
1536
|
+
}
|
|
1537
|
+
else {
|
|
1538
|
+
// Normal npm install - mcp-server is in the package root
|
|
1539
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
1540
|
+
const packageRoot = dirname(dirname(currentFile)); // Go up from dist/ to package root
|
|
1541
|
+
mcpServerPath = join(packageRoot, "mcp-server");
|
|
1542
|
+
this.debugLog(`Standard install detected, MCP server path: ${mcpServerPath}`);
|
|
1543
|
+
}
|
|
1363
1544
|
this.debugLog(`Initial MCP server path: ${mcpServerPath}`);
|
|
1364
1545
|
// For pnpm global installs, resolve symlinks to get the real path
|
|
1365
1546
|
if (existsSync(mcpServerPath)) {
|
|
@@ -1790,13 +1971,12 @@ export class DevEnvironment {
|
|
|
1790
1971
|
}
|
|
1791
1972
|
initializeD3KLog() {
|
|
1792
1973
|
try {
|
|
1793
|
-
const
|
|
1794
|
-
if (!existsSync(
|
|
1795
|
-
mkdirSync(
|
|
1974
|
+
const projectDir = getProjectDir();
|
|
1975
|
+
if (!existsSync(projectDir)) {
|
|
1976
|
+
mkdirSync(projectDir, { recursive: true });
|
|
1796
1977
|
}
|
|
1797
|
-
// Create
|
|
1798
|
-
const
|
|
1799
|
-
const d3kLogFile = join(debugLogDir, `${projectName}-d3k.log`);
|
|
1978
|
+
// Create D3K log file and clear it for new session
|
|
1979
|
+
const d3kLogFile = join(projectDir, "d3k.log");
|
|
1800
1980
|
writeFileSync(d3kLogFile, "");
|
|
1801
1981
|
}
|
|
1802
1982
|
catch {
|
|
@@ -1809,13 +1989,11 @@ export class DevEnvironment {
|
|
|
1809
1989
|
const timestamp = formatTimestamp(new Date(), this.options.dateTimeFormat || "local");
|
|
1810
1990
|
const logEntry = `[${timestamp}] [D3K] ${message}\n`;
|
|
1811
1991
|
try {
|
|
1812
|
-
const
|
|
1813
|
-
if (!existsSync(
|
|
1814
|
-
mkdirSync(
|
|
1992
|
+
const projectDir = getProjectDir();
|
|
1993
|
+
if (!existsSync(projectDir)) {
|
|
1994
|
+
mkdirSync(projectDir, { recursive: true });
|
|
1815
1995
|
}
|
|
1816
|
-
|
|
1817
|
-
const projectName = getProjectName();
|
|
1818
|
-
const d3kLogFile = join(debugLogDir, `${projectName}-d3k.log`);
|
|
1996
|
+
const d3kLogFile = join(projectDir, "d3k.log");
|
|
1819
1997
|
appendFileSync(d3kLogFile, logEntry);
|
|
1820
1998
|
}
|
|
1821
1999
|
catch {
|
|
@@ -1881,7 +2059,7 @@ export class DevEnvironment {
|
|
|
1881
2059
|
}
|
|
1882
2060
|
// Always write session info after CDP monitoring starts - this is critical for
|
|
1883
2061
|
// sandbox environments where external tools poll for the cdpUrl in the session file
|
|
1884
|
-
writeSessionInfo(projectName, this.options.logFile, this.options.port, this.options.mcpPort, cdpUrl || undefined, chromePids, this.options.serverCommand);
|
|
2062
|
+
writeSessionInfo(projectName, this.options.logFile, this.options.port, this.options.mcpPort, cdpUrl || undefined, chromePids, this.options.serverCommand, this.options.framework, this.serverProcess?.pid);
|
|
1885
2063
|
this.debugLog(`Updated session info with CDP URL: ${cdpUrl}, Chrome PIDs: [${chromePids.join(", ")}]`);
|
|
1886
2064
|
this.logger.log("browser", `[CDP] Session info written with cdpUrl: ${cdpUrl ? "available" : "null"}`);
|
|
1887
2065
|
// Navigate to the app
|
|
@@ -1905,10 +2083,12 @@ export class DevEnvironment {
|
|
|
1905
2083
|
await this.screencastManager.stop();
|
|
1906
2084
|
this.screencastManager = null;
|
|
1907
2085
|
}
|
|
2086
|
+
// Read server PID from session file BEFORE deleting it (needed for cleanup)
|
|
2087
|
+
const projectName = getProjectName();
|
|
2088
|
+
const savedServerPid = getSessionServerPid(projectName);
|
|
1908
2089
|
// Clean up session file
|
|
1909
2090
|
try {
|
|
1910
|
-
const
|
|
1911
|
-
const sessionFile = join(homedir(), ".d3k", `${projectName}.json`);
|
|
2091
|
+
const sessionFile = join(homedir(), ".d3k", projectName, "session.json");
|
|
1912
2092
|
if (existsSync(sessionFile)) {
|
|
1913
2093
|
unlinkSync(sessionFile);
|
|
1914
2094
|
}
|
|
@@ -1954,6 +2134,24 @@ export class DevEnvironment {
|
|
|
1954
2134
|
// Kill app server only (MCP server remains as singleton)
|
|
1955
2135
|
console.log(chalk.cyan("🔄 Killing app server..."));
|
|
1956
2136
|
await killPortProcess(this.options.port, "your app server");
|
|
2137
|
+
// Kill server process and its children using the saved PID (from before session file was deleted)
|
|
2138
|
+
if (!isInSandbox() && savedServerPid) {
|
|
2139
|
+
try {
|
|
2140
|
+
const { spawnSync } = await import("child_process");
|
|
2141
|
+
// Kill all child processes of the server
|
|
2142
|
+
spawnSync("pkill", ["-P", savedServerPid.toString()], { stdio: "ignore" });
|
|
2143
|
+
// Kill the server process itself
|
|
2144
|
+
try {
|
|
2145
|
+
process.kill(savedServerPid, "SIGKILL");
|
|
2146
|
+
}
|
|
2147
|
+
catch {
|
|
2148
|
+
// Process may already be dead
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
catch {
|
|
2152
|
+
// Ignore pkill errors
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
1957
2155
|
// Shutdown CDP monitor if it was started
|
|
1958
2156
|
if (this.cdpMonitor) {
|
|
1959
2157
|
try {
|
|
@@ -1966,8 +2164,10 @@ export class DevEnvironment {
|
|
|
1966
2164
|
}
|
|
1967
2165
|
}
|
|
1968
2166
|
console.log(chalk.red(`❌ ${this.options.commandName} exited due to server failure`));
|
|
1969
|
-
|
|
1970
|
-
|
|
2167
|
+
// Show recent log entries to help diagnose the issue
|
|
2168
|
+
this.showRecentLogs();
|
|
2169
|
+
// Check for common issues and provide specific guidance
|
|
2170
|
+
this.checkForCommonIssues();
|
|
1971
2171
|
process.exit(1);
|
|
1972
2172
|
}
|
|
1973
2173
|
setupCleanupHandlers() {
|
|
@@ -2038,16 +2238,32 @@ export class DevEnvironment {
|
|
|
2038
2238
|
process.exit(1);
|
|
2039
2239
|
});
|
|
2040
2240
|
});
|
|
2241
|
+
// Handle SIGHUP (sent by tmux when session/pane is killed)
|
|
2242
|
+
process.on("SIGHUP", () => {
|
|
2243
|
+
this.debugLog("SIGHUP received (tmux session closing)");
|
|
2244
|
+
if (this.isShuttingDown)
|
|
2245
|
+
return;
|
|
2246
|
+
this.isShuttingDown = true;
|
|
2247
|
+
this.handleShutdown()
|
|
2248
|
+
.then(() => {
|
|
2249
|
+
process.exit(0);
|
|
2250
|
+
})
|
|
2251
|
+
.catch(() => {
|
|
2252
|
+
process.exit(1);
|
|
2253
|
+
});
|
|
2254
|
+
});
|
|
2041
2255
|
}
|
|
2042
2256
|
async handleShutdown() {
|
|
2043
2257
|
// Stop health monitoring
|
|
2044
2258
|
this.stopHealthCheck();
|
|
2045
2259
|
// Release the lock file
|
|
2046
2260
|
this.releaseLock();
|
|
2261
|
+
// Read server PID from session file BEFORE deleting it (needed for cleanup)
|
|
2262
|
+
const projectName = getProjectName();
|
|
2263
|
+
const savedServerPid = getSessionServerPid(projectName);
|
|
2047
2264
|
// Clean up session file
|
|
2048
2265
|
try {
|
|
2049
|
-
const
|
|
2050
|
-
const sessionFile = join(homedir(), ".d3k", `${projectName}.json`);
|
|
2266
|
+
const sessionFile = join(homedir(), ".d3k", projectName, "session.json");
|
|
2051
2267
|
if (existsSync(sessionFile)) {
|
|
2052
2268
|
unlinkSync(sessionFile);
|
|
2053
2269
|
}
|
|
@@ -2176,12 +2392,15 @@ export class DevEnvironment {
|
|
|
2176
2392
|
// Next.js's next-server and webpack workers) are also killed.
|
|
2177
2393
|
if (this.serverProcess?.pid) {
|
|
2178
2394
|
try {
|
|
2179
|
-
//
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2395
|
+
// Use graceful kill: SIGTERM first, wait, then SIGKILL if needed
|
|
2396
|
+
// This allows Next.js to clean up .next/dev/lock before terminating
|
|
2397
|
+
const result = await gracefulKillProcess({
|
|
2398
|
+
pid: this.serverProcess.pid,
|
|
2399
|
+
debugLog: (msg) => this.debugLog(msg)
|
|
2400
|
+
});
|
|
2401
|
+
if (!this.options.tui && result.terminated) {
|
|
2402
|
+
const method = result.graceful ? "gracefully" : "forcefully";
|
|
2403
|
+
console.log(chalk.green(`✅ Killed server process group ${method} (PID: ${this.serverProcess.pid})`));
|
|
2185
2404
|
}
|
|
2186
2405
|
}
|
|
2187
2406
|
catch (error) {
|
|
@@ -2195,9 +2414,24 @@ export class DevEnvironment {
|
|
|
2195
2414
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2196
2415
|
// Double-check: try to kill any remaining processes on the app port
|
|
2197
2416
|
try {
|
|
2198
|
-
const {
|
|
2199
|
-
|
|
2417
|
+
const { spawnSync } = await import("child_process");
|
|
2418
|
+
// Kill by port
|
|
2419
|
+
spawnSync("sh", ["-c", `pkill -f ":${this.options.port}"`], { stdio: "ignore" });
|
|
2200
2420
|
this.debugLog(`Sent pkill signal for port ${this.options.port}`);
|
|
2421
|
+
// Kill server process and its children using the saved PID (from before session file was deleted)
|
|
2422
|
+
if (savedServerPid) {
|
|
2423
|
+
// Kill all child processes of the server
|
|
2424
|
+
spawnSync("pkill", ["-P", savedServerPid.toString()], { stdio: "ignore" });
|
|
2425
|
+
this.debugLog(`Killed children of server PID ${savedServerPid}`);
|
|
2426
|
+
// Kill the server process itself
|
|
2427
|
+
try {
|
|
2428
|
+
process.kill(savedServerPid, "SIGKILL");
|
|
2429
|
+
this.debugLog(`Killed server PID ${savedServerPid}`);
|
|
2430
|
+
}
|
|
2431
|
+
catch {
|
|
2432
|
+
// Process may already be dead
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2201
2435
|
}
|
|
2202
2436
|
catch {
|
|
2203
2437
|
// Ignore pkill errors
|