@camaradesuk/git-worktree-tools 1.0.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/LICENSE +21 -0
- package/README.md +259 -0
- package/dist/cli/cleanpr.d.ts +13 -0
- package/dist/cli/cleanpr.d.ts.map +1 -0
- package/dist/cli/cleanpr.js +441 -0
- package/dist/cli/cleanpr.js.map +1 -0
- package/dist/cli/lswt.d.ts +11 -0
- package/dist/cli/lswt.d.ts.map +1 -0
- package/dist/cli/lswt.js +313 -0
- package/dist/cli/lswt.js.map +1 -0
- package/dist/cli/newpr.d.ts +11 -0
- package/dist/cli/newpr.d.ts.map +1 -0
- package/dist/cli/newpr.js +888 -0
- package/dist/cli/newpr.js.map +1 -0
- package/dist/cli/wtlink.d.ts +15 -0
- package/dist/cli/wtlink.d.ts.map +1 -0
- package/dist/cli/wtlink.js +124 -0
- package/dist/cli/wtlink.js.map +1 -0
- package/dist/e2e/cli.e2e.test.d.ts +2 -0
- package/dist/e2e/cli.e2e.test.d.ts.map +1 -0
- package/dist/e2e/cli.e2e.test.js +215 -0
- package/dist/e2e/cli.e2e.test.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/integration/git.integration.test.d.ts +2 -0
- package/dist/integration/git.integration.test.d.ts.map +1 -0
- package/dist/integration/git.integration.test.js +333 -0
- package/dist/integration/git.integration.test.js.map +1 -0
- package/dist/lib/colors.d.ts +59 -0
- package/dist/lib/colors.d.ts.map +1 -0
- package/dist/lib/colors.js +145 -0
- package/dist/lib/colors.js.map +1 -0
- package/dist/lib/colors.test.d.ts +2 -0
- package/dist/lib/colors.test.d.ts.map +1 -0
- package/dist/lib/colors.test.js +69 -0
- package/dist/lib/colors.test.js.map +1 -0
- package/dist/lib/config.d.ts +58 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +91 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/config.test.d.ts +2 -0
- package/dist/lib/config.test.d.ts.map +1 -0
- package/dist/lib/config.test.js +84 -0
- package/dist/lib/config.test.js.map +1 -0
- package/dist/lib/constants.d.ts +37 -0
- package/dist/lib/constants.d.ts.map +1 -0
- package/dist/lib/constants.js +37 -0
- package/dist/lib/constants.js.map +1 -0
- package/dist/lib/errors.d.ts +88 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +112 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/errors.test.d.ts +2 -0
- package/dist/lib/errors.test.d.ts.map +1 -0
- package/dist/lib/errors.test.js +117 -0
- package/dist/lib/errors.test.js.map +1 -0
- package/dist/lib/git.d.ts +224 -0
- package/dist/lib/git.d.ts.map +1 -0
- package/dist/lib/git.js +524 -0
- package/dist/lib/git.js.map +1 -0
- package/dist/lib/git.test.d.ts +2 -0
- package/dist/lib/git.test.d.ts.map +1 -0
- package/dist/lib/git.test.js +402 -0
- package/dist/lib/git.test.js.map +1 -0
- package/dist/lib/github.d.ts +82 -0
- package/dist/lib/github.d.ts.map +1 -0
- package/dist/lib/github.js +254 -0
- package/dist/lib/github.js.map +1 -0
- package/dist/lib/github.test.d.ts +2 -0
- package/dist/lib/github.test.d.ts.map +1 -0
- package/dist/lib/github.test.js +258 -0
- package/dist/lib/github.test.js.map +1 -0
- package/dist/lib/prompts.d.ts +39 -0
- package/dist/lib/prompts.d.ts.map +1 -0
- package/dist/lib/prompts.js +213 -0
- package/dist/lib/prompts.js.map +1 -0
- package/dist/lib/prompts.test.d.ts +2 -0
- package/dist/lib/prompts.test.d.ts.map +1 -0
- package/dist/lib/prompts.test.js +250 -0
- package/dist/lib/prompts.test.js.map +1 -0
- package/dist/lib/state-detection.d.ts +65 -0
- package/dist/lib/state-detection.d.ts.map +1 -0
- package/dist/lib/state-detection.js +186 -0
- package/dist/lib/state-detection.js.map +1 -0
- package/dist/lib/state-detection.test.d.ts +2 -0
- package/dist/lib/state-detection.test.d.ts.map +1 -0
- package/dist/lib/state-detection.test.js +164 -0
- package/dist/lib/state-detection.test.js.map +1 -0
- package/dist/lib/wtlink/index.d.ts +5 -0
- package/dist/lib/wtlink/index.d.ts.map +1 -0
- package/dist/lib/wtlink/index.js +7 -0
- package/dist/lib/wtlink/index.js.map +1 -0
- package/dist/lib/wtlink/link-configs.d.ts +10 -0
- package/dist/lib/wtlink/link-configs.d.ts.map +1 -0
- package/dist/lib/wtlink/link-configs.js +411 -0
- package/dist/lib/wtlink/link-configs.js.map +1 -0
- package/dist/lib/wtlink/link-configs.test.d.ts +2 -0
- package/dist/lib/wtlink/link-configs.test.d.ts.map +1 -0
- package/dist/lib/wtlink/link-configs.test.js +179 -0
- package/dist/lib/wtlink/link-configs.test.js.map +1 -0
- package/dist/lib/wtlink/main-menu.d.ts +2 -0
- package/dist/lib/wtlink/main-menu.d.ts.map +1 -0
- package/dist/lib/wtlink/main-menu.js +149 -0
- package/dist/lib/wtlink/main-menu.js.map +1 -0
- package/dist/lib/wtlink/manage-manifest.d.ts +9 -0
- package/dist/lib/wtlink/manage-manifest.d.ts.map +1 -0
- package/dist/lib/wtlink/manage-manifest.js +1262 -0
- package/dist/lib/wtlink/manage-manifest.js.map +1 -0
- package/dist/lib/wtlink/validate-manifest.d.ts +6 -0
- package/dist/lib/wtlink/validate-manifest.d.ts.map +1 -0
- package/dist/lib/wtlink/validate-manifest.js +51 -0
- package/dist/lib/wtlink/validate-manifest.js.map +1 -0
- package/dist/lib/wtlink/validate-manifest.test.d.ts +2 -0
- package/dist/lib/wtlink/validate-manifest.test.d.ts.map +1 -0
- package/dist/lib/wtlink/validate-manifest.test.js +115 -0
- package/dist/lib/wtlink/validate-manifest.test.js.map +1 -0
- package/package.json +84 -0
|
@@ -0,0 +1,888 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* newpr - Create or setup a PR with a dedicated worktree
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* newpr "feature description" Create new branch + PR + worktree
|
|
7
|
+
* newpr --pr <NUMBER> Setup worktree for existing PR
|
|
8
|
+
* newpr --branch <NAME> Create PR for existing branch + worktree
|
|
9
|
+
*/
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import * as git from '../lib/git.js';
|
|
13
|
+
import * as github from '../lib/github.js';
|
|
14
|
+
import * as colors from '../lib/colors.js';
|
|
15
|
+
import { promptChoiceIndex } from '../lib/prompts.js';
|
|
16
|
+
import { loadConfig, generateBranchName, generateWorktreePath, } from '../lib/config.js';
|
|
17
|
+
import { analyzeGitState, detectScenario, } from '../lib/state-detection.js';
|
|
18
|
+
/**
|
|
19
|
+
* Parse command line arguments
|
|
20
|
+
*/
|
|
21
|
+
function parseArgs(args) {
|
|
22
|
+
const options = {
|
|
23
|
+
mode: 'new',
|
|
24
|
+
baseBranch: 'main',
|
|
25
|
+
draft: true,
|
|
26
|
+
installDeps: false,
|
|
27
|
+
openEditor: false,
|
|
28
|
+
runWtlink: true,
|
|
29
|
+
};
|
|
30
|
+
let i = 0;
|
|
31
|
+
while (i < args.length) {
|
|
32
|
+
const arg = args[i];
|
|
33
|
+
switch (arg) {
|
|
34
|
+
case '-h':
|
|
35
|
+
case '--help':
|
|
36
|
+
printHelp();
|
|
37
|
+
process.exit(0);
|
|
38
|
+
break;
|
|
39
|
+
case '--pr':
|
|
40
|
+
case '-p':
|
|
41
|
+
options.mode = 'pr';
|
|
42
|
+
i++;
|
|
43
|
+
if (!args[i] || args[i].startsWith('-')) {
|
|
44
|
+
console.error(colors.error('--pr requires a PR number'));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
options.prNumber = parseInt(args[i], 10);
|
|
48
|
+
if (isNaN(options.prNumber)) {
|
|
49
|
+
console.error(colors.error('PR number must be numeric'));
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
break;
|
|
53
|
+
case '--branch':
|
|
54
|
+
case '-B':
|
|
55
|
+
options.mode = 'branch';
|
|
56
|
+
i++;
|
|
57
|
+
if (!args[i] || args[i].startsWith('-')) {
|
|
58
|
+
console.error(colors.error('--branch requires a branch name'));
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
options.branchName = args[i];
|
|
62
|
+
break;
|
|
63
|
+
case '-b':
|
|
64
|
+
case '--base':
|
|
65
|
+
i++;
|
|
66
|
+
if (!args[i] || args[i].startsWith('-')) {
|
|
67
|
+
console.error(colors.error('--base requires a branch name'));
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
options.baseBranch = args[i];
|
|
71
|
+
break;
|
|
72
|
+
case '-i':
|
|
73
|
+
case '--install':
|
|
74
|
+
options.installDeps = true;
|
|
75
|
+
break;
|
|
76
|
+
case '-c':
|
|
77
|
+
case '--code':
|
|
78
|
+
options.openEditor = true;
|
|
79
|
+
break;
|
|
80
|
+
case '-r':
|
|
81
|
+
case '--ready':
|
|
82
|
+
options.draft = false;
|
|
83
|
+
break;
|
|
84
|
+
case '--no-wtlink':
|
|
85
|
+
options.runWtlink = false;
|
|
86
|
+
break;
|
|
87
|
+
default:
|
|
88
|
+
if (arg.startsWith('-')) {
|
|
89
|
+
console.error(colors.error(`Unknown option: ${arg}`));
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
// Positional argument = description
|
|
93
|
+
if (!options.description && options.mode === 'new') {
|
|
94
|
+
options.description = arg;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
console.error(colors.error(`Unexpected argument: ${arg}`));
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
i++;
|
|
102
|
+
}
|
|
103
|
+
// Validate
|
|
104
|
+
if (options.mode === 'new' && !options.description) {
|
|
105
|
+
console.error(colors.error('Description required. Usage: newpr "feature description"'));
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
return options;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Print help message
|
|
112
|
+
*/
|
|
113
|
+
function printHelp() {
|
|
114
|
+
console.log(`
|
|
115
|
+
${colors.bold('newpr')} - Create or setup a PR with a dedicated worktree
|
|
116
|
+
|
|
117
|
+
${colors.bold('Usage:')}
|
|
118
|
+
newpr "description" Create new branch + PR + worktree
|
|
119
|
+
newpr --pr <NUMBER> Setup worktree for existing PR
|
|
120
|
+
newpr --branch <NAME> Create PR for existing branch + worktree
|
|
121
|
+
|
|
122
|
+
${colors.bold('Options:')}
|
|
123
|
+
-b, --base BRANCH Base branch for PR (default: main)
|
|
124
|
+
-i, --install Install dependencies after setup
|
|
125
|
+
-c, --code Open editor to the new worktree
|
|
126
|
+
-r, --ready Create PR as ready for review (default: draft)
|
|
127
|
+
--no-wtlink Skip wtlink config sync
|
|
128
|
+
-h, --help Show this help message
|
|
129
|
+
|
|
130
|
+
${colors.bold('Examples:')}
|
|
131
|
+
newpr "Add user authentication"
|
|
132
|
+
newpr "Fix login bug" --install --code
|
|
133
|
+
newpr --pr 1234
|
|
134
|
+
newpr --branch feat/my-feature
|
|
135
|
+
`);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Check prerequisites
|
|
139
|
+
*/
|
|
140
|
+
function checkPrerequisites() {
|
|
141
|
+
console.log(colors.info('Checking prerequisites...'));
|
|
142
|
+
// Check gh CLI
|
|
143
|
+
if (!github.isGhInstalled()) {
|
|
144
|
+
console.error(colors.error('GitHub CLI (gh) is required. See: https://cli.github.com'));
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
// Check gh auth
|
|
148
|
+
if (!github.isAuthenticated()) {
|
|
149
|
+
console.error(colors.error('GitHub CLI not authenticated. Run: gh auth login'));
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
console.log(colors.success('Prerequisites OK'));
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Show local commits not in base branch
|
|
156
|
+
*/
|
|
157
|
+
function showLocalCommits(baseBranch, cwd) {
|
|
158
|
+
const commits = git.getCommitsAhead(baseBranch, cwd);
|
|
159
|
+
if (commits.length > 0) {
|
|
160
|
+
console.log();
|
|
161
|
+
for (const commit of commits.slice(0, 10)) {
|
|
162
|
+
console.log(` ${commit}`);
|
|
163
|
+
}
|
|
164
|
+
if (commits.length > 10) {
|
|
165
|
+
console.log(` ... and ${commits.length - 10} more commits`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Show uncommitted changes
|
|
171
|
+
*/
|
|
172
|
+
function showUncommittedChanges(cwd) {
|
|
173
|
+
const status = git.getStatusOutput(cwd);
|
|
174
|
+
if (status) {
|
|
175
|
+
console.log();
|
|
176
|
+
console.log(status);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Show staged changes
|
|
181
|
+
*/
|
|
182
|
+
function showStagedChanges(cwd) {
|
|
183
|
+
const files = git.getStagedFiles(cwd);
|
|
184
|
+
if (files.length > 0) {
|
|
185
|
+
console.log();
|
|
186
|
+
console.log('Staged:');
|
|
187
|
+
for (const file of files) {
|
|
188
|
+
console.log(` ${file}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Show unstaged changes
|
|
194
|
+
*/
|
|
195
|
+
function showUnstagedChanges(cwd) {
|
|
196
|
+
const files = git.getUnstagedFiles(cwd);
|
|
197
|
+
if (files.length > 0) {
|
|
198
|
+
console.log();
|
|
199
|
+
console.log('Unstaged:');
|
|
200
|
+
for (const file of files) {
|
|
201
|
+
console.log(` ${file}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Handle scenario and return action to take
|
|
207
|
+
*/
|
|
208
|
+
async function handleScenario(scenario, state, baseBranch) {
|
|
209
|
+
const defaultAction = {
|
|
210
|
+
action: 'empty_commit',
|
|
211
|
+
branchFrom: 'origin_main',
|
|
212
|
+
stashUnstaged: false,
|
|
213
|
+
};
|
|
214
|
+
switch (scenario) {
|
|
215
|
+
case 'main_clean_same': {
|
|
216
|
+
// Scenario 1: On main, same as origin/main, clean
|
|
217
|
+
console.log(colors.warning('No changes detected from main branch.'));
|
|
218
|
+
console.log();
|
|
219
|
+
console.log("You are on 'main' with no local commits or uncommitted changes.");
|
|
220
|
+
console.log('A PR requires at least one commit difference from the base branch.');
|
|
221
|
+
const choice = await promptChoiceIndex('How would you like to proceed?', [
|
|
222
|
+
'Continue with empty initial commit',
|
|
223
|
+
"Cancel - I'll make some changes first",
|
|
224
|
+
]);
|
|
225
|
+
if (choice === 1) {
|
|
226
|
+
return { action: 'empty_commit', branchFrom: 'origin_main', stashUnstaged: false };
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
case 'main_staged_same': {
|
|
231
|
+
// Scenario 2a: On main, same as origin/main, staged changes only
|
|
232
|
+
console.log(colors.info('You have staged changes ready to commit:'));
|
|
233
|
+
showStagedChanges();
|
|
234
|
+
const choice = await promptChoiceIndex('How would you like to proceed?', [
|
|
235
|
+
'Commit staged changes to the new PR branch',
|
|
236
|
+
'Leave changes here and continue with empty initial commit',
|
|
237
|
+
'Cancel',
|
|
238
|
+
]);
|
|
239
|
+
switch (choice) {
|
|
240
|
+
case 1:
|
|
241
|
+
return { action: 'commit_staged', branchFrom: 'origin_main', stashUnstaged: false };
|
|
242
|
+
case 2:
|
|
243
|
+
return { action: 'empty_commit', branchFrom: 'origin_main', stashUnstaged: false };
|
|
244
|
+
default:
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
case 'main_unstaged_same': {
|
|
249
|
+
// Scenario 2b: On main, same as origin/main, unstaged changes only
|
|
250
|
+
console.log(colors.info('You have unstaged changes:'));
|
|
251
|
+
showUncommittedChanges();
|
|
252
|
+
const choice = await promptChoiceIndex('How would you like to proceed?', [
|
|
253
|
+
'Stage all and commit to the new PR branch',
|
|
254
|
+
'Leave changes here and continue with empty initial commit',
|
|
255
|
+
'Stash changes (will restore after)',
|
|
256
|
+
'Cancel',
|
|
257
|
+
]);
|
|
258
|
+
switch (choice) {
|
|
259
|
+
case 1:
|
|
260
|
+
return { action: 'commit_all', branchFrom: 'origin_main', stashUnstaged: false };
|
|
261
|
+
case 2:
|
|
262
|
+
return { action: 'empty_commit', branchFrom: 'origin_main', stashUnstaged: false };
|
|
263
|
+
case 3:
|
|
264
|
+
return { action: 'stash_and_empty', branchFrom: 'origin_main', stashUnstaged: false };
|
|
265
|
+
default:
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
case 'main_both_same': {
|
|
270
|
+
// Scenario 2c: On main, same as origin/main, both staged and unstaged
|
|
271
|
+
console.log(colors.info('You have both staged and unstaged changes:'));
|
|
272
|
+
showStagedChanges();
|
|
273
|
+
showUnstagedChanges();
|
|
274
|
+
const choice = await promptChoiceIndex('How would you like to proceed?', [
|
|
275
|
+
'Commit staged to PR branch, move unstaged to new worktree',
|
|
276
|
+
'Stage all and commit everything to the new PR branch',
|
|
277
|
+
'Leave all changes here and continue with empty initial commit',
|
|
278
|
+
'Stash all changes (will restore after)',
|
|
279
|
+
'Cancel',
|
|
280
|
+
]);
|
|
281
|
+
switch (choice) {
|
|
282
|
+
case 1:
|
|
283
|
+
return { action: 'commit_staged', branchFrom: 'origin_main', stashUnstaged: true };
|
|
284
|
+
case 2:
|
|
285
|
+
return { action: 'commit_all', branchFrom: 'origin_main', stashUnstaged: false };
|
|
286
|
+
case 3:
|
|
287
|
+
return { action: 'empty_commit', branchFrom: 'origin_main', stashUnstaged: false };
|
|
288
|
+
case 4:
|
|
289
|
+
return { action: 'stash_and_empty', branchFrom: 'origin_main', stashUnstaged: false };
|
|
290
|
+
default:
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
case 'main_clean_ahead': {
|
|
295
|
+
// Scenario 3: On main, ahead of origin/main, clean
|
|
296
|
+
console.log(colors.info("You have local commits on 'main' not yet pushed:"));
|
|
297
|
+
showLocalCommits(baseBranch);
|
|
298
|
+
console.log();
|
|
299
|
+
console.log('These commits will NOT be included in the new PR branch by default.');
|
|
300
|
+
const choice = await promptChoiceIndex('How would you like to proceed?', [
|
|
301
|
+
'Use these commits for the PR (create branch from HEAD)',
|
|
302
|
+
'Push commits to origin/main first, then create PR branch',
|
|
303
|
+
'Start fresh from origin/main (ignore local commits)',
|
|
304
|
+
'Cancel',
|
|
305
|
+
]);
|
|
306
|
+
switch (choice) {
|
|
307
|
+
case 1:
|
|
308
|
+
return { action: 'use_commits', branchFrom: 'head', stashUnstaged: false };
|
|
309
|
+
case 2:
|
|
310
|
+
return { action: 'push_then_branch', branchFrom: 'origin_main', stashUnstaged: false };
|
|
311
|
+
case 3:
|
|
312
|
+
return { action: 'empty_commit', branchFrom: 'origin_main', stashUnstaged: false };
|
|
313
|
+
default:
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
case 'main_changes_ahead': {
|
|
318
|
+
// Scenario 4: On main, ahead of origin/main, has changes
|
|
319
|
+
console.log(colors.info('You have local commits AND uncommitted changes:'));
|
|
320
|
+
console.log();
|
|
321
|
+
console.log('Local commits (not pushed):');
|
|
322
|
+
showLocalCommits(baseBranch);
|
|
323
|
+
console.log();
|
|
324
|
+
console.log('Uncommitted changes:');
|
|
325
|
+
showUncommittedChanges();
|
|
326
|
+
const choice = await promptChoiceIndex('How would you like to proceed?', [
|
|
327
|
+
'Include commits + commit uncommitted changes to PR branch',
|
|
328
|
+
'Include commits only, stash uncommitted changes',
|
|
329
|
+
'Start fresh from origin/main (ignore all local work)',
|
|
330
|
+
'Cancel',
|
|
331
|
+
]);
|
|
332
|
+
switch (choice) {
|
|
333
|
+
case 1:
|
|
334
|
+
return { action: 'use_commits_and_commit_all', branchFrom: 'head', stashUnstaged: false };
|
|
335
|
+
case 2:
|
|
336
|
+
return { action: 'use_commits_and_stash', branchFrom: 'head', stashUnstaged: false };
|
|
337
|
+
case 3:
|
|
338
|
+
return { action: 'empty_commit', branchFrom: 'origin_main', stashUnstaged: false };
|
|
339
|
+
default:
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
case 'branch_same_as_main': {
|
|
344
|
+
// Scenario 5: On different branch, same commit as main
|
|
345
|
+
const branch = state.currentBranch || 'unknown';
|
|
346
|
+
console.log(colors.warning(`Branch '${branch}' is at the same commit as main.`));
|
|
347
|
+
console.log();
|
|
348
|
+
console.log('No divergent commits detected. A PR requires at least one commit difference.');
|
|
349
|
+
const choice = await promptChoiceIndex('How would you like to proceed?', [
|
|
350
|
+
'Continue with empty initial commit (new branch from main)',
|
|
351
|
+
'Cancel',
|
|
352
|
+
]);
|
|
353
|
+
if (choice === 1) {
|
|
354
|
+
return { action: 'empty_commit', branchFrom: 'origin_main', stashUnstaged: false };
|
|
355
|
+
}
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
case 'branch_ancestor': {
|
|
359
|
+
// Scenario 6: On different branch, already merged
|
|
360
|
+
const branch = state.currentBranch || 'unknown';
|
|
361
|
+
const shortSha = git.getShortCommit();
|
|
362
|
+
console.log(colors.warning(`Branch '${branch}' appears to be already merged into main.`));
|
|
363
|
+
console.log();
|
|
364
|
+
console.log(`Current commit (${shortSha}) is an ancestor of origin/main.`);
|
|
365
|
+
console.log('Creating a PR would result in no changes.');
|
|
366
|
+
const choice = await promptChoiceIndex('How would you like to proceed?', [
|
|
367
|
+
'Continue with empty initial commit (new branch from main)',
|
|
368
|
+
"Cancel - I'll check the branch status first",
|
|
369
|
+
]);
|
|
370
|
+
if (choice === 1) {
|
|
371
|
+
return { action: 'empty_commit', branchFrom: 'origin_main', stashUnstaged: false };
|
|
372
|
+
}
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
case 'branch_divergent': {
|
|
376
|
+
// Scenario 7: On different branch, divergent commits
|
|
377
|
+
const branch = state.currentBranch || 'unknown';
|
|
378
|
+
console.log(colors.info(`You are on branch '${branch}' with commits not in main:`));
|
|
379
|
+
showLocalCommits(baseBranch);
|
|
380
|
+
const choice = await promptChoiceIndex('How would you like to proceed?', [
|
|
381
|
+
`Create PR for THIS branch (${branch} → main)`,
|
|
382
|
+
"Create NEW branch from main (ignore current branch's commits)",
|
|
383
|
+
'Cancel',
|
|
384
|
+
]);
|
|
385
|
+
switch (choice) {
|
|
386
|
+
case 1:
|
|
387
|
+
return { action: 'create_pr_for_branch', branchFrom: 'head', stashUnstaged: false };
|
|
388
|
+
case 2:
|
|
389
|
+
return { action: 'empty_commit', branchFrom: 'origin_main', stashUnstaged: false };
|
|
390
|
+
default:
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
case 'branch_with_changes': {
|
|
395
|
+
// Scenario 8: On different branch with uncommitted changes
|
|
396
|
+
const branch = state.currentBranch || 'unknown';
|
|
397
|
+
console.log(colors.info(`You are on branch '${branch}' with uncommitted changes:`));
|
|
398
|
+
showUncommittedChanges();
|
|
399
|
+
// Check if branch has divergent commits
|
|
400
|
+
const hasDivergent = state.localCommits.length > 0;
|
|
401
|
+
if (hasDivergent) {
|
|
402
|
+
console.log();
|
|
403
|
+
console.log('Branch also has commits not in main:');
|
|
404
|
+
showLocalCommits(baseBranch);
|
|
405
|
+
const choice = await promptChoiceIndex('How would you like to proceed?', [
|
|
406
|
+
'Create PR for THIS branch, commit changes first',
|
|
407
|
+
'Create PR for THIS branch, stash uncommitted changes',
|
|
408
|
+
'Create NEW branch from main (ignore current branch)',
|
|
409
|
+
'Cancel',
|
|
410
|
+
]);
|
|
411
|
+
switch (choice) {
|
|
412
|
+
case 1:
|
|
413
|
+
return { action: 'pr_for_branch_commit_all', branchFrom: 'head', stashUnstaged: false };
|
|
414
|
+
case 2:
|
|
415
|
+
return { action: 'pr_for_branch_stash', branchFrom: 'head', stashUnstaged: false };
|
|
416
|
+
case 3:
|
|
417
|
+
return { action: 'empty_commit', branchFrom: 'origin_main', stashUnstaged: false };
|
|
418
|
+
default:
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
const choice = await promptChoiceIndex('How would you like to proceed?', [
|
|
424
|
+
'Stage all and commit to a new PR branch',
|
|
425
|
+
'Leave changes and continue with empty initial commit',
|
|
426
|
+
'Stash changes (will restore after)',
|
|
427
|
+
'Cancel',
|
|
428
|
+
]);
|
|
429
|
+
switch (choice) {
|
|
430
|
+
case 1:
|
|
431
|
+
return { action: 'commit_all', branchFrom: 'origin_main', stashUnstaged: false };
|
|
432
|
+
case 2:
|
|
433
|
+
return { action: 'empty_commit', branchFrom: 'origin_main', stashUnstaged: false };
|
|
434
|
+
case 3:
|
|
435
|
+
return { action: 'stash_and_empty', branchFrom: 'origin_main', stashUnstaged: false };
|
|
436
|
+
default:
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
case 'detached_head': {
|
|
442
|
+
// Scenario 9: Detached HEAD
|
|
443
|
+
const shortSha = git.getShortCommit();
|
|
444
|
+
console.log(colors.warning(`You are in detached HEAD state at commit ${shortSha}.`));
|
|
445
|
+
const choice = await promptChoiceIndex('How would you like to proceed?', [
|
|
446
|
+
'Create branch from this commit',
|
|
447
|
+
'Create branch from origin/main',
|
|
448
|
+
'Cancel',
|
|
449
|
+
]);
|
|
450
|
+
switch (choice) {
|
|
451
|
+
case 1:
|
|
452
|
+
return { action: 'branch_from_detached', branchFrom: 'head', stashUnstaged: false };
|
|
453
|
+
case 2:
|
|
454
|
+
return { action: 'empty_commit', branchFrom: 'origin_main', stashUnstaged: false };
|
|
455
|
+
default:
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
case 'pr_worktree': {
|
|
460
|
+
// Scenario 10: Running from PR worktree
|
|
461
|
+
console.log(colors.warning('You are in a PR worktree, not the main worktree.'));
|
|
462
|
+
console.log();
|
|
463
|
+
console.log('Creating a new PR is best done from the main worktree.');
|
|
464
|
+
const choice = await promptChoiceIndex('How would you like to proceed?', [
|
|
465
|
+
"Continue anyway (create PR from this worktree's state)",
|
|
466
|
+
"Cancel - I'll switch to the main worktree",
|
|
467
|
+
]);
|
|
468
|
+
if (choice === 1) {
|
|
469
|
+
// Analyze actual state and recurse
|
|
470
|
+
const newState = analyzeGitState(baseBranch);
|
|
471
|
+
const newScenario = detectScenario(newState);
|
|
472
|
+
return handleScenario(newScenario, newState, baseBranch);
|
|
473
|
+
}
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
default:
|
|
477
|
+
return defaultAction;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Execute state action
|
|
482
|
+
*/
|
|
483
|
+
function executeStateAction(action, description, branchName, cwd) {
|
|
484
|
+
let stashRef = null;
|
|
485
|
+
switch (action.action) {
|
|
486
|
+
case 'empty_commit':
|
|
487
|
+
// No action needed before branch creation
|
|
488
|
+
break;
|
|
489
|
+
case 'commit_staged':
|
|
490
|
+
// Will commit staged changes after creating branch
|
|
491
|
+
break;
|
|
492
|
+
case 'commit_all':
|
|
493
|
+
console.log(colors.info('Staging all changes...'));
|
|
494
|
+
git.add('.', cwd);
|
|
495
|
+
break;
|
|
496
|
+
case 'stash_and_empty':
|
|
497
|
+
console.log(colors.info('Stashing all changes...'));
|
|
498
|
+
stashRef = git.stash({ message: `newpr: auto-stash before creating ${branchName}` }, cwd);
|
|
499
|
+
break;
|
|
500
|
+
case 'use_commits':
|
|
501
|
+
case 'branch_from_detached':
|
|
502
|
+
// Branch from HEAD instead of origin/main
|
|
503
|
+
break;
|
|
504
|
+
case 'use_commits_and_commit_all':
|
|
505
|
+
console.log(colors.info('Staging all uncommitted changes...'));
|
|
506
|
+
git.add('.', cwd);
|
|
507
|
+
break;
|
|
508
|
+
case 'use_commits_and_stash':
|
|
509
|
+
console.log(colors.info('Stashing uncommitted changes...'));
|
|
510
|
+
stashRef = git.stash({ message: `newpr: auto-stash before creating ${branchName}` }, cwd);
|
|
511
|
+
break;
|
|
512
|
+
case 'push_then_branch':
|
|
513
|
+
console.log(colors.info('Pushing local commits to origin/main...'));
|
|
514
|
+
git.push({ remote: 'origin', branch: 'main' }, cwd);
|
|
515
|
+
break;
|
|
516
|
+
case 'pr_for_branch_commit_all':
|
|
517
|
+
console.log(colors.info('Staging and committing all changes to current branch...'));
|
|
518
|
+
git.add('.', cwd);
|
|
519
|
+
git.commit({ message: 'chore: work in progress\n\n🤖 Committed with newpr' }, cwd);
|
|
520
|
+
break;
|
|
521
|
+
case 'pr_for_branch_stash':
|
|
522
|
+
console.log(colors.info('Stashing uncommitted changes...'));
|
|
523
|
+
stashRef = git.stash({ message: 'newpr: auto-stash before creating PR' }, cwd);
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
526
|
+
return stashRef;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Setup worktree (symlinks, wtlink, deps)
|
|
530
|
+
*/
|
|
531
|
+
async function setupWorktree(worktreePath, config, options) {
|
|
532
|
+
const repoRoot = git.getRepoRoot();
|
|
533
|
+
const parentDir = path.dirname(repoRoot);
|
|
534
|
+
// Create symlinks for shared repos
|
|
535
|
+
if (config.sharedRepos.length > 0) {
|
|
536
|
+
console.log(colors.info('Creating symlinks for shared repositories...'));
|
|
537
|
+
for (const repo of config.sharedRepos) {
|
|
538
|
+
const target = path.join(parentDir, repo);
|
|
539
|
+
const link = path.join(worktreePath, repo);
|
|
540
|
+
if (fs.existsSync(target)) {
|
|
541
|
+
if (!fs.existsSync(link)) {
|
|
542
|
+
try {
|
|
543
|
+
fs.symlinkSync(target, link, 'dir');
|
|
544
|
+
console.log(colors.success(`Linked ${repo}`));
|
|
545
|
+
}
|
|
546
|
+
catch (error) {
|
|
547
|
+
console.log(colors.warning(`Failed to link ${repo}: ${error}`));
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
console.log(colors.warning(`${repo} already exists in worktree`));
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
console.log(colors.warning(`${repo} not found at ${target}`));
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
// TODO: Run wtlink if available
|
|
560
|
+
// if (options.runWtlink) { ... }
|
|
561
|
+
// TODO: Install dependencies if requested
|
|
562
|
+
// if (options.installDeps) { ... }
|
|
563
|
+
// TODO: Open editor if requested
|
|
564
|
+
// if (options.openEditor) { ... }
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Print summary
|
|
568
|
+
*/
|
|
569
|
+
function printSummary(prNumber, branchName, worktreePath, prUrl) {
|
|
570
|
+
console.log();
|
|
571
|
+
console.log(colors.green('════════════════════════════════════════════════════════════'));
|
|
572
|
+
console.log(colors.green(` PR #${prNumber} worktree ready!`));
|
|
573
|
+
console.log(colors.green('════════════════════════════════════════════════════════════'));
|
|
574
|
+
console.log();
|
|
575
|
+
console.log(` Branch: ${branchName}`);
|
|
576
|
+
console.log(` Worktree: ${worktreePath}`);
|
|
577
|
+
console.log(` PR URL: ${prUrl}`);
|
|
578
|
+
console.log();
|
|
579
|
+
console.log(' Next steps:');
|
|
580
|
+
console.log(` cd ${worktreePath}`);
|
|
581
|
+
console.log();
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Mode: Setup worktree for existing PR
|
|
585
|
+
*/
|
|
586
|
+
async function modeExistingPr(prNumber, options) {
|
|
587
|
+
console.log(colors.info(`Setting up worktree for existing PR #${prNumber}...`));
|
|
588
|
+
const repoRoot = git.getRepoRoot();
|
|
589
|
+
const repoName = git.getRepoName(repoRoot);
|
|
590
|
+
const config = loadConfig(repoRoot);
|
|
591
|
+
// Get PR info
|
|
592
|
+
const pr = github.getPr(prNumber);
|
|
593
|
+
if (!pr) {
|
|
594
|
+
console.error(colors.error(`Could not find PR #${prNumber}`));
|
|
595
|
+
process.exit(1);
|
|
596
|
+
}
|
|
597
|
+
if (pr.state !== 'OPEN') {
|
|
598
|
+
console.log(colors.warning(`PR #${prNumber} is ${pr.state}`));
|
|
599
|
+
}
|
|
600
|
+
console.log(colors.info(`PR branch: ${pr.headBranch}`));
|
|
601
|
+
// Generate worktree path
|
|
602
|
+
const worktreePath = generateWorktreePath(config, repoRoot, repoName, prNumber);
|
|
603
|
+
if (fs.existsSync(worktreePath)) {
|
|
604
|
+
console.error(colors.error(`Worktree already exists: ${worktreePath}`));
|
|
605
|
+
process.exit(1);
|
|
606
|
+
}
|
|
607
|
+
// Fetch the branch
|
|
608
|
+
console.log(colors.info('Fetching branch from origin...'));
|
|
609
|
+
git.fetch('origin');
|
|
610
|
+
// Create worktree
|
|
611
|
+
console.log(colors.info(`Creating worktree at ${worktreePath}...`));
|
|
612
|
+
try {
|
|
613
|
+
git.addWorktree(worktreePath, pr.headBranch, {
|
|
614
|
+
createBranch: true,
|
|
615
|
+
startPoint: `origin/${pr.headBranch}`,
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
catch {
|
|
619
|
+
// Branch might already exist locally
|
|
620
|
+
git.addWorktree(worktreePath, pr.headBranch);
|
|
621
|
+
}
|
|
622
|
+
console.log(colors.success(`Created worktree: ${worktreePath}`));
|
|
623
|
+
// Setup worktree
|
|
624
|
+
await setupWorktree(worktreePath, config, options);
|
|
625
|
+
// Print summary
|
|
626
|
+
printSummary(prNumber, pr.headBranch, worktreePath, pr.url);
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Mode: Create PR for existing branch
|
|
630
|
+
*/
|
|
631
|
+
async function modeExistingBranch(branchName, options) {
|
|
632
|
+
console.log(colors.info(`Creating PR for existing branch: ${branchName}...`));
|
|
633
|
+
const repoRoot = git.getRepoRoot();
|
|
634
|
+
const repoName = git.getRepoName(repoRoot);
|
|
635
|
+
const config = loadConfig(repoRoot);
|
|
636
|
+
// Fetch latest
|
|
637
|
+
console.log(colors.info('Fetching latest from origin...'));
|
|
638
|
+
git.fetch('origin');
|
|
639
|
+
// Check if branch exists on remote
|
|
640
|
+
if (!git.remoteBranchExists(branchName)) {
|
|
641
|
+
// Check if it exists locally
|
|
642
|
+
if (git.branchExists(branchName)) {
|
|
643
|
+
console.log(colors.info('Branch exists locally, pushing to origin...'));
|
|
644
|
+
git.push({ setUpstream: true, remote: 'origin', branch: branchName });
|
|
645
|
+
}
|
|
646
|
+
else {
|
|
647
|
+
console.error(colors.error(`Branch '${branchName}' does not exist locally or on remote`));
|
|
648
|
+
process.exit(1);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
// Check if PR already exists
|
|
652
|
+
const existingPr = github.getPrByBranch(branchName);
|
|
653
|
+
if (existingPr) {
|
|
654
|
+
console.log(colors.info(`PR #${existingPr.number} already exists for branch ${branchName}`));
|
|
655
|
+
await modeExistingPr(existingPr.number, options);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
// Create PR
|
|
659
|
+
console.log(colors.info('Creating pull request...'));
|
|
660
|
+
// Generate title from branch name
|
|
661
|
+
const title = branchName
|
|
662
|
+
.replace(/^(feat|fix|chore)\//, '')
|
|
663
|
+
.replace(/-/g, ' ')
|
|
664
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
665
|
+
const pr = github.createPr({
|
|
666
|
+
title,
|
|
667
|
+
body: `## Summary
|
|
668
|
+
|
|
669
|
+
PR created from existing branch: \`${branchName}\`
|
|
670
|
+
|
|
671
|
+
## Changes
|
|
672
|
+
|
|
673
|
+
-
|
|
674
|
+
|
|
675
|
+
## Test Plan
|
|
676
|
+
|
|
677
|
+
- [ ]
|
|
678
|
+
|
|
679
|
+
---
|
|
680
|
+
🤖 PR created with \`newpr --branch\``,
|
|
681
|
+
base: options.baseBranch,
|
|
682
|
+
head: branchName,
|
|
683
|
+
draft: options.draft,
|
|
684
|
+
});
|
|
685
|
+
console.log(colors.success(`Created PR #${pr.number}: ${pr.url}`));
|
|
686
|
+
// Generate worktree path
|
|
687
|
+
const worktreePath = generateWorktreePath(config, repoRoot, repoName, pr.number);
|
|
688
|
+
// Create worktree
|
|
689
|
+
console.log(colors.info(`Creating worktree at ${worktreePath}...`));
|
|
690
|
+
try {
|
|
691
|
+
git.addWorktree(worktreePath, branchName, {
|
|
692
|
+
createBranch: true,
|
|
693
|
+
startPoint: `origin/${branchName}`,
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
catch {
|
|
697
|
+
git.addWorktree(worktreePath, branchName);
|
|
698
|
+
}
|
|
699
|
+
console.log(colors.success(`Created worktree: ${worktreePath}`));
|
|
700
|
+
// Setup worktree
|
|
701
|
+
await setupWorktree(worktreePath, config, options);
|
|
702
|
+
// Print summary
|
|
703
|
+
printSummary(pr.number, branchName, worktreePath, pr.url);
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Mode: Create new branch + PR + worktree
|
|
707
|
+
*/
|
|
708
|
+
async function modeNewFeature(description, options) {
|
|
709
|
+
const repoRoot = git.getRepoRoot();
|
|
710
|
+
const repoName = git.getRepoName(repoRoot);
|
|
711
|
+
const config = loadConfig(repoRoot);
|
|
712
|
+
// Generate branch name
|
|
713
|
+
const branchName = generateBranchName(config, description);
|
|
714
|
+
// Fetch latest
|
|
715
|
+
console.log(colors.info('Fetching latest from origin...'));
|
|
716
|
+
try {
|
|
717
|
+
git.fetch('origin');
|
|
718
|
+
}
|
|
719
|
+
catch {
|
|
720
|
+
console.log(colors.warning('Could not fetch from origin (network unavailable?)'));
|
|
721
|
+
}
|
|
722
|
+
// Analyze git state
|
|
723
|
+
const state = analyzeGitState(options.baseBranch);
|
|
724
|
+
const scenario = detectScenario(state);
|
|
725
|
+
// Handle scenario and get action
|
|
726
|
+
const action = await handleScenario(scenario, state, options.baseBranch);
|
|
727
|
+
if (!action) {
|
|
728
|
+
console.log(colors.error('Aborted by user.'));
|
|
729
|
+
process.exit(1);
|
|
730
|
+
}
|
|
731
|
+
// Handle special case: create PR for existing branch
|
|
732
|
+
if (action.action === 'create_pr_for_branch' ||
|
|
733
|
+
action.action === 'pr_for_branch_commit_all' ||
|
|
734
|
+
action.action === 'pr_for_branch_stash') {
|
|
735
|
+
const currentBranch = state.currentBranch;
|
|
736
|
+
if (!currentBranch) {
|
|
737
|
+
console.error(colors.error('Cannot determine current branch'));
|
|
738
|
+
process.exit(1);
|
|
739
|
+
}
|
|
740
|
+
// Execute action for current branch
|
|
741
|
+
executeStateAction(action, description, currentBranch);
|
|
742
|
+
// Push if needed
|
|
743
|
+
if (!git.remoteBranchExists(currentBranch)) {
|
|
744
|
+
console.log(colors.info('Pushing branch to origin...'));
|
|
745
|
+
git.push({ setUpstream: true, remote: 'origin', branch: currentBranch });
|
|
746
|
+
}
|
|
747
|
+
// Delegate to existing branch mode
|
|
748
|
+
await modeExistingBranch(currentBranch, options);
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
console.log(colors.info(`Creating feature branch: ${branchName}`));
|
|
752
|
+
// Check if branch already exists on remote
|
|
753
|
+
if (git.remoteBranchExists(branchName)) {
|
|
754
|
+
console.log(colors.warning(`Branch ${branchName} already exists on remote`));
|
|
755
|
+
const existingPr = github.getPrByBranch(branchName);
|
|
756
|
+
if (existingPr) {
|
|
757
|
+
console.log(colors.info(`PR #${existingPr.number} already exists, setting up worktree...`));
|
|
758
|
+
await modeExistingPr(existingPr.number, options);
|
|
759
|
+
}
|
|
760
|
+
else {
|
|
761
|
+
console.log(colors.info('No PR exists, creating one...'));
|
|
762
|
+
await modeExistingBranch(branchName, options);
|
|
763
|
+
}
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
// Save original branch
|
|
767
|
+
const originalBranch = git.getCurrentBranch() || 'main';
|
|
768
|
+
// Execute pre-branch actions
|
|
769
|
+
const stashRef = executeStateAction(action, description, branchName);
|
|
770
|
+
// Stash unstaged changes if needed
|
|
771
|
+
let unstagedStashRef = null;
|
|
772
|
+
if (action.stashUnstaged) {
|
|
773
|
+
console.log(colors.info('Stashing unstaged changes (will move to worktree)...'));
|
|
774
|
+
unstagedStashRef = git.stash({
|
|
775
|
+
keepIndex: true,
|
|
776
|
+
message: 'newpr: unstaged changes for worktree',
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
try {
|
|
780
|
+
// Determine branch point
|
|
781
|
+
const branchFrom = action.branchFrom === 'head' ? 'HEAD' : `origin/${options.baseBranch}`;
|
|
782
|
+
console.log(colors.info(`Creating branch from ${branchFrom}...`));
|
|
783
|
+
// Create and checkout new branch
|
|
784
|
+
git.exec(['checkout', '-b', branchName, branchFrom]);
|
|
785
|
+
// Create initial commit
|
|
786
|
+
const stagedFiles = git.getStagedFiles();
|
|
787
|
+
if (stagedFiles.length > 0) {
|
|
788
|
+
console.log(colors.info('Committing staged changes...'));
|
|
789
|
+
git.commit({ message: `feat: ${description}\n\n🤖 Created with newpr` });
|
|
790
|
+
}
|
|
791
|
+
else if (action.branchFrom === 'origin_main') {
|
|
792
|
+
// No commits ahead and no staged changes - create empty commit
|
|
793
|
+
console.log(colors.info('Creating initial commit (required for PR creation)...'));
|
|
794
|
+
git.commit({
|
|
795
|
+
message: `chore: initialize ${branchName}\n\nBranch created for: ${description}\n\n🤖 Created with newpr`,
|
|
796
|
+
allowEmpty: true,
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
// Push branch
|
|
800
|
+
console.log(colors.info('Pushing branch to origin...'));
|
|
801
|
+
git.push({ setUpstream: true, remote: 'origin', branch: branchName });
|
|
802
|
+
// Switch back to original branch
|
|
803
|
+
git.checkout(originalBranch);
|
|
804
|
+
// Create PR
|
|
805
|
+
console.log(colors.info('Creating pull request...'));
|
|
806
|
+
const pr = github.createPr({
|
|
807
|
+
title: description,
|
|
808
|
+
body: `## Summary
|
|
809
|
+
|
|
810
|
+
${description}
|
|
811
|
+
|
|
812
|
+
## Changes
|
|
813
|
+
|
|
814
|
+
-
|
|
815
|
+
|
|
816
|
+
## Test Plan
|
|
817
|
+
|
|
818
|
+
- [ ]
|
|
819
|
+
|
|
820
|
+
---
|
|
821
|
+
🤖 PR created with \`newpr\``,
|
|
822
|
+
base: options.baseBranch,
|
|
823
|
+
head: branchName,
|
|
824
|
+
draft: options.draft,
|
|
825
|
+
});
|
|
826
|
+
console.log(colors.success(`Created PR #${pr.number}: ${pr.url}`));
|
|
827
|
+
// Generate worktree path
|
|
828
|
+
const worktreePath = generateWorktreePath(config, repoRoot, repoName, pr.number);
|
|
829
|
+
// Create worktree
|
|
830
|
+
console.log(colors.info(`Creating worktree at ${worktreePath}...`));
|
|
831
|
+
git.addWorktree(worktreePath, branchName);
|
|
832
|
+
console.log(colors.success(`Created worktree: ${worktreePath}`));
|
|
833
|
+
// Apply unstaged changes to worktree if we stashed them
|
|
834
|
+
if (unstagedStashRef) {
|
|
835
|
+
console.log(colors.info('Moving unstaged changes to worktree...'));
|
|
836
|
+
try {
|
|
837
|
+
git.stashApply(unstagedStashRef, worktreePath);
|
|
838
|
+
console.log(colors.success('Unstaged changes applied to worktree'));
|
|
839
|
+
git.stashDrop(unstagedStashRef);
|
|
840
|
+
}
|
|
841
|
+
catch {
|
|
842
|
+
console.log(colors.warning('Failed to apply unstaged changes to worktree.'));
|
|
843
|
+
console.log(colors.warning("Run 'git stash pop' in main worktree to recover them."));
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
// Setup worktree
|
|
847
|
+
await setupWorktree(worktreePath, config, options);
|
|
848
|
+
// Print summary
|
|
849
|
+
printSummary(pr.number, branchName, worktreePath, pr.url);
|
|
850
|
+
}
|
|
851
|
+
catch (error) {
|
|
852
|
+
// Restore stashed changes on failure
|
|
853
|
+
if (stashRef) {
|
|
854
|
+
console.log(colors.info('Restoring stashed changes...'));
|
|
855
|
+
try {
|
|
856
|
+
git.stashPop(stashRef);
|
|
857
|
+
}
|
|
858
|
+
catch {
|
|
859
|
+
console.log(colors.warning("Failed to restore stash. Run 'git stash pop' manually."));
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
throw error;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Main entry point
|
|
867
|
+
*/
|
|
868
|
+
async function main() {
|
|
869
|
+
const args = process.argv.slice(2);
|
|
870
|
+
const options = parseArgs(args);
|
|
871
|
+
checkPrerequisites();
|
|
872
|
+
switch (options.mode) {
|
|
873
|
+
case 'pr':
|
|
874
|
+
await modeExistingPr(options.prNumber, options);
|
|
875
|
+
break;
|
|
876
|
+
case 'branch':
|
|
877
|
+
await modeExistingBranch(options.branchName, options);
|
|
878
|
+
break;
|
|
879
|
+
case 'new':
|
|
880
|
+
await modeNewFeature(options.description, options);
|
|
881
|
+
break;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
main().catch((error) => {
|
|
885
|
+
console.error(colors.error(error instanceof Error ? error.message : String(error)));
|
|
886
|
+
process.exit(1);
|
|
887
|
+
});
|
|
888
|
+
//# sourceMappingURL=newpr.js.map
|