dev3000 0.0.131 → 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 (106) hide show
  1. package/bin/dev3000 +90 -0
  2. package/dist/cli.js +202 -33
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/cloud-fix.js +8 -8
  5. package/dist/commands/cloud-fix.js.map +1 -1
  6. package/dist/components/AgentSelector.d.ts +12 -0
  7. package/dist/components/AgentSelector.d.ts.map +1 -0
  8. package/dist/components/AgentSelector.js +99 -0
  9. package/dist/components/AgentSelector.js.map +1 -0
  10. package/dist/components/SkillSelector.d.ts +9 -0
  11. package/dist/components/SkillSelector.d.ts.map +1 -0
  12. package/dist/components/SkillSelector.js +70 -0
  13. package/dist/components/SkillSelector.js.map +1 -0
  14. package/dist/dev-environment.d.ts +37 -0
  15. package/dist/dev-environment.d.ts.map +1 -1
  16. package/dist/dev-environment.js +245 -106
  17. package/dist/dev-environment.js.map +1 -1
  18. package/dist/src/tui-interface-impl.tsx +32 -20
  19. package/dist/tui-interface-impl.d.ts +10 -6
  20. package/dist/tui-interface-impl.d.ts.map +1 -1
  21. package/dist/tui-interface-impl.js +20 -14
  22. package/dist/tui-interface-impl.js.map +1 -1
  23. package/dist/tui-interface.d.ts +11 -7
  24. package/dist/tui-interface.d.ts.map +1 -1
  25. package/dist/tui-interface.js +6 -6
  26. package/dist/tui-interface.js.map +1 -1
  27. package/dist/utils/agent-selection.d.ts +24 -0
  28. package/dist/utils/agent-selection.d.ts.map +1 -0
  29. package/dist/utils/agent-selection.js +34 -0
  30. package/dist/utils/agent-selection.js.map +1 -0
  31. package/dist/utils/project-name.d.ts +6 -0
  32. package/dist/utils/project-name.d.ts.map +1 -1
  33. package/dist/utils/project-name.js +10 -0
  34. package/dist/utils/project-name.js.map +1 -1
  35. package/dist/utils/skill-installer.d.ts +29 -0
  36. package/dist/utils/skill-installer.d.ts.map +1 -0
  37. package/dist/utils/skill-installer.js +185 -0
  38. package/dist/utils/skill-installer.js.map +1 -0
  39. package/dist/utils/tmux-helpers.d.ts.map +1 -1
  40. package/dist/utils/tmux-helpers.js +4 -2
  41. package/dist/utils/tmux-helpers.js.map +1 -1
  42. package/dist/utils/user-config.d.ts +6 -0
  43. package/dist/utils/user-config.d.ts.map +1 -1
  44. package/dist/utils/user-config.js +30 -5
  45. package/dist/utils/user-config.js.map +1 -1
  46. package/dist/utils/version-check.d.ts +10 -1
  47. package/dist/utils/version-check.d.ts.map +1 -1
  48. package/dist/utils/version-check.js +60 -2
  49. package/dist/utils/version-check.js.map +1 -1
  50. package/mcp-server/.next/BUILD_ID +1 -1
  51. package/mcp-server/.next/build-manifest.json +2 -2
  52. package/mcp-server/.next/fallback-build-manifest.json +2 -2
  53. package/mcp-server/.next/next-minimal-server.js.nft.json +1 -1
  54. package/mcp-server/.next/next-server.js.nft.json +1 -1
  55. package/mcp-server/.next/prerender-manifest.json +3 -3
  56. package/mcp-server/.next/server/app/_global-error/page.js.nft.json +1 -1
  57. package/mcp-server/.next/server/app/_global-error.html +2 -2
  58. package/mcp-server/.next/server/app/_global-error.rsc +1 -1
  59. package/mcp-server/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  60. package/mcp-server/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  61. package/mcp-server/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  62. package/mcp-server/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  63. package/mcp-server/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  64. package/mcp-server/.next/server/app/_not-found/page.js.nft.json +1 -1
  65. package/mcp-server/.next/server/app/_not-found.html +1 -1
  66. package/mcp-server/.next/server/app/_not-found.rsc +1 -1
  67. package/mcp-server/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  68. package/mcp-server/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  69. package/mcp-server/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  70. package/mcp-server/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  71. package/mcp-server/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  72. package/mcp-server/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  73. package/mcp-server/.next/server/app/api/jank/[session]/route.js.nft.json +1 -1
  74. package/mcp-server/.next/server/app/api/logs/append/route.js.nft.json +1 -1
  75. package/mcp-server/.next/server/app/api/logs/head/route.js.nft.json +1 -1
  76. package/mcp-server/.next/server/app/api/logs/list/route.js.nft.json +1 -1
  77. package/mcp-server/.next/server/app/api/logs/rotate/route.js.nft.json +1 -1
  78. package/mcp-server/.next/server/app/api/logs/stream/route.js.nft.json +1 -1
  79. package/mcp-server/.next/server/app/api/logs/tail/route.js.nft.json +1 -1
  80. package/mcp-server/.next/server/app/api/orchestrator/route.js.nft.json +1 -1
  81. package/mcp-server/.next/server/app/api/screenshots/[filename]/route.js.nft.json +1 -1
  82. package/mcp-server/.next/server/app/api/screenshots/capture/route.js.nft.json +1 -1
  83. package/mcp-server/.next/server/app/api/screenshots/clear/route.js.nft.json +1 -1
  84. package/mcp-server/.next/server/app/api/screenshots/list/route.js.nft.json +1 -1
  85. package/mcp-server/.next/server/app/api/teams/route.js.nft.json +1 -1
  86. package/mcp-server/.next/server/app/api/tools/route.js.nft.json +1 -1
  87. package/mcp-server/.next/server/app/index.html +1 -1
  88. package/mcp-server/.next/server/app/index.rsc +1 -1
  89. package/mcp-server/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  90. package/mcp-server/.next/server/app/index.segments/_full.segment.rsc +1 -1
  91. package/mcp-server/.next/server/app/index.segments/_head.segment.rsc +1 -1
  92. package/mcp-server/.next/server/app/index.segments/_index.segment.rsc +1 -1
  93. package/mcp-server/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  94. package/mcp-server/.next/server/app/logs/page.js.nft.json +1 -1
  95. package/mcp-server/.next/server/app/mcp/route.js.nft.json +1 -1
  96. package/mcp-server/.next/server/app/page.js.nft.json +1 -1
  97. package/mcp-server/.next/server/app/video/[session]/page.js.nft.json +1 -1
  98. package/mcp-server/.next/server/pages/404.html +1 -1
  99. package/mcp-server/.next/server/pages/500.html +2 -2
  100. package/mcp-server/.next/server/server-reference-manifest.js +1 -1
  101. package/mcp-server/.next/server/server-reference-manifest.json +1 -1
  102. package/package.json +11 -4
  103. package/src/tui-interface-impl.tsx +32 -20
  104. /package/mcp-server/.next/static/{i5SOhj7cf3JSMPksTBNpr → Fekb6bDxTd8hH8dFQNlIK}/_buildManifest.js +0 -0
  105. /package/mcp-server/.next/static/{i5SOhj7cf3JSMPksTBNpr → Fekb6bDxTd8hH8dFQNlIK}/_clientMiddlewareManifest.json +0 -0
  106. /package/mcp-server/.next/static/{i5SOhj7cf3JSMPksTBNpr → 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 });
@@ -540,11 +617,11 @@ export function createPersistentLogFile() {
540
617
  }
541
618
  // Write session info for MCP server to discover
542
619
  function writeSessionInfo(projectName, logFilePath, appPort, mcpPort, cdpUrl, chromePids, serverCommand, framework, serverPid) {
543
- const sessionDir = join(homedir(), ".d3k");
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 = {
@@ -561,8 +638,8 @@ function writeSessionInfo(projectName, logFilePath, appPort, mcpPort, cdpUrl, ch
561
638
  framework: framework || null,
562
639
  serverPid: serverPid || null
563
640
  };
564
- // Write session file - use project name as filename for easy lookup
565
- const sessionFile = join(sessionDir, `${projectName}.json`);
641
+ // Write session file in project directory
642
+ const sessionFile = join(projectDir, "session.json");
566
643
  writeFileSync(sessionFile, JSON.stringify(sessionInfo, null, 2));
567
644
  }
568
645
  catch (error) {
@@ -572,8 +649,7 @@ function writeSessionInfo(projectName, logFilePath, appPort, mcpPort, cdpUrl, ch
572
649
  }
573
650
  // Get Chrome PIDs for this instance
574
651
  function getSessionChromePids(projectName) {
575
- const sessionDir = join(homedir(), ".d3k");
576
- const sessionFile = join(sessionDir, `${projectName}.json`);
652
+ const sessionFile = join(homedir(), ".d3k", projectName, "session.json");
577
653
  try {
578
654
  if (existsSync(sessionFile)) {
579
655
  const sessionInfo = JSON.parse(readFileSync(sessionFile, "utf8"));
@@ -587,8 +663,7 @@ function getSessionChromePids(projectName) {
587
663
  }
588
664
  // Get server PID for this instance
589
665
  function getSessionServerPid(projectName) {
590
- const sessionDir = join(homedir(), ".d3k");
591
- const sessionFile = join(sessionDir, `${projectName}.json`);
666
+ const sessionFile = join(homedir(), ".d3k", projectName, "session.json");
592
667
  try {
593
668
  if (existsSync(sessionFile)) {
594
669
  const sessionInfo = JSON.parse(readFileSync(sessionFile, "utf8"));
@@ -600,23 +675,31 @@ function getSessionServerPid(projectName) {
600
675
  }
601
676
  return null;
602
677
  }
603
- function createLogFileInDir(baseDir, projectName) {
604
- // Create timestamp
605
- const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
606
- // Create log file path
607
- const logFileName = `${projectName}-${timestamp}.log`;
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`;
608
691
  const logFilePath = join(baseDir, logFileName);
609
- // Prune old logs for this project (keep only 10 most recent)
610
- pruneOldLogs(baseDir, projectName);
692
+ // Prune old logs (keep only 10 most recent)
693
+ pruneOldLogs(baseDir);
611
694
  // Create the log file
612
695
  writeFileSync(logFilePath, "");
613
696
  return logFilePath;
614
697
  }
615
- function pruneOldLogs(baseDir, projectName) {
698
+ function pruneOldLogs(baseDir) {
616
699
  try {
617
- // Find all log files for this project
700
+ // Find all log files in directory
618
701
  const files = readdirSync(baseDir)
619
- .filter((file) => file.startsWith(`${projectName}-`) && file.endsWith(".log"))
702
+ .filter((file) => file.endsWith(".log"))
620
703
  .map((file) => ({
621
704
  name: file,
622
705
  path: join(baseDir, file),
@@ -636,8 +719,8 @@ function pruneOldLogs(baseDir, projectName) {
636
719
  }
637
720
  }
638
721
  }
639
- catch (error) {
640
- console.warn(chalk.yellow(`⚠️ Could not prune logs: ${error}`));
722
+ catch (_error) {
723
+ // Silently ignore prune errors
641
724
  }
642
725
  }
643
726
  export class DevEnvironment {
@@ -673,9 +756,20 @@ export class DevEnvironment {
673
756
  this.disabledMcpConfigSet = new Set(this.options.disabledMcpConfigs);
674
757
  this.logger = new Logger(options.logFile, options.tail || false, options.dateTimeFormat || "local");
675
758
  this.outputProcessor = new OutputProcessor(new StandardLogParser(), new NextJsErrorDetector());
676
- // Set up MCP server public directory for web-accessible screenshots
677
- const currentFile = fileURLToPath(import.meta.url);
678
- 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
+ }
679
773
  // Always use MCP server's public directory for screenshots to ensure they're web-accessible
680
774
  // and avoid permission issues with /var/log paths
681
775
  this.screenshotDir = join(packageRoot, "mcp-server", "public", "screenshots");
@@ -684,30 +778,36 @@ export class DevEnvironment {
684
778
  this.pidFile = join(tmpdir(), `dev3000-${projectName}.pid`);
685
779
  this.lockFile = join(tmpdir(), `dev3000-${projectName}.lock`);
686
780
  this.mcpPublicDir = join(packageRoot, "mcp-server", "public", "screenshots");
687
- // Read version from package.json for startup message
781
+ // Read version - for compiled binaries, use injected version; otherwise read from package.json
688
782
  this.version = "0.0.0";
689
- try {
690
- const packageJsonPath = join(packageRoot, "package.json");
691
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
692
- this.version = packageJson.version;
693
- // 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 {
694
788
  try {
695
- const { execSync } = require("child_process");
696
- const gitRemote = execSync("git remote get-url origin 2>/dev/null", {
697
- cwd: packageRoot,
698
- encoding: "utf8"
699
- }).trim();
700
- if (gitRemote.includes("vercel-labs/dev3000") && !this.version.includes("canary")) {
701
- 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
702
805
  }
703
806
  }
704
- catch {
705
- // Not in git repo or no git - use version as-is
807
+ catch (_error) {
808
+ // Use fallback version
706
809
  }
707
810
  }
708
- catch (_error) {
709
- // Use fallback version
710
- }
711
811
  // Initialize spinner for clean output management (only if not in TUI mode)
712
812
  this.spinner = ora({
713
813
  text: "Initializing...",
@@ -825,7 +925,7 @@ export class DevEnvironment {
825
925
  this.debugLog(`Waiting ${waitTime}ms for port ${this.options.mcpPort} to be released...`);
826
926
  await new Promise((resolve) => setTimeout(resolve, waitTime));
827
927
  // Check if port is now free
828
- const available = await isPortAvailable(this.options.mcpPort.toString());
928
+ const available = await isPortAvailable(this.options.mcpPort?.toString() ?? "");
829
929
  if (available) {
830
930
  this.debugLog(`Port ${this.options.mcpPort} released successfully`);
831
931
  return;
@@ -945,14 +1045,26 @@ export class DevEnvironment {
945
1045
  serversOnly: this.options.serversOnly,
946
1046
  version: this.version,
947
1047
  projectName: projectDisplayName,
948
- updateAvailable: null // Will be updated async
1048
+ updateInfo: null // Will be updated async after auto-upgrade
949
1049
  });
950
1050
  await this.tui.start();
951
- // Update TUI with version check result (non-blocking)
952
- updateCheckPromise.then((versionInfo) => {
1051
+ // Auto-upgrade if update available (non-blocking)
1052
+ updateCheckPromise.then(async (versionInfo) => {
953
1053
  if (versionInfo?.updateAvailable && versionInfo.latestVersion && this.tui) {
954
- this.debugLog(`Update available: ${versionInfo.currentVersion} -> ${versionInfo.latestVersion}`);
955
- 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
+ }
956
1068
  }
957
1069
  });
958
1070
  // Check ports in background after TUI is visible
@@ -989,7 +1101,7 @@ export class DevEnvironment {
989
1101
  if (!serverStarted) {
990
1102
  await this.tui.updateStatus("❌ Server failed to start");
991
1103
  console.error(chalk.red("\n❌ Your app server failed to start after 30 seconds."));
992
- 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.`));
993
1105
  console.error(chalk.yellow("Exiting without launching browser."));
994
1106
  process.exit(1);
995
1107
  }
@@ -1052,7 +1164,7 @@ export class DevEnvironment {
1052
1164
  if (!serverStarted) {
1053
1165
  this.spinner.fail("Server failed to start");
1054
1166
  console.error(chalk.red("\n❌ Your app server failed to start after 30 seconds."));
1055
- 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.`));
1056
1168
  console.error(chalk.yellow("Exiting without launching browser."));
1057
1169
  process.exit(1);
1058
1170
  }
@@ -1100,12 +1212,21 @@ export class DevEnvironment {
1100
1212
  console.log(chalk.cyan("🖥️ Servers-only mode - use Chrome extension for browser monitoring"));
1101
1213
  }
1102
1214
  console.log(chalk.cyan("\nUse Ctrl-C to stop.\n"));
1103
- // Check for updates in non-TUI mode (non-blocking)
1215
+ // Auto-upgrade in non-TUI mode (non-blocking)
1104
1216
  checkForUpdates()
1105
- .then((versionInfo) => {
1217
+ .then(async (versionInfo) => {
1106
1218
  if (versionInfo?.updateAvailable && versionInfo.latestVersion) {
1107
- console.log(chalk.yellow(`\n↑ Update available: v${versionInfo.currentVersion} v${versionInfo.latestVersion}`));
1108
- 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
+ }
1109
1230
  }
1110
1231
  })
1111
1232
  .catch(() => {
@@ -1331,11 +1452,11 @@ export class DevEnvironment {
1331
1452
  }
1332
1453
  // Always write to d3k debug log file (even when not in debug mode)
1333
1454
  try {
1334
- const debugLogDir = join(homedir(), ".d3k");
1335
- if (!existsSync(debugLogDir)) {
1336
- mkdirSync(debugLogDir, { recursive: true });
1455
+ const projectDir = getProjectDir();
1456
+ if (!existsSync(projectDir)) {
1457
+ mkdirSync(projectDir, { recursive: true });
1337
1458
  }
1338
- const debugLogFile = join(debugLogDir, "d3k.log");
1459
+ const debugLogFile = join(projectDir, "debug.log");
1339
1460
  const logEntry = `[${timestamp}] [DEBUG] ${message}\n`;
1340
1461
  appendFileSync(debugLogFile, logEntry);
1341
1462
  }
@@ -1399,9 +1520,27 @@ export class DevEnvironment {
1399
1520
  // Note: MCP server cleanup now happens earlier in checkPortsAvailable()
1400
1521
  // to ensure the port is free before we check availability
1401
1522
  // Get the path to our bundled MCP server
1402
- const currentFile = fileURLToPath(import.meta.url);
1403
- const packageRoot = dirname(dirname(currentFile)); // Go up from dist/ to package root
1404
- 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
+ }
1405
1544
  this.debugLog(`Initial MCP server path: ${mcpServerPath}`);
1406
1545
  // For pnpm global installs, resolve symlinks to get the real path
1407
1546
  if (existsSync(mcpServerPath)) {
@@ -1832,13 +1971,12 @@ export class DevEnvironment {
1832
1971
  }
1833
1972
  initializeD3KLog() {
1834
1973
  try {
1835
- const debugLogDir = join(homedir(), ".d3k", "logs");
1836
- if (!existsSync(debugLogDir)) {
1837
- mkdirSync(debugLogDir, { recursive: true });
1974
+ const projectDir = getProjectDir();
1975
+ if (!existsSync(projectDir)) {
1976
+ mkdirSync(projectDir, { recursive: true });
1838
1977
  }
1839
- // Create project-specific D3K log file and clear it for new session
1840
- const projectName = getProjectName();
1841
- 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");
1842
1980
  writeFileSync(d3kLogFile, "");
1843
1981
  }
1844
1982
  catch {
@@ -1851,13 +1989,11 @@ export class DevEnvironment {
1851
1989
  const timestamp = formatTimestamp(new Date(), this.options.dateTimeFormat || "local");
1852
1990
  const logEntry = `[${timestamp}] [D3K] ${message}\n`;
1853
1991
  try {
1854
- const debugLogDir = join(homedir(), ".d3k", "logs");
1855
- if (!existsSync(debugLogDir)) {
1856
- mkdirSync(debugLogDir, { recursive: true });
1992
+ const projectDir = getProjectDir();
1993
+ if (!existsSync(projectDir)) {
1994
+ mkdirSync(projectDir, { recursive: true });
1857
1995
  }
1858
- // Create project-specific D3K log file to avoid confusion between multiple instances
1859
- const projectName = getProjectName();
1860
- const d3kLogFile = join(debugLogDir, `${projectName}-d3k.log`);
1996
+ const d3kLogFile = join(projectDir, "d3k.log");
1861
1997
  appendFileSync(d3kLogFile, logEntry);
1862
1998
  }
1863
1999
  catch {
@@ -1952,7 +2088,7 @@ export class DevEnvironment {
1952
2088
  const savedServerPid = getSessionServerPid(projectName);
1953
2089
  // Clean up session file
1954
2090
  try {
1955
- const sessionFile = join(homedir(), ".d3k", `${projectName}.json`);
2091
+ const sessionFile = join(homedir(), ".d3k", projectName, "session.json");
1956
2092
  if (existsSync(sessionFile)) {
1957
2093
  unlinkSync(sessionFile);
1958
2094
  }
@@ -2127,7 +2263,7 @@ export class DevEnvironment {
2127
2263
  const savedServerPid = getSessionServerPid(projectName);
2128
2264
  // Clean up session file
2129
2265
  try {
2130
- const sessionFile = join(homedir(), ".d3k", `${projectName}.json`);
2266
+ const sessionFile = join(homedir(), ".d3k", projectName, "session.json");
2131
2267
  if (existsSync(sessionFile)) {
2132
2268
  unlinkSync(sessionFile);
2133
2269
  }
@@ -2256,12 +2392,15 @@ export class DevEnvironment {
2256
2392
  // Next.js's next-server and webpack workers) are also killed.
2257
2393
  if (this.serverProcess?.pid) {
2258
2394
  try {
2259
- // Negative PID kills the entire process group
2260
- const pgid = -this.serverProcess.pid;
2261
- this.debugLog(`Killing process group ${pgid} (server PID: ${this.serverProcess.pid})`);
2262
- process.kill(pgid, "SIGKILL");
2263
- if (!this.options.tui) {
2264
- 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})`));
2265
2404
  }
2266
2405
  }
2267
2406
  catch (error) {