@cestoliv/wt 0.1.0-pr1.f38ce15

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 ADDED
@@ -0,0 +1,116 @@
1
+ # wt
2
+
3
+ A fast, interactive TUI for managing git worktrees.
4
+
5
+ Browse, create, open, and delete worktrees without leaving the terminal. Fuzzy search across branches, auto-open your IDE, and run setup commands on new worktrees — all from one tool.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g @cestoliv/wt
11
+ ```
12
+
13
+ Requires Node.js 20+ and Git. The command is `wt`.
14
+
15
+ ### Update
16
+
17
+ ```bash
18
+ npm install -g @cestoliv/wt
19
+ ```
20
+
21
+ ### Pre-release builds
22
+
23
+ Add the `publish-dev` label to a PR to publish that branch as a unique,
24
+ pinned prerelease version (e.g. `0.1.0-pr12.abc1234`). The exact install
25
+ command is posted as a comment on the PR. There is no rolling `dev` channel —
26
+ each build is a distinct version you install explicitly.
27
+
28
+ ## Quick Start
29
+
30
+ ```bash
31
+ # Inside any git repo
32
+ wt # Browse worktrees
33
+ wt create my-feat # Create a new worktree and open it in your IDE
34
+ wt config # Edit config in $EDITOR
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ### Browse worktrees — `wt`
40
+
41
+ Launches an interactive list of worktrees with fuzzy search.
42
+
43
+ ```
44
+ MY-PROJECT
45
+ ▶ main ~/dev/my-project
46
+ fix: resolve auth bug (2h ago)
47
+ feat/dashboard ~/dev/my-project-feat-dashboard
48
+ wip: add chart component (1d ago)
49
+
50
+ ↕ navigate · Enter open · D delete · C create · Q quit
51
+ ```
52
+
53
+ | Key | Action |
54
+ | ------- | ------------------------------- |
55
+ | `↑` `↓` | Navigate |
56
+ | `Enter` | Open worktree in IDE |
57
+ | `D` | Delete worktree (with confirm) |
58
+ | `C` | Create new worktree (repo mode) |
59
+ | `Q` | Quit |
60
+
61
+ Type to fuzzy-filter branches instantly.
62
+
63
+ **Repo mode** (inside a git repo): shows worktrees for that repo.
64
+ **Global mode** (outside a repo): shows worktrees across all registered repos.
65
+
66
+ ### Create a worktree — `wt create [branch]`
67
+
68
+ ```bash
69
+ wt create feat/login # Create from base branch (origin/main by default)
70
+ wt create # Prompts for branch name
71
+ ```
72
+
73
+ What happens:
74
+
75
+ 1. Creates a worktree as a sibling directory: `../my-project-feat-login`
76
+ 2. Runs configured setup commands (e.g., `npm install`)
77
+ 3. Opens the worktree in your IDE
78
+
79
+ Run outside a repo to pick from registered repos via an interactive picker.
80
+
81
+ ### Edit config — `wt config`
82
+
83
+ Opens the config file in `$EDITOR`.
84
+
85
+ ## Configuration
86
+
87
+ Config is stored at `~/Library/Preferences/wt-nodejs/config.json` (macOS).
88
+
89
+ ```json
90
+ {
91
+ "ide": "code",
92
+ "ide_open_args": ["-n"],
93
+ "base_branch": "origin/main",
94
+ "worktree_path": "../",
95
+ "setup_commands": ["npm install"],
96
+ "repo_overrides": {
97
+ "/path/to/special-repo": {
98
+ "base_branch": "origin/develop",
99
+ "setup_commands": ["pnpm install", "pnpm build"]
100
+ }
101
+ }
102
+ }
103
+ ```
104
+
105
+ | Key | Default | Description |
106
+ | ---------------- | --------------- | --------------------------------------------- |
107
+ | `ide` | `"zed"` | Editor to open worktrees with |
108
+ | `ide_open_args` | `["-n"]` | Extra args passed to the IDE command |
109
+ | `base_branch` | `"origin/main"` | Branch new worktrees are created from |
110
+ | `worktree_path` | `"../"` | Where worktrees are placed (relative to repo) |
111
+ | `setup_commands` | `[]` | Commands to run in new worktrees |
112
+ | `repo_overrides` | `{}` | Per-repo overrides for any of the above |
113
+
114
+ ## License
115
+
116
+ MIT
@@ -0,0 +1,490 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ createStore,
4
+ getGlobalConfig
5
+ } from "./chunk-HIGP6FSR.js";
6
+
7
+ // src/lib/git.ts
8
+ import { execFileSync } from "child_process";
9
+ import { realpathSync } from "fs";
10
+ import path from "path";
11
+ function getRepoRoot(cwd = process.cwd()) {
12
+ try {
13
+ const realCwd = realpathSync(cwd);
14
+ return execFileSync("git", ["rev-parse", "--show-toplevel"], {
15
+ cwd: realCwd,
16
+ encoding: "utf8",
17
+ stdio: "pipe"
18
+ }).trim();
19
+ } catch {
20
+ throw new Error("Not in a git repository");
21
+ }
22
+ }
23
+ function listWorktrees(repoRoot, cwd = process.cwd()) {
24
+ const realRepoRoot = realpathSync(repoRoot);
25
+ const realCwd = realpathSync(cwd);
26
+ const output = execFileSync("git", ["worktree", "list", "--porcelain"], {
27
+ cwd: realRepoRoot,
28
+ encoding: "utf8"
29
+ });
30
+ const worktrees = parseWorktreeList(output, realRepoRoot, realCwd);
31
+ const script = worktrees.map(
32
+ (wt) => `(cd ${JSON.stringify(wt.path)} 2>/dev/null && git log -1 --format='%s' 2>/dev/null) || echo ''`
33
+ ).join('; echo "---SEP---"; ');
34
+ let commits = [];
35
+ try {
36
+ const batchOutput = execFileSync("sh", ["-c", script], {
37
+ encoding: "utf8",
38
+ timeout: 8e3
39
+ });
40
+ commits = batchOutput.split("---SEP---").map((s) => s.trim());
41
+ } catch {
42
+ }
43
+ return worktrees.map((wt, i) => ({
44
+ ...wt,
45
+ lastCommit: commits[i] ?? ""
46
+ }));
47
+ }
48
+ function parseWorktreeList(output, repoRoot, cwd) {
49
+ return output.trim().split("\n\n").map((block) => {
50
+ const lines = block.trim().split("\n");
51
+ const wtPath = lines[0].slice("worktree ".length);
52
+ const branchLine = lines.find((l) => l.startsWith("branch "));
53
+ const branch = branchLine ? branchLine.replace("branch refs/heads/", "") : "(detached)";
54
+ return {
55
+ path: wtPath,
56
+ branch,
57
+ isCurrent: cwd === wtPath || cwd.startsWith(wtPath + path.sep),
58
+ repoRoot
59
+ };
60
+ });
61
+ }
62
+ function addWorktree(repoRoot, worktreePath, branch, baseBranch) {
63
+ if (baseBranch) {
64
+ execFileSync(
65
+ "git",
66
+ ["worktree", "add", "-b", branch, worktreePath, baseBranch],
67
+ {
68
+ cwd: repoRoot
69
+ }
70
+ );
71
+ } else {
72
+ execFileSync("git", ["worktree", "add", worktreePath, branch], {
73
+ cwd: repoRoot
74
+ });
75
+ }
76
+ }
77
+ function removeWorktree(repoRoot, worktreePath, force = false) {
78
+ execFileSync(
79
+ "git",
80
+ ["worktree", "remove", ...force ? ["--force"] : [], worktreePath],
81
+ { cwd: repoRoot, stdio: "pipe" }
82
+ );
83
+ }
84
+ function listWorktreeDirtyFiles(worktreePath) {
85
+ try {
86
+ const out = execFileSync("git", ["status", "--short"], {
87
+ cwd: worktreePath,
88
+ encoding: "utf8"
89
+ });
90
+ return out.split("\n").filter(Boolean);
91
+ } catch {
92
+ return [];
93
+ }
94
+ }
95
+ function branchExists(repoRoot, branch) {
96
+ try {
97
+ const local = execFileSync("git", ["branch", "--list", branch], {
98
+ cwd: repoRoot,
99
+ encoding: "utf8"
100
+ }).trim();
101
+ if (local) return true;
102
+ const remote = execFileSync(
103
+ "git",
104
+ ["ls-remote", "--heads", "origin", branch],
105
+ {
106
+ cwd: repoRoot,
107
+ encoding: "utf8",
108
+ timeout: 8e3
109
+ }
110
+ ).trim();
111
+ return remote.length > 0;
112
+ } catch {
113
+ return false;
114
+ }
115
+ }
116
+ function resolveWorktreePath(repoRoot, worktreePath, branch) {
117
+ const repoName = path.basename(repoRoot);
118
+ const safeBranch = branch.replace(/\//g, "-");
119
+ const resolved = path.resolve(
120
+ repoRoot,
121
+ worktreePath,
122
+ `${repoName}-${safeBranch}`
123
+ );
124
+ const expectedParent = path.resolve(repoRoot, worktreePath);
125
+ if (!resolved.startsWith(expectedParent + path.sep) && resolved !== expectedParent) {
126
+ throw new Error(
127
+ `Branch name "${branch}" would resolve outside the expected worktree directory`
128
+ );
129
+ }
130
+ return resolved;
131
+ }
132
+
133
+ // src/lib/ide.ts
134
+ import { spawn } from "child_process";
135
+ function buildIdeCommand(ide, ideOpenArgs, worktreePath) {
136
+ return { cmd: ide, args: [...ideOpenArgs, worktreePath] };
137
+ }
138
+ function openIde(ide, ideOpenArgs, worktreePath) {
139
+ return new Promise((resolve) => {
140
+ if (!ide) {
141
+ resolve(false);
142
+ return;
143
+ }
144
+ const { cmd, args } = buildIdeCommand(ide, ideOpenArgs, worktreePath);
145
+ const child = spawn(cmd, args, { detached: true, stdio: "ignore" });
146
+ child.on("spawn", () => {
147
+ child.unref();
148
+ resolve(true);
149
+ });
150
+ child.on("error", (err) => {
151
+ process.stderr.write(
152
+ `
153
+ Warning: could not open "${ide}": ${err.message}
154
+ `
155
+ );
156
+ resolve(false);
157
+ });
158
+ });
159
+ }
160
+
161
+ // src/lib/registry.ts
162
+ function registerRepo(repoPath, store = createStore()) {
163
+ const { repos } = getGlobalConfig(store);
164
+ if (!repos.includes(repoPath)) {
165
+ store.set("repos", [...repos, repoPath]);
166
+ }
167
+ }
168
+ function getRegisteredRepos(store = createStore()) {
169
+ return getGlobalConfig(store).repos;
170
+ }
171
+
172
+ // src/lib/tui.ts
173
+ import path2 from "path";
174
+ import Fuse from "fuse.js";
175
+ import pc from "picocolors";
176
+ var cachedFuse = null;
177
+ function filterItems(items, query) {
178
+ if (!query) return items;
179
+ if (!cachedFuse || cachedFuse.items !== items) {
180
+ cachedFuse = {
181
+ items,
182
+ fuse: new Fuse(items, { keys: ["branch"], threshold: 0.4 })
183
+ };
184
+ }
185
+ return cachedFuse.fuse.search(query).map((r) => r.item);
186
+ }
187
+ function groupByRepo(items) {
188
+ const map = /* @__PURE__ */ new Map();
189
+ for (const item of items) {
190
+ const existing = map.get(item.repoRoot) ?? [];
191
+ map.set(item.repoRoot, [...existing, item]);
192
+ }
193
+ return map;
194
+ }
195
+ function shortenPath(p) {
196
+ const home = process.env.HOME ?? "";
197
+ return home && p.startsWith(home) ? `~${p.slice(home.length)}` : p;
198
+ }
199
+ function renderList(items, selectedIndex, query, mode) {
200
+ const lines = [];
201
+ if (mode === "global") {
202
+ lines.push(
203
+ pc.dim("\u2139 Not in a git repository \u2014 showing all registered worktrees")
204
+ );
205
+ lines.push("");
206
+ }
207
+ lines.push(pc.cyan(`> ${query}_`));
208
+ lines.push("");
209
+ const groups = groupByRepo(items);
210
+ let i = 0;
211
+ for (const [repoPath, groupItems] of groups) {
212
+ lines.push(pc.bold(path2.basename(repoPath).toUpperCase()));
213
+ for (const item of groupItems) {
214
+ const cursor = i === selectedIndex ? pc.cyan("\u25B6") : " ";
215
+ const branchLabel = item.isCurrent ? pc.dim(`${item.branch} (current)`) : pc.white(item.branch);
216
+ const pathLabel = pc.dim(shortenPath(item.path));
217
+ lines.push(` ${cursor} ${branchLabel} ${pathLabel}`);
218
+ if (item.lastCommit) {
219
+ lines.push(` ${pc.dim(item.lastCommit)}`);
220
+ }
221
+ i++;
222
+ }
223
+ }
224
+ lines.push("");
225
+ const createHint = mode === "repo" ? " \xB7 C create" : "";
226
+ lines.push(
227
+ pc.dim(`\u2195 navigate \xB7 Enter open \xB7 D delete${createHint} \xB7 Q quit`)
228
+ );
229
+ return lines.join("\n");
230
+ }
231
+ function setupRawMode() {
232
+ process.stdin.setRawMode(true);
233
+ process.stdin.resume();
234
+ process.stdin.setEncoding("utf8");
235
+ }
236
+ function cleanupRawMode() {
237
+ process.stdin.setRawMode(false);
238
+ process.stdin.pause();
239
+ process.stdout.write("\x1B[2J\x1B[H");
240
+ }
241
+ function renderRepoPicker(repos, selectedIndex, query) {
242
+ const lines = [];
243
+ lines.push(pc.dim("\u2139 Not in a git repository \u2014 select a repo to create in"));
244
+ lines.push("");
245
+ lines.push(pc.cyan(`> ${query}_`));
246
+ lines.push("");
247
+ for (let i = 0; i < repos.length; i++) {
248
+ const cursor = i === selectedIndex ? pc.cyan("\u25B6") : " ";
249
+ lines.push(
250
+ ` ${cursor} ${pc.bold(path2.basename(repos[i]).toUpperCase())} ${pc.dim(shortenPath(repos[i]))}`
251
+ );
252
+ }
253
+ lines.push("");
254
+ lines.push(pc.dim("\u2195 navigate \xB7 Enter select \xB7 Q quit"));
255
+ return lines.join("\n");
256
+ }
257
+ async function runRepoPicker(repos) {
258
+ const filterRepos = (all, q) => q ? all.filter(
259
+ (p) => path2.basename(p).toLowerCase().includes(q.toLowerCase())
260
+ ) : all;
261
+ let query = "";
262
+ let selectedIndex = 0;
263
+ let filtered = repos;
264
+ const render = () => {
265
+ process.stdout.write("\x1B[2J\x1B[H");
266
+ process.stdout.write(renderRepoPicker(filtered, selectedIndex, query));
267
+ };
268
+ setupRawMode();
269
+ render();
270
+ return new Promise((resolve, reject) => {
271
+ let listenerActive = false;
272
+ const attachListener = () => {
273
+ if (!listenerActive) {
274
+ process.stdin.on("data", onData);
275
+ listenerActive = true;
276
+ }
277
+ };
278
+ const detachListener = () => {
279
+ process.stdin.removeListener("data", onData);
280
+ listenerActive = false;
281
+ };
282
+ const onData = (key) => {
283
+ try {
284
+ if (key === "" || key === "q" || key === "Q" || key === "\x1B") {
285
+ detachListener();
286
+ cleanupRawMode();
287
+ resolve(null);
288
+ } else if (key === "\x1B[A") {
289
+ selectedIndex = Math.max(0, selectedIndex - 1);
290
+ render();
291
+ } else if (key === "\x1B[B") {
292
+ selectedIndex = Math.min(filtered.length - 1, selectedIndex + 1);
293
+ render();
294
+ } else if (key === "\r") {
295
+ const repo = filtered[selectedIndex];
296
+ if (repo) {
297
+ detachListener();
298
+ cleanupRawMode();
299
+ resolve(repo);
300
+ }
301
+ } else if (key === "\x7F") {
302
+ query = query.slice(0, -1);
303
+ filtered = filterRepos(repos, query);
304
+ selectedIndex = 0;
305
+ render();
306
+ } else if (key.length === 1 && key >= " ") {
307
+ query += key;
308
+ filtered = filterRepos(repos, query);
309
+ selectedIndex = 0;
310
+ render();
311
+ }
312
+ } catch (err) {
313
+ detachListener();
314
+ cleanupRawMode();
315
+ reject(err);
316
+ }
317
+ };
318
+ attachListener();
319
+ });
320
+ }
321
+ function renderBranchInput(repoName, branch, error) {
322
+ const lines = [];
323
+ lines.push(pc.bold(`Repo: ${repoName}`));
324
+ lines.push("");
325
+ lines.push(`Branch: ${pc.cyan(`${branch}_`)}`);
326
+ if (error) {
327
+ lines.push("");
328
+ lines.push(pc.red(error));
329
+ }
330
+ lines.push("");
331
+ lines.push(pc.dim("Enter confirm \xB7 Esc cancel"));
332
+ return lines.join("\n");
333
+ }
334
+ async function runBranchInput(repoRoot) {
335
+ let branch = "";
336
+ let error;
337
+ const repoName = path2.basename(repoRoot);
338
+ const render = () => {
339
+ process.stdout.write("\x1B[2J\x1B[H");
340
+ process.stdout.write(renderBranchInput(repoName, branch, error));
341
+ };
342
+ setupRawMode();
343
+ render();
344
+ return new Promise((resolve, reject) => {
345
+ const onData = (key) => {
346
+ try {
347
+ if (key === "" || key === "\x1B") {
348
+ process.stdin.removeListener("data", onData);
349
+ cleanupRawMode();
350
+ resolve(null);
351
+ } else if (key === "\r") {
352
+ if (!branch) {
353
+ error = "Branch name is required";
354
+ render();
355
+ } else {
356
+ process.stdin.removeListener("data", onData);
357
+ cleanupRawMode();
358
+ resolve(branch);
359
+ }
360
+ } else if (key === "\x7F") {
361
+ branch = branch.slice(0, -1);
362
+ error = void 0;
363
+ render();
364
+ } else if (key.length === 1 && key >= " ") {
365
+ branch += key;
366
+ error = void 0;
367
+ render();
368
+ }
369
+ } catch (err) {
370
+ process.stdin.removeListener("data", onData);
371
+ cleanupRawMode();
372
+ reject(err);
373
+ }
374
+ };
375
+ process.stdin.on("data", onData);
376
+ });
377
+ }
378
+ async function runInteractiveList(allItems, mode, handlers) {
379
+ let query = "";
380
+ let selectedIndex = 0;
381
+ let filtered = allItems;
382
+ const render = () => {
383
+ process.stdout.write("\x1B[2J\x1B[H");
384
+ process.stdout.write(renderList(filtered, selectedIndex, query, mode));
385
+ };
386
+ setupRawMode();
387
+ render();
388
+ return new Promise((resolve, reject) => {
389
+ let listenerActive = false;
390
+ const attachListener = () => {
391
+ if (!listenerActive) {
392
+ process.stdin.on("data", onData);
393
+ listenerActive = true;
394
+ }
395
+ };
396
+ const detachListener = () => {
397
+ process.stdin.removeListener("data", onData);
398
+ listenerActive = false;
399
+ };
400
+ const onData = async (key) => {
401
+ try {
402
+ if (key === "" || key === "q" || key === "Q" || key === "\x1B") {
403
+ detachListener();
404
+ cleanupRawMode();
405
+ resolve();
406
+ } else if (key === "\x1B[A") {
407
+ selectedIndex = Math.max(0, selectedIndex - 1);
408
+ render();
409
+ } else if (key === "\x1B[B") {
410
+ selectedIndex = Math.min(filtered.length - 1, selectedIndex + 1);
411
+ render();
412
+ } else if (key === "\r") {
413
+ const item = filtered[selectedIndex];
414
+ if (item) {
415
+ detachListener();
416
+ cleanupRawMode();
417
+ handlers.onOpen(item);
418
+ resolve();
419
+ }
420
+ } else if (key === "d" || key === "D") {
421
+ const item = filtered[selectedIndex];
422
+ if (item) {
423
+ if (item.isCurrent) {
424
+ process.stdout.write(
425
+ pc.red("\nCannot delete the worktree you are currently in.\n")
426
+ );
427
+ return;
428
+ }
429
+ detachListener();
430
+ cleanupRawMode();
431
+ const confirmed = await handlers.onDelete(item);
432
+ if (confirmed) {
433
+ allItems = allItems.filter((w) => w !== item);
434
+ filtered = filtered.filter((w) => w !== item);
435
+ }
436
+ selectedIndex = Math.min(
437
+ selectedIndex,
438
+ Math.max(0, filtered.length - 1)
439
+ );
440
+ setupRawMode();
441
+ attachListener();
442
+ render();
443
+ }
444
+ } else if (key === "c" || key === "C") {
445
+ if (mode === "repo") {
446
+ detachListener();
447
+ cleanupRawMode();
448
+ await handlers.onCreate();
449
+ resolve();
450
+ } else {
451
+ process.stdout.write(
452
+ pc.dim("\ncd into a repo first to create a worktree.\n")
453
+ );
454
+ }
455
+ } else if (key === "\x7F") {
456
+ query = query.slice(0, -1);
457
+ filtered = filterItems(allItems, query);
458
+ selectedIndex = 0;
459
+ render();
460
+ } else if (key.length === 1 && key >= " ") {
461
+ query += key;
462
+ filtered = filterItems(allItems, query);
463
+ selectedIndex = 0;
464
+ render();
465
+ }
466
+ } catch (err) {
467
+ detachListener();
468
+ cleanupRawMode();
469
+ reject(err);
470
+ }
471
+ };
472
+ attachListener();
473
+ });
474
+ }
475
+
476
+ export {
477
+ getRepoRoot,
478
+ listWorktrees,
479
+ addWorktree,
480
+ removeWorktree,
481
+ listWorktreeDirtyFiles,
482
+ branchExists,
483
+ resolveWorktreePath,
484
+ openIde,
485
+ registerRepo,
486
+ getRegisteredRepos,
487
+ runRepoPicker,
488
+ runBranchInput,
489
+ runInteractiveList
490
+ };
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/lib/config.ts
4
+ import Conf from "conf";
5
+ var DEFAULT_CONFIG = {
6
+ worktree_path: "../",
7
+ base_branch: "origin/main",
8
+ setup_commands: [],
9
+ ide: "zed",
10
+ ide_open_args: ["-n"],
11
+ repos: [],
12
+ repo_overrides: {}
13
+ };
14
+ function createStore(cwd) {
15
+ return new Conf({
16
+ projectName: "wt",
17
+ defaults: DEFAULT_CONFIG,
18
+ ...cwd ? { cwd } : {}
19
+ });
20
+ }
21
+ function getGlobalConfig(store = createStore()) {
22
+ return store.store;
23
+ }
24
+ function getEffectiveConfig(repoPath, store = createStore()) {
25
+ const {
26
+ repos: _repos,
27
+ repo_overrides,
28
+ ...repoFields
29
+ } = getGlobalConfig(store);
30
+ const override = repo_overrides[repoPath] ?? {};
31
+ return { ...repoFields, ...override };
32
+ }
33
+
34
+ export {
35
+ createStore,
36
+ getGlobalConfig,
37
+ getEffectiveConfig
38
+ };
package/dist/cli.js ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+ var program = new Command();
6
+ program.name("wt").description("Git worktree manager").version("0.1.0-pr1.f38ce15").action(async () => {
7
+ const { runList } = await import("./list-Q7CFJ66L.js");
8
+ await runList();
9
+ });
10
+ program.command("create [branch]").description("Create a new worktree").action(async (branch) => {
11
+ const { createWorktree } = await import("./create-42OYRFQF.js");
12
+ await createWorktree(branch);
13
+ });
14
+ program.command("config").description("Open the config file in $EDITOR").action(async () => {
15
+ const { openConfig } = await import("./config-FRLXDMRY.js");
16
+ openConfig();
17
+ });
18
+ await program.parseAsync(process.argv);
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ createStore
4
+ } from "./chunk-HIGP6FSR.js";
5
+
6
+ // src/commands/config.ts
7
+ import { spawn } from "child_process";
8
+ function getConfigPath(store = createStore()) {
9
+ return store.path;
10
+ }
11
+ function openConfig(store = createStore()) {
12
+ const configPath = store.path;
13
+ console.log(`Config: ${configPath}`);
14
+ const editor = process.env.EDITOR ?? "nano";
15
+ const child = spawn(editor, [configPath], { stdio: "inherit" });
16
+ child.on("error", (err) => {
17
+ console.error(`Failed to open editor: ${err.message}`);
18
+ process.exit(1);
19
+ });
20
+ child.on("close", (code) => process.exit(code ?? 0));
21
+ }
22
+ export {
23
+ getConfigPath,
24
+ openConfig
25
+ };
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ addWorktree,
4
+ branchExists,
5
+ getRegisteredRepos,
6
+ getRepoRoot,
7
+ openIde,
8
+ registerRepo,
9
+ resolveWorktreePath,
10
+ runBranchInput,
11
+ runRepoPicker
12
+ } from "./chunk-25JVZOCI.js";
13
+ import {
14
+ createStore,
15
+ getEffectiveConfig
16
+ } from "./chunk-HIGP6FSR.js";
17
+
18
+ // src/commands/create.ts
19
+ import { existsSync } from "fs";
20
+ import * as clack from "@clack/prompts";
21
+ import pc from "picocolors";
22
+
23
+ // src/lib/setup.ts
24
+ import { spawn } from "child_process";
25
+ async function runSetupCommands(commands, cwd) {
26
+ for (const command of commands) {
27
+ const result = await runCommand(command, cwd);
28
+ if (!result.success) {
29
+ return {
30
+ success: false,
31
+ failedCommand: command,
32
+ exitCode: result.exitCode
33
+ };
34
+ }
35
+ }
36
+ return { success: true };
37
+ }
38
+ function runCommand(command, cwd) {
39
+ return new Promise((resolve) => {
40
+ const child = spawn(command, { cwd, stdio: "inherit", shell: true });
41
+ child.on("error", () => {
42
+ resolve({ success: false, exitCode: void 0 });
43
+ });
44
+ child.on("close", (code) => {
45
+ resolve({ success: code === 0, exitCode: code ?? void 0 });
46
+ });
47
+ });
48
+ }
49
+
50
+ // src/commands/create.ts
51
+ async function createWorktree(branch, options = {}) {
52
+ const {
53
+ cwd = process.cwd(),
54
+ store = createStore(),
55
+ repoPicker = runRepoPicker,
56
+ branchInput = runBranchInput
57
+ } = options;
58
+ let repoRoot;
59
+ try {
60
+ repoRoot = getRepoRoot(cwd);
61
+ } catch {
62
+ const repos = getRegisteredRepos(store);
63
+ if (repos.length === 0) {
64
+ console.error(
65
+ pc.red(
66
+ "No repos registered. cd into a repo and run wt create to get started."
67
+ )
68
+ );
69
+ return;
70
+ }
71
+ if (!process.stdin.isTTY) {
72
+ console.error(
73
+ pc.red(
74
+ "Interactive repo picker requires a TTY. Please run this command in an interactive terminal."
75
+ )
76
+ );
77
+ process.exit(1);
78
+ }
79
+ const picked = await repoPicker(repos);
80
+ if (!picked) return;
81
+ repoRoot = picked;
82
+ if (!branch) {
83
+ const entered = await branchInput(repoRoot);
84
+ if (!entered) return;
85
+ branch = entered;
86
+ }
87
+ }
88
+ if (!repoRoot) return;
89
+ if (!branch) {
90
+ const input = await clack.text({
91
+ message: "Branch name:",
92
+ validate: (v) => !v || v.length === 0 ? "Required" : void 0
93
+ });
94
+ if (clack.isCancel(input)) return;
95
+ branch = input;
96
+ }
97
+ registerRepo(repoRoot, store);
98
+ const config = getEffectiveConfig(repoRoot, store);
99
+ const worktreePath = resolveWorktreePath(
100
+ repoRoot,
101
+ config.worktree_path,
102
+ branch
103
+ );
104
+ if (existsSync(worktreePath)) {
105
+ throw new Error(`Worktree path already exists: ${worktreePath}`);
106
+ }
107
+ const exists = branchExists(repoRoot, branch);
108
+ if (exists) {
109
+ addWorktree(repoRoot, worktreePath, branch);
110
+ } else {
111
+ addWorktree(repoRoot, worktreePath, branch, config.base_branch);
112
+ }
113
+ console.log(pc.green(`\u2713 Created worktree at ${worktreePath}`));
114
+ if (config.setup_commands.length > 0) {
115
+ console.log(pc.dim("Running setup commands..."));
116
+ const result = await runSetupCommands(config.setup_commands, worktreePath);
117
+ if (!result.success) {
118
+ console.error(
119
+ pc.red(
120
+ `\u2717 Setup failed: ${result.failedCommand} (exit code ${result.exitCode})`
121
+ )
122
+ );
123
+ console.error(pc.dim(`Worktree left at ${worktreePath} for inspection`));
124
+ process.exit(1);
125
+ }
126
+ }
127
+ if (config.ide) {
128
+ const opened = await openIde(
129
+ config.ide,
130
+ config.ide_open_args,
131
+ worktreePath
132
+ );
133
+ if (opened) {
134
+ console.log(pc.green(`\u2713 Opened ${config.ide}`));
135
+ }
136
+ }
137
+ }
138
+ export {
139
+ createWorktree
140
+ };
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getRegisteredRepos,
4
+ getRepoRoot,
5
+ listWorktreeDirtyFiles,
6
+ listWorktrees,
7
+ openIde,
8
+ registerRepo,
9
+ removeWorktree,
10
+ runInteractiveList
11
+ } from "./chunk-25JVZOCI.js";
12
+ import {
13
+ createStore,
14
+ getEffectiveConfig
15
+ } from "./chunk-HIGP6FSR.js";
16
+
17
+ // src/commands/list.ts
18
+ import * as clack from "@clack/prompts";
19
+ import pc from "picocolors";
20
+ async function prepareListItems(options = {}) {
21
+ const { cwd = process.cwd(), store = createStore() } = options;
22
+ let repoRoot = null;
23
+ try {
24
+ repoRoot = getRepoRoot(cwd);
25
+ } catch {
26
+ }
27
+ if (repoRoot) {
28
+ registerRepo(repoRoot, store);
29
+ const items2 = listWorktrees(repoRoot, cwd);
30
+ return { items: items2, mode: "repo", repoRoot };
31
+ }
32
+ const repos = getRegisteredRepos(store);
33
+ const items = repos.flatMap((repo) => {
34
+ try {
35
+ return listWorktrees(repo, cwd);
36
+ } catch {
37
+ return [];
38
+ }
39
+ });
40
+ return { items, mode: "global", repoRoot: null };
41
+ }
42
+ async function runList(options = {}) {
43
+ const { store = createStore(), cwd = process.cwd() } = options;
44
+ const { items, mode, repoRoot } = await prepareListItems({ cwd, store });
45
+ if (items.length === 0 && mode === "global") {
46
+ console.log(
47
+ pc.dim(
48
+ "No repos registered. Run `wt create` inside a repo to get started."
49
+ )
50
+ );
51
+ return;
52
+ }
53
+ await runInteractiveList(items, mode, {
54
+ onOpen: (item) => {
55
+ const config = getEffectiveConfig(item.repoRoot, store);
56
+ openIde(config.ide, config.ide_open_args, item.path);
57
+ },
58
+ onDelete: async (item) => {
59
+ const confirmed = await clack.confirm({
60
+ message: `Remove worktree ${pc.bold(item.branch)}? This cannot be undone.`
61
+ });
62
+ if (clack.isCancel(confirmed) || !confirmed) return false;
63
+ try {
64
+ removeWorktree(item.repoRoot, item.path);
65
+ console.log(pc.green(`\u2713 Removed ${item.branch}`));
66
+ return true;
67
+ } catch (err) {
68
+ const msg = String(err);
69
+ if (msg.includes("modified or untracked files")) {
70
+ const dirty = listWorktreeDirtyFiles(item.path);
71
+ if (dirty.length > 0) {
72
+ clack.log.warn(
73
+ `Worktree has uncommitted changes:
74
+ ${dirty.map((f) => ` ${f}`).join("\n")}`
75
+ );
76
+ }
77
+ const force = await clack.confirm({
78
+ message: `Force delete ${pc.bold(item.branch)}? All changes will be lost.`
79
+ });
80
+ if (clack.isCancel(force) || !force) return false;
81
+ try {
82
+ removeWorktree(item.repoRoot, item.path, true);
83
+ console.log(pc.green(`\u2713 Force-removed ${item.branch}`));
84
+ return true;
85
+ } catch (err2) {
86
+ console.error(pc.red(`\u2717 Failed to force-remove: ${String(err2)}`));
87
+ return false;
88
+ }
89
+ }
90
+ console.error(pc.red(`\u2717 Failed to remove: ${msg}`));
91
+ return false;
92
+ }
93
+ },
94
+ onCreate: async () => {
95
+ if (repoRoot) {
96
+ const { createWorktree } = await import("./create-42OYRFQF.js");
97
+ await createWorktree(void 0, { cwd: repoRoot, store });
98
+ }
99
+ }
100
+ });
101
+ }
102
+ export {
103
+ prepareListItems,
104
+ runList
105
+ };
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@cestoliv/wt",
3
+ "version": "0.1.0-pr1.f38ce15",
4
+ "description": "Fast, interactive TUI to browse, create, open, and delete git worktrees across repos.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/cestoliv/worktrees.git"
8
+ },
9
+ "engines": {
10
+ "node": ">=20"
11
+ },
12
+ "type": "module",
13
+ "bin": {
14
+ "wt": "./dist/cli.js"
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "scripts": {
23
+ "dev": "tsx src/cli.ts",
24
+ "build": "tsup && chmod +x dist/cli.js",
25
+ "prepublishOnly": "npm run build",
26
+ "test": "vitest run",
27
+ "test:watch": "vitest",
28
+ "typecheck": "tsc --noEmit",
29
+ "lint": "biome check .",
30
+ "format": "biome format --write ."
31
+ },
32
+ "dependencies": {
33
+ "@clack/prompts": "^1.0.1",
34
+ "commander": "^14.0.3",
35
+ "conf": "^15.1.0",
36
+ "fuse.js": "^7.1.0",
37
+ "picocolors": "^1.1.1"
38
+ },
39
+ "devDependencies": {
40
+ "@biomejs/biome": "^2.4.4",
41
+ "@types/node": "^22.0.0",
42
+ "tsup": "^8.0.0",
43
+ "tsx": "^4.19.0",
44
+ "typescript": "^5.0.0",
45
+ "vitest": "^3.0.0"
46
+ }
47
+ }