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