@junheep/gwt 0.1.0 → 0.2.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 (3) hide show
  1. package/README.md +40 -4
  2. package/bin/gwt.mjs +207 -15
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -33,6 +33,38 @@ confirmation. The integration also provides Zsh completion for commands,
33
33
  options, worktrees, and Git refs. It only changes directories; it does not load
34
34
  environment variables or run project hooks.
35
35
 
36
+ ## Coding agents
37
+
38
+ Coding agents do not know about `gwt` and reach for `git worktree add`, which
39
+ skips the copied files, assigned ports, and setup hooks. Install a skill that
40
+ tells them otherwise:
41
+
42
+ ```sh
43
+ gwt skill install claude
44
+ gwt skill install codex
45
+ ```
46
+
47
+ The installer shows the target path and asks for confirmation before writing.
48
+ Both agents read the same `SKILL.md` format and differ only in location, so
49
+ the installed skill is identical:
50
+
51
+ | Agent | Default | With `--project` |
52
+ | ------ | ------------------------------ | ---------------------------- |
53
+ | Claude | `~/.claude/skills/gwt/SKILL.md` | `.claude/skills/gwt/SKILL.md` |
54
+ | Codex | `~/.agents/skills/gwt/SKILL.md` | `.agents/skills/gwt/SKILL.md` |
55
+
56
+ `--project` writes into the primary worktree so the skill can be committed for
57
+ the team.
58
+
59
+ The skill covers what `gwt --help` does not: that gwt is preferred over native
60
+ `git worktree`, that project hooks need `gwt trust`, that a failed setup is
61
+ retried rather than recreated, that ports come from `gwt info`, and that
62
+ removal is destructive. It points at `gwt <command> --help` for command
63
+ details instead of repeating them, so it does not go stale as gwt changes.
64
+ Reinstall after upgrading to pick up a revised skill; an unchanged file is
65
+ reported as already installed, and a modified one is replaced only after
66
+ confirmation.
67
+
36
68
  ## Configuration
37
69
 
38
70
  Create user configuration for the current repository:
@@ -141,7 +173,8 @@ pnpm install --frozen-lockfile
141
173
 
142
174
  Hook paths in user config are resolved relative to the directory containing
143
175
  `config.json`; hook paths in `.gwt.json` are resolved relative to the target
144
- worktree. Both run with the target worktree as their working directory.
176
+ worktree. Both run with the target worktree as their working directory, and
177
+ their standard output and errors are streamed directly to the terminal.
145
178
 
146
179
  Hooks in user config are trusted because the user added them directly. Hooks
147
180
  from a committed `.gwt.json` require explicit trust because they execute
@@ -166,6 +199,7 @@ gwt trust [--revoke]
166
199
  gwt config create [--project]
167
200
  gwt config show
168
201
  gwt shell install zsh [--dry-run] [--yes]
202
+ gwt skill install <claude|codex> [--project] [--dry-run] [--yes]
169
203
  ```
170
204
 
171
205
  Run `gwt --help` for the command overview, or `gwt <command> --help` for
@@ -184,6 +218,8 @@ arrow keys, `j`/`k`, or Ctrl-n/Ctrl-p to move; press 1–9 to select a numbered
184
218
  row immediately; or press `/` to filter by branch, ID, or path. Enter switches
185
219
  to the selected worktree. Escape leaves filter mode or cancels the picker.
186
220
 
187
- `gwt remove` refuses dirty worktrees. It removes the branch only when
188
- `git branch -d` considers the deletion safe. `--discard --yes` explicitly
189
- allows dirty worktree removal and forced branch deletion.
221
+ `gwt remove` refuses dirty worktrees and first tries to delete the branch with
222
+ `git branch -d`. If Git rejects safe deletion, an interactive terminal asks
223
+ whether to force-delete the branch; non-interactive use keeps it and prints a
224
+ command for deleting it later. `--discard --yes` explicitly allows dirty
225
+ worktree removal and forced branch deletion.
package/bin/gwt.mjs CHANGED
@@ -23,12 +23,13 @@ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path"
23
23
  import { emitKeypressEvents } from "node:readline"
24
24
  import { createInterface } from "node:readline/promises"
25
25
 
26
- const VERSION = "0.1.0"
27
26
  const PROJECT_CONFIG_FILE = ".gwt.json"
28
27
  const PORT_MIN = 20_000
29
28
  const PORT_MAX = 39_999
30
29
  const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/
31
30
  const DEFAULT_CONFIG = { worktreeDirectory: ".worktrees", copyFiles: [], ports: [] }
31
+ const SKILL_DIRECTORIES = { claude: ".claude", codex: ".agents" }
32
+ const SKILL_USAGE = `Usage: gwt skill install <${Object.keys(SKILL_DIRECTORIES).join("|")}> [--project] [--dry-run] [--yes]`
32
33
 
33
34
  class CliError extends Error {}
34
35
 
@@ -496,14 +497,14 @@ function runHook(name, repository, configDocument, worktree, metadata) {
496
497
  GWT_BRANCH: context.branch,
497
498
  ...Object.fromEntries(Object.entries(context.ports).map(([key, value]) => [key, String(value)])),
498
499
  }
500
+ console.log(`Running ${name}...`)
499
501
  const result = run(hook.path, [], {
500
502
  cwd: context.path,
501
503
  env,
502
504
  input: `${JSON.stringify(context)}\n`,
503
505
  allowFailure: true,
506
+ stdio: ["pipe", "inherit", "inherit"],
504
507
  })
505
- if (result.stdout) process.stdout.write(result.stdout)
506
- if (result.stderr) process.stderr.write(result.stderr)
507
508
  if (result.status !== 0) throw new CliError(`${name} failed with status ${result.status}`)
508
509
  }
509
510
 
@@ -817,18 +818,46 @@ function worktreeRows(repository) {
817
818
  return rows
818
819
  }
819
820
 
821
+ function displayWidth(value) {
822
+ let width = 0
823
+ for (const character of value.normalize("NFC")) {
824
+ const codePoint = character.codePointAt(0)
825
+ if (/\p{Mark}/u.test(character) || codePoint === 0x200d) continue
826
+ const fullWidth = codePoint >= 0x1100 && (
827
+ codePoint <= 0x115f
828
+ || codePoint === 0x2329
829
+ || codePoint === 0x232a
830
+ || (codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f)
831
+ || (codePoint >= 0xac00 && codePoint <= 0xd7a3)
832
+ || (codePoint >= 0xf900 && codePoint <= 0xfaff)
833
+ || (codePoint >= 0xfe10 && codePoint <= 0xfe19)
834
+ || (codePoint >= 0xfe30 && codePoint <= 0xfe6f)
835
+ || (codePoint >= 0xff00 && codePoint <= 0xff60)
836
+ || (codePoint >= 0xffe0 && codePoint <= 0xffe6)
837
+ || (codePoint >= 0x1f300 && codePoint <= 0x1faff)
838
+ || (codePoint >= 0x20000 && codePoint <= 0x3fffd)
839
+ )
840
+ width += fullWidth ? 2 : 1
841
+ }
842
+ return width
843
+ }
844
+
845
+ function padDisplay(value, width) {
846
+ return `${value}${" ".repeat(Math.max(0, width - displayWidth(value)))}`
847
+ }
848
+
820
849
  function commandList(args) {
821
850
  if (args.length > 0) throw new CliError("Usage: gwt list")
822
851
  const repository = discoverRepository()
823
852
  const rows = worktreeRows(repository)
824
853
  const widths = {
825
- id: Math.max(2, ...rows.map((row) => row.id.length)),
826
- branch: Math.max(6, ...rows.map((row) => row.branch.length)),
827
- setup: Math.max(5, ...rows.map((row) => row.setup.length)),
854
+ id: Math.max(2, ...rows.map((row) => displayWidth(row.id))),
855
+ branch: Math.max(6, ...rows.map((row) => displayWidth(row.branch))),
856
+ setup: Math.max(5, ...rows.map((row) => displayWidth(row.setup))),
828
857
  }
829
- console.log(` ${"ID".padEnd(widths.id)} ${"BRANCH".padEnd(widths.branch)} ${"SETUP".padEnd(widths.setup)} PATH`)
858
+ console.log(` ${padDisplay("ID", widths.id)} ${padDisplay("BRANCH", widths.branch)} ${padDisplay("SETUP", widths.setup)} PATH`)
830
859
  for (const row of rows) {
831
- console.log(`${row.current ? "*" : " "} ${row.id.padEnd(widths.id)} ${row.branch.padEnd(widths.branch)} ${row.setup.padEnd(widths.setup)} ${row.path}`)
860
+ console.log(`${row.current ? "*" : " "} ${padDisplay(row.id, widths.id)} ${padDisplay(row.branch, widths.branch)} ${padDisplay(row.setup, widths.setup)} ${row.path}`)
832
861
  }
833
862
  }
834
863
 
@@ -890,6 +919,7 @@ async function commandRemove(args) {
890
919
  const removeArgs = ["worktree", "remove"]
891
920
  if (options.discard) removeArgs.push("--force")
892
921
  removeArgs.push(targetPath)
922
+ console.log(`Removing worktree ${metadata?.id ?? targetPath}...`)
893
923
  git(removeArgs, repository.primaryPath)
894
924
  if (metadata?.metadataPath && existsSync(metadata.metadataPath)) unlinkSync(metadata.metadataPath)
895
925
 
@@ -897,7 +927,20 @@ async function commandRemove(args) {
897
927
  if (worktree.branch && !options["keep-branch"]) {
898
928
  const deleteArgs = ["branch", options.discard ? "-D" : "-d", "--", worktree.branch]
899
929
  const result = git(deleteArgs, repository.primaryPath, { allowFailure: true })
900
- branchMessage = result.status === 0 ? `Deleted branch: ${worktree.branch}` : `Kept branch: ${worktree.branch}`
930
+ if (result.status === 0) {
931
+ branchMessage = `Deleted branch: ${worktree.branch}`
932
+ } else if (!options.discard) {
933
+ console.log(`Branch '${worktree.branch}' could not be deleted safely.`)
934
+ const forceDelete = await ask("Force-delete the branch? [y/N] ")
935
+ if (forceDelete) {
936
+ git(["branch", "-D", "--", worktree.branch], repository.primaryPath)
937
+ branchMessage = `Deleted branch: ${worktree.branch}`
938
+ } else {
939
+ branchMessage = `Kept branch: ${worktree.branch}\nDelete later: git branch -D -- ${worktree.branch}`
940
+ }
941
+ } else {
942
+ branchMessage = `Kept branch: ${worktree.branch}`
943
+ }
901
944
  } else if (worktree.branch) branchMessage = `Kept branch: ${worktree.branch}`
902
945
 
903
946
  console.log(`Removed worktree: ${metadata?.id ?? targetPath}`)
@@ -1034,6 +1077,7 @@ if command -v gwt >/dev/null 2>&1; then
1034
1077
  'trust:Approve project hooks'
1035
1078
  'config:Manage user and project configuration'
1036
1079
  'shell:Install shell integration'
1080
+ 'skill:Install the gwt skill for coding agents'
1037
1081
  )
1038
1082
 
1039
1083
  if (( CURRENT == 2 )); then
@@ -1091,6 +1135,15 @@ if command -v gwt >/dev/null 2>&1; then
1091
1135
  '--yes[skip installation confirmation]' \
1092
1136
  '(-h --help)'{-h,--help}'[show help]'
1093
1137
  ;;
1138
+ skill)
1139
+ _arguments \
1140
+ '2:action:(install)' \
1141
+ '3:agent:(claude codex)' \
1142
+ '--project[install into the repository instead of the home directory]' \
1143
+ '--dry-run[show the change without writing]' \
1144
+ '--yes[skip installation confirmation]' \
1145
+ '(-h --help)'{-h,--help}'[show help]'
1146
+ ;;
1094
1147
  esac
1095
1148
  }
1096
1149
 
@@ -1100,6 +1153,92 @@ if command -v gwt >/dev/null 2>&1; then
1100
1153
  fi`
1101
1154
  }
1102
1155
 
1156
+ function agentSkill() {
1157
+ return `---
1158
+ name: gwt
1159
+ description: Use gwt to create, list, switch, and remove Git worktrees. Use whenever a task needs an isolated worktree, or in place of running 'git worktree' directly.
1160
+ ---
1161
+
1162
+ gwt wraps native Git worktrees and prepares each one with the project's local
1163
+ files, assigned ports, and setup hooks.
1164
+
1165
+ ## Prefer gwt over 'git worktree'
1166
+
1167
+ Create worktrees with \`gwt new\`, not \`git worktree add\`. A worktree added with
1168
+ plain Git skips the configured file copies, port assignment, and postCreate
1169
+ hook, and records no gwt metadata, so \`gwt list\` and \`gwt remove\` cannot manage
1170
+ it. Adopt an existing one with \`gwt setup <path>\`.
1171
+
1172
+ ## Read the help instead of guessing flags
1173
+
1174
+ \`gwt --help\` lists the commands, \`gwt <command> --help\` documents arguments,
1175
+ options, and behavior, and nested commands such as \`gwt config create --help\`
1176
+ have their own help. This skill does not repeat command signatures so that they
1177
+ stay accurate across versions.
1178
+
1179
+ ## What the help does not make obvious
1180
+
1181
+ - Hooks declared by a committed \`.gwt.json\` do not run until the repository is
1182
+ approved with \`gwt trust\`. Approval is invalidated whenever the config or a
1183
+ hook changes, so a repository that worked before can start asking again.
1184
+ - A failed setup keeps the worktree and records the failure. Retry it with
1185
+ \`gwt setup <id>\` rather than removing and recreating the worktree.
1186
+ - Ports are assigned per worktree. Read them from \`gwt info\` instead of assuming
1187
+ a project default; two worktrees of the same project never share a port.
1188
+ - \`gwt switch\` changes the shell's directory only when the shell integration is
1189
+ installed. Otherwise it just prints the path.
1190
+ - \`gwt switch\` with no target opens an interactive picker, so always pass an
1191
+ explicit target when running non-interactively.
1192
+
1193
+ ## Removal is destructive
1194
+
1195
+ \`gwt remove\` deletes the worktree and, by default, its branch. Confirm with the
1196
+ user before running it, and never pass \`--discard --yes\` on your own: together
1197
+ they discard uncommitted changes and force-delete an unmerged branch.
1198
+ `
1199
+ }
1200
+
1201
+ function skillPath(agent, project) {
1202
+ const base = project ? discoverRepository().primaryPath : homedir()
1203
+ return join(base, SKILL_DIRECTORIES[agent], "skills", "gwt", "SKILL.md")
1204
+ }
1205
+
1206
+ async function installSkill(agent, args) {
1207
+ const { options, positionals } = parseOptions(args, {
1208
+ "--project": "boolean",
1209
+ "--dry-run": "boolean",
1210
+ "--yes": "boolean",
1211
+ })
1212
+ if (positionals.length > 0) throw new CliError(SKILL_USAGE)
1213
+
1214
+ const path = skillPath(agent, options.project)
1215
+ const contents = agentSkill()
1216
+ const existing = existsSync(path) ? readFileSync(path, "utf8") : null
1217
+ if (existing === contents) {
1218
+ console.log(`Skill is already installed in ${path}`)
1219
+ return
1220
+ }
1221
+
1222
+ const verb = existing === null ? "Create" : "Replace"
1223
+ console.log(`${verb} ${path}`)
1224
+ if (options["dry-run"]) {
1225
+ console.log(`\n${contents}`)
1226
+ return
1227
+ }
1228
+ if (!options.yes && !(await ask(`${verb}? [y/N] `))) throw new CliError("Skill installation cancelled")
1229
+
1230
+ mkdirSync(dirname(path), { recursive: true })
1231
+ writeFileSync(path, contents)
1232
+ console.log(`Installed skill in ${path}`)
1233
+ }
1234
+
1235
+ async function commandSkill(args) {
1236
+ if (args[0] === "install" && Object.hasOwn(SKILL_DIRECTORIES, args[1])) {
1237
+ return installSkill(args[1], args.slice(2))
1238
+ }
1239
+ throw new CliError(SKILL_USAGE)
1240
+ }
1241
+
1103
1242
  function zshConfigPath() {
1104
1243
  return join(process.env.ZDOTDIR ? resolve(process.env.ZDOTDIR) : homedir(), ".zshrc")
1105
1244
  }
@@ -1169,10 +1308,15 @@ function commandComplete(args) {
1169
1308
  throw new CliError("Invalid completion request")
1170
1309
  }
1171
1310
 
1311
+ function version() {
1312
+ const path = join(import.meta.dirname, "..", "package.json")
1313
+ return JSON.parse(readFileSync(path, "utf8")).version
1314
+ }
1315
+
1172
1316
  function help(command, subcommand) {
1173
1317
  const topic = [command, subcommand].filter(Boolean).join(" ")
1174
1318
  const texts = {
1175
- "": `gwt ${VERSION} - lightweight native Git worktree workflows
1319
+ "": `gwt ${version()} - lightweight native Git worktree workflows
1176
1320
 
1177
1321
  Usage:
1178
1322
  gwt <command> [options]
@@ -1187,6 +1331,7 @@ Commands:
1187
1331
  trust Approve or revoke repository project hooks
1188
1332
  config Create or inspect configuration
1189
1333
  shell Install shell integration
1334
+ skill Install the gwt skill for coding agents
1190
1335
 
1191
1336
  Options:
1192
1337
  -h, --help Show help.
@@ -1198,6 +1343,7 @@ Examples:
1198
1343
  gwt remove
1199
1344
  gwt config create
1200
1345
  gwt shell install zsh
1346
+ gwt skill install claude
1201
1347
 
1202
1348
  Run 'gwt <command> --help' for command behavior and more examples.`,
1203
1349
  new: `Create a worktree, prepare its development environment, and switch to it.
@@ -1311,9 +1457,10 @@ Options:
1311
1457
 
1312
1458
  Behavior:
1313
1459
  Without --discard, dirty worktrees are rejected and branches are deleted only
1314
- when 'git branch -d' considers deletion safe. The primary worktree cannot be
1315
- removed. Removing the current worktree returns an integrated shell to the
1316
- primary worktree.
1460
+ when 'git branch -d' considers deletion safe. If safe deletion fails, an
1461
+ interactive terminal asks whether to force-delete the branch; non-interactive
1462
+ use keeps it. The primary worktree cannot be removed. Removing the current
1463
+ worktree returns an integrated shell to the primary worktree.
1317
1464
 
1318
1465
  Examples:
1319
1466
  gwt remove
@@ -1417,6 +1564,50 @@ when ZDOTDIR is set. Restart Zsh or source the file after installation.
1417
1564
  Examples:
1418
1565
  gwt shell install zsh
1419
1566
  gwt shell install zsh --dry-run`,
1567
+ skill: `Install the gwt skill so coding agents use gwt correctly.
1568
+
1569
+ Usage:
1570
+ gwt skill install <claude|codex> [--project] [--dry-run] [--yes]
1571
+
1572
+ Options:
1573
+ -h, --help Show help for this command.
1574
+
1575
+ The skill teaches an agent to prefer gwt over native 'git worktree', to read
1576
+ 'gwt <command> --help' for command details, and to treat removal as
1577
+ destructive. It does not duplicate command signatures, so it stays accurate
1578
+ as gwt changes.
1579
+
1580
+ Examples:
1581
+ gwt skill install claude
1582
+ gwt skill install codex`,
1583
+ "skill install": `Install the gwt skill for a coding agent.
1584
+
1585
+ Usage:
1586
+ gwt skill install <claude|codex> [--project] [--dry-run] [--yes]
1587
+
1588
+ Arguments:
1589
+ claude Install for Claude Code, under .claude/skills.
1590
+ codex Install for Codex, under .agents/skills.
1591
+
1592
+ Options:
1593
+ --project Write the skill inside the primary worktree, so it can be
1594
+ committed for the team, instead of the home directory.
1595
+ --dry-run Print the target path and the skill without writing it.
1596
+ --yes Install without asking for confirmation.
1597
+ -h, --help Show help for this command.
1598
+
1599
+ Both agents read the same SKILL.md format and only differ in location, so the
1600
+ installed skill is identical. Install it once per agent.
1601
+
1602
+ Reinstall after upgrading gwt to pick up a revised skill. The command reports
1603
+ an unchanged file as already installed and asks before replacing a modified
1604
+ one.
1605
+
1606
+ Examples:
1607
+ gwt skill install claude
1608
+ gwt skill install codex
1609
+ gwt skill install codex --project
1610
+ gwt skill install claude --dry-run`,
1420
1611
  }
1421
1612
 
1422
1613
  if (!Object.hasOwn(texts, topic)) throw new CliError(`Unknown help topic: ${topic}`)
@@ -1427,9 +1618,9 @@ async function main() {
1427
1618
  const [command, ...args] = process.argv.slice(2)
1428
1619
  if (!command || command === "--help" || command === "-h") return help()
1429
1620
  if (command === "help") return help(args[0], args[1])
1430
- if (command === "--version" || command === "-V") return console.log(VERSION)
1621
+ if (command === "--version" || command === "-V") return console.log(version())
1431
1622
  if (args.includes("--help") || args.includes("-h")) {
1432
- const subcommand = ["config", "shell"].includes(command)
1623
+ const subcommand = ["config", "shell", "skill"].includes(command)
1433
1624
  ? args.find((argument) => !argument.startsWith("-"))
1434
1625
  : undefined
1435
1626
  return help(command, subcommand)
@@ -1443,6 +1634,7 @@ async function main() {
1443
1634
  if (command === "trust") return commandTrust(args)
1444
1635
  if (command === "config") return commandConfig(args)
1445
1636
  if (command === "shell") return commandShell(args)
1637
+ if (command === "skill") return commandSkill(args)
1446
1638
  if (command === "__complete") return commandComplete(args)
1447
1639
  throw new CliError(`Unknown command: ${command}`)
1448
1640
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junheep/gwt",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Lightweight native Git worktree workflows",
5
5
  "license": "MIT",
6
6
  "author": "Junhee Park",