@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.
Files changed (119) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +259 -0
  3. package/dist/cli/cleanpr.d.ts +13 -0
  4. package/dist/cli/cleanpr.d.ts.map +1 -0
  5. package/dist/cli/cleanpr.js +441 -0
  6. package/dist/cli/cleanpr.js.map +1 -0
  7. package/dist/cli/lswt.d.ts +11 -0
  8. package/dist/cli/lswt.d.ts.map +1 -0
  9. package/dist/cli/lswt.js +313 -0
  10. package/dist/cli/lswt.js.map +1 -0
  11. package/dist/cli/newpr.d.ts +11 -0
  12. package/dist/cli/newpr.d.ts.map +1 -0
  13. package/dist/cli/newpr.js +888 -0
  14. package/dist/cli/newpr.js.map +1 -0
  15. package/dist/cli/wtlink.d.ts +15 -0
  16. package/dist/cli/wtlink.d.ts.map +1 -0
  17. package/dist/cli/wtlink.js +124 -0
  18. package/dist/cli/wtlink.js.map +1 -0
  19. package/dist/e2e/cli.e2e.test.d.ts +2 -0
  20. package/dist/e2e/cli.e2e.test.d.ts.map +1 -0
  21. package/dist/e2e/cli.e2e.test.js +215 -0
  22. package/dist/e2e/cli.e2e.test.js.map +1 -0
  23. package/dist/index.d.ts +20 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +17 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/integration/git.integration.test.d.ts +2 -0
  28. package/dist/integration/git.integration.test.d.ts.map +1 -0
  29. package/dist/integration/git.integration.test.js +333 -0
  30. package/dist/integration/git.integration.test.js.map +1 -0
  31. package/dist/lib/colors.d.ts +59 -0
  32. package/dist/lib/colors.d.ts.map +1 -0
  33. package/dist/lib/colors.js +145 -0
  34. package/dist/lib/colors.js.map +1 -0
  35. package/dist/lib/colors.test.d.ts +2 -0
  36. package/dist/lib/colors.test.d.ts.map +1 -0
  37. package/dist/lib/colors.test.js +69 -0
  38. package/dist/lib/colors.test.js.map +1 -0
  39. package/dist/lib/config.d.ts +58 -0
  40. package/dist/lib/config.d.ts.map +1 -0
  41. package/dist/lib/config.js +91 -0
  42. package/dist/lib/config.js.map +1 -0
  43. package/dist/lib/config.test.d.ts +2 -0
  44. package/dist/lib/config.test.d.ts.map +1 -0
  45. package/dist/lib/config.test.js +84 -0
  46. package/dist/lib/config.test.js.map +1 -0
  47. package/dist/lib/constants.d.ts +37 -0
  48. package/dist/lib/constants.d.ts.map +1 -0
  49. package/dist/lib/constants.js +37 -0
  50. package/dist/lib/constants.js.map +1 -0
  51. package/dist/lib/errors.d.ts +88 -0
  52. package/dist/lib/errors.d.ts.map +1 -0
  53. package/dist/lib/errors.js +112 -0
  54. package/dist/lib/errors.js.map +1 -0
  55. package/dist/lib/errors.test.d.ts +2 -0
  56. package/dist/lib/errors.test.d.ts.map +1 -0
  57. package/dist/lib/errors.test.js +117 -0
  58. package/dist/lib/errors.test.js.map +1 -0
  59. package/dist/lib/git.d.ts +224 -0
  60. package/dist/lib/git.d.ts.map +1 -0
  61. package/dist/lib/git.js +524 -0
  62. package/dist/lib/git.js.map +1 -0
  63. package/dist/lib/git.test.d.ts +2 -0
  64. package/dist/lib/git.test.d.ts.map +1 -0
  65. package/dist/lib/git.test.js +402 -0
  66. package/dist/lib/git.test.js.map +1 -0
  67. package/dist/lib/github.d.ts +82 -0
  68. package/dist/lib/github.d.ts.map +1 -0
  69. package/dist/lib/github.js +254 -0
  70. package/dist/lib/github.js.map +1 -0
  71. package/dist/lib/github.test.d.ts +2 -0
  72. package/dist/lib/github.test.d.ts.map +1 -0
  73. package/dist/lib/github.test.js +258 -0
  74. package/dist/lib/github.test.js.map +1 -0
  75. package/dist/lib/prompts.d.ts +39 -0
  76. package/dist/lib/prompts.d.ts.map +1 -0
  77. package/dist/lib/prompts.js +213 -0
  78. package/dist/lib/prompts.js.map +1 -0
  79. package/dist/lib/prompts.test.d.ts +2 -0
  80. package/dist/lib/prompts.test.d.ts.map +1 -0
  81. package/dist/lib/prompts.test.js +250 -0
  82. package/dist/lib/prompts.test.js.map +1 -0
  83. package/dist/lib/state-detection.d.ts +65 -0
  84. package/dist/lib/state-detection.d.ts.map +1 -0
  85. package/dist/lib/state-detection.js +186 -0
  86. package/dist/lib/state-detection.js.map +1 -0
  87. package/dist/lib/state-detection.test.d.ts +2 -0
  88. package/dist/lib/state-detection.test.d.ts.map +1 -0
  89. package/dist/lib/state-detection.test.js +164 -0
  90. package/dist/lib/state-detection.test.js.map +1 -0
  91. package/dist/lib/wtlink/index.d.ts +5 -0
  92. package/dist/lib/wtlink/index.d.ts.map +1 -0
  93. package/dist/lib/wtlink/index.js +7 -0
  94. package/dist/lib/wtlink/index.js.map +1 -0
  95. package/dist/lib/wtlink/link-configs.d.ts +10 -0
  96. package/dist/lib/wtlink/link-configs.d.ts.map +1 -0
  97. package/dist/lib/wtlink/link-configs.js +411 -0
  98. package/dist/lib/wtlink/link-configs.js.map +1 -0
  99. package/dist/lib/wtlink/link-configs.test.d.ts +2 -0
  100. package/dist/lib/wtlink/link-configs.test.d.ts.map +1 -0
  101. package/dist/lib/wtlink/link-configs.test.js +179 -0
  102. package/dist/lib/wtlink/link-configs.test.js.map +1 -0
  103. package/dist/lib/wtlink/main-menu.d.ts +2 -0
  104. package/dist/lib/wtlink/main-menu.d.ts.map +1 -0
  105. package/dist/lib/wtlink/main-menu.js +149 -0
  106. package/dist/lib/wtlink/main-menu.js.map +1 -0
  107. package/dist/lib/wtlink/manage-manifest.d.ts +9 -0
  108. package/dist/lib/wtlink/manage-manifest.d.ts.map +1 -0
  109. package/dist/lib/wtlink/manage-manifest.js +1262 -0
  110. package/dist/lib/wtlink/manage-manifest.js.map +1 -0
  111. package/dist/lib/wtlink/validate-manifest.d.ts +6 -0
  112. package/dist/lib/wtlink/validate-manifest.d.ts.map +1 -0
  113. package/dist/lib/wtlink/validate-manifest.js +51 -0
  114. package/dist/lib/wtlink/validate-manifest.js.map +1 -0
  115. package/dist/lib/wtlink/validate-manifest.test.d.ts +2 -0
  116. package/dist/lib/wtlink/validate-manifest.test.d.ts.map +1 -0
  117. package/dist/lib/wtlink/validate-manifest.test.js +115 -0
  118. package/dist/lib/wtlink/validate-manifest.test.js.map +1 -0
  119. 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