gwqadd 0.3.5 → 0.4.0
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 +31 -0
- package/README.md +19 -0
- package/bin/gwqadd.mjs +248 -19
- package/package.json +1 -1
package/LICENSE
CHANGED
|
@@ -19,3 +19,34 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
21
|
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
This package embeds word lists derived from third-party sources:
|
|
26
|
+
|
|
27
|
+
glitchdotcom/friendly-words — https://github.com/glitchdotcom/friendly-words
|
|
28
|
+
|
|
29
|
+
MIT License
|
|
30
|
+
|
|
31
|
+
Copyright (c) 2018 Glitch
|
|
32
|
+
|
|
33
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
34
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
35
|
+
in the Software without restriction, including without limitation the rights
|
|
36
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
37
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
38
|
+
furnished to do so, subject to the following conditions:
|
|
39
|
+
|
|
40
|
+
The above copyright notice and this permission notice shall be included in all
|
|
41
|
+
copies or substantial portions of the Software.
|
|
42
|
+
|
|
43
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
44
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
45
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
46
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
47
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
48
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
49
|
+
SOFTWARE.
|
|
50
|
+
|
|
51
|
+
dariusk/corpora — https://github.com/dariusk/corpora — released under CC0
|
|
52
|
+
(public domain). No attribution is required; it is given anyway.
|
package/README.md
CHANGED
|
@@ -99,6 +99,23 @@ So the fix is one flag:
|
|
|
99
99
|
gwqadd feat/logout --from main
|
|
100
100
|
```
|
|
101
101
|
|
|
102
|
+
## No name? Take a random one
|
|
103
|
+
|
|
104
|
+
Run `gwqadd` with no branch name and it rolls one before it asks you anything:
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
│ plume-melting-bearskin off main
|
|
108
|
+
│ create it? [Y]es · [n]o, name it properly · [e]dit · [r]eroll
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Three words, no prefix, no waiting — nothing has been sent anywhere and nothing
|
|
112
|
+
created. `r` rolls again, `e` edits it, and `n` drops you into the naming help
|
|
113
|
+
below, where an AI names the branch in your repository's own style.
|
|
114
|
+
|
|
115
|
+
`--random` skips the confirmation and is the only naming path that works without
|
|
116
|
+
a terminal, which makes it the one scripts and agents should use. `--no-random`
|
|
117
|
+
(or `GWQADD_RANDOM=off`) starts at the description prompt instead.
|
|
118
|
+
|
|
102
119
|
## Naming help
|
|
103
120
|
|
|
104
121
|
One question, one confirmation:
|
|
@@ -180,6 +197,8 @@ gwqadd [options] [<branch>]
|
|
|
180
197
|
| `--expires <dur>` | hand gwq an expiry (`1h`, `7d`, …) for a throwaway worktree |
|
|
181
198
|
| `--ai <cmd>` | AI CLI used to suggest names (default: autodetected) |
|
|
182
199
|
| `--no-ai` | never ask an AI, even when one is installed |
|
|
200
|
+
| `--random` | skip the questions and generate a name |
|
|
201
|
+
| `--no-random` | start by describing the work instead of rolling a name |
|
|
183
202
|
| `--no-submodules` | skip `git submodule update --init --recursive` |
|
|
184
203
|
| `-f`, `--force` | move a colliding worktree directory aside instead of failing |
|
|
185
204
|
| `-n`, `--no-cd` | do the work and report the path, but do not move the shell |
|
package/bin/gwqadd.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawnSync, spawn } from 'node:child_process';
|
|
3
|
+
import { randomBytes } from 'node:crypto';
|
|
3
4
|
import { parseArgs } from 'node:util';
|
|
4
5
|
import { Buffer } from 'node:buffer';
|
|
5
6
|
import {
|
|
@@ -34,6 +35,8 @@ OPTIONS
|
|
|
34
35
|
--expires <dur> hand gwq an expiry (1h, 7d, …) for a throwaway worktree
|
|
35
36
|
--ai <cmd> AI CLI used to suggest names (default: autodetected)
|
|
36
37
|
--no-ai never ask an AI, even when one is installed
|
|
38
|
+
--random skip the questions and generate a name
|
|
39
|
+
--no-random start by describing the work instead of rolling a name
|
|
37
40
|
--no-submodules skip \`git submodule update --init --recursive\`
|
|
38
41
|
-f, --force move a colliding worktree directory aside instead of failing
|
|
39
42
|
-n, --no-cd do the work and report the path, but do not move the shell
|
|
@@ -47,12 +50,19 @@ EXAMPLES
|
|
|
47
50
|
${PKG} feat/login branch off HEAD, worktree, cd
|
|
48
51
|
${PKG} feat/login --from main branch off main wherever you happen to be
|
|
49
52
|
${PKG} hotfix/x --expires 1d gwq will mark it expired after a day
|
|
50
|
-
${PKG}
|
|
53
|
+
${PKG} roll a name, confirm, done
|
|
54
|
+
${PKG} --random roll a name for a script, no questions
|
|
51
55
|
${PKG} -n --json feat/login machine-readable, shell stays put
|
|
52
56
|
|
|
53
57
|
NAMING HELP
|
|
54
|
-
Run ${PKG} with no branch name and it
|
|
55
|
-
|
|
58
|
+
Run ${PKG} with no branch name and it rolls one immediately — three words, no
|
|
59
|
+
prefix, no waiting:
|
|
60
|
+
|
|
61
|
+
Y create it n name it properly e edit the name r reroll
|
|
62
|
+
|
|
63
|
+
Nothing has been sent anywhere and nothing created at that point. Press n and
|
|
64
|
+
it asks what you want to do, in any language, and hands that to an AI CLI
|
|
65
|
+
which answers with one name in this repository's own style:
|
|
56
66
|
|
|
57
67
|
Y create it n describe it again e edit the name
|
|
58
68
|
|
|
@@ -67,6 +77,10 @@ NAMING HELP
|
|
|
67
77
|
GWQADD_AI='<cmd>'; disable with --no-ai or GWQADD_AI=off, which leaves a plain
|
|
68
78
|
prompt for an ASCII name.
|
|
69
79
|
|
|
80
|
+
--no-random (or GWQADD_RANDOM=off) starts at the description prompt instead.
|
|
81
|
+
--random goes the other way and never asks; it is the only naming path that
|
|
82
|
+
works without a terminal, which is what scripts and agents should use.
|
|
83
|
+
|
|
70
84
|
None of this happens with a branch name on the command line, or without a
|
|
71
85
|
terminal — scripts and agents keep the plain, silent contract.
|
|
72
86
|
|
|
@@ -135,6 +149,8 @@ try {
|
|
|
135
149
|
expires: { type: 'string' },
|
|
136
150
|
ai: { type: 'string' },
|
|
137
151
|
'no-ai': { type: 'boolean' },
|
|
152
|
+
random: { type: 'boolean' },
|
|
153
|
+
'no-random': { type: 'boolean' },
|
|
138
154
|
'no-submodules': { type: 'boolean' },
|
|
139
155
|
force: { type: 'boolean', short: 'f' },
|
|
140
156
|
'no-cd': { type: 'boolean', short: 'n' },
|
|
@@ -403,6 +419,17 @@ const doSubmodules = !values['no-submodules'];
|
|
|
403
419
|
const force = !!values.force;
|
|
404
420
|
const stayOut = !!values['no-cd'];
|
|
405
421
|
|
|
422
|
+
// The random-first prompt is the default; --no-random restores the 0.3.x flow
|
|
423
|
+
// of describing the work to an AI straight away.
|
|
424
|
+
const randomOff =
|
|
425
|
+
!!values['no-random'] ||
|
|
426
|
+
['off', '0', 'false', 'none'].includes(process.env.GWQADD_RANDOM ?? '');
|
|
427
|
+
const randomFirst = !randomOff;
|
|
428
|
+
|
|
429
|
+
if (values.random && values['no-random']) {
|
|
430
|
+
die('E_VALIDATION', '--random and --no-random cannot both be given');
|
|
431
|
+
}
|
|
432
|
+
|
|
406
433
|
// ── interactivity ────────────────────────────────────────────────────────────
|
|
407
434
|
|
|
408
435
|
const stdinTTY = !!process.stdin.isTTY;
|
|
@@ -598,6 +625,161 @@ function defaultBranch(dir) {
|
|
|
598
625
|
return head ? head.replace(/^refs\/remotes\/origin\//, '') : '';
|
|
599
626
|
}
|
|
600
627
|
|
|
628
|
+
// ── word lists (generated — do not hand-edit) ────────────────────────────────
|
|
629
|
+
//
|
|
630
|
+
// Regenerate with `node tools/build-words.mjs`. Editing these by hand drifts
|
|
631
|
+
// the counts from the recipe and discards the licence trail (I23).
|
|
632
|
+
//
|
|
633
|
+
// Adjectives and nouns: glitchdotcom/friendly-words, MIT (c) 2018 Glitch.
|
|
634
|
+
// Gerunds: dariusk/corpora, CC0.
|
|
635
|
+
//
|
|
636
|
+
// The counts match `claude -w` (216 * 109 * 407 = 9,582,408 names); the words
|
|
637
|
+
// deliberately do not. See the design doc for why they were not copied.
|
|
638
|
+
|
|
639
|
+
const ADJECTIVES = [
|
|
640
|
+
'aback', 'abrupt', 'achieved', 'adaptable', 'aerial', 'airy', 'alike',
|
|
641
|
+
'alpine', 'amplified', 'apricot', 'ash', 'available', 'azure', 'basalt',
|
|
642
|
+
'bejeweled', 'bevel', 'bloom', 'bold', 'boundless', 'branched', 'brawny',
|
|
643
|
+
'bright', 'bronzed', 'burly', 'butternut', 'cactus', 'candied', 'caramel',
|
|
644
|
+
'carpal', 'celestial', 'chambray', 'checkered', 'chestnut', 'chisel',
|
|
645
|
+
'citrine', 'clean', 'cloudy', 'coconut', 'colossal', 'concise', 'cookie',
|
|
646
|
+
'cord', 'creative', 'cuddly', 'cyber', 'daily', 'darkened', 'decorous',
|
|
647
|
+
'delicious', 'dented', 'diamond', 'dolomite', 'dune', 'early', 'educated',
|
|
648
|
+
'elated', 'elite', 'endurable', 'equinox', 'evergreen', 'expensive',
|
|
649
|
+
'faceted', 'familiar', 'fantastic', 'fearless', 'field', 'first', 'flannel',
|
|
650
|
+
'flax', 'flower', 'foremost', 'fortune', 'freckle', 'frill', 'furtive',
|
|
651
|
+
'garrulous', 'geode', 'ginger', 'glib', 'glorious', 'goldenrod', 'graceful',
|
|
652
|
+
'grass', 'grey', 'guiltless', 'half', 'happy', 'heathered', 'helpful',
|
|
653
|
+
'hill', 'honorable', 'humane', 'hurricane', 'icy', 'important', 'innate',
|
|
654
|
+
'iodized', 'island', 'jet', 'judicious', 'juvenile', 'knotty', 'languid',
|
|
655
|
+
'lavender', 'learned', 'level', 'lime', 'lively', 'lopsided', 'lumbar',
|
|
656
|
+
'luxurious', 'magical', 'malleable', 'marble', 'marred', 'massive', 'mellow',
|
|
657
|
+
'mercurial', 'mica', 'military', 'mire', 'modest', 'mousy', 'narrow',
|
|
658
|
+
'nebula', 'nice', 'ninth', 'numerous', 'occipital', 'olivine', 'orchid',
|
|
659
|
+
'ossified', 'pale', 'past', 'pear', 'perfect', 'petalite', 'picayune',
|
|
660
|
+
'pineapple', 'plaid', 'platinum', 'plume', 'polarized', 'polyester',
|
|
661
|
+
'prairie', 'private', 'proximal', 'pyrite', 'quickest', 'quilted', 'radical',
|
|
662
|
+
'raspy', 'regal', 'repeated', 'respected', 'ringed', 'road', 'romantic',
|
|
663
|
+
'round', 'rustic', 'salt', 'sapphire', 'scented', 'season', 'sedate',
|
|
664
|
+
'separate', 'shaded', 'sheer', 'shiny', 'shrub', 'silky', 'sincere',
|
|
665
|
+
'skitter', 'slimy', 'smooth', 'solar', 'southern', 'speckle', 'spiced',
|
|
666
|
+
'spiral', 'spotless', 'spurious', 'steel', 'stone', 'strong', 'subdued',
|
|
667
|
+
'sugar', 'sumptuous', 'superb', 'sweet', 'tame', 'tartan', 'temporal',
|
|
668
|
+
'thankful', 'thoracic', 'tide', 'tiny', 'torpid', 'tropical', 'tundra',
|
|
669
|
+
'typhoon', 'unique', 'upbeat', 'valiant', 'vast', 'verbose', 'violet',
|
|
670
|
+
'volcano', 'water', 'west', 'wholesale', 'winter', 'wobbly', 'woolen',
|
|
671
|
+
'young', 'zest'
|
|
672
|
+
];
|
|
673
|
+
|
|
674
|
+
const GERUNDS = [
|
|
675
|
+
'abiding', 'adding', 'affording', 'amazing', 'appearing', 'asking',
|
|
676
|
+
'attracting', 'baring', 'behaving', 'blotting', 'booking', 'boxing',
|
|
677
|
+
'brushing', 'bustling', 'carrying', 'charming', 'chopping', 'clipping',
|
|
678
|
+
'colouring', 'completing', 'continuing', 'counting', 'curing', 'daring',
|
|
679
|
+
'delighting', 'deserving', 'doubling', 'dropping', 'employing', 'escaping',
|
|
680
|
+
'exercising', 'exploding', 'fastening', 'filling', 'floating', 'flying',
|
|
681
|
+
'forming', 'gathering', 'gluing', 'guessing', 'hanging', 'heating',
|
|
682
|
+
'hopping', 'hunting', 'including', 'intending', 'joining', 'kicking',
|
|
683
|
+
'knowing', 'launching', 'licking', 'listing', 'loving', 'matching',
|
|
684
|
+
'melting', 'mining', 'muddling', 'nesting', 'obeying', 'offering', 'owning',
|
|
685
|
+
'parting', 'pedaling', 'phoning', 'planning', 'poking', 'pouring',
|
|
686
|
+
'preferring', 'printing', 'pulling', 'pushing', 'raining', 'recording',
|
|
687
|
+
'rejoicing', 'reminding', 'replying', 'returning', 'rotating', 'sailing',
|
|
688
|
+
'scorching', 'sealing', 'shading', 'shining', 'signing', 'slapping',
|
|
689
|
+
'smiling', 'snoring', 'sparing', 'spotting', 'squeaking', 'standing',
|
|
690
|
+
'stepping', 'stretching', 'suggesting', 'surprising', 'taming', 'testing',
|
|
691
|
+
'ticking', 'touching', 'trading', 'trusting', 'tying', 'unpacking',
|
|
692
|
+
'wailing', 'warming', 'weighing', 'whistling', 'wondering', 'yawning'
|
|
693
|
+
];
|
|
694
|
+
|
|
695
|
+
const NOUNS = [
|
|
696
|
+
'aardvark', 'acorn', 'actress', 'aftermath', 'air', 'airport', 'alibi',
|
|
697
|
+
'almanac', 'aluminum', 'amp', 'ancient', 'anise', 'antimony', 'apology',
|
|
698
|
+
'appliance', 'archduke', 'armchair', 'article', 'asterisk', 'attempt',
|
|
699
|
+
'author', 'axolotl', 'bag', 'ball', 'barbecue', 'barn', 'barracuda',
|
|
700
|
+
'basket', 'bathtub', 'beak', 'bearskin', 'bedbug', 'beginner', 'beluga',
|
|
701
|
+
'bicycle', 'birch', 'bit', 'blarney', 'blouse', 'boat', 'bongo', 'booth',
|
|
702
|
+
'bow', 'braid', 'brass', 'breath', 'broccoli', 'brow', 'buckaroo', 'buffet',
|
|
703
|
+
'bun', 'butter', 'cabbage', 'cafe', 'camera', 'candytuft', 'cap', 'caption',
|
|
704
|
+
'carbon', 'caribou', 'carpet', 'carver', 'cat', 'catmint', 'ceder', 'cello',
|
|
705
|
+
'centipede', 'chair', 'change', 'chauffeur', 'chemistry', 'chevre', 'chill',
|
|
706
|
+
'chive', 'cicada', 'cirrus', 'clarinet', 'click', 'clock', 'clover', 'coat',
|
|
707
|
+
'cockroach', 'cold', 'colossus', 'comfort', 'concrete', 'conifer', 'copy',
|
|
708
|
+
'corn', 'couch', 'course', 'cowl', 'crate', 'creature', 'cricket', 'crow',
|
|
709
|
+
'cub', 'cupcake', 'curve', 'cylinder', 'dancer', 'dataset', 'decade', 'den',
|
|
710
|
+
'desk', 'dewberry', 'dichondra', 'dinghy', 'discovery', 'dogwood', 'donut',
|
|
711
|
+
'drain', 'drifter', 'drizzle', 'duckling', 'durian', 'earth', 'echo',
|
|
712
|
+
'education', 'elbow', 'elm', 'energy', 'entree', 'ermine', 'evergreen',
|
|
713
|
+
'eyebrow', 'falcon', 'farm', 'feather', 'femur', 'ferret', 'fibre', 'figure',
|
|
714
|
+
'fine', 'flag', 'flavor', 'flood', 'flyaway', 'football', 'form', 'foxtail',
|
|
715
|
+
'freedom', 'friction', 'frog', 'function', 'galley', 'garage', 'garnet',
|
|
716
|
+
'gauge', 'gemini', 'gerbera', 'ginger', 'glasses', 'glow', 'golf', 'gouda',
|
|
717
|
+
'gram', 'grey', 'group', 'guarantee', 'gull', 'haddock', 'hallway',
|
|
718
|
+
'handsaw', 'hare', 'hawthorn', 'health', 'heaven', 'hellebore', 'herring',
|
|
719
|
+
'hiss', 'homegrown', 'hoof', 'hose', 'hourglass', 'humerus', 'hydrangea',
|
|
720
|
+
'hyphen', 'icon', 'income', 'ink', 'iron', 'jacket', 'jasmine', 'jellyfish',
|
|
721
|
+
'jodhpur', 'judge', 'juniper', 'kayak', 'keyboard', 'king', 'knee', 'krill',
|
|
722
|
+
'lake', 'land', 'larkspur', 'launch', 'lead', 'legal', 'lemur', 'letter',
|
|
723
|
+
'license', 'lighter', 'limpet', 'lion', 'liver', 'lobster', 'logic', 'lunch',
|
|
724
|
+
'lychee', 'macaw', 'magician', 'mailbox', 'mallow', 'mandible', 'manta',
|
|
725
|
+
'march', 'market', 'mars', 'mastodon', 'may', 'medallion', 'memory',
|
|
726
|
+
'meteoroid', 'midnight', 'mine', 'mirror', 'molasses', 'money', 'moon',
|
|
727
|
+
'mosquito', 'mountain', 'muenster', 'museum', 'mustang', 'napkin', 'nebula',
|
|
728
|
+
'neon', 'net', 'newt', 'nitrogen', 'nurse', 'oatmeal', 'octagon', 'office',
|
|
729
|
+
'onion', 'opinion', 'orca', 'origami', 'ounce', 'owl', 'pail', 'pan',
|
|
730
|
+
'panther', 'papyrus', 'park', 'particle', 'passive', 'path', 'pea', 'pear',
|
|
731
|
+
'pencil', 'perch', 'pet', 'pharaoh', 'piccolo', 'pig', 'pin', 'piper',
|
|
732
|
+
'place', 'plant', 'platypus', 'plot', 'plutonium', 'polyester', 'porter',
|
|
733
|
+
'potato', 'prawn', 'prince', 'process', 'proof', 'ptarmigan', 'puppet',
|
|
734
|
+
'pyramid', 'quart', 'quill', 'quotation', 'radiator', 'raft', 'rainstorm',
|
|
735
|
+
'range', 'reaction', 'recess', 'region', 'repair', 'research', 'reward',
|
|
736
|
+
'riddle', 'riverbed', 'rock', 'rook', 'rosemary', 'rubidium', 'runner',
|
|
737
|
+
'saguaro', 'salary', 'salute', 'sapphire', 'saturn', 'saxophone', 'scapula',
|
|
738
|
+
'school', 'scooter', 'screen', 'seaplane', 'second', 'seer', 'server',
|
|
739
|
+
'shallot', 'shear', 'shift', 'shop', 'shroud', 'silence', 'silver', 'skull',
|
|
740
|
+
'slice', 'slipper', 'smoke', 'sneeze', 'snowman', 'soarer', 'sodalite',
|
|
741
|
+
'sole', 'soul', 'soy', 'spark', 'spectrum', 'spider', 'split', 'sprint',
|
|
742
|
+
'spy', 'stage', 'station', 'step', 'sting', 'stocking', 'story', 'streetcar',
|
|
743
|
+
'subject', 'suit', 'sundial', 'sunspot', 'surgeon', 'sweater', 'swordfish',
|
|
744
|
+
'system', 'tailor', 'tangelo', 'target', 'tartan', 'teal', 'tellurium',
|
|
745
|
+
'tent', 'textbook', 'thorium', 'throne', 'tick', 'tile', 'tip', 'toast',
|
|
746
|
+
'topaz', 'town', 'traffic', 'traveler', 'tricorne', 'trouser', 'trust',
|
|
747
|
+
'tugboat', 'turkey', 'turret', 'twine', 'uncle', 'vacation', 'variety',
|
|
748
|
+
'vein', 'vertebra', 'viola', 'viscose', 'voice', 'walk', 'walleye',
|
|
749
|
+
'warbler', 'wasp', 'wave', 'weaver', 'whale', 'whitefish', 'wineberry',
|
|
750
|
+
'wisteria', 'wolfsbane', 'woolen', 'wrinkle', 'xylophone', 'yarrow', 'zebra',
|
|
751
|
+
'zinnia'
|
|
752
|
+
];
|
|
753
|
+
|
|
754
|
+
// ── random names ─────────────────────────────────────────────────────────────
|
|
755
|
+
|
|
756
|
+
// crypto, not Math.random: two shells started in the same second must not be
|
|
757
|
+
// able to agree on a branch name. The modulo biases the first few words of each
|
|
758
|
+
// list upward by about 407 / 2^32, which is invisible at any number of branches
|
|
759
|
+
// a person will ever create.
|
|
760
|
+
const pick = (a) => a[randomBytes(4).readUInt32BE(0) % a.length];
|
|
761
|
+
const randomName = () => `${pick(ADJECTIVES)}-${pick(GERUNDS)}-${pick(NOUNS)}`;
|
|
762
|
+
|
|
763
|
+
const RANDOM_TRIES = 10;
|
|
764
|
+
|
|
765
|
+
// `claude -w` lets a collision become a hard error; we reroll instead. The
|
|
766
|
+
// check has to happen before the name is shown, so the confirmation prompt can
|
|
767
|
+
// never offer something that cannot be created (I24). Ten failures in a
|
|
768
|
+
// 9,582,408-name space means our randomness is broken, not the user's luck.
|
|
769
|
+
//
|
|
770
|
+
// The branch check alone is enough, and a `worktreePath()` call beside it would
|
|
771
|
+
// be unreachable: `git worktree list --porcelain` only prints `branch
|
|
772
|
+
// refs/heads/<name>` for a worktree that has that branch checked out, and a
|
|
773
|
+
// worktree with no branch prints `detached` instead. So a name a worktree holds
|
|
774
|
+
// is always a name a branch holds. Verified against git before this was cut.
|
|
775
|
+
function freeRandomName(dir) {
|
|
776
|
+
for (let i = 0; i < RANDOM_TRIES; i++) {
|
|
777
|
+
const name = randomName();
|
|
778
|
+
if (!hasLocalBranch(dir, name)) return name;
|
|
779
|
+
}
|
|
780
|
+
return '';
|
|
781
|
+
}
|
|
782
|
+
|
|
601
783
|
// ── naming (interactive only) ────────────────────────────────────────────────
|
|
602
784
|
|
|
603
785
|
// Everything about the repository worth telling a model that is choosing a
|
|
@@ -837,13 +1019,16 @@ async function typeItYourself(initial = '') {
|
|
|
837
1019
|
|
|
838
1020
|
// The single checkpoint before anything is created. `n` sends the user back to
|
|
839
1021
|
// describing the work, which is what they asked for; `e` is there because a
|
|
840
|
-
// suggestion that is one word off should not cost another round trip
|
|
841
|
-
|
|
1022
|
+
// suggestion that is one word off should not cost another round trip; `r` only
|
|
1023
|
+
// appears for a random name, where rerolling costs nothing at all.
|
|
1024
|
+
async function confirmCreate(name, base, { reroll = false } = {}) {
|
|
842
1025
|
log(`${dim('│')}`);
|
|
843
1026
|
log(`${dim('│')} ${bold(cyan(name))} ${dim(`off ${base}`)}`);
|
|
1027
|
+
const no = reroll ? '[n]o, name it properly' : '[n]o, describe again';
|
|
1028
|
+
const extra = reroll ? `${dim('·')} ${dim('[r]eroll')} ` : '';
|
|
844
1029
|
stderr.write(
|
|
845
|
-
`${dim('│')} create it? ${dim('[Y]es')} ${dim('·')} ${dim(
|
|
846
|
-
`${dim('·')} ${dim('[e]dit the name')} `,
|
|
1030
|
+
`${dim('│')} create it? ${dim('[Y]es')} ${dim('·')} ${dim(no)} ` +
|
|
1031
|
+
`${dim('·')} ${dim('[e]dit the name')} ${extra}`,
|
|
847
1032
|
);
|
|
848
1033
|
for (;;) {
|
|
849
1034
|
const buf = await waitForKey();
|
|
@@ -852,14 +1037,40 @@ async function confirmCreate(name, base) {
|
|
|
852
1037
|
if (c === 0x79 || c === 0x59 || c === 0x0d || c === 0x0a) { stderr.write('\n'); return { create: true }; }
|
|
853
1038
|
if (c === 0x6e || c === 0x4e) { stderr.write('\n'); return { again: true }; }
|
|
854
1039
|
if (c === 0x65 || c === 0x45) { stderr.write('\n'); return { edit: true }; }
|
|
1040
|
+
if (reroll && (c === 0x72 || c === 0x52)) { stderr.write('\n'); return { reroll: true }; }
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// The fast half of the naming layer: a name appears with no prompt, no
|
|
1045
|
+
// subprocess and no network, and the expensive path costs one keystroke.
|
|
1046
|
+
// Returns null when the user wants to describe the work to an AI instead.
|
|
1047
|
+
async function offerRandom(dir, baseRef) {
|
|
1048
|
+
for (;;) {
|
|
1049
|
+
const name = freeRandomName(dir);
|
|
1050
|
+
if (!name) {
|
|
1051
|
+
warn(`could not find an unused random name in ${RANDOM_TRIES} tries — name it yourself instead`);
|
|
1052
|
+
return { branch: await typeItYourself(), named: 'manual' };
|
|
1053
|
+
}
|
|
1054
|
+
const choice = await confirmCreate(name, baseRef, { reroll: true });
|
|
1055
|
+
if (choice.create) return { branch: name, named: 'random' };
|
|
1056
|
+
if (choice.edit) return { branch: await typeItYourself(name), named: 'manual' };
|
|
1057
|
+
if (choice.reroll) continue;
|
|
1058
|
+
return null; // `n` — fall through to describing the work
|
|
855
1059
|
}
|
|
856
1060
|
}
|
|
857
1061
|
|
|
858
1062
|
// The whole interactive path. Returns a branch name git has already accepted;
|
|
859
1063
|
// never runs unless there is a terminal and no positional was given.
|
|
860
1064
|
async function composeBranchName(dir, repo, base) {
|
|
1065
|
+
// Random first: it is free, and a user who wanted to think about the name is
|
|
1066
|
+
// one keystroke away from the prompt that lets them.
|
|
1067
|
+
if (randomFirst || values.random) {
|
|
1068
|
+
const chosen = await offerRandom(dir, base.ref);
|
|
1069
|
+
if (chosen) return chosen;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
861
1072
|
const ai = detectAi();
|
|
862
|
-
if (!ai) return typeItYourself();
|
|
1073
|
+
if (!ai) return { branch: await typeItYourself(), named: 'manual' };
|
|
863
1074
|
|
|
864
1075
|
const ctx = repoContext(dir, repo, base);
|
|
865
1076
|
const rejected = [];
|
|
@@ -870,24 +1081,24 @@ async function composeBranchName(dir, repo, base) {
|
|
|
870
1081
|
`${dim('│')} what do you want to do? ${dim('(any language)')}\n${dim('│')} ${dim('>')} `,
|
|
871
1082
|
);
|
|
872
1083
|
// An empty answer is the escape hatch out of the AI entirely.
|
|
873
|
-
if (!description) return typeItYourself();
|
|
1084
|
+
if (!description) return { branch: await typeItYourself(), named: 'manual' };
|
|
874
1085
|
|
|
875
1086
|
const res = await askAi(ai, namingPrompt(ctx, description, rejected));
|
|
876
1087
|
if (!res.ok) {
|
|
877
1088
|
warn(`${aiLabel(ai)} failed — name it yourself instead`);
|
|
878
1089
|
const first = (res.err || '').trim().split('\n')[0];
|
|
879
1090
|
if (first) log(`${dim('│')} ${dim(first.slice(0, 120))}`);
|
|
880
|
-
return typeItYourself();
|
|
1091
|
+
return { branch: await typeItYourself(), named: 'manual' };
|
|
881
1092
|
}
|
|
882
1093
|
const candidates = parseCandidates(res.out);
|
|
883
1094
|
if (candidates.length === 0) {
|
|
884
1095
|
warn(`${aiLabel(ai)} returned nothing usable — name it yourself instead`);
|
|
885
|
-
return typeItYourself();
|
|
1096
|
+
return { branch: await typeItYourself(), named: 'manual' };
|
|
886
1097
|
}
|
|
887
1098
|
|
|
888
1099
|
const choice = await confirmCreate(candidates[0], ctx.base);
|
|
889
|
-
if (choice.create) return candidates[0];
|
|
890
|
-
if (choice.edit) return typeItYourself(candidates[0]);
|
|
1100
|
+
if (choice.create) return { branch: candidates[0], named: 'ai' };
|
|
1101
|
+
if (choice.edit) return { branch: await typeItYourself(candidates[0]), named: 'manual' };
|
|
891
1102
|
// Rejected: remember every suggestion from this round so the next prompt
|
|
892
1103
|
// cannot come back with a near-identical name.
|
|
893
1104
|
rejected.push(...candidates);
|
|
@@ -1051,17 +1262,32 @@ async function main() {
|
|
|
1051
1262
|
}
|
|
1052
1263
|
|
|
1053
1264
|
let branch = positionals[0];
|
|
1265
|
+
let named = 'argument';
|
|
1054
1266
|
if (branch) {
|
|
1267
|
+
// A name on the command line is the user speaking; never second-guess it.
|
|
1268
|
+
if (values.random) {
|
|
1269
|
+
die('E_VALIDATION', '--random cannot be combined with a branch name');
|
|
1270
|
+
}
|
|
1055
1271
|
// git would reject this later with a less obvious message.
|
|
1056
1272
|
if (!validBranchName(branch)) {
|
|
1057
1273
|
die('E_VALIDATION', `'${branch}' is not a valid branch name`);
|
|
1058
1274
|
}
|
|
1059
|
-
} else {
|
|
1060
|
-
|
|
1275
|
+
} else if (isNonInteractive) {
|
|
1276
|
+
// --random is the one naming path that needs no terminal: it is arithmetic
|
|
1277
|
+
// over a constant array, so nothing is triggered that the caller did not
|
|
1278
|
+
// ask for by name (I15).
|
|
1279
|
+
if (!values.random) {
|
|
1061
1280
|
die('E_VALIDATION', 'a branch name is required — `gwqadd <branch>`');
|
|
1062
1281
|
}
|
|
1282
|
+
branch = freeRandomName(cwd);
|
|
1283
|
+
if (!branch) {
|
|
1284
|
+
die('E_VALIDATION',
|
|
1285
|
+
`could not find an unused random name in ${RANDOM_TRIES} tries — pass a branch name`);
|
|
1286
|
+
}
|
|
1287
|
+
named = 'random';
|
|
1288
|
+
} else {
|
|
1063
1289
|
// composeBranchName only ever returns a name git has already accepted.
|
|
1064
|
-
branch = await composeBranchName(cwd, repo, base);
|
|
1290
|
+
({ branch, named } = await composeBranchName(cwd, repo, base));
|
|
1065
1291
|
}
|
|
1066
1292
|
|
|
1067
1293
|
const branchExisted = hasLocalBranch(cwd, branch);
|
|
@@ -1076,7 +1302,7 @@ async function main() {
|
|
|
1076
1302
|
if (existing && existsSync(existing)) {
|
|
1077
1303
|
log(`${dim('│')} ${dim('worktree already exists')}`);
|
|
1078
1304
|
log(`${dim('└')} ${green('✓')} ${cyan(branch)} ${dim('→')} ${existing}`);
|
|
1079
|
-
return finish({ repo, branch, base, path: existing, created: 'none' });
|
|
1305
|
+
return finish({ repo, branch, base, path: existing, created: 'none', named });
|
|
1080
1306
|
}
|
|
1081
1307
|
|
|
1082
1308
|
// Two ways in. Without --from, `gwq add -b` creates branch and worktree in
|
|
@@ -1167,14 +1393,14 @@ async function main() {
|
|
|
1167
1393
|
|
|
1168
1394
|
log(`${dim('└')} ${green('✓')} ${cyan(branch)} ${dim('→')} ${created}`);
|
|
1169
1395
|
return finish({
|
|
1170
|
-
repo, branch, base, path: created,
|
|
1396
|
+
repo, branch, base, path: created, named,
|
|
1171
1397
|
created: branchExisted ? 'worktree' : 'branch+worktree',
|
|
1172
1398
|
});
|
|
1173
1399
|
}
|
|
1174
1400
|
|
|
1175
1401
|
// ── output ───────────────────────────────────────────────────────────────────
|
|
1176
1402
|
|
|
1177
|
-
async function finish({ repo, branch, base, path, created }) {
|
|
1403
|
+
async function finish({ repo, branch, base, path, created, named }) {
|
|
1178
1404
|
if (isJson) {
|
|
1179
1405
|
process.stdout.write(JSON.stringify({
|
|
1180
1406
|
schemaVersion: SCHEMA_VERSION,
|
|
@@ -1183,6 +1409,9 @@ async function finish({ repo, branch, base, path, created }) {
|
|
|
1183
1409
|
base: { ref: base.ref, sha: base.sha },
|
|
1184
1410
|
repo: { root: repo.root, name: repo.name },
|
|
1185
1411
|
created,
|
|
1412
|
+
// How the name was chosen, so a caller can tell a name it picked from one
|
|
1413
|
+
// the tool invented. Adding a field does not bump schemaVersion (I10).
|
|
1414
|
+
named,
|
|
1186
1415
|
cd: !stayOut,
|
|
1187
1416
|
}) + '\n');
|
|
1188
1417
|
return;
|