vde-worktree 0.0.20 → 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/dist/index.mjs +1047 -733
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -750,22 +750,9 @@ const resolveExistingConfigFiles = async ({ cwd, repoRoot }) => {
|
|
|
750
750
|
}
|
|
751
751
|
return [...deduped.values()].sort((a, b) => a.order - b.order).map((entry) => entry.path);
|
|
752
752
|
};
|
|
753
|
-
const isPathInsideOrEqual$1 = ({ parentPath, childPath }) => {
|
|
754
|
-
const rel = relative(parentPath, childPath);
|
|
755
|
-
if (rel.length === 0) return true;
|
|
756
|
-
return rel !== ".." && rel.startsWith(`..${sep}`) !== true;
|
|
757
|
-
};
|
|
758
753
|
const validateWorktreeRoot = async ({ repoRoot, config }) => {
|
|
759
754
|
const rawWorktreeRoot = config.paths.worktreeRoot;
|
|
760
755
|
const resolvedWorktreeRoot = isAbsolute(rawWorktreeRoot) ? resolve(rawWorktreeRoot) : resolve(repoRoot, rawWorktreeRoot);
|
|
761
|
-
if (isPathInsideOrEqual$1({
|
|
762
|
-
parentPath: resolve(repoRoot, ".git"),
|
|
763
|
-
childPath: resolvedWorktreeRoot
|
|
764
|
-
})) throwInvalidConfig({
|
|
765
|
-
file: "<resolved>",
|
|
766
|
-
keyPath: "paths.worktreeRoot",
|
|
767
|
-
reason: "must not point inside .git"
|
|
768
|
-
});
|
|
769
756
|
try {
|
|
770
757
|
if ((await lstat(resolvedWorktreeRoot)).isDirectory() !== true) throwInvalidConfig({
|
|
771
758
|
file: "<resolved>",
|
|
@@ -989,11 +976,10 @@ const appendHookLog = async ({ repoRoot, action, branch, content }) => {
|
|
|
989
976
|
branch
|
|
990
977
|
})), content, "utf8");
|
|
991
978
|
};
|
|
992
|
-
const
|
|
993
|
-
if (context.enabled !== true) return;
|
|
994
|
-
const path = hookPath(context.repoRoot, hookName);
|
|
979
|
+
const ensureHookExists = async ({ path, hookName, requireExists }) => {
|
|
995
980
|
try {
|
|
996
981
|
await access(path, constants.F_OK);
|
|
982
|
+
return true;
|
|
997
983
|
} catch {
|
|
998
984
|
if (requireExists) throw createCliError("HOOK_NOT_FOUND", {
|
|
999
985
|
message: `Hook not found: ${hookName}`,
|
|
@@ -1002,8 +988,10 @@ const runHook = async ({ phase, hookName, args, context, requireExists = false }
|
|
|
1002
988
|
path
|
|
1003
989
|
}
|
|
1004
990
|
});
|
|
1005
|
-
return;
|
|
991
|
+
return false;
|
|
1006
992
|
}
|
|
993
|
+
};
|
|
994
|
+
const ensureHookExecutable = async ({ path, hookName }) => {
|
|
1007
995
|
try {
|
|
1008
996
|
await access(path, constants.X_OK);
|
|
1009
997
|
} catch {
|
|
@@ -1015,43 +1003,102 @@ const runHook = async ({ phase, hookName, args, context, requireExists = false }
|
|
|
1015
1003
|
}
|
|
1016
1004
|
});
|
|
1017
1005
|
}
|
|
1006
|
+
};
|
|
1007
|
+
const executeHookProcess = async ({ path, args, context }) => {
|
|
1018
1008
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
env
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
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: [
|
|
1037
1039
|
`hook=${hookName}`,
|
|
1038
1040
|
`phase=${phase}`,
|
|
1039
|
-
`start=${startedAt}`,
|
|
1040
|
-
`end=${endedAt}`,
|
|
1041
|
-
`exitCode=${String(result.exitCode
|
|
1042
|
-
`
|
|
1041
|
+
`start=${result.startedAt}`,
|
|
1042
|
+
`end=${result.endedAt}`,
|
|
1043
|
+
`exitCode=${String(result.exitCode)}`,
|
|
1044
|
+
`timedOut=${result.timedOut ? "1" : "0"}`,
|
|
1045
|
+
`stderr=${result.stderr}`,
|
|
1043
1046
|
""
|
|
1044
|
-
].join("\n")
|
|
1045
|
-
|
|
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({
|
|
1046
1075
|
repoRoot: context.repoRoot,
|
|
1047
1076
|
action: context.action,
|
|
1048
1077
|
branch: context.branch,
|
|
1049
|
-
|
|
1078
|
+
hookName,
|
|
1079
|
+
phase,
|
|
1080
|
+
result
|
|
1081
|
+
});
|
|
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
|
+
}
|
|
1050
1090
|
});
|
|
1051
|
-
if (
|
|
1052
|
-
const message = `Hook failed: ${hookName} (exitCode=${String(result.exitCode
|
|
1053
|
-
if (
|
|
1054
|
-
|
|
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
|
+
});
|
|
1055
1102
|
return;
|
|
1056
1103
|
}
|
|
1057
1104
|
throw createCliError("HOOK_FAILED", {
|
|
@@ -1074,8 +1121,14 @@ const runHook = async ({ phase, hookName, args, context, requireExists = false }
|
|
|
1074
1121
|
},
|
|
1075
1122
|
cause: error
|
|
1076
1123
|
});
|
|
1077
|
-
if (
|
|
1078
|
-
|
|
1124
|
+
if (shouldIgnorePostHookFailure({
|
|
1125
|
+
phase,
|
|
1126
|
+
context
|
|
1127
|
+
})) {
|
|
1128
|
+
handleIgnoredPostHookFailure({
|
|
1129
|
+
context,
|
|
1130
|
+
hookName
|
|
1131
|
+
});
|
|
1079
1132
|
return;
|
|
1080
1133
|
}
|
|
1081
1134
|
throw createCliError("HOOK_FAILED", {
|
|
@@ -1204,6 +1257,79 @@ const initializeRepository = async ({ repoRoot, managedWorktreeRoot }) => {
|
|
|
1204
1257
|
return { alreadyInitialized: wasInitialized };
|
|
1205
1258
|
};
|
|
1206
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
|
+
|
|
1207
1333
|
//#endregion
|
|
1208
1334
|
//#region src/core/repo-lock.ts
|
|
1209
1335
|
const sleep = async (ms) => {
|
|
@@ -1221,16 +1347,8 @@ const isProcessAlive = (pid) => {
|
|
|
1221
1347
|
return true;
|
|
1222
1348
|
}
|
|
1223
1349
|
};
|
|
1224
|
-
const
|
|
1225
|
-
|
|
1226
|
-
const parsed = JSON.parse(content);
|
|
1227
|
-
if (parsed.schemaVersion !== 1) return null;
|
|
1228
|
-
if (typeof parsed.command !== "string" || typeof parsed.owner !== "string") return null;
|
|
1229
|
-
if (typeof parsed.pid !== "number" || typeof parsed.host !== "string" || typeof parsed.startedAt !== "string") return null;
|
|
1230
|
-
return parsed;
|
|
1231
|
-
} catch {
|
|
1232
|
-
return null;
|
|
1233
|
-
}
|
|
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";
|
|
1234
1352
|
};
|
|
1235
1353
|
const lockFilePath$1 = async (repoRoot) => {
|
|
1236
1354
|
const stateDir = getStateDirectoryPath(repoRoot);
|
|
@@ -1259,23 +1377,15 @@ const canRecoverStaleLock = ({ lock, staleLockTTLSeconds }) => {
|
|
|
1259
1377
|
if (lock.host === hostname() && isProcessAlive(lock.pid)) return false;
|
|
1260
1378
|
return true;
|
|
1261
1379
|
};
|
|
1262
|
-
const writeNewLockFile = async (path, payload) => {
|
|
1263
|
-
try {
|
|
1264
|
-
const handle = await open(path, "wx");
|
|
1265
|
-
await handle.writeFile(`${JSON.stringify(payload)}\n`, "utf8");
|
|
1266
|
-
await handle.close();
|
|
1267
|
-
return true;
|
|
1268
|
-
} catch (error) {
|
|
1269
|
-
if (error.code === "EEXIST") return false;
|
|
1270
|
-
throw error;
|
|
1271
|
-
}
|
|
1272
|
-
};
|
|
1273
1380
|
const acquireRepoLock = async ({ repoRoot, command, timeoutMs = DEFAULT_LOCK_TIMEOUT_MS, staleLockTTLSeconds = DEFAULT_STALE_LOCK_TTL_SECONDS }) => {
|
|
1274
1381
|
const path = await lockFilePath$1(repoRoot);
|
|
1275
1382
|
const startAt = Date.now();
|
|
1276
1383
|
const payload = buildLockPayload(command);
|
|
1277
1384
|
while (Date.now() - startAt <= timeoutMs) {
|
|
1278
|
-
if (await
|
|
1385
|
+
if (await writeJsonExclusively({
|
|
1386
|
+
path,
|
|
1387
|
+
payload
|
|
1388
|
+
})) return { release: async () => {
|
|
1279
1389
|
try {
|
|
1280
1390
|
await rm(path, { force: true });
|
|
1281
1391
|
} catch {
|
|
@@ -1290,7 +1400,11 @@ const acquireRepoLock = async ({ repoRoot, command, timeoutMs = DEFAULT_LOCK_TIM
|
|
|
1290
1400
|
continue;
|
|
1291
1401
|
}
|
|
1292
1402
|
if (canRecoverStaleLock({
|
|
1293
|
-
lock:
|
|
1403
|
+
lock: parseJsonRecord({
|
|
1404
|
+
content: lockContent,
|
|
1405
|
+
schemaVersion: 1,
|
|
1406
|
+
validate: isRepoLockFileSchema
|
|
1407
|
+
}).record,
|
|
1294
1408
|
staleLockTTLSeconds
|
|
1295
1409
|
})) {
|
|
1296
1410
|
try {
|
|
@@ -1339,57 +1453,16 @@ const hasStateDirectory = async (repoRoot) => {
|
|
|
1339
1453
|
return false;
|
|
1340
1454
|
}
|
|
1341
1455
|
};
|
|
1342
|
-
const
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
const isLastDivergedHeadValid = parsed.lastDivergedHead === null || typeof parsed.lastDivergedHead === "string" && parsed.lastDivergedHead.length > 0;
|
|
1346
|
-
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 {
|
|
1347
|
-
valid: false,
|
|
1348
|
-
record: null
|
|
1349
|
-
};
|
|
1350
|
-
return {
|
|
1351
|
-
valid: true,
|
|
1352
|
-
record: parsed
|
|
1353
|
-
};
|
|
1354
|
-
} catch {
|
|
1355
|
-
return {
|
|
1356
|
-
valid: false,
|
|
1357
|
-
record: null
|
|
1358
|
-
};
|
|
1359
|
-
}
|
|
1360
|
-
};
|
|
1361
|
-
const writeJsonAtomically$1 = async ({ filePath, payload }) => {
|
|
1362
|
-
await mkdir(dirname(filePath), { recursive: true });
|
|
1363
|
-
const tmpPath = `${filePath}.tmp-${String(process.pid)}-${String(Date.now())}`;
|
|
1364
|
-
await writeFile(tmpPath, `${JSON.stringify(payload)}\n`, "utf8");
|
|
1365
|
-
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";
|
|
1366
1459
|
};
|
|
1367
1460
|
const readWorktreeMergeLifecycle = async ({ repoRoot, branch }) => {
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
path,
|
|
1374
|
-
exists: false,
|
|
1375
|
-
valid: true,
|
|
1376
|
-
record: null
|
|
1377
|
-
};
|
|
1378
|
-
}
|
|
1379
|
-
try {
|
|
1380
|
-
return {
|
|
1381
|
-
path,
|
|
1382
|
-
exists: true,
|
|
1383
|
-
...parseLifecycle(await readFile(path, "utf8"))
|
|
1384
|
-
};
|
|
1385
|
-
} catch {
|
|
1386
|
-
return {
|
|
1387
|
-
path,
|
|
1388
|
-
exists: true,
|
|
1389
|
-
valid: false,
|
|
1390
|
-
record: null
|
|
1391
|
-
};
|
|
1392
|
-
}
|
|
1461
|
+
return readJsonRecord({
|
|
1462
|
+
path: lifecycleFilePath(repoRoot, branch),
|
|
1463
|
+
schemaVersion: 2,
|
|
1464
|
+
validate: isWorktreeMergeLifecycleRecord
|
|
1465
|
+
});
|
|
1393
1466
|
};
|
|
1394
1467
|
const upsertWorktreeMergeLifecycle = async ({ repoRoot, branch, baseBranch, observedDivergedHead }) => {
|
|
1395
1468
|
const normalizedObservedHead = typeof observedDivergedHead === "string" && observedDivergedHead.length > 0 ? observedDivergedHead : null;
|
|
@@ -1424,9 +1497,10 @@ const upsertWorktreeMergeLifecycle = async ({ repoRoot, branch, baseBranch, obse
|
|
|
1424
1497
|
createdAt: current.record?.createdAt ?? now,
|
|
1425
1498
|
updatedAt: now
|
|
1426
1499
|
};
|
|
1427
|
-
await writeJsonAtomically
|
|
1500
|
+
await writeJsonAtomically({
|
|
1428
1501
|
filePath: current.path,
|
|
1429
|
-
payload: next
|
|
1502
|
+
payload: next,
|
|
1503
|
+
ensureDir: true
|
|
1430
1504
|
});
|
|
1431
1505
|
return next;
|
|
1432
1506
|
};
|
|
@@ -1463,9 +1537,10 @@ const moveWorktreeMergeLifecycle = async ({ repoRoot, fromBranch, toBranch, base
|
|
|
1463
1537
|
createdAt: source.record?.createdAt ?? now,
|
|
1464
1538
|
updatedAt: now
|
|
1465
1539
|
};
|
|
1466
|
-
await writeJsonAtomically
|
|
1540
|
+
await writeJsonAtomically({
|
|
1467
1541
|
filePath: targetPath,
|
|
1468
|
-
payload: next
|
|
1542
|
+
payload: next,
|
|
1543
|
+
ensureDir: true
|
|
1469
1544
|
});
|
|
1470
1545
|
if (source.path !== targetPath) await rm(source.path, { force: true });
|
|
1471
1546
|
return next;
|
|
@@ -1476,58 +1551,18 @@ const deleteWorktreeMergeLifecycle = async ({ repoRoot, branch }) => {
|
|
|
1476
1551
|
|
|
1477
1552
|
//#endregion
|
|
1478
1553
|
//#region src/core/worktree-lock.ts
|
|
1479
|
-
const
|
|
1480
|
-
|
|
1481
|
-
const parsed = JSON.parse(content);
|
|
1482
|
-
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 {
|
|
1483
|
-
valid: false,
|
|
1484
|
-
record: null
|
|
1485
|
-
};
|
|
1486
|
-
return {
|
|
1487
|
-
valid: true,
|
|
1488
|
-
record: parsed
|
|
1489
|
-
};
|
|
1490
|
-
} catch {
|
|
1491
|
-
return {
|
|
1492
|
-
valid: false,
|
|
1493
|
-
record: null
|
|
1494
|
-
};
|
|
1495
|
-
}
|
|
1496
|
-
};
|
|
1497
|
-
const writeJsonAtomically = async ({ filePath, payload }) => {
|
|
1498
|
-
const tmpPath = `${filePath}.tmp-${String(process.pid)}-${String(Date.now())}`;
|
|
1499
|
-
await writeFile(tmpPath, `${JSON.stringify(payload)}\n`, "utf8");
|
|
1500
|
-
await rename(tmpPath, filePath);
|
|
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";
|
|
1501
1556
|
};
|
|
1502
1557
|
const lockFilePath = (repoRoot, branch) => {
|
|
1503
1558
|
return join(getLocksDirectoryPath(repoRoot), `${branchToWorktreeId(branch)}.json`);
|
|
1504
1559
|
};
|
|
1505
1560
|
const readWorktreeLock = async ({ repoRoot, branch }) => {
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
path,
|
|
1512
|
-
exists: false,
|
|
1513
|
-
valid: true,
|
|
1514
|
-
record: null
|
|
1515
|
-
};
|
|
1516
|
-
}
|
|
1517
|
-
try {
|
|
1518
|
-
return {
|
|
1519
|
-
path,
|
|
1520
|
-
exists: true,
|
|
1521
|
-
...parseLock(await readFile(path, "utf8"))
|
|
1522
|
-
};
|
|
1523
|
-
} catch {
|
|
1524
|
-
return {
|
|
1525
|
-
path,
|
|
1526
|
-
exists: true,
|
|
1527
|
-
valid: false,
|
|
1528
|
-
record: null
|
|
1529
|
-
};
|
|
1530
|
-
}
|
|
1561
|
+
return readJsonRecord({
|
|
1562
|
+
path: lockFilePath(repoRoot, branch),
|
|
1563
|
+
schemaVersion: 1,
|
|
1564
|
+
validate: isWorktreeLockRecord
|
|
1565
|
+
});
|
|
1531
1566
|
};
|
|
1532
1567
|
const upsertWorktreeLock = async ({ repoRoot, branch, reason, owner }) => {
|
|
1533
1568
|
const { path, record } = await readWorktreeLock({
|
|
@@ -1558,16 +1593,40 @@ const deleteWorktreeLock = async ({ repoRoot, branch }) => {
|
|
|
1558
1593
|
|
|
1559
1594
|
//#endregion
|
|
1560
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
|
+
};
|
|
1561
1615
|
const defaultRunGh = async ({ cwd, args }) => {
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
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
|
+
}
|
|
1571
1630
|
};
|
|
1572
1631
|
const toTargetBranches = ({ branches, baseBranch }) => {
|
|
1573
1632
|
const uniqueBranches = /* @__PURE__ */ new Set();
|
|
@@ -1669,7 +1728,10 @@ const resolvePrStateByBranchBatch = async ({ repoRoot, baseBranch, branches, ena
|
|
|
1669
1728
|
"headRefName,state,mergedAt,updatedAt,url"
|
|
1670
1729
|
]
|
|
1671
1730
|
});
|
|
1672
|
-
if (result.exitCode !== 0)
|
|
1731
|
+
if (result.exitCode !== 0) throw new GhCommandError({
|
|
1732
|
+
exitCode: result.exitCode,
|
|
1733
|
+
stderr: result.stderr
|
|
1734
|
+
});
|
|
1673
1735
|
const prStatusByBranch = parsePrStateByBranch({
|
|
1674
1736
|
raw: result.stdout,
|
|
1675
1737
|
targetBranches
|
|
@@ -1677,6 +1739,7 @@ const resolvePrStateByBranchBatch = async ({ repoRoot, baseBranch, branches, ena
|
|
|
1677
1739
|
if (prStatusByBranch === null) return buildUnknownPrStateMap(targetBranches);
|
|
1678
1740
|
return prStatusByBranch;
|
|
1679
1741
|
} catch (error) {
|
|
1742
|
+
if (error instanceof GhUnavailableError || error instanceof GhCommandError) return buildUnknownPrStateMap(targetBranches);
|
|
1680
1743
|
if (error.code === "ENOENT") return buildUnknownPrStateMap(targetBranches);
|
|
1681
1744
|
return buildUnknownPrStateMap(targetBranches);
|
|
1682
1745
|
}
|
|
@@ -1743,6 +1806,9 @@ const listGitWorktrees = async (repoRoot) => {
|
|
|
1743
1806
|
|
|
1744
1807
|
//#endregion
|
|
1745
1808
|
//#region src/core/worktree-state.ts
|
|
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");
|
|
1811
|
+
};
|
|
1746
1812
|
const resolveDirty = async (worktreePath) => {
|
|
1747
1813
|
return (await runGitCommand({
|
|
1748
1814
|
cwd: worktreePath,
|
|
@@ -1750,16 +1816,6 @@ const resolveDirty = async (worktreePath) => {
|
|
|
1750
1816
|
reject: false
|
|
1751
1817
|
})).stdout.trim().length > 0;
|
|
1752
1818
|
};
|
|
1753
|
-
const parseLockPayload = (content) => {
|
|
1754
|
-
try {
|
|
1755
|
-
const parsed = JSON.parse(content);
|
|
1756
|
-
if (parsed.schemaVersion !== 1) return null;
|
|
1757
|
-
if (typeof parsed.branch !== "string" || typeof parsed.worktreeId !== "string" || typeof parsed.reason !== "string" || parsed.reason.length === 0) return null;
|
|
1758
|
-
return parsed;
|
|
1759
|
-
} catch {
|
|
1760
|
-
return null;
|
|
1761
|
-
}
|
|
1762
|
-
};
|
|
1763
1819
|
const resolveLockState = async ({ repoRoot, branch }) => {
|
|
1764
1820
|
if (branch === null) return {
|
|
1765
1821
|
value: false,
|
|
@@ -1767,38 +1823,62 @@ const resolveLockState = async ({ repoRoot, branch }) => {
|
|
|
1767
1823
|
owner: null
|
|
1768
1824
|
};
|
|
1769
1825
|
const id = branchToWorktreeId(branch);
|
|
1770
|
-
const
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
}
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
owner: typeof lock.owner === "string" && lock.owner.length > 0 ? lock.owner : null
|
|
1791
|
-
};
|
|
1792
|
-
} catch {
|
|
1793
|
-
return {
|
|
1794
|
-
value: true,
|
|
1795
|
-
reason: "invalid lock metadata",
|
|
1796
|
-
owner: null
|
|
1797
|
-
};
|
|
1798
|
-
}
|
|
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
|
+
};
|
|
1799
1846
|
};
|
|
1800
1847
|
const WORK_REFLOG_MESSAGE_PATTERN = /^(commit(?: \([^)]*\))?|cherry-pick|revert|rebase \(pick\)|merge):/;
|
|
1801
|
-
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 }) => {
|
|
1802
1882
|
const reflog = await runGitCommand({
|
|
1803
1883
|
cwd: repoRoot,
|
|
1804
1884
|
args: [
|
|
@@ -1813,17 +1893,13 @@ const resolveLifecycleFromReflog = async ({ repoRoot, branch, baseBranch }) => {
|
|
|
1813
1893
|
merged: null,
|
|
1814
1894
|
divergedHead: null
|
|
1815
1895
|
};
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
const
|
|
1823
|
-
const message = trimmed.slice(separatorIndex + 1).trim();
|
|
1824
|
-
if (head.length === 0 || WORK_REFLOG_MESSAGE_PATTERN.test(message) !== true) continue;
|
|
1825
|
-
if (latestWorkHead === null) latestWorkHead = head;
|
|
1826
|
-
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({
|
|
1827
1903
|
cwd: repoRoot,
|
|
1828
1904
|
args: [
|
|
1829
1905
|
"merge-base",
|
|
@@ -1832,19 +1908,52 @@ const resolveLifecycleFromReflog = async ({ repoRoot, branch, baseBranch }) => {
|
|
|
1832
1908
|
baseBranch
|
|
1833
1909
|
],
|
|
1834
1910
|
reject: false
|
|
1835
|
-
});
|
|
1836
|
-
if (
|
|
1911
|
+
})).exitCode);
|
|
1912
|
+
if (merged === true) return {
|
|
1837
1913
|
merged: true,
|
|
1838
1914
|
divergedHead: head
|
|
1839
1915
|
};
|
|
1840
|
-
if (
|
|
1916
|
+
if (merged === null) return {
|
|
1841
1917
|
merged: null,
|
|
1842
|
-
divergedHead:
|
|
1918
|
+
divergedHead: parsedHeads.latestHead
|
|
1843
1919
|
};
|
|
1844
1920
|
}
|
|
1845
1921
|
return {
|
|
1846
1922
|
merged: false,
|
|
1847
|
-
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
|
+
}
|
|
1848
1957
|
};
|
|
1849
1958
|
};
|
|
1850
1959
|
const resolveMergedState = async ({ repoRoot, branch, head, baseBranch, prStateByBranch }) => {
|
|
@@ -1853,64 +1962,42 @@ const resolveMergedState = async ({ repoRoot, branch, head, baseBranch, prStateB
|
|
|
1853
1962
|
byPR: null,
|
|
1854
1963
|
overall: null
|
|
1855
1964
|
};
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
});
|
|
1868
|
-
if (result.exitCode === 0) byAncestry = true;
|
|
1869
|
-
else if (result.exitCode === 1) byAncestry = false;
|
|
1870
|
-
}
|
|
1871
|
-
const prStatus = branch === baseBranch ? null : prStateByBranch.get(branch)?.status ?? null;
|
|
1872
|
-
let byPR = null;
|
|
1873
|
-
if (prStatus === "merged") byPR = true;
|
|
1874
|
-
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
|
+
});
|
|
1875
1976
|
let byLifecycle = null;
|
|
1876
1977
|
if (baseBranch !== null) {
|
|
1877
|
-
const lifecycle = await
|
|
1878
|
-
repoRoot,
|
|
1978
|
+
const lifecycle = await mergeLifecycleRepository.upsert({
|
|
1879
1979
|
branch,
|
|
1880
1980
|
baseBranch,
|
|
1881
1981
|
observedDivergedHead: byAncestry === false ? head : null
|
|
1882
1982
|
});
|
|
1883
1983
|
if (byAncestry === false) byLifecycle = false;
|
|
1884
|
-
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;
|
|
1885
1989
|
else {
|
|
1886
|
-
const probe = await
|
|
1887
|
-
repoRoot,
|
|
1990
|
+
const probe = await mergeProbeRepository.probeLifecycleFromReflog({
|
|
1888
1991
|
branch,
|
|
1889
1992
|
baseBranch
|
|
1890
1993
|
});
|
|
1891
1994
|
byLifecycle = probe.merged;
|
|
1892
|
-
if (probe.divergedHead !== null) await
|
|
1893
|
-
repoRoot,
|
|
1995
|
+
if (probe.divergedHead !== null) await mergeLifecycleRepository.upsert({
|
|
1894
1996
|
branch,
|
|
1895
1997
|
baseBranch,
|
|
1896
1998
|
observedDivergedHead: probe.divergedHead
|
|
1897
1999
|
});
|
|
1898
2000
|
}
|
|
1899
|
-
else {
|
|
1900
|
-
const lifecycleResult = await runGitCommand({
|
|
1901
|
-
cwd: repoRoot,
|
|
1902
|
-
args: [
|
|
1903
|
-
"merge-base",
|
|
1904
|
-
"--is-ancestor",
|
|
1905
|
-
lifecycle.lastDivergedHead,
|
|
1906
|
-
baseBranch
|
|
1907
|
-
],
|
|
1908
|
-
reject: false
|
|
1909
|
-
});
|
|
1910
|
-
if (lifecycleResult.exitCode === 0) byLifecycle = true;
|
|
1911
|
-
else if (lifecycleResult.exitCode === 1) byLifecycle = false;
|
|
1912
|
-
else byLifecycle = null;
|
|
1913
|
-
}
|
|
1914
2001
|
}
|
|
1915
2002
|
return {
|
|
1916
2003
|
byAncestry,
|
|
@@ -2046,6 +2133,50 @@ const RESERVED_FZF_ARGS = new Set([
|
|
|
2046
2133
|
]);
|
|
2047
2134
|
const ANSI_ESCAPE_SEQUENCE_PATTERN = String.raw`\u001B\[[0-?]*[ -/]*[@-~]`;
|
|
2048
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
|
+
};
|
|
2049
2180
|
const sanitizeCandidate = (value) => value.replace(/[\r\n]+/g, " ").trim();
|
|
2050
2181
|
const stripAnsi = (value) => value.replace(ANSI_ESCAPE_SEQUENCE_REGEX, "");
|
|
2051
2182
|
const stripTrailingNewlines = (value) => value.replace(/[\r\n]+$/g, "");
|
|
@@ -2054,12 +2185,12 @@ const buildFzfInput = (candidates) => {
|
|
|
2054
2185
|
};
|
|
2055
2186
|
const validateExtraFzfArgs = (fzfExtraArgs) => {
|
|
2056
2187
|
for (const arg of fzfExtraArgs) {
|
|
2057
|
-
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");
|
|
2058
2189
|
if (!arg.startsWith("--")) continue;
|
|
2059
2190
|
const withoutPrefix = arg.slice(2);
|
|
2060
2191
|
if (withoutPrefix.length === 0) continue;
|
|
2061
2192
|
const optionName = withoutPrefix.split("=")[0];
|
|
2062
|
-
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}`);
|
|
2063
2194
|
}
|
|
2064
2195
|
};
|
|
2065
2196
|
const buildFzfArgs = ({ prompt, fzfExtraArgs }) => {
|
|
@@ -2099,7 +2230,7 @@ const defaultRunFzf = async ({ args, input, cwd, env }) => {
|
|
|
2099
2230
|
};
|
|
2100
2231
|
const ensureFzfAvailable = async (checkFzfAvailability) => {
|
|
2101
2232
|
if (await checkFzfAvailability()) return;
|
|
2102
|
-
throw new
|
|
2233
|
+
throw new FzfDependencyError();
|
|
2103
2234
|
};
|
|
2104
2235
|
const shouldTryTmuxPopup = async ({ surface, env, checkFzfTmuxSupport }) => {
|
|
2105
2236
|
if (surface === "inline") return false;
|
|
@@ -2122,8 +2253,8 @@ const isTmuxUnknownOptionError = (error) => {
|
|
|
2122
2253
|
return /unknown option.*--tmux|--tmux.*unknown option/i.test(text);
|
|
2123
2254
|
};
|
|
2124
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 }) => {
|
|
2125
|
-
if (candidates.length === 0) throw new
|
|
2126
|
-
if (isInteractive() !== true) throw new
|
|
2256
|
+
if (candidates.length === 0) throw new FzfInvalidArgumentError("No candidates provided for fzf selection");
|
|
2257
|
+
if (isInteractive() !== true) throw new FzfInteractiveRequiredError();
|
|
2127
2258
|
await ensureFzfAvailable(checkFzfAvailability);
|
|
2128
2259
|
const baseArgs = buildFzfArgs({
|
|
2129
2260
|
prompt,
|
|
@@ -2136,7 +2267,7 @@ const selectPathWithFzf = async ({ candidates, prompt = "worktree> ", surface =
|
|
|
2136
2267
|
});
|
|
2137
2268
|
const args = tryTmuxPopup ? [...baseArgs, `--tmux=${tmuxPopupOpts}`] : baseArgs;
|
|
2138
2269
|
const input = buildFzfInput(candidates);
|
|
2139
|
-
if (input.length === 0) throw new
|
|
2270
|
+
if (input.length === 0) throw new FzfInvalidArgumentError("All candidates are empty after sanitization");
|
|
2140
2271
|
const candidateSet = new Set(input.split("\n").map((candidate) => stripAnsi(candidate)));
|
|
2141
2272
|
const runWithValidation = async (fzfArgs) => {
|
|
2142
2273
|
const selectedPath = stripAnsi(stripTrailingNewlines((await runFzf({
|
|
@@ -2146,7 +2277,7 @@ const selectPathWithFzf = async ({ candidates, prompt = "worktree> ", surface =
|
|
|
2146
2277
|
env
|
|
2147
2278
|
})).stdout));
|
|
2148
2279
|
if (selectedPath.length === 0) return { status: "cancelled" };
|
|
2149
|
-
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");
|
|
2150
2281
|
return {
|
|
2151
2282
|
status: "selected",
|
|
2152
2283
|
path: selectedPath
|
|
@@ -2217,6 +2348,157 @@ const createLogger = (options = {}) => {
|
|
|
2217
2348
|
return build(prefix, level);
|
|
2218
2349
|
};
|
|
2219
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
|
+
|
|
2220
2502
|
//#endregion
|
|
2221
2503
|
//#region src/cli/package-version.ts
|
|
2222
2504
|
const CANDIDATE_PATHS = ["../package.json", "../../package.json"];
|
|
@@ -2584,6 +2866,7 @@ const commandHelpEntries = [
|
|
|
2584
2866
|
options: ["--install", "--path <file>"]
|
|
2585
2867
|
}
|
|
2586
2868
|
];
|
|
2869
|
+
const commandHelpNames = commandHelpEntries.map((entry) => entry.name);
|
|
2587
2870
|
const splitRawArgsByDoubleDash = (args) => {
|
|
2588
2871
|
const separatorIndex = args.indexOf("--");
|
|
2589
2872
|
if (separatorIndex < 0) return {
|
|
@@ -2601,7 +2884,8 @@ const toKebabCase = (value) => {
|
|
|
2601
2884
|
const toOptionSpec = (kind, optionName) => {
|
|
2602
2885
|
return {
|
|
2603
2886
|
kind,
|
|
2604
|
-
allowOptionLikeValue: optionNamesAllowOptionLikeValue.has(optionName)
|
|
2887
|
+
allowOptionLikeValue: optionNamesAllowOptionLikeValue.has(optionName),
|
|
2888
|
+
allowNegation: kind === "boolean"
|
|
2605
2889
|
};
|
|
2606
2890
|
};
|
|
2607
2891
|
const buildOptionSpecs = (argsDef) => {
|
|
@@ -2629,6 +2913,69 @@ const buildOptionSpecs = (argsDef) => {
|
|
|
2629
2913
|
shortOptions
|
|
2630
2914
|
};
|
|
2631
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
|
+
};
|
|
2632
2979
|
const validateRawOptions = (args, optionSpecs) => {
|
|
2633
2980
|
for (let index = 0; index < args.length; index += 1) {
|
|
2634
2981
|
const token = args[index];
|
|
@@ -2636,39 +2983,20 @@ const validateRawOptions = (args, optionSpecs) => {
|
|
|
2636
2983
|
if (token === "--") break;
|
|
2637
2984
|
if (!token.startsWith("-") || token === "-") continue;
|
|
2638
2985
|
if (token.startsWith("--")) {
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
const optionSpec = directOptionSpec ?? optionSpecs.longOptions.get(optionNameForNegation);
|
|
2646
|
-
if (optionSpec === void 0) throw createCliError("INVALID_ARGUMENT", { message: `Unknown option: --${rawOptionName}` });
|
|
2647
|
-
if (optionSpec.kind === "value") if (separatorIndex >= 0) {
|
|
2648
|
-
if (value.slice(separatorIndex + 1).length === 0) throw createCliError("INVALID_ARGUMENT", { message: `Missing value for option: --${rawOptionName}` });
|
|
2649
|
-
} else {
|
|
2650
|
-
const nextToken = args[index + 1];
|
|
2651
|
-
if (typeof nextToken !== "string" || nextToken.length === 0) throw createCliError("INVALID_ARGUMENT", { message: `Missing value for option: --${rawOptionName}` });
|
|
2652
|
-
if (nextToken.startsWith("-") && optionSpec.allowOptionLikeValue !== true) throw createCliError("INVALID_ARGUMENT", { message: `Missing value for option: --${rawOptionName}` });
|
|
2653
|
-
index += 1;
|
|
2654
|
-
}
|
|
2986
|
+
index = validateLongOptionToken({
|
|
2987
|
+
args,
|
|
2988
|
+
index,
|
|
2989
|
+
token,
|
|
2990
|
+
optionSpecs
|
|
2991
|
+
});
|
|
2655
2992
|
continue;
|
|
2656
2993
|
}
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
if (optionSpec.kind === "value") {
|
|
2664
|
-
if (flagIndex < shortFlags.length - 1) break;
|
|
2665
|
-
const nextToken = args[index + 1];
|
|
2666
|
-
if (typeof nextToken !== "string" || nextToken.length === 0) throw createCliError("INVALID_ARGUMENT", { message: `Missing value for option: -${option}` });
|
|
2667
|
-
if (nextToken.startsWith("-") && optionSpec.allowOptionLikeValue !== true) throw createCliError("INVALID_ARGUMENT", { message: `Missing value for option: -${option}` });
|
|
2668
|
-
index += 1;
|
|
2669
|
-
break;
|
|
2670
|
-
}
|
|
2671
|
-
}
|
|
2994
|
+
index = validateShortOptionToken({
|
|
2995
|
+
args,
|
|
2996
|
+
index,
|
|
2997
|
+
token,
|
|
2998
|
+
optionSpecs
|
|
2999
|
+
});
|
|
2672
3000
|
}
|
|
2673
3001
|
};
|
|
2674
3002
|
const getPositionals = (args) => {
|
|
@@ -3629,97 +3957,30 @@ const createCli = (options = {}) => {
|
|
|
3629
3957
|
const parsedArgsRecord = parsedArgs;
|
|
3630
3958
|
const positionals = getPositionals(parsedArgs);
|
|
3631
3959
|
command = positionals[0] ?? "unknown";
|
|
3960
|
+
const commandArgs = positionals.slice(1);
|
|
3632
3961
|
jsonEnabled = parsedArgs.json === true;
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
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;
|
|
3649
3983
|
logger = parsedArgs.verbose === true ? createLogger({ level: LogLevel.INFO }) : createLogger();
|
|
3650
|
-
if (positionals.length === 0) {
|
|
3651
|
-
stdout(`${renderGeneralHelpText({ version })}\n`);
|
|
3652
|
-
return EXIT_CODE.OK;
|
|
3653
|
-
}
|
|
3654
|
-
if (command === "help") {
|
|
3655
|
-
const helpTarget = positionals[1];
|
|
3656
|
-
if (typeof helpTarget !== "string" || helpTarget.length === 0) {
|
|
3657
|
-
stdout(`${renderGeneralHelpText({ version })}\n`);
|
|
3658
|
-
return EXIT_CODE.OK;
|
|
3659
|
-
}
|
|
3660
|
-
const entry = findCommandHelp(helpTarget);
|
|
3661
|
-
if (entry === void 0) throw createCliError("INVALID_ARGUMENT", {
|
|
3662
|
-
message: `Unknown command for help: ${helpTarget}`,
|
|
3663
|
-
details: {
|
|
3664
|
-
requested: helpTarget,
|
|
3665
|
-
availableCommands: commandHelpEntries.map((item) => item.name)
|
|
3666
|
-
}
|
|
3667
|
-
});
|
|
3668
|
-
stdout(`${renderCommandHelpText({ entry })}\n`);
|
|
3669
|
-
return EXIT_CODE.OK;
|
|
3670
|
-
}
|
|
3671
|
-
const commandArgs = positionals.slice(1);
|
|
3672
|
-
if (command === "completion") {
|
|
3673
|
-
ensureArgumentCount({
|
|
3674
|
-
command,
|
|
3675
|
-
args: commandArgs,
|
|
3676
|
-
min: 1,
|
|
3677
|
-
max: 1
|
|
3678
|
-
});
|
|
3679
|
-
const shell = resolveCompletionShell(commandArgs[0]);
|
|
3680
|
-
const script = await loadCompletionScript(shell);
|
|
3681
|
-
if (parsedArgs.install === true) {
|
|
3682
|
-
const destinationPath = resolveCompletionInstallPath({
|
|
3683
|
-
shell,
|
|
3684
|
-
requestedPath: readStringOption(parsedArgsRecord, "path")
|
|
3685
|
-
});
|
|
3686
|
-
await installCompletionScript({
|
|
3687
|
-
content: script,
|
|
3688
|
-
destinationPath
|
|
3689
|
-
});
|
|
3690
|
-
if (jsonEnabled) {
|
|
3691
|
-
stdout(JSON.stringify(buildJsonSuccess({
|
|
3692
|
-
command,
|
|
3693
|
-
status: "ok",
|
|
3694
|
-
repoRoot: null,
|
|
3695
|
-
details: {
|
|
3696
|
-
shell,
|
|
3697
|
-
installed: true,
|
|
3698
|
-
path: destinationPath
|
|
3699
|
-
}
|
|
3700
|
-
})));
|
|
3701
|
-
return EXIT_CODE.OK;
|
|
3702
|
-
}
|
|
3703
|
-
stdout(`installed completion: ${destinationPath}`);
|
|
3704
|
-
if (shell === "zsh") stdout("zsh note: ensure completion path is in fpath, then run: autoload -Uz compinit && compinit");
|
|
3705
|
-
return EXIT_CODE.OK;
|
|
3706
|
-
}
|
|
3707
|
-
if (jsonEnabled) {
|
|
3708
|
-
stdout(JSON.stringify(buildJsonSuccess({
|
|
3709
|
-
command,
|
|
3710
|
-
status: "ok",
|
|
3711
|
-
repoRoot: null,
|
|
3712
|
-
details: {
|
|
3713
|
-
shell,
|
|
3714
|
-
installed: false,
|
|
3715
|
-
script
|
|
3716
|
-
}
|
|
3717
|
-
})));
|
|
3718
|
-
return EXIT_CODE.OK;
|
|
3719
|
-
}
|
|
3720
|
-
stdout(script);
|
|
3721
|
-
return EXIT_CODE.OK;
|
|
3722
|
-
}
|
|
3723
3984
|
const allowUnsafe = parsedArgs.allowUnsafe === true;
|
|
3724
3985
|
if (parsedArgs.hooks === false && allowUnsafe !== true) throw createCliError("UNSAFE_FLAG_REQUIRED", { message: "UNSAFE_FLAG_REQUIRED: --no-hooks requires --allow-unsafe" });
|
|
3725
3986
|
const repoContext = await resolveRepoContext(runtimeCwd);
|
|
@@ -3777,7 +4038,30 @@ const createCli = (options = {}) => {
|
|
|
3777
4038
|
staleLockTTLSeconds
|
|
3778
4039
|
}, task);
|
|
3779
4040
|
};
|
|
3780
|
-
|
|
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 () => {
|
|
3781
4065
|
ensureArgumentCount({
|
|
3782
4066
|
command,
|
|
3783
4067
|
args: commandArgs,
|
|
@@ -3807,21 +4091,18 @@ const createCli = (options = {}) => {
|
|
|
3807
4091
|
});
|
|
3808
4092
|
return initialized;
|
|
3809
4093
|
});
|
|
3810
|
-
if (runtime.json) {
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
})));
|
|
3820
|
-
return EXIT_CODE.OK;
|
|
3821
|
-
}
|
|
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
|
+
})));
|
|
3822
4103
|
return EXIT_CODE.OK;
|
|
3823
|
-
}
|
|
3824
|
-
|
|
4104
|
+
};
|
|
4105
|
+
const handleList = async () => {
|
|
3825
4106
|
ensureArgumentCount({
|
|
3826
4107
|
command,
|
|
3827
4108
|
args: commandArgs,
|
|
@@ -3893,8 +4174,8 @@ const createCli = (options = {}) => {
|
|
|
3893
4174
|
}) : rendered.trimEnd();
|
|
3894
4175
|
for (const line of colorized.split("\n")) stdout(line);
|
|
3895
4176
|
return EXIT_CODE.OK;
|
|
3896
|
-
}
|
|
3897
|
-
|
|
4177
|
+
};
|
|
4178
|
+
const handleStatus = async () => {
|
|
3898
4179
|
ensureArgumentCount({
|
|
3899
4180
|
command,
|
|
3900
4181
|
args: commandArgs,
|
|
@@ -3924,8 +4205,8 @@ const createCli = (options = {}) => {
|
|
|
3924
4205
|
stdout(`dirty: ${targetWorktree.dirty ? "true" : "false"}`);
|
|
3925
4206
|
stdout(`locked: ${targetWorktree.locked.value ? "true" : "false"}`);
|
|
3926
4207
|
return EXIT_CODE.OK;
|
|
3927
|
-
}
|
|
3928
|
-
|
|
4208
|
+
};
|
|
4209
|
+
const handlePath = async () => {
|
|
3929
4210
|
ensureArgumentCount({
|
|
3930
4211
|
command,
|
|
3931
4212
|
args: commandArgs,
|
|
@@ -3951,8 +4232,18 @@ const createCli = (options = {}) => {
|
|
|
3951
4232
|
}
|
|
3952
4233
|
stdout(target.path);
|
|
3953
4234
|
return EXIT_CODE.OK;
|
|
3954
|
-
}
|
|
3955
|
-
|
|
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 () => {
|
|
3956
4247
|
ensureArgumentCount({
|
|
3957
4248
|
command,
|
|
3958
4249
|
args: commandArgs,
|
|
@@ -3960,61 +4251,56 @@ const createCli = (options = {}) => {
|
|
|
3960
4251
|
max: 1
|
|
3961
4252
|
});
|
|
3962
4253
|
const branch = commandArgs[0] ?? randomWipBranchName();
|
|
4254
|
+
const targetPath = branchToWorktreePath(repoRoot, branch, resolvedConfig.paths.worktreeRoot);
|
|
3963
4255
|
const result = await runWriteOperation(async () => {
|
|
3964
|
-
|
|
3965
|
-
branch,
|
|
3966
|
-
worktrees: (await collectWorktreeSnapshot$1(repoRoot)).worktrees
|
|
3967
|
-
})) throw createCliError("BRANCH_ALREADY_ATTACHED", {
|
|
3968
|
-
message: `Branch is already attached to a worktree: ${branch}`,
|
|
3969
|
-
details: { branch }
|
|
3970
|
-
});
|
|
3971
|
-
if (await doesGitRefExist(repoRoot, `refs/heads/${branch}`)) throw createCliError("BRANCH_ALREADY_EXISTS", {
|
|
3972
|
-
message: `Branch already exists locally: ${branch}`,
|
|
3973
|
-
details: { branch }
|
|
3974
|
-
});
|
|
3975
|
-
const targetPath = branchToWorktreePath(repoRoot, branch, resolvedConfig.paths.worktreeRoot);
|
|
3976
|
-
await ensureTargetPathWritable(targetPath);
|
|
3977
|
-
const baseBranch = await resolveBaseBranch({
|
|
3978
|
-
repoRoot,
|
|
3979
|
-
config: resolvedConfig
|
|
3980
|
-
});
|
|
3981
|
-
const hookContext = createHookContext({
|
|
3982
|
-
runtime,
|
|
3983
|
-
repoRoot,
|
|
3984
|
-
action: "new",
|
|
3985
|
-
branch,
|
|
3986
|
-
worktreePath: targetPath,
|
|
3987
|
-
stderr
|
|
3988
|
-
});
|
|
3989
|
-
await runPreHook({
|
|
4256
|
+
return executeWorktreeMutation({
|
|
3990
4257
|
name: "new",
|
|
3991
|
-
context: hookContext
|
|
3992
|
-
});
|
|
3993
|
-
await runGitCommand({
|
|
3994
|
-
cwd: repoRoot,
|
|
3995
|
-
args: [
|
|
3996
|
-
"worktree",
|
|
3997
|
-
"add",
|
|
3998
|
-
"-b",
|
|
3999
|
-
branch,
|
|
4000
|
-
targetPath,
|
|
4001
|
-
baseBranch
|
|
4002
|
-
]
|
|
4003
|
-
});
|
|
4004
|
-
await upsertWorktreeMergeLifecycle({
|
|
4005
|
-
repoRoot,
|
|
4006
4258
|
branch,
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
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
|
+
}
|
|
4013
4303
|
});
|
|
4014
|
-
return {
|
|
4015
|
-
branch,
|
|
4016
|
-
path: targetPath
|
|
4017
|
-
};
|
|
4018
4304
|
});
|
|
4019
4305
|
if (runtime.json) {
|
|
4020
4306
|
stdout(JSON.stringify(buildJsonSuccess({
|
|
@@ -4027,8 +4313,8 @@ const createCli = (options = {}) => {
|
|
|
4027
4313
|
}
|
|
4028
4314
|
stdout(result.path);
|
|
4029
4315
|
return EXIT_CODE.OK;
|
|
4030
|
-
}
|
|
4031
|
-
|
|
4316
|
+
};
|
|
4317
|
+
const handleSwitch = async () => {
|
|
4032
4318
|
ensureArgumentCount({
|
|
4033
4319
|
command,
|
|
4034
4320
|
args: commandArgs,
|
|
@@ -4053,62 +4339,57 @@ const createCli = (options = {}) => {
|
|
|
4053
4339
|
};
|
|
4054
4340
|
}
|
|
4055
4341
|
const targetPath = branchToWorktreePath(repoRoot, branch, resolvedConfig.paths.worktreeRoot);
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
runtime,
|
|
4059
|
-
repoRoot,
|
|
4060
|
-
action: "switch",
|
|
4342
|
+
return executeWorktreeMutation({
|
|
4343
|
+
name: "switch",
|
|
4061
4344
|
branch,
|
|
4062
4345
|
worktreePath: targetPath,
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
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",
|
|
4091
4380
|
branch,
|
|
4092
|
-
targetPath
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
})
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
await runPostHook({
|
|
4104
|
-
name: "switch",
|
|
4105
|
-
context: hookContext
|
|
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
|
+
}
|
|
4106
4392
|
});
|
|
4107
|
-
return {
|
|
4108
|
-
status: "created",
|
|
4109
|
-
branch,
|
|
4110
|
-
path: targetPath
|
|
4111
|
-
};
|
|
4112
4393
|
});
|
|
4113
4394
|
if (runtime.json) {
|
|
4114
4395
|
stdout(JSON.stringify(buildJsonSuccess({
|
|
@@ -4124,8 +4405,16 @@ const createCli = (options = {}) => {
|
|
|
4124
4405
|
}
|
|
4125
4406
|
stdout(result.path);
|
|
4126
4407
|
return EXIT_CODE.OK;
|
|
4127
|
-
}
|
|
4128
|
-
|
|
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 () => {
|
|
4129
4418
|
ensureArgumentCount({
|
|
4130
4419
|
command,
|
|
4131
4420
|
args: commandArgs,
|
|
@@ -4152,68 +4441,68 @@ const createCli = (options = {}) => {
|
|
|
4152
4441
|
branch: newBranch,
|
|
4153
4442
|
path: current.path
|
|
4154
4443
|
};
|
|
4155
|
-
if (containsBranch({
|
|
4156
|
-
branch: newBranch,
|
|
4157
|
-
worktrees: snapshot.worktrees
|
|
4158
|
-
})) throw createCliError("BRANCH_ALREADY_ATTACHED", {
|
|
4159
|
-
message: `Branch is already attached to another worktree: ${newBranch}`,
|
|
4160
|
-
details: { branch: newBranch }
|
|
4161
|
-
});
|
|
4162
|
-
if (await doesGitRefExist(repoRoot, `refs/heads/${newBranch}`)) throw createCliError("BRANCH_ALREADY_EXISTS", {
|
|
4163
|
-
message: `Branch already exists locally: ${newBranch}`,
|
|
4164
|
-
details: { branch: newBranch }
|
|
4165
|
-
});
|
|
4166
4444
|
const newPath = branchToWorktreePath(repoRoot, newBranch, resolvedConfig.paths.worktreeRoot);
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
runtime,
|
|
4170
|
-
repoRoot,
|
|
4171
|
-
action: "mv",
|
|
4445
|
+
return executeWorktreeMutation({
|
|
4446
|
+
name: "mv",
|
|
4172
4447
|
branch: newBranch,
|
|
4173
4448
|
worktreePath: newPath,
|
|
4174
|
-
stderr,
|
|
4175
4449
|
extraEnv: {
|
|
4176
4450
|
WT_OLD_BRANCH: oldBranch,
|
|
4177
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
|
+
});
|
|
4178
4504
|
}
|
|
4179
4505
|
});
|
|
4180
|
-
await runPreHook({
|
|
4181
|
-
name: "mv",
|
|
4182
|
-
context: hookContext
|
|
4183
|
-
});
|
|
4184
|
-
await runGitCommand({
|
|
4185
|
-
cwd: current.path,
|
|
4186
|
-
args: [
|
|
4187
|
-
"branch",
|
|
4188
|
-
"-m",
|
|
4189
|
-
oldBranch,
|
|
4190
|
-
newBranch
|
|
4191
|
-
]
|
|
4192
|
-
});
|
|
4193
|
-
await runGitCommand({
|
|
4194
|
-
cwd: repoRoot,
|
|
4195
|
-
args: [
|
|
4196
|
-
"worktree",
|
|
4197
|
-
"move",
|
|
4198
|
-
current.path,
|
|
4199
|
-
newPath
|
|
4200
|
-
]
|
|
4201
|
-
});
|
|
4202
|
-
if (snapshot.baseBranch !== null) await moveWorktreeMergeLifecycle({
|
|
4203
|
-
repoRoot,
|
|
4204
|
-
fromBranch: oldBranch,
|
|
4205
|
-
toBranch: newBranch,
|
|
4206
|
-
baseBranch: snapshot.baseBranch,
|
|
4207
|
-
observedDivergedHead: null
|
|
4208
|
-
});
|
|
4209
|
-
await runPostHook({
|
|
4210
|
-
name: "mv",
|
|
4211
|
-
context: hookContext
|
|
4212
|
-
});
|
|
4213
|
-
return {
|
|
4214
|
-
branch: newBranch,
|
|
4215
|
-
path: newPath
|
|
4216
|
-
};
|
|
4217
4506
|
});
|
|
4218
4507
|
if (runtime.json) {
|
|
4219
4508
|
stdout(JSON.stringify(buildJsonSuccess({
|
|
@@ -4226,8 +4515,8 @@ const createCli = (options = {}) => {
|
|
|
4226
4515
|
}
|
|
4227
4516
|
stdout(result.path);
|
|
4228
4517
|
return EXIT_CODE.OK;
|
|
4229
|
-
}
|
|
4230
|
-
|
|
4518
|
+
};
|
|
4519
|
+
const handleDel = async () => {
|
|
4231
4520
|
ensureArgumentCount({
|
|
4232
4521
|
command,
|
|
4233
4522
|
args: commandArgs,
|
|
@@ -4268,56 +4557,58 @@ const createCli = (options = {}) => {
|
|
|
4268
4557
|
managedWorktreeRoot
|
|
4269
4558
|
}
|
|
4270
4559
|
});
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
forceFlags
|
|
4274
|
-
});
|
|
4275
|
-
const hookContext = createHookContext({
|
|
4276
|
-
runtime,
|
|
4277
|
-
repoRoot,
|
|
4278
|
-
action: "del",
|
|
4279
|
-
branch: target.branch,
|
|
4280
|
-
worktreePath: target.path,
|
|
4281
|
-
stderr
|
|
4282
|
-
});
|
|
4283
|
-
await runPreHook({
|
|
4284
|
-
name: "del",
|
|
4285
|
-
context: hookContext
|
|
4286
|
-
});
|
|
4287
|
-
const removeArgs = [
|
|
4288
|
-
"worktree",
|
|
4289
|
-
"remove",
|
|
4290
|
-
target.path
|
|
4291
|
-
];
|
|
4292
|
-
if (forceFlags.forceDirty) removeArgs.push("--force");
|
|
4293
|
-
await runGitCommand({
|
|
4294
|
-
cwd: repoRoot,
|
|
4295
|
-
args: removeArgs
|
|
4296
|
-
});
|
|
4297
|
-
await runGitCommand({
|
|
4298
|
-
cwd: repoRoot,
|
|
4299
|
-
args: [
|
|
4300
|
-
"branch",
|
|
4301
|
-
resolveBranchDeleteMode(forceFlags),
|
|
4302
|
-
target.branch
|
|
4303
|
-
]
|
|
4304
|
-
});
|
|
4305
|
-
await deleteWorktreeLock({
|
|
4306
|
-
repoRoot,
|
|
4307
|
-
branch: target.branch
|
|
4308
|
-
});
|
|
4309
|
-
await deleteWorktreeMergeLifecycle({
|
|
4310
|
-
repoRoot,
|
|
4311
|
-
branch: target.branch
|
|
4312
|
-
});
|
|
4313
|
-
await runPostHook({
|
|
4560
|
+
const targetBranch = target.branch;
|
|
4561
|
+
return executeWorktreeMutation({
|
|
4314
4562
|
name: "del",
|
|
4315
|
-
|
|
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
|
+
}
|
|
4316
4611
|
});
|
|
4317
|
-
return {
|
|
4318
|
-
branch: target.branch,
|
|
4319
|
-
path: target.path
|
|
4320
|
-
};
|
|
4321
4612
|
});
|
|
4322
4613
|
if (runtime.json) {
|
|
4323
4614
|
stdout(JSON.stringify(buildJsonSuccess({
|
|
@@ -4330,8 +4621,16 @@ const createCli = (options = {}) => {
|
|
|
4330
4621
|
}
|
|
4331
4622
|
stdout(result.path);
|
|
4332
4623
|
return EXIT_CODE.OK;
|
|
4333
|
-
}
|
|
4334
|
-
|
|
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 () => {
|
|
4335
4634
|
ensureArgumentCount({
|
|
4336
4635
|
command,
|
|
4337
4636
|
args: commandArgs,
|
|
@@ -4421,8 +4720,8 @@ const createCli = (options = {}) => {
|
|
|
4421
4720
|
const branches = result.dryRun ? result.candidates : result.deleted;
|
|
4422
4721
|
for (const branch of branches) stdout(`${label}: ${branch}`);
|
|
4423
4722
|
return EXIT_CODE.OK;
|
|
4424
|
-
}
|
|
4425
|
-
|
|
4723
|
+
};
|
|
4724
|
+
const handleGet = async () => {
|
|
4426
4725
|
ensureArgumentCount({
|
|
4427
4726
|
command,
|
|
4428
4727
|
args: commandArgs,
|
|
@@ -4541,8 +4840,8 @@ const createCli = (options = {}) => {
|
|
|
4541
4840
|
}
|
|
4542
4841
|
stdout(result.path);
|
|
4543
4842
|
return EXIT_CODE.OK;
|
|
4544
|
-
}
|
|
4545
|
-
|
|
4843
|
+
};
|
|
4844
|
+
const handleExtract = async () => {
|
|
4546
4845
|
ensureArgumentCount({
|
|
4547
4846
|
command,
|
|
4548
4847
|
args: commandArgs,
|
|
@@ -4674,8 +4973,17 @@ const createCli = (options = {}) => {
|
|
|
4674
4973
|
}
|
|
4675
4974
|
stdout(result.path);
|
|
4676
4975
|
return EXIT_CODE.OK;
|
|
4677
|
-
}
|
|
4678
|
-
|
|
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 () => {
|
|
4679
4987
|
ensureArgumentCount({
|
|
4680
4988
|
command,
|
|
4681
4989
|
args: commandArgs,
|
|
@@ -4797,8 +5105,8 @@ const createCli = (options = {}) => {
|
|
|
4797
5105
|
}
|
|
4798
5106
|
stdout(result.path);
|
|
4799
5107
|
return EXIT_CODE.OK;
|
|
4800
|
-
}
|
|
4801
|
-
|
|
5108
|
+
};
|
|
5109
|
+
const handleUnabsorb = async () => {
|
|
4802
5110
|
ensureArgumentCount({
|
|
4803
5111
|
command,
|
|
4804
5112
|
args: commandArgs,
|
|
@@ -4930,8 +5238,8 @@ const createCli = (options = {}) => {
|
|
|
4930
5238
|
}
|
|
4931
5239
|
stdout(result.path);
|
|
4932
5240
|
return EXIT_CODE.OK;
|
|
4933
|
-
}
|
|
4934
|
-
|
|
5241
|
+
};
|
|
5242
|
+
const handleUse = async () => {
|
|
4935
5243
|
ensureArgumentCount({
|
|
4936
5244
|
command,
|
|
4937
5245
|
args: commandArgs,
|
|
@@ -5024,8 +5332,17 @@ const createCli = (options = {}) => {
|
|
|
5024
5332
|
}
|
|
5025
5333
|
stdout(result.path);
|
|
5026
5334
|
return EXIT_CODE.OK;
|
|
5027
|
-
}
|
|
5028
|
-
|
|
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 () => {
|
|
5029
5346
|
ensureArgumentCount({
|
|
5030
5347
|
command,
|
|
5031
5348
|
args: commandArgs,
|
|
@@ -5080,8 +5397,8 @@ const createCli = (options = {}) => {
|
|
|
5080
5397
|
return EXIT_CODE.CHILD_PROCESS_FAILED;
|
|
5081
5398
|
}
|
|
5082
5399
|
return childExitCode === 0 ? EXIT_CODE.OK : EXIT_CODE.CHILD_PROCESS_FAILED;
|
|
5083
|
-
}
|
|
5084
|
-
|
|
5400
|
+
};
|
|
5401
|
+
const handleInvoke = async () => {
|
|
5085
5402
|
ensureArgumentCount({
|
|
5086
5403
|
command,
|
|
5087
5404
|
args: commandArgs,
|
|
@@ -5105,21 +5422,18 @@ const createCli = (options = {}) => {
|
|
|
5105
5422
|
stderr
|
|
5106
5423
|
})
|
|
5107
5424
|
});
|
|
5108
|
-
if (runtime.json) {
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
})));
|
|
5118
|
-
return EXIT_CODE.OK;
|
|
5119
|
-
}
|
|
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
|
+
})));
|
|
5120
5434
|
return EXIT_CODE.OK;
|
|
5121
|
-
}
|
|
5122
|
-
|
|
5435
|
+
};
|
|
5436
|
+
const handleCopy = async () => {
|
|
5123
5437
|
ensureArgumentCount({
|
|
5124
5438
|
command,
|
|
5125
5439
|
args: commandArgs,
|
|
@@ -5145,21 +5459,18 @@ const createCli = (options = {}) => {
|
|
|
5145
5459
|
dereference: false
|
|
5146
5460
|
});
|
|
5147
5461
|
}
|
|
5148
|
-
if (runtime.json) {
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
})));
|
|
5158
|
-
return EXIT_CODE.OK;
|
|
5159
|
-
}
|
|
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
|
+
})));
|
|
5160
5471
|
return EXIT_CODE.OK;
|
|
5161
|
-
}
|
|
5162
|
-
|
|
5472
|
+
};
|
|
5473
|
+
const handleLink = async () => {
|
|
5163
5474
|
ensureArgumentCount({
|
|
5164
5475
|
command,
|
|
5165
5476
|
args: commandArgs,
|
|
@@ -5205,22 +5516,19 @@ const createCli = (options = {}) => {
|
|
|
5205
5516
|
});
|
|
5206
5517
|
}
|
|
5207
5518
|
}
|
|
5208
|
-
if (runtime.json) {
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
})));
|
|
5219
|
-
return EXIT_CODE.OK;
|
|
5220
|
-
}
|
|
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
|
+
})));
|
|
5221
5529
|
return EXIT_CODE.OK;
|
|
5222
|
-
}
|
|
5223
|
-
|
|
5530
|
+
};
|
|
5531
|
+
const handleLock = async () => {
|
|
5224
5532
|
ensureArgumentCount({
|
|
5225
5533
|
command,
|
|
5226
5534
|
args: commandArgs,
|
|
@@ -5262,25 +5570,22 @@ const createCli = (options = {}) => {
|
|
|
5262
5570
|
owner
|
|
5263
5571
|
});
|
|
5264
5572
|
});
|
|
5265
|
-
if (runtime.json) {
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
owner: result.owner
|
|
5276
|
-
}
|
|
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
|
|
5277
5583
|
}
|
|
5278
|
-
}
|
|
5279
|
-
|
|
5280
|
-
}
|
|
5584
|
+
}
|
|
5585
|
+
})));
|
|
5281
5586
|
return EXIT_CODE.OK;
|
|
5282
|
-
}
|
|
5283
|
-
|
|
5587
|
+
};
|
|
5588
|
+
const handleUnlock = async () => {
|
|
5284
5589
|
ensureArgumentCount({
|
|
5285
5590
|
command,
|
|
5286
5591
|
args: commandArgs,
|
|
@@ -5325,24 +5630,21 @@ const createCli = (options = {}) => {
|
|
|
5325
5630
|
branch
|
|
5326
5631
|
});
|
|
5327
5632
|
});
|
|
5328
|
-
if (runtime.json) {
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
reason: null
|
|
5338
|
-
}
|
|
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
|
|
5339
5642
|
}
|
|
5340
|
-
}
|
|
5341
|
-
|
|
5342
|
-
}
|
|
5643
|
+
}
|
|
5644
|
+
})));
|
|
5343
5645
|
return EXIT_CODE.OK;
|
|
5344
|
-
}
|
|
5345
|
-
|
|
5646
|
+
};
|
|
5647
|
+
const handleCd = async () => {
|
|
5346
5648
|
ensureArgumentCount({
|
|
5347
5649
|
command,
|
|
5348
5650
|
args: commandArgs,
|
|
@@ -5389,8 +5691,7 @@ const createCli = (options = {}) => {
|
|
|
5389
5691
|
cwd: repoRoot,
|
|
5390
5692
|
isInteractive: () => runtime.isInteractive || process.stderr.isTTY === true
|
|
5391
5693
|
}).catch((error) => {
|
|
5392
|
-
|
|
5393
|
-
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}` });
|
|
5394
5695
|
throw error;
|
|
5395
5696
|
});
|
|
5396
5697
|
if (selection.status === "cancelled") return EXIT_CODE_CANCELLED;
|
|
@@ -5406,7 +5707,20 @@ const createCli = (options = {}) => {
|
|
|
5406
5707
|
}
|
|
5407
5708
|
stdout(selectedPath);
|
|
5408
5709
|
return EXIT_CODE.OK;
|
|
5409
|
-
}
|
|
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;
|
|
5410
5724
|
throw createCliError("UNKNOWN_COMMAND", { message: `Unknown command: ${command}` });
|
|
5411
5725
|
} catch (error) {
|
|
5412
5726
|
const cliError = ensureCliError(error);
|