zdev 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -475,11 +475,11 @@ var require_option = __commonJS((exports) => {
475
475
  }
476
476
 
477
477
  class DualOptions {
478
- constructor(options2) {
478
+ constructor(options) {
479
479
  this.positiveOptions = new Map;
480
480
  this.negativeOptions = new Map;
481
481
  this.dualOptions = new Set;
482
- options2.forEach((option) => {
482
+ options.forEach((option) => {
483
483
  if (option.negate) {
484
484
  this.negativeOptions.set(option.attributeName(), option);
485
485
  } else {
@@ -940,12 +940,12 @@ Expecting one of '${allowedValues.join("', '")}'`);
940
940
  }
941
941
  return this;
942
942
  }
943
- _optionEx(config2, flags, description, fn, defaultValue) {
943
+ _optionEx(config, flags, description, fn, defaultValue) {
944
944
  if (typeof flags === "object" && flags instanceof Option) {
945
945
  throw new Error("To add an Option object use addOption() instead of option() or requiredOption()");
946
946
  }
947
947
  const option = this.createOption(flags, description);
948
- option.makeOptionMandatory(!!config2.mandatory);
948
+ option.makeOptionMandatory(!!config.mandatory);
949
949
  if (typeof fn === "function") {
950
950
  option.default(defaultValue).argParser(fn);
951
951
  } else if (fn instanceof RegExp) {
@@ -1514,9 +1514,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
1514
1514
  `);
1515
1515
  this.outputHelp({ error: true });
1516
1516
  }
1517
- const config2 = errorOptions || {};
1518
- const exitCode = config2.exitCode || 1;
1519
- const code = config2.code || "commander.error";
1517
+ const config = errorOptions || {};
1518
+ const exitCode = config.exitCode || 1;
1519
+ const code = config.code || "commander.error";
1520
1520
  this._exit(exitCode, code, message);
1521
1521
  }
1522
1522
  _parseOptionsEnv() {
@@ -1929,32 +1929,32 @@ function loadConfig() {
1929
1929
  return DEFAULT_CONFIG;
1930
1930
  }
1931
1931
  }
1932
- function saveConfig(config2) {
1932
+ function saveConfig(config) {
1933
1933
  ensurezdevDirs();
1934
- writeFileSync(CONFIG_PATH, JSON.stringify(config2, null, 2));
1934
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
1935
1935
  }
1936
- function allocatePorts(config2, includeConvex = true) {
1937
- const frontend = config2.nextFrontendPort;
1938
- config2.nextFrontendPort = frontend + 1;
1936
+ function allocatePorts(config, includeConvex = true) {
1937
+ const frontend = config.nextFrontendPort;
1938
+ config.nextFrontendPort = frontend + 1;
1939
1939
  let convex = 0;
1940
1940
  if (includeConvex) {
1941
- convex = config2.nextConvexPort;
1942
- config2.nextConvexPort = convex + 1;
1941
+ convex = config.nextConvexPort;
1942
+ config.nextConvexPort = convex + 1;
1943
1943
  }
1944
1944
  return { frontend, convex };
1945
1945
  }
1946
1946
  function getWorktreePath(name) {
1947
1947
  return join(WORKTREES_DIR, name);
1948
1948
  }
1949
- function getSeedPath(projectName2) {
1950
- return join(SEEDS_DIR, `${projectName2}.zip`);
1949
+ function getSeedPath(projectName) {
1950
+ return join(SEEDS_DIR, `${projectName}.zip`);
1951
1951
  }
1952
1952
 
1953
1953
  // src/utils.ts
1954
- function run(command, args, options2) {
1954
+ function run(command, args, options) {
1955
1955
  const result = spawnSync(command, args, {
1956
1956
  encoding: "utf-8",
1957
- ...options2
1957
+ ...options
1958
1958
  });
1959
1959
  return {
1960
1960
  success: result.status === 0,
@@ -1963,11 +1963,11 @@ function run(command, args, options2) {
1963
1963
  code: result.status
1964
1964
  };
1965
1965
  }
1966
- function runBackground(command, args, options2) {
1966
+ function runBackground(command, args, options) {
1967
1967
  const child = spawn(command, args, {
1968
1968
  detached: true,
1969
1969
  stdio: "ignore",
1970
- ...options2
1970
+ ...options
1971
1971
  });
1972
1972
  child.unref();
1973
1973
  return child.pid;
@@ -1989,15 +1989,15 @@ function gitFetch(repoPath) {
1989
1989
  const result = run("git", ["fetch", "origin"], { cwd: repoPath });
1990
1990
  return result.success;
1991
1991
  }
1992
- function createWorktree(repoPath, worktreePath2, branch2, baseBranch = "origin/main") {
1993
- const result = run("git", ["worktree", "add", worktreePath2, "-b", branch2, baseBranch], { cwd: repoPath });
1992
+ function createWorktree(repoPath, worktreePath, branch, baseBranch = "origin/main") {
1993
+ const result = run("git", ["worktree", "add", worktreePath, "-b", branch, baseBranch], { cwd: repoPath });
1994
1994
  if (!result.success) {
1995
1995
  return { success: false, error: result.stderr };
1996
1996
  }
1997
1997
  return { success: true };
1998
1998
  }
1999
- function removeWorktree(repoPath, worktreePath2) {
2000
- const result = run("git", ["worktree", "remove", worktreePath2, "--force"], {
1999
+ function removeWorktree(repoPath, worktreePath) {
2000
+ const result = run("git", ["worktree", "remove", worktreePath, "--force"], {
2001
2001
  cwd: repoPath
2002
2002
  });
2003
2003
  if (!result.success) {
@@ -2007,14 +2007,14 @@ function removeWorktree(repoPath, worktreePath2) {
2007
2007
  return { success: true };
2008
2008
  }
2009
2009
  function traefikAddRoute(name, port) {
2010
- const config2 = loadConfig();
2011
- const configPath = `${config2.traefikConfigDir}/${name}.yml`;
2010
+ const config = loadConfig();
2011
+ const configPath = `${config.traefikConfigDir}/${name}.yml`;
2012
2012
  const subdomain = name;
2013
2013
  const traefikConfig = `# zdev auto-generated config for ${name}
2014
2014
  http:
2015
2015
  routers:
2016
2016
  ${name}:
2017
- rule: "Host(\`${subdomain}.${config2.devDomain}\`)"
2017
+ rule: "Host(\`${subdomain}.${config.devDomain}\`)"
2018
2018
  entrypoints:
2019
2019
  - websecure
2020
2020
  service: ${name}
@@ -2025,7 +2025,7 @@ http:
2025
2025
  ${name}:
2026
2026
  loadBalancer:
2027
2027
  servers:
2028
- - url: "http://${config2.dockerHostIp}:${port}"
2028
+ - url: "http://${config.dockerHostIp}:${port}"
2029
2029
  `;
2030
2030
  try {
2031
2031
  writeFileSync2(configPath, traefikConfig);
@@ -2047,15 +2047,15 @@ function traefikRemoveRoute(name) {
2047
2047
  }
2048
2048
  }
2049
2049
  function getTraefikStatus() {
2050
- const config2 = loadConfig();
2051
- if (!config2.devDomain) {
2050
+ const config = loadConfig();
2051
+ if (!config.devDomain) {
2052
2052
  return { running: false, devDomain: undefined };
2053
2053
  }
2054
- const configDirExists = existsSync2(config2.traefikConfigDir);
2054
+ const configDirExists = existsSync2(config.traefikConfigDir);
2055
2055
  return {
2056
2056
  running: configDirExists,
2057
- baseUrl: configDirExists ? `https://*.${config2.devDomain}` : undefined,
2058
- devDomain: config2.devDomain
2057
+ baseUrl: configDirExists ? `https://*.${config.devDomain}` : undefined,
2058
+ devDomain: config.devDomain
2059
2059
  };
2060
2060
  }
2061
2061
  function killProcess(pid) {
@@ -2238,22 +2238,22 @@ bun install
2238
2238
  # bunx prisma generate
2239
2239
  # cp ../.env.local .
2240
2240
  `;
2241
- async function create(projectName2, options2 = {}) {
2242
- const targetPath = resolve2(projectName2);
2241
+ async function create(projectName, options = {}) {
2242
+ const targetPath = resolve2(projectName);
2243
2243
  if (existsSync3(targetPath)) {
2244
2244
  console.error(`❌ Directory already exists: ${targetPath}`);
2245
2245
  process.exit(1);
2246
2246
  }
2247
- console.log(`\uD83D\uDC02 Creating new project: ${projectName2}`);
2248
- console.log(` Convex: ${options2.convex ? "yes" : "no"}`);
2249
- console.log(` Structure: ${options2.flat ? "flat" : "monorepo"}`);
2247
+ console.log(`\uD83D\uDC02 Creating new project: ${projectName}`);
2248
+ console.log(` Convex: ${options.convex ? "yes" : "no"}`);
2249
+ console.log(` Structure: ${options.flat ? "flat" : "monorepo"}`);
2250
2250
  console.log(`
2251
2251
  \uD83D\uDCE5 Cloning TanStack Start template...`);
2252
2252
  const cloneResult = run("npx", [
2253
2253
  "-y",
2254
2254
  "gitpick",
2255
2255
  "TanStack/router/tree/main/examples/react/start-basic",
2256
- projectName2
2256
+ projectName
2257
2257
  ]);
2258
2258
  if (!cloneResult.success) {
2259
2259
  console.error(`❌ Failed to clone template: ${cloneResult.stderr}`);
@@ -2261,7 +2261,7 @@ async function create(projectName2, options2 = {}) {
2261
2261
  }
2262
2262
  console.log(` Template cloned`);
2263
2263
  let webPath;
2264
- if (options2.flat) {
2264
+ if (options.flat) {
2265
2265
  webPath = targetPath;
2266
2266
  } else {
2267
2267
  console.log(`
@@ -2278,7 +2278,7 @@ async function create(projectName2, options2 = {}) {
2278
2278
  renameSync(tempDir, webDir);
2279
2279
  webPath = webDir;
2280
2280
  const rootPackageJson = {
2281
- name: projectName2,
2281
+ name: projectName,
2282
2282
  private: true,
2283
2283
  workspaces: ["web"],
2284
2284
  scripts: {
@@ -2324,12 +2324,12 @@ async function create(projectName2, options2 = {}) {
2324
2324
  const pkgPath = join2(webPath, "package.json");
2325
2325
  if (existsSync3(pkgPath)) {
2326
2326
  const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
2327
- pkg.name = options2.flat ? projectName2 : `${projectName2}-web`;
2327
+ pkg.name = options.flat ? projectName : `${projectName}-web`;
2328
2328
  pkg.dependencies = pkg.dependencies || {};
2329
2329
  pkg.dependencies["agentation"] = "latest";
2330
2330
  writeFileSync3(pkgPath, JSON.stringify(pkg, null, 2));
2331
2331
  }
2332
- if (options2.convex) {
2332
+ if (options.convex) {
2333
2333
  console.log(`
2334
2334
  \uD83D\uDD27 Setting up Convex...`);
2335
2335
  const addResult = run("bun", ["add", "convex", "convex-react"], { cwd: webPath });
@@ -2353,7 +2353,7 @@ async function create(projectName2, options2 = {}) {
2353
2353
  console.log(` Created .env.local.example`);
2354
2354
  console.log(`
2355
2355
  ⚠️ To complete Convex setup:`);
2356
- console.log(` 1. cd ${options2.flat ? projectName2 : projectName2 + "/web"}`);
2356
+ console.log(` 1. cd ${options.flat ? projectName : projectName + "/web"}`);
2357
2357
  console.log(` 2. bunx convex dev (select/create project)`);
2358
2358
  console.log(` 3. Wrap your app with <ConvexClientProvider> in app/root.tsx`);
2359
2359
  }
@@ -2380,16 +2380,16 @@ async function create(projectName2, options2 = {}) {
2380
2380
  console.log(` Git initialized`);
2381
2381
  console.log(`
2382
2382
  ${"-".repeat(50)}`);
2383
- console.log(`✅ Project "${projectName2}" created!
2383
+ console.log(`✅ Project "${projectName}" created!
2384
2384
  `);
2385
2385
  console.log(`\uD83D\uDCC1 Location: ${targetPath}`);
2386
2386
  console.log(`
2387
2387
  \uD83D\uDCDD Next steps:`);
2388
- console.log(` cd ${projectName2}`);
2389
- if (!options2.flat) {
2388
+ console.log(` cd ${projectName}`);
2389
+ if (!options.flat) {
2390
2390
  console.log(` cd web`);
2391
2391
  }
2392
- if (options2.convex) {
2392
+ if (options.convex) {
2393
2393
  console.log(` bunx convex dev # Setup Convex project`);
2394
2394
  }
2395
2395
  console.log(` bun dev # Start dev server`);
@@ -2399,7 +2399,7 @@ ${"-".repeat(50)}`);
2399
2399
  // src/commands/init.ts
2400
2400
  import { existsSync as existsSync4, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, readFileSync as readFileSync3 } from "fs";
2401
2401
  import { resolve as resolve3 } from "path";
2402
- async function init(projectPath = ".", options2 = {}) {
2402
+ async function init(projectPath = ".", options = {}) {
2403
2403
  const fullPath = resolve3(projectPath);
2404
2404
  if (!existsSync4(fullPath)) {
2405
2405
  console.error(`❌ Path does not exist: ${fullPath}`);
@@ -2434,7 +2434,7 @@ async function init(projectPath = ".", options2 = {}) {
2434
2434
  console.log(` Added .zdev/ to .gitignore`);
2435
2435
  }
2436
2436
  }
2437
- if (options2.seed) {
2437
+ if (options.seed) {
2438
2438
  console.log(`
2439
2439
  \uD83D\uDCE6 Creating seed data...`);
2440
2440
  const convexDir = resolve3(fullPath, "convex");
@@ -2463,20 +2463,20 @@ Next steps:`);
2463
2463
  // src/commands/start.ts
2464
2464
  import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
2465
2465
  import { resolve as resolve4, basename as basename3, join as join3 } from "path";
2466
- function detectWebDir(worktreePath2) {
2466
+ function detectWebDir(worktreePath) {
2467
2467
  const commonDirs = ["web", "frontend", "app", "client", "packages/web", "apps/web"];
2468
2468
  for (const dir of commonDirs) {
2469
- const packagePath = join3(worktreePath2, dir, "package.json");
2469
+ const packagePath = join3(worktreePath, dir, "package.json");
2470
2470
  if (existsSync5(packagePath)) {
2471
2471
  return dir;
2472
2472
  }
2473
2473
  }
2474
- if (existsSync5(join3(worktreePath2, "package.json"))) {
2474
+ if (existsSync5(join3(worktreePath, "package.json"))) {
2475
2475
  return ".";
2476
2476
  }
2477
2477
  return "web";
2478
2478
  }
2479
- async function start(featureName, projectPath = ".", options2 = {}) {
2479
+ async function start(featureName, projectPath = ".", options = {}) {
2480
2480
  const fullPath = resolve4(projectPath);
2481
2481
  if (!existsSync5(fullPath)) {
2482
2482
  console.error(`❌ Path does not exist: ${fullPath}`);
@@ -2488,9 +2488,9 @@ async function start(featureName, projectPath = ".", options2 = {}) {
2488
2488
  }
2489
2489
  const repoName = getRepoName(fullPath);
2490
2490
  const worktreeName = `${repoName}-${featureName}`;
2491
- const worktreePath2 = getWorktreePath(worktreeName);
2491
+ const worktreePath = getWorktreePath(worktreeName);
2492
2492
  const branchName = `feature/${featureName}`;
2493
- let baseBranch = options2.baseBranch;
2493
+ let baseBranch = options.baseBranch;
2494
2494
  if (!baseBranch) {
2495
2495
  const candidates = ["origin/main", "origin/master", "main", "master"];
2496
2496
  for (const candidate of candidates) {
@@ -2508,8 +2508,8 @@ async function start(featureName, projectPath = ".", options2 = {}) {
2508
2508
  console.log(`\uD83D\uDC02 Starting feature: ${featureName}`);
2509
2509
  console.log(` Project: ${repoName}`);
2510
2510
  console.log(` Branch: ${branchName}`);
2511
- const config2 = loadConfig();
2512
- if (config2.allocations[worktreeName]) {
2511
+ const config = loadConfig();
2512
+ if (config.allocations[worktreeName]) {
2513
2513
  console.error(`
2514
2514
  ❌ Feature "${featureName}" already exists for ${repoName}`);
2515
2515
  console.log(` Run: zdev stop ${featureName} --project ${fullPath}`);
@@ -2525,25 +2525,25 @@ async function start(featureName, projectPath = ".", options2 = {}) {
2525
2525
  }
2526
2526
  console.log(`
2527
2527
  \uD83C\uDF33 Creating worktree...`);
2528
- if (existsSync5(worktreePath2)) {
2529
- console.error(` Worktree path already exists: ${worktreePath2}`);
2528
+ if (existsSync5(worktreePath)) {
2529
+ console.error(` Worktree path already exists: ${worktreePath}`);
2530
2530
  process.exit(1);
2531
2531
  }
2532
- const worktreeResult = createWorktree(fullPath, worktreePath2, branchName, baseBranch);
2532
+ const worktreeResult = createWorktree(fullPath, worktreePath, branchName, baseBranch);
2533
2533
  if (!worktreeResult.success) {
2534
2534
  console.error(` Failed to create worktree: ${worktreeResult.error}`);
2535
2535
  process.exit(1);
2536
2536
  }
2537
- console.log(` Created: ${worktreePath2}`);
2538
- const webDir = options2.webDir || detectWebDir(worktreePath2);
2539
- const webPath = webDir === "." ? worktreePath2 : join3(worktreePath2, webDir);
2537
+ console.log(` Created: ${worktreePath}`);
2538
+ const webDir = options.webDir || detectWebDir(worktreePath);
2539
+ const webPath = webDir === "." ? worktreePath : join3(worktreePath, webDir);
2540
2540
  console.log(`
2541
2541
  \uD83D\uDCC1 Web directory: ${webDir === "." ? "(root)" : webDir}`);
2542
- if (config2.copyPatterns && config2.copyPatterns.length > 0) {
2542
+ if (config.copyPatterns && config.copyPatterns.length > 0) {
2543
2543
  console.log(`
2544
2544
  \uD83D\uDCCB Copying config files...`);
2545
2545
  const mainWebPath = webDir === "." ? fullPath : join3(fullPath, webDir);
2546
- for (const pattern of config2.copyPatterns) {
2546
+ for (const pattern of config.copyPatterns) {
2547
2547
  const srcPath = join3(mainWebPath, pattern);
2548
2548
  const destPath = join3(webPath, pattern);
2549
2549
  if (existsSync5(srcPath) && !existsSync5(destPath)) {
@@ -2557,7 +2557,7 @@ async function start(featureName, projectPath = ".", options2 = {}) {
2557
2557
  }
2558
2558
  }
2559
2559
  }
2560
- const setupScriptPath = join3(worktreePath2, ".zdev", "setup.sh");
2560
+ const setupScriptPath = join3(worktreePath, ".zdev", "setup.sh");
2561
2561
  if (existsSync5(setupScriptPath)) {
2562
2562
  console.log(`
2563
2563
  \uD83D\uDCE6 Running setup script...`);
@@ -2572,9 +2572,9 @@ async function start(featureName, projectPath = ".", options2 = {}) {
2572
2572
  ⚠️ No .zdev/setup.sh found, skipping setup`);
2573
2573
  console.log(` Create one in your project to automate dependency installation`);
2574
2574
  }
2575
- const hasConvex = existsSync5(join3(webPath, "convex")) || existsSync5(join3(worktreePath2, "convex"));
2575
+ const hasConvex = existsSync5(join3(webPath, "convex")) || existsSync5(join3(worktreePath, "convex"));
2576
2576
  const seedPath = getSeedPath(repoName);
2577
- if (options2.seed && hasConvex && existsSync5(seedPath)) {
2577
+ if (options.seed && hasConvex && existsSync5(seedPath)) {
2578
2578
  console.log(`
2579
2579
  \uD83C\uDF31 Importing seed data...`);
2580
2580
  const seedResult = run("bunx", ["convex", "import", seedPath], {
@@ -2586,7 +2586,7 @@ async function start(featureName, projectPath = ".", options2 = {}) {
2586
2586
  console.error(` Failed to import seed: ${seedResult.stderr}`);
2587
2587
  }
2588
2588
  }
2589
- const ports = options2.port ? { frontend: options2.port, convex: hasConvex ? options2.port + 100 : 0 } : allocatePorts(config2, hasConvex);
2589
+ const ports = options.port ? { frontend: options.port, convex: hasConvex ? options.port + 100 : 0 } : allocatePorts(config, hasConvex);
2590
2590
  console.log(`
2591
2591
  \uD83D\uDD0C Allocated ports:`);
2592
2592
  console.log(` Frontend: ${ports.frontend}`);
@@ -2636,7 +2636,7 @@ async function start(featureName, projectPath = ".", options2 = {}) {
2636
2636
  console.log(` Frontend PID: ${frontendPid}`);
2637
2637
  let routePath = "";
2638
2638
  let publicUrl = "";
2639
- if (!options2.local) {
2639
+ if (!options.local) {
2640
2640
  const traefikStatus = getTraefikStatus();
2641
2641
  if (traefikStatus.running && traefikStatus.devDomain) {
2642
2642
  routePath = worktreeName;
@@ -2655,7 +2655,7 @@ async function start(featureName, projectPath = ".", options2 = {}) {
2655
2655
  console.log(` Run: zdev config --set devDomain=dev.yourdomain.com`);
2656
2656
  }
2657
2657
  }
2658
- const allocation2 = {
2658
+ const allocation = {
2659
2659
  project: repoName,
2660
2660
  projectPath: fullPath,
2661
2661
  branch: branchName,
@@ -2663,7 +2663,7 @@ async function start(featureName, projectPath = ".", options2 = {}) {
2663
2663
  frontendPort: ports.frontend,
2664
2664
  convexPort: ports.convex,
2665
2665
  funnelPath: routePath,
2666
- worktreePath: worktreePath2,
2666
+ worktreePath,
2667
2667
  publicUrl: publicUrl || undefined,
2668
2668
  pids: {
2669
2669
  frontend: frontendPid,
@@ -2671,104 +2671,116 @@ async function start(featureName, projectPath = ".", options2 = {}) {
2671
2671
  },
2672
2672
  started: new Date().toISOString()
2673
2673
  };
2674
- config2.allocations[worktreeName] = allocation2;
2675
- saveConfig(config2);
2674
+ config.allocations[worktreeName] = allocation;
2675
+ saveConfig(config);
2676
2676
  console.log(`
2677
2677
  ${"-".repeat(50)}`);
2678
2678
  console.log(`✅ Feature "${featureName}" is ready!
2679
2679
  `);
2680
- console.log(`\uD83D\uDCC1 Worktree: ${worktreePath2}`);
2680
+ console.log(`\uD83D\uDCC1 Worktree: ${worktreePath}`);
2681
2681
  console.log(`\uD83C\uDF10 Local: http://localhost:${ports.frontend}`);
2682
2682
  if (publicUrl) {
2683
2683
  console.log(`\uD83D\uDD17 Public: ${publicUrl}`);
2684
2684
  }
2685
2685
  console.log(`
2686
2686
  \uD83D\uDCDD Commands:`);
2687
- console.log(` cd ${worktreePath2}`);
2687
+ console.log(` cd ${worktreePath}`);
2688
2688
  console.log(` zdev stop ${featureName} --project ${fullPath}`);
2689
2689
  console.log(`${"-".repeat(50)}`);
2690
2690
  }
2691
2691
 
2692
2692
  // src/commands/stop.ts
2693
2693
  import { resolve as resolve5 } from "path";
2694
- async function stop(featureName, options2 = {}) {
2695
- const config2 = loadConfig();
2694
+ async function stop(featureName, options = {}) {
2695
+ const config = loadConfig();
2696
2696
  let worktreeName;
2697
- let allocation2;
2698
- if (options2.project) {
2699
- const projectPath = resolve5(options2.project);
2700
- const repoName = isGitRepo(projectPath) ? getRepoName(projectPath) : options2.project;
2697
+ let allocation;
2698
+ if (options.project) {
2699
+ const projectPath = resolve5(options.project);
2700
+ const repoName = isGitRepo(projectPath) ? getRepoName(projectPath) : options.project;
2701
2701
  worktreeName = `${repoName}-${featureName}`;
2702
- allocation2 = config2.allocations[worktreeName];
2702
+ allocation = config.allocations[worktreeName];
2703
2703
  } else {
2704
- for (const [name, alloc] of Object.entries(config2.allocations)) {
2704
+ for (const [name, alloc] of Object.entries(config.allocations)) {
2705
2705
  if (name.endsWith(`-${featureName}`)) {
2706
2706
  worktreeName = name;
2707
- allocation2 = alloc;
2707
+ allocation = alloc;
2708
2708
  break;
2709
2709
  }
2710
2710
  }
2711
2711
  }
2712
- if (!worktreeName || !allocation2) {
2712
+ if (!worktreeName || !allocation) {
2713
2713
  console.error(`❌ Feature "${featureName}" not found`);
2714
2714
  console.log(`
2715
2715
  Run 'zdev list' to see active features`);
2716
2716
  process.exit(1);
2717
2717
  }
2718
2718
  console.log(`\uD83D\uDC02 Stopping feature: ${featureName}`);
2719
- console.log(` Project: ${allocation2.project}`);
2720
- if (allocation2.pids.frontend && isProcessRunning(allocation2.pids.frontend)) {
2719
+ console.log(` Project: ${allocation.project}`);
2720
+ if (allocation.pids.frontend && isProcessRunning(allocation.pids.frontend)) {
2721
2721
  console.log(`
2722
- \uD83D\uDED1 Stopping frontend (PID: ${allocation2.pids.frontend})...`);
2723
- if (killProcess(allocation2.pids.frontend)) {
2722
+ \uD83D\uDED1 Stopping frontend (PID: ${allocation.pids.frontend})...`);
2723
+ if (killProcess(allocation.pids.frontend)) {
2724
2724
  console.log(` Frontend stopped`);
2725
2725
  } else {
2726
2726
  console.error(` Failed to stop frontend`);
2727
2727
  }
2728
2728
  }
2729
- if (allocation2.pids.convex && isProcessRunning(allocation2.pids.convex)) {
2729
+ if (allocation.pids.convex && isProcessRunning(allocation.pids.convex)) {
2730
2730
  console.log(`
2731
- \uD83D\uDED1 Stopping Convex (PID: ${allocation2.pids.convex})...`);
2732
- if (killProcess(allocation2.pids.convex)) {
2731
+ \uD83D\uDED1 Stopping Convex (PID: ${allocation.pids.convex})...`);
2732
+ if (killProcess(allocation.pids.convex)) {
2733
2733
  console.log(` Convex stopped`);
2734
2734
  } else {
2735
2735
  console.error(` Failed to stop Convex`);
2736
2736
  }
2737
2737
  }
2738
- if (allocation2.funnelPath) {
2738
+ if (allocation.funnelPath) {
2739
2739
  console.log(`
2740
2740
  \uD83D\uDD17 Removing Traefik route...`);
2741
- if (traefikRemoveRoute(allocation2.funnelPath)) {
2741
+ if (traefikRemoveRoute(allocation.funnelPath)) {
2742
2742
  console.log(` Route removed`);
2743
2743
  } else {
2744
2744
  console.error(` Failed to remove route (may already be removed)`);
2745
2745
  }
2746
2746
  }
2747
- delete config2.allocations[worktreeName];
2748
- saveConfig(config2);
2749
- const worktreePath2 = getWorktreePath(worktreeName);
2750
- if (options2.keep) {
2747
+ delete config.allocations[worktreeName];
2748
+ saveConfig(config);
2749
+ const worktreePath = getWorktreePath(worktreeName);
2750
+ if (options.keep) {
2751
2751
  console.log(`
2752
2752
  ✅ Feature "${featureName}" stopped (worktree kept)`);
2753
- console.log(` Worktree: ${worktreePath2}`);
2753
+ console.log(` Worktree: ${worktreePath}`);
2754
2754
  console.log(`
2755
2755
  To remove worktree: zdev clean ${featureName}`);
2756
2756
  } else {
2757
2757
  console.log(`
2758
2758
  ✅ Feature "${featureName}" stopped`);
2759
2759
  console.log(`
2760
- Worktree still exists at: ${worktreePath2}`);
2761
- console.log(` To remove: zdev clean ${featureName} --project ${allocation2.projectPath}`);
2760
+ Worktree still exists at: ${worktreePath}`);
2761
+ console.log(` To remove: zdev clean ${featureName} --project ${allocation.projectPath}`);
2762
2762
  }
2763
2763
  }
2764
2764
 
2765
2765
  // src/commands/list.ts
2766
2766
  import { existsSync as existsSync6 } from "fs";
2767
- async function list(options2 = {}) {
2768
- const config2 = loadConfig();
2769
- const allocations = Object.entries(config2.allocations);
2770
- if (options2.json) {
2771
- console.log(JSON.stringify(config2, null, 2));
2767
+ function getTmuxSessions(pattern) {
2768
+ const socketDir = process.env.CLAWDBOT_TMUX_SOCKET_DIR || "/tmp/clawdbot-tmux-sockets";
2769
+ const socket = `${socketDir}/clawdbot.sock`;
2770
+ let result = run("tmux", ["-S", socket, "list-sessions", "-F", "#{session_name}"]);
2771
+ if (!result.success) {
2772
+ result = run("tmux", ["list-sessions", "-F", "#{session_name}"]);
2773
+ }
2774
+ if (!result.success)
2775
+ return [];
2776
+ return result.stdout.split(`
2777
+ `).filter(Boolean).filter((name) => name.toLowerCase().includes(pattern.toLowerCase()));
2778
+ }
2779
+ async function list(options = {}) {
2780
+ const config = loadConfig();
2781
+ const allocations = Object.entries(config.allocations);
2782
+ if (options.json) {
2783
+ console.log(JSON.stringify(config, null, 2));
2772
2784
  return;
2773
2785
  }
2774
2786
  console.log(`\uD83D\uDC02 zdev Status
@@ -2794,21 +2806,33 @@ No active features.
2794
2806
  \uD83D\uDCCB Active Features (${allocations.length}):
2795
2807
  `);
2796
2808
  for (const [name, alloc] of allocations) {
2797
- const worktreePath2 = getWorktreePath(name);
2798
- const worktreeExists = existsSync6(worktreePath2);
2809
+ const worktreePath = getWorktreePath(name);
2810
+ const worktreeExists = existsSync6(worktreePath);
2799
2811
  const frontendRunning = alloc.pids.frontend ? isProcessRunning(alloc.pids.frontend) : false;
2800
2812
  const convexRunning = alloc.pids.convex ? isProcessRunning(alloc.pids.convex) : false;
2801
- const statusEmoji = frontendRunning && convexRunning ? "\uD83D\uDFE2" : frontendRunning || convexRunning ? "\uD83D\uDFE1" : "\uD83D\uDD34";
2813
+ const featureSlug = name.toLowerCase().replace(/[^a-z0-9]/g, "-");
2814
+ const tmuxSessions = getTmuxSessions(featureSlug);
2815
+ const hasTmux = tmuxSessions.length > 0;
2816
+ const isRunning = frontendRunning || convexRunning || hasTmux;
2817
+ const isFullyRunning = frontendRunning && convexRunning || hasTmux && tmuxSessions.length >= 2;
2818
+ const statusEmoji = isFullyRunning ? "\uD83D\uDFE2" : isRunning ? "\uD83D\uDFE1" : "\uD83D\uDD34";
2802
2819
  console.log(`${statusEmoji} ${name}`);
2803
2820
  console.log(` Project: ${alloc.project}`);
2804
2821
  console.log(` Branch: ${alloc.branch}`);
2805
- console.log(` Path: ${worktreePath2} ${worktreeExists ? "" : "(missing)"}`);
2822
+ console.log(` Path: ${worktreePath} ${worktreeExists ? "" : "(missing)"}`);
2806
2823
  console.log(` Local: http://localhost:${alloc.frontendPort}`);
2807
2824
  if (alloc.funnelPath && traefikStatus.devDomain) {
2808
2825
  console.log(` Public: https://${alloc.funnelPath}.${traefikStatus.devDomain}`);
2809
2826
  }
2810
- console.log(` Frontend: ${frontendRunning ? `running (PID: ${alloc.pids.frontend})` : "stopped"}`);
2811
- console.log(` Convex: ${convexRunning ? `running (PID: ${alloc.pids.convex})` : "stopped"}`);
2827
+ if (alloc.pids.frontend || alloc.pids.convex) {
2828
+ console.log(` Frontend: ${frontendRunning ? `running (PID: ${alloc.pids.frontend})` : "stopped"}`);
2829
+ console.log(` Convex: ${convexRunning ? `running (PID: ${alloc.pids.convex})` : "stopped"}`);
2830
+ }
2831
+ if (hasTmux) {
2832
+ console.log(` Tmux: ${tmuxSessions.join(", ")}`);
2833
+ } else if (!frontendRunning && !convexRunning) {
2834
+ console.log(` Servers: stopped`);
2835
+ }
2812
2836
  console.log(` Started: ${new Date(alloc.started).toLocaleString()}`);
2813
2837
  console.log();
2814
2838
  }
@@ -2822,27 +2846,27 @@ Commands:`);
2822
2846
  // src/commands/clean.ts
2823
2847
  import { existsSync as existsSync7, rmSync as rmSync2 } from "fs";
2824
2848
  import { resolve as resolve6 } from "path";
2825
- async function clean(featureName, options2 = {}) {
2826
- const config2 = loadConfig();
2849
+ async function clean(featureName, options = {}) {
2850
+ const config = loadConfig();
2827
2851
  let worktreeName;
2828
- let allocation2;
2852
+ let allocation;
2829
2853
  let projectPath;
2830
- if (options2.project) {
2831
- projectPath = resolve6(options2.project);
2832
- const repoName = isGitRepo(projectPath) ? getRepoName(projectPath) : options2.project;
2854
+ if (options.project) {
2855
+ projectPath = resolve6(options.project);
2856
+ const repoName = isGitRepo(projectPath) ? getRepoName(projectPath) : options.project;
2833
2857
  worktreeName = `${repoName}-${featureName}`;
2834
- allocation2 = config2.allocations[worktreeName];
2858
+ allocation = config.allocations[worktreeName];
2835
2859
  } else {
2836
- for (const [name, alloc] of Object.entries(config2.allocations)) {
2860
+ for (const [name, alloc] of Object.entries(config.allocations)) {
2837
2861
  if (name.endsWith(`-${featureName}`)) {
2838
2862
  worktreeName = name;
2839
- allocation2 = alloc;
2863
+ allocation = alloc;
2840
2864
  projectPath = alloc.projectPath;
2841
2865
  break;
2842
2866
  }
2843
2867
  }
2844
2868
  if (!worktreeName) {
2845
- const entries = Object.keys(config2.allocations);
2869
+ const entries = Object.keys(config.allocations);
2846
2870
  console.error(`❌ Feature "${featureName}" not found in active allocations`);
2847
2871
  if (entries.length > 0) {
2848
2872
  console.log(`
@@ -2852,41 +2876,41 @@ Active features:`);
2852
2876
  process.exit(1);
2853
2877
  }
2854
2878
  }
2855
- const worktreePath2 = getWorktreePath(worktreeName);
2879
+ const worktreePath = getWorktreePath(worktreeName);
2856
2880
  console.log(`\uD83D\uDC02 Cleaning feature: ${featureName}`);
2857
- if (allocation2) {
2858
- if (allocation2.pids.frontend && isProcessRunning(allocation2.pids.frontend)) {
2881
+ if (allocation) {
2882
+ if (allocation.pids.frontend && isProcessRunning(allocation.pids.frontend)) {
2859
2883
  console.log(`
2860
2884
  \uD83D\uDED1 Stopping frontend...`);
2861
- killProcess(allocation2.pids.frontend);
2885
+ killProcess(allocation.pids.frontend);
2862
2886
  }
2863
- if (allocation2.pids.convex && isProcessRunning(allocation2.pids.convex)) {
2887
+ if (allocation.pids.convex && isProcessRunning(allocation.pids.convex)) {
2864
2888
  console.log(`\uD83D\uDED1 Stopping Convex...`);
2865
- killProcess(allocation2.pids.convex);
2889
+ killProcess(allocation.pids.convex);
2866
2890
  }
2867
- if (allocation2.funnelPath) {
2891
+ if (allocation.funnelPath) {
2868
2892
  console.log(`\uD83D\uDD17 Removing Traefik route...`);
2869
- traefikRemoveRoute(allocation2.funnelPath);
2893
+ traefikRemoveRoute(allocation.funnelPath);
2870
2894
  }
2871
- projectPath = allocation2.projectPath;
2895
+ projectPath = allocation.projectPath;
2872
2896
  }
2873
- if (existsSync7(worktreePath2)) {
2897
+ if (existsSync7(worktreePath)) {
2874
2898
  console.log(`
2875
2899
  \uD83D\uDDD1️ Removing worktree...`);
2876
2900
  if (projectPath && isGitRepo(projectPath)) {
2877
- const result = removeWorktree(projectPath, worktreePath2);
2901
+ const result = removeWorktree(projectPath, worktreePath);
2878
2902
  if (!result.success) {
2879
- if (options2.force) {
2903
+ if (options.force) {
2880
2904
  console.log(` Git worktree remove failed, force removing directory...`);
2881
- rmSync2(worktreePath2, { recursive: true, force: true });
2905
+ rmSync2(worktreePath, { recursive: true, force: true });
2882
2906
  } else {
2883
2907
  console.error(` Failed to remove worktree: ${result.error}`);
2884
2908
  console.log(` Use --force to force remove`);
2885
2909
  process.exit(1);
2886
2910
  }
2887
2911
  }
2888
- } else if (options2.force) {
2889
- rmSync2(worktreePath2, { recursive: true, force: true });
2912
+ } else if (options.force) {
2913
+ rmSync2(worktreePath, { recursive: true, force: true });
2890
2914
  } else {
2891
2915
  console.error(` Cannot remove worktree: project path unknown`);
2892
2916
  console.log(` Use --force to force remove, or specify --project`);
@@ -2897,9 +2921,9 @@ Active features:`);
2897
2921
  console.log(`
2898
2922
  Worktree already removed`);
2899
2923
  }
2900
- if (worktreeName && config2.allocations[worktreeName]) {
2901
- delete config2.allocations[worktreeName];
2902
- saveConfig(config2);
2924
+ if (worktreeName && config.allocations[worktreeName]) {
2925
+ delete config.allocations[worktreeName];
2926
+ saveConfig(config);
2903
2927
  }
2904
2928
  console.log(`
2905
2929
  ✅ Feature "${featureName}" cleaned up`);
@@ -2908,7 +2932,7 @@ Active features:`);
2908
2932
  // src/commands/seed.ts
2909
2933
  import { existsSync as existsSync8 } from "fs";
2910
2934
  import { resolve as resolve7 } from "path";
2911
- async function seedExport(projectPath = ".", options2 = {}) {
2935
+ async function seedExport(projectPath = ".", options = {}) {
2912
2936
  const fullPath = resolve7(projectPath);
2913
2937
  if (!existsSync8(fullPath)) {
2914
2938
  console.error(`❌ Path does not exist: ${fullPath}`);
@@ -2935,7 +2959,7 @@ async function seedExport(projectPath = ".", options2 = {}) {
2935
2959
  process.exit(1);
2936
2960
  }
2937
2961
  }
2938
- async function seedImport(projectPath = ".", options2 = {}) {
2962
+ async function seedImport(projectPath = ".", options = {}) {
2939
2963
  const fullPath = resolve7(projectPath);
2940
2964
  if (!existsSync8(fullPath)) {
2941
2965
  console.error(`❌ Path does not exist: ${fullPath}`);
@@ -2945,8 +2969,8 @@ async function seedImport(projectPath = ".", options2 = {}) {
2945
2969
  const projectConfigPath = resolve7(fullPath, ".zdev", "project.json");
2946
2970
  if (existsSync8(projectConfigPath)) {
2947
2971
  try {
2948
- const config2 = JSON.parse(await Bun.file(projectConfigPath).text());
2949
- repoName = config2.name;
2972
+ const config = JSON.parse(await Bun.file(projectConfigPath).text());
2973
+ repoName = config.name;
2950
2974
  } catch {
2951
2975
  repoName = getRepoName(fullPath);
2952
2976
  }
@@ -2981,10 +3005,10 @@ async function seedImport(projectPath = ".", options2 = {}) {
2981
3005
  }
2982
3006
 
2983
3007
  // src/commands/config.ts
2984
- async function configCmd(options2 = {}) {
2985
- const config2 = loadConfig();
2986
- if (options2.set) {
2987
- const [key, ...valueParts] = options2.set.split("=");
3008
+ async function configCmd(options = {}) {
3009
+ const config = loadConfig();
3010
+ if (options.set) {
3011
+ const [key, ...valueParts] = options.set.split("=");
2988
3012
  const value = valueParts.join("=");
2989
3013
  if (!value) {
2990
3014
  console.error(`Usage: zdev config --set key=value`);
@@ -2996,35 +3020,35 @@ Configurable keys:`);
2996
3020
  return;
2997
3021
  }
2998
3022
  if (key === "devDomain") {
2999
- config2.devDomain = value;
3000
- saveConfig(config2);
3023
+ config.devDomain = value;
3024
+ saveConfig(config);
3001
3025
  console.log(`✅ Set devDomain = ${value}`);
3002
3026
  } else if (key === "dockerHostIp") {
3003
- config2.dockerHostIp = value;
3004
- saveConfig(config2);
3027
+ config.dockerHostIp = value;
3028
+ saveConfig(config);
3005
3029
  console.log(`✅ Set dockerHostIp = ${value}`);
3006
3030
  } else if (key === "traefikConfigDir") {
3007
- config2.traefikConfigDir = value;
3008
- saveConfig(config2);
3031
+ config.traefikConfigDir = value;
3032
+ saveConfig(config);
3009
3033
  console.log(`✅ Set traefikConfigDir = ${value}`);
3010
3034
  } else {
3011
3035
  console.error(`Unknown config key: ${key}`);
3012
3036
  }
3013
3037
  return;
3014
3038
  }
3015
- if (options2.list || !options2.add && !options2.remove) {
3039
+ if (options.list || !options.add && !options.remove) {
3016
3040
  console.log(`\uD83D\uDC02 zdev Configuration
3017
3041
  `);
3018
3042
  console.log(`\uD83D\uDCC1 Config file: ${CONFIG_PATH}`);
3019
3043
  console.log(`
3020
3044
  \uD83C\uDF10 Traefik / Public URLs:`);
3021
- console.log(` Dev domain: ${config2.devDomain}`);
3022
- console.log(` Docker host IP: ${config2.dockerHostIp}`);
3023
- console.log(` Config dir: ${config2.traefikConfigDir}`);
3045
+ console.log(` Dev domain: ${config.devDomain}`);
3046
+ console.log(` Docker host IP: ${config.dockerHostIp}`);
3047
+ console.log(` Config dir: ${config.traefikConfigDir}`);
3024
3048
  console.log(`
3025
3049
  \uD83D\uDCCB Copy patterns (files auto-copied to worktrees):`);
3026
- if (config2.copyPatterns && config2.copyPatterns.length > 0) {
3027
- for (const pattern of config2.copyPatterns) {
3050
+ if (config.copyPatterns && config.copyPatterns.length > 0) {
3051
+ for (const pattern of config.copyPatterns) {
3028
3052
  console.log(` - ${pattern}`);
3029
3053
  }
3030
3054
  } else {
@@ -3032,8 +3056,8 @@ Configurable keys:`);
3032
3056
  }
3033
3057
  console.log(`
3034
3058
  \uD83D\uDD0C Port allocation:`);
3035
- console.log(` Next frontend port: ${config2.nextFrontendPort}`);
3036
- console.log(` Next Convex port: ${config2.nextConvexPort}`);
3059
+ console.log(` Next frontend port: ${config.nextFrontendPort}`);
3060
+ console.log(` Next Convex port: ${config.nextConvexPort}`);
3037
3061
  console.log(`
3038
3062
  Commands:`);
3039
3063
  console.log(` zdev config --set devDomain=dev.example.com`);
@@ -3041,31 +3065,31 @@ Commands:`);
3041
3065
  console.log(` zdev config --remove ".env.local"`);
3042
3066
  return;
3043
3067
  }
3044
- if (options2.add) {
3045
- if (!config2.copyPatterns) {
3046
- config2.copyPatterns = [];
3068
+ if (options.add) {
3069
+ if (!config.copyPatterns) {
3070
+ config.copyPatterns = [];
3047
3071
  }
3048
- if (config2.copyPatterns.includes(options2.add)) {
3049
- console.log(`Pattern "${options2.add}" already exists`);
3072
+ if (config.copyPatterns.includes(options.add)) {
3073
+ console.log(`Pattern "${options.add}" already exists`);
3050
3074
  } else {
3051
- config2.copyPatterns.push(options2.add);
3052
- saveConfig(config2);
3053
- console.log(`✅ Added copy pattern: ${options2.add}`);
3075
+ config.copyPatterns.push(options.add);
3076
+ saveConfig(config);
3077
+ console.log(`✅ Added copy pattern: ${options.add}`);
3054
3078
  }
3055
3079
  return;
3056
3080
  }
3057
- if (options2.remove) {
3058
- if (!config2.copyPatterns) {
3059
- console.log(`Pattern "${options2.remove}" not found`);
3081
+ if (options.remove) {
3082
+ if (!config.copyPatterns) {
3083
+ console.log(`Pattern "${options.remove}" not found`);
3060
3084
  return;
3061
3085
  }
3062
- const index = config2.copyPatterns.indexOf(options2.remove);
3086
+ const index = config.copyPatterns.indexOf(options.remove);
3063
3087
  if (index === -1) {
3064
- console.log(`Pattern "${options2.remove}" not found`);
3088
+ console.log(`Pattern "${options.remove}" not found`);
3065
3089
  } else {
3066
- config2.copyPatterns.splice(index, 1);
3067
- saveConfig(config2);
3068
- console.log(`✅ Removed copy pattern: ${options2.remove}`);
3090
+ config.copyPatterns.splice(index, 1);
3091
+ saveConfig(config);
3092
+ console.log(`✅ Removed copy pattern: ${options.remove}`);
3069
3093
  }
3070
3094
  return;
3071
3095
  }
@@ -3073,88 +3097,47 @@ Commands:`);
3073
3097
 
3074
3098
  // src/commands/pr.ts
3075
3099
  import { resolve as resolve8, basename as basename4 } from "path";
3076
- async function pr(featureName, projectPath = ".", options2 = {}) {
3077
- const fullPath = resolve8(projectPath);
3078
- let worktreePath2 = fullPath;
3079
- let allocation2;
3080
- let projectName2 = getRepoName(fullPath) || basename4(fullPath);
3081
- const config2 = loadConfig();
3082
- if (featureName) {
3083
- const allocKey = `${projectName2}-${featureName}`;
3084
- allocation2 = config2.allocations[allocKey];
3085
- if (!allocation2) {
3086
- const found = Object.entries(config2.allocations).find(([key, alloc]) => key.endsWith(`-${featureName}`));
3087
- if (found) {
3088
- allocation2 = found[1];
3089
- projectName2 = allocation2.project;
3090
- }
3091
- }
3092
- if (allocation2) {
3093
- worktreePath2 = allocation2.worktreePath || resolve8(config2.worktreesDir, `${projectName2}-${featureName}`);
3094
- }
3095
- } else {
3096
- const cwd = process.cwd();
3097
- const found = Object.entries(config2.allocations).find(([_, alloc]) => alloc.worktreePath === cwd || cwd.startsWith(alloc.worktreePath || ""));
3098
- if (found) {
3099
- allocation2 = found[1];
3100
- featureName = found[0].split("-").slice(1).join("-");
3101
- projectName2 = allocation2.project;
3102
- worktreePath2 = allocation2.worktreePath || cwd;
3103
- }
3104
- }
3105
- if (!isGitRepo(worktreePath2)) {
3106
- console.error(`❌ Not a git repository: ${worktreePath2}`);
3107
- process.exit(1);
3108
- }
3109
- const branchResult = run("git", ["branch", "--show-current"], { cwd: worktreePath2 });
3110
- if (!branchResult.success || !branchResult.stdout.trim()) {
3111
- console.error("❌ Could not determine current branch");
3112
- process.exit(1);
3100
+ function generateWithAI(diff, commits, worktreePath) {
3101
+ const claudeCheck = run("which", ["claude"]);
3102
+ if (!claudeCheck.success) {
3103
+ return null;
3113
3104
  }
3114
- const branch2 = branchResult.stdout.trim();
3115
- console.log(`\uD83D\uDC02 Creating PR for: ${branch2}`);
3116
- if (allocation2) {
3117
- console.log(` Project: ${projectName2}`);
3118
- console.log(` Feature: ${featureName}`);
3119
- }
3120
- const ghCheck = run("which", ["gh"]);
3121
- if (!ghCheck.success) {
3122
- console.error("❌ GitHub CLI (gh) not found. Install: https://cli.github.com");
3123
- process.exit(1);
3124
- }
3125
- const authCheck = run("gh", ["auth", "status"], { cwd: worktreePath2 });
3126
- if (!authCheck.success) {
3127
- console.error("❌ Not authenticated with GitHub. Run: gh auth login");
3128
- process.exit(1);
3129
- }
3130
- console.log(`
3131
- \uD83D\uDCE4 Pushing branch...`);
3132
- const pushResult = run("git", ["push", "-u", "origin", branch2], { cwd: worktreePath2 });
3133
- if (!pushResult.success) {
3134
- if (!pushResult.stderr.includes("Everything up-to-date")) {
3135
- console.error(` Failed to push: ${pushResult.stderr}`);
3136
- process.exit(1);
3137
- }
3138
- console.log(` Already up to date`);
3139
- } else {
3140
- console.log(` Pushed to origin/${branch2}`);
3105
+ const prompt = `You are generating a GitHub PR title and description.
3106
+
3107
+ Based on the following git diff and commit messages, generate:
3108
+ 1. A concise PR title (max 72 chars, no quotes)
3109
+ 2. A brief description of the changes (2-4 bullet points)
3110
+
3111
+ Commit messages:
3112
+ ${commits.map((c) => `- ${c}`).join(`
3113
+ `)}
3114
+
3115
+ Diff summary (first 3000 chars):
3116
+ ${diff.slice(0, 3000)}
3117
+
3118
+ Respond in this exact format:
3119
+ TITLE: <your title here>
3120
+ BODY:
3121
+ - <bullet point 1>
3122
+ - <bullet point 2>
3123
+ - <bullet point 3>`;
3124
+ const result = run("claude", ["-p", prompt, "--no-input"], {
3125
+ cwd: worktreePath,
3126
+ env: { ...process.env, CLAUDE_CODE_ENTRYPOINT: "zdev" }
3127
+ });
3128
+ if (!result.success || !result.stdout) {
3129
+ return null;
3141
3130
  }
3142
- const defaultBranch = run("git", ["symbolic-ref", "refs/remotes/origin/HEAD", "--short"], { cwd: worktreePath2 });
3143
- const baseBranch = defaultBranch.success ? defaultBranch.stdout.trim().replace("origin/", "") : "main";
3144
- const commitsResult = run("git", ["log", `origin/${baseBranch}..HEAD`, "--pretty=format:%s"], { cwd: worktreePath2 });
3145
- const commits = commitsResult.success ? commitsResult.stdout.trim().split(`
3146
- `).filter(Boolean) : [];
3147
- const filesResult = run("git", ["diff", `origin/${baseBranch}..HEAD`, "--stat", "--stat-width=60"], { cwd: worktreePath2 });
3148
- const filesSummary = filesResult.success ? filesResult.stdout.trim() : "";
3149
- const diffStatResult = run("git", ["diff", `origin/${baseBranch}..HEAD`, "--shortstat"], { cwd: worktreePath2 });
3150
- const diffStat2 = diffStatResult.success ? diffStatResult.stdout.trim() : "";
3151
- const changedFilesResult = run("git", ["diff", `origin/${baseBranch}..HEAD`, "--name-only"], { cwd: worktreePath2 });
3152
- const changedFiles = changedFilesResult.success ? changedFilesResult.stdout.trim().split(`
3153
- `).filter(Boolean) : [];
3154
- let title2 = options2.title;
3155
- if (!title2) {
3156
- title2 = generateSmartTitle(changedFiles, commits, featureName || branch2.replace(/^feature\//, ""));
3131
+ const output = result.stdout.trim();
3132
+ const titleMatch = output.match(/TITLE:\s*(.+)/);
3133
+ const bodyMatch = output.match(/BODY:\s*([\s\S]+)/);
3134
+ if (!titleMatch) {
3135
+ return null;
3157
3136
  }
3137
+ return {
3138
+ title: titleMatch[1].trim().replace(/^["']|["']$/g, ""),
3139
+ body: bodyMatch ? bodyMatch[1].trim() : ""
3140
+ };
3158
3141
  }
3159
3142
  function generateSmartTitle(files, commits, featureName) {
3160
3143
  const components = new Set;
@@ -3208,8 +3191,101 @@ function generateSmartTitle(files, commits, featureName) {
3208
3191
  return commits[0];
3209
3192
  }
3210
3193
  return featureName.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
3194
+ }
3195
+ async function pr(featureName, projectPath = ".", options = {}) {
3196
+ const fullPath = resolve8(projectPath);
3197
+ let worktreePath = fullPath;
3198
+ let allocation;
3199
+ let projectName = getRepoName(fullPath) || basename4(fullPath);
3200
+ const config = loadConfig();
3201
+ if (featureName) {
3202
+ const allocKey = `${projectName}-${featureName}`;
3203
+ allocation = config.allocations[allocKey];
3204
+ if (!allocation) {
3205
+ const found = Object.entries(config.allocations).find(([key]) => key.endsWith(`-${featureName}`));
3206
+ if (found) {
3207
+ allocation = found[1];
3208
+ projectName = allocation.project;
3209
+ }
3210
+ }
3211
+ if (allocation) {
3212
+ worktreePath = allocation.worktreePath || resolve8(config.worktreesDir, `${projectName}-${featureName}`);
3213
+ }
3214
+ } else {
3215
+ const cwd = process.cwd();
3216
+ const found = Object.entries(config.allocations).find(([_, alloc]) => alloc.worktreePath === cwd || cwd.startsWith(alloc.worktreePath || ""));
3217
+ if (found) {
3218
+ allocation = found[1];
3219
+ featureName = found[0].split("-").slice(1).join("-");
3220
+ projectName = allocation.project;
3221
+ worktreePath = allocation.worktreePath || cwd;
3222
+ }
3223
+ }
3224
+ if (!isGitRepo(worktreePath)) {
3225
+ console.error(`❌ Not a git repository: ${worktreePath}`);
3226
+ process.exit(1);
3227
+ }
3228
+ const branchResult = run("git", ["branch", "--show-current"], { cwd: worktreePath });
3229
+ if (!branchResult.success || !branchResult.stdout.trim()) {
3230
+ console.error("❌ Could not determine current branch");
3231
+ process.exit(1);
3232
+ }
3233
+ const branch = branchResult.stdout.trim();
3234
+ console.log(`\uD83D\uDC02 Creating PR for: ${branch}`);
3235
+ if (allocation) {
3236
+ console.log(` Project: ${projectName}`);
3237
+ console.log(` Feature: ${featureName}`);
3238
+ }
3239
+ const ghCheck = run("which", ["gh"]);
3240
+ if (!ghCheck.success) {
3241
+ console.error("❌ GitHub CLI (gh) not found. Install: https://cli.github.com");
3242
+ process.exit(1);
3243
+ }
3244
+ const authCheck = run("gh", ["auth", "status"], { cwd: worktreePath });
3245
+ if (!authCheck.success) {
3246
+ console.error("❌ Not authenticated with GitHub. Run: gh auth login");
3247
+ process.exit(1);
3248
+ }
3249
+ console.log(`
3250
+ \uD83D\uDCE4 Pushing branch...`);
3251
+ const pushResult = run("git", ["push", "-u", "origin", branch], { cwd: worktreePath });
3252
+ if (!pushResult.success && !pushResult.stderr.includes("Everything up-to-date")) {
3253
+ console.error(` Failed to push: ${pushResult.stderr}`);
3254
+ process.exit(1);
3255
+ }
3256
+ console.log(` Pushed to origin/${branch}`);
3257
+ const defaultBranch = run("git", ["symbolic-ref", "refs/remotes/origin/HEAD", "--short"], { cwd: worktreePath });
3258
+ const baseBranch = defaultBranch.success ? defaultBranch.stdout.trim().replace("origin/", "") : "main";
3259
+ const commitsResult = run("git", ["log", `origin/${baseBranch}..HEAD`, "--pretty=format:%s"], { cwd: worktreePath });
3260
+ const commits = commitsResult.success ? commitsResult.stdout.trim().split(`
3261
+ `).filter(Boolean) : [];
3262
+ const diffResult = run("git", ["diff", `origin/${baseBranch}..HEAD`], { cwd: worktreePath });
3263
+ const diff = diffResult.success ? diffResult.stdout : "";
3264
+ const diffStatResult = run("git", ["diff", `origin/${baseBranch}..HEAD`, "--shortstat"], { cwd: worktreePath });
3265
+ const diffStat = diffStatResult.success ? diffStatResult.stdout.trim() : "";
3266
+ const changedFilesResult = run("git", ["diff", `origin/${baseBranch}..HEAD`, "--name-only"], { cwd: worktreePath });
3267
+ const changedFiles = changedFilesResult.success ? changedFilesResult.stdout.trim().split(`
3268
+ `).filter(Boolean) : [];
3269
+ let title = options.title;
3270
+ let aiBody = "";
3271
+ if (options.ai || !title && diff.length > 0) {
3272
+ console.log(`
3273
+ \uD83E\uDD16 Generating PR content with AI...`);
3274
+ const aiResult = generateWithAI(diff, commits, worktreePath);
3275
+ if (aiResult) {
3276
+ if (!title)
3277
+ title = aiResult.title;
3278
+ aiBody = aiResult.body;
3279
+ console.log(` Generated title: ${title}`);
3280
+ } else if (options.ai) {
3281
+ console.log(` AI generation failed, using smart fallback`);
3282
+ }
3283
+ }
3284
+ if (!title) {
3285
+ title = generateSmartTitle(changedFiles, commits, featureName || branch.replace(/^feature\//, ""));
3286
+ }
3211
3287
  let body = options.body || "";
3212
- if (allocation && allocation.publicUrl) {
3288
+ if (allocation?.publicUrl) {
3213
3289
  body += `## Preview
3214
3290
  \uD83D\uDD17 ${allocation.publicUrl}
3215
3291
 
@@ -3221,7 +3297,12 @@ function generateSmartTitle(files, commits, featureName) {
3221
3297
 
3222
3298
  `;
3223
3299
  }
3224
- if (commits.length > 0) {
3300
+ if (aiBody) {
3301
+ body += `## Changes
3302
+ ${aiBody}
3303
+
3304
+ `;
3305
+ } else if (commits.length > 0) {
3225
3306
  body += `## Changes
3226
3307
  `;
3227
3308
  commits.forEach((commit) => {
@@ -3258,6 +3339,7 @@ ${diffStat}
3258
3339
  }
3259
3340
  console.log(`
3260
3341
  \uD83D\uDCDD Creating pull request...`);
3342
+ console.log(` Title: ${title}`);
3261
3343
  const prArgs = ["pr", "create", "--title", title, "--body", body];
3262
3344
  if (options.draft) {
3263
3345
  prArgs.push("--draft");
@@ -3308,32 +3390,32 @@ var __dirname2 = dirname(fileURLToPath(import.meta.url));
3308
3390
  var pkg = JSON.parse(readFileSync5(join4(__dirname2, "..", "package.json"), "utf-8"));
3309
3391
  var program2 = new Command;
3310
3392
  program2.name("zdev").description(`\uD83D\uDC02 zdev v${pkg.version} - Multi-agent worktree development environment`).version(pkg.version);
3311
- program2.command("create <name>").description("Create a new TanStack Start project").option("--convex", "Add Convex backend integration").option("--flat", "Flat structure (no monorepo)").action(async (name, options2) => {
3393
+ program2.command("create <name>").description("Create a new TanStack Start project").option("--convex", "Add Convex backend integration").option("--flat", "Flat structure (no monorepo)").action(async (name, options) => {
3312
3394
  await create(name, {
3313
- convex: options2.convex,
3314
- flat: options2.flat
3395
+ convex: options.convex,
3396
+ flat: options.flat
3315
3397
  });
3316
3398
  });
3317
- program2.command("init [path]").description("Initialize zdev for a project").option("-s, --seed", "Create initial seed data from current Convex state").action(async (path, options2) => {
3318
- await init(path, options2);
3399
+ program2.command("init [path]").description("Initialize zdev for a project").option("-s, --seed", "Create initial seed data from current Convex state").action(async (path, options) => {
3400
+ await init(path, options);
3319
3401
  });
3320
- program2.command("start <feature>").description("Start working on a feature (creates worktree, starts servers)").option("-p, --project <path>", "Project path (default: current directory)", ".").option("--port <number>", "Frontend port (auto-allocated if not specified)", parseInt).option("--local", "Local only - skip public URL setup via Traefik").option("-s, --seed", "Import seed data into the new worktree").option("-b, --base-branch <branch>", "Base branch to create from (auto-detected if not specified)").option("-w, --web-dir <dir>", "Subdirectory containing package.json (auto-detected if not specified)").action(async (feature, options2) => {
3321
- await start(feature, options2.project, {
3322
- port: options2.port,
3323
- local: options2.local,
3324
- seed: options2.seed,
3325
- baseBranch: options2.baseBranch,
3326
- webDir: options2.webDir
3402
+ program2.command("start <feature>").description("Start working on a feature (creates worktree, starts servers)").option("-p, --project <path>", "Project path (default: current directory)", ".").option("--port <number>", "Frontend port (auto-allocated if not specified)", parseInt).option("--local", "Local only - skip public URL setup via Traefik").option("-s, --seed", "Import seed data into the new worktree").option("-b, --base-branch <branch>", "Base branch to create from (auto-detected if not specified)").option("-w, --web-dir <dir>", "Subdirectory containing package.json (auto-detected if not specified)").action(async (feature, options) => {
3403
+ await start(feature, options.project, {
3404
+ port: options.port,
3405
+ local: options.local,
3406
+ seed: options.seed,
3407
+ baseBranch: options.baseBranch,
3408
+ webDir: options.webDir
3327
3409
  });
3328
3410
  });
3329
- program2.command("stop <feature>").description("Stop servers for a feature").option("-p, --project <path>", "Project path (to disambiguate features)").option("-k, --keep", "Keep worktree, just stop servers").action(async (feature, options2) => {
3330
- await stop(feature, options2);
3411
+ program2.command("stop <feature>").description("Stop servers for a feature").option("-p, --project <path>", "Project path (to disambiguate features)").option("-k, --keep", "Keep worktree, just stop servers").action(async (feature, options) => {
3412
+ await stop(feature, options);
3331
3413
  });
3332
- program2.command("list").description("List active features and their status").option("--json", "Output as JSON").action(async (options2) => {
3333
- await list(options2);
3414
+ program2.command("list").description("List active features and their status").option("--json", "Output as JSON").action(async (options) => {
3415
+ await list(options);
3334
3416
  });
3335
- program2.command("clean <feature>").description("Remove a feature worktree (use after PR is merged)").option("-p, --project <path>", "Project path").option("-f, --force", "Force remove even if git worktree fails").action(async (feature, options2) => {
3336
- await clean(feature, options2);
3417
+ program2.command("clean <feature>").description("Remove a feature worktree (use after PR is merged)").option("-p, --project <path>", "Project path").option("-f, --force", "Force remove even if git worktree fails").action(async (feature, options) => {
3418
+ await clean(feature, options);
3337
3419
  });
3338
3420
  var seedCmd = program2.command("seed").description("Manage seed data for projects");
3339
3421
  seedCmd.command("export [path]").description("Export current Convex data as seed").action(async (path) => {
@@ -3342,15 +3424,16 @@ seedCmd.command("export [path]").description("Export current Convex data as seed
3342
3424
  seedCmd.command("import [path]").description("Import seed data into current worktree").action(async (path) => {
3343
3425
  await seedImport(path);
3344
3426
  });
3345
- program2.command("config").description("View and manage zdev configuration").option("-a, --add <pattern>", "Add a file pattern to auto-copy").option("-r, --remove <pattern>", "Remove a file pattern").option("-s, --set <key=value>", "Set a config value (devDomain, dockerHostIp, traefikConfigDir)").option("-l, --list", "List current configuration").action(async (options2) => {
3346
- await configCmd(options2);
3427
+ program2.command("config").description("View and manage zdev configuration").option("-a, --add <pattern>", "Add a file pattern to auto-copy").option("-r, --remove <pattern>", "Remove a file pattern").option("-s, --set <key=value>", "Set a config value (devDomain, dockerHostIp, traefikConfigDir)").option("-l, --list", "List current configuration").action(async (options) => {
3428
+ await configCmd(options);
3347
3429
  });
3348
- program2.command("pr [feature]").description("Create a pull request for a feature branch").option("-p, --project <path>", "Project path", ".").option("-t, --title <title>", "PR title (auto-generated if not specified)").option("-b, --body <body>", "PR body (preview URL auto-added)").option("-d, --draft", "Create as draft PR").option("-w, --web", "Open in browser instead of CLI").action(async (feature, options2) => {
3349
- await pr(feature, options2.project, {
3350
- title: options2.title,
3351
- body: options2.body,
3352
- draft: options2.draft,
3353
- web: options2.web
3430
+ program2.command("pr [feature]").description("Create a pull request for a feature branch").option("-p, --project <path>", "Project path", ".").option("-t, --title <title>", "PR title (auto-generated if not specified)").option("-b, --body <body>", "PR body (preview URL auto-added)").option("-d, --draft", "Create as draft PR").option("-w, --web", "Open in browser instead of CLI").option("--ai", "Use Claude to generate title and description from diff").action(async (feature, options) => {
3431
+ await pr(feature, options.project, {
3432
+ title: options.title,
3433
+ body: options.body,
3434
+ draft: options.draft,
3435
+ web: options.web,
3436
+ ai: options.ai
3354
3437
  });
3355
3438
  });
3356
3439
  program2.command("status").description("Show zdev status (alias for list)").action(async () => {