gwqadd 0.3.0 → 0.3.2
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/README.md +5 -0
- package/bin/gwqadd.mjs +70 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -135,6 +135,11 @@ question. At that point the sentence, the repository name, its branch names and
|
|
|
135
135
|
your modified file *paths* (never contents) go to that CLI. Nothing is created
|
|
136
136
|
until you confirm.
|
|
137
137
|
|
|
138
|
+
The CLI is run in an **empty temporary directory**, not in your repository.
|
|
139
|
+
These tools are agents: given a repo they will read `CLAUDE.md`, your source and
|
|
140
|
+
your git log, and then name the branch after what they found instead of what you
|
|
141
|
+
asked for. Everything they should know is already in the prompt.
|
|
142
|
+
|
|
138
143
|
None of this happens when you pass a branch name on the command line, or when
|
|
139
144
|
there is no terminal. Scripts and agents keep the plain, silent contract.
|
|
140
145
|
|
package/bin/gwqadd.mjs
CHANGED
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
import { spawnSync, spawn } from 'node:child_process';
|
|
3
3
|
import { parseArgs } from 'node:util';
|
|
4
4
|
import { Buffer } from 'node:buffer';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
readFileSync, existsSync, readdirSync, renameSync, realpathSync,
|
|
7
|
+
mkdtempSync, rmSync,
|
|
8
|
+
} from 'node:fs';
|
|
9
|
+
import { tmpdir } from 'node:os';
|
|
10
|
+
import { join as joinPath } from 'node:path';
|
|
6
11
|
import { fileURLToPath } from 'node:url';
|
|
7
12
|
import { createInterface } from 'node:readline/promises';
|
|
8
13
|
|
|
@@ -627,6 +632,7 @@ function namingPrompt(ctx, description, rejected) {
|
|
|
627
632
|
? `Files already modified in the working tree, which is what the work touches:\n${ctx.dirty.map((f) => ` ${f}`).join('\n')}`
|
|
628
633
|
: '',
|
|
629
634
|
'Include the prefix and a slash. After it use lowercase ASCII words joined by hyphens. Under 40 characters. Be specific about this change, not generic.',
|
|
635
|
+
'The description below is the only thing the branch is about. Do not name it after anything else you can see.',
|
|
630
636
|
rejected.length
|
|
631
637
|
? `These were rejected — do not propose them or close variants:\n${rejected.map((r) => ` ${r}`).join('\n')}`
|
|
632
638
|
: '',
|
|
@@ -662,15 +668,42 @@ function detectAi() {
|
|
|
662
668
|
return AI_CLIS.find((c) => commandExists(c.bin)) ?? null;
|
|
663
669
|
}
|
|
664
670
|
|
|
665
|
-
//
|
|
666
|
-
//
|
|
671
|
+
// These CLIs are agents, not text transformers: run one inside a repository and
|
|
672
|
+
// it reads CLAUDE.md, the source and the git history, then names the branch
|
|
673
|
+
// after what it found instead of what the user asked for. Measured in this very
|
|
674
|
+
// repository — description "uiのバグの修正", three runs each:
|
|
675
|
+
//
|
|
676
|
+
// cwd = the repo feat/ui-bug-fix, fix/ui-display-bug, feat/ui-bug-fix
|
|
677
|
+
// cwd = empty dir fix/ui-display-bug, fix/ui-display-bug, fix/ui-display-bug
|
|
678
|
+
//
|
|
679
|
+
// In-repo it both wavered and picked `feat/` for a bug fix twice. It once
|
|
680
|
+
// answered `feat/naming-prompt-repo-context`, which is a phrase straight out of
|
|
681
|
+
// this repo's CLAUDE.md. Everything the model legitimately needs is already in
|
|
682
|
+
// the prompt, so it runs in an empty directory and is given nothing else.
|
|
683
|
+
//
|
|
684
|
+
// spawnSync would also freeze the terminal for the 6-8 seconds these CLIs take
|
|
685
|
+
// to boot, with no sign of life; async plus an elapsed counter is the difference
|
|
667
686
|
// between "working" and "hung".
|
|
668
687
|
function runAi(ai, prompt) {
|
|
669
688
|
return new Promise((resolve) => {
|
|
689
|
+
let sandbox = '';
|
|
690
|
+
try {
|
|
691
|
+
sandbox = mkdtempSync(joinPath(tmpdir(), 'gwqadd-ai-'));
|
|
692
|
+
} catch { /* fall back to inheriting cwd rather than not answering at all */ }
|
|
693
|
+
const cleanup = () => {
|
|
694
|
+
if (!sandbox) return;
|
|
695
|
+
try { rmSync(sandbox, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
696
|
+
sandbox = '';
|
|
697
|
+
};
|
|
698
|
+
|
|
670
699
|
let child;
|
|
671
700
|
try {
|
|
672
|
-
child = spawn(ai.bin, [...ai.args, prompt], {
|
|
701
|
+
child = spawn(ai.bin, [...ai.args, prompt], {
|
|
702
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
703
|
+
...(sandbox ? { cwd: sandbox } : {}),
|
|
704
|
+
});
|
|
673
705
|
} catch (err) {
|
|
706
|
+
cleanup();
|
|
674
707
|
return resolve({ ok: false, out: '', err: String(err?.message ?? err) });
|
|
675
708
|
}
|
|
676
709
|
let out = '';
|
|
@@ -680,10 +713,12 @@ function runAi(ai, prompt) {
|
|
|
680
713
|
const kill = setTimeout(() => { try { child.kill('SIGKILL'); } catch { /* gone */ } }, 60_000);
|
|
681
714
|
child.on('error', (e) => {
|
|
682
715
|
clearTimeout(kill);
|
|
716
|
+
cleanup();
|
|
683
717
|
resolve({ ok: false, out: '', err: String(e?.message ?? e) });
|
|
684
718
|
});
|
|
685
719
|
child.on('close', (code) => {
|
|
686
720
|
clearTimeout(kill);
|
|
721
|
+
cleanup();
|
|
687
722
|
resolve({ ok: code === 0, out, err });
|
|
688
723
|
});
|
|
689
724
|
});
|
|
@@ -906,9 +941,36 @@ function copyToClipboard(text) {
|
|
|
906
941
|
|
|
907
942
|
// ── worktree creation ────────────────────────────────────────────────────────
|
|
908
943
|
|
|
909
|
-
//
|
|
910
|
-
//
|
|
911
|
-
|
|
944
|
+
// A collision's destination has to be recovered from gwq's error text. Two
|
|
945
|
+
// sources, in order of reliability:
|
|
946
|
+
//
|
|
947
|
+
// fatal: '<path>' already exists <- git, quoted, unambiguous
|
|
948
|
+
// ...: git worktree add [-b <branch>] <path>: ...
|
|
949
|
+
//
|
|
950
|
+
// The quoted form is preferred because the command echo is not parseable in
|
|
951
|
+
// general: `-b` swaps the argument order (`add -b <branch> <path>` versus
|
|
952
|
+
// `add <path> <branch>`), and a path containing a space silently truncated the
|
|
953
|
+
// old pattern — a gwq basedir under a directory with a space made `-f` do
|
|
954
|
+
// nothing at all, without saying so.
|
|
955
|
+
const COLLISION_QUOTED = /fatal: '([^']+)' already exists/;
|
|
956
|
+
|
|
957
|
+
// The command echo needs to know which form was used, because `-b` swaps the
|
|
958
|
+
// argument order and both a path and a branch can follow `add`:
|
|
959
|
+
//
|
|
960
|
+
// gwq add -b <branch> -> git worktree add -b <branch> <path>: …
|
|
961
|
+
// gwq add <branch> -> git worktree add <path> <branch>: …
|
|
962
|
+
//
|
|
963
|
+
// Guessing cost real time once already: a pattern that stopped at the first
|
|
964
|
+
// space read the path-first form correctly by accident, and a pattern that ran
|
|
965
|
+
// to the colon swallowed the branch with it.
|
|
966
|
+
const collisionFromCmd = (out, withB) => (withB
|
|
967
|
+
? out.match(/git worktree add -b \S+ (.+?): /)
|
|
968
|
+
: out.match(/git worktree add (.+?) \S+: /))?.[1];
|
|
969
|
+
|
|
970
|
+
function collisionPath(out, withB) {
|
|
971
|
+
const quoted = out.match(COLLISION_QUOTED)?.[1];
|
|
972
|
+
return (quoted ?? collisionFromCmd(out, withB) ?? '').trim();
|
|
973
|
+
}
|
|
912
974
|
|
|
913
975
|
function timestamp() {
|
|
914
976
|
const d = new Date();
|
|
@@ -1019,7 +1081,7 @@ async function main() {
|
|
|
1019
1081
|
};
|
|
1020
1082
|
|
|
1021
1083
|
if (status !== 0) {
|
|
1022
|
-
const collide = out
|
|
1084
|
+
const collide = collisionPath(out, addArgs[0] === '-b');
|
|
1023
1085
|
// gwq's own -f does not reach `git worktree add` (verified against v0.1.1),
|
|
1024
1086
|
// so a collision has to be cleared here or not at all.
|
|
1025
1087
|
if (collide && existsSync(collide) && force) {
|