vde-worktree 0.0.19 → 0.0.21
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 +2019 -969
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
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,678 @@ 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 validateWorktreeRoot = async ({ repoRoot, config }) => {
|
|
754
|
+
const rawWorktreeRoot = config.paths.worktreeRoot;
|
|
755
|
+
const resolvedWorktreeRoot = isAbsolute(rawWorktreeRoot) ? resolve(rawWorktreeRoot) : resolve(repoRoot, rawWorktreeRoot);
|
|
756
|
+
try {
|
|
757
|
+
if ((await lstat(resolvedWorktreeRoot)).isDirectory() !== true) throwInvalidConfig({
|
|
758
|
+
file: "<resolved>",
|
|
759
|
+
keyPath: "paths.worktreeRoot",
|
|
760
|
+
reason: "must not point to an existing file"
|
|
761
|
+
});
|
|
762
|
+
} catch (error) {
|
|
763
|
+
if (error.code === "ENOENT") return;
|
|
764
|
+
throw error;
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
const parseConfigFile = async (file) => {
|
|
768
|
+
const rawContent = await readFile(file, "utf8");
|
|
769
|
+
let parsed;
|
|
770
|
+
try {
|
|
771
|
+
parsed = parse(rawContent);
|
|
772
|
+
} catch (error) {
|
|
773
|
+
throwInvalidConfig({
|
|
774
|
+
file,
|
|
775
|
+
keyPath: "<root>",
|
|
776
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
return validatePartialConfig({
|
|
780
|
+
rawConfig: parsed,
|
|
781
|
+
ctx: { file }
|
|
782
|
+
});
|
|
783
|
+
};
|
|
784
|
+
const cloneDefaultConfig = () => {
|
|
785
|
+
return mergeConfig(DEFAULT_CONFIG, {});
|
|
786
|
+
};
|
|
787
|
+
const loadResolvedConfig = async ({ cwd, repoRoot }) => {
|
|
788
|
+
const files = await resolveExistingConfigFiles({
|
|
789
|
+
cwd,
|
|
790
|
+
repoRoot
|
|
791
|
+
});
|
|
792
|
+
let config = cloneDefaultConfig();
|
|
793
|
+
for (const file of files) {
|
|
794
|
+
const partial = await parseConfigFile(file);
|
|
795
|
+
config = mergeConfig(config, partial);
|
|
796
|
+
}
|
|
797
|
+
await validateWorktreeRoot({
|
|
798
|
+
repoRoot,
|
|
799
|
+
config
|
|
800
|
+
});
|
|
801
|
+
return {
|
|
802
|
+
config,
|
|
803
|
+
loadedFiles: files
|
|
804
|
+
};
|
|
805
|
+
};
|
|
806
|
+
|
|
133
807
|
//#endregion
|
|
134
808
|
//#region src/git/exec.ts
|
|
135
809
|
const runGitCommand = async ({ cwd, args, reject = true }) => {
|
|
@@ -175,6 +849,7 @@ const doesGitRefExist = async (cwd, ref) => {
|
|
|
175
849
|
//#endregion
|
|
176
850
|
//#region src/core/paths.ts
|
|
177
851
|
const GIT_DIR_NAME = ".git";
|
|
852
|
+
const DEFAULT_WORKTREE_ROOT = ".worktree";
|
|
178
853
|
const WORKTREE_ID_HASH_LENGTH = 12;
|
|
179
854
|
const WORKTREE_ID_SLUG_MAX_LENGTH = 48;
|
|
180
855
|
const resolveRepoRootFromCommonDir = ({ currentWorktreeRoot, gitCommonDir }) => {
|
|
@@ -212,8 +887,9 @@ const resolveRepoContext = async (cwd) => {
|
|
|
212
887
|
gitCommonDir
|
|
213
888
|
};
|
|
214
889
|
};
|
|
215
|
-
const getWorktreeRootPath = (repoRoot) => {
|
|
216
|
-
return
|
|
890
|
+
const getWorktreeRootPath = (repoRoot, configuredWorktreeRoot = DEFAULT_WORKTREE_ROOT) => {
|
|
891
|
+
if (isAbsolute(configuredWorktreeRoot)) return resolve(configuredWorktreeRoot);
|
|
892
|
+
return resolve(repoRoot, configuredWorktreeRoot);
|
|
217
893
|
};
|
|
218
894
|
const getWorktreeMetaRootPath = (repoRoot) => {
|
|
219
895
|
return join(repoRoot, ".vde", "worktree");
|
|
@@ -233,25 +909,33 @@ const getStateDirectoryPath = (repoRoot) => {
|
|
|
233
909
|
const branchToWorktreeId = (branch) => {
|
|
234
910
|
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
911
|
};
|
|
236
|
-
const branchToWorktreePath = (repoRoot, branch) => {
|
|
237
|
-
const worktreeRoot = getWorktreeRootPath(repoRoot);
|
|
238
|
-
return
|
|
239
|
-
|
|
240
|
-
path: join(worktreeRoot, ...branch.split("/"))
|
|
912
|
+
const branchToWorktreePath = (repoRoot, branch, configuredWorktreeRoot = DEFAULT_WORKTREE_ROOT) => {
|
|
913
|
+
const worktreeRoot = getWorktreeRootPath(repoRoot, configuredWorktreeRoot);
|
|
914
|
+
return ensurePathInsideRoot({
|
|
915
|
+
rootPath: worktreeRoot,
|
|
916
|
+
path: join(worktreeRoot, ...branch.split("/")),
|
|
917
|
+
message: "Path is outside managed worktree root"
|
|
241
918
|
});
|
|
242
919
|
};
|
|
243
|
-
const
|
|
244
|
-
const rel = relative(
|
|
920
|
+
const ensurePathInsideRoot = ({ rootPath, path, message = "Path is outside allowed root" }) => {
|
|
921
|
+
const rel = relative(rootPath, path);
|
|
245
922
|
if (rel === "") return path;
|
|
246
923
|
if (rel === ".." || rel.startsWith(`..${sep}`)) throw createCliError("PATH_OUTSIDE_REPO", {
|
|
247
|
-
message
|
|
924
|
+
message,
|
|
248
925
|
details: {
|
|
249
|
-
|
|
926
|
+
rootPath,
|
|
250
927
|
path
|
|
251
928
|
}
|
|
252
929
|
});
|
|
253
930
|
return path;
|
|
254
931
|
};
|
|
932
|
+
const ensurePathInsideRepo = ({ repoRoot, path }) => {
|
|
933
|
+
return ensurePathInsideRoot({
|
|
934
|
+
rootPath: repoRoot,
|
|
935
|
+
path,
|
|
936
|
+
message: "Path is outside repository root"
|
|
937
|
+
});
|
|
938
|
+
};
|
|
255
939
|
const resolveRepoRelativePath = ({ repoRoot, relativePath }) => {
|
|
256
940
|
if (isAbsolute(relativePath)) throw createCliError("ABSOLUTE_PATH_NOT_ALLOWED", {
|
|
257
941
|
message: "Absolute path is not allowed",
|
|
@@ -266,6 +950,11 @@ const resolvePathFromCwd = ({ cwd, path }) => {
|
|
|
266
950
|
if (isAbsolute(path)) return path;
|
|
267
951
|
return resolve(cwd, path);
|
|
268
952
|
};
|
|
953
|
+
const isManagedWorktreePath = ({ worktreePath, managedWorktreeRoot }) => {
|
|
954
|
+
const rel = relative(managedWorktreeRoot, worktreePath);
|
|
955
|
+
if (rel === "" || rel === "." || rel === "..") return false;
|
|
956
|
+
return rel.startsWith(`..${sep}`) !== true;
|
|
957
|
+
};
|
|
269
958
|
|
|
270
959
|
//#endregion
|
|
271
960
|
//#region src/core/hooks.ts
|
|
@@ -287,11 +976,10 @@ const appendHookLog = async ({ repoRoot, action, branch, content }) => {
|
|
|
287
976
|
branch
|
|
288
977
|
})), content, "utf8");
|
|
289
978
|
};
|
|
290
|
-
const
|
|
291
|
-
if (context.enabled !== true) return;
|
|
292
|
-
const path = hookPath(context.repoRoot, hookName);
|
|
979
|
+
const ensureHookExists = async ({ path, hookName, requireExists }) => {
|
|
293
980
|
try {
|
|
294
981
|
await access(path, constants.F_OK);
|
|
982
|
+
return true;
|
|
295
983
|
} catch {
|
|
296
984
|
if (requireExists) throw createCliError("HOOK_NOT_FOUND", {
|
|
297
985
|
message: `Hook not found: ${hookName}`,
|
|
@@ -300,8 +988,10 @@ const runHook = async ({ phase, hookName, args, context, requireExists = false }
|
|
|
300
988
|
path
|
|
301
989
|
}
|
|
302
990
|
});
|
|
303
|
-
return;
|
|
991
|
+
return false;
|
|
304
992
|
}
|
|
993
|
+
};
|
|
994
|
+
const ensureHookExecutable = async ({ path, hookName }) => {
|
|
305
995
|
try {
|
|
306
996
|
await access(path, constants.X_OK);
|
|
307
997
|
} catch {
|
|
@@ -313,43 +1003,102 @@ const runHook = async ({ phase, hookName, args, context, requireExists = false }
|
|
|
313
1003
|
}
|
|
314
1004
|
});
|
|
315
1005
|
}
|
|
1006
|
+
};
|
|
1007
|
+
const executeHookProcess = async ({ path, args, context }) => {
|
|
316
1008
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
env
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
1009
|
+
const result = await execa(path, [...args], {
|
|
1010
|
+
cwd: context.worktreePath ?? context.repoRoot,
|
|
1011
|
+
env: {
|
|
1012
|
+
...process.env,
|
|
1013
|
+
WT_REPO_ROOT: context.repoRoot,
|
|
1014
|
+
WT_ACTION: context.action,
|
|
1015
|
+
WT_BRANCH: context.branch ?? "",
|
|
1016
|
+
WT_WORKTREE_PATH: context.worktreePath ?? "",
|
|
1017
|
+
WT_IS_TTY: process.stdout.isTTY === true ? "1" : "0",
|
|
1018
|
+
WT_TOOL: "vde-worktree",
|
|
1019
|
+
...context.extraEnv ?? {}
|
|
1020
|
+
},
|
|
1021
|
+
timeout: context.timeoutMs ?? DEFAULT_HOOK_TIMEOUT_MS,
|
|
1022
|
+
reject: false
|
|
1023
|
+
});
|
|
1024
|
+
const endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1025
|
+
return {
|
|
1026
|
+
exitCode: result.exitCode ?? 0,
|
|
1027
|
+
stderr: result.stderr ?? "",
|
|
1028
|
+
timedOut: result.timedOut === true,
|
|
1029
|
+
startedAt,
|
|
1030
|
+
endedAt
|
|
1031
|
+
};
|
|
1032
|
+
};
|
|
1033
|
+
const writeHookLog = async ({ repoRoot, action, branch, hookName, phase, result }) => {
|
|
1034
|
+
await appendHookLog({
|
|
1035
|
+
repoRoot,
|
|
1036
|
+
action,
|
|
1037
|
+
branch,
|
|
1038
|
+
content: [
|
|
335
1039
|
`hook=${hookName}`,
|
|
336
1040
|
`phase=${phase}`,
|
|
337
|
-
`start=${startedAt}`,
|
|
338
|
-
`end=${endedAt}`,
|
|
339
|
-
`exitCode=${String(result.exitCode
|
|
340
|
-
`
|
|
1041
|
+
`start=${result.startedAt}`,
|
|
1042
|
+
`end=${result.endedAt}`,
|
|
1043
|
+
`exitCode=${String(result.exitCode)}`,
|
|
1044
|
+
`timedOut=${result.timedOut ? "1" : "0"}`,
|
|
1045
|
+
`stderr=${result.stderr}`,
|
|
341
1046
|
""
|
|
342
|
-
].join("\n")
|
|
343
|
-
|
|
1047
|
+
].join("\n")
|
|
1048
|
+
});
|
|
1049
|
+
};
|
|
1050
|
+
const shouldIgnorePostHookFailure = ({ phase, context }) => {
|
|
1051
|
+
return phase === "post" && context.strictPostHooks !== true;
|
|
1052
|
+
};
|
|
1053
|
+
const handleIgnoredPostHookFailure = ({ context, hookName, message }) => {
|
|
1054
|
+
context.stderr(message ?? `Hook failed: ${hookName}`);
|
|
1055
|
+
};
|
|
1056
|
+
const runHook = async ({ phase, hookName, args, context, requireExists = false }) => {
|
|
1057
|
+
if (context.enabled !== true) return;
|
|
1058
|
+
const path = hookPath(context.repoRoot, hookName);
|
|
1059
|
+
if (await ensureHookExists({
|
|
1060
|
+
path,
|
|
1061
|
+
hookName,
|
|
1062
|
+
requireExists
|
|
1063
|
+
}) !== true) return;
|
|
1064
|
+
await ensureHookExecutable({
|
|
1065
|
+
path,
|
|
1066
|
+
hookName
|
|
1067
|
+
});
|
|
1068
|
+
try {
|
|
1069
|
+
const result = await executeHookProcess({
|
|
1070
|
+
path,
|
|
1071
|
+
args,
|
|
1072
|
+
context
|
|
1073
|
+
});
|
|
1074
|
+
await writeHookLog({
|
|
344
1075
|
repoRoot: context.repoRoot,
|
|
345
1076
|
action: context.action,
|
|
346
1077
|
branch: context.branch,
|
|
347
|
-
|
|
1078
|
+
hookName,
|
|
1079
|
+
phase,
|
|
1080
|
+
result
|
|
348
1081
|
});
|
|
349
|
-
if (
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
1082
|
+
if (result.timedOut) throw createCliError("HOOK_TIMEOUT", {
|
|
1083
|
+
message: `Hook timed out: ${hookName}`,
|
|
1084
|
+
details: {
|
|
1085
|
+
hook: hookName,
|
|
1086
|
+
timeoutMs: context.timeoutMs ?? DEFAULT_HOOK_TIMEOUT_MS,
|
|
1087
|
+
exitCode: result.exitCode,
|
|
1088
|
+
stderr: result.stderr
|
|
1089
|
+
}
|
|
1090
|
+
});
|
|
1091
|
+
if (result.exitCode === 0) return;
|
|
1092
|
+
const message = `Hook failed: ${hookName} (exitCode=${String(result.exitCode)})`;
|
|
1093
|
+
if (shouldIgnorePostHookFailure({
|
|
1094
|
+
phase,
|
|
1095
|
+
context
|
|
1096
|
+
})) {
|
|
1097
|
+
handleIgnoredPostHookFailure({
|
|
1098
|
+
context,
|
|
1099
|
+
hookName,
|
|
1100
|
+
message
|
|
1101
|
+
});
|
|
353
1102
|
return;
|
|
354
1103
|
}
|
|
355
1104
|
throw createCliError("HOOK_FAILED", {
|
|
@@ -372,8 +1121,14 @@ const runHook = async ({ phase, hookName, args, context, requireExists = false }
|
|
|
372
1121
|
},
|
|
373
1122
|
cause: error
|
|
374
1123
|
});
|
|
375
|
-
if (
|
|
376
|
-
|
|
1124
|
+
if (shouldIgnorePostHookFailure({
|
|
1125
|
+
phase,
|
|
1126
|
+
context
|
|
1127
|
+
})) {
|
|
1128
|
+
handleIgnoredPostHookFailure({
|
|
1129
|
+
context,
|
|
1130
|
+
hookName
|
|
1131
|
+
});
|
|
377
1132
|
return;
|
|
378
1133
|
}
|
|
379
1134
|
throw createCliError("HOOK_FAILED", {
|
|
@@ -414,7 +1169,7 @@ const invokeHook = async ({ hookName, args, context }) => {
|
|
|
414
1169
|
|
|
415
1170
|
//#endregion
|
|
416
1171
|
//#region src/core/init.ts
|
|
417
|
-
const
|
|
1172
|
+
const EXCLUDE_MARKER = "# vde-worktree (managed)";
|
|
418
1173
|
const DEFAULT_HOOKS = [{
|
|
419
1174
|
name: "post-new",
|
|
420
1175
|
lines: [
|
|
@@ -448,7 +1203,27 @@ const createHookTemplate = async (hooksDir, name, lines) => {
|
|
|
448
1203
|
await chmod(targetPath, 493);
|
|
449
1204
|
}
|
|
450
1205
|
};
|
|
451
|
-
const
|
|
1206
|
+
const isPathInsideOrEqual = ({ rootPath, candidatePath }) => {
|
|
1207
|
+
const rel = relative(rootPath, candidatePath);
|
|
1208
|
+
if (rel.length === 0) return true;
|
|
1209
|
+
return rel !== ".." && rel.startsWith(`..${sep}`) !== true;
|
|
1210
|
+
};
|
|
1211
|
+
const toExcludeEntry = ({ repoRoot, managedWorktreeRoot }) => {
|
|
1212
|
+
if (isPathInsideOrEqual({
|
|
1213
|
+
rootPath: repoRoot,
|
|
1214
|
+
candidatePath: managedWorktreeRoot
|
|
1215
|
+
}) !== true) return null;
|
|
1216
|
+
const rel = relative(repoRoot, managedWorktreeRoot).split(sep).join("/");
|
|
1217
|
+
const normalized = rel.length === 0 ? "." : rel;
|
|
1218
|
+
return normalized.endsWith("/") ? normalized : `${normalized}/`;
|
|
1219
|
+
};
|
|
1220
|
+
const ensureExcludeBlock = async ({ repoRoot, managedWorktreeRoot }) => {
|
|
1221
|
+
const managedEntry = toExcludeEntry({
|
|
1222
|
+
repoRoot,
|
|
1223
|
+
managedWorktreeRoot
|
|
1224
|
+
});
|
|
1225
|
+
if (managedEntry === null) return;
|
|
1226
|
+
const managedExcludeBlock = `${EXCLUDE_MARKER}\n${managedEntry}\n.vde/worktree/\n`;
|
|
452
1227
|
const excludePath = join(repoRoot, ".git", "info", "exclude");
|
|
453
1228
|
let current = "";
|
|
454
1229
|
try {
|
|
@@ -456,8 +1231,8 @@ const ensureExcludeBlock = async (repoRoot) => {
|
|
|
456
1231
|
} catch {
|
|
457
1232
|
current = "";
|
|
458
1233
|
}
|
|
459
|
-
if (current.includes(
|
|
460
|
-
await writeFile(excludePath, `${current.endsWith("\n") || current.length === 0 ? current : `${current}\n`}${
|
|
1234
|
+
if (current.includes(managedExcludeBlock)) return;
|
|
1235
|
+
await writeFile(excludePath, `${current.endsWith("\n") || current.length === 0 ? current : `${current}\n`}${managedExcludeBlock}`, "utf8");
|
|
461
1236
|
};
|
|
462
1237
|
const isInitialized = async (repoRoot) => {
|
|
463
1238
|
try {
|
|
@@ -467,18 +1242,94 @@ const isInitialized = async (repoRoot) => {
|
|
|
467
1242
|
return false;
|
|
468
1243
|
}
|
|
469
1244
|
};
|
|
470
|
-
const initializeRepository = async (repoRoot) => {
|
|
1245
|
+
const initializeRepository = async ({ repoRoot, managedWorktreeRoot }) => {
|
|
471
1246
|
const wasInitialized = await isInitialized(repoRoot);
|
|
472
|
-
await mkdir(
|
|
1247
|
+
await mkdir(managedWorktreeRoot, { recursive: true });
|
|
473
1248
|
await mkdir(getHooksDirectoryPath(repoRoot), { recursive: true });
|
|
474
1249
|
await mkdir(getLogsDirectoryPath(repoRoot), { recursive: true });
|
|
475
1250
|
await mkdir(getLocksDirectoryPath(repoRoot), { recursive: true });
|
|
476
1251
|
await mkdir(getStateDirectoryPath(repoRoot), { recursive: true });
|
|
477
|
-
await ensureExcludeBlock(
|
|
1252
|
+
await ensureExcludeBlock({
|
|
1253
|
+
repoRoot,
|
|
1254
|
+
managedWorktreeRoot
|
|
1255
|
+
});
|
|
478
1256
|
for (const hook of DEFAULT_HOOKS) await createHookTemplate(getHooksDirectoryPath(repoRoot), hook.name, hook.lines);
|
|
479
1257
|
return { alreadyInitialized: wasInitialized };
|
|
480
1258
|
};
|
|
481
1259
|
|
|
1260
|
+
//#endregion
|
|
1261
|
+
//#region src/core/json-storage.ts
|
|
1262
|
+
const parseJsonRecord = ({ content, schemaVersion, validate }) => {
|
|
1263
|
+
try {
|
|
1264
|
+
const parsed = JSON.parse(content);
|
|
1265
|
+
if (parsed.schemaVersion !== schemaVersion || validate(parsed) !== true) return {
|
|
1266
|
+
valid: false,
|
|
1267
|
+
record: null
|
|
1268
|
+
};
|
|
1269
|
+
return {
|
|
1270
|
+
valid: true,
|
|
1271
|
+
record: parsed
|
|
1272
|
+
};
|
|
1273
|
+
} catch {
|
|
1274
|
+
return {
|
|
1275
|
+
valid: false,
|
|
1276
|
+
record: null
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
};
|
|
1280
|
+
const readJsonRecord = async ({ path, schemaVersion, validate }) => {
|
|
1281
|
+
try {
|
|
1282
|
+
return {
|
|
1283
|
+
path,
|
|
1284
|
+
exists: true,
|
|
1285
|
+
...parseJsonRecord({
|
|
1286
|
+
content: await readFile(path, "utf8"),
|
|
1287
|
+
schemaVersion,
|
|
1288
|
+
validate
|
|
1289
|
+
})
|
|
1290
|
+
};
|
|
1291
|
+
} catch (error) {
|
|
1292
|
+
if (error.code === "ENOENT") return {
|
|
1293
|
+
path,
|
|
1294
|
+
exists: false,
|
|
1295
|
+
valid: true,
|
|
1296
|
+
record: null
|
|
1297
|
+
};
|
|
1298
|
+
return {
|
|
1299
|
+
path,
|
|
1300
|
+
exists: true,
|
|
1301
|
+
valid: false,
|
|
1302
|
+
record: null
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
1306
|
+
const writeJsonAtomically = async ({ filePath, payload, ensureDir = false }) => {
|
|
1307
|
+
if (ensureDir) await mkdir(dirname(filePath), { recursive: true });
|
|
1308
|
+
const tmpPath = `${filePath}.tmp-${String(process.pid)}-${String(Date.now())}`;
|
|
1309
|
+
try {
|
|
1310
|
+
await writeFile(tmpPath, `${JSON.stringify(payload)}\n`, "utf8");
|
|
1311
|
+
await rename(tmpPath, filePath);
|
|
1312
|
+
} catch (error) {
|
|
1313
|
+
try {
|
|
1314
|
+
await rm(tmpPath, { force: true });
|
|
1315
|
+
} catch {}
|
|
1316
|
+
throw error;
|
|
1317
|
+
}
|
|
1318
|
+
};
|
|
1319
|
+
const writeJsonExclusively = async ({ path, payload }) => {
|
|
1320
|
+
let handle;
|
|
1321
|
+
try {
|
|
1322
|
+
handle = await open(path, "wx");
|
|
1323
|
+
await handle.writeFile(`${JSON.stringify(payload)}\n`, "utf8");
|
|
1324
|
+
return true;
|
|
1325
|
+
} catch (error) {
|
|
1326
|
+
if (error.code === "EEXIST") return false;
|
|
1327
|
+
throw error;
|
|
1328
|
+
} finally {
|
|
1329
|
+
if (handle !== void 0) await handle.close();
|
|
1330
|
+
}
|
|
1331
|
+
};
|
|
1332
|
+
|
|
482
1333
|
//#endregion
|
|
483
1334
|
//#region src/core/repo-lock.ts
|
|
484
1335
|
const sleep = async (ms) => {
|
|
@@ -496,16 +1347,8 @@ const isProcessAlive = (pid) => {
|
|
|
496
1347
|
return true;
|
|
497
1348
|
}
|
|
498
1349
|
};
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
const parsed = JSON.parse(content);
|
|
502
|
-
if (parsed.schemaVersion !== 1) return null;
|
|
503
|
-
if (typeof parsed.command !== "string" || typeof parsed.owner !== "string") return null;
|
|
504
|
-
if (typeof parsed.pid !== "number" || typeof parsed.host !== "string" || typeof parsed.startedAt !== "string") return null;
|
|
505
|
-
return parsed;
|
|
506
|
-
} catch {
|
|
507
|
-
return null;
|
|
508
|
-
}
|
|
1350
|
+
const isRepoLockFileSchema = (parsed) => {
|
|
1351
|
+
return typeof parsed.owner === "string" && typeof parsed.command === "string" && typeof parsed.pid === "number" && typeof parsed.host === "string" && typeof parsed.startedAt === "string";
|
|
509
1352
|
};
|
|
510
1353
|
const lockFilePath$1 = async (repoRoot) => {
|
|
511
1354
|
const stateDir = getStateDirectoryPath(repoRoot);
|
|
@@ -534,23 +1377,15 @@ const canRecoverStaleLock = ({ lock, staleLockTTLSeconds }) => {
|
|
|
534
1377
|
if (lock.host === hostname() && isProcessAlive(lock.pid)) return false;
|
|
535
1378
|
return true;
|
|
536
1379
|
};
|
|
537
|
-
const writeNewLockFile = async (path, payload) => {
|
|
538
|
-
try {
|
|
539
|
-
const handle = await open(path, "wx");
|
|
540
|
-
await handle.writeFile(`${JSON.stringify(payload)}\n`, "utf8");
|
|
541
|
-
await handle.close();
|
|
542
|
-
return true;
|
|
543
|
-
} catch (error) {
|
|
544
|
-
if (error.code === "EEXIST") return false;
|
|
545
|
-
throw error;
|
|
546
|
-
}
|
|
547
|
-
};
|
|
548
1380
|
const acquireRepoLock = async ({ repoRoot, command, timeoutMs = DEFAULT_LOCK_TIMEOUT_MS, staleLockTTLSeconds = DEFAULT_STALE_LOCK_TTL_SECONDS }) => {
|
|
549
1381
|
const path = await lockFilePath$1(repoRoot);
|
|
550
1382
|
const startAt = Date.now();
|
|
551
1383
|
const payload = buildLockPayload(command);
|
|
552
1384
|
while (Date.now() - startAt <= timeoutMs) {
|
|
553
|
-
if (await
|
|
1385
|
+
if (await writeJsonExclusively({
|
|
1386
|
+
path,
|
|
1387
|
+
payload
|
|
1388
|
+
})) return { release: async () => {
|
|
554
1389
|
try {
|
|
555
1390
|
await rm(path, { force: true });
|
|
556
1391
|
} catch {
|
|
@@ -565,7 +1400,11 @@ const acquireRepoLock = async ({ repoRoot, command, timeoutMs = DEFAULT_LOCK_TIM
|
|
|
565
1400
|
continue;
|
|
566
1401
|
}
|
|
567
1402
|
if (canRecoverStaleLock({
|
|
568
|
-
lock:
|
|
1403
|
+
lock: parseJsonRecord({
|
|
1404
|
+
content: lockContent,
|
|
1405
|
+
schemaVersion: 1,
|
|
1406
|
+
validate: isRepoLockFileSchema
|
|
1407
|
+
}).record,
|
|
569
1408
|
staleLockTTLSeconds
|
|
570
1409
|
})) {
|
|
571
1410
|
try {
|
|
@@ -614,57 +1453,16 @@ const hasStateDirectory = async (repoRoot) => {
|
|
|
614
1453
|
return false;
|
|
615
1454
|
}
|
|
616
1455
|
};
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
const isLastDivergedHeadValid = parsed.lastDivergedHead === null || typeof parsed.lastDivergedHead === "string" && parsed.lastDivergedHead.length > 0;
|
|
621
|
-
if (parsed.schemaVersion !== 2 || typeof parsed.branch !== "string" || typeof parsed.worktreeId !== "string" || typeof parsed.baseBranch !== "string" || typeof parsed.everDiverged !== "boolean" || isLastDivergedHeadValid !== true || typeof parsed.createdAt !== "string" || typeof parsed.updatedAt !== "string") return {
|
|
622
|
-
valid: false,
|
|
623
|
-
record: null
|
|
624
|
-
};
|
|
625
|
-
return {
|
|
626
|
-
valid: true,
|
|
627
|
-
record: parsed
|
|
628
|
-
};
|
|
629
|
-
} catch {
|
|
630
|
-
return {
|
|
631
|
-
valid: false,
|
|
632
|
-
record: null
|
|
633
|
-
};
|
|
634
|
-
}
|
|
635
|
-
};
|
|
636
|
-
const writeJsonAtomically$1 = async ({ filePath, payload }) => {
|
|
637
|
-
await mkdir(dirname(filePath), { recursive: true });
|
|
638
|
-
const tmpPath = `${filePath}.tmp-${String(process.pid)}-${String(Date.now())}`;
|
|
639
|
-
await writeFile(tmpPath, `${JSON.stringify(payload)}\n`, "utf8");
|
|
640
|
-
await rename(tmpPath, filePath);
|
|
1456
|
+
const isWorktreeMergeLifecycleRecord = (parsed) => {
|
|
1457
|
+
const isLastDivergedHeadValid = parsed.lastDivergedHead === null || typeof parsed.lastDivergedHead === "string" && parsed.lastDivergedHead.length > 0;
|
|
1458
|
+
return typeof parsed.branch === "string" && typeof parsed.worktreeId === "string" && typeof parsed.baseBranch === "string" && typeof parsed.everDiverged === "boolean" && isLastDivergedHeadValid && typeof parsed.createdAt === "string" && typeof parsed.updatedAt === "string";
|
|
641
1459
|
};
|
|
642
1460
|
const readWorktreeMergeLifecycle = async ({ repoRoot, branch }) => {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
path,
|
|
649
|
-
exists: false,
|
|
650
|
-
valid: true,
|
|
651
|
-
record: null
|
|
652
|
-
};
|
|
653
|
-
}
|
|
654
|
-
try {
|
|
655
|
-
return {
|
|
656
|
-
path,
|
|
657
|
-
exists: true,
|
|
658
|
-
...parseLifecycle(await readFile(path, "utf8"))
|
|
659
|
-
};
|
|
660
|
-
} catch {
|
|
661
|
-
return {
|
|
662
|
-
path,
|
|
663
|
-
exists: true,
|
|
664
|
-
valid: false,
|
|
665
|
-
record: null
|
|
666
|
-
};
|
|
667
|
-
}
|
|
1461
|
+
return readJsonRecord({
|
|
1462
|
+
path: lifecycleFilePath(repoRoot, branch),
|
|
1463
|
+
schemaVersion: 2,
|
|
1464
|
+
validate: isWorktreeMergeLifecycleRecord
|
|
1465
|
+
});
|
|
668
1466
|
};
|
|
669
1467
|
const upsertWorktreeMergeLifecycle = async ({ repoRoot, branch, baseBranch, observedDivergedHead }) => {
|
|
670
1468
|
const normalizedObservedHead = typeof observedDivergedHead === "string" && observedDivergedHead.length > 0 ? observedDivergedHead : null;
|
|
@@ -699,9 +1497,10 @@ const upsertWorktreeMergeLifecycle = async ({ repoRoot, branch, baseBranch, obse
|
|
|
699
1497
|
createdAt: current.record?.createdAt ?? now,
|
|
700
1498
|
updatedAt: now
|
|
701
1499
|
};
|
|
702
|
-
await writeJsonAtomically
|
|
1500
|
+
await writeJsonAtomically({
|
|
703
1501
|
filePath: current.path,
|
|
704
|
-
payload: next
|
|
1502
|
+
payload: next,
|
|
1503
|
+
ensureDir: true
|
|
705
1504
|
});
|
|
706
1505
|
return next;
|
|
707
1506
|
};
|
|
@@ -738,71 +1537,32 @@ const moveWorktreeMergeLifecycle = async ({ repoRoot, fromBranch, toBranch, base
|
|
|
738
1537
|
createdAt: source.record?.createdAt ?? now,
|
|
739
1538
|
updatedAt: now
|
|
740
1539
|
};
|
|
741
|
-
await writeJsonAtomically
|
|
742
|
-
filePath: targetPath,
|
|
743
|
-
payload: next
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
const deleteWorktreeMergeLifecycle = async ({ repoRoot, branch }) => {
|
|
749
|
-
await rm(lifecycleFilePath(repoRoot, branch), { force: true });
|
|
750
|
-
};
|
|
751
|
-
|
|
752
|
-
//#endregion
|
|
753
|
-
//#region src/core/worktree-lock.ts
|
|
754
|
-
const parseLock = (content) => {
|
|
755
|
-
try {
|
|
756
|
-
const parsed = JSON.parse(content);
|
|
757
|
-
if (parsed.schemaVersion !== 1 || typeof parsed.branch !== "string" || typeof parsed.worktreeId !== "string" || typeof parsed.reason !== "string" || typeof parsed.owner !== "string" || typeof parsed.host !== "string" || typeof parsed.pid !== "number" || typeof parsed.createdAt !== "string" || typeof parsed.updatedAt !== "string") return {
|
|
758
|
-
valid: false,
|
|
759
|
-
record: null
|
|
760
|
-
};
|
|
761
|
-
return {
|
|
762
|
-
valid: true,
|
|
763
|
-
record: parsed
|
|
764
|
-
};
|
|
765
|
-
} catch {
|
|
766
|
-
return {
|
|
767
|
-
valid: false,
|
|
768
|
-
record: null
|
|
769
|
-
};
|
|
770
|
-
}
|
|
1540
|
+
await writeJsonAtomically({
|
|
1541
|
+
filePath: targetPath,
|
|
1542
|
+
payload: next,
|
|
1543
|
+
ensureDir: true
|
|
1544
|
+
});
|
|
1545
|
+
if (source.path !== targetPath) await rm(source.path, { force: true });
|
|
1546
|
+
return next;
|
|
771
1547
|
};
|
|
772
|
-
const
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
1548
|
+
const deleteWorktreeMergeLifecycle = async ({ repoRoot, branch }) => {
|
|
1549
|
+
await rm(lifecycleFilePath(repoRoot, branch), { force: true });
|
|
1550
|
+
};
|
|
1551
|
+
|
|
1552
|
+
//#endregion
|
|
1553
|
+
//#region src/core/worktree-lock.ts
|
|
1554
|
+
const isWorktreeLockRecord = (parsed) => {
|
|
1555
|
+
return parsed.schemaVersion === 1 && typeof parsed.branch === "string" && typeof parsed.worktreeId === "string" && typeof parsed.reason === "string" && typeof parsed.owner === "string" && typeof parsed.host === "string" && typeof parsed.pid === "number" && typeof parsed.createdAt === "string" && typeof parsed.updatedAt === "string";
|
|
776
1556
|
};
|
|
777
1557
|
const lockFilePath = (repoRoot, branch) => {
|
|
778
1558
|
return join(getLocksDirectoryPath(repoRoot), `${branchToWorktreeId(branch)}.json`);
|
|
779
1559
|
};
|
|
780
1560
|
const readWorktreeLock = async ({ repoRoot, branch }) => {
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
path,
|
|
787
|
-
exists: false,
|
|
788
|
-
valid: true,
|
|
789
|
-
record: null
|
|
790
|
-
};
|
|
791
|
-
}
|
|
792
|
-
try {
|
|
793
|
-
return {
|
|
794
|
-
path,
|
|
795
|
-
exists: true,
|
|
796
|
-
...parseLock(await readFile(path, "utf8"))
|
|
797
|
-
};
|
|
798
|
-
} catch {
|
|
799
|
-
return {
|
|
800
|
-
path,
|
|
801
|
-
exists: true,
|
|
802
|
-
valid: false,
|
|
803
|
-
record: null
|
|
804
|
-
};
|
|
805
|
-
}
|
|
1561
|
+
return readJsonRecord({
|
|
1562
|
+
path: lockFilePath(repoRoot, branch),
|
|
1563
|
+
schemaVersion: 1,
|
|
1564
|
+
validate: isWorktreeLockRecord
|
|
1565
|
+
});
|
|
806
1566
|
};
|
|
807
1567
|
const upsertWorktreeLock = async ({ repoRoot, branch, reason, owner }) => {
|
|
808
1568
|
const { path, record } = await readWorktreeLock({
|
|
@@ -833,16 +1593,40 @@ const deleteWorktreeLock = async ({ repoRoot, branch }) => {
|
|
|
833
1593
|
|
|
834
1594
|
//#endregion
|
|
835
1595
|
//#region src/integrations/gh.ts
|
|
1596
|
+
var GhUnavailableError = class extends Error {
|
|
1597
|
+
code = "GH_UNAVAILABLE";
|
|
1598
|
+
constructor(message = "gh command is unavailable") {
|
|
1599
|
+
super(message);
|
|
1600
|
+
this.name = "GhUnavailableError";
|
|
1601
|
+
}
|
|
1602
|
+
};
|
|
1603
|
+
var GhCommandError = class extends Error {
|
|
1604
|
+
code = "GH_COMMAND_FAILED";
|
|
1605
|
+
details;
|
|
1606
|
+
constructor({ exitCode, stderr }) {
|
|
1607
|
+
super(`gh command failed with exitCode=${String(exitCode)}`);
|
|
1608
|
+
this.name = "GhCommandError";
|
|
1609
|
+
this.details = {
|
|
1610
|
+
exitCode,
|
|
1611
|
+
stderr
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
};
|
|
836
1615
|
const defaultRunGh = async ({ cwd, args }) => {
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
1616
|
+
try {
|
|
1617
|
+
const result = await execa("gh", [...args], {
|
|
1618
|
+
cwd,
|
|
1619
|
+
reject: false
|
|
1620
|
+
});
|
|
1621
|
+
return {
|
|
1622
|
+
exitCode: result.exitCode ?? 0,
|
|
1623
|
+
stdout: result.stdout,
|
|
1624
|
+
stderr: result.stderr
|
|
1625
|
+
};
|
|
1626
|
+
} catch (error) {
|
|
1627
|
+
if (error.code === "ENOENT") throw new GhUnavailableError("gh command not found");
|
|
1628
|
+
throw error;
|
|
1629
|
+
}
|
|
846
1630
|
};
|
|
847
1631
|
const toTargetBranches = ({ branches, baseBranch }) => {
|
|
848
1632
|
const uniqueBranches = /* @__PURE__ */ new Set();
|
|
@@ -944,7 +1728,10 @@ const resolvePrStateByBranchBatch = async ({ repoRoot, baseBranch, branches, ena
|
|
|
944
1728
|
"headRefName,state,mergedAt,updatedAt,url"
|
|
945
1729
|
]
|
|
946
1730
|
});
|
|
947
|
-
if (result.exitCode !== 0)
|
|
1731
|
+
if (result.exitCode !== 0) throw new GhCommandError({
|
|
1732
|
+
exitCode: result.exitCode,
|
|
1733
|
+
stderr: result.stderr
|
|
1734
|
+
});
|
|
948
1735
|
const prStatusByBranch = parsePrStateByBranch({
|
|
949
1736
|
raw: result.stdout,
|
|
950
1737
|
targetBranches
|
|
@@ -952,6 +1739,7 @@ const resolvePrStateByBranchBatch = async ({ repoRoot, baseBranch, branches, ena
|
|
|
952
1739
|
if (prStatusByBranch === null) return buildUnknownPrStateMap(targetBranches);
|
|
953
1740
|
return prStatusByBranch;
|
|
954
1741
|
} catch (error) {
|
|
1742
|
+
if (error instanceof GhUnavailableError || error instanceof GhCommandError) return buildUnknownPrStateMap(targetBranches);
|
|
955
1743
|
if (error.code === "ENOENT") return buildUnknownPrStateMap(targetBranches);
|
|
956
1744
|
return buildUnknownPrStateMap(targetBranches);
|
|
957
1745
|
}
|
|
@@ -1018,35 +1806,8 @@ const listGitWorktrees = async (repoRoot) => {
|
|
|
1018
1806
|
|
|
1019
1807
|
//#endregion
|
|
1020
1808
|
//#region src/core/worktree-state.ts
|
|
1021
|
-
const
|
|
1022
|
-
|
|
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;
|
|
1809
|
+
const isLockPayload = (parsed) => {
|
|
1810
|
+
return typeof parsed.branch === "string" && typeof parsed.worktreeId === "string" && typeof parsed.reason === "string" && parsed.reason.length > 0 && (typeof parsed.owner === "undefined" || typeof parsed.owner === "string");
|
|
1050
1811
|
};
|
|
1051
1812
|
const resolveDirty = async (worktreePath) => {
|
|
1052
1813
|
return (await runGitCommand({
|
|
@@ -1055,16 +1816,6 @@ const resolveDirty = async (worktreePath) => {
|
|
|
1055
1816
|
reject: false
|
|
1056
1817
|
})).stdout.trim().length > 0;
|
|
1057
1818
|
};
|
|
1058
|
-
const parseLockPayload = (content) => {
|
|
1059
|
-
try {
|
|
1060
|
-
const parsed = JSON.parse(content);
|
|
1061
|
-
if (parsed.schemaVersion !== 1) return null;
|
|
1062
|
-
if (typeof parsed.branch !== "string" || typeof parsed.worktreeId !== "string" || typeof parsed.reason !== "string" || parsed.reason.length === 0) return null;
|
|
1063
|
-
return parsed;
|
|
1064
|
-
} catch {
|
|
1065
|
-
return null;
|
|
1066
|
-
}
|
|
1067
|
-
};
|
|
1068
1819
|
const resolveLockState = async ({ repoRoot, branch }) => {
|
|
1069
1820
|
if (branch === null) return {
|
|
1070
1821
|
value: false,
|
|
@@ -1072,38 +1823,62 @@ const resolveLockState = async ({ repoRoot, branch }) => {
|
|
|
1072
1823
|
owner: null
|
|
1073
1824
|
};
|
|
1074
1825
|
const id = branchToWorktreeId(branch);
|
|
1075
|
-
const
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
owner: typeof lock.owner === "string" && lock.owner.length > 0 ? lock.owner : null
|
|
1096
|
-
};
|
|
1097
|
-
} catch {
|
|
1098
|
-
return {
|
|
1099
|
-
value: true,
|
|
1100
|
-
reason: "invalid lock metadata",
|
|
1101
|
-
owner: null
|
|
1102
|
-
};
|
|
1103
|
-
}
|
|
1826
|
+
const lock = await readJsonRecord({
|
|
1827
|
+
path: join(getLocksDirectoryPath(repoRoot), `${id}.json`),
|
|
1828
|
+
schemaVersion: 1,
|
|
1829
|
+
validate: isLockPayload
|
|
1830
|
+
});
|
|
1831
|
+
if (lock.exists !== true) return {
|
|
1832
|
+
value: false,
|
|
1833
|
+
reason: null,
|
|
1834
|
+
owner: null
|
|
1835
|
+
};
|
|
1836
|
+
if (lock.valid !== true || lock.record === null) return {
|
|
1837
|
+
value: true,
|
|
1838
|
+
reason: "invalid lock metadata",
|
|
1839
|
+
owner: null
|
|
1840
|
+
};
|
|
1841
|
+
return {
|
|
1842
|
+
value: true,
|
|
1843
|
+
reason: lock.record.reason,
|
|
1844
|
+
owner: typeof lock.record.owner === "string" && lock.record.owner.length > 0 ? lock.record.owner : null
|
|
1845
|
+
};
|
|
1104
1846
|
};
|
|
1105
1847
|
const WORK_REFLOG_MESSAGE_PATTERN = /^(commit(?: \([^)]*\))?|cherry-pick|revert|rebase \(pick\)|merge):/;
|
|
1106
|
-
const
|
|
1848
|
+
const resolveAncestryFromExitCode = (exitCode) => {
|
|
1849
|
+
if (exitCode === 0) return true;
|
|
1850
|
+
if (exitCode === 1) return false;
|
|
1851
|
+
return null;
|
|
1852
|
+
};
|
|
1853
|
+
const resolveMergedByPr = ({ branch, baseBranch, prStateByBranch }) => {
|
|
1854
|
+
const prStatus = branch === baseBranch ? null : prStateByBranch.get(branch)?.status ?? null;
|
|
1855
|
+
if (prStatus === "merged") return true;
|
|
1856
|
+
if (prStatus === "none" || prStatus === "open" || prStatus === "closed_unmerged") return false;
|
|
1857
|
+
return null;
|
|
1858
|
+
};
|
|
1859
|
+
const hasLifecycleDivergedHead = (lifecycle) => {
|
|
1860
|
+
return lifecycle.everDiverged === true && lifecycle.lastDivergedHead !== null;
|
|
1861
|
+
};
|
|
1862
|
+
const parseWorkReflogHeads = (reflogOutput) => {
|
|
1863
|
+
const heads = [];
|
|
1864
|
+
let latestHead = null;
|
|
1865
|
+
for (const line of reflogOutput.split("\n")) {
|
|
1866
|
+
const trimmed = line.trim();
|
|
1867
|
+
if (trimmed.length === 0) continue;
|
|
1868
|
+
const separatorIndex = trimmed.indexOf(" ");
|
|
1869
|
+
if (separatorIndex <= 0) continue;
|
|
1870
|
+
const head = trimmed.slice(0, separatorIndex).trim();
|
|
1871
|
+
const message = trimmed.slice(separatorIndex + 1).trim();
|
|
1872
|
+
if (head.length === 0 || WORK_REFLOG_MESSAGE_PATTERN.test(message) !== true) continue;
|
|
1873
|
+
if (latestHead === null) latestHead = head;
|
|
1874
|
+
heads.push(head);
|
|
1875
|
+
}
|
|
1876
|
+
return {
|
|
1877
|
+
heads,
|
|
1878
|
+
latestHead
|
|
1879
|
+
};
|
|
1880
|
+
};
|
|
1881
|
+
const probeLifecycleFromReflog = async ({ repoRoot, branch, baseBranch }) => {
|
|
1107
1882
|
const reflog = await runGitCommand({
|
|
1108
1883
|
cwd: repoRoot,
|
|
1109
1884
|
args: [
|
|
@@ -1118,17 +1893,13 @@ const resolveLifecycleFromReflog = async ({ repoRoot, branch, baseBranch }) => {
|
|
|
1118
1893
|
merged: null,
|
|
1119
1894
|
divergedHead: null
|
|
1120
1895
|
};
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
const
|
|
1128
|
-
const message = trimmed.slice(separatorIndex + 1).trim();
|
|
1129
|
-
if (head.length === 0 || WORK_REFLOG_MESSAGE_PATTERN.test(message) !== true) continue;
|
|
1130
|
-
if (latestWorkHead === null) latestWorkHead = head;
|
|
1131
|
-
const result = await runGitCommand({
|
|
1896
|
+
const parsedHeads = parseWorkReflogHeads(reflog.stdout);
|
|
1897
|
+
if (parsedHeads.heads.length === 0) return {
|
|
1898
|
+
merged: null,
|
|
1899
|
+
divergedHead: null
|
|
1900
|
+
};
|
|
1901
|
+
for (const head of parsedHeads.heads) {
|
|
1902
|
+
const merged = resolveAncestryFromExitCode((await runGitCommand({
|
|
1132
1903
|
cwd: repoRoot,
|
|
1133
1904
|
args: [
|
|
1134
1905
|
"merge-base",
|
|
@@ -1137,19 +1908,52 @@ const resolveLifecycleFromReflog = async ({ repoRoot, branch, baseBranch }) => {
|
|
|
1137
1908
|
baseBranch
|
|
1138
1909
|
],
|
|
1139
1910
|
reject: false
|
|
1140
|
-
});
|
|
1141
|
-
if (
|
|
1911
|
+
})).exitCode);
|
|
1912
|
+
if (merged === true) return {
|
|
1142
1913
|
merged: true,
|
|
1143
1914
|
divergedHead: head
|
|
1144
1915
|
};
|
|
1145
|
-
if (
|
|
1916
|
+
if (merged === null) return {
|
|
1146
1917
|
merged: null,
|
|
1147
|
-
divergedHead:
|
|
1918
|
+
divergedHead: parsedHeads.latestHead
|
|
1148
1919
|
};
|
|
1149
1920
|
}
|
|
1150
1921
|
return {
|
|
1151
1922
|
merged: false,
|
|
1152
|
-
divergedHead:
|
|
1923
|
+
divergedHead: parsedHeads.latestHead
|
|
1924
|
+
};
|
|
1925
|
+
};
|
|
1926
|
+
const createMergeLifecycleRepository = ({ repoRoot }) => {
|
|
1927
|
+
return { upsert: async ({ branch, baseBranch, observedDivergedHead }) => {
|
|
1928
|
+
return upsertWorktreeMergeLifecycle({
|
|
1929
|
+
repoRoot,
|
|
1930
|
+
branch,
|
|
1931
|
+
baseBranch,
|
|
1932
|
+
observedDivergedHead
|
|
1933
|
+
});
|
|
1934
|
+
} };
|
|
1935
|
+
};
|
|
1936
|
+
const createMergeProbeRepository = ({ repoRoot }) => {
|
|
1937
|
+
return {
|
|
1938
|
+
probeAncestry: async ({ branch, baseBranch }) => {
|
|
1939
|
+
return resolveAncestryFromExitCode((await runGitCommand({
|
|
1940
|
+
cwd: repoRoot,
|
|
1941
|
+
args: [
|
|
1942
|
+
"merge-base",
|
|
1943
|
+
"--is-ancestor",
|
|
1944
|
+
branch,
|
|
1945
|
+
baseBranch
|
|
1946
|
+
],
|
|
1947
|
+
reject: false
|
|
1948
|
+
})).exitCode);
|
|
1949
|
+
},
|
|
1950
|
+
probeLifecycleFromReflog: async ({ branch, baseBranch }) => {
|
|
1951
|
+
return probeLifecycleFromReflog({
|
|
1952
|
+
repoRoot,
|
|
1953
|
+
branch,
|
|
1954
|
+
baseBranch
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1153
1957
|
};
|
|
1154
1958
|
};
|
|
1155
1959
|
const resolveMergedState = async ({ repoRoot, branch, head, baseBranch, prStateByBranch }) => {
|
|
@@ -1158,64 +1962,42 @@ const resolveMergedState = async ({ repoRoot, branch, head, baseBranch, prStateB
|
|
|
1158
1962
|
byPR: null,
|
|
1159
1963
|
overall: null
|
|
1160
1964
|
};
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
});
|
|
1173
|
-
if (result.exitCode === 0) byAncestry = true;
|
|
1174
|
-
else if (result.exitCode === 1) byAncestry = false;
|
|
1175
|
-
}
|
|
1176
|
-
const prStatus = branch === baseBranch ? null : prStateByBranch.get(branch)?.status ?? null;
|
|
1177
|
-
let byPR = null;
|
|
1178
|
-
if (prStatus === "merged") byPR = true;
|
|
1179
|
-
else if (prStatus === "none" || prStatus === "open" || prStatus === "closed_unmerged") byPR = false;
|
|
1965
|
+
const mergeProbeRepository = createMergeProbeRepository({ repoRoot });
|
|
1966
|
+
const mergeLifecycleRepository = createMergeLifecycleRepository({ repoRoot });
|
|
1967
|
+
const byAncestry = baseBranch === null ? null : await mergeProbeRepository.probeAncestry({
|
|
1968
|
+
branch,
|
|
1969
|
+
baseBranch
|
|
1970
|
+
});
|
|
1971
|
+
const byPR = resolveMergedByPr({
|
|
1972
|
+
branch,
|
|
1973
|
+
baseBranch,
|
|
1974
|
+
prStateByBranch
|
|
1975
|
+
});
|
|
1180
1976
|
let byLifecycle = null;
|
|
1181
1977
|
if (baseBranch !== null) {
|
|
1182
|
-
const lifecycle = await
|
|
1183
|
-
repoRoot,
|
|
1978
|
+
const lifecycle = await mergeLifecycleRepository.upsert({
|
|
1184
1979
|
branch,
|
|
1185
1980
|
baseBranch,
|
|
1186
1981
|
observedDivergedHead: byAncestry === false ? head : null
|
|
1187
1982
|
});
|
|
1188
1983
|
if (byAncestry === false) byLifecycle = false;
|
|
1189
|
-
else if (byAncestry === true) if (lifecycle
|
|
1984
|
+
else if (byAncestry === true) if (hasLifecycleDivergedHead(lifecycle)) byLifecycle = await mergeProbeRepository.probeAncestry({
|
|
1985
|
+
branch: lifecycle.lastDivergedHead,
|
|
1986
|
+
baseBranch
|
|
1987
|
+
});
|
|
1988
|
+
else if (byPR === true) byLifecycle = null;
|
|
1190
1989
|
else {
|
|
1191
|
-
const probe = await
|
|
1192
|
-
repoRoot,
|
|
1990
|
+
const probe = await mergeProbeRepository.probeLifecycleFromReflog({
|
|
1193
1991
|
branch,
|
|
1194
1992
|
baseBranch
|
|
1195
1993
|
});
|
|
1196
1994
|
byLifecycle = probe.merged;
|
|
1197
|
-
if (probe.divergedHead !== null) await
|
|
1198
|
-
repoRoot,
|
|
1995
|
+
if (probe.divergedHead !== null) await mergeLifecycleRepository.upsert({
|
|
1199
1996
|
branch,
|
|
1200
1997
|
baseBranch,
|
|
1201
1998
|
observedDivergedHead: probe.divergedHead
|
|
1202
1999
|
});
|
|
1203
2000
|
}
|
|
1204
|
-
else {
|
|
1205
|
-
const lifecycleResult = await runGitCommand({
|
|
1206
|
-
cwd: repoRoot,
|
|
1207
|
-
args: [
|
|
1208
|
-
"merge-base",
|
|
1209
|
-
"--is-ancestor",
|
|
1210
|
-
lifecycle.lastDivergedHead,
|
|
1211
|
-
baseBranch
|
|
1212
|
-
],
|
|
1213
|
-
reject: false
|
|
1214
|
-
});
|
|
1215
|
-
if (lifecycleResult.exitCode === 0) byLifecycle = true;
|
|
1216
|
-
else if (lifecycleResult.exitCode === 1) byLifecycle = false;
|
|
1217
|
-
else byLifecycle = null;
|
|
1218
|
-
}
|
|
1219
2001
|
}
|
|
1220
2002
|
return {
|
|
1221
2003
|
byAncestry,
|
|
@@ -1316,17 +2098,13 @@ const enrichWorktree = async ({ repoRoot, worktree, baseBranch, prStateByBranch
|
|
|
1316
2098
|
upstream
|
|
1317
2099
|
};
|
|
1318
2100
|
};
|
|
1319
|
-
const collectWorktreeSnapshot = async (repoRoot, { noGh = false } = {}) => {
|
|
1320
|
-
const
|
|
1321
|
-
resolveBaseBranch$1(repoRoot),
|
|
1322
|
-
listGitWorktrees(repoRoot),
|
|
1323
|
-
resolveEnableGh(repoRoot)
|
|
1324
|
-
]);
|
|
2101
|
+
const collectWorktreeSnapshot = async (repoRoot, { baseBranch = null, ghEnabled = true, noGh = false } = {}) => {
|
|
2102
|
+
const worktrees = await listGitWorktrees(repoRoot);
|
|
1325
2103
|
const prStateByBranch = await resolvePrStateByBranchBatch({
|
|
1326
2104
|
repoRoot,
|
|
1327
2105
|
baseBranch,
|
|
1328
2106
|
branches: worktrees.map((worktree) => worktree.branch),
|
|
1329
|
-
enabled:
|
|
2107
|
+
enabled: ghEnabled && noGh !== true
|
|
1330
2108
|
});
|
|
1331
2109
|
return {
|
|
1332
2110
|
repoRoot,
|
|
@@ -1350,10 +2128,55 @@ const RESERVED_FZF_ARGS = new Set([
|
|
|
1350
2128
|
"prompt",
|
|
1351
2129
|
"layout",
|
|
1352
2130
|
"height",
|
|
1353
|
-
"border"
|
|
2131
|
+
"border",
|
|
2132
|
+
"tmux"
|
|
1354
2133
|
]);
|
|
1355
2134
|
const ANSI_ESCAPE_SEQUENCE_PATTERN = String.raw`\u001B\[[0-?]*[ -/]*[@-~]`;
|
|
1356
2135
|
const ANSI_ESCAPE_SEQUENCE_REGEX = new RegExp(ANSI_ESCAPE_SEQUENCE_PATTERN, "g");
|
|
2136
|
+
var FzfError = class extends Error {
|
|
2137
|
+
code;
|
|
2138
|
+
constructor(options) {
|
|
2139
|
+
super(options.message);
|
|
2140
|
+
this.name = "FzfError";
|
|
2141
|
+
this.code = options.code;
|
|
2142
|
+
}
|
|
2143
|
+
};
|
|
2144
|
+
var FzfDependencyError = class extends FzfError {
|
|
2145
|
+
constructor(message = "fzf is required for interactive selection") {
|
|
2146
|
+
super({
|
|
2147
|
+
code: "FZF_DEPENDENCY_MISSING",
|
|
2148
|
+
message
|
|
2149
|
+
});
|
|
2150
|
+
this.name = "FzfDependencyError";
|
|
2151
|
+
}
|
|
2152
|
+
};
|
|
2153
|
+
var FzfInteractiveRequiredError = class extends FzfError {
|
|
2154
|
+
constructor(message = "fzf selection requires an interactive terminal") {
|
|
2155
|
+
super({
|
|
2156
|
+
code: "FZF_INTERACTIVE_REQUIRED",
|
|
2157
|
+
message
|
|
2158
|
+
});
|
|
2159
|
+
this.name = "FzfInteractiveRequiredError";
|
|
2160
|
+
}
|
|
2161
|
+
};
|
|
2162
|
+
var FzfInvalidArgumentError = class extends FzfError {
|
|
2163
|
+
constructor(message) {
|
|
2164
|
+
super({
|
|
2165
|
+
code: "FZF_INVALID_ARGUMENT",
|
|
2166
|
+
message
|
|
2167
|
+
});
|
|
2168
|
+
this.name = "FzfInvalidArgumentError";
|
|
2169
|
+
}
|
|
2170
|
+
};
|
|
2171
|
+
var FzfInvalidSelectionError = class extends FzfError {
|
|
2172
|
+
constructor(message) {
|
|
2173
|
+
super({
|
|
2174
|
+
code: "FZF_INVALID_SELECTION",
|
|
2175
|
+
message
|
|
2176
|
+
});
|
|
2177
|
+
this.name = "FzfInvalidSelectionError";
|
|
2178
|
+
}
|
|
2179
|
+
};
|
|
1357
2180
|
const sanitizeCandidate = (value) => value.replace(/[\r\n]+/g, " ").trim();
|
|
1358
2181
|
const stripAnsi = (value) => value.replace(ANSI_ESCAPE_SEQUENCE_REGEX, "");
|
|
1359
2182
|
const stripTrailingNewlines = (value) => value.replace(/[\r\n]+$/g, "");
|
|
@@ -1362,12 +2185,12 @@ const buildFzfInput = (candidates) => {
|
|
|
1362
2185
|
};
|
|
1363
2186
|
const validateExtraFzfArgs = (fzfExtraArgs) => {
|
|
1364
2187
|
for (const arg of fzfExtraArgs) {
|
|
1365
|
-
if (typeof arg !== "string" || arg.length === 0) throw new
|
|
2188
|
+
if (typeof arg !== "string" || arg.length === 0) throw new FzfInvalidArgumentError("Empty value is not allowed for --fzf-arg");
|
|
1366
2189
|
if (!arg.startsWith("--")) continue;
|
|
1367
2190
|
const withoutPrefix = arg.slice(2);
|
|
1368
2191
|
if (withoutPrefix.length === 0) continue;
|
|
1369
2192
|
const optionName = withoutPrefix.split("=")[0];
|
|
1370
|
-
if (optionName !== void 0 && RESERVED_FZF_ARGS.has(optionName)) throw new
|
|
2193
|
+
if (optionName !== void 0 && RESERVED_FZF_ARGS.has(optionName)) throw new FzfInvalidArgumentError(`--fzf-arg cannot override reserved fzf option: --${optionName}`);
|
|
1371
2194
|
}
|
|
1372
2195
|
};
|
|
1373
2196
|
const buildFzfArgs = ({ prompt, fzfExtraArgs }) => {
|
|
@@ -1390,6 +2213,13 @@ const defaultCheckFzfAvailability = async () => {
|
|
|
1390
2213
|
throw error;
|
|
1391
2214
|
}
|
|
1392
2215
|
};
|
|
2216
|
+
const defaultCheckFzfTmuxSupport = async () => {
|
|
2217
|
+
try {
|
|
2218
|
+
return (await execa(FZF_BINARY, ["--help"], { timeout: FZF_CHECK_TIMEOUT_MS })).stdout.includes("--tmux");
|
|
2219
|
+
} catch {
|
|
2220
|
+
return false;
|
|
2221
|
+
}
|
|
2222
|
+
};
|
|
1393
2223
|
const defaultRunFzf = async ({ args, input, cwd, env }) => {
|
|
1394
2224
|
return { stdout: (await execa(FZF_BINARY, args, {
|
|
1395
2225
|
input,
|
|
@@ -1400,33 +2230,68 @@ const defaultRunFzf = async ({ args, input, cwd, env }) => {
|
|
|
1400
2230
|
};
|
|
1401
2231
|
const ensureFzfAvailable = async (checkFzfAvailability) => {
|
|
1402
2232
|
if (await checkFzfAvailability()) return;
|
|
1403
|
-
throw new
|
|
2233
|
+
throw new FzfDependencyError();
|
|
2234
|
+
};
|
|
2235
|
+
const shouldTryTmuxPopup = async ({ surface, env, checkFzfTmuxSupport }) => {
|
|
2236
|
+
if (surface === "inline") return false;
|
|
2237
|
+
if (surface === "tmux-popup") return true;
|
|
2238
|
+
if (typeof env.TMUX !== "string" || env.TMUX.length === 0) return false;
|
|
2239
|
+
try {
|
|
2240
|
+
return await checkFzfTmuxSupport();
|
|
2241
|
+
} catch {
|
|
2242
|
+
return false;
|
|
2243
|
+
}
|
|
1404
2244
|
};
|
|
1405
|
-
const
|
|
1406
|
-
|
|
1407
|
-
|
|
2245
|
+
const isTmuxUnknownOptionError = (error) => {
|
|
2246
|
+
const execaError = error;
|
|
2247
|
+
const text = [
|
|
2248
|
+
execaError.message,
|
|
2249
|
+
execaError.shortMessage,
|
|
2250
|
+
execaError.stderr,
|
|
2251
|
+
execaError.stdout
|
|
2252
|
+
].filter((value) => typeof value === "string" && value.length > 0).join("\n");
|
|
2253
|
+
return /unknown option.*--tmux|--tmux.*unknown option/i.test(text);
|
|
2254
|
+
};
|
|
2255
|
+
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 }) => {
|
|
2256
|
+
if (candidates.length === 0) throw new FzfInvalidArgumentError("No candidates provided for fzf selection");
|
|
2257
|
+
if (isInteractive() !== true) throw new FzfInteractiveRequiredError();
|
|
1408
2258
|
await ensureFzfAvailable(checkFzfAvailability);
|
|
1409
|
-
const
|
|
2259
|
+
const baseArgs = buildFzfArgs({
|
|
1410
2260
|
prompt,
|
|
1411
2261
|
fzfExtraArgs
|
|
1412
2262
|
});
|
|
2263
|
+
const tryTmuxPopup = await shouldTryTmuxPopup({
|
|
2264
|
+
surface,
|
|
2265
|
+
env,
|
|
2266
|
+
checkFzfTmuxSupport
|
|
2267
|
+
});
|
|
2268
|
+
const args = tryTmuxPopup ? [...baseArgs, `--tmux=${tmuxPopupOpts}`] : baseArgs;
|
|
1413
2269
|
const input = buildFzfInput(candidates);
|
|
1414
|
-
if (input.length === 0) throw new
|
|
2270
|
+
if (input.length === 0) throw new FzfInvalidArgumentError("All candidates are empty after sanitization");
|
|
1415
2271
|
const candidateSet = new Set(input.split("\n").map((candidate) => stripAnsi(candidate)));
|
|
1416
|
-
|
|
2272
|
+
const runWithValidation = async (fzfArgs) => {
|
|
1417
2273
|
const selectedPath = stripAnsi(stripTrailingNewlines((await runFzf({
|
|
1418
|
-
args,
|
|
2274
|
+
args: fzfArgs,
|
|
1419
2275
|
input,
|
|
1420
2276
|
cwd,
|
|
1421
2277
|
env
|
|
1422
2278
|
})).stdout));
|
|
1423
2279
|
if (selectedPath.length === 0) return { status: "cancelled" };
|
|
1424
|
-
if (!candidateSet.has(selectedPath)) throw new
|
|
2280
|
+
if (!candidateSet.has(selectedPath)) throw new FzfInvalidSelectionError("fzf returned a value that is not in the candidate list");
|
|
1425
2281
|
return {
|
|
1426
2282
|
status: "selected",
|
|
1427
2283
|
path: selectedPath
|
|
1428
2284
|
};
|
|
2285
|
+
};
|
|
2286
|
+
try {
|
|
2287
|
+
return await runWithValidation(args);
|
|
1429
2288
|
} catch (error) {
|
|
2289
|
+
if (tryTmuxPopup && isTmuxUnknownOptionError(error)) try {
|
|
2290
|
+
return await runWithValidation(baseArgs);
|
|
2291
|
+
} catch (fallbackError) {
|
|
2292
|
+
if (fallbackError.exitCode === 130) return { status: "cancelled" };
|
|
2293
|
+
throw fallbackError;
|
|
2294
|
+
}
|
|
1430
2295
|
if (error.exitCode === 130) return { status: "cancelled" };
|
|
1431
2296
|
throw error;
|
|
1432
2297
|
}
|
|
@@ -1483,6 +2348,157 @@ const createLogger = (options = {}) => {
|
|
|
1483
2348
|
return build(prefix, level);
|
|
1484
2349
|
};
|
|
1485
2350
|
|
|
2351
|
+
//#endregion
|
|
2352
|
+
//#region src/cli/commands/handler-groups.ts
|
|
2353
|
+
const createHandlerMap = (entries) => {
|
|
2354
|
+
return new Map(entries);
|
|
2355
|
+
};
|
|
2356
|
+
const dispatchCommandHandler = async ({ command, handlers }) => {
|
|
2357
|
+
const handler = handlers.get(command);
|
|
2358
|
+
if (handler === void 0) return;
|
|
2359
|
+
return await handler();
|
|
2360
|
+
};
|
|
2361
|
+
const createEarlyRepoCommandHandlers = ({ initHandler, listHandler, statusHandler, pathHandler }) => {
|
|
2362
|
+
return createHandlerMap([
|
|
2363
|
+
["init", initHandler],
|
|
2364
|
+
["list", listHandler],
|
|
2365
|
+
["status", statusHandler],
|
|
2366
|
+
["path", pathHandler]
|
|
2367
|
+
]);
|
|
2368
|
+
};
|
|
2369
|
+
const createWriteCommandHandlers = ({ newHandler, switchHandler }) => {
|
|
2370
|
+
return createHandlerMap([["new", newHandler], ["switch", switchHandler]]);
|
|
2371
|
+
};
|
|
2372
|
+
const createWriteMutationHandlers = ({ mvHandler, delHandler }) => {
|
|
2373
|
+
return createHandlerMap([["mv", mvHandler], ["del", delHandler]]);
|
|
2374
|
+
};
|
|
2375
|
+
const createWorktreeActionHandlers = ({ goneHandler, getHandler, extractHandler }) => {
|
|
2376
|
+
return createHandlerMap([
|
|
2377
|
+
["gone", goneHandler],
|
|
2378
|
+
["get", getHandler],
|
|
2379
|
+
["extract", extractHandler]
|
|
2380
|
+
]);
|
|
2381
|
+
};
|
|
2382
|
+
const createSynchronizationHandlers = ({ absorbHandler, unabsorbHandler, useHandler }) => {
|
|
2383
|
+
return createHandlerMap([
|
|
2384
|
+
["absorb", absorbHandler],
|
|
2385
|
+
["unabsorb", unabsorbHandler],
|
|
2386
|
+
["use", useHandler]
|
|
2387
|
+
]);
|
|
2388
|
+
};
|
|
2389
|
+
const createMiscCommandHandlers = ({ execHandler, invokeHandler, copyHandler, linkHandler, lockHandler, unlockHandler, cdHandler }) => {
|
|
2390
|
+
return createHandlerMap([
|
|
2391
|
+
["exec", execHandler],
|
|
2392
|
+
["invoke", invokeHandler],
|
|
2393
|
+
["copy", copyHandler],
|
|
2394
|
+
["link", linkHandler],
|
|
2395
|
+
["lock", lockHandler],
|
|
2396
|
+
["unlock", unlockHandler],
|
|
2397
|
+
["cd", cdHandler]
|
|
2398
|
+
]);
|
|
2399
|
+
};
|
|
2400
|
+
|
|
2401
|
+
//#endregion
|
|
2402
|
+
//#region src/cli/commands/read/dispatcher.ts
|
|
2403
|
+
const handled = (exitCode) => {
|
|
2404
|
+
return {
|
|
2405
|
+
handled: true,
|
|
2406
|
+
exitCode
|
|
2407
|
+
};
|
|
2408
|
+
};
|
|
2409
|
+
const NOT_HANDLED = { handled: false };
|
|
2410
|
+
const dispatchReadOnlyCommands = async (input) => {
|
|
2411
|
+
if (input.parsedArgs.help === true) {
|
|
2412
|
+
const commandHelpTarget = input.command !== "unknown" && input.command !== "help" ? input.command : null;
|
|
2413
|
+
if (commandHelpTarget !== null) {
|
|
2414
|
+
const entry = input.findCommandHelp(commandHelpTarget);
|
|
2415
|
+
if (entry !== void 0) {
|
|
2416
|
+
input.stdout(`${input.renderCommandHelpText({ entry })}\n`);
|
|
2417
|
+
return handled(EXIT_CODE.OK);
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
input.stdout(`${input.renderGeneralHelpText({ version: input.version })}\n`);
|
|
2421
|
+
return handled(EXIT_CODE.OK);
|
|
2422
|
+
}
|
|
2423
|
+
if (input.parsedArgs.version === true) {
|
|
2424
|
+
input.stdout(input.version);
|
|
2425
|
+
return handled(EXIT_CODE.OK);
|
|
2426
|
+
}
|
|
2427
|
+
if (input.positionals.length === 0) {
|
|
2428
|
+
input.stdout(`${input.renderGeneralHelpText({ version: input.version })}\n`);
|
|
2429
|
+
return handled(EXIT_CODE.OK);
|
|
2430
|
+
}
|
|
2431
|
+
if (input.command === "help") {
|
|
2432
|
+
const helpTarget = input.positionals[1];
|
|
2433
|
+
if (typeof helpTarget !== "string" || helpTarget.length === 0) {
|
|
2434
|
+
input.stdout(`${input.renderGeneralHelpText({ version: input.version })}\n`);
|
|
2435
|
+
return handled(EXIT_CODE.OK);
|
|
2436
|
+
}
|
|
2437
|
+
const entry = input.findCommandHelp(helpTarget);
|
|
2438
|
+
if (entry === void 0) throw createCliError("INVALID_ARGUMENT", {
|
|
2439
|
+
message: `Unknown command for help: ${helpTarget}`,
|
|
2440
|
+
details: {
|
|
2441
|
+
requested: helpTarget,
|
|
2442
|
+
availableCommands: input.availableCommandNames
|
|
2443
|
+
}
|
|
2444
|
+
});
|
|
2445
|
+
input.stdout(`${input.renderCommandHelpText({ entry })}\n`);
|
|
2446
|
+
return handled(EXIT_CODE.OK);
|
|
2447
|
+
}
|
|
2448
|
+
if (input.command === "completion") {
|
|
2449
|
+
input.ensureArgumentCount({
|
|
2450
|
+
command: input.command,
|
|
2451
|
+
args: input.commandArgs,
|
|
2452
|
+
min: 1,
|
|
2453
|
+
max: 1
|
|
2454
|
+
});
|
|
2455
|
+
const shell = input.resolveCompletionShell(input.commandArgs[0]);
|
|
2456
|
+
const script = await input.loadCompletionScript(shell);
|
|
2457
|
+
if (input.parsedArgs.install === true) {
|
|
2458
|
+
const destinationPath = input.resolveCompletionInstallPath({
|
|
2459
|
+
shell,
|
|
2460
|
+
requestedPath: input.readStringOption(input.parsedArgs, "path")
|
|
2461
|
+
});
|
|
2462
|
+
await input.installCompletionScript({
|
|
2463
|
+
content: script,
|
|
2464
|
+
destinationPath
|
|
2465
|
+
});
|
|
2466
|
+
if (input.jsonEnabled) {
|
|
2467
|
+
input.stdout(JSON.stringify(input.buildJsonSuccess({
|
|
2468
|
+
command: input.command,
|
|
2469
|
+
status: "ok",
|
|
2470
|
+
repoRoot: null,
|
|
2471
|
+
details: {
|
|
2472
|
+
shell,
|
|
2473
|
+
installed: true,
|
|
2474
|
+
path: destinationPath
|
|
2475
|
+
}
|
|
2476
|
+
})));
|
|
2477
|
+
return handled(EXIT_CODE.OK);
|
|
2478
|
+
}
|
|
2479
|
+
input.stdout(`installed completion: ${destinationPath}`);
|
|
2480
|
+
if (shell === "zsh") input.stdout("zsh note: ensure completion path is in fpath, then run: autoload -Uz compinit && compinit");
|
|
2481
|
+
return handled(EXIT_CODE.OK);
|
|
2482
|
+
}
|
|
2483
|
+
if (input.jsonEnabled) {
|
|
2484
|
+
input.stdout(JSON.stringify(input.buildJsonSuccess({
|
|
2485
|
+
command: input.command,
|
|
2486
|
+
status: "ok",
|
|
2487
|
+
repoRoot: null,
|
|
2488
|
+
details: {
|
|
2489
|
+
shell,
|
|
2490
|
+
installed: false,
|
|
2491
|
+
script
|
|
2492
|
+
}
|
|
2493
|
+
})));
|
|
2494
|
+
return handled(EXIT_CODE.OK);
|
|
2495
|
+
}
|
|
2496
|
+
input.stdout(script);
|
|
2497
|
+
return handled(EXIT_CODE.OK);
|
|
2498
|
+
}
|
|
2499
|
+
return NOT_HANDLED;
|
|
2500
|
+
};
|
|
2501
|
+
|
|
1486
2502
|
//#endregion
|
|
1487
2503
|
//#region src/cli/package-version.ts
|
|
1488
2504
|
const CANDIDATE_PATHS = ["../package.json", "../../package.json"];
|
|
@@ -1514,9 +2530,7 @@ const CD_FZF_EXTRA_ARGS = [
|
|
|
1514
2530
|
"--preview-window=right,60%,wrap",
|
|
1515
2531
|
"--ansi"
|
|
1516
2532
|
];
|
|
1517
|
-
const
|
|
1518
|
-
const LIST_TABLE_PATH_COLUMN_INDEX = 7;
|
|
1519
|
-
const LIST_TABLE_PATH_MIN_WIDTH = 12;
|
|
2533
|
+
const DEFAULT_LIST_TABLE_COLUMNS = [...LIST_TABLE_COLUMNS];
|
|
1520
2534
|
const LIST_TABLE_CELL_HORIZONTAL_PADDING = 2;
|
|
1521
2535
|
const COMPLETION_SHELLS = ["zsh", "fish"];
|
|
1522
2536
|
const COMPLETION_FILE_BY_SHELL = {
|
|
@@ -1538,6 +2552,10 @@ const CATPPUCCIN_MOCHA = {
|
|
|
1538
2552
|
overlay0: "#6c7086"
|
|
1539
2553
|
};
|
|
1540
2554
|
const identityColor = (value) => value;
|
|
2555
|
+
const hasDefaultListColumnOrder = (columns) => {
|
|
2556
|
+
if (columns.length !== DEFAULT_LIST_TABLE_COLUMNS.length) return false;
|
|
2557
|
+
return columns.every((column, index) => column === DEFAULT_LIST_TABLE_COLUMNS[index]);
|
|
2558
|
+
};
|
|
1541
2559
|
const createCatppuccinTheme = ({ enabled }) => {
|
|
1542
2560
|
if (enabled !== true) return {
|
|
1543
2561
|
header: identityColor,
|
|
@@ -1848,6 +2866,7 @@ const commandHelpEntries = [
|
|
|
1848
2866
|
options: ["--install", "--path <file>"]
|
|
1849
2867
|
}
|
|
1850
2868
|
];
|
|
2869
|
+
const commandHelpNames = commandHelpEntries.map((entry) => entry.name);
|
|
1851
2870
|
const splitRawArgsByDoubleDash = (args) => {
|
|
1852
2871
|
const separatorIndex = args.indexOf("--");
|
|
1853
2872
|
if (separatorIndex < 0) return {
|
|
@@ -1865,7 +2884,8 @@ const toKebabCase = (value) => {
|
|
|
1865
2884
|
const toOptionSpec = (kind, optionName) => {
|
|
1866
2885
|
return {
|
|
1867
2886
|
kind,
|
|
1868
|
-
allowOptionLikeValue: optionNamesAllowOptionLikeValue.has(optionName)
|
|
2887
|
+
allowOptionLikeValue: optionNamesAllowOptionLikeValue.has(optionName),
|
|
2888
|
+
allowNegation: kind === "boolean"
|
|
1869
2889
|
};
|
|
1870
2890
|
};
|
|
1871
2891
|
const buildOptionSpecs = (argsDef) => {
|
|
@@ -1893,6 +2913,69 @@ const buildOptionSpecs = (argsDef) => {
|
|
|
1893
2913
|
shortOptions
|
|
1894
2914
|
};
|
|
1895
2915
|
};
|
|
2916
|
+
const ensureOptionValueToken = ({ valueToken, optionLabel, optionSpec }) => {
|
|
2917
|
+
if (valueToken.length === 0) throw createCliError("INVALID_ARGUMENT", { message: `Missing value for option: ${optionLabel}` });
|
|
2918
|
+
if (valueToken.startsWith("-") && optionSpec.allowOptionLikeValue !== true) throw createCliError("INVALID_ARGUMENT", { message: `Missing value for option: ${optionLabel}` });
|
|
2919
|
+
};
|
|
2920
|
+
const resolveLongOption = ({ rawOptionName, optionSpecs }) => {
|
|
2921
|
+
const directOptionSpec = optionSpecs.longOptions.get(rawOptionName);
|
|
2922
|
+
if (directOptionSpec !== void 0) return {
|
|
2923
|
+
optionSpec: directOptionSpec,
|
|
2924
|
+
optionName: rawOptionName
|
|
2925
|
+
};
|
|
2926
|
+
if (rawOptionName.startsWith("no-")) {
|
|
2927
|
+
const optionName = rawOptionName.slice(3);
|
|
2928
|
+
const negatedOptionSpec = optionSpecs.longOptions.get(optionName);
|
|
2929
|
+
if (negatedOptionSpec?.allowNegation === true) return {
|
|
2930
|
+
optionSpec: negatedOptionSpec,
|
|
2931
|
+
optionName
|
|
2932
|
+
};
|
|
2933
|
+
}
|
|
2934
|
+
};
|
|
2935
|
+
const validateLongOptionToken = ({ args, index, token, optionSpecs }) => {
|
|
2936
|
+
const value = token.slice(2);
|
|
2937
|
+
if (value.length === 0) return index;
|
|
2938
|
+
const separatorIndex = value.indexOf("=");
|
|
2939
|
+
const rawOptionName = separatorIndex >= 0 ? value.slice(0, separatorIndex) : value;
|
|
2940
|
+
const resolved = resolveLongOption({
|
|
2941
|
+
rawOptionName,
|
|
2942
|
+
optionSpecs
|
|
2943
|
+
});
|
|
2944
|
+
if (resolved === void 0) throw createCliError("INVALID_ARGUMENT", { message: `Unknown option: --${rawOptionName}` });
|
|
2945
|
+
if (resolved.optionSpec.kind !== "value") return index;
|
|
2946
|
+
if (separatorIndex >= 0) {
|
|
2947
|
+
if (value.slice(separatorIndex + 1).length === 0) throw createCliError("INVALID_ARGUMENT", { message: `Missing value for option: --${rawOptionName}` });
|
|
2948
|
+
return index;
|
|
2949
|
+
}
|
|
2950
|
+
const nextToken = args[index + 1];
|
|
2951
|
+
if (typeof nextToken !== "string") throw createCliError("INVALID_ARGUMENT", { message: `Missing value for option: --${rawOptionName}` });
|
|
2952
|
+
ensureOptionValueToken({
|
|
2953
|
+
valueToken: nextToken,
|
|
2954
|
+
optionLabel: `--${rawOptionName}`,
|
|
2955
|
+
optionSpec: resolved.optionSpec
|
|
2956
|
+
});
|
|
2957
|
+
return index + 1;
|
|
2958
|
+
};
|
|
2959
|
+
const validateShortOptionToken = ({ args, index, token, optionSpecs }) => {
|
|
2960
|
+
const shortFlags = token.slice(1);
|
|
2961
|
+
for (let flagIndex = 0; flagIndex < shortFlags.length; flagIndex += 1) {
|
|
2962
|
+
const option = shortFlags[flagIndex];
|
|
2963
|
+
if (typeof option !== "string" || option.length === 0) continue;
|
|
2964
|
+
const optionSpec = optionSpecs.shortOptions.get(option);
|
|
2965
|
+
if (optionSpec === void 0) throw createCliError("INVALID_ARGUMENT", { message: `Unknown option: -${option}` });
|
|
2966
|
+
if (optionSpec.kind !== "value") continue;
|
|
2967
|
+
if (flagIndex < shortFlags.length - 1) throw createCliError("INVALID_ARGUMENT", { message: `Missing value for option: -${option}` });
|
|
2968
|
+
const nextToken = args[index + 1];
|
|
2969
|
+
if (typeof nextToken !== "string") throw createCliError("INVALID_ARGUMENT", { message: `Missing value for option: -${option}` });
|
|
2970
|
+
ensureOptionValueToken({
|
|
2971
|
+
valueToken: nextToken,
|
|
2972
|
+
optionLabel: `-${option}`,
|
|
2973
|
+
optionSpec
|
|
2974
|
+
});
|
|
2975
|
+
return index + 1;
|
|
2976
|
+
}
|
|
2977
|
+
return index;
|
|
2978
|
+
};
|
|
1896
2979
|
const validateRawOptions = (args, optionSpecs) => {
|
|
1897
2980
|
for (let index = 0; index < args.length; index += 1) {
|
|
1898
2981
|
const token = args[index];
|
|
@@ -1900,39 +2983,20 @@ const validateRawOptions = (args, optionSpecs) => {
|
|
|
1900
2983
|
if (token === "--") break;
|
|
1901
2984
|
if (!token.startsWith("-") || token === "-") continue;
|
|
1902
2985
|
if (token.startsWith("--")) {
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
const optionSpec = directOptionSpec ?? optionSpecs.longOptions.get(optionNameForNegation);
|
|
1910
|
-
if (optionSpec === void 0) throw createCliError("INVALID_ARGUMENT", { message: `Unknown option: --${rawOptionName}` });
|
|
1911
|
-
if (optionSpec.kind === "value") if (separatorIndex >= 0) {
|
|
1912
|
-
if (value.slice(separatorIndex + 1).length === 0) throw createCliError("INVALID_ARGUMENT", { message: `Missing value for option: --${rawOptionName}` });
|
|
1913
|
-
} else {
|
|
1914
|
-
const nextToken = args[index + 1];
|
|
1915
|
-
if (typeof nextToken !== "string" || nextToken.length === 0) throw createCliError("INVALID_ARGUMENT", { message: `Missing value for option: --${rawOptionName}` });
|
|
1916
|
-
if (nextToken.startsWith("-") && optionSpec.allowOptionLikeValue !== true) throw createCliError("INVALID_ARGUMENT", { message: `Missing value for option: --${rawOptionName}` });
|
|
1917
|
-
index += 1;
|
|
1918
|
-
}
|
|
2986
|
+
index = validateLongOptionToken({
|
|
2987
|
+
args,
|
|
2988
|
+
index,
|
|
2989
|
+
token,
|
|
2990
|
+
optionSpecs
|
|
2991
|
+
});
|
|
1919
2992
|
continue;
|
|
1920
2993
|
}
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
if (optionSpec.kind === "value") {
|
|
1928
|
-
if (flagIndex < shortFlags.length - 1) break;
|
|
1929
|
-
const nextToken = args[index + 1];
|
|
1930
|
-
if (typeof nextToken !== "string" || nextToken.length === 0) throw createCliError("INVALID_ARGUMENT", { message: `Missing value for option: -${option}` });
|
|
1931
|
-
if (nextToken.startsWith("-") && optionSpec.allowOptionLikeValue !== true) throw createCliError("INVALID_ARGUMENT", { message: `Missing value for option: -${option}` });
|
|
1932
|
-
index += 1;
|
|
1933
|
-
break;
|
|
1934
|
-
}
|
|
1935
|
-
}
|
|
2994
|
+
index = validateShortOptionToken({
|
|
2995
|
+
args,
|
|
2996
|
+
index,
|
|
2997
|
+
token,
|
|
2998
|
+
optionSpecs
|
|
2999
|
+
});
|
|
1936
3000
|
}
|
|
1937
3001
|
};
|
|
1938
3002
|
const getPositionals = (args) => {
|
|
@@ -1985,85 +3049,29 @@ const ensureArgumentCount = ({ command, args, min, max }) => {
|
|
|
1985
3049
|
const ensureHasCommandAfterDoubleDash = ({ command, argsAfterDoubleDash }) => {
|
|
1986
3050
|
if (argsAfterDoubleDash.length === 0) throw createCliError("INVALID_ARGUMENT", { message: `${command} requires arguments after --` });
|
|
1987
3051
|
};
|
|
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({
|
|
3052
|
+
const resolveBaseBranch = async ({ repoRoot, config }) => {
|
|
3053
|
+
if (typeof config.git.baseBranch === "string" && config.git.baseBranch.length > 0) return config.git.baseBranch;
|
|
3054
|
+
const remote = config.git.baseRemote;
|
|
3055
|
+
const resolved = await runGitCommand({
|
|
2020
3056
|
cwd: repoRoot,
|
|
2021
3057
|
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"
|
|
3058
|
+
"symbolic-ref",
|
|
3059
|
+
"--quiet",
|
|
3060
|
+
"--short",
|
|
3061
|
+
`refs/remotes/${remote}/HEAD`
|
|
2038
3062
|
],
|
|
2039
3063
|
reject: false
|
|
2040
3064
|
});
|
|
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;
|
|
3065
|
+
if (resolved.exitCode === 0) {
|
|
2061
3066
|
const raw = resolved.stdout.trim();
|
|
2062
3067
|
const prefix = `${remote}/`;
|
|
2063
3068
|
if (raw.startsWith(prefix)) return raw.slice(prefix.length);
|
|
2064
3069
|
}
|
|
2065
3070
|
for (const candidate of ["main", "master"]) if (await doesGitRefExist(repoRoot, `refs/heads/${candidate}`)) return candidate;
|
|
2066
|
-
throw createCliError("INVALID_ARGUMENT", {
|
|
3071
|
+
throw createCliError("INVALID_ARGUMENT", {
|
|
3072
|
+
message: "Unable to resolve base branch from config.yml (baseRemote/HEAD -> main/master).",
|
|
3073
|
+
details: { remote }
|
|
3074
|
+
});
|
|
2067
3075
|
};
|
|
2068
3076
|
const ensureTargetPathWritable = async (targetPath) => {
|
|
2069
3077
|
try {
|
|
@@ -2256,7 +3264,7 @@ const validateDeleteSafety = ({ target, forceFlags }) => {
|
|
|
2256
3264
|
const resolveLinkTargetPath = ({ sourcePath, destinationPath }) => {
|
|
2257
3265
|
return relative(dirname(destinationPath), sourcePath);
|
|
2258
3266
|
};
|
|
2259
|
-
const resolveFileCopyTargets = ({ repoRoot,
|
|
3267
|
+
const resolveFileCopyTargets = ({ repoRoot, targetWorktreeRoot, relativePath }) => {
|
|
2260
3268
|
const sourcePath = resolveRepoRelativePath({
|
|
2261
3269
|
repoRoot,
|
|
2262
3270
|
relativePath
|
|
@@ -2264,13 +3272,32 @@ const resolveFileCopyTargets = ({ repoRoot, worktreePath, relativePath }) => {
|
|
|
2264
3272
|
const relativeFromRoot = relative(repoRoot, sourcePath);
|
|
2265
3273
|
return {
|
|
2266
3274
|
sourcePath,
|
|
2267
|
-
destinationPath:
|
|
2268
|
-
|
|
2269
|
-
path: resolve(
|
|
3275
|
+
destinationPath: ensurePathInsideRoot({
|
|
3276
|
+
rootPath: targetWorktreeRoot,
|
|
3277
|
+
path: resolve(targetWorktreeRoot, relativeFromRoot),
|
|
3278
|
+
message: "Path is outside target worktree root"
|
|
2270
3279
|
}),
|
|
2271
3280
|
relativeFromRoot
|
|
2272
3281
|
};
|
|
2273
3282
|
};
|
|
3283
|
+
const resolveTargetWorktreeRootForCopyLink = ({ repoContext, snapshot }) => {
|
|
3284
|
+
const rawTarget = process.env.WT_WORKTREE_PATH ?? repoContext.currentWorktreeRoot;
|
|
3285
|
+
const resolvedTarget = resolvePathFromCwd({
|
|
3286
|
+
cwd: repoContext.currentWorktreeRoot,
|
|
3287
|
+
path: rawTarget
|
|
3288
|
+
});
|
|
3289
|
+
const matched = snapshot.worktrees.filter((worktree) => {
|
|
3290
|
+
return worktree.path === resolvedTarget || resolvedTarget.startsWith(`${worktree.path}${sep}`);
|
|
3291
|
+
}).sort((a, b) => b.path.length - a.path.length)[0];
|
|
3292
|
+
if (matched === void 0) throw createCliError("WORKTREE_NOT_FOUND", {
|
|
3293
|
+
message: "copy/link target worktree not found",
|
|
3294
|
+
details: {
|
|
3295
|
+
rawTarget,
|
|
3296
|
+
resolvedTarget
|
|
3297
|
+
}
|
|
3298
|
+
});
|
|
3299
|
+
return matched.path;
|
|
3300
|
+
};
|
|
2274
3301
|
const ensureBranchIsNotPrimary = ({ branch, baseBranch }) => {
|
|
2275
3302
|
if (branch !== baseBranch) return;
|
|
2276
3303
|
throw createCliError("INVALID_ARGUMENT", {
|
|
@@ -2281,12 +3308,14 @@ const ensureBranchIsNotPrimary = ({ branch, baseBranch }) => {
|
|
|
2281
3308
|
}
|
|
2282
3309
|
});
|
|
2283
3310
|
};
|
|
2284
|
-
const toManagedWorktreeName = ({
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
3311
|
+
const toManagedWorktreeName = ({ managedWorktreeRoot, worktreePath }) => {
|
|
3312
|
+
if (isManagedWorktreePath({
|
|
3313
|
+
worktreePath,
|
|
3314
|
+
managedWorktreeRoot
|
|
3315
|
+
}) !== true) return null;
|
|
3316
|
+
return relative(managedWorktreeRoot, worktreePath).split(sep).join("/");
|
|
2288
3317
|
};
|
|
2289
|
-
const resolveManagedWorktreePathFromName = ({
|
|
3318
|
+
const resolveManagedWorktreePathFromName = ({ managedWorktreeRoot, optionName, worktreeName }) => {
|
|
2290
3319
|
const normalized = worktreeName.trim();
|
|
2291
3320
|
if (normalized.length === 0) throw createCliError("INVALID_ARGUMENT", {
|
|
2292
3321
|
message: `${optionName} requires non-empty worktree name`,
|
|
@@ -2295,18 +3324,10 @@ const resolveManagedWorktreePathFromName = ({ repoRoot, optionName, worktreeName
|
|
|
2295
3324
|
worktreeName
|
|
2296
3325
|
}
|
|
2297
3326
|
});
|
|
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
3327
|
let resolvedPath;
|
|
2307
3328
|
try {
|
|
2308
3329
|
resolvedPath = resolveRepoRelativePath({
|
|
2309
|
-
repoRoot:
|
|
3330
|
+
repoRoot: managedWorktreeRoot,
|
|
2310
3331
|
relativePath: normalized
|
|
2311
3332
|
});
|
|
2312
3333
|
} catch (error) {
|
|
@@ -2319,7 +3340,7 @@ const resolveManagedWorktreePathFromName = ({ repoRoot, optionName, worktreeName
|
|
|
2319
3340
|
cause: error
|
|
2320
3341
|
});
|
|
2321
3342
|
}
|
|
2322
|
-
if (resolvedPath ===
|
|
3343
|
+
if (resolvedPath === managedWorktreeRoot) throw createCliError("INVALID_ARGUMENT", {
|
|
2323
3344
|
message: `${optionName} expects vw-managed worktree name`,
|
|
2324
3345
|
details: {
|
|
2325
3346
|
optionName,
|
|
@@ -2328,16 +3349,16 @@ const resolveManagedWorktreePathFromName = ({ repoRoot, optionName, worktreeName
|
|
|
2328
3349
|
});
|
|
2329
3350
|
return resolvedPath;
|
|
2330
3351
|
};
|
|
2331
|
-
const resolveManagedNonPrimaryWorktreeByBranch = ({ repoRoot, branch, worktrees, optionName, worktreeName, role }) => {
|
|
3352
|
+
const resolveManagedNonPrimaryWorktreeByBranch = ({ repoRoot, managedWorktreeRoot, branch, worktrees, optionName, worktreeName, role }) => {
|
|
2332
3353
|
const managedCandidates = worktrees.filter((worktree) => {
|
|
2333
3354
|
return worktree.branch === branch && worktree.path !== repoRoot && toManagedWorktreeName({
|
|
2334
|
-
|
|
3355
|
+
managedWorktreeRoot,
|
|
2335
3356
|
worktreePath: worktree.path
|
|
2336
3357
|
}) !== null;
|
|
2337
3358
|
});
|
|
2338
3359
|
if (typeof worktreeName === "string") {
|
|
2339
3360
|
const resolvedPath = resolveManagedWorktreePathFromName({
|
|
2340
|
-
|
|
3361
|
+
managedWorktreeRoot,
|
|
2341
3362
|
optionName,
|
|
2342
3363
|
worktreeName
|
|
2343
3364
|
});
|
|
@@ -2368,7 +3389,7 @@ const resolveManagedNonPrimaryWorktreeByBranch = ({ repoRoot, branch, worktrees,
|
|
|
2368
3389
|
optionName,
|
|
2369
3390
|
candidates: managedCandidates.map((worktree) => {
|
|
2370
3391
|
return toManagedWorktreeName({
|
|
2371
|
-
|
|
3392
|
+
managedWorktreeRoot,
|
|
2372
3393
|
worktreePath: worktree.path
|
|
2373
3394
|
}) ?? worktree.path;
|
|
2374
3395
|
})
|
|
@@ -2525,19 +3546,24 @@ const resolveListColumnContentWidth = ({ rows, columnIndex }) => {
|
|
|
2525
3546
|
return Math.max(width, stringWidth(cell));
|
|
2526
3547
|
}, 0);
|
|
2527
3548
|
};
|
|
2528
|
-
const resolveListPathColumnWidth = ({ rows,
|
|
2529
|
-
|
|
3549
|
+
const resolveListPathColumnWidth = ({ rows, columns, truncateMode, fullPath, minWidth }) => {
|
|
3550
|
+
const pathColumnIndex = columns.indexOf("path");
|
|
3551
|
+
if (pathColumnIndex < 0) return null;
|
|
3552
|
+
if (fullPath || truncateMode === "never") return null;
|
|
2530
3553
|
if (process.stdout.isTTY !== true) return null;
|
|
2531
3554
|
const terminalColumns = process.stdout.columns;
|
|
2532
3555
|
if (typeof terminalColumns !== "number" || Number.isFinite(terminalColumns) !== true || terminalColumns <= 0) return null;
|
|
2533
|
-
const measuredNonPathWidth =
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
3556
|
+
const measuredNonPathWidth = columns.map((_, index) => {
|
|
3557
|
+
if (index === pathColumnIndex) return 0;
|
|
3558
|
+
return resolveListColumnContentWidth({
|
|
3559
|
+
rows,
|
|
3560
|
+
columnIndex: index
|
|
3561
|
+
});
|
|
3562
|
+
}).reduce((sum, width) => sum + width, 0);
|
|
3563
|
+
const borderWidth = columns.length + 1;
|
|
3564
|
+
const paddingWidth = columns.length * LIST_TABLE_CELL_HORIZONTAL_PADDING;
|
|
2539
3565
|
const availablePathWidth = Math.floor(terminalColumns) - borderWidth - paddingWidth - measuredNonPathWidth;
|
|
2540
|
-
return Math.max(
|
|
3566
|
+
return Math.max(minWidth, availablePathWidth);
|
|
2541
3567
|
};
|
|
2542
3568
|
const resolveAheadBehindAgainstBaseBranch = async ({ repoRoot, baseBranch, worktree }) => {
|
|
2543
3569
|
if (baseBranch === null) return {
|
|
@@ -2878,7 +3904,7 @@ const createCli = (options = {}) => {
|
|
|
2878
3904
|
from: {
|
|
2879
3905
|
type: "string",
|
|
2880
3906
|
valueHint: "value",
|
|
2881
|
-
description: "For extract: filesystem path. For absorb: managed worktree name
|
|
3907
|
+
description: "For extract: filesystem path. For absorb: managed worktree name."
|
|
2882
3908
|
},
|
|
2883
3909
|
to: {
|
|
2884
3910
|
type: "string",
|
|
@@ -2931,135 +3957,76 @@ const createCli = (options = {}) => {
|
|
|
2931
3957
|
const parsedArgsRecord = parsedArgs;
|
|
2932
3958
|
const positionals = getPositionals(parsedArgs);
|
|
2933
3959
|
command = positionals[0] ?? "unknown";
|
|
3960
|
+
const commandArgs = positionals.slice(1);
|
|
2934
3961
|
jsonEnabled = parsedArgs.json === true;
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
3962
|
+
const readOnlyDispatch = await dispatchReadOnlyCommands({
|
|
3963
|
+
command,
|
|
3964
|
+
commandArgs,
|
|
3965
|
+
positionals,
|
|
3966
|
+
parsedArgs: parsedArgsRecord,
|
|
3967
|
+
jsonEnabled,
|
|
3968
|
+
version,
|
|
3969
|
+
availableCommandNames: commandHelpNames,
|
|
3970
|
+
stdout,
|
|
3971
|
+
findCommandHelp,
|
|
3972
|
+
renderGeneralHelpText,
|
|
3973
|
+
renderCommandHelpText,
|
|
3974
|
+
ensureArgumentCount,
|
|
3975
|
+
resolveCompletionShell,
|
|
3976
|
+
loadCompletionScript,
|
|
3977
|
+
resolveCompletionInstallPath,
|
|
3978
|
+
installCompletionScript,
|
|
3979
|
+
readStringOption,
|
|
3980
|
+
buildJsonSuccess
|
|
3981
|
+
});
|
|
3982
|
+
if (readOnlyDispatch.handled) return readOnlyDispatch.exitCode;
|
|
2951
3983
|
logger = parsedArgs.verbose === true ? createLogger({ level: LogLevel.INFO }) : createLogger();
|
|
2952
|
-
if (positionals.length === 0) {
|
|
2953
|
-
stdout(`${renderGeneralHelpText({ version })}\n`);
|
|
2954
|
-
return EXIT_CODE.OK;
|
|
2955
|
-
}
|
|
2956
|
-
if (command === "help") {
|
|
2957
|
-
const helpTarget = positionals[1];
|
|
2958
|
-
if (typeof helpTarget !== "string" || helpTarget.length === 0) {
|
|
2959
|
-
stdout(`${renderGeneralHelpText({ version })}\n`);
|
|
2960
|
-
return EXIT_CODE.OK;
|
|
2961
|
-
}
|
|
2962
|
-
const entry = findCommandHelp(helpTarget);
|
|
2963
|
-
if (entry === void 0) throw createCliError("INVALID_ARGUMENT", {
|
|
2964
|
-
message: `Unknown command for help: ${helpTarget}`,
|
|
2965
|
-
details: {
|
|
2966
|
-
requested: helpTarget,
|
|
2967
|
-
availableCommands: commandHelpEntries.map((item) => item.name)
|
|
2968
|
-
}
|
|
2969
|
-
});
|
|
2970
|
-
stdout(`${renderCommandHelpText({ entry })}\n`);
|
|
2971
|
-
return EXIT_CODE.OK;
|
|
2972
|
-
}
|
|
2973
|
-
const commandArgs = positionals.slice(1);
|
|
2974
|
-
if (command === "completion") {
|
|
2975
|
-
ensureArgumentCount({
|
|
2976
|
-
command,
|
|
2977
|
-
args: commandArgs,
|
|
2978
|
-
min: 1,
|
|
2979
|
-
max: 1
|
|
2980
|
-
});
|
|
2981
|
-
const shell = resolveCompletionShell(commandArgs[0]);
|
|
2982
|
-
const script = await loadCompletionScript(shell);
|
|
2983
|
-
if (parsedArgs.install === true) {
|
|
2984
|
-
const destinationPath = resolveCompletionInstallPath({
|
|
2985
|
-
shell,
|
|
2986
|
-
requestedPath: readStringOption(parsedArgsRecord, "path")
|
|
2987
|
-
});
|
|
2988
|
-
await installCompletionScript({
|
|
2989
|
-
content: script,
|
|
2990
|
-
destinationPath
|
|
2991
|
-
});
|
|
2992
|
-
if (jsonEnabled) {
|
|
2993
|
-
stdout(JSON.stringify(buildJsonSuccess({
|
|
2994
|
-
command,
|
|
2995
|
-
status: "ok",
|
|
2996
|
-
repoRoot: null,
|
|
2997
|
-
details: {
|
|
2998
|
-
shell,
|
|
2999
|
-
installed: true,
|
|
3000
|
-
path: destinationPath
|
|
3001
|
-
}
|
|
3002
|
-
})));
|
|
3003
|
-
return EXIT_CODE.OK;
|
|
3004
|
-
}
|
|
3005
|
-
stdout(`installed completion: ${destinationPath}`);
|
|
3006
|
-
if (shell === "zsh") stdout("zsh note: ensure completion path is in fpath, then run: autoload -Uz compinit && compinit");
|
|
3007
|
-
return EXIT_CODE.OK;
|
|
3008
|
-
}
|
|
3009
|
-
if (jsonEnabled) {
|
|
3010
|
-
stdout(JSON.stringify(buildJsonSuccess({
|
|
3011
|
-
command,
|
|
3012
|
-
status: "ok",
|
|
3013
|
-
repoRoot: null,
|
|
3014
|
-
details: {
|
|
3015
|
-
shell,
|
|
3016
|
-
installed: false,
|
|
3017
|
-
script
|
|
3018
|
-
}
|
|
3019
|
-
})));
|
|
3020
|
-
return EXIT_CODE.OK;
|
|
3021
|
-
}
|
|
3022
|
-
stdout(script);
|
|
3023
|
-
return EXIT_CODE.OK;
|
|
3024
|
-
}
|
|
3025
3984
|
const allowUnsafe = parsedArgs.allowUnsafe === true;
|
|
3026
3985
|
if (parsedArgs.hooks === false && allowUnsafe !== true) throw createCliError("UNSAFE_FLAG_REQUIRED", { message: "UNSAFE_FLAG_REQUIRED: --no-hooks requires --allow-unsafe" });
|
|
3027
3986
|
const repoContext = await resolveRepoContext(runtimeCwd);
|
|
3028
3987
|
const repoRoot = repoContext.repoRoot;
|
|
3029
3988
|
repoRootForJson = repoRoot;
|
|
3030
|
-
const
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3989
|
+
const { config: resolvedConfig } = await loadResolvedConfig({
|
|
3990
|
+
cwd: runtimeCwd,
|
|
3991
|
+
repoRoot
|
|
3992
|
+
});
|
|
3993
|
+
const managedWorktreeRoot = getWorktreeRootPath(repoRoot, resolvedConfig.paths.worktreeRoot);
|
|
3034
3994
|
const runtime = {
|
|
3035
3995
|
command,
|
|
3036
3996
|
json: jsonEnabled,
|
|
3037
|
-
hooksEnabled: parsedArgs.hooks !== false &&
|
|
3038
|
-
ghEnabled: parsedArgs.gh !== false,
|
|
3997
|
+
hooksEnabled: parsedArgs.hooks !== false && resolvedConfig.hooks.enabled,
|
|
3998
|
+
ghEnabled: parsedArgs.gh !== false && resolvedConfig.github.enabled,
|
|
3039
3999
|
strictPostHooks: parsedArgs.strictPostHooks === true,
|
|
3040
4000
|
hookTimeoutMs: readNumberFromEnvOrDefault({
|
|
3041
4001
|
rawValue: toNumberOption({
|
|
3042
4002
|
value: parsedArgs.hookTimeoutMs,
|
|
3043
4003
|
optionName: "--hook-timeout-ms"
|
|
3044
|
-
}) ??
|
|
4004
|
+
}) ?? resolvedConfig.hooks.timeoutMs,
|
|
3045
4005
|
defaultValue: DEFAULT_HOOK_TIMEOUT_MS
|
|
3046
4006
|
}),
|
|
3047
4007
|
lockTimeoutMs: readNumberFromEnvOrDefault({
|
|
3048
4008
|
rawValue: toNumberOption({
|
|
3049
4009
|
value: parsedArgs.lockTimeoutMs,
|
|
3050
4010
|
optionName: "--lock-timeout-ms"
|
|
3051
|
-
}) ??
|
|
4011
|
+
}) ?? resolvedConfig.locks.timeoutMs,
|
|
3052
4012
|
defaultValue: DEFAULT_LOCK_TIMEOUT_MS
|
|
3053
4013
|
}),
|
|
3054
4014
|
allowUnsafe,
|
|
3055
4015
|
isInteractive: isInteractiveFn()
|
|
3056
4016
|
};
|
|
3057
4017
|
const staleLockTTLSeconds = readNumberFromEnvOrDefault({
|
|
3058
|
-
rawValue:
|
|
4018
|
+
rawValue: resolvedConfig.locks.staleLockTTLSeconds,
|
|
3059
4019
|
defaultValue: DEFAULT_STALE_LOCK_TTL_SECONDS
|
|
3060
4020
|
});
|
|
3061
4021
|
const collectWorktreeSnapshot$1 = async (_ignoredRepoRoot) => {
|
|
3062
|
-
return collectWorktreeSnapshot(repoRoot, {
|
|
4022
|
+
return collectWorktreeSnapshot(repoRoot, {
|
|
4023
|
+
baseBranch: await resolveBaseBranch({
|
|
4024
|
+
repoRoot,
|
|
4025
|
+
config: resolvedConfig
|
|
4026
|
+
}),
|
|
4027
|
+
ghEnabled: runtime.ghEnabled,
|
|
4028
|
+
noGh: runtime.ghEnabled !== true
|
|
4029
|
+
});
|
|
3063
4030
|
};
|
|
3064
4031
|
const runWriteOperation = async (task) => {
|
|
3065
4032
|
if (WRITE_COMMANDS.has(command) !== true) return task();
|
|
@@ -3071,7 +4038,30 @@ const createCli = (options = {}) => {
|
|
|
3071
4038
|
staleLockTTLSeconds
|
|
3072
4039
|
}, task);
|
|
3073
4040
|
};
|
|
3074
|
-
|
|
4041
|
+
const executeWorktreeMutation = async ({ name, branch, worktreePath, extraEnv, precheck, runGit, finalize }) => {
|
|
4042
|
+
const precheckResult = await precheck();
|
|
4043
|
+
const hookContext = createHookContext({
|
|
4044
|
+
runtime,
|
|
4045
|
+
repoRoot,
|
|
4046
|
+
action: name,
|
|
4047
|
+
branch,
|
|
4048
|
+
worktreePath,
|
|
4049
|
+
stderr,
|
|
4050
|
+
extraEnv
|
|
4051
|
+
});
|
|
4052
|
+
await runPreHook({
|
|
4053
|
+
name,
|
|
4054
|
+
context: hookContext
|
|
4055
|
+
});
|
|
4056
|
+
const result = await runGit(precheckResult);
|
|
4057
|
+
if (finalize !== void 0) await finalize(precheckResult, result);
|
|
4058
|
+
await runPostHook({
|
|
4059
|
+
name,
|
|
4060
|
+
context: hookContext
|
|
4061
|
+
});
|
|
4062
|
+
return result;
|
|
4063
|
+
};
|
|
4064
|
+
const handleInit = async () => {
|
|
3075
4065
|
ensureArgumentCount({
|
|
3076
4066
|
command,
|
|
3077
4067
|
args: commandArgs,
|
|
@@ -3091,28 +4081,28 @@ const createCli = (options = {}) => {
|
|
|
3091
4081
|
name: "init",
|
|
3092
4082
|
context: hookContext
|
|
3093
4083
|
});
|
|
3094
|
-
const initialized = await initializeRepository(
|
|
4084
|
+
const initialized = await initializeRepository({
|
|
4085
|
+
repoRoot,
|
|
4086
|
+
managedWorktreeRoot
|
|
4087
|
+
});
|
|
3095
4088
|
await runPostHook({
|
|
3096
4089
|
name: "init",
|
|
3097
4090
|
context: hookContext
|
|
3098
4091
|
});
|
|
3099
4092
|
return initialized;
|
|
3100
4093
|
});
|
|
3101
|
-
if (runtime.json) {
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
})));
|
|
3111
|
-
return EXIT_CODE.OK;
|
|
3112
|
-
}
|
|
4094
|
+
if (runtime.json) stdout(JSON.stringify(buildJsonSuccess({
|
|
4095
|
+
command,
|
|
4096
|
+
status: "ok",
|
|
4097
|
+
repoRoot,
|
|
4098
|
+
details: {
|
|
4099
|
+
initialized: true,
|
|
4100
|
+
alreadyInitialized: result.alreadyInitialized
|
|
4101
|
+
}
|
|
4102
|
+
})));
|
|
3113
4103
|
return EXIT_CODE.OK;
|
|
3114
|
-
}
|
|
3115
|
-
|
|
4104
|
+
};
|
|
4105
|
+
const handleList = async () => {
|
|
3116
4106
|
ensureArgumentCount({
|
|
3117
4107
|
command,
|
|
3118
4108
|
args: commandArgs,
|
|
@@ -3127,22 +4117,15 @@ const createCli = (options = {}) => {
|
|
|
3127
4117
|
repoRoot,
|
|
3128
4118
|
details: {
|
|
3129
4119
|
baseBranch: snapshot.baseBranch,
|
|
4120
|
+
managedWorktreeRoot,
|
|
3130
4121
|
worktrees: snapshot.worktrees
|
|
3131
4122
|
}
|
|
3132
4123
|
})));
|
|
3133
4124
|
return EXIT_CODE.OK;
|
|
3134
4125
|
}
|
|
3135
4126
|
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) => {
|
|
4127
|
+
const columns = resolvedConfig.list.table.columns;
|
|
4128
|
+
const rows = [[...columns], ...await Promise.all(snapshot.worktrees.map(async (worktree) => {
|
|
3146
4129
|
const distanceFromBase = await resolveAheadBehindAgainstBaseBranch({
|
|
3147
4130
|
repoRoot,
|
|
3148
4131
|
baseBranch: snapshot.baseBranch,
|
|
@@ -3154,39 +4137,45 @@ const createCli = (options = {}) => {
|
|
|
3154
4137
|
prStatus: worktree.pr.status,
|
|
3155
4138
|
isBaseBranch
|
|
3156
4139
|
});
|
|
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
|
-
|
|
4140
|
+
const valuesByColumn = {
|
|
4141
|
+
branch: `${worktree.path === repoContext.currentWorktreeRoot ? "*" : " "} ${worktree.branch ?? "(detached)"}`,
|
|
4142
|
+
dirty: worktree.dirty ? "dirty" : "clean",
|
|
4143
|
+
merged: mergedState,
|
|
4144
|
+
pr: prState,
|
|
4145
|
+
locked: worktree.locked.value ? "locked" : "-",
|
|
4146
|
+
ahead: formatListUpstreamCount(distanceFromBase.ahead),
|
|
4147
|
+
behind: formatListUpstreamCount(distanceFromBase.behind),
|
|
4148
|
+
path: formatDisplayPath(worktree.path)
|
|
4149
|
+
};
|
|
4150
|
+
return columns.map((column) => valuesByColumn[column]);
|
|
3167
4151
|
}))];
|
|
3168
4152
|
const pathColumnWidth = resolveListPathColumnWidth({
|
|
3169
4153
|
rows,
|
|
3170
|
-
|
|
4154
|
+
columns,
|
|
4155
|
+
truncateMode: resolvedConfig.list.table.path.truncate,
|
|
4156
|
+
fullPath: parsedArgs.fullPath === true,
|
|
4157
|
+
minWidth: resolvedConfig.list.table.path.minWidth
|
|
3171
4158
|
});
|
|
3172
|
-
const
|
|
4159
|
+
const pathColumnIndex = columns.indexOf("path");
|
|
4160
|
+
const columnsConfig = pathColumnWidth === null || pathColumnIndex < 0 ? void 0 : { [pathColumnIndex]: {
|
|
3173
4161
|
width: pathColumnWidth,
|
|
3174
4162
|
truncate: pathColumnWidth
|
|
3175
4163
|
} };
|
|
3176
|
-
const
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
columns: columnsConfig
|
|
3183
|
-
}),
|
|
3184
|
-
theme
|
|
4164
|
+
const rendered = table(rows, {
|
|
4165
|
+
border: getBorderCharacters("norc"),
|
|
4166
|
+
drawHorizontalLine: (lineIndex, rowCount) => {
|
|
4167
|
+
return lineIndex === 0 || lineIndex === 1 || lineIndex === rowCount;
|
|
4168
|
+
},
|
|
4169
|
+
columns: columnsConfig
|
|
3185
4170
|
});
|
|
4171
|
+
const colorized = hasDefaultListColumnOrder(columns) ? colorizeListTable({
|
|
4172
|
+
rendered,
|
|
4173
|
+
theme
|
|
4174
|
+
}) : rendered.trimEnd();
|
|
3186
4175
|
for (const line of colorized.split("\n")) stdout(line);
|
|
3187
4176
|
return EXIT_CODE.OK;
|
|
3188
|
-
}
|
|
3189
|
-
|
|
4177
|
+
};
|
|
4178
|
+
const handleStatus = async () => {
|
|
3190
4179
|
ensureArgumentCount({
|
|
3191
4180
|
command,
|
|
3192
4181
|
args: commandArgs,
|
|
@@ -3216,8 +4205,8 @@ const createCli = (options = {}) => {
|
|
|
3216
4205
|
stdout(`dirty: ${targetWorktree.dirty ? "true" : "false"}`);
|
|
3217
4206
|
stdout(`locked: ${targetWorktree.locked.value ? "true" : "false"}`);
|
|
3218
4207
|
return EXIT_CODE.OK;
|
|
3219
|
-
}
|
|
3220
|
-
|
|
4208
|
+
};
|
|
4209
|
+
const handlePath = async () => {
|
|
3221
4210
|
ensureArgumentCount({
|
|
3222
4211
|
command,
|
|
3223
4212
|
args: commandArgs,
|
|
@@ -3243,8 +4232,18 @@ const createCli = (options = {}) => {
|
|
|
3243
4232
|
}
|
|
3244
4233
|
stdout(target.path);
|
|
3245
4234
|
return EXIT_CODE.OK;
|
|
3246
|
-
}
|
|
3247
|
-
|
|
4235
|
+
};
|
|
4236
|
+
const earlyRepoExitCode = await dispatchCommandHandler({
|
|
4237
|
+
command,
|
|
4238
|
+
handlers: createEarlyRepoCommandHandlers({
|
|
4239
|
+
initHandler: handleInit,
|
|
4240
|
+
listHandler: handleList,
|
|
4241
|
+
statusHandler: handleStatus,
|
|
4242
|
+
pathHandler: handlePath
|
|
4243
|
+
})
|
|
4244
|
+
});
|
|
4245
|
+
if (earlyRepoExitCode !== void 0) return earlyRepoExitCode;
|
|
4246
|
+
const handleNew = async () => {
|
|
3248
4247
|
ensureArgumentCount({
|
|
3249
4248
|
command,
|
|
3250
4249
|
args: commandArgs,
|
|
@@ -3252,58 +4251,56 @@ const createCli = (options = {}) => {
|
|
|
3252
4251
|
max: 1
|
|
3253
4252
|
});
|
|
3254
4253
|
const branch = commandArgs[0] ?? randomWipBranchName();
|
|
4254
|
+
const targetPath = branchToWorktreePath(repoRoot, branch, resolvedConfig.paths.worktreeRoot);
|
|
3255
4255
|
const result = await runWriteOperation(async () => {
|
|
3256
|
-
|
|
3257
|
-
branch,
|
|
3258
|
-
worktrees: (await collectWorktreeSnapshot$1(repoRoot)).worktrees
|
|
3259
|
-
})) throw createCliError("BRANCH_ALREADY_ATTACHED", {
|
|
3260
|
-
message: `Branch is already attached to a worktree: ${branch}`,
|
|
3261
|
-
details: { branch }
|
|
3262
|
-
});
|
|
3263
|
-
if (await doesGitRefExist(repoRoot, `refs/heads/${branch}`)) throw createCliError("BRANCH_ALREADY_EXISTS", {
|
|
3264
|
-
message: `Branch already exists locally: ${branch}`,
|
|
3265
|
-
details: { branch }
|
|
3266
|
-
});
|
|
3267
|
-
const targetPath = branchToWorktreePath(repoRoot, branch);
|
|
3268
|
-
await ensureTargetPathWritable(targetPath);
|
|
3269
|
-
const baseBranch = await resolveBaseBranch(repoRoot);
|
|
3270
|
-
const hookContext = createHookContext({
|
|
3271
|
-
runtime,
|
|
3272
|
-
repoRoot,
|
|
3273
|
-
action: "new",
|
|
3274
|
-
branch,
|
|
3275
|
-
worktreePath: targetPath,
|
|
3276
|
-
stderr
|
|
3277
|
-
});
|
|
3278
|
-
await runPreHook({
|
|
4256
|
+
return executeWorktreeMutation({
|
|
3279
4257
|
name: "new",
|
|
3280
|
-
context: hookContext
|
|
3281
|
-
});
|
|
3282
|
-
await runGitCommand({
|
|
3283
|
-
cwd: repoRoot,
|
|
3284
|
-
args: [
|
|
3285
|
-
"worktree",
|
|
3286
|
-
"add",
|
|
3287
|
-
"-b",
|
|
3288
|
-
branch,
|
|
3289
|
-
targetPath,
|
|
3290
|
-
baseBranch
|
|
3291
|
-
]
|
|
3292
|
-
});
|
|
3293
|
-
await upsertWorktreeMergeLifecycle({
|
|
3294
|
-
repoRoot,
|
|
3295
4258
|
branch,
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
4259
|
+
worktreePath: targetPath,
|
|
4260
|
+
precheck: async () => {
|
|
4261
|
+
if (containsBranch({
|
|
4262
|
+
branch,
|
|
4263
|
+
worktrees: (await collectWorktreeSnapshot$1(repoRoot)).worktrees
|
|
4264
|
+
})) throw createCliError("BRANCH_ALREADY_ATTACHED", {
|
|
4265
|
+
message: `Branch is already attached to a worktree: ${branch}`,
|
|
4266
|
+
details: { branch }
|
|
4267
|
+
});
|
|
4268
|
+
if (await doesGitRefExist(repoRoot, `refs/heads/${branch}`)) throw createCliError("BRANCH_ALREADY_EXISTS", {
|
|
4269
|
+
message: `Branch already exists locally: ${branch}`,
|
|
4270
|
+
details: { branch }
|
|
4271
|
+
});
|
|
4272
|
+
await ensureTargetPathWritable(targetPath);
|
|
4273
|
+
return { baseBranch: await resolveBaseBranch({
|
|
4274
|
+
repoRoot,
|
|
4275
|
+
config: resolvedConfig
|
|
4276
|
+
}) };
|
|
4277
|
+
},
|
|
4278
|
+
runGit: async ({ baseBranch }) => {
|
|
4279
|
+
await runGitCommand({
|
|
4280
|
+
cwd: repoRoot,
|
|
4281
|
+
args: [
|
|
4282
|
+
"worktree",
|
|
4283
|
+
"add",
|
|
4284
|
+
"-b",
|
|
4285
|
+
branch,
|
|
4286
|
+
targetPath,
|
|
4287
|
+
baseBranch
|
|
4288
|
+
]
|
|
4289
|
+
});
|
|
4290
|
+
return {
|
|
4291
|
+
branch,
|
|
4292
|
+
path: targetPath
|
|
4293
|
+
};
|
|
4294
|
+
},
|
|
4295
|
+
finalize: async ({ baseBranch }) => {
|
|
4296
|
+
await upsertWorktreeMergeLifecycle({
|
|
4297
|
+
repoRoot,
|
|
4298
|
+
branch,
|
|
4299
|
+
baseBranch,
|
|
4300
|
+
observedDivergedHead: null
|
|
4301
|
+
});
|
|
4302
|
+
}
|
|
3302
4303
|
});
|
|
3303
|
-
return {
|
|
3304
|
-
branch,
|
|
3305
|
-
path: targetPath
|
|
3306
|
-
};
|
|
3307
4304
|
});
|
|
3308
4305
|
if (runtime.json) {
|
|
3309
4306
|
stdout(JSON.stringify(buildJsonSuccess({
|
|
@@ -3316,8 +4313,8 @@ const createCli = (options = {}) => {
|
|
|
3316
4313
|
}
|
|
3317
4314
|
stdout(result.path);
|
|
3318
4315
|
return EXIT_CODE.OK;
|
|
3319
|
-
}
|
|
3320
|
-
|
|
4316
|
+
};
|
|
4317
|
+
const handleSwitch = async () => {
|
|
3321
4318
|
ensureArgumentCount({
|
|
3322
4319
|
command,
|
|
3323
4320
|
args: commandArgs,
|
|
@@ -3327,74 +4324,72 @@ const createCli = (options = {}) => {
|
|
|
3327
4324
|
const branch = commandArgs[0];
|
|
3328
4325
|
const result = await runWriteOperation(async () => {
|
|
3329
4326
|
const snapshot = await collectWorktreeSnapshot$1(repoRoot);
|
|
3330
|
-
const existing = snapshot.worktrees.find((worktree) => worktree.branch === branch);
|
|
3331
|
-
if (existing !== void 0) {
|
|
3332
|
-
if (snapshot.baseBranch !== null) await upsertWorktreeMergeLifecycle({
|
|
3333
|
-
repoRoot,
|
|
3334
|
-
branch,
|
|
3335
|
-
baseBranch: snapshot.baseBranch,
|
|
3336
|
-
observedDivergedHead: null
|
|
3337
|
-
});
|
|
3338
|
-
return {
|
|
3339
|
-
status: "existing",
|
|
3340
|
-
branch,
|
|
3341
|
-
path: existing.path
|
|
3342
|
-
};
|
|
3343
|
-
}
|
|
3344
|
-
const targetPath = branchToWorktreePath(repoRoot, branch);
|
|
3345
|
-
await ensureTargetPathWritable(targetPath);
|
|
3346
|
-
const hookContext = createHookContext({
|
|
3347
|
-
runtime,
|
|
3348
|
-
repoRoot,
|
|
3349
|
-
action: "switch",
|
|
3350
|
-
branch,
|
|
3351
|
-
worktreePath: targetPath,
|
|
3352
|
-
stderr
|
|
3353
|
-
});
|
|
3354
|
-
await runPreHook({
|
|
3355
|
-
name: "switch",
|
|
3356
|
-
context: hookContext
|
|
3357
|
-
});
|
|
3358
|
-
let lifecycleBaseBranch = snapshot.baseBranch;
|
|
3359
|
-
if (await doesGitRefExist(repoRoot, `refs/heads/${branch}`)) await runGitCommand({
|
|
3360
|
-
cwd: repoRoot,
|
|
3361
|
-
args: [
|
|
3362
|
-
"worktree",
|
|
3363
|
-
"add",
|
|
3364
|
-
targetPath,
|
|
3365
|
-
branch
|
|
3366
|
-
]
|
|
3367
|
-
});
|
|
3368
|
-
else {
|
|
3369
|
-
const baseBranch = await resolveBaseBranch(repoRoot);
|
|
3370
|
-
lifecycleBaseBranch = baseBranch;
|
|
3371
|
-
await runGitCommand({
|
|
3372
|
-
cwd: repoRoot,
|
|
3373
|
-
args: [
|
|
3374
|
-
"worktree",
|
|
3375
|
-
"add",
|
|
3376
|
-
"-b",
|
|
3377
|
-
branch,
|
|
3378
|
-
targetPath,
|
|
3379
|
-
baseBranch
|
|
3380
|
-
]
|
|
4327
|
+
const existing = snapshot.worktrees.find((worktree) => worktree.branch === branch);
|
|
4328
|
+
if (existing !== void 0) {
|
|
4329
|
+
if (snapshot.baseBranch !== null) await upsertWorktreeMergeLifecycle({
|
|
4330
|
+
repoRoot,
|
|
4331
|
+
branch,
|
|
4332
|
+
baseBranch: snapshot.baseBranch,
|
|
4333
|
+
observedDivergedHead: null
|
|
3381
4334
|
});
|
|
4335
|
+
return {
|
|
4336
|
+
status: "existing",
|
|
4337
|
+
branch,
|
|
4338
|
+
path: existing.path
|
|
4339
|
+
};
|
|
3382
4340
|
}
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
branch,
|
|
3386
|
-
baseBranch: lifecycleBaseBranch,
|
|
3387
|
-
observedDivergedHead: null
|
|
3388
|
-
});
|
|
3389
|
-
await runPostHook({
|
|
4341
|
+
const targetPath = branchToWorktreePath(repoRoot, branch, resolvedConfig.paths.worktreeRoot);
|
|
4342
|
+
return executeWorktreeMutation({
|
|
3390
4343
|
name: "switch",
|
|
3391
|
-
context: hookContext
|
|
3392
|
-
});
|
|
3393
|
-
return {
|
|
3394
|
-
status: "created",
|
|
3395
4344
|
branch,
|
|
3396
|
-
|
|
3397
|
-
|
|
4345
|
+
worktreePath: targetPath,
|
|
4346
|
+
precheck: async () => {
|
|
4347
|
+
await ensureTargetPathWritable(targetPath);
|
|
4348
|
+
if (await doesGitRefExist(repoRoot, `refs/heads/${branch}`)) return {
|
|
4349
|
+
gitArgs: [
|
|
4350
|
+
"worktree",
|
|
4351
|
+
"add",
|
|
4352
|
+
targetPath,
|
|
4353
|
+
branch
|
|
4354
|
+
],
|
|
4355
|
+
lifecycleBaseBranch: snapshot.baseBranch
|
|
4356
|
+
};
|
|
4357
|
+
const baseBranch = await resolveBaseBranch({
|
|
4358
|
+
repoRoot,
|
|
4359
|
+
config: resolvedConfig
|
|
4360
|
+
});
|
|
4361
|
+
return {
|
|
4362
|
+
gitArgs: [
|
|
4363
|
+
"worktree",
|
|
4364
|
+
"add",
|
|
4365
|
+
"-b",
|
|
4366
|
+
branch,
|
|
4367
|
+
targetPath,
|
|
4368
|
+
baseBranch
|
|
4369
|
+
],
|
|
4370
|
+
lifecycleBaseBranch: baseBranch
|
|
4371
|
+
};
|
|
4372
|
+
},
|
|
4373
|
+
runGit: async ({ gitArgs }) => {
|
|
4374
|
+
await runGitCommand({
|
|
4375
|
+
cwd: repoRoot,
|
|
4376
|
+
args: [...gitArgs]
|
|
4377
|
+
});
|
|
4378
|
+
return {
|
|
4379
|
+
status: "created",
|
|
4380
|
+
branch,
|
|
4381
|
+
path: targetPath
|
|
4382
|
+
};
|
|
4383
|
+
},
|
|
4384
|
+
finalize: async ({ lifecycleBaseBranch }) => {
|
|
4385
|
+
if (lifecycleBaseBranch !== null) await upsertWorktreeMergeLifecycle({
|
|
4386
|
+
repoRoot,
|
|
4387
|
+
branch,
|
|
4388
|
+
baseBranch: lifecycleBaseBranch,
|
|
4389
|
+
observedDivergedHead: null
|
|
4390
|
+
});
|
|
4391
|
+
}
|
|
4392
|
+
});
|
|
3398
4393
|
});
|
|
3399
4394
|
if (runtime.json) {
|
|
3400
4395
|
stdout(JSON.stringify(buildJsonSuccess({
|
|
@@ -3410,8 +4405,16 @@ const createCli = (options = {}) => {
|
|
|
3410
4405
|
}
|
|
3411
4406
|
stdout(result.path);
|
|
3412
4407
|
return EXIT_CODE.OK;
|
|
3413
|
-
}
|
|
3414
|
-
|
|
4408
|
+
};
|
|
4409
|
+
const writeCommandExitCode = await dispatchCommandHandler({
|
|
4410
|
+
command,
|
|
4411
|
+
handlers: createWriteCommandHandlers({
|
|
4412
|
+
newHandler: handleNew,
|
|
4413
|
+
switchHandler: handleSwitch
|
|
4414
|
+
})
|
|
4415
|
+
});
|
|
4416
|
+
if (writeCommandExitCode !== void 0) return writeCommandExitCode;
|
|
4417
|
+
const handleMv = async () => {
|
|
3415
4418
|
ensureArgumentCount({
|
|
3416
4419
|
command,
|
|
3417
4420
|
args: commandArgs,
|
|
@@ -3438,68 +4441,68 @@ const createCli = (options = {}) => {
|
|
|
3438
4441
|
branch: newBranch,
|
|
3439
4442
|
path: current.path
|
|
3440
4443
|
};
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
})) throw createCliError("BRANCH_ALREADY_ATTACHED", {
|
|
3445
|
-
message: `Branch is already attached to another worktree: ${newBranch}`,
|
|
3446
|
-
details: { branch: newBranch }
|
|
3447
|
-
});
|
|
3448
|
-
if (await doesGitRefExist(repoRoot, `refs/heads/${newBranch}`)) throw createCliError("BRANCH_ALREADY_EXISTS", {
|
|
3449
|
-
message: `Branch already exists locally: ${newBranch}`,
|
|
3450
|
-
details: { branch: newBranch }
|
|
3451
|
-
});
|
|
3452
|
-
const newPath = branchToWorktreePath(repoRoot, newBranch);
|
|
3453
|
-
await ensureTargetPathWritable(newPath);
|
|
3454
|
-
const hookContext = createHookContext({
|
|
3455
|
-
runtime,
|
|
3456
|
-
repoRoot,
|
|
3457
|
-
action: "mv",
|
|
4444
|
+
const newPath = branchToWorktreePath(repoRoot, newBranch, resolvedConfig.paths.worktreeRoot);
|
|
4445
|
+
return executeWorktreeMutation({
|
|
4446
|
+
name: "mv",
|
|
3458
4447
|
branch: newBranch,
|
|
3459
4448
|
worktreePath: newPath,
|
|
3460
|
-
stderr,
|
|
3461
4449
|
extraEnv: {
|
|
3462
4450
|
WT_OLD_BRANCH: oldBranch,
|
|
3463
4451
|
WT_NEW_BRANCH: newBranch
|
|
4452
|
+
},
|
|
4453
|
+
precheck: async () => {
|
|
4454
|
+
if (containsBranch({
|
|
4455
|
+
branch: newBranch,
|
|
4456
|
+
worktrees: snapshot.worktrees
|
|
4457
|
+
})) throw createCliError("BRANCH_ALREADY_ATTACHED", {
|
|
4458
|
+
message: `Branch is already attached to another worktree: ${newBranch}`,
|
|
4459
|
+
details: { branch: newBranch }
|
|
4460
|
+
});
|
|
4461
|
+
if (await doesGitRefExist(repoRoot, `refs/heads/${newBranch}`)) throw createCliError("BRANCH_ALREADY_EXISTS", {
|
|
4462
|
+
message: `Branch already exists locally: ${newBranch}`,
|
|
4463
|
+
details: { branch: newBranch }
|
|
4464
|
+
});
|
|
4465
|
+
await ensureTargetPathWritable(newPath);
|
|
4466
|
+
return {
|
|
4467
|
+
oldBranch,
|
|
4468
|
+
currentPath: current.path,
|
|
4469
|
+
baseBranch: snapshot.baseBranch
|
|
4470
|
+
};
|
|
4471
|
+
},
|
|
4472
|
+
runGit: async ({ oldBranch: resolvedOldBranch, currentPath }) => {
|
|
4473
|
+
await runGitCommand({
|
|
4474
|
+
cwd: currentPath,
|
|
4475
|
+
args: [
|
|
4476
|
+
"branch",
|
|
4477
|
+
"-m",
|
|
4478
|
+
resolvedOldBranch,
|
|
4479
|
+
newBranch
|
|
4480
|
+
]
|
|
4481
|
+
});
|
|
4482
|
+
await runGitCommand({
|
|
4483
|
+
cwd: repoRoot,
|
|
4484
|
+
args: [
|
|
4485
|
+
"worktree",
|
|
4486
|
+
"move",
|
|
4487
|
+
currentPath,
|
|
4488
|
+
newPath
|
|
4489
|
+
]
|
|
4490
|
+
});
|
|
4491
|
+
return {
|
|
4492
|
+
branch: newBranch,
|
|
4493
|
+
path: newPath
|
|
4494
|
+
};
|
|
4495
|
+
},
|
|
4496
|
+
finalize: async ({ oldBranch: resolvedOldBranch, baseBranch }) => {
|
|
4497
|
+
if (baseBranch !== null) await moveWorktreeMergeLifecycle({
|
|
4498
|
+
repoRoot,
|
|
4499
|
+
fromBranch: resolvedOldBranch,
|
|
4500
|
+
toBranch: newBranch,
|
|
4501
|
+
baseBranch,
|
|
4502
|
+
observedDivergedHead: null
|
|
4503
|
+
});
|
|
3464
4504
|
}
|
|
3465
4505
|
});
|
|
3466
|
-
await runPreHook({
|
|
3467
|
-
name: "mv",
|
|
3468
|
-
context: hookContext
|
|
3469
|
-
});
|
|
3470
|
-
await runGitCommand({
|
|
3471
|
-
cwd: current.path,
|
|
3472
|
-
args: [
|
|
3473
|
-
"branch",
|
|
3474
|
-
"-m",
|
|
3475
|
-
oldBranch,
|
|
3476
|
-
newBranch
|
|
3477
|
-
]
|
|
3478
|
-
});
|
|
3479
|
-
await runGitCommand({
|
|
3480
|
-
cwd: repoRoot,
|
|
3481
|
-
args: [
|
|
3482
|
-
"worktree",
|
|
3483
|
-
"move",
|
|
3484
|
-
current.path,
|
|
3485
|
-
newPath
|
|
3486
|
-
]
|
|
3487
|
-
});
|
|
3488
|
-
if (snapshot.baseBranch !== null) await moveWorktreeMergeLifecycle({
|
|
3489
|
-
repoRoot,
|
|
3490
|
-
fromBranch: oldBranch,
|
|
3491
|
-
toBranch: newBranch,
|
|
3492
|
-
baseBranch: snapshot.baseBranch,
|
|
3493
|
-
observedDivergedHead: null
|
|
3494
|
-
});
|
|
3495
|
-
await runPostHook({
|
|
3496
|
-
name: "mv",
|
|
3497
|
-
context: hookContext
|
|
3498
|
-
});
|
|
3499
|
-
return {
|
|
3500
|
-
branch: newBranch,
|
|
3501
|
-
path: newPath
|
|
3502
|
-
};
|
|
3503
4506
|
});
|
|
3504
4507
|
if (runtime.json) {
|
|
3505
4508
|
stdout(JSON.stringify(buildJsonSuccess({
|
|
@@ -3512,8 +4515,8 @@ const createCli = (options = {}) => {
|
|
|
3512
4515
|
}
|
|
3513
4516
|
stdout(result.path);
|
|
3514
4517
|
return EXIT_CODE.OK;
|
|
3515
|
-
}
|
|
3516
|
-
|
|
4518
|
+
};
|
|
4519
|
+
const handleDel = async () => {
|
|
3517
4520
|
ensureArgumentCount({
|
|
3518
4521
|
command,
|
|
3519
4522
|
args: commandArgs,
|
|
@@ -3543,56 +4546,69 @@ const createCli = (options = {}) => {
|
|
|
3543
4546
|
message: "Cannot delete the primary worktree",
|
|
3544
4547
|
details: { path: target.path }
|
|
3545
4548
|
});
|
|
3546
|
-
|
|
3547
|
-
target,
|
|
3548
|
-
forceFlags
|
|
3549
|
-
});
|
|
3550
|
-
const hookContext = createHookContext({
|
|
3551
|
-
runtime,
|
|
3552
|
-
repoRoot,
|
|
3553
|
-
action: "del",
|
|
3554
|
-
branch: target.branch,
|
|
4549
|
+
if (isManagedWorktreePath({
|
|
3555
4550
|
worktreePath: target.path,
|
|
3556
|
-
|
|
3557
|
-
})
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
"remove",
|
|
3565
|
-
target.path
|
|
3566
|
-
];
|
|
3567
|
-
if (forceFlags.forceDirty) removeArgs.push("--force");
|
|
3568
|
-
await runGitCommand({
|
|
3569
|
-
cwd: repoRoot,
|
|
3570
|
-
args: removeArgs
|
|
3571
|
-
});
|
|
3572
|
-
await runGitCommand({
|
|
3573
|
-
cwd: repoRoot,
|
|
3574
|
-
args: [
|
|
3575
|
-
"branch",
|
|
3576
|
-
resolveBranchDeleteMode(forceFlags),
|
|
3577
|
-
target.branch
|
|
3578
|
-
]
|
|
3579
|
-
});
|
|
3580
|
-
await deleteWorktreeLock({
|
|
3581
|
-
repoRoot,
|
|
3582
|
-
branch: target.branch
|
|
3583
|
-
});
|
|
3584
|
-
await deleteWorktreeMergeLifecycle({
|
|
3585
|
-
repoRoot,
|
|
3586
|
-
branch: target.branch
|
|
4551
|
+
managedWorktreeRoot
|
|
4552
|
+
}) !== true) throw createCliError("WORKTREE_NOT_FOUND", {
|
|
4553
|
+
message: "Target branch is not in managed worktree root",
|
|
4554
|
+
details: {
|
|
4555
|
+
branch: target.branch,
|
|
4556
|
+
path: target.path,
|
|
4557
|
+
managedWorktreeRoot
|
|
4558
|
+
}
|
|
3587
4559
|
});
|
|
3588
|
-
|
|
4560
|
+
const targetBranch = target.branch;
|
|
4561
|
+
return executeWorktreeMutation({
|
|
3589
4562
|
name: "del",
|
|
3590
|
-
|
|
4563
|
+
branch: targetBranch,
|
|
4564
|
+
worktreePath: target.path,
|
|
4565
|
+
precheck: async () => {
|
|
4566
|
+
validateDeleteSafety({
|
|
4567
|
+
target,
|
|
4568
|
+
forceFlags
|
|
4569
|
+
});
|
|
4570
|
+
const removeArgs = [
|
|
4571
|
+
"worktree",
|
|
4572
|
+
"remove",
|
|
4573
|
+
target.path
|
|
4574
|
+
];
|
|
4575
|
+
if (forceFlags.forceDirty) removeArgs.push("--force");
|
|
4576
|
+
return {
|
|
4577
|
+
branch: targetBranch,
|
|
4578
|
+
path: target.path,
|
|
4579
|
+
removeArgs,
|
|
4580
|
+
branchDeleteMode: resolveBranchDeleteMode(forceFlags)
|
|
4581
|
+
};
|
|
4582
|
+
},
|
|
4583
|
+
runGit: async ({ branch: targetBranch, removeArgs, branchDeleteMode, path }) => {
|
|
4584
|
+
await runGitCommand({
|
|
4585
|
+
cwd: repoRoot,
|
|
4586
|
+
args: removeArgs
|
|
4587
|
+
});
|
|
4588
|
+
await runGitCommand({
|
|
4589
|
+
cwd: repoRoot,
|
|
4590
|
+
args: [
|
|
4591
|
+
"branch",
|
|
4592
|
+
branchDeleteMode,
|
|
4593
|
+
targetBranch
|
|
4594
|
+
]
|
|
4595
|
+
});
|
|
4596
|
+
return {
|
|
4597
|
+
branch: targetBranch,
|
|
4598
|
+
path
|
|
4599
|
+
};
|
|
4600
|
+
},
|
|
4601
|
+
finalize: async ({ branch: targetBranch }) => {
|
|
4602
|
+
await deleteWorktreeLock({
|
|
4603
|
+
repoRoot,
|
|
4604
|
+
branch: targetBranch
|
|
4605
|
+
});
|
|
4606
|
+
await deleteWorktreeMergeLifecycle({
|
|
4607
|
+
repoRoot,
|
|
4608
|
+
branch: targetBranch
|
|
4609
|
+
});
|
|
4610
|
+
}
|
|
3591
4611
|
});
|
|
3592
|
-
return {
|
|
3593
|
-
branch: target.branch,
|
|
3594
|
-
path: target.path
|
|
3595
|
-
};
|
|
3596
4612
|
});
|
|
3597
4613
|
if (runtime.json) {
|
|
3598
4614
|
stdout(JSON.stringify(buildJsonSuccess({
|
|
@@ -3605,8 +4621,16 @@ const createCli = (options = {}) => {
|
|
|
3605
4621
|
}
|
|
3606
4622
|
stdout(result.path);
|
|
3607
4623
|
return EXIT_CODE.OK;
|
|
3608
|
-
}
|
|
3609
|
-
|
|
4624
|
+
};
|
|
4625
|
+
const writeMutationExitCode = await dispatchCommandHandler({
|
|
4626
|
+
command,
|
|
4627
|
+
handlers: createWriteMutationHandlers({
|
|
4628
|
+
mvHandler: handleMv,
|
|
4629
|
+
delHandler: handleDel
|
|
4630
|
+
})
|
|
4631
|
+
});
|
|
4632
|
+
if (writeMutationExitCode !== void 0) return writeMutationExitCode;
|
|
4633
|
+
const handleGone = async () => {
|
|
3610
4634
|
ensureArgumentCount({
|
|
3611
4635
|
command,
|
|
3612
4636
|
args: commandArgs,
|
|
@@ -3616,7 +4640,10 @@ const createCli = (options = {}) => {
|
|
|
3616
4640
|
if (parsedArgs.apply === true && parsedArgs.dryRun === true) throw createCliError("INVALID_ARGUMENT", { message: "Cannot use --apply and --dry-run together" });
|
|
3617
4641
|
const dryRun = parsedArgs.apply !== true;
|
|
3618
4642
|
const execute = async () => {
|
|
3619
|
-
const candidates = (await collectWorktreeSnapshot$1(repoRoot)).worktrees.filter((worktree) => worktree.branch !== null).filter((worktree) => worktree.path !== repoRoot).filter((worktree) =>
|
|
4643
|
+
const candidates = (await collectWorktreeSnapshot$1(repoRoot)).worktrees.filter((worktree) => worktree.branch !== null).filter((worktree) => worktree.path !== repoRoot).filter((worktree) => isManagedWorktreePath({
|
|
4644
|
+
worktreePath: worktree.path,
|
|
4645
|
+
managedWorktreeRoot
|
|
4646
|
+
})).filter((worktree) => worktree.dirty === false).filter((worktree) => worktree.locked.value === false).filter((worktree) => worktree.merged.overall === true).map((worktree) => worktree.branch);
|
|
3620
4647
|
if (dryRun) return {
|
|
3621
4648
|
deleted: [],
|
|
3622
4649
|
candidates,
|
|
@@ -3693,8 +4720,8 @@ const createCli = (options = {}) => {
|
|
|
3693
4720
|
const branches = result.dryRun ? result.candidates : result.deleted;
|
|
3694
4721
|
for (const branch of branches) stdout(`${label}: ${branch}`);
|
|
3695
4722
|
return EXIT_CODE.OK;
|
|
3696
|
-
}
|
|
3697
|
-
|
|
4723
|
+
};
|
|
4724
|
+
const handleGet = async () => {
|
|
3698
4725
|
ensureArgumentCount({
|
|
3699
4726
|
command,
|
|
3700
4727
|
args: commandArgs,
|
|
@@ -3721,7 +4748,7 @@ const createCli = (options = {}) => {
|
|
|
3721
4748
|
repoRoot,
|
|
3722
4749
|
action: "get",
|
|
3723
4750
|
branch,
|
|
3724
|
-
worktreePath: branchToWorktreePath(repoRoot, branch),
|
|
4751
|
+
worktreePath: branchToWorktreePath(repoRoot, branch, resolvedConfig.paths.worktreeRoot),
|
|
3725
4752
|
stderr
|
|
3726
4753
|
});
|
|
3727
4754
|
await runPreHook({
|
|
@@ -3772,7 +4799,7 @@ const createCli = (options = {}) => {
|
|
|
3772
4799
|
path: existing.path
|
|
3773
4800
|
};
|
|
3774
4801
|
}
|
|
3775
|
-
const targetPath = branchToWorktreePath(repoRoot, branch);
|
|
4802
|
+
const targetPath = branchToWorktreePath(repoRoot, branch, resolvedConfig.paths.worktreeRoot);
|
|
3776
4803
|
await ensureTargetPathWritable(targetPath);
|
|
3777
4804
|
await runGitCommand({
|
|
3778
4805
|
cwd: repoRoot,
|
|
@@ -3813,8 +4840,8 @@ const createCli = (options = {}) => {
|
|
|
3813
4840
|
}
|
|
3814
4841
|
stdout(result.path);
|
|
3815
4842
|
return EXIT_CODE.OK;
|
|
3816
|
-
}
|
|
3817
|
-
|
|
4843
|
+
};
|
|
4844
|
+
const handleExtract = async () => {
|
|
3818
4845
|
ensureArgumentCount({
|
|
3819
4846
|
command,
|
|
3820
4847
|
args: commandArgs,
|
|
@@ -3846,12 +4873,15 @@ const createCli = (options = {}) => {
|
|
|
3846
4873
|
details: { path: sourceWorktree.path }
|
|
3847
4874
|
});
|
|
3848
4875
|
const branch = sourceWorktree.branch;
|
|
3849
|
-
const baseBranch = await resolveBaseBranch(
|
|
4876
|
+
const baseBranch = await resolveBaseBranch({
|
|
4877
|
+
repoRoot,
|
|
4878
|
+
config: resolvedConfig
|
|
4879
|
+
});
|
|
3850
4880
|
ensureBranchIsNotPrimary({
|
|
3851
4881
|
branch,
|
|
3852
4882
|
baseBranch
|
|
3853
4883
|
});
|
|
3854
|
-
const targetPath = branchToWorktreePath(repoRoot, branch);
|
|
4884
|
+
const targetPath = branchToWorktreePath(repoRoot, branch, resolvedConfig.paths.worktreeRoot);
|
|
3855
4885
|
await ensureTargetPathWritable(targetPath);
|
|
3856
4886
|
const dirty = (await runGitCommand({
|
|
3857
4887
|
cwd: repoRoot,
|
|
@@ -3943,8 +4973,17 @@ const createCli = (options = {}) => {
|
|
|
3943
4973
|
}
|
|
3944
4974
|
stdout(result.path);
|
|
3945
4975
|
return EXIT_CODE.OK;
|
|
3946
|
-
}
|
|
3947
|
-
|
|
4976
|
+
};
|
|
4977
|
+
const worktreeActionExitCode = await dispatchCommandHandler({
|
|
4978
|
+
command,
|
|
4979
|
+
handlers: createWorktreeActionHandlers({
|
|
4980
|
+
goneHandler: handleGone,
|
|
4981
|
+
getHandler: handleGet,
|
|
4982
|
+
extractHandler: handleExtract
|
|
4983
|
+
})
|
|
4984
|
+
});
|
|
4985
|
+
if (worktreeActionExitCode !== void 0) return worktreeActionExitCode;
|
|
4986
|
+
const handleAbsorb = async () => {
|
|
3948
4987
|
ensureArgumentCount({
|
|
3949
4988
|
command,
|
|
3950
4989
|
args: commandArgs,
|
|
@@ -3972,6 +5011,7 @@ const createCli = (options = {}) => {
|
|
|
3972
5011
|
});
|
|
3973
5012
|
const sourceWorktree = resolveManagedNonPrimaryWorktreeByBranch({
|
|
3974
5013
|
repoRoot,
|
|
5014
|
+
managedWorktreeRoot,
|
|
3975
5015
|
branch,
|
|
3976
5016
|
worktrees: (await collectWorktreeSnapshot$1(repoRoot)).worktrees,
|
|
3977
5017
|
optionName: "--from",
|
|
@@ -4065,8 +5105,8 @@ const createCli = (options = {}) => {
|
|
|
4065
5105
|
}
|
|
4066
5106
|
stdout(result.path);
|
|
4067
5107
|
return EXIT_CODE.OK;
|
|
4068
|
-
}
|
|
4069
|
-
|
|
5108
|
+
};
|
|
5109
|
+
const handleUnabsorb = async () => {
|
|
4070
5110
|
ensureArgumentCount({
|
|
4071
5111
|
command,
|
|
4072
5112
|
args: commandArgs,
|
|
@@ -4106,6 +5146,7 @@ const createCli = (options = {}) => {
|
|
|
4106
5146
|
});
|
|
4107
5147
|
const targetWorktree = resolveManagedNonPrimaryWorktreeByBranch({
|
|
4108
5148
|
repoRoot,
|
|
5149
|
+
managedWorktreeRoot,
|
|
4109
5150
|
branch,
|
|
4110
5151
|
worktrees: (await collectWorktreeSnapshot$1(repoRoot)).worktrees,
|
|
4111
5152
|
optionName: "--to",
|
|
@@ -4197,8 +5238,8 @@ const createCli = (options = {}) => {
|
|
|
4197
5238
|
}
|
|
4198
5239
|
stdout(result.path);
|
|
4199
5240
|
return EXIT_CODE.OK;
|
|
4200
|
-
}
|
|
4201
|
-
|
|
5241
|
+
};
|
|
5242
|
+
const handleUse = async () => {
|
|
4202
5243
|
ensureArgumentCount({
|
|
4203
5244
|
command,
|
|
4204
5245
|
args: commandArgs,
|
|
@@ -4291,8 +5332,17 @@ const createCli = (options = {}) => {
|
|
|
4291
5332
|
}
|
|
4292
5333
|
stdout(result.path);
|
|
4293
5334
|
return EXIT_CODE.OK;
|
|
4294
|
-
}
|
|
4295
|
-
|
|
5335
|
+
};
|
|
5336
|
+
const synchronizationExitCode = await dispatchCommandHandler({
|
|
5337
|
+
command,
|
|
5338
|
+
handlers: createSynchronizationHandlers({
|
|
5339
|
+
absorbHandler: handleAbsorb,
|
|
5340
|
+
unabsorbHandler: handleUnabsorb,
|
|
5341
|
+
useHandler: handleUse
|
|
5342
|
+
})
|
|
5343
|
+
});
|
|
5344
|
+
if (synchronizationExitCode !== void 0) return synchronizationExitCode;
|
|
5345
|
+
const handleExec = async () => {
|
|
4296
5346
|
ensureArgumentCount({
|
|
4297
5347
|
command,
|
|
4298
5348
|
args: commandArgs,
|
|
@@ -4347,8 +5397,8 @@ const createCli = (options = {}) => {
|
|
|
4347
5397
|
return EXIT_CODE.CHILD_PROCESS_FAILED;
|
|
4348
5398
|
}
|
|
4349
5399
|
return childExitCode === 0 ? EXIT_CODE.OK : EXIT_CODE.CHILD_PROCESS_FAILED;
|
|
4350
|
-
}
|
|
4351
|
-
|
|
5400
|
+
};
|
|
5401
|
+
const handleInvoke = async () => {
|
|
4352
5402
|
ensureArgumentCount({
|
|
4353
5403
|
command,
|
|
4354
5404
|
args: commandArgs,
|
|
@@ -4372,38 +5422,32 @@ const createCli = (options = {}) => {
|
|
|
4372
5422
|
stderr
|
|
4373
5423
|
})
|
|
4374
5424
|
});
|
|
4375
|
-
if (runtime.json) {
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
})));
|
|
4385
|
-
return EXIT_CODE.OK;
|
|
4386
|
-
}
|
|
5425
|
+
if (runtime.json) stdout(JSON.stringify(buildJsonSuccess({
|
|
5426
|
+
command,
|
|
5427
|
+
status: "ok",
|
|
5428
|
+
repoRoot,
|
|
5429
|
+
details: {
|
|
5430
|
+
hook: hookName,
|
|
5431
|
+
exitCode: 0
|
|
5432
|
+
}
|
|
5433
|
+
})));
|
|
4387
5434
|
return EXIT_CODE.OK;
|
|
4388
|
-
}
|
|
4389
|
-
|
|
5435
|
+
};
|
|
5436
|
+
const handleCopy = async () => {
|
|
4390
5437
|
ensureArgumentCount({
|
|
4391
5438
|
command,
|
|
4392
5439
|
args: commandArgs,
|
|
4393
5440
|
min: 1,
|
|
4394
5441
|
max: Number.MAX_SAFE_INTEGER
|
|
4395
5442
|
});
|
|
4396
|
-
const
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
cwd: repoContext.currentWorktreeRoot,
|
|
4400
|
-
path: process.env.WT_WORKTREE_PATH ?? "."
|
|
4401
|
-
})
|
|
5443
|
+
const targetWorktreeRoot = resolveTargetWorktreeRootForCopyLink({
|
|
5444
|
+
repoContext,
|
|
5445
|
+
snapshot: await collectWorktreeSnapshot$1(repoRoot)
|
|
4402
5446
|
});
|
|
4403
5447
|
for (const relativePath of commandArgs) {
|
|
4404
5448
|
const { sourcePath, destinationPath } = resolveFileCopyTargets({
|
|
4405
5449
|
repoRoot,
|
|
4406
|
-
|
|
5450
|
+
targetWorktreeRoot,
|
|
4407
5451
|
relativePath
|
|
4408
5452
|
});
|
|
4409
5453
|
await access(sourcePath, constants.F_OK);
|
|
@@ -4415,39 +5459,33 @@ const createCli = (options = {}) => {
|
|
|
4415
5459
|
dereference: false
|
|
4416
5460
|
});
|
|
4417
5461
|
}
|
|
4418
|
-
if (runtime.json) {
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
})));
|
|
4428
|
-
return EXIT_CODE.OK;
|
|
4429
|
-
}
|
|
5462
|
+
if (runtime.json) stdout(JSON.stringify(buildJsonSuccess({
|
|
5463
|
+
command,
|
|
5464
|
+
status: "ok",
|
|
5465
|
+
repoRoot,
|
|
5466
|
+
details: {
|
|
5467
|
+
copied: commandArgs,
|
|
5468
|
+
worktreePath: targetWorktreeRoot
|
|
5469
|
+
}
|
|
5470
|
+
})));
|
|
4430
5471
|
return EXIT_CODE.OK;
|
|
4431
|
-
}
|
|
4432
|
-
|
|
5472
|
+
};
|
|
5473
|
+
const handleLink = async () => {
|
|
4433
5474
|
ensureArgumentCount({
|
|
4434
5475
|
command,
|
|
4435
5476
|
args: commandArgs,
|
|
4436
5477
|
min: 1,
|
|
4437
5478
|
max: Number.MAX_SAFE_INTEGER
|
|
4438
5479
|
});
|
|
4439
|
-
const
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
cwd: repoContext.currentWorktreeRoot,
|
|
4443
|
-
path: process.env.WT_WORKTREE_PATH ?? "."
|
|
4444
|
-
})
|
|
5480
|
+
const targetWorktreeRoot = resolveTargetWorktreeRootForCopyLink({
|
|
5481
|
+
repoContext,
|
|
5482
|
+
snapshot: await collectWorktreeSnapshot$1(repoRoot)
|
|
4445
5483
|
});
|
|
4446
5484
|
const fallbackEnabled = parsedArgs.fallback !== false;
|
|
4447
5485
|
for (const relativePath of commandArgs) {
|
|
4448
5486
|
const { sourcePath, destinationPath } = resolveFileCopyTargets({
|
|
4449
5487
|
repoRoot,
|
|
4450
|
-
|
|
5488
|
+
targetWorktreeRoot,
|
|
4451
5489
|
relativePath
|
|
4452
5490
|
});
|
|
4453
5491
|
await access(sourcePath, constants.F_OK);
|
|
@@ -4478,22 +5516,19 @@ const createCli = (options = {}) => {
|
|
|
4478
5516
|
});
|
|
4479
5517
|
}
|
|
4480
5518
|
}
|
|
4481
|
-
if (runtime.json) {
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
})));
|
|
4492
|
-
return EXIT_CODE.OK;
|
|
4493
|
-
}
|
|
5519
|
+
if (runtime.json) stdout(JSON.stringify(buildJsonSuccess({
|
|
5520
|
+
command,
|
|
5521
|
+
status: "ok",
|
|
5522
|
+
repoRoot,
|
|
5523
|
+
details: {
|
|
5524
|
+
linked: commandArgs,
|
|
5525
|
+
worktreePath: targetWorktreeRoot,
|
|
5526
|
+
fallback: fallbackEnabled
|
|
5527
|
+
}
|
|
5528
|
+
})));
|
|
4494
5529
|
return EXIT_CODE.OK;
|
|
4495
|
-
}
|
|
4496
|
-
|
|
5530
|
+
};
|
|
5531
|
+
const handleLock = async () => {
|
|
4497
5532
|
ensureArgumentCount({
|
|
4498
5533
|
command,
|
|
4499
5534
|
args: commandArgs,
|
|
@@ -4535,25 +5570,22 @@ const createCli = (options = {}) => {
|
|
|
4535
5570
|
owner
|
|
4536
5571
|
});
|
|
4537
5572
|
});
|
|
4538
|
-
if (runtime.json) {
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
owner: result.owner
|
|
4549
|
-
}
|
|
5573
|
+
if (runtime.json) stdout(JSON.stringify(buildJsonSuccess({
|
|
5574
|
+
command,
|
|
5575
|
+
status: "ok",
|
|
5576
|
+
repoRoot,
|
|
5577
|
+
details: {
|
|
5578
|
+
branch,
|
|
5579
|
+
locked: {
|
|
5580
|
+
value: true,
|
|
5581
|
+
reason: result.reason,
|
|
5582
|
+
owner: result.owner
|
|
4550
5583
|
}
|
|
4551
|
-
}
|
|
4552
|
-
|
|
4553
|
-
}
|
|
5584
|
+
}
|
|
5585
|
+
})));
|
|
4554
5586
|
return EXIT_CODE.OK;
|
|
4555
|
-
}
|
|
4556
|
-
|
|
5587
|
+
};
|
|
5588
|
+
const handleUnlock = async () => {
|
|
4557
5589
|
ensureArgumentCount({
|
|
4558
5590
|
command,
|
|
4559
5591
|
args: commandArgs,
|
|
@@ -4598,24 +5630,21 @@ const createCli = (options = {}) => {
|
|
|
4598
5630
|
branch
|
|
4599
5631
|
});
|
|
4600
5632
|
});
|
|
4601
|
-
if (runtime.json) {
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
reason: null
|
|
4611
|
-
}
|
|
5633
|
+
if (runtime.json) stdout(JSON.stringify(buildJsonSuccess({
|
|
5634
|
+
command,
|
|
5635
|
+
status: "ok",
|
|
5636
|
+
repoRoot,
|
|
5637
|
+
details: {
|
|
5638
|
+
branch,
|
|
5639
|
+
locked: {
|
|
5640
|
+
value: false,
|
|
5641
|
+
reason: null
|
|
4612
5642
|
}
|
|
4613
|
-
}
|
|
4614
|
-
|
|
4615
|
-
}
|
|
5643
|
+
}
|
|
5644
|
+
})));
|
|
4616
5645
|
return EXIT_CODE.OK;
|
|
4617
|
-
}
|
|
4618
|
-
|
|
5646
|
+
};
|
|
5647
|
+
const handleCd = async () => {
|
|
4619
5648
|
ensureArgumentCount({
|
|
4620
5649
|
command,
|
|
4621
5650
|
args: commandArgs,
|
|
@@ -4640,21 +5669,29 @@ const createCli = (options = {}) => {
|
|
|
4640
5669
|
}));
|
|
4641
5670
|
if (candidates.length === 0) throw createCliError("WORKTREE_NOT_FOUND", { message: "No worktree candidates found" });
|
|
4642
5671
|
const promptValue = readStringOption(parsedArgsRecord, "prompt");
|
|
5672
|
+
const prompt = typeof promptValue === "string" && promptValue.length > 0 ? promptValue : resolvedConfig.selector.cd.prompt;
|
|
5673
|
+
const cliFzfExtraArgs = collectOptionValues({
|
|
5674
|
+
args: beforeDoubleDash,
|
|
5675
|
+
optionNames: ["fzfArg", "fzf-arg"]
|
|
5676
|
+
});
|
|
5677
|
+
const mergedConfigFzfArgs = mergeFzfArgs({
|
|
5678
|
+
defaults: resolvedConfig.selector.cd.fzf.extraArgs,
|
|
5679
|
+
extras: cliFzfExtraArgs
|
|
5680
|
+
});
|
|
5681
|
+
const surface = resolvedConfig.selector.cd.surface;
|
|
4643
5682
|
const selection = await selectPathWithFzf$1({
|
|
4644
5683
|
candidates,
|
|
4645
|
-
prompt
|
|
5684
|
+
prompt,
|
|
4646
5685
|
fzfExtraArgs: mergeFzfArgs({
|
|
4647
5686
|
defaults: CD_FZF_EXTRA_ARGS,
|
|
4648
|
-
extras:
|
|
4649
|
-
args: beforeDoubleDash,
|
|
4650
|
-
optionNames: ["fzfArg", "fzf-arg"]
|
|
4651
|
-
})
|
|
5687
|
+
extras: mergedConfigFzfArgs
|
|
4652
5688
|
}),
|
|
5689
|
+
surface,
|
|
5690
|
+
tmuxPopupOpts: resolvedConfig.selector.cd.tmuxPopupOpts,
|
|
4653
5691
|
cwd: repoRoot,
|
|
4654
5692
|
isInteractive: () => runtime.isInteractive || process.stderr.isTTY === true
|
|
4655
5693
|
}).catch((error) => {
|
|
4656
|
-
|
|
4657
|
-
if (message.includes("interactive terminal") || message.includes("fzf is required")) throw createCliError("DEPENDENCY_MISSING", { message: `DEPENDENCY_MISSING: ${message}` });
|
|
5694
|
+
if (error instanceof FzfDependencyError || error instanceof FzfInteractiveRequiredError) throw createCliError("DEPENDENCY_MISSING", { message: `DEPENDENCY_MISSING: ${error.message}` });
|
|
4658
5695
|
throw error;
|
|
4659
5696
|
});
|
|
4660
5697
|
if (selection.status === "cancelled") return EXIT_CODE_CANCELLED;
|
|
@@ -4670,7 +5707,20 @@ const createCli = (options = {}) => {
|
|
|
4670
5707
|
}
|
|
4671
5708
|
stdout(selectedPath);
|
|
4672
5709
|
return EXIT_CODE.OK;
|
|
4673
|
-
}
|
|
5710
|
+
};
|
|
5711
|
+
const miscCommandExitCode = await dispatchCommandHandler({
|
|
5712
|
+
command,
|
|
5713
|
+
handlers: createMiscCommandHandlers({
|
|
5714
|
+
execHandler: handleExec,
|
|
5715
|
+
invokeHandler: handleInvoke,
|
|
5716
|
+
copyHandler: handleCopy,
|
|
5717
|
+
linkHandler: handleLink,
|
|
5718
|
+
lockHandler: handleLock,
|
|
5719
|
+
unlockHandler: handleUnlock,
|
|
5720
|
+
cdHandler: handleCd
|
|
5721
|
+
})
|
|
5722
|
+
});
|
|
5723
|
+
if (miscCommandExitCode !== void 0) return miscCommandExitCode;
|
|
4674
5724
|
throw createCliError("UNKNOWN_COMMAND", { message: `Unknown command: ${command}` });
|
|
4675
5725
|
} catch (error) {
|
|
4676
5726
|
const cliError = ensureCliError(error);
|