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 +131 -21
- package/package.json +1 -1
- package/src/commands/pr.ts +160 -47
- package/src/index.ts +2 -0
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
|
|
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
|
-
|
|
3135
|
-
|
|
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
|
|
3148
|
-
const
|
|
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
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
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
|
|
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 (
|
|
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
package/src/commands/pr.ts
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
-
|
|
98
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
116
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
title =
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
143
|
-
if (allocation
|
|
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
|
|
152
|
-
if (
|
|
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
|
|
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
|
|