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.
Files changed (110) hide show
  1. package/bin/dev3000 +90 -0
  2. package/dist/cli.d.ts +1 -1
  3. package/dist/cli.js +216 -51
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/cloud-fix.js +8 -8
  6. package/dist/commands/cloud-fix.js.map +1 -1
  7. package/dist/components/AgentSelector.d.ts +12 -0
  8. package/dist/components/AgentSelector.d.ts.map +1 -0
  9. package/dist/components/AgentSelector.js +99 -0
  10. package/dist/components/AgentSelector.js.map +1 -0
  11. package/dist/components/SkillSelector.d.ts +9 -0
  12. package/dist/components/SkillSelector.d.ts.map +1 -0
  13. package/dist/components/SkillSelector.js +70 -0
  14. package/dist/components/SkillSelector.js.map +1 -0
  15. package/dist/dev-environment.d.ts +38 -0
  16. package/dist/dev-environment.d.ts.map +1 -1
  17. package/dist/dev-environment.js +350 -116
  18. package/dist/dev-environment.js.map +1 -1
  19. package/dist/src/tui-interface-impl.tsx +36 -22
  20. package/dist/tui-interface-impl.d.ts +10 -6
  21. package/dist/tui-interface-impl.d.ts.map +1 -1
  22. package/dist/tui-interface-impl.js +24 -16
  23. package/dist/tui-interface-impl.js.map +1 -1
  24. package/dist/tui-interface.d.ts +11 -7
  25. package/dist/tui-interface.d.ts.map +1 -1
  26. package/dist/tui-interface.js +6 -6
  27. package/dist/tui-interface.js.map +1 -1
  28. package/dist/utils/agent-selection.d.ts +24 -0
  29. package/dist/utils/agent-selection.d.ts.map +1 -0
  30. package/dist/utils/agent-selection.js +34 -0
  31. package/dist/utils/agent-selection.js.map +1 -0
  32. package/dist/utils/project-name.d.ts +6 -0
  33. package/dist/utils/project-name.d.ts.map +1 -1
  34. package/dist/utils/project-name.js +10 -0
  35. package/dist/utils/project-name.js.map +1 -1
  36. package/dist/utils/skill-installer.d.ts +29 -0
  37. package/dist/utils/skill-installer.d.ts.map +1 -0
  38. package/dist/utils/skill-installer.js +185 -0
  39. package/dist/utils/skill-installer.js.map +1 -0
  40. package/dist/utils/tmux-helpers.d.ts +35 -0
  41. package/dist/utils/tmux-helpers.d.ts.map +1 -0
  42. package/dist/utils/tmux-helpers.js +62 -0
  43. package/dist/utils/tmux-helpers.js.map +1 -0
  44. package/dist/utils/user-config.d.ts +6 -0
  45. package/dist/utils/user-config.d.ts.map +1 -1
  46. package/dist/utils/user-config.js +30 -5
  47. package/dist/utils/user-config.js.map +1 -1
  48. package/dist/utils/version-check.d.ts +10 -1
  49. package/dist/utils/version-check.d.ts.map +1 -1
  50. package/dist/utils/version-check.js +60 -2
  51. package/dist/utils/version-check.js.map +1 -1
  52. package/mcp-server/.next/BUILD_ID +1 -1
  53. package/mcp-server/.next/build-manifest.json +2 -2
  54. package/mcp-server/.next/fallback-build-manifest.json +2 -2
  55. package/mcp-server/.next/next-minimal-server.js.nft.json +1 -1
  56. package/mcp-server/.next/next-server.js.nft.json +1 -1
  57. package/mcp-server/.next/prerender-manifest.json +3 -3
  58. package/mcp-server/.next/server/app/_global-error/page.js.nft.json +1 -1
  59. package/mcp-server/.next/server/app/_global-error.html +2 -2
  60. package/mcp-server/.next/server/app/_global-error.rsc +1 -1
  61. package/mcp-server/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  62. package/mcp-server/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  63. package/mcp-server/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  64. package/mcp-server/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  65. package/mcp-server/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  66. package/mcp-server/.next/server/app/_not-found/page.js.nft.json +1 -1
  67. package/mcp-server/.next/server/app/_not-found.html +1 -1
  68. package/mcp-server/.next/server/app/_not-found.rsc +1 -1
  69. package/mcp-server/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  70. package/mcp-server/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  71. package/mcp-server/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  72. package/mcp-server/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  73. package/mcp-server/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  74. package/mcp-server/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  75. package/mcp-server/.next/server/app/api/jank/[session]/route.js.nft.json +1 -1
  76. package/mcp-server/.next/server/app/api/logs/append/route.js.nft.json +1 -1
  77. package/mcp-server/.next/server/app/api/logs/head/route.js.nft.json +1 -1
  78. package/mcp-server/.next/server/app/api/logs/list/route.js.nft.json +1 -1
  79. package/mcp-server/.next/server/app/api/logs/rotate/route.js.nft.json +1 -1
  80. package/mcp-server/.next/server/app/api/logs/stream/route.js.nft.json +1 -1
  81. package/mcp-server/.next/server/app/api/logs/tail/route.js.nft.json +1 -1
  82. package/mcp-server/.next/server/app/api/orchestrator/route.js.nft.json +1 -1
  83. package/mcp-server/.next/server/app/api/screenshots/[filename]/route.js.nft.json +1 -1
  84. package/mcp-server/.next/server/app/api/screenshots/capture/route.js.nft.json +1 -1
  85. package/mcp-server/.next/server/app/api/screenshots/clear/route.js.nft.json +1 -1
  86. package/mcp-server/.next/server/app/api/screenshots/list/route.js.nft.json +1 -1
  87. package/mcp-server/.next/server/app/api/teams/route.js.nft.json +1 -1
  88. package/mcp-server/.next/server/app/api/tools/route.js.nft.json +1 -1
  89. package/mcp-server/.next/server/app/index.html +1 -1
  90. package/mcp-server/.next/server/app/index.rsc +1 -1
  91. package/mcp-server/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  92. package/mcp-server/.next/server/app/index.segments/_full.segment.rsc +1 -1
  93. package/mcp-server/.next/server/app/index.segments/_head.segment.rsc +1 -1
  94. package/mcp-server/.next/server/app/index.segments/_index.segment.rsc +1 -1
  95. package/mcp-server/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  96. package/mcp-server/.next/server/app/logs/page.js.nft.json +1 -1
  97. package/mcp-server/.next/server/app/mcp/route.js.nft.json +1 -1
  98. package/mcp-server/.next/server/app/page.js.nft.json +1 -1
  99. package/mcp-server/.next/server/app/video/[session]/page.js.nft.json +1 -1
  100. package/mcp-server/.next/server/chunks/[root-of-the-server]__141a5bc7._.js.map +1 -1
  101. package/mcp-server/.next/server/pages/404.html +1 -1
  102. package/mcp-server/.next/server/pages/500.html +2 -2
  103. package/mcp-server/.next/server/server-reference-manifest.js +1 -1
  104. package/mcp-server/.next/server/server-reference-manifest.json +1 -1
  105. package/mcp-server/app/mcp/route.ts +2 -0
  106. package/package.json +11 -4
  107. package/src/tui-interface-impl.tsx +36 -22
  108. /package/mcp-server/.next/static/{YTIno5QuIO6mgeAWJ8zng → Fekb6bDxTd8hH8dFQNlIK}/_buildManifest.js +0 -0
  109. /package/mcp-server/.next/static/{YTIno5QuIO6mgeAWJ8zng → Fekb6bDxTd8hH8dFQNlIK}/_clientMiddlewareManifest.json +0 -0
  110. /package/mcp-server/.next/static/{YTIno5QuIO6mgeAWJ8zng → Fekb6bDxTd8hH8dFQNlIK}/_ssgManifest.js +0 -0
@@ -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" and "type": "local" for stdio servers
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 added = false;
458
- // Add dev3000 MCP server - use npx with mcp-client to connect to HTTP server
459
- // NOTE: dev3000 now acts as an MCP orchestrator/gateway that internally
460
- // spawns and connects to chrome-devtools-mcp and next-devtools-mcp as stdio processes
461
- if (!settings.mcp[MCP_NAMES.DEV3000]) {
462
- settings.mcp[MCP_NAMES.DEV3000] = {
463
- type: "remote",
464
- url: `http://localhost:${mcpPort}/mcp`,
465
- enabled: true
466
- };
467
- added = true;
468
- }
469
- // Add Vercel MCP if this is a Vercel project (.vercel directory exists)
470
- // Vercel MCP uses OAuth authentication handled by the client (OpenCode)
471
- if (hasVercelProject() && !settings.mcp[MCP_NAMES.VERCEL]) {
472
- settings.mcp[MCP_NAMES.VERCEL] = {
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
- added = true;
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 added anything
480
- if (added) {
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(homedir(), ".d3k", "logs");
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 sessionDir = join(homedir(), ".d3k");
619
+ function writeSessionInfo(projectName, logFilePath, appPort, mcpPort, cdpUrl, chromePids, serverCommand, framework, serverPid) {
620
+ const projectDir = getProjectDir();
544
621
  try {
545
- // Create ~/.d3k directory if it doesn't exist
546
- if (!existsSync(sessionDir)) {
547
- mkdirSync(sessionDir, { recursive: true });
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 - use project name as filename for easy lookup
564
- const sessionFile = join(sessionDir, `${projectName}.json`);
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 sessionDir = join(homedir(), ".d3k");
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
- function createLogFileInDir(baseDir, projectName) {
588
- // Create timestamp
589
- const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
590
- // Create log file path
591
- const logFileName = `${projectName}-${timestamp}.log`;
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 for this project (keep only 10 most recent)
594
- pruneOldLogs(baseDir, projectName);
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, projectName) {
698
+ function pruneOldLogs(baseDir) {
600
699
  try {
601
- // Find all log files for this project
700
+ // Find all log files in directory
602
701
  const files = readdirSync(baseDir)
603
- .filter((file) => file.startsWith(`${projectName}-`) && file.endsWith(".log"))
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 (error) {
624
- console.warn(chalk.yellow(`⚠️ Could not prune logs: ${error}`));
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
- // Set up MCP server public directory for web-accessible screenshots
661
- const currentFile = fileURLToPath(import.meta.url);
662
- const packageRoot = dirname(dirname(currentFile));
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 for startup message
781
+ // Read version - for compiled binaries, use injected version; otherwise read from package.json
672
782
  this.version = "0.0.0";
673
- try {
674
- const packageJsonPath = join(packageRoot, "package.json");
675
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
676
- this.version = packageJson.version;
677
- // Use git to detect if we're in the dev3000 source repository
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 { execSync } = require("child_process");
680
- const gitRemote = execSync("git remote get-url origin 2>/dev/null", {
681
- cwd: packageRoot,
682
- encoding: "utf8"
683
- }).trim();
684
- if (gitRemote.includes("vercel-labs/dev3000") && !this.version.includes("canary")) {
685
- this.version += "-local";
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
- // Not in git repo or no git - use version as-is
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.toString());
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
- updateAvailable: null // Will be updated async
1048
+ updateInfo: null // Will be updated async after auto-upgrade
933
1049
  });
934
1050
  await this.tui.start();
935
- // Update TUI with version check result (non-blocking)
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
- this.tui.updateUpdateAvailable({ latestVersion: versionInfo.latestVersion });
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/logs/${getProjectName()}-d3k.log for errors.`));
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/logs/${getProjectName()}-d3k.log for errors.`));
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
- // Check for updates in non-TUI mode (non-blocking)
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
- console.log(chalk.yellow(`\n↑ Update available: v${versionInfo.currentVersion} v${versionInfo.latestVersion}`));
1092
- console.log(chalk.gray(` Run 'd3k upgrade' to update\n`));
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 debugLogDir = join(homedir(), ".d3k");
1319
- if (!existsSync(debugLogDir)) {
1320
- mkdirSync(debugLogDir, { recursive: true });
1455
+ const projectDir = getProjectDir();
1456
+ if (!existsSync(projectDir)) {
1457
+ mkdirSync(projectDir, { recursive: true });
1321
1458
  }
1322
- const debugLogFile = join(debugLogDir, "d3k.log");
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
- const currentFile = fileURLToPath(import.meta.url);
1361
- const packageRoot = dirname(dirname(currentFile)); // Go up from dist/ to package root
1362
- let mcpServerPath = join(packageRoot, "mcp-server");
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 debugLogDir = join(homedir(), ".d3k", "logs");
1794
- if (!existsSync(debugLogDir)) {
1795
- mkdirSync(debugLogDir, { recursive: true });
1974
+ const projectDir = getProjectDir();
1975
+ if (!existsSync(projectDir)) {
1976
+ mkdirSync(projectDir, { recursive: true });
1796
1977
  }
1797
- // Create project-specific D3K log file and clear it for new session
1798
- const projectName = getProjectName();
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 debugLogDir = join(homedir(), ".d3k", "logs");
1813
- if (!existsSync(debugLogDir)) {
1814
- mkdirSync(debugLogDir, { recursive: true });
1992
+ const projectDir = getProjectDir();
1993
+ if (!existsSync(projectDir)) {
1994
+ mkdirSync(projectDir, { recursive: true });
1815
1995
  }
1816
- // Create project-specific D3K log file to avoid confusion between multiple instances
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 projectName = getProjectName();
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
- const projectName = getProjectName();
1970
- console.log(chalk.yellow(`Check the logs at ~/.d3k/logs/${projectName}-d3k.log for errors. Feeling like helping? Run dev3000 --debug and file an issue at https://github.com/vercel-labs/dev3000/issues`));
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 projectName = getProjectName();
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
- // Negative PID kills the entire process group
2180
- const pgid = -this.serverProcess.pid;
2181
- this.debugLog(`Killing process group ${pgid} (server PID: ${this.serverProcess.pid})`);
2182
- process.kill(pgid, "SIGKILL");
2183
- if (!this.options.tui) {
2184
- console.log(chalk.green(`✅ Killed server process group (PID: ${this.serverProcess.pid})`));
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 { spawn } = await import("child_process");
2199
- spawn("sh", ["-c", `pkill -f ":${this.options.port}"`], { stdio: "ignore" });
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