gwqadd 0.1.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +163 -0
  3. package/bin/gwqadd.mjs +842 -0
  4. package/package.json +42 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ryoshin0830
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,163 @@
1
+ # gwqadd
2
+
3
+ Create a branch and its [gwq](https://github.com/d-kuro/gwq) worktree in the repository you are in, and `cd` there.
4
+
5
+ ```console
6
+ ~/ghq/github.com/you/api $ gwqadd feat/login
7
+ ┌ gwqadd api
8
+ │ repo api /Users/you/ghq/github.com/you/api
9
+ │ base main 8f2c1a9
10
+ │ Created worktree at /Users/you/worktrees/github.com/you/api/feat-login
11
+ └ ✓ feat/login → /Users/you/worktrees/github.com/you/api/feat-login
12
+
13
+ ~/worktrees/github.com/you/api/feat-login $
14
+ ```
15
+
16
+ One command instead of: `git checkout -b` → realise you wanted a worktree →
17
+ `gwq add -b` → find where it landed → `cd`.
18
+
19
+ ## Install
20
+
21
+ ```sh
22
+ npm install -g gwqadd
23
+ ```
24
+
25
+ Then add the shell integration:
26
+
27
+ ```sh
28
+ # zsh — ~/.zshrc
29
+ eval "$(gwqadd --init zsh)"
30
+
31
+ # bash — ~/.bashrc
32
+ eval "$(gwqadd --init bash)"
33
+
34
+ # fish — ~/.config/fish/config.fish
35
+ gwqadd --init fish | source
36
+ ```
37
+
38
+ Reload the shell and `gwqadd` moves it.
39
+
40
+ Prefer a different name? `eval "$(gwqadd --init zsh --cmd gwa)"` gives you `gwa`.
41
+
42
+ ### Without installing
43
+
44
+ ```sh
45
+ eval "$(npx -y gwqadd --init zsh)"
46
+ ```
47
+
48
+ The emitted function resolves its binary in three steps — `gwqadd` on `PATH`,
49
+ then the script that generated the snippet, then `npx -y gwqadd@<version>` — so
50
+ it keeps working after npm garbage-collects the npx cache.
51
+
52
+ Requires `git` and `gwq` on `PATH` (`brew install git d-kuro/tap/gwq`), and
53
+ Node >= 20.12. No `fzf`, no `jq`, no npm dependencies.
54
+
55
+ ## Where it branches from
56
+
57
+ **The current HEAD, exactly like `git checkout -b`.** Running it inside a
58
+ feature worktree therefore branches off that feature, not off `main` — which is
59
+ almost never what you meant, and `gwq add -b` does it silently.
60
+
61
+ `gwqadd` always prints the base it used, and warns when that is not the
62
+ repository's default branch:
63
+
64
+ ```console
65
+ ~/worktrees/github.com/you/api/feat-login $ gwqadd feat/logout
66
+ ┌ gwqadd api
67
+ │ repo api /Users/you/ghq/github.com/you/api
68
+ │ cwd a linked worktree of it /Users/you/worktrees/github.com/you/api/feat-login
69
+ │ base feat/login 3b7d004
70
+ gwqadd: branching from feat/login, not the default branch — pass `--from main` if that is not what you meant
71
+ ```
72
+
73
+ So the fix is one flag:
74
+
75
+ ```sh
76
+ gwqadd feat/logout --from main
77
+ ```
78
+
79
+ ## What it does
80
+
81
+ 1. Work out which repository you are in — any worktree of it will do.
82
+ 2. Create the branch and its worktree. If the branch already exists, create
83
+ just the worktree. If both exist, go there.
84
+ 3. `git submodule update --init --recursive` when the tree has submodules.
85
+ 4. Hand the path back so the shell can `cd` there.
86
+
87
+ Re-running is safe.
88
+
89
+ ### It will not eat your work
90
+
91
+ - A colliding directory is only touched with `-f`, and then it is **moved** to
92
+ `<path>.bak-<timestamp>`, never deleted.
93
+ - A branch that already existed is never deleted, even when the run fails.
94
+ - A branch **`gwqadd` created** *is* rolled back if the worktree could not be
95
+ made — otherwise `git worktree add -b`'s half-finished state would turn every
96
+ later attempt into `branch already exists`.
97
+
98
+ ## Usage
99
+
100
+ ```
101
+ gwqadd [options] [<branch>]
102
+ ```
103
+
104
+ | Option | Meaning |
105
+ | --- | --- |
106
+ | `--init <shell>` | print shell integration for `zsh` \| `bash` \| `fish` |
107
+ | `--cmd <name>` | function name emitted by `--init` (default: `gwqadd`) |
108
+ | `--from <ref>` | branch from this ref instead of the current HEAD |
109
+ | `--expires <dur>` | hand gwq an expiry (`1h`, `7d`, …) for a throwaway worktree |
110
+ | `--no-submodules` | skip `git submodule update --init --recursive` |
111
+ | `-f`, `--force` | move a colliding worktree directory aside instead of failing |
112
+ | `-n`, `--no-cd` | do the work and report the path, but do not move the shell |
113
+ | `--json` | stdout = 1-line JSON |
114
+ | `--quiet` | stdout = path only |
115
+ | `--no-color` | disable ANSI colors (also respects `NO_COLOR`) |
116
+ | `-h`, `--help` | show help |
117
+ | `-V`, `--version` | show version |
118
+
119
+ Run it with no branch name and it asks for one.
120
+
121
+ ## For scripts and AI agents
122
+
123
+ ```console
124
+ $ gwqadd -n --json feat/login
125
+ {"schemaVersion":1,"path":"/Users/you/worktrees/github.com/you/api/feat-login","branch":"feat/login","base":{"ref":"main","sha":"8f2c1a9…"},"repo":{"root":"/Users/you/ghq/github.com/you/api","name":"api"},"created":"branch+worktree","cd":false}
126
+ ```
127
+
128
+ `created` is `branch+worktree`, `worktree` (the branch already existed) or
129
+ `none` (nothing to do). Progress narrates on stderr, so stdout stays parseable.
130
+
131
+ Errors go to stderr as JSON with stdout empty:
132
+
133
+ ```console
134
+ $ gwqadd --json feat/x
135
+ {"schemaVersion":1,"error":{"code":"E_NOT_REPO","message":"not inside a git repository (/tmp). …"},"exitCode":2}
136
+ ```
137
+
138
+ | Exit | Code | Meaning |
139
+ | --- | --- | --- |
140
+ | 0 | — | success |
141
+ | 1 | `E_VALIDATION` | bad flags, bad branch name, no branch name |
142
+ | 1 | `E_BRANCH` | `--from` ref unknown, or the branch could not be created |
143
+ | 1 | `E_WORKTREE` | `gwq add` failed (see the message for collisions) |
144
+ | 2 | `E_NOT_REPO` | not inside a git repository |
145
+ | 127 | `E_DEPS` | `git` or `gwq` not installed |
146
+ | 130 | `E_INTERRUPTED` | Ctrl-C |
147
+
148
+ Pass `-n` in an agent session, and pass `--from` explicitly rather than relying
149
+ on whatever HEAD the harness happens to be sitting on.
150
+
151
+ ## Which one do I want?
152
+
153
+ | | |
154
+ | --- | --- |
155
+ | [`gwqadd`](https://github.com/ryoshin0830/gwqadd) | new branch + worktree, **in the repo I am in** |
156
+ | [`gwqpull`](https://github.com/ryoshin0830/gwqpull) | get a repo **from a remote** and land on a branch or PR |
157
+ | [`gwqcd`](https://github.com/ryoshin0830/gwqcd) | jump to a worktree that **already exists** |
158
+ | [`ghqcd`](https://github.com/ryoshin0830/ghqcd) | jump to a **ghq repository** |
159
+ | [`ghnew`](https://github.com/ryoshin0830/ghnew) | create a **brand-new GitHub repo** |
160
+
161
+ ## License
162
+
163
+ MIT © ryoshin0830
package/bin/gwqadd.mjs ADDED
@@ -0,0 +1,842 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from 'node:child_process';
3
+ import { parseArgs } from 'node:util';
4
+ import { Buffer } from 'node:buffer';
5
+ import { readFileSync, existsSync, readdirSync, renameSync, realpathSync } from 'node:fs';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { createInterface } from 'node:readline/promises';
8
+
9
+ // Read from package.json rather than a hand-maintained constant: `npm version`
10
+ // only bumps the manifest, so a literal here silently drifts and `--version`
11
+ // then reports a build the user isn't running.
12
+ const VERSION = JSON.parse(
13
+ readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
14
+ ).version;
15
+ const SCHEMA_VERSION = 1;
16
+ const PKG = 'gwqadd';
17
+ const SELF = fileURLToPath(import.meta.url);
18
+
19
+ const HELP = `${PKG} ${VERSION} — create a branch and its gwq worktree here, and cd there.
20
+
21
+ USAGE
22
+ ${PKG} [options] [<branch>]
23
+ eval "$(${PKG} --init zsh)" # then \`${PKG}\` moves the shell itself
24
+
25
+ OPTIONS
26
+ --init <shell> print shell integration for zsh | bash | fish, then exit
27
+ --cmd <name> function name emitted by --init (default: ${PKG})
28
+ --from <ref> branch from this ref instead of the current HEAD
29
+ --expires <dur> hand gwq an expiry (1h, 7d, …) for a throwaway worktree
30
+ --no-submodules skip \`git submodule update --init --recursive\`
31
+ -f, --force move a colliding worktree directory aside instead of failing
32
+ -n, --no-cd do the work and report the path, but do not move the shell
33
+ --json stdout = 1-line JSON
34
+ --quiet stdout = path only (this is what the shell function uses)
35
+ --no-color disable ANSI colors (also respects NO_COLOR env)
36
+ -h, --help show this help
37
+ -V, --version show version
38
+
39
+ EXAMPLES
40
+ ${PKG} feat/login branch off HEAD, worktree, cd
41
+ ${PKG} feat/login --from main branch off main wherever you happen to be
42
+ ${PKG} hotfix/x --expires 1d gwq will mark it expired after a day
43
+ ${PKG} asks for the branch name
44
+ ${PKG} -n --json feat/login machine-readable, shell stays put
45
+
46
+ WHERE IT BRANCHES FROM
47
+ The current HEAD, exactly like \`git checkout -b\` — which means running this
48
+ inside a feature worktree branches off that feature, not off main. ${PKG}
49
+ always prints the base it used, and warns when that is not the default
50
+ branch. Pass --from to be explicit.
51
+
52
+ WHAT IT DOES
53
+ 1. work out which repository you are in (any worktree of it will do)
54
+ 2. create the branch and its worktree — or just the worktree if the branch
55
+ already exists, or neither if both do
56
+ 3. \`git submodule update --init --recursive\` when the tree has submodules
57
+ 4. hand the path back so the shell can cd there
58
+
59
+ Re-running is safe. A half-created branch is rolled back rather than left
60
+ to collide with the next attempt.
61
+
62
+ OUTPUT
63
+ Progress goes to stderr. stdout carries only the machine-readable result:
64
+ the path in --quiet, one line of JSON in --json, nothing in pretty mode.
65
+
66
+ --json:
67
+ {"schemaVersion":1,"path":"…","branch":"…","base":{"ref":"…","sha":"…"},
68
+ "repo":{"root":"…","name":"…"},"created":"branch+worktree","cd":true}
69
+
70
+ On error in --json mode, stdout is empty and stderr gets:
71
+ {"schemaVersion":1,"error":{"code":"E_NOT_REPO","message":"…"},"exitCode":2}
72
+
73
+ EXIT CODES
74
+ 0 success
75
+ 1 validation / branch / worktree failure
76
+ 2 not inside a git repository (E_NOT_REPO)
77
+ 127 git or gwq not installed (E_DEPS)
78
+ 130 interrupted — Ctrl-C (E_INTERRUPTED)
79
+ `;
80
+
81
+ // ── arg parsing ──────────────────────────────────────────────────────────────
82
+
83
+ // Detect --json early so even parseArgs / uncaughtException failures can
84
+ // produce a schema-compliant JSON error on stderr.
85
+ const rawJson = process.argv.slice(2).includes('--json');
86
+
87
+ function emitEarlyError(message, code = 'E_VALIDATION', exitCode = 1) {
88
+ if (rawJson) {
89
+ process.stderr.write(JSON.stringify({
90
+ schemaVersion: SCHEMA_VERSION,
91
+ error: { code, message },
92
+ exitCode,
93
+ }) + '\n');
94
+ } else {
95
+ process.stderr.write(`${PKG}: ${message}\n`);
96
+ process.stderr.write(`run \`${PKG} --help\` for usage.\n`);
97
+ }
98
+ process.exit(exitCode);
99
+ }
100
+
101
+ let values, positionals;
102
+ try {
103
+ ({ values, positionals } = parseArgs({
104
+ options: {
105
+ init: { type: 'string' },
106
+ cmd: { type: 'string' },
107
+ from: { type: 'string' },
108
+ expires: { type: 'string' },
109
+ 'no-submodules': { type: 'boolean' },
110
+ force: { type: 'boolean', short: 'f' },
111
+ 'no-cd': { type: 'boolean', short: 'n' },
112
+ json: { type: 'boolean' },
113
+ quiet: { type: 'boolean' },
114
+ 'no-color': { type: 'boolean' },
115
+ help: { type: 'boolean', short: 'h' },
116
+ version: { type: 'boolean', short: 'V' },
117
+ },
118
+ allowPositionals: true,
119
+ }));
120
+ } catch (err) {
121
+ emitEarlyError(err.message);
122
+ }
123
+
124
+ if (values.help) {
125
+ process.stdout.write(HELP);
126
+ process.exit(0);
127
+ }
128
+ if (values.version) {
129
+ process.stdout.write(`${PKG} ${VERSION}\n`);
130
+ process.exit(0);
131
+ }
132
+
133
+ // ── color helpers ────────────────────────────────────────────────────────────
134
+
135
+ const noColorEnv =
136
+ process.env.NO_COLOR != null && process.env.NO_COLOR !== '';
137
+ const useColor =
138
+ !noColorEnv && !values['no-color'] && process.stderr.isTTY;
139
+ const ansi = (code) =>
140
+ useColor ? (s) => `\x1b[${code}m${s}\x1b[0m` : (s) => String(s);
141
+ const dim = ansi(2);
142
+ const cyan = ansi(36);
143
+ const green = ansi(32);
144
+ const yellow = ansi(33);
145
+ const red = ansi(31);
146
+ const bold = ansi(1);
147
+
148
+ // ── output helpers ───────────────────────────────────────────────────────────
149
+
150
+ const isJson = !!values.json;
151
+ const isQuiet = !!values.quiet;
152
+
153
+ const stderr = process.stderr;
154
+ // --quiet is the shell function's mode and still narrates: it is the only way
155
+ // the user sees which repository and base ref were chosen. Only --json, whose
156
+ // contract is one line, goes silent. Nothing here ever touches stdout (I1).
157
+ const log = (s) => {
158
+ if (isJson) return;
159
+ stderr.write(s + '\n');
160
+ };
161
+ const warn = (s) => {
162
+ if (isJson) return;
163
+ stderr.write(`${yellow(`${PKG}:`)} ${s}\n`);
164
+ };
165
+
166
+ // ── error reporting ──────────────────────────────────────────────────────────
167
+
168
+ const EXIT = {
169
+ E_VALIDATION: 1,
170
+ E_BRANCH: 1,
171
+ E_WORKTREE: 1,
172
+ E_NOT_REPO: 2,
173
+ E_DEPS: 127,
174
+ E_INTERRUPTED: 130,
175
+ };
176
+
177
+ function die(code, message, extra = []) {
178
+ const exitCode = EXIT[code] ?? 1;
179
+ if (isJson) {
180
+ stderr.write(JSON.stringify({
181
+ schemaVersion: SCHEMA_VERSION,
182
+ error: { code, message },
183
+ exitCode,
184
+ }) + '\n');
185
+ } else if (code !== 'E_INTERRUPTED') {
186
+ stderr.write(`${red(`${PKG}:`)} ${message}\n`);
187
+ for (const line of extra) stderr.write(` ${line}\n`);
188
+ }
189
+ process.exit(exitCode);
190
+ }
191
+
192
+ // ── shell integration (--init) ───────────────────────────────────────────────
193
+
194
+ const SHELLS = ['zsh', 'bash', 'fish'];
195
+
196
+ // Single-quote for POSIX shells: close, escape, reopen.
197
+ const shq = (s) => `'${String(s).replaceAll("'", `'\\''`)}'`;
198
+ // fish single-quotes only treat \ and ' as special.
199
+ const fishq = (s) => `'${String(s).replaceAll('\\', '\\\\').replaceAll("'", "\\'")}'`;
200
+
201
+ // The emitted function resolves the binary in three steps, in this order:
202
+ //
203
+ // 1. `${PKG}` on PATH — a global install (`npm i -g ${PKG}`). Fastest, and
204
+ // the only one that picks up upgrades.
205
+ // 2. the absolute path of the script that generated this snippet. Covers
206
+ // `eval "$(npx -y ${PKG} --init zsh)"` for as long as that file survives.
207
+ // 3. `npx -y ${PKG}@<version>` — always correct, ~1s per call.
208
+ //
209
+ // Step 2 matters because npx caches under ~/.npm/_npx/<hash>/ and npm may
210
+ // garbage-collect it; step 3 is what keeps the shell working when it does.
211
+ // The lookup is PATH-only (`whence -p` / `type -P` / `command -s`) — the
212
+ // function usually shares its name with the binary, so a function-aware
213
+ // lookup would find the function and recurse forever.
214
+ function shellInit(shell, fnName) {
215
+ const desc = 'Create a branch and its gwq worktree here, and cd there';
216
+ const v = `${PKG}@${VERSION}`;
217
+ const slug = fnName.replaceAll(/[^A-Za-z0-9_]/g, '_');
218
+
219
+ if (shell === 'zsh') {
220
+ return `# ${PKG} ${VERSION} — zsh integration
221
+ # Add to ~/.zshrc: eval "$(${PKG} --init zsh)"
222
+
223
+ __${slug}_fallback=${shq(SELF)}
224
+
225
+ __${slug}_exec() {
226
+ local __bin
227
+ __bin=$(whence -p ${PKG} 2>/dev/null)
228
+ if [[ -n $__bin ]]; then
229
+ "$__bin" "$@"
230
+ elif [[ -x $__${slug}_fallback ]]; then
231
+ "$__${slug}_fallback" "$@"
232
+ else
233
+ npx -y ${shq(v)} "$@"
234
+ fi
235
+ }
236
+
237
+ # ${desc}.
238
+ ${fnName}() {
239
+ emulate -L zsh
240
+ local __dir
241
+ __dir=$(__${slug}_exec --quiet "$@") || return $?
242
+ # Empty is not a failure: --no-cd, --help and --version all succeed without
243
+ # naming a destination.
244
+ [[ -n $__dir ]] || return 0
245
+ builtin cd -- "$__dir"
246
+ }
247
+ `;
248
+ }
249
+
250
+ if (shell === 'bash') {
251
+ return `# ${PKG} ${VERSION} — bash integration
252
+ # Add to ~/.bashrc: eval "$(${PKG} --init bash)"
253
+
254
+ __${slug}_fallback=${shq(SELF)}
255
+
256
+ __${slug}_exec() {
257
+ local __bin
258
+ __bin=$(type -P ${PKG} 2>/dev/null)
259
+ if [ -n "$__bin" ]; then
260
+ "$__bin" "$@"
261
+ elif [ -x "$__${slug}_fallback" ]; then
262
+ "$__${slug}_fallback" "$@"
263
+ else
264
+ npx -y ${shq(v)} "$@"
265
+ fi
266
+ }
267
+
268
+ # ${desc}.
269
+ ${fnName}() {
270
+ local __dir
271
+ __dir=$(__${slug}_exec --quiet "$@") || return $?
272
+ [ -n "$__dir" ] || return 0
273
+ cd -- "$__dir"
274
+ }
275
+ `;
276
+ }
277
+
278
+ if (shell === 'fish') {
279
+ return `# ${PKG} ${VERSION} — fish integration
280
+ # Add to ~/.config/fish/config.fish: ${PKG} --init fish | source
281
+
282
+ set -g __${slug}_fallback ${fishq(SELF)}
283
+
284
+ function __${slug}_exec
285
+ set -l __bin (command -s ${PKG})
286
+ if test -n "$__bin"
287
+ $__bin $argv
288
+ else if test -x "$__${slug}_fallback"
289
+ $__${slug}_fallback $argv
290
+ else
291
+ npx -y ${fishq(v)} $argv
292
+ end
293
+ end
294
+
295
+ function ${fnName} --description ${fishq(desc)}
296
+ set -l __dir (__${slug}_exec --quiet $argv)
297
+ # \`set\` reports the command substitution's status, but not every fish
298
+ # release agrees on that. Capturing it keeps a failed run from cd'ing,
299
+ # and the empty-string guard below is correct either way.
300
+ set -l __st $status
301
+ if test $__st -ne 0
302
+ return $__st
303
+ end
304
+ if test -z "$__dir"
305
+ return 0
306
+ end
307
+ cd -- $__dir
308
+ end
309
+ `;
310
+ }
311
+
312
+ return null;
313
+ }
314
+
315
+ if (values.init != null) {
316
+ const shell = values.init;
317
+ if (!SHELLS.includes(shell)) {
318
+ emitEarlyError(`--init expects one of ${SHELLS.join(' | ')}, got '${shell}'`);
319
+ }
320
+ const fnName = values.cmd ?? PKG;
321
+ if (!/^[A-Za-z_][A-Za-z0-9_-]*$/.test(fnName)) {
322
+ emitEarlyError(`--cmd must be a valid shell function name, got '${fnName}'`);
323
+ }
324
+ process.stdout.write(shellInit(shell, fnName));
325
+ process.exit(0);
326
+ }
327
+
328
+ // ── argument validation ──────────────────────────────────────────────────────
329
+
330
+ if (values.json && values.quiet) {
331
+ die('E_VALIDATION', '--json and --quiet are mutually exclusive');
332
+ }
333
+ if (values.cmd != null) {
334
+ die('E_VALIDATION', '--cmd is only meaningful together with --init');
335
+ }
336
+ if (positionals.length > 1) {
337
+ die('E_VALIDATION', `unexpected extra arguments: ${positionals.slice(1).join(' ')}`);
338
+ }
339
+
340
+ const doSubmodules = !values['no-submodules'];
341
+ const force = !!values.force;
342
+ const stayOut = !!values['no-cd'];
343
+
344
+ // ── interactivity ────────────────────────────────────────────────────────────
345
+
346
+ const stdinTTY = !!process.stdin.isTTY;
347
+ const stderrTTY = !!process.stderr.isTTY;
348
+ const isNonInteractive = isJson || !stdinTTY || !stderrTTY;
349
+
350
+ // Children inherit stderr and have their stdout folded onto ours — which is
351
+ // stderr, never stdout. gwq's "Created worktree at …" must not end up inside
352
+ // the path the shell function is about to cd into.
353
+ const childStdio = isJson
354
+ ? ['inherit', 'ignore', 'ignore']
355
+ : ['inherit', 2, 'inherit'];
356
+
357
+ // ── tool checks ──────────────────────────────────────────────────────────────
358
+
359
+ function commandExists(cmd) {
360
+ const r = spawnSync(cmd, ['--version'], { stdio: 'ignore' });
361
+ return !(r.error && r.error.code === 'ENOENT');
362
+ }
363
+
364
+ const INSTALL = {
365
+ git: { brew: 'git', url: 'https://git-scm.com/downloads' },
366
+ gwq: { brew: 'd-kuro/tap/gwq', url: 'https://github.com/d-kuro/gwq#installation' },
367
+ };
368
+
369
+ function brewAvailable() {
370
+ return spawnSync('brew', ['--version'], { stdio: 'ignore' }).status === 0;
371
+ }
372
+
373
+ async function ensureTool(cmd) {
374
+ if (commandExists(cmd)) return;
375
+ const { brew, url } = INSTALL[cmd];
376
+ if (isNonInteractive) {
377
+ die('E_DEPS', `'${cmd}' not found in PATH. Install it with \`brew install ${brew}\` — ${url}`);
378
+ }
379
+ const ok = await confirmYesNo(`'${cmd}' not found. Install via 'brew install ${brew}'?`);
380
+ if (!ok) die('E_DEPS', `Aborted. See ${url}`);
381
+ if (!brewAvailable()) die('E_DEPS', `Homebrew unavailable. See ${url}`);
382
+ const r = spawnSync('brew', ['install', brew], { stdio: ['inherit', 2, 'inherit'] });
383
+ if (r.status !== 0) die('E_DEPS', `brew install ${brew} failed`);
384
+ }
385
+
386
+ // ── raw-mode keypress (no dependency on a prompt library) ────────────────────
387
+
388
+ let rawModeEngaged = false;
389
+ function disengageRawMode() {
390
+ if (rawModeEngaged && process.stdin.isTTY) {
391
+ try { process.stdin.setRawMode(false); } catch { /* ignore */ }
392
+ }
393
+ rawModeEngaged = false;
394
+ }
395
+ function restoreCursor() {
396
+ if (process.stderr.isTTY) {
397
+ try { process.stderr.write('\x1b[?25h'); } catch { /* ignore */ }
398
+ }
399
+ }
400
+
401
+ process.on('exit', () => { disengageRawMode(); restoreCursor(); });
402
+ for (const sig of ['SIGTERM', 'SIGHUP']) {
403
+ process.on(sig, () => { disengageRawMode(); restoreCursor(); process.exit(130); });
404
+ }
405
+ process.on('uncaughtException', (err) => {
406
+ disengageRawMode(); restoreCursor();
407
+ if (rawJson) {
408
+ stderr.write(JSON.stringify({
409
+ schemaVersion: SCHEMA_VERSION,
410
+ error: { code: 'E_VALIDATION', message: String(err?.message ?? err) },
411
+ exitCode: 1,
412
+ }) + '\n');
413
+ } else {
414
+ stderr.write(`${red(`${PKG}:`)} ${err?.stack ?? err}\n`);
415
+ }
416
+ process.exit(1);
417
+ });
418
+
419
+ async function waitForKey() {
420
+ process.stdin.removeAllListeners('data');
421
+ process.stdin.removeAllListeners('keypress');
422
+ try {
423
+ process.stdin.setRawMode(true);
424
+ rawModeEngaged = true;
425
+ } catch { /* setRawMode throws on non-TTY; let the keypress fall through */ }
426
+ process.stdin.resume();
427
+ try {
428
+ return await new Promise((resolve) => {
429
+ const handler = (buf) => {
430
+ process.stdin.removeListener('data', handler);
431
+ resolve(buf);
432
+ };
433
+ process.stdin.on('data', handler);
434
+ });
435
+ } finally {
436
+ disengageRawMode();
437
+ process.stdin.pause();
438
+ }
439
+ }
440
+
441
+ async function confirmYesNo(question) {
442
+ stderr.write(`${question} ${dim('[Y/n]')} `);
443
+ const buf = await waitForKey();
444
+ stderr.write('\n');
445
+ if (buf.includes(0x03)) process.exit(130);
446
+ const c = buf[0];
447
+ return c === 0x0d || c === 0x0a || c === 0x79 || c === 0x59; // Enter, y, Y
448
+ }
449
+
450
+ // A line, not a keypress — the branch name needs editing and history. The
451
+ // prompt goes to stderr so stdout stays the path channel (I1).
452
+ async function askLine(question) {
453
+ const rl = createInterface({ input: process.stdin, output: stderr, terminal: true });
454
+ try {
455
+ return (await rl.question(question)).trim();
456
+ } finally {
457
+ rl.close();
458
+ }
459
+ }
460
+
461
+ // ── git helpers ──────────────────────────────────────────────────────────────
462
+
463
+ const git = (dir, args, opts = {}) =>
464
+ spawnSync('git', ['-C', dir, ...args], { encoding: 'utf8', ...opts });
465
+
466
+ const gitOut = (dir, args) => {
467
+ const r = git(dir, args);
468
+ return r.status === 0 ? (r.stdout ?? '').trim() : '';
469
+ };
470
+
471
+ const hasLocalBranch = (dir, br) =>
472
+ git(dir, ['show-ref', '--verify', '--quiet', `refs/heads/${br}`], { stdio: 'ignore' }).status === 0;
473
+
474
+ // git reports resolved paths in `worktree list`, so a plain string compare
475
+ // against a path we assembled ourselves can miss (/var vs /private/var on
476
+ // macOS, or any symlinked checkout).
477
+ function samePath(a, b) {
478
+ if (a === b) return true;
479
+ try { return realpathSync(a) === realpathSync(b); } catch { return false; }
480
+ }
481
+
482
+ // The worktree path for a branch, or '' — read from git rather than
483
+ // reimplementing gwq's naming template, which we do not control.
484
+ function worktreePath(dir, branch) {
485
+ const out = gitOut(dir, ['worktree', 'list', '--porcelain']);
486
+ let current = '';
487
+ for (const line of out.split('\n')) {
488
+ if (line.startsWith('worktree ')) current = line.slice('worktree '.length);
489
+ else if (line === `branch refs/heads/${branch}`) return current;
490
+ }
491
+ return '';
492
+ }
493
+
494
+ // The repository we are standing in. Any worktree of it will do: gwq add works
495
+ // from a linked worktree, and running it from the user's cwd is what makes the
496
+ // default base match `git checkout -b`.
497
+ function resolveRepo() {
498
+ const top = gitOut(process.cwd(), ['rev-parse', '--show-toplevel']);
499
+ if (!top) {
500
+ die('E_NOT_REPO',
501
+ `not inside a git repository (${process.cwd()}). ` +
502
+ 'cd into one first, or use `gwqpull <repo> <branch>` to clone one.');
503
+ }
504
+ // The main working tree is the first entry of `worktree list`; we only need
505
+ // it to name the repository and to tell the user where they actually are.
506
+ const list = gitOut(top, ['worktree', 'list', '--porcelain']);
507
+ const mainWorktree = list.split('\n').find((l) => l.startsWith('worktree '))
508
+ ?.slice('worktree '.length) ?? top;
509
+ const name = mainWorktree.split('/').filter(Boolean).pop() ?? 'repository';
510
+ return { cwdTop: top, root: mainWorktree, name, inLinkedWorktree: !samePath(top, mainWorktree) };
511
+ }
512
+
513
+ // The ref a bare `gwq add -b` would branch from, resolved for display.
514
+ function resolveBase(dir, from) {
515
+ const ref = from || 'HEAD';
516
+ const sha = gitOut(dir, ['rev-parse', ref]);
517
+ if (!sha) {
518
+ die(from ? 'E_BRANCH' : 'E_NOT_REPO', from
519
+ ? `--from ${from} is not a ref this repository knows`
520
+ : 'HEAD does not resolve — is this an empty repository?');
521
+ }
522
+ const label = from || gitOut(dir, ['rev-parse', '--abbrev-ref', 'HEAD']) || sha.slice(0, 7);
523
+ return { ref: label, sha };
524
+ }
525
+
526
+ // origin/HEAD is the closest thing to "the branch people cut from". Missing on
527
+ // clones that never ran `git remote set-head`, so treat absence as unknown
528
+ // rather than guessing between main and master.
529
+ function defaultBranch(dir) {
530
+ const head = gitOut(dir, ['symbolic-ref', '--quiet', 'refs/remotes/origin/HEAD']);
531
+ return head ? head.replace(/^refs\/remotes\/origin\//, '') : '';
532
+ }
533
+
534
+ // ── width / box ──────────────────────────────────────────────────────────────
535
+
536
+ // Rough East Asian Width: 全角 CJK + 全角ラテン + half-symbols treated as wide.
537
+ // Good enough for box layouts; bail to one-line fallback when uncertain.
538
+ function charWidth(ch) {
539
+ const cp = ch.codePointAt(0);
540
+ if (cp === undefined) return 0;
541
+ if (cp < 0x20) return 0;
542
+ if (
543
+ (cp >= 0x1100 && cp <= 0x115F) ||
544
+ (cp >= 0x2E80 && cp <= 0x303E) ||
545
+ (cp >= 0x3041 && cp <= 0x33FF) ||
546
+ (cp >= 0x3400 && cp <= 0x4DBF) ||
547
+ (cp >= 0x4E00 && cp <= 0x9FFF) ||
548
+ (cp >= 0xA000 && cp <= 0xA4CF) ||
549
+ (cp >= 0xAC00 && cp <= 0xD7A3) ||
550
+ (cp >= 0xF900 && cp <= 0xFAFF) ||
551
+ (cp >= 0xFE30 && cp <= 0xFE4F) ||
552
+ (cp >= 0xFF00 && cp <= 0xFF60) ||
553
+ (cp >= 0xFFE0 && cp <= 0xFFE6) ||
554
+ (cp >= 0x1F300 && cp <= 0x1FAFF)
555
+ ) return 2;
556
+ return 1;
557
+ }
558
+ function strWidth(s) {
559
+ let w = 0;
560
+ for (const ch of s) w += charWidth(ch);
561
+ return w;
562
+ }
563
+
564
+ function renderBox(cdCommand) {
565
+ const cols = process.stdout.columns || process.stderr.columns || 80;
566
+ const inner = strWidth(cdCommand) + 4;
567
+ if (inner + 2 > cols - 2) return `${dim('next:')} ${cyan(cdCommand)}`;
568
+ const titleRaw = ' next ';
569
+ const titleW = strWidth(titleRaw);
570
+ const top = `╭─${titleRaw}${'─'.repeat(Math.max(0, inner - titleW - 1))}╮`;
571
+ const empty = `│${' '.repeat(inner)}│`;
572
+ const bot = `╰${'─'.repeat(inner)}╯`;
573
+ const pad = ' '.repeat(Math.max(0, inner - strWidth(cdCommand) - 2));
574
+ return [
575
+ dim(top), dim(empty),
576
+ dim('│ ') + cyan(cdCommand) + dim(pad + '│'),
577
+ dim(empty), dim(bot),
578
+ ].join('\n');
579
+ }
580
+
581
+ // ── clipboard ────────────────────────────────────────────────────────────────
582
+
583
+ function hasCmd(c) {
584
+ return spawnSync(c, ['--version'], { stdio: 'ignore' }).error?.code !== 'ENOENT'
585
+ || spawnSync('which', [c], { stdio: 'ignore' }).status === 0;
586
+ }
587
+ function clipboardCommand() {
588
+ if (process.platform === 'darwin') return { bin: 'pbcopy', args: [] };
589
+ if (process.env.WAYLAND_DISPLAY && hasCmd('wl-copy')) return { bin: 'wl-copy', args: [] };
590
+ if (process.env.DISPLAY && hasCmd('xclip')) {
591
+ return { bin: 'xclip', args: ['-selection', 'clipboard'] };
592
+ }
593
+ return null;
594
+ }
595
+ function copyToClipboard(text) {
596
+ if (process.env.SSH_CONNECTION || process.env.TMUX) {
597
+ try {
598
+ stderr.write(`\x1b]52;c;${Buffer.from(text).toString('base64')}\x07`);
599
+ } catch { /* ignore */ }
600
+ }
601
+ const cmd = clipboardCommand();
602
+ if (!cmd) {
603
+ stderr.write(dim('clipboard tool not found, copy manually\n'));
604
+ return false;
605
+ }
606
+ const r = spawnSync(cmd.bin, cmd.args, { input: text });
607
+ if (r.status !== 0) {
608
+ stderr.write(dim(`${cmd.bin} failed, copy manually\n`));
609
+ return false;
610
+ }
611
+ return true;
612
+ }
613
+
614
+ // ── worktree creation ────────────────────────────────────────────────────────
615
+
616
+ // gwq reports the git command it ran, so a collision's destination can be read
617
+ // back out of its error text.
618
+ const COLLISION = /git worktree add (?:-b [^ ]* )?(\/[^ :]*)/;
619
+
620
+ function timestamp() {
621
+ const d = new Date();
622
+ const p = (n) => String(n).padStart(2, '0');
623
+ return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}` +
624
+ `${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
625
+ }
626
+
627
+ // gwq add must run where the user is: `-b` branches from the HEAD of its cwd,
628
+ // which is what makes the default base behave like `git checkout -b`.
629
+ function runGwqAdd(cwd, args) {
630
+ const full = ['add', ...args];
631
+ if (values.expires) full.push('--expires', values.expires);
632
+ const r = spawnSync('gwq', full, { cwd, encoding: 'utf8' });
633
+ if (r.error) die('E_WORKTREE', `could not run gwq: ${r.error.message}`);
634
+ return { out: `${r.stdout ?? ''}${r.stderr ?? ''}`.trim(), status: r.status };
635
+ }
636
+
637
+ // ── main flow ────────────────────────────────────────────────────────────────
638
+
639
+ async function main() {
640
+ await ensureTool('git');
641
+ await ensureTool('gwq');
642
+
643
+ const repo = resolveRepo();
644
+ const cwd = process.cwd();
645
+
646
+ let branch = positionals[0];
647
+ if (!branch) {
648
+ if (isNonInteractive) {
649
+ die('E_VALIDATION', 'a branch name is required — `gwqadd <branch>`');
650
+ }
651
+ log(`${dim('┌')} ${bold(PKG)} ${dim(repo.name)}`);
652
+ branch = await askLine(`${dim('│')} new branch: `);
653
+ if (!branch) die('E_INTERRUPTED', 'cancelled');
654
+ } else {
655
+ log(`${dim('┌')} ${bold(PKG)} ${dim(repo.name)}`);
656
+ }
657
+
658
+ // git would reject this later with a less obvious message.
659
+ const check = spawnSync('git', ['check-ref-format', '--branch', branch], { stdio: 'ignore' });
660
+ if (check.status !== 0) {
661
+ die('E_VALIDATION', `'${branch}' is not a valid branch name`);
662
+ }
663
+
664
+ const base = resolveBase(cwd, values.from);
665
+ const branchExisted = hasLocalBranch(cwd, branch);
666
+
667
+ log(`${dim('│')} repo ${cyan(repo.name)} ${dim(repo.root)}`);
668
+ if (repo.inLinkedWorktree) {
669
+ log(`${dim('│')} cwd ${dim('a linked worktree of it')} ${dim(repo.cwdTop)}`);
670
+ }
671
+ if (!branchExisted) {
672
+ log(`${dim('│')} base ${cyan(base.ref)} ${dim(base.sha.slice(0, 7))}`);
673
+ // The silent version of this is the whole reason --from exists: running
674
+ // inside a feature worktree branches off that feature, not off main.
675
+ const def = defaultBranch(cwd);
676
+ if (!values.from && def && base.ref !== def && base.ref !== `origin/${def}`) {
677
+ warn(`branching from ${base.ref}, not the default branch — pass \`--from ${def}\` if that is not what you meant`);
678
+ }
679
+ }
680
+
681
+ git(cwd, ['worktree', 'prune'], { stdio: 'ignore' }); // clear hand-deleted leftovers
682
+
683
+ // Already done? Say so and go there. Re-running must be safe.
684
+ const existing = worktreePath(cwd, branch);
685
+ if (existing && existsSync(existing)) {
686
+ log(`${dim('│')} ${dim('worktree already exists')}`);
687
+ log(`${dim('└')} ${green('✓')} ${cyan(branch)} ${dim('→')} ${existing}`);
688
+ return finish({ repo, branch, base, path: existing, created: 'none' });
689
+ }
690
+
691
+ // Two ways in. Without --from, `gwq add -b` creates branch and worktree in
692
+ // one git call. With --from, gwq has no way to express a base, so the branch
693
+ // is made first and gwq is asked for the worktree only.
694
+ let weCreatedBranch = false;
695
+ let addArgs;
696
+ if (branchExisted) {
697
+ addArgs = [branch];
698
+ log(`${dim('│')} ${dim('branch exists — creating its worktree only')}`);
699
+ } else if (values.from) {
700
+ const made = git(cwd, ['branch', branch, values.from]);
701
+ if (made.status !== 0) {
702
+ die('E_BRANCH', `could not create ${branch} from ${values.from}`,
703
+ (made.stderr ?? '').trim().split('\n').filter(Boolean).slice(0, 2));
704
+ }
705
+ weCreatedBranch = true;
706
+ addArgs = [branch];
707
+ } else {
708
+ addArgs = ['-b', branch];
709
+ }
710
+
711
+ let { out, status } = runGwqAdd(cwd, addArgs);
712
+
713
+ // `git worktree add -b` creates the branch *before* it fails on the
714
+ // destination, so a failed run leaves a branch with no worktree. Left alone
715
+ // it turns the next attempt into "branch already exists" — the tool would
716
+ // stop being idempotent after its first bad day.
717
+ const rollbackBranch = () => {
718
+ const madeHere = weCreatedBranch || (!branchExisted && hasLocalBranch(cwd, branch));
719
+ if (!madeHere) return;
720
+ if (worktreePath(cwd, branch)) return; // it did get a worktree after all
721
+ if (git(cwd, ['branch', '-D', branch], { stdio: 'ignore' }).status === 0) {
722
+ warn(`rolled back the half-created branch ${branch}`);
723
+ }
724
+ };
725
+
726
+ if (status !== 0) {
727
+ const collide = out.match(COLLISION)?.[1] ?? '';
728
+ // gwq's own -f does not reach `git worktree add` (verified against v0.1.1),
729
+ // so a collision has to be cleared here or not at all.
730
+ if (collide && existsSync(collide) && force) {
731
+ const aside = `${collide}.bak-${timestamp()}`;
732
+ warn(`${collide} already exists — moving it to ${aside}`);
733
+ try {
734
+ renameSync(collide, aside);
735
+ } catch (err) {
736
+ rollbackBranch();
737
+ die('E_WORKTREE', `could not move ${collide} aside: ${err.message}`);
738
+ }
739
+ // The first attempt may have left the branch behind; reuse it rather
740
+ // than asking git to create it twice.
741
+ if (!branchExisted && hasLocalBranch(cwd, branch)) {
742
+ weCreatedBranch = true;
743
+ addArgs = [branch];
744
+ }
745
+ ({ out, status } = runGwqAdd(cwd, addArgs));
746
+ }
747
+
748
+ if (status !== 0) {
749
+ // A racing run may have created it already; that is a success.
750
+ const late = worktreePath(cwd, branch);
751
+ if (!late || !existsSync(late)) {
752
+ const detail = out.split('\n').filter((l) => /^(Error:|fatal:)/.test(l));
753
+ if (collide && existsSync(collide)) {
754
+ let count = '?';
755
+ try { count = String(readdirSync(collide).length); } catch { /* ignore */ }
756
+ detail.push(`${collide} still holds ${count} entries`);
757
+ detail.push('inspect and remove it, or re-run with -f to move it aside');
758
+ }
759
+ rollbackBranch();
760
+ die('E_WORKTREE', `could not create a worktree for ${branch}`, detail);
761
+ }
762
+ }
763
+ }
764
+
765
+ const created = worktreePath(cwd, branch);
766
+ if (!created || !existsSync(created)) {
767
+ rollbackBranch();
768
+ die('E_WORKTREE', `gwq reported success but no worktree for ${branch} could be found`);
769
+ }
770
+
771
+ if (doSubmodules && existsSync(`${created}/.gitmodules`)) {
772
+ log(`${dim('│')} initialising submodules`);
773
+ const r = git(created, ['submodule', 'update', '--init', '--recursive'], { stdio: childStdio });
774
+ if (r.status !== 0) warn('submodule initialisation failed');
775
+ }
776
+
777
+ log(`${dim('└')} ${green('✓')} ${cyan(branch)} ${dim('→')} ${created}`);
778
+ return finish({
779
+ repo, branch, base, path: created,
780
+ created: branchExisted ? 'worktree' : 'branch+worktree',
781
+ });
782
+ }
783
+
784
+ // ── output ───────────────────────────────────────────────────────────────────
785
+
786
+ async function finish({ repo, branch, base, path, created }) {
787
+ if (isJson) {
788
+ process.stdout.write(JSON.stringify({
789
+ schemaVersion: SCHEMA_VERSION,
790
+ path,
791
+ branch,
792
+ base: { ref: base.ref, sha: base.sha },
793
+ repo: { root: repo.root, name: repo.name },
794
+ created,
795
+ cd: !stayOut,
796
+ }) + '\n');
797
+ return;
798
+ }
799
+
800
+ if (isQuiet) {
801
+ // The shell function cds to whatever lands here, so --no-cd must print
802
+ // nothing at all rather than a path the wrapper would then follow.
803
+ if (!stayOut) process.stdout.write(path + '\n');
804
+ return;
805
+ }
806
+
807
+ if (stayOut) return;
808
+
809
+ // Pretty mode is the `npx ${PKG}` path: no shell function is in play, so the
810
+ // best we can do is hand over a cd command the user can paste or copy.
811
+ const cdCommand = `cd "${path}"`;
812
+ stderr.write('\n');
813
+ stderr.write(renderBox(cdCommand) + '\n');
814
+ stderr.write(
815
+ ` ${dim('tip:')} ${dim(`eval "$(${PKG} --init zsh)"`)} ${dim('lets')} ` +
816
+ `${bold(PKG)} ${dim('cd for you')}\n`,
817
+ );
818
+
819
+ if (!stdinTTY || !stderrTTY) return;
820
+ stderr.write(` ${dim('press')} ${bold('c')} ${dim('to copy')} ${dim('·')} ${dim('any other key to exit')}\n`);
821
+ const buf = await waitForKey();
822
+ if (buf.includes(0x03)) process.exit(130);
823
+ if (buf[0] === 99 || buf[0] === 67) {
824
+ if (copyToClipboard(cdCommand)) stderr.write(` ${green('✓')} ${dim('copied')}\n`);
825
+ }
826
+ }
827
+
828
+ main().catch((err) => {
829
+ disengageRawMode();
830
+ restoreCursor();
831
+ if (err?.code === 'ABORT_ERR' || err?.name === 'AbortError') process.exit(130);
832
+ if (isJson) {
833
+ stderr.write(JSON.stringify({
834
+ schemaVersion: SCHEMA_VERSION,
835
+ error: { code: 'E_VALIDATION', message: String(err?.message ?? err) },
836
+ exitCode: 1,
837
+ }) + '\n');
838
+ } else {
839
+ stderr.write(`${red(`${PKG}:`)} ${err?.stack ?? err}\n`);
840
+ }
841
+ process.exit(1);
842
+ });
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "gwqadd",
3
+ "version": "0.1.0",
4
+ "description": "Create a branch and its gwq worktree in the repository you are in, and cd there.",
5
+ "type": "module",
6
+ "bin": {
7
+ "gwqadd": "bin/gwqadd.mjs"
8
+ },
9
+ "engines": {
10
+ "node": ">=20.12.0"
11
+ },
12
+ "files": [
13
+ "bin",
14
+ "LICENSE",
15
+ "README.md"
16
+ ],
17
+ "keywords": [
18
+ "gwq",
19
+ "git",
20
+ "worktree",
21
+ "branch",
22
+ "cd",
23
+ "zsh",
24
+ "bash",
25
+ "fish",
26
+ "cli"
27
+ ],
28
+ "license": "MIT",
29
+ "author": "ryoshin0830",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/ryoshin0830/gwqadd.git"
33
+ },
34
+ "bugs": {
35
+ "url": "https://github.com/ryoshin0830/gwqadd/issues"
36
+ },
37
+ "homepage": "https://github.com/ryoshin0830/gwqadd#readme",
38
+ "scripts": {
39
+ "test": "node --test test/*.test.mjs",
40
+ "prepublishOnly": "npm test && npm pack --dry-run && node bin/gwqadd.mjs --help"
41
+ }
42
+ }