@jhorst11/wt 1.0.1 → 2.0.1
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 +54 -8
- package/bin/wt.js +6 -5
- package/package.json +3 -2
- package/src/commands.js +534 -389
- package/src/config.js +257 -0
- package/src/git.js +56 -27
- package/src/setup.js +39 -28
- package/src/ui.js +10 -4
package/src/config.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
|
|
6
|
+
const CONFIG_DIR = '.wt';
|
|
7
|
+
const CONFIG_FILE = 'config.json';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Load configuration from .wt/config.json or config.json at the given directory.
|
|
11
|
+
* Tries .wt/config.json first (for repo/directory configs), then config.json (for global configs).
|
|
12
|
+
* Returns an object with defaults for any missing fields.
|
|
13
|
+
*/
|
|
14
|
+
export function loadConfig(dirPath) {
|
|
15
|
+
const configPaths = [
|
|
16
|
+
join(dirPath, CONFIG_DIR, CONFIG_FILE), // .wt/config.json (for repo/directory)
|
|
17
|
+
join(dirPath, CONFIG_FILE), // config.json (for global config dir)
|
|
18
|
+
];
|
|
19
|
+
const defaults = {
|
|
20
|
+
projectsDir: undefined,
|
|
21
|
+
worktreesDir: undefined,
|
|
22
|
+
branchPrefix: undefined,
|
|
23
|
+
hooks: {},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
let raw;
|
|
27
|
+
for (const configPath of configPaths) {
|
|
28
|
+
try {
|
|
29
|
+
raw = readFileSync(configPath, 'utf8');
|
|
30
|
+
break;
|
|
31
|
+
} catch {
|
|
32
|
+
// Try next path
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!raw) {
|
|
37
|
+
return defaults;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let parsed;
|
|
41
|
+
try {
|
|
42
|
+
parsed = JSON.parse(raw);
|
|
43
|
+
} catch {
|
|
44
|
+
return defaults;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
48
|
+
return defaults;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const result = { ...defaults };
|
|
52
|
+
|
|
53
|
+
if (typeof parsed.projectsDir === 'string') {
|
|
54
|
+
result.projectsDir = parsed.projectsDir;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (typeof parsed.worktreesDir === 'string') {
|
|
58
|
+
result.worktreesDir = parsed.worktreesDir;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (typeof parsed.branchPrefix === 'string') {
|
|
62
|
+
result.branchPrefix = parsed.branchPrefix;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (typeof parsed.hooks === 'object' && parsed.hooks !== null && !Array.isArray(parsed.hooks)) {
|
|
66
|
+
for (const [hookName, commands] of Object.entries(parsed.hooks)) {
|
|
67
|
+
if (Array.isArray(commands) && commands.every((c) => typeof c === 'string')) {
|
|
68
|
+
result.hooks[hookName] = commands;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Find all config files from global to cwd within repo boundaries.
|
|
78
|
+
* Walks up from cwd to repoRoot, collecting all .wt/config.json paths.
|
|
79
|
+
*
|
|
80
|
+
* @param {string} cwd - Current working directory
|
|
81
|
+
* @param {string} repoRoot - Git repository root
|
|
82
|
+
* @param {string} [globalConfigPath] - Path to global config (default: ~/.wt/config.json)
|
|
83
|
+
* @returns {string[]} Array of config file paths (global first)
|
|
84
|
+
*/
|
|
85
|
+
function findConfigFiles(cwd, repoRoot, globalConfigPath = join(homedir(), '.wt', CONFIG_FILE)) {
|
|
86
|
+
const paths = [];
|
|
87
|
+
|
|
88
|
+
// Add global config if it exists
|
|
89
|
+
try {
|
|
90
|
+
readFileSync(globalConfigPath, 'utf8');
|
|
91
|
+
paths.push(globalConfigPath);
|
|
92
|
+
} catch {
|
|
93
|
+
// Global config doesn't exist, that's fine
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Build list of directories from repoRoot down to cwd
|
|
97
|
+
// Normalize paths for comparison
|
|
98
|
+
const normalizedCwd = cwd.replace(/\/$/, '');
|
|
99
|
+
const normalizedRepoRoot = repoRoot.replace(/\/$/, '');
|
|
100
|
+
|
|
101
|
+
if (!normalizedCwd.startsWith(normalizedRepoRoot)) {
|
|
102
|
+
// cwd is outside repo root, skip walking
|
|
103
|
+
return paths;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Collect config paths from repoRoot up to cwd
|
|
107
|
+
const configPaths = [];
|
|
108
|
+
|
|
109
|
+
// Start at repoRoot
|
|
110
|
+
let current = normalizedRepoRoot;
|
|
111
|
+
const parts = normalizedCwd.slice(normalizedRepoRoot.length).split('/').filter(Boolean);
|
|
112
|
+
|
|
113
|
+
// Add repo root config
|
|
114
|
+
const repoConfigPath = join(normalizedRepoRoot, CONFIG_DIR, CONFIG_FILE);
|
|
115
|
+
try {
|
|
116
|
+
readFileSync(repoConfigPath, 'utf8');
|
|
117
|
+
configPaths.push(repoConfigPath);
|
|
118
|
+
} catch {
|
|
119
|
+
// No config at repo root
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Add configs for each directory down to cwd
|
|
123
|
+
for (const part of parts) {
|
|
124
|
+
current = join(current, part);
|
|
125
|
+
const configPath = join(current, CONFIG_DIR, CONFIG_FILE);
|
|
126
|
+
try {
|
|
127
|
+
readFileSync(configPath, 'utf8');
|
|
128
|
+
configPaths.push(configPath);
|
|
129
|
+
} catch {
|
|
130
|
+
// No config at this directory
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return paths.concat(configPaths);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Merge multiple config objects with last-wins strategy.
|
|
139
|
+
* For scalar fields, last defined value wins.
|
|
140
|
+
* For hooks object, merge all hook definitions.
|
|
141
|
+
*
|
|
142
|
+
* @param {Object[]} configs - Array of config objects (least specific first)
|
|
143
|
+
* @returns {Object} Merged config
|
|
144
|
+
*/
|
|
145
|
+
function mergeConfigs(configs) {
|
|
146
|
+
const result = {
|
|
147
|
+
projectsDir: undefined,
|
|
148
|
+
worktreesDir: undefined,
|
|
149
|
+
branchPrefix: undefined,
|
|
150
|
+
hooks: {},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
for (const config of configs) {
|
|
154
|
+
if (config.projectsDir !== undefined) {
|
|
155
|
+
result.projectsDir = config.projectsDir;
|
|
156
|
+
}
|
|
157
|
+
if (config.worktreesDir !== undefined) {
|
|
158
|
+
result.worktreesDir = config.worktreesDir;
|
|
159
|
+
}
|
|
160
|
+
if (config.branchPrefix !== undefined) {
|
|
161
|
+
result.branchPrefix = config.branchPrefix;
|
|
162
|
+
}
|
|
163
|
+
if (config.hooks && typeof config.hooks === 'object') {
|
|
164
|
+
for (const [hookName, commands] of Object.entries(config.hooks)) {
|
|
165
|
+
result.hooks[hookName] = commands;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Resolve hierarchical config by walking up from cwd to repoRoot.
|
|
175
|
+
* Returns merged config with defaults for any missing fields.
|
|
176
|
+
*
|
|
177
|
+
* @param {string} [cwd] - Current working directory (default: process.cwd())
|
|
178
|
+
* @param {string} repoRoot - Git repository root
|
|
179
|
+
* @param {string} [globalConfigPath] - Override global config path (for testing)
|
|
180
|
+
* @returns {Object} Resolved config with all fields
|
|
181
|
+
*/
|
|
182
|
+
export function resolveConfig(cwd = process.cwd(), repoRoot, globalConfigPath) {
|
|
183
|
+
const defaults = {
|
|
184
|
+
projectsDir: join(homedir(), 'projects'),
|
|
185
|
+
worktreesDir: join(homedir(), 'projects', 'worktrees'),
|
|
186
|
+
branchPrefix: '',
|
|
187
|
+
hooks: {},
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// Determine the effective global config path
|
|
191
|
+
const effectiveGlobalConfigPath = globalConfigPath || join(homedir(), '.wt', CONFIG_FILE);
|
|
192
|
+
|
|
193
|
+
// Find all config files from global to cwd
|
|
194
|
+
const configPaths = findConfigFiles(cwd, repoRoot, effectiveGlobalConfigPath);
|
|
195
|
+
|
|
196
|
+
// Load each config file by extracting the directory path
|
|
197
|
+
const configs = configPaths.map((path) => {
|
|
198
|
+
// For global config, extract the directory containing it
|
|
199
|
+
if (path === effectiveGlobalConfigPath) {
|
|
200
|
+
const globalConfigDir = path.endsWith(CONFIG_FILE)
|
|
201
|
+
? path.slice(0, path.lastIndexOf('/'))
|
|
202
|
+
: path;
|
|
203
|
+
return loadConfig(globalConfigDir);
|
|
204
|
+
}
|
|
205
|
+
// For other configs at /path/.wt/config.json, directory is /path
|
|
206
|
+
const dirPath = path.slice(0, path.lastIndexOf('/.wt'));
|
|
207
|
+
return loadConfig(dirPath);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Merge configs
|
|
211
|
+
const merged = mergeConfigs(configs);
|
|
212
|
+
|
|
213
|
+
// Apply defaults for missing fields
|
|
214
|
+
return {
|
|
215
|
+
projectsDir: merged.projectsDir ?? defaults.projectsDir,
|
|
216
|
+
worktreesDir: merged.worktreesDir ?? defaults.worktreesDir,
|
|
217
|
+
branchPrefix: merged.branchPrefix ?? defaults.branchPrefix,
|
|
218
|
+
hooks: merged.hooks,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Run hook commands sequentially. Each command runs with cwd set to `wtPath`
|
|
224
|
+
* and receives WT_SOURCE, WT_BRANCH, and WT_PATH as environment variables.
|
|
225
|
+
*
|
|
226
|
+
* Returns an array of { command, success, error? } results.
|
|
227
|
+
* Hook failures are non-fatal — they produce warnings but don't throw.
|
|
228
|
+
*/
|
|
229
|
+
export function runHooks(hookName, config, { source, path: wtPath, branch }) {
|
|
230
|
+
const commands = config.hooks?.[hookName];
|
|
231
|
+
if (!commands || commands.length === 0) return [];
|
|
232
|
+
|
|
233
|
+
const env = {
|
|
234
|
+
...process.env,
|
|
235
|
+
WT_SOURCE: source,
|
|
236
|
+
WT_BRANCH: branch,
|
|
237
|
+
WT_PATH: wtPath,
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const results = [];
|
|
241
|
+
|
|
242
|
+
for (const cmd of commands) {
|
|
243
|
+
try {
|
|
244
|
+
execSync(cmd, {
|
|
245
|
+
cwd: wtPath,
|
|
246
|
+
env,
|
|
247
|
+
stdio: 'pipe',
|
|
248
|
+
timeout: 300_000, // 5 minute timeout per command
|
|
249
|
+
});
|
|
250
|
+
results.push({ command: cmd, success: true });
|
|
251
|
+
} catch (err) {
|
|
252
|
+
results.push({ command: cmd, success: false, error: err.message });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return results;
|
|
257
|
+
}
|
package/src/git.js
CHANGED
|
@@ -1,18 +1,7 @@
|
|
|
1
1
|
import { simpleGit } from 'simple-git';
|
|
2
|
-
import { homedir } from 'os';
|
|
3
2
|
import { join, basename, relative } from 'path';
|
|
4
3
|
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
|
-
}
|
|
4
|
+
import { resolveConfig } from './config.js';
|
|
16
5
|
|
|
17
6
|
export async function getGit(cwd = process.cwd()) {
|
|
18
7
|
return simpleGit(cwd);
|
|
@@ -96,7 +85,7 @@ export async function getAllBranches(cwd = process.cwd()) {
|
|
|
96
85
|
};
|
|
97
86
|
}
|
|
98
87
|
|
|
99
|
-
export function getWorktreesBase(repoRoot) {
|
|
88
|
+
export function getWorktreesBase(repoRoot, config) {
|
|
100
89
|
const projectsDir = config.projectsDir.replace(/\/$/, '');
|
|
101
90
|
let repoRel;
|
|
102
91
|
|
|
@@ -141,8 +130,8 @@ export async function getWorktrees(cwd = process.cwd()) {
|
|
|
141
130
|
}
|
|
142
131
|
}
|
|
143
132
|
|
|
144
|
-
export async function getWorktreesInBase(repoRoot) {
|
|
145
|
-
const base = getWorktreesBase(repoRoot);
|
|
133
|
+
export async function getWorktreesInBase(repoRoot, config) {
|
|
134
|
+
const base = getWorktreesBase(repoRoot, config);
|
|
146
135
|
if (!existsSync(base)) return [];
|
|
147
136
|
|
|
148
137
|
try {
|
|
@@ -182,8 +171,9 @@ export async function getMainRepoPath(cwd = process.cwd()) {
|
|
|
182
171
|
}
|
|
183
172
|
}
|
|
184
173
|
|
|
185
|
-
export function buildBranchName(leaf,
|
|
174
|
+
export function buildBranchName(leaf, config) {
|
|
186
175
|
const cleanLeaf = leaf.replace(/^\//, '').replace(/ /g, '-');
|
|
176
|
+
const prefix = config.branchPrefix || '';
|
|
187
177
|
if (prefix) {
|
|
188
178
|
return `${prefix.replace(/\/$/, '')}/${cleanLeaf}`;
|
|
189
179
|
}
|
|
@@ -193,8 +183,13 @@ export function buildBranchName(leaf, prefix = config.branchPrefix) {
|
|
|
193
183
|
export async function branchExistsLocal(branchName, cwd = process.cwd()) {
|
|
194
184
|
try {
|
|
195
185
|
const git = await getGit(cwd);
|
|
196
|
-
|
|
197
|
-
|
|
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;
|
|
198
193
|
} catch {
|
|
199
194
|
return false;
|
|
200
195
|
}
|
|
@@ -213,10 +208,37 @@ export async function branchExistsRemote(branchName, cwd = process.cwd()) {
|
|
|
213
208
|
export async function ensureBranch(branchName, baseBranch = null, cwd = process.cwd()) {
|
|
214
209
|
const git = await getGit(cwd);
|
|
215
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
|
+
|
|
216
220
|
// If baseBranch is a remote ref, fetch it first to ensure it's up to date
|
|
217
221
|
if (baseBranch && baseBranch.startsWith('origin/')) {
|
|
218
222
|
const remoteBranchName = baseBranch.replace('origin/', '');
|
|
219
|
-
|
|
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
|
+
}
|
|
220
242
|
}
|
|
221
243
|
|
|
222
244
|
// Check if branch exists locally
|
|
@@ -228,9 +250,7 @@ export async function ensureBranch(branchName, baseBranch = null, cwd = process.
|
|
|
228
250
|
const remoteSha = (await git.revparse([baseBranch])).trim();
|
|
229
251
|
|
|
230
252
|
if (localSha !== remoteSha) {
|
|
231
|
-
|
|
232
|
-
// Reset the local branch to match the remote
|
|
233
|
-
await git.branch(['-f', branchName, baseBranch]);
|
|
253
|
+
await git.raw(['branch', '-f', branchName, baseBranch]);
|
|
234
254
|
return { created: false, source: 'updated-from-remote' };
|
|
235
255
|
}
|
|
236
256
|
} catch {
|
|
@@ -248,9 +268,9 @@ export async function ensureBranch(branchName, baseBranch = null, cwd = process.
|
|
|
248
268
|
|
|
249
269
|
// Create new branch from base
|
|
250
270
|
if (baseBranch) {
|
|
251
|
-
await git.
|
|
271
|
+
await git.raw(['branch', branchName, baseBranch]);
|
|
252
272
|
} else {
|
|
253
|
-
await git.
|
|
273
|
+
await git.raw(['branch', branchName]);
|
|
254
274
|
}
|
|
255
275
|
|
|
256
276
|
return { created: true, source: 'new' };
|
|
@@ -259,7 +279,11 @@ export async function ensureBranch(branchName, baseBranch = null, cwd = process.
|
|
|
259
279
|
export async function createWorktree(name, branchName, baseBranch = null, cwd = process.cwd()) {
|
|
260
280
|
const git = await getGit(cwd);
|
|
261
281
|
const repoRoot = await getRepoRoot(cwd);
|
|
262
|
-
const
|
|
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);
|
|
263
287
|
|
|
264
288
|
// Ensure worktrees directory exists
|
|
265
289
|
if (!existsSync(worktreesBase)) {
|
|
@@ -276,10 +300,11 @@ export async function createWorktree(name, branchName, baseBranch = null, cwd =
|
|
|
276
300
|
// Fetch all remotes
|
|
277
301
|
await git.fetch(['--all', '--prune']).catch(() => {});
|
|
278
302
|
|
|
279
|
-
// Ensure branch exists
|
|
303
|
+
// Ensure branch exists (or determine if we need to create it)
|
|
280
304
|
const branchResult = await ensureBranch(branchName, baseBranch, cwd);
|
|
281
305
|
|
|
282
|
-
// Create worktree
|
|
306
|
+
// Create worktree — ensureBranch guarantees the branch already exists, so we
|
|
307
|
+
// just attach the worktree to it.
|
|
283
308
|
await git.raw(['worktree', 'add', worktreePath, branchName]);
|
|
284
309
|
|
|
285
310
|
return {
|
|
@@ -293,6 +318,10 @@ export async function createWorktree(name, branchName, baseBranch = null, cwd =
|
|
|
293
318
|
|
|
294
319
|
export async function removeWorktree(path, force = false, cwd = process.cwd()) {
|
|
295
320
|
const git = await getGit(cwd);
|
|
321
|
+
|
|
322
|
+
// Prune stale worktree references before removing
|
|
323
|
+
await pruneWorktrees(cwd);
|
|
324
|
+
|
|
296
325
|
const args = ['worktree', 'remove'];
|
|
297
326
|
if (force) args.push('--force');
|
|
298
327
|
args.push(path);
|
package/src/setup.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { select, confirm } from '@inquirer/prompts';
|
|
2
|
+
import { ExitPromptError } from '@inquirer/core';
|
|
2
3
|
import { homedir } from 'os';
|
|
3
4
|
import { existsSync, readFileSync, appendFileSync, writeFileSync } from 'fs';
|
|
4
5
|
import { join } from 'path';
|
|
@@ -130,7 +131,7 @@ export async function setupCommand() {
|
|
|
130
131
|
info(`SHELL environment variable: ${process.env.SHELL || 'not set'}`);
|
|
131
132
|
spacer();
|
|
132
133
|
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/
|
|
134
|
+
console.log(` ${colors.muted('See:')} ${colors.path('https://github.com/jhorst11/wt#shell-integration')}`);
|
|
134
135
|
spacer();
|
|
135
136
|
return;
|
|
136
137
|
}
|
|
@@ -158,34 +159,44 @@ export async function setupCommand() {
|
|
|
158
159
|
console.log(` ${colors.muted('add a small shell function to your')} ${colors.path(config.rcFile)}`);
|
|
159
160
|
spacer();
|
|
160
161
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
+
});
|
|
181
183
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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;
|
|
189
200
|
}
|
|
190
201
|
}
|
|
191
202
|
|
package/src/ui.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import gradient from 'gradient-string';
|
|
3
3
|
import figures from 'figures';
|
|
4
|
+
import { createRequire } from 'module';
|
|
5
|
+
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const { version } = require('../package.json');
|
|
4
8
|
|
|
5
9
|
// Custom gradient for the logo
|
|
6
10
|
const wtGradient = gradient(['#00d4ff', '#7c3aed', '#f472b6']);
|
|
@@ -52,7 +56,7 @@ export function showLogo() {
|
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
export function showMiniLogo() {
|
|
55
|
-
console.log(`\n ${icons.tree} ${wtGradient('worktree')} ${colors.muted(
|
|
59
|
+
console.log(`\n ${icons.tree} ${wtGradient('worktree')} ${colors.muted(`v${version}`)}\n`);
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
export function success(message) {
|
|
@@ -125,10 +129,12 @@ export function showHelp() {
|
|
|
125
129
|
const commands = [
|
|
126
130
|
['wt', 'Interactive menu to manage worktrees'],
|
|
127
131
|
['wt new', 'Create a new worktree interactively'],
|
|
128
|
-
['wt list', 'List all worktrees for current repo'],
|
|
129
|
-
['wt
|
|
132
|
+
['wt list|ls', 'List all worktrees for current repo'],
|
|
133
|
+
['wt go [name]', 'Jump to a worktree (interactive if no name)'],
|
|
134
|
+
['wt merge', 'Merge a worktree branch into another branch'],
|
|
135
|
+
['wt remove|rm', 'Remove a worktree interactively'],
|
|
130
136
|
['wt home', 'Jump back to the main repository'],
|
|
131
|
-
['wt
|
|
137
|
+
['wt setup', 'Configure shell integration for auto-navigation'],
|
|
132
138
|
];
|
|
133
139
|
|
|
134
140
|
commands.forEach(([cmd, desc]) => {
|