zdev 0.2.2 → 0.2.3

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,104 @@ 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
+ async function list(options = {}) {
2768
+ const config = loadConfig();
2769
+ const allocations = Object.entries(config.allocations);
2770
+ if (options.json) {
2771
+ console.log(JSON.stringify(config, null, 2));
2772
2772
  return;
2773
2773
  }
2774
2774
  console.log(`\uD83D\uDC02 zdev Status
@@ -2794,15 +2794,15 @@ No active features.
2794
2794
  \uD83D\uDCCB Active Features (${allocations.length}):
2795
2795
  `);
2796
2796
  for (const [name, alloc] of allocations) {
2797
- const worktreePath2 = getWorktreePath(name);
2798
- const worktreeExists = existsSync6(worktreePath2);
2797
+ const worktreePath = getWorktreePath(name);
2798
+ const worktreeExists = existsSync6(worktreePath);
2799
2799
  const frontendRunning = alloc.pids.frontend ? isProcessRunning(alloc.pids.frontend) : false;
2800
2800
  const convexRunning = alloc.pids.convex ? isProcessRunning(alloc.pids.convex) : false;
2801
2801
  const statusEmoji = frontendRunning && convexRunning ? "\uD83D\uDFE2" : frontendRunning || convexRunning ? "\uD83D\uDFE1" : "\uD83D\uDD34";
2802
2802
  console.log(`${statusEmoji} ${name}`);
2803
2803
  console.log(` Project: ${alloc.project}`);
2804
2804
  console.log(` Branch: ${alloc.branch}`);
2805
- console.log(` Path: ${worktreePath2} ${worktreeExists ? "" : "(missing)"}`);
2805
+ console.log(` Path: ${worktreePath} ${worktreeExists ? "" : "(missing)"}`);
2806
2806
  console.log(` Local: http://localhost:${alloc.frontendPort}`);
2807
2807
  if (alloc.funnelPath && traefikStatus.devDomain) {
2808
2808
  console.log(` Public: https://${alloc.funnelPath}.${traefikStatus.devDomain}`);
@@ -2822,27 +2822,27 @@ Commands:`);
2822
2822
  // src/commands/clean.ts
2823
2823
  import { existsSync as existsSync7, rmSync as rmSync2 } from "fs";
2824
2824
  import { resolve as resolve6 } from "path";
2825
- async function clean(featureName, options2 = {}) {
2826
- const config2 = loadConfig();
2825
+ async function clean(featureName, options = {}) {
2826
+ const config = loadConfig();
2827
2827
  let worktreeName;
2828
- let allocation2;
2828
+ let allocation;
2829
2829
  let projectPath;
2830
- if (options2.project) {
2831
- projectPath = resolve6(options2.project);
2832
- const repoName = isGitRepo(projectPath) ? getRepoName(projectPath) : options2.project;
2830
+ if (options.project) {
2831
+ projectPath = resolve6(options.project);
2832
+ const repoName = isGitRepo(projectPath) ? getRepoName(projectPath) : options.project;
2833
2833
  worktreeName = `${repoName}-${featureName}`;
2834
- allocation2 = config2.allocations[worktreeName];
2834
+ allocation = config.allocations[worktreeName];
2835
2835
  } else {
2836
- for (const [name, alloc] of Object.entries(config2.allocations)) {
2836
+ for (const [name, alloc] of Object.entries(config.allocations)) {
2837
2837
  if (name.endsWith(`-${featureName}`)) {
2838
2838
  worktreeName = name;
2839
- allocation2 = alloc;
2839
+ allocation = alloc;
2840
2840
  projectPath = alloc.projectPath;
2841
2841
  break;
2842
2842
  }
2843
2843
  }
2844
2844
  if (!worktreeName) {
2845
- const entries = Object.keys(config2.allocations);
2845
+ const entries = Object.keys(config.allocations);
2846
2846
  console.error(`❌ Feature "${featureName}" not found in active allocations`);
2847
2847
  if (entries.length > 0) {
2848
2848
  console.log(`
@@ -2852,41 +2852,41 @@ Active features:`);
2852
2852
  process.exit(1);
2853
2853
  }
2854
2854
  }
2855
- const worktreePath2 = getWorktreePath(worktreeName);
2855
+ const worktreePath = getWorktreePath(worktreeName);
2856
2856
  console.log(`\uD83D\uDC02 Cleaning feature: ${featureName}`);
2857
- if (allocation2) {
2858
- if (allocation2.pids.frontend && isProcessRunning(allocation2.pids.frontend)) {
2857
+ if (allocation) {
2858
+ if (allocation.pids.frontend && isProcessRunning(allocation.pids.frontend)) {
2859
2859
  console.log(`
2860
2860
  \uD83D\uDED1 Stopping frontend...`);
2861
- killProcess(allocation2.pids.frontend);
2861
+ killProcess(allocation.pids.frontend);
2862
2862
  }
2863
- if (allocation2.pids.convex && isProcessRunning(allocation2.pids.convex)) {
2863
+ if (allocation.pids.convex && isProcessRunning(allocation.pids.convex)) {
2864
2864
  console.log(`\uD83D\uDED1 Stopping Convex...`);
2865
- killProcess(allocation2.pids.convex);
2865
+ killProcess(allocation.pids.convex);
2866
2866
  }
2867
- if (allocation2.funnelPath) {
2867
+ if (allocation.funnelPath) {
2868
2868
  console.log(`\uD83D\uDD17 Removing Traefik route...`);
2869
- traefikRemoveRoute(allocation2.funnelPath);
2869
+ traefikRemoveRoute(allocation.funnelPath);
2870
2870
  }
2871
- projectPath = allocation2.projectPath;
2871
+ projectPath = allocation.projectPath;
2872
2872
  }
2873
- if (existsSync7(worktreePath2)) {
2873
+ if (existsSync7(worktreePath)) {
2874
2874
  console.log(`
2875
2875
  \uD83D\uDDD1️ Removing worktree...`);
2876
2876
  if (projectPath && isGitRepo(projectPath)) {
2877
- const result = removeWorktree(projectPath, worktreePath2);
2877
+ const result = removeWorktree(projectPath, worktreePath);
2878
2878
  if (!result.success) {
2879
- if (options2.force) {
2879
+ if (options.force) {
2880
2880
  console.log(` Git worktree remove failed, force removing directory...`);
2881
- rmSync2(worktreePath2, { recursive: true, force: true });
2881
+ rmSync2(worktreePath, { recursive: true, force: true });
2882
2882
  } else {
2883
2883
  console.error(` Failed to remove worktree: ${result.error}`);
2884
2884
  console.log(` Use --force to force remove`);
2885
2885
  process.exit(1);
2886
2886
  }
2887
2887
  }
2888
- } else if (options2.force) {
2889
- rmSync2(worktreePath2, { recursive: true, force: true });
2888
+ } else if (options.force) {
2889
+ rmSync2(worktreePath, { recursive: true, force: true });
2890
2890
  } else {
2891
2891
  console.error(` Cannot remove worktree: project path unknown`);
2892
2892
  console.log(` Use --force to force remove, or specify --project`);
@@ -2897,9 +2897,9 @@ Active features:`);
2897
2897
  console.log(`
2898
2898
  Worktree already removed`);
2899
2899
  }
2900
- if (worktreeName && config2.allocations[worktreeName]) {
2901
- delete config2.allocations[worktreeName];
2902
- saveConfig(config2);
2900
+ if (worktreeName && config.allocations[worktreeName]) {
2901
+ delete config.allocations[worktreeName];
2902
+ saveConfig(config);
2903
2903
  }
2904
2904
  console.log(`
2905
2905
  ✅ Feature "${featureName}" cleaned up`);
@@ -2908,7 +2908,7 @@ Active features:`);
2908
2908
  // src/commands/seed.ts
2909
2909
  import { existsSync as existsSync8 } from "fs";
2910
2910
  import { resolve as resolve7 } from "path";
2911
- async function seedExport(projectPath = ".", options2 = {}) {
2911
+ async function seedExport(projectPath = ".", options = {}) {
2912
2912
  const fullPath = resolve7(projectPath);
2913
2913
  if (!existsSync8(fullPath)) {
2914
2914
  console.error(`❌ Path does not exist: ${fullPath}`);
@@ -2935,7 +2935,7 @@ async function seedExport(projectPath = ".", options2 = {}) {
2935
2935
  process.exit(1);
2936
2936
  }
2937
2937
  }
2938
- async function seedImport(projectPath = ".", options2 = {}) {
2938
+ async function seedImport(projectPath = ".", options = {}) {
2939
2939
  const fullPath = resolve7(projectPath);
2940
2940
  if (!existsSync8(fullPath)) {
2941
2941
  console.error(`❌ Path does not exist: ${fullPath}`);
@@ -2945,8 +2945,8 @@ async function seedImport(projectPath = ".", options2 = {}) {
2945
2945
  const projectConfigPath = resolve7(fullPath, ".zdev", "project.json");
2946
2946
  if (existsSync8(projectConfigPath)) {
2947
2947
  try {
2948
- const config2 = JSON.parse(await Bun.file(projectConfigPath).text());
2949
- repoName = config2.name;
2948
+ const config = JSON.parse(await Bun.file(projectConfigPath).text());
2949
+ repoName = config.name;
2950
2950
  } catch {
2951
2951
  repoName = getRepoName(fullPath);
2952
2952
  }
@@ -2981,10 +2981,10 @@ async function seedImport(projectPath = ".", options2 = {}) {
2981
2981
  }
2982
2982
 
2983
2983
  // src/commands/config.ts
2984
- async function configCmd(options2 = {}) {
2985
- const config2 = loadConfig();
2986
- if (options2.set) {
2987
- const [key, ...valueParts] = options2.set.split("=");
2984
+ async function configCmd(options = {}) {
2985
+ const config = loadConfig();
2986
+ if (options.set) {
2987
+ const [key, ...valueParts] = options.set.split("=");
2988
2988
  const value = valueParts.join("=");
2989
2989
  if (!value) {
2990
2990
  console.error(`Usage: zdev config --set key=value`);
@@ -2996,35 +2996,35 @@ Configurable keys:`);
2996
2996
  return;
2997
2997
  }
2998
2998
  if (key === "devDomain") {
2999
- config2.devDomain = value;
3000
- saveConfig(config2);
2999
+ config.devDomain = value;
3000
+ saveConfig(config);
3001
3001
  console.log(`✅ Set devDomain = ${value}`);
3002
3002
  } else if (key === "dockerHostIp") {
3003
- config2.dockerHostIp = value;
3004
- saveConfig(config2);
3003
+ config.dockerHostIp = value;
3004
+ saveConfig(config);
3005
3005
  console.log(`✅ Set dockerHostIp = ${value}`);
3006
3006
  } else if (key === "traefikConfigDir") {
3007
- config2.traefikConfigDir = value;
3008
- saveConfig(config2);
3007
+ config.traefikConfigDir = value;
3008
+ saveConfig(config);
3009
3009
  console.log(`✅ Set traefikConfigDir = ${value}`);
3010
3010
  } else {
3011
3011
  console.error(`Unknown config key: ${key}`);
3012
3012
  }
3013
3013
  return;
3014
3014
  }
3015
- if (options2.list || !options2.add && !options2.remove) {
3015
+ if (options.list || !options.add && !options.remove) {
3016
3016
  console.log(`\uD83D\uDC02 zdev Configuration
3017
3017
  `);
3018
3018
  console.log(`\uD83D\uDCC1 Config file: ${CONFIG_PATH}`);
3019
3019
  console.log(`
3020
3020
  \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}`);
3021
+ console.log(` Dev domain: ${config.devDomain}`);
3022
+ console.log(` Docker host IP: ${config.dockerHostIp}`);
3023
+ console.log(` Config dir: ${config.traefikConfigDir}`);
3024
3024
  console.log(`
3025
3025
  \uD83D\uDCCB Copy patterns (files auto-copied to worktrees):`);
3026
- if (config2.copyPatterns && config2.copyPatterns.length > 0) {
3027
- for (const pattern of config2.copyPatterns) {
3026
+ if (config.copyPatterns && config.copyPatterns.length > 0) {
3027
+ for (const pattern of config.copyPatterns) {
3028
3028
  console.log(` - ${pattern}`);
3029
3029
  }
3030
3030
  } else {
@@ -3032,8 +3032,8 @@ Configurable keys:`);
3032
3032
  }
3033
3033
  console.log(`
3034
3034
  \uD83D\uDD0C Port allocation:`);
3035
- console.log(` Next frontend port: ${config2.nextFrontendPort}`);
3036
- console.log(` Next Convex port: ${config2.nextConvexPort}`);
3035
+ console.log(` Next frontend port: ${config.nextFrontendPort}`);
3036
+ console.log(` Next Convex port: ${config.nextConvexPort}`);
3037
3037
  console.log(`
3038
3038
  Commands:`);
3039
3039
  console.log(` zdev config --set devDomain=dev.example.com`);
@@ -3041,31 +3041,31 @@ Commands:`);
3041
3041
  console.log(` zdev config --remove ".env.local"`);
3042
3042
  return;
3043
3043
  }
3044
- if (options2.add) {
3045
- if (!config2.copyPatterns) {
3046
- config2.copyPatterns = [];
3044
+ if (options.add) {
3045
+ if (!config.copyPatterns) {
3046
+ config.copyPatterns = [];
3047
3047
  }
3048
- if (config2.copyPatterns.includes(options2.add)) {
3049
- console.log(`Pattern "${options2.add}" already exists`);
3048
+ if (config.copyPatterns.includes(options.add)) {
3049
+ console.log(`Pattern "${options.add}" already exists`);
3050
3050
  } else {
3051
- config2.copyPatterns.push(options2.add);
3052
- saveConfig(config2);
3053
- console.log(`✅ Added copy pattern: ${options2.add}`);
3051
+ config.copyPatterns.push(options.add);
3052
+ saveConfig(config);
3053
+ console.log(`✅ Added copy pattern: ${options.add}`);
3054
3054
  }
3055
3055
  return;
3056
3056
  }
3057
- if (options2.remove) {
3058
- if (!config2.copyPatterns) {
3059
- console.log(`Pattern "${options2.remove}" not found`);
3057
+ if (options.remove) {
3058
+ if (!config.copyPatterns) {
3059
+ console.log(`Pattern "${options.remove}" not found`);
3060
3060
  return;
3061
3061
  }
3062
- const index = config2.copyPatterns.indexOf(options2.remove);
3062
+ const index = config.copyPatterns.indexOf(options.remove);
3063
3063
  if (index === -1) {
3064
- console.log(`Pattern "${options2.remove}" not found`);
3064
+ console.log(`Pattern "${options.remove}" not found`);
3065
3065
  } else {
3066
- config2.copyPatterns.splice(index, 1);
3067
- saveConfig(config2);
3068
- console.log(`✅ Removed copy pattern: ${options2.remove}`);
3066
+ config.copyPatterns.splice(index, 1);
3067
+ saveConfig(config);
3068
+ console.log(`✅ Removed copy pattern: ${options.remove}`);
3069
3069
  }
3070
3070
  return;
3071
3071
  }
@@ -3073,88 +3073,47 @@ Commands:`);
3073
3073
 
3074
3074
  // src/commands/pr.ts
3075
3075
  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);
3113
- }
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);
3076
+ function generateWithAI(diff, commits, worktreePath) {
3077
+ const claudeCheck = run("which", ["claude"]);
3078
+ if (!claudeCheck.success) {
3079
+ return null;
3129
3080
  }
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}`);
3081
+ const prompt = `You are generating a GitHub PR title and description.
3082
+
3083
+ Based on the following git diff and commit messages, generate:
3084
+ 1. A concise PR title (max 72 chars, no quotes)
3085
+ 2. A brief description of the changes (2-4 bullet points)
3086
+
3087
+ Commit messages:
3088
+ ${commits.map((c) => `- ${c}`).join(`
3089
+ `)}
3090
+
3091
+ Diff summary (first 3000 chars):
3092
+ ${diff.slice(0, 3000)}
3093
+
3094
+ Respond in this exact format:
3095
+ TITLE: <your title here>
3096
+ BODY:
3097
+ - <bullet point 1>
3098
+ - <bullet point 2>
3099
+ - <bullet point 3>`;
3100
+ const result = run("claude", ["-p", prompt, "--no-input"], {
3101
+ cwd: worktreePath,
3102
+ env: { ...process.env, CLAUDE_CODE_ENTRYPOINT: "zdev" }
3103
+ });
3104
+ if (!result.success || !result.stdout) {
3105
+ return null;
3141
3106
  }
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\//, ""));
3107
+ const output = result.stdout.trim();
3108
+ const titleMatch = output.match(/TITLE:\s*(.+)/);
3109
+ const bodyMatch = output.match(/BODY:\s*([\s\S]+)/);
3110
+ if (!titleMatch) {
3111
+ return null;
3157
3112
  }
3113
+ return {
3114
+ title: titleMatch[1].trim().replace(/^["']|["']$/g, ""),
3115
+ body: bodyMatch ? bodyMatch[1].trim() : ""
3116
+ };
3158
3117
  }
3159
3118
  function generateSmartTitle(files, commits, featureName) {
3160
3119
  const components = new Set;
@@ -3208,8 +3167,101 @@ function generateSmartTitle(files, commits, featureName) {
3208
3167
  return commits[0];
3209
3168
  }
3210
3169
  return featureName.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
3170
+ }
3171
+ async function pr(featureName, projectPath = ".", options = {}) {
3172
+ const fullPath = resolve8(projectPath);
3173
+ let worktreePath = fullPath;
3174
+ let allocation;
3175
+ let projectName = getRepoName(fullPath) || basename4(fullPath);
3176
+ const config = loadConfig();
3177
+ if (featureName) {
3178
+ const allocKey = `${projectName}-${featureName}`;
3179
+ allocation = config.allocations[allocKey];
3180
+ if (!allocation) {
3181
+ const found = Object.entries(config.allocations).find(([key]) => key.endsWith(`-${featureName}`));
3182
+ if (found) {
3183
+ allocation = found[1];
3184
+ projectName = allocation.project;
3185
+ }
3186
+ }
3187
+ if (allocation) {
3188
+ worktreePath = allocation.worktreePath || resolve8(config.worktreesDir, `${projectName}-${featureName}`);
3189
+ }
3190
+ } else {
3191
+ const cwd = process.cwd();
3192
+ const found = Object.entries(config.allocations).find(([_, alloc]) => alloc.worktreePath === cwd || cwd.startsWith(alloc.worktreePath || ""));
3193
+ if (found) {
3194
+ allocation = found[1];
3195
+ featureName = found[0].split("-").slice(1).join("-");
3196
+ projectName = allocation.project;
3197
+ worktreePath = allocation.worktreePath || cwd;
3198
+ }
3199
+ }
3200
+ if (!isGitRepo(worktreePath)) {
3201
+ console.error(`❌ Not a git repository: ${worktreePath}`);
3202
+ process.exit(1);
3203
+ }
3204
+ const branchResult = run("git", ["branch", "--show-current"], { cwd: worktreePath });
3205
+ if (!branchResult.success || !branchResult.stdout.trim()) {
3206
+ console.error("❌ Could not determine current branch");
3207
+ process.exit(1);
3208
+ }
3209
+ const branch = branchResult.stdout.trim();
3210
+ console.log(`\uD83D\uDC02 Creating PR for: ${branch}`);
3211
+ if (allocation) {
3212
+ console.log(` Project: ${projectName}`);
3213
+ console.log(` Feature: ${featureName}`);
3214
+ }
3215
+ const ghCheck = run("which", ["gh"]);
3216
+ if (!ghCheck.success) {
3217
+ console.error("❌ GitHub CLI (gh) not found. Install: https://cli.github.com");
3218
+ process.exit(1);
3219
+ }
3220
+ const authCheck = run("gh", ["auth", "status"], { cwd: worktreePath });
3221
+ if (!authCheck.success) {
3222
+ console.error("❌ Not authenticated with GitHub. Run: gh auth login");
3223
+ process.exit(1);
3224
+ }
3225
+ console.log(`
3226
+ \uD83D\uDCE4 Pushing branch...`);
3227
+ const pushResult = run("git", ["push", "-u", "origin", branch], { cwd: worktreePath });
3228
+ if (!pushResult.success && !pushResult.stderr.includes("Everything up-to-date")) {
3229
+ console.error(` Failed to push: ${pushResult.stderr}`);
3230
+ process.exit(1);
3231
+ }
3232
+ console.log(` Pushed to origin/${branch}`);
3233
+ const defaultBranch = run("git", ["symbolic-ref", "refs/remotes/origin/HEAD", "--short"], { cwd: worktreePath });
3234
+ const baseBranch = defaultBranch.success ? defaultBranch.stdout.trim().replace("origin/", "") : "main";
3235
+ const commitsResult = run("git", ["log", `origin/${baseBranch}..HEAD`, "--pretty=format:%s"], { cwd: worktreePath });
3236
+ const commits = commitsResult.success ? commitsResult.stdout.trim().split(`
3237
+ `).filter(Boolean) : [];
3238
+ const diffResult = run("git", ["diff", `origin/${baseBranch}..HEAD`], { cwd: worktreePath });
3239
+ const diff = diffResult.success ? diffResult.stdout : "";
3240
+ const diffStatResult = run("git", ["diff", `origin/${baseBranch}..HEAD`, "--shortstat"], { cwd: worktreePath });
3241
+ const diffStat = diffStatResult.success ? diffStatResult.stdout.trim() : "";
3242
+ const changedFilesResult = run("git", ["diff", `origin/${baseBranch}..HEAD`, "--name-only"], { cwd: worktreePath });
3243
+ const changedFiles = changedFilesResult.success ? changedFilesResult.stdout.trim().split(`
3244
+ `).filter(Boolean) : [];
3245
+ let title = options.title;
3246
+ let aiBody = "";
3247
+ if (options.ai || !title && diff.length > 0) {
3248
+ console.log(`
3249
+ \uD83E\uDD16 Generating PR content with AI...`);
3250
+ const aiResult = generateWithAI(diff, commits, worktreePath);
3251
+ if (aiResult) {
3252
+ if (!title)
3253
+ title = aiResult.title;
3254
+ aiBody = aiResult.body;
3255
+ console.log(` Generated title: ${title}`);
3256
+ } else if (options.ai) {
3257
+ console.log(` AI generation failed, using smart fallback`);
3258
+ }
3259
+ }
3260
+ if (!title) {
3261
+ title = generateSmartTitle(changedFiles, commits, featureName || branch.replace(/^feature\//, ""));
3262
+ }
3211
3263
  let body = options.body || "";
3212
- if (allocation && allocation.publicUrl) {
3264
+ if (allocation?.publicUrl) {
3213
3265
  body += `## Preview
3214
3266
  \uD83D\uDD17 ${allocation.publicUrl}
3215
3267
 
@@ -3221,7 +3273,12 @@ function generateSmartTitle(files, commits, featureName) {
3221
3273
 
3222
3274
  `;
3223
3275
  }
3224
- if (commits.length > 0) {
3276
+ if (aiBody) {
3277
+ body += `## Changes
3278
+ ${aiBody}
3279
+
3280
+ `;
3281
+ } else if (commits.length > 0) {
3225
3282
  body += `## Changes
3226
3283
  `;
3227
3284
  commits.forEach((commit) => {
@@ -3258,6 +3315,7 @@ ${diffStat}
3258
3315
  }
3259
3316
  console.log(`
3260
3317
  \uD83D\uDCDD Creating pull request...`);
3318
+ console.log(` Title: ${title}`);
3261
3319
  const prArgs = ["pr", "create", "--title", title, "--body", body];
3262
3320
  if (options.draft) {
3263
3321
  prArgs.push("--draft");
@@ -3308,32 +3366,32 @@ var __dirname2 = dirname(fileURLToPath(import.meta.url));
3308
3366
  var pkg = JSON.parse(readFileSync5(join4(__dirname2, "..", "package.json"), "utf-8"));
3309
3367
  var program2 = new Command;
3310
3368
  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) => {
3369
+ 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
3370
  await create(name, {
3313
- convex: options2.convex,
3314
- flat: options2.flat
3371
+ convex: options.convex,
3372
+ flat: options.flat
3315
3373
  });
3316
3374
  });
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);
3375
+ 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) => {
3376
+ await init(path, options);
3319
3377
  });
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
3378
+ 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) => {
3379
+ await start(feature, options.project, {
3380
+ port: options.port,
3381
+ local: options.local,
3382
+ seed: options.seed,
3383
+ baseBranch: options.baseBranch,
3384
+ webDir: options.webDir
3327
3385
  });
3328
3386
  });
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);
3387
+ 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) => {
3388
+ await stop(feature, options);
3331
3389
  });
3332
- program2.command("list").description("List active features and their status").option("--json", "Output as JSON").action(async (options2) => {
3333
- await list(options2);
3390
+ program2.command("list").description("List active features and their status").option("--json", "Output as JSON").action(async (options) => {
3391
+ await list(options);
3334
3392
  });
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);
3393
+ 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) => {
3394
+ await clean(feature, options);
3337
3395
  });
3338
3396
  var seedCmd = program2.command("seed").description("Manage seed data for projects");
3339
3397
  seedCmd.command("export [path]").description("Export current Convex data as seed").action(async (path) => {
@@ -3342,15 +3400,16 @@ seedCmd.command("export [path]").description("Export current Convex data as seed
3342
3400
  seedCmd.command("import [path]").description("Import seed data into current worktree").action(async (path) => {
3343
3401
  await seedImport(path);
3344
3402
  });
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);
3403
+ 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) => {
3404
+ await configCmd(options);
3347
3405
  });
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
3406
+ 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) => {
3407
+ await pr(feature, options.project, {
3408
+ title: options.title,
3409
+ body: options.body,
3410
+ draft: options.draft,
3411
+ web: options.web,
3412
+ ai: options.ai
3354
3413
  });
3355
3414
  });
3356
3415
  program2.command("status").description("Show zdev status (alias for list)").action(async () => {