zdev 0.2.1 → 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
@@ -3073,6 +3073,101 @@ Commands:`);
3073
3073
 
3074
3074
  // src/commands/pr.ts
3075
3075
  import { resolve as resolve8, basename as basename4 } from "path";
3076
+ function generateWithAI(diff, commits, worktreePath) {
3077
+ const claudeCheck = run("which", ["claude"]);
3078
+ if (!claudeCheck.success) {
3079
+ return null;
3080
+ }
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;
3106
+ }
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;
3112
+ }
3113
+ return {
3114
+ title: titleMatch[1].trim().replace(/^["']|["']$/g, ""),
3115
+ body: bodyMatch ? bodyMatch[1].trim() : ""
3116
+ };
3117
+ }
3118
+ function generateSmartTitle(files, commits, featureName) {
3119
+ const components = new Set;
3120
+ const areas = new Set;
3121
+ for (const file of files) {
3122
+ if (!file.match(/\.(tsx?|jsx?|css|scss)$/))
3123
+ continue;
3124
+ const componentMatch = file.match(/components\/([^/]+)\/([^/]+)\.(tsx?|jsx?)$/);
3125
+ if (componentMatch) {
3126
+ components.add(componentMatch[2].replace(/\.(tsx?|jsx?)$/, ""));
3127
+ continue;
3128
+ }
3129
+ const singleComponent = file.match(/components\/([^/]+)\.(tsx?|jsx?)$/);
3130
+ if (singleComponent) {
3131
+ components.add(singleComponent[1]);
3132
+ continue;
3133
+ }
3134
+ const routeMatch = file.match(/routes\/(.+)\.(tsx?|jsx?)$/);
3135
+ if (routeMatch) {
3136
+ const routeName = routeMatch[1].replace(/[[\]$_.]/g, " ").trim();
3137
+ if (routeName && routeName !== "index") {
3138
+ areas.add(routeName);
3139
+ }
3140
+ continue;
3141
+ }
3142
+ const pathParts = file.split("/");
3143
+ if (pathParts.length > 1) {
3144
+ const folder = pathParts[pathParts.length - 2];
3145
+ if (!["src", "web", "app", "lib", "utils"].includes(folder)) {
3146
+ areas.add(folder);
3147
+ }
3148
+ }
3149
+ }
3150
+ const items = [...components, ...areas].slice(0, 3);
3151
+ if (items.length > 0) {
3152
+ let action = "Update";
3153
+ const commitText = commits.join(" ").toLowerCase();
3154
+ if (commitText.includes("fix"))
3155
+ action = "Fix";
3156
+ else if (commitText.includes("add") || commitText.includes("new"))
3157
+ action = "Add";
3158
+ else if (commitText.includes("refactor"))
3159
+ action = "Refactor";
3160
+ else if (commitText.includes("improve") || commitText.includes("enhance"))
3161
+ action = "Improve";
3162
+ else if (commitText.includes("mobile") || commitText.includes("responsive"))
3163
+ action = "Improve";
3164
+ return `${action} ${items.join(", ")}`;
3165
+ }
3166
+ if (commits.length > 0 && commits[0].length < 72) {
3167
+ return commits[0];
3168
+ }
3169
+ return featureName.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
3170
+ }
3076
3171
  async function pr(featureName, projectPath = ".", options = {}) {
3077
3172
  const fullPath = resolve8(projectPath);
3078
3173
  let worktreePath = fullPath;
@@ -3083,7 +3178,7 @@ async function pr(featureName, projectPath = ".", options = {}) {
3083
3178
  const allocKey = `${projectName}-${featureName}`;
3084
3179
  allocation = config.allocations[allocKey];
3085
3180
  if (!allocation) {
3086
- const found = Object.entries(config.allocations).find(([key, alloc]) => key.endsWith(`-${featureName}`));
3181
+ const found = Object.entries(config.allocations).find(([key]) => key.endsWith(`-${featureName}`));
3087
3182
  if (found) {
3088
3183
  allocation = found[1];
3089
3184
  projectName = allocation.project;
@@ -3130,35 +3225,43 @@ async function pr(featureName, projectPath = ".", options = {}) {
3130
3225
  console.log(`
3131
3226
  \uD83D\uDCE4 Pushing branch...`);
3132
3227
  const pushResult = run("git", ["push", "-u", "origin", branch], { cwd: worktreePath });
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/${branch}`);
3228
+ if (!pushResult.success && !pushResult.stderr.includes("Everything up-to-date")) {
3229
+ console.error(` Failed to push: ${pushResult.stderr}`);
3230
+ process.exit(1);
3141
3231
  }
3232
+ console.log(` Pushed to origin/${branch}`);
3142
3233
  const defaultBranch = run("git", ["symbolic-ref", "refs/remotes/origin/HEAD", "--short"], { cwd: worktreePath });
3143
3234
  const baseBranch = defaultBranch.success ? defaultBranch.stdout.trim().replace("origin/", "") : "main";
3144
3235
  const commitsResult = run("git", ["log", `origin/${baseBranch}..HEAD`, "--pretty=format:%s"], { cwd: worktreePath });
3145
3236
  const commits = commitsResult.success ? commitsResult.stdout.trim().split(`
3146
3237
  `).filter(Boolean) : [];
3147
- const filesResult = run("git", ["diff", `origin/${baseBranch}..HEAD`, "--stat", "--stat-width=60"], { cwd: worktreePath });
3148
- const filesSummary = filesResult.success ? filesResult.stdout.trim() : "";
3238
+ const diffResult = run("git", ["diff", `origin/${baseBranch}..HEAD`], { cwd: worktreePath });
3239
+ const diff = diffResult.success ? diffResult.stdout : "";
3149
3240
  const diffStatResult = run("git", ["diff", `origin/${baseBranch}..HEAD`, "--shortstat"], { cwd: worktreePath });
3150
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) : [];
3151
3245
  let title = options.title;
3152
- if (!title) {
3153
- if (commits.length > 0) {
3154
- title = commits[0];
3155
- } else {
3156
- const featureForTitle = featureName || branch.replace(/^feature\//, "");
3157
- title = featureForTitle.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
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`);
3158
3258
  }
3159
3259
  }
3260
+ if (!title) {
3261
+ title = generateSmartTitle(changedFiles, commits, featureName || branch.replace(/^feature\//, ""));
3262
+ }
3160
3263
  let body = options.body || "";
3161
- if (allocation && allocation.publicUrl) {
3264
+ if (allocation?.publicUrl) {
3162
3265
  body += `## Preview
3163
3266
  \uD83D\uDD17 ${allocation.publicUrl}
3164
3267
 
@@ -3170,7 +3273,12 @@ async function pr(featureName, projectPath = ".", options = {}) {
3170
3273
 
3171
3274
  `;
3172
3275
  }
3173
- if (commits.length > 0) {
3276
+ if (aiBody) {
3277
+ body += `## Changes
3278
+ ${aiBody}
3279
+
3280
+ `;
3281
+ } else if (commits.length > 0) {
3174
3282
  body += `## Changes
3175
3283
  `;
3176
3284
  commits.forEach((commit) => {
@@ -3207,6 +3315,7 @@ ${diffStat}
3207
3315
  }
3208
3316
  console.log(`
3209
3317
  \uD83D\uDCDD Creating pull request...`);
3318
+ console.log(` Title: ${title}`);
3210
3319
  const prArgs = ["pr", "create", "--title", title, "--body", body];
3211
3320
  if (options.draft) {
3212
3321
  prArgs.push("--draft");
@@ -3294,12 +3403,13 @@ seedCmd.command("import [path]").description("Import seed data into current work
3294
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) => {
3295
3404
  await configCmd(options);
3296
3405
  });
3297
- 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, options) => {
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) => {
3298
3407
  await pr(feature, options.project, {
3299
3408
  title: options.title,
3300
3409
  body: options.body,
3301
3410
  draft: options.draft,
3302
- web: options.web
3411
+ web: options.web,
3412
+ ai: options.ai
3303
3413
  });
3304
3414
  });
3305
3415
  program2.command("status").description("Show zdev status (alias for list)").action(async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zdev",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Multi-agent worktree development environment for cloud dev with preview URLs",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,6 +8,123 @@ export interface PrOptions {
8
8
  body?: string;
9
9
  draft?: boolean;
10
10
  web?: boolean;
11
+ ai?: boolean; // Use AI (Claude) to generate title/body
12
+ }
13
+
14
+ /**
15
+ * Use Claude to generate PR title and description from diff
16
+ */
17
+ function generateWithAI(diff: string, commits: string[], worktreePath: string): { title: string; body: string } | null {
18
+ // Check if claude CLI is available
19
+ const claudeCheck = run("which", ["claude"]);
20
+ if (!claudeCheck.success) {
21
+ return null;
22
+ }
23
+
24
+ const prompt = `You are generating a GitHub PR title and description.
25
+
26
+ Based on the following git diff and commit messages, generate:
27
+ 1. A concise PR title (max 72 chars, no quotes)
28
+ 2. A brief description of the changes (2-4 bullet points)
29
+
30
+ Commit messages:
31
+ ${commits.map(c => `- ${c}`).join("\n")}
32
+
33
+ Diff summary (first 3000 chars):
34
+ ${diff.slice(0, 3000)}
35
+
36
+ Respond in this exact format:
37
+ TITLE: <your title here>
38
+ BODY:
39
+ - <bullet point 1>
40
+ - <bullet point 2>
41
+ - <bullet point 3>`;
42
+
43
+ const result = run("claude", ["-p", prompt, "--no-input"], {
44
+ cwd: worktreePath,
45
+ env: { ...process.env, CLAUDE_CODE_ENTRYPOINT: "zdev" }
46
+ });
47
+
48
+ if (!result.success || !result.stdout) {
49
+ return null;
50
+ }
51
+
52
+ const output = result.stdout.trim();
53
+ const titleMatch = output.match(/TITLE:\s*(.+)/);
54
+ const bodyMatch = output.match(/BODY:\s*([\s\S]+)/);
55
+
56
+ if (!titleMatch) {
57
+ return null;
58
+ }
59
+
60
+ return {
61
+ title: titleMatch[1].trim().replace(/^["']|["']$/g, ""),
62
+ body: bodyMatch ? bodyMatch[1].trim() : "",
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Generate a smart PR title based on changed files and commits
68
+ */
69
+ function generateSmartTitle(files: string[], commits: string[], featureName: string): string {
70
+ const components = new Set<string>();
71
+ const areas = new Set<string>();
72
+
73
+ for (const file of files) {
74
+ if (!file.match(/\.(tsx?|jsx?|css|scss)$/)) continue;
75
+
76
+ const componentMatch = file.match(/components\/([^/]+)\/([^/]+)\.(tsx?|jsx?)$/);
77
+ if (componentMatch) {
78
+ components.add(componentMatch[2].replace(/\.(tsx?|jsx?)$/, ""));
79
+ continue;
80
+ }
81
+
82
+ const singleComponent = file.match(/components\/([^/]+)\.(tsx?|jsx?)$/);
83
+ if (singleComponent) {
84
+ components.add(singleComponent[1]);
85
+ continue;
86
+ }
87
+
88
+ const routeMatch = file.match(/routes\/(.+)\.(tsx?|jsx?)$/);
89
+ if (routeMatch) {
90
+ const routeName = routeMatch[1].replace(/[[\]$_.]/g, " ").trim();
91
+ if (routeName && routeName !== "index") {
92
+ areas.add(routeName);
93
+ }
94
+ continue;
95
+ }
96
+
97
+ const pathParts = file.split("/");
98
+ if (pathParts.length > 1) {
99
+ const folder = pathParts[pathParts.length - 2];
100
+ if (!["src", "web", "app", "lib", "utils"].includes(folder)) {
101
+ areas.add(folder);
102
+ }
103
+ }
104
+ }
105
+
106
+ const items = [...components, ...areas].slice(0, 3);
107
+
108
+ if (items.length > 0) {
109
+ let action = "Update";
110
+ const commitText = commits.join(" ").toLowerCase();
111
+ if (commitText.includes("fix")) action = "Fix";
112
+ else if (commitText.includes("add") || commitText.includes("new")) action = "Add";
113
+ else if (commitText.includes("refactor")) action = "Refactor";
114
+ else if (commitText.includes("improve") || commitText.includes("enhance")) action = "Improve";
115
+ else if (commitText.includes("mobile") || commitText.includes("responsive")) action = "Improve";
116
+
117
+ return `${action} ${items.join(", ")}`;
118
+ }
119
+
120
+ if (commits.length > 0 && commits[0].length < 72) {
121
+ return commits[0];
122
+ }
123
+
124
+ return featureName
125
+ .split(/[-_]/)
126
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
127
+ .join(" ");
11
128
  }
12
129
 
13
130
  export async function pr(
@@ -17,22 +134,20 @@ export async function pr(
17
134
  ): Promise<void> {
18
135
  const fullPath = resolve(projectPath);
19
136
 
20
- // Check if we're in a worktree or need to find one
21
137
  let worktreePath = fullPath;
22
138
  let allocation: WorktreeAllocation | undefined;
23
139
  let projectName = getRepoName(fullPath) || basename(fullPath);
24
140
 
25
141
  const config = loadConfig();
26
142
 
27
- // If featureName provided, find the allocation
143
+ // Find allocation if featureName provided
28
144
  if (featureName) {
29
145
  const allocKey = `${projectName}-${featureName}`;
30
146
  allocation = config.allocations[allocKey];
31
147
 
32
148
  if (!allocation) {
33
- // Try to find by just feature name
34
149
  const found = Object.entries(config.allocations).find(
35
- ([key, alloc]) => key.endsWith(`-${featureName}`)
150
+ ([key]) => key.endsWith(`-${featureName}`)
36
151
  );
37
152
  if (found) {
38
153
  allocation = found[1];
@@ -44,7 +159,7 @@ export async function pr(
44
159
  worktreePath = allocation.worktreePath || resolve(config.worktreesDir, `${projectName}-${featureName}`);
45
160
  }
46
161
  } else {
47
- // Try to detect from current directory
162
+ // Detect from current directory
48
163
  const cwd = process.cwd();
49
164
  const found = Object.entries(config.allocations).find(
50
165
  ([_, alloc]) => alloc.worktreePath === cwd || cwd.startsWith(alloc.worktreePath || "")
@@ -76,80 +191,82 @@ export async function pr(
76
191
  console.log(` Feature: ${featureName}`);
77
192
  }
78
193
 
79
- // Check if gh CLI is available
194
+ // Check gh CLI
80
195
  const ghCheck = run("which", ["gh"]);
81
196
  if (!ghCheck.success) {
82
197
  console.error("āŒ GitHub CLI (gh) not found. Install: https://cli.github.com");
83
198
  process.exit(1);
84
199
  }
85
200
 
86
- // Check if authenticated
87
201
  const authCheck = run("gh", ["auth", "status"], { cwd: worktreePath });
88
202
  if (!authCheck.success) {
89
203
  console.error("āŒ Not authenticated with GitHub. Run: gh auth login");
90
204
  process.exit(1);
91
205
  }
92
206
 
93
- // Push branch if not pushed
207
+ // Push branch
94
208
  console.log(`\nšŸ“¤ Pushing branch...`);
95
209
  const pushResult = run("git", ["push", "-u", "origin", branch], { cwd: worktreePath });
96
- if (!pushResult.success) {
97
- // Check if it's just "already up to date"
98
- if (!pushResult.stderr.includes("Everything up-to-date")) {
99
- console.error(` Failed to push: ${pushResult.stderr}`);
100
- process.exit(1);
101
- }
102
- console.log(` Already up to date`);
103
- } else {
104
- console.log(` Pushed to origin/${branch}`);
210
+ if (!pushResult.success && !pushResult.stderr.includes("Everything up-to-date")) {
211
+ console.error(` Failed to push: ${pushResult.stderr}`);
212
+ process.exit(1);
105
213
  }
214
+ console.log(` Pushed to origin/${branch}`);
106
215
 
107
- // Get base branch for comparison
216
+ // Get base branch
108
217
  const defaultBranch = run("git", ["symbolic-ref", "refs/remotes/origin/HEAD", "--short"], { cwd: worktreePath });
109
218
  const baseBranch = defaultBranch.success ? defaultBranch.stdout.trim().replace("origin/", "") : "main";
110
219
 
111
- // Get commits since base branch
220
+ // Get commits and diff info
112
221
  const commitsResult = run("git", ["log", `origin/${baseBranch}..HEAD`, "--pretty=format:%s"], { cwd: worktreePath });
113
222
  const commits = commitsResult.success ? commitsResult.stdout.trim().split("\n").filter(Boolean) : [];
114
223
 
115
- // Get files changed
116
- const filesResult = run("git", ["diff", `origin/${baseBranch}..HEAD`, "--stat", "--stat-width=60"], { cwd: worktreePath });
117
- const filesSummary = filesResult.success ? filesResult.stdout.trim() : "";
224
+ const diffResult = run("git", ["diff", `origin/${baseBranch}..HEAD`], { cwd: worktreePath });
225
+ const diff = diffResult.success ? diffResult.stdout : "";
118
226
 
119
- // Get short diff summary
120
227
  const diffStatResult = run("git", ["diff", `origin/${baseBranch}..HEAD`, "--shortstat"], { cwd: worktreePath });
121
228
  const diffStat = diffStatResult.success ? diffStatResult.stdout.trim() : "";
122
229
 
123
- // Build PR title
230
+ const changedFilesResult = run("git", ["diff", `origin/${baseBranch}..HEAD`, "--name-only"], { cwd: worktreePath });
231
+ const changedFiles = changedFilesResult.success ? changedFilesResult.stdout.trim().split("\n").filter(Boolean) : [];
232
+
233
+ // Generate title and body
124
234
  let title = options.title;
125
- if (!title) {
126
- if (commits.length > 0) {
127
- // Use first commit message as title
128
- title = commits[0];
129
- } else {
130
- // Fallback to feature name
131
- const featureForTitle = featureName || branch.replace(/^feature\//, "");
132
- title = featureForTitle
133
- .split(/[-_]/)
134
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
135
- .join(" ");
235
+ let aiBody = "";
236
+
237
+ // Try AI generation if --ai flag or no title provided
238
+ if (options.ai || (!title && diff.length > 0)) {
239
+ console.log(`\nšŸ¤– Generating PR content with AI...`);
240
+ const aiResult = generateWithAI(diff, commits, worktreePath);
241
+ if (aiResult) {
242
+ if (!title) title = aiResult.title;
243
+ aiBody = aiResult.body;
244
+ console.log(` Generated title: ${title}`);
245
+ } else if (options.ai) {
246
+ console.log(` AI generation failed, using smart fallback`);
136
247
  }
137
248
  }
138
249
 
250
+ // Fallback to smart title
251
+ if (!title) {
252
+ title = generateSmartTitle(changedFiles, commits, featureName || branch.replace(/^feature\//, ""));
253
+ }
254
+
139
255
  // Build PR body
140
256
  let body = options.body || "";
141
257
 
142
- // Add preview URL if we have allocation
143
- if (allocation && allocation.publicUrl) {
258
+ // Add preview URL
259
+ if (allocation?.publicUrl) {
144
260
  body += `## Preview\nšŸ”— ${allocation.publicUrl}\n\n`;
145
261
  } else if (config.devDomain && featureName && projectName) {
146
- // Try to construct preview URL
147
262
  const previewUrl = `https://${projectName}-${featureName}.${config.devDomain}`;
148
263
  body += `## Preview\nšŸ”— ${previewUrl}\n\n`;
149
264
  }
150
265
 
151
- // Add commits section if we have commits
152
- if (commits.length > 0) {
266
+ // Add AI-generated body or commits
267
+ if (aiBody) {
268
+ body += `## Changes\n${aiBody}\n\n`;
269
+ } else if (commits.length > 0) {
153
270
  body += `## Changes\n`;
154
271
  commits.forEach((commit) => {
155
272
  body += `- ${commit}\n`;
@@ -157,7 +274,7 @@ export async function pr(
157
274
  body += "\n";
158
275
  }
159
276
 
160
- // Add files changed
277
+ // Add stats
161
278
  if (diffStat) {
162
279
  body += `## Summary\n\`\`\`\n${diffStat}\n\`\`\`\n\n`;
163
280
  }
@@ -182,6 +299,7 @@ export async function pr(
182
299
 
183
300
  // Create PR
184
301
  console.log(`\nšŸ“ Creating pull request...`);
302
+ console.log(` Title: ${title}`);
185
303
 
186
304
  const prArgs = ["pr", "create", "--title", title, "--body", body];
187
305
 
@@ -203,7 +321,6 @@ export async function pr(
203
321
  const prResult = run("gh", prArgs, { cwd: worktreePath });
204
322
 
205
323
  if (!prResult.success) {
206
- // Check if it's because PR already exists
207
324
  if (prResult.stderr.includes("already exists")) {
208
325
  console.log(` PR already exists for this branch`);
209
326
  const viewResult = run("gh", ["pr", "view", "--json", "url"], { cwd: worktreePath });
@@ -211,9 +328,7 @@ export async function pr(
211
328
  try {
212
329
  const prData = JSON.parse(viewResult.stdout);
213
330
  console.log(`\nšŸ”— ${prData.url}`);
214
- } catch {
215
- // Ignore
216
- }
331
+ } catch {}
217
332
  }
218
333
  return;
219
334
  }
@@ -221,13 +336,11 @@ export async function pr(
221
336
  process.exit(1);
222
337
  }
223
338
 
224
- // Extract PR URL from output
225
339
  const prUrl = prResult.stdout.trim();
226
340
 
227
341
  console.log(`\nāœ… Pull request created!`);
228
342
  console.log(`\nšŸ”— ${prUrl}`);
229
343
 
230
- // Show preview URL again for easy access
231
344
  if (allocation?.publicUrl) {
232
345
  console.log(`\nšŸ“± Preview: ${allocation.publicUrl}`);
233
346
  }
package/src/index.ts CHANGED
@@ -134,12 +134,14 @@ program
134
134
  .option("-b, --body <body>", "PR body (preview URL auto-added)")
135
135
  .option("-d, --draft", "Create as draft PR")
136
136
  .option("-w, --web", "Open in browser instead of CLI")
137
+ .option("--ai", "Use Claude to generate title and description from diff")
137
138
  .action(async (feature, options) => {
138
139
  await pr(feature, options.project, {
139
140
  title: options.title,
140
141
  body: options.body,
141
142
  draft: options.draft,
142
143
  web: options.web,
144
+ ai: options.ai,
143
145
  });
144
146
  });
145
147