@jhorst11/wt 2.0.2 → 2.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.
- package/README.md +2 -0
- package/dist/bin/wt.d.ts +3 -0
- package/dist/bin/wt.d.ts.map +1 -0
- package/dist/bin/wt.js +99 -0
- package/dist/bin/wt.js.map +1 -0
- package/dist/src/commands.d.ts +9 -0
- package/dist/src/commands.d.ts.map +1 -0
- package/dist/src/commands.js +1101 -0
- package/dist/src/commands.js.map +1 -0
- package/dist/src/config.d.ts +51 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/config.js +384 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/git.d.ts +55 -0
- package/dist/src/git.d.ts.map +1 -0
- package/dist/src/git.js +387 -0
- package/dist/src/git.js.map +1 -0
- package/dist/src/setup.d.ts +8 -0
- package/dist/src/setup.d.ts.map +1 -0
- package/dist/src/setup.js +245 -0
- package/dist/src/setup.js.map +1 -0
- package/dist/src/types.d.ts +70 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/ui.d.ts +93 -0
- package/dist/src/ui.d.ts.map +1 -0
- package/dist/src/ui.js +273 -0
- package/dist/src/ui.js.map +1 -0
- package/package.json +20 -6
- package/bin/wt.js +0 -88
- package/shell/wt.sh +0 -66
- package/src/commands.js +0 -1019
- package/src/config.js +0 -426
- package/src/git.js +0 -416
- package/src/setup.js +0 -267
- package/src/ui.js +0 -302
package/src/git.js
DELETED
|
@@ -1,416 +0,0 @@
|
|
|
1
|
-
import { simpleGit } from 'simple-git';
|
|
2
|
-
import { join, basename, relative } from 'path';
|
|
3
|
-
import { existsSync, mkdirSync, readdirSync, statSync } from 'fs';
|
|
4
|
-
import { resolveConfig } from './config.js';
|
|
5
|
-
|
|
6
|
-
export async function getGit(cwd = process.cwd()) {
|
|
7
|
-
return simpleGit(cwd);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export async function isGitRepo(cwd = process.cwd()) {
|
|
11
|
-
try {
|
|
12
|
-
const git = await getGit(cwd);
|
|
13
|
-
await git.revparse(['--git-dir']);
|
|
14
|
-
return true;
|
|
15
|
-
} catch {
|
|
16
|
-
return false;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export async function getRepoRoot(cwd = process.cwd()) {
|
|
21
|
-
try {
|
|
22
|
-
const git = await getGit(cwd);
|
|
23
|
-
const root = await git.revparse(['--show-toplevel']);
|
|
24
|
-
return root.trim();
|
|
25
|
-
} catch {
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export async function getCurrentBranch(cwd = process.cwd()) {
|
|
31
|
-
try {
|
|
32
|
-
const git = await getGit(cwd);
|
|
33
|
-
const branch = await git.revparse(['--abbrev-ref', 'HEAD']);
|
|
34
|
-
return branch.trim();
|
|
35
|
-
} catch {
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export async function getLocalBranches(cwd = process.cwd()) {
|
|
41
|
-
try {
|
|
42
|
-
const git = await getGit(cwd);
|
|
43
|
-
const result = await git.branchLocal();
|
|
44
|
-
return result.all.map((name) => ({
|
|
45
|
-
name,
|
|
46
|
-
isCurrent: name === result.current,
|
|
47
|
-
}));
|
|
48
|
-
} catch {
|
|
49
|
-
return [];
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export async function getRemoteBranches(cwd = process.cwd()) {
|
|
54
|
-
try {
|
|
55
|
-
const git = await getGit(cwd);
|
|
56
|
-
// Fetch latest from remote first
|
|
57
|
-
await git.fetch(['--all', '--prune']).catch(() => {});
|
|
58
|
-
const result = await git.branch(['-r']);
|
|
59
|
-
return result.all
|
|
60
|
-
.filter((name) => !name.includes('HEAD'))
|
|
61
|
-
.map((name) => ({
|
|
62
|
-
name: name.replace(/^origin\//, ''),
|
|
63
|
-
fullName: name,
|
|
64
|
-
isRemote: true,
|
|
65
|
-
}));
|
|
66
|
-
} catch {
|
|
67
|
-
return [];
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export async function getAllBranches(cwd = process.cwd()) {
|
|
72
|
-
const [local, remote] = await Promise.all([
|
|
73
|
-
getLocalBranches(cwd),
|
|
74
|
-
getRemoteBranches(cwd),
|
|
75
|
-
]);
|
|
76
|
-
|
|
77
|
-
// Merge and dedupe, preferring local branches
|
|
78
|
-
const localNames = new Set(local.map((b) => b.name));
|
|
79
|
-
const uniqueRemote = remote.filter((b) => !localNames.has(b.name));
|
|
80
|
-
|
|
81
|
-
return {
|
|
82
|
-
local,
|
|
83
|
-
remote: uniqueRemote,
|
|
84
|
-
all: [...local.map((b) => ({ ...b, type: 'local' })), ...uniqueRemote.map((b) => ({ ...b, type: 'remote' }))],
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function getWorktreesBase(repoRoot, config) {
|
|
89
|
-
const projectsDir = config.projectsDir.replace(/\/$/, '');
|
|
90
|
-
let repoRel;
|
|
91
|
-
|
|
92
|
-
if (repoRoot.startsWith(projectsDir + '/')) {
|
|
93
|
-
repoRel = repoRoot.slice(projectsDir.length + 1);
|
|
94
|
-
} else {
|
|
95
|
-
repoRel = basename(repoRoot);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return join(config.worktreesDir, repoRel);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export async function getWorktrees(cwd = process.cwd()) {
|
|
102
|
-
try {
|
|
103
|
-
const git = await getGit(cwd);
|
|
104
|
-
const result = await git.raw(['worktree', 'list', '--porcelain']);
|
|
105
|
-
const worktrees = [];
|
|
106
|
-
let current = {};
|
|
107
|
-
|
|
108
|
-
for (const line of result.split('\n')) {
|
|
109
|
-
if (line.startsWith('worktree ')) {
|
|
110
|
-
if (current.path) worktrees.push(current);
|
|
111
|
-
current = { path: line.slice(9) };
|
|
112
|
-
} else if (line.startsWith('branch ')) {
|
|
113
|
-
current.branch = line.slice(7).replace('refs/heads/', '');
|
|
114
|
-
} else if (line === 'bare') {
|
|
115
|
-
current.bare = true;
|
|
116
|
-
} else if (line === 'detached') {
|
|
117
|
-
current.detached = true;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
if (current.path) worktrees.push(current);
|
|
121
|
-
|
|
122
|
-
// Add name (last part of path) and identify main vs worktrees
|
|
123
|
-
return worktrees.map((wt, index) => ({
|
|
124
|
-
...wt,
|
|
125
|
-
name: basename(wt.path),
|
|
126
|
-
isMain: index === 0,
|
|
127
|
-
}));
|
|
128
|
-
} catch {
|
|
129
|
-
return [];
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export async function getWorktreesInBase(repoRoot, config) {
|
|
134
|
-
const base = getWorktreesBase(repoRoot, config);
|
|
135
|
-
if (!existsSync(base)) return [];
|
|
136
|
-
|
|
137
|
-
try {
|
|
138
|
-
const entries = readdirSync(base);
|
|
139
|
-
const worktrees = [];
|
|
140
|
-
|
|
141
|
-
for (const entry of entries) {
|
|
142
|
-
const entryPath = join(base, entry);
|
|
143
|
-
if (statSync(entryPath).isDirectory()) {
|
|
144
|
-
// Check if it's a valid git worktree
|
|
145
|
-
const gitFile = join(entryPath, '.git');
|
|
146
|
-
if (existsSync(gitFile)) {
|
|
147
|
-
const branch = await getCurrentBranch(entryPath);
|
|
148
|
-
worktrees.push({
|
|
149
|
-
name: entry,
|
|
150
|
-
path: entryPath,
|
|
151
|
-
branch: branch || 'unknown',
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return worktrees;
|
|
158
|
-
} catch {
|
|
159
|
-
return [];
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export async function getMainRepoPath(cwd = process.cwd()) {
|
|
164
|
-
try {
|
|
165
|
-
const git = await getGit(cwd);
|
|
166
|
-
const result = await git.raw(['worktree', 'list', '--porcelain']);
|
|
167
|
-
const firstLine = result.split('\n').find((l) => l.startsWith('worktree '));
|
|
168
|
-
return firstLine ? firstLine.slice(9) : null;
|
|
169
|
-
} catch {
|
|
170
|
-
return null;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export function buildBranchName(leaf, config) {
|
|
175
|
-
const cleanLeaf = leaf.replace(/^\//, '').replace(/ /g, '-');
|
|
176
|
-
const prefix = config.branchPrefix || '';
|
|
177
|
-
if (prefix) {
|
|
178
|
-
return `${prefix.replace(/\/$/, '')}/${cleanLeaf}`;
|
|
179
|
-
}
|
|
180
|
-
return cleanLeaf;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
export async function branchExistsLocal(branchName, cwd = process.cwd()) {
|
|
184
|
-
try {
|
|
185
|
-
const git = await getGit(cwd);
|
|
186
|
-
// Do not use --quiet: simple-git swallows non-zero exit codes when output is
|
|
187
|
-
// empty, so `--quiet` (which suppresses output) causes the try-block to
|
|
188
|
-
// succeed even when the ref does not exist. Without --quiet the command
|
|
189
|
-
// prints the SHA on success (keeping the try path) and writes to stderr on
|
|
190
|
-
// failure (causing simple-git to throw into the catch path).
|
|
191
|
-
const result = await git.raw(['show-ref', '--verify', `refs/heads/${branchName}`]);
|
|
192
|
-
return result.trim().length > 0;
|
|
193
|
-
} catch {
|
|
194
|
-
return false;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export async function branchExistsRemote(branchName, cwd = process.cwd()) {
|
|
199
|
-
try {
|
|
200
|
-
const git = await getGit(cwd);
|
|
201
|
-
const result = await git.raw(['ls-remote', '--exit-code', '--heads', 'origin', branchName]);
|
|
202
|
-
return result.length > 0;
|
|
203
|
-
} catch {
|
|
204
|
-
return false;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
export async function ensureBranch(branchName, baseBranch = null, cwd = process.cwd()) {
|
|
209
|
-
const git = await getGit(cwd);
|
|
210
|
-
|
|
211
|
-
// Resolve detached HEAD to the actual commit SHA so it's usable as a base
|
|
212
|
-
if (baseBranch === 'HEAD') {
|
|
213
|
-
try {
|
|
214
|
-
baseBranch = (await git.revparse(['HEAD'])).trim();
|
|
215
|
-
} catch {
|
|
216
|
-
throw new Error('HEAD does not point to a valid commit. Is this a new repository with no commits?');
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// If baseBranch is a remote ref, fetch it first to ensure it's up to date
|
|
221
|
-
if (baseBranch && baseBranch.startsWith('origin/')) {
|
|
222
|
-
const remoteBranchName = baseBranch.replace('origin/', '');
|
|
223
|
-
try {
|
|
224
|
-
await git.fetch(['origin', `${remoteBranchName}:refs/remotes/origin/${remoteBranchName}`]);
|
|
225
|
-
} catch (fetchErr) {
|
|
226
|
-
// Fetch failed - verify the remote ref still exists locally from a previous fetch
|
|
227
|
-
try {
|
|
228
|
-
await git.revparse(['--verify', baseBranch]);
|
|
229
|
-
} catch {
|
|
230
|
-
throw new Error(`Failed to fetch remote branch '${remoteBranchName}' and no local copy exists. The remote branch may have been deleted.`);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// If baseBranch is specified, verify it resolves to a valid ref
|
|
236
|
-
if (baseBranch) {
|
|
237
|
-
try {
|
|
238
|
-
await git.revparse(['--verify', baseBranch]);
|
|
239
|
-
} catch {
|
|
240
|
-
throw new Error(`Base branch '${baseBranch}' does not exist or is not a valid reference.`);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Check if branch exists locally
|
|
245
|
-
if (await branchExistsLocal(branchName, cwd)) {
|
|
246
|
-
// If we have a remote baseBranch, update local branch to match it
|
|
247
|
-
if (baseBranch && baseBranch.startsWith('origin/')) {
|
|
248
|
-
try {
|
|
249
|
-
const localSha = (await git.revparse([branchName])).trim();
|
|
250
|
-
const remoteSha = (await git.revparse([baseBranch])).trim();
|
|
251
|
-
|
|
252
|
-
if (localSha !== remoteSha) {
|
|
253
|
-
await git.raw(['branch', '-f', branchName, baseBranch]);
|
|
254
|
-
return { created: false, source: 'updated-from-remote' };
|
|
255
|
-
}
|
|
256
|
-
} catch {
|
|
257
|
-
// If we can't compare, fall through to use local as-is
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
return { created: false, source: 'local' };
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Check if branch exists on remote (with same name as branchName)
|
|
264
|
-
if (await branchExistsRemote(branchName, cwd)) {
|
|
265
|
-
await git.fetch(['origin', `${branchName}:${branchName}`]);
|
|
266
|
-
return { created: false, source: 'remote' };
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Create new branch from base
|
|
270
|
-
if (baseBranch) {
|
|
271
|
-
await git.raw(['branch', branchName, baseBranch]);
|
|
272
|
-
} else {
|
|
273
|
-
await git.raw(['branch', branchName]);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
return { created: true, source: 'new' };
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
export async function createWorktree(name, branchName, baseBranch = null, cwd = process.cwd()) {
|
|
280
|
-
const git = await getGit(cwd);
|
|
281
|
-
const repoRoot = await getRepoRoot(cwd);
|
|
282
|
-
const config = resolveConfig(cwd, repoRoot);
|
|
283
|
-
const worktreesBase = getWorktreesBase(repoRoot, config);
|
|
284
|
-
|
|
285
|
-
// Prune stale worktree references before creating a new one
|
|
286
|
-
await pruneWorktrees(cwd);
|
|
287
|
-
|
|
288
|
-
// Ensure worktrees directory exists
|
|
289
|
-
if (!existsSync(worktreesBase)) {
|
|
290
|
-
mkdirSync(worktreesBase, { recursive: true });
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const worktreePath = join(worktreesBase, name);
|
|
294
|
-
|
|
295
|
-
// Check if worktree already exists
|
|
296
|
-
if (existsSync(worktreePath)) {
|
|
297
|
-
return { success: false, error: 'Worktree directory already exists', path: worktreePath };
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Fetch all remotes
|
|
301
|
-
await git.fetch(['--all', '--prune']).catch(() => {});
|
|
302
|
-
|
|
303
|
-
// Ensure branch exists (or determine if we need to create it)
|
|
304
|
-
const branchResult = await ensureBranch(branchName, baseBranch, cwd);
|
|
305
|
-
|
|
306
|
-
// Create worktree — ensureBranch guarantees the branch already exists, so we
|
|
307
|
-
// just attach the worktree to it.
|
|
308
|
-
await git.raw(['worktree', 'add', worktreePath, branchName]);
|
|
309
|
-
|
|
310
|
-
return {
|
|
311
|
-
success: true,
|
|
312
|
-
path: worktreePath,
|
|
313
|
-
branch: branchName,
|
|
314
|
-
branchCreated: branchResult.created,
|
|
315
|
-
branchSource: branchResult.source,
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
export async function removeWorktree(path, force = false, cwd = process.cwd()) {
|
|
320
|
-
const git = await getGit(cwd);
|
|
321
|
-
|
|
322
|
-
// Prune stale worktree references before removing
|
|
323
|
-
await pruneWorktrees(cwd);
|
|
324
|
-
|
|
325
|
-
const args = ['worktree', 'remove'];
|
|
326
|
-
if (force) args.push('--force');
|
|
327
|
-
args.push(path);
|
|
328
|
-
|
|
329
|
-
await git.raw(args);
|
|
330
|
-
return { success: true };
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
export async function pruneWorktrees(cwd = process.cwd()) {
|
|
334
|
-
const git = await getGit(cwd);
|
|
335
|
-
await git.raw(['worktree', 'prune']);
|
|
336
|
-
return { success: true };
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
export function isValidBranchName(name) {
|
|
340
|
-
// Basic validation - git allows most characters but not some special ones
|
|
341
|
-
if (!name || name.length === 0) return false;
|
|
342
|
-
if (name.startsWith('-') || name.startsWith('.')) return false;
|
|
343
|
-
if (name.endsWith('/') || name.endsWith('.')) return false;
|
|
344
|
-
if (name.includes('..') || name.includes('//')) return false;
|
|
345
|
-
if (/[\s~^:?*\[\]\\]/.test(name)) return false;
|
|
346
|
-
return true;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
export async function mergeBranch(sourceBranch, targetBranch = null, cwd = process.cwd()) {
|
|
350
|
-
const git = await getGit(cwd);
|
|
351
|
-
|
|
352
|
-
// If target specified, checkout to it first
|
|
353
|
-
if (targetBranch) {
|
|
354
|
-
await git.checkout(targetBranch);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Merge the source branch
|
|
358
|
-
const result = await git.merge([sourceBranch, '--no-edit']);
|
|
359
|
-
|
|
360
|
-
return {
|
|
361
|
-
success: true,
|
|
362
|
-
merged: sourceBranch,
|
|
363
|
-
into: targetBranch || await getCurrentBranch(cwd),
|
|
364
|
-
result,
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
export async function getMainBranch(cwd = process.cwd()) {
|
|
369
|
-
const git = await getGit(cwd);
|
|
370
|
-
|
|
371
|
-
// Try common main branch names
|
|
372
|
-
const candidates = ['main', 'master', 'develop'];
|
|
373
|
-
const branches = await getLocalBranches(cwd);
|
|
374
|
-
const branchNames = branches.map(b => b.name);
|
|
375
|
-
|
|
376
|
-
for (const candidate of candidates) {
|
|
377
|
-
if (branchNames.includes(candidate)) {
|
|
378
|
-
return candidate;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// Fall back to first branch
|
|
383
|
-
return branchNames[0] || 'main';
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
export async function hasUncommittedChanges(cwd = process.cwd()) {
|
|
387
|
-
const git = await getGit(cwd);
|
|
388
|
-
const status = await git.status();
|
|
389
|
-
return !status.isClean();
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
export async function deleteBranch(branchName, force = false, cwd = process.cwd()) {
|
|
393
|
-
const git = await getGit(cwd);
|
|
394
|
-
const flag = force ? '-D' : '-d';
|
|
395
|
-
await git.branch([flag, branchName]);
|
|
396
|
-
return { success: true };
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
/**
|
|
400
|
-
* Get information about the current worktree if user is inside one.
|
|
401
|
-
* Returns the worktree object with name, path, and branch if inside a worktree.
|
|
402
|
-
* Returns null if in main repository or error occurs.
|
|
403
|
-
* @param {string} repoRoot - Git repository root
|
|
404
|
-
* @param {object} config - Configuration object from resolveConfig()
|
|
405
|
-
* @returns {Promise<object|null>} Worktree info object or null
|
|
406
|
-
*/
|
|
407
|
-
export async function getCurrentWorktreeInfo(repoRoot, config) {
|
|
408
|
-
const currentPath = process.cwd();
|
|
409
|
-
const worktrees = await getWorktreesInBase(repoRoot, config);
|
|
410
|
-
|
|
411
|
-
const currentWt = worktrees.find(wt =>
|
|
412
|
-
currentPath === wt.path || currentPath.startsWith(wt.path + '/')
|
|
413
|
-
);
|
|
414
|
-
|
|
415
|
-
return currentWt || null;
|
|
416
|
-
}
|
package/src/setup.js
DELETED
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
import { select, confirm } from '@inquirer/prompts';
|
|
2
|
-
import { ExitPromptError } from '@inquirer/core';
|
|
3
|
-
import { homedir } from 'os';
|
|
4
|
-
import { existsSync, readFileSync, appendFileSync, writeFileSync } from 'fs';
|
|
5
|
-
import { join } from 'path';
|
|
6
|
-
import {
|
|
7
|
-
showMiniLogo,
|
|
8
|
-
success,
|
|
9
|
-
error,
|
|
10
|
-
warning,
|
|
11
|
-
info,
|
|
12
|
-
heading,
|
|
13
|
-
spacer,
|
|
14
|
-
colors,
|
|
15
|
-
icons,
|
|
16
|
-
divider,
|
|
17
|
-
} from './ui.js';
|
|
18
|
-
|
|
19
|
-
// Shell detection
|
|
20
|
-
export function detectShell() {
|
|
21
|
-
const shell = process.env.SHELL || '';
|
|
22
|
-
|
|
23
|
-
if (shell.includes('zsh')) return 'zsh';
|
|
24
|
-
if (shell.includes('bash')) return 'bash';
|
|
25
|
-
if (shell.includes('fish')) return 'fish';
|
|
26
|
-
if (process.env.FISH_VERSION) return 'fish';
|
|
27
|
-
if (process.env.ZSH_VERSION) return 'zsh';
|
|
28
|
-
if (process.env.BASH_VERSION) return 'bash';
|
|
29
|
-
|
|
30
|
-
return 'unknown';
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function getShellConfig() {
|
|
34
|
-
const shell = detectShell();
|
|
35
|
-
const home = homedir();
|
|
36
|
-
|
|
37
|
-
const configs = {
|
|
38
|
-
zsh: {
|
|
39
|
-
name: 'Zsh',
|
|
40
|
-
rcFile: join(home, '.zshrc'),
|
|
41
|
-
wrapper: `
|
|
42
|
-
# wt-cli: Git worktree manager shell integration
|
|
43
|
-
wt() {
|
|
44
|
-
local wt_cd_file="/tmp/wt_cd_$$"
|
|
45
|
-
rm -f "$wt_cd_file"
|
|
46
|
-
WT_WRAPPER=1 WT_CD_FILE="$wt_cd_file" command wt "$@"
|
|
47
|
-
local exit_code=$?
|
|
48
|
-
if [[ -f "$wt_cd_file" ]]; then
|
|
49
|
-
local dir=$(cat "$wt_cd_file")
|
|
50
|
-
rm -f "$wt_cd_file"
|
|
51
|
-
[[ -d "$dir" ]] && cd "$dir"
|
|
52
|
-
fi
|
|
53
|
-
return $exit_code
|
|
54
|
-
}`,
|
|
55
|
-
},
|
|
56
|
-
bash: {
|
|
57
|
-
name: 'Bash',
|
|
58
|
-
rcFile: join(home, '.bashrc'),
|
|
59
|
-
wrapper: `
|
|
60
|
-
# wt-cli: Git worktree manager shell integration
|
|
61
|
-
wt() {
|
|
62
|
-
local wt_cd_file="/tmp/wt_cd_$$"
|
|
63
|
-
rm -f "$wt_cd_file"
|
|
64
|
-
WT_WRAPPER=1 WT_CD_FILE="$wt_cd_file" command wt "$@"
|
|
65
|
-
local exit_code=$?
|
|
66
|
-
if [[ -f "$wt_cd_file" ]]; then
|
|
67
|
-
local dir=$(cat "$wt_cd_file")
|
|
68
|
-
rm -f "$wt_cd_file"
|
|
69
|
-
[[ -d "$dir" ]] && cd "$dir"
|
|
70
|
-
fi
|
|
71
|
-
return $exit_code
|
|
72
|
-
}`,
|
|
73
|
-
},
|
|
74
|
-
fish: {
|
|
75
|
-
name: 'Fish',
|
|
76
|
-
rcFile: join(home, '.config/fish/config.fish'),
|
|
77
|
-
wrapper: `
|
|
78
|
-
# wt-cli: Git worktree manager shell integration
|
|
79
|
-
function wt
|
|
80
|
-
set -l wt_cd_file "/tmp/wt_cd_fish_$fish_pid"
|
|
81
|
-
rm -f "$wt_cd_file"
|
|
82
|
-
env WT_WRAPPER=1 WT_CD_FILE="$wt_cd_file" command wt $argv
|
|
83
|
-
set -l exit_code $status
|
|
84
|
-
if test -f "$wt_cd_file"
|
|
85
|
-
set -l dir (cat "$wt_cd_file")
|
|
86
|
-
rm -f "$wt_cd_file"
|
|
87
|
-
test -d "$dir"; and cd "$dir"
|
|
88
|
-
end
|
|
89
|
-
return $exit_code
|
|
90
|
-
end`,
|
|
91
|
-
},
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
return configs[shell] || null;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export function isWrapperInstalled() {
|
|
98
|
-
// Check if we're running through the wrapper
|
|
99
|
-
// The wrapper would need to set this env var
|
|
100
|
-
return process.env.WT_WRAPPER === '1';
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function checkWrapperInRcFile() {
|
|
104
|
-
const config = getShellConfig();
|
|
105
|
-
if (!config) return { installed: false, reason: 'unknown-shell' };
|
|
106
|
-
|
|
107
|
-
if (!existsSync(config.rcFile)) {
|
|
108
|
-
return { installed: false, reason: 'no-rc-file', rcFile: config.rcFile };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
const content = readFileSync(config.rcFile, 'utf-8');
|
|
113
|
-
if (content.includes('wt-cli') || content.includes('__WT_CD__')) {
|
|
114
|
-
return { installed: true, rcFile: config.rcFile };
|
|
115
|
-
}
|
|
116
|
-
return { installed: false, reason: 'not-configured', rcFile: config.rcFile };
|
|
117
|
-
} catch {
|
|
118
|
-
return { installed: false, reason: 'read-error', rcFile: config.rcFile };
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export async function setupCommand() {
|
|
123
|
-
showMiniLogo();
|
|
124
|
-
heading(`${icons.sparkles} Shell Setup`);
|
|
125
|
-
|
|
126
|
-
const shell = detectShell();
|
|
127
|
-
const config = getShellConfig();
|
|
128
|
-
|
|
129
|
-
if (shell === 'unknown' || !config) {
|
|
130
|
-
warning(`Could not detect your shell type`);
|
|
131
|
-
info(`SHELL environment variable: ${process.env.SHELL || 'not set'}`);
|
|
132
|
-
spacer();
|
|
133
|
-
console.log(` ${colors.muted('Please manually add the shell wrapper to your shell config.')}`);
|
|
134
|
-
console.log(` ${colors.muted('See:')} ${colors.path('https://github.com/jhorst11/wt#shell-integration')}`);
|
|
135
|
-
spacer();
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
success(`Detected shell: ${colors.primary(config.name)}`);
|
|
140
|
-
info(`Config file: ${colors.path(config.rcFile)}`);
|
|
141
|
-
spacer();
|
|
142
|
-
|
|
143
|
-
const status = checkWrapperInRcFile();
|
|
144
|
-
|
|
145
|
-
if (status.installed) {
|
|
146
|
-
success(`Shell integration is already installed! ${icons.check}`);
|
|
147
|
-
spacer();
|
|
148
|
-
info(`If directory jumping isn't working, try restarting your terminal`);
|
|
149
|
-
info(`or run: ${colors.primary(`source ${config.rcFile}`)}`);
|
|
150
|
-
spacer();
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Not installed - offer to install
|
|
155
|
-
divider();
|
|
156
|
-
spacer();
|
|
157
|
-
|
|
158
|
-
console.log(` ${colors.muted('To enable directory jumping (wt go, wt home), we need to')}`);
|
|
159
|
-
console.log(` ${colors.muted('add a small shell function to your')} ${colors.path(config.rcFile)}`);
|
|
160
|
-
spacer();
|
|
161
|
-
|
|
162
|
-
try {
|
|
163
|
-
const action = await select({
|
|
164
|
-
message: 'How would you like to proceed?',
|
|
165
|
-
choices: [
|
|
166
|
-
{
|
|
167
|
-
name: `${icons.sparkles} Auto-install (append to ${config.rcFile})`,
|
|
168
|
-
value: 'auto',
|
|
169
|
-
description: 'Recommended - automatically adds the integration',
|
|
170
|
-
},
|
|
171
|
-
{
|
|
172
|
-
name: `${icons.info} Show me the code to copy`,
|
|
173
|
-
value: 'show',
|
|
174
|
-
description: 'Display the code so you can add it manually',
|
|
175
|
-
},
|
|
176
|
-
{
|
|
177
|
-
name: `${colors.muted(icons.cross + ' Skip for now')}`,
|
|
178
|
-
value: 'skip',
|
|
179
|
-
},
|
|
180
|
-
],
|
|
181
|
-
theme: { prefix: icons.tree },
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
if (action === 'auto') {
|
|
185
|
-
await autoInstall(config);
|
|
186
|
-
} else if (action === 'show') {
|
|
187
|
-
showManualInstructions(config);
|
|
188
|
-
} else {
|
|
189
|
-
info('Skipped. You can run `wt setup` anytime to configure shell integration.');
|
|
190
|
-
spacer();
|
|
191
|
-
}
|
|
192
|
-
} catch (err) {
|
|
193
|
-
if (err instanceof ExitPromptError) {
|
|
194
|
-
spacer();
|
|
195
|
-
info('Cancelled');
|
|
196
|
-
spacer();
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
throw err;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
async function autoInstall(config) {
|
|
204
|
-
spacer();
|
|
205
|
-
|
|
206
|
-
try {
|
|
207
|
-
appendFileSync(config.rcFile, '\n' + config.wrapper + '\n');
|
|
208
|
-
success(`Added shell integration to ${colors.path(config.rcFile)}`);
|
|
209
|
-
spacer();
|
|
210
|
-
|
|
211
|
-
console.log(` ${icons.rocket} ${colors.primary('Almost done!')} Run this to activate:`);
|
|
212
|
-
spacer();
|
|
213
|
-
console.log(` ${colors.secondary(`source ${config.rcFile}`)}`);
|
|
214
|
-
spacer();
|
|
215
|
-
console.log(` ${colors.muted('Or just restart your terminal.')}`);
|
|
216
|
-
spacer();
|
|
217
|
-
} catch (err) {
|
|
218
|
-
error(`Failed to write to ${config.rcFile}`);
|
|
219
|
-
error(err.message);
|
|
220
|
-
spacer();
|
|
221
|
-
showManualInstructions(config);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function showManualInstructions(config) {
|
|
226
|
-
spacer();
|
|
227
|
-
console.log(` ${colors.muted('Add this to')} ${colors.path(config.rcFile)}${colors.muted(':')}`);
|
|
228
|
-
spacer();
|
|
229
|
-
divider();
|
|
230
|
-
console.log(colors.secondary(config.wrapper));
|
|
231
|
-
divider();
|
|
232
|
-
spacer();
|
|
233
|
-
console.log(` ${colors.muted('Then run:')} ${colors.primary(`source ${config.rcFile}`)}`);
|
|
234
|
-
spacer();
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// Helper to show a gentle nudge if wrapper isn't set up
|
|
238
|
-
export function showCdHint(path) {
|
|
239
|
-
// Check if we're running through the shell wrapper with a cd file
|
|
240
|
-
const cdFile = process.env.WT_CD_FILE;
|
|
241
|
-
if (cdFile && isWrapperInstalled()) {
|
|
242
|
-
// Write path to temp file for shell wrapper to read
|
|
243
|
-
try {
|
|
244
|
-
writeFileSync(cdFile, path);
|
|
245
|
-
} catch {
|
|
246
|
-
// Fall through to show manual instructions
|
|
247
|
-
}
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Fall back to checking rc file
|
|
252
|
-
const status = checkWrapperInRcFile();
|
|
253
|
-
|
|
254
|
-
if (status.installed) {
|
|
255
|
-
// Wrapper is in rc file but not active - show path
|
|
256
|
-
spacer();
|
|
257
|
-
console.log(` ${icons.rocket} ${colors.muted('Switching to:')} ${colors.path(path)}`);
|
|
258
|
-
spacer();
|
|
259
|
-
} else {
|
|
260
|
-
// No wrapper - show a friendly message instead
|
|
261
|
-
spacer();
|
|
262
|
-
console.log(` ${icons.rocket} ${colors.muted('Run:')} ${colors.primary(`cd "${path}"`)}`);
|
|
263
|
-
spacer();
|
|
264
|
-
console.log(` ${colors.muted(`Tip: Run`)} ${colors.secondary('wt setup')} ${colors.muted('to enable auto-navigation')}`);
|
|
265
|
-
spacer();
|
|
266
|
-
}
|
|
267
|
-
}
|