vde-worktree 0.0.19 → 0.0.20
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.ja.md +34 -10
- package/README.md +34 -12
- package/completions/fish/vw.fish +26 -7
- package/completions/zsh/_vw +29 -8
- package/dist/index.mjs +975 -239
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { constants } from "node:fs";
|
|
4
|
-
import { access, appendFile, chmod, cp, mkdir, open, readFile, readdir, rename, rm, symlink, writeFile } from "node:fs/promises";
|
|
4
|
+
import { access, appendFile, chmod, cp, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, symlink, writeFile } from "node:fs/promises";
|
|
5
5
|
import { homedir, hostname } from "node:os";
|
|
6
6
|
import { dirname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -10,6 +10,7 @@ import { parseArgs } from "citty";
|
|
|
10
10
|
import { execa } from "execa";
|
|
11
11
|
import stringWidth from "string-width";
|
|
12
12
|
import { getBorderCharacters, table } from "table";
|
|
13
|
+
import { parse } from "yaml";
|
|
13
14
|
import { createHash } from "node:crypto";
|
|
14
15
|
|
|
15
16
|
//#region src/core/constants.ts
|
|
@@ -74,6 +75,7 @@ const WRITE_COMMANDS = new Set([
|
|
|
74
75
|
const ERROR_CODE_TO_EXIT_CODE = {
|
|
75
76
|
NOT_GIT_REPOSITORY: EXIT_CODE.NOT_GIT_REPOSITORY,
|
|
76
77
|
INVALID_ARGUMENT: EXIT_CODE.INVALID_ARGUMENT,
|
|
78
|
+
INVALID_CONFIG: EXIT_CODE.INVALID_ARGUMENT,
|
|
77
79
|
UNKNOWN_COMMAND: EXIT_CODE.INVALID_ARGUMENT,
|
|
78
80
|
UNSAFE_FLAG_REQUIRED: EXIT_CODE.SAFETY_REJECTED,
|
|
79
81
|
NOT_INITIALIZED: EXIT_CODE.SAFETY_REJECTED,
|
|
@@ -130,6 +132,691 @@ const ensureCliError = (error) => {
|
|
|
130
132
|
});
|
|
131
133
|
};
|
|
132
134
|
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/config/git-boundary.ts
|
|
137
|
+
const hasGitMarker = async (directory) => {
|
|
138
|
+
try {
|
|
139
|
+
const stat = await lstat(join(directory, ".git"));
|
|
140
|
+
return stat.isDirectory() || stat.isFile();
|
|
141
|
+
} catch {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const findGitBoundaryDirectory = async (cwd) => {
|
|
146
|
+
let current = resolve(cwd);
|
|
147
|
+
while (true) {
|
|
148
|
+
if (await hasGitMarker(current)) return current;
|
|
149
|
+
const parent = dirname(current);
|
|
150
|
+
if (parent === current) return null;
|
|
151
|
+
current = parent;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
const collectConfigSearchDirectories = async (cwd) => {
|
|
155
|
+
const absoluteCwd = resolve(cwd);
|
|
156
|
+
const boundary = await findGitBoundaryDirectory(absoluteCwd);
|
|
157
|
+
if (boundary === null) return [absoluteCwd];
|
|
158
|
+
const directories = [];
|
|
159
|
+
let current = absoluteCwd;
|
|
160
|
+
while (true) {
|
|
161
|
+
directories.push(current);
|
|
162
|
+
if (current === boundary) break;
|
|
163
|
+
const parent = dirname(current);
|
|
164
|
+
if (parent === current) break;
|
|
165
|
+
current = parent;
|
|
166
|
+
}
|
|
167
|
+
return directories.reverse();
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
//#endregion
|
|
171
|
+
//#region src/config/types.ts
|
|
172
|
+
const LIST_TABLE_COLUMNS = [
|
|
173
|
+
"branch",
|
|
174
|
+
"dirty",
|
|
175
|
+
"merged",
|
|
176
|
+
"pr",
|
|
177
|
+
"locked",
|
|
178
|
+
"ahead",
|
|
179
|
+
"behind",
|
|
180
|
+
"path"
|
|
181
|
+
];
|
|
182
|
+
const LIST_PATH_TRUNCATE_VALUES = ["auto", "never"];
|
|
183
|
+
const SELECTOR_CD_SURFACE_VALUES = [
|
|
184
|
+
"auto",
|
|
185
|
+
"inline",
|
|
186
|
+
"tmux-popup"
|
|
187
|
+
];
|
|
188
|
+
const DEFAULT_CONFIG = {
|
|
189
|
+
paths: { worktreeRoot: ".worktree" },
|
|
190
|
+
git: {
|
|
191
|
+
baseBranch: null,
|
|
192
|
+
baseRemote: "origin"
|
|
193
|
+
},
|
|
194
|
+
github: { enabled: true },
|
|
195
|
+
hooks: {
|
|
196
|
+
enabled: true,
|
|
197
|
+
timeoutMs: DEFAULT_HOOK_TIMEOUT_MS
|
|
198
|
+
},
|
|
199
|
+
locks: {
|
|
200
|
+
timeoutMs: DEFAULT_LOCK_TIMEOUT_MS,
|
|
201
|
+
staleLockTTLSeconds: DEFAULT_STALE_LOCK_TTL_SECONDS
|
|
202
|
+
},
|
|
203
|
+
list: { table: {
|
|
204
|
+
columns: [...LIST_TABLE_COLUMNS],
|
|
205
|
+
path: {
|
|
206
|
+
truncate: "auto",
|
|
207
|
+
minWidth: 12
|
|
208
|
+
}
|
|
209
|
+
} },
|
|
210
|
+
selector: { cd: {
|
|
211
|
+
prompt: "worktree> ",
|
|
212
|
+
surface: "auto",
|
|
213
|
+
tmuxPopupOpts: "80%,70%",
|
|
214
|
+
fzf: { extraArgs: [] }
|
|
215
|
+
} }
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
//#endregion
|
|
219
|
+
//#region src/config/loader.ts
|
|
220
|
+
const CONFIG_FILE_BASENAME = "config.yml";
|
|
221
|
+
const LOCAL_CONFIG_PATH_SEGMENTS = [
|
|
222
|
+
".vde",
|
|
223
|
+
"worktree",
|
|
224
|
+
CONFIG_FILE_BASENAME
|
|
225
|
+
];
|
|
226
|
+
const GLOBAL_CONFIG_PATH_SEGMENTS = [
|
|
227
|
+
"vde",
|
|
228
|
+
"worktree",
|
|
229
|
+
CONFIG_FILE_BASENAME
|
|
230
|
+
];
|
|
231
|
+
const isRecord = (value) => {
|
|
232
|
+
return value !== null && typeof value === "object" && Array.isArray(value) !== true;
|
|
233
|
+
};
|
|
234
|
+
const toKeyPath = (segments) => {
|
|
235
|
+
if (segments.length === 0) return "<root>";
|
|
236
|
+
return segments.join(".");
|
|
237
|
+
};
|
|
238
|
+
const throwInvalidConfig = ({ file, keyPath, reason }) => {
|
|
239
|
+
throw createCliError("INVALID_CONFIG", {
|
|
240
|
+
message: `Invalid config: ${file} (${keyPath}: ${reason})`,
|
|
241
|
+
details: {
|
|
242
|
+
file,
|
|
243
|
+
keyPath,
|
|
244
|
+
reason
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
};
|
|
248
|
+
const expectRecord = ({ value, ctx, keyPath }) => {
|
|
249
|
+
if (isRecord(value)) return value;
|
|
250
|
+
return throwInvalidConfig({
|
|
251
|
+
file: ctx.file,
|
|
252
|
+
keyPath: toKeyPath(keyPath),
|
|
253
|
+
reason: "must be an object"
|
|
254
|
+
});
|
|
255
|
+
};
|
|
256
|
+
const ensureNoUnknownKeys = ({ record, allowedKeys, ctx, keyPath }) => {
|
|
257
|
+
const allowed = new Set(allowedKeys);
|
|
258
|
+
for (const key of Object.keys(record)) {
|
|
259
|
+
if (allowed.has(key)) continue;
|
|
260
|
+
const path = [...keyPath, key];
|
|
261
|
+
throwInvalidConfig({
|
|
262
|
+
file: ctx.file,
|
|
263
|
+
keyPath: toKeyPath(path),
|
|
264
|
+
reason: "unknown key"
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
const parseBoolean = ({ value, ctx, keyPath }) => {
|
|
269
|
+
if (typeof value === "boolean") return value;
|
|
270
|
+
return throwInvalidConfig({
|
|
271
|
+
file: ctx.file,
|
|
272
|
+
keyPath: toKeyPath(keyPath),
|
|
273
|
+
reason: "must be boolean"
|
|
274
|
+
});
|
|
275
|
+
};
|
|
276
|
+
const parseNonEmptyString = ({ value, ctx, keyPath }) => {
|
|
277
|
+
if (typeof value === "string" && value.trim().length > 0) return value;
|
|
278
|
+
return throwInvalidConfig({
|
|
279
|
+
file: ctx.file,
|
|
280
|
+
keyPath: toKeyPath(keyPath),
|
|
281
|
+
reason: "must be a non-empty string"
|
|
282
|
+
});
|
|
283
|
+
};
|
|
284
|
+
const parsePositiveInteger = ({ value, ctx, keyPath }) => {
|
|
285
|
+
if (typeof value === "number" && Number.isInteger(value) && value > 0) return value;
|
|
286
|
+
return throwInvalidConfig({
|
|
287
|
+
file: ctx.file,
|
|
288
|
+
keyPath: toKeyPath(keyPath),
|
|
289
|
+
reason: "must be a positive integer"
|
|
290
|
+
});
|
|
291
|
+
};
|
|
292
|
+
const parseStringArray = ({ value, ctx, keyPath }) => {
|
|
293
|
+
if (Array.isArray(value) !== true) return throwInvalidConfig({
|
|
294
|
+
file: ctx.file,
|
|
295
|
+
keyPath: toKeyPath(keyPath),
|
|
296
|
+
reason: "must be an array"
|
|
297
|
+
});
|
|
298
|
+
const values = value;
|
|
299
|
+
const result = [];
|
|
300
|
+
for (const [index, item] of values.entries()) {
|
|
301
|
+
if (typeof item !== "string" || item.length === 0) throwInvalidConfig({
|
|
302
|
+
file: ctx.file,
|
|
303
|
+
keyPath: toKeyPath([...keyPath, String(index)]),
|
|
304
|
+
reason: "must be a non-empty string"
|
|
305
|
+
});
|
|
306
|
+
result.push(item);
|
|
307
|
+
}
|
|
308
|
+
return result;
|
|
309
|
+
};
|
|
310
|
+
const parseColumns = ({ value, ctx, keyPath }) => {
|
|
311
|
+
if (Array.isArray(value) !== true) return throwInvalidConfig({
|
|
312
|
+
file: ctx.file,
|
|
313
|
+
keyPath: toKeyPath(keyPath),
|
|
314
|
+
reason: "must be an array"
|
|
315
|
+
});
|
|
316
|
+
const values = value;
|
|
317
|
+
if (values.length === 0) return throwInvalidConfig({
|
|
318
|
+
file: ctx.file,
|
|
319
|
+
keyPath: toKeyPath(keyPath),
|
|
320
|
+
reason: "must not be empty"
|
|
321
|
+
});
|
|
322
|
+
const allowed = new Set(LIST_TABLE_COLUMNS);
|
|
323
|
+
const seen = /* @__PURE__ */ new Set();
|
|
324
|
+
const parsed = [];
|
|
325
|
+
for (const [index, item] of values.entries()) {
|
|
326
|
+
if (typeof item !== "string") throwInvalidConfig({
|
|
327
|
+
file: ctx.file,
|
|
328
|
+
keyPath: toKeyPath([...keyPath, String(index)]),
|
|
329
|
+
reason: "must be a string"
|
|
330
|
+
});
|
|
331
|
+
if (allowed.has(item) !== true) throwInvalidConfig({
|
|
332
|
+
file: ctx.file,
|
|
333
|
+
keyPath: toKeyPath([...keyPath, String(index)]),
|
|
334
|
+
reason: `unsupported column: ${item}`
|
|
335
|
+
});
|
|
336
|
+
if (seen.has(item)) throwInvalidConfig({
|
|
337
|
+
file: ctx.file,
|
|
338
|
+
keyPath: toKeyPath([...keyPath, String(index)]),
|
|
339
|
+
reason: `duplicate column: ${item}`
|
|
340
|
+
});
|
|
341
|
+
seen.add(item);
|
|
342
|
+
parsed.push(item);
|
|
343
|
+
}
|
|
344
|
+
return parsed;
|
|
345
|
+
};
|
|
346
|
+
const parseListPathTruncate = ({ value, ctx, keyPath }) => {
|
|
347
|
+
if (typeof value !== "string" || LIST_PATH_TRUNCATE_VALUES.includes(value) !== true) throwInvalidConfig({
|
|
348
|
+
file: ctx.file,
|
|
349
|
+
keyPath: toKeyPath(keyPath),
|
|
350
|
+
reason: `must be one of: ${LIST_PATH_TRUNCATE_VALUES.join(", ")}`
|
|
351
|
+
});
|
|
352
|
+
return value;
|
|
353
|
+
};
|
|
354
|
+
const parseSelectorSurface = ({ value, ctx, keyPath }) => {
|
|
355
|
+
if (typeof value !== "string" || SELECTOR_CD_SURFACE_VALUES.includes(value) !== true) throwInvalidConfig({
|
|
356
|
+
file: ctx.file,
|
|
357
|
+
keyPath: toKeyPath(keyPath),
|
|
358
|
+
reason: `must be one of: ${SELECTOR_CD_SURFACE_VALUES.join(", ")}`
|
|
359
|
+
});
|
|
360
|
+
return value;
|
|
361
|
+
};
|
|
362
|
+
const validatePartialConfig = ({ rawConfig, ctx }) => {
|
|
363
|
+
if (rawConfig === null || rawConfig === void 0) return {};
|
|
364
|
+
const root = expectRecord({
|
|
365
|
+
value: rawConfig,
|
|
366
|
+
ctx,
|
|
367
|
+
keyPath: []
|
|
368
|
+
});
|
|
369
|
+
ensureNoUnknownKeys({
|
|
370
|
+
record: root,
|
|
371
|
+
allowedKeys: [
|
|
372
|
+
"paths",
|
|
373
|
+
"git",
|
|
374
|
+
"github",
|
|
375
|
+
"hooks",
|
|
376
|
+
"locks",
|
|
377
|
+
"list",
|
|
378
|
+
"selector"
|
|
379
|
+
],
|
|
380
|
+
ctx,
|
|
381
|
+
keyPath: []
|
|
382
|
+
});
|
|
383
|
+
const partial = {};
|
|
384
|
+
if (root.paths !== void 0) {
|
|
385
|
+
const paths = expectRecord({
|
|
386
|
+
value: root.paths,
|
|
387
|
+
ctx,
|
|
388
|
+
keyPath: ["paths"]
|
|
389
|
+
});
|
|
390
|
+
ensureNoUnknownKeys({
|
|
391
|
+
record: paths,
|
|
392
|
+
allowedKeys: ["worktreeRoot"],
|
|
393
|
+
ctx,
|
|
394
|
+
keyPath: ["paths"]
|
|
395
|
+
});
|
|
396
|
+
partial.paths = {};
|
|
397
|
+
if (paths.worktreeRoot !== void 0) partial.paths.worktreeRoot = parseNonEmptyString({
|
|
398
|
+
value: paths.worktreeRoot,
|
|
399
|
+
ctx,
|
|
400
|
+
keyPath: ["paths", "worktreeRoot"]
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
if (root.git !== void 0) {
|
|
404
|
+
const git = expectRecord({
|
|
405
|
+
value: root.git,
|
|
406
|
+
ctx,
|
|
407
|
+
keyPath: ["git"]
|
|
408
|
+
});
|
|
409
|
+
ensureNoUnknownKeys({
|
|
410
|
+
record: git,
|
|
411
|
+
allowedKeys: ["baseBranch", "baseRemote"],
|
|
412
|
+
ctx,
|
|
413
|
+
keyPath: ["git"]
|
|
414
|
+
});
|
|
415
|
+
partial.git = {};
|
|
416
|
+
if (git.baseBranch !== void 0) {
|
|
417
|
+
if (git.baseBranch !== null && typeof git.baseBranch !== "string") throwInvalidConfig({
|
|
418
|
+
file: ctx.file,
|
|
419
|
+
keyPath: toKeyPath(["git", "baseBranch"]),
|
|
420
|
+
reason: "must be a string or null"
|
|
421
|
+
});
|
|
422
|
+
partial.git.baseBranch = git.baseBranch === null ? null : parseNonEmptyString({
|
|
423
|
+
value: git.baseBranch,
|
|
424
|
+
ctx,
|
|
425
|
+
keyPath: ["git", "baseBranch"]
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
if (git.baseRemote !== void 0) partial.git.baseRemote = parseNonEmptyString({
|
|
429
|
+
value: git.baseRemote,
|
|
430
|
+
ctx,
|
|
431
|
+
keyPath: ["git", "baseRemote"]
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
if (root.github !== void 0) {
|
|
435
|
+
const github = expectRecord({
|
|
436
|
+
value: root.github,
|
|
437
|
+
ctx,
|
|
438
|
+
keyPath: ["github"]
|
|
439
|
+
});
|
|
440
|
+
ensureNoUnknownKeys({
|
|
441
|
+
record: github,
|
|
442
|
+
allowedKeys: ["enabled"],
|
|
443
|
+
ctx,
|
|
444
|
+
keyPath: ["github"]
|
|
445
|
+
});
|
|
446
|
+
partial.github = {};
|
|
447
|
+
if (github.enabled !== void 0) partial.github.enabled = parseBoolean({
|
|
448
|
+
value: github.enabled,
|
|
449
|
+
ctx,
|
|
450
|
+
keyPath: ["github", "enabled"]
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
if (root.hooks !== void 0) {
|
|
454
|
+
const hooks = expectRecord({
|
|
455
|
+
value: root.hooks,
|
|
456
|
+
ctx,
|
|
457
|
+
keyPath: ["hooks"]
|
|
458
|
+
});
|
|
459
|
+
ensureNoUnknownKeys({
|
|
460
|
+
record: hooks,
|
|
461
|
+
allowedKeys: ["enabled", "timeoutMs"],
|
|
462
|
+
ctx,
|
|
463
|
+
keyPath: ["hooks"]
|
|
464
|
+
});
|
|
465
|
+
partial.hooks = {};
|
|
466
|
+
if (hooks.enabled !== void 0) partial.hooks.enabled = parseBoolean({
|
|
467
|
+
value: hooks.enabled,
|
|
468
|
+
ctx,
|
|
469
|
+
keyPath: ["hooks", "enabled"]
|
|
470
|
+
});
|
|
471
|
+
if (hooks.timeoutMs !== void 0) partial.hooks.timeoutMs = parsePositiveInteger({
|
|
472
|
+
value: hooks.timeoutMs,
|
|
473
|
+
ctx,
|
|
474
|
+
keyPath: ["hooks", "timeoutMs"]
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
if (root.locks !== void 0) {
|
|
478
|
+
const locks = expectRecord({
|
|
479
|
+
value: root.locks,
|
|
480
|
+
ctx,
|
|
481
|
+
keyPath: ["locks"]
|
|
482
|
+
});
|
|
483
|
+
ensureNoUnknownKeys({
|
|
484
|
+
record: locks,
|
|
485
|
+
allowedKeys: ["timeoutMs", "staleLockTTLSeconds"],
|
|
486
|
+
ctx,
|
|
487
|
+
keyPath: ["locks"]
|
|
488
|
+
});
|
|
489
|
+
partial.locks = {};
|
|
490
|
+
if (locks.timeoutMs !== void 0) partial.locks.timeoutMs = parsePositiveInteger({
|
|
491
|
+
value: locks.timeoutMs,
|
|
492
|
+
ctx,
|
|
493
|
+
keyPath: ["locks", "timeoutMs"]
|
|
494
|
+
});
|
|
495
|
+
if (locks.staleLockTTLSeconds !== void 0) partial.locks.staleLockTTLSeconds = parsePositiveInteger({
|
|
496
|
+
value: locks.staleLockTTLSeconds,
|
|
497
|
+
ctx,
|
|
498
|
+
keyPath: ["locks", "staleLockTTLSeconds"]
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
if (root.list !== void 0) {
|
|
502
|
+
const list = expectRecord({
|
|
503
|
+
value: root.list,
|
|
504
|
+
ctx,
|
|
505
|
+
keyPath: ["list"]
|
|
506
|
+
});
|
|
507
|
+
ensureNoUnknownKeys({
|
|
508
|
+
record: list,
|
|
509
|
+
allowedKeys: ["table"],
|
|
510
|
+
ctx,
|
|
511
|
+
keyPath: ["list"]
|
|
512
|
+
});
|
|
513
|
+
partial.list = {};
|
|
514
|
+
if (list.table !== void 0) {
|
|
515
|
+
const table = expectRecord({
|
|
516
|
+
value: list.table,
|
|
517
|
+
ctx,
|
|
518
|
+
keyPath: ["list", "table"]
|
|
519
|
+
});
|
|
520
|
+
ensureNoUnknownKeys({
|
|
521
|
+
record: table,
|
|
522
|
+
allowedKeys: ["columns", "path"],
|
|
523
|
+
ctx,
|
|
524
|
+
keyPath: ["list", "table"]
|
|
525
|
+
});
|
|
526
|
+
partial.list.table = {};
|
|
527
|
+
if (table.columns !== void 0) partial.list.table.columns = parseColumns({
|
|
528
|
+
value: table.columns,
|
|
529
|
+
ctx,
|
|
530
|
+
keyPath: [
|
|
531
|
+
"list",
|
|
532
|
+
"table",
|
|
533
|
+
"columns"
|
|
534
|
+
]
|
|
535
|
+
});
|
|
536
|
+
if (table.path !== void 0) {
|
|
537
|
+
const pathConfig = expectRecord({
|
|
538
|
+
value: table.path,
|
|
539
|
+
ctx,
|
|
540
|
+
keyPath: [
|
|
541
|
+
"list",
|
|
542
|
+
"table",
|
|
543
|
+
"path"
|
|
544
|
+
]
|
|
545
|
+
});
|
|
546
|
+
ensureNoUnknownKeys({
|
|
547
|
+
record: pathConfig,
|
|
548
|
+
allowedKeys: ["truncate", "minWidth"],
|
|
549
|
+
ctx,
|
|
550
|
+
keyPath: [
|
|
551
|
+
"list",
|
|
552
|
+
"table",
|
|
553
|
+
"path"
|
|
554
|
+
]
|
|
555
|
+
});
|
|
556
|
+
partial.list.table.path = {};
|
|
557
|
+
if (pathConfig.truncate !== void 0) partial.list.table.path.truncate = parseListPathTruncate({
|
|
558
|
+
value: pathConfig.truncate,
|
|
559
|
+
ctx,
|
|
560
|
+
keyPath: [
|
|
561
|
+
"list",
|
|
562
|
+
"table",
|
|
563
|
+
"path",
|
|
564
|
+
"truncate"
|
|
565
|
+
]
|
|
566
|
+
});
|
|
567
|
+
if (pathConfig.minWidth !== void 0) {
|
|
568
|
+
const minWidth = parsePositiveInteger({
|
|
569
|
+
value: pathConfig.minWidth,
|
|
570
|
+
ctx,
|
|
571
|
+
keyPath: [
|
|
572
|
+
"list",
|
|
573
|
+
"table",
|
|
574
|
+
"path",
|
|
575
|
+
"minWidth"
|
|
576
|
+
]
|
|
577
|
+
});
|
|
578
|
+
if (minWidth < 8 || minWidth > 200) throwInvalidConfig({
|
|
579
|
+
file: ctx.file,
|
|
580
|
+
keyPath: toKeyPath([
|
|
581
|
+
"list",
|
|
582
|
+
"table",
|
|
583
|
+
"path",
|
|
584
|
+
"minWidth"
|
|
585
|
+
]),
|
|
586
|
+
reason: "must be in range 8..200"
|
|
587
|
+
});
|
|
588
|
+
partial.list.table.path.minWidth = minWidth;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
if (root.selector !== void 0) {
|
|
594
|
+
const selector = expectRecord({
|
|
595
|
+
value: root.selector,
|
|
596
|
+
ctx,
|
|
597
|
+
keyPath: ["selector"]
|
|
598
|
+
});
|
|
599
|
+
ensureNoUnknownKeys({
|
|
600
|
+
record: selector,
|
|
601
|
+
allowedKeys: ["cd"],
|
|
602
|
+
ctx,
|
|
603
|
+
keyPath: ["selector"]
|
|
604
|
+
});
|
|
605
|
+
partial.selector = {};
|
|
606
|
+
if (selector.cd !== void 0) {
|
|
607
|
+
const cd = expectRecord({
|
|
608
|
+
value: selector.cd,
|
|
609
|
+
ctx,
|
|
610
|
+
keyPath: ["selector", "cd"]
|
|
611
|
+
});
|
|
612
|
+
ensureNoUnknownKeys({
|
|
613
|
+
record: cd,
|
|
614
|
+
allowedKeys: [
|
|
615
|
+
"prompt",
|
|
616
|
+
"surface",
|
|
617
|
+
"tmuxPopupOpts",
|
|
618
|
+
"fzf"
|
|
619
|
+
],
|
|
620
|
+
ctx,
|
|
621
|
+
keyPath: ["selector", "cd"]
|
|
622
|
+
});
|
|
623
|
+
partial.selector.cd = {};
|
|
624
|
+
if (cd.prompt !== void 0) partial.selector.cd.prompt = parseNonEmptyString({
|
|
625
|
+
value: cd.prompt,
|
|
626
|
+
ctx,
|
|
627
|
+
keyPath: [
|
|
628
|
+
"selector",
|
|
629
|
+
"cd",
|
|
630
|
+
"prompt"
|
|
631
|
+
]
|
|
632
|
+
});
|
|
633
|
+
if (cd.surface !== void 0) partial.selector.cd.surface = parseSelectorSurface({
|
|
634
|
+
value: cd.surface,
|
|
635
|
+
ctx,
|
|
636
|
+
keyPath: [
|
|
637
|
+
"selector",
|
|
638
|
+
"cd",
|
|
639
|
+
"surface"
|
|
640
|
+
]
|
|
641
|
+
});
|
|
642
|
+
if (cd.tmuxPopupOpts !== void 0) partial.selector.cd.tmuxPopupOpts = parseNonEmptyString({
|
|
643
|
+
value: cd.tmuxPopupOpts,
|
|
644
|
+
ctx,
|
|
645
|
+
keyPath: [
|
|
646
|
+
"selector",
|
|
647
|
+
"cd",
|
|
648
|
+
"tmuxPopupOpts"
|
|
649
|
+
]
|
|
650
|
+
});
|
|
651
|
+
if (cd.fzf !== void 0) {
|
|
652
|
+
const fzf = expectRecord({
|
|
653
|
+
value: cd.fzf,
|
|
654
|
+
ctx,
|
|
655
|
+
keyPath: [
|
|
656
|
+
"selector",
|
|
657
|
+
"cd",
|
|
658
|
+
"fzf"
|
|
659
|
+
]
|
|
660
|
+
});
|
|
661
|
+
ensureNoUnknownKeys({
|
|
662
|
+
record: fzf,
|
|
663
|
+
allowedKeys: ["extraArgs"],
|
|
664
|
+
ctx,
|
|
665
|
+
keyPath: [
|
|
666
|
+
"selector",
|
|
667
|
+
"cd",
|
|
668
|
+
"fzf"
|
|
669
|
+
]
|
|
670
|
+
});
|
|
671
|
+
partial.selector.cd.fzf = {};
|
|
672
|
+
if (fzf.extraArgs !== void 0) partial.selector.cd.fzf.extraArgs = parseStringArray({
|
|
673
|
+
value: fzf.extraArgs,
|
|
674
|
+
ctx,
|
|
675
|
+
keyPath: [
|
|
676
|
+
"selector",
|
|
677
|
+
"cd",
|
|
678
|
+
"fzf",
|
|
679
|
+
"extraArgs"
|
|
680
|
+
]
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return partial;
|
|
686
|
+
};
|
|
687
|
+
const mergeConfig = (base, partial) => {
|
|
688
|
+
return {
|
|
689
|
+
paths: { worktreeRoot: partial.paths?.worktreeRoot ?? base.paths.worktreeRoot },
|
|
690
|
+
git: {
|
|
691
|
+
baseBranch: partial.git?.baseBranch === void 0 ? base.git.baseBranch : partial.git.baseBranch,
|
|
692
|
+
baseRemote: partial.git?.baseRemote ?? base.git.baseRemote
|
|
693
|
+
},
|
|
694
|
+
github: { enabled: partial.github?.enabled ?? base.github.enabled },
|
|
695
|
+
hooks: {
|
|
696
|
+
enabled: partial.hooks?.enabled ?? base.hooks.enabled,
|
|
697
|
+
timeoutMs: partial.hooks?.timeoutMs ?? base.hooks.timeoutMs
|
|
698
|
+
},
|
|
699
|
+
locks: {
|
|
700
|
+
timeoutMs: partial.locks?.timeoutMs ?? base.locks.timeoutMs,
|
|
701
|
+
staleLockTTLSeconds: partial.locks?.staleLockTTLSeconds ?? base.locks.staleLockTTLSeconds
|
|
702
|
+
},
|
|
703
|
+
list: { table: {
|
|
704
|
+
columns: partial.list?.table?.columns ? [...partial.list.table.columns] : [...base.list.table.columns],
|
|
705
|
+
path: {
|
|
706
|
+
truncate: partial.list?.table?.path?.truncate ?? base.list.table.path.truncate,
|
|
707
|
+
minWidth: partial.list?.table?.path?.minWidth ?? base.list.table.path.minWidth
|
|
708
|
+
}
|
|
709
|
+
} },
|
|
710
|
+
selector: { cd: {
|
|
711
|
+
prompt: partial.selector?.cd?.prompt ?? base.selector.cd.prompt,
|
|
712
|
+
surface: partial.selector?.cd?.surface ?? base.selector.cd.surface,
|
|
713
|
+
tmuxPopupOpts: partial.selector?.cd?.tmuxPopupOpts ?? base.selector.cd.tmuxPopupOpts,
|
|
714
|
+
fzf: { extraArgs: partial.selector?.cd?.fzf?.extraArgs ? [...partial.selector.cd.fzf.extraArgs] : [...base.selector.cd.fzf.extraArgs] }
|
|
715
|
+
} }
|
|
716
|
+
};
|
|
717
|
+
};
|
|
718
|
+
const configPathExists = async (filePath) => {
|
|
719
|
+
try {
|
|
720
|
+
await access(filePath, constants.F_OK);
|
|
721
|
+
return true;
|
|
722
|
+
} catch {
|
|
723
|
+
return false;
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
const resolveLocalConfigPath = (directory) => {
|
|
727
|
+
return join(directory, ...LOCAL_CONFIG_PATH_SEGMENTS);
|
|
728
|
+
};
|
|
729
|
+
const resolveGlobalConfigPath = () => {
|
|
730
|
+
const xdgConfigHome = process.env.XDG_CONFIG_HOME;
|
|
731
|
+
if (typeof xdgConfigHome === "string" && xdgConfigHome.length > 0) return join(resolve(xdgConfigHome), ...GLOBAL_CONFIG_PATH_SEGMENTS);
|
|
732
|
+
return join(homedir(), ".config", ...GLOBAL_CONFIG_PATH_SEGMENTS);
|
|
733
|
+
};
|
|
734
|
+
const resolveExistingConfigFiles = async ({ cwd, repoRoot }) => {
|
|
735
|
+
const localCandidates = (await collectConfigSearchDirectories(cwd)).map((directory) => resolveLocalConfigPath(directory));
|
|
736
|
+
const repoRootCandidate = resolveLocalConfigPath(repoRoot);
|
|
737
|
+
const lowToHighCandidates = [
|
|
738
|
+
resolveGlobalConfigPath(),
|
|
739
|
+
repoRootCandidate,
|
|
740
|
+
...localCandidates
|
|
741
|
+
];
|
|
742
|
+
const deduped = /* @__PURE__ */ new Map();
|
|
743
|
+
for (const [order, candidate] of lowToHighCandidates.entries()) {
|
|
744
|
+
if (await configPathExists(candidate) !== true) continue;
|
|
745
|
+
const canonical = await realpath(candidate).catch(() => resolve(candidate));
|
|
746
|
+
deduped.set(canonical, {
|
|
747
|
+
path: candidate,
|
|
748
|
+
order
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
return [...deduped.values()].sort((a, b) => a.order - b.order).map((entry) => entry.path);
|
|
752
|
+
};
|
|
753
|
+
const isPathInsideOrEqual$1 = ({ parentPath, childPath }) => {
|
|
754
|
+
const rel = relative(parentPath, childPath);
|
|
755
|
+
if (rel.length === 0) return true;
|
|
756
|
+
return rel !== ".." && rel.startsWith(`..${sep}`) !== true;
|
|
757
|
+
};
|
|
758
|
+
const validateWorktreeRoot = async ({ repoRoot, config }) => {
|
|
759
|
+
const rawWorktreeRoot = config.paths.worktreeRoot;
|
|
760
|
+
const resolvedWorktreeRoot = isAbsolute(rawWorktreeRoot) ? resolve(rawWorktreeRoot) : resolve(repoRoot, rawWorktreeRoot);
|
|
761
|
+
if (isPathInsideOrEqual$1({
|
|
762
|
+
parentPath: resolve(repoRoot, ".git"),
|
|
763
|
+
childPath: resolvedWorktreeRoot
|
|
764
|
+
})) throwInvalidConfig({
|
|
765
|
+
file: "<resolved>",
|
|
766
|
+
keyPath: "paths.worktreeRoot",
|
|
767
|
+
reason: "must not point inside .git"
|
|
768
|
+
});
|
|
769
|
+
try {
|
|
770
|
+
if ((await lstat(resolvedWorktreeRoot)).isDirectory() !== true) throwInvalidConfig({
|
|
771
|
+
file: "<resolved>",
|
|
772
|
+
keyPath: "paths.worktreeRoot",
|
|
773
|
+
reason: "must not point to an existing file"
|
|
774
|
+
});
|
|
775
|
+
} catch (error) {
|
|
776
|
+
if (error.code === "ENOENT") return;
|
|
777
|
+
throw error;
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
const parseConfigFile = async (file) => {
|
|
781
|
+
const rawContent = await readFile(file, "utf8");
|
|
782
|
+
let parsed;
|
|
783
|
+
try {
|
|
784
|
+
parsed = parse(rawContent);
|
|
785
|
+
} catch (error) {
|
|
786
|
+
throwInvalidConfig({
|
|
787
|
+
file,
|
|
788
|
+
keyPath: "<root>",
|
|
789
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
return validatePartialConfig({
|
|
793
|
+
rawConfig: parsed,
|
|
794
|
+
ctx: { file }
|
|
795
|
+
});
|
|
796
|
+
};
|
|
797
|
+
const cloneDefaultConfig = () => {
|
|
798
|
+
return mergeConfig(DEFAULT_CONFIG, {});
|
|
799
|
+
};
|
|
800
|
+
const loadResolvedConfig = async ({ cwd, repoRoot }) => {
|
|
801
|
+
const files = await resolveExistingConfigFiles({
|
|
802
|
+
cwd,
|
|
803
|
+
repoRoot
|
|
804
|
+
});
|
|
805
|
+
let config = cloneDefaultConfig();
|
|
806
|
+
for (const file of files) {
|
|
807
|
+
const partial = await parseConfigFile(file);
|
|
808
|
+
config = mergeConfig(config, partial);
|
|
809
|
+
}
|
|
810
|
+
await validateWorktreeRoot({
|
|
811
|
+
repoRoot,
|
|
812
|
+
config
|
|
813
|
+
});
|
|
814
|
+
return {
|
|
815
|
+
config,
|
|
816
|
+
loadedFiles: files
|
|
817
|
+
};
|
|
818
|
+
};
|
|
819
|
+
|
|
133
820
|
//#endregion
|
|
134
821
|
//#region src/git/exec.ts
|
|
135
822
|
const runGitCommand = async ({ cwd, args, reject = true }) => {
|
|
@@ -175,6 +862,7 @@ const doesGitRefExist = async (cwd, ref) => {
|
|
|
175
862
|
//#endregion
|
|
176
863
|
//#region src/core/paths.ts
|
|
177
864
|
const GIT_DIR_NAME = ".git";
|
|
865
|
+
const DEFAULT_WORKTREE_ROOT = ".worktree";
|
|
178
866
|
const WORKTREE_ID_HASH_LENGTH = 12;
|
|
179
867
|
const WORKTREE_ID_SLUG_MAX_LENGTH = 48;
|
|
180
868
|
const resolveRepoRootFromCommonDir = ({ currentWorktreeRoot, gitCommonDir }) => {
|
|
@@ -212,8 +900,9 @@ const resolveRepoContext = async (cwd) => {
|
|
|
212
900
|
gitCommonDir
|
|
213
901
|
};
|
|
214
902
|
};
|
|
215
|
-
const getWorktreeRootPath = (repoRoot) => {
|
|
216
|
-
return
|
|
903
|
+
const getWorktreeRootPath = (repoRoot, configuredWorktreeRoot = DEFAULT_WORKTREE_ROOT) => {
|
|
904
|
+
if (isAbsolute(configuredWorktreeRoot)) return resolve(configuredWorktreeRoot);
|
|
905
|
+
return resolve(repoRoot, configuredWorktreeRoot);
|
|
217
906
|
};
|
|
218
907
|
const getWorktreeMetaRootPath = (repoRoot) => {
|
|
219
908
|
return join(repoRoot, ".vde", "worktree");
|
|
@@ -233,25 +922,33 @@ const getStateDirectoryPath = (repoRoot) => {
|
|
|
233
922
|
const branchToWorktreeId = (branch) => {
|
|
234
923
|
return `${branch.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, WORKTREE_ID_SLUG_MAX_LENGTH) || "branch"}--${createHash("sha256").update(branch).digest("hex").slice(0, WORKTREE_ID_HASH_LENGTH)}`;
|
|
235
924
|
};
|
|
236
|
-
const branchToWorktreePath = (repoRoot, branch) => {
|
|
237
|
-
const worktreeRoot = getWorktreeRootPath(repoRoot);
|
|
238
|
-
return
|
|
239
|
-
|
|
240
|
-
path: join(worktreeRoot, ...branch.split("/"))
|
|
925
|
+
const branchToWorktreePath = (repoRoot, branch, configuredWorktreeRoot = DEFAULT_WORKTREE_ROOT) => {
|
|
926
|
+
const worktreeRoot = getWorktreeRootPath(repoRoot, configuredWorktreeRoot);
|
|
927
|
+
return ensurePathInsideRoot({
|
|
928
|
+
rootPath: worktreeRoot,
|
|
929
|
+
path: join(worktreeRoot, ...branch.split("/")),
|
|
930
|
+
message: "Path is outside managed worktree root"
|
|
241
931
|
});
|
|
242
932
|
};
|
|
243
|
-
const
|
|
244
|
-
const rel = relative(
|
|
933
|
+
const ensurePathInsideRoot = ({ rootPath, path, message = "Path is outside allowed root" }) => {
|
|
934
|
+
const rel = relative(rootPath, path);
|
|
245
935
|
if (rel === "") return path;
|
|
246
936
|
if (rel === ".." || rel.startsWith(`..${sep}`)) throw createCliError("PATH_OUTSIDE_REPO", {
|
|
247
|
-
message
|
|
937
|
+
message,
|
|
248
938
|
details: {
|
|
249
|
-
|
|
939
|
+
rootPath,
|
|
250
940
|
path
|
|
251
941
|
}
|
|
252
942
|
});
|
|
253
943
|
return path;
|
|
254
944
|
};
|
|
945
|
+
const ensurePathInsideRepo = ({ repoRoot, path }) => {
|
|
946
|
+
return ensurePathInsideRoot({
|
|
947
|
+
rootPath: repoRoot,
|
|
948
|
+
path,
|
|
949
|
+
message: "Path is outside repository root"
|
|
950
|
+
});
|
|
951
|
+
};
|
|
255
952
|
const resolveRepoRelativePath = ({ repoRoot, relativePath }) => {
|
|
256
953
|
if (isAbsolute(relativePath)) throw createCliError("ABSOLUTE_PATH_NOT_ALLOWED", {
|
|
257
954
|
message: "Absolute path is not allowed",
|
|
@@ -266,6 +963,11 @@ const resolvePathFromCwd = ({ cwd, path }) => {
|
|
|
266
963
|
if (isAbsolute(path)) return path;
|
|
267
964
|
return resolve(cwd, path);
|
|
268
965
|
};
|
|
966
|
+
const isManagedWorktreePath = ({ worktreePath, managedWorktreeRoot }) => {
|
|
967
|
+
const rel = relative(managedWorktreeRoot, worktreePath);
|
|
968
|
+
if (rel === "" || rel === "." || rel === "..") return false;
|
|
969
|
+
return rel.startsWith(`..${sep}`) !== true;
|
|
970
|
+
};
|
|
269
971
|
|
|
270
972
|
//#endregion
|
|
271
973
|
//#region src/core/hooks.ts
|
|
@@ -414,7 +1116,7 @@ const invokeHook = async ({ hookName, args, context }) => {
|
|
|
414
1116
|
|
|
415
1117
|
//#endregion
|
|
416
1118
|
//#region src/core/init.ts
|
|
417
|
-
const
|
|
1119
|
+
const EXCLUDE_MARKER = "# vde-worktree (managed)";
|
|
418
1120
|
const DEFAULT_HOOKS = [{
|
|
419
1121
|
name: "post-new",
|
|
420
1122
|
lines: [
|
|
@@ -448,7 +1150,27 @@ const createHookTemplate = async (hooksDir, name, lines) => {
|
|
|
448
1150
|
await chmod(targetPath, 493);
|
|
449
1151
|
}
|
|
450
1152
|
};
|
|
451
|
-
const
|
|
1153
|
+
const isPathInsideOrEqual = ({ rootPath, candidatePath }) => {
|
|
1154
|
+
const rel = relative(rootPath, candidatePath);
|
|
1155
|
+
if (rel.length === 0) return true;
|
|
1156
|
+
return rel !== ".." && rel.startsWith(`..${sep}`) !== true;
|
|
1157
|
+
};
|
|
1158
|
+
const toExcludeEntry = ({ repoRoot, managedWorktreeRoot }) => {
|
|
1159
|
+
if (isPathInsideOrEqual({
|
|
1160
|
+
rootPath: repoRoot,
|
|
1161
|
+
candidatePath: managedWorktreeRoot
|
|
1162
|
+
}) !== true) return null;
|
|
1163
|
+
const rel = relative(repoRoot, managedWorktreeRoot).split(sep).join("/");
|
|
1164
|
+
const normalized = rel.length === 0 ? "." : rel;
|
|
1165
|
+
return normalized.endsWith("/") ? normalized : `${normalized}/`;
|
|
1166
|
+
};
|
|
1167
|
+
const ensureExcludeBlock = async ({ repoRoot, managedWorktreeRoot }) => {
|
|
1168
|
+
const managedEntry = toExcludeEntry({
|
|
1169
|
+
repoRoot,
|
|
1170
|
+
managedWorktreeRoot
|
|
1171
|
+
});
|
|
1172
|
+
if (managedEntry === null) return;
|
|
1173
|
+
const managedExcludeBlock = `${EXCLUDE_MARKER}\n${managedEntry}\n.vde/worktree/\n`;
|
|
452
1174
|
const excludePath = join(repoRoot, ".git", "info", "exclude");
|
|
453
1175
|
let current = "";
|
|
454
1176
|
try {
|
|
@@ -456,8 +1178,8 @@ const ensureExcludeBlock = async (repoRoot) => {
|
|
|
456
1178
|
} catch {
|
|
457
1179
|
current = "";
|
|
458
1180
|
}
|
|
459
|
-
if (current.includes(
|
|
460
|
-
await writeFile(excludePath, `${current.endsWith("\n") || current.length === 0 ? current : `${current}\n`}${
|
|
1181
|
+
if (current.includes(managedExcludeBlock)) return;
|
|
1182
|
+
await writeFile(excludePath, `${current.endsWith("\n") || current.length === 0 ? current : `${current}\n`}${managedExcludeBlock}`, "utf8");
|
|
461
1183
|
};
|
|
462
1184
|
const isInitialized = async (repoRoot) => {
|
|
463
1185
|
try {
|
|
@@ -467,14 +1189,17 @@ const isInitialized = async (repoRoot) => {
|
|
|
467
1189
|
return false;
|
|
468
1190
|
}
|
|
469
1191
|
};
|
|
470
|
-
const initializeRepository = async (repoRoot) => {
|
|
1192
|
+
const initializeRepository = async ({ repoRoot, managedWorktreeRoot }) => {
|
|
471
1193
|
const wasInitialized = await isInitialized(repoRoot);
|
|
472
|
-
await mkdir(
|
|
1194
|
+
await mkdir(managedWorktreeRoot, { recursive: true });
|
|
473
1195
|
await mkdir(getHooksDirectoryPath(repoRoot), { recursive: true });
|
|
474
1196
|
await mkdir(getLogsDirectoryPath(repoRoot), { recursive: true });
|
|
475
1197
|
await mkdir(getLocksDirectoryPath(repoRoot), { recursive: true });
|
|
476
1198
|
await mkdir(getStateDirectoryPath(repoRoot), { recursive: true });
|
|
477
|
-
await ensureExcludeBlock(
|
|
1199
|
+
await ensureExcludeBlock({
|
|
1200
|
+
repoRoot,
|
|
1201
|
+
managedWorktreeRoot
|
|
1202
|
+
});
|
|
478
1203
|
for (const hook of DEFAULT_HOOKS) await createHookTemplate(getHooksDirectoryPath(repoRoot), hook.name, hook.lines);
|
|
479
1204
|
return { alreadyInitialized: wasInitialized };
|
|
480
1205
|
};
|
|
@@ -1018,36 +1743,6 @@ const listGitWorktrees = async (repoRoot) => {
|
|
|
1018
1743
|
|
|
1019
1744
|
//#endregion
|
|
1020
1745
|
//#region src/core/worktree-state.ts
|
|
1021
|
-
const resolveBaseBranch$1 = async (repoRoot) => {
|
|
1022
|
-
const explicit = await runGitCommand({
|
|
1023
|
-
cwd: repoRoot,
|
|
1024
|
-
args: [
|
|
1025
|
-
"config",
|
|
1026
|
-
"--get",
|
|
1027
|
-
"vde-worktree.baseBranch"
|
|
1028
|
-
],
|
|
1029
|
-
reject: false
|
|
1030
|
-
});
|
|
1031
|
-
if (explicit.exitCode === 0 && explicit.stdout.trim().length > 0) return explicit.stdout.trim();
|
|
1032
|
-
for (const candidate of ["main", "master"]) if (await doesGitRefExist(repoRoot, `refs/heads/${candidate}`)) return candidate;
|
|
1033
|
-
return null;
|
|
1034
|
-
};
|
|
1035
|
-
const resolveEnableGh = async (repoRoot) => {
|
|
1036
|
-
const result = await runGitCommand({
|
|
1037
|
-
cwd: repoRoot,
|
|
1038
|
-
args: [
|
|
1039
|
-
"config",
|
|
1040
|
-
"--bool",
|
|
1041
|
-
"--get",
|
|
1042
|
-
"vde-worktree.enableGh"
|
|
1043
|
-
],
|
|
1044
|
-
reject: false
|
|
1045
|
-
});
|
|
1046
|
-
if (result.exitCode !== 0) return true;
|
|
1047
|
-
const value = result.stdout.trim().toLowerCase();
|
|
1048
|
-
if (value === "false" || value === "no" || value === "off" || value === "0") return false;
|
|
1049
|
-
return true;
|
|
1050
|
-
};
|
|
1051
1746
|
const resolveDirty = async (worktreePath) => {
|
|
1052
1747
|
return (await runGitCommand({
|
|
1053
1748
|
cwd: worktreePath,
|
|
@@ -1316,17 +2011,13 @@ const enrichWorktree = async ({ repoRoot, worktree, baseBranch, prStateByBranch
|
|
|
1316
2011
|
upstream
|
|
1317
2012
|
};
|
|
1318
2013
|
};
|
|
1319
|
-
const collectWorktreeSnapshot = async (repoRoot, { noGh = false } = {}) => {
|
|
1320
|
-
const
|
|
1321
|
-
resolveBaseBranch$1(repoRoot),
|
|
1322
|
-
listGitWorktrees(repoRoot),
|
|
1323
|
-
resolveEnableGh(repoRoot)
|
|
1324
|
-
]);
|
|
2014
|
+
const collectWorktreeSnapshot = async (repoRoot, { baseBranch = null, ghEnabled = true, noGh = false } = {}) => {
|
|
2015
|
+
const worktrees = await listGitWorktrees(repoRoot);
|
|
1325
2016
|
const prStateByBranch = await resolvePrStateByBranchBatch({
|
|
1326
2017
|
repoRoot,
|
|
1327
2018
|
baseBranch,
|
|
1328
2019
|
branches: worktrees.map((worktree) => worktree.branch),
|
|
1329
|
-
enabled:
|
|
2020
|
+
enabled: ghEnabled && noGh !== true
|
|
1330
2021
|
});
|
|
1331
2022
|
return {
|
|
1332
2023
|
repoRoot,
|
|
@@ -1350,7 +2041,8 @@ const RESERVED_FZF_ARGS = new Set([
|
|
|
1350
2041
|
"prompt",
|
|
1351
2042
|
"layout",
|
|
1352
2043
|
"height",
|
|
1353
|
-
"border"
|
|
2044
|
+
"border",
|
|
2045
|
+
"tmux"
|
|
1354
2046
|
]);
|
|
1355
2047
|
const ANSI_ESCAPE_SEQUENCE_PATTERN = String.raw`\u001B\[[0-?]*[ -/]*[@-~]`;
|
|
1356
2048
|
const ANSI_ESCAPE_SEQUENCE_REGEX = new RegExp(ANSI_ESCAPE_SEQUENCE_PATTERN, "g");
|
|
@@ -1390,6 +2082,13 @@ const defaultCheckFzfAvailability = async () => {
|
|
|
1390
2082
|
throw error;
|
|
1391
2083
|
}
|
|
1392
2084
|
};
|
|
2085
|
+
const defaultCheckFzfTmuxSupport = async () => {
|
|
2086
|
+
try {
|
|
2087
|
+
return (await execa(FZF_BINARY, ["--help"], { timeout: FZF_CHECK_TIMEOUT_MS })).stdout.includes("--tmux");
|
|
2088
|
+
} catch {
|
|
2089
|
+
return false;
|
|
2090
|
+
}
|
|
2091
|
+
};
|
|
1393
2092
|
const defaultRunFzf = async ({ args, input, cwd, env }) => {
|
|
1394
2093
|
return { stdout: (await execa(FZF_BINARY, args, {
|
|
1395
2094
|
input,
|
|
@@ -1402,20 +2101,46 @@ const ensureFzfAvailable = async (checkFzfAvailability) => {
|
|
|
1402
2101
|
if (await checkFzfAvailability()) return;
|
|
1403
2102
|
throw new Error("fzf is required for interactive selection");
|
|
1404
2103
|
};
|
|
1405
|
-
const
|
|
2104
|
+
const shouldTryTmuxPopup = async ({ surface, env, checkFzfTmuxSupport }) => {
|
|
2105
|
+
if (surface === "inline") return false;
|
|
2106
|
+
if (surface === "tmux-popup") return true;
|
|
2107
|
+
if (typeof env.TMUX !== "string" || env.TMUX.length === 0) return false;
|
|
2108
|
+
try {
|
|
2109
|
+
return await checkFzfTmuxSupport();
|
|
2110
|
+
} catch {
|
|
2111
|
+
return false;
|
|
2112
|
+
}
|
|
2113
|
+
};
|
|
2114
|
+
const isTmuxUnknownOptionError = (error) => {
|
|
2115
|
+
const execaError = error;
|
|
2116
|
+
const text = [
|
|
2117
|
+
execaError.message,
|
|
2118
|
+
execaError.shortMessage,
|
|
2119
|
+
execaError.stderr,
|
|
2120
|
+
execaError.stdout
|
|
2121
|
+
].filter((value) => typeof value === "string" && value.length > 0).join("\n");
|
|
2122
|
+
return /unknown option.*--tmux|--tmux.*unknown option/i.test(text);
|
|
2123
|
+
};
|
|
2124
|
+
const selectPathWithFzf = async ({ candidates, prompt = "worktree> ", surface = "inline", tmuxPopupOpts = "80%,70%", fzfExtraArgs = [], cwd = process.cwd(), env = process.env, isInteractive = () => process.stdout.isTTY === true && process.stderr.isTTY === true, checkFzfAvailability = defaultCheckFzfAvailability, checkFzfTmuxSupport = defaultCheckFzfTmuxSupport, runFzf = defaultRunFzf }) => {
|
|
1406
2125
|
if (candidates.length === 0) throw new Error("No candidates provided for fzf selection");
|
|
1407
2126
|
if (isInteractive() !== true) throw new Error("fzf selection requires an interactive terminal");
|
|
1408
2127
|
await ensureFzfAvailable(checkFzfAvailability);
|
|
1409
|
-
const
|
|
2128
|
+
const baseArgs = buildFzfArgs({
|
|
1410
2129
|
prompt,
|
|
1411
2130
|
fzfExtraArgs
|
|
1412
2131
|
});
|
|
2132
|
+
const tryTmuxPopup = await shouldTryTmuxPopup({
|
|
2133
|
+
surface,
|
|
2134
|
+
env,
|
|
2135
|
+
checkFzfTmuxSupport
|
|
2136
|
+
});
|
|
2137
|
+
const args = tryTmuxPopup ? [...baseArgs, `--tmux=${tmuxPopupOpts}`] : baseArgs;
|
|
1413
2138
|
const input = buildFzfInput(candidates);
|
|
1414
2139
|
if (input.length === 0) throw new Error("All candidates are empty after sanitization");
|
|
1415
2140
|
const candidateSet = new Set(input.split("\n").map((candidate) => stripAnsi(candidate)));
|
|
1416
|
-
|
|
2141
|
+
const runWithValidation = async (fzfArgs) => {
|
|
1417
2142
|
const selectedPath = stripAnsi(stripTrailingNewlines((await runFzf({
|
|
1418
|
-
args,
|
|
2143
|
+
args: fzfArgs,
|
|
1419
2144
|
input,
|
|
1420
2145
|
cwd,
|
|
1421
2146
|
env
|
|
@@ -1426,7 +2151,16 @@ const selectPathWithFzf = async ({ candidates, prompt = "worktree> ", fzfExtraAr
|
|
|
1426
2151
|
status: "selected",
|
|
1427
2152
|
path: selectedPath
|
|
1428
2153
|
};
|
|
2154
|
+
};
|
|
2155
|
+
try {
|
|
2156
|
+
return await runWithValidation(args);
|
|
1429
2157
|
} catch (error) {
|
|
2158
|
+
if (tryTmuxPopup && isTmuxUnknownOptionError(error)) try {
|
|
2159
|
+
return await runWithValidation(baseArgs);
|
|
2160
|
+
} catch (fallbackError) {
|
|
2161
|
+
if (fallbackError.exitCode === 130) return { status: "cancelled" };
|
|
2162
|
+
throw fallbackError;
|
|
2163
|
+
}
|
|
1430
2164
|
if (error.exitCode === 130) return { status: "cancelled" };
|
|
1431
2165
|
throw error;
|
|
1432
2166
|
}
|
|
@@ -1514,9 +2248,7 @@ const CD_FZF_EXTRA_ARGS = [
|
|
|
1514
2248
|
"--preview-window=right,60%,wrap",
|
|
1515
2249
|
"--ansi"
|
|
1516
2250
|
];
|
|
1517
|
-
const
|
|
1518
|
-
const LIST_TABLE_PATH_COLUMN_INDEX = 7;
|
|
1519
|
-
const LIST_TABLE_PATH_MIN_WIDTH = 12;
|
|
2251
|
+
const DEFAULT_LIST_TABLE_COLUMNS = [...LIST_TABLE_COLUMNS];
|
|
1520
2252
|
const LIST_TABLE_CELL_HORIZONTAL_PADDING = 2;
|
|
1521
2253
|
const COMPLETION_SHELLS = ["zsh", "fish"];
|
|
1522
2254
|
const COMPLETION_FILE_BY_SHELL = {
|
|
@@ -1538,6 +2270,10 @@ const CATPPUCCIN_MOCHA = {
|
|
|
1538
2270
|
overlay0: "#6c7086"
|
|
1539
2271
|
};
|
|
1540
2272
|
const identityColor = (value) => value;
|
|
2273
|
+
const hasDefaultListColumnOrder = (columns) => {
|
|
2274
|
+
if (columns.length !== DEFAULT_LIST_TABLE_COLUMNS.length) return false;
|
|
2275
|
+
return columns.every((column, index) => column === DEFAULT_LIST_TABLE_COLUMNS[index]);
|
|
2276
|
+
};
|
|
1541
2277
|
const createCatppuccinTheme = ({ enabled }) => {
|
|
1542
2278
|
if (enabled !== true) return {
|
|
1543
2279
|
header: identityColor,
|
|
@@ -1985,85 +2721,29 @@ const ensureArgumentCount = ({ command, args, min, max }) => {
|
|
|
1985
2721
|
const ensureHasCommandAfterDoubleDash = ({ command, argsAfterDoubleDash }) => {
|
|
1986
2722
|
if (argsAfterDoubleDash.length === 0) throw createCliError("INVALID_ARGUMENT", { message: `${command} requires arguments after --` });
|
|
1987
2723
|
};
|
|
1988
|
-
const
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
"config",
|
|
1993
|
-
"--get",
|
|
1994
|
-
key
|
|
1995
|
-
],
|
|
1996
|
-
reject: false
|
|
1997
|
-
});
|
|
1998
|
-
if (result.exitCode !== 0) return;
|
|
1999
|
-
const parsed = Number.parseInt(result.stdout.trim(), 10);
|
|
2000
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
2001
|
-
};
|
|
2002
|
-
const readGitConfigBoolean = async (repoRoot, key) => {
|
|
2003
|
-
const result = await runGitCommand({
|
|
2004
|
-
cwd: repoRoot,
|
|
2005
|
-
args: [
|
|
2006
|
-
"config",
|
|
2007
|
-
"--bool",
|
|
2008
|
-
"--get",
|
|
2009
|
-
key
|
|
2010
|
-
],
|
|
2011
|
-
reject: false
|
|
2012
|
-
});
|
|
2013
|
-
if (result.exitCode !== 0) return;
|
|
2014
|
-
const value = result.stdout.trim().toLowerCase();
|
|
2015
|
-
if (value === "true" || value === "yes" || value === "on" || value === "1") return true;
|
|
2016
|
-
if (value === "false" || value === "no" || value === "off" || value === "0") return false;
|
|
2017
|
-
};
|
|
2018
|
-
const resolveConfiguredBaseRemote = async (repoRoot) => {
|
|
2019
|
-
const configured = await runGitCommand({
|
|
2724
|
+
const resolveBaseBranch = async ({ repoRoot, config }) => {
|
|
2725
|
+
if (typeof config.git.baseBranch === "string" && config.git.baseBranch.length > 0) return config.git.baseBranch;
|
|
2726
|
+
const remote = config.git.baseRemote;
|
|
2727
|
+
const resolved = await runGitCommand({
|
|
2020
2728
|
cwd: repoRoot,
|
|
2021
2729
|
args: [
|
|
2022
|
-
"
|
|
2023
|
-
"--
|
|
2024
|
-
"
|
|
2025
|
-
|
|
2026
|
-
reject: false
|
|
2027
|
-
});
|
|
2028
|
-
if (configured.exitCode === 0 && configured.stdout.trim().length > 0) return configured.stdout.trim();
|
|
2029
|
-
return "origin";
|
|
2030
|
-
};
|
|
2031
|
-
const resolveBaseBranch = async (repoRoot) => {
|
|
2032
|
-
const configured = await runGitCommand({
|
|
2033
|
-
cwd: repoRoot,
|
|
2034
|
-
args: [
|
|
2035
|
-
"config",
|
|
2036
|
-
"--get",
|
|
2037
|
-
"vde-worktree.baseBranch"
|
|
2730
|
+
"symbolic-ref",
|
|
2731
|
+
"--quiet",
|
|
2732
|
+
"--short",
|
|
2733
|
+
`refs/remotes/${remote}/HEAD`
|
|
2038
2734
|
],
|
|
2039
2735
|
reject: false
|
|
2040
2736
|
});
|
|
2041
|
-
if (
|
|
2042
|
-
const remotesToProbe = [
|
|
2043
|
-
await resolveConfiguredBaseRemote(repoRoot),
|
|
2044
|
-
"origin",
|
|
2045
|
-
"upstream"
|
|
2046
|
-
].filter((value, index, arr) => {
|
|
2047
|
-
return arr.indexOf(value) === index;
|
|
2048
|
-
});
|
|
2049
|
-
for (const remote of remotesToProbe) {
|
|
2050
|
-
const resolved = await runGitCommand({
|
|
2051
|
-
cwd: repoRoot,
|
|
2052
|
-
args: [
|
|
2053
|
-
"symbolic-ref",
|
|
2054
|
-
"--quiet",
|
|
2055
|
-
"--short",
|
|
2056
|
-
`refs/remotes/${remote}/HEAD`
|
|
2057
|
-
],
|
|
2058
|
-
reject: false
|
|
2059
|
-
});
|
|
2060
|
-
if (resolved.exitCode !== 0) continue;
|
|
2737
|
+
if (resolved.exitCode === 0) {
|
|
2061
2738
|
const raw = resolved.stdout.trim();
|
|
2062
2739
|
const prefix = `${remote}/`;
|
|
2063
2740
|
if (raw.startsWith(prefix)) return raw.slice(prefix.length);
|
|
2064
2741
|
}
|
|
2065
2742
|
for (const candidate of ["main", "master"]) if (await doesGitRefExist(repoRoot, `refs/heads/${candidate}`)) return candidate;
|
|
2066
|
-
throw createCliError("INVALID_ARGUMENT", {
|
|
2743
|
+
throw createCliError("INVALID_ARGUMENT", {
|
|
2744
|
+
message: "Unable to resolve base branch from config.yml (baseRemote/HEAD -> main/master).",
|
|
2745
|
+
details: { remote }
|
|
2746
|
+
});
|
|
2067
2747
|
};
|
|
2068
2748
|
const ensureTargetPathWritable = async (targetPath) => {
|
|
2069
2749
|
try {
|
|
@@ -2256,7 +2936,7 @@ const validateDeleteSafety = ({ target, forceFlags }) => {
|
|
|
2256
2936
|
const resolveLinkTargetPath = ({ sourcePath, destinationPath }) => {
|
|
2257
2937
|
return relative(dirname(destinationPath), sourcePath);
|
|
2258
2938
|
};
|
|
2259
|
-
const resolveFileCopyTargets = ({ repoRoot,
|
|
2939
|
+
const resolveFileCopyTargets = ({ repoRoot, targetWorktreeRoot, relativePath }) => {
|
|
2260
2940
|
const sourcePath = resolveRepoRelativePath({
|
|
2261
2941
|
repoRoot,
|
|
2262
2942
|
relativePath
|
|
@@ -2264,13 +2944,32 @@ const resolveFileCopyTargets = ({ repoRoot, worktreePath, relativePath }) => {
|
|
|
2264
2944
|
const relativeFromRoot = relative(repoRoot, sourcePath);
|
|
2265
2945
|
return {
|
|
2266
2946
|
sourcePath,
|
|
2267
|
-
destinationPath:
|
|
2268
|
-
|
|
2269
|
-
path: resolve(
|
|
2947
|
+
destinationPath: ensurePathInsideRoot({
|
|
2948
|
+
rootPath: targetWorktreeRoot,
|
|
2949
|
+
path: resolve(targetWorktreeRoot, relativeFromRoot),
|
|
2950
|
+
message: "Path is outside target worktree root"
|
|
2270
2951
|
}),
|
|
2271
2952
|
relativeFromRoot
|
|
2272
2953
|
};
|
|
2273
2954
|
};
|
|
2955
|
+
const resolveTargetWorktreeRootForCopyLink = ({ repoContext, snapshot }) => {
|
|
2956
|
+
const rawTarget = process.env.WT_WORKTREE_PATH ?? repoContext.currentWorktreeRoot;
|
|
2957
|
+
const resolvedTarget = resolvePathFromCwd({
|
|
2958
|
+
cwd: repoContext.currentWorktreeRoot,
|
|
2959
|
+
path: rawTarget
|
|
2960
|
+
});
|
|
2961
|
+
const matched = snapshot.worktrees.filter((worktree) => {
|
|
2962
|
+
return worktree.path === resolvedTarget || resolvedTarget.startsWith(`${worktree.path}${sep}`);
|
|
2963
|
+
}).sort((a, b) => b.path.length - a.path.length)[0];
|
|
2964
|
+
if (matched === void 0) throw createCliError("WORKTREE_NOT_FOUND", {
|
|
2965
|
+
message: "copy/link target worktree not found",
|
|
2966
|
+
details: {
|
|
2967
|
+
rawTarget,
|
|
2968
|
+
resolvedTarget
|
|
2969
|
+
}
|
|
2970
|
+
});
|
|
2971
|
+
return matched.path;
|
|
2972
|
+
};
|
|
2274
2973
|
const ensureBranchIsNotPrimary = ({ branch, baseBranch }) => {
|
|
2275
2974
|
if (branch !== baseBranch) return;
|
|
2276
2975
|
throw createCliError("INVALID_ARGUMENT", {
|
|
@@ -2281,12 +2980,14 @@ const ensureBranchIsNotPrimary = ({ branch, baseBranch }) => {
|
|
|
2281
2980
|
}
|
|
2282
2981
|
});
|
|
2283
2982
|
};
|
|
2284
|
-
const toManagedWorktreeName = ({
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2983
|
+
const toManagedWorktreeName = ({ managedWorktreeRoot, worktreePath }) => {
|
|
2984
|
+
if (isManagedWorktreePath({
|
|
2985
|
+
worktreePath,
|
|
2986
|
+
managedWorktreeRoot
|
|
2987
|
+
}) !== true) return null;
|
|
2988
|
+
return relative(managedWorktreeRoot, worktreePath).split(sep).join("/");
|
|
2288
2989
|
};
|
|
2289
|
-
const resolveManagedWorktreePathFromName = ({
|
|
2990
|
+
const resolveManagedWorktreePathFromName = ({ managedWorktreeRoot, optionName, worktreeName }) => {
|
|
2290
2991
|
const normalized = worktreeName.trim();
|
|
2291
2992
|
if (normalized.length === 0) throw createCliError("INVALID_ARGUMENT", {
|
|
2292
2993
|
message: `${optionName} requires non-empty worktree name`,
|
|
@@ -2295,18 +2996,10 @@ const resolveManagedWorktreePathFromName = ({ repoRoot, optionName, worktreeName
|
|
|
2295
2996
|
worktreeName
|
|
2296
2997
|
}
|
|
2297
2998
|
});
|
|
2298
|
-
if (normalized === ".worktree" || normalized.startsWith(".worktree/") || normalized.startsWith(".worktree\\")) throw createCliError("INVALID_ARGUMENT", {
|
|
2299
|
-
message: `${optionName} expects vw-managed worktree name (without .worktree/ prefix)`,
|
|
2300
|
-
details: {
|
|
2301
|
-
optionName,
|
|
2302
|
-
worktreeName
|
|
2303
|
-
}
|
|
2304
|
-
});
|
|
2305
|
-
const worktreeRoot = getWorktreeRootPath(repoRoot);
|
|
2306
2999
|
let resolvedPath;
|
|
2307
3000
|
try {
|
|
2308
3001
|
resolvedPath = resolveRepoRelativePath({
|
|
2309
|
-
repoRoot:
|
|
3002
|
+
repoRoot: managedWorktreeRoot,
|
|
2310
3003
|
relativePath: normalized
|
|
2311
3004
|
});
|
|
2312
3005
|
} catch (error) {
|
|
@@ -2319,7 +3012,7 @@ const resolveManagedWorktreePathFromName = ({ repoRoot, optionName, worktreeName
|
|
|
2319
3012
|
cause: error
|
|
2320
3013
|
});
|
|
2321
3014
|
}
|
|
2322
|
-
if (resolvedPath ===
|
|
3015
|
+
if (resolvedPath === managedWorktreeRoot) throw createCliError("INVALID_ARGUMENT", {
|
|
2323
3016
|
message: `${optionName} expects vw-managed worktree name`,
|
|
2324
3017
|
details: {
|
|
2325
3018
|
optionName,
|
|
@@ -2328,16 +3021,16 @@ const resolveManagedWorktreePathFromName = ({ repoRoot, optionName, worktreeName
|
|
|
2328
3021
|
});
|
|
2329
3022
|
return resolvedPath;
|
|
2330
3023
|
};
|
|
2331
|
-
const resolveManagedNonPrimaryWorktreeByBranch = ({ repoRoot, branch, worktrees, optionName, worktreeName, role }) => {
|
|
3024
|
+
const resolveManagedNonPrimaryWorktreeByBranch = ({ repoRoot, managedWorktreeRoot, branch, worktrees, optionName, worktreeName, role }) => {
|
|
2332
3025
|
const managedCandidates = worktrees.filter((worktree) => {
|
|
2333
3026
|
return worktree.branch === branch && worktree.path !== repoRoot && toManagedWorktreeName({
|
|
2334
|
-
|
|
3027
|
+
managedWorktreeRoot,
|
|
2335
3028
|
worktreePath: worktree.path
|
|
2336
3029
|
}) !== null;
|
|
2337
3030
|
});
|
|
2338
3031
|
if (typeof worktreeName === "string") {
|
|
2339
3032
|
const resolvedPath = resolveManagedWorktreePathFromName({
|
|
2340
|
-
|
|
3033
|
+
managedWorktreeRoot,
|
|
2341
3034
|
optionName,
|
|
2342
3035
|
worktreeName
|
|
2343
3036
|
});
|
|
@@ -2368,7 +3061,7 @@ const resolveManagedNonPrimaryWorktreeByBranch = ({ repoRoot, branch, worktrees,
|
|
|
2368
3061
|
optionName,
|
|
2369
3062
|
candidates: managedCandidates.map((worktree) => {
|
|
2370
3063
|
return toManagedWorktreeName({
|
|
2371
|
-
|
|
3064
|
+
managedWorktreeRoot,
|
|
2372
3065
|
worktreePath: worktree.path
|
|
2373
3066
|
}) ?? worktree.path;
|
|
2374
3067
|
})
|
|
@@ -2525,19 +3218,24 @@ const resolveListColumnContentWidth = ({ rows, columnIndex }) => {
|
|
|
2525
3218
|
return Math.max(width, stringWidth(cell));
|
|
2526
3219
|
}, 0);
|
|
2527
3220
|
};
|
|
2528
|
-
const resolveListPathColumnWidth = ({ rows,
|
|
2529
|
-
|
|
3221
|
+
const resolveListPathColumnWidth = ({ rows, columns, truncateMode, fullPath, minWidth }) => {
|
|
3222
|
+
const pathColumnIndex = columns.indexOf("path");
|
|
3223
|
+
if (pathColumnIndex < 0) return null;
|
|
3224
|
+
if (fullPath || truncateMode === "never") return null;
|
|
2530
3225
|
if (process.stdout.isTTY !== true) return null;
|
|
2531
3226
|
const terminalColumns = process.stdout.columns;
|
|
2532
3227
|
if (typeof terminalColumns !== "number" || Number.isFinite(terminalColumns) !== true || terminalColumns <= 0) return null;
|
|
2533
|
-
const measuredNonPathWidth =
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
3228
|
+
const measuredNonPathWidth = columns.map((_, index) => {
|
|
3229
|
+
if (index === pathColumnIndex) return 0;
|
|
3230
|
+
return resolveListColumnContentWidth({
|
|
3231
|
+
rows,
|
|
3232
|
+
columnIndex: index
|
|
3233
|
+
});
|
|
3234
|
+
}).reduce((sum, width) => sum + width, 0);
|
|
3235
|
+
const borderWidth = columns.length + 1;
|
|
3236
|
+
const paddingWidth = columns.length * LIST_TABLE_CELL_HORIZONTAL_PADDING;
|
|
2539
3237
|
const availablePathWidth = Math.floor(terminalColumns) - borderWidth - paddingWidth - measuredNonPathWidth;
|
|
2540
|
-
return Math.max(
|
|
3238
|
+
return Math.max(minWidth, availablePathWidth);
|
|
2541
3239
|
};
|
|
2542
3240
|
const resolveAheadBehindAgainstBaseBranch = async ({ repoRoot, baseBranch, worktree }) => {
|
|
2543
3241
|
if (baseBranch === null) return {
|
|
@@ -2878,7 +3576,7 @@ const createCli = (options = {}) => {
|
|
|
2878
3576
|
from: {
|
|
2879
3577
|
type: "string",
|
|
2880
3578
|
valueHint: "value",
|
|
2881
|
-
description: "For extract: filesystem path. For absorb: managed worktree name
|
|
3579
|
+
description: "For extract: filesystem path. For absorb: managed worktree name."
|
|
2882
3580
|
},
|
|
2883
3581
|
to: {
|
|
2884
3582
|
type: "string",
|
|
@@ -3027,39 +3725,47 @@ const createCli = (options = {}) => {
|
|
|
3027
3725
|
const repoContext = await resolveRepoContext(runtimeCwd);
|
|
3028
3726
|
const repoRoot = repoContext.repoRoot;
|
|
3029
3727
|
repoRootForJson = repoRoot;
|
|
3030
|
-
const
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3728
|
+
const { config: resolvedConfig } = await loadResolvedConfig({
|
|
3729
|
+
cwd: runtimeCwd,
|
|
3730
|
+
repoRoot
|
|
3731
|
+
});
|
|
3732
|
+
const managedWorktreeRoot = getWorktreeRootPath(repoRoot, resolvedConfig.paths.worktreeRoot);
|
|
3034
3733
|
const runtime = {
|
|
3035
3734
|
command,
|
|
3036
3735
|
json: jsonEnabled,
|
|
3037
|
-
hooksEnabled: parsedArgs.hooks !== false &&
|
|
3038
|
-
ghEnabled: parsedArgs.gh !== false,
|
|
3736
|
+
hooksEnabled: parsedArgs.hooks !== false && resolvedConfig.hooks.enabled,
|
|
3737
|
+
ghEnabled: parsedArgs.gh !== false && resolvedConfig.github.enabled,
|
|
3039
3738
|
strictPostHooks: parsedArgs.strictPostHooks === true,
|
|
3040
3739
|
hookTimeoutMs: readNumberFromEnvOrDefault({
|
|
3041
3740
|
rawValue: toNumberOption({
|
|
3042
3741
|
value: parsedArgs.hookTimeoutMs,
|
|
3043
3742
|
optionName: "--hook-timeout-ms"
|
|
3044
|
-
}) ??
|
|
3743
|
+
}) ?? resolvedConfig.hooks.timeoutMs,
|
|
3045
3744
|
defaultValue: DEFAULT_HOOK_TIMEOUT_MS
|
|
3046
3745
|
}),
|
|
3047
3746
|
lockTimeoutMs: readNumberFromEnvOrDefault({
|
|
3048
3747
|
rawValue: toNumberOption({
|
|
3049
3748
|
value: parsedArgs.lockTimeoutMs,
|
|
3050
3749
|
optionName: "--lock-timeout-ms"
|
|
3051
|
-
}) ??
|
|
3750
|
+
}) ?? resolvedConfig.locks.timeoutMs,
|
|
3052
3751
|
defaultValue: DEFAULT_LOCK_TIMEOUT_MS
|
|
3053
3752
|
}),
|
|
3054
3753
|
allowUnsafe,
|
|
3055
3754
|
isInteractive: isInteractiveFn()
|
|
3056
3755
|
};
|
|
3057
3756
|
const staleLockTTLSeconds = readNumberFromEnvOrDefault({
|
|
3058
|
-
rawValue:
|
|
3757
|
+
rawValue: resolvedConfig.locks.staleLockTTLSeconds,
|
|
3059
3758
|
defaultValue: DEFAULT_STALE_LOCK_TTL_SECONDS
|
|
3060
3759
|
});
|
|
3061
3760
|
const collectWorktreeSnapshot$1 = async (_ignoredRepoRoot) => {
|
|
3062
|
-
return collectWorktreeSnapshot(repoRoot, {
|
|
3761
|
+
return collectWorktreeSnapshot(repoRoot, {
|
|
3762
|
+
baseBranch: await resolveBaseBranch({
|
|
3763
|
+
repoRoot,
|
|
3764
|
+
config: resolvedConfig
|
|
3765
|
+
}),
|
|
3766
|
+
ghEnabled: runtime.ghEnabled,
|
|
3767
|
+
noGh: runtime.ghEnabled !== true
|
|
3768
|
+
});
|
|
3063
3769
|
};
|
|
3064
3770
|
const runWriteOperation = async (task) => {
|
|
3065
3771
|
if (WRITE_COMMANDS.has(command) !== true) return task();
|
|
@@ -3091,7 +3797,10 @@ const createCli = (options = {}) => {
|
|
|
3091
3797
|
name: "init",
|
|
3092
3798
|
context: hookContext
|
|
3093
3799
|
});
|
|
3094
|
-
const initialized = await initializeRepository(
|
|
3800
|
+
const initialized = await initializeRepository({
|
|
3801
|
+
repoRoot,
|
|
3802
|
+
managedWorktreeRoot
|
|
3803
|
+
});
|
|
3095
3804
|
await runPostHook({
|
|
3096
3805
|
name: "init",
|
|
3097
3806
|
context: hookContext
|
|
@@ -3127,22 +3836,15 @@ const createCli = (options = {}) => {
|
|
|
3127
3836
|
repoRoot,
|
|
3128
3837
|
details: {
|
|
3129
3838
|
baseBranch: snapshot.baseBranch,
|
|
3839
|
+
managedWorktreeRoot,
|
|
3130
3840
|
worktrees: snapshot.worktrees
|
|
3131
3841
|
}
|
|
3132
3842
|
})));
|
|
3133
3843
|
return EXIT_CODE.OK;
|
|
3134
3844
|
}
|
|
3135
3845
|
const theme = createCatppuccinTheme({ enabled: shouldUseAnsiColors({ interactive: runtime.isInteractive }) });
|
|
3136
|
-
const
|
|
3137
|
-
|
|
3138
|
-
"dirty",
|
|
3139
|
-
"merged",
|
|
3140
|
-
"pr",
|
|
3141
|
-
"locked",
|
|
3142
|
-
"ahead",
|
|
3143
|
-
"behind",
|
|
3144
|
-
"path"
|
|
3145
|
-
], ...await Promise.all(snapshot.worktrees.map(async (worktree) => {
|
|
3846
|
+
const columns = resolvedConfig.list.table.columns;
|
|
3847
|
+
const rows = [[...columns], ...await Promise.all(snapshot.worktrees.map(async (worktree) => {
|
|
3146
3848
|
const distanceFromBase = await resolveAheadBehindAgainstBaseBranch({
|
|
3147
3849
|
repoRoot,
|
|
3148
3850
|
baseBranch: snapshot.baseBranch,
|
|
@@ -3154,35 +3856,41 @@ const createCli = (options = {}) => {
|
|
|
3154
3856
|
prStatus: worktree.pr.status,
|
|
3155
3857
|
isBaseBranch
|
|
3156
3858
|
});
|
|
3157
|
-
|
|
3158
|
-
`${worktree.path === repoContext.currentWorktreeRoot ? "*" : " "} ${worktree.branch ?? "(detached)"}`,
|
|
3159
|
-
worktree.dirty ? "dirty" : "clean",
|
|
3160
|
-
mergedState,
|
|
3161
|
-
prState,
|
|
3162
|
-
worktree.locked.value ? "locked" : "-",
|
|
3163
|
-
formatListUpstreamCount(distanceFromBase.ahead),
|
|
3164
|
-
formatListUpstreamCount(distanceFromBase.behind),
|
|
3165
|
-
formatDisplayPath(worktree.path)
|
|
3166
|
-
|
|
3859
|
+
const valuesByColumn = {
|
|
3860
|
+
branch: `${worktree.path === repoContext.currentWorktreeRoot ? "*" : " "} ${worktree.branch ?? "(detached)"}`,
|
|
3861
|
+
dirty: worktree.dirty ? "dirty" : "clean",
|
|
3862
|
+
merged: mergedState,
|
|
3863
|
+
pr: prState,
|
|
3864
|
+
locked: worktree.locked.value ? "locked" : "-",
|
|
3865
|
+
ahead: formatListUpstreamCount(distanceFromBase.ahead),
|
|
3866
|
+
behind: formatListUpstreamCount(distanceFromBase.behind),
|
|
3867
|
+
path: formatDisplayPath(worktree.path)
|
|
3868
|
+
};
|
|
3869
|
+
return columns.map((column) => valuesByColumn[column]);
|
|
3167
3870
|
}))];
|
|
3168
3871
|
const pathColumnWidth = resolveListPathColumnWidth({
|
|
3169
3872
|
rows,
|
|
3170
|
-
|
|
3873
|
+
columns,
|
|
3874
|
+
truncateMode: resolvedConfig.list.table.path.truncate,
|
|
3875
|
+
fullPath: parsedArgs.fullPath === true,
|
|
3876
|
+
minWidth: resolvedConfig.list.table.path.minWidth
|
|
3171
3877
|
});
|
|
3172
|
-
const
|
|
3878
|
+
const pathColumnIndex = columns.indexOf("path");
|
|
3879
|
+
const columnsConfig = pathColumnWidth === null || pathColumnIndex < 0 ? void 0 : { [pathColumnIndex]: {
|
|
3173
3880
|
width: pathColumnWidth,
|
|
3174
3881
|
truncate: pathColumnWidth
|
|
3175
3882
|
} };
|
|
3176
|
-
const
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
columns: columnsConfig
|
|
3183
|
-
}),
|
|
3184
|
-
theme
|
|
3883
|
+
const rendered = table(rows, {
|
|
3884
|
+
border: getBorderCharacters("norc"),
|
|
3885
|
+
drawHorizontalLine: (lineIndex, rowCount) => {
|
|
3886
|
+
return lineIndex === 0 || lineIndex === 1 || lineIndex === rowCount;
|
|
3887
|
+
},
|
|
3888
|
+
columns: columnsConfig
|
|
3185
3889
|
});
|
|
3890
|
+
const colorized = hasDefaultListColumnOrder(columns) ? colorizeListTable({
|
|
3891
|
+
rendered,
|
|
3892
|
+
theme
|
|
3893
|
+
}) : rendered.trimEnd();
|
|
3186
3894
|
for (const line of colorized.split("\n")) stdout(line);
|
|
3187
3895
|
return EXIT_CODE.OK;
|
|
3188
3896
|
}
|
|
@@ -3264,9 +3972,12 @@ const createCli = (options = {}) => {
|
|
|
3264
3972
|
message: `Branch already exists locally: ${branch}`,
|
|
3265
3973
|
details: { branch }
|
|
3266
3974
|
});
|
|
3267
|
-
const targetPath = branchToWorktreePath(repoRoot, branch);
|
|
3975
|
+
const targetPath = branchToWorktreePath(repoRoot, branch, resolvedConfig.paths.worktreeRoot);
|
|
3268
3976
|
await ensureTargetPathWritable(targetPath);
|
|
3269
|
-
const baseBranch = await resolveBaseBranch(
|
|
3977
|
+
const baseBranch = await resolveBaseBranch({
|
|
3978
|
+
repoRoot,
|
|
3979
|
+
config: resolvedConfig
|
|
3980
|
+
});
|
|
3270
3981
|
const hookContext = createHookContext({
|
|
3271
3982
|
runtime,
|
|
3272
3983
|
repoRoot,
|
|
@@ -3341,7 +4052,7 @@ const createCli = (options = {}) => {
|
|
|
3341
4052
|
path: existing.path
|
|
3342
4053
|
};
|
|
3343
4054
|
}
|
|
3344
|
-
const targetPath = branchToWorktreePath(repoRoot, branch);
|
|
4055
|
+
const targetPath = branchToWorktreePath(repoRoot, branch, resolvedConfig.paths.worktreeRoot);
|
|
3345
4056
|
await ensureTargetPathWritable(targetPath);
|
|
3346
4057
|
const hookContext = createHookContext({
|
|
3347
4058
|
runtime,
|
|
@@ -3366,7 +4077,10 @@ const createCli = (options = {}) => {
|
|
|
3366
4077
|
]
|
|
3367
4078
|
});
|
|
3368
4079
|
else {
|
|
3369
|
-
const baseBranch = await resolveBaseBranch(
|
|
4080
|
+
const baseBranch = await resolveBaseBranch({
|
|
4081
|
+
repoRoot,
|
|
4082
|
+
config: resolvedConfig
|
|
4083
|
+
});
|
|
3370
4084
|
lifecycleBaseBranch = baseBranch;
|
|
3371
4085
|
await runGitCommand({
|
|
3372
4086
|
cwd: repoRoot,
|
|
@@ -3449,7 +4163,7 @@ const createCli = (options = {}) => {
|
|
|
3449
4163
|
message: `Branch already exists locally: ${newBranch}`,
|
|
3450
4164
|
details: { branch: newBranch }
|
|
3451
4165
|
});
|
|
3452
|
-
const newPath = branchToWorktreePath(repoRoot, newBranch);
|
|
4166
|
+
const newPath = branchToWorktreePath(repoRoot, newBranch, resolvedConfig.paths.worktreeRoot);
|
|
3453
4167
|
await ensureTargetPathWritable(newPath);
|
|
3454
4168
|
const hookContext = createHookContext({
|
|
3455
4169
|
runtime,
|
|
@@ -3543,6 +4257,17 @@ const createCli = (options = {}) => {
|
|
|
3543
4257
|
message: "Cannot delete the primary worktree",
|
|
3544
4258
|
details: { path: target.path }
|
|
3545
4259
|
});
|
|
4260
|
+
if (isManagedWorktreePath({
|
|
4261
|
+
worktreePath: target.path,
|
|
4262
|
+
managedWorktreeRoot
|
|
4263
|
+
}) !== true) throw createCliError("WORKTREE_NOT_FOUND", {
|
|
4264
|
+
message: "Target branch is not in managed worktree root",
|
|
4265
|
+
details: {
|
|
4266
|
+
branch: target.branch,
|
|
4267
|
+
path: target.path,
|
|
4268
|
+
managedWorktreeRoot
|
|
4269
|
+
}
|
|
4270
|
+
});
|
|
3546
4271
|
validateDeleteSafety({
|
|
3547
4272
|
target,
|
|
3548
4273
|
forceFlags
|
|
@@ -3616,7 +4341,10 @@ const createCli = (options = {}) => {
|
|
|
3616
4341
|
if (parsedArgs.apply === true && parsedArgs.dryRun === true) throw createCliError("INVALID_ARGUMENT", { message: "Cannot use --apply and --dry-run together" });
|
|
3617
4342
|
const dryRun = parsedArgs.apply !== true;
|
|
3618
4343
|
const execute = async () => {
|
|
3619
|
-
const candidates = (await collectWorktreeSnapshot$1(repoRoot)).worktrees.filter((worktree) => worktree.branch !== null).filter((worktree) => worktree.path !== repoRoot).filter((worktree) =>
|
|
4344
|
+
const candidates = (await collectWorktreeSnapshot$1(repoRoot)).worktrees.filter((worktree) => worktree.branch !== null).filter((worktree) => worktree.path !== repoRoot).filter((worktree) => isManagedWorktreePath({
|
|
4345
|
+
worktreePath: worktree.path,
|
|
4346
|
+
managedWorktreeRoot
|
|
4347
|
+
})).filter((worktree) => worktree.dirty === false).filter((worktree) => worktree.locked.value === false).filter((worktree) => worktree.merged.overall === true).map((worktree) => worktree.branch);
|
|
3620
4348
|
if (dryRun) return {
|
|
3621
4349
|
deleted: [],
|
|
3622
4350
|
candidates,
|
|
@@ -3721,7 +4449,7 @@ const createCli = (options = {}) => {
|
|
|
3721
4449
|
repoRoot,
|
|
3722
4450
|
action: "get",
|
|
3723
4451
|
branch,
|
|
3724
|
-
worktreePath: branchToWorktreePath(repoRoot, branch),
|
|
4452
|
+
worktreePath: branchToWorktreePath(repoRoot, branch, resolvedConfig.paths.worktreeRoot),
|
|
3725
4453
|
stderr
|
|
3726
4454
|
});
|
|
3727
4455
|
await runPreHook({
|
|
@@ -3772,7 +4500,7 @@ const createCli = (options = {}) => {
|
|
|
3772
4500
|
path: existing.path
|
|
3773
4501
|
};
|
|
3774
4502
|
}
|
|
3775
|
-
const targetPath = branchToWorktreePath(repoRoot, branch);
|
|
4503
|
+
const targetPath = branchToWorktreePath(repoRoot, branch, resolvedConfig.paths.worktreeRoot);
|
|
3776
4504
|
await ensureTargetPathWritable(targetPath);
|
|
3777
4505
|
await runGitCommand({
|
|
3778
4506
|
cwd: repoRoot,
|
|
@@ -3846,12 +4574,15 @@ const createCli = (options = {}) => {
|
|
|
3846
4574
|
details: { path: sourceWorktree.path }
|
|
3847
4575
|
});
|
|
3848
4576
|
const branch = sourceWorktree.branch;
|
|
3849
|
-
const baseBranch = await resolveBaseBranch(
|
|
4577
|
+
const baseBranch = await resolveBaseBranch({
|
|
4578
|
+
repoRoot,
|
|
4579
|
+
config: resolvedConfig
|
|
4580
|
+
});
|
|
3850
4581
|
ensureBranchIsNotPrimary({
|
|
3851
4582
|
branch,
|
|
3852
4583
|
baseBranch
|
|
3853
4584
|
});
|
|
3854
|
-
const targetPath = branchToWorktreePath(repoRoot, branch);
|
|
4585
|
+
const targetPath = branchToWorktreePath(repoRoot, branch, resolvedConfig.paths.worktreeRoot);
|
|
3855
4586
|
await ensureTargetPathWritable(targetPath);
|
|
3856
4587
|
const dirty = (await runGitCommand({
|
|
3857
4588
|
cwd: repoRoot,
|
|
@@ -3972,6 +4703,7 @@ const createCli = (options = {}) => {
|
|
|
3972
4703
|
});
|
|
3973
4704
|
const sourceWorktree = resolveManagedNonPrimaryWorktreeByBranch({
|
|
3974
4705
|
repoRoot,
|
|
4706
|
+
managedWorktreeRoot,
|
|
3975
4707
|
branch,
|
|
3976
4708
|
worktrees: (await collectWorktreeSnapshot$1(repoRoot)).worktrees,
|
|
3977
4709
|
optionName: "--from",
|
|
@@ -4106,6 +4838,7 @@ const createCli = (options = {}) => {
|
|
|
4106
4838
|
});
|
|
4107
4839
|
const targetWorktree = resolveManagedNonPrimaryWorktreeByBranch({
|
|
4108
4840
|
repoRoot,
|
|
4841
|
+
managedWorktreeRoot,
|
|
4109
4842
|
branch,
|
|
4110
4843
|
worktrees: (await collectWorktreeSnapshot$1(repoRoot)).worktrees,
|
|
4111
4844
|
optionName: "--to",
|
|
@@ -4393,17 +5126,14 @@ const createCli = (options = {}) => {
|
|
|
4393
5126
|
min: 1,
|
|
4394
5127
|
max: Number.MAX_SAFE_INTEGER
|
|
4395
5128
|
});
|
|
4396
|
-
const
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
cwd: repoContext.currentWorktreeRoot,
|
|
4400
|
-
path: process.env.WT_WORKTREE_PATH ?? "."
|
|
4401
|
-
})
|
|
5129
|
+
const targetWorktreeRoot = resolveTargetWorktreeRootForCopyLink({
|
|
5130
|
+
repoContext,
|
|
5131
|
+
snapshot: await collectWorktreeSnapshot$1(repoRoot)
|
|
4402
5132
|
});
|
|
4403
5133
|
for (const relativePath of commandArgs) {
|
|
4404
5134
|
const { sourcePath, destinationPath } = resolveFileCopyTargets({
|
|
4405
5135
|
repoRoot,
|
|
4406
|
-
|
|
5136
|
+
targetWorktreeRoot,
|
|
4407
5137
|
relativePath
|
|
4408
5138
|
});
|
|
4409
5139
|
await access(sourcePath, constants.F_OK);
|
|
@@ -4422,7 +5152,7 @@ const createCli = (options = {}) => {
|
|
|
4422
5152
|
repoRoot,
|
|
4423
5153
|
details: {
|
|
4424
5154
|
copied: commandArgs,
|
|
4425
|
-
worktreePath
|
|
5155
|
+
worktreePath: targetWorktreeRoot
|
|
4426
5156
|
}
|
|
4427
5157
|
})));
|
|
4428
5158
|
return EXIT_CODE.OK;
|
|
@@ -4436,18 +5166,15 @@ const createCli = (options = {}) => {
|
|
|
4436
5166
|
min: 1,
|
|
4437
5167
|
max: Number.MAX_SAFE_INTEGER
|
|
4438
5168
|
});
|
|
4439
|
-
const
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
cwd: repoContext.currentWorktreeRoot,
|
|
4443
|
-
path: process.env.WT_WORKTREE_PATH ?? "."
|
|
4444
|
-
})
|
|
5169
|
+
const targetWorktreeRoot = resolveTargetWorktreeRootForCopyLink({
|
|
5170
|
+
repoContext,
|
|
5171
|
+
snapshot: await collectWorktreeSnapshot$1(repoRoot)
|
|
4445
5172
|
});
|
|
4446
5173
|
const fallbackEnabled = parsedArgs.fallback !== false;
|
|
4447
5174
|
for (const relativePath of commandArgs) {
|
|
4448
5175
|
const { sourcePath, destinationPath } = resolveFileCopyTargets({
|
|
4449
5176
|
repoRoot,
|
|
4450
|
-
|
|
5177
|
+
targetWorktreeRoot,
|
|
4451
5178
|
relativePath
|
|
4452
5179
|
});
|
|
4453
5180
|
await access(sourcePath, constants.F_OK);
|
|
@@ -4485,7 +5212,7 @@ const createCli = (options = {}) => {
|
|
|
4485
5212
|
repoRoot,
|
|
4486
5213
|
details: {
|
|
4487
5214
|
linked: commandArgs,
|
|
4488
|
-
worktreePath,
|
|
5215
|
+
worktreePath: targetWorktreeRoot,
|
|
4489
5216
|
fallback: fallbackEnabled
|
|
4490
5217
|
}
|
|
4491
5218
|
})));
|
|
@@ -4640,16 +5367,25 @@ const createCli = (options = {}) => {
|
|
|
4640
5367
|
}));
|
|
4641
5368
|
if (candidates.length === 0) throw createCliError("WORKTREE_NOT_FOUND", { message: "No worktree candidates found" });
|
|
4642
5369
|
const promptValue = readStringOption(parsedArgsRecord, "prompt");
|
|
5370
|
+
const prompt = typeof promptValue === "string" && promptValue.length > 0 ? promptValue : resolvedConfig.selector.cd.prompt;
|
|
5371
|
+
const cliFzfExtraArgs = collectOptionValues({
|
|
5372
|
+
args: beforeDoubleDash,
|
|
5373
|
+
optionNames: ["fzfArg", "fzf-arg"]
|
|
5374
|
+
});
|
|
5375
|
+
const mergedConfigFzfArgs = mergeFzfArgs({
|
|
5376
|
+
defaults: resolvedConfig.selector.cd.fzf.extraArgs,
|
|
5377
|
+
extras: cliFzfExtraArgs
|
|
5378
|
+
});
|
|
5379
|
+
const surface = resolvedConfig.selector.cd.surface;
|
|
4643
5380
|
const selection = await selectPathWithFzf$1({
|
|
4644
5381
|
candidates,
|
|
4645
|
-
prompt
|
|
5382
|
+
prompt,
|
|
4646
5383
|
fzfExtraArgs: mergeFzfArgs({
|
|
4647
5384
|
defaults: CD_FZF_EXTRA_ARGS,
|
|
4648
|
-
extras:
|
|
4649
|
-
args: beforeDoubleDash,
|
|
4650
|
-
optionNames: ["fzfArg", "fzf-arg"]
|
|
4651
|
-
})
|
|
5385
|
+
extras: mergedConfigFzfArgs
|
|
4652
5386
|
}),
|
|
5387
|
+
surface,
|
|
5388
|
+
tmuxPopupOpts: resolvedConfig.selector.cd.tmuxPopupOpts,
|
|
4653
5389
|
cwd: repoRoot,
|
|
4654
5390
|
isInteractive: () => runtime.isInteractive || process.stderr.isTTY === true
|
|
4655
5391
|
}).catch((error) => {
|