gwqadd 0.3.4 → 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 +251 -21
- 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;
|
|
@@ -479,8 +506,6 @@ process.on('uncaughtException', (err) => {
|
|
|
479
506
|
});
|
|
480
507
|
|
|
481
508
|
async function waitForKey() {
|
|
482
|
-
process.stdin.removeAllListeners('data');
|
|
483
|
-
process.stdin.removeAllListeners('keypress');
|
|
484
509
|
try {
|
|
485
510
|
process.stdin.setRawMode(true);
|
|
486
511
|
rawModeEngaged = true;
|
|
@@ -519,6 +544,9 @@ async function askLine(question, initial = '') {
|
|
|
519
544
|
const answer = rl.question(question);
|
|
520
545
|
if (initial) rl.write(initial);
|
|
521
546
|
return (await answer).trim();
|
|
547
|
+
} catch (err) {
|
|
548
|
+
if (err?.code === 'ABORT_ERR') die('E_INTERRUPTED', 'cancelled');
|
|
549
|
+
throw err;
|
|
522
550
|
} finally {
|
|
523
551
|
rl.close();
|
|
524
552
|
}
|
|
@@ -597,6 +625,161 @@ function defaultBranch(dir) {
|
|
|
597
625
|
return head ? head.replace(/^refs\/remotes\/origin\//, '') : '';
|
|
598
626
|
}
|
|
599
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
|
+
|
|
600
783
|
// ── naming (interactive only) ────────────────────────────────────────────────
|
|
601
784
|
|
|
602
785
|
// Everything about the repository worth telling a model that is choosing a
|
|
@@ -836,13 +1019,16 @@ async function typeItYourself(initial = '') {
|
|
|
836
1019
|
|
|
837
1020
|
// The single checkpoint before anything is created. `n` sends the user back to
|
|
838
1021
|
// describing the work, which is what they asked for; `e` is there because a
|
|
839
|
-
// suggestion that is one word off should not cost another round trip
|
|
840
|
-
|
|
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 } = {}) {
|
|
841
1025
|
log(`${dim('│')}`);
|
|
842
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')} ` : '';
|
|
843
1029
|
stderr.write(
|
|
844
|
-
`${dim('│')} create it? ${dim('[Y]es')} ${dim('·')} ${dim(
|
|
845
|
-
`${dim('·')} ${dim('[e]dit the name')} `,
|
|
1030
|
+
`${dim('│')} create it? ${dim('[Y]es')} ${dim('·')} ${dim(no)} ` +
|
|
1031
|
+
`${dim('·')} ${dim('[e]dit the name')} ${extra}`,
|
|
846
1032
|
);
|
|
847
1033
|
for (;;) {
|
|
848
1034
|
const buf = await waitForKey();
|
|
@@ -851,14 +1037,40 @@ async function confirmCreate(name, base) {
|
|
|
851
1037
|
if (c === 0x79 || c === 0x59 || c === 0x0d || c === 0x0a) { stderr.write('\n'); return { create: true }; }
|
|
852
1038
|
if (c === 0x6e || c === 0x4e) { stderr.write('\n'); return { again: true }; }
|
|
853
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
|
|
854
1059
|
}
|
|
855
1060
|
}
|
|
856
1061
|
|
|
857
1062
|
// The whole interactive path. Returns a branch name git has already accepted;
|
|
858
1063
|
// never runs unless there is a terminal and no positional was given.
|
|
859
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
|
+
|
|
860
1072
|
const ai = detectAi();
|
|
861
|
-
if (!ai) return typeItYourself();
|
|
1073
|
+
if (!ai) return { branch: await typeItYourself(), named: 'manual' };
|
|
862
1074
|
|
|
863
1075
|
const ctx = repoContext(dir, repo, base);
|
|
864
1076
|
const rejected = [];
|
|
@@ -869,24 +1081,24 @@ async function composeBranchName(dir, repo, base) {
|
|
|
869
1081
|
`${dim('│')} what do you want to do? ${dim('(any language)')}\n${dim('│')} ${dim('>')} `,
|
|
870
1082
|
);
|
|
871
1083
|
// An empty answer is the escape hatch out of the AI entirely.
|
|
872
|
-
if (!description) return typeItYourself();
|
|
1084
|
+
if (!description) return { branch: await typeItYourself(), named: 'manual' };
|
|
873
1085
|
|
|
874
1086
|
const res = await askAi(ai, namingPrompt(ctx, description, rejected));
|
|
875
1087
|
if (!res.ok) {
|
|
876
1088
|
warn(`${aiLabel(ai)} failed — name it yourself instead`);
|
|
877
1089
|
const first = (res.err || '').trim().split('\n')[0];
|
|
878
1090
|
if (first) log(`${dim('│')} ${dim(first.slice(0, 120))}`);
|
|
879
|
-
return typeItYourself();
|
|
1091
|
+
return { branch: await typeItYourself(), named: 'manual' };
|
|
880
1092
|
}
|
|
881
1093
|
const candidates = parseCandidates(res.out);
|
|
882
1094
|
if (candidates.length === 0) {
|
|
883
1095
|
warn(`${aiLabel(ai)} returned nothing usable — name it yourself instead`);
|
|
884
|
-
return typeItYourself();
|
|
1096
|
+
return { branch: await typeItYourself(), named: 'manual' };
|
|
885
1097
|
}
|
|
886
1098
|
|
|
887
1099
|
const choice = await confirmCreate(candidates[0], ctx.base);
|
|
888
|
-
if (choice.create) return candidates[0];
|
|
889
|
-
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' };
|
|
890
1102
|
// Rejected: remember every suggestion from this round so the next prompt
|
|
891
1103
|
// cannot come back with a near-identical name.
|
|
892
1104
|
rejected.push(...candidates);
|
|
@@ -1050,17 +1262,32 @@ async function main() {
|
|
|
1050
1262
|
}
|
|
1051
1263
|
|
|
1052
1264
|
let branch = positionals[0];
|
|
1265
|
+
let named = 'argument';
|
|
1053
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
|
+
}
|
|
1054
1271
|
// git would reject this later with a less obvious message.
|
|
1055
1272
|
if (!validBranchName(branch)) {
|
|
1056
1273
|
die('E_VALIDATION', `'${branch}' is not a valid branch name`);
|
|
1057
1274
|
}
|
|
1058
|
-
} else {
|
|
1059
|
-
|
|
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) {
|
|
1060
1280
|
die('E_VALIDATION', 'a branch name is required — `gwqadd <branch>`');
|
|
1061
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 {
|
|
1062
1289
|
// composeBranchName only ever returns a name git has already accepted.
|
|
1063
|
-
branch = await composeBranchName(cwd, repo, base);
|
|
1290
|
+
({ branch, named } = await composeBranchName(cwd, repo, base));
|
|
1064
1291
|
}
|
|
1065
1292
|
|
|
1066
1293
|
const branchExisted = hasLocalBranch(cwd, branch);
|
|
@@ -1075,7 +1302,7 @@ async function main() {
|
|
|
1075
1302
|
if (existing && existsSync(existing)) {
|
|
1076
1303
|
log(`${dim('│')} ${dim('worktree already exists')}`);
|
|
1077
1304
|
log(`${dim('└')} ${green('✓')} ${cyan(branch)} ${dim('→')} ${existing}`);
|
|
1078
|
-
return finish({ repo, branch, base, path: existing, created: 'none' });
|
|
1305
|
+
return finish({ repo, branch, base, path: existing, created: 'none', named });
|
|
1079
1306
|
}
|
|
1080
1307
|
|
|
1081
1308
|
// Two ways in. Without --from, `gwq add -b` creates branch and worktree in
|
|
@@ -1166,14 +1393,14 @@ async function main() {
|
|
|
1166
1393
|
|
|
1167
1394
|
log(`${dim('└')} ${green('✓')} ${cyan(branch)} ${dim('→')} ${created}`);
|
|
1168
1395
|
return finish({
|
|
1169
|
-
repo, branch, base, path: created,
|
|
1396
|
+
repo, branch, base, path: created, named,
|
|
1170
1397
|
created: branchExisted ? 'worktree' : 'branch+worktree',
|
|
1171
1398
|
});
|
|
1172
1399
|
}
|
|
1173
1400
|
|
|
1174
1401
|
// ── output ───────────────────────────────────────────────────────────────────
|
|
1175
1402
|
|
|
1176
|
-
async function finish({ repo, branch, base, path, created }) {
|
|
1403
|
+
async function finish({ repo, branch, base, path, created, named }) {
|
|
1177
1404
|
if (isJson) {
|
|
1178
1405
|
process.stdout.write(JSON.stringify({
|
|
1179
1406
|
schemaVersion: SCHEMA_VERSION,
|
|
@@ -1182,6 +1409,9 @@ async function finish({ repo, branch, base, path, created }) {
|
|
|
1182
1409
|
base: { ref: base.ref, sha: base.sha },
|
|
1183
1410
|
repo: { root: repo.root, name: repo.name },
|
|
1184
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,
|
|
1185
1415
|
cd: !stayOut,
|
|
1186
1416
|
}) + '\n');
|
|
1187
1417
|
return;
|